import fs from "fs"
import { logApp, logLLM, getDebugFlags } from "./debug"
import { getErrorMessage } from "./error-utils"
import { getRendererHandlers, tipc } from "@egoist/tipc/main"
import {
  showPanelWindow,
  showMainWindow,
  WINDOWS,
  resizePanelForAgentMode,
  resizePanelToNormal,
  closeAgentModeAndHidePanelWindow,
  getWindowRendererHandlers,
  setPanelMode,
  getCurrentPanelMode,
  hideFloatingPanelWindow,
  markManualResize,
  resetFloatingPanelPositionAndSize,
  setPanelFocusable,
  emergencyStopAgentMode,
  showPanelWindowAndShowTextInput,
  showPanelWindowAndStartMcpRecording,
  WAVEFORM_MIN_HEIGHT,
  TEXT_INPUT_MIN_WIDTH,
  TEXT_INPUT_MIN_HEIGHT,
  PROGRESS_MIN_HEIGHT,
  MIN_WAVEFORM_WIDTH,
  LEGACY_WAVEFORM_SIZE_MAX_WIDTH,
  LEGACY_WAVEFORM_SIZE_MAX_HEIGHT,
  PANEL_SAVED_SIZE_MAX_WIDTH,
  PANEL_SAVED_SIZE_MAX_HEIGHT,
  clearPanelOpenedWithMain,
  resizePanelForWaveformPreview,
} from "./window"
import {
  setTrackedAgentSessionSnoozed,
  snoozeAgentSessionsAndHidePanelWindow,
} from "./floating-panel-session-state"
import {
  app,
  clipboard,
  Menu,
  shell,
  systemPreferences,
  dialog,
  BrowserWindow,
} from "electron"
import path from "path"
import { configStore, recordingsFolder, conversationsFolder, trySaveConfig } from "./config"
import {
  Config,
  RecordingHistoryItem,
  MCPConfig,
  MCPServerConfig,
  Conversation,
  ConversationCompactionMetadata,
  ConversationHistoryItem,
  DesktopTTSPlaybackCommand,
  DesktopTTSPlaybackRequest,
  DesktopTTSPlaybackState,
  AgentProgressUpdate,
  SessionProfileSnapshot,
  LoopConfig,
} from "../shared/types"
import { DEFAULT_STT_MODELS, getConfiguredSttModel } from "@dotagents/shared"
import { getBranchMessageIndexMap } from "@shared/conversation-progress"
import { inferTransportType, normalizeMcpConfig } from "../shared/mcp-utils"
import { conversationService } from "./conversation-service"
import { RendererHandlers } from "./renderer-handlers"
import {
  postProcessTranscript,
  processTranscriptWithTools,
  processTranscriptWithAgentMode,
} from "./llm"
import { mcpService, MCPToolResult, WHATSAPP_SERVER_NAME, getInternalWhatsAppServerPath } from "./mcp-service"
import {
  saveCustomPosition,
  updatePanelPosition,
  constrainPositionToScreen,
  PanelPosition,
} from "./panel-position"
import { state, agentProcessManager, suppressPanelAutoShow, toolApprovalManager, agentSessionStateManager } from "./state"
import { generateTTS } from "./tts-service"
import { desktopTTSPlaybackCoordinator } from "./tts-playback-coordinator"


import { startRemoteServer, stopRemoteServer, restartRemoteServer, printQRCodeToTerminal, getRemoteServerStatus, getRemoteServerPairingApiKey } from "./remote-server"
import { getDiscordLifecycleAction } from "./discord-config"
import { discordService } from "./discord-service"
import { emitAgentProgress } from "./emit-agent-progress"
import { agentSessionTracker } from "./agent-session-tracker"
import { messageQueueService } from "./message-queue-service"
import { agentProfileService, createSessionSnapshotFromProfile, refreshSessionSnapshotSkillsFromProfile, toolConfigToMcpServerConfig } from "./agent-profile-service"
import { resolvePreferredTopLevelAcpAgentSelection } from "./main-agent-selection"
import { processTranscriptWithACPAgent } from "./acp-main-agent"
import { getAppSessionForAcpSession } from "./acp-session-state"
import { fetchModelsDevData, getModelFromModelsDevByProviderId, findBestModelMatch, refreshModelsDevCache } from "./models-dev-service"
import * as parakeetStt from "./parakeet-stt"
import { loopService } from "./loop-service"
import { clearSessionUserResponse } from "./session-user-response-store"
import { isMissingApiKeyErrorMessage } from "@dotagents/shared"
import { hasRepeatTaskTitlePrefix } from "../shared/repeat-tasks"

function describeAgentSessionId(sessionId?: string | null): "missing" | "pending" | "subsession" | "session" | "unknown" {
  if (!sessionId) return "missing"
  if (sessionId.startsWith("pending-")) return "pending"
  if (sessionId.startsWith("subsession_")) return "subsession"
  if (sessionId.startsWith("session_")) return "session"
  return "unknown"
}

function reconcileAgentSessionsWithRuntime(): void {
  agentSessionTracker.reconcileActiveSessions((session) =>
    agentSessionStateManager.isSessionRegistered(session.id),
  )
}

async function withRepeatTaskSessionFlag<T extends {
  conversationId?: string
  conversationTitle?: string
}>(session: T): Promise<T & { isRepeatTask?: boolean }> {
  if (hasRepeatTaskTitlePrefix(session.conversationTitle)) {
    return { ...session, isRepeatTask: true }
  }

  const loops = loopService.getLoops()
  const title = session.conversationTitle?.trim()
  if (title && loops.some((loop) => loop.name.trim() === title)) {
    return { ...session, isRepeatTask: true }
  }

  if (session.conversationId) {
    try {
      const conversation = await conversationService.loadConversation(session.conversationId)
      const firstUserMessage = conversation?.messages
        ?.find((message) => message.role === "user" && message.content.trim())
        ?.content.trim()
      if (
        firstUserMessage &&
        loops.some((loop) => loop.prompt.trim() === firstUserMessage)
      ) {
        return { ...session, isRepeatTask: true }
      }
    } catch (error) {
      logApp("[tipc] Failed to inspect session conversation for repeat-task grouping", {
        conversationId: session.conversationId,
        error,
      })
    }
  }

  return session
}

const isFinitePanelSize = (value: unknown): value is { width: number; height: number } =>
  !!value &&
  typeof value === "object" &&
  "width" in value &&
  "height" in value &&
  typeof (value as { width: unknown }).width === "number" &&
  typeof (value as { height: unknown }).height === "number" &&
  Number.isFinite((value as { width: number }).width) &&
  Number.isFinite((value as { height: number }).height)

const isBoundedPanelSize = (value: unknown): value is { width: number; height: number } =>
  isFinitePanelSize(value) &&
  value.width <= PANEL_SAVED_SIZE_MAX_WIDTH &&
  value.height <= PANEL_SAVED_SIZE_MAX_HEIGHT

type PanelSizeMode = "normal" | "agent" | "textInput"

const isPanelSizeMode = (value: unknown): value is PanelSizeMode =>
  value === "normal" || value === "agent" || value === "textInput"

const getPanelMinWidthForMode = (mode: PanelSizeMode) =>
  mode === "textInput" ? TEXT_INPUT_MIN_WIDTH : Math.max(200, MIN_WAVEFORM_WIDTH)

const getPanelMinHeightForMode = (mode: PanelSizeMode) =>
  mode === "agent"
    ? PROGRESS_MIN_HEIGHT
    : mode === "textInput"
      ? TEXT_INPUT_MIN_HEIGHT
      : WAVEFORM_MIN_HEIGHT

const normalizePanelSize = (
  input: { width: number; height: number },
  minWidth: number,
  minHeight: number,
) => ({
  width: Math.min(PANEL_SAVED_SIZE_MAX_WIDTH, Math.max(minWidth, Math.round(input.width))),
  height: Math.min(PANEL_SAVED_SIZE_MAX_HEIGHT, Math.max(minHeight, Math.round(input.height))),
})

/**
 * Convert Float32Array audio samples to WAV format buffer
 */
function float32ToWav(samples: Float32Array, sampleRate: number): Buffer {
  const numChannels = 1
  const bitsPerSample = 16
  const byteRate = sampleRate * numChannels * (bitsPerSample / 8)
  const blockAlign = numChannels * (bitsPerSample / 8)
  const dataSize = samples.length * (bitsPerSample / 8)
  const headerSize = 44
  const totalSize = headerSize + dataSize

  const buffer = Buffer.alloc(totalSize)
  let offset = 0

  // RIFF header
  buffer.write('RIFF', offset); offset += 4
  buffer.writeUInt32LE(totalSize - 8, offset); offset += 4
  buffer.write('WAVE', offset); offset += 4

  // fmt subchunk
  buffer.write('fmt ', offset); offset += 4
  buffer.writeUInt32LE(16, offset); offset += 4 // subchunk1Size (16 for PCM)
  buffer.writeUInt16LE(1, offset); offset += 2  // audioFormat (1 = PCM)
  buffer.writeUInt16LE(numChannels, offset); offset += 2
  buffer.writeUInt32LE(sampleRate, offset); offset += 4
  buffer.writeUInt32LE(byteRate, offset); offset += 4
  buffer.writeUInt16LE(blockAlign, offset); offset += 2
  buffer.writeUInt16LE(bitsPerSample, offset); offset += 2

  // data subchunk
  buffer.write('data', offset); offset += 4
  buffer.writeUInt32LE(dataSize, offset); offset += 4

  // Convert Float32 samples to 16-bit PCM
  for (let i = 0; i < samples.length; i++) {
    // Clamp to [-1, 1] and scale to 16-bit signed integer range
    const sample = Math.max(-1, Math.min(1, samples[i]))
    const intSample = Math.round(sample * 32767)
    buffer.writeInt16LE(intSample, offset)
    offset += 2
  }

  return buffer
}

async function postProcessTranscriptSafely(transcript: string, context: string): Promise<string> {
  const config = configStore.get()

  if (!config.transcriptPostProcessingEnabled) {
    return transcript
  }

  try {
    return await postProcessTranscript(transcript)
  } catch (error) {
    logLLM(`[${context}] Transcript post-processing failed, using raw transcript instead:`, error)
    return transcript
  }
}

function getRemoteSttModel(config: Config): string {
  return getConfiguredSttModel(config) || DEFAULT_STT_MODELS.openai
}

async function initializeMcpWithProgress(config: Config, sessionId: string, runId?: number): Promise<void> {
  const shouldStop = () => agentSessionStateManager.shouldStopSession(sessionId)
  const effectiveMaxIterations = config.mcpUnlimitedIterations ? Infinity : (config.mcpMaxIterations ?? 10)

  if (shouldStop()) {
    return
  }

  const initStatus = mcpService.getInitializationStatus()

  await emitAgentProgress({
    sessionId,
    runId,
    currentIteration: 0,
    maxIterations: effectiveMaxIterations,
    steps: [
      {
        id: `mcp_init_${Date.now()}`,
        type: "thinking",
        title: "Preparing agent tools",
        description: initStatus.progress.currentServer
          ? `Connecting ${initStatus.progress.currentServer} (${initStatus.progress.current}/${initStatus.progress.total})`
          : `Checking tool connections (${initStatus.progress.current}/${initStatus.progress.total})`,
        status: "in_progress",
        timestamp: Date.now(),
      },
    ],
    isComplete: false,
  })

  const progressInterval = setInterval(async () => {
    if (shouldStop()) {
      clearInterval(progressInterval)
      return
    }

    const currentStatus = mcpService.getInitializationStatus()
    if (currentStatus.isInitializing) {
      await emitAgentProgress({
        sessionId,
        runId,
        currentIteration: 0,
        maxIterations: effectiveMaxIterations,
        steps: [
          {
            id: `mcp_init_${Date.now()}`,
            type: "thinking",
            title: "Preparing agent tools",
            description: currentStatus.progress.currentServer
              ? `Connecting ${currentStatus.progress.currentServer} (${currentStatus.progress.current}/${currentStatus.progress.total})`
              : `Checking tool connections (${currentStatus.progress.current}/${currentStatus.progress.total})`,
            status: "in_progress",
            timestamp: Date.now(),
          },
        ],
        isComplete: false,
      })
    } else {
      clearInterval(progressInterval)
    }
  }, 500)

  try {
    await mcpService.initialize()
  } finally {
    clearInterval(progressInterval)
  }

  if (shouldStop()) {
    return
  }

  await emitAgentProgress({
    sessionId,
    runId,
    currentIteration: 0,
    maxIterations: effectiveMaxIterations,
    steps: [
      {
        id: `mcp_init_complete_${Date.now()}`,
        type: "thinking",
        title: "Agent tools ready",
        description: `${mcpService.getAvailableTools().length} tools available`,
        status: "completed",
        timestamp: Date.now(),
      },
    ],
    isComplete: false,
  })
}

// Unified agent mode processing function
async function processWithAgentMode(
  text: string,
  conversationId?: string,
  existingSessionId?: string, // Optional: reuse existing session instead of creating new one
  launchStateOrStartSnoozed: boolean | AgentLaunchState = false, // Defaults to foreground/panel-focused work.
  maxIterationsOverride?: number,
): Promise<string> {
  const launchState = normalizeAgentLaunchState(launchStateOrStartSnoozed)
  const { startSnoozed } = launchState
  const config = configStore.get()
  const maxIterationsFromOverride = typeof maxIterationsOverride === "number" && Number.isFinite(maxIterationsOverride)
    ? maxIterationsOverride
    : undefined
  const effectiveMaxIterations = maxIterationsFromOverride ?? (config.mcpUnlimitedIterations ? Infinity : (config.mcpMaxIterations ?? 10))
  const allProfiles = agentProfileService.getAll()
  const currentProfile = agentProfileService.getCurrentProfile()
  const existingProfileSnapshotFromSession = existingSessionId
    ? agentSessionStateManager.getSessionProfileSnapshot(existingSessionId)
      ?? agentSessionTracker.getSessionProfileSnapshot(existingSessionId)
    : undefined
  const existingProfileSnapshot = refreshSessionSnapshotSkillsFromProfile(
    existingProfileSnapshotFromSession,
    currentProfile,
  )
  const topLevelAcpSelection = resolvePreferredTopLevelAcpAgentSelection({
    currentProfile,
    sessionProfileId: existingProfileSnapshot?.profileId,
    mainAgentMode: config.mainAgentMode,
    mainAgentName: config.mainAgentName,
    profileAgents: allProfiles,
    legacyAgents: config.acpAgents || [],
  })

  // Route via ACP when the selected profile itself is ACP-backed, or when global ACP main-agent mode is enabled.
  if (topLevelAcpSelection) {
    if ("error" in topLevelAcpSelection) {
      logLLM(`[processWithAgentMode] ${topLevelAcpSelection.error}`)
      return topLevelAcpSelection.error
    }

    const resolvedMainAgentName = topLevelAcpSelection.resolvedName
    if (
      topLevelAcpSelection.source === "main-agent"
      && topLevelAcpSelection.repairedName
      && topLevelAcpSelection.repairedName !== config.mainAgentName
    ) {
      // Persist the repaired selection so future runs don't fail on stale names.
      const saveError = trySaveConfig({ ...config, mainAgentName: resolvedMainAgentName })
      if (saveError) {
        logLLM(
          `[processWithAgentMode] Failed to persist repaired ACP main agent name "${resolvedMainAgentName}": ${saveError.message}`
        )
      }
      logLLM(
        `[processWithAgentMode] ACP main agent \"${config.mainAgentName}\" not found. ` +
        `Auto-switched to \"${resolvedMainAgentName}\".`
      )
    }

    logLLM(`[processWithAgentMode] ACP routing via ${topLevelAcpSelection.source}, agent: ${resolvedMainAgentName}`)

    // Create conversation title for session tracking
    const conversationTitle = text
    const profileSnapshot = existingProfileSnapshot
      ?? (currentProfile ? createSessionSnapshotFromProfile(currentProfile) : undefined)

    // Start tracking this agent session (or reuse existing one)
    const sessionId = existingSessionId
      || agentSessionTracker.startSession(conversationId, conversationTitle, startSnoozed, profileSnapshot)
    agentSessionTracker.updateSession(sessionId, {
      isSnoozed: startSnoozed,
      suppressPanelAutoShow: launchState.shouldSuppressPanelAutoShow,
    })
    const runId = agentSessionStateManager.startSessionRun(sessionId, profileSnapshot)

    try {
      // Process with ACP agent
      const result = await processTranscriptWithACPAgent(text, {
        agentName: resolvedMainAgentName,
        conversationId: conversationId || sessionId,
        sessionId,
        runId,
        profileSnapshot,
      })

      // Mark session as completed
      if (result.success) {
        logLLM(`[processWithAgentMode] ACP mode completed successfully for session ${sessionId}, conversation ${conversationId}`)
        agentSessionTracker.completeSession(sessionId, "ACP agent completed successfully")
      } else {
        logLLM(`[processWithAgentMode] ACP mode failed for session ${sessionId}: ${result.error}`)
        agentSessionTracker.errorSession(sessionId, result.error || "Unknown error")
      }

      logLLM(`[processWithAgentMode] ACP mode returning, queue processing should trigger in .finally()`)
      return result.response || result.error || "No response from agent"
    } finally {
      agentSessionStateManager.cleanupSession(sessionId)
    }
  }

  // NOTE: Don't clear all agent progress here - we support multiple concurrent sessions
  // Each session manages its own progress lifecycle independently

  // Agent mode state is managed per-session via agentSessionStateManager

  // Determine profile snapshot for session isolation
  // If reusing an existing session, use its stored snapshot to maintain isolation
  // Only capture a new snapshot from the current global profile when creating a new session
  let profileSnapshot: SessionProfileSnapshot | undefined = existingProfileSnapshot

  // Only capture a new snapshot if we don't have one from an existing session
  if (!profileSnapshot && currentProfile) {
    profileSnapshot = createSessionSnapshotFromProfile(currentProfile)
  }

  // Start tracking this agent session (or reuse existing one)
  let conversationTitle = text
  // When creating a new session from keybind/UI, start unsnoozed unless the
  // caller explicitly requested true background/snoozed work.
  const sessionId = existingSessionId || agentSessionTracker.startSession(conversationId, conversationTitle, startSnoozed, profileSnapshot)
  agentSessionTracker.updateSession(sessionId, {
    isSnoozed: startSnoozed,
    suppressPanelAutoShow: launchState.shouldSuppressPanelAutoShow,
  })
  const runId = agentSessionStateManager.startSessionRun(sessionId, profileSnapshot)

  try {
    // Initialize MCP with progress feedback
    await initializeMcpWithProgress(config, sessionId, runId)

    // Register any existing MCP server processes with the agent process manager
    // This handles the case where servers were already initialized before agent mode was activated
    mcpService.registerExistingProcessesWithAgentManager()

    // Get available tools filtered by profile snapshot if available (for session isolation)
    // This ensures revived sessions use the same tool list they started with
    const availableTools = profileSnapshot?.mcpServerConfig
      ? mcpService.getAvailableToolsForProfile(profileSnapshot.mcpServerConfig)
      : mcpService.getAvailableTools()

    // Use agent mode for iterative tool calling
    const executeToolCall = async (toolCall: any, onProgress?: (message: string) => void): Promise<MCPToolResult> => {
      // Handle inline tool approval if enabled in config
      if (config.mcpRequireApprovalBeforeToolCall) {
        // Request approval and wait for user response via the UI
        const { approvalId, promise: approvalPromise } = toolApprovalManager.requestApproval(
          sessionId,
          toolCall.name,
          toolCall.arguments
        )

        // Emit progress update with pending approval to show approve/deny buttons
        await emitAgentProgress({
          sessionId,
          runId,
          currentIteration: 0, // Will be updated by the agent loop
          maxIterations: effectiveMaxIterations,
          steps: [],
          isComplete: false,
          pendingToolApproval: {
            approvalId,
            toolName: toolCall.name,
            arguments: toolCall.arguments,
          },
        })

        // Wait for user response
        const approved = await approvalPromise

        // Clear the pending approval from the UI by explicitly setting pendingToolApproval to undefined
        await emitAgentProgress({
          sessionId,
          runId,
          currentIteration: 0,
          maxIterations: effectiveMaxIterations,
          steps: [],
          isComplete: false,
          pendingToolApproval: undefined, // Explicitly clear to sync state across all windows
        })

        if (!approved) {
          return {
            content: [
              {
                type: "text",
                text: `Tool call denied by user: ${toolCall.name}`,
              },
            ],
            isError: true,
          }
        }
      }

      // Execute the tool call (approval either not required or was granted)
      // Pass sessionId for ACP router tools progress, and profileSnapshot.mcpServerConfig for session-aware server availability
      return await mcpService.executeToolCall(toolCall, onProgress, true, sessionId, profileSnapshot?.mcpServerConfig)
    }

    // Load previous conversation history if continuing a conversation.
    // IMPORTANT: Load this BEFORE emitting initial progress to ensure consistency.
    // For long conversations, compact on load to persist a stable checkpoint while
    // still replaying preserved raw messages to the prompt builder when available.
    let previousConversationHistory:
      | Array<{
          role: "user" | "assistant" | "tool"
          content: string
          displayContent?: string
          toolCalls?: any[]
          toolResults?: any[]
          timestamp?: number
          branchMessageIndex?: number
        }>
      | undefined
    let previousConversationCompaction: ConversationCompactionMetadata | undefined

    if (conversationId) {
      logLLM(`[tipc.ts processWithAgentMode] Loading conversation history for conversationId: ${conversationId}`)
      const conversation = await conversationService.loadConversationWithCompaction(conversationId, sessionId)

      if (conversation && conversation.messages.length > 0) {
        logLLM(`[tipc.ts processWithAgentMode] Loaded conversation with ${conversation.messages.length} messages`)
        previousConversationCompaction = conversation.compaction

        // Convert conversation messages to the format expected by agent mode
        // Exclude the last message since it's the current user input that will be added
        const replayMessages = Array.isArray(conversation.rawMessages) && conversation.rawMessages.length > 0
          ? conversation.rawMessages
          : conversation.messages
        const messagesToConvert = replayMessages.slice(0, -1)
        const branchMessageIndexMap = getBranchMessageIndexMap(messagesToConvert)
        logLLM(`[tipc.ts processWithAgentMode] Converting ${messagesToConvert.length} messages (excluding last message)`)
        previousConversationHistory = messagesToConvert.map((msg, index) => ({
          role: msg.role,
          content: msg.content,
          ...(msg.displayContent ? { displayContent: msg.displayContent } : {}),
          toolCalls: msg.toolCalls,
          timestamp: msg.timestamp,
          // Convert toolResults from stored format (content as string) to MCPToolResult format (content as array)
          toolResults: msg.toolResults?.map((tr) => ({
            content: [
              {
                type: "text" as const,
                // Use content for successful results, error message for failures
                text: tr.success ? tr.content : (tr.error || tr.content),
              },
            ],
            isError: !tr.success,
          })),
          branchMessageIndex: branchMessageIndexMap[index],
        }))

        logLLM(`[tipc.ts processWithAgentMode] previousConversationHistory roles: [${previousConversationHistory.map(m => m.role).join(', ')}]`)
      } else {
        logLLM(`[tipc.ts processWithAgentMode] No conversation found or conversation is empty`)
      }
    } else {
      logLLM(`[tipc.ts processWithAgentMode] No conversationId provided, starting fresh conversation`)
    }

    // Focus only callers that explicitly want panel session selection. A tile
    // send can be foreground/audible without forcing the floating panel open.
    if (launchState.shouldFocusPanelSession) {
      try {
        getWindowRendererHandlers("panel")?.focusAgentSession.send(sessionId)
      } catch (e) {
        logApp("[tipc] Failed to focus new agent session:", e)
      }
    }

    const agentResult = await processTranscriptWithAgentMode(
      text,
      availableTools,
      executeToolCall,
      effectiveMaxIterations, // Use configured max iterations or Infinity if unlimited mode
      previousConversationHistory,
      conversationId, // Pass conversation ID for linking to conversation history
      sessionId, // Pass session ID for progress routing and isolation
      undefined, // onProgress callback (not used here, progress is emitted via emitAgentProgress)
      profileSnapshot, // Pass profile snapshot for session isolation
      runId,
      previousConversationCompaction,
    )

    // Mark session as completed
    agentSessionTracker.completeSession(sessionId, "Agent completed successfully")

    return agentResult.content
  } catch (error) {
    // Mark session as errored
    const errorMessage = getErrorMessage(error)
    agentSessionTracker.errorSession(sessionId, errorMessage)

    // Emit error progress update to the UI so users see the error message
    await emitAgentProgress({
      sessionId,
      runId,
      conversationId: conversationId || "",
      conversationTitle: conversationTitle,
      currentIteration: 1,
      maxIterations: effectiveMaxIterations,
      steps: [{
        id: `error_${Date.now()}`,
        type: "thinking",
        title: "Error",
        description: errorMessage,
        status: "error",
        timestamp: Date.now(),
      }],
      isComplete: true,
      finalContent: `Error: ${errorMessage}`,
      // Intentionally omit conversationHistory: the renderer agent-store
      // preserves existingProgress.conversationHistory when this field is
      // missing, so the user's prior transcript remains visible in the UI.
      // Emitting a 2-item synthetic history here would clobber it.
    })

    throw error
  } finally {

  }
}

