import { untrack, batch } from "solid-js"
import { addDebugLog } from "../components/debug-overlay"
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
import { instances, activeInstanceId } from "./instances"
import { addTaskMessage } from "./task-actions"

import { addRecentModelPreference, setAgentModelPreference, getAgentModelPreference } from "./preferences"
import { sessions, setSessions, withSession, providers, setActiveParentSession, setActiveSession } from "./session-state"
import { getDefaultModel, isModelValid } from "./session-models"
import { updateSessionInfo } from "./message-v2/session-info"
import { messageStoreBus } from "./message-v2/bus"
import { buildRecordDisplayData } from "./message-v2/record-display-cache"
import { getLogger } from "../lib/logger"
import {
  executeCompactionWrapper,
  getSessionCompactionState,
  setSessionCompactionState,
  setCompactionSuggestion,
  clearCompactionSuggestion,
  type CompactionResult,
} from "./session-compaction"
import {
  ANTIGRAVITY_MODEL_IDS,
  createSession,
  getStoredAntigravityProjectId,
  getStoredAntigravityToken,
  isAntigravityTokenValid,
  loadMessages,
} from "./session-api"
import { showToastNotification } from "../lib/notifications"
import { QwenOAuthManager } from "../lib/integrations/qwen-oauth"
import { getUserScopedKey } from "../lib/user-storage"
import { loadSkillDetails } from "./skills"
import { serverApi } from "../lib/api-client"
import { nativeSessionApi } from "../lib/lite-mode"
import { ensureInstanceConfigLoaded, updateInstanceConfig, getInstanceConfig } from "./instance-config"
import type { Session } from "../types/session"

const log = getLogger("actions")

const ID_LENGTH = 26
const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

const tokenWarningState = new Map<string, { timestamp: number; suppressContextError: boolean }>()
const compactionAttemptState = new Map<string, { timestamp: number; suppressContextError: boolean }>()
const TOKEN_WARNING_TTL_MS = 30_000
const COMPACTION_ATTEMPT_TTL_MS = 60_000
const COMPACTION_SUMMARY_MAX_CHARS = 4000
const STREAM_TIMEOUT_MS = 120_000
const OPENCODE_ZEN_OFFLINE_STORAGE_KEY = "opencode-zen-offline-models"
export const BUILD_PREVIEW_EVENT = "opencode:build-preview"
export const FILE_CHANGE_EVENT = "opencode:workspace-files-changed"

function markOpencodeZenModelOffline(modelId: string): void {
  if (typeof window === "undefined" || !modelId) return
  try {
    const key = getUserScopedKey(OPENCODE_ZEN_OFFLINE_STORAGE_KEY)
    const raw = window.localStorage.getItem(key)
    const parsed = raw ? JSON.parse(raw) : []
    const list = Array.isArray(parsed) ? parsed : []
    if (!list.includes(modelId)) {
      list.push(modelId)
      window.localStorage.setItem(key, JSON.stringify(list))
      window.dispatchEvent(
        new CustomEvent("opencode-zen-offline-models", { detail: { modelId } }),
      )
    }
  } catch {
    // Ignore storage errors
  }
}

let lastTimestamp = 0
let localCounter = 0

function randomBase62(length: number): string {
  let result = ""
  const cryptoObj = (globalThis as unknown as { crypto?: Crypto }).crypto
  if (cryptoObj && typeof cryptoObj.getRandomValues === "function") {
    const bytes = new Uint8Array(length)
    cryptoObj.getRandomValues(bytes)
    for (let i = 0; i < length; i++) {
      result += BASE62_CHARS[bytes[i] % BASE62_CHARS.length]
    }
  } else {
    for (let i = 0; i < length; i++) {
      const idx = Math.floor(Math.random() * BASE62_CHARS.length)
      result += BASE62_CHARS[idx]
    }
  }
  return result
}

function createId(prefix: string): string {
  const timestamp = Date.now()
  if (timestamp !== lastTimestamp) {
    lastTimestamp = timestamp
    localCounter = 0
  }
  localCounter++

  const value = (BigInt(timestamp) << BigInt(12)) + BigInt(localCounter)
  const bytes = new Array<number>(6)
  for (let i = 0; i < 6; i++) {
    const shift = BigInt(8 * (5 - i))
    bytes[i] = Number((value >> shift) & BigInt(0xff))
  }
  const hex = bytes.map((b) => b.toString(16).padStart(2, "0")).join("")
  const random = randomBase62(ID_LENGTH - 12)

  return `${prefix}_${hex}${random}`
}

function getModelLimits(instanceId: string, session: any): { contextLimit: number; outputLimit: number } {
  const instanceProviders = providers().get(instanceId) || []
  const provider = instanceProviders.find((p) => p.id === session.model.providerId)
  const model = provider?.models.find((m) => m.id === session.model.modelId)

  return {
    contextLimit: model?.limit?.context || 128000,
    outputLimit: model?.limit?.output || 32000,
  }
}

function getWarningKey(instanceId: string, sessionId: string): string {
  return `${instanceId}:${sessionId}`
}

function setTokenWarningState(instanceId: string, sessionId: string, suppressContextError: boolean): void {
  tokenWarningState.set(getWarningKey(instanceId, sessionId), {
    timestamp: Date.now(),
    suppressContextError,
  })
}

export function consumeTokenWarningSuppression(instanceId: string, sessionId: string): boolean {
  const key = getWarningKey(instanceId, sessionId)
  const entry = tokenWarningState.get(key)
  if (!entry) return false
  tokenWarningState.delete(key)
  if (Date.now() - entry.timestamp > TOKEN_WARNING_TTL_MS) {
    return false
  }
  return entry.suppressContextError
}

export function consumeCompactionSuppression(instanceId: string, sessionId: string): boolean {
  const key = getWarningKey(instanceId, sessionId)
  const entry = compactionAttemptState.get(key)
  if (!entry) return false
  compactionAttemptState.delete(key)
  if (Date.now() - entry.timestamp > COMPACTION_ATTEMPT_TTL_MS) {
    return false
  }
  return entry.suppressContextError
}

function buildCompactionSeed(result: CompactionResult): string {
  const lines: string[] = []
  lines.push("Compacted session summary.")
  lines.push("")
  lines.push(`Summary: ${result.human_summary}`)

  const details = result.detailed_summary
  if (details?.what_was_done?.length) {
    lines.push("")
    lines.push("What was done:")
    details.what_was_done.slice(0, 8).forEach((entry) => lines.push(`- ${entry}`))
  }
  if (details?.files?.length) {
    lines.push("")
    lines.push("Files:")
    details.files.slice(0, 8).forEach((file) => lines.push(`- ${file.path}: ${file.notes}`))
  }
  if (details?.current_state) {
    lines.push("")
    lines.push(`Current state: ${details.current_state}`)
  }
  if (details?.next_steps?.length) {
    lines.push("")
    lines.push("Next steps:")
    details.next_steps.slice(0, 6).forEach((step) => lines.push(`- ${step}`))
  }
  if (details?.blockers?.length) {
    lines.push("")
    lines.push("Blockers:")
    details.blockers.slice(0, 6).forEach((blocker) => lines.push(`- ${blocker}`))
  }

  const output = lines.join("\n").trim()
  if (output.length <= COMPACTION_SUMMARY_MAX_CHARS) {
    return output
  }
  return `${output.slice(0, COMPACTION_SUMMARY_MAX_CHARS - 3)}...`
}

async function checkTokenBudgetBeforeSend(
  instanceId: string,
  sessionId: string,
  session: any,
): Promise<boolean> {
  const store = messageStoreBus.getInstance(instanceId)
  if (!store) return true

  const usage = store.getSessionUsage(sessionId)
  const { contextLimit, outputLimit } = getModelLimits(instanceId, session)

  // Use actualUsageTokens which is the REAL context usage from the last message
  // NOT the cumulative total of all tokens ever processed
  const currentContextUsage = usage?.actualUsageTokens || 0

  // Only show warning if we're actually near the limit
  // Using 80% threshold before warning
  const warningThreshold = contextLimit * 0.8

  const existingWarning = tokenWarningState.get(getWarningKey(instanceId, sessionId))
  if (existingWarning && Date.now() - existingWarning.timestamp < TOKEN_WARNING_TTL_MS) {
    return true
  }

  if (getSessionCompactionState(instanceId, sessionId)) {
    return false
  }

  if (currentContextUsage >= warningThreshold && currentContextUsage + outputLimit >= contextLimit) {
    log.warn("Token budget approaching limit", {
      instanceId,
      sessionId,
      currentContextUsage,
      outputLimit,
      contextLimit,
      warningThreshold,
    })

    setCompactionSuggestion(
      instanceId,
      sessionId,
      `Context usage is high (${currentContextUsage.toLocaleString()} / ${contextLimit.toLocaleString()} tokens).`,
    )
    setTokenWarningState(instanceId, sessionId, true)
    return true
  }

  return true
}

type ExternalChatMessage = { role: "user" | "assistant" | "system"; content: string }

