"use client";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { lazy, memo, Suspense, useCallback, useMemo, useState } from "react";
import { create } from "@bufbuild/protobuf";
import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
import { ExecutionPhase, FileChangeSetStatus, InteractionMode, MessageType, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
import { displayFileChangeSets } from "@stigmer/sdk";
import { cn } from "@stigmer/theme";
import { isTerminalPhase } from "./execution-phases.js";
import { MessageEntry } from "./MessageEntry.js";
import { ToolCallGroup } from "./ToolCallGroup.js";
import { SubAgentSection } from "./SubAgentSection.js";
import { ExecutionPhaseBadge } from "./ExecutionPhaseBadge.js";
import { SetupProgress } from "./SetupProgress.js";
import { ApprovalCard } from "./ApprovalCard.js";
import { FileReviewCard } from "./FileReviewCard.js";
import { SummarizationCard } from "./SummarizationCard.js";
import { PlanCompletionCard } from "./PlanCompletionCard.js";
import { PlanArtifactCard } from "./PlanArtifactCard.js";
import { PlanStreamingCard } from "./PlanStreamingCard.js";
import { TodoCard } from "./TodoCard.js";
import { findPlanArtifact } from "../library/detect-plan-artifact.js";
import { findStreamingPlan } from "../library/detect-streaming-plan.js";
import { isInternalTool, isCollapsedToolCall } from "./tool-categories.js";
import { FilePathContext } from "./FilePathContext.js";
import { SandboxContext } from "./SandboxContext.js";
import { ApprovalContext } from "./ApprovalContext.js";
import { FileReviewContext } from "./FileReviewContext.js";
import { extractLeadingH1, unwrapEnclosingMarkdownFence, } from "../internal/markdown-components.js";
import { useRenderTracer, useKeyStability, useDomNodeCount, DevProfiler } from "../internal/dev/index.js";
import { useAutoScroll } from "../internal/useAutoScroll.js";
import { JumpToLatestButton } from "../internal/JumpToLatestButton.js";
import { ApprovalPeekBar } from "../internal/ApprovalPeekBar.js";
import { ThreadItemWrapper } from "../internal/ThreadItemWrapper.js";
const LazyVirtualizedThread = lazy(() => import("../internal/VirtualizedThread.js").then((m) => ({
    default: m.VirtualizedThread,
})));
/** Stable empty collections so an approval-free thread keeps referential identity. */
const EMPTY_APPROVALS = [];
const EMPTY_SUBMITTING_IDS = new Set();
const EMPTY_APPROVAL_ERRORS = new Map();
/**
 * Class set for the optional reading column — one definition shared by both
 * render paths (non-virtualized and virtualized) so their geometry can never
 * drift.
 *
 * @internal
 */
export function threadContentColumnClass(column) {
    return column != null && cn("w-full max-w-3xl px-4", column === "center" && "mx-auto");
}
/**
 * True once the agent has produced any real, renderable response for this turn —
 * assistant text, a tool call (running or terminal), or streamed model thinking.
 *
 * Gates the synthetic "Thinking…" setup placeholder (see usage below): the
 * placeholder is only for the genuine pre-first-content window and must yield the
 * moment any of these stream in, otherwise it would render *alongside* the real
 * ThinkingMessage / ToolCallGroup cards (the GitHub #179 duplicate-spinner).
 */
function hasStartedResponding(execution) {
    const messages = execution.status?.messages;
    if (!messages || messages.length === 0)
        return false;
    return messages.some((m) => {
        // Any tool call — RUNNING or terminal — is visible activity, regardless of
        // which message type hosts it.
        if (m.toolCalls.length > 0)
            return true;
        if (m.type === MessageType.MESSAGE_AI && m.content.trim().length > 0)
            return true;
        // Streamed reasoning renders in its own collapsible ThinkingMessage card, so
        // it counts as "responding" even before the first assistant token arrives.
        if (m.type === MessageType.MESSAGE_THINKING && m.content.trim().length > 0)
            return true;
        return false;
    });
}
/**
 * True for an execution that ran in Plan mode and completed — the only kind of
 * execution that has a reviewable plan (streaming/failed/terminated Plan turns
 * have nothing final to review or build from).
 */
function isCompletedPlanExecution(execution) {
    return (execution.status?.phase === ExecutionPhase.EXECUTION_COMPLETED &&
        execution.spec?.executionConfig?.interactionMode === InteractionMode.PLAN);
}
/**
 * Index of the message that IS the plan in a completed Plan execution: the
 * last AI message with content — the SAME selection rule the runner's
 * `extractFinalPlanText` uses to publish `plan.md`, so the message promoted to
 * a document in the thread is byte-identical to the published artifact.
 * Returns -1 when no such message exists.
 */
function findPlanMessageIndex(messages) {
    for (let i = messages.length - 1; i >= 0; i--) {
        const msg = messages[i];
        if (msg.type === MessageType.MESSAGE_AI && msg.content.trim().length > 0) {
            return i;
        }
    }
    return -1;
}
/**
 * The plan's title for its compact card: the leading `# H1` of the plan
 * message, after the same plan-scoped bare-fence unwrap the document renderers
 * apply — so the card and the plan tab always agree on the title. `undefined`
 * when the plan has no H1 (the card falls back to "Plan").
 */
function extractPlanTitle(content) {
    return extractLeadingH1(unwrapEnclosingMarkdownFence(content, true)).title ?? undefined;
}
/**
 * Adds the tool-call ids that a sub-agent renders as nested rows (the same
 * non-internal AI tool calls {@link SubAgentSection} surfaces) to `target`,
 * so their approvals are treated as inline and skip the bottom backstop.
 */
function collectSubAgentInlineToolCallIds(sub, target) {
    for (const msg of sub.messages) {
        if (msg.type !== MessageType.MESSAGE_AI)
            continue;
        for (const tc of msg.toolCalls) {
            if (tc.id && !isInternalTool(tc.name))
                target.add(tc.id);
        }
    }
}
/**
 * Records, for each change set referenced by a just-pushed tool-group item, the
 * item's index — later groups overwrite, so the map converges on each set's
 * LAST stamped row. That index is where the set's decision bar anchors: the
 * review surface appears where the turn's editing activity ended, not at the
 * thread tail.
 */
function recordFileReviewAnchors(toolCalls, itemIndex, anchorIndexBySetId) {
    for (const tc of toolCalls) {
        if (tc.fileChangeSetId)
            anchorIndexBySetId.set(tc.fileChangeSetId, itemIndex);
    }
}
/**
 * Inserts one execution's settled file-review records into its own thread
 * segment: each change set's read-only `file-review-record` item is spliced
 * immediately after the set's last stamped tool row
 * ({@link recordFileReviewAnchors}); a set with no stamped row (its changes
 * were made by shell commands, or the rows predate stamping) falls back to the
 * segment's tail — this record is that set's ONLY in-thread trace, so it is
 * never dropped.
 *
 * A *pending* set — AWAITING_REVIEW on a live (non-terminal) execution — is
 * deliberately NOT emitted: its decision surface is the composer-docked
 * `FileReviewDock`, which cannot scroll out of view, and the stamped rows'
 * badges carry the pending state in place. The same set re-enters here as a
 * record the moment it settles (or its execution terminates mid-review — an
 * honest "not reviewed" record). A CAPTURING set (baseline only, no diff yet)
 * has nothing to show and is skipped.
 */
function insertFileReviewItems(items, changeSets, anchorIndexBySetId, execTerminal) {
    const anchored = [];
    const tail = [];
    for (const changeSet of changeSets) {
        if (changeSet.changes.length === 0)
            continue;
        const pending = !execTerminal && changeSet.status === FileChangeSetStatus.AWAITING_REVIEW;
        if (pending)
            continue;
        const item = {
            kind: "file-review-record",
            fileChangeSet: changeSet,
            key: `file-review-${changeSet.id}`,
        };
        const index = anchorIndexBySetId.get(changeSet.id);
        if (index !== undefined)
            anchored.push({ index, item });
        else
            tail.push(item);
    }
    // Splice in ascending anchor order with a running offset so earlier inserts
    // do not displace later anchors.
    anchored.sort((a, b) => a.index - b.index);
    let offset = 0;
    for (const { index, item } of anchored) {
        items.splice(index + 1 + offset, 0, item);
        offset++;
    }
    items.push(...tail);
}
/**
 * Builds a flat list of renderable thread items from execution data.
 *
 * Keys use stable execution IDs (not array indices) so React can
 * reconcile items across renders without unnecessary remounts.
 *
 * @internal Exported for testing — not part of the public API.
 */
export function buildThreadItems(executions, activeStreamExecution, pendingUserMessage, includeApprovals, workspaceEntries, summarizationEvents, pendingMessageFailed = false, editableActiveTurn = false, includeFileReviewRecords = false, collapseStreamingPlan = false) {
    const items = [];
    // Tool-call ids that render as an inline-approval-capable ToolCallItem (a
    // regular parent tool, or a sub-agent's nested tool). Their approvals show
    // inline on the row, so they are excluded from the bottom backstop below —
    // a task-spawn call (rendered as a SubAgentSection, not a tool row) is NOT
    // collected, so its spawn-gate approval still surfaces at the bottom.
    const inlineToolCallIds = new Set();
    // Dedupe by execution id, with activeStreamExecution authoritative for its own
    // id. The hook passes `completedExecutions` (filtered on activeExecutionId),
    // but that filter and activeStreamExecution (stream.execution ??
    // fetchedActiveExecution) derive from different sources, so a transient skew
    // can leave the active execution in BOTH lists. Concatenating blindly then
    // emits the same execution twice → duplicate React keys → React silently drops
    // one subtree, intermittently the one carrying a live ApprovalCard (the gate
    // buttons vanish). Dropping any prior entry sharing the active id keeps the
    // streamed copy as the single source of truth and the append-at-end invariant
    // (activeStreamIndex === last) intact.
    const allExecutions = activeStreamExecution
        ? [
            ...executions.filter((e) => e.metadata?.id !== activeStreamExecution.metadata?.id),
            activeStreamExecution,
        ]
        : executions;
    const activeStreamIndex = activeStreamExecution
        ? allExecutions.length - 1
        : -1;
    // The most recent completed Plan execution owns the primary "Build" action;
    // every earlier plan renders as a review-only record. Resolved up front so
    // each segment can stamp its own card with the right authority.
    let latestPlanExecutionId = null;
    for (let i = allExecutions.length - 1; i >= 0; i--) {
        if (isCompletedPlanExecution(allExecutions[i])) {
            latestPlanExecutionId = allExecutions[i].metadata?.id ?? `_e${i}`;
            break;
        }
    }
    // Build a queue of summarization events to interleave by timestamp.
    // Events are consumed as messages pass their timestamp.
    const pendingEvents = summarizationEvents?.length
        ? [...summarizationEvents]
        : [];
    let eventCursor = 0;
    function flushEventsUntil(messageTimestamp) {
        if (!messageTimestamp || pendingEvents.length === 0)
            return;
        while (eventCursor < pendingEvents.length &&
            pendingEvents[eventCursor].timestamp &&
            pendingEvents[eventCursor].timestamp <= messageTimestamp) {
            const evt = pendingEvents[eventCursor];
            items.push({
                kind: "context-compacted",
                event: evt,
                key: `compacted-${evt.timestamp}`,
            });
            eventCursor++;
        }
    }
    for (let ei = 0; ei < allExecutions.length; ei++) {
        const exec = allExecutions[ei];
        const execId = exec.metadata?.id ?? `_e${ei}`;
        const isActiveStreamExec = ei === activeStreamIndex;
        const messages = exec.status?.messages ?? [];
        const subAgents = exec.status?.subAgentExecutions ?? [];
        // A completed Plan turn collapses its plan message into the compact plan
        // card (the plan-completion item): the document lives in the panel's plan
        // tab, not the thread. While the plan is still streaming, the same
        // treatment applies live when the host has a plan surface (streamingPlan
        // below): the in-flight plan message is suppressed behind a plan-writing
        // card while the panel's plan tab renders it as it streams — the content
        // moves, it doesn't disappear. Completion swaps the live card for the
        // settled one (same key), and the paired auto-open (useSessionPanel's
        // planKey trigger) keeps the document side-by-side. Collapse of a
        // COMPLETED plan is gated on the ARTIFACT existing: a plan that never
        // published (older executions, failed upload) exists only in the message,
        // so that path keeps the inline document promotion (isPlanDocument) —
        // collapsing it would orphan the plan behind a card with nothing to open.
        const isCompletedPlanExec = isCompletedPlanExecution(exec);
        const planMessageIndex = isCompletedPlanExec
            ? findPlanMessageIndex(messages)
            : -1;
        const planArtifact = isCompletedPlanExec
            ? findPlanArtifact(exec)
            : undefined;
        const collapsePlanMessage = planMessageIndex >= 0 && planArtifact !== undefined;
        // The plan the active Plan-mode turn is writing RIGHT NOW (detected by
        // the H1 convention — see findStreamingPlan). Its message is suppressed
        // in favor of a live plan-writing card, mirroring the completed-turn
        // collapse above, while the panel's plan tab renders the document live.
        // Two gates: collapseStreamingPlan (the host wired onOpenPlan — without a
        // document surface the plan must keep streaming inline, DD-011) and
        // isActiveStreamExec (a stale non-terminal execution left in the
        // completed list by the transient skew documented above must never
        // sprout a live card). Mutually exclusive with collapsePlanMessage by
        // construction: one requires a terminal phase, the other forbids it.
        const streamingPlan = collapseStreamingPlan && isActiveStreamExec
            ? findStreamingPlan(exec)
            : undefined;
        // This execution's settled change sets render as read-only records inside
        // its own segment, anchored to the last stamped edit row of each set. The
        // display seam reads the server's live projection when present and folds
        // the durable ledger for a terminal execution (whose projection is nil),
        // so records surface for EVERY execution, not just the last. Pending sets
        // are excluded inside insertFileReviewItems — the composer dock owns them.
        const execChangeSets = includeFileReviewRecords
            ? displayFileChangeSets(exec.status)
            : [];
        const fileReviewAnchors = new Map();
        // The agent's plan renders as one inline card per turn, anchored to the
        // first of: the opening AI/thinking message, the first unit of work
        // (tool-group / sub-agent), or — as a backstop — the turn's tail. We carry
        // the live `status.todos` reference (structural sharing keeps it stable),
        // so the card updates in place and a settled card skips re-renders.
        const execTodos = exec.status?.todos;
        const hasTodos = execTodos != null && Object.keys(execTodos).length > 0;
        let todosEmitted = false;
        function emitTodos() {
            if (todosEmitted || !hasTodos)
                return;
            todosEmitted = true;
            items.push({
                kind: "todos",
                executionId: execId,
                todos: execTodos,
                key: `${execId}-todos`,
            });
        }
        const specMessage = exec.spec?.message;
        // A Build-from-plan turn synthesizes NO prompt item: its message is a
        // machine-written label ("Build from plan"), not user prose — the real
        // instruction is runner-injected from the same flag. The plan card
        // directly above the turn is the visible cause; rendering the label as a
        // user bubble would attribute words to the user they never typed.
        const isBuildTurn = exec.spec?.executionConfig?.buildFromPlan === true;
        if (specMessage && specMessage !== "execute" && !isBuildTurn) {
            const syntheticHumanMsg = create(AgentMessageSchema);
            syntheticHumanMsg.type = MessageType.MESSAGE_HUMAN;
            syntheticHumanMsg.content = specMessage;
            // When the active stream execution's spec message matches the
            // pending user message, use a shared bridging key so React
            // updates the pending bubble in place instead of remounting.
            const bridgePending = isActiveStreamExec &&
                pendingUserMessage != null &&
                specMessage === pendingUserMessage;
            items.push({
                kind: "message",
                message: syntheticHumanMsg,
                key: bridgePending ? "pending-user-turn" : `${execId}-spec`,
                // The active execution's prompt is the one a user can edit-and-resubmit
                // (stop + rephrase). Only mark it when the consumer enabled editing.
                isEditable: isActiveStreamExec && editableActiveTurn,
                // The turn's mode marks the prompt bubble (a "Plan" pill on Plan
                // turns) so the transcript reads unambiguously after mode switches.
                interactionMode: exec.spec?.executionConfig?.interactionMode,
            });
        }
        for (let mi = 0; mi < messages.length; mi++) {
            const msg = messages[mi];
            // MESSAGE_TOOL messages are not rendered — tool calls are attached
            // to their parent MESSAGE_AI and rendered via ToolCallGroup.
            if (msg.type === MessageType.MESSAGE_TOOL)
                continue;
            flushEventsUntil(msg.timestamp);
            const isEmptyAi = msg.type === MessageType.MESSAGE_AI && !msg.content.trim();
            if (!isEmptyAi) {
                // The collapsed plan message is not rendered — the plan-completion
                // card at the segment's end is the turn's sole plan representation.
                // Its tool calls (below) and the todo anchor still process normally.
                // A STREAMING plan message is suppressed the same way: its stand-in
                // is the plan-writing card at the segment's end, and the document
                // renders live in the panel's plan tab.
                const suppressAsPlan = (collapsePlanMessage && mi === planMessageIndex) ||
                    mi === streamingPlan?.messageIndex;
                if (!suppressAsPlan) {
                    items.push({
                        kind: "message",
                        message: msg,
                        key: `${execId}-m${mi}`,
                        isPlanDocument: !collapsePlanMessage && mi === planMessageIndex,
                    });
                }
                // Anchor (1): the plan sits right under the agent's opening narration.
                if (msg.type === MessageType.MESSAGE_AI ||
                    msg.type === MessageType.MESSAGE_THINKING) {
                    emitTodos();
                }
            }
            // Runner-collapsed duplicates (a superseded same-turn denial twin of an
            // approval gate) are not rendered — they are kept in the transcript only to
            // satisfy the backend's append-only guard. Filtering them here keeps a
            // message whose tool calls are ALL collapsed from emitting an empty group.
            // Preserve the original array reference when nothing is collapsed (the
            // common case) so structural sharing / memoization (T04) is not defeated by
            // a fresh array on every build.
            const renderableToolCalls = msg.type === MessageType.MESSAGE_AI && msg.toolCalls.some(isCollapsedToolCall)
                ? msg.toolCalls.filter((tc) => !isCollapsedToolCall(tc))
                : msg.toolCalls;
            if (msg.type === MessageType.MESSAGE_AI &&
                renderableToolCalls.length > 0) {
                // Anchor (2): if work begins before any rendered narration, the plan
                // still leads it.
                emitTodos();
                const needsSplit = renderableToolCalls.some((tc) => tc.name === "task" || isInternalTool(tc.name));
                if (needsSplit) {
                    const regularTools = [];
                    const matchedSubAgents = [];
                    for (const tc of renderableToolCalls) {
                        if (tc.name === "task") {
                            const matched = subAgents.find((sa) => sa.id === tc.id);
                            if (matched)
                                matchedSubAgents.push(matched);
                        }
                        else if (!isInternalTool(tc.name)) {
                            regularTools.push(tc);
                        }
                    }
                    if (regularTools.length > 0) {
                        items.push({
                            kind: "tool-group",
                            toolCalls: regularTools,
                            subAgentExecutions: subAgents,
                            key: `${execId}-m${mi}-tc`,
                        });
                        recordFileReviewAnchors(regularTools, items.length - 1, fileReviewAnchors);
                        for (const tc of regularTools) {
                            if (tc.id)
                                inlineToolCallIds.add(tc.id);
                        }
                    }
                    for (const sa of matchedSubAgents) {
                        items.push({
                            kind: "sub-agent",
                            subAgentExecution: sa,
                            key: `sa-${sa.id}`,
                        });
                        collectSubAgentInlineToolCallIds(sa, inlineToolCallIds);
                    }
                }
                else {
                    items.push({
                        kind: "tool-group",
                        toolCalls: renderableToolCalls,
                        subAgentExecutions: subAgents,
                        key: `${execId}-m${mi}-tc`,
                    });
                    recordFileReviewAnchors(renderableToolCalls, items.length - 1, fileReviewAnchors);
                    for (const tc of renderableToolCalls) {
                        if (tc.id)
                            inlineToolCallIds.add(tc.id);
                    }
                }
            }
        }
        // A completed Plan turn closes its segment with the plan card — the
        // compact stand-in for the collapsed plan message, carrying the title the
        // message would have shown. Every plan stays reviewable forever; only the
        // latest carries the build authority (isLatestPlan). A superseded plan
        // that never published an artifact is skipped — with no artifact and no
        // build CTA it would render an empty shell.
        if (isCompletedPlanExec) {
            const isLatestPlan = execId === latestPlanExecutionId;
            if (planArtifact || isLatestPlan) {
                items.push({
                    kind: "plan-completion",
                    key: `${execId}-plan-completion`,
                    executionId: exec.metadata?.id ?? "",
                    planArtifact,
                    planTitle: collapsePlanMessage
                        ? extractPlanTitle(messages[planMessageIndex].content)
                        : undefined,
                    isLatestPlan,
                });
            }
        }
        else if (streamingPlan) {
            // The live stand-in for the suppressed streaming plan message. It
            // deliberately SHARES the completion item's key: on completion this
            // item becomes the plan-completion item above (a kind change under a
            // stable key), so the card holds one thread position and the handoff
            // reads as the card settling rather than a new element appearing.
            items.push({
                kind: "plan-writing",
                key: `${execId}-plan-completion`,
                executionId: exec.metadata?.id ?? "",
                planTitle: extractLeadingH1(streamingPlan.displayText).title ?? undefined,
                planSize: streamingPlan.displayText.length,
            });
        }
        // Anchor (3): a plan that produced no rendered narration or work item still
        // surfaces — never silently dropped.
        emitTodos();
        // Settled records land inside this execution's segment, at their anchors.
        if (execChangeSets.length > 0) {
            insertFileReviewItems(items, execChangeSets, fileReviewAnchors, isTerminalPhase(exec.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED));
        }
    }
    // Flush any remaining summarization events that occurred after all messages
    while (eventCursor < pendingEvents.length) {
        const evt = pendingEvents[eventCursor];
        items.push({
            kind: "context-compacted",
            event: evt,
            key: `compacted-${evt.timestamp}`,
        });
        eventCursor++;
    }
    const lastExec = allExecutions[allExecutions.length - 1];
    const lastPhase = lastExec?.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
    if (activeStreamExecution && !hasStartedResponding(activeStreamExecution)) {
        const isPending = lastPhase === ExecutionPhase.EXECUTION_PENDING ||
            lastPhase === ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
        const isInProgressNoMessages = lastPhase === ExecutionPhase.EXECUTION_IN_PROGRESS;
        if (isPending) {
            const serverPhase = activeStreamExecution.status?.setupProgress?.currentPhase || undefined;
            items.push({
                kind: "setup-progress",
                workspaceEntries: workspaceEntries ?? [],
                serverPhase,
                key: "setup-progress",
            });
        }
        else if (isInProgressNoMessages) {
            items.push({
                kind: "setup-progress",
                workspaceEntries: workspaceEntries ?? [],
                isAwaitingResponse: true,
                key: "setup-progress",
            });
        }
    }
    if (isTerminalPhase(lastPhase) &&
        lastPhase !== ExecutionPhase.EXECUTION_COMPLETED) {
        items.push({
            kind: "phase-badge",
            phase: lastPhase,
            key: `phase-${lastPhase}`,
        });
        // The server populates `status.error` only on EXECUTION_FAILED. Surface it
        // beside the badge so a failure that produced no messages still explains
        // itself (the CLI shows this reason; the chat previously showed nothing).
        // Kept as its own item so the badge component stays presentational.
        const reason = lastExec?.status?.error;
        if (reason) {
            const specMessage = lastExec?.spec?.message;
            // A failed build turn offers no inline Retry: resending its label as an
            // ordinary message would drop the buildFromPlan flag (no runner
            // directive, no plan attachment). The plan card above the error is the
            // fully-wired retry — its Build button re-runs the whole pipeline.
            const isBuildTurn = lastExec?.spec?.executionConfig?.buildFromPlan === true;
            items.push({
                kind: "execution-error",
                error: reason,
                retryMessage: specMessage && specMessage !== "execute" && !isBuildTurn
                    ? specMessage
                    : undefined,
                key: `execution-error-${lastExec?.metadata?.id ?? lastPhase}`,
            });
        }
    }
    // Plan cards are emitted inside each execution's segment (see the
    // isCompletedPlanExec block in the loop above) — attached to their plan
    // document, persistent across later turns, never appended at the tail.
    if (includeApprovals) {
        // Backstop only: an approval whose tool call renders inline shows its gate
        // on that row (see ApprovalContext / ToolCallItem). We emit a bottom card
        // ONLY for an approval with no inline home — a true orphan, or a task-spawn
        // approval that precedes its SubAgentExecution — so a pending gate is never
        // invisible, and never duplicated.
        const allApprovals = lastExec?.status?.pendingApprovals ?? [];
        for (let ai = 0; ai < allApprovals.length; ai++) {
            const approval = allApprovals[ai];
            if (approval.toolCallId && inlineToolCallIds.has(approval.toolCallId)) {
                continue;
            }
            items.push({
                kind: "approval-request",
                pendingApproval: approval,
                key: `approval-${approval.toolCallId || ai}`,
            });
        }
    }
    // File-review records are emitted inside each execution's segment (see
    // insertFileReviewItems in the loop above) — anchored to the last stamped
    // edit row, never appended at the thread tail. Pending decision bars are
    // not thread items at all: they live in the composer-docked FileReviewDock.
    if (pendingUserMessage) {
        // A FAILED send never created an execution, so an existing execution with
        // identical text (e.g. two build attempts of the same plan — both
        // "Build from plan") must never swallow the failed bubble. Suppression
        // only applies to the succeeded-send window where the stream has
        // delivered the same turn but the pending state hasn't cleared yet.
        const alreadySynthesized = !pendingMessageFailed && lastExec?.spec?.message === pendingUserMessage;
        if (!alreadySynthesized) {
            const syntheticPending = create(AgentMessageSchema);
            syntheticPending.type = MessageType.MESSAGE_HUMAN;
            syntheticPending.content = pendingUserMessage;
            items.push({
                kind: "message",
                message: syntheticPending,
                key: "pending-user-turn",
                // A failed turn shows the inline error instead of the dimmed
                // sending state — the two are mutually exclusive.
                isPending: !pendingMessageFailed,
                isFailed: pendingMessageFailed,
            });
        }
    }
    return items;
}
/**
 * Renders a continuous conversation thread from one or more
 * {@link AgentExecution} snapshots.
 *
 * Composes {@link MessageEntry}, {@link ToolCallGroup}, and
 * {@link ExecutionPhaseBadge} into a scrollable, auto-scrolling log
 * that follows new content when the user is near the bottom.
 *
 * Renders nothing when no executions are provided.
 *
 * All visual properties flow through `--stgm-*` tokens.
 *
 * @example
 * ```tsx
 * const { executions } = useSessionExecutions(sessionId);
 * const stream = useExecutionStream(activeId);
 *
 * <MessageThread
 *   executions={executions ?? []}
 *   activeStreamExecution={stream.execution}
 * />
 * ```
 */
export function MessageThread({ executions, activeStreamExecution, pendingUserMessage, pendingMessageFailed = false, onRetrySend, onRetryExecution, onEditMessage, className, formatToolCallSummary, onApprovalSubmit, submittingApprovalIds, approvalErrors, showFileReviewRecords = false, workspaceEntries, onFilePathClick, sandboxWorkspaceRoot, summarizationEvents, virtualized = false, onBuildFromPlan, onOpenPlan, org, planActionsDisabled, planBuildPending, contentColumn, }) {
    useRenderTracer("MessageThread", { executions, activeStreamExecution });
    const includeApprovals = onApprovalSubmit != null;
    const editableActiveTurn = onEditMessage != null;
    // A streaming plan is collapsed behind its live card only when the host can
    // open the plan document surface — a panel-less host (no onOpenPlan) keeps
    // the plan streaming inline, where it remains readable (DD-011).
    const collapseStreamingPlan = onOpenPlan != null;
    const items = useMemo(() => buildThreadItems(executions, activeStreamExecution, pendingUserMessage, includeApprovals, workspaceEntries, summarizationEvents, pendingMessageFailed, editableActiveTurn, showFileReviewRecords, collapseStreamingPlan), [executions, activeStreamExecution, pendingUserMessage, includeApprovals, workspaceEntries, summarizationEvents, pendingMessageFailed, editableActiveTurn, showFileReviewRecords, collapseStreamingPlan]);
    useKeyStability(items);
    const filePathCtx = useMemo(() => ({
        workspaceEntries: workspaceEntries ?? [],
        onFilePathClick,
    }), [workspaceEntries, onFilePathClick]);
    const sandboxCtx = useMemo(() => ({ sandboxWorkspaceRoot: sandboxWorkspaceRoot ?? "" }), [sandboxWorkspaceRoot]);
    // Pending approvals live on the latest execution. We project them into a
    // tool-call-id-keyed map so each gated tool row can render its own approval
    // inline (see ApprovalContext); buildThreadItems still emits a bottom card
    // for any approval with no matching inline row (orphan backstop).
    const lastExec = activeStreamExecution ?? executions[executions.length - 1];
    const pendingApprovals = includeApprovals
        ? lastExec?.status?.pendingApprovals ?? EMPTY_APPROVALS
        : EMPTY_APPROVALS;
    const approvalsByToolCallId = useMemo(() => {
        const map = new Map();
        for (const approval of pendingApprovals) {
            if (approval.toolCallId)
                map.set(approval.toolCallId, approval);
        }
        return map;
    }, [pendingApprovals]);
    const approvalCtx = useMemo(() => ({
        approvalsByToolCallId,
        onSubmit: onApprovalSubmit,
        submittingIds: submittingApprovalIds ?? EMPTY_SUBMITTING_IDS,
        errorsByToolCallId: approvalErrors ?? EMPTY_APPROVAL_ERRORS,
    }), [approvalsByToolCallId, onApprovalSubmit, submittingApprovalIds, approvalErrors]);
    // Every displayable change set across the session, keyed by id, so a stamped
    // edit row (ToolCall.file_change_set_id) can badge its set's live review
    // state wherever it renders (parent thread or nested groups). The map is
    // rebuilt only when the execution list reference changes; structural sharing
    // keeps each FileChangeSet reference stable across streaming frames, so
    // subscribing rows re-render on review events, not every snapshot.
    const fileReviewCtx = useMemo(() => {
        const map = new Map();
        const all = activeStreamExecution
            ? [...executions, activeStreamExecution]
            : executions;
        for (const exec of all) {
            for (const changeSet of displayFileChangeSets(exec.status)) {
                // Later wins: the active stream's copy supersedes a stale fetched one.
                map.set(changeSet.id, changeSet);
            }
        }
        return { changeSetsById: map };
    }, [executions, activeStreamExecution]);
    // Drives the global "approval needed" peek affordance — a count, not the
    // cards, so the bar reuses the existing scroll machine without a new observer.
    const unresolvedApprovalCount = pendingApprovals.length;
    if (virtualized) {
        return (_jsx("div", { className: cn("relative min-h-0", className), children: _jsx(Suspense, { fallback: null, children: _jsx(LazyVirtualizedThread, { items: items, formatToolCallSummary: formatToolCallSummary, onApprovalSubmit: onApprovalSubmit, submittingApprovalIds: submittingApprovalIds, approvalErrors: approvalErrors, filePathCtx: filePathCtx, sandboxCtx: sandboxCtx, approvalCtx: approvalCtx, fileReviewCtx: fileReviewCtx, unresolvedApprovalCount: unresolvedApprovalCount, onBuildFromPlan: onBuildFromPlan, onOpenPlan: onOpenPlan, org: org, planActionsDisabled: planActionsDisabled, planBuildPending: planBuildPending, contentColumn: contentColumn, onRetrySend: onRetrySend, onRetryExecution: onRetryExecution, onEditMessage: onEditMessage }) }) }));
    }
    return (_jsx(NonVirtualizedThread, { items: items, className: className, contentColumn: contentColumn, formatToolCallSummary: formatToolCallSummary, onApprovalSubmit: onApprovalSubmit, submittingApprovalIds: submittingApprovalIds, approvalErrors: approvalErrors, filePathCtx: filePathCtx, sandboxCtx: sandboxCtx, approvalCtx: approvalCtx, fileReviewCtx: fileReviewCtx, unresolvedApprovalCount: unresolvedApprovalCount, onBuildFromPlan: onBuildFromPlan, onOpenPlan: onOpenPlan, org: org, planActionsDisabled: planActionsDisabled, planBuildPending: planBuildPending, onRetrySend: onRetrySend, onRetryExecution: onRetryExecution, onEditMessage: onEditMessage }));
}
function NonVirtualizedThread({ items, className, contentColumn, formatToolCallSummary, onApprovalSubmit, submittingApprovalIds, approvalErrors, filePathCtx, sandboxCtx, approvalCtx, fileReviewCtx, unresolvedApprovalCount, onBuildFromPlan, onOpenPlan, org, planActionsDisabled, planBuildPending, onRetrySend, onRetryExecution, onEditMessage, }) {
    const { scrollRef, sentinelRef, contentRef, isFollowing, jumpToLatest } = useAutoScroll();
    useDomNodeCount(scrollRef, "MessageThread");
    return (_jsxs("div", { className: cn("relative min-h-0", className), children: [_jsxs("div", { ref: scrollRef, role: "log", "aria-live": "polite", "aria-relevant": "additions", className: cn("h-full overflow-y-auto pt-6 pb-4 [overflow-anchor:none]", "[scrollbar-width:thin] [scrollbar-color:var(--color-border)_transparent]", "[&::-webkit-scrollbar]:w-1.5", "[&::-webkit-scrollbar-track]:bg-transparent", "[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/40"), children: [_jsx(SandboxContext.Provider, { value: sandboxCtx, children: _jsx(FilePathContext.Provider, { value: filePathCtx, children: _jsx(ApprovalContext.Provider, { value: approvalCtx, children: _jsx(FileReviewContext.Provider, { value: fileReviewCtx, children: _jsx(DevProfiler, { id: "MessageThread", children: _jsx("div", { ref: contentRef, className: cn("flex flex-col gap-4", threadContentColumnClass(contentColumn)), children: items.map((item) => (_jsx(ThreadItemWrapper, { animate: true, children: _jsx(ThreadItemRenderer, { item: item, formatToolCallSummary: formatToolCallSummary, onApprovalSubmit: onApprovalSubmit, submittingApprovalIds: submittingApprovalIds, approvalErrors: approvalErrors, onBuildFromPlan: onBuildFromPlan, onOpenPlan: onOpenPlan, org: org, planActionsDisabled: planActionsDisabled, planBuildPending: planBuildPending, onRetrySend: onRetrySend, onRetryExecution: onRetryExecution, onEditMessage: onEditMessage }) }, item.key))) }) }) }) }) }) }), _jsx("div", { ref: sentinelRef, "aria-hidden": "true" })] }), _jsx(JumpToLatestButton, { onClick: jumpToLatest, visible: !isFollowing && unresolvedApprovalCount === 0 }), _jsx(ApprovalPeekBar, { visible: !isFollowing && unresolvedApprovalCount > 0, count: unresolvedApprovalCount, onClick: jumpToLatest })] }));
}
/**
 * Renders a single thread item by discriminated `kind`. Used by both
 * the non-virtualized `items.map()` path and the virtualized
 * `Virtuoso.itemContent` callback.
 *
 * Does not receive a `key` prop — the caller is responsible for
 * keying (either via `items.map` or `computeItemKey`).
 *
 * @internal Exported for internal use by `VirtualizedThread` — not
 * part of the public API.
 */
export function ThreadItemRenderer({ item, formatToolCallSummary, onApprovalSubmit, submittingApprovalIds, approvalErrors, onBuildFromPlan, onOpenPlan, org, planActionsDisabled, planBuildPending, onRetrySend, onRetryExecution, onEditMessage, }) {
    switch (item.kind) {
        case "message":
            if (item.isFailed) {
                return (_jsx(FailedUserMessage, { message: item.message, onRetry: onRetrySend }));
            }
            return (_jsx(MessageEntry, { message: item.message, className: item.isPending ? "opacity-70" : undefined, isPlanDocument: item.isPlanDocument, interactionMode: item.interactionMode, onEdit: item.isEditable && onEditMessage
                    ? () => onEditMessage(item.message.content)
                    : undefined }));
        case "tool-group":
            return (_jsx(ToolCallGroup, { toolCalls: item.toolCalls, subAgentExecutions: item.subAgentExecutions, formatSummary: formatToolCallSummary, className: "mx-4" }));
        case "sub-agent":
            return (_jsx(SubAgentSection, { subAgentExecution: item.subAgentExecution, className: "mx-4" }));
        case "phase-badge":
            return (_jsx("div", { className: "flex justify-center py-3", children: _jsx(ExecutionPhaseBadge, { phase: item.phase }) }));
        case "execution-error":
            return (_jsx(ExecutionErrorNotice, { error: item.error, retryMessage: item.retryMessage, onRetry: onRetryExecution }));
        case "approval-request":
            return (_jsx(ApprovalCardRow, { pendingApproval: item.pendingApproval, onApprovalSubmit: onApprovalSubmit, isSubmitting: submittingApprovalIds?.has(item.pendingApproval.toolCallId) ?? false, error: approvalErrors?.get(item.pendingApproval.toolCallId) ?? null }));
        case "file-review-record":
            return _jsx(FileReviewRecordRow, { fileChangeSet: item.fileChangeSet });
        case "setup-progress":
            return (_jsx(SetupProgress, { workspaceEntries: item.workspaceEntries, serverPhase: item.serverPhase, isAwaitingResponse: item.isAwaitingResponse }));
        case "context-compacted":
            return _jsx(SummarizationCard, { event: item.event });
        case "todos":
            return _jsx(TodoCard, { todos: item.todos, className: "mx-4" });
        case "plan-completion":
            // When the plan was published as an artifact, show the richer reviewable
            // card (preview / copy / download / implement). Otherwise fall back to the
            // bare Implement CTA (older executions, or a plan that failed to publish).
            // Only the latest plan receives the build action — a superseded plan's
            // card is review-only, so a stale plan can never be implemented from it.
            return item.planArtifact && item.executionId ? (_jsx(PlanArtifactCard, { executionId: item.executionId, artifact: item.planArtifact, title: item.planTitle, org: org, onImplement: item.isLatestPlan ? onBuildFromPlan : undefined, 
                // Every card routes to the plan document tab — the host renders a
                // superseded plan read-only there (it receives the execution id).
                onOpenPlan: onOpenPlan
                    ? () => onOpenPlan(item.executionId)
                    : undefined, disabled: planActionsDisabled, buildPending: planBuildPending })) : (_jsx(PlanCompletionCard, { onImplement: item.isLatestPlan ? onBuildFromPlan : undefined, disabled: planActionsDisabled }));
        case "plan-writing":
            // The live stand-in for a plan the active turn is writing. Emitted only
            // when onOpenPlan is wired (see buildThreadItems' collapseStreamingPlan
            // gate), so the action is always available in practice.
            return (_jsx(PlanStreamingCard, { title: item.planTitle, sizeBytes: item.planSize, onOpenPlan: onOpenPlan ? () => onOpenPlan(item.executionId) : undefined }));
    }
}
// ---------------------------------------------------------------------------
// FailedUserMessage — optimistic turn whose send failed
// ---------------------------------------------------------------------------
/**
 * Renders a user message whose send failed: the message itself stays visible
 * (so the typed text is never lost) with an inline, actionable error beneath
 * it. The error copy is intentionally short — the full reason is surfaced by
 * the consumer's send-error banner; this is the in-thread "Retry" affordance.
 */
function FailedUserMessage({ message, onRetry, }) {
    return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(MessageEntry, { message: message }), _jsxs("div", { role: "alert", className: "mx-4 flex items-center gap-2 text-xs text-destructive", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: "Couldn\u2019t send." }), onRetry && (_jsx("button", { type: "button", onClick: onRetry, className: "shrink-0 rounded font-medium underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: "Retry" }))] })] }));
}
// ---------------------------------------------------------------------------
// ExecutionErrorNotice — server failure reason for a terminal-failed execution
// ---------------------------------------------------------------------------
/**
 * Recognizes a recoverable *interruption* (worker reaped / heartbeat timeout /
 * stall) as opposed to a genuine application failure. The runner stamps these
 * with a stable signature ("[StallTimeoutError]", "Execution interrupted",
 * "Retry or resume."), and the workflow auto-resumes them while recovery cycles
 * remain; by the time one reaches the UI as terminal it is resumable from the
 * session's persisted harness_state_id rather than a dead end.
 */
function isInterruptedError(error) {
    return /\[StallTimeoutError\]|execution interrupted|retry or resume/i.test(error);
}
/**
 * Renders the server-reported failure reason (`AgentExecutionStatus.error`)
 * for an execution that died — typically before producing any messages. The
 * reason can be a long Temporal error, so it is clamped by default with a
 * Show more / Show less toggle.
 *
 * A genuine failure renders as a destructive alert with a Retry. A *recoverable
 * interruption* renders as a neutral notice with a Resume — both resend the
 * originating message, which the server continues from the session's persisted
 * harness_state_id (the same data path; the framing differs so an interruption
 * never looks like a dead-end crash).
 */
function ExecutionErrorNotice({ error, retryMessage, onRetry, }) {
    const [expanded, setExpanded] = useState(false);
    const canRetry = !!onRetry && !!retryMessage;
    const interrupted = isInterruptedError(error);
    return (_jsxs("div", { role: interrupted ? "status" : "alert", className: cn("mx-4 flex flex-col gap-1.5 rounded-md px-3 py-2", interrupted ? "bg-muted" : "bg-destructive-subtle"), children: [_jsx("p", { className: cn("text-xs whitespace-pre-wrap break-words", interrupted ? "text-foreground" : "text-destructive", !expanded && "line-clamp-3"), children: error }), _jsxs("div", { className: "flex items-center gap-3 text-xs", children: [_jsx("button", { type: "button", onClick: () => setExpanded((v) => !v), className: "font-medium text-muted-foreground underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: expanded ? "Show less" : "Show more" }), canRetry && (_jsx("button", { type: "button", onClick: () => onRetry(retryMessage), className: "font-medium text-foreground underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: interrupted ? "Resume" : "Retry" }))] })] }));
}
const ApprovalCardRow = memo(function ApprovalCardRow({ pendingApproval, onApprovalSubmit, isSubmitting, error = null, }) {
    const handleSubmit = useCallback((action, comment) => {
        onApprovalSubmit(pendingApproval.toolCallId, action, comment);
    }, [onApprovalSubmit, pendingApproval.toolCallId]);
    return (_jsx(ApprovalCard, { pendingApproval: pendingApproval, onSubmit: handleSubmit, isSubmitting: isSubmitting, error: error, className: "mx-4" }));
});
// ---------------------------------------------------------------------------
// FileReviewRecordRow — a settled change set's read-only in-thread record
// ---------------------------------------------------------------------------
/**
 * Renders a settled change set as a read-only record at its position in the
 * transcript — what changed and how it was decided ("2 kept · 1 discarded").
 * Never interactive: the pending decision surface is the composer-docked
 * `FileReviewDock`, not a thread item (see {@link insertFileReviewItems}).
 */
const FileReviewRecordRow = memo(function FileReviewRecordRow({ fileChangeSet, }) {
    return (_jsx(FileReviewCard, { fileChangeSet: fileChangeSet, interactive: false, 
        // The thread's stamped edit rows already show every diff in place, so
        // the record renders its compact file-list body — the history never
        // duplicates the transcript's diffs.
        showDiffs: false, className: "mx-4" }));
});
//# sourceMappingURL=MessageThread.js.map