Files
orcs-code/src/components/tasks/RemoteSessionDetailDialog.tsx
Kevin Codex 46a9d3eec4 chore: rebrand user-facing copy to OpenClaude (#851)
* chore: rebrand user-facing copy to OpenClaude

Replace lingering Claude Code branding in CLI, tips, and runtime UI with OpenClaude/openclaude, including the startup tip Gitlawb mention.

Co-Authored-By: Claude GPT-5.4 <noreply@openclaude.dev>

* chore: address branding-sweep review feedback

- PermissionRequest.tsx: rebrand the two remaining "Claude needs your
  approval/permission" notifications to OpenClaude (review-artifact and
  generic tool permission paths).
- main.tsx, teleport.tsx, session.tsx, WebFetchTool/utils.ts,
  skills/bundled/{debug,updateConfig}.ts: replace leftover `claude --…`
  CLI hints and "Claude Code" labels missed by the original sweep.
- main.tsx: drop the inline gitlawb.com marketing copy from the
  stale-prompt tip; keep it a pure rebrand.
- auth.ts: finish the half-rename so both `claude setup-token` and
  `claude auth login` references in the same error block now read
  `openclaude …`.
- mcp/client.ts: keep `name: 'claude-code'` for MCP server allowlist
  compatibility (now explicit via comment) and replace the
  "Anthropic's agentic coding tool" description with an OpenClaude one.
- MCPSettings.tsx: point the empty-server-list hint at
  https://github.com/Gitlawb/openclaude instead of code.claude.com.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: replace help link with OpenClaude repo URL

Replace https://code.claude.com/docs/en/overview with
https://github.com/Gitlawb/openclaude in the help screen.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: Claude GPT-5.4 <noreply@openclaude.dev>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-26 22:14:36 +08:00

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 OpenClaude 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 OpenClaude 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 OpenClaude 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 OpenClaude on the web",
value: "open"
}, {
label: "Dismiss",
value: "dismiss"
}] : [{
label: "Open in OpenClaude 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>;
}