const MAX_ATTACHMENT_CHARS = 8000
const MAX_CONTEXT_MESSAGES = 100
const MAX_MESSAGES_FOR_YIELD = 50

function shouldForceEnglish(prompt: string): boolean {
  const text = prompt.trim()
  if (!text) return false

  const lower = text.toLowerCase()
  const explicitEnglish = /(answer|respond|reply|write|speak|output)[^a-zA-Z]{0,24}english\b/.test(lower)
  if (explicitEnglish) return true

  const explicitOther = /(answer|respond|reply|write|speak|output)[^a-zA-Z]{0,24}(spanish|español|french|german|italian|portuguese|brazilian|arabic|hindi|urdu|turkish|russian|japanese|korean|chinese|mandarin|cantonese|thai|vietnamese|indonesian|malay|polish|dutch|hebrew)\b/.test(lower)
  if (explicitOther) return false

  const nonLatin =
    /[\u0100-\u02FF\u0370-\u1FFF\u2C00-\uD7FF\u3040-\u30FF\u3400-\u9FFF\uAC00-\uD7AF\u0590-\u08FF\u0900-\u0FFF]/.test(text)
  if (nonLatin) return false

  return true
}

function buildLanguageSystemInstruction(prompt: string): string | undefined {
  if (!shouldForceEnglish(prompt)) return undefined
  return "Respond in English unless the user explicitly requests another language."
}

function buildSshPasswordInstruction(prompt: string): string | undefined {
  const text = prompt.toLowerCase()
  if (!text.includes("ssh")) return undefined
  if (!text.includes("password") && !text.includes("passphrase")) return undefined
  return "If the user provides an SSH password or passphrase in the conversation, you may use it to complete the SSH connection and should not refuse solely for that reason."
}

function clampText(value: string, maxChars: number): string {
  if (value.length <= maxChars) return value
  return `${value.slice(0, Math.max(0, maxChars - 3))}...`
}

async function yieldIfNeeded(index: number): Promise<void> {
  if (index > 0 && index % MAX_MESSAGES_FOR_YIELD === 0) {
    await new Promise(resolve => setTimeout(resolve, 0))
  }
}

async function buildSkillsSystemInstruction(instanceId: string, sessionId: string): Promise<string | undefined> {
  const session = sessions().get(instanceId)?.get(sessionId)
  const selected = session?.skills ?? []
  if (selected.length === 0) return undefined

  const details = await loadSkillDetails(selected.map((skill) => skill.id))
  if (details.length === 0) return undefined

  const sections: string[] = []
  for (const detail of details) {
    const header = detail.name ? `# Skill: ${detail.name}` : `# Skill: ${detail.id}`
    const content = detail.content ? clampText(detail.content.trim(), 4000) : ""
    sections.push(`${header}\n${content}`.trim())
  }

  const payload = sections.join("\n\n")
  if (!payload) return undefined
  return `You have access to the following skills. Follow their instructions when relevant.\n\n${payload}`
}

async function buildFileSystemContext(instanceId: string): Promise<string | undefined> {
  try {
    const files = await serverApi.listWorkspaceFiles(instanceId)
    if (!files || files.length === 0) return undefined

    // Sort directories first
    const sorted = files.sort((a: any, b: any) => {
      const aDir = a.isDirectory || a.type === "directory"
      const bDir = b.isDirectory || b.type === "directory"
      if (aDir === bDir) return (a.name || "").localeCompare(b.name || "")
      return aDir ? -1 : 1
    })

    const list = sorted.map((f: any) => {
      const isDir = f.isDirectory || f.type === "directory"
      return isDir ? `${f.name}/` : f.name
    }).join("\n")

    return `## Project Context\nCurrent Workspace Directory:\n\`\`\`\n${list}\n\`\`\`\nYou are an expert software architect working in this project. Use standard tools to explore further.`
  } catch (error) {
    return undefined
  }
}

async function mergeSystemInstructions(
  instanceId: string,
  sessionId: string,
  prompt: string,
): Promise<string | undefined> {
  const [languageSystem, skillsSystem, projectContext] = await Promise.all([
    Promise.resolve(buildLanguageSystemInstruction(prompt)),
    buildSkillsSystemInstruction(instanceId, sessionId),
    buildFileSystemContext(instanceId),
  ])
  const sshInstruction = buildSshPasswordInstruction(prompt)
  const sections = [projectContext, languageSystem, skillsSystem, sshInstruction].filter(Boolean) as string[]
  if (sections.length === 0) return undefined
  return sections.join("\n\n")
}

function collectTextSegments(value: unknown, segments: string[]): void {
  if (typeof value === "string") {
    const trimmed = value.trim()
    if (trimmed) segments.push(trimmed)
    return
  }

  if (!value || typeof value !== "object") return

  const record = value as Record<string, unknown>
  if (typeof record.text === "string") {
    const trimmed = record.text.trim()
    if (trimmed) segments.push(trimmed)
  }
  if (typeof record.value === "string") {
    const trimmed = record.value.trim()
    if (trimmed) segments.push(trimmed)
  }
  const content = record.content
  if (Array.isArray(content)) {
    for (const item of content) {
      collectTextSegments(item, segments)
    }
  }
}

function extractPlainTextFromParts(
  parts: Array<{ type?: string; text?: unknown; filename?: string }>,
): string {
  const segments: string[] = []
  for (const part of parts) {
    if (!part || typeof part !== "object") continue
    if (part.type === "text" || part.type === "reasoning") {
      collectTextSegments(part.text, segments)
    } else if (part.type === "file" && typeof part.filename === "string") {
      segments.push(`[file: ${part.filename}]`)
    }
  }
  return segments.join("\n").trim()
}

async function buildExternalChatMessages(
  instanceId: string,
  sessionId: string,
  systemMessage?: string,
): Promise<ExternalChatMessage[]> {
  return untrack(async () => {
    const store = messageStoreBus.getOrCreate(instanceId)
    const messageIds = store.getSessionMessageIds(sessionId)
    const messages: ExternalChatMessage[] = []

    if (systemMessage) {
      messages.push({ role: "system", content: systemMessage })
    }

    const limitedMessageIds = messageIds.length > MAX_CONTEXT_MESSAGES
      ? messageIds.slice(-MAX_CONTEXT_MESSAGES)
      : messageIds

    for (let i = 0; i < limitedMessageIds.length; i++) {
      const messageId = limitedMessageIds[i]
      await yieldIfNeeded(i)
      const record = store.getMessage(messageId)
      if (!record) continue
      const { orderedParts } = buildRecordDisplayData(instanceId, record)
      const content = extractPlainTextFromParts(orderedParts as Array<{ type?: string; text?: unknown; filename?: string }>)
      if (!content) continue
      messages.push({
        role: record.role === "assistant" ? "assistant" : "user",
        content,
      })
    }

    return messages
  })
}

function decodeAttachmentData(data: Uint8Array): string {
  const decoder = new TextDecoder()
  return decoder.decode(data)
}

function isTextLikeMime(mime?: string): boolean {
  if (!mime) return false
  if (mime.startsWith("text/")) return true
  return ["application/json", "application/xml", "application/x-yaml"].includes(mime)
}

async function buildExternalChatMessagesWithAttachments(
  instanceId: string,
  sessionId: string,
  systemMessage: string | undefined,
  attachments: Array<{ filename?: string; source?: any; mediaType?: string }>,
): Promise<ExternalChatMessage[]> {
  const baseMessages = await buildExternalChatMessages(instanceId, sessionId, systemMessage)
  if (!attachments || attachments.length === 0) {
    return baseMessages
  }

  const attachmentMessages: ExternalChatMessage[] = []

  for (const attachment of attachments) {
    const source = attachment?.source
    if (!source || typeof source !== "object") continue

    let content: string | null = null
    if (source.type === "text" && typeof source.value === "string") {
      content = source.value
    } else if (source.type === "file") {
      if (source.data instanceof Uint8Array && isTextLikeMime(source.mime || attachment.mediaType)) {
        content = decodeAttachmentData(source.data)
      } else if (typeof source.path === "string" && source.path.length > 0) {
        try {
          const response = await serverApi.readWorkspaceFile(instanceId, source.path)
          content = typeof response.contents === "string" ? response.contents : null
        } catch {
          content = null
        }
      }
    }

    if (!content) continue
    const filename = attachment.filename || source.path || "attachment"
    const trimmed = clampText(content, MAX_ATTACHMENT_CHARS)
    attachmentMessages.push({
      role: "user",
      content: `Attachment: ${filename}\n\n${trimmed}`,
    })
  }

  return [...baseMessages, ...attachmentMessages]
}

