'use client';

import { memo, useMemo, useState, useEffect, useRef } from 'react';
import { Box, Text, Badge, Stack } from '@tidbcloud/uikit';
import { MessagePart, TextPart, ToolPart, ReasoningPart, ImagePart, type ToolSubagentMeta } from './types';
import { useTranslation, type TranslationKey } from '@/lib/i18n';
import { MarkdownContent } from './MarkdownContent';
import { fetchWithAuth } from '@/lib/auth';

const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? '';


interface MessagePartViewProps {
  part: MessagePart;
  isStreaming?: boolean;
}

export function MessagePartView({ part, isStreaming = false }: MessagePartViewProps) {
  switch (part.type) {
    case 'tool':
      return <ToolPartView part={part} />;
    case 'reasoning':
      return <ReasoningPartView part={part} isStreaming={isStreaming} />;
    case 'text':
      return <SubagentTextPartView part={part} />;
    case 'image':
      return <ImagePartView part={part} />;
    default:
      return null;
  }
}

function SubagentTextPartView({ part }: { part: TextPart }) {
  const { t } = useTranslation();
  const subagentMeta = getPartSubagentMeta(part);
  if (!subagentMeta) return null;

  return (
    <Box
      p="sm"
      mt="xs"
      style={{
        backgroundColor: '#eff6ff',
        borderLeft: '3px solid #3b82f6',
        borderRadius: '4px',
      }}
    >
      <Text size="xs" fw={700} c="blue.7">
        {withSubagentPrefix(t('chat.subagentAnswerMessage'), subagentMeta)}
      </Text>
      <Box mt={6}>
        <ExpandablePre content={part.text.trim() || t('chat.emptyMessage')} backgroundColor="#ffffff" />
      </Box>
    </Box>
  );
}

/**
 * Groups consecutive non-text parts (tool/reasoning) and short text labels
 * into a collapsible summary bar.
 * Shows counts: running / completed. Click to expand individual items.
 */
function LastPartPreview({ part, isStreaming }: { part: MessagePart; isStreaming: boolean }) {
  const { t } = useTranslation();
  const isToolRunning = isStreaming && part.type === 'tool' && part.state.status === 'running';
  const isThinkingStreaming = isStreaming && part.type === 'reasoning' && !part.doneAt;
  const startedAt = part.type === 'tool' ? part.startedAt : part.type === 'reasoning' ? part.startedAt : undefined;
  const elapsed = useElapsedSeconds(startedAt, isToolRunning || isThinkingStreaming);

  if (part.type === 'tool') {
    const label = withSubagentPrefix(
      getToolPrimaryLabel(part.tool, part.state.title, part.state.input, t('chat.toolCallFallback')),
      getToolSubagentMeta(part),
    );
    const commandPreview = getToolCommandPreview(part.state.input);
    const icon = part.state.status === 'completed' ? '✓' : part.state.status === 'failed' ? '✗' : part.state.status === 'cancelled' ? '■' : '⚙️';
    const color = part.state.status === 'completed' ? '#10b981' : part.state.status === 'failed' ? '#ef4444' : part.state.status === 'cancelled' ? '#f59e0b' : '#3b82f6';
    const dur = isToolRunning
      ? (elapsed > 0 ? `${elapsed}s` : null)
      : part.state.duration_ms != null ? formatDuration(part.state.duration_ms) : null;
    return (
      <div style={{ marginTop: '6px', paddingLeft: '2px' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
          <Text size="xs" style={{ color, fontWeight: 600 }}>{icon}</Text>
          <Text size="xs" c="carbon.6" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {label}
          </Text>
          {dur && <Text size="xs" c="dimmed" style={{ fontFamily: 'monospace', fontVariantNumeric: 'tabular-nums', flexShrink: 0 }}>{dur}</Text>}
        </div>
        {commandPreview && (
          <Text
            size="xs"
            c="carbon.5"
            style={{
              display: 'block',
              marginTop: '2px',
              marginLeft: '18px',
              overflow: 'hidden',
              textOverflow: 'ellipsis',
              whiteSpace: 'nowrap',
              fontFamily: 'monospace',
            }}
          >
            {commandPreview}
          </Text>
        )}
      </div>
    );
  }

  if (part.type === 'reasoning') {
    const reasoningMs = part.startedAt && part.doneAt ? part.doneAt - part.startedAt : null;
    const dur = isThinkingStreaming
      ? (elapsed > 0 ? `${elapsed}s` : null)
      : (reasoningMs != null && reasoningMs >= 100) ? `${(reasoningMs / 1000).toFixed(1)}s` : null;
    const lastLine = part.text.trim().split('\n').filter(Boolean).pop() ?? '';
    const preview = lastLine.length > 80 ? '…' + lastLine.slice(lastLine.length - 80) : lastLine;
    const label = withSubagentPrefix(preview || t('chat.thinking'), getPartSubagentMeta(part));
    return (
      <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '6px', paddingLeft: '2px' }}>
        <Text size="xs" c="yellow.6">💭</Text>
        <Text size="xs" c="dimmed" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontStyle: 'italic' }}>
          {label}
        </Text>
        {dur && <Text size="xs" c="yellow.6" style={{ fontFamily: 'monospace', fontVariantNumeric: 'tabular-nums', flexShrink: 0 }}>{dur}</Text>}
      </div>
    );
  }

  if (part.type === 'text' && getPartSubagentMeta(part)) {
    const preview = part.text.trim().replace(/\s+/g, ' ');
    const label = withSubagentPrefix(t('chat.subagentAnswerMessage'), getPartSubagentMeta(part));
    return (
      <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '6px', paddingLeft: '2px' }}>
        <Text size="xs" c="blue.6" fw={700}>A</Text>
        <Text size="xs" c="carbon.6" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {label}{preview ? `: ${truncate(preview, 120)}` : ''}
        </Text>
      </div>
    );
  }

  return null;
}

interface NestedPartRow {
  part: MessagePart;
  childParts: MessagePart[];
  attemptParts: ToolPart[];
}

function getToolSubagentMeta(part: MessagePart): ToolSubagentMeta | undefined {
  if (part.type !== 'tool') return undefined;
  return part.state.meta?.['opencode/subagent'];
}

function getPartSubagentMeta(part: MessagePart): ToolSubagentMeta | undefined {
  if (part.type === 'tool') return part.state.meta?.['opencode/subagent'];
  if (part.type === 'reasoning') return part.meta?.['opencode/subagent'];
  if (part.type === 'text') return part.meta?.['opencode/subagent'];
  return undefined;
}

function getSubagentParentCallId(part: MessagePart): string | undefined {
  const parentTaskCallId = getPartSubagentMeta(part)?.parentTaskCallId;
  return parentTaskCallId && parentTaskCallId.trim() ? parentTaskCallId : undefined;
}

/**
 * Score a candidate representative for a merged subagent task container. Higher
 * wins. A row that carried child execution events is always preferred; among
 * the rest, a completed/running attempt beats a launch failure or cancellation.
 */
function subagentRepresentativeRank(row: NestedPartRow): number {
  if (row.part.type !== 'tool') return -1;
  if (row.childParts.length > 0) return 4;
  if (getSubagentResult(row.part).launchError) return 0;
  switch (row.part.state.status) {
    case 'completed':
      return 3;
    case 'running':
      return 2;
    case 'failed':
    case 'cancelled':
      return 1;
    default:
      return 1;
  }
}

function buildNestedPartRows(parts: MessagePart[]): NestedPartRow[] {
  const parentCallIds = new Set(
    parts
      .filter((part): part is ToolPart => part.type === 'tool')
      .flatMap(part => [part.callID, part.id])
      .filter(Boolean),
  );
  const childPartsByParent = new Map<string, MessagePart[]>();
  const topLevelParts: MessagePart[] = [];

  for (const part of parts) {
    const parentTaskCallId = getSubagentParentCallId(part);
    if (parentTaskCallId && parentCallIds.has(parentTaskCallId)) {
      const children = childPartsByParent.get(parentTaskCallId) ?? [];
      children.push(part);
      childPartsByParent.set(parentTaskCallId, children);
    } else {
      topLevelParts.push(part);
    }
  }

  const baseRows: NestedPartRow[] = topLevelParts.map(part => ({
    part,
    childParts: part.type === 'tool'
      ? [
        ...(childPartsByParent.get(part.callID) ?? []),
        ...(childPartsByParent.get(part.id) ?? []),
      ]
      : [],
    attemptParts: [],
  }));

  const rows: NestedPartRow[] = [];
  const subagentRowsByKey = new Map<string, NestedPartRow>();
  for (const row of baseRows) {
    const key = row.part.type === 'tool' ? getSubagentTaskGroupKey(row.part) : undefined;
    if (!key) {
      rows.push(row);
      continue;
    }

    const existing = subagentRowsByKey.get(key);
    if (!existing) {
      subagentRowsByKey.set(key, row);
      rows.push(row);
      continue;
    }

    // Merge retries of the same subagent task into one container. The
    // representative is the attempt that best reflects the real run — the one
    // carrying child events, or otherwise the most "successful" status — rather
    // than simply the last one seen (which is often a cancelled/failed retry).
    const mergedChildParts = [...existing.childParts, ...row.childParts];
    const incomingIsBetter =
      row.part.type === 'tool'
      && existing.part.type === 'tool'
      && subagentRepresentativeRank(row) > subagentRepresentativeRank(existing);

    const winner = incomingIsBetter ? row : existing;
    const loser = incomingIsBetter ? existing : row;
    const loserAttempts = [...existing.attemptParts, ...row.attemptParts];
    if (loser.part.type === 'tool') loserAttempts.push(loser.part);

    existing.part = winner.part;
    existing.childParts = mergedChildParts;
    existing.attemptParts = loserAttempts;
  }

  return rows;
}

