import { c as _c } from "react-compiler-runtime"; import figures from 'figures'; import React, { useMemo, useState } from 'react'; import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js'; import type { ToolUseContext } from 'src/Tool.js'; import type { DeepImmutable } from 'src/types/utils.js'; import type { CommandResultDisplay } from '../../commands.js'; import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'; import { useElapsedTime } from '../../hooks/useElapsedTime.js'; import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; import { Box, Link, Text } from '../../ink.js'; import type { RemoteAgentTaskState } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js'; import { getRemoteTaskSessionUrl } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js'; import { AGENT_TOOL_NAME, LEGACY_AGENT_TOOL_NAME } from '../../tools/AgentTool/constants.js'; import { ASK_USER_QUESTION_TOOL_NAME } from '../../tools/AskUserQuestionTool/prompt.js'; import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../tools/ExitPlanModeTool/constants.js'; import { openBrowser } from '../../utils/browser.js'; import { errorMessage } from '../../utils/errors.js'; import { formatDuration, truncateToWidth } from '../../utils/format.js'; import { toInternalMessages } from '../../utils/messages/mappers.js'; import { EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js'; import { plural } from '../../utils/stringUtils.js'; import { teleportResumeCodeSession } from '../../utils/teleport.js'; import { Select } from '../CustomSelect/select.js'; import { Byline } from '../design-system/Byline.js'; import { Dialog } from '../design-system/Dialog.js'; import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; import { Message } from '../Message.js'; import { formatReviewStageCounts, RemoteSessionProgress } from './RemoteSessionProgress.js'; type Props = { session: DeepImmutable; toolUseContext: ToolUseContext; onDone: (result?: string, options?: { display?: CommandResultDisplay; }) => void; onBack?: () => void; onKill?: () => void; }; // Compact one-line summary: tool name + first meaningful string arg. // Lighter than tool.renderToolUseMessage (no registry lookup / schema parse). // Collapses whitespace so multi-line inputs (e.g. Bash command text) // render on one line. export function formatToolUseSummary(name: string, input: unknown): string { // plan_ready phase is only reached via ExitPlanMode tool if (name === EXIT_PLAN_MODE_V2_TOOL_NAME) { return 'Review the plan in Claude Code on the web'; } if (!input || typeof input !== 'object') return name; // AskUserQuestion: show the question text as a CTA, not the tool name. // Input shape is {questions: [{question, header, options}]}. if (name === ASK_USER_QUESTION_TOOL_NAME && 'questions' in input) { const qs = input.questions; if (Array.isArray(qs) && qs[0] && typeof qs[0] === 'object') { // Prefer question (full text) over header (max-12-char tag). header // is a required schema field so checking it first would make the // question fallback dead code. const q = 'question' in qs[0] && typeof qs[0].question === 'string' && qs[0].question ? qs[0].question : 'header' in qs[0] && typeof qs[0].header === 'string' ? qs[0].header : null; if (q) { const oneLine = q.replace(/\s+/g, ' ').trim(); return `Answer in browser: ${truncateToWidth(oneLine, 50)}`; } } } for (const v of Object.values(input)) { if (typeof v === 'string' && v.trim()) { const oneLine = v.replace(/\s+/g, ' ').trim(); return `${name} ${truncateToWidth(oneLine, 60)}`; } } return name; } const PHASE_LABEL = { needs_input: 'input required', plan_ready: 'ready' } as const; const AGENT_VERB = { needs_input: 'waiting', plan_ready: 'done' } as const; function UltraplanSessionDetail(t0) { const $ = _c(70); const { session, onDone, onBack, onKill } = t0; const running = session.status === "running" || session.status === "pending"; const phase = session.ultraplanPhase; const statusText = running ? phase ? PHASE_LABEL[phase] : "running" : session.status; const elapsedTime = useElapsedTime(session.startTime, running, 1000, 0, session.endTime); let spawns = 0; let calls = 0; let lastBlock = null; for (const msg of session.log) { if (msg.type !== "assistant") { continue; } for (const block of msg.message.content) { if (block.type !== "tool_use") { continue; } calls++; lastBlock = block; if (block.name === AGENT_TOOL_NAME || block.name === LEGACY_AGENT_TOOL_NAME) { spawns++; } } } const t1 = 1 + spawns; let t2; if ($[0] !== lastBlock) { t2 = lastBlock ? formatToolUseSummary(lastBlock.name, lastBlock.input) : null; $[0] = lastBlock; $[1] = t2; } else { t2 = $[1]; } let t3; if ($[2] !== calls || $[3] !== t1 || $[4] !== t2) { t3 = { agentsWorking: t1, toolCalls: calls, lastToolCall: t2 }; $[2] = calls; $[3] = t1; $[4] = t2; $[5] = t3; } else { t3 = $[5]; } const { agentsWorking, toolCalls, lastToolCall } = t3; let t4; if ($[6] !== session.sessionId) { t4 = getRemoteTaskSessionUrl(session.sessionId); $[6] = session.sessionId; $[7] = t4; } else { t4 = $[7]; } const sessionUrl = t4; let t5; if ($[8] !== onBack || $[9] !== onDone) { t5 = onBack ?? (() => onDone("Remote session details dismissed", { display: "system" })); $[8] = onBack; $[9] = onDone; $[10] = t5; } else { t5 = $[10]; } const goBackOrClose = t5; const [confirmingStop, setConfirmingStop] = useState(false); if (confirmingStop) { let t6; if ($[11] === Symbol.for("react.memo_cache_sentinel")) { t6 = () => setConfirmingStop(false); $[11] = t6; } else { t6 = $[11]; } let t7; if ($[12] === Symbol.for("react.memo_cache_sentinel")) { t7 = This will terminate the Claude Code on the web session.; $[12] = t7; } else { t7 = $[12]; } let t8; if ($[13] === Symbol.for("react.memo_cache_sentinel")) { t8 = { label: "Terminate session", value: "stop" as const }; $[13] = t8; } else { t8 = $[13]; } let t9; if ($[14] === Symbol.for("react.memo_cache_sentinel")) { t9 = [t8, { label: "Back", value: "back" as const }]; $[14] = t9; } else { t9 = $[14]; } let t10; if ($[15] !== goBackOrClose || $[16] !== onKill) { t10 = {t7}; $[58] = t22; $[59] = t23; $[60] = t24; } else { t24 = $[60]; } let t25; if ($[61] !== t15 || $[62] !== t16 || $[63] !== t18 || $[64] !== t24) { t25 = {t15}{t16}{t18}{t24}; $[61] = t15; $[62] = t16; $[63] = t18; $[64] = t24; $[65] = t25; } else { t25 = $[65]; } let t26; if ($[66] !== goBackOrClose || $[67] !== t10 || $[68] !== t25) { t26 = {t25}; $[66] = goBackOrClose; $[67] = t10; $[68] = t25; $[69] = t26; } else { t26 = $[69]; } return t26; } const STAGES = ['finding', 'verifying', 'synthesizing'] as const; const STAGE_LABELS: Record<(typeof STAGES)[number], string> = { finding: 'Find', verifying: 'Verify', synthesizing: 'Dedupe' }; // Setup → Find → Verify → Dedupe pipeline. Current stage in cloud teal, // rest dim. When completed, all stages dim with a trailing green ✓. The // "Setup" label shows before the orchestrator writes its first progress // snapshot (container boot + repo clone), so the 0-found display doesn't // look like a hung finder. function StagePipeline(t0) { const $ = _c(15); const { stage, completed, hasProgress } = t0; let t1; if ($[0] !== stage) { t1 = stage ? STAGES.indexOf(stage) : -1; $[0] = stage; $[1] = t1; } else { t1 = $[1]; } const currentIdx = t1; const inSetup = !completed && !hasProgress; let t2; if ($[2] !== inSetup) { t2 = inSetup ? Setup : Setup; $[2] = inSetup; $[3] = t2; } else { t2 = $[3]; } let t3; if ($[4] === Symbol.for("react.memo_cache_sentinel")) { t3 = ; $[4] = t3; } else { t3 = $[4]; } let t4; if ($[5] !== completed || $[6] !== currentIdx || $[7] !== inSetup) { t4 = STAGES.map((s, i) => { const isCurrent = !completed && !inSetup && i === currentIdx; return {i > 0 && }{isCurrent ? {STAGE_LABELS[s]} : {STAGE_LABELS[s]}}; }); $[5] = completed; $[6] = currentIdx; $[7] = inSetup; $[8] = t4; } else { t4 = $[8]; } let t5; if ($[9] !== completed) { t5 = completed && ; $[9] = completed; $[10] = t5; } else { t5 = $[10]; } let t6; if ($[11] !== t2 || $[12] !== t4 || $[13] !== t5) { t6 = {t2}{t3}{t4}{t5}; $[11] = t2; $[12] = t4; $[13] = t5; $[14] = t6; } else { t6 = $[14]; } return t6; } // Stage-appropriate counts line. Running-state formatting delegates to // formatReviewStageCounts (shared with the pill) so the two views can't // drift; completed state is dialog-specific (findings summary). function reviewCountsLine(session: DeepImmutable): string { const p = session.reviewProgress; // No progress data — the orchestrator never wrote a snapshot. Don't // claim "0 findings" when completed; we just don't know. if (!p) return session.status === 'completed' ? 'done' : 'setting up'; const verified = p.bugsVerified; const refuted = p.bugsRefuted ?? 0; if (session.status === 'completed') { const parts = [`${verified} ${plural(verified, 'finding')}`]; if (refuted > 0) parts.push(`${refuted} refuted`); return parts.join(' · '); } return formatReviewStageCounts(p.stage, p.bugsFound, verified, refuted); } type MenuAction = 'open' | 'stop' | 'back' | 'dismiss'; function ReviewSessionDetail(t0) { const $ = _c(56); const { session, onDone, onBack, onKill } = t0; const completed = session.status === "completed"; const running = session.status === "running" || session.status === "pending"; const [confirmingStop, setConfirmingStop] = useState(false); const elapsedTime = useElapsedTime(session.startTime, running, 1000, 0, session.endTime); let t1; if ($[0] !== onDone) { t1 = () => onDone("Remote session details dismissed", { display: "system" }); $[0] = onDone; $[1] = t1; } else { t1 = $[1]; } const handleClose = t1; const goBackOrClose = onBack ?? handleClose; let t2; if ($[2] !== session.sessionId) { t2 = getRemoteTaskSessionUrl(session.sessionId); $[2] = session.sessionId; $[3] = t2; } else { t2 = $[3]; } const sessionUrl = t2; const statusLabel = completed ? "ready" : running ? "running" : session.status; if (confirmingStop) { let t3; if ($[4] === Symbol.for("react.memo_cache_sentinel")) { t3 = () => setConfirmingStop(false); $[4] = t3; } else { t3 = $[4]; } let t4; if ($[5] === Symbol.for("react.memo_cache_sentinel")) { t4 = This archives the remote session and stops local tracking. The review will not complete and any findings so far are discarded.; $[5] = t4; } else { t4 = $[5]; } let t5; if ($[6] === Symbol.for("react.memo_cache_sentinel")) { t5 = { label: "Stop ultrareview", value: "stop" as const }; $[6] = t5; } else { t5 = $[6]; } let t6; if ($[7] === Symbol.for("react.memo_cache_sentinel")) { t6 = [t5, { label: "Back", value: "back" as const }]; $[7] = t6; } else { t6 = $[7]; } let t7; if ($[8] !== goBackOrClose || $[9] !== onKill) { t7 = {t4}; $[45] = handleSelect; $[46] = options; $[47] = t18; } else { t18 = $[47]; } let t19; if ($[48] !== t12 || $[49] !== t17 || $[50] !== t18) { t19 = {t12}{t17}{t18}; $[48] = t12; $[49] = t17; $[50] = t18; $[51] = t19; } else { t19 = $[51]; } let t20; if ($[52] !== goBackOrClose || $[53] !== t19 || $[54] !== t9) { t20 = {t19}; $[52] = goBackOrClose; $[53] = t19; $[54] = t9; $[55] = t20; } else { t20 = $[55]; } return t20; } function _temp(exitState) { return exitState.pending ? Press {exitState.keyName} again to exit : ; } export function RemoteSessionDetailDialog({ session, toolUseContext, onDone, onBack, onKill }: Props): React.ReactNode { const [isTeleporting, setIsTeleporting] = useState(false); const [teleportError, setTeleportError] = useState(null); // Get last few messages from remote session for display. // Scan all messages (not just the last 3 raw entries) because the tail of // the log is often thinking-only blocks that normalise to 'progress' type. // Placed before the early returns so hook call order is stable (Rules of Hooks). // Ultraplan/review sessions never read this — skip the normalize work for them. const lastMessages = useMemo(() => { if (session.isUltraplan || session.isRemoteReview) return []; return normalizeMessages(toInternalMessages(session.log as SDKMessage[])).filter(_ => _.type !== 'progress').slice(-3); }, [session]); if (session.isUltraplan) { return ; } // Review sessions get the stage-pipeline view; everything else keeps the // generic label/value + recent-messages dialog below. if (session.isRemoteReview) { return ; } const handleClose = () => onDone('Remote session details dismissed', { display: 'system' }); // Component-specific shortcuts shown in UI hints (t=teleport, space=dismiss, // left=back). These are state-dependent actions, not standard dialog keybindings. const handleKeyDown = (e: KeyboardEvent) => { if (e.key === ' ') { e.preventDefault(); onDone('Remote session details dismissed', { display: 'system' }); } else if (e.key === 'left' && onBack) { e.preventDefault(); onBack(); } else if (e.key === 't' && !isTeleporting) { e.preventDefault(); void handleTeleport(); } else if (e.key === 'return') { e.preventDefault(); handleClose(); } }; // Handle teleporting to remote session async function handleTeleport(): Promise { setIsTeleporting(true); setTeleportError(null); try { await teleportResumeCodeSession(session.sessionId); } catch (err) { setTeleportError(errorMessage(err)); } finally { setIsTeleporting(false); } } // Truncate title if too long (for display purposes) const displayTitle = truncateToWidth(session.title, 50); // Map TaskStatus to display status (handle 'pending') const displayStatus = session.status === 'pending' ? 'starting' : session.status; return exitState.pending ? Press {exitState.keyName} again to exit : {onBack && } {!isTeleporting && } }> Status:{' '} {displayStatus === 'running' || displayStatus === 'starting' ? {displayStatus} : displayStatus === 'completed' ? {displayStatus} : {displayStatus}} Runtime:{' '} {formatDuration((session.endTime ?? Date.now()) - session.startTime)} Title: {displayTitle} Progress:{' '} Session URL:{' '} {getRemoteTaskSessionUrl(session.sessionId)} {/* Remote session messages section */} {session.log.length > 0 && Recent messages: {lastMessages.map((msg, i) => 0} tools={toolUseContext.options.tools} commands={toolUseContext.options.commands} verbose={toolUseContext.options.verbose} inProgressToolUseIDs={new Set()} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} style="condensed" isTranscriptMode={false} isStatic={true} />)} Showing last {lastMessages.length} of {session.log.length}{' '} messages } {/* Teleport error message */} {teleportError && Teleport failed: {teleportError} } {/* Teleporting status */} {isTeleporting && Teleporting to session…} ; }