"use client";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { create } from "@bufbuild/protobuf";
import { cn } from "@stigmer/theme";
import { getUserMessage } from "@stigmer/sdk";
import { GetArtifactContentRequestSchema, UploadAttachmentRequestSchema, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
import { resolveWorkspaceFileSelection } from "../workspace/resolveWorkspaceFileSelection.js";
import { WorkspaceSurface, } from "../workspace/WorkspaceSurface.js";
import { ResizableSplit } from "../internal/ResizableSplit.js";
import { SelectionStore } from "../internal/store/selection-store.js";
import { useWorkspaceEditors, isVirtualEntryId } from "../internal/store/index.js";
import { ThreadSelectionContext } from "../execution/ThreadSelectionContext.js";
import { useSelectedThreadItem } from "../execution/useThreadSelection.js";
import { MessageThread } from "../execution/MessageThread.js";
import { FileChangeProgressBar } from "../execution/FileChangeProgressBar.js";
import { FileReviewDock } from "../execution/FileReviewDock.js";
import { FilePathContext, } from "../execution/FilePathContext.js";
import { ThreadSkeleton } from "../execution/ThreadSkeleton.js";
import { SessionComposer } from "../composer/index.js";
import { SecretFlowErrorGuide, isSecretFlowError } from "../error/index.js";
import { useStigmer } from "../hooks.js";
import { findLatestSessionPlan, findPlanArtifact, } from "../library/detect-plan-artifact.js";
import { findStreamingPlan } from "../library/detect-streaming-plan.js";
import { useSessionPageFlow } from "./useSessionPageFlow.js";
import { useOpenFileChange } from "./useOpenFileChange.js";
import { usePlanDraft, planDraftKey } from "./usePlanDraft.js";
import { PlanEditor } from "./PlanEditor.js";
import { PlanStreamingDocument } from "./PlanStreamingDocument.js";
import { PLAN_DOCUMENT_ENTRY_ID, PLAN_DOCUMENT_PATH } from "./plan-document.js";
import { useSessionRailViews } from "./useSessionRailViews.js";
import { useSessionPanel } from "./useSessionPanel.js";
import { SessionPanelChip } from "./SessionPanelChip.js";
import { useSessionWriteBacks } from "./useSessionWriteBacks.js";
import { useSessionArtifacts } from "./useSessionArtifacts.js";
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
/**
 * Where the approved plan mounts in the implement execution's workspace —
 * the harnesses' standard attachment inputs directory, where the runner's
 * implement-plan directive points the agent (shared/implement-plan-prompt.ts).
 * Attaching under the plan's own filename (e.g. `feature-x_a1b2c3d4.plan.md`) is what
 * makes the build implement the document the user approved (including in-place
 * edits), not a paraphrase from memory; the runner detects it by the plan
 * filename convention (`isPlanArtifactName`), so the exact name is free to vary.
 */
function approvedPlanMountPath(planFileName) {
    return `.stigmer/inputs/${planFileName}`;
}
/**
 * The build turn's user message — a short human-readable label, NOT the
 * implement instruction. The agent-facing instruction is runner-injected
 * (keyed off `executionConfig.buildFromPlan`). The thread hides the turn
 * entirely from the same flag (the plan card above it is the visible cause);
 * this label exists for surfaces without that treatment (the CLI, execution
 * history, older clients), which show it as-is.
 */
const BUILD_FROM_PLAN_MESSAGE = "Build from plan";
/**
 * Reads the published plan's full text for the build handoff. Refuses a
 * truncated read (content RPC caps at 512 KB): attaching a partial plan would
 * silently implement half a document — the caller falls back to
 * conversation-history implement instead.
 */
async function fetchPlanText(stigmer, plan) {
    const result = await stigmer.agentExecution.getArtifactContent(create(GetArtifactContentRequestSchema, {
        executionId: plan.executionId,
        storageKey: plan.artifact.storageKey,
    }));
    if (result.truncated) {
        throw new Error("plan content truncated — not attaching a partial plan");
    }
    return new TextDecoder().decode(result.content);
}
/**
 * Width and anchoring of the conversation reading column, shared by the
 * thread and the composer/banners block so they can never drift apart.
 *
 * Centered (the classic chat reading view, matching Cursor's agent panel):
 * with the session panel collapsed the column sits in the middle of the
 * row; opening the panel narrows the chat pane below the column max, so
 * the column simply fills it. The thread applies the same geometry via
 * `contentColumn="center"`, which adds the item gutter; the composer/banner
 * block's children carry their own edge padding, so only width and
 * anchoring live here.
 */
const CONVERSATION_COLUMN_CLASS = "mx-auto w-full max-w-3xl";
/**
 * Full-featured agent session viewer — the graph-less analog of
 * `WorkflowExecutionViewer`.
 *
 * Owns `useSessionPageFlow` internally and composes:
 * - **Conversation column** (primary): `MessageThread` + error
 *   banners + `SessionComposer`
 * - **Session panel** (secondary): the unified `WorkspaceSurface` — explorer,
 *   search, read-only editor, and the session facets (Config, Changes,
 *   Artifacts, Usage, Inspect) as injected rail views. Collapsed by default
 *   to a top-right chip; opening a file (tree, search, or a transcript path)
 *   expands it.
 *
 * Connected via `ResizableSplit` with persisted chat width.
 *
 * Thread selection is provided via `ThreadSelectionContext` (Phase 2
 * opt-in) for render-isolated per-item selection.
 *
 * Framework-agnostic — no Next.js, no Tauri, no routing deps. Host
 * apps inject platform-specific values via props (DD-004/DD-016).
 *
 * @example
 * ```tsx
 * // Web (cloud execution)
 * <SessionViewer
 *   sessionId={id}
 *   org={org}
 *   gitHubConnection={gitHubConnection}
 *   enableGitHub
 *   headerActions={<ShareButton sessionId={id} />}
 * />
 *
 * // Desktop (local execution with native picker)
 * <SessionViewer
 *   sessionId={id}
 *   org={org}
 *   enableLocal
 *   onBrowseLocalFolder={browseLocalFolder}
 *   headerActions={<ShareButton sessionId={id} />}
 * />
 * ```
 */
export function SessionViewer({ sessionId, org, gitHubConnection, enableGitHub = true, enableLocal = false, onBrowseLocalFolder, workspaceFileLister, workspaceFileReader, workspaceContentSearcher, getRuntimeEnv, audience = "integrator", headerActions, accessSlot, onApplied, className, }) {
    const flow = useSessionPageFlow({ sessionId, org, getRuntimeEnv });
    const { conv } = flow;
    const isEndUser = audience === "endUser";
    const [modelId, setModelId] = flow.model;
    const [interactionMode, setInteractionMode] = flow.interactionMode;
    const composerRef = useRef(null);
    const selectionStoreRef = useRef(null);
    if (!selectionStoreRef.current) {
        selectionStoreRef.current = new SelectionStore();
    }
    const selectionStore = selectionStoreRef.current;
    const phase = flow.displayExecution?.status?.phase ??
        ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
    const hasPhase = phase !== ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
    // Facet arrival counts: rail badges inside the panel, the chip's dot-count
    // while collapsed. Arrivals never auto-open the panel (badge-only signal).
    const { hasWriteBacks, writeBackCount } = useSessionWriteBacks(flow.allExecutions);
    const { artifactCount } = useSessionArtifacts(flow.allExecutions);
    // The session's current plan (latest published plan.md across executions)
    // and its viewer-owned draft. Ownership at this level is deliberate: the
    // panel unmounts facet content on every view switch, so a facet-local
    // draft would silently lose the user's edits (see usePlanDraft).
    const stigmer = useStigmer();
    const sessionPlan = useMemo(() => findLatestSessionPlan(flow.allExecutions), [flow.allExecutions]);
    const planDraft = usePlanDraft(sessionPlan);
    // The plan the ACTIVE turn is writing right now (the H1 convention — see
    // findStreamingPlan), resolved at this level for the same reason
    // sessionPlan is: the panel's plan tab renders it. Recomputed per stream
    // commit by design — this component already re-renders on every commit
    // (it reads `flow`), and the scan touches only the last AI message.
    const streamingPlan = useMemo(() => {
        const activeExec = conv.activeStreamExecution;
        const live = findStreamingPlan(activeExec);
        const executionId = activeExec?.metadata?.id;
        return live && executionId
            ? { executionId, displayText: live.displayText }
            : undefined;
    }, [conv.activeStreamExecution]);
    const [isBuildingFromPlan, setIsBuildingFromPlan] = useState(false);
    const [planAttachFailed, setPlanAttachFailed] = useState(false);
    // WHICH plan the panel's plan document tab shows. `null` means the session's
    // current (latest) plan — the editable, buildable one; an execution id
    // selects that turn's historical plan, rendered read-only. One tab, host-
    // controlled content (Decision 3 in DD-16).
    const [openPlanExecutionId, setOpenPlanExecutionId] = useState(null);
    // The plan tab's identity across both plan lifecycles: while a plan
    // streams, the in-flight turn owns the tab (`<executionId>:streaming`);
    // once published, the artifact identity (planDraftKey) takes over. Each
    // transition is a NEW identity, so useSessionPanel's planKey trigger
    // auto-opens the tab the moment a plan starts streaming AND (idempotently)
    // when it settles into the published plan.
    const currentPlanKey = sessionPlan ? planDraftKey(sessionPlan) : null;
    const panelPlanKey = streamingPlan
        ? `${streamingPlan.executionId}:streaming`
        : currentPlanKey;
    // A new plan identity (a plan starting to stream, the first published
    // plan, or a refinement) supersedes any historical plan the user had
    // opened: the tab snaps back to "latest" so the auto-open always surfaces
    // the incoming plan. Adjust-state-during-render — own state only, the
    // established idiom.
    const [prevPlanKey, setPrevPlanKey] = useState(panelPlanKey);
    if (panelPlanKey !== prevPlanKey) {
        setPrevPlanKey(panelPlanKey);
        setOpenPlanExecutionId(null);
    }
    // The unified-panel controller: owns the open-editor group store, the
    // open/collapsed state, and the rail-view FSM. Shared with the launcher
    // (DD-016). The editor store is owned here (never subscribed at this level)
    // so opening/switching files re-renders only the panel subtree — the
    // SessionPanelRegion subscribes, not the conversation column — preserving
    // streaming render isolation (DD-009/DD-010, invariant 2).
    const panel = useSessionPanel({
        phase: hasPhase ? phase : null,
        hasChanges: hasWriteBacks,
        planKey: panelPlanKey,
    });
    const handleBuildFromPlan = useCallback(() => {
        // Switch the picker to Agent (so subsequent turns stay in Agent) and submit
        // the implement message through the composer's full pipeline.
        // `interactionMode: "agent"` is passed explicitly to win the same-tick race
        // where the composer prop has not yet re-rendered from "plan".
        setInteractionMode("agent");
        setPlanAttachFailed(false);
        // `buildFromPlan` (not message text) is what tells the runner to inject
        // the implement directive and the thread to hide the turn's message —
        // with or without the attachment (the runner picks the directive variant).
        const submitImplementTurn = (attachments) => {
            composerRef.current?.submit(BUILD_FROM_PLAN_MESSAGE, {
                interactionMode: "agent",
                buildFromPlan: true,
                attachments,
            });
        };
        // No published artifact (the bare-CTA fallback card) — nothing to attach.
        if (!sessionPlan) {
            submitImplementTurn();
            return;
        }
        // Deterministic handoff: upload the APPROVED plan (draft if edited, else
        // the artifact text — one uniform path) and attach it to the implement
        // execution. The published artifact itself stays immutable; the approved
        // copy is a new input on the build turn (edit-as-input provenance).
        setIsBuildingFromPlan(true);
        // The approved copy keeps the published plan's filename, so the mounted
        // input, the download, and the plan card all agree on one name.
        const planFileName = sessionPlan.artifact.name;
        void (async () => {
            try {
                const approvedText = planDraft.readDraft() ?? (await fetchPlanText(stigmer, sessionPlan));
                const response = await stigmer.agentExecution.uploadAttachment(create(UploadAttachmentRequestSchema, {
                    filename: planFileName,
                    content: new TextEncoder().encode(approvedText),
                    contentType: "text/markdown",
                }));
                submitImplementTurn([
                    {
                        filename: planFileName,
                        storageKey: response.storageKey,
                        mountPath: approvedPlanMountPath(planFileName),
                        contentType: "text/markdown",
                    },
                ]);
            }
            catch {
                // Attachment plumbing must never block the build: implement from
                // conversation history alone, and say so (non-blocking notice).
                setPlanAttachFailed(true);
                submitImplementTurn();
            }
            finally {
                setIsBuildingFromPlan(false);
            }
        })();
    }, [setInteractionMode, sessionPlan, planDraft.readDraft, stigmer]);
    // "Open plan" (a thread plan card, or the Artifacts facet's plan.md) → the
    // panel's plan document tab. Opening the LATEST plan clears the historical
    // selection (the tab shows the editable current plan); any other execution
    // id opens that turn's plan read-only in the same tab.
    const handleOpenPlan = useCallback((executionId) => {
        setOpenPlanExecutionId(sessionPlan && executionId === sessionPlan.executionId
            ? null
            : executionId);
        panel.openPlanDocument();
    }, [sessionPlan, panel.openPlanDocument]);
    // Open a transcript tool-call file path in the panel's read-only editor.
    // Resolves the (possibly absolute / subdir-prefixed) path to a
    // repo/root-relative selection the editor can fetch; on a definite hit it
    // opens the file (expanding the panel) and returns `true` (handled),
    // otherwise returns `false` so the path keeps its default copy / GitHub-link
    // behavior. Writing the editor store re-renders only the panel subtree
    // (which subscribes), never this streaming column.
    const handleTranscriptFilePathClick = useCallback((path) => {
        const selection = resolveWorkspaceFileSelection(path, flow.workspace.entries, flow.sandboxWorkspaceRoot);
        if (!selection)
            return false;
        panel.openFile(selection.entryId, selection.path);
        return true;
    }, [flow.workspace.entries, flow.sandboxWorkspaceRoot, panel.openFile]);
    if (conv.isLoading) {
        return (_jsx("div", { className: cn("flex h-full w-full flex-col", className), children: _jsx(ThreadSkeleton, { className: "flex-1 px-0" }) }));
    }
    if (conv.loadError) {
        return _jsx(SessionLoadError, { error: conv.loadError });
    }
    if (!conv.session && !conv.isLoading) {
        return _jsx(SessionStarting, {});
    }
    return (_jsxs("div", { className: cn("relative flex h-full w-full flex-col", className), children: [_jsxs("div", { className: "absolute top-2 right-6 z-10 flex items-center gap-2", children: [headerActions, _jsx(SessionPanelChip, { isOpen: panel.isOpen, onToggle: panel.isOpen ? panel.closePanel : panel.openPanel, badgeCount: writeBackCount + artifactCount })] }), _jsx(ThreadSelectionContext.Provider, { value: selectionStore, children: _jsx(ResizableSplit, { resizablePane: "primary", collapsedPane: panel.isOpen ? "none" : "secondary", defaultSize: 420, minSize: 320, maxSize: 640, storageKey: "stgm-session-chat-width", responsiveCollapse: panel.isOpen ? "primary" : "none", ariaLabel: "Resize chat panel", className: "min-h-0 flex-1", primary: _jsx(ConversationColumn, { flow: flow, modelId: modelId, setModelId: setModelId, interactionMode: interactionMode, setInteractionMode: setInteractionMode, composerRef: composerRef, org: org, gitHubConnection: gitHubConnection, enableGitHub: enableGitHub, enableLocal: enableLocal, onBrowseLocalFolder: onBrowseLocalFolder, onBuildFromPlan: handleBuildFromPlan, onOpenPlan: handleOpenPlan, isBuildingFromPlan: isBuildingFromPlan, planAttachFailed: planAttachFailed, onDismissPlanAttachFailed: () => setPlanAttachFailed(false), onFilePathClick: handleTranscriptFilePathClick, isEndUser: isEndUser }), secondary: panel.isOpen ? (_jsx(SessionPanelRegion, { flow: flow, org: org, panel: panel, accessSlot: accessSlot, onApplied: onApplied, onImplementPlan: handleBuildFromPlan, implementPlanDisabled: !conv.canSendFollowUp || isBuildingFromPlan, sessionPlan: sessionPlan, streamingPlan: streamingPlan, planDraft: planDraft, openPlanExecutionId: openPlanExecutionId, onOpenPlan: handleOpenPlan, enableLocal: enableLocal, onBrowseLocalFolder: onBrowseLocalFolder, workspaceFileLister: workspaceFileLister, workspaceFileReader: workspaceFileReader, workspaceContentSearcher: workspaceContentSearcher, isEndUser: isEndUser })) : null }) })] }));
}
const ConversationColumn = memo(function ConversationColumn({ flow, modelId, setModelId, interactionMode, setInteractionMode, composerRef, org, gitHubConnection, enableGitHub, enableLocal, onBrowseLocalFolder, onBuildFromPlan, onOpenPlan, isBuildingFromPlan, planAttachFailed, onDismissPlanAttachFailed, onFilePathClick, isEndUser, }) {
    const { conv } = flow;
    // Approval failures are NOT folded into this banner: they now surface in-card,
    // beside the exact gate that failed (see MessageThread's approvalErrors →
    // ApprovalCard), matching the file-review card. A single banner cannot name
    // which of several concurrent gates failed. The scalar conv.approvalError
    // stays available for headless/`ink` consumers.
    const sendError = flow.submitError ?? conv.sendError ?? conv.stopError;
    // Retry a terminal-failed execution by resending its originating message
    // through the full submit pipeline (agent override, runtime-env, workspace).
    const onRetryExecution = useCallback((message) => {
        void flow.handleSubmit(message);
    }, [flow.handleSubmit]);
    // Stop the in-flight turn (graceful cancel, escalating to terminate on a
    // repeat press). Only wired while the active execution is stoppable.
    const handleStop = useCallback(() => {
        void conv.stop();
    }, [conv.stop]);
    // Edit-and-resubmit: stop the in-flight turn and pre-fill the composer with
    // the original text. The append-only execution log can't be rewritten, so
    // the user reviews the prefilled message and resubmits it as a NEW execution
    // through the normal Send pipeline. The cancelled turn stays in history with
    // its phase badge — an honest record rather than a silent edit.
    const handleEditMessage = useCallback((text) => {
        void conv.stop();
        composerRef.current?.setMessage(text);
        composerRef.current?.focus();
    }, [conv.stop, composerRef]);
    // The composer-docked FileReviewDock sits OUTSIDE MessageThread, so the
    // thread's own FilePathContext provider does not reach it. This provider
    // gives the dock's file list the same path resolution and click routing
    // (open in the panel's viewer; GitHub/copy fallback) as the transcript —
    // one click behavior for a path everywhere in the session. Memoized so the
    // dock's memoized cards are not invalidated by unrelated renders (DD-010).
    const dockFilePathCtx = useMemo(() => ({
        workspaceEntries: conv.workspaceEntries ?? [],
        onFilePathClick,
    }), [conv.workspaceEntries, onFilePathClick]);
    return (_jsxs("div", { className: "flex h-full min-w-0 flex-col", children: [_jsx(MessageThread, { executions: conv.completedExecutions, activeStreamExecution: conv.activeStreamExecution, pendingUserMessage: conv.pendingUserMessage, pendingMessageFailed: !!conv.sendError && !!conv.pendingUserMessage, onRetrySend: conv.retryLastSend, onRetryExecution: onRetryExecution, onApprovalSubmit: flow.submitApproval, submittingApprovalIds: conv.submittingApprovalIds, approvalErrors: conv.approvalErrors, showFileReviewRecords: true, onEditMessage: conv.isStoppable ? handleEditMessage : undefined, workspaceEntries: conv.workspaceEntries, onFilePathClick: onFilePathClick, sandboxWorkspaceRoot: flow.sandboxWorkspaceRoot, onBuildFromPlan: onBuildFromPlan, onOpenPlan: onOpenPlan, org: org, planActionsDisabled: !conv.canSendFollowUp || isBuildingFromPlan, planBuildPending: isBuildingFromPlan, contentColumn: "center", className: "flex-1" }), _jsxs("div", { className: CONVERSATION_COLUMN_CLASS, children: [planAttachFailed && (_jsx(PlanAttachFailedNotice, { onDismiss: onDismissPlanAttachFailed })), conv.isReconnecting && _jsx(ReconnectingIndicator, {}), conv.connectTimedOut && (_jsx(ConnectTimedOutBanner, { onRetry: conv.reconnectStream })), conv.isSlow && !conv.connectTimedOut && _jsx(SlowIndicator, {}), conv.streamError && (_jsx(StreamErrorBanner, { error: conv.streamError, onReconnect: conv.reconnectStream })), sendError && _jsx(SendErrorBanner, { error: sendError }), flow.autoApproveAll && (_jsx(AutoApproveIndicator, { onTurnOff: () => flow.setAutoApproveAll(false) })), _jsxs(FilePathContext.Provider, { value: dockFilePathCtx, children: [_jsx(FileChangeProgressBar, { progress: conv.fileChangeProgress }), _jsx(FileReviewDock, { changeSets: conv.fileChangeSets, onSubmit: conv.submitFileDecision, submittingDecisionKeys: conv.submittingFileDecisionKeys, decisionErrors: conv.fileDecisionErrors })] }), _jsx(SessionComposer, { ref: composerRef, onSubmit: flow.handleSubmit, isSubmitting: conv.isSending, disabled: !conv.canSendFollowUp, onStop: conv.isStoppable ? handleStop : undefined, isStopping: conv.isStopping, org: org, harness: flow.harness, defaultModelId: modelId, onModelChange: setModelId, interactionMode: interactionMode, onInteractionModeChange: setInteractionMode, showInteractionModePicker: true, workspace: flow.workspace, gitHubConnection: gitHubConnection, enableGitHub: enableGitHub, enableLocal: enableLocal, onBrowseLocalFolder: onBrowseLocalFolder, agentRef: flow.agentRef, onAgentRefChange: flow.setAgentRef, onAgentResolutionChange: flow.setResolution, isDefaultAgent: flow.isDefaultAgent, lockAgent: isEndUser, mcpServerUsages: isEndUser ? undefined : flow.mcpServerUsages, onMcpServerUsagesChange: isEndUser ? undefined : flow.setMcpServerUsages, skillRefs: isEndUser ? undefined : flow.skillRefs, onSkillRefsChange: isEndUser ? undefined : flow.setSkillRefs, sessionVariables: isEndUser ? undefined : flow.sessionVariables, className: "px-4 py-3" })] })] }));
});
function SessionPanelRegion({ flow, org, panel, accessSlot, onApplied, onImplementPlan, implementPlanDisabled, sessionPlan, streamingPlan, planDraft, openPlanExecutionId, onOpenPlan, enableLocal, onBrowseLocalFolder, workspaceFileLister, workspaceFileReader, workspaceContentSearcher, isEndUser, }) {
    const selectedItem = useSelectedThreadItem();
    const { editors, activeKey, activeFile, reveal } = useWorkspaceEditors(panel.editorsStore);
    // Honor a pending jump-to-line reveal only while it targets the active editor
    // (an EditorReveal is structurally a RevealTarget). Switching tabs changes
    // activeKey, so a stale reveal for another tab is naturally ignored.
    const activeReveal = reveal && reveal.key === activeKey ? reveal : undefined;
    // This region is the level that subscribes to thread selection; report it to
    // the controller so an open panel can auto-surface the Inspect view. (A
    // collapsed panel never sees — and deliberately never reacts to — selection.)
    useEffect(() => {
        panel.notifySelection(selectedItem);
    }, [selectedItem, panel.notifySelection]);
    // Correlate the active file with its session change for diff-as-default
    // (DD-06 parity with the transcript's rendering of the same change). A
    // virtual document (the plan tab) is not a workspace file — pass `null` so
    // the hook's no-file guard skips the net-change fold entirely, rather than
    // relying on the correlation harmlessly missing the sentinel id.
    const openFileChange = useOpenFileChange(activeFile && !isVirtualEntryId(activeFile.entryId) ? activeFile : null, flow.allExecutions, flow.workspace.entries, flow.sandboxWorkspaceRoot);
    // Resolve WHICH plan the plan document tab shows: the latest (editable,
    // buildable, draft-backed) unless a historical execution id was opened —
    // that plan renders read-only. A stale id (execution gone, or its plan
    // artifact missing) degrades to the latest rather than an empty tab.
    const openPlan = useMemo(() => {
        if (openPlanExecutionId === null ||
            openPlanExecutionId === sessionPlan?.executionId) {
            return sessionPlan;
        }
        const execution = flow.allExecutions.find((e) => e.metadata?.id === openPlanExecutionId);
        const artifact = findPlanArtifact(execution);
        return artifact
            ? { executionId: openPlanExecutionId, artifact }
            : sessionPlan;
    }, [openPlanExecutionId, sessionPlan, flow.allExecutions]);
    // The plan document tab (SurfaceVirtualDocument): the panel's editor-area
    // rendering of the session's plan. Three states, in precedence order:
    // a STREAMING plan owns the tab (live document, no actions); otherwise the
    // resolved `openPlan` renders in the editor (keyed by plan identity so
    // switching plans resets view state cleanly); otherwise an honest empty
    // notice (DD-006) — reachable only when a streaming plan auto-opened the
    // tab and its turn then ended without publishing, with no earlier plan to
    // fall back to.
    const openPlanIsLatest = openPlan?.executionId === sessionPlan?.executionId;
    const virtualDocuments = useMemo(() => {
        return [
            {
                entryId: PLAN_DOCUMENT_ENTRY_ID,
                path: PLAN_DOCUMENT_PATH,
                content: streamingPlan ? (_jsx(PlanStreamingDocument, { displayText: streamingPlan.displayText }, `streaming-${streamingPlan.executionId}`)) : openPlan ? (_jsx(PlanEditor, { plan: openPlan, draft: openPlanIsLatest ? planDraft : undefined, onBuildFromPlan: openPlanIsLatest ? onImplementPlan : undefined, buildDisabled: implementPlanDisabled, readOnly: !openPlanIsLatest }, planDraftKey(openPlan))) : (_jsx(PlanUnavailableNotice, {})),
            },
        ];
    }, [
        streamingPlan,
        openPlan,
        openPlanIsLatest,
        planDraft,
        onImplementPlan,
        implementPlanDisabled,
    ]);
    const handleRemoveAgent = useCallback(() => {
        flow.setAgentRef(null);
        flow.setResolution(null);
    }, [flow.setAgentRef, flow.setResolution]);
    const handleRemoveMcp = useCallback((ref) => {
        flow.setMcpServerUsages(flow.mcpServerUsages.filter((u) => !(u.mcpServerRef.org === ref.org && u.mcpServerRef.slug === ref.slug)));
    }, [flow.mcpServerUsages, flow.setMcpServerUsages]);
    const handleRemoveSkill = useCallback((ref) => {
        flow.setSkillRefs(flow.skillRefs.filter((r) => !(r.org === ref.org && r.slug === ref.slug)));
    }, [flow.skillRefs, flow.setSkillRefs]);
    const sessionConfig = useMemo(() => ({
        agentRef: flow.agentRef,
        isDefaultAgent: flow.isDefaultAgent,
        mcpServerUsages: flow.mcpServerUsages,
        skillRefs: flow.skillRefs,
        sessionVariables: flow.sessionVariables,
        harness: flow.harness,
        executionTarget: flow.executionTarget,
        modelId: flow.model[0],
        // End users see the configuration but cannot strip it — the Config
        // facet renders read-only without mutation callbacks (DD-011).
        mutations: isEndUser
            ? undefined
            : {
                onRemoveAgent: flow.isDefaultAgent ? undefined : handleRemoveAgent,
                onRemoveMcp: handleRemoveMcp,
                onRemoveSkill: handleRemoveSkill,
            },
        accessSlot,
    }), [
        flow.agentRef, flow.isDefaultAgent, flow.mcpServerUsages, flow.skillRefs,
        flow.sessionVariables, flow.harness, flow.executionTarget, flow.model,
        isEndUser, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
        accessSlot,
    ]);
    // The session facets (Config / Changes / Artifacts / Usage / Inspect) as
    // injected rail views — the full inspector feature set inside one panel.
    const railViews = useSessionRailViews({
        allExecutions: flow.allExecutions,
        org,
        sessionConfig,
        selectedItem,
        onApplied,
        onImplementPlan,
        onOpenPlan,
    });
    // Explorer-footer folder attach (desktop only — needs the native picker).
    // Same wiring the retired Workspace tab used.
    const canAddLocalFolder = enableLocal && !!onBrowseLocalFolder;
    const handleAddLocalFolder = useCallback(async () => {
        const path = await onBrowseLocalFolder?.();
        if (path)
            flow.workspace.addLocalPath(path);
    }, [onBrowseLocalFolder, flow.workspace.addLocalPath]);
    return (_jsx(WorkspaceSurface, { entries: flow.workspace.entries, lister: workspaceFileLister, reader: workspaceFileReader, searcher: workspaceContentSearcher, view: panel.view, onViewChange: panel.setView, extraViews: railViews, virtualDocuments: virtualDocuments, onRemoveEntry: flow.workspace.remove, onAddLocalFolder: canAddLocalFolder ? handleAddLocalFolder : undefined, editors: editors, selectedFile: activeFile, onOpenFile: panel.openFile, onActivateEditor: panel.activateEditor, onPinEditor: panel.pinEditor, onCloseEditor: panel.closeEditor, onCollapse: panel.closePanel, change: openFileChange ?? undefined, reveal: activeReveal, className: "h-full" }));
}
// ---------------------------------------------------------------------------
// Local sub-components (error states)
// ---------------------------------------------------------------------------
function SessionLoadError({ error }) {
    return (_jsx("div", { className: "flex min-h-[60vh] flex-col items-center justify-center px-4", children: _jsxs("div", { className: "w-full max-w-sm space-y-6 text-center", children: [_jsx("div", { className: "mx-auto flex size-12 items-center justify-center rounded-full bg-destructive-subtle", children: _jsx(ErrorTriangleIcon, {}) }), _jsxs("div", { className: "space-y-2", children: [_jsx("h1", { className: "text-lg font-semibold", children: "Failed to load session" }), _jsx("p", { className: "text-sm text-muted-foreground", children: getUserMessage(error) })] }), _jsx("button", { type: "button", onClick: () => window.location.reload(), className: "inline-flex items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-sm font-medium hover:bg-muted", children: "Try again" })] }) }));
}
function SessionStarting() {
    return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "space-y-2 text-center", children: [_jsx(LoaderIcon, {}), _jsx("p", { className: "text-sm text-muted-foreground", children: "Starting session\u2026" })] }) }));
}
/**
 * Low-weight, always-visible indicator shown while the session-scoped
 * auto-approve preference is active. The "Turn off" control reverts the
 * preference in one click — the safety affordance for "Approve & don't ask
 * again". Nothing about approvals appears until the user opts in at a gate.
 */
