import type { Message } from '#cli/core/model'
import type { Style } from '@simon_he/vue-tui'
import type { SkillPaletteItem } from './utils/skills'
import {
  getCommitIdFromMention,
  renderCommitMarkdownLink,
} from '#cli/core/commit-mentions'
import { focusFileDisplayLabel } from '#cli/core/focus-files'
import {
  cacheTextToSessionBlob,
  deleteSessionBlobByPath,
  relocateSessionBlobPath,
} from '#cli/core/long-input-cache'
import { snapshotToUiMessages } from '#cli/core/session-ui-messages'
import {
  formatShortcutLabel,
  matchShortcut,
  normalizeShortcutBindings,
} from '#cli/core/ui-settings'
import { readRuntimeVersion } from '#cli/core/version-env'
import { t } from '#cli/i18n'
import {
  getBuiltinProvider,
  getModelDisplayName,
  getProviderDisplayName,
  isCustomProviderId,
} from '#cli/compat/providers'
import {
  createNodeMentionPathProvider,
  createPromptMentionPlugin,
  sliceByCellsRange,
  textCellWidth,
  TInput,
  TRenderLayer,
  TText,
  TView,
  useLayout,
  useRouter,
  useTerminal,
  wrapByCells,
} from '@simon_he/vue-tui'
import {
  computed,
  defineComponent,
  getCurrentInstance,
  h,
  inject,
  nextTick,
  onBeforeUnmount,
  ref,
  watch,
} from 'vue'
import { clearActiveSession } from '../cli/active-session'
import { exitImmediately } from '../cli/exit-controller'
import { isNextApiAuthEnabled } from '../core/feature-flags'
import { DimSdkBridgeKey } from '../keys'
import { DimSdkCommandPalette } from '../ui/components/CommandPalette'
import { DimSdkConfigDialog } from '../ui/components/ConfigDialog'
import { DimSdkCustomProviderModelsDialog } from '../ui/components/CustomProviderModelsDialog'
import { DimSdkCustomProviderModelsFetchDialog } from '../ui/components/CustomProviderModelsFetchDialog'
import { DimSdkCustomProviderNameDialog } from '../ui/components/CustomProviderNameDialog'
import { DimSdkLanguageDialog } from '../ui/components/LanguageDialog'
import { DimSdkMcpSettingsDialog } from '../ui/components/McpSettingsDialog'
import { DimSdkNextApiOAuthDialog } from '../ui/components/NextApiOAuthDialog'
import { DimSdkPathPickerDialog } from '../ui/components/PathPickerDialog'
import { DimSdkProviderConnectDialog } from '../ui/components/ProviderConnectDialog'
import { DimSdkSessionsDialog } from '../ui/components/SessionsDialog'
import { DimSdkSwitchSessionDialog } from '../ui/components/SwitchSessionDialog'
import { DimSdkThemeDialog } from '../ui/components/ThemeDialog'
import { DimSdkToolApprovalsDialog } from '../ui/components/ToolApprovalsDialog'
import { DimSdkToolSettingsDialog } from '../ui/components/ToolSettingsDialog'
import { useUiTheme } from '../ui/theme'
import { dialogSize } from '../ui/utils/dialog-sizes'
import {
  cacheDroppedFileToSession,
  HOME_SESSION_ATTACHMENT_SESSION_ID,
  isHomeAttachmentPath,
  pasteClipboardImageToTempFile,
  relocateHomeAttachmentsToSession,
} from './utils/clipboard-image'
import { createDimSdkBaseCommands } from './utils/command-palette'
import { parseDimSdkSlashCommand } from './utils/commands'
import { renderCopyToastOverlay } from './utils/copy-toast-overlay'
import { formatCwdDisplay, readGitBranch } from './utils/cwd-display'
import { createGitCommitMentionProvider } from './utils/git-commit-mentions'
import { buildHelpOptions } from './utils/help'
import { provideToastInputHostPlugin } from './utils/input-host-plugin'
import { renderBigWord, renderSmallWord } from './utils/logo'
import { clamp } from './utils/math'
import { useNextApiOAuthDialog } from './utils/next-api-oauth-dialog'
import { openPathExternally } from './utils/open-external'
import { usePathPicker } from './utils/path-picker'
import { useProviderDialogs } from './utils/provider-dialogs'
import { isNodeLike } from './utils/runtime'
import { useSessionsDialog } from './utils/sessions'
import {
  useLanguageDialog,
  useMcpSettingsDialogs,
  usePluginsDialogs,
  useSettingsDialogs,
  useThemeDialog,
  useToolSettingsDialogs,
} from './utils/settings-dialogs'
import {
  buildSkillPaletteOptions,
  createSkillPaletteItems,
  resolveSkillNameFromPaletteLabel,
} from './utils/skills'
import { consumeStartupToast } from './utils/startup-toast'
import { fitModelProviderLabel } from './utils/text-fit'
import { useTimedToast } from './utils/toast'
import { renderUpgradeToastOverlay } from './utils/upgrade-toast'

const MULTILINE_TOKEN = '\uFFFC'
const MENTION_TOKEN = '\uFFF9'

async function openFileExternally(absPath: string): Promise<void> {
  await openPathExternally(absPath)
}

function multilineLabel(text: string): string {
  const lineCount = (text.match(/\n/g) || []).length + 1
  return `[... ${lineCount} lines]`
}

function mentionLabel(absPath: string, index: number): string {
  const p = String(absPath || '')
  if (!p)
    return '[file]'
  return `[${focusFileDisplayLabel(p, index)}]`
}

function expandMultilineTokens(
  value: string,
  multilineTexts: readonly string[],
  mentions: readonly string[] = [],
): string {
  if (!value)
    return ''
  const out: string[] = []
  let tokenIndex = 0
  let mentionIndex = 0
  for (let i = 0; i < value.length;) {
    const ch = value[i]!
    if (ch === MULTILINE_TOKEN) {
      out.push(multilineTexts[tokenIndex] ?? '')
      tokenIndex++
      i += 1
      continue
    }
    if (ch === MENTION_TOKEN) {
      const absPath = String(mentions[mentionIndex] ?? '').trim()
      const label = focusFileDisplayLabel(absPath, mentionIndex)
      mentionIndex++
      if (absPath) {
        const commitId = getCommitIdFromMention(absPath)
        if (commitId) {
          out.push(renderCommitMarkdownLink(commitId))
        }
        else {
          const escapedName = label.replace(/\[/g, '\\[').replace(/\]/g, '\\]')
          const escapedPath = absPath.replace(/\)/g, '\\)')
          out.push(`[${escapedName}](${escapedPath})`)
        }
      }
      i += 1
      continue
    }
    const cp = value.codePointAt(i) ?? 0
    const seg = String.fromCodePoint(cp)
    out.push(seg)
    i += seg.length
  }
  return out.join('')
}

function expandMultilineTokensForDisplay(
  value: string,
  multilineTexts: readonly string[],
  mentions: readonly string[],
): string {
  if (!value)
    return ''
  const out: string[] = []
  let tokenIndex = 0
  let mentionIndex = 0
  for (let i = 0; i < value.length;) {
    const ch = value[i]!
    if (ch === MULTILINE_TOKEN) {
      const text = multilineTexts[tokenIndex] ?? ''
      out.push(multilineLabel(text))
      tokenIndex++
      i += 1
      continue
    }
    if (ch === MENTION_TOKEN) {
      out.push(mentionLabel(mentions[mentionIndex] ?? '', mentionIndex))
      mentionIndex++
      i += 1
      continue
    }
    const cp = value.codePointAt(i) ?? 0
    const seg = String.fromCodePoint(cp)
    out.push(seg)
    i += seg.length
  }
  return out.join('')
}

function normalizeUserHistoryText(value: unknown): string {
  return String(value ?? '')
    .replace(/\r\n?/g, '\n')
    .trim()
}

function buildUserHistoryFromMessages(messages: readonly Message[]): string[] {
  const out: string[] = []
  for (const m of messages) {
    if (!m || m.role !== 'user')
      continue
    const normalized = normalizeUserHistoryText((m as any).content)
    if (normalized)
      out.push(normalized)
  }
  return out
}

