import { RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
import { For, Show, createComputed, createEffect, createMemo, createSignal, untrack } from "solid-js"
import { useTerminalDimensions } from "@opentui/solid"
import { useConfig } from "../config"
import { useSessionTabs } from "../context/session-tabs"
import { useData } from "../context/data"
import { useTheme, useThemes } from "../context/theme"
import {
  adaptiveSessionTabLayout,
  moveSessionTab,
  NEW_SESSION_TAB_TITLE,
  sessionTabComplete,
  seedSessionTabMotion,
  sessionTabOverflowWidth,
  type SessionTab,
  type SessionTabUnread,
} from "../context/session-tabs-model"
import { createAnimatable, spring } from "../ui/animation"
import { Locale } from "../util/locale"
import { stringWidth } from "../util/string-width"
import { TabPulse, unreadGlowIntensity } from "./tab-pulse"
import { tint } from "../theme/color"
import { SESSION_SIDEBAR_WIDTH } from "../ui/layout"
import { projectName } from "../util/project"

// A long title fades out over its last cells instead of cutting hard.
const FADE_WIDTH = 4

type ContextController = ReturnType<typeof useSessionTabs>
export type SessionTabsStatus = Omit<ReturnType<ContextController["status"]>, "unread"> & {
  unread: SessionTabUnread | undefined
}
export const EMPTY_SESSION_TAB_STATUS: SessionTabsStatus = {
  unread: undefined,
  promptPulse: 0,
  attention: false,
  busy: false,
}
export type SessionTabsController = Pick<ContextController, "tabs" | "current" | "select" | "close" | "move"> & {
  newTab?: () => boolean
  status(sessionID: string): SessionTabsStatus
}

const NEW_SESSION_TAB: SessionTab = { sessionID: "new", title: NEW_SESSION_TAB_TITLE }
const glowTextColor = (base: RGBA, glow: RGBA, index: number, width: number) =>
  tint(base, glow, 0.12 * unreadGlowIntensity(index, width))

export function SessionTabs(
  props: { controller?: SessionTabsController; animations?: boolean; orientation?: "horizontal" | "vertical" } = {},
) {
  if (props.orientation === "vertical")
    return <VerticalSessionTabs controller={props.controller} animations={props.animations} />
  return <HorizontalSessionTabs controller={props.controller} animations={props.animations} />
}

function VerticalSessionTabs(props: { controller?: SessionTabsController; animations?: boolean }) {
  const tabs = props.controller ?? useSessionTabs()
  const data = useData()
  const theme = useTheme("elevated")
  const { mode } = useThemes()
  const config = useConfig().data
  const animations = () => props.animations ?? config.animations ?? true
  const width = () => SESSION_SIDEBAR_WIDTH
  const hueStep = () => (mode() === "light" ? 800 : 200)
  const accent = () => theme.hue.accent[hueStep()]
  const activeNumber = () => theme.hue.interactive[hueStep()]
  const idleNumber = () => tint(theme.text.subdued, theme.background.default, 0.35)
  const separatorUpperPulseColor = createMemo(() => tint(theme.background.default, theme.text.default, 0.04))
  const separatorLowerPulseColor = createMemo(() => tint(theme.background.default, theme.text.default, 0.05))
  const [hovered, setHovered] = createSignal<string>()
  const [dragging, setDragging] = createSignal<string>()
  const [preview, setPreview] = createSignal<{ sessionID: string; index: number }>()
  const newTab = () => tabs.newTab?.() ?? false
  const activeID = createMemo(() => (newTab() ? NEW_SESSION_TAB.sessionID : tabs.current()))
  const ordered = createMemo(() => {
    const pending = preview()
    if (!pending) return tabs.tabs()
    return moveSessionTab(tabs.tabs(), pending.sessionID, pending.index)
  })
  const items = createMemo(() => (newTab() ? [...ordered(), NEW_SESSION_TAB] : ordered()))
  const statuses = createMemo(
    () =>
      new Map(
        items().map((tab) => {
          const status = tab === NEW_SESSION_TAB ? EMPTY_SESSION_TAB_STATUS : tabs.status(tab.sessionID)
          return [
            tab.sessionID,
            {
              ...status,
              complete: sessionTabComplete(status.unread, status.busy),
              runs: status.busy && !status.attention,
              glows:
                tab.sessionID !== activeID() && (status.attention || (!status.busy && status.unread !== undefined)),
            },
          ] as const
        }),
      ),
  )
  const itemStatus = (tab: SessionTab) => statuses().get(tab.sessionID)!
  let rail: { screenY: number } | undefined
  let scroll: ScrollBoxRenderable | undefined

  createEffect(() => {
    const pending = preview()
    if (!pending || dragging()) return
    const index = tabs.tabs().findIndex((tab) => tab.sessionID === pending.sessionID)
    if (index === -1 || index === Math.min(pending.index, tabs.tabs().length - 1)) setPreview(undefined)
  })

  createEffect(() => {
    if (!scroll) return
    const index = items().findIndex((tab) => tab.sessionID === activeID())
    if (index === -1) return
    const top = index * 3
    if (top < scroll.scrollTop) return scroll.scrollTo(top)
    if (top + 2 > scroll.scrollTop + scroll.viewport.height) {
      scroll.scrollTo(top + 2 - scroll.viewport.height)
    }
  })

  return (
    <box
      ref={(element) => (rail = element)}
      width={width()}
      height="100%"
      flexShrink={0}
      flexDirection="column"
      paddingTop={1}
      backgroundColor={theme.background.default}
    >
      <scrollbox ref={(element) => (scroll = element)} flexGrow={1} scrollbarOptions={{ visible: false }}>
        <box flexShrink={0} flexDirection="column" gap={1}>
          <For each={items()}>
            {(tab, index) => {
              const selected = () => activeID() === tab.sessionID
              const status = createMemo(() => itemStatus(tab))
              const [sweepLevel, setSweepLevel] = createSignal(0)
              const session = createMemo(() => (tab === NEW_SESSION_TAB ? undefined : data.session.get(tab.sessionID)))
              const project = createMemo(() => {
                const value = session()
                return value ? data.project.get(value.projectID) : undefined
              })
              const numberWidth = () => String(index() + 1).length + 1
              const titleWidth = () => Math.max(1, width() - numberWidth() - 2 - (hovered() === tab.sessionID ? 1 : 0))
              const title = () => tab.title ?? "Untitled session"
              const visibleTitle = createMemo(() => Locale.takeWidth(title(), titleWidth()))
              const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
              const titleFades = createMemo(() => stringWidth(title()) >= titleWidth() && titleWidth() > FADE_WIDTH)
              const detail = createMemo(() => {
                if (tab === NEW_SESSION_TAB) return Locale.takeWidth("Start a new session", titleWidth())
                const value = session()
                return Locale.takeWidth(projectName(project(), value?.location.directory) ?? "", titleWidth())
              })
              const background = createMemo(() => {
                if (selected()) return theme.background.action.primary.selected
                if (hovered() === tab.sessionID || dragging() === tab.sessionID)
                  return theme.background.action.primary.hovered
                return theme.background.default
              })
              const pulseBackground = createMemo(() => tint(theme.background.default, background(), background().a))
              const numberColor = () => {
                if (status().attention) return theme.text.feedback.warning.default
                if (status().unread === "error") return theme.text.feedback.error.default
                const base =
                  hovered() === tab.sessionID && !selected()
                    ? foreground()
                    : tint(idleNumber(), activeNumber(), Number(selected()))
                const color = tint(base, accent(), Number(complete()))
                return sweepLevel() === 0 ? color : tint(color, theme.text.default, 0.15 * sweepLevel())
              }
              const foreground = () => {
                if (hovered() === tab.sessionID) return theme.text.default
                return selected() ? theme.text.default : theme.text.subdued
              }
              const complete = () => status().complete
              const glowHue = () => {
                if (status().attention) return theme.text.feedback.warning.default
                if (status().unread === "error") return theme.text.feedback.error.default
                return accent()
              }
              const pulseColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.25))
              const glowColor = createMemo(() => tint(pulseBackground(), glowHue(), 0.45))
              const detailPulseColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.13))
              const detailGlowColor = createMemo(() => tint(pulseBackground(), glowHue(), 0.25))
              const detailColor = createMemo(() => tint(theme.text.subdued, pulseBackground(), 0.35))
              const glows = () => status().glows
              const previous = createMemo(() => items()[index() - 1])
              const previousStatus = createMemo(() => {
                const tab = previous()
                return tab
                  ? itemStatus(tab)
                  : { ...EMPTY_SESSION_TAB_STATUS, complete: false, runs: false, glows: false }
              })
              const previousGlows = () => previousStatus().glows
              const runs = () => status().runs
              const previousRuns = () => previousStatus().runs
              const previousGlowHue = () => {
                if (previousStatus().attention) return theme.text.feedback.warning.default
                if (previousStatus().unread === "error") return theme.text.feedback.error.default
                return accent()
              }
              const separatorUpperColor = createMemo(() => tint(theme.background.default, previousGlowHue(), 0.1))
              const separatorLowerColor = createMemo(() => tint(theme.background.default, glowHue(), 0.12))
              const titleColor = (index: number) => {
                const color = glows()
                  ? glowTextColor(foreground(), glowColor(), 1 + numberWidth() + index, width())
                  : foreground()
                if (!titleFades() || index < visibleTitleParts().length - FADE_WIDTH) return color
                const position = index - (visibleTitleParts().length - FADE_WIDTH)
                return tint(color, pulseBackground(), 0.2 + 0.72 * (position / Math.max(1, FADE_WIDTH - 1)))
              }
              const release = () => {
                setDragging(undefined)
                const pending = preview()
                if (pending?.sessionID === tab.sessionID) tabs.move(pending.sessionID, pending.index)
                if (tab !== NEW_SESSION_TAB) tabs.select(tab.sessionID)
              }
              return (
                <box
                  height={2}
                  width="100%"
                  position="relative"
                  flexDirection="column"
                  backgroundColor={background()}
                  onMouseOver={() => setHovered(tab.sessionID)}
                  onMouseOut={() => setHovered(undefined)}
                  onMouseDown={() => setDragging(tab.sessionID)}
                  onMouseUp={release}
                  onMouseDrag={(event) => {
                    if (!rail || tab === NEW_SESSION_TAB) return
                    const target = Math.max(
                      0,
                      Math.min(
                        tabs.tabs().length - 1,
                        Math.floor((event.y - rail.screenY - 1 + (scroll?.scrollTop ?? 0)) / 3),
                      ),
                    )
                    if (target !== index() && preview()?.index !== target)
                      setPreview({ sessionID: tab.sessionID, index: target })
                  }}
                  onMouseDragEnd={release}
                >
                  <TabPulse
                    top={-1}
                    edge="above"
                    enabled={animations()}
                    active={runs()}
                    outerActive={previousRuns()}
                    promptPulse={status().promptPulse}
                    outerPromptPulse={previousStatus().promptPulse}
                    complete={complete() && !status().attention}
                    outerComplete={previousStatus().complete && !previousStatus().attention}
                    glow={glows()}
                    outerGlow={previousGlows()}
                    breathe={status().attention}
                    outerBreathe={previousStatus().attention}
                    color={separatorLowerPulseColor()}
                    outerColor={separatorUpperPulseColor()}
                    glowColor={separatorLowerColor()}
                    outerGlowColor={separatorUpperColor()}
                    glowTail={8}
                    outerGlowTail={5}
                    completionColor={separatorLowerColor()}
                    outerCompletionColor={separatorUpperColor()}
                    backgroundColor={theme.background.default}
                  />
                  <Show when={index() === items().length - 1}>
                    <TabPulse
                      top={2}
                      edge="below"
                      enabled={animations()}
                      active={runs()}
                      outerActive={false}
                      promptPulse={status().promptPulse}
                      outerPromptPulse={0}
                      complete={complete() && !status().attention}
                      outerComplete={false}
                      glow={glows()}
                      outerGlow={false}
                      breathe={status().attention}
                      outerBreathe={false}
                      color={tint(theme.background.default, theme.text.default, 0.04)}
                      outerColor={tint(theme.background.default, theme.text.default, 0.006)}
                      glowColor={tint(theme.background.default, glowHue(), 0.1)}
                      outerGlowColor={theme.background.default}
                      glowTail={8}
                      outerGlowTail={5}
                      completionColor={tint(theme.background.default, glowHue(), 0.1)}
                      outerCompletionColor={theme.background.default}
                      backgroundColor={theme.background.default}
                    />
                  </Show>
                  <box height={1} width="100%" flexDirection="row" position="relative">
                    <TabPulse
                      enabled={animations()}
                      active={runs()}
                      promptPulse={status().promptPulse}
                      complete={complete() && !status().attention}
                      glow={glows()}
                      breathe={status().attention}
                      color={pulseColor()}
                      glowColor={glowColor()}
                      completionColor={glowColor()}
                      backgroundColor={pulseBackground()}
                      onLevel={setSweepLevel}
                    />
                    <box zIndex={1} width="100%" flexDirection="row" paddingLeft={1} paddingRight={1}>
                      <text
                        width={numberWidth()}
                        fg={numberColor()}
                        selectable={false}
                        attributes={selected() ? TextAttributes.BOLD : undefined}
                      >
                        {index() + 1}
                      </text>
                      <text
                        width={titleWidth()}
                        fg={foreground()}
                        wrapMode="none"
                        selectable={false}
                        attributes={selected() ? TextAttributes.BOLD : undefined}
                      >
                        <Show when={glows() || titleFades()} fallback={visibleTitle()}>
                          <For each={visibleTitleParts()}>
                            {(character, index) => <span style={{ fg: titleColor(index()) }}>{character}</span>}
                          </For>
                        </Show>
                      </text>
                      <text
                        position="absolute"
                        right={1}
                        zIndex={2}
                        width={1}
                        fg={theme.text.subdued}
                        selectable={false}
                        onMouseUp={(event) => {
                          if (hovered() !== tab.sessionID) return
                          event.stopPropagation()
                          tabs.close(tab === NEW_SESSION_TAB ? undefined : tab.sessionID)
                        }}
                      >
                        {hovered() === tab.sessionID ? "×" : ""}
                      </text>
                    </box>
                  </box>
                  <box height={1} width="100%" position="relative" flexDirection="row">
                    <TabPulse
                      enabled={animations()}
                      active={runs()}
                      promptPulse={status().promptPulse}
                      complete={complete() && !status().attention}
                      glow={glows()}
                      breathe={status().attention}
                      color={detailPulseColor()}
                      glowColor={detailGlowColor()}
                      glowTail={10}
                      completionColor={detailGlowColor()}
                      backgroundColor={pulseBackground()}
                    />
                    <box zIndex={1} width="100%" flexDirection="row" paddingLeft={numberWidth() + 1} paddingRight={2}>
                      <text fg={detailColor()} wrapMode="none" selectable={false}>
                        {detail()}
                      </text>
                    </box>
                  </box>
                </box>
              )
            }}
          </For>
        </box>
      </scrollbox>
    </box>
  )
}

