/** @jsxImportSource solid-js */

import {
  For,
  Show,
  createSignal,
  createMemo,
  createEffect,
  on,
  onMount,
  onCleanup,
  type Component,
  type JSX,
} from "solid-js"
import type {
  ExtensionMessage,
  AgentManagerRepoInfoMessage,
  AgentManagerWorktreeSetupMessage,
  AgentManagerStateMessage,
  AgentManagerKeybindingsMessage,
  AgentManagerMultiVersionProgressMessage,
  AgentManagerSendInitialMessage,
  AgentManagerBranchesMessage,
  AgentManagerWorktreeDiffMessage,
  AgentManagerWorktreeDiffFileMessage,
  AgentManagerWorktreeDiffLoadingMessage,
  AgentManagerApplyWorktreeDiffResultMessage,
  AgentManagerApplyWorktreeDiffStatus,
  AgentManagerApplyWorktreeDiffConflict,
  AgentManagerWorktreeStatsMessage,
  AgentManagerLocalStatsMessage,
  WorktreeFileDiff,
  WorktreeGitStats,
  LocalGitStats,
  WorktreeState,
  RunStatus,
  PRStatus,
  AgentManagerPRStatusMessage,
  ManagedSessionState,
  SectionState,
  SessionInfo,
  SessionCreatedMessage,
  BranchInfo,
} from "../src/types/messages"
import { IndexingProvider } from "../src/context/indexing"
import {
  DragDropProvider,
  DragDropSensors,
  DragOverlay,
  SortableProvider,
  closestCenter,
  createSortable,
} from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import { ThemeProvider } from "@kilocode/kilo-ui/theme"
import { DialogProvider, useDialog } from "@kilocode/kilo-ui/context/dialog"
import { Dialog } from "@kilocode/kilo-ui/dialog"
import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu"
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
import { MarkedProvider } from "@kilocode/kilo-ui/context/marked"
import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code"
import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff"
import { FileComponentProvider } from "@kilocode/kilo-ui/context/file"
import { Code } from "@kilocode/kilo-ui/code"
import { Diff } from "@kilocode/kilo-ui/diff"
import { File } from "@kilocode/kilo-ui/file"
import { Toast, showToast } from "@kilocode/kilo-ui/toast"
import { ResizeHandle } from "@kilocode/kilo-ui/resize-handle"
import { Icon } from "@kilocode/kilo-ui/icon"
import { Button } from "@kilocode/kilo-ui/button"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
import { Popover } from "@kilocode/kilo-ui/popover"
import { VSCodeProvider, useVSCode } from "../src/context/vscode"
import { ServerProvider } from "../src/context/server"
import { ProviderProvider } from "../src/context/provider"
import { ConfigProvider } from "../src/context/config"
import { DisplayProvider } from "../src/context/display"
import { KiloEmbeddingModelsProvider } from "../src/context/kilo-embedding-models"
import { NotificationsProvider } from "../src/context/notifications"
import { FeedbackProvider } from "../src/context/feedback"
import { SessionProvider, useSession } from "../src/context/session"
import { WorktreeModeProvider } from "../src/context/worktree-mode"
import { ChatView } from "../src/components/chat"
import HistoryView from "../src/components/history/HistoryView"
import { NewWorktreeDialog } from "./NewWorktreeDialog"
import { DataBridge, MermaidDownloadBridge } from "../src/App"
import { LanguageBridge } from "../src/context/language-bridge"
import { useLanguage } from "../src/context/language"
import { formatRelativeDate } from "../src/utils/date"
import {
  nextSelectionAfterDelete,
  adjacentHint,
  restoreLocalSessions,
  reconcileLocalSessions,
  filterUnassignedSessions,
  LOCAL,
} from "./navigate"
import { reorderTabs, applyTabOrder, firstOrderedTitle } from "./tab-order"
import { createTabOrderSync } from "./tab-order-sync"
import { reportRemoteSessions } from "./remote-sessions"
import { ConstrainDragYAxis } from "./sortable-tab"
import { isTerminalTabId, createTerminalState, createTerminalHandlers, createTerminalMessageHandler } from "./terminal"
import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering"
import { useTabScroll } from "./tab-scroll"
import { DiffPanel } from "./DiffPanel"
import { createRevertFile } from "./revert-file"
import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView"
import { ApplyDialog } from "./ApplyDialog"
import { groupApplyConflicts } from "./apply-conflicts"
import type { ReviewComment } from "../diff-viewer/review-comments"
import { clearReviewComposer, createReviewComposer } from "../diff-viewer/review-annotations"
import type { SidebarSearchMenuRef } from "./SidebarSearchMenu"
import { createSidebarSearch, type SidebarSearchItem } from "./sidebar-search"
import { WorktreeSectionActions } from "./WorktreeSectionActions"
import { BranchSelect } from "../src/components/shared/BranchSelect"
import { WorktreeItem } from "./WorktreeItem"
import SectionHeader from "./SectionHeader"
import { randomColor } from "./section-colors"
import { createNewTaskDrafts } from "./new-task-drafts"
import {
  buildTopLevelItems,
  buildSidebarOrder,
  buildShortcutMap,
  isGrouped,
  isGroupStart,
  isGroupEnd,
  type TopLevelItem,
} from "./section-helpers"
import { sectionAwareDetector } from "./section-dnd"
import { ConstrainDragXAxis } from "./constrain-drag-x"
import { mergeWorktreeDiffs } from "../diff-viewer/diff-state"
import { initialMessage, seedInitialVariant } from "./initial-message"
import { createMarkdownRender } from "./review-preferences"
import { createSidebarCollapse } from "./sidebar-collapse"
import { SidebarToggleButton } from "./SidebarToggleButton"
import { setTabWidths } from "./tab-widths"
import { buildShortcutCategories } from "./shortcuts"
import { tracker } from "./telemetry"
import "./agent-manager.css"
import "./agent-manager-review.css"
const REVIEW_TAB_ID = "review"

interface SetupState {
  active: boolean
  message: string
  branch?: string
  error?: boolean
  worktreeId?: string
  errorCode?: string
}

interface WorktreeBusyState {
  reason: "setting-up" | "deleting"
  message?: string
  branch?: string
}

interface ApplyState {
  status: AgentManagerApplyWorktreeDiffStatus
  message: string
  conflicts: AgentManagerApplyWorktreeDiffConflict[]
}
/** Sidebar selection: LOCAL for local repo, worktree ID for a worktree, or null for an unassigned session. */
type SidebarSelection = typeof LOCAL | string | null
type SidePanel = "diff" | "pr" | null
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
// Fallback keybindings before extension sends resolved ones
const MAX_JUMP_INDEX = 9

const defaultBindings: Record<string, string> = {
  previousSession: isMac ? "⌘⌥↑" : "Ctrl+Alt+↑",
  nextSession: isMac ? "⌘⌥↓" : "Ctrl+Alt+↓",
  previousTab: isMac ? "⌘⌥←" : "Ctrl+Alt+←",
  nextTab: isMac ? "⌘⌥→" : "Ctrl+Alt+→",
  search: isMac ? "⌘F" : "Ctrl+F",
  showTerminal: isMac ? "⌘/" : "Ctrl+/",
  newTerminal: isMac ? "⌘⇧T" : "Ctrl+Shift+T",
  runScript: isMac ? "⌘E" : "Ctrl+E",
  toggleDiff: isMac ? "⌘D" : "Ctrl+D",
  showShortcuts: isMac ? "⌘⇧/" : "Ctrl+Shift+/",
  newTab: isMac ? "⌘T" : "Ctrl+T",
  closeTab: isMac ? "⌘W" : "Ctrl+W",
  newWorktree: isMac ? "⌘N" : "Ctrl+N",
  advancedWorktree: isMac ? "⌘⇧N" : "Ctrl+Shift+N",
  closeWorktree: isMac ? "⌘⇧W" : "Ctrl+Shift+W",
  openWorktree: isMac ? "⌘⇧O" : "Ctrl+Shift+O",
  openPR: isMac ? "⌘⇧R" : "Ctrl+Shift+R",
  agentManagerOpen: isMac ? "⌘⇧M" : "Ctrl+Shift+M",
  cycleAgentMode: isMac ? "⌘." : "Ctrl+.",
  cyclePreviousAgentMode: isMac ? "⌘⇧." : "Ctrl+Shift+.",
  ...Object.fromEntries(
    Array.from({ length: MAX_JUMP_INDEX }, (_, i) => [`jumpTo${i + 1}`, isMac ? `⌘${i + 1}` : `Ctrl+${i + 1}`]),
  ),
}

import { parseBindingTokens } from "./keybind-tokens"