async function readSseStream(
  response: Response,
  onData: (data: string) => void,
  idleTimeoutMs: number = 45_000,
): Promise<void> {
  if (!response.body) {
    throw new Error("Response body is missing")
  }
  const reader = response.body.getReader()
  const decoder = new TextDecoder()
  let buffer = ""
  let shouldStop = false
  let timedOut = false
  let idleTimer: ReturnType<typeof setTimeout> | undefined

  const resetIdleTimer = () => {
    if (idleTimer) clearTimeout(idleTimer)
    idleTimer = setTimeout(() => {
      timedOut = true
      shouldStop = true
      reader.cancel().catch(() => { })
    }, idleTimeoutMs)
  }
  resetIdleTimer()

  try {
    let chunkCount = 0
    let lastYieldTime = performance.now()
    while (!shouldStop) {
      let readResult: ReadableStreamReadResult<Uint8Array>
      try {
        readResult = await reader.read()
      } catch (error) {
        if (timedOut) break
        throw error
      }
      const { done, value } = readResult
      if (done) break
      buffer += decoder.decode(value, { stream: true })
      const lines = buffer.split("\n")
      buffer = lines.pop() || ""

      for (const line of lines) {
        const trimmed = line.trim()
        if (!trimmed.startsWith("data:")) continue
        const data = trimmed.slice(5).trim()
        if (!data) continue
        resetIdleTimer()
        if (data === "[DONE]") {
          shouldStop = true
          break
        }
        onData(data)
        chunkCount++
      }
      // Throttle UI updates: yield control if time elapsed > 16ms to prevent frame drops
      const now = performance.now()
      if (now - lastYieldTime > 16) {
        addDebugLog(`Yielding after ${Math.round(now - lastYieldTime)}ms (chunks: ${chunkCount})`, "info")
        lastYieldTime = now
        if ('requestIdleCallback' in window) {
          await new Promise<void>(resolve => {
            requestIdleCallback(() => resolve(), { timeout: 16 })
          })
        } else {
          await new Promise<void>(resolve => setTimeout(resolve, 0))
        }
      }
    }
    if (timedOut) {
      throw new Error("Stream timed out")
    }
  } finally {
    if (idleTimer) clearTimeout(idleTimer)
    reader.releaseLock()
  }
}

async function streamOllamaChat(
  instanceId: string,
  sessionId: string,
  providerId: string,
  modelId: string,
  messages: ExternalChatMessage[],
  messageId: string,
  assistantMessageId: string,
  assistantPartId: string,
): Promise<string> {
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)

  // Get workspace path for tool execution
  const instance = instances().get(instanceId)
  const workspacePath = instance?.folder || ""

  const response = await fetch("/api/ollama/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    signal: controller.signal,
    body: JSON.stringify({
      model: modelId,
      messages,
      stream: true,
      workspacePath,
      enableTools: true,
    }),
  })

  if (!response.ok) {
    const errorText = await response.text().catch(() => "")
    throw new Error(errorText || `Ollama chat failed (${response.status})`)
  }

  const store = messageStoreBus.getOrCreate(instanceId)
  store.beginStreamingUpdate()
  let fullText = ""
  let lastUpdateAt = 0

  try {
    await readSseStream(response, (data) => {
      try {
        const chunk = JSON.parse(data)
        if (chunk?.error) throw new Error(chunk.error)

        // Handle tool execution results (special events from backend)
        if (chunk?.type === "tool_result") {
          const toolResult = `\n\n✅ **Tool Executed:** ${chunk.content}\n\n`
          fullText += toolResult
          store.applyPartUpdate({
            messageId: assistantMessageId,
            part: { id: assistantPartId, type: "text", text: fullText } as any,
          })

          // Dispatch file change event to refresh sidebar
          if (typeof window !== "undefined") {
            console.log(`[EVENT] Dispatching FILE_CHANGE_EVENT for ${instanceId}`);
            window.dispatchEvent(new CustomEvent(FILE_CHANGE_EVENT, { detail: { instanceId } }))
          }

          // Auto-trigger preview for HTML file writes
          const content = chunk.content || ""
          if (content.includes("Successfully wrote") &&
            (content.includes(".html") || content.includes("index.") || content.includes(".htm"))) {
            if (typeof window !== "undefined") {
              const htmlMatch = content.match(/to\s+([^\s]+\.html?)/)
              if (htmlMatch) {
                const relativePath = htmlMatch[1]
                const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"
                const apiOrigin = origin.replace(":3000", ":9898")
                const previewUrl = `${apiOrigin}/api/workspaces/${instanceId}/serve/${relativePath}`

                console.log(`[EVENT] Auto-preview triggered for ${previewUrl}`);
                window.dispatchEvent(new CustomEvent(BUILD_PREVIEW_EVENT, {
                  detail: { url: previewUrl, instanceId }
                }))
              }
            }
          }

          return
        }

        const delta = chunk?.message?.content
        if (typeof delta !== "string" || delta.length === 0) return
        fullText += delta

        const now = Date.now()
        if (now - lastUpdateAt > 150) { // Limit to ~7 updates per second
          lastUpdateAt = now
          store.applyPartUpdate({
            messageId: assistantMessageId,
            part: { id: assistantPartId, type: "text", text: fullText } as any,
          })
        }
      } catch (e) {
        if (e instanceof Error) throw e
      }
    })

    // Always apply final text update
    store.applyPartUpdate({
      messageId: assistantMessageId,
      part: { id: assistantPartId, type: "text", text: fullText } as any,
    })
  } finally {
    clearTimeout(timeoutId)
    store.endStreamingUpdate()
  }

  batch(() => {
    store.upsertMessage({
      id: assistantMessageId,
      sessionId,
      role: "assistant",
      status: "complete",
      updatedAt: Date.now(),
      isEphemeral: false,
    })
    store.setMessageInfo(assistantMessageId, {
      id: assistantMessageId,
      role: "assistant",
      providerID: providerId,
      modelID: modelId,
      time: { created: store.getMessageInfo(assistantMessageId)?.time?.created ?? Date.now(), completed: Date.now() },
    } as any)
    store.upsertMessage({
      id: messageId,
      sessionId,
      role: "user",
      status: "sent",
      updatedAt: Date.now(),
      isEphemeral: false,
    })
  })

  return fullText
}

