import { useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback, type CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import { LocalTask, LocalTaskComment } from '../../api';
import { ChatMessage } from './ChatMessage';
import { isThinkingComment, hasTrailingThinking } from './thinkingUtils';
import { ChatPetOverlay } from '../pets/ChatPetOverlay';
import { ContextUsageGauge } from '../detail/ContextUsageGauge';
import { groupCommentsByMovement, MovementGroupExpanded } from './MovementGroup';
import { SubtaskInlineCard } from './SubtaskInlineCard';
import RotatingTips from './RotatingTips';
import { ToolRequestApproval } from './ToolRequestApproval';
import { PackageRequestApproval } from './PackageRequestApproval';
import { DelegateLiveConsole } from './DelegateLiveConsole';
import { useJobStream } from '../../hooks/useJobStream';
import { extractStreamingField, CONTENT_FIELD } from '../../lib/streamFieldExtract';
import { supportsFieldSizing, autosizeTextarea } from '../../lib/composerAutosize';


async function toBase64(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const result = String(reader.result ?? '');
      resolve(result.includes(',') ? result.split(',')[1]! : result);
    };
    reader.onerror = () => reject(reader.error ?? new Error('file read error'));
    reader.readAsDataURL(file);
  });
}

interface ChatPaneProps {
  task: LocalTask;
  comments: LocalTaskComment[];
  onSubmit: (body: string, attachments?: Array<{ name: string; contentBase64: string }>) => Promise<void>;
  onCancel?: () => Promise<void>;
  /** Tablet: the detail tabs. Tapping one opens the detail drawer to that tab,
   *  replacing the single "詳細" button so you jump straight where you want. */
  detailTabs?: Array<{ id: string; labelKey: string }>;
  activeDetailTab?: string;
  onSelectDetailTab?: (id: string) => void;
}