function NestedPartView({
  row,
  isStreaming,
  workspaceId,
}: {
  row: NestedPartRow;
  isStreaming: boolean;
  workspaceId?: string;
}) {
  const isSubagentContainer =
    row.part.type === 'tool'
    && (row.childParts.length > 0 || row.attemptParts.length > 0 || isSubagentTaskLike(row.part));

  return (
    <>
      {isSubagentContainer ? (
        <Box ml={10} pl={10} style={{ borderLeft: '2px solid #bfdbfe' }}>
          <SubagentTaskContainerView
            part={row.part as ToolPart}
            childParts={row.childParts}
            attemptParts={row.attemptParts}
            workspaceId={workspaceId}
          />
        </Box>
      ) : (
        <MessagePartView part={row.part} isStreaming={isStreaming} />
      )}
      {!isSubagentContainer && row.childParts.length > 0 && (
        <Box
          mt={6}
          ml={14}
          pl={12}
          style={{
            borderLeft: '2px solid #dbeafe',
          }}
        >
          <SubagentChildExecutionHeader childParts={row.childParts} />
          {row.childParts.map(childPart => (
            <MessagePartView key={childPart.id} part={childPart} isStreaming={isStreaming} />
          ))}
        </Box>
      )}
    </>
  );
}

function ToolGroupViewComponent({
  parts,
  label,
  isStreaming = false,
  workspaceId,
}: {
  parts: MessagePart[];
  label?: string;
  isStreaming?: boolean;
  workspaceId?: string;
}) {
  const { t } = useTranslation();
  const [expanded, setExpanded] = useState(false);

  const nestedRows = useMemo(() => buildNestedPartRows(parts), [parts]);

  // Summary counts only TOP-LEVEL rows: each subagent task counts as one item,
  // its internal child events are summarized inside that container — not added
  // to the group total (which otherwise balloons to hundreds).
  const summary = useMemo(() => {
    let toolCount = 0;
    let running = 0;
    let completed = 0;
    let failed = 0;
    let cancelled = 0;
    let reasoningCount = 0;
    let messageCount = 0;
    let subagentCount = 0;
    let hasActiveReasoning = false;

    for (const row of nestedRows) {
      const part = row.part;
      const isSubagent = part.type === 'tool'
        && (row.childParts.length > 0 || row.attemptParts.length > 0 || isSubagentTaskLike(part));
      if (isSubagent) subagentCount += 1;

      if (part.type === 'tool') {
        toolCount += 1;
        if (part.state.status === 'running') running += 1;
        if (part.state.status === 'completed') completed += 1;
        if (part.state.status === 'failed') failed += 1;
        if (part.state.status === 'cancelled') cancelled += 1;
      } else if (part.type === 'reasoning') {
        reasoningCount += 1;
        if (!part.doneAt) hasActiveReasoning = true;
      } else if (part.type === 'text') {
        messageCount += 1;
      }
    }

    return { toolCount, running, completed, failed, cancelled, reasoningCount, messageCount, subagentCount, hasActiveReasoning };
  }, [nestedRows]);

  // True wall-clock span of the whole group, across every part (incl. children):
  // earliest start → latest (start + duration).
  const timing = useMemo(() => {
    let firstStart: number | undefined;
    let lastEnd: number | undefined;
    for (const part of parts) {
      const startedAt = part.type === 'tool' || part.type === 'reasoning' ? part.startedAt : undefined;
      if (startedAt == null) continue;
      firstStart = firstStart == null ? startedAt : Math.min(firstStart, startedAt);
      const dur = part.type === 'tool' ? part.state.duration_ms
        : part.type === 'reasoning' && part.doneAt ? part.doneAt - (part.startedAt ?? part.doneAt)
        : 0;
      const end = startedAt + (dur ?? 0);
      lastEnd = lastEnd == null ? end : Math.max(lastEnd, end);
    }
    return { firstStart, totalMs: firstStart != null && lastEnd != null ? lastEnd - firstStart : undefined };
  }, [parts]);

  const badges: { label: string; color: string }[] = [];
  if (summary.subagentCount > 0) badges.push({ label: t('chat.subagentSubagentCount', { count: summary.subagentCount }), color: 'violet' });
  if (summary.running > 0) badges.push({ label: t('chat.toolRunning', { count: summary.running }), color: 'blue' });
  if (summary.completed > 0) badges.push({ label: t('chat.toolCompleted', { count: summary.completed }), color: 'green' });
  if (summary.failed > 0) badges.push({ label: t('chat.toolFailed', { count: summary.failed }), color: 'red' });
  if (summary.cancelled > 0) badges.push({ label: t('chat.toolCancelled', { count: summary.cancelled }), color: 'yellow' });
  if (summary.reasoningCount > 0) badges.push({ label: t('chat.toolThinking', { count: summary.reasoningCount }), color: 'yellow' });
  if (summary.messageCount > 0) badges.push({ label: t('chat.subagentMessageCount', { count: summary.messageCount }), color: 'blue' });

  const total = summary.toolCount + summary.reasoningCount + summary.messageCount;
  const lastPart = parts[parts.length - 1];

  // Left border color: red if any failed, blue if any running, green if all completed
  const borderColor = summary.failed > 0 ? '#ef4444' : summary.running > 0 ? '#3b82f6' : summary.cancelled > 0 ? '#f59e0b' : '#10b981';

  // Total elapsed for the whole group — only active when still streaming
  const groupIsActive = isStreaming && (summary.running > 0 || summary.hasActiveReasoning);
  const groupElapsed = useElapsedSeconds(timing.firstStart, groupIsActive);

  return (
    <Box
      p="sm"
      mt="xs"
      style={{
        backgroundColor: '#f9fafb',
        borderLeft: `3px solid ${borderColor}`,
        borderRadius: '4px',
      }}
    >
      {/* Summary bar */}
      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: '8px',
          cursor: 'pointer',
          userSelect: 'none',
        }}
        onClick={() => setExpanded(!expanded)}
      >
        <Text size="xs" fw={600} style={{ color: '#1f2937' }}>
          {label || (total !== 1 ? t('chat.subtaskPlural', { count: total }) : t('chat.subtask', { count: total }))}
        </Text>
        {badges.map((b, i) => (
          <Badge key={i} size="xs" color={b.color as any} variant="light">
            {b.label}
          </Badge>
        ))}
        {groupIsActive && timing.firstStart ? (
          <Text size="xs" c="dimmed" style={{ fontFamily: 'monospace', fontVariantNumeric: 'tabular-nums' }}>
            {groupElapsed}s
          </Text>
        ) : timing.totalMs != null && timing.totalMs > 0 ? (
          <Text size="xs" c="dimmed" style={{ fontFamily: 'monospace', fontVariantNumeric: 'tabular-nums' }}>
            {formatDuration(timing.totalMs)}
          </Text>
        ) : null}
        <Text size="xs" c="carbon.5" style={{ marginLeft: 'auto', userSelect: 'none' }}>
          {expanded ? '▾' : '▸'}
        </Text>
      </div>

      {/* Collapsed: show last part as preview */}
      {!expanded && lastPart && (
        <LastPartPreview part={lastPart} isStreaming={isStreaming} />
      )}

      {/* Expanded: show individual parts */}
      {expanded && (
        <Box mt={4}>
          {nestedRows.map(row => (
            <NestedPartView key={row.part.id} row={row} isStreaming={isStreaming} workspaceId={workspaceId} />
          ))}
        </Box>
      )}
    </Box>
  );
}

export const ToolGroupView = memo(ToolGroupViewComponent);

/** Extract the display-friendly input string: prefer command, fall back to full JSON */
function formatInput(val: unknown): string {
  if (val == null) return '';
  if (typeof val === 'string') return val;
  if (typeof val === 'object' && val !== null) {
    const obj = val as Record<string, unknown>;
    if (Object.keys(obj).length === 0) return '';
    const description = getToolDescription(val);
    const command = getToolCommandPreview(val, 4000);
    if (description && command) return `${description}\n\n${command}`;
    if (command) return command;
    if (description) return description;
    // For skill tools: show name
    if (typeof obj.name === 'string') return obj.name;
  }
  try {
    return JSON.stringify(val, null, 2);
  } catch {
    return String(val);
  }
}

/** Extract the display-friendly output: prefer output string + exit code, fall back to full JSON */
function formatOutput(val: unknown): string {
  if (val == null) return '';
  if (typeof val === 'string') return val;
  if (typeof val === 'object' && val !== null) {
    const obj = val as Record<string, unknown>;
    const outputText = extractOutputText(obj);
    const meta = obj.metadata as Record<string, unknown> | undefined;
    const exitCode = meta?.exit;
    if (outputText) {
      const exitSuffix = exitCode != null && exitCode !== 0 ? `\n[exit code: ${exitCode}]` : '';
      return outputText + exitSuffix;
    }
    // API error responses
    if (typeof obj.error === 'string') return `Error: ${obj.error}`;
  }
  try {
    return JSON.stringify(val, null, 2);
  } catch {
    return String(val);
  }
}

function extractOutputText(value: unknown, depth = 0): string {
  if (typeof value === 'string') return value;
  if (!value || typeof value !== 'object' || depth > 3) return '';

  const obj = value as Record<string, unknown>;
  for (const key of ['output', 'content', 'stdout', 'stderr', 'text', 'message']) {
    const direct = obj[key];
    if (typeof direct === 'string' && direct.trim()) return direct;
    const nested = extractOutputText(direct, depth + 1);
    if (nested) return nested;
  }
  return '';
}

function valueSearchText(value: unknown, maxLen = 1600): string {
  if (value == null) return '';
  if (typeof value === 'string') return value.slice(0, maxLen);
  if (typeof value !== 'object') return String(value).slice(0, maxLen);
  try {
    return JSON.stringify(value, null, 0).slice(0, maxLen);
  } catch {
    return String(value).slice(0, maxLen);
  }
}

