import * as fs from "fs"
import * as path from "path"
import type { KiloClient, Session } from "@kilocode/sdk/v2/client"
import type { KiloConnectionService } from "../services/cli-backend"
import { getErrorMessage } from "../kilo-provider-utils"
import { resolveLocalDiffTarget } from "../diff/shared/target"
import { getDiffMarkdownRender, setDiffMarkdownRender } from "../review-settings"
import { isAbsolutePath } from "../path-utils"
import { WorktreeManager, type CreateWorktreeResult } from "./WorktreeManager"
import { remoteRef, WorktreeStateManager, type Worktree } from "./WorktreeStateManager"
import { handleSection } from "./section-handler"
import { chooseBaseBranch, normalizeBaseBranch } from "./base-branch"
import { GitStatsPoller, type LocalStats, type WorktreePresenceResult, type WorktreeStats } from "./GitStatsPoller"
import { PRStatusBridge } from "./pr-status-bridge"
import { GitOps } from "./GitOps"
import { versionedName } from "./branch-name"
import { classifyWorktreeError } from "./git-import"
import { SetupScriptService } from "./SetupScriptService"
import { SetupScriptRunner } from "./SetupScriptRunner"
import { copyEnvFiles } from "./env-copy"
import { SessionTerminalManager } from "./SessionTerminalManager"
import { createTerminalHost } from "./terminal-host"
import { TerminalRouter } from "./terminal-routing"
import { executeVscodeTask } from "./task-runner"
import { startVscodeRunTask } from "./run/task"
import { RunController } from "./run/controller"
import { handleRunMessage } from "./run/message"
import { forkSession } from "./fork-session"
import { continueInWorktree } from "./continue-in-worktree"
import { WorktreeDiffController } from "./worktree-diff-controller"
import { WorktreeImporter } from "./worktree-importer"
import { recordPromotionHandoff } from "./promotion-handoff"
import { restoreWorktrees } from "./state-recovery"
import { createLocalDiff, diffSummary as localDiffSummary } from "./local-diff"
import { parseToolRequest, startFromTool, type ToolRequest } from "./tool-start"
import { stopSessionProcesses } from "../kilo-provider/background-process"

import { startSession } from "./mcp-warmup"
import { readTerminalFont, watchTerminalFont } from "./terminal-font"
import { buildKeybindingMap } from "./format-keybinding"
import { resolveVersionModels, buildInitialMessages, type CreatedVersion } from "./multi-version"
import { Semaphore } from "./semaphore"
import { PLATFORM } from "./constants"
import type { AgentManagerOutMessage, AgentManagerInMessage } from "./types"
import type { Host, PanelContext, OutputHandle, Disposable } from "./host"

/**
 * AgentManagerProvider opens the Agent Manager panel.
 *
 * Uses WorktreeStateManager for centralized state persistence. Worktrees and
 * sessions are stored in `.kilo/agent-manager.json`. The UI shows two
 * sections: WORKTREES (top) with managed worktrees + their sessions, and
 * SESSIONS (bottom) with unassociated local sessions.
 */
export class AgentManagerProvider implements Disposable {
  public static readonly viewType = "kilo-code.new.AgentManagerPanel"

  private panel: PanelContext | undefined
  private outputChannel: OutputHandle
  private worktrees: WorktreeManager | undefined
  private state: WorktreeStateManager | undefined
  private setupScript: SetupScriptService | undefined
  private importer: WorktreeImporter
  private terminalManager: SessionTerminalManager
  private terminalRouter: TerminalRouter
  private run: RunController
  private stateReady: Promise<void> | undefined
  private statsPoller: GitStatsPoller
  private prBridge!: PRStatusBridge
  private gitOps: GitOps
  private diffs: WorktreeDiffController
  private staleWorktreeIds = new Set<string>()
  private toolRequests = new Set<string>()
  private cachedWorktreeStats: { type: "agentManager.worktreeStats"; stats: WorktreeStats[] } | undefined
  private cachedLocalStats: { type: "agentManager.localStats"; stats: LocalStats } | undefined
  private unsubTool: (() => void) | undefined
  private unsubFont: (() => void) | undefined
  private closing: Promise<void> | undefined
  private onVisibilityChange: ((visible: boolean) => void) | undefined

  /** Session ID most recently loaded via a `loadMessages` message from the webview.
   *  Updated synchronously — unlike the session provider's currentSession which depends on
   *  an async `session.get` round-trip and can be stale during rapid tab switches. */
  private activeSessionId: string | undefined
  constructor(
    private readonly host: Host,
    private readonly connectionService: KiloConnectionService,
  ) {
    this.outputChannel = host.createOutput("Kilo Agent Manager")
    this.terminalManager = new SessionTerminalManager(
      (msg) => this.outputChannel.appendLine(`[SessionTerminal] ${msg}`),
      createTerminalHost(),
    )
    this.terminalRouter = new TerminalRouter({
      getClient: () => this.connectionService.getClient(),
      getServerConfig: () => this.connectionService.getServerConfig() ?? undefined,
      getRoot: () => this.getRoot(),
      getWorktreePath: (id) => this.getStateManager()?.getWorktree(id)?.path,
      log: (...args) => this.log("[XTerm]", ...args),
      post: (msg) => this.postToWebview(msg),
      getTerminalFont: () => readTerminalFont(),
    })
    this.unsubFont = watchTerminalFont((font) => {
      this.postToWebview({ type: "agentManager.terminal.fontChanged", font })
    })
    this.run = new RunController({
      root: () => this.getRoot(),
      state: () => this.getStateManager(),
      open: (file) => this.host.openDocument(file),
      start: startVscodeRunTask,
      post: (status) => this.postToWebview({ type: "agentManager.runStatus", ...status }),
      error: (message) => this.postToWebview({ type: "error", message }),
      log: (msg) => this.outputChannel.appendLine(`[RunScript] ${msg}`),
      refresh: () => this.pushState(),
    })
    this.importer = new WorktreeImporter({
      manager: () => this.getWorktreeManager(),
      state: () => this.getStateManager(),
      post: (msg) => this.postToWebview(msg),
      push: () => this.pushState(),
      setup: (dir, branch, id) => this.runSetupScriptForWorktree(dir, branch, id),
      session: (dir, branch, id) => this.createSessionInWorktree(dir, branch, id),
      register: (sid, dir) => this.registerWorktreeSession(sid, dir),
      ready: (sid, result, id) => this.notifyWorktreeReady(sid, result, id),
      log: (...args) => this.log(...args),
    })
    const semaphore = new Semaphore(3)
    this.gitOps = new GitOps({ log: (...args) => this.log(...args), semaphore })
    const local = createLocalDiff(this.gitOps, (...args) => this.log(...args))
    this.diffs = new WorktreeDiffController({
      getState: () => this.getStateManager(),
      getRoot: () => this.getRoot(),
      getStateReady: () => this.stateReady,
      git: this.gitOps,
      localDiff: local.summary,
      localDiffFile: local.file,
      post: (msg) => this.postToWebview(msg),
      log: (...args) => this.log(...args),
    })
    this.statsPoller = new GitStatsPoller({
      getWorktrees: () => this.state?.getWorktrees() ?? [],
      getWorkspaceRoot: () => this.getRoot(),
      localDiff: (dir, base) => localDiffSummary(this.gitOps, dir, base, (...args) => this.log(...args)),
      semaphore,
      onStats: (stats) => {
        const msg = { type: "agentManager.worktreeStats" as const, stats }
        this.cachedWorktreeStats = msg
        this.postToWebview(msg)
      },
      onLocalStats: (stats) => {
        const msg = { type: "agentManager.localStats" as const, stats }
        this.cachedLocalStats = msg
        this.postToWebview(msg)
      },
      onWorktreePresence: (presence) => {
        this.onWorktreePresence(presence)
      },
      log: (...args) => this.log(...args),
      git: this.gitOps,
    })
    this.prBridge = PRStatusBridge.create({
      getWorktrees: () => this.state?.getWorktrees() ?? [],
      getWorkspaceRoot: () => this.getRoot(),
      postToWebview: (m) => this.postToWebview(m),
      updateWorktreePR: (id, n, u, s) => this.state?.updateWorktreePR(id, n, u, s),
      hasPersistedPR: (id: string) => !!this.state?.getWorktree(id)?.prNumber,
      openExternal: (u) => this.host.openExternal(u),
      log: (...a) => this.log(...a),
      semaphore,
    })
    this.unsubTool = this.connectionService.onEventFiltered(
      (event) => (event as { type?: string }).type === "kilocode.agent_manager.start",
      (event, directory) => this.onToolEvent(event, directory),
    )
  }