function HorizontalSessionTabs(props: { controller?: SessionTabsController; animations?: boolean } = {}) {
  const tabs = props.controller ?? useSessionTabs()
  const dimensions = useTerminalDimensions()
  const theme = useTheme()
  const { mode } = useThemes()
  const config = useConfig().data
  const animations = () => props.animations ?? config.animations ?? true
  const [hovered, setHovered] = createSignal<string>()
  const [dragging, setDragging] = createSignal<string>()
  // A drag reorders a local preview and persists one move on release instead of writing
  // per slot crossing; the preview holds after release until the store reflects the move,
  // so the strip never flashes the pre-drag order while the write is in flight.
  const [preview, setPreview] = createSignal<{ sessionID: string; index: number }>()
  let strip: { screenX: number } | undefined
  const hueStep = () => (mode() === "light" ? 800 : 200)
  const accent = () => theme.hue.accent[hueStep()]
  const activeNumber = () => theme.hue.interactive[hueStep()]
  const idleNumber = () => tint(theme.text.subdued, theme.background.default, 0.35)
  const newTab = () => tabs.newTab?.() ?? false
  const activeID = createMemo(() => (newTab() ? NEW_SESSION_TAB.sessionID : tabs.current()))
  const ordered = createMemo(() => {
    const pending = preview()
    if (!pending) return tabs.tabs()
    return moveSessionTab(tabs.tabs(), pending.sessionID, pending.index)
  })
  const items = createMemo(() => (newTab() ? [...ordered(), NEW_SESSION_TAB] : ordered()))
  createEffect(() => {
    const pending = preview()
    if (!pending || dragging()) return
    const index = tabs.tabs().findIndex((tab) => tab.sessionID === pending.sessionID)
    if (index === -1 || index === Math.min(pending.index, tabs.tabs().length - 1)) setPreview(undefined)
  })
  const layout = createMemo((previous: ReturnType<typeof adaptiveSessionTabLayout> | undefined) =>
    adaptiveSessionTabLayout(items(), activeID(), dimensions().width, previous?.start),
  )
  const statuses = createMemo(
    () =>
      new Map(
        layout().tabs.map((tab) => {
          const status = tab === NEW_SESSION_TAB ? EMPTY_SESSION_TAB_STATUS : tabs.status(tab.sessionID)
          return [
            tab.sessionID,
            {
              ...status,
              complete: sessionTabComplete(status.unread, status.busy),
            },
          ] as const
        }),
      ),
  )
  const targets = createMemo(() => ({
    widths: layout().widths,
    selections: layout().tabs.map((tab) => Number(tab.sessionID === activeID())),
    activities: layout().tabs.map((tab) => Number(statuses().get(tab.sessionID)!.complete)),
  }))
  const motion = createAnimatable(targets(), {
    enabled: animations,
    transition: spring({ visualDuration: 0.1 }),
  })
  const identity = createMemo(() =>
    layout()
      .tabs.map((tab) => tab.sessionID)
      .join(":"),
  )
  let signature = ""
  let total = 0

  // createComputed runs before render effects, so seeded widths are visible on the first frame
  // of a membership change instead of flashing the final layout.
  createComputed(() => {
    const next = targets()
    const nextSignature = identity()
    const changed = Boolean(signature) && signature !== nextSignature
    const resized = Boolean(total) && total !== layout().total
    const previous = signature
    signature = nextSignature
    total = layout().total
    if (!changed && !resized) return motion.animate(next)
    // Identity-stable total changes are terminal resizes and still jump.
    if (!changed) return motion.jump(next)
    const seeded = seedSessionTabMotion(
      previous.split(":"),
      layout().tabs.map((tab) => tab.sessionID),
      untrack(motion.value),
      next,
    )
    if (!seeded) return motion.jump(next)
    motion.jump(seeded)
    motion.animate(next)
  })

  const activeIndex = createMemo(() => layout().tabs.findIndex((tab) => tab.sessionID === activeID()))
  const visuals = createMemo(() => {
    const current = signature === identity() && total === layout().total ? motion.value() : targets()
    const widths = current.widths.map((width) => Math.max(1, Math.round(width)))
    const active = activeIndex()
    const remainder = layout().total - widths.reduce((sum, width) => sum + width, 0)
    // Absorb only rounding slack; membership animations leave a real gap while widths grow into place.
    if (active !== -1 && Math.abs(remainder) <= layout().tabs.length) widths[active]! += remainder
    return new Map(
      layout().tabs.map((tab, index) => [
        tab.sessionID,
        {
          width: widths[index]!,
          selection: current.selections[index] ?? Number(tab.sessionID === activeID()),
          activity: current.activities[index] ?? Number(statuses().get(tab.sessionID)!.complete),
        },
      ]),
    )
  })

  // Map an absolute pointer column to the items index of the visible slot beneath it.
  const slotAt = (x: number) => {
    if (!strip) return undefined
    const stripX = x - strip.screenX
    let edge = layout().before > 0 ? sessionTabOverflowWidth(layout().before) : 0
    for (const [index, width] of layout().widths.entries()) {
      edge += width
      if (stripX < edge) return layout().before + index
    }
    return layout().before + layout().widths.length - 1
  }

  return (
    <box
      ref={(element) => (strip = element)}
      height={1}
      flexShrink={0}
      position="relative"
      flexDirection="row"
      zIndex={1}
      renderAfter={function (buffer) {
        const x = Math.max(0, this.screenX)
        const y = this.screenY + this.height
        const width = Math.min(this.width, buffer.width - x)
        if (y < 0 || y >= buffer.height || width <= 0) return
        buffer.fillRect(
          x,
          y,
          width,
          1,
          RGBA.fromValues(
            theme.background.default.r,
            theme.background.default.g,
            theme.background.default.b,
            mode() === "light" ? 0.14 : 0.28,
          ),
        )
      }}
    >
      <Show when={layout().before > 0}>
        <text width={sessionTabOverflowWidth(layout().before)} fg={theme.text.subdued} selectable={false}>
          ‹{layout().before}
        </text>
      </Show>
      <For each={layout().tabs}>
        {(tab) => {
          const selected = () => activeID() === tab.sessionID
          const status = () => statuses().get(tab.sessionID)!
          const width = () => visuals().get(tab.sessionID)?.width ?? 1
          const selection = () => visuals().get(tab.sessionID)?.selection ?? Number(selected())
          const activity = () => visuals().get(tab.sessionID)?.activity ?? Number(status().complete)
          const dragged = () => dragging() === tab.sessionID
          const background = createMemo(() => {
            const lifted = (hovered() === tab.sessionID || dragged()) && !selected()
            const base = lifted ? theme.background.action.primary.hovered : theme.background.default
            // A dragged tab lifts to full selected elevation while it is held.
            return tint(base, theme.raise(theme.background.surface.offset), dragged() ? 1 : selection())
          })
          const pulseColor = () => tint(background(), theme.text.default, 0.45)
          // The edge flash washes toward a brighter stop on the same background-to-text ramp,
          // so it reads as a lift of the pulse color rather than a different hue.
          const flashColor = () => tint(background(), theme.text.default, 0.65)
          const feedbackColor = () => {
            if (status().attention) return theme.text.feedback.warning.default
            if (status().unread === "error") return theme.text.feedback.error.default
            return undefined
          }
          const glowColor = () => feedbackColor() ?? accent()
          const glows = () => !selected() && (status().attention || (!status().busy && status().unread !== undefined))
          const title = () => tab.title ?? "Untitled session"
          const tabNumber = createMemo(() => items().findIndex((item) => item.sessionID === tab.sessionID) + 1)
          // The number cell keeps one trailing space, even for double-digit tabs.
          const numberWidth = () => String(tabNumber()).length + 1
          // Hovering reveals the close mark, so the title's right bound shifts left of it.
          const availableTitleWidth = () =>
            Math.max(1, width() - 1 - numberWidth() - (hovered() === tab.sessionID ? 2 : 0))
          const visibleTitle = createMemo(() => Locale.takeWidth(title(), availableTitleWidth()))
          const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
          const titleFades = createMemo(
            () => stringWidth(title()) >= availableTitleWidth() && availableTitleWidth() > FADE_WIDTH,
          )
          const foreground = () => {
            if (hovered() === tab.sessionID) return theme.text.default
            return tint(theme.text.subdued, theme.text.default, selection())
          }
          // Title characters sitting over the glow tinge toward its color, following the same
          // spatial falloff as the glow itself; characters beyond the tail stay neutral.
          const characterColor = (index: number) => {
            const base = foreground()
            const color = glows() ? glowTextColor(base, glowColor(), 1 + numberWidth() + index, width()) : base
            if (!titleFades() || index < visibleTitleParts().length - FADE_WIDTH) return color
            const position = index - (visibleTitleParts().length - FADE_WIDTH)
            return tint(color, background(), 0.2 + 0.72 * (position / Math.max(1, FADE_WIDTH - 1)))
          }
          // The running sweep's level under the number cell, reported by the pulse renderable.
          const [sweepLevel, setSweepLevel] = createSignal(0)
          const numberColor = () => {
            const feedback = feedbackColor()
            if (feedback) return feedback
            const base =
              hovered() === tab.sessionID && !selected()
                ? foreground()
                : tint(idleNumber(), activeNumber(), selection())
            const color = tint(base, accent(), activity())
            // The number brightens faintly as the running sweep passes beneath it.
            return sweepLevel() === 0 ? color : tint(color, theme.text.default, 0.15 * sweepLevel())
          }
          const bold = () => (selected() || dragged() ? TextAttributes.BOLD : undefined)
          const closeColor = () => tint(theme.text.subdued, theme.text.default, 0.6)
          // Releasing a drag (or a plain click) selects the tab, matching browser tab strips and
          // keeping sloppy clicks indistinguishable from clean ones.
          const release = () => {
            setDragging(undefined)
            const pending = preview()
            if (pending?.sessionID === tab.sessionID) tabs.move(pending.sessionID, pending.index)
            if (tab === NEW_SESSION_TAB) return
            tabs.select(tab.sessionID)
          }
          return (
            <box
              width={width()}
              position="relative"
              flexDirection="row"
              backgroundColor={background()}
              onMouseOver={() => setHovered(tab.sessionID)}
              onMouseOut={() => setHovered(undefined)}
              onMouseDown={() => setDragging(tab.sessionID)}
              onMouseUp={release}
              onMouseDrag={(event) => {
                if (tab === NEW_SESSION_TAB) return
                const slot = slotAt(event.x)
                if (slot !== undefined && slot !== tabNumber() - 1)
                  setPreview({ sessionID: tab.sessionID, index: slot })
              }}
              onMouseDragEnd={release}
            >
              <TabPulse
                enabled={animations()}
                active={status().busy && !status().attention}
                promptPulse={status().promptPulse}
                complete={status().complete && !status().attention}
                glow={glows()}
                breathe={status().attention}
                color={pulseColor()}
                glowColor={glowColor()}
                flashColor={flashColor()}
                completionColor={accent()}
                backgroundColor={background()}
                onLevel={setSweepLevel}
              />
              <box zIndex={1} width="100%" flexDirection="row">
                <text width={1} selectable={false}>
                  {" "}
                </text>
                <text width={numberWidth()} fg={numberColor()} selectable={false} attributes={bold()}>
                  {tabNumber()}
                </text>
                <text
                  width={availableTitleWidth()}
                  fg={foreground()}
                  wrapMode="none"
                  selectable={false}
                  attributes={bold()}
                >
                  <Show when={glows() || titleFades()} fallback={visibleTitle()}>
                    <For each={visibleTitleParts()}>
                      {(character, index) => <span style={{ fg: characterColor(index()) }}>{character}</span>}
                    </For>
                  </Show>
                </text>
                <text
                  position="absolute"
                  right={1}
                  zIndex={2}
                  width={1}
                  fg={closeColor()}
                  selectable={false}
                  onMouseUp={(event) => {
                    // The close mark only renders while hovered; without motion events a click can
                    // land here first, and must select the tab instead of closing it invisibly.
                    if (hovered() !== tab.sessionID) return
                    event.stopPropagation()
                    tabs.close(tab === NEW_SESSION_TAB ? undefined : tab.sessionID)
                  }}
                >
                  {hovered() === tab.sessionID ? "×" : ""}
                </text>
              </box>
            </box>
          )
        }}
      </For>
      <Show when={layout().after > 0}>
        <text width={sessionTabOverflowWidth(layout().after)} fg={theme.text.subdued} selectable={false}>
          {" " + layout().after}›
        </text>
      </Show>
    </box>
  )
}