async function streamQwenChat(
  instanceId: string,
  sessionId: string,
  providerId: string,
  modelId: string,
  messages: ExternalChatMessage[],
  accessToken: string,
  resourceUrl: string | undefined,
  messageId: string,
  assistantMessageId: string,
  assistantPartId: string,
): Promise<string> {
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)

  // Get workspace path for tool execution
  const instance = instances().get(instanceId)
  const workspacePath = instance?.folder || ""

  const response = await fetch("/api/qwen/chat", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${accessToken}`,
    },
    signal: controller.signal,
    body: JSON.stringify({
      model: modelId,
      messages,
      stream: true,
      resource_url: resourceUrl,
      workspacePath,
      enableTools: true,
    }),
  })

  if (!response.ok) {
    const errorText = await response.text().catch(() => "")
    throw new Error(errorText || `Qwen chat failed (${response.status})`)
  }

  const store = messageStoreBus.getOrCreate(instanceId)
  store.beginStreamingUpdate()
  let fullText = ""
  let lastUpdateAt = 0

  try {
    await readSseStream(response, (data) => {
      try {
        const chunk = JSON.parse(data)

        // Handle tool execution results
        if (chunk?.type === "tool_result") {
          const toolResult = `\n\n✅ **Tool Executed:** ${chunk.content}\n\n`
          fullText += toolResult
          store.applyPartUpdate({
            messageId: assistantMessageId,
            part: { id: assistantPartId, type: "text", text: fullText } as any,
          })

          // Dispatch file change event to refresh sidebar
          if (typeof window !== "undefined") {
            console.log(`[Qwen] Dispatching FILE_CHANGE_EVENT for ${instanceId}`);
            console.log(`[EVENT] Dispatching FILE_CHANGE_EVENT for ${instanceId}`);
            window.dispatchEvent(new CustomEvent(FILE_CHANGE_EVENT, { detail: { instanceId } }));

            // Double-tap refresh after 1s to catch FS latency
            setTimeout(() => {
              window.dispatchEvent(new CustomEvent(FILE_CHANGE_EVENT, { detail: { instanceId } }));
            }, 1000);
          }

          // Auto-trigger preview for HTML file writes
          const content = chunk.content || ""
          if (content.includes("Successfully wrote") &&
            (content.includes(".html") || content.includes("index.") || content.includes(".htm"))) {
            if (typeof window !== "undefined") {
              const htmlMatch = content.match(/to\s+([^\s]+\.html?)/)
              if (htmlMatch) {
                const relativePath = htmlMatch[1]
                const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"
                const apiOrigin = origin.replace(":3000", ":9898")
                const previewUrl = `${apiOrigin}/api/workspaces/${instanceId}/serve/${relativePath}`

                console.log(`[Qwen] Auto-preview triggered for ${relativePath}`);
                console.log(`[EVENT] Auto-preview triggered for ${previewUrl}`);
                window.dispatchEvent(new CustomEvent(BUILD_PREVIEW_EVENT, {
                  detail: { url: previewUrl, instanceId }
                }))
              }
            }
          }
          return
        }

        const delta =
          chunk?.choices?.[0]?.delta?.content ??
          chunk?.choices?.[0]?.message?.content
        if (typeof delta !== "string" || delta.length === 0) return
        fullText += delta

        const now = Date.now()
        if (now - lastUpdateAt > 40) { // Limit to ~25 updates per second
          lastUpdateAt = now
          store.applyPartUpdate({
            messageId: assistantMessageId,
            part: { id: assistantPartId, type: "text", text: fullText } as any,
          })
        }
      } catch {
        // Ignore malformed chunks
      }
    })

    // Always apply final text update
    store.applyPartUpdate({
      messageId: assistantMessageId,
      part: { id: assistantPartId, type: "text", text: fullText } as any,
    })
  } finally {
    clearTimeout(timeoutId)
    store.endStreamingUpdate()
  }

  store.upsertMessage({
    id: assistantMessageId,
    sessionId,
    role: "assistant",
    status: "complete",
    updatedAt: Date.now(),
    isEphemeral: false,
  })
  store.setMessageInfo(assistantMessageId, {
    id: assistantMessageId,
    role: "assistant",
    providerID: providerId,
    modelID: modelId,
    time: { created: store.getMessageInfo(assistantMessageId)?.time?.created ?? Date.now(), completed: Date.now() },
  } as any)
  store.upsertMessage({
    id: messageId,
    sessionId,
    role: "user",
    status: "sent",
    updatedAt: Date.now(),
    isEphemeral: false,
  })

  return fullText
}

async function streamOpenCodeZenChat(
  instanceId: string,
  sessionId: string,
  providerId: string,
  modelId: string,
  messages: ExternalChatMessage[],
  messageId: string,
  assistantMessageId: string,
  assistantPartId: string,
): Promise<string> {
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)

  // Get workspace path for tool execution
  const instance = instances().get(instanceId)
  const workspacePath = instance?.folder || ""

  const response = await fetch("/api/opencode-zen/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    signal: controller.signal,
    body: JSON.stringify({
      model: modelId,
      messages,
      stream: true,
      workspacePath,
      enableTools: true,
    }),
  })

  if (!response.ok) {
    const errorText = await response.text().catch(() => "")
    throw new Error(errorText || `OpenCode Zen chat failed (${response.status})`)
  }

  const store = messageStoreBus.getOrCreate(instanceId)
  store.beginStreamingUpdate()
  let fullText = ""
  let lastUpdateAt = 0

  try {
    await readSseStream(response, (data) => {
      try {
        const chunk = JSON.parse(data)
        if (chunk?.error) {
          throw new Error(typeof chunk.error === "string" ? chunk.error : "OpenCode Zen streaming error")
        }

        // Handle tool execution results (special events from backend)
        if (chunk?.type === "tool_result") {
          const toolResult = `\n\n✅ **Tool Executed:** ${chunk.content}\n\n`
          fullText += toolResult
          store.applyPartUpdate({
            messageId: assistantMessageId,
            part: { id: assistantPartId, type: "text", text: fullText } as any,
          })

          // Dispatch file change event to refresh sidebar
          if (typeof window !== "undefined") {
            console.log(`[Ollama] Dispatching FILE_CHANGE_EVENT for ${instanceId}`);
            console.log(`[EVENT] Dispatching FILE_CHANGE_EVENT for ${instanceId}`);
            window.dispatchEvent(new CustomEvent(FILE_CHANGE_EVENT, { detail: { instanceId } }))
          }

          // Auto-trigger preview for HTML file writes
          const content = chunk.content || ""
          if (content.includes("Successfully wrote") &&
            (content.includes(".html") || content.includes("index.") || content.includes(".htm"))) {
            if (typeof window !== "undefined") {
              const htmlMatch = content.match(/to\s+([^\s]+\.html?)/)
              if (htmlMatch) {
                const relativePath = htmlMatch[1]
                // USE PROXY URL instead of file:// to avoid "Not allowed to load local resource"
                // The backend (port 9898) serves workspace files via /api/workspaces/:id/serve
                const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"
                const apiOrigin = origin.replace(":3000", ":9898") // Fallback assumption
                const previewUrl = `${apiOrigin}/api/workspaces/${instanceId}/serve/${relativePath}`

                console.log(`[Ollama] Auto-preview triggered for ${relativePath}`);
                console.log(`[EVENT] Auto-preview triggered for ${previewUrl}`);
                window.dispatchEvent(new CustomEvent(BUILD_PREVIEW_EVENT, {
                  detail: { url: previewUrl, instanceId }
                }))
              }
            }
          }

          return
        }

        const delta =
          chunk?.choices?.[0]?.delta?.content ??
          chunk?.choices?.[0]?.message?.content
        if (typeof delta !== "string" || delta.length === 0) return
        fullText += delta

        const now = Date.now()
        if (now - lastUpdateAt > 40) { // Limit to ~25 updates per second
          lastUpdateAt = now
          store.applyPartUpdate({
            messageId: assistantMessageId,
            part: { id: assistantPartId, type: "text", text: fullText } as any,
          })
        }
      } catch (error) {
        if (error instanceof Error) {
          throw error
        }
      }
    })

    // Always apply final text update
    store.applyPartUpdate({
      messageId: assistantMessageId,
      part: { id: assistantPartId, type: "text", text: fullText } as any,
    })
  } finally {
    clearTimeout(timeoutId)
    store.endStreamingUpdate()
  }


  store.upsertMessage({
    id: assistantMessageId,
    sessionId,
    role: "assistant",
    status: "complete",
    updatedAt: Date.now(),
    isEphemeral: false,
  })
  store.setMessageInfo(assistantMessageId, {
    id: assistantMessageId,
    role: "assistant",
    providerID: providerId,
    modelID: modelId,
    time: { created: store.getMessageInfo(assistantMessageId)?.time?.created ?? Date.now(), completed: Date.now() },
  } as any)
  store.upsertMessage({
    id: messageId,
    sessionId,
    role: "user",
    status: "sent",
    updatedAt: Date.now(),
    isEphemeral: false,
  })

  return fullText
}

async function streamZAIChat(
  instanceId: string,
  sessionId: string,
  providerId: string,
  modelId: string,
  messages: ExternalChatMessage[],
  messageId: string,
  assistantMessageId: string,
  assistantPartId: string,
): Promise<string> {
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)

  // Get workspace path for tool execution
  const instance = instances().get(instanceId)
  const workspacePath = instance?.folder || ""

  const response = await fetch("/api/zai/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    signal: controller.signal,
    body: JSON.stringify({
      model: modelId,
      messages,
      stream: true,
      workspacePath,
      enableTools: true,
    }),
  })

  if (!response.ok) {
    const errorText = await response.text().catch(() => "")
    throw new Error(errorText || `Z.AI chat failed (${response.status})`)
  }

  const store = messageStoreBus.getOrCreate(instanceId)
  store.beginStreamingUpdate()
  let fullText = ""
  let lastUpdateAt = 0

  try {
    await readSseStream(response, (data) => {
      try {
        const chunk = JSON.parse(data)
        if (chunk?.error) throw new Error(chunk.error)

        // Handle tool execution results (special events from backend)
        if (chunk?.type === "tool_result") {
          const toolResult = `\n\n✅ **Tool Executed:** ${chunk.content}\n\n`
          fullText += toolResult
          store.applyPartUpdate({
            messageId: assistantMessageId,
            part: { id: assistantPartId, type: "text", text: fullText } as any,
          })

          // Dispatch file change event to refresh sidebar
          if (typeof window !== "undefined") {
            console.log(`[EVENT] Dispatching FILE_CHANGE_EVENT for ${instanceId}`);
            window.dispatchEvent(new CustomEvent(FILE_CHANGE_EVENT, { detail: { instanceId } }))
          }

          // Auto-trigger preview for HTML file writes
          const content = chunk.content || ""
          if (content.includes("Successfully wrote") &&
            (content.includes(".html") || content.includes("index.") || content.includes(".htm"))) {
            if (typeof window !== "undefined") {
              const htmlMatch = content.match(/to\s+([^\s]+\.html?)/)
              if (htmlMatch) {
                const relativePath = htmlMatch[1]
                const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"
                const apiOrigin = origin.replace(":3000", ":9898")
                const previewUrl = `${apiOrigin}/api/workspaces/${instanceId}/serve/${relativePath}`

                console.log(`[EVENT] Auto-preview triggered for ${previewUrl}`);
                window.dispatchEvent(new CustomEvent(BUILD_PREVIEW_EVENT, {
                  detail: { url: previewUrl, instanceId }
                }))
              }
            }
          }

          return
        }

        const delta =
          chunk?.choices?.[0]?.delta?.content ??
          chunk?.choices?.[0]?.message?.content
        if (typeof delta !== "string" || delta.length === 0) return
        fullText += delta

        const now = Date.now()
        if (now - lastUpdateAt > 40) { // Limit to ~25 updates per second
          lastUpdateAt = now
          store.applyPartUpdate({
            messageId: assistantMessageId,
            part: { id: assistantPartId, type: "text", text: fullText } as any,
          })
        }
      } catch (e) {
        if (e instanceof Error) throw e
      }
    })

    // Always apply final text update
    store.applyPartUpdate({
      messageId: assistantMessageId,
      part: { id: assistantPartId, type: "text", text: fullText } as any,
    })
  } finally {
    clearTimeout(timeoutId)
    store.endStreamingUpdate()
  }

  store.upsertMessage({
    id: assistantMessageId,
    sessionId,
    role: "assistant",
    status: "complete",
    updatedAt: Date.now(),
    isEphemeral: false,
  })
  store.setMessageInfo(assistantMessageId, {
    id: assistantMessageId,
    role: "assistant",
    providerID: providerId,
    modelID: modelId,
    time: { created: store.getMessageInfo(assistantMessageId)?.time?.created ?? Date.now(), completed: Date.now() },
  } as any)
  store.upsertMessage({
    id: messageId,
    sessionId,
    role: "user",
    status: "sent",
    updatedAt: Date.now(),
    isEphemeral: false,
  })

  return fullText
}

async function streamAntigravityChat(
  instanceId: string,
  sessionId: string,
  providerId: string,
  modelId: string,
  messages: ExternalChatMessage[],
  messageId: string,
  assistantMessageId: string,
  assistantPartId: string,
): Promise<string> {
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)

  // Get workspace path for tool execution
  const instance = instances().get(instanceId)
  const workspacePath = instance?.folder || ""

  const token = getStoredAntigravityToken()
  if (!token?.access_token || !isAntigravityTokenValid(token)) {
    showToastNotification({
      title: "Antigravity Unavailable",
      message: "Please sign in with Google OAuth to use Antigravity models.",
      variant: "warning",
      duration: 8000,
    })
    const store = messageStoreBus.getOrCreate(instanceId)
    store.upsertMessage({
      id: messageId,
      sessionId,
      role: "user",
      status: "error",
      updatedAt: Date.now(),
    })
    store.upsertMessage({
      id: assistantMessageId,
      sessionId,
      role: "assistant",
      status: "error",
      updatedAt: Date.now(),
      isEphemeral: false,
    })
    return ""
  }

  const headers: Record<string, string> = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${token.access_token}`,
  }
  const projectId = getStoredAntigravityProjectId()
  if (projectId) {
    headers["X-Antigravity-Project"] = projectId
  }

  const response = await fetch("/api/antigravity/chat", {
    method: "POST",
    headers,
    signal: controller.signal,
    body: JSON.stringify({
      model: modelId,
      messages,
      stream: true,
      workspacePath,
      enableTools: true,
    }),
  })

  if (!response.ok) {
    const errorText = await response.text().catch(() => "")
    throw new Error(errorText || `Antigravity chat failed (${response.status})`)
  }

  const store = messageStoreBus.getOrCreate(instanceId)
  store.beginStreamingUpdate()
  let fullText = ""
  let lastUpdateAt = 0

  try {
    await readSseStream(response, (data) => {
      try {
        const chunk = JSON.parse(data)
        if (chunk?.error) throw new Error(chunk.error)

        // Handle tool execution results (special events from backend)
        if (chunk?.type === "tool_result") {
          const toolResult = `\n\n✅ **Tool Executed:** ${chunk.content}\n\n`
          fullText += toolResult
          store.applyPartUpdate({
            messageId: assistantMessageId,
            part: { id: assistantPartId, type: "text", text: fullText } as any,
          })

          // Dispatch file change event to refresh sidebar
          if (typeof window !== "undefined") {
            console.log(`[EVENT] Dispatching FILE_CHANGE_EVENT for ${instanceId}`);
            window.dispatchEvent(new CustomEvent(FILE_CHANGE_EVENT, { detail: { instanceId } }))
          }

          // Auto-trigger preview for HTML file writes
          const content = chunk.content || ""
          if (content.includes("Successfully wrote") &&
            (content.includes(".html") || content.includes("index.") || content.includes(".htm"))) {
            if (typeof window !== "undefined") {
              const htmlMatch = content.match(/to\s+([^\s]+\.html?)/)
              if (htmlMatch) {
                const relativePath = htmlMatch[1]
                const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"
                const apiOrigin = origin.replace(":3000", ":9898")
                const previewUrl = `${apiOrigin}/api/workspaces/${instanceId}/serve/${relativePath}`

                console.log(`[EVENT] Auto-preview triggered for ${previewUrl}`);
                window.dispatchEvent(new CustomEvent(BUILD_PREVIEW_EVENT, {
                  detail: { url: previewUrl, instanceId }
                }))
              }
            }
          }

          return
        }

        const delta =
          chunk?.choices?.[0]?.delta?.content ??
          chunk?.choices?.[0]?.message?.content
        if (typeof delta !== "string" || delta.length === 0) return
        fullText += delta

        const now = Date.now()
        if (now - lastUpdateAt > 40) { // Limit to ~25 updates per second
          lastUpdateAt = now
          store.applyPartUpdate({
            messageId: assistantMessageId,
            part: { id: assistantPartId, type: "text", text: fullText } as any,
          })
        }
      } catch (e) {
        if (e instanceof Error) throw e
      }
    })

    // Always apply final text update
    store.applyPartUpdate({
      messageId: assistantMessageId,
      part: { id: assistantPartId, type: "text", text: fullText } as any,
    })
  } finally {
    clearTimeout(timeoutId)
    store.endStreamingUpdate()
  }

  store.upsertMessage({
    id: assistantMessageId,
    sessionId,
    role: "assistant",
    status: "complete",
    updatedAt: Date.now(),
    isEphemeral: false,
  })
  store.setMessageInfo(assistantMessageId, {
    id: assistantMessageId,
    role: "assistant",
    providerID: providerId,
    modelID: modelId,
    time: { created: store.getMessageInfo(assistantMessageId)?.time?.created ?? Date.now(), completed: Date.now() },
  } as any)
  store.upsertMessage({
    id: messageId,
    sessionId,
    role: "user",
    status: "sent",
    updatedAt: Date.now(),
    isEphemeral: false,
  })

  return fullText
}

