import process from 'node:process'
import { getCommitIdFromMention } from '#cli/core/commit-mentions'
import { SESSION_QUEUE_LIMIT } from '#cli/core/session-meta'
import { t } from '#cli/i18n'
import {
  createNodeMentionPathProvider,
  createPromptMentionPlugin,
  padEndByCells,
  sliceByCells,
  textCellWidth,
  TInput,
  TRenderLayer,
  TText,
  TView,
  useTerminal,
} from '@simon_he/vue-tui'
import { defineComponent, getCurrentInstance, h, inject, onMounted, ref, watch, watchEffect } from 'vue'
import {
  cacheDroppedFileToSession,
  HOME_SESSION_ATTACHMENT_SESSION_ID,
  pasteClipboardImageToTempFile,
} from '../../utils/clipboard-image'
import { createGitCommitMentionProvider } from '../../utils/git-commit-mentions'
import { openPathExternally } from '../../utils/open-external'
import { fitModelProviderLabel } from '../../utils/text-fit'
import { ChatPageContextKey } from '../context'

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

function fitApprovalButtonLabels(availableCells: number): {
  approve: string
  always: string
  deny: string
} {
  const gap = availableCells < 34 ? 1 : 2
  const n = 3
  const maxTotal = Math.max(0, Math.floor(availableCells))

  const totalW = (approve: string, always: string, deny: string) => {
    const labels = [approve, always, deny]
    const widths = labels.map((l) => textCellWidth(`[ ${l} ]`))
    return widths.reduce((sum, w) => sum + w, 0) + gap * Math.max(0, n - 1)
  }

  const variants = [
    {
      approve: t('approvalDialog.buttons.approve'),
      always: t('approvalDialog.buttons.alwaysApprove'),
      deny: t('approvalDialog.buttons.deny'),
    },
    {
      approve: t('approvalDialog.buttons.approve'),
      always: t('approvalDialog.buttons.always'),
      deny: t('approvalDialog.buttons.deny'),
    },
    {
      approve: t('approvalDialog.buttons.ok'),
      always: t('approvalDialog.buttons.always'),
      deny: t('approvalDialog.buttons.no'),
    },
  ] as const

  for (const v of variants) {
    if (totalW(v.approve, v.always, v.deny) <= maxTotal) return v
  }
  return variants[variants.length - 1]!
}

function stripAutoAppendedReasonDetail(reason: string): string {
  const text = String(reason ?? '')
    .replace(/\s+/g, ' ')
    .trim()
  const idx = text.search(/\s(?:Args|Command|Description|命令|说明)[：:]\s/i)
  if (idx <= 0) return text
  return text.slice(0, idx).trimEnd()
}

function extractApprovalCommand(args: unknown): string {
  if (!args || typeof args !== 'object' || Array.isArray(args)) return ''
  const command = String((args as any)?.command ?? '').trim()
  return command
}

function clipWithEllipsis(text: string, width: number): string {
  const maxW = Math.max(0, Math.floor(width))
  if (maxW <= 0) return ''
  const raw = String(text ?? '')
  if (textCellWidth(raw) <= maxW) return raw
  if (maxW <= 1) return '…'
  return `${sliceByCells(raw, maxW - 1)}…`
}