type SubagentName = 'evidence-agent' | 'diagnosis-analyst';

type SubagentDetailsTab = 'timeline' | 'debug';

interface SubagentTaskInfo {
  subagentName?: string;
  sessionId?: string;
  taskId?: string;
  continuation?: string;
}

function rawToolOutputText(part: ToolPart): string {
  return extractOutputText(part.state.output) || formatOutput(part.state.output);
}

/**
 * Structured view of a subagent task output. Task outputs follow the shape:
 *   `Task completed in <runtime>.\n\nAgent: <name>\n\n---\n\n<result body>`
 * followed by a `<task_metadata>` block and a "to continue:" hint.
 * Launch failures (e.g. "Skills not found: ...") have no metadata block.
 */
interface SubagentResult {
  reportedRuntime?: string;
  agentName?: string;
  body: string;
  launchError?: string;
}

function parseSubagentResult(raw: string): SubagentResult {
  const cleaned = raw
    .replace(/<task_metadata>[\s\S]*?<\/task_metadata>/gi, '')
    .replace(/\bto continue:\s*[\s\S]*$/i, '')
    .trim();

  if (!/<task_metadata>/i.test(raw)) {
    const firstLine = cleaned.split(/\r?\n/).find(line => line.trim())?.trim() ?? '';
    if (/^(skills? not found|error\b|failed\b|invalid\b|unknown (?:sub)?agent)/i.test(firstLine)) {
      return { body: cleaned, launchError: firstLine.replace(/\.?\s*Available:.*$/i, '') };
    }
  }

  const lines = cleaned.split(/\r?\n/);
  let index = 0;
  let reportedRuntime: string | undefined;
  let agentName: string | undefined;
  while (index < lines.length) {
    const line = lines[index].trim();
    if (!line) { index += 1; continue; }
    const runtimeMatch = line.match(/^task completed in\s+(.+?)\.?$/i);
    if (runtimeMatch && reportedRuntime === undefined) { reportedRuntime = runtimeMatch[1].trim(); index += 1; continue; }
    const agentMatch = line.match(/^agent:\s*(.+)$/i);
    if (agentMatch && agentName === undefined) { agentName = agentMatch[1].trim(); index += 1; continue; }
    if (/^-{3,}$/.test(line) && (reportedRuntime !== undefined || agentName !== undefined)) { index += 1; continue; }
    break;
  }

  return { reportedRuntime, agentName, body: lines.slice(index).join('\n').trim() };
}

function getSubagentResult(part: ToolPart): SubagentResult {
  return parseSubagentResult(rawToolOutputText(part));
}

const TOOL_STATUS_LABEL_KEYS: Record<ToolPart['state']['status'], TranslationKey> = {
  running: 'chat.toolStatusRunning',
  completed: 'chat.toolStatusCompleted',
  failed: 'chat.toolStatusFailed',
  cancelled: 'chat.toolStatusCancelled',
};

interface WorkspaceFileActivity {
  path: string;
  mtimeMs: number;
}

/**
 * Polls the workspace file tree while a subagent task is running and returns
 * files written since the task started. Subagents run in their own session and
 * do not stream events back, so freshly written files are the only live signal
 * of progress we can show.
 */
function useWorkspaceFileActivity(
  workspaceId: string | undefined,
  active: boolean,
  sinceMs: number | undefined,
): WorkspaceFileActivity[] {
  const [files, setFiles] = useState<WorkspaceFileActivity[]>([]);

  useEffect(() => {
    if (!active || !workspaceId) return;
    let cancelled = false;

    const load = async () => {
      try {
        const res = await fetchWithAuth(`${API_BASE_URL}/api/workspace/${encodeURIComponent(workspaceId)}/files`);
        if (!res.ok || cancelled) return;
        const data = await res.json();
        const flat: WorkspaceFileActivity[] = [];
        const walk = (nodes: unknown) => {
          if (!Array.isArray(nodes)) return;
          for (const node of nodes) {
            if (!node || typeof node !== 'object') continue;
            const n = node as Record<string, unknown>;
            if (n.type === 'directory') walk(n.children);
            else if (typeof n.mtimeMs === 'number' && typeof n.path === 'string') {
              flat.push({ path: n.path, mtimeMs: n.mtimeMs });
            }
          }
        };
        walk(data.files);
        const cutoff = sinceMs ?? Date.now() - 15 * 60_000;
        const recent = flat
          .filter(f => f.mtimeMs >= cutoff && !f.path.startsWith('.opencode'))
          .sort((a, b) => b.mtimeMs - a.mtimeMs)
          .slice(0, 5);
        if (!cancelled) setFiles(recent);
      } catch {
        // Polling is best-effort; ignore network errors.
      }
    };

    load();
    const id = setInterval(load, 5000);
    return () => {
      cancelled = true;
      clearInterval(id);
    };
  }, [workspaceId, active, sinceMs]);

  return active ? files : [];
}

function formatTimeAgo(t: (key: TranslationKey, vars?: Record<string, string | number>) => string, mtimeMs: number): string {
  const deltaSec = Math.max(0, Math.round((Date.now() - mtimeMs) / 1000));
  if (deltaSec < 60) return t('chat.timeAgoSeconds', { count: deltaSec });
  return t('chat.timeAgoMinutes', { count: Math.floor(deltaSec / 60) });
}

