diff --git a/src/components/DevBar.tsx b/src/components/DevBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fd4c5ea145075052237a05a2014592bbb8d080e5 --- /dev/null +++ b/src/components/DevBar.tsx @@ -0,0 +1,49 @@ +import { c as _c } from "react/compiler-runtime"; +import * as React from 'react'; +import { useState } from 'react'; +import { getSlowOperations } from '../bootstrap/state.js'; +import { Text, useInterval } from '../ink.js'; + +// Show DevBar for dev builds or all ants +function shouldShowDevBar(): boolean { + return "production" === 'development' || "external" === 'ant'; +} +export function DevBar() { + const $ = _c(5); + const [slowOps, setSlowOps] = useState(getSlowOperations); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + setSlowOps(getSlowOperations()); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + useInterval(t0, shouldShowDevBar() ? 500 : null); + if (!shouldShowDevBar() || slowOps.length === 0) { + return null; + } + let t1; + if ($[1] !== slowOps) { + t1 = slowOps.slice(-3).map(_temp).join(" \xB7 "); + $[1] = slowOps; + $[2] = t1; + } else { + t1 = $[2]; + } + const recentOps = t1; + let t2; + if ($[3] !== recentOps) { + t2 = [ANT-ONLY] slow sync: {recentOps}; + $[3] = recentOps; + $[4] = t2; + } else { + t2 = $[4]; + } + return t2; +} +function _temp(op) { + return `${op.operation} (${Math.round(op.durationMs)}ms)`; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZVN0YXRlIiwiZ2V0U2xvd09wZXJhdGlvbnMiLCJUZXh0IiwidXNlSW50ZXJ2YWwiLCJzaG91bGRTaG93RGV2QmFyIiwiRGV2QmFyIiwiJCIsIl9jIiwic2xvd09wcyIsInNldFNsb3dPcHMiLCJ0MCIsIlN5bWJvbCIsImZvciIsImxlbmd0aCIsInQxIiwic2xpY2UiLCJtYXAiLCJfdGVtcCIsImpvaW4iLCJyZWNlbnRPcHMiLCJ0MiIsIm9wIiwib3BlcmF0aW9uIiwiTWF0aCIsInJvdW5kIiwiZHVyYXRpb25NcyJdLCJzb3VyY2VzIjpbIkRldkJhci50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgZ2V0U2xvd09wZXJhdGlvbnMgfSBmcm9tICcuLi9ib290c3RyYXAvc3RhdGUuanMnXG5pbXBvcnQgeyBUZXh0LCB1c2VJbnRlcnZhbCB9IGZyb20gJy4uL2luay5qcydcblxuLy8gU2hvdyBEZXZCYXIgZm9yIGRldiBidWlsZHMgb3IgYWxsIGFudHNcbmZ1bmN0aW9uIHNob3VsZFNob3dEZXZCYXIoKTogYm9vbGVhbiB7XG4gIHJldHVybiAoXG4gICAgXCJwcm9kdWN0aW9uXCIgPT09ICdkZXZlbG9wbWVudCcgfHwgXCJleHRlcm5hbFwiID09PSAnYW50J1xuICApXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBEZXZCYXIoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgW3Nsb3dPcHMsIHNldFNsb3dPcHNdID1cbiAgICB1c2VTdGF0ZTxcbiAgICAgIFJlYWRvbmx5QXJyYXk8e1xuICAgICAgICBvcGVyYXRpb246IHN0cmluZ1xuICAgICAgICBkdXJhdGlvbk1zOiBudW1iZXJcbiAgICAgICAgdGltZXN0YW1wOiBudW1iZXJcbiAgICAgIH0+XG4gICAgPihnZXRTbG93T3BlcmF0aW9ucylcblxuICB1c2VJbnRlcnZhbChcbiAgICAoKSA9PiB7XG4gICAgICBzZXRTbG93T3BzKGdldFNsb3dPcGVyYXRpb25zKCkpXG4gICAgfSxcbiAgICBzaG91bGRTaG93RGV2QmFyKCkgPyA1MDAgOiBudWxsLFxuICApXG5cbiAgLy8gT25seSBzaG93IHdoZW4gdGhlcmUncyBzb21ldGhpbmcgdG8gZGlzcGxheVxuICBpZiAoIXNob3VsZFNob3dEZXZCYXIoKSB8fCBzbG93T3BzLmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybiBudWxsXG4gIH1cblxuICAvLyBTaW5nbGUtbGluZSBmb3JtYXQgc28gc2hvcnQgdGVybWluYWxzIGRvbid0IGxvc2Ugcm93cyB0byBkZXYgbm9pc2UuXG4gIGNvbnN0IHJlY2VudE9wcyA9IHNsb3dPcHNcbiAgICAuc2xpY2UoLTMpXG4gICAgLm1hcChvcCA9PiBgJHtvcC5vcGVyYXRpb259ICgke01hdGgucm91bmQob3AuZHVyYXRpb25Ncyl9bXMpYClcbiAgICAuam9pbignIMK3ICcpXG5cbiAgcmV0dXJuIChcbiAgICA8VGV4dCB3cmFwPVwidHJ1bmNhdGUtZW5kXCIgY29sb3I9XCJ3YXJuaW5nXCI+XG4gICAgICBbQU5ULU9OTFldIHNsb3cgc3luYzoge3JlY2VudE9wc31cbiAgICA8L1RleHQ+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsUUFBUSxRQUFRLE9BQU87QUFDaEMsU0FBU0MsaUJBQWlCLFFBQVEsdUJBQXVCO0FBQ3pELFNBQVNDLElBQUksRUFBRUMsV0FBVyxRQUFRLFdBQVc7O0FBRTdDO0FBQ0EsU0FBU0MsZ0JBQWdCQSxDQUFBLENBQUUsRUFBRSxPQUFPLENBQUM7RUFDbkMsT0FDRSxZQUFZLEtBQUssYUFBYSxJQUFJLFVBQVUsS0FBSyxLQUFLO0FBRTFEO0FBRUEsT0FBTyxTQUFBQyxPQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQ0wsT0FBQUMsT0FBQSxFQUFBQyxVQUFBLElBQ0VULFFBQVEsQ0FNTkMsaUJBQWlCLENBQUM7RUFBQSxJQUFBUyxFQUFBO0VBQUEsSUFBQUosQ0FBQSxRQUFBSyxNQUFBLENBQUFDLEdBQUE7SUFHcEJGLEVBQUEsR0FBQUEsQ0FBQTtNQUNFRCxVQUFVLENBQUNSLGlCQUFpQixDQUFDLENBQUMsQ0FBQztJQUFBLENBQ2hDO0lBQUFLLENBQUEsTUFBQUksRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUosQ0FBQTtFQUFBO0VBSEhILFdBQVcsQ0FDVE8sRUFFQyxFQUNETixnQkFBZ0IsQ0FBYyxDQUFDLEdBQS9CLEdBQStCLEdBQS9CLElBQ0YsQ0FBQztFQUdELElBQUksQ0FBQ0EsZ0JBQWdCLENBQUMsQ0FBeUIsSUFBcEJJLE9BQU8sQ0FBQUssTUFBTyxLQUFLLENBQUM7SUFBQSxPQUN0QyxJQUFJO0VBQUE7RUFDWixJQUFBQyxFQUFBO0VBQUEsSUFBQVIsQ0FBQSxRQUFBRSxPQUFBO0lBR2lCTSxFQUFBLEdBQUFOLE9BQU8sQ0FBQU8sS0FDakIsQ0FBQyxFQUFFLENBQUMsQ0FBQUMsR0FDTixDQUFDQyxLQUF3RCxDQUFDLENBQUFDLElBQ3pELENBQUMsUUFBSyxDQUFDO0lBQUFaLENBQUEsTUFBQUUsT0FBQTtJQUFBRixDQUFBLE1BQUFRLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFSLENBQUE7RUFBQTtFQUhkLE1BQUFhLFNBQUEsR0FBa0JMLEVBR0o7RUFBQSxJQUFBTSxFQUFBO0VBQUEsSUFBQWQsQ0FBQSxRQUFBYSxTQUFBO0lBR1pDLEVBQUEsSUFBQyxJQUFJLENBQU0sSUFBYyxDQUFkLGNBQWMsQ0FBTyxLQUFTLENBQVQsU0FBUyxDQUFDLHNCQUNqQkQsVUFBUSxDQUNqQyxFQUZDLElBQUksQ0FFRTtJQUFBYixDQUFBLE1BQUFhLFNBQUE7SUFBQWIsQ0FBQSxNQUFBYyxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBZCxDQUFBO0VBQUE7RUFBQSxPQUZQYyxFQUVPO0FBQUE7QUEvQkosU0FBQUgsTUFBQUksRUFBQTtFQUFBLE9BeUJRLEdBQUdBLEVBQUUsQ0FBQUMsU0FBVSxLQUFLQyxJQUFJLENBQUFDLEtBQU0sQ0FBQ0gsRUFBRSxDQUFBSSxVQUFXLENBQUMsS0FBSztBQUFBIiwiaWdub3JlTGlzdCI6W119 \ No newline at end of file diff --git a/src/components/TaskListV2.tsx b/src/components/TaskListV2.tsx new file mode 100644 index 0000000000000000000000000000000000000000..addc083d1506c084c29896c8cf1bcca29f2c4fcc --- /dev/null +++ b/src/components/TaskListV2.tsx @@ -0,0 +1,378 @@ +import { c as _c } from "react/compiler-runtime"; +import figures from 'figures'; +import * as React from 'react'; +import { useTerminalSize } from '../hooks/useTerminalSize.js'; +import { stringWidth } from '../ink/stringWidth.js'; +import { Box, Text } from '../ink.js'; +import { useAppState } from '../state/AppState.js'; +import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js'; +import { AGENT_COLOR_TO_THEME_COLOR, type AgentColorName } from '../tools/AgentTool/agentColorManager.js'; +import { isAgentSwarmsEnabled } from '../utils/agentSwarmsEnabled.js'; +import { count } from '../utils/array.js'; +import { summarizeRecentActivities } from '../utils/collapseReadSearch.js'; +import { truncateToWidth } from '../utils/format.js'; +import { isTodoV2Enabled, type Task } from '../utils/tasks.js'; +import type { Theme } from '../utils/theme.js'; +import ThemedText from './design-system/ThemedText.js'; +type Props = { + tasks: Task[]; + isStandalone?: boolean; +}; +const RECENT_COMPLETED_TTL_MS = 30_000; +function byIdAsc(a: Task, b: Task): number { + const aNum = parseInt(a.id, 10); + const bNum = parseInt(b.id, 10); + if (!isNaN(aNum) && !isNaN(bNum)) { + return aNum - bNum; + } + return a.id.localeCompare(b.id); +} +export function TaskListV2({ + tasks, + isStandalone = false +}: Props): React.ReactNode { + const teamContext = useAppState(s => s.teamContext); + const appStateTasks = useAppState(s_0 => s_0.tasks); + const [, forceUpdate] = React.useState(0); + const { + rows, + columns + } = useTerminalSize(); + + // Track when each task was last observed transitioning to completed + const completionTimestampsRef = React.useRef(new Map()); + const previousCompletedIdsRef = React.useRef | null>(null); + if (previousCompletedIdsRef.current === null) { + previousCompletedIdsRef.current = new Set(tasks.filter(t => t.status === 'completed').map(t_0 => t_0.id)); + } + const maxDisplay = rows <= 10 ? 0 : Math.min(10, Math.max(3, rows - 14)); + + // Update completion timestamps: reset when a task transitions to completed + const currentCompletedIds = new Set(tasks.filter(t_1 => t_1.status === 'completed').map(t_2 => t_2.id)); + const now = Date.now(); + for (const id of currentCompletedIds) { + if (!previousCompletedIdsRef.current.has(id)) { + completionTimestampsRef.current.set(id, now); + } + } + for (const id_0 of completionTimestampsRef.current.keys()) { + if (!currentCompletedIds.has(id_0)) { + completionTimestampsRef.current.delete(id_0); + } + } + previousCompletedIdsRef.current = currentCompletedIds; + + // Schedule re-render when the next recent completion expires. + // Depend on `tasks` so the timer is only reset when the task list changes, + // not on every render (which was causing unnecessary work). + React.useEffect(() => { + if (completionTimestampsRef.current.size === 0) { + return; + } + const currentNow = Date.now(); + let earliestExpiry = Infinity; + for (const ts of completionTimestampsRef.current.values()) { + const expiry = ts + RECENT_COMPLETED_TTL_MS; + if (expiry > currentNow && expiry < earliestExpiry) { + earliestExpiry = expiry; + } + } + if (earliestExpiry === Infinity) { + return; + } + const timer = setTimeout(forceUpdate_0 => forceUpdate_0((n: number) => n + 1), earliestExpiry - currentNow, forceUpdate); + return () => clearTimeout(timer); + }, [tasks]); + if (!isTodoV2Enabled()) { + return null; + } + if (tasks.length === 0) { + return null; + } + + // Build a map of teammate name -> theme color + const teammateColors: Record = {}; + if (isAgentSwarmsEnabled() && teamContext?.teammates) { + for (const teammate of Object.values(teamContext.teammates)) { + if (teammate.color) { + const themeColor = AGENT_COLOR_TO_THEME_COLOR[teammate.color as AgentColorName]; + if (themeColor) { + teammateColors[teammate.name] = themeColor; + } + } + } + } + + // Build a map of teammate name -> current activity description + // Map both agentName ("researcher") and agentId ("researcher@team") so + // task owners match regardless of which format the model used. + // Rolls up consecutive search/read tool uses into a compact summary. + // Also track which teammates are still running (not shut down). + const teammateActivity: Record = {}; + const activeTeammates = new Set(); + if (isAgentSwarmsEnabled()) { + for (const bgTask of Object.values(appStateTasks)) { + if (isInProcessTeammateTask(bgTask) && bgTask.status === 'running') { + activeTeammates.add(bgTask.identity.agentName); + activeTeammates.add(bgTask.identity.agentId); + const activities = bgTask.progress?.recentActivities; + const desc = (activities && summarizeRecentActivities(activities)) ?? bgTask.progress?.lastActivity?.activityDescription; + if (desc) { + teammateActivity[bgTask.identity.agentName] = desc; + teammateActivity[bgTask.identity.agentId] = desc; + } + } + } + } + + // Get task counts for display + const completedCount = count(tasks, t_3 => t_3.status === 'completed'); + const pendingCount = count(tasks, t_4 => t_4.status === 'pending'); + const inProgressCount = tasks.length - completedCount - pendingCount; + // Unresolved tasks (open or in_progress) block dependent tasks + const unresolvedTaskIds = new Set(tasks.filter(t_5 => t_5.status !== 'completed').map(t_6 => t_6.id)); + + // Check if we need to truncate + const needsTruncation = tasks.length > maxDisplay; + let visibleTasks: Task[]; + let hiddenTasks: Task[]; + if (needsTruncation) { + // Prioritize: recently completed (within 30s), in-progress, pending, older completed + const recentCompleted: Task[] = []; + const olderCompleted: Task[] = []; + for (const task of tasks.filter(t_7 => t_7.status === 'completed')) { + const ts_0 = completionTimestampsRef.current.get(task.id); + if (ts_0 && now - ts_0 < RECENT_COMPLETED_TTL_MS) { + recentCompleted.push(task); + } else { + olderCompleted.push(task); + } + } + recentCompleted.sort(byIdAsc); + olderCompleted.sort(byIdAsc); + const inProgress = tasks.filter(t_8 => t_8.status === 'in_progress').sort(byIdAsc); + const pending = tasks.filter(t_9 => t_9.status === 'pending').sort((a, b) => { + const aBlocked = a.blockedBy.some(id_1 => unresolvedTaskIds.has(id_1)); + const bBlocked = b.blockedBy.some(id_2 => unresolvedTaskIds.has(id_2)); + if (aBlocked !== bBlocked) { + return aBlocked ? 1 : -1; + } + return byIdAsc(a, b); + }); + const prioritized = [...recentCompleted, ...inProgress, ...pending, ...olderCompleted]; + visibleTasks = prioritized.slice(0, maxDisplay); + hiddenTasks = prioritized.slice(maxDisplay); + } else { + // No truncation needed — sort by ID for stable ordering + visibleTasks = [...tasks].sort(byIdAsc); + hiddenTasks = []; + } + let hiddenSummary = ''; + if (hiddenTasks.length > 0) { + const parts: string[] = []; + const hiddenPending = count(hiddenTasks, t_10 => t_10.status === 'pending'); + const hiddenInProgress = count(hiddenTasks, t_11 => t_11.status === 'in_progress'); + const hiddenCompleted = count(hiddenTasks, t_12 => t_12.status === 'completed'); + if (hiddenInProgress > 0) { + parts.push(`${hiddenInProgress} in progress`); + } + if (hiddenPending > 0) { + parts.push(`${hiddenPending} pending`); + } + if (hiddenCompleted > 0) { + parts.push(`${hiddenCompleted} completed`); + } + hiddenSummary = ` … +${parts.join(', ')}`; + } + const content = <> + {visibleTasks.map(task_0 => unresolvedTaskIds.has(id_3))} activity={task_0.owner ? teammateActivity[task_0.owner] : undefined} ownerActive={task_0.owner ? activeTeammates.has(task_0.owner) : false} columns={columns} />)} + {maxDisplay > 0 && hiddenSummary && {hiddenSummary}} + ; + if (isStandalone) { + return + + + {tasks.length} + {' tasks ('} + {completedCount} + {' done, '} + {inProgressCount > 0 && <> + {inProgressCount} + {' in progress, '} + } + {pendingCount} + {' open)'} + + + {content} + ; + } + return {content}; +} +type TaskItemProps = { + task: Task; + ownerColor?: keyof Theme; + openBlockers: string[]; + activity?: string; + ownerActive: boolean; + columns: number; +}; +function getTaskIcon(status: Task['status']): { + icon: string; + color: keyof Theme | undefined; +} { + switch (status) { + case 'completed': + return { + icon: figures.tick, + color: 'success' + }; + case 'in_progress': + return { + icon: figures.squareSmallFilled, + color: 'claude' + }; + case 'pending': + return { + icon: figures.squareSmall, + color: undefined + }; + } +} +function TaskItem(t0) { + const $ = _c(37); + const { + task, + ownerColor, + openBlockers, + activity, + ownerActive, + columns + } = t0; + const isCompleted = task.status === "completed"; + const isInProgress = task.status === "in_progress"; + const isBlocked = openBlockers.length > 0; + let t1; + if ($[0] !== task.status) { + t1 = getTaskIcon(task.status); + $[0] = task.status; + $[1] = t1; + } else { + t1 = $[1]; + } + const { + icon, + color + } = t1; + const showActivity = isInProgress && !isBlocked && activity; + const showOwner = columns >= 60 && task.owner && ownerActive; + let t2; + if ($[2] !== showOwner || $[3] !== task.owner) { + t2 = showOwner ? stringWidth(` (@${task.owner})`) : 0; + $[2] = showOwner; + $[3] = task.owner; + $[4] = t2; + } else { + t2 = $[4]; + } + const ownerWidth = t2; + const maxSubjectWidth = Math.max(15, columns - 15 - ownerWidth); + let t3; + if ($[5] !== maxSubjectWidth || $[6] !== task.subject) { + t3 = truncateToWidth(task.subject, maxSubjectWidth); + $[5] = maxSubjectWidth; + $[6] = task.subject; + $[7] = t3; + } else { + t3 = $[7]; + } + const displaySubject = t3; + const maxActivityWidth = Math.max(15, columns - 15); + let t4; + if ($[8] !== activity || $[9] !== maxActivityWidth) { + t4 = activity ? truncateToWidth(activity, maxActivityWidth) : undefined; + $[8] = activity; + $[9] = maxActivityWidth; + $[10] = t4; + } else { + t4 = $[10]; + } + const displayActivity = t4; + let t5; + if ($[11] !== color || $[12] !== icon) { + t5 = {icon} ; + $[11] = color; + $[12] = icon; + $[13] = t5; + } else { + t5 = $[13]; + } + const t6 = isCompleted || isBlocked; + let t7; + if ($[14] !== displaySubject || $[15] !== isCompleted || $[16] !== isInProgress || $[17] !== t6) { + t7 = {displaySubject}; + $[14] = displaySubject; + $[15] = isCompleted; + $[16] = isInProgress; + $[17] = t6; + $[18] = t7; + } else { + t7 = $[18]; + } + let t8; + if ($[19] !== ownerColor || $[20] !== showOwner || $[21] !== task.owner) { + t8 = showOwner && {" ("}{ownerColor ? @{task.owner} : `@${task.owner}`}{")"}; + $[19] = ownerColor; + $[20] = showOwner; + $[21] = task.owner; + $[22] = t8; + } else { + t8 = $[22]; + } + let t9; + if ($[23] !== isBlocked || $[24] !== openBlockers) { + t9 = isBlocked && {" "}{figures.pointerSmall} blocked by{" "}{[...openBlockers].sort(_temp).map(_temp2).join(", ")}; + $[23] = isBlocked; + $[24] = openBlockers; + $[25] = t9; + } else { + t9 = $[25]; + } + let t10; + if ($[26] !== t5 || $[27] !== t7 || $[28] !== t8 || $[29] !== t9) { + t10 = {t5}{t7}{t8}{t9}; + $[26] = t5; + $[27] = t7; + $[28] = t8; + $[29] = t9; + $[30] = t10; + } else { + t10 = $[30]; + } + let t11; + if ($[31] !== displayActivity || $[32] !== showActivity) { + t11 = showActivity && displayActivity && {" "}{displayActivity}{figures.ellipsis}; + $[31] = displayActivity; + $[32] = showActivity; + $[33] = t11; + } else { + t11 = $[33]; + } + let t12; + if ($[34] !== t10 || $[35] !== t11) { + t12 = {t10}{t11}; + $[34] = t10; + $[35] = t11; + $[36] = t12; + } else { + t12 = $[36]; + } + return t12; +} +function _temp2(id) { + return `#${id}`; +} +function _temp(a, b) { + return parseInt(a, 10) - parseInt(b, 10); +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VUZXJtaW5hbFNpemUiLCJzdHJpbmdXaWR0aCIsIkJveCIsIlRleHQiLCJ1c2VBcHBTdGF0ZSIsImlzSW5Qcm9jZXNzVGVhbW1hdGVUYXNrIiwiQUdFTlRfQ09MT1JfVE9fVEhFTUVfQ09MT1IiLCJBZ2VudENvbG9yTmFtZSIsImlzQWdlbnRTd2FybXNFbmFibGVkIiwiY291bnQiLCJzdW1tYXJpemVSZWNlbnRBY3Rpdml0aWVzIiwidHJ1bmNhdGVUb1dpZHRoIiwiaXNUb2RvVjJFbmFibGVkIiwiVGFzayIsIlRoZW1lIiwiVGhlbWVkVGV4dCIsIlByb3BzIiwidGFza3MiLCJpc1N0YW5kYWxvbmUiLCJSRUNFTlRfQ09NUExFVEVEX1RUTF9NUyIsImJ5SWRBc2MiLCJhIiwiYiIsImFOdW0iLCJwYXJzZUludCIsImlkIiwiYk51bSIsImlzTmFOIiwibG9jYWxlQ29tcGFyZSIsIlRhc2tMaXN0VjIiLCJSZWFjdE5vZGUiLCJ0ZWFtQ29udGV4dCIsInMiLCJhcHBTdGF0ZVRhc2tzIiwiZm9yY2VVcGRhdGUiLCJ1c2VTdGF0ZSIsInJvd3MiLCJjb2x1bW5zIiwiY29tcGxldGlvblRpbWVzdGFtcHNSZWYiLCJ1c2VSZWYiLCJNYXAiLCJwcmV2aW91c0NvbXBsZXRlZElkc1JlZiIsIlNldCIsImN1cnJlbnQiLCJmaWx0ZXIiLCJ0Iiwic3RhdHVzIiwibWFwIiwibWF4RGlzcGxheSIsIk1hdGgiLCJtaW4iLCJtYXgiLCJjdXJyZW50Q29tcGxldGVkSWRzIiwibm93IiwiRGF0ZSIsImhhcyIsInNldCIsImtleXMiLCJkZWxldGUiLCJ1c2VFZmZlY3QiLCJzaXplIiwiY3VycmVudE5vdyIsImVhcmxpZXN0RXhwaXJ5IiwiSW5maW5pdHkiLCJ0cyIsInZhbHVlcyIsImV4cGlyeSIsInRpbWVyIiwic2V0VGltZW91dCIsIm4iLCJjbGVhclRpbWVvdXQiLCJsZW5ndGgiLCJ0ZWFtbWF0ZUNvbG9ycyIsIlJlY29yZCIsInRlYW1tYXRlcyIsInRlYW1tYXRlIiwiT2JqZWN0IiwiY29sb3IiLCJ0aGVtZUNvbG9yIiwibmFtZSIsInRlYW1tYXRlQWN0aXZpdHkiLCJhY3RpdmVUZWFtbWF0ZXMiLCJiZ1Rhc2siLCJhZGQiLCJpZGVudGl0eSIsImFnZW50TmFtZSIsImFnZW50SWQiLCJhY3Rpdml0aWVzIiwicHJvZ3Jlc3MiLCJyZWNlbnRBY3Rpdml0aWVzIiwiZGVzYyIsImxhc3RBY3Rpdml0eSIsImFjdGl2aXR5RGVzY3JpcHRpb24iLCJjb21wbGV0ZWRDb3VudCIsInBlbmRpbmdDb3VudCIsImluUHJvZ3Jlc3NDb3VudCIsInVucmVzb2x2ZWRUYXNrSWRzIiwibmVlZHNUcnVuY2F0aW9uIiwidmlzaWJsZVRhc2tzIiwiaGlkZGVuVGFza3MiLCJyZWNlbnRDb21wbGV0ZWQiLCJvbGRlckNvbXBsZXRlZCIsInRhc2siLCJnZXQiLCJwdXNoIiwic29ydCIsImluUHJvZ3Jlc3MiLCJwZW5kaW5nIiwiYUJsb2NrZWQiLCJibG9ja2VkQnkiLCJzb21lIiwiYkJsb2NrZWQiLCJwcmlvcml0aXplZCIsInNsaWNlIiwiaGlkZGVuU3VtbWFyeSIsInBhcnRzIiwiaGlkZGVuUGVuZGluZyIsImhpZGRlbkluUHJvZ3Jlc3MiLCJoaWRkZW5Db21wbGV0ZWQiLCJqb2luIiwiY29udGVudCIsIm93bmVyIiwidW5kZWZpbmVkIiwiVGFza0l0ZW1Qcm9wcyIsIm93bmVyQ29sb3IiLCJvcGVuQmxvY2tlcnMiLCJhY3Rpdml0eSIsIm93bmVyQWN0aXZlIiwiZ2V0VGFza0ljb24iLCJpY29uIiwidGljayIsInNxdWFyZVNtYWxsRmlsbGVkIiwic3F1YXJlU21hbGwiLCJUYXNrSXRlbSIsInQwIiwiJCIsIl9jIiwiaXNDb21wbGV0ZWQiLCJpc0luUHJvZ3Jlc3MiLCJpc0Jsb2NrZWQiLCJ0MSIsInNob3dBY3Rpdml0eSIsInNob3dPd25lciIsInQyIiwib3duZXJXaWR0aCIsIm1heFN1YmplY3RXaWR0aCIsInQzIiwic3ViamVjdCIsImRpc3BsYXlTdWJqZWN0IiwibWF4QWN0aXZpdHlXaWR0aCIsInQ0IiwiZGlzcGxheUFjdGl2aXR5IiwidDUiLCJ0NiIsInQ3IiwidDgiLCJ0OSIsInBvaW50ZXJTbWFsbCIsIl90ZW1wIiwiX3RlbXAyIiwidDEwIiwidDExIiwiZWxsaXBzaXMiLCJ0MTIiXSwic291cmNlcyI6WyJUYXNrTGlzdFYyLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgZmlndXJlcyBmcm9tICdmaWd1cmVzJ1xuaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VUZXJtaW5hbFNpemUgfSBmcm9tICcuLi9ob29rcy91c2VUZXJtaW5hbFNpemUuanMnXG5pbXBvcnQgeyBzdHJpbmdXaWR0aCB9IGZyb20gJy4uL2luay9zdHJpbmdXaWR0aC5qcydcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCB7IHVzZUFwcFN0YXRlIH0gZnJvbSAnLi4vc3RhdGUvQXBwU3RhdGUuanMnXG5pbXBvcnQgeyBpc0luUHJvY2Vzc1RlYW1tYXRlVGFzayB9IGZyb20gJy4uL3Rhc2tzL0luUHJvY2Vzc1RlYW1tYXRlVGFzay90eXBlcy5qcydcbmltcG9ydCB7XG4gIEFHRU5UX0NPTE9SX1RPX1RIRU1FX0NPTE9SLFxuICB0eXBlIEFnZW50Q29sb3JOYW1lLFxufSBmcm9tICcuLi90b29scy9BZ2VudFRvb2wvYWdlbnRDb2xvck1hbmFnZXIuanMnXG5pbXBvcnQgeyBpc0FnZW50U3dhcm1zRW5hYmxlZCB9IGZyb20gJy4uL3V0aWxzL2FnZW50U3dhcm1zRW5hYmxlZC5qcydcbmltcG9ydCB7IGNvdW50IH0gZnJvbSAnLi4vdXRpbHMvYXJyYXkuanMnXG5pbXBvcnQgeyBzdW1tYXJpemVSZWNlbnRBY3Rpdml0aWVzIH0gZnJvbSAnLi4vdXRpbHMvY29sbGFwc2VSZWFkU2VhcmNoLmpzJ1xuaW1wb3J0IHsgdHJ1bmNhdGVUb1dpZHRoIH0gZnJvbSAnLi4vdXRpbHMvZm9ybWF0LmpzJ1xuaW1wb3J0IHsgaXNUb2RvVjJFbmFibGVkLCB0eXBlIFRhc2sgfSBmcm9tICcuLi91dGlscy90YXNrcy5qcydcbmltcG9ydCB0eXBlIHsgVGhlbWUgfSBmcm9tICcuLi91dGlscy90aGVtZS5qcydcbmltcG9ydCBUaGVtZWRUZXh0IGZyb20gJy4vZGVzaWduLXN5c3RlbS9UaGVtZWRUZXh0LmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICB0YXNrczogVGFza1tdXG4gIGlzU3RhbmRhbG9uZT86IGJvb2xlYW5cbn1cblxuY29uc3QgUkVDRU5UX0NPTVBMRVRFRF9UVExfTVMgPSAzMF8wMDBcblxuZnVuY3Rpb24gYnlJZEFzYyhhOiBUYXNrLCBiOiBUYXNrKTogbnVtYmVyIHtcbiAgY29uc3QgYU51bSA9IHBhcnNlSW50KGEuaWQsIDEwKVxuICBjb25zdCBiTnVtID0gcGFyc2VJbnQoYi5pZCwgMTApXG4gIGlmICghaXNOYU4oYU51bSkgJiYgIWlzTmFOKGJOdW0pKSB7XG4gICAgcmV0dXJuIGFOdW0gLSBiTnVtXG4gIH1cbiAgcmV0dXJuIGEuaWQubG9jYWxlQ29tcGFyZShiLmlkKVxufVxuXG5leHBvcnQgZnVuY3Rpb24gVGFza0xpc3RWMih7XG4gIHRhc2tzLFxuICBpc1N0YW5kYWxvbmUgPSBmYWxzZSxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgdGVhbUNvbnRleHQgPSB1c2VBcHBTdGF0ZShzID0+IHMudGVhbUNvbnRleHQpXG4gIGNvbnN0IGFwcFN0YXRlVGFza3MgPSB1c2VBcHBTdGF0ZShzID0+IHMudGFza3MpXG4gIGNvbnN0IFssIGZvcmNlVXBkYXRlXSA9IFJlYWN0LnVzZVN0YXRlKDApXG4gIGNvbnN0IHsgcm93cywgY29sdW1ucyB9ID0gdXNlVGVybWluYWxTaXplKClcblxuICAvLyBUcmFjayB3aGVuIGVhY2ggdGFzayB3YXMgbGFzdCBvYnNlcnZlZCB0cmFuc2l0aW9uaW5nIHRvIGNvbXBsZXRlZFxuICBjb25zdCBjb21wbGV0aW9uVGltZXN0YW1wc1JlZiA9IFJlYWN0LnVzZVJlZihuZXcgTWFwPHN0cmluZywgbnVtYmVyPigpKVxuICBjb25zdCBwcmV2aW91c0NvbXBsZXRlZElkc1JlZiA9IFJlYWN0LnVzZVJlZjxTZXQ8c3RyaW5nPiB8IG51bGw+KG51bGwpXG4gIGlmIChwcmV2aW91c0NvbXBsZXRlZElkc1JlZi5jdXJyZW50ID09PSBudWxsKSB7XG4gICAgcHJldmlvdXNDb21wbGV0ZWRJZHNSZWYuY3VycmVudCA9IG5ldyBTZXQoXG4gICAgICB0YXNrcy5maWx0ZXIodCA9PiB0LnN0YXR1cyA9PT0gJ2NvbXBsZXRlZCcpLm1hcCh0ID0+IHQuaWQpLFxuICAgIClcbiAgfVxuICBjb25zdCBtYXhEaXNwbGF5ID0gcm93cyA8PSAxMCA/IDAgOiBNYXRoLm1pbigxMCwgTWF0aC5tYXgoMywgcm93cyAtIDE0KSlcblxuICAvLyBVcGRhdGUgY29tcGxldGlvbiB0aW1lc3RhbXBzOiByZXNldCB3aGVuIGEgdGFzayB0cmFuc2l0aW9ucyB0byBjb21wbGV0ZWRcbiAgY29uc3QgY3VycmVudENvbXBsZXRlZElkcyA9IG5ldyBTZXQoXG4gICAgdGFza3MuZmlsdGVyKHQgPT4gdC5zdGF0dXMgPT09ICdjb21wbGV0ZWQnKS5tYXAodCA9PiB0LmlkKSxcbiAgKVxuICBjb25zdCBub3cgPSBEYXRlLm5vdygpXG4gIGZvciAoY29uc3QgaWQgb2YgY3VycmVudENvbXBsZXRlZElkcykge1xuICAgIGlmICghcHJldmlvdXNDb21wbGV0ZWRJZHNSZWYuY3VycmVudC5oYXMoaWQpKSB7XG4gICAgICBjb21wbGV0aW9uVGltZXN0YW1wc1JlZi5jdXJyZW50LnNldChpZCwgbm93KVxuICAgIH1cbiAgfVxuICBmb3IgKGNvbnN0IGlkIG9mIGNvbXBsZXRpb25UaW1lc3RhbXBzUmVmLmN1cnJlbnQua2V5cygpKSB7XG4gICAgaWYgKCFjdXJyZW50Q29tcGxldGVkSWRzLmhhcyhpZCkpIHtcbiAgICAgIGNvbXBsZXRpb25UaW1lc3RhbXBzUmVmLmN1cnJlbnQuZGVsZXRlKGlkKVxuICAgIH1cbiAgfVxuICBwcmV2aW91c0NvbXBsZXRlZElkc1JlZi5jdXJyZW50ID0gY3VycmVudENvbXBsZXRlZElkc1xuXG4gIC8vIFNjaGVkdWxlIHJlLXJlbmRlciB3aGVuIHRoZSBuZXh0IHJlY2VudCBjb21wbGV0aW9uIGV4cGlyZXMuXG4gIC8vIERlcGVuZCBvbiBgdGFza3NgIHNvIHRoZSB0aW1lciBpcyBvbmx5IHJlc2V0IHdoZW4gdGhlIHRhc2sgbGlzdCBjaGFuZ2VzLFxuICAvLyBub3Qgb24gZXZlcnkgcmVuZGVyICh3aGljaCB3YXMgY2F1c2luZyB1bm5lY2Vzc2FyeSB3b3JrKS5cbiAgUmVhY3QudXNlRWZmZWN0KCgpID0+IHtcbiAgICBpZiAoY29tcGxldGlvblRpbWVzdGFtcHNSZWYuY3VycmVudC5zaXplID09PSAwKSB7XG4gICAgICByZXR1cm5cbiAgICB9XG4gICAgY29uc3QgY3VycmVudE5vdyA9IERhdGUubm93KClcbiAgICBsZXQgZWFybGllc3RFeHBpcnkgPSBJbmZpbml0eVxuICAgIGZvciAoY29uc3QgdHMgb2YgY29tcGxldGlvblRpbWVzdGFtcHNSZWYuY3VycmVudC52YWx1ZXMoKSkge1xuICAgICAgY29uc3QgZXhwaXJ5ID0gdHMgKyBSRUNFTlRfQ09NUExFVEVEX1RUTF9NU1xuICAgICAgaWYgKGV4cGlyeSA+IGN1cnJlbnROb3cgJiYgZXhwaXJ5IDwgZWFybGllc3RFeHBpcnkpIHtcbiAgICAgICAgZWFybGllc3RFeHBpcnkgPSBleHBpcnlcbiAgICAgIH1cbiAgICB9XG4gICAgaWYgKGVhcmxpZXN0RXhwaXJ5ID09PSBJbmZpbml0eSkge1xuICAgICAgcmV0dXJuXG4gICAgfVxuICAgIGNvbnN0IHRpbWVyID0gc2V0VGltZW91dChcbiAgICAgIGZvcmNlVXBkYXRlID0+IGZvcmNlVXBkYXRlKChuOiBudW1iZXIpID0+IG4gKyAxKSxcbiAgICAgIGVhcmxpZXN0RXhwaXJ5IC0gY3VycmVudE5vdyxcbiAgICAgIGZvcmNlVXBkYXRlLFxuICAgIClcbiAgICByZXR1cm4gKCkgPT4gY2xlYXJUaW1lb3V0KHRpbWVyKVxuICB9LCBbdGFza3NdKVxuXG4gIGlmICghaXNUb2RvVjJFbmFibGVkKCkpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgaWYgKHRhc2tzLmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybiBudWxsXG4gIH1cblxuICAvLyBCdWlsZCBhIG1hcCBvZiB0ZWFtbWF0ZSBuYW1lIC0+IHRoZW1lIGNvbG9yXG4gIGNvbnN0IHRlYW1tYXRlQ29sb3JzOiBSZWNvcmQ8c3RyaW5nLCBrZXlvZiBUaGVtZT4gPSB7fVxuICBpZiAoaXNBZ2VudFN3YXJtc0VuYWJsZWQoKSAmJiB0ZWFtQ29udGV4dD8udGVhbW1hdGVzKSB7XG4gICAgZm9yIChjb25zdCB0ZWFtbWF0ZSBvZiBPYmplY3QudmFsdWVzKHRlYW1Db250ZXh0LnRlYW1tYXRlcykpIHtcbiAgICAgIGlmICh0ZWFtbWF0ZS5jb2xvcikge1xuICAgICAgICBjb25zdCB0aGVtZUNvbG9yID1cbiAgICAgICAgICBBR0VOVF9DT0xPUl9UT19USEVNRV9DT0xPUlt0ZWFtbWF0ZS5jb2xvciBhcyBBZ2VudENvbG9yTmFtZV1cbiAgICAgICAgaWYgKHRoZW1lQ29sb3IpIHtcbiAgICAgICAgICB0ZWFtbWF0ZUNvbG9yc1t0ZWFtbWF0ZS5uYW1lXSA9IHRoZW1lQ29sb3JcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIC8vIEJ1aWxkIGEgbWFwIG9mIHRlYW1tYXRlIG5hbWUgLT4gY3VycmVudCBhY3Rpdml0eSBkZXNjcmlwdGlvblxuICAvLyBNYXAgYm90aCBhZ2VudE5hbWUgKFwicmVzZWFyY2hlclwiKSBhbmQgYWdlbnRJZCAoXCJyZXNlYXJjaGVyQHRlYW1cIikgc29cbiAgLy8gdGFzayBvd25lcnMgbWF0Y2ggcmVnYXJkbGVzcyBvZiB3aGljaCBmb3JtYXQgdGhlIG1vZGVsIHVzZWQuXG4gIC8vIFJvbGxzIHVwIGNvbnNlY3V0aXZlIHNlYXJjaC9yZWFkIHRvb2wgdXNlcyBpbnRvIGEgY29tcGFjdCBzdW1tYXJ5LlxuICAvLyBBbHNvIHRyYWNrIHdoaWNoIHRlYW1tYXRlcyBhcmUgc3RpbGwgcnVubmluZyAobm90IHNodXQgZG93bikuXG4gIGNvbnN0IHRlYW1tYXRlQWN0aXZpdHk6IFJlY29yZDxzdHJpbmcsIHN0cmluZz4gPSB7fVxuICBjb25zdCBhY3RpdmVUZWFtbWF0ZXMgPSBuZXcgU2V0PHN0cmluZz4oKVxuICBpZiAoaXNBZ2VudFN3YXJtc0VuYWJsZWQoKSkge1xuICAgIGZvciAoY29uc3QgYmdUYXNrIG9mIE9iamVjdC52YWx1ZXMoYXBwU3RhdGVUYXNrcykpIHtcbiAgICAgIGlmIChpc0luUHJvY2Vzc1RlYW1tYXRlVGFzayhiZ1Rhc2spICYmIGJnVGFzay5zdGF0dXMgPT09ICdydW5uaW5nJykge1xuICAgICAgICBhY3RpdmVUZWFtbWF0ZXMuYWRkKGJnVGFzay5pZGVudGl0eS5hZ2VudE5hbWUpXG4gICAgICAgIGFjdGl2ZVRlYW1tYXRlcy5hZGQoYmdUYXNrLmlkZW50aXR5LmFnZW50SWQpXG4gICAgICAgIGNvbnN0IGFjdGl2aXRpZXMgPSBiZ1Rhc2sucHJvZ3Jlc3M/LnJlY2VudEFjdGl2aXRpZXNcbiAgICAgICAgY29uc3QgZGVzYyA9XG4gICAgICAgICAgKGFjdGl2aXRpZXMgJiYgc3VtbWFyaXplUmVjZW50QWN0aXZpdGllcyhhY3Rpdml0aWVzKSkgPz9cbiAgICAgICAgICBiZ1Rhc2sucHJvZ3Jlc3M/Lmxhc3RBY3Rpdml0eT8uYWN0aXZpdHlEZXNjcmlwdGlvblxuICAgICAgICBpZiAoZGVzYykge1xuICAgICAgICAgIHRlYW1tYXRlQWN0aXZpdHlbYmdUYXNrLmlkZW50aXR5LmFnZW50TmFtZV0gPSBkZXNjXG4gICAgICAgICAgdGVhbW1hdGVBY3Rpdml0eVtiZ1Rhc2suaWRlbnRpdHkuYWdlbnRJZF0gPSBkZXNjXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICAvLyBHZXQgdGFzayBjb3VudHMgZm9yIGRpc3BsYXlcbiAgY29uc3QgY29tcGxldGVkQ291bnQgPSBjb3VudCh0YXNrcywgdCA9PiB0LnN0YXR1cyA9PT0gJ2NvbXBsZXRlZCcpXG4gIGNvbnN0IHBlbmRpbmdDb3VudCA9IGNvdW50KHRhc2tzLCB0ID0+IHQuc3RhdHVzID09PSAncGVuZGluZycpXG4gIGNvbnN0IGluUHJvZ3Jlc3NDb3VudCA9IHRhc2tzLmxlbmd0aCAtIGNvbXBsZXRlZENvdW50IC0gcGVuZGluZ0NvdW50XG4gIC8vIFVucmVzb2x2ZWQgdGFza3MgKG9wZW4gb3IgaW5fcHJvZ3Jlc3MpIGJsb2NrIGRlcGVuZGVudCB0YXNrc1xuICBjb25zdCB1bnJlc29sdmVkVGFza0lkcyA9IG5ldyBTZXQoXG4gICAgdGFza3MuZmlsdGVyKHQgPT4gdC5zdGF0dXMgIT09ICdjb21wbGV0ZWQnKS5tYXAodCA9PiB0LmlkKSxcbiAgKVxuXG4gIC8vIENoZWNrIGlmIHdlIG5lZWQgdG8gdHJ1bmNhdGVcbiAgY29uc3QgbmVlZHNUcnVuY2F0aW9uID0gdGFza3MubGVuZ3RoID4gbWF4RGlzcGxheVxuXG4gIGxldCB2aXNpYmxlVGFza3M6IFRhc2tbXVxuICBsZXQgaGlkZGVuVGFza3M6IFRhc2tbXVxuXG4gIGlmIChuZWVkc1RydW5jYXRpb24pIHtcbiAgICAvLyBQcmlvcml0aXplOiByZWNlbnRseSBjb21wbGV0ZWQgKHdpdGhpbiAzMHMpLCBpbi1wcm9ncmVzcywgcGVuZGluZywgb2xkZXIgY29tcGxldGVkXG4gICAgY29uc3QgcmVjZW50Q29tcGxldGVkOiBUYXNrW10gPSBbXVxuICAgIGNvbnN0IG9sZGVyQ29tcGxldGVkOiBUYXNrW10gPSBbXVxuICAgIGZvciAoY29uc3QgdGFzayBvZiB0YXNrcy5maWx0ZXIodCA9PiB0LnN0YXR1cyA9PT0gJ2NvbXBsZXRlZCcpKSB7XG4gICAgICBjb25zdCB0cyA9IGNvbXBsZXRpb25UaW1lc3RhbXBzUmVmLmN1cnJlbnQuZ2V0KHRhc2suaWQpXG4gICAgICBpZiAodHMgJiYgbm93IC0gdHMgPCBSRUNFTlRfQ09NUExFVEVEX1RUTF9NUykge1xuICAgICAgICByZWNlbnRDb21wbGV0ZWQucHVzaCh0YXNrKVxuICAgICAgfSBlbHNlIHtcbiAgICAgICAgb2xkZXJDb21wbGV0ZWQucHVzaCh0YXNrKVxuICAgICAgfVxuICAgIH1cbiAgICByZWNlbnRDb21wbGV0ZWQuc29ydChieUlkQXNjKVxuICAgIG9sZGVyQ29tcGxldGVkLnNvcnQoYnlJZEFzYylcbiAgICBjb25zdCBpblByb2dyZXNzID0gdGFza3NcbiAgICAgIC5maWx0ZXIodCA9PiB0LnN0YXR1cyA9PT0gJ2luX3Byb2dyZXNzJylcbiAgICAgIC5zb3J0KGJ5SWRBc2MpXG4gICAgY29uc3QgcGVuZGluZyA9IHRhc2tzXG4gICAgICAuZmlsdGVyKHQgPT4gdC5zdGF0dXMgPT09ICdwZW5kaW5nJylcbiAgICAgIC5zb3J0KChhLCBiKSA9PiB7XG4gICAgICAgIGNvbnN0IGFCbG9ja2VkID0gYS5ibG9ja2VkQnkuc29tZShpZCA9PiB1bnJlc29sdmVkVGFza0lkcy5oYXMoaWQpKVxuICAgICAgICBjb25zdCBiQmxvY2tlZCA9IGIuYmxvY2tlZEJ5LnNvbWUoaWQgPT4gdW5yZXNvbHZlZFRhc2tJZHMuaGFzKGlkKSlcbiAgICAgICAgaWYgKGFCbG9ja2VkICE9PSBiQmxvY2tlZCkge1xuICAgICAgICAgIHJldHVybiBhQmxvY2tlZCA/IDEgOiAtMVxuICAgICAgICB9XG4gICAgICAgIHJldHVybiBieUlkQXNjKGEsIGIpXG4gICAgICB9KVxuXG4gICAgY29uc3QgcHJpb3JpdGl6ZWQgPSBbXG4gICAgICAuLi5yZWNlbnRDb21wbGV0ZWQsXG4gICAgICAuLi5pblByb2dyZXNzLFxuICAgICAgLi4ucGVuZGluZyxcbiAgICAgIC4uLm9sZGVyQ29tcGxldGVkLFxuICAgIF1cbiAgICB2aXNpYmxlVGFza3MgPSBwcmlvcml0aXplZC5zbGljZSgwLCBtYXhEaXNwbGF5KVxuICAgIGhpZGRlblRhc2tzID0gcHJpb3JpdGl6ZWQuc2xpY2UobWF4RGlzcGxheSlcbiAgfSBlbHNlIHtcbiAgICAvLyBObyB0cnVuY2F0aW9uIG5lZWRlZCDigJQgc29ydCBieSBJRCBmb3Igc3RhYmxlIG9yZGVyaW5nXG4gICAgdmlzaWJsZVRhc2tzID0gWy4uLnRhc2tzXS5zb3J0KGJ5SWRBc2MpXG4gICAgaGlkZGVuVGFza3MgPSBbXVxuICB9XG5cbiAgbGV0IGhpZGRlblN1bW1hcnkgPSAnJ1xuICBpZiAoaGlkZGVuVGFza3MubGVuZ3RoID4gMCkge1xuICAgIGNvbnN0IHBhcnRzOiBzdHJpbmdbXSA9IFtdXG4gICAgY29uc3QgaGlkZGVuUGVuZGluZyA9IGNvdW50KGhpZGRlblRhc2tzLCB0ID0+IHQuc3RhdHVzID09PSAncGVuZGluZycpXG4gICAgY29uc3QgaGlkZGVuSW5Qcm9ncmVzcyA9IGNvdW50KGhpZGRlblRhc2tzLCB0ID0+IHQuc3RhdHVzID09PSAnaW5fcHJvZ3Jlc3MnKVxuICAgIGNvbnN0IGhpZGRlbkNvbXBsZXRlZCA9IGNvdW50KGhpZGRlblRhc2tzLCB0ID0+IHQuc3RhdHVzID09PSAnY29tcGxldGVkJylcbiAgICBpZiAoaGlkZGVuSW5Qcm9ncmVzcyA+IDApIHtcbiAgICAgIHBhcnRzLnB1c2goYCR7aGlkZGVuSW5Qcm9ncmVzc30gaW4gcHJvZ3Jlc3NgKVxuICAgIH1cbiAgICBpZiAoaGlkZGVuUGVuZGluZyA+IDApIHtcbiAgICAgIHBhcnRzLnB1c2goYCR7aGlkZGVuUGVuZGluZ30gcGVuZGluZ2ApXG4gICAgfVxuICAgIGlmIChoaWRkZW5Db21wbGV0ZWQgPiAwKSB7XG4gICAgICBwYXJ0cy5wdXNoKGAke2hpZGRlbkNvbXBsZXRlZH0gY29tcGxldGVkYClcbiAgICB9XG4gICAgaGlkZGVuU3VtbWFyeSA9IGAg4oCmICske3BhcnRzLmpvaW4oJywgJyl9YFxuICB9XG5cbiAgY29uc3QgY29udGVudCA9IChcbiAgICA8PlxuICAgICAge3Zpc2libGVUYXNrcy5tYXAodGFzayA9PiAoXG4gICAgICAgIDxUYXNrSXRlbVxuICAgICAgICAgIGtleT17dGFzay5pZH1cbiAgICAgICAgICB0YXNrPXt0YXNrfVxuICAgICAgICAgIG93bmVyQ29sb3I9e3Rhc2sub3duZXIgPyB0ZWFtbWF0ZUNvbG9yc1t0YXNrLm93bmVyXSA6IHVuZGVmaW5lZH1cbiAgICAgICAgICBvcGVuQmxvY2tlcnM9e3Rhc2suYmxvY2tlZEJ5LmZpbHRlcihpZCA9PiB1bnJlc29sdmVkVGFza0lkcy5oYXMoaWQpKX1cbiAgICAgICAgICBhY3Rpdml0eT17dGFzay5vd25lciA/IHRlYW1tYXRlQWN0aXZpdHlbdGFzay5vd25lcl0gOiB1bmRlZmluZWR9XG4gICAgICAgICAgb3duZXJBY3RpdmU9e3Rhc2sub3duZXIgPyBhY3RpdmVUZWFtbWF0ZXMuaGFzKHRhc2sub3duZXIpIDogZmFsc2V9XG4gICAgICAgICAgY29sdW1ucz17Y29sdW1uc31cbiAgICAgICAgLz5cbiAgICAgICkpfVxuICAgICAge21heERpc3BsYXkgPiAwICYmIGhpZGRlblN1bW1hcnkgJiYgPFRleHQgZGltQ29sb3I+e2hpZGRlblN1bW1hcnl9PC9UZXh0Pn1cbiAgICA8Lz5cbiAgKVxuXG4gIGlmIChpc1N0YW5kYWxvbmUpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfSBtYXJnaW5MZWZ0PXsyfT5cbiAgICAgICAgPEJveD5cbiAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgIDxUZXh0IGJvbGQ+e3Rhc2tzLmxlbmd0aH08L1RleHQ+XG4gICAgICAgICAgICB7JyB0YXNrcyAoJ31cbiAgICAgICAgICAgIDxUZXh0IGJvbGQ+e2NvbXBsZXRlZENvdW50fTwvVGV4dD5cbiAgICAgICAgICAgIHsnIGRvbmUsICd9XG4gICAgICAgICAgICB7aW5Qcm9ncmVzc0NvdW50ID4gMCAmJiAoXG4gICAgICAgICAgICAgIDw+XG4gICAgICAgICAgICAgICAgPFRleHQgYm9sZD57aW5Qcm9ncmVzc0NvdW50fTwvVGV4dD5cbiAgICAgICAgICAgICAgICB7JyBpbiBwcm9ncmVzcywgJ31cbiAgICAgICAgICAgICAgPC8+XG4gICAgICAgICAgICApfVxuICAgICAgICAgICAgPFRleHQgYm9sZD57cGVuZGluZ0NvdW50fTwvVGV4dD5cbiAgICAgICAgICAgIHsnIG9wZW4pJ31cbiAgICAgICAgICA8L1RleHQ+XG4gICAgICAgIDwvQm94PlxuICAgICAgICB7Y29udGVudH1cbiAgICAgIDwvQm94PlxuICAgIClcbiAgfVxuXG4gIHJldHVybiA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj57Y29udGVudH08L0JveD5cbn1cblxudHlwZSBUYXNrSXRlbVByb3BzID0ge1xuICB0YXNrOiBUYXNrXG4gIG93bmVyQ29sb3I/OiBrZXlvZiBUaGVtZVxuICBvcGVuQmxvY2tlcnM6IHN0cmluZ1tdXG4gIGFjdGl2aXR5Pzogc3RyaW5nXG4gIG93bmVyQWN0aXZlOiBib29sZWFuXG4gIGNvbHVtbnM6IG51bWJlclxufVxuXG5mdW5jdGlvbiBnZXRUYXNrSWNvbihzdGF0dXM6IFRhc2tbJ3N0YXR1cyddKToge1xuICBpY29uOiBzdHJpbmdcbiAgY29sb3I6IGtleW9mIFRoZW1lIHwgdW5kZWZpbmVkXG59IHtcbiAgc3dpdGNoIChzdGF0dXMpIHtcbiAgICBjYXNlICdjb21wbGV0ZWQnOlxuICAgICAgcmV0dXJuIHsgaWNvbjogZmlndXJlcy50aWNrLCBjb2xvcjogJ3N1Y2Nlc3MnIH1cbiAgICBjYXNlICdpbl9wcm9ncmVzcyc6XG4gICAgICByZXR1cm4geyBpY29uOiBmaWd1cmVzLnNxdWFyZVNtYWxsRmlsbGVkLCBjb2xvcjogJ2NsYXVkZScgfVxuICAgIGNhc2UgJ3BlbmRpbmcnOlxuICAgICAgcmV0dXJuIHsgaWNvbjogZmlndXJlcy5zcXVhcmVTbWFsbCwgY29sb3I6IHVuZGVmaW5lZCB9XG4gIH1cbn1cblxuZnVuY3Rpb24gVGFza0l0ZW0oe1xuICB0YXNrLFxuICBvd25lckNvbG9yLFxuICBvcGVuQmxvY2tlcnMsXG4gIGFjdGl2aXR5LFxuICBvd25lckFjdGl2ZSxcbiAgY29sdW1ucyxcbn06IFRhc2tJdGVtUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBpc0NvbXBsZXRlZCA9IHRhc2suc3RhdHVzID09PSAnY29tcGxldGVkJ1xuICBjb25zdCBpc0luUHJvZ3Jlc3MgPSB0YXNrLnN0YXR1cyA9PT0gJ2luX3Byb2dyZXNzJ1xuICBjb25zdCBpc0Jsb2NrZWQgPSBvcGVuQmxvY2tlcnMubGVuZ3RoID4gMFxuXG4gIGNvbnN0IHsgaWNvbiwgY29sb3IgfSA9IGdldFRhc2tJY29uKHRhc2suc3RhdHVzKVxuXG4gIGNvbnN0IHNob3dBY3Rpdml0eSA9IGlzSW5Qcm9ncmVzcyAmJiAhaXNCbG9ja2VkICYmIGFjdGl2aXR5XG5cbiAgLy8gUmVzcG9uc2l2ZSBsYXlvdXQ6IGhpZGUgb3duZXIgb24gbmFycm93IHNjcmVlbnMgKDw2MCBjb2xzKVxuICAvLyBUcnVuY2F0ZSBzdWJqZWN0IGJhc2VkIG9uIGF2YWlsYWJsZSBzcGFjZVxuICBjb25zdCBzaG93T3duZXIgPSBjb2x1bW5zID49IDYwICYmIHRhc2sub3duZXIgJiYgb3duZXJBY3RpdmVcbiAgY29uc3Qgb3duZXJXaWR0aCA9IHNob3dPd25lciA/IHN0cmluZ1dpZHRoKGAgKEAke3Rhc2sub3duZXJ9KWApIDogMFxuICAvLyBBY2NvdW50IGZvcjogaWNvbigyKSArIGluZGVudGF0aW9uKH44IHdoZW4gbmVzdGVkIHVuZGVyIHNwaW5uZXIpICsgb3duZXIgKyBzYWZldHlcbiAgLy8gVXNlIGNvbHVtbnMgLSAxNSBhcyBhIGNvbnNlcnZhdGl2ZSBlc3RpbWF0ZSBmb3IgbmVzdGVkIGxheW91dHNcbiAgY29uc3QgbWF4U3ViamVjdFdpZHRoID0gTWF0aC5tYXgoMTUsIGNvbHVtbnMgLSAxNSAtIG93bmVyV2lkdGgpXG4gIGNvbnN0IGRpc3BsYXlTdWJqZWN0ID0gdHJ1bmNhdGVUb1dpZHRoKHRhc2suc3ViamVjdCwgbWF4U3ViamVjdFdpZHRoKVxuXG4gIC8vIFRydW5jYXRlIGFjdGl2aXR5IGZvciBuYXJyb3cgc2NyZWVuc1xuICBjb25zdCBtYXhBY3Rpdml0eVdpZHRoID0gTWF0aC5tYXgoMTUsIGNvbHVtbnMgLSAxNSlcbiAgY29uc3QgZGlzcGxheUFjdGl2aXR5ID0gYWN0aXZpdHlcbiAgICA/IHRydW5jYXRlVG9XaWR0aChhY3Rpdml0eSwgbWF4QWN0aXZpdHlXaWR0aClcbiAgICA6IHVuZGVmaW5lZFxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICA8Qm94PlxuICAgICAgICA8VGV4dCBjb2xvcj17Y29sb3J9PntpY29ufSA8L1RleHQ+XG4gICAgICAgIDxUZXh0XG4gICAgICAgICAgYm9sZD17aXNJblByb2dyZXNzfVxuICAgICAgICAgIHN0cmlrZXRocm91Z2g9e2lzQ29tcGxldGVkfVxuICAgICAgICAgIGRpbUNvbG9yPXtpc0NvbXBsZXRlZCB8fCBpc0Jsb2NrZWR9XG4gICAgICAgID5cbiAgICAgICAgICB7ZGlzcGxheVN1YmplY3R9XG4gICAgICAgIDwvVGV4dD5cbiAgICAgICAge3Nob3dPd25lciAmJiAoXG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+XG4gICAgICAgICAgICB7JyAoJ31cbiAgICAgICAgICAgIHtvd25lckNvbG9yID8gKFxuICAgICAgICAgICAgICA8VGhlbWVkVGV4dCBjb2xvcj17b3duZXJDb2xvcn0+QHt0YXNrLm93bmVyfTwvVGhlbWVkVGV4dD5cbiAgICAgICAgICAgICkgOiAoXG4gICAgICAgICAgICAgIGBAJHt0YXNrLm93bmVyfWBcbiAgICAgICAgICAgICl9XG4gICAgICAgICAgICB7JyknfVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgKX1cbiAgICAgICAge2lzQmxvY2tlZCAmJiAoXG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+XG4gICAgICAgICAgICB7JyAnfVxuICAgICAgICAgICAge2ZpZ3VyZXMucG9pbnRlclNtYWxsfSBibG9ja2VkIGJ5eycgJ31cbiAgICAgICAgICAgIHtbLi4ub3BlbkJsb2NrZXJzXVxuICAgICAgICAgICAgICAuc29ydCgoYSwgYikgPT4gcGFyc2VJbnQoYSwgMTApIC0gcGFyc2VJbnQoYiwgMTApKVxuICAgICAgICAgICAgICAubWFwKGlkID0+IGAjJHtpZH1gKVxuICAgICAgICAgICAgICAuam9pbignLCAnKX1cbiAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICl9XG4gICAgICA8L0JveD5cbiAgICAgIHtzaG93QWN0aXZpdHkgJiYgZGlzcGxheUFjdGl2aXR5ICYmIChcbiAgICAgICAgPEJveD5cbiAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgIHsnICAnfVxuICAgICAgICAgICAge2Rpc3BsYXlBY3Rpdml0eX1cbiAgICAgICAgICAgIHtmaWd1cmVzLmVsbGlwc2lzfVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICApfVxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxPQUFPQSxPQUFPLE1BQU0sU0FBUztBQUM3QixPQUFPLEtBQUtDLEtBQUssTUFBTSxPQUFPO0FBQzlCLFNBQVNDLGVBQWUsUUFBUSw2QkFBNkI7QUFDN0QsU0FBU0MsV0FBVyxRQUFRLHVCQUF1QjtBQUNuRCxTQUFTQyxHQUFHLEVBQUVDLElBQUksUUFBUSxXQUFXO0FBQ3JDLFNBQVNDLFdBQVcsUUFBUSxzQkFBc0I7QUFDbEQsU0FBU0MsdUJBQXVCLFFBQVEseUNBQXlDO0FBQ2pGLFNBQ0VDLDBCQUEwQixFQUMxQixLQUFLQyxjQUFjLFFBQ2QseUNBQXlDO0FBQ2hELFNBQVNDLG9CQUFvQixRQUFRLGdDQUFnQztBQUNyRSxTQUFTQyxLQUFLLFFBQVEsbUJBQW1CO0FBQ3pDLFNBQVNDLHlCQUF5QixRQUFRLGdDQUFnQztBQUMxRSxTQUFTQyxlQUFlLFFBQVEsb0JBQW9CO0FBQ3BELFNBQVNDLGVBQWUsRUFBRSxLQUFLQyxJQUFJLFFBQVEsbUJBQW1CO0FBQzlELGNBQWNDLEtBQUssUUFBUSxtQkFBbUI7QUFDOUMsT0FBT0MsVUFBVSxNQUFNLCtC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 \ No newline at end of file diff --git a/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d17ee6980d9bef3298ab6b3a8dd4d2d5489b88a8 --- /dev/null +++ b/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx @@ -0,0 +1,143 @@ +import { APIUserAbortError } from '@anthropic-ai/sdk'; +import React, { type ReactNode, useCallback, useRef, useState } from 'react'; +import { useMainLoopModel } from '../../../../hooks/useMainLoopModel.js'; +import { Box, Text } from '../../../../ink.js'; +import { useKeybinding } from '../../../../keybindings/useKeybinding.js'; +import { createAbortController } from '../../../../utils/abortController.js'; +import { editPromptInEditor } from '../../../../utils/promptEditor.js'; +import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'; +import { Byline } from '../../../design-system/Byline.js'; +import { Spinner } from '../../../Spinner.js'; +import TextInput from '../../../TextInput.js'; +import { useWizard } from '../../../wizard/index.js'; +import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'; +import { generateAgent } from '../../generateAgent.js'; +import type { AgentWizardData } from '../types.js'; +export function GenerateStep(): ReactNode { + const { + updateWizardData, + goBack, + goToStep, + wizardData + } = useWizard(); + const [prompt, setPrompt] = useState(wizardData.generationPrompt || ''); + const [isGenerating, setIsGenerating] = useState(false); + const [error, setError] = useState(null); + const [cursorOffset, setCursorOffset] = useState(prompt.length); + const model = useMainLoopModel(); + const abortControllerRef = useRef(null); + + // Cancel generation when escape pressed during generation + const handleCancelGeneration = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + setIsGenerating(false); + setError('Generation cancelled'); + } + }, []); + + // Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input) + useKeybinding('confirm:no', handleCancelGeneration, { + context: 'Settings', + isActive: isGenerating + }); + const handleExternalEditor = useCallback(async () => { + const result = await editPromptInEditor(prompt); + if (result.content !== null) { + setPrompt(result.content); + setCursorOffset(result.content.length); + } + }, [prompt]); + useKeybinding('chat:externalEditor', handleExternalEditor, { + context: 'Chat', + isActive: !isGenerating + }); + + // Go back when escape pressed while not generating + const handleGoBack = useCallback(() => { + updateWizardData({ + generationPrompt: '', + agentType: '', + systemPrompt: '', + whenToUse: '', + generatedAgent: undefined, + wasGenerated: false + }); + setPrompt(''); + setError(null); + goBack(); + }, [updateWizardData, goBack]); + + // Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input) + useKeybinding('confirm:no', handleGoBack, { + context: 'Settings', + isActive: !isGenerating + }); + const handleGenerate = async (): Promise => { + const trimmedPrompt = prompt.trim(); + if (!trimmedPrompt) { + setError('Please describe what the agent should do'); + return; + } + setError(null); + setIsGenerating(true); + updateWizardData({ + generationPrompt: trimmedPrompt, + isGenerating: true + }); + + // Create abort controller for this generation + const controller = createAbortController(); + abortControllerRef.current = controller; + try { + const generated = await generateAgent(trimmedPrompt, model, [], controller.signal); + updateWizardData({ + agentType: generated.identifier, + whenToUse: generated.whenToUse, + systemPrompt: generated.systemPrompt, + generatedAgent: generated, + isGenerating: false, + wasGenerated: true + }); + + // Skip directly to ToolsStep (index 6) - matching original flow + goToStep(6); + } catch (err) { + // Don't show error if it was cancelled (already set in escape handler) + if (err instanceof APIUserAbortError) { + // User cancelled - no error to show + } else if (err instanceof Error && !err.message.includes('No assistant message found')) { + setError(err.message || 'Failed to generate agent'); + } + updateWizardData({ + isGenerating: false + }); + } finally { + setIsGenerating(false); + abortControllerRef.current = null; + } + }; + const subtitle = 'Describe what this agent should do and when it should be used (be comprehensive for best results)'; + if (isGenerating) { + return }> + + + Generating agent from description... + + ; + } + return + + + + }> + + {error && + {error} + } + + + ; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJBUElVc2VyQWJvcnRFcnJvciIsIlJlYWN0IiwiUmVhY3ROb2RlIiwidXNlQ2FsbGJhY2siLCJ1c2VSZWYiLCJ1c2VTdGF0ZSIsInVzZU1haW5Mb29wTW9kZWwiLCJCb3giLCJUZXh0IiwidXNlS2V5YmluZGluZyIsImNyZWF0ZUFib3J0Q29udHJvbGxlciIsImVkaXRQcm9tcHRJbkVkaXRvciIsIkNvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCIsIkJ5bGluZSIsIlNwaW5uZXIiLCJUZXh0SW5wdXQiLCJ1c2VXaXphcmQiLCJXaXphcmREaWFsb2dMYXlvdXQiLCJnZW5lcmF0ZUFnZW50IiwiQWdlbnRXaXphcmREYXRhIiwiR2VuZXJhdGVTdGVwIiwidXBkYXRlV2l6YXJkRGF0YSIsImdvQmFjayIsImdvVG9TdGVwIiwid2l6YXJkRGF0YSIsInByb21wdCIsInNldFByb21wdCIsImdlbmVyYXRpb25Qcm9tcHQiLCJpc0dlbmVyYXRpbmciLCJzZXRJc0dlbmVyYXRpbmciLCJlcnJvciIsInNldEVycm9yIiwiY3Vyc29yT2Zmc2V0Iiwic2V0Q3Vyc29yT2Zmc2V0IiwibGVuZ3RoIiwibW9kZWwiLCJhYm9ydENvbnRyb2xsZXJSZWYiLCJBYm9ydENvbnRyb2xsZXIiLCJoYW5kbGVDYW5jZWxHZW5lcmF0aW9uIiwiY3VycmVudCIsImFib3J0IiwiY29udGV4dCIsImlzQWN0aXZlIiwiaGFuZGxlRXh0ZXJuYWxFZGl0b3IiLCJyZXN1bHQiLCJjb250ZW50IiwiaGFuZGxlR29CYWNrIiwiYWdlbnRUeXBlIiwic3lzdGVtUHJvbXB0Iiwid2hlblRvVXNlIiwiZ2VuZXJhdGVkQWdlbnQiLCJ1bmRlZmluZWQiLCJ3YXNHZW5lcmF0ZWQiLCJoYW5kbGVHZW5lcmF0ZSIsIlByb21pc2UiLCJ0cmltbWVkUHJvbXB0IiwidHJpbSIsImNvbnRyb2xsZXIiLCJnZW5lcmF0ZWQiLCJzaWduYWwiLCJpZGVudGlmaWVyIiwiZXJyIiwiRXJyb3IiLCJtZXNzYWdlIiwiaW5jbHVkZXMiLCJzdWJ0aXRsZSJdLCJzb3VyY2VzIjpbIkdlbmVyYXRlU3RlcC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgQVBJVXNlckFib3J0RXJyb3IgfSBmcm9tICdAYW50aHJvcGljLWFpL3NkaydcbmltcG9ydCBSZWFjdCwgeyB0eXBlIFJlYWN0Tm9kZSwgdXNlQ2FsbGJhY2ssIHVzZVJlZiwgdXNlU3RhdGUgfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZU1haW5Mb29wTW9kZWwgfSBmcm9tICcuLi8uLi8uLi8uLi9ob29rcy91c2VNYWluTG9vcE1vZGVsLmpzJ1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnLi4vLi4vLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHsgdXNlS2V5YmluZGluZyB9IGZyb20gJy4uLy4uLy4uLy4uL2tleWJpbmRpbmdzL3VzZUtleWJpbmRpbmcuanMnXG5pbXBvcnQgeyBjcmVhdGVBYm9ydENvbnRyb2xsZXIgfSBmcm9tICcuLi8uLi8uLi8uLi91dGlscy9hYm9ydENvbnRyb2xsZXIuanMnXG5pbXBvcnQgeyBlZGl0UHJvbXB0SW5FZGl0b3IgfSBmcm9tICcuLi8uLi8uLi8uLi91dGlscy9wcm9tcHRFZGl0b3IuanMnXG5pbXBvcnQgeyBDb25maWd1cmFibGVTaG9ydGN1dEhpbnQgfSBmcm9tICcuLi8uLi8uLi9Db25maWd1cmFibGVTaG9ydGN1dEhpbnQuanMnXG5pbXBvcnQgeyBCeWxpbmUgfSBmcm9tICcuLi8uLi8uLi9kZXNpZ24tc3lzdGVtL0J5bGluZS5qcydcbmltcG9ydCB7IFNwaW5uZXIgfSBmcm9tICcuLi8uLi8uLi9TcGlubmVyLmpzJ1xuaW1wb3J0IFRleHRJbnB1dCBmcm9tICcuLi8uLi8uLi9UZXh0SW5wdXQuanMnXG5pbXBvcnQgeyB1c2VXaXphcmQgfSBmcm9tICcuLi8uLi8uLi93aXphcmQvaW5kZXguanMnXG5pbXBvcnQgeyBXaXphcmREaWFsb2dMYXlvdXQgfSBmcm9tICcuLi8uLi8uLi93aXphcmQvV2l6YXJkRGlhbG9nTGF5b3V0LmpzJ1xuaW1wb3J0IHsgZ2VuZXJhdGVBZ2VudCB9IGZyb20gJy4uLy4uL2dlbmVyYXRlQWdlbnQuanMnXG5pbXBvcnQgdHlwZSB7IEFnZW50V2l6YXJkRGF0YSB9IGZyb20gJy4uL3R5cGVzLmpzJ1xuXG5leHBvcnQgZnVuY3Rpb24gR2VuZXJhdGVTdGVwKCk6IFJlYWN0Tm9kZSB7XG4gIGNvbnN0IHsgdXBkYXRlV2l6YXJkRGF0YSwgZ29CYWNrLCBnb1RvU3RlcCwgd2l6YXJkRGF0YSB9ID1cbiAgICB1c2VXaXphcmQ8QWdlbnRXaXphcmREYXRhPigpXG4gIGNvbnN0IFtwcm9tcHQsIHNldFByb21wdF0gPSB1c2VTdGF0ZSh3aXphcmREYXRhLmdlbmVyYXRpb25Qcm9tcHQgfHwgJycpXG4gIGNvbnN0IFtpc0dlbmVyYXRpbmcsIHNldElzR2VuZXJhdGluZ10gPSB1c2VTdGF0ZShmYWxzZSlcbiAgY29uc3QgW2Vycm9yLCBzZXRFcnJvcl0gPSB1c2VTdGF0ZTxzdHJpbmcgfCBudWxsPihudWxsKVxuICBjb25zdCBbY3Vyc29yT2Zmc2V0LCBzZXRDdXJzb3JPZmZzZXRdID0gdXNlU3RhdGUocHJvbXB0Lmxlbmd0aClcbiAgY29uc3QgbW9kZWwgPSB1c2VNYWluTG9vcE1vZGVsKClcbiAgY29uc3QgYWJvcnRDb250cm9sbGVyUmVmID0gdXNlUmVmPEFib3J0Q29udHJvbGxlciB8IG51bGw+KG51bGwpXG5cbiAgLy8gQ2FuY2VsIGdlbmVyYXRpb24gd2hlbiBlc2NhcGUgcHJlc3NlZCBkdXJpbmcgZ2VuZXJhdGlvblxuICBjb25zdCBoYW5kbGVDYW5jZWxHZW5lcmF0aW9uID0gdXNlQ2FsbGJhY2soKCkgPT4ge1xuICAgIGlmIChhYm9ydENvbnRyb2xsZXJSZWYuY3VycmVudCkge1xuICAgICAgYWJvcnRDb250cm9sbGVyUmVmLmN1cnJlbnQuYWJvcnQoKVxuICAgICAgYWJvcnRDb250cm9sbGVyUmVmLmN1cnJlbnQgPSBudWxsXG4gICAgICBzZXRJc0dlbmVyYXRpbmcoZmFsc2UpXG4gICAgICBzZXRFcnJvcignR2VuZXJhdGlvbiBjYW5jZWxsZWQnKVxuICAgIH1cbiAgfSwgW10pXG5cbiAgLy8gVXNlIFNldHRpbmdzIGNvbnRleHQgc28gJ24nIGtleSBkb2Vzbid0IGNhbmNlbCAoYWxsb3dzIHR5cGluZyAnbicgaW4gcHJvbXB0IGlucHV0KVxuICB1c2VLZXliaW5kaW5nKCdjb25maXJtOm5vJywgaGFuZGxlQ2FuY2VsR2VuZXJhdGlvbiwge1xuICAgIGNvbnRleHQ6ICdTZXR0aW5ncycsXG4gICAgaXNBY3RpdmU6IGlzR2VuZXJhdGluZyxcbiAgfSlcblxuICBjb25zdCBoYW5kbGVFeHRlcm5hbEVkaXRvciA9IHVzZUNhbGxiYWNrKGFzeW5jICgpID0+IHtcbiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBlZGl0UHJvbXB0SW5FZGl0b3IocHJvbXB0KVxuICAgIGlmIChyZXN1bHQuY29udGVudCAhPT0gbnVsbCkge1xuICAgICAgc2V0UHJvbXB0KHJlc3VsdC5jb250ZW50KVxuICAgICAgc2V0Q3Vyc29yT2Zmc2V0KHJlc3VsdC5jb250ZW50Lmxlbmd0aClcbiAgICB9XG4gIH0sIFtwcm9tcHRdKVxuXG4gIHVzZUtleWJpbmRpbmcoJ2NoYXQ6ZXh0ZXJuYWxFZGl0b3InLCBoYW5kbGVFeHRlcm5hbEVkaXRvciwge1xuICAgIGNvbnRleHQ6ICdDaGF0JyxcbiAgICBpc0FjdGl2ZTogIWlzR2VuZXJhdGluZyxcbiAgfSlcblxuICAvLyBHbyBiYWNrIHdoZW4gZXNjYXBlIHByZXNzZWQgd2hpbGUgbm90IGdlbmVyYXRpbmdcbiAgY29uc3QgaGFuZGxlR29CYWNrID0gdXNlQ2FsbGJhY2soKCkgPT4ge1xuICAgIHVwZGF0ZVdpemFyZERhdGEoe1xuICAgICAgZ2VuZXJhdGlvblByb21wdDogJycsXG4gICAgICBhZ2VudFR5cGU6ICcnLFxuICAgICAgc3lzdGVtUHJvbXB0OiAnJyxcbiAgICAgIHdoZW5Ub1VzZTogJycsXG4gICAgICBnZW5lcmF0ZWRBZ2VudDogdW5kZWZpbmVkLFxuICAgICAgd2FzR2VuZXJhdGVkOiBmYWxzZSxcbiAgICB9KVxuICAgIHNldFByb21wdCgnJylcbiAgICBzZXRFcnJvcihudWxsKVxuICAgIGdvQmFjaygpXG4gIH0sIFt1cGRhdGVXaXphcmREYXRhLCBnb0JhY2tdKVxuXG4gIC8vIFVzZSBTZXR0aW5ncyBjb250ZXh0IHNvICduJyBrZXkgZG9lc24ndCBjYW5jZWwgKGFsbG93cyB0eXBpbmcgJ24nIGluIHByb21wdCBpbnB1dClcbiAgdXNlS2V5YmluZGluZygnY29uZmlybTpubycsIGhhbmRsZUdvQmFjaywge1xuICAgIGNvbnRleHQ6ICdTZXR0aW5ncycsXG4gICAgaXNBY3RpdmU6ICFpc0dlbmVyYXRpbmcsXG4gIH0pXG5cbiAgY29uc3QgaGFuZGxlR2VuZXJhdGUgPSBhc3luYyAoKTogUHJvbWlzZTx2b2lkPiA9PiB7XG4gICAgY29uc3QgdHJpbW1lZFByb21wdCA9IHByb21wdC50cmltKClcbiAgICBpZiAoIXRyaW1tZWRQcm9tcHQpIHtcbiAgICAgIHNldEVycm9yKCdQbGVhc2UgZGVzY3JpYmUgd2hhdCB0aGUgYWdlbnQgc2hvdWxkIGRvJylcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIHNldEVycm9yKG51bGwpXG4gICAgc2V0SXNHZW5lcmF0aW5nKHRydWUpXG4gICAgdXBkYXRlV2l6YXJkRGF0YSh7XG4gICAgICBnZW5lcmF0aW9uUHJvbXB0OiB0cmltbWVkUHJvbXB0LFxuICAgICAgaXNHZW5lcmF0aW5nOiB0cnVlLFxuICAgIH0pXG5cbiAgICAvLyBDcmVhdGUgYWJvcnQgY29udHJvbGxlciBmb3IgdGhpcyBnZW5lcmF0aW9uXG4gICAgY29uc3QgY29udHJvbGxlciA9IGNyZWF0ZUFib3J0Q29udHJvbGxlcigpXG4gICAgYWJvcnRDb250cm9sbGVyUmVmLmN1cnJlbnQgPSBjb250cm9sbGVyXG5cbiAgICB0cnkge1xuICAgICAgY29uc3QgZ2VuZXJhdGVkID0gYXdhaXQgZ2VuZXJhdGVBZ2VudChcbiAgICAgICAgdHJpbW1lZFByb21wdCxcbiAgICAgICAgbW9kZWwsXG4gICAgICAgIFtdLFxuICAgICAgICBjb250cm9sbGVyLnNpZ25hbCxcbiAgICAgIClcblxuICAgICAgdXBkYXRlV2l6YXJkRGF0YSh7XG4gICAgICAgIGFnZW50VHlwZTogZ2VuZXJhdGVkLmlkZW50aWZpZXIsXG4gICAgICAgIHdoZW5Ub1VzZTogZ2VuZXJhdGVkLndoZW5Ub1VzZSxcbiAgICAgICAgc3lzdGVtUHJvbXB0OiBnZW5lcmF0ZWQuc3lzdGVtUHJvbXB0LFxuICAgICAgICBnZW5lcmF0ZWRBZ2VudDogZ2VuZXJhdGVkLFxuICAgICAgICBpc0dlbmVyYXRpbmc6IGZhbHNlLFxuICAgICAgICB3YXNHZW5lcmF0ZWQ6IHRydWUsXG4gICAgICB9KVxuXG4gICAgICAvLyBTa2lwIGRpcmVjdGx5IHRvIFRvb2xzU3RlcCAoaW5kZXggNikgLSBtYXRjaGluZyBvcmlnaW5hbCBmbG93XG4gICAgICBnb1RvU3RlcCg2KVxuICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgLy8gRG9uJ3Qgc2hvdyBlcnJvciBpZiBpdCB3YXMgY2FuY2VsbGVkIChhbHJlYWR5IHNldCBpbiBlc2NhcGUgaGFuZGxlcilcbiAgICAgIGlmIChlcnIgaW5zdGFuY2VvZiBBUElVc2VyQWJvcnRFcnJvcikge1xuICAgICAgICAvLyBVc2VyIGNhbmNlbGxlZCAtIG5vIGVycm9yIHRvIHNob3dcbiAgICAgIH0gZWxzZSBpZiAoXG4gICAgICAgIGVyciBpbnN0YW5jZW9mIEVycm9yICYmXG4gICAgICAgICFlcnIubWVzc2FnZS5pbmNsdWRlcygnTm8gYXNzaXN0YW50IG1lc3NhZ2UgZm91bmQnKVxuICAgICAgKSB7XG4gICAgICAgIHNldEVycm9yKGVyci5tZXNzYWdlIHx8ICdGYWlsZWQgdG8gZ2VuZXJhdGUgYWdlbnQnKVxuICAgICAgfVxuICAgICAgdXBkYXRlV2l6YXJkRGF0YSh7IGlzR2VuZXJhdGluZzogZmFsc2UgfSlcbiAgICB9IGZpbmFsbHkge1xuICAgICAgc2V0SXNHZW5lcmF0aW5nKGZhbHNlKVxuICAgICAgYWJvcnRDb250cm9sbGVyUmVmLmN1cnJlbnQgPSBudWxsXG4gICAgfVxuICB9XG5cbiAgY29uc3Qgc3VidGl0bGUgPVxuICAgICdEZXNjcmliZSB3aGF0IHRoaXMgYWdlbnQgc2hvdWxkIGRvIGFuZCB3aGVuIGl0IHNob3VsZCBiZSB1c2VkIChiZSBjb21wcmVoZW5zaXZlIGZvciBiZXN0IHJlc3VsdHMpJ1xuXG4gIGlmIChpc0dlbmVyYXRpbmcpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPFdpemFyZERpYWxvZ0xheW91dFxuICAgICAgICBzdWJ0aXRsZT17c3VidGl0bGV9XG4gICAgICAgIGZvb3RlclRleHQ9e1xuICAgICAgICAgIDxDb25maWd1cmFibGVTaG9ydGN1dEhpbnRcbiAgICAgICAgICAgIGFjdGlvbj1cImNvbmZpcm06bm9cIlxuICAgICAgICAgICAgY29udGV4dD1cIlNldHRpbmdzXCJcbiAgICAgICAgICAgIGZhbGxiYWNrPVwiRXNjXCJcbiAgICAgICAgICAgIGRlc2NyaXB0aW9uPVwiY2FuY2VsXCJcbiAgICAgICAgICAvPlxuICAgICAgICB9XG4gICAgICA+XG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiIGFsaWduSXRlbXM9XCJjZW50ZXJcIj5cbiAgICAgICAgICA8U3Bpbm5lciAvPlxuICAgICAgICAgIDxUZXh0IGNvbG9yPVwic3VnZ2VzdGlvblwiPiBHZW5lcmF0aW5nIGFnZW50IGZyb20gZGVzY3JpcHRpb24uLi48L1RleHQ+XG4gICAgICAgIDwvQm94PlxuICAgICAgPC9XaXphcmREaWFsb2dMYXlvdXQ+XG4gICAgKVxuICB9XG5cbiAgcmV0dXJuIChcbiAgICA8V2l6YXJkRGlhbG9nTGF5b3V0XG4gICAgICBzdWJ0aXRsZT17c3VidGl0bGV9XG4gICAgICBmb290ZXJUZXh0PXtcbiAgICAgICAgPEJ5bGluZT5cbiAgICAgICAgICA8Q29uZmlndXJhYmxlU2hvcnRjdXRIaW50XG4gICAgICAgICAgICBhY3Rpb249XCJjb25maXJtOnllc1wiXG4gICAgICAgICAgICBjb250ZXh0PVwiQ29uZmlybWF0aW9uXCJcbiAgICAgICAgICAgIGZhbGxiYWNrPVwiRW50ZXJcIlxuICAgICAgICAgICAgZGVzY3JpcHRpb249XCJzdWJtaXRcIlxuICAgICAgICAgIC8+XG4gICAgICAgICAgPENvbmZpZ3VyYWJsZVNob3J0Y3V0SGludFxuICAgICAgICAgICAgYWN0aW9uPVwiY2hhdDpleHRlcm5hbEVkaXRvclwiXG4gICAgICAgICAgICBjb250ZXh0PVwiQ2hhdFwiXG4gICAgICAgICAgICBmYWxsYmFjaz1cImN0cmwrZ1wiXG4gICAgICAgICAgICBkZXNjcmlwdGlvbj1cIm9wZW4gaW4gZWRpdG9yXCJcbiAgICAgICAgICAvPlxuICAgICAgICAgIDxDb25maWd1cmFibGVTaG9ydGN1dEhpbnRcbiAgICAgICAgICAgIGFjdGlvbj1cImNvbmZpcm06bm9cIlxuICAgICAgICAgICAgY29udGV4dD1cIlNldHRpbmdzXCJcbiAgICAgICAgICAgIGZhbGxiYWNrPVwiRXNjXCJcbiAgICAgICAgICAgIGRlc2NyaXB0aW9uPVwiZ28gYmFja1wiXG4gICAgICAgICAgLz5cbiAgICAgICAgPC9CeWxpbmU+XG4gICAgICB9XG4gICAgPlxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICAgIHtlcnJvciAmJiAoXG4gICAgICAgICAgPEJveCBtYXJnaW5Cb3R0b209ezF9PlxuICAgICAgICAgICAgPFRleHQgY29sb3I9XCJlcnJvclwiPntlcnJvcn08L1RleHQ+XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgICl9XG4gICAgICAgIDxUZXh0SW5wdXRcbiAgICAgICAgICB2YWx1ZT17cHJvbXB0fVxuICAgICAgICAgIG9uQ2hhbmdlPXtzZXRQcm9tcHR9XG4gICAgICAgICAgb25TdWJtaXQ9e2hhbmRsZUdlbmVyYXRlfVxuICAgICAgICAgIHBsYWNlaG9sZGVyPVwiZS5nLiwgSGVscCBtZSB3cml0ZSB1bml0IHRlc3RzIGZvciBteSBjb2RlLi4uXCJcbiAgICAgICAgICBjb2x1bW5zPXs4MH1cbiAgICAgICAgICBjdXJzb3JPZmZzZXQ9e2N1cnNvck9mZnNldH1cbiAgICAgICAgICBvbkNoYW5nZUN1cnNvck9mZnNldD17c2V0Q3Vyc29yT2Zmc2V0fVxuICAgICAgICAgIGZvY3VzXG4gICAgICAgICAgc2hvd0N1cnNvclxuICAgICAgICAvPlxuICAgICAgPC9Cb3g+XG4gICAgPC9XaXphcmREaWFsb2dMYXlvdXQ+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IkFBQUEsU0FBU0EsaUJBQWlCLFFBQVEsbUJBQW1CO0FBQ3JELE9BQU9DLEtBQUssSUFBSSxLQUFLQyxTQUFTLEVBQUVDLFdBQVcsRUFBRUMsTUFBTSxFQUFFQyxRQUFRLFFBQVEsT0FBTztBQUM1RSxTQUFTQyxnQkFBZ0IsUUFBUSx1Q0FBdUM7QUFDeEUsU0FBU0MsR0FBRyxFQUFFQyxJQUFJLFFBQVEsb0JBQW9CO0FBQzlDLFNBQVNDLGFBQWEsUUFBUSwwQ0FBMEM7QUFDeEUsU0FBU0MscUJBQXFCLFFBQVEsc0NBQXNDO0FBQzVFLFNBQVNDLGtCQUFrQixRQUFRLG1DQUFtQztBQUN0RSxTQUFTQyx3QkFBd0IsUUFBUSxzQ0FBc0M7QUFDL0UsU0FBU0MsTUFBTSxRQUFRLGtDQUFrQztBQUN6RCxTQUFTQyxPQUFPLFFBQVEscUJBQXFCO0FBQzdDLE9BQU9DLFNBQVMsTUFBTSx1QkFBdUI7QUFDN0MsU0FBU0MsU0FBUyxRQUFRLDBCQUEwQjtBQUNwRCxTQUFTQyxrQkFBa0IsUUFBUSx1Q0FBdUM7QUFDMUUsU0FBU0MsYUFBYSxRQUFRLHdCQUF3QjtBQUN0RCxjQUFjQyxlQUFlLFFBQVEsYUFBYTtBQUVsRCxPQUFPLFNBQVNDLFlBQVlBLENBQUEsQ0FBRSxFQUFFbEIsU0FBUyxDQUFDO0VBQ3hDLE1BQU07SUFBRW1CLGdCQUFnQjtJQUFFQyxNQUFNO0lBQUVDLFFBQVE7SUFBRUM7RUFBVyxDQUFDLEdBQ3REUixTQUFTLENBQUNHLGVBQWUsQ0FBQyxDQUFDLENBQUM7RUFDOUIsTUFBTSxDQUFD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 \ No newline at end of file diff --git a/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d64c165665517461d2f6c6f41d4cda081f9a843c --- /dev/null +++ b/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx @@ -0,0 +1,80 @@ +import { c as _c } from "react/compiler-runtime"; +import React, { type ReactNode } from 'react'; +import { Box } from '../../../../ink.js'; +import type { SettingSource } from '../../../../utils/settings/constants.js'; +import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'; +import { Select } from '../../../CustomSelect/select.js'; +import { Byline } from '../../../design-system/Byline.js'; +import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'; +import { useWizard } from '../../../wizard/index.js'; +import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'; +import type { AgentWizardData } from '../types.js'; +export function LocationStep() { + const $ = _c(11); + const { + goNext, + updateWizardData, + cancel + } = useWizard(); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = { + label: "Project (.claude/agents/)", + value: "projectSettings" as SettingSource + }; + $[0] = t0; + } else { + t0 = $[0]; + } + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = [t0, { + label: "Personal (~/.claude/agents/)", + value: "userSettings" as SettingSource + }]; + $[1] = t1; + } else { + t1 = $[1]; + } + const locationOptions = t1; + let t2; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 = ; + $[2] = t2; + } else { + t2 = $[2]; + } + let t3; + if ($[3] !== goNext || $[4] !== updateWizardData) { + t3 = value => { + updateWizardData({ + location: value as SettingSource + }); + goNext(); + }; + $[3] = goNext; + $[4] = updateWizardData; + $[5] = t3; + } else { + t3 = $[5]; + } + let t4; + if ($[6] !== cancel) { + t4 = () => cancel(); + $[6] = cancel; + $[7] = t4; + } else { + t4 = $[7]; + } + let t5; + if ($[8] !== t3 || $[9] !== t4) { + t5 = ; + $[10] = onCancel; + $[11] = t2; + $[12] = t3; + $[13] = t4; + } else { + t4 = $[13]; + } + let t5; + if ($[14] !== hookEventMetadata.description || $[15] !== onCancel || $[16] !== t4 || $[17] !== title) { + t5 = {t4}; + $[14] = hookEventMetadata.description; + $[15] = onCancel; + $[16] = t4; + $[17] = title; + $[18] = t5; + } else { + t5 = $[18]; + } + return t5; +} +function _temp2(hook, index) { + return { + label: `[${hook.config.type}] ${getHookDisplayText(hook.config)}`, + value: index.toString(), + description: hook.source === "pluginHook" && hook.pluginName ? `${hookSourceHeaderDisplayString(hook.source)} (${hook.pluginName})` : hookSourceHeaderDisplayString(hook.source) + }; +} +function _temp() { + return Esc to go back; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkhvb2tFdmVudCIsIkhvb2tFdmVudE1ldGFkYXRhIiwiQm94IiwiVGV4dCIsImdldEhvb2tEaXNwbGF5VGV4dCIsImhvb2tTb3VyY2VIZWFkZXJEaXNwbGF5U3RyaW5nIiwiSW5kaXZpZHVhbEhvb2tDb25maWciLCJTZWxlY3QiLCJEaWFsb2ciLCJQcm9wcyIsInNlbGVjdGVkRXZlbnQiLCJzZWxlY3RlZE1hdGNoZXIiLCJob29rc0ZvclNlbGVjdGVkTWF0Y2hlciIsImhvb2tFdmVudE1ldGFkYXRhIiwib25TZWxlY3QiLCJob29rIiwib25DYW5jZWwiLCJTZWxlY3RIb29rTW9kZSIsInQwIiwiJCIsIl9jIiwidGl0bGUiLCJtYXRjaGVyTWV0YWRhdGEiLCJ1bmRlZmluZWQiLCJsZW5ndGgiLCJ0MSIsIlN5bWJvbCIsImZvciIsInQyIiwiZGVzY3JpcHRpb24iLCJfdGVtcCIsIm1hcCIsIl90ZW1wMiIsInQzIiwidmFsdWUiLCJpbmRleF8wIiwicGFyc2VJbnQiLCJob29rXzAiLCJpbmRleCIsInQ0IiwidDUiLCJsYWJlbCIsImNvbmZpZyIsInR5cGUiLCJ0b1N0cmluZyIsInNvdXJjZSIsInBsdWdpbk5hbWUiXSwic291cmNlcyI6WyJTZWxlY3RIb29rTW9kZS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTZWxlY3RIb29rTW9kZSBzaG93cyBhbGwgaG9va3MgY29uZmlndXJlZCBmb3IgYSBnaXZlbiBldmVudCttYXRjaGVyIHBhaXIuXG4gKlxuICogVGhlIC9ob29rcyBtZW51IGlzIHJlYWQtb25seTogdGhpcyB2aWV3IG5vIGxvbmdlciBvZmZlcnMgXCJhZGQgbmV3IGhvb2tcIlxuICogYW5kIHNlbGVjdGluZyBhIGhvb2sgc2hvd3MgaXRzIHJlYWQtb25seSBkZXRhaWxzIGluc3RlYWQgb2YgYSBkZWxldGVcbiAqIGNvbmZpcm1hdGlvbi5cbiAqL1xuaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgdHlwZSB7IEhvb2tFdmVudCB9IGZyb20gJ3NyYy9lbnRyeXBvaW50cy9hZ2VudFNka1R5cGVzLmpzJ1xuaW1wb3J0IHR5cGUgeyBIb29rRXZlbnRNZXRhZGF0YSB9IGZyb20gJ3NyYy91dGlscy9ob29rcy9ob29rc0NvbmZpZ01hbmFnZXIuanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQge1xuICBnZXRIb29rRGlzcGxheVRleHQsXG4gIGhvb2tTb3VyY2VIZWFkZXJEaXNwbGF5U3RyaW5nLFxuICB0eXBlIEluZGl2aWR1YWxIb29rQ29uZmlnLFxufSBmcm9tICcuLi8uLi91dGlscy9ob29rcy9ob29rc1NldHRpbmdzLmpzJ1xuaW1wb3J0IHsgU2VsZWN0IH0gZnJvbSAnLi4vQ3VzdG9tU2VsZWN0L3NlbGVjdC5qcydcbmltcG9ydCB7IERpYWxvZyB9IGZyb20gJy4uL2Rlc2lnbi1zeXN0ZW0vRGlhbG9nLmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBzZWxlY3RlZEV2ZW50OiBIb29rRXZlbnRcbiAgc2VsZWN0ZWRNYXRjaGVyOiBzdHJpbmcgfCBudWxsXG4gIGhvb2tzRm9yU2VsZWN0ZWRNYXRjaGVyOiBJbmRpdmlkdWFsSG9va0NvbmZpZ1tdXG4gIGhvb2tFdmVudE1ldGFkYXRhOiBIb29rRXZlbnRNZXRhZGF0YVxuICBvblNlbGVjdDogKGhvb2s6IEluZGl2aWR1YWxIb29rQ29uZmlnKSA9PiB2b2lkXG4gIG9uQ2FuY2VsOiAoKSA9PiB2b2lkXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBTZWxlY3RIb29rTW9kZSh7XG4gIHNlbGVjdGVkRXZlbnQsXG4gIHNlbGVjdGVkTWF0Y2hlcixcbiAgaG9va3NGb3JTZWxlY3RlZE1hdGNoZXIsXG4gIGhvb2tFdmVudE1ldGFkYXRhLFxuICBvblNlbGVjdCxcbiAgb25DYW5jZWwsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IHRpdGxlID1cbiAgICBob29rRXZlbnRNZXRhZGF0YS5tYXRjaGVyTWV0YWRhdGEgIT09IHVuZGVmaW5lZFxuICAgICAgPyBgJHtzZWxlY3RlZEV2ZW50fSAtIE1hdGNoZXI6ICR7c2VsZWN0ZWRNYXRjaGVyIHx8ICcoYWxsKSd9YFxuICAgICAgOiBzZWxlY3RlZEV2ZW50XG5cbiAgaWYgKGhvb2tzRm9yU2VsZWN0ZWRNYXRjaGVyLmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybiAoXG4gICAgICA8RGlhbG9nXG4gICAgICAgIHRpdGxlPXt0aXRsZX1cbiAgICAgICAgc3VidGl0bGU9e2hvb2tFdmVudE1ldGFkYXRhLmRlc2NyaXB0aW9ufVxuICAgICAgICBvbkNhbmNlbD17b25DYW5jZWx9XG4gICAgICAgIGlucHV0R3VpZGU9eygpID0+IDxUZXh0PkVzYyB0byBnbyBiYWNrPC9UZXh0Pn1cbiAgICAgID5cbiAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgZ2FwPXsxfT5cbiAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5ObyBob29rcyBjb25maWd1cmVkIGZvciB0aGlzIGV2ZW50LjwvVGV4dD5cbiAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgIFRvIGFkZCBob29rcywgZWRpdCBzZXR0aW5ncy5qc29uIGRpcmVjdGx5IG9yIGFzayBDbGF1ZGUuXG4gICAgICAgICAgPC9UZXh0PlxuICAgICAgICA8L0JveD5cbiAgICAgIDwvRGlhbG9nPlxuICAgIClcbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPERpYWxvZ1xuICAgICAgdGl0bGU9e3RpdGxlfVxuICAgICAgc3VidGl0bGU9e2hvb2tFdmVudE1ldGFkYXRhLmRlc2NyaXB0aW9ufVxuICAgICAgb25DYW5jZWw9e29uQ2FuY2VsfVxuICAgID5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICA8U2VsZWN0XG4gICAgICAgICAgb3B0aW9ucz17aG9va3NGb3JTZWxlY3RlZE1hdGNoZXIubWFwKChob29rLCBpbmRleCkgPT4gKHtcbiAgICAgICAgICAgIGxhYmVsOiBgWyR7aG9vay5jb25maWcudHlwZX1dICR7Z2V0SG9va0Rpc3BsYXlUZXh0KGhvb2suY29uZmlnKX1gLFxuICAgICAgICAgICAgdmFsdWU6IGluZGV4LnRvU3RyaW5nKCksXG4gICAgICAgICAgICBkZXNjcmlwdGlvbjpcbiAgICAgICAgICAgICAgaG9vay5zb3VyY2UgPT09ICdwbHVnaW5Ib29rJyAmJiBob29rLnBsdWdpbk5hbWVcbiAgICAgICAgICAgICAgICA/IGAke2hvb2tTb3VyY2VIZWFkZXJEaXNwbGF5U3RyaW5nKGhvb2suc291cmNlKX0gKCR7aG9vay5wbHVnaW5OYW1lfSlgXG4gICAgICAgICAgICAgICAgOiBob29rU291cmNlSGVhZGVyRGlzcGxheVN0cmluZyhob29rLnNvdXJjZSksXG4gICAgICAgICAgfSkpfVxuICAgICAgICAgIG9uQ2hhbmdlPXt2YWx1ZSA9PiB7XG4gICAgICAgICAgICBjb25zdCBpbmRleCA9IHBhcnNlSW50KHZhbHVlLCAxMClcbiAgICAgICAgICAgIGNvbnN0IGhvb2sgPSBob29rc0ZvclNlbGVjdGVkTWF0Y2hlcltpbmRleF1cbiAgICAgICAgICAgIGlmIChob29rKSB7XG4gICAgICAgICAgICAgIG9uU2VsZWN0KGhvb2spXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfX1cbiAgICAgICAgICBvbkNhbmNlbD17b25DYW5jZWx9XG4gICAgICAgIC8+XG4gICAgICA8L0JveD5cbiAgICA8L0RpYWxvZz5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPLEtBQUtBLEtBQUssTUFBTSxPQUFPO0FBQzlCLGNBQWNDLFNBQVMsUUFBUSxrQ0FBa0M7QUFDakUsY0FBY0MsaUJBQWlCLFFBQVEsdUNBQXVDO0FBQzlFLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLGNBQWM7QUFDeEMsU0FDRUMsa0JBQWtCLEVBQ2xCQyw2QkFBNkIsRUFDN0IsS0FBS0Msb0JBQW9CLFFB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 \ No newline at end of file diff --git a/src/components/hooks/ViewHookMode.tsx b/src/components/hooks/ViewHookMode.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b84d4b0f0af0bfd510934ed7e8e28266bbd6908b --- /dev/null +++ b/src/components/hooks/ViewHookMode.tsx @@ -0,0 +1,199 @@ +import { c as _c } from "react/compiler-runtime"; +/** + * ViewHookMode shows read-only details for a single configured hook. + * + * The /hooks menu is read-only; this view replaces the former delete-hook + * confirmation screen and directs users to settings.json or Claude for edits. + */ +import * as React from 'react'; +import { Box, Text } from '../../ink.js'; +import { hookSourceDescriptionDisplayString, type IndividualHookConfig } from '../../utils/hooks/hooksSettings.js'; +import { Dialog } from '../design-system/Dialog.js'; +type Props = { + selectedHook: IndividualHookConfig; + eventSupportsMatcher: boolean; + onCancel: () => void; +}; +export function ViewHookMode(t0) { + const $ = _c(40); + const { + selectedHook, + eventSupportsMatcher, + onCancel + } = t0; + let t1; + if ($[0] !== selectedHook.event) { + t1 = Event: {selectedHook.event}; + $[0] = selectedHook.event; + $[1] = t1; + } else { + t1 = $[1]; + } + let t2; + if ($[2] !== eventSupportsMatcher || $[3] !== selectedHook.matcher) { + t2 = eventSupportsMatcher && Matcher: {selectedHook.matcher || "(all)"}; + $[2] = eventSupportsMatcher; + $[3] = selectedHook.matcher; + $[4] = t2; + } else { + t2 = $[4]; + } + let t3; + if ($[5] !== selectedHook.config.type) { + t3 = Type: {selectedHook.config.type}; + $[5] = selectedHook.config.type; + $[6] = t3; + } else { + t3 = $[6]; + } + let t4; + if ($[7] !== selectedHook.source) { + t4 = hookSourceDescriptionDisplayString(selectedHook.source); + $[7] = selectedHook.source; + $[8] = t4; + } else { + t4 = $[8]; + } + let t5; + if ($[9] !== t4) { + t5 = Source:{" "}{t4}; + $[9] = t4; + $[10] = t5; + } else { + t5 = $[10]; + } + let t6; + if ($[11] !== selectedHook.pluginName) { + t6 = selectedHook.pluginName && Plugin: {selectedHook.pluginName}; + $[11] = selectedHook.pluginName; + $[12] = t6; + } else { + t6 = $[12]; + } + let t7; + if ($[13] !== t1 || $[14] !== t2 || $[15] !== t3 || $[16] !== t5 || $[17] !== t6) { + t7 = {t1}{t2}{t3}{t5}{t6}; + $[13] = t1; + $[14] = t2; + $[15] = t3; + $[16] = t5; + $[17] = t6; + $[18] = t7; + } else { + t7 = $[18]; + } + let t8; + if ($[19] !== selectedHook.config) { + t8 = getContentFieldLabel(selectedHook.config); + $[19] = selectedHook.config; + $[20] = t8; + } else { + t8 = $[20]; + } + let t9; + if ($[21] !== t8) { + t9 = {t8}:; + $[21] = t8; + $[22] = t9; + } else { + t9 = $[22]; + } + let t10; + if ($[23] !== selectedHook.config) { + t10 = getContentFieldValue(selectedHook.config); + $[23] = selectedHook.config; + $[24] = t10; + } else { + t10 = $[24]; + } + let t11; + if ($[25] !== t10) { + t11 = {t10}; + $[25] = t10; + $[26] = t11; + } else { + t11 = $[26]; + } + let t12; + if ($[27] !== t11 || $[28] !== t9) { + t12 = {t9}{t11}; + $[27] = t11; + $[28] = t9; + $[29] = t12; + } else { + t12 = $[29]; + } + let t13; + if ($[30] !== selectedHook.config) { + t13 = "statusMessage" in selectedHook.config && selectedHook.config.statusMessage && Status message:{" "}{selectedHook.config.statusMessage}; + $[30] = selectedHook.config; + $[31] = t13; + } else { + t13 = $[31]; + } + let t14; + if ($[32] === Symbol.for("react.memo_cache_sentinel")) { + t14 = To modify or remove this hook, edit settings.json directly or ask Claude to help.; + $[32] = t14; + } else { + t14 = $[32]; + } + let t15; + if ($[33] !== t12 || $[34] !== t13 || $[35] !== t7) { + t15 = {t7}{t12}{t13}{t14}; + $[33] = t12; + $[34] = t13; + $[35] = t7; + $[36] = t15; + } else { + t15 = $[36]; + } + let t16; + if ($[37] !== onCancel || $[38] !== t15) { + t16 = {t15}; + $[37] = onCancel; + $[38] = t15; + $[39] = t16; + } else { + t16 = $[39]; + } + return t16; +} + +/** + * Get a human-readable label for the primary content field of a hook + * based on its type. + */ +function _temp() { + return Esc to go back; +} +function getContentFieldLabel(config: IndividualHookConfig['config']): string { + switch (config.type) { + case 'command': + return 'Command'; + case 'prompt': + return 'Prompt'; + case 'agent': + return 'Prompt'; + case 'http': + return 'URL'; + } +} + +/** + * Get the actual content value for a hook's primary field, bypassing + * statusMessage so the detail view always shows the real command/prompt/URL. + */ +function getContentFieldValue(config: IndividualHookConfig['config']): string { + switch (config.type) { + case 'command': + return config.command; + case 'prompt': + return config.prompt; + case 'agent': + return config.prompt; + case 'http': + return config.url; + } +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJveCIsIlRleHQiLCJob29rU291cmNlRGVzY3JpcHRpb25EaXNwbGF5U3RyaW5nIiwiSW5kaXZpZHVhbEhvb2tDb25maWciLCJEaWFsb2ciLCJQcm9wcyIsInNlbGVjdGVkSG9vayIsImV2ZW50U3VwcG9ydHNNYXRjaGVyIiwib25DYW5jZWwiLCJWaWV3SG9va01vZGUiLCJ0MCIsIiQiLCJfYyIsInQxIiwiZXZlbnQiLCJ0MiIsIm1hdGNoZXIiLCJ0MyIsImNvbmZpZyIsInR5cGUiLCJ0NCIsInNvdXJjZSIsInQ1IiwidDYiLCJwbHVnaW5OYW1lIiwidDciLCJ0OCIsImdldENvbnRlbnRGaWVsZExhYmVsIiwidDkiLCJ0MTAiLCJnZXRDb250ZW50RmllbGRWYWx1ZSIsInQxMSIsInQxMiIsInQxMyIsInN0YXR1c01lc3NhZ2UiLCJ0MTQiLCJTeW1ib2wiLCJmb3IiLCJ0MTUiLCJ0MTYiLCJfdGVtcCIsImNvbW1hbmQiLCJwcm9tcHQiLCJ1cmwiXSwic291cmNlcyI6WyJWaWV3SG9va01vZGUudHN4Il0sInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogVmlld0hvb2tNb2RlIHNob3dzIHJlYWQtb25seSBkZXRhaWxzIGZvciBhIHNpbmdsZSBjb25maWd1cmVkIGhvb2suXG4gKlxuICogVGhlIC9ob29rcyBtZW51IGlzIHJlYWQtb25seTsgdGhpcyB2aWV3IHJlcGxhY2VzIHRoZSBmb3JtZXIgZGVsZXRlLWhvb2tcbiAqIGNvbmZpcm1hdGlvbiBzY3JlZW4gYW5kIGRpcmVjdHMgdXNlcnMgdG8gc2V0dGluZ3MuanNvbiBvciBDbGF1ZGUgZm9yIGVkaXRzLlxuICovXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7XG4gIGhvb2tTb3VyY2VEZXNjcmlwdGlvbkRpc3BsYXlTdHJpbmcsXG4gIHR5cGUgSW5kaXZpZHVhbEhvb2tDb25maWcsXG59IGZyb20gJy4uLy4uL3V0aWxzL2hvb2tzL2hvb2tzU2V0dGluZ3MuanMnXG5pbXBvcnQgeyBEaWFsb2cgfSBmcm9tICcuLi9kZXNpZ24tc3lzdGVtL0RpYWxvZy5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgc2VsZWN0ZWRIb29rOiBJbmRpdmlkdWFsSG9va0NvbmZpZ1xuICBldmVudFN1cHBvcnRzTWF0Y2hlcjogYm9vbGVhblxuICBvbkNhbmNlbDogKCkgPT4gdm9pZFxufVxuXG5leHBvcnQgZnVuY3Rpb24gVmlld0hvb2tNb2RlKHtcbiAgc2VsZWN0ZWRIb29rLFxuICBldmVudFN1cHBvcnRzTWF0Y2hlcixcbiAgb25DYW5jZWwsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIHJldHVybiAoXG4gICAgPERpYWxvZ1xuICAgICAgdGl0bGU9XCJIb29rIGRldGFpbHNcIlxuICAgICAgb25DYW5jZWw9e29uQ2FuY2VsfVxuICAgICAgaW5wdXRHdWlkZT17KCkgPT4gPFRleHQ+RXNjIHRvIGdvIGJhY2s8L1RleHQ+fVxuICAgID5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiIGdhcD17MX0+XG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgIDxUZXh0PlxuICAgICAgICAgICAgRXZlbnQ6IDxUZXh0IGJvbGQ+e3NlbGVjdGVkSG9vay5ldmVudH08L1RleHQ+XG4gICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgIHtldmVudFN1cHBvcnRzTWF0Y2hlciAmJiAoXG4gICAgICAgICAgICA8VGV4dD5cbiAgICAgICAgICAgICAgTWF0Y2hlcjogPFRleHQgYm9sZD57c2VsZWN0ZWRIb29rLm1hdGNoZXIgfHwgJyhhbGwpJ308L1RleHQ+XG4gICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgKX1cbiAgICAgICAgICA8VGV4dD5cbiAgICAgICAgICAgIFR5cGU6IDxUZXh0IGJvbGQ+e3NlbGVjdGVkSG9vay5jb25maWcudHlwZX08L1RleHQ+XG4gICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgIDxUZXh0PlxuICAgICAgICAgICAgU291cmNlOnsnICd9XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAge2hvb2tTb3VyY2VEZXNjcmlwdGlvbkRpc3BsYXlTdHJpbmcoc2VsZWN0ZWRIb29rLnNvdXJjZSl9XG4gICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgIHtzZWxlY3RlZEhvb2sucGx1Z2luTmFtZSAmJiAoXG4gICAgICAgICAgICA8VGV4dD5cbiAgICAgICAgICAgICAgUGx1Z2luOiA8VGV4dCBkaW1Db2xvcj57c2VsZWN0ZWRIb29rLnBsdWdpbk5hbWV9PC9UZXh0PlxuICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgICl9XG4gICAgICAgIDwvQm94PlxuICAgICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgICAgICA8VGV4dCBkaW1Db2xvcj57Z2V0Q29udGVudEZpZWxkTGFiZWwoc2VsZWN0ZWRIb29rLmNvbmZpZyl9OjwvVGV4dD5cbiAgICAgICAgICA8Qm94XG4gICAgICAgICAgICBib3JkZXJTdHlsZT1cInJvdW5kXCJcbiAgICAgICAgICAgIGJvcmRlckRpbUNvbG9yXG4gICAgICAgICAgICBwYWRkaW5nTGVmdD17MX1cbiAgICAgICAgICAgIHBhZGRpbmdSaWdodD17MX1cbiAgICAgICAgICA+XG4gICAgICAgICAgICA8VGV4dD57Z2V0Q29udGVudEZpZWxkVmFsdWUoc2VsZWN0ZWRIb29rLmNvbmZpZyl9PC9UZXh0PlxuICAgICAgICAgIDwvQm94PlxuICAgICAgICA8L0JveD5cbiAgICAgICAgeydzdGF0dXNNZXNzYWdlJyBpbiBzZWxlY3RlZEhvb2suY29uZmlnICYmXG4gICAgICAgICAgc2VsZWN0ZWRIb29rLmNvbmZpZy5zdGF0dXNNZXNzYWdlICYmIChcbiAgICAgICAgICAgIDxUZXh0PlxuICAgICAgICAgICAgICBTdGF0dXMgbWVzc2FnZTp7JyAnfVxuICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj57c2VsZWN0ZWRIb29rLmNvbmZpZy5zdGF0dXNNZXNzYWdlfTwvVGV4dD5cbiAgICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICApfVxuICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICBUbyBtb2RpZnkgb3IgcmVtb3ZlIHRoaXMgaG9vaywgZWRpdCBzZXR0aW5ncy5qc29uIGRpcmVjdGx5IG9yIGFza1xuICAgICAgICAgIENsYXVkZSB0byBoZWxwLlxuICAgICAgICA8L1RleHQ+XG4gICAgICA8L0JveD5cbiAgICA8L0RpYWxvZz5cbiAgKVxufVxuXG4vKipcbiAqIEdldCBhIGh1bWFuLXJlYWRhYmxlIGxhYmVsIGZvciB0aGUgcHJpbWFyeSBjb250ZW50IGZpZWxkIG9mIGEgaG9va1xuICogYmFzZWQgb24gaXRzIHR5cGUuXG4gKi9cbmZ1bmN0aW9uIGdldENvbnRlbnRGaWVsZExhYmVsKGNvbmZpZzogSW5kaXZpZHVhbEhvb2tDb25maWdbJ2NvbmZpZyddKTogc3RyaW5nIHtcbiAgc3dpdGNoIChjb25maWcudHlwZSkge1xuICAgIGNhc2UgJ2NvbW1hbmQnOlxuICAgICAgcmV0dXJuICdDb21tYW5kJ1xuICAgIGNhc2UgJ3Byb21wdCc6XG4gICAgICByZXR1cm4gJ1Byb21wdCdcbiAgICBjYXNlICdhZ2VudCc6XG4gICAgICByZXR1cm4gJ1Byb21wdCdcbiAgICBjYXNlICdodHRwJzpcbiAgICAgIHJldHVybiAnVVJMJ1xuICB9XG59XG5cbi8qKlxuICogR2V0IHRoZSBhY3R1YWwgY29udGVudCB2YWx1ZSBmb3IgYSBob29rJ3MgcHJpbWFyeSBmaWVsZCwgYnlwYXNzaW5nXG4gKiBzdGF0dXNNZXNzYWdlIHNvIHRoZSBkZXRhaWwgdmlldyBhbHdheXMgc2hvd3MgdGhlIHJlYWwgY29tbWFuZC9wcm9tcHQvVVJMLlxuICovXG5mdW5jdGlvbiBnZXRDb250ZW50RmllbGRWYWx1ZShjb25maWc6IEluZGl2aWR1YWxIb29rQ29uZmlnWydjb25maWcnXSk6IHN0cmluZyB7XG4gIHN3aXRjaCAoY29uZmlnLnR5cGUpIHtcbiAgICBjYXNlICdjb21tYW5kJzpcbiAgICAgIHJldHVybiBjb25maWcuY29tbWFuZFxuICAgIGNhc2UgJ3Byb21wdCc6XG4gICAgICByZXR1cm4gY29uZmlnLnByb21wdFxuICAgIGNhc2UgJ2FnZW50JzpcbiAgICAgIHJldHVybiBjb25maWcucHJvbXB0XG4gICAgY2FzZSAnaHR0cCc6XG4gICAgICByZXR1cm4gY29uZmlnLnVybFxuICB9XG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPLEtBQUtBLEtBQUssTUFBTSxPQUFPO0FBQzlCLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLGNBQWM7QUFDeEMsU0FDRUMsa0NBQWtDLEVBQ2xDLEtBQUtDLG9CQUFvQixRQUNwQixvQ0FBb0M7QUFDM0MsU0FBU0MsTUFBTSxRQUFRLDRCQUE0QjtBQUVuRCxLQUFLQyxLQUFLLEdBQUc7RUFDWEMsWUFBWSxFQUFFSCxvQkFBb0I7RUFDbENJLG9CQUFvQixFQUFFLE9BQU87RUFDN0JDLFFBQVEsRUFBRSxHQUFHLEdBQUcsSUFBSTtBQUN0QixDQUFDO0FBRUQsT0FBTyxTQUFBQyxhQUFBQyxFQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQXNCO0lBQUFOLFlBQUE7SUFBQUMsb0JBQUE7SUFBQUM7RUFBQSxJQUFBRSxFQUlyQjtFQUFBLElBQUFHLEVBQUE7RUFBQSxJQUFBRixDQUFBLFFBQUFMLFlBQUEsQ0FBQVEsS0FBQTtJQVNFRCxFQUFBLElBQUMsSUFBSSxDQUFDLE9BQ0csQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFKLEtBQUcsQ0FBQyxDQUFFLENBQUFQLFlBQVksQ0FBQVEsS0FBSyxDQUFFLEVBQTlCLElBQUksQ0FDZCxFQUZDLElBQUksQ0FFRTtJQUFBSCxDQUFBLE1BQUFMLFlBQUEsQ0FBQVEsS0FBQTtJ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 \ No newline at end of file diff --git a/src/components/mcp/CapabilitiesSection.tsx b/src/components/mcp/CapabilitiesSection.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7d136f5679f193b708538ff64e5b6a9657ceaaa9 --- /dev/null +++ b/src/components/mcp/CapabilitiesSection.tsx @@ -0,0 +1,61 @@ +import { c as _c } from "react/compiler-runtime"; +import React from 'react'; +import { Box, Text } from '../../ink.js'; +import { Byline } from '../design-system/Byline.js'; +type Props = { + serverToolsCount: number; + serverPromptsCount: number; + serverResourcesCount: number; +}; +export function CapabilitiesSection(t0) { + const $ = _c(9); + const { + serverToolsCount, + serverPromptsCount, + serverResourcesCount + } = t0; + let capabilities; + if ($[0] !== serverPromptsCount || $[1] !== serverResourcesCount || $[2] !== serverToolsCount) { + capabilities = []; + if (serverToolsCount > 0) { + capabilities.push("tools"); + } + if (serverResourcesCount > 0) { + capabilities.push("resources"); + } + if (serverPromptsCount > 0) { + capabilities.push("prompts"); + } + $[0] = serverPromptsCount; + $[1] = serverResourcesCount; + $[2] = serverToolsCount; + $[3] = capabilities; + } else { + capabilities = $[3]; + } + let t1; + if ($[4] === Symbol.for("react.memo_cache_sentinel")) { + t1 = Capabilities: ; + $[4] = t1; + } else { + t1 = $[4]; + } + let t2; + if ($[5] !== capabilities) { + t2 = capabilities.length > 0 ? {capabilities} : "none"; + $[5] = capabilities; + $[6] = t2; + } else { + t2 = $[6]; + } + let t3; + if ($[7] !== t2) { + t3 = {t1}{t2}; + $[7] = t2; + $[8] = t3; + } else { + t3 = $[8]; + } + return t3; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJveCIsIlRleHQiLCJCeWxpbmUiLCJQcm9wcyIsInNlcnZlclRvb2xzQ291bnQiLCJzZXJ2ZXJQcm9tcHRzQ291bnQiLCJzZXJ2ZXJSZXNvdXJjZXNDb3VudCIsIkNhcGFiaWxpdGllc1NlY3Rpb24iLCJ0MCIsIiQiLCJfYyIsImNhcGFiaWxpdGllcyIsInB1c2giLCJ0MSIsIlN5bWJvbCIsImZvciIsInQyIiwibGVuZ3RoIiwidDMiXSwic291cmNlcyI6WyJDYXBhYmlsaXRpZXNTZWN0aW9uLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgeyBCeWxpbmUgfSBmcm9tICcuLi9kZXNpZ24tc3lzdGVtL0J5bGluZS5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgc2VydmVyVG9vbHNDb3VudDogbnVtYmVyXG4gIHNlcnZlclByb21wdHNDb3VudDogbnVtYmVyXG4gIHNlcnZlclJlc291cmNlc0NvdW50OiBudW1iZXJcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIENhcGFiaWxpdGllc1NlY3Rpb24oe1xuICBzZXJ2ZXJUb29sc0NvdW50LFxuICBzZXJ2ZXJQcm9tcHRzQ291bnQsXG4gIHNlcnZlclJlc291cmNlc0NvdW50LFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBjYXBhYmlsaXRpZXMgPSBbXVxuICBpZiAoc2VydmVyVG9vbHNDb3VudCA+IDApIHtcbiAgICBjYXBhYmlsaXRpZXMucHVzaCgndG9vbHMnKVxuICB9XG4gIGlmIChzZXJ2ZXJSZXNvdXJjZXNDb3VudCA+IDApIHtcbiAgICBjYXBhYmlsaXRpZXMucHVzaCgncmVzb3VyY2VzJylcbiAgfVxuICBpZiAoc2VydmVyUHJvbXB0c0NvdW50ID4gMCkge1xuICAgIGNhcGFiaWxpdGllcy5wdXNoKCdwcm9tcHRzJylcbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPEJveD5cbiAgICAgIDxUZXh0IGJvbGQ+Q2FwYWJpbGl0aWVzOiA8L1RleHQ+XG4gICAgICA8VGV4dCBjb2xvcj1cInRleHRcIj5cbiAgICAgICAge2NhcGFiaWxpdGllcy5sZW5ndGggPiAwID8gPEJ5bGluZT57Y2FwYWJpbGl0aWVzfTwvQnlsaW5lPiA6ICdub25lJ31cbiAgICAgIDwvVGV4dD5cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBT0EsS0FBSyxNQUFNLE9BQU87QUFDekIsU0FBU0MsR0FBRyxFQUFFQyxJQUFJLFFBQVEsY0FBYztBQUN4QyxTQUFTQyxNQUFNLFFBQVEsNEJBQTRCO0FBRW5ELEtBQUtDLEtBQUssR0FBRztFQUNYQyxnQkFBZ0IsRUFBRSxNQUFNO0VBQ3hCQyxrQkFBa0IsRUFBRSxNQUFNO0VBQzFCQyxvQkFBb0IsRUFBRSxNQUFNO0FBQzlCLENBQUM7QUFFRCxPQUFPLFNBQUFDLG9CQUFBQyxFQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQTZCO0lBQUFOLGdCQUFBO0lBQUFDLGtCQUFBO0lBQUFDO0VBQUEsSUFBQUUsRUFJNUI7RUFBQSxJQUFBRyxZQUFBO0VBQUEsSUFBQUYsQ0FBQSxRQUFBSixrQkFBQSxJQUFBSSxDQUFBLFFBQUFILG9CQUFBLElBQUFHLENBQUEsUUFBQUwsZ0JBQUE7SUFDTk8sWUFBQSxHQUFxQixFQUFFO0lBQ3ZCLElBQUlQLGdCQUFnQixHQUFHLENBQUM7TUFDdEJPLFlBQVksQ0FBQUMsSUFBSyxDQUFDLE9BQU8sQ0FBQztJQUFBO0lBRTVCLElBQUlOLG9CQUFvQixHQUFHLENBQUM7TUFDMUJLLFlBQVksQ0FBQUMsSUFBSyxDQUFDLFdBQVcsQ0FBQztJQUFBO0lBRWhDLElBQUlQLGtCQUFrQixHQUFHLENBQUM7TUFDeEJNLFlBQVksQ0FBQUMsSUFBSyxDQUFDLFNBQVMsQ0FBQztJQUFBO0lBQzdCSCxDQUFBLE1BQUFKLGtCQUFBO0lBQUFJLENBQUEsTUFBQUgsb0JBQUE7SUFBQUcsQ0FBQSxNQUFBTCxnQkFBQTtJQUFBSyxDQUFBLE1BQUFFLFlBQUE7RUFBQTtJQUFBQSxZQUFBLEdBQUFGLENBQUE7RUFBQTtFQUFBLElBQUFJLEVBQUE7RUFBQSxJQUFBSixDQUFBLFFBQUFLLE1BQUEsQ0FBQUMsR0FBQTtJQUlHRixFQUFBLElBQUMsSUFBSSxDQUFDLElBQUksQ0FBSixLQUFHLENBQUMsQ0FBQyxjQUFjLEVBQXhCLElBQUksQ0FBMkI7SUFBQUosQ0FBQSxNQUFBSSxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBSixDQUFBO0VBQUE7RUFBQSxJQUFBTyxFQUFBO0VBQUEsSUFBQVAsQ0FBQSxRQUFBRSxZQUFBO0lBRTdCSyxFQUFBLEdBQUFMLFlBQVksQ0FBQU0sTUFBTyxHQUFHLENBQTRDLEdBQXhDLENBQUMsTUFBTSxDQUFFTixhQUFXLENBQUUsRUFBckIsTUFBTSxDQUFpQyxHQUFsRSxNQUFrRTtJQUFBRixDQUFBLE1BQUFFLFlBQUE7SUFBQUYsQ0FBQSxNQUFBTyxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBUCxDQUFBO0VBQUE7RUFBQSxJQUFBUyxFQUFBO0VBQUEsSUFBQVQsQ0FBQSxRQUFBTyxFQUFBO0lBSHZFRSxFQUFBLElBQUMsR0FBRyxDQUNGLENBQUFMLEVBQStCLENBQy9CLENBQUMsSUFBSSxDQUFPLEtBQU0sQ0FBTixNQUFNLENBQ2YsQ0FBQUcsRUFBaUUsQ0FDcEUsRUFGQyxJQUFJLENBR1AsRUFMQyxHQUFHLENBS0U7SUFBQVAsQ0FBQSxNQUFBTyxFQUFBO0lBQUFQLENBQUEsTUFBQVMsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQVQsQ0FBQTtFQUFBO0VBQUEsT0FMTlMsRUFLTTtBQUFBIiwiaWdub3JlTGlzdCI6W119 \ No newline at end of file diff --git a/src/components/mcp/MCPRemoteServerMenu.tsx b/src/components/mcp/MCPRemoteServerMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..954c6f30b72fb1ead33cbf13936c0e7f1e56d104 --- /dev/null +++ b/src/components/mcp/MCPRemoteServerMenu.tsx @@ -0,0 +1,649 @@ +import figures from 'figures'; +import React, { useEffect, useRef, useState } from 'react'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import type { CommandResultDisplay } from '../../commands.js'; +import { getOauthConfig } from '../../constants/oauth.js'; +import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; +import { useTerminalSize } from '../../hooks/useTerminalSize.js'; +import { setClipboard } from '../../ink/termio/osc.js'; +// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw j/k/arrow menu navigation +import { Box, color, Link, Text, useInput, useTheme } from '../../ink.js'; +import { useKeybinding } from '../../keybindings/useKeybinding.js'; +import { AuthenticationCancelledError, performMCPOAuthFlow, revokeServerTokens } from '../../services/mcp/auth.js'; +import { clearServerCache } from '../../services/mcp/client.js'; +import { useMcpReconnect, useMcpToggleEnabled } from '../../services/mcp/MCPConnectionManager.js'; +import { describeMcpConfigFilePath, excludeCommandsByServer, excludeResourcesByServer, excludeToolsByServer, filterMcpPromptsByServer } from '../../services/mcp/utils.js'; +import { useAppState, useSetAppState } from '../../state/AppState.js'; +import { getOauthAccountInfo } from '../../utils/auth.js'; +import { openBrowser } from '../../utils/browser.js'; +import { errorMessage } from '../../utils/errors.js'; +import { logMCPDebug } from '../../utils/log.js'; +import { capitalize } from '../../utils/stringUtils.js'; +import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; +import { Select } from '../CustomSelect/index.js'; +import { Byline } from '../design-system/Byline.js'; +import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; +import { Spinner } from '../Spinner.js'; +import TextInput from '../TextInput.js'; +import { CapabilitiesSection } from './CapabilitiesSection.js'; +import type { ClaudeAIServerInfo, HTTPServerInfo, SSEServerInfo } from './types.js'; +import { handleReconnectError, handleReconnectResult } from './utils/reconnectHelpers.js'; +type Props = { + server: SSEServerInfo | HTTPServerInfo | ClaudeAIServerInfo; + serverToolsCount: number; + onViewTools: () => void; + onCancel: () => void; + onComplete?: (result?: string, options?: { + display?: CommandResultDisplay; + }) => void; + borderless?: boolean; +}; +export function MCPRemoteServerMenu({ + server, + serverToolsCount, + onViewTools, + onCancel, + onComplete, + borderless = false +}: Props): React.ReactNode { + const [theme] = useTheme(); + const exitState = useExitOnCtrlCDWithKeybindings(); + const { + columns: terminalColumns + } = useTerminalSize(); + const [isAuthenticating, setIsAuthenticating] = React.useState(false); + const [error, setError] = React.useState(null); + const mcp = useAppState(s => s.mcp); + const setAppState = useSetAppState(); + const [authorizationUrl, setAuthorizationUrl] = React.useState(null); + const [isReconnecting, setIsReconnecting] = useState(false); + const authAbortControllerRef = useRef(null); + const [isClaudeAIAuthenticating, setIsClaudeAIAuthenticating] = useState(false); + const [claudeAIAuthUrl, setClaudeAIAuthUrl] = useState(null); + const [isClaudeAIClearingAuth, setIsClaudeAIClearingAuth] = useState(false); + const [claudeAIClearAuthUrl, setClaudeAIClearAuthUrl] = useState(null); + const [claudeAIClearAuthBrowserOpened, setClaudeAIClearAuthBrowserOpened] = useState(false); + const [urlCopied, setUrlCopied] = useState(false); + const copyTimeoutRef = useRef | undefined>(undefined); + const unmountedRef = useRef(false); + const [callbackUrlInput, setCallbackUrlInput] = useState(''); + const [callbackUrlCursorOffset, setCallbackUrlCursorOffset] = useState(0); + const [manualCallbackSubmit, setManualCallbackSubmit] = useState<((url: string) => void) | null>(null); + + // If the component unmounts mid-auth (e.g. a parent component's Esc handler + // navigates away before ours fires), abort the OAuth flow so the callback + // server is closed. Without this, the server stays bound and the process + // can outlive the terminal. Also clear the copy-feedback timer and mark + // unmounted so the async setClipboard callback doesn't setUrlCopied / + // schedule a new timer after unmount. + useEffect(() => () => { + unmountedRef.current = true; + authAbortControllerRef.current?.abort(); + if (copyTimeoutRef.current !== undefined) { + clearTimeout(copyTimeoutRef.current); + } + }, []); + + // A server is effectively authenticated if: + // 1. It has OAuth tokens (server.isAuthenticated), OR + // 2. It's connected and has tools (meaning it's working via some auth mechanism) + const isEffectivelyAuthenticated = server.isAuthenticated || server.client.type === 'connected' && serverToolsCount > 0; + const reconnectMcpServer = useMcpReconnect(); + const handleClaudeAIAuthComplete = React.useCallback(async () => { + setIsClaudeAIAuthenticating(false); + setClaudeAIAuthUrl(null); + setIsReconnecting(true); + try { + const result = await reconnectMcpServer(server.name); + const success = result.client.type === 'connected'; + logEvent('tengu_claudeai_mcp_auth_completed', { + success + }); + if (success) { + onComplete?.(`Authentication successful. Connected to ${server.name}.`); + } else if (result.client.type === 'needs-auth') { + onComplete?.('Authentication successful, but server still requires authentication. You may need to manually restart Claude Code.'); + } else { + onComplete?.('Authentication successful, but server reconnection failed. You may need to manually restart Claude Code for the changes to take effect.'); + } + } catch (err) { + logEvent('tengu_claudeai_mcp_auth_completed', { + success: false + }); + onComplete?.(handleReconnectError(err, server.name)); + } finally { + setIsReconnecting(false); + } + }, [reconnectMcpServer, server.name, onComplete]); + const handleClaudeAIClearAuthComplete = React.useCallback(async () => { + await clearServerCache(server.name, { + ...server.config, + scope: server.scope + }); + setAppState(prev => { + const newClients = prev.mcp.clients.map(c => c.name === server.name ? { + ...c, + type: 'needs-auth' as const + } : c); + const newTools = excludeToolsByServer(prev.mcp.tools, server.name); + const newCommands = excludeCommandsByServer(prev.mcp.commands, server.name); + const newResources = excludeResourcesByServer(prev.mcp.resources, server.name); + return { + ...prev, + mcp: { + ...prev.mcp, + clients: newClients, + tools: newTools, + commands: newCommands, + resources: newResources + } + }; + }); + logEvent('tengu_claudeai_mcp_clear_auth_completed', {}); + onComplete?.(`Disconnected from ${server.name}.`); + setIsClaudeAIClearingAuth(false); + setClaudeAIClearAuthUrl(null); + setClaudeAIClearAuthBrowserOpened(false); + }, [server.name, server.config, server.scope, setAppState, onComplete]); + + // Escape to cancel authentication flow + useKeybinding('confirm:no', () => { + authAbortControllerRef.current?.abort(); + authAbortControllerRef.current = null; + setIsAuthenticating(false); + setAuthorizationUrl(null); + }, { + context: 'Confirmation', + isActive: isAuthenticating + }); + + // Escape to cancel Claude AI authentication + useKeybinding('confirm:no', () => { + setIsClaudeAIAuthenticating(false); + setClaudeAIAuthUrl(null); + }, { + context: 'Confirmation', + isActive: isClaudeAIAuthenticating + }); + + // Escape to cancel Claude AI clear auth + useKeybinding('confirm:no', () => { + setIsClaudeAIClearingAuth(false); + setClaudeAIClearAuthUrl(null); + setClaudeAIClearAuthBrowserOpened(false); + }, { + context: 'Confirmation', + isActive: isClaudeAIClearingAuth + }); + + // Return key handling for authentication flows and 'c' to copy URL + useInput((input, key) => { + if (key.return && isClaudeAIAuthenticating) { + void handleClaudeAIAuthComplete(); + } + if (key.return && isClaudeAIClearingAuth) { + if (claudeAIClearAuthBrowserOpened) { + void handleClaudeAIClearAuthComplete(); + } else { + // First Enter: open the browser + const connectorsUrl = `${getOauthConfig().CLAUDE_AI_ORIGIN}/settings/connectors`; + setClaudeAIClearAuthUrl(connectorsUrl); + setClaudeAIClearAuthBrowserOpened(true); + void openBrowser(connectorsUrl); + } + } + if (input === 'c' && !urlCopied) { + const urlToCopy = authorizationUrl || claudeAIAuthUrl || claudeAIClearAuthUrl; + if (urlToCopy) { + void setClipboard(urlToCopy).then(raw => { + if (unmountedRef.current) return; + if (raw) process.stdout.write(raw); + setUrlCopied(true); + if (copyTimeoutRef.current !== undefined) { + clearTimeout(copyTimeoutRef.current); + } + copyTimeoutRef.current = setTimeout(setUrlCopied, 2000, false); + }); + } + } + }); + const capitalizedServerName = capitalize(String(server.name)); + + // Count MCP prompts for this server (skills are shown in /skills, not here) + const serverCommandsCount = filterMcpPromptsByServer(mcp.commands, server.name).length; + const toggleMcpServer = useMcpToggleEnabled(); + const handleClaudeAIAuth = React.useCallback(async () => { + const claudeAiBaseUrl = getOauthConfig().CLAUDE_AI_ORIGIN; + const accountInfo = getOauthAccountInfo(); + const orgUuid = accountInfo?.organizationUuid; + let authUrl: string; + if (orgUuid && server.config.type === 'claudeai-proxy' && server.config.id) { + // Use the direct auth URL with org and server IDs + // Replace 'mcprs' prefix with 'mcpsrv' if present + const serverId = server.config.id.startsWith('mcprs') ? 'mcpsrv' + server.config.id.slice(5) : server.config.id; + const productSurface = encodeURIComponent(process.env.CLAUDE_CODE_ENTRYPOINT || 'cli'); + authUrl = `${claudeAiBaseUrl}/api/organizations/${orgUuid}/mcp/start-auth/${serverId}?product_surface=${productSurface}`; + } else { + // Fall back to settings/connectors if we don't have the required IDs + authUrl = `${claudeAiBaseUrl}/settings/connectors`; + } + setClaudeAIAuthUrl(authUrl); + setIsClaudeAIAuthenticating(true); + logEvent('tengu_claudeai_mcp_auth_started', {}); + await openBrowser(authUrl); + }, [server.config]); + const handleClaudeAIClearAuth = React.useCallback(() => { + setIsClaudeAIClearingAuth(true); + logEvent('tengu_claudeai_mcp_clear_auth_started', {}); + }, []); + const handleToggleEnabled = React.useCallback(async () => { + const wasEnabled = server.client.type !== 'disabled'; + try { + await toggleMcpServer(server.name); + if (server.config.type === 'claudeai-proxy') { + logEvent('tengu_claudeai_mcp_toggle', { + new_state: (wasEnabled ? 'disabled' : 'enabled') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS + }); + } + + // Return to the server list so user can continue managing other servers + onCancel(); + } catch (err_0) { + const action = wasEnabled ? 'disable' : 'enable'; + onComplete?.(`Failed to ${action} MCP server '${server.name}': ${errorMessage(err_0)}`); + } + }, [server.client.type, server.config.type, server.name, toggleMcpServer, onCancel, onComplete]); + const handleAuthenticate = React.useCallback(async () => { + if (server.config.type === 'claudeai-proxy') return; + setIsAuthenticating(true); + setError(null); + const controller = new AbortController(); + authAbortControllerRef.current = controller; + try { + // Revoke existing tokens if re-authenticating, but preserve step-up + // auth state so the next OAuth flow can reuse cached scope/discovery. + if (server.isAuthenticated && server.config) { + await revokeServerTokens(server.name, server.config, { + preserveStepUpState: true + }); + } + if (server.config) { + await performMCPOAuthFlow(server.name, server.config, setAuthorizationUrl, controller.signal, { + onWaitingForCallback: submit => { + setManualCallbackSubmit(() => submit); + } + }); + logEvent('tengu_mcp_auth_config_authenticate', { + wasAuthenticated: server.isAuthenticated + }); + const result_0 = await reconnectMcpServer(server.name); + if (result_0.client.type === 'connected') { + const message = isEffectivelyAuthenticated ? `Authentication successful. Reconnected to ${server.name}.` : `Authentication successful. Connected to ${server.name}.`; + onComplete?.(message); + } else if (result_0.client.type === 'needs-auth') { + onComplete?.('Authentication successful, but server still requires authentication. You may need to manually restart Claude Code.'); + } else { + // result.client.type === 'failed' + logMCPDebug(server.name, `Reconnection failed after authentication`); + onComplete?.('Authentication successful, but server reconnection failed. You may need to manually restart Claude Code for the changes to take effect.'); + } + } + } catch (err_1) { + // Don't show error if it was a cancellation + if (err_1 instanceof Error && !(err_1 instanceof AuthenticationCancelledError)) { + setError(err_1.message); + } + } finally { + setIsAuthenticating(false); + authAbortControllerRef.current = null; + setManualCallbackSubmit(null); + setCallbackUrlInput(''); + } + }, [server.isAuthenticated, server.config, server.name, onComplete, reconnectMcpServer, isEffectivelyAuthenticated]); + const handleClearAuth = async () => { + if (server.config.type === 'claudeai-proxy') return; + if (server.config) { + // First revoke the authentication tokens and clear all auth state + await revokeServerTokens(server.name, server.config); + logEvent('tengu_mcp_auth_config_clear', {}); + + // Disconnect the client and clear the cache + await clearServerCache(server.name, { + ...server.config, + scope: server.scope + }); + + // Update app state to remove the disconnected server's tools, commands, and resources + setAppState(prev_0 => { + const newClients_0 = prev_0.mcp.clients.map(c_0 => + // 'failed' is a misnomer here, but we don't really differentiate between "not connected" and "failed" at the moment + c_0.name === server.name ? { + ...c_0, + type: 'failed' as const + } : c_0); + const newTools_0 = excludeToolsByServer(prev_0.mcp.tools, server.name); + const newCommands_0 = excludeCommandsByServer(prev_0.mcp.commands, server.name); + const newResources_0 = excludeResourcesByServer(prev_0.mcp.resources, server.name); + return { + ...prev_0, + mcp: { + ...prev_0.mcp, + clients: newClients_0, + tools: newTools_0, + commands: newCommands_0, + resources: newResources_0 + } + }; + }); + onComplete?.(`Authentication cleared for ${server.name}.`); + } + }; + if (isAuthenticating) { + // XAA: silent exchange (cached id_token → no browser), so don't claim + // one will open. If IdP login IS needed, authorizationUrl populates and + // the URL fallback block below still renders. + const authCopy = server.config.type !== 'claudeai-proxy' && server.config.oauth?.xaa ? ' Authenticating via your identity provider' : ' A browser window will open for authentication'; + return + Authenticating with {server.name}… + + + {authCopy} + + {authorizationUrl && + + + If your browser doesn't open automatically, copy this URL + manually{' '} + + {urlCopied ? (Copied!) : + + } + + + } + {isAuthenticating && authorizationUrl && manualCallbackSubmit && + + If the redirect page shows a connection error, paste the URL from + your browser's address bar: + + + URL {'>'} + { + manualCallbackSubmit(value.trim()); + setCallbackUrlInput(''); + }} cursorOffset={callbackUrlCursorOffset} onChangeCursorOffset={setCallbackUrlCursorOffset} columns={terminalColumns - 8} /> + + } + + + Return here after authenticating in your browser. Press Esc to go + back. + + + ; + } + if (isClaudeAIAuthenticating) { + return + Authenticating with {server.name}… + + + A browser window will open for authentication + + {claudeAIAuthUrl && + + + If your browser doesn't open automatically, copy this URL + manually{' '} + + {urlCopied ? (Copied!) : + + } + + + } + + + Press Enter after authenticating in your browser. + + + + + + ; + } + if (isClaudeAIClearingAuth) { + return + Clear authentication for {server.name} + {claudeAIClearAuthBrowserOpened ? <> + + Find the MCP server in the browser and click + "Disconnect". + + {claudeAIClearAuthUrl && + + + If your browser didn't open automatically, copy this + URL manually{' '} + + {urlCopied ? (Copied!) : + + } + + + } + + + Press Enter when done. + + + + + + : <> + + This will open claude.ai in the browser. Find the MCP server in + the list and click "Disconnect". + + + + Press Enter to open the browser. + + + + + + } + ; + } + if (isReconnecting) { + return + + Connecting to {server.name}… + + + + Establishing connection to MCP server + + This may take a few moments. + ; + } + const menuOptions = []; + + // If server is disabled, show Enable first as the primary action + if (server.client.type === 'disabled') { + menuOptions.push({ + label: 'Enable', + value: 'toggle-enabled' + }); + } + if (server.client.type === 'connected' && serverToolsCount > 0) { + menuOptions.push({ + label: 'View tools', + value: 'tools' + }); + } + if (server.config.type === 'claudeai-proxy') { + if (server.client.type === 'connected') { + menuOptions.push({ + label: 'Clear authentication', + value: 'claudeai-clear-auth' + }); + } else if (server.client.type !== 'disabled') { + menuOptions.push({ + label: 'Authenticate', + value: 'claudeai-auth' + }); + } + } else { + if (isEffectivelyAuthenticated) { + menuOptions.push({ + label: 'Re-authenticate', + value: 'reauth' + }); + menuOptions.push({ + label: 'Clear authentication', + value: 'clear-auth' + }); + } + if (!isEffectivelyAuthenticated) { + menuOptions.push({ + label: 'Authenticate', + value: 'auth' + }); + } + } + if (server.client.type !== 'disabled') { + if (server.client.type !== 'needs-auth') { + menuOptions.push({ + label: 'Reconnect', + value: 'reconnectMcpServer' + }); + } + menuOptions.push({ + label: 'Disable', + value: 'toggle-enabled' + }); + } + + // If there are no other options, add a back option so Select handles escape + if (menuOptions.length === 0) { + menuOptions.push({ + label: 'Back', + value: 'back' + }); + } + return + + + {capitalizedServerName} MCP Server + + + + + Status: + {server.client.type === 'disabled' ? {color('inactive', theme)(figures.radioOff)} disabled : server.client.type === 'connected' ? {color('success', theme)(figures.tick)} connected : server.client.type === 'pending' ? <> + {figures.radioOff} + connecting… + : server.client.type === 'needs-auth' ? + {color('warning', theme)(figures.triangleUpOutline)} needs + authentication + : {color('error', theme)(figures.cross)} failed} + + + {server.transport !== 'claudeai-proxy' && + Auth: + {isEffectivelyAuthenticated ? + {color('success', theme)(figures.tick)} authenticated + : + {color('error', theme)(figures.cross)} not authenticated + } + } + + + URL: + {server.config.url} + + + + Config location: + {describeMcpConfigFilePath(server.scope)} + + + {server.client.type === 'connected' && } + + {server.client.type === 'connected' && serverToolsCount > 0 && + Tools: + {serverToolsCount} tools + } + + + {error && + Error: {error} + } + + {menuOptions.length > 0 && + { + if (value === 'tools') { + onViewTools(); + } else if (value === 'reconnectMcpServer') { + setIsReconnecting(true); + try { + const result = await reconnectMcpServer(server.name); + const { + message + } = handleReconnectResult(result, server.name); + onComplete?.(message); + } catch (err_0) { + onComplete?.(handleReconnectError(err_0, server.name)); + } finally { + setIsReconnecting(false); + } + } else if (value === 'toggle-enabled') { + await handleToggleEnabled(); + } else if (value === 'back') { + onCancel(); + } + }} onCancel={onCancel} /> + } + + + + + {exitState.pending ? <>Press {exitState.keyName} again to exit : + + + + } + + + ; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VTdGF0ZSIsIkNvbW1hbmRSZXN1bHREaXNwbGF5IiwidXNlRXhpdE9uQ3RybENEV2l0aEtleWJpbmRpbmdzIiwiQm94IiwiY29sb3IiLCJUZXh0IiwidXNlVGhlbWUiLCJnZXRNY3BDb25maWdCeU5hbWUiLCJ1c2VNY3BSZWNvbm5lY3QiLCJ1c2VNY3BUb2dnbGVFbmFibGVkIiwiZGVzY3JpYmVNY3BDb25maWdGaWxlUGF0aCIsImZpbHRlck1jcFByb21wdHNCeVNlcnZlciIsInVzZUFwcFN0YXRlIiwiZXJyb3JNZXNzYWdlIiwiY2FwaXRhbGl6ZSIsIkNvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCIsIlNlbGVjdCIsIkJ5bGluZSIsIktleWJvYXJkU2hvcnRjdXRIaW50IiwiU3Bpbm5lciIsIkNhcGFiaWxpdGllc1NlY3Rpb24iLCJTdGRpb1NlcnZlckluZm8iLCJoYW5kbGVSZWNvbm5lY3RFcnJvciIsImhhbmRsZVJlY29ubmVjdFJlc3VsdCIsIlByb3BzIiwic2VydmVyIiwic2VydmVyVG9vbHNDb3VudCIsIm9uVmlld1Rvb2xzIiwib25DYW5jZWwiLCJvbkNvbXBsZXRlIiwicmVzdWx0Iiwib3B0aW9ucyIsImRpc3BsYXkiLCJib3JkZXJsZXNzIiwiTUNQU3RkaW9TZXJ2ZXJNZW51IiwiUmVhY3ROb2RlIiwidGhlbWUiLCJleGl0U3RhdGUiLCJtY3AiLCJzIiwicmVjb25uZWN0TWNwU2VydmVyIiwidG9nZ2xlTWNwU2VydmVyIiwiaXNSZWNvbm5lY3RpbmciLCJzZXRJc1JlY29ubmVjdGluZyIsImhhbmRsZVRvZ2dsZUVuYWJsZWQiLCJ1c2VDYWxsYmFjayIsIndhc0VuYWJsZWQiLCJjbGllbnQiLCJ0eXBlIiwibmFtZSIsImVyciIsImFjdGlvbiIsImNhcGl0YWxpemVkU2VydmVyTmFtZSIsIlN0cmluZyIsInNlcnZlckNvbW1hbmRzQ291bnQiLCJjb21tYW5kcyIsImxlbmd0aCIsIm1lbnVPcHRpb25zIiwicHVzaCIsImxhYmVsIiwidmFsdWUiLCJ1bmRlZmluZWQiLCJyYWRpb09mZiIsInRpY2siLCJjcm9zcyIsImNvbmZpZyIsImNvbW1hbmQiLCJhcmdzIiwiam9pbiIsInNjb3BlIiwicmVzb3VyY2VzIiwibWVzc2FnZSIsInBlbmRpbmciLCJrZXlOYW1lIl0sInNvdXJjZXMiOlsiTUNQU3RkaW9TZXJ2ZXJNZW51LnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgZmlndXJlcyBmcm9tICdmaWd1cmVzJ1xuaW1wb3J0IFJlYWN0LCB7IHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgdHlwZSB7IENvbW1hbmRSZXN1bHREaXNwbGF5IH0gZnJvbSAnLi4vLi4vY29tbWFuZHMuanMnXG5pbXBvcnQgeyB1c2VFeGl0T25DdHJsQ0RXaXRoS2V5YmluZGluZ3MgfSBmcm9tICcuLi8uLi9ob29rcy91c2VFeGl0T25DdHJsQ0RXaXRoS2V5YmluZGluZ3MuanMnXG5pbXBvcnQgeyBCb3gsIGNvbG9yLCBUZXh0LCB1c2VUaGVtZSB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IGdldE1jcENvbmZpZ0J5TmFtZSB9IGZyb20gJy4uLy4uL3NlcnZpY2VzL21jcC9jb25maWcuanMnXG5pbXBvcnQge1xuICB1c2VNY3BSZWNvbm5lY3QsXG4gIHVzZU1jcFRvZ2dsZUVuYWJsZWQsXG59IGZyb20gJy4uLy4uL3NlcnZpY2VzL21jcC9NQ1BDb25uZWN0aW9uTWFuYWdlci5qcydcbmltcG9ydCB7XG4gIGRlc2NyaWJlTWNwQ29uZmlnRmlsZVBhdGgsXG4gIGZpbHRlck1jcFByb21wdHNCeVNlcnZlcixcbn0gZnJvbSAnLi4vLi4vc2VydmljZXMvbWNwL3V0aWxzLmpzJ1xuaW1wb3J0IHsgdXNlQXBwU3RhdGUgfSBmcm9tICcuLi8uLi9zdGF0ZS9BcHBTdGF0ZS5qcydcbmltcG9ydCB7IGVycm9yTWVzc2FnZSB9IGZyb20gJy4uLy4uL3V0aWxzL2Vycm9ycy5qcydcbmltcG9ydCB7IGNhcGl0YWxpemUgfSBmcm9tICcuLi8uLi91dGlscy9zdHJpbmdVdGlscy5qcydcbmltcG9ydCB7IENvbmZpZ3VyYWJsZVNob3J0Y3V0SGludCB9IGZyb20gJy4uL0NvbmZpZ3VyYWJsZVNob3J0Y3V0SGludC5qcydcbmltcG9ydCB7IFNlbGVjdCB9IGZyb20gJy4uL0N1c3RvbVNlbGVjdC9pbmRleC5qcydcbmltcG9ydCB7IEJ5bGluZSB9IGZyb20gJy4uL2Rlc2lnbi1zeXN0ZW0vQnlsaW5lLmpzJ1xuaW1wb3J0IHsgS2V5Ym9hcmRTaG9ydGN1dEhpbnQgfSBmcm9tICcuLi9kZXNpZ24tc3lzdGVtL0tleWJvYXJkU2hvcnRjdXRIaW50LmpzJ1xuaW1wb3J0IHsgU3Bpbm5lciB9IGZyb20gJy4uL1NwaW5uZXIuanMnXG5pbXBvcnQgeyBDYXBhYmlsaXRpZXNTZWN0aW9uIH0gZnJvbSAnLi9DYXBhYmlsaXRpZXNTZWN0aW9uLmpzJ1xuaW1wb3J0IHR5cGUgeyBTdGRpb1NlcnZlckluZm8gfSBmcm9tICcuL3R5cGVzLmpzJ1xuaW1wb3J0IHtcbiAgaGFuZGxlUmVjb25uZWN0RXJyb3IsXG4gIGhhbmRsZVJlY29ubmVjdFJlc3VsdCxcbn0gZnJvbSAnLi91dGlscy9yZWNvbm5lY3RIZWxwZXJzLmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBzZXJ2ZXI6IFN0ZGlvU2VydmVySW5mb1xuICBzZXJ2ZXJUb29sc0NvdW50OiBudW1iZXJcbiAgb25WaWV3VG9vbHM6ICgpID0+IHZvaWRcbiAgb25DYW5jZWw6ICgpID0+IHZvaWRcbiAgb25Db21wbGV0ZTogKFxuICAgIHJlc3VsdD86IHN0cmluZyxcbiAgICBvcHRpb25zPzogeyBkaXNwbGF5PzogQ29tbWFuZFJlc3VsdERpc3BsYXkgfSxcbiAgKSA9PiB2b2lkXG4gIGJvcmRlcmxlc3M/OiBib29sZWFuXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBNQ1BTdGRpb1NlcnZlck1lbnUoe1xuICBzZXJ2ZXIsXG4gIHNlcnZlclRvb2xzQ291bnQsXG4gIG9uVmlld1Rvb2xzLFxuICBvbkNhbmNlbCxcbiAgb25Db21wbGV0ZSxcbiAgYm9yZGVybGVzcyA9IGZhbHNlLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBbdGhlbWVdID0gdXNlVGhlbWUoKVxuICBjb25zdCBleGl0U3RhdGUgPSB1c2VFeGl0T25DdHJsQ0RXaXRoS2V5YmluZGluZ3MoKVxuICBjb25zdCBtY3AgPSB1c2VBcHBTdGF0ZShzID0+IHMubWNwKVxuICBjb25zdCByZWNvbm5lY3RNY3BTZXJ2ZXIgPSB1c2VNY3BSZWNvbm5lY3QoKVxuICBjb25zdCB0b2dnbGVNY3BTZXJ2ZXIgPSB1c2VNY3BUb2dnbGVFbmFibGVkKClcbiAgY29uc3QgW2lzUmVjb25uZWN0aW5nLCBzZXRJc1JlY29ubmVjdGluZ10gPSB1c2VTdGF0ZShmYWxzZSlcblxuICBjb25zdCBoYW5kbGVUb2dnbGVFbmFibGVkID0gUmVhY3QudXNlQ2FsbGJhY2soYXN5bmMgKCkgPT4ge1xuICAgIGNvbnN0IHdhc0VuYWJsZWQgPSBzZXJ2ZXIuY2xpZW50LnR5cGUgIT09ICdkaXNhYmxlZCdcblxuICAgIHRyeSB7XG4gICAgICBhd2FpdCB0b2dnbGVNY3BTZXJ2ZXIoc2VydmVyLm5hbWUpXG4gICAgICAvLyBSZXR1cm4gdG8gdGhlIHNlcnZlciBsaXN0IHNvIHVzZXIgY2FuIGNvbnRpbnVlIG1hbmFnaW5nIG90aGVyIHNlcnZlcnNcbiAgICAgIG9uQ2FuY2VsKClcbiAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgIGNvbnN0IGFjdGlvbiA9IHdhc0VuYWJsZWQgPyAnZGlzYWJsZScgOiAnZW5hYmxlJ1xuICAgICAgb25Db21wbGV0ZShcbiAgICAgICAgYEZhaWxlZCB0byAke2FjdGlvbn0gTUNQIHNlcnZlciAnJHtzZXJ2ZXIubmFtZX0nOiAke2Vycm9yTWVzc2FnZShlcnIpfWAsXG4gICAgICApXG4gICAgfVxuICB9LCBbc2VydmVyLmNsaWVudC50eXBlLCBzZXJ2ZXIubmFtZSwgdG9nZ2xlTWNwU2VydmVyLCBvbkNhbmNlbCwgb25Db21wbGV0ZV0pXG5cbiAgY29uc3QgY2FwaXRhbGl6ZWRTZXJ2ZXJOYW1lID0gY2FwaXRhbGl6ZShTdHJpbmcoc2VydmVyLm5hbWUpKVxuXG4gIC8vIENvdW50IE1DUCBwcm9tcHRzIGZvciB0aGlzIHNlcnZlciAoc2tpbGxzIGFyZSBzaG93biBpbiAvc2tpbGxzLCBub3QgaGVyZSlcbiAgY29uc3Qgc2VydmVyQ29tbWFuZHNDb3VudCA9IGZpbHRlck1jcFByb21wdHNCeVNlcnZlcihcbiAgICBtY3AuY29tbWFuZHMsXG4gICAgc2VydmVyLm5hbWUsXG4gICkubGVuZ3RoXG5cbiAgY29uc3QgbWVudU9wdGlvbnMgPSBbXVxuXG4gIC8vIE9ubHkgc2hvdyBcIlZpZXcgdG9vbHNcIiBpZiBzZXJ2ZXIgaXMgbm90IGRpc2FibGVkIGFuZCBoYXMgdG9vbHNcbiAgaWYgKHNlcnZlci5jbGllbnQudHlwZSAhPT0gJ2Rpc2FibGVkJyAmJiBzZXJ2ZXJUb29sc0NvdW50ID4gMCkge1xuICAgIG1lbnVPcHRpb25zLnB1c2goe1xuICAgICAgbGFiZWw6ICdWaWV3IHRvb2xzJyxcbiAgICAgIHZhbHVlOiAndG9vbHMnLFxuICAgIH0pXG4gIH1cblxuICAvLyBPbmx5IHNob3cgcmVjb25uZWN0IG9wdGlvbiBpZiB0aGUgc2VydmVyIGlzIG5vdCBkaXNhYmxlZFxuICBpZiAoc2VydmVyLmNsaWVudC50eXBlICE9PSAnZGlzYWJsZWQnKSB7XG4gICAgbWVudU9wdGlvbnMucHVzaCh7XG4gICAgICBsYWJlbDogJ1JlY29ubmVjdCcsXG4gICAgICB2YWx1ZTogJ3JlY29ubmVjdE1jcFNlcnZlcicsXG4gICAgfSlcbiAgfVxuXG4gIG1lbnVPcHRpb25zLnB1c2goe1xuICAgIGxhYmVsOiBzZXJ2ZXIuY2xpZW50LnR5cGUgIT09ICdkaXNhYmxlZCcgPyAnRGlzYWJsZScgOiAnRW5hYmxlJyxcbiAgICB2YWx1ZTogJ3RvZ2dsZS1lbmFibGVkJyxcbiAgfSlcblxuICAvLyBJZiB0aGVyZSBhcmUgbm8gb3RoZXIgb3B0aW9ucywgYWRkIGEgYmFjayBvcHRpb24gc28gU2VsZWN0IGhhbmRsZXMgZXNjYXBlXG4gIGlmIChtZW51T3B0aW9ucy5sZW5ndGggPT09IDApIHtcbiAgICBtZW51T3B0aW9ucy5wdXNoKHtcbiAgICAgIGxhYmVsOiAnQmFjaycsXG4gICAgICB2YWx1ZTogJ2JhY2snLFxuICAgIH0pXG4gIH1cblxuICBpZiAoaXNSZWNvbm5lY3RpbmcpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgZ2FwPXsxfSBwYWRkaW5nPXsxfT5cbiAgICAgICAgPFRleHQgY29sb3I9XCJ0ZXh0XCI+XG4gICAgICAgICAgUmVjb25uZWN0aW5nIHRvIDxUZXh0IGJvbGQ+e3NlcnZlci5uYW1lfTwvVGV4dD5cbiAgICAgICAgPC9UZXh0PlxuICAgICAgICA8Qm94PlxuICAgICAgICAgIDxTcGlubmVyIC8+XG4gICAgICAgICAgPFRleHQ+IFJlc3RhcnRpbmcgTUNQIHNlcnZlciBwcm9jZXNzPC9UZXh0PlxuICAgICAgICA8L0JveD5cbiAgICAgICAgPFRleHQgZGltQ29sb3I+VGhpcyBtYXkgdGFrZSBhIGZldyBtb21lbnRzLjwvVGV4dD5cbiAgICAgIDwvQm94PlxuICAgIClcbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICA8Qm94XG4gICAgICAgIGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIlxuICAgICAgICBwYWRkaW5nWD17MX1cbiAgICAgICAgYm9yZGVyU3R5bGU9e2JvcmRlcmxlc3MgPyB1bmRlZmluZWQgOiAncm91bmQnfVxuICAgICAgPlxuICAgICAgICA8Qm94IG1hcmdpbkJvdHRvbT17MX0+XG4gICAgICAgICAgPFRleHQgYm9sZD57Y2FwaXRhbGl6ZWRTZXJ2ZXJOYW1lfSBNQ1AgU2VydmVyPC9UZXh0PlxuICAgICAgICA8L0JveD5cblxuICAgICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIiBnYXA9ezB9PlxuICAgICAgICAgIDxCb3g+XG4gICAgICAgICAgICA8VGV4dCBib2xkPlN0YXR1czogPC9UZXh0PlxuICAgICAgICAgICAge3NlcnZlci5jbGllbnQudHlwZSA9PT0gJ2Rpc2FibGVkJyA/IChcbiAgICAgICAgICAgICAgPFRleHQ+e2NvbG9yKCdpbmFjdGl2ZScsIHRoZW1lKShmaWd1cmVzLnJhZGlvT2ZmKX0gZGlzYWJsZWQ8L1RleHQ+XG4gICAgICAgICAgICApIDogc2VydmVyLmNsaWVudC50eXBlID09PSAnY29ubmVjdGVkJyA/IChcbiAgICAgICAgICAgICAgPFRleHQ+e2NvbG9yKCdzdWNjZXNzJywgdGhlbWUpKGZpZ3VyZXMudGljayl9IGNvbm5lY3RlZDwvVGV4dD5cbiAgICAgICAgICAgICkgOiBzZXJ2ZXIuY2xpZW50LnR5cGUgPT09ICdwZW5kaW5nJyA/IChcbiAgICAgICAgICAgICAgPD5cbiAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj57ZmlndXJlcy5yYWRpb09mZn08L1RleHQ+XG4gICAgICAgICAgICAgICAgPFRleHQ+IGNvbm5lY3RpbmfigKY8L1RleHQ+XG4gICAgICAgICAgICAgIDwvPlxuICAgICAgICAgICAgKSA6IChcbiAgICAgICAgICAgICAgPFRleHQ+e2NvbG9yKCdlcnJvcicsIHRoZW1lKShmaWd1cmVzLmNyb3NzKX0gZmFpbGVkPC9UZXh0PlxuICAgICAgICAgICAgKX1cbiAgICAgICAgICA8L0JveD5cblxuICAgICAgICAgIDxCb3g+XG4gICAgICAgICAgICA8VGV4dCBib2xkPkNvbW1hbmQ6IDwvVGV4dD5cbiAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPntzZXJ2ZXIuY29uZmlnLmNvbW1hbmR9PC9UZXh0PlxuICAgICAgICAgIDwvQm94PlxuXG4gICAgICAgICAge3NlcnZlci5jb25maWcuYXJncyAmJiBzZXJ2ZXIuY29uZmlnLmFyZ3MubGVuZ3RoID4gMCAmJiAoXG4gICAgICAgICAgICA8Qm94PlxuICAgICAgICAgICAgICA8VGV4dCBib2xkPkFyZ3M6IDwvVGV4dD5cbiAgICAgICAgICAgICAgPFRleHQgZGltQ29sb3I+e3NlcnZlci5jb25maWcuYXJncy5qb2luKCcgJyl9PC9UZXh0PlxuICAgICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgKX1cblxuICAgICAgICAgIDxCb3g+XG4gICAgICAgICAgICA8VGV4dCBib2xkPkNvbmZpZyBsb2NhdGlvbjogPC9UZXh0PlxuICAgICAgICAgICAgPFRleHQgZGltQ29sb3I+XG4gICAgICAgICAgICAgIHtkZXNjcmliZU1jcENvbmZpZ0ZpbGVQYXRoKFxuICAgICAgICAgICAgICAgIGdldE1jcENvbmZpZ0J5TmFtZShzZXJ2ZXIubmFtZSk/LnNjb3BlID8/ICdkeW5hbWljJyxcbiAgICAgICAgICAgICAgKX1cbiAgICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICA8L0JveD5cblxuICAgICAgICAgIHtzZXJ2ZXIuY2xpZW50LnR5cGUgPT09ICdjb25uZWN0ZWQnICYmIChcbiAgICAgICAgICAgIDxDYXBhYmlsaXRpZXNTZWN0aW9uXG4gICAgICAgICAgICAgIHNlcnZlclRvb2xzQ291bnQ9e3NlcnZlclRvb2xzQ291bnR9XG4gICAgICAgICAgICAgIHNlcnZlclByb21wdHNDb3VudD17c2VydmVyQ29tbWFuZHNDb3VudH1cbiAgICAgICAgICAgICAgc2VydmVyUmVzb3VyY2VzQ291bnQ9e21jcC5yZXNvdXJjZXNbc2VydmVyLm5hbWVdPy5sZW5ndGggfHwgMH1cbiAgICAgICAgICAgIC8+XG4gICAgICAgICAgKX1cblxuICAgICAgICAgIHtzZXJ2ZXIuY2xpZW50LnR5cGUgPT09ICdjb25uZWN0ZWQnICYmIHNlcnZlclRvb2xzQ291bnQgPiAwICYmIChcbiAgICAgICAgICAgIDxCb3g+XG4gICAgICAgICAgICAgIDxUZXh0IGJvbGQ+VG9vbHM6IDwvVGV4dD5cbiAgICAgICAgICAgICAgPFRleHQgZGltQ29sb3I+e3NlcnZlclRvb2xzQ291bnR9IHRvb2xzPC9UZXh0PlxuICAgICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgKX1cbiAgICAgICAgPC9Cb3g+XG5cbiAgICAgICAge21lbnVPcHRpb25zLmxlbmd0aCA+IDAgJiYgKFxuICAgICAgICAgIDxCb3ggbWFyZ2luVG9wPXsxfT5cbiAgICAgICAgICAgIDxTZWxlY3RcbiAgICAgICAgICAgICAgb3B0aW9ucz17bWVudU9wdGlvbnN9XG4gICAgICAgICAgICAgIG9uQ2hhbmdlPXthc3luYyB2YWx1ZSA9PiB7XG4gICAgICAgICAgICAgICAgaWYgKHZhbHVlID09PSAndG9vbHMnKSB7XG4gICAgICAgICAgICAgICAgICBvblZpZXdUb29scygpXG4gICAgICAgICAgICAgICAgfSBlbHNlIGlmICh2YWx1ZSA9PT0gJ3JlY29ubmVjdE1jcFNlcnZlcicpIHtcbiAgICAgICAgICAgICAgICAgIHNldElzUmVjb25uZWN0aW5nKHRydWUpXG4gICAgICAgICAgICAgICAgICB0cnkge1xuICAgICAgICAgICAgICAgICAgICBjb25zdCByZXN1bHQgPSBhd2FpdCByZWNvbm5lY3RNY3BTZXJ2ZXIoc2VydmVyLm5hbWUpXG4gICAgICAgICAgICAgICAgICAgIGNvbnN0IHsgbWVzc2FnZSB9ID0gaGFuZGxlUmVjb25uZWN0UmVzdWx0KFxuICAgICAgICAgICAgICAgICAgICAgIHJlc3VsdCxcbiAgICAgICAgICAgICAgICAgICAgICBzZXJ2ZXIubmFtZSxcbiAgICAgICAgICAgICAgICAgICAgKVxuICAgICAgICAgICAgICAgICAgICBvbkNvbXBsZXRlPy4obWVzc2FnZSlcbiAgICAgICAgICAgICAgICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgICAgICAgICAgICAgICBvbkNvbXBsZXRlPy4oaGFuZGxlUmVjb25uZWN0RXJyb3IoZXJyLCBzZXJ2ZXIubmFtZSkpXG4gICAgICAgICAgICAgICAgICB9IGZpbmFsbHkge1xuICAgICAgICAgICAgICAgICAgICBzZXRJc1JlY29ubmVjdGluZyhmYWxzZSlcbiAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9IGVsc2UgaWYgKHZhbHVlID09PSAndG9nZ2xlLWVuYWJsZWQnKSB7XG4gICAgICAgICAgICAgICAgICBhd2FpdCBoYW5kbGVUb2dnbGVFbmFibGVkKClcbiAgICAgICAgICAgICAgICB9IGVsc2UgaWYgKHZhbHVlID09PSAnYmFjaycpIHtcbiAgICAgICAgICAgICAgICAgIG9uQ2FuY2VsKClcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgIH19XG4gICAgICAgICAgICAgIG9uQ2FuY2VsPXtvbkNhbmNlbH1cbiAgICAgICAgICAgIC8+XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgICl9XG4gICAgICA8L0JveD5cblxuICAgICAgPEJveCBtYXJnaW5Ub3A9ezF9PlxuICAgICAgICA8VGV4dCBkaW1Db2xvciBpdGFsaWM+XG4gICAgICAgICAge2V4aXRTdGF0ZS5wZW5kaW5nID8gKFxuICAgICAgICAgICAgPD5QcmVzcyB7ZXhpdFN0YXRlLmtleU5hbWV9IGFnYWluIHRvIGV4aXQ8Lz5cbiAgICAgICAgICApIDogKFxuICAgICAgICAgICAgPEJ5bGluZT5cbiAgICAgICAgICAgICAgPEtleWJvYXJkU2hvcnRjdXRIaW50IHNob3J0Y3V0PVwi4oaR4oaTXCIgYWN0aW9uPVwibmF2aWdhdGVcIiAvPlxuICAgICAgICAgICAgICA8S2V5Ym9hcmRTaG9ydGN1dEhpbnQgc2hvcnRjdXQ9XCJFbnRlclwiIGFjdGlvbj1cInNlbGVjdFwiIC8+XG4gICAgICAgICAgICAgIDxDb25maWd1cmFibGVTaG9ydGN1dEhpbnRcbiAgICAgICAgICAgICAgICBhY3Rpb249XCJjb25maXJtOm5vXCJcbiAgICAgICAgICAgICAgICBjb250ZXh0PVwiQ29uZmlybWF0aW9uXCJcbiAgICAgICAgICAgICAgICBmYWxsYmFjaz1cIkVzY1wiXG4gICAgICAgICAgICAgICAgZGVzY3JpcHRpb249XCJiYWNrXCJcbiAgICAgICAgICAgICAgLz5cbiAgICAgICAgICAgIDwvQnlsaW5lPlxuICAgICAgICAgICl9XG4g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 \ No newline at end of file diff --git a/src/components/mcp/MCPToolListView.tsx b/src/components/mcp/MCPToolListView.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0e1e8bee6c6fd52cd22a05e50b880a42c03c844c --- /dev/null +++ b/src/components/mcp/MCPToolListView.tsx @@ -0,0 +1,141 @@ +import { c as _c } from "react/compiler-runtime"; +import React from 'react'; +import { Text } from '../../ink.js'; +import { extractMcpToolDisplayName, getMcpDisplayName } from '../../services/mcp/mcpStringUtils.js'; +import { filterToolsByServer } from '../../services/mcp/utils.js'; +import { useAppState } from '../../state/AppState.js'; +import type { Tool } from '../../Tool.js'; +import { plural } from '../../utils/stringUtils.js'; +import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; +import { Select } from '../CustomSelect/index.js'; +import { Byline } from '../design-system/Byline.js'; +import { Dialog } from '../design-system/Dialog.js'; +import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; +import type { ServerInfo } from './types.js'; +type Props = { + server: ServerInfo; + onSelectTool: (tool: Tool, index: number) => void; + onBack: () => void; +}; +export function MCPToolListView(t0) { + const $ = _c(21); + const { + server, + onSelectTool, + onBack + } = t0; + const mcpTools = useAppState(_temp); + let t1; + bb0: { + if (server.client.type !== "connected") { + let t2; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t2 = []; + $[0] = t2; + } else { + t2 = $[0]; + } + t1 = t2; + break bb0; + } + let t2; + if ($[1] !== mcpTools || $[2] !== server.name) { + t2 = filterToolsByServer(mcpTools, server.name); + $[1] = mcpTools; + $[2] = server.name; + $[3] = t2; + } else { + t2 = $[3]; + } + t1 = t2; + } + const serverTools = t1; + let t2; + if ($[4] !== server.name || $[5] !== serverTools) { + let t3; + if ($[7] !== server.name) { + t3 = (tool, index) => { + const toolName = getMcpDisplayName(tool.name, server.name); + const fullDisplayName = tool.userFacingName ? tool.userFacingName({}) : toolName; + const displayName = extractMcpToolDisplayName(fullDisplayName); + const isReadOnly = tool.isReadOnly?.({}) ?? false; + const isDestructive = tool.isDestructive?.({}) ?? false; + const isOpenWorld = tool.isOpenWorld?.({}) ?? false; + const annotations = []; + if (isReadOnly) { + annotations.push("read-only"); + } + if (isDestructive) { + annotations.push("destructive"); + } + if (isOpenWorld) { + annotations.push("open-world"); + } + return { + label: displayName, + value: index.toString(), + description: annotations.length > 0 ? annotations.join(", ") : undefined, + descriptionColor: isDestructive ? "error" : isReadOnly ? "success" : undefined + }; + }; + $[7] = server.name; + $[8] = t3; + } else { + t3 = $[8]; + } + t2 = serverTools.map(t3); + $[4] = server.name; + $[5] = serverTools; + $[6] = t2; + } else { + t2 = $[6]; + } + const toolOptions = t2; + const t3 = `Tools for ${server.name}`; + const t4 = serverTools.length; + let t5; + if ($[9] !== serverTools.length) { + t5 = plural(serverTools.length, "tool"); + $[9] = serverTools.length; + $[10] = t5; + } else { + t5 = $[10]; + } + const t6 = `${t4} ${t5}`; + let t7; + if ($[11] !== onBack || $[12] !== onSelectTool || $[13] !== serverTools || $[14] !== toolOptions) { + t7 = serverTools.length === 0 ? No tools available : onFinalResponse(value as 'submit' | 'cancel')} onCancel={() => onFinalResponse("cancel")} />; + $[16] = onFinalResponse; + $[17] = t10; + } else { + t10 = $[17]; + } + let t11; + if ($[18] !== minContentHeight || $[19] !== t10 || $[20] !== t4 || $[21] !== t5 || $[22] !== t6) { + t11 = {t4}{t5}{t6}{t7}{t10}; + $[18] = minContentHeight; + $[19] = t10; + $[20] = t4; + $[21] = t5; + $[22] = t6; + $[23] = t11; + } else { + t11 = $[23]; + } + let t12; + if ($[24] !== t11 || $[25] !== t2) { + t12 = {t1}{t2}{t3}{t11}; + $[24] = t11; + $[25] = t2; + $[26] = t12; + } else { + t12 = $[26]; + } + return t12; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJCb3giLCJUZXh0IiwiUXVlc3Rpb24iLCJQZXJtaXNzaW9uRGVjaXNpb24iLCJTZWxlY3QiLCJEaXZpZGVyIiwiUGVybWlzc2lvblJlcXVlc3RUaXRsZSIsIlBlcm1pc3Npb25SdWxlRXhwbGFuYXRpb24iLCJRdWVzdGlvbk5hdmlnYXRpb25CYXIiLCJQcm9wcyIsInF1ZXN0aW9ucyIsImN1cnJlbnRRdWVzdGlvbkluZGV4IiwiYW5zd2VycyIsIlJlY29yZCIsImFsbFF1ZXN0aW9uc0Fuc3dlcmVkIiwicGVybWlzc2lvblJlc3VsdCIsIm1pbkNvbnRlbnRIZWlnaHQiLCJvbkZpbmFsUmVzcG9uc2UiLCJ2YWx1ZSIsIlN1Ym1pdFF1ZXN0aW9uc1ZpZXciLCJ0MCIsIiQiLCJfYyIsInQxIiwiU3ltYm9sIiwiZm9yIiwidDIiLCJ0MyIsInQ0Iiwid2FybmluZyIsInQ1IiwiT2JqZWN0Iiwia2V5cyIsImxlbmd0aCIsImZpbHRlciIsInEiLCJxdWVzdGlvbiIsIm1hcCIsInFfMCIsImFuc3dlciIsImJ1bGxldCIsImFycm93UmlnaHQiLCJ0NiIsInQ3IiwidDgiLCJ0eXBlIiwiY29uc3QiLCJsYWJlbCIsInQ5IiwidDEwIiwidDExIiwidDEyIl0sInNvdXJjZXMiOlsiU3VibWl0UXVlc3Rpb25zVmlldy50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZpZ3VyZXMgZnJvbSAnZmlndXJlcydcbmltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uLy4uLy4uL2luay5qcydcbmltcG9ydCB0eXBlIHsgUXVlc3Rpb24gfSBmcm9tICcuLi8uLi8uLi90b29scy9Bc2tVc2VyUXVlc3Rpb25Ub29sL0Fza1VzZXJRdWVzdGlvblRvb2wuanMnXG5pbXBvcnQgdHlwZSB7IFBlcm1pc3Npb25EZWNpc2lvbiB9IGZyb20gJy4uLy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL1Blcm1pc3Npb25SZXN1bHQuanMnXG5pbXBvcnQgeyBTZWxlY3QgfSBmcm9tICcuLi8uLi9DdXN0b21TZWxlY3QvaW5kZXguanMnXG5pbXBvcnQgeyBEaXZpZGVyIH0gZnJvbSAnLi4vLi4vZGVzaWduLXN5c3RlbS9EaXZpZGVyLmpzJ1xuaW1wb3J0IHsgUGVybWlzc2lvblJlcXVlc3RUaXRsZSB9IGZyb20gJy4uL1Blcm1pc3Npb25SZXF1ZXN0VGl0bGUuanMnXG5pbXBvcnQgeyBQZXJtaXNzaW9uUnVsZUV4cGxhbmF0aW9uIH0gZnJvbSAnLi4vUGVybWlzc2lvblJ1bGVFeHBsYW5hdGlvbi5qcydcbmltcG9ydCB7IFF1ZXN0aW9uTmF2aWdhdGlvbkJhciB9IGZyb20gJy4vUXVlc3Rpb25OYXZpZ2F0aW9uQmFyLmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBxdWVzdGlvbnM6IFF1ZXN0aW9uW11cbiAgY3VycmVudFF1ZXN0aW9uSW5kZXg6IG51bWJlclxuICBhbnN3ZXJzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+XG4gIGFsbFF1ZXN0aW9uc0Fuc3dlcmVkOiBib29sZWFuXG4gIHBlcm1pc3Npb25SZXN1bHQ6IFBlcm1pc3Npb25EZWNpc2lvblxuICBtaW5Db250ZW50SGVpZ2h0PzogbnVtYmVyXG4gIG9uRmluYWxSZXNwb25zZTogKHZhbHVlOiAnc3VibWl0JyB8ICdjYW5jZWwnKSA9PiB2b2lkXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBTdWJtaXRRdWVzdGlvbnNWaWV3KHtcbiAgcXVlc3Rpb25zLFxuICBjdXJyZW50UXVlc3Rpb25JbmRleCxcbiAgYW5zd2VycyxcbiAgYWxsUXVlc3Rpb25zQW5zd2VyZWQsXG4gIHBlcm1pc3Npb25SZXN1bHQsXG4gIG1pbkNvbnRlbnRIZWlnaHQsXG4gIG9uRmluYWxSZXNwb25zZSxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuIChcbiAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIiBtYXJnaW5Ub3A9ezF9PlxuICAgICAgPERpdmlkZXIgY29sb3I9XCJpbmFjdGl2ZVwiIC8+XG4gICAgICA8Qm94XG4gICAgICAgIGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIlxuICAgICAgICBib3JkZXJUb3BcbiAgICAgICAgYm9yZGVyQ29sb3I9XCJpbmFjdGl2ZVwiXG4gICAgICAgIHBhZGRpbmdUb3A9ezB9XG4gICAgICA+XG4gICAgICAgIDxRdWVzdGlvbk5hdmlnYXRpb25CYXJcbiAgICAgICAgICBxdWVzdGlvbnM9e3F1ZXN0aW9uc31cbiAgICAgICAgICBjdXJyZW50UXVlc3Rpb25JbmRleD17Y3VycmVudFF1ZXN0aW9uSW5kZXh9XG4gICAgICAgICAgYW5zd2Vycz17YW5zd2Vyc31cbiAgICAgICAgLz5cbiAgICAgICAgPFBlcm1pc3Npb25SZXF1ZXN0VGl0bGUgdGl0bGU9XCJSZXZpZXcgeW91ciBhbnN3ZXJzXCIgY29sb3I9XCJ0ZXh0XCIgLz5cbiAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfSBtaW5IZWlnaHQ9e21pbkNvbnRlbnRIZWlnaHR9PlxuICAgICAgICAgIHshYWxsUXVlc3Rpb25zQW5zd2VyZWQgJiYgKFxuICAgICAgICAgICAgPEJveCBtYXJnaW5Cb3R0b209ezF9PlxuICAgICAgICAgICAgICA8VGV4dCBjb2xvcj1cIndhcm5pbmdcIj5cbiAgICAgICAgICAgICAgICB7ZmlndXJlcy53YXJuaW5nfSBZb3UgaGF2ZSBub3QgYW5zd2VyZWQgYWxsIHF1ZXN0aW9uc1xuICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICApfVxuICAgICAgICAgIHtPYmplY3Qua2V5cyhhbnN3ZXJzKS5sZW5ndGggPiAwICYmIChcbiAgICAgICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiIG1hcmdpbkJvdHRvbT17MX0+XG4gICAgICAgICAgICAgIHtxdWVzdGlvbnNcbiAgICAgICAgICAgICAgICAuZmlsdGVyKChxOiBRdWVzdGlvbikgPT4gcT8ucXVlc3Rpb24gJiYgYW5zd2Vyc1txLnF1ZXN0aW9uXSlcbiAgICAgICAgICAgICAgICAubWFwKChxOiBRdWVzdGlvbikgPT4ge1xuICAgICAgICAgICAgICAgICAgY29uc3QgYW5zd2VyID0gYW5zd2Vyc1txPy5xdWVzdGlvbl1cblxuICAgICAgICAgICAgICAgICAgcmV0dXJuIChcbiAgICAgICAgICAgICAgICAgICAgPEJveFxuICAgICAgICAgICAgICAgICAgICAgIGtleT17cT8ucXVlc3Rpb24gfHwgJ2Fuc3dlcid9XG4gICAgICAgICAgICAgICAgICAgICAgZmxleERpcmVjdGlvbj1cImNvbHVtblwiXG4gICAgICAgICAgICAgICAgICAgICAgbWFyZ2luTGVmdD17MX1cbiAgICAgICAgICAgICAgICAgICAgPlxuICAgICAgICAgICAgICAgICAgICAgIDxUZXh0PlxuICAgICAgICAgICAgICAgICAgICAgICAge2ZpZ3VyZXMuYnVsbGV0fSB7cT8ucXVlc3Rpb24gfHwgJ1F1ZXN0aW9uJ31cbiAgICAgICAgICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgICAgICAgICAgPEJveCBtYXJnaW5MZWZ0PXsyfT5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxUZXh0IGNvbG9yPVwic3VjY2Vzc1wiPlxuICAgICAgICAgICAgICAgICAgICAgICAgICB7ZmlndXJlcy5hcnJvd1JpZ2h0fSB7YW5zd2VyfVxuICAgICAgICAgICAgICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgICAgICAgICAgICAgIDwvQm94PlxuICAgICAgICAgICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICAgICAgICAgIClcbiAgICAgICAgICAgICAgICB9KX1cbiAgICAgICAgICAgIDwvQm94PlxuICAgICAgICAgICl9XG5cbiAgICAgICAgICA8UGVybWlzc2lvblJ1bGVFeHBsYW5hdGlvblxuICAgICAgICAgICAgcGVybWlzc2lvblJlc3VsdD17cGVybWlzc2lvblJlc3VsdH1cbiAgICAgICAgICAgIHRvb2xUeXBlPVwidG9vbFwiXG4gICAgICAgICAgLz5cbiAgICAgICAgICA8VGV4dCBjb2xvcj1cImluYWN0aXZlXCI+UmVhZHkgdG8gc3VibWl0IHlvdXIgYW5zd2Vycz88L1RleHQ+XG4gICAgICAgICAgPEJveCBtYXJnaW5Ub3A9ezF9PlxuICAgICAgICAgICAgPFNlbGVjdFxuICAgICAgICAgICAgICBvcHRpb25zPXtbXG4gICAgICAgICAgICAgICAge1xuICAgICAgICAgICAgICAgICAgdHlwZTogJ3RleHQnIGFzIGNvbnN0LFxuICAgICAgICAgICAgICAgICAgbGFiZWw6ICdTdWJtaXQgYW5zd2VycycsXG4gICAgICAgICAgICAgICAgICB2YWx1ZTogJ3N1Ym1pdCcsXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICB7IHR5cGU6ICd0ZXh0JyBhcyBjb25zdCwgbGFiZWw6ICdDYW5jZWwnLCB2YWx1ZTogJ2NhbmNlbCcgfSxcbiAgICAgICAgICAgICAgXX1cbiAgICAgICAgICAgICAgb25DaGFuZ2U9e3ZhbHVlID0+IG9uRmluYWxSZXNwb25zZSh2YWx1ZSBhcyAnc3VibWl0JyB8ICdjYW5jZWwnKX1cbiAgICAgICAgICAgICAgb25DYW5jZWw9eygpID0+IG9uRmluYWxSZXNwb25zZSgnY2FuY2VsJyl9XG4gICAgICAgICAgICAvPlxuICAgICAgICAgIDwvQm94PlxuICAgICAgICA8L0JveD5cbiAgICAgIDwvQm94PlxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxPQUFPQSxPQUFPLE1BQU0sU0FBUztBQUM3QixPQUFPQyxLQUFLLE1BQU0sT0FBTztBQUN6QixTQUFTQyxHQUFHLEVBQUVDLElBQUksUUFBUSxpQkFBaUI7QUFDM0MsY0FBY0MsUUFBUSxRQUFRLDJEQUEyRDtBQUN6RixjQUFjQyxrQkFBa0IsUUFBUSxnREFBZ0Q7QUFDeEYsU0FBU0MsTUFBTSxRQUFRLDZCQUE2QjtBQUNwRCxTQUFTQyxPQUFPLFFBQVEsZ0NBQWdDO0FBQ3hELFNBQVND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 \ No newline at end of file diff --git a/src/components/permissions/AskUserQuestionPermissionRequest/use-multiple-choice-state.ts b/src/components/permissions/AskUserQuestionPermissionRequest/use-multiple-choice-state.ts new file mode 100644 index 0000000000000000000000000000000000000000..d500e8dafb71f2410d4a08b26d61fe2cbd40f552 --- /dev/null +++ b/src/components/permissions/AskUserQuestionPermissionRequest/use-multiple-choice-state.ts @@ -0,0 +1,179 @@ +import { useCallback, useReducer } from 'react' + +export type AnswerValue = string + +export type QuestionState = { + selectedValue?: string | string[] + textInputValue: string +} + +type State = { + currentQuestionIndex: number + answers: Record + questionStates: Record + isInTextInput: boolean +} + +type Action = + | { type: 'next-question' } + | { type: 'prev-question' } + | { + type: 'update-question-state' + questionText: string + updates: Partial + isMultiSelect: boolean + } + | { + type: 'set-answer' + questionText: string + answer: string + shouldAdvance: boolean + } + | { type: 'set-text-input-mode'; isInInput: boolean } + +function reducer(state: State, action: Action): State { + switch (action.type) { + case 'next-question': + return { + ...state, + currentQuestionIndex: state.currentQuestionIndex + 1, + isInTextInput: false, + } + + case 'prev-question': + return { + ...state, + currentQuestionIndex: Math.max(0, state.currentQuestionIndex - 1), + isInTextInput: false, + } + + case 'update-question-state': { + const existing = state.questionStates[action.questionText] + const newState: QuestionState = { + selectedValue: + action.updates.selectedValue ?? + existing?.selectedValue ?? + (action.isMultiSelect ? [] : undefined), + textInputValue: + action.updates.textInputValue ?? existing?.textInputValue ?? '', + } + + return { + ...state, + questionStates: { + ...state.questionStates, + [action.questionText]: newState, + }, + } + } + + case 'set-answer': { + const newState = { + ...state, + answers: { + ...state.answers, + [action.questionText]: action.answer, + }, + } + + if (action.shouldAdvance) { + return { + ...newState, + currentQuestionIndex: newState.currentQuestionIndex + 1, + isInTextInput: false, + } + } + + return newState + } + + case 'set-text-input-mode': + return { + ...state, + isInTextInput: action.isInInput, + } + } +} + +const INITIAL_STATE: State = { + currentQuestionIndex: 0, + answers: {}, + questionStates: {}, + isInTextInput: false, +} + +export type MultipleChoiceState = { + currentQuestionIndex: number + answers: Record + questionStates: Record + isInTextInput: boolean + nextQuestion: () => void + prevQuestion: () => void + updateQuestionState: ( + questionText: string, + updates: Partial, + isMultiSelect: boolean, + ) => void + setAnswer: ( + questionText: string, + answer: string, + shouldAdvance?: boolean, + ) => void + setTextInputMode: (isInInput: boolean) => void +} + +export function useMultipleChoiceState(): MultipleChoiceState { + const [state, dispatch] = useReducer(reducer, INITIAL_STATE) + + const nextQuestion = useCallback(() => { + dispatch({ type: 'next-question' }) + }, []) + + const prevQuestion = useCallback(() => { + dispatch({ type: 'prev-question' }) + }, []) + + const updateQuestionState = useCallback( + ( + questionText: string, + updates: Partial, + isMultiSelect: boolean, + ) => { + dispatch({ + type: 'update-question-state', + questionText, + updates, + isMultiSelect, + }) + }, + [], + ) + + const setAnswer = useCallback( + (questionText: string, answer: string, shouldAdvance: boolean = true) => { + dispatch({ + type: 'set-answer', + questionText, + answer, + shouldAdvance, + }) + }, + [], + ) + + const setTextInputMode = useCallback((isInInput: boolean) => { + dispatch({ type: 'set-text-input-mode', isInInput }) + }, []) + + return { + currentQuestionIndex: state.currentQuestionIndex, + answers: state.answers, + questionStates: state.questionStates, + isInTextInput: state.isInTextInput, + nextQuestion, + prevQuestion, + updateQuestionState, + setAnswer, + setTextInputMode, + } +} diff --git a/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx b/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8e0ca6c5e070dcdd9e78a3756b5cc72dda04dd2e --- /dev/null +++ b/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx @@ -0,0 +1,768 @@ +import { feature } from 'bun:bundle'; +import type { UUID } from 'crypto'; +import figures from 'figures'; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useNotifications } from 'src/context/notifications.js'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { useAppState, useAppStateStore, useSetAppState } from 'src/state/AppState.js'; +import { getSdkBetas, getSessionId, isSessionPersistenceDisabled, setHasExitedPlanMode, setNeedsAutoModeExitAttachment, setNeedsPlanModeExitAttachment } from '../../../bootstrap/state.js'; +import { generateSessionName } from '../../../commands/rename/generateSessionName.js'; +import { launchUltraplan } from '../../../commands/ultraplan.js'; +import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'; +import { Box, Text } from '../../../ink.js'; +import type { AppState } from '../../../state/AppStateStore.js'; +import { AGENT_TOOL_NAME } from '../../../tools/AgentTool/constants.js'; +import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../../tools/ExitPlanModeTool/constants.js'; +import type { AllowedPrompt } from '../../../tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'; +import { TEAM_CREATE_TOOL_NAME } from '../../../tools/TeamCreateTool/constants.js'; +import { isAgentSwarmsEnabled } from '../../../utils/agentSwarmsEnabled.js'; +import { calculateContextPercentages, getContextWindowForModel } from '../../../utils/context.js'; +import { getExternalEditor } from '../../../utils/editor.js'; +import { getDisplayPath } from '../../../utils/file.js'; +import { toIDEDisplayName } from '../../../utils/ide.js'; +import { logError } from '../../../utils/log.js'; +import { enqueuePendingNotification } from '../../../utils/messageQueueManager.js'; +import { createUserMessage } from '../../../utils/messages.js'; +import { getMainLoopModel, getRuntimeMainLoopModel } from '../../../utils/model/model.js'; +import { createPromptRuleContent, isClassifierPermissionsEnabled, PROMPT_PREFIX } from '../../../utils/permissions/bashClassifier.js'; +import { type PermissionMode, toExternalPermissionMode } from '../../../utils/permissions/PermissionMode.js'; +import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'; +import { isAutoModeGateEnabled, restoreDangerousPermissions, stripDangerousPermissionsForAutoMode } from '../../../utils/permissions/permissionSetup.js'; +import { getPewterLedgerVariant, isPlanModeInterviewPhaseEnabled } from '../../../utils/planModeV2.js'; +import { getPlan, getPlanFilePath } from '../../../utils/plans.js'; +import { editFileInEditor, editPromptInEditor } from '../../../utils/promptEditor.js'; +import { getCurrentSessionTitle, getTranscriptPath, saveAgentName, saveCustomTitle } from '../../../utils/sessionStorage.js'; +import { getSettings_DEPRECATED } from '../../../utils/settings/settings.js'; +import { type OptionWithDescription, Select } from '../../CustomSelect/index.js'; +import { Markdown } from '../../Markdown.js'; +import { PermissionDialog } from '../PermissionDialog.js'; +import type { PermissionRequestProps } from '../PermissionRequest.js'; +import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'; + +/* eslint-disable @typescript-eslint/no-require-imports */ +const autoModeStateModule = feature('TRANSCRIPT_CLASSIFIER') ? require('../../../utils/permissions/autoModeState.js') as typeof import('../../../utils/permissions/autoModeState.js') : null; +import type { Base64ImageSource, ImageBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'; +/* eslint-enable @typescript-eslint/no-require-imports */ +import type { PastedContent } from '../../../utils/config.js'; +import type { ImageDimensions } from '../../../utils/imageResizer.js'; +import { maybeResizeAndDownsampleImageBlock } from '../../../utils/imageResizer.js'; +import { cacheImagePath, storeImage } from '../../../utils/imageStore.js'; +type ResponseValue = 'yes-bypass-permissions' | 'yes-accept-edits' | 'yes-accept-edits-keep-context' | 'yes-default-keep-context' | 'yes-resume-auto-mode' | 'yes-auto-clear-context' | 'ultraplan' | 'no'; + +/** + * Build permission updates for plan approval, including prompt-based rules if provided. + * Prompt-based rules are only added when classifier permissions are enabled (Ant-only). + */ +export function buildPermissionUpdates(mode: PermissionMode, allowedPrompts?: AllowedPrompt[]): PermissionUpdate[] { + const updates: PermissionUpdate[] = [{ + type: 'setMode', + mode: toExternalPermissionMode(mode), + destination: 'session' + }]; + + // Add prompt-based permission rules if provided (Ant-only feature) + if (isClassifierPermissionsEnabled() && allowedPrompts && allowedPrompts.length > 0) { + updates.push({ + type: 'addRules', + rules: allowedPrompts.map(p => ({ + toolName: p.tool, + ruleContent: createPromptRuleContent(p.prompt) + })), + behavior: 'allow', + destination: 'session' + }); + } + return updates; +} + +/** + * Auto-name the session from the plan content when the user accepts a plan, + * if they haven't already named it via /rename or --name. Fire-and-forget. + * Mirrors /rename: kebab-case name, updates the prompt-border badge. + */ +export function autoNameSessionFromPlan(plan: string, setAppState: (updater: (prev: AppState) => AppState) => void, isClearContext: boolean): void { + if (isSessionPersistenceDisabled() || getSettings_DEPRECATED()?.cleanupPeriodDays === 0) { + return; + } + // On clear-context, the current session is about to be abandoned — its + // title (which may have been set by a PRIOR auto-name) is irrelevant. + // Checking it would make the feature self-defeating after first use. + if (!isClearContext && getCurrentSessionTitle(getSessionId())) return; + void generateSessionName( + // generateSessionName tail-slices to the last 1000 chars (correct for + // conversations, where recency matters). Plans front-load the goal and + // end with testing steps — head-slice so Haiku sees the summary. + [createUserMessage({ + content: plan.slice(0, 1000) + })], new AbortController().signal).then(async name => { + // On clear-context acceptance, regenerateSessionId() has run by now — + // this intentionally names the NEW execution session. Do not "fix" by + // capturing sessionId once; that would name the abandoned planning session. + if (!name || getCurrentSessionTitle(getSessionId())) return; + const sessionId = getSessionId() as UUID; + const fullPath = getTranscriptPath(); + await saveCustomTitle(sessionId, name, fullPath, 'auto'); + await saveAgentName(sessionId, name, fullPath, 'auto'); + setAppState(prev => { + if (prev.standaloneAgentContext?.name === name) return prev; + return { + ...prev, + standaloneAgentContext: { + ...prev.standaloneAgentContext, + name + } + }; + }); + }).catch(logError); +} +export function ExitPlanModePermissionRequest({ + toolUseConfirm, + onDone, + onReject, + workerBadge, + setStickyFooter +}: PermissionRequestProps): React.ReactNode { + const toolPermissionContext = useAppState(s => s.toolPermissionContext); + const setAppState = useSetAppState(); + const store = useAppStateStore(); + const { + addNotification + } = useNotifications(); + // Feedback text from the 'No' option's input. Threaded through onAllow as + // acceptFeedback when the user approves — lets users annotate the plan + // ("also update the README") without a reject+re-plan round-trip. + const [planFeedback, setPlanFeedback] = useState(''); + const [pastedContents, setPastedContents] = useState>({}); + const nextPasteIdRef = useRef(0); + const showClearContext = useAppState(s => s.settings.showClearContextOnPlanAccept) ?? false; + const ultraplanSessionUrl = useAppState(s => s.ultraplanSessionUrl); + const ultraplanLaunching = useAppState(s => s.ultraplanLaunching); + // Hide the Ultraplan button while a session is active or launching — + // selecting it would dismiss the dialog and reject locally before + // launchUltraplan can notice the session exists and return "already polling". + // feature() must sit directly in an if/ternary (bun:bundle DCE constraint). + const showUltraplan = feature('ULTRAPLAN') ? !ultraplanSessionUrl && !ultraplanLaunching : false; + const usage = toolUseConfirm.assistantMessage.message.usage; + const { + mode, + isAutoModeAvailable, + isBypassPermissionsModeAvailable + } = toolPermissionContext; + const options = useMemo(() => buildPlanApprovalOptions({ + showClearContext, + showUltraplan, + usedPercent: showClearContext ? getContextUsedPercent(usage, mode) : null, + isAutoModeAvailable, + isBypassPermissionsModeAvailable, + onFeedbackChange: setPlanFeedback + }), [showClearContext, showUltraplan, usage, mode, isAutoModeAvailable, isBypassPermissionsModeAvailable]); + function onImagePaste(base64Image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, _sourcePath?: string) { + const pasteId = nextPasteIdRef.current++; + const newContent: PastedContent = { + id: pasteId, + type: 'image', + content: base64Image, + mediaType: mediaType || 'image/png', + filename: filename || 'Pasted image', + dimensions + }; + cacheImagePath(newContent); + void storeImage(newContent); + setPastedContents(prev => ({ + ...prev, + [pasteId]: newContent + })); + } + const onRemoveImage = useCallback((id: number) => { + setPastedContents(prev => { + const next = { + ...prev + }; + delete next[id]; + return next; + }); + }, []); + const imageAttachments = Object.values(pastedContents).filter(c => c.type === 'image'); + const hasImages = imageAttachments.length > 0; + + // TODO: Delete the branch after moving to V2 + // Use tool name to detect V2 instead of checking input.plan, because PR #10394 + // injects plan content into input.plan for hooks/SDK, which broke the old detection + // (see issue #10878) + const isV2 = toolUseConfirm.tool.name === EXIT_PLAN_MODE_V2_TOOL_NAME; + const inputPlan = isV2 ? undefined : toolUseConfirm.input.plan as string | undefined; + const planFilePath = isV2 ? getPlanFilePath() : undefined; + + // Extract allowed prompts requested by the plan (Ant-only feature) + const allowedPrompts = toolUseConfirm.input.allowedPrompts as AllowedPrompt[] | undefined; + + // Get the raw plan to check if it's empty + const rawPlan = inputPlan ?? getPlan(); + const isEmpty = !rawPlan || rawPlan.trim() === ''; + + // Capture the variant once on mount. GrowthBook reads from a disk cache + // so the value is stable across a single planning session. undefined = + // control arm. The variant is a fixed 3-value enum of short literals, + // not user input. + const [planStructureVariant] = useState(() => (getPewterLedgerVariant() ?? undefined) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS); + const [currentPlan, setCurrentPlan] = useState(() => { + if (inputPlan) return inputPlan; + const plan = getPlan(); + return plan ?? 'No plan found. Please write your plan to the plan file first.'; + }); + const [showSaveMessage, setShowSaveMessage] = useState(false); + // Track Ctrl+G local edits so updatedInput can include the plan (the tool + // only echoes the plan in tool_result when input.plan is set — otherwise + // the model already has it in context from writing the plan file). + const [planEditedLocally, setPlanEditedLocally] = useState(false); + + // Auto-hide save message after 5 seconds + useEffect(() => { + if (showSaveMessage) { + const timer = setTimeout(setShowSaveMessage, 5000, false); + return () => clearTimeout(timer); + } + }, [showSaveMessage]); + + // Handle Ctrl+G to edit plan in $EDITOR, Shift+Tab for auto-accept edits + const handleKeyDown = (e: KeyboardEvent): void => { + if (e.ctrl && e.key === 'g') { + e.preventDefault(); + logEvent('tengu_plan_external_editor_used', {}); + void (async () => { + if (isV2 && planFilePath) { + const result = await editFileInEditor(planFilePath); + if (result.error) { + addNotification({ + key: 'external-editor-error', + text: result.error, + color: 'warning', + priority: 'high' + }); + } + if (result.content !== null) { + if (result.content !== currentPlan) setPlanEditedLocally(true); + setCurrentPlan(result.content); + setShowSaveMessage(true); + } + } else { + const result = await editPromptInEditor(currentPlan); + if (result.error) { + addNotification({ + key: 'external-editor-error', + text: result.error, + color: 'warning', + priority: 'high' + }); + } + if (result.content !== null && result.content !== currentPlan) { + setCurrentPlan(result.content); + setShowSaveMessage(true); + } + } + })(); + return; + } + + // Shift+Tab immediately selects "auto-accept edits" + if (e.shift && e.key === 'tab') { + e.preventDefault(); + void handleResponse(showClearContext ? 'yes-accept-edits' : 'yes-accept-edits-keep-context'); + return; + } + }; + async function handleResponse(value: ResponseValue): Promise { + const trimmedFeedback = planFeedback.trim(); + const acceptFeedback = trimmedFeedback || undefined; + + // Ultraplan: reject locally, teleport the plan to CCR as a seed draft. + // Dialog dismisses immediately so the query loop unblocks; the teleport + // runs detached and its launch message lands via the command queue. + if (value === 'ultraplan') { + logEvent('tengu_plan_exit', { + planLengthChars: currentPlan.length, + outcome: 'ultraplan' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), + planStructureVariant + }); + onDone(); + onReject(); + toolUseConfirm.onReject('Plan being refined via Ultraplan — please wait for the result.'); + void launchUltraplan({ + blurb: '', + seedPlan: currentPlan, + getAppState: store.getState, + setAppState: store.setState, + signal: new AbortController().signal + }).then(msg => enqueuePendingNotification({ + value: msg, + mode: 'task-notification' + })).catch(logError); + return; + } + + // V1: pass plan in input. V2: plan is on disk, but if the user edited it + // via Ctrl+G we pass it through so the tool echoes the edit in tool_result + // (otherwise the model never sees the user's changes). + const updatedInput = isV2 && !planEditedLocally ? {} : { + plan: currentPlan + }; + + // If auto was active during plan (from auto mode or opt-in) and NOT going + // to auto, deactivate auto + restore permissions + fire exit attachment. + if (feature('TRANSCRIPT_CLASSIFIER')) { + const goingToAuto = (value === 'yes-resume-auto-mode' || value === 'yes-auto-clear-context') && isAutoModeGateEnabled(); + // isAutoModeActive() is the authoritative signal — prePlanMode/ + // strippedDangerousRules are stale after transitionPlanAutoMode + // deactivates mid-plan (would cause duplicate exit attachment). + const autoWasUsedDuringPlan = autoModeStateModule?.isAutoModeActive() ?? false; + if (value !== 'no' && !goingToAuto && autoWasUsedDuringPlan) { + autoModeStateModule?.setAutoModeActive(false); + setNeedsAutoModeExitAttachment(true); + setAppState(prev => ({ + ...prev, + toolPermissionContext: { + ...restoreDangerousPermissions(prev.toolPermissionContext), + prePlanMode: undefined + } + })); + } + } + + // Clear-context options: set pending plan implementation and reject the dialog + // The REPL will handle context clear and trigger a fresh query + // Keep-context options skip this block and go through the normal flow below + const isResumeAutoOption = feature('TRANSCRIPT_CLASSIFIER') ? value === 'yes-resume-auto-mode' : false; + const isKeepContextOption = value === 'yes-accept-edits-keep-context' || value === 'yes-default-keep-context' || isResumeAutoOption; + if (value !== 'no') { + autoNameSessionFromPlan(currentPlan, setAppState, !isKeepContextOption); + } + if (value !== 'no' && !isKeepContextOption) { + // Determine the permission mode based on the selected option + let mode: PermissionMode = 'default'; + if (value === 'yes-bypass-permissions') { + mode = 'bypassPermissions'; + } else if (value === 'yes-accept-edits') { + mode = 'acceptEdits'; + } else if (feature('TRANSCRIPT_CLASSIFIER') && value === 'yes-auto-clear-context' && isAutoModeGateEnabled()) { + // REPL's processInitialMessage handles stripDangerousPermissions + mode, + // but does NOT set autoModeActive. Gate-off falls through to 'default'. + mode = 'auto'; + autoModeStateModule?.setAutoModeActive(true); + } + + // Log plan exit event + logEvent('tengu_plan_exit', { + planLengthChars: currentPlan.length, + outcome: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + clearContext: true, + interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), + planStructureVariant, + hasFeedback: !!acceptFeedback + }); + + // Set initial message - REPL will handle context clear and fresh query + // Add verification instruction if the feature is enabled + // Dead code elimination: CLAUDE_CODE_VERIFY_PLAN='false' in external builds, so === 'true' check allows Bun to eliminate the string + const verificationInstruction = undefined === 'true' ? `\n\nIMPORTANT: When you have finished implementing the plan, you MUST call the "VerifyPlanExecution" tool directly (NOT the ${AGENT_TOOL_NAME} tool or an agent) to trigger background verification.` : ''; + + // Capture the transcript path before context is cleared (session ID will be regenerated) + const transcriptPath = getTranscriptPath(); + const transcriptHint = `\n\nIf you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: ${transcriptPath}`; + const teamHint = isAgentSwarmsEnabled() ? `\n\nIf this plan can be broken down into multiple independent tasks, consider using the ${TEAM_CREATE_TOOL_NAME} tool to create a team and parallelize the work.` : ''; + const feedbackSuffix = acceptFeedback ? `\n\nUser feedback on this plan: ${acceptFeedback}` : ''; + setAppState(prev => ({ + ...prev, + initialMessage: { + message: { + ...createUserMessage({ + content: `Implement the following plan:\n\n${currentPlan}${verificationInstruction}${transcriptHint}${teamHint}${feedbackSuffix}` + }), + planContent: currentPlan + }, + clearContext: true, + mode, + allowedPrompts + } + })); + setHasExitedPlanMode(true); + onDone(); + onReject(); + // Reject the tool use to unblock the query loop + // The REPL will see pendingInitialQuery and trigger fresh query + toolUseConfirm.onReject(); + return; + } + + // Handle auto keep-context option — needs special handling because + // buildPermissionUpdates maps auto to 'default' via toExternalPermissionMode. + // We set the mode directly via setAppState and sync the bootstrap state. + if (feature('TRANSCRIPT_CLASSIFIER') && value === 'yes-resume-auto-mode' && isAutoModeGateEnabled()) { + logEvent('tengu_plan_exit', { + planLengthChars: currentPlan.length, + outcome: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + clearContext: false, + interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), + planStructureVariant, + hasFeedback: !!acceptFeedback + }); + setHasExitedPlanMode(true); + setNeedsPlanModeExitAttachment(true); + autoModeStateModule?.setAutoModeActive(true); + setAppState(prev => ({ + ...prev, + toolPermissionContext: stripDangerousPermissionsForAutoMode({ + ...prev.toolPermissionContext, + mode: 'auto', + prePlanMode: undefined + }) + })); + onDone(); + toolUseConfirm.onAllow(updatedInput, [], acceptFeedback); + return; + } + + // Handle keep-context options (goes through normal onAllow flow) + // yes-resume-auto-mode falls through here when the auto mode gate is + // disabled (e.g. circuit breaker fired after the dialog rendered). + // Without this fallback the function would return without resolving the + // dialog, leaving the query loop blocked and safety state corrupted. + const keepContextModes: Record = { + 'yes-accept-edits-keep-context': toolPermissionContext.isBypassPermissionsModeAvailable ? 'bypassPermissions' : 'acceptEdits', + 'yes-default-keep-context': 'default', + ...(feature('TRANSCRIPT_CLASSIFIER') ? { + 'yes-resume-auto-mode': 'default' as const + } : {}) + }; + const keepContextMode = keepContextModes[value]; + if (keepContextMode) { + logEvent('tengu_plan_exit', { + planLengthChars: currentPlan.length, + outcome: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + clearContext: false, + interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), + planStructureVariant, + hasFeedback: !!acceptFeedback + }); + setHasExitedPlanMode(true); + setNeedsPlanModeExitAttachment(true); + onDone(); + toolUseConfirm.onAllow(updatedInput, buildPermissionUpdates(keepContextMode, allowedPrompts), acceptFeedback); + return; + } + + // Handle standard approval options + const standardModes: Record = { + 'yes-bypass-permissions': 'bypassPermissions', + 'yes-accept-edits': 'acceptEdits' + }; + const standardMode = standardModes[value]; + if (standardMode) { + logEvent('tengu_plan_exit', { + planLengthChars: currentPlan.length, + outcome: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), + planStructureVariant, + hasFeedback: !!acceptFeedback + }); + setHasExitedPlanMode(true); + setNeedsPlanModeExitAttachment(true); + onDone(); + toolUseConfirm.onAllow(updatedInput, buildPermissionUpdates(standardMode, allowedPrompts), acceptFeedback); + return; + } + + // Handle 'no' - stay in plan mode + if (value === 'no') { + if (!trimmedFeedback && !hasImages) { + // No feedback yet - user is still on the input field + return; + } + logEvent('tengu_plan_exit', { + planLengthChars: currentPlan.length, + outcome: 'no' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), + planStructureVariant + }); + + // Convert pasted images to ImageBlockParam[] with resizing + let imageBlocks: ImageBlockParam[] | undefined; + if (hasImages) { + imageBlocks = await Promise.all(imageAttachments.map(async img => { + const block: ImageBlockParam = { + type: 'image', + source: { + type: 'base64', + media_type: (img.mediaType || 'image/png') as Base64ImageSource['media_type'], + data: img.content + } + }; + const resized = await maybeResizeAndDownsampleImageBlock(block); + return resized.block; + })); + } + onDone(); + onReject(); + toolUseConfirm.onReject(trimmedFeedback || (hasImages ? '(See attached image)' : undefined), imageBlocks && imageBlocks.length > 0 ? imageBlocks : undefined); + } + } + const editor = getExternalEditor(); + const editorName = editor ? toIDEDisplayName(editor) : null; + + // Sticky footer: when setStickyFooter is provided (fullscreen mode), the + // Select options render in FullscreenLayout's `bottom` slot so they stay + // visible while the user scrolls through a long plan. handleResponse is + // wrapped in a ref so the JSX (set once per options/images change) can call + // the latest closure without re-registering on every keystroke. React + // reconciles the sticky-footer Select by type, preserving focus/input state. + const handleResponseRef = useRef(handleResponse); + handleResponseRef.current = handleResponse; + const handleCancelRef = useRef<() => void>(undefined); + handleCancelRef.current = () => { + logEvent('tengu_plan_exit', { + planLengthChars: currentPlan.length, + outcome: 'no' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), + planStructureVariant + }); + onDone(); + onReject(); + toolUseConfirm.onReject(); + }; + const useStickyFooter = !isEmpty && !!setStickyFooter; + useLayoutEffect(() => { + if (!useStickyFooter) return; + setStickyFooter( + Would you like to proceed? + + { + logEvent('tengu_plan_exit', { + planLengthChars: 0, + outcome: 'no' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), + planStructureVariant + }); + onDone(); + onReject(); + toolUseConfirm.onReject(); + }} /> + + + ; + } + return + + + + Here is Claude's plan: + + + {currentPlan} + + + + {isClassifierPermissionsEnabled() && allowedPrompts && allowedPrompts.length > 0 && + Requested permissions: + {allowedPrompts.map((p, i) => + {' '}· {p.tool}({PROMPT_PREFIX} {p.prompt}) + )} + } + {!useStickyFooter && <> + + Claude has written up a plan and is ready to execute. Would + you like to proceed? + + + ; + $[32] = onCancel; + $[33] = t11; + $[34] = t13; + } else { + t13 = $[34]; + } + let t14; + if ($[35] !== ruleDescription || $[36] !== t13 || $[37] !== t9) { + t14 = {t9}{ruleDescription}{t10}{t13}; + $[35] = ruleDescription; + $[36] = t13; + $[37] = t9; + $[38] = t14; + } else { + t14 = $[38]; + } + let t15; + if ($[39] !== footer || $[40] !== t14) { + t15 = <>{t14}{footer}; + $[39] = footer; + $[40] = t14; + $[41] = t15; + } else { + t15 = $[41]; + } + return t15; +} +type RulesTabContentProps = { + options: Option[]; + searchQuery: string; + isSearchMode: boolean; + isFocused: boolean; + onSelect: (value: string) => void; + onCancel: () => void; + lastFocusedRuleKey: string | undefined; + cursorOffset?: number; + onHeaderFocusChange?: (focused: boolean) => void; +}; + +// Component for rendering rules tab content with full width support +function RulesTabContent(props) { + const $ = _c(26); + const { + options, + searchQuery, + isSearchMode, + isFocused, + onSelect, + onCancel, + lastFocusedRuleKey, + cursorOffset, + onHeaderFocusChange + } = props; + const tabWidth = useTabsWidth(); + const { + headerFocused, + focusHeader, + blurHeader + } = useTabHeaderFocus(); + let t0; + let t1; + if ($[0] !== blurHeader || $[1] !== headerFocused || $[2] !== isSearchMode) { + t0 = () => { + if (isSearchMode && headerFocused) { + blurHeader(); + } + }; + t1 = [isSearchMode, headerFocused, blurHeader]; + $[0] = blurHeader; + $[1] = headerFocused; + $[2] = isSearchMode; + $[3] = t0; + $[4] = t1; + } else { + t0 = $[3]; + t1 = $[4]; + } + useEffect(t0, t1); + let t2; + let t3; + if ($[5] !== headerFocused || $[6] !== onHeaderFocusChange) { + t2 = () => { + onHeaderFocusChange?.(headerFocused); + }; + t3 = [headerFocused, onHeaderFocusChange]; + $[5] = headerFocused; + $[6] = onHeaderFocusChange; + $[7] = t2; + $[8] = t3; + } else { + t2 = $[7]; + t3 = $[8]; + } + useEffect(t2, t3); + const t4 = isSearchMode && !headerFocused; + let t5; + if ($[9] !== cursorOffset || $[10] !== isFocused || $[11] !== searchQuery || $[12] !== t4 || $[13] !== tabWidth) { + t5 = ; + $[9] = cursorOffset; + $[10] = isFocused; + $[11] = searchQuery; + $[12] = t4; + $[13] = tabWidth; + $[14] = t5; + } else { + t5 = $[14]; + } + const t6 = Math.min(10, options.length); + const t7 = isSearchMode || headerFocused; + let t8; + if ($[15] !== focusHeader || $[16] !== lastFocusedRuleKey || $[17] !== onCancel || $[18] !== onSelect || $[19] !== options || $[20] !== t6 || $[21] !== t7) { + t8 = ; + $[25] = focusHeader; + $[26] = headerFocused; + $[27] = options; + $[28] = t12; + $[29] = t13; + } else { + t13 = $[29]; + } + return t13; +} +function _temp3() { + return new Set(); +} +function _temp2() { + return new Set(); +} +function _temp() { + return getAutoModeDenials(); +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZUNhbGxiYWNrIiwidXNlRWZmZWN0IiwidXNlU3RhdGUiLCJCb3giLCJUZXh0IiwidXNlSW5wdXQiLCJBdXRvTW9kZURlbmlhbCIsImdldEF1dG9Nb2RlRGVuaWFscyIsIlNlbGVjdCIsIlN0YXR1c0ljb24iLCJ1c2VUYWJIZWFkZXJGb2N1cyIsIlByb3BzIiwib25IZWFkZXJGb2N1c0NoYW5nZSIsImZvY3VzZWQiLCJvblN0YXRlQ2hhbmdlIiwic3RhdGUiLCJhcHByb3ZlZCIsIlNldCIsInJldHJ5IiwiZGVuaWFscyIsIlJlY2VudERlbmlhbHNUYWIiLCJ0MCIsIiQiLCJfYyIsImhlYWRlckZvY3VzZWQiLCJmb2N1c0hlYWRlciIsInQxIiwidDIiLCJfdGVtcCIsInNldEFwcHJvdmVkIiwiX3RlbXAyIiwic2V0UmV0cnkiLCJfdGVtcDMiLCJmb2N1c2VkSWR4Iiwic2V0Rm9jdXNlZElkeCIsInQzIiwidDQiLCJ0NSIsIlN5bWJvbCIsImZvciIsInZhbHVlIiwiaWR4IiwiTnVtYmVyIiwicHJldiIsIm5leHQiLCJoYXMiLCJkZWxldGUiLCJhZGQiLCJoYW5kbGVTZWxlY3QiLCJ0NiIsInZhbHVlXzAiLCJoYW5kbGVGb2N1cyIsInQ3IiwiaW5wdXQiLCJfa2V5IiwicHJldl8wIiwibmV4dF8wIiwicHJldl8xIiwibmV4dF8xIiwidDgiLCJsZW5ndGgiLCJ0OSIsImlzQWN0aXZlIiwidDEwIiwidDExIiwiZCIsImlkeF8wIiwiaXNBcHByb3ZlZCIsInN1ZmZpeCIsImxhYmVsIiwiZGlzcGxheSIsIlN0cmluZyIsIm1hcCIsIm9wdGlvbnMiLCJ0MTIiLCJNYXRoIiwibWluIiwidDEzIl0sInNvdXJjZXMiOlsiUmVjZW50RGVuaWFsc1RhYi50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VDYWxsYmFjaywgdXNlRWZmZWN0LCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGN1c3RvbS1ydWxlcy9wcmVmZXItdXNlLWtleWJpbmRpbmdzIC0tICdyJyBpcyBhIHZpZXctc3BlY2lmaWMga2V5LCBub3QgYSBnbG9iYWwga2V5YmluZGluZ1xuaW1wb3J0IHsgQm94LCBUZXh0LCB1c2VJbnB1dCB9IGZyb20gJy4uLy4uLy4uL2luay5qcydcbmltcG9ydCB7XG4gIHR5cGUgQXV0b01vZGVEZW5pYWwsXG4gIGdldEF1dG9Nb2RlRGVuaWFscyxcbn0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvYXV0b01vZGVEZW5pYWxzLmpzJ1xuaW1wb3J0IHsgU2VsZWN0IH0gZnJvbSAnLi4vLi4vQ3VzdG9tU2VsZWN0L3NlbGVjdC5qcydcbmltcG9ydCB7IFN0YXR1c0ljb24gfSBmcm9tICcuLi8uLi9kZXNpZ24tc3lzdGVtL1N0YXR1c0ljb24uanMnXG5pbXBvcnQgeyB1c2VUYWJIZWFkZXJGb2N1cyB9IGZyb20gJy4uLy4uL2Rlc2lnbi1zeXN0ZW0vVGFicy5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgb25IZWFkZXJGb2N1c0NoYW5nZT86IChmb2N1c2VkOiBib29sZWFuKSA9PiB2b2lkXG4gIC8qKiBDYWxsZWQgd2hlbiBhcHByb3ZlZC9yZXRyeSBzdGF0ZSBjaGFuZ2VzIHNvIHBhcmVudCBjYW4gYWN0IG9uIGV4aXQgKi9cbiAgb25TdGF0ZUNoYW5nZTogKHN0YXRlOiB7XG4gICAgYXBwcm92ZWQ6IFNldDxudW1iZXI+XG4gICAgcmV0cnk6IFNldDxudW1iZXI+XG4gICAgZGVuaWFsczogcmVhZG9ubHkgQXV0b01vZGVEZW5pYWxbXVxuICB9KSA9PiB2b2lkXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBSZWNlbnREZW5pYWxzVGFiKHtcbiAgb25IZWFkZXJGb2N1c0NoYW5nZSxcbiAgb25TdGF0ZUNoYW5nZSxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgeyBoZWFkZXJGb2N1c2VkLCBmb2N1c0hlYWRlciB9ID0gdXNlVGFiSGVhZGVyRm9jdXMoKVxuICB1c2VFZmZlY3QoKCkgPT4ge1xuICAgIG9uSGVhZGVyRm9jdXNDaGFuZ2U/LihoZWFkZXJGb2N1c2VkKVxuICB9LCBbaGVhZGVyRm9jdXNlZCwgb25IZWFkZXJGb2N1c0NoYW5nZV0pXG5cbiAgLy8gU25hcHNob3Qgb24gbW91bnQg4oCUIGFwcHJvdmVkL3JldHJ5IFNldHMga2V5IGJ5IGluZGV4LCBhbmQgdGhlIGxpdmUgc3RvcmVcbiAgLy8gcHJlcGVuZHMuIEEgY29uY3VycmVudCBkZW5pYWwgd291bGQgc2hpZnQgYWxsIGluZGljZXMgbWlkLWVkaXQuXG4gIGNvbnN0IFtkZW5pYWxzXSA9IHVzZVN0YXRlKCgpID0+IGdldEF1dG9Nb2RlRGVuaWFscygpKVxuXG4gIGNvbnN0IFthcHByb3ZlZCwgc2V0QXBwcm92ZWRdID0gdXNlU3RhdGU8U2V0PG51bWJlcj4+KCgpID0+IG5ldyBTZXQoKSlcbiAgY29uc3QgW3JldHJ5LCBzZXRSZXRyeV0gPSB1c2VTdGF0ZTxTZXQ8bnVtYmVyPj4oKCkgPT4gbmV3IFNldCgpKVxuICBjb25zdCBbZm9jdXNlZElkeCwgc2V0Rm9jdXNlZElkeF0gPSB1c2VTdGF0ZSgwKVxuXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgb25TdGF0ZUNoYW5nZSh7IGFwcHJvdmVkLCByZXRyeSwgZGVuaWFscyB9KVxuICB9LCBbYXBwcm92ZWQsIHJldHJ5LCBkZW5pYWxzLCBvblN0YXRlQ2hhbmdlXSlcblxuICBjb25zdCBoYW5kbGVTZWxlY3QgPSB1c2VDYWxsYmFjaygodmFsdWU6IHN0cmluZykgPT4ge1xuICAgIGNvbnN0IGlkeCA9IE51bWJlcih2YWx1ZSlcbiAgICBzZXRBcHByb3ZlZChwcmV2ID0+IHtcbiAgICAgIGNvbnN0IG5leHQgPSBuZXcgU2V0KHByZXYpXG4gICAgICBpZiAobmV4dC5oYXMoaWR4KSkgbmV4dC5kZWxldGUoaWR4KVxuICAgICAgZWxzZSBuZXh0LmFkZChpZHgpXG4gICAgICByZXR1cm4gbmV4dFxuICAgIH0pXG4gIH0sIFtdKVxuXG4gIGNvbnN0IGhhbmRsZUZvY3VzID0gdXNlQ2FsbGJhY2soKHZhbHVlOiBzdHJpbmcpID0+IHtcbiAgICBzZXRGb2N1c2VkSWR4KE51bWJlcih2YWx1ZSkpXG4gIH0sIFtdKVxuXG4gIHVzZUlucHV0KFxuICAgIChpbnB1dCwgX2tleSkgPT4ge1xuICAgICAgaWYgKGlucHV0ID09PSAncicpIHtcbiAgICAgICAgc2V0UmV0cnkocHJldiA9PiB7XG4gICAgICAgICAgY29uc3QgbmV4dCA9IG5ldyBTZXQocHJldilcbiAgICAgICAgICBpZiAobmV4dC5oYXMoZm9jdXNlZElkeCkpIG5leHQuZGVsZXRlKGZvY3VzZWRJZHgpXG4gICAgICAgICAgZWxzZSBuZXh0LmFkZChmb2N1c2VkSWR4KVxuICAgICAgICAgIHJldHVybiBuZXh0XG4gICAgICAgIH0pXG4gICAgICAgIC8vIFJldHJ5IGltcGxpZXMgYXBwcm92ZVxuICAgICAgICBzZXRBcHByb3ZlZChwcmV2ID0+IHtcbiAgICAgICAgICBpZiAocHJldi5oYXMoZm9jdXNlZElkeCkpIHJldHVybiBwcmV2XG4gICAgICAgICAgY29uc3QgbmV4dCA9IG5ldyBTZXQocHJldilcbiAgICAgICAgICBuZXh0LmFkZChmb2N1c2VkSWR4KVxuICAgICAgICAgIHJldHVybiBuZXh0XG4gICAgICAgIH0pXG4gICAgICB9XG4gICAgfSxcbiAgICB7IGlzQWN0aXZlOiBkZW5pYWxzLmxlbmd0aCA+IDAgfSxcbiAgKVxuXG4gIGlmIChkZW5pYWxzLmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybiAoXG4gICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgTm8gcmVjZW50IGRlbmlhbHMuIENvbW1hbmRzIGRlbmllZCBieSB0aGUgYXV0byBtb2RlIGNsYXNzaWZpZXIgd2lsbFxuICAgICAgICBhcHBlYXIgaGVyZS5cbiAgICAgIDwvVGV4dD5cbiAgICApXG4gIH1cblxuICBjb25zdCBvcHRpb25zID0gZGVuaWFscy5tYXAoKGQsIGlkeCkgPT4ge1xuICAgIGNvbnN0IGlzQXBwcm92ZWQgPSBhcHByb3ZlZC5oYXMoaWR4KVxuICAgIGNvbnN0IHN1ZmZpeCA9IHJldHJ5LmhhcyhpZHgpID8gJyAocmV0cnkpJyA6ICcnXG4gICAgcmV0dXJuIHtcbiAgICAgIGxhYmVsOiAoXG4gICAgICAgIDxUZXh0PlxuICAgICAgICAgIDxTdGF0dXNJY29uIHN0YXR1cz17aXNBcHByb3ZlZCA/ICdzdWNjZXNzJyA6ICdlcnJvcid9IHdpdGhTcGFjZSAvPlxuICAgICAgICAgIHtkLmRpc3BsYXl9XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+e3N1ZmZpeH08L1RleHQ+XG4gICAgICAgIDwvVGV4dD5cbiAgICAgICksXG4gICAgICB2YWx1ZTogU3RyaW5nKGlkeCksXG4gICAgfVxuICB9KVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICA8VGV4dD5Db21tYW5kcyByZWNlbnRseSBkZW5pZWQgYnkgdGhlIGF1dG8gbW9kZSBjbGFzc2lmaWVyLjwvVGV4dD5cbiAgICAgIDxCb3ggbWFyZ2luVG9wPXsxfT5cbiAgICAgICAgPFNlbGVjdFxuICAgICAgICAgIG9wdGlvbnM9e29wdGlvbnN9XG4gICAgICAgICAgb25DaGFuZ2U9e2hhbmRsZVNlbGVjdH1cbiAgICAgICAgICBvbkZvY3VzPXtoYW5kbGVGb2N1c31cbiAgICAgICAgICB2aXNpYmxlT3B0aW9uQ291bnQ9e01hdGgubWluKDEwLCBvcHRpb25zLmxlbmd0aCl9XG4gICAgICAgICAgaXNEaXNhYmxlZD17aGVhZGVyRm9jdXNlZH1cbiAgICAgICAgICBvblVwRnJvbUZpcnN0SXRlbT17Zm9jdXNIZWFkZXJ9XG4gICAgICAgIC8+XG4gICAgICA8L0JveD5cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxXQUFXLEVBQUVDLFNBQVMsRUFBRUMsUUFBUSxRQUFRLE9BQU87QUFD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 \ No newline at end of file diff --git a/src/components/permissions/shellPermissionHelpers.tsx b/src/components/permissions/shellPermissionHelpers.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e4408a605c1413f74846d296a63ff65b1d38ef6f --- /dev/null +++ b/src/components/permissions/shellPermissionHelpers.tsx @@ -0,0 +1,164 @@ +import { basename, sep } from 'path'; +import React, { type ReactNode } from 'react'; +import { getOriginalCwd } from '../../bootstrap/state.js'; +import { Text } from '../../ink.js'; +import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'; +import { permissionRuleExtractPrefix } from '../../utils/permissions/shellRuleMatching.js'; +function commandListDisplay(commands: string[]): ReactNode { + switch (commands.length) { + case 0: + return ''; + case 1: + return {commands[0]}; + case 2: + return + {commands[0]} and {commands[1]} + ; + default: + return + {commands.slice(0, -1).join(', ')}, and{' '} + {commands.slice(-1)[0]} + ; + } +} +function commandListDisplayTruncated(commands: string[]): ReactNode { + // Check if the plain text representation would be too long + const plainText = commands.join(', '); + if (plainText.length > 50) { + return 'similar'; + } + return commandListDisplay(commands); +} +function formatPathList(paths: string[]): ReactNode { + if (paths.length === 0) return ''; + + // Extract directory names from paths + const names = paths.map(p => basename(p) || p); + if (names.length === 1) { + return + {names[0]} + {sep} + ; + } + if (names.length === 2) { + return + {names[0]} + {sep} and {names[1]} + {sep} + ; + } + + // For 3+, show first two with "and N more" + return + {names[0]} + {sep}, {names[1]} + {sep} and {paths.length - 2} more + ; +} + +/** + * Generate the label for the "Yes, and apply suggestions" option in shell + * permission dialogs (Bash, PowerShell). Parametrized by the shell tool name + * and an optional command transform (e.g., Bash strips output redirections so + * filenames don't show as commands). + */ +export function generateShellSuggestionsLabel(suggestions: PermissionUpdate[], shellToolName: string, commandTransform?: (command: string) => string): ReactNode | null { + // Collect all rules for display + const allRules = suggestions.filter(s => s.type === 'addRules').flatMap(s => s.rules || []); + + // Separate Read rules from shell rules + const readRules = allRules.filter(r => r.toolName === 'Read'); + const shellRules = allRules.filter(r => r.toolName === shellToolName); + + // Get directory info + const directories = suggestions.filter(s => s.type === 'addDirectories').flatMap(s => s.directories || []); + + // Extract paths from Read rules (keep separate from directories) + const readPaths = readRules.map(r => r.ruleContent?.replace('/**', '') || '').filter(p => p); + + // Extract shell command prefixes, optionally transforming for display + const shellCommands = [...new Set(shellRules.flatMap(rule => { + if (!rule.ruleContent) return []; + const command = permissionRuleExtractPrefix(rule.ruleContent) ?? rule.ruleContent; + return commandTransform ? commandTransform(command) : command; + }))]; + + // Check what we have + const hasDirectories = directories.length > 0; + const hasReadPaths = readPaths.length > 0; + const hasCommands = shellCommands.length > 0; + + // Handle single type cases + if (hasReadPaths && !hasDirectories && !hasCommands) { + // Only Read rules - use "reading from" language + if (readPaths.length === 1) { + const firstPath = readPaths[0]!; + const dirName = basename(firstPath) || firstPath; + return + Yes, allow reading from {dirName} + {sep} from this project + ; + } + + // Multiple read paths + return + Yes, allow reading from {formatPathList(readPaths)} from this project + ; + } + if (hasDirectories && !hasReadPaths && !hasCommands) { + // Only directory permissions - use "access to" language + if (directories.length === 1) { + const firstDir = directories[0]!; + const dirName = basename(firstDir) || firstDir; + return + Yes, and always allow access to {dirName} + {sep} from this project + ; + } + + // Multiple directories + return + Yes, and always allow access to {formatPathList(directories)} from this + project + ; + } + if (hasCommands && !hasDirectories && !hasReadPaths) { + // Only shell command permissions + return + {"Yes, and don't ask again for "} + {commandListDisplayTruncated(shellCommands)} commands in{' '} + {getOriginalCwd()} + ; + } + + // Handle mixed cases + if ((hasDirectories || hasReadPaths) && !hasCommands) { + // Combine directories and read paths since they're both path access + const allPaths = [...directories, ...readPaths]; + if (hasDirectories && hasReadPaths) { + // Mixed - use generic "access to" + return + Yes, and always allow access to {formatPathList(allPaths)} from this + project + ; + } + } + if ((hasDirectories || hasReadPaths) && hasCommands) { + // Build descriptive message for both types + const allPaths = [...directories, ...readPaths]; + + // Keep it concise but informative + if (allPaths.length === 1 && shellCommands.length === 1) { + return + Yes, and allow access to {formatPathList(allPaths)} and{' '} + {commandListDisplayTruncated(shellCommands)} commands + ; + } + return + Yes, and allow {formatPathList(allPaths)} access and{' '} + {commandListDisplayTruncated(shellCommands)} commands + ; + } + return null; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJiYXNlbmFtZSIsInNlcCIsIlJlYWN0IiwiUmVhY3ROb2RlIiwiZ2V0T3JpZ2luYWxDd2QiLCJUZXh0IiwiUGVybWlzc2lvblVwZGF0ZSIsInBlcm1pc3Npb25SdWxlRXh0cmFjdFByZWZpeCIsImNvbW1hbmRMaXN0RGlzcGxheSIsImNvbW1hbmRzIiwibGVuZ3RoIiwic2xpY2UiLCJqb2luIiwiY29tbWFuZExpc3REaXNwbGF5VHJ1bmNhdGVkIiwicGxhaW5UZXh0IiwiZm9ybWF0UGF0aExpc3QiLCJwYXRocyIsIm5hbWVzIiwibWFwIiwicCIsImdlbmVyYXRlU2hlbGxTdWdnZXN0aW9uc0xhYmVsIiwic3VnZ2VzdGlvbnMiLCJzaGVsbFRvb2xOYW1lIiwiY29tbWFuZFRyYW5zZm9ybSIsImNvbW1hbmQiLCJhbGxSdWxlcyIsImZpbHRlciIsInMiLCJ0eXBlIiwiZmxhdE1hcCIsInJ1bGVzIiwicmVhZFJ1bGVzIiwiciIsInRvb2xOYW1lIiwic2hlbGxSdWxlcyIsImRpcmVjdG9yaWVzIiwicmVhZFBhdGhzIiwicnVsZUNvbnRlbnQiLCJyZXBsYWNlIiwic2hlbGxDb21tYW5kcyIsIlNldCIsInJ1bGUiLCJoYXNEaXJlY3RvcmllcyIsImhhc1JlYWRQYXRocyIsImhhc0NvbW1hbmRzIiwiZmlyc3RQYXRoIiwiZGlyTmFtZSIsImZpcnN0RGlyIiwiYWxsUGF0aHMiXSwic291cmNlcyI6WyJzaGVsbFBlcm1pc3Npb25IZWxwZXJzLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBiYXNlbmFtZSwgc2VwIH0gZnJvbSAncGF0aCdcbmltcG9ydCBSZWFjdCwgeyB0eXBlIFJlYWN0Tm9kZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgZ2V0T3JpZ2luYWxDd2QgfSBmcm9tICcuLi8uLi9ib290c3RyYXAvc3RhdGUuanMnXG5pbXBvcnQgeyBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBQZXJtaXNzaW9uVXBkYXRlIH0gZnJvbSAnLi4vLi4vdXRpbHMvcGVybWlzc2lvbnMvUGVybWlzc2lvblVwZGF0ZVNjaGVtYS5qcydcbmltcG9ydCB7IHBlcm1pc3Npb25SdWxlRXh0cmFjdFByZWZpeCB9IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL3NoZWxsUnVsZU1hdGNoaW5nLmpzJ1xuXG5mdW5jdGlvbiBjb21tYW5kTGlzdERpc3BsYXkoY29tbWFuZHM6IHN0cmluZ1tdKTogUmVhY3ROb2RlIHtcbiAgc3dpdGNoIChjb21tYW5kcy5sZW5ndGgpIHtcbiAgICBjYXNlIDA6XG4gICAgICByZXR1cm4gJydcbiAgICBjYXNlIDE6XG4gICAgICByZXR1cm4gPFRleHQgYm9sZD57Y29tbWFuZHNbMF19PC9UZXh0PlxuICAgIGNhc2UgMjpcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxUZXh0PlxuICAgICAgICAgIDxUZXh0IGJvbGQ+e2NvbW1hbmRzWzBdfTwvVGV4dD4gYW5kIDxUZXh0IGJvbGQ+e2NvbW1hbmRzWzFdfTwvVGV4dD5cbiAgICAgICAgPC9UZXh0PlxuICAgICAgKVxuICAgIGRlZmF1bHQ6XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8VGV4dD5cbiAgICAgICAgICA8VGV4dCBib2xkPntjb21tYW5kcy5zbGljZSgwLCAtMSkuam9pbignLCAnKX08L1RleHQ+LCBhbmR7JyAnfVxuICAgICAgICAgIDxUZXh0IGJvbGQ+e2NvbW1hbmRzLnNsaWNlKC0xKVswXX08L1RleHQ+XG4gICAgICAgIDwvVGV4dD5cbiAgICAgIClcbiAgfVxufVxuXG5mdW5jdGlvbiBjb21tYW5kTGlzdERpc3BsYXlUcnVuY2F0ZWQoY29tbWFuZHM6IHN0cmluZ1tdKTogUmVhY3ROb2RlIHtcbiAgLy8gQ2hlY2sgaWYgdGhlIHBsYWluIHRleHQgcmVwcmVzZW50YXRpb24gd291bGQgYmUgdG9vIGxvbmdcbiAgY29uc3QgcGxhaW5UZXh0ID0gY29tbWFuZHMuam9pbignLCAnKVxuICBpZiAocGxhaW5UZXh0Lmxlbmd0aCA+IDUwKSB7XG4gICAgcmV0dXJuICdzaW1pbGFyJ1xuICB9XG4gIHJldHVybiBjb21tYW5kTGlzdERpc3BsYXkoY29tbWFuZHMpXG59XG5cbmZ1bmN0aW9uIGZvcm1hdFBhdGhMaXN0KHBhdGhzOiBzdHJpbmdbXSk6IFJlYWN0Tm9kZSB7XG4gIGlmIChwYXRocy5sZW5ndGggPT09IDApIHJldHVybiAnJ1xuXG4gIC8vIEV4dHJhY3QgZGlyZWN0b3J5IG5hbWVzIGZyb20gcGF0aHNcbiAgY29uc3QgbmFtZXMgPSBwYXRocy5tYXAocCA9PiBiYXNlbmFtZShwKSB8fCBwKVxuXG4gIGlmIChuYW1lcy5sZW5ndGggPT09IDEpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPFRleHQ+XG4gICAgICAgIDxUZXh0IGJvbGQ+e25hbWVzWzBdfTwvVGV4dD5cbiAgICAgICAge3NlcH1cbiAgICAgIDwvVGV4dD5cbiAgICApXG4gIH1cbiAgaWYgKG5hbWVzLmxlbmd0aCA9PT0gMikge1xuICAgIHJldHVybiAoXG4gICAgICA8VGV4dD5cbiAgICAgICAgPFRleHQgYm9sZD57bmFtZXNbMF19PC9UZXh0PlxuICAgICAgICB7c2VwfSBhbmQgPFRleHQgYm9sZD57bmFtZXNbMV19PC9UZXh0PlxuICAgICAgICB7c2VwfVxuICAgICAgPC9UZXh0PlxuICAgIClcbiAgfVxuXG4gIC8vIEZvciAzKywgc2hvdyBmaXJzdCB0d28gd2l0aCBcImFuZCBOIG1vcmVcIlxuICByZXR1cm4gKFxuICAgIDxUZXh0PlxuICAgICAgPFRleHQgYm9sZD57bmFtZXNbMF19PC9UZXh0PlxuICAgICAge3NlcH0sIDxUZXh0IGJvbGQ+e25hbWVzWzFdfTwvVGV4dD5cbiAgICAgIHtzZXB9IGFuZCB7cGF0aHMubGVuZ3RoIC0gMn0gbW9yZVxuICAgIDwvVGV4dD5cbiAgKVxufVxuXG4vKipcbiAqIEdlbmVyYXRlIHRoZSBsYWJlbCBmb3IgdGhlIFwiWWVzLCBhbmQgYXBwbHkgc3VnZ2VzdGlvbnNcIiBvcHRpb24gaW4gc2hlbGxcbiAqIHBlcm1pc3Npb24gZGlhbG9ncyAoQmFzaCwgUG93ZXJTaGVsbCkuIFBhcmFtZXRyaXplZCBieSB0aGUgc2hlbGwgdG9vbCBuYW1lXG4gKiBhbmQgYW4gb3B0aW9uYWwgY29tbWFuZCB0cmFuc2Zvcm0gKGUuZy4sIEJhc2ggc3RyaXBzIG91dHB1dCByZWRpcmVjdGlvbnMgc29cbiAqIGZpbGVuYW1lcyBkb24ndCBzaG93IGFzIGNvbW1hbmRzKS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGdlbmVyYXRlU2hlbGxTdWdnZXN0aW9uc0xhYmVsKFxuICBzdWdnZXN0aW9uczogUGVybWlzc2lvblVwZGF0ZVtdLFxuICBzaGVsbFRvb2xOYW1lOiBzdHJpbmcsXG4gIGNvbW1hbmRUcmFuc2Zvcm0/OiAoY29tbWFuZDogc3RyaW5nKSA9PiBzdHJpbmcsXG4pOiBSZWFjdE5vZGUgfCBudWxsIHtcbiAgLy8gQ29sbGVjdCBhbGwgcnVsZXMgZm9yIGRpc3BsYXlcbiAgY29uc3QgYWxsUnVsZXMgPSBzdWdnZXN0aW9uc1xuICAgIC5maWx0ZXIocyA9PiBzLnR5cGUgPT09ICdhZGRSdWxlcycpXG4gICAgLmZsYXRNYXAocyA9PiBzLnJ1bGVzIHx8IFtdKVxuXG4gIC8vIFNlcGFyYXRlIFJlYWQgcnVsZXMgZnJvbSBzaGVsbCBydWxlc1xuICBjb25zdCByZWFkUnVsZXMgPSBhbGxSdWxlcy5maWx0ZXIociA9PiByLnRvb2xOYW1lID09PSAnUmVhZCcpXG4gIGNvbnN0IHNoZWxsUnVsZXMgPSBhbGxSdWxlcy5maWx0ZXIociA9PiByLnRvb2xOYW1lID09PSBzaGVsbFRvb2xOYW1lKVxuXG4gIC8vIEdldCBkaXJlY3RvcnkgaW5mb1xuICBjb25zdCBkaXJlY3RvcmllcyA9IHN1Z2dlc3Rpb25zXG4gICAgLmZpbHRlcihzID0+IHMudHlwZSA9PT0gJ2FkZERpcmVjdG9yaWVzJylcbiAgICAuZmxhdE1hcChzID0+IHMuZGlyZWN0b3JpZXMgfHwgW10pXG5cbiAgLy8gRXh0cmFjdCBwYXRocyBmcm9tIFJlYWQgcnVsZXMgKGtlZXAgc2VwYXJhdGUgZnJvbSBkaXJlY3RvcmllcylcbiAgY29uc3QgcmVhZFBhdGhzID0gcmVhZFJ1bGVzXG4gICAgLm1hcChyID0+IHIucnVsZUNvbnRlbnQ/LnJlcGxhY2UoJy8qKicsICcnKSB8fCAnJylcbiAgICAuZmlsdGVyKHAgPT4gcClcblxuICAvLyBFeHRyYWN0IHNoZWxsIGNvbW1hbmQgcHJlZml4ZXMsIG9wdGlvbmFsbHkgdHJhbnNmb3JtaW5nIGZvciBkaXNwbGF5XG4gIGNvbnN0IHNoZWxsQ29tbWFuZHMgPSBbXG4gICAgLi4ubmV3IFNldChcbiAgICAgIHNoZWxsUnVsZXMuZmxhdE1hcChydWxlID0+IHtcbiAgICAgICAgaWYgKCFydWxlLnJ1bGVDb250ZW50KSByZXR1cm4gW11cbiAgICAgICAgY29uc3QgY29tbWFuZCA9XG4gICAgICAgICAgcGVybWlzc2lvblJ1bGVFeHRyYWN0UHJlZml4KHJ1bGUucnVsZUNvbnRlbnQpID8/IHJ1bGUucnVsZUNvbnRlbnRcbiAgICAgICAgcmV0dXJuIGNvbW1hbmRUcmFuc2Zvcm0gPyBjb21tYW5kVHJhbnNmb3JtKGNvbW1hbmQpIDogY29tbWFuZFxuICAgICAgfSksXG4gICAgKSxcbiAgXVxuXG4gIC8vIENoZWNrIHdoYXQgd2UgaGF2ZVxuICBjb25zdCBoYXNEaXJlY3RvcmllcyA9IGRpcmVjdG9yaWVzLmxlbmd0aCA+IDBcbiAgY29uc3QgaGFzUmVhZFBhdGhzID0gcmVhZFBhdGhzLmxlbmd0aCA+IDBcbiAgY29uc3QgaGFzQ29tbWFuZHMgPSBzaGVsbENvbW1hbmRzLmxlbmd0aCA+IDBcblxuICAvLyBIYW5kbGUgc2luZ2xlIHR5cGUgY2FzZXNcbiAgaWYgKGhhc1JlYWRQYXRocyAmJiAhaGFzRGlyZWN0b3JpZXMgJiYgIWhhc0NvbW1hbmRzKSB7XG4gICAgLy8gT25seSBSZWFkIHJ1bGVzIC0gdXNlIFwicmVhZGluZyBmcm9tXCIgbGFuZ3VhZ2VcbiAgICBpZiAocmVhZFBhdGhzLmxlbmd0aCA9PT0gMSkge1xuICAgICAgY29uc3QgZmlyc3RQYXRoID0gcmVhZFBhdGhzWzBdIVxuICAgICAgY29uc3QgZGlyTmFtZSA9IGJhc2VuYW1lKGZpcnN0UGF0aCkgfHwgZmlyc3RQYXRoXG4gICAgICByZXR1cm4gKFxuICAgICAgICA8VGV4dD5cbiAgICAgICAgICBZZXMsIGFsbG93IHJlYWRpbmcgZnJvbSA8VGV4dCBib2xkPntkaXJOYW1lfTwvVGV4dD5cbiAgICAgICAgICB7c2VwfSBmcm9tIHRoaXMgcHJvamVjdFxuICAgICAgICA8L1RleHQ+XG4gICAgICApXG4gICAgfVxuXG4gICAgLy8gTXVsdGlwbGUgcmVhZCBwYXRoc1xuICAgIHJldHVybiAoXG4gICAgICA8VGV4dD5cbiAgICAgICAgWWVzLCBhbGxvdyByZWFkaW5nIGZyb20ge2Zvcm1hdFBhdGhMaXN0KHJlYWRQYXRocyl9IGZyb20gdGhpcyBwcm9qZWN0XG4gICAgICA8L1RleHQ+XG4gICAgKVxuICB9XG5cbiAgaWYgKGhhc0RpcmVjdG9yaWVzICYmICFoYXNSZWFkUGF0aHMgJiYgIWhhc0NvbW1hbmRzKSB7XG4gICAgLy8gT25seSBkaXJlY3RvcnkgcGVybWlzc2lvbnMgLSB1c2UgXCJhY2Nlc3MgdG9cIiBsYW5ndWFnZVxuICAgIGlmIChkaXJlY3Rvcmllcy5sZW5ndGggPT09IDEpIHtcbiAgICAgIGNvbnN0IGZpcnN0RGlyID0gZGlyZWN0b3JpZXNbMF0hXG4gICAgICBjb25zdCBkaXJOYW1lID0gYmFzZW5hbWUoZmlyc3REaXIpIHx8IGZpcnN0RGlyXG4gICAgICByZXR1cm4gKFxuICAgICAgICA8VGV4dD5cbiAgICAgICAgICBZZXMsIGFuZCBhbHdheXMgYWxsb3cgYWNjZXNzIHRvIDxUZXh0IGJvbGQ+e2Rpck5hbWV9PC9UZXh0PlxuICAgICAgICAgIHtzZXB9IGZyb20gdGhpcyBwcm9qZWN0XG4gICAgICAgIDwvVGV4dD5cbiAgICAgIClcbiAgICB9XG5cbiAgICAvLyBNdWx0aXBsZSBkaXJlY3Rvcmllc1xuICAgIHJldHVybiAoXG4gICAgICA8VGV4dD5cbiAgICAgICAgWWVzLCBhbmQgYWx3YXlzIGFsbG93IGFjY2VzcyB0byB7Zm9ybWF0UGF0aExpc3QoZGlyZWN0b3JpZXMpfSBmcm9tIHRoaXNcbiAgICAgICAgcHJvamVjdFxuICAgICAgPC9UZXh0PlxuICAgIClcbiAgfVxuXG4gIGlmIChoYXNDb21tYW5kcyAmJiAhaGFzRGlyZWN0b3JpZXMgJiYgIWhhc1JlYWRQYXRocykge1xuICAgIC8vIE9ubHkgc2hlbGwgY29tbWFuZCBwZXJtaXNzaW9uc1xuICAgIHJldHVybiAoXG4gICAgICA8VGV4dD5cbiAgICAgICAge1wiWWVzLCBhbmQgZG9uJ3QgYXNrIGFnYWluIGZvciBcIn1cbiAgICAgICAge2NvbW1hbmRMaXN0RGlzcGxheVRydW5jYXRlZChzaGVsbENvbW1hbmRzKX0gY29tbWFuZHMgaW57JyAnfVxuICAgICAgICA8VGV4dCBib2xkPntnZXRPcmlnaW5hbEN3ZCgpfTwvVGV4dD5cbiAgICAgIDwvVGV4dD5cbiAgICApXG4gIH1cblxuICAvLyBIYW5kbGUgbWl4ZWQgY2FzZXNcbiAgaWYgKChoYXNEaXJlY3RvcmllcyB8fCBoYXNSZWFkUGF0aHMpICYmICFoYXNDb21tYW5kcykge1xuICAgIC8vIENvbWJpbmUgZGlyZWN0b3JpZXMgYW5kIHJlYWQgcGF0aHMgc2luY2UgdGhleSdyZSBib3RoIHBhdGggYWNjZXNzXG4gICAgY29uc3QgYWxsUGF0aHMgPSBbLi4uZGlyZWN0b3JpZXMsIC4uLnJlYWRQYXRoc11cbiAgICBpZiAoaGFzRGlyZWN0b3JpZXMgJiYgaGFzUmVhZFBhdGhzKSB7XG4gICAgICAvLyBNaXhlZCAtIHVzZSBnZW5lcmljIFwiYWNjZXNzIHRvXCJcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxUZXh0PlxuICAgICAgICAgIFllcywgYW5kIGFsd2F5cyBhbGxvdyBhY2Nlc3MgdG8ge2Zvcm1hdFBhdGhMaXN0KGFsbFBhdGhzKX0gZnJvbSB0aGlzXG4gICAgICAgICAgcHJvamVjdFxuICAgICAgICA8L1RleHQ+XG4gICAgICApXG4gICAgfVxuICB9XG5cbiAgaWYgKChoYXNEaXJlY3RvcmllcyB8fCBoYXNSZWFkUGF0aHMpICYmIGhhc0NvbW1hbmRzKSB7XG4gICAgLy8gQnVpbGQgZGVzY3JpcHRpdmUgbWVzc2FnZSBmb3IgYm90aCB0eXBlc1xuICAgIGNvbnN0IGFsbFBhdGhzID0gWy4uLmRpcmVjdG9yaWVzLCAuLi5yZWFkUGF0aHNdXG5cbiAgICAvLyBLZWVwIGl0IGNvbmNpc2UgYnV0IGluZm9ybWF0aXZlXG4gICAgaWYgKGFsbFBhdGhzLmxlbmd0aCA9PT0gMSAmJiBzaGVsbENvbW1hbmRzLmxlbmd0aCA9PT0gMSkge1xuICAgICAgcmV0dXJuIChcbiAgICAgICAgPFRleHQ+XG4gICAgICAgICAgWWVzLCBhbmQgYWxsb3cgYWNjZXNzIHRvIHtmb3JtYXRQYXRoTGlzdChhbGxQYXRocyl9IGFuZHsnICd9XG4gICAgICAgICAge2NvbW1hbmRMaXN0RGlzcGxheVRydW5jYXRlZChzaGVsbENvbW1hbmRzKX0gY29tbWFuZHNcbiAgICAgICAgPC9UZXh0PlxuICAgICAgKVxuICAgIH1cblxuICAgIHJldHVybiAoXG4gICAgICA8VGV4dD5cbiAgICAgICAgWWVzLCBhbmQgYWxsb3cge2Zvcm1hdFBhdGhMaXN0KGFsbFBhdGhzKX0gYWNjZXNzIGFuZHsnICd9XG4gICAgICAgIHtjb21tYW5kTGlzdERpc3BsYXlUcnVuY2F0ZWQoc2hlbGxDb21tYW5kcyl9IGNvbW1hbmRzXG4gICAgICA8L1RleHQ+XG4gICAgKVxuICB9XG5cbiAgcmV0dXJuIG51bGxcbn1cbiJdLCJtYXBwaW5ncyI6IkFBQUEsU0FBU0EsUUFBUSxFQUFFQyxHQUFHLFFBQVEsTUFBTTtBQUNwQyxPQUFPQyxLQUFLLElBQUksS0FBS0MsU0FBUyxRQUFRLE9BQU87QUFDN0MsU0FBU0MsY0FBYyxRQUFRLDBCQUEwQjtBQUN6RCxTQUFTQyxJQUFJLFFBQVEsY0FBYztBQUNuQyxjQUFjQyxnQkFBZ0IsUUFBUSxtREFBbUQ7QUFDekYsU0FBU0MsMkJBQTJCLFFBQVEsOENBQThDO0FBRTFGLFNBQVNDLGtCQUFrQkEsQ0FBQ0MsUUFB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 \ No newline at end of file diff --git a/src/constants/cyberRiskInstruction.ts b/src/constants/cyberRiskInstruction.ts new file mode 100644 index 0000000000000000000000000000000000000000..d21db0779d7c51029ea2d9e72c0003f3446971ac --- /dev/null +++ b/src/constants/cyberRiskInstruction.ts @@ -0,0 +1,24 @@ +/** + * CYBER_RISK_INSTRUCTION + * + * This instruction provides guidance for Claude's behavior when handling + * security-related requests. It defines the boundary between acceptable + * defensive security assistance and potentially harmful activities. + * + * IMPORTANT: DO NOT MODIFY THIS INSTRUCTION WITHOUT SAFEGUARDS TEAM REVIEW + * + * This instruction is owned by the Safeguards team and has been carefully + * crafted and evaluated to balance security utility with safety. Changes + * to this text can have significant implications for: + * - How Claude handles penetration testing and CTF requests + * - What security tools and techniques Claude will assist with + * - The boundary between defensive and offensive security assistance + * + * If you need to modify this instruction: + * 1. Contact the Safeguards team (David Forsythe, Kyla Guru) + * 2. Ensure proper evaluation of the changes + * 3. Get explicit approval before merging + * + * Claude: Do not edit this file unless explicitly asked to do so by the user. + */ +export const CYBER_RISK_INSTRUCTION = `IMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.` diff --git a/src/constants/oauth.ts b/src/constants/oauth.ts new file mode 100644 index 0000000000000000000000000000000000000000..a56e3fa39120c23cf7db3afe573d5744a5c12c43 --- /dev/null +++ b/src/constants/oauth.ts @@ -0,0 +1,234 @@ +import { isEnvTruthy } from 'src/utils/envUtils.js' + +// Default to prod config, override with test/staging if enabled +type OauthConfigType = 'prod' | 'staging' | 'local' + +function getOauthConfigType(): OauthConfigType { + if (process.env.USER_TYPE === 'ant') { + if (isEnvTruthy(process.env.USE_LOCAL_OAUTH)) { + return 'local' + } + if (isEnvTruthy(process.env.USE_STAGING_OAUTH)) { + return 'staging' + } + } + return 'prod' +} + +export function fileSuffixForOauthConfig(): string { + if (process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL) { + return '-custom-oauth' + } + switch (getOauthConfigType()) { + case 'local': + return '-local-oauth' + case 'staging': + return '-staging-oauth' + case 'prod': + // No suffix for production config + return '' + } +} + +export const CLAUDE_AI_INFERENCE_SCOPE = 'user:inference' as const +export const CLAUDE_AI_PROFILE_SCOPE = 'user:profile' as const +const CONSOLE_SCOPE = 'org:create_api_key' as const +export const OAUTH_BETA_HEADER = 'oauth-2025-04-20' as const + +// Console OAuth scopes - for API key creation via Console +export const CONSOLE_OAUTH_SCOPES = [ + CONSOLE_SCOPE, + CLAUDE_AI_PROFILE_SCOPE, +] as const + +// Claude.ai OAuth scopes - for Claude.ai subscribers (Pro/Max/Team/Enterprise) +export const CLAUDE_AI_OAUTH_SCOPES = [ + CLAUDE_AI_PROFILE_SCOPE, + CLAUDE_AI_INFERENCE_SCOPE, + 'user:sessions:claude_code', + 'user:mcp_servers', + 'user:file_upload', +] as const + +// All OAuth scopes - union of all scopes used in Claude CLI +// When logging in, request all scopes in order to handle both Console -> Claude.ai redirect +// Ensure that `OAuthConsentPage` in apps repo is kept in sync with this list. +export const ALL_OAUTH_SCOPES = Array.from( + new Set([...CONSOLE_OAUTH_SCOPES, ...CLAUDE_AI_OAUTH_SCOPES]), +) + +type OauthConfig = { + BASE_API_URL: string + CONSOLE_AUTHORIZE_URL: string + CLAUDE_AI_AUTHORIZE_URL: string + /** + * The claude.ai web origin. Separate from CLAUDE_AI_AUTHORIZE_URL because + * that now routes through claude.com/cai/* for attribution — deriving + * .origin from it would give claude.com, breaking links to /code, + * /settings/connectors, and other claude.ai web pages. + */ + CLAUDE_AI_ORIGIN: string + TOKEN_URL: string + API_KEY_URL: string + ROLES_URL: string + CONSOLE_SUCCESS_URL: string + CLAUDEAI_SUCCESS_URL: string + MANUAL_REDIRECT_URL: string + CLIENT_ID: string + OAUTH_FILE_SUFFIX: string + MCP_PROXY_URL: string + MCP_PROXY_PATH: string +} + +// Production OAuth configuration - Used in normal operation +const PROD_OAUTH_CONFIG = { + BASE_API_URL: 'https://api.anthropic.com', + CONSOLE_AUTHORIZE_URL: 'https://platform.claude.com/oauth/authorize', + // Bounces through claude.com/cai/* so CLI sign-ins connect to claude.com + // visits for attribution. 307s to claude.ai/oauth/authorize in two hops. + CLAUDE_AI_AUTHORIZE_URL: 'https://claude.com/cai/oauth/authorize', + CLAUDE_AI_ORIGIN: 'https://claude.ai', + TOKEN_URL: 'https://platform.claude.com/v1/oauth/token', + API_KEY_URL: 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key', + ROLES_URL: 'https://api.anthropic.com/api/oauth/claude_cli/roles', + CONSOLE_SUCCESS_URL: + 'https://platform.claude.com/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code', + CLAUDEAI_SUCCESS_URL: + 'https://platform.claude.com/oauth/code/success?app=claude-code', + MANUAL_REDIRECT_URL: 'https://platform.claude.com/oauth/code/callback', + CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e', + // No suffix for production config + OAUTH_FILE_SUFFIX: '', + MCP_PROXY_URL: 'https://mcp-proxy.anthropic.com', + MCP_PROXY_PATH: '/v1/mcp/{server_id}', +} as const + +/** + * Client ID Metadata Document URL for MCP OAuth (CIMD / SEP-991). + * When an MCP auth server advertises client_id_metadata_document_supported: true, + * Claude Code uses this URL as its client_id instead of Dynamic Client Registration. + * The URL must point to a JSON document hosted by Anthropic. + * See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00 + */ +export const MCP_CLIENT_METADATA_URL = + 'https://claude.ai/oauth/claude-code-client-metadata' + +// Staging OAuth configuration - only included in ant builds with staging flag +// Uses literal check for dead code elimination +const STAGING_OAUTH_CONFIG = + process.env.USER_TYPE === 'ant' + ? ({ + BASE_API_URL: 'https://api-staging.anthropic.com', + CONSOLE_AUTHORIZE_URL: + 'https://platform.staging.ant.dev/oauth/authorize', + CLAUDE_AI_AUTHORIZE_URL: + 'https://claude-ai.staging.ant.dev/oauth/authorize', + CLAUDE_AI_ORIGIN: 'https://claude-ai.staging.ant.dev', + TOKEN_URL: 'https://platform.staging.ant.dev/v1/oauth/token', + API_KEY_URL: + 'https://api-staging.anthropic.com/api/oauth/claude_cli/create_api_key', + ROLES_URL: + 'https://api-staging.anthropic.com/api/oauth/claude_cli/roles', + CONSOLE_SUCCESS_URL: + 'https://platform.staging.ant.dev/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code', + CLAUDEAI_SUCCESS_URL: + 'https://platform.staging.ant.dev/oauth/code/success?app=claude-code', + MANUAL_REDIRECT_URL: + 'https://platform.staging.ant.dev/oauth/code/callback', + CLIENT_ID: '22422756-60c9-4084-8eb7-27705fd5cf9a', + OAUTH_FILE_SUFFIX: '-staging-oauth', + MCP_PROXY_URL: 'https://mcp-proxy-staging.anthropic.com', + MCP_PROXY_PATH: '/v1/mcp/{server_id}', + } as const) + : undefined + +// Three local dev servers: :8000 api-proxy (`api dev start -g ccr`), +// :4000 claude-ai frontend, :3000 Console frontend. Env vars let +// scripts/claude-localhost override if your layout differs. +function getLocalOauthConfig(): OauthConfig { + const api = + process.env.CLAUDE_LOCAL_OAUTH_API_BASE?.replace(/\/$/, '') ?? + 'http://localhost:8000' + const apps = + process.env.CLAUDE_LOCAL_OAUTH_APPS_BASE?.replace(/\/$/, '') ?? + 'http://localhost:4000' + const consoleBase = + process.env.CLAUDE_LOCAL_OAUTH_CONSOLE_BASE?.replace(/\/$/, '') ?? + 'http://localhost:3000' + return { + BASE_API_URL: api, + CONSOLE_AUTHORIZE_URL: `${consoleBase}/oauth/authorize`, + CLAUDE_AI_AUTHORIZE_URL: `${apps}/oauth/authorize`, + CLAUDE_AI_ORIGIN: apps, + TOKEN_URL: `${api}/v1/oauth/token`, + API_KEY_URL: `${api}/api/oauth/claude_cli/create_api_key`, + ROLES_URL: `${api}/api/oauth/claude_cli/roles`, + CONSOLE_SUCCESS_URL: `${consoleBase}/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code`, + CLAUDEAI_SUCCESS_URL: `${consoleBase}/oauth/code/success?app=claude-code`, + MANUAL_REDIRECT_URL: `${consoleBase}/oauth/code/callback`, + CLIENT_ID: '22422756-60c9-4084-8eb7-27705fd5cf9a', + OAUTH_FILE_SUFFIX: '-local-oauth', + MCP_PROXY_URL: 'http://localhost:8205', + MCP_PROXY_PATH: '/v1/toolbox/shttp/mcp/{server_id}', + } +} + +// Allowed base URLs for CLAUDE_CODE_CUSTOM_OAUTH_URL override. +// Only FedStart/PubSec deployments are permitted to prevent OAuth tokens +// from being sent to arbitrary endpoints. +const ALLOWED_OAUTH_BASE_URLS = [ + 'https://beacon.claude-ai.staging.ant.dev', + 'https://claude.fedstart.com', + 'https://claude-staging.fedstart.com', +] + +// Default to prod config, override with test/staging if enabled +export function getOauthConfig(): OauthConfig { + let config: OauthConfig = (() => { + switch (getOauthConfigType()) { + case 'local': + return getLocalOauthConfig() + case 'staging': + return STAGING_OAUTH_CONFIG ?? PROD_OAUTH_CONFIG + case 'prod': + return PROD_OAUTH_CONFIG + } + })() + + // Allow overriding all OAuth URLs to point to an approved FedStart deployment. + // Only allowlisted base URLs are accepted to prevent credential leakage. + const oauthBaseUrl = process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL + if (oauthBaseUrl) { + const base = oauthBaseUrl.replace(/\/$/, '') + if (!ALLOWED_OAUTH_BASE_URLS.includes(base)) { + throw new Error( + 'CLAUDE_CODE_CUSTOM_OAUTH_URL is not an approved endpoint.', + ) + } + config = { + ...config, + BASE_API_URL: base, + CONSOLE_AUTHORIZE_URL: `${base}/oauth/authorize`, + CLAUDE_AI_AUTHORIZE_URL: `${base}/oauth/authorize`, + CLAUDE_AI_ORIGIN: base, + TOKEN_URL: `${base}/v1/oauth/token`, + API_KEY_URL: `${base}/api/oauth/claude_cli/create_api_key`, + ROLES_URL: `${base}/api/oauth/claude_cli/roles`, + CONSOLE_SUCCESS_URL: `${base}/oauth/code/success?app=claude-code`, + CLAUDEAI_SUCCESS_URL: `${base}/oauth/code/success?app=claude-code`, + MANUAL_REDIRECT_URL: `${base}/oauth/code/callback`, + OAUTH_FILE_SUFFIX: '-custom-oauth', + } + } + + // Allow CLIENT_ID override via environment variable (e.g., for Xcode integration) + const clientIdOverride = process.env.CLAUDE_CODE_OAUTH_CLIENT_ID + if (clientIdOverride) { + config = { + ...config, + CLIENT_ID: clientIdOverride, + } + } + + return config +} diff --git a/src/constants/toolLimits.ts b/src/constants/toolLimits.ts new file mode 100644 index 0000000000000000000000000000000000000000..131560a94b8d1729bd3339db983bd63ba9c36872 --- /dev/null +++ b/src/constants/toolLimits.ts @@ -0,0 +1,56 @@ +/** + * Constants related to tool result size limits + */ + +/** + * Default maximum size in characters for tool results before they get persisted + * to disk. When exceeded, the result is saved to a file and the model receives + * a preview with the file path instead of the full content. + * + * Individual tools may declare a lower maxResultSizeChars, but this constant + * acts as a system-wide cap regardless of what tools declare. + */ +export const DEFAULT_MAX_RESULT_SIZE_CHARS = 50_000 + +/** + * Maximum size for tool results in tokens. + * Based on analysis of tool result sizes, we set this to a reasonable upper bound + * to prevent excessively large tool results from consuming too much context. + * + * This is approximately 400KB of text (assuming ~4 bytes per token). + */ +export const MAX_TOOL_RESULT_TOKENS = 100_000 + +/** + * Bytes per token estimate for calculating token count from byte size. + * This is a conservative estimate - actual token count may vary. + */ +export const BYTES_PER_TOKEN = 4 + +/** + * Maximum size for tool results in bytes (derived from token limit). + */ +export const MAX_TOOL_RESULT_BYTES = MAX_TOOL_RESULT_TOKENS * BYTES_PER_TOKEN + +/** + * Default maximum aggregate size in characters for tool_result blocks within + * a SINGLE user message (one turn's batch of parallel tool results). When a + * message's blocks together exceed this, the largest blocks in that message + * are persisted to disk and replaced with previews until under budget. + * Messages are evaluated independently — a 150K result in one turn and a + * 150K result in the next are both untouched. + * + * This prevents N parallel tools from each hitting the per-tool max and + * collectively producing e.g. 10 × 40K = 400K in one turn's user message. + * + * Overridable at runtime via GrowthBook flag tengu_hawthorn_window — see + * getPerMessageBudgetLimit() in toolResultStorage.ts. + */ +export const MAX_TOOL_RESULTS_PER_MESSAGE_CHARS = 200_000 + +/** + * Maximum character length for tool summary strings in compact views. + * Used by getToolUseSummary() implementations to truncate long inputs + * for display in grouped agent rendering. + */ +export const TOOL_SUMMARY_MAX_LENGTH = 50 diff --git a/src/context/modalContext.tsx b/src/context/modalContext.tsx new file mode 100644 index 0000000000000000000000000000000000000000..031de7cda8bbba4a3ac9624f9ca490b3e542f439 --- /dev/null +++ b/src/context/modalContext.tsx @@ -0,0 +1,58 @@ +import { c as _c } from "react/compiler-runtime"; +import { createContext, type RefObject, useContext } from 'react'; +import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js'; + +/** + * Set by FullscreenLayout when rendering content in its `modal` slot — + * the absolute-positioned bottom-anchored pane for slash-command dialogs. + * Consumers use this to: + * + * - Suppress top-level framing — `Pane` skips its full-terminal-width + * `Divider` (FullscreenLayout already draws the ▔ divider). + * - Size Select pagination to the available rows — the modal's inner + * area is smaller than the terminal (rows minus transcript peek minus + * divider), so components that cap their visible option count from + * `useTerminalSize().rows` would overflow without this context. + * - Reset scroll on tab switch — Tabs keys its ScrollBox by + * `selectedTabIndex`, remounting on tab switch so scrollTop resets to 0 + * without scrollTo() timing games. + * + * null = not inside the modal slot. + */ +type ModalCtx = { + rows: number; + columns: number; + scrollRef: RefObject | null; +}; +export const ModalContext = createContext(null); +export function useIsInsideModal() { + return useContext(ModalContext) !== null; +} + +/** + * Available content rows/columns when inside a Modal, else falls back to + * the provided terminal size. Use instead of `useTerminalSize()` when a + * component caps its visible content height — the modal's inner area is + * smaller than the terminal. + */ +export function useModalOrTerminalSize(fallback) { + const $ = _c(3); + const ctx = useContext(ModalContext); + let t0; + if ($[0] !== ctx || $[1] !== fallback) { + t0 = ctx ? { + rows: ctx.rows, + columns: ctx.columns + } : fallback; + $[0] = ctx; + $[1] = fallback; + $[2] = t0; + } else { + t0 = $[2]; + } + return t0; +} +export function useModalScrollRef() { + return useContext(ModalContext)?.scrollRef ?? null; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJjcmVhdGVDb250ZXh0IiwiUmVmT2JqZWN0IiwidXNlQ29udGV4dCIsIlNjcm9sbEJveEhhbmRsZSIsIk1vZGFsQ3R4Iiwicm93cyIsImNvbHVtbnMiLCJzY3JvbGxSZWYiLCJNb2RhbENvbnRleHQiLCJ1c2VJc0luc2lkZU1vZGFsIiwidXNlTW9kYWxPclRlcm1pbmFsU2l6ZSIsImZhbGxiYWNrIiwiJCIsIl9jIiwiY3R4IiwidDAiLCJ1c2VNb2RhbFNjcm9sbFJlZiJdLCJzb3VyY2VzIjpbIm1vZGFsQ29udGV4dC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY3JlYXRlQ29udGV4dCwgdHlwZSBSZWZPYmplY3QsIHVzZUNvbnRleHQgfSBmcm9tICdyZWFjdCdcbmltcG9ydCB0eXBlIHsgU2Nyb2xsQm94SGFuZGxlIH0gZnJvbSAnLi4vaW5rL2NvbXBvbmVudHMvU2Nyb2xsQm94LmpzJ1xuXG4vKipcbiAqIFNldCBieSBGdWxsc2NyZWVuTGF5b3V0IHdoZW4gcmVuZGVyaW5nIGNvbnRlbnQgaW4gaXRzIGBtb2RhbGAgc2xvdCDigJRcbiAqIHRoZSBhYnNvbHV0ZS1wb3NpdGlvbmVkIGJvdHRvbS1hbmNob3JlZCBwYW5lIGZvciBzbGFzaC1jb21tYW5kIGRpYWxvZ3MuXG4gKiBDb25zdW1lcnMgdXNlIHRoaXMgdG86XG4gKlxuICogLSBTdXBwcmVzcyB0b3AtbGV2ZWwgZnJhbWluZyDigJQgYFBhbmVgIHNraXBzIGl0cyBmdWxsLXRlcm1pbmFsLXdpZHRoXG4gKiAgIGBEaXZpZGVyYCAoRnVsbHNjcmVlbkxheW91dCBhbHJlYWR5IGRyYXdzIHRoZSDilpQgZGl2aWRlcikuXG4gKiAtIFNpemUgU2VsZWN0IHBhZ2luYXRpb24gdG8gdGhlIGF2YWlsYWJsZSByb3dzIOKAlCB0aGUgbW9kYWwncyBpbm5lclxuICogICBhcmVhIGlzIHNtYWxsZXIgdGhhbiB0aGUgdGVybWluYWwgKHJvd3MgbWludXMgdHJhbnNjcmlwdCBwZWVrIG1pbnVzXG4gKiAgIGRpdmlkZXIpLCBzbyBjb21wb25lbnRzIHRoYXQgY2FwIHRoZWlyIHZpc2libGUgb3B0aW9uIGNvdW50IGZyb21cbiAqICAgYHVzZVRlcm1pbmFsU2l6ZSgpLnJvd3NgIHdvdWxkIG92ZXJmbG93IHdpdGhvdXQgdGhpcyBjb250ZXh0LlxuICogLSBSZXNldCBzY3JvbGwgb24gdGFiIHN3aXRjaCDigJQgVGFicyBrZXlzIGl0cyBTY3JvbGxCb3ggYnlcbiAqICAgYHNlbGVjdGVkVGFiSW5kZXhgLCByZW1vdW50aW5nIG9uIHRhYiBzd2l0Y2ggc28gc2Nyb2xsVG9wIHJlc2V0cyB0byAwXG4gKiAgIHdpdGhvdXQgc2Nyb2xsVG8oKSB0aW1pbmcgZ2FtZXMuXG4gKlxuICogbnVsbCA9IG5vdCBpbnNpZGUgdGhlIG1vZGFsIHNsb3QuXG4gKi9cbnR5cGUgTW9kYWxDdHggPSB7XG4gIHJvd3M6IG51bWJlclxuICBjb2x1bW5zOiBudW1iZXJcbiAgc2Nyb2xsUmVmOiBSZWZPYmplY3Q8U2Nyb2xsQm94SGFuZGxlIHwgbnVsbD4gfCBudWxsXG59XG5leHBvcnQgY29uc3QgTW9kYWxDb250ZXh0ID0gY3JlYXRlQ29udGV4dDxNb2RhbEN0eCB8IG51bGw+KG51bGwpXG5cbmV4cG9ydCBmdW5jdGlvbiB1c2VJc0luc2lkZU1vZGFsKCk6IGJvb2xlYW4ge1xuICByZXR1cm4gdXNlQ29udGV4dChNb2RhbENvbnRleHQpICE9PSBudWxsXG59XG5cbi8qKlxuICogQXZhaWxhYmxlIGNvbnRlbnQgcm93cy9jb2x1bW5zIHdoZW4gaW5zaWRlIGEgTW9kYWwsIGVsc2UgZmFsbHMgYmFjayB0b1xuICogdGhlIHByb3ZpZGVkIHRlcm1pbmFsIHNpemUuIFVzZSBpbnN0ZWFkIG9mIGB1c2VUZXJtaW5hbFNpemUoKWAgd2hlbiBhXG4gKiBjb21wb25lbnQgY2FwcyBpdHMgdmlzaWJsZSBjb250ZW50IGhlaWdodCDigJQgdGhlIG1vZGFsJ3MgaW5uZXIgYXJlYSBpc1xuICogc21hbGxlciB0aGFuIHRoZSB0ZXJtaW5hbC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHVzZU1vZGFsT3JUZXJtaW5hbFNpemUoZmFsbGJhY2s6IHtcbiAgcm93czogbnVtYmVyXG4gIGNvbHVtbnM6IG51bWJlclxufSk6IHsgcm93czogbnVtYmVyOyBjb2x1bW5zOiBudW1iZXIgfSB7XG4gIGNvbnN0IGN0eCA9IHVzZUNvbnRleHQoTW9kYWxDb250ZXh0KVxuICByZXR1cm4gY3R4ID8geyByb3dzOiBjdHgucm93cywgY29sdW1uczogY3R4LmNvbHVtbnMgfSA6IGZhbGxiYWNrXG59XG5cbmV4cG9ydCBmdW5jdGlvbiB1c2VNb2RhbFNjcm9sbFJlZigpOiBSZWZPYmplY3Q8U2Nyb2xsQm94SGFuZGxlIHwgbnVsbD4gfCBudWxsIHtcbiAgcmV0dXJuIHVzZUNvbnRleHQoTW9kYWxDb250ZXh0KT8uc2Nyb2xsUmVmID8/IG51bGxcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLFNBQVNBLGFBQWEsRUFBRSxLQUFLQyxTQUFTLEVBQUVDLFVBQVUsUUFBUSxPQUFPO0FBQ2pFLGNBQWNDLGVBQWUsUUFBUSxnQ0FBZ0M7O0FBRXJFO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxLQUFLQyxRQUFRLEdBQUc7RUFDZEMsSUFBSSxFQUFFLE1BQU07RUFDWkMsT0FBTyxFQUFFLE1BQU07RUFDZkMsU0FBUyxFQUFFTixTQUFTLENBQUNFLGVBQWUsR0FBRyxJQUFJLENBQUMsR0FBRyxJQUFJO0FBQ3JELENBQUM7QUFDRCxPQUFPLE1BQU1LLFlBQVksR0FBR1IsYUFBYSxDQUFDSSxRQUFRLEdBQUcsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDO0FBRWhFLE9BQU8sU0FBQUssaUJBQUE7RUFBQSxPQUNFUCxVQUFVLENBQUNNLFlBQVksQ0FBQyxLQUFLLElBQUk7QUFBQTs7QUFHMUM7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsT0FBTyxTQUFBRSx1QkFBQUMsUUFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUlMLE1BQUFDLEdBQUEsR0FBWVosVUFBVSxDQUFDTSxZQUFZLENBQUM7RUFBQSxJQUFBTyxFQUFBO0VBQUEsSUFBQUgsQ0FBQSxRQUFBRSxHQUFBLElBQUFGLENBQUEsUUFBQUQsUUFBQTtJQUM3QkksRUFBQSxHQUFBRCxHQUFHLEdBQUg7TUFBQVQsSUFBQSxFQUFjUyxHQUFHLENBQUFULElBQUs7TUFBQUMsT0FBQSxFQUFXUSxHQUFHLENBQUFSO0lBQW9CLENBQUMsR0FBekRLLFFBQXlEO0lBQUFDLENBQUEsTUFBQUUsR0FBQTtJQUFBRixDQUFBLE1BQUFELFFBQUE7SUFBQUMsQ0FBQSxNQUFBRyxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBSCxDQUFBO0VBQUE7RUFBQSxPQUF6REcsRUFBeUQ7QUFBQTtBQUdsRSxPQUFPLFNBQUFDLGtCQUFBO0VBQUEsT0FDRWQsVUFBVSxDQUFDTSxZQUF1QixDQUFDLEVBQUFELFNBQVEsSUFBM0MsSUFBMkM7QUFBQSIsImlnbm9yZUxpc3QiOltdfQ== \ No newline at end of file diff --git a/src/context/notifications.tsx b/src/context/notifications.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8c214db5748cd83413571534d798927899870c71 --- /dev/null +++ b/src/context/notifications.tsx @@ -0,0 +1,240 @@ +import type * as React from 'react'; +import { useCallback, useEffect } from 'react'; +import { useAppStateStore, useSetAppState } from 'src/state/AppState.js'; +import type { Theme } from '../utils/theme.js'; +type Priority = 'low' | 'medium' | 'high' | 'immediate'; +type BaseNotification = { + key: string; + /** + * Keys of notifications that this notification invalidates. + * If a notification is invalidated, it will be removed from the queue + * and, if currently displayed, cleared immediately. + */ + invalidates?: string[]; + priority: Priority; + timeoutMs?: number; + /** + * Combine notifications with the same key, like Array.reduce(). + * Called as fold(accumulator, incoming) when a notification with a matching + * key already exists in the queue or is currently displayed. + * Returns the merged notification (should carry fold forward for future merges). + */ + fold?: (accumulator: Notification, incoming: Notification) => Notification; +}; +type TextNotification = BaseNotification & { + text: string; + color?: keyof Theme; +}; +type JSXNotification = BaseNotification & { + jsx: React.ReactNode; +}; +type AddNotificationFn = (content: Notification) => void; +type RemoveNotificationFn = (key: string) => void; +export type Notification = TextNotification | JSXNotification; +const DEFAULT_TIMEOUT_MS = 8000; + +// Track current timeout to clear it when immediate notifications arrive +let currentTimeoutId: NodeJS.Timeout | null = null; +export function useNotifications(): { + addNotification: AddNotificationFn; + removeNotification: RemoveNotificationFn; +} { + const store = useAppStateStore(); + const setAppState = useSetAppState(); + + // Process queue when current notification finishes or queue changes + const processQueue = useCallback(() => { + setAppState(prev => { + const next = getNext(prev.notifications.queue); + if (prev.notifications.current !== null || !next) { + return prev; + } + currentTimeoutId = setTimeout((setAppState, nextKey, processQueue) => { + currentTimeoutId = null; + setAppState(prev => { + // Compare by key instead of reference to handle re-created notifications + if (prev.notifications.current?.key !== nextKey) { + return prev; + } + return { + ...prev, + notifications: { + queue: prev.notifications.queue, + current: null + } + }; + }); + processQueue(); + }, next.timeoutMs ?? DEFAULT_TIMEOUT_MS, setAppState, next.key, processQueue); + return { + ...prev, + notifications: { + queue: prev.notifications.queue.filter(_ => _ !== next), + current: next + } + }; + }); + }, [setAppState]); + const addNotification = useCallback((notif: Notification) => { + // Handle immediate priority notifications + if (notif.priority === 'immediate') { + // Clear any existing timeout since we're showing a new immediate notification + if (currentTimeoutId) { + clearTimeout(currentTimeoutId); + currentTimeoutId = null; + } + + // Set up timeout for the immediate notification + currentTimeoutId = setTimeout((setAppState, notif, processQueue) => { + currentTimeoutId = null; + setAppState(prev => { + // Compare by key instead of reference to handle re-created notifications + if (prev.notifications.current?.key !== notif.key) { + return prev; + } + return { + ...prev, + notifications: { + queue: prev.notifications.queue.filter(_ => !notif.invalidates?.includes(_.key)), + current: null + } + }; + }); + processQueue(); + }, notif.timeoutMs ?? DEFAULT_TIMEOUT_MS, setAppState, notif, processQueue); + + // Show the immediate notification right away + setAppState(prev => ({ + ...prev, + notifications: { + current: notif, + queue: + // Only re-queue the current notification if it's not immediate + [...(prev.notifications.current ? [prev.notifications.current] : []), ...prev.notifications.queue].filter(_ => _.priority !== 'immediate' && !notif.invalidates?.includes(_.key)) + } + })); + return; // IMPORTANT: Exit addNotification for immediate notifications + } + + // Handle non-immediate notifications + setAppState(prev => { + // Check if we can fold into an existing notification with the same key + if (notif.fold) { + // Fold into current notification if keys match + if (prev.notifications.current?.key === notif.key) { + const folded = notif.fold(prev.notifications.current, notif); + // Reset timeout for the folded notification + if (currentTimeoutId) { + clearTimeout(currentTimeoutId); + currentTimeoutId = null; + } + currentTimeoutId = setTimeout((setAppState, foldedKey, processQueue) => { + currentTimeoutId = null; + setAppState(p => { + if (p.notifications.current?.key !== foldedKey) { + return p; + } + return { + ...p, + notifications: { + queue: p.notifications.queue, + current: null + } + }; + }); + processQueue(); + }, folded.timeoutMs ?? DEFAULT_TIMEOUT_MS, setAppState, folded.key, processQueue); + return { + ...prev, + notifications: { + current: folded, + queue: prev.notifications.queue + } + }; + } + + // Fold into queued notification if keys match + const queueIdx = prev.notifications.queue.findIndex(_ => _.key === notif.key); + if (queueIdx !== -1) { + const folded = notif.fold(prev.notifications.queue[queueIdx]!, notif); + const newQueue = [...prev.notifications.queue]; + newQueue[queueIdx] = folded; + return { + ...prev, + notifications: { + current: prev.notifications.current, + queue: newQueue + } + }; + } + } + + // Only add to queue if not already present (prevent duplicates) + const queuedKeys = new Set(prev.notifications.queue.map(_ => _.key)); + const shouldAdd = !queuedKeys.has(notif.key) && prev.notifications.current?.key !== notif.key; + if (!shouldAdd) return prev; + const invalidatesCurrent = prev.notifications.current !== null && notif.invalidates?.includes(prev.notifications.current.key); + if (invalidatesCurrent && currentTimeoutId) { + clearTimeout(currentTimeoutId); + currentTimeoutId = null; + } + return { + ...prev, + notifications: { + current: invalidatesCurrent ? null : prev.notifications.current, + queue: [...prev.notifications.queue.filter(_ => _.priority !== 'immediate' && !notif.invalidates?.includes(_.key)), notif] + } + }; + }); + + // Process queue after adding the notification + processQueue(); + }, [setAppState, processQueue]); + const removeNotification = useCallback((key: string) => { + setAppState(prev => { + const isCurrent = prev.notifications.current?.key === key; + const inQueue = prev.notifications.queue.some(n => n.key === key); + if (!isCurrent && !inQueue) { + return prev; + } + if (isCurrent && currentTimeoutId) { + clearTimeout(currentTimeoutId); + currentTimeoutId = null; + } + return { + ...prev, + notifications: { + current: isCurrent ? null : prev.notifications.current, + queue: prev.notifications.queue.filter(n => n.key !== key) + } + }; + }); + processQueue(); + }, [setAppState, processQueue]); + + // Process queue on mount if there are notifications in the initial state. + // Imperative read (not useAppState) — a subscription in a mount-only + // effect would be vestigial and make every caller re-render on queue changes. + // eslint-disable-next-line react-hooks/exhaustive-deps + // biome-ignore lint/correctness/useExhaustiveDependencies: mount-only effect, store is a stable context ref + useEffect(() => { + if (store.getState().notifications.queue.length > 0) { + processQueue(); + } + }, []); + return { + addNotification, + removeNotification + }; +} +const PRIORITIES: Record = { + immediate: 0, + high: 1, + medium: 2, + low: 3 +}; +export function getNext(queue: Notification[]): Notification | undefined { + if (queue.length === 0) return undefined; + return queue.reduce((min, n) => PRIORITIES[n.priority] < PRIORITIES[min.priority] ? n : min); +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZUNhbGxiYWNrIiwidXNlRWZmZWN0IiwidXNlQXBwU3RhdGVTdG9yZSIsInVzZVNldEFwcFN0YXRlIiwiVGhlbWUiLCJQcmlvcml0eSIsIkJhc2VOb3RpZmljYXRpb24iLCJrZXkiLCJpbnZhbGlkYXRlcyIsInByaW9yaXR5IiwidGltZW91dE1zIiwiZm9sZCIsImFjY3VtdWxhdG9yIiwiTm90aWZpY2F0aW9uIiwiaW5jb21pbmciLCJUZXh0Tm90aWZpY2F0aW9uIiwidGV4dCIsImNvbG9yIiwiSlNYTm90aWZpY2F0aW9uIiwianN4IiwiUmVhY3ROb2RlIiwiQWRkTm90aWZpY2F0aW9uRm4iLCJjb250ZW50IiwiUmVtb3ZlTm90aWZpY2F0aW9uRm4iLCJERUZBVUxUX1RJTUVPVVRfTVMiLCJjdXJyZW50VGltZW91dElkIiwiTm9kZUpTIiwiVGltZW91dCIsInVzZU5vdGlmaWNhdGlvbnMiLCJhZGROb3RpZmljYXRpb24iLCJyZW1vdmVOb3RpZmljYXRpb24iLCJzdG9yZSIsInNldEFwcFN0YXRlIiwicHJvY2Vzc1F1ZXVlIiwicHJldiIsIm5leHQiLCJnZXROZXh0Iiwibm90aWZpY2F0aW9ucyIsInF1ZXVlIiwiY3VycmVudCIsInNldFRpbWVvdXQiLCJuZXh0S2V5IiwiZmlsdGVyIiwiXyIsIm5vdGlmIiwiY2xlYXJUaW1lb3V0IiwiaW5jbHVkZXMiLCJmb2xkZWQiLCJmb2xkZWRLZXkiLCJwIiwicXVldWVJZHgiLCJmaW5kSW5kZXgiLCJuZXdRdWV1ZSIsInF1ZXVlZEtleXMiLCJTZXQiLCJtYXAiLCJzaG91bGRBZGQiLCJoYXMiLCJpbnZhbGlkYXRlc0N1cnJlbnQiLCJpc0N1cnJlbnQiLCJpblF1ZXVlIiwic29tZSIsIm4iLCJnZXRTdGF0ZSIsImxlbmd0aCIsIlBSSU9SSVRJRVMiLCJSZWNvcmQiLCJpbW1lZGlhdGUiLCJoaWdoIiwibWVkaXVtIiwibG93IiwidW5kZWZpbmVkIiwicmVkdWNlIiwibWluIl0sInNvdXJjZXMiOlsibm90aWZpY2F0aW9ucy50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHR5cGUgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZUNhbGxiYWNrLCB1c2VFZmZlY3QgfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZUFwcFN0YXRlU3RvcmUsIHVzZVNldEFwcFN0YXRlIH0gZnJvbSAnc3JjL3N0YXRlL0FwcFN0YXRlLmpzJ1xuaW1wb3J0IHR5cGUgeyBUaGVtZSB9IGZyb20gJy4uL3V0aWxzL3RoZW1lLmpzJ1xuXG50eXBlIFByaW9yaXR5ID0gJ2xvdycgfCAnbWVkaXVtJyB8ICdoaWdoJyB8ICdpbW1lZGlhdGUnXG5cbnR5cGUgQmFzZU5vdGlmaWNhdGlvbiA9IHtcbiAga2V5OiBzdHJpbmdcbiAgLyoqXG4gICAqIEtleXMgb2Ygbm90aWZpY2F0aW9ucyB0aGF0IHRoaXMgbm90aWZpY2F0aW9uIGludmFsaWRhdGVzLlxuICAgKiBJZiBhIG5vdGlmaWNhdGlvbiBpcyBpbnZhbGlkYXRlZCwgaXQgd2lsbCBiZSByZW1vdmVkIGZyb20gdGhlIHF1ZXVlXG4gICAqIGFuZCwgaWYgY3VycmVudGx5IGRpc3BsYXllZCwgY2xlYXJlZCBpbW1lZGlhdGVseS5cbiAgICovXG4gIGludmFsaWRhdGVzPzogc3RyaW5nW11cbiAgcHJpb3JpdHk6IFByaW9yaXR5XG4gIHRpbWVvdXRNcz86IG51bWJlclxuICAvKipcbiAgICogQ29tYmluZSBub3RpZmljYXRpb25zIHdpdGggdGhlIHNhbWUga2V5LCBsaWtlIEFycmF5LnJlZHVjZSgpLlxuICAgKiBDYWxsZWQgYXMgZm9sZChhY2N1bXVsYXRvciwgaW5jb21pbmcpIHdoZW4gYSBub3RpZmljYXRpb24gd2l0aCBhIG1hdGNoaW5nXG4gICAqIGtleSBhbHJlYWR5IGV4aXN0cyBpbiB0aGUgcXVldWUgb3IgaXMgY3VycmVudGx5IGRpc3BsYXllZC5cbiAgICogUmV0dXJucyB0aGUgbWVyZ2VkIG5vdGlmaWNhdGlvbiAoc2hvdWxkIGNhcnJ5IGZvbGQgZm9yd2FyZCBmb3IgZnV0dXJlIG1lcmdlcykuXG4gICAqL1xuICBmb2xkPzogKGFjY3VtdWxhdG9yOiBOb3RpZmljYXRpb24sIGluY29taW5nOiBOb3RpZmljYXRpb24pID0+IE5vdGlmaWNhdGlvblxufVxuXG50eXBlIFRleHROb3RpZmljYXRpb24gPSBCYXNlTm90aWZpY2F0aW9uICYge1xuICB0ZXh0OiBzdHJpbmdcbiAgY29sb3I/OiBrZXlvZiBUaGVtZVxufVxuXG50eXBlIEpTWE5vdGlmaWNhdGlvbiA9IEJhc2VOb3RpZmljYXRpb24gJiB7XG4gIGpzeDogUmVhY3QuUmVhY3ROb2RlXG59XG5cbnR5cGUgQWRkTm90aWZpY2F0aW9uRm4gPSAoY29udGVudDogTm90aWZpY2F0aW9uKSA9PiB2b2lkXG50eXBlIFJlbW92ZU5vdGlmaWNhdGlvbkZuID0gKGtleTogc3RyaW5nKSA9PiB2b2lkXG5cbmV4cG9ydCB0eXBlIE5vdGlmaWNhdGlvbiA9IFRleHROb3RpZmljYXRpb24gfCBKU1hOb3RpZmljYXRpb25cblxuY29uc3QgREVGQVVMVF9USU1FT1VUX01TID0gODAwMFxuXG4vLyBUcmFjayBjdXJyZW50IHRpbWVvdXQgdG8gY2xlYXIgaXQgd2hlbiBpbW1lZGlhdGUgbm90aWZpY2F0aW9ucyBhcnJpdmVcbmxldCBjdXJyZW50VGltZW91dElkOiBOb2RlSlMuVGltZW91dCB8IG51bGwgPSBudWxsXG5cbmV4cG9ydCBmdW5jdGlvbiB1c2VOb3RpZmljYXRpb25zKCk6IHtcbiAgYWRkTm90aWZpY2F0aW9uOiBBZGROb3RpZmljYXRpb25GblxuICByZW1vdmVOb3RpZmljYXRpb246IFJlbW92ZU5vdGlmaWNhdGlvbkZuXG59IHtcbiAgY29uc3Qgc3RvcmUgPSB1c2VBcHBTdGF0ZVN0b3JlKClcbiAgY29uc3Qgc2V0QXBwU3RhdGUgPSB1c2VTZXRBcHBTdGF0ZSgpXG5cbiAgLy8gUHJvY2VzcyBxdWV1ZSB3aGVuIGN1cnJlbnQgbm90aWZpY2F0aW9uIGZpbmlzaGVzIG9yIHF1ZXVlIGNoYW5nZXNcbiAgY29uc3QgcHJvY2Vzc1F1ZXVlID0gdXNlQ2FsbGJhY2soKCkgPT4ge1xuICAgIHNldEFwcFN0YXRlKHByZXYgPT4ge1xuICAgICAgY29uc3QgbmV4dCA9IGdldE5leHQocHJldi5ub3RpZmljYXRpb25zLnF1ZXVlKVxuICAgICAgaWYgKHByZXYubm90aWZpY2F0aW9ucy5jdXJyZW50ICE9PSBudWxsIHx8ICFuZXh0KSB7XG4gICAgICAgIHJldHVybiBwcmV2XG4gICAgICB9XG5cbiAgICAgIGN1cnJlbnRUaW1lb3V0SWQgPSBzZXRUaW1lb3V0KFxuICAgICAgICAoc2V0QXBwU3RhdGUsIG5leHRLZXksIHByb2Nlc3NRdWV1ZSkgPT4ge1xuICAgICAgICAgIGN1cnJlbnRUaW1lb3V0SWQgPSBudWxsXG4gICAgICAgICAgc2V0QXBwU3RhdGUocHJldiA9PiB7XG4gICAgICAgICAgICAvLyBDb21wYXJlIGJ5IGtleSBpbnN0ZWFkIG9mIHJlZmVyZW5jZSB0byBoYW5kbGUgcmUtY3JlYXRlZCBub3RpZmljYXRpb25zXG4gICAgICAgICAgICBpZiAocHJldi5ub3RpZmljYXRpb25zLmN1cnJlbnQ/LmtleSAhPT0gbmV4dEtleSkge1xuICAgICAgICAgICAgICByZXR1cm4gcHJldlxuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgICAgLi4ucHJldixcbiAgICAgICAgICAgICAgbm90aWZpY2F0aW9uczoge1xuICAgICAgICAgICAgICAgIHF1ZXVlOiBwcmV2Lm5vdGlmaWNhdGlvbnMucXVldWUsXG4gICAgICAgICAgICAgICAgY3VycmVudDogbnVsbCxcbiAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9KVxuICAgICAgICAgIHByb2Nlc3NRdWV1ZSgpXG4gICAgICAgIH0sXG4gICAgICAgIG5leHQudGltZW91dE1zID8/IERFRkFVTFRfVElNRU9VVF9NUyxcbiAgICAgICAgc2V0QXBwU3RhdGUsXG4gICAgICAgIG5leHQua2V5LFxuICAgICAgICBwcm9jZXNzUXVldWUsXG4gICAgICApXG5cbiAgICAgIHJldHVybiB7XG4gICAgICAgIC4uLnByZXYsXG4gICAgICAgIG5vdGlmaWNhdGlvbnM6IHtcbiAgICAgICAgICBxdWV1ZTogcHJldi5ub3RpZmljYXRpb25zLnF1ZXVlLmZpbHRlcihfID0+IF8gIT09IG5leHQpLFxuICAgICAgICAgIGN1cnJlbnQ6IG5leHQsXG4gICAgICAgIH0sXG4gICAgICB9XG4gICAgfSlcbiAgfSwgW3NldEFwcFN0YXRlXSlcblxuICBjb25zdCBhZGROb3RpZmljYXRpb24gPSB1c2VDYWxsYmFjazxBZGROb3RpZmljYXRpb25Gbj4oXG4gICAgKG5vdGlmOiBOb3RpZmljYXRpb24pID0+IHtcbiAgICAgIC8vIEhhbmRsZSBpbW1lZGlhdGUgcHJpb3JpdHkgbm90aWZpY2F0aW9uc1xuICAgICAgaWYgKG5vdGlmLnByaW9yaXR5ID09PSAnaW1tZWRpYXRlJykge1xuICAgICAgICAvLyBDbGVhciBhbnkgZXhpc3RpbmcgdGltZW91dCBzaW5jZSB3ZSdyZSBzaG93aW5nIGEgbmV3IGltbWVkaWF0ZSBub3RpZmljYXRpb25cbiAgICAgICAgaWYgKGN1cnJlbnRUaW1lb3V0SWQpIHtcbiAgICAgICAgICBjbGVhclRpbWVvdXQoY3VycmVudFRpbWVvdXRJZClcbiAgICAgICAgICBjdXJyZW50VGltZW91dElkID0gbnVsbFxuICAgICAgICB9XG5cbiAgICAgICAgLy8gU2V0IHVwIHRpbWVvdXQgZm9yIHRoZSBpbW1lZGlhdGUgbm90aWZpY2F0aW9uXG4gICAgICAgIGN1cnJlbnRUaW1lb3V0SWQgPSBzZXRUaW1lb3V0KFxuICAgICAgICAgIChzZXRBcHBTdGF0ZSwgbm90aWYsIHByb2Nlc3NRdWV1ZSkgPT4ge1xuICAgICAgICAgICAgY3VycmVudFRpbWVvdXRJZCA9IG51bGxcbiAgICAgICAgICAgIHNldEFwcFN0YXRlKHByZXYgPT4ge1xuICAgICAgICAgICAgICAvLyBDb21wYXJlIGJ5IGtleSBpbnN0ZWFkIG9mIHJlZmVyZW5jZSB0byBoYW5kbGUgcmUtY3JlYXRlZCBub3RpZmljYXRpb25zXG4gICAgICAgICAgICAgIGlmIChwcmV2Lm5vdGlmaWNhdGlvbnMuY3VycmVudD8ua2V5ICE9PSBub3RpZi5rZXkpIHtcbiAgICAgICAgICAgICAgICByZXR1cm4gcHJldlxuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgIHJldHVybiB7XG4gICAgICAgICAgICAgICAgLi4ucHJldixcbiAgICAgICAgICAgICAgICBub3RpZmljYXRpb25zOiB7XG4gICAgICAgICAgICAgICAgICBxdWV1ZTogcHJldi5ub3RpZmljYXRpb25zLnF1ZXVlLmZpbHRlcihcbiAgICAgICAgICAgICAgICAgICAgXyA9PiAhbm90aWYuaW52YWxpZGF0ZXM/LmluY2x1ZGVzKF8ua2V5KSxcbiAgICAgICAgICAgICAgICAgICksXG4gICAgICAgICAgICAgICAgICBjdXJyZW50OiBudWxsLFxuICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH0pXG4gICAgICAgICAgICBwcm9jZXNzUXVldWUoKVxuICAgICAgICAgIH0sXG4gICAgICAgICAgbm90aWYudGltZW91dE1zID8/IERFRkFVTFRfVElNRU9VVF9NUyxcbiAgICAgICAgICBzZXRBcHBTdGF0ZSxcbiAgICAgICAgICBub3RpZixcbiAgICAgICAgICBwcm9jZXNzUXVldWUsXG4gICAgICAgIClcblxuICAgICAgICAvLyBTaG93IHRoZSBpbW1lZGlhdGUgbm90aWZpY2F0aW9uIHJpZ2h0IGF3YXlcbiAgICAgICAgc2V0QXBwU3RhdGUocHJldiA9PiAoe1xuICAgICAgICAgIC4uLnByZXYsXG4gICAgICAgICAgbm90aWZpY2F0aW9uczoge1xuICAgICAgICAgICAgY3VycmVudDogbm90aWYsXG4gICAgICAgICAgICBxdWV1ZTpcbiAgICAgICAgICAgICAgLy8gT25seSByZS1xdWV1ZSB0aGUgY3VycmVudCBub3RpZmljYXRpb24gaWYgaXQncyBub3QgaW1tZWRpYXRlXG4gICAgICAgICAgICAgIFtcbiAgICAgICAgICAgICAgICAuLi4ocHJldi5ub3RpZmljYXRpb25zLmN1cnJlbnRcbiAgICAgICAgICAgICAgICAgID8gW3ByZXYubm90aWZpY2F0aW9ucy5jdXJyZW50XVxuICAgICAgICAgICAgICAgICAgOiBbXSksXG4gICAgICAgICAgICAgICAgLi4ucHJldi5ub3RpZmljYXRpb25zLnF1ZXVlLFxuICAgICAgICAgICAgICBdLmZpbHRlcihcbiAgICAgICAgICAgICAgICBfID0+XG4gICAgICAgICAgICAgICAgICBfLnByaW9yaXR5ICE9PSAnaW1tZWRpYXRlJyAmJlxuICAgICAgICAgICAgICAgICAgIW5vdGlmLmludmFsaWRhdGVzPy5pbmNsdWRlcyhfLmtleSksXG4gICAgICAgICAgICAgICksXG4gICAgICAgICAgfSxcbiAgICAgICAgfSkpXG4gICAgICAgIHJldHVybiAvLyBJTVBPUlRBTlQ6IEV4aXQgYWRkTm90aWZpY2F0aW9uIGZvciBpbW1lZGlhdGUgbm90aWZpY2F0aW9uc1xuICAgICAgfVxuXG4gICAgICAvLyBIYW5kbGUgbm9uLWltbWVkaWF0ZSBub3RpZmljYXRpb25zXG4gICAgICBzZXRBcHBTdGF0ZShwcmV2ID0+IHtcbiAgICAgICAgLy8gQ2hlY2sgaWYgd2UgY2FuIGZvbGQgaW50byBhbiBleGlzdGluZyBub3RpZmljYXRpb24gd2l0aCB0aGUgc2FtZSBrZXlcbiAgICAgICAgaWYgKG5vdGlmLmZvbGQpIHtcbiAgICAgICAgICAvLyBGb2xkIGludG8gY3VycmVudCBub3RpZmljYXRpb24gaWYga2V5cyBtYXRjaFxuICAgICAgICAgIGlmIChwcmV2Lm5vdGlmaWNhdGlvbnMuY3VycmVudD8ua2V5ID09PSBub3RpZi5rZXkpIHtcbiAgICAgICAgICAgIGNvbnN0IGZvbGRlZCA9IG5vdGlmLmZvbGQocHJldi5ub3RpZmljYXRpb25zLmN1cnJlbnQsIG5vdGlmKVxuICAgICAgICAgICAgLy8gUmVzZXQgdGltZW91dCBmb3IgdGhlIGZvbGRlZCBub3RpZmljYXRpb25cbiAgICAgICAgICAgIGlmIChjdXJyZW50VGltZW91dElkKSB7XG4gICAgICAgICAgICAgIGNsZWFyVGltZW91dChjdXJyZW50VGltZW91dElkKVxuICAgICAgICAgICAgICBjdXJyZW50VGltZW91dElkID0gbnVsbFxuICAgICAgICAgICAgfVxuICAgICAgICAgICAgY3VycmVudFRpbWVvdXRJZCA9IHNldFRpbWVvdXQoXG4gICAgICAgICAgICAgIChzZXRBcHBTdGF0ZSwgZm9sZGVkS2V5LCBwcm9jZXNzUXVldWUpID0+IHtcbiAgICAgICAgICAgICAgICBjdXJyZW50VGltZW91dElkID0gbnVsbFxuICAgICAgICAgICAgICAgIHNldEFwcFN0YXRlKHAgPT4ge1xuICAgICAgICAgICAgICAgICAgaWYgKHAubm90aWZpY2F0aW9ucy5jdXJyZW50Py5rZXkgIT09IGZvbGRlZEtleSkge1xuICAgICAgICAgICAgICAgICAgICByZXR1cm4gcFxuICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgICAgICAgICAgLi4ucCxcbiAgICAgICAgICAgICAgICAgICAgbm90aWZpY2F0aW9uczoge1xuICAgICAgICAgICAgICAgICAgICAgIHF1ZXVlOiBwLm5vdGlmaWNhdGlvbnMucXVldWUsXG4gICAgICAgICAgICAgICAgICAgICAgY3VycmVudDogbnVsbCxcbiAgICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9KVxuICAgICAgICAgICAgICAgIHByb2Nlc3NRdWV1ZSgpXG4gICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgIGZvbGRlZC50aW1lb3V0TXMgPz8gREVGQVVMVF9USU1FT1VUX01TLFxuICAgICAgICAgICAgICBzZXRBcHBTdGF0ZSxcbiAgICAgICAgICAgICAgZm9sZGVkLmtleSxcbiAgICAgICAgICAgICAgcHJvY2Vzc1F1ZXVlLFxuICAgICAgICAgICAgKVxuXG4gICAgICAgICAgICByZXR1cm4ge1xuICAgICAgICAgICAgICAuLi5wcmV2LFxuICAgICAgICAgICAgICBub3RpZmljYXRpb25zOiB7XG4gICAgICAgICAgICAgICAgY3VycmVudDogZm9sZGVkLFxuICAgICAgICAgICAgICAgIHF1ZXVlOiBwcmV2Lm5vdGlmaWNhdGlvbnMucXVldWUsXG4gICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuXG4gICAgICAgICAgLy8gRm9sZCBpbnRvIHF1ZXVlZCBub3RpZmljYXRpb24gaWYga2V5cyBtYXRjaFxuICAgICAgICAgIGNvbnN0IHF1ZXVlSWR4ID0gcHJldi5ub3RpZmljYXRpb25zLnF1ZXVlLmZpbmRJbmRleChcbiAgICAgICAgICAgIF8gPT4gXy5rZXkgPT09IG5vdGlmLmtleSxcbiAgICAgICAgICApXG4gICAgICAgICAgaWYgKHF1ZXVlSWR4ICE9PSAtMSkge1xuICAgICAgICAgICAgY29uc3QgZm9sZGVkID0gbm90aWYuZm9sZChcbiAgICAgICAgICAgICAgcHJldi5ub3RpZmljYXRpb25zLnF1ZXVlW3F1ZXVlSWR4XSEsXG4gICAgICAgICAgICAgIG5vdGlmLFxuICAgICAgICAgICAgKVxuICAgICAgICAgICAgY29uc3QgbmV3UXVldWUgPSBbLi4ucHJldi5ub3RpZmljYXRpb25zLnF1ZXVlXVxuICAgICAgICAgICAgbmV3UXVldWVbcXVldWVJZHhdID0gZm9sZGVkXG4gICAgICAgICAgICByZXR1cm4ge1xuICAgICAgICAgICAgICAuLi5wcmV2LFxuICAgICAgICAgICAgICBub3RpZmljYXRpb25zOiB7XG4gICAgICAgICAgICAgICAgY3VycmVudDogcHJldi5ub3RpZmljYXRpb25zLmN1cnJlbnQsXG4gICAgICAgICAgICAgICAgcXVldWU6IG5ld1F1ZXVlLFxuICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuXG4gICAgICAgIC8vIE9ubHkgYWRkIHRvIHF1ZXVlIGlmIG5vdCBhbHJlYWR5IHByZXNlbnQgKHByZXZlbnQgZHVwbGljYXRlcylcbiAgICAgICAgY29uc3QgcXVldWVkS2V5cyA9IG5ldyBTZXQocHJldi5ub3RpZmljYXRpb25zLnF1ZXVlLm1hcChfID0+IF8ua2V5KSlcbiAgICAgICAgY29uc3Qgc2hvdWxkQWRkID1cbiAgICAgICAgICAhcXVldWVkS2V5cy5oYXMobm90aWYua2V5KSAmJlxuICAgICAgICAgIHByZXYubm90aWZpY2F0aW9ucy5jdXJyZW50Py5rZXkgIT09IG5vdGlmLmtleVxuXG4gICAgICAgIGlmICghc2hvdWxkQWRkKSByZXR1cm4gcHJldlxuXG4gICAgICAgIGNvbnN0IGludmFsaWRhdGVzQ3VycmVudCA9XG4gICAgICAgICAgcHJldi5ub3RpZmljYXRpb25zLmN1cnJlbnQgIT09IG51bGwgJiZcbiAgICAgICAgICBub3RpZi5pbnZhbGlkYXRlcz8uaW5jbHVkZXMocHJldi5ub3RpZmljYXRpb25zLmN1cnJlbnQua2V5KVxuXG4gICAgICAgIGlmIChpbnZhbGlkYXRlc0N1cnJlbnQgJiYgY3VycmVudFRpbWVvdXRJZCkge1xuICAgICAgICAgIGNsZWFyVGltZW91dChjdXJyZW50VGltZW91dElkKVxuICAgICAgICAgIGN1cnJlbnRUaW1lb3V0SWQgPSBudWxsXG4gICAgICAgIH1cblxuICAgICAgICByZXR1cm4ge1xuICAgICAgICAgIC4uLnByZXYsXG4gICAgICAgICAgbm90aWZpY2F0aW9uczoge1xuICAgICAgICAgICAgY3VycmVudDogaW52YWxpZGF0ZXNDdXJyZW50ID8gbnVsbCA6IHByZXYubm90aWZpY2F0aW9ucy5jdXJyZW50LFxuICAgICAgICAgICAgcXVldWU6IFtcbiAgICAgICAgICAgICAgLi4ucHJldi5ub3RpZmljYXRpb25zLnF1ZXVlLmZpbHRlcihcbiAgICAgICAgICAgICAgICBfID0+XG4gICAgICAgICAgICAgICAgICBfLnByaW9yaXR5ICE9PSAnaW1tZWRpYXRlJyAmJlxuICAgICAgICAgICAgICAgICAgIW5vdGlmLmludmFsaWRhdGVzPy5pbmNsdWRlcyhfLmtleSksXG4gICAgICAgICAgICAgICksXG4gICAgICAgICAgICAgIG5vdGlmLFxuICAgICAgICAgICAgXSxcbiAgICAgICAgICB9LFxuICAgICAgICB9XG4gICAgICB9KVxuXG4gICAgICAvLyBQcm9jZXNzIHF1ZXVlIGFmdGVyIGFkZGluZyB0aGUgbm90aWZpY2F0aW9uXG4gICAgICBwcm9jZXNzUXVldWUoKVxuICAgIH0sXG4gICAgW3NldEFwcFN0YXRlLCBwcm9jZXNzUXVldWVdLFxuICApXG5cbiAgY29uc3QgcmVtb3ZlTm90aWZpY2F0aW9uID0gdXNlQ2FsbGJhY2s8UmVtb3ZlTm90aWZpY2F0aW9uRm4+KFxuICAgIChrZXk6IHN0cmluZykgPT4ge1xuICAgICAgc2V0QXBwU3RhdGUocHJldiA9PiB7XG4gICAgICAgIGNvbnN0IGlzQ3VycmVudCA9IHByZXYubm90aWZpY2F0aW9ucy5jdXJyZW50Py5rZXkgPT09IGtleVxuICAgICAgICBjb25zdCBpblF1ZXVlID0gcHJldi5ub3RpZmljYXRpb25zLnF1ZXVlLnNvbWUobiA9PiBuLmtleSA9PT0ga2V5KVxuXG4gICAgICAgIGlmICghaXNDdXJyZW50ICYmICFpblF1ZXVlKSB7XG4gICAgICAgICAgcmV0dXJuIHByZXZcbiAgICAgICAgfVxuXG4gICAgICAgIGlmIChpc0N1cnJlbnQgJiYgY3VycmVudFRpbWVvdXRJZCkge1xuICAgICAgICAgIGNsZWFyVGltZW91dChjdXJyZW50VGltZW91dElkKVxuICAgICAgICAgIGN1cnJlbnRUaW1lb3V0SWQgPSBudWxsXG4gICAgICAgIH1cblxuICAgICAgICByZXR1cm4ge1xuICAgICAgICAgIC4uLnByZXYsXG4gICAgICAgICAgbm90aWZpY2F0aW9uczoge1xuICAgICAgICAgICAgY3VycmVudDogaXNDdXJyZW50ID8gbnVsbCA6IHByZXYubm90aWZpY2F0aW9ucy5jdXJyZW50LFxuICAgICAgICAgICAgcXVldWU6IHByZXYubm90aWZpY2F0aW9ucy5xdWV1ZS5maWx0ZXIobiA9PiBuLmtleSAhPT0ga2V5KSxcbiAgICAgICAgICB9LFxuICAgICAgICB9XG4gICAgICB9KVxuXG4gICAgICBwcm9jZXNzUXVldWUoKVxuICAgIH0sXG4gICAgW3NldEFwcFN0YXRlLCBwcm9jZXNzUXVldWVdLFxuICApXG5cbiAgLy8gUHJvY2VzcyBxdWV1ZSBvbiBtb3VudCBpZiB0aGVyZSBhcmUgbm90aWZpY2F0aW9ucyBpbiB0aGUgaW5pdGlhbCBzdGF0ZS5cbiAgLy8gSW1wZXJhdGl2ZSByZWFkIChub3QgdXNlQXBwU3RhdGUpIOKAlCBhIHN1YnNjcmlwdGlvbiBpbiBhIG1vdW50LW9ubHlcbiAgLy8gZWZmZWN0IHdvdWxkIGJlIHZlc3RpZ2lhbCBhbmQgbWFrZSBldmVyeSBjYWxsZXIgcmUtcmVuZGVyIG9uIHF1ZXVlIGNoYW5nZXMuXG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSByZWFjdC1ob29rcy9leGhhdXN0aXZlLWRlcHNcbiAgLy8gYmlvbWUtaWdub3JlIGxpbnQvY29ycmVjdG5lc3MvdXNlRXhoYXVzdGl2ZURlcGVuZGVuY2llczogbW91bnQtb25seSBlZmZlY3QsIHN0b3JlIGlzIGEgc3RhYmxlIGNvbnRleHQgcmVmXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKHN0b3JlLmdldFN0YXRlKCkubm90aWZpY2F0aW9ucy5xdWV1ZS5sZW5ndGggPiAwKSB7XG4gICAgICBwcm9jZXNzUXVldWUoKVxuICAgIH1cbiAgfSwgW10pXG5cbiAgcmV0dXJuIHsgYWRkTm90aWZpY2F0aW9uLCByZW1vdmVOb3RpZmljYXRpb24gfVxufVxuXG5jb25zdCBQUklPUklUSUVTOiBSZWNvcmQ8UHJpb3JpdHksIG51bWJlcj4gPSB7XG4gIGltbWVkaWF0ZTogMCxcbiAgaGlnaDogMSxcbiAgbWVkaXVtOiAyLFxuICBsb3c6IDMsXG59XG5leHBvcnQgZnVuY3Rpb24gZ2V0TmV4dChxdWV1ZTogTm90aWZpY2F0aW9uW10pOiBOb3RpZmljYXRpb24gfCB1bmRlZmluZWQge1xuICBpZiAocXVldWUubGVuZ3RoID09PSAwKSByZXR1cm4gdW5kZWZpbmVkXG4gIHJldHVybiBxdWV1ZS5yZWR1Y2UoKG1pbiwgbikgPT5cbiAgICBQUklPUklUSUVTW24ucHJpb3JpdHldIDwgUFJJT1JJVElFU1ttaW4ucHJpb3JpdHldID8gbiA6IG1pbixcbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxZQUFZLEtBQUtBLEtBQUssTUFBTSxPQUFPO0FBQ25DLFNBQVNDLFdBQVcsRUFBRUMsU0FBUyxRQUFRLE9BQU87QUFDOUMsU0FBU0MsZ0JBQWdCLEVBQUVDLGNBQWMsUUFBUSx1QkFBdUI7QUFDeEUsY0FBY0MsS0FBSyxRQUFRLG1CQUFtQjtBQUU5QyxLQUFLQyxRQUFRLEdBQUcsS0FBSyxHQUFHLFFBQVEsR0FBRyxNQUFNLEdBQUcsV0FBVztBQUV2RCxLQUFLQyxnQkFBZ0IsR0FBRztFQUN0QkMsR0FBRyxFQUFFLE1BQU07RUFDWDtBQUNGO0FBQ0E7QUFDQTtBQUNBO0VBQ0VDLFdBQVcsQ0FBQyxFQUFFLE1BQU0sRUFBRTtFQUN0QkMsUUFBUSxFQUFFSixRQUFRO0VBQ2xCSyxTQUFTLENBQUMsRUFBRSxNQUFNO0VBQ2xCO0FBQ0Y7QUFDQTtBQUNBO0FBQ0E7QUFDQTtFQUNFQyxJQUFJLENBQUMsRUFBRSxDQUFDQyxXQUFXLEVBQUVDLFlBQVksRUFBRUMsUUFBUSxFQUFFRCxZQUFZLEVBQUUsR0FBR0EsWUFBWTtBQUM1RSxDQUFDO0FBRUQsS0FBS0UsZ0JBQWdCLEdBQUdULGdCQUFnQixHQUFHO0VBQ3pDVSxJQUFJLEVBQUUsTUFBTTtFQUNaQyxLQUFLLENBQUMsRUFBRSxNQUFNYixLQUFLO0FBQ3JCLENBQUM7QUFFRCxLQUFLYyxlQUFlLEdBQUdaLGdCQUFnQixHQUFHO0VBQ3hDYSxHQUFHLEVBQUVwQixLQUFLLENBQUNxQixTQUFTO0FBQ3RCLENBQUM7QUFFRCxLQUFLQyxpQkFBaUIsR0FBRyxDQUFDQyxPQUFPLEVBQUVULFlBQVksRUFBRSxHQUFHLElB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 \ No newline at end of file diff --git a/src/context/overlayContext.tsx b/src/context/overlayContext.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0f20f62c12137bc4b456d639354b18ee63078f80 --- /dev/null +++ b/src/context/overlayContext.tsx @@ -0,0 +1,151 @@ +import { c as _c } from "react/compiler-runtime"; +/** + * Overlay tracking for Escape key coordination. + * + * This solves the problem of escape key handling when overlays (like Select with onCancel) + * are open. The CancelRequestHandler needs to know when an overlay is active so it doesn't + * cancel requests when the user just wants to dismiss the overlay. + * + * Usage: + * 1. Call useRegisterOverlay() in any overlay component to automatically register it + * 2. Call useIsOverlayActive() to check if any overlay is currently active + * + * The hook automatically registers on mount and unregisters on unmount, + * so no manual cleanup or state management is needed. + */ +import { useContext, useEffect, useLayoutEffect } from 'react'; +import instances from '../ink/instances.js'; +import { AppStoreContext, useAppState } from '../state/AppState.js'; + +// Non-modal overlays that shouldn't disable TextInput focus +const NON_MODAL_OVERLAYS = new Set(['autocomplete']); + +/** + * Hook to register a component as an active overlay. + * Automatically registers on mount and unregisters on unmount. + * + * @param id - Unique identifier for this overlay (e.g., 'select', 'multi-select') + * @param enabled - Whether to register (default: true). Use this to conditionally register + * based on component props, e.g., only register when onCancel is provided. + * + * @example + * // Conditional registration based on whether cancel is supported + * function useSelectInput({ state }) { + * useRegisterOverlay('select', !!state.onCancel) + * // ... + * } + */ +export function useRegisterOverlay(id, t0) { + const $ = _c(8); + const enabled = t0 === undefined ? true : t0; + const store = useContext(AppStoreContext); + const setAppState = store?.setState; + let t1; + let t2; + if ($[0] !== enabled || $[1] !== id || $[2] !== setAppState) { + t1 = () => { + if (!enabled || !setAppState) { + return; + } + setAppState(prev => { + if (prev.activeOverlays.has(id)) { + return prev; + } + const next = new Set(prev.activeOverlays); + next.add(id); + return { + ...prev, + activeOverlays: next + }; + }); + return () => { + setAppState(prev_0 => { + if (!prev_0.activeOverlays.has(id)) { + return prev_0; + } + const next_0 = new Set(prev_0.activeOverlays); + next_0.delete(id); + return { + ...prev_0, + activeOverlays: next_0 + }; + }); + }; + }; + t2 = [id, enabled, setAppState]; + $[0] = enabled; + $[1] = id; + $[2] = setAppState; + $[3] = t1; + $[4] = t2; + } else { + t1 = $[3]; + t2 = $[4]; + } + useEffect(t1, t2); + let t3; + let t4; + if ($[5] !== enabled) { + t3 = () => { + if (!enabled) { + return; + } + return _temp; + }; + t4 = [enabled]; + $[5] = enabled; + $[6] = t3; + $[7] = t4; + } else { + t3 = $[6]; + t4 = $[7]; + } + useLayoutEffect(t3, t4); +} + +/** + * Hook to check if any overlay is currently active. + * This is reactive - the component will re-render when the overlay state changes. + * + * @returns true if any overlay is currently active + * + * @example + * function CancelRequestHandler() { + * const isOverlayActive = useIsOverlayActive() + * const isActive = !isOverlayActive && canCancelRunningTask + * useKeybinding('chat:cancel', handleCancel, { isActive }) + * } + */ +function _temp() { + return instances.get(process.stdout)?.invalidatePrevFrame(); +} +export function useIsOverlayActive() { + return useAppState(_temp2); +} + +/** + * Hook to check if any modal overlay is currently active. + * Modal overlays are overlays that should capture all input (like Select dialogs). + * Non-modal overlays (like autocomplete) don't disable TextInput focus. + * + * @returns true if any modal overlay is currently active + * + * @example + * // Use for TextInput focus - allows typing during autocomplete + * focus: !isSearchingHistory && !isModalOverlayActive + */ +function _temp2(s) { + return s.activeOverlays.size > 0; +} +export function useIsModalOverlayActive() { + return useAppState(_temp3); +} +function _temp3(s) { + for (const id of s.activeOverlays) { + if (!NON_MODAL_OVERLAYS.has(id)) { + return true; + } + } + return false; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJ1c2VDb250ZXh0IiwidXNlRWZmZWN0IiwidXNlTGF5b3V0RWZmZWN0IiwiaW5zdGFuY2VzIiwiQXBwU3RvcmVDb250ZXh0IiwidXNlQXBwU3RhdGUiLCJOT05fTU9EQUxfT1ZFUkxBWVMiLCJTZXQiLCJ1c2VSZWdpc3Rlck92ZXJsYXkiLCJpZCIsInQwIiwiJCIsIl9jIiwiZW5hYmxlZCIsInVuZGVmaW5lZCIsInN0b3JlIiwic2V0QXBwU3RhdGUiLCJzZXRTdGF0ZSIsInQxIiwidDIiLCJwcmV2IiwiYWN0aXZlT3ZlcmxheXMiLCJoYXMiLCJuZXh0IiwiYWRkIiwicHJldl8wIiwibmV4dF8wIiwiZGVsZXRlIiwidDMiLCJ0NCIsIl90ZW1wIiwiZ2V0IiwicHJvY2VzcyIsInN0ZG91dCIsImludmFsaWRhdGVQcmV2RnJhbWUiLCJ1c2VJc092ZXJsYXlBY3RpdmUiLCJfdGVtcDIiLCJzIiwic2l6ZSIsInVzZUlzTW9kYWxPdmVybGF5QWN0aXZlIiwiX3RlbXAzIl0sInNvdXJjZXMiOlsib3ZlcmxheUNvbnRleHQudHN4Il0sInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogT3ZlcmxheSB0cmFja2luZyBmb3IgRXNjYXBlIGtleSBjb29yZGluYXRpb24uXG4gKlxuICogVGhpcyBzb2x2ZXMgdGhlIHByb2JsZW0gb2YgZXNjYXBlIGtleSBoYW5kbGluZyB3aGVuIG92ZXJsYXlzIChsaWtlIFNlbGVjdCB3aXRoIG9uQ2FuY2VsKVxuICogYXJlIG9wZW4uIFRoZSBDYW5jZWxSZXF1ZXN0SGFuZGxlciBuZWVkcyB0byBrbm93IHdoZW4gYW4gb3ZlcmxheSBpcyBhY3RpdmUgc28gaXQgZG9lc24ndFxuICogY2FuY2VsIHJlcXVlc3RzIHdoZW4gdGhlIHVzZXIganVzdCB3YW50cyB0byBkaXNtaXNzIHRoZSBvdmVybGF5LlxuICpcbiAqIFVzYWdlOlxuICogMS4gQ2FsbCB1c2VSZWdpc3Rlck92ZXJsYXkoKSBpbiBhbnkgb3ZlcmxheSBjb21wb25lbnQgdG8gYXV0b21hdGljYWxseSByZWdpc3RlciBpdFxuICogMi4gQ2FsbCB1c2VJc092ZXJsYXlBY3RpdmUoKSB0byBjaGVjayBpZiBhbnkgb3ZlcmxheSBpcyBjdXJyZW50bHkgYWN0aXZlXG4gKlxuICogVGhlIGhvb2sgYXV0b21hdGljYWxseSByZWdpc3RlcnMgb24gbW91bnQgYW5kIHVucmVnaXN0ZXJzIG9uIHVubW91bnQsXG4gKiBzbyBubyBtYW51YWwgY2xlYW51cCBvciBzdGF0ZSBtYW5hZ2VtZW50IGlzIG5lZWRlZC5cbiAqL1xuaW1wb3J0IHsgdXNlQ29udGV4dCwgdXNlRWZmZWN0LCB1c2VMYXlvdXRFZmZlY3QgfSBmcm9tICdyZWFjdCdcbmltcG9ydCBpbnN0YW5jZXMgZnJvbSAnLi4vaW5rL2luc3RhbmNlcy5qcydcbmltcG9ydCB7IEFwcFN0b3JlQ29udGV4dCwgdXNlQXBwU3RhdGUgfSBmcm9tICcuLi9zdGF0ZS9BcHBTdGF0ZS5qcydcblxuLy8gTm9uLW1vZGFsIG92ZXJsYXlzIHRoYXQgc2hvdWxkbid0IGRpc2FibGUgVGV4dElucHV0IGZvY3VzXG5jb25zdCBOT05fTU9EQUxfT1ZFUkxBWVMgPSBuZXcgU2V0KFsnYXV0b2NvbXBsZXRlJ10pXG5cbi8qKlxuICogSG9vayB0byByZWdpc3RlciBhIGNvbXBvbmVudCBhcyBhbiBhY3RpdmUgb3ZlcmxheS5cbiAqIEF1dG9tYXRpY2FsbHkgcmVnaXN0ZXJzIG9uIG1vdW50IGFuZCB1bnJlZ2lzdGVycyBvbiB1bm1vdW50LlxuICpcbiAqIEBwYXJhbSBpZCAtIFVuaXF1ZSBpZGVudGlmaWVyIGZvciB0aGlzIG92ZXJsYXkgKGUuZy4sICdzZWxlY3QnLCAnbXVsdGktc2VsZWN0JylcbiAqIEBwYXJhbSBlbmFibGVkIC0gV2hldGhlciB0byByZWdpc3RlciAoZGVmYXVsdDogdHJ1ZSkuIFVzZSB0aGlzIHRvIGNvbmRpdGlvbmFsbHkgcmVnaXN0ZXJcbiAqICAgICAgICAgICAgICAgICAgYmFzZWQgb24gY29tcG9uZW50IHByb3BzLCBlLmcuLCBvbmx5IHJlZ2lzdGVyIHdoZW4gb25DYW5jZWwgaXMgcHJvdmlkZWQuXG4gKlxuICogQGV4YW1wbGVcbiAqIC8vIENvbmRpdGlvbmFsIHJlZ2lzdHJhdGlvbiBiYXNlZCBvbiB3aGV0aGVyIGNhbmNlbCBpcyBzdXBwb3J0ZWRcbiAqIGZ1bmN0aW9uIHVzZVNlbGVjdElucHV0KHsgc3RhdGUgfSkge1xuICogICB1c2VSZWdpc3Rlck92ZXJsYXkoJ3NlbGVjdCcsICEhc3RhdGUub25DYW5jZWwpXG4gKiAgIC8vIC4uLlxuICogfVxuICovXG5leHBvcnQgZnVuY3Rpb24gdXNlUmVnaXN0ZXJPdmVybGF5KGlkOiBzdHJpbmcsIGVuYWJsZWQgPSB0cnVlKTogdm9pZCB7XG4gIC8vIFVzZSBjb250ZXh0IGRpcmVjdGx5IHNvIHRoaXMgaXMgYSBuby1vcCB3aGVuIHJlbmRlcmVkIG91dHNpZGUgQXBwU3RhdGVQcm92aWRlclxuICAvLyAoZS5nLiwgaW4gaXNvbGF0ZWQgY29tcG9uZW50IHRlc3RzIHRoYXQgZG9uJ3QgbmVlZCB0aGUgZnVsbCBhcHAgc3RhdGUgdHJlZSkuXG4gIGNvbnN0IHN0b3JlID0gdXNlQ29udGV4dChBcHBTdG9yZUNvbnRleHQpXG4gIGNvbnN0IHNldEFwcFN0YXRlID0gc3RvcmU/LnNldFN0YXRlXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKCFlbmFibGVkIHx8ICFzZXRBcHBTdGF0ZSkgcmV0dXJuXG4gICAgc2V0QXBwU3RhdGUocHJldiA9PiB7XG4gICAgICBpZiAocHJldi5hY3RpdmVPdmVybGF5cy5oYXMoaWQpKSByZXR1cm4gcHJldlxuICAgICAgY29uc3QgbmV4dCA9IG5ldyBTZXQocHJldi5hY3RpdmVPdmVybGF5cylcbiAgICAgIG5leHQuYWRkKGlkKVxuICAgICAgcmV0dXJuIHsgLi4ucHJldiwgYWN0aXZlT3ZlcmxheXM6IG5leHQgfVxuICAgIH0pXG4gICAgcmV0dXJuICgpID0+IHtcbiAgICAgIHNldEFwcFN0YXRlKHByZXYgPT4ge1xuICAgICAgICBpZiAoIXByZXYuYWN0aXZlT3ZlcmxheXMuaGFzKGlkKSkgcmV0dXJuIHByZXZcbiAgICAgICAgY29uc3QgbmV4dCA9IG5ldyBTZXQocHJldi5hY3RpdmVPdmVybGF5cylcbiAgICAgICAgbmV4dC5kZWxldGUoaWQpXG4gICAgICAgIHJldHVybiB7IC4uLnByZXYsIGFjdGl2ZU92ZXJsYXlzOiBuZXh0IH1cbiAgICAgIH0pXG4gICAgfVxuICB9LCBbaWQsIGVuYWJsZWQsIHNldEFwcFN0YXRlXSlcblxuICAvLyBPbiBvdmVybGF5IGNsb3NlLCBmb3JjZSB0aGUgbmV4dCByZW5kZXIgdG8gZnVsbC1kYW1hZ2UgZGlmZiBpbnN0ZWFkXG4gIC8vIG9mIGJsaXQuIEEgdGFsbCBvdmVybGF5IChlLmcuIEZ1enp5UGlja2VyIHdpdGggYSAyMC1saW5lIHByZXZpZXcpXG4gIC8vIHNocmlua3MgdGhlIEluay1tYW5hZ2VkIHJlZ2lvbiBvbiB1bm1vdW50OyB0aGUgYmxpdCBmYXN0IHBhdGggY2FuXG4gIC8vIGNvcHkgc3RhbGUgY2VsbHMgZnJvbSB0aGUgb3ZlcmxheSdzIHByZXZpb3VzIGZyYW1lIGludG8gcm93cyB0aGVcbiAgLy8gc2hvcnRlciBsYXlvdXQgbm8gbG9uZ2VyIHJlYWNoZXMsIGxlYXZpbmcgYSBnaG9zdCB0aXRsZS9kaXZpZGVyLlxuICAvLyB1c2VMYXlvdXRFZmZlY3Qgc28gY2xlYW51cCBydW5zIHN5bmNocm9ub3VzbHkgYmVmb3JlIHRoZSBtaWNyb3Rhc2stXG4gIC8vIGRlZmVycmVkIG9uUmVuZGVyIChzY2hlZHVsZVJlbmRlciBxdWV1ZXMgYSBtaWNyb3Rhc2sgZnJvbVxuICAvLyByZXNldEFmdGVyQ29tbWl0OyBwYXNzaXZlLWVmZmVjdCBjbGVhbnVwIHdvdWxkIGxhbmQgYWZ0ZXIgaXQpLlxuICB1c2VMYXlvdXRFZmZlY3QoKCkgPT4ge1xuICAgIGlmICghZW5hYmxlZCkgcmV0dXJuXG4gICAgcmV0dXJuICgpID0+IGluc3RhbmNlcy5nZXQocHJvY2Vzcy5zdGRvdXQpPy5pbnZhbGlkYXRlUHJldkZyYW1lKClcbiAgfSwgW2VuYWJsZWRdKVxufVxuXG4vKipcbiAqIEhvb2sgdG8gY2hlY2sgaWYgYW55IG92ZXJsYXkgaXMgY3VycmVudGx5IGFjdGl2ZS5cbiAqIFRoaXMgaXMgcmVhY3RpdmUgLSB0aGUgY29tcG9uZW50IHdpbGwgcmUtcmVuZGVyIHdoZW4gdGhlIG92ZXJsYXkgc3RhdGUgY2hhbmdlcy5cbiAqXG4gKiBAcmV0dXJucyB0cnVlIGlmIGFueSBvdmVybGF5IGlzIGN1cnJlbnRseSBhY3RpdmVcbiAqXG4gKiBAZXhhbXBsZVxuICogZnVuY3Rpb24gQ2FuY2VsUmVxdWVzdEhhbmRsZXIoKSB7XG4gKiAgIGNvbnN0IGlzT3ZlcmxheUFjdGl2ZSA9IHVzZUlzT3ZlcmxheUFjdGl2ZSgpXG4gKiAgIGNvbnN0IGlzQWN0aXZlID0gIWlzT3ZlcmxheUFjdGl2ZSAmJiBjYW5DYW5jZWxSdW5uaW5nVGFza1xuICogICB1c2VLZXliaW5kaW5nKCdjaGF0OmNhbmNlbCcsIGhhbmRsZUNhbmNlbCwgeyBpc0FjdGl2ZSB9KVxuICogfVxuICovXG5leHBvcnQgZnVuY3Rpb24gdXNlSXNPdmVybGF5QWN0aXZlKCk6IGJvb2xlYW4ge1xuICByZXR1cm4gdXNlQXBwU3RhdGUocyA9PiBzLmFjdGl2ZU92ZXJsYXlzLnNpemUgPiAwKVxufVxuXG4vKipcbiAqIEhvb2sgdG8gY2hlY2sgaWYgYW55IG1vZGFsIG92ZXJsYXkgaXMgY3VycmVudGx5IGFjdGl2ZS5cbiAqIE1vZGFsIG92ZXJsYXlzIGFyZSBvdmVybGF5cyB0aGF0IHNob3VsZCBjYXB0dXJlIGFsbCBpbnB1dCAobGlrZSBTZWxlY3QgZGlhbG9ncykuXG4gKiBOb24tbW9kYWwgb3ZlcmxheXMgKGxpa2UgYXV0b2NvbXBsZXRlKSBkb24ndCBkaXNhYmxlIFRleHRJbnB1dCBmb2N1cy5cbiAqXG4gKiBAcmV0dXJucyB0cnVlIGlmIGFueSBtb2RhbCBvdmVybGF5IGlzIGN1cnJlbnRseSBhY3RpdmVcbiAqXG4gKiBAZXhhbXBsZVxuICogLy8gVXNlIGZvciBUZXh0SW5wdXQgZm9jdXMgLSBhbGxvd3MgdHlwaW5nIGR1cmluZyBhdXRvY29tcGxldGVcbiAqIGZvY3VzOiAhaXNTZWFyY2hpbmdIaXN0b3J5ICYmICFpc01vZGFsT3ZlcmxheUFjdGl2ZVxuICovXG5leHBvcnQgZnVuY3Rpb24gdXNlSXNNb2RhbE92ZXJsYXlBY3RpdmUoKTogYm9vbGVhbiB7XG4gIHJldHVybiB1c2VBcHBTdGF0ZShzID0+IHtcbiAgICBmb3IgKGNvbnN0IGlkIG9mIHMuYWN0aXZlT3ZlcmxheXMpIHtcbiAgICAgIGlmICghTk9OX01PREFMX09WRVJMQVlTLmhhcyhpZCkpIHJldHVybiB0cnVlXG4gICAgfVxuICAgIHJldHVybiBmYWxzZVxuICB9KVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFNBQVNBLFVBQVUsRUFBRUMsU0FBUyxFQUFFQyxlQUFlLFFBQVEsT0FBTztBQUM5RCxPQUFPQyxTQUFTLE1BQU0scUJBQXFCO0FBQzNDLFNBQVNDLGVBQWUsRUFBRUMsV0FBVyxRQUFRLHNCQUFzQjs7QUFFbkU7QUFDQSxNQUFNQyxrQkFBa0IsR0FBRyxJQUFJQyxHQUFHLENBQUMsQ0FBQyxjQUFjLENBQUMsQ0FBQzs7QUFFcEQ7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsT0FBTyxTQUFBQyxtQkFBQUMsRUFBQSxFQUFBQyxFQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQXdDLE1BQUFDLE9BQUEsR0FBQUgsRUFBYyxLQUFkSSxTQUFjLEdBQWQsSUFBYyxHQUFkSixFQUFjO0VBRzNELE1BQUFLLEtBQUEsR0FBY2YsVUFBVSxDQUFDSSxlQUFlLENBQUM7RUFDekMsTUFBQVksV0FBQSxHQUFvQkQsS0FBSyxFQUFBRSxRQUFVO0VBQUEsSUFBQUMsRUFBQTtFQUFBLElB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 \ No newline at end of file diff --git a/src/context/voice.tsx b/src/context/voice.tsx new file mode 100644 index 0000000000000000000000000000000000000000..04e14ddc16a603aa5426a5eaa49816a9aa161289 --- /dev/null +++ b/src/context/voice.tsx @@ -0,0 +1,88 @@ +import { c as _c } from "react/compiler-runtime"; +import React, { createContext, useContext, useState, useSyncExternalStore } from 'react'; +import { createStore, type Store } from '../state/store.js'; +export type VoiceState = { + voiceState: 'idle' | 'recording' | 'processing'; + voiceError: string | null; + voiceInterimTranscript: string; + voiceAudioLevels: number[]; + voiceWarmingUp: boolean; +}; +const DEFAULT_STATE: VoiceState = { + voiceState: 'idle', + voiceError: null, + voiceInterimTranscript: '', + voiceAudioLevels: [], + voiceWarmingUp: false +}; +type VoiceStore = Store; +const VoiceContext = createContext(null); +type Props = { + children: React.ReactNode; +}; +export function VoiceProvider(t0) { + const $ = _c(3); + const { + children + } = t0; + const [store] = useState(_temp); + let t1; + if ($[0] !== children || $[1] !== store) { + t1 = {children}; + $[0] = children; + $[1] = store; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} +function _temp() { + return createStore(DEFAULT_STATE); +} +function useVoiceStore() { + const store = useContext(VoiceContext); + if (!store) { + throw new Error("useVoiceState must be used within a VoiceProvider"); + } + return store; +} + +/** + * Subscribe to a slice of voice state. Only re-renders when the selected + * value changes (compared via Object.is). + */ +export function useVoiceState(selector) { + const $ = _c(3); + const store = useVoiceStore(); + let t0; + if ($[0] !== selector || $[1] !== store) { + t0 = () => selector(store.getState()); + $[0] = selector; + $[1] = store; + $[2] = t0; + } else { + t0 = $[2]; + } + const get = t0; + return useSyncExternalStore(store.subscribe, get, get); +} + +/** + * Get the voice state setter. Stable reference — never causes re-renders. + * store.setState is synchronous: callers can read getVoiceState() immediately + * after to observe the new value (VoiceKeybindingHandler relies on this). + */ +export function useSetVoiceState() { + return useVoiceStore().setState; +} + +/** + * Get a synchronous reader for fresh state inside callbacks. Unlike + * useVoiceState (which subscribes), this doesn't cause re-renders — use + * inside event handlers that need to read state set earlier in the same tick. + */ +export function useGetVoiceState() { + return useVoiceStore().getState; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsImNyZWF0ZUNvbnRleHQiLCJ1c2VDb250ZXh0IiwidXNlU3RhdGUiLCJ1c2VTeW5jRXh0ZXJuYWxTdG9yZSIsImNyZWF0ZVN0b3JlIiwiU3RvcmUiLCJWb2ljZVN0YXRlIiwidm9pY2VTdGF0ZSIsInZvaWNlRXJyb3IiLCJ2b2ljZUludGVyaW1UcmFuc2NyaXB0Iiwidm9pY2VBdWRpb0xldmVscyIsInZvaWNlV2FybWluZ1VwIiwiREVGQVVMVF9TVEFURSIsIlZvaWNlU3RvcmUiLCJWb2ljZUNvbnRleHQiLCJQcm9wcyIsImNoaWxkcmVuIiwiUmVhY3ROb2RlIiwiVm9pY2VQcm92aWRlciIsInQwIiwiJCIsIl9jIiwic3RvcmUiLCJfdGVtcCIsInQxIiwidXNlVm9pY2VTdG9yZSIsIkVycm9yIiwidXNlVm9pY2VTdGF0ZSIsInNlbGVjdG9yIiwiZ2V0U3RhdGUiLCJnZXQiLCJzdWJzY3JpYmUiLCJ1c2VTZXRWb2ljZVN0YXRlIiwic2V0U3RhdGUiLCJ1c2VHZXRWb2ljZVN0YXRlIl0sInNvdXJjZXMiOlsidm9pY2UudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBSZWFjdCwge1xuICBjcmVhdGVDb250ZXh0LFxuICB1c2VDb250ZXh0LFxuICB1c2VTdGF0ZSxcbiAgdXNlU3luY0V4dGVybmFsU3RvcmUsXG59IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgY3JlYXRlU3RvcmUsIHR5cGUgU3RvcmUgfSBmcm9tICcuLi9zdGF0ZS9zdG9yZS5qcydcblxuZXhwb3J0IHR5cGUgVm9pY2VTdGF0ZSA9IHtcbiAgdm9pY2VTdGF0ZTogJ2lkbGUnIHwgJ3JlY29yZGluZycgfCAncHJvY2Vzc2luZydcbiAgdm9pY2VFcnJvcjogc3RyaW5nIHwgbnVsbFxuICB2b2ljZUludGVyaW1UcmFuc2NyaXB0OiBzdHJpbmdcbiAgdm9pY2VBdWRpb0xldmVsczogbnVtYmVyW11cbiAgdm9pY2VXYXJtaW5nVXA6IGJvb2xlYW5cbn1cblxuY29uc3QgREVGQVVMVF9TVEFURTogVm9pY2VTdGF0ZSA9IHtcbiAgdm9pY2VTdGF0ZTogJ2lkbGUnLFxuICB2b2ljZUVycm9yOiBudWxsLFxuICB2b2ljZUludGVyaW1UcmFuc2NyaXB0OiAnJyxcbiAgdm9pY2VBdWRpb0xldmVsczogW10sXG4gIHZvaWNlV2FybWluZ1VwOiBmYWxzZSxcbn1cblxudHlwZSBWb2ljZVN0b3JlID0gU3RvcmU8Vm9pY2VTdGF0ZT5cblxuY29uc3QgVm9pY2VDb250ZXh0ID0gY3JlYXRlQ29udGV4dDxWb2ljZVN0b3JlIHwgbnVsbD4obnVsbClcblxudHlwZSBQcm9wcyA9IHtcbiAgY2hpbGRyZW46IFJlYWN0LlJlYWN0Tm9kZVxufVxuXG5leHBvcnQgZnVuY3Rpb24gVm9pY2VQcm92aWRlcih7IGNoaWxkcmVuIH06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgLy8gU3RvcmUgaXMgY3JlYXRlZCBvbmNlIOKAlCBzdGFibGUgY29udGV4dCB2YWx1ZSBtZWFucyB0aGUgcHJvdmlkZXIgbmV2ZXJcbiAgLy8gdHJpZ2dlcnMgcmUtcmVuZGVycy4gQ29uc3VtZXJzIHN1YnNjcmliZSB0byBzbGljZXMgdmlhIHVzZVZvaWNlU3RhdGUuXG4gIGNvbnN0IFtzdG9yZV0gPSB1c2VTdGF0ZSgoKSA9PiBjcmVhdGVTdG9yZTxWb2ljZVN0YXRlPihERUZBVUxUX1NUQVRFKSlcbiAgcmV0dXJuIDxWb2ljZUNvbnRleHQuUHJvdmlkZXIgdmFsdWU9e3N0b3JlfT57Y2hpbGRyZW59PC9Wb2ljZUNvbnRleHQuUHJvdmlkZXI+XG59XG5cbmZ1bmN0aW9uIHVzZVZvaWNlU3RvcmUoKTogVm9pY2VTdG9yZSB7XG4gIGNvbnN0IHN0b3JlID0gdXNlQ29udGV4dChWb2ljZUNvbnRleHQpXG4gIGlmICghc3RvcmUpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ3VzZVZvaWNlU3RhdGUgbXVzdCBiZSB1c2VkIHdpdGhpbiBhIFZvaWNlUHJvdmlkZXInKVxuICB9XG4gIHJldHVybiBzdG9yZVxufVxuXG4vKipcbiAqIFN1YnNjcmliZSB0byBhIHNsaWNlIG9mIHZvaWNlIHN0YXRlLiBPbmx5IHJlLXJlbmRlcnMgd2hlbiB0aGUgc2VsZWN0ZWRcbiAqIHZhbHVlIGNoYW5nZXMgKGNvbXBhcmVkIHZpYSBPYmplY3QuaXMpLlxuICovXG5leHBvcnQgZnVuY3Rpb24gdXNlVm9pY2VTdGF0ZTxUPihzZWxlY3RvcjogKHN0YXRlOiBWb2ljZVN0YXRlKSA9PiBUKTogVCB7XG4gIGNvbnN0IHN0b3JlID0gdXNlVm9pY2VTdG9yZSgpXG4gIGNvbnN0IGdldCA9ICgpID0+IHNlbGVjdG9yKHN0b3JlLmdldFN0YXRlKCkpXG4gIHJldHVybiB1c2VTeW5jRXh0ZXJuYWxTdG9yZShzdG9yZS5zdWJzY3JpYmUsIGdldCwgZ2V0KVxufVxuXG4vKipcbiAqIEdldCB0aGUgdm9pY2Ugc3RhdGUgc2V0dGVyLiBTdGFibGUgcmVmZXJlbmNlIOKAlCBuZXZlciBjYXVzZXMgcmUtcmVuZGVycy5cbiAqIHN0b3JlLnNldFN0YXRlIGlzIHN5bmNocm9ub3VzOiBjYWxsZXJzIGNhbiByZWFkIGdldFZvaWNlU3RhdGUoKSBpbW1lZGlhdGVseVxuICogYWZ0ZXIgdG8gb2JzZXJ2ZSB0aGUgbmV3IHZhbHVlIChWb2ljZUtleWJpbmRpbmdIYW5kbGVyIHJlbGllcyBvbiB0aGlzKS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHVzZVNldFZvaWNlU3RhdGUoKTogKFxuICB1cGRhdGVyOiAocHJldjogVm9pY2VTdGF0ZSkgPT4gVm9pY2VTdGF0ZSxcbikgPT4gdm9pZCB7XG4gIHJldHVybiB1c2VWb2ljZVN0b3JlKCkuc2V0U3RhdGVcbn1cblxuLyoqXG4gKiBHZXQgYSBzeW5jaHJvbm91cyByZWFkZXIgZm9yIGZyZXNoIHN0YXRlIGluc2lkZSBjYWxsYmFja3MuIFVubGlrZVxuICogdXNlVm9pY2VTdGF0ZSAod2hpY2ggc3Vic2NyaWJlcyksIHRoaXMgZG9lc24ndCBjYXVzZSByZS1yZW5kZXJzIOKAlCB1c2VcbiAqIGluc2lkZSBldmVudCBoYW5kbGVycyB0aGF0IG5lZWQgdG8gcmVhZCBzdGF0ZSBzZXQgZWFybGllciBpbiB0aGUgc2FtZSB0aWNrLlxuICovXG5leHBvcnQgZnVuY3Rpb24gdXNlR2V0Vm9pY2VTdGF0ZSgpOiAoKSA9PiBWb2ljZVN0YXRlIHtcbiAgcmV0dXJuIHVzZVZvaWNlU3RvcmUoKS5nZXRTdGF0ZVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBT0EsS0FBSyxJQUNWQyxhQUFhLEVBQ2JDLFVBQVUsRUFDVkMsUUFBUSxFQUNSQyxvQkFBb0IsUUFDZixPQUFPO0FBQ2QsU0FBU0MsV0FBVyxFQUFFLEtBQUtDLEtBQUssUUFBUSxtQkFBbUI7QUFFM0QsT0FBTyxLQUFLQyxVQUFVLEdBQUc7RUFDdkJDLFVBQVUsRUFBRSxNQUFNLEdBQUcsV0FBVyxHQUFHLFlBQVk7RUFDL0NDLFVBQVUsRUFBRSxNQUFNLEdBQUcsSUFBSTtFQUN6QkMsc0JBQXNCLEVBQUUsTUFBTTtFQUM5QkMsZ0JBQWdCLEVBQUUsTUFBTSxFQUFFO0VBQzFCQyxjQUFjLEVBQUUsT0FBTztBQUN6QixDQUFDO0FBRUQsTUFBTUMsYUFBYSxFQUFFTixVQUFVLEdBQUc7RUFDaENDLFVBQVUsRUFBRSxNQUFNO0VBQ2xCQyxVQUFVLEVBQUUsSUFBSTtFQUNoQkMsc0JBQXNCLEVBQUUsRUFBRTtFQUMxQkMsZ0JBQWdCLEVBQUUsRUFBRTtFQUNwQkMsY0FBYyxFQUFFO0FBQ2xCLENBQUM7QUFFRCxLQUFLRSxVQUFVLEdBQUdSLEtBQUssQ0FBQ0MsVUFBVSxDQUFDO0FBRW5DLE1BQU1RLFlBQVksR0FBR2QsYUFBYSxDQUFDYSxVQUFVLEdBQUcsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDO0FBRTNELEtBQUtFLEtBQUssR0FBRztFQUNYQyxRQUFRLEVBQUVqQixLQUFLLENBQUNrQixTQUFTO0FBQzNCLENBQUM7QUFFRCxPQUFPLFNBQUFDLGNBQUFDLEVBQUE7RUFBQSxNQUFBQyxDQUFBLEdBQUFDLEVBQUE7RUFBdUI7SUFBQUw7RUFBQSxJQUFBRyxFQUFtQjtFQUcvQyxPQUFBRyxLQUFBLElBQWdCcEIsUUFBUSxDQUFDcUIsS0FBNEMsQ0FBQztFQUFBLElBQUFDLEVBQUE7RUFBQSxJQUFBSixDQUFBLFFBQUFKLFFBQUEsSUFBQUksQ0FBQSxRQUFBRSxLQUFBO0lBQy9ERSxFQUFBLDBCQUE4QkYsS0FBSyxDQUFMQSxNQUFJLENBQUMsQ0FBR04sU0FBTyxDQUFFLHdCQUF3QjtJQUFBSSxDQUFBLE1BQUFKLFFBQUE7SUFBQUksQ0FBQSxNQUFBRSxLQUFBO0lBQUFGLENBQUEsTUFBQUksRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUosQ0FBQTtFQUFBO0VBQUEsT0FBdkVJLEVBQXVFO0FBQUE7QUFKekUsU0FBQUQsTUFBQTtFQUFBLE9BRzBCbkIsV0FBVyxDQUFhUSxhQUFhLENBQUM7QUFBQTtBQUl2RSxTQUFBYSxjQUFBO0VBQ0UsTUFBQUgsS0FBQSxHQUFjckIsVUFBVSxDQUFDYSxZQUFZLENBQUM7RUFDdEMsSUFBSSxDQUFDUSxLQUFLO0lBQ1IsTUFBTSxJQUFJSSxLQUFLLENBQUMsbURBQW1ELENBQUM7RUFBQTtFQUNyRSxPQUNNSixLQUFLO0FBQUE7O0FBR2Q7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPLFNBQUFLLGNBQUFDLFFBQUE7RUFBQSxNQUFBUixDQUFBLEdBQUFDLEVBQUE7RUFDTCxNQUFBQyxLQUFBLEdBQWNHLGFBQWEsQ0FBQyxDQUFDO0VBQUEsSUFBQU4sRUFBQTtFQUFBLElBQUFDLENBQUEsUUFBQVEsUUFBQSxJQUFBUixDQUFBLFFBQUFFLEtBQUE7SUFDakJILEVBQUEsR0FBQUEsQ0FBQSxLQUFNUyxRQUFRLENBQUNOLEtBQUssQ0FBQU8sUUFBUyxDQUFDLENBQUMsQ0FBQztJQUFBVCxDQUFBLE1BQUFRLFFBQUE7SUFBQVIsQ0FBQSxNQUFBRSxLQUFBO0lBQUFGLENBQUEsTUFBQUQsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUMsQ0FBQTtFQUFBO0VBQTVDLE1BQUFVLEdBQUEsR0FBWVgsRUFBZ0M7RUFBQSxPQUNyQ2hCLG9CQUFvQixDQUFDbUIsS0FBSyxDQUFBUyxTQUFVLEVBQUVELEdBQUcsRUFBRUEsR0FBRyxDQUFDO0FBQUE7O0FBR3hEO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPLFNBQUFFLGlCQUFBO0VBQUEsT0FHRVAsYUFBYSxDQUFDLENBQUMsQ0FBQVEsUUFBUztBQUFBOztBQUdqQztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsT0FBTyxTQUFBQyxpQkFBQTtFQUFBLE9BQ0VULGFBQWEsQ0FBQyxDQUFDLENBQUFJLFFBQVM7QUFBQSIsImlnbm9yZUxpc3QiOltdfQ== \ No newline at end of file diff --git a/src/coordinator/coordinatorMode.ts b/src/coordinator/coordinatorMode.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc5dc4eb174d8895a7698cb74381d75bfd12868a --- /dev/null +++ b/src/coordinator/coordinatorMode.ts @@ -0,0 +1,369 @@ +import { feature } from 'bun:bundle' +import { ASYNC_AGENT_ALLOWED_TOOLS } from '../constants/tools.js' +import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' +import { + type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + logEvent, +} from '../services/analytics/index.js' +import { AGENT_TOOL_NAME } from '../tools/AgentTool/constants.js' +import { BASH_TOOL_NAME } from '../tools/BashTool/toolName.js' +import { FILE_EDIT_TOOL_NAME } from '../tools/FileEditTool/constants.js' +import { FILE_READ_TOOL_NAME } from '../tools/FileReadTool/prompt.js' +import { SEND_MESSAGE_TOOL_NAME } from '../tools/SendMessageTool/constants.js' +import { SYNTHETIC_OUTPUT_TOOL_NAME } from '../tools/SyntheticOutputTool/SyntheticOutputTool.js' +import { TASK_STOP_TOOL_NAME } from '../tools/TaskStopTool/prompt.js' +import { TEAM_CREATE_TOOL_NAME } from '../tools/TeamCreateTool/constants.js' +import { TEAM_DELETE_TOOL_NAME } from '../tools/TeamDeleteTool/constants.js' +import { isEnvTruthy } from '../utils/envUtils.js' + +// Checks the same gate as isScratchpadEnabled() in +// utils/permissions/filesystem.ts. Duplicated here because importing +// filesystem.ts creates a circular dependency (filesystem -> permissions +// -> ... -> coordinatorMode). The actual scratchpad path is passed in via +// getCoordinatorUserContext's scratchpadDir parameter (dependency injection +// from QueryEngine.ts, which lives higher in the dep graph). +function isScratchpadGateEnabled(): boolean { + return checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_scratch') +} + +const INTERNAL_WORKER_TOOLS = new Set([ + TEAM_CREATE_TOOL_NAME, + TEAM_DELETE_TOOL_NAME, + SEND_MESSAGE_TOOL_NAME, + SYNTHETIC_OUTPUT_TOOL_NAME, +]) + +export function isCoordinatorMode(): boolean { + if (feature('COORDINATOR_MODE')) { + return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) + } + return false +} + +/** + * Checks if the current coordinator mode matches the session's stored mode. + * If mismatched, flips the environment variable so isCoordinatorMode() returns + * the correct value for the resumed session. Returns a warning message if + * the mode was switched, or undefined if no switch was needed. + */ +export function matchSessionMode( + sessionMode: 'coordinator' | 'normal' | undefined, +): string | undefined { + // No stored mode (old session before mode tracking) — do nothing + if (!sessionMode) { + return undefined + } + + const currentIsCoordinator = isCoordinatorMode() + const sessionIsCoordinator = sessionMode === 'coordinator' + + if (currentIsCoordinator === sessionIsCoordinator) { + return undefined + } + + // Flip the env var — isCoordinatorMode() reads it live, no caching + if (sessionIsCoordinator) { + process.env.CLAUDE_CODE_COORDINATOR_MODE = '1' + } else { + delete process.env.CLAUDE_CODE_COORDINATOR_MODE + } + + logEvent('tengu_coordinator_mode_switched', { + to: sessionMode as unknown as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + }) + + return sessionIsCoordinator + ? 'Entered coordinator mode to match resumed session.' + : 'Exited coordinator mode to match resumed session.' +} + +export function getCoordinatorUserContext( + mcpClients: ReadonlyArray<{ name: string }>, + scratchpadDir?: string, +): { [k: string]: string } { + if (!isCoordinatorMode()) { + return {} + } + + const workerTools = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE) + ? [BASH_TOOL_NAME, FILE_READ_TOOL_NAME, FILE_EDIT_TOOL_NAME] + .sort() + .join(', ') + : Array.from(ASYNC_AGENT_ALLOWED_TOOLS) + .filter(name => !INTERNAL_WORKER_TOOLS.has(name)) + .sort() + .join(', ') + + let content = `Workers spawned via the ${AGENT_TOOL_NAME} tool have access to these tools: ${workerTools}` + + if (mcpClients.length > 0) { + const serverNames = mcpClients.map(c => c.name).join(', ') + content += `\n\nWorkers also have access to MCP tools from connected MCP servers: ${serverNames}` + } + + if (scratchpadDir && isScratchpadGateEnabled()) { + content += `\n\nScratchpad directory: ${scratchpadDir}\nWorkers can read and write here without permission prompts. Use this for durable cross-worker knowledge — structure files however fits the work.` + } + + return { workerToolsContext: content } +} + +export function getCoordinatorSystemPrompt(): string { + const workerCapabilities = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE) + ? 'Workers have access to Bash, Read, and Edit tools, plus MCP tools from configured MCP servers.' + : 'Workers have access to standard tools, MCP tools from configured MCP servers, and project skills via the Skill tool. Delegate skill invocations (e.g. /commit, /verify) to workers.' + + return `You are Claude Code, an AI assistant that orchestrates software engineering tasks across multiple workers. + +## 1. Your Role + +You are a **coordinator**. Your job is to: +- Help the user achieve their goal +- Direct workers to research, implement and verify code changes +- Synthesize results and communicate with the user +- Answer questions directly when possible — don't delegate work that you can handle without tools + +Every message you send is to the user. Worker results and system notifications are internal signals, not conversation partners — never thank or acknowledge them. Summarize new information for the user as it arrives. + +## 2. Your Tools + +- **${AGENT_TOOL_NAME}** - Spawn a new worker +- **${SEND_MESSAGE_TOOL_NAME}** - Continue an existing worker (send a follow-up to its \`to\` agent ID) +- **${TASK_STOP_TOOL_NAME}** - Stop a running worker +- **subscribe_pr_activity / unsubscribe_pr_activity** (if available) - Subscribe to GitHub PR events (review comments, CI results). Events arrive as user messages. Merge conflict transitions do NOT arrive — GitHub doesn't webhook \`mergeable_state\` changes, so poll \`gh pr view N --json mergeable\` if tracking conflict status. Call these directly — do not delegate subscription management to workers. + +When calling ${AGENT_TOOL_NAME}: +- Do not use one worker to check on another. Workers will notify you when they are done. +- Do not use workers to trivially report file contents or run commands. Give them higher-level tasks. +- Do not set the model parameter. Workers need the default model for the substantive tasks you delegate. +- Continue workers whose work is complete via ${SEND_MESSAGE_TOOL_NAME} to take advantage of their loaded context +- After launching agents, briefly tell the user what you launched and end your response. Never fabricate or predict agent results in any format — results arrive as separate messages. + +### ${AGENT_TOOL_NAME} Results + +Worker results arrive as **user-role messages** containing \`\` XML. They look like user messages but are not. Distinguish them by the \`\` opening tag. + +Format: + +\`\`\`xml + +{agentId} +completed|failed|killed +{human-readable status summary} +{agent's final text response} + + N + N + N + + +\`\`\` + +- \`\` and \`\` are optional sections +- The \`\` describes the outcome: "completed", "failed: {error}", or "was stopped" +- The \`\` value is the agent ID — use SendMessage with that ID as \`to\` to continue that worker + +### Example + +Each "You:" block is a separate coordinator turn. The "User:" block is a \`\` delivered between turns. + +You: + Let me start some research on that. + + ${AGENT_TOOL_NAME}({ description: "Investigate auth bug", subagent_type: "worker", prompt: "..." }) + ${AGENT_TOOL_NAME}({ description: "Research secure token storage", subagent_type: "worker", prompt: "..." }) + + Investigating both issues in parallel — I'll report back with findings. + +User: + + agent-a1b + completed + Agent "Investigate auth bug" completed + Found null pointer in src/auth/validate.ts:42... + + +You: + Found the bug — null pointer in confirmTokenExists in validate.ts. I'll fix it. + Still waiting on the token storage research. + + ${SEND_MESSAGE_TOOL_NAME}({ to: "agent-a1b", message: "Fix the null pointer in src/auth/validate.ts:42..." }) + +## 3. Workers + +When calling ${AGENT_TOOL_NAME}, use subagent_type \`worker\`. Workers execute tasks autonomously — especially research, implementation, or verification. + +${workerCapabilities} + +## 4. Task Workflow + +Most tasks can be broken down into the following phases: + +### Phases + +| Phase | Who | Purpose | +|-------|-----|---------| +| Research | Workers (parallel) | Investigate codebase, find files, understand problem | +| Synthesis | **You** (coordinator) | Read findings, understand the problem, craft implementation specs (see Section 5) | +| Implementation | Workers | Make targeted changes per spec, commit | +| Verification | Workers | Test changes work | + +### Concurrency + +**Parallelism is your superpower. Workers are async. Launch independent workers concurrently whenever possible — don't serialize work that can run simultaneously and look for opportunities to fan out. When doing research, cover multiple angles. To launch workers in parallel, make multiple tool calls in a single message.** + +Manage concurrency: +- **Read-only tasks** (research) — run in parallel freely +- **Write-heavy tasks** (implementation) — one at a time per set of files +- **Verification** can sometimes run alongside implementation on different file areas + +### What Real Verification Looks Like + +Verification means **proving the code works**, not confirming it exists. A verifier that rubber-stamps weak work undermines everything. + +- Run tests **with the feature enabled** — not just "tests pass" +- Run typechecks and **investigate errors** — don't dismiss as "unrelated" +- Be skeptical — if something looks off, dig in +- **Test independently** — prove the change works, don't rubber-stamp + +### Handling Worker Failures + +When a worker reports failure (tests failed, build errors, file not found): +- Continue the same worker with ${SEND_MESSAGE_TOOL_NAME} — it has the full error context +- If a correction attempt fails, try a different approach or report to the user + +### Stopping Workers + +Use ${TASK_STOP_TOOL_NAME} to stop a worker you sent in the wrong direction — for example, when you realize mid-flight that the approach is wrong, or the user changes requirements after you launched the worker. Pass the \`task_id\` from the ${AGENT_TOOL_NAME} tool's launch result. Stopped workers can be continued with ${SEND_MESSAGE_TOOL_NAME}. + +\`\`\` +// Launched a worker to refactor auth to use JWT +${AGENT_TOOL_NAME}({ description: "Refactor auth to JWT", subagent_type: "worker", prompt: "Replace session-based auth with JWT..." }) +// ... returns task_id: "agent-x7q" ... + +// User clarifies: "Actually, keep sessions — just fix the null pointer" +${TASK_STOP_TOOL_NAME}({ task_id: "agent-x7q" }) + +// Continue with corrected instructions +${SEND_MESSAGE_TOOL_NAME}({ to: "agent-x7q", message: "Stop the JWT refactor. Instead, fix the null pointer in src/auth/validate.ts:42..." }) +\`\`\` + +## 5. Writing Worker Prompts + +**Workers can't see your conversation.** Every prompt must be self-contained with everything the worker needs. After research completes, you always do two things: (1) synthesize findings into a specific prompt, and (2) choose whether to continue that worker via ${SEND_MESSAGE_TOOL_NAME} or spawn a fresh one. + +### Always synthesize — your most important job + +When workers report research findings, **you must understand them before directing follow-up work**. Read the findings. Identify the approach. Then write a prompt that proves you understood by including specific file paths, line numbers, and exactly what to change. + +Never write "based on your findings" or "based on the research." These phrases delegate understanding to the worker instead of doing it yourself. You never hand off understanding to another worker. + +\`\`\` +// Anti-pattern — lazy delegation (bad whether continuing or spawning) +${AGENT_TOOL_NAME}({ prompt: "Based on your findings, fix the auth bug", ... }) +${AGENT_TOOL_NAME}({ prompt: "The worker found an issue in the auth module. Please fix it.", ... }) + +// Good — synthesized spec (works with either continue or spawn) +${AGENT_TOOL_NAME}({ prompt: "Fix the null pointer in src/auth/validate.ts:42. The user field on Session (src/auth/types.ts:15) is undefined when sessions expire but the token remains cached. Add a null check before user.id access — if null, return 401 with 'Session expired'. Commit and report the hash.", ... }) +\`\`\` + +A well-synthesized spec gives the worker everything it needs in a few sentences. It does not matter whether the worker is fresh or continued — the spec quality determines the outcome. + +### Add a purpose statement + +Include a brief purpose so workers can calibrate depth and emphasis: + +- "This research will inform a PR description — focus on user-facing changes." +- "I need this to plan an implementation — report file paths, line numbers, and type signatures." +- "This is a quick check before we merge — just verify the happy path." + +### Choose continue vs. spawn by context overlap + +After synthesizing, decide whether the worker's existing context helps or hurts: + +| Situation | Mechanism | Why | +|-----------|-----------|-----| +| Research explored exactly the files that need editing | **Continue** (${SEND_MESSAGE_TOOL_NAME}) with synthesized spec | Worker already has the files in context AND now gets a clear plan | +| Research was broad but implementation is narrow | **Spawn fresh** (${AGENT_TOOL_NAME}) with synthesized spec | Avoid dragging along exploration noise; focused context is cleaner | +| Correcting a failure or extending recent work | **Continue** | Worker has the error context and knows what it just tried | +| Verifying code a different worker just wrote | **Spawn fresh** | Verifier should see the code with fresh eyes, not carry implementation assumptions | +| First implementation attempt used the wrong approach entirely | **Spawn fresh** | Wrong-approach context pollutes the retry; clean slate avoids anchoring on the failed path | +| Completely unrelated task | **Spawn fresh** | No useful context to reuse | + +There is no universal default. Think about how much of the worker's context overlaps with the next task. High overlap -> continue. Low overlap -> spawn fresh. + +### Continue mechanics + +When continuing a worker with ${SEND_MESSAGE_TOOL_NAME}, it has full context from its previous run: +\`\`\` +// Continuation — worker finished research, now give it a synthesized implementation spec +${SEND_MESSAGE_TOOL_NAME}({ to: "xyz-456", message: "Fix the null pointer in src/auth/validate.ts:42. The user field is undefined when Session.expired is true but the token is still cached. Add a null check before accessing user.id — if null, return 401 with 'Session expired'. Commit and report the hash." }) +\`\`\` + +\`\`\` +// Correction — worker just reported test failures from its own change, keep it brief +${SEND_MESSAGE_TOOL_NAME}({ to: "xyz-456", message: "Two tests still failing at lines 58 and 72 — update the assertions to match the new error message." }) +\`\`\` + +### Prompt tips + +**Good examples:** + +1. Implementation: "Fix the null pointer in src/auth/validate.ts:42. The user field can be undefined when the session expires. Add a null check and return early with an appropriate error. Commit and report the hash." + +2. Precise git operation: "Create a new branch from main called 'fix/session-expiry'. Cherry-pick only commit abc123 onto it. Push and create a draft PR targeting main. Add anthropics/claude-code as reviewer. Report the PR URL." + +3. Correction (continued worker, short): "The tests failed on the null check you added — validate.test.ts:58 expects 'Invalid session' but you changed it to 'Session expired'. Fix the assertion. Commit and report the hash." + +**Bad examples:** + +1. "Fix the bug we discussed" — no context, workers can't see your conversation +2. "Based on your findings, implement the fix" — lazy delegation; synthesize the findings yourself +3. "Create a PR for the recent changes" — ambiguous scope: which changes? which branch? draft? +4. "Something went wrong with the tests, can you look?" — no error message, no file path, no direction + +Additional tips: +- Include file paths, line numbers, error messages — workers start fresh and need complete context +- State what "done" looks like +- For implementation: "Run relevant tests and typecheck, then commit your changes and report the hash" — workers self-verify before reporting done. This is the first layer of QA; a separate verification worker is the second layer. +- For research: "Report findings — do not modify files" +- Be precise about git operations — specify branch names, commit hashes, draft vs ready, reviewers +- When continuing for corrections: reference what the worker did ("the null check you added") not what you discussed with the user +- For implementation: "Fix the root cause, not the symptom" — guide workers toward durable fixes +- For verification: "Prove the code works, don't just confirm it exists" +- For verification: "Try edge cases and error paths — don't just re-run what the implementation worker ran" +- For verification: "Investigate failures — don't dismiss as unrelated without evidence" + +## 6. Example Session + +User: "There's a null pointer in the auth module. Can you fix it?" + +You: + Let me investigate first. + + ${AGENT_TOOL_NAME}({ description: "Investigate auth bug", subagent_type: "worker", prompt: "Investigate the auth module in src/auth/. Find where null pointer exceptions could occur around session handling and token validation... Report specific file paths, line numbers, and types involved. Do not modify files." }) + ${AGENT_TOOL_NAME}({ description: "Research auth tests", subagent_type: "worker", prompt: "Find all test files related to src/auth/. Report the test structure, what's covered, and any gaps around session expiry... Do not modify files." }) + + Investigating from two angles — I'll report back with findings. + +User: + + agent-a1b + completed + Agent "Investigate auth bug" completed + Found null pointer in src/auth/validate.ts:42. The user field on Session is undefined when the session expires but ... + + +You: + Found the bug — null pointer in validate.ts:42. + + ${SEND_MESSAGE_TOOL_NAME}({ to: "agent-a1b", message: "Fix the null pointer in src/auth/validate.ts:42. Add a null check before accessing user.id — if null, ... Commit and report the hash." }) + + Fix is in progress. + +User: + How's it going? + +You: + Fix for the new test is in progress. Still waiting to hear back about the test suite.` +} diff --git a/src/entrypoints/cli.tsx b/src/entrypoints/cli.tsx new file mode 100644 index 0000000000000000000000000000000000000000..67326f890053cb527d70b58a38fa3bd41854cedd --- /dev/null +++ b/src/entrypoints/cli.tsx @@ -0,0 +1,303 @@ +import { feature } from 'bun:bundle'; + +// Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons +// eslint-disable-next-line custom-rules/no-top-level-side-effects +process.env.COREPACK_ENABLE_AUTO_PIN = '0'; + +// Set max heap size for child processes in CCR environments (containers have 16GB) +// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level, custom-rules/safe-env-boolean-check +if (process.env.CLAUDE_CODE_REMOTE === 'true') { + // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level + const existing = process.env.NODE_OPTIONS || ''; + // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level + process.env.NODE_OPTIONS = existing ? `${existing} --max-old-space-size=8192` : '--max-old-space-size=8192'; +} + +// Harness-science L0 ablation baseline. Inlined here (not init.ts) because +// BashTool/AgentTool/PowerShellTool capture DISABLE_BACKGROUND_TASKS into +// module-level consts at import time — init() runs too late. feature() gate +// DCEs this entire block from external builds. +// eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level +if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) { + for (const k of ['CLAUDE_CODE_SIMPLE', 'CLAUDE_CODE_DISABLE_THINKING', 'DISABLE_INTERLEAVED_THINKING', 'DISABLE_COMPACT', 'DISABLE_AUTO_COMPACT', 'CLAUDE_CODE_DISABLE_AUTO_MEMORY', 'CLAUDE_CODE_DISABLE_BACKGROUND_TASKS']) { + // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level + process.env[k] ??= '1'; + } +} + +/** + * Bootstrap entrypoint - checks for special flags before loading the full CLI. + * All imports are dynamic to minimize module evaluation for fast paths. + * Fast-path for --version has zero imports beyond this file. + */ +async function main(): Promise { + const args = process.argv.slice(2); + + // Fast-path for --version/-v: zero module loading needed + if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) { + // MACRO.VERSION is inlined at build time + // biome-ignore lint/suspicious/noConsole:: intentional console output + console.log(`${MACRO.VERSION} (Claude Code)`); + return; + } + + // For all other paths, load the startup profiler + const { + profileCheckpoint + } = await import('../utils/startupProfiler.js'); + profileCheckpoint('cli_entry'); + + // Fast-path for --dump-system-prompt: output the rendered system prompt and exit. + // Used by prompt sensitivity evals to extract the system prompt at a specific commit. + // Ant-only: eliminated from external builds via feature flag. + if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') { + profileCheckpoint('cli_dump_system_prompt_path'); + const { + enableConfigs + } = await import('../utils/config.js'); + enableConfigs(); + const { + getMainLoopModel + } = await import('../utils/model/model.js'); + const modelIdx = args.indexOf('--model'); + const model = modelIdx !== -1 && args[modelIdx + 1] || getMainLoopModel(); + const { + getSystemPrompt + } = await import('../constants/prompts.js'); + const prompt = await getSystemPrompt([], model); + // biome-ignore lint/suspicious/noConsole:: intentional console output + console.log(prompt.join('\n')); + return; + } + if (process.argv[2] === '--claude-in-chrome-mcp') { + profileCheckpoint('cli_claude_in_chrome_mcp_path'); + const { + runClaudeInChromeMcpServer + } = await import('../utils/claudeInChrome/mcpServer.js'); + await runClaudeInChromeMcpServer(); + return; + } else if (process.argv[2] === '--chrome-native-host') { + profileCheckpoint('cli_chrome_native_host_path'); + const { + runChromeNativeHost + } = await import('../utils/claudeInChrome/chromeNativeHost.js'); + await runChromeNativeHost(); + return; + } else if (feature('CHICAGO_MCP') && process.argv[2] === '--computer-use-mcp') { + profileCheckpoint('cli_computer_use_mcp_path'); + const { + runComputerUseMcpServer + } = await import('../utils/computerUse/mcpServer.js'); + await runComputerUseMcpServer(); + return; + } + + // Fast-path for `--daemon-worker=` (internal — supervisor spawns this). + // Must come before the daemon subcommand check: spawned per-worker, so + // perf-sensitive. No enableConfigs(), no analytics sinks at this layer — + // workers are lean. If a worker kind needs configs/auth (assistant will), + // it calls them inside its run() fn. + if (feature('DAEMON') && args[0] === '--daemon-worker') { + const { + runDaemonWorker + } = await import('../daemon/workerRegistry.js'); + await runDaemonWorker(args[1]); + return; + } + + // Fast-path for `claude remote-control` (also accepts legacy `claude remote` / `claude sync` / `claude bridge`): + // serve local machine as bridge environment. + // feature() must stay inline for build-time dead code elimination; + // isBridgeEnabled() checks the runtime GrowthBook gate. + if (feature('BRIDGE_MODE') && (args[0] === 'remote-control' || args[0] === 'rc' || args[0] === 'remote' || args[0] === 'sync' || args[0] === 'bridge')) { + profileCheckpoint('cli_bridge_path'); + const { + enableConfigs + } = await import('../utils/config.js'); + enableConfigs(); + const { + getBridgeDisabledReason, + checkBridgeMinVersion + } = await import('../bridge/bridgeEnabled.js'); + const { + BRIDGE_LOGIN_ERROR + } = await import('../bridge/types.js'); + const { + bridgeMain + } = await import('../bridge/bridgeMain.js'); + const { + exitWithError + } = await import('../utils/process.js'); + + // Auth check must come before the GrowthBook gate check — without auth, + // GrowthBook has no user context and would return a stale/default false. + // getBridgeDisabledReason awaits GB init, so the returned value is fresh + // (not the stale disk cache), but init still needs auth headers to work. + const { + getClaudeAIOAuthTokens + } = await import('../utils/auth.js'); + if (!getClaudeAIOAuthTokens()?.accessToken) { + exitWithError(BRIDGE_LOGIN_ERROR); + } + const disabledReason = await getBridgeDisabledReason(); + if (disabledReason) { + exitWithError(`Error: ${disabledReason}`); + } + const versionError = checkBridgeMinVersion(); + if (versionError) { + exitWithError(versionError); + } + + // Bridge is a remote control feature - check policy limits + const { + waitForPolicyLimitsToLoad, + isPolicyAllowed + } = await import('../services/policyLimits/index.js'); + await waitForPolicyLimitsToLoad(); + if (!isPolicyAllowed('allow_remote_control')) { + exitWithError("Error: Remote Control is disabled by your organization's policy."); + } + await bridgeMain(args.slice(1)); + return; + } + + // Fast-path for `claude daemon [subcommand]`: long-running supervisor. + if (feature('DAEMON') && args[0] === 'daemon') { + profileCheckpoint('cli_daemon_path'); + const { + enableConfigs + } = await import('../utils/config.js'); + enableConfigs(); + const { + initSinks + } = await import('../utils/sinks.js'); + initSinks(); + const { + daemonMain + } = await import('../daemon/main.js'); + await daemonMain(args.slice(1)); + return; + } + + // Fast-path for `claude ps|logs|attach|kill` and `--bg`/`--background`. + // Session management against the ~/.claude/sessions/ registry. Flag + // literals are inlined so bg.js only loads when actually dispatching. + if (feature('BG_SESSIONS') && (args[0] === 'ps' || args[0] === 'logs' || args[0] === 'attach' || args[0] === 'kill' || args.includes('--bg') || args.includes('--background'))) { + profileCheckpoint('cli_bg_path'); + const { + enableConfigs + } = await import('../utils/config.js'); + enableConfigs(); + const bg = await import('../cli/bg.js'); + switch (args[0]) { + case 'ps': + await bg.psHandler(args.slice(1)); + break; + case 'logs': + await bg.logsHandler(args[1]); + break; + case 'attach': + await bg.attachHandler(args[1]); + break; + case 'kill': + await bg.killHandler(args[1]); + break; + default: + await bg.handleBgFlag(args); + } + return; + } + + // Fast-path for template job commands. + if (feature('TEMPLATES') && (args[0] === 'new' || args[0] === 'list' || args[0] === 'reply')) { + profileCheckpoint('cli_templates_path'); + const { + templatesMain + } = await import('../cli/handlers/templateJobs.js'); + await templatesMain(args); + // process.exit (not return) — mountFleetView's Ink TUI can leave event + // loop handles that prevent natural exit. + // eslint-disable-next-line custom-rules/no-process-exit + process.exit(0); + } + + // Fast-path for `claude environment-runner`: headless BYOC runner. + // feature() must stay inline for build-time dead code elimination. + if (feature('BYOC_ENVIRONMENT_RUNNER') && args[0] === 'environment-runner') { + profileCheckpoint('cli_environment_runner_path'); + const { + environmentRunnerMain + } = await import('../environment-runner/main.js'); + await environmentRunnerMain(args.slice(1)); + return; + } + + // Fast-path for `claude self-hosted-runner`: headless self-hosted-runner + // targeting the SelfHostedRunnerWorkerService API (register + poll; poll IS + // heartbeat). feature() must stay inline for build-time dead code elimination. + if (feature('SELF_HOSTED_RUNNER') && args[0] === 'self-hosted-runner') { + profileCheckpoint('cli_self_hosted_runner_path'); + const { + selfHostedRunnerMain + } = await import('../self-hosted-runner/main.js'); + await selfHostedRunnerMain(args.slice(1)); + return; + } + + // Fast-path for --worktree --tmux: exec into tmux before loading full CLI + const hasTmuxFlag = args.includes('--tmux') || args.includes('--tmux=classic'); + if (hasTmuxFlag && (args.includes('-w') || args.includes('--worktree') || args.some(a => a.startsWith('--worktree=')))) { + profileCheckpoint('cli_tmux_worktree_fast_path'); + const { + enableConfigs + } = await import('../utils/config.js'); + enableConfigs(); + const { + isWorktreeModeEnabled + } = await import('../utils/worktreeModeEnabled.js'); + if (isWorktreeModeEnabled()) { + const { + execIntoTmuxWorktree + } = await import('../utils/worktree.js'); + const result = await execIntoTmuxWorktree(args); + if (result.handled) { + return; + } + // If not handled (e.g., error), fall through to normal CLI + if (result.error) { + const { + exitWithError + } = await import('../utils/process.js'); + exitWithError(result.error); + } + } + } + + // Redirect common update flag mistakes to the update subcommand + if (args.length === 1 && (args[0] === '--update' || args[0] === '--upgrade')) { + process.argv = [process.argv[0]!, process.argv[1]!, 'update']; + } + + // --bare: set SIMPLE early so gates fire during module eval / commander + // option building (not just inside the action handler). + if (args.includes('--bare')) { + process.env.CLAUDE_CODE_SIMPLE = '1'; + } + + // No special flags detected, load and run the full CLI + const { + startCapturingEarlyInput + } = await import('../utils/earlyInput.js'); + startCapturingEarlyInput(); + profileCheckpoint('cli_before_main_import'); + const { + main: cliMain + } = await import('../main.js'); + profileCheckpoint('cli_after_main_import'); + await cliMain(); + profileCheckpoint('cli_after_main_complete'); +} + +// eslint-disable-next-line custom-rules/no-top-level-side-effects +void main(); +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwicHJvY2VzcyIsImVudiIsIkNPUkVQQUNLX0VOQUJMRV9BVVRPX1BJTiIsIkNMQVVERV9DT0RFX1JFTU9URSIsImV4aXN0aW5nIiwiTk9ERV9PUFRJT05TIiwiQ0xBVURFX0NPREVfQUJMQVRJT05fQkFTRUxJTkUiLCJrIiwibWFpbiIsIlByb21pc2UiLCJhcmdzIiwiYXJndiIsInNsaWNlIiwibGVuZ3RoIiwiY29uc29sZSIsImxvZyIsIk1BQ1JPIiwiVkVSU0lPTiIsInByb2ZpbGVDaGVja3BvaW50IiwiZW5hYmxlQ29uZmlncyIsImdldE1haW5Mb29wTW9kZWwiLCJtb2RlbElkeCIsImluZGV4T2YiLCJtb2RlbCIsImdldFN5c3RlbVByb21wdCIsInByb21wdCIsImpvaW4iLCJydW5DbGF1ZGVJbkNocm9tZU1jcFNlcnZlciIsInJ1bkNocm9tZU5hdGl2ZUhvc3QiLCJydW5Db21wdXRlclVzZU1jcFNlcnZlciIsInJ1bkRhZW1vbldvcmtlciIsImdldEJyaWRnZURpc2FibGVkUmVhc29uIiwiY2hlY2tCcmlkZ2VNaW5WZXJzaW9uIiwiQlJJREdFX0xPR0lOX0VSUk9SIiwiYnJpZGdlTWFpbiIsImV4aXRXaXRoRXJyb3IiLCJnZXRDbGF1ZGVBSU9BdXRoVG9rZW5zIiwiYWNjZXNzVG9rZW4iLCJkaXNhYmxlZFJlYXNvbiIsInZlcnNpb25FcnJvciIsIndhaXRGb3JQb2xpY3lMaW1pdHNUb0xvYWQiLCJpc1BvbGljeUFsbG93ZWQiLCJpbml0U2lua3MiLCJkYWVtb25NYWluIiwiaW5jbHVkZXMiLCJiZyIsInBzSGFuZGxlciIsImxvZ3NIYW5kbGVyIiwiYXR0YWNoSGFuZGxlciIsImtpbGxIYW5kbGVyIiwiaGFuZGxlQmdGbGFnIiwidGVtcGxhdGVzTWFpbiIsImV4aXQiLCJlbnZpcm9ubWVudFJ1bm5lck1haW4iLCJzZWxmSG9zdGVkUnVubmVyTWFpbiIsImhhc1RtdXhGbGFnIiwic29tZSIsImEiLCJzdGFydHNXaXRoIiwiaXNXb3JrdHJlZU1vZGVFbmFibGVkIiwiZXhlY0ludG9UbXV4V29ya3RyZWUiLCJyZXN1bHQiLCJoYW5kbGVkIiwiZXJyb3IiLCJDTEFVREVfQ09ERV9TSU1QTEUiLCJzdGFydENhcHR1cmluZ0Vhcmx5SW5wdXQiLCJjbGlNYWluIl0sInNvdXJjZXMiOlsiY2xpLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBmZWF0dXJlIH0gZnJvbSAnYnVuOmJ1bmRsZSdcblxuLy8gQnVnZml4IGZvciBjb3JlcGFjayBhdXRvLXBpbm5pbmcsIHdoaWNoIGFkZHMgeWFybnBrZyB0byBwZW9wbGVzJyBwYWNrYWdlLmpzb25zXG4vLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgY3VzdG9tLXJ1bGVzL25vLXRvcC1sZXZlbC1zaWRlLWVmZmVjdHNcbnByb2Nlc3MuZW52LkNPUkVQQUNLX0VOQUJMRV9BVVRPX1BJTiA9ICcwJ1xuXG4vLyBTZXQgbWF4IGhlYXAgc2l6ZSBmb3IgY2hpbGQgcHJvY2Vzc2VzIGluIENDUiBlbnZpcm9ubWVudHMgKGNvbnRhaW5lcnMgaGF2ZSAxNkdCKVxuLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGN1c3RvbS1ydWxlcy9uby10b3AtbGV2ZWwtc2lkZS1lZmZlY3RzLCBjdXN0b20tcnVsZXMvbm8tcHJvY2Vzcy1lbnYtdG9wLWxldmVsLCBjdXN0b20tcnVsZXMvc2FmZS1lbnYtYm9vbGVhbi1jaGVja1xuaWYgKHByb2Nlc3MuZW52LkNMQVVERV9DT0RFX1JFTU9URSA9PT0gJ3RydWUnKSB7XG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBjdXN0b20tcnVsZXMvbm8tdG9wLWxldmVsLXNpZGUtZWZmZWN0cywgY3VzdG9tLXJ1bGVzL25vLXByb2Nlc3MtZW52LXRvcC1sZXZlbFxuICBjb25zdCBleGlzdGluZyA9IHByb2Nlc3MuZW52Lk5PREVfT1BUSU9OUyB8fCAnJ1xuICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgY3VzdG9tLXJ1bGVzL25vLXRvcC1sZXZlbC1zaWRlLWVmZmVjdHMsIGN1c3RvbS1ydWxlcy9uby1wcm9jZXNzLWVudi10b3AtbGV2ZWxcbiAgcHJvY2Vzcy5lbnYuTk9ERV9PUFRJT05TID0gZXhpc3RpbmdcbiAgICA/IGAke2V4aXN0aW5nfSAtLW1heC1vbGQtc3BhY2Utc2l6ZT04MTkyYFxuICAgIDogJy0tbWF4LW9sZC1zcGFjZS1zaXplPTgxOTInXG59XG5cbi8vIEhhcm5lc3Mtc2NpZW5jZSBMMCBhYmxhdGlvbiBiYXNlbGluZS4gSW5saW5lZCBoZXJlIChub3QgaW5pdC50cykgYmVjYXVzZVxuLy8gQmFzaFRvb2wvQWdlbnRUb29sL1Bvd2VyU2hlbGxUb29sIGNhcHR1cmUgRElTQUJMRV9CQUNLR1JPVU5EX1RBU0tTIGludG9cbi8vIG1vZHVsZS1sZXZlbCBjb25zdHMgYXQgaW1wb3J0IHRpbWUg4oCUIGluaXQoKSBydW5zIHRvbyBsYXRlLiBmZWF0dXJlKCkgZ2F0ZVxuLy8gRENFcyB0aGlzIGVudGlyZSBibG9jayBmcm9tIGV4dGVybmFsIGJ1aWxkcy5cbi8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBjdXN0b20tcnVsZXMvbm8tdG9wLWxldmVsLXNpZGUtZWZmZWN0cywgY3VzdG9tLXJ1bGVzL25vLXByb2Nlc3MtZW52LXRvcC1sZXZlbFxuaWYgKGZlYXR1cmUoJ0FCTEFUSU9OX0JBU0VMSU5FJykgJiYgcHJvY2Vzcy5lbnYuQ0xBVURFX0NPREVfQUJMQVRJT05fQkFTRUxJTkUpIHtcbiAgZm9yIChjb25zdCBrIG9mIFtcbiAgICAnQ0xBVURFX0NPREVfU0lNUExFJyxcbiAgICAnQ0xBVURFX0NPREVfRElTQUJMRV9USElOS0lORycsXG4gICAgJ0RJU0FCTEVfSU5URVJMRUFWRURfVEhJTktJTkcnLFxuICAgICdESVNBQkxFX0NPTVBBQ1QnLFxuICAgICdESVNBQkxFX0FVVE9fQ09NUEFDVCcsXG4gICAgJ0NMQVVERV9DT0RFX0RJU0FCTEVfQVVUT19NRU1PUlknLFxuICAgICdDTEFVREVfQ09ERV9ESVNBQkxFX0JBQ0tHUk9VTkRfVEFTS1MnLFxuICBdKSB7XG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGN1c3RvbS1ydWxlcy9uby10b3AtbGV2ZWwtc2lkZS1lZmZlY3RzLCBjdXN0b20tcnVsZXMvbm8tcHJvY2Vzcy1lbnYtdG9wLWxldmVsXG4gICAgcHJvY2Vzcy5lbnZba10gPz89ICcxJ1xuICB9XG59XG5cbi8qKlxuICogQm9vdHN0cmFwIGVudHJ5cG9pbnQgLSBjaGVja3MgZm9yIHNwZWNpYWwgZmxhZ3MgYmVmb3JlIGxvYWRpbmcgdGhlIGZ1bGwgQ0xJLlxuICogQWxsIGltcG9ydHMgYXJlIGR5bmFtaWMgdG8gbWluaW1pemUgbW9kdWxlIGV2YWx1YXRpb24gZm9yIGZhc3QgcGF0aHMuXG4gKiBGYXN0LXBhdGggZm9yIC0tdmVyc2lvbiBoYXMgemVybyBpbXBvcnRzIGJleW9uZCB0aGlzIGZpbGUuXG4gKi9cbmFzeW5jIGZ1bmN0aW9uIG1haW4oKTogUHJvbWlzZTx2b2lkPiB7XG4gIGNvbnN0IGFyZ3MgPSBwcm9jZXNzLmFyZ3Yuc2xpY2UoMilcblxuICAvLyBGYXN0LXBhdGggZm9yIC0tdmVyc2lvbi8tdjogemVybyBtb2R1bGUgbG9hZGluZyBuZWVkZWRcbiAgaWYgKFxuICAgIGFyZ3MubGVuZ3RoID09PSAxICYmXG4gICAgKGFyZ3NbMF0gPT09ICctLXZlcnNpb24nIHx8IGFyZ3NbMF0gPT09ICctdicgfHwgYXJnc1swXSA9PT0gJy1WJylcbiAgKSB7XG4gICAgLy8gTUFDUk8uVkVSU0lPTiBpcyBpbmxpbmVkIGF0IGJ1aWxkIHRpbWVcbiAgICAvLyBiaW9tZS1pZ25vcmUgbGludC9zdXNwaWNpb3VzL25vQ29uc29sZTo6IGludGVudGlvbmFsIGNvbnNvbGUgb3V0cHV0XG4gICAgY29uc29sZS5sb2coYCR7TUFDUk8uVkVSU0lPTn0gKENsYXVkZSBDb2RlKWApXG4gICAgcmV0dXJuXG4gIH1cblxuICAvLyBGb3IgYWxsIG90aGVyIHBhdGhzLCBsb2FkIHRoZSBzdGFydHVwIHByb2ZpbGVyXG4gIGNvbnN0IHsgcHJvZmlsZUNoZWNrcG9pbnQgfSA9IGF3YWl0IGltcG9ydCgnLi4vdXRpbHMvc3RhcnR1cFByb2ZpbGVyLmpzJylcbiAgcHJvZmlsZUNoZWNrcG9pbnQoJ2NsaV9lbnRyeScpXG5cbiAgLy8gRmFzdC1wYXRoIGZvciAtLWR1bXAtc3lzdGVtLXByb21wdDogb3V0cHV0IHRoZSByZW5kZXJlZCBzeXN0ZW0gcHJvbXB0IGFuZCBleGl0LlxuICAvLyBVc2VkIGJ5IHByb21wdCBzZW5zaXRpdml0eSBldmFscyB0byBleHRyYWN0IHRoZSBzeXN0ZW0gcHJvbXB0IGF0IGEgc3BlY2lmaWMgY29tbWl0LlxuICAvLyBBbnQtb25seTogZWxpbWluYXRlZCBmcm9tIGV4dGVybmFsIGJ1aWxkcyB2aWEgZmVhdHVyZSBmbGFnLlxuICBpZiAoZmVhdHVyZSgnRFVNUF9TWVNURU1fUFJPTVBUJykgJiYgYXJnc1swXSA9PT0gJy0tZHVtcC1zeXN0ZW0tcHJvbXB0Jykge1xuICAgIHByb2ZpbGVDaGVja3BvaW50KCdjbGlfZHVtcF9zeXN0ZW1fcHJvbXB0X3BhdGgnKVxuICAgIGNvbnN0IHsgZW5hYmxlQ29uZmlncyB9ID0gYXdhaXQgaW1wb3J0KCcuLi91dGlscy9jb25maWcuanMnKVxuICAgIGVuYWJsZUNvbmZpZ3MoKVxuICAgIGNvbnN0IHsgZ2V0TWFpbkxvb3BNb2RlbCB9ID0gYXdhaXQgaW1wb3J0KCcuLi91dGlscy9tb2RlbC9tb2RlbC5qcycpXG4gICAgY29uc3QgbW9kZWxJZHggPSBhcmdzLmluZGV4T2YoJy0tbW9kZWwnKVxuICAgIGNvbnN0IG1vZGVsID0gKG1vZGVsSWR4ICE9PSAtMSAmJiBhcmdzW21vZGVsSWR4ICsgMV0pIHx8IGdldE1haW5Mb29wTW9kZWwoKVxuICAgIGNvbnN0IHsgZ2V0U3lzdGVtUHJvbXB0IH0gPSBhd2FpdCBpbXBvcnQoJy4uL2NvbnN0YW50cy9wcm9tcHRzLmpzJylcbiAgICBjb25zdCBwcm9tcHQgPSBhd2FpdCBnZXRTeXN0ZW1Qcm9tcHQoW10sIG1vZGVsKVxuICAgIC8vIGJpb21lLWlnbm9yZSBsaW50L3N1c3BpY2lvdXMvbm9Db25zb2xlOjogaW50ZW50aW9uYWwgY29uc29sZSBvdXRwdXRcbiAgICBjb25zb2xlLmxvZyhwcm9tcHQuam9pbignXFxuJykpXG4gICAgcmV0dXJuXG4gIH1cblxuICBpZiAocHJvY2Vzcy5hcmd2WzJdID09PSAnLS1jbGF1ZGUtaW4tY2hyb21lLW1jcCcpIHtcbiAgICBwcm9maWxlQ2hlY2twb2ludCgnY2xpX2NsYXVkZV9pbl9jaHJvbWVfbWNwX3BhdGgnKVxuICAgIGNvbnN0IHsgcnVuQ2xhdWRlSW5DaHJvbWVNY3BTZXJ2ZXIgfSA9IGF3YWl0IGltcG9ydChcbiAgICAgICcuLi91dGlscy9jbGF1ZGVJbkNocm9tZS9tY3BTZXJ2ZXIuanMnXG4gICAgKVxuICAgIGF3YWl0IHJ1bkNsYXVkZUluQ2hyb21lTWNwU2VydmVyKClcbiAgICByZXR1cm5cbiAgfSBlbHNlIGlmIChwcm9jZXNzLmFyZ3ZbMl0gPT09ICctLWNocm9tZS1uYXRpdmUtaG9zdCcpIHtcbiAgICBwcm9maWxlQ2hlY2twb2ludCgnY2xpX2Nocm9tZV9uYXRpdmVfaG9zdF9wYXRoJylcbiAgICBjb25zdCB7IHJ1bkNocm9tZU5hdGl2ZUhvc3QgfSA9IGF3YWl0IGltcG9ydChcbiAgICAgICcuLi91dGlscy9jbGF1ZGVJbkNocm9tZS9jaHJvbWVOYXRpdmVIb3N0LmpzJ1xuICAgIClcbiAgICBhd2FpdCBydW5DaHJvbWVOYXRpdmVIb3N0KClcbiAgICByZXR1cm5cbiAgfSBlbHNlIGlmIChcbiAgICBmZWF0dXJlKCdDSElDQUdPX01DUCcpICYmXG4gICAgcHJvY2Vzcy5hcmd2WzJdID09PSAnLS1jb21wdXRlci11c2UtbWNwJ1xuICApIHtcbiAgICBwcm9maWxlQ2hlY2twb2ludCgnY2xpX2NvbXB1dGVyX3VzZV9tY3BfcGF0aCcpXG4gICAgY29uc3QgeyBydW5Db21wdXRlclVzZU1jcFNlcnZlciB9ID0gYXdhaXQgaW1wb3J0KFxuICAgICAgJy4uL3V0aWxzL2NvbXB1dGVyVXNlL21jcFNlcnZlci5qcydcbiAgICApXG4gICAgYXdhaXQgcnVuQ29tcHV0ZXJVc2VNY3BTZXJ2ZXIoKVxuICAgIHJldHVyblxuICB9XG5cbiAgLy8gRmFzdC1wYXRoIGZvciBgLS1kYWVtb24td29ya2VyPTxraW5kPmAgKGludGVybmFsIOKAlCBzdXBlcnZpc29yIHNwYXducyB0aGlzKS5cbiAgLy8gTXVzdCBjb21lIGJlZm9yZSB0aGUgZGFlbW9uIHN1YmNvbW1hbmQgY2hlY2s6IHNwYXduZWQgcGVyLXdvcmtlciwgc29cbiAgLy8gcGVyZi1zZW5zaXRpdmUuIE5vIGVuYWJsZUNvbmZpZ3MoKSwgbm8gYW5hbHl0aWNzIHNpbmtzIGF0IHRoaXMgbGF5ZXIg4oCUXG4gIC8vIHdvcmtlcnMgYXJlIGxlYW4uIElmIGEgd29ya2VyIGtpbmQgbmVlZHMgY29uZmlncy9hdXRoIChhc3Npc3RhbnQgd2lsbCksXG4gIC8vIGl0IGNhbGxzIHRoZW0gaW5zaWRlIGl0cyBydW4oKSBmbi5cbiAgaWYgKGZlYXR1cmUoJ0RBRU1PTicpICYmIGFyZ3NbMF0gPT09ICctLWRhZW1vbi13b3JrZXInKSB7XG4gICAgY29uc3QgeyBydW5EYWVtb25Xb3JrZXIgfSA9IGF3YWl0IGltcG9ydCgnLi4vZGFlbW9uL3dvcmtlclJlZ2lzdHJ5LmpzJylcbiAgICBhd2FpdCBydW5EYWVtb25Xb3JrZXIoYXJnc1sxXSlcbiAgICByZXR1cm5cbiAgfVxuXG4gIC8vIEZhc3QtcGF0aCBmb3IgYGNsYXVkZSByZW1vdGUtY29udHJvbGAgKGFsc28gYWNjZXB0cyBsZWdhY3kgYGNsYXVkZSByZW1vdGVgIC8gYGNsYXVkZSBzeW5jYCAvIGBjbGF1ZGUgYnJpZGdlYCk6XG4gIC8vIHNlcnZlIGxvY2FsIG1hY2hpbmUgYXMgYnJpZGdlIGVudmlyb25tZW50LlxuICAvLyBmZWF0dXJlKCkgbXVzdCBzdGF5IGlubGluZSBmb3IgYnVpbGQtdGltZSBkZWFkIGNvZGUgZWxpbWluYXRpb247XG4gIC8vIGlzQnJpZGdlRW5hYmxlZCgpIGNoZWNrcyB0aGUgcnVudGltZSBHcm93dGhCb29rIGdhdGUuXG4gIGlmIChcbiAgICBmZWF0dXJlKCdCUklER0VfTU9ERScpICYmXG4gICAgKGFyZ3NbMF0gPT09ICdyZW1vdGUtY29udHJvbCcgfHxcbiAgICAgIGFyZ3NbMF0gPT09ICdyYycgfHxcbiAgICAgIGFyZ3NbMF0gPT09ICdyZW1vdGUnIHx8XG4gICAgICBhcmdzWzBdID09PSAnc3luYycgfHxcbiAgICAgIGFyZ3NbMF0gPT09ICdicmlkZ2UnKVxuICApIHtcbiAgICBwcm9maWxlQ2hlY2twb2ludCgnY2xpX2JyaWRnZV9wYXRoJylcbiAgICBjb25zdCB7IGVuYWJsZUNvbmZpZ3MgfSA9IGF3YWl0IGltcG9ydCgnLi4vdXRpbHMvY29uZmlnLmpzJylcbiAgICBlbmFibGVDb25maWdzKClcblxuICAgIGNvbnN0IHsgZ2V0QnJpZGdlRGlzYWJsZWRSZWFzb24sIGNoZWNrQnJpZGdlTWluVmVyc2lvbiB9ID0gYXdhaXQgaW1wb3J0KFxuICAgICAgJy4uL2JyaWRnZS9icmlkZ2VFbmFibGVkLmpzJ1xuICAgIClcbiAgICBjb25zdCB7IEJSSURHRV9MT0dJTl9FUlJPUiB9ID0gYXdhaXQgaW1wb3J0KCcuLi9icmlkZ2UvdHlwZXMuanMnKVxuICAgIGNvbnN0IHsgYnJpZGdlTWFpbiB9ID0gYXdhaXQgaW1wb3J0KCcuLi9icmlkZ2UvYnJpZGdlTWFpbi5qcycpXG4gICAgY29uc3QgeyBleGl0V2l0aEVycm9yIH0gPSBhd2FpdCBpbXBvcnQoJy4uL3V0aWxzL3Byb2Nlc3MuanMnKVxuXG4gICAgLy8gQXV0aCBjaGVjayBtdXN0IGNvbWUgYmVmb3JlIHRoZSBHcm93dGhCb29rIGdhdGUgY2hlY2sg4oCUIHdpdGhvdXQgYXV0aCxcbiAgICAvLyBHcm93dGhCb29rIGhhcyBubyB1c2VyIGNvbnRleHQgYW5kIHdvdWxkIHJldHVybiBhIHN0YWxlL2RlZmF1bHQgZmFsc2UuXG4gICAgLy8gZ2V0QnJpZGdlRGlzYWJsZWRSZWFzb24gYXdhaXRzIEdCIGluaXQsIHNvIHRoZSByZXR1cm5lZCB2YWx1ZSBpcyBmcmVzaFxuICAgIC8vIChub3QgdGhlIHN0YWxlIGRpc2sgY2FjaGUpLCBidXQgaW5pdCBzdGlsbCBuZWVkcyBhdXRoIGhlYWRlcnMgdG8gd29yay5cbiAgICBjb25zdCB7IGdldENsYXVkZUFJT0F1dGhUb2tlbnMgfSA9IGF3YWl0IGltcG9ydCgnLi4vdXRpbHMvYXV0aC5qcycpXG4gICAgaWYgKCFnZXRDbGF1ZGVBSU9BdXRoVG9rZW5zKCk/LmFjY2Vzc1Rva2VuKSB7XG4gICAgICBleGl0V2l0aEVycm9yKEJSSURHRV9MT0dJTl9FUlJPUilcbiAgICB9XG4gICAgY29uc3QgZGlzYWJsZWRSZWFzb24gPSBhd2FpdCBnZXRCcmlkZ2VEaXNhYmxlZFJlYXNvbigpXG4gICAgaWYgKGRpc2FibGVkUmVhc29uKSB7XG4gICAgICBleGl0V2l0aEVycm9yKGBFcnJvcjogJHtkaXNhYmxlZFJlYXNvbn1gKVxuICAgIH1cbiAgICBjb25zdCB2ZXJzaW9uRXJyb3IgPSBjaGVja0JyaWRnZU1pblZlcnNpb24oKVxuICAgIGlmICh2ZXJzaW9uRXJyb3IpIHtcbiAgICAgIGV4aXRXaXRoRXJyb3IodmVyc2lvbkVycm9yKVxuICAgIH1cblxuICAgIC8vIEJyaWRnZSBpcyBhIHJlbW90ZSBjb250cm9sIGZlYXR1cmUgLSBjaGVjayBwb2xpY3kgbGltaXRzXG4gICAgY29uc3QgeyB3YWl0Rm9yUG9saWN5TGltaXRzVG9Mb2FkLCBpc1BvbGljeUFsbG93ZWQgfSA9IGF3YWl0IGltcG9ydChcbiAgICAgICcuLi9zZXJ2aWNlcy9wb2xpY3lMaW1pdHMvaW5kZXguanMnXG4gICAgKVxuICAgIGF3YWl0IHdhaXRGb3JQb2xpY3lMaW1pdHNUb0xvYWQoKVxuICAgIGlmICghaXNQb2xpY3lBbGxvd2VkKCdhbGxvd19yZW1vdGVfY29udHJvbCcpKSB7XG4gICAgICBleGl0V2l0aEVycm9yKFxuICAgICAgICBcIkVycm9yOiBSZW1vdGUgQ29udHJvbCBpcyBkaXNhYmxlZCBieSB5b3VyIG9yZ2FuaXphdGlvbidzIHBvbGljeS5cIixcbiAgICAgIClcbiAgICB9XG5cbiAgICBhd2FpdCBicmlkZ2VNYWluKGFyZ3Muc2xpY2UoMSkpXG4gICAgcmV0dXJuXG4gIH1cblxuICAvLyBGYXN0LXBhdGggZm9yIGBjbGF1ZGUgZGFlbW9uIFtzdWJjb21tYW5kXWA6IGxvbmctcnVubmluZyBzdXBlcnZpc29yLlxuICBpZiAoZmVhdHVyZSgnREFFTU9OJykgJiYgYXJnc1swXSA9PT0gJ2RhZW1vbicpIHtcbiAgICBwcm9maWxlQ2hlY2twb2ludCgnY2xpX2RhZW1vbl9wYXRoJylcbiAgICBjb25zdCB7IGVuYWJsZUNvbmZpZ3MgfSA9IGF3YWl0IGltcG9ydCgnLi4vdXRpbHMvY29uZmlnLmpzJylcbiAgICBlbmFibGVDb25maWdzKClcbiAgICBjb25zdCB7IGluaXRTaW5rcyB9ID0gYXdhaXQgaW1wb3J0KCcuLi91dGlscy9zaW5rcy5qcycpXG4gICAgaW5pdFNpbmtzKClcbiAgICBjb25zdCB7IGRhZW1vbk1haW4gfSA9IGF3YWl0IGltcG9ydCgnLi4vZGFlbW9uL21haW4uanMnKVxuICAgIGF3YWl0IGRhZW1vbk1haW4oYXJncy5zbGljZSgxKSlcbiAgICByZXR1cm5cbiAgfVxuXG4gIC8vIEZhc3QtcGF0aCBmb3IgYGNsYXVkZSBwc3xsb2dzfGF0dGFjaHxraWxsYCBhbmQgYC0tYmdgL2AtLWJhY2tncm91bmRgLlxuICAvLyBTZXNzaW9uIG1hbmFnZW1lbnQgYWdhaW5zdCB0aGUgfi8uY2xhdWRlL3Nlc3Npb25zLyByZWdpc3RyeS4gRmxhZ1xuICAvLyBsaXRlcmFscyBhcmUgaW5saW5lZCBzbyBiZy5qcyBvbmx5IGxvYWRzIHdoZW4gYWN0dWFsbHkgZGlzcGF0Y2hpbmcuXG4gIGlmIChcbiAgICBmZWF0dXJlKCdCR19TRVNTSU9OUycpICYmXG4gICAgKGFyZ3NbMF0gPT09ICdwcycgfHxcbiAgICAgIGFyZ3NbMF0gPT09ICdsb2dzJyB8fFxuICAgICAgYXJnc1swXSA9PT0gJ2F0dGFjaCcgfHxcbiAgICAgIGFyZ3NbMF0gPT09ICdraWxsJyB8fFxuICAgICAgYXJncy5pbmNsdWRlcygnLS1iZycpIHx8XG4gICAgICBhcmdzLmluY2x1ZGVzKCctLWJhY2tncm91bmQnKSlcbiAgKSB7XG4gICAgcHJvZmlsZUNoZWNrcG9pbnQoJ2NsaV9iZ19wYXRoJylcbiAgICBjb25zdCB7IGVuYWJsZUNvbmZpZ3MgfSA9IGF3YWl0IGltcG9ydCgnLi4vdXRpbHMvY29uZmlnLmpzJylcbiAgICBlbmFibGVDb25maWdzKClcbiAgICBjb25zdCBiZyA9IGF3YWl0IGltcG9ydCgnLi4vY2xpL2JnLmpzJylcbiAgICBzd2l0Y2ggKGFyZ3NbMF0pIHtcbiAgICAgIGNhc2UgJ3BzJzpcbiAgICAgICAgYXdhaXQgYmcucHNIYW5kbGVyKGFyZ3Muc2xpY2UoMSkpXG4gICAgICAgIGJyZWFrXG4gICAgICBjYXNlICdsb2dzJzpcbiAgICAgICAgYXdhaXQgYmcubG9nc0hhbmRsZXIoYXJnc1sxXSlcbiAgICAgICAgYnJlYWtcbiAgICAgIGNhc2UgJ2F0dGFjaCc6XG4gICAgICAgIGF3YWl0IGJnLmF0dGFjaEhhbmRsZXIoYXJnc1sxXSlcbiAgICAgICAgYnJlYWtcbiAgICAgIGNhc2UgJ2tpbGwnOlxuICAgICAgICBhd2FpdCBiZy5raWxsSGFuZGxlcihhcmdzWzFdKVxuICAgICAgICBicmVha1xuICAgICAgZGVmYXVsdDpcbiAgICAgICAgYXdhaXQgYmcuaGFuZGxlQmdGbGFnKGFyZ3MpXG4gICAgfVxuICAgIHJldHVyblxuICB9XG5cbiAgLy8gRmFzdC1wYXRoIGZvciB0ZW1wbGF0ZSBqb2IgY29tbWFuZHMuXG4gIGlmIChcbiAgICBmZWF0dXJlKCdURU1QTEFURVMnKSAmJlxuICAgIChhcmdzWzBdID09PSAnbmV3JyB8fCBhcmdzWzBdID09PSAnbGlzdCcgfHwgYXJnc1swXSA9PT0gJ3JlcGx5JylcbiAgKSB7XG4gICAgcHJvZmlsZUNoZWNrcG9pbnQoJ2NsaV90ZW1wbGF0ZXNfcGF0aCcpXG4gICAgY29uc3QgeyB0ZW1wbGF0ZXNNYWluIH0gPSBhd2FpdCBpbXBvcnQoJy4uL2NsaS9oYW5kbGVycy90ZW1wbGF0ZUpvYnMuanMnKVxuICAgIGF3YWl0IHRlbXBsYXRlc01haW4oYXJncylcbiAgICAvLyBwcm9jZXNzLmV4aXQgKG5vdCByZXR1cm4pIOKAlCBtb3VudEZsZWV0VmlldydzIEluayBUVUkgY2FuIGxlYXZlIGV2ZW50XG4gICAgLy8gbG9vcCBoYW5kbGVzIHRoYXQgcHJldmVudCBuYXR1cmFsIGV4aXQuXG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGN1c3RvbS1ydWxlcy9uby1wcm9jZXNzLWV4aXRcbiAgICBwcm9jZXNzLmV4aXQoMClcbiAgfVxuXG4gIC8vIEZhc3QtcGF0aCBmb3IgYGNsYXVkZSBlbnZpcm9ubWVudC1ydW5uZXJgOiBoZWFkbGVzcyBCWU9DIHJ1bm5lci5cbiAgLy8gZmVhdHVyZSgpIG11c3Qgc3RheSBpbmxpbmUgZm9yIGJ1aWxkLXRpbWUgZGVhZCBjb2RlIGVsaW1pbmF0aW9uLlxuICBpZiAoZmVhdHVyZSgnQllPQ19FTlZJUk9OTUVOVF9SVU5ORVInKSAmJiBhcmdzWzBdID09PSAnZW52aXJvbm1lbnQtcnVubmVyJykge1xuICAgIHByb2ZpbGVDaGVja3BvaW50KCdjbGlfZW52aXJvbm1lbnRfcnVubmVyX3BhdGgnKVxuICAgIGNvbnN0IHsgZW52aXJvbm1lbnRSdW5uZXJNYWluIH0gPSBhd2FpdCBpbXBvcnQoXG4gICAgICAnLi4vZW52aXJvbm1lbnQtcnVubmVyL21haW4uanMnXG4gICAgKVxuICAgIGF3YWl0IGVudmlyb25tZW50UnVubmVyTWFpbihhcmdzLnNsaWNlKDEpKVxuICAgIHJldHVyblxuICB9XG5cbiAgLy8gRmFzdC1wYXRoIGZvciBgY2xhdWRlIHNlbGYtaG9zdGVkLXJ1bm5lcmA6IGhlYWRsZXNzIHNlbGYtaG9zdGVkLXJ1bm5lclxuICAvLyB0YXJnZXRpbmcgdGhlIFNlbGZIb3N0ZWRSdW5uZXJXb3JrZXJTZXJ2aWNlIEFQSSAocmVnaXN0ZXIgKyBwb2xsOyBwb2xsIElTXG4gIC8vIGhlYXJ0YmVhdCkuIGZlYXR1cmUoKSBtdXN0IHN0YXkgaW5saW5lIGZvciBidWlsZC10aW1lIGRlYWQgY29kZSBlbGltaW5hdGlvbi5cbiAgaWYgKGZlYXR1cmUoJ1NFTEZfSE9TVEVEX1JVTk5FUicpICYmIGFyZ3NbMF0gPT09ICdzZWxmLWhvc3RlZC1ydW5uZXInKSB7XG4gICAgcHJvZmlsZUNoZWNrcG9pbnQoJ2NsaV9zZWxmX2hvc3RlZF9ydW5uZXJfcGF0aCcpXG4gICAgY29uc3QgeyBzZWxmSG9zdGVkUnVubmVyTWFpbiB9ID0gYXdhaXQgaW1wb3J0KFxuICAgICAgJy4uL3NlbGYtaG9zdGVkLXJ1bm5lci9tYWluLmpzJ1xuICAgIClcbiAgICBhd2FpdCBzZWxmSG9zdGVkUnVubmVyTWFpbihhcmdzLnNsaWNlKDEpKVxuICAgIHJldHVyblxuICB9XG5cbiAgLy8gRmFzdC1wYXRoIGZvciAtLXdvcmt0cmVlIC0tdG11eDogZXhlYyBpbnRvIHRtdXggYmVmb3JlIGxvYWRpbmcgZnVsbCBDTElcbiAgY29uc3QgaGFzVG11eEZsYWcgPSBhcmdzLmluY2x1ZGVzKCctLXRtdXgnKSB8fCBhcmdzLmluY2x1ZGVzKCctLXRtdXg9Y2xhc3NpYycpXG4gIGlmIChcbiAgICBoYXNUbXV4RmxhZyAmJlxuICAgIChhcmdzLmluY2x1ZGVzKCctdycpIHx8XG4gICAgICBhcmdzLmluY2x1ZGVzKCctLXdvcmt0cmVlJykgfHxcbiAgICAgIGFyZ3Muc29tZShhID0+IGEuc3RhcnRzV2l0aCgnLS13b3JrdHJlZT0nKSkpXG4gICkge1xuICAgIHByb2ZpbGVDaGVja3BvaW50KCdjbGlfdG11eF93b3JrdHJlZV9mYXN0X3BhdGgnKVxuICAgIGNvbnN0IHsgZW5hYmxlQ29uZmlncyB9ID0gYXdhaXQgaW1wb3J0KCcuLi91dGlscy9jb25maWcuanMnKVxuICAgIGVuYWJsZUNvbmZpZ3MoKVxuICAgIGNvbnN0IHsgaXNXb3JrdHJlZU1vZGVFbmFibGVkIH0gPSBhd2FpdCBpbXBvcnQoXG4gICAgICAnLi4vdXRpbHMvd29ya3RyZWVNb2RlRW5hYmxlZC5qcydcbiAgICApXG4gICAgaWYgKGlzV29ya3RyZWVNb2RlRW5hYmxlZCgpKSB7XG4gICAgICBjb25zdCB7IGV4ZWNJbnRvVG11eFdvcmt0cmVlIH0gPSBhd2FpdCBpbXBvcnQoJy4uL3V0aWxzL3dvcmt0cmVlLmpzJylcbiAgICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IGV4ZWNJbnRvVG11eFdvcmt0cmVlKGFyZ3MpXG4gICAgICBpZiAocmVzdWx0LmhhbmRsZWQpIHtcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG4gICAgICAvLyBJZiBub3QgaGFuZGxlZCAoZS5nLiwgZXJyb3IpLCBmYWxsIHRocm91Z2ggdG8gbm9ybWFsIENMSVxuICAgICAgaWYgKHJlc3VsdC5lcnJvcikge1xuICAgICAgICBjb25zdCB7IGV4aXRXaXRoRXJyb3IgfSA9IGF3YWl0IGltcG9ydCgnLi4vdXRpbHMvcHJvY2Vzcy5qcycpXG4gICAgICAgIGV4aXRXaXRoRXJyb3IocmVzdWx0LmVycm9yKVxuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIC8vIFJlZGlyZWN0IGNvbW1vbiB1cGRhdGUgZmxhZyBtaXN0YWtlcyB0byB0aGUgdXBkYXRlIHN1YmNvbW1hbmRcbiAgaWYgKFxuICAgIGFyZ3MubGVuZ3RoID09PSAxICYmXG4gICAgKGFyZ3NbMF0gPT09ICctLXVwZGF0ZScgfHwgYXJnc1swXSA9PT0gJy0tdXBncmFkZScpXG4gICkge1xuICAgIHByb2Nlc3MuYXJndiA9IFtwcm9jZXNzLmFyZ3ZbMF0hLCBwcm9jZXNzLmFyZ3ZbMV0hLCAndXBkYXRlJ11cbiAgfVxuXG4gIC8vIC0tYmFyZTogc2V0IFNJTVBMRSBlYXJseSBzbyBnYXRlcyBmaXJlIGR1cmluZyBtb2R1bGUgZXZhbCAvIGNvbW1hbmRlclxuICAvLyBvcHRpb24gYnVpbGRpbmcgKG5vdCBqdXN0IGluc2lkZSB0aGUgYWN0aW9uIGhhbmRsZXIpLlxuICBpZiAoYXJncy5pbmNsdWRlcygnLS1iYXJlJykpIHtcbiAgICBwcm9jZXNzLmVudi5DTEFVREVfQ09ERV9TSU1QTEUgPSAnMSdcbiAgfVxuXG4gIC8vIE5vIHNwZWNpYWwgZmxhZ3MgZGV0ZWN0ZWQsIGxvYWQgYW5kIHJ1biB0aGUgZnVsbCBDTElcbiAgY29uc3QgeyBzdGFydENhcHR1cmluZ0Vhcmx5SW5wdXQgfSA9IGF3YWl0IGltcG9ydCgnLi4vdXRpbHMvZWFybHlJbnB1dC5qcycpXG4gIHN0YXJ0Q2FwdHVyaW5nRWFybHlJbnB1dCgpXG4gIHByb2ZpbGVDaGVja3BvaW50KCdjbGlfYmVmb3JlX21haW5faW1wb3J0JylcbiAgY29uc3QgeyBtYWluOiBjbGlNYWluIH0gPSBhd2FpdCBpbXBvcnQoJy4uL21haW4uanMnKVxuICBwcm9maWxlQ2hlY2twb2ludCgnY2xpX2FmdGVyX21haW5faW1wb3J0JylcbiAgYXdhaXQgY2xpTWFpbigpXG4gIHByb2ZpbGVDaGVja3BvaW50KCdjbGlfYWZ0ZXJfbWFpbl9jb21wbGV0ZScpXG59XG5cbi8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBjdXN0b20tcnVsZXMvbm8tdG9wLWxldmVsLXNpZGUtZWZmZWN0c1xudm9pZCBtYWluKClcbiJdLCJtYXBwaW5ncyI6IkFBQUEsU0FBU0EsT0FBTyxRQUFRLFlBQVk7O0FBRXBDO0FBQ0E7QUFDQUMsT0FBTyxDQUFDQyxHQUFHLENBQUNDLHdCQUF3QixHQUFHLEdBQUc7O0FBRTFDO0FBQ0E7QUFDQSxJQUFJRixPQUFPLENBQUNDLEdBQUcsQ0FBQ0Usa0JBQWtCLEtBQUssTUFBTSxFQUFFO0VBQzdDO0VBQ0EsTUFBTUMsUUFBUSxHQUFHSixPQUFPLENBQUNDLEdBQUcsQ0FBQ0ksWUFBWSxJQUFJLEVBQUU7RUFDL0M7RUFDQUwsT0FBTyxDQUFDQyxHQUFHLENBQUNJLFlBQVksR0FBR0QsUUFBUSxHQUMvQixHQUFHQSxRQUFRLDRCQUE0QixHQUN2QywyQkFBMkI7QUFDakM7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLElBQUlMLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxJQUFJQyxP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 \ No newline at end of file diff --git a/src/entrypoints/init.ts b/src/entrypoints/init.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e926cf031a262be4211b1b665387cdfff4f40f7 --- /dev/null +++ b/src/entrypoints/init.ts @@ -0,0 +1,348 @@ +import { profileCheckpoint } from '../utils/startupProfiler.js' +import '../bootstrap/state.js' +import '../utils/config.js' +import type { Attributes, MetricOptions } from '@opentelemetry/api' +import memoize from 'lodash-es/memoize.js' +import { getIsNonInteractiveSession } from 'src/bootstrap/state.js' +import type { AttributedCounter } from '../bootstrap/state.js' +import { getSessionCounter, setMeter } from '../bootstrap/state.js' +import { shutdownLspServerManager } from '../services/lsp/manager.js' +import { populateOAuthAccountInfoIfNeeded } from '../services/oauth/client.js' +import { + initializePolicyLimitsLoadingPromise, + isPolicyLimitsEligible, +} from '../services/policyLimits/index.js' +import { + initializeRemoteManagedSettingsLoadingPromise, + isEligibleForRemoteManagedSettings, + waitForRemoteManagedSettingsToLoad, +} from '../services/remoteManagedSettings/index.js' +import { preconnectAnthropicApi } from '../utils/apiPreconnect.js' +import { applyExtraCACertsFromConfig } from '../utils/caCertsConfig.js' +import { registerCleanup } from '../utils/cleanupRegistry.js' +import { enableConfigs, recordFirstStartTime } from '../utils/config.js' +import { logForDebugging } from '../utils/debug.js' +import { detectCurrentRepository } from '../utils/detectRepository.js' +import { logForDiagnosticsNoPII } from '../utils/diagLogs.js' +import { initJetBrainsDetection } from '../utils/envDynamic.js' +import { isEnvTruthy } from '../utils/envUtils.js' +import { ConfigParseError, errorMessage } from '../utils/errors.js' +// showInvalidConfigDialog is dynamically imported in the error path to avoid loading React at init +import { + gracefulShutdownSync, + setupGracefulShutdown, +} from '../utils/gracefulShutdown.js' +import { + applyConfigEnvironmentVariables, + applySafeConfigEnvironmentVariables, +} from '../utils/managedEnv.js' +import { configureGlobalMTLS } from '../utils/mtls.js' +import { + ensureScratchpadDir, + isScratchpadEnabled, +} from '../utils/permissions/filesystem.js' +// initializeTelemetry is loaded lazily via import() in setMeterState() to defer +// ~400KB of OpenTelemetry + protobuf modules until telemetry is actually initialized. +// gRPC exporters (~700KB via @grpc/grpc-js) are further lazy-loaded within instrumentation.ts. +import { configureGlobalAgents } from '../utils/proxy.js' +import { isBetaTracingEnabled } from '../utils/telemetry/betaSessionTracing.js' +import { getTelemetryAttributes } from '../utils/telemetryAttributes.js' +import { setShellIfWindows } from '../utils/windowsPaths.js' + +// initialize1PEventLogging is dynamically imported to defer OpenTelemetry sdk-logs/resources + +// Track if telemetry has been initialized to prevent double initialization +let telemetryInitialized = false + +export const init = memoize(async (): Promise => { + const initStartTime = Date.now() + logForDiagnosticsNoPII('info', 'init_started') + profileCheckpoint('init_function_start') + + // Validate configs are valid and enable configuration system + try { + const configsStart = Date.now() + enableConfigs() + logForDiagnosticsNoPII('info', 'init_configs_enabled', { + duration_ms: Date.now() - configsStart, + }) + profileCheckpoint('init_configs_enabled') + + // Apply only safe environment variables before trust dialog + // Full environment variables are applied after trust is established + const envVarsStart = Date.now() + applySafeConfigEnvironmentVariables() + + // Apply NODE_EXTRA_CA_CERTS from settings.json to process.env early, + // before any TLS connections. Bun caches the TLS cert store at boot + // via BoringSSL, so this must happen before the first TLS handshake. + applyExtraCACertsFromConfig() + + logForDiagnosticsNoPII('info', 'init_safe_env_vars_applied', { + duration_ms: Date.now() - envVarsStart, + }) + profileCheckpoint('init_safe_env_vars_applied') + + // Make sure things get flushed on exit + setupGracefulShutdown() + profileCheckpoint('init_after_graceful_shutdown') + + // Initialize 1P event logging (no security concerns, but deferred to avoid + // loading OpenTelemetry sdk-logs at startup). growthbook.js is already in + // the module cache by this point (firstPartyEventLogger imports it), so the + // second dynamic import adds no load cost. + void Promise.all([ + import('../services/analytics/firstPartyEventLogger.js'), + import('../services/analytics/growthbook.js'), + ]).then(([fp, gb]) => { + fp.initialize1PEventLogging() + // Rebuild the logger provider if tengu_1p_event_batch_config changes + // mid-session. Change detection (isEqual) is inside the handler so + // unchanged refreshes are no-ops. + gb.onGrowthBookRefresh(() => { + void fp.reinitialize1PEventLoggingIfConfigChanged() + }) + }) + profileCheckpoint('init_after_1p_event_logging') + + // Populate OAuth account info if it is not already cached in config. This is needed since the + // OAuth account info may not be populated when logging in through the VSCode extension. + void populateOAuthAccountInfoIfNeeded() + profileCheckpoint('init_after_oauth_populate') + + // Initialize JetBrains IDE detection asynchronously (populates cache for later sync access) + void initJetBrainsDetection() + profileCheckpoint('init_after_jetbrains_detection') + + // Detect GitHub repository asynchronously (populates cache for gitDiff PR linking) + void detectCurrentRepository() + + // Initialize the loading promise early so that other systems (like plugin hooks) + // can await remote settings loading. The promise includes a timeout to prevent + // deadlocks if loadRemoteManagedSettings() is never called (e.g., Agent SDK tests). + if (isEligibleForRemoteManagedSettings()) { + initializeRemoteManagedSettingsLoadingPromise() + } + if (isPolicyLimitsEligible()) { + initializePolicyLimitsLoadingPromise() + } + profileCheckpoint('init_after_remote_settings_check') + + // Record the first start time + recordFirstStartTime() + + // Configure global mTLS settings + const mtlsStart = Date.now() + logForDebugging('[init] configureGlobalMTLS starting') + configureGlobalMTLS() + logForDiagnosticsNoPII('info', 'init_mtls_configured', { + duration_ms: Date.now() - mtlsStart, + }) + logForDebugging('[init] configureGlobalMTLS complete') + + // Configure global HTTP agents (proxy and/or mTLS) + const proxyStart = Date.now() + logForDebugging('[init] configureGlobalAgents starting') + configureGlobalAgents() + logForDiagnosticsNoPII('info', 'init_proxy_configured', { + duration_ms: Date.now() - proxyStart, + }) + logForDebugging('[init] configureGlobalAgents complete') + profileCheckpoint('init_network_configured') + + // Preconnect to the Anthropic API — overlap TCP+TLS handshake + // (~100-200ms) with the ~100ms of action-handler work before the API + // request. After CA certs + proxy agents are configured so the warmed + // connection uses the right transport. Fire-and-forget; skipped for + // proxy/mTLS/unix/cloud-provider where the SDK's dispatcher wouldn't + // reuse the global pool. + preconnectAnthropicApi() + + // CCR upstreamproxy: start the local CONNECT relay so agent subprocesses + // can reach org-configured upstreams with credential injection. Gated on + // CLAUDE_CODE_REMOTE + GrowthBook; fail-open on any error. Lazy import so + // non-CCR startups don't pay the module load. The getUpstreamProxyEnv + // function is registered with subprocessEnv.ts so subprocess spawning can + // inject proxy vars without a static import of the upstreamproxy module. + if (isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)) { + try { + const { initUpstreamProxy, getUpstreamProxyEnv } = await import( + '../upstreamproxy/upstreamproxy.js' + ) + const { registerUpstreamProxyEnvFn } = await import( + '../utils/subprocessEnv.js' + ) + registerUpstreamProxyEnvFn(getUpstreamProxyEnv) + await initUpstreamProxy() + } catch (err) { + logForDebugging( + `[init] upstreamproxy init failed: ${err instanceof Error ? err.message : String(err)}; continuing without proxy`, + { level: 'warn' }, + ) + } + } + + // Set up git-bash if relevant + setShellIfWindows() + + // Register LSP manager cleanup (initialization happens in main.tsx after --plugin-dir is processed) + registerCleanup(shutdownLspServerManager) + + // gh-32730: teams created by subagents (or main agent without + // explicit TeamDelete) were left on disk forever. Register cleanup + // for all teams created this session. Lazy import: swarm code is + // behind feature gate and most sessions never create teams. + registerCleanup(async () => { + const { cleanupSessionTeams } = await import( + '../utils/swarm/teamHelpers.js' + ) + await cleanupSessionTeams() + }) + + // Initialize scratchpad directory if enabled + if (isScratchpadEnabled()) { + const scratchpadStart = Date.now() + await ensureScratchpadDir() + logForDiagnosticsNoPII('info', 'init_scratchpad_created', { + duration_ms: Date.now() - scratchpadStart, + }) + } + + logForDiagnosticsNoPII('info', 'init_completed', { + duration_ms: Date.now() - initStartTime, + }) + profileCheckpoint('init_function_end') + } catch (error) { + if (error instanceof ConfigParseError) { + // Skip the interactive Ink dialog when we can't safely render it. + // The dialog breaks JSON consumers (e.g. desktop marketplace plugin + // manager running `plugin marketplace list --json` in a VM sandbox). + if (getIsNonInteractiveSession()) { + process.stderr.write( + `Configuration error in ${error.filePath}: ${error.message}\n`, + ) + gracefulShutdownSync(1) + return + } + + // Show the invalid config dialog with the error object and wait for it to complete + return import('../components/InvalidConfigDialog.js').then(m => + m.showInvalidConfigDialog({ error }), + ) + // Dialog itself handles process.exit, so we don't need additional cleanup here + } else { + // For non-config errors, rethrow them + throw error + } + } +}) + +/** + * Initialize telemetry after trust has been granted. + * For remote-settings-eligible users, waits for settings to load (non-blocking), + * then re-applies env vars (to include remote settings) before initializing telemetry. + * For non-eligible users, initializes telemetry immediately. + * This should only be called once, after the trust dialog has been accepted. + */ +export function initializeTelemetryAfterTrust(): void { + if ( + process.env.DISABLE_TELEMETRY || + process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC + ) { + logForDebugging('[3P telemetry] Telemetry disabled by environment') + return + } + + if (isEligibleForRemoteManagedSettings()) { + // For SDK/headless mode with beta tracing, initialize eagerly first + // to ensure the tracer is ready before the first query runs. + // The async path below will still run but doInitializeTelemetry() guards against double init. + if (getIsNonInteractiveSession() && isBetaTracingEnabled()) { + void doInitializeTelemetry().catch(error => { + logForDebugging( + `[3P telemetry] Eager telemetry init failed (beta tracing): ${errorMessage(error)}`, + { level: 'error' }, + ) + }) + } + logForDebugging( + '[3P telemetry] Waiting for remote managed settings before telemetry init', + ) + void waitForRemoteManagedSettingsToLoad() + .then(async () => { + logForDebugging( + '[3P telemetry] Remote managed settings loaded, initializing telemetry', + ) + // Re-apply env vars to pick up remote settings before initializing telemetry. + applyConfigEnvironmentVariables() + await doInitializeTelemetry() + }) + .catch(error => { + logForDebugging( + `[3P telemetry] Telemetry init failed (remote settings path): ${errorMessage(error)}`, + { level: 'error' }, + ) + }) + } else { + void doInitializeTelemetry().catch(error => { + logForDebugging( + `[3P telemetry] Telemetry init failed: ${errorMessage(error)}`, + { level: 'error' }, + ) + }) + } +} + +async function doInitializeTelemetry(): Promise { + if (telemetryInitialized) { + // Already initialized, nothing to do + return + } + + // Set flag before init to prevent double initialization + telemetryInitialized = true + try { + await setMeterState() + } catch (error) { + // Reset flag on failure so subsequent calls can retry + telemetryInitialized = false + throw error + } +} + +async function setMeterState(): Promise { + // Lazy-load instrumentation to defer ~400KB of OpenTelemetry + protobuf + const { initializeTelemetry } = await import( + '../utils/telemetry/instrumentation.js' + ) + // Initialize customer OTLP telemetry (metrics, logs, traces) + const meter = await initializeTelemetry() + if (meter) { + // Create factory function for attributed counters + const createAttributedCounter = ( + name: string, + options: MetricOptions, + ): AttributedCounter => { + const counter = meter?.createCounter(name, options) + + return { + add(value: number, additionalAttributes: Attributes = {}) { + // Always fetch fresh telemetry attributes to ensure they're up to date + const currentAttributes = getTelemetryAttributes() + const mergedAttributes = { + ...currentAttributes, + ...additionalAttributes, + } + counter?.add(value, mergedAttributes) + }, + } + } + + setMeter(meter, createAttributedCounter) + + // Increment session counter here because the startup telemetry path + // runs before this async initialization completes, so the counter + // would be null there. + getSessionCounter()?.add(1) + } +} diff --git a/src/entrypoints/mcp.ts b/src/entrypoints/mcp.ts new file mode 100644 index 0000000000000000000000000000000000000000..deaf9d6a582aad798e1a422bdcf3edd437112a7c --- /dev/null +++ b/src/entrypoints/mcp.ts @@ -0,0 +1,196 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { + CallToolRequestSchema, + type CallToolResult, + ListToolsRequestSchema, + type ListToolsResult, + type Tool, +} from '@modelcontextprotocol/sdk/types.js' +import { getDefaultAppState } from 'src/state/AppStateStore.js' +import review from '../commands/review.js' +import type { Command } from '../commands.js' +import { + findToolByName, + getEmptyToolPermissionContext, + type ToolUseContext, +} from '../Tool.js' +import { getTools } from '../tools.js' +import { createAbortController } from '../utils/abortController.js' +import { createFileStateCacheWithSizeLimit } from '../utils/fileStateCache.js' +import { logError } from '../utils/log.js' +import { createAssistantMessage } from '../utils/messages.js' +import { getMainLoopModel } from '../utils/model/model.js' +import { hasPermissionsToUseTool } from '../utils/permissions/permissions.js' +import { setCwd } from '../utils/Shell.js' +import { jsonStringify } from '../utils/slowOperations.js' +import { getErrorParts } from '../utils/toolErrors.js' +import { zodToJsonSchema } from '../utils/zodToJsonSchema.js' + +type ToolInput = Tool['inputSchema'] +type ToolOutput = Tool['outputSchema'] + +const MCP_COMMANDS: Command[] = [review] + +export async function startMCPServer( + cwd: string, + debug: boolean, + verbose: boolean, +): Promise { + // Use size-limited LRU cache for readFileState to prevent unbounded memory growth + // 100 files and 25MB limit should be sufficient for MCP server operations + const READ_FILE_STATE_CACHE_SIZE = 100 + const readFileStateCache = createFileStateCacheWithSizeLimit( + READ_FILE_STATE_CACHE_SIZE, + ) + setCwd(cwd) + const server = new Server( + { + name: 'claude/tengu', + version: MACRO.VERSION, + }, + { + capabilities: { + tools: {}, + }, + }, + ) + + server.setRequestHandler( + ListToolsRequestSchema, + async (): Promise => { + // TODO: Also re-expose any MCP tools + const toolPermissionContext = getEmptyToolPermissionContext() + const tools = getTools(toolPermissionContext) + return { + tools: await Promise.all( + tools.map(async tool => { + let outputSchema: ToolOutput | undefined + if (tool.outputSchema) { + const convertedSchema = zodToJsonSchema(tool.outputSchema) + // MCP SDK requires outputSchema to have type: "object" at root level + // Skip schemas with anyOf/oneOf at root (from z.union, z.discriminatedUnion, etc.) + // See: https://github.com/anthropics/claude-code/issues/8014 + if ( + typeof convertedSchema === 'object' && + convertedSchema !== null && + 'type' in convertedSchema && + convertedSchema.type === 'object' + ) { + outputSchema = convertedSchema as ToolOutput + } + } + return { + ...tool, + description: await tool.prompt({ + getToolPermissionContext: async () => toolPermissionContext, + tools, + agents: [], + }), + inputSchema: zodToJsonSchema(tool.inputSchema) as ToolInput, + outputSchema, + } + }), + ), + } + }, + ) + + server.setRequestHandler( + CallToolRequestSchema, + async ({ params: { name, arguments: args } }): Promise => { + const toolPermissionContext = getEmptyToolPermissionContext() + // TODO: Also re-expose any MCP tools + const tools = getTools(toolPermissionContext) + const tool = findToolByName(tools, name) + if (!tool) { + throw new Error(`Tool ${name} not found`) + } + + // Assume MCP servers do not read messages separately from the tool + // call arguments. + const toolUseContext: ToolUseContext = { + abortController: createAbortController(), + options: { + commands: MCP_COMMANDS, + tools, + mainLoopModel: getMainLoopModel(), + thinkingConfig: { type: 'disabled' }, + mcpClients: [], + mcpResources: {}, + isNonInteractiveSession: true, + debug, + verbose, + agentDefinitions: { activeAgents: [], allAgents: [] }, + }, + getAppState: () => getDefaultAppState(), + setAppState: () => {}, + messages: [], + readFileState: readFileStateCache, + setInProgressToolUseIDs: () => {}, + setResponseLength: () => {}, + updateFileHistoryState: () => {}, + updateAttributionState: () => {}, + } + + // TODO: validate input types with zod + try { + if (!tool.isEnabled()) { + throw new Error(`Tool ${name} is not enabled`) + } + const validationResult = await tool.validateInput?.( + (args as never) ?? {}, + toolUseContext, + ) + if (validationResult && !validationResult.result) { + throw new Error( + `Tool ${name} input is invalid: ${validationResult.message}`, + ) + } + const finalResult = await tool.call( + (args ?? {}) as never, + toolUseContext, + hasPermissionsToUseTool, + createAssistantMessage({ + content: [], + }), + ) + + return { + content: [ + { + type: 'text' as const, + text: + typeof finalResult === 'string' + ? finalResult + : jsonStringify(finalResult.data), + }, + ], + } + } catch (error) { + logError(error) + + const parts = + error instanceof Error ? getErrorParts(error) : [String(error)] + const errorText = parts.filter(Boolean).join('\n').trim() || 'Error' + + return { + isError: true, + content: [ + { + type: 'text', + text: errorText, + }, + ], + } + } + }, + ) + + async function runServer() { + const transport = new StdioServerTransport() + await server.connect(transport) + } + + return await runServer() +} diff --git a/src/hooks/renderPlaceholder.ts b/src/hooks/renderPlaceholder.ts new file mode 100644 index 0000000000000000000000000000000000000000..50913ff01a554cdd0e98835bb7524ee469f69ea0 --- /dev/null +++ b/src/hooks/renderPlaceholder.ts @@ -0,0 +1,51 @@ +import chalk from 'chalk' + +type PlaceholderRendererProps = { + placeholder?: string + value: string + showCursor?: boolean + focus?: boolean + terminalFocus: boolean + invert?: (text: string) => string + hidePlaceholderText?: boolean +} + +export function renderPlaceholder({ + placeholder, + value, + showCursor, + focus, + terminalFocus = true, + invert = chalk.inverse, + hidePlaceholderText = false, +}: PlaceholderRendererProps): { + renderedPlaceholder: string | undefined + showPlaceholder: boolean +} { + let renderedPlaceholder: string | undefined = undefined + + if (placeholder) { + if (hidePlaceholderText) { + // Voice recording: show only the cursor, no placeholder text + renderedPlaceholder = + showCursor && focus && terminalFocus ? invert(' ') : '' + } else { + renderedPlaceholder = chalk.dim(placeholder) + + // Show inverse cursor only when both input and terminal are focused + if (showCursor && focus && terminalFocus) { + renderedPlaceholder = + placeholder.length > 0 + ? invert(placeholder[0]!) + chalk.dim(placeholder.slice(1)) + : invert(' ') + } + } + } + + const showPlaceholder = value.length === 0 && Boolean(placeholder) + + return { + renderedPlaceholder, + showPlaceholder, + } +} diff --git a/src/hooks/useAfterFirstRender.ts b/src/hooks/useAfterFirstRender.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5ec2249fd1fbb0bc01604c971eccc30c132b9da --- /dev/null +++ b/src/hooks/useAfterFirstRender.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react' +import { isEnvTruthy } from '../utils/envUtils.js' + +export function useAfterFirstRender(): void { + useEffect(() => { + if ( + process.env.USER_TYPE === 'ant' && + isEnvTruthy(process.env.CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER) + ) { + process.stderr.write( + `\nStartup time: ${Math.round(process.uptime() * 1000)}ms\n`, + ) + // eslint-disable-next-line custom-rules/no-process-exit + process.exit(0) + } + }, []) +} diff --git a/src/hooks/useAssistantHistory.ts b/src/hooks/useAssistantHistory.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5348d20e04d5c4bdfcd21fc2dc8abadb693f1f0 --- /dev/null +++ b/src/hooks/useAssistantHistory.ts @@ -0,0 +1,250 @@ +import { randomUUID } from 'crypto' +import { + type RefObject, + useCallback, + useEffect, + useLayoutEffect, + useRef, +} from 'react' +import { + createHistoryAuthCtx, + fetchLatestEvents, + fetchOlderEvents, + type HistoryAuthCtx, + type HistoryPage, +} from '../assistant/sessionHistory.js' +import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js' +import type { RemoteSessionConfig } from '../remote/RemoteSessionManager.js' +import { convertSDKMessage } from '../remote/sdkMessageAdapter.js' +import type { Message, SystemInformationalMessage } from '../types/message.js' +import { logForDebugging } from '../utils/debug.js' + +type Props = { + /** Gated on viewerOnly — non-viewer sessions have no remote history to page. */ + config: RemoteSessionConfig | undefined + setMessages: React.Dispatch> + scrollRef: RefObject + /** Called after prepend from the layout effect with message count + height + * delta. Lets useUnseenDivider shift dividerIndex + dividerYRef. */ + onPrepend?: (indexDelta: number, heightDelta: number) => void +} + +type Result = { + /** Trigger for ScrollKeybindingHandler's onScroll composition. */ + maybeLoadOlder: (handle: ScrollBoxHandle) => void +} + +/** Fire loadOlder when scrolled within this many rows of the top. */ +const PREFETCH_THRESHOLD_ROWS = 40 + +/** Max chained page loads to fill the viewport on mount. Bounds the loop if + * events convert to zero visible messages (everything filtered). */ +const MAX_FILL_PAGES = 10 + +const SENTINEL_LOADING = 'loading older messages…' +const SENTINEL_LOADING_FAILED = + 'failed to load older messages — scroll up to retry' +const SENTINEL_START = 'start of session' + +/** Convert a HistoryPage to REPL Message[] using the same opts as viewer mode. */ +function pageToMessages(page: HistoryPage): Message[] { + const out: Message[] = [] + for (const ev of page.events) { + const c = convertSDKMessage(ev, { + convertUserTextMessages: true, + convertToolResults: true, + }) + if (c.type === 'message') out.push(c.message) + } + return out +} + +/** + * Lazy-load `claude assistant` history on scroll-up. + * + * On mount: fetch newest page via anchor_to_latest, prepend to messages. + * On scroll-up near top: fetch next-older page via before_id, prepend with + * scroll anchoring (viewport stays put). + * + * No-op unless config.viewerOnly. REPL only calls this hook inside a + * feature('KAIROS') gate, so build-time elimination is handled there. + */ +export function useAssistantHistory({ + config, + setMessages, + scrollRef, + onPrepend, +}: Props): Result { + const enabled = config?.viewerOnly === true + + // Cursor state: ref-only (no re-render on cursor change). `null` = no + // older pages. `undefined` = initial page not fetched yet. + const cursorRef = useRef(undefined) + const ctxRef = useRef(null) + const inflightRef = useRef(false) + + // Scroll-anchor: snapshot height + prepended count before setMessages; + // compensate in useLayoutEffect after React commits. getFreshScrollHeight + // reads Yoga directly so the value is correct post-commit. + const anchorRef = useRef<{ beforeHeight: number; count: number } | null>(null) + + // Fill-viewport chaining: after the initial page commits, if content doesn't + // fill the viewport yet, load another page. Self-chains via the layout effect + // until filled or the budget runs out. Budget set once on initial load; user + // scroll-ups don't need it (maybeLoadOlder re-fires on next wheel event). + const fillBudgetRef = useRef(0) + + // Stable sentinel UUID — reused across swaps so virtual-scroll treats it + // as one item (text-only mutation, not remove+insert). + const sentinelUuidRef = useRef(randomUUID()) + + function mkSentinel(text: string): SystemInformationalMessage { + return { + type: 'system', + subtype: 'informational', + content: text, + isMeta: false, + timestamp: new Date().toISOString(), + uuid: sentinelUuidRef.current, + level: 'info', + } + } + + /** Prepend a page at the front, with scroll-anchor snapshot for non-initial. + * Replaces the sentinel (always at index 0 when present) in-place. */ + const prepend = useCallback( + (page: HistoryPage, isInitial: boolean) => { + const msgs = pageToMessages(page) + cursorRef.current = page.hasMore ? page.firstId : null + + if (!isInitial) { + const s = scrollRef.current + anchorRef.current = s + ? { beforeHeight: s.getFreshScrollHeight(), count: msgs.length } + : null + } + + const sentinel = page.hasMore ? null : mkSentinel(SENTINEL_START) + setMessages(prev => { + // Drop existing sentinel (index 0, known stable UUID — O(1)). + const base = + prev[0]?.uuid === sentinelUuidRef.current ? prev.slice(1) : prev + return sentinel ? [sentinel, ...msgs, ...base] : [...msgs, ...base] + }) + + logForDebugging( + `[useAssistantHistory] ${isInitial ? 'initial' : 'older'} page: ${msgs.length} msgs (raw ${page.events.length}), hasMore=${page.hasMore}`, + ) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps -- scrollRef is a stable ref; mkSentinel reads refs only + [setMessages], + ) + + // Initial fetch on mount — best-effort. + useEffect(() => { + if (!enabled || !config) return + let cancelled = false + void (async () => { + const ctx = await createHistoryAuthCtx(config.sessionId).catch(() => null) + if (!ctx || cancelled) return + ctxRef.current = ctx + const page = await fetchLatestEvents(ctx) + if (cancelled || !page) return + fillBudgetRef.current = MAX_FILL_PAGES + prepend(page, true) + })() + return () => { + cancelled = true + } + // config identity is stable (created once in main.tsx, never recreated) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [enabled]) + + const loadOlder = useCallback(async () => { + if (!enabled || inflightRef.current) return + const cursor = cursorRef.current + const ctx = ctxRef.current + if (!cursor || !ctx) return // null=exhausted, undefined=initial pending + inflightRef.current = true + // Swap sentinel to "loading…" — O(1) slice since sentinel is at index 0. + setMessages(prev => { + const base = + prev[0]?.uuid === sentinelUuidRef.current ? prev.slice(1) : prev + return [mkSentinel(SENTINEL_LOADING), ...base] + }) + try { + const page = await fetchOlderEvents(ctx, cursor) + if (!page) { + // Fetch failed — revert sentinel back to "start" placeholder so the user + // can retry on next scroll-up. Cursor is preserved (not nulled out). + setMessages(prev => { + const base = + prev[0]?.uuid === sentinelUuidRef.current ? prev.slice(1) : prev + return [mkSentinel(SENTINEL_LOADING_FAILED), ...base] + }) + return + } + prepend(page, false) + } finally { + inflightRef.current = false + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- mkSentinel reads refs only + }, [enabled, prepend, setMessages]) + + // Scroll-anchor compensation — after React commits the prepended items, + // shift scrollTop by the height delta so the viewport stays put. Also + // fire onPrepend here (not in prepend()) so dividerIndex + baseline ref + // are shifted with the ACTUAL height delta, not an estimate. + // No deps: runs every render; cheap no-op when anchorRef is null. + useLayoutEffect(() => { + const anchor = anchorRef.current + if (anchor === null) return + anchorRef.current = null + const s = scrollRef.current + if (!s || s.isSticky()) return // sticky = pinned bottom; prepend is invisible + const delta = s.getFreshScrollHeight() - anchor.beforeHeight + if (delta > 0) s.scrollBy(delta) + onPrepend?.(anchor.count, delta) + }) + + // Fill-viewport chain: after paint, if content doesn't exceed the viewport, + // load another page. Runs as useEffect (not layout effect) so Ink has + // painted and scrollViewportHeight is populated. Self-chains via next + // render's effect; budget caps the chain. + // + // The ScrollBox content wrapper has flexGrow:1 flexShrink:0 — it's clamped + // to ≥ viewport. So `content < viewport` is never true; `<=` detects "no + // overflow yet" correctly. Stops once there's at least something to scroll. + useEffect(() => { + if ( + fillBudgetRef.current <= 0 || + !cursorRef.current || + inflightRef.current + ) { + return + } + const s = scrollRef.current + if (!s) return + const contentH = s.getFreshScrollHeight() + const viewH = s.getViewportHeight() + logForDebugging( + `[useAssistantHistory] fill-check: content=${contentH} viewport=${viewH} budget=${fillBudgetRef.current}`, + ) + if (contentH <= viewH) { + fillBudgetRef.current-- + void loadOlder() + } else { + fillBudgetRef.current = 0 + } + }) + + // Trigger wrapper for onScroll composition in REPL. + const maybeLoadOlder = useCallback( + (handle: ScrollBoxHandle) => { + if (handle.getScrollTop() < PREFETCH_THRESHOLD_ROWS) void loadOlder() + }, + [loadOlder], + ) + + return { maybeLoadOlder } +} diff --git a/src/hooks/useBackgroundTaskNavigation.ts b/src/hooks/useBackgroundTaskNavigation.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1a9b14fd78cebf34f96b6e652f6229858b92278 --- /dev/null +++ b/src/hooks/useBackgroundTaskNavigation.ts @@ -0,0 +1,251 @@ +import { useEffect, useRef } from 'react' +import { KeyboardEvent } from '../ink/events/keyboard-event.js' +// eslint-disable-next-line custom-rules/prefer-use-keybindings -- backward-compat bridge until REPL wires handleKeyDown to +import { useInput } from '../ink.js' +import { + type AppState, + useAppState, + useSetAppState, +} from '../state/AppState.js' +import { + enterTeammateView, + exitTeammateView, +} from '../state/teammateViewHelpers.js' +import { + getRunningTeammatesSorted, + InProcessTeammateTask, +} from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js' +import { + type InProcessTeammateTaskState, + isInProcessTeammateTask, +} from '../tasks/InProcessTeammateTask/types.js' +import { isBackgroundTask } from '../tasks/types.js' + +// Step teammate selection by delta, wrapping across leader(-1)..teammates(0..n-1)..hide(n). +// First step from a collapsed tree expands it and parks on leader. +function stepTeammateSelection( + delta: 1 | -1, + setAppState: (updater: (prev: AppState) => AppState) => void, +): void { + setAppState(prev => { + const currentCount = getRunningTeammatesSorted(prev.tasks).length + if (currentCount === 0) return prev + + if (prev.expandedView !== 'teammates') { + return { + ...prev, + expandedView: 'teammates' as const, + viewSelectionMode: 'selecting-agent', + selectedIPAgentIndex: -1, + } + } + + const maxIdx = currentCount // hide row + const cur = prev.selectedIPAgentIndex + const next = + delta === 1 + ? cur >= maxIdx + ? -1 + : cur + 1 + : cur <= -1 + ? maxIdx + : cur - 1 + return { + ...prev, + selectedIPAgentIndex: next, + viewSelectionMode: 'selecting-agent', + } + }) +} + +/** + * Custom hook that handles Shift+Up/Down keyboard navigation for background tasks. + * When teammates (swarm) are present, navigates between leader and teammates. + * When only non-teammate background tasks exist, opens the background tasks dialog. + * Also handles Enter to confirm selection, 'f' to view transcript, and 'k' to kill. + */ +export function useBackgroundTaskNavigation(options?: { + onOpenBackgroundTasks?: () => void +}): { handleKeyDown: (e: KeyboardEvent) => void } { + const tasks = useAppState(s => s.tasks) + const viewSelectionMode = useAppState(s => s.viewSelectionMode) + const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId) + const selectedIPAgentIndex = useAppState(s => s.selectedIPAgentIndex) + const setAppState = useSetAppState() + + // Filter to running teammates and sort alphabetically to match TeammateSpinnerTree display + const teammateTasks = getRunningTeammatesSorted(tasks) + const teammateCount = teammateTasks.length + + // Check for non-teammate background tasks (local_agent, local_bash, etc.) + const hasNonTeammateBackgroundTasks = Object.values(tasks).some( + t => isBackgroundTask(t) && t.type !== 'in_process_teammate', + ) + + // Track previous teammate count to detect when teammates are removed + const prevTeammateCountRef = useRef(teammateCount) + + // Clamp selection index if teammates are removed or reset when count becomes 0 + useEffect(() => { + const prevCount = prevTeammateCountRef.current + prevTeammateCountRef.current = teammateCount + + setAppState(prev => { + const currentTeammates = getRunningTeammatesSorted(prev.tasks) + const currentCount = currentTeammates.length + + // When teammates are removed (count goes from >0 to 0), reset selection + // Only reset if we previously had teammates (not on initial mount with 0) + // Don't clobber viewSelectionMode if actively viewing a teammate transcript — + // the user may be reviewing a completed teammate and needs escape to exit + if ( + currentCount === 0 && + prevCount > 0 && + prev.selectedIPAgentIndex !== -1 + ) { + if (prev.viewSelectionMode === 'viewing-agent') { + return { + ...prev, + selectedIPAgentIndex: -1, + } + } + return { + ...prev, + selectedIPAgentIndex: -1, + viewSelectionMode: 'none', + } + } + + // Clamp if index is out of bounds + // Max valid index is currentCount (the "hide" row) when spinner tree is shown + const maxIndex = + prev.expandedView === 'teammates' ? currentCount : currentCount - 1 + if (currentCount > 0 && prev.selectedIPAgentIndex > maxIndex) { + return { + ...prev, + selectedIPAgentIndex: maxIndex, + } + } + + return prev + }) + }, [teammateCount, setAppState]) + + // Get the selected teammate's task info + const getSelectedTeammate = (): { + taskId: string + task: InProcessTeammateTaskState + } | null => { + if (teammateCount === 0) return null + const selectedIndex = selectedIPAgentIndex + const task = teammateTasks[selectedIndex] + if (!task) return null + + return { taskId: task.id, task } + } + + const handleKeyDown = (e: KeyboardEvent): void => { + // Escape in viewing mode: + // - If teammate is running: abort current work only (stops current turn, teammate stays alive) + // - If teammate is not running (completed/killed/failed): exit the view back to leader + if (e.key === 'escape' && viewSelectionMode === 'viewing-agent') { + e.preventDefault() + const taskId = viewingAgentTaskId + if (taskId) { + const task = tasks[taskId] + if (isInProcessTeammateTask(task) && task.status === 'running') { + // Abort currentWorkAbortController (stops current turn) NOT abortController (kills teammate) + task.currentWorkAbortController?.abort() + return + } + } + // Teammate is not running or task doesn't exist — exit the view + exitTeammateView(setAppState) + return + } + + // Escape in selection mode: exit selection without aborting leader + if (e.key === 'escape' && viewSelectionMode === 'selecting-agent') { + e.preventDefault() + setAppState(prev => ({ + ...prev, + viewSelectionMode: 'none', + selectedIPAgentIndex: -1, + })) + return + } + + // Shift+Up/Down for teammate transcript switching (with wrapping) + // Index -1 represents the leader, 0+ are teammates + // When showSpinnerTree is true, index === teammateCount is the "hide" row + if (e.shift && (e.key === 'up' || e.key === 'down')) { + e.preventDefault() + if (teammateCount > 0) { + stepTeammateSelection(e.key === 'down' ? 1 : -1, setAppState) + } else if (hasNonTeammateBackgroundTasks) { + options?.onOpenBackgroundTasks?.() + } + return + } + + // 'f' to view selected teammate's transcript (only in selecting mode) + if ( + e.key === 'f' && + viewSelectionMode === 'selecting-agent' && + teammateCount > 0 + ) { + e.preventDefault() + const selected = getSelectedTeammate() + if (selected) { + enterTeammateView(selected.taskId, setAppState) + } + return + } + + // Enter to confirm selection (only when in selecting mode) + if (e.key === 'return' && viewSelectionMode === 'selecting-agent') { + e.preventDefault() + if (selectedIPAgentIndex === -1) { + exitTeammateView(setAppState) + } else if (selectedIPAgentIndex >= teammateCount) { + // "Hide" row selected - collapse the spinner tree + setAppState(prev => ({ + ...prev, + expandedView: 'none' as const, + viewSelectionMode: 'none', + selectedIPAgentIndex: -1, + })) + } else { + const selected = getSelectedTeammate() + if (selected) { + enterTeammateView(selected.taskId, setAppState) + } + } + return + } + + // k to kill selected teammate (only in selecting mode) + if ( + e.key === 'k' && + viewSelectionMode === 'selecting-agent' && + selectedIPAgentIndex >= 0 + ) { + e.preventDefault() + const selected = getSelectedTeammate() + if (selected && selected.task.status === 'running') { + void InProcessTeammateTask.kill(selected.taskId, setAppState) + } + return + } + } + + // Backward-compat bridge: REPL.tsx doesn't yet wire handleKeyDown to + // . Subscribe via useInput and adapt InputEvent → + // KeyboardEvent until the consumer is migrated (separate PR). + // TODO(onKeyDown-migration): remove once REPL passes handleKeyDown. + useInput((_input, _key, event) => { + handleKeyDown(new KeyboardEvent(event.keypress)) + }) + + return { handleKeyDown } +} diff --git a/src/hooks/useBlink.ts b/src/hooks/useBlink.ts new file mode 100644 index 0000000000000000000000000000000000000000..33cac69747ca503a105625d6083c648edd86d5df --- /dev/null +++ b/src/hooks/useBlink.ts @@ -0,0 +1,34 @@ +import { type DOMElement, useAnimationFrame, useTerminalFocus } from '../ink.js' + +const BLINK_INTERVAL_MS = 600 + +/** + * Hook for synchronized blinking animations that pause when offscreen. + * + * Returns a ref to attach to the animated element and the current blink state. + * All instances blink together because they derive state from the same + * animation clock. The clock only runs when at least one subscriber is visible. + * Pauses when the terminal is blurred. + * + * @param enabled - Whether blinking is active + * @returns [ref, isVisible] - Ref to attach to element, true when visible in blink cycle + * + * @example + * function BlinkingDot({ shouldAnimate }) { + * const [ref, isVisible] = useBlink(shouldAnimate) + * return {isVisible ? '●' : ' '} + * } + */ +export function useBlink( + enabled: boolean, + intervalMs: number = BLINK_INTERVAL_MS, +): [ref: (element: DOMElement | null) => void, isVisible: boolean] { + const focused = useTerminalFocus() + const [ref, time] = useAnimationFrame(enabled && focused ? intervalMs : null) + + if (!enabled || !focused) return [ref, true] + + // Derive blink state from time - all instances see the same time so they sync + const isVisible = Math.floor(time / intervalMs) % 2 === 0 + return [ref, isVisible] +} diff --git a/src/hooks/useCanUseTool.tsx b/src/hooks/useCanUseTool.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1eb77d56acfe08262ecdfa0e1c5af2431d6d8a77 --- /dev/null +++ b/src/hooks/useCanUseTool.tsx @@ -0,0 +1,204 @@ +import { c as _c } from "react/compiler-runtime"; +import { feature } from 'bun:bundle'; +import { APIUserAbortError } from '@anthropic-ai/sdk'; +import * as React from 'react'; +import { useCallback } from 'react'; +import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; +import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js'; +import type { ToolUseConfirm } from '../components/permissions/PermissionRequest.js'; +import { Text } from '../ink.js'; +import type { ToolPermissionContext, Tool as ToolType, ToolUseContext } from '../Tool.js'; +import { consumeSpeculativeClassifierCheck, peekSpeculativeClassifierCheck } from '../tools/BashTool/bashPermissions.js'; +import { BASH_TOOL_NAME } from '../tools/BashTool/toolName.js'; +import type { AssistantMessage } from '../types/message.js'; +import { recordAutoModeDenial } from '../utils/autoModeDenials.js'; +import { clearClassifierChecking, setClassifierApproval, setYoloClassifierApproval } from '../utils/classifierApprovals.js'; +import { logForDebugging } from '../utils/debug.js'; +import { AbortError } from '../utils/errors.js'; +import { logError } from '../utils/log.js'; +import type { PermissionDecision } from '../utils/permissions/PermissionResult.js'; +import { hasPermissionsToUseTool } from '../utils/permissions/permissions.js'; +import { jsonStringify } from '../utils/slowOperations.js'; +import { handleCoordinatorPermission } from './toolPermission/handlers/coordinatorHandler.js'; +import { handleInteractivePermission } from './toolPermission/handlers/interactiveHandler.js'; +import { handleSwarmWorkerPermission } from './toolPermission/handlers/swarmWorkerHandler.js'; +import { createPermissionContext, createPermissionQueueOps } from './toolPermission/PermissionContext.js'; +import { logPermissionDecision } from './toolPermission/permissionLogging.js'; +export type CanUseToolFn = Record> = (tool: ToolType, input: Input, toolUseContext: ToolUseContext, assistantMessage: AssistantMessage, toolUseID: string, forceDecision?: PermissionDecision) => Promise>; +function useCanUseTool(setToolUseConfirmQueue, setToolPermissionContext) { + const $ = _c(3); + let t0; + if ($[0] !== setToolPermissionContext || $[1] !== setToolUseConfirmQueue) { + t0 = async (tool, input, toolUseContext, assistantMessage, toolUseID, forceDecision) => new Promise(resolve => { + const ctx = createPermissionContext(tool, input, toolUseContext, assistantMessage, toolUseID, setToolPermissionContext, createPermissionQueueOps(setToolUseConfirmQueue)); + if (ctx.resolveIfAborted(resolve)) { + return; + } + const decisionPromise = forceDecision !== undefined ? Promise.resolve(forceDecision) : hasPermissionsToUseTool(tool, input, toolUseContext, assistantMessage, toolUseID); + return decisionPromise.then(async result => { + if (result.behavior === "allow") { + if (ctx.resolveIfAborted(resolve)) { + return; + } + if (feature("TRANSCRIPT_CLASSIFIER") && result.decisionReason?.type === "classifier" && result.decisionReason.classifier === "auto-mode") { + setYoloClassifierApproval(toolUseID, result.decisionReason.reason); + } + ctx.logDecision({ + decision: "accept", + source: "config" + }); + resolve(ctx.buildAllow(result.updatedInput ?? input, { + decisionReason: result.decisionReason + })); + return; + } + const appState = toolUseContext.getAppState(); + const description = await tool.description(input as never, { + isNonInteractiveSession: toolUseContext.options.isNonInteractiveSession, + toolPermissionContext: appState.toolPermissionContext, + tools: toolUseContext.options.tools + }); + if (ctx.resolveIfAborted(resolve)) { + return; + } + switch (result.behavior) { + case "deny": + { + logPermissionDecision({ + tool, + input, + toolUseContext, + messageId: ctx.messageId, + toolUseID + }, { + decision: "reject", + source: "config" + }); + if (feature("TRANSCRIPT_CLASSIFIER") && result.decisionReason?.type === "classifier" && result.decisionReason.classifier === "auto-mode") { + recordAutoModeDenial({ + toolName: tool.name, + display: description, + reason: result.decisionReason.reason ?? "", + timestamp: Date.now() + }); + toolUseContext.addNotification?.({ + key: "auto-mode-denied", + priority: "immediate", + jsx: <>{tool.userFacingName(input).toLowerCase()} denied by auto mode · /permissions + }); + } + resolve(result); + return; + } + case "ask": + { + if (appState.toolPermissionContext.awaitAutomatedChecksBeforeDialog) { + const coordinatorDecision = await handleCoordinatorPermission({ + ctx, + ...(feature("BASH_CLASSIFIER") ? { + pendingClassifierCheck: result.pendingClassifierCheck + } : {}), + updatedInput: result.updatedInput, + suggestions: result.suggestions, + permissionMode: appState.toolPermissionContext.mode + }); + if (coordinatorDecision) { + resolve(coordinatorDecision); + return; + } + } + if (ctx.resolveIfAborted(resolve)) { + return; + } + const swarmDecision = await handleSwarmWorkerPermission({ + ctx, + description, + ...(feature("BASH_CLASSIFIER") ? { + pendingClassifierCheck: result.pendingClassifierCheck + } : {}), + updatedInput: result.updatedInput, + suggestions: result.suggestions + }); + if (swarmDecision) { + resolve(swarmDecision); + return; + } + if (feature("BASH_CLASSIFIER") && result.pendingClassifierCheck && tool.name === BASH_TOOL_NAME && !appState.toolPermissionContext.awaitAutomatedChecksBeforeDialog) { + const speculativePromise = peekSpeculativeClassifierCheck((input as { + command: string; + }).command); + if (speculativePromise) { + const raceResult = await Promise.race([speculativePromise.then(_temp), new Promise(_temp2)]); + if (ctx.resolveIfAborted(resolve)) { + return; + } + if (raceResult.type === "result" && raceResult.result.matches && raceResult.result.confidence === "high" && feature("BASH_CLASSIFIER")) { + consumeSpeculativeClassifierCheck((input as { + command: string; + }).command); + const matchedRule = raceResult.result.matchedDescription ?? undefined; + if (matchedRule) { + setClassifierApproval(toolUseID, matchedRule); + } + ctx.logDecision({ + decision: "accept", + source: { + type: "classifier" + } + }); + resolve(ctx.buildAllow(result.updatedInput ?? input as Record, { + decisionReason: { + type: "classifier" as const, + classifier: "bash_allow" as const, + reason: `Allowed by prompt rule: "${raceResult.result.matchedDescription}"` + } + })); + return; + } + } + } + handleInteractivePermission({ + ctx, + description, + result, + awaitAutomatedChecksBeforeDialog: appState.toolPermissionContext.awaitAutomatedChecksBeforeDialog, + bridgeCallbacks: feature("BRIDGE_MODE") ? appState.replBridgePermissionCallbacks : undefined, + channelCallbacks: feature("KAIROS") || feature("KAIROS_CHANNELS") ? appState.channelPermissionCallbacks : undefined + }, resolve); + return; + } + } + }).catch(error => { + if (error instanceof AbortError || error instanceof APIUserAbortError) { + logForDebugging(`Permission check threw ${error.constructor.name} for tool=${tool.name}: ${error.message}`); + ctx.logCancelled(); + resolve(ctx.cancelAndAbort(undefined, true)); + } else { + logError(error); + resolve(ctx.cancelAndAbort(undefined, true)); + } + }).finally(() => { + clearClassifierChecking(toolUseID); + }); + }); + $[0] = setToolPermissionContext; + $[1] = setToolUseConfirmQueue; + $[2] = t0; + } else { + t0 = $[2]; + } + return t0; +} +function _temp2(res) { + return setTimeout(res, 2000, { + type: "timeout" as const + }); +} +function _temp(r) { + return { + type: "result" as const, + result: r + }; +} +export default useCanUseTool; +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwiQVBJVXNlckFib3J0RXJyb3IiLCJSZWFjdCIsInVzZUNhbGxiYWNrIiwiQW5hbHl0aWNzTWV0YWRhdGFfSV9WRVJJRklFRF9USElTX0lTX05PVF9DT0RFX09SX0ZJTEVQQVRIUyIsImxvZ0V2ZW50Iiwic2FuaXRpemVUb29sTmFtZUZvckFuYWx5dGljcyIsIlRvb2xVc2VDb25maXJtIiwiVGV4dCIsIlRvb2xQZXJtaXNzaW9uQ29udGV4dCIsIlRvb2wiLCJUb29sVHlwZSIsIlRvb2xVc2VDb250ZXh0IiwiY29uc3VtZVNwZWN1bGF0aXZlQ2xhc3NpZmllckNoZWNrIiwicGVla1NwZWN1bGF0aXZlQ2xhc3NpZmllckNoZWNrIiwiQkFTSF9UT09MX05BTUUiLCJBc3Npc3RhbnRNZXNzYWdlIiwicmVjb3JkQXV0b01vZGVEZW5pYWwiLCJjbGVhckNsYXNzaWZpZXJDaGVja2luZyIsInNldENsYXNzaWZpZXJBcHByb3ZhbCIsInNldFlvbG9DbGFzc2lmaWVyQXBwcm92YWwiLCJsb2dGb3JEZWJ1Z2dpbmciLCJBYm9ydEVycm9yIiwibG9nRXJyb3IiLCJQZXJtaXNzaW9uRGVjaXNpb24iLCJoYXNQZXJtaXNzaW9uc1RvVXNlVG9vbCIsImpzb25TdHJpbmdpZnkiLCJoYW5kbGVDb29yZGluYXRvclBlcm1pc3Npb24iLCJoYW5kbGVJbnRlcmFjdGl2ZVBlcm1pc3Npb24iLCJoYW5kbGVTd2FybVdvcmtlclBlcm1pc3Npb24iLCJjcmVhdGVQZXJtaXNzaW9uQ29udGV4dCIsImNyZWF0ZVBlcm1pc3Npb25RdWV1ZU9wcyIsImxvZ1Blcm1pc3Npb25EZWNpc2lvbiIsIkNhblVzZVRvb2xGbiIsIlJlY29yZCIsInRvb2wiLCJpbnB1dCIsIklucHV0IiwidG9vbFVzZUNvbnRleHQiLCJhc3Npc3RhbnRNZXNzYWdlIiwidG9vbFVzZUlEIiwiZm9yY2VEZWNpc2lvbiIsIlByb21pc2UiLCJ1c2VDYW5Vc2VUb29sIiwic2V0VG9vbFVzZUNvbmZpcm1RdWV1ZSIsInNldFRvb2xQZXJtaXNzaW9uQ29udGV4dCIsIiQiLCJfYyIsInQwIiwicmVzb2x2ZSIsImN0eCIsInJlc29sdmVJZkFib3J0ZWQiLCJkZWNpc2lvblByb21pc2UiLCJ1bmRlZmluZWQiLCJ0aGVuIiwicmVzdWx0IiwiYmVoYXZpb3IiLCJkZWNpc2lvblJlYXNvbiIsInR5cGUiLCJjbGFzc2lmaWVyIiwicmVhc29uIiwibG9nRGVjaXNpb24iLCJkZWNpc2lvbiIsInNvdXJjZSIsImJ1aWxkQWxsb3ciLCJ1cGRhdGVkSW5wdXQiLCJhcHBTdGF0ZSIsImdldEFwcFN0YXRlIiwiZGVzY3JpcHRpb24iLCJpc05vbkludGVyYWN0aXZlU2Vzc2lvbiIsIm9wdGlvbnMiLCJ0b29sUGVybWlzc2lvbkNvbnRleHQiLCJ0b29scyIsIm1lc3NhZ2VJZCIsInRvb2xOYW1lIiwibmFtZSIsImRpc3BsYXkiLCJ0aW1lc3RhbXAiLCJEYXRlIiwibm93IiwiYWRkTm90aWZpY2F0aW9uIiwia2V5IiwicHJpb3JpdHkiLCJqc3giLCJ1c2VyRmFjaW5nTmFtZSIsInRvTG93ZXJDYXNlIiwiYXdhaXRBdXRvbWF0ZWRDaGVja3NCZWZvcmVEaWFsb2ciLCJjb29yZGluYXRvckRlY2lzaW9uIiwicGVuZGluZ0NsYXNzaWZpZXJDaGVjayIsInN1Z2dlc3Rpb25zIiwicGVybWlzc2lvbk1vZGUiLCJtb2RlIiwic3dhcm1EZWNpc2lvbiIsInNwZWN1bGF0aXZlUHJvbWlzZSIsImNvbW1hbmQiLCJyYWNlUmVzdWx0IiwicmFjZSIsIl90ZW1wIiwiX3RlbXAyIiwibWF0Y2hlcyIsImNvbmZpZGVuY2UiLCJtYXRjaGVkUnVsZSIsIm1hdGNoZWREZXNjcmlwdGlvbiIsImNvbnN0IiwiYnJpZGdlQ2FsbGJhY2tzIiwicmVwbEJyaWRnZVBlcm1pc3Npb25DYWxsYmFja3MiLCJjaGFubmVsQ2FsbGJhY2tzIiwiY2hhbm5lbFBlcm1pc3Npb25DYWxsYmFja3MiLCJjYXRjaCIsImVycm9yIiwiY29uc3RydWN0b3IiLCJtZXNzYWdlIiwibG9nQ2FuY2VsbGVkIiwiY2FuY2VsQW5kQWJvcnQiLCJmaW5hbGx5IiwicmVzIiwic2V0VGltZW91dCIsInIiXSwic291cmNlcyI6WyJ1c2VDYW5Vc2VUb29sLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBmZWF0dXJlIH0gZnJvbSAnYnVuOmJ1bmRsZSdcbmltcG9ydCB7IEFQSVVzZXJBYm9ydEVycm9yIH0gZnJvbSAnQGFudGhyb3BpYy1haS9zZGsnXG5pbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZUNhbGxiYWNrIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQge1xuICB0eXBlIEFuYWx5dGljc01ldGFkYXRhX0lfVkVSSUZJRURfVEhJU19JU19OT1RfQ09ERV9PUl9GSUxFUEFUSFMsXG4gIGxvZ0V2ZW50LFxufSBmcm9tICdzcmMvc2VydmljZXMvYW5hbHl0aWNzL2luZGV4LmpzJ1xuaW1wb3J0IHsgc2FuaXRpemVUb29sTmFtZUZvckFuYWx5dGljcyB9IGZyb20gJ3NyYy9zZXJ2aWNlcy9hbmFseXRpY3MvbWV0YWRhdGEuanMnXG5pbXBvcnQgdHlwZSB7IFRvb2xVc2VDb25maXJtIH0gZnJvbSAnLi4vY29tcG9uZW50cy9wZXJtaXNzaW9ucy9QZXJtaXNzaW9uUmVxdWVzdC5qcydcbmltcG9ydCB7IFRleHQgfSBmcm9tICcuLi9pbmsuanMnXG5pbXBvcnQgdHlwZSB7XG4gIFRvb2xQZXJtaXNzaW9uQ29udGV4dCxcbiAgVG9vbCBhcyBUb29sVHlwZSxcbiAgVG9vbFVzZUNvbnRleHQsXG59IGZyb20gJy4uL1Rvb2wuanMnXG5pbXBvcnQge1xuICBjb25zdW1lU3BlY3VsYXRpdmVDbGFzc2lmaWVyQ2hlY2ssXG4gIHBlZWtTcGVjdWxhdGl2ZUNsYXNzaWZpZXJDaGVjayxcbn0gZnJvbSAnLi4vdG9vbHMvQmFzaFRvb2wvYmFzaFBlcm1pc3Npb25zLmpzJ1xuaW1wb3J0IHsgQkFTSF9UT09MX05BTUUgfSBmcm9tICcuLi90b29scy9CYXNoVG9vbC90b29sTmFtZS5qcydcbmltcG9ydCB0eXBlIHsgQXNzaXN0YW50TWVzc2FnZSB9IGZyb20gJy4uL3R5cGVzL21lc3NhZ2UuanMnXG5pbXBvcnQgeyByZWNvcmRBdXRvTW9kZURlbmlhbCB9IGZyb20gJy4uL3V0aWxzL2F1dG9Nb2RlRGVuaWFscy5qcydcbmltcG9ydCB7XG4gIGNsZWFyQ2xhc3NpZmllckNoZWNraW5nLFxuICBzZXRDbGFzc2lmaWVyQXBwcm92YWwsXG4gIHNldFlvbG9DbGFzc2lmaWVyQXBwcm92YWwsXG59IGZyb20gJy4uL3V0aWxzL2NsYXNzaWZpZXJBcHByb3ZhbHMuanMnXG5pbXBvcnQgeyBsb2dGb3JEZWJ1Z2dpbmcgfSBmcm9tICcuLi91dGlscy9kZWJ1Zy5qcydcbmltcG9ydCB7IEFib3J0RXJyb3IgfSBmcm9tICcuLi91dGlscy9lcnJvcnMuanMnXG5pbXBvcnQgeyBsb2dFcnJvciB9IGZyb20gJy4uL3V0aWxzL2xvZy5qcydcbmltcG9ydCB0eXBlIHsgUGVybWlzc2lvbkRlY2lzaW9uIH0gZnJvbSAnLi4vdXRpbHMvcGVybWlzc2lvbnMvUGVybWlzc2lvblJlc3VsdC5qcydcbmltcG9ydCB7IGhhc1Blcm1pc3Npb25zVG9Vc2VUb29sIH0gZnJvbSAnLi4vdXRpbHMvcGVybWlzc2lvbnMvcGVybWlzc2lvbnMuanMnXG5pbXBvcnQgeyBqc29uU3RyaW5naWZ5IH0gZnJvbSAnLi4vdXRpbHMvc2xvd09wZXJhdGlvbnMuanMnXG5pbXBvcnQgeyBoYW5kbGVDb29yZGluYXRvclBlcm1pc3Npb24gfSBmcm9tICcuL3Rvb2xQZXJtaXNzaW9uL2hhbmRsZXJzL2Nvb3JkaW5hdG9ySGFuZGxlci5qcydcbmltcG9ydCB7IGhhbmRsZUludGVyYWN0aXZlUGVybWlzc2lvbiB9IGZyb20gJy4vdG9vbFBlcm1pc3Npb24vaGFuZGxlcnMvaW50ZXJhY3RpdmVIYW5kbGVyLmpzJ1xuaW1wb3J0IHsgaGFuZGxlU3dhcm1Xb3JrZXJQZXJtaXNzaW9uIH0gZnJvbSAnLi90b29sUGVybWlzc2lvbi9oYW5kbGVycy9zd2FybVdvcmtlckhhbmRsZXIuanMnXG5pbXBvcnQge1xuICBjcmVhdGVQZXJtaXNzaW9uQ29udGV4dCxcbiAgY3JlYXRlUGVybWlzc2lvblF1ZXVlT3BzLFxufSBmcm9tICcuL3Rvb2xQZXJtaXNzaW9uL1Blcm1pc3Npb25Db250ZXh0LmpzJ1xuaW1wb3J0IHsgbG9nUGVybWlzc2lvbkRlY2lzaW9uIH0gZnJvbSAnLi90b29sUGVybWlzc2lvbi9wZXJtaXNzaW9uTG9nZ2luZy5qcydcblxuZXhwb3J0IHR5cGUgQ2FuVXNlVG9vbEZuPFxuICBJbnB1dCBleHRlbmRzIFJlY29yZDxzdHJpbmcsIHVua25vd24+ID0gUmVjb3JkPHN0cmluZywgdW5rbm93bj4sXG4+ID0gKFxuICB0b29sOiBUb29sVHlwZSxcbiAgaW5wdXQ6IElucHV0LFxuICB0b29sVXNlQ29udGV4dDogVG9vbFVzZUNvbnRleHQsXG4gIGFzc2lzdGFudE1lc3NhZ2U6IEFzc2lzdGFudE1lc3NhZ2UsXG4gIHRvb2xVc2VJRDogc3RyaW5nLFxuICBmb3JjZURlY2lzaW9uPzogUGVybWlzc2lvbkRlY2lzaW9uPElucHV0PixcbikgPT4gUHJvbWlzZTxQZXJtaXNzaW9uRGVjaXNpb248SW5wdXQ+PlxuXG5mdW5jdGlvbiB1c2VDYW5Vc2VUb29sKFxuICBzZXRUb29sVXNlQ29uZmlybVF1ZXVlOiBSZWFjdC5EaXNwYXRjaDxcbiAgICBSZWFjdC5TZXRTdGF0ZUFjdGlvbjxUb29sVXNlQ29uZmlybVtdPlxuICA+LFxuICBzZXRUb29sUGVybWlzc2lvbkNvbnRleHQ6IChjb250ZXh0OiBUb29sUGVybWlzc2lvbkNvbnRleHQpID0+IHZvaWQsXG4pOiBDYW5Vc2VUb29sRm4ge1xuICByZXR1cm4gdXNlQ2FsbGJhY2s8Q2FuVXNlVG9vbEZuPihcbiAgICBhc3luYyAoXG4gICAgICB0b29sLFxuICAgICAgaW5wdXQsXG4gICAgICB0b29sVXNlQ29udGV4dCxcbiAgICAgIGFzc2lzdGFudE1lc3NhZ2UsXG4gICAgICB0b29sVXNlSUQsXG4gICAgICBmb3JjZURlY2lzaW9uLFxuICAgICkgPT4ge1xuICAgICAgcmV0dXJuIG5ldyBQcm9taXNlKHJlc29sdmUgPT4ge1xuICAgICAgICBjb25zdCBjdHggPSBjcmVhdGVQZXJtaXNzaW9uQ29udGV4dChcbiAgICAgICAgICB0b29sLFxuICAgICAgICAgIGlucHV0LFxuICAgICAgICAgIHRvb2xVc2VDb250ZXh0LFxuICAgICAgICAgIGFzc2lzdGFudE1lc3NhZ2UsXG4gICAgICAgICAgdG9vbFVzZUlELFxuICAgICAgICAgIHNldFRvb2xQZXJtaXNzaW9uQ29udGV4dCxcbiAgICAgICAgICBjcmVhdGVQZXJtaXNzaW9uUXVldWVPcHMoc2V0VG9vbFVzZUNvbmZpcm1RdWV1ZSksXG4gICAgICAgIClcblxuICAgICAgICBpZiAoY3R4LnJlc29sdmVJZkFib3J0ZWQocmVzb2x2ZSkpIHJldHVyblxuXG4gICAgICAgIGNvbnN0IGRlY2lzaW9uUHJvbWlzZSA9XG4gICAgICAgICAgZm9yY2VEZWNpc2lvbiAhPT0gdW5kZWZpbmVkXG4gICAgICAgICAgICA/IFByb21pc2UucmVzb2x2ZShmb3JjZURlY2lzaW9uKVxuICAgICAgICAgICAgOiBoYXNQZXJtaXNzaW9uc1RvVXNlVG9vbChcbiAgICAgICAgICAgICAgICB0b29sLFxuICAgICAgICAgICAgICAgIGlucHV0LFxuICAgICAgICAgICAgICAgIHRvb2xVc2VDb250ZXh0LFxuICAgICAgICAgICAgICAgIGFzc2lzdGFudE1lc3NhZ2UsXG4gICAgICAgICAgICAgICAgdG9vbFVzZUlELFxuICAgICAgICAgICAgICApXG5cbiAgICAgICAgcmV0dXJuIGRlY2lzaW9uUHJvbWlzZVxuICAgICAgICAgIC50aGVuKGFzeW5jIHJlc3VsdCA9PiB7XG4gICAgICAgICAgICAvLyBbQU5ULU9OTFldIExvZyBhbGwgdG9vbCBwZXJtaXNzaW9uIGRlY2lzaW9ucyB3aXRoIHRvb2wgbmFtZSBhbmQgYXJnc1xuICAgICAgICAgICAgaWYgKFwiZXh0ZXJuYWxcIiA9PT0gJ2FudCcpIHtcbiAgICAgICAgICAgICAgbG9nRXZlbnQoJ3Rlbmd1X2ludGVybmFsX3Rvb2xfcGVybWlzc2lvbl9kZWNpc2lvbicsIHtcbiAgICAgICAgICAgICAgICB0b29sTmFtZTogc2FuaXRpemVUb29sTmFtZUZvckFuYWx5dGljcyh0b29sLm5hbWUpLFxuICAgICAgICAgICAgICAgIGJlaGF2aW9yOlxuICAgICAgICAgICAgICAgICAgcmVzdWx0LmJlaGF2aW9yIGFzIEFuYWx5dGljc01ldGFkYXRhX0lfVkVSSUZJRURfVEhJU19JU19OT1RfQ09ERV9PUl9GSUxFUEFUSFMsXG4gICAgICAgICAgICAgICAgLy8gTm90ZTogaW5wdXQgY29udGFpbnMgY29kZS9maWxlcGF0aHMsIG9ubHkgbG9nIGZvciBhbnRzXG4gICAgICAgICAgICAgICAgaW5wdXQ6IGpzb25TdHJpbmdpZnkoXG4gICAgICAgICAgICAgICAgICBpbnB1dCxcbiAgICAgICAgICAgICAgICApIGFzIEFuYWx5dGljc01ldGFkYXRhX0lfVkVSSUZJRURfVEhJU19JU19OT1RfQ09ERV9PUl9GSUxFUEFUSFMsXG4gICAgICAgICAgICAgICAgbWVzc2FnZUlEOlxuICAgICAgICAgICAgICAgICAgY3R4Lm1lc3NhZ2VJZCBhcyBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICAgICAgICAgICAgICAgIGlzTWNwOiB0b29sLmlzTWNwID8/IGZhbHNlLFxuICAgICAgICAgICAgICB9KVxuICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICAvLyBIYXMgcGVybWlzc2lvbnMgdG8gdXNlIHRvb2wsIGdyYW50ZWQgaW4gY29uZmlnXG4gICAgICAgICAgICBpZiAocmVzdWx0LmJlaGF2aW9yID09PSAnYWxsb3cnKSB7XG4gICAgICAgICAgICAgIGlmIChjdHgucmVzb2x2ZUlmQWJvcnRlZChyZXNvbHZlKSkgcmV0dXJuXG4gICAgICAgICAgICAgIC8vIFRyYWNrIGF1dG8gbW9kZSBjbGFzc2lmaWVyIGFwcHJvdmFscyBmb3IgVUkgZGlzcGxheVxuICAgICAgICAgICAgICBpZiAoXG4gICAgICAgICAgICAgICAgZmVhdHVyZSgnVFJBTlNDUklQVF9DTEFTU0lGSUVSJykgJiZcbiAgICAgICAgICAgICAgICByZXN1bHQuZGVjaXNpb25SZWFzb24/LnR5cGUgPT09ICdjbGFzc2lmaWVyJyAmJlxuICAgICAgICAgICAgICAgIHJlc3VsdC5kZWNpc2lvblJlYXNvbi5jbGFzc2lmaWVyID09PSAnYXV0by1tb2RlJ1xuICAgICAgICAgICAgICApIHtcbiAgICAgICAgICAgICAgICBzZXRZb2xvQ2xhc3NpZmllckFwcHJvdmFsKFxuICAgICAgICAgICAgICAgICAgdG9vbFVzZUlELFxuICAgICAgICAgICAgICAgICAgcmVzdWx0LmRlY2lzaW9uUmVhc29uLnJlYXNvbixcbiAgICAgICAgICAgICAgICApXG4gICAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgICBjdHgubG9nRGVjaXNpb24oeyBkZWNpc2lvbjogJ2FjY2VwdCcsIHNvdXJjZTogJ2NvbmZpZycgfSlcblxuICAgICAgICAgICAgICByZXNvbHZlKFxuICAgICAgICAgICAgICAgIGN0eC5idWlsZEFsbG93KHJlc3VsdC51cGRhdGVkSW5wdXQgPz8gaW5wdXQsIHtcbiAgICAgICAgICAgICAgICAgIGRlY2lzaW9uUmVhc29uOiByZXN1bHQuZGVjaXNpb25SZWFzb24sXG4gICAgICAgICAgICAgICAgfSksXG4gICAgICAgICAgICAgIClcbiAgICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIGNvbnN0IGFwcFN0YXRlID0gdG9vbFVzZUNvbnRleHQuZ2V0QXBwU3RhdGUoKVxuICAgICAgICAgICAgY29uc3QgZGVzY3JpcHRpb24gPSBhd2FpdCB0b29sLmRlc2NyaXB0aW9uKGlucHV0IGFzIG5ldmVyLCB7XG4gICAgICAgICAgICAgIGlzTm9uSW50ZXJhY3RpdmVTZXNzaW9uOlxuICAgICAgICAgICAgICAgIHRvb2xVc2VDb250ZXh0Lm9wdGlvbnMuaXNOb25JbnRlcmFjdGl2ZVNlc3Npb24sXG4gICAgICAgICAgICAgIHRvb2xQZXJtaXNzaW9uQ29udGV4dDogYXBwU3RhdGUudG9vbFBlcm1pc3Npb25Db250ZXh0LFxuICAgICAgICAgICAgICB0b29sczogdG9vbFVzZUNvbnRleHQub3B0aW9ucy50b29scyxcbiAgICAgICAgICAgIH0pXG5cbiAgICAgICAgICAgIGlmIChjdHgucmVzb2x2ZUlmQWJvcnRlZChyZXNvbHZlKSkgcmV0dXJuXG5cbiAgICAgICAgICAgIC8vIERvZXMgbm90IGhhdmUgcGVybWlzc2lvbnMgdG8gdXNlIHRvb2wsIGNoZWNrIHRoZSBiZWhhdmlvclxuICAgICAgICAgICAgc3dpdGNoIChyZXN1bHQuYmVoYXZpb3IpIHtcbiAgICAgICAgICAgICAgY2FzZSAnZGVueSc6IHtcbiAgICAgICAgICAgICAgICBsb2dQZXJtaXNzaW9uRGVjaXNpb24oXG4gICAgICAgICAgICAgICAgICB7XG4gICAgICAgICAgICAgICAgICAgIHRvb2wsXG4gICAgICAgICAgICAgICAgICAgIGlucHV0LFxuICAgICAgICAgICAgICAgICAgICB0b29sVXNlQ29udGV4dCxcbiAgICAgICAgICAgICAgICAgICAgbWVzc2FnZUlkOiBjdHgubWVzc2FnZUlkLFxuICAgICAgICAgICAgICAgICAgICB0b29sVXNlSUQsXG4gICAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgICAgeyBkZWNpc2lvbjogJ3JlamVjdCcsIHNvdXJjZTogJ2NvbmZpZycgfSxcbiAgICAgICAgICAgICAgICApXG4gICAgICAgICAgICAgICAgaWYgKFxuICAgICAgICAgICAgICAgICAgZmVhdHVyZSgnVFJBTlNDUklQVF9DTEFTU0lGSUVSJykgJiZcbiAgICAgICAgICAgICAgICAgIHJlc3VsdC5kZWNpc2lvblJlYXNvbj8udHlwZSA9PT0gJ2NsYXNzaWZpZXInICYmXG4gICAgICAgICAgICAgICAgICByZXN1bHQuZGVjaXNpb25SZWFzb24uY2xhc3NpZmllciA9PT0gJ2F1dG8tbW9kZSdcbiAgICAgICAgICAgICAgICApIHtcbiAgICAgICAgICAgICAgICAgIHJlY29yZEF1dG9Nb2RlRGVuaWFsKHtcbiAgICAgICAgICAgICAgICAgICAgdG9vbE5hbWU6IHRvb2wubmFtZSxcbiAgICAgICAgICAgICAgICAgICAgZGlzcGxheTogZGVzY3JpcHRpb24sXG4gICAgICAgICAgICAgICAgICAgIHJlYXNvbjogcmVzdWx0LmRlY2lzaW9uUmVhc29uLnJlYXNvbiA/PyAnJyxcbiAgICAgICAgICAgICAgICAgICAgdGltZXN0YW1wOiBEYXRlLm5vdygpLFxuICAgICAgICAgICAgICAgICAgfSlcbiAgICAgICAgICAgICAgICAgIHRvb2xVc2VDb250ZXh0LmFkZE5vdGlmaWNhdGlvbj8uKHtcbiAgICAgICAgICAgICAgICAgICAga2V5OiAnYXV0by1tb2RlLWRlbmllZCcsXG4gICAgICAgICAgICAgICAgICAgIHByaW9yaXR5OiAnaW1tZWRpYXRlJyxcbiAgICAgICAgICAgICAgICAgICAganN4OiAoXG4gICAgICAgICAgICAgICAgICAgICAgPD5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxUZXh0IGNvbG9yPVwiZXJyb3JcIj5cbiAgICAgICAgICAgICAgICAgICAgICAgICAge3Rvb2wudXNlckZhY2luZ05hbWUoaW5wdXQpLnRvTG93ZXJDYXNlKCl9IGRlbmllZCBieVxuICAgICAgICAgICAgICAgICAgICAgICAgICBhdXRvIG1vZGVcbiAgICAgICAgICAgICAgICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICAgICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPiDCtyAvcGVybWlzc2lvbnM8L1RleHQ+XG4gICAgICAgICAgICAgICAgICAgICAgPC8+XG4gICAgICAgICAgICAgICAgICAgICksXG4gICAgICAgICAgICAgICAgICB9KVxuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICByZXNvbHZlKHJlc3VsdClcbiAgICAgICAgICAgICAgICByZXR1cm5cbiAgICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICAgIGNhc2UgJ2Fzayc6IHtcbiAgICAgICAgICAgICAgICAvLyBGb3IgY29vcmRpbmF0b3Igd29ya2VycywgYXdhaXQgYXV0b21hdGVkIGNoZWNrcyBiZWZvcmUgc2hvd2luZyBkaWFsb2cuXG4gICAgICAgICAgICAgICAgLy8gQmFja2dyb3VuZCB3b3JrZXJzIHNob3VsZCBvbmx5IGludGVycnVwdCB0aGUgdXNlciB3aGVuIGF1dG9tYXRlZCBjaGVja3MgY2FuJ3QgZGVjaWRlLlxuICAgICAgICAgICAgICAgIGlmIChcbiAgICAgICAgICAgICAgICAgIGFwcFN0YXRlLnRvb2xQZXJtaXNzaW9uQ29udGV4dFxuICAgICAgICAgICAgICAgICAgICAuYXdhaXRBdXRvbWF0ZWRDaGVja3NCZWZvcmVEaWFsb2dcbiAgICAgICAgICAgICAgICApIHtcbiAgICAgICAgICAgICAgICAgIGNvbnN0IGNvb3JkaW5hdG9yRGVjaXNpb24gPSBhd2FpdCBoYW5kbGVDb29yZGluYXRvclBlcm1pc3Npb24oXG4gICAgICAgICAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICAgICAgICBjdHgsXG4gICAgICAgICAgICAgICAgICAgICAgLi4uKGZlYXR1cmUoJ0JBU0hfQ0xBU1NJRklFUicpXG4gICAgICAgICAgICAgICAgICAgICAgICA/IHtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBwZW5kaW5nQ2xhc3NpZmllckNoZWNrOlxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVzdWx0LnBlbmRpbmdDbGFzc2lmaWVyQ2hlY2ssXG4gICAgICAgICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgICAgIDoge30pLFxuICAgICAgICAgICAgICAgICAgICAgIHVwZGF0ZWRJbnB1dDogcmVzdWx0LnVwZGF0ZWRJbnB1dCxcbiAgICAgICAgICAgICAgICAgICAgICBzdWdnZXN0aW9uczogcmVzdWx0LnN1Z2dlc3Rpb25zLFxuICAgICAgICAgICAgICAgICAgICAgIHBlcm1pc3Npb25Nb2RlOiBhcHBTdGF0ZS50b29sUGVybWlzc2lvbkNvbnRleHQubW9kZSxcbiAgICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgIClcbiAgICAgICAgICAgICAgICAgIGlmIChjb29yZGluYXRvckRlY2lzaW9uKSB7XG4gICAgICAgICAgICAgICAgICAgIHJlc29sdmUoY29vcmRpbmF0b3JEZWNpc2lvbilcbiAgICAgICAgICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAvLyBudWxsIG1lYW5zIG5laXRoZXIgYXV0b21hdGVkIGNoZWNrIHJlc29sdmVkIC0tIGZhbGwgdGhyb3VnaCB0byBkaWFsb2cgYmVsb3cuXG4gICAgICAgICAgICAgICAgICAvLyBIb29rcyBhbHJlYWR5IHJhbiwgY2xhc3NpZmllciBhbHJlYWR5IGNvbnN1bWVkLlxuICAgICAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgICAgIC8vIEFmdGVyIGF3YWl0aW5nIGF1dG9tYXRlZCBjaGVja3MsIHZlcmlmeSB0aGUgcmVxdWVzdCB3YXNuJ3QgYWJvcnRlZFxuICAgICAgICAgICAgICAgIC8vIHdoaWxlIHdlIHdlcmUgd2FpdGluZy4gV2l0aG91dCB0aGlzIGNoZWNrLCBhIHN0YWxlIGRpYWxvZyBjb3VsZCBhcHBlYXIuXG4gICAgICAgICAgICAgICAgaWYgKGN0eC5yZXNvbHZlSWZBYm9ydGVkKHJlc29sdmUpKSByZXR1cm5cblxuICAgICAgICAgICAgICAgIC8vIEZvciBzd2FybSB3b3JrZXJzLCB0cnkgY2xhc3NpZmllciBhdXRvLWFwcHJvdmFsIHRoZW5cbiAgICAgICAgICAgICAgICAvLyBmb3J3YXJkIHBlcm1pc3Npb24gcmVxdWVzdHMgdG8gdGhlIGxlYWRlciB2aWEgbWFpbGJveC5cbiAgICAgICAgICAgICAgICBjb25zdCBzd2FybURlY2lzaW9uID0gYXdhaXQgaGFuZGxlU3dhcm1Xb3JrZXJQZXJtaXNzaW9uKHtcbiAgICAgICAgICAgICAgICAgIGN0eCxcbiAgICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uLFxuICAgICAgICAgICAgICAgICAgLi4uKGZlYXR1cmUoJ0JBU0hfQ0xBU1NJRklFUicpXG4gICAgICAgICAgICAgICAgICAgID8ge1xuICAgICAgICAgICAgICAgICAgICAgICAgcGVuZGluZ0NsYXNzaWZpZXJDaGVjazogcmVzdWx0LnBlbmRpbmdDbGFzc2lmaWVyQ2hlY2ssXG4gICAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgICA6IHt9KSxcbiAgICAgICAgICAgICAgICAgIHVwZGF0ZWRJbnB1dDogcmVzdWx0LnVwZGF0ZWRJbnB1dCxcbiAgICAgICAgICAgICAgICAgIHN1Z2dlc3Rpb25zOiByZXN1bHQuc3VnZ2VzdGlvbnMsXG4gICAgICAgICAgICAgICAgfSlcbiAgICAgICAgICAgICAgICBpZiAoc3dhcm1EZWNpc2lvbikge1xuICAgICAgICAgICAgICAgICAgcmVzb2x2ZShzd2FybURlY2lzaW9uKVxuICAgICAgICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICAgICAgLy8gR3JhY2UgcGVyaW9kOiB3YWl0IHVwIHRvIDJzIGZvciBzcGVjdWxhdGl2ZSBjbGFzc2lmaWVyXG4gICAgICAgICAgICAgICAgLy8gdG8gcmVzb2x2ZSBiZWZvcmUgc2hvd2luZyB0aGUgZGlhbG9nIChtYWluIGFnZW50IG9ubHkpXG4gICAgICAgICAgICAgICAgaWYgKFxuICAgICAgICAgICAgICAgICAgZmVhdHVyZSgnQkFTSF9DTEFTU0lGSUVSJykgJiZcbiAgICAgICAgICAgICAgICAgIHJlc3VsdC5wZW5kaW5nQ2xhc3NpZmllckNoZWNrICYmXG4gICAgICAgICAgICAgICAgICB0b29sLm5hbWUgPT09IEJBU0hfVE9PTF9OQU1FICYmXG4gICAgICAgICAgICAgICAgICAhYXBwU3RhdGUudG9vbFBlcm1pc3Npb25Db250ZXh0XG4gICAgICAgICAgICAgICAgICAgIC5hd2FpdEF1dG9tYXRlZENoZWNrc0JlZm9yZURpYWxvZ1xuICAgICAgICAgICAgICAgICkge1xuICAgICAgICAgICAgICAgICAgY29uc3Qgc3BlY3VsYXRpdmVQcm9taXNlID0gcGVla1NwZWN1bGF0aXZlQ2xhc3NpZmllckNoZWNrKFxuICAgICAgICAgICAgICAgICAgICAoaW5wdXQgYXMgeyBjb21tYW5kOiBzdHJpbmcgfSkuY29tbWFuZCxcbiAgICAgICAgICAgICAgICAgIClcbiAgICAgICAgICAgICAgICAgIGlmIChzcGVjdWxhdGl2ZVByb21pc2UpIHtcbiAgICAgICAgICAgICAgICAgICAgY29uc3QgcmFjZVJlc3VsdCA9IGF3YWl0IFByb21pc2UucmFjZShbXG4gICAgICAgICAgICAgICAgICAgICAgc3BlY3VsYXRpdmVQcm9taXNlLnRoZW4ociA9PiAoe1xuICAgICAgICAgICAgICAgICAgICAgICAgdHlwZTogJ3Jlc3VsdCcgYXMgY29uc3QsXG4gICAgICAgICAgICAgICAgICAgICAgICByZXN1bHQ6IHIsXG4gICAgICAgICAgICAgICAgICAgICAgfSkpLFxuICAgICAgICAgICAgICAgICAgICAgIG5ldyBQcm9taXNlPHsgdHlwZTogJ3RpbWVvdXQnIH0+KHJlcyA9PlxuICAgICAgICAgICAgICAgICAgICAgICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLXJlc3RyaWN0ZWQtc3ludGF4IC0tIHJlc29sdmVzIHdpdGggYSB2YWx1ZSwgbm90IHZvaWRcbiAgICAgICAgICAgICAgICAgICAgICAgIHNldFRpbWVvdXQocmVzLCAyMDAwLCB7IHR5cGU6ICd0aW1lb3V0JyBhcyBjb25zdCB9KSxcbiAgICAgICAgICAgICAgICAgICAgICApLFxuICAgICAgICAgICAgICAgICAgICBdKVxuXG4gICAgICAgICAgICAgICAgICAgIGlmIChjdHgucmVzb2x2ZUlmQWJvcnRlZChyZXNvbHZlKSkgcmV0dXJuXG5cbiAgICAgICAgICAgICAgICAgICAgaWYgKFxuICAgICAgICAgICAgICAgICAgICAgIHJhY2VSZXN1bHQudHlwZSA9PT0gJ3Jlc3VsdCcgJiZcbiAgICAgICAgICAgICAgICAgICAgICByYWNlUmVzdWx0LnJlc3VsdC5tYXRjaGVzICYmXG4gICAgICAgICAgICAgICAgICAgICAgcmFjZVJlc3VsdC5yZXN1bHQuY29uZmlkZW5jZSA9PT0gJ2hpZ2gnICYmXG4gICAgICAgICAgICAgICAgICAgICAgZmVhdHVyZSgnQkFTSF9DTEFTU0lGSUVSJylcbiAgICAgICAgICAgICAgICAgICAgKSB7XG4gICAgICAgICAgICAgICAgICAgICAgLy8gQ2xhc3NpZmllciBhcHByb3ZlZCB3aXRoaW4gZ3JhY2UgcGVyaW9kIOKAlCBza2lwIGRpYWxvZ1xuICAgICAgICAgICAgICAgICAgICAgIHZvaWQgY29uc3VtZVNwZWN1bGF0aXZlQ2xhc3NpZmllckNoZWNrKFxuICAgICAgICAgICAgICAgICAgICAgICAgKGlucHV0IGFzIHsgY29tbWFuZDogc3RyaW5nIH0pLmNvbW1hbmQsXG4gICAgICAgICAgICAgICAgICAgICAgKVxuXG4gICAgICAgICAgICAgICAgICAgICAgY29uc3QgbWF0Y2hlZFJ1bGUgPVxuICAgICAgICAgICAgICAgICAgICAgICAgcmFjZVJlc3VsdC5yZXN1bHQubWF0Y2hlZERlc2NyaXB0aW9uID8/IHVuZGVmaW5lZFxuICAgICAgICAgICAgICAgICAgICAgIGlmIChtYXRjaGVkUnVsZSkge1xuICAgICAgICAgICAgICAgICAgICAgICAgc2V0Q2xhc3NpZmllckFwcHJvdmFsKHRvb2xVc2VJRCwgbWF0Y2hlZFJ1bGUpXG4gICAgICAgICAgICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICAgICAgICAgICAgY3R4LmxvZ0RlY2lzaW9uKHtcbiAgICAgICAgICAgICAgICAgICAgICAgIGRlY2lzaW9uOiAnYWNjZXB0JyxcbiAgICAgICAgICAgICAgICAgICAgICAgIHNvdXJjZTogeyB0eXBlOiAnY2xhc3NpZmllcicgfSxcbiAgICAgICAgICAgICAgICAgICAgICB9KVxuICAgICAgICAgICAgICAgICAgICAgIHJlc29sdmUoXG4gICAgICAgICAgICAgICAgICAgICAgICBjdHguYnVpbGRBbGxvdyhcbiAgICAgICAgICAgICAgICAgICAgICAgICAgcmVzdWx0LnVwZGF0ZWRJbnB1dCA/P1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIChpbnB1dCBhcyBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPiksXG4gICAgICAgICAgICAgICAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBkZWNpc2lvblJlYXNvbjoge1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHlwZTogJ2NsYXNzaWZpZXInIGFzIGNvbnN0LFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NpZmllcjogJ2Jhc2hfYWxsb3cnIGFzIGNvbnN0LFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVhc29uOiBgQWxsb3dlZCBieSBwcm9tcHQgcnVsZTogXCIke3JhY2VSZXN1bHQucmVzdWx0Lm1hdGNoZWREZXNjcmlwdGlvbn1cImAsXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgICAgICAgICksXG4gICAgICAgICAgICAgICAgICAgICAgKVxuICAgICAgICAgICAgICAgICAgICAgIHJldHVyblxuICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgIC8vIFRpbWVvdXQgb3Igbm8gbWF0Y2gg4oCUIGZhbGwgdGhyb3VnaCB0byBzaG93IGRpYWxvZ1xuICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgICAgIC8vIFNob3cgZGlhbG9nIGFuZCBzdGFydCBob29rcy9jbGFzc2lmaWVyIGluIGJhY2tncm91bmRcbiAgICAgICAgICAgICAgICBoYW5kbGVJbnRlcmFjdGl2ZVBlcm1pc3Npb24oXG4gICAgICAgICAgICAgICAgICB7XG4gICAgICAgICAgICAgICAgICAgIGN0eCxcbiAgICAgICAgICAgICAgICAgICAgZGVzY3JpcHRpb24sXG4gICAgICAgICAgICAgICAgICAgIHJlc3VsdCxcbiAgICAgICAgICAgICAgICAgICAgYXdhaXRBdXRvbWF0ZWRDaGVja3NCZWZvcmVEaWFsb2c6XG4gICAgICAgICAgICAgICAgICAgICAgYXBwU3RhdGUudG9vbFBlcm1pc3Npb25Db250ZXh0XG4gICAgICAgICAgICAgICAgICAgICAgICAuYXdhaXRBdXRvbWF0ZWRDaGVja3NCZWZvcmVEaWFsb2csXG4gICAgICAgICAgICAgICAgICAgIGJyaWRnZUNhbGxiYWNrczogZmVhdHVyZSgnQlJJREdFX01PREUnKVxuICAgICAgICAgICAgICAgICAgICAgID8gYXBwU3RhdGUucmVwbEJyaWRnZVBlcm1pc3Npb25DYWxsYmFja3NcbiAgICAgICAgICAgICAgICAgICAgICA6IHVuZGVmaW5lZCxcbiAgICAgICAgICAgICAgICAgICAgY2hhbm5lbENhbGxiYWNrczpcbiAgICAgICAgICAgICAgICAgICAgICBmZWF0dXJlKCdLQUlST1MnKSB8fCBmZWF0dXJlKCdLQUlST1NfQ0hBTk5FTFMnKVxuICAgICAgICAgICAgICAgICAgICAgICAgPyBhcHBTdGF0ZS5jaGFubmVsUGVybWlzc2lvbkNhbGxiYWNrc1xuICAgICAgICAgICAgICAgICAgICAgICAgOiB1bmRlZmluZWQsXG4gICAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgICAgcmVzb2x2ZSxcbiAgICAgICAgICAgICAgICApXG5cbiAgICAgICAgICAgICAgICByZXR1cm5cbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0pXG4gICAgICAgICAgLmNhdGNoKGVycm9yID0+IHtcbiAgICAgICAgICAgIGlmIChcbiAgICAgICAgICAgICAgZXJyb3IgaW5zdGFuY2VvZiBBYm9ydEVycm9yIHx8XG4gICAgICAgICAgICAgIGVycm9yIGluc3RhbmNlb2YgQVBJVXNlckFib3J0RXJyb3JcbiAgICAgICAgICAgICkge1xuICAgICAgICAgICAgICBsb2dGb3JEZWJ1Z2dpbmcoXG4gICAgICAgICAgICAgICAgYFBlcm1pc3Npb24gY2hlY2sgdGhyZXcgJHtlcnJvci5jb25zdHJ1Y3Rvci5uYW1lfSBmb3IgdG9vbD0ke3Rvb2wubmFtZX06ICR7ZXJyb3IubWVzc2FnZX1gLFxuICAgICAgICAgICAgICApXG4gICAgICAgICAgICAgIGN0eC5sb2dDYW5jZWxsZWQoKVxuICAgICAgICAgICAgICByZXNvbHZlKGN0eC5jYW5jZWxBbmRBYm9ydCh1bmRlZmluZWQsIHRydWUpKVxuICAgICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgICAgbG9nRXJyb3IoZXJyb3IpXG4gICAgICAgICAgICAgIHJlc29sdmUoY3R4LmNhbmNlbEFuZEFib3J0KHVuZGVmaW5lZCwgdHJ1ZSkpXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSlcbiAgICAgICAgICAuZmluYWxseSgoKSA9PiB7XG4gICAgICAgICAgICBjbGVhckNsYXNzaWZpZXJDaGVja2luZyh0b29sVXNlSUQpXG4gICAgICAgICAgfSlcbiAgICAgIH0pXG4gICAgfSxcbiAgICBbc2V0VG9vbFVzZUNvbmZpcm1RdWV1ZSwgc2V0VG9vbFBlcm1pc3Npb25Db250ZXh0XSxcbiAgKVxufVxuXG5leHBvcnQgZGVmYXVsdCB1c2VDYW5Vc2VUb29sXG4iXSwibWFwcGluZ3MiOiI7QUFBQSxTQUFTQSxPQUFPLFFBQVEsWUFBWTtBQUNwQyxTQUFTQyxpQkFBaUIsUUFBUSxtQkFBbUI7QUFDckQsT0FBTyxLQUFLQyxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxXQUFXLFFBQVEsT0FBTztBQUNuQyxTQUNFLEtBQUtDLDBEQUEwRCxFQUMvREMsUUFBUSxRQUNILGlDQUFpQztBQUN4QyxTQUFTQyw0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 \ No newline at end of file diff --git a/src/hooks/useChromeExtensionNotification.tsx b/src/hooks/useChromeExtensionNotification.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a7d4416a9dbec3e2762aab31fd676db321657249 --- /dev/null +++ b/src/hooks/useChromeExtensionNotification.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { Text } from '../ink.js'; +import { isClaudeAISubscriber } from '../utils/auth.js'; +import { isChromeExtensionInstalled, shouldEnableClaudeInChrome } from '../utils/claudeInChrome/setup.js'; +import { isRunningOnHomespace } from '../utils/envUtils.js'; +import { useStartupNotification } from './notifs/useStartupNotification.js'; +function getChromeFlag(): boolean | undefined { + if (process.argv.includes('--chrome')) { + return true; + } + if (process.argv.includes('--no-chrome')) { + return false; + } + return undefined; +} +export function useChromeExtensionNotification() { + useStartupNotification(_temp); +} +async function _temp() { + const chromeFlag = getChromeFlag(); + if (!shouldEnableClaudeInChrome(chromeFlag)) { + return null; + } + if (true && !isClaudeAISubscriber()) { + return { + key: "chrome-requires-subscription", + jsx: Claude in Chrome requires a claude.ai subscription, + priority: "immediate", + timeoutMs: 5000 + }; + } + const installed = await isChromeExtensionInstalled(); + if (!installed && !isRunningOnHomespace()) { + return { + key: "chrome-extension-not-detected", + jsx: Chrome extension not detected · https://claude.ai/chrome to install, + priority: "immediate", + timeoutMs: 3000 + }; + } + if (chromeFlag === undefined) { + return { + key: "claude-in-chrome-default-enabled", + text: "Claude in Chrome enabled \xB7 /chrome", + priority: "low" + }; + } + return null; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJpc0NsYXVkZUFJU3Vic2NyaWJlciIsImlzQ2hyb21lRXh0ZW5zaW9uSW5zdGFsbGVkIiwic2hvdWxkRW5hYmxlQ2xhdWRlSW5DaHJvbWUiLCJpc1J1bm5pbmdPbkhvbWVzcGFjZSIsInVzZVN0YXJ0dXBOb3RpZmljYXRpb24iLCJnZXRDaHJvbWVGbGFnIiwicHJvY2VzcyIsImFyZ3YiLCJpbmNsdWRlcyIsInVuZGVmaW5lZCIsInVzZUNocm9tZUV4dGVuc2lvbk5vdGlmaWNhdGlvbiIsIl90ZW1wIiwiY2hyb21lRmxhZyIsImtleSIsImpzeCIsInByaW9yaXR5IiwidGltZW91dE1zIiwiaW5zdGFsbGVkIiwidGV4dCJdLCJzb3VyY2VzIjpbInVzZUNocm9tZUV4dGVuc2lvbk5vdGlmaWNhdGlvbi50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBUZXh0IH0gZnJvbSAnLi4vaW5rLmpzJ1xuaW1wb3J0IHsgaXNDbGF1ZGVBSVN1YnNjcmliZXIgfSBmcm9tICcuLi91dGlscy9hdXRoLmpzJ1xuaW1wb3J0IHtcbiAgaXNDaHJvbWVFeHRlbnNpb25JbnN0YWxsZWQsXG4gIHNob3VsZEVuYWJsZUNsYXVkZUluQ2hyb21lLFxufSBmcm9tICcuLi91dGlscy9jbGF1ZGVJbkNocm9tZS9zZXR1cC5qcydcbmltcG9ydCB7IGlzUnVubmluZ09uSG9tZXNwYWNlIH0gZnJvbSAnLi4vdXRpbHMvZW52VXRpbHMuanMnXG5pbXBvcnQgeyB1c2VTdGFydHVwTm90aWZpY2F0aW9uIH0gZnJvbSAnLi9ub3RpZnMvdXNlU3RhcnR1cE5vdGlmaWNhdGlvbi5qcydcblxuZnVuY3Rpb24gZ2V0Q2hyb21lRmxhZygpOiBib29sZWFuIHwgdW5kZWZpbmVkIHtcbiAgaWYgKHByb2Nlc3MuYXJndi5pbmNsdWRlcygnLS1jaHJvbWUnKSkge1xuICAgIHJldHVybiB0cnVlXG4gIH1cbiAgaWYgKHByb2Nlc3MuYXJndi5pbmNsdWRlcygnLS1uby1jaHJvbWUnKSkge1xuICAgIHJldHVybiBmYWxzZVxuICB9XG4gIHJldHVybiB1bmRlZmluZWRcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHVzZUNocm9tZUV4dGVuc2lvbk5vdGlmaWNhdGlvbigpOiB2b2lkIHtcbiAgdXNlU3RhcnR1cE5vdGlmaWNhdGlvbihhc3luYyAoKSA9PiB7XG4gICAgY29uc3QgY2hyb21lRmxhZyA9IGdldENocm9tZUZsYWcoKVxuICAgIGlmICghc2hvdWxkRW5hYmxlQ2xhdWRlSW5DaHJvbWUoY2hyb21lRmxhZykpIHJldHVybiBudWxsXG5cbiAgICAvLyBDbGF1ZGUgaW4gQ2hyb21lIGlzIG9ubHkgc3VwcG9ydGVkIGZvciBjbGF1ZGUuYWkgc3Vic2NyaWJlcnMgKHVubGVzcyB1c2VyIGlzIGFudClcbiAgICBpZiAoXCJleHRlcm5hbFwiICE9PSAnYW50JyAmJiAhaXNDbGF1ZGVBSVN1YnNjcmliZXIoKSkge1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAga2V5OiAnY2hyb21lLXJlcXVpcmVzLXN1YnNjcmlwdGlvbicsXG4gICAgICAgIGpzeDogKFxuICAgICAgICAgIDxUZXh0IGNvbG9yPVwiZXJyb3JcIj5cbiAgICAgICAgICAgIENsYXVkZSBpbiBDaHJvbWUgcmVxdWlyZXMgYSBjbGF1ZGUuYWkgc3Vic2NyaXB0aW9uXG4gICAgICAgICAgPC9UZXh0PlxuICAgICAgICApLFxuICAgICAgICBwcmlvcml0eTogJ2ltbWVkaWF0ZScsXG4gICAgICAgIHRpbWVvdXRNczogNTAwMCxcbiAgICAgIH1cbiAgICB9XG5cbiAgICBjb25zdCBpbnN0YWxsZWQgPSBhd2FpdCBpc0Nocm9tZUV4dGVuc2lvbkluc3RhbGxlZCgpXG4gICAgaWYgKCFpbnN0YWxsZWQgJiYgIWlzUnVubmluZ09uSG9tZXNwYWNlKCkpIHtcbiAgICAgIC8vIFNraXAgbm90aWZpY2F0aW9uIG9uIEhvbWVzcGFjZSBzaW5jZSBDaHJvbWUgc2V0dXAgcmVxdWlyZXMgZGlmZmVyZW50IHN0ZXBzIChzZWUgZ28vaHNwcm94eSlcbiAgICAgIHJldHVybiB7XG4gICAgICAgIGtleTogJ2Nocm9tZS1leHRlbnNpb24tbm90LWRldGVjdGVkJyxcbiAgICAgICAganN4OiAoXG4gICAgICAgICAgPFRleHQgY29sb3I9XCJ3YXJuaW5nXCI+XG4gICAgICAgICAgICBDaHJvbWUgZXh0ZW5zaW9uIG5vdCBkZXRlY3RlZCDCtyBodHRwczovL2NsYXVkZS5haS9jaHJvbWUgdG8gaW5zdGFsbFxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgKSxcbiAgICAgICAgLy8gVE9ETyhoYWNreW9uKTogTG93ZXIgdGhlIHByaW9yaXR5IGlmIHRoZSBjbGF1ZGUtaW4tY2hyb21lIGludGVncmF0aW9uIGlzIG5vIGxvbmdlciBvcHQtaW5cbiAgICAgICAgcHJpb3JpdHk6ICdpbW1lZGlhdGUnLFxuICAgICAgICB0aW1lb3V0TXM6IDMwMDAsXG4gICAgICB9XG4gICAgfVxuICAgIGlmIChjaHJvbWVGbGFnID09PSB1bmRlZmluZWQpIHtcbiAgICAgIC8vIFNob3cgbG93IHByaW9yaXR5IG5vdGlmaWNhdGlvbiBvbmx5IHdoZW4gQ2hyb21lIGlzIGVuYWJsZWQgYnkgZGVmYXVsdFxuICAgICAgLy8gKG5vdCBleHBsaWNpdGx5IGVuYWJsZWQgd2l0aCAtLWNocm9tZSBvciBkaXNhYmxlZCB3aXRoIC0tbm8tY2hyb21lKVxuICAgICAgcmV0dXJuIHtcbiAgICAgICAga2V5OiAnY2xhdWRlLWluLWNocm9tZS1kZWZhdWx0LWVuYWJsZWQnLFxuICAgICAgICB0ZXh0OiBgQ2xhdWRlIGluIENocm9tZSBlbmFibGVkIMK3IC9jaHJvbWVgLFxuICAgICAgICBwcmlvcml0eTogJ2xvdycsXG4gICAgICB9XG4gICAgfVxuICAgIHJldHVybiBudWxsXG4gIH0pXG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsSUFBSSxRQUFRLFdBQVc7QUFDaEMsU0FBU0Msb0JBQW9CLFFBQVEsa0JBQWtCO0FBQ3ZELFNBQ0VDLDBCQUEwQixFQUMxQkMsMEJBQTBCLFFBQ3JCLGtDQUFrQztBQUN6QyxTQUFTQyxvQkFBb0IsUUFBUSxzQkFBc0I7QUFDM0QsU0FBU0Msc0JBQXNCLFFBQVEsb0NBQW9DO0FBRTNFLFNBQVNDLGFBQWFBLENBQUEsQ0FBRSxFQUFFLE9BQU8sR0FBRyxTQUFTLENBQUM7RUFDNUMsSUFBSUMsT0FBTyxDQUFDQyxJQUFJLENBQUNDLFFBQVEsQ0FBQyxVQUFVLENBQUMsRUFBRTtJQUNyQyxPQUFPLElBQUk7RUFDYjtFQUNBLElBQUlGLE9BQU8sQ0FBQ0MsSUFBSSxDQUFDQyxRQUFRLENBQUMsYUFBYSxDQUFDLEVBQUU7SUFDeEMsT0FBTyxLQUFLO0VBQ2Q7RUFDQSxPQUFPQyxTQUFTO0FBQ2xCO0FBRUEsT0FBTyxTQUFBQywrQkFBQTtFQUNMTixzQkFBc0IsQ0FBQ08sS0EyQ3RCLENBQUM7QUFBQTtBQTVDRyxlQUFBQSxNQUFBO0VBRUgsTUFBQUMsVUFBQSxHQUFtQlAsYUFBYSxDQUFDLENBQUM7RUFDbEMsSUFBSSxDQUFDSCwwQkFBMEIsQ0FBQ1UsVUFBVSxDQUFDO0lBQUEsT0FBUyxJQUFJO0VBQUE7RUFHeEQsSUFBSSxJQUErQyxJQUEvQyxDQUF5Qlosb0JBQW9CLENBQUMsQ0FBQztJQUFBLE9BQzFDO01BQUFhLEdBQUEsRUFDQSw4QkFBOEI7TUFBQUMsR0FBQSxFQUVqQyxDQUFDLElBQUksQ0FBTyxLQUFPLENBQVAsT0FBTyxDQUFDLGtEQUVwQixFQUZDLElBQUksQ0FFRTtNQUFBQyxRQUFBLEVBRUMsV0FBVztNQUFBQyxTQUFBLEVBQ1Y7SUFDYixDQUFDO0VBQUE7RUFHSCxNQUFBQyxTQUFBLEdBQWtCLE1BQU1oQiwwQkFBMEIsQ0FBQyxDQUFDO0VBQ3BELElBQUksQ0FBQ2dCLFNBQW9DLElBQXJDLENBQWVkLG9CQUFvQixDQUFDLENBQUM7SUFBQSxPQUVoQztNQUFBVSxHQUFBLEVBQ0EsK0JBQStCO01BQUFDLEdBQUEsRUFFbEMsQ0FBQyxJQUFJLENBQU8sS0FBUyxDQUFULFNBQVMsQ0FBQyxtRUFFdEIsRUFGQyxJQUFJLENBRUU7TUFBQUMsUUFBQSxFQUdDLFdBQVc7TUFBQUMsU0FBQSxFQUNWO0lBQ2IsQ0FBQztFQUFBO0VBRUgsSUFBSUosVUFBVSxLQUFLSCxTQUFTO0lBQUEsT0FHbkI7TUFBQUksR0FBQSxFQUNBLGtDQUFrQztNQUFBSyxJQUFBLEVBQ2pDLHVDQUFvQztNQUFBSCxRQUFBLEVBQ2hDO0lBQ1osQ0FBQztFQUFBO0VBQ0YsT0FDTSxJQUFJO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0= \ No newline at end of file diff --git a/src/hooks/useClipboardImageHint.ts b/src/hooks/useClipboardImageHint.ts new file mode 100644 index 0000000000000000000000000000000000000000..48aa5286594ecb9173cd7aed25cbcae59f4180e6 --- /dev/null +++ b/src/hooks/useClipboardImageHint.ts @@ -0,0 +1,77 @@ +import { useEffect, useRef } from 'react' +import { useNotifications } from '../context/notifications.js' +import { getShortcutDisplay } from '../keybindings/shortcutFormat.js' +import { hasImageInClipboard } from '../utils/imagePaste.js' + +const NOTIFICATION_KEY = 'clipboard-image-hint' +// Small debounce to batch rapid focus changes +const FOCUS_CHECK_DEBOUNCE_MS = 1000 +// Don't show the hint more than once per this interval +const HINT_COOLDOWN_MS = 30000 + +/** + * Hook that shows a notification when the terminal regains focus + * and the clipboard contains an image. + * + * @param isFocused - Whether the terminal is currently focused + * @param enabled - Whether image paste is enabled (onImagePaste is defined) + */ +export function useClipboardImageHint( + isFocused: boolean, + enabled: boolean, +): void { + const { addNotification } = useNotifications() + const lastFocusedRef = useRef(isFocused) + const lastHintTimeRef = useRef(0) + const checkTimeoutRef = useRef(null) + + useEffect(() => { + // Only trigger on focus regain (was unfocused, now focused) + const wasFocused = lastFocusedRef.current + lastFocusedRef.current = isFocused + + if (!enabled || !isFocused || wasFocused) { + return + } + + // Clear any pending check + if (checkTimeoutRef.current) { + clearTimeout(checkTimeoutRef.current) + } + + // Small debounce to batch rapid focus changes + checkTimeoutRef.current = setTimeout( + async (checkTimeoutRef, lastHintTimeRef, addNotification) => { + checkTimeoutRef.current = null + + // Check cooldown to avoid spamming the user + const now = Date.now() + if (now - lastHintTimeRef.current < HINT_COOLDOWN_MS) { + return + } + + // Check if clipboard has an image (async osascript call) + if (await hasImageInClipboard()) { + lastHintTimeRef.current = now + addNotification({ + key: NOTIFICATION_KEY, + text: `Image in clipboard · ${getShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v')} to paste`, + priority: 'immediate', + timeoutMs: 8000, + }) + } + }, + FOCUS_CHECK_DEBOUNCE_MS, + checkTimeoutRef, + lastHintTimeRef, + addNotification, + ) + + return () => { + if (checkTimeoutRef.current) { + clearTimeout(checkTimeoutRef.current) + checkTimeoutRef.current = null + } + } + }, [isFocused, enabled, addNotification]) +} diff --git a/src/hooks/useCommandKeybindings.tsx b/src/hooks/useCommandKeybindings.tsx new file mode 100644 index 0000000000000000000000000000000000000000..55810d623579a88e98e9ed80a3b9910b78ef2bf5 --- /dev/null +++ b/src/hooks/useCommandKeybindings.tsx @@ -0,0 +1,108 @@ +import { c as _c } from "react/compiler-runtime"; +/** + * Component that registers keybinding handlers for command bindings. + * + * Must be rendered inside KeybindingSetup to have access to the keybinding context. + * Reads "command:*" actions from the current keybinding configuration and registers + * handlers that invoke the corresponding slash command via onSubmit. + * + * Commands triggered via keybinding are treated as "immediate" - they execute right + * away and preserve the user's existing input text (the prompt is not cleared). + */ +import { useMemo } from 'react'; +import { useIsModalOverlayActive } from '../context/overlayContext.js'; +import { useOptionalKeybindingContext } from '../keybindings/KeybindingContext.js'; +import { useKeybindings } from '../keybindings/useKeybinding.js'; +import type { PromptInputHelpers } from '../utils/handlePromptSubmit.js'; +type Props = { + // onSubmit accepts additional parameters beyond what we pass here, + // so we use a rest parameter to allow any additional args + onSubmit: (input: string, helpers: PromptInputHelpers, ...rest: [speculationAccept?: undefined, options?: { + fromKeybinding?: boolean; + }]) => void; + /** Set to false to disable command keybindings (e.g., when a dialog is open) */ + isActive?: boolean; +}; +const NOOP_HELPERS: PromptInputHelpers = { + setCursorOffset: () => {}, + clearBuffer: () => {}, + resetHistory: () => {} +}; + +/** + * Registers keybinding handlers for all "command:*" actions found in the + * user's keybinding configuration. When triggered, each handler submits + * the corresponding slash command (e.g., "command:commit" submits "/commit"). + */ +export function CommandKeybindingHandlers(t0) { + const $ = _c(8); + const { + onSubmit, + isActive: t1 + } = t0; + const isActive = t1 === undefined ? true : t1; + const keybindingContext = useOptionalKeybindingContext(); + const isModalOverlayActive = useIsModalOverlayActive(); + let t2; + bb0: { + if (!keybindingContext) { + let t3; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t3 = new Set(); + $[0] = t3; + } else { + t3 = $[0]; + } + t2 = t3; + break bb0; + } + let actions; + if ($[1] !== keybindingContext.bindings) { + actions = new Set(); + for (const binding of keybindingContext.bindings) { + if (binding.action?.startsWith("command:")) { + actions.add(binding.action); + } + } + $[1] = keybindingContext.bindings; + $[2] = actions; + } else { + actions = $[2]; + } + t2 = actions; + } + const commandActions = t2; + let map; + if ($[3] !== commandActions || $[4] !== onSubmit) { + map = {}; + for (const action of commandActions) { + const commandName = action.slice(8); + map[action] = () => { + onSubmit(`/${commandName}`, NOOP_HELPERS, undefined, { + fromKeybinding: true + }); + }; + } + $[3] = commandActions; + $[4] = onSubmit; + $[5] = map; + } else { + map = $[5]; + } + const handlers = map; + const t3 = isActive && !isModalOverlayActive; + let t4; + if ($[6] !== t3) { + t4 = { + context: "Chat", + isActive: t3 + }; + $[6] = t3; + $[7] = t4; + } else { + t4 = $[7]; + } + useKeybindings(handlers, t4); + return null; +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJ1c2VNZW1vIiwidXNlSXNNb2RhbE92ZXJsYXlBY3RpdmUiLCJ1c2VPcHRpb25hbEtleWJpbmRpbmdDb250ZXh0IiwidXNlS2V5YmluZGluZ3MiLCJQcm9tcHRJbnB1dEhlbHBlcnMiLCJQcm9wcyIsIm9uU3VibWl0IiwiaW5wdXQiLCJoZWxwZXJzIiwicmVzdCIsInNwZWN1bGF0aW9uQWNjZXB0Iiwib3B0aW9ucyIsImZyb21LZXliaW5kaW5nIiwiaXNBY3RpdmUiLCJOT09QX0hFTFBFUlMiLCJzZXRDdXJzb3JPZmZzZXQiLCJjbGVhckJ1ZmZlciIsInJlc2V0SGlzdG9yeSIsIkNvbW1hbmRLZXliaW5kaW5nSGFuZGxlcnMiLCJ0MCIsIiQiLCJfYyIsInQxIiwidW5kZWZpbmVkIiwia2V5YmluZGluZ0NvbnRleHQiLCJpc01vZGFsT3ZlcmxheUFjdGl2ZSIsInQyIiwiYmIwIiwidDMiLCJTeW1ib2wiLCJmb3IiLCJTZXQiLCJhY3Rpb25zIiwiYmluZGluZ3MiLCJiaW5kaW5nIiwiYWN0aW9uIiwic3RhcnRzV2l0aCIsImFkZCIsImNvbW1hbmRBY3Rpb25zIiwibWFwIiwiY29tbWFuZE5hbWUiLCJzbGljZSIsImhhbmRsZXJzIiwidDQiLCJjb250ZXh0Il0sInNvdXJjZXMiOlsidXNlQ29tbWFuZEtleWJpbmRpbmdzLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIENvbXBvbmVudCB0aGF0IHJlZ2lzdGVycyBrZXliaW5kaW5nIGhhbmRsZXJzIGZvciBjb21tYW5kIGJpbmRpbmdzLlxuICpcbiAqIE11c3QgYmUgcmVuZGVyZWQgaW5zaWRlIEtleWJpbmRpbmdTZXR1cCB0byBoYXZlIGFjY2VzcyB0byB0aGUga2V5YmluZGluZyBjb250ZXh0LlxuICogUmVhZHMgXCJjb21tYW5kOipcIiBhY3Rpb25zIGZyb20gdGhlIGN1cnJlbnQga2V5YmluZGluZyBjb25maWd1cmF0aW9uIGFuZCByZWdpc3RlcnNcbiAqIGhhbmRsZXJzIHRoYXQgaW52b2tlIHRoZSBjb3JyZXNwb25kaW5nIHNsYXNoIGNvbW1hbmQgdmlhIG9uU3VibWl0LlxuICpcbiAqIENvbW1hbmRzIHRyaWdnZXJlZCB2aWEga2V5YmluZGluZyBhcmUgdHJlYXRlZCBhcyBcImltbWVkaWF0ZVwiIC0gdGhleSBleGVjdXRlIHJpZ2h0XG4gKiBhd2F5IGFuZCBwcmVzZXJ2ZSB0aGUgdXNlcidzIGV4aXN0aW5nIGlucHV0IHRleHQgKHRoZSBwcm9tcHQgaXMgbm90IGNsZWFyZWQpLlxuICovXG5pbXBvcnQgeyB1c2VNZW1vIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VJc01vZGFsT3ZlcmxheUFjdGl2ZSB9IGZyb20gJy4uL2NvbnRleHQvb3ZlcmxheUNvbnRleHQuanMnXG5pbXBvcnQgeyB1c2VPcHRpb25hbEtleWJpbmRpbmdDb250ZXh0IH0gZnJvbSAnLi4va2V5YmluZGluZ3MvS2V5YmluZGluZ0NvbnRleHQuanMnXG5pbXBvcnQgeyB1c2VLZXliaW5kaW5ncyB9IGZyb20gJy4uL2tleWJpbmRpbmdzL3VzZUtleWJpbmRpbmcuanMnXG5pbXBvcnQgdHlwZSB7IFByb21wdElucHV0SGVscGVycyB9IGZyb20gJy4uL3V0aWxzL2hhbmRsZVByb21wdFN1Ym1pdC5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgLy8gb25TdWJtaXQgYWNjZXB0cyBhZGRpdGlvbmFsIHBhcmFtZXRlcnMgYmV5b25kIHdoYXQgd2UgcGFzcyBoZXJlLFxuICAvLyBzbyB3ZSB1c2UgYSByZXN0IHBhcmFtZXRlciB0byBhbGxvdyBhbnkgYWRkaXRpb25hbCBhcmdzXG4gIG9uU3VibWl0OiAoXG4gICAgaW5wdXQ6IHN0cmluZyxcbiAgICBoZWxwZXJzOiBQcm9tcHRJbnB1dEhlbHBlcnMsXG4gICAgLi4ucmVzdDogW1xuICAgICAgc3BlY3VsYXRpb25BY2NlcHQ/OiB1bmRlZmluZWQsXG4gICAgICBvcHRpb25zPzogeyBmcm9tS2V5YmluZGluZz86IGJvb2xlYW4gfSxcbiAgICBdXG4gICkgPT4gdm9pZFxuICAvKiogU2V0IHRvIGZhbHNlIHRvIGRpc2FibGUgY29tbWFuZCBrZXliaW5kaW5ncyAoZS5nLiwgd2hlbiBhIGRpYWxvZyBpcyBvcGVuKSAqL1xuICBpc0FjdGl2ZT86IGJvb2xlYW5cbn1cblxuY29uc3QgTk9PUF9IRUxQRVJTOiBQcm9tcHRJbnB1dEhlbHBlcnMgPSB7XG4gIHNldEN1cnNvck9mZnNldDogKCkgPT4ge30sXG4gIGNsZWFyQnVmZmVyOiAoKSA9PiB7fSxcbiAgcmVzZXRIaXN0b3J5OiAoKSA9PiB7fSxcbn1cblxuLyoqXG4gKiBSZWdpc3RlcnMga2V5YmluZGluZyBoYW5kbGVycyBmb3IgYWxsIFwiY29tbWFuZDoqXCIgYWN0aW9ucyBmb3VuZCBpbiB0aGVcbiAqIHVzZXIncyBrZXliaW5kaW5nIGNvbmZpZ3VyYXRpb24uIFdoZW4gdHJpZ2dlcmVkLCBlYWNoIGhhbmRsZXIgc3VibWl0c1xuICogdGhlIGNvcnJlc3BvbmRpbmcgc2xhc2ggY29tbWFuZCAoZS5nLiwgXCJjb21tYW5kOmNvbW1pdFwiIHN1Ym1pdHMgXCIvY29tbWl0XCIpLlxuICovXG5leHBvcnQgZnVuY3Rpb24gQ29tbWFuZEtleWJpbmRpbmdIYW5kbGVycyh7XG4gIG9uU3VibWl0LFxuICBpc0FjdGl2ZSA9IHRydWUsXG59OiBQcm9wcyk6IG51bGwge1xuICBjb25zdCBrZXliaW5kaW5nQ29udGV4dCA9IHVzZU9wdGlvbmFsS2V5YmluZGluZ0NvbnRleHQoKVxuICBjb25zdCBpc01vZGFsT3ZlcmxheUFjdGl2ZSA9IHVzZUlzTW9kYWxPdmVybGF5QWN0aXZlKClcblxuICAvLyBFeHRyYWN0IGNvbW1hbmQgYWN0aW9ucyBmcm9tIHBhcnNlZCBiaW5kaW5nc1xuICBjb25zdCBjb21tYW5kQWN0aW9ucyA9IHVzZU1lbW8oKCkgPT4ge1xuICAgIGlmICgha2V5YmluZGluZ0NvbnRleHQpIHJldHVybiBuZXcgU2V0PHN0cmluZz4oKVxuICAgIGNvbnN0IGFjdGlvbnMgPSBuZXcgU2V0PHN0cmluZz4oKVxuICAgIGZvciAoY29uc3QgYmluZGluZyBvZiBrZXliaW5kaW5nQ29udGV4dC5iaW5kaW5ncykge1xuICAgICAgaWYgKGJpbmRpbmcuYWN0aW9uPy5zdGFydHNXaXRoKCdjb21tYW5kOicpKSB7XG4gICAgICAgIGFjdGlvbnMuYWRkKGJpbmRpbmcuYWN0aW9uKVxuICAgICAgfVxuICAgIH1cbiAgICByZXR1cm4gYWN0aW9uc1xuICB9LCBba2V5YmluZGluZ0NvbnRleHRdKVxuXG4gIC8vIEJ1aWxkIGhhbmRsZXIgbWFwIGZvciBhbGwgY29tbWFuZCBhY3Rpb25zXG4gIGNvbnN0IGhhbmRsZXJzID0gdXNlTWVtbygoKSA9PiB7XG4gICAgY29uc3QgbWFwOiBSZWNvcmQ8c3RyaW5nLCAoKSA9PiB2b2lkPiA9IHt9XG4gICAgZm9yIChjb25zdCBhY3Rpb24gb2YgY29tbWFuZEFjdGlvbnMpIHtcbiAgICAgIGNvbnN0IGNvbW1hbmROYW1lID0gYWN0aW9uLnNsaWNlKCdjb21tYW5kOicubGVuZ3RoKVxuICAgICAgbWFwW2FjdGlvbl0gPSAoKSA9PiB7XG4gICAgICAgIG9uU3VibWl0KGAvJHtjb21tYW5kTmFtZX1gLCBOT09QX0hFTFBFUlMsIHVuZGVmaW5lZCwge1xuICAgICAgICAgIGZyb21LZXliaW5kaW5nOiB0cnVlLFxuICAgICAgICB9KVxuICAgICAgfVxuICAgIH1cbiAgICByZXR1cm4gbWFwXG4gIH0sIFtjb21tYW5kQWN0aW9ucywgb25TdWJtaXRdKVxuXG4gIHVzZUtleWJpbmRpbmdzKGhhbmRsZXJzLCB7XG4gICAgY29udGV4dDogJ0NoYXQnLFxuICAgIGlzQWN0aXZlOiBpc0FjdGl2ZSAmJiAhaXNNb2RhbE92ZXJsYXlBY3RpdmUsXG4gIH0pXG5cbiAgcmV0dXJuIG51bGxcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsU0FBU0EsT0FBTyxRQUFRLE9BQU87QUFDL0IsU0FBU0MsdUJBQXVCLFFBQVEsOEJBQThCO0FBQ3RFLFNBQVND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 \ No newline at end of file diff --git a/src/hooks/useDiffData.ts b/src/hooks/useDiffData.ts new file mode 100644 index 0000000000000000000000000000000000000000..176bcf09b266a9083e10f4ace6a9623bbe4ae3b2 --- /dev/null +++ b/src/hooks/useDiffData.ts @@ -0,0 +1,110 @@ +import type { StructuredPatchHunk } from 'diff' +import { useEffect, useMemo, useState } from 'react' +import { + fetchGitDiff, + fetchGitDiffHunks, + type GitDiffResult, + type GitDiffStats, +} from '../utils/gitDiff.js' + +const MAX_LINES_PER_FILE = 400 + +export type DiffFile = { + path: string + linesAdded: number + linesRemoved: number + isBinary: boolean + isLargeFile: boolean + isTruncated: boolean + isNewFile?: boolean + isUntracked?: boolean +} + +export type DiffData = { + stats: GitDiffStats | null + files: DiffFile[] + hunks: Map + loading: boolean +} + +/** + * Hook to fetch current git diff data on demand. + * Fetches both stats and hunks when component mounts. + */ +export function useDiffData(): DiffData { + const [diffResult, setDiffResult] = useState(null) + const [hunks, setHunks] = useState>( + new Map(), + ) + const [loading, setLoading] = useState(true) + + // Fetch diff data on mount + useEffect(() => { + let cancelled = false + + async function loadDiffData() { + try { + // Fetch both stats and hunks + const [statsResult, hunksResult] = await Promise.all([ + fetchGitDiff(), + fetchGitDiffHunks(), + ]) + + if (!cancelled) { + setDiffResult(statsResult) + setHunks(hunksResult) + setLoading(false) + } + } catch (_error) { + if (!cancelled) { + setDiffResult(null) + setHunks(new Map()) + setLoading(false) + } + } + } + + void loadDiffData() + + return () => { + cancelled = true + } + }, []) + + return useMemo(() => { + if (!diffResult) { + return { stats: null, files: [], hunks: new Map(), loading } + } + + const { stats, perFileStats } = diffResult + const files: DiffFile[] = [] + + // Iterate over perFileStats to get all files including large/skipped ones + for (const [path, fileStats] of perFileStats) { + const fileHunks = hunks.get(path) + const isUntracked = fileStats.isUntracked ?? false + + // Detect large file (in perFileStats but not in hunks, and not binary/untracked) + const isLargeFile = !fileStats.isBinary && !isUntracked && !fileHunks + + // Detect truncated file (total > limit means we truncated) + const totalLines = fileStats.added + fileStats.removed + const isTruncated = + !isLargeFile && !fileStats.isBinary && totalLines > MAX_LINES_PER_FILE + + files.push({ + path, + linesAdded: fileStats.added, + linesRemoved: fileStats.removed, + isBinary: fileStats.isBinary, + isLargeFile, + isTruncated, + isUntracked, + }) + } + + files.sort((a, b) => a.path.localeCompare(b.path)) + + return { stats, files, hunks, loading: false } + }, [diffResult, hunks, loading]) +} diff --git a/src/hooks/useDiffInIDE.ts b/src/hooks/useDiffInIDE.ts new file mode 100644 index 0000000000000000000000000000000000000000..8fb0d106fbae846c7be06b93991563157a67a06a --- /dev/null +++ b/src/hooks/useDiffInIDE.ts @@ -0,0 +1,379 @@ +import { randomUUID } from 'crypto' +import { basename } from 'path' +import { useEffect, useMemo, useRef, useState } from 'react' +import { logEvent } from 'src/services/analytics/index.js' +import { readFileSync } from 'src/utils/fileRead.js' +import { expandPath } from 'src/utils/path.js' +import type { PermissionOption } from '../components/permissions/FilePermissionDialog/permissionOptions.js' +import type { + MCPServerConnection, + McpSSEIDEServerConfig, + McpWebSocketIDEServerConfig, +} from '../services/mcp/types.js' +import type { ToolUseContext } from '../Tool.js' +import type { FileEdit } from '../tools/FileEditTool/types.js' +import { + getEditsForPatch, + getPatchForEdits, +} from '../tools/FileEditTool/utils.js' +import { getGlobalConfig } from '../utils/config.js' +import { getPatchFromContents } from '../utils/diff.js' +import { isENOENT } from '../utils/errors.js' +import { + callIdeRpc, + getConnectedIdeClient, + getConnectedIdeName, + hasAccessToIDEExtensionDiffFeature, +} from '../utils/ide.js' +import { WindowsToWSLConverter } from '../utils/idePathConversion.js' +import { logError } from '../utils/log.js' +import { getPlatform } from '../utils/platform.js' + +type Props = { + onChange( + option: PermissionOption, + input: { + file_path: string + edits: FileEdit[] + }, + ): void + toolUseContext: ToolUseContext + filePath: string + edits: FileEdit[] + editMode: 'single' | 'multiple' +} + +export function useDiffInIDE({ + onChange, + toolUseContext, + filePath, + edits, + editMode, +}: Props): { + closeTabInIDE: () => void + showingDiffInIDE: boolean + ideName: string + hasError: boolean +} { + const isUnmounted = useRef(false) + const [hasError, setHasError] = useState(false) + + const sha = useMemo(() => randomUUID().slice(0, 6), []) + const tabName = useMemo( + () => `✻ [Claude Code] ${basename(filePath)} (${sha}) ⧉`, + [filePath, sha], + ) + + const shouldShowDiffInIDE = + hasAccessToIDEExtensionDiffFeature(toolUseContext.options.mcpClients) && + getGlobalConfig().diffTool === 'auto' && + // Diffs should only be for file edits. + // File writes may come through here but are not supported for diffs. + !filePath.endsWith('.ipynb') + + const ideName = + getConnectedIdeName(toolUseContext.options.mcpClients) ?? 'IDE' + + async function showDiff(): Promise { + if (!shouldShowDiffInIDE) { + return + } + + try { + logEvent('tengu_ext_will_show_diff', {}) + + const { oldContent, newContent } = await showDiffInIDE( + filePath, + edits, + toolUseContext, + tabName, + ) + // Skip if component has been unmounted + if (isUnmounted.current) { + return + } + + logEvent('tengu_ext_diff_accepted', {}) + + const newEdits = computeEditsFromContents( + filePath, + oldContent, + newContent, + editMode, + ) + + if (newEdits.length === 0) { + // No changes -- edit was rejected (eg. reverted) + logEvent('tengu_ext_diff_rejected', {}) + // We close the tab here because 'no' no longer auto-closes + const ideClient = getConnectedIdeClient( + toolUseContext.options.mcpClients, + ) + if (ideClient) { + // Close the tab in the IDE + await closeTabInIDE(tabName, ideClient) + } + onChange( + { type: 'reject' }, + { + file_path: filePath, + edits: edits, + }, + ) + return + } + + // File was modified - edit was accepted + onChange( + { type: 'accept-once' }, + { + file_path: filePath, + edits: newEdits, + }, + ) + } catch (error) { + logError(error as Error) + setHasError(true) + } + } + + useEffect(() => { + void showDiff() + + // Set flag on unmount + return () => { + isUnmounted.current = true + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return { + closeTabInIDE() { + const ideClient = getConnectedIdeClient(toolUseContext.options.mcpClients) + + if (!ideClient) { + return Promise.resolve() + } + + return closeTabInIDE(tabName, ideClient) + }, + showingDiffInIDE: shouldShowDiffInIDE && !hasError, + ideName: ideName, + hasError, + } +} + +/** + * Re-computes the edits from the old and new contents. This is necessary + * to apply any edits the user may have made to the new contents. + */ +export function computeEditsFromContents( + filePath: string, + oldContent: string, + newContent: string, + editMode: 'single' | 'multiple', +): FileEdit[] { + // Use unformatted patches, otherwise the edits will be formatted. + const singleHunk = editMode === 'single' + const patch = getPatchFromContents({ + filePath, + oldContent, + newContent, + singleHunk, + }) + + if (patch.length === 0) { + return [] + } + + // For single edit mode, verify we only got one hunk + if (singleHunk && patch.length > 1) { + logError( + new Error( + `Unexpected number of hunks: ${patch.length}. Expected 1 hunk.`, + ), + ) + } + + // Re-compute the edits to match the patch + return getEditsForPatch(patch) +} + +/** + * Done if: + * + * 1. Tab is closed in IDE + * 2. Tab is saved in IDE (we then close the tab) + * 3. User selected an option in IDE + * 4. User selected an option in terminal (or hit esc) + * + * Resolves with the new file content. + * + * TODO: Time out after 5 mins of inactivity? + * TODO: Update auto-approval UI when IDE exits + * TODO: Close the IDE tab when the approval prompt is unmounted + */ +async function showDiffInIDE( + file_path: string, + edits: FileEdit[], + toolUseContext: ToolUseContext, + tabName: string, +): Promise<{ oldContent: string; newContent: string }> { + let isCleanedUp = false + + const oldFilePath = expandPath(file_path) + let oldContent = '' + try { + oldContent = readFileSync(oldFilePath) + } catch (e: unknown) { + if (!isENOENT(e)) { + throw e + } + } + + async function cleanup() { + // Careful to avoid race conditions, since this + // function can be called from multiple places. + if (isCleanedUp) { + return + } + isCleanedUp = true + + // Don't fail if this fails + try { + await closeTabInIDE(tabName, ideClient) + } catch (e) { + logError(e as Error) + } + + process.off('beforeExit', cleanup) + toolUseContext.abortController.signal.removeEventListener('abort', cleanup) + } + + // Cleanup if the user hits esc to cancel the tool call - or on exit + toolUseContext.abortController.signal.addEventListener('abort', cleanup) + process.on('beforeExit', cleanup) + + // Open the diff in the IDE + const ideClient = getConnectedIdeClient(toolUseContext.options.mcpClients) + try { + const { updatedFile } = getPatchForEdits({ + filePath: oldFilePath, + fileContents: oldContent, + edits, + }) + + if (!ideClient || ideClient.type !== 'connected') { + throw new Error('IDE client not available') + } + let ideOldPath = oldFilePath + + // Only convert paths if we're in WSL and IDE is on Windows + const ideRunningInWindows = + (ideClient.config as McpSSEIDEServerConfig | McpWebSocketIDEServerConfig) + .ideRunningInWindows === true + if ( + getPlatform() === 'wsl' && + ideRunningInWindows && + process.env.WSL_DISTRO_NAME + ) { + const converter = new WindowsToWSLConverter(process.env.WSL_DISTRO_NAME) + ideOldPath = converter.toIDEPath(oldFilePath) + } + + const rpcResult = await callIdeRpc( + 'openDiff', + { + old_file_path: ideOldPath, + new_file_path: ideOldPath, + new_file_contents: updatedFile, + tab_name: tabName, + }, + ideClient, + ) + + // Convert the raw RPC result to a ToolCallResponse format + const data = Array.isArray(rpcResult) ? rpcResult : [rpcResult] + + // If the user saved the file then take the new contents and resolve with that. + if (isSaveMessage(data)) { + void cleanup() + return { + oldContent: oldContent, + newContent: data[1].text, + } + } else if (isClosedMessage(data)) { + void cleanup() + return { + oldContent: oldContent, + newContent: updatedFile, + } + } else if (isRejectedMessage(data)) { + void cleanup() + return { + oldContent: oldContent, + newContent: oldContent, + } + } + + // Indicates that the tool call completed with none of the expected + // results. Did the user close the IDE? + throw new Error('Not accepted') + } catch (error) { + logError(error as Error) + void cleanup() + throw error + } +} + +async function closeTabInIDE( + tabName: string, + ideClient?: MCPServerConnection | undefined, +): Promise { + try { + if (!ideClient || ideClient.type !== 'connected') { + throw new Error('IDE client not available') + } + + // Use direct RPC to close the tab + await callIdeRpc('close_tab', { tab_name: tabName }, ideClient) + } catch (error) { + logError(error as Error) + // Don't throw - this is a cleanup operation + } +} + +function isClosedMessage(data: unknown): data is { text: 'TAB_CLOSED' } { + return ( + Array.isArray(data) && + typeof data[0] === 'object' && + data[0] !== null && + 'type' in data[0] && + data[0].type === 'text' && + 'text' in data[0] && + data[0].text === 'TAB_CLOSED' + ) +} + +function isRejectedMessage(data: unknown): data is { text: 'DIFF_REJECTED' } { + return ( + Array.isArray(data) && + typeof data[0] === 'object' && + data[0] !== null && + 'type' in data[0] && + data[0].type === 'text' && + 'text' in data[0] && + data[0].text === 'DIFF_REJECTED' + ) +} + +function isSaveMessage( + data: unknown, +): data is [{ text: 'FILE_SAVED' }, { text: string }] { + return ( + Array.isArray(data) && + data[0]?.type === 'text' && + data[0].text === 'FILE_SAVED' && + typeof data[1].text === 'string' + ) +} diff --git a/src/hooks/useHistorySearch.ts b/src/hooks/useHistorySearch.ts new file mode 100644 index 0000000000000000000000000000000000000000..b48c880b794ddc6ece8326a56085b599dcda1536 --- /dev/null +++ b/src/hooks/useHistorySearch.ts @@ -0,0 +1,303 @@ +import { feature } from 'bun:bundle' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { + getModeFromInput, + getValueFromInput, +} from '../components/PromptInput/inputModes.js' +import { makeHistoryReader } from '../history.js' +import { KeyboardEvent } from '../ink/events/keyboard-event.js' +// eslint-disable-next-line custom-rules/prefer-use-keybindings -- backward-compat bridge until consumers wire handleKeyDown to +import { useInput } from '../ink.js' +import { useKeybinding, useKeybindings } from '../keybindings/useKeybinding.js' +import type { PromptInputMode } from '../types/textInputTypes.js' +import type { HistoryEntry } from '../utils/config.js' + +export function useHistorySearch( + onAcceptHistory: (entry: HistoryEntry) => void, + currentInput: string, + onInputChange: (input: string) => void, + onCursorChange: (cursorOffset: number) => void, + currentCursorOffset: number, + onModeChange: (mode: PromptInputMode) => void, + currentMode: PromptInputMode, + isSearching: boolean, + setIsSearching: (isSearching: boolean) => void, + setPastedContents: (pastedContents: HistoryEntry['pastedContents']) => void, + currentPastedContents: HistoryEntry['pastedContents'], +): { + historyQuery: string + setHistoryQuery: (query: string) => void + historyMatch: HistoryEntry | undefined + historyFailedMatch: boolean + handleKeyDown: (e: KeyboardEvent) => void +} { + const [historyQuery, setHistoryQuery] = useState('') + const [historyFailedMatch, setHistoryFailedMatch] = useState(false) + const [originalInput, setOriginalInput] = useState('') + const [originalCursorOffset, setOriginalCursorOffset] = useState(0) + const [originalMode, setOriginalMode] = useState('prompt') + const [originalPastedContents, setOriginalPastedContents] = useState< + HistoryEntry['pastedContents'] + >({}) + const [historyMatch, setHistoryMatch] = useState( + undefined, + ) + const historyReader = useRef | undefined>( + undefined, + ) + const seenPrompts = useRef>(new Set()) + const searchAbortController = useRef(null) + + const closeHistoryReader = useCallback((): void => { + if (historyReader.current) { + // Must explicitly call .return() to trigger the finally block in readLinesReverse, + // which closes the file handle. Without this, file descriptors leak. + void historyReader.current.return(undefined) + historyReader.current = undefined + } + }, []) + + const reset = useCallback((): void => { + setIsSearching(false) + setHistoryQuery('') + setHistoryFailedMatch(false) + setOriginalInput('') + setOriginalCursorOffset(0) + setOriginalMode('prompt') + setOriginalPastedContents({}) + setHistoryMatch(undefined) + closeHistoryReader() + seenPrompts.current.clear() + }, [setIsSearching, closeHistoryReader]) + + const searchHistory = useCallback( + async (resume: boolean, signal?: AbortSignal): Promise => { + if (!isSearching) { + return + } + + if (historyQuery.length === 0) { + closeHistoryReader() + seenPrompts.current.clear() + setHistoryMatch(undefined) + setHistoryFailedMatch(false) + onInputChange(originalInput) + onCursorChange(originalCursorOffset) + onModeChange(originalMode) + setPastedContents(originalPastedContents) + return + } + + if (!resume) { + closeHistoryReader() + historyReader.current = makeHistoryReader() + seenPrompts.current.clear() + } + + if (!historyReader.current) { + return + } + + while (true) { + if (signal?.aborted) { + return + } + + const item = await historyReader.current.next() + if (item.done) { + // No match found - keep last match but mark as failed + setHistoryFailedMatch(true) + return + } + + const display = item.value.display + + const matchPosition = display.lastIndexOf(historyQuery) + if (matchPosition !== -1 && !seenPrompts.current.has(display)) { + seenPrompts.current.add(display) + setHistoryMatch(item.value) + setHistoryFailedMatch(false) + const mode = getModeFromInput(display) + onModeChange(mode) + onInputChange(display) + setPastedContents(item.value.pastedContents) + + // Position cursor relative to the clean value, not the display + const value = getValueFromInput(display) + const cleanMatchPosition = value.lastIndexOf(historyQuery) + onCursorChange( + cleanMatchPosition !== -1 ? cleanMatchPosition : matchPosition, + ) + return + } + } + }, + [ + isSearching, + historyQuery, + closeHistoryReader, + onInputChange, + onCursorChange, + onModeChange, + setPastedContents, + originalInput, + originalCursorOffset, + originalMode, + originalPastedContents, + ], + ) + + // Handler: Start history search (when not searching) + const handleStartSearch = useCallback(() => { + setIsSearching(true) + setOriginalInput(currentInput) + setOriginalCursorOffset(currentCursorOffset) + setOriginalMode(currentMode) + setOriginalPastedContents(currentPastedContents) + historyReader.current = makeHistoryReader() + seenPrompts.current.clear() + }, [ + setIsSearching, + currentInput, + currentCursorOffset, + currentMode, + currentPastedContents, + ]) + + // Handler: Find next match (when searching) + const handleNextMatch = useCallback(() => { + void searchHistory(true) + }, [searchHistory]) + + // Handler: Accept current match and exit search + const handleAccept = useCallback(() => { + if (historyMatch) { + const mode = getModeFromInput(historyMatch.display) + const value = getValueFromInput(historyMatch.display) + onInputChange(value) + onModeChange(mode) + setPastedContents(historyMatch.pastedContents) + } else { + // No match - restore original pasted contents + setPastedContents(originalPastedContents) + } + reset() + }, [ + historyMatch, + onInputChange, + onModeChange, + setPastedContents, + originalPastedContents, + reset, + ]) + + // Handler: Cancel search and restore original input + const handleCancel = useCallback(() => { + onInputChange(originalInput) + onCursorChange(originalCursorOffset) + setPastedContents(originalPastedContents) + reset() + }, [ + onInputChange, + onCursorChange, + setPastedContents, + originalInput, + originalCursorOffset, + originalPastedContents, + reset, + ]) + + // Handler: Execute (accept and submit) + const handleExecute = useCallback(() => { + if (historyQuery.length === 0) { + onAcceptHistory({ + display: originalInput, + pastedContents: originalPastedContents, + }) + } else if (historyMatch) { + const mode = getModeFromInput(historyMatch.display) + const value = getValueFromInput(historyMatch.display) + onModeChange(mode) + onAcceptHistory({ + display: value, + pastedContents: historyMatch.pastedContents, + }) + } + reset() + }, [ + historyQuery, + historyMatch, + onAcceptHistory, + onModeChange, + originalInput, + originalPastedContents, + reset, + ]) + + // Gated off under HISTORY_PICKER — the modal dialog owns ctrl+r there. + useKeybinding('history:search', handleStartSearch, { + context: 'Global', + isActive: feature('HISTORY_PICKER') ? false : !isSearching, + }) + + // History search context keybindings (only active when searching) + const historySearchHandlers = useMemo( + () => ({ + 'historySearch:next': handleNextMatch, + 'historySearch:accept': handleAccept, + 'historySearch:cancel': handleCancel, + 'historySearch:execute': handleExecute, + }), + [handleNextMatch, handleAccept, handleCancel, handleExecute], + ) + + useKeybindings(historySearchHandlers, { + context: 'HistorySearch', + isActive: isSearching, + }) + + // Handle backspace when query is empty (cancels search) + // This is a conditional behavior that doesn't fit the keybinding model + // well (backspace only cancels when query is empty) + const handleKeyDown = (e: KeyboardEvent): void => { + if (!isSearching) return + if (e.key === 'backspace' && historyQuery === '') { + e.preventDefault() + handleCancel() + } + } + + // Backward-compat bridge: PromptInput doesn't yet wire handleKeyDown to + // . Subscribe via useInput and adapt InputEvent → + // KeyboardEvent until the consumer is migrated (separate PR). + // TODO(onKeyDown-migration): remove once PromptInput passes handleKeyDown. + useInput( + (_input, _key, event) => { + handleKeyDown(new KeyboardEvent(event.keypress)) + }, + { isActive: isSearching }, + ) + + // Keep a ref to searchHistory to avoid it being a dependency of useEffect + const searchHistoryRef = useRef(searchHistory) + searchHistoryRef.current = searchHistory + + // Reset history search when query changes + useEffect(() => { + searchAbortController.current?.abort() + const controller = new AbortController() + searchAbortController.current = controller + void searchHistoryRef.current(false, controller.signal) + return () => { + controller.abort() + } + }, [historyQuery]) + + return { + historyQuery, + setHistoryQuery, + historyMatch, + historyFailedMatch, + handleKeyDown, + } +} diff --git a/src/hooks/useInboxPoller.ts b/src/hooks/useInboxPoller.ts new file mode 100644 index 0000000000000000000000000000000000000000..361ba636df4a83309e87bfa26476e2ed2432b306 --- /dev/null +++ b/src/hooks/useInboxPoller.ts @@ -0,0 +1,969 @@ +import { randomUUID } from 'crypto' +import { useCallback, useEffect, useRef } from 'react' +import { useInterval } from 'usehooks-ts' +import type { ToolUseConfirm } from '../components/permissions/PermissionRequest.js' +import { TEAMMATE_MESSAGE_TAG } from '../constants/xml.js' +import { useTerminalNotification } from '../ink/useTerminalNotification.js' +import { sendNotification } from '../services/notifier.js' +import { + type AppState, + useAppState, + useAppStateStore, + useSetAppState, +} from '../state/AppState.js' +import { findToolByName } from '../Tool.js' +import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js' +import { getAllBaseTools } from '../tools.js' +import type { PermissionUpdate } from '../types/permissions.js' +import { logForDebugging } from '../utils/debug.js' +import { + findInProcessTeammateTaskId, + handlePlanApprovalResponse, +} from '../utils/inProcessTeammateHelpers.js' +import { createAssistantMessage } from '../utils/messages.js' +import { + permissionModeFromString, + toExternalPermissionMode, +} from '../utils/permissions/PermissionMode.js' +import { applyPermissionUpdate } from '../utils/permissions/PermissionUpdate.js' +import { jsonStringify } from '../utils/slowOperations.js' +import { isInsideTmux } from '../utils/swarm/backends/detection.js' +import { + ensureBackendsRegistered, + getBackendByType, +} from '../utils/swarm/backends/registry.js' +import type { PaneBackendType } from '../utils/swarm/backends/types.js' +import { TEAM_LEAD_NAME } from '../utils/swarm/constants.js' +import { getLeaderToolUseConfirmQueue } from '../utils/swarm/leaderPermissionBridge.js' +import { sendPermissionResponseViaMailbox } from '../utils/swarm/permissionSync.js' +import { + removeTeammateFromTeamFile, + setMemberMode, +} from '../utils/swarm/teamHelpers.js' +import { unassignTeammateTasks } from '../utils/tasks.js' +import { + getAgentName, + isPlanModeRequired, + isTeamLead, + isTeammate, +} from '../utils/teammate.js' +import { isInProcessTeammate } from '../utils/teammateContext.js' +import { + isModeSetRequest, + isPermissionRequest, + isPermissionResponse, + isPlanApprovalRequest, + isPlanApprovalResponse, + isSandboxPermissionRequest, + isSandboxPermissionResponse, + isShutdownApproved, + isShutdownRequest, + isTeamPermissionUpdate, + markMessagesAsRead, + readUnreadMessages, + type TeammateMessage, + writeToMailbox, +} from '../utils/teammateMailbox.js' +import { + hasPermissionCallback, + hasSandboxPermissionCallback, + processMailboxPermissionResponse, + processSandboxPermissionResponse, +} from './useSwarmPermissionPoller.js' + +/** + * Get the agent name to poll for messages. + * - In-process teammates return undefined (they use waitForNextPromptOrShutdown instead) + * - Process-based teammates use their CLAUDE_CODE_AGENT_NAME + * - Team leads use their name from teamContext.teammates + * - Standalone sessions return undefined + */ +function getAgentNameToPoll(appState: AppState): string | undefined { + // In-process teammates should NOT use useInboxPoller - they have their own + // polling mechanism via waitForNextPromptOrShutdown() in inProcessRunner.ts. + // Using useInboxPoller would cause message routing issues since in-process + // teammates share the same React context and AppState with the leader. + // + // Note: This can be called when the leader's REPL re-renders while an + // in-process teammate's AsyncLocalStorage context is active (due to shared + // setAppState). We return undefined to gracefully skip polling rather than + // throwing, since this is a normal occurrence during concurrent execution. + if (isInProcessTeammate()) { + return undefined + } + if (isTeammate()) { + return getAgentName() + } + // Team lead polls using their agent name (not ID) + if (isTeamLead(appState.teamContext)) { + const leadAgentId = appState.teamContext!.leadAgentId + // Look up the lead's name from teammates map + const leadName = appState.teamContext!.teammates[leadAgentId]?.name + return leadName || 'team-lead' + } + return undefined +} + +const INBOX_POLL_INTERVAL_MS = 1000 + +type Props = { + enabled: boolean + isLoading: boolean + focusedInputDialog: string | undefined + // Returns true if submission succeeded, false if rejected (e.g., query already running) + // Dead code elimination: parameter named onSubmitMessage to avoid "teammate" string in external builds + onSubmitMessage: (formatted: string) => boolean +} + +/** + * Polls the teammate inbox for new messages and submits them as turns. + * + * This hook: + * 1. Polls every 1s for unread messages (teammates or team leads) + * 2. When idle: submits messages immediately as a new turn + * 3. When busy: queues messages in AppState.inbox for UI display, delivers when turn ends + */ +export function useInboxPoller({ + enabled, + isLoading, + focusedInputDialog, + onSubmitMessage, +}: Props): void { + // Assign to original name for clarity within the function + const onSubmitTeammateMessage = onSubmitMessage + const store = useAppStateStore() + const setAppState = useSetAppState() + const inboxMessageCount = useAppState(s => s.inbox.messages.length) + const terminal = useTerminalNotification() + + const poll = useCallback(async () => { + if (!enabled) return + + // Use ref to avoid dependency on appState object (prevents infinite loop) + const currentAppState = store.getState() + const agentName = getAgentNameToPoll(currentAppState) + if (!agentName) return + + const unread = await readUnreadMessages( + agentName, + currentAppState.teamContext?.teamName, + ) + + if (unread.length === 0) return + + logForDebugging(`[InboxPoller] Found ${unread.length} unread message(s)`) + + // Check for plan approval responses and transition out of plan mode if approved + // Security: Only accept approval responses from the team lead + if (isTeammate() && isPlanModeRequired()) { + for (const msg of unread) { + const approvalResponse = isPlanApprovalResponse(msg.text) + // Verify the message is from the team lead to prevent teammates from forging approvals + if (approvalResponse && msg.from === 'team-lead') { + logForDebugging( + `[InboxPoller] Received plan approval response from team-lead: approved=${approvalResponse.approved}`, + ) + if (approvalResponse.approved) { + // Use leader's permission mode if provided, otherwise default + const targetMode = approvalResponse.permissionMode ?? 'default' + + // Transition out of plan mode + setAppState(prev => ({ + ...prev, + toolPermissionContext: applyPermissionUpdate( + prev.toolPermissionContext, + { + type: 'setMode', + mode: toExternalPermissionMode(targetMode), + destination: 'session', + }, + ), + })) + logForDebugging( + `[InboxPoller] Plan approved by team lead, exited plan mode to ${targetMode}`, + ) + } else { + logForDebugging( + `[InboxPoller] Plan rejected by team lead: ${approvalResponse.feedback || 'No feedback provided'}`, + ) + } + } else if (approvalResponse) { + logForDebugging( + `[InboxPoller] Ignoring plan approval response from non-team-lead: ${msg.from}`, + ) + } + } + } + + // Helper to mark messages as read in the inbox file. + // Called after messages are successfully delivered or reliably queued. + const markRead = () => { + void markMessagesAsRead(agentName, currentAppState.teamContext?.teamName) + } + + // Separate permission messages from regular teammate messages + const permissionRequests: TeammateMessage[] = [] + const permissionResponses: TeammateMessage[] = [] + const sandboxPermissionRequests: TeammateMessage[] = [] + const sandboxPermissionResponses: TeammateMessage[] = [] + const shutdownRequests: TeammateMessage[] = [] + const shutdownApprovals: TeammateMessage[] = [] + const teamPermissionUpdates: TeammateMessage[] = [] + const modeSetRequests: TeammateMessage[] = [] + const planApprovalRequests: TeammateMessage[] = [] + const regularMessages: TeammateMessage[] = [] + + for (const m of unread) { + const permReq = isPermissionRequest(m.text) + const permResp = isPermissionResponse(m.text) + const sandboxReq = isSandboxPermissionRequest(m.text) + const sandboxResp = isSandboxPermissionResponse(m.text) + const shutdownReq = isShutdownRequest(m.text) + const shutdownApproval = isShutdownApproved(m.text) + const teamPermUpdate = isTeamPermissionUpdate(m.text) + const modeSetReq = isModeSetRequest(m.text) + const planApprovalReq = isPlanApprovalRequest(m.text) + + if (permReq) { + permissionRequests.push(m) + } else if (permResp) { + permissionResponses.push(m) + } else if (sandboxReq) { + sandboxPermissionRequests.push(m) + } else if (sandboxResp) { + sandboxPermissionResponses.push(m) + } else if (shutdownReq) { + shutdownRequests.push(m) + } else if (shutdownApproval) { + shutdownApprovals.push(m) + } else if (teamPermUpdate) { + teamPermissionUpdates.push(m) + } else if (modeSetReq) { + modeSetRequests.push(m) + } else if (planApprovalReq) { + planApprovalRequests.push(m) + } else { + regularMessages.push(m) + } + } + + // Handle permission requests (leader side) - route to ToolUseConfirmQueue + if ( + permissionRequests.length > 0 && + isTeamLead(currentAppState.teamContext) + ) { + logForDebugging( + `[InboxPoller] Found ${permissionRequests.length} permission request(s)`, + ) + + const setToolUseConfirmQueue = getLeaderToolUseConfirmQueue() + const teamName = currentAppState.teamContext?.teamName + + for (const m of permissionRequests) { + const parsed = isPermissionRequest(m.text) + if (!parsed) continue + + if (setToolUseConfirmQueue) { + // Route through the standard ToolUseConfirmQueue so tmux workers + // get the same tool-specific UI (BashPermissionRequest, FileEditToolDiff, etc.) + // as in-process teammates. + const tool = findToolByName(getAllBaseTools(), parsed.tool_name) + if (!tool) { + logForDebugging( + `[InboxPoller] Unknown tool ${parsed.tool_name}, skipping permission request`, + ) + continue + } + + const entry: ToolUseConfirm = { + assistantMessage: createAssistantMessage({ content: '' }), + tool, + description: parsed.description, + input: parsed.input, + toolUseContext: {} as ToolUseConfirm['toolUseContext'], + toolUseID: parsed.tool_use_id, + permissionResult: { + behavior: 'ask', + message: parsed.description, + }, + permissionPromptStartTimeMs: Date.now(), + workerBadge: { + name: parsed.agent_id, + color: 'cyan', + }, + onUserInteraction() { + // No-op for tmux workers (no classifier auto-approval) + }, + onAbort() { + void sendPermissionResponseViaMailbox( + parsed.agent_id, + { decision: 'rejected', resolvedBy: 'leader' }, + parsed.request_id, + teamName, + ) + }, + onAllow( + updatedInput: Record, + permissionUpdates: PermissionUpdate[], + ) { + void sendPermissionResponseViaMailbox( + parsed.agent_id, + { + decision: 'approved', + resolvedBy: 'leader', + updatedInput, + permissionUpdates, + }, + parsed.request_id, + teamName, + ) + }, + onReject(feedback?: string) { + void sendPermissionResponseViaMailbox( + parsed.agent_id, + { + decision: 'rejected', + resolvedBy: 'leader', + feedback, + }, + parsed.request_id, + teamName, + ) + }, + async recheckPermission() { + // No-op for tmux workers — permission state is on the worker side + }, + } + + // Deduplicate: if markMessagesAsRead failed on a prior poll, + // the same message will be re-read — skip if already queued. + setToolUseConfirmQueue(queue => { + if (queue.some(q => q.toolUseID === parsed.tool_use_id)) { + return queue + } + return [...queue, entry] + }) + } else { + logForDebugging( + `[InboxPoller] ToolUseConfirmQueue unavailable, dropping permission request from ${parsed.agent_id}`, + ) + } + } + + // Send desktop notification for the first request + const firstParsed = isPermissionRequest(permissionRequests[0]?.text ?? '') + if (firstParsed && !isLoading && !focusedInputDialog) { + void sendNotification( + { + message: `${firstParsed.agent_id} needs permission for ${firstParsed.tool_name}`, + notificationType: 'worker_permission_prompt', + }, + terminal, + ) + } + } + + // Handle permission responses (worker side) - invoke registered callbacks + if (permissionResponses.length > 0 && isTeammate()) { + logForDebugging( + `[InboxPoller] Found ${permissionResponses.length} permission response(s)`, + ) + + for (const m of permissionResponses) { + const parsed = isPermissionResponse(m.text) + if (!parsed) continue + + if (hasPermissionCallback(parsed.request_id)) { + logForDebugging( + `[InboxPoller] Processing permission response for ${parsed.request_id}: ${parsed.subtype}`, + ) + + if (parsed.subtype === 'success') { + processMailboxPermissionResponse({ + requestId: parsed.request_id, + decision: 'approved', + updatedInput: parsed.response?.updated_input, + permissionUpdates: parsed.response?.permission_updates, + }) + } else { + processMailboxPermissionResponse({ + requestId: parsed.request_id, + decision: 'rejected', + feedback: parsed.error, + }) + } + } + } + } + + // Handle sandbox permission requests (leader side) - add to workerSandboxPermissions queue + if ( + sandboxPermissionRequests.length > 0 && + isTeamLead(currentAppState.teamContext) + ) { + logForDebugging( + `[InboxPoller] Found ${sandboxPermissionRequests.length} sandbox permission request(s)`, + ) + + const newSandboxRequests: Array<{ + requestId: string + workerId: string + workerName: string + workerColor?: string + host: string + createdAt: number + }> = [] + + for (const m of sandboxPermissionRequests) { + const parsed = isSandboxPermissionRequest(m.text) + if (!parsed) continue + + // Validate required nested fields to prevent crashes from malformed messages + if (!parsed.hostPattern?.host) { + logForDebugging( + `[InboxPoller] Invalid sandbox permission request: missing hostPattern.host`, + ) + continue + } + + newSandboxRequests.push({ + requestId: parsed.requestId, + workerId: parsed.workerId, + workerName: parsed.workerName, + workerColor: parsed.workerColor, + host: parsed.hostPattern.host, + createdAt: parsed.createdAt, + }) + } + + if (newSandboxRequests.length > 0) { + setAppState(prev => ({ + ...prev, + workerSandboxPermissions: { + ...prev.workerSandboxPermissions, + queue: [ + ...prev.workerSandboxPermissions.queue, + ...newSandboxRequests, + ], + }, + })) + + // Send desktop notification for the first new request + const firstRequest = newSandboxRequests[0] + if (firstRequest && !isLoading && !focusedInputDialog) { + void sendNotification( + { + message: `${firstRequest.workerName} needs network access to ${firstRequest.host}`, + notificationType: 'worker_permission_prompt', + }, + terminal, + ) + } + } + } + + // Handle sandbox permission responses (worker side) - invoke registered callbacks + if (sandboxPermissionResponses.length > 0 && isTeammate()) { + logForDebugging( + `[InboxPoller] Found ${sandboxPermissionResponses.length} sandbox permission response(s)`, + ) + + for (const m of sandboxPermissionResponses) { + const parsed = isSandboxPermissionResponse(m.text) + if (!parsed) continue + + // Check if we have a registered callback for this request + if (hasSandboxPermissionCallback(parsed.requestId)) { + logForDebugging( + `[InboxPoller] Processing sandbox permission response for ${parsed.requestId}: allow=${parsed.allow}`, + ) + + // Process the response using the exported function + processSandboxPermissionResponse({ + requestId: parsed.requestId, + host: parsed.host, + allow: parsed.allow, + }) + + // Clear the pending sandbox request indicator + setAppState(prev => ({ + ...prev, + pendingSandboxRequest: null, + })) + } + } + } + + // Handle team permission updates (teammate side) - apply permission to context + if (teamPermissionUpdates.length > 0 && isTeammate()) { + logForDebugging( + `[InboxPoller] Found ${teamPermissionUpdates.length} team permission update(s)`, + ) + + for (const m of teamPermissionUpdates) { + const parsed = isTeamPermissionUpdate(m.text) + if (!parsed) { + logForDebugging( + `[InboxPoller] Failed to parse team permission update: ${m.text.substring(0, 100)}`, + ) + continue + } + + // Validate required nested fields to prevent crashes from malformed messages + if ( + !parsed.permissionUpdate?.rules || + !parsed.permissionUpdate?.behavior + ) { + logForDebugging( + `[InboxPoller] Invalid team permission update: missing permissionUpdate.rules or permissionUpdate.behavior`, + ) + continue + } + + // Apply the permission update to the teammate's context + logForDebugging( + `[InboxPoller] Applying team permission update: ${parsed.toolName} allowed in ${parsed.directoryPath}`, + ) + logForDebugging( + `[InboxPoller] Permission update rules: ${jsonStringify(parsed.permissionUpdate.rules)}`, + ) + + setAppState(prev => { + const updated = applyPermissionUpdate(prev.toolPermissionContext, { + type: 'addRules', + rules: parsed.permissionUpdate.rules, + behavior: parsed.permissionUpdate.behavior, + destination: 'session', + }) + logForDebugging( + `[InboxPoller] Updated session allow rules: ${jsonStringify(updated.alwaysAllowRules.session)}`, + ) + return { + ...prev, + toolPermissionContext: updated, + } + }) + } + } + + // Handle mode set requests (teammate side) - team lead changing teammate's mode + if (modeSetRequests.length > 0 && isTeammate()) { + logForDebugging( + `[InboxPoller] Found ${modeSetRequests.length} mode set request(s)`, + ) + + for (const m of modeSetRequests) { + // Only accept mode changes from team-lead + if (m.from !== 'team-lead') { + logForDebugging( + `[InboxPoller] Ignoring mode set request from non-team-lead: ${m.from}`, + ) + continue + } + + const parsed = isModeSetRequest(m.text) + if (!parsed) { + logForDebugging( + `[InboxPoller] Failed to parse mode set request: ${m.text.substring(0, 100)}`, + ) + continue + } + + const targetMode = permissionModeFromString(parsed.mode) + logForDebugging( + `[InboxPoller] Applying mode change from team-lead: ${targetMode}`, + ) + + // Update local permission context + setAppState(prev => ({ + ...prev, + toolPermissionContext: applyPermissionUpdate( + prev.toolPermissionContext, + { + type: 'setMode', + mode: toExternalPermissionMode(targetMode), + destination: 'session', + }, + ), + })) + + // Update config.json so team lead can see the new mode + const teamName = currentAppState.teamContext?.teamName + const agentName = getAgentName() + if (teamName && agentName) { + setMemberMode(teamName, agentName, targetMode) + } + } + } + + // Handle plan approval requests (leader side) - auto-approve and write response to teammate inbox + if ( + planApprovalRequests.length > 0 && + isTeamLead(currentAppState.teamContext) + ) { + logForDebugging( + `[InboxPoller] Found ${planApprovalRequests.length} plan approval request(s), auto-approving`, + ) + + const teamName = currentAppState.teamContext?.teamName + const leaderExternalMode = toExternalPermissionMode( + currentAppState.toolPermissionContext.mode, + ) + const modeToInherit = + leaderExternalMode === 'plan' ? 'default' : leaderExternalMode + + for (const m of planApprovalRequests) { + const parsed = isPlanApprovalRequest(m.text) + if (!parsed) continue + + // Write approval response to teammate's inbox + const approvalResponse = { + type: 'plan_approval_response', + requestId: parsed.requestId, + approved: true, + timestamp: new Date().toISOString(), + permissionMode: modeToInherit, + } + + void writeToMailbox( + m.from, + { + from: TEAM_LEAD_NAME, + text: jsonStringify(approvalResponse), + timestamp: new Date().toISOString(), + }, + teamName, + ) + + // Update in-process teammate task state if applicable + const taskId = findInProcessTeammateTaskId(m.from, currentAppState) + if (taskId) { + handlePlanApprovalResponse( + taskId, + { + type: 'plan_approval_response', + requestId: parsed.requestId, + approved: true, + timestamp: new Date().toISOString(), + permissionMode: modeToInherit, + }, + setAppState, + ) + } + + logForDebugging( + `[InboxPoller] Auto-approved plan from ${m.from} (request ${parsed.requestId})`, + ) + + // Still pass through as a regular message so the model has context + // about what the teammate is doing, but the approval is already sent + regularMessages.push(m) + } + } + + // Handle shutdown requests (teammate side) - preserve JSON for UI rendering + if (shutdownRequests.length > 0 && isTeammate()) { + logForDebugging( + `[InboxPoller] Found ${shutdownRequests.length} shutdown request(s)`, + ) + + // Pass through shutdown requests - the UI component will render them nicely + // and the model will receive instructions via the tool prompt documentation + for (const m of shutdownRequests) { + regularMessages.push(m) + } + } + + // Handle shutdown approvals (leader side) - kill the teammate's pane + if ( + shutdownApprovals.length > 0 && + isTeamLead(currentAppState.teamContext) + ) { + logForDebugging( + `[InboxPoller] Found ${shutdownApprovals.length} shutdown approval(s)`, + ) + + for (const m of shutdownApprovals) { + const parsed = isShutdownApproved(m.text) + if (!parsed) continue + + // Kill the pane if we have the info (pane-based teammates) + if (parsed.paneId && parsed.backendType) { + void (async () => { + try { + // Ensure backend classes are imported (no subprocess probes) + await ensureBackendsRegistered() + const insideTmux = await isInsideTmux() + const backend = getBackendByType( + parsed.backendType as PaneBackendType, + ) + const success = await backend?.killPane( + parsed.paneId!, + !insideTmux, + ) + logForDebugging( + `[InboxPoller] Killed pane ${parsed.paneId} for ${parsed.from}: ${success}`, + ) + } catch (error) { + logForDebugging( + `[InboxPoller] Failed to kill pane for ${parsed.from}: ${error}`, + ) + } + })() + } + + // Remove the teammate from teamContext.teammates so the count is accurate + const teammateToRemove = parsed.from + if (teammateToRemove && currentAppState.teamContext?.teammates) { + // Find the teammate ID by name + const teammateId = Object.entries( + currentAppState.teamContext.teammates, + ).find(([, t]) => t.name === teammateToRemove)?.[0] + + if (teammateId) { + // Remove from team file (leader owns team file mutations) + const teamName = currentAppState.teamContext?.teamName + if (teamName) { + removeTeammateFromTeamFile(teamName, { + agentId: teammateId, + name: teammateToRemove, + }) + } + + // Unassign tasks and build notification message + const { notificationMessage } = teamName + ? await unassignTeammateTasks( + teamName, + teammateId, + teammateToRemove, + 'shutdown', + ) + : { notificationMessage: `${teammateToRemove} has shut down.` } + + setAppState(prev => { + if (!prev.teamContext?.teammates) return prev + if (!(teammateId in prev.teamContext.teammates)) return prev + const { [teammateId]: _, ...remainingTeammates } = + prev.teamContext.teammates + + // Mark the teammate's task as completed so hasRunningTeammates + // becomes false and the spinner stops. Without this, out-of-process + // (tmux) teammate tasks stay status:'running' forever because + // only in-process teammates have a runner that sets 'completed'. + const updatedTasks = { ...prev.tasks } + for (const [tid, task] of Object.entries(updatedTasks)) { + if ( + isInProcessTeammateTask(task) && + task.identity.agentId === teammateId + ) { + updatedTasks[tid] = { + ...task, + status: 'completed' as const, + endTime: Date.now(), + } + } + } + + return { + ...prev, + tasks: updatedTasks, + teamContext: { + ...prev.teamContext, + teammates: remainingTeammates, + }, + inbox: { + messages: [ + ...prev.inbox.messages, + { + id: randomUUID(), + from: 'system', + text: jsonStringify({ + type: 'teammate_terminated', + message: notificationMessage, + }), + timestamp: new Date().toISOString(), + status: 'pending' as const, + }, + ], + }, + } + }) + logForDebugging( + `[InboxPoller] Removed ${teammateToRemove} (${teammateId}) from teamContext`, + ) + } + } + + // Pass through for UI rendering - the component will render it nicely + regularMessages.push(m) + } + } + + // Process regular teammate messages (existing logic) + if (regularMessages.length === 0) { + // No regular messages, but we may have processed non-regular messages + // (permissions, shutdown requests, etc.) above — mark those as read. + markRead() + return + } + + // Format messages with XML wrapper for Claude (include color if available) + // Transform plan approval requests to include instructions for Claude + const formatted = regularMessages + .map(m => { + const colorAttr = m.color ? ` color="${m.color}"` : '' + const summaryAttr = m.summary ? ` summary="${m.summary}"` : '' + const messageContent = m.text + + return `<${TEAMMATE_MESSAGE_TAG} teammate_id="${m.from}"${colorAttr}${summaryAttr}>\n${messageContent}\n` + }) + .join('\n\n') + + // Helper to queue messages in AppState for later delivery + const queueMessages = () => { + setAppState(prev => ({ + ...prev, + inbox: { + messages: [ + ...prev.inbox.messages, + ...regularMessages.map(m => ({ + id: randomUUID(), + from: m.from, + text: m.text, + timestamp: m.timestamp, + status: 'pending' as const, + color: m.color, + summary: m.summary, + })), + ], + }, + })) + } + + if (!isLoading && !focusedInputDialog) { + // IDLE: Submit as new turn immediately + logForDebugging(`[InboxPoller] Session idle, submitting immediately`) + const submitted = onSubmitTeammateMessage(formatted) + if (!submitted) { + // Submission rejected (query already running), queue for later + logForDebugging( + `[InboxPoller] Submission rejected, queuing for later delivery`, + ) + queueMessages() + } + } else { + // BUSY: Add to inbox queue for UI display + later delivery + logForDebugging(`[InboxPoller] Session busy, queuing for later delivery`) + queueMessages() + } + + // Mark messages as read only after they have been successfully delivered + // or reliably queued in AppState. This prevents permanent message loss + // when the session is busy — if we crash before this point, the messages + // will be re-read on the next poll cycle instead of being silently dropped. + markRead() + }, [ + enabled, + isLoading, + focusedInputDialog, + onSubmitTeammateMessage, + setAppState, + terminal, + store, + ]) + + // When session becomes idle, deliver any pending messages and clean up processed ones + useEffect(() => { + if (!enabled) return + + // Skip if busy or in a dialog + if (isLoading || focusedInputDialog) { + return + } + + // Use ref to avoid dependency on appState object (prevents infinite loop) + const currentAppState = store.getState() + const agentName = getAgentNameToPoll(currentAppState) + if (!agentName) return + + const pendingMessages = currentAppState.inbox.messages.filter( + m => m.status === 'pending', + ) + const processedMessages = currentAppState.inbox.messages.filter( + m => m.status === 'processed', + ) + + // Clean up processed messages (they were already delivered mid-turn as attachments) + if (processedMessages.length > 0) { + logForDebugging( + `[InboxPoller] Cleaning up ${processedMessages.length} processed message(s) that were delivered mid-turn`, + ) + const processedIds = new Set(processedMessages.map(m => m.id)) + setAppState(prev => ({ + ...prev, + inbox: { + messages: prev.inbox.messages.filter(m => !processedIds.has(m.id)), + }, + })) + } + + // No pending messages to deliver + if (pendingMessages.length === 0) return + + logForDebugging( + `[InboxPoller] Session idle, delivering ${pendingMessages.length} pending message(s)`, + ) + + // Format messages with XML wrapper for Claude (include color if available) + const formatted = pendingMessages + .map(m => { + const colorAttr = m.color ? ` color="${m.color}"` : '' + const summaryAttr = m.summary ? ` summary="${m.summary}"` : '' + return `<${TEAMMATE_MESSAGE_TAG} teammate_id="${m.from}"${colorAttr}${summaryAttr}>\n${m.text}\n` + }) + .join('\n\n') + + // Try to submit - only clear messages if successful + const submitted = onSubmitTeammateMessage(formatted) + if (submitted) { + // Clear the specific messages we just submitted by their IDs + const submittedIds = new Set(pendingMessages.map(m => m.id)) + setAppState(prev => ({ + ...prev, + inbox: { + messages: prev.inbox.messages.filter(m => !submittedIds.has(m.id)), + }, + })) + } else { + logForDebugging( + `[InboxPoller] Submission rejected, keeping messages queued`, + ) + } + }, [ + enabled, + isLoading, + focusedInputDialog, + onSubmitTeammateMessage, + setAppState, + inboxMessageCount, + store, + ]) + + // Poll if running as a teammate or as a team lead + const shouldPoll = enabled && !!getAgentNameToPoll(store.getState()) + useInterval(() => void poll(), shouldPoll ? INBOX_POLL_INTERVAL_MS : null) + + // Initial poll on mount (only once) + const hasDoneInitialPollRef = useRef(false) + useEffect(() => { + if (!enabled) return + if (hasDoneInitialPollRef.current) return + // Use store.getState() to avoid dependency on appState object + if (getAgentNameToPoll(store.getState())) { + hasDoneInitialPollRef.current = true + void poll() + } + // Note: poll uses store.getState() (not appState) so it won't re-run on appState changes + // The ref guard is a safety measure to ensure initial poll only happens once + }, [enabled, poll, store]) +} diff --git a/src/hooks/useLogMessages.ts b/src/hooks/useLogMessages.ts new file mode 100644 index 0000000000000000000000000000000000000000..c244c29dd6b707bac18ed748de18f5105d3a94a0 --- /dev/null +++ b/src/hooks/useLogMessages.ts @@ -0,0 +1,119 @@ +import type { UUID } from 'crypto' +import { useEffect, useRef } from 'react' +import { useAppState } from '../state/AppState.js' +import type { Message } from '../types/message.js' +import { isAgentSwarmsEnabled } from '../utils/agentSwarmsEnabled.js' +import { + cleanMessagesForLogging, + isChainParticipant, + recordTranscript, +} from '../utils/sessionStorage.js' + +/** + * Hook that logs messages to the transcript + * conversation ID that only changes when a new conversation is started. + * + * @param messages The current conversation messages + * @param ignore When true, messages will not be recorded to the transcript + */ +export function useLogMessages(messages: Message[], ignore: boolean = false) { + const teamContext = useAppState(s => s.teamContext) + + // messages is append-only between compactions, so track where we left off + // and only pass the new tail to recordTranscript. Avoids O(n) filter+scan + // on every setMessages (~20x/turn, so n=3000 was ~120k wasted iterations). + const lastRecordedLengthRef = useRef(0) + const lastParentUuidRef = useRef(undefined) + // First-uuid change = compaction or /clear rebuilt the array; length alone + // can't detect this since post-compact [CB,summary,...keep,new] may be longer. + const firstMessageUuidRef = useRef(undefined) + // Guard against stale async .then() overwriting a fresher sync update when + // an incremental render fires before the compaction .then() resolves. + const callSeqRef = useRef(0) + + useEffect(() => { + if (ignore) return + + const currentFirstUuid = messages[0]?.uuid as UUID | undefined + const prevLength = lastRecordedLengthRef.current + + // First-render: firstMessageUuidRef is undefined. Compaction: first uuid changes. + // Both are !isIncremental, but first-render sync-walk is safe (no messagesToKeep). + const wasFirstRender = firstMessageUuidRef.current === undefined + const isIncremental = + currentFirstUuid !== undefined && + !wasFirstRender && + currentFirstUuid === firstMessageUuidRef.current && + prevLength <= messages.length + // Same-head shrink: tombstone filter, rewind, snip, partial-compact. + // Distinguished from compaction (first uuid changes) because the tail + // is either an existing on-disk message or a fresh message that this + // same effect's recordTranscript(fullArray) will write — see sync-walk + // guard below. + const isSameHeadShrink = + currentFirstUuid !== undefined && + !wasFirstRender && + currentFirstUuid === firstMessageUuidRef.current && + prevLength > messages.length + + const startIndex = isIncremental ? prevLength : 0 + if (startIndex === messages.length) return + + // Full array on first call + after compaction: recordTranscript's own + // O(n) dedup loop handles messagesToKeep interleaving correctly there. + const slice = startIndex === 0 ? messages : messages.slice(startIndex) + const parentHint = isIncremental ? lastParentUuidRef.current : undefined + + // Fire and forget - we don't want to block the UI. + const seq = ++callSeqRef.current + void recordTranscript( + slice, + isAgentSwarmsEnabled() + ? { + teamName: teamContext?.teamName, + agentName: teamContext?.selfAgentName, + } + : {}, + parentHint, + messages, + ).then(lastRecordedUuid => { + // For compaction/full array case (!isIncremental): use the async return + // value. After compaction, messagesToKeep in the array are skipped + // (already in transcript), so the sync loop would find a wrong UUID. + // Skip if a newer effect already ran (stale closure would overwrite the + // fresher sync update from the subsequent incremental render). + if (seq !== callSeqRef.current) return + if (lastRecordedUuid && !isIncremental) { + lastParentUuidRef.current = lastRecordedUuid + } + }) + + // Sync-walk safe for: incremental (pure new-tail slice), first-render + // (no messagesToKeep interleaving), and same-head shrink. Shrink is the + // subtle one: the picked uuid is either already on disk (tombstone/rewind + // — survivors were written before) or is being written by THIS effect's + // recordTranscript(fullArray) call (snip boundary / partial-compact tail + // — enqueueWrite ordering guarantees it lands before any later write that + // chains to it). Without this, the ref stays stale at a tombstoned uuid: + // the async .then() correction is raced out by the next effect's seq bump + // on large sessions where recordTranscript(fullArray) is slow. Only the + // compaction case (first uuid changed) remains unsafe — tail may be + // messagesToKeep whose last-actually-recorded uuid differs. + if (isIncremental || wasFirstRender || isSameHeadShrink) { + // Match EXACTLY what recordTranscript persists: cleanMessagesForLogging + // applies both the isLoggableMessage filter and (for external users) the + // REPL-strip + isVirtual-promote transform. Using the raw predicate here + // would pick a UUID that the transform drops, leaving the parent hint + // pointing at a message that never reached disk. Pass full messages as + // replId context — REPL tool_use and its tool_result land in separate + // render cycles, so the slice alone can't pair them. + const last = cleanMessagesForLogging(slice, messages).findLast( + isChainParticipant, + ) + if (last) lastParentUuidRef.current = last.uuid as UUID + } + + lastRecordedLengthRef.current = messages.length + firstMessageUuidRef.current = currentFirstUuid + }, [messages, ignore, teamContext?.teamName, teamContext?.selfAgentName]) +} diff --git a/src/hooks/usePromptsFromClaudeInChrome.tsx b/src/hooks/usePromptsFromClaudeInChrome.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bc4673a75392cda0034c5b5483dddfd1a224181e --- /dev/null +++ b/src/hooks/usePromptsFromClaudeInChrome.tsx @@ -0,0 +1,71 @@ +import { c as _c } from "react/compiler-runtime"; +import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'; +import { useEffect, useRef } from 'react'; +import { logError } from 'src/utils/log.js'; +import { z } from 'zod/v4'; +import { callIdeRpc } from '../services/mcp/client.js'; +import type { ConnectedMCPServer, MCPServerConnection } from '../services/mcp/types.js'; +import type { PermissionMode } from '../types/permissions.js'; +import { CLAUDE_IN_CHROME_MCP_SERVER_NAME, isTrackedClaudeInChromeTabId } from '../utils/claudeInChrome/common.js'; +import { lazySchema } from '../utils/lazySchema.js'; +import { enqueuePendingNotification } from '../utils/messageQueueManager.js'; + +// Schema for the prompt notification from Chrome extension (JSON-RPC 2.0 format) +const ClaudeInChromePromptNotificationSchema = lazySchema(() => z.object({ + method: z.literal('notifications/message'), + params: z.object({ + prompt: z.string(), + image: z.object({ + type: z.literal('base64'), + media_type: z.enum(['image/jpeg', 'image/png', 'image/gif', 'image/webp']), + data: z.string() + }).optional(), + tabId: z.number().optional() + }) +})); + +/** + * A hook that listens for prompt notifications from the Claude for Chrome extension, + * enqueues them as user prompts, and syncs permission mode changes to the extension. + */ +export function usePromptsFromClaudeInChrome(mcpClients, toolPermissionMode) { + const $ = _c(6); + useRef(undefined); + let t0; + if ($[0] !== mcpClients) { + t0 = [mcpClients]; + $[0] = mcpClients; + $[1] = t0; + } else { + t0 = $[1]; + } + useEffect(_temp, t0); + let t1; + let t2; + if ($[2] !== mcpClients || $[3] !== toolPermissionMode) { + t1 = () => { + const chromeClient = findChromeClient(mcpClients); + if (!chromeClient) { + return; + } + const chromeMode = toolPermissionMode === "bypassPermissions" ? "skip_all_permission_checks" : "ask"; + callIdeRpc("set_permission_mode", { + mode: chromeMode + }, chromeClient); + }; + t2 = [mcpClients, toolPermissionMode]; + $[2] = mcpClients; + $[3] = toolPermissionMode; + $[4] = t1; + $[5] = t2; + } else { + t1 = $[4]; + t2 = $[5]; + } + useEffect(t1, t2); +} +function _temp() {} +function findChromeClient(clients: MCPServerConnection[]): ConnectedMCPServer | undefined { + return clients.find((client): client is ConnectedMCPServer => client.type === 'connected' && client.name === CLAUDE_IN_CHROME_MCP_SERVER_NAME); +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJDb250ZW50QmxvY2tQYXJhbSIsInVzZUVmZmVjdCIsInVzZVJlZiIsImxvZ0Vycm9yIiwieiIsImNhbGxJZGVScGMiLCJDb25uZWN0ZWRNQ1BTZXJ2ZXIiLCJNQ1BTZXJ2ZXJDb25uZWN0aW9uIiwiUGVybWlzc2lvbk1vZGUiLCJDTEFVREVfSU5fQ0hST01FX01DUF9TRVJWRVJfTkFNRSIsImlzVHJhY2tlZENsYXVkZUluQ2hyb21lVGFiSWQiLCJsYXp5U2NoZW1hIiwiZW5xdWV1ZVBlbmRpbmdOb3RpZmljYXRpb24iLCJDbGF1ZGVJbkNocm9tZVByb21wdE5vdGlmaWNhdGlvblNjaGVtYSIsIm9iamVjdCIsIm1ldGhvZCIsImxpdGVyYWwiLCJwYXJhbXMiLCJwcm9tcHQiLCJzdHJpbmciLCJpbWFnZSIsInR5cGUiLCJtZWRpYV90eXBlIiwiZW51bSIsImRhdGEiLCJvcHRpb25hbCIsInRhYklkIiwibnVtYmVyIiwidXNlUHJvbXB0c0Zyb21DbGF1ZGVJbkNocm9tZSIsIm1jcENsaWVudHMiLCJ0b29sUGVybWlzc2lvbk1vZGUiLCIkIiwiX2MiLCJ1bmRlZmluZWQiLCJ0MCIsIl90ZW1wIiwidDEiLCJ0MiIsImNocm9tZUNsaWVudCIsImZpbmRDaHJvbWVDbGllbnQiLCJjaHJvbWVNb2RlIiwibW9kZSIsImNsaWVudHMiLCJmaW5kIiwiY2xpZW50IiwibmFtZSJdLCJzb3VyY2VzIjpbInVzZVByb21wdHNGcm9tQ2xhdWRlSW5DaHJvbWUudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB0eXBlIHsgQ29udGVudEJsb2NrUGFyYW0gfSBmcm9tICdAYW50aHJvcGljLWFpL3Nkay9yZXNvdXJjZXMvbWVzc2FnZXMubWpzJ1xuaW1wb3J0IHsgdXNlRWZmZWN0LCB1c2VSZWYgfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IGxvZ0Vycm9yIH0gZnJvbSAnc3JjL3V0aWxzL2xvZy5qcydcbmltcG9ydCB7IHogfSBmcm9tICd6b2QvdjQnXG5pbXBvcnQgeyBjYWxsSWRlUnBjIH0gZnJvbSAnLi4vc2VydmljZXMvbWNwL2NsaWVudC5qcydcbmltcG9ydCB0eXBlIHtcbiAgQ29ubmVjdGVkTUNQU2VydmVyLFxuICBNQ1BTZXJ2ZXJDb25uZWN0aW9uLFxufSBmcm9tICcuLi9zZXJ2aWNlcy9tY3AvdHlwZXMuanMnXG5pbXBvcnQgdHlwZSB7IFBlcm1pc3Npb25Nb2RlIH0gZnJvbSAnLi4vdHlwZXMvcGVybWlzc2lvbnMuanMnXG5pbXBvcnQge1xuICBDTEFVREVfSU5fQ0hST01FX01DUF9TRVJWRVJfTkFNRSxcbiAgaXNUcmFja2VkQ2xhdWRlSW5DaHJvbWVUYWJJZCxcbn0gZnJvbSAnLi4vdXRpbHMvY2xhdWRlSW5DaHJvbWUvY29tbW9uLmpzJ1xuaW1wb3J0IHsgbGF6eVNjaGVtYSB9IGZyb20gJy4uL3V0aWxzL2xhenlTY2hlbWEuanMnXG5pbXBvcnQgeyBlbnF1ZXVlUGVuZGluZ05vdGlmaWNhdGlvbiB9IGZyb20gJy4uL3V0aWxzL21lc3NhZ2VRdWV1ZU1hbmFnZXIuanMnXG5cbi8vIFNjaGVtYSBmb3IgdGhlIHByb21wdCBub3RpZmljYXRpb24gZnJvbSBDaHJvbWUgZXh0ZW5zaW9uIChKU09OLVJQQyAyLjAgZm9ybWF0KVxuY29uc3QgQ2xhdWRlSW5DaHJvbWVQcm9tcHROb3RpZmljYXRpb25TY2hlbWEgPSBsYXp5U2NoZW1hKCgpID0+XG4gIHoub2JqZWN0KHtcbiAgICBtZXRob2Q6IHoubGl0ZXJhbCgnbm90aWZpY2F0aW9ucy9tZXNzYWdlJyksXG4gICAgcGFyYW1zOiB6Lm9iamVjdCh7XG4gICAgICBwcm9tcHQ6IHouc3RyaW5nKCksXG4gICAgICBpbWFnZTogelxuICAgICAgICAub2JqZWN0KHtcbiAgICAgICAgICB0eXBlOiB6LmxpdGVyYWwoJ2Jhc2U2NCcpLFxuICAgICAgICAgIG1lZGlhX3R5cGU6IHouZW51bShbXG4gICAgICAgICAgICAnaW1hZ2UvanBlZycsXG4gICAgICAgICAgICAnaW1hZ2UvcG5nJyxcbiAgICAgICAgICAgICdpbWFnZS9naWYnLFxuICAgICAgICAgICAgJ2ltYWdlL3dlYnAnLFxuICAgICAgICAgIF0pLFxuICAgICAgICAgIGRhdGE6IHouc3RyaW5nKCksXG4gICAgICAgIH0pXG4gICAgICAgIC5vcHRpb25hbCgpLFxuICAgICAgdGFiSWQ6IHoubnVtYmVyKCkub3B0aW9uYWwoKSxcbiAgICB9KSxcbiAgfSksXG4pXG5cbi8qKlxuICogQSBob29rIHRoYXQgbGlzdGVucyBmb3IgcHJvbXB0IG5vdGlmaWNhdGlvbnMgZnJvbSB0aGUgQ2xhdWRlIGZvciBDaHJvbWUgZXh0ZW5zaW9uLFxuICogZW5xdWV1ZXMgdGhlbSBhcyB1c2VyIHByb21wdHMsIGFuZCBzeW5jcyBwZXJtaXNzaW9uIG1vZGUgY2hhbmdlcyB0byB0aGUgZXh0ZW5zaW9uLlxuICovXG5leHBvcnQgZnVuY3Rpb24gdXNlUHJvbXB0c0Zyb21DbGF1ZGVJbkNocm9tZShcbiAgbWNwQ2xpZW50czogTUNQU2VydmVyQ29ubmVjdGlvbltdLFxuICB0b29sUGVybWlzc2lvbk1vZGU6IFBlcm1pc3Npb25Nb2RlLFxuKTogdm9pZCB7XG4gIGNvbnN0IG1jcENsaWVudFJlZiA9IHVzZVJlZjxDb25uZWN0ZWRNQ1BTZXJ2ZXIgfCB1bmRlZmluZWQ+KHVuZGVmaW5lZClcblxuICB1c2VFZmZlY3QoKCkgPT4ge1xuICAgIGlmIChcImV4dGVybmFsXCIgIT09ICdhbnQnKSB7XG4gICAgICByZXR1cm5cbiAgICB9XG5cbiAgICBjb25zdCBtY3BDbGllbnQgPSBmaW5kQ2hyb21lQ2xpZW50KG1jcENsaWVudHMpXG4gICAgaWYgKG1jcENsaWVudFJlZi5jdXJyZW50ICE9PSBtY3BDbGllbnQpIHtcbiAgICAgIG1jcENsaWVudFJlZi5jdXJyZW50ID0gbWNwQ2xpZW50XG4gICAgfVxuXG4gICAgaWYgKG1jcENsaWVudCkge1xuICAgICAgbWNwQ2xpZW50LmNsaWVudC5zZXROb3RpZmljYXRpb25IYW5kbGVyKFxuICAgICAgICBDbGF1ZGVJbkNocm9tZVByb21wdE5vdGlmaWNhdGlvblNjaGVtYSgpLFxuICAgICAgICBub3RpZmljYXRpb24gPT4ge1xuICAgICAgICAgIGlmIChtY3BDbGllbnRSZWYuY3VycmVudCAhPT0gbWNwQ2xpZW50KSB7XG4gICAgICAgICAgICByZXR1cm5cbiAgICAgICAgICB9XG4gICAgICAgICAgY29uc3QgeyB0YWJJZCwgcHJvbXB0LCBpbWFnZSB9ID0gbm90aWZpY2F0aW9uLnBhcmFtc1xuXG4gICAgICAgICAgLy8gUHJvY2VzcyBub3RpZmljYXRpb25zIGZyb20gdGFicyB3ZSdyZSB0cmFja2luZyBzaW5jZSBub3RpZmljYXRpb25zIGFyZSBicm9hZGNhc3RlZFxuICAgICAgICAgIGlmIChcbiAgICAgICAgICAgIHR5cGVvZiB0YWJJZCAhPT0gJ251bWJlcicgfHxcbiAgICAgICAgICAgICFpc1RyYWNrZWRDbGF1ZGVJbkNocm9tZVRhYklkKHRhYklkKVxuICAgICAgICAgICkge1xuICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgfVxuXG4gICAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIC8vIEJ1aWxkIGNvbnRlbnQgYmxvY2tzIGlmIHRoZXJlJ3MgYW4gaW1hZ2UsIG90aGVyd2lzZSBqdXN0IHVzZSB0aGUgcHJvbXB0IHN0cmluZ1xuICAgICAgICAgICAgaWYgKGltYWdlKSB7XG4gICAgICAgICAgICAgIGNvbnN0IGNvbnRlbnRCbG9ja3M6IENvbnRlbnRCbG9ja1BhcmFtW10gPSBbXG4gICAgICAgICAgICAgICAgeyB0eXBlOiAndGV4dCcsIHRleHQ6IHByb21wdCB9LFxuICAgICAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICAgIHR5cGU6ICdpbWFnZScsXG4gICAgICAgICAgICAgICAgICBzb3VyY2U6IHtcbiAgICAgICAgICAgICAgICAgICAgdHlwZTogaW1hZ2UudHlwZSxcbiAgICAgICAgICAgICAgICAgICAgbWVkaWFfdHlwZTogaW1hZ2UubWVkaWFfdHlwZSxcbiAgICAgICAgICAgICAgICAgICAgZGF0YTogaW1hZ2UuZGF0YSxcbiAgICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgXVxuICAgICAgICAgICAgICBlbnF1ZXVlUGVuZGluZ05vdGlmaWNhdGlvbih7XG4gICAgICAgICAgICAgICAgdmFsdWU6IGNvbnRlbnRCbG9ja3MsXG4gICAgICAgICAgICAgICAgbW9kZTogJ3Byb21wdCcsXG4gICAgICAgICAgICAgIH0pXG4gICAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgICBlbnF1ZXVlUGVuZGluZ05vdGlmaWNhdGlvbih7IHZhbHVlOiBwcm9tcHQsIG1vZGU6ICdwcm9tcHQnIH0pXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgICAgICAgIGxvZ0Vycm9yKGVycm9yIGFzIEVycm9yKVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgIClcbiAgICB9XG4gIH0sIFttY3BDbGllbnRzXSlcblxuICAvLyBTeW5jIHBlcm1pc3Npb24gbW9kZSB3aXRoIENocm9tZSBleHRlbnNpb24gd2hlbmV2ZXIgaXQgY2hhbmdlc1xuICB1c2VFZmZlY3QoKCkgPT4ge1xuICAgIGNvbnN0IGNocm9tZUNsaWVudCA9IGZpbmRDaHJvbWVDbGllbnQobWNwQ2xpZW50cylcbiAgICBpZiAoIWNocm9tZUNsaWVudCkgcmV0dXJuXG5cbiAgICBjb25zdCBjaHJvbWVNb2RlID1cbiAgICAgIHRvb2xQZXJtaXNzaW9uTW9kZSA9PT0gJ2J5cGFzc1Blcm1pc3Npb25zJ1xuICAgICAgICA/ICdza2lwX2FsbF9wZXJtaXNzaW9uX2NoZWNrcydcbiAgICAgICAgOiAnYXNrJ1xuXG4gICAgdm9pZCBjYWxsSWRlUnBjKCdzZXRfcGVybWlzc2lvbl9tb2RlJywgeyBtb2RlOiBjaHJvbWVNb2RlIH0sIGNocm9tZUNsaWVudClcbiAgfSwgW21jcENsaWVudHMsIHRvb2xQZXJtaXNzaW9uTW9kZV0pXG59XG5cbmZ1bmN0aW9uIGZpbmRDaHJvbWVDbGllbnQoXG4gIGNsaWVudHM6IE1DUFNlcnZlckNvbm5lY3Rpb25bXSxcbik6IENvbm5lY3RlZE1DUFNlcnZlciB8IHVuZGVmaW5lZCB7XG4gIHJldHVybiBjbGllbnRzLmZpbmQoXG4gICAgKGNsaWVudCk6IGNsaWVudCBpcyBDb25uZWN0ZWRNQ1BTZXJ2ZXIgPT5cbiAgICAgIGNsaWVudC50eXBlID09PSAnY29ubmVjdGVkJyAmJlxuICAgICAgY2xpZW50Lm5hbWUgPT09IENMQVVERV9JTl9DSFJPTUVfTUNQX1NFUlZFUl9OQU1FLFxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxjQUFjQSxpQkFBaUIsUUFBUSwwQ0FBMEM7QUFDakYsU0FBU0MsU0FBUyxFQUFFQyxNQUFNLFFBQVEsT0FBTztBQUN6QyxTQUFTQyxRQUFRLFFBQVEsa0JBQWtCO0FBQzNDLFNBQVNDLENBQUMsUUFBUSxRQUFRO0FBQzFCLFNBQVNDLFVBQVUsUUFBUSwyQkFBMkI7QUFDdEQsY0FDRUMsa0JBQWtCLEVBQ2xCQyxtQkFBbUIsUUFDZCwwQkFBMEI7QUFDakMsY0FBY0MsY0FBYyxRQUFRLHlCQUF5QjtBQUM3RCxTQUNFQyxnQ0FBZ0MsRUFDaENDLDRCQUE0QixRQUN2QixtQ0FBbUM7QUFDMUMsU0FBU0MsVUFBVSxRQUFRLHdCQUF3QjtBQUNuRCxTQUFTQywwQkFBMEIsUUFBUSxpQ0FBaUM7O0FBRTVFO0FBQ0EsTUFBTUMsc0NBQXNDLEdBQUdGLFVBQVUsQ0FBQyxNQUN4RFAsQ0FBQyxDQUFDVSxNQUFNLENBQUM7RUFDUEMsTUFBTSxFQUFFWCxDQUFDLENBQUNZLE9BQU8sQ0FBQyx1QkFBdUIsQ0FBQztFQUMxQ0MsTUFBTSxFQUFFYixDQUFDLENBQUNVLE1BQU0sQ0FBQztJQUNmSSxNQUFNLEVBQUVkLENBQUMsQ0FBQ2UsTUFBTSxDQUFDLENBQUM7SUFDbEJDLEtBQUssRUFBRWhCLENBQUMsQ0FDTFUsTUFBTSxDQUFDO01BQ05PLElBQUksRUFBRWpCLENBQUMsQ0FBQ1ksT0FBTyxDQUFDLFFBQVEsQ0FBQztNQUN6Qk0sVUFBVSxFQUFFbEIsQ0FBQyxDQUFDbUIsSUFBSSxDQUFDLENBQ2pCLFlBQVksRUFDWixXQUFXLEVBQ1gsV0FBVyxFQUNYLFlBQVksQ0FDYixDQUFDO01BQ0ZDLElBQUksRUFBRXBCLENBQUMsQ0FBQ2UsTUFBTSxDQUFDO0lBQ2pCLENBQUMsQ0FBQyxDQUNETSxRQUFRLENBQUMsQ0FBQztJQUNiQyxLQUFLLEVBQUV0QixDQUFDLENBQUN1QixNQUFNLENBQUMsQ0FBQyxDQUFDRixRQUFRLENBQUM7RUFDN0IsQ0FBQztBQUNILENBQUMsQ0FDSCxDQUFDOztBQUVEO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsT0FBTyxTQUFBRyw2QkFBQUMsVUFBQSxFQUFBQyxrQkFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUlnQjlCLE1BQU0sQ0FBaUMrQixTQUFTLENBQUM7RUFBQSxJQUFBQyxFQUFBO0VBQUEsSUFBQUgsQ0FBQSxRQUFBRixVQUFBO0lBd0RuRUssRUFBQSxJQUFDTCxVQUFVLENBQUM7SUFBQUUsQ0FBQSxNQUFBRixVQUFBO0lBQUFFLENBQUEsTUFBQUcsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUgsQ0FBQTtFQUFBO0VBdERmOUIsU0FBUyxDQUFDa0MsS0FzRFQsRUFBRUQsRUFBWSxDQUFDO0VBQUEsSUFBQUUsRUFBQTtFQUFBLElBQUFDLEVBQUE7RUFBQSxJQUFBTixDQUFBLFFBQUFGLFVBQUEsSUFBQUUsQ0FBQSxRQUFBRCxrQkFBQTtJQUdOTSxFQUFBLEdBQUFBLENBQUE7TUFDUixNQUFBRSxZQUFBLEdBQXFCQyxnQkFBZ0IsQ0FBQ1YsVUFBVSxDQUFDO01BQ2pELElBQUksQ0FBQ1MsWUFBWTtRQUFBO01BQUE7TUFFakIsTUFBQUUsVUFBQSxHQUNFVixrQkFBa0IsS0FBSyxtQkFFZCxHQUZULDRCQUVTLEdBRlQsS0FFUztNQUVOekIsVUFBVSxDQUFDLHFCQUFxQixFQUFFO1FBQUFvQyxJQUFBLEVBQVFEO01BQVcsQ0FBQyxFQUFFRixZQUFZLENBQUM7SUFBQSxDQUMzRTtJQUFFRCxFQUFBLElBQUNSLFVBQVUsRUFBRUMsa0JBQWtCLENBQUM7SUFBQUMsQ0FBQSxNQUFBRixVQUFBO0lBQUFFLENBQUEsTUFBQUQsa0JBQUE7SUFBQUMsQ0FBQSxNQUFBSyxFQUFBO0lBQUFMLENBQUEsTUFBQU0sRUFBQTtFQUFBO0lBQUFELEVBQUEsR0FBQUwsQ0FBQTtJQUFBTSxFQUFBLEdBQUFOLENBQUE7RUFBQTtFQVZuQzlCLFNBQVMsQ0FBQ21DLEVBVVQsRUFBRUMsRUFBZ0MsQ0FBQztBQUFBO0FBekUvQixTQUFBRixNQUFBO0FBNEVQLFNBQVNJLGdCQUFnQkEsQ0FDdkJHLE9BQU8sRUFBRW5DLG1CQUFtQixFQUFFLENBQy9CLEVBQUVELGtCQUFrQixHQUFHLFNBQVMsQ0FBQztFQUNoQyxPQUFPb0MsT0FBTyxDQUFDQyxJQUFJLENBQ2pCLENBQUNDLE1BQU0sQ0FBQyxFQUFFQSxNQUFNLElBQUl0QyxrQkFBa0IsSUFDcENzQyxNQUFNLENBQUN2QixJQUFJLEtBQUssV0FBVyxJQUMzQnVCLE1BQU0sQ0FBQ0MsSUFBSSxLQUFLcEMsZ0NBQ3BCLENBQUM7QUFDSCIsImlnbm9yZUxpc3QiOltdfQ== \ No newline at end of file diff --git a/src/hooks/useSwarmInitialization.ts b/src/hooks/useSwarmInitialization.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b9cd6105e7429e86f4dfb483ab4f1e35102a891 --- /dev/null +++ b/src/hooks/useSwarmInitialization.ts @@ -0,0 +1,81 @@ +/** + * Swarm Initialization Hook + * + * Initializes swarm features: teammate hooks and context. + * Handles both fresh spawns and resumed teammate sessions. + * + * This hook is conditionally loaded to allow dead code elimination when swarms are disabled. + */ + +import { useEffect } from 'react' +import { getSessionId } from '../bootstrap/state.js' +import type { AppState } from '../state/AppState.js' +import type { Message } from '../types/message.js' +import { isAgentSwarmsEnabled } from '../utils/agentSwarmsEnabled.js' +import { initializeTeammateContextFromSession } from '../utils/swarm/reconnection.js' +import { readTeamFile } from '../utils/swarm/teamHelpers.js' +import { initializeTeammateHooks } from '../utils/swarm/teammateInit.js' +import { getDynamicTeamContext } from '../utils/teammate.js' + +type SetAppState = (f: (prevState: AppState) => AppState) => void + +/** + * Hook that initializes swarm features when ENABLE_AGENT_SWARMS is true. + * + * Handles both: + * - Resumed teammate sessions (from --resume or /resume) where teamName/agentName + * are stored in transcript messages + * - Fresh spawns where context is read from environment variables + */ +export function useSwarmInitialization( + setAppState: SetAppState, + initialMessages: Message[] | undefined, + { enabled = true }: { enabled?: boolean } = {}, +): void { + useEffect(() => { + if (!enabled) return + if (isAgentSwarmsEnabled()) { + // Check if this is a resumed agent session (from --resume or /resume) + // Resumed sessions have teamName/agentName stored in transcript messages + const firstMessage = initialMessages?.[0] + const teamName = + firstMessage && 'teamName' in firstMessage + ? (firstMessage.teamName as string | undefined) + : undefined + const agentName = + firstMessage && 'agentName' in firstMessage + ? (firstMessage.agentName as string | undefined) + : undefined + + if (teamName && agentName) { + // Resumed agent session - set up team context from stored info + initializeTeammateContextFromSession(setAppState, teamName, agentName) + + // Get agentId from team file for hook initialization + const teamFile = readTeamFile(teamName) + const member = teamFile?.members.find( + (m: { name: string }) => m.name === agentName, + ) + if (member) { + initializeTeammateHooks(setAppState, getSessionId(), { + teamName, + agentId: member.agentId, + agentName, + }) + } + } else { + // Fresh spawn or standalone session + // teamContext is already computed in main.tsx via computeInitialTeamContext() + // and included in initialState, so we only need to initialize hooks here + const context = getDynamicTeamContext?.() + if (context?.teamName && context?.agentId && context?.agentName) { + initializeTeammateHooks(setAppState, getSessionId(), { + teamName: context.teamName, + agentId: context.agentId, + agentName: context.agentName, + }) + } + } + } + }, [setAppState, initialMessages, enabled]) +} diff --git a/src/tools/utils.ts b/src/tools/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..c55ea2ef9474c7c8eddfcf6c92cd2c65d5343dfa --- /dev/null +++ b/src/tools/utils.ts @@ -0,0 +1,40 @@ +import type { + AssistantMessage, + AttachmentMessage, + SystemMessage, + UserMessage, +} from 'src/types/message.js' + +/** + * Tags user messages with a sourceToolUseID so they stay transient until the tool resolves. + * This prevents the "is running" message from being duplicated in the UI. + */ +export function tagMessagesWithToolUseID( + messages: (UserMessage | AttachmentMessage | SystemMessage)[], + toolUseID: string | undefined, +): (UserMessage | AttachmentMessage | SystemMessage)[] { + if (!toolUseID) { + return messages + } + return messages.map(m => { + if (m.type === 'user') { + return { ...m, sourceToolUseID: toolUseID } + } + return m + }) +} + +/** + * Extracts the tool use ID from a parent message for a given tool name. + */ +export function getToolUseIDFromParentMessage( + parentMessage: AssistantMessage, + toolName: string, +): string | undefined { + const toolUseBlock = parentMessage.message.content.find( + block => block.type === 'tool_use' && block.name === toolName, + ) + return toolUseBlock && toolUseBlock.type === 'tool_use' + ? toolUseBlock.id + : undefined +}