export const DimSdkChatBottomPanel = defineComponent({
  name: 'DimSdkChatBottomPanel',
  props: {
    refreshKey: { type: String, required: true },
  },
  setup(props) {
    const injected = inject(ChatPageContextKey)
    if (!injected)
      throw new Error('DimSdkChatBottomPanel missing ChatPageContext')
    const ctx = injected
    const instance = getCurrentInstance()
    const { events, scheduler, render } = useTerminal()
    const inputFocused = ref(false)
    const mentionProviders = ctx.nodeLike
      ? [createGitCommitMentionProvider()]
      : []
    const inputPlugins = [
      createPromptMentionPlugin({
        mentionSuggestionProviders: mentionProviders,
        mentionPathProvider: ctx.nodeLike
          ? createNodeMentionPathProvider()
          : undefined,
      }),
    ]
    const getPendingApprovalRequest = () => {
      const req =
        (ctx as any)?.activeApproval?.value ??
        (ctx as any)?.store?.state?.approval?.active ??
        null
      if (!req || req.status !== 'pending') return null
      return req as any
    }
    const getAskUserState = () => {
      if ((ctx as any)?.showAskUser?.value)
        return null
      const paused = (ctx as any)?.store?.state?.paused
      if (!paused || paused.kind !== 'ask_user') return null
      const state = (ctx as any)?.askUserState?.value
      if (
        !state ||
        !Array.isArray(state.questions) ||
        state.questions.length === 0
      )
        return null
      return state as any
    }

    function getAskUserOptionsRect(): Readonly<{
      x: number
      y: number
      w: number
      h: number
    }> | null {
      const state = getAskUserState()
      if (!state || state.focusTarget === 'input') return null
      return {
        x: ctx.inputX.value - 1,
        y: ctx.panelTopY.value,
        w: Math.max(1, ctx.inputInnerW.value),
        h: Math.max(1, Math.floor(ctx.panelH.value)),
      }
    }
    const hasPendingApproval = () => {
      return Boolean(getPendingApprovalRequest())
    }
    const isLivePendingQueueItem = (
      item: { livePending?: boolean } | null | undefined,
    ): boolean => {
      return Boolean(item?.livePending)
    }
    const getQueueStateLabel = (): string => {
      switch (ctx.queueStatus.value) {
        case 'running':
          return t('chatQueue.state.running')
        case 'paused_confirm':
          return t('chatQueue.state.paused')
        case 'error':
          return t('chatQueue.state.error')
        default:
          return t('chatQueue.state.idle')
      }
    }
    const getQueueSteerStateLabel = (item: {
      kind?: string
      livePending?: boolean
    }): string => {
      if (item.kind !== 'steer') return ''
      if (isLivePendingQueueItem(item)) return t('chatQueue.item.steerPending')
      if ((ctx as any)?.store?.state?.runningAgentRound)
        return t('chatQueue.item.steerPending')
      if (ctx.queueStatus.value === 'paused_confirm')
        return t('chatQueue.item.steerPaused')
      return t('chatQueue.item.steerReady')
    }

    function getAskUserCustomInputRect(): Readonly<{
      x: number
      y: number
      w: number
      h: number
    }> | null {
      const state = getAskUserState()
      if (!state || state.focusTarget !== 'input') return null
      const qTotal = Math.max(1, Number(state.questions?.length ?? 0))
      const qIdx = Math.max(
        0,
        Math.min(qTotal - 1, Math.floor(Number(state.currentIndex ?? 0))),
      )
      const question = state.questions?.[qIdx]
      const qKey = String(qIdx)
      const otherLabel = t('askUserDialog.otherOption')
      const selectedLabels = Array.isArray(state.selections?.[qKey])
        ? state.selections[qKey]
        : []
      const selectedSet = new Set(selectedLabels.map((v: any) => String(v)))
      const showCustomInput =
        String(question?.kind ?? '') === 'input' ||
        selectedSet.has(otherLabel) || state.focusTarget === 'input'
      if (!showCustomInput) return null

      const contentRows = Math.max(1, Math.floor(ctx.panelH.value))
      const topY = ctx.panelTopY.value
      const actionY = topY + Math.max(0, contentRows - 1)
      const questionY = topY + 1
      const customY = Math.max(questionY + 1, actionY - 1)
      if (customY >= actionY) return null

      const summaryW = Math.max(0, ctx.inputInnerW.value)
      const customLabel = t('askUserDialog.customAnswerLabel')
      const labelW = Math.max(0, Math.min(summaryW, textCellWidth(customLabel)))
      const x = ctx.inputX.value + Math.min(summaryW, labelW + 1)
      const w = Math.max(1, summaryW - Math.min(summaryW, labelW + 1))
      return { x, y: customY, w, h: 1 }
    }

    function focusAskUserCustomInput(attempt = 0): void {
      if (ctx.anyDialogOpen.value) return
      const targetRect = getAskUserCustomInputRect()
      if (!targetRect) return
      const manager: any = events.value
      if (
        !manager ||
        typeof manager.debugNodes !== 'function' ||
        typeof manager.focus !== 'function'
      )
        return
      const focusedId =
        typeof manager.getFocused === 'function' ? manager.getFocused() : null
      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 === targetRect.x &&
          r.y === targetRect.y &&
          r.w === targetRect.w &&
          r.h === targetRect.h
        )
      })
      if (!exact?.id) {
        if (attempt === 0) setTimeout(() => focusAskUserCustomInput(1), 0)
        return
      }
      if (focusedId === exact.id) {
        ctx.focusMode.value = 'input'
        render.invalidatePlane('default')
        scheduler.invalidate({ priority: 'high' })
        return
      }
      manager.focus(exact.id)
      ctx.focusMode.value = 'input'
      render.invalidatePlane('default')
      scheduler.invalidate({ priority: 'high' })
    }

    function focusAskUserOptions(attempt = 0): void {
      if (ctx.anyDialogOpen.value) return
      const targetRect = getAskUserOptionsRect()
      if (!targetRect) return
      const manager: any = events.value
      if (
        !manager ||
        typeof manager.debugNodes !== 'function' ||
        typeof manager.focus !== 'function'
      )
        return

      const focusedId =
        typeof manager.getFocused === 'function' ? manager.getFocused() : null
      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 === targetRect.x &&
          r.y === targetRect.y &&
          r.w === targetRect.w &&
          r.h === targetRect.h
        )
      })
      if (!exact?.id) {
        if (attempt === 0) setTimeout(() => focusAskUserOptions(1), 0)
        return
      }
      if (focusedId === exact.id) {
        ctx.focusMode.value = 'input'
        render.invalidatePlane('default')
        scheduler.invalidate({ priority: 'high' })
        return
      }
      manager.focus(exact.id)
      ctx.focusMode.value = 'input'
      render.invalidatePlane('default')
      scheduler.invalidate({ priority: 'high' })
    }

    function focusChatInput(): void {
      if (hasPendingApproval()) return
      const askUserState = getAskUserState()
      if (askUserState) {
        if (askUserState.focusTarget === 'input') focusAskUserCustomInput()
        else focusAskUserOptions()
        return
      }
      if (ctx.anyDialogOpen.value) return
      const manager: any = events.value
      if (
        !manager ||
        typeof manager.debugNodes !== 'function' ||
        typeof manager.focus !== 'function'
      )
        return

      const focusedId =
        typeof manager.getFocused === 'function' ? manager.getFocused() : null
      const wantX = Math.floor(ctx.inputX.value - 1)
      const wantY = Math.floor(ctx.panelTopY.value)
      const wantW = Math.floor(ctx.inputInnerW.value)
      const wantH = Math.floor(ctx.inputLines.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) {
        if (focusedId === exact.id) {
          inputFocused.value = true
          render.invalidatePlane('default')
          scheduler.invalidate({ priority: 'high' })
          return
        }
        manager.focus(exact.id)
        inputFocused.value = true
        render.invalidatePlane('default')
        scheduler.invalidate({ priority: 'high' })
        return
      }

      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) {
        if (focusedId === picked.id) {
          inputFocused.value = true
          render.invalidatePlane('default')
          scheduler.invalidate({ priority: 'high' })
          return
        }
        manager.focus(picked.id)
        inputFocused.value = true
        render.invalidatePlane('default')
        scheduler.invalidate({ priority: 'high' })
      }
    }

    onMounted(() => {
      setTimeout(() => {
        focusChatInput()
      })
    })

    watchEffect(() => {
      const askUserState = getAskUserState()
      if (!askUserState) return
      if (askUserState.focusTarget === 'input') {
        focusAskUserCustomInput()
        return
      }
      focusAskUserOptions()
    })

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

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

    watch(
      () => [
        props.refreshKey,
        ctx.panelTopY.value,
        ctx.panelH.value,
        ctx.inputX.value,
        ctx.inputInnerW.value,
        ctx.inputLines.value,
        ctx.modelRowGap.value,
        ctx.input.value,
        ctx.inputPlaceholder.value,
        ctx.focusMode.value,
        ctx.ctxPctChip.value,
        ctx.modelInfo.value.model,
        ctx.modelInfo.value.providerName,
        ctx.modeLabel.value,
        ctx.assistantStatusDisplay.value,
        ctx.thinkingStatusDisplay.value,
        ctx.loadingSpinnerGlyph.value,
        ctx.isLoading.value,
        String((ctx as any)?.askUserState?.value?.toolCallId ?? ''),
        Number((ctx as any)?.askUserState?.value?.currentIndex ?? 0),
        String((ctx as any)?.askUserState?.value?.focusTarget ?? ''),
        JSON.stringify((ctx as any)?.askUserState?.value?.optionFocusByQuestion ?? {}),
        JSON.stringify((ctx as any)?.askUserState?.value?.selections ?? {}),
        JSON.stringify((ctx as any)?.askUserState?.value?.otherInputs ?? {}),
        Number((ctx as any)?.store?.state?.chromeVersion ?? 0),
      ],
      () => {
        if (instance?.isMounted)
          ;(instance as any)?.update?.()
        render.invalidatePlane('chrome')
        scheduler.invalidate({ priority: 'high', plane: 'chrome' })
      },
      { flush: 'post' },
    )

    return () => {
      void props.refreshKey
      const ui = ctx.ui
      const sessionId = String(
        (ctx as any)?.store?.state?.sessionId ?? '',
      ).trim()
      const attachmentSessionId = sessionId || HOME_SESSION_ATTACHMENT_SESSION_ID
      const pendingApprovalReq = getPendingApprovalRequest()
      const pendingApproval = Boolean(pendingApprovalReq)
      const askUserState = getAskUserState()
      const askUserActive = Boolean(askUserState)
      const emphasizePanel = pendingApproval || askUserActive
      const borderToken = pendingApproval
        ? 'status.warning'
        : askUserActive
          ? 'focus.ring'
          : inputFocused.value
            ? 'focus.ring'
            : 'border.default'
      const borderStyle = ui.style(
        [borderToken, 'bg.window'],
        emphasizePanel ? { bold: true } : undefined,
      )
      const panelFillStyle = ui.style(['text.primary', 'bg.window'])
      const inputStyle = ui.style(['input.field', 'bg.window'])
      const promptSelectedStyle = ui.style(['select.item'])
      const innerW = ctx.panelW.value - 2
      const innerH = ctx.panelH.value
      const topBorder = emphasizePanel
        ? `┏${'━'.repeat(Math.max(0, innerW))}┓`
        : `╭${'─'.repeat(Math.max(0, innerW))}╮`
      const bottomBorder = emphasizePanel
        ? `┗${'━'.repeat(Math.max(0, innerW))}┛`
        : `╰${'─'.repeat(Math.max(0, innerW))}╯`
      const sideBorderLeft = Array.from({ length: innerH }, () =>
        emphasizePanel ? '┃' : '│',
      ).join('\n')
      const sideBorderRight = Array.from({ length: innerH }, () =>
        emphasizePanel ? '┃' : '│',
      ).join('\n')
      const panelFill = (() => {
        const w = Math.max(0, ctx.panelW.value - 2)
        const h = Math.max(0, ctx.panelH.value)
        if (w === 0 || h === 0) return ''
        const line = ' '.repeat(w)
        return Array.from({ length: h }, () => line).join('\n')
      })()

      const nodes = [
        h(TText, {
          key: 'panel-top-border',
          x: ctx.panelX.value,
          y: ctx.panelTopY.value - 1,
          w: ctx.panelW.value,
          value: topBorder,
          style: borderStyle,
          zIndex: -20,
        }),
        h(TText, {
          key: 'panel-fill',
          x: ctx.panelX.value + 1,
          y: ctx.panelTopY.value,
          w: Math.max(0, ctx.panelW.value - 2),
          h: ctx.panelH.value,
          value: panelFill,
          style: panelFillStyle,
          zIndex: -20,
        }),
        h(TText, {
          key: 'panel-left-border',
          x: ctx.panelX.value,
          y: ctx.panelTopY.value,
          w: 1,
          h: innerH,
          value: sideBorderLeft,
          style: borderStyle,
          zIndex: -19,
        }),
        h(TText, {
          key: 'panel-right-border',
          x: ctx.panelX.value + ctx.panelW.value - 1,
          y: ctx.panelTopY.value,
          w: 1,
          h: innerH,
          value: sideBorderRight,
          style: borderStyle,
          zIndex: -19,
        }),
        h(TText, {
          key: 'panel-bottom-border',
          x: ctx.panelX.value,
          y: ctx.panelTopY.value + ctx.panelH.value,
          w: ctx.panelW.value,
          value: bottomBorder,
          style: borderStyle,
          zIndex: -20,
        }),
        // Make the whole panel clickable to focus the input (bigger hitbox than TInput itself).
        h(TView, {
          key: 'panel-hit',
          x: ctx.panelX.value,
          y: ctx.panelTopY.value - 1,
          w: ctx.panelW.value,
          h: ctx.panelH.value + 2,
          zIndex: 10,
          focusable: false,
          onPointerdownCapture: () => {
            if (ctx.anyDialogOpen.value) return
            ctx.focusBrowserTextarea()
            if (hasPendingApproval()) {
              ctx.focusMode.value = 'messages'
              scheduler.invalidate()
              return
            }
            ctx.focusMode.value = 'input'
            focusChatInput()
            scheduler.invalidate()
          },
        }),
      ] as any[]

      if (!pendingApproval && !askUserActive) {
        nodes.push(
          h(TInput, {
            key: 'chat-input',
            x: ctx.inputX.value - 1,
            y: ctx.panelTopY.value,
            w: ctx.inputInnerW.value,
            h: ctx.inputLines.value,
            modelValue: ctx.input.value,
            'onUpdate:modelValue': (v: string) => (ctx.input.value = v),
            cursorToEndOnExternalUpdate: true,
            autoFocus:
              ctx.focusMode.value === 'input' &&
              !inputFocused.value &&
              !ctx.anyDialogOpen.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: ctx.inputPlaceholder.value,
            placeholderWhenFocused: true,
            style: inputStyle,
            plugins: inputPlugins,
            pasteImageHandler: ctx.nodeLike
              ? () =>
                  pasteClipboardImageToTempFile({
                    sessionId: attachmentSessionId,
                  })
              : undefined,
            filePasteHandler: ctx.nodeLike
              ? (absPath: string) =>
                  cacheDroppedFileToSession(absPath, {
                    sessionId: attachmentSessionId,
                  })
              : undefined,
            promptSuggestions: ctx.promptSuggestions.value,
            skillTrigger: '$',
            skillSuggestions: ctx.skillSuggestions.value,
            skillHighlightStyle: { fg: 'cyanBright', underline: true, bold: true },
            promptAlign: 'center',
            promptSelectedStyle: promptSelectedStyle,
            mentionWorkspace: ctx.nodeLike ? ctx.pickerWorkspace.value : '',
            mentionMode: 'any',
            collectMentions: true,
            dedupeMentions: false,
            mentions: ctx.focusFiles.value,
            'onUpdate:mentions': (v: readonly string[]) =>
              (ctx.focusFiles.value = [...v]),
            onMentionClick: (absPath: string, e?: any) => {
              if (!ctx.nodeLike) return
              if (getCommitIdFromMention(absPath)) return
              // Terminals report ctrl/alt for mouse events, but not Cmd; keep metaKey for DOM renderers.
              if (e?.metaKey || e?.ctrlKey || e?.altKey)
                void openFileExternally(absPath)
            },
            collapseMultiline: true,
            multilineTexts: ctx.multilineTexts.value,
            'onUpdate:multilineTexts': (v: readonly string[]) => {
              void ctx.setMultilineTexts(v)
            },
            onMultilineClick: (idx: number) => {
              void ctx.handleMultilineClick(idx)
            },
            onChange: (v: string) => ctx.send(v),
            onKeydown: ctx.onInputKeydown,
            onValidationError: (info: any) => {
              if (info?.reason === 'paste_image_pending')
                ctx.showCopyToast(t('chatPage.toast.pasteImagePending'))
            },
            onFocus: () => {
              inputFocused.value = true
              ctx.focusMode.value = 'input'
            },
            onBlur: () => {
              inputFocused.value = false
              ctx.focusMode.value = 'messages'
            },
            zIndex: 11,
          }),
        )
      } else if (pendingApproval) {
        inputFocused.value = false
        const approval = pendingApprovalReq as any
        const requestId = String(approval?.id ?? '').trim()
        const tool =
          String(approval?.tool ?? t('chatStatus.toolFallback')).trim() ||
          t('chatStatus.toolFallback')
        const permission = String(approval?.permission ?? '').trim()
        const risk =
          String(approval?.riskLevel ?? t('chatStatus.riskFallback')).trim() ||
          t('chatStatus.riskFallback')
        const summary = `⏸ ${t('approvalDialog.title')} · ${tool}`
        const reason = stripAutoAppendedReasonDetail(
          String(approval?.reason ?? ''),
        )
        const command = extractApprovalCommand(approval?.args)
        const permissionLine = permission
          ? t('approvalDialog.permissionLabel', { permission, risk })
          : ''
        const contentRows = Math.max(1, Math.floor(ctx.panelH.value))
        const topY = ctx.panelTopY.value
        const actionY = topY + Math.max(0, contentRows - 1)
        const summaryW = Math.max(0, ctx.inputInnerW.value)

        nodes.push(
          h(TView, {
            key: 'approval-focus-anchor',
            x: ctx.inputX.value - 1,
            y: topY,
            w: Math.max(1, ctx.inputInnerW.value),
            h: contentRows,
            zIndex: 10,
            focusable: true,
            autoFocus: true,
            onFocus: () => {
              ctx.focusMode.value = 'messages'
            },
            onClick: () => {
              ctx.focusMode.value = 'messages'
            },
          }),
        )

        nodes.push(
          h(TText, {
            key: 'approval-summary',
            x: ctx.inputX.value,
            y: topY,
            w: summaryW,
            value: clipWithEllipsis(summary, summaryW),
            style: ui.style(['status.warning', 'bg.window'], { bold: true }),
            zIndex: 11,
          }),
        )

        const details: Array<{ key: string; text: string; style: any }> = []
        if (command) {
          details.push({
            key: 'approval-command',
            text: `${t('approvalDialog.commandLabel')} ${command}`,
            style: ui.style(['text.primary', 'bg.window']),
          })
        }
        if (permissionLine) {
          details.push({
            key: 'approval-permission',
            text: permissionLine,
            style: ui.style(['text.muted', 'bg.window']),
          })
        }
        if (reason) {
          details.push({
            key: 'approval-reason',
            text: reason,
            style: ui.style(['text.muted', 'bg.window']),
          })
        }
        const blockedLine = `${ctx.modeLabel.value.toUpperCase()} · ${t('chatPage.status.blockedByApproval')}`
        details.push({
          key: 'approval-blocked',
          text: blockedLine,
          style: ui.style(['status.warning', 'bg.window'], { bold: true }),
        })

        let detailY = topY + 1
        for (const detail of details) {
          if (detailY >= actionY) break
          nodes.push(
            h(TText, {
              key: detail.key,
              x: ctx.inputX.value,
              y: detailY,
              w: summaryW,
              value: clipWithEllipsis(detail.text, summaryW),
              style: detail.style,
              zIndex: 11,
            }),
          )
          detailY++
        }

        const labels = fitApprovalButtonLabels(ctx.inputInnerW.value)
        const buttons = [
          {
            label: labels.approve,
            action: 'approve' as const,
            style: ui.style(['status.success']),
          },
          {
            label: labels.always,
            action: 'alwaysApprove' as const,
            style: ui.style(['status.warning']),
          },
          {
            label: labels.deny,
            action: 'deny' as const,
            style: ui.style(['status.danger']),
          },
        ]
        const selectedIndex = Math.max(
          0,
          Math.min(
            2,
            Math.floor(Number((ctx as any)?.approvalActionIndex?.value ?? 0)),
          ),
        )
        const selectedBg =
          (ui.token('border.default') as any)?.fg ??
          (ui.token('bg.chrome') as any)?.bg ??
          (ui.style(['select.item']) as any)?.bg
        const buttonGap = ctx.inputInnerW.value < 34 ? 1 : 2
        let btnX = ctx.inputX.value
        const right = ctx.inputX.value + ctx.inputInnerW.value

        for (let i = 0; i < buttons.length; i++) {
          const button = buttons[i]!
          const text = `[ ${button.label} ]`
          const w = textCellWidth(text)
          if (btnX + w > right) break
          const focused = i === selectedIndex
          const style = focused
            ? {
                ...button.style,
                ...(selectedBg ? { bg: selectedBg } : { inverse: true }),
                bold: true,
                dim: false,
              }
            : { ...button.style, dim: true }
          nodes.push(
            h(TText, {
              key: `approval-btn-${button.action}`,
              x: btnX,
              y: actionY,
              w,
              value: text,
              style,
              zIndex: 12,
            }),
          )
          nodes.push(
            h(TView, {
              key: `approval-btn-hit-${button.action}`,
              x: btnX,
              y: actionY,
              w,
              h: 1,
              zIndex: 13,
              focusable: false,
              onClick: (e: any) => {
                e?.stopPropagation?.()
                const handler = (ctx as any)?.handleToolApprovalAction
                if (typeof handler === 'function') {
                  handler(button.action, requestId || undefined)
                  return
                }
                if (button.action === 'approve') {
                  void (ctx as any)?.store?.approve?.()
                } else if (button.action === 'alwaysApprove') {
                  if ((ctx as any)?.store?.state?.approval)
                    (ctx as any).store.state.approval.mode = 'auto'
                  void (ctx as any)?.store?.approve?.()
                } else {
                  void (ctx as any)?.store?.deny?.()
                }
              },
            }),
          )
          btnX += w + buttonGap
        }

        const hintCandidates = [
          String(t('approvalDialog.inlineActionHint') ?? ''),
          String(t('approvalDialog.inlineActionHintCompact') ?? ''),
        ].filter(Boolean)
        for (const hint of hintCandidates) {
          const hintW = textCellWidth(hint)
          if (hintW <= 0) continue
          const hintX = right - hintW
          if (hintX < btnX) continue
          nodes.push(
            h(TText, {
              key: `approval-inline-hint-${hintW}`,
              x: hintX,
              y: actionY,
              w: hintW,
              value: hint,
              style: ui.style(['text.muted', 'bg.window'], { dim: true }),
              zIndex: 11,
            }),
          )
          break
        }
      } else if (askUserActive) {
        inputFocused.value = false
        const state = askUserState as any
        const qTotal = Math.max(1, Number(state.questions?.length ?? 0))
        const qIdx = Math.max(
          0,
          Math.min(qTotal - 1, Math.floor(Number(state.currentIndex ?? 0))),
        )
        const q = state.questions[qIdx]
        const qHeader = String(q?.header ?? t('askUserDialog.defaultHeader'))
        const qTitle = t('askUserDialog.title', {
          index: qIdx + 1,
          total: qTotal,
          header: qHeader,
        })
        const qText = String(q?.question ?? '').trim()
        const qKey = String(qIdx)
        const otherLabel = t('askUserDialog.otherOption')
        const isInputQuestion = String(q?.kind ?? '') === 'input'
        const allowCustom = isInputQuestion ? true : q?.allowCustom !== false
        const optionLabels = [
          ...(Array.isArray(q?.options)
            ? q.options.map((opt: any) => String(opt?.label ?? ''))
            : []),
          ...(!isInputQuestion && allowCustom ? [otherLabel] : []),
        ].filter(Boolean)
        const selectedLabels = Array.isArray(state.selections?.[qKey])
          ? state.selections[qKey]
          : []
        const selectedSet = new Set(selectedLabels.map((v: any) => String(v)))
        const focusedRaw = Number(state.optionFocusByQuestion?.[qKey] ?? 0)
        const focusedOptionIndex = Number.isFinite(focusedRaw)
          ? Math.max(
              0,
              Math.min(
                Math.max(0, optionLabels.length - 1),
                Math.floor(focusedRaw),
              ),
            )
          : 0
        const optionsFocused = !isInputQuestion && state.focusTarget !== 'input'
        const otherOptionIndex = !isInputQuestion && allowCustom
          ? Math.max(0, optionLabels.length - 1)
          : -1
        const otherFocused = otherOptionIndex >= 0 && focusedOptionIndex === otherOptionIndex
        const draftCustom = String(ctx.input.value ?? '').trim()
        const showCustomLine = isInputQuestion || selectedSet.has(otherLabel) || otherFocused
        const showCustomInput =
          isInputQuestion ||
          state.focusTarget === 'input' ||
          selectedSet.has(otherLabel) ||
          otherFocused ||
          draftCustom.length > 0
        const isMulti = Boolean(q?.multiSelect)
        const customPlaceholder = String(
          q?.placeholder ?? t('askUserDialog.customAnswerPlaceholder'),
        )
        const handleAskUserAction = (action: any) => {
          const handler = (ctx as any)?.handleAskUserAction
          if (typeof handler === 'function') return Boolean(handler(action))
          return false
        }

        const contentRows = Math.max(1, Math.floor(ctx.panelH.value))
        const topY = ctx.panelTopY.value
        const actionY = topY + Math.max(0, contentRows - 1)
        const summaryW = Math.max(0, ctx.inputInnerW.value)
        const questionY = topY + 1
        const showCustomArea = showCustomLine || showCustomInput
        const customY = showCustomArea
          ? Math.max(questionY + 1, actionY - 1)
          : -1
        const optionsStartY = topY + 2
        const optionsEndY = showCustomArea ? customY : actionY
        const maxOptionRows = Math.max(0, optionsEndY - optionsStartY)
        const visibleRows =
          optionLabels.length > 0
            ? Math.min(maxOptionRows, optionLabels.length)
            : 0
        const windowStart =
          optionLabels.length > visibleRows
            ? Math.max(
                0,
                Math.min(
                  optionLabels.length - visibleRows,
                  focusedOptionIndex - Math.floor(visibleRows / 2),
                ),
              )
            : 0
        const windowEnd = visibleRows > 0 ? windowStart + visibleRows : 0
        const hasHiddenOptions =
          optionLabels.length > visibleRows && visibleRows > 0
        const optionsMeta = hasHiddenOptions
          ? t('askUserDialog.optionsSummaryVisibleMore', {
              count: optionLabels.length,
              start: windowStart + 1,
              end: windowEnd,
            })
          : t('askUserDialog.optionsSummaryAll', { count: optionLabels.length })
        const optionsMetaTag = isInputQuestion ? '' : `[${optionsMeta}]`
        if (optionsFocused) {
          nodes.push(
            h(TView, {
              key: 'askuser-focus-anchor',
              x: ctx.inputX.value - 1,
              y: topY,
              w: Math.max(1, ctx.inputInnerW.value),
              h: contentRows,
              zIndex: 10,
              focusable: true,
              autoFocus: true,
              onFocus: () => {
                ctx.focusMode.value = 'input'
              },
              onClick: () => {
                ctx.focusMode.value = 'input'
              },
            }),
          )
        }

        nodes.push(
          h(TText, {
            key: 'askuser-title',
            x: ctx.inputX.value,
            y: topY,
            w: summaryW,
            value: clipWithEllipsis(
              qTitle,
              Math.max(0, summaryW - textCellWidth(optionsMetaTag) - 1),
            ),
            style: ui.style(['focus.ring', 'bg.window'], { bold: true }),
            zIndex: 11,
          }),
        )
        const optionsMetaW = Math.max(
          0,
          Math.min(summaryW, textCellWidth(optionsMetaTag)),
        )
        if (!isInputQuestion && optionsMetaW > 0) {
          nodes.push(
            h(TText, {
              key: 'askuser-options-meta',
              x: ctx.inputX.value + Math.max(0, summaryW - optionsMetaW),
              y: topY,
              w: optionsMetaW,
              value: clipWithEllipsis(optionsMetaTag, optionsMetaW),
              style: ui.style(['focus.ring', 'bg.window'], { dim: true }),
              zIndex: 11,
            }),
          )
        }
        if (questionY < actionY && qText) {
          nodes.push(
            h(TText, {
              key: 'askuser-question',
              x: ctx.inputX.value,
              y: questionY,
              w: summaryW,
              value: clipWithEllipsis(qText, summaryW),
              style: ui.style(['text.primary', 'bg.window']),
              zIndex: 11,
            }),
          )
        }

        if (!isInputQuestion && maxOptionRows > 0 && optionLabels.length > 0) {
          for (let row = 0; row < visibleRows; row++) {
            const optionIndex = windowStart + row
            const label = optionLabels[optionIndex] ?? ''
            const y = optionsStartY + row
            const selected = selectedSet.has(label)
            const focused = optionsFocused && optionIndex === focusedOptionIndex
            const marker = isMulti
              ? selected
                ? '[x]'
                : '[ ]'
              : selected
                ? '(*)'
                : '( )'
            const prefix = focused ? '>' : ' '
            const lineText = clipWithEllipsis(
              `${prefix} ${marker} ${label}`,
              summaryW,
            )
            const lineStyle = focused
              ? ui.style(['select.item'], { bold: true })
              : selected
                ? ui.style(['status.success', 'bg.window'], { bold: true })
                : ui.style(['text.muted', 'bg.window'])
            nodes.push(
              h(TText, {
                key: `askuser-option-${optionIndex}`,
                x: ctx.inputX.value,
                y,
                w: summaryW,
                value: lineText,
                style: lineStyle,
                zIndex: 11,
              }),
            )
            nodes.push(
              h(TView, {
                key: `askuser-option-hit-${optionIndex}`,
                x: ctx.inputX.value,
                y,
                w: summaryW,
                h: 1,
                zIndex: 12,
                focusable: false,
                onClick: (e: any) => {
                  e?.stopPropagation?.()
                  handleAskUserAction({ type: 'option', index: optionIndex })
                  if (!isMulti) {
                    handleAskUserAction({ type: 'activate' })
                  } else if (allowCustom && label === otherLabel)
                    handleAskUserAction({ type: 'focus', target: 'input' })
                  else handleAskUserAction({ type: 'focus', target: 'options' })
                },
              }),
            )
          }
        }

        if (showCustomArea && customY < actionY) {
          const customLabel = t('askUserDialog.customAnswerLabel')
          const savedCustom = String(state.otherInputs?.[qKey] ?? '').trim()
          const customPreview = draftCustom || savedCustom
          const labelText = customPreview
            ? `${customLabel} ${customPreview}`
            : customLabel
          const labelW = Math.max(
            0,
            Math.min(summaryW, textCellWidth(customLabel)),
          )
          const inputX = ctx.inputX.value + Math.min(summaryW, labelW + 1)
          const inputW = Math.max(1, summaryW - Math.min(summaryW, labelW + 1))
          nodes.push(
            h(TText, {
              key: 'askuser-custom-label',
              x: ctx.inputX.value,
              y: customY,
              w: summaryW,
              value: clipWithEllipsis(labelText, summaryW),
              style: ui.style(['text.muted', 'bg.window']),
              zIndex: 11,
            }),
          )
          if (showCustomInput) {
            nodes.push(
              h(TInput, {
                key: `askuser-custom-input-${state.focusTarget}`,
                x: inputX,
                y: customY,
                w: inputW,
                h: 1,
                modelValue: ctx.input.value,
                'onUpdate:modelValue': (v: string) => (ctx.input.value = v),
                autoFocus:
                  state.focusTarget === 'input' && !ctx.anyDialogOpen.value,
                cursorShape: 'bar',
                cursorBlink: process.env.DIMCODE_TEST !== '1',
                placeholder: customPlaceholder,
                placeholderWhenFocused: true,
                style: inputStyle,
                onKeydown: ctx.onInputKeydown,
                onChange: () => {},
                onFocus: () => {
                  ctx.focusMode.value = 'input'
                },
                zIndex: 12,
              }),
            )
          }
        }

        const shouldShowActionButtons = !(isMulti && qTotal === 1)
        if (shouldShowActionButtons) {
          const isFirstQuestion = qIdx <= 0
          const isLastQuestion = qIdx >= qTotal - 1
          const rowEndX = ctx.inputX.value + summaryW
          const actionItems = [
            {
              key: 'prev',
              label: t('askUserDialog.inlineButtonPrev'),
              disabled: isFirstQuestion,
              style: isFirstQuestion
                ? ui.style(['text.muted', 'bg.window'], { dim: true })
                : ui.style(['focus.ring', 'bg.window'], { bold: true }),
              onClick: () =>
                handleAskUserAction({ type: 'nav', direction: 'prev' }),
            },
            {
              key: 'next',
              label: t('askUserDialog.inlineButtonNext'),
              disabled: isLastQuestion,
              style: isLastQuestion
                ? ui.style(['text.muted', 'bg.window'], { dim: true })
                : ui.style(['focus.ring', 'bg.window'], { bold: true }),
              onClick: () =>
                handleAskUserAction({ type: 'nav', direction: 'next' }),
            },
            {
              key: 'submit',
              label: t('askUserDialog.inlineButtonSubmit'),
              disabled: false,
              style: ui.style(['status.success', 'bg.window'], { bold: true }),
              onClick: () => handleAskUserAction({ type: 'submit' }),
            },
            {
              key: 'cancel',
              label: t('askUserDialog.inlineButtonCancel'),
              disabled: false,
              style: ui.style(['status.danger', 'bg.window'], { bold: true }),
              onClick: () => handleAskUserAction({ type: 'cancel' }),
            },
          ]
          const actionGap = 1
          let actionX = ctx.inputX.value
          for (const item of actionItems) {
            const text = String(item.label ?? '')
            const w = textCellWidth(text)
            if (actionX + w > rowEndX) break
            nodes.push(
              h(TText, {
                key: `askuser-action-${item.key}`,
                x: actionX,
                y: actionY,
                w,
                value: text,
                style: item.style,
                zIndex: 11,
              }),
            )
            nodes.push(
              h(TView, {
                key: `askuser-action-hit-${item.key}`,
                x: actionX,
                y: actionY,
                w,
                h: 1,
                zIndex: 12,
                focusable: false,
                onClick: (e: any) => {
                  e?.stopPropagation?.()
                  if (item.disabled) return
                  item.onClick()
                },
              }),
            )
            actionX += w + actionGap
          }

          const hintCandidates = [
            String(t('askUserDialog.inlineActionHint') ?? ''),
            String(t('askUserDialog.inlineActionHintCompact') ?? ''),
          ].filter(Boolean)
          for (const hint of hintCandidates) {
            const hintW = textCellWidth(hint)
            if (hintW <= 0) continue
            const hintX = rowEndX - hintW
            if (hintX < actionX) continue
            nodes.push(
              h(TText, {
                key: `askuser-action-hint-${hintW}`,
                x: hintX,
                y: actionY,
                w: hintW,
                value: hint,
                style: ui.style(['text.muted', 'bg.window'], { dim: true }),
                zIndex: 11,
              }),
            )
            break
          }
        }
      }

      const bannerText = pendingApproval
        ? t('chatPage.status.awaitingApprovalAction')
        : ctx.thinkingStatusDisplay.value
      if (bannerText) {
        const bannerDisplay = pendingApproval
          ? `⏸ ${bannerText}`
          : `${ctx.loadingSpinnerGlyph.value} ${bannerText}`
        const rowY = ctx.panelTopY.value - 2
        if (rowY >= 0) {
          const rowW = Math.max(0, ctx.panelW.value)
          const gapY = rowY - 1
          if (rowW > 0) {
            if (!pendingApproval && gapY >= ctx.mainY) {
              // Keep one blank separator line between transcript and loading banner.
              nodes.push(
                h(TText, {
                  key: 'panel-thinking-gap',
                  x: 0,
                  y: gapY,
                  w: ctx.cols.value,
                  value: ' '.repeat(Math.max(0, ctx.cols.value)),
                  style: ui.style(['bg.window']),
                  zIndex: 3,
                }),
              )
            }
            nodes.push(
              h(TText, {
                key: 'panel-thinking-bg',
                x: ctx.panelX.value,
                y: rowY,
                w: rowW,
                value: ' '.repeat(Math.max(0, rowW)),
                style: ui.style(['bg.window']),
                zIndex: -21,
              }),
            )
          }
          nodes.push(
            h(TText, {
              key: 'panel-thinking',
              x: ctx.inputX.value,
              y: rowY,
              w: ctx.inputInnerW.value,
              value: bannerDisplay,
              style: pendingApproval
                ? ui.style(['status.warning', 'bg.window'], { bold: true })
                : ctx.thinkingStatusStyle.value,
              zIndex: -18,
            }),
          )
        }
      }

      if (!pendingApproval && !askUserActive) {
        const queue = ctx.queueItems.value
        if (queue.length > 0) {
          const maxVisible = Math.min(SESSION_QUEUE_LIMIT, queue.length)
          const queueStateLabel = getQueueStateLabel()
          const queueLabel = `${t('chatQueue.label', { count: queue.length })} · ${queueStateLabel}`
          const labelW = textCellWidth(queueLabel) + 2
          const maxQueueW = Math.max(
            28,
            Math.floor(ctx.inputInnerW.value * 0.78),
          )
          const minQueueW = Math.min(40, maxQueueW)
          const queueW = Math.min(Math.max(minQueueW, labelW + 18), maxQueueW)
          const queueH = maxVisible + 2
          const queueX = Math.max(
            0,
            ctx.inputX.value + ctx.inputInnerW.value - queueW + 1,
          )
          const queueY = ctx.panelTopY.value - queueH - 1
          const canFloat = queueY >= ctx.mainY

          if (canFloat) {
            const topLine = (() => {
              const inner = Math.max(0, queueW - 2)
              const labelRaw = ` ${queueLabel} `
              const label = sliceByCells(labelRaw, inner)
              const fill = Math.max(0, inner - textCellWidth(label))
              return `╭${label}${'─'.repeat(fill)}╮`
            })()
            const bottomLine = `╰${'─'.repeat(Math.max(0, queueW - 2))}╯`
            const queueLines: string[] = []
            const hasMore = queue.length > maxVisible
            const steerActionLabel = t('chatQueue.item.steerAction')
            const deleteLabel = '✕'
            const deleteW = textCellWidth(deleteLabel)
            for (let i = 0; i < maxVisible; i++) {
              const item = queue[i]!
              const canDelete = !isLivePendingQueueItem(item as any)
              const steerStateLabel = getQueueSteerStateLabel(item)
              const steerLabel = steerStateLabel
                ? `${steerActionLabel} ${steerStateLabel}`
                : steerActionLabel
              const steerW = textCellWidth(steerLabel)
              const summaryW = Math.max(0, queueW - 6 - steerW - deleteW)
              const summary = ctx.summarizeQueueText(item.text, summaryW)
              const clipped = sliceByCells(summary, summaryW)
              const padded = padEndByCells(clipped, summaryW)
              queueLines.push(
                `│ ${padded} ${steerLabel} ${canDelete ? deleteLabel : ' '} │`,
              )
            }
            if (hasMore) {
              const remaining = queue.length - maxVisible
              const moreLabel = t('chatQueue.more', { count: remaining })
              const moreW = Math.max(0, queueW - 4)
              const moreText = sliceByCells(moreLabel, moreW)
              const morePad = padEndByCells(moreText, moreW)
              const moreLine = `│ ${morePad} │`
              queueLines[queueLines.length - 1] = moreLine
            }
            const overlayNodes: any[] = [
              h(TText, {
                key: 'queue-box',
                x: queueX,
                y: queueY,
                w: queueW,
                h: queueH,
                value: [topLine, ...queueLines, bottomLine].join('\n'),
                style: ui.style(['dialog.chrome']),
                zIndex: 40,
              }),
            ]
            const deleteRows = hasMore
              ? Math.max(0, maxVisible - 1)
              : maxVisible
            for (let i = 0; i < deleteRows && i < queue.length; i++) {
              const item = queue[i]!
              const canDelete = !isLivePendingQueueItem(item as any)
              const steerStateLabel = getQueueSteerStateLabel(item)
              const steerLabel = steerStateLabel
                ? `${steerActionLabel} ${steerStateLabel}`
                : steerActionLabel
              const steerActionW = textCellWidth(steerActionLabel)
              const steerStateW = textCellWidth(steerStateLabel)
              const steerW = textCellWidth(steerLabel)
              const steerX = queueX + queueW - 3 - deleteW - steerW
              const x = queueX + queueW - 2 - deleteW
              const y = queueY + 1 + i
              if (item.kind === 'steer' && steerStateLabel) {
                overlayNodes.push(
                  h(TText, {
                    key: `queue-steer-action-${item.id}`,
                    x: steerX,
                    y,
                    w: steerActionW,
                    value: steerActionLabel,
                    style: ui.style(['status.warning']),
                    zIndex: 44,
                  }),
                )
                overlayNodes.push(
                  h(TText, {
                    key: `queue-steer-state-${item.id}`,
                    x: steerX + steerActionW + 1,
                    y,
                    w: steerStateW,
                    value: steerStateLabel,
                    style: ui.style(['text.muted']),
                    zIndex: 44,
                  }),
                )
              } else {
                overlayNodes.push(
                  h(TText, {
                    key: `queue-steer-label-${item.id}`,
                    x: steerX,
                    y,
                    w: steerActionW,
                    value: steerActionLabel,
                    style: ui.style(['accent.cyan']),
                    zIndex: 44,
                  }),
                )
              }
              if (item.kind !== 'steer') {
                overlayNodes.push(
                  h(TView, {
                    key: `queue-steer-${item.id}`,
                    x: steerX,
                    y,
                    w: steerActionW,
                    h: 1,
                    zIndex: 45,
                    onClick: () => void ctx.upgradeQueueItemToSteer(item.id),
                  }),
                )
              }
              if (canDelete) {
                overlayNodes.push(
                  h(TView, {
                    key: `queue-del-${item.id}`,
                    x,
                    y,
                    w: deleteW,
                    h: 1,
                    zIndex: 45,
                    onClick: () => ctx.deleteQueueItem(item.id),
                  }),
                )
              }
            }
            nodes.push(
              h(
                TRenderLayer,
                { key: 'queue-layer', zIndex: 5 },
                () => overlayNodes,
              ),
            )
          } else {
            const rowY =
              ctx.panelTopY.value + ctx.inputLines.value + ctx.modelRowGap.value
            const compactLabel = `${t('chatQueue.labelCompact', { count: queue.length })} · ${queueStateLabel}`
            const compactLabelW = textCellWidth(compactLabel)
            nodes.push(
              h(TText, {
                key: 'queue-compact',
                x: Math.max(
                  ctx.inputX.value,
                  ctx.inputX.value + ctx.inputInnerW.value - compactLabelW,
                ),
                y: rowY,
                value: compactLabel,
                style: ui.style(['text.muted', 'bg.window']),
                zIndex: -18,
              }),
            )
          }
        }

        const rowY =
          ctx.panelTopY.value + ctx.inputLines.value + ctx.modelRowGap.value
        const ctxText = ctx.ctxPctChip.value
        const chip = ctx.modelInfo.value.build
        const ctxW = textCellWidth('● 100%')
        const chipW = textCellWidth(chip)
        const ctxGap = 2
        const maxLabelW = Math.max(
          0,
          ctx.inputInnerW.value - (ctxW + ctxGap + chipW + 1),
        )
        const label = fitModelProviderLabel(
          ctx.modelInfo.value.model,
          ctx.modelInfo.value.providerName,
          maxLabelW,
        )
        const labelW = textCellWidth(label)
        const totalW = ctxW + ctxGap + chipW + 1 + labelW
        const startX =
          ctx.inputX.value + Math.max(0, ctx.inputInnerW.value - totalW)

        const statusW = Math.max(0, startX - ctx.inputX.value - 1)
        if (statusW > 0) {
          const mode = ctx.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: ctx.inputX.value,
              y: rowY,
              w: modeW,
              value: modeTag,
              style: modeStyle,
              zIndex: -18,
            }),
          )

          const statusX = ctx.inputX.value + modeW + 1
          const statusAvail = Math.max(0, statusW - modeW - 1)
          const statusText = ctx.assistantStatusDisplay.value
          if (statusText && statusAvail > 0) {
            nodes.push(
              h(TText, {
                key: 'panel-status',
                x: statusX,
                y: rowY,
                w: statusAvail,
                value: statusText,
                style: ctx.assistantStatusStyle.value,
                zIndex: -18,
              }),
            )
          }
        }

        nodes.push(
          h(TText, {
            key: 'ctx-pct',
            x: startX,
            y: rowY,
            w: ctxW,
            value: ctxText,
            style: ctx.ctxPctStyle.value,
            zIndex: -18,
          }),
        )
        nodes.push(
          h(TText, {
            key: 'model-chip',
            x: startX + ctxW + ctxGap,
            y: rowY,
            value: chip,
            style: ui.style(['chip.meta', 'bg.window']),
            zIndex: -18,
          }),
        )
        nodes.push(
          h(TText, {
            key: 'model-label',
            x: startX + ctxW + ctxGap + chipW + 1,
            y: rowY,
            w: Math.max(
              0,
              ctx.inputX.value +
                ctx.inputInnerW.value -
                (startX + ctxW + ctxGap + chipW + 1),
            ),
            value: label,
            style: ui.style(['text.muted', 'bg.window']),
            zIndex: -18,
          }),
        )
      }

      return nodes
    }
  },
})