function SubagentLiveActivity({
  workspaceId,
  active,
  sinceMs,
}: {
  workspaceId?: string;
  active: boolean;
  sinceMs?: number;
}) {
  const { t } = useTranslation();
  const files = useWorkspaceFileActivity(workspaceId, active, sinceMs);
  if (!active) return null;

  return (
    <div style={{ border: '1px solid #dbeafe', borderRadius: '6px', background: '#ffffff', padding: '8px 10px' }}>
      <Text size="xs" c="carbon.6" style={{ overflowWrap: 'anywhere' }}>
        {t('chat.subagentRunningHint')}
      </Text>
      {files.length > 0 && (
        <Box mt={6}>
          <Text size="xs" fw={700} c="carbon.6" mb={3}>
            {t('chat.subagentLiveActivity')}
          </Text>
          {files.map(file => (
            <div key={file.path} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
              <Text size="xs" c="carbon.7" style={{ fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>
                📄 {file.path}
              </Text>
              <Text size="xs" c="dimmed" style={{ flexShrink: 0, fontVariantNumeric: 'tabular-nums' }}>
                {formatTimeAgo(t, file.mtimeMs)}
              </Text>
            </div>
          ))}
        </Box>
      )}
    </div>
  );
}

function summarizeResultBody(body: string, maxLen = 140): string {
  const firstLine = body
    .split(/\r?\n/)
    .map(line => line.trim())
    .find(line => line && line !== '{' && line !== '}' && !/^<[^>]+>$/.test(line));
  return truncate((firstLine || body).replace(/\s+/g, ' ').trim(), maxLen);
}

function SubagentResultBody({ result }: { result: SubagentResult }) {
  const { t } = useTranslation();
  const [expanded, setExpanded] = useState(false);

  if (result.launchError) {
    return (
      <div style={{ border: '1px solid #fecaca', borderRadius: '6px', background: '#fef2f2', padding: '8px 10px' }}>
        <Text size="xs" fw={700} c="red.7" mb={3}>
          {t('chat.subagentLaunchFailed')}
        </Text>
        <Text size="xs" c="red.8" style={{ overflowWrap: 'anywhere' }}>
          {result.launchError}
        </Text>
      </div>
    );
  }
  if (!result.body) return null;

  const summary = summarizeResultBody(result.body);
  return (
    <div style={{ border: '1px solid #e5e7eb', borderRadius: '6px', background: '#ffffff', padding: '8px 10px' }}>
      <div
        style={{ display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer' }}
        onClick={() => setExpanded(v => !v)}
      >
        <Text size="xs" fw={700} c="carbon.6" style={{ flexShrink: 0 }}>
          {t('chat.subagentResult')}
        </Text>
        {!expanded && (
          <Text size="xs" c="carbon.6" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {summary}
          </Text>
        )}
        <Text size="xs" c="carbon.5" style={{ marginLeft: 'auto', userSelect: 'none', flexShrink: 0 }}>
          {expanded ? '▾' : '▸'}
        </Text>
      </div>
      {expanded && (
        <div style={{ maxHeight: '320px', overflowY: 'auto', fontSize: '12px', marginTop: '6px' }}>
          <MarkdownContent content={result.body} />
        </div>
      )}
    </div>
  );
}

function parseSubagentTaskInfo(outputText: string): SubagentTaskInfo {
  const info: SubagentTaskInfo = {};
  const metadata = outputText.match(/<task_metadata>\s*([\s\S]*?)<\/task_metadata>/i)?.[1] ?? '';

  for (const line of metadata.split(/\r?\n/)) {
    const match = line.match(/^\s*([a-z_]+)\s*:\s*(.*?)\s*$/i);
    if (!match) continue;
    const key = match[1].toLowerCase();
    const value = match[2].trim();
    if (!value) continue;
    if (key === 'subagent') info.subagentName = value;
    if (key === 'session_id') info.sessionId = value;
    if (key === 'task_id') info.taskId = value;
  }

  if (!info.subagentName) {
    const match = outputText.match(/(?:subagent|agent):\s*([a-z0-9_-]+)/i);
    if (match?.[1]) info.subagentName = match[1];
  }

  const continuation = outputText.match(/to continue:\s*([^\n]+)/i)?.[1]?.trim();
  if (continuation) info.continuation = continuation;

  return info;
}

function getSubagentTaskInfo(part: ToolPart): SubagentTaskInfo {
  const meta = getToolSubagentMeta(part);
  const outputInfo = parseSubagentTaskInfo(rawToolOutputText(part));
  return {
    ...outputInfo,
    subagentName: meta?.subagentName?.trim() || outputInfo.subagentName,
    sessionId: meta?.subagentSessionId?.trim() || outputInfo.sessionId,
    taskId: meta?.parentTaskCallId?.trim() || outputInfo.taskId,
  };
}

function summarizeSubagentTaskOutput(part: ToolPart, maxLen = 520): string {
  const raw = rawToolOutputText(part);
  if (!raw.trim()) return '';
  const cleaned = raw
    .replace(/<task_metadata>[\s\S]*?<\/task_metadata>/gi, '')
    .replace(/\bto continue:\s*[\s\S]*$/i, '')
    .trim();
  if (!cleaned) return '';

  const lines = cleaned
    .split(/\r?\n/)
    .map(line => line.trim())
    .filter(Boolean)
    .filter(line => line !== '{' && line !== '}');
  if (!lines.length) return '';

  const important = lines.filter(line =>
    /^(备注|note|summary|error|failed|skills not found|evidence files saved|collecting|completed|done|root cause|根因|结论|影响|建议)/i.test(line),
  );
  const selected = important.length > 0 ? important.slice(-5) : lines.slice(-5);
  return truncate(selected.join(' · '), maxLen);
}

function detectSubagents(part: ToolPart): SubagentName[] {
  const text = [
    part.tool,
    part.state.title,
    getToolDescription(part.state.input),
    valueSearchText(part.state.input),
    valueSearchText(part.state.output),
  ].join('\n').toLowerCase();

  const names: SubagentName[] = [];
  if (text.includes('evidence-agent')) names.push('evidence-agent');
  if (text.includes('diagnosis-analyst')) names.push('diagnosis-analyst');
  return names;
}

function getSubagentNameFromPart(part: ToolPart): string | undefined {
  const metaName = getToolSubagentMeta(part)?.subagentName?.trim();
  if (metaName) return metaName;
  const taskInfoName = parseSubagentTaskInfo(rawToolOutputText(part)).subagentName?.trim();
  if (taskInfoName) return taskInfoName;
  const detected = detectSubagents(part)[0];
  if (detected) return detected;
  const outputText = formatOutput(part.state.output);
  const match = outputText.match(/(?:subagent|agent):\s*([a-z0-9_-]+)/i);
  return match?.[1];
}

function isSubagentTaskLike(part: ToolPart): boolean {
  if (part.tool.trim().toLowerCase() === 'task') return true;
  const title = (part.state.title || '').trim().toLowerCase();
  if (title === 'task') return true;
  // ACP metadata is the authoritative signal that this tool call spawned a
  // subagent. Avoid sniffing the output text for "subagent:" / agent names —
  // a plain read of a file that mentions those words must not be promoted to
  // a subagent container.
  if (getToolSubagentMeta(part)) return true;
  const outputText = formatOutput(part.state.output).toLowerCase();
  return outputText.includes('background task launched') || outputText.includes('<task_metadata>');
}

/**
 * Human-readable title for a subagent task container. Prefers the task's own
 * description, then the resolved subagent name — never the generic "task" label
 * or a file path that a fallback chain might otherwise surface.
 */
function getSubagentTaskTitle(
  part: ToolPart,
  subagentName: string | undefined,
  fallback: string,
): string {
  const description = getToolDescription(part.state.input);
  if (description) return description;
  if (subagentName) return subagentName;
  return fallback;
}

function isBackgroundSubagentLaunch(part: ToolPart): boolean {
  const outputText = formatOutput(part.state.output).toLowerCase();
  return outputText.includes('background task launched');
}

function getSubagentTaskGroupKey(part: ToolPart): string | undefined {
  if (!isSubagentTaskLike(part)) return undefined;
  const description = getToolDescription(part.state.input) || getToolPrimaryLabel(part.tool, part.state.title, part.state.input, 'task');
  const normalizedDescription = description.trim().toLowerCase().replace(/\s+/g, ' ');
  if (!normalizedDescription) return undefined;
  return `${getSubagentNameFromPart(part) || 'subagent'}:${normalizedDescription}`;
}

function getToolStatusVisuals(status: ToolPart['state']['status']) {
  return {
    borderColor: status === 'failed' ? '#ef4444' : status === 'completed' ? '#10b981' : status === 'cancelled' ? '#f59e0b' : '#3b82f6',
    statusColor: (status === 'failed' ? 'red' : status === 'completed' ? 'green' : status === 'cancelled' ? 'yellow' : 'blue') as any,
    statusIcon: status === 'failed' ? '✗' : status === 'completed' ? '✓' : status === 'cancelled' ? '■' : '⚙️',
  };
}

function summarizeParts(parts: MessagePart[]) {
  let toolCount = 0;
  let running = 0;
  let completed = 0;
  let failed = 0;
  let cancelled = 0;
  let reasoningCount = 0;
  let messageCount = 0;

  for (const part of parts) {
    if (part.type === 'tool') {
      toolCount += 1;
      if (part.state.status === 'running') running += 1;
      if (part.state.status === 'completed') completed += 1;
      if (part.state.status === 'failed') failed += 1;
      if (part.state.status === 'cancelled') cancelled += 1;
    } else if (part.type === 'reasoning') {
      reasoningCount += 1;
    } else if (part.type === 'text') {
      messageCount += 1;
    }
  }

  return { toolCount, running, completed, failed, cancelled, reasoningCount, messageCount };
}

function useElapsedSeconds(startedAt: number | undefined, active: boolean): number {
  const [elapsed, setElapsed] = useState(0);
  const startedAtRef = useRef(startedAt);
  // Only update ref when value first becomes defined, to avoid resetting the clock
  if (startedAt != null && startedAtRef.current == null) {
    startedAtRef.current = startedAt;
  }
  useEffect(() => {
    const t = startedAtRef.current;
    if (!active || !t) return;
    const tick = () => setElapsed(Math.floor((Date.now() - t) / 1000));
    tick();
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [active]);
  return elapsed;
}

function formatDuration(ms: number): string {
  return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
}

function getToolPrimaryLabel(tool: string, title: string | undefined, input: unknown, fallback: string = 'Tool call'): string {
  const normalizedTitle = (title || tool || '').trim();
  const description = getToolDescription(input);
  if (description && isGenericToolTitle(normalizedTitle)) return description;
  return normalizedTitle || description || tool || fallback;
}

/**
 * Infer a stable tool *kind* (read / write / bash / skill / glob / grep / task
 * / todo) from the call's input shape. The backend overloads the `tool`/`title`
 * fields — for a read it is the file name, for a bash it is the description — so
 * the only reliable signal of "what tool ran" is the input parameter set.
 */
function getToolKind(part: ToolPart): string {
  const tool = (part.tool || '').trim().toLowerCase();
  const input = (part.state.input && typeof part.state.input === 'object'
    ? (part.state.input as Record<string, unknown>)
    : {});
  const has = (k: string) => typeof input[k] !== 'undefined';

  if (tool === 'task') return 'task';
  if (tool === 'skill' || (has('name') && has('user_message'))) return 'skill';
  if (has('patchText') || has('patch_text')) return 'edit';
  if (has('todos')) return 'todo';
  if (has('command') || has('cmd')) return 'bash';
  if (has('content') && (has('filePath') || has('path'))) return 'write';
  if (has('filePath') || has('file_path')) return 'read';
  // grep vs glob both carry `pattern`+`path`; only the tool name or grep's
  // `include`/`output_mode` field distinguishes them.
  if (tool === 'grep' || (has('pattern') && (has('include') || has('output_mode')))) return 'grep';
  if (has('pattern')) return 'glob';
  // Known-name tools whose input we don't special-case.
  if (['read', 'write', 'edit', 'bash', 'glob', 'grep', 'apply_patch', 'todowrite', 'webfetch'].includes(tool)) {
    return tool === 'apply_patch' ? 'edit' : tool === 'todowrite' ? 'todo' : tool;
  }
  return '';
}

/**
 * The concrete operand to show after the tool kind: the file (basename), the
 * skill name, the command, the glob pattern, etc. Avoids dumping a full
 * absolute path that just repeats the title.
 */
function getToolOperand(part: ToolPart, kind: string): string {
  const input = (part.state.input && typeof part.state.input === 'object'
    ? (part.state.input as Record<string, unknown>)
    : {});
  const str = (k: string) => (typeof input[k] === 'string' ? (input[k] as string).trim() : '');

  if (kind === 'skill') return str('name');
  if (kind === 'bash') {
    const cmd = str('command') || str('cmd');
    const desc = getToolDescription(part.state.input);
    return cmd || desc;
  }
  const num = (k: string) => (typeof input[k] === 'number' ? (input[k] as number) : undefined);

  if (kind === 'read' || kind === 'write' || kind === 'edit') {
    const fp = shortenWorkspacePath(str('filePath') || str('file_path') || str('path'));
    if (kind !== 'read') return fp;
    // Surface offset/limit only when they deviate from the defaults
    // (offset 1 = file start, limit 2000 = the default read window).
    const offset = num('offset');
    const limit = num('limit');
    const parts: string[] = [];
    if (offset != null && offset > 1) parts.push(`L${offset}`);
    if (limit != null && limit !== 2000) parts.push(`${limit} lines`);
    return parts.length ? `${fp}  ·  ${parts.join(', ')}` : fp;
  }
  if (kind === 'grep' || kind === 'glob') {
    const pat = str('pattern');
    const where = shortenWorkspacePath(str('path'));
    const include = str('include');
    const meta = [where, include && `include ${include}`].filter(Boolean).join(', ');
    return meta ? `${pat}  (${meta})` : pat;
  }
  if (kind === 'todo') return '';
  if (kind === 'task') return getToolDescription(part.state.input);
  return getToolDescription(part.state.input);
}

/** Trim the workspace/abs-path prefix so only the meaningful tail shows. */
function shortenWorkspacePath(p: string): string {
  if (!p) return '';
  const wsMatch = p.match(/workspaces\/ws-[^/]+\/(.+)$/);
  if (wsMatch) return wsMatch[1];
  const parts = p.split('/').filter(Boolean);
  return parts.length > 3 ? '…/' + parts.slice(-3).join('/') : p;
}

/**
 * Compact one-line descriptor for a tool call: `<kind> <operand>`, e.g.
 * `read diagnosis-analyst.json` or `bash Collect TiDB QPS metrics`. Falls back
 * to the primary label when the kind can't be inferred.
 */
function getToolCompactLabel(part: ToolPart, t: (key: TranslationKey, vars?: Record<string, string | number>) => string): { kind: string; operand: string } {
  const kind = getToolKind(part);
  if (!kind) {
    return { kind: '', operand: getToolPrimaryLabel(part.tool, part.state.title, part.state.input, t('chat.toolCallFallback')) };
  }
  return { kind, operand: getToolOperand(part, kind) };
}

function withSubagentPrefix(label: string, subagentMeta: ToolSubagentMeta | undefined): string {
  const subagentName = subagentMeta?.subagentName?.trim();
  if (!subagentName) return label;
  return `${subagentName} / ${label}`;
}

function isGenericToolTitle(title: string): boolean {
  const normalized = title.trim().toLowerCase();
  return ['bash', 'sh', 'zsh', 'shell', 'command', 'terminal', 'run command', 'task'].includes(normalized);
}

/**
 * Concrete operand of a tool call (the file it reads, the pattern it globs,
 * etc). When present, this is what the user actually wants to see — not the
 * model's free-text `description`, which gpt-style models often fill with
 * planning narration ("我识别到这是…") shared across many adjacent calls.
 */
function getToolTarget(input: unknown): string {
  if (!input || typeof input !== 'object') return '';
  const obj = input as Record<string, unknown>;
  for (const key of ['filePath', 'file_path', 'path', 'pattern', 'url']) {
    const value = obj[key];
    if (typeof value === 'string' && value.trim()) return value.trim();
  }
  return '';
}

/**
 * `description` is meant to be a 3-5 word label, but models frequently fill it
 * with multi-sentence reasoning. Treat such text as narration and ignore it so
 * it does not become the tool's Input/label (and repeat across calls).
 */
function isNarrationText(text: string): boolean {
  const trimmed = text.trim();
  if (trimmed.length > 80) return true;
  if (/[。.!?！？\n]/.test(trimmed) && trimmed.length > 40) return true;
  // Markdown todo-list fragments (e.g. "- [pending] Write artifacts") that
  // models sometimes paste into every call's description.
  if (/^[-*]\s*\[[^\]]*\]/.test(trimmed)) return true;
  return false;
}

function getToolDescription(input: unknown): string {
  if (!input || typeof input !== 'object') return '';
  const obj = input as Record<string, unknown>;
  // If the call has a concrete operand or command, a long free-text
  // `description` is narration — don't surface it.
  const hasConcreteAction = Boolean(getToolTarget(input) || extractCommandString(input));
  for (const key of ['description', 'summary', 'reason', 'purpose', 'task', 'title', 'label']) {
    const value = obj[key];
    if (typeof value === 'string' && value.trim()) {
      if (hasConcreteAction && isNarrationText(value)) continue;
      return value.trim();
    }
  }
  return '';
}

function getToolCommandPreview(input: unknown, maxLen = 180): string {
  const command = extractCommandString(input);
  return command ? truncate(command, maxLen) : '';
}

function extractCommandString(input: unknown): string {
  if (typeof input === 'string') return input.trim();
  if (!input || typeof input !== 'object') return '';
  const obj = input as Record<string, unknown>;

  for (const key of ['command', 'cmd', 'script', 'shell_command', 'bash_command']) {
    const value = obj[key];
    if (typeof value === 'string' && value.trim()) return value.trim();
  }

  const args = obj.args;
  if (Array.isArray(args)) {
    const joined = args.filter((v): v is string => typeof v === 'string' && v.trim().length > 0).join(' ');
    if (joined) return joined;
  }

  const inputValue = obj.input;
  if (typeof inputValue === 'string' && inputValue.trim()) return inputValue.trim();

  // File/glob/fetch tools: their operand is the meaningful "command" to show.
  const target = getToolTarget(obj);
  if (target) return target;

  for (const value of Object.values(obj)) {
    if (typeof value === 'object' && value !== null) {
      const nested = extractCommandString(value);
      if (nested) return nested;
    }
  }

  return '';
}

function SubagentTaskContainerView({
  part,
  childParts,
  attemptParts,
  workspaceId,
}: {
  part: ToolPart;
  childParts: MessagePart[];
  attemptParts: ToolPart[];
  workspaceId?: string;
}) {
  const { t } = useTranslation();
  const [showPayload, setShowPayload] = useState(false);
  const [showDetails, setShowDetails] = useState(false);
  const isRunning = part.state.status === 'running';
  const elapsed = useElapsedSeconds(part.startedAt, isRunning);
  const result = useMemo(() => getSubagentResult(part), [part]);
  const effectiveStatus = result.launchError ? 'failed' : part.state.status;
  const { borderColor, statusColor, statusIcon } = getToolStatusVisuals(effectiveStatus);

  const inputStr = formatInput(part.state.input);
  const outputStr = formatOutput(part.state.output);
  const hasPayload = !!(inputStr || outputStr);
  const hasPendingBackgroundTask = isBackgroundSubagentLaunch(part) && childParts.length === 0;
  const inputPreview = inputStr ? truncate(inputStr.replace(/\s+/g, ' ').trim(), 220) : '';
  const childSummary = useMemo(() => summarizeParts(childParts), [childParts]);
  const childEventCount = childSummary.toolCount + childSummary.reasoningCount + childSummary.messageCount;
  const taskInfo = useMemo(() => getSubagentTaskInfo(part), [part]);
  const failedLaunchCount = useMemo(
    () => attemptParts.filter(attempt => Boolean(getSubagentResult(attempt).launchError)).length,
    [attemptParts],
  );
  const subagentName = taskInfo.subagentName || result.agentName;
  const label = getSubagentTaskTitle(part, subagentName, t('chat.toolCallFallback'));

  // Prefer the runtime the subagent itself reported: orchestrator-side duration_ms
  // only covers the final launch attempt's wait.
  const durationLabel = isRunning
    ? (elapsed > 0 ? `${elapsed}s` : null)
    : result.reportedRuntime
      ?? (part.state.duration_ms != null ? formatDuration(part.state.duration_ms) : null);

  return (
    <Box
      p="sm"
      mt="xs"
      style={{
        backgroundColor: '#f8fbff',
        border: '1px solid #dbeafe',
        borderLeft: `3px solid ${borderColor}`,
        borderRadius: '6px',
      }}
    >
      <Stack gap={6}>
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
          <Text size="xs" fw={700} style={{ color: '#1f2937' }}>
            {statusIcon} {label}
          </Text>
          <Badge size="xs" color={statusColor} variant="light">
            {result.launchError ? t('chat.subagentLaunchFailed') : t(TOOL_STATUS_LABEL_KEYS[part.state.status])}
          </Badge>
          {subagentName && (
            <Badge size="xs" color="violet" variant="light">
              {t('chat.subagentNameBadge', { name: subagentName })}
            </Badge>
          )}
          {childEventCount > 0 && (
            <Badge size="xs" color="blue" variant="light">
              {t('chat.subagentChildEventCount', { count: childEventCount })}
            </Badge>
          )}
          {attemptParts.length > 0 && (
            <Badge size="xs" color="yellow" variant="light">
              {t('chat.subagentAttemptCount', { count: attemptParts.length + 1 })}
            </Badge>
          )}
          {failedLaunchCount > 0 && (
            <Badge size="xs" color="red" variant="light">
              {t('chat.subagentFailedLaunchCount', { count: failedLaunchCount })}
            </Badge>
          )}
          {hasPendingBackgroundTask && (
            <Badge size="xs" color="yellow" variant="light">
              {t('chat.subagentBackgroundPending')}
            </Badge>
          )}
          {durationLabel && (
            <Text size="xs" c="dimmed" style={{ fontFamily: 'monospace', fontVariantNumeric: 'tabular-nums' }}>
              {durationLabel}
            </Text>
          )}
        </div>

        <SubagentResultBody result={result} />

        <SubagentLiveActivity
          workspaceId={workspaceId}
          active={isRunning && childParts.length === 0}
          sinceMs={attemptParts[0]?.startedAt ?? part.startedAt}
        />

        {hasPendingBackgroundTask && (
          <Text size="xs" c="yellow.7" style={{ overflowWrap: 'anywhere' }}>
            {t('chat.subagentNoChildEventsExplanation')}
          </Text>
        )}

        {inputPreview && (
          <Text
            size="xs"
            c="carbon.5"
            style={{
              fontFamily: 'monospace',
              overflow: 'hidden',
              textOverflow: 'ellipsis',
              whiteSpace: 'nowrap',
            }}
          >
            {inputPreview}
          </Text>
        )}

        <div style={{ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
          <button
            type="button"
            onClick={() => setShowDetails(v => !v)}
            style={{
              border: '1px solid #bfdbfe',
              background: '#eff6ff',
              color: '#1d4ed8',
              borderRadius: '4px',
              padding: '3px 8px',
              fontSize: '11px',
              lineHeight: '18px',
              cursor: 'pointer',
              fontFamily: 'inherit',
            }}
          >
            {showDetails ? t('chat.hideSubagentTaskDetails') : t('chat.openSubagentTaskDetails')}
          </button>

          {hasPayload && (
            <button
              type="button"
              onClick={() => setShowPayload(v => !v)}
              style={{
                border: 0,
                background: 'transparent',
                color: '#2563eb',
                cursor: 'pointer',
                padding: 0,
                fontSize: '11px',
                fontFamily: 'inherit',
              }}
            >
              {showPayload ? t('chat.hideSubagentTaskPayload') : t('chat.showSubagentTaskPayload')}
            </button>
          )}
        </div>

        {showDetails && (
          <SubagentTaskInlineDetails
            part={part}
            childParts={childParts}
            attemptParts={attemptParts}
          />
        )}

        {showPayload && (
          <Box mt={2} style={{ fontSize: '12px' }}>
            {inputStr && (
              <div style={{ marginBottom: '6px' }}>
                <Text size="xs" fw={500} c="carbon.5" mb={2}>{t('chat.toolInput')}</Text>
                <ExpandablePre content={inputStr} backgroundColor="#eef2ff" />
              </div>
            )}

            {outputStr && (
              <div style={{ marginBottom: '6px' }}>
                <Text size="xs" fw={500} c="carbon.5" mb={2}>{t('chat.toolOutput')}</Text>
                <ExpandablePre
                  content={outputStr}
                  backgroundColor={part.state.status === 'failed' ? '#fef2f2' : part.state.status === 'cancelled' ? '#fffbeb' : '#eef2ff'}
                />
              </div>
            )}
          </Box>
        )}
      </Stack>
    </Box>
  );
}

function SubagentTaskInlineDetails({
  part,
  childParts,
  attemptParts,
}: {
  part: ToolPart;
  childParts: MessagePart[];
  attemptParts: ToolPart[];
}) {
  const { t } = useTranslation();
  const [activeTab, setActiveTab] = useState<SubagentDetailsTab>('timeline');
  const isRunning = part.state.status === 'running';
  const elapsed = useElapsedSeconds(part.startedAt, isRunning);
  const result = useMemo(() => getSubagentResult(part), [part]);
  const effectiveStatus = result.launchError ? 'failed' : part.state.status;
  const { statusColor, statusIcon } = getToolStatusVisuals(effectiveStatus);
  const childSummary = useMemo(() => summarizeParts(childParts), [childParts]);
  const childEventCount = childSummary.toolCount + childSummary.reasoningCount + childSummary.messageCount;
  const taskInfo = useMemo(() => getSubagentTaskInfo(part), [part]);
  const subagentName = taskInfo.subagentName || result.agentName;
  const title = getSubagentTaskTitle(part, subagentName, t('chat.toolCallFallback'));
  const durationLabel = isRunning
    ? (elapsed > 0 ? `${elapsed}s` : null)
    : result.reportedRuntime
      ?? (part.state.duration_ms != null ? formatDuration(part.state.duration_ms) : null);
  const tabs: { key: SubagentDetailsTab; label: string }[] = [
    { key: 'timeline', label: t('chat.subagentTaskTimeline') },
    { key: 'debug', label: t('chat.subagentTaskDebug') },
  ];

  return (
    <Box
      mt={4}
      p="sm"
      style={{
        border: '1px solid #dbeafe',
        borderRadius: '6px',
        backgroundColor: '#ffffff',
      }}
    >
      <Stack gap={10}>
        <div>
          <Text size="xs" c="carbon.5" fw={600} style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>
            {t('chat.subagentTaskDetails')}
          </Text>
          <Text size="sm" fw={700} mt={3} style={{ color: '#111827', overflowWrap: 'anywhere' }}>
            {statusIcon} {title}
          </Text>
          <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap', marginTop: '8px' }}>
            <Badge size="xs" color={statusColor} variant="light">
              {result.launchError ? t('chat.subagentLaunchFailed') : t(TOOL_STATUS_LABEL_KEYS[part.state.status])}
            </Badge>
            {subagentName && (
              <Badge size="xs" color="violet" variant="light">
                {t('chat.subagentNameBadge', { name: subagentName })}
              </Badge>
            )}
            {childEventCount > 0 && (
              <Badge size="xs" color="blue" variant="light">
                {t('chat.subagentChildEventCount', { count: childEventCount })}
              </Badge>
            )}
            {attemptParts.length > 0 && (
              <Badge size="xs" color="yellow" variant="light">
                {t('chat.subagentAttemptCount', { count: attemptParts.length + 1 })}
              </Badge>
            )}
            {durationLabel && (
              <Text size="xs" c="dimmed" style={{ fontFamily: 'monospace', fontVariantNumeric: 'tabular-nums' }}>
                {durationLabel}
              </Text>
            )}
          </div>
          <Box mt={8}>
            <SubagentResultBody result={result} />
          </Box>
        </div>

        <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
          {tabs.map(tab => (
            <button
              key={tab.key}
              type="button"
              onClick={() => setActiveTab(tab.key)}
              style={{
                border: '1px solid #cbd5e1',
                background: activeTab === tab.key ? '#2563eb' : '#ffffff',
                color: activeTab === tab.key ? '#ffffff' : '#334155',
                borderRadius: '4px',
                padding: '4px 9px',
                fontSize: '12px',
                cursor: 'pointer',
                fontFamily: 'inherit',
              }}
            >
              {tab.label}
            </button>
          ))}
        </div>

        <div>
          {activeTab === 'timeline' && (
            <SubagentTaskTimeline taskPart={part} attemptParts={attemptParts} childParts={childParts} />
          )}
          {activeTab === 'debug' && <SubagentTaskDebug part={part} childParts={childParts} attemptParts={attemptParts} />}
        </div>
      </Stack>
    </Box>
  );
}

function SubagentTaskTimeline({
  taskPart,
  attemptParts,
  childParts,
}: {
  taskPart: ToolPart;
  attemptParts: ToolPart[];
  childParts: MessagePart[];
}) {
  const { t } = useTranslation();
  const summary = useMemo(() => summarizeParts(childParts), [childParts]);
  const launchParts: MessagePart[] = [...attemptParts, taskPart];

  return (
    <Stack gap={10}>
      <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
        <Text size="sm" fw={700} style={{ color: '#111827' }}>
          {t('chat.subagentTaskTimeline')}
        </Text>
        {summary.running > 0 && <Badge size="xs" color="blue" variant="light">{t('chat.toolRunning', { count: summary.running })}</Badge>}
        {summary.completed > 0 && <Badge size="xs" color="green" variant="light">{t('chat.toolCompleted', { count: summary.completed })}</Badge>}
        {summary.failed > 0 && <Badge size="xs" color="red" variant="light">{t('chat.toolFailed', { count: summary.failed })}</Badge>}
        {summary.cancelled > 0 && <Badge size="xs" color="yellow" variant="light">{t('chat.toolCancelled', { count: summary.cancelled })}</Badge>}
        {summary.reasoningCount > 0 && <Badge size="xs" color="yellow" variant="light">{t('chat.toolThinking', { count: summary.reasoningCount })}</Badge>}
        {summary.messageCount > 0 && <Badge size="xs" color="blue" variant="light">{t('chat.subagentMessageCount', { count: summary.messageCount })}</Badge>}
      </div>

      <Text size="xs" fw={700} c="carbon.6">
        {t('chat.subagentLaunchAttempts')}
      </Text>
      {launchParts.map((launchPart, index) => (
        <SubagentTimelineItem key={launchPart.id} part={launchPart} index={index + 1} />
      ))}

      <Text size="xs" fw={700} c="carbon.6" mt={6}>
        {t('chat.subagentChildExecution')}
      </Text>
      {childParts.length === 0 && (
        <Text size="xs" c="carbon.5">
          {t('chat.noSubagentChildEvents')} {t('chat.subagentNoChildEventsExplanation')}
        </Text>
      )}
      {childParts.map((childPart, index) => (
        <SubagentTimelineItem key={childPart.id} part={childPart} index={launchParts.length + index + 1} />
      ))}
    </Stack>
  );
}

function SubagentTimelineItem({ part, index }: { part: MessagePart; index: number }) {
  const { t } = useTranslation();
  const [expanded, setExpanded] = useState(false);
  const isRunningTool = part.type === 'tool' && part.state.status === 'running';
  const elapsed = useElapsedSeconds(
    part.type === 'tool' || part.type === 'reasoning' ? part.startedAt : undefined,
    isRunningTool,
  );

  if (part.type === 'tool') {
    // A nested task launch keeps its rich result body; ordinary child tools
    // (skill, read, bash, …) render as one compact line like the main agent.
    const isNestedTask = part.tool.trim().toLowerCase() === 'task';
    const result = isNestedTask ? getSubagentResult(part) : undefined;
    const effectiveStatus = result?.launchError ? 'failed' : part.state.status;
    const { statusIcon, borderColor } = getToolStatusVisuals(effectiveStatus);
    const durationLabel = isRunningTool
      ? (elapsed > 0 ? `${elapsed}s` : null)
      : result?.reportedRuntime
        ?? (part.state.duration_ms != null ? formatDuration(part.state.duration_ms) : null);

    // Compact, expandable line for ordinary child tools.
    if (!isNestedTask) {
      const { kind, operand } = getToolCompactLabel(part, t);
      const inputStr = formatInput(part.state.input);
      const outputStr = formatOutput(part.state.output);
      const hasDetails = !!(inputStr || outputStr);
      return (
        <div style={{ display: 'grid', gridTemplateColumns: '20px minmax(0, 1fr)', gap: '8px', marginTop: '2px' }}>
          <Text size="xs" c="carbon.4" style={{ fontFamily: 'monospace', textAlign: 'right', lineHeight: '20px' }}>
            {index}
          </Text>
          <div style={{ minWidth: 0 }}>
            <div
              style={{ display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, cursor: hasDetails ? 'pointer' : 'default' }}
              onClick={() => hasDetails && setExpanded(v => !v)}
            >
              <Text size="xs" style={{ color: borderColor, fontWeight: 600, flexShrink: 0 }}>{statusIcon}</Text>
              {kind && <ToolKindPill kind={kind} />}
              <Text size="xs" c="carbon.7" style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                {operand}
              </Text>
              {durationLabel && (
                <Text size="xs" c="dimmed" style={{ fontFamily: 'monospace', fontVariantNumeric: 'tabular-nums', flexShrink: 0 }}>
                  {durationLabel}
                </Text>
              )}
              {hasDetails && (
                <Text size="xs" c="carbon.5" style={{ userSelect: 'none', flexShrink: 0 }}>
                  {expanded ? '▾' : '▸'}
                </Text>
              )}
            </div>
            {expanded && (
              <Box mt={4} style={{ fontSize: '12px' }}>
                {inputStr && (
                  <div style={{ marginBottom: '6px' }}>
                    <Text size="xs" fw={500} c="carbon.5" mb={2}>{t('chat.toolInput')}</Text>
                    <ExpandablePre content={inputStr} backgroundColor="#f0f0f0" />
                  </div>
                )}
                {outputStr && (
                  <div>
                    <Text size="xs" fw={500} c="carbon.5" mb={2}>{t('chat.toolOutput')}</Text>
                    <ExpandablePre
                      content={outputStr}
                      backgroundColor={part.state.status === 'failed' ? '#fef2f2' : '#f0f0f0'}
                    />
                  </div>
                )}
              </Box>
            )}
          </div>
        </div>
      );
    }
    const label = getToolPrimaryLabel(part.tool, part.state.title, part.state.input, t('chat.toolCallFallback'));

    const { statusColor } = getToolStatusVisuals(effectiveStatus);
    const taskInfo = getSubagentTaskInfo(part);
    const subagentName = taskInfo.subagentName || result?.agentName;
    return (
      <div style={{ display: 'grid', gridTemplateColumns: '28px 1fr', gap: '10px' }}>
        <div style={{ textAlign: 'right', paddingTop: '8px' }}>
          <Text size="xs" c="carbon.5" style={{ fontFamily: 'monospace' }}>
            {index}
          </Text>
        </div>
        <div style={{ border: '1px solid #e5e7eb', borderRadius: '6px', padding: '10px 12px', background: '#fff' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
            <Text size="xs" fw={700} style={{ color: '#111827' }}>
              {statusIcon} {getSubagentTaskTitle(part, subagentName, label)}
            </Text>
            <Badge size="xs" color={statusColor} variant="light">
              {result?.launchError ? t('chat.subagentLaunchFailed') : t(TOOL_STATUS_LABEL_KEYS[part.state.status])}
            </Badge>
            {subagentName && (
              <Badge size="xs" color="violet" variant="light">
                {t('chat.subagentNameBadge', { name: subagentName })}
              </Badge>
            )}
            {durationLabel && (
              <Text size="xs" c="dimmed" style={{ fontFamily: 'monospace', fontVariantNumeric: 'tabular-nums' }}>
                {durationLabel}
              </Text>
            )}
          </div>
          {result && (
            <Box mt={6}>
              <SubagentResultBody result={result} />
            </Box>
          )}
        </div>
      </div>
    );
  }

  if (part.type === 'reasoning') {
    const preview = part.text.trim().replace(/\s+/g, ' ');
    const label = t('chat.thinkingLabel');
    return (
      <div style={{ display: 'grid', gridTemplateColumns: '28px 1fr', gap: '10px' }}>
        <div style={{ textAlign: 'right', paddingTop: '8px' }}>
          <Text size="xs" c="carbon.5" style={{ fontFamily: 'monospace' }}>
            {index}
          </Text>
        </div>
        <div style={{ border: '1px solid #fde68a', borderRadius: '6px', padding: '10px 12px', background: '#fffbeb' }}>
          <Text size="xs" fw={700} c="yellow.7">{label}</Text>
          <Text size="xs" c="carbon.6" mt={6} style={{ overflowWrap: 'anywhere' }}>
            {preview ? truncate(preview, 360) : t('chat.thinkingPlaceholder')}
          </Text>
        </div>
      </div>
    );
  }

  if (part.type === 'text') {
    const label = t('chat.subagentAnswerMessage');
    const bodyText = part.text.trim();
    return (
      <div style={{ display: 'grid', gridTemplateColumns: '28px 1fr', gap: '10px' }}>
        <div style={{ textAlign: 'right', paddingTop: '8px' }}>
          <Text size="xs" c="carbon.5" style={{ fontFamily: 'monospace' }}>
            {index}
          </Text>
        </div>
        <div style={{ border: '1px solid #bfdbfe', borderRadius: '6px', padding: '10px 12px', background: '#eff6ff' }}>
          <Text size="xs" fw={700} c="blue.7">{label}</Text>
          <Box mt={6}>
            <ExpandablePre content={bodyText || t('chat.emptyMessage')} backgroundColor="#ffffff" />
          </Box>
        </div>
      </div>
    );
  }

  return null;
}

function SubagentTaskDebug({
  part,
  childParts,
  attemptParts,
}: {
  part: ToolPart;
  childParts: MessagePart[];
  attemptParts: ToolPart[];
}) {
  const { t } = useTranslation();
  const inputStr = formatInput(part.state.input);
  const outputStr = formatOutput(part.state.output);
  const meta = part.state.meta?.['opencode/subagent'];
  const taskInfo = getSubagentTaskInfo(part);
  const debugRows = [
    { label: t('chat.subagentTaskCallId'), value: part.callID },
    { label: t('chat.subagentParentSessionId'), value: meta?.parentSessionId },
    { label: t('chat.subagentParentTaskCallId'), value: meta?.parentTaskCallId },
    { label: t('chat.subagentSessionId'), value: meta?.subagentSessionId || taskInfo.sessionId },
    { label: t('chat.subagentName'), value: meta?.subagentName || taskInfo.subagentName },
    { label: t('chat.subagentContinuation'), value: taskInfo.continuation },
    { label: t('chat.subagentAttempts'), value: String(attemptParts.length + 1) },
    { label: t('chat.subagentChildEvents'), value: String(childParts.length) },
  ].filter(row => row.value);

  return (
    <Stack gap={12}>
      <Text size="sm" fw={700} style={{ color: '#111827' }}>
        {t('chat.subagentTaskDebug')}
      </Text>

      <div style={{ border: '1px solid #e5e7eb', borderRadius: '6px', overflow: 'hidden' }}>
        {debugRows.map(row => (
          <div key={row.label} style={{ display: 'grid', gridTemplateColumns: '180px 1fr', borderBottom: '1px solid #f3f4f6' }}>
            <Text size="xs" c="carbon.5" style={{ padding: '8px 10px', background: '#f9fafb' }}>
              {row.label}
            </Text>
            <Text size="xs" style={{ padding: '8px 10px', fontFamily: 'monospace', overflowWrap: 'anywhere' }}>
              {row.value}
            </Text>
          </div>
        ))}
      </div>

      {inputStr && (
        <div>
          <Text size="xs" fw={600} c="carbon.5" mb={4}>{t('chat.toolInput')}</Text>
          <ExpandablePre content={inputStr} backgroundColor="#f8fafc" />
        </div>
      )}

      {outputStr && (
        <div>
          <Text size="xs" fw={600} c="carbon.5" mb={4}>{t('chat.toolOutput')}</Text>
          <ExpandablePre content={outputStr} backgroundColor={part.state.status === 'failed' ? '#fef2f2' : '#f8fafc'} />
        </div>
      )}
    </Stack>
  );
}

function SubagentChildExecutionHeader({ childParts }: { childParts: MessagePart[] }) {
  const { t } = useTranslation();
  const summary = useMemo(() => summarizeParts(childParts), [childParts]);
  const badges: { label: string; color: string }[] = [];
  if (summary.running > 0) badges.push({ label: t('chat.toolRunning', { count: summary.running }), color: 'blue' });
  if (summary.completed > 0) badges.push({ label: t('chat.toolCompleted', { count: summary.completed }), color: 'green' });
  if (summary.failed > 0) badges.push({ label: t('chat.toolFailed', { count: summary.failed }), color: 'red' });
  if (summary.cancelled > 0) badges.push({ label: t('chat.toolCancelled', { count: summary.cancelled }), color: 'yellow' });
  if (summary.reasoningCount > 0) badges.push({ label: t('chat.toolThinking', { count: summary.reasoningCount }), color: 'yellow' });
  if (summary.messageCount > 0) badges.push({ label: t('chat.subagentMessageCount', { count: summary.messageCount }), color: 'blue' });

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap', marginBottom: '4px' }}>
      <Text size="xs" fw={600} c="carbon.6">
        {t('chat.subagentChildExecution')}
      </Text>
      {badges.map((badge, index) => (
        <Badge key={index} size="xs" color={badge.color as any} variant="light">
          {badge.label}
        </Badge>
      ))}
    </div>
  );
}

const TOOL_KIND_COLORS: Record<string, { bg: string; fg: string }> = {
  read: { bg: '#eff6ff', fg: '#1d4ed8' },
  write: { bg: '#ecfdf5', fg: '#047857' },
  edit: { bg: '#ecfdf5', fg: '#047857' },
  bash: { bg: '#1f2937', fg: '#e5e7eb' },
  skill: { bg: '#f5f3ff', fg: '#6d28d9' },
  glob: { bg: '#fef3c7', fg: '#92400e' },
  grep: { bg: '#fef3c7', fg: '#92400e' },
  task: { bg: '#fce7f3', fg: '#9d174d' },
  todo: { bg: '#f3f4f6', fg: '#374151' },
  webfetch: { bg: '#e0f2fe', fg: '#0369a1' },
};

function ToolKindPill({ kind }: { kind: string }) {
  const c = TOOL_KIND_COLORS[kind] ?? { bg: '#f3f4f6', fg: '#374151' };
  return (
    <span
      style={{
        flexShrink: 0,
        background: c.bg,
        color: c.fg,
        borderRadius: '4px',
        padding: '1px 6px',
        fontSize: '10px',
        fontWeight: 700,
        fontFamily: 'monospace',
        textTransform: 'lowercase',
        letterSpacing: '0.02em',
      }}
    >
      {kind}
    </span>
  );
}

function ToolPartView({ part }: { part: ToolPart }) {
  const { t } = useTranslation();
  const { tool, state } = part;
  const [expanded, setExpanded] = useState(false);
  const isRunning = state.status === 'running';
  const elapsed = useElapsedSeconds(part.startedAt, isRunning);

  const { borderColor, statusColor, statusIcon } = getToolStatusVisuals(state.status);

  const { kind, operand } = getToolCompactLabel(part, t);
  const subagentMeta = getToolSubagentMeta(part);
  const inputStr = formatInput(state.input);
  const outputStr = formatOutput(state.output);
  const hasDetails = !!(inputStr || outputStr);

  const durationLabel = isRunning
    ? (elapsed > 0 ? `${elapsed}s` : null)
    : state.duration_ms != null ? formatDuration(state.duration_ms) : null;

  return (
    <Box
      p="sm"
      mt="xs"
      style={{
        backgroundColor: '#f9fafb',
        borderLeft: `3px solid ${borderColor}`,
        borderRadius: '4px',
      }}
    >
      <Stack gap={4}>
        <div
          style={{ display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0, cursor: hasDetails ? 'pointer' : 'default' }}
          onClick={() => hasDetails && setExpanded(!expanded)}
        >
          <Text size="xs" style={{ flexShrink: 0 }}>{statusIcon}</Text>
          {kind && <ToolKindPill kind={kind} />}
          <Text size="xs" fw={600} style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {subagentMeta?.subagentName ? `${subagentMeta.subagentName} / ${operand}` : operand}
          </Text>
          {durationLabel && (
            <Text size="xs" c="dimmed" style={{ fontFamily: 'monospace', fontVariantNumeric: 'tabular-nums', flexShrink: 0 }}>
              {durationLabel}
            </Text>
          )}
          {hasDetails && (
            <Text size="xs" c="carbon.5" style={{ userSelect: 'none', flexShrink: 0 }}>
              {expanded ? '▾' : '▸'}
            </Text>
          )}
        </div>

        {expanded && (
          <Box mt={4} style={{ fontSize: '12px' }}>
            {inputStr && (
              <div style={{ marginBottom: '6px' }}>
                <Text size="xs" fw={500} c="carbon.5" mb={2}>{t('chat.toolInput')}</Text>
                <ExpandablePre content={inputStr} backgroundColor="#f0f0f0" />
              </div>
            )}

            {outputStr && (
              <div style={{ marginBottom: '6px' }}>
                <Text size="xs" fw={500} c="carbon.5" mb={2}>{t('chat.toolOutput')}</Text>
                <ExpandablePre
                  content={outputStr}
                  backgroundColor={state.status === 'failed' ? '#fef2f2' : state.status === 'cancelled' ? '#fffbeb' : '#f0f0f0'}
                />
              </div>
            )}
          </Box>
        )}
      </Stack>
    </Box>
  );
}

function ExpandablePre({
  content,
  backgroundColor,
}: {
  content: string;
  backgroundColor: string;
}) {
  const { t } = useTranslation();
  const [showFull, setShowFull] = useState(false);
  const isLong = content.length > 2000;
  const rendered = showFull || !isLong ? content : truncate(content, 2000);

  return (
    <>
      <pre style={{
        margin: 0,
        padding: '6px 8px',
        backgroundColor,
        borderRadius: '4px',
        whiteSpace: 'pre-wrap',
        overflowWrap: 'anywhere',
        fontSize: '11px',
        lineHeight: '1.4',
        maxHeight: showFull ? 'min(60vh, 720px)' : '200px',
        overflowY: 'auto',
      }}>
        {rendered}
      </pre>
      {isLong && (
        <button
          type="button"
          onClick={() => setShowFull(v => !v)}
          style={{
            marginTop: '4px',
            border: 0,
            background: 'transparent',
            color: '#2563eb',
            cursor: 'pointer',
            padding: 0,
            fontSize: '11px',
            fontFamily: 'inherit',
          }}
        >
          {showFull ? t('chat.showPreviewContent') : t('chat.showFullContent')}
        </button>
      )}
    </>
  );
}

function ReasoningPartView({ part, isStreaming: isStreamingProp = false }: { part: ReasoningPart; isStreaming?: boolean }) {
  const { t } = useTranslation();
  // Consider streaming only if parent says so AND doneAt is not set
  const isStreaming = isStreamingProp && !part.doneAt;
  const [expanded, setExpanded] = useState(isStreaming);
  const elapsed = useElapsedSeconds(part.startedAt, isStreaming);

  // Auto-expand when streaming starts, collapse when done
  useEffect(() => {
    if (isStreaming) {
      setExpanded(true);
    } else {
      setExpanded(false);
    }
  }, [isStreaming]);

  const durationMs = part.startedAt && part.doneAt ? part.doneAt - part.startedAt : null;
  const durationS = durationMs != null && durationMs >= 100 ? (durationMs / 1000).toFixed(1) : null;
  const label = withSubagentPrefix(t('chat.thinkingLabel'), getPartSubagentMeta(part));
  const bodyText = part.text || t('chat.thinkingPlaceholder');

  return (
    <Box
      p="xs"
      mt="xs"
      style={{
        backgroundColor: '#fefce8',
        borderLeft: '2px solid #eab308',
        borderRadius: '4px',
        cursor: 'pointer',
      }}
      onClick={() => setExpanded(e => !e)}
    >
      <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
        <Text size="xs" c="yellow.7" fw={500}>
          {label}
        </Text>
        {isStreaming && part.startedAt && (
          <Text size="xs" c="yellow.6" style={{ fontFamily: 'monospace', fontVariantNumeric: 'tabular-nums' }}>
            {elapsed}s
          </Text>
        )}
        {!isStreaming && durationS && (
          <Text size="xs" c="yellow.6" style={{ fontFamily: 'monospace' }}>
            {durationS}s
          </Text>
        )}
        <Text size="xs" c="carbon.5" style={{ marginLeft: 'auto', userSelect: 'none' }}>
          {expanded ? '▾' : '▸'}
        </Text>
      </div>
      {expanded && (
        <Text size="xs" c="yellow.8" mt={4} style={{ fontStyle: 'italic', whiteSpace: 'pre-wrap' }}>
          {bodyText}
        </Text>
      )}
    </Box>
  );
}

function ImagePartView({ part }: { part: ImagePart }) {
  const { t } = useTranslation();
  const { url, alt } = part;

  // Skip rendering if url is empty (prevents broken image display)
  if (!url || url.trim() === '') {
    return null;
  }

  const [isExpanded, setIsExpanded] = useState(true); // Default to expanded
  const [hasError, setHasError] = useState(false);
  const [isFullscreen, setIsFullscreen] = useState(false);

  // Construct full URL with API base and authentication token if needed
  const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? '';
  let fullUrl = url.startsWith('http') ? url : `${API_BASE_URL}${url}`;


  if (hasError) {
    return (
      <Box
        p="sm"
        mt="xs"
        style={{
          backgroundColor: '#fef2f2',
          borderLeft: '3px solid #ef4444',
          borderRadius: '4px',
        }}
      >
        <Text size="xs" c="red.6">
          {t('chat.imageLoadFailed', { alt })}
        </Text>
      </Box>
    );
  }

  return (
    <>
      <Box
        p="sm"
        mt="xs"
        style={{
          backgroundColor: '#f9fafb',
          borderLeft: '3px solid #8b5cf6',
          borderRadius: '4px',
        }}
      >
        <Stack gap={8}>
          <div
            style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}
            onClick={() => setIsExpanded(!isExpanded)}
          >
            <Text size="xs" fw={600}>
              📸 {alt}
            </Text>
            <Text size="xs" c="carbon.5" style={{ marginLeft: 'auto', userSelect: 'none' }}>
              {isExpanded ? '▾' : '▸'}
            </Text>
          </div>

          {isExpanded && (
            <Box mt={4}>
              <img
                src={fullUrl}
                alt={alt}
                onError={() => setHasError(true)}
                onClick={() => setIsFullscreen(true)}
                style={{
                  maxWidth: '100%',
                  height: 'auto',
                  borderRadius: '4px',
                  border: '1px solid #e5e7eb',
                  cursor: 'zoom-in',
                }}
              />
            </Box>
          )}
        </Stack>
      </Box>

      {/* Fullscreen modal */}
      {isFullscreen && (
        <div
          onClick={() => setIsFullscreen(false)}
          style={{
            position: 'fixed',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            backgroundColor: 'rgba(0, 0, 0, 0.9)',
            zIndex: 9999,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            cursor: 'zoom-out',
            padding: '20px',
          }}
        >
          <img
            src={fullUrl}
            alt={alt}
            style={{
              maxWidth: '100%',
              maxHeight: '100%',
              objectFit: 'contain',
              borderRadius: '4px',
            }}
          />
        </div>
      )}
    </>
  );
}

function truncate(str: string, maxLength: number): string {
  if (str.length <= maxLength) return str;
  return str.substring(0, maxLength) + '...';
}