export async function runAgentLoopSession(
  text: string,
  conversationId: string,
  existingSessionId: string,
  startSnoozed: boolean = true,
  maxIterationsOverride?: number,
): Promise<string> {
  return processWithAgentMode(text, conversationId, existingSessionId, startSnoozed, maxIterationsOverride)
}
import { diagnosticsService } from "./diagnostics"
import { knowledgeNotesService } from "./knowledge-notes-service"
import { summarizationService } from "./summarization-service"
import { updateTrayIcon } from "./tray"
import { isAccessibilityGranted } from "./utils"
import { writeText, writeTextWithFocusRestore } from "./keyboard"

type AgentLaunchStateInput = {
  /** Origin hint: the action came from an in-app/session tile. */
  fromTile?: boolean
  /** True background mode: keep the session quiet/hidden and disable TTS autoplay. */
  startSnoozed?: boolean
  /** Prevent floating-panel auto-show without implying background/snoozed work. */
  suppressPanelAutoShow?: boolean
  /** Select this session in the panel renderer when it starts. */
  focusPanelSession?: boolean
}

type AgentLaunchState = {
  startSnoozed: boolean
  shouldSuppressPanelAutoShow: boolean
  shouldFocusPanelSession: boolean
}

function resolveAgentLaunchState(input: AgentLaunchStateInput = {}): AgentLaunchState {
  const startSnoozed = input.startSnoozed ?? false
  const shouldSuppressPanelAutoShow =
    startSnoozed || input.suppressPanelAutoShow === true || input.fromTile === true
  const shouldFocusPanelSession =
    input.focusPanelSession ?? (!startSnoozed && !shouldSuppressPanelAutoShow)

  return {
    startSnoozed,
    shouldSuppressPanelAutoShow,
    shouldFocusPanelSession,
  }
}

function normalizeAgentLaunchState(
  launchStateOrStartSnoozed: boolean | AgentLaunchState = false,
): AgentLaunchState {
  if (typeof launchStateOrStartSnoozed === "boolean") {
    return resolveAgentLaunchState({
      startSnoozed: launchStateOrStartSnoozed,
      suppressPanelAutoShow: launchStateOrStartSnoozed,
      focusPanelSession: !launchStateOrStartSnoozed,
    })
  }

  return launchStateOrStartSnoozed
}

function serializeAgentLaunchState(launchState: AgentLaunchState) {
  return {
    startSnoozed: launchState.startSnoozed,
    suppressPanelAutoShow: launchState.shouldSuppressPanelAutoShow,
    focusPanelSession: launchState.shouldFocusPanelSession,
  }
}



const t = tipc.create()

type ScreenshotAttachmentInput = { name?: string; dataUrl: string }

function escapeMarkdownImageAlt(text: string): string {
  return text.replace(/[\[\]]/g, "")
}

function appendScreenshotToTranscript(transcript: string, screenshot?: ScreenshotAttachmentInput): string {
  if (!screenshot?.dataUrl) return transcript

  const alt = escapeMarkdownImageAlt(screenshot.name || "Screen selection") || "Screen selection"
  const screenshotMarkdown = `![${alt}](${screenshot.dataUrl})`
  return transcript ? `${transcript}\n\n${screenshotMarkdown}` : screenshotMarkdown
}

function getLatestStoredUserMessageContent(conversation: Conversation | null, fallback: string): string {
  const latestUserMessage = conversation?.messages
    ?.slice()
    .reverse()
    .find((message) => message.role === "user")
  return latestUserMessage?.content || fallback
}

const getRecordingHistory = () => {
  try {
    const history = JSON.parse(
      fs.readFileSync(path.join(recordingsFolder, "history.json"), "utf8"),
    ) as RecordingHistoryItem[]

    // sort desc by createdAt
    return history.sort((a, b) => b.createdAt - a.createdAt)
  } catch {
    return []
  }
}

const saveRecordingsHitory = (history: RecordingHistoryItem[]) => {
  fs.writeFileSync(
    path.join(recordingsFolder, "history.json"),
    JSON.stringify(history),
  )
}

async function refreshRuntimeAfterBundleImport(): Promise<void> {
  try {
    configStore.reload()
  } catch (error) {
    logApp("[tipc] Failed to reload config after bundle load", { error })
  }

  try {
    agentProfileService.reload()
  } catch (error) {
    logApp("[tipc] Failed to reload agent profiles after bundle load", { error })
  }

  try {
    const { skillsService } = await import("./skills-service")
    skillsService.scanSkillsFolder()
  } catch (error) {
    logApp("[tipc] Failed to reload skills after bundle load", { error })
  }

  try {
    loopService.stopAllLoops()
    loopService.reload()
    loopService.resumeScheduling()
    loopService.startAllLoops()
  } catch (error) {
    logApp("[tipc] Failed to reload repeat tasks after bundle load", { error })
    // Avoid leaving loop scheduling paused if stopAllLoops() succeeded but reload/start failed.
    try {
      loopService.resumeScheduling()
    } catch {
      // best-effort
    }
  }

  try {
    await knowledgeNotesService.reload()
  } catch (error) {
    logApp("[tipc] Failed to reload knowledge notes after bundle load", { error })
  }

  try {
    const config = configStore.get()
    const configuredServers = config.mcpConfig?.mcpServers || {}
    const serverStatusBeforeRefresh = mcpService.getServerStatus()

    // Stop servers that were removed/disabled by the imported bundle.
    for (const [serverName, status] of Object.entries(serverStatusBeforeRefresh)) {
      const serverConfig = configuredServers[serverName]
      const shouldBeStopped = !serverConfig || !!(serverConfig as MCPServerConfig).disabled
      if (!shouldBeStopped) continue

      const stopResult = await mcpService.stopServer(serverName)
      if (!stopResult.success) {
        logApp("[tipc] Failed to stop MCP server after bundle load", {
          serverName,
          error: stopResult.error,
          wasConnected: status.connected,
        })
      }
    }

    const serverStatusAfterStops = mcpService.getServerStatus()

    // Restart currently connected enabled servers so overwritten configs take effect immediately.
    for (const [serverName, serverConfig] of Object.entries(configuredServers)) {
      if ((serverConfig as MCPServerConfig).disabled) continue
      const status = serverStatusAfterStops[serverName]
      if (status?.runtimeEnabled === false) continue
      if (!status?.connected) continue

      const restartResult = await mcpService.restartServer(serverName)
      if (!restartResult.success) {
        logApp("[tipc] Failed to restart MCP server after bundle load", {
          serverName,
          error: restartResult.error,
        })
      }
    }

    // Start any newly imported enabled servers that were previously absent.
    await mcpService.initialize()
  } catch (error) {
    logApp("[tipc] Failed to reinitialize MCP after bundle load", { error })
  }
}

type BundleConflictItem = {
  id: string
  name: string
  existingName?: string
}

type BundleConflictMap = {
  agentProfiles: BundleConflictItem[]
  mcpServers: BundleConflictItem[]
  skills: BundleConflictItem[]
  repeatTasks: BundleConflictItem[]
  knowledgeNotes: BundleConflictItem[]
}

type BundleConflictPreview = {
  success: boolean
  conflicts?: BundleConflictMap
}

const BUNDLE_CONFLICT_KEYS: Array<keyof BundleConflictMap> = [
  "agentProfiles",
  "mcpServers",
  "skills",
  "repeatTasks",
  "knowledgeNotes",
]

function mergeConflictItems(
  primary: BundleConflictItem[] | undefined,
  secondary: BundleConflictItem[] | undefined
): BundleConflictItem[] {
  const merged = new Map<string, BundleConflictItem>()

  for (const item of primary || []) {
    if (!item?.id) continue
    merged.set(item.id, item)
  }
  for (const item of secondary || []) {
    if (!item?.id) continue
    merged.set(item.id, item)
  }

  return Array.from(merged.values())
}

function mergeConflictMaps(
  primary: BundleConflictMap | undefined,
  secondary: BundleConflictMap | undefined
): BundleConflictMap | undefined {
  if (!primary && !secondary) return undefined

  const merged: BundleConflictMap = {
    agentProfiles: [],
    mcpServers: [],
    skills: [],
    repeatTasks: [],
    knowledgeNotes: [],
  }

  for (const key of BUNDLE_CONFLICT_KEYS) {
    merged[key] = mergeConflictItems(primary?.[key], secondary?.[key])
  }

  return merged
}

/**
 * Process queued messages for a conversation after the current session completes.
 * This function peeks at messages and only removes them after successful processing.
 * Uses a per-conversation lock to prevent concurrent processing of the same queue.
 */
async function processQueuedMessages(conversationId: string): Promise<void> {
  logLLM(`[processQueuedMessages] Starting queue processing for ${conversationId}`)

  // Try to acquire processing lock - if another processor is already running, skip
  if (!messageQueueService.tryAcquireProcessingLock(conversationId)) {
    logLLM(`[processQueuedMessages] Failed to acquire lock for ${conversationId}`)
    return
  }
  logLLM(`[processQueuedMessages] Acquired lock for ${conversationId}`)

  try {
    while (true) {
      // Check if queue is paused (e.g., by kill switch) before processing next message
      if (messageQueueService.isQueuePaused(conversationId)) {
        logLLM(`[processQueuedMessages] Queue is paused for ${conversationId}, stopping processing`)
        return
      }

      // Peek at the next message without removing it
      const queuedMessage = messageQueueService.peek(conversationId)
      if (!queuedMessage) {
        logLLM(`[processQueuedMessages] No more pending messages in queue for ${conversationId}`)
        // Debug: log the actual queue state
        const allMessages = messageQueueService.getQueue(conversationId)
        if (allMessages.length > 0) {
          logLLM(`[processQueuedMessages] Queue has ${allMessages.length} messages but peek returned null. First message status: ${allMessages[0]?.status}`)
        }
        return // No more messages in queue
      }

      logLLM(`[processQueuedMessages] Processing queued message ${queuedMessage.id} for ${conversationId}`)

      // Mark as processing - if this fails, the message was removed/modified between peek and now
      const markingSucceeded = messageQueueService.markProcessing(conversationId, queuedMessage.id)
      if (!markingSucceeded) {
        logLLM(`[processQueuedMessages] Message ${queuedMessage.id} was removed/modified before processing, re-checking queue`)
        continue
      }

      try {
        if (queuedMessage.sessionId?.startsWith("subsession_")) {
          const { continueInternalSubSession } = await import("./acp/internal-agent")
          const result = await continueInternalSubSession(queuedMessage.sessionId, queuedMessage.text)
          if (!result.success) {
            throw new Error(result.error || `Failed to continue internal sub-session ${queuedMessage.sessionId}`)
          }

          messageQueueService.markProcessed(conversationId, queuedMessage.id)
          continue
        }

        // Only add to conversation history if not already added (prevents duplicates on retry)
        if (!queuedMessage.addedToHistory) {
          // Add the queued message to the conversation
          const addResult = await conversationService.addMessageToConversation(
            conversationId,
            queuedMessage.text,
            "user",
          )
          // If adding to history failed (conversation not found/IO error), treat as failure
          // Don't continue processing since the message wasn't recorded
          if (!addResult) {
            throw new Error("Failed to add message to conversation history")
          }
          // Mark as added to history so retries don't duplicate
          messageQueueService.markAddedToHistory(conversationId, queuedMessage.id)
        }

        // Preserve the launch semantics from the UI action that queued this
        // message. Falling back to panel visibility is only for older/untyped
        // enqueue sites that did not capture launch state.
        const panelWindow = WINDOWS.get("panel")
        const isPanelVisible = panelWindow?.isVisible() ?? false
        const queuedLaunchState = queuedMessage.launchState
          ? resolveAgentLaunchState(queuedMessage.launchState)
          : resolveAgentLaunchState({
            startSnoozed: !isPanelVisible,
            suppressPanelAutoShow: !isPanelVisible,
            focusPanelSession: isPanelVisible,
          })
        if (queuedLaunchState.shouldSuppressPanelAutoShow) {
          suppressPanelAutoShow(2000)
        }
        logLLM(`[processQueuedMessages] Panel visible: ${isPanelVisible}, launchState: ${JSON.stringify(serializeAgentLaunchState(queuedLaunchState))}`)

        // Prefer the exact session captured at enqueue time for strict same-session semantics.
        // If revive fails, fall back to conversation lookup for backward compatibility and continuity.
        let existingSessionId: string | undefined
        const fallbackSessionId = agentSessionTracker.findSessionByConversationId(conversationId)
        const candidateSessionIds = [queuedMessage.sessionId, fallbackSessionId].filter(
          (sessionId, index, list): sessionId is string =>
            typeof sessionId === "string" && sessionId.length > 0 && list.indexOf(sessionId) === index,
        )

        for (const candidateSessionId of candidateSessionIds) {
          const revived = agentSessionTracker.reviveSession(candidateSessionId, queuedLaunchState.startSnoozed)
          if (revived) {
            existingSessionId = candidateSessionId
            logLLM(`[processQueuedMessages] Revived session ${existingSessionId} for conversation ${conversationId}, snoozed: ${queuedLaunchState.startSnoozed}`)
            break
          }

          if (candidateSessionId === queuedMessage.sessionId) {
            logLLM(`[processQueuedMessages] Preferred queued session ${candidateSessionId} could not be revived, trying fallback lookup`)
          }
        }

        await processWithAgentMode(queuedMessage.text, conversationId, existingSessionId, queuedLaunchState)

        // Only remove the message after successful processing
        messageQueueService.markProcessed(conversationId, queuedMessage.id)

        // Continue to check for more queued messages
      } catch (error) {
        logLLM(`[processQueuedMessages] Error processing queued message ${queuedMessage.id}:`, error)
        // Mark the message as failed so users can see it in the UI
        const errorMessage = getErrorMessage(error)
        messageQueueService.markFailed(conversationId, queuedMessage.id, errorMessage)
        // Stop processing - user needs to handle the failed message
        break
      }
    }
  } finally {
    // Always release the lock when done
    messageQueueService.releaseProcessingLock(conversationId)
  }
}

type OpenFileResult = {
  success: boolean
  error?: string
  path?: string
}

function revealFileInFolder(filePath: string): OpenFileResult {
  if (!fs.existsSync(filePath)) {
    return { success: false, path: filePath, error: "File does not exist" }
  }

  try {
    shell.showItemInFolder(filePath)
    return { success: true, path: filePath }
  } catch (error) {
    return {
      success: false,
      path: filePath,
      error: error instanceof Error ? error.message : String(error),
    }
  }
}

function broadcastTTSPlaybackState(state: DesktopTTSPlaybackState): number {
  let windowsNotified = 0
  logApp("[tipc][TTS] Broadcasting playback state", {
    playbackId: state.playbackId,
    status: state.status,
    sessionId: state.sessionId,
    source: state.source,
    totalWindows: WINDOWS.size,
    error: state.error,
  })
  for (const [id, win] of WINDOWS.entries()) {
    try {
      const handler = getRendererHandlers<RendererHandlers>(win.webContents).ttsPlaybackStateChanged
      if (!handler) {
        logApp(`[tipc] ttsPlaybackStateChanged handler missing for ${id}`)
        continue
      }
      handler.send(state)
      windowsNotified += 1
    } catch (e) {
      logApp(`[tipc] ttsPlaybackStateChanged send to ${id} failed:`, e)
    }
  }
  return windowsNotified
}

function sendTTSPlaybackCommandToHost(command: DesktopTTSPlaybackCommand): boolean {
  const main = WINDOWS.get("main")
  logApp("[tipc][TTS] Routing playback command to main host", {
    command,
    mainAvailable: !!main,
  })
  if (!main) {
    logApp("[tipc] Main window unavailable for TTS playback command", { command })
    return false
  }

  try {
    const handler = getRendererHandlers<RendererHandlers>(main.webContents).ttsPlaybackCommand
    if (!handler) {
      logApp("[tipc] Main renderer has no TTS playback command handler", { command })
      return false
    }
    handler.send(command)
    return true
  } catch (error) {
    logApp("[tipc] Failed to route TTS playback command to main renderer:", error)
    return false
  }
}

function sendTTSPlaybackRequestToHost(request: DesktopTTSPlaybackRequest): boolean {
  const main = WINDOWS.get("main")
  logApp("[tipc][TTS] Routing playback request to main host", {
    playbackId: request.playbackId,
    sessionId: request.sessionId,
    source: request.source,
    autoPlay: request.autoPlay,
    keyCount: request.ttsKeys?.length ?? 0,
    audioByteLength: request.audio instanceof ArrayBuffer
      ? request.audio.byteLength
      : ArrayBuffer.isView(request.audio)
        ? request.audio.byteLength
        : Array.isArray(request.audio)
          ? request.audio.length
          : undefined,
    mainAvailable: !!main,
  })
  if (!main) {
    logApp("[tipc] Main window unavailable for TTS playback request", {
      playbackId: request.playbackId,
      sessionId: request.sessionId,
    })
    return false
  }

  try {
    const handler = getRendererHandlers<RendererHandlers>(main.webContents).ttsPlaybackRequest
    if (!handler) {
      logApp("[tipc] Main renderer has no TTS playback request handler", {
        playbackId: request.playbackId,
        sessionId: request.sessionId,
      })
      return false
    }
    handler.send(request)
    return true
  } catch (error) {
    logApp("[tipc] Failed to route TTS playback request to main renderer:", error)
    return false
  }
}