  private log(...args: unknown[]) {
    const msg = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")
    this.outputChannel.appendLine(`${new Date().toISOString()} ${msg}`)
  }

  public openPanel(preserveFocus?: boolean): void {
    if (this.panel) {
      this.log("Panel already open, revealing")
      this.panel.reveal(preserveFocus)
      if (!preserveFocus) this.postToWebview({ type: "action", action: "focusInput" })
      return
    }
    this.log("Opening Agent Manager panel")
    this.host.capture("Agent Manager Opened", { source: PLATFORM })

    this.attachPanel(
      this.host.openPanel({
        onBeforeMessage: (msg) => this.onMessage(msg),
        worktreeDirectories: () => this.getWorktreeDirectories(),
      }),
    )
  }

  public onPanelVisibilityChange(cb: (visible: boolean) => void): void {
    this.onVisibilityChange = cb
  }

  /** Restore the Agent Manager panel from a previously serialized state.
   *  The caller (extension.ts / vscode-host.ts) wraps the raw panel before passing it. */
  public deserializePanel(ctx: PanelContext): void {
    if (this.panel) {
      this.log("Panel already exists during deserialization, disposing duplicate")
      ctx.dispose()
      return
    }
    this.log("Deserializing Agent Manager panel")
    this.attachPanel(ctx)
  }

  /** Message interceptor — exposed for the deserialization path in extension.ts. */
  public handleMessage(msg: Record<string, unknown>): Promise<Record<string, unknown> | null> {
    return this.onMessage(msg)
  }

  /** Wire up a panel context (shared by openPanel and deserializePanel). */
  private attachPanel(ctx: PanelContext): void {
    if (this.panel) {
      this.log("Disposing previous panel before attaching new one")
      this.panel.dispose()
      this.panel = undefined
    }
    this.panel = ctx

    this.statsPoller.setVisible(ctx.visible)
    this.onVisibilityChange?.(ctx.visible)
    ctx.onDidChangeVisibility((visible) => {
      this.statsPoller.setVisible(visible)
    })

    ctx.sessions.onFollowupAdopted((session, directory) => {
      this.adoptFollowupInWorktree(session, directory)
    })

    this.stateReady = this.initializeState()
    void this.sendRepoInfo()
    this.sendKeybindings()
    this.prBridge.attachPanel(ctx)
    ctx.onDidDispose(() => {
      // Only clear if this is still the active panel — a newer panel may
      // have already replaced us via attachPanel.
      if (this.panel === ctx) {
        this.log("Panel disposed")
        this.statsPoller.stop()
        this.prBridge.poller.stop()
        this.diffs.stop()
        this.activeSessionId = undefined
        this.connectionService.unregisterFocused("agent-manager")
        this.connectionService.registerOpen("agent-manager", [])
        this.panel = undefined
        this.onVisibilityChange?.(false)
      }
      ctx.sessions.dispose()
    })
  }

  // ---------------------------------------------------------------------------
  // State initialization
  // ---------------------------------------------------------------------------

  private async initializeState(): Promise<void> {
    const manager = this.getWorktreeManager()
    const state = this.getStateManager()
    if (!manager || !state) {
      this.pushEmptyState()
      return
    }

    await this.ensureGitExclude(manager)
    const loaded = await state.load()
    manager.cleanupOrphanedTempDirs()

    if (loaded.status === "failed" && !(await state.prepareRecovery())) {
      this.postToWebview({ type: "error", message: "Agent Manager state could not be recovered." })
      this.pushState()
      return
    }

    await this.recoverWorktrees(manager, state)

    // When the .kilocode → .kilo migration rewrote git worktree refs, nudge
    // VS Code's git extension to re-discover them. Without this, worktrees
    // won't appear in Source Control until the next VS Code restart.
    if (loaded.refsFixed > 0) {
      this.log(`Migration fixed ${loaded.refsFixed} git worktree ref(s), refreshing git`)
      this.host.refreshGit()
    }

    for (const wt of state.getWorktrees()) {
      for (const s of state.getSessions(wt.id)) {
        this.panel?.sessions.setSessionDirectory(s.id, wt.path)
        this.panel?.sessions.trackSession(s.id)
      }
    }
    for (const s of state.getSessions()) if (!s.worktreeId) this.panel?.sessions.trackSession(s.id)
    this.pushState()

    // Refresh sessions so worktree sessions appear in the list
    if (state.getSessions().length > 0) {
      this.panel?.sessions.refreshSessions()
    }

    // Recover any pending permission/question prompts that were missed during
    // panel recreation or SSE reconnection. Must run after all worktree sessions
    // are registered with their directory overrides so the recovery queries the
    // correct CLI backend Instances.
    this.panel?.sessions.recoverPendingPrompts()
  }

  private async ensureGitExclude(manager: WorktreeManager): Promise<void> {
    await manager.ensureGitExclude().catch((err) => {
      this.log("Failed to update git exclude:", err)
    })
  }

  private async recoverWorktrees(manager: WorktreeManager, state: WorktreeStateManager): Promise<void> {
    const infos = await manager.discoverWorktrees().catch((err) => {
      this.log("Failed to discover worktrees during state recovery:", err)
      return []
    })
    if (infos.length === 0) return

    const result = restoreWorktrees(state, infos)
    if (result.worktrees === 0 && result.sessions === 0) return

    this.log(`Recovered ${result.worktrees} worktree(s) and ${result.sessions} session(s) from disk`)
    await state.flush()
  }

  // ---------------------------------------------------------------------------
  // Message interceptor
  // ---------------------------------------------------------------------------

  private async onMessage(msg: Record<string, unknown>): Promise<Record<string, unknown> | null> {
    if (this.prBridge.handleMessage(msg)) return null
    if (msg.type === "requestFileSearch" && typeof msg.sessionID !== "string" && this.activeSessionId) {
      return { ...msg, sessionID: this.activeSessionId }
    }
    msg = await this.contextMessage(msg)
    const m = msg as unknown as AgentManagerInMessage
    if (this.shouldWaitForState(m)) await this.waitForStateReady(m.type)

    const worktree = await this.onWorktreeMessage(m)
    if (worktree !== undefined) return worktree
    const session = this.onSessionMessage(m, msg)
    if (session !== undefined) return session
    const ui = this.onUiMessage(m, msg)
    if (ui !== undefined) return ui
    const state = this.onStateMessage(m)
    if (state !== undefined) return state
    const imports = this.onImportMessage(m)
    if (imports !== undefined) return imports
    const diff = this.onDiffMessage(m)
    if (diff !== undefined) return diff
    const bridge = this.onBridgeMessage(m)
    if (bridge !== undefined) return bridge
    if (this.terminalRouter.handle(m)) return null

    return msg
  }

  private async contextMessage(msg: Record<string, unknown>): Promise<Record<string, unknown>> {
    if (msg.type !== "requestGitChangesContext") return msg
    const ctx = typeof msg.agentManagerContext === "string" ? msg.agentManagerContext : undefined
    const target = ctx ? await this.contextTarget(ctx) : undefined
    const sid = typeof msg.sessionID === "string" ? msg.sessionID : this.activeSessionId
    const next = sid && typeof msg.sessionID !== "string" ? { ...msg, sessionID: sid } : msg
    if (target) return { ...next, ...target }
    if (!sid) return next

    const state = this.getStateManager()
    const session = state?.getSession(sid)
    const worktree = session?.worktreeId ? state?.getWorktree(session.worktreeId) : undefined
    if (!worktree) return next
    return { ...next, contextDirectory: worktree.path, gitChangesBase: remoteRef(worktree) }
  }