const AgentManagerContent: Component = () => {
  const { t } = useLanguage()
  const session = useSession()
  const vscode = useVSCode()
  const dialog = useDialog()
  let sidebarSearchMenu: SidebarSearchMenuRef | undefined

  const [kb, setKb] = createSignal<Record<string, string>>(defaultBindings)

  const [setup, setSetup] = createSignal<SetupState>({ active: false, message: "" })
  const [worktrees, setWorktrees] = createSignal<WorktreeState[]>([])
  const [managedSessions, setManagedSessions] = createSignal<ManagedSessionState[]>([])
  const [selection, setSelection] = createSignal<SidebarSelection>(LOCAL)
  const metrics = tracker(vscode)
  const [repoBranch, setRepoBranch] = createSignal<string | undefined>()
  const [busyWorktrees, setBusyWorktrees] = createSignal<Map<string, WorktreeBusyState>>(new Map())
  const [staleWorktreeIds, setStaleWorktreeIds] = createSignal<Set<string>>(new Set())
  const [worktreesLoaded, setWorktreesLoaded] = createSignal(false)
  const [sessionsLoaded, setSessionsLoaded] = createSignal(false)
  const [isGitRepo, setIsGitRepo] = createSignal(true)
  const [repoDetectedBranch, setRepoDetectedBranch] = createSignal<string | undefined>()
  const [defaultBaseBranch, setDefaultBaseBranch] = createSignal<string | undefined>()

  const repoDefaultBranch = () => defaultBaseBranch() ?? repoDetectedBranch() ?? "main"
  const hasConfiguredBranch = () => !!defaultBaseBranch()

  const DEFAULT_SIDEBAR_WIDTH = 260
  const MIN_SIDEBAR_WIDTH = 200
  const MAX_SIDEBAR_WIDTH_RATIO = 0.4

  // Recover persisted local session IDs from webview state
  const persisted = vscode.getState<{ localSessionIDs?: string[]; sidebarWidth?: number }>()
  const [localSessionIDs, setLocalSessionIDs] = createSignal<string[]>(persisted?.localSessionIDs ?? [])
  /** Remove a session ID from the local tab (no-op if absent). */
  const evictLocal = (sid: string) =>
    setLocalSessionIDs((prev) => (prev.includes(sid) ? prev.filter((id) => id !== sid) : prev))
  const [sidebarWidth, setSidebarWidth] = createSignal(persisted?.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH)
  const [sessionsCollapsed, setSessionsCollapsed] = createSignal(false)
  const sidebar = createSidebarCollapse(vscode)
  const sidebarCollapsed = sidebar.collapsed
  const expandSidebar = sidebar.expand
  const toggleSidebar = sidebar.toggle
  const [sections, setSections] = createSignal<SectionState[]>([])

  // rAF coalescing for resize handlers — at most one signal write per frame
  let sidebarRaf: number | undefined
  let pendingSidebarWidth: number | undefined
  let diffRaf: number | undefined
  let pendingDiffWidth: number | undefined

  const [history, setHistory] = createSignal(false)
  const [sidePanel, setSidePanel] = createSignal<SidePanel>(null)
  const diffOpen = () => sidePanel() === "diff"
  const [diffDatas, setDiffDatas] = createSignal<Record<string, WorktreeFileDiff[]>>({})
  const [diffLoading, setDiffLoading] = createSignal(false)
  const [diffFileLoading, setDiffFileLoading] = createSignal<Record<string, Record<string, true>>>({})
  const [diffWidth, setDiffWidth] = createSignal(Math.round(window.innerWidth * 0.5))

  const [reviewOpenByContext, setReviewOpenByContext] = createSignal<Record<string, boolean>>({})
  const [reviewCommentsByContext, setReviewCommentsByContext] = createSignal<Record<string, ReviewComment[]>>({})
  const reviewComposer = createReviewComposer()
  const [reviewActive, setReviewActive] = createSignal(false)
  const [reviewDiffStyle, setReviewDiffStyle] = createSignal<"unified" | "split">("unified")
  const markdown = createMarkdownRender(vscode)
  // Per-worktree git stats (diff additions/deletions, commits missing from origin)
  const [worktreeStats, setWorktreeStats] = createSignal<Record<string, WorktreeGitStats>>({})

  // Per-worktree PR status data
  const [prStatuses, setPrStatuses] = createSignal<Record<string, PRStatus | null>>({})

  const [runStatuses, setRunStatuses] = createSignal<Record<string, RunStatus>>({})
  const [runScriptConfigured, setRunScriptConfigured] = createSignal(false)

  // Local repo git stats (branch name, diff additions/deletions, commits)
  const [localStats, setLocalStats] = createSignal<LocalGitStats | undefined>()

  // Per-worktree apply-to-local status
  const [applyStates, setApplyStates] = createSignal<Record<string, ApplyState>>({})
  const [applyTarget, setApplyTarget] = createSignal<string | undefined>()
  const [applySelectedFiles, setApplySelectedFiles] = createSignal<string[]>([])
  const [applySelectionTouched, setApplySelectionTouched] = createSignal(false)

  // Pending local tab counter for generating unique IDs
  let pendingCounter = 0
  const PENDING_PREFIX = "pending:"
  const [activePendingId, setActivePendingId] = createSignal<string | undefined>()

  // Per-sidebar-context terminal state. `terms.activeId` holds the id
  // of the focused terminal tab, if any — takes precedence over
  // session/pending/review when deriving the visible tab.
  const terms = createTerminalState(selection)

  // Inline delete confirmation: tracks which worktree is awaiting a second click/press
  const [pendingDelete, setPendingDelete] = createSignal<string | null>(null)
  let pendingDeleteTimer: ReturnType<typeof setTimeout> | undefined
  const cancelPendingDelete = () => {
    clearTimeout(pendingDeleteTimer)
    setPendingDelete(null)
  }
  createEffect(on(selection, () => cancelPendingDelete(), { defer: true }))
  createEffect(on(selection, () => clearReviewComposer(reviewComposer), { defer: true }))
  onCleanup(() => clearTimeout(pendingDeleteTimer))

  // Per-context tab memory: maps sidebar selection key -> last active session/pending ID
  const [tabMemory, setTabMemory] = createSignal<Record<string, string>>({})

  const reviewOpen = createMemo(() => {
    const sel = selection()
    if (sel === null) return false
    return reviewOpenByContext()[sel] === true
  })

  const setReviewOpenForContext = (context: string, open: boolean) => {
    setReviewOpenByContext((prev) => {
      if (prev[context] === open) return prev
      return { ...prev, [context]: open }
    })
  }

  const setReviewOpenForSelection = (open: boolean) => {
    const sel = selection()
    if (sel === null) return
    setReviewOpenForContext(sel, open)
  }

  const reviewComments = createMemo(() => {
    const sel = selection()
    if (sel === null) return [] as ReviewComment[]
    return reviewCommentsByContext()[sel] ?? []
  })

  const setReviewCommentsForSelection = (comments: ReviewComment[]) => {
    const sel = selection()
    if (sel === null) return
    setReviewCommentsByContext((prev) => ({ ...prev, [sel]: comments }))
  }

  const applyStateForSelection = createMemo(() => {
    const sel = selection()
    if (!sel || sel === LOCAL) return undefined
    return applyStates()[sel]
  })

  const resolveWorktreeSessionId = (worktreeId: string) => {
    const id = session.currentSessionID()
    if (id) {
      const current = managedSessions().find((entry) => entry.id === id)
      if (current?.worktreeId === worktreeId) return id
    }
    return managedSessions().find((entry) => entry.worktreeId === worktreeId)?.id
  }

  const applyTargetSessionId = createMemo(() => {
    const target = applyTarget()
    if (!target) return undefined
    return resolveWorktreeSessionId(target)
  })

  const applyDiffs = createMemo(() => {
    const target = applyTarget()
    if (!target) return [] as WorktreeFileDiff[]
    const data = diffDatas()
    const current = applyTargetSessionId()
    if (current && data[current]) return data[current]!
    const ids = managedSessions()
      .filter((entry) => entry.worktreeId === target)
      .map((entry) => entry.id)
    for (const id of ids) {
      if (data[id]) return data[id]!
    }
    return [] as WorktreeFileDiff[]
  })

  const applyStateForTarget = createMemo(() => {
    const target = applyTarget()
    if (!target) return undefined
    return applyStates()[target]
  })

  const applyBusyForTarget = createMemo(() => {
    const state = applyStateForTarget()
    if (!state) return false
    return state.status === "checking" || state.status === "applying"
  })

  const applySelectedSet = createMemo(() => new Set(applySelectedFiles()))

  const applySelectionStats = createMemo(() => {
    const set = applySelectedSet()
    const selected = applyDiffs().filter((diff) => set.has(diff.file))
    const additions = selected.reduce((sum, diff) => sum + diff.additions, 0)
    const deletions = selected.reduce((sum, diff) => sum + diff.deletions, 0)
    return {
      total: applyDiffs().length,
      selected: selected.length,
      additions,
      deletions,
    }
  })

  const applyHasSelection = createMemo(() => applySelectionStats().selected > 0)

  const applyConflictRows = createMemo(() => groupApplyConflicts(applyStateForTarget()?.conflicts ?? []))

  const applyToLocal = (worktreeId: string, selectedFiles: string[]) => {
    setApplyStates((prev) => ({
      ...prev,
      [worktreeId]: {
        status: "checking",
        message: t("agentManager.apply.checking"),
        conflicts: [],
      },
    }))
    vscode.postMessage({ type: "agentManager.applyWorktreeDiff", worktreeId, selectedFiles })
  }

  const resetApplyDialog = () => {
    setApplyTarget(undefined)
    setApplySelectedFiles([])
    setApplySelectionTouched(false)
  }

  const closeApplyDialog = () => {
    resetApplyDialog()
    dialog.close()
  }

  const applySelectAll = () => {
    setApplySelectionTouched(true)
    setApplySelectedFiles(applyDiffs().map((diff) => diff.file))
  }

  const applySelectNone = () => {
    setApplySelectionTouched(true)
    setApplySelectedFiles([])
  }

  const applyToggleFile = (file: string, checked: boolean) => {
    setApplySelectionTouched(true)
    setApplySelectedFiles((prev) => {
      if (checked) {
        if (prev.includes(file)) return prev
        const set = new Set(prev)
        set.add(file)
        return applyDiffs()
          .map((diff) => diff.file)
          .filter((path) => set.has(path))
      }
      if (!prev.includes(file)) return prev
      return prev.filter((path) => path !== file)
    })
  }

  const triggerApply = () => {
    const target = applyTarget()
    if (!target) return
    if (!applyHasSelection()) return
    if (applyBusyForTarget()) return
    metrics.track("apply_to_local", "apply_dialog", { fileCount: applySelectedFiles().length })
    applyToLocal(target, applySelectedFiles())
  }

  const openApplyDialog = () => {
    const sel = selection()
    if (!sel || sel === LOCAL) return
    setApplyStates((prev) => {
      if (!prev[sel]) return prev
      const next = { ...prev }
      delete next[sel]
      return next
    })
    setApplyTarget(sel)
    setApplySelectionTouched(false)
    setApplySelectedFiles([])
    const sid = resolveWorktreeSessionId(sel)
    if (sid) vscode.postMessage({ type: "agentManager.requestWorktreeDiff", sessionId: sid })

    setApplySelectedFiles(applyDiffs().map((diff) => diff.file))

    dialog.show(
      () => (
        <ApplyDialog
          diffs={applyDiffs()}
          loading={diffLoading()}
          selectedFiles={applySelectedSet()}
          selectedCount={applySelectionStats().selected}
          additions={applySelectionStats().additions}
          deletions={applySelectionStats().deletions}
          busy={applyBusyForTarget()}
          hasSelection={applyHasSelection()}
          status={applyStateForTarget()?.status}
          message={applyStateForTarget()?.message}
          conflictRows={applyConflictRows()}
          onSelectAll={applySelectAll}
          onSelectNone={applySelectNone}
          onToggleFile={applyToggleFile}
          onApply={triggerApply}
          onClose={closeApplyDialog}
        />
      ),
      resetApplyDialog,
    )
  }

  const openWorktreeDirectory = () => {
    const sel = selection()
    if (!sel || sel === LOCAL) return
    vscode.postMessage({ type: "agentManager.openWorktree", worktreeId: sel })
  }
  const openWindow = metrics.click("open_worktree_window", "tab_toolbar", openWorktreeDirectory)

  const openSelectedPR = () => {
    const sel = selection()
    if (!sel || sel === LOCAL || !prStatuses()[sel]) return
    metrics.track("open_pull_request", "keyboard_shortcut")
    vscode.postMessage({ type: "agentManager.openPR", worktreeId: sel })
  }

  const runWorktree = (id: string) => {
    const state = runStatuses()[id]?.state ?? "idle"
    if (state === "running" || state === "stopping") {
      vscode.postMessage({ type: "agentManager.stopRunScript", worktreeId: id })
      return
    }
    vscode.postMessage({ type: "agentManager.runScript", worktreeId: id })
  }

  const configureRunScript = () => vscode.postMessage({ type: "agentManager.configureRunScript" })

  const runSelected = () => {
    const sel = selection()
    if (sel) runWorktree(sel)
  }

  createEffect(
    on(
      () => [applyTarget(), applyDiffs(), applySelectionTouched()] as const,
      ([target, diffs, touched]) => {
        if (!target) return
        const files = diffs.map((diff) => diff.file)
        if (files.length === 0) {
          if (!touched) setApplySelectedFiles([])
          return
        }

        if (!touched) {
          setApplySelectedFiles(files)
          return
        }

        const current = applySelectedFiles()
        const set = new Set(current)
        const next = files.filter((file) => set.has(file))
        const same = next.length === current.length && next.every((file, index) => file === current[index])
        if (!same) setApplySelectedFiles(next)
      },
    ),
  )

  const isPending = (id: string) => id.startsWith(PENDING_PREFIX)
  reportRemoteSessions(vscode, localSessionIDs, managedSessions, isPending)

  // Drag-and-drop state for tab reordering
  const [draggingTab, setDraggingTab] = createSignal<string | undefined>()

  const freezeTabs = () => {
    const bar = document.querySelector(".am-tab-bar")
    if (bar instanceof HTMLElement && bar.matches(":hover")) setTabWidths(true)
  }

  const releaseTabs = () => setTabWidths(false)
  // Tab ordering: context key → ordered session ID array (recovered from extension state)
  const [worktreeTabOrder, setWorktreeTabOrder] = createSignal<Record<string, string[]>>({})
  // Sidebar worktree order (persisted to extension state)
  const [sidebarWorktreeOrder, setSidebarWorktreeOrder] = createSignal<string[]>([])
  const [draggingWorktree, setDraggingWorktree] = createSignal<string | undefined>()
  const [renamingSection, setRenamingSection] = createSignal<string | null>(null)
  let pendingNewSection = false

  // Pin new tabs at the tail (see tab-order-sync); strip ephemeral ids so agent-manager.json stays clean.
  const persistTabOrder = (key: string, order: string[]) => {
    const durable = order.filter((id) => id !== REVIEW_TAB_ID && !isTerminalTabId(id))
    vscode.postMessage({ type: "agentManager.setTabOrder", key, order: durable })
  }
  const tabOrderSync = createTabOrderSync({
    LOCAL,
    REVIEW_TAB_ID,
    order: worktreeTabOrder,
    setOrder: setWorktreeTabOrder,
    persist: persistTabOrder,
    localSessionIDs,
    sessions: session.sessions,
    managedSessions,
    reviewOpenByContext,
    terminalIdsFor: (key) => terms.forSelection(key).map((t) => t.id),
  })
  const appendToTabOrder = tabOrderSync.append

  const addPendingTab = () => {
    const id = `${PENDING_PREFIX}${++pendingCounter}`
    setLocalSessionIDs((prev) => [...prev, id])
    appendToTabOrder(LOCAL, id)
    // Deactivate any focused terminal so the new pending session is
    // actually visible — visibleTabId prioritises terms.activeId().
    terms.setActiveId(undefined)
    setActivePendingId(id)
    session.clearCurrentSession()
    return id
  }

  // Persist local session IDs and sidebar width to webview state for recovery (exclude pending tabs).
  // Debounced to avoid serializing state on every pixel during resize drag.
  let persistTimer: ReturnType<typeof setTimeout> | undefined
  createEffect(() => {
    // Read signals eagerly so Solid tracks them as dependencies
    const ids = localSessionIDs().filter((id) => !isPending(id))
    const width = sidebarWidth()
    clearTimeout(persistTimer)
    persistTimer = setTimeout(() => {
      const prev = vscode.getState<Record<string, unknown>>() ?? {}
      vscode.setState({ ...prev, localSessionIDs: ids, sidebarWidth: width })
    }, 300)
  })
  onCleanup(() => clearTimeout(persistTimer))

  // Save the currently active tab for the current sidebar context before switching away
  const saveTabMemory = () => {
    const sel = selection()
    if (sel === null) return
    const key = sel === LOCAL ? LOCAL : sel
    const active = visibleTabId()
    if (active) {
      setTabMemory((prev) => (prev[key] === active ? prev : { ...prev, [key]: active }))
    }
  }

  // Invalidate local session IDs if they no longer exist (preserve pending tabs)
  createEffect(() => {
    if (!worktreesLoaded()) return
    const all = session.sessions()
    if (all.length === 0) return // sessions not loaded yet
    const next = reconcileLocalSessions(
      localSessionIDs(),
      all.map((s) => s.id),
      managedSessions(),
      isPending,
    )
    if (!next) return
    for (const id of next.forget) vscode.postMessage({ type: "agentManager.forgetSession", sessionId: id })
    setLocalSessionIDs(next.ids)
  })
  // Drop in-memory review state for worktrees that no longer exist.
  createEffect(() => {
    const ids = new Set(worktrees().map((wt) => wt.id))
    setReviewOpenByContext((prev) => {
      const next = Object.fromEntries(Object.entries(prev).filter(([id]) => id === LOCAL || ids.has(id)))
      if (Object.keys(next).length === Object.keys(prev).length) return prev
      return next
    })
    setReviewCommentsByContext((prev) => {
      const next = Object.fromEntries(Object.entries(prev).filter(([id]) => id === LOCAL || ids.has(id)))
      if (Object.keys(next).length === Object.keys(prev).length) return prev
      return next
    })
    setApplyStates((prev) => {
      const next = Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id)))
      if (Object.keys(next).length === Object.keys(prev).length) return prev
      return next
    })

    const target = applyTarget()
    if (target && !ids.has(target)) closeApplyDialog()
  })

  const worktreeSessionIds = createMemo(
    () =>
      new Set(
        managedSessions()
          .filter((ms) => ms.worktreeId)
          .map((ms) => ms.id),
      ),
  )

  const localSet = createMemo(() => new Set(localSessionIDs()))

  // Sessions NOT in any worktree and not local
  const unassignedSessions = createMemo(() =>
    filterUnassignedSessions(session.sessions(), worktreeSessionIds(), localSet()),
  )

  // Local sessions (resolved from session list + pending tabs, in insertion order)
  const localSessions = createMemo((): SessionInfo[] => {
    const ids = localSessionIDs()
    const all = session.sessions()
    const lookup = new Map(all.map((s) => [s.id, s]))
    const result: SessionInfo[] = []
    const now = new Date().toISOString()
    for (const id of ids) {
      const real = lookup.get(id)
      if (real) {
        result.push(real)
      } else if (isPending(id)) {
        result.push({ id, title: t("agentManager.session.newSession"), createdAt: now, updatedAt: now })
      }
    }
    return result
  })

  // Oldest-first sort before applyTabOrder — worktree label and tab bar must agree on "first session".
  const sessionsForWorktree = (worktreeId: string): SessionInfo[] => {
    const ids = new Set(
      managedSessions()
        .filter((ms) => ms.worktreeId === worktreeId)
        .map((ms) => ms.id),
    )
    return applyTabOrder(
      session
        .sessions()
        .filter((s) => ids.has(s.id))
        .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()),
      worktreeTabOrder()[worktreeId],
    )
  }

  const activeWorktreeSessions = createMemo((): SessionInfo[] => {
    const sel = selection()
    if (!sel || sel === LOCAL) return []
    return sessionsForWorktree(sel)
  })

  const activeTabs = createMemo((): SessionInfo[] => {
    const sel = selection()
    if (sel === LOCAL) return localSessions()
    if (sel) return activeWorktreeSessions()
    return []
  })

  const contextEmpty = createMemo(() => {
    const sel = selection()
    if (terms.current().length > 0) return false
    if (sel === LOCAL) return localSessionIDs().length === 0
    if (sel) return activeWorktreeSessions().length === 0 && managedSessions().every((ms) => ms.worktreeId !== sel)
    return false
  })

  createEffect(() => {
    const sel = selection()
    if (sel === null) {
      if (reviewActive()) setReviewActive(false)
      return
    }
    if (reviewActive() && !reviewOpen()) {
      setReviewActive(false)
    }
  })

  createEffect(() => {
    const id = selection() ?? session.currentSessionID()
    if (!id) return
    requestAnimationFrame(() => {
      const el = document.querySelector(`[data-sidebar-id="${id}"]`)
      if (el instanceof HTMLElement) scrollIntoView(el)
    })
  })

  const readOnly = createMemo(() => selection() === null && !!session.currentSessionID())

  const visibleTabId = createMemo(() => {
    const term = terms.activeId()
    if (term) return term
    if (reviewActive()) return REVIEW_TAB_ID
    return session.currentSessionID() ?? activePendingId()
  })
  const tabScroll = useTabScroll(activeTabs, visibleTabId)

  const worktreeLabel = (wt: WorktreeState): string => {
    if (wt.label) return wt.label
    return firstOrderedTitle(sessionsForWorktree(wt.id), worktreeTabOrder()[wt.id], wt.branch)
  }

  const worktreeSubtitle = (wt: WorktreeState): string | undefined => {
    const label = worktreeLabel(wt)
    return label !== wt.branch ? wt.branch : undefined
  }

  const isStaleWorktree = (worktreeId: string): boolean => staleWorktreeIds().has(worktreeId)

  const isAnySessionBusy = (ids: string[]): boolean => {
    if (ids.length === 0) return false
    const statuses = session.allStatusMap()
    const perms = session.permissions()
    const qs = session.questions()
    for (const id of ids) {
      const info = statuses[id]
      if (!info || info.type === "idle") continue
      const blocked = perms.some((p) => p.sessionID === id) || qs.some((q) => q.sessionID === id)
      if (!blocked) return true
    }
    return false
  }

  /** True when an agent session assigned to this worktree is actively working. */
  const isAgentBusy = (worktreeId: string): boolean => {
    const ids = managedSessions()
      .filter((ms) => ms.worktreeId === worktreeId)
      .map((ms) => ms.id)
    return isAnySessionBusy(ids)
  }

  /** True when a local session is actively working. */
  const isLocalBusy = (): boolean => isAnySessionBusy(localSessionIDs())

  const isSessionBusy = (id: string): boolean => isAnySessionBusy([id])

  /** Worktrees sorted so that grouped items are always adjacent, respecting custom order if set. */
  const sortedWorktrees = createMemo(() => {
    const ordered = applyTabOrder(worktrees(), sidebarWorktreeOrder())
    if (ordered.length === 0) return []

    // Collect grouped worktrees by groupId
    const grouped = new Map<string, WorktreeState[]>()
    for (const wt of ordered) {
      if (!wt.groupId) continue
      const list = grouped.get(wt.groupId) ?? []
      list.push(wt)
      grouped.set(wt.groupId, list)
    }

    // Build output: interleave groups at the position of their earliest member
    const result: WorktreeState[] = []
    const placed = new Set<string>()
    for (const wt of ordered) {
      if (placed.has(wt.id)) continue
      if (wt.groupId) {
        if (placed.has(wt.groupId)) continue
        placed.add(wt.groupId)
        const group = grouped.get(wt.groupId) ?? []
        for (const g of group) {
          result.push(g)
          placed.add(g.id)
        }
      } else {
        result.push(wt)
        placed.add(wt.id)
      }
    }
    return result
  })

  const worktreesInSection = (id: string) => sortedWorktrees().filter((wt) => wt.sectionId === id)
  const ungrouped = createMemo(() => sortedWorktrees().filter((wt) => !wt.sectionId))
  const topLevelItems = createMemo((): TopLevelItem[] =>
    buildTopLevelItems(sections(), ungrouped(), sortedWorktrees(), sidebarWorktreeOrder()),
  )

  /** Flat visual order of all visible sidebar items — used for navigation and shortcut assignment. */
  const sidebarOrder = createMemo(() =>
    buildSidebarOrder(topLevelItems(), sortedWorktrees(), sections(), worktreesInSection, unassignedSessions()),
  )
  /** Map from sidebar item id → 1-based shortcut number (⌘1 for LOCAL, ⌘2 for first worktree, etc.) */
  const shortcutMap = createMemo(() => buildShortcutMap(sidebarOrder()))

  const moveToSection = (ids: string[], sec: string | null) =>
    vscode.postMessage({ type: "agentManager.moveToSection", worktreeIds: ids, sectionId: sec })
  const moveSection = (sectionId: string, dir: -1 | 1) =>
    vscode.postMessage({ type: "agentManager.moveSection", sectionId, dir })
  const newSection = (ids?: string[]) => {
    pendingNewSection = true
    vscode.postMessage({
      type: "agentManager.createSection",
      name: t("agentManager.section.defaultName"),
      color: randomColor(),
      worktreeIds: ids,
    })
  }

  const scrollIntoView = (el: HTMLElement) => el.scrollIntoView({ block: "nearest", behavior: "smooth" })

  const focusSidebarItem = (item: { type: string; id: string }) => {
    if (item.type === "local") selectLocal()
    else if (item.type === "wt") selectWorktree(item.id)
    else {
      saveTabMemory()
      setSelection(null)
      setReviewActive(false)
      session.selectSession(item.id)
    }
    const el = document.querySelector(`[data-sidebar-id="${item.id}"]`)
    if (el instanceof HTMLElement) scrollIntoView(el)
  }

  // Navigate sidebar items with arrow keys (uses visual order from sidebarOrder)
  const navigate = (direction: "up" | "down") => {
    const flat = sidebarOrder()
    if (flat.length === 0) return
    const current = selection() ?? session.currentSessionID()
    const idx = current ? flat.findIndex((f) => f.id === current) : -1
    const next = direction === "up" ? idx - 1 : idx + 1
    if (next < 0 || next >= flat.length) return
    focusSidebarItem(flat[next]!)
  }

  // Jump to sidebar item by 0-based index into sidebarOrder (⌘1 = index 0 = LOCAL, ⌘2 = index 1, etc.)
  const jumpToItem = (index: number) => {
    const item = sidebarOrder()[index]
    if (item) focusSidebarItem(item)
  }

  // Navigate tabs with Cmd+Alt+Left/Right
  const navigateTab = (direction: "left" | "right") => {
    const ids = tabIds()
    if (ids.length === 0) return
    const idx = ids.indexOf(visibleTabId() ?? "")
    if (idx === -1) return
    const next = direction === "left" ? idx - 1 : idx + 1
    if (next < 0 || next >= ids.length) return
    focusTab(ids[next]!)
  }

  const selectLocal = () => {
    saveTabMemory()
    setReviewActive(false)
    setSelection(LOCAL)
    vscode.postMessage({ type: "agentManager.requestRepoInfo" })
    const locals = localSessions()
    const remembered = tabMemory()[LOCAL]
    if (terms.hasRemembered(LOCAL, remembered)) return termHandlers.activate(remembered!)
    terms.setActiveId(undefined)
    const target = remembered ? locals.find((s) => s.id === remembered) : undefined
    const fallback = target ?? locals[0]
    if (fallback && !isPending(fallback.id)) {
      setActivePendingId(undefined)
      session.selectSession(fallback.id)
    } else {
      setActivePendingId(fallback && isPending(fallback.id) ? fallback.id : undefined)
      session.clearCurrentSession()
      vscode.postMessage({ type: "agentManager.showExistingLocalTerminal" })
    }
    setReviewActive(remembered === REVIEW_TAB_ID && reviewOpenByContext()[LOCAL] === true)
  }

  const selectWorktree = (worktreeId: string) => {
    saveTabMemory()
    setSelection(worktreeId)
    const remembered = tabMemory()[worktreeId]
    if (terms.hasRemembered(worktreeId, remembered)) return termHandlers.activate(remembered!)
    terms.setActiveId(undefined)
    // Try rich session list first, fall back to managed session IDs when
    // session.sessions() hasn't been populated yet for this worktree.
    const rich = sessionsForWorktree(worktreeId)
    const managed = managedSessions().filter((ms) => ms.worktreeId === worktreeId)
    const target = remembered
      ? (rich.find((s) => s.id === remembered) ?? managed.find((ms) => ms.id === remembered))
      : undefined
    const fallback = target ?? rich[0] ?? managed[0]
    if (fallback) session.selectSession(fallback.id)
    else session.setCurrentSessionID(undefined)
    setReviewActive(remembered === REVIEW_TAB_ID && reviewOpenByContext()[worktreeId] === true)
  }

  const addSessionToCurrentWorktree = (sid: string) => {
    const sel = selection()
    if (!sel || sel === LOCAL) return false
    const current = managedSessions().find((entry) => entry.id === sid)
    if (current?.worktreeId) return focusManagedSession(current.worktreeId, sid)
    saveTabMemory()
    setHistory(false)
    setReviewActive(false)
    appendToTabOrder(sel, sid)
    evictLocal(sid)
    vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel, sessionId: sid })
    return true
  }

  const focusManagedSession = (worktreeId: string, sid: string) => {
    selectWorktree(worktreeId)
    setHistory(false)
    session.selectSession(sid)
    return true
  }

  const sidebarSearch = createSidebarSearch({
    worktrees: sortedWorktrees,
    sections,
    local: localSessions,
    localBranch: repoBranch,
    selection,
    sessionId: session.currentSessionID,
    statuses: session.allStatusMap,
    permissions: session.permissions,
    questions: session.questions,
    label: worktreeLabel,
    sessions: sessionsForWorktree,
    pending: isPending,
    busy: (id) => busyWorktrees().has(id) || (runStatuses()[id]?.state ?? "idle") !== "idle",
    localBusy: isLocalBusy,
    t,
  })
  const focusSidebarSearchItem = (item: SidebarSearchItem) => {
    if (item.section?.collapsed)
      vscode.postMessage({ type: "agentManager.toggleSectionCollapsed", sectionId: item.section.id })
    setHistory(false)
    if (item.kind === "local") return selectLocal()
    if (item.kind === "worktree") return selectWorktree(item.worktreeId)
    if (item.location === "local") selectLocal()
    if (item.location === "worktree" && item.worktreeId) selectWorktree(item.worktreeId)
    terms.setActiveId(undefined)
    setReviewActive(false)
    setActivePendingId(undefined)
    session.selectSession(item.sessionId)
  }

  const cycleAgent = (direction: 1 | -1) => {
    const available = session.agents().filter((a) => a.mode !== "subagent" && !a.hidden)
    if (available.length <= 1) return
    const current = session.selectedAgent()
    const idx = available.findIndex((a) => a.name === current)
    const raw = idx + direction
    const next = raw < 0 ? available.length - 1 : raw >= available.length ? 0 : raw
    const agent = available[next]
    if (agent) session.selectAgent(agent.name)
  }

  const syncRunStatuses = (items: RunStatus[] = []) => {
    const map: Record<string, RunStatus> = {}
    for (const item of items) map[item.worktreeId] = item
    setRunStatuses(map)
  }

  onMount(() => {
    const handler = (event: MessageEvent) => {
      const msg = event.data
      if (msg?.type === "navigate" && msg.view === "history") return setHistory(true)
      if (msg?.type !== "action") return
      if (msg.action === "sessionPrevious") navigate("up")
      else if (msg.action === "sessionNext") navigate("down")
      else if (msg.action === "tabPrevious") navigateTab("left")
      else if (msg.action === "tabNext") navigateTab("right")
      else if (msg.action === "search") {
        if (!sidebarCollapsed()) sidebarSearchMenu?.open()
        else {
          expandSidebar()
          requestAnimationFrame(() => sidebarSearchMenu?.open())
        }
      } else if (msg.action === "showTerminal") {
        // Cmd+/ opens the legacy VS Code integrated terminal for the
        // active session (or local). The new xterm tab affordance has
        // its own keybind (Cmd+Shift+T) so both coexist.
        const id = session.currentSessionID()
        if (id) vscode.postMessage({ type: "agentManager.showTerminal", sessionId: id })
        else if (selection() === LOCAL) vscode.postMessage({ type: "agentManager.showLocalTerminal" })
      } else if (msg.action === "toggleDiff") {
        if (reviewActive()) {
          closeReviewTab()
          setSidePanel("diff")
        } else setSidePanel((prev) => (prev === "diff" ? null : "diff"))
      } else if (msg.action === "newTab") handleNewTabForCurrentSelection()
      else if (msg.action === "closeTab") closeActiveTab()
      else if (msg.action === "newWorktree") handleNewWorktreeOrPromote()
      else if (msg.action === "openWorktree") openWorktreeDirectory()
      else if (msg.action === "openPR") openSelectedPR()
      else if (msg.action === "runScript") runSelected()
      else if (msg.action === "advancedWorktree") showAdvancedWorktreeDialog()
      else if (msg.action === "closeWorktree") closeSelectedWorktree()
      else if (msg.action === "showShortcuts") handleShowKeyboardShortcuts()
      else if (msg.action === "focusInput") window.dispatchEvent(new Event("focusPrompt"))
      else if (msg.action === "newTerminal") termHandlers.requestNew()
      else if (msg.action === "cycleAgentMode" && document.hasFocus()) cycleAgent(1)
      else if (msg.action === "cyclePreviousAgentMode" && document.hasFocus()) cycleAgent(-1)
      else {
        // Handle jumpTo1 through jumpTo9
        const match = /^jumpTo([1-9])$/.exec(msg.action ?? "")
        if (match) jumpToItem(parseInt(match[1]!) - 1)
      }
    }
    window.addEventListener("message", handler)

    // Prevent Cmd/Ctrl shortcuts from triggering native browser actions
    const preventDefaults = (e: KeyboardEvent) => {
      if (!(e.metaKey || e.ctrlKey)) return
      const target = e.target as HTMLElement | null
      if (target?.closest("[data-agent-manager-native-text-shortcuts]")) return
      // Arrow navigation requires Alt modifier (Cmd+Alt+Arrow for tabs/sessions)
      if (e.altKey && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
        e.preventDefault()
      }
      // Prevent browser defaults for our shortcuts (new tab, close tab, new window, toggle diff, run, find)
      if (["t", "w", "n", "d", "e", "f"].includes(e.key.toLowerCase()) && !e.shiftKey) {
        e.preventDefault()
      }
      // Prevent defaults for shift variants (close worktree, advanced/new/open worktree, open PR)
      if (["w", "n", "o", "r"].includes(e.key.toLowerCase()) && e.shiftKey) {
        e.preventDefault()
      }
      // Prevent browser defaults for shortcuts help (Cmd/Ctrl+Shift+/)
      if (["/", "?"].includes(e.key) && e.shiftKey) {
        e.preventDefault()
      }
      // Prevent defaults for jump-to shortcuts (Cmd/Ctrl+1-9)
      if (/^[1-9]$/.test(e.key)) {
        e.preventDefault()
      }
    }
    window.addEventListener("keydown", preventDefaults, true)

    // Delete/Backspace on a selected worktree triggers inline delete confirmation.
    // Pressing the key twice in a row (within the 2500ms window) confirms the delete.
    const deleteKeyHandler = (e: KeyboardEvent) => {
      if (e.key !== "Delete" && e.key !== "Backspace") return
      const tag = (e.target as HTMLElement)?.tagName
      if (tag === "INPUT" || tag === "TEXTAREA" || (e.target as HTMLElement)?.isContentEditable) return
      const sel = selection()
      if (!sel || sel === LOCAL) return
      e.preventDefault()
      confirmDeleteWorktree(sel)
    }
    window.addEventListener("keydown", deleteKeyHandler)

    // When the panel regains focus (e.g. returning from terminal), focus the prompt
    // and clear any stale body styles left by Kobalte modal overlays (dropdowns/dialogs
    // set pointer-events:none and overflow:hidden on body, but cleanup never runs if
    // focus leaves the webview before the overlay closes).
    const onWindowFocus = () => {
      document.body.style.pointerEvents = ""
      document.body.style.overflow = ""
      window.dispatchEvent(new Event("focusPrompt"))
    }
    window.addEventListener("focus", onWindowFocus)

    const drafts = createNewTaskDrafts()
    const newTaskHandler = (e: Event) => {
      const sel = selection()
      if (!sel || sel === LOCAL) return
      e.stopImmediatePropagation()
      const draft = drafts.create(sel)
      window.dispatchEvent(new CustomEvent("agentManagerCaptureDraft", { detail: { id: draft.id } }))
      terms.setActiveId(undefined)
      vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
    }
    window.addEventListener("newTaskRequest", newTaskHandler, true)

    // Add created sessions as local tabs (both direct from the prompt and
    // backend follow-ups). Dedups HTTP + SSE firing together.
    const unsubCreate = vscode.onMessage((msg) => {
      if (msg.type !== "sessionCreated") return
      const created = msg as SessionCreatedMessage
      const pending = created.draftID && localSessionIDs().includes(created.draftID) ? created.draftID : undefined
      if (!pending && localSessionIDs().includes(created.session.id)) return
      if (worktreeSessionIds().has(created.session.id)) return

      const active = activePendingId()
      const focus = !pending || (selection() === LOCAL && pending === active)
      if (pending) {
        setLocalSessionIDs((prev) => prev.map((id) => (id === pending ? created.session.id : id)))
        tabOrderSync.replaceOrAppend(LOCAL, pending, created.session.id)
        if (pending === active) setActivePendingId(undefined)
      } else {
        saveTabMemory()
        setLocalSessionIDs((prev) => [...prev, created.session.id])
        tabOrderSync.append(LOCAL, created.session.id)
        setSelection(LOCAL)
      }
      vscode.postMessage({ type: "agentManager.persistSession", sessionId: created.session.id })
      if (focus) session.selectSession(created.session.id)
    })

    // Mark sessions loaded as soon as the session context receives data (even if empty)
    const unsubSessions = vscode.onMessage((msg) => {
      if (msg.type === "sessionsLoaded" && !sessionsLoaded()) setSessionsLoaded(true)
    })

    const unsubRun = vscode.onMessage((msg) => {
      if (msg.type !== "agentManager.runStatus") return
      const ev = msg as RunStatus
      setRunStatuses((prev) => ({ ...prev, [ev.worktreeId]: ev }))
    })

    // Terminal messages have their own subscription to keep main-handler complexity in check.
    const terminalDispatch = createTerminalMessageHandler({
      state: terms,
      activate: termHandlers.activate,
      saveTabMemory,
      setSelection,
      showError: (message) =>
        showToast({ variant: "error", title: t("agentManager.terminal.errorTitle"), description: message }),
      onCreated: (contextKey, terminalId) => appendToTabOrder(contextKey, terminalId),
    })
    const unsubTerminals = vscode.onMessage((msg) => {
      terminalDispatch(msg)
    })

    const unsub = vscode.onMessage((msg) => {
      if (msg.type === "agentManager.repoInfo") {
        const info = msg as AgentManagerRepoInfoMessage
        setRepoBranch(info.branch)
        if (info.defaultBranch) setRepoDetectedBranch(info.defaultBranch)
      }

      if (msg.type === "agentManager.worktreeSetup") {
        const ev = msg as AgentManagerWorktreeSetupMessage
        if (ev.status === "ready" || ev.status === "error") {
          const error = ev.status === "error"
          if (ev.worktreeId) setBusyWorktrees((prev) => new Map([...prev].filter(([k]) => k !== ev.worktreeId)))
          setSetup({
            active: true,
            message: ev.message,
            branch: ev.branch,
            error,
            worktreeId: ev.worktreeId,
            errorCode: ev.errorCode,
          })
          globalThis.setTimeout(() => setSetup({ active: false, message: "" }), error ? 3000 : 500)
          if (!error && ev.sessionId) {
            session.selectSession(ev.sessionId)
            const ms = managedSessions().find((s) => s.id === ev.sessionId)
            if (ms?.worktreeId) setSelection(ms.worktreeId)
            evictLocal(ev.sessionId)
          }
        } else {
          // Track this worktree as setting up and auto-select it in the sidebar
          if (ev.worktreeId) {
            setBusyWorktrees(
              (prev) =>
                new Map([...prev, [ev.worktreeId!, { reason: "setting-up", message: ev.message, branch: ev.branch }]]),
            )
            setSelection(ev.worktreeId)
          }
          // Close diff/review panels — nothing to show during setup
          setSidePanel(null)
          setReviewActive(false)
          setSetup({ active: true, message: ev.message, branch: ev.branch, worktreeId: ev.worktreeId })
        }
      }

      if (msg.type === "agentManager.sessionAdded") {
        const ev = msg as { type: string; sessionId: string; worktreeId: string }
        saveTabMemory()
        appendToTabOrder(ev.worktreeId, ev.sessionId)
        setSelection(ev.worktreeId)
        evictLocal(ev.sessionId)
        drafts.apply(ev.worktreeId, ev.sessionId)
        session.selectSession(ev.sessionId)
      }

      if (msg.type === "agentManager.sessionForked") {
        const ev = msg as { type: string; sessionId: string; forkedFromId: string; worktreeId?: string }
        tabOrderSync.insertAfter(ev.worktreeId, ev.forkedFromId, ev.sessionId)
        if (!ev.worktreeId) {
          // Local session: insert new tab after the forked-from tab
          setLocalSessionIDs((prev) => {
            const idx = prev.indexOf(ev.forkedFromId)
            if (idx >= 0) return [...prev.slice(0, idx + 1), ev.sessionId, ...prev.slice(idx + 1)]
            return [...prev, ev.sessionId]
          })
          vscode.postMessage({ type: "agentManager.persistSession", sessionId: ev.sessionId })
        } else {
          saveTabMemory()
          setSelection(ev.worktreeId)
          evictLocal(ev.sessionId)
        }
        session.selectSession(ev.sessionId)
      }

      if (msg.type === "agentManager.keybindings") {
        const ev = msg as AgentManagerKeybindingsMessage
        setKb(ev.bindings)
      }

      if (msg.type === "agentManager.state") {
        const state = msg as AgentManagerStateMessage
        setWorktrees(state.worktrees)
        setManagedSessions(state.sessions)
        setStaleWorktreeIds(new Set(state.staleWorktreeIds ?? []))
        if (state.isGitRepo !== undefined) setIsGitRepo(state.isGitRepo)
        if (!worktreesLoaded()) setWorktreesLoaded(true)
        // When not a git repo, also mark sessions as loaded since the Kilo
        // server won't connect to send the sessionsLoaded message.
        if (state.isGitRepo === false && !sessionsLoaded()) setSessionsLoaded(true)
        const prev = new Set(sections().map((s) => s.id)),
          incoming = state.sections ?? []
        setSections(incoming)
        if (pendingNewSection) {
          pendingNewSection = false
          const c = incoming.find((s) => !prev.has(s.id))
          if (c) setRenamingSection(c.id)
        }
        if (state.tabOrder) setWorktreeTabOrder(state.tabOrder)
        if (state.worktreeOrder) setSidebarWorktreeOrder(state.worktreeOrder)
        if (state.reviewDiffStyle === "split" || state.reviewDiffStyle === "unified") {
          setReviewDiffStyle(state.reviewDiffStyle)
        }
        markdown.setRender(state.reviewMarkdownRender === true)
        if ("defaultBaseBranch" in state) setDefaultBaseBranch(state.defaultBaseBranch || undefined)
        setRunScriptConfigured(state.runScriptConfigured === true)
        syncRunStatuses(state.runStatuses)
        const current = session.currentSessionID()
        if (current) {
          const ms = state.sessions.find((s) => s.id === current)
          if (ms?.worktreeId) setSelection(ms.worktreeId)
        }
        // Restore local session IDs from persisted state (sessions with no worktreeId)
        const restored = restoreLocalSessions(
          state.sessions,
          localSessionIDs(),
          state.tabOrder?.[LOCAL],
          isPending,
          applyTabOrder,
        )
        if (restored) setLocalSessionIDs(restored)
        // Recover sessions collapsed state from extension-persisted state
        if (state.sessionsCollapsed !== undefined) setSessionsCollapsed(state.sessionsCollapsed)
        // Recover sidebar collapsed state and mark hydrated so transitions enable
        sidebar.hydrate(state.sidebarCollapsed)
        // Clear busy state for worktrees that have been removed
        const ids = new Set(state.worktrees.map((wt) => wt.id))
        setBusyWorktrees((prev) => {
          const next = new Map([...prev].filter(([id]) => ids.has(id)))
          return next.size === prev.size ? prev : next
        })
        setRunStatuses((prev) =>
          Object.fromEntries(Object.entries(prev).filter(([id]) => id === "local" || ids.has(id))),
        )
      }

      // When a multi-version progress update arrives, mark newly created worktrees as loading
      if ((msg as { type: string }).type === "agentManager.multiVersionProgress") {
        const ev = msg as unknown as AgentManagerMultiVersionProgressMessage
        if (ev.status === "done" && ev.groupId) {
          // Clear busy state for all worktrees in this group
          setBusyWorktrees((prev) => {
            const next = new Map(prev)
            for (const wt of worktrees()) {
              if (wt.groupId === ev.groupId) next.delete(wt.id)
            }
            return next
          })
        }
      }

      // When state updates arrive, mark new grouped worktrees as loading
      // (they were just created and haven't received their prompt yet)
      if (msg.type === "agentManager.worktreeSetup") {
        const ev = msg as AgentManagerWorktreeSetupMessage
        if (ev.status === "ready" && ev.sessionId) {
          const ms = managedSessions().find((s) => s.id === ev.sessionId)
          const wt = ms?.worktreeId ? worktrees().find((w) => w.id === ms.worktreeId) : undefined
          if (wt?.groupId) {
            setBusyWorktrees((prev) => new Map([...prev, [wt.id, { reason: "setting-up" as const }]]))
          }
        }
      }

      // Set per-session model selection without clearing busy state.
      // Used during Phase 1 of multi-version creation so the UI selector
      // reflects the correct model as soon as the worktree appears.
      if ((msg as { type: string }).type === "agentManager.setSessionModel") {
        const ev = msg as { type: string; sessionId: string; providerID: string; modelID: string }
        session.setSessionModel(ev.sessionId, ev.providerID, ev.modelID)
      }

      // Handle initial message send for multi-version sessions.
      // The extension creates the worktrees/sessions, then asks the webview
      // to send the prompt through the normal KiloProvider sendMessage path.
      // Once the message is sent, clear the loading state for that worktree.
      if ((msg as { type: string }).type === "agentManager.sendInitialMessage") {
        const ev = msg as unknown as AgentManagerSendInitialMessage

        // Set agent first so setSessionModel (and getSessionModel) resolve the
        // correct agent — otherwise the session falls back to defaultAgent().
        if (ev.agent) {
          session.setSessionAgent(ev.sessionId, ev.agent)
        }
        if (ev.providerID && ev.modelID) {
          session.setSessionModel(ev.sessionId, ev.providerID, ev.modelID)
        }
        seedInitialVariant(session, ev)

        // Only send a message if there's text — otherwise just clear busy state
        const init = initialMessage(ev)
        if (init) {
          vscode.postMessage(init)
        }
        // Clear busy state — use worktreeId from the message directly
        // to avoid race condition where managedSessions() hasn't updated yet
        if (ev.worktreeId) {
          setBusyWorktrees((prev) => {
            const next = new Map(prev)
            next.delete(ev.worktreeId)
            return next
          })
        }
      }

      if (msg.type === "agentManager.worktreeDiff") {
        const ev = msg as AgentManagerWorktreeDiffMessage
        let staleFiles: Set<string> | undefined
        setDiffDatas((prev) => {
          const existing = prev[ev.sessionId]
          const merged = existing
            ? mergeWorktreeDiffs(existing, ev.diffs)
            : { diffs: ev.diffs, stale: new Set<string>() }
          staleFiles = merged.stale
          const next = merged.diffs
          if (existing && existing.length === next.length && existing.every((old, i) => old === next[i])) return prev
          return { ...prev, [ev.sessionId]: next }
        })
        if (staleFiles) refreshStaleDiffs(ev.sessionId, staleFiles)
      }

      if (msg.type === "agentManager.worktreeDiffFile") {
        const ev = msg as AgentManagerWorktreeDiffFileMessage
        if (ev.diff) {
          setDiffDatas((prev) => {
            const existing = prev[ev.sessionId] ?? []
            const next = existing.map((item) => (item.file === ev.diff!.file ? ev.diff! : item))
            return { ...prev, [ev.sessionId]: next }
          })
          setDiffFilePending(ev.sessionId, ev.diff.file, false)
          return
        }
        setDiffFilePending(ev.sessionId, ev.file, false)
      }

      if (msg.type === "agentManager.worktreeDiffLoading") {
        const ev = msg as AgentManagerWorktreeDiffLoadingMessage
        setDiffLoading(ev.loading)
      }

      if (msg.type === "agentManager.applyWorktreeDiffResult") {
        const ev = msg as AgentManagerApplyWorktreeDiffResultMessage
        const files = new Set((ev.conflicts ?? []).map((entry) => entry.file).filter(Boolean)).size
        const count = ev.conflicts?.length ?? 0
        setApplyStates((prev) => ({
          ...prev,
          [ev.worktreeId]: {
            status: ev.status,
            message: ev.message,
            conflicts: ev.conflicts ?? [],
          },
        }))

        if (ev.status === "success") {
          showToast({ variant: "success", title: t("agentManager.apply.success"), description: ev.message })
          if (applyTarget() === ev.worktreeId) closeApplyDialog()
        }
        if (ev.status === "conflict") {
          const summary =
            count > 0 ? t("agentManager.apply.conflictToast", { count, files: Math.max(files, 1) }) : ev.message
          showToast({ variant: "error", title: t("agentManager.apply.conflict"), description: summary })
        }
        if (ev.status === "error") {
          showToast({ variant: "error", title: t("agentManager.apply.error"), description: ev.message })
        }
      }

      if (msg.type === "agentManager.revertWorktreeFileResult") revertCtl.onResult(msg as never)

      if (msg.type === "agentManager.worktreeStats") {
        const ev = msg as AgentManagerWorktreeStatsMessage
        const map: Record<string, WorktreeGitStats> = {}
        for (const s of ev.stats) map[s.worktreeId] = s
        setWorktreeStats(map)
      }

      if (msg.type === "agentManager.localStats") {
        const ev = msg as AgentManagerLocalStatsMessage
        setLocalStats(ev.stats)
        setRepoBranch(ev.stats.branch)
      }

      if (msg.type === "agentManager.prStatus") {
        const ev = msg as AgentManagerPRStatusMessage
        setPrStatuses((prev) => ({ ...prev, [ev.worktreeId]: ev.pr }))
      }
    })

    onCleanup(() => {
      window.removeEventListener("message", handler)
      window.removeEventListener("keydown", preventDefaults, true)
      window.removeEventListener("keydown", deleteKeyHandler)
      window.removeEventListener("focus", onWindowFocus)
      window.removeEventListener("newTaskRequest", newTaskHandler, true)
      drafts.cleanup()
      unsubCreate()
      unsubSessions()
      unsubRun()
      unsubTerminals()
      unsub()
    })
  })

  // Always select local on mount to initialize branch info and session state
  onMount(() => {
    selectLocal()
    // Request worktree/session state from extension — handles race where
    // initializeState() pushState fires before the webview is mounted
    vscode.postMessage({ type: "agentManager.requestState" })
    // Open a pending "New Session" tab if there are no persisted local sessions
    if (localSessionIDs().length === 0) {
      addPendingTab()
    }
  })

  const selectedDiffSessionId = () => {
    const sel = selection()
    if (sel === LOCAL) return LOCAL
    if (!sel) return undefined

    const current = session.currentSessionID()
    if (current) {
      const item = managedSessions().find((entry) => entry.id === current)
      if (item?.worktreeId === sel) return current
    }

    return managedSessions().find((entry) => entry.worktreeId === sel)?.id
  }

  const currentDiffSessionId = createMemo(selectedDiffSessionId)

  // Start/stop diff watch when panel opens/closes, review tab opens, or session changes
  createEffect(() => {
    const panel = diffOpen()
    const review = reviewActive()

    if (panel || review) {
      const id = currentDiffSessionId()
      if (id) {
        vscode.postMessage({ type: "agentManager.startDiffWatch", sessionId: id })
        return
      }
      vscode.postMessage({ type: "agentManager.stopDiffWatch" })
      setDiffLoading(false)
      return
    }

    setDiffLoading(false)
    vscode.postMessage({ type: "agentManager.stopDiffWatch" })
  })

  onCleanup(() => {
    if (diffOpen() || reviewActive()) {
      vscode.postMessage({ type: "agentManager.stopDiffWatch" })
    }
  })

  const openReviewTab = () => {
    const sel = selection()
    if (sel === null) return
    terms.setActiveId(undefined)
    setSidePanel(null)
    setReviewOpenForContext(sel, true)
    setReviewActive(true)
  }

  const toggleReviewTab = () => {
    if (reviewActive()) {
      closeReviewTab()
      return
    }
    openReviewTab()
  }

  // Deferred close: flip signal immediately for instant UI feedback,
  // the <Show> unmount triggers heavy FileDiff cleanup but the tab bar
  // and chat view are already visible before that work runs.
  const closeReviewTab = () => {
    freezeTabs()
    setReviewActive(false)
    setReviewOpenForSelection(false)
  }

  // Data for the review tab: use local diff data for local context,
  // current session for selected worktree context, or first available in that worktree.
  const reviewDiffs = createMemo(() => {
    const data = diffDatas()
    const sel = selection()
    const id = session.currentSessionID()
    if (sel === LOCAL) return data[LOCAL] ?? []
    if (id && data[id]) {
      const current = managedSessions().find((s) => s.id === id)
      if (sel && current?.worktreeId === sel) return data[id]!
    }
    if (!sel) return []
    const ids = managedSessions()
      .filter((s) => s.worktreeId === sel)
      .map((s) => s.id)
    for (const sid of ids) {
      if (data[sid]) return data[sid]!
    }
    return []
  })

  const diffSessionKey = createMemo(() => {
    const sel = selection()
    if (sel === LOCAL) return `local:${LOCAL}`
    if (sel === null) return `session:${session.currentSessionID() ?? ""}`
    return `worktree:${sel}`
  })

  const setSharedDiffStyle = (style: "unified" | "split") => {
    if (reviewDiffStyle() === style) return
    setReviewDiffStyle(style)
    vscode.postMessage({ type: "agentManager.setReviewDiffStyle", style })
  }

  const setDiffFilePending = (sessionId: string, file: string, value: boolean) => {
    setDiffFileLoading((prev) => {
      const session = prev[sessionId] ?? {}
      if (value) {
        if (session[file]) return prev
        return {
          ...prev,
          [sessionId]: { ...session, [file]: true },
        }
      }

      if (!session[file]) return prev
      const next = { ...session }
      delete next[file]
      if (Object.keys(next).length === 0) {
        const result = { ...prev }
        delete result[sessionId]
        return result
      }
      return {
        ...prev,
        [sessionId]: next,
      }
    })
  }

  const requestDiffFile = (file: string) => {
    const sessionId = currentDiffSessionId()
    if (!sessionId) return
    if (diffFileLoading()[sessionId]?.[file]) return
    setDiffFilePending(sessionId, file, true)
    vscode.postMessage({ type: "agentManager.requestWorktreeDiffFile", sessionId, file })
  }

  const refreshStaleDiffs = (sessionId: string, files: Set<string>) => {
    const loading = diffFileLoading()[sessionId] ?? {}
    for (const file of files) {
      if (loading[file]) continue
      setDiffFilePending(sessionId, file, true)
      vscode.postMessage({ type: "agentManager.requestWorktreeDiffFile", sessionId, file })
    }
  }

  const diffFileLoadingForCurrent = createMemo(() => {
    const sessionId = currentDiffSessionId()
    if (!sessionId) return new Set<string>()
    return new Set(Object.keys(diffFileLoading()[sessionId] ?? {}))
  })

  const revertCtl = createRevertFile(currentDiffSessionId, vscode, showToast, t)

  const handleConfigureSetupScript = () => {
    vscode.postMessage({ type: "agentManager.configureSetupScript" })
  }
  const setupScript = metrics.click("configure_setup_script", "worktree_settings", handleConfigureSetupScript)

  const handleChangeDefaultBaseBranch = () => {
    const [search, setSearch] = createSignal("")
    const [branches, setBranches] = createSignal<BranchInfo[]>([])
    const [loading, setLoading] = createSignal(true)
    const [highlighted, setHighlighted] = createSignal(-1)

    const unsub = vscode.onMessage((msg) => {
      if (msg.type === "agentManager.branches") {
        const ev = msg as AgentManagerBranchesMessage
        setBranches(ev.branches)
        if (ev.defaultBranch) setRepoDetectedBranch(ev.defaultBranch)
        setLoading(false)
      }
    })

    vscode.postMessage({ type: "agentManager.requestBranches" })

    const filtered = createMemo(() => {
      const s = search().toLowerCase()
      if (!s) return branches()
      return branches().filter((b) => b.name.toLowerCase().includes(s))
    })

    const selectBranch = (name: string | undefined) => {
      vscode.postMessage({ type: "agentManager.setDefaultBaseBranch", branch: name })
      setDefaultBaseBranch(name)
      dialog.close()
    }

    const handleKeyDown = (e: KeyboardEvent) => {
      const items = filtered()
      // offset by 1 for auto-detect option (-1 = auto-detect)
      const total = items.length + 1
      if (e.key === "ArrowDown") {
        e.preventDefault()
        e.stopPropagation()
        setHighlighted((prev) => Math.min(prev + 1, total - 2))
      } else if (e.key === "ArrowUp") {
        e.preventDefault()
        e.stopPropagation()
        setHighlighted((prev) => Math.max(prev - 1, -1))
      } else if (e.key === "Enter") {
        e.preventDefault()
        e.stopPropagation()
        const idx = highlighted()
        if (idx === -1) {
          selectBranch(undefined)
        } else {
          const branch = items[idx]
          if (branch) selectBranch(branch.name)
        }
      } else if (e.key === "Escape") {
        e.preventDefault()
        e.stopPropagation()
        dialog.close()
      }
    }

    dialog.show(() => {
      onCleanup(unsub)
      return (
        <Dialog title={t("agentManager.worktree.defaultBaseBranch")} fit>
          <div class="am-default-base-branch">
            <BranchSelect
              branches={filtered()}
              loading={loading()}
              search={search()}
              onSearch={(v) => {
                setSearch(v)
                setHighlighted(-1)
              }}
              onSelect={(b) => selectBranch(b.name)}
              onSearchKeyDown={handleKeyDown}
              selected={defaultBaseBranch()}
              highlighted={highlighted()}
              onHighlight={setHighlighted}
              searchPlaceholder={t("agentManager.dialog.searchBranches")}
              emptyLabel={t("agentManager.import.noMatchingBranches")}
              loadingLabel={t("agentManager.import.loadingBranches")}
              defaultLabel={t("agentManager.dialog.branchBadge.default")}
              remoteLabel={t("agentManager.dialog.branchBadge.remote")}
              defaultName={defaultBaseBranch()}
              autoOption={{
                label: t("agentManager.worktree.defaultBaseBranchAuto"),
                hint: repoDetectedBranch(),
                active: !hasConfiguredBranch(),
                highlighted: highlighted() === -1,
                onSelect: () => selectBranch(undefined),
              }}
            />
          </div>
        </Dialog>
      )
    })
  }

  const handleShowKeyboardShortcuts = () => {
    const categories = buildShortcutCategories(kb(), t)
    dialog.show(() => (
      <Dialog title={t("agentManager.shortcuts.title")} fit>
        <div class="am-shortcuts">
          <For each={categories}>
            {(category) => (
              <div class="am-shortcuts-category">
                <div class="am-shortcuts-category-title">{category.title}</div>
                <div class="am-shortcuts-list">
                  <For each={category.shortcuts}>
                    {(shortcut) => (
                      <div class="am-shortcuts-row">
                        <span class="am-shortcuts-label">{shortcut.label}</span>
                        <span class="am-shortcuts-keys">
                          <For each={parseBindingTokens(shortcut.binding)}>
                            {(token) => <kbd class="am-kbd">{token}</kbd>}
                          </For>
                        </span>
                      </div>
                    )}
                  </For>
                </div>
              </div>
            )}
          </For>
        </div>
      </Dialog>
    ))
  }

  const loaded = () => worktreesLoaded() && sessionsLoaded()

  const handleCreateWorktree = () => {
    if (!loaded()) return
    expandSidebar()
    vscode.postMessage({ type: "agentManager.createWorktree" })
  }
  const createWorktree = metrics.click("new_worktree", "worktrees", handleCreateWorktree)

  // Advanced worktree dialog — opens a full dialog with prompt, versions, model, mode
  const showAdvancedWorktreeDialog = () => {
    if (!loaded()) return
    expandSidebar()
    dialog.show(() => <NewWorktreeDialog onClose={() => dialog.close()} defaultBaseBranch={repoDefaultBranch()} />)
  }

  const confirmDeleteWorktree = (worktreeId: string) => {
    const wt = worktrees().find((w) => w.id === worktreeId)
    if (!wt) return

    // Second press/click: execute the delete
    if (pendingDelete() === worktreeId) {
      cancelPendingDelete()
      setBusyWorktrees((prev) => new Map([...prev, [wt.id, { reason: "deleting" as const }]]))
      vscode.postMessage({ type: "agentManager.deleteWorktree", worktreeId: wt.id })
      if (selection() === wt.id) {
        const next = nextSelectionAfterDelete(
          wt.id,
          sidebarOrder()
            .filter((f) => f.type === "wt")
            .map((f) => f.id),
        )
        if (next === LOCAL) selectLocal()
        else selectWorktree(next)
      }
      return
    }

    // First press/click: enter pending-delete state
    clearTimeout(pendingDeleteTimer)
    setPendingDelete(worktreeId)
    pendingDeleteTimer = setTimeout(() => setPendingDelete(null), 2500)
  }

  const confirmRemoveStaleWorktree = (worktreeId: string) => {
    const wt = worktrees().find((w) => w.id === worktreeId)
    if (!wt) return

    const remove = () => {
      vscode.postMessage({ type: "agentManager.removeStaleWorktree", worktreeId: wt.id })
      if (selection() === wt.id) {
        const next = nextSelectionAfterDelete(
          wt.id,
          sidebarOrder()
            .filter((f) => f.type === "wt")
            .map((f) => f.id),
        )
        if (next === LOCAL) selectLocal()
        if (next !== LOCAL) selectWorktree(next)
      }
      dialog.close()
    }

    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault()
        remove()
      }
    }

    dialog.show(() => (
      <Dialog title={t("agentManager.dialog.removeStaleWorktree.title")} fit>
        <div class="am-confirm" onKeyDown={onKeyDown}>
          <div class="am-confirm-message">
            <Icon name="warning" size="small" />
            <span>
              {t("agentManager.dialog.removeStaleWorktree.messagePre")}
              <code class="am-confirm-branch">{wt.branch}</code>
              {t("agentManager.dialog.removeStaleWorktree.messagePost")}
            </span>
          </div>
          <div class="am-confirm-actions">
            <Button variant="ghost" size="large" onClick={() => dialog.close()}>
              {t("agentManager.dialog.removeStaleWorktree.cancel")}
            </Button>
            <Button variant="primary" size="large" class="am-confirm-delete" onClick={remove} autofocus>
              {t("agentManager.dialog.removeStaleWorktree.confirm")}
            </Button>
          </div>
        </div>
      </Dialog>
    ))
  }

  const handleDeleteWorktree = (worktreeId: string, e: MouseEvent) => {
    e.stopPropagation()
    confirmDeleteWorktree(worktreeId)
  }

  const handlePromote = (sessionId: string, e: MouseEvent) => {
    e.stopPropagation()
    if (!loaded()) return
    metrics.track("promote_session", "unassigned_session")
    vscode.postMessage({ type: "agentManager.promoteSession", sessionId })
  }

  const openLocally = (sid: string) => {
    saveTabMemory()
    expandSidebar()
    const pending = activePendingId()
    if (pending) {
      setLocalSessionIDs((prev) => prev.map((id) => (id === pending ? sid : id)))
      setActivePendingId(undefined)
    } else setLocalSessionIDs((prev) => [...prev, sid])
    setSelection(LOCAL)
    setReviewActive(false)
    session.selectSession(sid)
    vscode.postMessage({ type: "agentManager.openLocally", sessionId: sid })
  }

  const handleAddSession = () => {
    const sel = selection()
    expandSidebar()
    if (sel === LOCAL) return addPendingTab()
    if (sel) {
      // Deactivate any focused terminal so the new session is visible.
      terms.setActiveId(undefined)
      vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
    }
  }
  const handleForkSession = (sessionId: string, messageId?: string) => {
    const sel = selection()
    const msg = { type: "agentManager.forkSession" as const, sessionId, ...(messageId ? { messageId } : {}) }
    if (!sel || sel === LOCAL) return vscode.postMessage(msg)
    vscode.postMessage({ ...msg, worktreeId: sel })
  }
  const handleCloseTab = (sessionId: string) => {
    freezeTabs()
    const pending = isPending(sessionId)
    const isActive = pending ? sessionId === activePendingId() : session.currentSessionID() === sessionId
    if (isActive) {
      const tabs = activeTabs()
      const idx = tabs.findIndex((s) => s.id === sessionId)
      const next = tabs[idx + 1] ?? tabs[idx - 1]
      if (next && isPending(next.id)) {
        setActivePendingId(next.id)
        session.clearCurrentSession()
      } else if (next) {
        setActivePendingId(undefined)
        session.selectSession(next.id)
      } else {
        setActivePendingId(undefined)
        session.clearCurrentSession()
      }
    }
    if (pending || localSet().has(sessionId)) {
      setLocalSessionIDs((prev) => prev.filter((id) => id !== sessionId))
      if (!pending) vscode.postMessage({ type: "agentManager.forgetSession", sessionId })
    } else {
      vscode.postMessage({ type: "agentManager.closeSession", sessionId })
    }
  }

  const handleTabMouseDown = (sessionId: string, e: MouseEvent) => {
    if (e.button === 1) {
      e.preventDefault()
      e.stopPropagation()
      handleCloseTab(sessionId)
    }
  }

  const selectSessionTab = (id: string, pending: boolean) => {
    setReviewActive(false)
    if (pending) {
      setActivePendingId(id)
      session.clearCurrentSession()
    } else {
      setActivePendingId(undefined)
      session.selectSession(id)
    }
  }
  const termHandlers = createTerminalHandlers({
    state: terms,
    tabIds: () => tabIds(),
    selectReview: () => setReviewActive(true),
    selectSessionTab,
    clearSession: () => session.clearCurrentSession(),
    resetOthers: () => {
      setReviewActive(false)
      setActivePendingId(undefined)
      session.clearCurrentSession()
    },
    isPendingId: isPending,
    findTab: (id) => tabLookup().get(id),
    postMessage: (msg) => vscode.postMessage(msg as never),
    onRemove: freezeTabs,
    getSelection: selection,
    LOCAL,
    REVIEW_TAB_ID,
  })

  const handleReviewTabMouseDown = (e: MouseEvent) => {
    if (e.button !== 1) return
    e.preventDefault()
    e.stopPropagation()
    closeReviewTab()
  }

  // Drag-and-drop handlers for tab reordering
  const tabLookup = createMemo(() => new Map(activeTabs().map((s) => [s.id, s])))
  const tabIds = createMemo(() => {
    const ids = activeTabs().map((s) => s.id)
    const sel = selection()
    if (sel === null) return ids
    const withReview = reviewOpen() ? [...ids, REVIEW_TAB_ID] : ids
    const terminalIds = terms.current().map((t) => t.id)
    const base = [...withReview, ...terminalIds]
    // `worktreeTabOrder` stores the per-context mixed order. Applied
    // for every context (LOCAL too) and persisted server-side via
    // `setTabOrder`; unknown IDs are filtered by `applyTabOrder`.
    const key = sel === LOCAL ? LOCAL : sel
    return applyTabOrder(
      base.map((id) => ({ id })),
      worktreeTabOrder()[key],
    ).map((item) => item.id)
  })
  const handleDragStart = (event: DragEvent) => {
    const id = event.draggable?.id
    if (typeof id === "string") setDraggingTab(id)
  }

  const handleDragOver = (event: DragEvent) => {
    const from = event.draggable?.id
    const to = event.droppable?.id
    if (typeof from !== "string" || typeof to !== "string") return
    const sel = selection()
    if (sel === null) return
    const key = sel === LOCAL ? LOCAL : sel
    // Unified mixed-drag: the current visible order is `tabIds()` and
    // includes sessions, review, and terminals. `reorderTabs` moves
    // `from` to `to`'s position regardless of kind, so a user can slot
    // a terminal between two sessions or vice versa.
    const reordered = reorderTabs(tabIds(), from, to)
    if (!reordered) return
    setWorktreeTabOrder((prev) => ({ ...prev, [key]: reordered }))
    // Keep the session-only list in sync for LOCAL so `localSessions()`
    // and membership checks stay aligned after a drag.
    if (key === LOCAL) {
      const sessionSubset = reordered.filter((id) => id !== REVIEW_TAB_ID && !isTerminalTabId(id))
      setLocalSessionIDs(sessionSubset)
    }
    // Mirror the order into the terminal state so `terms.current()`
    // (the source for renderTerminalLayer's slot order) matches.
    const terminalSubset = reordered.filter(isTerminalTabId)
    if (terminalSubset.length > 0) terms.reorder(key, terminalSubset)
  }

  const handleDragEnd = () => {
    setDraggingTab(undefined)
    const sel = selection()
    if (sel === null) return
    const key = sel === LOCAL ? LOCAL : sel
    const order = worktreeTabOrder()[key]
    if (order && order.length > 0) persistTabOrder(key, order)
  }

  const draggedTab = createMemo(() => {
    const id = draggingTab()
    if (!id) return undefined
    if (id === REVIEW_TAB_ID) return { id, title: t("session.tab.review") }
    if (isTerminalTabId(id)) {
      const term = terms.lookup().get(id)
      return term ? { id, title: term.title } : undefined
    }
    return activeTabs().find((s) => s.id === id)
  })

  const focusTab = (id: string) =>
    focusCurrentTab({
      id,
      terms,
      isTerminal: isTerminalTabId,
      isPending,
      reviewId: REVIEW_TAB_ID,
      reviewOpen,
      setReviewOpen: setReviewOpenForSelection,
      setReviewActive,
      tabLookup,
      setActivePendingId,
      clearSession: session.clearCurrentSession,
      selectSession: session.selectSession,
      activateTerminal: termHandlers.activate,
    })

  // Close the currently active tab via keyboard shortcut.
  // If no tabs remain, fall through to close the selected worktree.
  const closeActiveTab = () => {
    if (termHandlers.closeActive()) return
    if (reviewActive()) {
      closeReviewTab()
      return
    }
    const tabs = activeTabs()
    if (tabs.length === 0) {
      closeSelectedWorktree()
      return
    }
    const current = session.currentSessionID()
    const pending = activePendingId()
    const target = current
      ? tabs.find((s) => s.id === current)
      : pending
        ? tabs.find((s) => s.id === pending)
        : undefined
    if (!target) return
    handleCloseTab(target.id)
  }

  // Cmd+T: add a new tab strictly to the current selection (no side effects)
  const handleNewTabForCurrentSelection = () => {
    const sel = selection()
    if (sel === LOCAL) {
      addPendingTab()
    } else if (sel) {
      // Pass the captured worktree ID directly to avoid race conditions
      vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
    }
  }

  // Cmd+N: if an unassigned session is selected, promote it; otherwise create a new worktree
  const handleNewWorktreeOrPromote = () => {
    if (!loaded()) return
    const sel = selection()
    const sid = session.currentSessionID()
    if (sel === null && sid && !worktreeSessionIds().has(sid)) {
      vscode.postMessage({ type: "agentManager.promoteSession", sessionId: sid })
      return
    }
    handleCreateWorktree()
  }

  // Close the currently selected worktree with a confirmation dialog
  const closeSelectedWorktree = () => {
    const sel = selection()
    if (!sel || sel === LOCAL) return
    confirmDeleteWorktree(sel)
  }

  return (
    <div
      class="am-layout"
      classList={{ "am-layout-hydrated": sidebar.hydrated() }}
      onContextMenu={(e) => e.preventDefault()}
    >
      <div
        class="am-sidebar"
        classList={{ "am-sidebar-collapsed": sidebarCollapsed() }}
        style={{ width: sidebarCollapsed() ? "0px" : `${sidebarWidth()}px` }}
        inert={sidebarCollapsed() || undefined}
      >
        <ResizeHandle
          direction="horizontal"
          size={sidebarWidth()}
          min={MIN_SIDEBAR_WIDTH}
          max={9999}
          onResize={(width) => {
            pendingSidebarWidth = Math.min(width, window.innerWidth * MAX_SIDEBAR_WIDTH_RATIO)
            if (sidebarRaf === undefined) {
              sidebarRaf = requestAnimationFrame(() => {
                sidebarRaf = undefined
                setSidebarWidth(pendingSidebarWidth!)
              })
            }
          }}
        />
        {/* Local repo item */}
        <button
          class={`am-local-item ${selection() === LOCAL ? "am-local-item-active" : ""}`}
          data-sidebar-id="local"
          onClick={() => selectLocal()}
        >
          <Show when={!isLocalBusy()} fallback={<Spinner class="am-worktree-spinner" />}>
            <svg class="am-local-icon" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
              <rect x="2.5" y="3.5" width="15" height="10" rx="1" stroke="currentColor" />
              <path d="M6 16.5H14" stroke="currentColor" stroke-linecap="square" />
              <path d="M10 13.5V16.5" stroke="currentColor" />
            </svg>
          </Show>
          <div class="am-local-text">
            <span class="am-local-label">{t("agentManager.local")}</span>
            <Show when={repoBranch()}>
              <span class="am-local-branch">{repoBranch()}</span>
            </Show>
          </div>
          <Show when={localStats() === undefined}>
            <div class="am-worktree-stats-skeleton">
              <div class="am-worktree-stats-skeleton-row" />
              <div class="am-worktree-stats-skeleton-row" style={{ width: "70%" }} />
            </div>
          </Show>
          <Show
            when={
              localStats() &&
              (localStats()!.files > 0 ||
                localStats()!.additions > 0 ||
                localStats()!.deletions > 0 ||
                localStats()!.ahead > 0 ||
                localStats()!.behind > 0)
            }
          >
            <div class="am-worktree-stats">
              <Show
                when={localStats()!.additions > 0 || localStats()!.deletions > 0}
                fallback={
                  <Show when={localStats()!.files > 0}>
                    <span class="am-stat-files">{localStats()!.files}f</span>
                  </Show>
                }
              >
                <div class="am-worktree-stats-row">
                  <Show when={localStats()!.additions > 0}>
                    <span class="am-stat-additions">+{localStats()!.additions}</span>
                  </Show>
                  <Show when={localStats()!.deletions > 0}>
                    <span class="am-stat-deletions">
                      {"\u2212"}
                      {localStats()!.deletions}
                    </span>
                  </Show>
                </div>
              </Show>
              <Show when={localStats()!.ahead > 0 || localStats()!.behind > 0}>
                <div class="am-worktree-stats-row">
                  <Show when={localStats()!.ahead > 0}>
                    <span class="am-worktree-commits">
                      {"↑"}
                      {localStats()!.ahead}
                    </span>
                  </Show>
                  <Show when={localStats()!.behind > 0}>
                    <span class="am-worktree-behind">
                      {"↓"}
                      {localStats()!.behind}
                    </span>
                  </Show>
                </div>
              </Show>
            </div>
          </Show>
          <span class="am-shortcut-badge">{isMac ? "⌘" : "Ctrl+"}1</span>
        </button>

        {/* WORKTREES section */}
        <div class={`am-section ${sessionsCollapsed() ? "am-section-grow" : ""}`}>
          <div class="am-section-header">
            <span class="am-section-label">{t("agentManager.section.worktrees")}</span>
            <WorktreeSectionActions
              items={sidebarSearch.items}
              current={sidebarSearch.current}
              bindings={kb()}
              branch={repoDefaultBranch()}
              git={isGitRepo()}
              loaded={loaded()}
              t={t}
              onRef={(value) => (sidebarSearchMenu = value)}
              onSelect={focusSidebarSearchItem}
              onCreate={createWorktree}
              onAdvanced={showAdvancedWorktreeDialog}
              onSection={newSection}
              onShortcuts={metrics.click("keyboard_shortcuts", "worktrees_header", handleShowKeyboardShortcuts)}
              onSetup={setupScript}
              onBranch={handleChangeDefaultBaseBranch}
            />
          </div>
          <div class="am-worktree-list">
            <Show
              when={worktreesLoaded() && sessionsLoaded()}
              fallback={
                <div class="am-skeleton-list">
                  <div class="am-skeleton-wt">
                    <div class="am-skeleton-wt-icon" />
                    <div class="am-skeleton-wt-text" style={{ width: "60%" }} />
                  </div>
                </div>
              }
            >
              <Show when={!isGitRepo()}>
                <div class="am-not-git-notice">
                  <Icon name="warning" size="small" />
                  <span>{t("agentManager.notGitRepo")}</span>
                </div>
              </Show>
              <Show when={isGitRepo()}>
                {(() => {
                  const [renamingWt, setRenamingWt] = createSignal<string | null>(null)
                  const [renameValue, setRenameValue] = createSignal("")

                  const startRename = (wtId: string, current: string) => {
                    setRenamingWt(wtId)
                    setRenameValue(current)
                  }

                  let cancelled = false

                  const commitRename = (wtId: string) => {
                    if (cancelled) {
                      cancelled = false
                      return
                    }
                    const value = renameValue().trim()
                    setRenamingWt(null)
                    if (!value) return
                    vscode.postMessage({ type: "agentManager.renameWorktree", worktreeId: wtId, label: value })
                  }

                  const cancelRename = () => {
                    cancelled = true
                    setRenamingWt(null)
                  }

                  const hasSections = createMemo(() => sections().length > 0)
                  const wtIds = createMemo(() => sortedWorktrees().map((wt) => wt.id))
                  const secIds = createMemo(() => new Set(sections().map((s) => s.id)))
                  const home = () => new Map(sortedWorktrees().map((w) => [w.id, w.sectionId] as const))
                  const sectionAware = sectionAwareDetector(secIds, home)

                  const onWtDragStart = (event: DragEvent) => {
                    const id = event.draggable?.id
                    if (typeof id === "string") setDraggingWorktree(id)
                    document.body.classList.add("am-wt-dragging-active")
                  }
                  const onWtDragOver = (event: DragEvent) => {
                    const from = event.draggable?.id
                    const to = event.droppable?.id
                    if (typeof from !== "string" || typeof to !== "string") return
                    if (secIds().has(to)) return
                    setSidebarWorktreeOrder((prev) => {
                      const cur = applyTabOrder(
                        sortedWorktrees().map((w) => ({ id: w.id })),
                        prev,
                      ).map((i) => i.id)
                      return reorderTabs(cur, from, to) ?? prev
                    })
                  }
                  const onWtDragEnd = (event: DragEvent) => {
                    const from = event.draggable?.id
                    const to = event.droppable?.id
                    setDraggingWorktree(undefined)
                    document.body.classList.remove("am-wt-dragging-active")
                    if (typeof from === "string" && typeof to === "string" && secIds().has(to)) {
                      moveToSection([from], to)
                      return
                    }
                    vscode.postMessage({ type: "agentManager.setWorktreeOrder", order: sidebarWorktreeOrder() })
                  }

                  return (
                    <DragDropProvider
                      onDragStart={onWtDragStart}
                      onDragEnd={onWtDragEnd}
                      onDragOver={onWtDragOver}
                      collisionDetector={sectionAware}
                    >
                      <DragDropSensors />
                      <ConstrainDragXAxis />
                      <SortableProvider ids={wtIds()}>
                        {(() => {
                          const renderWt = (wt: WorktreeState, idx: () => number, list?: WorktreeState[]) => {
                            const wtSessions = createMemo(() =>
                              managedSessions().filter((ms) => ms.worktreeId === wt.id),
                            )
                            const navHint = () =>
                              adjacentHint(
                                wt.id,
                                selection() ?? session.currentSessionID() ?? "",
                                sidebarOrder().map((f) => f.id),
                                kb().previousSession ?? "",
                                kb().nextSession ?? "",
                              )
                            const groupSize = () =>
                              !wt.groupId ? 0 : sortedWorktrees().filter((w) => w.groupId === wt.groupId).length
                            const sortable = createSortable(wt.id)
                            void sortable
                            return (
                              <div
                                use:sortable
                                class={`am-wt-sortable ${sortable.isActiveDraggable ? "am-wt-dragging" : ""}`}
                              >
                                <WorktreeItem
                                  worktree={wt}
                                  label={worktreeLabel(wt)}
                                  subtitle={worktreeSubtitle(wt)}
                                  active={selection() === wt.id}
                                  pendingDelete={pendingDelete() === wt.id}
                                  busy={busyWorktrees().has(wt.id)}
                                  working={isAgentBusy(wt.id)}
                                  stale={isStaleWorktree(wt.id)}
                                  shortcut={shortcutMap().get(wt.id)}
                                  stats={worktreeStats()[wt.id]}
                                  navHint={navHint()}
                                  sessions={wtSessions().length}
                                  grouped={isGrouped(wt)}
                                  groupStart={isGroupStart(wt, idx(), list ?? sortedWorktrees())}
                                  groupEnd={isGroupEnd(wt, idx(), list ?? sortedWorktrees())}
                                  groupSize={groupSize()}
                                  renaming={renamingWt() === wt.id}
                                  renameValue={renameValue()}
                                  closeKeybind={kb().closeWorktree ?? ""}
                                  openKeybind={kb().openWorktree ?? ""}
                                  pr={
                                    prStatuses()[wt.id] !== undefined ? (prStatuses()[wt.id] ?? undefined) : undefined
                                  }
                                  runStatus={runStatuses()[wt.id]}
                                  onOpenPR={metrics.click("open_pull_request", "worktree_menu", () =>
                                    vscode.postMessage({ type: "agentManager.openPR", worktreeId: wt.id }),
                                  )}
                                  sections={sections()}
                                  currentSectionId={wt.sectionId}
                                  onMoveToSection={(secId) => moveToSection([wt.id], secId)}
                                  onMoveToNewSection={metrics.click("new_section", "worktree_menu", () => newSection())}
                                  onClick={() => {
                                    if (pendingDelete() === wt.id) {
                                      confirmDeleteWorktree(wt.id)
                                      return
                                    }
                                    selectWorktree(wt.id)
                                  }}
                                  onDelete={(e) => handleDeleteWorktree(wt.id, e)}
                                  onStartRename={(current) => startRename(wt.id, current)}
                                  onRenameInput={(v) => setRenameValue(v)}
                                  onCommitRename={() => commitRename(wt.id)}
                                  onCancelRename={cancelRename}
                                  onRemoveStale={() => confirmRemoveStaleWorktree(wt.id)}
                                  onCopyPath={() => navigator.clipboard.writeText(wt.path)}
                                  onOpen={metrics.click("open_worktree_window", "worktree_menu", () =>
                                    vscode.postMessage({ type: "agentManager.openWorktree", worktreeId: wt.id }),
                                  )}
                                />
                              </div>
                            )
                          }
                          if (hasSections()) {
                            const post = vscode.postMessage.bind(vscode)
                            return (
                              <For each={topLevelItems()}>
                                {(item, idx) => {
                                  if (item.kind === "section") {
                                    const sec = item.section
                                    const members = createMemo(() => worktreesInSection(sec.id))
                                    return (
                                      <SectionHeader
                                        section={sec}
                                        count={members().length}
                                        autoRename={renamingSection() === sec.id}
                                        onRenameEnd={() => setRenamingSection(null)}
                                        onToggle={() =>
                                          post({ type: "agentManager.toggleSectionCollapsed", sectionId: sec.id })
                                        }
                                        onRename={(name) =>
                                          post({ type: "agentManager.renameSection", sectionId: sec.id, name })
                                        }
                                        onDelete={() => post({ type: "agentManager.deleteSection", sectionId: sec.id })}
                                        onSetColor={(color) =>
                                          post({ type: "agentManager.setSectionColor", sectionId: sec.id, color })
                                        }
                                        isFirst={idx() === 0}
                                        isLast={idx() === topLevelItems().length - 1}
                                        onMoveUp={() => moveSection(sec.id, -1)}
                                        onMoveDown={() => moveSection(sec.id, 1)}
                                      >
                                        <Show when={!sec.collapsed}>
                                          <div class="am-section-group-body">
                                            <For each={members()}>{(wt, wtIdx) => renderWt(wt, wtIdx, members())}</For>
                                          </div>
                                        </Show>
                                      </SectionHeader>
                                    )
                                  }
                                  const ug = ungrouped()
                                  const wtIdx = () => ug.indexOf(item.wt)
                                  return renderWt(item.wt, wtIdx, ug)
                                }}
                              </For>
                            )
                          }
                          return <For each={sortedWorktrees()}>{(wt, idx) => renderWt(wt, idx)}</For>
                        })()}
                      </SortableProvider>
                      <DragOverlay>
                        {(() => {
                          const wt = sortedWorktrees().find((w) => w.id === draggingWorktree())
                          if (!wt) return null
                          return (
                            <div class="am-wt-overlay">
                              <Icon name="branch" size="small" />
                              <span>{worktreeLabel(wt)}</span>
                            </div>
                          )
                        })()}
                      </DragOverlay>
                    </DragDropProvider>
                  )
                })()}
                <Show when={worktrees().length === 0}>
                  <button class="am-worktree-create" onClick={createWorktree}>
                    <Icon name="plus" size="small" />
                    <span>{t("agentManager.worktree.new")}</span>
                  </button>
                </Show>
              </Show>
            </Show>
          </div>
        </div>

        {/* SESSIONS section (unassigned) — collapsible */}
        <div class={`am-section ${sessionsCollapsed() ? "" : "am-section-grow"}`}>
          <button
            class="am-section-header am-section-toggle"
            onClick={() => {
              const next = !sessionsCollapsed()
              setSessionsCollapsed(next)
              vscode.postMessage({ type: "agentManager.setSessionsCollapsed", collapsed: next })
            }}
          >
            <span class="am-section-label">
              <Icon
                name={sessionsCollapsed() ? "chevron-right" : "chevron-down"}
                size="small"
                class="am-section-chevron"
              />
              {t("agentManager.section.sessions")}
            </span>
          </button>
          <Show when={!sessionsCollapsed()}>
            <div class="am-list">
              <Show
                when={sessionsLoaded()}
                fallback={
                  <div class="am-skeleton-list">
                    <div class="am-skeleton-session">
                      <div class="am-skeleton-session-title" style={{ width: "70%" }} />
                      <div class="am-skeleton-session-time" />
                    </div>
                    <div class="am-skeleton-session">
                      <div class="am-skeleton-session-title" style={{ width: "55%" }} />
                      <div class="am-skeleton-session-time" />
                    </div>
                    <div class="am-skeleton-session">
                      <div class="am-skeleton-session-title" style={{ width: "65%" }} />
                      <div class="am-skeleton-session-time" />
                    </div>
                  </div>
                }
              >
                <For each={unassignedSessions()}>
                  {(s) => (
                    <ContextMenu>
                      <ContextMenu.Trigger as="div" style={{ display: "contents" }}>
                        <button
                          class={`am-item ${s.id === session.currentSessionID() && selection() === null ? "am-item-active" : ""}`}
                          data-sidebar-id={s.id}
                          onClick={() => {
                            saveTabMemory()
                            setSelection(null)
                            setReviewActive(false)
                            session.selectSession(s.id)
                          }}
                        >
                          <span class="am-item-title">{s.title || t("agentManager.session.untitled")}</span>
                          <span class="am-item-time">{formatRelativeDate(s.updatedAt)}</span>
                          <div class="am-item-promote">
                            <TooltipKeybind
                              title={t("agentManager.session.openInWorktree")}
                              keybind={kb().newWorktree ?? ""}
                              placement="right"
                            >
                              <IconButton
                                icon="branch"
                                size="small"
                                variant="ghost"
                                label={t("agentManager.session.openInWorktree")}
                                onClick={(e: MouseEvent) => handlePromote(s.id, e)}
                              />
                            </TooltipKeybind>
                          </div>
                        </button>
                      </ContextMenu.Trigger>
                      <ContextMenu.Portal>
                        <ContextMenu.Content class="am-ctx-menu">
                          <ContextMenu.Item onSelect={() => handlePromote(s.id, new MouseEvent("click"))}>
                            <Icon name="branch" size="small" />
                            <ContextMenu.ItemLabel>{t("agentManager.session.openInWorktree")}</ContextMenu.ItemLabel>
                          </ContextMenu.Item>
                          <ContextMenu.Item
                            onSelect={metrics.click("open_session_locally", "unassigned_session_menu", () =>
                              openLocally(s.id),
                            )}
                          >
                            <Icon name="folder" size="small" />
                            <ContextMenu.ItemLabel>{t("agentManager.session.openLocally")}</ContextMenu.ItemLabel>
                          </ContextMenu.Item>
                        </ContextMenu.Content>
                      </ContextMenu.Portal>
                    </ContextMenu>
                  )}
                </For>
              </Show>
            </div>
          </Show>
        </div>
      </div>

      <div class="am-detail">
        {/* Tab bar — full version with tabs renders when a section is selected
            and has tabs; otherwise a minimal version still renders so the
            sidebar toggle button stays at a fixed position. */}
        <Show
          when={selection() !== null && !contextEmpty()}
          fallback={
            <div class="am-tab-bar am-tab-bar-empty">
              <div class="am-tab-leading">
                <SidebarToggleButton collapsed={sidebarCollapsed()} onClick={toggleSidebar} />
              </div>
            </div>
          }
        >
          <DragDropProvider
            onDragStart={handleDragStart}
            onDragEnd={handleDragEnd}
            onDragOver={handleDragOver}
            collisionDetector={closestCenter}
          >
            <DragDropSensors />
            <ConstrainDragYAxis />
            <div class="am-tab-bar" onPointerLeave={releaseTabs}>
              <div class="am-tab-leading">
                <SidebarToggleButton collapsed={sidebarCollapsed()} onClick={toggleSidebar} />
              </div>
              <div class="am-tab-scroll-area">
                <div class={`am-tab-fade am-tab-fade-left ${tabScroll.showLeft() ? "am-tab-fade-visible" : ""}`} />
                <div class="am-tab-list-wrap">
                  <div
                    class="am-tab-list"
                    ref={tabScroll.setRef}
                    style={{ "--tab-count": `${tabIds().length}` } as JSX.CSSProperties}
                  >
                    <SortableProvider ids={tabIds()}>
                      <For each={tabIds()}>
                        {(id) =>
                          renderTab(id, {
                            terms,
                            REVIEW_TAB_ID,
                            tabIds,
                            kb,
                            reviewActive,
                            currentSessionID: () => session.currentSessionID(),
                            activePendingId,
                            visibleTabId,
                            isPending,
                            isBusy: isSessionBusy,
                            tabLookup,
                            adjacentHint,
                            activateTerminal: termHandlers.activate,
                            deactivateTerminal: termHandlers.deactivate,
                            closeTerminal: termHandlers.closeTerminal,
                            terminalMiddleClick: termHandlers.middleClick,
                            closeReview: closeReviewTab,
                            reviewMiddleClick: handleReviewTabMouseDown,
                            selectReviewTab: () => setReviewActive(true),
                            selectSessionTab,
                            sessionMiddleClick: handleTabMouseDown,
                            sessionClose: handleCloseTab,
                            sessionFork: handleForkSession,
                            reviewLabel: t("session.tab.review"),
                            reviewTooltip: t("command.review.toggle"),
                          })
                        }
                      </For>
                    </SortableProvider>
                  </div>
                </div>
                <div class={`am-tab-fade am-tab-fade-right ${tabScroll.showRight() ? "am-tab-fade-visible" : ""}`} />
              </div>
              <Show when={selection() !== null}>
                <div class="am-tab-add-wrap">
                  <div class="am-tab-add-separator" />
                  {renderNewTabButton({
                    contextSelected: () => selection() !== null,
                    kb,
                    newSessionLabel: t("agentManager.session.new"),
                    newTerminalLabel: t("agentManager.terminal.new"),
                    newSessionMenuLabel: t("agentManager.session.newSession"),
                    moreOptionsLabel: t("agentManager.tab.newOptions"),
                    onNewSession: metrics.click("new_session", "tab_bar", handleAddSession),
                    onNewTerminal: metrics.click("embedded_terminal", "new_tab_menu", () => termHandlers.requestNew()),
                  })}
                </div>
              </Show>
              <div class="am-tab-actions">
                {(() => {
                  const sel = () => selection()
                  const isWorktree = () => typeof sel() === "string" && sel() !== LOCAL
                  const stats = () => {
                    if (sel() === LOCAL) return localStats()
                    return typeof sel() === "string" ? worktreeStats()[sel() as string] : undefined
                  }
                  const hasChanges = () => {
                    const s = stats()
                    return s && (s.files > 0 || s.additions > 0 || s.deletions > 0)
                  }
                  const applyBusy = () => {
                    const state = applyStateForSelection()
                    if (!state) return false
                    return state.status === "checking" || state.status === "applying"
                  }
                  return (
                    <>
                      <Show when={isWorktree()}>
                        <>
                          <Tooltip value={t("agentManager.open.tooltip")} placement="bottom">
                            <Button size="small" variant="ghost" icon="folder" onClick={openWindow}>
                              {t("agentManager.open.button")}
                            </Button>
                          </Tooltip>
                          <Tooltip value={t("agentManager.apply.tooltip")} placement="bottom">
                            <Button
                              size="small"
                              variant="ghost"
                              onClick={openApplyDialog}
                              disabled={!hasChanges() || applyBusy()}
                            >
                              <Show when={applyBusy()}>
                                <Spinner class="am-apply-spinner" />
                              </Show>
                              {t("agentManager.apply.globalButton")}
                            </Button>
                          </Tooltip>
                        </>
                      </Show>
                      <Show when={sel()}>
                        {(() => {
                          const rid = () => (sel() === LOCAL ? LOCAL : (sel() as string))
                          const rs = () => runStatuses()[rid()]
                          const active = () => rs()?.state === "running" || rs()?.state === "stopping"
                          const configured = runScriptConfigured
                          const title = () => (configured() ? (active() ? "Stop" : "Run") : "Configure run script")
                          return (
                            <span
                              class={`am-run-group ${active() ? "am-run-active" : ""} ${!configured() ? "am-run-unconfigured" : ""}`}
                            >
                              <TooltipKeybind title={title()} keybind={kb().runScript ?? ""} placement="bottom">
                                <Button
                                  size="small"
                                  variant="ghost"
                                  icon={active() ? "stop" : "play"}
                                  disabled={rs()?.state === "stopping"}
                                  onClick={metrics.click(
                                    "run_script",
                                    "tab_toolbar",
                                    () => runWorktree(rid()),
                                    () => ({
                                      action: active() ? "stop" : configured() ? "run" : "configure",
                                    }),
                                  )}
                                >
                                  {active() ? "Stop" : "Run"}
                                </Button>
                              </TooltipKeybind>
                              <DropdownMenu gutter={4} placement="bottom-end">
                                <DropdownMenu.Trigger
                                  as={(p: Record<string, unknown>) => (
                                    <IconButton
                                      {...p}
                                      icon="chevron-down"
                                      size="small"
                                      variant="ghost"
                                      label={t("agentManager.run.options")}
                                      class="am-run-group-chevron"
                                    />
                                  )}
                                />
                                <DropdownMenu.Portal>
                                  <DropdownMenu.Content class="am-split-menu">
                                    <DropdownMenu.Item
                                      onSelect={metrics.click("configure_run_script", "run_menu", configureRunScript)}
                                    >
                                      <Icon name="settings-gear" size="small" />
                                      <DropdownMenu.ItemLabel>{t("agentManager.run.configure")}</DropdownMenu.ItemLabel>
                                    </DropdownMenu.Item>
                                  </DropdownMenu.Content>
                                </DropdownMenu.Portal>
                              </DropdownMenu>
                            </span>
                          )
                        })()}
                      </Show>
                      <TooltipKeybind
                        title={t("agentManager.diff.toggle")}
                        keybind={kb().toggleDiff ?? ""}
                        placement="bottom"
                      >
                        <button
                          class={`am-diff-toggle-btn ${diffOpen() && !reviewActive() ? "am-tab-diff-btn-active" : ""} ${hasChanges() ? "am-diff-toggle-has-changes" : ""}`}
                          onClick={() => {
                            metrics.track("side_review", "tab_toolbar", {
                              action: diffOpen() && !reviewActive() ? "close" : "open",
                            })
                            if (reviewActive()) {
                              closeReviewTab()
                              setSidePanel("diff")
                              return
                            }
                            setSidePanel((prev) => (prev === "diff" ? null : "diff"))
                          }}
                          title={t("agentManager.diff.toggle")}
                        >
                          <Icon name="layers" size="small" />
                          <Show when={hasChanges()}>
                            <span class="am-diff-toggle-stats">
                              <Show when={stats()!.files > 0}>
                                <span class="am-stat-files">{stats()!.files}f</span>
                              </Show>
                              <span class="am-stat-additions">+{stats()!.additions}</span>
                              <span class="am-stat-deletions">−{stats()!.deletions}</span>
                            </span>
                          </Show>
                        </button>
                      </TooltipKeybind>
                    </>
                  )
                })()}
                <Show when={selection() !== null}>
                  <Tooltip value={t("command.review.toggle")} placement="bottom">
                    <IconButton
                      icon="expand"
                      size="small"
                      variant="ghost"
                      label={t("command.review.toggle")}
                      class={reviewActive() ? "am-tab-diff-btn-active" : ""}
                      onClick={metrics.click("fullscreen_review", "tab_toolbar", toggleReviewTab)}
                    />
                  </Tooltip>
                </Show>
                {/* Legacy VS Code integrated terminal shortcut. Coexists
                    with the xterm terminal tabs (accessed via the `+`
                    split-button or Cmd+Shift+T): Cmd+/ still opens the
                    integrated terminal for the active session. */}
                <TooltipKeybind
                  title={t("agentManager.tab.terminal")}
                  keybind={kb().showTerminal ?? ""}
                  placement="bottom"
                >
                  <IconButton
                    icon="console"
                    size="small"
                    variant="ghost"
                    label={t("agentManager.tab.openTerminal")}
                    onClick={() => {
                      metrics.track("vscode_terminal", "tab_toolbar")
                      const id = session.currentSessionID()
                      if (id) vscode.postMessage({ type: "agentManager.showTerminal", sessionId: id })
                      else if (selection() === LOCAL) vscode.postMessage({ type: "agentManager.showLocalTerminal" })
                    }}
                  />
                </TooltipKeybind>
              </div>
            </div>
            <DragOverlay>
              <Show when={draggedTab()}>
                {(tab) => (
                  <div class="am-tab am-tab-overlay">
                    <span class="am-tab-label">{tab().title || t("agentManager.session.untitled")}</span>
                  </div>
                )}
              </Show>
            </DragOverlay>
          </DragDropProvider>
        </Show>

        {/* Empty worktree state */}
        <Show when={contextEmpty()}>
          <div class="am-empty-state">
            <div class="am-empty-state-icon">
              <Icon name="branch" size="large" />
            </div>
            <div class="am-empty-state-text">{t("agentManager.session.noSessions")}</div>
            <Button variant="primary" size="small" onClick={handleAddSession}>
              {t("agentManager.session.new")}
              <span class="am-shortcut-hint">{kb().newTab ?? ""}</span>
            </Button>
          </div>
        </Show>

        {(() => {
          // Show setup overlay: either the transient ready/error state for the selected worktree,
          // or if the selected worktree is still being set up (from busyWorktrees map)
          const overlayState = (): SetupState | null => {
            const s = setup()
            const sel = selection()
            // Transient ready/error overlay for the selected worktree (or worktree-less setup)
            if (s.active && (!s.worktreeId || sel === s.worktreeId)) return s
            // Persistent setup-in-progress for the currently selected worktree
            if (typeof sel === "string" && sel !== LOCAL) {
              const busy = busyWorktrees().get(sel)
              if (busy?.reason === "setting-up") {
                const wt = worktrees().find((w) => w.id === sel)
                return {
                  active: true,
                  message: busy.message ?? "",
                  branch: busy.branch ?? wt?.branch,
                } satisfies SetupState
              }
            }
            return null
          }
          return (
            <Show when={overlayState()}>
              {(state) => (
                <div class="am-setup-overlay">
                  <div class="am-setup-card">
                    <Icon name="branch" size="large" />
                    <div class="am-setup-title">
                      {state().error ? t("agentManager.setup.failed") : t("agentManager.setup.settingUp")}
                    </div>
                    <Show when={state().branch}>
                      <div class="am-setup-branch">{state().branch}</div>
                    </Show>
                    <div class="am-setup-status">
                      <Show when={!state().error} fallback={<Icon name="circle-x" size="small" />}>
                        <Spinner class="am-setup-spinner" />
                      </Show>
                      <span>
                        {state().errorCode ? t(`agentManager.setup.error.${state().errorCode}`) : state().message}
                      </span>
                    </div>
                  </div>
                </div>
              )}
            </Show>
          )
        })()}
        <Show when={history()}>
          <HistoryView
            onSelectSession={(id) => {
              if (addSessionToCurrentWorktree(id)) return
              setHistory(false)
              if (localSessionIDs().includes(id)) {
                saveTabMemory()
                session.selectSession(id)
                setSelection(LOCAL)
                return
              }
              const ms = worktreeSessionIds().has(id) ? managedSessions().find((s) => s.id === id) : undefined
              if (ms?.worktreeId) {
                selectWorktree(ms.worktreeId)
                session.selectSession(id)
                setReviewActive(false)
                return
              }
              openLocally(id)
            }}
            onBack={() => setHistory(false)}
          />
        </Show>
        <Show when={!contextEmpty() && !history()}>
          {/* Terminal overlay is scoped to the main pane so it does not cover the tab bar or side panel. */}
          <div class="am-detail-stack">
            {/* Chat/terminal + side diff panel. Keep it mounted under the
                review tab so live xterm canvases never leave the paint tree. */}
            <div
              class={`am-detail-content ${sidePanel() !== null ? "am-detail-split" : ""} ${reviewActive() ? "am-detail-content-hidden" : ""}`}
            >
              <div class={`am-main-pane ${terms.activeId() ? "am-main-pane-terminal-active" : ""}`}>
                {/* Keep terminal tabs mounted so output streams across worktree switches. */}
                {renderTerminalLayer({ state: terms })}
                <div class="am-chat-wrapper">
                  <ChatView
                    onSelectSession={(id) => {
                      if (addSessionToCurrentWorktree(id)) return
                      if (localSessionIDs().includes(id)) {
                        session.selectSession(id)
                        if (selection() === null) setSelection(LOCAL)
                        return
                      }
                      // Navigate to owning worktree instead of forcing into local mode
                      if (worktreeSessionIds().has(id)) {
                        const ms = managedSessions().find((s) => s.id === id)
                        if (ms?.worktreeId) {
                          selectWorktree(ms.worktreeId)
                          session.selectSession(id)
                          setReviewActive(false)
                          return
                        }
                      }
                      openLocally(id)
                    }}
                    onShowHistory={() => setHistory(true)}
                    onForkMessage={readOnly() ? undefined : handleForkSession}
                    onForkSession={readOnly() ? undefined : handleForkSession}
                    readonly={readOnly()}
                    continueInWorktree={selection() === LOCAL}
                    promptBoxId={`agent-manager:${selection() ?? "unassigned"}`}
                    pendingSessionID={selection() === LOCAL ? activePendingId() : undefined}
                  />
                  <Show when={readOnly()}>
                    <div class="am-readonly-banner">
                      <Icon name="branch" size="small" />
                      <span class="am-readonly-text">{t("agentManager.session.readonly")}</span>
                      <Button
                        variant="secondary"
                        size="small"
                        onClick={() => {
                          if (!loaded()) return
                          const sid = session.currentSessionID()
                          if (!sid) return
                          metrics.track("open_session_locally", "readonly_banner")
                          openLocally(sid)
                        }}
                      >
                        {t("agentManager.session.openLocally")}
                      </Button>
                      <Button
                        variant="primary"
                        size="small"
                        onClick={() => {
                          if (!loaded()) return
                          const sid = session.currentSessionID()
                          if (!sid) return
                          metrics.track("promote_session", "readonly_banner")
                          vscode.postMessage({ type: "agentManager.promoteSession", sessionId: sid })
                        }}
                      >
                        {t("agentManager.session.openInWorktree")}
                      </Button>
                    </div>
                  </Show>
                </div>
              </div>
              <Show when={sidePanel() !== null}>
                <div class="am-diff-resize" style={{ width: `${diffWidth()}px` }}>
                  <ResizeHandle
                    direction="horizontal"
                    edge="start"
                    size={diffWidth()}
                    min={200}
                    max={Math.round(window.innerWidth * 0.8)}
                    onResize={(w) => {
                      pendingDiffWidth = Math.max(200, Math.min(w, window.innerWidth * 0.8))
                      if (diffRaf === undefined) {
                        diffRaf = requestAnimationFrame(() => {
                          diffRaf = undefined
                          setDiffWidth(pendingDiffWidth!)
                        })
                      }
                    }}
                  />
                  <div class="am-diff-panel-wrapper">
                    <Show when={sidePanel() === "diff"}>
                      <DiffPanel
                        diffs={reviewDiffs()}
                        loading={diffLoading()}
                        loadingFiles={diffFileLoadingForCurrent()}
                        sessionId={currentDiffSessionId()}
                        sessionKey={diffSessionKey()}
                        diffStyle={reviewDiffStyle()}
                        onDiffStyleChange={setSharedDiffStyle}
                        markdownRender={markdown.render()}
                        onMarkdownRenderChange={markdown.update}
                        comments={reviewComments()}
                        onCommentsChange={setReviewCommentsForSelection}
                        composer={reviewComposer}
                        onSendClick={() => metrics.track("send_review_comments", "side_review")}
                        onClose={metrics.click("side_review_close", "side_review", () => setSidePanel(null))}
                        onExpand={
                          selection() !== null
                            ? metrics.click("fullscreen_review", "side_review", openReviewTab, { action: "open" })
                            : undefined
                        }
                        onRequestDiff={requestDiffFile}
                        onOpenFile={(file, line) => {
                          const id = currentDiffSessionId()
                          if (id)
                            vscode.postMessage({ type: "agentManager.openFile", sessionId: id, filePath: file, line })
                          else if (selection() === LOCAL) vscode.postMessage({ type: "openFile", filePath: file, line })
                        }}
                        onRevertFile={metrics.use("revert_file", "side_review", revertCtl.revert)}
                        revertingFiles={revertCtl.reverting()}
                        activeTerminalId={terms.activeId()}
                      />
                    </Show>
                  </div>
                </div>
              </Show>
            </div>
            {/* Full-screen review tab (lazy-mounted, stays alive once opened for fast toggle) */}
            <Show when={reviewOpen()}>
              <div class="am-review-host" style={{ display: reviewActive() && !terms.activeId() ? undefined : "none" }}>
                <FullScreenDiffView
                  diffs={reviewDiffs()}
                  loading={diffLoading()}
                  loadingFiles={diffFileLoadingForCurrent()}
                  sessionId={currentDiffSessionId()}
                  sessionKey={diffSessionKey()}
                  comments={reviewComments()}
                  onCommentsChange={setReviewCommentsForSelection}
                  composer={reviewComposer}
                  onSendAll={closeReviewTab}
                  onSendClick={() => metrics.track("send_review_comments", "fullscreen_review")}
                  diffStyle={reviewDiffStyle()}
                  onDiffStyleChange={setSharedDiffStyle}
                  markdownRender={markdown.render()}
                  onMarkdownRenderChange={markdown.update}
                  onRequestDiff={requestDiffFile}
                  onOpenFile={(file, line) => {
                    const id = currentDiffSessionId()
                    if (id) vscode.postMessage({ type: "agentManager.openFile", sessionId: id, filePath: file, line })
                    else if (selection() === LOCAL) vscode.postMessage({ type: "openFile", filePath: file, line })
                  }}
                  onRevertFile={metrics.use("revert_file", "fullscreen_review", revertCtl.revert)}
                  revertingFiles={revertCtl.reverting()}
                  activeTerminalId={terms.activeId()}
                  onClose={metrics.click("fullscreen_review", "fullscreen_review", closeReviewTab, { action: "close" })}
                />
              </div>
            </Show>
          </div>
        </Show>
      </div>
    </div>
  )
}