export const DimSdkHomePage = defineComponent({
  name: 'DimSdkHomePage',
  setup() {
    const instance = getCurrentInstance()
    const ui = useUiTheme()
    const router = useRouter()
    const layout = useLayout()
    const { events, scheduler, render } = useTerminal()
    const cols = computed(() => layout.clipRect?.w ?? 0)
    const rows = computed(() => layout.clipRect?.h ?? 0)
    const bgFill = computed(() => {
      const w = Math.max(0, cols.value)
      const h = Math.max(0, rows.value)
      if (w === 0 || h === 0)
        return ''
      const line = ' '.repeat(w)
      return Array.from({ length: h }, () => line).join('\n')
    })
    const footerFill = computed(() => {
      const w = Math.max(0, cols.value)
      if (w === 0)
        return ''
      return ' '.repeat(w)
    })

    const bridge = inject(DimSdkBridgeKey, null)
    if (!bridge) {
      throw new Error(
        'DimCodeBridge is missing (did you forget to wrap with <DimCodeProvider />?)',
      )
    }
    const store = bridge.store
    const uiForcedSkills = bridge.uiForcedSkills
    const shortcuts = computed(() =>
      normalizeShortcutBindings((store.state as any)?.ui?.shortcuts),
    )
    const commandsShortcutLabel = computed(() =>
      formatShortcutLabel(shortcuts.value.commands),
    )
    const sessionsApi = bridge.sessions
    const nodeLike = isNodeLike()

    const input = ref('')
    const latestSessionHistory = ref<string[]>([])
    const latestSessionId = ref('')
    const latestHistoryLoaded = ref(false)
    const inputFocused = ref(false)
    const mentionProviders = nodeLike ? [createGitCommitMentionProvider()] : []
    const inputPlugins = [
      createPromptMentionPlugin({
        mentionSuggestionProviders: mentionProviders,
        mentionPathProvider: nodeLike
          ? createNodeMentionPathProvider()
          : undefined,
      }),
    ]
    const showCommands = ref(false)
    const showHelp = ref(false)
    const showSkills = ref(false)
    const commandIndex = ref(0)
    const helpIndex = ref(0)
    const skillsIndex = ref(0)
    const focusFiles = ref<string[]>([])
    const multilineTexts = ref<string[]>([])
    const multilineBlobPaths = ref<string[]>([])
    let multilineSyncSeq = 0
    const uiMode = ref<'plan' | 'agent'>(store.getMode?.() ?? 'agent')

    function clearHomeActiveSession(source: string): void {
      clearActiveSession({ source })
    }

    clearHomeActiveSession('home_mount')

    const history = computed(() => {
      const current = store.state.userHistory
      return current.length ? current : latestSessionHistory.value
    })
    const historySourceId = computed(() => {
      const current = store.state.userHistory
      if (current.length)
        return String(store.state.sessionId ?? '')
      return latestSessionId.value
    })
    const historyIndex = ref(history.value.length)

    watch(
      uiMode,
      (mode) => {
        store.setMode?.(mode)
        if (instance?.isMounted)
          ;(instance as any)?.update?.()
        render.invalidatePlane('default')
        scheduler.invalidate({ priority: 'high' })
      },
      { immediate: true },
    )

    watch(
      () => store.state.ui.mode,
      (mode) => {
        const nextMode = mode === 'plan' ? 'plan' : 'agent'
        if (uiMode.value !== nextMode)
          uiMode.value = nextMode
      },
      { immediate: true },
    )

    watch(
      () => store.state.ui.theme.mode,
      () => {
        if (instance?.isMounted)
          ;(instance as any)?.update?.()
        render.invalidatePlane('default')
        render.invalidatePlane('chrome')
        render.invalidatePlane('overlay')
        scheduler.invalidate({ priority: 'high' })
      },
    )

    watch(
      () => inputFocused.value,
      (focused) => {
        if (focused)
          clearHomeActiveSession('home_input_focus')
      },
      { immediate: true },
    )

    watch(
      () => [historySourceId.value, history.value.length] as const,
      ([sid, len], prev) => {
        const prevSid = prev?.[0]
        const prevLen = prev?.[1] ?? 0
        if (sid !== prevSid) {
          historyIndex.value = len
          return
        }
        if (len !== prevLen && historyIndex.value >= prevLen)
          historyIndex.value = len
        else if (historyIndex.value > len)
          historyIndex.value = len
      },
      { immediate: true },
    )

    async function refreshLatestSessionHistory(opts?: {
      force?: boolean
    }): Promise<void> {
      if (latestHistoryLoaded.value && !opts?.force)
        return
      if (store.state.userHistory.length) {
        latestHistoryLoaded.value = true
        return
      }
      const api = sessionsApi
      if (!api?.list || typeof api.get !== 'function') {
        latestHistoryLoaded.value = true
        return
      }
      latestHistoryLoaded.value = true
      try {
        const list = await api.list()
        const sorted = list
          .slice()
          .sort((a, b) => Number(b?.updatedAt ?? 0) - Number(a?.updatedAt ?? 0))
        const picked
          = sorted.find(s => Number(s?.messageCount ?? 0) > 0) ?? sorted[0]
        const pickedId = String(picked?.id ?? '').trim()
        if (!pickedId)
          return
        if (
          pickedId === latestSessionId.value
          && latestSessionHistory.value.length
          && !opts?.force
        ) {
          return
        }
        const snapshot = await api.get(pickedId)
        if (!snapshot) {
          latestSessionId.value = pickedId
          latestSessionHistory.value = []
          return
        }
        const messages = snapshotToUiMessages(snapshot)
        latestSessionId.value = pickedId
        latestSessionHistory.value = buildUserHistoryFromMessages(messages)
      }
      catch {
        // ignore failures; history remains empty
      }
    }

    void refreshLatestSessionHistory()

    function cycleUiMode(dir: 1 | -1 = 1): void {
      const order = ['plan', 'agent'] as const
      const cur = uiMode.value
      const idx = order.indexOf(cur)
      const nextIdx = (idx + dir + order.length) % order.length
      const newMode = order[nextIdx] ?? 'agent'
      uiMode.value = newMode
    }

    async function setMultilineTexts(
      nextTexts: readonly string[],
      opts?: Readonly<{ deleteRemoved?: boolean }>,
    ): Promise<void> {
      const seq = ++multilineSyncSeq
      const prevTexts = [...multilineTexts.value]
      const prevPaths = [...multilineBlobPaths.value]
      const incoming = [...nextTexts]
      multilineTexts.value = incoming

      const usedPrev = new Set<number>()
      const nextPaths = Array.from({ length: incoming.length }, () => '')

      for (let i = 0; i < incoming.length; i++) {
        const currentText = String(incoming[i] ?? '')
        for (let j = 0; j < prevTexts.length; j++) {
          if (usedPrev.has(j))
            continue
          if (currentText !== String(prevTexts[j] ?? ''))
            continue
          usedPrev.add(j)
          nextPaths[i] = String(prevPaths[j] ?? '')
          break
        }
      }

      const removedPaths: string[] = []
      for (let j = 0; j < prevPaths.length; j++) {
        if (usedPrev.has(j))
          continue
        const p = String(prevPaths[j] ?? '').trim()
        if (p)
          removedPaths.push(p)
      }

      const deleteRemoved = opts?.deleteRemoved !== false
      if (deleteRemoved && removedPaths.length > 0) {
        await Promise.all(
          removedPaths.map(async p =>
            deleteSessionBlobByPath(p).catch(() => false),
          ),
        )
      }

      if (seq !== multilineSyncSeq)
        return
      multilineBlobPaths.value = nextPaths
    }

    async function handleMultilineClick(index: number): Promise<void> {
      const text = String(multilineTexts.value[index] ?? '')
      if (!text)
        return
      let filePath = String(multilineBlobPaths.value[index] ?? '').trim()
      const sessionId = String(store.state.sessionId ?? '').trim()
      if (filePath && sessionId) {
        const moved = await relocateSessionBlobPath(filePath, sessionId)
        if (moved)
          filePath = moved
      }
      if (!filePath) {
        const sid = sessionId || HOME_SESSION_ATTACHMENT_SESSION_ID
        const cached = await cacheTextToSessionBlob({
          content: text,
          sessionId: sid,
          source: 'multiline_paste',
        })
        filePath = String(cached?.filePath ?? '').trim()
        if (filePath) {
          const next = [...multilineBlobPaths.value]
          next[index] = filePath
          multilineBlobPaths.value = next
        }
      }
      if (filePath)
        await openFileExternally(filePath)
    }

    const toast = useTimedToast(1200)
    const copyToastVisible = toast.visible
    const copyToastText = toast.text
    const showCopyToast = (text: string, durationMs?: number | null) => {
      toast.show(text, durationMs)
      render.invalidatePlane('default')
      scheduler.invalidate({ priority: 'high' })
      scheduler.flushNow()
    }

    if (nodeLike)
      provideToastInputHostPlugin(showCopyToast)

    const onCustomAppliedRef = { current: () => {} }
    const settingsDialogs = useSettingsDialogs({
      store,
      nodeLike,
      showToast: showCopyToast,
      guardPendingApprovals: false,
      onApplied: () => onCustomAppliedRef.current(),
    })
    const {
      showConfig,
      showToolApprovals,
      contextWindow,
      contextWindowLocked,
      maxTokens,
      temperature,
      interleavedThinking,
      interleavedThinkingLocked,
      shortcutCommands,
      shortcutTimeline,
      shortcutNewSession,
      openConfig,
      closeConfig,
      applyConfig,
      openToolApprovals,
      applyToolApprovals,
    } = settingsDialogs

    const languageDialog = useLanguageDialog({
      store,
      nodeLike,
      showToast: showCopyToast,
      guardPendingApprovals: false,
      onApplied: () => onCustomAppliedRef.current(),
    })
    const { showLanguage, locale, openLanguage, closeLanguage, applyLanguage }
      = languageDialog

    const themeDialogComposable = useThemeDialog({
      store,
      showToast: showCopyToast,
      guardPendingApprovals: false,
      onApplied: () => onCustomAppliedRef.current(),
    })
    const { showTheme, themeMode, openTheme, closeTheme, applyTheme }
      = themeDialogComposable

    const toolSettingsDialogs = useToolSettingsDialogs({
      store,
      showToast: showCopyToast,
      guardPendingApprovals: false,
      onApplied: () => onCustomAppliedRef.current(),
    })
    const {
      showToolSettings,
      apiKeyDraft: toolApiKeyDraft,
      apiKeyMasked: toolApiKeyMasked,
      closeToolSettings,
      applyToolSettings,
    } = toolSettingsDialogs

    const mcpSettingsDialogs = useMcpSettingsDialogs({
      store,
      showToast: showCopyToast,
      guardPendingApprovals: false,
      onApplied: () => onCustomAppliedRef.current(),
    })
    const {
      showMcpSettings,
      mcpJsonFilePath,
      mcpServerCount,
      testStatus: mcpTestStatus,
      openMcpSettings,
      closeMcpSettings,
      openMcpFile,
      reloadMcpConfig,
      testConnection: testMcpConnection,
    } = mcpSettingsDialogs

    const pluginsDialogs = usePluginsDialogs({
      store,
      showToast: showCopyToast,
      guardPendingApprovals: false,
      onApplied: () => onCustomAppliedRef.current(),
    })
    const {
      showPlugins,
      showPluginConfig,
      pluginsTitle,
      pluginsOptions,
      pluginsIndex,
      onPluginSelect,
      pluginApiKeyDraft,
      pluginApiKeyMasked,
      openPlugins,
      closePlugins,
      closeSerperConfig,
      applySerperConfig,
    } = pluginsDialogs

    const openNextApiOAuthFromConnect: { current: () => void } = {
      current: () => {},
    }
    const providerDialogs = useProviderDialogs({
      store,
      nodeLike,
      showToast: showCopyToast,
      openCustomSettings: openConfig,
      openNextApiOAuth: isNextApiAuthEnabled()
        ? () => openNextApiOAuthFromConnect.current()
        : undefined,
    })
    onCustomAppliedRef.current = providerDialogs.handleCustomApplied
    const {
      showConnect,
      showModels,
      showConnectDialog,
      providerOptions,
      providerIndex,
      modelOptions,
      modelIndex: providerModelIndex,
      showCustomManage,
      customManageTitle,
      customManageOptions,
      customManageIndex,
      onSelectCustomManage,
      closeCustomManage,
      showCustomDeleteConfirm,
      customDeleteConfirmTitle,
      customDeleteConfirmOptions,
      customDeleteConfirmIndex,
      onSelectCustomDeleteConfirm,
      closeCustomDeleteConfirm,
      connectProviderName,
      connectBaseUrl,
      updateConnectBaseUrlDraft,
      connectApiKeyDraft,
      connectApiKeyMasked,
      connectCanClearKey,
      connectApplying,
      connectFocusKey,
      connectBaseUrlMode,
      connectBaseUrlAfterApiKey,
      showCustomType,
      customTypeOptions,
      customTypeIndex,
      onSelectCustomType,
      closeCustomType,
      showCustomConnectDialog,
      customConnectBaseUrl,
      updateCustomConnectBaseUrlDraft,
      customConnectApiKeyDraft,
      customConnectApiKeyMasked,
      customConnectFocusKey,
      applyCustomConnect,
      cancelCustomConnectDialog,
      showCustomModelsDialog,
      customModelsDraft,
      customModelsFetching,
      openCustomModelsFetch,
      applyCustomModels,
      cancelCustomModelsDialog,
      showCustomModelsFetchDialog,
      customModelsFetchOptions,
      customModelsFetchSelected,
      applyCustomModelsFetch,
      cancelCustomModelsFetchDialog,
      showCustomNameDialog,
      customNameDraft,
      applyCustomName,
      cancelCustomNameDialog,
      openConnect,
      closeConnect,
      openModels,
      closeModels,
      ensureActiveProviderReadyForChat,
      applyWorkspaceSelectionForCwd,
      markFirstUserMessageSent,
      onSelectProvider,
      onSelectModel,
      applyConnect,
      clearConnectKey,
      cancelConnectDialog,
    } = providerDialogs
    applyWorkspaceSelectionForCwd(String(store.state.context?.cwd ?? '.'))

    const nextApiOAuthDialog = useNextApiOAuthDialog({
      store,
      nodeLike,
      showToast: showCopyToast,
    })
    const {
      showNextApiOAuth,
      nextApiOAuthStatus,
      nextApiOAuthEmail,
      nextApiOAuthUrl,
      nextApiOAuthError,
      selectedNextApiModel,
      nextApiModels,
      nextApiModelIndex,
      openNextApiOAuth,
      closeNextApiOAuth,
      loginWithNextApiOAuth,
      logoutNextApiOAuth,
      selectNextApiModel,
      viewNextApiPlans,
    } = nextApiOAuthDialog
    openNextApiOAuthFromConnect.current = openNextApiOAuth

    const sessions = useSessionsDialog({
      store,
      sessionsApi,
      showToast: showCopyToast,
    })
    const {
      showSessions,
      showSwitchConfirm,
      switchConfirmTitle,
      switchConfirmTargetLabel,
      switchConfirmReason,
      confirmSwitch,
      cancelSwitch,
      sessionsLoading,
      sessionsDeleteArmed,
      sessionsCwdOnly,
      sessionsAllCount,
      sessionsList,
      sessionsIndex,
      refreshSessions,
      openSessionsDialog,
      openSelectedSession,
      createNewSession,
    } = sessions

    watch(
      () => [
        showSessions.value,
        sessionsLoading.value,
        sessionsDeleteArmed.value,
        sessionsCwdOnly.value,
        sessionsAllCount.value,
        sessionsIndex.value,
        sessionsList.value
          .map(session => `${session.id}\u0000${session.updatedAt}\u0000${session.title}`)
          .join('\u0001'),
      ],
      () => {
        if (instance?.isMounted) {
          ;(instance as any)?.update?.()
        }
        render.invalidatePlane('default')
        scheduler.invalidate({ priority: 'high' })
        if (showSessions.value)
          scheduler.flushNow()
      },
    )

    const picker = usePathPicker({ store, cols, rows, nodeLike })
    const {
      showPathPicker,
      pathPickerMode,
      pathPickerQuery,
      pathPickerError,
      pickerWorkspace,
      pickerW,
      pickerH,
      pathPickerTitle,
      pathPickerPlaceholder,
      closePathPicker,
    } = picker

    // One-shot startup toast (e.g. upgrade completion).
    consumeStartupToast({
      store: store as any,
      nodeLike,
      showToast: showCopyToast,
    })

    // Prompt suggestions for / commands (like ChatPage)
    const isCustomProvider = computed(() => {
      const id = String((store.state as any)?.provider?.activeId ?? '').trim()
      return id === 'custom' || isCustomProviderId(id)
    })
    const openCustomSettingsCommand = (): void => {
      if (isCustomProvider.value) {
        openConfig()
        return
      }
      showCopyToast(t('homePage.toast.customProviderOnly'))
    }

    const skillsList = ref<readonly SkillPaletteItem[]>([])

    async function loadSkillsList(): Promise<void> {
      try {
        const cwd = String(store.state.context?.cwd ?? process.cwd())
        const { getSkillsCatalogRoots } = await import(
          '#cli/compat/runtime-host',
        )
        const { scanMultipleSkillsDirectories } = await import(
          '#cli/compat/runtime-host',
        )
        const roots = getSkillsCatalogRoots(cwd)
        const skills = await scanMultipleSkillsDirectories(roots)
        skillsList.value = createSkillPaletteItems(skills, uiForcedSkills)
      }
      catch {
        // Silently ignore skill loading errors
      }
    }

    // Eagerly load skills for $ trigger suggestions
    void loadSkillsList()

    const runPromptSuggestionAction = (action: () => void): void => {
      const manager: any = events.value
      if (manager && typeof manager.focus === 'function')
        manager.focus(null)
      render.invalidatePlane('default')
      scheduler.invalidate({ priority: 'high' })
      queueMicrotask(() => {
        queueMicrotask(() => {
          void nextTick().then(() => {
            action()
            render.invalidatePlane('default')
            scheduler.invalidate({ priority: 'high' })
          })
        })
      })
    }

    const promptSuggestions = computed(() => {
      const items = [
        {
          value: '/sessions',
          detail: t('homePage.prompts.sessions'),
          onSelect: () => {
            runPromptSuggestionAction(() => {
              void openSessionsDialog()
              input.value = ''
              void setMultilineTexts([])
            })
          },
        },
        {
          value: '/skills',
          detail: t('homePage.prompts.skills'),
          onSelect: () => {
            runPromptSuggestionAction(() => {
              void openSkills()
              input.value = ''
              void setMultilineTexts([])
            })
          },
        },
        {
          value: '/approvals',
          detail: t('homePage.prompts.approvals'),
          onSelect: () => {
            runPromptSuggestionAction(() => {
              input.value = '/approvals'
              void submit()
            })
          },
        },
        {
          value: '/connect',
          detail: t('homePage.prompts.connect'),
          onSelect: () => {
            runPromptSuggestionAction(() => {
              input.value = '/connect'
              void submit()
            })
          },
        },

        {
          value: '/models',
          detail: t('homePage.prompts.models'),
          onSelect: () => {
            runPromptSuggestionAction(() => {
              input.value = '/models'
              void submit()
            })
          },
        },
        {
          value: '/mcp',
          detail: t('homePage.prompts.mcpSettings'),
          onSelect: () => {
            runPromptSuggestionAction(() => {
              openMcpSettings()
              input.value = ''
              void setMultilineTexts([])
            })
          },
        },
        {
          value: '/plugins',
          detail: t('homePage.prompts.plugins'),
          onSelect: () => {
            runPromptSuggestionAction(() => {
              openPlugins()
              input.value = ''
              void setMultilineTexts([])
            })
          },
        },
        {
          value: '/language',
          detail: t('homePage.prompts.language'),
          onSelect: () => {
            runPromptSuggestionAction(() => {
              openLanguage()
              input.value = ''
              void setMultilineTexts([])
            })
          },
        },
        {
          value: '/theme',
          detail: t('homePage.prompts.theme'),
          onSelect: () => {
            runPromptSuggestionAction(() => {
              openTheme()
              input.value = ''
              void setMultilineTexts([])
            })
          },
        },
        {
          value: '/exit',
          detail: t('homePage.prompts.exit'),
          onSelect: () => {
            exitImmediately()
          },
        },
      ] as const
      const result: { value: string, detail: string, onSelect: () => void }[] = [...items]
      if (isCustomProvider.value) {
        const insertAt = Math.max(
          0,
          result.findIndex(item => item.value === '/approvals') + 1,
        )
        result.splice(insertAt, 0, {
          value: '/settings',
          detail: t('homePage.prompts.settings'),
          onSelect: () => {
            runPromptSuggestionAction(() => {
              openCustomSettingsCommand()
              input.value = ''
              void setMultilineTexts([])
            })
          },
        })
      }

      return result
    })

    // Skill suggestions for the $ position-independent trigger
    const skillSuggestions = computed(() =>
      skillsList.value.map(s => ({
        value: `$${s.name}`,
        detail: s.description,
      })),
    )

    function extractSkillsFromMessage(text: string): { cleaned: string, activatedSkills: string[] } {
      if (!uiForcedSkills)
        return { cleaned: text, activatedSkills: [] }
      const skillNameSet = new Set(skillsList.value.map(s => s.name))
      const activatedSkills: string[] = []
      const cleaned = text
        .replace(/\$(\S+)/gu, (match, name) => {
          if (skillNameSet.has(name)) {
            uiForcedSkills!.add(name)
            activatedSkills.push(name)
            return ''
          }
          return match
        })
        .trim()
      return { cleaned, activatedSkills }
    }

    watch(focusFiles, (next) => {
      store.setFocusFiles(next)
    })

    const RECENT_COMMANDS_LIMIT = 5
    const recordRecentCommand = (label: string) => {
      const text = String(label ?? '').trim()
      if (!text)
        return
      const uiState: any = (store.state as any).ui ?? {}
      const current = Array.isArray(uiState.recentCommands)
        ? uiState.recentCommands
        : []
      const next = [text, ...current.filter((v: string) => v !== text)].slice(
        0,
        RECENT_COMMANDS_LIMIT,
      )
      uiState.recentCommands = next
      ;(store.state as any).ui = uiState
    }

    const commands = computed(() => {
      return [
        ...createDimSdkBaseCommands({
          openSessionsDialog: () => void openSessionsDialog(),
          createNewSession: () => void createNewSession(),
          openHelp,
          openConfig,
          openLanguage,
          openToolApprovals,
          openMcpSettings,
          openPlugins,
          openConnect,
          openModels,
          includeSettings: true,
          includeNewSession: false,
          includeHelp: false,
        }),
      ] as Array<{ label: string, detail: string, run: () => void }>
    })

    const recentCommands = computed(() => {
      const uiState: any = (store.state as any).ui ?? {}
      const recent = Array.isArray(uiState.recentCommands)
        ? uiState.recentCommands
        : []
      const byLabel = new Map(commands.value.map(c => [c.label, c] as const))
      return recent
        .map((label: string) => byLabel.get(label))
        .filter(Boolean) as typeof commands.value
    })

    const commandPaletteCommands = computed(() => {
      const recent = recentCommands.value
      if (recent.length === 0)
        return commands.value
      const recentLabels = new Set(recent.map(c => c.label))
      const rest = commands.value.filter(c => !recentLabels.has(c.label))
      return [...recent, ...rest]
    })

    const commandOptions = computed(() =>
      commandPaletteCommands.value.map(c => ({
        label: c.label,
        detail: c.detail,
      })),
    )

    const windowStyle = computed<Style>(() => {
      return ui.style(['bg.window'])
    })

    const mutedOnWindow = computed<Style>(() => {
      return ui.style(['text.muted', 'bg.window'])
    })
    const panelFillStyle = computed<Style>(() => {
      return ui.style(['text.primary', 'bg.chrome'])
    })
    const inputStyle = computed<Style>(() => {
      return ui.style(['input.field'])
    })

    const bigLogo = computed(() => cols.value >= 70 && rows.value >= 26)
    const logoMode = computed<'big' | 'small' | 'text' | 'hidden'>(() => {
      if (bigLogo.value)
        return 'big'
      if (cols.value < 20 || rows.value < 10)
        return 'hidden'
      if (cols.value < 32 || rows.value < 16)
        return 'text'
      return 'small'
    })

    const logoHasAscii = computed(
      () => logoMode.value === 'big' || logoMode.value === 'small',
    )
    const logoLetterGap = computed(() => (logoMode.value === 'big' ? 1 : 0))
    const logoWordGap = computed(() => (logoMode.value === 'big' ? 2 : 1))
    const openLogoLines = computed(() => {
      if (logoMode.value === 'hidden')
        return []
      if (logoMode.value === 'big')
        return renderBigWord('dim', logoLetterGap.value)
      if (logoMode.value === 'small')
        return renderSmallWord('dim')
      return ['dim']
    })
    const logoH = computed(() => openLogoLines.value.length)
    const logoGapY = computed(() => (logoH.value > 0 ? 1 : 0))
    const openLogoW = computed(() =>
      textCellWidth(openLogoLines.value[0] ?? ''),
    )
    const logoW = computed(
      () => openLogoW.value + (logoH.value > 0 ? logoWordGap.value : 0),
    )
    const logoX = computed(() =>
      Math.max(0, Math.floor((cols.value - logoW.value) / 2)),
    )

    const panelW = computed(() => {
      const c = Math.max(0, Math.floor(cols.value))
      if (c <= 0)
        return 0
      const min = Math.min(44, c)
      const max = Math.max(min, c - 8)
      return clamp(Math.floor(c * 0.72), min, max)
    })
    const panelX = computed(() =>
      Math.max(0, Math.floor((cols.value - panelW.value) / 2)),
    )

    // Panel layout:
    // - 1 col for accent bar
    // - some left padding to match dim
    const contentPadX = 2
    const inputX = computed(() => panelX.value + 1 + contentPadX) // accent + padding
    const contentW = computed(() => Math.max(0, panelW.value - 1 - contentPadX))
    const inputInnerW = computed(() => Math.max(1, contentW.value))
    const inputLineCount = computed(() => {
      if (!input.value)
        return 1
      const w = inputInnerW.value
      if (w <= 0)
        return 1
      const displayValue = expandMultilineTokensForDisplay(
        input.value,
        multilineTexts.value,
        focusFiles.value,
      )
      return Math.max(1, wrapByCells(displayValue, w).length)
    })
    const inputH = computed(() => clamp(inputLineCount.value, 1, 7))

    const panelPadY = 1
    const panelGapY = 1
    const panelH = computed(
      () => panelPadY + inputH.value + panelGapY + 1 + panelPadY,
    )
    const panelFill = computed(() => {
      const w = Math.max(0, panelW.value)
      const h = Math.max(0, panelH.value)
      if (w === 0 || h === 0)
        return ''
      const line = ' '.repeat(w)
      return Array.from({ length: h }, () => line).join('\n')
    })

    // Reserve the last row for the footer so it never disappears when the input grows.
    // Keep some breathing room under the footer (like dim).
    const footerH = 2
    const topMinY = computed(() => (logoH.value > 0 ? 1 : 0))
    const topAnchor = computed(() => (logoH.value > 0 ? 0.32 : 0.18))
    const logoY = computed(() => {
      const breathingRoom = 3
      const maxY = Math.max(
        topMinY.value,
        rows.value
        - footerH
        - logoH.value
        - logoGapY.value
        - panelH.value
        - breathingRoom,
      )
      return clamp(
        Math.floor(rows.value * topAnchor.value),
        topMinY.value,
        maxY,
      )
    })
    const panelTopY = computed(() => logoY.value + logoH.value + logoGapY.value)
    const panelBottomY = computed(() => panelTopY.value + panelH.value - 1)
    const panelInnerY = computed(() => panelTopY.value + panelPadY)
    const modelRowY = computed(
      () => panelInnerY.value + inputH.value + panelGapY,
    )

    const anyDialogOpen = computed(
      () =>
        showCommands.value
        || showHelp.value
        || showPathPicker.value
        || showSessions.value
        || showConfig.value
        || showLanguage.value
        || showTheme.value
        || showConnect.value
        || showModels.value
        || showConnectDialog.value
        || showCustomManage.value
        || showCustomDeleteConfirm.value
        || showCustomType.value
        || showCustomConnectDialog.value
        || showCustomModelsDialog.value
        || showCustomModelsFetchDialog.value
        || showCustomNameDialog.value
        || showToolApprovals.value
        || showSkills.value
        || showToolSettings.value
        || showMcpSettings.value
        || showNextApiOAuth.value,
    )

    watch(
      () => [
        showCommands.value,
        showHelp.value,
        showPathPicker.value,
        showSessions.value,
        showConfig.value,
        showLanguage.value,
        showTheme.value,
        showConnect.value,
        showModels.value,
        showConnectDialog.value,
        showCustomManage.value,
        showCustomDeleteConfirm.value,
        showCustomType.value,
        showCustomConnectDialog.value,
        showCustomModelsDialog.value,
        showCustomModelsFetchDialog.value,
        showCustomNameDialog.value,
        showToolApprovals.value,
        showSkills.value,
        showToolSettings.value,
        showMcpSettings.value,
        showNextApiOAuth.value,
      ],
      () => {
        if (instance?.isMounted)
          ;(instance as any)?.update?.()
        render.invalidatePlane('default')
        scheduler.invalidate({ priority: 'high' })
      },
    )

    const logoFxEnabledByEnv = (() => {
      const env: any = (globalThis as any)?.process?.env ?? {}
      const raw = String(env.DIMCODE_LOGO_ANIM ?? '').trim()
      if (!raw)
        return true
      if (raw === '0' || /^(?:false|no|off)$/i.test(raw))
        return false
      if (raw === '1' || /^(?:true|yes|on)$/i.test(raw))
        return true
      return true
    })()

    const logoFxIsTest = computed(() => {
      const env: any = (globalThis as any)?.process?.env ?? {}
      return String(env.DIMCODE_TEST ?? '').trim() === '1'
    })

    const logoFxActive = computed(
      () =>
        logoHasAscii.value
        && logoFxEnabledByEnv
        && !logoFxIsTest.value
        // Freeze decorative background motion while overlays/dialogs are open to keep
        // foreground editors visually stable.
        && !anyDialogOpen.value,
    )
    const logoFxFrame = ref(0)
    let logoFxTimer: ReturnType<typeof setInterval> | null = null
    let promptFocusRetryTimer: ReturnType<typeof setTimeout> | null = null
    watch(
      logoFxActive,
      (active) => {
        if (active && !logoFxTimer) {
          logoFxTimer = setInterval(() => {
            logoFxFrame.value = (logoFxFrame.value + 1) % 1_000_000
            if (instance?.isMounted)
              ;(instance as any)?.update?.()
            render.invalidatePlane('default')
            scheduler.invalidate({ priority: 'high' })
          }, 66)
        }
        else if (!active && logoFxTimer) {
          clearInterval(logoFxTimer)
          logoFxTimer = null
          logoFxFrame.value = 0
          if (instance?.isMounted)
            ;(instance as any)?.update?.()
          render.invalidatePlane('default')
          scheduler.invalidate({ priority: 'high' })
        }
      },
      { immediate: true },
    )
    onBeforeUnmount(() => {
      if (logoFxTimer) {
        clearInterval(logoFxTimer)
        logoFxTimer = null
      }
      if (promptFocusRetryTimer) {
        clearTimeout(promptFocusRetryTimer)
        promptFocusRetryTimer = null
      }
    })

    const logoDrawFramesByEnv = (() => {
      const env: any = (globalThis as any)?.process?.env ?? {}
      const raw = String(env.DIMCODE_LOGO_DRAW_FRAMES ?? '').trim()
      if (!raw)
        return null
      const value = Number(raw)
      if (!Number.isFinite(value) || value <= 0)
        return null
      return clamp(Math.floor(value), 4, 120)
    })()
    const logoDrawFrames = computed(() => {
      if (logoDrawFramesByEnv != null)
        return logoDrawFramesByEnv
      return logoMode.value === 'big' ? 26 : 18
    })
    const logoDrawProgress = computed(() => {
      if (!logoFxActive.value)
        return 1
      const frames = Math.max(1, Math.floor(logoDrawFrames.value))
      const t = Math.min(1, (logoFxFrame.value + 1) / frames)
      return t
    })
    const logoDrawW = computed(() => {
      const fullW = openLogoW.value
      if (!logoFxActive.value)
        return fullW
      const frames = Math.max(1, Math.floor(logoDrawFrames.value))
      if (logoFxFrame.value + 1 >= frames)
        return fullW
      const w = Math.floor(fullW * logoDrawProgress.value)
      return Math.max(0, Math.min(fullW, w))
    })
    const logoRenderLines = computed(() => {
      if (logoDrawW.value >= openLogoW.value)
        return openLogoLines.value
      const w = Math.max(0, Math.floor(logoDrawW.value))
      if (w <= 0)
        return openLogoLines.value.map(() => '')
      return openLogoLines.value.map(line => sliceByCellsRange(line, 0, w))
    })

    const logoShadowStyle = computed<Style>(() =>
      ui.style(['border.default', 'bg.window'], { dim: true }),
    )
    const logoBaseStyle = computed<Style>(() =>
      ui.style(['text.muted', 'bg.window'], { bold: true }),
    )
    const logoFxChipToken = computed(() => {
      if (uiMode.value === 'plan')
        return 'chip.plan' as const
      return 'chip.agent' as const
    })
    const logoFxHue = computed<Style['fg'] | undefined>(() => {
      const chip = ui.token(logoFxChipToken.value)
      // Prefer the chip background as the FX hue when available.
      if (chip?.bg)
        return chip.bg as any
      if (chip?.fg)
        return chip.fg as any
      if (uiMode.value === 'plan') {
        return (ui.token('accent.purple')?.fg
          ?? ui.token('accent.cyan')?.fg) as any
      }
      return (ui.token('status.success')?.fg
        ?? ui.token('accent.cyan')?.fg) as any
    })
    const logoGlintCoreStyle = computed<Style>(() => {
      return ui.style(['bg.window'], { fg: logoFxHue.value, bold: true })
    })
    const logoGlintTailStyle = computed<Style>(() => {
      return ui.style(['bg.window'], { fg: logoFxHue.value, dim: true })
    })
    const logoSparkleStyle = computed<Style>(() => {
      return ui.style(['bg.window'], { fg: logoFxHue.value, dim: true })
    })

    const logoFxSlices = computed(() => {
      if (!logoFxActive.value)
        return { tail: [], core: [] } as const
      const w = Math.max(0, Math.floor(logoDrawW.value))
      if (w <= 0)
        return { tail: [], core: [] } as const

      const lines = logoRenderLines.value
      const coreW = Math.max(6, Math.min(w, logoMode.value === 'big' ? 14 : 10))
      const tailW = Math.max(4, Math.min(w, Math.floor(coreW * 0.55)))
      const runW = w + coreW + tailW + 12
      const base = (logoFxFrame.value % runW) - (coreW + tailW)

      const tail: Array<{ x: number, y: number, w: number, value: string }> = []
      const core: Array<{ x: number, y: number, w: number, value: string }> = []

      for (let i = 0; i < lines.length; i++) {
        const line = lines[i] ?? ''
        const coreStart = base + i * 2
        const coreEnd = coreStart + coreW
        const tailStart = coreStart - tailW
        const tailEnd = coreStart

        const coreVisStart = clamp(coreStart, 0, w)
        const coreVisEnd = clamp(coreEnd, 0, w)
        const coreValue
          = coreVisEnd > coreVisStart
            ? sliceByCellsRange(line, coreVisStart, coreVisEnd)
            : ''
        const coreWActual = textCellWidth(coreValue)
        if (coreWActual > 0) {
          core.push({
            x: logoX.value + coreVisStart,
            y: logoY.value + i,
            w: coreWActual,
            value: coreValue,
          })
        }

        const tailVisStart = clamp(tailStart, 0, w)
        const tailVisEnd = clamp(tailEnd, 0, w)
        const tailValue
          = tailVisEnd > tailVisStart
            ? sliceByCellsRange(line, tailVisStart, tailVisEnd)
            : ''
        const tailWActual = textCellWidth(tailValue)
        if (tailWActual > 0) {
          tail.push({
            x: logoX.value + tailVisStart,
            y: logoY.value + i,
            w: tailWActual,
            value: tailValue,
          })
        }
      }

      return { tail, core } as const
    })

    const logoSparkles = computed(() => {
      if (!logoFxActive.value)
        return []
      const w = Math.max(0, Math.floor(logoDrawW.value))
      const h = Math.max(0, Math.floor(logoH.value))
      if (w <= 0 || h <= 0)
        return []
      const lines = logoRenderLines.value
      const phase = Math.floor(logoFxFrame.value / 6)
      const count = logoMode.value === 'big' ? 10 : 6

      const out: Array<{ x: number, y: number, ch: string }> = []
      for (let i = 0; i < count; i++) {
        let s = (phase + 1) * 0x9E3779B1
        s ^= (i + 1) * 0x85EBCA6B
        s ^= (w << 16) ^ h
        // xorshift32
        s ^= s << 13
        s ^= s >>> 17
        s ^= s << 5
        const x = Math.abs(s) % w
        const y = Math.abs(s >>> 16) % h

        const line = lines[y] ?? ''
        const under = sliceByCellsRange(line, x, x + 1)
        if (under && under !== ' ')
          continue

        const ch = (phase + i) % 9 === 0 ? '•' : '·'
        out.push({ x: logoX.value + x, y: logoY.value + y, ch })
      }
      return out
    })
    const connectDialogH = computed(() => {
      const maxH = Math.max(12, rows.value - 4)
      const minH = Math.min(18, maxH)
      const preferred = Math.max(18, Math.floor(rows.value * 0.78))
      return clamp(preferred, minH, maxH)
    })

    function focusBrowserTextarea(): void {
      if (nodeLike)
        return
      try {
        const doc: any = (globalThis as any).document
        const el = doc?.querySelector?.(
          '[data-vt-host] textarea',
        ) as HTMLTextAreaElement | null
        el?.focus?.()
      }
      catch {
        // ignore
      }
    }

    function focusPromptInput(): boolean {
      const manager: any = events.value
      if (
        !manager
        || typeof manager.debugNodes !== 'function'
        || typeof manager.focus !== 'function'
      ) {
        return false
      }

      const wantX = Math.floor(inputX.value)
      const wantY = Math.floor(panelInnerY.value)
      const wantW = Math.floor(contentW.value)
      const wantH = Math.floor(inputH.value)

      const nodes = (manager.debugNodes?.() ?? []).filter(
        (n: any) => n?.visible && n?.focusable,
      )
      const exact = nodes.find((n: any) => {
        const r = n?.rect
        return (
          r && r.x === wantX && r.y === wantY && r.w === wantW && r.h === wantH
        )
      })
      if (exact?.id) {
        manager.focus(exact.id)
        render.invalidatePlane('default')
        scheduler.invalidate({ priority: 'high' })
        return true
      }

      const cx = wantX + Math.floor(Math.max(0, wantW - 1) / 2)
      const cy = wantY + Math.floor(Math.max(0, wantH - 1) / 2)
      const candidates = nodes.filter((n: any) => {
        const r = n?.rect
        return r && cx >= r.x && cy >= r.y && cx < r.x + r.w && cy < r.y + r.h
      })
      candidates.sort((a: any, b: any) => {
        const za = a?.zIndex ?? 0
        const zb = b?.zIndex ?? 0
        if (zb !== za)
          return zb - za
        const ra = a?.rect ?? { w: 0, h: 0 }
        const rb = b?.rect ?? { w: 0, h: 0 }
        return ra.w * ra.h - rb.w * rb.h
      })
      const picked = candidates[0]
      if (picked?.id) {
        manager.focus(picked.id)
        render.invalidatePlane('default')
        scheduler.invalidate({ priority: 'high' })
        return true
      }
      return false
    }

    watch(inputFocused, () => {
      render.invalidatePlane('default')
      scheduler.invalidate({ priority: 'high' })
    })

    watch(anyDialogOpen, (open) => {
      render.invalidatePlane('default')
      scheduler.invalidate({ priority: 'high' })
      if (open)
        return
      if (promptFocusRetryTimer) {
        clearTimeout(promptFocusRetryTimer)
        promptFocusRetryTimer = null
      }
      const attemptFocus = () => {
        if (anyDialogOpen.value)
          return
        focusBrowserTextarea()
        if (focusPromptInput())
          return
        promptFocusRetryTimer = setTimeout(() => {
          promptFocusRetryTimer = null
          if (anyDialogOpen.value)
            return
          focusBrowserTextarea()
          focusPromptInput()
        }, 0)
      }
      void nextTick().then(attemptFocus)
    })

    const modelInfo = computed(() => {
      const providerId = String(
        (store.state as any)?.provider?.activeId ?? '',
      ).trim()
      const modelId = String(store.state.config.model ?? '').trim() || 'model'
      let providerName = ''
      if (providerId) {
        if (providerId === 'custom') {
          providerName = t('homePage.providerCustom')
        }
        else if (isCustomProviderId(providerId)) {
          const customProviders = Array.isArray(
            (store.state as any)?.provider?.customProviders,
          )
            ? (store.state as any).provider.customProviders
            : []
          const match = customProviders.find(
            (p: any) => String(p?.id ?? '').trim() === providerId,
          )
          providerName
            = String(match?.name ?? '').trim() || t('homePage.providerCustom')
        }
        else {
          providerName = getProviderDisplayName(
            getBuiltinProvider(providerId) ?? { id: providerId },
          )
        }
      }

      let model = modelId
      if (providerId) {
        if (providerId === 'custom' || isCustomProviderId(providerId)) {
          const customModels
            = providerId === 'custom'
              ? (((store.state as any)?.provider?.custom?.models
                ?? []) as Array<{
                  id: string
                  name?: string
                  display_name?: string
                }>)
              : (() => {
                  const list = Array.isArray(
                    (store.state as any)?.provider?.customProviders,
                  )
                    ? (store.state as any).provider.customProviders
                    : []
                  const match = list.find(
                    (p: any) => String(p?.id ?? '').trim() === providerId,
                  )
                  return (match?.models ?? []) as Array<{
                    id: string
                    name?: string
                    display_name?: string
                  }>
                })()
          const match = customModels.find(
            m => String(m?.id ?? '').trim() === modelId,
          )
          if (match)
            model = getModelDisplayName(match)
        }
        else {
          const builtin = getBuiltinProvider(providerId)
          const customModels
            = builtin?.customModels
              && (store.state as any)?.provider?.connections?.[providerId]?.models
          const models = Array.isArray(customModels)
            ? customModels
            : builtin?.models
          const match = models?.find(
            m => String(m?.id ?? '').trim() === modelId,
          )
          if (match)
            model = getModelDisplayName(match)
        }
      }

      return { build: '■', model, providerName }
    })
    const modeLabel = computed(() => uiMode.value)

    const helpOptions = computed(() =>
      buildHelpOptions({
        scope: 'home',
        mode: modeLabel.value,
        includeSettings: true,
        commandsKey: commandsShortcutLabel.value,
      }),
    )

    const hintGap = '   '
    const hintKey1 = computed(() => 'tab')
    const hintLabel1 = computed(() => t('homePage.hints.switchMode'))
    const hintKey2 = computed(() => commandsShortcutLabel.value.toLowerCase())
    const hintLabel2 = computed(() => t('homePage.hints.commands'))
    const hintW = computed(() => {
      return (
        textCellWidth(hintKey1.value)
        + 1
        + textCellWidth(hintLabel1.value)
        + textCellWidth(hintGap)
        + textCellWidth(hintKey2.value)
        + 1
        + textCellWidth(hintLabel2.value)
      )
    })
    const hintFill = computed(() => {
      const w = Math.max(0, hintW.value)
      if (w === 0)
        return ''
      return ' '.repeat(w)
    })
    const hintX = computed(() => {
      // Left-align hints to panel position
      return panelX.value
    })
    const hintY = computed(() => panelBottomY.value + 2)
    const showHints = computed(
      () =>
        hintY.value >= 0
        && hintY.value <= rows.value - footerH - 1
        && hintX.value + hintW.value <= cols.value,
    )

    const footerGitBranch = ref('')
    let footerGitBranchReq = 0
    const refreshFooterGitBranch = (
      next: string,
      { reset }: { reset: boolean },
    ) => {
      if (reset)
        footerGitBranch.value = ''
      if (!nodeLike)
        return
      const reqId = ++footerGitBranchReq
      void (async () => {
        const branch = await readGitBranch(next)
        if (reqId === footerGitBranchReq)
          footerGitBranch.value = branch
      })()
    }

    watch(
      pickerWorkspace,
      (next) => {
        refreshFooterGitBranch(next, { reset: true })
      },
      { immediate: true },
    )

    if (nodeLike) {
      const interval = setInterval(() => {
        refreshFooterGitBranch(pickerWorkspace.value, { reset: false })
      }, 5000)
      onBeforeUnmount(() => clearInterval(interval))
    }

    const footerLeft = computed(() => {
      const cwd = formatCwdDisplay(pickerWorkspace.value)
      const branch = footerGitBranch.value
      return branch ? `${cwd}:${branch}` : cwd
    })
    const footerRight = computed(() => {
      const env = store.state.context?.env ?? {}
      const v = readRuntimeVersion(env)
      return v ? `v${v}` : ''
    })
    const submitInFlight = ref(false)

    function go(name: string) {
      void router.push(name)
    }

    function setContextCwd(absDir: string): void {
      store.setCwd(absDir)
      applyWorkspaceSelectionForCwd(absDir)
    }

    async function submit(): Promise<void> {
      if (submitInFlight.value)
        return
      submitInFlight.value = true
      try {
        const expanded = expandMultilineTokens(
          input.value,
          multilineTexts.value,
          focusFiles.value,
        )
        const text = expanded.trim()
        // Don't navigate to chat page if there's no content - require user input
        if (!text) {
          return
        }

        const cmd = parseDimSdkSlashCommand(text)
        if (cmd.kind === 'removed') {
          if (cmd.command === '/?' || cmd.command === '/help') {
            showCopyToast(t('homePage.toast.helpAliasRemoved'))
          }
          else {
            showCopyToast(
              t('homePage.toast.commandRemoved', { command: cmd.command }),
            )
          }
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'approvals') {
          openToolApprovals()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'approvals_invalid_args') {
          showCopyToast(t('homePage.toast.approvalsUsage'))
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'mcp_settings') {
          openMcpSettings()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'plugins') {
          openPlugins()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'connect') {
          openConnect()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'models') {
          openModels()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'language') {
          openLanguage()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'theme') {
          openTheme()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'settings') {
          openCustomSettingsCommand()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'sessions') {
          await openSessionsDialog()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'new') {
          await createNewSession()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'redo') {
          showCopyToast(t('homePage.toast.redoChatOnly'))
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'rename' || cmd.kind === 'rename_invalid_args') {
          showCopyToast(t('homePage.toast.renameChatOnly'))
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'skills') {
          void openSkills()
          input.value = ''
          void setMultilineTexts([])
          return
        }
        if (cmd.kind === 'exit') {
          exitImmediately()
          return
        }

        const expandedForSend = expandMultilineTokens(
          input.value,
          multilineTexts.value,
          focusFiles.value,
        )
        const { cleaned: sendText, activatedSkills } = extractSkillsFromMessage(expandedForSend.trim())
        if (!sendText && !activatedSkills.length) {
          input.value = ''
          void setMultilineTexts([])
          focusFiles.value = []
          return
        }
        const textToSend = sendText || (activatedSkills.length ? `activate skill: ${activatedSkills.join(', ')}` : '')
        if (!textToSend) {
          input.value = ''
          void setMultilineTexts([])
          focusFiles.value = []
          return
        }
        if (!ensureActiveProviderReadyForChat())
          return
        if (
          !String(store.state.sessionId ?? '').trim()
          && sessionsApi?.createNew
        ) {
          await sessionsApi.createNew()
        }

        const hasHomeImages = focusFiles.value.some(isHomeAttachmentPath)
        if (hasHomeImages && String(store.state.sessionId ?? '').trim()) {
          focusFiles.value = await relocateHomeAttachmentsToSession(
            focusFiles.value,
            String(store.state.sessionId ?? '').trim(),
          )
        }

        input.value = ''
        void setMultilineTexts([], { deleteRemoved: false })
        go('chat')
        markFirstUserMessageSent()
        void store.send(textToSend)
        // Clear focus files after sending
        focusFiles.value = []
      }
      finally {
        submitInFlight.value = false
      }
    }

    function openCommandPalette(): void {
      showCommands.value = true
      commandIndex.value = 0
    }

    function openHelp(): void {
      showHelp.value = true
      helpIndex.value = 0
    }

    function closeCommandPalette(): void {
      showCommands.value = false
    }

    function closeHelp(): void {
      showHelp.value = false
    }

    const skillsOptions = computed(() => {
      if (!showSkills.value)
        return []
      return buildSkillPaletteOptions(skillsList.value)
    })

    async function openSkills(): Promise<void> {
      await loadSkillsList()
      showSkills.value = true
      skillsIndex.value = 0
    }

    function closeSkills(): void {
      showSkills.value = false
    }

    function onSkillSelect(label: string | null): void {
      closeSkills()
      if (!label)
        return
      const skillName = resolveSkillNameFromPaletteLabel(skillsList.value, label)
      if (!skillName)
        return
      const prefix = input.value.trimEnd()
      input.value = prefix ? `${prefix} $${skillName} ` : `$${skillName} `
    }

    function onGlobalKeydownCapture(e: any): void {
      if (anyDialogOpen.value)
        return

      if (e?.key === 'F1') {
        e?.stopPropagation?.()
        e?.preventDefault?.()
        openHelp()
        return
      }

      if (matchShortcut(e, shortcuts.value.commands)) {
        e?.stopPropagation?.()
        e?.preventDefault?.()
        openCommandPalette()
        return
      }
      if (matchShortcut(e, shortcuts.value.newSession)) {
        e?.stopPropagation?.()
        e?.preventDefault?.()
        void createNewSession()
        return
      }

      if (e?.key === 'Tab' && !e?.metaKey && !e?.ctrlKey && !e?.altKey) {
        if (!inputFocused.value) {
          e?.stopPropagation?.()
          e?.preventDefault?.()
          focusBrowserTextarea()
          focusPromptInput()
        }
      }
    }

    function onInputKeydown(e: any): void {
      if (
        e?.key === 'Enter'
        && !e?.shiftKey
        && !e?.metaKey
        && !e?.ctrlKey
        && !e?.altKey
      ) {
        const command = parseDimSdkSlashCommand(
          expandMultilineTokens(
            input.value,
            multilineTexts.value,
            focusFiles.value,
          ).trim(),
        )
        if (command.kind !== 'none') {
          e?.preventDefault?.()
          void submit()
          return
        }
      }
      if (e?.defaultPrevented)
        return
      if (e?.key === 'F1') {
        e?.preventDefault?.()
        openHelp()
        return
      }
      if (matchShortcut(e, shortcuts.value.commands)) {
        e?.preventDefault?.()
        openCommandPalette()
        return
      }
      if (matchShortcut(e, shortcuts.value.newSession)) {
        e?.preventDefault?.()
        void createNewSession()
        return
      }
      if ((e?.ctrlKey || e?.metaKey) && (e?.key === 'k' || e?.key === 'K')) {
        e?.preventDefault?.()
        openCustomSettingsCommand()
        return
      }
      if (e?.key === 'Tab' && !e?.metaKey && !e?.ctrlKey && !e?.altKey) {
        e?.preventDefault?.()
        cycleUiMode(e.shiftKey ? -1 : 1)
        return
      }
      if (e?.key === 'ArrowUp') {
        if (inputLineCount.value > 1)
          return
        if (history.value.length === 0) {
          void refreshLatestSessionHistory({ force: true })
          return
        }
        e?.preventDefault?.()
        historyIndex.value = clamp(
          historyIndex.value - 1,
          0,
          history.value.length - 1,
        )
        input.value = history.value[historyIndex.value] ?? ''
        return
      }
      if (e?.key === 'ArrowDown') {
        if (inputLineCount.value > 1)
          return
        if (history.value.length === 0)
          return
        e?.preventDefault?.()
        if (historyIndex.value >= history.value.length - 1) {
          historyIndex.value = history.value.length
          input.value = ''
          return
        }
        historyIndex.value = clamp(
          historyIndex.value + 1,
          0,
          history.value.length - 1,
        )
        input.value = history.value[historyIndex.value] ?? ''
      }
    }

    function onCommandSelect(v: string | null): void {
      closeCommandPalette()
      if (!v)
        return
      recordRecentCommand(v)
      const cmd = commandPaletteCommands.value.find(c => c.label === v)
      cmd?.run()
    }

    return () =>
      h(
        TView,
        {
          x: 0,
          y: 0,
          w: cols.value,
          h: rows.value,
          zIndex: 0,
          focusable: false,
          onKeydownCapture: onGlobalKeydownCapture,
        },
        () => [
          // Background fill (ensures DOM renderer matches CLI black background).
          h(TText, {
            key: 'bg',
            x: 0,
            y: 0,
            w: cols.value,
            h: rows.value,
            value: bgFill.value,
            style: windowStyle.value,
            zIndex: -100,
          }),

          ...(logoH.value > 0
            ? [
                ...(logoFxActive.value
                  ? [
                      h(TText, {
                        key: 'logo-shadow-open',
                        x: logoX.value + 1,
                        y: logoY.value + 1,
                        value: logoRenderLines.value.join('\n'),
                        style: logoShadowStyle.value,
                        zIndex: -70,
                      }),
                    ]
                  : []),
                h(TText, {
                  key: 'logo-open',
                  x: logoX.value,
                  y: logoY.value,
                  value: logoRenderLines.value.join('\n'),
                  style: logoBaseStyle.value,
                  zIndex: -60,
                }),
                ...(logoFxActive.value
                  ? [
                      ...logoFxSlices.value.tail.map((s, i) =>
                        h(TText, {
                          key: `logo-glint-tail-${i}`,
                          x: s.x,
                          y: s.y,
                          w: s.w,
                          h: 1,
                          value: s.value,
                          style: logoGlintTailStyle.value,
                          zIndex: -59,
                        }),
                      ),
                      ...logoFxSlices.value.core.map((s, i) =>
                        h(TText, {
                          key: `logo-glint-core-${i}`,
                          x: s.x,
                          y: s.y,
                          w: s.w,
                          h: 1,
                          value: s.value,
                          style: logoGlintCoreStyle.value,
                          zIndex: -58,
                        }),
                      ),
                      ...logoSparkles.value.map((p, i) =>
                        h(TText, {
                          key: `logo-sparkle-${i}`,
                          x: p.x,
                          y: p.y,
                          w: 1,
                          h: 1,
                          value: p.ch,
                          style: logoSparkleStyle.value,
                          zIndex: -57,
                        }),
                      ),
                    ]
                  : []),
              ]
            : []),

          // Prompt panel fill + left accent.
          h(TText, {
            key: 'panel-fill',
            x: panelX.value,
            y: panelTopY.value,
            w: panelW.value,
            h: panelH.value,
            value: panelFill.value,
            style: panelFillStyle.value,
            zIndex: -20,
          }),
          // Make the whole panel clickable to focus the input (bigger hitbox than TInput itself).
          h(TView, {
            key: 'panel-hit',
            x: panelX.value,
            y: panelTopY.value,
            w: panelW.value,
            h: panelH.value,
            // Keep above the root view so it can receive pointer events in the CLI renderer.
            // (ChatPage uses positive zIndex for the same reason.)
            zIndex: 10,
            focusable: false,
            onPointerdownCapture: () => {
              focusBrowserTextarea()
              focusPromptInput()
            },
          }),

          // Prompt input (Shift+Enter inserts newline).
          h(TInput, {
            'key': 'prompt-input',
            'x': inputX.value - 1,
            'y': panelInnerY.value,
            'w': contentW.value,
            'h': inputH.value,
            'modelValue': input.value,
            'onUpdate:modelValue': (v: string) => (input.value = v),
            'cursorToEndOnExternalUpdate': true,
            'autoFocus': !anyDialogOpen.value && !inputFocused.value,
            'cursorShape': 'bar',
            // Keep UI snapshots deterministic in tests, but allow a real caret blink in the CLI.
            'cursorBlink': process.env.DIMCODE_TEST !== '1',
            'placeholder': t('chatPage.placeholder'),
            'placeholderWhenFocused': true,
            'clearOnEscape': true,
            'style': inputStyle.value,
            'plugins': inputPlugins,
            'pasteImageHandler': nodeLike
              ? () => {
                  const sid
                    = String(store.state.sessionId ?? '').trim()
                      || HOME_SESSION_ATTACHMENT_SESSION_ID
                  return pasteClipboardImageToTempFile({ sessionId: sid })
                }
              : undefined,
            'filePasteHandler': nodeLike
              ? (absPath: string) => {
                  const sid
                    = String(store.state.sessionId ?? '').trim()
                      || HOME_SESSION_ATTACHMENT_SESSION_ID
                  return cacheDroppedFileToSession(absPath, { sessionId: sid })
                }
              : undefined,
            'promptSuggestions': promptSuggestions.value,
            'skillTrigger': '$',
            'skillSuggestions': skillSuggestions.value,
            'skillHighlightStyle': { fg: 'cyanBright', underline: true, bold: true },
            'promptAlign': 'center',
            'promptSelectedStyle': ui.style(['select.item']),
            'mentionWorkspace': nodeLike ? pickerWorkspace.value : '',
            'mentionMode': 'any',
            'collectMentions': true,
            'dedupeMentions': false,
            'mentions': focusFiles.value,
            'onUpdate:mentions': (v: readonly string[]) =>
              (focusFiles.value = [...v]),
            'onMentionClick': (absPath: string, e?: any) => {
              if (!nodeLike)
                return
              if (getCommitIdFromMention(absPath))
                return
              if (
                e?.metaKey
                || e?.ctrlKey
                || e?.altKey
                || isHomeAttachmentPath(absPath)
              ) {
                void openFileExternally(absPath)
              }
            },
            'collapseMultiline': true,
            'multilineTexts': multilineTexts.value,
            'onUpdate:multilineTexts': (v: readonly string[]) => {
              void setMultilineTexts(v)
            },
            'onMultilineClick': (idx: number) => {
              void handleMultilineClick(idx)
            },
            'onChange': () => void submit(),
            'onKeydown': (e: any) => onInputKeydown(e),
            'onValidationError': (info: any) => {
              if (info?.reason === 'paste_image_pending')
                showCopyToast(t('homePage.toast.pasteImagePending'))
            },
            'onFocus': () => {
              inputFocused.value = true
            },
            'onBlur': () => (inputFocused.value = false),
            // Keep above the root view so it can receive pointer events in the CLI renderer.
            'zIndex': 11,
          }),

          // Model row (inside the panel).
          (() => {
            const rowY = modelRowY.value
            const chip = modelInfo.value.build
            const chipW = textCellWidth(chip)
            const rightPad = 2
            // `rightPad` is only for right alignment when there's extra space.
            // The label can still use the full remaining width when space is tight.
            const maxLabelW = Math.max(0, inputInnerW.value - chipW - 1)
            const label = fitModelProviderLabel(
              modelInfo.value.model,
              modelInfo.value.providerName,
              maxLabelW,
            )
            const labelW = textCellWidth(label)
            const totalW = chipW + 1 + labelW
            const startX
              = inputX.value + Math.max(0, inputInnerW.value - totalW - rightPad)

            const statusW = Math.max(0, startX - inputX.value - 1)
            const nodes = []

            if (statusW > 0) {
              const mode = modeLabel.value
              const modeTag = ` ${mode.toUpperCase()} `
              const modeStyle
                = mode === 'plan'
                  ? ui.style(['chip.plan'])
                  : ui.style(['chip.agent'])
              const modeW = Math.min(statusW, textCellWidth(modeTag))
              nodes.push(
                h(TText, {
                  key: 'panel-mode',
                  x: inputX.value,
                  y: rowY,
                  w: modeW,
                  value: modeTag,
                  style: modeStyle,
                  zIndex: -18,
                }),
              )
            }

            nodes.push(
              h(TText, {
                key: 'model-chip',
                x: startX,
                y: rowY,
                value: chip,
                style: ui.style(['chip.meta']),
                zIndex: -18,
              }),
            )

            nodes.push(
              h(TText, {
                key: 'model-label',
                x: startX + chipW + 1,
                y: rowY,
                w: Math.max(
                  0,
                  inputX.value + inputInnerW.value - (startX + chipW + 1),
                ),
                value: label,
                style: ui.style(['text.muted', 'bg.chrome']),
                zIndex: -18,
              }),
            )
            return nodes
          })(),

          // Hints (outside, below the panel).
          showHints.value
            ? [
                h(TText, {
                  key: 'hints-bg',
                  x: hintX.value,
                  y: hintY.value,
                  w: hintW.value,
                  value: hintFill.value,
                  style: windowStyle.value,
                  zIndex: 4,
                }),
                h(TText, {
                  key: 'hints-key1',
                  x: hintX.value,
                  y: hintY.value,
                  value: hintKey1.value,
                  style: ui.style(['accent.cyan', 'bg.window']),
                  clear: false,
                  zIndex: 5,
                }),
                h(TText, {
                  key: 'hints-label1',
                  x: hintX.value + textCellWidth(hintKey1.value) + 1,
                  y: hintY.value,
                  value: hintLabel1.value,
                  style: mutedOnWindow.value,
                  clear: false,
                  zIndex: 5,
                }),
                h(TText, {
                  key: 'hints-key2',
                  x:
                    hintX.value
                    + textCellWidth(hintKey1.value)
                    + 1
                    + textCellWidth(hintLabel1.value)
                    + textCellWidth(hintGap),
                  y: hintY.value,
                  value: hintKey2.value,
                  style: ui.style(['accent.cyan', 'bg.window']),
                  clear: false,
                  zIndex: 5,
                }),
                h(TText, {
                  key: 'hints-label2',
                  x:
                    hintX.value
                    + textCellWidth(hintKey1.value)
                    + 1
                    + textCellWidth(hintLabel1.value)
                    + textCellWidth(hintGap)
                    + textCellWidth(hintKey2.value)
                    + 1,
                  y: hintY.value,
                  value: hintLabel2.value,
                  style: mutedOnWindow.value,
                  clear: false,
                  zIndex: 5,
                }),
              ]
            : null,

          // Footer status (render one row above the bottom for spacing).
          rows.value > 0
            ? h(TText, {
                key: 'footer-bg',
                x: 0,
                y: rows.value - footerH,
                w: cols.value,
                value: footerFill.value,
                style: windowStyle.value,
                zIndex: 20,
              })
            : null,
          rows.value > 0
            ? h(TText, {
                key: 'footer-left',
                x: 2,
                y: rows.value - footerH,
                w: Math.max(0, cols.value - 4),
                value: t('homePage.footerCwd', { cwd: footerLeft.value }),
                style: mutedOnWindow.value,
                clear: false,
                zIndex: 21,
              })
            : null,
          rows.value > 0 && footerRight.value
            ? h(TText, {
                key: 'footer-right',
                x: Math.max(
                  2,
                  cols.value - 2 - textCellWidth(footerRight.value),
                ),
                y: rows.value - footerH,
                value: footerRight.value,
                style: mutedOnWindow.value,
                clear: false,
                zIndex: 21,
              })
            : null,

          h(DimSdkCommandPalette, {
            'modelValue': showCommands.value,
            'onUpdate:modelValue': (v: boolean) => (showCommands.value = v),
            'title': t('commandPalette.title'),
            'options': commandOptions.value,
            'selectedIndex': commandIndex.value,
            'onUpdate:selectedIndex': (v: number) => (commandIndex.value = v),
            'onSelect': (v: string | null) => onCommandSelect(v),
            'onClose': closeCommandPalette,
          }),

          h(DimSdkCommandPalette, {
            'modelValue': showHelp.value,
            'onUpdate:modelValue': (v: boolean) => (showHelp.value = v),
            'title': t('help.title'),
            'options': helpOptions.value,
            'selectedIndex': helpIndex.value,
            'onUpdate:selectedIndex': (v: number) => (helpIndex.value = v),
            'onSelect': () => closeHelp(),
            'onClose': closeHelp,
          }),

          h(DimSdkCommandPalette, {
            'modelValue': showSkills.value,
            'onUpdate:modelValue': (v: boolean) => (showSkills.value = v),
            'title': 'Skills',
            'options': skillsOptions.value,
            'selectedIndex': skillsIndex.value,
            'onUpdate:selectedIndex': (v: number) => (skillsIndex.value = v),
            'onSelect': (v: string | null) => onSkillSelect(v),
            'onClose': closeSkills,
          }),

          h(DimSdkCommandPalette, {
            'modelValue': showConnect.value,
            'onUpdate:modelValue': (v: boolean) => (showConnect.value = v),
            'title': t('homePage.commandPalette.connect'),
            'options': providerOptions.value,
            'selectedIndex': providerIndex.value,
            'onUpdate:selectedIndex': (v: number) => (providerIndex.value = v),
            'showRowDetails': true,
            'onSelect': (v: string | null) => onSelectProvider(v),
            'onClose': closeConnect,
          }),

          h(DimSdkCommandPalette, {
            'modelValue': showCustomManage.value,
            'onUpdate:modelValue': (v: boolean) => (showCustomManage.value = v),
            'title': customManageTitle.value,
            'options': customManageOptions.value,
            'selectedIndex': customManageIndex.value,
            'onUpdate:selectedIndex': (v: number) =>
              (customManageIndex.value = v),
            'onSelect': (v: string | null) => onSelectCustomManage(v),
            'onClose': closeCustomManage,
          }),

          h(DimSdkCommandPalette, {
            'modelValue': showCustomDeleteConfirm.value,
            'onUpdate:modelValue': (v: boolean) =>
              (showCustomDeleteConfirm.value = v),
            'title': customDeleteConfirmTitle.value,
            'options': customDeleteConfirmOptions.value,
            'selectedIndex': customDeleteConfirmIndex.value,
            'onUpdate:selectedIndex': (v: number) =>
              (customDeleteConfirmIndex.value = v),
            'onSelect': (v: string | null) => onSelectCustomDeleteConfirm(v),
            'onClose': closeCustomDeleteConfirm,
          }),

          h(DimSdkCommandPalette, {
            'modelValue': showModels.value,
            'onUpdate:modelValue': (v: boolean) => (showModels.value = v),
            'title': t('homePage.commandPalette.models'),
            'options': modelOptions.value,
            'selectedIndex': providerModelIndex.value,
            'onUpdate:selectedIndex': (v: number) =>
              (providerModelIndex.value = v),
            'showRowDetails': true,
            'onSelect': (v: string | null) => onSelectModel(v),
            'onClose': closeModels,
          }),

          h(DimSdkPathPickerDialog, {
            'modelValue': showPathPicker.value,
            'onUpdate:modelValue': (v: boolean) => {
              if (!v)
                closePathPicker()
              else showPathPicker.value = true
            },
            'w': pickerW.value,
            'h': pickerH.value,
            'title': pathPickerTitle.value,
            'workspace': pickerWorkspace.value,
            'mode': pathPickerMode.value,
            'query': pathPickerQuery.value,
            'onUpdate:query': (v: string) => (pathPickerQuery.value = v),
            'placeholder': pathPickerPlaceholder.value,
            'onInvalid': (reason: string) => (pathPickerError.value = reason),
            'onSelect': (absPath: string) => {
              if (pathPickerMode.value === 'directory') {
                setContextCwd(absPath)
              }
              else {
                if (!focusFiles.value.includes(absPath))
                  focusFiles.value = [...focusFiles.value, absPath]
              }
              closePathPicker()
            },
            'onClose': closePathPicker,
          }),

          h(DimSdkSessionsDialog, {
            'modelValue': showSessions.value,
            'onUpdate:modelValue': (v: boolean) => {
              showSessions.value = v
              if (!v)
                sessionsDeleteArmed.value = false
            },
            'loading': sessionsLoading.value,
            'deleteArmed': sessionsDeleteArmed.value,
            'cwdOnly': sessionsCwdOnly.value,
            'onUpdate:cwdOnly': (v: boolean) => {
              sessionsCwdOnly.value = v
              sessionsDeleteArmed.value = false
            },
            'currentCwd': String((store.state as any)?.context?.cwd ?? ''),
            'activeSessionId': String(store.state.sessionId ?? ''),
            'allSessionsCount': sessionsAllCount.value,
            'sessions': sessionsList.value,
            'selectedIndex': sessionsIndex.value,
            'onUpdate:selectedIndex': (v: number) => {
              sessionsIndex.value = v
              sessionsDeleteArmed.value = false
            },
            'onOpen': async () => {
              await openSelectedSession({ after: async () => go('chat') })
            },
            'onDelete': async () => {
              const api = sessionsApi
              const picked = sessionsList.value[sessionsIndex.value]
              if (!api || !picked)
                return
              const prevIndex = sessionsIndex.value
              if (!sessionsDeleteArmed.value) {
                sessionsDeleteArmed.value = true
                showCopyToast(t('homePage.toast.deleteConfirm'))
                return
              }
              sessionsDeleteArmed.value = false
              await api.delete(picked.id)
              await refreshSessions({ preferIndex: prevIndex })
            },
            'onRefresh': async () => {
              sessionsDeleteArmed.value = false
              await refreshSessions()
            },
          }),

          h(DimSdkSwitchSessionDialog, {
            'modelValue': showSwitchConfirm.value,
            'onUpdate:modelValue': (v: boolean) => {
              showSwitchConfirm.value = v
              if (!v)
                cancelSwitch()
            },
            'title': switchConfirmTitle.value,
            'targetLabel': switchConfirmTargetLabel.value,
            'reason': switchConfirmReason.value,
            'onConfirm': () => void confirmSwitch(),
          }),

          h(DimSdkConfigDialog, {
            'modelValue': showConfig.value,
            'onUpdate:modelValue': (v: boolean) => (showConfig.value = v),
            'w': dialogSize('lg', cols.value, rows.value).w,
            'h': dialogSize('lg', cols.value, rows.value).h,
            'showToast': showCopyToast,
            'contextWindow': contextWindow.value,
            'contextWindowLocked': contextWindowLocked.value,
            'onUpdate:contextWindow': (v: string) => (contextWindow.value = v),
            'maxTokens': maxTokens.value,
            'onUpdate:maxTokens': (v: string) => (maxTokens.value = v),
            'temperature': temperature.value,
            'onUpdate:temperature': (v: string) => (temperature.value = v),
            'interleavedThinking': interleavedThinking.value,
            'interleavedThinkingLocked': interleavedThinkingLocked.value,
            'onUpdate:interleavedThinking': (v: boolean) =>
              (interleavedThinking.value = v),
            'shortcutCommands': shortcutCommands.value,
            'onUpdate:shortcutCommands': (v: string) =>
              (shortcutCommands.value = v),
            'shortcutTimeline': shortcutTimeline.value,
            'onUpdate:shortcutTimeline': (v: string) =>
              (shortcutTimeline.value = v),
            'shortcutNewSession': shortcutNewSession.value,
            'onUpdate:shortcutNewSession': (v: string) =>
              (shortcutNewSession.value = v),
            'onApply': applyConfig,
            'onCancel': closeConfig,
          }),

          h(DimSdkLanguageDialog, {
            'modelValue': showLanguage.value,
            'onUpdate:modelValue': (v: boolean) => (showLanguage.value = v),
            'w': dialogSize('md', cols.value, rows.value).w,
            'h': dialogSize('md', cols.value, rows.value).h,
            'locale': locale.value,
            'onUpdate:locale': (v: string) => (locale.value = v),
            'onApply': applyLanguage,
            'onCancel': closeLanguage,
          }),

          h(DimSdkThemeDialog, {
            'modelValue': showTheme.value,
            'onUpdate:modelValue': (v: boolean) => (showTheme.value = v),
            'w': dialogSize('md', cols.value, rows.value).w,
            'mode': themeMode.value,
            'onUpdate:mode': (v: string) => (themeMode.value = v),
            'onApply': applyTheme,
            'onCancel': closeTheme,
          }),

          h(DimSdkToolSettingsDialog, {
            'modelValue': showToolSettings.value,
            'onUpdate:modelValue': (v: boolean) => (showToolSettings.value = v),
            'w': dialogSize('md', cols.value, rows.value).w,
            'h': dialogSize('md', cols.value, rows.value).h,
            'apiKeyDraft': toolApiKeyDraft.value,
            'onUpdate:apiKeyDraft': (v: string) => (toolApiKeyDraft.value = v),
            'apiKeyMasked': toolApiKeyMasked.value,
            'onApply': applyToolSettings,
            'onCancel': closeToolSettings,
          }),

          h(DimSdkMcpSettingsDialog, {
            'modelValue': showMcpSettings.value,
            'onUpdate:modelValue': (v: boolean) => (showMcpSettings.value = v),
            'w': dialogSize('xl', cols.value, rows.value).w,
            'h': dialogSize('xl', cols.value, rows.value).h,
            'mcpJsonFilePath': mcpJsonFilePath.value,
            'serverCount': mcpServerCount.value,
            'testStatus': mcpTestStatus.value,
            'onOpenFile': openMcpFile,
            'onReload': reloadMcpConfig,
            'onTest': testMcpConnection,
            'onClose': closeMcpSettings,
          }),

          h(DimSdkCommandPalette, {
            'modelValue': showPlugins.value,
            'onUpdate:modelValue': (v: boolean) => (showPlugins.value = v),
            'title': pluginsTitle.value,
            'options': pluginsOptions.value,
            'selectedIndex': pluginsIndex.value,
            'showRowDetails': true,
            'onUpdate:selectedIndex': (v: number) => (pluginsIndex.value = v),
            'onSelect': (v: string | null) => onPluginSelect(v),
            'onClose': closePlugins,
          }),

          h(DimSdkToolSettingsDialog, {
            'modelValue': showPluginConfig.value,
            'onUpdate:modelValue': (v: boolean) => (showPluginConfig.value = v),
            'title': t('pluginsDialog.serperConfigTitle'),
            'w': dialogSize('md', cols.value, rows.value).w,
            'h': dialogSize('md', cols.value, rows.value).h,
            'apiKeyDraft': pluginApiKeyDraft.value,
            'onUpdate:apiKeyDraft': (v: string) => (pluginApiKeyDraft.value = v),
            'apiKeyMasked': pluginApiKeyMasked.value,
            'onApply': applySerperConfig,
            'onCancel': closeSerperConfig,
          }),

          h(DimSdkProviderConnectDialog, {
            'modelValue': showConnectDialog.value,
            'onUpdate:modelValue': (v: boolean) =>
              (showConnectDialog.value = v),
            'w': dialogSize('lg', cols.value, rows.value).w,
            'h': connectDialogH.value,
            'providerName': connectProviderName.value,
            'baseUrl': connectBaseUrl.value,
            'onUpdate:baseUrl': (v: string) => updateConnectBaseUrlDraft(v),
            'apiKeyDraft': connectApiKeyDraft.value,
            'onUpdate:apiKeyDraft': (v: string) =>
              (connectApiKeyDraft.value = v),
            'apiKeyMasked': connectApiKeyMasked.value,
            'canClearKey': connectCanClearKey.value,
            'applying': connectApplying.value,
            'focusKey': connectFocusKey.value,
            'baseUrlMode': connectBaseUrlMode.value,
            'showApiKey': true,
            'baseUrlAfterApiKey': connectBaseUrlAfterApiKey.value,
            'onApply': applyConnect,
            'onClearKey': clearConnectKey,
            'onCancel': cancelConnectDialog,
          }),
          h(DimSdkNextApiOAuthDialog, {
            'modelValue': showNextApiOAuth.value,
            'onUpdate:modelValue': (v: boolean) => {
              if (!v)
                closeNextApiOAuth()
              else showNextApiOAuth.value = true
            },
            'w': dialogSize('xl', cols.value, rows.value).w,
            'h': dialogSize('xl', cols.value, rows.value).h,
            'oauthStatus': nextApiOAuthStatus.value,
            'oauthEmail': nextApiOAuthEmail.value,
            'oauthUrl': nextApiOAuthUrl.value,
            'oauthError': nextApiOAuthError.value,
            'selectedModel': selectedNextApiModel.value,
            'nextApiModels': nextApiModels.value,
            'modelIndex': nextApiModelIndex.value,
            'onUpdate:selectedModel': (v: string) =>
              (selectedNextApiModel.value = v),
            'onUpdate:modelIndex': (v: number) => (nextApiModelIndex.value = v),
            'onLogin': () => void loginWithNextApiOAuth(),
            'onLogout': () => void logoutNextApiOAuth(),
            'onClose': () => closeNextApiOAuth(),
            'onSelectModel': (modelId: string) =>
              void selectNextApiModel(modelId),
            'onViewPlans': () => void viewNextApiPlans(),
          }),

          h(DimSdkCommandPalette, {
            'modelValue': showCustomType.value,
            'onUpdate:modelValue': (v: boolean) => (showCustomType.value = v),
            'title': t('providerDialogs.addCustomLabel'),
            'options': customTypeOptions.value,
            'selectedIndex': customTypeIndex.value,
            'onUpdate:selectedIndex': (v: number) =>
              (customTypeIndex.value = v),
            'onSelect': (v: string | null) => onSelectCustomType(v),
            'onClose': closeCustomType,
          }),

          h(DimSdkProviderConnectDialog, {
            'modelValue': showCustomConnectDialog.value,
            'onUpdate:modelValue': (v: boolean) =>
              (showCustomConnectDialog.value = v),
            'w': dialogSize('lg', cols.value, rows.value).w,
            'h': connectDialogH.value,
            'providerName': t('providerDialogs.customLabel'),
            'baseUrl': customConnectBaseUrl.value,
            'onUpdate:baseUrl': (v: string) =>
              updateCustomConnectBaseUrlDraft(v),
            'apiKeyDraft': customConnectApiKeyDraft.value,
            'onUpdate:apiKeyDraft': (v: string) =>
              (customConnectApiKeyDraft.value = v),
            'apiKeyMasked': customConnectApiKeyMasked.value,
            'focusKey': customConnectFocusKey.value,
            'showBaseUrl': true,
            'showApiKey': true,
            'baseUrlAfterApiKey': false,
            'onApply': applyCustomConnect,
            'onCancel': cancelCustomConnectDialog,
          }),

          h(DimSdkCustomProviderModelsDialog, {
            'modelValue': showCustomModelsDialog.value,
            'onUpdate:modelValue': (v: boolean) =>
              (showCustomModelsDialog.value = v),
            'w': dialogSize('lg', cols.value, rows.value).w,
            'h': connectDialogH.value,
            'modelsDraft': customModelsDraft.value,
            'onUpdate:modelsDraft': (v: string) =>
              (customModelsDraft.value = v),
            'fetching': customModelsFetching.value,
            'onFetch': openCustomModelsFetch,
            'onApply': applyCustomModels,
            'onCancel': cancelCustomModelsDialog,
          }),

          h(DimSdkCustomProviderModelsFetchDialog, {
            'modelValue': showCustomModelsFetchDialog.value,
            'onUpdate:modelValue': (v: boolean) =>
              (showCustomModelsFetchDialog.value = v),
            'w': dialogSize('lg', cols.value, rows.value).w,
            'h': connectDialogH.value,
            'options': customModelsFetchOptions.value as any,
            'selectedIndices': customModelsFetchSelected.value,
            'onUpdate:selectedIndices': (v: number[]) =>
              (customModelsFetchSelected.value = v),
            'onConfirm': applyCustomModelsFetch,
            'onCancel': cancelCustomModelsFetchDialog,
          }),

          h(DimSdkCustomProviderNameDialog, {
            'modelValue': showCustomNameDialog.value,
            'onUpdate:modelValue': (v: boolean) =>
              (showCustomNameDialog.value = v),
            'w': dialogSize('md', cols.value, rows.value).w,
            'h': Math.max(10, Math.min(14, dialogSize('md', cols.value, rows.value).h)),
            'nameDraft': customNameDraft.value,
            'onUpdate:nameDraft': (v: string) => (customNameDraft.value = v),
            'onApply': applyCustomName,
            'onCancel': cancelCustomNameDialog,
          }),
          h(DimSdkToolApprovalsDialog, {
            'modelValue': showToolApprovals.value,
            'onUpdate:modelValue': (v: boolean) =>
              (showToolApprovals.value = v),
            'mode': String(store.state.approval.mode ?? 'auto') as any,
            'w': dialogSize('md', cols.value, rows.value).w,
            'h': dialogSize('md', cols.value, rows.value).h,
            'onSelect': (v: 'auto' | 'normal' | 'strict') => {
              applyToolApprovals(v)
              showToolApprovals.value = false
            },
          }),

          h(TRenderLayer, { zIndex: 2500 }, () => [
            // Upgrade toast overlay
            ...renderUpgradeToastOverlay({
              notification: store.upgradeNotification?.value,
              cols: cols.value,
              onDismiss: () => {
                store.dismissUpgradeNotification?.()
              },
            }),

            // Copy toast overlay (shared with ChatPage)
            ...renderCopyToastOverlay({
              visible: copyToastVisible,
              text: copyToastText,
              cols,
              ui,
            }),
          ]),
        ],
      )
  },
})