export const router = {
  restartApp: t.procedure.action(async () => {
    app.relaunch()
    app.quit()
  }),

  getUpdateInfo: t.procedure.action(async () => {
    const { getUpdateInfo } = await import("./updater")
    return getUpdateInfo()
  }),

  quitAndInstall: t.procedure.action(async () => {
    const { quitAndInstall } = await import("./updater")

    quitAndInstall()
  }),

  checkForUpdatesAndDownload: t.procedure.action(async () => {
    const { checkForUpdatesAndDownload } = await import("./updater")

    return checkForUpdatesAndDownload()
  }),

  openMicrophoneInSystemPreferences: t.procedure.action(async () => {
    await shell.openExternal(
      "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone",
    )
  }),

  hidePanelWindow: t.procedure.action(async () => {
    logApp("[hidePanelWindow] Hiding floating panel", {
      panelMode: getCurrentPanelMode(),
      panelVisible: WINDOWS.get("panel")?.isVisible?.() ?? false,
      textInputActive: state.isTextInputActive,
    })
    hideFloatingPanelWindow()
  }),

  getFloatingPanelVisibility: t.procedure.action(async () => {
    return { visible: WINDOWS.get("panel")?.isVisible?.() ?? false }
  }),

  snoozeAgentSessionsAndHidePanelWindow: t.procedure
    .input<{ sessionIds?: string[] }>()
    .action(async ({ input }) => {
      snoozeAgentSessionsAndHidePanelWindow(input.sessionIds)
      return { success: true }
    }),

  resetFloatingPanel: t.procedure.action(async () => {
    resetFloatingPanelPositionAndSize(true)
    return { success: true }
  }),

  resizePanelForAgentMode: t.procedure.action(async () => {
    resizePanelForAgentMode()
  }),

  resizePanelToNormal: t.procedure.action(async () => {
    resizePanelToNormal()
  }),

  resizePanelForWaveformPreview: t.procedure
    .input<{ showPreview: boolean }>()
    .action(async ({ input }) => {
      resizePanelForWaveformPreview(input.showPreview)
    }),

  setPanelMode: t.procedure
    .input<{ mode: "normal" | "agent" | "textInput" }>()
    .action(async ({ input }) => {
      setPanelMode(input.mode)
      return { success: true }
    }),

  /**
   * Set the focusability of the panel window.
   * Used to enable input interaction when agent has completed or when user wants to queue messages.
   * @param focusable - Whether the panel should be focusable
   * @param andFocus - If true and focusable is true, also focus the window (needed for macOS)
   */
  setPanelFocusable: t.procedure
    .input<{ focusable: boolean; andFocus?: boolean }>()
    .action(async ({ input }) => {
      setPanelFocusable(input.focusable, input.andFocus ?? false)
      return { success: true }
    }),

  debugPanelState: t.procedure.action(async () => {
    const panel = WINDOWS.get("panel")
    const state = {
      exists: !!panel,
      isVisible: panel?.isVisible() || false,
      isDestroyed: panel?.isDestroyed() || false,
      bounds: panel?.getBounds() || null,
      isAlwaysOnTop: panel?.isAlwaysOnTop() || false,
    }
    return state
  }),

  // Panel position management
  setPanelPosition: t.procedure
    .input<{ position: PanelPosition }>()
    .action(async ({ input }) => {
      updatePanelPosition(input.position)

      // Update the panel position if it's currently visible
      const panel = WINDOWS.get("panel")
      if (panel && panel.isVisible()) {
        showPanelWindow()
      }
    }),

  savePanelCustomPosition: t.procedure
    .input<{ x: number; y: number }>()
    .action(async ({ input }) => {
      // Get current panel size to constrain position
      const panel = WINDOWS.get("panel")
      if (panel) {
        const bounds = panel.getBounds()
        const constrainedPosition = constrainPositionToScreen(
          { x: input.x, y: input.y },
          { width: bounds.width, height: bounds.height },
        )

        saveCustomPosition(constrainedPosition)

        // Update the panel position immediately
        panel.setPosition(constrainedPosition.x, constrainedPosition.y)
      }
    }),

  updatePanelPosition: t.procedure
    .input<{ x: number; y: number }>()
    .action(async ({ input }) => {
      const panel = WINDOWS.get("panel")
      if (panel) {
        const bounds = panel.getBounds()
        const constrainedPosition = constrainPositionToScreen(
          { x: input.x, y: input.y },
          { width: bounds.width, height: bounds.height },
        )

        panel.setPosition(constrainedPosition.x, constrainedPosition.y)
      }
    }),

  getPanelPosition: t.procedure.action(async () => {
    const panel = WINDOWS.get("panel")
    if (panel) {
      const bounds = panel.getBounds()
      return { x: bounds.x, y: bounds.y }
    }
    return { x: 0, y: 0 }
  }),

  emergencyStopAgent: t.procedure.action(async () => {
    await emergencyStopAgentMode()

    return { success: true, message: "Agent mode emergency stopped" }
  }),

  claimTTSPlaybackKeys: t.procedure
    .input<{ ttsKeys?: string[]; sessionId?: string; forced?: boolean }>()
    .action(async ({ input }) => {
      logApp("[tipc][TTS] claimTTSPlaybackKeys", {
        sessionId: input.sessionId,
        forced: input.forced ?? false,
        keys: input.ttsKeys,
      })
      const claimed = desktopTTSPlaybackCoordinator.claimAutoPlayKeys(input.ttsKeys, {
        sessionId: input.sessionId,
        forced: input.forced ?? false,
      })
      logApp("[tipc][TTS] claimTTSPlaybackKeys result", {
        claimed,
        sessionId: input.sessionId,
      })
      return { claimed }
    }),

  releaseTTSPlaybackKeys: t.procedure
    .input<{ ttsKeys?: string[] }>()
    .action(async ({ input }) => {
      logApp("[tipc][TTS] releaseTTSPlaybackKeys", { keys: input.ttsKeys })
      desktopTTSPlaybackCoordinator.releaseAutoPlayKeys(input.ttsKeys)
      return { success: true }
    }),

  getTTSPlaybackState: t.procedure.action(async () => {
    return desktopTTSPlaybackCoordinator.getState()
  }),

  requestTTSPlayback: t.procedure
    .input<DesktopTTSPlaybackRequest>()
    .action(async ({ input }) => {
      logApp("[tipc][TTS] requestTTSPlayback received", {
        playbackId: input.playbackId,
        sessionId: input.sessionId,
        source: input.source,
        autoPlay: input.autoPlay,
        keyCount: input.ttsKeys?.length ?? 0,
        mimeType: input.mimeType,
        audioOutputDeviceId: input.audioOutputDeviceId,
      })
      const state = desktopTTSPlaybackCoordinator.setState({
        playbackId: input.playbackId,
        sourceWindowId: input.sourceWindowId,
        source: input.source,
        sessionId: input.sessionId,
        ttsKey: input.ttsKeys?.[0],
        textPreview: input.textPreview ?? input.text.slice(0, 120),
        status: "loading",
        currentTime: 0,
        duration: 0,
        audioOutputDeviceId: input.audioOutputDeviceId,
        error: undefined,
      })
      broadcastTTSPlaybackState(state)

      const routed = sendTTSPlaybackRequestToHost(input)
      if (!routed) {
        const failedState = desktopTTSPlaybackCoordinator.setState({
          playbackId: input.playbackId,
          status: "error",
          error: "Main playback host is unavailable",
        })
        broadcastTTSPlaybackState(failedState)
        logApp("[tipc][TTS] requestTTSPlayback failed to route", {
          playbackId: input.playbackId,
          sessionId: input.sessionId,
        })
        return { success: false, error: failedState.error }
      }

      logApp("[tipc][TTS] requestTTSPlayback routed", {
        playbackId: input.playbackId,
        sessionId: input.sessionId,
      })
      return { success: true }
    }),

  controlTTSPlayback: t.procedure
    .input<DesktopTTSPlaybackCommand>()
    .action(async ({ input }) => {
      logApp("[tipc][TTS] controlTTSPlayback received", { command: input })
      const routed = sendTTSPlaybackCommandToHost(input)

      if (input.type === "stop" || !routed) {
        const state = input.type === "stop"
          ? desktopTTSPlaybackCoordinator.reset(input.reason)
          : desktopTTSPlaybackCoordinator.setState({ status: "error", error: "Main playback host is unavailable" })
        broadcastTTSPlaybackState(state)
      }

      logApp("[tipc][TTS] controlTTSPlayback routed", { command: input, routed })
      return { success: routed }
    }),

  publishTTSPlaybackState: t.procedure
    .input<DesktopTTSPlaybackState>()
    .action(async ({ input }) => {
      logApp("[tipc][TTS] publishTTSPlaybackState received", {
        playbackId: input.playbackId,
        status: input.status,
        sessionId: input.sessionId,
        source: input.source,
        currentTime: input.currentTime,
        duration: input.duration,
        error: input.error,
      })
      const state = desktopTTSPlaybackCoordinator.setState(input)
      const windowsNotified = broadcastTTSPlaybackState(state)
      return { success: true, windowsNotified }
    }),

  stopAllTts: t.procedure.action(async () => {
    sendTTSPlaybackCommandToHost({ type: "stop", reason: "stopAllTts" })
    const state = desktopTTSPlaybackCoordinator.reset("stopAllTts")
    broadcastTTSPlaybackState(state)

    let windowsNotified = 0
    for (const [id, win] of WINDOWS.entries()) {
      try {
        const stopAllTtsHandler = getRendererHandlers<RendererHandlers>(win.webContents).stopAllTts
        if (!stopAllTtsHandler) continue
        stopAllTtsHandler.send()
        windowsNotified += 1
      } catch (e) {
        logApp(`[tipc] stopAllTts send to ${id} failed:`, e)
      }
    }

    logApp("[tipc] stopAllTts broadcast complete", {
      windowsNotified,
      totalWindows: WINDOWS.size,
    })
    return { success: true, windowsNotified }
  }),

  clearAgentProgress: t.procedure.action(async () => {
    // Send to all windows so both main and panel can update their state
    for (const [id, win] of WINDOWS.entries()) {
      try {
        getRendererHandlers<RendererHandlers>(win.webContents).clearAgentProgress.send()
      } catch (e) {
        logApp(`[tipc] clearAgentProgress send to ${id} failed:`, e)
      }
    }

    return { success: true }
  }),

  clearAgentSessionProgress: t.procedure
    .input<{ sessionId: string }>()
    .action(async ({ input }) => {
      // Session is being explicitly dismissed from UI; clear persisted
      // respond_to_user state for this session.
      clearSessionUserResponse(input.sessionId)
      desktopTTSPlaybackCoordinator.clearSessionKeys(input.sessionId)

      // Also remove from the tracker's completed sessions list so it
      // doesn't re-appear in the sidebar on the next agentSessionsUpdated.
      agentSessionTracker.removeCompletedSession(input.sessionId)

      // Send to all windows (panel and main) so both can update their state
      for (const [id, win] of WINDOWS.entries()) {
        try {
          getRendererHandlers<RendererHandlers>(win.webContents).clearAgentSessionProgress?.send(input.sessionId)
        } catch (e) {
          logApp(`[tipc] clearAgentSessionProgress send to ${id} failed:`, e)
        }
      }
      return { success: true }
    }),

  clearInactiveSessions: t.procedure.action(async () => {
    // Clear completed sessions from the tracker
    agentSessionTracker.clearCompletedSessions((session) => {
      if (!session.conversationId) return true
      const shouldClear = messageQueueService.getQueue(session.conversationId).length === 0
      if (shouldClear) desktopTTSPlaybackCoordinator.clearSessionKeys(session.id)
      return shouldClear
    })

    // Send to all windows so both main and panel can update their state
    for (const [id, win] of WINDOWS.entries()) {
      try {
        getRendererHandlers<RendererHandlers>(win.webContents).clearInactiveSessions?.send()
      } catch (e) {
        logApp(`[tipc] clearInactiveSessions send to ${id} failed:`, e)
      }
    }

    return { success: true }
  }),

  closeAgentModeAndHidePanelWindow: t.procedure.action(async () => {
    closeAgentModeAndHidePanelWindow()
    return { success: true }
  }),

  getAgentStatus: t.procedure.action(async () => {
    return {
      isAgentModeActive: state.isAgentModeActive,
      shouldStopAgent: state.shouldStopAgent,
      agentIterationCount: state.agentIterationCount,
      activeProcessCount: agentProcessManager.getActiveProcessCount(),
    }
  }),

  getAgentSessions: t.procedure.action(async () => {
      reconcileAgentSessionsWithRuntime()
      const activeSessions = await Promise.all(
        agentSessionTracker.getActiveSessions().map(withRepeatTaskSessionFlag),
      )
      const recentCompletedSessions = await Promise.all(
        agentSessionTracker.getRecentSessions(4).map(withRepeatTaskSessionFlag),
      )
      return {
      activeSessions,
      recentCompletedSessions,
      // Backward-compatible alias while renderer code migrates.
      recentSessions: recentCompletedSessions,
    }
  }),

  // List active + recent completed sessions for UI pickers (e.g. repeat-task
  // "continue from session" selector). Returns the shape the picker needs,
  // tolerant of an optional limit for completed sessions.
  listAgentSessionCandidates: t.procedure
    .input<{ limit?: number } | undefined>()
    .action(async ({ input }) => {
      // Guard against NaN / non-finite inputs; fall back to the default of 20.
      const rawLimit = input?.limit
      const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit)
        ? Math.max(1, Math.min(100, Math.floor(rawLimit)))
        : 20
      reconcileAgentSessionsWithRuntime()
      const active = agentSessionTracker.getActiveSessions().map(s => ({
        id: s.id,
        conversationId: s.conversationId,
        conversationTitle: s.conversationTitle,
        status: s.status,
        startTime: s.startTime,
        endTime: s.endTime,
      }))
      const completed = agentSessionTracker.getRecentSessions(limit).map(s => ({
        id: s.id,
        conversationId: s.conversationId,
        conversationTitle: s.conversationTitle,
        status: s.status,
        startTime: s.startTime,
        endTime: s.endTime,
      }))
      return { activeSessions: active, completedSessions: completed }
    }),

  // Get the profile snapshot for a specific session
  // This allows the UI to display which profile a session is using
  getSessionProfileSnapshot: t.procedure
    .input<{ sessionId: string }>()
    .action(async ({ input }) => {
      return agentSessionStateManager.getSessionProfileSnapshot(input.sessionId)
        ?? agentSessionTracker.getSessionProfileSnapshot(input.sessionId)
    }),

  stopAgentSession: t.procedure
    .input<{ sessionId: string }>()
    .action(async ({ input }) => {
      const requestedSessionId = input.sessionId
      const requestedSessionKind = describeAgentSessionId(requestedSessionId)
      const mappedAppSessionId = getAppSessionForAcpSession(requestedSessionId)
      const trackedSession = agentSessionTracker.getSession(requestedSessionId)
      const mappedTrackedSession = mappedAppSessionId
        ? agentSessionTracker.getSession(mappedAppSessionId)
        : undefined

      logApp("[stopAgentSession] Stop requested", {
        requestedSessionId,
        requestedSessionKind,
        mappedAppSessionId: mappedAppSessionId ?? null,
        trackerSessionFound: Boolean(trackedSession),
        trackerSessionStatus: trackedSession?.status ?? null,
        trackerConversationId: trackedSession?.conversationId ?? null,
        mappedTrackerSessionFound: Boolean(mappedTrackedSession),
        mappedTrackerSessionStatus: mappedTrackedSession?.status ?? null,
        mappedTrackerConversationId: mappedTrackedSession?.conversationId ?? null,
      })

      // Stop the session in the state manager (aborts LLM requests, kills processes)
      agentSessionStateManager.stopSession(input.sessionId)

      // Cancel any pending tool approvals for this session so executeToolCall doesn't hang
      toolApprovalManager.cancelSessionApprovals(input.sessionId)

      const conversationIdsToPause = new Set<string>()
      let requestedSubSessionConversationId: string | undefined

      // Cancel any internal sub-sessions spawned by this session
      try {
        const {
          getChildSubSessions,
          getAllSubSessionsForParent,
          cancelSubSession,
          getInternalSubSession,
        } = await import("./acp/internal-agent")
        requestedSubSessionConversationId = requestedSessionKind === "subsession"
          ? getInternalSubSession(requestedSessionId)?.conversationId
          : undefined
        if (requestedSubSessionConversationId) {
          conversationIdsToPause.add(requestedSubSessionConversationId)
        }
        const cancelledRequestedSubSession = requestedSessionKind === "subsession"
          ? cancelSubSession(requestedSessionId)
          : false
        const childSessionsById = new Map(
          [...getChildSubSessions(input.sessionId), ...getAllSubSessionsForParent(input.sessionId)]
            .map((child) => [child.id, child] as const),
        )
        const childSessions = Array.from(childSessionsById.values())
        const runningChildSessionIds = childSessions
          .filter((child) => child.status === "running")
          .map((child) => child.id)
        for (const child of childSessions) {
          if (child.conversationId) {
            conversationIdsToPause.add(child.conversationId)
          }
          if (child.status === "running") {
            cancelSubSession(child.id)
            logLLM(`[stopAgentSession] Cancelled internal sub-session ${child.id}`)
          }
        }
        logApp("[stopAgentSession] Internal sub-session scan complete", {
          requestedSessionId,
          cancelledRequestedSubSession,
          childSessionCount: childSessions.length,
          runningChildSessionIds,
        })
      } catch (error) {
        logApp("[stopAgentSession] Error cancelling internal sub-sessions:", error)
      }

      // Pause the message queue for this conversation to prevent processing the next queued message
      // The user can resume the queue later if they want to continue
      const session = agentSessionTracker.getSession(input.sessionId)
      const conversationIdToPause = session?.conversationId ?? requestedSubSessionConversationId ?? mappedTrackedSession?.conversationId
      if (conversationIdToPause) {
        conversationIdsToPause.add(conversationIdToPause)
      }

      if (conversationIdsToPause.size > 0) {
        for (const conversationId of conversationIdsToPause) {
          messageQueueService.pauseQueue(conversationId)
          logLLM(`[stopAgentSession] Paused queue for conversation ${conversationId}`)
        }
        logApp("[stopAgentSession] Queues paused", {
          requestedSessionId,
          pausedConversationIds: Array.from(conversationIdsToPause),
          pausedByTrackedSessionId: session?.id ?? mappedTrackedSession?.id ?? null,
          queueLengths: Array.from(conversationIdsToPause).map((conversationId) => ({
            conversationId,
            queueLength: messageQueueService.getQueue(conversationId).length,
          })),
        })
      } else {
        logApp("[stopAgentSession] Queue pause skipped because requested session is not tracked", {
          requestedSessionId,
          requestedSessionKind,
          mappedAppSessionId: mappedAppSessionId ?? null,
          mappedConversationId: mappedTrackedSession?.conversationId ?? null,
        })
      }

      const runId = agentSessionStateManager.getSessionRunId(input.sessionId)

      // Immediately emit a final progress update with isComplete: true
      // This ensures the UI updates immediately without waiting for the agent loop
      // to detect the stop signal and emit its own final update
      await emitAgentProgress({
        sessionId: input.sessionId,
        runId,
        currentIteration: 0,
        maxIterations: 0,
        steps: [
          {
            id: `stop_${Date.now()}`,
            type: "completion",
            title: "Agent stopped",
            description: "Agent mode was stopped by emergency kill switch. Queue paused.",
            status: "error",
            timestamp: Date.now(),
          },
        ],
        isComplete: true,
        finalContent: "(Agent mode was stopped by emergency kill switch)",
      })

      // Mark tracked top-level sessions as stopped. Internal sub-sessions are
      // tracked by the internal-agent service, not AgentSessionTracker.
      if (requestedSessionKind !== "subsession") {
        agentSessionTracker.stopSession(input.sessionId)
      }

      return { success: true }
    }),

  snoozeAgentSession: t.procedure
    .input<{ sessionId: string }>()
    .action(async ({ input }) => {
      // Snooze the session (runs in background without stealing focus)
      setTrackedAgentSessionSnoozed(input.sessionId, true)

      return { success: true }
    }),

  unsnoozeAgentSession: t.procedure
    .input<{ sessionId: string }>()
    .action(async ({ input }) => {
      // Unsnooze the session (allow it to show progress UI again)
      setTrackedAgentSessionSnoozed(input.sessionId, false)

      return { success: true }
    }),

  // Respond to a tool approval request
  respondToToolApproval: t.procedure
    .input<{ approvalId: string; approved: boolean }>()
    .action(async ({ input }) => {
      logApp(`[Tool Approval] respondToToolApproval called: approvalId=${input.approvalId}, approved=${input.approved}`)
      const success = toolApprovalManager.respondToApproval(input.approvalId, input.approved)
      logApp(`[Tool Approval] respondToApproval result: success=${success}`)
      return { success }
    }),

  // Request the Panel window to focus a specific agent session
  focusAgentSession: t.procedure
    .input<{ sessionId: string }>()
    .action(async ({ input }) => {
      try {
        getWindowRendererHandlers("panel")?.focusAgentSession.send(input.sessionId)
      } catch (e) {
        logApp("[tipc] focusAgentSession send failed:", e)
      }
      return { success: true }
    }),

  writeClipboard: t.procedure
    .input<{ text: string }>()
    .action(async ({ input }) => {
      clipboard.writeText(input.text ?? "")
      return { success: true }
    }),

  showContextMenu: t.procedure
    .input<{
      x: number
      y: number
      selectedText?: string
      messageContext?: {
        content: string
        role: "user" | "assistant" | "tool"
        messageId: string
      }
    }>()
    .action(async ({ input, context }) => {
      const items: Electron.MenuItemConstructorOptions[] = []

      if (input.selectedText) {
        items.push({
          label: "Copy",
          click() {
            clipboard.writeText(input.selectedText || "")
          },
        })
      }

      // Add message-specific context menu items
      if (input.messageContext) {
        const { content, role } = input.messageContext

        // Add "Copy Message" option for all message types
        items.push({
          label: "Copy Message",
          click() {
            clipboard.writeText(content)
          },
        })

        // Add separator if we have other items
        if (items.length > 0) {
          items.push({ type: "separator" })
        }
      }

      if (import.meta.env.DEV) {
        items.push({
          label: "Inspect Element",
          click() {
            context.sender.inspectElement(input.x, input.y)
          },
        })
      }

      const panelWindow = WINDOWS.get("panel")
      const isPanelWindow = panelWindow?.webContents.id === context.sender.id

      if (isPanelWindow) {
        items.push({
          label: "Close",
          click() {
            hideFloatingPanelWindow()
          },
        })
      }

      const menu = Menu.buildFromTemplate(items)
      menu.popup({
        x: input.x,
        y: input.y,
      })
    }),

  getMicrophoneStatus: t.procedure.action(async () => {
    return systemPreferences.getMediaAccessStatus("microphone")
  }),

  isAccessibilityGranted: t.procedure.action(async () => {
    return isAccessibilityGranted()
  }),

  requestAccesssbilityAccess: t.procedure.action(async () => {
    if (process.platform === "win32") return true

    return systemPreferences.isTrustedAccessibilityClient(true)
  }),

  requestMicrophoneAccess: t.procedure.action(async () => {
    return systemPreferences.askForMediaAccess("microphone")
  }),

  showPanelWindow: t.procedure.action(async () => {
    showPanelWindow()
  }),

  showPanelWindowWithTextInput: t.procedure
    .input<{ initialText?: string }>()
    .action(async ({ input }) => {
      await showPanelWindowAndShowTextInput(input.initialText)
    }),

  triggerMcpRecording: t.procedure
    .input<{ conversationId?: string; sessionId?: string; fromTile?: boolean }>()
    .action(async ({ input }) => {
      // Always show the panel during recording for waveform feedback
      // The fromTile flag tells the panel to hide after recording ends
      // fromButtonClick=true indicates this was triggered via UI button (not keyboard shortcut)
      await showPanelWindowAndStartMcpRecording(input.conversationId, input.sessionId, input.fromTile, true)
    }),

  // Broadcast theme change to all windows (cross-window sync)
  broadcastThemeChange: t.procedure
    .input<{ themeMode: string }>()
    .action(async ({ input }) => {
      for (const [, win] of WINDOWS) {
        try {
          getRendererHandlers<RendererHandlers>(win.webContents)?.themeChanged.send(input.themeMode)
        } catch {}
      }
    }),

  showMainWindow: t.procedure
    .input<{ url?: string }>()
    .action(async ({ input }) => {
      showMainWindow(input.url)
    }),

  displayError: t.procedure
    .input<{ title?: string; message: string }>()
    .action(async ({ input }) => {
      if (isMissingApiKeyErrorMessage(input.message)) {
        const result = await dialog.showMessageBox({
          type: "error",
          title: input.title || "Error",
          message: input.message,
          detail: "Configure your API key in Settings > Models.",
          buttons: ["Open Model Settings", "Close"],
          defaultId: 0,
          cancelId: 1,
          noLink: true,
        })

        if (result.response === 0) {
          showMainWindow("/settings/models")
        }
        return
      }

      dialog.showErrorBox(input.title || "Error", input.message)
    }),

  // OAuth methods
  initiateOAuthFlow: t.procedure
    .input<string>()
    .action(async ({ input: serverName }) => {
      return mcpService.initiateOAuthFlow(serverName)
    }),

  completeOAuthFlow: t.procedure
    .input<{ serverName: string; code: string; state: string }>()
    .action(async ({ input }) => {
      return mcpService.completeOAuthFlow(input.serverName, input.code, input.state)
    }),

  getOAuthStatus: t.procedure
    .input<string>()
    .action(async ({ input: serverName }) => {
      return mcpService.getOAuthStatus(serverName)
    }),

  revokeOAuthTokens: t.procedure
    .input<string>()
    .action(async ({ input: serverName }) => {
      return mcpService.revokeOAuthTokens(serverName)
    }),

  // Parakeet (local) STT model management
  getParakeetModelStatus: t.procedure.action(async () => {
    return parakeetStt.getModelStatus()
  }),

  downloadParakeetModel: t.procedure.action(async () => {
    await parakeetStt.downloadModel()
    return { success: true }
  }),

  initializeParakeetRecognizer: t.procedure
    .input<{ numThreads?: number }>()
    .action(async ({ input }) => {
      await parakeetStt.initializeRecognizer(input.numThreads)
      return { success: true }
    }),

  // Kitten (local) TTS model management
  getKittenModelStatus: t.procedure.action(async () => {
    const { getKittenModelStatus } = await import('./kitten-tts')
    return getKittenModelStatus()
  }),

  downloadKittenModel: t.procedure.action(async () => {
    const { downloadKittenModel } = await import('./kitten-tts')
    await downloadKittenModel((progress) => {
      // Send progress to renderer via webContents, guarding against destroyed windows
      BrowserWindow.getAllWindows().forEach(win => {
        if (!win.isDestroyed() && !win.webContents.isDestroyed()) {
          win.webContents.send('kitten-model-download-progress', progress)
        }
      })
    })
    return { success: true }
  }),

  synthesizeWithKitten: t.procedure
    .input<{
      text: string
      voiceId?: number
      speed?: number
    }>()
    .action(async ({ input }) => {
      const { synthesize } = await import('./kitten-tts')
      const result = await synthesize(input.text, input.voiceId, input.speed)
      // Convert Float32Array samples to WAV format
      const wavBuffer = float32ToWav(result.samples, result.sampleRate)
      return {
        audio: wavBuffer.toString('base64'),
        sampleRate: result.sampleRate
      }
    }),

  // Supertonic (local) TTS model management
  getSupertonicModelStatus: t.procedure.action(async () => {
    const { getSupertonicModelStatus } = await import('./supertonic-tts')
    return getSupertonicModelStatus()
  }),

  downloadSupertonicModel: t.procedure.action(async () => {
    const { downloadSupertonicModel } = await import('./supertonic-tts')
    await downloadSupertonicModel((progress) => {
      BrowserWindow.getAllWindows().forEach(win => {
        if (!win.isDestroyed() && !win.webContents.isDestroyed()) {
          win.webContents.send('supertonic-model-download-progress', progress)
        }
      })
    })
    return { success: true }
  }),

  synthesizeWithSupertonic: t.procedure
    .input<{
      text: string
      voice?: string
      lang?: string
      speed?: number
      steps?: number
    }>()
    .action(async ({ input }) => {
      const { synthesize } = await import('./supertonic-tts')
      const result = await synthesize(
        input.text,
        input.voice,
        input.lang,
        input.speed,
        input.steps,
      )
      const wavBuffer = float32ToWav(result.samples, result.sampleRate)
      return {
        audio: wavBuffer.toString('base64'),
        sampleRate: result.sampleRate
      }
    }),

  createRecording: t.procedure
    .input<{
      recording: ArrayBuffer
      pcmRecording?: ArrayBuffer
      duration: number
    }>()
    .action(async ({ input }) => {
      fs.mkdirSync(recordingsFolder, { recursive: true })

      const config = configStore.get()
      let transcript: string

      if (config.sttProviderId === "parakeet") {
        // Use Parakeet (local) STT
        if (!parakeetStt.isModelReady()) {
          throw new Error("Parakeet model not downloaded. Please download it in Settings.")
        }

        // Initialize recognizer if needed
        await parakeetStt.initializeRecognizer(config.parakeetNumThreads)

        if (!input.pcmRecording) {
          throw new Error("Parakeet STT requires pre-decoded float32 PCM audio. pcmRecording was not provided.")
        }
        transcript = await parakeetStt.transcribe(input.pcmRecording, 16000)
        transcript = await postProcessTranscriptSafely(transcript, "createRecording")
      } else {
        // Use OpenAI or Groq for transcription
        const form = new FormData()
        form.append(
          "file",
          new File([input.recording], "recording.webm", { type: "audio/webm" }),
        )
        form.append(
          "model",
          getRemoteSttModel(config),
        )
        form.append("response_format", "json")

        // Add prompt parameter for Groq if provided
        if (config.sttProviderId === "groq" && config.groqSttPrompt?.trim()) {
          form.append("prompt", config.groqSttPrompt.trim())
        }

        // Add language parameter if specified
        const languageCode = config.sttProviderId === "groq"
          ? config.groqSttLanguage || config.sttLanguage
          : config.openaiSttLanguage || config.sttLanguage;

        if (languageCode && languageCode !== "auto") {
          form.append("language", languageCode)
        }

        const groqBaseUrl = config.groqBaseUrl || "https://api.groq.com/openai/v1"
        const openaiBaseUrl = config.openaiBaseUrl || "https://api.openai.com/v1"

        const transcriptResponse = await fetch(
          config.sttProviderId === "groq"
            ? `${groqBaseUrl}/audio/transcriptions`
            : `${openaiBaseUrl}/audio/transcriptions`,
          {
            method: "POST",
            headers: {
              Authorization: `Bearer ${config.sttProviderId === "groq" ? config.groqApiKey : config.openaiApiKey}`,
            },
            body: form,
          },
        )

        if (!transcriptResponse.ok) {
          const message = `${transcriptResponse.statusText} ${(await transcriptResponse.text()).slice(0, 300)}`

          throw new Error(message)
        }

        const json: { text: string } = await transcriptResponse.json()
        transcript = await postProcessTranscriptSafely(json.text, "createRecording")
      }

      const history = getRecordingHistory()
      const item: RecordingHistoryItem = {
        id: Date.now().toString(),
        createdAt: Date.now(),
        duration: input.duration,
        transcript,
      }
      history.push(item)
      saveRecordingsHitory(history)

      fs.writeFileSync(
        path.join(recordingsFolder, `${item.id}.webm`),
        Buffer.from(input.recording),
      )

      const main = WINDOWS.get("main")
      if (main) {
        getRendererHandlers<RendererHandlers>(
          main.webContents,
        ).refreshRecordingHistory.send()
      }

      const panel = WINDOWS.get("panel")
      if (panel) {
        // Clear the "opened with main" flag since panel is being hidden
        clearPanelOpenedWithMain()
        panel.hide()
      }

      // paste
      clipboard.writeText(transcript)
      if (isAccessibilityGranted()) {
        // Add a small delay for regular transcripts too to be less disruptive
        const pasteDelay = 500 // 0.5 second delay for regular transcripts
        setTimeout(async () => {
          try {
            await writeTextWithFocusRestore(transcript)
          } catch (error) {
            // Don't throw here, just log the error so the recording still gets saved
          }
        }, pasteDelay)
      }
    }),

  transcribeChunk: t.procedure
    .input<{
      recording: ArrayBuffer
      pcmRecording?: ArrayBuffer
    }>()
    .action(async ({ input }) => {
      const config = configStore.get()
      let transcript: string

      if (config.sttProviderId === "parakeet") {
        if (!parakeetStt.isModelReady()) {
          return { text: "" }
        }
        await parakeetStt.initializeRecognizer(config.parakeetNumThreads)
        // Use pcmRecording if provided, otherwise skip preview for Parakeet
        // (WebM buffer would fail validation)
        if (!input.pcmRecording) {
          return { text: "" }
        }
        transcript = await parakeetStt.transcribe(input.pcmRecording, 16000)
      } else {
        const form = new FormData()
        form.append(
          "file",
          new File([input.recording], "recording.webm", { type: "audio/webm" }),
        )
        form.append(
          "model",
          getRemoteSttModel(config),
        )
        form.append("response_format", "json")

        if (config.sttProviderId === "groq" && config.groqSttPrompt?.trim()) {
          form.append("prompt", config.groqSttPrompt.trim())
        }

        const languageCode = config.sttProviderId === "groq"
          ? config.groqSttLanguage || config.sttLanguage
          : config.openaiSttLanguage || config.sttLanguage;

        if (languageCode && languageCode !== "auto") {
          form.append("language", languageCode)
        }

        const groqBaseUrl = config.groqBaseUrl || "https://api.groq.com/openai/v1"
        const openaiBaseUrl = config.openaiBaseUrl || "https://api.openai.com/v1"

        const transcriptResponse = await fetch(
          config.sttProviderId === "groq"
            ? `${groqBaseUrl}/audio/transcriptions`
            : `${openaiBaseUrl}/audio/transcriptions`,
          {
            method: "POST",
            headers: {
              Authorization: `Bearer ${config.sttProviderId === "groq" ? config.groqApiKey : config.openaiApiKey}`,
            },
            body: form,
          },
        )

        if (!transcriptResponse.ok) {
          const errBody = await transcriptResponse.text().catch(() => "<unreadable>")
          console.error(
            `[transcribeChunk] API error ${transcriptResponse.status} ${transcriptResponse.statusText}: ${errBody}`,
          )
          return { text: "" }
        }

        const json: { text: string } = await transcriptResponse.json()
        transcript = json.text || ""
      }

      return { text: transcript }
    }),

  createTextInput: t.procedure
    .input<{
      text: string
    }>()
    .action(async ({ input }) => {
      const config = configStore.get()
      let processedText = input.text

      // Apply post-processing if enabled
      if (config.transcriptPostProcessingEnabled) {
        try {
          processedText = await postProcessTranscript(input.text)
        } catch (error) {
          // Continue with original text if post-processing fails
        }
      }

      // Save to history
      const history = getRecordingHistory()
      const item: RecordingHistoryItem = {
        id: Date.now().toString(),
        createdAt: Date.now(),
        duration: 0, // Text input has no duration
        transcript: processedText,
      }
      history.push(item)
      saveRecordingsHitory(history)

      const main = WINDOWS.get("main")
      if (main) {
        getRendererHandlers<RendererHandlers>(
          main.webContents,
        ).refreshRecordingHistory.send()
      }

      const panel = WINDOWS.get("panel")
      if (panel) {
        // Clear the "opened with main" flag since panel is being hidden
        clearPanelOpenedWithMain()
        panel.hide()
      }

      // Auto-paste if enabled
      if (config.mcpAutoPasteEnabled && state.focusedAppBeforeRecording) {
        setTimeout(async () => {
          try {
            await writeText(processedText)
          } catch (error) {
            // Ignore paste errors
          }
        }, config.mcpAutoPasteDelay || 1000)
      }
    }),

  createMcpTextInput: t.procedure
    .input<{
      text: string
      conversationId?: string
      sessionId?: string
      fromTile?: boolean // Origin hint: suppress floating-panel auto-show, but do not imply snoozed/background work.
      startSnoozed?: boolean // True background mode: keep hidden/quiet and disable TTS autoplay.
      suppressPanelAutoShow?: boolean
      focusPanelSession?: boolean
    }>()
    .action(async ({ input }) => {
      const config = configStore.get()
      const queueEnabled = config.mcpMessageQueueEnabled !== false
      const launchState = resolveAgentLaunchState(input)
      reconcileAgentSessionsWithRuntime()

      logApp("[createMcpTextInput] Request received", {
        conversationId: input.conversationId ?? null,
        sessionId: input.sessionId ?? null,
        sessionKind: describeAgentSessionId(input.sessionId),
        fromTile: input.fromTile ?? false,
        ...serializeAgentLaunchState(launchState),
        messageLength: input.text.length,
        queueEnabled,
      })

      // Panel suppression is separate from snoozing. Tile-originated sessions
      // can stay foreground/audible while still avoiding floating-panel popups.
      if (launchState.shouldSuppressPanelAutoShow) {
        suppressPanelAutoShow(2000)
      }

      // Create or get conversation ID
      let conversationId = input.conversationId
      let agentInputText = input.text
      const requestedSessionId = input.sessionId && !input.sessionId.startsWith("pending-")
        ? input.sessionId
        : undefined
      if (!conversationId) {
        const conversation = await conversationService.createConversation(
          input.text,
          "user",
        )
        conversationId = conversation.id
        agentInputText = getLatestStoredUserMessageContent(conversation, input.text)
      } else {
        if (requestedSessionId?.startsWith("subsession_")) {
          const { getInternalSubSession, continueInternalSubSession } = await import("./acp/internal-agent")
          const subSession = getInternalSubSession(requestedSessionId)
          if (!subSession) {
            throw new Error(`Internal sub-session not found: ${requestedSessionId}`)
          }

          const targetConversationId = subSession.conversationId ?? conversationId
          const targetText = await conversationService.materializeInlineDataImagesInContent(targetConversationId, input.text)

          if (queueEnabled && (subSession.status === "running" || subSession.status === "pending")) {
            const queuedMessage = messageQueueService.enqueue(targetConversationId, targetText, requestedSessionId)
            logApp("[createMcpTextInput] Queued message for running internal sub-session", {
              conversationId: targetConversationId,
              queuedMessageId: queuedMessage.id,
              requestedSessionId,
              subSessionStatus: subSession.status,
              fromTile: input.fromTile ?? false,
              messageLength: input.text.length,
              queueLength: messageQueueService.getQueue(targetConversationId).length,
            })
            return { conversationId: targetConversationId, queued: true, queuedMessageId: queuedMessage.id }
          }

          continueInternalSubSession(requestedSessionId, targetText)
            .catch((error) => {
              logLLM("[createMcpTextInput] Internal sub-session continuation error:", error)
            })

          logApp("[createMcpTextInput] Continuing internal sub-session", {
            conversationId: targetConversationId,
            requestedSessionId,
            subSessionStatus: subSession.status,
            fromTile: input.fromTile ?? false,
            messageLength: input.text.length,
          })

          return { conversationId: targetConversationId }
        }

        // Check if message queuing is enabled and there's an active session
        if (queueEnabled) {
          const activeSessionId = requestedSessionId && agentSessionTracker.getSession(requestedSessionId)?.conversationId === conversationId
            ? requestedSessionId
            : agentSessionTracker.findSessionByConversationId(conversationId)
          if (activeSessionId) {
            const session = agentSessionTracker.getSession(activeSessionId)
            const isRuntimeSessionActive =
              session?.status === "active" &&
              agentSessionStateManager.isSessionRegistered(activeSessionId)
            if (isRuntimeSessionActive) {
              const queuedText = await conversationService.materializeInlineDataImagesInContent(conversationId, input.text)
              // Queue the message instead of starting a new session
              const queuedMessage = messageQueueService.enqueue(
                conversationId,
                queuedText,
                activeSessionId,
                serializeAgentLaunchState(launchState),
              )
              logApp("[createMcpTextInput] Queued message for active session", {
                conversationId,
                queuedMessageId: queuedMessage.id,
                activeSessionId,
                activeSessionKind: describeAgentSessionId(activeSessionId),
                activeSessionStatus: session.status,
                fromTile: input.fromTile ?? false,
                messageLength: input.text.length,
                queueLength: messageQueueService.getQueue(conversationId).length,
              })
              return { conversationId, queued: true, queuedMessageId: queuedMessage.id }
            }

            logApp("[createMcpTextInput] Active session lookup did not queue message", {
              conversationId,
              activeSessionId,
              activeSessionKind: describeAgentSessionId(activeSessionId),
              trackerSessionFound: Boolean(session),
              activeSessionStatus: session?.status ?? null,
              runtimeSessionRegistered: agentSessionStateManager.isSessionRegistered(activeSessionId),
              fromTile: input.fromTile ?? false,
            })
          }
        }

        // Add user message to existing conversation
        const updatedConversation = await conversationService.addMessageToConversation(
          conversationId,
          input.text,
          "user",
        )
        agentInputText = getLatestStoredUserMessageContent(updatedConversation, input.text)
      }

      // Try to find and revive an existing session for this conversation
      // This handles the case where user continues from history
      let existingSessionId: string | undefined
      if (input.conversationId) {
        const foundSessionId = requestedSessionId && agentSessionTracker.getConversationIdForSession(requestedSessionId) === input.conversationId
          ? requestedSessionId
          : agentSessionTracker.findSessionByConversationId(input.conversationId)
        if (foundSessionId) {
          const revived = agentSessionTracker.reviveSession(foundSessionId, launchState.startSnoozed)
          if (revived) {
            existingSessionId = foundSessionId
            logApp("[createMcpTextInput] Revived existing session", {
              conversationId: input.conversationId,
              sessionId: foundSessionId,
              fromTile: input.fromTile ?? false,
            })
          } else {
            logApp("[createMcpTextInput] Found session but failed to revive", {
              conversationId: input.conversationId,
              sessionId: foundSessionId,
              fromTile: input.fromTile ?? false,
            })
          }
        } else {
          logApp("[createMcpTextInput] No runtime session found for conversation; starting new session", {
            conversationId: input.conversationId,
            fromTile: input.fromTile ?? false,
              messageLength: input.text.length,
              queueEnabled,
          })
        }
      }

      // Fire-and-forget: Start agent processing without blocking
      // This allows multiple sessions to run concurrently
      // Pass existingSessionId to reuse the session if found
      processWithAgentMode(agentInputText, conversationId, existingSessionId, launchState)
        .then((finalResponse) => {
          // Save to history after completion
          const history = getRecordingHistory()
          const item: RecordingHistoryItem = {
            id: Date.now().toString(),
            createdAt: Date.now(),
            duration: 0, // Text input has no duration
            transcript: finalResponse,
          }
          history.push(item)
          saveRecordingsHitory(history)

          const main = WINDOWS.get("main")
          if (main) {
            getRendererHandlers<RendererHandlers>(
              main.webContents,
            ).refreshRecordingHistory.send()
          }

          // Auto-paste if enabled
          const pasteConfig = configStore.get()
          if (pasteConfig.mcpAutoPasteEnabled && state.focusedAppBeforeRecording) {
            setTimeout(async () => {
              try {
                await writeText(finalResponse)
              } catch (error) {
                // Ignore paste errors
              }
            }, pasteConfig.mcpAutoPasteDelay || 1000)
          }
        })
        .catch((error) => {
          logLLM("[createMcpTextInput] Agent processing error:", error)
        })
        .finally(() => {
          // Process queued messages after this session completes (success or error)
          logLLM(`[createMcpTextInput] .finally() triggered for conversation ${conversationId}, calling processQueuedMessages`)
          processQueuedMessages(conversationId!).catch((err) => {
            logLLM("[createMcpTextInput] Error processing queued messages:", err)
          })
        })

      // Return immediately with conversation ID
      // Progress updates will be sent via emitAgentProgress
      return { conversationId }
    }),

  createMcpRecording: t.procedure
    .input<{
      recording: ArrayBuffer
      pcmRecording?: ArrayBuffer
      duration: number
      conversationId?: string
      sessionId?: string
      screenshot?: ScreenshotAttachmentInput
      fromTile?: boolean // Origin hint: suppress floating-panel auto-show, but do not imply snoozed/background work.
      startSnoozed?: boolean // True background mode: keep hidden/quiet and disable TTS autoplay.
      suppressPanelAutoShow?: boolean
      focusPanelSession?: boolean
    }>()
    .action(async ({ input }) => {
      const launchState = resolveAgentLaunchState(input)

      // Panel suppression is separate from snoozing. Tile-originated recordings
      // can stay foreground/audible while still hiding the waveform panel after capture.
      if (launchState.shouldSuppressPanelAutoShow) {
        suppressPanelAutoShow(2000)
      }

      fs.mkdirSync(recordingsFolder, { recursive: true })

      const config = configStore.get()
      let transcript: string
      reconcileAgentSessionsWithRuntime()

      // Check if message queuing is enabled and there's an active session for this conversation
      // If so, we'll transcribe the audio and queue the transcript instead of processing immediately
      if (input.conversationId && config.mcpMessageQueueEnabled !== false) {
        const activeSessionId = agentSessionTracker.findSessionByConversationId(input.conversationId)
        if (activeSessionId) {
          const session = agentSessionTracker.getSession(activeSessionId)
          if (session && session.status === "active") {
            // Active session exists - transcribe audio and queue the result
            logApp(`[createMcpRecording] Active session ${activeSessionId} found for conversation ${input.conversationId}, will queue transcript`)

            // Transcribe the audio first
            if (config.sttProviderId === "parakeet") {
              // Use Parakeet (local) STT
              if (!parakeetStt.isModelReady()) {
                throw new Error("Parakeet model not downloaded. Please download it in Settings.")
              }

              await parakeetStt.initializeRecognizer(config.parakeetNumThreads)

              if (!input.pcmRecording) {
                throw new Error("Parakeet STT requires pre-decoded float32 PCM audio. pcmRecording was not provided.")
              }
              transcript = await parakeetStt.transcribe(input.pcmRecording, 16000)
            } else {
              const form = new FormData()
              form.append(
                "file",
                new File([input.recording], "recording.webm", { type: "audio/webm" }),
              )
              form.append(
                "model",
                getRemoteSttModel(config),
              )
              form.append("response_format", "json")

              if (config.sttProviderId === "groq" && config.groqSttPrompt?.trim()) {
                form.append("prompt", config.groqSttPrompt.trim())
              }

              const languageCode = config.sttProviderId === "groq"
                ? config.groqSttLanguage || config.sttLanguage
                : config.openaiSttLanguage || config.sttLanguage

              if (languageCode && languageCode !== "auto") {
                form.append("language", languageCode)
              }

              const groqBaseUrl = config.groqBaseUrl || "https://api.groq.com/openai/v1"
              const openaiBaseUrl = config.openaiBaseUrl || "https://api.openai.com/v1"

              const transcriptResponse = await fetch(
                config.sttProviderId === "groq"
                  ? `${groqBaseUrl}/audio/transcriptions`
                  : `${openaiBaseUrl}/audio/transcriptions`,
                {
                  method: "POST",
                  headers: {
                    Authorization: `Bearer ${config.sttProviderId === "groq" ? config.groqApiKey : config.openaiApiKey}`,
                  },
                  body: form,
                },
              )

              if (!transcriptResponse.ok) {
                const message = `${transcriptResponse.statusText} ${(await transcriptResponse.text()).slice(0, 300)}`
                throw new Error(message)
              }

              const json: { text: string } = await transcriptResponse.json()
              transcript = json.text
            }

            transcript = await postProcessTranscriptSafely(transcript, "createMcpRecording queued")

            // Save the recording file
            const recordingId = Date.now().toString()
            fs.writeFileSync(
              path.join(recordingsFolder, `${recordingId}.webm`),
              Buffer.from(input.recording),
            )

            const messageText = appendScreenshotToTranscript(transcript, input.screenshot)
            const queuedText = await conversationService.materializeInlineDataImagesInContent(input.conversationId, messageText)

            // Queue the transcript instead of processing immediately
            const queuedMessage = messageQueueService.enqueue(
              input.conversationId,
              queuedText,
              activeSessionId,
              serializeAgentLaunchState(launchState),
            )
            logApp(`[createMcpRecording] Queued voice transcript ${queuedMessage.id} for active session ${activeSessionId}`)

            return { conversationId: input.conversationId, queued: true, queuedMessageId: queuedMessage.id }
          }
        }
      }

      // No active session or queuing disabled - proceed with normal processing
      // Emit initial loading progress immediately BEFORE transcription
      // This ensures users see feedback during the (potentially long) STT call
      const tempConversationId = input.conversationId || `temp_${Date.now()}`

      // Determine profile snapshot for session isolation
      // If reusing an existing session, use its stored snapshot to maintain isolation
      // Only capture a new snapshot from the current global profile when creating a new session
      let profileSnapshot: SessionProfileSnapshot | undefined

      if (input.sessionId) {
        // Try to get the stored profile snapshot from the existing session
        profileSnapshot = agentSessionStateManager.getSessionProfileSnapshot(input.sessionId)
          ?? agentSessionTracker.getSessionProfileSnapshot(input.sessionId)
      } else if (input.conversationId) {
        // Try to find existing session for this conversation and get its profile snapshot
        const existingSessionId = agentSessionTracker.findSessionByConversationId(input.conversationId)
        if (existingSessionId) {
          profileSnapshot = agentSessionStateManager.getSessionProfileSnapshot(existingSessionId)
            ?? agentSessionTracker.getSessionProfileSnapshot(existingSessionId)
        }
      }

      // Only capture a new snapshot if we don't have one from an existing session
      if (!profileSnapshot) {
        const currentProfile = agentProfileService.getCurrentProfile()
        if (currentProfile) {
          profileSnapshot = createSessionSnapshotFromProfile(currentProfile)
        }
      }

      // If sessionId is provided, try to revive that session.
      // Otherwise, if conversationId is provided, try to find and revive a session for that conversation.
      // This handles the case where user continues from history (only conversationId is set).
      const startSnoozed = launchState.startSnoozed
      let sessionId: string
      if (input.sessionId) {
        // Try to revive the existing session by ID
        // Pass startSnoozed so session stays snoozed when continuing from a tile
        const revived = agentSessionTracker.reviveSession(input.sessionId, startSnoozed)
        if (revived) {
          sessionId = input.sessionId
          // Update the session title while transcribing
          agentSessionTracker.updateSession(sessionId, {
            conversationTitle: "Transcribing...",
            lastActivity: "Transcribing audio...",
          })
        } else {
          // Session not found, create a new one with profile snapshot
          sessionId = agentSessionTracker.startSession(tempConversationId, "Transcribing...", startSnoozed, profileSnapshot)
        }
      } else if (input.conversationId) {
        // No sessionId but have conversationId - try to find existing session for this conversation
        const existingSessionId = agentSessionTracker.findSessionByConversationId(input.conversationId)
        if (existingSessionId) {
          // Pass startSnoozed so session stays snoozed when continuing from a tile
          const revived = agentSessionTracker.reviveSession(existingSessionId, startSnoozed)
          if (revived) {
            sessionId = existingSessionId
            // Update the session title while transcribing
            agentSessionTracker.updateSession(sessionId, {
              conversationTitle: "Transcribing...",
              lastActivity: "Transcribing audio...",
            })
          } else {
            // Revive failed, create new session with profile snapshot
            sessionId = agentSessionTracker.startSession(tempConversationId, "Transcribing...", startSnoozed, profileSnapshot)
          }
        } else {
          // No existing session for this conversation, create new with profile snapshot
          sessionId = agentSessionTracker.startSession(tempConversationId, "Transcribing...", startSnoozed, profileSnapshot)
        }
      } else {
        // No sessionId or conversationId provided, create a new session with profile snapshot
        sessionId = agentSessionTracker.startSession(tempConversationId, "Transcribing...", startSnoozed, profileSnapshot)
      }

      agentSessionTracker.updateSession(sessionId, {
        isSnoozed: startSnoozed,
        suppressPanelAutoShow: launchState.shouldSuppressPanelAutoShow,
      })

      try {
        // Emit initial "initializing" progress update
        await emitAgentProgress({
          sessionId,
          conversationId: tempConversationId,
          currentIteration: 0,
          maxIterations: 1,
          steps: [{
            id: `transcribe_${Date.now()}`,
            type: "thinking",
            title: "Transcribing audio",
            description: "Processing audio input...",
            status: "in_progress",
            timestamp: Date.now(),
          }],
          isComplete: false,
          isSnoozed: startSnoozed,
          conversationTitle: "Transcribing...",
        })

        // First, transcribe the audio using the same logic as regular recording
        if (config.sttProviderId === "parakeet") {
          // Use Parakeet (local) STT
          if (!parakeetStt.isModelReady()) {
            throw new Error("Parakeet model not downloaded. Please download it in Settings.")
          }

          await parakeetStt.initializeRecognizer(config.parakeetNumThreads)

          if (!input.pcmRecording) {
            throw new Error("Parakeet STT requires pre-decoded float32 PCM audio. pcmRecording was not provided.")
          }
          transcript = await parakeetStt.transcribe(input.pcmRecording, 16000)
        } else {
          // Use OpenAI or Groq for transcription
          const form = new FormData()
          form.append(
            "file",
            new File([input.recording], "recording.webm", { type: "audio/webm" }),
          )
          form.append(
            "model",
            getRemoteSttModel(config),
          )
          form.append("response_format", "json")

          if (config.sttProviderId === "groq" && config.groqSttPrompt?.trim()) {
            form.append("prompt", config.groqSttPrompt.trim())
          }

          // Add language parameter if specified
          const languageCode = config.sttProviderId === "groq"
            ? config.groqSttLanguage || config.sttLanguage
            : config.openaiSttLanguage || config.sttLanguage;

          if (languageCode && languageCode !== "auto") {
            form.append("language", languageCode)
          }

          const groqBaseUrl = config.groqBaseUrl || "https://api.groq.com/openai/v1"
          const openaiBaseUrl = config.openaiBaseUrl || "https://api.openai.com/v1"

          const transcriptResponse = await fetch(
            config.sttProviderId === "groq"
              ? `${groqBaseUrl}/audio/transcriptions`
              : `${openaiBaseUrl}/audio/transcriptions`,
            {
              method: "POST",
              headers: {
                Authorization: `Bearer ${config.sttProviderId === "groq" ? config.groqApiKey : config.openaiApiKey}`,
              },
              body: form,
            },
          )

          if (!transcriptResponse.ok) {
            const message = `${transcriptResponse.statusText} ${(await transcriptResponse.text()).slice(0, 300)}`
            throw new Error(message)
          }

          const json: { text: string } = await transcriptResponse.json()
          transcript = json.text
        }

      transcript = await postProcessTranscriptSafely(transcript, "createMcpRecording")

      const messageText = appendScreenshotToTranscript(transcript, input.screenshot)
      let agentInputText = messageText

      // Create or continue conversation
      let conversationId = input.conversationId
      let conversation: Conversation | null = null

      if (!conversationId) {
        // Create new conversation with the transcript
        conversation = await conversationService.createConversation(
          messageText,
          "user",
        )
        conversationId = conversation.id
        agentInputText = getLatestStoredUserMessageContent(conversation, messageText)
      } else {
        // Load existing conversation and add user message
        conversation =
          await conversationService.loadConversation(conversationId)
        if (conversation) {
          const updatedConversation = await conversationService.addMessageToConversation(
            conversationId,
            messageText,
            "user",
          )
          conversation = updatedConversation ?? conversation
          agentInputText = getLatestStoredUserMessageContent(conversation, messageText)
        } else {
          conversation = await conversationService.createConversation(
            messageText,
            "user",
          )
          conversationId = conversation.id
          agentInputText = getLatestStoredUserMessageContent(conversation, messageText)
        }
      }

      // Update session with actual conversation ID and title after transcription
      const conversationTitle = transcript?.trim() || input.screenshot?.name || "Screen selection"
      agentSessionTracker.updateSession(sessionId, {
        conversationId,
        conversationTitle,
      })

      // Save the recording file immediately
      const recordingId = Date.now().toString()
      fs.writeFileSync(
        path.join(recordingsFolder, `${recordingId}.webm`),
        Buffer.from(input.recording),
      )

        // Fire-and-forget: Start agent processing without blocking.
        // Preserve the explicit launch state after transcription so tile voice
        // follow-ups remain audible without re-focusing the floating panel.
        processWithAgentMode(agentInputText, conversationId, sessionId, launchState)
        .then((finalResponse) => {
          // Save to history after completion
          const history = getRecordingHistory()
          const item: RecordingHistoryItem = {
            id: recordingId,
            createdAt: Date.now(),
            duration: input.duration,
            transcript: finalResponse,
          }
          history.push(item)
          saveRecordingsHitory(history)

          const main = WINDOWS.get("main")
          if (main) {
            getRendererHandlers<RendererHandlers>(
              main.webContents,
            ).refreshRecordingHistory.send()
          }
        })
          .catch((error) => {
            logLLM("[createMcpRecording] Agent processing error:", error)
          })
          .finally(() => {
            // Process queued messages after this session completes (success or error)
            processQueuedMessages(conversationId!).catch((err) => {
              logLLM("[createMcpRecording] Error processing queued messages:", err)
            })
          })

        // Return immediately with conversation ID
        // Progress updates will be sent via emitAgentProgress
        return { conversationId }
      } catch (error) {
        // Handle transcription or conversation creation errors
        logLLM("[createMcpRecording] Transcription error:", error)

        // Clean up the session and emit error state
        await emitAgentProgress({
          sessionId,
          conversationId: tempConversationId,
          currentIteration: 1,
          maxIterations: 1,
          steps: [{
            id: `transcribe_error_${Date.now()}`,
            type: "completion",
            title: "Transcription failed",
            description: getErrorMessage(error, "Unknown transcription error"),
            status: "error",
            timestamp: Date.now(),
          }],
          isComplete: true,
          isSnoozed: startSnoozed,
          conversationTitle: "Transcription Error",
          finalContent: `Transcription failed: ${getErrorMessage(error)}`,
        })

        // Mark the session as errored to clean up the UI
        agentSessionTracker.errorSession(sessionId, getErrorMessage(error, "Transcription failed"))

        // Re-throw the error so the caller knows transcription failed
        throw error
      }
    }),

  getRecordingHistory: t.procedure.action(async () => getRecordingHistory()),

  deleteRecordingItem: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
      const recordings = getRecordingHistory().filter(
        (item) => item.id !== input.id,
      )
      saveRecordingsHitory(recordings)
      fs.unlinkSync(path.join(recordingsFolder, `${input.id}.webm`))
    }),

  deleteRecordingHistory: t.procedure.action(async () => {
    fs.rmSync(recordingsFolder, { force: true, recursive: true })
  }),

  getConfig: t.procedure.action(async () => {
    return configStore.get()
  }),

  getChatGptWebAuthStatus: t.procedure.action(async () => {
    const { getChatGptWebAuthStatus } = await import("./chatgpt-web-provider")
    return getChatGptWebAuthStatus()
  }),

  loginChatGptWebOAuth: t.procedure.action(async () => {
    const { loginChatGptWebOAuth } = await import("./chatgpt-web-provider")
    return loginChatGptWebOAuth()
  }),

  logoutChatGptWebOAuth: t.procedure.action(async () => {
    const { logoutChatGptWebOAuth } = await import("./chatgpt-web-provider")
    await logoutChatGptWebOAuth()
    return { success: true }
  }),

  // ============================================================================
  // .agents (modular config) helpers
  // ============================================================================

  getAgentsFolders: t.procedure.action(async () => {
    const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
    const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
    const { getAgentsKnowledgeDir, getAgentsKnowledgeDirs } = await import("./agents-files/knowledge-notes")
    const { getAgentsSkillsDir } = await import("./agents-files/skills")
    const safeFileExists = (filePath: string) => {
      try {
        return fs.existsSync(filePath)
      } catch (error) {
        logApp(`[getAgentsFolders] Failed to check ${filePath}: ${getErrorMessage(error)}`)
        return false
      }
    }

    const globalLayer = getAgentsLayerPaths(globalAgentsFolder)
    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
    const workspaceLayer = workspaceAgentsFolder ? getAgentsLayerPaths(workspaceAgentsFolder) : null

    const workspaceSource = workspaceLayer ? "env" : null

    return {
      global: {
        agentsDir: globalLayer.agentsDir,
        systemPromptPath: globalLayer.systemPromptMdPath,
        systemPromptExists: safeFileExists(globalLayer.systemPromptMdPath),
        skillsDir: getAgentsSkillsDir(globalLayer),
        knowledgeDir: getAgentsKnowledgeDir(globalLayer),
        knowledgeDirs: getAgentsKnowledgeDirs(globalLayer),
        memoriesDir: getAgentsKnowledgeDir(globalLayer),
      },
      workspace: workspaceLayer
        ? {
            agentsDir: workspaceLayer.agentsDir,
            systemPromptPath: workspaceLayer.systemPromptMdPath,
            systemPromptExists: safeFileExists(workspaceLayer.systemPromptMdPath),
            skillsDir: getAgentsSkillsDir(workspaceLayer),
            knowledgeDir: getAgentsKnowledgeDir(workspaceLayer),
            knowledgeDirs: getAgentsKnowledgeDirs(workspaceLayer),
            memoriesDir: getAgentsKnowledgeDir(workspaceLayer),
          }
        : null,
      workspaceSource,
    }
  }),

  openAgentsFolder: t.procedure.action(async () => {
    const { globalAgentsFolder } = await import("./config")
    fs.mkdirSync(globalAgentsFolder, { recursive: true })
    const error = await shell.openPath(globalAgentsFolder)
    return { success: !error, error: error || undefined }
  }),

  openWorkspaceAgentsFolder: t.procedure.action(async () => {
    const { resolveWorkspaceAgentsFolder } = await import("./config")
    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
    if (!workspaceAgentsFolder) return { success: false, error: "No workspace .agents folder configured" }

    fs.mkdirSync(workspaceAgentsFolder, { recursive: true })
    const error = await shell.openPath(workspaceAgentsFolder)
    return { success: !error, error: error || undefined }
  }),

  openSystemPromptFile: t.procedure.action(async () => {
    const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
    const { getAgentsLayerPaths, writeAgentsPrompts } = await import("./agents-files/modular-config")
    const { DEFAULT_SYSTEM_PROMPT } = await import("./system-prompts-default")

    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
    const targetLayer = workspaceAgentsFolder
      ? getAgentsLayerPaths(workspaceAgentsFolder)
      : getAgentsLayerPaths(globalAgentsFolder)

    fs.mkdirSync(targetLayer.agentsDir, { recursive: true })

    const config = configStore.get()
    writeAgentsPrompts(
      targetLayer,
      config.mcpCustomSystemPrompt || "",
      config.mcpToolsSystemPrompt || "",
      DEFAULT_SYSTEM_PROMPT,
      { onlyIfMissing: true, maxBackups: 10 },
    )

    return revealFileInFolder(targetLayer.systemPromptMdPath)
  }),

  openAgentsGuidelinesFile: t.procedure.action(async () => {
    const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
    const { getAgentsLayerPaths, writeAgentsPrompts } = await import("./agents-files/modular-config")
    const { DEFAULT_SYSTEM_PROMPT } = await import("./system-prompts-default")

    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
    const targetLayer = workspaceAgentsFolder
      ? getAgentsLayerPaths(workspaceAgentsFolder)
      : getAgentsLayerPaths(globalAgentsFolder)

    fs.mkdirSync(targetLayer.agentsDir, { recursive: true })

    const config = configStore.get()
    writeAgentsPrompts(
      targetLayer,
      config.mcpCustomSystemPrompt || "",
      config.mcpToolsSystemPrompt || "",
      DEFAULT_SYSTEM_PROMPT,
      { onlyIfMissing: true, maxBackups: 10 },
    )

    return revealFileInFolder(targetLayer.agentsMdPath)
  }),

  openKnowledgeFolder: t.procedure.action(async () => {
    const { globalAgentsFolder } = await import("./config")
    const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
    const { getPrimaryAgentsKnowledgeDir } = await import("./agents-files/knowledge-notes")
    const knowledgeDir = getPrimaryAgentsKnowledgeDir(getAgentsLayerPaths(globalAgentsFolder))
    fs.mkdirSync(knowledgeDir, { recursive: true })
    const error = await shell.openPath(knowledgeDir)
    return { success: !error, error: error || undefined }
  }),

  openWorkspaceKnowledgeFolder: t.procedure.action(async () => {
    const { resolveWorkspaceAgentsFolder } = await import("./config")
    const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
    const { getPrimaryAgentsKnowledgeDir } = await import("./agents-files/knowledge-notes")
    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
    if (!workspaceAgentsFolder) return { success: false, error: "No workspace .agents folder configured" }

    const knowledgeDir = getPrimaryAgentsKnowledgeDir(getAgentsLayerPaths(workspaceAgentsFolder))
    fs.mkdirSync(knowledgeDir, { recursive: true })
    const error = await shell.openPath(knowledgeDir)
    return { success: !error, error: error || undefined }
  }),

  // Debug flags - exposed to renderer for synchronized debug logging
  getDebugFlags: t.procedure.action(async () => {
    return getDebugFlags()
  }),

  saveConfig: t.procedure
    .input<{ config: Config }>()
    .action(async ({ input }) => {
      const prev = configStore.get()
      const next = input.config
      const merged = { ...(prev as any), ...(next as any) } as Config

      // Persist merged config (ensures partial updates don't lose existing settings)
      configStore.save(merged)

      try {
        const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
        const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
        const { cleanupInvalidMcpServerReferencesInLayers } = await import("./agent-profile-mcp-cleanup")

        const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
        const layers = workspaceAgentsFolder
          ? [getAgentsLayerPaths(globalAgentsFolder), getAgentsLayerPaths(workspaceAgentsFolder)]
          : [getAgentsLayerPaths(globalAgentsFolder)]

        const validServerNames = Object.keys(merged.mcpConfig?.mcpServers || {})
        const cleanupResult = cleanupInvalidMcpServerReferencesInLayers(layers, validServerNames)
        if (cleanupResult.updatedProfileIds.length > 0) {
          agentProfileService.reload()
        }
      } catch (_e) {
        // best-effort cleanup only
      }

      // Clear models cache if provider endpoints or API keys changed
      try {
        const providerConfigChanged =
          (prev as any)?.openaiBaseUrl !== (merged as any)?.openaiBaseUrl ||
          (prev as any)?.openaiApiKey !== (merged as any)?.openaiApiKey ||
          (prev as any)?.groqBaseUrl !== (merged as any)?.groqBaseUrl ||
          (prev as any)?.groqApiKey !== (merged as any)?.groqApiKey ||
          (prev as any)?.geminiBaseUrl !== (merged as any)?.geminiBaseUrl ||
          (prev as any)?.geminiApiKey !== (merged as any)?.geminiApiKey ||
          (prev as any)?.chatgptWebBaseUrl !== (merged as any)?.chatgptWebBaseUrl ||
          (prev as any)?.chatgptWebAccessToken !== (merged as any)?.chatgptWebAccessToken ||
          (prev as any)?.chatgptWebSessionToken !== (merged as any)?.chatgptWebSessionToken

        if (providerConfigChanged) {
          const { clearModelsCache } = await import("./models-service")
          clearModelsCache()
        }
      } catch (_e) {
        // best-effort only; cache will eventually expire
      }

      // Apply login item setting when configuration changes (production only; dev would launch bare Electron)
      try {
        if ((process.env.NODE_ENV === "production" || !process.env.ELECTRON_RENDERER_URL) && process.platform !== "linux") {
          app.setLoginItemSettings({
            openAtLogin: !!merged.launchAtLogin,
            openAsHidden: true,
          })
        }
      } catch (_e) {
        // best-effort only
      }

      // Apply dock icon visibility changes immediately (macOS only)
      if (process.env.IS_MAC) {
        try {
          const prevHideDock = !!(prev as any)?.hideDockIcon
          const nextHideDock = !!(merged as any)?.hideDockIcon

          if (prevHideDock !== nextHideDock) {
            if (nextHideDock) {
              // User wants to hide dock icon - hide it now
              app.setActivationPolicy("accessory")
              app.dock?.hide()
            } else {
              // User wants to show dock icon - show it now
              app.dock?.show()
              app.setActivationPolicy("regular")
            }
          }
        } catch (_e) {
          // best-effort only
        }
      }

      // Manage Remote Server lifecycle on config changes
      try {
        const prevEnabled = !!(prev as any)?.remoteServerEnabled
        const nextEnabled = !!(merged as any)?.remoteServerEnabled

        if (prevEnabled !== nextEnabled) {
          if (nextEnabled) {
            await startRemoteServer()
          } else {
            await stopRemoteServer()
          }
        } else if (nextEnabled) {
          const changed =
            (prev as any)?.remoteServerPort !== (merged as any)?.remoteServerPort ||
            (prev as any)?.remoteServerBindAddress !== (merged as any)?.remoteServerBindAddress ||
            (prev as any)?.remoteServerApiKey !== (merged as any)?.remoteServerApiKey ||
            (prev as any)?.remoteServerLogLevel !== (merged as any)?.remoteServerLogLevel

          if (changed) {
            await restartRemoteServer()
          }
        }
      } catch (_e) {
        // lifecycle is best-effort
      }

      try {
        const discordLifecycleAction = getDiscordLifecycleAction(prev, merged)
        if (discordLifecycleAction === "start") {
          await discordService.start()
        } else if (discordLifecycleAction === "restart") {
          await discordService.restart()
        } else if (discordLifecycleAction === "stop") {
          await discordService.stop()
        }
      } catch (_e) {
        // lifecycle is best-effort
      }

      // Manage WhatsApp MCP server auto-configuration
      // Note: The actual server path is determined at runtime in mcp-service.ts createTransport()
      // This ensures the correct internal bundled path is always used, regardless of what's in config
      try {
        const prevWhatsappEnabled = !!(prev as any)?.whatsappEnabled
        const nextWhatsappEnabled = !!(merged as any)?.whatsappEnabled

        if (prevWhatsappEnabled !== nextWhatsappEnabled) {
          const currentMcpConfig = merged.mcpConfig || { mcpServers: {} }
          const hasWhatsappServer = !!currentMcpConfig.mcpServers?.[WHATSAPP_SERVER_NAME]

          if (nextWhatsappEnabled) {
            // WhatsApp is being enabled
            const { mcpService } = await import("./mcp-service")
            if (!hasWhatsappServer) {
              // Auto-add WhatsApp MCP server config when enabled
              // The path in config is just a placeholder - the actual path is determined
              // at runtime in createTransport() to ensure the correct bundled path is used
              const updatedMcpConfig: MCPConfig = {
                ...currentMcpConfig,
                mcpServers: {
                  ...currentMcpConfig.mcpServers,
                  [WHATSAPP_SERVER_NAME]: {
                    command: "node",
                    args: [getInternalWhatsAppServerPath()],
                    transport: "stdio",
                  },
                },
              }
              merged.mcpConfig = updatedMcpConfig
              configStore.save(merged)
            }
            // Start/restart the WhatsApp server (handles both new and existing configs)
            await mcpService.restartServer(WHATSAPP_SERVER_NAME)
          } else if (!nextWhatsappEnabled && hasWhatsappServer) {
            // Stop the WhatsApp server when disabled (but keep config for re-enabling)
            const { mcpService } = await import("./mcp-service")
            await mcpService.stopServer(WHATSAPP_SERVER_NAME)
          }
        } else if (nextWhatsappEnabled) {
          // Check if WhatsApp settings changed - restart server to pick up new env vars
          // Also watch Remote Server settings since prepareEnvironment() derives callback URL/API key from them
          const whatsappSettingsChanged =
            JSON.stringify((prev as any)?.whatsappAllowFrom) !== JSON.stringify((merged as any)?.whatsappAllowFrom) ||
            (prev as any)?.whatsappAutoReply !== (merged as any)?.whatsappAutoReply ||
            (prev as any)?.whatsappLogMessages !== (merged as any)?.whatsappLogMessages

          // If auto-reply is enabled, also restart when Remote Server settings change
          // This includes remoteServerEnabled because prepareEnvironment() only enables
          // callback URL/API key injection when remote server is enabled
          const remoteServerSettingsChanged = (merged as any)?.whatsappAutoReply && (
            (prev as any)?.remoteServerEnabled !== (merged as any)?.remoteServerEnabled ||
            (prev as any)?.remoteServerPort !== (merged as any)?.remoteServerPort ||
            (prev as any)?.remoteServerApiKey !== (merged as any)?.remoteServerApiKey
          )

          if (whatsappSettingsChanged || remoteServerSettingsChanged) {
            const { mcpService } = await import("./mcp-service")
            const currentMcpConfig = merged.mcpConfig || { mcpServers: {} }
            if (currentMcpConfig.mcpServers?.[WHATSAPP_SERVER_NAME]) {
              await mcpService.restartServer(WHATSAPP_SERVER_NAME)
            }
          }
        }
      } catch (_e) {
        // lifecycle is best-effort
      }

      // Reinitialize observability helpers if their config fields changed.
      // This ensures config changes take effect without requiring app restart.
      try {
        const langfuseConfigChanged =
          (prev as any)?.langfuseEnabled !== (merged as any)?.langfuseEnabled ||
          (prev as any)?.langfuseSecretKey !== (merged as any)?.langfuseSecretKey ||
          (prev as any)?.langfusePublicKey !== (merged as any)?.langfusePublicKey ||
          (prev as any)?.langfuseBaseUrl !== (merged as any)?.langfuseBaseUrl
        const localTraceConfigChanged =
          (prev as any)?.localTraceLoggingEnabled !== (merged as any)?.localTraceLoggingEnabled ||
          (prev as any)?.localTraceLogPath !== (merged as any)?.localTraceLogPath

        if (langfuseConfigChanged) {
          const { reinitializeLangfuse } = await import("./langfuse-service")
          reinitializeLangfuse()
        } else if (localTraceConfigChanged) {
          const { resetLocalTraceLogger } = await import("./local-trace-logger")
          resetLocalTraceLogger()
        }
      } catch (_e) {
        // Observability reinitialization is best-effort
      }
    }),

  // Check if langfuse package is installed (for UI to show install instructions)
  isLangfuseInstalled: t.procedure.action(async () => {
    try {
      const { isLangfuseInstalled } = await import("./langfuse-service")
      return isLangfuseInstalled()
    } catch {
      return false
    }
  }),

  recordEvent: t.procedure
    .input<{ type: "start" | "end"; mcpMode?: boolean }>()
    .action(async ({ input }) => {
      if (input.type === "start") {
        state.isRecording = true
        // Track agent mode state so main process knows if we're in agent toggle mode
        if (input.mcpMode !== undefined) {
          state.isRecordingMcpMode = input.mcpMode
        }
      } else {
        state.isRecording = false
        state.isRecordingMcpMode = false
      }
      updateTrayIcon()
    }),

  clearTextInputState: t.procedure.action(async () => {
    logApp("[clearTextInputState] Clearing text input state", {
      panelMode: getCurrentPanelMode(),
      panelVisible: WINDOWS.get("panel")?.isVisible?.() ?? false,
      previousTextInputActive: state.isTextInputActive,
    })
    state.isTextInputActive = false
  }),

  // MCP Config File Operations
  loadMcpConfigFile: t.procedure.action(async () => {
    const result = await dialog.showOpenDialog({
      title: "Load MCP Configuration",
      filters: [
        { name: "JSON Files", extensions: ["json"] },
        { name: "All Files", extensions: ["*"] },
      ],
      properties: ["openFile"],
    })

    if (result.canceled || !result.filePaths.length) {
      return null
    }

    try {
      const configContent = fs.readFileSync(result.filePaths[0], "utf8")
      const mcpConfig = JSON.parse(configContent) as MCPConfig
      const { normalized: normalizedConfig } = normalizeMcpConfig(mcpConfig)

      // Basic validation
      if (!normalizedConfig.mcpServers || typeof normalizedConfig.mcpServers !== "object") {
        throw new Error("Invalid MCP config: missing or invalid mcpServers")
      }

      // Validate each server config based on transport type
      for (const [serverName, serverConfig] of Object.entries(
        normalizedConfig.mcpServers,
      )) {
        const transportType = inferTransportType(serverConfig)

        if (transportType === "stdio") {
          // stdio transport requires command and args
          if (!serverConfig.command || !Array.isArray(serverConfig.args)) {
            throw new Error(
              `Invalid server config for "${serverName}": stdio transport requires "command" and "args" fields. For HTTP servers, use "transport": "streamableHttp" with "url" field.`,
            )
          }
        } else if (transportType === "websocket" || transportType === "streamableHttp") {
          // Remote transports require url
          if (!serverConfig.url) {
            throw new Error(
              `Invalid server config for "${serverName}": ${transportType} transport requires "url" field`,
            )
          }
        } else {
          throw new Error(
            `Invalid server config for "${serverName}": unsupported transport type "${transportType}". Valid types: "stdio", "websocket", "streamableHttp"`,
          )
        }
      }

      return normalizedConfig
    } catch (error) {
      throw new Error(
        `Failed to load MCP config: ${error instanceof Error ? error.message : String(error)}`,
      )
    }
  }),

  validateMcpConfigText: t.procedure
    .input<{ text: string }>()
    .action(async ({ input }) => {
      try {
        const mcpConfig = JSON.parse(input.text) as MCPConfig
        const { normalized: normalizedConfig } = normalizeMcpConfig(mcpConfig)

        // Basic validation - same as file upload
        if (!normalizedConfig.mcpServers || typeof normalizedConfig.mcpServers !== "object") {
          throw new Error("Invalid MCP config: missing or invalid mcpServers")
        }

        // Validate each server config based on transport type
        for (const [serverName, serverConfig] of Object.entries(
          normalizedConfig.mcpServers,
        )) {
          const transportType = inferTransportType(serverConfig)

          if (transportType === "stdio") {
            // stdio transport requires command and args
            if (!serverConfig.command || !Array.isArray(serverConfig.args)) {
              throw new Error(
                `Invalid server config for "${serverName}": stdio transport requires "command" and "args" fields. For HTTP servers, use "transport": "streamableHttp" with "url" field.`,
              )
            }
          } else if (transportType === "websocket" || transportType === "streamableHttp") {
            // Remote transports require url
            if (!serverConfig.url) {
              throw new Error(
                `Invalid server config for "${serverName}": ${transportType} transport requires "url" field`,
              )
            }
          } else {
            throw new Error(
              `Invalid server config for "${serverName}": unsupported transport type "${transportType}". Valid types: "stdio", "websocket", "streamableHttp"`,
            )
          }
        }

        return normalizedConfig
      } catch (error) {
        throw new Error(
          `Invalid MCP config: ${error instanceof Error ? error.message : String(error)}`,
        )
      }
    }),

  saveMcpConfigFile: t.procedure
    .input<{ config: MCPConfig }>()
    .action(async ({ input }) => {
      const result = await dialog.showSaveDialog({
        title: "Save MCP Configuration",
        defaultPath: "mcp.json",
        filters: [
          { name: "JSON Files", extensions: ["json"] },
          { name: "All Files", extensions: ["*"] },
        ],
      })

      if (result.canceled || !result.filePath) {
        return false
      }

      try {
        fs.writeFileSync(result.filePath, JSON.stringify(input.config, null, 2))
        return true
      } catch (error) {
        throw new Error(
          `Failed to save MCP config: ${error instanceof Error ? error.message : String(error)}`,
        )
      }
    }),

  validateMcpConfig: t.procedure
    .input<{ config: MCPConfig }>()
    .action(async ({ input }) => {
      try {
        const { normalized: normalizedConfig } = normalizeMcpConfig(input.config)

        if (!normalizedConfig.mcpServers || typeof normalizedConfig.mcpServers !== "object") {
          return { valid: false, error: "Missing or invalid mcpServers" }
        }

        for (const [serverName, serverConfig] of Object.entries(
          normalizedConfig.mcpServers,
        )) {
          const transportType = inferTransportType(serverConfig)

          // Validate based on transport type
          if (transportType === "stdio") {
            // stdio transport requires command and args
            if (!serverConfig.command) {
              return {
                valid: false,
                error: `Server "${serverName}": stdio transport requires "command" field. For HTTP servers, use "transport": "streamableHttp" with "url" field.`,
              }
            }
            if (!Array.isArray(serverConfig.args)) {
              return {
                valid: false,
                error: `Server "${serverName}": stdio transport requires "args" as an array`,
              }
            }
          } else if (transportType === "websocket" || transportType === "streamableHttp") {
            // Remote transports require url
            if (!serverConfig.url) {
              return {
                valid: false,
                error: `Server "${serverName}": ${transportType} transport requires "url" field`,
              }
            }
          } else {
            return {
              valid: false,
              error: `Server "${serverName}": unsupported transport type "${transportType}". Valid types: "stdio", "websocket", "streamableHttp"`,
            }
          }

          // Common validations for all transport types
          if (serverConfig.env && typeof serverConfig.env !== "object") {
            return {
              valid: false,
              error: `Server "${serverName}": env must be an object`,
            }
          }
          if (
            serverConfig.timeout &&
            typeof serverConfig.timeout !== "number"
          ) {
            return {
              valid: false,
              error: `Server "${serverName}": timeout must be a number`,
            }
          }
          if (
            serverConfig.disabled &&
            typeof serverConfig.disabled !== "boolean"
          ) {
            return {
              valid: false,
              error: `Server "${serverName}": disabled must be a boolean`,
            }
          }
        }

        return { valid: true }
      } catch (error) {
        return {
          valid: false,
          error: error instanceof Error ? error.message : String(error),
        }
      }
    }),

  getMcpServerStatus: t.procedure.action(async () => {
    return mcpService.getServerStatus()
  }),

  getMcpInitializationStatus: t.procedure.action(async () => {
    return mcpService.getInitializationStatus()
  }),

  getMcpDetailedToolList: t.procedure.action(async () => {
    return mcpService.getDetailedToolList()
  }),

  setMcpToolEnabled: t.procedure
    .input<{ toolName: string; enabled: boolean }>()
    .action(async ({ input }) => {
      const success = mcpService.setToolEnabled(input.toolName, input.enabled)
      return { success }
    }),

  setMcpServerRuntimeEnabled: t.procedure
    .input<{ serverName: string; enabled: boolean }>()
    .action(async ({ input }) => {
      const success = mcpService.setServerRuntimeEnabled(
        input.serverName,
        input.enabled,
      )
      return { success }
    }),

  getMcpServerRuntimeState: t.procedure
    .input<{ serverName: string }>()
    .action(async ({ input }) => {
      return {
        runtimeEnabled: mcpService.isServerRuntimeEnabled(input.serverName),
        available: mcpService.isServerAvailable(input.serverName),
      }
    }),

  getMcpDisabledTools: t.procedure.action(async () => {
    return mcpService.getDisabledTools()
  }),

  // Diagnostics endpoints
  getDiagnosticReport: t.procedure.action(async () => {
    try {
      return await diagnosticsService.generateDiagnosticReport()
    } catch (error) {
      diagnosticsService.logError(
        "tipc",
        "Failed to generate diagnostic report",
        error,
      )
      throw error
    }
  }),

  saveDiagnosticReport: t.procedure
    .input<{ filePath?: string }>()
    .action(async ({ input }) => {
      try {
        const savedPath = await diagnosticsService.saveDiagnosticReport(
          input.filePath,
        )
        return { success: true, filePath: savedPath }

      } catch (error) {
        diagnosticsService.logError(
          "tipc",
          "Failed to save diagnostic report",
          error,
        )
        return {
          success: false,
          error: error instanceof Error ? error.message : String(error),
        }
      }
    }),

  performHealthCheck: t.procedure.action(async () => {
    try {
      return await diagnosticsService.performHealthCheck()
    } catch (error) {
      diagnosticsService.logError(
        "tipc",
        "Failed to perform health check",
        error,
      )
      throw error
    }
  }),

  getRecentErrors: t.procedure
    .input<{ count?: number }>()

    .action(async ({ input }) => {
      return diagnosticsService.getRecentErrors(input.count || 10)
    }),

  clearErrorLog: t.procedure.action(async () => {
    diagnosticsService.clearErrorLog()
    return { success: true }
  }),

  testMcpServerConnection: t.procedure
    .input<{ serverName: string; serverConfig: MCPServerConfig }>()
    .action(async ({ input }) => {
      return mcpService.testServerConnection(
        input.serverName,
        input.serverConfig,
      )
    }),

  restartMcpServer: t.procedure
    .input<{ serverName: string }>()

    .action(async ({ input }) => {
      return mcpService.restartServer(input.serverName)
    }),

  stopMcpServer: t.procedure
    .input<{ serverName: string }>()
    .action(async ({ input }) => {
      return mcpService.stopServer(input.serverName)
    }),

  getMcpServerLogs: t.procedure
    .input<{ serverName: string }>()
    .action(async ({ input }) => {
      return mcpService.getServerLogs(input.serverName)
    }),

  clearMcpServerLogs: t.procedure
    .input<{ serverName: string }>()
    .action(async ({ input }) => {
      mcpService.clearServerLogs(input.serverName)
      return { success: true }
    }),

  // WhatsApp Integration
  whatsappConnect: t.procedure.action(async () => {
    const WHATSAPP_SERVER_NAME = "whatsapp"
    try {
      // Check if WhatsApp server is available
      const serverStatus = mcpService.getServerStatus()
      const whatsappServer = serverStatus[WHATSAPP_SERVER_NAME]
      if (!whatsappServer || !whatsappServer.connected) {
        return { success: false, error: "WhatsApp server is not running. Please enable WhatsApp in settings." }
      }

      // Call the whatsapp_connect tool
      const result = await mcpService.executeToolCall(
        { name: "whatsapp_connect", arguments: {} },
        undefined,
        true // skip approval check for internal calls
      )

      // Check if the tool returned an error result
      if (result.isError) {
        const errorText = result.content?.find((c: any) => c.type === "text")?.text || "Connection failed"
        return { success: false, error: errorText }
      }

      // Parse the result to extract QR code if present
      const textContent = result.content?.find((c: any) => c.type === "text")
      if (textContent?.text) {
        try {
          const parsed = JSON.parse(textContent.text)
          if (parsed.qrCode) {
            return { success: true, qrCode: parsed.qrCode, status: "qr_required" }
          } else if (parsed.status === "qr_required") {
            return { success: true, qrCode: parsed.qrCode, status: "qr_required" }
          }
        } catch {
          // Not JSON, check for connection success message
          if (textContent.text.includes("Connected successfully")) {
            return { success: true, status: "connected", message: textContent.text }
          }
          if (textContent.text.includes("Already connected")) {
            return { success: true, status: "connected", message: textContent.text }
          }
        }
        return { success: true, message: textContent.text }
      }

      return { success: true, message: "Connection initiated" }
    } catch (error) {
      return { success: false, error: error instanceof Error ? error.message : String(error) }
    }
  }),

  whatsappGetStatus: t.procedure.action(async () => {
    const WHATSAPP_SERVER_NAME = "whatsapp"
    try {
      // Check if WhatsApp server is available
      const serverStatus = mcpService.getServerStatus()
      const whatsappServer = serverStatus[WHATSAPP_SERVER_NAME]
      if (!whatsappServer || !whatsappServer.connected) {
        return { available: false, connected: false, error: "WhatsApp server is not running" }
      }

      // Call the whatsapp_get_status tool
      const result = await mcpService.executeToolCall(
        { name: "whatsapp_get_status", arguments: {} },
        undefined,
        true // skip approval check for internal calls
      )

      // Check if the tool returned an error result
      if (result.isError) {
        const errorText = result.content?.find((c: any) => c.type === "text")?.text || "Failed to get status"
        return { available: true, connected: false, error: errorText }
      }

      // Parse the result
      const textContent = result.content?.find((c: any) => c.type === "text")
      if (textContent?.text) {
        try {
          const parsed = JSON.parse(textContent.text)
          return { available: true, ...parsed }
        } catch {
          return { available: true, message: textContent.text }
        }
      }

      return { available: true, connected: false }
    } catch (error) {
      return { available: false, connected: false, error: error instanceof Error ? error.message : String(error) }
    }
  }),

  whatsappDisconnect: t.procedure.action(async () => {
    const WHATSAPP_SERVER_NAME = "whatsapp"
    try {
      const result = await mcpService.executeToolCall(
        { name: "whatsapp_disconnect", arguments: {} },
        undefined,
        true
      )
      // Check if the tool returned an error result
      if (result.isError) {
        const errorText = result.content?.find((c: any) => c.type === "text")?.text || "Disconnect failed"
        return { success: false, error: errorText }
      }
      return { success: true }
    } catch (error) {
      return { success: false, error: error instanceof Error ? error.message : String(error) }
    }
  }),

  whatsappLogout: t.procedure.action(async () => {
    const WHATSAPP_SERVER_NAME = "whatsapp"
    try {
      const result = await mcpService.executeToolCall(
        { name: "whatsapp_logout", arguments: {} },
        undefined,
        true
      )
      // Check if the tool returned an error result
      if (result.isError) {
        const errorText = result.content?.find((c: any) => c.type === "text")?.text || "Logout failed"
        return { success: false, error: errorText }
      }
      return { success: true }
    } catch (error) {
      return { success: false, error: error instanceof Error ? error.message : String(error) }
    }
  }),

  // Discord Integration
  discordConnect: t.procedure.action(async () => {
    return discordService.start()
  }),

  discordDisconnect: t.procedure.action(async () => {
    return discordService.stop()
  }),

  discordGetStatus: t.procedure.action(async () => {
    return discordService.getStatus()
  }),

  discordGetLogs: t.procedure.action(async () => {
    return discordService.getLogs()
  }),

  discordClearLogs: t.procedure.action(async () => {
    discordService.clearLogs()
    return { success: true }
  }),

  // Text-to-Speech
  generateSpeech: t.procedure
    .input<{
      text: string
      providerId?: string
      voice?: string
      model?: string
      speed?: number
    }>()
    .action(async ({ input }) => {
      try {
        const config = configStore.get()
        // Desktop-local TTS respects the user's global toggle. Remote clients
        // (mobile via /v1/tts/speak) intentionally bypass this gate.
        if (!config.ttsEnabled) {
          throw new Error("Text-to-Speech is not enabled")
        }
        return await generateTTS(input, config)
      } catch (error) {
        diagnosticsService.logError("tts", "TTS generation failed", error)
        throw error
      }
    }),

  // Models Management
  fetchAvailableModels: t.procedure
    .input<{ providerId: string }>()
    .action(async ({ input }) => {
      const { fetchAvailableModels } = await import("./models-service")
      return fetchAvailableModels(input.providerId)
    }),

  // Fetch models for a specific preset (base URL + API key)
  fetchModelsForPreset: t.procedure
    .input<{ baseUrl: string; apiKey: string }>()
    .action(async ({ input }) => {
      const { fetchModelsForPreset } = await import("./models-service")
      return fetchModelsForPreset(input.baseUrl, input.apiKey)
    }),

  // Get enhanced model info from models.dev
  getModelInfo: t.procedure
    .input<{ modelId: string; providerId?: string }>()
    .action(async ({ input }) => {
      // If providerId is given, use specific provider lookup
      if (input.providerId) {
        const model = getModelFromModelsDevByProviderId(input.modelId, input.providerId)
        return model || null
      }
      // Otherwise, search across ALL providers using fuzzy matching
      const matchResult = findBestModelMatch(input.modelId)
      return matchResult?.model || null
    }),

  // Get all models.dev data
  getModelsDevData: t.procedure.action(async () => {
    return await fetchModelsDevData()
  }),

  // Force refresh models.dev cache
  refreshModelsData: t.procedure.action(async () => {
    await refreshModelsDevCache()
    return { success: true }
  }),

  // Conversation Management
  getConversationHistory: t.procedure.action(async () => {
    logApp("[tipc] getConversationHistory called")
    const result = await conversationService.getConversationHistory()
    return result
  }),

  loadConversation: t.procedure
    .input<{ conversationId: string; messageLimit?: number }>()
    .action(async ({ input }) => {
      return conversationService.loadConversationForDisplay(input.conversationId, {
        messageLimit: input.messageLimit,
      })
    }),

  saveConversation: t.procedure
    .input<{ conversation: Conversation }>()
    .action(async ({ input }) => {
      await conversationService.saveConversation(input.conversation)
    }),

  createConversation: t.procedure
    .input<{ firstMessage: string; role?: "user" | "assistant" }>()
    .action(async ({ input }) => {
      return conversationService.createConversation(
        input.firstMessage,
        input.role,
      )
    }),

  addMessageToConversation: t.procedure
    .input<{
      conversationId: string
      content: string
      role: "user" | "assistant" | "tool"
      toolCalls?: Array<{ name: string; arguments: any }>
      toolResults?: Array<{ success: boolean; content: string; error?: string }>
    }>()
    .action(async ({ input }) => {
      return conversationService.addMessageToConversation(
        input.conversationId,
        input.content,
        input.role,
        input.toolCalls,
        input.toolResults,
      )
    }),

  renameConversationTitle: t.procedure
    .input<{ conversationId: string; title: string }>()
    .action(async ({ input }) => {
      const conversation = await conversationService.renameConversationTitle(
        input.conversationId,
        input.title,
      )

      if (conversation) {
        agentSessionTracker.updateConversationTitleForConversation(
          input.conversationId,
          conversation.title,
        )
      }

      return conversation
    }),

  deleteConversation: t.procedure
    .input<{ conversationId: string }>()
    .action(async ({ input }) => {
      await conversationService.deleteConversation(input.conversationId)
    }),

  branchConversation: t.procedure
    .input<{ conversationId: string; messageIndex: number }>()
    .action(async ({ input }) => {
      return conversationService.branchConversation(input.conversationId, input.messageIndex)
    }),

  deleteAllConversations: t.procedure.action(async () => {
    await conversationService.deleteAllConversations()
  }),

  openConversationsFolder: t.procedure.action(async () => {
    await shell.openPath(conversationsFolder)
  }),

  // Panel resize endpoints
  getPanelSize: t.procedure.action(async () => {
    const win = WINDOWS.get("panel")
    if (!win) {
      throw new Error("Panel window not found")
    }
    const [width, height] = win.getSize()
    return { width, height }
  }),

  updatePanelSize: t.procedure
    .input<{ width: number; height: number }>()
    .action(async ({ input }) => {
      if (!isFinitePanelSize(input)) {
        throw new Error("Invalid panel size")
      }

      const win = WINDOWS.get("panel")
      if (!win) {
        throw new Error("Panel window not found")
      }

      const rawMode = getCurrentPanelMode()
      const mode = isPanelSizeMode(rawMode) ? rawMode : "normal"
      const minWidth = getPanelMinWidthForMode(mode)
      const minHeight = getPanelMinHeightForMode(mode)
      const { width: finalWidth, height: finalHeight } = normalizePanelSize(input, minWidth, minHeight)

      // Update size constraints to allow resizing
      win.setMinimumSize(minWidth, minHeight)

      // Set the actual size
      // Mark manual resize to avoid immediate mode re-apply fighting user
      markManualResize()
      win.setSize(finalWidth, finalHeight, false)
      return { width: finalWidth, height: finalHeight }
    }),

  savePanelCustomSize: t.procedure
    .input<{ width: number; height: number }>()
    .action(async ({ input }) => {
      if (!isFinitePanelSize(input)) {
        throw new Error("Invalid panel size")
      }

      const minWidth = getPanelMinWidthForMode("normal")
      const { width, height } = normalizePanelSize(input, minWidth, WAVEFORM_MIN_HEIGHT)

      const config = configStore.get()
      const updatedConfig = {
        ...config,
        panelCustomSize: { width, height },
      }
      configStore.save(updatedConfig)
      return updatedConfig.panelCustomSize
    }),

  // Save panel size with mode-specific persistence
  savePanelModeSize: t.procedure
    .input<{ mode: "normal" | "agent" | "textInput"; width: number; height: number }>()
    .action(async ({ input }) => {
      if (!isFinitePanelSize(input)) {
        throw new Error("Invalid panel size")
      }

      if (!isPanelSizeMode(input.mode)) {
        throw new Error("Invalid panel mode")
      }

      const minWidth = getPanelMinWidthForMode(input.mode)
      const minHeight = getPanelMinHeightForMode(input.mode)
      const { width, height } = normalizePanelSize(input, minWidth, minHeight)

      const config = configStore.get()
      const updatedConfig = { ...config }

      if (input.mode === "agent") {
        updatedConfig.panelProgressSize = { width, height }
      } else if (input.mode === "textInput") {
        updatedConfig.panelTextInputSize = { width, height }
      } else {
        updatedConfig.panelWaveformSize = { width, height }
      }

      configStore.save(updatedConfig)
      return { mode: input.mode, size: { width, height } }
    }),

  // Get current panel mode (from centralized window state)
  getPanelMode: t.procedure.action(async () => {
    return getCurrentPanelMode()
  }),

  initializePanelSize: t.procedure.action(async () => {
    const win = WINDOWS.get("panel")
    if (!win) {
      throw new Error("Panel window not found")
    }

    const config = configStore.get()
    const minWidth = getPanelMinWidthForMode("normal")
    const legacyCustomSize = config.panelCustomSize
    const savedWaveformSize =
      isBoundedPanelSize(config.panelWaveformSize) &&
      config.panelWaveformSize.width >= minWidth &&
      config.panelWaveformSize.height >= WAVEFORM_MIN_HEIGHT
        ? config.panelWaveformSize
        : undefined
    const isCompactLegacyWaveformSize =
      isFinitePanelSize(legacyCustomSize) &&
      legacyCustomSize.width <= LEGACY_WAVEFORM_SIZE_MAX_WIDTH &&
      legacyCustomSize.height <= LEGACY_WAVEFORM_SIZE_MAX_HEIGHT
    const initialWaveformSize = savedWaveformSize ?? (isCompactLegacyWaveformSize ? legacyCustomSize : undefined)

    if (
      initialWaveformSize &&
      Number.isFinite(initialWaveformSize.width) &&
      Number.isFinite(initialWaveformSize.height)
    ) {
      // Apply saved waveform size (use MIN_WAVEFORM_WIDTH to ensure visualizer bars aren't clipped)
      const { width, height } = initialWaveformSize
      const { width: finalWidth, height: finalHeight } = normalizePanelSize(
        { width, height },
        minWidth,
        WAVEFORM_MIN_HEIGHT,
      )

      win.setMinimumSize(minWidth, WAVEFORM_MIN_HEIGHT)
      win.setSize(finalWidth, finalHeight, false) // no animation on init
      return { width: finalWidth, height: finalHeight }
    }

    // Return current size if no custom size saved
    const [width, height] = win.getSize()
    return { width, height }
  }),

  // Profile Management
  getProfiles: t.procedure.action(async () => {
    return agentProfileService.getProfilesLegacy()
  }),

  getProfile: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
        return agentProfileService.getProfileLegacy(input.id)
    }),

  getCurrentProfile: t.procedure.action(async () => {
    return agentProfileService.getCurrentProfileLegacy()
  }),

  // Get the default system prompt for restore functionality
  getDefaultSystemPrompt: t.procedure.action(async () => {
    const { DEFAULT_SYSTEM_PROMPT } = await import("./system-prompts")
    return DEFAULT_SYSTEM_PROMPT
  }),

  createProfile: t.procedure
    .input<{ name: string; guidelines: string; systemPrompt?: string }>()
    .action(async ({ input }) => {
        const profile = agentProfileService.createUserProfile(input.name, input.guidelines, input.systemPrompt)
        return agentProfileService.getProfileLegacy(profile.id)
    }),

  updateProfile: t.procedure
    .input<{ id: string; name?: string; guidelines?: string; systemPrompt?: string }>()
    .action(async ({ input }) => {
        const updates: Partial<import("@shared/types").AgentProfile> = {}
      if (input.name !== undefined) { updates.displayName = input.name }
      if (input.guidelines !== undefined) updates.guidelines = input.guidelines
      if (input.systemPrompt !== undefined) updates.systemPrompt = input.systemPrompt
      agentProfileService.update(input.id, updates)

      return agentProfileService.getProfileLegacy(input.id)
    }),

  deleteProfile: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
        return agentProfileService.delete(input.id)
    }),

  setCurrentProfile: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
        const profile = agentProfileService.setCurrentProfileStrict(input.id)

      // Update the config with the profile's guidelines, system prompt, and model config
      const config = configStore.get()
      const updatedConfig = {
        ...config,
        mcpCurrentProfileId: profile.id,
        // Apply model config if it exists
        // Agent model settings
        ...((profile.modelConfig?.agentProviderId || profile.modelConfig?.mcpToolsProviderId) && {
          agentProviderId: profile.modelConfig.agentProviderId || profile.modelConfig.mcpToolsProviderId,
        }),
        ...((profile.modelConfig?.agentOpenaiModel || profile.modelConfig?.mcpToolsOpenaiModel) && {
          agentOpenaiModel: profile.modelConfig.agentOpenaiModel || profile.modelConfig.mcpToolsOpenaiModel,
        }),
        ...((profile.modelConfig?.agentGroqModel || profile.modelConfig?.mcpToolsGroqModel) && {
          agentGroqModel: profile.modelConfig.agentGroqModel || profile.modelConfig.mcpToolsGroqModel,
        }),
        ...((profile.modelConfig?.agentGeminiModel || profile.modelConfig?.mcpToolsGeminiModel) && {
          agentGeminiModel: profile.modelConfig.agentGeminiModel || profile.modelConfig.mcpToolsGeminiModel,
        }),
        ...((profile.modelConfig?.agentChatgptWebModel || profile.modelConfig?.mcpToolsChatgptWebModel) && {
          agentChatgptWebModel: profile.modelConfig.agentChatgptWebModel || profile.modelConfig.mcpToolsChatgptWebModel,
        }),
        ...(profile.modelConfig?.currentModelPresetId && {
          currentModelPresetId: profile.modelConfig.currentModelPresetId,
        }),
        // STT Provider settings
        ...(profile.modelConfig?.sttProviderId && {
          sttProviderId: profile.modelConfig.sttProviderId,
        }),
        ...(profile.modelConfig?.openaiSttModel && {
          openaiSttModel: profile.modelConfig.openaiSttModel,
        }),
        ...(profile.modelConfig?.groqSttModel && {
          groqSttModel: profile.modelConfig.groqSttModel,
        }),
        // Transcript Post-Processing settings
        ...(profile.modelConfig?.transcriptPostProcessingProviderId && {
          transcriptPostProcessingProviderId: profile.modelConfig.transcriptPostProcessingProviderId,
        }),
        ...(profile.modelConfig?.transcriptPostProcessingOpenaiModel && {
          transcriptPostProcessingOpenaiModel: profile.modelConfig.transcriptPostProcessingOpenaiModel,
        }),
        ...(profile.modelConfig?.transcriptPostProcessingGroqModel && {
          transcriptPostProcessingGroqModel: profile.modelConfig.transcriptPostProcessingGroqModel,
        }),
        ...(profile.modelConfig?.transcriptPostProcessingGeminiModel && {
          transcriptPostProcessingGeminiModel: profile.modelConfig.transcriptPostProcessingGeminiModel,
        }),
        ...(profile.modelConfig?.transcriptPostProcessingChatgptWebModel && {
          transcriptPostProcessingChatgptWebModel: profile.modelConfig.transcriptPostProcessingChatgptWebModel,
        }),
        // TTS Provider settings
        ...(profile.modelConfig?.ttsProviderId && {
          ttsProviderId: profile.modelConfig.ttsProviderId,
        }),
      }
      configStore.save(updatedConfig)

      // Apply the profile's MCP server configuration
      // If the profile has no toolConfig, we pass empty arrays to reset to default (all enabled)
      const mcpServerConfig = toolConfigToMcpServerConfig(profile.toolConfig)
      mcpService.applyProfileMcpConfig(
        mcpServerConfig?.disabledServers ?? [],
        mcpServerConfig?.disabledTools ?? [],
        mcpServerConfig?.allServersDisabledByDefault ?? false,

        mcpServerConfig?.enabledServers ?? [],
        mcpServerConfig?.enabledRuntimeTools ?? [],
      )

      return agentProfileService.getProfileLegacy(profile.id)
    }),

  exportProfile: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
        return agentProfileService.exportProfile(input.id)
    }),

  importProfile: t.procedure
    .input<{ profileJson: string }>()
    .action(async ({ input }) => {
        return agentProfileService.importProfile(input.profileJson)
    }),

  // Save current MCP server state to a profile
  saveCurrentMcpStateToProfile: t.procedure
    .input<{ profileId: string }>()
    .action(async ({ input }) => {

      const currentState = mcpService.getCurrentMcpConfigState()
      return agentProfileService.saveCurrentMcpStateToProfile(
        input.profileId,
        currentState.disabledServers,
        currentState.disabledTools,

        currentState.enabledServers,
        currentState.enabledRuntimeTools,
      )
    }),

  // Update profile MCP server configuration
  updateProfileMcpConfig: t.procedure

    .input<{
      profileId: string
      disabledServers?: string[]
      disabledTools?: string[]
      enabledServers?: string[]
      enabledRuntimeTools?: string[]
    }>()
    .action(async ({ input }) => {
      return agentProfileService.updateProfileMcpConfig(input.profileId, {
        disabledServers: input.disabledServers,
        disabledTools: input.disabledTools,
        enabledServers: input.enabledServers,
        enabledRuntimeTools: input.enabledRuntimeTools,
      })
    }),

  // Save current model state to a profile
  saveCurrentModelStateToProfile: t.procedure
    .input<{ profileId: string }>()
    .action(async ({ input }) => {
        const config = configStore.get()
      return agentProfileService.saveCurrentModelStateToProfile(input.profileId, {
        // Agent model settings
        agentProviderId: config.agentProviderId || config.mcpToolsProviderId,
        agentOpenaiModel: config.agentOpenaiModel || config.mcpToolsOpenaiModel,
        agentGroqModel: config.agentGroqModel || config.mcpToolsGroqModel,
        agentGeminiModel: config.agentGeminiModel || config.mcpToolsGeminiModel,
        agentChatgptWebModel: config.agentChatgptWebModel || config.mcpToolsChatgptWebModel,
        currentModelPresetId: config.currentModelPresetId,
        // STT Provider settings
        sttProviderId: config.sttProviderId,
        openaiSttModel: config.openaiSttModel,
        groqSttModel: config.groqSttModel,
        // Transcript Post-Processing settings
        transcriptPostProcessingProviderId: config.transcriptPostProcessingProviderId,
        transcriptPostProcessingOpenaiModel: config.transcriptPostProcessingOpenaiModel,
        transcriptPostProcessingGroqModel: config.transcriptPostProcessingGroqModel,
        transcriptPostProcessingGeminiModel: config.transcriptPostProcessingGeminiModel,
        transcriptPostProcessingChatgptWebModel: config.transcriptPostProcessingChatgptWebModel,
        // TTS Provider settings
        ttsProviderId: config.ttsProviderId,
      })
    }),

  // Update profile model configuration
  updateProfileModelConfig: t.procedure
    .input<{
      profileId: string
      // Agent model settings
      agentProviderId?: "openai" | "groq" | "gemini" | "chatgpt-web"
      agentOpenaiModel?: string
      agentGroqModel?: string
      agentGeminiModel?: string
      agentChatgptWebModel?: string
      mcpToolsProviderId?: "openai" | "groq" | "gemini" | "chatgpt-web"
      mcpToolsOpenaiModel?: string
      mcpToolsGroqModel?: string
      mcpToolsGeminiModel?: string
      mcpToolsChatgptWebModel?: string
      currentModelPresetId?: string
      // STT Provider settings
      sttProviderId?: "openai" | "groq" | "parakeet"
      openaiSttModel?: string
      groqSttModel?: string
      // Transcript Post-Processing settings
      transcriptPostProcessingProviderId?: "openai" | "groq" | "gemini" | "chatgpt-web"
      transcriptPostProcessingOpenaiModel?: string
      transcriptPostProcessingGroqModel?: string
      transcriptPostProcessingGeminiModel?: string
      transcriptPostProcessingChatgptWebModel?: string
      // TTS Provider settings
      ttsProviderId?: "openai" | "groq" | "gemini" | "edge" | "kitten" | "supertonic"
    }>()
    .action(async ({ input }) => {
        return agentProfileService.updateProfileModelConfig(input.profileId, {
        // Agent model settings
        agentProviderId: input.agentProviderId ?? input.mcpToolsProviderId,
        agentOpenaiModel: input.agentOpenaiModel ?? input.mcpToolsOpenaiModel,
        agentGroqModel: input.agentGroqModel ?? input.mcpToolsGroqModel,
        agentGeminiModel: input.agentGeminiModel ?? input.mcpToolsGeminiModel,
        agentChatgptWebModel: input.agentChatgptWebModel ?? input.mcpToolsChatgptWebModel,
        currentModelPresetId: input.currentModelPresetId,
        // STT Provider settings
        sttProviderId: input.sttProviderId,
        openaiSttModel: input.openaiSttModel,
        groqSttModel: input.groqSttModel,
        // Transcript Post-Processing settings
        transcriptPostProcessingProviderId: input.transcriptPostProcessingProviderId,
        transcriptPostProcessingOpenaiModel: input.transcriptPostProcessingOpenaiModel,
        transcriptPostProcessingGroqModel: input.transcriptPostProcessingGroqModel,
        transcriptPostProcessingGeminiModel: input.transcriptPostProcessingGeminiModel,
        transcriptPostProcessingChatgptWebModel: input.transcriptPostProcessingChatgptWebModel,
        // TTS Provider settings
        ttsProviderId: input.ttsProviderId,
      })
    }),

  saveProfileFile: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
        const profileJson = agentProfileService.exportProfile(input.id)

      const result = await dialog.showSaveDialog({
        title: "Export Profile",
        defaultPath: "profile.json",
        filters: [
          { name: "JSON Files", extensions: ["json"] },
          { name: "All Files", extensions: ["*"] },
        ],
      })

      if (result.canceled || !result.filePath) {
        return false
      }

      try {
        fs.writeFileSync(result.filePath, profileJson)
        return true
      } catch (error) {
        throw new Error(
          `Failed to save profile: ${error instanceof Error ? error.message : String(error)}`,
        )
      }
    }),

  loadProfileFile: t.procedure.action(async () => {
    const result = await dialog.showOpenDialog({
      title: "Import Profile",
      filters: [
        { name: "JSON Files", extensions: ["json"] },
        { name: "All Files", extensions: ["*"] },
      ],
      properties: ["openFile"],
    })

    if (result.canceled || !result.filePaths.length) {
      return null
    }

    try {
      const profileJson = fs.readFileSync(result.filePaths[0], "utf8")
        return agentProfileService.importProfile(profileJson)
    } catch (error) {
      throw new Error(
        `Failed to import profile: ${error instanceof Error ? error.message : String(error)}`,
      )
    }
  }),

  // Cloudflare Tunnel handlers
  checkCloudflaredInstalled: t.procedure.action(async () => {
    const { checkCloudflaredInstalled } = await import("./cloudflare-tunnel")
    return checkCloudflaredInstalled()
  }),

  startCloudflareTunnel: t.procedure.action(async () => {
    const { startCloudflareTunnel } = await import("./cloudflare-tunnel")
    return startCloudflareTunnel()
  }),

  startNamedCloudflareTunnel: t.procedure
    .input<{
      tunnelId: string
      hostname: string
      credentialsPath?: string
    }>()
    .action(async ({ input }) => {
      const { startNamedCloudflareTunnel } = await import("./cloudflare-tunnel")
      return startNamedCloudflareTunnel(input)
    }),

  stopCloudflareTunnel: t.procedure.action(async () => {
    const { stopCloudflareTunnel } = await import("./cloudflare-tunnel")
    return stopCloudflareTunnel()
  }),

  getCloudflareTunnelStatus: t.procedure.action(async () => {
    const { getCloudflareTunnelStatus } = await import("./cloudflare-tunnel")
    return getCloudflareTunnelStatus()
  }),

  listCloudflareTunnels: t.procedure.action(async () => {
    const { listCloudflareTunnels } = await import("./cloudflare-tunnel")
    return listCloudflareTunnels()
  }),

  checkCloudflaredLoggedIn: t.procedure.action(async () => {
    const { checkCloudflaredLoggedIn } = await import("./cloudflare-tunnel")
    return checkCloudflaredLoggedIn()
  }),

  getRemoteServerStatus: t.procedure.action(async () => {
    return getRemoteServerStatus()
  }),

  getRemoteServerPairingApiKey: t.procedure.action(async () => {
    return getRemoteServerPairingApiKey()
  }),

  // Remote Server QR Code handler
  printRemoteServerQRCode: t.procedure.action(async () => {
    return printQRCodeToTerminal()
  }),

  // MCP Elicitation handlers (Protocol 2025-11-25)
  resolveElicitation: t.procedure
    .input<{
      requestId: string
      action: "accept" | "decline" | "cancel"
      content?: Record<string, string | number | boolean | string[]>
    }>()
    .action(async ({ input }) => {
      const { resolveElicitation } = await import("./mcp-elicitation")
      return resolveElicitation(input.requestId, {
        action: input.action,
        content: input.content,
      })
    }),

  // MCP Sampling handlers (Protocol 2025-11-25)
  resolveSampling: t.procedure
    .input<{
      requestId: string
      approved: boolean
    }>()
    .action(async ({ input }) => {
      const { resolveSampling } = await import("./mcp-sampling")
      return resolveSampling(input.requestId, input.approved)
    }),

  // Message Queue endpoints
  getMessageQueue: t.procedure
    .input<{ conversationId: string }>()
    .action(async ({ input }) => {
          return messageQueueService.getQueue(input.conversationId)
    }),

  getAllMessageQueues: t.procedure.action(async () => {
      const queues = messageQueueService.getAllQueues()
      // Include isPaused state for each queue
      return queues.map(q => ({
        ...q,
        isPaused: messageQueueService.isQueuePaused(q.conversationId),
      }))
  }),

  removeFromMessageQueue: t.procedure
    .input<{ conversationId: string; messageId: string }>()
    .action(async ({ input }) => {
          return messageQueueService.removeFromQueue(input.conversationId, input.messageId)
    }),

  clearMessageQueue: t.procedure
    .input<{ conversationId: string }>()
    .action(async ({ input }) => {
          return messageQueueService.clearQueue(input.conversationId)
    }),

  reorderMessageQueue: t.procedure
    .input<{ conversationId: string; messageIds: string[] }>()
    .action(async ({ input }) => {
          return messageQueueService.reorderQueue(input.conversationId, input.messageIds)
    }),

  updateQueuedMessageText: t.procedure
    .input<{ conversationId: string; messageId: string; text: string }>()
    .action(async ({ input }) => {
    
      // Check if this was a failed message before updating
      const queue = messageQueueService.getQueue(input.conversationId)
      const message = queue.find((m) => m.id === input.messageId)
      const wasFailed = message?.status === "failed"

      const success = messageQueueService.updateMessageText(input.conversationId, input.messageId, input.text)
      if (!success) return false

      // If this was a failed message that's now reset to pending,
      // check if conversation is idle and trigger queue processing
      if (wasFailed) {
              const activeSessionId = agentSessionTracker.findSessionByConversationId(input.conversationId)
        if (activeSessionId) {
          const session = agentSessionTracker.getSession(activeSessionId)
          if (session && session.status === "active") {
            // Session is active, queue will be processed when it completes
            return true
          }
        }

        // Conversation is idle, trigger queue processing
        processQueuedMessages(input.conversationId).catch((err) => {
          logLLM("[updateQueuedMessageText] Error processing queued messages:", err)
        })
      }

      return true
    }),

  retryQueuedMessage: t.procedure
    .input<{ conversationId: string; messageId: string }>()
    .action(async ({ input }) => {
        
      // Use resetToPending to reset failed message status without modifying text
      // This works even for addedToHistory messages since we're not changing the text
      const success = messageQueueService.resetToPending(input.conversationId, input.messageId)
      if (!success) return false

      // Check if conversation is idle (no active session)
      const activeSessionId = agentSessionTracker.findSessionByConversationId(input.conversationId)
      if (activeSessionId) {
        const session = agentSessionTracker.getSession(activeSessionId)
        if (session && session.status === "active") {
          // Session is active, queue will be processed when it completes
          return true
        }
      }

      // Conversation is idle, trigger queue processing
      processQueuedMessages(input.conversationId).catch((err) => {
        logLLM("[retryQueuedMessage] Error processing queued messages:", err)
      })

      return true
    }),

  isMessageQueuePaused: t.procedure
    .input<{ conversationId: string }>()
    .action(async ({ input }) => {
          return messageQueueService.isQueuePaused(input.conversationId)
    }),

  pauseMessageQueue: t.procedure
    .input<{ conversationId: string }>()
    .action(async ({ input }) => {
      messageQueueService.pauseQueue(input.conversationId)
      return true
    }),

  resumeMessageQueue: t.procedure
    .input<{ conversationId: string }>()
    .action(async ({ input }) => {
        
      // Resume the queue
      messageQueueService.resumeQueue(input.conversationId)

      // Check if conversation is idle (no active session) and trigger queue processing
      const activeSessionId = agentSessionTracker.findSessionByConversationId(input.conversationId)
      if (activeSessionId) {
        const session = agentSessionTracker.getSession(activeSessionId)
        if (session && session.status === "active") {
          // Session is active, queue will be processed when it completes
          return true
        }
      }

      // Conversation is idle, trigger queue processing
      processQueuedMessages(input.conversationId).catch((err) => {
        logLLM("[resumeMessageQueue] Error processing queued messages:", err)
      })

      return true
    }),

  verifyExternalAgentCommand: t.procedure
    .input<{ command: string; args?: string[]; cwd?: string; probeArgs?: string[] }>()
    .action(async ({ input }) => {
      const { verifyExternalAgentCommand } = await import("./command-verification-service")
      return verifyExternalAgentCommand(input)
    }),

  // Get all subagent delegations with conversations for a session
  getSubagentDelegations: t.procedure
    .input<{ sessionId: string }>()
    .action(async ({ input }) => {
      const { getAllDelegationsForSession } = await import("./acp/acp-router-tools")
      return getAllDelegationsForSession(input.sessionId)
    }),

  // Get details of a specific subagent delegation
  getSubagentDelegationDetails: t.procedure
    .input<{ runId: string }>()
    .action(async ({ input }) => {
      const { getDelegatedRunDetails } = await import("./acp/acp-router-tools")
      return getDelegatedRunDetails(input.runId)
    }),

  // ============================================================================
  // Agent Profile Handlers (Unified Profile + ACP Agent)
  // ============================================================================

  getAgentProfiles: t.procedure.action(async () => {
    return agentProfileService.getAll()
  }),

  getAgentProfile: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
      return agentProfileService.getById(input.id)
    }),

  getAgentProfileByName: t.procedure
    .input<{ name: string }>()
    .action(async ({ input }) => {
      return agentProfileService.getByName(input.name)
    }),

  createAgentProfile: t.procedure
    .input<{
      profile: {
        name: string
        displayName: string
        description?: string
        systemPrompt?: string
        guidelines?: string

        properties?: Record<string, string>
        modelConfig?: import("@shared/types").ProfileModelConfig
        toolConfig?: import("@shared/types").AgentProfileToolConfig
        skillsConfig?: import("@shared/types").ProfileSkillsConfig
        connection: import("@shared/types").AgentProfileConnection
        isStateful?: boolean
        enabled: boolean
        isUserProfile?: boolean
        isAgentTarget?: boolean
        isDefault?: boolean
        autoSpawn?: boolean
      }
    }>()
    .action(async ({ input }) => {
      return agentProfileService.create(input.profile)
    }),

  updateAgentProfile: t.procedure
    .input<{
      id: string
      updates: Partial<import("@shared/types").AgentProfile>
    }>()
    .action(async ({ input }) => {
      return agentProfileService.update(input.id, input.updates)
    }),

  deleteAgentProfile: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
      return agentProfileService.delete(input.id)
    }),

  getUserProfiles: t.procedure.action(async () => {
    return agentProfileService.getUserProfiles()
  }),

  getAgentTargets: t.procedure.action(async () => {
    return agentProfileService.getAgentTargets()
  }),

  getEnabledAgentTargets: t.procedure.action(async () => {
    return agentProfileService.getEnabledAgentTargets()
  }),

  getCurrentAgentProfile: t.procedure.action(async () => {
    return agentProfileService.getCurrentProfile()
  }),

  setCurrentAgentProfile: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
      const profile = agentProfileService.getById(input.id)
      if (!profile || !profile.enabled) {
        return { success: false }
      }
      agentProfileService.setCurrentProfile(input.id)
      return { success: true }
    }),

  getAgentProfilesByRole: t.procedure
    .input<{ role: import("@shared/types").AgentProfileRole }>()
    .action(async ({ input }) => {
      return agentProfileService.getByRole(input.role)
    }),

  getExternalAgents: t.procedure.action(async () => {
    return agentProfileService.getExternalAgents()
  }),

  getAgentProfileConversation: t.procedure
    .input<{ profileId: string }>()
    .action(async ({ input }) => {
      return agentProfileService.getConversation(input.profileId)
    }),

  setAgentProfileConversation: t.procedure
    .input<{
      profileId: string
      messages: import("@shared/types").ConversationMessage[]
    }>()
    .action(async ({ input }) => {
      agentProfileService.setConversation(input.profileId, input.messages)
      return { success: true }
    }),

  clearAgentProfileConversation: t.procedure
    .input<{ profileId: string }>()
    .action(async ({ input }) => {
      agentProfileService.clearConversation(input.profileId)
      return { success: true }
    }),

  reloadAgentProfiles: t.procedure.action(async () => {
    agentProfileService.reload()
    return { success: true }
  }),

  // Agent Skills Management
  getSkills: t.procedure.action(async () => {
    const { skillsService } = await import("./skills-service")
    return skillsService.getSkills()
  }),

  getSkill: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")
      return skillsService.getSkill(input.id)
    }),

  createSkill: t.procedure
    .input<{ name: string; description: string; instructions: string }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")
      const skill = skillsService.createSkill(input.name, input.description, input.instructions)
      // Auto-enable the new skill for the current profile so it's immediately usable
      agentProfileService.enableSkillForCurrentProfile(skill.id)
      return skill
    }),

  updateSkill: t.procedure
    .input<{ id: string; name?: string; description?: string; instructions?: string; enabled?: boolean }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")
      const { id, ...updates } = input
      return skillsService.updateSkill(id, updates)
    }),

  deleteSkill: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")
      const success = skillsService.deleteSkill(input.id)
      if (!success) return false

      const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
      const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
      const { cleanupInvalidSkillReferencesInLayers } = await import("./agent-profile-skill-cleanup")

      const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
      const layers = workspaceAgentsFolder
        ? [getAgentsLayerPaths(globalAgentsFolder), getAgentsLayerPaths(workspaceAgentsFolder)]
        : [getAgentsLayerPaths(globalAgentsFolder)]

      cleanupInvalidSkillReferencesInLayers(layers, skillsService.getSkills().map(skill => skill.id))
      agentProfileService.reload()
      return true
    }),

  deleteSkills: t.procedure
    .input<{ ids: string[] }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")
      const results: { id: string; success: boolean }[] = []
      for (const id of input.ids) {
        const success = skillsService.deleteSkill(id)
        results.push({ id, success })
      }

      if (results.some(result => result.success)) {
        const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
        const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
        const { cleanupInvalidSkillReferencesInLayers } = await import("./agent-profile-skill-cleanup")

        const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
        const layers = workspaceAgentsFolder
          ? [getAgentsLayerPaths(globalAgentsFolder), getAgentsLayerPaths(workspaceAgentsFolder)]
          : [getAgentsLayerPaths(globalAgentsFolder)]

        cleanupInvalidSkillReferencesInLayers(layers, skillsService.getSkills().map(skill => skill.id))
        agentProfileService.reload()
      }

      return results
    }),

  cleanupStaleSkillReferences: t.procedure.action(async () => {
    const { skillsService } = await import("./skills-service")
    const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
    const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
    const { cleanupInvalidSkillReferencesInLayers } = await import("./agent-profile-skill-cleanup")

    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
    const layers = workspaceAgentsFolder
      ? [getAgentsLayerPaths(globalAgentsFolder), getAgentsLayerPaths(workspaceAgentsFolder)]
      : [getAgentsLayerPaths(globalAgentsFolder)]

    const result = cleanupInvalidSkillReferencesInLayers(layers, skillsService.getSkills().map(skill => skill.id))
    if (result.updatedProfileIds.length > 0) {
      agentProfileService.reload()
    }
    return result
  }),

  cleanupStaleMcpServerReferences: t.procedure.action(async () => {
    const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
    const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
    const { cleanupInvalidMcpServerReferencesInLayers } = await import("./agent-profile-mcp-cleanup")

    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
    const layers = workspaceAgentsFolder
      ? [getAgentsLayerPaths(globalAgentsFolder), getAgentsLayerPaths(workspaceAgentsFolder)]
      : [getAgentsLayerPaths(globalAgentsFolder)]

    const validServerNames = Object.keys(configStore.get().mcpConfig?.mcpServers || {})
    const result = cleanupInvalidMcpServerReferencesInLayers(layers, validServerNames)
    if (result.updatedProfileIds.length > 0) {
      agentProfileService.reload()
    }
    return result
  }),

  importSkillFromMarkdown: t.procedure
    .input<{ content: string }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")
      const skill = skillsService.importSkillFromMarkdown(input.content)
      // Auto-enable the imported skill for the current profile so it's immediately usable
      agentProfileService.enableSkillForCurrentProfile(skill.id)
      return skill
    }),

  exportSkillToMarkdown: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")
      return skillsService.exportSkillToMarkdown(input.id)
    }),

  // Import a single skill - can be a .md file or a folder containing SKILL.md
  importSkillFile: t.procedure.action(async () => {
    const { skillsService } = await import("./skills-service")
    const result = await dialog.showOpenDialog({
      title: "Import Skill",
      filters: [
        { name: "Skill Files", extensions: ["md"] },
        { name: "All Files", extensions: ["*"] },
      ],
      properties: ["openFile", "showHiddenFiles"],
    })

    if (result.canceled || result.filePaths.length === 0) {
      return null
    }

    const skill = skillsService.importSkillFromFile(result.filePaths[0])
    // Auto-enable the imported skill for the current profile so it's immediately usable
    agentProfileService.enableSkillForCurrentProfile(skill.id)
    return skill
  }),

  // Import a skill from a folder containing SKILL.md
  importSkillFolder: t.procedure.action(async () => {
    const { skillsService } = await import("./skills-service")
    const result = await dialog.showOpenDialog({
      title: "Import Skill Folder",
      message: "Select a folder containing SKILL.md",
      properties: ["openDirectory", "showHiddenFiles"],
    })

    if (result.canceled || result.filePaths.length === 0) {
      return null
    }

    const skill = skillsService.importSkillFromFolder(result.filePaths[0])
    // Auto-enable the imported skill for the current profile so it's immediately usable
    agentProfileService.enableSkillForCurrentProfile(skill.id)
    return skill
  }),

  // Bulk import all skill folders from a parent directory
  importSkillsFromParentFolder: t.procedure.action(async () => {
    const { skillsService } = await import("./skills-service")
    const result = await dialog.showOpenDialog({
      title: "Import Skills from Folder",
      message: "Select a folder containing multiple skill folders (each with SKILL.md)",
      properties: ["openDirectory", "showHiddenFiles"],
    })

    if (result.canceled || result.filePaths.length === 0) {
      return null
    }

    const importResult = skillsService.importSkillsFromParentFolder(result.filePaths[0])
    // Auto-enable all imported skills for the current profile so they're immediately usable
    for (const skill of importResult.imported) {
      agentProfileService.enableSkillForCurrentProfile(skill.id)
    }
    return importResult
  }),

  saveSkillFile: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")
      const skill = skillsService.getSkill(input.id)
      if (!skill) {
        throw new Error(`Skill with id ${input.id} not found`)
      }

      const result = await dialog.showSaveDialog({
        title: "Export Skill",
        defaultPath: `${skill.name.replace(/[^a-z0-9]/gi, "-").toLowerCase()}.md`,
        filters: [
          { name: "Markdown Files", extensions: ["md"] },
        ],
      })

      if (result.canceled || !result.filePath) {
        return false
      }

      const content = skillsService.exportSkillToMarkdown(input.id)
      fs.writeFileSync(result.filePath, content)
      return true
    }),

  openSkillsFolder: t.procedure.action(async () => {
    const { globalAgentsFolder } = await import("./config")
    const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
    const { getAgentsSkillsDir } = await import("./agents-files/skills")

    // Canonical skills location is the global layer.
    const layer = getAgentsLayerPaths(globalAgentsFolder)
    const skillsDir = getAgentsSkillsDir(layer)

    fs.mkdirSync(skillsDir, { recursive: true })
    const error = await shell.openPath(skillsDir)
    return { success: !error, error: error || undefined }
  }),

  openWorkspaceSkillsFolder: t.procedure.action(async () => {
    const { resolveWorkspaceAgentsFolder } = await import("./config")
    const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
    const { getAgentsSkillsDir } = await import("./agents-files/skills")

    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
    if (!workspaceAgentsFolder) return { success: false, error: "No workspace .agents folder detected" }

    const layer = getAgentsLayerPaths(workspaceAgentsFolder)
    const skillsDir = getAgentsSkillsDir(layer)

    fs.mkdirSync(skillsDir, { recursive: true })
    const error = await shell.openPath(skillsDir)
    return { success: !error, error: error || undefined }
  }),

  openSkillFile: t.procedure
    .input<{ skillId: string }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")

      const skill = skillsService.getSkill(input.skillId)
      if (!skill) {
        return { success: false, error: `Skill with id ${input.skillId} not found` }
      }

      const filePath = skillsService.getSkillCanonicalFilePath(input.skillId)
      if (!filePath) {
        return { success: false, error: `No file path found for skill ${input.skillId}` }
      }

      if (!fs.existsSync(filePath)) {
        try {
          fs.mkdirSync(path.dirname(filePath), { recursive: true })
          fs.writeFileSync(filePath, skillsService.exportSkillToMarkdown(input.skillId), "utf8")
        } catch (error) {
          return {
            success: false,
            path: filePath,
            error: error instanceof Error ? error.message : String(error),
          }
        }
      }

      return revealFileInFolder(filePath)
    }),

  scanSkillsFolder: t.procedure.action(async () => {
    const { skillsService } = await import("./skills-service")
    const importedSkills = skillsService.scanSkillsFolder()
    // Auto-enable all newly imported skills for the current profile so they're immediately usable
    for (const skill of importedSkills) {
      agentProfileService.enableSkillForCurrentProfile(skill.id)
    }
    return importedSkills
  }),

  // Import skill(s) from a GitHub repository
  importSkillFromGitHub: t.procedure
    .input<{ repoIdentifier: string }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")
      const result = await skillsService.importSkillFromGitHub(input.repoIdentifier)
      // Auto-enable all imported skills for the current profile so they're immediately usable
      for (const skill of result.imported) {
        agentProfileService.enableSkillForCurrentProfile(skill.id)
      }
      return result
    }),

  // Per-profile skill management
  getProfileSkillsConfig: t.procedure
    .input<{ profileId: string }>()
    .action(async ({ input }) => {
      const profile = agentProfileService.getById(input.profileId)
      // When skillsConfig is undefined, all skills are enabled by default
      return profile?.skillsConfig ?? { enabledSkillIds: [], allSkillsDisabledByDefault: false }
    }),

  updateProfileSkillsConfig: t.procedure
    .input<{ profileId: string; enabledSkillIds?: string[]; allSkillsDisabledByDefault?: boolean }>()
    .action(async ({ input }) => {
      const { profileId, ...config } = input
      return agentProfileService.updateProfileSkillsConfig(profileId, config)
    }),

  toggleProfileSkill: t.procedure
    .input<{ profileId: string; skillId: string }>()
    .action(async ({ input }) => {
      // Pass all available skill IDs so the toggle can properly transition
      // from "all enabled by default" to explicit opt-in mode
      const { skillsService } = await import("./skills-service")
      const allSkillIds = skillsService.refreshFromDisk().map(s => s.id)
      return agentProfileService.toggleProfileSkill(input.profileId, input.skillId, allSkillIds)
    }),

  isSkillEnabledForProfile: t.procedure
    .input<{ profileId: string; skillId: string }>()
    .action(async ({ input }) => {
      return agentProfileService.isSkillEnabledForProfile(input.profileId, input.skillId)
    }),

  getEnabledSkillIdsForProfile: t.procedure
    .input<{ profileId: string }>()
    .action(async ({ input }) => {
      const enabledSkillIds = agentProfileService.getEnabledSkillIdsForProfile(input.profileId)
      if (enabledSkillIds === null) {
        // null means "all skills enabled" — return all available skill IDs
        const { skillsService } = await import("./skills-service")
        return skillsService.refreshFromDisk().map(s => s.id)
      }
      return enabledSkillIds
    }),

  // Get enabled skills instructions for a specific profile
  getEnabledSkillsInstructionsForProfile: t.procedure
    .input<{ profileId: string }>()
    .action(async ({ input }) => {
      const { skillsService } = await import("./skills-service")
      const enabledSkillIds = agentProfileService.getEnabledSkillIdsForProfile(input.profileId)
      if (enabledSkillIds === null) {
        // null means "all skills enabled" — use all available skill IDs
        const allSkillIds = skillsService.refreshFromDisk().map(s => s.id)
        return skillsService.getEnabledSkillsInstructionsForProfile(allSkillIds)
      }
      return skillsService.getEnabledSkillsInstructionsForProfile(enabledSkillIds)
    }),

  // ============================================================================
  // Bundle Export/Import (Issue #25: .dotagents bundles)
  // ============================================================================

  getBundleExportableItems: t.procedure.action(async () => {
    const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
    const { getBundleExportableItemsFromLayers } = await import("./bundle-service")
    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()

    return getBundleExportableItemsFromLayers(
      workspaceAgentsFolder
        ? [globalAgentsFolder, workspaceAgentsFolder]
        : [globalAgentsFolder]
    )
  }),

  exportBundle: t.procedure
    .input<{
      name?: string
      description?: string
      publicMetadata?: {
        summary: string
        author: {
          displayName: string
          handle?: string
          url?: string
        }
        tags: string[]
        compatibility?: {
          minDesktopVersion?: string
          notes?: string[]
        }
      }
      agentProfileIds?: string[]
      mcpServerNames?: string[]
      skillIds?: string[]
      repeatTaskIds?: string[]
      knowledgeNoteIds?: string[]
      components?: {
        agentProfiles?: boolean
        mcpServers?: boolean
        skills?: boolean
        repeatTasks?: boolean
        knowledgeNotes?: boolean
      }
    } | undefined>()
    .action(async ({ input }) => {
      const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
      const { exportBundleToFile, exportBundleToFileFromLayers } = await import("./bundle-service")
      const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
      const exportOptions = {
        name: input?.name,
        description: input?.description,
        publicMetadata: input?.publicMetadata,
        agentProfileIds: input?.agentProfileIds,
        mcpServerNames: input?.mcpServerNames,
        skillIds: input?.skillIds,
        repeatTaskIds: input?.repeatTaskIds,
        knowledgeNoteIds: input?.knowledgeNoteIds,
        components: input?.components,
      }

      // Export from merged layers when a workspace overlay exists (workspace wins on conflicts).
      if (workspaceAgentsFolder) {
        return exportBundleToFileFromLayers([globalAgentsFolder, workspaceAgentsFolder], exportOptions)
      }

      return exportBundleToFile(globalAgentsFolder, exportOptions)
    }),

  generatePublishPayload: t.procedure
    .input<{
      name?: string
      catalogId?: string
      artifactUrl?: string
      description?: string
      publicMetadata: {
        summary: string
        author: {
          displayName: string
          handle?: string
          url?: string
        }
        tags: string[]
        compatibility?: {
          minDesktopVersion?: string
          notes?: string[]
        }
      }
      components?: {
        agentProfiles?: boolean
        mcpServers?: boolean
        skills?: boolean
        repeatTasks?: boolean
        knowledgeNotes?: boolean
      }
      agentProfileIds?: string[]
      mcpServerNames?: string[]
      skillIds?: string[]
      repeatTaskIds?: string[]
      knowledgeNoteIds?: string[]
    }>()
    .action(async ({ input }) => {
      const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
      const { generatePublishPayload } = await import("./bundle-service")
      const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
      const dirs = workspaceAgentsFolder
        ? [globalAgentsFolder, workspaceAgentsFolder]
        : [globalAgentsFolder]
      return generatePublishPayload(dirs, {
        name: input.name,
        catalogId: input.catalogId,
        artifactUrl: input.artifactUrl,
        description: input.description,
        publicMetadata: input.publicMetadata,
        agentProfileIds: input.agentProfileIds,
        mcpServerNames: input.mcpServerNames,
        components: input.components,
        skillIds: input.skillIds,
        repeatTaskIds: input.repeatTaskIds,
        knowledgeNoteIds: input.knowledgeNoteIds,
      })
    }),

  saveHubPublishPayloadFile: t.procedure
    .input<{
      catalogId: string
      payloadJson: string
    }>()
    .action(async ({ input }) => {
      const safeBaseName = (input.catalogId || "hub-publish")
        .replace(/[^a-z0-9-_]+/gi, "-")
        .replace(/-+/g, "-")
        .replace(/^-|-$/g, "")
        || "hub-publish"

      const result = await dialog.showSaveDialog({
        title: "Save Hub Publish Package",
        defaultPath: `${safeBaseName}.hub-publish.json`,
        filters: [
          { name: "Hub Publish Package", extensions: ["json"] },
          { name: "All Files", extensions: ["*"] },
        ],
      })

      if (result.canceled || !result.filePath) {
        return { success: false, canceled: true }
      }

      fs.writeFileSync(result.filePath, input.payloadJson, "utf-8")
      return { success: true, canceled: false, filePath: result.filePath }
    }),

  previewBundle: t.procedure.action(async () => {
    const { previewBundleFromDialog } = await import("./bundle-service")
    return previewBundleFromDialog()
  }),

  previewBundleWithConflicts: t.procedure
    .input<{ filePath: string }>()
    .action(async ({ input }) => {
      const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
      const { previewBundleWithConflicts } = await import("./bundle-service")
      const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
      const targetDir = workspaceAgentsFolder || globalAgentsFolder
      const targetPreview = previewBundleWithConflicts(input.filePath, targetDir)

      // With workspace overlays, merge conflicts from global + workspace so preview
      // matches the merged UI view and catches shadowing collisions.
      if (!workspaceAgentsFolder || !targetPreview.success) {
        return targetPreview
      }

      const globalPreview = previewBundleWithConflicts(input.filePath, globalAgentsFolder)
      if (!globalPreview.success) {
        return targetPreview
      }

      const mergedConflicts = mergeConflictMaps(
        (globalPreview as BundleConflictPreview).conflicts,
        (targetPreview as BundleConflictPreview).conflicts
      )

      return {
        ...targetPreview,
        conflicts: mergedConflicts,
      }
    }),

  importBundle: t.procedure
    .input<{
      filePath: string
      conflictStrategy: "skip" | "overwrite" | "rename"
      components?: {
        agentProfiles?: boolean
        mcpServers?: boolean
        skills?: boolean
        repeatTasks?: boolean
        knowledgeNotes?: boolean
      }
    }>()
    .action(async ({ input }) => {
      const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
      const { importBundle } = await import("./bundle-service")
      // Use workspace layer if present, otherwise global
      const targetDir = resolveWorkspaceAgentsFolder() || globalAgentsFolder
      const result = await importBundle(input.filePath, targetDir, {
        conflictStrategy: input.conflictStrategy,
        components: input.components,
      })
      await refreshRuntimeAfterBundleImport()
      return result
    }),

  importBundleFromDialog: t.procedure
    .input<{
      conflictStrategy: "skip" | "overwrite" | "rename"
      components?: {
        agentProfiles?: boolean
        mcpServers?: boolean
        skills?: boolean
        repeatTasks?: boolean
        knowledgeNotes?: boolean
      }
    }>()
    .action(async ({ input }) => {
      const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
      const { importBundleFromDialog } = await import("./bundle-service")
      // Use workspace layer if present, otherwise global
      const targetDir = resolveWorkspaceAgentsFolder() || globalAgentsFolder
      const result = await importBundleFromDialog(targetDir, {
        conflictStrategy: input.conflictStrategy,
        components: input.components,
      })
      if (result) {
        await refreshRuntimeAfterBundleImport()
      }
      return result
    }),

  // ============================================================================
  // Sandbox Slots (Issue #141: bundle sandbox/profile slots)
  // ============================================================================
  getSandboxState: t.procedure.action(async () => {
    const { globalAgentsFolder } = await import("./config")
    const { getSandboxState } = await import("./sandbox-service")
    return getSandboxState(globalAgentsFolder)
  }),
  saveBaseline: t.procedure.action(async () => {
    const { globalAgentsFolder } = await import("./config")
    const { saveBaseline } = await import("./sandbox-service")
    return saveBaseline(globalAgentsFolder)
  }),
  saveCurrentAsSlot: t.procedure
    .input<{ name: string; sourceBundleName?: string }>()
    .action(async ({ input }) => {
      const { globalAgentsFolder } = await import("./config")
      const { saveCurrentAsSlot } = await import("./sandbox-service")
      return saveCurrentAsSlot(globalAgentsFolder, input.name, {
        sourceBundleName: input.sourceBundleName,
      })
    }),
  switchToSlot: t.procedure
    .input<{ name: string }>()
    .action(async ({ input }) => {
      const { globalAgentsFolder } = await import("./config")
      const { switchToSlot } = await import("./sandbox-service")
      const result = switchToSlot(globalAgentsFolder, input.name)
      if (result.success) {
        await refreshRuntimeAfterBundleImport()
      }
      return result
    }),
  restoreBaseline: t.procedure.action(async () => {
    const { globalAgentsFolder } = await import("./config")
    const { restoreBaseline } = await import("./sandbox-service")
    const result = restoreBaseline(globalAgentsFolder)
    if (result.success) {
      await refreshRuntimeAfterBundleImport()
    }
    return result
  }),
  deleteSlot: t.procedure
    .input<{ name: string }>()
    .action(async ({ input }) => {
      const { globalAgentsFolder } = await import("./config")
      const { deleteSlot } = await import("./sandbox-service")
      return deleteSlot(globalAgentsFolder, input.name)
    }),
  renameSlot: t.procedure
    .input<{ oldName: string; newName: string }>()
    .action(async ({ input }) => {
      const { globalAgentsFolder } = await import("./config")
      const { renameSlot } = await import("./sandbox-service")
      return renameSlot(globalAgentsFolder, input.oldName, input.newName)
    }),
  importBundleToSandbox: t.procedure
    .input<{
      filePath: string
      slotName: string
      conflictStrategy: "skip" | "overwrite" | "rename"
      components?: {
        agentProfiles?: boolean
        mcpServers?: boolean
        skills?: boolean
        repeatTasks?: boolean
        knowledgeNotes?: boolean
      }
    }>()
    .action(async ({ input }) => {
      const { globalAgentsFolder } = await import("./config")
      const { createSlotFromCurrentState, switchToSlot } = await import("./sandbox-service")
      const { importBundle, previewBundle } = await import("./bundle-service")

      // Get bundle name for the slot metadata
      const bundle = previewBundle(input.filePath)
      const sourceBundleName = bundle?.manifest.name

      // Reject reserved slot names to prevent overwriting the baseline
      const { sanitizeSlotName } = await import("./sandbox-service")
      if (sanitizeSlotName(input.slotName) === "default") {
        return { success: false, errors: ["Cannot import a bundle into the reserved \"default\" baseline slot"] }
      }

      // Save baseline if needed, then create the new slot from current state
      const slotResult = createSlotFromCurrentState(
        globalAgentsFolder,
        input.slotName,
        { sourceBundleName }
      )
      if (!slotResult.success) {
        return { success: false, errors: [slotResult.error || "Failed to create sandbox slot"] }
      }

      // Switch to the new slot
      const switchResult = switchToSlot(globalAgentsFolder, input.slotName)
      if (!switchResult.success) {
        return { success: false, errors: [switchResult.error || "Failed to switch to sandbox slot"] }
      }

      // Import the bundle into the now-active slot
      const importResult = await importBundle(input.filePath, globalAgentsFolder, {
        conflictStrategy: input.conflictStrategy,
        components: input.components,
      })

      // Re-save the slot with the imported bundle state
      const { saveCurrentAsSlot } = await import("./sandbox-service")
      const saveResult = saveCurrentAsSlot(globalAgentsFolder, input.slotName, { sourceBundleName })
      if (!saveResult.success) {
        return { success: false, errors: [saveResult.error || "Failed to save sandbox slot after importing bundle"] }
      }

      await refreshRuntimeAfterBundleImport()
      return importResult
    }),

  // Knowledge notes service handlers
  getAllKnowledgeNotes: t.procedure
    .input<{
      context?: import("../shared/types").KnowledgeNoteContext
      dateFilter?: import("../shared/types").KnowledgeNoteDateFilter
      sort?: import("../shared/types").KnowledgeNoteSort
      limit?: number
    }>()
    .action(async ({ input }) => {
      return knowledgeNotesService.getAllNotes({
        context: input?.context,
        dateFilter: input?.dateFilter,
        sort: input?.sort,
        limit: input?.limit,
      })
    }),

  getKnowledgeNotesOverview: t.procedure
    .input<{
      context?: import("../shared/types").KnowledgeNoteContext
      dateFilter?: import("../shared/types").KnowledgeNoteDateFilter
    }>()
    .action(async ({ input }) => {
      return knowledgeNotesService.getOverview({ context: input?.context, dateFilter: input?.dateFilter })
    }),

  getKnowledgeNotesByGroup: t.procedure
    .input<{
      groupKey: string
      seriesKey?: string
      context?: import("../shared/types").KnowledgeNoteContext
      dateFilter?: import("../shared/types").KnowledgeNoteDateFilter
      sort?: import("../shared/types").KnowledgeNoteSort
    }>()
    .action(async ({ input }) => {
      return knowledgeNotesService.getNotesByGroup(input)
    }),

  getKnowledgeNote: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
      return knowledgeNotesService.getNote(input.id)
    }),

  saveKnowledgeNoteFromSummary: t.procedure
    .input<{
      summary: import("../shared/types").AgentStepSummary
      title?: string
      userNotes?: string
      tags?: string[]
      conversationTitle?: string
      conversationId?: string
    }>()
    .action(async ({ input }) => {
      const note = knowledgeNotesService.createNoteFromSummary(
        input.summary,
        input.title,
        input.userNotes,
        input.tags,
        input.conversationTitle,
        input.conversationId,
      )
      if (!note) {
        return { success: true, note: null, reason: "no_durable_content" as const }
      }
      const success = await knowledgeNotesService.saveNote(note)
      return { success, note: success ? note : null }
    }),

  saveKnowledgeNote: t.procedure
    .input<{
      note: import("../shared/types").KnowledgeNote
    }>()
    .action(async ({ input }) => {
      return knowledgeNotesService.saveNote(input.note)
    }),

  updateKnowledgeNote: t.procedure
    .input<{
      id: string
      updates: Partial<Omit<import("../shared/types").KnowledgeNote, "id" | "createdAt">>
    }>()
    .action(async ({ input }) => {
      return knowledgeNotesService.updateNote(input.id, input.updates)
    }),

  deleteKnowledgeNote: t.procedure
    .input<{ id: string }>()
    .action(async ({ input }) => {
      return knowledgeNotesService.deleteNote(input.id)
    }),

  deleteMultipleKnowledgeNotes: t.procedure
    .input<{ ids: string[] }>()
    .action(async ({ input }) => {
      const result = await knowledgeNotesService.deleteMultipleNotes(input.ids)
      if (result.error) {
        throw new Error(result.error)
      }
      return result.deletedCount
    }),

  deleteAllKnowledgeNotes: t.procedure
    .action(async () => {
      const result = await knowledgeNotesService.deleteAllNotes()
      if (result.error) {
        throw new Error(result.error)
      }
      return result.deletedCount
    }),

  searchKnowledgeNotes: t.procedure
    .input<{
      query: string
      context?: import("../shared/types").KnowledgeNoteContext
      dateFilter?: import("../shared/types").KnowledgeNoteDateFilter
      sort?: import("../shared/types").KnowledgeNoteSort
      limit?: number
    }>()
    .action(async ({ input }) => {
      return knowledgeNotesService.searchNotes(input.query, {
        context: input.context,
        dateFilter: input.dateFilter,
        sort: input.sort,
        limit: input.limit,
      })
    }),

  // Summarization service handlers
  getSessionSummaries: t.procedure
    .input<{ sessionId: string }>()
    .action(async ({ input }) => {
      return summarizationService.getSummaries(input.sessionId)
    }),

  getImportantSummaries: t.procedure
    .input<{ sessionId: string }>()
    .action(async ({ input }) => {
      return summarizationService.getImportantSummaries(input.sessionId)
    }),

  // Repeat Tasks handlers
  getLoops: t.procedure.action(async () => {
    return loopService.getLoops()
  }),

  getLoopStatuses: t.procedure.action(async () => {
    return loopService.getLoopStatuses()
  }),

  openLoopTaskFile: t.procedure
    .input<{ loopId: string }>()
    .action(async ({ input }) => {
      const loop = loopService.getLoop(input.loopId)
      if (!loop) {
        return { success: false, error: `Task with id ${input.loopId} not found` }
      }

      const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
      const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
      const { loadTasksLayer, taskIdToFilePath, writeTaskFile } = await import("./agents-files/tasks")

      const globalLayer = getAgentsLayerPaths(globalAgentsFolder)
      const workspaceDir = resolveWorkspaceAgentsFolder()
      const workspaceLayer = workspaceDir ? getAgentsLayerPaths(workspaceDir) : null

      let filePath: string | undefined

      if (workspaceLayer) {
        const workspaceLoaded = loadTasksLayer(workspaceLayer)
        filePath = workspaceLoaded.originById.get(input.loopId)?.filePath
      }

      if (!filePath) {
        const globalLoaded = loadTasksLayer(globalLayer)
        filePath = globalLoaded.originById.get(input.loopId)?.filePath
      }

      if (!filePath) {
        writeTaskFile(globalLayer, loop, { maxBackups: 10 })
        filePath = taskIdToFilePath(globalLayer, input.loopId)
      }

      return revealFileInFolder(filePath)
    }),

  saveLoop: t.procedure
    .input<{ loop: LoopConfig }>()
    .action(async ({ input }) => {
      return { success: loopService.saveLoop(input.loop) }
    }),

  deleteLoop: t.procedure
    .input<{ loopId: string }>()
    .action(async ({ input }) => {
      return { success: loopService.deleteLoop(input.loopId) }
    }),

  startLoop: t.procedure
    .input<{ loopId: string }>()
    .action(async ({ input }) => {
      return { success: loopService.startLoop(input.loopId) }
    }),

  stopLoop: t.procedure
    .input<{ loopId: string }>()
    .action(async ({ input }) => {
      return { success: loopService.stopLoop(input.loopId) }
    }),

  triggerLoop: t.procedure
    .input<{ loopId: string }>()
    .action(async ({ input }) => {
      return { success: await loopService.triggerLoop(input.loopId) }
    }),

  startAllLoops: t.procedure.action(async () => {
    loopService.startAllLoops()
    return { success: true }
  }),

  stopAllLoops: t.procedure.action(async () => {
    loopService.stopAllLoops()
    return { success: true }
  }),
}

export type Router = typeof router