export const AgentManagerApp: Component = () => {
  return (
    <ThemeProvider defaultTheme="kilo-vscode">
      <DialogProvider>
        <VSCodeProvider>
          <MermaidDownloadBridge />
          <ServerProvider>
            <LanguageBridge>
              <MarkedProvider>
                <DiffComponentProvider component={Diff}>
                  <CodeComponentProvider component={Code}>
                    <FileComponentProvider component={File}>
                      <ProviderProvider>
                        <ConfigProvider>
                          <DisplayProvider>
                            <IndexingProvider>
                              <KiloEmbeddingModelsProvider>
                                <NotificationsProvider>
                                  <SessionProvider>
                                    <FeedbackProvider>
                                      <WorktreeModeProvider>
                                        <DataBridge>
                                          <AgentManagerContent />
                                        </DataBridge>
                                      </WorktreeModeProvider>
                                    </FeedbackProvider>
                                  </SessionProvider>
                                </NotificationsProvider>
                              </KiloEmbeddingModelsProvider>
                            </IndexingProvider>
                          </DisplayProvider>
                        </ConfigProvider>
                      </ProviderProvider>
                    </FileComponentProvider>
                  </CodeComponentProvider>
                </DiffComponentProvider>
              </MarkedProvider>
            </LanguageBridge>
          </ServerProvider>
        </VSCodeProvider>
        <Toast.Region />
      </DialogProvider>
    </ThemeProvider>
  )
}