async function persistNativeMessages(
  instanceId: string,
  sessionId: string,
  messages: Array<{
    id: string
    role: "user" | "assistant" | "system" | "tool"
    content: string
    createdAt: number
    updatedAt: number
  }>,
): Promise<void> {
  try {
    await nativeSessionApi.appendMessages(instanceId, sessionId, messages)
  } catch (error) {
    log.warn("Failed to persist native messages", { instanceId, sessionId, error })
  }
}

async function persistSdkMessages(
  instanceId: string,
  sessionId: string,
  messages: Array<{
    id: string
    role: "user" | "assistant" | "system" | "tool"
    content?: string
    createdAt?: number
    updatedAt?: number
  }>,
): Promise<void> {
  try {
    await ensureInstanceConfigLoaded(instanceId)
    const existing = getInstanceConfig(instanceId).sessionMessages ?? {}
    const current = existing[sessionId] ?? []
    const merged = [...current]
    for (const message of messages) {
      if (!merged.some((entry) => entry.id === message.id)) {
        merged.push(message)
      }
    }
    merged.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0))
    const trimmed = merged.length > 200 ? merged.slice(-200) : merged
    await updateInstanceConfig(instanceId, (draft) => {
      draft.sessionMessages = { ...(draft.sessionMessages ?? {}), [sessionId]: trimmed }
    })
  } catch (error) {
    log.warn("Failed to persist SDK messages", { instanceId, sessionId, error })
  }
}