export function ChatPane({ task, comments, onSubmit, onCancel, detailTabs, activeDetailTab, onSelectDetailTab }: ChatPaneProps) {
  const { t } = useTranslation('chat');
  const { t: dt } = useTranslation('detail');
  const [draft, setDraft] = useState('');
  const [attachments, setAttachments] = useState<Array<{ name: string; contentBase64: string }>>([]);
  const [submitting, setSubmitting] = useState(false);
  const [cancelling, setCancelling] = useState(false);
  const [sendError, setSendError] = useState<string | null>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const composerRef = useRef<HTMLTextAreaElement>(null);
  // Firefox など field-sizing 未対応環境だけ JS で高さ追従（判定は初回のみ）。
  const needsAutosizeFallback = useMemo(() => !supportsFieldSizing(), []);
  useEffect(() => {
    if (!needsAutosizeFallback) return;
    const el = composerRef.current;
    if (el) autosizeTextarea(el, 192); // 192px ≈ 8行 (max-h-48 と一致させる)
  }, [draft, needsAutosizeFallback]);
  // Snapshot of comments.length at submit start. We hold "submitting" until
  // (a) the new user comment is reflected in the list AND (b) the job is
  // visibly busy (= picked up by a worker). Without this, the gap between the
  // POST resolving and the worker dispatching the job lets the user fire
  // multiple sends in a row.
  const submitBaselineRef = useRef<number | null>(null);
  const submitTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const [isAtBottom, setIsAtBottom] = useState(true);
  const [newMessageCount, setNewMessageCount] = useState(0);
  const prevCommentCountRef = useRef(comments.length);

  const checkIfAtBottom = useCallback(() => {
    const el = scrollRef.current;
    if (!el) return true;
    return el.scrollHeight - el.scrollTop - el.clientHeight < 80;
  }, []);

  const scrollToBottom = useCallback(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
      setIsAtBottom(true);
      setNewMessageCount(0);
    }
  }, []);

  useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    const handler = () => setIsAtBottom(checkIfAtBottom());
    el.addEventListener('scroll', handler, { passive: true });
    return () => el.removeEventListener('scroll', handler);
  }, [checkIfAtBottom]);

  // タスクを開いたとき（ChatPane はチャット切替で remount される）は、最新の
  // メッセージが見える位置で開けるよう、初回描画前に最下部へジャンプする。
  // useLayoutEffect にすることで一番上→最下部のちらつきを避ける。以降の追従は
  // 上の comments.length 監視エフェクトが担う。
  useLayoutEffect(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    const delta = comments.length - prevCommentCountRef.current;
    prevCommentCountRef.current = comments.length;
    if (delta <= 0) return;
    if (isAtBottom) {
      requestAnimationFrame(() => {
        if (scrollRef.current) {
          scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
        }
      });
    } else {
      setNewMessageCount(prev => prev + delta);
    }
  }, [comments.length, isAtBottom]);

  const handleFiles = async (files: FileList | null) => {
    if (!files || files.length === 0) return;
    const converted = await Promise.all(
      Array.from(files).map(async f => ({ name: f.name, contentBase64: await toBase64(f) }))
    );
    setAttachments(prev => [...prev, ...converted]);
  };

  const removeAttachment = (name: string) => {
    setAttachments(prev => prev.filter(a => a.name !== name));
  };

  const releaseSubmitting = () => {
    if (submitTimeoutRef.current) {
      clearTimeout(submitTimeoutRef.current);
      submitTimeoutRef.current = null;
    }
    submitBaselineRef.current = null;
    setSubmitting(false);
  };

  const handleSubmit = async () => {
    if ((!draft.trim() && attachments.length === 0) || submitting) return;
    setSendError(null);
    setSubmitting(true);
    submitBaselineRef.current = comments.length;
    try {
      await onSubmit(draft, attachments.length > 0 ? attachments : undefined);
      setDraft('');
      setAttachments([]);
      // Hold the lock until the agent is visibly responding (see effect below).
      // Safety net: if the worker never picks the job up (queue stuck, server
      // crash, etc.), release the lock after 10s so the user isn't trapped.
      if (submitTimeoutRef.current) clearTimeout(submitTimeoutRef.current);
      submitTimeoutRef.current = setTimeout(releaseSubmitting, 10000);
    } catch (e) {
      setSendError(e instanceof Error && e.message ? e.message : t('pane.sendFailed'));
      releaseSubmitting();
    }
  };

  const handlePaste = async (e: React.ClipboardEvent) => {
    const items = e.clipboardData?.items;
    if (!items) return;
    const files: File[] = [];
    for (let i = 0; i < items.length; i++) {
      const item = items[i];
      if (item.kind === 'file') {
        const file = item.getAsFile();
        if (file) files.push(file);
      }
    }
    if (files.length === 0) return;
    e.preventDefault();
    const converted = await Promise.all(
      files.map(async f => {
        const name = f.name === 'image.png' ? `paste-${Date.now()}.png` : f.name;
        return { name, contentBase64: await toBase64(f) };
      })
    );
    setAttachments(prev => [...prev, ...converted]);
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
      e.preventDefault();
      handleSubmit();
    }
  };

  const handleCancel = async () => {
    if (!onCancel || cancelling) return;
    setCancelling(true);
    try {
      await onCancel();
    } finally {
      setCancelling(false);
    }
  };

  const jobStatus = task.latestJob?.status;
  const { promptProgress, streamingText, toolCallStream, connected, delegateStreams, llmState } = useJobStream(task.id, jobStatus);

  // Most-recent content-field tool with decoded content to show live.
  const liveToolContent = useMemo(() => {
    const entries = Object.values(toolCallStream).filter(e => e.name in CONTENT_FIELD);
    for (let k = entries.length - 1; k >= 0; k--) {
      const text = extractStreamingField(entries[k].name, entries[k].rawArgs);
      if (text) return { name: entries[k].name, text };
    }
    return null;
  }, [toolCallStream]);
  const liveToolRef = useRef<HTMLPreElement | null>(null);
  useEffect(() => {
    if (liveToolRef.current) liveToolRef.current.scrollTop = liveToolRef.current.scrollHeight;
  }, [liveToolContent?.text]);
  const isBusy = jobStatus === 'running' || jobStatus === 'dispatching' || jobStatus === 'waiting_subtasks';
  const isWaitingSubtasks = jobStatus === 'waiting_subtasks';
  const canInterject = jobStatus === 'running' || jobStatus === 'waiting_subtasks';
  const inputLocked = jobStatus === 'dispatching';
  // A job is already accepted but not yet running. The task is NOT idle: a new
  // message must fold into this pending job (server reuses it, no duplicate),
  // so the composer presents it as an addition rather than a fresh request.
  const isPending = jobStatus === 'queued' || jobStatus === 'retry';
  const hasActiveJob = isBusy || isPending;

  // Tool-request mechanism: while the agent is paused waiting for the user to
  // approve/deny a requested tool, lock the normal composer. Sending a regular
  // message here would create a SECOND job (a waiting_human reply) that runs in
  // parallel with the original once it's resumed by the approval → duplicate
  // execution. Gate on the JOB's waitReason (available immediately from the
  // task data) rather than the async tool-requests fetch, so there is no
  // unlocked gap right after the task flips to waiting_human. A disabled
  // textarea also blocks the Ctrl+Enter submit path.
  const awaitingToolApproval =
    jobStatus === 'waiting_human' && task.latestJob?.waitReason === 'tool_request';
  const composerLocked = inputLocked || awaitingToolApproval;

  // Release the submit lock once the agent is visibly responding: the new user
  // comment is reflected in the list AND the job has been picked up by a worker
  // (isBusy=true). This bridges the queued->dispatching gap where a stale
  // re-enabled send button would otherwise allow a double submit.
  useEffect(() => {
    if (!submitting) return;
    const baseline = submitBaselineRef.current;
    if (baseline === null) return;
    if (comments.length > baseline && hasActiveJob) {
      releaseSubmitting();
    }
  }, [submitting, comments.length, hasActiveJob]);

  // Clear the safety-net timeout on unmount.
  useEffect(() => {
    return () => {
      if (submitTimeoutRef.current) clearTimeout(submitTimeoutRef.current);
    };
  }, []);

  // During an active run, suppress the trailing thinking comment so the
  // live SSE preview is the single source of truth for in-flight text.
  // We keep the comment in history (MovementGroup will render it once the
  // movement completes). On SSE disconnect we keep it visible as fallback.
  const visibleComments = useMemo(() => {
    if (!isBusy) return comments;
    if (!connected) return comments; // SSE disconnect fallback
    if (!hasTrailingThinking(comments)) return comments;
    return comments.slice(0, -1);
  }, [comments, isBusy, connected]);

  const groupedItems = useMemo(() => groupCommentsByMovement(visibleComments), [visibleComments]);
  const animatingIdx = isBusy && hasTrailingThinking(visibleComments) ? visibleComments.length - 1 : -1;

  return (
    <div className="relative flex flex-col h-full overflow-hidden">
      {/* Shown at every breakpoint. The overlay is absolutely positioned at the
          bottom-right of the chat pane with pointer-events:none, so it never
          blocks the mobile composer — keep it visible on narrow screens too. */}
      <ChatPetOverlay
        taskId={task.id}
        taskStatus={task.latestJob?.status ?? null}
        currentActivity={task.latestJob?.currentActivity ?? null}
        workerId={task.latestJob?.workerId ?? null}
        lastBackendId={task.latestJob?.lastBackendId ?? null}
      />
      {/* Header */}
      <div className="flex-shrink-0 border-b border-hairline bg-canvas px-4 py-2">
        <div className="flex items-center gap-2">
          <h2 className="min-w-0 truncate text-sm font-semibold text-slate-900">{task.title}</h2>
          <span className="shrink-0 text-[10px] text-slate-400 font-mono tabular-nums">#{task.id} · {task.pieceName}</span>
          <div className="ml-auto flex items-center gap-1.5 flex-shrink-0">
            {(task.latestJob?.attempt ?? 1) > 1 && jobStatus !== 'succeeded' && (
              <div
                className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded border border-amber-200 dark:border-amber-500/30 bg-amber-50 dark:bg-amber-500/15"
                title={task.latestJob?.abortReason
                  ? t('pane.attemptBadgeTitle', { reason: task.latestJob.abortReason })
                  : undefined}
                data-testid="attempt-badge"
              >
                <span className="text-[10px] font-medium text-amber-700 dark:text-amber-300">
                  {t('pane.attemptBadge', { attempt: task.latestJob?.attempt, max: task.latestJob?.maxAttempts ?? 3 })}
                </span>
              </div>
            )}
            {isBusy && (
              <div className={`inline-flex items-center gap-1.5 px-1.5 py-0.5 rounded border ${
                isWaitingSubtasks
                  ? 'border-indigo-100 dark:border-indigo-500/30 bg-indigo-50 dark:bg-indigo-500/15'
                  : 'border-emerald-100 dark:border-emerald-500/30 bg-emerald-50 dark:bg-emerald-500/15'
              }`}>
                <span className={`w-1.5 h-1.5 rounded-full animate-pulse ${
                  isWaitingSubtasks ? 'bg-indigo-500' : 'bg-emerald-500'
                }`} />
                <span className={`text-[10px] font-medium ${
                  isWaitingSubtasks ? 'text-indigo-700 dark:text-indigo-300' : 'text-emerald-700 dark:text-emerald-300'
                }`}>
                  {isWaitingSubtasks ? 'subtasks' : 'running'}
                </span>
              </div>
            )}
          </div>
        </div>

        {/* Tablet: detail tabs as direct buttons (instead of one "詳細" button)
            so a tap opens the drawer straight to the chosen tab. */}
        {detailTabs && detailTabs.length > 0 && onSelectDetailTab && (
          <div className="mt-2 flex flex-wrap gap-x-1.5 gap-y-1">
            {detailTabs.map(tab => {
              const active = tab.id === activeDetailTab;
              const live = tab.id === 'browser' || tab.id === 'ssh';
              return (
                <button
                  key={tab.id}
                  onClick={() => onSelectDetailTab(tab.id)}
                  aria-pressed={active}
                  style={live ? ({ '--flow-color': tab.id === 'ssh' ? '#34d399' : '#38bdf8' } as CSSProperties) : undefined}
                  className={`whitespace-nowrap px-2 h-7 inline-flex items-center rounded-md text-2xs font-medium transition-colors ${
                    live ? 'live-flow-border' : 'border border-hairline'
                  } ${
                    active ? 'text-slate-900 font-semibold bg-surface' : 'text-slate-600 bg-canvas hover:bg-surface'
                  }`}
                >
                  {dt(tab.labelKey)}
                </button>
              );
            })}
          </div>
        )}
      </div>

      {/* Messages */}
      <div className="flex-1 relative min-h-0 overflow-x-hidden">
      <div ref={scrollRef} className="absolute inset-0 overflow-y-auto overflow-x-hidden p-4">
        <div className="max-w-3xl mx-auto min-w-0 flex flex-col gap-3">
          {comments.length === 0 && (
            <div className="text-center text-slate-400 text-[13px] py-8">
              {t('pane.empty')}
            </div>
          )}
          {(() => {
            let commentIdx = 0;
            return groupedItems.map((item, gi) => {
              if (item.type === 'movement') {
                const startIdx = commentIdx;
                commentIdx += item.inner.length;
                return (
                  <MovementGroupExpanded
                    key={`mg-${gi}`}
                    item={item}
                    taskId={task.id}
                    isLast={gi === groupedItems.length - 1}
                    isRunning={isBusy}
                    animatingIdx={animatingIdx}
                    startIdx={startIdx}
                  />
                );
              }
              const idx = commentIdx;
              commentIdx++;
              return (
                <ChatMessage
                  key={item.comment.id}
                  comment={item.comment}
                  taskId={task.id}
                  isStaleThinking={isThinkingComment(item.comment) && idx !== animatingIdx}
                />
              );
            });
          })()}

          {isWaitingSubtasks && task.subtasks && task.subtasks.length > 0 && (
            <SubtaskInlineCard
              subtasks={task.subtasks}
              subtaskCount={task.subtaskCount ?? task.subtasks.length}
              subtaskCompleted={task.subtaskCompleted ?? 0}
            />
          )}

          {isBusy && !isWaitingSubtasks && (
            <div className="flex justify-start">
              {promptProgress ? (
                <div className="inline-flex items-center gap-2 px-3 py-1.5 bg-surface border border-hairline rounded-md text-2xs text-slate-600 min-w-[180px]">
                  <div className="flex-1 min-w-0">
                    <div className="flex items-center justify-between mb-1">
                      <span>{t('pane.processing')}</span>
                      <span className="font-mono tabular-nums">{promptProgress.percent}%</span>
                    </div>
                    <div className="w-full bg-slate-200 rounded-full h-1">
                      <div
                        className="bg-emerald-500 h-1 rounded-full transition-all duration-300"
                        style={{ width: `${promptProgress.percent}%` }}
                      />
                    </div>
                  </div>
                </div>
              ) : streamingText ? (
                <div className="max-w-[80%] min-w-0 px-3 py-2 bg-canvas border border-hairline rounded-lg text-[13px] text-slate-800 leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] opacity-70">
                  {streamingText}
                  <span className="inline-block w-0.5 h-3.5 bg-slate-400 animate-pulse ml-0.5 align-text-bottom" />
                </div>
              ) : liveToolContent ? (
                <div className="max-w-[80%] min-w-0 w-full px-3 py-2 bg-slate-50 border border-hairline rounded-lg">
                  <div className="text-2xs text-slate-500 mb-1 font-mono">{t('pane.generating', { name: liveToolContent.name })}</div>
                  <pre ref={liveToolRef} className="max-h-64 overflow-auto text-[12px] text-slate-800 whitespace-pre-wrap break-words [overflow-wrap:anywhere] m-0">
                    {liveToolContent.text}
                    <span className="inline-block w-0.5 h-3.5 bg-slate-400 animate-pulse ml-0.5 align-text-bottom" />
                  </pre>
                </div>
              ) : (
                <div className="inline-flex items-center gap-2 px-2.5 py-1 bg-surface border border-hairline rounded-md text-2xs text-slate-600">
                  <svg className="w-3 h-3 animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                    <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
                    <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
                  </svg>
                  {llmState?.phase === 'thinking' ? t('pane.llmThinking', { chars: (llmState.chars ?? 0).toLocaleString() })
                    : llmState?.phase === 'retrying' ? t('pane.llmRetrying', { attempt: llmState.attempt ?? 0, max: llmState.maxAttempts ?? 0, reason: llmState.reason ?? llmState.errorClass ?? '' })
                    : llmState?.phase === 'recovering' ? t('pane.llmRecovering', { stage: llmState.stage ?? '' })
                    : llmState?.phase === 'waiting' ? t('pane.llmWaiting')
                    : t('pane.agentResponding')}
                </div>
              )}
            </div>
          )}

          {isBusy && Object.keys(delegateStreams).length > 0 && (
            <div className="flex justify-start mt-1.5">
              <DelegateLiveConsole streams={delegateStreams} />
            </div>
          )}

          {/* General rotating tips to make the wait useful (running only). */}
          {isBusy && <RotatingTips />}
        </div>
      </div>

        {/* Scroll-to-bottom button */}
        {!isAtBottom && (
          <button
            onClick={scrollToBottom}
            className="absolute bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1.5 px-3 py-1.5 bg-canvas border border-slate-200 rounded-full shadow-md text-xs text-slate-600 hover:bg-slate-50 transition-colors z-10"
          >
            <svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M4 6l4 4 4-4" />
            </svg>
            {newMessageCount > 0 ? (
              <span className="text-blue-600 font-medium">{t('pane.newMessages', { count: newMessageCount })}</span>
            ) : (
              <span>{t('pane.toLatest')}</span>
            )}
          </button>
        )}
      </div>

      {/* Composer — カード+ツールバー型。上段 textarea（自動拡張）、下段に
          添付・コンテキスト残量・送信系を集約。旧: 独立ゲージ行+2行 textarea で
          約114px → 待機時約85px。 */}
      <div className="flex-shrink-0 border-t border-hairline bg-canvas p-2.5" style={{ paddingBottom: 'calc(10px + env(safe-area-inset-bottom, 0px))' }}>
        {hasActiveJob && (
          <div className={`flex items-center gap-2 mb-2 px-2.5 py-1 rounded-md text-2xs ${
            canInterject
              ? 'bg-amber-50 dark:bg-amber-500/15 border border-amber-100 dark:border-amber-500/30 text-amber-700 dark:text-amber-300'
              : isPending
                ? 'bg-surface-2 border border-hairline text-slate-600 dark:text-slate-300'
                : 'bg-blue-50 dark:bg-blue-500/15 border border-blue-100 dark:border-blue-500/30 text-blue-700 dark:text-blue-300'
          }`}>
            <svg className="w-3 h-3 animate-spin flex-shrink-0" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
            </svg>
            <span>{canInterject ? t('pane.interjectHint') : isPending ? t('pane.queuedHint') : t('pane.agentRunningWait')}</span>
          </div>
        )}
        <ToolRequestApproval taskId={task.id} poll={jobStatus === 'waiting_human' || isBusy} />
        <PackageRequestApproval taskId={task.id} poll={jobStatus === 'waiting_human' || isBusy} />
        {sendError && !isBusy && (
          <div className="flex items-center justify-between gap-2 mb-2 px-2.5 py-1 bg-red-50 dark:bg-red-500/15 border border-red-100 dark:border-red-500/30 rounded-md text-2xs text-red-700 dark:text-red-300">
            <span className="truncate">⚠ {sendError}</span>
            <button
              type="button"
              onClick={() => void handleSubmit()}
              disabled={submitting}
              className="flex-shrink-0 px-2 h-6 bg-canvas border border-red-200 rounded text-[10px] font-medium text-red-700 dark:text-red-300 hover:bg-red-100 dark:hover:bg-red-500/15 disabled:opacity-50"
            >
              {t('pane.resend')}
            </button>
          </div>
        )}
        <div className={`rounded-xl border transition-shadow ${composerLocked ? 'border-hairline bg-surface' : 'border-hairline bg-canvas focus-within:border-accent focus-within:ring-2 focus-within:ring-accent-ring'}`}>
          <textarea
            ref={composerRef}
            value={draft}
            onChange={e => setDraft(e.target.value)}
            onKeyDown={handleKeyDown}
            onPaste={e => void handlePaste(e)}
            rows={1}
            disabled={composerLocked}
            placeholder={awaitingToolApproval ? t('toolRequest.composerLocked') : inputLocked ? t('pane.placeholder.dispatching') : canInterject ? t('pane.placeholder.interject') : isPending ? t('pane.placeholder.queued') : t('pane.placeholder.default')}
            className="block w-full resize-none border-0 bg-transparent px-3 pt-2.5 pb-1 text-sm leading-6 text-slate-900 outline-none [field-sizing:content] min-h-6 max-h-48 overflow-y-auto disabled:text-slate-400 disabled:cursor-not-allowed"
          />
          <div className="flex flex-wrap items-center gap-1.5 px-2 pb-2">
            <input
              ref={fileInputRef}
              type="file"
              multiple
              className="hidden"
              onChange={e => { void handleFiles(e.target.files); e.target.value = ''; }}
            />
            <button
              onClick={() => fileInputRef.current?.click()}
              disabled={composerLocked || submitting}
              className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-surface hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"
              title={t('pane.attachFile')}
              aria-label={t('pane.attachFile')}
            >
              <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
                <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" />
              </svg>
            </button>
            {attachments.length > 0 && (
              <div className="flex max-h-12 min-w-0 flex-1 flex-wrap gap-1 overflow-y-auto">
                {attachments.map(a => (
                  <span key={a.name} className="inline-flex items-center gap-1 rounded border border-hairline bg-surface-2 px-1.5 py-0.5 font-mono text-[10px] text-slate-700">
                    {a.name}
                    <button onClick={() => removeAttachment(a.name)} className="ml-0.5 text-slate-400 hover:text-slate-700">&times;</button>
                  </span>
                ))}
              </div>
            )}
            <div className="ml-auto flex flex-shrink-0 items-center gap-2">
              {/* コンテキスト残量: 入力しながら「あとどれくらい書けるか」を把握
                  できる位置に常時表示（issue #009 の趣旨を維持）。 */}
              <ContextUsageGauge
                inline
                promptTokens={task.latestJob?.contextPromptTokens}
                limitTokens={task.latestJob?.contextLimitTokens}
                jobStatus={task.latestJob?.status}
              />
              {isBusy && onCancel ? (
                <div className="flex gap-1.5">
                  {canInterject && (
                    <button
                      disabled={submitting || (!draft.trim() && attachments.length === 0)}
                      onClick={handleSubmit}
                      className="inline-flex items-center gap-1.5 px-3 h-9 rounded-md text-xs font-semibold flex-shrink-0 transition-colors bg-amber-100 text-amber-800 border border-amber-300 hover:bg-amber-200 dark:bg-amber-500/15 dark:text-amber-300 dark:border-amber-500/30 dark:hover:bg-amber-500/25 disabled:opacity-50"
                    >
                      <svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                        <polyline points="15 10 20 15 15 20" />
                        <path d="M4 4v7a4 4 0 0 0 4 4h12" />
                      </svg>
                      {t('pane.interject')}
                    </button>
                  )}
                  <button
                    disabled={cancelling}
                    onClick={() => void handleCancel()}
                    className="inline-flex items-center gap-1.5 px-3 h-9 rounded-md text-xs font-semibold flex-shrink-0 transition-colors bg-canvas border border-red-200 text-red-700 hover:bg-red-50 dark:border-red-500/40 dark:text-red-300 dark:hover:bg-red-500/15 disabled:opacity-50"
                    title={t('pane.stopAgent')}
                  >
                    <svg className="w-3 h-3" viewBox="0 0 24 24" aria-hidden="true">
                      <rect x="6" y="6" width="12" height="12" rx="2.5" fill="currentColor" />
                    </svg>
                    {cancelling ? t('pane.stopping') : t('pane.stop')}
                  </button>
                </div>
              ) : isPending ? (
                <button
                  disabled={submitting || (!draft.trim() && attachments.length === 0)}
                  onClick={handleSubmit}
                  title={t('pane.addToQueuedHint')}
                  className="inline-flex items-center gap-1.5 px-3 h-9 rounded-md text-xs font-semibold flex-shrink-0 transition-colors bg-amber-100 text-amber-800 border border-amber-300 hover:bg-amber-200 dark:bg-amber-500/15 dark:text-amber-300 dark:border-amber-500/30 dark:hover:bg-amber-500/25 disabled:opacity-50"
                >
                  <svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <polyline points="15 10 20 15 15 20" />
                    <path d="M4 4v7a4 4 0 0 0 4 4h12" />
                  </svg>
                  {t('pane.addToQueued')}
                </button>
              ) : (
                <button
                  disabled={submitting || composerLocked || (!draft.trim() && attachments.length === 0)}
                  onClick={handleSubmit}
                  className="inline-flex items-center gap-1.5 px-3 h-9 bg-accent text-accent-fg rounded-md text-xs font-semibold disabled:opacity-50 hover:bg-accent-deep flex-shrink-0 transition-colors"
                >
                  <svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <path d="M22 2 11 13" />
                    <path d="M22 2 15 22 11 13 2 9 22 2Z" />
                  </svg>
                  {t('pane.send')}
                </button>
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