function AutoApproveIndicator({ onTurnOff }) {
    return (_jsxs("div", { role: "status", className: "flex items-center gap-2 border-t border-border-muted px-4 py-1.5 text-xs text-muted-foreground", children: [_jsx(ShieldCheckIcon, {}), _jsx("span", { className: "min-w-0 flex-1 truncate", children: "Auto-approving tool calls for this session" }), _jsx("button", { type: "button", onClick: onTurnOff, className: "shrink-0 rounded font-medium text-foreground underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: "Turn off" })] }));
}
/**
 * The plan document tab's empty state — reachable only when a streaming plan
 * auto-opened the tab and its turn then ended without publishing (stopped or
 * failed) while the session has no earlier published plan to fall back to.
 * The partial plan reverts to the conversation (the thread un-collapses it),
 * so the notice points there. Never a blank pane (DD-006).
 */
function PlanUnavailableNotice() {
    return (_jsxs("div", { role: "status", className: "mx-auto flex w-full max-w-3xl flex-col items-center gap-1 px-4 py-8 text-center", children: [_jsx("p", { className: "text-xs font-medium text-foreground", children: "This turn ended before a plan was completed." }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Any partial plan text remains in the conversation." })] }));
}
/**
 * Non-blocking notice for a build that fell back to message-only implement
 * (the approved plan.md could not be uploaded/attached). The build itself
 * proceeded — the agent follows the plan from conversation history — so this
 * is a status, not an alert, and it is dismissible.
 */
function PlanAttachFailedNotice({ onDismiss }) {
    return (_jsxs("div", { role: "status", className: "flex items-center gap-2 border-t border-border-muted px-4 py-1.5 text-xs text-muted-foreground", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: "Couldn\u2019t attach plan.md \u2014 the agent will follow the plan from the conversation instead." }), _jsx("button", { type: "button", onClick: onDismiss, className: "shrink-0 rounded font-medium text-foreground underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: "Dismiss" })] }));
}
function SendErrorBanner({ error }) {
    if (isSecretFlowError(error)) {
        return _jsx(SecretFlowErrorGuide, { error: error, className: "mx-4 my-2" });
    }
    return (_jsx("div", { role: "alert", className: "border-t border-border px-4 py-2 text-xs text-destructive", children: getUserMessage(error) }));
}
function ReconnectingIndicator() {
    return (_jsxs("div", { role: "status", "aria-live": "polite", className: "flex items-center gap-2 border-t border-border bg-muted px-4 py-2 text-sm text-muted-foreground", children: [_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "shrink-0 animate-spin motion-reduce:animate-none", "aria-hidden": "true", children: _jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }), _jsx("span", { className: "truncate", children: "Reconnecting\u2026" })] }));
}
/**
 * Actionable banner for the connect-timeout watchdog: the stream opened but
 * never delivered a first snapshot — the agent hasn't started. Mirrors
 * {@link StreamErrorBanner}'s shape (message + Retry) since both resolve via
 * the same `reconnect()` path, but its copy names the specific failure.
 */
function ConnectTimedOutBanner({ onRetry }) {
    return (_jsxs("div", { role: "alert", className: "flex items-center gap-3 border-t border-border bg-muted px-4 py-2.5", children: [_jsx("p", { className: "min-w-0 flex-1 truncate text-sm text-muted-foreground", children: "The agent hasn\u2019t started yet." }), _jsx("button", { type: "button", onClick: onRetry, className: "inline-flex shrink-0 items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs font-medium hover:bg-card", children: "Retry" })] }));
}
/**
 * Low-weight informational hint for the slow-stall watchdog: a live stream has
 * gone quiet longer than usual. Never an error and offers no action — the
 * stream is still expected to resume on its own.
 */
function SlowIndicator() {
    return (_jsxs("div", { role: "status", "aria-live": "polite", className: "flex items-center gap-2 border-t border-border-muted px-4 py-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "relative flex h-2 w-2 shrink-0", "aria-hidden": "true", children: [_jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-muted-foreground opacity-75 motion-reduce:animate-none" }), _jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-muted-foreground" })] }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: "Still working \u2014 this is taking longer than usual." })] }));
}
function StreamErrorBanner({ error, onReconnect, }) {
    return (_jsxs("div", { role: "alert", className: "flex items-center gap-3 border-t border-border bg-muted px-4 py-2.5", children: [_jsx("p", { className: "min-w-0 flex-1 truncate text-sm text-muted-foreground", children: getUserMessage(error) }), _jsx("button", { type: "button", onClick: onReconnect, className: "inline-flex shrink-0 items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs font-medium hover:bg-card", children: "Reconnect" })] }));
}
// ---------------------------------------------------------------------------
// Inline SVG icons (SDK independence — no lucide dependency)
// ---------------------------------------------------------------------------
function ErrorTriangleIcon() {
    return (_jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-destructive", children: [_jsx("path", { d: "M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" }), _jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }), _jsx("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })] }));
}
function LoaderIcon() {
    return (_jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "mx-auto animate-spin text-muted-foreground", children: _jsx("path", { d: "M10 2a8 8 0 0 1 0 16", strokeLinecap: "round" }) }));
}
function ShieldCheckIcon() {
    return (_jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "shrink-0 text-success", "aria-hidden": "true", children: [_jsx("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }), _jsx("path", { d: "m9 12 2 2 4-4" })] }));
}
//# sourceMappingURL=SessionViewer.js.map