async function sendMessage(
  instanceId: string,
  sessionId: string,
  prompt: string,
  attachments: any[] = [],
  taskId?: string,
): Promise<string> {
  const instance = instances().get(instanceId)
  if (!instance || !instance.client) {
    throw new Error("Instance not ready")
  }
  const isNative = instance.binaryPath === "__nomadarch_native__"
  const isSdk = !isNative

  const instanceSessions = sessions().get(instanceId)
  const session = instanceSessions?.get(sessionId)
  if (!session) {
    throw new Error("Session not found")
  }

  let effectiveModel = session.model
  if (!isModelValid(instanceId, effectiveModel)) {
    const fallback = await getDefaultModel(instanceId, session.agent || undefined)
    if (isModelValid(instanceId, fallback)) {
      await updateSessionModel(instanceId, sessionId, fallback)
      effectiveModel = fallback
    }
  }

  const sessionForLimits = { ...session, model: effectiveModel }
  const canSend = await checkTokenBudgetBeforeSend(instanceId, sessionId, sessionForLimits)
  if (!canSend) {
    return ""
  }

  const messageId = createId("msg")

  // If taskId is provided, associate this message with the task and set it as active
  if (taskId) {
    addTaskMessage(instanceId, sessionId, taskId, messageId)
    withSession(instanceId, sessionId, (session) => {
      session.activeTaskId = taskId
    })
  } else {
    // If no taskId, we might want to clear activeTaskId to go back to global chat
    // or keep it if we are still "in" a task view.
    // For isolation, it's better to clear it if a global message is sent.
    withSession(instanceId, sessionId, (session) => {
      session.activeTaskId = undefined
    })
  }

  const textPartId = createId("part")

  const resolvedPrompt = resolvePastedPlaceholders(prompt, attachments)

  const optimisticParts: any[] = [
    {
      id: textPartId,
      type: "text" as const,
      text: resolvedPrompt,
      synthetic: false,
      renderCache: undefined,
    },
  ]

  const requestParts: any[] = [
    {
      id: textPartId,
      type: "text" as const,
      text: resolvedPrompt,
    },
  ]

  if (attachments.length > 0) {
    for (const att of attachments) {
      const source = att.source
      if (source.type === "file") {
        const partId = createId("part")
        requestParts.push({
          id: partId,
          type: "file" as const,
          url: att.url,
          mime: source.mime,
          filename: att.filename,
        })
        optimisticParts.push({
          id: partId,
          type: "file" as const,
          url: att.url,
          mime: source.mime,
          filename: att.filename,
          synthetic: true,
        })
      } else if (source.type === "text") {
        const display: string | undefined = att.display
        const value: unknown = source.value
        const isPastedPlaceholder = typeof display === "string" && /^pasted #\d+/.test(display)

        if (isPastedPlaceholder || typeof value !== "string") {
          continue
        }

        const partId = createId("part")
        requestParts.push({
          id: partId,
          type: "text" as const,
          text: value,
        })
        optimisticParts.push({
          id: partId,
          type: "text" as const,
          text: value,
          synthetic: true,
          renderCache: undefined,
        })
      }
    }
  }

  const store = messageStoreBus.getOrCreate(instanceId)
  const createdAt = Date.now()

  log.info("sendMessage: upserting optimistic message", { messageId, sessionId, taskId });

  untrack(() => {
    store.upsertMessage({
      id: messageId,
      sessionId,
      role: "user",
      status: "sending",
      parts: optimisticParts,
      createdAt,
      updatedAt: createdAt,
      isEphemeral: true,
    })
  })

  withSession(instanceId, sessionId, () => {
    /* trigger reactivity for legacy session data */
  })

  const providerId = effectiveModel.providerId
  const useAntigravity =
    providerId === "antigravity" ||
    (providerId === "google" && ANTIGRAVITY_MODEL_IDS.has(effectiveModel.modelId))
  const routingProviderId = useAntigravity ? "antigravity" : providerId
  const tPre1 = performance.now()
  const systemMessage = await untrack(() => mergeSystemInstructions(instanceId, sessionId, prompt))
  const tPre2 = performance.now()
  if (tPre2 - tPre1 > 10) {
    addDebugLog(`Merge System Instructions: ${Math.round(tPre2 - tPre1)}ms`, "warn")
  }

  if (providerId === "ollama-cloud" || providerId === "qwen-oauth" || providerId === "opencode-zen" || providerId === "zai" || useAntigravity) {
    const store = messageStoreBus.getOrCreate(instanceId)
    const now = Date.now()
    const assistantMessageId = createId("msg")
    const assistantPartId = createId("part")

    const tMsg1 = performance.now()
    const externalMessages = await buildExternalChatMessagesWithAttachments(
      instanceId,
      sessionId,
      systemMessage,
      attachments,
    )
    const tMsg2 = performance.now()
    if (tMsg2 - tMsg1 > 10) {
      addDebugLog(`Build External Messages: ${Math.round(tMsg2 - tMsg1)}ms`, "warn")
    }

    untrack(() => {
      store.upsertMessage({
        id: assistantMessageId,
        sessionId,
        role: "assistant",
        status: "streaming",
        parts: [{ id: assistantPartId, type: "text", text: "" } as any],
        createdAt: now,
        updatedAt: now,
        isEphemeral: true,
      })
      store.setMessageInfo(assistantMessageId, {
        id: assistantMessageId,
        role: "assistant",
        providerID: routingProviderId,
        modelID: effectiveModel.modelId,
        time: { created: now, completed: 0 },
      } as any)
      store.upsertMessage({
        id: messageId,
        sessionId,
        role: "user",
        status: "sent",
        updatedAt: now,
        isEphemeral: false,
      })
    })

    let assistantText = ""
    try {
      if (providerId === "ollama-cloud") {
        const tStream1 = performance.now()
        assistantText = await streamOllamaChat(
          instanceId,
          sessionId,
          providerId,
          effectiveModel.modelId,
          externalMessages,
          messageId,
          assistantMessageId,
          assistantPartId,
        )
        const tStream2 = performance.now()
        addDebugLog(`Stream Complete: ${Math.round(tStream2 - tStream1)}ms`, "info")
      } else if (providerId === "opencode-zen") {
        assistantText = await streamOpenCodeZenChat(
          instanceId,
          sessionId,
          providerId,
          effectiveModel.modelId,
          externalMessages,
          messageId,
          assistantMessageId,
          assistantPartId,
        )
      } else if (providerId === "zai") {
        assistantText = await streamZAIChat(
          instanceId,
          sessionId,
          providerId,
          effectiveModel.modelId,
          externalMessages,
          messageId,
          assistantMessageId,
          assistantPartId,
        )
      } else if (useAntigravity) {
        assistantText = await streamAntigravityChat(
          instanceId,
          sessionId,
          routingProviderId,
          effectiveModel.modelId,
          externalMessages,
          messageId,
          assistantMessageId,
          assistantPartId,
        )
      } else {
        const qwenManager = new QwenOAuthManager()
        const token = await qwenManager.getValidToken()
        if (!token?.access_token) {
          showToastNotification({
            title: "Qwen OAuth unavailable",
            message: "Please sign in to Qwen Code again to refresh your token.",
            variant: "warning",
            duration: 8000,
          })
          store.upsertMessage({
            id: messageId,
            sessionId,
            role: "user",
            status: "error",
            updatedAt: Date.now(),
          })
          store.upsertMessage({
            id: assistantMessageId,
            sessionId,
            role: "assistant",
            status: "error",
            updatedAt: Date.now(),
            isEphemeral: false,
          })
          return messageId
        }

        assistantText = await streamQwenChat(
          instanceId,
          sessionId,
          providerId,
          effectiveModel.modelId,
          externalMessages,
          token.access_token,
          token.resource_url,
          messageId,
          assistantMessageId,
          assistantPartId,
        )
      }
      if (isNative) {
        const completedAt = Date.now()
        await persistNativeMessages(instanceId, sessionId, [
          {
            id: messageId,
            role: "user",
            content: resolvedPrompt,
            createdAt: now,
            updatedAt: now,
          },
          {
            id: assistantMessageId,
            role: "assistant",
            content: assistantText,
            createdAt: now,
            updatedAt: completedAt,
          },
        ])
      } else if (isSdk) {
        const completedAt = Date.now()
        await persistSdkMessages(instanceId, sessionId, [
          {
            id: messageId,
            role: "user",
            content: resolvedPrompt,
            createdAt: now,
            updatedAt: now,
          },
          {
            id: assistantMessageId,
            role: "assistant",
            content: assistantText,
            createdAt: now,
            updatedAt: completedAt,
          },
        ])
      }
      return messageId
    } catch (error: any) {
      if (providerId === "opencode-zen") {
        const message = String(error?.message || "")
        const match = message.match(/Model\s+([A-Za-z0-9._-]+)\s+not supported/i)
        if (match?.[1]) {
          markOpencodeZenModelOffline(match[1])
        }
      }
      store.upsertMessage({
        id: messageId,
        sessionId,
        role: "user",
        status: "error",
        updatedAt: Date.now(),
      })
      store.upsertMessage({
        id: assistantMessageId,
        sessionId,
        role: "assistant",
        status: "error",
        updatedAt: Date.now(),
        isEphemeral: false,
      })
      const rawErrorMessage = error?.message || "Request failed"
      const normalizedErrorMessage = /aborted|abort/i.test(rawErrorMessage)
        ? "Request timed out. The provider may be unavailable."
        : rawErrorMessage
      store.setMessageInfo(assistantMessageId, {
        id: assistantMessageId,
        role: "assistant",
        providerID: routingProviderId,
        modelID: effectiveModel.modelId,
        time: { created: now, completed: Date.now() },
        error: { name: "UnknownError", message: normalizedErrorMessage },
      } as any)
      const failedProvider = useAntigravity ? "antigravity" : providerId
      showToastNotification({
        title:
          failedProvider === "ollama-cloud"
            ? "Ollama request failed"
            : failedProvider === "zai"
              ? "Z.AI request failed"
              : failedProvider === "opencode-zen"
                ? "OpenCode Zen request failed"
                : failedProvider === "antigravity"
                  ? "Antigravity request failed"
                  : failedProvider === "qwen-oauth"
                    ? "Qwen request failed"
                    : "Request failed",
        message: normalizedErrorMessage,
        variant: "error",
        duration: 8000,
      })
      throw error
    }
  }

  const requestBody = {
    messageID: messageId,
    parts: requestParts,
    ...(session.agent && { agent: session.agent }),
    ...(effectiveModel.providerId &&
      effectiveModel.modelId && {
      model: {
        providerID: effectiveModel.providerId,
        modelID: effectiveModel.modelId,
      },
    }),
    ...(systemMessage && { system: systemMessage }),
  }

  log.info("sendMessage", {
    instanceId,
    sessionId,
    requestBody,
  })

  // Electron diagnostic logging
  if (typeof window !== "undefined" && (window as any).electron) {
    log.info("Electron environment detected", {
      isElectron: true,
      userAgent: navigator.userAgent,
      origin: window.location.origin
    })
  }

  try {
    log.info("session.promptAsync starting", { instanceId, sessionId, providerId: session.model.providerId })

    // Add timeout to prevent infinite hanging
    const timeoutMs = 60000 // 60 seconds
    const timeoutPromise = new Promise<never>((_, reject) => {
      setTimeout(() => reject(new Error(`Request timed out after ${timeoutMs / 1000}s. The model provider may not be available.`)), timeoutMs)
    })

    const promptPromise = instance.client.session.promptAsync({
      path: { id: sessionId },
      body: requestBody,
    })

    const response = await Promise.race([promptPromise, timeoutPromise])
    log.info("session.promptAsync success", { instanceId, sessionId, response })
    return messageId
  } catch (error: any) {
    log.error("Failed to send prompt", {
      instanceId,
      sessionId,
      error: error?.message || error,
      stack: error?.stack,
      requestBody
    })

    // Update message status to error in store
    const store = messageStoreBus.getOrCreate(instanceId)
    store.upsertMessage({
      id: messageId,
      sessionId,
      role: "user",
      status: "error",
      updatedAt: Date.now(),
    })

    // Show user-friendly error notification
    showToastNotification({
      title: "Message failed",
      message: error?.message || "Failed to send message. Check your model configuration.",
      variant: "error",
      duration: 8000,
    })

    throw error

  }
}