  private async contextTarget(ctx: string): Promise<Record<string, unknown> | undefined> {
    if (ctx === "local") {
      const root = this.getRoot()
      if (!root) return undefined
      const target = await resolveLocalDiffTarget(this.gitOps, (...args) => this.log(...args), root)
      if (!target) return { contextDirectory: root }
      return { contextDirectory: target.directory, gitChangesBase: target.baseBranch }
    }

    const worktree = this.getStateManager()?.getWorktree(ctx)
    if (!worktree) return undefined
    return { contextDirectory: worktree.path, gitChangesBase: remoteRef(worktree) }
  }

  private async onWorktreeMessage(m: AgentManagerInMessage): Promise<Record<string, unknown> | null | undefined> {
    if (m.type === "agentManager.createWorktree") return this.onCreateWorktree(m.baseBranch, m.branchName)
    if (m.type === "agentManager.deleteWorktree") return this.onDeleteWorktree(m.worktreeId)
    if (m.type === "agentManager.removeStaleWorktree") return this.onRemoveStaleWorktree(m.worktreeId)
    if (m.type === "agentManager.promoteSession") return this.onPromoteSession(m.sessionId)
    if (m.type === "agentManager.addSessionToWorktree") return this.onAddSessionToWorktree(m.worktreeId, m.sessionId)
    if (m.type === "agentManager.forkSession") return this.onForkSession(m.sessionId, m.worktreeId, m.messageId)
    if (m.type === "agentManager.closeSession") return this.onCloseSession(m.sessionId)
  }

  private onSessionMessage(
    m: AgentManagerInMessage,
    msg: Record<string, unknown>,
  ): Record<string, unknown> | null | undefined {
    if (m.type === "agentManager.openLocally") {
      this.panel?.sessions.clearSessionDirectory(m.sessionId)
      const state = this.getStateManager()
      if (state?.getSession(m.sessionId)) {
        state.moveSession(m.sessionId, null)
        this.pushState()
      }
      return null
    }

    if (m.type === "continueInWorktree") {
      void this.continueFromSidebar(m.sessionId, (status, detail, error) => {
        this.panel?.postMessage({ type: "continueInWorktreeProgress", status, detail, error })
      })
      return null
    }

    if (m.type === "agentManager.persistSession" || m.type === "agentManager.forgetSession") {
      const persist = m.type === "agentManager.persistSession"
      void this.stateReady?.then(() => {
        const state = this.getStateManager()
        if (!state) return
        if (persist) {
          if (!state.getSession(m.sessionId)) state.addSession(m.sessionId, null)
          return
        }
        state.removeSession(m.sessionId)
      })
      return null
    }

    if ((m.type === "sendMessage" || m.type === "sendCommand") && !m.sessionID) {
      const ctx = typeof m.agentManagerContext === "string" ? m.agentManagerContext : undefined
      const worktree = ctx && ctx !== "local" ? this.getStateManager()?.getWorktree(ctx) : undefined
      if (worktree) {
        if (m.draftID) this.activeSessionId = m.draftID
        return { ...msg, contextDirectory: worktree.path }
      }
    }

    if ((m.type === "sendMessage" || m.type === "sendCommand") && m.draftID && !m.sessionID) {
      this.activeSessionId = m.draftID
      return msg
    }

    if (m.type === "requestTerminalContext") {
      if (m.sessionID && !this.terminalManager.hasActiveTerminal()) this.terminalManager.showExisting(m.sessionID)
      return msg
    }

    if (m.type === "loadMessages") {
      this.activeSessionId = m.sessionID
      this.connectionService.registerFocused("agent-manager", m.sessionID)
      this.terminalManager.syncOnSessionSwitch(m.sessionID)
      this.prBridge.poller.setActiveWorktreeId(this.state?.getSession(m.sessionID)?.worktreeId ?? undefined)
      return msg
    }

    if (m.type === "clearSession") {
      this.activeSessionId = undefined
      this.connectionService.unregisterFocused("agent-manager")
      void Promise.resolve().then(() => {
        if (!this.panel || !this.state) return
        for (const id of this.state.worktreeSessionIds()) {
          this.panel.sessions.trackSession(id)
        }
      })
      return msg
    }

    if (m.type === "abort") {
      this.host.capture("Agent Manager Session Stopped", {
        source: PLATFORM,
        sessionId: m.sessionID,
      })
      return msg
    }

    if (m.type === "agentManager.openSessions") {
      this.connectionService.registerOpen("agent-manager", m.sessionIDs)
      return null
    }
  }

  private onUiMessage(
    m: AgentManagerInMessage,
    msg: Record<string, unknown>,
  ): Record<string, unknown> | null | undefined {
    if (m.type === "agentManager.configureSetupScript") {
      void this.configureSetupScript()
      return null
    }
    if (handleRunMessage(this.run, m)) return null
    if (m.type === "agentManager.showTerminal") {
      this.terminalManager.showTerminal(m.sessionId, this.state)
      return null
    }
    if (m.type === "agentManager.showLocalTerminal") {
      this.terminalManager.showLocalTerminal()
      return null
    }
    if (m.type === "agentManager.openWorktree") {
      this.openWorktreeDirectory(m.worktreeId)
      return null
    }
    if (m.type === "agentManager.copyToClipboard") {
      this.host.copyToClipboard(m.text)
      return null
    }
    if (m.type === "previewImage") return msg
    if (m.type === "saveImage") return msg
    if (m.type === "agentManager.showExistingLocalTerminal") {
      this.terminalManager.syncLocalOnSessionSwitch()
      return null
    }
    if (m.type === "agentManager.requestRepoInfo") {
      void this.sendRepoInfo()
      return null
    }
    if (m.type === "agentManager.createMultiVersion") {
      void this.onCreateMultiVersion(m)
      return null
    }
    if (m.type === "agentManager.renameWorktree") {
      const state = this.getStateManager()
      if (state) {
        state.updateWorktreeLabel(m.worktreeId, m.label)
        this.pushState()
      }
      return null
    }
  }

  private onStateMessage(m: AgentManagerInMessage): Record<string, unknown> | null | undefined {
    if (m.type === "agentManager.requestState") {
      this.onRequestState()
      return null
    }
    if (m.type === "agentManager.setTabOrder") {
      this.state?.setTabOrder(m.key, m.order)
      return null
    }
    if (m.type === "agentManager.setWorktreeOrder") {
      this.state?.setWorktreeOrder(m.order)
      return null
    }
    if (m.type === "agentManager.setSessionsCollapsed") {
      this.state?.setSessionsCollapsed(m.collapsed)
      return null
    }
    if (m.type === "agentManager.setSidebarCollapsed") {
      this.state?.setSidebarCollapsed(m.collapsed)
      return null
    }
    if (this.handleSection(m)) return null
    if (m.type === "agentManager.setReviewDiffStyle") {
      this.state?.setReviewDiffStyle(m.style)
      return null
    }
    if (m.type === "agentManager.setReviewMarkdownRender") {
      void setDiffMarkdownRender(m.render).then(() => this.pushState())
      return null
    }
    if (m.type === "agentManager.setDefaultBaseBranch") {
      this.state?.setDefaultBaseBranch(normalizeBaseBranch(m.branch))
      this.pushState()
      return null
    }
  }

  private onImportMessage(m: AgentManagerInMessage): Record<string, unknown> | null | undefined {
    if (m.type === "agentManager.requestBranches") {
      void this.importer.branches()
      return null
    }
    if (m.type === "agentManager.requestExternalWorktrees") {
      void this.importer.external()
      return null
    }
    if (m.type === "agentManager.importFromBranch") {
      void this.importer.branch(m.branch)
      return null
    }
    if (m.type === "agentManager.importFromPR") {
      void this.importer.pr(m.url)
      return null
    }
    if (m.type === "agentManager.importExternalWorktree") {
      void this.importer.path(m.path, m.branch)
      return null
    }
    if (m.type === "agentManager.importAllExternalWorktrees") {
      void this.importer.all()
      return null
    }
  }

