Inline base64 source maps had been checked into tracked src files. This strips those comments from the repository without changing runtime behavior or adding ongoing guardrails, per the requested one-time cleanup scope. Constraint: Keep this change limited to tracked source cleanup only Rejected: Add CI/source verification guard | user requested one-time cleanup only Confidence: high Scope-risk: narrow Reversibility: clean Directive: If these directives reappear, fix the producing transform instead of reintroducing repo-side cleanup code Tested: rg -n "sourceMappingURL" ., bun run smoke, bun run verify:privacy, bun run test:provider, npm run test:provider-recommendation Not-tested: bun run typecheck (repository has many pre-existing unrelated failures) Co-authored-by: anandh8x <test@example.com>
904 lines
26 KiB
TypeScript
904 lines
26 KiB
TypeScript
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<RemoteAgentTaskState>;
|
|
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 = <Text dimColor={true}>This will terminate the Claude Code on the web session.</Text>;
|
|
$[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 = <Dialog title="Stop ultraplan?" onCancel={t6} color="background"><Box flexDirection="column" gap={1}>{t7}<Select options={t9} onChange={v => {
|
|
if (v === "stop") {
|
|
onKill?.();
|
|
goBackOrClose();
|
|
} else {
|
|
setConfirmingStop(false);
|
|
}
|
|
}} /></Box></Dialog>;
|
|
$[15] = goBackOrClose;
|
|
$[16] = onKill;
|
|
$[17] = t10;
|
|
} else {
|
|
t10 = $[17];
|
|
}
|
|
return t10;
|
|
}
|
|
const t6 = phase === "plan_ready" ? DIAMOND_FILLED : DIAMOND_OPEN;
|
|
let t7;
|
|
if ($[18] !== t6) {
|
|
t7 = <Text color="background">{t6}{" "}</Text>;
|
|
$[18] = t6;
|
|
$[19] = t7;
|
|
} else {
|
|
t7 = $[19];
|
|
}
|
|
let t8;
|
|
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
|
|
t8 = <Text bold={true}>ultraplan</Text>;
|
|
$[20] = t8;
|
|
} else {
|
|
t8 = $[20];
|
|
}
|
|
let t9;
|
|
if ($[21] !== elapsedTime || $[22] !== statusText) {
|
|
t9 = <Text dimColor={true}>{" \xB7 "}{elapsedTime}{" \xB7 "}{statusText}</Text>;
|
|
$[21] = elapsedTime;
|
|
$[22] = statusText;
|
|
$[23] = t9;
|
|
} else {
|
|
t9 = $[23];
|
|
}
|
|
let t10;
|
|
if ($[24] !== t7 || $[25] !== t9) {
|
|
t10 = <Text>{t7}{t8}{t9}</Text>;
|
|
$[24] = t7;
|
|
$[25] = t9;
|
|
$[26] = t10;
|
|
} else {
|
|
t10 = $[26];
|
|
}
|
|
let t11;
|
|
if ($[27] !== phase) {
|
|
t11 = phase === "plan_ready" && <Text color="success">{figures.tick} </Text>;
|
|
$[27] = phase;
|
|
$[28] = t11;
|
|
} else {
|
|
t11 = $[28];
|
|
}
|
|
let t12;
|
|
if ($[29] !== agentsWorking) {
|
|
t12 = plural(agentsWorking, "agent");
|
|
$[29] = agentsWorking;
|
|
$[30] = t12;
|
|
} else {
|
|
t12 = $[30];
|
|
}
|
|
const t13 = phase ? AGENT_VERB[phase] : "working";
|
|
let t14;
|
|
if ($[31] !== toolCalls) {
|
|
t14 = plural(toolCalls, "call");
|
|
$[31] = toolCalls;
|
|
$[32] = t14;
|
|
} else {
|
|
t14 = $[32];
|
|
}
|
|
let t15;
|
|
if ($[33] !== agentsWorking || $[34] !== t11 || $[35] !== t12 || $[36] !== t13 || $[37] !== t14 || $[38] !== toolCalls) {
|
|
t15 = <Text>{t11}{agentsWorking} {t12}{" "}{t13} · {toolCalls} tool{" "}{t14}</Text>;
|
|
$[33] = agentsWorking;
|
|
$[34] = t11;
|
|
$[35] = t12;
|
|
$[36] = t13;
|
|
$[37] = t14;
|
|
$[38] = toolCalls;
|
|
$[39] = t15;
|
|
} else {
|
|
t15 = $[39];
|
|
}
|
|
let t16;
|
|
if ($[40] !== lastToolCall) {
|
|
t16 = lastToolCall && <Text dimColor={true}>{lastToolCall}</Text>;
|
|
$[40] = lastToolCall;
|
|
$[41] = t16;
|
|
} else {
|
|
t16 = $[41];
|
|
}
|
|
let t17;
|
|
if ($[42] !== sessionUrl) {
|
|
t17 = <Text dimColor={true}>{sessionUrl}</Text>;
|
|
$[42] = sessionUrl;
|
|
$[43] = t17;
|
|
} else {
|
|
t17 = $[43];
|
|
}
|
|
let t18;
|
|
if ($[44] !== sessionUrl || $[45] !== t17) {
|
|
t18 = <Link url={sessionUrl}>{t17}</Link>;
|
|
$[44] = sessionUrl;
|
|
$[45] = t17;
|
|
$[46] = t18;
|
|
} else {
|
|
t18 = $[46];
|
|
}
|
|
let t19;
|
|
if ($[47] === Symbol.for("react.memo_cache_sentinel")) {
|
|
t19 = {
|
|
label: "Review in Claude Code on the web",
|
|
value: "open" as const
|
|
};
|
|
$[47] = t19;
|
|
} else {
|
|
t19 = $[47];
|
|
}
|
|
let t20;
|
|
if ($[48] !== onKill || $[49] !== running) {
|
|
t20 = onKill && running ? [{
|
|
label: "Stop ultraplan",
|
|
value: "stop" as const
|
|
}] : [];
|
|
$[48] = onKill;
|
|
$[49] = running;
|
|
$[50] = t20;
|
|
} else {
|
|
t20 = $[50];
|
|
}
|
|
let t21;
|
|
if ($[51] === Symbol.for("react.memo_cache_sentinel")) {
|
|
t21 = {
|
|
label: "Back",
|
|
value: "back" as const
|
|
};
|
|
$[51] = t21;
|
|
} else {
|
|
t21 = $[51];
|
|
}
|
|
let t22;
|
|
if ($[52] !== t20) {
|
|
t22 = [t19, ...t20, t21];
|
|
$[52] = t20;
|
|
$[53] = t22;
|
|
} else {
|
|
t22 = $[53];
|
|
}
|
|
let t23;
|
|
if ($[54] !== goBackOrClose || $[55] !== onDone || $[56] !== sessionUrl) {
|
|
t23 = v_0 => {
|
|
switch (v_0) {
|
|
case "open":
|
|
{
|
|
openBrowser(sessionUrl);
|
|
onDone();
|
|
return;
|
|
}
|
|
case "stop":
|
|
{
|
|
setConfirmingStop(true);
|
|
return;
|
|
}
|
|
case "back":
|
|
{
|
|
goBackOrClose();
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
$[54] = goBackOrClose;
|
|
$[55] = onDone;
|
|
$[56] = sessionUrl;
|
|
$[57] = t23;
|
|
} else {
|
|
t23 = $[57];
|
|
}
|
|
let t24;
|
|
if ($[58] !== t22 || $[59] !== t23) {
|
|
t24 = <Select options={t22} onChange={t23} />;
|
|
$[58] = t22;
|
|
$[59] = t23;
|
|
$[60] = t24;
|
|
} else {
|
|
t24 = $[60];
|
|
}
|
|
let t25;
|
|
if ($[61] !== t15 || $[62] !== t16 || $[63] !== t18 || $[64] !== t24) {
|
|
t25 = <Box flexDirection="column" gap={1}>{t15}{t16}{t18}{t24}</Box>;
|
|
$[61] = t15;
|
|
$[62] = t16;
|
|
$[63] = t18;
|
|
$[64] = t24;
|
|
$[65] = t25;
|
|
} else {
|
|
t25 = $[65];
|
|
}
|
|
let t26;
|
|
if ($[66] !== goBackOrClose || $[67] !== t10 || $[68] !== t25) {
|
|
t26 = <Dialog title={t10} onCancel={goBackOrClose} color="background">{t25}</Dialog>;
|
|
$[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 ? <Text color="background">Setup</Text> : <Text dimColor={true}>Setup</Text>;
|
|
$[2] = inSetup;
|
|
$[3] = t2;
|
|
} else {
|
|
t2 = $[3];
|
|
}
|
|
let t3;
|
|
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
|
t3 = <Text dimColor={true}> → </Text>;
|
|
$[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 <React.Fragment key={s}>{i > 0 && <Text dimColor={true}> → </Text>}{isCurrent ? <Text color="background">{STAGE_LABELS[s]}</Text> : <Text dimColor={true}>{STAGE_LABELS[s]}</Text>}</React.Fragment>;
|
|
});
|
|
$[5] = completed;
|
|
$[6] = currentIdx;
|
|
$[7] = inSetup;
|
|
$[8] = t4;
|
|
} else {
|
|
t4 = $[8];
|
|
}
|
|
let t5;
|
|
if ($[9] !== completed) {
|
|
t5 = completed && <Text color="success"> ✓</Text>;
|
|
$[9] = completed;
|
|
$[10] = t5;
|
|
} else {
|
|
t5 = $[10];
|
|
}
|
|
let t6;
|
|
if ($[11] !== t2 || $[12] !== t4 || $[13] !== t5) {
|
|
t6 = <Text>{t2}{t3}{t4}{t5}</Text>;
|
|
$[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<RemoteAgentTaskState>): 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 = <Text dimColor={true}>This archives the remote session and stops local tracking. The review will not complete and any findings so far are discarded.</Text>;
|
|
$[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 = <Dialog title="Stop ultrareview?" onCancel={t3} color="background"><Box flexDirection="column" gap={1}>{t4}<Select options={t6} onChange={v => {
|
|
if (v === "stop") {
|
|
onKill?.();
|
|
goBackOrClose();
|
|
} else {
|
|
setConfirmingStop(false);
|
|
}
|
|
}} /></Box></Dialog>;
|
|
$[8] = goBackOrClose;
|
|
$[9] = onKill;
|
|
$[10] = t7;
|
|
} else {
|
|
t7 = $[10];
|
|
}
|
|
return t7;
|
|
}
|
|
let t3;
|
|
if ($[11] !== completed || $[12] !== onKill || $[13] !== running) {
|
|
t3 = completed ? [{
|
|
label: "Open in Claude Code on the web",
|
|
value: "open"
|
|
}, {
|
|
label: "Dismiss",
|
|
value: "dismiss"
|
|
}] : [{
|
|
label: "Open in Claude Code on the web",
|
|
value: "open"
|
|
}, ...(onKill && running ? [{
|
|
label: "Stop ultrareview",
|
|
value: "stop" as const
|
|
}] : []), {
|
|
label: "Back",
|
|
value: "back"
|
|
}];
|
|
$[11] = completed;
|
|
$[12] = onKill;
|
|
$[13] = running;
|
|
$[14] = t3;
|
|
} else {
|
|
t3 = $[14];
|
|
}
|
|
const options = t3;
|
|
let t4;
|
|
if ($[15] !== goBackOrClose || $[16] !== handleClose || $[17] !== onDone || $[18] !== sessionUrl) {
|
|
t4 = action => {
|
|
bb45: switch (action) {
|
|
case "open":
|
|
{
|
|
openBrowser(sessionUrl);
|
|
onDone();
|
|
break bb45;
|
|
}
|
|
case "stop":
|
|
{
|
|
setConfirmingStop(true);
|
|
break bb45;
|
|
}
|
|
case "back":
|
|
{
|
|
goBackOrClose();
|
|
break bb45;
|
|
}
|
|
case "dismiss":
|
|
{
|
|
handleClose();
|
|
}
|
|
}
|
|
};
|
|
$[15] = goBackOrClose;
|
|
$[16] = handleClose;
|
|
$[17] = onDone;
|
|
$[18] = sessionUrl;
|
|
$[19] = t4;
|
|
} else {
|
|
t4 = $[19];
|
|
}
|
|
const handleSelect = t4;
|
|
const t5 = completed ? DIAMOND_FILLED : DIAMOND_OPEN;
|
|
let t6;
|
|
if ($[20] !== t5) {
|
|
t6 = <Text color="background">{t5}{" "}</Text>;
|
|
$[20] = t5;
|
|
$[21] = t6;
|
|
} else {
|
|
t6 = $[21];
|
|
}
|
|
let t7;
|
|
if ($[22] === Symbol.for("react.memo_cache_sentinel")) {
|
|
t7 = <Text bold={true}>ultrareview</Text>;
|
|
$[22] = t7;
|
|
} else {
|
|
t7 = $[22];
|
|
}
|
|
let t8;
|
|
if ($[23] !== elapsedTime || $[24] !== statusLabel) {
|
|
t8 = <Text dimColor={true}>{" \xB7 "}{elapsedTime}{" \xB7 "}{statusLabel}</Text>;
|
|
$[23] = elapsedTime;
|
|
$[24] = statusLabel;
|
|
$[25] = t8;
|
|
} else {
|
|
t8 = $[25];
|
|
}
|
|
let t9;
|
|
if ($[26] !== t6 || $[27] !== t8) {
|
|
t9 = <Text>{t6}{t7}{t8}</Text>;
|
|
$[26] = t6;
|
|
$[27] = t8;
|
|
$[28] = t9;
|
|
} else {
|
|
t9 = $[28];
|
|
}
|
|
const t10 = session.reviewProgress?.stage;
|
|
const t11 = !!session.reviewProgress;
|
|
let t12;
|
|
if ($[29] !== completed || $[30] !== t10 || $[31] !== t11) {
|
|
t12 = <StagePipeline stage={t10} completed={completed} hasProgress={t11} />;
|
|
$[29] = completed;
|
|
$[30] = t10;
|
|
$[31] = t11;
|
|
$[32] = t12;
|
|
} else {
|
|
t12 = $[32];
|
|
}
|
|
let t13;
|
|
if ($[33] !== session) {
|
|
t13 = reviewCountsLine(session);
|
|
$[33] = session;
|
|
$[34] = t13;
|
|
} else {
|
|
t13 = $[34];
|
|
}
|
|
let t14;
|
|
if ($[35] !== t13) {
|
|
t14 = <Text>{t13}</Text>;
|
|
$[35] = t13;
|
|
$[36] = t14;
|
|
} else {
|
|
t14 = $[36];
|
|
}
|
|
let t15;
|
|
if ($[37] !== sessionUrl) {
|
|
t15 = <Text dimColor={true}>{sessionUrl}</Text>;
|
|
$[37] = sessionUrl;
|
|
$[38] = t15;
|
|
} else {
|
|
t15 = $[38];
|
|
}
|
|
let t16;
|
|
if ($[39] !== sessionUrl || $[40] !== t15) {
|
|
t16 = <Link url={sessionUrl}>{t15}</Link>;
|
|
$[39] = sessionUrl;
|
|
$[40] = t15;
|
|
$[41] = t16;
|
|
} else {
|
|
t16 = $[41];
|
|
}
|
|
let t17;
|
|
if ($[42] !== t14 || $[43] !== t16) {
|
|
t17 = <Box flexDirection="column">{t14}{t16}</Box>;
|
|
$[42] = t14;
|
|
$[43] = t16;
|
|
$[44] = t17;
|
|
} else {
|
|
t17 = $[44];
|
|
}
|
|
let t18;
|
|
if ($[45] !== handleSelect || $[46] !== options) {
|
|
t18 = <Select options={options} onChange={handleSelect} />;
|
|
$[45] = handleSelect;
|
|
$[46] = options;
|
|
$[47] = t18;
|
|
} else {
|
|
t18 = $[47];
|
|
}
|
|
let t19;
|
|
if ($[48] !== t12 || $[49] !== t17 || $[50] !== t18) {
|
|
t19 = <Box flexDirection="column" gap={1}>{t12}{t17}{t18}</Box>;
|
|
$[48] = t12;
|
|
$[49] = t17;
|
|
$[50] = t18;
|
|
$[51] = t19;
|
|
} else {
|
|
t19 = $[51];
|
|
}
|
|
let t20;
|
|
if ($[52] !== goBackOrClose || $[53] !== t19 || $[54] !== t9) {
|
|
t20 = <Dialog title={t9} onCancel={goBackOrClose} color="background" inputGuide={_temp}>{t19}</Dialog>;
|
|
$[52] = goBackOrClose;
|
|
$[53] = t19;
|
|
$[54] = t9;
|
|
$[55] = t20;
|
|
} else {
|
|
t20 = $[55];
|
|
}
|
|
return t20;
|
|
}
|
|
function _temp(exitState) {
|
|
return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut="Enter" action="select" /><KeyboardShortcutHint shortcut="Esc" action="go back" /></Byline>;
|
|
}
|
|
export function RemoteSessionDetailDialog({
|
|
session,
|
|
toolUseContext,
|
|
onDone,
|
|
onBack,
|
|
onKill
|
|
}: Props): React.ReactNode {
|
|
const [isTeleporting, setIsTeleporting] = useState(false);
|
|
const [teleportError, setTeleportError] = useState<string | null>(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 <UltraplanSessionDetail session={session} onDone={onDone} onBack={onBack} onKill={onKill} />;
|
|
}
|
|
|
|
// Review sessions get the stage-pipeline view; everything else keeps the
|
|
// generic label/value + recent-messages dialog below.
|
|
if (session.isRemoteReview) {
|
|
return <ReviewSessionDetail session={session} onDone={onDone} onBack={onBack} onKill={onKill} />;
|
|
}
|
|
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<void> {
|
|
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 <Box flexDirection="column" tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
|
|
<Dialog title="Remote session details" onCancel={handleClose} color="background" inputGuide={exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>
|
|
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
|
|
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
|
|
{!isTeleporting && <KeyboardShortcutHint shortcut="t" action="teleport" />}
|
|
</Byline>}>
|
|
<Box flexDirection="column">
|
|
<Text>
|
|
<Text bold>Status</Text>:{' '}
|
|
{displayStatus === 'running' || displayStatus === 'starting' ? <Text color="background">{displayStatus}</Text> : displayStatus === 'completed' ? <Text color="success">{displayStatus}</Text> : <Text color="error">{displayStatus}</Text>}
|
|
</Text>
|
|
<Text>
|
|
<Text bold>Runtime</Text>:{' '}
|
|
{formatDuration((session.endTime ?? Date.now()) - session.startTime)}
|
|
</Text>
|
|
<Text wrap="truncate-end">
|
|
<Text bold>Title</Text>: {displayTitle}
|
|
</Text>
|
|
<Text>
|
|
<Text bold>Progress</Text>:{' '}
|
|
<RemoteSessionProgress session={session} />
|
|
</Text>
|
|
<Text>
|
|
<Text bold>Session URL</Text>:{' '}
|
|
<Link url={getRemoteTaskSessionUrl(session.sessionId)}>
|
|
<Text dimColor>{getRemoteTaskSessionUrl(session.sessionId)}</Text>
|
|
</Link>
|
|
</Text>
|
|
</Box>
|
|
|
|
{/* Remote session messages section */}
|
|
{session.log.length > 0 && <Box flexDirection="column" marginTop={1}>
|
|
<Text>
|
|
<Text bold>Recent messages</Text>:
|
|
</Text>
|
|
<Box flexDirection="column" height={10} overflowY="hidden">
|
|
{lastMessages.map((msg, i) => <Message key={i} message={msg} lookups={EMPTY_LOOKUPS} addMargin={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} />)}
|
|
</Box>
|
|
<Box marginTop={1}>
|
|
<Text dimColor italic>
|
|
Showing last {lastMessages.length} of {session.log.length}{' '}
|
|
messages
|
|
</Text>
|
|
</Box>
|
|
</Box>}
|
|
|
|
{/* Teleport error message */}
|
|
{teleportError && <Box marginTop={1}>
|
|
<Text color="error">Teleport failed: {teleportError}</Text>
|
|
</Box>}
|
|
|
|
{/* Teleporting status */}
|
|
{isTeleporting && <Text color="background">Teleporting to session…</Text>}
|
|
</Dialog>
|
|
</Box>;
|
|
}
|