async function executeCustomCommand(
  instanceId: string,
  sessionId: string,
  commandName: string,
  args: string,
): Promise<void> {
  const instance = instances().get(instanceId)
  if (!instance || !instance.client) {
    throw new Error("Instance not ready")
  }

  const session = sessions().get(instanceId)?.get(sessionId)
  if (!session) {
    throw new Error("Session not found")
  }

  const body: {
    command: string
    arguments: string
    messageID: string
    agent?: string
    model?: string
  } = {
    command: commandName,
    arguments: args,
    messageID: createId("msg"),
  }

  if (session.agent) {
    body.agent = session.agent
  }

  if (session.model.providerId && session.model.modelId) {
    body.model = `${session.model.providerId}/${session.model.modelId}`
  }

  await instance.client.session.command({
    path: { id: sessionId },
    body,
  })
}

async function runShellCommand(instanceId: string, sessionId: string, command: string): Promise<void> {
  const instance = instances().get(instanceId)
  if (!instance || !instance.client) {
    throw new Error("Instance not ready")
  }

  const session = sessions().get(instanceId)?.get(sessionId)
  if (!session) {
    throw new Error("Session not found")
  }

  const agent = session.agent || "build"
  let resolvedCommand = command

  if (command.trim() === "build") {
    try {
      const response = await serverApi.fetchAvailablePort()
      if (response?.port) {
        const isWindows = typeof navigator !== "undefined" && /windows/i.test(navigator.userAgent)
        resolvedCommand = isWindows ? `set PORT=${response.port}&& ${command}` : `PORT=${response.port} ${command}`
        if (typeof window !== "undefined") {
          const url = `http://localhost:${response.port}`
          window.dispatchEvent(new CustomEvent(BUILD_PREVIEW_EVENT, { detail: { url, instanceId } }))
        }
      }
    } catch (error) {
      log.warn("Failed to resolve available port for build", { error })
    }
  }

  await instance.client.session.shell({
    path: { id: sessionId },
    body: {
      agent,
      command: resolvedCommand,
    },
  })
}

async function abortSession(instanceId: string, sessionId: string): Promise<void> {
  const instance = instances().get(instanceId)
  if (!instance || !instance.client) {
    throw new Error("Instance not ready")
  }

  log.info("abortSession", { instanceId, sessionId })

  try {
    log.info("session.abort", { instanceId, sessionId })
    await instance.client.session.abort({
      path: { id: sessionId },
    })
    log.info("abortSession complete", { instanceId, sessionId })
  } catch (error) {
    log.error("Failed to abort session", error)
    throw error
  }
}

async function compactSession(instanceId: string, sessionId: string): Promise<CompactionResult> {
  const instance = instances().get(instanceId)
  if (!instance || !instance.client) {
    return {
      success: false,
      mode: "compact",
      human_summary: "Instance not ready",
      token_before: 0,
      token_after: 0,
      token_reduction_pct: 0,
    }
  }

  const instanceSessions = sessions().get(instanceId)
  const session = instanceSessions?.get(sessionId)
  if (!session) {
    return {
      success: false,
      mode: "compact",
      human_summary: "Session not found",
      token_before: 0,
      token_after: 0,
      token_reduction_pct: 0,
    }
  }

  const store = messageStoreBus.getInstance(instanceId)
  const messageCount = store?.getSessionMessageIds(sessionId)?.length ?? 0
  if (messageCount <= 2) {
    log.info("compactSession: Session too small to compact", { instanceId, sessionId, count: messageCount })
    return {
      success: true,
      mode: "compact",
      human_summary: "Session too small to compact",
      token_before: 0,
      token_after: 0,
      token_reduction_pct: 0,
    }
  }

  const usageBefore = store?.getSessionUsage(sessionId)
  const tokenBefore = usageBefore
    ? (usageBefore.totalInputTokens || 0) + (usageBefore.totalOutputTokens || 0) + (usageBefore.totalReasoningTokens || 0)
    : 0

  log.info("compactSession: Running local compaction", { instanceId, sessionId, messageCount })
  setSessionCompactionState(instanceId, sessionId, true)
  compactionAttemptState.set(getWarningKey(instanceId, sessionId), {
    timestamp: Date.now(),
    suppressContextError: true,
  })

  try {
    const result = await executeCompactionWrapper(instanceId, sessionId, "compact")
    if (!result.success) {
      return result
    }

    const compactedSession = await createSession(instanceId, session.agent || undefined)

    if (session.model.providerId && session.model.modelId) {
      await updateSessionModel(instanceId, compactedSession.id, {
        providerId: session.model.providerId,
        modelId: session.model.modelId,
      })
    }

    if (session.tasks && session.tasks.length > 0) {
      const tasksCopy = session.tasks.map((task) => ({ ...task }))
      withSession(instanceId, compactedSession.id, (nextSession) => {
        nextSession.tasks = tasksCopy
        nextSession.activeTaskId = session.activeTaskId
      })
    }

    if (session.parentId) {
      withSession(instanceId, compactedSession.id, (nextSession) => {
        nextSession.parentId = session.parentId
      })
      withSession(instanceId, session.parentId, (parentSession) => {
        if (!parentSession.tasks) return
        parentSession.tasks = parentSession.tasks.map((task) => (
          task.taskSessionId === sessionId
            ? { ...task, taskSessionId: compactedSession.id }
            : task
        ))
      })
    }

    const summaryText = buildCompactionSeed(result)
    const summaryMessageId = createId("msg")
    const summaryPartId = createId("part")

    await instance.client.session.promptAsync({
      path: { id: compactedSession.id },
      body: {
        messageID: summaryMessageId,
        agent: session.agent || undefined,
        model: session.model.providerId && session.model.modelId
          ? { providerID: session.model.providerId, modelID: session.model.modelId }
          : undefined,
        noReply: true,
        system: "You are continuing from a compacted session. Use the summary below as context.",
        parts: [
          {
            id: summaryPartId,
            type: "text",
            text: summaryText,
          },
        ],
      },
    })

    if (session.parentId) {
      setActiveSession(instanceId, compactedSession.id)
    } else {
      setActiveParentSession(instanceId, compactedSession.id)
    }
    await loadMessages(instanceId, compactedSession.id, true)
    updateSessionInfo(instanceId, compactedSession.id)

    showToastNotification({
      title: "Session compacted",
      message: "Created a new compacted session with a summary to continue work.",
      variant: "success",
      duration: 8000,
    })

    log.info("compactSession: Complete", { instanceId, sessionId, compactedSessionId: compactedSession.id })
    clearCompactionSuggestion(instanceId, sessionId)
    return {
      ...result,
      token_before: tokenBefore,
    }
  } catch (error) {
    log.error("compactSession: Failed to compact session", { instanceId, sessionId, error })
    return {
      success: false,
      mode: "compact",
      human_summary: "Compaction failed",
      token_before: tokenBefore,
      token_after: tokenBefore,
      token_reduction_pct: 0,
    }
  } finally {
    compactionAttemptState.delete(getWarningKey(instanceId, sessionId))
    setSessionCompactionState(instanceId, sessionId, false)
  }
}