  private onDiffMessage(m: AgentManagerInMessage): Record<string, unknown> | null | undefined {
    if (m.type === "agentManager.requestWorktreeDiff") {
      void this.diffs.request(m.sessionId)
      return null
    }
    if (m.type === "agentManager.requestWorktreeDiffFile") {
      void this.diffs.requestFile(m.sessionId, m.file)
      return null
    }
    if (m.type === "agentManager.applyWorktreeDiff") {
      void this.diffs.apply(m.worktreeId, m.selectedFiles)
      return null
    }
    if (m.type === "agentManager.revertWorktreeFile") {
      void this.diffs.revert(m.sessionId, m.file)
      return null
    }
    if (m.type === "agentManager.startDiffWatch") {
      this.diffs.start(m.sessionId)
      return null
    }
    if (m.type === "agentManager.stopDiffWatch") {
      this.diffs.stop()
      return null
    }
    if (m.type === "agentManager.openFile") {
      this.openWorktreeFile(m.sessionId, m.filePath, m.line, m.column)
      return null
    }
  }

  private onBridgeMessage(m: AgentManagerInMessage): Record<string, unknown> | null | undefined {
    if (m.type !== "openFile") return undefined

    const sessionId = this.activeSessionId
    const state = this.getStateManager()
    if (sessionId && state?.directoryFor(sessionId)) {
      this.openWorktreeFile(sessionId, m.filePath, m.line, m.column)
      return null
    }
  }

  private onRequestState(): void {
    void this.stateReady
      ?.then(() => {
        // When the folder is not a git repo (or has no folder open),
        // this.state is never created. pushState() silently returns in that
        // case, so re-send the empty/non-git state explicitly.
        if (!this.state) {
          this.pushEmptyState()
          return
        }
        this.pushState()
        // Re-send cached stats so the webview gets them even if the poller
        // already emitted before the webview was ready to receive messages.
        if (this.cachedWorktreeStats) this.postToWebview(this.cachedWorktreeStats)
        if (this.cachedLocalStats) this.postToWebview(this.cachedLocalStats)
        this.prBridge.replay()
        // Refresh sessions after pushState so the webview's sessionsLoaded
        // handler is guaranteed to be registered (requestState fires from
        // onMount). Without this, the initial refreshSessions() in
        // initializeState() can race ahead of webview mount, causing
        // sessionsLoaded to never flip to true.
        if (this.state.getSessions().length > 0) {
          this.panel?.sessions.refreshSessions()
        }
      })
      .catch((err) => {
        this.log("initializeState failed, pushing partial state:", err)
        if (!this.state) {
          this.pushEmptyState()
          return
        }
        this.pushState()
      })
  }

  // ---------------------------------------------------------------------------
  // Shared helpers
  // ---------------------------------------------------------------------------

  /** Resolve the effective base branch using the configured default, explicit override, and existence check. */
  private async resolveBaseBranch(
    manager: WorktreeManager,
    state: WorktreeStateManager,
    explicit?: string,
  ): Promise<string | undefined> {
    const configured = state.getDefaultBaseBranch()
    if (!configured && !explicit) return undefined

    const configuredExists = configured ? await manager.branchExists(configured) : false
    const result = chooseBaseBranch({ explicit, configured, configuredExists })

    if (result.stale) {
      this.clearStaleDefaultBaseBranch(state, result.stale)
    }
    return result.branch
  }

  /** Reset a stale default base branch and notify the webview. */
  private clearStaleDefaultBaseBranch(state: WorktreeStateManager, stale: string): void {
    this.log(`Default base branch "${stale}" no longer exists, clearing`)
    state.setDefaultBaseBranch(undefined)
    this.pushState()
  }

  /** Create a git worktree on disk and register it in state. Returns null on failure. */
  private async createWorktreeOnDisk(opts?: {
    groupId?: string
    baseBranch?: string
    branchName?: string
    existingBranch?: string
    name?: string
    label?: string
  }): Promise<{
    worktree: ReturnType<WorktreeStateManager["addWorktree"]>
    result: CreateWorktreeResult
  } | null> {
    const manager = this.getWorktreeManager()
    const state = this.getStateManager()
    if (!manager || !state) {
      this.postToWebview({
        type: "agentManager.worktreeSetup",
        status: "error",
        message: "Open a folder that contains a git repository to use worktrees",
        errorCode: "not_git_repo",
      })
      return null
    }

    this.postToWebview({ type: "agentManager.worktreeSetup", status: "creating", message: "Creating git worktree..." })

    // Resolve effective base branch using configured default
    const effectiveBase = opts?.existingBranch
      ? undefined
      : await this.resolveBaseBranch(manager, state, opts?.baseBranch)

    let result: CreateWorktreeResult
    try {
      result = await manager.createWorktree({
        prompt: opts?.name || "kilo",
        baseBranch: effectiveBase ?? opts?.baseBranch,
        branchName: opts?.branchName,
        existingBranch: opts?.existingBranch,
      })
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error)
      this.postToWebview({
        type: "agentManager.worktreeSetup",
        status: "error",
        message: msg,
        errorCode: classifyWorktreeError(msg),
      })
      this.host.capture("Agent Manager Session Error", {
        source: PLATFORM,
        error: msg,
        context: "createWorktree",
      })
      return null
    }

    const worktree = state.addWorktree({
      branch: result.branch,
      path: result.path,
      parentBranch: result.parentBranch,
      remote: result.remote,
      groupId: opts?.groupId,
      label: opts?.label,
    })

    // Push state immediately so the sidebar shows the new worktree with a loading indicator
    this.pushState()
    this.postToWebview({
      type: "agentManager.worktreeSetup",
      status: "creating",
      message: "Setting up worktree...",
      branch: result.branch,
      worktreeId: worktree.id,
    })

    return { worktree, result }
  }

  /** Create a CLI session in a worktree directory. Returns null on failure. */
  private async createSessionInWorktree(
    worktreePath: string,
    branch: string,
    worktreeId?: string,
  ): Promise<Session | null> {
    let client: KiloClient
    try {
      client = this.connectionService.getClient()
    } catch (err) {
      this.log("createSessionInWorktree: client not available:", err)
      this.postToWebview({
        type: "agentManager.worktreeSetup",
        status: "error",
        message: "Not connected to CLI backend",
        worktreeId,
      })
      this.host.capture("Agent Manager Session Error", {
        source: PLATFORM,
        error: "Not connected to CLI backend",
        context: "createSession",
      })
      return null
    }

    this.postToWebview({
      type: "agentManager.worktreeSetup",
      status: "starting",
      message: "Starting session...",
      branch,
      worktreeId,
    })

    try {
      const { data: session } = await startSession(
        client,
        worktreePath,
        () => client.session.create({ directory: worktreePath, platform: PLATFORM }, { throwOnError: true }),
        (...args) => this.log(...args),
      )
      return session
    } catch (error) {
      const err = getErrorMessage(error)
      this.postToWebview({
        type: "agentManager.worktreeSetup",
        status: "error",
        message: `Failed to create session: ${err}`,
        worktreeId,
      })
      this.host.capture("Agent Manager Session Error", {
        source: PLATFORM,
        error: err,
        context: "createSession",
      })
      return null
    }
  }

  /** Send worktreeSetup.ready + sessionMeta + pushState after worktree creation. */
  private notifyWorktreeReady(sessionId: string, result: CreateWorktreeResult, worktreeId?: string): void {
    this.pushState()
    this.postToWebview({
      type: "agentManager.worktreeSetup",
      status: "ready",
      message: "Worktree ready",
      sessionId,
      branch: result.branch,
      worktreeId,
    })
    this.postToWebview({
      type: "agentManager.sessionMeta",
      sessionId,
      mode: "worktree",
      branch: result.branch,
      path: result.path,
      parentBranch: result.parentBranch,
    })
  }

  private async waitForStateReady(context: string): Promise<void> {
    if (!this.stateReady) return
    await this.stateReady.catch((err) => this.log(`${context}: stateReady rejected, continuing:`, err))
  }

  private shouldWaitForState(m: AgentManagerInMessage): boolean {
    switch (m.type) {
      case "agentManager.deleteWorktree":
      case "agentManager.removeStaleWorktree":
      case "agentManager.openLocally":
      case "agentManager.addSessionToWorktree":
      case "agentManager.closeSession":
      case "agentManager.persistSession":
      case "agentManager.forgetSession":
      case "agentManager.renameWorktree":
      case "agentManager.requestBranches":
      case "agentManager.importFromBranch":
      case "agentManager.importFromPR":
      case "agentManager.importExternalWorktree":
      case "agentManager.importAllExternalWorktrees":
      case "agentManager.setTabOrder":
      case "agentManager.setWorktreeOrder":
      case "agentManager.setSessionsCollapsed":
      case "agentManager.setSidebarCollapsed":
      case "agentManager.setReviewDiffStyle":
      case "agentManager.setDefaultBaseBranch":
      case "agentManager.createSection":
      case "agentManager.renameSection":
      case "agentManager.deleteSection":
      case "agentManager.setSectionColor":
      case "agentManager.toggleSectionCollapsed":
      case "agentManager.moveToSection":
      case "agentManager.moveSection":
        return true
      default:
        return false
    }
  }

  private onToolEvent(event: unknown, directory?: string): void {
    const properties = (event as { properties?: unknown }).properties
    const req = parseToolRequest(properties)
    if (!req) return
    if (directory) req.directory = directory
    void this.startToolRequest(req)
  }

  private async startToolRequest(req: ToolRequest): Promise<void> {
    await startFromTool(
      {
        getClient: () => this.connectionService.getClient(),
        getRoot: () => this.getRoot(),
        getState: () => this.getStateManager(),
        getPanel: () => this.panel,
        openPanel: (preserveFocus) => this.openPanel(preserveFocus),
        waitReady: (context) => this.waitForStateReady(context),
        createWorktree: (opts) => this.createWorktreeOnDisk(opts),
        claimRequest: (id) => {
          if (this.toolRequests.has(id)) return false
          const oldest = this.toolRequests.size >= 100 ? this.toolRequests.values().next().value : undefined
          if (oldest) this.toolRequests.delete(oldest)
          this.toolRequests.add(id)
          return true
        },
        cleanupWorktree: async (wid, dir) => {
          this.getStateManager()?.removeWorktree(wid)
          await this.getWorktreeManager()?.removeWorktree(dir)
          this.pushState()
        },
        setup: (dir, branch, id) => this.runSetupScriptForWorktree(dir, branch, id),
        createSessionInWorktree: (dir, branch, id) => this.createSessionInWorktree(dir, branch, id),
        registerWorktreeSession: (sid, dir) => this.registerWorktreeSession(sid, dir),
        notifyReady: (sid, result, wid) => this.notifyWorktreeReady(sid, result, wid),
        push: () => this.pushState(),
        post: (msg) => this.postToWebview(msg as AgentManagerOutMessage),
        capture: (event, props) => this.host.capture(event, props),
        log: (...args) => this.log(...args),
        error: (msg) => this.host.showError(msg),
      },
      req,
    )
  }

  // ---------------------------------------------------------------------------
  // Worktree actions
  // ---------------------------------------------------------------------------

  /** Create a new worktree with an auto-created first session. */
  private async onCreateWorktree(baseBranch?: string, branchName?: string): Promise<null> {
    await this.waitForStateReady("onCreateWorktree")

    const created = await this.createWorktreeOnDisk({ baseBranch, branchName })
    if (!created) return null

    // Run setup script for new worktree (blocks until complete, shows in overlay)
    await this.runSetupScriptForWorktree(created.result.path, created.result.branch, created.worktree.id)

    const session = await this.createSessionInWorktree(created.result.path, created.result.branch, created.worktree.id)
    if (!session) {
      const state = this.getStateManager()
      const manager = this.getWorktreeManager()
      state?.removeWorktree(created.worktree.id)
      await manager?.removeWorktree(created.result.path)
      this.pushState()
      return null
    }

    const state = this.getStateManager()!
    state.addSession(session.id, created.worktree.id)
    this.registerWorktreeSession(session.id, created.result.path)
    // Push state before registerSession so the webview's sessionCreated handler
    // sees the worktree mapping and routes the session to the worktree tab.
    this.notifyWorktreeReady(session.id, created.result, created.worktree.id)
    this.panel?.sessions.registerSession(session)
    this.host.capture("Agent Manager Session Started", {
      source: PLATFORM,
      sessionId: session.id,
      worktreeId: created.worktree.id,
      branch: created.result.branch,
    })
    this.log(`Created worktree ${created.worktree.id} with session ${session.id}`)
    return null
  }

  /** Delete a worktree and dissociate its sessions. */
  private async onDeleteWorktree(worktreeId: string): Promise<null> {
    const manager = this.getWorktreeManager()
    const state = this.getStateManager()
    if (!manager || !state) return null
    const worktree = state.getWorktree(worktreeId)
    if (!worktree) {
      this.log(`Worktree ${worktreeId} not found in state`)
      return null
    }
    // Remove from state BEFORE disk removal so pollers immediately stop targeting this worktree.
    // Pre-emptive skip covers any in-flight poll that already captured getWorktrees().
    this.statsPoller.skipWorktree(worktreeId)
    this.prBridge.remove(worktreeId)
    this.run.remove(worktreeId)
    const orphaned = state.removeWorktree(worktreeId)
    if (this.diffs.shouldStopForWorktree(worktree.path, orphaned)) {
      this.diffs.stop()
    }
    for (const s of orphaned) this.panel?.sessions.clearSessionDirectory(s.id)
    this.pushState()
    // Disk removal after state is clean — pollers no longer reference this worktree.
    try {
      await manager.removeWorktree(worktree.path, worktree.originalBranch ?? worktree.branch)
    } catch (error) {
      this.log(`Failed to remove worktree from disk: ${error}`)
    }
    this.log(`Deleted worktree ${worktreeId} (${worktree.originalBranch ?? worktree.branch})`)
    return null
  }

  /** Remove a stale worktree entry from state without touching the filesystem. */
  private async onRemoveStaleWorktree(worktreeId: string): Promise<null> {
    const state = this.getStateManager()
    if (!state) return null
    if (!this.staleWorktreeIds.has(worktreeId)) {
      this.log(`Ignored stale removal for non-stale worktree ${worktreeId}`)
      return null
    }

    const worktree = state.getWorktree(worktreeId)
    if (!worktree) {
      this.clearStaleTracking(worktreeId)
      this.pushState()
      return null
    }

    const orphaned = state.removeWorktree(worktreeId)
    if (this.diffs.shouldStopForWorktree(worktree.path, orphaned)) {
      this.diffs.stop()
    }
    for (const session of orphaned) {
      this.panel?.sessions.clearSessionDirectory(session.id)
    }
    this.clearStaleTracking(worktreeId)
    this.pushState()
    this.log(`Removed stale worktree entry ${worktreeId} (${worktree.branch})`)
    return null
  }

  /** Promote a session: create a worktree and move the session into it. */
  private async onPromoteSession(sessionId: string): Promise<null> {
    await this.waitForStateReady("onPromoteSession")
    const created = await this.createWorktreeOnDisk({})
    if (!created) return null

    // Run setup script for new worktree (blocks until complete, shows in overlay)
    await this.runSetupScriptForWorktree(created.result.path, created.result.branch, created.worktree.id)

    const state = this.getStateManager()!
    if (!state.getSession(sessionId)) {
      state.addSession(sessionId, created.worktree.id)
    } else {
      state.moveSession(sessionId, created.worktree.id)
    }

    this.registerWorktreeSession(sessionId, created.result.path)
    await this.recordPromotionHandoff(sessionId, created.result.path, created.result.branch)
    this.notifyWorktreeReady(sessionId, created.result, created.worktree.id)
    this.log(`Promoted session ${sessionId} to worktree ${created.worktree.id}`)
    return null
  }

  private async recordPromotionHandoff(sessionId: string, dir: string, branch: string): Promise<void> {
    try {
      await recordPromotionHandoff({
        client: this.connectionService.getClient(),
        sessionId,
        directory: dir,
        branch,
      })
    } catch (err) {
      this.log("Failed to record worktree promotion handoff:", getErrorMessage(err))
    }
  }

  /** Add a new session to an existing worktree. */
  private async onAddSessionToWorktree(worktreeId: string, sessionId?: string): Promise<null> {
    let client: KiloClient
    try {
      client = this.connectionService.getClient()
    } catch (err) {
      this.log("onAddSessionToWorktree: client not available:", err)
      this.postToWebview({ type: "error", message: "Not connected to CLI backend" })
      return null
    }

    const state = this.getStateManager()
    if (!state) return null

    const worktree = state.getWorktree(worktreeId)
    if (!worktree) {
      this.log(`Worktree ${worktreeId} not found`)
      return null
    }

    if (sessionId) {
      if (state.getSession(sessionId)) state.moveSession(sessionId, worktreeId)
      else state.addSession(sessionId, worktreeId)
      this.registerWorktreeSession(sessionId, worktree.path)
      this.pushState()
      this.postToWebview({
        type: "agentManager.sessionAdded",
        sessionId,
        worktreeId,
      })
      this.host.capture("Agent Manager Session Started", {
        source: PLATFORM,
        sessionId,
        worktreeId,
        existing: true,
      })
      this.log(`Added existing session ${sessionId} to worktree ${worktreeId}`)
      return null
    }

    let session: Session
    try {
      const { data } = await client.session.create(
        { directory: worktree.path, platform: PLATFORM },
        { throwOnError: true },
      )
      session = data
    } catch (error) {
      const err = getErrorMessage(error)
      this.postToWebview({ type: "error", message: `Failed to create session: ${err}` })
      this.host.capture("Agent Manager Session Error", {
        source: PLATFORM,
        error: err,
        context: "addSessionToWorktree",
        worktreeId,
      })
      return null
    }

    state.addSession(session.id, worktreeId)
    this.registerWorktreeSession(session.id, worktree.path)
    this.pushState()
    this.postToWebview({
      type: "agentManager.sessionAdded",
      sessionId: session.id,
      worktreeId,
    })

    if (this.panel) {
      this.panel.sessions.registerSession(session)
    }

    this.host.capture("Agent Manager Session Started", {
      source: PLATFORM,
      sessionId: session.id,
      worktreeId,
    })
    this.log(`Added session ${session.id} to worktree ${worktreeId}`)
    return null
  }

  private onForkSession(sessionId: string, worktreeId?: string, messageId?: string) {
    return forkSession(
      {
        getClient: () => this.connectionService.getClient(),
        state: this.getStateManager(),
        directory: this.getRoot(),
        postError: (msg) => this.postToWebview({ type: "error", message: msg }),
        registerWorktreeSession: (sid, dir) => this.registerWorktreeSession(sid, dir),
        pushState: () => this.pushState(),
        notifyForked: (s, from, wt) =>
          this.postToWebview({
            type: "agentManager.sessionForked",
            sessionId: s.id,
            forkedFromId: from,
            worktreeId: wt,
          }),
        registerSession: (s) => this.panel?.sessions.registerSession(s),
        log: (...args) => this.log(...args),
      },
      sessionId,
      worktreeId,
      messageId,
    )
  }

  /** Close (remove) a session from its worktree. */
  private async onCloseSession(sessionId: string): Promise<null> {
    const state = this.getStateManager()
    if (!state) return null

    const dirs = this.panel?.sessions.getSessionDirectories()
    const dir = state.directoryFor(sessionId) ?? dirs?.get(sessionId) ?? this.getRoot() ?? process.cwd()
    try {
      await stopSessionProcesses(this.connectionService.getClient(), sessionId, dir)
    } catch (err) {
      this.log("onCloseSession: client not available:", err)
    }

    state.removeSession(sessionId)
    this.panel?.sessions.clearSessionDirectory(sessionId)
    this.pushState()
    this.log(`Closed session ${sessionId}`)
    return null
  }

  // ---------------------------------------------------------------------------
  // Multi-version worktree creation
  // ---------------------------------------------------------------------------

  /** Create N worktree sessions for the same prompt (multi-version mode). */
  private async onCreateMultiVersion(
    msg: Extract<AgentManagerInMessage, { type: "agentManager.createMultiVersion" }>,
  ): Promise<null> {
    await this.waitForStateReady("onCreateMultiVersion")
    const text = msg.text?.trim() || undefined

    const worktreeName = msg.name?.trim() || undefined
    const agent = msg.agent
    const files = msg.files
    const baseBranch = msg.baseBranch
    const branchName = msg.branchName?.trim() || undefined

    const fallback = msg.providerID && msg.modelID ? { providerID: msg.providerID, modelID: msg.modelID } : undefined
    const resolved = resolveVersionModels(msg.modelAllocations, fallback, Number(msg.versions) || 1)
    const { models, versions, providerID, modelID } = resolved

    // Generate a shared group ID for multi-version worktrees
    const groupId = versions > 1 ? `grp-${Date.now()}` : undefined

    this.log(
      `Creating ${versions} worktrees${models.length > 0 ? " (model comparison)" : ""}${text ? ` for: ${text.slice(0, 60)}` : ""}${groupId ? ` (group=${groupId})` : ""}`,
    )

    // Notify webview that multi-version creation has started
    this.postToWebview({
      type: "agentManager.multiVersionProgress",
      status: "creating",
      total: versions,
      completed: 0,
      groupId,
    })

    // Phase 1: Create all worktrees + sessions first
    const created: CreatedVersion[] = []

    for (let i = 0; i < versions; i++) {
      this.log(`Creating worktree ${i + 1}/${versions}`)

      const version = versionedName(branchName || worktreeName, i, versions)
      const wt = await this.createWorktreeOnDisk({
        groupId,
        baseBranch,
        branchName: version.branch,
        name: version.branch,
        label: version.label,
      })
      if (!wt) {
        this.log(`Failed to create worktree for version ${i + 1}`)
        continue
      }

      await this.runSetupScriptForWorktree(wt.result.path, wt.result.branch, wt.worktree.id)

      const session = await this.createSessionInWorktree(wt.result.path, wt.result.branch, wt.worktree.id)
      if (!session) {
        const state = this.getStateManager()
        const manager = this.getWorktreeManager()
        state?.removeWorktree(wt.worktree.id)
        await manager?.removeWorktree(wt.result.path)
        this.log(`Failed to create session for version ${i + 1}`)
        continue
      }

      const state = this.getStateManager()!
      state.addSession(session.id, wt.worktree.id)
      this.registerWorktreeSession(session.id, wt.result.path)
      this.notifyWorktreeReady(session.id, wt.result, wt.worktree.id)

      // Set the per-version model immediately so the UI selector reflects
      // the correct model as soon as the worktree appears, before Phase 2.
      // Uses a dedicated message type to avoid clearing the busy state.
      const versionModel = models[i]
      const earlyProviderID = versionModel?.providerID ?? providerID
      const earlyModelID = versionModel?.modelID ?? modelID
      if (earlyProviderID && earlyModelID) {
        this.postToWebview({
          type: "agentManager.setSessionModel",
          sessionId: session.id,
          providerID: earlyProviderID,
          modelID: earlyModelID,
        })
      }

      created.push({
        worktreeId: wt.worktree.id,
        sessionId: session.id,
        path: wt.result.path,
        branch: wt.result.branch,
        parentBranch: wt.result.parentBranch,
        versionIndex: i,
      })

      this.host.capture("Agent Manager Session Started", {
        source: PLATFORM,
        sessionId: session.id,
        worktreeId: wt.worktree.id,
        branch: wt.result.branch,
        multiVersion: true,
        version: i + 1,
        totalVersions: versions,
        groupId,
      })
      this.log(`Version ${i + 1} worktree ready: session=${session.id}`)

      // Update progress
      this.postToWebview({
        type: "agentManager.multiVersionProgress",
        status: "creating",
        total: versions,
        completed: created.length,
        groupId,
      })
    }

    // Phase 2: Send the initial prompt to all sessions, or clear busy state if no text.
    const messages = buildInitialMessages(created, models, { providerID, modelID }, text, agent, msg.variant, files)
    for (let i = 0; i < messages.length; i++) {
      const msg = messages[i]!
      if (text) {
        this.log(`Sending initial message to version ${i + 1} (session=${msg.sessionId})`)
      }
      this.postToWebview({ type: "agentManager.sendInitialMessage", ...msg })
      if (text && i < messages.length - 1) {
        await new Promise((resolve) => setTimeout(resolve, 300))
      }
    }

    // Notify completion
    this.postToWebview({
      type: "agentManager.multiVersionProgress",
      status: "done",
      total: versions,
      completed: created.length,
      groupId,
    })

    if (created.length === 0) {
      this.host.showError(`Failed to create any of the ${versions} multi-version worktrees.`)
    }

    this.log(`Multi-version creation complete: ${created.length}/${versions} versions`)
    return null
  }

  // ---------------------------------------------------------------------------
  // Keybindings
  // ---------------------------------------------------------------------------

  private sendKeybindings(): void {
    const keybindings = this.host.extensionKeybindings()
    const bindings = buildKeybindingMap(keybindings, process.platform === "darwin")
    this.postToWebview({ type: "agentManager.keybindings", bindings })
  }

  // ---------------------------------------------------------------------------
  // Setup script
  // ---------------------------------------------------------------------------

  /** Open the worktree setup script in the editor for user configuration. */
  private async configureSetupScript(): Promise<void> {
    const service = this.getSetupScriptService()
    if (!service) return
    try {
      if (!service.hasScript()) {
        await service.createDefaultScript()
      }
      const resolved = service.resolveScript()
      if (!resolved) return
      await this.host.openDocument(resolved.path)
    } catch (error) {
      this.log(`Failed to open setup script: ${error}`)
    }
  }

  /** Copy .env files and run the worktree setup script. Blocks until complete. Shows progress in overlay. */
  private async runSetupScriptForWorktree(worktreePath: string, branch?: string, worktreeId?: string): Promise<void> {
    const root = this.getRoot()
    if (!root) return

    // Always copy .env files from the main repo (before the setup script so it can override)
    await copyEnvFiles(root, worktreePath, (msg) => this.outputChannel.appendLine(`[EnvCopy] ${msg}`))

    try {
      const service = this.getSetupScriptService()
      if (!service || !service.hasScript()) return
      this.postToWebview({
        type: "agentManager.worktreeSetup",
        status: "creating",
        message: "Running setup script...",
        branch,
        worktreeId,
      })
      const runner = new SetupScriptRunner(
        (msg) => this.outputChannel.appendLine(`[SetupScriptRunner] ${msg}`),
        service,
        executeVscodeTask,
      )
      await runner.runIfConfigured({ worktreePath, repoPath: root })
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error)
      this.outputChannel.appendLine(`[AgentManager] Setup script error: ${msg}`)
      this.postToWebview({
        type: "agentManager.worktreeSetup",
        status: "error",
        message: `Setup script failed: ${msg}`,
        branch,
        worktreeId,
      })
    }
  }

  // ---------------------------------------------------------------------------
  // Repo info
  // ---------------------------------------------------------------------------

  private async sendRepoInfo(): Promise<void> {
    const manager = this.getWorktreeManager()
    if (!manager) return
    try {
      const branch = await manager.currentBranch()
      const defaultBranch = await manager.defaultBranch()
      this.postToWebview({ type: "agentManager.repoInfo", branch, defaultBranch })
    } catch (error) {
      this.log(`Failed to get current branch: ${error}`)
    }
  }

  // ---------------------------------------------------------------------------
  // State helpers
  // ---------------------------------------------------------------------------

  private registerWorktreeSession(sessionId: string, directory: string): void {
    const worktree = this.state?.findWorktreeByPath(directory)
    if (worktree) this.writeMetadata(sessionId, worktree)

    if (!this.panel) return
    this.panel.sessions.setSessionDirectory(sessionId, directory)
    this.panel.sessions.trackSession(sessionId)
    // Recover any permission/question prompts that arrived before the session
    // was tracked. The CLI backend may have emitted permission.asked between
    // session.create() returning and this registration completing.
    this.panel.sessions.recoverPendingPrompts()
  }

  private writeMetadata(sessionId: string, worktree: Worktree): void {
    const manager = this.getWorktreeManager()
    if (!manager) return
    void manager
      .writeMetadata(worktree.path, sessionId, worktree.parentBranch, worktree.remote)
      .catch((err) => this.log(`Failed to write worktree metadata for ${worktree.id}:`, err))
  }

  /** Route a plan follow-up session to its worktree instead of LOCAL. */
  private adoptFollowupInWorktree(session: Session, directory: string): void {
    const state = this.getStateManager()
    if (!state) return
    const worktree = state.findWorktreeByPath(directory)
    if (!worktree) return

    state.addSession(session.id, worktree.id)
    this.registerWorktreeSession(session.id, directory)
    this.pushState()
    this.postToWebview({
      type: "agentManager.sessionAdded",
      sessionId: session.id,
      worktreeId: worktree.id,
    })
    this.log(`Adopted follow-up session ${session.id} into worktree ${worktree.id}`)
  }

  private onWorktreePresence(result: WorktreePresenceResult): void {
    const state = this.state
    if (!state) return

    const worktrees = state.getWorktrees()
    const ids = new Set(worktrees.map((wt) => wt.id))
    this.pruneStaleWorktreeIds(ids)

    if (result.degraded) {
      this.log("Skipping stale worktree update: degraded worktree probe")
      return
    }

    const entries = result.worktrees.filter((item) => ids.has(item.worktreeId))
    if (entries.length === 0) return

    // Sync branches from git worktree list (no extra git calls)
    let branchChanged = false
    for (const entry of entries) {
      if (entry.branch && state.updateWorktreeBranch(entry.worktreeId, entry.branch)) {
        branchChanged = true
      }
    }

    const next = new Set(entries.filter((entry) => entry.missing).map((entry) => entry.worktreeId))
    const staleChanged =
      next.size !== this.staleWorktreeIds.size || [...next].some((worktreeId) => !this.staleWorktreeIds.has(worktreeId))
    this.staleWorktreeIds = next

    if (staleChanged || branchChanged) {
      this.pushState()
    }
  }

  private clearStaleTracking(worktreeId: string): void {
    this.staleWorktreeIds.delete(worktreeId)
  }

  private staleWorktreesForState(worktrees: ReturnType<WorktreeStateManager["getWorktrees"]>): string[] {
    const ids = new Set(worktrees.map((wt) => wt.id))
    this.pruneStaleWorktreeIds(ids)
    return worktrees.filter((wt) => this.staleWorktreeIds.has(wt.id)).map((wt) => wt.id)
  }

  private pruneStaleWorktreeIds(ids: Set<string>): void {
    for (const id of [...this.staleWorktreeIds]) {
      if (ids.has(id)) continue
      this.staleWorktreeIds.delete(id)
    }
  }

  /** Sync the poller's skip set with currently collapsed sections. */
  private syncPollerSkips(): void {
    const state = this.state
    if (!state) return
    const skipped = new Set<string>()
    for (const sec of state.getSections()) {
      if (!sec.collapsed) continue
      for (const id of state.getWorktreesInSection(sec.id)) skipped.add(id)
    }
    const stats = this.statsPoller.syncSkips(skipped)
    if (!stats) return
    const msg = { type: "agentManager.worktreeStats" as const, stats }
    this.cachedWorktreeStats = msg
    this.postToWebview(msg)
  }

  private pushState(): void {
    const state = this.state
    if (!state) return
    const worktrees = state.getWorktrees()
    const staleWorktreeIds = this.staleWorktreesForState(worktrees)
    const run = this.run.state()
    this.postToWebview({
      type: "agentManager.state",
      worktrees,
      sessions: state.getSessions(),
      sections: state.getSections(),
      staleWorktreeIds,
      tabOrder: state.getTabOrder(),
      worktreeOrder: state.getWorktreeOrder(),
      sessionsCollapsed: state.getSessionsCollapsed(),
      sidebarCollapsed: state.getSidebarCollapsed(),
      reviewDiffStyle: state.getReviewDiffStyle(),
      reviewMarkdownRender: getDiffMarkdownRender(),
      isGitRepo: true,
      defaultBaseBranch: state.getDefaultBaseBranch(),
      ...run,
    })

    // Sync skip set before enabling the poller so the first poll cycle
    // already excludes worktrees in collapsed sections.
    this.syncPollerSkips()
    this.statsPoller.setEnabled(worktrees.length > 0 || this.panel !== undefined)
    this.prBridge.poller.setEnabled(worktrees.length > 0)
  }

  /** Push empty state when the folder is not a git repo or has no folder open. */
  private pushEmptyState(): void {
    this.staleWorktreeIds.clear()
    this.postToWebview({
      type: "agentManager.state",
      worktrees: [],
      sessions: [],
      staleWorktreeIds: [],
      reviewDiffStyle: "unified",
      reviewMarkdownRender: getDiffMarkdownRender(),
      isGitRepo: false,
      runStatuses: [],
      runScriptConfigured: false,
    })
  }

  // ---------------------------------------------------------------------------
  // Manager accessors
  // ---------------------------------------------------------------------------

  private getRoot(): string | undefined {
    return this.host.workspacePath()
  }

  private getWorktreeManager(): WorktreeManager | undefined {
    if (this.worktrees) return this.worktrees
    const root = this.getRoot()
    if (!root) {
      this.log("getWorktreeManager: no folder available")
      return undefined
    }
    this.worktrees = new WorktreeManager(
      root,
      (msg) => this.outputChannel.appendLine(`[WorktreeManager] ${msg}`),
      this.gitOps,
    )
    return this.worktrees
  }

  private getStateManager(): WorktreeStateManager | undefined {
    if (this.state) return this.state
    const root = this.getRoot()
    if (!root) {
      this.log("getStateManager: no folder available")
      return undefined
    }
    this.state = new WorktreeStateManager(root, (msg) => this.outputChannel.appendLine(`[StateManager] ${msg}`))
    return this.state
  }

  private getSetupScriptService(): SetupScriptService | undefined {
    if (this.setupScript) return this.setupScript
    const root = this.getRoot()
    if (!root) {
      this.log("getSetupScriptService: no folder available")
      return undefined
    }
    this.setupScript = new SetupScriptService(root)
    return this.setupScript
  }

  // ---------------------------------------------------------------------------
  // Worktree file helpers
  // ---------------------------------------------------------------------------

  /** Open a worktree directory directly in VS Code. */
  private openWorktreeDirectory(worktreeId: string): void {
    const state = this.getStateManager()
    if (!state) return
    const worktree = state.getWorktree(worktreeId)
    if (!worktree) return
    const target = path.normalize(worktree.path)
    if (!fs.existsSync(target)) {
      this.log(`openWorktreeDirectory: missing path ${target}`)
      this.host.showError("Worktree folder does not exist on disk.")
      return
    }
    this.host.openFolder(target, true)
  }

  /** Open a file from a worktree or local session in the VS Code editor.
   * Absolute paths (Unix `/…` or Windows `C:\…`) are opened directly.
   * Relative paths are resolved against the session's worktree directory
   * (or repo root for local sessions) with symlink-traversal protection. */
  private openWorktreeFile(sessionId: string, filePath: string, line?: number, column?: number): void {
    if (isAbsolutePath(filePath)) {
      this.host.openFile(filePath, line, column)
      return
    }
    const state = this.getStateManager()
    if (!state) return
    const session = state.getSession(sessionId)
    const base = session?.worktreeId ? state.getWorktree(session.worktreeId)?.path : this.getRoot()
    if (!base) return
    // Resolve real paths to prevent symlink traversal and normalize for
    // consistent comparison on both Unix and Windows.
    let resolved: string
    try {
      const root = fs.realpathSync(base)
      resolved = fs.realpathSync(path.resolve(base, filePath))
      // Directory-boundary check: append path.sep so "/foo/bar" won't match "/foo/bar2/..."
      if (resolved !== root && !resolved.startsWith(root + path.sep)) return
    } catch (err) {
      console.error("[Kilo New] AgentManagerProvider: Cannot resolve file path:", err)
      return
    }
    this.host.openFile(resolved, line, column)
  }

  private postToWebview(message: AgentManagerOutMessage): void {
    this.panel?.postMessage(message)
  }

  /**
   * Reveal the Agent Manager panel and focus the prompt input.
   * Used for the keyboard shortcut to switch back from terminal.
   */
  public focusPanel(): void {
    if (!this.panel) return
    this.panel.reveal(false)
    this.postToWebview({ type: "action", action: "focusInput" })
  }

  public isActive(): boolean {
    return this.panel?.active === true
  }

  /** Expose worktree session→directory mappings for the auto-approve toggle. */
  public getSessionDirectories(): ReadonlyMap<string, string> {
    return this.panel?.sessions.getSessionDirectories() ?? new Map()
  }

  public getWorktreeDirectories(): string[] {
    return (
      this.getStateManager()
        ?.getWorktrees()
        .map((wt) => wt.path) ?? []
    )
  }

  /**
   * Continue a sidebar session in a new worktree.
   * Captures git state, creates worktree, applies state, forks session.
   * Called from KiloProvider when the sidebar sends "continueInWorktree".
   */
  public async continueFromSidebar(
    sessionId: string,
    progress: (status: string, detail?: string, error?: string) => void,
  ): Promise<void> {
    const root = this.getRoot()
    if (!root) {
      progress("error", undefined, "No workspace folder open")
      return
    }

    this.openPanel()
    await this.waitForStateReady("continueFromSidebar")

    await continueInWorktree(
      {
        root,
        getClient: () => this.connectionService.getClient(),
        createWorktreeOnDisk: (opts) => this.createWorktreeOnDisk(opts),
        runSetupScript: (p, b, id) => this.runSetupScriptForWorktree(p, b, id),
        cleanupWorktree: async (id) => {
          await this.onDeleteWorktree(id)
        },
        notifyError: (error, result, id) => {
          this.postToWebview({
            type: "agentManager.worktreeSetup",
            status: "error",
            message: error,
            branch: result.branch,
            worktreeId: id,
          })
        },
        getStateManager: () => this.getStateManager(),
        registerWorktreeSession: (sid, dir) => this.registerWorktreeSession(sid, dir),
        registerSession: (session) => this.panel?.sessions.registerSession(session),
        notifyReady: (sid, result, wid) => this.notifyWorktreeReady(sid, result, wid),
        capture: (event, props) => this.host.capture(event, props),
        log: (...args) => this.log(...args),
      },
      sessionId,
      progress,
    )
  }

  public async createFromSidebar(baseBranch?: string, branchName?: string): Promise<void> {
    this.openPanel()
    const panel = this.panel
    if (!panel) return
    await panel.waitForReady()
    await this.waitForStateReady("createFromSidebar")
    await this.onCreateWorktree(baseBranch, branchName)
  }

  public async openAdvancedWorktree(): Promise<void> {
    this.openPanel()
    const panel = this.panel
    if (!panel) return
    await panel.waitForActive()
    await panel.waitForReady()
    await this.waitForStateReady("openAdvancedWorktree")
    queueMicrotask(() => this.postToWebview({ type: "action", action: "advancedWorktree" }))
  }

  private handleSection(m: AgentManagerInMessage): boolean {
    return handleSection(this.state, m, () => this.pushState())
  }

  public postMessage(message: unknown): void {
    this.panel?.postMessage(message)
  }

  public shutdown(): Promise<void> {
    if (!this.closing) this.closing = this.disposeAsync()
    return this.closing
  }

  public dispose(): void {
    void this.shutdown()
  }

  private async disposeAsync(): Promise<void> {
    await this.stateReady?.catch((err) => this.log("dispose: stateReady rejected:", err))
    await this.state?.flush().catch((err) => this.log("dispose: state flush failed:", err))
    this.unsubTool?.()
    this.unsubFont?.()
    this.connectionService.unregisterFocused("agent-manager")
    this.connectionService.registerOpen("agent-manager", [])
    this.diffs.stop()
    this.statsPoller.stop()
    this.gitOps.dispose()
    this.prBridge.poller.stop()
    this.run.dispose()
    this.terminalManager.dispose()
    await this.terminalRouter.dispose()
    this.panel?.dispose()
    this.outputChannel.dispose()
    this.host.dispose()
  }
}