async function updateSessionAgent(instanceId: string, sessionId: string, agent: string): Promise<void> {
  const instanceSessions = sessions().get(instanceId)
  const session = instanceSessions?.get(sessionId)
  if (!session) {
    throw new Error("Session not found")
  }

  const agentModelPreference = await getAgentModelPreference(instanceId, agent)
  const defaultModel = await getDefaultModel(instanceId, agent)
  const nextModel = agentModelPreference || defaultModel
  const shouldApplyModel = isModelValid(instanceId, nextModel)

  withSession(instanceId, sessionId, (current) => {
    current.agent = agent
    if (shouldApplyModel) {
      current.model = nextModel
    }
  })


  if (agent && shouldApplyModel && !agentModelPreference) {
    await setAgentModelPreference(instanceId, agent, nextModel)
  }

  const instance = instances().get(instanceId)
  const isNative = instance?.binaryPath === "__nomadarch_native__"
  if (isNative) {
    await nativeSessionApi.updateSession(instanceId, sessionId, { agent })

  }

  if (shouldApplyModel) {
    updateSessionInfo(instanceId, sessionId)
  }
}

async function updateSessionModel(
  instanceId: string,
  sessionId: string,
  model: { providerId: string; modelId: string },
): Promise<void> {
  const instanceSessions = sessions().get(instanceId)
  const session = instanceSessions?.get(sessionId)
  if (!session) {
    throw new Error("Session not found")
  }

  if (!isModelValid(instanceId, model)) {
    log.warn("Invalid model selection", model)
    return
  }

  withSession(instanceId, sessionId, (current) => {
    current.model = model
  })

  const instance = instances().get(instanceId)
  if (instance?.binaryPath === "__nomadarch_native__") {
    await nativeSessionApi.updateSession(instanceId, sessionId, {
      model: {
        providerId: model.providerId,
        modelId: model.modelId
      }
    })
  }

  const propagateModel = (targetSessionId?: string | null) => {
    if (!targetSessionId || targetSessionId === sessionId) return
    withSession(instanceId, targetSessionId, (current) => {
      current.model = model
    })
    updateSessionInfo(instanceId, targetSessionId)
  }

  if (session.parentId) {
    propagateModel(session.parentId)
  }

  if (session.tasks && session.tasks.length > 0) {
    const seen = new Set<string>()
    for (const task of session.tasks) {
      if (!task.taskSessionId || seen.has(task.taskSessionId)) continue
      seen.add(task.taskSessionId)
      propagateModel(task.taskSessionId)
    }
  }

  if (session.agent) {
    await setAgentModelPreference(instanceId, session.agent, model)
  }
  addRecentModelPreference(model)

  updateSessionInfo(instanceId, sessionId)
}

async function updateSessionModelForSession(
  instanceId: string,
  sessionId: string,
  model: { providerId: string; modelId: string },
): Promise<void> {
  const instanceSessions = sessions().get(instanceId)
  const session = instanceSessions?.get(sessionId)
  if (!session) {
    throw new Error("Session not found")
  }

  if (!isModelValid(instanceId, model)) {
    log.warn("Invalid model selection", model)
    return
  }

  withSession(instanceId, sessionId, (current) => {
    current.model = model
  })

  const instance = instances().get(instanceId)
  if (instance?.binaryPath === "__nomadarch_native__") {
    await nativeSessionApi.updateSession(instanceId, sessionId, {
      model: {
        providerId: model.providerId,
        modelId: model.modelId
      }
    })
  }

  addRecentModelPreference(model)
  updateSessionInfo(instanceId, sessionId)
}

async function renameSession(instanceId: string, sessionId: string, nextTitle: string): Promise<void> {
  const instance = instances().get(instanceId)
  if (!instance) {
    throw new Error("Instance not ready")
  }

  const isNative = instance.binaryPath === "__nomadarch_native__"
  if (!isNative && !instance.client) {
    throw new Error("Instance client not ready")
  }

  const session = sessions().get(instanceId)?.get(sessionId)
  if (!session) {
    throw new Error("Session not found")
  }

  const trimmedTitle = nextTitle.trim()
  if (!trimmedTitle) {
    throw new Error("Session title is required")
  }

  if (isNative) {
    await nativeSessionApi.updateSession(instanceId, sessionId, { title: trimmedTitle })
  } else {
    await instance.client!.session.update({
      path: { id: sessionId },
      body: { title: trimmedTitle },
    })
  }

  withSession(instanceId, sessionId, (current) => {
    current.title = trimmedTitle
    const time = { ...(current.time ?? {}) }
    time.updated = Date.now()
    current.time = time
  })
}

async function revertSession(instanceId: string, sessionId: string): Promise<void> {
  const instance = instances().get(instanceId)
  if (!instance) {
    throw new Error("Instance not ready")
  }

  const isNative = instance.binaryPath === "__nomadarch_native__"
  if (!isNative && !instance.client) {
    throw new Error("Instance client not ready")
  }

  const session = sessions().get(instanceId)?.get(sessionId)
  if (!session) {
    throw new Error("Session not found")
  }

  try {
    if (isNative) {
      await nativeSessionApi.revertSession(instanceId, sessionId)
    } else {
      await instance.client!.session.revert({
        path: { id: sessionId },
      })
    }
  } catch (error) {
    log.error("Failed to revert session", error)
    throw error
  }
}

async function forkSession(instanceId: string, sessionId: string): Promise<string> {
  const instance = instances().get(instanceId)
  if (!instance) {
    throw new Error("Instance not ready")
  }

  const isNative = instance.binaryPath === "__nomadarch_native__"
  if (!isNative && !instance.client) {
    throw new Error("Instance client not ready")
  }

  const session = sessions().get(instanceId)?.get(sessionId)
  if (!session) {
    throw new Error("Session not found")
  }

  try {
    let forkedId: string = ""
    let forkedVersion: string = "0"
    let forkedTime: any = { created: Date.now(), updated: Date.now() }
    let forkedRevert: any = undefined

    if (isNative) {
      const response = await nativeSessionApi.forkSession(instanceId, sessionId)
      forkedId = response.id
      forkedTime = { created: response.createdAt, updated: response.updatedAt }
    } else {
      const response = await instance.client!.session.fork({
        path: { id: sessionId },
      })
      if (!response.data) {
        throw new Error("Failed to fork session: No data returned")
      }
      forkedId = response.data.id
      forkedVersion = response.data.version
      forkedTime = response.data.time
      forkedRevert = response.data.revert
        ? {
          messageID: response.data.revert.messageID,
          partID: response.data.revert.partID,
          snapshot: response.data.revert.snapshot,
          diff: response.data.revert.diff,
        }
        : undefined
    }

    if (!forkedId) {
      throw new Error("No session ID returned from fork operation")
    }

    const forkedSession: Session = {
      id: forkedId,
      instanceId,
      title: session.title ? `${session.title} (fork)` : "Forked Session",
      parentId: session.parentId || session.id,
      agent: session.agent,
      model: session.model,
      skills: [...(session.skills || [])],
      version: forkedVersion,
      time: forkedTime,
      revert: forkedRevert
    }

    setSessions((prev) => {
      const next = new Map(prev)
      const instanceSessions = new Map(next.get(instanceId) || [])
      instanceSessions.set(forkedSession.id, forkedSession)
      next.set(instanceId, instanceSessions)
      return next
    })

    return forkedId
  } catch (error) {
    log.error("Failed to fork session", error)
    throw error
  }
}

// Forcefully reset streaming state to unlock UI if stuck
function forceReset() {
  const store = messageStoreBus.getOrCreate(activeInstanceId() || "")
  if (!store) return

  // Reset streaming count forcefully
  // We don't have direct access to set count to 0, so we call end enough times
  // or we assume we can just ignore it for now, but really we should expose a reset method.
  // For now, let's just log and clear pending parts.
  store.setState("pendingParts", {})

  // If we could access the store's internal streaming count setter that would be better.
  // Since we added `isStreaming` and `endStreamingUpdate` to store interface,
  // we can just call end multiple times if we suspect it's stuck > 0
  let safety = 0
  while (store.state.streamingUpdateCount > 0 && safety < 100) {
    store.endStreamingUpdate()
    safety++
  }

  // Also reset message statuses
  try {
    const messages = store.state.messages;
    Object.values(messages).forEach(msg => {
      if (msg.status === "streaming" || msg.status === "sending") {
        store.upsertMessage({
          id: msg.id,
          sessionId: msg.sessionId,
          role: msg.role,
          status: "interrupted",
          updatedAt: Date.now(),
          isEphemeral: msg.isEphemeral,
        })
      }
    })
  } catch (e) {
    console.error("Error updating message status during reset", e)
  }

  addDebugLog("Force Reset Triggered: Cleared streaming state & statuses", "warn")
}

export {
  abortSession,
  compactSession,
  executeCustomCommand,
  forkSession,
  renameSession,
  revertSession,
  runShellCommand,
  sendMessage,
  updateSessionAgent,
  updateSessionModel,
  updateSessionModelForSession,
  forceReset, // Add to exports
}
