流出したclaude codeのソースコード2

@Kongyokongyo / 更新: 2026/04/19 12:43
MD 1.95MB
0

331: if ($[40] !== focusIndex || $[41] !== showIndividualTools || $[42] !== toggleButtonIndex) { 332: t12 = () => { 333: setShowIndividualTools(!showIndividualTools); 334: if (showIndividualTools && focusIndex > toggleButtonIndex) { 335: setFocusIndex(toggleButtonIndex); 336: } 337: }; 338: $[40] = focusIndex; 339: $[41] = showIndividualTools; 340: $[42] = toggleButtonIndex; 341: $[43] = t12; 342: } else { 343: t12 = $[43]; 344: } 345: navigableItems.push({ 346: id: “toggle-individual”, 347: label: showIndividualTools ? “Hide advanced options” : “Show advanced options”, 348: action: t12, 349: isToggle: true 350: }); 351: const mcpServerBuckets = getMcpServerBuckets(customAgentTools); 352: if (showIndividualTools) { 353: if (mcpServerBuckets.length > 0) { 354: navigableItems.push({ 355: id: “mcp-servers-header”, 356: label: “MCP Servers:”, 357: action: _temp6, 358: isHeader: true 359: }); 360: mcpServerBuckets.forEach(t13 => { 361: const { 362: serverName, 363: tools: serverTools 364: } = t13; 365: const selected_1 = count(serverTools, t_9 => selectedSet.has(t_9.name)); 366: const isFullySelected_0 = selected_1 === serverTools.length; 367: navigableItems.push({ 368: id: mcp-server-${serverName}, 369: label: ${isFullySelected_0 ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, "tool")}), 370: action: () => { 371: const toolNames_2 = serverTools.map(_temp7); 372: handleToggleTools(toolNames_2, !isFullySelected_0); 373: } 374: }); 375: }); 376: navigableItems.push({ 377: id: “tools-header”, 378: label: “Individual Tools:”, 379: action: _temp8, 380: isHeader: true 381: }); 382: } 383: customAgentTools.forEach(tool_0 => { 384: let displayName = tool_0.name; 385: if (tool_0.name.startsWith(“mcp__”)) { 386: const mcpInfo = mcpInfoFromString(tool_0.name); 387: displayName = mcpInfo ? ${mcpInfo.toolName} (${mcpInfo.serverName}) : tool_0.name; 388: } 389: navigableItems.push({ 390: id: tool-${tool_0.name}, 391: label: ${selectedSet.has(tool_0.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}, 392: action: () => handleToggleTool(tool_0.name) 393: }); 394: }); 395: } 396: $[24] = createBucketToggleAction; 397: $[25] = customAgentTools; 398: $[26] = focusIndex; 399: $[27] = handleConfirm; 400: $[28] = isAllSelected; 401: $[29] = selectedSet; 402: $[30] = showIndividualTools; 403: $[31] = toolsByBucket.edit; 404: $[32] = toolsByBucket.execution; 405: $[33] = toolsByBucket.mcp; 406: $[34] = toolsByBucket.other; 407: $[35] = toolsByBucket.readOnly; 408: $[36] = navigableItems; 409: } else { 410: navigableItems = $[36]; 411: } 412: let t10; 413: if ($[44] !== initialTools || $[45] !== onCancel || $[46] !== onComplete) { 414: t10 = () => { 415: if (onCancel) { 416: onCancel(); 417: } else { 418: onComplete(initialTools); 419: } 420: }; 421: $[44] = initialTools; 422: $[45] = onCancel; 423: $[46] = onComplete; 424: $[47] = t10; 425: } else { 426: t10 = $[47]; 427: } 428: const handleCancel = t10; 429: let t11; 430: if ($[48] === Symbol.for(“react.memo_cache_sentinel”)) { 431: t11 = { 432: context: “Confirmation” 433: }; 434: $[48] = t11; 435: } else { 436: t11 = $[48]; 437: } 438: useKeybinding(“confirm:no”, handleCancel, t11); 439: let t12; 440: if ($[49] !== focusIndex || $[50] !== navigableItems) { 441: t12 = e => { 442: if (e.key === “return”) { 443: e.preventDefault(); 444: const item = navigableItems[focusIndex]; 445: if (item && !item.isHeader) { 446: item.action(); 447: } 448: } else { 449: if (e.key === “up”) { 450: e.preventDefault(); 451: let newIndex = focusIndex - 1; 452: while (newIndex > 0 && navigableItems[newIndex]?.isHeader) { 453: newIndex–; 454: } 455: setFocusIndex(Math.max(0, newIndex)); 456: } else { 457: if (e.key === “down”) { 458: e.preventDefault(); 459: let newIndex_0 = focusIndex + 1; 460: while (newIndex_0 < navigableItems.length - 1 && navigableItems[newIndex_0]?.isHeader) { 461: newIndex_0++; 462: } 463: setFocusIndex(Math.min(navigableItems.length - 1, newIndex_0)); 464: } 465: } 466: } 467: }; 468: $[49] = focusIndex; 469: $[50] = navigableItems; 470: $[51] = t12; 471: } else { 472: t12 = $[51]; 473: } 474: const handleKeyDown = t12; 475: const t13 = focusIndex === 0 ? “suggestion” : undefined; 476: const t14 = focusIndex === 0; 477: const t15 = focusIndex === 0 ? ${figures.pointer} : “ “; 478: let t16; 479: if ($[52] !== t13 || $[53] !== t14 || $[54] !== t15) { 480: t16 = <Text color={t13} bold={t14}>{t15}[ Continue ]</Text>; 481: $[52] = t13; 482: $[53] = t14; 483: $[54] = t15; 484: $[55] = t16; 485: } else { 486: t16 = $[55]; 487: } 488: let t17; 489: if ($[56] === Symbol.for(“react.memo_cache_sentinel”)) { 490: t17 = <Divider width={40} />; 491: $[56] = t17; 492: } else { 493: t17 = $[56]; 494: } 495: let t18; 496: if ($[57] !== navigableItems) { 497: t18 = navigableItems.slice(1); 498: $[57] = navigableItems; 499: $[58] = t18; 500: } else { 501: t18 = $[58]; 502: } 503: let t19; 504: if ($[59] !== focusIndex || $[60] !== t18) { 505: t19 = t18.map((item_0, index) => { 506: const isCurrentlyFocused = index + 1 === focusIndex; 507: const isToggleButton = item_0.isToggle; 508: const isHeader = item_0.isHeader; 509: return <React.Fragment key={item_0.id}>{isToggleButton && <Divider width={40} />}{isHeader && index > 0 && <Box marginTop={1} />}<Text color={isHeader ? undefined : isCurrentlyFocused ? “suggestion” : undefined} dimColor={isHeader} bold={isToggleButton && isCurrentlyFocused}>{isHeader ? “” : isCurrentlyFocused ? ${figures.pointer} : “ “}{isToggleButton ? [ ${item_0.label} ] : item_0.label}</Text></React.Fragment>; 510: }); 511: $[59] = focusIndex; 512: $[60] = t18; 513: $[61] = t19; 514: } else { 515: t19 = $[61]; 516: } 517: const t20 = isAllSelected ? “All tools selected” : ${selectedSet.size} of ${customAgentTools.length} tools selected; 518: let t21; 519: if ($[62] !== t20) { 520: t21 = <Box marginTop={1} flexDirection=”column”><Text dimColor={true}>{t20}</Text></Box>; 521: $[62] = t20; 522: $[63] = t21; 523: } else { 524: t21 = $[63]; 525: } 526: let t22; 527: if ($[64] !== handleKeyDown || $[65] !== t16 || $[66] !== t19 || $[67] !== t21) { 528: t22 = <Box flexDirection=”column” marginTop={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t16}{t17}{t19}{t21}</Box>; 529: $[64] = handleKeyDown; 530: $[65] = t16; 531: $[66] = t19; 532: $[67] = t21; 533: $[68] = t22; 534: } else { 535: t22 = $[68]; 536: } 537: return t22; 538: } 539: function _temp8() {} 540: function _temp7(t_10) { 541: return t_10.name; 542: } 543: function _temp6() {} 544: function _temp5(t_7) { 545: return t_7.name; 546: } 547: function _temp4(t_6) { 548: return t_6.name; 549: } 550: function _temp3(t_4) { 551: return t_4.name; 552: } 553: function _temp2(t_0) { 554: return t_0.name; 555: } 556: function _temp(t) { 557: return t.name; 558: } ````

File: src/components/agents/types.ts

typescript 1: import type { SettingSource } from 'src/utils/settings/constants.js' 2: import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js' 3: export const AGENT_PATHS = { 4: FOLDER_NAME: '.claude', 5: AGENTS_DIR: 'agents', 6: } as const 7: type WithPreviousMode = { previousMode: ModeState } 8: type WithAgent = { agent: AgentDefinition } 9: export type ModeState = 10: | { mode: 'main-menu' } 11: | { mode: 'list-agents'; source: SettingSource | 'all' | 'built-in' } 12: | ({ mode: 'agent-menu' } & WithAgent & WithPreviousMode) 13: | ({ mode: 'view-agent' } & WithAgent & WithPreviousMode) 14: | { mode: 'create-agent' } 15: | ({ mode: 'edit-agent' } & WithAgent & WithPreviousMode) 16: | ({ mode: 'delete-confirm' } & WithAgent & WithPreviousMode) 17: export type AgentValidationResult = { 18: isValid: boolean 19: warnings: string[] 20: errors: string[] 21: }

File: src/components/agents/utils.ts

typescript 1: import capitalize from 'lodash-es/capitalize.js' 2: import type { SettingSource } from 'src/utils/settings/constants.js' 3: import { getSettingSourceName } from 'src/utils/settings/constants.js' 4: export function getAgentSourceDisplayName( 5: source: SettingSource | 'all' | 'built-in' | 'plugin', 6: ): string { 7: if (source === 'all') { 8: return 'Agents' 9: } 10: if (source === 'built-in') { 11: return 'Built-in agents' 12: } 13: if (source === 'plugin') { 14: return 'Plugin agents' 15: } 16: return capitalize(getSettingSourceName(source)) 17: }

File: src/components/agents/validateAgent.ts

typescript 1: import type { Tools } from '../../Tool.js' 2: import { resolveAgentTools } from '../../tools/AgentTool/agentToolUtils.js' 3: import type { 4: AgentDefinition, 5: CustomAgentDefinition, 6: } from '../../tools/AgentTool/loadAgentsDir.js' 7: import { getAgentSourceDisplayName } from './utils.js' 8: export type AgentValidationResult = { 9: isValid: boolean 10: errors: string[] 11: warnings: string[] 12: } 13: export function validateAgentType(agentType: string): string | null { 14: if (!agentType) { 15: return 'Agent type is required' 16: } 17: if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$/.test(agentType)) { 18: return 'Agent type must start and end with alphanumeric characters and contain only letters, numbers, and hyphens' 19: } 20: if (agentType.length < 3) { 21: return 'Agent type must be at least 3 characters long' 22: } 23: if (agentType.length > 50) { 24: return 'Agent type must be less than 50 characters' 25: } 26: return null 27: } 28: export function validateAgent( 29: agent: Omit<CustomAgentDefinition, 'location'>, 30: availableTools: Tools, 31: existingAgents: AgentDefinition[], 32: ): AgentValidationResult { 33: const errors: string[] = [] 34: const warnings: string[] = [] 35: if (!agent.agentType) { 36: errors.push('Agent type is required') 37: } else { 38: const typeError = validateAgentType(agent.agentType) 39: if (typeError) { 40: errors.push(typeError) 41: } 42: const duplicate = existingAgents.find( 43: a => a.agentType === agent.agentType && a.source !== agent.source, 44: ) 45: if (duplicate) { 46: errors.push( 47: `Agent type "${agent.agentType}" already exists in ${getAgentSourceDisplayName(duplicate.source)}`, 48: ) 49: } 50: } 51: if (!agent.whenToUse) { 52: errors.push('Description (description) is required') 53: } else if (agent.whenToUse.length < 10) { 54: warnings.push( 55: 'Description should be more descriptive (at least 10 characters)', 56: ) 57: } else if (agent.whenToUse.length > 5000) { 58: warnings.push('Description is very long (over 5000 characters)') 59: } 60: if (agent.tools !== undefined && !Array.isArray(agent.tools)) { 61: errors.push('Tools must be an array') 62: } else { 63: if (agent.tools === undefined) { 64: warnings.push('Agent has access to all tools') 65: } else if (agent.tools.length === 0) { 66: warnings.push( 67: 'No tools selected - agent will have very limited capabilities', 68: ) 69: } 70: const resolvedTools = resolveAgentTools(agent, availableTools, false) 71: if (resolvedTools.invalidTools.length > 0) { 72: errors.push(`Invalid tools: ${resolvedTools.invalidTools.join(', ')}`) 73: } 74: } 75: const systemPrompt = agent.getSystemPrompt() 76: if (!systemPrompt) { 77: errors.push('System prompt is required') 78: } else if (systemPrompt.length < 20) { 79: errors.push('System prompt is too short (minimum 20 characters)') 80: } else if (systemPrompt.length > 10000) { 81: warnings.push('System prompt is very long (over 10,000 characters)') 82: } 83: return { 84: isValid: errors.length === 0, 85: errors, 86: warnings, 87: } 88: }

File: src/components/ClaudeCodeHint/PluginHintMenu.tsx

typescript 1: import * as React from 'react'; 2: import { Box, Text } from '../../ink.js'; 3: import { Select } from '../CustomSelect/select.js'; 4: import { PermissionDialog } from '../permissions/PermissionDialog.js'; 5: type Props = { 6: pluginName: string; 7: pluginDescription?: string; 8: marketplaceName: string; 9: sourceCommand: string; 10: onResponse: (response: 'yes' | 'no' | 'disable') => void; 11: }; 12: const AUTO_DISMISS_MS = 30_000; 13: export function PluginHintMenu({ 14: pluginName, 15: pluginDescription, 16: marketplaceName, 17: sourceCommand, 18: onResponse 19: }: Props): React.ReactNode { 20: const onResponseRef = React.useRef(onResponse); 21: onResponseRef.current = onResponse; 22: React.useEffect(() => { 23: const timeoutId = setTimeout(ref => ref.current('no'), AUTO_DISMISS_MS, onResponseRef); 24: return () => clearTimeout(timeoutId); 25: }, []); 26: function onSelect(value: string): void { 27: switch (value) { 28: case 'yes': 29: onResponse('yes'); 30: break; 31: case 'disable': 32: onResponse('disable'); 33: break; 34: default: 35: onResponse('no'); 36: } 37: } 38: const options = [{ 39: label: <Text> 40: Yes, install <Text bold>{pluginName}</Text> 41: </Text>, 42: value: 'yes' 43: }, { 44: label: 'No', 45: value: 'no' 46: }, { 47: label: "No, and don't show plugin installation hints again", 48: value: 'disable' 49: }]; 50: return <PermissionDialog title="Plugin Recommendation"> 51: <Box flexDirection="column" paddingX={2} paddingY={1}> 52: <Box marginBottom={1}> 53: <Text dimColor> 54: The <Text bold>{sourceCommand}</Text> command suggests installing a 55: plugin. 56: </Text> 57: </Box> 58: <Box> 59: <Text dimColor>Plugin:</Text> 60: <Text> {pluginName}</Text> 61: </Box> 62: <Box> 63: <Text dimColor>Marketplace:</Text> 64: <Text> {marketplaceName}</Text> 65: </Box> 66: {pluginDescription && <Box> 67: <Text dimColor>{pluginDescription}</Text> 68: </Box>} 69: <Box marginTop={1}> 70: <Text>Would you like to install it?</Text> 71: </Box> 72: <Box> 73: <Select options={options} onChange={onSelect} onCancel={() => onResponse('no')} /> 74: </Box> 75: </Box> 76: </PermissionDialog>; 77: }

File: src/components/CustomSelect/index.ts

typescript 1: export * from './SelectMulti.js' 2: export type { OptionWithDescription } from './select.js' 3: export * from './select.js'

File: src/components/CustomSelect/option-map.ts

typescript 1: import type { ReactNode } from 'react' 2: import type { OptionWithDescription } from './select.js' 3: type OptionMapItem<T> = { 4: label: ReactNode 5: value: T 6: description?: string 7: previous: OptionMapItem<T> | undefined 8: next: OptionMapItem<T> | undefined 9: index: number 10: } 11: export default class OptionMap<T> extends Map<T, OptionMapItem<T>> { 12: readonly first: OptionMapItem<T> | undefined 13: readonly last: OptionMapItem<T> | undefined 14: constructor(options: OptionWithDescription<T>[]) { 15: const items: Array<[T, OptionMapItem<T>]> = [] 16: let firstItem: OptionMapItem<T> | undefined 17: let lastItem: OptionMapItem<T> | undefined 18: let previous: OptionMapItem<T> | undefined 19: let index = 0 20: for (const option of options) { 21: const item = { 22: label: option.label, 23: value: option.value, 24: description: option.description, 25: previous, 26: next: undefined, 27: index, 28: } 29: if (previous) { 30: previous.next = item 31: } 32: firstItem ||= item 33: lastItem = item 34: items.push([option.value, item]) 35: index++ 36: previous = item 37: } 38: super(items) 39: this.first = firstItem 40: this.last = lastItem 41: } 42: }

File: src/components/CustomSelect/select-input-option.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { type ReactNode, useEffect, useRef, useState } from 'react'; 3: import { Box, Text, useInput } from '../../ink.js'; 4: import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js'; 5: import type { PastedContent } from '../../utils/config.js'; 6: import { getImageFromClipboard } from '../../utils/imagePaste.js'; 7: import type { ImageDimensions } from '../../utils/imageResizer.js'; 8: import { ClickableImageRef } from '../ClickableImageRef.js'; 9: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 10: import { Byline } from '../design-system/Byline.js'; 11: import TextInput from '../TextInput.js'; 12: import type { OptionWithDescription } from './select.js'; 13: import { SelectOption } from './select-option.js'; 14: type Props<T> = { 15: option: Extract<OptionWithDescription<T>, { 16: type: 'input'; 17: }>; 18: isFocused: boolean; 19: isSelected: boolean; 20: shouldShowDownArrow: boolean; 21: shouldShowUpArrow: boolean; 22: maxIndexWidth: number; 23: index: number; 24: inputValue: string; 25: onInputChange: (value: string) => void; 26: onSubmit: (value: string) => void; 27: onExit?: () => void; 28: layout: 'compact' | 'expanded'; 29: children?: ReactNode; 30: showLabel?: boolean; 31: onOpenEditor?: (currentValue: string, setValue: (value: string) => void) => void; 32: resetCursorOnUpdate?: boolean; 33: onImagePaste?: (base64Image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) => void; 34: pastedContents?: Record<number, PastedContent>; 35: onRemoveImage?: (id: number) => void; 36: imagesSelected?: boolean; 37: selectedImageIndex?: number; 38: onImagesSelectedChange?: (selected: boolean) => void; 39: onSelectedImageIndexChange?: (index: number) => void; 40: }; 41: export function SelectInputOption(t0) { 42: const $ = _c(100); 43: const { 44: option, 45: isFocused, 46: isSelected, 47: shouldShowDownArrow, 48: shouldShowUpArrow, 49: maxIndexWidth, 50: index, 51: inputValue, 52: onInputChange, 53: onSubmit, 54: onExit, 55: layout, 56: children, 57: showLabel: t1, 58: onOpenEditor, 59: resetCursorOnUpdate: t2, 60: onImagePaste, 61: pastedContents, 62: onRemoveImage, 63: imagesSelected, 64: selectedImageIndex: t3, 65: onImagesSelectedChange, 66: onSelectedImageIndexChange 67: } = t0; 68: const showLabelProp = t1 === undefined ? false : t1; 69: const resetCursorOnUpdate = t2 === undefined ? false : t2; 70: const selectedImageIndex = t3 === undefined ? 0 : t3; 71: let t4; 72: if ($[0] !== pastedContents) { 73: t4 = pastedContents ? Object.values(pastedContents).filter(_temp) : []; 74: $[0] = pastedContents; 75: $[1] = t4; 76: } else { 77: t4 = $[1]; 78: } 79: const imageAttachments = t4; 80: const showLabel = showLabelProp || option.showLabelWithValue === true; 81: const [cursorOffset, setCursorOffset] = useState(inputValue.length); 82: const isUserEditing = useRef(false); 83: let t5; 84: if ($[2] !== inputValue.length || $[3] !== isFocused || $[4] !== resetCursorOnUpdate) { 85: t5 = () => { 86: if (resetCursorOnUpdate && isFocused) { 87: if (isUserEditing.current) { 88: isUserEditing.current = false; 89: } else { 90: setCursorOffset(inputValue.length); 91: } 92: } 93: }; 94: $[2] = inputValue.length; 95: $[3] = isFocused; 96: $[4] = resetCursorOnUpdate; 97: $[5] = t5; 98: } else { 99: t5 = $[5]; 100: } 101: let t6; 102: if ($[6] !== inputValue || $[7] !== isFocused || $[8] !== resetCursorOnUpdate) { 103: t6 = [resetCursorOnUpdate, isFocused, inputValue]; 104: $[6] = inputValue; 105: $[7] = isFocused; 106: $[8] = resetCursorOnUpdate; 107: $[9] = t6; 108: } else { 109: t6 = $[9]; 110: } 111: useEffect(t5, t6); 112: let t7; 113: if ($[10] !== inputValue || $[11] !== onInputChange || $[12] !== onOpenEditor) { 114: t7 = () => { 115: onOpenEditor?.(inputValue, onInputChange); 116: }; 117: $[10] = inputValue; 118: $[11] = onInputChange; 119: $[12] = onOpenEditor; 120: $[13] = t7; 121: } else { 122: t7 = $[13]; 123: } 124: const t8 = isFocused && !!onOpenEditor; 125: let t9; 126: if ($[14] !== t8) { 127: t9 = { 128: context: "Chat", 129: isActive: t8 130: }; 131: $[14] = t8; 132: $[15] = t9; 133: } else { 134: t9 = $[15]; 135: } 136: useKeybinding("chat:externalEditor", t7, t9); 137: let t10; 138: if ($[16] !== onImagePaste) { 139: t10 = () => { 140: if (!onImagePaste) { 141: return; 142: } 143: getImageFromClipboard().then(imageData => { 144: if (imageData) { 145: onImagePaste(imageData.base64, imageData.mediaType, undefined, imageData.dimensions); 146: } 147: }); 148: }; 149: $[16] = onImagePaste; 150: $[17] = t10; 151: } else { 152: t10 = $[17]; 153: } 154: const t11 = isFocused && !!onImagePaste; 155: let t12; 156: if ($[18] !== t11) { 157: t12 = { 158: context: "Chat", 159: isActive: t11 160: }; 161: $[18] = t11; 162: $[19] = t12; 163: } else { 164: t12 = $[19]; 165: } 166: useKeybinding("chat:imagePaste", t10, t12); 167: let t13; 168: if ($[20] !== imageAttachments || $[21] !== onRemoveImage) { 169: t13 = () => { 170: if (imageAttachments.length > 0 && onRemoveImage) { 171: onRemoveImage(imageAttachments.at(-1).id); 172: } 173: }; 174: $[20] = imageAttachments; 175: $[21] = onRemoveImage; 176: $[22] = t13; 177: } else { 178: t13 = $[22]; 179: } 180: const t14 = isFocused && !imagesSelected && inputValue === "" && imageAttachments.length > 0 && !!onRemoveImage; 181: let t15; 182: if ($[23] !== t14) { 183: t15 = { 184: context: "Attachments", 185: isActive: t14 186: }; 187: $[23] = t14; 188: $[24] = t15; 189: } else { 190: t15 = $[24]; 191: } 192: useKeybinding("attachments:remove", t13, t15); 193: let t16; 194: let t17; 195: if ($[25] !== imageAttachments.length || $[26] !== onSelectedImageIndexChange || $[27] !== selectedImageIndex) { 196: t16 = () => { 197: if (imageAttachments.length > 1) { 198: onSelectedImageIndexChange?.((selectedImageIndex + 1) % imageAttachments.length); 199: } 200: }; 201: t17 = () => { 202: if (imageAttachments.length > 1) { 203: onSelectedImageIndexChange?.((selectedImageIndex - 1 + imageAttachments.length) % imageAttachments.length); 204: } 205: }; 206: $[25] = imageAttachments.length; 207: $[26] = onSelectedImageIndexChange; 208: $[27] = selectedImageIndex; 209: $[28] = t16; 210: $[29] = t17; 211: } else { 212: t16 = $[28]; 213: t17 = $[29]; 214: } 215: let t18; 216: if ($[30] !== imageAttachments || $[31] !== onImagesSelectedChange || $[32] !== onRemoveImage || $[33] !== onSelectedImageIndexChange || $[34] !== selectedImageIndex) { 217: t18 = () => { 218: const img = imageAttachments[selectedImageIndex]; 219: if (img && onRemoveImage) { 220: onRemoveImage(img.id); 221: if (imageAttachments.length <= 1) { 222: onImagesSelectedChange?.(false); 223: } else { 224: onSelectedImageIndexChange?.(Math.min(selectedImageIndex, imageAttachments.length - 2)); 225: } 226: } 227: }; 228: $[30] = imageAttachments; 229: $[31] = onImagesSelectedChange; 230: $[32] = onRemoveImage; 231: $[33] = onSelectedImageIndexChange; 232: $[34] = selectedImageIndex; 233: $[35] = t18; 234: } else { 235: t18 = $[35]; 236: } 237: let t19; 238: if ($[36] !== onImagesSelectedChange) { 239: t19 = () => { 240: onImagesSelectedChange?.(false); 241: }; 242: $[36] = onImagesSelectedChange; 243: $[37] = t19; 244: } else { 245: t19 = $[37]; 246: } 247: let t20; 248: if ($[38] !== t16 || $[39] !== t17 || $[40] !== t18 || $[41] !== t19) { 249: t20 = { 250: "attachments:next": t16, 251: "attachments:previous": t17, 252: "attachments:remove": t18, 253: "attachments:exit": t19 254: }; 255: $[38] = t16; 256: $[39] = t17; 257: $[40] = t18; 258: $[41] = t19; 259: $[42] = t20; 260: } else { 261: t20 = $[42]; 262: } 263: const t21 = isFocused && !!imagesSelected; 264: let t22; 265: if ($[43] !== t21) { 266: t22 = { 267: context: "Attachments", 268: isActive: t21 269: }; 270: $[43] = t21; 271: $[44] = t22; 272: } else { 273: t22 = $[44]; 274: } 275: useKeybindings(t20, t22); 276: let t23; 277: if ($[45] !== onImagesSelectedChange) { 278: t23 = (_input, key) => { 279: if (key.upArrow) { 280: onImagesSelectedChange?.(false); 281: } 282: }; 283: $[45] = onImagesSelectedChange; 284: $[46] = t23; 285: } else { 286: t23 = $[46]; 287: } 288: const t24 = isFocused && !!imagesSelected; 289: let t25; 290: if ($[47] !== t24) { 291: t25 = { 292: isActive: t24 293: }; 294: $[47] = t24; 295: $[48] = t25; 296: } else { 297: t25 = $[48]; 298: } 299: useInput(t23, t25); 300: let t26; 301: let t27; 302: if ($[49] !== imagesSelected || $[50] !== isFocused || $[51] !== onImagesSelectedChange) { 303: t26 = () => { 304: if (!isFocused && imagesSelected) { 305: onImagesSelectedChange?.(false); 306: } 307: }; 308: t27 = [isFocused, imagesSelected, onImagesSelectedChange]; 309: $[49] = imagesSelected; 310: $[50] = isFocused; 311: $[51] = onImagesSelectedChange; 312: $[52] = t26; 313: $[53] = t27; 314: } else { 315: t26 = $[52]; 316: t27 = $[53]; 317: } 318: useEffect(t26, t27); 319: const descriptionPaddingLeft = layout === "expanded" ? maxIndexWidth + 3 : maxIndexWidth + 4; 320: const t28 = layout === "compact" ? 0 : undefined; 321: const t29 = `${index}.`; 322: let t30; 323: if ($[54] !== maxIndexWidth || $[55] !== t29) { 324: t30 = t29.padEnd(maxIndexWidth + 2); 325: $[54] = maxIndexWidth; 326: $[55] = t29; 327: $[56] = t30; 328: } else { 329: t30 = $[56]; 330: } 331: let t31; 332: if ($[57] !== t30) { 333: t31 = <Text dimColor={true}>{t30}</Text>; 334: $[57] = t30; 335: $[58] = t31; 336: } else { 337: t31 = $[58]; 338: } 339: let t32; 340: if ($[59] !== cursorOffset || $[60] !== imagesSelected || $[61] !== inputValue || $[62] !== isFocused || $[63] !== onExit || $[64] !== onImagePaste || $[65] !== onInputChange || $[66] !== onSubmit || $[67] !== option || $[68] !== showLabel) { 341: t32 = showLabel ? <><Text color={isFocused ? "suggestion" : undefined}>{option.label}</Text>{isFocused ? <><Text color="suggestion">{option.labelValueSeparator ?? ", "}</Text><TextInput value={inputValue} onChange={value => { 342: isUserEditing.current = true; 343: onInputChange(value); 344: option.onChange(value); 345: }} onSubmit={onSubmit} onExit={onExit} placeholder={option.placeholder} focus={!imagesSelected} showCursor={true} multiline={true} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} columns={80} onImagePaste={onImagePaste} onPaste={pastedText => { 346: isUserEditing.current = true; 347: const before = inputValue.slice(0, cursorOffset); 348: const after = inputValue.slice(cursorOffset); 349: const newValue = before + pastedText + after; 350: onInputChange(newValue); 351: option.onChange(newValue); 352: setCursorOffset(before.length + pastedText.length); 353: }} /></> : inputValue && <Text>{option.labelValueSeparator ?? ", "}{inputValue}</Text>}</> : isFocused ? <TextInput value={inputValue} onChange={value_0 => { 354: isUserEditing.current = true; 355: onInputChange(value_0); 356: option.onChange(value_0); 357: }} onSubmit={onSubmit} onExit={onExit} placeholder={option.placeholder || (typeof option.label === "string" ? option.label : undefined)} focus={!imagesSelected} showCursor={true} multiline={true} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} columns={80} onImagePaste={onImagePaste} onPaste={pastedText_0 => { 358: isUserEditing.current = true; 359: const before_0 = inputValue.slice(0, cursorOffset); 360: const after_0 = inputValue.slice(cursorOffset); 361: const newValue_0 = before_0 + pastedText_0 + after_0; 362: onInputChange(newValue_0); 363: option.onChange(newValue_0); 364: setCursorOffset(before_0.length + pastedText_0.length); 365: }} /> : <Text color={inputValue ? undefined : "inactive"}>{inputValue || option.placeholder || option.label}</Text>; 366: $[59] = cursorOffset; 367: $[60] = imagesSelected; 368: $[61] = inputValue; 369: $[62] = isFocused; 370: $[63] = onExit; 371: $[64] = onImagePaste; 372: $[65] = onInputChange; 373: $[66] = onSubmit; 374: $[67] = option; 375: $[68] = showLabel; 376: $[69] = t32; 377: } else { 378: t32 = $[69]; 379: } 380: let t33; 381: if ($[70] !== children || $[71] !== t28 || $[72] !== t31 || $[73] !== t32) { 382: t33 = <Box flexDirection="row" flexShrink={t28}>{t31}{children}{t32}</Box>; 383: $[70] = children; 384: $[71] = t28; 385: $[72] = t31; 386: $[73] = t32; 387: $[74] = t33; 388: } else { 389: t33 = $[74]; 390: } 391: let t34; 392: if ($[75] !== isFocused || $[76] !== isSelected || $[77] !== shouldShowDownArrow || $[78] !== shouldShowUpArrow || $[79] !== t33) { 393: t34 = <SelectOption isFocused={isFocused} isSelected={isSelected} shouldShowDownArrow={shouldShowDownArrow} shouldShowUpArrow={shouldShowUpArrow} declareCursor={false}>{t33}</SelectOption>; 394: $[75] = isFocused; 395: $[76] = isSelected; 396: $[77] = shouldShowDownArrow; 397: $[78] = shouldShowUpArrow; 398: $[79] = t33; 399: $[80] = t34; 400: } else { 401: t34 = $[80]; 402: } 403: let t35; 404: if ($[81] !== descriptionPaddingLeft || $[82] !== isFocused || $[83] !== isSelected || $[84] !== option.description || $[85] !== option.dimDescription) { 405: t35 = option.description && <Box paddingLeft={descriptionPaddingLeft}><Text dimColor={option.dimDescription !== false} color={isSelected ? "success" : isFocused ? "suggestion" : undefined}>{option.description}</Text></Box>; 406: $[81] = descriptionPaddingLeft; 407: $[82] = isFocused; 408: $[83] = isSelected; 409: $[84] = option.description; 410: $[85] = option.dimDescription; 411: $[86] = t35; 412: } else { 413: t35 = $[86]; 414: } 415: let t36; 416: if ($[87] !== descriptionPaddingLeft || $[88] !== imageAttachments || $[89] !== imagesSelected || $[90] !== isFocused || $[91] !== selectedImageIndex) { 417: t36 = imageAttachments.length > 0 && <Box flexDirection="row" gap={1} paddingLeft={descriptionPaddingLeft}>{imageAttachments.map((img_0, idx) => <ClickableImageRef key={img_0.id} imageId={img_0.id} isSelected={!!imagesSelected && idx === selectedImageIndex} />)}<Box flexGrow={1} justifyContent="flex-start" flexDirection="row"><Text dimColor={true}>{imagesSelected ? <Byline>{imageAttachments.length > 1 && <><ConfigurableShortcutHint action="attachments:next" context="Attachments" fallback={"\u2192"} description="next" /><ConfigurableShortcutHint action="attachments:previous" context="Attachments" fallback={"\u2190"} description="prev" /></>}<ConfigurableShortcutHint action="attachments:remove" context="Attachments" fallback="backspace" description="remove" /><ConfigurableShortcutHint action="attachments:exit" context="Attachments" fallback="esc" description="cancel" /></Byline> : isFocused ? "(\u2193 to select)" : null}</Text></Box></Box>; 418: $[87] = descriptionPaddingLeft; 419: $[88] = imageAttachments; 420: $[89] = imagesSelected; 421: $[90] = isFocused; 422: $[91] = selectedImageIndex; 423: $[92] = t36; 424: } else { 425: t36 = $[92]; 426: } 427: let t37; 428: if ($[93] !== layout) { 429: t37 = layout === "expanded" && <Text> </Text>; 430: $[93] = layout; 431: $[94] = t37; 432: } else { 433: t37 = $[94]; 434: } 435: let t38; 436: if ($[95] !== t34 || $[96] !== t35 || $[97] !== t36 || $[98] !== t37) { 437: t38 = <Box flexDirection="column" flexShrink={0}>{t34}{t35}{t36}{t37}</Box>; 438: $[95] = t34; 439: $[96] = t35; 440: $[97] = t36; 441: $[98] = t37; 442: $[99] = t38; 443: } else { 444: t38 = $[99]; 445: } 446: return t38; 447: } 448: function _temp(c) { 449: return c.type === "image"; 450: }

File: src/components/CustomSelect/select-option.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { type ReactNode } from 'react'; 3: import { ListItem } from '../design-system/ListItem.js'; 4: export type SelectOptionProps = { 5: readonly isFocused: boolean; 6: readonly isSelected: boolean; 7: readonly children: ReactNode; 8: readonly description?: string; 9: readonly shouldShowDownArrow?: boolean; 10: readonly shouldShowUpArrow?: boolean; 11: readonly declareCursor?: boolean; 12: }; 13: export function SelectOption(t0) { 14: const $ = _c(8); 15: const { 16: isFocused, 17: isSelected, 18: children, 19: description, 20: shouldShowDownArrow, 21: shouldShowUpArrow, 22: declareCursor 23: } = t0; 24: let t1; 25: if ($[0] !== children || $[1] !== declareCursor || $[2] !== description || $[3] !== isFocused || $[4] !== isSelected || $[5] !== shouldShowDownArrow || $[6] !== shouldShowUpArrow) { 26: t1 = <ListItem isFocused={isFocused} isSelected={isSelected} description={description} showScrollDown={shouldShowDownArrow} showScrollUp={shouldShowUpArrow} styled={false} declareCursor={declareCursor}>{children}</ListItem>; 27: $[0] = children; 28: $[1] = declareCursor; 29: $[2] = description; 30: $[3] = isFocused; 31: $[4] = isSelected; 32: $[5] = shouldShowDownArrow; 33: $[6] = shouldShowUpArrow; 34: $[7] = t1; 35: } else { 36: t1 = $[7]; 37: } 38: return t1; 39: }

File: src/components/CustomSelect/select.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React, { type ReactNode, useEffect, useRef, useState } from 'react'; 4: import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js'; 5: import { stringWidth } from '../../ink/stringWidth.js'; 6: import { Ansi, Box, Text } from '../../ink.js'; 7: import { count } from '../../utils/array.js'; 8: import type { PastedContent } from '../../utils/config.js'; 9: import type { ImageDimensions } from '../../utils/imageResizer.js'; 10: import { SelectInputOption } from './select-input-option.js'; 11: import { SelectOption } from './select-option.js'; 12: import { useSelectInput } from './use-select-input.js'; 13: import { useSelectState } from './use-select-state.js'; 14: function getTextContent(node: ReactNode): string { 15: if (typeof node === 'string') return node; 16: if (typeof node === 'number') return String(node); 17: if (!node) return ''; 18: if (Array.isArray(node)) return node.map(getTextContent).join(''); 19: if (React.isValidElement<{ 20: children?: ReactNode; 21: }>(node)) { 22: return getTextContent(node.props.children); 23: } 24: return ''; 25: } 26: type BaseOption<T> = { 27: description?: string; 28: dimDescription?: boolean; 29: label: ReactNode; 30: value: T; 31: disabled?: boolean; 32: }; 33: export type OptionWithDescription<T = string> = (BaseOption<T> & { 34: type?: 'text'; 35: }) | (BaseOption<T> & { 36: type: 'input'; 37: onChange: (value: string) => void; 38: placeholder?: string; 39: initialValue?: string; 40: allowEmptySubmitToCancel?: boolean; 41: showLabelWithValue?: boolean; 42: labelValueSeparator?: string; 43: resetCursorOnUpdate?: boolean; 44: }); 45: export type SelectProps<T> = { 46: readonly isDisabled?: boolean; 47: readonly disableSelection?: boolean; 48: readonly hideIndexes?: boolean; 49: readonly visibleOptionCount?: number; 50: readonly highlightText?: string; 51: readonly options: OptionWithDescription<T>[]; 52: readonly defaultValue?: T; 53: readonly onCancel?: () => void; 54: readonly onChange?: (value: T) => void; 55: readonly onFocus?: (value: T) => void; 56: readonly defaultFocusValue?: T; 57: readonly layout?: 'compact' | 'expanded' | 'compact-vertical'; 58: readonly inlineDescriptions?: boolean; 59: readonly onUpFromFirstItem?: () => void; 60: readonly onDownFromLastItem?: () => void; 61: readonly onInputModeToggle?: (value: T) => void; 62: readonly onOpenEditor?: (currentValue: string, setValue: (value: string) => void) => void; 63: readonly onImagePaste?: (base64Image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) => void; 64: readonly pastedContents?: Record<number, PastedContent>; 65: readonly onRemoveImage?: (id: number) => void; 66: }; 67: export function Select(t0) { 68: const $ = _c(72); 69: const { 70: isDisabled: t1, 71: hideIndexes: t2, 72: visibleOptionCount: t3, 73: highlightText, 74: options, 75: defaultValue, 76: onCancel, 77: onChange, 78: onFocus, 79: defaultFocusValue, 80: layout: t4, 81: disableSelection: t5, 82: inlineDescriptions: t6, 83: onUpFromFirstItem, 84: onDownFromLastItem, 85: onInputModeToggle, 86: onOpenEditor, 87: onImagePaste, 88: pastedContents, 89: onRemoveImage 90: } = t0; 91: const isDisabled = t1 === undefined ? false : t1; 92: const hideIndexes = t2 === undefined ? false : t2; 93: const visibleOptionCount = t3 === undefined ? 5 : t3; 94: const layout = t4 === undefined ? "compact" : t4; 95: const disableSelection = t5 === undefined ? false : t5; 96: const inlineDescriptions = t6 === undefined ? false : t6; 97: const [imagesSelected, setImagesSelected] = useState(false); 98: const [selectedImageIndex, setSelectedImageIndex] = useState(0); 99: let t7; 100: if ($[0] !== options) { 101: t7 = () => { 102: const initialMap = new Map(); 103: options.forEach(option => { 104: if (option.type === "input" && option.initialValue) { 105: initialMap.set(option.value, option.initialValue); 106: } 107: }); 108: return initialMap; 109: }; 110: $[0] = options; 111: $[1] = t7; 112: } else { 113: t7 = $[1]; 114: } 115: const [inputValues, setInputValues] = useState(t7); 116: let t8; 117: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 118: t8 = new Map(); 119: $[2] = t8; 120: } else { 121: t8 = $[2]; 122: } 123: const lastInitialValues = useRef(t8); 124: let t10; 125: let t9; 126: if ($[3] !== inputValues || $[4] !== options) { 127: t9 = () => { 128: for (const option_0 of options) { 129: if (option_0.type === "input" && option_0.initialValue !== undefined) { 130: const lastInitial = lastInitialValues.current.get(option_0.value) ?? ""; 131: const currentValue = inputValues.get(option_0.value) ?? ""; 132: const newInitial = option_0.initialValue; 133: if (newInitial !== lastInitial && currentValue === lastInitial) { 134: setInputValues(prev => { 135: const next = new Map(prev); 136: next.set(option_0.value, newInitial); 137: return next; 138: }); 139: } 140: lastInitialValues.current.set(option_0.value, newInitial); 141: } 142: } 143: }; 144: t10 = [options, inputValues]; 145: $[3] = inputValues; 146: $[4] = options; 147: $[5] = t10; 148: $[6] = t9; 149: } else { 150: t10 = $[5]; 151: t9 = $[6]; 152: } 153: useEffect(t9, t10); 154: let t11; 155: if ($[7] !== defaultFocusValue || $[8] !== defaultValue || $[9] !== onCancel || $[10] !== onChange || $[11] !== onFocus || $[12] !== options || $[13] !== visibleOptionCount) { 156: t11 = { 157: visibleOptionCount, 158: options, 159: defaultValue, 160: onChange, 161: onCancel, 162: onFocus, 163: focusValue: defaultFocusValue 164: }; 165: $[7] = defaultFocusValue; 166: $[8] = defaultValue; 167: $[9] = onCancel; 168: $[10] = onChange; 169: $[11] = onFocus; 170: $[12] = options; 171: $[13] = visibleOptionCount; 172: $[14] = t11; 173: } else { 174: t11 = $[14]; 175: } 176: const state = useSelectState(t11); 177: const t12 = disableSelection || (hideIndexes ? "numeric" : false); 178: let t13; 179: if ($[15] !== pastedContents) { 180: t13 = () => { 181: if (pastedContents && Object.values(pastedContents).some(_temp)) { 182: const imageCount = count(Object.values(pastedContents), _temp2); 183: setImagesSelected(true); 184: setSelectedImageIndex(imageCount - 1); 185: return true; 186: } 187: return false; 188: }; 189: $[15] = pastedContents; 190: $[16] = t13; 191: } else { 192: t13 = $[16]; 193: } 194: let t14; 195: if ($[17] !== imagesSelected || $[18] !== inputValues || $[19] !== isDisabled || $[20] !== onDownFromLastItem || $[21] !== onInputModeToggle || $[22] !== onUpFromFirstItem || $[23] !== options || $[24] !== state || $[25] !== t12 || $[26] !== t13) { 196: t14 = { 197: isDisabled, 198: disableSelection: t12, 199: state, 200: options, 201: isMultiSelect: false, 202: onUpFromFirstItem, 203: onDownFromLastItem, 204: onInputModeToggle, 205: inputValues, 206: imagesSelected, 207: onEnterImageSelection: t13 208: }; 209: $[17] = imagesSelected; 210: $[18] = inputValues; 211: $[19] = isDisabled; 212: $[20] = onDownFromLastItem; 213: $[21] = onInputModeToggle; 214: $[22] = onUpFromFirstItem; 215: $[23] = options; 216: $[24] = state; 217: $[25] = t12; 218: $[26] = t13; 219: $[27] = t14; 220: } else { 221: t14 = $[27]; 222: } 223: useSelectInput(t14); 224: let T0; 225: let t15; 226: let t16; 227: let t17; 228: if ($[28] !== hideIndexes || $[29] !== highlightText || $[30] !== imagesSelected || $[31] !== inlineDescriptions || $[32] !== inputValues || $[33] !== isDisabled || $[34] !== layout || $[35] !== onCancel || $[36] !== onChange || $[37] !== onImagePaste || $[38] !== onOpenEditor || $[39] !== onRemoveImage || $[40] !== options.length || $[41] !== pastedContents || $[42] !== selectedImageIndex || $[43] !== state.focusedValue || $[44] !== state.options || $[45] !== state.value || $[46] !== state.visibleFromIndex || $[47] !== state.visibleOptions || $[48] !== state.visibleToIndex) { 229: t17 = Symbol.for("react.early_return_sentinel"); 230: bb0: { 231: const styles = { 232: container: _temp3, 233: highlightedText: _temp4 234: }; 235: if (layout === "expanded") { 236: let t18; 237: if ($[53] !== state.options.length) { 238: t18 = state.options.length.toString(); 239: $[53] = state.options.length; 240: $[54] = t18; 241: } else { 242: t18 = $[54]; 243: } 244: const maxIndexWidth = t18.length; 245: t17 = <Box {...styles.container()}>{state.visibleOptions.map((option_1, index) => { 246: const isFirstVisibleOption = option_1.index === state.visibleFromIndex; 247: const isLastVisibleOption = option_1.index === state.visibleToIndex - 1; 248: const areMoreOptionsBelow = state.visibleToIndex < options.length; 249: const areMoreOptionsAbove = state.visibleFromIndex > 0; 250: const i = state.visibleFromIndex + index + 1; 251: const isFocused = !isDisabled && state.focusedValue === option_1.value; 252: const isSelected = state.value === option_1.value; 253: if (option_1.type === "input") { 254: const inputValue = inputValues.has(option_1.value) ? inputValues.get(option_1.value) : option_1.initialValue || ""; 255: return <SelectInputOption key={String(option_1.value)} option={option_1} isFocused={isFocused} isSelected={isSelected} shouldShowDownArrow={areMoreOptionsBelow && isLastVisibleOption} shouldShowUpArrow={areMoreOptionsAbove && isFirstVisibleOption} maxIndexWidth={maxIndexWidth} index={i} inputValue={inputValue} onInputChange={value => { 256: setInputValues(prev_0 => { 257: const next_0 = new Map(prev_0); 258: next_0.set(option_1.value, value); 259: return next_0; 260: }); 261: }} onSubmit={value_0 => { 262: const hasImageAttachments = pastedContents && Object.values(pastedContents).some(_temp5); 263: if (value_0.trim() || hasImageAttachments || option_1.allowEmptySubmitToCancel) { 264: onChange?.(option_1.value); 265: } else { 266: onCancel?.(); 267: } 268: }} onExit={onCancel} layout="expanded" showLabel={inlineDescriptions} onOpenEditor={onOpenEditor} resetCursorOnUpdate={option_1.resetCursorOnUpdate} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage} imagesSelected={imagesSelected} selectedImageIndex={selectedImageIndex} onImagesSelectedChange={setImagesSelected} onSelectedImageIndexChange={setSelectedImageIndex} />; 269: } 270: let label = option_1.label; 271: if (typeof option_1.label === "string" && highlightText && option_1.label.includes(highlightText)) { 272: const labelText = option_1.label; 273: const index_0 = labelText.indexOf(highlightText); 274: label = <>{labelText.slice(0, index_0)}<Text {...styles.highlightedText()}>{highlightText}</Text>{labelText.slice(index_0 + highlightText.length)}</>; 275: } 276: const isOptionDisabled = option_1.disabled === true; 277: const optionColor = isOptionDisabled ? undefined : isSelected ? "success" : isFocused ? "suggestion" : undefined; 278: return <Box key={String(option_1.value)} flexDirection="column" flexShrink={0}><SelectOption isFocused={isFocused} isSelected={isSelected} shouldShowDownArrow={areMoreOptionsBelow && isLastVisibleOption} shouldShowUpArrow={areMoreOptionsAbove && isFirstVisibleOption}><Text dimColor={isOptionDisabled} color={optionColor}>{label}</Text></SelectOption>{option_1.description && <Box paddingLeft={2}><Text dimColor={isOptionDisabled || option_1.dimDescription !== false} color={optionColor}><Ansi>{option_1.description}</Ansi></Text></Box>}<Text> </Text></Box>; 279: })}</Box>; 280: break bb0; 281: } 282: if (layout === "compact-vertical") { 283: let t18; 284: if ($[55] !== hideIndexes || $[56] !== state.options) { 285: t18 = hideIndexes ? 0 : state.options.length.toString().length; 286: $[55] = hideIndexes; 287: $[56] = state.options; 288: $[57] = t18; 289: } else { 290: t18 = $[57]; 291: } 292: const maxIndexWidth_0 = t18; 293: t17 = <Box {...styles.container()}>{state.visibleOptions.map((option_2, index_1) => { 294: const isFirstVisibleOption_0 = option_2.index === state.visibleFromIndex; 295: const isLastVisibleOption_0 = option_2.index === state.visibleToIndex - 1; 296: const areMoreOptionsBelow_0 = state.visibleToIndex < options.length; 297: const areMoreOptionsAbove_0 = state.visibleFromIndex > 0; 298: const i_0 = state.visibleFromIndex + index_1 + 1; 299: const isFocused_0 = !isDisabled && state.focusedValue === option_2.value; 300: const isSelected_0 = state.value === option_2.value; 301: if (option_2.type === "input") { 302: const inputValue_0 = inputValues.has(option_2.value) ? inputValues.get(option_2.value) : option_2.initialValue || ""; 303: return <SelectInputOption key={String(option_2.value)} option={option_2} isFocused={isFocused_0} isSelected={isSelected_0} shouldShowDownArrow={areMoreOptionsBelow_0 && isLastVisibleOption_0} shouldShowUpArrow={areMoreOptionsAbove_0 && isFirstVisibleOption_0} maxIndexWidth={maxIndexWidth_0} index={i_0} inputValue={inputValue_0} onInputChange={value_1 => { 304: setInputValues(prev_1 => { 305: const next_1 = new Map(prev_1); 306: next_1.set(option_2.value, value_1); 307: return next_1; 308: }); 309: }} onSubmit={value_2 => { 310: const hasImageAttachments_0 = pastedContents && Object.values(pastedContents).some(_temp6); 311: if (value_2.trim() || hasImageAttachments_0 || option_2.allowEmptySubmitToCancel) { 312: onChange?.(option_2.value); 313: } else { 314: onCancel?.(); 315: } 316: }} onExit={onCancel} layout="compact" showLabel={inlineDescriptions} onOpenEditor={onOpenEditor} resetCursorOnUpdate={option_2.resetCursorOnUpdate} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage} imagesSelected={imagesSelected} selectedImageIndex={selectedImageIndex} onImagesSelectedChange={setImagesSelected} onSelectedImageIndexChange={setSelectedImageIndex} />; 317: } 318: let label_0 = option_2.label; 319: if (typeof option_2.label === "string" && highlightText && option_2.label.includes(highlightText)) { 320: const labelText_0 = option_2.label; 321: const index_2 = labelText_0.indexOf(highlightText); 322: label_0 = <>{labelText_0.slice(0, index_2)}<Text {...styles.highlightedText()}>{highlightText}</Text>{labelText_0.slice(index_2 + highlightText.length)}</>; 323: } 324: const isOptionDisabled_0 = option_2.disabled === true; 325: return <Box key={String(option_2.value)} flexDirection="column" flexShrink={0}><SelectOption isFocused={isFocused_0} isSelected={isSelected_0} shouldShowDownArrow={areMoreOptionsBelow_0 && isLastVisibleOption_0} shouldShowUpArrow={areMoreOptionsAbove_0 && isFirstVisibleOption_0}><>{!hideIndexes && <Text dimColor={true}>{`${i_0}.`.padEnd(maxIndexWidth_0 + 1)}</Text>}<Text dimColor={isOptionDisabled_0} color={isOptionDisabled_0 ? undefined : isSelected_0 ? "success" : isFocused_0 ? "suggestion" : undefined}>{label_0}</Text></></SelectOption>{option_2.description && <Box paddingLeft={hideIndexes ? 4 : maxIndexWidth_0 + 4}><Text dimColor={isOptionDisabled_0 || option_2.dimDescription !== false} color={isOptionDisabled_0 ? undefined : isSelected_0 ? "success" : isFocused_0 ? "suggestion" : undefined}><Ansi>{option_2.description}</Ansi></Text></Box>}</Box>; 326: })}</Box>; 327: break bb0; 328: } 329: let t18; 330: if ($[58] !== hideIndexes || $[59] !== state.options) { 331: t18 = hideIndexes ? 0 : state.options.length.toString().length; 332: $[58] = hideIndexes; 333: $[59] = state.options; 334: $[60] = t18; 335: } else { 336: t18 = $[60]; 337: } 338: const maxIndexWidth_1 = t18; 339: const hasInputOptions = state.visibleOptions.some(_temp7); 340: const hasDescriptions = !inlineDescriptions && !hasInputOptions && state.visibleOptions.some(_temp8); 341: const optionData = state.visibleOptions.map((option_3, index_3) => { 342: const isFirstVisibleOption_1 = option_3.index === state.visibleFromIndex; 343: const isLastVisibleOption_1 = option_3.index === state.visibleToIndex - 1; 344: const areMoreOptionsBelow_1 = state.visibleToIndex < options.length; 345: const areMoreOptionsAbove_1 = state.visibleFromIndex > 0; 346: const i_1 = state.visibleFromIndex + index_3 + 1; 347: const isFocused_1 = !isDisabled && state.focusedValue === option_3.value; 348: const isSelected_1 = state.value === option_3.value; 349: const isOptionDisabled_1 = option_3.disabled === true; 350: let label_1 = option_3.label; 351: if (typeof option_3.label === "string" && highlightText && option_3.label.includes(highlightText)) { 352: const labelText_1 = option_3.label; 353: const idx = labelText_1.indexOf(highlightText); 354: label_1 = <>{labelText_1.slice(0, idx)}<Text {...styles.highlightedText()}>{highlightText}</Text>{labelText_1.slice(idx + highlightText.length)}</>; 355: } 356: return { 357: option: option_3, 358: index: i_1, 359: label: label_1, 360: isFocused: isFocused_1, 361: isSelected: isSelected_1, 362: isOptionDisabled: isOptionDisabled_1, 363: shouldShowDownArrow: areMoreOptionsBelow_1 && isLastVisibleOption_1, 364: shouldShowUpArrow: areMoreOptionsAbove_1 && isFirstVisibleOption_1 365: }; 366: }); 367: if (hasDescriptions) { 368: let t19; 369: if ($[61] !== hideIndexes || $[62] !== maxIndexWidth_1) { 370: t19 = data => { 371: if (data.option.type === "input") { 372: return 0; 373: } 374: const labelText_2 = getTextContent(data.option.label); 375: const indexWidth = hideIndexes ? 0 : maxIndexWidth_1 + 2; 376: const checkmarkWidth = data.isSelected ? 2 : 0; 377: return 2 + indexWidth + stringWidth(labelText_2) + checkmarkWidth; 378: }; 379: $[61] = hideIndexes; 380: $[62] = maxIndexWidth_1; 381: $[63] = t19; 382: } else { 383: t19 = $[63]; 384: } 385: const maxLabelWidth = Math.max(...optionData.map(t19)); 386: let t20; 387: if ($[64] !== hideIndexes || $[65] !== maxIndexWidth_1 || $[66] !== maxLabelWidth) { 388: t20 = data_0 => { 389: if (data_0.option.type === "input") { 390: return null; 391: } 392: const labelText_3 = getTextContent(data_0.option.label); 393: const indexWidth_0 = hideIndexes ? 0 : maxIndexWidth_1 + 2; 394: const checkmarkWidth_0 = data_0.isSelected ? 2 : 0; 395: const currentLabelWidth = 2 + indexWidth_0 + stringWidth(labelText_3) + checkmarkWidth_0; 396: const padding = maxLabelWidth - currentLabelWidth; 397: return <TwoColumnRow key={String(data_0.option.value)} isFocused={data_0.isFocused}><Box flexDirection="row" flexShrink={0}>{data_0.isFocused ? <Text color="suggestion">{figures.pointer}</Text> : data_0.shouldShowDownArrow ? <Text dimColor={true}>{figures.arrowDown}</Text> : data_0.shouldShowUpArrow ? <Text dimColor={true}>{figures.arrowUp}</Text> : <Text> </Text>}<Text> </Text><Text dimColor={data_0.isOptionDisabled} color={data_0.isOptionDisabled ? undefined : data_0.isSelected ? "success" : data_0.isFocused ? "suggestion" : undefined}>{!hideIndexes && <Text dimColor={true}>{`${data_0.index}.`.padEnd(maxIndexWidth_1 + 2)}</Text>}{data_0.label}</Text>{data_0.isSelected && <Text color="success"> {figures.tick}</Text>}{padding > 0 && <Text>{" ".repeat(padding)}</Text>}</Box><Box flexGrow={1} marginLeft={2}><Text wrap="wrap" dimColor={data_0.isOptionDisabled || data_0.option.dimDescription !== false} color={data_0.isOptionDisabled ? undefined : data_0.isSelected ? "success" : data_0.isFocused ? "suggestion" : undefined}><Ansi>{data_0.option.description || " "}</Ansi></Text></Box></TwoColumnRow>; 398: }; 399: $[64] = hideIndexes; 400: $[65] = maxIndexWidth_1; 401: $[66] = maxLabelWidth; 402: $[67] = t20; 403: } else { 404: t20 = $[67]; 405: } 406: t17 = <Box {...styles.container()}>{optionData.map(t20)}</Box>; 407: break bb0; 408: } 409: T0 = Box; 410: t15 = styles.container(); 411: t16 = state.visibleOptions.map((option_4, index_4) => { 412: if (option_4.type === "input") { 413: const inputValue_1 = inputValues.has(option_4.value) ? inputValues.get(option_4.value) : option_4.initialValue || ""; 414: const isFirstVisibleOption_2 = option_4.index === state.visibleFromIndex; 415: const isLastVisibleOption_2 = option_4.index === state.visibleToIndex - 1; 416: const areMoreOptionsBelow_2 = state.visibleToIndex < options.length; 417: const areMoreOptionsAbove_2 = state.visibleFromIndex > 0; 418: const i_2 = state.visibleFromIndex + index_4 + 1; 419: const isFocused_2 = !isDisabled && state.focusedValue === option_4.value; 420: const isSelected_2 = state.value === option_4.value; 421: return <SelectInputOption key={String(option_4.value)} option={option_4} isFocused={isFocused_2} isSelected={isSelected_2} shouldShowDownArrow={areMoreOptionsBelow_2 && isLastVisibleOption_2} shouldShowUpArrow={areMoreOptionsAbove_2 && isFirstVisibleOption_2} maxIndexWidth={maxIndexWidth_1} index={i_2} inputValue={inputValue_1} onInputChange={value_3 => { 422: setInputValues(prev_2 => { 423: const next_2 = new Map(prev_2); 424: next_2.set(option_4.value, value_3); 425: return next_2; 426: }); 427: }} onSubmit={value_4 => { 428: const hasImageAttachments_1 = pastedContents && Object.values(pastedContents).some(_temp9); 429: if (value_4.trim() || hasImageAttachments_1 || option_4.allowEmptySubmitToCancel) { 430: onChange?.(option_4.value); 431: } else { 432: onCancel?.(); 433: } 434: }} onExit={onCancel} layout="compact" showLabel={inlineDescriptions} onOpenEditor={onOpenEditor} resetCursorOnUpdate={option_4.resetCursorOnUpdate} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage} imagesSelected={imagesSelected} selectedImageIndex={selectedImageIndex} onImagesSelectedChange={setImagesSelected} onSelectedImageIndexChange={setSelectedImageIndex} />; 435: } 436: let label_2 = option_4.label; 437: if (typeof option_4.label === "string" && highlightText && option_4.label.includes(highlightText)) { 438: const labelText_4 = option_4.label; 439: const index_5 = labelText_4.indexOf(highlightText); 440: label_2 = <>{labelText_4.slice(0, index_5)}<Text {...styles.highlightedText()}>{highlightText}</Text>{labelText_4.slice(index_5 + highlightText.length)}</>; 441: } 442: const isFirstVisibleOption_3 = option_4.index === state.visibleFromIndex; 443: const isLastVisibleOption_3 = option_4.index === state.visibleToIndex - 1; 444: const areMoreOptionsBelow_3 = state.visibleToIndex < options.length; 445: const areMoreOptionsAbove_3 = state.visibleFromIndex > 0; 446: const i_3 = state.visibleFromIndex + index_4 + 1; 447: const isFocused_3 = !isDisabled && state.focusedValue === option_4.value; 448: const isSelected_3 = state.value === option_4.value; 449: const isOptionDisabled_2 = option_4.disabled === true; 450: return <SelectOption key={String(option_4.value)} isFocused={isFocused_3} isSelected={isSelected_3} shouldShowDownArrow={areMoreOptionsBelow_3 && isLastVisibleOption_3} shouldShowUpArrow={areMoreOptionsAbove_3 && isFirstVisibleOption_3}><Box flexDirection="row" flexShrink={0}>{!hideIndexes && <Text dimColor={true}>{`${i_3}.`.padEnd(maxIndexWidth_1 + 2)}</Text>}<Text dimColor={isOptionDisabled_2} color={isOptionDisabled_2 ? undefined : isSelected_3 ? "success" : isFocused_3 ? "suggestion" : undefined}>{label_2}{inlineDescriptions && option_4.description && <Text dimColor={isOptionDisabled_2 || option_4.dimDescription !== false}>{" "}{option_4.description}</Text>}</Text></Box>{!inlineDescriptions && option_4.description && <Box flexShrink={99} marginLeft={2}><Text wrap="wrap-trim" dimColor={isOptionDisabled_2 || option_4.dimDescription !== false} color={isOptionDisabled_2 ? undefined : isSelected_3 ? "success" : isFocused_3 ? "suggestion" : undefined}><Ansi>{option_4.description}</Ansi></Text></Box>}</SelectOption>; 451: }); 452: } 453: $[28] = hideIndexes; 454: $[29] = highlightText; 455: $[30] = imagesSelected; 456: $[31] = inlineDescriptions; 457: $[32] = inputValues; 458: $[33] = isDisabled; 459: $[34] = layout; 460: $[35] = onCancel; 461: $[36] = onChange; 462: $[37] = onImagePaste; 463: $[38] = onOpenEditor; 464: $[39] = onRemoveImage; 465: $[40] = options.length; 466: $[41] = pastedContents; 467: $[42] = selectedImageIndex; 468: $[43] = state.focusedValue; 469: $[44] = state.options; 470: $[45] = state.value; 471: $[46] = state.visibleFromIndex; 472: $[47] = state.visibleOptions; 473: $[48] = state.visibleToIndex; 474: $[49] = T0; 475: $[50] = t15; 476: $[51] = t16; 477: $[52] = t17; 478: } else { 479: T0 = $[49]; 480: t15 = $[50]; 481: t16 = $[51]; 482: t17 = $[52]; 483: } 484: if (t17 !== Symbol.for("react.early_return_sentinel")) { 485: return t17; 486: } 487: let t18; 488: if ($[68] !== T0 || $[69] !== t15 || $[70] !== t16) { 489: t18 = <T0 {...t15}>{t16}</T0>; 490: $[68] = T0; 491: $[69] = t15; 492: $[70] = t16; 493: $[71] = t18; 494: } else { 495: t18 = $[71]; 496: } 497: return t18; 498: } 499: function _temp9(c_3) { 500: return c_3.type === "image"; 501: } 502: function _temp8(opt_0) { 503: return opt_0.description; 504: } 505: function _temp7(opt) { 506: return opt.type === "input"; 507: } 508: function _temp6(c_2) { 509: return c_2.type === "image"; 510: } 511: function _temp5(c_1) { 512: return c_1.type === "image"; 513: } 514: function _temp4() { 515: return { 516: bold: true 517: }; 518: } 519: function _temp3() { 520: return { 521: flexDirection: "column" as const 522: }; 523: } 524: function _temp2(c) { 525: return c.type === "image"; 526: } 527: function _temp(c_0) { 528: return c_0.type === "image"; 529: } 530: function TwoColumnRow(t0) { 531: const $ = _c(5); 532: const { 533: isFocused, 534: children 535: } = t0; 536: let t1; 537: if ($[0] !== isFocused) { 538: t1 = { 539: line: 0, 540: column: 0, 541: active: isFocused 542: }; 543: $[0] = isFocused; 544: $[1] = t1; 545: } else { 546: t1 = $[1]; 547: } 548: const cursorRef = useDeclaredCursor(t1); 549: let t2; 550: if ($[2] !== children || $[3] !== cursorRef) { 551: t2 = <Box ref={cursorRef} flexDirection="row">{children}</Box>; 552: $[2] = children; 553: $[3] = cursorRef; 554: $[4] = t2; 555: } else { 556: t2 = $[4]; 557: } 558: return t2; 559: }

File: src/components/CustomSelect/SelectMulti.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React from 'react'; 4: import { Box, Text } from '../../ink.js'; 5: import type { PastedContent } from '../../utils/config.js'; 6: import type { ImageDimensions } from '../../utils/imageResizer.js'; 7: import type { OptionWithDescription } from './select.js'; 8: import { SelectInputOption } from './select-input-option.js'; 9: import { SelectOption } from './select-option.js'; 10: import { useMultiSelectState } from './use-multi-select-state.js'; 11: export type SelectMultiProps<T> = { 12: readonly isDisabled?: boolean; 13: readonly visibleOptionCount?: number; 14: readonly options: OptionWithDescription<T>[]; 15: readonly defaultValue?: T[]; 16: readonly onCancel: () => void; 17: readonly onChange?: (values: T[]) => void; 18: readonly onFocus?: (value: T) => void; 19: readonly focusValue?: T; 20: readonly submitButtonText?: string; 21: readonly onSubmit?: (values: T[]) => void; 22: readonly hideIndexes?: boolean; 23: readonly onDownFromLastItem?: () => void; 24: readonly onUpFromFirstItem?: () => void; 25: readonly initialFocusLast?: boolean; 26: readonly onOpenEditor?: (currentValue: string, setValue: (value: string) => void) => void; 27: readonly onImagePaste?: (base64Image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) => void; 28: readonly pastedContents?: Record<number, PastedContent>; 29: readonly onRemoveImage?: (id: number) => void; 30: }; 31: export function SelectMulti(t0) { 32: const $ = _c(44); 33: const { 34: isDisabled: t1, 35: visibleOptionCount: t2, 36: options, 37: defaultValue: t3, 38: onCancel, 39: onChange, 40: onFocus, 41: focusValue, 42: submitButtonText, 43: onSubmit, 44: onDownFromLastItem, 45: onUpFromFirstItem, 46: initialFocusLast, 47: onOpenEditor, 48: hideIndexes: t4, 49: onImagePaste, 50: pastedContents, 51: onRemoveImage 52: } = t0; 53: const isDisabled = t1 === undefined ? false : t1; 54: const visibleOptionCount = t2 === undefined ? 5 : t2; 55: let t5; 56: if ($[0] !== t3) { 57: t5 = t3 === undefined ? [] : t3; 58: $[0] = t3; 59: $[1] = t5; 60: } else { 61: t5 = $[1]; 62: } 63: const defaultValue = t5; 64: const hideIndexes = t4 === undefined ? false : t4; 65: let t6; 66: if ($[2] !== defaultValue || $[3] !== focusValue || $[4] !== hideIndexes || $[5] !== initialFocusLast || $[6] !== isDisabled || $[7] !== onCancel || $[8] !== onChange || $[9] !== onDownFromLastItem || $[10] !== onFocus || $[11] !== onSubmit || $[12] !== onUpFromFirstItem || $[13] !== options || $[14] !== submitButtonText || $[15] !== visibleOptionCount) { 67: t6 = { 68: isDisabled, 69: visibleOptionCount, 70: options, 71: defaultValue, 72: onChange, 73: onCancel, 74: onFocus, 75: focusValue, 76: submitButtonText, 77: onSubmit, 78: onDownFromLastItem, 79: onUpFromFirstItem, 80: initialFocusLast, 81: hideIndexes 82: }; 83: $[2] = defaultValue; 84: $[3] = focusValue; 85: $[4] = hideIndexes; 86: $[5] = initialFocusLast; 87: $[6] = isDisabled; 88: $[7] = onCancel; 89: $[8] = onChange; 90: $[9] = onDownFromLastItem; 91: $[10] = onFocus; 92: $[11] = onSubmit; 93: $[12] = onUpFromFirstItem; 94: $[13] = options; 95: $[14] = submitButtonText; 96: $[15] = visibleOptionCount; 97: $[16] = t6; 98: } else { 99: t6 = $[16]; 100: } 101: const state = useMultiSelectState(t6); 102: let T0; 103: let T1; 104: let t7; 105: let t8; 106: let t9; 107: if ($[17] !== hideIndexes || $[18] !== isDisabled || $[19] !== onCancel || $[20] !== onImagePaste || $[21] !== onOpenEditor || $[22] !== onRemoveImage || $[23] !== options.length || $[24] !== pastedContents || $[25] !== state) { 108: const maxIndexWidth = options.length.toString().length; 109: T1 = Box; 110: t9 = "column"; 111: T0 = Box; 112: t7 = "column"; 113: t8 = state.visibleOptions.map((option, index) => { 114: const isOptionFocused = !isDisabled && state.focusedValue === option.value && !state.isSubmitFocused; 115: const isSelected = state.selectedValues.includes(option.value); 116: const isFirstVisibleOption = option.index === state.visibleFromIndex; 117: const isLastVisibleOption = option.index === state.visibleToIndex - 1; 118: const areMoreOptionsBelow = state.visibleToIndex < options.length; 119: const areMoreOptionsAbove = state.visibleFromIndex > 0; 120: const i = state.visibleFromIndex + index + 1; 121: if (option.type === "input") { 122: const inputValue = state.inputValues.get(option.value) || ""; 123: return <Box key={String(option.value)} gap={1}><SelectInputOption option={option} isFocused={isOptionFocused} isSelected={false} shouldShowDownArrow={areMoreOptionsBelow && isLastVisibleOption} shouldShowUpArrow={areMoreOptionsAbove && isFirstVisibleOption} maxIndexWidth={maxIndexWidth} index={i} inputValue={inputValue} onInputChange={value => { 124: state.updateInputValue(option.value, value); 125: }} onSubmit={_temp} onExit={() => { 126: onCancel(); 127: }} layout="compact" onOpenEditor={onOpenEditor} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage}><Text color={isSelected ? "success" : undefined}>[{isSelected ? figures.tick : " "}]{" "}</Text></SelectInputOption></Box>; 128: } 129: return <Box key={String(option.value)} gap={1}><SelectOption isFocused={isOptionFocused} isSelected={false} shouldShowDownArrow={areMoreOptionsBelow && isLastVisibleOption} shouldShowUpArrow={areMoreOptionsAbove && isFirstVisibleOption} description={option.description}>{!hideIndexes && <Text dimColor={true}>{`${i}.`.padEnd(maxIndexWidth)}</Text>}<Text color={isSelected ? "success" : undefined}>[{isSelected ? figures.tick : " "}]</Text><Text color={isOptionFocused ? "suggestion" : undefined}>{option.label}</Text></SelectOption></Box>; 130: }); 131: $[17] = hideIndexes; 132: $[18] = isDisabled; 133: $[19] = onCancel; 134: $[20] = onImagePaste; 135: $[21] = onOpenEditor; 136: $[22] = onRemoveImage; 137: $[23] = options.length; 138: $[24] = pastedContents; 139: $[25] = state; 140: $[26] = T0; 141: $[27] = T1; 142: $[28] = t7; 143: $[29] = t8; 144: $[30] = t9; 145: } else { 146: T0 = $[26]; 147: T1 = $[27]; 148: t7 = $[28]; 149: t8 = $[29]; 150: t9 = $[30]; 151: } 152: let t10; 153: if ($[31] !== T0 || $[32] !== t7 || $[33] !== t8) { 154: t10 = <T0 flexDirection={t7}>{t8}</T0>; 155: $[31] = T0; 156: $[32] = t7; 157: $[33] = t8; 158: $[34] = t10; 159: } else { 160: t10 = $[34]; 161: } 162: let t11; 163: if ($[35] !== onSubmit || $[36] !== state.isSubmitFocused || $[37] !== submitButtonText) { 164: t11 = submitButtonText && onSubmit && <Box marginTop={0} gap={1}>{state.isSubmitFocused ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}<Box marginLeft={3}><Text color={state.isSubmitFocused ? "suggestion" : undefined} bold={true}>{submitButtonText}</Text></Box></Box>; 165: $[35] = onSubmit; 166: $[36] = state.isSubmitFocused; 167: $[37] = submitButtonText; 168: $[38] = t11; 169: } else { 170: t11 = $[38]; 171: } 172: let t12; 173: if ($[39] !== T1 || $[40] !== t10 || $[41] !== t11 || $[42] !== t9) { 174: t12 = <T1 flexDirection={t9}>{t10}{t11}</T1>; 175: $[39] = T1; 176: $[40] = t10; 177: $[41] = t11; 178: $[42] = t9; 179: $[43] = t12; 180: } else { 181: t12 = $[43]; 182: } 183: return t12; 184: } 185: function _temp() {}

File: src/components/CustomSelect/use-multi-select-state.ts

typescript 1: import { useCallback, useState } from 'react' 2: import { isDeepStrictEqual } from 'util' 3: import { useRegisterOverlay } from '../../context/overlayContext.js' 4: import type { InputEvent } from '../../ink/events/input-event.js' 5: import { useInput } from '../../ink.js' 6: import { 7: normalizeFullWidthDigits, 8: normalizeFullWidthSpace, 9: } from '../../utils/stringUtils.js' 10: import type { OptionWithDescription } from './select.js' 11: import { useSelectNavigation } from './use-select-navigation.js' 12: export type UseMultiSelectStateProps<T> = { 13: isDisabled?: boolean 14: visibleOptionCount?: number 15: options: OptionWithDescription<T>[] 16: defaultValue?: T[] 17: onChange?: (values: T[]) => void 18: onCancel: () => void 19: onFocus?: (value: T) => void 20: focusValue?: T 21: submitButtonText?: string 22: onSubmit?: (values: T[]) => void 23: onDownFromLastItem?: () => void 24: onUpFromFirstItem?: () => void 25: initialFocusLast?: boolean 26: hideIndexes?: boolean 27: } 28: export type MultiSelectState<T> = { 29: focusedValue: T | undefined 30: visibleFromIndex: number 31: visibleToIndex: number 32: options: OptionWithDescription<T>[] 33: visibleOptions: Array<OptionWithDescription<T> & { index: number }> 34: isInInput: boolean 35: selectedValues: T[] 36: inputValues: Map<T, string> 37: isSubmitFocused: boolean 38: updateInputValue: (value: T, inputValue: string) => void 39: onCancel: () => void 40: } 41: export function useMultiSelectState<T>({ 42: isDisabled = false, 43: visibleOptionCount = 5, 44: options, 45: defaultValue = [], 46: onChange, 47: onCancel, 48: onFocus, 49: focusValue, 50: submitButtonText, 51: onSubmit, 52: onDownFromLastItem, 53: onUpFromFirstItem, 54: initialFocusLast, 55: hideIndexes = false, 56: }: UseMultiSelectStateProps<T>): MultiSelectState<T> { 57: const [selectedValues, setSelectedValues] = useState<T[]>(defaultValue) 58: const [isSubmitFocused, setIsSubmitFocused] = useState(false) 59: const [lastOptions, setLastOptions] = useState(options) 60: if (options !== lastOptions && !isDeepStrictEqual(options, lastOptions)) { 61: setSelectedValues(defaultValue) 62: setLastOptions(options) 63: } 64: const [inputValues, setInputValues] = useState<Map<T, string>>(() => { 65: const initialMap = new Map<T, string>() 66: options.forEach(option => { 67: if (option.type === 'input' && option.initialValue) { 68: initialMap.set(option.value, option.initialValue) 69: } 70: }) 71: return initialMap 72: }) 73: const updateSelectedValues = useCallback( 74: (values: T[] | ((prev: T[]) => T[])) => { 75: const newValues = 76: typeof values === 'function' ? values(selectedValues) : values 77: setSelectedValues(newValues) 78: onChange?.(newValues) 79: }, 80: [selectedValues, onChange], 81: ) 82: const navigation = useSelectNavigation<T>({ 83: visibleOptionCount, 84: options, 85: initialFocusValue: initialFocusLast 86: ? options[options.length - 1]?.value 87: : undefined, 88: onFocus, 89: focusValue, 90: }) 91: useRegisterOverlay('multi-select') 92: const updateInputValue = useCallback( 93: (value: T, inputValue: string) => { 94: setInputValues(prev => { 95: const next = new Map(prev) 96: next.set(value, inputValue) 97: return next 98: }) 99: const option = options.find(opt => opt.value === value) 100: if (option && option.type === 'input') { 101: option.onChange(inputValue) 102: } 103: updateSelectedValues(prev => { 104: if (inputValue) { 105: if (!prev.includes(value)) { 106: return [...prev, value] 107: } 108: return prev 109: } else { 110: return prev.filter(v => v !== value) 111: } 112: }) 113: }, 114: [options, updateSelectedValues], 115: ) 116: useInput( 117: (input, key, event: InputEvent) => { 118: const normalizedInput = normalizeFullWidthDigits(input) 119: const focusedOption = options.find( 120: opt => opt.value === navigation.focusedValue, 121: ) 122: const isInInput = focusedOption?.type === 'input' 123: if (isInInput) { 124: const isAllowedKey = 125: key.upArrow || 126: key.downArrow || 127: key.escape || 128: key.tab || 129: key.return || 130: (key.ctrl && (input === 'n' || input === 'p' || key.return)) 131: if (!isAllowedKey) return 132: } 133: const lastOptionValue = options[options.length - 1]?.value 134: if (key.tab && !key.shift) { 135: if ( 136: submitButtonText && 137: onSubmit && 138: navigation.focusedValue === lastOptionValue && 139: !isSubmitFocused 140: ) { 141: setIsSubmitFocused(true) 142: } else if (!isSubmitFocused) { 143: navigation.focusNextOption() 144: } 145: return 146: } 147: if (key.tab && key.shift) { 148: if (submitButtonText && onSubmit && isSubmitFocused) { 149: setIsSubmitFocused(false) 150: navigation.focusOption(lastOptionValue) 151: } else { 152: navigation.focusPreviousOption() 153: } 154: return 155: } 156: if ( 157: key.downArrow || 158: (key.ctrl && input === 'n') || 159: (!key.ctrl && !key.shift && input === 'j') 160: ) { 161: if (isSubmitFocused && onDownFromLastItem) { 162: onDownFromLastItem() 163: } else if ( 164: submitButtonText && 165: onSubmit && 166: navigation.focusedValue === lastOptionValue && 167: !isSubmitFocused 168: ) { 169: setIsSubmitFocused(true) 170: } else if ( 171: !submitButtonText && 172: onDownFromLastItem && 173: navigation.focusedValue === lastOptionValue 174: ) { 175: onDownFromLastItem() 176: } else if (!isSubmitFocused) { 177: navigation.focusNextOption() 178: } 179: return 180: } 181: if ( 182: key.upArrow || 183: (key.ctrl && input === 'p') || 184: (!key.ctrl && !key.shift && input === 'k') 185: ) { 186: if (submitButtonText && onSubmit && isSubmitFocused) { 187: setIsSubmitFocused(false) 188: navigation.focusOption(lastOptionValue) 189: } else if ( 190: onUpFromFirstItem && 191: navigation.focusedValue === options[0]?.value 192: ) { 193: onUpFromFirstItem() 194: } else { 195: navigation.focusPreviousOption() 196: } 197: return 198: } 199: if (key.pageDown) { 200: navigation.focusNextPage() 201: return 202: } 203: if (key.pageUp) { 204: navigation.focusPreviousPage() 205: return 206: } 207: if (key.return || normalizeFullWidthSpace(input) === ' ') { 208: if (key.ctrl && key.return && isInInput && onSubmit) { 209: onSubmit(selectedValues) 210: return 211: } 212: if (isSubmitFocused && onSubmit) { 213: onSubmit(selectedValues) 214: return 215: } 216: if (key.return && !submitButtonText && onSubmit) { 217: onSubmit(selectedValues) 218: return 219: } 220: if (navigation.focusedValue !== undefined) { 221: const newValues = selectedValues.includes(navigation.focusedValue) 222: ? selectedValues.filter(v => v !== navigation.focusedValue) 223: : [...selectedValues, navigation.focusedValue] 224: updateSelectedValues(newValues) 225: } 226: return 227: } 228: if (!hideIndexes && /^[0-9]+$/.test(normalizedInput)) { 229: const index = parseInt(normalizedInput) - 1 230: if (index >= 0 && index < options.length) { 231: const value = options[index]!.value 232: const newValues = selectedValues.includes(value) 233: ? selectedValues.filter(v => v !== value) 234: : [...selectedValues, value] 235: updateSelectedValues(newValues) 236: } 237: return 238: } 239: if (key.escape) { 240: onCancel() 241: event.stopImmediatePropagation() 242: } 243: }, 244: { isActive: !isDisabled }, 245: ) 246: return { 247: ...navigation, 248: selectedValues, 249: inputValues, 250: isSubmitFocused, 251: updateInputValue, 252: onCancel, 253: } 254: }

File: src/components/CustomSelect/use-select-input.ts

typescript 1: import { useMemo } from 'react' 2: import { useRegisterOverlay } from '../../context/overlayContext.js' 3: import type { InputEvent } from '../../ink/events/input-event.js' 4: import { useInput } from '../../ink.js' 5: import { useKeybindings } from '../../keybindings/useKeybinding.js' 6: import { 7: normalizeFullWidthDigits, 8: normalizeFullWidthSpace, 9: } from '../../utils/stringUtils.js' 10: import type { OptionWithDescription } from './select.js' 11: import type { SelectState } from './use-select-state.js' 12: export type UseSelectProps<T> = { 13: isDisabled?: boolean 14: readonly disableSelection?: boolean | 'numeric' 15: state: SelectState<T> 16: options: OptionWithDescription<T>[] 17: isMultiSelect?: boolean 18: onUpFromFirstItem?: () => void 19: onDownFromLastItem?: () => void 20: onInputModeToggle?: (value: T) => void 21: inputValues?: Map<T, string> 22: imagesSelected?: boolean 23: onEnterImageSelection?: () => boolean 24: } 25: export const useSelectInput = <T>({ 26: isDisabled = false, 27: disableSelection = false, 28: state, 29: options, 30: isMultiSelect = false, 31: onUpFromFirstItem, 32: onDownFromLastItem, 33: onInputModeToggle, 34: inputValues, 35: imagesSelected = false, 36: onEnterImageSelection, 37: }: UseSelectProps<T>) => { 38: useRegisterOverlay('select', !!state.onCancel) 39: const isInInput = useMemo(() => { 40: const focusedOption = options.find(opt => opt.value === state.focusedValue) 41: return focusedOption?.type === 'input' 42: }, [options, state.focusedValue]) 43: const keybindingHandlers = useMemo(() => { 44: const handlers: Record<string, () => void> = {} 45: if (!isInInput) { 46: handlers['select:next'] = () => { 47: if (onDownFromLastItem) { 48: const lastOption = options[options.length - 1] 49: if (lastOption && state.focusedValue === lastOption.value) { 50: onDownFromLastItem() 51: return 52: } 53: } 54: state.focusNextOption() 55: } 56: handlers['select:previous'] = () => { 57: if (onUpFromFirstItem && state.visibleFromIndex === 0) { 58: const firstOption = options[0] 59: if (firstOption && state.focusedValue === firstOption.value) { 60: onUpFromFirstItem() 61: return 62: } 63: } 64: state.focusPreviousOption() 65: } 66: handlers['select:accept'] = () => { 67: if (disableSelection === true) return 68: if (state.focusedValue === undefined) return 69: const focusedOption = options.find( 70: opt => opt.value === state.focusedValue, 71: ) 72: if (focusedOption?.disabled === true) return 73: state.selectFocusedOption?.() 74: state.onChange?.(state.focusedValue) 75: } 76: } 77: if (state.onCancel) { 78: handlers['select:cancel'] = () => { 79: state.onCancel!() 80: } 81: } 82: return handlers 83: }, [ 84: options, 85: state, 86: onDownFromLastItem, 87: onUpFromFirstItem, 88: isInInput, 89: disableSelection, 90: ]) 91: useKeybindings(keybindingHandlers, { 92: context: 'Select', 93: isActive: !isDisabled, 94: }) 95: useInput( 96: (input, key, event: InputEvent) => { 97: const normalizedInput = normalizeFullWidthDigits(input) 98: const focusedOption = options.find( 99: opt => opt.value === state.focusedValue, 100: ) 101: const currentIsInInput = focusedOption?.type === 'input' 102: if (key.tab && onInputModeToggle && state.focusedValue !== undefined) { 103: onInputModeToggle(state.focusedValue) 104: return 105: } 106: if (currentIsInInput) { 107: if (imagesSelected) return 108: if (key.downArrow && onEnterImageSelection?.()) { 109: event.stopImmediatePropagation() 110: return 111: } 112: if (key.downArrow || (key.ctrl && input === 'n')) { 113: if (onDownFromLastItem) { 114: const lastOption = options[options.length - 1] 115: if (lastOption && state.focusedValue === lastOption.value) { 116: onDownFromLastItem() 117: event.stopImmediatePropagation() 118: return 119: } 120: } 121: state.focusNextOption() 122: event.stopImmediatePropagation() 123: return 124: } 125: if (key.upArrow || (key.ctrl && input === 'p')) { 126: if (onUpFromFirstItem && state.visibleFromIndex === 0) { 127: const firstOption = options[0] 128: if (firstOption && state.focusedValue === firstOption.value) { 129: onUpFromFirstItem() 130: event.stopImmediatePropagation() 131: return 132: } 133: } 134: state.focusPreviousOption() 135: event.stopImmediatePropagation() 136: return 137: } 138: return 139: } 140: if (key.pageDown) { 141: state.focusNextPage() 142: } 143: if (key.pageUp) { 144: state.focusPreviousPage() 145: } 146: if (disableSelection !== true) { 147: if ( 148: isMultiSelect && 149: normalizeFullWidthSpace(input) === ' ' && 150: state.focusedValue !== undefined 151: ) { 152: const isFocusedOptionDisabled = focusedOption?.disabled === true 153: if (!isFocusedOptionDisabled) { 154: state.selectFocusedOption?.() 155: state.onChange?.(state.focusedValue) 156: } 157: } 158: if ( 159: disableSelection !== 'numeric' && 160: /^[0-9]+$/.test(normalizedInput) 161: ) { 162: const index = parseInt(normalizedInput) - 1 163: if (index >= 0 && index < state.options.length) { 164: const selectedOption = state.options[index]! 165: if (selectedOption.disabled === true) { 166: return 167: } 168: if (selectedOption.type === 'input') { 169: const currentValue = inputValues?.get(selectedOption.value) ?? '' 170: if (currentValue.trim()) { 171: state.onChange?.(selectedOption.value) 172: return 173: } 174: if (selectedOption.allowEmptySubmitToCancel) { 175: state.onChange?.(selectedOption.value) 176: return 177: } 178: state.focusOption(selectedOption.value) 179: return 180: } 181: state.onChange?.(selectedOption.value) 182: return 183: } 184: } 185: } 186: }, 187: { isActive: !isDisabled }, 188: ) 189: }

File: src/components/CustomSelect/use-select-navigation.ts

typescript 1: import { 2: useCallback, 3: useEffect, 4: useMemo, 5: useReducer, 6: useRef, 7: useState, 8: } from 'react' 9: import { isDeepStrictEqual } from 'util' 10: import OptionMap from './option-map.js' 11: import type { OptionWithDescription } from './select.js' 12: type State<T> = { 13: optionMap: OptionMap<T> 14: visibleOptionCount: number 15: focusedValue: T | undefined 16: visibleFromIndex: number 17: visibleToIndex: number 18: } 19: type Action<T> = 20: | FocusNextOptionAction 21: | FocusPreviousOptionAction 22: | FocusNextPageAction 23: | FocusPreviousPageAction 24: | SetFocusAction<T> 25: | ResetAction<T> 26: type SetFocusAction<T> = { 27: type: 'set-focus' 28: value: T 29: } 30: type FocusNextOptionAction = { 31: type: 'focus-next-option' 32: } 33: type FocusPreviousOptionAction = { 34: type: 'focus-previous-option' 35: } 36: type FocusNextPageAction = { 37: type: 'focus-next-page' 38: } 39: type FocusPreviousPageAction = { 40: type: 'focus-previous-page' 41: } 42: type ResetAction<T> = { 43: type: 'reset' 44: state: State<T> 45: } 46: const reducer = <T>(state: State<T>, action: Action<T>): State<T> => { 47: switch (action.type) { 48: case 'focus-next-option': { 49: if (state.focusedValue === undefined) { 50: return state 51: } 52: const item = state.optionMap.get(state.focusedValue) 53: if (!item) { 54: return state 55: } 56: const next = item.next || state.optionMap.first 57: if (!next) { 58: return state 59: } 60: if (!item.next && next === state.optionMap.first) { 61: return { 62: ...state, 63: focusedValue: next.value, 64: visibleFromIndex: 0, 65: visibleToIndex: state.visibleOptionCount, 66: } 67: } 68: const needsToScroll = next.index >= state.visibleToIndex 69: if (!needsToScroll) { 70: return { 71: ...state, 72: focusedValue: next.value, 73: } 74: } 75: const nextVisibleToIndex = Math.min( 76: state.optionMap.size, 77: state.visibleToIndex + 1, 78: ) 79: const nextVisibleFromIndex = nextVisibleToIndex - state.visibleOptionCount 80: return { 81: ...state, 82: focusedValue: next.value, 83: visibleFromIndex: nextVisibleFromIndex, 84: visibleToIndex: nextVisibleToIndex, 85: } 86: } 87: case 'focus-previous-option': { 88: if (state.focusedValue === undefined) { 89: return state 90: } 91: const item = state.optionMap.get(state.focusedValue) 92: if (!item) { 93: return state 94: } 95: const previous = item.previous || state.optionMap.last 96: if (!previous) { 97: return state 98: } 99: if (!item.previous && previous === state.optionMap.last) { 100: const nextVisibleToIndex = state.optionMap.size 101: const nextVisibleFromIndex = Math.max( 102: 0, 103: nextVisibleToIndex - state.visibleOptionCount, 104: ) 105: return { 106: ...state, 107: focusedValue: previous.value, 108: visibleFromIndex: nextVisibleFromIndex, 109: visibleToIndex: nextVisibleToIndex, 110: } 111: } 112: const needsToScroll = previous.index <= state.visibleFromIndex 113: if (!needsToScroll) { 114: return { 115: ...state, 116: focusedValue: previous.value, 117: } 118: } 119: const nextVisibleFromIndex = Math.max(0, state.visibleFromIndex - 1) 120: const nextVisibleToIndex = nextVisibleFromIndex + state.visibleOptionCount 121: return { 122: ...state, 123: focusedValue: previous.value, 124: visibleFromIndex: nextVisibleFromIndex, 125: visibleToIndex: nextVisibleToIndex, 126: } 127: } 128: case 'focus-next-page': { 129: if (state.focusedValue === undefined) { 130: return state 131: } 132: const item = state.optionMap.get(state.focusedValue) 133: if (!item) { 134: return state 135: } 136: const targetIndex = Math.min( 137: state.optionMap.size - 1, 138: item.index + state.visibleOptionCount, 139: ) 140: let targetItem = state.optionMap.first 141: while (targetItem && targetItem.index < targetIndex) { 142: if (targetItem.next) { 143: targetItem = targetItem.next 144: } else { 145: break 146: } 147: } 148: if (!targetItem) { 149: return state 150: } 151: const nextVisibleToIndex = Math.min( 152: state.optionMap.size, 153: targetItem.index + 1, 154: ) 155: const nextVisibleFromIndex = Math.max( 156: 0, 157: nextVisibleToIndex - state.visibleOptionCount, 158: ) 159: return { 160: ...state, 161: focusedValue: targetItem.value, 162: visibleFromIndex: nextVisibleFromIndex, 163: visibleToIndex: nextVisibleToIndex, 164: } 165: } 166: case 'focus-previous-page': { 167: if (state.focusedValue === undefined) { 168: return state 169: } 170: const item = state.optionMap.get(state.focusedValue) 171: if (!item) { 172: return state 173: } 174: const targetIndex = Math.max(0, item.index - state.visibleOptionCount) 175: let targetItem = state.optionMap.first 176: while (targetItem && targetItem.index < targetIndex) { 177: if (targetItem.next) { 178: targetItem = targetItem.next 179: } else { 180: break 181: } 182: } 183: if (!targetItem) { 184: return state 185: } 186: const nextVisibleFromIndex = Math.max(0, targetItem.index) 187: const nextVisibleToIndex = Math.min( 188: state.optionMap.size, 189: nextVisibleFromIndex + state.visibleOptionCount, 190: ) 191: return { 192: ...state, 193: focusedValue: targetItem.value, 194: visibleFromIndex: nextVisibleFromIndex, 195: visibleToIndex: nextVisibleToIndex, 196: } 197: } 198: case 'reset': { 199: return action.state 200: } 201: case 'set-focus': { 202: if (state.focusedValue === action.value) { 203: return state 204: } 205: const item = state.optionMap.get(action.value) 206: if (!item) { 207: return state 208: } 209: if ( 210: item.index >= state.visibleFromIndex && 211: item.index < state.visibleToIndex 212: ) { 213: return { 214: ...state, 215: focusedValue: action.value, 216: } 217: } 218: let nextVisibleFromIndex: number 219: let nextVisibleToIndex: number 220: if (item.index < state.visibleFromIndex) { 221: nextVisibleFromIndex = item.index 222: nextVisibleToIndex = Math.min( 223: state.optionMap.size, 224: nextVisibleFromIndex + state.visibleOptionCount, 225: ) 226: } else { 227: nextVisibleToIndex = Math.min(state.optionMap.size, item.index + 1) 228: nextVisibleFromIndex = Math.max( 229: 0, 230: nextVisibleToIndex - state.visibleOptionCount, 231: ) 232: } 233: return { 234: ...state, 235: focusedValue: action.value, 236: visibleFromIndex: nextVisibleFromIndex, 237: visibleToIndex: nextVisibleToIndex, 238: } 239: } 240: } 241: } 242: export type UseSelectNavigationProps<T> = { 243: visibleOptionCount?: number 244: options: OptionWithDescription<T>[] 245: initialFocusValue?: T 246: onFocus?: (value: T) => void 247: focusValue?: T 248: } 249: export type SelectNavigation<T> = { 250: focusedValue: T | undefined 251: focusedIndex: number 252: visibleFromIndex: number 253: visibleToIndex: number 254: options: OptionWithDescription<T>[] 255: visibleOptions: Array<OptionWithDescription<T> & { index: number }> 256: isInInput: boolean 257: focusNextOption: () => void 258: focusPreviousOption: () => void 259: focusNextPage: () => void 260: focusPreviousPage: () => void 261: focusOption: (value: T | undefined) => void 262: } 263: const createDefaultState = <T>({ 264: visibleOptionCount: customVisibleOptionCount, 265: options, 266: initialFocusValue, 267: currentViewport, 268: }: Pick<UseSelectNavigationProps<T>, 'visibleOptionCount' | 'options'> & { 269: initialFocusValue?: T 270: currentViewport?: { visibleFromIndex: number; visibleToIndex: number } 271: }): State<T> => { 272: const visibleOptionCount = 273: typeof customVisibleOptionCount === 'number' 274: ? Math.min(customVisibleOptionCount, options.length) 275: : options.length 276: const optionMap = new OptionMap<T>(options) 277: const focusedItem = 278: initialFocusValue !== undefined && optionMap.get(initialFocusValue) 279: const focusedValue = focusedItem ? initialFocusValue : optionMap.first?.value 280: let visibleFromIndex = 0 281: let visibleToIndex = visibleOptionCount 282: if (focusedItem) { 283: const focusedIndex = focusedItem.index 284: if (currentViewport) { 285: if ( 286: focusedIndex >= currentViewport.visibleFromIndex && 287: focusedIndex < currentViewport.visibleToIndex 288: ) { 289: visibleFromIndex = currentViewport.visibleFromIndex 290: visibleToIndex = Math.min( 291: optionMap.size, 292: currentViewport.visibleToIndex, 293: ) 294: } else { 295: if (focusedIndex < currentViewport.visibleFromIndex) { 296: visibleFromIndex = focusedIndex 297: visibleToIndex = Math.min( 298: optionMap.size, 299: visibleFromIndex + visibleOptionCount, 300: ) 301: } else { 302: visibleToIndex = Math.min(optionMap.size, focusedIndex + 1) 303: visibleFromIndex = Math.max(0, visibleToIndex - visibleOptionCount) 304: } 305: } 306: } else if (focusedIndex >= visibleOptionCount) { 307: visibleToIndex = Math.min(optionMap.size, focusedIndex + 1) 308: visibleFromIndex = Math.max(0, visibleToIndex - visibleOptionCount) 309: } 310: visibleFromIndex = Math.max( 311: 0, 312: Math.min(visibleFromIndex, optionMap.size - 1), 313: ) 314: visibleToIndex = Math.min( 315: optionMap.size, 316: Math.max(visibleOptionCount, visibleToIndex), 317: ) 318: } 319: return { 320: optionMap, 321: visibleOptionCount, 322: focusedValue, 323: visibleFromIndex, 324: visibleToIndex, 325: } 326: } 327: export function useSelectNavigation<T>({ 328: visibleOptionCount = 5, 329: options, 330: initialFocusValue, 331: onFocus, 332: focusValue, 333: }: UseSelectNavigationProps<T>): SelectNavigation<T> { 334: const [state, dispatch] = useReducer( 335: reducer<T>, 336: { 337: visibleOptionCount, 338: options, 339: initialFocusValue: focusValue || initialFocusValue, 340: } as Parameters<typeof createDefaultState<T>>[0], 341: createDefaultState<T>, 342: ) 343: const onFocusRef = useRef(onFocus) 344: onFocusRef.current = onFocus 345: const [lastOptions, setLastOptions] = useState(options) 346: if (options !== lastOptions && !isDeepStrictEqual(options, lastOptions)) { 347: dispatch({ 348: type: 'reset', 349: state: createDefaultState({ 350: visibleOptionCount, 351: options, 352: initialFocusValue: 353: focusValue ?? state.focusedValue ?? initialFocusValue, 354: currentViewport: { 355: visibleFromIndex: state.visibleFromIndex, 356: visibleToIndex: state.visibleToIndex, 357: }, 358: }), 359: }) 360: setLastOptions(options) 361: } 362: const focusNextOption = useCallback(() => { 363: dispatch({ 364: type: 'focus-next-option', 365: }) 366: }, []) 367: const focusPreviousOption = useCallback(() => { 368: dispatch({ 369: type: 'focus-previous-option', 370: }) 371: }, []) 372: const focusNextPage = useCallback(() => { 373: dispatch({ 374: type: 'focus-next-page', 375: }) 376: }, []) 377: const focusPreviousPage = useCallback(() => { 378: dispatch({ 379: type: 'focus-previous-page', 380: }) 381: }, []) 382: const focusOption = useCallback((value: T | undefined) => { 383: if (value !== undefined) { 384: dispatch({ 385: type: 'set-focus', 386: value, 387: }) 388: } 389: }, []) 390: const visibleOptions = useMemo(() => { 391: return options 392: .map((option, index) => ({ 393: ...option, 394: index, 395: })) 396: .slice(state.visibleFromIndex, state.visibleToIndex) 397: }, [options, state.visibleFromIndex, state.visibleToIndex]) 398: const validatedFocusedValue = useMemo(() => { 399: if (state.focusedValue === undefined) { 400: return undefined 401: } 402: const exists = options.some(opt => opt.value === state.focusedValue) 403: if (exists) { 404: return state.focusedValue 405: } 406: return options[0]?.value 407: }, [state.focusedValue, options]) 408: const isInInput = useMemo(() => { 409: const focusedOption = options.find( 410: opt => opt.value === validatedFocusedValue, 411: ) 412: return focusedOption?.type === 'input' 413: }, [validatedFocusedValue, options]) 414: useEffect(() => { 415: if (validatedFocusedValue !== undefined) { 416: onFocusRef.current?.(validatedFocusedValue) 417: } 418: }, [validatedFocusedValue]) 419: useEffect(() => { 420: if (focusValue !== undefined) { 421: dispatch({ 422: type: 'set-focus', 423: value: focusValue, 424: }) 425: } 426: }, [focusValue]) 427: const focusedIndex = useMemo(() => { 428: if (validatedFocusedValue === undefined) { 429: return 0 430: } 431: const index = options.findIndex(opt => opt.value === validatedFocusedValue) 432: return index >= 0 ? index + 1 : 0 433: }, [validatedFocusedValue, options]) 434: return { 435: focusedValue: validatedFocusedValue, 436: focusedIndex, 437: visibleFromIndex: state.visibleFromIndex, 438: visibleToIndex: state.visibleToIndex, 439: visibleOptions, 440: isInInput: isInInput ?? false, 441: focusNextOption, 442: focusPreviousOption, 443: focusNextPage, 444: focusPreviousPage, 445: focusOption, 446: options, 447: } 448: }

File: src/components/CustomSelect/use-select-state.ts

typescript 1: import { useCallback, useState } from 'react' 2: import type { OptionWithDescription } from './select.js' 3: import { useSelectNavigation } from './use-select-navigation.js' 4: export type UseSelectStateProps<T> = { 5: visibleOptionCount?: number 6: options: OptionWithDescription<T>[] 7: defaultValue?: T 8: onChange?: (value: T) => void 9: onCancel?: () => void 10: onFocus?: (value: T) => void 11: focusValue?: T 12: } 13: export type SelectState<T> = { 14: focusedValue: T | undefined 15: focusedIndex: number 16: visibleFromIndex: number 17: visibleToIndex: number 18: value: T | undefined 19: options: OptionWithDescription<T>[] 20: visibleOptions: Array<OptionWithDescription<T> & { index: number }> 21: isInInput: boolean 22: focusNextOption: () => void 23: focusPreviousOption: () => void 24: focusNextPage: () => void 25: focusPreviousPage: () => void 26: focusOption: (value: T | undefined) => void 27: selectFocusedOption: () => void 28: onChange?: (value: T) => void 29: onCancel?: () => void 30: } 31: export function useSelectState<T>({ 32: visibleOptionCount = 5, 33: options, 34: defaultValue, 35: onChange, 36: onCancel, 37: onFocus, 38: focusValue, 39: }: UseSelectStateProps<T>): SelectState<T> { 40: const [value, setValue] = useState<T | undefined>(defaultValue) 41: const navigation = useSelectNavigation<T>({ 42: visibleOptionCount, 43: options, 44: initialFocusValue: undefined, 45: onFocus, 46: focusValue, 47: }) 48: const selectFocusedOption = useCallback(() => { 49: setValue(navigation.focusedValue) 50: }, [navigation.focusedValue]) 51: return { 52: ...navigation, 53: value, 54: selectFocusedOption, 55: onChange, 56: onCancel, 57: } 58: }

File: src/components/design-system/Byline.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { Children, isValidElement } from 'react'; 3: import { Text } from '../../ink.js'; 4: type Props = { 5: children: React.ReactNode; 6: }; 7: export function Byline(t0) { 8: const $ = _c(5); 9: const { 10: children 11: } = t0; 12: let t1; 13: let t2; 14: if ($[0] !== children) { 15: t2 = Symbol.for("react.early_return_sentinel"); 16: bb0: { 17: const validChildren = Children.toArray(children); 18: if (validChildren.length === 0) { 19: t2 = null; 20: break bb0; 21: } 22: t1 = validChildren.map(_temp); 23: } 24: $[0] = children; 25: $[1] = t1; 26: $[2] = t2; 27: } else { 28: t1 = $[1]; 29: t2 = $[2]; 30: } 31: if (t2 !== Symbol.for("react.early_return_sentinel")) { 32: return t2; 33: } 34: let t3; 35: if ($[3] !== t1) { 36: t3 = <>{t1}</>; 37: $[3] = t1; 38: $[4] = t3; 39: } else { 40: t3 = $[4]; 41: } 42: return t3; 43: } 44: function _temp(child, index) { 45: return <React.Fragment key={isValidElement(child) ? child.key ?? index : index}>{index > 0 && <Text dimColor={true}> · </Text>}{child}</React.Fragment>; 46: }

File: src/components/design-system/color.ts

typescript 1: import { type ColorType, colorize } from '../../ink/colorize.js' 2: import type { Color } from '../../ink/styles.js' 3: import { getTheme, type Theme, type ThemeName } from '../../utils/theme.js' 4: export function color( 5: c: keyof Theme | Color | undefined, 6: theme: ThemeName, 7: type: ColorType = 'foreground', 8: ): (text: string) => string { 9: return text => { 10: if (!c) { 11: return text 12: } 13: if ( 14: c.startsWith('rgb(') || 15: c.startsWith('#') || 16: c.startsWith('ansi256(') || 17: c.startsWith('ansi:') 18: ) { 19: return colorize(text, c, type) 20: } 21: return colorize(text, getTheme(theme)[c as keyof Theme], type) 22: } 23: }

File: src/components/design-system/Dialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { type ExitState, useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 4: import { Box, Text } from '../../ink.js'; 5: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 6: import type { Theme } from '../../utils/theme.js'; 7: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 8: import { Byline } from './Byline.js'; 9: import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'; 10: import { Pane } from './Pane.js'; 11: type DialogProps = { 12: title: React.ReactNode; 13: subtitle?: React.ReactNode; 14: children: React.ReactNode; 15: onCancel: () => void; 16: color?: keyof Theme; 17: hideInputGuide?: boolean; 18: hideBorder?: boolean; 19: inputGuide?: (exitState: ExitState) => React.ReactNode; 20: isCancelActive?: boolean; 21: }; 22: export function Dialog(t0) { 23: const $ = _c(27); 24: const { 25: title, 26: subtitle, 27: children, 28: onCancel, 29: color: t1, 30: hideInputGuide, 31: hideBorder, 32: inputGuide, 33: isCancelActive: t2 34: } = t0; 35: const color = t1 === undefined ? "permission" : t1; 36: const isCancelActive = t2 === undefined ? true : t2; 37: const exitState = useExitOnCtrlCDWithKeybindings(undefined, undefined, isCancelActive); 38: let t3; 39: if ($[0] !== isCancelActive) { 40: t3 = { 41: context: "Confirmation", 42: isActive: isCancelActive 43: }; 44: $[0] = isCancelActive; 45: $[1] = t3; 46: } else { 47: t3 = $[1]; 48: } 49: useKeybinding("confirm:no", onCancel, t3); 50: let t4; 51: if ($[2] !== exitState.keyName || $[3] !== exitState.pending) { 52: t4 = exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut="Enter" action="confirm" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>; 53: $[2] = exitState.keyName; 54: $[3] = exitState.pending; 55: $[4] = t4; 56: } else { 57: t4 = $[4]; 58: } 59: const defaultInputGuide = t4; 60: let t5; 61: if ($[5] !== color || $[6] !== title) { 62: t5 = <Text bold={true} color={color}>{title}</Text>; 63: $[5] = color; 64: $[6] = title; 65: $[7] = t5; 66: } else { 67: t5 = $[7]; 68: } 69: let t6; 70: if ($[8] !== subtitle) { 71: t6 = subtitle && <Text dimColor={true}>{subtitle}</Text>; 72: $[8] = subtitle; 73: $[9] = t6; 74: } else { 75: t6 = $[9]; 76: } 77: let t7; 78: if ($[10] !== t5 || $[11] !== t6) { 79: t7 = <Box flexDirection="column">{t5}{t6}</Box>; 80: $[10] = t5; 81: $[11] = t6; 82: $[12] = t7; 83: } else { 84: t7 = $[12]; 85: } 86: let t8; 87: if ($[13] !== children || $[14] !== t7) { 88: t8 = <Box flexDirection="column" gap={1}>{t7}{children}</Box>; 89: $[13] = children; 90: $[14] = t7; 91: $[15] = t8; 92: } else { 93: t8 = $[15]; 94: } 95: let t9; 96: if ($[16] !== defaultInputGuide || $[17] !== exitState || $[18] !== hideInputGuide || $[19] !== inputGuide) { 97: t9 = !hideInputGuide && <Box marginTop={1}><Text dimColor={true} italic={true}>{inputGuide ? inputGuide(exitState) : defaultInputGuide}</Text></Box>; 98: $[16] = defaultInputGuide; 99: $[17] = exitState; 100: $[18] = hideInputGuide; 101: $[19] = inputGuide; 102: $[20] = t9; 103: } else { 104: t9 = $[20]; 105: } 106: let t10; 107: if ($[21] !== t8 || $[22] !== t9) { 108: t10 = <>{t8}{t9}</>; 109: $[21] = t8; 110: $[22] = t9; 111: $[23] = t10; 112: } else { 113: t10 = $[23]; 114: } 115: const content = t10; 116: if (hideBorder) { 117: return content; 118: } 119: let t11; 120: if ($[24] !== color || $[25] !== content) { 121: t11 = <Pane color={color}>{content}</Pane>; 122: $[24] = color; 123: $[25] = content; 124: $[26] = t11; 125: } else { 126: t11 = $[26]; 127: } 128: return t11; 129: }

File: src/components/design-system/Divider.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 4: import { stringWidth } from '../../ink/stringWidth.js'; 5: import { Ansi, Text } from '../../ink.js'; 6: import type { Theme } from '../../utils/theme.js'; 7: type DividerProps = { 8: width?: number; 9: color?: keyof Theme; 10: char?: string; 11: padding?: number; 12: title?: string; 13: }; 14: export function Divider(t0) { 15: const $ = _c(21); 16: const { 17: width, 18: color, 19: char: t1, 20: padding: t2, 21: title 22: } = t0; 23: const char = t1 === undefined ? "\u2500" : t1; 24: const padding = t2 === undefined ? 0 : t2; 25: const { 26: columns: terminalWidth 27: } = useTerminalSize(); 28: const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding); 29: if (title) { 30: const titleWidth = stringWidth(title) + 2; 31: const sideWidth = Math.max(0, effectiveWidth - titleWidth); 32: const leftWidth = Math.floor(sideWidth / 2); 33: const rightWidth = sideWidth - leftWidth; 34: const t3 = !color; 35: let t4; 36: if ($[0] !== char || $[1] !== leftWidth) { 37: t4 = char.repeat(leftWidth); 38: $[0] = char; 39: $[1] = leftWidth; 40: $[2] = t4; 41: } else { 42: t4 = $[2]; 43: } 44: let t5; 45: if ($[3] !== title) { 46: t5 = <Text dimColor={true}><Ansi>{title}</Ansi></Text>; 47: $[3] = title; 48: $[4] = t5; 49: } else { 50: t5 = $[4]; 51: } 52: let t6; 53: if ($[5] !== char || $[6] !== rightWidth) { 54: t6 = char.repeat(rightWidth); 55: $[5] = char; 56: $[6] = rightWidth; 57: $[7] = t6; 58: } else { 59: t6 = $[7]; 60: } 61: let t7; 62: if ($[8] !== color || $[9] !== t3 || $[10] !== t4 || $[11] !== t5 || $[12] !== t6) { 63: t7 = <Text color={color} dimColor={t3}>{t4}{" "}{t5}{" "}{t6}</Text>; 64: $[8] = color; 65: $[9] = t3; 66: $[10] = t4; 67: $[11] = t5; 68: $[12] = t6; 69: $[13] = t7; 70: } else { 71: t7 = $[13]; 72: } 73: return t7; 74: } 75: const t3 = !color; 76: let t4; 77: if ($[14] !== char || $[15] !== effectiveWidth) { 78: t4 = char.repeat(effectiveWidth); 79: $[14] = char; 80: $[15] = effectiveWidth; 81: $[16] = t4; 82: } else { 83: t4 = $[16]; 84: } 85: let t5; 86: if ($[17] !== color || $[18] !== t3 || $[19] !== t4) { 87: t5 = <Text color={color} dimColor={t3}>{t4}</Text>; 88: $[17] = color; 89: $[18] = t3; 90: $[19] = t4; 91: $[20] = t5; 92: } else { 93: t5 = $[20]; 94: } 95: return t5; 96: }

File: src/components/design-system/FuzzyPicker.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useEffect, useState } from 'react'; 4: import { useSearchInput } from '../../hooks/useSearchInput.js'; 5: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 6: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 7: import { clamp } from '../../ink/layout/geometry.js'; 8: import { Box, Text, useTerminalFocus } from '../../ink.js'; 9: import { SearchBox } from '../SearchBox.js'; 10: import { Byline } from './Byline.js'; 11: import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'; 12: import { ListItem } from './ListItem.js'; 13: import { Pane } from './Pane.js'; 14: type PickerAction<T> = { 15: action: string; 16: handler: (item: T) => void; 17: }; 18: type Props<T> = { 19: title: string; 20: placeholder?: string; 21: initialQuery?: string; 22: items: readonly T[]; 23: getKey: (item: T) => string; 24: renderItem: (item: T, isFocused: boolean) => React.ReactNode; 25: renderPreview?: (item: T) => React.ReactNode; 26: previewPosition?: 'bottom' | 'right'; 27: visibleCount?: number; 28: direction?: 'down' | 'up'; 29: onQueryChange: (query: string) => void; 30: onSelect: (item: T) => void; 31: onTab?: PickerAction<T>; 32: onShiftTab?: PickerAction<T>; 33: onFocus?: (item: T | undefined) => void; 34: onCancel: () => void; 35: emptyMessage?: string | ((query: string) => string); 36: matchLabel?: string; 37: selectAction?: string; 38: extraHints?: React.ReactNode; 39: }; 40: const DEFAULT_VISIBLE = 8; 41: const CHROME_ROWS = 10; 42: const MIN_VISIBLE = 2; 43: export function FuzzyPicker<T>({ 44: title, 45: placeholder = 'Type to search…', 46: initialQuery, 47: items, 48: getKey, 49: renderItem, 50: renderPreview, 51: previewPosition = 'bottom', 52: visibleCount: requestedVisible = DEFAULT_VISIBLE, 53: direction = 'down', 54: onQueryChange, 55: onSelect, 56: onTab, 57: onShiftTab, 58: onFocus, 59: onCancel, 60: emptyMessage = 'No results', 61: matchLabel, 62: selectAction = 'select', 63: extraHints 64: }: Props<T>): React.ReactNode { 65: const isTerminalFocused = useTerminalFocus(); 66: const { 67: rows, 68: columns 69: } = useTerminalSize(); 70: const [focusedIndex, setFocusedIndex] = useState(0); 71: const visibleCount = Math.max(MIN_VISIBLE, Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0))); 72: const compact = columns < 120; 73: const step = (delta: 1 | -1) => { 74: setFocusedIndex(i => clamp(i + delta, 0, items.length - 1)); 75: }; 76: const { 77: query, 78: cursorOffset 79: } = useSearchInput({ 80: isActive: true, 81: onExit: () => {}, 82: onCancel, 83: initialQuery, 84: backspaceExitsOnEmpty: false 85: }); 86: const handleKeyDown = (e: KeyboardEvent) => { 87: if (e.key === 'up' || e.ctrl && e.key === 'p') { 88: e.preventDefault(); 89: e.stopImmediatePropagation(); 90: step(direction === 'up' ? 1 : -1); 91: return; 92: } 93: if (e.key === 'down' || e.ctrl && e.key === 'n') { 94: e.preventDefault(); 95: e.stopImmediatePropagation(); 96: step(direction === 'up' ? -1 : 1); 97: return; 98: } 99: if (e.key === 'return') { 100: e.preventDefault(); 101: e.stopImmediatePropagation(); 102: const selected = items[focusedIndex]; 103: if (selected) onSelect(selected); 104: return; 105: } 106: if (e.key === 'tab') { 107: e.preventDefault(); 108: e.stopImmediatePropagation(); 109: const selected = items[focusedIndex]; 110: if (!selected) return; 111: const tabAction = e.shift ? onShiftTab ?? onTab : onTab; 112: if (tabAction) { 113: tabAction.handler(selected); 114: } else { 115: onSelect(selected); 116: } 117: } 118: }; 119: useEffect(() => { 120: onQueryChange(query); 121: setFocusedIndex(0); 122: }, [query]); 123: useEffect(() => { 124: setFocusedIndex(i => clamp(i, 0, items.length - 1)); 125: }, [items.length]); 126: const focused = items[focusedIndex]; 127: useEffect(() => { 128: onFocus?.(focused); 129: }, [focused]); 130: const windowStart = clamp(focusedIndex - visibleCount + 1, 0, items.length - visibleCount); 131: const visible = items.slice(windowStart, windowStart + visibleCount); 132: const emptyText = typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage; 133: const searchBox = <SearchBox query={query} cursorOffset={cursorOffset} placeholder={placeholder} isFocused isTerminalFocused={isTerminalFocused} />; 134: const listBlock = <List visible={visible} windowStart={windowStart} visibleCount={visibleCount} total={items.length} focusedIndex={focusedIndex} direction={direction} getKey={getKey} renderItem={renderItem} emptyText={emptyText} />; 135: const preview = renderPreview && focused ? <Box flexDirection="column" flexGrow={1}> 136: {renderPreview(focused)} 137: </Box> : null; 138: const listGroup = renderPreview && previewPosition === 'right' ? <Box flexDirection="row" gap={2} height={visibleCount + (matchLabel ? 1 : 0)}> 139: <Box flexDirection="column" flexShrink={0}> 140: {listBlock} 141: {matchLabel && <Text dimColor>{matchLabel}</Text>} 142: </Box> 143: {preview ?? <Box flexGrow={1} />} 144: </Box> : 145: <Box flexDirection="column"> 146: {listBlock} 147: {matchLabel && <Text dimColor>{matchLabel}</Text>} 148: {preview} 149: </Box>; 150: const inputAbove = direction !== 'up'; 151: return <Pane color="permission"> 152: <Box flexDirection="column" gap={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}> 153: <Text bold color="permission"> 154: {title} 155: </Text> 156: {inputAbove && searchBox} 157: {listGroup} 158: {!inputAbove && searchBox} 159: <Text dimColor> 160: <Byline> 161: <KeyboardShortcutHint shortcut="↑/↓" action={compact ? 'nav' : 'navigate'} /> 162: <KeyboardShortcutHint shortcut="Enter" action={compact ? firstWord(selectAction) : selectAction} /> 163: {onTab && <KeyboardShortcutHint shortcut="Tab" action={onTab.action} />} 164: {onShiftTab && !compact && <KeyboardShortcutHint shortcut="shift+tab" action={onShiftTab.action} />} 165: <KeyboardShortcutHint shortcut="Esc" action="cancel" /> 166: {extraHints} 167: </Byline> 168: </Text> 169: </Box> 170: </Pane>; 171: } 172: type ListProps<T> = Pick<Props<T>, 'visibleCount' | 'direction' | 'getKey' | 'renderItem'> & { 173: visible: readonly T[]; 174: windowStart: number; 175: total: number; 176: focusedIndex: number; 177: emptyText: string; 178: }; 179: function List(t0) { 180: const $ = _c(27); 181: const { 182: visible, 183: windowStart, 184: visibleCount, 185: total, 186: focusedIndex, 187: direction, 188: getKey, 189: renderItem, 190: emptyText 191: } = t0; 192: if (visible.length === 0) { 193: let t1; 194: if ($[0] !== emptyText) { 195: t1 = <Text dimColor={true}>{emptyText}</Text>; 196: $[0] = emptyText; 197: $[1] = t1; 198: } else { 199: t1 = $[1]; 200: } 201: let t2; 202: if ($[2] !== t1 || $[3] !== visibleCount) { 203: t2 = <Box height={visibleCount} flexShrink={0}>{t1}</Box>; 204: $[2] = t1; 205: $[3] = visibleCount; 206: $[4] = t2; 207: } else { 208: t2 = $[4]; 209: } 210: return t2; 211: } 212: let t1; 213: if ($[5] !== direction || $[6] !== focusedIndex || $[7] !== getKey || $[8] !== renderItem || $[9] !== total || $[10] !== visible || $[11] !== visibleCount || $[12] !== windowStart) { 214: let t2; 215: if ($[14] !== direction || $[15] !== focusedIndex || $[16] !== getKey || $[17] !== renderItem || $[18] !== total || $[19] !== visible.length || $[20] !== visibleCount || $[21] !== windowStart) { 216: t2 = (item, i) => { 217: const actualIndex = windowStart + i; 218: const isFocused = actualIndex === focusedIndex; 219: const atLowEdge = i === 0 && windowStart > 0; 220: const atHighEdge = i === visible.length - 1 && windowStart + visibleCount < total; 221: return <ListItem key={getKey(item)} isFocused={isFocused} showScrollUp={direction === "up" ? atHighEdge : atLowEdge} showScrollDown={direction === "up" ? atLowEdge : atHighEdge} styled={false}>{renderItem(item, isFocused)}</ListItem>; 222: }; 223: $[14] = direction; 224: $[15] = focusedIndex; 225: $[16] = getKey; 226: $[17] = renderItem; 227: $[18] = total; 228: $[19] = visible.length; 229: $[20] = visibleCount; 230: $[21] = windowStart; 231: $[22] = t2; 232: } else { 233: t2 = $[22]; 234: } 235: t1 = visible.map(t2); 236: $[5] = direction; 237: $[6] = focusedIndex; 238: $[7] = getKey; 239: $[8] = renderItem; 240: $[9] = total; 241: $[10] = visible; 242: $[11] = visibleCount; 243: $[12] = windowStart; 244: $[13] = t1; 245: } else { 246: t1 = $[13]; 247: } 248: const rows = t1; 249: const t2 = direction === "up" ? "column-reverse" : "column"; 250: let t3; 251: if ($[23] !== rows || $[24] !== t2 || $[25] !== visibleCount) { 252: t3 = <Box height={visibleCount} flexShrink={0} flexDirection={t2}>{rows}</Box>; 253: $[23] = rows; 254: $[24] = t2; 255: $[25] = visibleCount; 256: $[26] = t3; 257: } else { 258: t3 = $[26]; 259: } 260: return t3; 261: } 262: function firstWord(s: string): string { 263: const i = s.indexOf(' '); 264: return i === -1 ? s : s.slice(0, i); 265: }

File: src/components/design-system/KeyboardShortcutHint.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import Text from '../../ink/components/Text.js'; 4: type Props = { 5: shortcut: string; 6: action: string; 7: parens?: boolean; 8: bold?: boolean; 9: }; 10: export function KeyboardShortcutHint(t0) { 11: const $ = _c(9); 12: const { 13: shortcut, 14: action, 15: parens: t1, 16: bold: t2 17: } = t0; 18: const parens = t1 === undefined ? false : t1; 19: const bold = t2 === undefined ? false : t2; 20: let t3; 21: if ($[0] !== bold || $[1] !== shortcut) { 22: t3 = bold ? <Text bold={true}>{shortcut}</Text> : shortcut; 23: $[0] = bold; 24: $[1] = shortcut; 25: $[2] = t3; 26: } else { 27: t3 = $[2]; 28: } 29: const shortcutText = t3; 30: if (parens) { 31: let t4; 32: if ($[3] !== action || $[4] !== shortcutText) { 33: t4 = <Text>({shortcutText} to {action})</Text>; 34: $[3] = action; 35: $[4] = shortcutText; 36: $[5] = t4; 37: } else { 38: t4 = $[5]; 39: } 40: return t4; 41: } 42: let t4; 43: if ($[6] !== action || $[7] !== shortcutText) { 44: t4 = <Text>{shortcutText} to {action}</Text>; 45: $[6] = action; 46: $[7] = shortcutText; 47: $[8] = t4; 48: } else { 49: t4 = $[8]; 50: } 51: return t4; 52: }

File: src/components/design-system/ListItem.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import type { ReactNode } from 'react'; 4: import React from 'react'; 5: import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js'; 6: import { Box, Text } from '../../ink.js'; 7: type ListItemProps = { 8: isFocused: boolean; 9: isSelected?: boolean; 10: children: ReactNode; 11: description?: string; 12: showScrollDown?: boolean; 13: showScrollUp?: boolean; 14: styled?: boolean; 15: disabled?: boolean; 16: declareCursor?: boolean; 17: }; 18: export function ListItem(t0) { 19: const $ = _c(32); 20: const { 21: isFocused, 22: isSelected: t1, 23: children, 24: description, 25: showScrollDown, 26: showScrollUp, 27: styled: t2, 28: disabled: t3, 29: declareCursor 30: } = t0; 31: const isSelected = t1 === undefined ? false : t1; 32: const styled = t2 === undefined ? true : t2; 33: const disabled = t3 === undefined ? false : t3; 34: let t4; 35: if ($[0] !== disabled || $[1] !== isFocused || $[2] !== showScrollDown || $[3] !== showScrollUp) { 36: t4 = function renderIndicator() { 37: if (disabled) { 38: return <Text> </Text>; 39: } 40: if (isFocused) { 41: return <Text color="suggestion">{figures.pointer}</Text>; 42: } 43: if (showScrollDown) { 44: return <Text dimColor={true}>{figures.arrowDown}</Text>; 45: } 46: if (showScrollUp) { 47: return <Text dimColor={true}>{figures.arrowUp}</Text>; 48: } 49: return <Text> </Text>; 50: }; 51: $[0] = disabled; 52: $[1] = isFocused; 53: $[2] = showScrollDown; 54: $[3] = showScrollUp; 55: $[4] = t4; 56: } else { 57: t4 = $[4]; 58: } 59: const renderIndicator = t4; 60: let t5; 61: if ($[5] !== disabled || $[6] !== isFocused || $[7] !== isSelected || $[8] !== styled) { 62: const getTextColor = function getTextColor() { 63: if (disabled) { 64: return "inactive"; 65: } 66: if (!styled) { 67: return; 68: } 69: if (isSelected) { 70: return "success"; 71: } 72: if (isFocused) { 73: return "suggestion"; 74: } 75: }; 76: t5 = getTextColor(); 77: $[5] = disabled; 78: $[6] = isFocused; 79: $[7] = isSelected; 80: $[8] = styled; 81: $[9] = t5; 82: } else { 83: t5 = $[9]; 84: } 85: const textColor = t5; 86: const t6 = isFocused && !disabled && declareCursor !== false; 87: let t7; 88: if ($[10] !== t6) { 89: t7 = { 90: line: 0, 91: column: 0, 92: active: t6 93: }; 94: $[10] = t6; 95: $[11] = t7; 96: } else { 97: t7 = $[11]; 98: } 99: const cursorRef = useDeclaredCursor(t7); 100: let t8; 101: if ($[12] !== renderIndicator) { 102: t8 = renderIndicator(); 103: $[12] = renderIndicator; 104: $[13] = t8; 105: } else { 106: t8 = $[13]; 107: } 108: let t9; 109: if ($[14] !== children || $[15] !== disabled || $[16] !== styled || $[17] !== textColor) { 110: t9 = styled ? <Text color={textColor} dimColor={disabled}>{children}</Text> : children; 111: $[14] = children; 112: $[15] = disabled; 113: $[16] = styled; 114: $[17] = textColor; 115: $[18] = t9; 116: } else { 117: t9 = $[18]; 118: } 119: let t10; 120: if ($[19] !== disabled || $[20] !== isSelected) { 121: t10 = isSelected && !disabled && <Text color="success">{figures.tick}</Text>; 122: $[19] = disabled; 123: $[20] = isSelected; 124: $[21] = t10; 125: } else { 126: t10 = $[21]; 127: } 128: let t11; 129: if ($[22] !== t10 || $[23] !== t8 || $[24] !== t9) { 130: t11 = <Box flexDirection="row" gap={1}>{t8}{t9}{t10}</Box>; 131: $[22] = t10; 132: $[23] = t8; 133: $[24] = t9; 134: $[25] = t11; 135: } else { 136: t11 = $[25]; 137: } 138: let t12; 139: if ($[26] !== description) { 140: t12 = description && <Box paddingLeft={2}><Text color="inactive">{description}</Text></Box>; 141: $[26] = description; 142: $[27] = t12; 143: } else { 144: t12 = $[27]; 145: } 146: let t13; 147: if ($[28] !== cursorRef || $[29] !== t11 || $[30] !== t12) { 148: t13 = <Box ref={cursorRef} flexDirection="column">{t11}{t12}</Box>; 149: $[28] = cursorRef; 150: $[29] = t11; 151: $[30] = t12; 152: $[31] = t13; 153: } else { 154: t13 = $[31]; 155: } 156: return t13; 157: }

File: src/components/design-system/LoadingState.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { Spinner } from '../Spinner.js'; 5: type LoadingStateProps = { 6: message: string; 7: bold?: boolean; 8: dimColor?: boolean; 9: subtitle?: string; 10: }; 11: export function LoadingState(t0) { 12: const $ = _c(10); 13: const { 14: message, 15: bold: t1, 16: dimColor: t2, 17: subtitle 18: } = t0; 19: const bold = t1 === undefined ? false : t1; 20: const dimColor = t2 === undefined ? false : t2; 21: let t3; 22: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 23: t3 = <Spinner />; 24: $[0] = t3; 25: } else { 26: t3 = $[0]; 27: } 28: let t4; 29: if ($[1] !== bold || $[2] !== dimColor || $[3] !== message) { 30: t4 = <Box flexDirection="row">{t3}<Text bold={bold} dimColor={dimColor}>{" "}{message}</Text></Box>; 31: $[1] = bold; 32: $[2] = dimColor; 33: $[3] = message; 34: $[4] = t4; 35: } else { 36: t4 = $[4]; 37: } 38: let t5; 39: if ($[5] !== subtitle) { 40: t5 = subtitle && <Text dimColor={true}>{subtitle}</Text>; 41: $[5] = subtitle; 42: $[6] = t5; 43: } else { 44: t5 = $[6]; 45: } 46: let t6; 47: if ($[7] !== t4 || $[8] !== t5) { 48: t6 = <Box flexDirection="column">{t4}{t5}</Box>; 49: $[7] = t4; 50: $[8] = t5; 51: $[9] = t6; 52: } else { 53: t6 = $[9]; 54: } 55: return t6; 56: }

File: src/components/design-system/Pane.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { useIsInsideModal } from '../../context/modalContext.js'; 4: import { Box } from '../../ink.js'; 5: import type { Theme } from '../../utils/theme.js'; 6: import { Divider } from './Divider.js'; 7: type PaneProps = { 8: children: React.ReactNode; 9: color?: keyof Theme; 10: }; 11: export function Pane(t0) { 12: const $ = _c(9); 13: const { 14: children, 15: color 16: } = t0; 17: if (useIsInsideModal()) { 18: let t1; 19: if ($[0] !== children) { 20: t1 = <Box flexDirection="column" paddingX={1} flexShrink={0}>{children}</Box>; 21: $[0] = children; 22: $[1] = t1; 23: } else { 24: t1 = $[1]; 25: } 26: return t1; 27: } 28: let t1; 29: if ($[2] !== color) { 30: t1 = <Divider color={color} />; 31: $[2] = color; 32: $[3] = t1; 33: } else { 34: t1 = $[3]; 35: } 36: let t2; 37: if ($[4] !== children) { 38: t2 = <Box flexDirection="column" paddingX={2}>{children}</Box>; 39: $[4] = children; 40: $[5] = t2; 41: } else { 42: t2 = $[5]; 43: } 44: let t3; 45: if ($[6] !== t1 || $[7] !== t2) { 46: t3 = <Box flexDirection="column" paddingTop={1}>{t1}{t2}</Box>; 47: $[6] = t1; 48: $[7] = t2; 49: $[8] = t3; 50: } else { 51: t3 = $[8]; 52: } 53: return t3; 54: }

File: src/components/design-system/ProgressBar.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Text } from '../../ink.js'; 4: import type { Theme } from '../../utils/theme.js'; 5: type Props = { 6: ratio: number; 7: width: number; 8: fillColor?: keyof Theme; 9: emptyColor?: keyof Theme; 10: }; 11: const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']; 12: export function ProgressBar(t0) { 13: const $ = _c(13); 14: const { 15: ratio: inputRatio, 16: width, 17: fillColor, 18: emptyColor 19: } = t0; 20: const ratio = Math.min(1, Math.max(0, inputRatio)); 21: const whole = Math.floor(ratio * width); 22: let t1; 23: if ($[0] !== whole) { 24: t1 = BLOCKS[BLOCKS.length - 1].repeat(whole); 25: $[0] = whole; 26: $[1] = t1; 27: } else { 28: t1 = $[1]; 29: } 30: let segments; 31: if ($[2] !== ratio || $[3] !== t1 || $[4] !== whole || $[5] !== width) { 32: segments = [t1]; 33: if (whole < width) { 34: const remainder = ratio * width - whole; 35: const middle = Math.floor(remainder * BLOCKS.length); 36: segments.push(BLOCKS[middle]); 37: const empty = width - whole - 1; 38: if (empty > 0) { 39: let t2; 40: if ($[7] !== empty) { 41: t2 = BLOCKS[0].repeat(empty); 42: $[7] = empty; 43: $[8] = t2; 44: } else { 45: t2 = $[8]; 46: } 47: segments.push(t2); 48: } 49: } 50: $[2] = ratio; 51: $[3] = t1; 52: $[4] = whole; 53: $[5] = width; 54: $[6] = segments; 55: } else { 56: segments = $[6]; 57: } 58: const t2 = segments.join(""); 59: let t3; 60: if ($[9] !== emptyColor || $[10] !== fillColor || $[11] !== t2) { 61: t3 = <Text color={fillColor} backgroundColor={emptyColor}>{t2}</Text>; 62: $[9] = emptyColor; 63: $[10] = fillColor; 64: $[11] = t2; 65: $[12] = t3; 66: } else { 67: t3 = $[12]; 68: } 69: return t3; 70: }

File: src/components/design-system/Ratchet.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; 3: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 4: import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js'; 5: import { Box, type DOMElement, measureElement } from '../../ink.js'; 6: type Props = { 7: children: React.ReactNode; 8: lock?: 'always' | 'offscreen'; 9: }; 10: export function Ratchet(t0) { 11: const $ = _c(10); 12: const { 13: children, 14: lock: t1 15: } = t0; 16: const lock = t1 === undefined ? "always" : t1; 17: const [viewportRef, t2] = useTerminalViewport(); 18: const { 19: isVisible 20: } = t2; 21: const { 22: rows 23: } = useTerminalSize(); 24: const innerRef = useRef(null); 25: const maxHeight = useRef(0); 26: const [minHeight, setMinHeight] = useState(0); 27: let t3; 28: if ($[0] !== viewportRef) { 29: t3 = el => { 30: viewportRef(el); 31: }; 32: $[0] = viewportRef; 33: $[1] = t3; 34: } else { 35: t3 = $[1]; 36: } 37: const outerRef = t3; 38: const engaged = lock === "always" || !isVisible; 39: let t4; 40: if ($[2] !== rows) { 41: t4 = () => { 42: if (!innerRef.current) { 43: return; 44: } 45: const { 46: height 47: } = measureElement(innerRef.current); 48: if (height > maxHeight.current) { 49: maxHeight.current = Math.min(height, rows); 50: setMinHeight(maxHeight.current); 51: } 52: }; 53: $[2] = rows; 54: $[3] = t4; 55: } else { 56: t4 = $[3]; 57: } 58: useLayoutEffect(t4); 59: const t5 = engaged ? minHeight : undefined; 60: let t6; 61: if ($[4] !== children) { 62: t6 = <Box ref={innerRef} flexDirection="column">{children}</Box>; 63: $[4] = children; 64: $[5] = t6; 65: } else { 66: t6 = $[5]; 67: } 68: let t7; 69: if ($[6] !== outerRef || $[7] !== t5 || $[8] !== t6) { 70: t7 = <Box minHeight={t5} ref={outerRef}>{t6}</Box>; 71: $[6] = outerRef; 72: $[7] = t5; 73: $[8] = t6; 74: $[9] = t7; 75: } else { 76: t7 = $[9]; 77: } 78: return t7; 79: }

File: src/components/design-system/StatusIcon.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React from 'react'; 4: import { Text } from '../../ink.js'; 5: type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading'; 6: type Props = { 7: status: Status; 8: withSpace?: boolean; 9: }; 10: const STATUS_CONFIG: Record<Status, { 11: icon: string; 12: color: 'success' | 'error' | 'warning' | 'suggestion' | undefined; 13: }> = { 14: success: { 15: icon: figures.tick, 16: color: 'success' 17: }, 18: error: { 19: icon: figures.cross, 20: color: 'error' 21: }, 22: warning: { 23: icon: figures.warning, 24: color: 'warning' 25: }, 26: info: { 27: icon: figures.info, 28: color: 'suggestion' 29: }, 30: pending: { 31: icon: figures.circle, 32: color: undefined 33: }, 34: loading: { 35: icon: '…', 36: color: undefined 37: } 38: }; 39: export function StatusIcon(t0) { 40: const $ = _c(5); 41: const { 42: status, 43: withSpace: t1 44: } = t0; 45: const withSpace = t1 === undefined ? false : t1; 46: const config = STATUS_CONFIG[status]; 47: const t2 = !config.color; 48: const t3 = withSpace && " "; 49: let t4; 50: if ($[0] !== config.color || $[1] !== config.icon || $[2] !== t2 || $[3] !== t3) { 51: t4 = <Text color={config.color} dimColor={t2}>{config.icon}{t3}</Text>; 52: $[0] = config.color; 53: $[1] = config.icon; 54: $[2] = t2; 55: $[3] = t3; 56: $[4] = t4; 57: } else { 58: t4 = $[4]; 59: } 60: return t4; 61: }

File: src/components/design-system/Tabs.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; 3: import { useIsInsideModal, useModalScrollRef } from '../../context/modalContext.js'; 4: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 5: import ScrollBox from '../../ink/components/ScrollBox.js'; 6: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 7: import { stringWidth } from '../../ink/stringWidth.js'; 8: import { Box, Text } from '../../ink.js'; 9: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 10: import type { Theme } from '../../utils/theme.js'; 11: type TabsProps = { 12: children: Array<React.ReactElement<TabProps>>; 13: title?: string; 14: color?: keyof Theme; 15: defaultTab?: string; 16: hidden?: boolean; 17: useFullWidth?: boolean; 18: selectedTab?: string; 19: onTabChange?: (tabId: string) => void; 20: banner?: React.ReactNode; 21: disableNavigation?: boolean; 22: initialHeaderFocused?: boolean; 23: contentHeight?: number; 24: navFromContent?: boolean; 25: }; 26: type TabsContextValue = { 27: selectedTab: string | undefined; 28: width: number | undefined; 29: headerFocused: boolean; 30: focusHeader: () => void; 31: blurHeader: () => void; 32: registerOptIn: () => () => void; 33: }; 34: const TabsContext = createContext<TabsContextValue>({ 35: selectedTab: undefined, 36: width: undefined, 37: headerFocused: false, 38: focusHeader: () => {}, 39: blurHeader: () => {}, 40: registerOptIn: () => () => {} 41: }); 42: export function Tabs(t0) { 43: const $ = _c(25); 44: const { 45: title, 46: color, 47: defaultTab, 48: children, 49: hidden, 50: useFullWidth, 51: selectedTab: controlledSelectedTab, 52: onTabChange, 53: banner, 54: disableNavigation, 55: initialHeaderFocused: t1, 56: contentHeight, 57: navFromContent: t2 58: } = t0; 59: const initialHeaderFocused = t1 === undefined ? true : t1; 60: const navFromContent = t2 === undefined ? false : t2; 61: const { 62: columns: terminalWidth 63: } = useTerminalSize(); 64: const tabs = children.map(_temp); 65: const defaultTabIndex = defaultTab ? tabs.findIndex(tab => defaultTab === tab[0]) : 0; 66: const isControlled = controlledSelectedTab !== undefined; 67: const [internalSelectedTab, setInternalSelectedTab] = useState(defaultTabIndex !== -1 ? defaultTabIndex : 0); 68: const controlledTabIndex = isControlled ? tabs.findIndex(tab_0 => tab_0[0] === controlledSelectedTab) : -1; 69: const selectedTabIndex = isControlled ? controlledTabIndex !== -1 ? controlledTabIndex : 0 : internalSelectedTab; 70: const modalScrollRef = useModalScrollRef(); 71: const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused); 72: let t3; 73: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 74: t3 = () => setHeaderFocused(true); 75: $[0] = t3; 76: } else { 77: t3 = $[0]; 78: } 79: const focusHeader = t3; 80: let t4; 81: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 82: t4 = () => setHeaderFocused(false); 83: $[1] = t4; 84: } else { 85: t4 = $[1]; 86: } 87: const blurHeader = t4; 88: const [optInCount, setOptInCount] = useState(0); 89: let t5; 90: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 91: t5 = () => { 92: setOptInCount(_temp2); 93: return () => setOptInCount(_temp3); 94: }; 95: $[2] = t5; 96: } else { 97: t5 = $[2]; 98: } 99: const registerOptIn = t5; 100: const optedIn = optInCount > 0; 101: const handleTabChange = offset => { 102: const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length; 103: const newTabId = tabs[newIndex]?.[0]; 104: if (isControlled && onTabChange && newTabId) { 105: onTabChange(newTabId); 106: } else { 107: setInternalSelectedTab(newIndex); 108: } 109: setHeaderFocused(true); 110: }; 111: const t6 = !hidden && !disableNavigation && headerFocused; 112: let t7; 113: if ($[3] !== t6) { 114: t7 = { 115: context: "Tabs", 116: isActive: t6 117: }; 118: $[3] = t6; 119: $[4] = t7; 120: } else { 121: t7 = $[4]; 122: } 123: useKeybindings({ 124: "tabs:next": () => handleTabChange(1), 125: "tabs:previous": () => handleTabChange(-1) 126: }, t7); 127: let t8; 128: if ($[5] !== headerFocused || $[6] !== hidden || $[7] !== optedIn) { 129: t8 = e => { 130: if (!headerFocused || !optedIn || hidden) { 131: return; 132: } 133: if (e.key === "down") { 134: e.preventDefault(); 135: setHeaderFocused(false); 136: } 137: }; 138: $[5] = headerFocused; 139: $[6] = hidden; 140: $[7] = optedIn; 141: $[8] = t8; 142: } else { 143: t8 = $[8]; 144: } 145: const handleKeyDown = t8; 146: const t9 = navFromContent && !headerFocused && optedIn && !hidden && !disableNavigation; 147: let t10; 148: if ($[9] !== t9) { 149: t10 = { 150: context: "Tabs", 151: isActive: t9 152: }; 153: $[9] = t9; 154: $[10] = t10; 155: } else { 156: t10 = $[10]; 157: } 158: useKeybindings({ 159: "tabs:next": () => { 160: handleTabChange(1); 161: setHeaderFocused(true); 162: }, 163: "tabs:previous": () => { 164: handleTabChange(-1); 165: setHeaderFocused(true); 166: } 167: }, t10); 168: const titleWidth = title ? stringWidth(title) + 1 : 0; 169: const tabsWidth = tabs.reduce(_temp4, 0); 170: const usedWidth = titleWidth + tabsWidth; 171: const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0; 172: const contentWidth = useFullWidth ? terminalWidth : undefined; 173: const T0 = Box; 174: const t11 = "column"; 175: const t12 = 0; 176: const t13 = true; 177: const t14 = modalScrollRef ? 0 : undefined; 178: const t15 = !hidden && <Box flexDirection="row" gap={1} flexShrink={modalScrollRef ? 0 : undefined}>{title !== undefined && <Text bold={true} color={color}>{title}</Text>}{tabs.map((t16, i) => { 179: const [id, title_0] = t16; 180: const isCurrent = selectedTabIndex === i; 181: const hasColorCursor = color && isCurrent && headerFocused; 182: return <Text key={id} backgroundColor={hasColorCursor ? color : undefined} color={hasColorCursor ? "inverseText" : undefined} inverse={isCurrent && !hasColorCursor} bold={isCurrent}>{" "}{title_0}{" "}</Text>; 183: })}{spacerWidth > 0 && <Text>{" ".repeat(spacerWidth)}</Text>}</Box>; 184: let t17; 185: if ($[11] !== children || $[12] !== contentHeight || $[13] !== contentWidth || $[14] !== hidden || $[15] !== modalScrollRef || $[16] !== selectedTabIndex) { 186: t17 = modalScrollRef ? <Box width={contentWidth} marginTop={hidden ? 0 : 1} flexShrink={0}><ScrollBox key={selectedTabIndex} ref={modalScrollRef} flexDirection="column" flexShrink={0}>{children}</ScrollBox></Box> : <Box width={contentWidth} marginTop={hidden ? 0 : 1} height={contentHeight} overflowY={contentHeight !== undefined ? "hidden" : undefined}>{children}</Box>; 187: $[11] = children; 188: $[12] = contentHeight; 189: $[13] = contentWidth; 190: $[14] = hidden; 191: $[15] = modalScrollRef; 192: $[16] = selectedTabIndex; 193: $[17] = t17; 194: } else { 195: t17 = $[17]; 196: } 197: let t18; 198: if ($[18] !== T0 || $[19] !== banner || $[20] !== handleKeyDown || $[21] !== t14 || $[22] !== t15 || $[23] !== t17) { 199: t18 = <T0 flexDirection={t11} tabIndex={t12} autoFocus={t13} onKeyDown={handleKeyDown} flexShrink={t14}>{t15}{banner}{t17}</T0>; 200: $[18] = T0; 201: $[19] = banner; 202: $[20] = handleKeyDown; 203: $[21] = t14; 204: $[22] = t15; 205: $[23] = t17; 206: $[24] = t18; 207: } else { 208: t18 = $[24]; 209: } 210: return <TabsContext.Provider value={{ 211: selectedTab: tabs[selectedTabIndex][0], 212: width: contentWidth, 213: headerFocused, 214: focusHeader, 215: blurHeader, 216: registerOptIn 217: }}>{t18}</TabsContext.Provider>; 218: } 219: function _temp4(sum, t0) { 220: const [, tabTitle] = t0; 221: return sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1; 222: } 223: function _temp3(n_0) { 224: return n_0 - 1; 225: } 226: function _temp2(n) { 227: return n + 1; 228: } 229: function _temp(child) { 230: return [child.props.id ?? child.props.title, child.props.title]; 231: } 232: type TabProps = { 233: title: string; 234: id?: string; 235: children: React.ReactNode; 236: }; 237: export function Tab(t0) { 238: const $ = _c(4); 239: const { 240: title, 241: id, 242: children 243: } = t0; 244: const { 245: selectedTab, 246: width 247: } = useContext(TabsContext); 248: const insideModal = useIsInsideModal(); 249: if (selectedTab !== (id ?? title)) { 250: return null; 251: } 252: const t1 = insideModal ? 0 : undefined; 253: let t2; 254: if ($[0] !== children || $[1] !== t1 || $[2] !== width) { 255: t2 = <Box width={width} flexShrink={t1}>{children}</Box>; 256: $[0] = children; 257: $[1] = t1; 258: $[2] = width; 259: $[3] = t2; 260: } else { 261: t2 = $[3]; 262: } 263: return t2; 264: } 265: export function useTabsWidth() { 266: const { 267: width 268: } = useContext(TabsContext); 269: return width; 270: } 271: export function useTabHeaderFocus() { 272: const $ = _c(6); 273: const { 274: headerFocused, 275: focusHeader, 276: blurHeader, 277: registerOptIn 278: } = useContext(TabsContext); 279: let t0; 280: if ($[0] !== registerOptIn) { 281: t0 = [registerOptIn]; 282: $[0] = registerOptIn; 283: $[1] = t0; 284: } else { 285: t0 = $[1]; 286: } 287: useEffect(registerOptIn, t0); 288: let t1; 289: if ($[2] !== blurHeader || $[3] !== focusHeader || $[4] !== headerFocused) { 290: t1 = { 291: headerFocused, 292: focusHeader, 293: blurHeader 294: }; 295: $[2] = blurHeader; 296: $[3] = focusHeader; 297: $[4] = headerFocused; 298: $[5] = t1; 299: } else { 300: t1 = $[5]; 301: } 302: return t1; 303: }

File: src/components/design-system/ThemedBox.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { type PropsWithChildren, type Ref } from 'react'; 3: import Box from '../../ink/components/Box.js'; 4: import type { DOMElement } from '../../ink/dom.js'; 5: import type { ClickEvent } from '../../ink/events/click-event.js'; 6: import type { FocusEvent } from '../../ink/events/focus-event.js'; 7: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 8: import type { Color, Styles } from '../../ink/styles.js'; 9: import { getTheme, type Theme } from '../../utils/theme.js'; 10: import { useTheme } from './ThemeProvider.js'; 11: type ThemedColorProps = { 12: readonly borderColor?: keyof Theme | Color; 13: readonly borderTopColor?: keyof Theme | Color; 14: readonly borderBottomColor?: keyof Theme | Color; 15: readonly borderLeftColor?: keyof Theme | Color; 16: readonly borderRightColor?: keyof Theme | Color; 17: readonly backgroundColor?: keyof Theme | Color; 18: }; 19: type BaseStylesWithoutColors = Omit<Styles, 'textWrap' | 'borderColor' | 'borderTopColor' | 'borderBottomColor' | 'borderLeftColor' | 'borderRightColor' | 'backgroundColor'>; 20: export type Props = BaseStylesWithoutColors & ThemedColorProps & { 21: ref?: Ref<DOMElement>; 22: tabIndex?: number; 23: autoFocus?: boolean; 24: onClick?: (event: ClickEvent) => void; 25: onFocus?: (event: FocusEvent) => void; 26: onFocusCapture?: (event: FocusEvent) => void; 27: onBlur?: (event: FocusEvent) => void; 28: onBlurCapture?: (event: FocusEvent) => void; 29: onKeyDown?: (event: KeyboardEvent) => void; 30: onKeyDownCapture?: (event: KeyboardEvent) => void; 31: onMouseEnter?: () => void; 32: onMouseLeave?: () => void; 33: }; 34: function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined { 35: if (!color) return undefined; 36: if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) { 37: return color as Color; 38: } 39: return theme[color as keyof Theme] as Color; 40: } 41: function ThemedBox(t0) { 42: const $ = _c(33); 43: let backgroundColor; 44: let borderBottomColor; 45: let borderColor; 46: let borderLeftColor; 47: let borderRightColor; 48: let borderTopColor; 49: let children; 50: let ref; 51: let rest; 52: if ($[0] !== t0) { 53: ({ 54: borderColor, 55: borderTopColor, 56: borderBottomColor, 57: borderLeftColor, 58: borderRightColor, 59: backgroundColor, 60: children, 61: ref, 62: ...rest 63: } = t0); 64: $[0] = t0; 65: $[1] = backgroundColor; 66: $[2] = borderBottomColor; 67: $[3] = borderColor; 68: $[4] = borderLeftColor; 69: $[5] = borderRightColor; 70: $[6] = borderTopColor; 71: $[7] = children; 72: $[8] = ref; 73: $[9] = rest; 74: } else { 75: backgroundColor = $[1]; 76: borderBottomColor = $[2]; 77: borderColor = $[3]; 78: borderLeftColor = $[4]; 79: borderRightColor = $[5]; 80: borderTopColor = $[6]; 81: children = $[7]; 82: ref = $[8]; 83: rest = $[9]; 84: } 85: const [themeName] = useTheme(); 86: let resolvedBorderBottomColor; 87: let resolvedBorderColor; 88: let resolvedBorderLeftColor; 89: let resolvedBorderRightColor; 90: let resolvedBorderTopColor; 91: let t1; 92: if ($[10] !== backgroundColor || $[11] !== borderBottomColor || $[12] !== borderColor || $[13] !== borderLeftColor || $[14] !== borderRightColor || $[15] !== borderTopColor || $[16] !== themeName) { 93: const theme = getTheme(themeName); 94: resolvedBorderColor = resolveColor(borderColor, theme); 95: resolvedBorderTopColor = resolveColor(borderTopColor, theme); 96: resolvedBorderBottomColor = resolveColor(borderBottomColor, theme); 97: resolvedBorderLeftColor = resolveColor(borderLeftColor, theme); 98: resolvedBorderRightColor = resolveColor(borderRightColor, theme); 99: t1 = resolveColor(backgroundColor, theme); 100: $[10] = backgroundColor; 101: $[11] = borderBottomColor; 102: $[12] = borderColor; 103: $[13] = borderLeftColor; 104: $[14] = borderRightColor; 105: $[15] = borderTopColor; 106: $[16] = themeName; 107: $[17] = resolvedBorderBottomColor; 108: $[18] = resolvedBorderColor; 109: $[19] = resolvedBorderLeftColor; 110: $[20] = resolvedBorderRightColor; 111: $[21] = resolvedBorderTopColor; 112: $[22] = t1; 113: } else { 114: resolvedBorderBottomColor = $[17]; 115: resolvedBorderColor = $[18]; 116: resolvedBorderLeftColor = $[19]; 117: resolvedBorderRightColor = $[20]; 118: resolvedBorderTopColor = $[21]; 119: t1 = $[22]; 120: } 121: const resolvedBackgroundColor = t1; 122: let t2; 123: if ($[23] !== children || $[24] !== ref || $[25] !== resolvedBackgroundColor || $[26] !== resolvedBorderBottomColor || $[27] !== resolvedBorderColor || $[28] !== resolvedBorderLeftColor || $[29] !== resolvedBorderRightColor || $[30] !== resolvedBorderTopColor || $[31] !== rest) { 124: t2 = <Box ref={ref} borderColor={resolvedBorderColor} borderTopColor={resolvedBorderTopColor} borderBottomColor={resolvedBorderBottomColor} borderLeftColor={resolvedBorderLeftColor} borderRightColor={resolvedBorderRightColor} backgroundColor={resolvedBackgroundColor} {...rest}>{children}</Box>; 125: $[23] = children; 126: $[24] = ref; 127: $[25] = resolvedBackgroundColor; 128: $[26] = resolvedBorderBottomColor; 129: $[27] = resolvedBorderColor; 130: $[28] = resolvedBorderLeftColor; 131: $[29] = resolvedBorderRightColor; 132: $[30] = resolvedBorderTopColor; 133: $[31] = rest; 134: $[32] = t2; 135: } else { 136: t2 = $[32]; 137: } 138: return t2; 139: } 140: export default ThemedBox;

File: src/components/design-system/ThemedText.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { ReactNode } from 'react'; 3: import React, { useContext } from 'react'; 4: import Text from '../../ink/components/Text.js'; 5: import type { Color, Styles } from '../../ink/styles.js'; 6: import { getTheme, type Theme } from '../../utils/theme.js'; 7: import { useTheme } from './ThemeProvider.js'; 8: export const TextHoverColorContext = React.createContext<keyof Theme | undefined>(undefined); 9: export type Props = { 10: readonly color?: keyof Theme | Color; 11: readonly backgroundColor?: keyof Theme; 12: readonly dimColor?: boolean; 13: readonly bold?: boolean; 14: readonly italic?: boolean; 15: readonly underline?: boolean; 16: readonly strikethrough?: boolean; 17: readonly inverse?: boolean; 18: readonly wrap?: Styles['textWrap']; 19: readonly children?: ReactNode; 20: }; 21: function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined { 22: if (!color) return undefined; 23: if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) { 24: return color as Color; 25: } 26: return theme[color as keyof Theme] as Color; 27: } 28: export default function ThemedText(t0) { 29: const $ = _c(10); 30: const { 31: color, 32: backgroundColor, 33: dimColor: t1, 34: bold: t2, 35: italic: t3, 36: underline: t4, 37: strikethrough: t5, 38: inverse: t6, 39: wrap: t7, 40: children 41: } = t0; 42: const dimColor = t1 === undefined ? false : t1; 43: const bold = t2 === undefined ? false : t2; 44: const italic = t3 === undefined ? false : t3; 45: const underline = t4 === undefined ? false : t4; 46: const strikethrough = t5 === undefined ? false : t5; 47: const inverse = t6 === undefined ? false : t6; 48: const wrap = t7 === undefined ? "wrap" : t7; 49: const [themeName] = useTheme(); 50: const theme = getTheme(themeName); 51: const hoverColor = useContext(TextHoverColorContext); 52: const resolvedColor = !color && hoverColor ? resolveColor(hoverColor, theme) : dimColor ? theme.inactive as Color : resolveColor(color, theme); 53: const resolvedBackgroundColor = backgroundColor ? theme[backgroundColor] as Color : undefined; 54: let t8; 55: if ($[0] !== bold || $[1] !== children || $[2] !== inverse || $[3] !== italic || $[4] !== resolvedBackgroundColor || $[5] !== resolvedColor || $[6] !== strikethrough || $[7] !== underline || $[8] !== wrap) { 56: t8 = <Text color={resolvedColor} backgroundColor={resolvedBackgroundColor} bold={bold} italic={italic} underline={underline} strikethrough={strikethrough} inverse={inverse} wrap={wrap}>{children}</Text>; 57: $[0] = bold; 58: $[1] = children; 59: $[2] = inverse; 60: $[3] = italic; 61: $[4] = resolvedBackgroundColor; 62: $[5] = resolvedColor; 63: $[6] = strikethrough; 64: $[7] = underline; 65: $[8] = wrap; 66: $[9] = t8; 67: } else { 68: t8 = $[9]; 69: } 70: return t8; 71: }

File: src/components/design-system/ThemeProvider.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; 4: import useStdin from '../../ink/hooks/use-stdin.js'; 5: import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; 6: import { getSystemThemeName, type SystemTheme } from '../../utils/systemTheme.js'; 7: import type { ThemeName, ThemeSetting } from '../../utils/theme.js'; 8: type ThemeContextValue = { 9: themeSetting: ThemeSetting; 10: setThemeSetting: (setting: ThemeSetting) => void; 11: setPreviewTheme: (setting: ThemeSetting) => void; 12: savePreview: () => void; 13: cancelPreview: () => void; 14: currentTheme: ThemeName; 15: }; 16: const DEFAULT_THEME: ThemeName = 'dark'; 17: const ThemeContext = createContext<ThemeContextValue>({ 18: themeSetting: DEFAULT_THEME, 19: setThemeSetting: () => {}, 20: setPreviewTheme: () => {}, 21: savePreview: () => {}, 22: cancelPreview: () => {}, 23: currentTheme: DEFAULT_THEME 24: }); 25: type Props = { 26: children: React.ReactNode; 27: initialState?: ThemeSetting; 28: onThemeSave?: (setting: ThemeSetting) => void; 29: }; 30: function defaultInitialTheme(): ThemeSetting { 31: return getGlobalConfig().theme; 32: } 33: function defaultSaveTheme(setting: ThemeSetting): void { 34: saveGlobalConfig(current => ({ 35: ...current, 36: theme: setting 37: })); 38: } 39: export function ThemeProvider({ 40: children, 41: initialState, 42: onThemeSave = defaultSaveTheme 43: }: Props) { 44: const [themeSetting, setThemeSetting] = useState(initialState ?? defaultInitialTheme); 45: const [previewTheme, setPreviewTheme] = useState<ThemeSetting | null>(null); 46: const [systemTheme, setSystemTheme] = useState<SystemTheme>(() => (initialState ?? themeSetting) === 'auto' ? getSystemThemeName() : 'dark'); 47: const activeSetting = previewTheme ?? themeSetting; 48: const { 49: internal_querier 50: } = useStdin(); 51: useEffect(() => { 52: if (feature('AUTO_THEME')) { 53: if (activeSetting !== 'auto' || !internal_querier) return; 54: let cleanup: (() => void) | undefined; 55: let cancelled = false; 56: void import('../../utils/systemThemeWatcher.js').then(({ 57: watchSystemTheme 58: }) => { 59: if (cancelled) return; 60: cleanup = watchSystemTheme(internal_querier, setSystemTheme); 61: }); 62: return () => { 63: cancelled = true; 64: cleanup?.(); 65: }; 66: } 67: }, [activeSetting, internal_querier]); 68: const currentTheme: ThemeName = activeSetting === 'auto' ? systemTheme : activeSetting; 69: const value = useMemo<ThemeContextValue>(() => ({ 70: themeSetting, 71: setThemeSetting: (newSetting: ThemeSetting) => { 72: setThemeSetting(newSetting); 73: setPreviewTheme(null); 74: if (newSetting === 'auto') { 75: setSystemTheme(getSystemThemeName()); 76: } 77: onThemeSave?.(newSetting); 78: }, 79: setPreviewTheme: (newSetting_0: ThemeSetting) => { 80: setPreviewTheme(newSetting_0); 81: if (newSetting_0 === 'auto') { 82: setSystemTheme(getSystemThemeName()); 83: } 84: }, 85: savePreview: () => { 86: if (previewTheme !== null) { 87: setThemeSetting(previewTheme); 88: setPreviewTheme(null); 89: onThemeSave?.(previewTheme); 90: } 91: }, 92: cancelPreview: () => { 93: if (previewTheme !== null) { 94: setPreviewTheme(null); 95: } 96: }, 97: currentTheme 98: }), [themeSetting, previewTheme, currentTheme, onThemeSave]); 99: return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>; 100: } 101: export function useTheme() { 102: const $ = _c(3); 103: const { 104: currentTheme, 105: setThemeSetting 106: } = useContext(ThemeContext); 107: let t0; 108: if ($[0] !== currentTheme || $[1] !== setThemeSetting) { 109: t0 = [currentTheme, setThemeSetting]; 110: $[0] = currentTheme; 111: $[1] = setThemeSetting; 112: $[2] = t0; 113: } else { 114: t0 = $[2]; 115: } 116: return t0; 117: } 118: export function useThemeSetting() { 119: return useContext(ThemeContext).themeSetting; 120: } 121: export function usePreviewTheme() { 122: const $ = _c(4); 123: const { 124: setPreviewTheme, 125: savePreview, 126: cancelPreview 127: } = useContext(ThemeContext); 128: let t0; 129: if ($[0] !== cancelPreview || $[1] !== savePreview || $[2] !== setPreviewTheme) { 130: t0 = { 131: setPreviewTheme, 132: savePreview, 133: cancelPreview 134: }; 135: $[0] = cancelPreview; 136: $[1] = savePreview; 137: $[2] = setPreviewTheme; 138: $[3] = t0; 139: } else { 140: t0 = $[3]; 141: } 142: return t0; 143: }

File: src/components/DesktopUpsell/DesktopUpsellStartup.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useEffect, useState } from 'react'; 4: import { Box, Text } from '../../ink.js'; 5: import { getDynamicConfig_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; 6: import { logEvent } from '../../services/analytics/index.js'; 7: import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; 8: import { Select } from '../CustomSelect/select.js'; 9: import { DesktopHandoff } from '../DesktopHandoff.js'; 10: import { PermissionDialog } from '../permissions/PermissionDialog.js'; 11: type DesktopUpsellConfig = { 12: enable_shortcut_tip: boolean; 13: enable_startup_dialog: boolean; 14: }; 15: const DESKTOP_UPSELL_DEFAULT: DesktopUpsellConfig = { 16: enable_shortcut_tip: false, 17: enable_startup_dialog: false 18: }; 19: export function getDesktopUpsellConfig(): DesktopUpsellConfig { 20: return getDynamicConfig_CACHED_MAY_BE_STALE('tengu_desktop_upsell', DESKTOP_UPSELL_DEFAULT); 21: } 22: function isSupportedPlatform(): boolean { 23: return process.platform === 'darwin' || process.platform === 'win32' && process.arch === 'x64'; 24: } 25: export function shouldShowDesktopUpsellStartup(): boolean { 26: if (!isSupportedPlatform()) return false; 27: if (!getDesktopUpsellConfig().enable_startup_dialog) return false; 28: const config = getGlobalConfig(); 29: if (config.desktopUpsellDismissed) return false; 30: if ((config.desktopUpsellSeenCount ?? 0) >= 3) return false; 31: return true; 32: } 33: type DesktopUpsellSelection = 'try' | 'not-now' | 'never'; 34: type Props = { 35: onDone: () => void; 36: }; 37: export function DesktopUpsellStartup(t0) { 38: const $ = _c(14); 39: const { 40: onDone 41: } = t0; 42: const [showHandoff, setShowHandoff] = useState(false); 43: let t1; 44: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 45: t1 = []; 46: $[0] = t1; 47: } else { 48: t1 = $[0]; 49: } 50: useEffect(_temp, t1); 51: if (showHandoff) { 52: let t2; 53: if ($[1] !== onDone) { 54: t2 = <DesktopHandoff onDone={() => onDone()} />; 55: $[1] = onDone; 56: $[2] = t2; 57: } else { 58: t2 = $[2]; 59: } 60: return t2; 61: } 62: let t2; 63: if ($[3] !== onDone) { 64: t2 = function handleSelect(value) { 65: switch (value) { 66: case "try": 67: { 68: setShowHandoff(true); 69: return; 70: } 71: case "never": 72: { 73: saveGlobalConfig(_temp2); 74: onDone(); 75: return; 76: } 77: case "not-now": 78: { 79: onDone(); 80: return; 81: } 82: } 83: }; 84: $[3] = onDone; 85: $[4] = t2; 86: } else { 87: t2 = $[4]; 88: } 89: const handleSelect = t2; 90: let t3; 91: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 92: t3 = { 93: label: "Open in Claude Code Desktop", 94: value: "try" as const 95: }; 96: $[5] = t3; 97: } else { 98: t3 = $[5]; 99: } 100: let t4; 101: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 102: t4 = { 103: label: "Not now", 104: value: "not-now" as const 105: }; 106: $[6] = t4; 107: } else { 108: t4 = $[6]; 109: } 110: let t5; 111: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 112: t5 = [t3, t4, { 113: label: "Don't ask again", 114: value: "never" as const 115: }]; 116: $[7] = t5; 117: } else { 118: t5 = $[7]; 119: } 120: const options = t5; 121: let t6; 122: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 123: t6 = <Box marginBottom={1}><Text>Same Claude Code with visual diffs, live app preview, parallel sessions, and more.</Text></Box>; 124: $[8] = t6; 125: } else { 126: t6 = $[8]; 127: } 128: let t7; 129: if ($[9] !== handleSelect) { 130: t7 = () => handleSelect("not-now"); 131: $[9] = handleSelect; 132: $[10] = t7; 133: } else { 134: t7 = $[10]; 135: } 136: let t8; 137: if ($[11] !== handleSelect || $[12] !== t7) { 138: t8 = <PermissionDialog title="Try Claude Code Desktop"><Box flexDirection="column" paddingX={2} paddingY={1}>{t6}<Select options={options} onChange={handleSelect} onCancel={t7} /></Box></PermissionDialog>; 139: $[11] = handleSelect; 140: $[12] = t7; 141: $[13] = t8; 142: } else { 143: t8 = $[13]; 144: } 145: return t8; 146: } 147: function _temp2(prev_0) { 148: if (prev_0.desktopUpsellDismissed) { 149: return prev_0; 150: } 151: return { 152: ...prev_0, 153: desktopUpsellDismissed: true 154: }; 155: } 156: function _temp() { 157: const newCount = (getGlobalConfig().desktopUpsellSeenCount ?? 0) + 1; 158: saveGlobalConfig(prev => { 159: if ((prev.desktopUpsellSeenCount ?? 0) >= newCount) { 160: return prev; 161: } 162: return { 163: ...prev, 164: desktopUpsellSeenCount: newCount 165: }; 166: }); 167: logEvent("tengu_desktop_upsell_shown", { 168: seen_count: newCount 169: }); 170: }

File: src/components/diff/DiffDetailView.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { StructuredPatchHunk } from 'diff'; 3: import { resolve } from 'path'; 4: import React, { useMemo } from 'react'; 5: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 6: import { Box, Text } from '../../ink.js'; 7: import { getCwd } from '../../utils/cwd.js'; 8: import { readFileSafe } from '../../utils/file.js'; 9: import { Divider } from '../design-system/Divider.js'; 10: import { StructuredDiff } from '../StructuredDiff.js'; 11: type Props = { 12: filePath: string; 13: hunks: StructuredPatchHunk[]; 14: isLargeFile?: boolean; 15: isBinary?: boolean; 16: isTruncated?: boolean; 17: isUntracked?: boolean; 18: }; 19: export function DiffDetailView(t0) { 20: const $ = _c(53); 21: const { 22: filePath, 23: hunks, 24: isLargeFile, 25: isBinary, 26: isTruncated, 27: isUntracked 28: } = t0; 29: const { 30: columns 31: } = useTerminalSize(); 32: let t1; 33: bb0: { 34: if (!filePath) { 35: let t2; 36: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 37: t2 = { 38: firstLine: null, 39: fileContent: undefined 40: }; 41: $[0] = t2; 42: } else { 43: t2 = $[0]; 44: } 45: t1 = t2; 46: break bb0; 47: } 48: let content; 49: let t2; 50: if ($[1] !== filePath) { 51: const fullPath = resolve(getCwd(), filePath); 52: content = readFileSafe(fullPath); 53: t2 = content?.split("\n")[0] ?? null; 54: $[1] = filePath; 55: $[2] = content; 56: $[3] = t2; 57: } else { 58: content = $[2]; 59: t2 = $[3]; 60: } 61: const t3 = content ?? undefined; 62: let t4; 63: if ($[4] !== t2 || $[5] !== t3) { 64: t4 = { 65: firstLine: t2, 66: fileContent: t3 67: }; 68: $[4] = t2; 69: $[5] = t3; 70: $[6] = t4; 71: } else { 72: t4 = $[6]; 73: } 74: t1 = t4; 75: } 76: const { 77: firstLine, 78: fileContent 79: } = t1; 80: if (isUntracked) { 81: let t2; 82: if ($[7] !== filePath) { 83: t2 = <Text bold={true}>{filePath}</Text>; 84: $[7] = filePath; 85: $[8] = t2; 86: } else { 87: t2 = $[8]; 88: } 89: let t3; 90: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 91: t3 = <Text dimColor={true}> (untracked)</Text>; 92: $[9] = t3; 93: } else { 94: t3 = $[9]; 95: } 96: let t4; 97: if ($[10] !== t2) { 98: t4 = <Box>{t2}{t3}</Box>; 99: $[10] = t2; 100: $[11] = t4; 101: } else { 102: t4 = $[11]; 103: } 104: let t5; 105: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 106: t5 = <Divider padding={4} />; 107: $[12] = t5; 108: } else { 109: t5 = $[12]; 110: } 111: let t6; 112: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 113: t6 = <Text dimColor={true} italic={true}>New file not yet staged.</Text>; 114: $[13] = t6; 115: } else { 116: t6 = $[13]; 117: } 118: let t7; 119: if ($[14] !== filePath) { 120: t7 = <Box flexDirection="column">{t6}<Text dimColor={true} italic={true}>Run `git add {filePath}` to see line counts.</Text></Box>; 121: $[14] = filePath; 122: $[15] = t7; 123: } else { 124: t7 = $[15]; 125: } 126: let t8; 127: if ($[16] !== t4 || $[17] !== t7) { 128: t8 = <Box flexDirection="column" width="100%">{t4}{t5}{t7}</Box>; 129: $[16] = t4; 130: $[17] = t7; 131: $[18] = t8; 132: } else { 133: t8 = $[18]; 134: } 135: return t8; 136: } 137: if (isBinary) { 138: let t2; 139: if ($[19] !== filePath) { 140: t2 = <Box><Text bold={true}>{filePath}</Text></Box>; 141: $[19] = filePath; 142: $[20] = t2; 143: } else { 144: t2 = $[20]; 145: } 146: let t3; 147: if ($[21] === Symbol.for("react.memo_cache_sentinel")) { 148: t3 = <Divider padding={4} />; 149: $[21] = t3; 150: } else { 151: t3 = $[21]; 152: } 153: let t4; 154: if ($[22] === Symbol.for("react.memo_cache_sentinel")) { 155: t4 = <Box flexDirection="column"><Text dimColor={true} italic={true}>Binary file - cannot display diff</Text></Box>; 156: $[22] = t4; 157: } else { 158: t4 = $[22]; 159: } 160: let t5; 161: if ($[23] !== t2) { 162: t5 = <Box flexDirection="column" width="100%">{t2}{t3}{t4}</Box>; 163: $[23] = t2; 164: $[24] = t5; 165: } else { 166: t5 = $[24]; 167: } 168: return t5; 169: } 170: if (isLargeFile) { 171: let t2; 172: if ($[25] !== filePath) { 173: t2 = <Box><Text bold={true}>{filePath}</Text></Box>; 174: $[25] = filePath; 175: $[26] = t2; 176: } else { 177: t2 = $[26]; 178: } 179: let t3; 180: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 181: t3 = <Divider padding={4} />; 182: $[27] = t3; 183: } else { 184: t3 = $[27]; 185: } 186: let t4; 187: if ($[28] === Symbol.for("react.memo_cache_sentinel")) { 188: t4 = <Box flexDirection="column"><Text dimColor={true} italic={true}>Large file - diff exceeds 1 MB limit</Text></Box>; 189: $[28] = t4; 190: } else { 191: t4 = $[28]; 192: } 193: let t5; 194: if ($[29] !== t2) { 195: t5 = <Box flexDirection="column" width="100%">{t2}{t3}{t4}</Box>; 196: $[29] = t2; 197: $[30] = t5; 198: } else { 199: t5 = $[30]; 200: } 201: return t5; 202: } 203: let t2; 204: if ($[31] !== filePath) { 205: t2 = <Text bold={true}>{filePath}</Text>; 206: $[31] = filePath; 207: $[32] = t2; 208: } else { 209: t2 = $[32]; 210: } 211: let t3; 212: if ($[33] !== isTruncated) { 213: t3 = isTruncated && <Text dimColor={true}> (truncated)</Text>; 214: $[33] = isTruncated; 215: $[34] = t3; 216: } else { 217: t3 = $[34]; 218: } 219: let t4; 220: if ($[35] !== t2 || $[36] !== t3) { 221: t4 = <Box>{t2}{t3}</Box>; 222: $[35] = t2; 223: $[36] = t3; 224: $[37] = t4; 225: } else { 226: t4 = $[37]; 227: } 228: let t5; 229: if ($[38] === Symbol.for("react.memo_cache_sentinel")) { 230: t5 = <Divider padding={4} />; 231: $[38] = t5; 232: } else { 233: t5 = $[38]; 234: } 235: let t6; 236: if ($[39] !== columns || $[40] !== fileContent || $[41] !== filePath || $[42] !== firstLine || $[43] !== hunks) { 237: t6 = hunks.length === 0 ? <Text dimColor={true}>No diff content</Text> : hunks.map((hunk, index) => <StructuredDiff key={index} patch={hunk} filePath={filePath} firstLine={firstLine} fileContent={fileContent} dim={false} width={columns - 2 - 2} />); 238: $[39] = columns; 239: $[40] = fileContent; 240: $[41] = filePath; 241: $[42] = firstLine; 242: $[43] = hunks; 243: $[44] = t6; 244: } else { 245: t6 = $[44]; 246: } 247: let t7; 248: if ($[45] !== t6) { 249: t7 = <Box flexDirection="column">{t6}</Box>; 250: $[45] = t6; 251: $[46] = t7; 252: } else { 253: t7 = $[46]; 254: } 255: let t8; 256: if ($[47] !== isTruncated) { 257: t8 = isTruncated && <Text dimColor={true} italic={true}>… diff truncated (exceeded 400 line limit)</Text>; 258: $[47] = isTruncated; 259: $[48] = t8; 260: } else { 261: t8 = $[48]; 262: } 263: let t9; 264: if ($[49] !== t4 || $[50] !== t7 || $[51] !== t8) { 265: t9 = <Box flexDirection="column" width="100%">{t4}{t5}{t7}{t8}</Box>; 266: $[49] = t4; 267: $[50] = t7; 268: $[51] = t8; 269: $[52] = t9; 270: } else { 271: t9 = $[52]; 272: } 273: return t9; 274: }

File: src/components/diff/DiffDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { StructuredPatchHunk } from 'diff'; 3: import React, { useEffect, useMemo, useRef, useState } from 'react'; 4: import type { CommandResultDisplay } from '../../commands.js'; 5: import { useRegisterOverlay } from '../../context/overlayContext.js'; 6: import { type DiffData, useDiffData } from '../../hooks/useDiffData.js'; 7: import { type TurnDiff, useTurnDiffs } from '../../hooks/useTurnDiffs.js'; 8: import { Box, Text } from '../../ink.js'; 9: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 10: import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'; 11: import type { Message } from '../../types/message.js'; 12: import { plural } from '../../utils/stringUtils.js'; 13: import { Byline } from '../design-system/Byline.js'; 14: import { Dialog } from '../design-system/Dialog.js'; 15: import { DiffDetailView } from './DiffDetailView.js'; 16: import { DiffFileList } from './DiffFileList.js'; 17: type Props = { 18: messages: Message[]; 19: onDone: (result?: string, options?: { 20: display?: CommandResultDisplay; 21: }) => void; 22: }; 23: type ViewMode = 'list' | 'detail'; 24: type DiffSource = { 25: type: 'current'; 26: } | { 27: type: 'turn'; 28: turn: TurnDiff; 29: }; 30: function turnDiffToDiffData(turn: TurnDiff): DiffData { 31: const files = Array.from(turn.files.values()).map(f => ({ 32: path: f.filePath, 33: linesAdded: f.linesAdded, 34: linesRemoved: f.linesRemoved, 35: isBinary: false, 36: isLargeFile: false, 37: isTruncated: false, 38: isNewFile: f.isNewFile 39: })).sort((a, b) => a.path.localeCompare(b.path)); 40: const hunks = new Map<string, StructuredPatchHunk[]>(); 41: for (const f of turn.files.values()) { 42: hunks.set(f.filePath, f.hunks); 43: } 44: return { 45: stats: { 46: filesCount: turn.stats.filesChanged, 47: linesAdded: turn.stats.linesAdded, 48: linesRemoved: turn.stats.linesRemoved 49: }, 50: files, 51: hunks, 52: loading: false 53: }; 54: } 55: export function DiffDialog(t0) { 56: const $ = _c(73); 57: const { 58: messages, 59: onDone 60: } = t0; 61: const gitDiffData = useDiffData(); 62: const turnDiffs = useTurnDiffs(messages); 63: const [viewMode, setViewMode] = useState("list"); 64: const [selectedIndex, setSelectedIndex] = useState(0); 65: const [sourceIndex, setSourceIndex] = useState(0); 66: let t1; 67: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 68: t1 = { 69: type: "current" 70: }; 71: $[0] = t1; 72: } else { 73: t1 = $[0]; 74: } 75: let t2; 76: if ($[1] !== turnDiffs) { 77: t2 = [t1, ...turnDiffs.map(_temp)]; 78: $[1] = turnDiffs; 79: $[2] = t2; 80: } else { 81: t2 = $[2]; 82: } 83: const sources = t2; 84: const currentSource = sources[sourceIndex]; 85: const currentTurn = currentSource?.type === "turn" ? currentSource.turn : null; 86: let t3; 87: if ($[3] !== currentTurn || $[4] !== gitDiffData) { 88: t3 = currentTurn ? turnDiffToDiffData(currentTurn) : gitDiffData; 89: $[3] = currentTurn; 90: $[4] = gitDiffData; 91: $[5] = t3; 92: } else { 93: t3 = $[5]; 94: } 95: const diffData = t3; 96: const selectedFile = diffData.files[selectedIndex]; 97: let t4; 98: if ($[6] !== diffData.hunks || $[7] !== selectedFile) { 99: t4 = selectedFile ? diffData.hunks.get(selectedFile.path) || [] : []; 100: $[6] = diffData.hunks; 101: $[7] = selectedFile; 102: $[8] = t4; 103: } else { 104: t4 = $[8]; 105: } 106: const selectedHunks = t4; 107: let t5; 108: let t6; 109: if ($[9] !== sourceIndex || $[10] !== sources.length) { 110: t5 = () => { 111: if (sourceIndex >= sources.length) { 112: setSourceIndex(Math.max(0, sources.length - 1)); 113: } 114: }; 115: t6 = [sources.length, sourceIndex]; 116: $[9] = sourceIndex; 117: $[10] = sources.length; 118: $[11] = t5; 119: $[12] = t6; 120: } else { 121: t5 = $[11]; 122: t6 = $[12]; 123: } 124: useEffect(t5, t6); 125: const prevSourceIndex = useRef(sourceIndex); 126: let t7; 127: let t8; 128: if ($[13] !== sourceIndex) { 129: t7 = () => { 130: if (prevSourceIndex.current !== sourceIndex) { 131: setSelectedIndex(0); 132: prevSourceIndex.current = sourceIndex; 133: } 134: }; 135: t8 = [sourceIndex]; 136: $[13] = sourceIndex; 137: $[14] = t7; 138: $[15] = t8; 139: } else { 140: t7 = $[14]; 141: t8 = $[15]; 142: } 143: useEffect(t7, t8); 144: useRegisterOverlay("diff-dialog"); 145: let t10; 146: let t9; 147: if ($[16] !== sources.length || $[17] !== viewMode) { 148: t9 = () => { 149: if (viewMode === "detail") { 150: setViewMode("list"); 151: } else { 152: if (viewMode === "list" && sources.length > 1) { 153: setSourceIndex(_temp2); 154: } 155: } 156: }; 157: t10 = () => { 158: if (viewMode === "list" && sources.length > 1) { 159: setSourceIndex(prev_0 => Math.min(sources.length - 1, prev_0 + 1)); 160: } 161: }; 162: $[16] = sources.length; 163: $[17] = viewMode; 164: $[18] = t10; 165: $[19] = t9; 166: } else { 167: t10 = $[18]; 168: t9 = $[19]; 169: } 170: let t11; 171: if ($[20] !== viewMode) { 172: t11 = () => { 173: if (viewMode === "detail") { 174: setViewMode("list"); 175: } 176: }; 177: $[20] = viewMode; 178: $[21] = t11; 179: } else { 180: t11 = $[21]; 181: } 182: let t12; 183: if ($[22] !== selectedFile || $[23] !== viewMode) { 184: t12 = () => { 185: if (viewMode === "list" && selectedFile) { 186: setViewMode("detail"); 187: } 188: }; 189: $[22] = selectedFile; 190: $[23] = viewMode; 191: $[24] = t12; 192: } else { 193: t12 = $[24]; 194: } 195: let t13; 196: if ($[25] !== viewMode) { 197: t13 = () => { 198: if (viewMode === "list") { 199: setSelectedIndex(_temp3); 200: } 201: }; 202: $[25] = viewMode; 203: $[26] = t13; 204: } else { 205: t13 = $[26]; 206: } 207: let t14; 208: if ($[27] !== diffData.files.length || $[28] !== viewMode) { 209: t14 = () => { 210: if (viewMode === "list") { 211: setSelectedIndex(prev_2 => Math.min(diffData.files.length - 1, prev_2 + 1)); 212: } 213: }; 214: $[27] = diffData.files.length; 215: $[28] = viewMode; 216: $[29] = t14; 217: } else { 218: t14 = $[29]; 219: } 220: let t15; 221: if ($[30] !== t10 || $[31] !== t11 || $[32] !== t12 || $[33] !== t13 || $[34] !== t14 || $[35] !== t9) { 222: t15 = { 223: "diff:previousSource": t9, 224: "diff:nextSource": t10, 225: "diff:back": t11, 226: "diff:viewDetails": t12, 227: "diff:previousFile": t13, 228: "diff:nextFile": t14 229: }; 230: $[30] = t10; 231: $[31] = t11; 232: $[32] = t12; 233: $[33] = t13; 234: $[34] = t14; 235: $[35] = t9; 236: $[36] = t15; 237: } else { 238: t15 = $[36]; 239: } 240: let t16; 241: if ($[37] === Symbol.for("react.memo_cache_sentinel")) { 242: t16 = { 243: context: "DiffDialog" 244: }; 245: $[37] = t16; 246: } else { 247: t16 = $[37]; 248: } 249: useKeybindings(t15, t16); 250: let t17; 251: if ($[38] !== diffData.stats) { 252: t17 = diffData.stats ? <Text dimColor={true}>{diffData.stats.filesCount} {plural(diffData.stats.filesCount, "file")}{" "}changed{diffData.stats.linesAdded > 0 && <Text color="diffAddedWord"> +{diffData.stats.linesAdded}</Text>}{diffData.stats.linesRemoved > 0 && <Text color="diffRemovedWord"> -{diffData.stats.linesRemoved}</Text>}</Text> : null; 253: $[38] = diffData.stats; 254: $[39] = t17; 255: } else { 256: t17 = $[39]; 257: } 258: const subtitle = t17; 259: const headerTitle = currentTurn ? `Turn ${currentTurn.turnIndex}` : "Uncommitted changes"; 260: const headerSubtitle = currentTurn ? currentTurn.userPromptPreview ? `"${currentTurn.userPromptPreview}"` : "" : "(git diff HEAD)"; 261: let t18; 262: if ($[40] !== sourceIndex || $[41] !== sources) { 263: t18 = sources.length > 1 ? <Box>{sourceIndex > 0 && <Text dimColor={true}>◀ </Text>}{sources.map((source, i) => { 264: const isSelected = i === sourceIndex; 265: const label = source.type === "current" ? "Current" : `T${source.turn.turnIndex}`; 266: return <Text key={i} dimColor={!isSelected} bold={isSelected}>{i > 0 ? " \xB7 " : ""}{label}</Text>; 267: })}{sourceIndex < sources.length - 1 && <Text dimColor={true}> ▶</Text>}</Box> : null; 268: $[40] = sourceIndex; 269: $[41] = sources; 270: $[42] = t18; 271: } else { 272: t18 = $[42]; 273: } 274: const sourceSelector = t18; 275: const dismissShortcut = useShortcutDisplay("diff:dismiss", "DiffDialog", "esc"); 276: let t19; 277: bb0: { 278: if (diffData.loading) { 279: t19 = "Loading diff\u2026"; 280: break bb0; 281: } 282: if (currentTurn) { 283: t19 = "No file changes in this turn"; 284: break bb0; 285: } 286: if (diffData.stats && diffData.stats.filesCount > 0 && diffData.files.length === 0) { 287: t19 = "Too many files to display details"; 288: break bb0; 289: } 290: t19 = "Working tree is clean"; 291: } 292: const emptyMessage = t19; 293: let t20; 294: if ($[43] !== headerSubtitle) { 295: t20 = headerSubtitle && <Text dimColor={true}> {headerSubtitle}</Text>; 296: $[43] = headerSubtitle; 297: $[44] = t20; 298: } else { 299: t20 = $[44]; 300: } 301: let t21; 302: if ($[45] !== headerTitle || $[46] !== t20) { 303: t21 = <Text>{headerTitle}{t20}</Text>; 304: $[45] = headerTitle; 305: $[46] = t20; 306: $[47] = t21; 307: } else { 308: t21 = $[47]; 309: } 310: const title = t21; 311: let t22; 312: if ($[48] !== onDone || $[49] !== viewMode) { 313: t22 = function handleCancel() { 314: if (viewMode === "detail") { 315: setViewMode("list"); 316: } else { 317: onDone("Diff dialog dismissed", { 318: display: "system" 319: }); 320: } 321: }; 322: $[48] = onDone; 323: $[49] = viewMode; 324: $[50] = t22; 325: } else { 326: t22 = $[50]; 327: } 328: const handleCancel = t22; 329: let t23; 330: if ($[51] !== dismissShortcut || $[52] !== sources.length || $[53] !== viewMode) { 331: t23 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : viewMode === "list" ? <Byline>{sources.length > 1 && <Text>←/→ source</Text>}<Text>↑/↓ select</Text><Text>Enter view</Text><Text>{dismissShortcut} close</Text></Byline> : <Byline><Text>← back</Text><Text>{dismissShortcut} close</Text></Byline>; 332: $[51] = dismissShortcut; 333: $[52] = sources.length; 334: $[53] = viewMode; 335: $[54] = t23; 336: } else { 337: t23 = $[54]; 338: } 339: let t24; 340: if ($[55] !== diffData.files || $[56] !== emptyMessage || $[57] !== selectedFile?.isBinary || $[58] !== selectedFile?.isLargeFile || $[59] !== selectedFile?.isTruncated || $[60] !== selectedFile?.isUntracked || $[61] !== selectedFile?.path || $[62] !== selectedHunks || $[63] !== selectedIndex || $[64] !== viewMode) { 341: t24 = diffData.files.length === 0 ? <Box marginTop={1}><Text dimColor={true}>{emptyMessage}</Text></Box> : viewMode === "list" ? <Box flexDirection="column" marginTop={1}><DiffFileList files={diffData.files} selectedIndex={selectedIndex} /></Box> : <Box flexDirection="column" marginTop={1}><DiffDetailView filePath={selectedFile?.path || ""} hunks={selectedHunks} isLargeFile={selectedFile?.isLargeFile} isBinary={selectedFile?.isBinary} isTruncated={selectedFile?.isTruncated} isUntracked={selectedFile?.isUntracked} /></Box>; 342: $[55] = diffData.files; 343: $[56] = emptyMessage; 344: $[57] = selectedFile?.isBinary; 345: $[58] = selectedFile?.isLargeFile; 346: $[59] = selectedFile?.isTruncated; 347: $[60] = selectedFile?.isUntracked; 348: $[61] = selectedFile?.path; 349: $[62] = selectedHunks; 350: $[63] = selectedIndex; 351: $[64] = viewMode; 352: $[65] = t24; 353: } else { 354: t24 = $[65]; 355: } 356: let t25; 357: if ($[66] !== handleCancel || $[67] !== sourceSelector || $[68] !== subtitle || $[69] !== t23 || $[70] !== t24 || $[71] !== title) { 358: t25 = <Dialog title={title} onCancel={handleCancel} color="background" inputGuide={t23}>{sourceSelector}{subtitle}{t24}</Dialog>; 359: $[66] = handleCancel; 360: $[67] = sourceSelector; 361: $[68] = subtitle; 362: $[69] = t23; 363: $[70] = t24; 364: $[71] = title; 365: $[72] = t25; 366: } else { 367: t25 = $[72]; 368: } 369: return t25; 370: } 371: function _temp3(prev_1) { 372: return Math.max(0, prev_1 - 1); 373: } 374: function _temp2(prev) { 375: return Math.max(0, prev - 1); 376: } 377: function _temp(turn) { 378: return { 379: type: "turn", 380: turn 381: }; 382: }

File: src/components/diff/DiffFileList.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React, { useMemo } from 'react'; 4: import type { DiffFile } from '../../hooks/useDiffData.js'; 5: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 6: import { Box, Text } from '../../ink.js'; 7: import { truncateStartToWidth } from '../../utils/format.js'; 8: import { plural } from '../../utils/stringUtils.js'; 9: const MAX_VISIBLE_FILES = 5; 10: type Props = { 11: files: DiffFile[]; 12: selectedIndex: number; 13: }; 14: export function DiffFileList(t0) { 15: const $ = _c(36); 16: const { 17: files, 18: selectedIndex 19: } = t0; 20: const { 21: columns 22: } = useTerminalSize(); 23: let t1; 24: bb0: { 25: if (files.length === 0 || files.length <= MAX_VISIBLE_FILES) { 26: let t2; 27: if ($[0] !== files.length) { 28: t2 = { 29: startIndex: 0, 30: endIndex: files.length 31: }; 32: $[0] = files.length; 33: $[1] = t2; 34: } else { 35: t2 = $[1]; 36: } 37: t1 = t2; 38: break bb0; 39: } 40: let start = Math.max(0, selectedIndex - Math.floor(MAX_VISIBLE_FILES / 2)); 41: let end = start + MAX_VISIBLE_FILES; 42: if (end > files.length) { 43: end = files.length; 44: start = Math.max(0, end - MAX_VISIBLE_FILES); 45: } 46: let t2; 47: if ($[2] !== end || $[3] !== start) { 48: t2 = { 49: startIndex: start, 50: endIndex: end 51: }; 52: $[2] = end; 53: $[3] = start; 54: $[4] = t2; 55: } else { 56: t2 = $[4]; 57: } 58: t1 = t2; 59: } 60: const { 61: startIndex, 62: endIndex 63: } = t1; 64: if (files.length === 0) { 65: let t2; 66: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 67: t2 = <Text dimColor={true}>No changed files</Text>; 68: $[5] = t2; 69: } else { 70: t2 = $[5]; 71: } 72: return t2; 73: } 74: let T0; 75: let hasMoreBelow; 76: let needsPagination; 77: let t2; 78: let t3; 79: let t4; 80: if ($[6] !== columns || $[7] !== endIndex || $[8] !== files || $[9] !== selectedIndex || $[10] !== startIndex) { 81: const visibleFiles = files.slice(startIndex, endIndex); 82: const hasMoreAbove = startIndex > 0; 83: hasMoreBelow = endIndex < files.length; 84: needsPagination = files.length > MAX_VISIBLE_FILES; 85: const maxPathWidth = Math.max(20, columns - 16 - 3 - 4); 86: T0 = Box; 87: t2 = "column"; 88: if ($[17] !== hasMoreAbove || $[18] !== needsPagination || $[19] !== startIndex) { 89: t3 = needsPagination && <Text dimColor={true}>{hasMoreAbove ? ` ↑ ${startIndex} more ${plural(startIndex, "file")}` : " "}</Text>; 90: $[17] = hasMoreAbove; 91: $[18] = needsPagination; 92: $[19] = startIndex; 93: $[20] = t3; 94: } else { 95: t3 = $[20]; 96: } 97: let t5; 98: if ($[21] !== maxPathWidth || $[22] !== selectedIndex || $[23] !== startIndex) { 99: t5 = (file, index) => <FileItem key={file.path} file={file} isSelected={startIndex + index === selectedIndex} maxPathWidth={maxPathWidth} />; 100: $[21] = maxPathWidth; 101: $[22] = selectedIndex; 102: $[23] = startIndex; 103: $[24] = t5; 104: } else { 105: t5 = $[24]; 106: } 107: t4 = visibleFiles.map(t5); 108: $[6] = columns; 109: $[7] = endIndex; 110: $[8] = files; 111: $[9] = selectedIndex; 112: $[10] = startIndex; 113: $[11] = T0; 114: $[12] = hasMoreBelow; 115: $[13] = needsPagination; 116: $[14] = t2; 117: $[15] = t3; 118: $[16] = t4; 119: } else { 120: T0 = $[11]; 121: hasMoreBelow = $[12]; 122: needsPagination = $[13]; 123: t2 = $[14]; 124: t3 = $[15]; 125: t4 = $[16]; 126: } 127: let t5; 128: if ($[25] !== endIndex || $[26] !== files.length || $[27] !== hasMoreBelow || $[28] !== needsPagination) { 129: t5 = needsPagination && <Text dimColor={true}>{hasMoreBelow ? ` ↓ ${files.length - endIndex} more ${plural(files.length - endIndex, "file")}` : " "}</Text>; 130: $[25] = endIndex; 131: $[26] = files.length; 132: $[27] = hasMoreBelow; 133: $[28] = needsPagination; 134: $[29] = t5; 135: } else { 136: t5 = $[29]; 137: } 138: let t6; 139: if ($[30] !== T0 || $[31] !== t2 || $[32] !== t3 || $[33] !== t4 || $[34] !== t5) { 140: t6 = <T0 flexDirection={t2}>{t3}{t4}{t5}</T0>; 141: $[30] = T0; 142: $[31] = t2; 143: $[32] = t3; 144: $[33] = t4; 145: $[34] = t5; 146: $[35] = t6; 147: } else { 148: t6 = $[35]; 149: } 150: return t6; 151: } 152: function FileItem(t0) { 153: const $ = _c(14); 154: const { 155: file, 156: isSelected, 157: maxPathWidth 158: } = t0; 159: let t1; 160: if ($[0] !== file.path || $[1] !== maxPathWidth) { 161: t1 = truncateStartToWidth(file.path, maxPathWidth); 162: $[0] = file.path; 163: $[1] = maxPathWidth; 164: $[2] = t1; 165: } else { 166: t1 = $[2]; 167: } 168: const displayPath = t1; 169: const pointer = isSelected ? figures.pointer + " " : " "; 170: const line = `${pointer}${displayPath}`; 171: const t2 = isSelected ? "background" : undefined; 172: let t3; 173: if ($[3] !== isSelected || $[4] !== line || $[5] !== t2) { 174: t3 = <Text bold={isSelected} color={t2} inverse={isSelected}>{line}</Text>; 175: $[3] = isSelected; 176: $[4] = line; 177: $[5] = t2; 178: $[6] = t3; 179: } else { 180: t3 = $[6]; 181: } 182: let t4; 183: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 184: t4 = <Box flexGrow={1} />; 185: $[7] = t4; 186: } else { 187: t4 = $[7]; 188: } 189: let t5; 190: if ($[8] !== file || $[9] !== isSelected) { 191: t5 = <FileStats file={file} isSelected={isSelected} />; 192: $[8] = file; 193: $[9] = isSelected; 194: $[10] = t5; 195: } else { 196: t5 = $[10]; 197: } 198: let t6; 199: if ($[11] !== t3 || $[12] !== t5) { 200: t6 = <Box flexDirection="row">{t3}{t4}{t5}</Box>; 201: $[11] = t3; 202: $[12] = t5; 203: $[13] = t6; 204: } else { 205: t6 = $[13]; 206: } 207: return t6; 208: } 209: function FileStats(t0) { 210: const $ = _c(20); 211: const { 212: file, 213: isSelected 214: } = t0; 215: if (file.isUntracked) { 216: const t1 = !isSelected; 217: let t2; 218: if ($[0] !== t1) { 219: t2 = <Text dimColor={t1} italic={true}>untracked</Text>; 220: $[0] = t1; 221: $[1] = t2; 222: } else { 223: t2 = $[1]; 224: } 225: return t2; 226: } 227: if (file.isBinary) { 228: const t1 = !isSelected; 229: let t2; 230: if ($[2] !== t1) { 231: t2 = <Text dimColor={t1} italic={true}>Binary file</Text>; 232: $[2] = t1; 233: $[3] = t2; 234: } else { 235: t2 = $[3]; 236: } 237: return t2; 238: } 239: if (file.isLargeFile) { 240: const t1 = !isSelected; 241: let t2; 242: if ($[4] !== t1) { 243: t2 = <Text dimColor={t1} italic={true}>Large file modified</Text>; 244: $[4] = t1; 245: $[5] = t2; 246: } else { 247: t2 = $[5]; 248: } 249: return t2; 250: } 251: let t1; 252: if ($[6] !== file.linesAdded || $[7] !== isSelected) { 253: t1 = file.linesAdded > 0 && <Text color="diffAddedWord" bold={isSelected}>+{file.linesAdded}</Text>; 254: $[6] = file.linesAdded; 255: $[7] = isSelected; 256: $[8] = t1; 257: } else { 258: t1 = $[8]; 259: } 260: const t2 = file.linesAdded > 0 && file.linesRemoved > 0 && " "; 261: let t3; 262: if ($[9] !== file.linesRemoved || $[10] !== isSelected) { 263: t3 = file.linesRemoved > 0 && <Text color="diffRemovedWord" bold={isSelected}>-{file.linesRemoved}</Text>; 264: $[9] = file.linesRemoved; 265: $[10] = isSelected; 266: $[11] = t3; 267: } else { 268: t3 = $[11]; 269: } 270: let t4; 271: if ($[12] !== file.isTruncated || $[13] !== isSelected) { 272: t4 = file.isTruncated && <Text dimColor={!isSelected}> (truncated)</Text>; 273: $[12] = file.isTruncated; 274: $[13] = isSelected; 275: $[14] = t4; 276: } else { 277: t4 = $[14]; 278: } 279: let t5; 280: if ($[15] !== t1 || $[16] !== t2 || $[17] !== t3 || $[18] !== t4) { 281: t5 = <Text>{t1}{t2}{t3}{t4}</Text>; 282: $[15] = t1; 283: $[16] = t2; 284: $[17] = t3; 285: $[18] = t4; 286: $[19] = t5; 287: } else { 288: t5 = $[19]; 289: } 290: return t5; 291: }

File: src/components/FeedbackSurvey/FeedbackSurvey.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; 4: import { Box, Text } from '../../ink.js'; 5: import { FeedbackSurveyView, isValidResponseInput } from './FeedbackSurveyView.js'; 6: import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'; 7: import { TranscriptSharePrompt } from './TranscriptSharePrompt.js'; 8: import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'; 9: import type { FeedbackSurveyResponse } from './utils.js'; 10: type Props = { 11: state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted'; 12: lastResponse: FeedbackSurveyResponse | null; 13: handleSelect: (selected: FeedbackSurveyResponse) => void; 14: handleTranscriptSelect?: (selected: TranscriptShareResponse) => void; 15: inputValue: string; 16: setInputValue: (value: string) => void; 17: onRequestFeedback?: () => void; 18: message?: string; 19: }; 20: export function FeedbackSurvey(t0) { 21: const $ = _c(16); 22: const { 23: state, 24: lastResponse, 25: handleSelect, 26: handleTranscriptSelect, 27: inputValue, 28: setInputValue, 29: onRequestFeedback, 30: message 31: } = t0; 32: if (state === "closed") { 33: return null; 34: } 35: if (state === "thanks") { 36: let t1; 37: if ($[0] !== inputValue || $[1] !== lastResponse || $[2] !== onRequestFeedback || $[3] !== setInputValue) { 38: t1 = <FeedbackSurveyThanks lastResponse={lastResponse} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={onRequestFeedback} />; 39: $[0] = inputValue; 40: $[1] = lastResponse; 41: $[2] = onRequestFeedback; 42: $[3] = setInputValue; 43: $[4] = t1; 44: } else { 45: t1 = $[4]; 46: } 47: return t1; 48: } 49: if (state === "submitted") { 50: let t1; 51: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 52: t1 = <Box marginTop={1}><Text color="success">{"\u2713"} Thanks for sharing your transcript!</Text></Box>; 53: $[5] = t1; 54: } else { 55: t1 = $[5]; 56: } 57: return t1; 58: } 59: if (state === "submitting") { 60: let t1; 61: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 62: t1 = <Box marginTop={1}><Text dimColor={true}>Sharing transcript{"\u2026"}</Text></Box>; 63: $[6] = t1; 64: } else { 65: t1 = $[6]; 66: } 67: return t1; 68: } 69: if (state === "transcript_prompt") { 70: if (!handleTranscriptSelect) { 71: return null; 72: } 73: if (inputValue && !["1", "2", "3"].includes(inputValue)) { 74: return null; 75: } 76: let t1; 77: if ($[7] !== handleTranscriptSelect || $[8] !== inputValue || $[9] !== setInputValue) { 78: t1 = <TranscriptSharePrompt onSelect={handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />; 79: $[7] = handleTranscriptSelect; 80: $[8] = inputValue; 81: $[9] = setInputValue; 82: $[10] = t1; 83: } else { 84: t1 = $[10]; 85: } 86: return t1; 87: } 88: if (inputValue && !isValidResponseInput(inputValue)) { 89: return null; 90: } 91: let t1; 92: if ($[11] !== handleSelect || $[12] !== inputValue || $[13] !== message || $[14] !== setInputValue) { 93: t1 = <FeedbackSurveyView onSelect={handleSelect} inputValue={inputValue} setInputValue={setInputValue} message={message} />; 94: $[11] = handleSelect; 95: $[12] = inputValue; 96: $[13] = message; 97: $[14] = setInputValue; 98: $[15] = t1; 99: } else { 100: t1 = $[15]; 101: } 102: return t1; 103: } 104: type ThanksProps = { 105: lastResponse: FeedbackSurveyResponse | null; 106: inputValue: string; 107: setInputValue: (value: string) => void; 108: onRequestFeedback?: () => void; 109: }; 110: const isFollowUpDigit = (char: string): char is '1' => char === '1'; 111: function FeedbackSurveyThanks(t0) { 112: const $ = _c(12); 113: const { 114: lastResponse, 115: inputValue, 116: setInputValue, 117: onRequestFeedback 118: } = t0; 119: const showFollowUp = onRequestFeedback && lastResponse === "good"; 120: const t1 = Boolean(showFollowUp); 121: let t2; 122: if ($[0] !== lastResponse || $[1] !== onRequestFeedback) { 123: t2 = () => { 124: logEvent("tengu_feedback_survey_event", { 125: event_type: "followup_accepted" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 126: response: lastResponse as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 127: }); 128: onRequestFeedback?.(); 129: }; 130: $[0] = lastResponse; 131: $[1] = onRequestFeedback; 132: $[2] = t2; 133: } else { 134: t2 = $[2]; 135: } 136: let t3; 137: if ($[3] !== inputValue || $[4] !== setInputValue || $[5] !== t1 || $[6] !== t2) { 138: t3 = { 139: inputValue, 140: setInputValue, 141: isValidDigit: isFollowUpDigit, 142: enabled: t1, 143: once: true, 144: onDigit: t2 145: }; 146: $[3] = inputValue; 147: $[4] = setInputValue; 148: $[5] = t1; 149: $[6] = t2; 150: $[7] = t3; 151: } else { 152: t3 = $[7]; 153: } 154: useDebouncedDigitInput(t3); 155: const feedbackCommand = false ? "/issue" : "/feedback"; 156: let t4; 157: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 158: t4 = <Text color="success">Thanks for the feedback!</Text>; 159: $[8] = t4; 160: } else { 161: t4 = $[8]; 162: } 163: let t5; 164: if ($[9] !== lastResponse || $[10] !== showFollowUp) { 165: t5 = <Box marginTop={1} flexDirection="column">{t4}{showFollowUp ? <Text dimColor={true}>(Optional) Press [<Text color="ansi:cyan">1</Text>] to tell us what went well {" \xB7 "}{feedbackCommand}</Text> : lastResponse === "bad" ? <Text dimColor={true}>Use /issue to report model behavior issues.</Text> : <Text dimColor={true}>Use {feedbackCommand} to share detailed feedback anytime.</Text>}</Box>; 166: $[9] = lastResponse; 167: $[10] = showFollowUp; 168: $[11] = t5; 169: } else { 170: t5 = $[11]; 171: } 172: return t5; 173: }

File: src/components/FeedbackSurvey/FeedbackSurveyView.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'; 5: import type { FeedbackSurveyResponse } from './utils.js'; 6: type Props = { 7: onSelect: (option: FeedbackSurveyResponse) => void; 8: inputValue: string; 9: setInputValue: (value: string) => void; 10: message?: string; 11: }; 12: const RESPONSE_INPUTS = ['0', '1', '2', '3'] as const; 13: type ResponseInput = (typeof RESPONSE_INPUTS)[number]; 14: const inputToResponse: Record<ResponseInput, FeedbackSurveyResponse> = { 15: '0': 'dismissed', 16: '1': 'bad', 17: '2': 'fine', 18: '3': 'good' 19: } as const; 20: export const isValidResponseInput = (input: string): input is ResponseInput => (RESPONSE_INPUTS as readonly string[]).includes(input); 21: const DEFAULT_MESSAGE = 'How is Claude doing this session? (optional)'; 22: export function FeedbackSurveyView(t0) { 23: const $ = _c(15); 24: const { 25: onSelect, 26: inputValue, 27: setInputValue, 28: message: t1 29: } = t0; 30: const message = t1 === undefined ? DEFAULT_MESSAGE : t1; 31: let t2; 32: if ($[0] !== onSelect) { 33: t2 = digit => onSelect(inputToResponse[digit]); 34: $[0] = onSelect; 35: $[1] = t2; 36: } else { 37: t2 = $[1]; 38: } 39: let t3; 40: if ($[2] !== inputValue || $[3] !== setInputValue || $[4] !== t2) { 41: t3 = { 42: inputValue, 43: setInputValue, 44: isValidDigit: isValidResponseInput, 45: onDigit: t2 46: }; 47: $[2] = inputValue; 48: $[3] = setInputValue; 49: $[4] = t2; 50: $[5] = t3; 51: } else { 52: t3 = $[5]; 53: } 54: useDebouncedDigitInput(t3); 55: let t4; 56: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 57: t4 = <Text color="ansi:cyan">● </Text>; 58: $[6] = t4; 59: } else { 60: t4 = $[6]; 61: } 62: let t5; 63: if ($[7] !== message) { 64: t5 = <Box>{t4}<Text bold={true}>{message}</Text></Box>; 65: $[7] = message; 66: $[8] = t5; 67: } else { 68: t5 = $[8]; 69: } 70: let t6; 71: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 72: t6 = <Box width={10}><Text><Text color="ansi:cyan">1</Text>: Bad</Text></Box>; 73: $[9] = t6; 74: } else { 75: t6 = $[9]; 76: } 77: let t7; 78: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 79: t7 = <Box width={10}><Text><Text color="ansi:cyan">2</Text>: Fine</Text></Box>; 80: $[10] = t7; 81: } else { 82: t7 = $[10]; 83: } 84: let t8; 85: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 86: t8 = <Box width={10}><Text><Text color="ansi:cyan">3</Text>: Good</Text></Box>; 87: $[11] = t8; 88: } else { 89: t8 = $[11]; 90: } 91: let t9; 92: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 93: t9 = <Box marginLeft={2}>{t6}{t7}{t8}<Box><Text><Text color="ansi:cyan">0</Text>: Dismiss</Text></Box></Box>; 94: $[12] = t9; 95: } else { 96: t9 = $[12]; 97: } 98: let t10; 99: if ($[13] !== t5) { 100: t10 = <Box flexDirection="column" marginTop={1}>{t5}{t9}</Box>; 101: $[13] = t5; 102: $[14] = t10; 103: } else { 104: t10 = $[14]; 105: } 106: return t10; 107: }

File: src/components/FeedbackSurvey/submitTranscriptShare.ts

typescript 1: import axios from 'axios' 2: import { readFile, stat } from 'fs/promises' 3: import type { Message } from '../../types/message.js' 4: import { checkAndRefreshOAuthTokenIfNeeded } from '../../utils/auth.js' 5: import { logForDebugging } from '../../utils/debug.js' 6: import { errorMessage } from '../../utils/errors.js' 7: import { getAuthHeaders, getUserAgent } from '../../utils/http.js' 8: import { normalizeMessagesForAPI } from '../../utils/messages.js' 9: import { 10: extractAgentIdsFromMessages, 11: getTranscriptPath, 12: loadSubagentTranscripts, 13: MAX_TRANSCRIPT_READ_BYTES, 14: } from '../../utils/sessionStorage.js' 15: import { jsonStringify } from '../../utils/slowOperations.js' 16: import { redactSensitiveInfo } from '../Feedback.js' 17: type TranscriptShareResult = { 18: success: boolean 19: transcriptId?: string 20: } 21: export type TranscriptShareTrigger = 22: | 'bad_feedback_survey' 23: | 'good_feedback_survey' 24: | 'frustration' 25: | 'memory_survey' 26: export async function submitTranscriptShare( 27: messages: Message[], 28: trigger: TranscriptShareTrigger, 29: appearanceId: string, 30: ): Promise<TranscriptShareResult> { 31: try { 32: logForDebugging('Collecting transcript for sharing', { level: 'info' }) 33: const transcript = normalizeMessagesForAPI(messages) 34: const agentIds = extractAgentIdsFromMessages(messages) 35: const subagentTranscripts = await loadSubagentTranscripts(agentIds) 36: let rawTranscriptJsonl: string | undefined 37: try { 38: const transcriptPath = getTranscriptPath() 39: const { size } = await stat(transcriptPath) 40: if (size <= MAX_TRANSCRIPT_READ_BYTES) { 41: rawTranscriptJsonl = await readFile(transcriptPath, 'utf-8') 42: } else { 43: logForDebugging( 44: `Skipping raw transcript read: file too large (${size} bytes)`, 45: { level: 'warn' }, 46: ) 47: } 48: } catch { 49: } 50: const data = { 51: trigger, 52: version: MACRO.VERSION, 53: platform: process.platform, 54: transcript, 55: subagentTranscripts: 56: Object.keys(subagentTranscripts).length > 0 57: ? subagentTranscripts 58: : undefined, 59: rawTranscriptJsonl, 60: } 61: const content = redactSensitiveInfo(jsonStringify(data)) 62: await checkAndRefreshOAuthTokenIfNeeded() 63: const authResult = getAuthHeaders() 64: if (authResult.error) { 65: return { success: false } 66: } 67: const headers: Record<string, string> = { 68: 'Content-Type': 'application/json', 69: 'User-Agent': getUserAgent(), 70: ...authResult.headers, 71: } 72: const response = await axios.post( 73: 'https://api.anthropic.com/api/claude_code_shared_session_transcripts', 74: { content, appearance_id: appearanceId }, 75: { 76: headers, 77: timeout: 30000, 78: }, 79: ) 80: if (response.status === 200 || response.status === 201) { 81: const result = response.data 82: logForDebugging('Transcript shared successfully', { level: 'info' }) 83: return { 84: success: true, 85: transcriptId: result?.transcript_id, 86: } 87: } 88: return { success: false } 89: } catch (err) { 90: logForDebugging(errorMessage(err), { 91: level: 'error', 92: }) 93: return { success: false } 94: } 95: }

File: src/components/FeedbackSurvey/TranscriptSharePrompt.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { BLACK_CIRCLE } from '../../constants/figures.js'; 4: import { Box, Text } from '../../ink.js'; 5: import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'; 6: export type TranscriptShareResponse = 'yes' | 'no' | 'dont_ask_again'; 7: type Props = { 8: onSelect: (option: TranscriptShareResponse) => void; 9: inputValue: string; 10: setInputValue: (value: string) => void; 11: }; 12: const RESPONSE_INPUTS = ['1', '2', '3'] as const; 13: type ResponseInput = (typeof RESPONSE_INPUTS)[number]; 14: const inputToResponse: Record<ResponseInput, TranscriptShareResponse> = { 15: '1': 'yes', 16: '2': 'no', 17: '3': 'dont_ask_again' 18: } as const; 19: const isValidResponseInput = (input: string): input is ResponseInput => (RESPONSE_INPUTS as readonly string[]).includes(input); 20: export function TranscriptSharePrompt(t0) { 21: const $ = _c(11); 22: const { 23: onSelect, 24: inputValue, 25: setInputValue 26: } = t0; 27: let t1; 28: if ($[0] !== onSelect) { 29: t1 = digit => onSelect(inputToResponse[digit]); 30: $[0] = onSelect; 31: $[1] = t1; 32: } else { 33: t1 = $[1]; 34: } 35: let t2; 36: if ($[2] !== inputValue || $[3] !== setInputValue || $[4] !== t1) { 37: t2 = { 38: inputValue, 39: setInputValue, 40: isValidDigit: isValidResponseInput, 41: onDigit: t1 42: }; 43: $[2] = inputValue; 44: $[3] = setInputValue; 45: $[4] = t1; 46: $[5] = t2; 47: } else { 48: t2 = $[5]; 49: } 50: useDebouncedDigitInput(t2); 51: let t3; 52: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 53: t3 = <Box><Text color="ansi:cyan">{BLACK_CIRCLE} </Text><Text bold={true}>Can Anthropic look at your session transcript to help us improve Claude Code?</Text></Box>; 54: $[6] = t3; 55: } else { 56: t3 = $[6]; 57: } 58: let t4; 59: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 60: t4 = <Box marginLeft={2}><Text dimColor={true}>Learn more: https: 61: $[7] = t4; 62: } else { 63: t4 = $[7]; 64: } 65: let t5; 66: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 67: t5 = <Box width={10}><Text><Text color="ansi:cyan">1</Text>: Yes</Text></Box>; 68: $[8] = t5; 69: } else { 70: t5 = $[8]; 71: } 72: let t6; 73: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 74: t6 = <Box width={10}><Text><Text color="ansi:cyan">2</Text>: No</Text></Box>; 75: $[9] = t6; 76: } else { 77: t6 = $[9]; 78: } 79: let t7; 80: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 81: t7 = <Box flexDirection="column" marginTop={1}>{t3}{t4}<Box marginLeft={2}>{t5}{t6}<Box><Text><Text color="ansi:cyan">3</Text>: Don't ask again</Text></Box></Box></Box>; 82: $[10] = t7; 83: } else { 84: t7 = $[10]; 85: } 86: return t7; 87: }

File: src/components/FeedbackSurvey/useDebouncedDigitInput.ts

typescript 1: import { useEffect, useRef } from 'react' 2: import { normalizeFullWidthDigits } from '../../utils/stringUtils.js' 3: const DEFAULT_DEBOUNCE_MS = 400 4: export function useDebouncedDigitInput<T extends string = string>({ 5: inputValue, 6: setInputValue, 7: isValidDigit, 8: onDigit, 9: enabled = true, 10: once = false, 11: debounceMs = DEFAULT_DEBOUNCE_MS, 12: }: { 13: inputValue: string 14: setInputValue: (value: string) => void 15: isValidDigit: (char: string) => char is T 16: onDigit: (digit: T) => void 17: enabled?: boolean 18: once?: boolean 19: debounceMs?: number 20: }): void { 21: const initialInputValue = useRef(inputValue) 22: const hasTriggeredRef = useRef(false) 23: const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null) 24: const callbacksRef = useRef({ setInputValue, isValidDigit, onDigit }) 25: callbacksRef.current = { setInputValue, isValidDigit, onDigit } 26: useEffect(() => { 27: if (!enabled || (once && hasTriggeredRef.current)) { 28: return 29: } 30: if (debounceRef.current !== null) { 31: clearTimeout(debounceRef.current) 32: debounceRef.current = null 33: } 34: if (inputValue !== initialInputValue.current) { 35: const lastChar = normalizeFullWidthDigits(inputValue.slice(-1)) 36: if (callbacksRef.current.isValidDigit(lastChar)) { 37: const trimmed = inputValue.slice(0, -1) 38: debounceRef.current = setTimeout( 39: (debounceRef, hasTriggeredRef, callbacksRef, trimmed, lastChar) => { 40: debounceRef.current = null 41: hasTriggeredRef.current = true 42: callbacksRef.current.setInputValue(trimmed) 43: callbacksRef.current.onDigit(lastChar) 44: }, 45: debounceMs, 46: debounceRef, 47: hasTriggeredRef, 48: callbacksRef, 49: trimmed, 50: lastChar, 51: ) 52: } 53: } 54: return () => { 55: if (debounceRef.current !== null) { 56: clearTimeout(debounceRef.current) 57: debounceRef.current = null 58: } 59: } 60: }, [inputValue, enabled, once, debounceMs]) 61: }

File: src/components/FeedbackSurvey/useFeedbackSurvey.tsx

typescript 1: import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 2: import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js'; 3: import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'; 4: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; 5: import { isPolicyAllowed } from '../../services/policyLimits/index.js'; 6: import type { Message } from '../../types/message.js'; 7: import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; 8: import { isEnvTruthy } from '../../utils/envUtils.js'; 9: import { getLastAssistantMessage } from '../../utils/messages.js'; 10: import { getMainLoopModel } from '../../utils/model/model.js'; 11: import { getInitialSettings } from '../../utils/settings/settings.js'; 12: import { logOTelEvent } from '../../utils/telemetry/events.js'; 13: import { submitTranscriptShare, type TranscriptShareTrigger } from './submitTranscriptShare.js'; 14: import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'; 15: import { useSurveyState } from './useSurveyState.js'; 16: import type { FeedbackSurveyResponse, FeedbackSurveyType } from './utils.js'; 17: type FeedbackSurveyConfig = { 18: minTimeBeforeFeedbackMs: number; 19: minTimeBetweenFeedbackMs: number; 20: minTimeBetweenGlobalFeedbackMs: number; 21: minUserTurnsBeforeFeedback: number; 22: minUserTurnsBetweenFeedback: number; 23: hideThanksAfterMs: number; 24: onForModels: string[]; 25: probability: number; 26: }; 27: type TranscriptAskConfig = { 28: probability: number; 29: }; 30: const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = { 31: minTimeBeforeFeedbackMs: 600000, 32: minTimeBetweenFeedbackMs: 3600000, 33: minTimeBetweenGlobalFeedbackMs: 100000000, 34: minUserTurnsBeforeFeedback: 5, 35: minUserTurnsBetweenFeedback: 10, 36: hideThanksAfterMs: 3000, 37: onForModels: ['*'], 38: probability: 0.005 39: }; 40: const DEFAULT_TRANSCRIPT_ASK_CONFIG: TranscriptAskConfig = { 41: probability: 0 42: }; 43: export function useFeedbackSurvey(messages: Message[], isLoading: boolean, submitCount: number, surveyType: FeedbackSurveyType = 'session', hasActivePrompt: boolean = false): { 44: state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted'; 45: lastResponse: FeedbackSurveyResponse | null; 46: handleSelect: (selected: FeedbackSurveyResponse) => boolean; 47: handleTranscriptSelect: (selected: TranscriptShareResponse) => void; 48: } { 49: const lastAssistantMessageIdRef = useRef('unknown'); 50: lastAssistantMessageIdRef.current = getLastAssistantMessage(messages)?.message?.id || 'unknown'; 51: const [feedbackSurvey, setFeedbackSurvey] = useState<{ 52: timeLastShown: number | null; 53: submitCountAtLastAppearance: number | null; 54: }>(() => ({ 55: timeLastShown: null, 56: submitCountAtLastAppearance: null 57: })); 58: const config = useDynamicConfig<FeedbackSurveyConfig>('tengu_feedback_survey_config', DEFAULT_FEEDBACK_SURVEY_CONFIG); 59: const badTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>('tengu_bad_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG); 60: const goodTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>('tengu_good_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG); 61: const settingsRate = getInitialSettings().feedbackSurveyRate; 62: const sessionStartTime = useRef(Date.now()); 63: const submitCountAtSessionStart = useRef(submitCount); 64: const submitCountRef = useRef(submitCount); 65: submitCountRef.current = submitCount; 66: const messagesRef = useRef(messages); 67: messagesRef.current = messages; 68: const probabilityPassedRef = useRef(false); 69: const lastEligibleSubmitCountRef = useRef<number | null>(null); 70: const updateLastShownTime = useCallback((timestamp: number, submitCountValue: number) => { 71: setFeedbackSurvey(prev => { 72: if (prev.timeLastShown === timestamp && prev.submitCountAtLastAppearance === submitCountValue) { 73: return prev; 74: } 75: return { 76: timeLastShown: timestamp, 77: submitCountAtLastAppearance: submitCountValue 78: }; 79: }); 80: if (getGlobalConfig().feedbackSurveyState?.lastShownTime !== timestamp) { 81: saveGlobalConfig(current => ({ 82: ...current, 83: feedbackSurveyState: { 84: lastShownTime: timestamp 85: } 86: })); 87: } 88: }, []); 89: const onOpen = useCallback((appearanceId: string) => { 90: updateLastShownTime(Date.now(), submitCountRef.current); 91: logEvent('tengu_feedback_survey_event', { 92: event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 93: appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 94: last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 95: survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 96: }); 97: void logOTelEvent('feedback_survey', { 98: event_type: 'appeared', 99: appearance_id: appearanceId, 100: survey_type: surveyType 101: }); 102: }, [updateLastShownTime, surveyType]); 103: const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => { 104: updateLastShownTime(Date.now(), submitCountRef.current); 105: logEvent('tengu_feedback_survey_event', { 106: event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 107: appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 108: response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 109: last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 110: survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 111: }); 112: void logOTelEvent('feedback_survey', { 113: event_type: 'responded', 114: appearance_id: appearanceId_0, 115: response: selected, 116: survey_type: surveyType 117: }); 118: }, [updateLastShownTime, surveyType]); 119: const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => { 120: if (selected_0 !== 'bad' && selected_0 !== 'good') { 121: return false; 122: } 123: if (getGlobalConfig().transcriptShareDismissed) { 124: return false; 125: } 126: if (!isPolicyAllowed('allow_product_feedback')) { 127: return false; 128: } 129: const probability = selected_0 === 'bad' ? badTranscriptAskConfig.probability : goodTranscriptAskConfig.probability; 130: return Math.random() <= probability; 131: }, [badTranscriptAskConfig.probability, goodTranscriptAskConfig.probability]); 132: const onTranscriptPromptShown = useCallback((appearanceId_1: string, surveyResponse: FeedbackSurveyResponse) => { 133: const trigger: TranscriptShareTrigger = surveyResponse === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey'; 134: logEvent('tengu_feedback_survey_event', { 135: event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 136: appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 137: last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 138: survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 139: trigger: trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 140: }); 141: void logOTelEvent('feedback_survey', { 142: event_type: 'transcript_prompt_appeared', 143: appearance_id: appearanceId_1, 144: survey_type: surveyType 145: }); 146: }, [surveyType]); 147: const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse, surveyResponse_0: FeedbackSurveyResponse | null): Promise<boolean> => { 148: const trigger_0: TranscriptShareTrigger = surveyResponse_0 === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey'; 149: logEvent('tengu_feedback_survey_event', { 150: event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 151: appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 152: last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 153: survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 154: trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 155: }); 156: if (selected_1 === 'dont_ask_again') { 157: saveGlobalConfig(current_0 => ({ 158: ...current_0, 159: transcriptShareDismissed: true 160: })); 161: } 162: if (selected_1 === 'yes') { 163: const result = await submitTranscriptShare(messagesRef.current, trigger_0, appearanceId_2); 164: logEvent('tengu_feedback_survey_event', { 165: event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 166: appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 167: trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 168: }); 169: return result.success; 170: } 171: return false; 172: }, [surveyType]); 173: const { 174: state, 175: lastResponse, 176: open, 177: handleSelect, 178: handleTranscriptSelect 179: } = useSurveyState({ 180: hideThanksAfterMs: config.hideThanksAfterMs, 181: onOpen, 182: onSelect, 183: shouldShowTranscriptPrompt, 184: onTranscriptPromptShown, 185: onTranscriptSelect 186: }); 187: const currentModel = getMainLoopModel(); 188: const isModelAllowed = useMemo(() => { 189: if (config.onForModels.length === 0) { 190: return false; 191: } 192: if (config.onForModels.includes('*')) { 193: return true; 194: } 195: return config.onForModels.includes(currentModel); 196: }, [config.onForModels, currentModel]); 197: const shouldOpen = useMemo(() => { 198: if (state !== 'closed') { 199: return false; 200: } 201: if (isLoading) { 202: return false; 203: } 204: if (hasActivePrompt) { 205: return false; 206: } 207: if (process.env.CLAUDE_FORCE_DISPLAY_SURVEY && !feedbackSurvey.timeLastShown) { 208: return true; 209: } 210: if (!isModelAllowed) { 211: return false; 212: } 213: if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) { 214: return false; 215: } 216: if (isFeedbackSurveyDisabled()) { 217: return false; 218: } 219: if (!isPolicyAllowed('allow_product_feedback')) { 220: return false; 221: } 222: if (feedbackSurvey.timeLastShown) { 223: const timeSinceLastShown = Date.now() - feedbackSurvey.timeLastShown; 224: if (timeSinceLastShown < config.minTimeBetweenFeedbackMs) { 225: return false; 226: } 227: if (feedbackSurvey.submitCountAtLastAppearance !== null && submitCount < feedbackSurvey.submitCountAtLastAppearance + config.minUserTurnsBetweenFeedback) { 228: return false; 229: } 230: } else { 231: const timeSinceSessionStart = Date.now() - sessionStartTime.current; 232: if (timeSinceSessionStart < config.minTimeBeforeFeedbackMs) { 233: return false; 234: } 235: if (submitCount < submitCountAtSessionStart.current + config.minUserTurnsBeforeFeedback) { 236: return false; 237: } 238: } 239: if (lastEligibleSubmitCountRef.current !== submitCount) { 240: lastEligibleSubmitCountRef.current = submitCount; 241: probabilityPassedRef.current = Math.random() <= (settingsRate ?? config.probability); 242: } 243: if (!probabilityPassedRef.current) { 244: return false; 245: } 246: const globalFeedbackState = getGlobalConfig().feedbackSurveyState; 247: if (globalFeedbackState?.lastShownTime) { 248: const timeSinceGlobalLastShown = Date.now() - globalFeedbackState.lastShownTime; 249: if (timeSinceGlobalLastShown < config.minTimeBetweenGlobalFeedbackMs) { 250: return false; 251: } 252: } 253: return true; 254: }, [state, isLoading, hasActivePrompt, isModelAllowed, feedbackSurvey.timeLastShown, feedbackSurvey.submitCountAtLastAppearance, submitCount, config.minTimeBetweenFeedbackMs, config.minTimeBetweenGlobalFeedbackMs, config.minUserTurnsBetweenFeedback, config.minTimeBeforeFeedbackMs, config.minUserTurnsBeforeFeedback, config.probability, settingsRate]); 255: useEffect(() => { 256: if (shouldOpen) { 257: open(); 258: } 259: }, [shouldOpen, open]); 260: return { 261: state, 262: lastResponse, 263: handleSelect, 264: handleTranscriptSelect 265: }; 266: }

File: src/components/FeedbackSurvey/useMemorySurvey.tsx

typescript 1: import { useCallback, useEffect, useMemo, useRef } from 'react'; 2: import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'; 3: import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; 4: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; 5: import { isAutoMemoryEnabled } from '../../memdir/paths.js'; 6: import { isPolicyAllowed } from '../../services/policyLimits/index.js'; 7: import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js'; 8: import type { Message } from '../../types/message.js'; 9: import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; 10: import { isEnvTruthy } from '../../utils/envUtils.js'; 11: import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js'; 12: import { extractTextContent, getLastAssistantMessage } from '../../utils/messages.js'; 13: import { logOTelEvent } from '../../utils/telemetry/events.js'; 14: import { submitTranscriptShare } from './submitTranscriptShare.js'; 15: import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'; 16: import { useSurveyState } from './useSurveyState.js'; 17: import type { FeedbackSurveyResponse } from './utils.js'; 18: const HIDE_THANKS_AFTER_MS = 3000; 19: const MEMORY_SURVEY_GATE = 'tengu_dunwich_bell'; 20: const MEMORY_SURVEY_EVENT = 'tengu_memory_survey_event'; 21: const SURVEY_PROBABILITY = 0.2; 22: const TRANSCRIPT_SHARE_TRIGGER = 'memory_survey'; 23: const MEMORY_WORD_RE = /\bmemor(?:y|ies)\b/i; 24: function hasMemoryFileRead(messages: Message[]): boolean { 25: for (const message of messages) { 26: if (message.type !== 'assistant') { 27: continue; 28: } 29: const content = message.message.content; 30: if (!Array.isArray(content)) { 31: continue; 32: } 33: for (const block of content) { 34: if (block.type !== 'tool_use' || block.name !== FILE_READ_TOOL_NAME) { 35: continue; 36: } 37: const input = block.input as { 38: file_path?: unknown; 39: }; 40: if (typeof input.file_path === 'string' && isAutoManagedMemoryFile(input.file_path)) { 41: return true; 42: } 43: } 44: } 45: return false; 46: } 47: export function useMemorySurvey(messages: Message[], isLoading: boolean, hasActivePrompt = false, { 48: enabled = true 49: }: { 50: enabled?: boolean; 51: } = {}): { 52: state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted'; 53: lastResponse: FeedbackSurveyResponse | null; 54: handleSelect: (selected: FeedbackSurveyResponse) => void; 55: handleTranscriptSelect: (selected: TranscriptShareResponse) => void; 56: } { 57: const seenAssistantUuids = useRef<Set<string>>(new Set()); 58: const memoryReadSeen = useRef(false); 59: const messagesRef = useRef(messages); 60: messagesRef.current = messages; 61: const onOpen = useCallback((appearanceId: string) => { 62: logEvent(MEMORY_SURVEY_EVENT, { 63: event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 64: appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 65: }); 66: void logOTelEvent('feedback_survey', { 67: event_type: 'appeared', 68: appearance_id: appearanceId, 69: survey_type: 'memory' 70: }); 71: }, []); 72: const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => { 73: logEvent(MEMORY_SURVEY_EVENT, { 74: event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 75: appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 76: response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 77: }); 78: void logOTelEvent('feedback_survey', { 79: event_type: 'responded', 80: appearance_id: appearanceId_0, 81: response: selected, 82: survey_type: 'memory' 83: }); 84: }, []); 85: const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => { 86: if ("external" !== 'ant') { 87: return false; 88: } 89: if (selected_0 !== 'bad' && selected_0 !== 'good') { 90: return false; 91: } 92: if (getGlobalConfig().transcriptShareDismissed) { 93: return false; 94: } 95: if (!isPolicyAllowed('allow_product_feedback')) { 96: return false; 97: } 98: return true; 99: }, []); 100: const onTranscriptPromptShown = useCallback((appearanceId_1: string) => { 101: logEvent(MEMORY_SURVEY_EVENT, { 102: event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 103: appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 104: trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 105: }); 106: void logOTelEvent('feedback_survey', { 107: event_type: 'transcript_prompt_appeared', 108: appearance_id: appearanceId_1, 109: survey_type: 'memory' 110: }); 111: }, []); 112: const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse): Promise<boolean> => { 113: logEvent(MEMORY_SURVEY_EVENT, { 114: event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 115: appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 116: trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 117: }); 118: if (selected_1 === 'dont_ask_again') { 119: saveGlobalConfig(current => ({ 120: ...current, 121: transcriptShareDismissed: true 122: })); 123: } 124: if (selected_1 === 'yes') { 125: const result = await submitTranscriptShare(messagesRef.current, TRANSCRIPT_SHARE_TRIGGER, appearanceId_2); 126: logEvent(MEMORY_SURVEY_EVENT, { 127: event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 128: appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 129: trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 130: }); 131: return result.success; 132: } 133: return false; 134: }, []); 135: const { 136: state, 137: lastResponse, 138: open, 139: handleSelect, 140: handleTranscriptSelect 141: } = useSurveyState({ 142: hideThanksAfterMs: HIDE_THANKS_AFTER_MS, 143: onOpen, 144: onSelect, 145: shouldShowTranscriptPrompt, 146: onTranscriptPromptShown, 147: onTranscriptSelect 148: }); 149: const lastAssistant = useMemo(() => getLastAssistantMessage(messages), [messages]); 150: useEffect(() => { 151: if (!enabled) return; 152: if (messages.length === 0) { 153: memoryReadSeen.current = false; 154: seenAssistantUuids.current.clear(); 155: return; 156: } 157: if (state !== 'closed' || isLoading || hasActivePrompt) { 158: return; 159: } 160: if (!getFeatureValue_CACHED_MAY_BE_STALE(MEMORY_SURVEY_GATE, false)) { 161: return; 162: } 163: if (!isAutoMemoryEnabled()) { 164: return; 165: } 166: if (isFeedbackSurveyDisabled()) { 167: return; 168: } 169: if (!isPolicyAllowed('allow_product_feedback')) { 170: return; 171: } 172: if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) { 173: return; 174: } 175: if (!lastAssistant || seenAssistantUuids.current.has(lastAssistant.uuid)) { 176: return; 177: } 178: const text = extractTextContent(lastAssistant.message.content, ' '); 179: if (!MEMORY_WORD_RE.test(text)) { 180: return; 181: } 182: seenAssistantUuids.current.add(lastAssistant.uuid); 183: if (!memoryReadSeen.current) { 184: memoryReadSeen.current = hasMemoryFileRead(messages); 185: } 186: if (!memoryReadSeen.current) { 187: return; 188: } 189: if (Math.random() < SURVEY_PROBABILITY) { 190: open(); 191: } 192: }, [enabled, state, isLoading, hasActivePrompt, lastAssistant, messages, open]); 193: return { 194: state, 195: lastResponse, 196: handleSelect, 197: handleTranscriptSelect 198: }; 199: }

File: src/components/FeedbackSurvey/usePostCompactSurvey.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 3: import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'; 4: import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; 5: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; 6: import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js'; 7: import type { Message } from '../../types/message.js'; 8: import { isEnvTruthy } from '../../utils/envUtils.js'; 9: import { isCompactBoundaryMessage } from '../../utils/messages.js'; 10: import { logOTelEvent } from '../../utils/telemetry/events.js'; 11: import { useSurveyState } from './useSurveyState.js'; 12: import type { FeedbackSurveyResponse } from './utils.js'; 13: const HIDE_THANKS_AFTER_MS = 3000; 14: const POST_COMPACT_SURVEY_GATE = 'tengu_post_compact_survey'; 15: const SURVEY_PROBABILITY = 0.2; 16: function hasMessageAfterBoundary(messages: Message[], boundaryUuid: string): boolean { 17: const boundaryIndex = messages.findIndex(msg => msg.uuid === boundaryUuid); 18: if (boundaryIndex === -1) { 19: return false; 20: } 21: for (let i = boundaryIndex + 1; i < messages.length; i++) { 22: const msg = messages[i]; 23: if (msg && (msg.type === 'user' || msg.type === 'assistant')) { 24: return true; 25: } 26: } 27: return false; 28: } 29: export function usePostCompactSurvey(messages, isLoading, t0, t1) { 30: const $ = _c(23); 31: const hasActivePrompt = t0 === undefined ? false : t0; 32: let t2; 33: if ($[0] !== t1) { 34: t2 = t1 === undefined ? {} : t1; 35: $[0] = t1; 36: $[1] = t2; 37: } else { 38: t2 = $[1]; 39: } 40: const { 41: enabled: t3 42: } = t2; 43: const enabled = t3 === undefined ? true : t3; 44: const [gateEnabled, setGateEnabled] = useState(null); 45: let t4; 46: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 47: t4 = new Set(); 48: $[2] = t4; 49: } else { 50: t4 = $[2]; 51: } 52: const seenCompactBoundaries = useRef(t4); 53: const pendingCompactBoundaryUuid = useRef(null); 54: const onOpen = _temp; 55: const onSelect = _temp2; 56: let t5; 57: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 58: t5 = { 59: hideThanksAfterMs: HIDE_THANKS_AFTER_MS, 60: onOpen, 61: onSelect 62: }; 63: $[3] = t5; 64: } else { 65: t5 = $[3]; 66: } 67: const { 68: state, 69: lastResponse, 70: open, 71: handleSelect 72: } = useSurveyState(t5); 73: let t6; 74: let t7; 75: if ($[4] !== enabled) { 76: t6 = () => { 77: if (!enabled) { 78: return; 79: } 80: setGateEnabled(checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE)); 81: }; 82: t7 = [enabled]; 83: $[4] = enabled; 84: $[5] = t6; 85: $[6] = t7; 86: } else { 87: t6 = $[5]; 88: t7 = $[6]; 89: } 90: useEffect(t6, t7); 91: let t8; 92: if ($[7] !== messages) { 93: t8 = new Set(messages.filter(_temp3).map(_temp4)); 94: $[7] = messages; 95: $[8] = t8; 96: } else { 97: t8 = $[8]; 98: } 99: const currentCompactBoundaries = t8; 100: let t10; 101: let t9; 102: if ($[9] !== currentCompactBoundaries || $[10] !== enabled || $[11] !== gateEnabled || $[12] !== hasActivePrompt || $[13] !== isLoading || $[14] !== messages || $[15] !== open || $[16] !== state) { 103: t9 = () => { 104: if (!enabled) { 105: return; 106: } 107: if (state !== "closed" || isLoading) { 108: return; 109: } 110: if (hasActivePrompt) { 111: return; 112: } 113: if (gateEnabled !== true) { 114: return; 115: } 116: if (isFeedbackSurveyDisabled()) { 117: return; 118: } 119: if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) { 120: return; 121: } 122: if (pendingCompactBoundaryUuid.current !== null) { 123: if (hasMessageAfterBoundary(messages, pendingCompactBoundaryUuid.current)) { 124: pendingCompactBoundaryUuid.current = null; 125: if (Math.random() < SURVEY_PROBABILITY) { 126: open(); 127: } 128: return; 129: } 130: } 131: const newBoundaries = Array.from(currentCompactBoundaries).filter(uuid => !seenCompactBoundaries.current.has(uuid)); 132: if (newBoundaries.length > 0) { 133: seenCompactBoundaries.current = new Set(currentCompactBoundaries); 134: pendingCompactBoundaryUuid.current = newBoundaries[newBoundaries.length - 1]; 135: } 136: }; 137: t10 = [enabled, currentCompactBoundaries, state, isLoading, hasActivePrompt, gateEnabled, messages, open]; 138: $[9] = currentCompactBoundaries; 139: $[10] = enabled; 140: $[11] = gateEnabled; 141: $[12] = hasActivePrompt; 142: $[13] = isLoading; 143: $[14] = messages; 144: $[15] = open; 145: $[16] = state; 146: $[17] = t10; 147: $[18] = t9; 148: } else { 149: t10 = $[17]; 150: t9 = $[18]; 151: } 152: useEffect(t9, t10); 153: let t11; 154: if ($[19] !== handleSelect || $[20] !== lastResponse || $[21] !== state) { 155: t11 = { 156: state, 157: lastResponse, 158: handleSelect 159: }; 160: $[19] = handleSelect; 161: $[20] = lastResponse; 162: $[21] = state; 163: $[22] = t11; 164: } else { 165: t11 = $[22]; 166: } 167: return t11; 168: } 169: function _temp4(msg_0) { 170: return msg_0.uuid; 171: } 172: function _temp3(msg) { 173: return isCompactBoundaryMessage(msg); 174: } 175: function _temp2(appearanceId_0, selected) { 176: const smCompactionEnabled_0 = shouldUseSessionMemoryCompaction(); 177: logEvent("tengu_post_compact_survey_event", { 178: event_type: "responded" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 179: appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 180: response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 181: session_memory_compaction_enabled: smCompactionEnabled_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 182: }); 183: logOTelEvent("feedback_survey", { 184: event_type: "responded", 185: appearance_id: appearanceId_0, 186: response: selected, 187: survey_type: "post_compact" 188: }); 189: } 190: function _temp(appearanceId) { 191: const smCompactionEnabled = shouldUseSessionMemoryCompaction(); 192: logEvent("tengu_post_compact_survey_event", { 193: event_type: "appeared" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 194: appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 195: session_memory_compaction_enabled: smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 196: }); 197: logOTelEvent("feedback_survey", { 198: event_type: "appeared", 199: appearance_id: appearanceId, 200: survey_type: "post_compact" 201: }); 202: }

File: src/components/FeedbackSurvey/useSurveyState.tsx

typescript 1: import { randomUUID } from 'crypto'; 2: import { useCallback, useRef, useState } from 'react'; 3: import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'; 4: import type { FeedbackSurveyResponse } from './utils.js'; 5: type SurveyState = 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted'; 6: type UseSurveyStateOptions = { 7: hideThanksAfterMs: number; 8: onOpen: (appearanceId: string) => void | Promise<void>; 9: onSelect: (appearanceId: string, selected: FeedbackSurveyResponse) => void | Promise<void>; 10: shouldShowTranscriptPrompt?: (selected: FeedbackSurveyResponse) => boolean; 11: onTranscriptPromptShown?: (appearanceId: string, surveyResponse: FeedbackSurveyResponse) => void; 12: onTranscriptSelect?: (appearanceId: string, selected: TranscriptShareResponse, surveyResponse: FeedbackSurveyResponse | null) => boolean | Promise<boolean>; 13: }; 14: export function useSurveyState({ 15: hideThanksAfterMs, 16: onOpen, 17: onSelect, 18: shouldShowTranscriptPrompt, 19: onTranscriptPromptShown, 20: onTranscriptSelect 21: }: UseSurveyStateOptions): { 22: state: SurveyState; 23: lastResponse: FeedbackSurveyResponse | null; 24: open: () => void; 25: handleSelect: (selected: FeedbackSurveyResponse) => boolean; 26: handleTranscriptSelect: (selected: TranscriptShareResponse) => void; 27: } { 28: const [state, setState] = useState<SurveyState>('closed'); 29: const [lastResponse, setLastResponse] = useState<FeedbackSurveyResponse | null>(null); 30: const appearanceId = useRef(randomUUID()); 31: const lastResponseRef = useRef<FeedbackSurveyResponse | null>(null); 32: const showThanksThenClose = useCallback(() => { 33: setState('thanks'); 34: setTimeout((setState_0, setLastResponse_0) => { 35: setState_0('closed'); 36: setLastResponse_0(null); 37: }, hideThanksAfterMs, setState, setLastResponse); 38: }, [hideThanksAfterMs]); 39: const showSubmittedThenClose = useCallback(() => { 40: setState('submitted'); 41: setTimeout(setState, hideThanksAfterMs, 'closed'); 42: }, [hideThanksAfterMs]); 43: const open = useCallback(() => { 44: if (state !== 'closed') { 45: return; 46: } 47: setState('open'); 48: appearanceId.current = randomUUID(); 49: void onOpen(appearanceId.current); 50: }, [state, onOpen]); 51: const handleSelect = useCallback((selected: FeedbackSurveyResponse): boolean => { 52: setLastResponse(selected); 53: lastResponseRef.current = selected; 54: void onSelect(appearanceId.current, selected); 55: if (selected === 'dismissed') { 56: setState('closed'); 57: setLastResponse(null); 58: } else if (shouldShowTranscriptPrompt?.(selected)) { 59: setState('transcript_prompt'); 60: onTranscriptPromptShown?.(appearanceId.current, selected); 61: return true; 62: } else { 63: showThanksThenClose(); 64: } 65: return false; 66: }, [showThanksThenClose, onSelect, shouldShowTranscriptPrompt, onTranscriptPromptShown]); 67: const handleTranscriptSelect = useCallback((selected_0: TranscriptShareResponse) => { 68: switch (selected_0) { 69: case 'yes': 70: setState('submitting'); 71: void (async () => { 72: try { 73: const success = await onTranscriptSelect?.(appearanceId.current, selected_0, lastResponseRef.current); 74: if (success) { 75: showSubmittedThenClose(); 76: } else { 77: showThanksThenClose(); 78: } 79: } catch { 80: showThanksThenClose(); 81: } 82: })(); 83: break; 84: case 'no': 85: case 'dont_ask_again': 86: void onTranscriptSelect?.(appearanceId.current, selected_0, lastResponseRef.current); 87: showThanksThenClose(); 88: break; 89: } 90: }, [showThanksThenClose, showSubmittedThenClose, onTranscriptSelect]); 91: return { 92: state, 93: lastResponse, 94: open, 95: handleSelect, 96: handleTranscriptSelect 97: }; 98: }

File: src/components/grove/Grove.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useEffect, useState } from 'react'; 3: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; 4: import { Box, Link, Text, useInput } from '../../ink.js'; 5: import { type AccountSettings, calculateShouldShowGrove, type GroveConfig, getGroveNoticeConfig, getGroveSettings, markGroveNoticeViewed, updateGroveSettings } from '../../services/api/grove.js'; 6: import { Select } from '../CustomSelect/index.js'; 7: import { Byline } from '../design-system/Byline.js'; 8: import { Dialog } from '../design-system/Dialog.js'; 9: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 10: export type GroveDecision = 'accept_opt_in' | 'accept_opt_out' | 'defer' | 'escape' | 'skip_rendering'; 11: type Props = { 12: showIfAlreadyViewed: boolean; 13: location: 'settings' | 'policy_update_modal' | 'onboarding'; 14: onDone(decision: GroveDecision): void; 15: }; 16: const NEW_TERMS_ASCII = ` _____________ 17: | \\ \\ 18: | NEW TERMS \\__\\ 19: | | 20: | ---------- | 21: | ---------- | 22: | ---------- | 23: | ---------- | 24: | ---------- | 25: | | 26: |______________|`; 27: function GracePeriodContentBody() { 28: const $ = _c(9); 29: let t0; 30: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 31: t0 = <Text>An update to our Consumer Terms and Privacy Policy will take effect on{" "}<Text bold={true}>October 8, 2025</Text>. You can accept the updated terms today.</Text>; 32: $[0] = t0; 33: } else { 34: t0 = $[0]; 35: } 36: let t1; 37: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 38: t1 = <Text>What's changing?</Text>; 39: $[1] = t1; 40: } else { 41: t1 = $[1]; 42: } 43: let t2; 44: let t3; 45: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 46: t2 = <Text>· </Text>; 47: t3 = <Text bold={true}>You can help improve Claude </Text>; 48: $[2] = t2; 49: $[3] = t3; 50: } else { 51: t2 = $[2]; 52: t3 = $[3]; 53: } 54: let t4; 55: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 56: t4 = <Box paddingLeft={1}><Text>{t2}{t3}<Text>— Allow the use of your chats and coding sessions to train and improve Anthropic AI models. Change anytime in your Privacy Settings (<Link url="https://claude.ai/settings/data-privacy-controls" />).</Text></Text></Box>; 57: $[4] = t4; 58: } else { 59: t4 = $[4]; 60: } 61: let t5; 62: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 63: t5 = <Box flexDirection="column">{t1}{t4}<Box paddingLeft={1}><Text><Text>· </Text><Text bold={true}>Updates to data retention </Text><Text>— To help us improve our AI models and safety protections, we're extending data retention to 5 years.</Text></Text></Box></Box>; 64: $[5] = t5; 65: } else { 66: t5 = $[5]; 67: } 68: let t6; 69: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 70: t6 = <Link url="https://www.anthropic.com/news/updates-to-our-consumer-terms" />; 71: $[6] = t6; 72: } else { 73: t6 = $[6]; 74: } 75: let t7; 76: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 77: t7 = <Link url="https://anthropic.com/legal/terms" />; 78: $[7] = t7; 79: } else { 80: t7 = $[7]; 81: } 82: let t8; 83: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 84: t8 = <>{t0}{t5}<Text>Learn more ({t6}) or read the updated Consumer Terms ({t7}) and Privacy Policy (<Link url="https://anthropic.com/legal/privacy" />)</Text></>; 85: $[8] = t8; 86: } else { 87: t8 = $[8]; 88: } 89: return t8; 90: } 91: function PostGracePeriodContentBody() { 92: const $ = _c(7); 93: let t0; 94: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 95: t0 = <Text>We've updated our Consumer Terms and Privacy Policy.</Text>; 96: $[0] = t0; 97: } else { 98: t0 = $[0]; 99: } 100: let t1; 101: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 102: t1 = <Text>What's changing?</Text>; 103: $[1] = t1; 104: } else { 105: t1 = $[1]; 106: } 107: let t2; 108: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 109: t2 = <Box flexDirection="column"><Text bold={true}>Help improve Claude</Text><Text>Allow the use of your chats and coding sessions to train and improve Anthropic AI models. You can change this anytime in Privacy Settings</Text><Link url="https://claude.ai/settings/data-privacy-controls" /></Box>; 110: $[2] = t2; 111: } else { 112: t2 = $[2]; 113: } 114: let t3; 115: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 116: t3 = <Box flexDirection="column" gap={1}>{t1}{t2}<Box flexDirection="column"><Text bold={true}>How this affects data retention</Text><Text>Turning ON the improve Claude setting extends data retention from 30 days to 5 years. Turning it OFF keeps the default 30-day data retention. Delete data anytime.</Text></Box></Box>; 117: $[3] = t3; 118: } else { 119: t3 = $[3]; 120: } 121: let t4; 122: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 123: t4 = <Link url="https://www.anthropic.com/news/updates-to-our-consumer-terms" />; 124: $[4] = t4; 125: } else { 126: t4 = $[4]; 127: } 128: let t5; 129: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 130: t5 = <Link url="https://anthropic.com/legal/terms" />; 131: $[5] = t5; 132: } else { 133: t5 = $[5]; 134: } 135: let t6; 136: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 137: t6 = <>{t0}{t3}<Text>Learn more ({t4}) or read the updated Consumer Terms ({t5}) and Privacy Policy (<Link url="https://anthropic.com/legal/privacy" />)</Text></>; 138: $[6] = t6; 139: } else { 140: t6 = $[6]; 141: } 142: return t6; 143: } 144: export function GroveDialog(t0) { 145: const $ = _c(34); 146: const { 147: showIfAlreadyViewed, 148: location, 149: onDone 150: } = t0; 151: const [shouldShowDialog, setShouldShowDialog] = useState(null); 152: const [groveConfig, setGroveConfig] = useState(null); 153: let t1; 154: let t2; 155: if ($[0] !== location || $[1] !== onDone || $[2] !== showIfAlreadyViewed) { 156: t1 = () => { 157: const checkGroveSettings = async function checkGroveSettings() { 158: const [settingsResult, configResult] = await Promise.all([getGroveSettings(), getGroveNoticeConfig()]); 159: const config = configResult.success ? configResult.data : null; 160: setGroveConfig(config); 161: const shouldShow = calculateShouldShowGrove(settingsResult, configResult, showIfAlreadyViewed); 162: setShouldShowDialog(shouldShow); 163: if (!shouldShow) { 164: onDone("skip_rendering"); 165: return; 166: } 167: markGroveNoticeViewed(); 168: logEvent("tengu_grove_policy_viewed", { 169: location: location as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 170: dismissable: config?.notice_is_grace_period as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 171: }); 172: }; 173: checkGroveSettings(); 174: }; 175: t2 = [showIfAlreadyViewed, location, onDone]; 176: $[0] = location; 177: $[1] = onDone; 178: $[2] = showIfAlreadyViewed; 179: $[3] = t1; 180: $[4] = t2; 181: } else { 182: t1 = $[3]; 183: t2 = $[4]; 184: } 185: useEffect(t1, t2); 186: if (shouldShowDialog === null) { 187: return null; 188: } 189: if (!shouldShowDialog) { 190: return null; 191: } 192: let t3; 193: if ($[5] !== groveConfig?.notice_is_grace_period || $[6] !== onDone) { 194: t3 = async function onChange(value) { 195: bb21: switch (value) { 196: case "accept_opt_in": 197: { 198: await updateGroveSettings(true); 199: logEvent("tengu_grove_policy_submitted", { 200: state: true, 201: dismissable: groveConfig?.notice_is_grace_period as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 202: }); 203: break bb21; 204: } 205: case "accept_opt_out": 206: { 207: await updateGroveSettings(false); 208: logEvent("tengu_grove_policy_submitted", { 209: state: false, 210: dismissable: groveConfig?.notice_is_grace_period as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 211: }); 212: break bb21; 213: } 214: case "defer": 215: { 216: logEvent("tengu_grove_policy_dismissed", { 217: state: true 218: }); 219: break bb21; 220: } 221: case "escape": 222: { 223: logEvent("tengu_grove_policy_escaped", {}); 224: } 225: } 226: onDone(value); 227: }; 228: $[5] = groveConfig?.notice_is_grace_period; 229: $[6] = onDone; 230: $[7] = t3; 231: } else { 232: t3 = $[7]; 233: } 234: const onChange = t3; 235: let t4; 236: if ($[8] !== groveConfig?.domain_excluded) { 237: t4 = groveConfig?.domain_excluded ? [{ 238: label: "Accept terms \xB7 Help improve Claude: OFF (for emails with your domain)", 239: value: "accept_opt_out" 240: }] : [{ 241: label: "Accept terms \xB7 Help improve Claude: ON", 242: value: "accept_opt_in" 243: }, { 244: label: "Accept terms \xB7 Help improve Claude: OFF", 245: value: "accept_opt_out" 246: }]; 247: $[8] = groveConfig?.domain_excluded; 248: $[9] = t4; 249: } else { 250: t4 = $[9]; 251: } 252: const acceptOptions = t4; 253: let t5; 254: if ($[10] !== groveConfig?.notice_is_grace_period || $[11] !== onChange) { 255: t5 = function handleCancel() { 256: if (groveConfig?.notice_is_grace_period) { 257: onChange("defer"); 258: return; 259: } 260: onChange("escape"); 261: }; 262: $[10] = groveConfig?.notice_is_grace_period; 263: $[11] = onChange; 264: $[12] = t5; 265: } else { 266: t5 = $[12]; 267: } 268: const handleCancel = t5; 269: let t6; 270: if ($[13] !== groveConfig?.notice_is_grace_period) { 271: t6 = <Box flexDirection="column" gap={1} flexGrow={1}>{groveConfig?.notice_is_grace_period ? <GracePeriodContentBody /> : <PostGracePeriodContentBody />}</Box>; 272: $[13] = groveConfig?.notice_is_grace_period; 273: $[14] = t6; 274: } else { 275: t6 = $[14]; 276: } 277: let t7; 278: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 279: t7 = <Box flexShrink={0}><Text color="professionalBlue">{NEW_TERMS_ASCII}</Text></Box>; 280: $[15] = t7; 281: } else { 282: t7 = $[15]; 283: } 284: let t8; 285: if ($[16] !== t6) { 286: t8 = <Box flexDirection="row">{t6}{t7}</Box>; 287: $[16] = t6; 288: $[17] = t8; 289: } else { 290: t8 = $[17]; 291: } 292: let t9; 293: if ($[18] === Symbol.for("react.memo_cache_sentinel")) { 294: t9 = <Box flexDirection="column"><Text bold={true}>Please select how you'd like to continue</Text><Text>Your choice takes effect immediately upon confirmation.</Text></Box>; 295: $[18] = t9; 296: } else { 297: t9 = $[18]; 298: } 299: let t10; 300: if ($[19] !== groveConfig?.notice_is_grace_period) { 301: t10 = groveConfig?.notice_is_grace_period ? [{ 302: label: "Not now", 303: value: "defer" 304: }] : []; 305: $[19] = groveConfig?.notice_is_grace_period; 306: $[20] = t10; 307: } else { 308: t10 = $[20]; 309: } 310: let t11; 311: if ($[21] !== acceptOptions || $[22] !== t10) { 312: t11 = [...acceptOptions, ...t10]; 313: $[21] = acceptOptions; 314: $[22] = t10; 315: $[23] = t11; 316: } else { 317: t11 = $[23]; 318: } 319: let t12; 320: if ($[24] !== onChange) { 321: t12 = value_0 => onChange(value_0 as 'accept_opt_in' | 'accept_opt_out' | 'defer'); 322: $[24] = onChange; 323: $[25] = t12; 324: } else { 325: t12 = $[25]; 326: } 327: let t13; 328: if ($[26] !== handleCancel || $[27] !== t11 || $[28] !== t12) { 329: t13 = <Box flexDirection="column" gap={1}>{t9}<Select options={t11} onChange={t12} onCancel={handleCancel} /></Box>; 330: $[26] = handleCancel; 331: $[27] = t11; 332: $[28] = t12; 333: $[29] = t13; 334: } else { 335: t13 = $[29]; 336: } 337: let t14; 338: if ($[30] !== handleCancel || $[31] !== t13 || $[32] !== t8) { 339: t14 = <Dialog title="Updates to Consumer Terms and Policies" color="professionalBlue" onCancel={handleCancel} inputGuide={_temp}>{t8}{t13}</Dialog>; 340: $[30] = handleCancel; 341: $[31] = t13; 342: $[32] = t8; 343: $[33] = t14; 344: } else { 345: t14 = $[33]; 346: } 347: return t14; 348: } 349: function _temp(exitState) { 350: return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut="Enter" action="confirm" /><KeyboardShortcutHint shortcut="Esc" action="cancel" /></Byline>; 351: } 352: type PrivacySettingsDialogProps = { 353: settings: AccountSettings; 354: domainExcluded?: boolean; 355: onDone(): void; 356: }; 357: export function PrivacySettingsDialog(t0) { 358: const $ = _c(17); 359: const { 360: settings, 361: domainExcluded, 362: onDone 363: } = t0; 364: const [groveEnabled, setGroveEnabled] = useState(settings.grove_enabled); 365: let t1; 366: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 367: t1 = []; 368: $[0] = t1; 369: } else { 370: t1 = $[0]; 371: } 372: React.useEffect(_temp2, t1); 373: let t2; 374: if ($[1] !== domainExcluded || $[2] !== groveEnabled) { 375: t2 = async (input, key) => { 376: if (!domainExcluded && (key.tab || key.return || input === " ")) { 377: const newValue = !groveEnabled; 378: setGroveEnabled(newValue); 379: await updateGroveSettings(newValue); 380: } 381: }; 382: $[1] = domainExcluded; 383: $[2] = groveEnabled; 384: $[3] = t2; 385: } else { 386: t2 = $[3]; 387: } 388: useInput(t2); 389: let t3; 390: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 391: t3 = <Text color="error">false</Text>; 392: $[4] = t3; 393: } else { 394: t3 = $[4]; 395: } 396: let valueComponent = t3; 397: if (domainExcluded) { 398: let t4; 399: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 400: t4 = <Text color="error">false (for emails with your domain)</Text>; 401: $[5] = t4; 402: } else { 403: t4 = $[5]; 404: } 405: valueComponent = t4; 406: } else { 407: if (groveEnabled) { 408: let t4; 409: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 410: t4 = <Text color="success">true</Text>; 411: $[6] = t4; 412: } else { 413: t4 = $[6]; 414: } 415: valueComponent = t4; 416: } 417: } 418: let t4; 419: if ($[7] !== domainExcluded) { 420: t4 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : domainExcluded ? <KeyboardShortcutHint shortcut="Esc" action="cancel" /> : <Byline><KeyboardShortcutHint shortcut="Enter/Tab/Space" action="toggle" /><KeyboardShortcutHint shortcut="Esc" action="cancel" /></Byline>; 421: $[7] = domainExcluded; 422: $[8] = t4; 423: } else { 424: t4 = $[8]; 425: } 426: let t5; 427: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 428: t5 = <Text>Review and manage your privacy settings at{" "}<Link url="https://claude.ai/settings/data-privacy-controls" /></Text>; 429: $[9] = t5; 430: } else { 431: t5 = $[9]; 432: } 433: let t6; 434: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 435: t6 = <Box width={44}><Text bold={true}>Help improve Claude</Text></Box>; 436: $[10] = t6; 437: } else { 438: t6 = $[10]; 439: } 440: let t7; 441: if ($[11] !== valueComponent) { 442: t7 = <Box>{t6}<Box>{valueComponent}</Box></Box>; 443: $[11] = valueComponent; 444: $[12] = t7; 445: } else { 446: t7 = $[12]; 447: } 448: let t8; 449: if ($[13] !== onDone || $[14] !== t4 || $[15] !== t7) { 450: t8 = <Dialog title="Data Privacy" color="professionalBlue" onCancel={onDone} inputGuide={t4}>{t5}{t7}</Dialog>; 451: $[13] = onDone; 452: $[14] = t4; 453: $[15] = t7; 454: $[16] = t8; 455: } else { 456: t8 = $[16]; 457: } 458: return t8; 459: } 460: function _temp2() { 461: logEvent("tengu_grove_privacy_settings_viewed", {}); 462: }

File: src/components/HelpV2/Commands.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useMemo } from 'react'; 4: import { type Command, formatDescriptionWithSource } from '../../commands.js'; 5: import { Box, Text } from '../../ink.js'; 6: import { truncate } from '../../utils/format.js'; 7: import { Select } from '../CustomSelect/select.js'; 8: import { useTabHeaderFocus } from '../design-system/Tabs.js'; 9: type Props = { 10: commands: Command[]; 11: maxHeight: number; 12: columns: number; 13: title: string; 14: onCancel: () => void; 15: emptyMessage?: string; 16: }; 17: export function Commands(t0) { 18: const $ = _c(14); 19: const { 20: commands, 21: maxHeight, 22: columns, 23: title, 24: onCancel, 25: emptyMessage 26: } = t0; 27: const { 28: headerFocused, 29: focusHeader 30: } = useTabHeaderFocus(); 31: const maxWidth = Math.max(1, columns - 10); 32: const visibleCount = Math.max(1, Math.floor((maxHeight - 10) / 2)); 33: let t1; 34: if ($[0] !== commands || $[1] !== maxWidth) { 35: const seen = new Set(); 36: let t2; 37: if ($[3] !== maxWidth) { 38: t2 = cmd_0 => ({ 39: label: `/${cmd_0.name}`, 40: value: cmd_0.name, 41: description: truncate(formatDescriptionWithSource(cmd_0), maxWidth, true) 42: }); 43: $[3] = maxWidth; 44: $[4] = t2; 45: } else { 46: t2 = $[4]; 47: } 48: t1 = commands.filter(cmd => { 49: if (seen.has(cmd.name)) { 50: return false; 51: } 52: seen.add(cmd.name); 53: return true; 54: }).sort(_temp).map(t2); 55: $[0] = commands; 56: $[1] = maxWidth; 57: $[2] = t1; 58: } else { 59: t1 = $[2]; 60: } 61: const options = t1; 62: let t2; 63: if ($[5] !== commands.length || $[6] !== emptyMessage || $[7] !== focusHeader || $[8] !== headerFocused || $[9] !== onCancel || $[10] !== options || $[11] !== title || $[12] !== visibleCount) { 64: t2 = <Box flexDirection="column" paddingY={1}>{commands.length === 0 && emptyMessage ? <Text dimColor={true}>{emptyMessage}</Text> : <><Text>{title}</Text><Box marginTop={1}><Select options={options} visibleOptionCount={visibleCount} onCancel={onCancel} disableSelection={true} hideIndexes={true} layout="compact-vertical" onUpFromFirstItem={focusHeader} isDisabled={headerFocused} /></Box></>}</Box>; 65: $[5] = commands.length; 66: $[6] = emptyMessage; 67: $[7] = focusHeader; 68: $[8] = headerFocused; 69: $[9] = onCancel; 70: $[10] = options; 71: $[11] = title; 72: $[12] = visibleCount; 73: $[13] = t2; 74: } else { 75: t2 = $[13]; 76: } 77: return t2; 78: } 79: function _temp(a, b) { 80: return a.name.localeCompare(b.name); 81: }

File: src/components/HelpV2/General.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { PromptInputHelpMenu } from '../PromptInput/PromptInputHelpMenu.js'; 5: export function General() { 6: const $ = _c(2); 7: let t0; 8: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 9: t0 = <Box><Text>Claude understands your codebase, makes edits with your permission, and executes commands — right from your terminal.</Text></Box>; 10: $[0] = t0; 11: } else { 12: t0 = $[0]; 13: } 14: let t1; 15: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 16: t1 = <Box flexDirection="column" paddingY={1} gap={1}>{t0}<Box flexDirection="column"><Box><Text bold={true}>Shortcuts</Text></Box><PromptInputHelpMenu gap={2} fixedWidth={true} /></Box></Box>; 17: $[1] = t1; 18: } else { 19: t1 = $[1]; 20: } 21: return t1; 22: }

File: src/components/HelpV2/HelpV2.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useExitOnCtrlCDWithKeybindings } from 'src/hooks/useExitOnCtrlCDWithKeybindings.js'; 4: import { useShortcutDisplay } from 'src/keybindings/useShortcutDisplay.js'; 5: import { builtInCommandNames, type Command, type CommandResultDisplay, INTERNAL_ONLY_COMMANDS } from '../../commands.js'; 6: import { useIsInsideModal } from '../../context/modalContext.js'; 7: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 8: import { Box, Link, Text } from '../../ink.js'; 9: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 10: import { Pane } from '../design-system/Pane.js'; 11: import { Tab, Tabs } from '../design-system/Tabs.js'; 12: import { Commands } from './Commands.js'; 13: import { General } from './General.js'; 14: type Props = { 15: onClose: (result?: string, options?: { 16: display?: CommandResultDisplay; 17: }) => void; 18: commands: Command[]; 19: }; 20: export function HelpV2(t0) { 21: const $ = _c(44); 22: const { 23: onClose, 24: commands 25: } = t0; 26: const { 27: rows, 28: columns 29: } = useTerminalSize(); 30: const maxHeight = Math.floor(rows / 2); 31: const insideModal = useIsInsideModal(); 32: let t1; 33: if ($[0] !== onClose) { 34: t1 = () => onClose("Help dialog dismissed", { 35: display: "system" 36: }); 37: $[0] = onClose; 38: $[1] = t1; 39: } else { 40: t1 = $[1]; 41: } 42: const close = t1; 43: let t2; 44: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 45: t2 = { 46: context: "Help" 47: }; 48: $[2] = t2; 49: } else { 50: t2 = $[2]; 51: } 52: useKeybinding("help:dismiss", close, t2); 53: const exitState = useExitOnCtrlCDWithKeybindings(close); 54: const dismissShortcut = useShortcutDisplay("help:dismiss", "Help", "esc"); 55: let antOnlyCommands; 56: let builtinCommands; 57: let t3; 58: if ($[3] !== commands) { 59: const builtinNames = builtInCommandNames(); 60: builtinCommands = commands.filter(cmd => builtinNames.has(cmd.name) && !cmd.isHidden); 61: let t4; 62: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 63: t4 = []; 64: $[7] = t4; 65: } else { 66: t4 = $[7]; 67: } 68: antOnlyCommands = t4; 69: t3 = commands.filter(cmd_2 => !builtinNames.has(cmd_2.name) && !cmd_2.isHidden); 70: $[3] = commands; 71: $[4] = antOnlyCommands; 72: $[5] = builtinCommands; 73: $[6] = t3; 74: } else { 75: antOnlyCommands = $[4]; 76: builtinCommands = $[5]; 77: t3 = $[6]; 78: } 79: const customCommands = t3; 80: let t4; 81: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 82: t4 = <Tab key="general" title="general"><General /></Tab>; 83: $[8] = t4; 84: } else { 85: t4 = $[8]; 86: } 87: let tabs; 88: if ($[9] !== antOnlyCommands || $[10] !== builtinCommands || $[11] !== close || $[12] !== columns || $[13] !== customCommands || $[14] !== maxHeight) { 89: tabs = [t4]; 90: let t5; 91: if ($[16] !== builtinCommands || $[17] !== close || $[18] !== columns || $[19] !== maxHeight) { 92: t5 = <Tab key="commands" title="commands"><Commands commands={builtinCommands} maxHeight={maxHeight} columns={columns} title="Browse default commands:" onCancel={close} /></Tab>; 93: $[16] = builtinCommands; 94: $[17] = close; 95: $[18] = columns; 96: $[19] = maxHeight; 97: $[20] = t5; 98: } else { 99: t5 = $[20]; 100: } 101: tabs.push(t5); 102: let t6; 103: if ($[21] !== close || $[22] !== columns || $[23] !== customCommands || $[24] !== maxHeight) { 104: t6 = <Tab key="custom" title="custom-commands"><Commands commands={customCommands} maxHeight={maxHeight} columns={columns} title="Browse custom commands:" emptyMessage="No custom commands found" onCancel={close} /></Tab>; 105: $[21] = close; 106: $[22] = columns; 107: $[23] = customCommands; 108: $[24] = maxHeight; 109: $[25] = t6; 110: } else { 111: t6 = $[25]; 112: } 113: tabs.push(t6); 114: if (false && antOnlyCommands.length > 0) { 115: let t7; 116: if ($[26] !== antOnlyCommands || $[27] !== close || $[28] !== columns || $[29] !== maxHeight) { 117: t7 = <Tab key="ant-only" title="[ant-only]"><Commands commands={antOnlyCommands} maxHeight={maxHeight} columns={columns} title="Browse ant-only commands:" onCancel={close} /></Tab>; 118: $[26] = antOnlyCommands; 119: $[27] = close; 120: $[28] = columns; 121: $[29] = maxHeight; 122: $[30] = t7; 123: } else { 124: t7 = $[30]; 125: } 126: tabs.push(t7); 127: } 128: $[9] = antOnlyCommands; 129: $[10] = builtinCommands; 130: $[11] = close; 131: $[12] = columns; 132: $[13] = customCommands; 133: $[14] = maxHeight; 134: $[15] = tabs; 135: } else { 136: tabs = $[15]; 137: } 138: const t5 = insideModal ? undefined : maxHeight; 139: let t6; 140: if ($[31] !== tabs) { 141: t6 = <Tabs title={false ? "/help" : `Claude Code v${MACRO.VERSION}`} color="professionalBlue" defaultTab="general">{tabs}</Tabs>; 142: $[31] = tabs; 143: $[32] = t6; 144: } else { 145: t6 = $[32]; 146: } 147: let t7; 148: if ($[33] === Symbol.for("react.memo_cache_sentinel")) { 149: t7 = <Box marginTop={1}><Text>For more help:{" "}<Link url="https://code.claude.com/docs/en/overview" /></Text></Box>; 150: $[33] = t7; 151: } else { 152: t7 = $[33]; 153: } 154: let t8; 155: if ($[34] !== dismissShortcut || $[35] !== exitState.keyName || $[36] !== exitState.pending) { 156: t8 = <Box marginTop={1}><Text dimColor={true}>{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <Text italic={true}>{dismissShortcut} to cancel</Text>}</Text></Box>; 157: $[34] = dismissShortcut; 158: $[35] = exitState.keyName; 159: $[36] = exitState.pending; 160: $[37] = t8; 161: } else { 162: t8 = $[37]; 163: } 164: let t9; 165: if ($[38] !== t6 || $[39] !== t8) { 166: t9 = <Pane color="professionalBlue">{t6}{t7}{t8}</Pane>; 167: $[38] = t6; 168: $[39] = t8; 169: $[40] = t9; 170: } else { 171: t9 = $[40]; 172: } 173: let t10; 174: if ($[41] !== t5 || $[42] !== t9) { 175: t10 = <Box flexDirection="column" height={t5}>{t9}</Box>; 176: $[41] = t5; 177: $[42] = t9; 178: $[43] = t10; 179: } else { 180: t10 = $[43]; 181: } 182: return t10; 183: }

File: src/components/HighlightedCode/Fallback.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { extname } from 'path'; 3: import React, { Suspense, use, useMemo } from 'react'; 4: import { Ansi, Text } from '../../ink.js'; 5: import { getCliHighlightPromise } from '../../utils/cliHighlight.js'; 6: import { logForDebugging } from '../../utils/debug.js'; 7: import { convertLeadingTabsToSpaces } from '../../utils/file.js'; 8: import { hashPair } from '../../utils/hash.js'; 9: type Props = { 10: code: string; 11: filePath: string; 12: dim?: boolean; 13: skipColoring?: boolean; 14: }; 15: const HL_CACHE_MAX = 500; 16: const hlCache = new Map<string, string>(); 17: function cachedHighlight(hl: NonNullable<Awaited<ReturnType<typeof getCliHighlightPromise>>>, code: string, language: string): string { 18: const key = hashPair(language, code); 19: const hit = hlCache.get(key); 20: if (hit !== undefined) { 21: hlCache.delete(key); 22: hlCache.set(key, hit); 23: return hit; 24: } 25: const out = hl.highlight(code, { 26: language 27: }); 28: if (hlCache.size >= HL_CACHE_MAX) { 29: const first = hlCache.keys().next().value; 30: if (first !== undefined) hlCache.delete(first); 31: } 32: hlCache.set(key, out); 33: return out; 34: } 35: export function HighlightedCodeFallback(t0) { 36: const $ = _c(20); 37: const { 38: code, 39: filePath, 40: dim: t1, 41: skipColoring: t2 42: } = t0; 43: const dim = t1 === undefined ? false : t1; 44: const skipColoring = t2 === undefined ? false : t2; 45: let t3; 46: if ($[0] !== code) { 47: t3 = convertLeadingTabsToSpaces(code); 48: $[0] = code; 49: $[1] = t3; 50: } else { 51: t3 = $[1]; 52: } 53: const codeWithSpaces = t3; 54: if (skipColoring) { 55: let t4; 56: if ($[2] !== codeWithSpaces) { 57: t4 = <Ansi>{codeWithSpaces}</Ansi>; 58: $[2] = codeWithSpaces; 59: $[3] = t4; 60: } else { 61: t4 = $[3]; 62: } 63: let t5; 64: if ($[4] !== dim || $[5] !== t4) { 65: t5 = <Text dimColor={dim}>{t4}</Text>; 66: $[4] = dim; 67: $[5] = t4; 68: $[6] = t5; 69: } else { 70: t5 = $[6]; 71: } 72: return t5; 73: } 74: let t4; 75: if ($[7] !== filePath) { 76: t4 = extname(filePath).slice(1); 77: $[7] = filePath; 78: $[8] = t4; 79: } else { 80: t4 = $[8]; 81: } 82: const language = t4; 83: let t5; 84: if ($[9] !== codeWithSpaces) { 85: t5 = <Ansi>{codeWithSpaces}</Ansi>; 86: $[9] = codeWithSpaces; 87: $[10] = t5; 88: } else { 89: t5 = $[10]; 90: } 91: let t6; 92: if ($[11] !== codeWithSpaces || $[12] !== language) { 93: t6 = <Highlighted codeWithSpaces={codeWithSpaces} language={language} />; 94: $[11] = codeWithSpaces; 95: $[12] = language; 96: $[13] = t6; 97: } else { 98: t6 = $[13]; 99: } 100: let t7; 101: if ($[14] !== t5 || $[15] !== t6) { 102: t7 = <Suspense fallback={t5}>{t6}</Suspense>; 103: $[14] = t5; 104: $[15] = t6; 105: $[16] = t7; 106: } else { 107: t7 = $[16]; 108: } 109: let t8; 110: if ($[17] !== dim || $[18] !== t7) { 111: t8 = <Text dimColor={dim}>{t7}</Text>; 112: $[17] = dim; 113: $[18] = t7; 114: $[19] = t8; 115: } else { 116: t8 = $[19]; 117: } 118: return t8; 119: } 120: function Highlighted(t0) { 121: const $ = _c(10); 122: const { 123: codeWithSpaces, 124: language 125: } = t0; 126: let t1; 127: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 128: t1 = getCliHighlightPromise(); 129: $[0] = t1; 130: } else { 131: t1 = $[0]; 132: } 133: const hl = use(t1); 134: let t2; 135: if ($[1] !== codeWithSpaces || $[2] !== hl || $[3] !== language) { 136: bb0: { 137: if (!hl) { 138: t2 = codeWithSpaces; 139: break bb0; 140: } 141: let highlightLang = "markdown"; 142: if (language) { 143: if (hl.supportsLanguage(language)) { 144: highlightLang = language; 145: } else { 146: logForDebugging(`Language not supported while highlighting code, falling back to markdown: ${language}`); 147: } 148: } 149: ; 150: try { 151: t2 = cachedHighlight(hl, codeWithSpaces, highlightLang); 152: } catch (t3) { 153: const e = t3; 154: if (e instanceof Error && e.message.includes("Unknown language")) { 155: logForDebugging(`Language not supported while highlighting code, falling back to markdown: ${e}`); 156: let t4; 157: if ($[5] !== codeWithSpaces || $[6] !== hl) { 158: t4 = cachedHighlight(hl, codeWithSpaces, "markdown"); 159: $[5] = codeWithSpaces; 160: $[6] = hl; 161: $[7] = t4; 162: } else { 163: t4 = $[7]; 164: } 165: t2 = t4; 166: break bb0; 167: } 168: t2 = codeWithSpaces; 169: } 170: } 171: $[1] = codeWithSpaces; 172: $[2] = hl; 173: $[3] = language; 174: $[4] = t2; 175: } else { 176: t2 = $[4]; 177: } 178: const out = t2; 179: let t3; 180: if ($[8] !== out) { 181: t3 = <Ansi>{out}</Ansi>; 182: $[8] = out; 183: $[9] = t3; 184: } else { 185: t3 = $[9]; 186: } 187: return t3; 188: }

File: src/components/hooks/HooksConfigMenu.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useCallback, useMemo, useState } from 'react'; 4: import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'; 5: import { useAppState, useAppStateStore } from 'src/state/AppState.js'; 6: import type { CommandResultDisplay } from '../../commands.js'; 7: import { useSettingsChange } from '../../hooks/useSettingsChange.js'; 8: import { Box, Text } from '../../ink.js'; 9: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 10: import { getHookEventMetadata, getHooksForMatcher, getMatcherMetadata, getSortedMatchersForEvent, groupHooksByEventAndMatcher } from '../../utils/hooks/hooksConfigManager.js'; 11: import type { IndividualHookConfig } from '../../utils/hooks/hooksSettings.js'; 12: import { getSettings_DEPRECATED, getSettingsForSource } from '../../utils/settings/settings.js'; 13: import { plural } from '../../utils/stringUtils.js'; 14: import { Dialog } from '../design-system/Dialog.js'; 15: import { SelectEventMode } from './SelectEventMode.js'; 16: import { SelectHookMode } from './SelectHookMode.js'; 17: import { SelectMatcherMode } from './SelectMatcherMode.js'; 18: import { ViewHookMode } from './ViewHookMode.js'; 19: type Props = { 20: toolNames: string[]; 21: onExit: (result?: string, options?: { 22: display?: CommandResultDisplay; 23: }) => void; 24: }; 25: type ModeState = { 26: mode: 'select-event'; 27: } | { 28: mode: 'select-matcher'; 29: event: HookEvent; 30: } | { 31: mode: 'select-hook'; 32: event: HookEvent; 33: matcher: string; 34: } | { 35: mode: 'view-hook'; 36: event: HookEvent; 37: hook: IndividualHookConfig; 38: }; 39: export function HooksConfigMenu(t0) { 40: const $ = _c(100); 41: const { 42: toolNames, 43: onExit 44: } = t0; 45: let t1; 46: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 47: t1 = { 48: mode: "select-event" 49: }; 50: $[0] = t1; 51: } else { 52: t1 = $[0]; 53: } 54: const [modeState, setModeState] = useState(t1); 55: const [disabledByPolicy, setDisabledByPolicy] = useState(_temp); 56: const [restrictedByPolicy, setRestrictedByPolicy] = useState(_temp2); 57: let t2; 58: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 59: t2 = source => { 60: if (source === "policySettings") { 61: const settings_0 = getSettings_DEPRECATED(); 62: const hooksDisabled_0 = settings_0?.disableAllHooks === true; 63: setDisabledByPolicy(hooksDisabled_0 && getSettingsForSource("policySettings")?.disableAllHooks === true); 64: setRestrictedByPolicy(getSettingsForSource("policySettings")?.allowManagedHooksOnly === true); 65: } 66: }; 67: $[1] = t2; 68: } else { 69: t2 = $[1]; 70: } 71: useSettingsChange(t2); 72: const mode = modeState.mode; 73: const selectedEvent = "event" in modeState ? modeState.event : "PreToolUse"; 74: const selectedMatcher = "matcher" in modeState ? modeState.matcher : null; 75: const mcp = useAppState(_temp3); 76: const appStateStore = useAppStateStore(); 77: let t3; 78: if ($[2] !== mcp.tools || $[3] !== toolNames) { 79: t3 = [...toolNames, ...mcp.tools.map(_temp4)]; 80: $[2] = mcp.tools; 81: $[3] = toolNames; 82: $[4] = t3; 83: } else { 84: t3 = $[4]; 85: } 86: const combinedToolNames = t3; 87: let t4; 88: if ($[5] !== appStateStore || $[6] !== combinedToolNames) { 89: t4 = groupHooksByEventAndMatcher(appStateStore.getState(), combinedToolNames); 90: $[5] = appStateStore; 91: $[6] = combinedToolNames; 92: $[7] = t4; 93: } else { 94: t4 = $[7]; 95: } 96: const hooksByEventAndMatcher = t4; 97: let t5; 98: if ($[8] !== hooksByEventAndMatcher || $[9] !== selectedEvent) { 99: t5 = getSortedMatchersForEvent(hooksByEventAndMatcher, selectedEvent); 100: $[8] = hooksByEventAndMatcher; 101: $[9] = selectedEvent; 102: $[10] = t5; 103: } else { 104: t5 = $[10]; 105: } 106: const sortedMatchersForSelectedEvent = t5; 107: let t6; 108: if ($[11] !== hooksByEventAndMatcher || $[12] !== selectedEvent || $[13] !== selectedMatcher) { 109: t6 = getHooksForMatcher(hooksByEventAndMatcher, selectedEvent, selectedMatcher); 110: $[11] = hooksByEventAndMatcher; 111: $[12] = selectedEvent; 112: $[13] = selectedMatcher; 113: $[14] = t6; 114: } else { 115: t6 = $[14]; 116: } 117: const hooksForSelectedMatcher = t6; 118: let t7; 119: if ($[15] !== onExit) { 120: t7 = () => { 121: onExit("Hooks dialog dismissed", { 122: display: "system" 123: }); 124: }; 125: $[15] = onExit; 126: $[16] = t7; 127: } else { 128: t7 = $[16]; 129: } 130: const handleExit = t7; 131: const t8 = mode === "select-event"; 132: let t9; 133: if ($[17] !== t8) { 134: t9 = { 135: context: "Confirmation", 136: isActive: t8 137: }; 138: $[17] = t8; 139: $[18] = t9; 140: } else { 141: t9 = $[18]; 142: } 143: useKeybinding("confirm:no", handleExit, t9); 144: let t10; 145: if ($[19] === Symbol.for("react.memo_cache_sentinel")) { 146: t10 = () => { 147: setModeState({ 148: mode: "select-event" 149: }); 150: }; 151: $[19] = t10; 152: } else { 153: t10 = $[19]; 154: } 155: const t11 = mode === "select-matcher"; 156: let t12; 157: if ($[20] !== t11) { 158: t12 = { 159: context: "Confirmation", 160: isActive: t11 161: }; 162: $[20] = t11; 163: $[21] = t12; 164: } else { 165: t12 = $[21]; 166: } 167: useKeybinding("confirm:no", t10, t12); 168: let t13; 169: if ($[22] !== combinedToolNames || $[23] !== modeState) { 170: t13 = () => { 171: if ("event" in modeState) { 172: if (getMatcherMetadata(modeState.event, combinedToolNames) !== undefined) { 173: setModeState({ 174: mode: "select-matcher", 175: event: modeState.event 176: }); 177: } else { 178: setModeState({ 179: mode: "select-event" 180: }); 181: } 182: } 183: }; 184: $[22] = combinedToolNames; 185: $[23] = modeState; 186: $[24] = t13; 187: } else { 188: t13 = $[24]; 189: } 190: const t14 = mode === "select-hook"; 191: let t15; 192: if ($[25] !== t14) { 193: t15 = { 194: context: "Confirmation", 195: isActive: t14 196: }; 197: $[25] = t14; 198: $[26] = t15; 199: } else { 200: t15 = $[26]; 201: } 202: useKeybinding("confirm:no", t13, t15); 203: let t16; 204: if ($[27] !== modeState) { 205: t16 = () => { 206: if (modeState.mode === "view-hook") { 207: const { 208: event, 209: hook 210: } = modeState; 211: setModeState({ 212: mode: "select-hook", 213: event, 214: matcher: hook.matcher || "" 215: }); 216: } 217: }; 218: $[27] = modeState; 219: $[28] = t16; 220: } else { 221: t16 = $[28]; 222: } 223: const t17 = mode === "view-hook"; 224: let t18; 225: if ($[29] !== t17) { 226: t18 = { 227: context: "Confirmation", 228: isActive: t17 229: }; 230: $[29] = t17; 231: $[30] = t18; 232: } else { 233: t18 = $[30]; 234: } 235: useKeybinding("confirm:no", t16, t18); 236: let t19; 237: if ($[31] !== combinedToolNames) { 238: t19 = getHookEventMetadata(combinedToolNames); 239: $[31] = combinedToolNames; 240: $[32] = t19; 241: } else { 242: t19 = $[32]; 243: } 244: const hookEventMetadata = t19; 245: const settings_1 = getSettings_DEPRECATED(); 246: const hooksDisabled_1 = settings_1?.disableAllHooks === true; 247: let t20; 248: if ($[33] !== hooksByEventAndMatcher) { 249: const byEvent = {}; 250: let total = 0; 251: for (const [event_0, matchers] of Object.entries(hooksByEventAndMatcher)) { 252: const eventCount = Object.values(matchers).reduce(_temp5, 0); 253: byEvent[event_0 as HookEvent] = eventCount; 254: total = total + eventCount; 255: } 256: t20 = { 257: hooksByEvent: byEvent, 258: totalHooksCount: total 259: }; 260: $[33] = hooksByEventAndMatcher; 261: $[34] = t20; 262: } else { 263: t20 = $[34]; 264: } 265: const { 266: hooksByEvent, 267: totalHooksCount 268: } = t20; 269: if (hooksDisabled_1) { 270: let t21; 271: if ($[35] === Symbol.for("react.memo_cache_sentinel")) { 272: t21 = <Text bold={true}>disabled</Text>; 273: $[35] = t21; 274: } else { 275: t21 = $[35]; 276: } 277: const t22 = disabledByPolicy && " by a managed settings file"; 278: let t23; 279: if ($[36] !== totalHooksCount) { 280: t23 = <Text bold={true}>{totalHooksCount}</Text>; 281: $[36] = totalHooksCount; 282: $[37] = t23; 283: } else { 284: t23 = $[37]; 285: } 286: let t24; 287: if ($[38] !== totalHooksCount) { 288: t24 = plural(totalHooksCount, "hook"); 289: $[38] = totalHooksCount; 290: $[39] = t24; 291: } else { 292: t24 = $[39]; 293: } 294: let t25; 295: if ($[40] !== totalHooksCount) { 296: t25 = plural(totalHooksCount, "is", "are"); 297: $[40] = totalHooksCount; 298: $[41] = t25; 299: } else { 300: t25 = $[41]; 301: } 302: let t26; 303: if ($[42] !== t22 || $[43] !== t23 || $[44] !== t24 || $[45] !== t25) { 304: t26 = <Text>All hooks are currently {t21}{t22}. You have{" "}{t23} configured{" "}{t24} that{" "}{t25} not running.</Text>; 305: $[42] = t22; 306: $[43] = t23; 307: $[44] = t24; 308: $[45] = t25; 309: $[46] = t26; 310: } else { 311: t26 = $[46]; 312: } 313: let t27; 314: let t28; 315: let t29; 316: let t30; 317: if ($[47] === Symbol.for("react.memo_cache_sentinel")) { 318: t27 = <Box marginTop={1}><Text dimColor={true}>When hooks are disabled:</Text></Box>; 319: t28 = <Text dimColor={true}>· No hook commands will execute</Text>; 320: t29 = <Text dimColor={true}>· StatusLine will not be displayed</Text>; 321: t30 = <Text dimColor={true}>· Tool operations will proceed without hook validation</Text>; 322: $[47] = t27; 323: $[48] = t28; 324: $[49] = t29; 325: $[50] = t30; 326: } else { 327: t27 = $[47]; 328: t28 = $[48]; 329: t29 = $[49]; 330: t30 = $[50]; 331: } 332: let t31; 333: if ($[51] !== t26) { 334: t31 = <Box flexDirection="column">{t26}{t27}{t28}{t29}{t30}</Box>; 335: $[51] = t26; 336: $[52] = t31; 337: } else { 338: t31 = $[52]; 339: } 340: let t32; 341: if ($[53] !== disabledByPolicy) { 342: t32 = !disabledByPolicy && <Text dimColor={true}>To re-enable hooks, remove "disableAllHooks" from settings.json or ask Claude.</Text>; 343: $[53] = disabledByPolicy; 344: $[54] = t32; 345: } else { 346: t32 = $[54]; 347: } 348: let t33; 349: if ($[55] !== t31 || $[56] !== t32) { 350: t33 = <Box flexDirection="column" gap={1}>{t31}{t32}</Box>; 351: $[55] = t31; 352: $[56] = t32; 353: $[57] = t33; 354: } else { 355: t33 = $[57]; 356: } 357: let t34; 358: if ($[58] !== handleExit || $[59] !== t33) { 359: t34 = <Dialog title="Hook Configuration - Disabled" onCancel={handleExit} inputGuide={_temp6}>{t33}</Dialog>; 360: $[58] = handleExit; 361: $[59] = t33; 362: $[60] = t34; 363: } else { 364: t34 = $[60]; 365: } 366: return t34; 367: } 368: switch (modeState.mode) { 369: case "select-event": 370: { 371: let t21; 372: if ($[61] !== combinedToolNames) { 373: t21 = event_2 => { 374: if (getMatcherMetadata(event_2, combinedToolNames) !== undefined) { 375: setModeState({ 376: mode: "select-matcher", 377: event: event_2 378: }); 379: } else { 380: setModeState({ 381: mode: "select-hook", 382: event: event_2, 383: matcher: "" 384: }); 385: } 386: }; 387: $[61] = combinedToolNames; 388: $[62] = t21; 389: } else { 390: t21 = $[62]; 391: } 392: let t22; 393: if ($[63] !== handleExit || $[64] !== hookEventMetadata || $[65] !== hooksByEvent || $[66] !== restrictedByPolicy || $[67] !== t21 || $[68] !== totalHooksCount) { 394: t22 = <SelectEventMode hookEventMetadata={hookEventMetadata} hooksByEvent={hooksByEvent} totalHooksCount={totalHooksCount} restrictedByPolicy={restrictedByPolicy} onSelectEvent={t21} onCancel={handleExit} />; 395: $[63] = handleExit; 396: $[64] = hookEventMetadata; 397: $[65] = hooksByEvent; 398: $[66] = restrictedByPolicy; 399: $[67] = t21; 400: $[68] = totalHooksCount; 401: $[69] = t22; 402: } else { 403: t22 = $[69]; 404: } 405: return t22; 406: } 407: case "select-matcher": 408: { 409: const t21 = hookEventMetadata[modeState.event]; 410: let t22; 411: if ($[70] !== modeState.event) { 412: t22 = matcher => { 413: setModeState({ 414: mode: "select-hook", 415: event: modeState.event, 416: matcher 417: }); 418: }; 419: $[70] = modeState.event; 420: $[71] = t22; 421: } else { 422: t22 = $[71]; 423: } 424: let t23; 425: if ($[72] === Symbol.for("react.memo_cache_sentinel")) { 426: t23 = () => { 427: setModeState({ 428: mode: "select-event" 429: }); 430: }; 431: $[72] = t23; 432: } else { 433: t23 = $[72]; 434: } 435: let t24; 436: if ($[73] !== hooksByEventAndMatcher || $[74] !== modeState.event || $[75] !== sortedMatchersForSelectedEvent || $[76] !== t21.description || $[77] !== t22) { 437: t24 = <SelectMatcherMode selectedEvent={modeState.event} matchersForSelectedEvent={sortedMatchersForSelectedEvent} hooksByEventAndMatcher={hooksByEventAndMatcher} eventDescription={t21.description} onSelect={t22} onCancel={t23} />; 438: $[73] = hooksByEventAndMatcher; 439: $[74] = modeState.event; 440: $[75] = sortedMatchersForSelectedEvent; 441: $[76] = t21.description; 442: $[77] = t22; 443: $[78] = t24; 444: } else { 445: t24 = $[78]; 446: } 447: return t24; 448: } 449: case "select-hook": 450: { 451: const t21 = hookEventMetadata[modeState.event]; 452: let t22; 453: if ($[79] !== modeState.event) { 454: t22 = hook_1 => { 455: setModeState({ 456: mode: "view-hook", 457: event: modeState.event, 458: hook: hook_1 459: }); 460: }; 461: $[79] = modeState.event; 462: $[80] = t22; 463: } else { 464: t22 = $[80]; 465: } 466: let t23; 467: if ($[81] !== combinedToolNames || $[82] !== modeState.event) { 468: t23 = () => { 469: if (getMatcherMetadata(modeState.event, combinedToolNames) !== undefined) { 470: setModeState({ 471: mode: "select-matcher", 472: event: modeState.event 473: }); 474: } else { 475: setModeState({ 476: mode: "select-event" 477: }); 478: } 479: }; 480: $[81] = combinedToolNames; 481: $[82] = modeState.event; 482: $[83] = t23; 483: } else { 484: t23 = $[83]; 485: } 486: let t24; 487: if ($[84] !== hooksForSelectedMatcher || $[85] !== modeState.event || $[86] !== modeState.matcher || $[87] !== t21 || $[88] !== t22 || $[89] !== t23) { 488: t24 = <SelectHookMode selectedEvent={modeState.event} selectedMatcher={modeState.matcher} hooksForSelectedMatcher={hooksForSelectedMatcher} hookEventMetadata={t21} onSelect={t22} onCancel={t23} />; 489: $[84] = hooksForSelectedMatcher; 490: $[85] = modeState.event; 491: $[86] = modeState.matcher; 492: $[87] = t21; 493: $[88] = t22; 494: $[89] = t23; 495: $[90] = t24; 496: } else { 497: t24 = $[90]; 498: } 499: return t24; 500: } 501: case "view-hook": 502: { 503: const t21 = modeState.hook; 504: let t22; 505: if ($[91] !== combinedToolNames || $[92] !== modeState.event) { 506: t22 = getMatcherMetadata(modeState.event, combinedToolNames); 507: $[91] = combinedToolNames; 508: $[92] = modeState.event; 509: $[93] = t22; 510: } else { 511: t22 = $[93]; 512: } 513: const t23 = t22 !== undefined; 514: let t24; 515: if ($[94] !== modeState) { 516: t24 = () => { 517: const { 518: event: event_1, 519: hook: hook_0 520: } = modeState; 521: setModeState({ 522: mode: "select-hook", 523: event: event_1, 524: matcher: hook_0.matcher || "" 525: }); 526: }; 527: $[94] = modeState; 528: $[95] = t24; 529: } else { 530: t24 = $[95]; 531: } 532: let t25; 533: if ($[96] !== modeState.hook || $[97] !== t23 || $[98] !== t24) { 534: t25 = <ViewHookMode selectedHook={t21} eventSupportsMatcher={t23} onCancel={t24} />; 535: $[96] = modeState.hook; 536: $[97] = t23; 537: $[98] = t24; 538: $[99] = t25; 539: } else { 540: t25 = $[99]; 541: } 542: return t25; 543: } 544: } 545: } 546: function _temp6() { 547: return <Text>Esc to close</Text>; 548: } 549: function _temp5(sum, hooks) { 550: return sum + hooks.length; 551: } 552: function _temp4(tool) { 553: return tool.name; 554: } 555: function _temp3(s) { 556: return s.mcp; 557: } 558: function _temp2() { 559: return getSettingsForSource("policySettings")?.allowManagedHooksOnly === true; 560: } 561: function _temp() { 562: const settings = getSettings_DEPRECATED(); 563: const hooksDisabled = settings?.disableAllHooks === true; 564: return hooksDisabled && getSettingsForSource("policySettings")?.disableAllHooks === true; 565: }

File: src/components/hooks/PromptDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 5: import type { PromptRequest } from '../../types/hooks.js'; 6: import { Select } from '../CustomSelect/select.js'; 7: import { PermissionDialog } from '../permissions/PermissionDialog.js'; 8: type Props = { 9: title: string; 10: toolInputSummary?: string | null; 11: request: PromptRequest; 12: onRespond: (key: string) => void; 13: onAbort: () => void; 14: }; 15: export function PromptDialog(t0) { 16: const $ = _c(15); 17: const { 18: title, 19: toolInputSummary, 20: request, 21: onRespond, 22: onAbort 23: } = t0; 24: let t1; 25: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 26: t1 = { 27: isActive: true 28: }; 29: $[0] = t1; 30: } else { 31: t1 = $[0]; 32: } 33: useKeybinding("app:interrupt", onAbort, t1); 34: let t2; 35: if ($[1] !== request.options) { 36: t2 = request.options.map(_temp); 37: $[1] = request.options; 38: $[2] = t2; 39: } else { 40: t2 = $[2]; 41: } 42: const options = t2; 43: let t3; 44: if ($[3] !== toolInputSummary) { 45: t3 = toolInputSummary ? <Text dimColor={true}>{toolInputSummary}</Text> : undefined; 46: $[3] = toolInputSummary; 47: $[4] = t3; 48: } else { 49: t3 = $[4]; 50: } 51: let t4; 52: if ($[5] !== onRespond) { 53: t4 = value => { 54: onRespond(value); 55: }; 56: $[5] = onRespond; 57: $[6] = t4; 58: } else { 59: t4 = $[6]; 60: } 61: let t5; 62: if ($[7] !== options || $[8] !== t4) { 63: t5 = <Box flexDirection="column" paddingY={1}><Select options={options} onChange={t4} /></Box>; 64: $[7] = options; 65: $[8] = t4; 66: $[9] = t5; 67: } else { 68: t5 = $[9]; 69: } 70: let t6; 71: if ($[10] !== request.message || $[11] !== t3 || $[12] !== t5 || $[13] !== title) { 72: t6 = <PermissionDialog title={title} subtitle={request.message} titleRight={t3}>{t5}</PermissionDialog>; 73: $[10] = request.message; 74: $[11] = t3; 75: $[12] = t5; 76: $[13] = title; 77: $[14] = t6; 78: } else { 79: t6 = $[14]; 80: } 81: return t6; 82: } 83: function _temp(opt) { 84: return { 85: label: opt.label, 86: value: opt.key, 87: description: opt.description 88: }; 89: }

File: src/components/hooks/SelectEventMode.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'; 5: import type { HookEventMetadata } from 'src/utils/hooks/hooksConfigManager.js'; 6: import { Box, Link, Text } from '../../ink.js'; 7: import { plural } from '../../utils/stringUtils.js'; 8: import { Select } from '../CustomSelect/select.js'; 9: import { Dialog } from '../design-system/Dialog.js'; 10: type Props = { 11: hookEventMetadata: Record<HookEvent, HookEventMetadata>; 12: hooksByEvent: Partial<Record<HookEvent, number>>; 13: totalHooksCount: number; 14: restrictedByPolicy: boolean; 15: onSelectEvent: (event: HookEvent) => void; 16: onCancel: () => void; 17: }; 18: export function SelectEventMode(t0) { 19: const $ = _c(23); 20: const { 21: hookEventMetadata, 22: hooksByEvent, 23: totalHooksCount, 24: restrictedByPolicy, 25: onSelectEvent, 26: onCancel 27: } = t0; 28: let t1; 29: if ($[0] !== totalHooksCount) { 30: t1 = plural(totalHooksCount, "hook"); 31: $[0] = totalHooksCount; 32: $[1] = t1; 33: } else { 34: t1 = $[1]; 35: } 36: const subtitle = `${totalHooksCount} ${t1} configured`; 37: let t2; 38: if ($[2] !== restrictedByPolicy) { 39: t2 = restrictedByPolicy && <Box flexDirection="column"><Text color="suggestion">{figures.info} Hooks Restricted by Policy</Text><Text dimColor={true}>Only hooks from managed settings can run. User-defined hooks from ~/.claude/settings.json, .claude/settings.json, and .claude/settings.local.json are blocked.</Text></Box>; 40: $[2] = restrictedByPolicy; 41: $[3] = t2; 42: } else { 43: t2 = $[3]; 44: } 45: let t3; 46: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 47: t3 = <Box flexDirection="column"><Text dimColor={true}>{figures.info} This menu is read-only. To add or modify hooks, edit settings.json directly or ask Claude.{" "}<Link url="https://code.claude.com/docs/en/hooks">Learn more</Link></Text></Box>; 48: $[4] = t3; 49: } else { 50: t3 = $[4]; 51: } 52: let t4; 53: if ($[5] !== onSelectEvent) { 54: t4 = value => { 55: onSelectEvent(value as HookEvent); 56: }; 57: $[5] = onSelectEvent; 58: $[6] = t4; 59: } else { 60: t4 = $[6]; 61: } 62: let t5; 63: if ($[7] !== hookEventMetadata) { 64: t5 = Object.entries(hookEventMetadata); 65: $[7] = hookEventMetadata; 66: $[8] = t5; 67: } else { 68: t5 = $[8]; 69: } 70: let t6; 71: if ($[9] !== hooksByEvent || $[10] !== t5) { 72: t6 = t5.map(t7 => { 73: const [name, metadata] = t7; 74: const count = hooksByEvent[name as HookEvent] || 0; 75: return { 76: label: count > 0 ? <Text>{name} <Text color="suggestion">({count})</Text></Text> : name, 77: value: name, 78: description: metadata.summary 79: }; 80: }); 81: $[9] = hooksByEvent; 82: $[10] = t5; 83: $[11] = t6; 84: } else { 85: t6 = $[11]; 86: } 87: let t7; 88: if ($[12] !== onCancel || $[13] !== t4 || $[14] !== t6) { 89: t7 = <Box flexDirection="column"><Select onChange={t4} onCancel={onCancel} options={t6} /></Box>; 90: $[12] = onCancel; 91: $[13] = t4; 92: $[14] = t6; 93: $[15] = t7; 94: } else { 95: t7 = $[15]; 96: } 97: let t8; 98: if ($[16] !== t2 || $[17] !== t7) { 99: t8 = <Box flexDirection="column" gap={1}>{t2}{t3}{t7}</Box>; 100: $[16] = t2; 101: $[17] = t7; 102: $[18] = t8; 103: } else { 104: t8 = $[18]; 105: } 106: let t9; 107: if ($[19] !== onCancel || $[20] !== subtitle || $[21] !== t8) { 108: t9 = <Dialog title="Hooks" subtitle={subtitle} onCancel={onCancel}>{t8}</Dialog>; 109: $[19] = onCancel; 110: $[20] = subtitle; 111: $[21] = t8; 112: $[22] = t9; 113: } else { 114: t9 = $[22]; 115: } 116: return t9; 117: }

File: src/components/hooks/SelectHookMode.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'; 4: import type { HookEventMetadata } from 'src/utils/hooks/hooksConfigManager.js'; 5: import { Box, Text } from '../../ink.js'; 6: import { getHookDisplayText, hookSourceHeaderDisplayString, type IndividualHookConfig } from '../../utils/hooks/hooksSettings.js'; 7: import { Select } from '../CustomSelect/select.js'; 8: import { Dialog } from '../design-system/Dialog.js'; 9: type Props = { 10: selectedEvent: HookEvent; 11: selectedMatcher: string | null; 12: hooksForSelectedMatcher: IndividualHookConfig[]; 13: hookEventMetadata: HookEventMetadata; 14: onSelect: (hook: IndividualHookConfig) => void; 15: onCancel: () => void; 16: }; 17: export function SelectHookMode(t0) { 18: const $ = _c(19); 19: const { 20: selectedEvent, 21: selectedMatcher, 22: hooksForSelectedMatcher, 23: hookEventMetadata, 24: onSelect, 25: onCancel 26: } = t0; 27: const title = hookEventMetadata.matcherMetadata !== undefined ? `${selectedEvent} - Matcher: ${selectedMatcher || "(all)"}` : selectedEvent; 28: if (hooksForSelectedMatcher.length === 0) { 29: let t1; 30: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 31: t1 = <Box flexDirection="column" gap={1}><Text dimColor={true}>No hooks configured for this event.</Text><Text dimColor={true}>To add hooks, edit settings.json directly or ask Claude.</Text></Box>; 32: $[0] = t1; 33: } else { 34: t1 = $[0]; 35: } 36: let t2; 37: if ($[1] !== hookEventMetadata.description || $[2] !== onCancel || $[3] !== title) { 38: t2 = <Dialog title={title} subtitle={hookEventMetadata.description} onCancel={onCancel} inputGuide={_temp}>{t1}</Dialog>; 39: $[1] = hookEventMetadata.description; 40: $[2] = onCancel; 41: $[3] = title; 42: $[4] = t2; 43: } else { 44: t2 = $[4]; 45: } 46: return t2; 47: } 48: const t1 = hookEventMetadata.description; 49: let t2; 50: if ($[5] !== hooksForSelectedMatcher) { 51: t2 = hooksForSelectedMatcher.map(_temp2); 52: $[5] = hooksForSelectedMatcher; 53: $[6] = t2; 54: } else { 55: t2 = $[6]; 56: } 57: let t3; 58: if ($[7] !== hooksForSelectedMatcher || $[8] !== onSelect) { 59: t3 = value => { 60: const index_0 = parseInt(value, 10); 61: const hook_0 = hooksForSelectedMatcher[index_0]; 62: if (hook_0) { 63: onSelect(hook_0); 64: } 65: }; 66: $[7] = hooksForSelectedMatcher; 67: $[8] = onSelect; 68: $[9] = t3; 69: } else { 70: t3 = $[9]; 71: } 72: let t4; 73: if ($[10] !== onCancel || $[11] !== t2 || $[12] !== t3) { 74: t4 = <Box flexDirection="column"><Select options={t2} onChange={t3} onCancel={onCancel} /></Box>; 75: $[10] = onCancel; 76: $[11] = t2; 77: $[12] = t3; 78: $[13] = t4; 79: } else { 80: t4 = $[13]; 81: } 82: let t5; 83: if ($[14] !== hookEventMetadata.description || $[15] !== onCancel || $[16] !== t4 || $[17] !== title) { 84: t5 = <Dialog title={title} subtitle={t1} onCancel={onCancel}>{t4}</Dialog>; 85: $[14] = hookEventMetadata.description; 86: $[15] = onCancel; 87: $[16] = t4; 88: $[17] = title; 89: $[18] = t5; 90: } else { 91: t5 = $[18]; 92: } 93: return t5; 94: } 95: function _temp2(hook, index) { 96: return { 97: label: `[${hook.config.type}] ${getHookDisplayText(hook.config)}`, 98: value: index.toString(), 99: description: hook.source === "pluginHook" && hook.pluginName ? `${hookSourceHeaderDisplayString(hook.source)} (${hook.pluginName})` : hookSourceHeaderDisplayString(hook.source) 100: }; 101: } 102: function _temp() { 103: return <Text>Esc to go back</Text>; 104: }

File: src/components/hooks/SelectMatcherMode.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'; 4: import { Box, Text } from '../../ink.js'; 5: import { type HookSource, hookSourceInlineDisplayString, type IndividualHookConfig } from '../../utils/hooks/hooksSettings.js'; 6: import { plural } from '../../utils/stringUtils.js'; 7: import { Select } from '../CustomSelect/select.js'; 8: import { Dialog } from '../design-system/Dialog.js'; 9: type MatcherWithSource = { 10: matcher: string; 11: sources: HookSource[]; 12: hookCount: number; 13: }; 14: type Props = { 15: selectedEvent: HookEvent; 16: matchersForSelectedEvent: string[]; 17: hooksByEventAndMatcher: Record<HookEvent, Record<string, IndividualHookConfig[]>>; 18: eventDescription: string; 19: onSelect: (matcher: string) => void; 20: onCancel: () => void; 21: }; 22: export function SelectMatcherMode(t0) { 23: const $ = _c(25); 24: const { 25: selectedEvent, 26: matchersForSelectedEvent, 27: hooksByEventAndMatcher, 28: eventDescription, 29: onSelect, 30: onCancel 31: } = t0; 32: let t1; 33: if ($[0] !== hooksByEventAndMatcher || $[1] !== matchersForSelectedEvent || $[2] !== selectedEvent) { 34: let t2; 35: if ($[4] !== hooksByEventAndMatcher || $[5] !== selectedEvent) { 36: t2 = matcher => { 37: const hooks = hooksByEventAndMatcher[selectedEvent]?.[matcher] || []; 38: const sources = Array.from(new Set(hooks.map(_temp))); 39: return { 40: matcher, 41: sources, 42: hookCount: hooks.length 43: }; 44: }; 45: $[4] = hooksByEventAndMatcher; 46: $[5] = selectedEvent; 47: $[6] = t2; 48: } else { 49: t2 = $[6]; 50: } 51: t1 = matchersForSelectedEvent.map(t2); 52: $[0] = hooksByEventAndMatcher; 53: $[1] = matchersForSelectedEvent; 54: $[2] = selectedEvent; 55: $[3] = t1; 56: } else { 57: t1 = $[3]; 58: } 59: const matchersWithSources = t1; 60: if (matchersForSelectedEvent.length === 0) { 61: const t2 = `${selectedEvent} - Matchers`; 62: let t3; 63: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 64: t3 = <Box flexDirection="column" gap={1}><Text dimColor={true}>No hooks configured for this event.</Text><Text dimColor={true}>To add hooks, edit settings.json directly or ask Claude.</Text></Box>; 65: $[7] = t3; 66: } else { 67: t3 = $[7]; 68: } 69: let t4; 70: if ($[8] !== eventDescription || $[9] !== onCancel || $[10] !== t2) { 71: t4 = <Dialog title={t2} subtitle={eventDescription} onCancel={onCancel} inputGuide={_temp2}>{t3}</Dialog>; 72: $[8] = eventDescription; 73: $[9] = onCancel; 74: $[10] = t2; 75: $[11] = t4; 76: } else { 77: t4 = $[11]; 78: } 79: return t4; 80: } 81: const t2 = `${selectedEvent} - Matchers`; 82: let t3; 83: if ($[12] !== matchersWithSources) { 84: t3 = matchersWithSources.map(_temp3); 85: $[12] = matchersWithSources; 86: $[13] = t3; 87: } else { 88: t3 = $[13]; 89: } 90: let t4; 91: if ($[14] !== onSelect) { 92: t4 = value => { 93: onSelect(value); 94: }; 95: $[14] = onSelect; 96: $[15] = t4; 97: } else { 98: t4 = $[15]; 99: } 100: let t5; 101: if ($[16] !== onCancel || $[17] !== t3 || $[18] !== t4) { 102: t5 = <Box flexDirection="column"><Select options={t3} onChange={t4} onCancel={onCancel} /></Box>; 103: $[16] = onCancel; 104: $[17] = t3; 105: $[18] = t4; 106: $[19] = t5; 107: } else { 108: t5 = $[19]; 109: } 110: let t6; 111: if ($[20] !== eventDescription || $[21] !== onCancel || $[22] !== t2 || $[23] !== t5) { 112: t6 = <Dialog title={t2} subtitle={eventDescription} onCancel={onCancel}>{t5}</Dialog>; 113: $[20] = eventDescription; 114: $[21] = onCancel; 115: $[22] = t2; 116: $[23] = t5; 117: $[24] = t6; 118: } else { 119: t6 = $[24]; 120: } 121: return t6; 122: } 123: function _temp3(item) { 124: const sourceText = item.sources.map(hookSourceInlineDisplayString).join(", "); 125: const matcherLabel = item.matcher || "(all)"; 126: return { 127: label: `[${sourceText}] ${matcherLabel}`, 128: value: item.matcher, 129: description: `${item.hookCount} ${plural(item.hookCount, "hook")}` 130: }; 131: } 132: function _temp2() { 133: return <Text>Esc to go back</Text>; 134: } 135: function _temp(h) { 136: return h.source; 137: }

File: src/components/hooks/ViewHookMode.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { hookSourceDescriptionDisplayString, type IndividualHookConfig } from '../../utils/hooks/hooksSettings.js'; 5: import { Dialog } from '../design-system/Dialog.js'; 6: type Props = { 7: selectedHook: IndividualHookConfig; 8: eventSupportsMatcher: boolean; 9: onCancel: () => void; 10: }; 11: export function ViewHookMode(t0) { 12: const $ = _c(40); 13: const { 14: selectedHook, 15: eventSupportsMatcher, 16: onCancel 17: } = t0; 18: let t1; 19: if ($[0] !== selectedHook.event) { 20: t1 = <Text>Event: <Text bold={true}>{selectedHook.event}</Text></Text>; 21: $[0] = selectedHook.event; 22: $[1] = t1; 23: } else { 24: t1 = $[1]; 25: } 26: let t2; 27: if ($[2] !== eventSupportsMatcher || $[3] !== selectedHook.matcher) { 28: t2 = eventSupportsMatcher && <Text>Matcher: <Text bold={true}>{selectedHook.matcher || "(all)"}</Text></Text>; 29: $[2] = eventSupportsMatcher; 30: $[3] = selectedHook.matcher; 31: $[4] = t2; 32: } else { 33: t2 = $[4]; 34: } 35: let t3; 36: if ($[5] !== selectedHook.config.type) { 37: t3 = <Text>Type: <Text bold={true}>{selectedHook.config.type}</Text></Text>; 38: $[5] = selectedHook.config.type; 39: $[6] = t3; 40: } else { 41: t3 = $[6]; 42: } 43: let t4; 44: if ($[7] !== selectedHook.source) { 45: t4 = hookSourceDescriptionDisplayString(selectedHook.source); 46: $[7] = selectedHook.source; 47: $[8] = t4; 48: } else { 49: t4 = $[8]; 50: } 51: let t5; 52: if ($[9] !== t4) { 53: t5 = <Text>Source:{" "}<Text dimColor={true}>{t4}</Text></Text>; 54: $[9] = t4; 55: $[10] = t5; 56: } else { 57: t5 = $[10]; 58: } 59: let t6; 60: if ($[11] !== selectedHook.pluginName) { 61: t6 = selectedHook.pluginName && <Text>Plugin: <Text dimColor={true}>{selectedHook.pluginName}</Text></Text>; 62: $[11] = selectedHook.pluginName; 63: $[12] = t6; 64: } else { 65: t6 = $[12]; 66: } 67: let t7; 68: if ($[13] !== t1 || $[14] !== t2 || $[15] !== t3 || $[16] !== t5 || $[17] !== t6) { 69: t7 = <Box flexDirection="column">{t1}{t2}{t3}{t5}{t6}</Box>; 70: $[13] = t1; 71: $[14] = t2; 72: $[15] = t3; 73: $[16] = t5; 74: $[17] = t6; 75: $[18] = t7; 76: } else { 77: t7 = $[18]; 78: } 79: let t8; 80: if ($[19] !== selectedHook.config) { 81: t8 = getContentFieldLabel(selectedHook.config); 82: $[19] = selectedHook.config; 83: $[20] = t8; 84: } else { 85: t8 = $[20]; 86: } 87: let t9; 88: if ($[21] !== t8) { 89: t9 = <Text dimColor={true}>{t8}:</Text>; 90: $[21] = t8; 91: $[22] = t9; 92: } else { 93: t9 = $[22]; 94: } 95: let t10; 96: if ($[23] !== selectedHook.config) { 97: t10 = getContentFieldValue(selectedHook.config); 98: $[23] = selectedHook.config; 99: $[24] = t10; 100: } else { 101: t10 = $[24]; 102: } 103: let t11; 104: if ($[25] !== t10) { 105: t11 = <Box borderStyle="round" borderDimColor={true} paddingLeft={1} paddingRight={1}><Text>{t10}</Text></Box>; 106: $[25] = t10; 107: $[26] = t11; 108: } else { 109: t11 = $[26]; 110: } 111: let t12; 112: if ($[27] !== t11 || $[28] !== t9) { 113: t12 = <Box flexDirection="column">{t9}{t11}</Box>; 114: $[27] = t11; 115: $[28] = t9; 116: $[29] = t12; 117: } else { 118: t12 = $[29]; 119: } 120: let t13; 121: if ($[30] !== selectedHook.config) { 122: t13 = "statusMessage" in selectedHook.config && selectedHook.config.statusMessage && <Text>Status message:{" "}<Text dimColor={true}>{selectedHook.config.statusMessage}</Text></Text>; 123: $[30] = selectedHook.config; 124: $[31] = t13; 125: } else { 126: t13 = $[31]; 127: } 128: let t14; 129: if ($[32] === Symbol.for("react.memo_cache_sentinel")) { 130: t14 = <Text dimColor={true}>To modify or remove this hook, edit settings.json directly or ask Claude to help.</Text>; 131: $[32] = t14; 132: } else { 133: t14 = $[32]; 134: } 135: let t15; 136: if ($[33] !== t12 || $[34] !== t13 || $[35] !== t7) { 137: t15 = <Box flexDirection="column" gap={1}>{t7}{t12}{t13}{t14}</Box>; 138: $[33] = t12; 139: $[34] = t13; 140: $[35] = t7; 141: $[36] = t15; 142: } else { 143: t15 = $[36]; 144: } 145: let t16; 146: if ($[37] !== onCancel || $[38] !== t15) { 147: t16 = <Dialog title="Hook details" onCancel={onCancel} inputGuide={_temp}>{t15}</Dialog>; 148: $[37] = onCancel; 149: $[38] = t15; 150: $[39] = t16; 151: } else { 152: t16 = $[39]; 153: } 154: return t16; 155: } 156: function _temp() { 157: return <Text>Esc to go back</Text>; 158: } 159: function getContentFieldLabel(config: IndividualHookConfig['config']): string { 160: switch (config.type) { 161: case 'command': 162: return 'Command'; 163: case 'prompt': 164: return 'Prompt'; 165: case 'agent': 166: return 'Prompt'; 167: case 'http': 168: return 'URL'; 169: } 170: } 171: function getContentFieldValue(config: IndividualHookConfig['config']): string { 172: switch (config.type) { 173: case 'command': 174: return config.command; 175: case 'prompt': 176: return config.prompt; 177: case 'agent': 178: return config.prompt; 179: case 'http': 180: return config.url; 181: } 182: }

File: src/components/LogoV2/AnimatedAsterisk.tsx

typescript 1: import * as React from 'react'; 2: import { useEffect, useRef, useState } from 'react'; 3: import { TEARDROP_ASTERISK } from '../../constants/figures.js'; 4: import { Box, Text, useAnimationFrame } from '../../ink.js'; 5: import { getInitialSettings } from '../../utils/settings/settings.js'; 6: import { hueToRgb, toRGBColor } from '../Spinner/utils.js'; 7: const SWEEP_DURATION_MS = 1500; 8: const SWEEP_COUNT = 2; 9: const TOTAL_ANIMATION_MS = SWEEP_DURATION_MS * SWEEP_COUNT; 10: const SETTLED_GREY = toRGBColor({ 11: r: 153, 12: g: 153, 13: b: 153 14: }); 15: export function AnimatedAsterisk({ 16: char = TEARDROP_ASTERISK 17: }: { 18: char?: string; 19: }): React.ReactNode { 20: const [reducedMotion] = useState(() => getInitialSettings().prefersReducedMotion ?? false); 21: const [done, setDone] = useState(reducedMotion); 22: const startTimeRef = useRef<number | null>(null); 23: const [ref, time] = useAnimationFrame(done ? null : 50); 24: useEffect(() => { 25: if (done) return; 26: const t = setTimeout(setDone, TOTAL_ANIMATION_MS, true); 27: return () => clearTimeout(t); 28: }, [done]); 29: if (done) { 30: return <Box ref={ref}> 31: <Text color={SETTLED_GREY}>{char}</Text> 32: </Box>; 33: } 34: if (startTimeRef.current === null) { 35: startTimeRef.current = time; 36: } 37: const elapsed = time - startTimeRef.current; 38: const hue = elapsed / SWEEP_DURATION_MS * 360 % 360; 39: return <Box ref={ref}> 40: <Text color={toRGBColor(hueToRgb(hue))}>{char}</Text> 41: </Box>; 42: }

File: src/components/LogoV2/AnimatedClawd.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useEffect, useRef, useState } from 'react'; 4: import { Box } from '../../ink.js'; 5: import { getInitialSettings } from '../../utils/settings/settings.js'; 6: import { Clawd, type ClawdPose } from './Clawd.js'; 7: type Frame = { 8: pose: ClawdPose; 9: offset: number; 10: }; 11: function hold(pose: ClawdPose, offset: number, frames: number): Frame[] { 12: return Array.from({ 13: length: frames 14: }, () => ({ 15: pose, 16: offset 17: })); 18: } 19: const JUMP_WAVE: readonly Frame[] = [...hold('default', 1, 2), 20: ...hold('arms-up', 0, 3), 21: ...hold('default', 0, 1), ...hold('default', 1, 2), 22: ...hold('arms-up', 0, 3), 23: ...hold('default', 0, 1)]; 24: const LOOK_AROUND: readonly Frame[] = [...hold('look-right', 0, 5), ...hold('look-left', 0, 5), ...hold('default', 0, 1)]; 25: const CLICK_ANIMATIONS: readonly (readonly Frame[])[] = [JUMP_WAVE, LOOK_AROUND]; 26: const IDLE: Frame = { 27: pose: 'default', 28: offset: 0 29: }; 30: const FRAME_MS = 60; 31: const incrementFrame = (i: number) => i + 1; 32: const CLAWD_HEIGHT = 3; 33: export function AnimatedClawd() { 34: const $ = _c(8); 35: const { 36: pose, 37: bounceOffset, 38: onClick 39: } = useClawdAnimation(); 40: let t0; 41: if ($[0] !== pose) { 42: t0 = <Clawd pose={pose} />; 43: $[0] = pose; 44: $[1] = t0; 45: } else { 46: t0 = $[1]; 47: } 48: let t1; 49: if ($[2] !== bounceOffset || $[3] !== t0) { 50: t1 = <Box marginTop={bounceOffset} flexShrink={0}>{t0}</Box>; 51: $[2] = bounceOffset; 52: $[3] = t0; 53: $[4] = t1; 54: } else { 55: t1 = $[4]; 56: } 57: let t2; 58: if ($[5] !== onClick || $[6] !== t1) { 59: t2 = <Box height={CLAWD_HEIGHT} flexDirection="column" onClick={onClick}>{t1}</Box>; 60: $[5] = onClick; 61: $[6] = t1; 62: $[7] = t2; 63: } else { 64: t2 = $[7]; 65: } 66: return t2; 67: } 68: function useClawdAnimation(): { 69: pose: ClawdPose; 70: bounceOffset: number; 71: onClick: () => void; 72: } { 73: const [reducedMotion] = useState(() => getInitialSettings().prefersReducedMotion ?? false); 74: const [frameIndex, setFrameIndex] = useState(-1); 75: const sequenceRef = useRef<readonly Frame[]>(JUMP_WAVE); 76: const onClick = () => { 77: if (reducedMotion || frameIndex !== -1) return; 78: sequenceRef.current = CLICK_ANIMATIONS[Math.floor(Math.random() * CLICK_ANIMATIONS.length)]!; 79: setFrameIndex(0); 80: }; 81: useEffect(() => { 82: if (frameIndex === -1) return; 83: if (frameIndex >= sequenceRef.current.length) { 84: setFrameIndex(-1); 85: return; 86: } 87: const timer = setTimeout(setFrameIndex, FRAME_MS, incrementFrame); 88: return () => clearTimeout(timer); 89: }, [frameIndex]); 90: const seq = sequenceRef.current; 91: const current = frameIndex >= 0 && frameIndex < seq.length ? seq[frameIndex]! : IDLE; 92: return { 93: pose: current.pose, 94: bounceOffset: current.offset, 95: onClick 96: }; 97: }

File: src/components/LogoV2/ChannelsNotice.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useState } from 'react'; 4: import { type ChannelEntry, getAllowedChannels, getHasDevChannels } from '../../bootstrap/state.js'; 5: import { Box, Text } from '../../ink.js'; 6: import { isChannelsEnabled } from '../../services/mcp/channelAllowlist.js'; 7: import { getEffectiveChannelAllowlist } from '../../services/mcp/channelNotification.js'; 8: import { getMcpConfigsByScope } from '../../services/mcp/config.js'; 9: import { getClaudeAIOAuthTokens, getSubscriptionType } from '../../utils/auth.js'; 10: import { loadInstalledPluginsV2 } from '../../utils/plugins/installedPluginsManager.js'; 11: import { getSettingsForSource } from '../../utils/settings/settings.js'; 12: export function ChannelsNotice() { 13: const $ = _c(32); 14: const [t0] = useState(_temp); 15: const { 16: channels, 17: disabled, 18: noAuth, 19: policyBlocked, 20: list, 21: unmatched 22: } = t0; 23: if (channels.length === 0) { 24: return null; 25: } 26: const hasNonDev = channels.some(_temp2); 27: const flag = getHasDevChannels() && hasNonDev ? "Channels" : getHasDevChannels() ? "--dangerously-load-development-channels" : "--channels"; 28: if (disabled) { 29: let t1; 30: if ($[0] !== flag || $[1] !== list) { 31: t1 = <Text color="error">{flag} ignored ({list})</Text>; 32: $[0] = flag; 33: $[1] = list; 34: $[2] = t1; 35: } else { 36: t1 = $[2]; 37: } 38: let t2; 39: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 40: t2 = <Text dimColor={true}>Channels are not currently available</Text>; 41: $[3] = t2; 42: } else { 43: t2 = $[3]; 44: } 45: let t3; 46: if ($[4] !== t1) { 47: t3 = <Box paddingLeft={2} flexDirection="column">{t1}{t2}</Box>; 48: $[4] = t1; 49: $[5] = t3; 50: } else { 51: t3 = $[5]; 52: } 53: return t3; 54: } 55: if (noAuth) { 56: let t1; 57: if ($[6] !== flag || $[7] !== list) { 58: t1 = <Text color="error">{flag} ignored ({list})</Text>; 59: $[6] = flag; 60: $[7] = list; 61: $[8] = t1; 62: } else { 63: t1 = $[8]; 64: } 65: let t2; 66: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 67: t2 = <Text dimColor={true}>Channels require claude.ai authentication · run /login, then restart</Text>; 68: $[9] = t2; 69: } else { 70: t2 = $[9]; 71: } 72: let t3; 73: if ($[10] !== t1) { 74: t3 = <Box paddingLeft={2} flexDirection="column">{t1}{t2}</Box>; 75: $[10] = t1; 76: $[11] = t3; 77: } else { 78: t3 = $[11]; 79: } 80: return t3; 81: } 82: if (policyBlocked) { 83: let t1; 84: if ($[12] !== flag || $[13] !== list) { 85: t1 = <Text color="error">{flag} blocked by org policy ({list})</Text>; 86: $[12] = flag; 87: $[13] = list; 88: $[14] = t1; 89: } else { 90: t1 = $[14]; 91: } 92: let t2; 93: let t3; 94: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 95: t2 = <Text dimColor={true}>Inbound messages will be silently dropped</Text>; 96: t3 = <Text dimColor={true}>Have an administrator set channelsEnabled: true in managed settings to enable</Text>; 97: $[15] = t2; 98: $[16] = t3; 99: } else { 100: t2 = $[15]; 101: t3 = $[16]; 102: } 103: let t4; 104: if ($[17] !== unmatched) { 105: t4 = unmatched.map(_temp3); 106: $[17] = unmatched; 107: $[18] = t4; 108: } else { 109: t4 = $[18]; 110: } 111: let t5; 112: if ($[19] !== t1 || $[20] !== t4) { 113: t5 = <Box paddingLeft={2} flexDirection="column">{t1}{t2}{t3}{t4}</Box>; 114: $[19] = t1; 115: $[20] = t4; 116: $[21] = t5; 117: } else { 118: t5 = $[21]; 119: } 120: return t5; 121: } 122: let t1; 123: if ($[22] !== list) { 124: t1 = <Text color="error">Listening for channel messages from: {list}</Text>; 125: $[22] = list; 126: $[23] = t1; 127: } else { 128: t1 = $[23]; 129: } 130: let t2; 131: if ($[24] !== flag) { 132: t2 = <Text dimColor={true}>Experimental · inbound messages will be pushed into this session, this carries prompt injection risks. Restart Claude Code without {flag} to disable.</Text>; 133: $[24] = flag; 134: $[25] = t2; 135: } else { 136: t2 = $[25]; 137: } 138: let t3; 139: if ($[26] !== unmatched) { 140: t3 = unmatched.map(_temp4); 141: $[26] = unmatched; 142: $[27] = t3; 143: } else { 144: t3 = $[27]; 145: } 146: let t4; 147: if ($[28] !== t1 || $[29] !== t2 || $[30] !== t3) { 148: t4 = <Box paddingLeft={2} flexDirection="column">{t1}{t2}{t3}</Box>; 149: $[28] = t1; 150: $[29] = t2; 151: $[30] = t3; 152: $[31] = t4; 153: } else { 154: t4 = $[31]; 155: } 156: return t4; 157: } 158: function _temp4(u_0) { 159: return <Text key={`${formatEntry(u_0.entry)}:${u_0.why}`} color="warning">{formatEntry(u_0.entry)} · {u_0.why}</Text>; 160: } 161: function _temp3(u) { 162: return <Text key={`${formatEntry(u.entry)}:${u.why}`} color="warning">{formatEntry(u.entry)} · {u.why}</Text>; 163: } 164: function _temp2(c) { 165: return !c.dev; 166: } 167: function _temp() { 168: const ch = getAllowedChannels(); 169: if (ch.length === 0) { 170: return { 171: channels: ch, 172: disabled: false, 173: noAuth: false, 174: policyBlocked: false, 175: list: "", 176: unmatched: [] as Unmatched[] 177: }; 178: } 179: const l = ch.map(formatEntry).join(", "); 180: const sub = getSubscriptionType(); 181: const managed = sub === "team" || sub === "enterprise"; 182: const policy = getSettingsForSource("policySettings"); 183: const allowlist = getEffectiveChannelAllowlist(sub, policy?.allowedChannelPlugins); 184: return { 185: channels: ch, 186: disabled: !isChannelsEnabled(), 187: noAuth: !getClaudeAIOAuthTokens()?.accessToken, 188: policyBlocked: managed && policy?.channelsEnabled !== true, 189: list: l, 190: unmatched: findUnmatched(ch, allowlist) 191: }; 192: } 193: function formatEntry(c: ChannelEntry): string { 194: return c.kind === 'plugin' ? `plugin:${c.name}@${c.marketplace}` : `server:${c.name}`; 195: } 196: type Unmatched = { 197: entry: ChannelEntry; 198: why: string; 199: }; 200: function findUnmatched(entries: readonly ChannelEntry[], allowlist: ReturnType<typeof getEffectiveChannelAllowlist>): Unmatched[] { 201: const scopes = ['enterprise', 'user', 'project', 'local'] as const; 202: const configured = new Set<string>(); 203: for (const scope of scopes) { 204: for (const name of Object.keys(getMcpConfigsByScope(scope).servers)) { 205: configured.add(name); 206: } 207: } 208: const installedPluginIds = new Set(Object.keys(loadInstalledPluginsV2().plugins)); 209: const { 210: entries: allowed, 211: source 212: } = allowlist; 213: const out: Unmatched[] = []; 214: for (const entry of entries) { 215: if (entry.kind === 'server') { 216: if (!configured.has(entry.name)) { 217: out.push({ 218: entry, 219: why: 'no MCP server configured with that name' 220: }); 221: } 222: if (!entry.dev) { 223: out.push({ 224: entry, 225: why: 'server: entries need --dangerously-load-development-channels' 226: }); 227: } 228: continue; 229: } 230: if (!installedPluginIds.has(`${entry.name}@${entry.marketplace}`)) { 231: out.push({ 232: entry, 233: why: 'plugin not installed' 234: }); 235: } 236: if (!entry.dev && !allowed.some(e => e.plugin === entry.name && e.marketplace === entry.marketplace)) { 237: out.push({ 238: entry, 239: why: source === 'org' ? "not on your org's approved channels list" : 'not on the approved channels allowlist' 240: }); 241: } 242: } 243: return out; 244: }

File: src/components/LogoV2/Clawd.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { env } from '../../utils/env.js'; 5: export type ClawdPose = 'default' | 'arms-up' 6: | 'look-left' 7: | 'look-right'; 8: type Props = { 9: pose?: ClawdPose; 10: }; 11: type Segments = { 12: r1L: string; 13: r1E: string; 14: r1R: string; 15: r2L: string; 16: r2R: string; 17: }; 18: const POSES: Record<ClawdPose, Segments> = { 19: default: { 20: r1L: ' ▐', 21: r1E: '▛███▜', 22: r1R: '▌', 23: r2L: '▝▜', 24: r2R: '▛▘' 25: }, 26: 'look-left': { 27: r1L: ' ▐', 28: r1E: '▟███▟', 29: r1R: '▌', 30: r2L: '▝▜', 31: r2R: '▛▘' 32: }, 33: 'look-right': { 34: r1L: ' ▐', 35: r1E: '▙███▙', 36: r1R: '▌', 37: r2L: '▝▜', 38: r2R: '▛▘' 39: }, 40: 'arms-up': { 41: r1L: '▗▟', 42: r1E: '▛███▜', 43: r1R: '▙▖', 44: r2L: ' ▜', 45: r2R: '▛ ' 46: } 47: }; 48: const APPLE_EYES: Record<ClawdPose, string> = { 49: default: ' ▗ ▖ ', 50: 'look-left': ' ▘ ▘ ', 51: 'look-right': ' ▝ ▝ ', 52: 'arms-up': ' ▗ ▖ ' 53: }; 54: export function Clawd(t0) { 55: const $ = _c(26); 56: let t1; 57: if ($[0] !== t0) { 58: t1 = t0 === undefined ? {} : t0; 59: $[0] = t0; 60: $[1] = t1; 61: } else { 62: t1 = $[1]; 63: } 64: const { 65: pose: t2 66: } = t1; 67: const pose = t2 === undefined ? "default" : t2; 68: if (env.terminal === "Apple_Terminal") { 69: let t3; 70: if ($[2] !== pose) { 71: t3 = <AppleTerminalClawd pose={pose} />; 72: $[2] = pose; 73: $[3] = t3; 74: } else { 75: t3 = $[3]; 76: } 77: return t3; 78: } 79: const p = POSES[pose]; 80: let t3; 81: if ($[4] !== p.r1L) { 82: t3 = <Text color="clawd_body">{p.r1L}</Text>; 83: $[4] = p.r1L; 84: $[5] = t3; 85: } else { 86: t3 = $[5]; 87: } 88: let t4; 89: if ($[6] !== p.r1E) { 90: t4 = <Text color="clawd_body" backgroundColor="clawd_background">{p.r1E}</Text>; 91: $[6] = p.r1E; 92: $[7] = t4; 93: } else { 94: t4 = $[7]; 95: } 96: let t5; 97: if ($[8] !== p.r1R) { 98: t5 = <Text color="clawd_body">{p.r1R}</Text>; 99: $[8] = p.r1R; 100: $[9] = t5; 101: } else { 102: t5 = $[9]; 103: } 104: let t6; 105: if ($[10] !== t3 || $[11] !== t4 || $[12] !== t5) { 106: t6 = <Text>{t3}{t4}{t5}</Text>; 107: $[10] = t3; 108: $[11] = t4; 109: $[12] = t5; 110: $[13] = t6; 111: } else { 112: t6 = $[13]; 113: } 114: let t7; 115: if ($[14] !== p.r2L) { 116: t7 = <Text color="clawd_body">{p.r2L}</Text>; 117: $[14] = p.r2L; 118: $[15] = t7; 119: } else { 120: t7 = $[15]; 121: } 122: let t8; 123: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 124: t8 = <Text color="clawd_body" backgroundColor="clawd_background">█████</Text>; 125: $[16] = t8; 126: } else { 127: t8 = $[16]; 128: } 129: let t9; 130: if ($[17] !== p.r2R) { 131: t9 = <Text color="clawd_body">{p.r2R}</Text>; 132: $[17] = p.r2R; 133: $[18] = t9; 134: } else { 135: t9 = $[18]; 136: } 137: let t10; 138: if ($[19] !== t7 || $[20] !== t9) { 139: t10 = <Text>{t7}{t8}{t9}</Text>; 140: $[19] = t7; 141: $[20] = t9; 142: $[21] = t10; 143: } else { 144: t10 = $[21]; 145: } 146: let t11; 147: if ($[22] === Symbol.for("react.memo_cache_sentinel")) { 148: t11 = <Text color="clawd_body">{" "}▘▘ ▝▝{" "}</Text>; 149: $[22] = t11; 150: } else { 151: t11 = $[22]; 152: } 153: let t12; 154: if ($[23] !== t10 || $[24] !== t6) { 155: t12 = <Box flexDirection="column">{t6}{t10}{t11}</Box>; 156: $[23] = t10; 157: $[24] = t6; 158: $[25] = t12; 159: } else { 160: t12 = $[25]; 161: } 162: return t12; 163: } 164: function AppleTerminalClawd(t0) { 165: const $ = _c(10); 166: const { 167: pose 168: } = t0; 169: let t1; 170: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 171: t1 = <Text color="clawd_body">▗</Text>; 172: $[0] = t1; 173: } else { 174: t1 = $[0]; 175: } 176: const t2 = APPLE_EYES[pose]; 177: let t3; 178: if ($[1] !== t2) { 179: t3 = <Text color="clawd_background" backgroundColor="clawd_body">{t2}</Text>; 180: $[1] = t2; 181: $[2] = t3; 182: } else { 183: t3 = $[2]; 184: } 185: let t4; 186: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 187: t4 = <Text color="clawd_body">▖</Text>; 188: $[3] = t4; 189: } else { 190: t4 = $[3]; 191: } 192: let t5; 193: if ($[4] !== t3) { 194: t5 = <Text>{t1}{t3}{t4}</Text>; 195: $[4] = t3; 196: $[5] = t5; 197: } else { 198: t5 = $[5]; 199: } 200: let t6; 201: let t7; 202: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 203: t6 = <Text backgroundColor="clawd_body">{" ".repeat(7)}</Text>; 204: t7 = <Text color="clawd_body">▘▘ ▝▝</Text>; 205: $[6] = t6; 206: $[7] = t7; 207: } else { 208: t6 = $[6]; 209: t7 = $[7]; 210: } 211: let t8; 212: if ($[8] !== t5) { 213: t8 = <Box flexDirection="column" alignItems="center">{t5}{t6}{t7}</Box>; 214: $[8] = t5; 215: $[9] = t8; 216: } else { 217: t8 = $[9]; 218: } 219: return t8; 220: }

File: src/components/LogoV2/CondensedLogo.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { type ReactNode, useEffect } from 'react'; 4: import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; 5: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 6: import { stringWidth } from '../../ink/stringWidth.js'; 7: import { Box, Text } from '../../ink.js'; 8: import { useAppState } from '../../state/AppState.js'; 9: import { getEffortSuffix } from '../../utils/effort.js'; 10: import { truncate } from '../../utils/format.js'; 11: import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; 12: import { formatModelAndBilling, getLogoDisplayData, truncatePath } from '../../utils/logoV2Utils.js'; 13: import { renderModelSetting } from '../../utils/model/model.js'; 14: import { OffscreenFreeze } from '../OffscreenFreeze.js'; 15: import { AnimatedClawd } from './AnimatedClawd.js'; 16: import { Clawd } from './Clawd.js'; 17: import { GuestPassesUpsell, incrementGuestPassesSeenCount, useShowGuestPassesUpsell } from './GuestPassesUpsell.js'; 18: import { incrementOverageCreditUpsellSeenCount, OverageCreditUpsell, useShowOverageCreditUpsell } from './OverageCreditUpsell.js'; 19: export function CondensedLogo() { 20: const $ = _c(29); 21: const { 22: columns 23: } = useTerminalSize(); 24: const agent = useAppState(_temp); 25: const effortValue = useAppState(_temp2); 26: const model = useMainLoopModel(); 27: const modelDisplayName = renderModelSetting(model); 28: const { 29: version, 30: cwd, 31: billingType, 32: agentName: agentNameFromSettings 33: } = getLogoDisplayData(); 34: const agentName = agent ?? agentNameFromSettings; 35: const showGuestPassesUpsell = useShowGuestPassesUpsell(); 36: const showOverageCreditUpsell = useShowOverageCreditUpsell(); 37: let t0; 38: let t1; 39: if ($[0] !== showGuestPassesUpsell) { 40: t0 = () => { 41: if (showGuestPassesUpsell) { 42: incrementGuestPassesSeenCount(); 43: } 44: }; 45: t1 = [showGuestPassesUpsell]; 46: $[0] = showGuestPassesUpsell; 47: $[1] = t0; 48: $[2] = t1; 49: } else { 50: t0 = $[1]; 51: t1 = $[2]; 52: } 53: useEffect(t0, t1); 54: let t2; 55: let t3; 56: if ($[3] !== showGuestPassesUpsell || $[4] !== showOverageCreditUpsell) { 57: t2 = () => { 58: if (showOverageCreditUpsell && !showGuestPassesUpsell) { 59: incrementOverageCreditUpsellSeenCount(); 60: } 61: }; 62: t3 = [showOverageCreditUpsell, showGuestPassesUpsell]; 63: $[3] = showGuestPassesUpsell; 64: $[4] = showOverageCreditUpsell; 65: $[5] = t2; 66: $[6] = t3; 67: } else { 68: t2 = $[5]; 69: t3 = $[6]; 70: } 71: useEffect(t2, t3); 72: const textWidth = Math.max(columns - 15, 20); 73: const truncatedVersion = truncate(version, Math.max(textWidth - 13, 6)); 74: const effortSuffix = getEffortSuffix(model, effortValue); 75: const { 76: shouldSplit, 77: truncatedModel, 78: truncatedBilling 79: } = formatModelAndBilling(modelDisplayName + effortSuffix, billingType, textWidth); 80: const cwdAvailableWidth = agentName ? textWidth - 1 - stringWidth(agentName) - 3 : textWidth; 81: const truncatedCwd = truncatePath(cwd, Math.max(cwdAvailableWidth, 10)); 82: let t4; 83: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 84: t4 = isFullscreenEnvEnabled() ? <AnimatedClawd /> : <Clawd />; 85: $[7] = t4; 86: } else { 87: t4 = $[7]; 88: } 89: let t5; 90: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 91: t5 = <Text bold={true}>Claude Code</Text>; 92: $[8] = t5; 93: } else { 94: t5 = $[8]; 95: } 96: let t6; 97: if ($[9] !== truncatedVersion) { 98: t6 = <Text>{t5}{" "}<Text dimColor={true}>v{truncatedVersion}</Text></Text>; 99: $[9] = truncatedVersion; 100: $[10] = t6; 101: } else { 102: t6 = $[10]; 103: } 104: let t7; 105: if ($[11] !== shouldSplit || $[12] !== truncatedBilling || $[13] !== truncatedModel) { 106: t7 = shouldSplit ? <><Text dimColor={true}>{truncatedModel}</Text><Text dimColor={true}>{truncatedBilling}</Text></> : <Text dimColor={true}>{truncatedModel} · {truncatedBilling}</Text>; 107: $[11] = shouldSplit; 108: $[12] = truncatedBilling; 109: $[13] = truncatedModel; 110: $[14] = t7; 111: } else { 112: t7 = $[14]; 113: } 114: const t8 = agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd; 115: let t9; 116: if ($[15] !== t8) { 117: t9 = <Text dimColor={true}>{t8}</Text>; 118: $[15] = t8; 119: $[16] = t9; 120: } else { 121: t9 = $[16]; 122: } 123: let t10; 124: if ($[17] !== showGuestPassesUpsell) { 125: t10 = showGuestPassesUpsell && <GuestPassesUpsell />; 126: $[17] = showGuestPassesUpsell; 127: $[18] = t10; 128: } else { 129: t10 = $[18]; 130: } 131: let t11; 132: if ($[19] !== showGuestPassesUpsell || $[20] !== showOverageCreditUpsell || $[21] !== textWidth) { 133: t11 = !showGuestPassesUpsell && showOverageCreditUpsell && <OverageCreditUpsell maxWidth={textWidth} twoLine={true} />; 134: $[19] = showGuestPassesUpsell; 135: $[20] = showOverageCreditUpsell; 136: $[21] = textWidth; 137: $[22] = t11; 138: } else { 139: t11 = $[22]; 140: } 141: let t12; 142: if ($[23] !== t10 || $[24] !== t11 || $[25] !== t6 || $[26] !== t7 || $[27] !== t9) { 143: t12 = <OffscreenFreeze><Box flexDirection="row" gap={2} alignItems="center">{t4}<Box flexDirection="column">{t6}{t7}{t9}{t10}{t11}</Box></Box></OffscreenFreeze>; 144: $[23] = t10; 145: $[24] = t11; 146: $[25] = t6; 147: $[26] = t7; 148: $[27] = t9; 149: $[28] = t12; 150: } else { 151: t12 = $[28]; 152: } 153: return t12; 154: } 155: function _temp2(s_0) { 156: return s_0.effortValue; 157: } 158: function _temp(s) { 159: return s.agent; 160: }

File: src/components/LogoV2/EmergencyTip.tsx

typescript 1: import * as React from 'react'; 2: import { useEffect, useMemo } from 'react'; 3: import { Box, Text } from 'src/ink.js'; 4: import { getDynamicConfig_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; 5: import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js'; 6: const CONFIG_NAME = 'tengu-top-of-feed-tip'; 7: export function EmergencyTip(): React.ReactNode { 8: const tip = useMemo(getTipOfFeed, []); 9: const lastShownTip = useMemo(() => getGlobalConfig().lastShownEmergencyTip, []); 10: const shouldShow = tip.tip && tip.tip !== lastShownTip; 11: useEffect(() => { 12: if (shouldShow) { 13: saveGlobalConfig(current => { 14: if (current.lastShownEmergencyTip === tip.tip) return current; 15: return { 16: ...current, 17: lastShownEmergencyTip: tip.tip 18: }; 19: }); 20: } 21: }, [shouldShow, tip.tip]); 22: if (!shouldShow) { 23: return null; 24: } 25: return <Box paddingLeft={2} flexDirection="column"> 26: <Text {...tip.color === 'warning' ? { 27: color: 'warning' 28: } : tip.color === 'error' ? { 29: color: 'error' 30: } : { 31: dimColor: true 32: }}> 33: {tip.tip} 34: </Text> 35: </Box>; 36: } 37: type TipOfFeed = { 38: tip: string; 39: color?: 'dim' | 'warning' | 'error'; 40: }; 41: const DEFAULT_TIP: TipOfFeed = { 42: tip: '', 43: color: 'dim' 44: }; 45: function getTipOfFeed(): TipOfFeed { 46: return getDynamicConfig_CACHED_MAY_BE_STALE<TipOfFeed>(CONFIG_NAME, DEFAULT_TIP); 47: }

File: src/components/LogoV2/Feed.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { stringWidth } from '../../ink/stringWidth.js'; 4: import { Box, Text } from '../../ink.js'; 5: import { truncate } from '../../utils/format.js'; 6: export type FeedLine = { 7: text: string; 8: timestamp?: string; 9: }; 10: export type FeedConfig = { 11: title: string; 12: lines: FeedLine[]; 13: footer?: string; 14: emptyMessage?: string; 15: customContent?: { 16: content: React.ReactNode; 17: width: number; 18: }; 19: }; 20: type FeedProps = { 21: config: FeedConfig; 22: actualWidth: number; 23: }; 24: export function calculateFeedWidth(config: FeedConfig): number { 25: const { 26: title, 27: lines, 28: footer, 29: emptyMessage, 30: customContent 31: } = config; 32: let maxWidth = stringWidth(title); 33: if (customContent !== undefined) { 34: maxWidth = Math.max(maxWidth, customContent.width); 35: } else if (lines.length === 0 && emptyMessage) { 36: maxWidth = Math.max(maxWidth, stringWidth(emptyMessage)); 37: } else { 38: const gap = ' '; 39: const maxTimestampWidth = Math.max(0, ...lines.map(line => line.timestamp ? stringWidth(line.timestamp) : 0)); 40: for (const line of lines) { 41: const timestampWidth = maxTimestampWidth > 0 ? maxTimestampWidth : 0; 42: const lineWidth = stringWidth(line.text) + (timestampWidth > 0 ? timestampWidth + gap.length : 0); 43: maxWidth = Math.max(maxWidth, lineWidth); 44: } 45: } 46: if (footer) { 47: maxWidth = Math.max(maxWidth, stringWidth(footer)); 48: } 49: return maxWidth; 50: } 51: export function Feed(t0) { 52: const $ = _c(15); 53: const { 54: config, 55: actualWidth 56: } = t0; 57: const { 58: title, 59: lines, 60: footer, 61: emptyMessage, 62: customContent 63: } = config; 64: let t1; 65: if ($[0] !== lines) { 66: t1 = Math.max(0, ...lines.map(_temp)); 67: $[0] = lines; 68: $[1] = t1; 69: } else { 70: t1 = $[1]; 71: } 72: const maxTimestampWidth = t1; 73: let t2; 74: if ($[2] !== title) { 75: t2 = <Text bold={true} color="claude">{title}</Text>; 76: $[2] = title; 77: $[3] = t2; 78: } else { 79: t2 = $[3]; 80: } 81: let t3; 82: if ($[4] !== actualWidth || $[5] !== customContent || $[6] !== emptyMessage || $[7] !== footer || $[8] !== lines || $[9] !== maxTimestampWidth) { 83: t3 = customContent ? <>{customContent.content}{footer && <Text dimColor={true} italic={true}>{truncate(footer, actualWidth)}</Text>}</> : lines.length === 0 && emptyMessage ? <Text dimColor={true}>{truncate(emptyMessage, actualWidth)}</Text> : <>{lines.map((line_0, index) => { 84: const textWidth = Math.max(10, actualWidth - (maxTimestampWidth > 0 ? maxTimestampWidth + 2 : 0)); 85: return <Text key={index}>{maxTimestampWidth > 0 && <><Text dimColor={true}>{(line_0.timestamp || "").padEnd(maxTimestampWidth)}</Text>{" "}</>}<Text>{truncate(line_0.text, textWidth)}</Text></Text>; 86: })}{footer && <Text dimColor={true} italic={true}>{truncate(footer, actualWidth)}</Text>}</>; 87: $[4] = actualWidth; 88: $[5] = customContent; 89: $[6] = emptyMessage; 90: $[7] = footer; 91: $[8] = lines; 92: $[9] = maxTimestampWidth; 93: $[10] = t3; 94: } else { 95: t3 = $[10]; 96: } 97: let t4; 98: if ($[11] !== actualWidth || $[12] !== t2 || $[13] !== t3) { 99: t4 = <Box flexDirection="column" width={actualWidth}>{t2}{t3}</Box>; 100: $[11] = actualWidth; 101: $[12] = t2; 102: $[13] = t3; 103: $[14] = t4; 104: } else { 105: t4 = $[14]; 106: } 107: return t4; 108: } 109: function _temp(line) { 110: return line.timestamp ? stringWidth(line.timestamp) : 0; 111: }

File: src/components/LogoV2/FeedColumn.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box } from '../../ink.js'; 4: import { Divider } from '../design-system/Divider.js'; 5: import type { FeedConfig } from './Feed.js'; 6: import { calculateFeedWidth, Feed } from './Feed.js'; 7: type FeedColumnProps = { 8: feeds: FeedConfig[]; 9: maxWidth: number; 10: }; 11: export function FeedColumn(t0) { 12: const $ = _c(10); 13: const { 14: feeds, 15: maxWidth 16: } = t0; 17: let t1; 18: if ($[0] !== feeds) { 19: const feedWidths = feeds.map(_temp); 20: t1 = Math.max(...feedWidths); 21: $[0] = feeds; 22: $[1] = t1; 23: } else { 24: t1 = $[1]; 25: } 26: const maxOfAllFeeds = t1; 27: const actualWidth = Math.min(maxOfAllFeeds, maxWidth); 28: let t2; 29: if ($[2] !== actualWidth || $[3] !== feeds) { 30: let t3; 31: if ($[5] !== actualWidth || $[6] !== feeds.length) { 32: t3 = (feed_0, index) => <React.Fragment key={index}><Feed config={feed_0} actualWidth={actualWidth} />{index < feeds.length - 1 && <Divider color="claude" width={actualWidth} />}</React.Fragment>; 33: $[5] = actualWidth; 34: $[6] = feeds.length; 35: $[7] = t3; 36: } else { 37: t3 = $[7]; 38: } 39: t2 = feeds.map(t3); 40: $[2] = actualWidth; 41: $[3] = feeds; 42: $[4] = t2; 43: } else { 44: t2 = $[4]; 45: } 46: let t3; 47: if ($[8] !== t2) { 48: t3 = <Box flexDirection="column">{t2}</Box>; 49: $[8] = t2; 50: $[9] = t3; 51: } else { 52: t3 = $[9]; 53: } 54: return t3; 55: } 56: function _temp(feed) { 57: return calculateFeedWidth(feed); 58: }

File: src/components/LogoV2/feedConfigs.tsx

typescript 1: import figures from 'figures'; 2: import { homedir } from 'os'; 3: import * as React from 'react'; 4: import { Box, Text } from '../../ink.js'; 5: import type { Step } from '../../projectOnboardingState.js'; 6: import { formatCreditAmount, getCachedReferrerReward } from '../../services/api/referral.js'; 7: import type { LogOption } from '../../types/logs.js'; 8: import { getCwd } from '../../utils/cwd.js'; 9: import { formatRelativeTimeAgo } from '../../utils/format.js'; 10: import type { FeedConfig, FeedLine } from './Feed.js'; 11: export function createRecentActivityFeed(activities: LogOption[]): FeedConfig { 12: const lines: FeedLine[] = activities.map(log => { 13: const time = formatRelativeTimeAgo(log.modified); 14: const description = log.summary && log.summary !== 'No prompt' ? log.summary : log.firstPrompt; 15: return { 16: text: description || '', 17: timestamp: time 18: }; 19: }); 20: return { 21: title: 'Recent activity', 22: lines, 23: footer: lines.length > 0 ? '/resume for more' : undefined, 24: emptyMessage: 'No recent activity' 25: }; 26: } 27: export function createWhatsNewFeed(releaseNotes: string[]): FeedConfig { 28: const lines: FeedLine[] = releaseNotes.map(note => { 29: if ("external" === 'ant') { 30: const match = note.match(/^(\d+\s+\w+\s+ago)\s+(.+)$/); 31: if (match) { 32: return { 33: timestamp: match[1], 34: text: match[2] || '' 35: }; 36: } 37: } 38: return { 39: text: note 40: }; 41: }); 42: const emptyMessage = "external" === 'ant' ? 'Unable to fetch latest claude-cli-internal commits' : 'Check the Claude Code changelog for updates'; 43: return { 44: title: "external" === 'ant' ? "What's new [ANT-ONLY: Latest CC commits]" : "What's new", 45: lines, 46: footer: lines.length > 0 ? '/release-notes for more' : undefined, 47: emptyMessage 48: }; 49: } 50: export function createProjectOnboardingFeed(steps: Step[]): FeedConfig { 51: const enabledSteps = steps.filter(({ 52: isEnabled 53: }) => isEnabled).sort((a, b) => Number(a.isComplete) - Number(b.isComplete)); 54: const lines: FeedLine[] = enabledSteps.map(({ 55: text, 56: isComplete 57: }) => { 58: const checkmark = isComplete ? `${figures.tick} ` : ''; 59: return { 60: text: `${checkmark}${text}` 61: }; 62: }); 63: const warningText = getCwd() === homedir() ? 'Note: You have launched claude in your home directory. For the best experience, launch it in a project directory instead.' : undefined; 64: if (warningText) { 65: lines.push({ 66: text: warningText 67: }); 68: } 69: return { 70: title: 'Tips for getting started', 71: lines 72: }; 73: } 74: export function createGuestPassesFeed(): FeedConfig { 75: const reward = getCachedReferrerReward(); 76: const subtitle = reward ? `Share Claude Code and earn ${formatCreditAmount(reward)} of extra usage` : 'Share Claude Code with friends'; 77: return { 78: title: '3 guest passes', 79: lines: [], 80: customContent: { 81: content: <> 82: <Box marginY={1}> 83: <Text color="claude">[✻] [✻] [✻]</Text> 84: </Box> 85: <Text dimColor>{subtitle}</Text> 86: </>, 87: width: 48 88: }, 89: footer: '/passes' 90: }; 91: }

File: src/components/LogoV2/GuestPassesUpsell.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useState } from 'react'; 4: import { Text } from '../../ink.js'; 5: import { logEvent } from '../../services/analytics/index.js'; 6: import { checkCachedPassesEligibility, formatCreditAmount, getCachedReferrerReward, getCachedRemainingPasses } from '../../services/api/referral.js'; 7: import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; 8: function resetIfPassesRefreshed(): void { 9: const remaining = getCachedRemainingPasses(); 10: if (remaining == null || remaining <= 0) return; 11: const config = getGlobalConfig(); 12: const lastSeen = config.passesLastSeenRemaining ?? 0; 13: if (remaining > lastSeen) { 14: saveGlobalConfig(prev => ({ 15: ...prev, 16: passesUpsellSeenCount: 0, 17: hasVisitedPasses: false, 18: passesLastSeenRemaining: remaining 19: })); 20: } 21: } 22: function shouldShowGuestPassesUpsell(): boolean { 23: const { 24: eligible, 25: hasCache 26: } = checkCachedPassesEligibility(); 27: if (!eligible || !hasCache) return false; 28: resetIfPassesRefreshed(); 29: const config = getGlobalConfig(); 30: if ((config.passesUpsellSeenCount ?? 0) >= 3) return false; 31: if (config.hasVisitedPasses) return false; 32: return true; 33: } 34: export function useShowGuestPassesUpsell() { 35: const [show] = useState(_temp); 36: return show; 37: } 38: function _temp() { 39: return shouldShowGuestPassesUpsell(); 40: } 41: export function incrementGuestPassesSeenCount(): void { 42: let newCount = 0; 43: saveGlobalConfig(prev => { 44: newCount = (prev.passesUpsellSeenCount ?? 0) + 1; 45: return { 46: ...prev, 47: passesUpsellSeenCount: newCount 48: }; 49: }); 50: logEvent('tengu_guest_passes_upsell_shown', { 51: seen_count: newCount 52: }); 53: } 54: export function GuestPassesUpsell() { 55: const $ = _c(1); 56: let t0; 57: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 58: const reward = getCachedReferrerReward(); 59: t0 = <Text dimColor={true}><Text color="claude">[✻]</Text> <Text color="claude">[✻]</Text>{" "}<Text color="claude">[✻]</Text> ·{" "}{reward ? `Share Claude Code and earn ${formatCreditAmount(reward)} of extra usage · /passes` : "3 guest passes at /passes"}</Text>; 60: $[0] = t0; 61: } else { 62: t0 = $[0]; 63: } 64: return t0; 65: }

File: src/components/LogoV2/LogoV2.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text, color } from '../../ink.js'; 4: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 5: import { stringWidth } from '../../ink/stringWidth.js'; 6: import { getLayoutMode, calculateLayoutDimensions, calculateOptimalLeftWidth, formatWelcomeMessage, truncatePath, getRecentActivitySync, getRecentReleaseNotesSync, getLogoDisplayData } from '../../utils/logoV2Utils.js'; 7: import { truncate } from '../../utils/format.js'; 8: import { getDisplayPath } from '../../utils/file.js'; 9: import { Clawd } from './Clawd.js'; 10: import { FeedColumn } from './FeedColumn.js'; 11: import { createRecentActivityFeed, createWhatsNewFeed, createProjectOnboardingFeed, createGuestPassesFeed } from './feedConfigs.js'; 12: import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js'; 13: import { resolveThemeSetting } from 'src/utils/systemTheme.js'; 14: import { getInitialSettings } from 'src/utils/settings/settings.js'; 15: import { isDebugMode, isDebugToStdErr, getDebugLogPath } from 'src/utils/debug.js'; 16: import { useEffect, useState } from 'react'; 17: import { getSteps, shouldShowProjectOnboarding, incrementProjectOnboardingSeenCount } from '../../projectOnboardingState.js'; 18: import { CondensedLogo } from './CondensedLogo.js'; 19: import { OffscreenFreeze } from '../OffscreenFreeze.js'; 20: import { checkForReleaseNotesSync } from '../../utils/releaseNotes.js'; 21: import { getDumpPromptsPath } from 'src/services/api/dumpPrompts.js'; 22: import { isEnvTruthy } from 'src/utils/envUtils.js'; 23: import { getStartupPerfLogPath, isDetailedProfilingEnabled } from 'src/utils/startupProfiler.js'; 24: import { EmergencyTip } from './EmergencyTip.js'; 25: import { VoiceModeNotice } from './VoiceModeNotice.js'; 26: import { Opus1mMergeNotice } from './Opus1mMergeNotice.js'; 27: import { feature } from 'bun:bundle'; 28: const ChannelsNoticeModule = feature('KAIROS') || feature('KAIROS_CHANNELS') ? require('./ChannelsNotice.js') as typeof import('./ChannelsNotice.js') : null; 29: import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'; 30: import { useShowGuestPassesUpsell, incrementGuestPassesSeenCount } from './GuestPassesUpsell.js'; 31: import { useShowOverageCreditUpsell, incrementOverageCreditUpsellSeenCount, createOverageCreditFeed } from './OverageCreditUpsell.js'; 32: import { plural } from '../../utils/stringUtils.js'; 33: import { useAppState } from '../../state/AppState.js'; 34: import { getEffortSuffix } from '../../utils/effort.js'; 35: import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; 36: import { renderModelSetting } from '../../utils/model/model.js'; 37: const LEFT_PANEL_MAX_WIDTH = 50; 38: export function LogoV2() { 39: const $ = _c(94); 40: const activities = getRecentActivitySync(); 41: const username = getGlobalConfig().oauthAccount?.displayName ?? ""; 42: const { 43: columns 44: } = useTerminalSize(); 45: let t0; 46: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 47: t0 = shouldShowProjectOnboarding(); 48: $[0] = t0; 49: } else { 50: t0 = $[0]; 51: } 52: const showOnboarding = t0; 53: let t1; 54: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 55: t1 = SandboxManager.isSandboxingEnabled(); 56: $[1] = t1; 57: } else { 58: t1 = $[1]; 59: } 60: const showSandboxStatus = t1; 61: const showGuestPassesUpsell = useShowGuestPassesUpsell(); 62: const showOverageCreditUpsell = useShowOverageCreditUpsell(); 63: const agent = useAppState(_temp); 64: const effortValue = useAppState(_temp2); 65: const config = getGlobalConfig(); 66: let changelog; 67: try { 68: changelog = getRecentReleaseNotesSync(3); 69: } catch { 70: changelog = []; 71: } 72: const [announcement] = useState(() => { 73: const announcements = getInitialSettings().companyAnnouncements; 74: if (!announcements || announcements.length === 0) { 75: return; 76: } 77: return config.numStartups === 1 ? announcements[0] : announcements[Math.floor(Math.random() * announcements.length)]; 78: }); 79: const { 80: hasReleaseNotes 81: } = checkForReleaseNotesSync(config.lastReleaseNotesSeen); 82: let t2; 83: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 84: t2 = () => { 85: const currentConfig = getGlobalConfig(); 86: if (currentConfig.lastReleaseNotesSeen === MACRO.VERSION) { 87: return; 88: } 89: saveGlobalConfig(_temp3); 90: if (showOnboarding) { 91: incrementProjectOnboardingSeenCount(); 92: } 93: }; 94: $[2] = t2; 95: } else { 96: t2 = $[2]; 97: } 98: let t3; 99: if ($[3] !== config) { 100: t3 = [config, showOnboarding]; 101: $[3] = config; 102: $[4] = t3; 103: } else { 104: t3 = $[4]; 105: } 106: useEffect(t2, t3); 107: let t4; 108: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 109: t4 = !hasReleaseNotes && !showOnboarding && !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO); 110: $[5] = t4; 111: } else { 112: t4 = $[5]; 113: } 114: const isCondensedMode = t4; 115: let t5; 116: let t6; 117: if ($[6] !== showGuestPassesUpsell) { 118: t5 = () => { 119: if (showGuestPassesUpsell && !showOnboarding && !isCondensedMode) { 120: incrementGuestPassesSeenCount(); 121: } 122: }; 123: t6 = [showGuestPassesUpsell, showOnboarding, isCondensedMode]; 124: $[6] = showGuestPassesUpsell; 125: $[7] = t5; 126: $[8] = t6; 127: } else { 128: t5 = $[7]; 129: t6 = $[8]; 130: } 131: useEffect(t5, t6); 132: let t7; 133: let t8; 134: if ($[9] !== showGuestPassesUpsell || $[10] !== showOverageCreditUpsell) { 135: t7 = () => { 136: if (showOverageCreditUpsell && !showOnboarding && !showGuestPassesUpsell && !isCondensedMode) { 137: incrementOverageCreditUpsellSeenCount(); 138: } 139: }; 140: t8 = [showOverageCreditUpsell, showOnboarding, showGuestPassesUpsell, isCondensedMode]; 141: $[9] = showGuestPassesUpsell; 142: $[10] = showOverageCreditUpsell; 143: $[11] = t7; 144: $[12] = t8; 145: } else { 146: t7 = $[11]; 147: t8 = $[12]; 148: } 149: useEffect(t7, t8); 150: const model = useMainLoopModel(); 151: const fullModelDisplayName = renderModelSetting(model); 152: const { 153: version, 154: cwd, 155: billingType, 156: agentName: agentNameFromSettings 157: } = getLogoDisplayData(); 158: const agentName = agent ?? agentNameFromSettings; 159: const effortSuffix = getEffortSuffix(model, effortValue); 160: const t9 = fullModelDisplayName + effortSuffix; 161: let t10; 162: if ($[13] !== t9) { 163: t10 = truncate(t9, LEFT_PANEL_MAX_WIDTH - 20); 164: $[13] = t9; 165: $[14] = t10; 166: } else { 167: t10 = $[14]; 168: } 169: const modelDisplayName = t10; 170: if (!hasReleaseNotes && !showOnboarding && !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO)) { 171: let t11; 172: let t12; 173: let t13; 174: let t14; 175: let t15; 176: let t16; 177: let t17; 178: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 179: t11 = <CondensedLogo />; 180: t12 = <VoiceModeNotice />; 181: t13 = <Opus1mMergeNotice />; 182: t14 = ChannelsNoticeModule && <ChannelsNoticeModule.ChannelsNotice />; 183: t15 = isDebugMode() && <Box paddingLeft={2} flexDirection="column"><Text color="warning">Debug mode enabled</Text><Text dimColor={true}>Logging to: {isDebugToStdErr() ? "stderr" : getDebugLogPath()}</Text></Box>; 184: t16 = <EmergencyTip />; 185: t17 = process.env.CLAUDE_CODE_TMUX_SESSION && <Box paddingLeft={2} flexDirection="column"><Text dimColor={true}>tmux session: {process.env.CLAUDE_CODE_TMUX_SESSION}</Text><Text dimColor={true}>{process.env.CLAUDE_CODE_TMUX_PREFIX_CONFLICTS ? `Detach: ${process.env.CLAUDE_CODE_TMUX_PREFIX} ${process.env.CLAUDE_CODE_TMUX_PREFIX} d (press prefix twice - Claude uses ${process.env.CLAUDE_CODE_TMUX_PREFIX})` : `Detach: ${process.env.CLAUDE_CODE_TMUX_PREFIX} d`}</Text></Box>; 186: $[15] = t11; 187: $[16] = t12; 188: $[17] = t13; 189: $[18] = t14; 190: $[19] = t15; 191: $[20] = t16; 192: $[21] = t17; 193: } else { 194: t11 = $[15]; 195: t12 = $[16]; 196: t13 = $[17]; 197: t14 = $[18]; 198: t15 = $[19]; 199: t16 = $[20]; 200: t17 = $[21]; 201: } 202: let t18; 203: if ($[22] !== announcement || $[23] !== config) { 204: t18 = announcement && <Box paddingLeft={2} flexDirection="column">{!process.env.IS_DEMO && config.oauthAccount?.organizationName && <Text dimColor={true}>Message from {config.oauthAccount.organizationName}:</Text>}<Text>{announcement}</Text></Box>; 205: $[22] = announcement; 206: $[23] = config; 207: $[24] = t18; 208: } else { 209: t18 = $[24]; 210: } 211: let t19; 212: let t20; 213: let t21; 214: let t22; 215: if ($[25] === Symbol.for("react.memo_cache_sentinel")) { 216: t19 = false && !process.env.DEMO_VERSION && <Box paddingLeft={2} flexDirection="column"><Text dimColor={true}>Use /issue to report model behavior issues</Text></Box>; 217: t20 = false && !process.env.DEMO_VERSION && <Box paddingLeft={2} flexDirection="column"><Text color="warning">[ANT-ONLY] Logs:</Text><Text dimColor={true}>API calls: {getDisplayPath(getDumpPromptsPath())}</Text><Text dimColor={true}>Debug logs: {getDisplayPath(getDebugLogPath())}</Text>{isDetailedProfilingEnabled() && <Text dimColor={true}>Startup Perf: {getDisplayPath(getStartupPerfLogPath())}</Text>}</Box>; 218: t21 = false && <GateOverridesWarning />; 219: t22 = false && <ExperimentEnrollmentNotice />; 220: $[25] = t19; 221: $[26] = t20; 222: $[27] = t21; 223: $[28] = t22; 224: } else { 225: t19 = $[25]; 226: t20 = $[26]; 227: t21 = $[27]; 228: t22 = $[28]; 229: } 230: let t23; 231: if ($[29] !== t18) { 232: t23 = <>{t11}{t12}{t13}{t14}{t15}{t16}{t17}{t18}{t19}{t20}{t21}{t22}</>; 233: $[29] = t18; 234: $[30] = t23; 235: } else { 236: t23 = $[30]; 237: } 238: return t23; 239: } 240: const layoutMode = getLayoutMode(columns); 241: const userTheme = resolveThemeSetting(getGlobalConfig().theme); 242: const borderTitle = ` ${color("claude", userTheme)("Claude Code")} ${color("inactive", userTheme)(`v${version}`)} `; 243: const compactBorderTitle = color("claude", userTheme)(" Claude Code "); 244: if (layoutMode === "compact") { 245: let welcomeMessage = formatWelcomeMessage(username); 246: if (stringWidth(welcomeMessage) > columns - 4) { 247: let t11; 248: if ($[31] === Symbol.for("react.memo_cache_sentinel")) { 249: t11 = formatWelcomeMessage(null); 250: $[31] = t11; 251: } else { 252: t11 = $[31]; 253: } 254: welcomeMessage = t11; 255: } 256: const cwdAvailableWidth = agentName ? columns - 4 - 1 - stringWidth(agentName) - 3 : columns - 4; 257: const truncatedCwd = truncatePath(cwd, Math.max(cwdAvailableWidth, 10)); 258: let t11; 259: if ($[32] !== compactBorderTitle) { 260: t11 = { 261: content: compactBorderTitle, 262: position: "top", 263: align: "start", 264: offset: 1 265: }; 266: $[32] = compactBorderTitle; 267: $[33] = t11; 268: } else { 269: t11 = $[33]; 270: } 271: let t12; 272: if ($[34] === Symbol.for("react.memo_cache_sentinel")) { 273: t12 = <Box marginY={1}><Clawd /></Box>; 274: $[34] = t12; 275: } else { 276: t12 = $[34]; 277: } 278: let t13; 279: if ($[35] !== modelDisplayName) { 280: t13 = <Text dimColor={true}>{modelDisplayName}</Text>; 281: $[35] = modelDisplayName; 282: $[36] = t13; 283: } else { 284: t13 = $[36]; 285: } 286: let t14; 287: let t15; 288: let t16; 289: if ($[37] === Symbol.for("react.memo_cache_sentinel")) { 290: t14 = <VoiceModeNotice />; 291: t15 = <Opus1mMergeNotice />; 292: t16 = ChannelsNoticeModule && <ChannelsNoticeModule.ChannelsNotice />; 293: $[37] = t14; 294: $[38] = t15; 295: $[39] = t16; 296: } else { 297: t14 = $[37]; 298: t15 = $[38]; 299: t16 = $[39]; 300: } 301: let t17; 302: if ($[40] !== showSandboxStatus) { 303: t17 = showSandboxStatus && <Box marginTop={1} flexDirection="column"><Text color="warning">Your bash commands will be sandboxed. Disable with /sandbox.</Text></Box>; 304: $[40] = showSandboxStatus; 305: $[41] = t17; 306: } else { 307: t17 = $[41]; 308: } 309: let t18; 310: let t19; 311: if ($[42] === Symbol.for("react.memo_cache_sentinel")) { 312: t18 = false && <GateOverridesWarning />; 313: t19 = false && <ExperimentEnrollmentNotice />; 314: $[42] = t18; 315: $[43] = t19; 316: } else { 317: t18 = $[42]; 318: t19 = $[43]; 319: } 320: return <><OffscreenFreeze><Box flexDirection="column" borderStyle="round" borderColor="claude" borderText={t11} paddingX={1} paddingY={1} alignItems="center" width={columns}><Text bold={true}>{welcomeMessage}</Text>{t12}{t13}<Text dimColor={true}>{billingType}</Text><Text dimColor={true}>{agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd}</Text></Box></OffscreenFreeze>{t14}{t15}{t16}{t17}{t18}{t19}</>; 321: } 322: const welcomeMessage_0 = formatWelcomeMessage(username); 323: const modelLine = !process.env.IS_DEMO && config.oauthAccount?.organizationName ? `${modelDisplayName} · ${billingType} · ${config.oauthAccount.organizationName}` : `${modelDisplayName} · ${billingType}`; 324: const cwdAvailableWidth_0 = agentName ? LEFT_PANEL_MAX_WIDTH - 1 - stringWidth(agentName) - 3 : LEFT_PANEL_MAX_WIDTH; 325: const truncatedCwd_0 = truncatePath(cwd, Math.max(cwdAvailableWidth_0, 10)); 326: const cwdLine = agentName ? `@${agentName} · ${truncatedCwd_0}` : truncatedCwd_0; 327: const optimalLeftWidth = calculateOptimalLeftWidth(welcomeMessage_0, cwdLine, modelLine); 328: const { 329: leftWidth, 330: rightWidth 331: } = calculateLayoutDimensions(columns, layoutMode, optimalLeftWidth); 332: const T0 = OffscreenFreeze; 333: const T1 = Box; 334: const t11 = "column"; 335: const t12 = "round"; 336: const t13 = "claude"; 337: let t14; 338: if ($[44] !== borderTitle) { 339: t14 = { 340: content: borderTitle, 341: position: "top", 342: align: "start", 343: offset: 3 344: }; 345: $[44] = borderTitle; 346: $[45] = t14; 347: } else { 348: t14 = $[45]; 349: } 350: const T2 = Box; 351: const t15 = layoutMode === "horizontal" ? "row" : "column"; 352: const t16 = 1; 353: const t17 = 1; 354: let t18; 355: if ($[46] !== welcomeMessage_0) { 356: t18 = <Box marginTop={1}><Text bold={true}>{welcomeMessage_0}</Text></Box>; 357: $[46] = welcomeMessage_0; 358: $[47] = t18; 359: } else { 360: t18 = $[47]; 361: } 362: let t19; 363: if ($[48] === Symbol.for("react.memo_cache_sentinel")) { 364: t19 = <Clawd />; 365: $[48] = t19; 366: } else { 367: t19 = $[48]; 368: } 369: let t20; 370: if ($[49] !== modelLine) { 371: t20 = <Text dimColor={true}>{modelLine}</Text>; 372: $[49] = modelLine; 373: $[50] = t20; 374: } else { 375: t20 = $[50]; 376: } 377: let t21; 378: if ($[51] !== cwdLine) { 379: t21 = <Text dimColor={true}>{cwdLine}</Text>; 380: $[51] = cwdLine; 381: $[52] = t21; 382: } else { 383: t21 = $[52]; 384: } 385: let t22; 386: if ($[53] !== t20 || $[54] !== t21) { 387: t22 = <Box flexDirection="column" alignItems="center">{t20}{t21}</Box>; 388: $[53] = t20; 389: $[54] = t21; 390: $[55] = t22; 391: } else { 392: t22 = $[55]; 393: } 394: let t23; 395: if ($[56] !== leftWidth || $[57] !== t18 || $[58] !== t22) { 396: t23 = <Box flexDirection="column" width={leftWidth} justifyContent="space-between" alignItems="center" minHeight={9}>{t18}{t19}{t22}</Box>; 397: $[56] = leftWidth; 398: $[57] = t18; 399: $[58] = t22; 400: $[59] = t23; 401: } else { 402: t23 = $[59]; 403: } 404: let t24; 405: if ($[60] !== layoutMode) { 406: t24 = layoutMode === "horizontal" && <Box height="100%" borderStyle="single" borderColor="claude" borderDimColor={true} borderTop={false} borderBottom={false} borderLeft={false} />; 407: $[60] = layoutMode; 408: $[61] = t24; 409: } else { 410: t24 = $[61]; 411: } 412: const t25 = layoutMode === "horizontal" && <FeedColumn feeds={showOnboarding ? [createProjectOnboardingFeed(getSteps()), createRecentActivityFeed(activities)] : showGuestPassesUpsell ? [createRecentActivityFeed(activities), createGuestPassesFeed()] : showOverageCreditUpsell ? [createRecentActivityFeed(activities), createOverageCreditFeed()] : [createRecentActivityFeed(activities), createWhatsNewFeed(changelog)]} maxWidth={rightWidth} />; 413: let t26; 414: if ($[62] !== T2 || $[63] !== t15 || $[64] !== t23 || $[65] !== t24 || $[66] !== t25) { 415: t26 = <T2 flexDirection={t15} paddingX={t16} gap={t17}>{t23}{t24}{t25}</T2>; 416: $[62] = T2; 417: $[63] = t15; 418: $[64] = t23; 419: $[65] = t24; 420: $[66] = t25; 421: $[67] = t26; 422: } else { 423: t26 = $[67]; 424: } 425: let t27; 426: if ($[68] !== T1 || $[69] !== t14 || $[70] !== t26) { 427: t27 = <T1 flexDirection={t11} borderStyle={t12} borderColor={t13} borderText={t14}>{t26}</T1>; 428: $[68] = T1; 429: $[69] = t14; 430: $[70] = t26; 431: $[71] = t27; 432: } else { 433: t27 = $[71]; 434: } 435: let t28; 436: if ($[72] !== T0 || $[73] !== t27) { 437: t28 = <T0>{t27}</T0>; 438: $[72] = T0; 439: $[73] = t27; 440: $[74] = t28; 441: } else { 442: t28 = $[74]; 443: } 444: let t29; 445: let t30; 446: let t31; 447: let t32; 448: let t33; 449: let t34; 450: if ($[75] === Symbol.for("react.memo_cache_sentinel")) { 451: t29 = <VoiceModeNotice />; 452: t30 = <Opus1mMergeNotice />; 453: t31 = ChannelsNoticeModule && <ChannelsNoticeModule.ChannelsNotice />; 454: t32 = isDebugMode() && <Box paddingLeft={2} flexDirection="column"><Text color="warning">Debug mode enabled</Text><Text dimColor={true}>Logging to: {isDebugToStdErr() ? "stderr" : getDebugLogPath()}</Text></Box>; 455: t33 = <EmergencyTip />; 456: t34 = process.env.CLAUDE_CODE_TMUX_SESSION && <Box paddingLeft={2} flexDirection="column"><Text dimColor={true}>tmux session: {process.env.CLAUDE_CODE_TMUX_SESSION}</Text><Text dimColor={true}>{process.env.CLAUDE_CODE_TMUX_PREFIX_CONFLICTS ? `Detach: ${process.env.CLAUDE_CODE_TMUX_PREFIX} ${process.env.CLAUDE_CODE_TMUX_PREFIX} d (press prefix twice - Claude uses ${process.env.CLAUDE_CODE_TMUX_PREFIX})` : `Detach: ${process.env.CLAUDE_CODE_TMUX_PREFIX} d`}</Text></Box>; 457: $[75] = t29; 458: $[76] = t30; 459: $[77] = t31; 460: $[78] = t32; 461: $[79] = t33; 462: $[80] = t34; 463: } else { 464: t29 = $[75]; 465: t30 = $[76]; 466: t31 = $[77]; 467: t32 = $[78]; 468: t33 = $[79]; 469: t34 = $[80]; 470: } 471: let t35; 472: if ($[81] !== announcement || $[82] !== config) { 473: t35 = announcement && <Box paddingLeft={2} flexDirection="column">{!process.env.IS_DEMO && config.oauthAccount?.organizationName && <Text dimColor={true}>Message from {config.oauthAccount.organizationName}:</Text>}<Text>{announcement}</Text></Box>; 474: $[81] = announcement; 475: $[82] = config; 476: $[83] = t35; 477: } else { 478: t35 = $[83]; 479: } 480: let t36; 481: if ($[84] !== showSandboxStatus) { 482: t36 = showSandboxStatus && <Box paddingLeft={2} flexDirection="column"><Text color="warning">Your bash commands will be sandboxed. Disable with /sandbox.</Text></Box>; 483: $[84] = showSandboxStatus; 484: $[85] = t36; 485: } else { 486: t36 = $[85]; 487: } 488: let t37; 489: let t38; 490: let t39; 491: let t40; 492: if ($[86] === Symbol.for("react.memo_cache_sentinel")) { 493: t37 = false && !process.env.DEMO_VERSION && <Box paddingLeft={2} flexDirection="column"><Text dimColor={true}>Use /issue to report model behavior issues</Text></Box>; 494: t38 = false && !process.env.DEMO_VERSION && <Box paddingLeft={2} flexDirection="column"><Text color="warning">[ANT-ONLY] Logs:</Text><Text dimColor={true}>API calls: {getDisplayPath(getDumpPromptsPath())}</Text><Text dimColor={true}>Debug logs: {getDisplayPath(getDebugLogPath())}</Text>{isDetailedProfilingEnabled() && <Text dimColor={true}>Startup Perf: {getDisplayPath(getStartupPerfLogPath())}</Text>}</Box>; 495: t39 = false && <GateOverridesWarning />; 496: t40 = false && <ExperimentEnrollmentNotice />; 497: $[86] = t37; 498: $[87] = t38; 499: $[88] = t39; 500: $[89] = t40; 501: } else { 502: t37 = $[86]; 503: t38 = $[87]; 504: t39 = $[88]; 505: t40 = $[89]; 506: } 507: let t41; 508: if ($[90] !== t28 || $[91] !== t35 || $[92] !== t36) { 509: t41 = <>{t28}{t29}{t30}{t31}{t32}{t33}{t34}{t35}{t36}{t37}{t38}{t39}{t40}</>; 510: $[90] = t28; 511: $[91] = t35; 512: $[92] = t36; 513: $[93] = t41; 514: } else { 515: t41 = $[93]; 516: } 517: return t41; 518: } 519: function _temp3(current) { 520: if (current.lastReleaseNotesSeen === MACRO.VERSION) { 521: return current; 522: } 523: return { 524: ...current, 525: lastReleaseNotesSeen: MACRO.VERSION 526: }; 527: } 528: function _temp2(s_0) { 529: return s_0.effortValue; 530: } 531: function _temp(s) { 532: return s.agent; 533: }

File: src/components/LogoV2/Opus1mMergeNotice.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useEffect, useState } from 'react'; 4: import { UP_ARROW } from '../../constants/figures.js'; 5: import { Box, Text } from '../../ink.js'; 6: import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; 7: import { isOpus1mMergeEnabled } from '../../utils/model/model.js'; 8: import { AnimatedAsterisk } from './AnimatedAsterisk.js'; 9: const MAX_SHOW_COUNT = 6; 10: export function shouldShowOpus1mMergeNotice(): boolean { 11: return isOpus1mMergeEnabled() && (getGlobalConfig().opus1mMergeNoticeSeenCount ?? 0) < MAX_SHOW_COUNT; 12: } 13: export function Opus1mMergeNotice() { 14: const $ = _c(4); 15: const [show] = useState(shouldShowOpus1mMergeNotice); 16: let t0; 17: let t1; 18: if ($[0] !== show) { 19: t0 = () => { 20: if (!show) { 21: return; 22: } 23: const newCount = (getGlobalConfig().opus1mMergeNoticeSeenCount ?? 0) + 1; 24: saveGlobalConfig(prev => { 25: if ((prev.opus1mMergeNoticeSeenCount ?? 0) >= newCount) { 26: return prev; 27: } 28: return { 29: ...prev, 30: opus1mMergeNoticeSeenCount: newCount 31: }; 32: }); 33: }; 34: t1 = [show]; 35: $[0] = show; 36: $[1] = t0; 37: $[2] = t1; 38: } else { 39: t0 = $[1]; 40: t1 = $[2]; 41: } 42: useEffect(t0, t1); 43: if (!show) { 44: return null; 45: } 46: let t2; 47: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 48: t2 = <Box paddingLeft={2}><AnimatedAsterisk char={UP_ARROW} /><Text dimColor={true}>{" "}Opus now defaults to 1M context · 5x more room, same pricing</Text></Box>; 49: $[3] = t2; 50: } else { 51: t2 = $[3]; 52: } 53: return t2; 54: }

File: src/components/LogoV2/OverageCreditUpsell.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useState } from 'react'; 4: import { Text } from '../../ink.js'; 5: import { logEvent } from '../../services/analytics/index.js'; 6: import { formatGrantAmount, getCachedOverageCreditGrant, refreshOverageCreditGrantCache } from '../../services/api/overageCreditGrant.js'; 7: import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; 8: import { truncate } from '../../utils/format.js'; 9: import type { FeedConfig } from './Feed.js'; 10: const MAX_IMPRESSIONS = 3; 11: export function isEligibleForOverageCreditGrant(): boolean { 12: const info = getCachedOverageCreditGrant(); 13: if (!info || !info.available || info.granted) return false; 14: return formatGrantAmount(info) !== null; 15: } 16: export function shouldShowOverageCreditUpsell(): boolean { 17: if (!isEligibleForOverageCreditGrant()) return false; 18: const config = getGlobalConfig(); 19: if (config.hasVisitedExtraUsage) return false; 20: if ((config.overageCreditUpsellSeenCount ?? 0) >= MAX_IMPRESSIONS) return false; 21: return true; 22: } 23: export function maybeRefreshOverageCreditCache(): void { 24: if (getCachedOverageCreditGrant() !== null) return; 25: void refreshOverageCreditGrantCache(); 26: } 27: export function useShowOverageCreditUpsell() { 28: const [show] = useState(_temp); 29: return show; 30: } 31: function _temp() { 32: maybeRefreshOverageCreditCache(); 33: return shouldShowOverageCreditUpsell(); 34: } 35: export function incrementOverageCreditUpsellSeenCount(): void { 36: let newCount = 0; 37: saveGlobalConfig(prev => { 38: newCount = (prev.overageCreditUpsellSeenCount ?? 0) + 1; 39: return { 40: ...prev, 41: overageCreditUpsellSeenCount: newCount 42: }; 43: }); 44: logEvent('tengu_overage_credit_upsell_shown', { 45: seen_count: newCount 46: }); 47: } 48: function getUsageText(amount: string): string { 49: return `${amount} in extra usage for third-party apps · /extra-usage`; 50: } 51: const FEED_SUBTITLE = 'On us. Works on third-party apps · /extra-usage'; 52: function getFeedTitle(amount: string): string { 53: return `${amount} in extra usage`; 54: } 55: type Props = { 56: maxWidth?: number; 57: twoLine?: boolean; 58: }; 59: export function OverageCreditUpsell(t0) { 60: const $ = _c(8); 61: const { 62: maxWidth, 63: twoLine 64: } = t0; 65: let t1; 66: let t2; 67: if ($[0] !== maxWidth || $[1] !== twoLine) { 68: t2 = Symbol.for("react.early_return_sentinel"); 69: bb0: { 70: const info = getCachedOverageCreditGrant(); 71: if (!info) { 72: t2 = null; 73: break bb0; 74: } 75: const amount = formatGrantAmount(info); 76: if (!amount) { 77: t2 = null; 78: break bb0; 79: } 80: if (twoLine) { 81: const title = getFeedTitle(amount); 82: let t3; 83: if ($[4] !== maxWidth) { 84: t3 = maxWidth ? truncate(FEED_SUBTITLE, maxWidth) : FEED_SUBTITLE; 85: $[4] = maxWidth; 86: $[5] = t3; 87: } else { 88: t3 = $[5]; 89: } 90: let t4; 91: if ($[6] !== t3) { 92: t4 = <Text dimColor={true}>{t3}</Text>; 93: $[6] = t3; 94: $[7] = t4; 95: } else { 96: t4 = $[7]; 97: } 98: t2 = <><Text color="claude">{maxWidth ? truncate(title, maxWidth) : title}</Text>{t4}</>; 99: break bb0; 100: } 101: const text = getUsageText(amount); 102: const display = maxWidth ? truncate(text, maxWidth) : text; 103: const highlightLen = Math.min(getFeedTitle(amount).length, display.length); 104: t1 = <Text dimColor={true}><Text color="claude">{display.slice(0, highlightLen)}</Text>{display.slice(highlightLen)}</Text>; 105: } 106: $[0] = maxWidth; 107: $[1] = twoLine; 108: $[2] = t1; 109: $[3] = t2; 110: } else { 111: t1 = $[2]; 112: t2 = $[3]; 113: } 114: if (t2 !== Symbol.for("react.early_return_sentinel")) { 115: return t2; 116: } 117: return t1; 118: } 119: export function createOverageCreditFeed(): FeedConfig { 120: const info = getCachedOverageCreditGrant(); 121: const amount = info ? formatGrantAmount(info) : null; 122: const title = amount ? getFeedTitle(amount) : 'extra usage credit'; 123: return { 124: title, 125: lines: [], 126: customContent: { 127: content: <Text dimColor>{FEED_SUBTITLE}</Text>, 128: width: Math.max(title.length, FEED_SUBTITLE.length) 129: } 130: }; 131: }

File: src/components/LogoV2/VoiceModeNotice.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import * as React from 'react'; 4: import { useEffect, useState } from 'react'; 5: import { Box, Text } from '../../ink.js'; 6: import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; 7: import { getInitialSettings } from '../../utils/settings/settings.js'; 8: import { isVoiceModeEnabled } from '../../voice/voiceModeEnabled.js'; 9: import { AnimatedAsterisk } from './AnimatedAsterisk.js'; 10: import { shouldShowOpus1mMergeNotice } from './Opus1mMergeNotice.js'; 11: const MAX_SHOW_COUNT = 3; 12: export function VoiceModeNotice() { 13: const $ = _c(1); 14: let t0; 15: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 16: t0 = feature("VOICE_MODE") ? <VoiceModeNoticeInner /> : null; 17: $[0] = t0; 18: } else { 19: t0 = $[0]; 20: } 21: return t0; 22: } 23: function VoiceModeNoticeInner() { 24: const $ = _c(4); 25: const [show] = useState(_temp); 26: let t0; 27: let t1; 28: if ($[0] !== show) { 29: t0 = () => { 30: if (!show) { 31: return; 32: } 33: const newCount = (getGlobalConfig().voiceNoticeSeenCount ?? 0) + 1; 34: saveGlobalConfig(prev => { 35: if ((prev.voiceNoticeSeenCount ?? 0) >= newCount) { 36: return prev; 37: } 38: return { 39: ...prev, 40: voiceNoticeSeenCount: newCount 41: }; 42: }); 43: }; 44: t1 = [show]; 45: $[0] = show; 46: $[1] = t0; 47: $[2] = t1; 48: } else { 49: t0 = $[1]; 50: t1 = $[2]; 51: } 52: useEffect(t0, t1); 53: if (!show) { 54: return null; 55: } 56: let t2; 57: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 58: t2 = <Box paddingLeft={2}><AnimatedAsterisk /><Text dimColor={true}> Voice mode is now available · /voice to enable</Text></Box>; 59: $[3] = t2; 60: } else { 61: t2 = $[3]; 62: } 63: return t2; 64: } 65: function _temp() { 66: return isVoiceModeEnabled() && getInitialSettings().voiceEnabled !== true && (getGlobalConfig().voiceNoticeSeenCount ?? 0) < MAX_SHOW_COUNT && !shouldShowOpus1mMergeNotice(); 67: }

File: src/components/LogoV2/WelcomeV2.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, Text, useTheme } from 'src/ink.js'; 4: import { env } from '../../utils/env.js'; 5: const WELCOME_V2_WIDTH = 58; 6: export function WelcomeV2() { 7: const $ = _c(35); 8: const [theme] = useTheme(); 9: if (env.terminal === "Apple_Terminal") { 10: let t0; 11: if ($[0] !== theme) { 12: t0 = <AppleTerminalWelcomeV2 theme={theme} welcomeMessage="Welcome to Claude Code" />; 13: $[0] = theme; 14: $[1] = t0; 15: } else { 16: t0 = $[1]; 17: } 18: return t0; 19: } 20: if (["light", "light-daltonized", "light-ansi"].includes(theme)) { 21: let t0; 22: let t1; 23: let t2; 24: let t3; 25: let t4; 26: let t5; 27: let t6; 28: let t7; 29: let t8; 30: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 31: t0 = <Text><Text color="claude">{"Welcome to Claude Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>; 32: t1 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>; 33: t2 = <Text>{" "}</Text>; 34: t3 = <Text>{" "}</Text>; 35: t4 = <Text>{" "}</Text>; 36: t5 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; 37: t6 = <Text>{" \u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; 38: t7 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; 39: t8 = <Text>{" "}</Text>; 40: $[2] = t0; 41: $[3] = t1; 42: $[4] = t2; 43: $[5] = t3; 44: $[6] = t4; 45: $[7] = t5; 46: $[8] = t6; 47: $[9] = t7; 48: $[10] = t8; 49: } else { 50: t0 = $[2]; 51: t1 = $[3]; 52: t2 = $[4]; 53: t3 = $[5]; 54: t4 = $[6]; 55: t5 = $[7]; 56: t6 = $[8]; 57: t7 = $[9]; 58: t8 = $[10]; 59: } 60: let t9; 61: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 62: t9 = <Text><Text dimColor={true}>{" \u2591\u2591\u2591\u2591"}</Text><Text>{" \u2588\u2588 "}</Text></Text>; 63: $[11] = t9; 64: } else { 65: t9 = $[11]; 66: } 67: let t10; 68: let t11; 69: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 70: t10 = <Text><Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591"}</Text><Text>{" \u2588\u2588\u2592\u2592\u2588\u2588 "}</Text></Text>; 71: t11 = <Text>{" \u2592\u2592 \u2588\u2588 \u2592"}</Text>; 72: $[12] = t10; 73: $[13] = t11; 74: } else { 75: t10 = $[12]; 76: t11 = $[13]; 77: } 78: let t12; 79: if ($[14] === Symbol.for("react.memo_cache_sentinel")) { 80: t12 = <Text>{" "}<Text color="clawd_body"> █████████ </Text>{" \u2592\u2592\u2591\u2591\u2592\u2592 \u2592 \u2592\u2592"}</Text>; 81: $[14] = t12; 82: } else { 83: t12 = $[14]; 84: } 85: let t13; 86: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 87: t13 = <Text>{" "}<Text color="clawd_body" backgroundColor="clawd_background">██▄█████▄██</Text>{" \u2592\u2592 \u2592\u2592 "}</Text>; 88: $[15] = t13; 89: } else { 90: t13 = $[15]; 91: } 92: let t14; 93: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 94: t14 = <Text>{" "}<Text color="clawd_body"> █████████ </Text>{" \u2591 \u2592 "}</Text>; 95: $[16] = t14; 96: } else { 97: t14 = $[16]; 98: } 99: let t15; 100: if ($[17] === Symbol.for("react.memo_cache_sentinel")) { 101: t15 = <Box width={WELCOME_V2_WIDTH}><Text>{t0}{t1}{t2}{t3}{t4}{t5}{t6}{t7}{t8}{t9}{t10}{t11}{t12}{t13}{t14}<Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}<Text color="clawd_body">{"\u2588 \u2588 \u2588 \u2588"}</Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2591\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2592\u2026\u2026\u2026\u2026"}</Text></Text></Box>; 102: $[17] = t15; 103: } else { 104: t15 = $[17]; 105: } 106: return t15; 107: } 108: let t0; 109: let t1; 110: let t2; 111: let t3; 112: let t4; 113: let t5; 114: let t6; 115: if ($[18] === Symbol.for("react.memo_cache_sentinel")) { 116: t0 = <Text><Text color="claude">{"Welcome to Claude Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>; 117: t1 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>; 118: t2 = <Text>{" "}</Text>; 119: t3 = <Text>{" * \u2588\u2588\u2588\u2588\u2588\u2593\u2593\u2591 "}</Text>; 120: t4 = <Text>{" * \u2588\u2588\u2588\u2593\u2591 \u2591\u2591 "}</Text>; 121: t5 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591 \u2588\u2588\u2588\u2593\u2591 "}</Text>; 122: t6 = <Text>{" \u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 \u2588\u2588\u2588\u2593\u2591 "}</Text>; 123: $[18] = t0; 124: $[19] = t1; 125: $[20] = t2; 126: $[21] = t3; 127: $[22] = t4; 128: $[23] = t5; 129: $[24] = t6; 130: } else { 131: t0 = $[18]; 132: t1 = $[19]; 133: t2 = $[20]; 134: t3 = $[21]; 135: t4 = $[22]; 136: t5 = $[23]; 137: t6 = $[24]; 138: } 139: let t10; 140: let t11; 141: let t7; 142: let t8; 143: let t9; 144: if ($[25] === Symbol.for("react.memo_cache_sentinel")) { 145: t7 = <Text><Text>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text><Text bold={true}>*</Text><Text>{" \u2588\u2588\u2593\u2591\u2591 \u2593 "}</Text></Text>; 146: t8 = <Text>{" \u2591\u2593\u2593\u2588\u2588\u2588\u2593\u2593\u2591 "}</Text>; 147: t9 = <Text dimColor={true}>{" * \u2591\u2591\u2591\u2591 "}</Text>; 148: t10 = <Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; 149: t11 = <Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; 150: $[25] = t10; 151: $[26] = t11; 152: $[27] = t7; 153: $[28] = t8; 154: $[29] = t9; 155: } else { 156: t10 = $[25]; 157: t11 = $[26]; 158: t7 = $[27]; 159: t8 = $[28]; 160: t9 = $[29]; 161: } 162: let t12; 163: if ($[30] === Symbol.for("react.memo_cache_sentinel")) { 164: t12 = <Text color="clawd_body"> █████████ </Text>; 165: $[30] = t12; 166: } else { 167: t12 = $[30]; 168: } 169: let t13; 170: if ($[31] === Symbol.for("react.memo_cache_sentinel")) { 171: t13 = <Text>{" "}{t12}{" "}<Text dimColor={true}>*</Text><Text> </Text></Text>; 172: $[31] = t13; 173: } else { 174: t13 = $[31]; 175: } 176: let t14; 177: if ($[32] === Symbol.for("react.memo_cache_sentinel")) { 178: t14 = <Text>{" "}<Text color="clawd_body">██▄█████▄██</Text><Text>{" "}</Text><Text bold={true}>*</Text><Text>{" "}</Text></Text>; 179: $[32] = t14; 180: } else { 181: t14 = $[32]; 182: } 183: let t15; 184: if ($[33] === Symbol.for("react.memo_cache_sentinel")) { 185: t15 = <Text>{" "}<Text color="clawd_body"> █████████ </Text>{" * "}</Text>; 186: $[33] = t15; 187: } else { 188: t15 = $[33]; 189: } 190: let t16; 191: if ($[34] === Symbol.for("react.memo_cache_sentinel")) { 192: t16 = <Box width={WELCOME_V2_WIDTH}><Text>{t0}{t1}{t2}{t3}{t4}{t5}{t6}{t7}{t8}{t9}{t10}{t11}{t13}{t14}{t15}<Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}<Text color="clawd_body">{"\u2588 \u2588 \u2588 \u2588"}</Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text></Text></Box>; 193: $[34] = t16; 194: } else { 195: t16 = $[34]; 196: } 197: return t16; 198: } 199: type AppleTerminalWelcomeV2Props = { 200: theme: string; 201: welcomeMessage: string; 202: }; 203: function AppleTerminalWelcomeV2(t0) { 204: const $ = _c(44); 205: const { 206: theme, 207: welcomeMessage 208: } = t0; 209: const isLightTheme = ["light", "light-daltonized", "light-ansi"].includes(theme); 210: if (isLightTheme) { 211: let t1; 212: if ($[0] !== welcomeMessage) { 213: t1 = <Text color="claude">{welcomeMessage} </Text>; 214: $[0] = welcomeMessage; 215: $[1] = t1; 216: } else { 217: t1 = $[1]; 218: } 219: let t2; 220: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 221: t2 = <Text dimColor={true}>v{MACRO.VERSION} </Text>; 222: $[2] = t2; 223: } else { 224: t2 = $[2]; 225: } 226: let t3; 227: if ($[3] !== t1) { 228: t3 = <Text>{t1}{t2}</Text>; 229: $[3] = t1; 230: $[4] = t3; 231: } else { 232: t3 = $[4]; 233: } 234: let t10; 235: let t11; 236: let t4; 237: let t5; 238: let t6; 239: let t7; 240: let t8; 241: let t9; 242: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 243: t4 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>; 244: t5 = <Text>{" "}</Text>; 245: t6 = <Text>{" "}</Text>; 246: t7 = <Text>{" "}</Text>; 247: t8 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; 248: t9 = <Text>{" \u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; 249: t10 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; 250: t11 = <Text>{" "}</Text>; 251: $[5] = t10; 252: $[6] = t11; 253: $[7] = t4; 254: $[8] = t5; 255: $[9] = t6; 256: $[10] = t7; 257: $[11] = t8; 258: $[12] = t9; 259: } else { 260: t10 = $[5]; 261: t11 = $[6]; 262: t4 = $[7]; 263: t5 = $[8]; 264: t6 = $[9]; 265: t7 = $[10]; 266: t8 = $[11]; 267: t9 = $[12]; 268: } 269: let t12; 270: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 271: t12 = <Text><Text dimColor={true}>{" \u2591\u2591\u2591\u2591"}</Text><Text>{" \u2588\u2588 "}</Text></Text>; 272: $[13] = t12; 273: } else { 274: t12 = $[13]; 275: } 276: let t13; 277: let t14; 278: let t15; 279: if ($[14] === Symbol.for("react.memo_cache_sentinel")) { 280: t13 = <Text><Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591"}</Text><Text>{" \u2588\u2588\u2592\u2592\u2588\u2588 "}</Text></Text>; 281: t14 = <Text>{" \u2592\u2592 \u2588\u2588 \u2592"}</Text>; 282: t15 = <Text>{" \u2592\u2592\u2591\u2591\u2592\u2592 \u2592 \u2592\u2592"}</Text>; 283: $[14] = t13; 284: $[15] = t14; 285: $[16] = t15; 286: } else { 287: t13 = $[14]; 288: t14 = $[15]; 289: t15 = $[16]; 290: } 291: let t16; 292: if ($[17] === Symbol.for("react.memo_cache_sentinel")) { 293: t16 = <Text>{" "}<Text color="clawd_body">▗</Text><Text color="clawd_background" backgroundColor="clawd_body">{" "}▗{" "}▖{" "}</Text><Text color="clawd_body">▖</Text>{" \u2592\u2592 \u2592\u2592 "}</Text>; 294: $[17] = t16; 295: } else { 296: t16 = $[17]; 297: } 298: let t17; 299: if ($[18] === Symbol.for("react.memo_cache_sentinel")) { 300: t17 = <Text>{" "}<Text backgroundColor="clawd_body">{" ".repeat(9)}</Text>{" \u2591 \u2592 "}</Text>; 301: $[18] = t17; 302: } else { 303: t17 = $[18]; 304: } 305: let t18; 306: if ($[19] === Symbol.for("react.memo_cache_sentinel")) { 307: t18 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}<Text backgroundColor="clawd_body"> </Text><Text> </Text><Text backgroundColor="clawd_body"> </Text><Text>{" "}</Text><Text backgroundColor="clawd_body"> </Text><Text> </Text><Text backgroundColor="clawd_body"> </Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2591\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2592\u2026\u2026\u2026\u2026"}</Text>; 308: $[19] = t18; 309: } else { 310: t18 = $[19]; 311: } 312: let t19; 313: if ($[20] !== t3) { 314: t19 = <Box width={WELCOME_V2_WIDTH}><Text>{t3}{t4}{t5}{t6}{t7}{t8}{t9}{t10}{t11}{t12}{t13}{t14}{t15}{t16}{t17}{t18}</Text></Box>; 315: $[20] = t3; 316: $[21] = t19; 317: } else { 318: t19 = $[21]; 319: } 320: return t19; 321: } 322: let t1; 323: if ($[22] !== welcomeMessage) { 324: t1 = <Text color="claude">{welcomeMessage} </Text>; 325: $[22] = welcomeMessage; 326: $[23] = t1; 327: } else { 328: t1 = $[23]; 329: } 330: let t2; 331: if ($[24] === Symbol.for("react.memo_cache_sentinel")) { 332: t2 = <Text dimColor={true}>v{MACRO.VERSION} </Text>; 333: $[24] = t2; 334: } else { 335: t2 = $[24]; 336: } 337: let t3; 338: if ($[25] !== t1) { 339: t3 = <Text>{t1}{t2}</Text>; 340: $[25] = t1; 341: $[26] = t3; 342: } else { 343: t3 = $[26]; 344: } 345: let t4; 346: let t5; 347: let t6; 348: let t7; 349: let t8; 350: let t9; 351: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 352: t4 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>; 353: t5 = <Text>{" "}</Text>; 354: t6 = <Text>{" * \u2588\u2588\u2588\u2588\u2588\u2593\u2593\u2591 "}</Text>; 355: t7 = <Text>{" * \u2588\u2588\u2588\u2593\u2591 \u2591\u2591 "}</Text>; 356: t8 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591 \u2588\u2588\u2588\u2593\u2591 "}</Text>; 357: t9 = <Text>{" \u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 \u2588\u2588\u2588\u2593\u2591 "}</Text>; 358: $[27] = t4; 359: $[28] = t5; 360: $[29] = t6; 361: $[30] = t7; 362: $[31] = t8; 363: $[32] = t9; 364: } else { 365: t4 = $[27]; 366: t5 = $[28]; 367: t6 = $[29]; 368: t7 = $[30]; 369: t8 = $[31]; 370: t9 = $[32]; 371: } 372: let t10; 373: let t11; 374: let t12; 375: let t13; 376: let t14; 377: if ($[33] === Symbol.for("react.memo_cache_sentinel")) { 378: t10 = <Text><Text>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text><Text bold={true}>*</Text><Text>{" \u2588\u2588\u2593\u2591\u2591 \u2593 "}</Text></Text>; 379: t11 = <Text>{" \u2591\u2593\u2593\u2588\u2588\u2588\u2593\u2593\u2591 "}</Text>; 380: t12 = <Text dimColor={true}>{" * \u2591\u2591\u2591\u2591 "}</Text>; 381: t13 = <Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; 382: t14 = <Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; 383: $[33] = t10; 384: $[34] = t11; 385: $[35] = t12; 386: $[36] = t13; 387: $[37] = t14; 388: } else { 389: t10 = $[33]; 390: t11 = $[34]; 391: t12 = $[35]; 392: t13 = $[36]; 393: t14 = $[37]; 394: } 395: let t15; 396: if ($[38] === Symbol.for("react.memo_cache_sentinel")) { 397: t15 = <Text>{" "}<Text dimColor={true}>*</Text><Text> </Text></Text>; 398: $[38] = t15; 399: } else { 400: t15 = $[38]; 401: } 402: let t16; 403: if ($[39] === Symbol.for("react.memo_cache_sentinel")) { 404: t16 = <Text>{" "}<Text color="clawd_body">▗</Text><Text color="clawd_background" backgroundColor="clawd_body">{" "}▗{" "}▖{" "}</Text><Text color="clawd_body">▖</Text><Text>{" "}</Text><Text bold={true}>*</Text><Text>{" "}</Text></Text>; 405: $[39] = t16; 406: } else { 407: t16 = $[39]; 408: } 409: let t17; 410: if ($[40] === Symbol.for("react.memo_cache_sentinel")) { 411: t17 = <Text>{" "}<Text backgroundColor="clawd_body">{" ".repeat(9)}</Text>{" * "}</Text>; 412: $[40] = t17; 413: } else { 414: t17 = $[40]; 415: } 416: let t18; 417: if ($[41] === Symbol.for("react.memo_cache_sentinel")) { 418: t18 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}<Text backgroundColor="clawd_body"> </Text><Text> </Text><Text backgroundColor="clawd_body"> </Text><Text>{" "}</Text><Text backgroundColor="clawd_body"> </Text><Text> </Text><Text backgroundColor="clawd_body"> </Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>; 419: $[41] = t18; 420: } else { 421: t18 = $[41]; 422: } 423: let t19; 424: if ($[42] !== t3) { 425: t19 = <Box width={WELCOME_V2_WIDTH}><Text>{t3}{t4}{t5}{t6}{t7}{t8}{t9}{t10}{t11}{t12}{t13}{t14}{t15}{t16}{t17}{t18}</Text></Box>; 426: $[42] = t3; 427: $[43] = t19; 428: } else { 429: t19 = $[43]; 430: } 431: return t19; 432: }

File: src/components/LspRecommendation/LspRecommendationMenu.tsx

typescript 1: import * as React from 'react'; 2: import { Box, Text } from '../../ink.js'; 3: import { Select } from '../CustomSelect/select.js'; 4: import { PermissionDialog } from '../permissions/PermissionDialog.js'; 5: type Props = { 6: pluginName: string; 7: pluginDescription?: string; 8: fileExtension: string; 9: onResponse: (response: 'yes' | 'no' | 'never' | 'disable') => void; 10: }; 11: const AUTO_DISMISS_MS = 30_000; 12: export function LspRecommendationMenu({ 13: pluginName, 14: pluginDescription, 15: fileExtension, 16: onResponse 17: }: Props): React.ReactNode { 18: const onResponseRef = React.useRef(onResponse); 19: onResponseRef.current = onResponse; 20: React.useEffect(() => { 21: const timeoutId = setTimeout(ref => ref.current('no'), AUTO_DISMISS_MS, onResponseRef); 22: return () => clearTimeout(timeoutId); 23: }, []); 24: function onSelect(value: string): void { 25: switch (value) { 26: case 'yes': 27: onResponse('yes'); 28: break; 29: case 'no': 30: onResponse('no'); 31: break; 32: case 'never': 33: onResponse('never'); 34: break; 35: case 'disable': 36: onResponse('disable'); 37: break; 38: } 39: } 40: const options = [{ 41: label: <Text> 42: Yes, install <Text bold>{pluginName}</Text> 43: </Text>, 44: value: 'yes' 45: }, { 46: label: 'No, not now', 47: value: 'no' 48: }, { 49: label: <Text> 50: Never for <Text bold>{pluginName}</Text> 51: </Text>, 52: value: 'never' 53: }, { 54: label: 'Disable all LSP recommendations', 55: value: 'disable' 56: }]; 57: return <PermissionDialog title="LSP Plugin Recommendation"> 58: <Box flexDirection="column" paddingX={2} paddingY={1}> 59: <Box marginBottom={1}> 60: <Text dimColor> 61: LSP provides code intelligence like go-to-definition and error 62: checking 63: </Text> 64: </Box> 65: <Box> 66: <Text dimColor>Plugin:</Text> 67: <Text> {pluginName}</Text> 68: </Box> 69: {pluginDescription && <Box> 70: <Text dimColor>{pluginDescription}</Text> 71: </Box>} 72: <Box> 73: <Text dimColor>Triggered by:</Text> 74: <Text> {fileExtension} files</Text> 75: </Box> 76: <Box marginTop={1}> 77: <Text>Would you like to install this LSP plugin?</Text> 78: </Box> 79: <Box> 80: <Select options={options} onChange={onSelect} onCancel={() => onResponse('no')} /> 81: </Box> 82: </Box> 83: </PermissionDialog>; 84: }

File: src/components/ManagedSettingsSecurityDialog/ManagedSettingsSecurityDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 4: import { Box, Text } from '../../ink.js'; 5: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 6: import type { SettingsJson } from '../../utils/settings/types.js'; 7: import { Select } from '../CustomSelect/index.js'; 8: import { PermissionDialog } from '../permissions/PermissionDialog.js'; 9: import { extractDangerousSettings, formatDangerousSettingsList } from './utils.js'; 10: type Props = { 11: settings: SettingsJson; 12: onAccept: () => void; 13: onReject: () => void; 14: }; 15: export function ManagedSettingsSecurityDialog(t0) { 16: const $ = _c(26); 17: const { 18: settings, 19: onAccept, 20: onReject 21: } = t0; 22: const dangerous = extractDangerousSettings(settings); 23: const settingsList = formatDangerousSettingsList(dangerous); 24: const exitState = useExitOnCtrlCDWithKeybindings(); 25: let t1; 26: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 27: t1 = { 28: context: "Confirmation" 29: }; 30: $[0] = t1; 31: } else { 32: t1 = $[0]; 33: } 34: useKeybinding("confirm:no", onReject, t1); 35: let t2; 36: if ($[1] !== onAccept || $[2] !== onReject) { 37: t2 = function onChange(value) { 38: if (value === "exit") { 39: onReject(); 40: return; 41: } 42: onAccept(); 43: }; 44: $[1] = onAccept; 45: $[2] = onReject; 46: $[3] = t2; 47: } else { 48: t2 = $[3]; 49: } 50: const onChange = t2; 51: const T0 = PermissionDialog; 52: const t3 = "warning"; 53: const t4 = "warning"; 54: const t5 = "Managed settings require approval"; 55: const T1 = Box; 56: const t6 = "column"; 57: const t7 = 1; 58: const t8 = 1; 59: let t9; 60: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 61: t9 = <Text>Your organization has configured managed settings that could allow execution of arbitrary code or interception of your prompts and responses.</Text>; 62: $[4] = t9; 63: } else { 64: t9 = $[4]; 65: } 66: const T2 = Box; 67: const t10 = "column"; 68: let t11; 69: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 70: t11 = <Text dimColor={true}>Settings requiring approval:</Text>; 71: $[5] = t11; 72: } else { 73: t11 = $[5]; 74: } 75: const t12 = settingsList.map(_temp); 76: let t13; 77: if ($[6] !== T2 || $[7] !== t11 || $[8] !== t12) { 78: t13 = <T2 flexDirection={t10}>{t11}{t12}</T2>; 79: $[6] = T2; 80: $[7] = t11; 81: $[8] = t12; 82: $[9] = t13; 83: } else { 84: t13 = $[9]; 85: } 86: let t14; 87: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 88: t14 = <Text>Only accept if you trust your organization's IT administration and expect these settings to be configured.</Text>; 89: $[10] = t14; 90: } else { 91: t14 = $[10]; 92: } 93: let t15; 94: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 95: t15 = [{ 96: label: "Yes, I trust these settings", 97: value: "accept" 98: }, { 99: label: "No, exit Claude Code", 100: value: "exit" 101: }]; 102: $[11] = t15; 103: } else { 104: t15 = $[11]; 105: } 106: let t16; 107: if ($[12] !== onChange) { 108: t16 = <Select options={t15} onChange={value_0 => onChange(value_0 as 'accept' | 'exit')} onCancel={() => onChange("exit")} />; 109: $[12] = onChange; 110: $[13] = t16; 111: } else { 112: t16 = $[13]; 113: } 114: let t17; 115: if ($[14] !== exitState.keyName || $[15] !== exitState.pending) { 116: t17 = <Text dimColor={true}>{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <>Enter to confirm · Esc to exit</>}</Text>; 117: $[14] = exitState.keyName; 118: $[15] = exitState.pending; 119: $[16] = t17; 120: } else { 121: t17 = $[16]; 122: } 123: let t18; 124: if ($[17] !== T1 || $[18] !== t13 || $[19] !== t16 || $[20] !== t17 || $[21] !== t9) { 125: t18 = <T1 flexDirection={t6} gap={t7} paddingTop={t8}>{t9}{t13}{t14}{t16}{t17}</T1>; 126: $[17] = T1; 127: $[18] = t13; 128: $[19] = t16; 129: $[20] = t17; 130: $[21] = t9; 131: $[22] = t18; 132: } else { 133: t18 = $[22]; 134: } 135: let t19; 136: if ($[23] !== T0 || $[24] !== t18) { 137: t19 = <T0 color={t3} titleColor={t4} title={t5}>{t18}</T0>; 138: $[23] = T0; 139: $[24] = t18; 140: $[25] = t19; 141: } else { 142: t19 = $[25]; 143: } 144: return t19; 145: } 146: function _temp(item, index) { 147: return <Box key={index} paddingLeft={2}><Text><Text dimColor={true}>· </Text><Text>{item}</Text></Text></Box>; 148: }

File: src/components/ManagedSettingsSecurityDialog/utils.ts

typescript 1: import { 2: DANGEROUS_SHELL_SETTINGS, 3: SAFE_ENV_VARS, 4: } from '../../utils/managedEnvConstants.js' 5: import type { SettingsJson } from '../../utils/settings/types.js' 6: import { jsonStringify } from '../../utils/slowOperations.js' 7: type DangerousShellSetting = (typeof DANGEROUS_SHELL_SETTINGS)[number] 8: export type DangerousSettings = { 9: shellSettings: Partial<Record<DangerousShellSetting, string>> 10: envVars: Record<string, string> 11: hasHooks: boolean 12: hooks?: unknown 13: } 14: export function extractDangerousSettings( 15: settings: SettingsJson | null | undefined, 16: ): DangerousSettings { 17: if (!settings) { 18: return { 19: shellSettings: {}, 20: envVars: {}, 21: hasHooks: false, 22: } 23: } 24: const shellSettings: Partial<Record<DangerousShellSetting, string>> = {} 25: for (const key of DANGEROUS_SHELL_SETTINGS) { 26: const value = settings[key] 27: if (typeof value === 'string' && value.length > 0) { 28: shellSettings[key] = value 29: } 30: } 31: const envVars: Record<string, string> = {} 32: if (settings.env && typeof settings.env === 'object') { 33: for (const [key, value] of Object.entries(settings.env)) { 34: if (typeof value === 'string' && value.length > 0) { 35: if (!SAFE_ENV_VARS.has(key.toUpperCase())) { 36: envVars[key] = value 37: } 38: } 39: } 40: } 41: const hasHooks = 42: settings.hooks !== undefined && 43: settings.hooks !== null && 44: typeof settings.hooks === 'object' && 45: Object.keys(settings.hooks).length > 0 46: return { 47: shellSettings, 48: envVars, 49: hasHooks, 50: hooks: hasHooks ? settings.hooks : undefined, 51: } 52: } 53: export function hasDangerousSettings(dangerous: DangerousSettings): boolean { 54: return ( 55: Object.keys(dangerous.shellSettings).length > 0 || 56: Object.keys(dangerous.envVars).length > 0 || 57: dangerous.hasHooks 58: ) 59: } 60: export function hasDangerousSettingsChanged( 61: oldSettings: SettingsJson | null | undefined, 62: newSettings: SettingsJson | null | undefined, 63: ): boolean { 64: const oldDangerous = extractDangerousSettings(oldSettings) 65: const newDangerous = extractDangerousSettings(newSettings) 66: if (!hasDangerousSettings(newDangerous)) { 67: return false 68: } 69: if (!hasDangerousSettings(oldDangerous)) { 70: return true 71: } 72: const oldJson = jsonStringify({ 73: shellSettings: oldDangerous.shellSettings, 74: envVars: oldDangerous.envVars, 75: hooks: oldDangerous.hooks, 76: }) 77: const newJson = jsonStringify({ 78: shellSettings: newDangerous.shellSettings, 79: envVars: newDangerous.envVars, 80: hooks: newDangerous.hooks, 81: }) 82: return oldJson !== newJson 83: } 84: export function formatDangerousSettingsList( 85: dangerous: DangerousSettings, 86: ): string[] { 87: const items: string[] = [] 88: for (const key of Object.keys(dangerous.shellSettings)) { 89: items.push(key) 90: } 91: for (const key of Object.keys(dangerous.envVars)) { 92: items.push(key) 93: } 94: if (dangerous.hasHooks) { 95: items.push('hooks') 96: } 97: return items 98: }

File: src/components/mcp/utils/reconnectHelpers.tsx

typescript 1: import type { Command } from '../../../commands.js'; 2: import type { MCPServerConnection, ServerResource } from '../../../services/mcp/types.js'; 3: import type { Tool } from '../../../Tool.js'; 4: export interface ReconnectResult { 5: message: string; 6: success: boolean; 7: } 8: export function handleReconnectResult(result: { 9: client: MCPServerConnection; 10: tools: Tool[]; 11: commands: Command[]; 12: resources?: ServerResource[]; 13: }, serverName: string): ReconnectResult { 14: switch (result.client.type) { 15: case 'connected': 16: return { 17: message: `Reconnected to ${serverName}.`, 18: success: true 19: }; 20: case 'needs-auth': 21: return { 22: message: `${serverName} requires authentication. Use the 'Authenticate' option.`, 23: success: false 24: }; 25: case 'failed': 26: return { 27: message: `Failed to reconnect to ${serverName}.`, 28: success: false 29: }; 30: default: 31: return { 32: message: `Unknown result when reconnecting to ${serverName}.`, 33: success: false 34: }; 35: } 36: } 37: export function handleReconnectError(error: unknown, serverName: string): string { 38: const errorMessage = error instanceof Error ? error.message : String(error); 39: return `Error reconnecting to ${serverName}: ${errorMessage}`; 40: }

File: src/components/mcp/CapabilitiesSection.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { Byline } from '../design-system/Byline.js'; 5: type Props = { 6: serverToolsCount: number; 7: serverPromptsCount: number; 8: serverResourcesCount: number; 9: }; 10: export function CapabilitiesSection(t0) { 11: const $ = _c(9); 12: const { 13: serverToolsCount, 14: serverPromptsCount, 15: serverResourcesCount 16: } = t0; 17: let capabilities; 18: if ($[0] !== serverPromptsCount || $[1] !== serverResourcesCount || $[2] !== serverToolsCount) { 19: capabilities = []; 20: if (serverToolsCount > 0) { 21: capabilities.push("tools"); 22: } 23: if (serverResourcesCount > 0) { 24: capabilities.push("resources"); 25: } 26: if (serverPromptsCount > 0) { 27: capabilities.push("prompts"); 28: } 29: $[0] = serverPromptsCount; 30: $[1] = serverResourcesCount; 31: $[2] = serverToolsCount; 32: $[3] = capabilities; 33: } else { 34: capabilities = $[3]; 35: } 36: let t1; 37: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 38: t1 = <Text bold={true}>Capabilities: </Text>; 39: $[4] = t1; 40: } else { 41: t1 = $[4]; 42: } 43: let t2; 44: if ($[5] !== capabilities) { 45: t2 = capabilities.length > 0 ? <Byline>{capabilities}</Byline> : "none"; 46: $[5] = capabilities; 47: $[6] = t2; 48: } else { 49: t2 = $[6]; 50: } 51: let t3; 52: if ($[7] !== t2) { 53: t3 = <Box>{t1}<Text color="text">{t2}</Text></Box>; 54: $[7] = t2; 55: $[8] = t3; 56: } else { 57: t3 = $[8]; 58: } 59: return t3; 60: }

File: src/components/mcp/ElicitationDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { ElicitRequestFormParams, ElicitRequestURLParams, ElicitResult, PrimitiveSchemaDefinition } from '@modelcontextprotocol/sdk/types.js'; 3: import figures from 'figures'; 4: import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 5: import { useRegisterOverlay } from '../../context/overlayContext.js'; 6: import { useNotifyAfterTimeout } from '../../hooks/useNotifyAfterTimeout.js'; 7: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 8: import { Box, Text, useInput } from '../../ink.js'; 9: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 10: import type { ElicitationRequestEvent } from '../../services/mcp/elicitationHandler.js'; 11: import { openBrowser } from '../../utils/browser.js'; 12: import { getEnumLabel, getEnumValues, getMultiSelectLabel, getMultiSelectValues, isDateTimeSchema, isEnumSchema, isMultiSelectEnumSchema, validateElicitationInput, validateElicitationInputAsync } from '../../utils/mcp/elicitationValidation.js'; 13: import { plural } from '../../utils/stringUtils.js'; 14: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 15: import { Byline } from '../design-system/Byline.js'; 16: import { Dialog } from '../design-system/Dialog.js'; 17: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 18: import TextInput from '../TextInput.js'; 19: type Props = { 20: event: ElicitationRequestEvent; 21: onResponse: (action: ElicitResult['action'], content?: ElicitResult['content']) => void; 22: onWaitingDismiss?: (action: 'dismiss' | 'retry' | 'cancel') => void; 23: }; 24: const isTextField = (s: PrimitiveSchemaDefinition) => ['string', 'number', 'integer'].includes(s.type); 25: const RESOLVING_SPINNER_CHARS = '\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F'; 26: const advanceSpinnerFrame = (f: number) => (f + 1) % RESOLVING_SPINNER_CHARS.length; 27: function resetTypeahead(ta: { 28: buffer: string; 29: timer: ReturnType<typeof setTimeout> | undefined; 30: }): void { 31: ta.buffer = ''; 32: ta.timer = undefined; 33: } 34: /** 35: * Isolated spinner glyph for a field that is being resolved asynchronously. 36: * Owns its own 80ms animation timer so ticks only re-render this tiny leaf, 37: * not the entire ElicitationFormDialog (~1200 lines + renderFormFields). 38: * Mounted/unmounted by the parent via the `isResolving` condition. 39: * 40: * Not using the shared <Spinner /> from ../Spinner.js: that one renders in a 41: * <Box width={2}> with color="text", which would break the 1-col checkbox 42: * column alignment here (other checkbox states are width-1 glyphs). 43: */ 44: function ResolvingSpinner() { 45: const $ = _c(4); 46: const [frame, setFrame] = useState(0); 47: let t0; 48: let t1; 49: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 50: t0 = () => { 51: const timer = setInterval(setFrame, 80, advanceSpinnerFrame); 52: return () => clearInterval(timer); 53: }; 54: t1 = []; 55: $[0] = t0; 56: $[1] = t1; 57: } else { 58: t0 = $[0]; 59: t1 = $[1]; 60: } 61: useEffect(t0, t1); 62: const t2 = RESOLVING_SPINNER_CHARS[frame]; 63: let t3; 64: if ($[2] !== t2) { 65: t3 = <Text color="warning">{t2}</Text>; 66: $[2] = t2; 67: $[3] = t3; 68: } else { 69: t3 = $[3]; 70: } 71: return t3; 72: } 73: /** Format an ISO date/datetime for display, keeping the ISO value for submission. */ 74: function formatDateDisplay(isoValue: string, schema: PrimitiveSchemaDefinition): string { 75: try { 76: const date = new Date(isoValue); 77: if (Number.isNaN(date.getTime())) return isoValue; 78: const format = 'format' in schema ? schema.format : undefined; 79: if (format === 'date-time') { 80: return date.toLocaleDateString('en-US', { 81: weekday: 'short', 82: year: 'numeric', 83: month: 'short', 84: day: 'numeric', 85: hour: 'numeric', 86: minute: '2-digit', 87: timeZoneName: 'short' 88: }); 89: } 90: const parts = isoValue.split('-'); 91: if (parts.length === 3) { 92: const local = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2])); 93: return local.toLocaleDateString('en-US', { 94: weekday: 'short', 95: year: 'numeric', 96: month: 'short', 97: day: 'numeric' 98: }); 99: } 100: return isoValue; 101: } catch { 102: return isoValue; 103: } 104: } 105: export function ElicitationDialog(t0) { 106: const $ = _c(7); 107: const { 108: event, 109: onResponse, 110: onWaitingDismiss 111: } = t0; 112: if (event.params.mode === "url") { 113: let t1; 114: if ($[0] !== event || $[1] !== onResponse || $[2] !== onWaitingDismiss) { 115: t1 = <ElicitationURLDialog event={event} onResponse={onResponse} onWaitingDismiss={onWaitingDismiss} />; 116: $[0] = event; 117: $[1] = onResponse; 118: $[2] = onWaitingDismiss; 119: $[3] = t1; 120: } else { 121: t1 = $[3]; 122: } 123: return t1; 124: } 125: let t1; 126: if ($[4] !== event || $[5] !== onResponse) { 127: t1 = <ElicitationFormDialog event={event} onResponse={onResponse} />; 128: $[4] = event; 129: $[5] = onResponse; 130: $[6] = t1; 131: } else { 132: t1 = $[6]; 133: } 134: return t1; 135: } 136: function ElicitationFormDialog({ 137: event, 138: onResponse 139: }: { 140: event: ElicitationRequestEvent; 141: onResponse: Props['onResponse']; 142: }): React.ReactNode { 143: const { 144: serverName, 145: signal 146: } = event; 147: const request = event.params as ElicitRequestFormParams; 148: const { 149: message, 150: requestedSchema 151: } = request; 152: const hasFields = Object.keys(requestedSchema.properties).length > 0; 153: const [focusedButton, setFocusedButton] = useState<'accept' | 'decline' | null>(hasFields ? null : 'accept'); 154: const [formValues, setFormValues] = useState<Record<string, string | number | boolean | string[]>>(() => { 155: const initialValues: Record<string, string | number | boolean | string[]> = {}; 156: if (requestedSchema.properties) { 157: for (const [propName, propSchema] of Object.entries(requestedSchema.properties)) { 158: if (typeof propSchema === 'object' && propSchema !== null) { 159: if (propSchema.default !== undefined) { 160: initialValues[propName] = propSchema.default; 161: } 162: } 163: } 164: } 165: return initialValues; 166: }); 167: const [validationErrors, setValidationErrors] = useState<Record<string, string>>(() => { 168: const initialErrors: Record<string, string> = {}; 169: for (const [propName_0, propSchema_0] of Object.entries(requestedSchema.properties)) { 170: if (isTextField(propSchema_0) && propSchema_0?.default !== undefined) { 171: const validation = validateElicitationInput(String(propSchema_0.default), propSchema_0); 172: if (!validation.isValid && validation.error) { 173: initialErrors[propName_0] = validation.error; 174: } 175: } 176: } 177: return initialErrors; 178: }); 179: useEffect(() => { 180: if (!signal) return; 181: const handleAbort = () => { 182: onResponse('cancel'); 183: }; 184: if (signal.aborted) { 185: handleAbort(); 186: return; 187: } 188: signal.addEventListener('abort', handleAbort); 189: return () => { 190: signal.removeEventListener('abort', handleAbort); 191: }; 192: }, [signal, onResponse]); 193: const schemaFields = useMemo(() => { 194: const requiredFields = requestedSchema.required ?? []; 195: return Object.entries(requestedSchema.properties).map(([name, schema]) => ({ 196: name, 197: schema, 198: isRequired: requiredFields.includes(name) 199: })); 200: }, [requestedSchema]); 201: const [currentFieldIndex, setCurrentFieldIndex] = useState<number | undefined>(hasFields ? 0 : undefined); 202: const [textInputValue, setTextInputValue] = useState(() => { 203: const firstField = schemaFields[0]; 204: if (firstField && isTextField(firstField.schema)) { 205: const val = formValues[firstField.name]; 206: if (val === undefined) return ''; 207: return String(val); 208: } 209: return ''; 210: }); 211: const [textInputCursorOffset, setTextInputCursorOffset] = useState(textInputValue.length); 212: const [resolvingFields, setResolvingFields] = useState<Set<string>>(() => new Set()); 213: // Accordion state (shared by multi-select and single-select enum) 214: const [expandedAccordion, setExpandedAccordion] = useState<string | undefined>(); 215: const [accordionOptionIndex, setAccordionOptionIndex] = useState(0); 216: const dateDebounceRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); 217: const resolveAbortRef = useRef<Map<string, AbortController>>(new Map()); 218: const enumTypeaheadRef = useRef({ 219: buffer: '', 220: timer: undefined as ReturnType<typeof setTimeout> | undefined 221: }); 222: // Clear pending debounce/typeahead timers and abort in-flight async 223: // validations on unmount so they don't fire against an unmounted component 224: useEffect(() => () => { 225: if (dateDebounceRef.current !== undefined) { 226: clearTimeout(dateDebounceRef.current); 227: } 228: const ta = enumTypeaheadRef.current; 229: if (ta.timer !== undefined) { 230: clearTimeout(ta.timer); 231: } 232: for (const controller of resolveAbortRef.current.values()) { 233: controller.abort(); 234: } 235: resolveAbortRef.current.clear(); 236: }, []); 237: const { 238: columns, 239: rows 240: } = useTerminalSize(); 241: const currentField = currentFieldIndex !== undefined ? schemaFields[currentFieldIndex] : undefined; 242: const currentFieldIsText = currentField !== undefined && isTextField(currentField.schema) && !isEnumSchema(currentField.schema); 243: const isEditingTextField = currentFieldIsText && !focusedButton; 244: useRegisterOverlay('elicitation'); 245: useNotifyAfterTimeout('Claude Code needs your input', 'elicitation_dialog'); 246: const syncTextInput = useCallback((fieldIndex: number | undefined) => { 247: if (fieldIndex === undefined) { 248: setTextInputValue(''); 249: setTextInputCursorOffset(0); 250: return; 251: } 252: const field = schemaFields[fieldIndex]; 253: if (field && isTextField(field.schema) && !isEnumSchema(field.schema)) { 254: const val_0 = formValues[field.name]; 255: const text = val_0 !== undefined ? String(val_0) : ''; 256: setTextInputValue(text); 257: setTextInputCursorOffset(text.length); 258: } 259: }, [schemaFields, formValues]); 260: function validateMultiSelect(fieldName: string, schema_0: PrimitiveSchemaDefinition) { 261: if (!isMultiSelectEnumSchema(schema_0)) return; 262: const selected = formValues[fieldName] as string[] | undefined ?? []; 263: const fieldRequired = schemaFields.find(f => f.name === fieldName)?.isRequired ?? false; 264: const min = schema_0.minItems; 265: const max = schema_0.maxItems; 266: // Skip minItems check when field is optional and unset 267: if (min !== undefined && selected.length < min && (selected.length > 0 || fieldRequired)) { 268: updateValidationError(fieldName, `Select at least ${min} ${plural(min, 'item')}`); 269: } else if (max !== undefined && selected.length > max) { 270: updateValidationError(fieldName, `Select at most ${max} ${plural(max, 'item')}`); 271: } else { 272: updateValidationError(fieldName); 273: } 274: } 275: function handleNavigation(direction: 'up' | 'down'): void { 276: // Collapse accordion and validate on navigate away 277: if (currentField && isMultiSelectEnumSchema(currentField.schema)) { 278: validateMultiSelect(currentField.name, currentField.schema); 279: setExpandedAccordion(undefined); 280: } else if (currentField && isEnumSchema(currentField.schema)) { 281: setExpandedAccordion(undefined); 282: } 283: // Commit current text field before navigating away 284: if (isEditingTextField && currentField) { 285: commitTextField(currentField.name, currentField.schema, textInputValue); 286: // Cancel any pending debounce — we're resolving now on navigate-away 287: if (dateDebounceRef.current !== undefined) { 288: clearTimeout(dateDebounceRef.current); 289: dateDebounceRef.current = undefined; 290: } 291: // For date/datetime fields that failed sync validation, try async NL parsing 292: if (isDateTimeSchema(currentField.schema) && textInputValue.trim() !== '' && validationErrors[currentField.name]) { 293: resolveFieldAsync(currentField.name, currentField.schema, textInputValue); 294: } 295: } 296: // Fields + accept + decline 297: const itemCount = schemaFields.length + 2; 298: const index = currentFieldIndex ?? (focusedButton === 'accept' ? schemaFields.length : focusedButton === 'decline' ? schemaFields.length + 1 : undefined); 299: const nextIndex = index !== undefined ? (index + (direction === 'up' ? itemCount - 1 : 1)) % itemCount : 0; 300: if (nextIndex < schemaFields.length) { 301: setCurrentFieldIndex(nextIndex); 302: setFocusedButton(null); 303: syncTextInput(nextIndex); 304: } else { 305: setCurrentFieldIndex(undefined); 306: setFocusedButton(nextIndex === schemaFields.length ? 'accept' : 'decline'); 307: setTextInputValue(''); 308: } 309: } 310: function setField(fieldName_0: string, value: number | string | boolean | string[] | undefined) { 311: setFormValues(prev => { 312: const next = { 313: ...prev 314: }; 315: if (value === undefined) { 316: delete next[fieldName_0]; 317: } else { 318: next[fieldName_0] = value; 319: } 320: return next; 321: }); 322: // Clear "required" error when a value is provided 323: if (value !== undefined && validationErrors[fieldName_0] === 'This field is required') { 324: updateValidationError(fieldName_0); 325: } 326: } 327: function updateValidationError(fieldName_1: string, error?: string) { 328: setValidationErrors(prev_0 => { 329: const next_0 = { 330: ...prev_0 331: }; 332: if (error) { 333: next_0[fieldName_1] = error; 334: } else { 335: delete next_0[fieldName_1]; 336: } 337: return next_0; 338: }); 339: } 340: function unsetField(fieldName_2: string) { 341: if (!fieldName_2) return; 342: setField(fieldName_2, undefined); 343: updateValidationError(fieldName_2); 344: setTextInputValue(''); 345: setTextInputCursorOffset(0); 346: } 347: function commitTextField(fieldName_3: string, schema_1: PrimitiveSchemaDefinition, value_0: string) { 348: const trimmedValue = value_0.trim(); 349: // Empty input for non-plain-string types means unset 350: if (trimmedValue === '' && (schema_1.type !== 'string' || 'format' in schema_1 && schema_1.format !== undefined)) { 351: unsetField(fieldName_3); 352: return; 353: } 354: if (trimmedValue === '') { 355: // Empty plain string — keep or unset depending on whether it was set 356: if (formValues[fieldName_3] !== undefined) { 357: setField(fieldName_3, ''); 358: } 359: return; 360: } 361: const validation_0 = validateElicitationInput(value_0, schema_1); 362: setField(fieldName_3, validation_0.isValid ? validation_0.value : value_0); 363: updateValidationError(fieldName_3, validation_0.isValid ? undefined : validation_0.error); 364: } 365: function resolveFieldAsync(fieldName_4: string, schema_2: PrimitiveSchemaDefinition, rawValue: string) { 366: if (!signal) return; 367: // Abort any existing resolution for this field 368: const existing = resolveAbortRef.current.get(fieldName_4); 369: if (existing) { 370: existing.abort(); 371: } 372: const controller_0 = new AbortController(); 373: resolveAbortRef.current.set(fieldName_4, controller_0); 374: setResolvingFields(prev_1 => new Set(prev_1).add(fieldName_4)); 375: void validateElicitationInputAsync(rawValue, schema_2, controller_0.signal).then(result => { 376: resolveAbortRef.current.delete(fieldName_4); 377: setResolvingFields(prev_2 => { 378: const next_1 = new Set(prev_2); 379: next_1.delete(fieldName_4); 380: return next_1; 381: }); 382: if (controller_0.signal.aborted) return; 383: if (result.isValid) { 384: setField(fieldName_4, result.value); 385: updateValidationError(fieldName_4); 386: // Update the text input if we're still on this field 387: const isoText = String(result.value); 388: setTextInputValue(prev_3 => { 389: // Only replace if the field is still showing the raw input 390: if (prev_3 === rawValue) { 391: setTextInputCursorOffset(isoText.length); 392: return isoText; 393: } 394: return prev_3; 395: }); 396: } else { 397: // Keep raw text, show validation error 398: updateValidationError(fieldName_4, result.error); 399: } 400: }, () => { 401: resolveAbortRef.current.delete(fieldName_4); 402: setResolvingFields(prev_4 => { 403: const next_2 = new Set(prev_4); 404: next_2.delete(fieldName_4); 405: return next_2; 406: }); 407: }); 408: } 409: function handleTextInputChange(newValue: string) { 410: setTextInputValue(newValue); 411: // Commit immediately on each keystroke (sync validation) 412: if (currentField) { 413: commitTextField(currentField.name, currentField.schema, newValue); 414: // For date/datetime fields, debounce async NL parsing after 2s of inactivity 415: if (dateDebounceRef.current !== undefined) { 416: clearTimeout(dateDebounceRef.current); 417: dateDebounceRef.current = undefined; 418: } 419: if (isDateTimeSchema(currentField.schema) && newValue.trim() !== '' && validationErrors[currentField.name]) { 420: const fieldName_5 = currentField.name; 421: const schema_3 = currentField.schema; 422: dateDebounceRef.current = setTimeout((dateDebounceRef_0, resolveFieldAsync_0, fieldName_6, schema_4, newValue_0) => { 423: dateDebounceRef_0.current = undefined; 424: resolveFieldAsync_0(fieldName_6, schema_4, newValue_0); 425: }, 2000, dateDebounceRef, resolveFieldAsync, fieldName_5, schema_3, newValue); 426: } 427: } 428: } 429: function handleTextInputSubmit() { 430: handleNavigation('down'); 431: } 432: /** 433: * Append a keystroke to the typeahead buffer (reset after 2s idle) and 434: * call `onMatch` with the index of the first label that prefix-matches. 435: * Shared by boolean y/n, enum accordion, and multi-select accordion. 436: */ 437: function runTypeahead(char: string, labels: string[], onMatch: (index: number) => void) { 438: const ta_0 = enumTypeaheadRef.current; 439: if (ta_0.timer !== undefined) clearTimeout(ta_0.timer); 440: ta_0.buffer += char.toLowerCase(); 441: ta_0.timer = setTimeout(resetTypeahead, 2000, ta_0); 442: const match = labels.findIndex(l => l.startsWith(ta_0.buffer)); 443: if (match !== -1) onMatch(match); 444: } 445: useKeybinding('confirm:no', () => { 446: if (isEditingTextField && currentField) { 447: const val_1 = formValues[currentField.name]; 448: setTextInputValue(val_1 !== undefined ? String(val_1) : ''); 449: setTextInputCursorOffset(0); 450: } 451: onResponse('cancel'); 452: }, { 453: context: 'Settings', 454: isActive: !!currentField && !focusedButton && !expandedAccordion 455: }); 456: useInput((_input, key) => { 457: if (isEditingTextField && !key.upArrow && !key.downArrow && !key.return && !key.backspace) { 458: return; 459: } 460: if (expandedAccordion && currentField && isMultiSelectEnumSchema(currentField.schema)) { 461: const msSchema = currentField.schema; 462: const msValues = getMultiSelectValues(msSchema); 463: const selected_0 = formValues[currentField.name] as string[] ?? []; 464: if (key.leftArrow || key.escape) { 465: setExpandedAccordion(undefined); 466: validateMultiSelect(currentField.name, msSchema); 467: return; 468: } 469: if (key.upArrow) { 470: if (accordionOptionIndex === 0) { 471: setExpandedAccordion(undefined); 472: validateMultiSelect(currentField.name, msSchema); 473: } else { 474: setAccordionOptionIndex(accordionOptionIndex - 1); 475: } 476: return; 477: } 478: if (key.downArrow) { 479: if (accordionOptionIndex >= msValues.length - 1) { 480: setExpandedAccordion(undefined); 481: handleNavigation('down'); 482: } else { 483: setAccordionOptionIndex(accordionOptionIndex + 1); 484: } 485: return; 486: } 487: if (_input === ' ') { 488: const optionValue = msValues[accordionOptionIndex]; 489: if (optionValue !== undefined) { 490: const newSelected = selected_0.includes(optionValue) ? selected_0.filter(v => v !== optionValue) : [...selected_0, optionValue]; 491: const newValue_1 = newSelected.length > 0 ? newSelected : undefined; 492: setField(currentField.name, newValue_1); 493: const min_0 = msSchema.minItems; 494: const max_0 = msSchema.maxItems; 495: if (min_0 !== undefined && newSelected.length < min_0 && (newSelected.length > 0 || currentField.isRequired)) { 496: updateValidationError(currentField.name, `Select at least ${min_0} ${plural(min_0, 'item')}`); 497: } else if (max_0 !== undefined && newSelected.length > max_0) { 498: updateValidationError(currentField.name, `Select at most ${max_0} ${plural(max_0, 'item')}`); 499: } else { 500: updateValidationError(currentField.name); 501: } 502: } 503: return; 504: } 505: if (key.return) { 506: const optionValue_0 = msValues[accordionOptionIndex]; 507: if (optionValue_0 !== undefined && !selected_0.includes(optionValue_0)) { 508: setField(currentField.name, [...selected_0, optionValue_0]); 509: } 510: setExpandedAccordion(undefined); 511: handleNavigation('down'); 512: return; 513: } 514: if (_input) { 515: const labels_0 = msValues.map(v_0 => getMultiSelectLabel(msSchema, v_0).toLowerCase()); 516: runTypeahead(_input, labels_0, setAccordionOptionIndex); 517: return; 518: } 519: return; 520: } 521: if (expandedAccordion && currentField && isEnumSchema(currentField.schema)) { 522: const enumSchema = currentField.schema; 523: const enumValues = getEnumValues(enumSchema); 524: if (key.leftArrow || key.escape) { 525: setExpandedAccordion(undefined); 526: return; 527: } 528: if (key.upArrow) { 529: if (accordionOptionIndex === 0) { 530: setExpandedAccordion(undefined); 531: } else { 532: setAccordionOptionIndex(accordionOptionIndex - 1); 533: } 534: return; 535: } 536: if (key.downArrow) { 537: if (accordionOptionIndex >= enumValues.length - 1) { 538: setExpandedAccordion(undefined); 539: handleNavigation('down'); 540: } else { 541: setAccordionOptionIndex(accordionOptionIndex + 1); 542: } 543: return; 544: } 545: if (_input === ' ') { 546: const optionValue_1 = enumValues[accordionOptionIndex]; 547: if (optionValue_1 !== undefined) { 548: setField(currentField.name, optionValue_1); 549: } 550: setExpandedAccordion(undefined); 551: return; 552: } 553: if (key.return) { 554: const optionValue_2 = enumValues[accordionOptionIndex]; 555: if (optionValue_2 !== undefined) { 556: setField(currentField.name, optionValue_2); 557: } 558: setExpandedAccordion(undefined); 559: handleNavigation('down'); 560: return; 561: } 562: if (_input) { 563: const labels_1 = enumValues.map(v_1 => getEnumLabel(enumSchema, v_1).toLowerCase()); 564: runTypeahead(_input, labels_1, setAccordionOptionIndex); 565: return; 566: } 567: return; 568: } 569: if (key.return && focusedButton === 'accept') { 570: if (validateRequired() && Object.keys(validationErrors).length === 0) { 571: onResponse('accept', formValues); 572: } else { 573: const requiredFields_0 = requestedSchema.required || []; 574: for (const fieldName_7 of requiredFields_0) { 575: if (formValues[fieldName_7] === undefined) { 576: updateValidationError(fieldName_7, 'This field is required'); 577: } 578: } 579: const firstBadIndex = schemaFields.findIndex(f_0 => requiredFields_0.includes(f_0.name) && formValues[f_0.name] === undefined || validationErrors[f_0.name] !== undefined); 580: if (firstBadIndex !== -1) { 581: setCurrentFieldIndex(firstBadIndex); 582: setFocusedButton(null); 583: syncTextInput(firstBadIndex); 584: } 585: } 586: return; 587: } 588: if (key.return && focusedButton === 'decline') { 589: onResponse('decline'); 590: return; 591: } 592: if (key.upArrow || key.downArrow) { 593: const ta_1 = enumTypeaheadRef.current; 594: ta_1.buffer = ''; 595: if (ta_1.timer !== undefined) { 596: clearTimeout(ta_1.timer); 597: ta_1.timer = undefined; 598: } 599: handleNavigation(key.upArrow ? 'up' : 'down'); 600: return; 601: } 602: if (focusedButton && (key.leftArrow || key.rightArrow)) { 603: setFocusedButton(focusedButton === 'accept' ? 'decline' : 'accept'); 604: return; 605: } 606: if (!currentField) return; 607: const { 608: schema: schema_5, 609: name: name_0 610: } = currentField; 611: const value_1 = formValues[name_0]; 612: if (schema_5.type === 'boolean') { 613: if (_input === ' ') { 614: setField(name_0, value_1 === undefined ? true : !value_1); 615: return; 616: } 617: if (key.return) { 618: handleNavigation('down'); 619: return; 620: } 621: if (key.backspace && value_1 !== undefined) { 622: unsetField(name_0); 623: return; 624: } 625: if (_input && !key.return) { 626: runTypeahead(_input, ['yes', 'no'], i => setField(name_0, i === 0)); 627: return; 628: } 629: return; 630: } 631: if (isEnumSchema(schema_5) || isMultiSelectEnumSchema(schema_5)) { 632: if (key.return) { 633: handleNavigation('down'); 634: return; 635: } 636: if (key.backspace && value_1 !== undefined) { 637: unsetField(name_0); 638: return; 639: } 640: let labels_2: string[]; 641: let startIdx = 0; 642: if (isEnumSchema(schema_5)) { 643: const vals = getEnumValues(schema_5); 644: labels_2 = vals.map(v_2 => getEnumLabel(schema_5, v_2).toLowerCase()); 645: if (value_1 !== undefined) { 646: startIdx = Math.max(0, vals.indexOf(value_1 as string)); 647: } 648: } else { 649: const vals_0 = getMultiSelectValues(schema_5); 650: labels_2 = vals_0.map(v_3 => getMultiSelectLabel(schema_5, v_3).toLowerCase()); 651: } 652: if (key.rightArrow) { 653: setExpandedAccordion(name_0); 654: setAccordionOptionIndex(startIdx); 655: return; 656: } 657: if (_input && !key.leftArrow) { 658: runTypeahead(_input, labels_2, i_0 => { 659: setExpandedAccordion(name_0); 660: setAccordionOptionIndex(i_0); 661: }); 662: return; 663: } 664: return; 665: } 666: if (key.backspace) { 667: if (isEditingTextField && textInputValue === '') { 668: unsetField(name_0); 669: return; 670: } 671: } 672: // Text field Enter is handled by TextInput's onSubmit 673: }, { 674: isActive: true 675: }); 676: function validateRequired(): boolean { 677: const requiredFields_1 = requestedSchema.required || []; 678: for (const fieldName_8 of requiredFields_1) { 679: const value_2 = formValues[fieldName_8]; 680: if (value_2 === undefined || value_2 === null || value_2 === '') { 681: return false; 682: } 683: if (Array.isArray(value_2) && value_2.length === 0) { 684: return false; 685: } 686: } 687: return true; 688: } 689: // Scroll windowing: compute visible field range 690: // Overhead: ~9 lines (dialog chrome, buttons, footer). 691: // Each field: ~3 lines (label + description + validation spacer). 692: // NOTE(v2): Multi-select accordion expands to N+3 lines when open. 693: // For now we assume 3 lines per field; an expanded accordion may 694: // temporarily push content off-screen (terminal scrollback handles it). 695: // To generalize: track per-field height (3 for collapsed, N+3 for 696: // expanded multi-select) and compute a pixel-budget window instead 697: // of a simple item-count window. 698: const LINES_PER_FIELD = 3; 699: const DIALOG_OVERHEAD = 14; 700: const maxVisibleFields = Math.max(2, Math.floor((rows - DIALOG_OVERHEAD) / LINES_PER_FIELD)); 701: const scrollWindow = useMemo(() => { 702: const total = schemaFields.length; 703: if (total <= maxVisibleFields) { 704: return { 705: start: 0, 706: end: total 707: }; 708: } 709: // When buttons are focused (currentFieldIndex undefined), pin to end 710: const focusIdx = currentFieldIndex ?? total - 1; 711: let start = Math.max(0, focusIdx - Math.floor(maxVisibleFields / 2)); 712: const end = Math.min(start + maxVisibleFields, total); 713: // Adjust start if we hit the bottom 714: start = Math.max(0, end - maxVisibleFields); 715: return { 716: start, 717: end 718: }; 719: }, [schemaFields.length, maxVisibleFields, currentFieldIndex]); 720: const hasFieldsAbove = scrollWindow.start > 0; 721: const hasFieldsBelow = scrollWindow.end < schemaFields.length; 722: function renderFormFields(): React.ReactNode { 723: if (!schemaFields.length) return null; 724: return <Box flexDirection="column"> 725: {hasFieldsAbove && <Box marginLeft={2}> 726: <Text dimColor> 727: {figures.arrowUp} {scrollWindow.start} more above 728: </Text> 729: </Box>} 730: {schemaFields.slice(scrollWindow.start, scrollWindow.end).map((field_0, visibleIdx) => { 731: const index_0 = scrollWindow.start + visibleIdx; 732: const { 733: name: name_1, 734: schema: schema_6, 735: isRequired 736: } = field_0; 737: const isActive = index_0 === currentFieldIndex && !focusedButton; 738: const value_3 = formValues[name_1]; 739: const hasValue = value_3 !== undefined && (!Array.isArray(value_3) || value_3.length > 0); 740: const error_0 = validationErrors[name_1]; 741: // Checkbox: spinner → ⚠ error → ✔ set → * required → space 742: const isResolving = resolvingFields.has(name_1); 743: const checkbox = isResolving ? <ResolvingSpinner /> : error_0 ? <Text color="error">{figures.warning}</Text> : hasValue ? <Text color="success" dimColor={!isActive}> 744: {figures.tick} 745: </Text> : isRequired ? <Text color="error">*</Text> : <Text> </Text>; 746: // Selection color matches field status 747: const selectionColor = error_0 ? 'error' : hasValue ? 'success' : isRequired ? 'error' : 'suggestion'; 748: const activeColor = isActive ? selectionColor : undefined; 749: const label = <Text color={activeColor} bold={isActive}> 750: {schema_6.title || name_1} 751: </Text>; 752: let valueContent: React.ReactNode; 753: let accordionContent: React.ReactNode = null; 754: if (isMultiSelectEnumSchema(schema_6)) { 755: const msValues_0 = getMultiSelectValues(schema_6); 756: const selected_1 = value_3 as string[] | undefined ?? []; 757: const isExpanded = expandedAccordion === name_1 && isActive; 758: if (isExpanded) { 759: valueContent = <Text dimColor>{figures.triangleDownSmall}</Text>; 760: accordionContent = <Box flexDirection="column" marginLeft={6}> 761: {msValues_0.map((optVal, optIdx) => { 762: const optLabel = getMultiSelectLabel(schema_6, optVal); 763: const isChecked = selected_1.includes(optVal); 764: const isFocused = optIdx === accordionOptionIndex; 765: return <Box key={optVal} gap={1}> 766: <Text color="suggestion"> 767: {isFocused ? figures.pointer : ' '} 768: </Text> 769: <Text color={isChecked ? 'success' : undefined}> 770: {isChecked ? figures.checkboxOn : figures.checkboxOff} 771: </Text> 772: <Text color={isFocused ? 'suggestion' : undefined} bold={isFocused}> 773: {optLabel} 774: </Text> 775: </Box>; 776: })} 777: </Box>; 778: } else { 779: const arrow = isActive ? <Text dimColor>{figures.triangleRightSmall} </Text> : null; 780: if (selected_1.length > 0) { 781: const displayLabels = selected_1.map(v_4 => getMultiSelectLabel(schema_6, v_4)); 782: valueContent = <Text> 783: {arrow} 784: <Text color={activeColor} bold={isActive}> 785: {displayLabels.join(', ')} 786: </Text> 787: </Text>; 788: } else { 789: valueContent = <Text> 790: {arrow} 791: <Text dimColor italic> 792: not set 793: </Text> 794: </Text>; 795: } 796: } 797: } else if (isEnumSchema(schema_6)) { 798: const enumValues_0 = getEnumValues(schema_6); 799: const isExpanded_0 = expandedAccordion === name_1 && isActive; 800: if (isExpanded_0) { 801: valueContent = <Text dimColor>{figures.triangleDownSmall}</Text>; 802: accordionContent = <Box flexDirection="column" marginLeft={6}> 803: {enumValues_0.map((optVal_0, optIdx_0) => { 804: const optLabel_0 = getEnumLabel(schema_6, optVal_0); 805: const isSelected = value_3 === optVal_0; 806: const isFocused_0 = optIdx_0 === accordionOptionIndex; 807: return <Box key={optVal_0} gap={1}> 808: <Text color="suggestion"> 809: {isFocused_0 ? figures.pointer : ' '} 810: </Text> 811: <Text color={isSelected ? 'success' : undefined}> 812: {isSelected ? figures.radioOn : figures.radioOff} 813: </Text> 814: <Text color={isFocused_0 ? 'suggestion' : undefined} bold={isFocused_0}> 815: {optLabel_0} 816: </Text> 817: </Box>; 818: })} 819: </Box>; 820: } else { 821: const arrow_0 = isActive ? <Text dimColor>{figures.triangleRightSmall} </Text> : null; 822: if (hasValue) { 823: valueContent = <Text> 824: {arrow_0} 825: <Text color={activeColor} bold={isActive}> 826: {getEnumLabel(schema_6, value_3 as string)} 827: </Text> 828: </Text>; 829: } else { 830: valueContent = <Text> 831: {arrow_0} 832: <Text dimColor italic> 833: not set 834: </Text> 835: </Text>; 836: } 837: } 838: } else if (schema_6.type === 'boolean') { 839: if (isActive) { 840: valueContent = hasValue ? <Text color={activeColor} bold> 841: {value_3 ? figures.checkboxOn : figures.checkboxOff} 842: </Text> : <Text dimColor>{figures.checkboxOff}</Text>; 843: } else { 844: valueContent = hasValue ? <Text> 845: {value_3 ? figures.checkboxOn : figures.checkboxOff} 846: </Text> : <Text dimColor italic> 847: not set 848: </Text>; 849: } 850: } else if (isTextField(schema_6)) { 851: if (isActive) { 852: valueContent = <TextInput value={textInputValue} onChange={handleTextInputChange} onSubmit={handleTextInputSubmit} placeholder={`Type something\u{2026}`} columns={Math.min(columns - 20, 60)} cursorOffset={textInputCursorOffset} onChangeCursorOffset={setTextInputCursorOffset} focus showCursor />; 853: } else { 854: const displayValue = hasValue && isDateTimeSchema(schema_6) ? formatDateDisplay(String(value_3), schema_6) : String(value_3); 855: valueContent = hasValue ? <Text>{displayValue}</Text> : <Text dimColor italic> 856: not set 857: </Text>; 858: } 859: } else { 860: valueContent = hasValue ? <Text>{String(value_3)}</Text> : <Text dimColor italic> 861: not set 862: </Text>; 863: } 864: return <Box key={name_1} flexDirection="column"> 865: <Box gap={1}> 866: <Text color={selectionColor}> 867: {isActive ? figures.pointer : ' '} 868: </Text> 869: {checkbox} 870: <Box> 871: {label} 872: <Text color={activeColor}>: </Text> 873: {valueContent} 874: </Box> 875: </Box> 876: {accordionContent} 877: {schema_6.description && <Box marginLeft={6}> 878: <Text dimColor>{schema_6.description}</Text> 879: </Box>} 880: <Box marginLeft={6} height={1}> 881: {error_0 ? <Text color="error" italic> 882: {error_0} 883: </Text> : <Text> </Text>} 884: </Box> 885: </Box>; 886: })} 887: {hasFieldsBelow && <Box marginLeft={2}> 888: <Text dimColor> 889: {figures.arrowDown} {schemaFields.length - scrollWindow.end} more 890: below 891: </Text> 892: </Box>} 893: </Box>; 894: } 895: return <Dialog title={`MCP server \u201c${serverName}\u201d requests your input`} subtitle={`\n${message}`} color="permission" onCancel={() => onResponse('cancel')} isCancelActive={(!currentField || !!focusedButton) && !expandedAccordion} inputGuide={exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline> 896: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /> 897: <KeyboardShortcutHint shortcut="↑↓" action="navigate" /> 898: {currentField && <KeyboardShortcutHint shortcut="Backspace" action="unset" />} 899: {currentField && currentField.schema.type === 'boolean' && <KeyboardShortcutHint shortcut="Space" action="toggle" />} 900: {currentField && isEnumSchema(currentField.schema) && (expandedAccordion ? <KeyboardShortcutHint shortcut="Space" action="select" /> : <KeyboardShortcutHint shortcut="→" action="expand" />)} 901: {currentField && isMultiSelectEnumSchema(currentField.schema) && (expandedAccordion ? <KeyboardShortcutHint shortcut="Space" action="toggle" /> : <KeyboardShortcutHint shortcut="→" action="expand" />)} 902: </Byline>}> 903: <Box flexDirection="column"> 904: {renderFormFields()} 905: <Box> 906: <Text color="success"> 907: {focusedButton === 'accept' ? figures.pointer : ' '} 908: </Text> 909: <Text bold={focusedButton === 'accept'} color={focusedButton === 'accept' ? 'success' : undefined} dimColor={focusedButton !== 'accept'}> 910: {' Accept '} 911: </Text> 912: <Text color="error"> 913: {focusedButton === 'decline' ? figures.pointer : ' '} 914: </Text> 915: <Text bold={focusedButton === 'decline'} color={focusedButton === 'decline' ? 'error' : undefined} dimColor={focusedButton !== 'decline'}> 916: {' Decline'} 917: </Text> 918: </Box> 919: </Box> 920: </Dialog>; 921: } 922: function ElicitationURLDialog({ 923: event, 924: onResponse, 925: onWaitingDismiss 926: }: { 927: event: ElicitationRequestEvent; 928: onResponse: Props['onResponse']; 929: onWaitingDismiss: Props['onWaitingDismiss']; 930: }): React.ReactNode { 931: const { 932: serverName, 933: signal, 934: waitingState 935: } = event; 936: const urlParams = event.params as ElicitRequestURLParams; 937: const { 938: message, 939: url 940: } = urlParams; 941: const [phase, setPhase] = useState<'prompt' | 'waiting'>('prompt'); 942: const phaseRef = useRef<'prompt' | 'waiting'>('prompt'); 943: const [focusedButton, setFocusedButton] = useState<'accept' | 'decline' | 'open' | 'action' | 'cancel'>('accept'); 944: const showCancel = waitingState?.showCancel ?? false; 945: useNotifyAfterTimeout('Claude Code needs your input', 'elicitation_url_dialog'); 946: useRegisterOverlay('elicitation-url'); 947: phaseRef.current = phase; 948: const onWaitingDismissRef = useRef(onWaitingDismiss); 949: onWaitingDismissRef.current = onWaitingDismiss; 950: useEffect(() => { 951: const handleAbort = () => { 952: if (phaseRef.current === 'waiting') { 953: onWaitingDismissRef.current?.('cancel'); 954: } else { 955: onResponse('cancel'); 956: } 957: }; 958: if (signal.aborted) { 959: handleAbort(); 960: return; 961: } 962: signal.addEventListener('abort', handleAbort); 963: return () => signal.removeEventListener('abort', handleAbort); 964: }, [signal, onResponse]); 965: let domain = ''; 966: let urlBeforeDomain = ''; 967: let urlAfterDomain = ''; 968: try { 969: const parsed = new URL(url); 970: domain = parsed.hostname; 971: const domainStart = url.indexOf(domain); 972: urlBeforeDomain = url.slice(0, domainStart); 973: urlAfterDomain = url.slice(domainStart + domain.length); 974: } catch { 975: domain = url; 976: } 977: // Auto-dismiss when the server sends a completion notification (sets completed flag) 978: useEffect(() => { 979: if (phase === 'waiting' && event.completed) { 980: onWaitingDismiss?.(showCancel ? 'retry' : 'dismiss'); 981: } 982: }, [phase, event.completed, onWaitingDismiss, showCancel]); 983: const handleAccept = useCallback(() => { 984: void openBrowser(url); 985: onResponse('accept'); 986: setPhase('waiting'); 987: phaseRef.current = 'waiting'; 988: setFocusedButton('open'); 989: }, [onResponse, url]); 990: useInput((_input, key) => { 991: if (phase === 'prompt') { 992: if (key.leftArrow || key.rightArrow) { 993: setFocusedButton(prev => prev === 'accept' ? 'decline' : 'accept'); 994: return; 995: } 996: if (key.return) { 997: if (focusedButton === 'accept') { 998: handleAccept(); 999: } else { 1000: onResponse('decline'); 1001: } 1002: } 1003: } else { 1004: type ButtonName = 'accept' | 'decline' | 'open' | 'action' | 'cancel'; 1005: const waitingButtons: readonly ButtonName[] = showCancel ? ['open', 'action', 'cancel'] : ['open', 'action']; 1006: if (key.leftArrow || key.rightArrow) { 1007: setFocusedButton(prev_0 => { 1008: const idx = waitingButtons.indexOf(prev_0); 1009: const delta = key.rightArrow ? 1 : -1; 1010: return waitingButtons[(idx + delta + waitingButtons.length) % waitingButtons.length]!; 1011: }); 1012: return; 1013: } 1014: if (key.return) { 1015: if (focusedButton === 'open') { 1016: void openBrowser(url); 1017: } else if (focusedButton === 'cancel') { 1018: onWaitingDismiss?.('cancel'); 1019: } else { 1020: onWaitingDismiss?.(showCancel ? 'retry' : 'dismiss'); 1021: } 1022: } 1023: } 1024: }); 1025: if (phase === 'waiting') { 1026: const actionLabel = waitingState?.actionLabel ?? 'Continue without waiting'; 1027: return <Dialog title={`MCP server \u201c${serverName}\u201d \u2014 waiting for completion`} subtitle={`\n${message}`} color="permission" onCancel={() => onWaitingDismiss?.('cancel')} isCancelActive inputGuide={exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline> 1028: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /> 1029: <KeyboardShortcutHint shortcut="\u2190\u2192" action="switch" /> 1030: </Byline>}> 1031: <Box flexDirection="column"> 1032: <Box marginBottom={1} flexDirection="column"> 1033: <Text> 1034: {urlBeforeDomain} 1035: <Text bold>{domain}</Text> 1036: {urlAfterDomain} 1037: </Text> 1038: </Box> 1039: <Box marginBottom={1}> 1040: <Text dimColor italic> 1041: Waiting for the server to confirm completion… 1042: </Text> 1043: </Box> 1044: <Box> 1045: <Text color="success"> 1046: {focusedButton === 'open' ? figures.pointer : ' '} 1047: </Text> 1048: <Text bold={focusedButton === 'open'} color={focusedButton === 'open' ? 'success' : undefined} dimColor={focusedButton !== 'open'}> 1049: {' Reopen URL '} 1050: </Text> 1051: <Text color="success"> 1052: {focusedButton === 'action' ? figures.pointer : ' '} 1053: </Text> 1054: <Text bold={focusedButton === 'action'} color={focusedButton === 'action' ? 'success' : undefined} dimColor={focusedButton !== 'action'}> 1055: {` ${actionLabel}`} 1056: </Text> 1057: {showCancel && <> 1058: <Text> </Text> 1059: <Text color="error"> 1060: {focusedButton === 'cancel' ? figures.pointer : ' '} 1061: </Text> 1062: <Text bold={focusedButton === 'cancel'} color={focusedButton === 'cancel' ? 'error' : undefined} dimColor={focusedButton !== 'cancel'}> 1063: {' Cancel'} 1064: </Text> 1065: </>} 1066: </Box> 1067: </Box> 1068: </Dialog>; 1069: } 1070: return <Dialog title={`MCP server \u201c${serverName}\u201d wants to open a URL`} subtitle={`\n${message}`} color="permission" onCancel={() => onResponse('cancel')} isCancelActive inputGuide={exitState_0 => exitState_0.pending ? <Text>Press {exitState_0.keyName} again to exit</Text> : <Byline> 1071: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /> 1072: <KeyboardShortcutHint shortcut="\u2190\u2192" action="switch" /> 1073: </Byline>}> 1074: <Box flexDirection="column"> 1075: <Box marginBottom={1} flexDirection="column"> 1076: <Text> 1077: {urlBeforeDomain} 1078: <Text bold>{domain}</Text> 1079: {urlAfterDomain} 1080: </Text> 1081: </Box> 1082: <Box> 1083: <Text color="success"> 1084: {focusedButton === 'accept' ? figures.pointer : ' '} 1085: </Text> 1086: <Text bold={focusedButton === 'accept'} color={focusedButton === 'accept' ? 'success' : undefined} dimColor={focusedButton !== 'accept'}> 1087: {' Accept '} 1088: </Text> 1089: <Text color="error"> 1090: {focusedButton === 'decline' ? figures.pointer : ' '} 1091: </Text> 1092: <Text bold={focusedButton === 'decline'} color={focusedButton === 'decline' ? 'error' : undefined} dimColor={focusedButton !== 'decline'}> 1093: {' Decline'} 1094: </Text> 1095: </Box> 1096: </Box> 1097: </Dialog>; 1098: }

File: src/components/mcp/index.ts

typescript 1: export { MCPAgentServerMenu } from './MCPAgentServerMenu.js' 2: export { MCPListPanel } from './MCPListPanel.js' 3: export { MCPReconnect } from './MCPReconnect.js' 4: export { MCPRemoteServerMenu } from './MCPRemoteServerMenu.js' 5: export { MCPSettings } from './MCPSettings.js' 6: export { MCPStdioServerMenu } from './MCPStdioServerMenu.js' 7: export { MCPToolDetailView } from './MCPToolDetailView.js' 8: export { MCPToolListView } from './MCPToolListView.js' 9: export type { AgentMcpServerInfo, MCPViewState, ServerInfo } from './types.js'

File: src/components/mcp/MCPAgentServerMenu.tsx

typescript 1: import figures from 'figures'; 2: import React, { useCallback, useEffect, useRef, useState } from 'react'; 3: import type { CommandResultDisplay } from '../../commands.js'; 4: import { Box, color, Link, Text, useTheme } from '../../ink.js'; 5: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 6: import { AuthenticationCancelledError, performMCPOAuthFlow } from '../../services/mcp/auth.js'; 7: import { capitalize } from '../../utils/stringUtils.js'; 8: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 9: import { Select } from '../CustomSelect/index.js'; 10: import { Byline } from '../design-system/Byline.js'; 11: import { Dialog } from '../design-system/Dialog.js'; 12: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 13: import { Spinner } from '../Spinner.js'; 14: import type { AgentMcpServerInfo } from './types.js'; 15: type Props = { 16: agentServer: AgentMcpServerInfo; 17: onCancel: () => void; 18: onComplete?: (result?: string, options?: { 19: display?: CommandResultDisplay; 20: }) => void; 21: }; 22: export function MCPAgentServerMenu({ 23: agentServer, 24: onCancel, 25: onComplete 26: }: Props): React.ReactNode { 27: const [theme] = useTheme(); 28: const [isAuthenticating, setIsAuthenticating] = useState(false); 29: const [error, setError] = useState<string | null>(null); 30: const [authorizationUrl, setAuthorizationUrl] = useState<string | null>(null); 31: const authAbortControllerRef = useRef<AbortController | null>(null); 32: useEffect(() => () => authAbortControllerRef.current?.abort(), []); 33: const handleEscCancel = useCallback(() => { 34: if (isAuthenticating) { 35: authAbortControllerRef.current?.abort(); 36: authAbortControllerRef.current = null; 37: setIsAuthenticating(false); 38: setAuthorizationUrl(null); 39: } 40: }, [isAuthenticating]); 41: useKeybinding('confirm:no', handleEscCancel, { 42: context: 'Confirmation', 43: isActive: isAuthenticating 44: }); 45: const handleAuthenticate = useCallback(async () => { 46: if (!agentServer.needsAuth || !agentServer.url) { 47: return; 48: } 49: setIsAuthenticating(true); 50: setError(null); 51: const controller = new AbortController(); 52: authAbortControllerRef.current = controller; 53: try { 54: const tempConfig = { 55: type: agentServer.transport as 'http' | 'sse', 56: url: agentServer.url 57: }; 58: await performMCPOAuthFlow(agentServer.name, tempConfig, setAuthorizationUrl, controller.signal); 59: onComplete?.(`Authentication successful for ${agentServer.name}. The server will connect when the agent runs.`); 60: } catch (err) { 61: if (err instanceof Error && !(err instanceof AuthenticationCancelledError)) { 62: setError(err.message); 63: } 64: } finally { 65: setIsAuthenticating(false); 66: authAbortControllerRef.current = null; 67: } 68: }, [agentServer, onComplete]); 69: const capitalizedServerName = capitalize(String(agentServer.name)); 70: if (isAuthenticating) { 71: return <Box flexDirection="column" gap={1} padding={1}> 72: <Text color="claude">Authenticating with {agentServer.name}…</Text> 73: <Box> 74: <Spinner /> 75: <Text> A browser window will open for authentication</Text> 76: </Box> 77: {authorizationUrl && <Box flexDirection="column"> 78: <Text dimColor> 79: If your browser doesn&apos;t open automatically, copy this URL 80: manually: 81: </Text> 82: <Link url={authorizationUrl} /> 83: </Box>} 84: <Box marginLeft={3}> 85: <Text dimColor> 86: Return here after authenticating in your browser.{' '} 87: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /> 88: </Text> 89: </Box> 90: </Box>; 91: } 92: const menuOptions = []; 93: if (agentServer.needsAuth) { 94: menuOptions.push({ 95: label: agentServer.isAuthenticated ? 'Re-authenticate' : 'Authenticate', 96: value: 'auth' 97: }); 98: } 99: menuOptions.push({ 100: label: 'Back', 101: value: 'back' 102: }); 103: return <Dialog title={`${capitalizedServerName} MCP Server`} subtitle="agent-only" onCancel={onCancel} inputGuide={exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline> 104: <KeyboardShortcutHint shortcut="↑↓" action="navigate" /> 105: <KeyboardShortcutHint shortcut="Enter" action="confirm" /> 106: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /> 107: </Byline>}> 108: <Box flexDirection="column" gap={0}> 109: <Box> 110: <Text bold>Type: </Text> 111: <Text dimColor>{agentServer.transport}</Text> 112: </Box> 113: {agentServer.url && <Box> 114: <Text bold>URL: </Text> 115: <Text dimColor>{agentServer.url}</Text> 116: </Box>} 117: {agentServer.command && <Box> 118: <Text bold>Command: </Text> 119: <Text dimColor>{agentServer.command}</Text> 120: </Box>} 121: <Box> 122: <Text bold>Used by: </Text> 123: <Text dimColor>{agentServer.sourceAgents.join(', ')}</Text> 124: </Box> 125: <Box marginTop={1}> 126: <Text bold>Status: </Text> 127: <Text> 128: {color('inactive', theme)(figures.radioOff)} not connected 129: (agent-only) 130: </Text> 131: </Box> 132: {agentServer.needsAuth && <Box> 133: <Text bold>Auth: </Text> 134: {agentServer.isAuthenticated ? <Text>{color('success', theme)(figures.tick)} authenticated</Text> : <Text> 135: {color('warning', theme)(figures.triangleUpOutline)} may need 136: authentication 137: </Text>} 138: </Box>} 139: </Box> 140: <Box> 141: <Text dimColor>This server connects only when running the agent.</Text> 142: </Box> 143: {error && <Box> 144: <Text color="error">Error: {error}</Text> 145: </Box>} 146: <Box> 147: <Select options={menuOptions} onChange={async value => { 148: switch (value) { 149: case 'auth': 150: await handleAuthenticate(); 151: break; 152: case 'back': 153: onCancel(); 154: break; 155: } 156: }} onCancel={onCancel} /> 157: </Box> 158: </Dialog>; 159: }

File: src/components/mcp/MCPListPanel.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React, { useCallback, useState } from 'react'; 4: import type { CommandResultDisplay } from '../../commands.js'; 5: import { Box, color, Link, Text, useTheme } from '../../ink.js'; 6: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 7: import type { ConfigScope } from '../../services/mcp/types.js'; 8: import { describeMcpConfigFilePath } from '../../services/mcp/utils.js'; 9: import { isDebugMode } from '../../utils/debug.js'; 10: import { plural } from '../../utils/stringUtils.js'; 11: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 12: import { Byline } from '../design-system/Byline.js'; 13: import { Dialog } from '../design-system/Dialog.js'; 14: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 15: import { McpParsingWarnings } from './McpParsingWarnings.js'; 16: import type { AgentMcpServerInfo, ServerInfo } from './types.js'; 17: type Props = { 18: servers: ServerInfo[]; 19: agentServers?: AgentMcpServerInfo[]; 20: onSelectServer: (server: ServerInfo) => void; 21: onSelectAgentServer?: (agentServer: AgentMcpServerInfo) => void; 22: onComplete: (result?: string, options?: { 23: display?: CommandResultDisplay; 24: }) => void; 25: defaultTab?: string; 26: }; 27: type SelectableItem = { 28: type: 'server'; 29: server: ServerInfo; 30: } | { 31: type: 'agent-server'; 32: agentServer: AgentMcpServerInfo; 33: }; 34: const SCOPE_ORDER: ConfigScope[] = ['project', 'local', 'user', 'enterprise']; 35: function getScopeHeading(scope: ConfigScope): { 36: label: string; 37: path?: string; 38: } { 39: switch (scope) { 40: case 'project': 41: return { 42: label: 'Project MCPs', 43: path: describeMcpConfigFilePath(scope) 44: }; 45: case 'user': 46: return { 47: label: 'User MCPs', 48: path: describeMcpConfigFilePath(scope) 49: }; 50: case 'local': 51: return { 52: label: 'Local MCPs', 53: path: describeMcpConfigFilePath(scope) 54: }; 55: case 'enterprise': 56: return { 57: label: 'Enterprise MCPs' 58: }; 59: case 'dynamic': 60: return { 61: label: 'Built-in MCPs', 62: path: 'always available' 63: }; 64: default: 65: return { 66: label: scope 67: }; 68: } 69: } 70: function groupServersByScope(serverList: ServerInfo[]): Map<ConfigScope, ServerInfo[]> { 71: const groups = new Map<ConfigScope, ServerInfo[]>(); 72: for (const server of serverList) { 73: const scope = server.scope; 74: if (!groups.has(scope)) { 75: groups.set(scope, []); 76: } 77: groups.get(scope)!.push(server); 78: } 79: for (const [, groupServers] of groups) { 80: groupServers.sort((a, b) => a.name.localeCompare(b.name)); 81: } 82: return groups; 83: } 84: export function MCPListPanel(t0) { 85: const $ = _c(78); 86: const { 87: servers, 88: agentServers: t1, 89: onSelectServer, 90: onSelectAgentServer, 91: onComplete 92: } = t0; 93: let t2; 94: if ($[0] !== t1) { 95: t2 = t1 === undefined ? [] : t1; 96: $[0] = t1; 97: $[1] = t2; 98: } else { 99: t2 = $[1]; 100: } 101: const agentServers = t2; 102: const [theme] = useTheme(); 103: const [selectedIndex, setSelectedIndex] = useState(0); 104: let t3; 105: if ($[2] !== servers) { 106: const regularServers = servers.filter(_temp); 107: t3 = groupServersByScope(regularServers); 108: $[2] = servers; 109: $[3] = t3; 110: } else { 111: t3 = $[3]; 112: } 113: const serversByScope = t3; 114: let t4; 115: if ($[4] !== servers) { 116: t4 = servers.filter(_temp2).sort(_temp3); 117: $[4] = servers; 118: $[5] = t4; 119: } else { 120: t4 = $[5]; 121: } 122: const claudeAiServers = t4; 123: let t5; 124: if ($[6] !== serversByScope) { 125: t5 = (serversByScope.get("dynamic") ?? []).sort(_temp4); 126: $[6] = serversByScope; 127: $[7] = t5; 128: } else { 129: t5 = $[7]; 130: } 131: const dynamicServers = t5; 132: let t6; 133: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 134: t6 = getScopeHeading("dynamic"); 135: $[8] = t6; 136: } else { 137: t6 = $[8]; 138: } 139: const dynamicHeading = t6; 140: let items; 141: if ($[9] !== agentServers || $[10] !== claudeAiServers || $[11] !== dynamicServers || $[12] !== serversByScope) { 142: items = []; 143: for (const scope of SCOPE_ORDER) { 144: const scopeServers = serversByScope.get(scope) ?? []; 145: for (const server of scopeServers) { 146: items.push({ 147: type: "server", 148: server 149: }); 150: } 151: } 152: for (const server_0 of claudeAiServers) { 153: items.push({ 154: type: "server", 155: server: server_0 156: }); 157: } 158: for (const agentServer of agentServers) { 159: items.push({ 160: type: "agent-server", 161: agentServer 162: }); 163: } 164: for (const server_1 of dynamicServers) { 165: items.push({ 166: type: "server", 167: server: server_1 168: }); 169: } 170: $[9] = agentServers; 171: $[10] = claudeAiServers; 172: $[11] = dynamicServers; 173: $[12] = serversByScope; 174: $[13] = items; 175: } else { 176: items = $[13]; 177: } 178: const selectableItems = items; 179: let t7; 180: if ($[14] !== onComplete) { 181: t7 = () => { 182: onComplete("MCP dialog dismissed", { 183: display: "system" 184: }); 185: }; 186: $[14] = onComplete; 187: $[15] = t7; 188: } else { 189: t7 = $[15]; 190: } 191: const handleCancel = t7; 192: let t8; 193: if ($[16] !== onSelectAgentServer || $[17] !== onSelectServer || $[18] !== selectableItems || $[19] !== selectedIndex) { 194: t8 = () => { 195: const item = selectableItems[selectedIndex]; 196: if (!item) { 197: return; 198: } 199: if (item.type === "server") { 200: onSelectServer(item.server); 201: } else { 202: if (item.type === "agent-server" && onSelectAgentServer) { 203: onSelectAgentServer(item.agentServer); 204: } 205: } 206: }; 207: $[16] = onSelectAgentServer; 208: $[17] = onSelectServer; 209: $[18] = selectableItems; 210: $[19] = selectedIndex; 211: $[20] = t8; 212: } else { 213: t8 = $[20]; 214: } 215: const handleSelect = t8; 216: let t10; 217: let t9; 218: if ($[21] !== selectableItems) { 219: t9 = () => setSelectedIndex(prev => prev === 0 ? selectableItems.length - 1 : prev - 1); 220: t10 = () => setSelectedIndex(prev_0 => prev_0 === selectableItems.length - 1 ? 0 : prev_0 + 1); 221: $[21] = selectableItems; 222: $[22] = t10; 223: $[23] = t9; 224: } else { 225: t10 = $[22]; 226: t9 = $[23]; 227: } 228: let t11; 229: if ($[24] !== handleCancel || $[25] !== handleSelect || $[26] !== t10 || $[27] !== t9) { 230: t11 = { 231: "confirm:previous": t9, 232: "confirm:next": t10, 233: "confirm:yes": handleSelect, 234: "confirm:no": handleCancel 235: }; 236: $[24] = handleCancel; 237: $[25] = handleSelect; 238: $[26] = t10; 239: $[27] = t9; 240: $[28] = t11; 241: } else { 242: t11 = $[28]; 243: } 244: let t12; 245: if ($[29] === Symbol.for("react.memo_cache_sentinel")) { 246: t12 = { 247: context: "Confirmation" 248: }; 249: $[29] = t12; 250: } else { 251: t12 = $[29]; 252: } 253: useKeybindings(t11, t12); 254: let t13; 255: if ($[30] !== selectableItems) { 256: t13 = server_2 => selectableItems.findIndex(item_0 => item_0.type === "server" && item_0.server === server_2); 257: $[30] = selectableItems; 258: $[31] = t13; 259: } else { 260: t13 = $[31]; 261: } 262: const getServerIndex = t13; 263: let t14; 264: if ($[32] !== selectableItems) { 265: t14 = agentServer_0 => selectableItems.findIndex(item_1 => item_1.type === "agent-server" && item_1.agentServer === agentServer_0); 266: $[32] = selectableItems; 267: $[33] = t14; 268: } else { 269: t14 = $[33]; 270: } 271: const getAgentServerIndex = t14; 272: let t15; 273: if ($[34] === Symbol.for("react.memo_cache_sentinel")) { 274: t15 = isDebugMode(); 275: $[34] = t15; 276: } else { 277: t15 = $[34]; 278: } 279: const debugMode = t15; 280: let t16; 281: if ($[35] !== servers) { 282: t16 = servers.some(_temp5); 283: $[35] = servers; 284: $[36] = t16; 285: } else { 286: t16 = $[36]; 287: } 288: const hasFailedClients = t16; 289: if (servers.length === 0 && agentServers.length === 0) { 290: return null; 291: } 292: let t17; 293: if ($[37] !== getServerIndex || $[38] !== selectedIndex || $[39] !== theme) { 294: t17 = server_3 => { 295: const index = getServerIndex(server_3); 296: const isSelected = selectedIndex === index; 297: let statusIcon; 298: let statusText; 299: if (server_3.client.type === "disabled") { 300: statusIcon = color("inactive", theme)(figures.radioOff); 301: statusText = "disabled"; 302: } else { 303: if (server_3.client.type === "connected") { 304: statusIcon = color("success", theme)(figures.tick); 305: statusText = "connected"; 306: } else { 307: if (server_3.client.type === "pending") { 308: statusIcon = color("inactive", theme)(figures.radioOff); 309: const { 310: reconnectAttempt, 311: maxReconnectAttempts 312: } = server_3.client; 313: if (reconnectAttempt && maxReconnectAttempts) { 314: statusText = `reconnecting (${reconnectAttempt}/${maxReconnectAttempts})…`; 315: } else { 316: statusText = "connecting\u2026"; 317: } 318: } else { 319: if (server_3.client.type === "needs-auth") { 320: statusIcon = color("warning", theme)(figures.triangleUpOutline); 321: statusText = "needs authentication"; 322: } else { 323: statusIcon = color("error", theme)(figures.cross); 324: statusText = "failed"; 325: } 326: } 327: } 328: } 329: return <Box key={`${server_3.name}-${index}`}><Text color={isSelected ? "suggestion" : undefined}>{isSelected ? `${figures.pointer} ` : " "}</Text><Text color={isSelected ? "suggestion" : undefined}>{server_3.name}</Text><Text dimColor={!isSelected}> · {statusIcon} </Text><Text dimColor={!isSelected}>{statusText}</Text></Box>; 330: }; 331: $[37] = getServerIndex; 332: $[38] = selectedIndex; 333: $[39] = theme; 334: $[40] = t17; 335: } else { 336: t17 = $[40]; 337: } 338: const renderServerItem = t17; 339: let t18; 340: if ($[41] !== getAgentServerIndex || $[42] !== selectedIndex || $[43] !== theme) { 341: t18 = agentServer_1 => { 342: const index_0 = getAgentServerIndex(agentServer_1); 343: const isSelected_0 = selectedIndex === index_0; 344: const statusIcon_0 = agentServer_1.needsAuth ? color("warning", theme)(figures.triangleUpOutline) : color("inactive", theme)(figures.radioOff); 345: const statusText_0 = agentServer_1.needsAuth ? "may need auth" : "agent-only"; 346: return <Box key={`agent-${agentServer_1.name}-${index_0}`}><Text color={isSelected_0 ? "suggestion" : undefined}>{isSelected_0 ? `${figures.pointer} ` : " "}</Text><Text color={isSelected_0 ? "suggestion" : undefined}>{agentServer_1.name}</Text><Text dimColor={!isSelected_0}> · {statusIcon_0} </Text><Text dimColor={!isSelected_0}>{statusText_0}</Text></Box>; 347: }; 348: $[41] = getAgentServerIndex; 349: $[42] = selectedIndex; 350: $[43] = theme; 351: $[44] = t18; 352: } else { 353: t18 = $[44]; 354: } 355: const renderAgentServerItem = t18; 356: const totalServers = servers.length + agentServers.length; 357: let t19; 358: if ($[45] === Symbol.for("react.memo_cache_sentinel")) { 359: t19 = <McpParsingWarnings />; 360: $[45] = t19; 361: } else { 362: t19 = $[45]; 363: } 364: let t20; 365: if ($[46] !== totalServers) { 366: t20 = plural(totalServers, "server"); 367: $[46] = totalServers; 368: $[47] = t20; 369: } else { 370: t20 = $[47]; 371: } 372: const t21 = `${totalServers} ${t20}`; 373: let t22; 374: if ($[48] !== renderServerItem || $[49] !== serversByScope) { 375: t22 = SCOPE_ORDER.map(scope_0 => { 376: const scopeServers_0 = serversByScope.get(scope_0); 377: if (!scopeServers_0 || scopeServers_0.length === 0) { 378: return null; 379: } 380: const heading = getScopeHeading(scope_0); 381: return <Box key={scope_0} flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true}>{heading.label}</Text>{heading.path && <Text dimColor={true}> ({heading.path})</Text>}</Box>{scopeServers_0.map(server_4 => renderServerItem(server_4))}</Box>; 382: }); 383: $[48] = renderServerItem; 384: $[49] = serversByScope; 385: $[50] = t22; 386: } else { 387: t22 = $[50]; 388: } 389: let t23; 390: if ($[51] !== claudeAiServers || $[52] !== renderServerItem) { 391: t23 = claudeAiServers.length > 0 && <Box flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true}>claude.ai</Text></Box>{claudeAiServers.map(server_5 => renderServerItem(server_5))}</Box>; 392: $[51] = claudeAiServers; 393: $[52] = renderServerItem; 394: $[53] = t23; 395: } else { 396: t23 = $[53]; 397: } 398: let t24; 399: if ($[54] !== agentServers || $[55] !== renderAgentServerItem) { 400: t24 = agentServers.length > 0 && <Box flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true}>Agent MCPs</Text></Box>{[...new Set(agentServers.flatMap(_temp6))].map(agentName => <Box key={agentName} flexDirection="column" marginTop={1}><Box paddingLeft={2}><Text dimColor={true}>@{agentName}</Text></Box>{agentServers.filter(s_3 => s_3.sourceAgents.includes(agentName)).map(agentServer_2 => renderAgentServerItem(agentServer_2))}</Box>)}</Box>; 401: $[54] = agentServers; 402: $[55] = renderAgentServerItem; 403: $[56] = t24; 404: } else { 405: t24 = $[56]; 406: } 407: let t25; 408: if ($[57] !== dynamicServers || $[58] !== renderServerItem) { 409: t25 = dynamicServers.length > 0 && <Box flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true}>{dynamicHeading.label}</Text>{dynamicHeading.path && <Text dimColor={true}> ({dynamicHeading.path})</Text>}</Box>{dynamicServers.map(server_6 => renderServerItem(server_6))}</Box>; 410: $[57] = dynamicServers; 411: $[58] = renderServerItem; 412: $[59] = t25; 413: } else { 414: t25 = $[59]; 415: } 416: let t26; 417: if ($[60] !== hasFailedClients) { 418: t26 = hasFailedClients && <Text dimColor={true}>{debugMode ? "\u203B Error logs shown inline with --debug" : "\u203B Run claude --debug to see error logs"}</Text>; 419: $[60] = hasFailedClients; 420: $[61] = t26; 421: } else { 422: t26 = $[61]; 423: } 424: let t27; 425: if ($[62] === Symbol.for("react.memo_cache_sentinel")) { 426: t27 = <Text dimColor={true}><Link url="https://code.claude.com/docs/en/mcp">https: 427: $[62] = t27; 428: } else { 429: t27 = $[62]; 430: } 431: let t28; 432: if ($[63] !== t26) { 433: t28 = <Box flexDirection="column">{t26}{t27}</Box>; 434: $[63] = t26; 435: $[64] = t28; 436: } else { 437: t28 = $[64]; 438: } 439: let t29; 440: if ($[65] !== t22 || $[66] !== t23 || $[67] !== t24 || $[68] !== t25 || $[69] !== t28) { 441: t29 = <Box flexDirection="column">{t22}{t23}{t24}{t25}{t28}</Box>; 442: $[65] = t22; 443: $[66] = t23; 444: $[67] = t24; 445: $[68] = t25; 446: $[69] = t28; 447: $[70] = t29; 448: } else { 449: t29 = $[70]; 450: } 451: let t30; 452: if ($[71] !== handleCancel || $[72] !== t21 || $[73] !== t29) { 453: t30 = <Dialog title="Manage MCP servers" subtitle={t21} onCancel={handleCancel} hideInputGuide={true}>{t29}</Dialog>; 454: $[71] = handleCancel; 455: $[72] = t21; 456: $[73] = t29; 457: $[74] = t30; 458: } else { 459: t30 = $[74]; 460: } 461: let t31; 462: if ($[75] === Symbol.for("react.memo_cache_sentinel")) { 463: t31 = <Box paddingX={1}><Text dimColor={true} italic={true}><Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="confirm" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline></Text></Box>; 464: $[75] = t31; 465: } else { 466: t31 = $[75]; 467: } 468: let t32; 469: if ($[76] !== t30) { 470: t32 = <Box flexDirection="column">{t19}{t30}{t31}</Box>; 471: $[76] = t30; 472: $[77] = t32; 473: } else { 474: t32 = $[77]; 475: } 476: return t32; 477: } 478: function _temp6(s_2) { 479: return s_2.sourceAgents; 480: } 481: function _temp5(s_1) { 482: return s_1.client.type === "failed"; 483: } 484: function _temp4(a_0, b_0) { 485: return a_0.name.localeCompare(b_0.name); 486: } 487: function _temp3(a, b) { 488: return a.name.localeCompare(b.name); 489: } 490: function _temp2(s_0) { 491: return s_0.client.config.type === "claudeai-proxy"; 492: } 493: function _temp(s) { 494: return s.client.config.type !== "claudeai-proxy"; 495: }

File: src/components/mcp/McpParsingWarnings.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useMemo } from 'react'; 3: import { getMcpConfigsByScope } from 'src/services/mcp/config.js'; 4: import type { ConfigScope } from 'src/services/mcp/types.js'; 5: import { describeMcpConfigFilePath, getScopeLabel } from 'src/services/mcp/utils.js'; 6: import type { ValidationError } from 'src/utils/settings/validation.js'; 7: import { Box, Link, Text } from '../../ink.js'; 8: function McpConfigErrorSection(t0) { 9: const $ = _c(26); 10: const { 11: scope, 12: parsingErrors, 13: warnings 14: } = t0; 15: const hasErrors = parsingErrors.length > 0; 16: const hasWarnings = warnings.length > 0; 17: if (!hasErrors && !hasWarnings) { 18: return null; 19: } 20: let t1; 21: if ($[0] !== hasErrors || $[1] !== hasWarnings) { 22: t1 = (hasErrors || hasWarnings) && <Text color={hasErrors ? "error" : "warning"}>[{hasErrors ? "Failed to parse" : "Contains warnings"}]{" "}</Text>; 23: $[0] = hasErrors; 24: $[1] = hasWarnings; 25: $[2] = t1; 26: } else { 27: t1 = $[2]; 28: } 29: let t2; 30: if ($[3] !== scope) { 31: t2 = getScopeLabel(scope); 32: $[3] = scope; 33: $[4] = t2; 34: } else { 35: t2 = $[4]; 36: } 37: let t3; 38: if ($[5] !== t2) { 39: t3 = <Text>{t2}</Text>; 40: $[5] = t2; 41: $[6] = t3; 42: } else { 43: t3 = $[6]; 44: } 45: let t4; 46: if ($[7] !== t1 || $[8] !== t3) { 47: t4 = <Box>{t1}{t3}</Box>; 48: $[7] = t1; 49: $[8] = t3; 50: $[9] = t4; 51: } else { 52: t4 = $[9]; 53: } 54: let t5; 55: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 56: t5 = <Text dimColor={true}>Location: </Text>; 57: $[10] = t5; 58: } else { 59: t5 = $[10]; 60: } 61: let t6; 62: if ($[11] !== scope) { 63: t6 = describeMcpConfigFilePath(scope); 64: $[11] = scope; 65: $[12] = t6; 66: } else { 67: t6 = $[12]; 68: } 69: let t7; 70: if ($[13] !== t6) { 71: t7 = <Box>{t5}<Text dimColor={true}>{t6}</Text></Box>; 72: $[13] = t6; 73: $[14] = t7; 74: } else { 75: t7 = $[14]; 76: } 77: let t8; 78: if ($[15] !== parsingErrors) { 79: t8 = parsingErrors.map(_temp); 80: $[15] = parsingErrors; 81: $[16] = t8; 82: } else { 83: t8 = $[16]; 84: } 85: let t9; 86: if ($[17] !== warnings) { 87: t9 = warnings.map(_temp2); 88: $[17] = warnings; 89: $[18] = t9; 90: } else { 91: t9 = $[18]; 92: } 93: let t10; 94: if ($[19] !== t8 || $[20] !== t9) { 95: t10 = <Box marginLeft={1} flexDirection="column">{t8}{t9}</Box>; 96: $[19] = t8; 97: $[20] = t9; 98: $[21] = t10; 99: } else { 100: t10 = $[21]; 101: } 102: let t11; 103: if ($[22] !== t10 || $[23] !== t4 || $[24] !== t7) { 104: t11 = <Box flexDirection="column" marginTop={1}>{t4}{t7}{t10}</Box>; 105: $[22] = t10; 106: $[23] = t4; 107: $[24] = t7; 108: $[25] = t11; 109: } else { 110: t11 = $[25]; 111: } 112: return t11; 113: } 114: function _temp2(warning, i_0) { 115: const serverName_0 = warning.mcpErrorMetadata?.serverName; 116: return <Box key={`warning-${i_0}`}><Text><Text dimColor={true}>└ </Text><Text color="warning">[Warning]</Text><Text dimColor={true}>{" "}{serverName_0 && `[${serverName_0}] `}{warning.path && warning.path !== "" ? `${warning.path}: ` : ""}{warning.message}</Text></Text></Box>; 117: } 118: function _temp(error, i) { 119: const serverName = error.mcpErrorMetadata?.serverName; 120: return <Box key={`error-${i}`}><Text><Text dimColor={true}>└ </Text><Text color="error">[Error]</Text><Text dimColor={true}>{" "}{serverName && `[${serverName}] `}{error.path && error.path !== "" ? `${error.path}: ` : ""}{error.message}</Text></Text></Box>; 121: } 122: export function McpParsingWarnings() { 123: const $ = _c(6); 124: let t0; 125: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 126: t0 = { 127: scope: "user", 128: config: getMcpConfigsByScope("user") 129: }; 130: $[0] = t0; 131: } else { 132: t0 = $[0]; 133: } 134: let t1; 135: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 136: t1 = { 137: scope: "project", 138: config: getMcpConfigsByScope("project") 139: }; 140: $[1] = t1; 141: } else { 142: t1 = $[1]; 143: } 144: let t2; 145: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 146: t2 = { 147: scope: "local", 148: config: getMcpConfigsByScope("local") 149: }; 150: $[2] = t2; 151: } else { 152: t2 = $[2]; 153: } 154: let t3; 155: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 156: t3 = [t0, t1, t2, { 157: scope: "enterprise", 158: config: getMcpConfigsByScope("enterprise") 159: }]; 160: $[3] = t3; 161: } else { 162: t3 = $[3]; 163: } 164: const scopes = t3 satisfies Array<{ 165: scope: ConfigScope; 166: config: { 167: errors: ValidationError[]; 168: }; 169: }>; 170: const hasParsingErrors = scopes.some(_temp3); 171: const hasWarnings = scopes.some(_temp4); 172: if (!hasParsingErrors && !hasWarnings) { 173: return null; 174: } 175: let t4; 176: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 177: t4 = <Text bold={true}>MCP Config Diagnostics</Text>; 178: $[4] = t4; 179: } else { 180: t4 = $[4]; 181: } 182: let t5; 183: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 184: t5 = <Box flexDirection="column" marginTop={1} marginBottom={1}>{t4}<Box marginTop={1}><Text dimColor={true}>For help configuring MCP servers, see:{" "}<Link url="https://code.claude.com/docs/en/mcp">https: 185: $[5] = t5; 186: } else { 187: t5 = $[5]; 188: } 189: return t5; 190: } 191: function _temp5(t0) { 192: const { 193: scope, 194: config: config_1 195: } = t0; 196: return <McpConfigErrorSection key={scope} scope={scope} parsingErrors={filterErrors(config_1.errors, "fatal")} warnings={filterErrors(config_1.errors, "warning")} />; 197: } 198: function _temp4(t0) { 199: const { 200: config: config_0 201: } = t0; 202: return filterErrors(config_0.errors, "warning").length > 0; 203: } 204: function _temp3(t0) { 205: const { 206: config 207: } = t0; 208: return filterErrors(config.errors, "fatal").length > 0; 209: } 210: function filterErrors(errors: ValidationError[], severity: 'fatal' | 'warning'): ValidationError[] { 211: return errors.filter(e => e.mcpErrorMetadata?.severity === severity); 212: }

File: src/components/mcp/MCPReconnect.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React, { useEffect, useState } from 'react'; 4: import type { CommandResultDisplay } from '../../commands.js'; 5: import { Box, color, Text, useTheme } from '../../ink.js'; 6: import { useMcpReconnect } from '../../services/mcp/MCPConnectionManager.js'; 7: import { useAppStateStore } from '../../state/AppState.js'; 8: import { Spinner } from '../Spinner.js'; 9: type Props = { 10: serverName: string; 11: onComplete: (result?: string, options?: { 12: display?: CommandResultDisplay; 13: }) => void; 14: }; 15: export function MCPReconnect(t0) { 16: const $ = _c(25); 17: const { 18: serverName, 19: onComplete 20: } = t0; 21: const [theme] = useTheme(); 22: const store = useAppStateStore(); 23: const reconnectMcpServer = useMcpReconnect(); 24: const [isReconnecting, setIsReconnecting] = useState(true); 25: const [error, setError] = useState(null); 26: let t1; 27: let t2; 28: if ($[0] !== onComplete || $[1] !== reconnectMcpServer || $[2] !== serverName || $[3] !== store) { 29: t1 = () => { 30: const attemptReconnect = async function attemptReconnect() { 31: ; 32: try { 33: const server = store.getState().mcp.clients.find(c => c.name === serverName); 34: if (!server) { 35: setError(`MCP server "${serverName}" not found`); 36: setIsReconnecting(false); 37: onComplete(`MCP server "${serverName}" not found`); 38: return; 39: } 40: const result = await reconnectMcpServer(serverName); 41: bb43: switch (result.client.type) { 42: case "connected": 43: { 44: setIsReconnecting(false); 45: onComplete(`Successfully reconnected to ${serverName}`); 46: break bb43; 47: } 48: case "needs-auth": 49: { 50: setError(`${serverName} requires authentication`); 51: setIsReconnecting(false); 52: onComplete(`${serverName} requires authentication. Use /mcp to authenticate.`); 53: break bb43; 54: } 55: case "pending": 56: case "failed": 57: case "disabled": 58: { 59: setError(`Failed to reconnect to ${serverName}`); 60: setIsReconnecting(false); 61: onComplete(`Failed to reconnect to ${serverName}`); 62: } 63: } 64: } catch (t3) { 65: const err = t3; 66: const errorMessage = err instanceof Error ? err.message : String(err); 67: setError(errorMessage); 68: setIsReconnecting(false); 69: onComplete(`Error: ${errorMessage}`); 70: } 71: }; 72: attemptReconnect(); 73: }; 74: t2 = [serverName, reconnectMcpServer, store, onComplete]; 75: $[0] = onComplete; 76: $[1] = reconnectMcpServer; 77: $[2] = serverName; 78: $[3] = store; 79: $[4] = t1; 80: $[5] = t2; 81: } else { 82: t1 = $[4]; 83: t2 = $[5]; 84: } 85: useEffect(t1, t2); 86: if (isReconnecting) { 87: let t3; 88: if ($[6] !== serverName) { 89: t3 = <Text color="text">Reconnecting to <Text bold={true}>{serverName}</Text></Text>; 90: $[6] = serverName; 91: $[7] = t3; 92: } else { 93: t3 = $[7]; 94: } 95: let t4; 96: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 97: t4 = <Box><Spinner /><Text> Establishing connection to MCP server</Text></Box>; 98: $[8] = t4; 99: } else { 100: t4 = $[8]; 101: } 102: let t5; 103: if ($[9] !== t3) { 104: t5 = <Box flexDirection="column" gap={1} padding={1}>{t3}{t4}</Box>; 105: $[9] = t3; 106: $[10] = t5; 107: } else { 108: t5 = $[10]; 109: } 110: return t5; 111: } 112: if (error) { 113: let t3; 114: if ($[11] !== theme) { 115: t3 = color("error", theme)(figures.cross); 116: $[11] = theme; 117: $[12] = t3; 118: } else { 119: t3 = $[12]; 120: } 121: let t4; 122: if ($[13] !== t3) { 123: t4 = <Text>{t3} </Text>; 124: $[13] = t3; 125: $[14] = t4; 126: } else { 127: t4 = $[14]; 128: } 129: let t5; 130: if ($[15] !== serverName) { 131: t5 = <Text color="error">Failed to reconnect to {serverName}</Text>; 132: $[15] = serverName; 133: $[16] = t5; 134: } else { 135: t5 = $[16]; 136: } 137: let t6; 138: if ($[17] !== t4 || $[18] !== t5) { 139: t6 = <Box>{t4}{t5}</Box>; 140: $[17] = t4; 141: $[18] = t5; 142: $[19] = t6; 143: } else { 144: t6 = $[19]; 145: } 146: let t7; 147: if ($[20] !== error) { 148: t7 = <Text dimColor={true}>Error: {error}</Text>; 149: $[20] = error; 150: $[21] = t7; 151: } else { 152: t7 = $[21]; 153: } 154: let t8; 155: if ($[22] !== t6 || $[23] !== t7) { 156: t8 = <Box flexDirection="column" gap={1} padding={1}>{t6}{t7}</Box>; 157: $[22] = t6; 158: $[23] = t7; 159: $[24] = t8; 160: } else { 161: t8 = $[24]; 162: } 163: return t8; 164: } 165: return null; 166: }

File: src/components/mcp/MCPRemoteServerMenu.tsx

typescript 1: import figures from 'figures'; 2: import React, { useEffect, useRef, useState } from 'react'; 3: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; 4: import type { CommandResultDisplay } from '../../commands.js'; 5: import { getOauthConfig } from '../../constants/oauth.js'; 6: import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 7: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 8: import { setClipboard } from '../../ink/termio/osc.js'; 9: import { Box, color, Link, Text, useInput, useTheme } from '../../ink.js'; 10: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 11: import { AuthenticationCancelledError, performMCPOAuthFlow, revokeServerTokens } from '../../services/mcp/auth.js'; 12: import { clearServerCache } from '../../services/mcp/client.js'; 13: import { useMcpReconnect, useMcpToggleEnabled } from '../../services/mcp/MCPConnectionManager.js'; 14: import { describeMcpConfigFilePath, excludeCommandsByServer, excludeResourcesByServer, excludeToolsByServer, filterMcpPromptsByServer } from '../../services/mcp/utils.js'; 15: import { useAppState, useSetAppState } from '../../state/AppState.js'; 16: import { getOauthAccountInfo } from '../../utils/auth.js'; 17: import { openBrowser } from '../../utils/browser.js'; 18: import { errorMessage } from '../../utils/errors.js'; 19: import { logMCPDebug } from '../../utils/log.js'; 20: import { capitalize } from '../../utils/stringUtils.js'; 21: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 22: import { Select } from '../CustomSelect/index.js'; 23: import { Byline } from '../design-system/Byline.js'; 24: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 25: import { Spinner } from '../Spinner.js'; 26: import TextInput from '../TextInput.js'; 27: import { CapabilitiesSection } from './CapabilitiesSection.js'; 28: import type { ClaudeAIServerInfo, HTTPServerInfo, SSEServerInfo } from './types.js'; 29: import { handleReconnectError, handleReconnectResult } from './utils/reconnectHelpers.js'; 30: type Props = { 31: server: SSEServerInfo | HTTPServerInfo | ClaudeAIServerInfo; 32: serverToolsCount: number; 33: onViewTools: () => void; 34: onCancel: () => void; 35: onComplete?: (result?: string, options?: { 36: display?: CommandResultDisplay; 37: }) => void; 38: borderless?: boolean; 39: }; 40: export function MCPRemoteServerMenu({ 41: server, 42: serverToolsCount, 43: onViewTools, 44: onCancel, 45: onComplete, 46: borderless = false 47: }: Props): React.ReactNode { 48: const [theme] = useTheme(); 49: const exitState = useExitOnCtrlCDWithKeybindings(); 50: const { 51: columns: terminalColumns 52: } = useTerminalSize(); 53: const [isAuthenticating, setIsAuthenticating] = React.useState(false); 54: const [error, setError] = React.useState<string | null>(null); 55: const mcp = useAppState(s => s.mcp); 56: const setAppState = useSetAppState(); 57: const [authorizationUrl, setAuthorizationUrl] = React.useState<string | null>(null); 58: const [isReconnecting, setIsReconnecting] = useState(false); 59: const authAbortControllerRef = useRef<AbortController | null>(null); 60: const [isClaudeAIAuthenticating, setIsClaudeAIAuthenticating] = useState(false); 61: const [claudeAIAuthUrl, setClaudeAIAuthUrl] = useState<string | null>(null); 62: const [isClaudeAIClearingAuth, setIsClaudeAIClearingAuth] = useState(false); 63: const [claudeAIClearAuthUrl, setClaudeAIClearAuthUrl] = useState<string | null>(null); 64: const [claudeAIClearAuthBrowserOpened, setClaudeAIClearAuthBrowserOpened] = useState(false); 65: const [urlCopied, setUrlCopied] = useState(false); 66: const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); 67: const unmountedRef = useRef(false); 68: const [callbackUrlInput, setCallbackUrlInput] = useState(''); 69: const [callbackUrlCursorOffset, setCallbackUrlCursorOffset] = useState(0); 70: const [manualCallbackSubmit, setManualCallbackSubmit] = useState<((url: string) => void) | null>(null); 71: // If the component unmounts mid-auth (e.g. a parent component's Esc handler 72: useEffect(() => () => { 73: unmountedRef.current = true; 74: authAbortControllerRef.current?.abort(); 75: if (copyTimeoutRef.current !== undefined) { 76: clearTimeout(copyTimeoutRef.current); 77: } 78: }, []); 79: const isEffectivelyAuthenticated = server.isAuthenticated || server.client.type === 'connected' && serverToolsCount > 0; 80: const reconnectMcpServer = useMcpReconnect(); 81: const handleClaudeAIAuthComplete = React.useCallback(async () => { 82: setIsClaudeAIAuthenticating(false); 83: setClaudeAIAuthUrl(null); 84: setIsReconnecting(true); 85: try { 86: const result = await reconnectMcpServer(server.name); 87: const success = result.client.type === 'connected'; 88: logEvent('tengu_claudeai_mcp_auth_completed', { 89: success 90: }); 91: if (success) { 92: onComplete?.(`Authentication successful. Connected to ${server.name}.`); 93: } else if (result.client.type === 'needs-auth') { 94: onComplete?.('Authentication successful, but server still requires authentication. You may need to manually restart Claude Code.'); 95: } else { 96: onComplete?.('Authentication successful, but server reconnection failed. You may need to manually restart Claude Code for the changes to take effect.'); 97: } 98: } catch (err) { 99: logEvent('tengu_claudeai_mcp_auth_completed', { 100: success: false 101: }); 102: onComplete?.(handleReconnectError(err, server.name)); 103: } finally { 104: setIsReconnecting(false); 105: } 106: }, [reconnectMcpServer, server.name, onComplete]); 107: const handleClaudeAIClearAuthComplete = React.useCallback(async () => { 108: await clearServerCache(server.name, { 109: ...server.config, 110: scope: server.scope 111: }); 112: setAppState(prev => { 113: const newClients = prev.mcp.clients.map(c => c.name === server.name ? { 114: ...c, 115: type: 'needs-auth' as const 116: } : c); 117: const newTools = excludeToolsByServer(prev.mcp.tools, server.name); 118: const newCommands = excludeCommandsByServer(prev.mcp.commands, server.name); 119: const newResources = excludeResourcesByServer(prev.mcp.resources, server.name); 120: return { 121: ...prev, 122: mcp: { 123: ...prev.mcp, 124: clients: newClients, 125: tools: newTools, 126: commands: newCommands, 127: resources: newResources 128: } 129: }; 130: }); 131: logEvent('tengu_claudeai_mcp_clear_auth_completed', {}); 132: onComplete?.(`Disconnected from ${server.name}.`); 133: setIsClaudeAIClearingAuth(false); 134: setClaudeAIClearAuthUrl(null); 135: setClaudeAIClearAuthBrowserOpened(false); 136: }, [server.name, server.config, server.scope, setAppState, onComplete]); 137: useKeybinding('confirm:no', () => { 138: authAbortControllerRef.current?.abort(); 139: authAbortControllerRef.current = null; 140: setIsAuthenticating(false); 141: setAuthorizationUrl(null); 142: }, { 143: context: 'Confirmation', 144: isActive: isAuthenticating 145: }); 146: useKeybinding('confirm:no', () => { 147: setIsClaudeAIAuthenticating(false); 148: setClaudeAIAuthUrl(null); 149: }, { 150: context: 'Confirmation', 151: isActive: isClaudeAIAuthenticating 152: }); 153: useKeybinding('confirm:no', () => { 154: setIsClaudeAIClearingAuth(false); 155: setClaudeAIClearAuthUrl(null); 156: setClaudeAIClearAuthBrowserOpened(false); 157: }, { 158: context: 'Confirmation', 159: isActive: isClaudeAIClearingAuth 160: }); 161: useInput((input, key) => { 162: if (key.return && isClaudeAIAuthenticating) { 163: void handleClaudeAIAuthComplete(); 164: } 165: if (key.return && isClaudeAIClearingAuth) { 166: if (claudeAIClearAuthBrowserOpened) { 167: void handleClaudeAIClearAuthComplete(); 168: } else { 169: const connectorsUrl = `${getOauthConfig().CLAUDE_AI_ORIGIN}/settings/connectors`; 170: setClaudeAIClearAuthUrl(connectorsUrl); 171: setClaudeAIClearAuthBrowserOpened(true); 172: void openBrowser(connectorsUrl); 173: } 174: } 175: if (input === 'c' && !urlCopied) { 176: const urlToCopy = authorizationUrl || claudeAIAuthUrl || claudeAIClearAuthUrl; 177: if (urlToCopy) { 178: void setClipboard(urlToCopy).then(raw => { 179: if (unmountedRef.current) return; 180: if (raw) process.stdout.write(raw); 181: setUrlCopied(true); 182: if (copyTimeoutRef.current !== undefined) { 183: clearTimeout(copyTimeoutRef.current); 184: } 185: copyTimeoutRef.current = setTimeout(setUrlCopied, 2000, false); 186: }); 187: } 188: } 189: }); 190: const capitalizedServerName = capitalize(String(server.name)); 191: const serverCommandsCount = filterMcpPromptsByServer(mcp.commands, server.name).length; 192: const toggleMcpServer = useMcpToggleEnabled(); 193: const handleClaudeAIAuth = React.useCallback(async () => { 194: const claudeAiBaseUrl = getOauthConfig().CLAUDE_AI_ORIGIN; 195: const accountInfo = getOauthAccountInfo(); 196: const orgUuid = accountInfo?.organizationUuid; 197: let authUrl: string; 198: if (orgUuid && server.config.type === 'claudeai-proxy' && server.config.id) { 199: const serverId = server.config.id.startsWith('mcprs') ? 'mcpsrv' + server.config.id.slice(5) : server.config.id; 200: const productSurface = encodeURIComponent(process.env.CLAUDE_CODE_ENTRYPOINT || 'cli'); 201: authUrl = `${claudeAiBaseUrl}/api/organizations/${orgUuid}/mcp/start-auth/${serverId}?product_surface=${productSurface}`; 202: } else { 203: authUrl = `${claudeAiBaseUrl}/settings/connectors`; 204: } 205: setClaudeAIAuthUrl(authUrl); 206: setIsClaudeAIAuthenticating(true); 207: logEvent('tengu_claudeai_mcp_auth_started', {}); 208: await openBrowser(authUrl); 209: }, [server.config]); 210: const handleClaudeAIClearAuth = React.useCallback(() => { 211: setIsClaudeAIClearingAuth(true); 212: logEvent('tengu_claudeai_mcp_clear_auth_started', {}); 213: }, []); 214: const handleToggleEnabled = React.useCallback(async () => { 215: const wasEnabled = server.client.type !== 'disabled'; 216: try { 217: await toggleMcpServer(server.name); 218: if (server.config.type === 'claudeai-proxy') { 219: logEvent('tengu_claudeai_mcp_toggle', { 220: new_state: (wasEnabled ? 'disabled' : 'enabled') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 221: }); 222: } 223: onCancel(); 224: } catch (err_0) { 225: const action = wasEnabled ? 'disable' : 'enable'; 226: onComplete?.(`Failed to ${action} MCP server '${server.name}': ${errorMessage(err_0)}`); 227: } 228: }, [server.client.type, server.config.type, server.name, toggleMcpServer, onCancel, onComplete]); 229: const handleAuthenticate = React.useCallback(async () => { 230: if (server.config.type === 'claudeai-proxy') return; 231: setIsAuthenticating(true); 232: setError(null); 233: const controller = new AbortController(); 234: authAbortControllerRef.current = controller; 235: try { 236: if (server.isAuthenticated && server.config) { 237: await revokeServerTokens(server.name, server.config, { 238: preserveStepUpState: true 239: }); 240: } 241: if (server.config) { 242: await performMCPOAuthFlow(server.name, server.config, setAuthorizationUrl, controller.signal, { 243: onWaitingForCallback: submit => { 244: setManualCallbackSubmit(() => submit); 245: } 246: }); 247: logEvent('tengu_mcp_auth_config_authenticate', { 248: wasAuthenticated: server.isAuthenticated 249: }); 250: const result_0 = await reconnectMcpServer(server.name); 251: if (result_0.client.type === 'connected') { 252: const message = isEffectivelyAuthenticated ? `Authentication successful. Reconnected to ${server.name}.` : `Authentication successful. Connected to ${server.name}.`; 253: onComplete?.(message); 254: } else if (result_0.client.type === 'needs-auth') { 255: onComplete?.('Authentication successful, but server still requires authentication. You may need to manually restart Claude Code.'); 256: } else { 257: logMCPDebug(server.name, `Reconnection failed after authentication`); 258: onComplete?.('Authentication successful, but server reconnection failed. You may need to manually restart Claude Code for the changes to take effect.'); 259: } 260: } 261: } catch (err_1) { 262: if (err_1 instanceof Error && !(err_1 instanceof AuthenticationCancelledError)) { 263: setError(err_1.message); 264: } 265: } finally { 266: setIsAuthenticating(false); 267: authAbortControllerRef.current = null; 268: setManualCallbackSubmit(null); 269: setCallbackUrlInput(''); 270: } 271: }, [server.isAuthenticated, server.config, server.name, onComplete, reconnectMcpServer, isEffectivelyAuthenticated]); 272: const handleClearAuth = async () => { 273: if (server.config.type === 'claudeai-proxy') return; 274: if (server.config) { 275: await revokeServerTokens(server.name, server.config); 276: logEvent('tengu_mcp_auth_config_clear', {}); 277: await clearServerCache(server.name, { 278: ...server.config, 279: scope: server.scope 280: }); 281: setAppState(prev_0 => { 282: const newClients_0 = prev_0.mcp.clients.map(c_0 => 283: c_0.name === server.name ? { 284: ...c_0, 285: type: 'failed' as const 286: } : c_0); 287: const newTools_0 = excludeToolsByServer(prev_0.mcp.tools, server.name); 288: const newCommands_0 = excludeCommandsByServer(prev_0.mcp.commands, server.name); 289: const newResources_0 = excludeResourcesByServer(prev_0.mcp.resources, server.name); 290: return { 291: ...prev_0, 292: mcp: { 293: ...prev_0.mcp, 294: clients: newClients_0, 295: tools: newTools_0, 296: commands: newCommands_0, 297: resources: newResources_0 298: } 299: }; 300: }); 301: onComplete?.(`Authentication cleared for ${server.name}.`); 302: } 303: }; 304: if (isAuthenticating) { 305: const authCopy = server.config.type !== 'claudeai-proxy' && server.config.oauth?.xaa ? ' Authenticating via your identity provider' : ' A browser window will open for authentication'; 306: return <Box flexDirection="column" gap={1} padding={1}> 307: <Text color="claude">Authenticating with {server.name}…</Text> 308: <Box> 309: <Spinner /> 310: <Text>{authCopy}</Text> 311: </Box> 312: {authorizationUrl && <Box flexDirection="column"> 313: <Box> 314: <Text dimColor> 315: If your browser doesn&apos;t open automatically, copy this URL 316: manually{' '} 317: </Text> 318: {urlCopied ? <Text color="success">(Copied!)</Text> : <Text dimColor> 319: <KeyboardShortcutHint shortcut="c" action="copy" parens /> 320: </Text>} 321: </Box> 322: <Link url={authorizationUrl} /> 323: </Box>} 324: {isAuthenticating && authorizationUrl && manualCallbackSubmit && <Box flexDirection="column" marginTop={1}> 325: <Text dimColor> 326: If the redirect page shows a connection error, paste the URL from 327: your browser&apos;s address bar: 328: </Text> 329: <Box> 330: <Text dimColor>URL {'>'} </Text> 331: <TextInput value={callbackUrlInput} onChange={setCallbackUrlInput} onSubmit={(value: string) => { 332: manualCallbackSubmit(value.trim()); 333: setCallbackUrlInput(''); 334: }} cursorOffset={callbackUrlCursorOffset} onChangeCursorOffset={setCallbackUrlCursorOffset} columns={terminalColumns - 8} /> 335: </Box> 336: </Box>} 337: <Box marginLeft={3}> 338: <Text dimColor> 339: Return here after authenticating in your browser. Press Esc to go 340: back. 341: </Text> 342: </Box> 343: </Box>; 344: } 345: if (isClaudeAIAuthenticating) { 346: return <Box flexDirection="column" gap={1} padding={1}> 347: <Text color="claude">Authenticating with {server.name}…</Text> 348: <Box> 349: <Spinner /> 350: <Text> A browser window will open for authentication</Text> 351: </Box> 352: {claudeAIAuthUrl && <Box flexDirection="column"> 353: <Box> 354: <Text dimColor> 355: If your browser doesn&apos;t open automatically, copy this URL 356: manually{' '} 357: </Text> 358: {urlCopied ? <Text color="success">(Copied!)</Text> : <Text dimColor> 359: <KeyboardShortcutHint shortcut="c" action="copy" parens /> 360: </Text>} 361: </Box> 362: <Link url={claudeAIAuthUrl} /> 363: </Box>} 364: <Box marginLeft={3} flexDirection="column"> 365: <Text color="permission"> 366: Press <Text bold>Enter</Text> after authenticating in your browser. 367: </Text> 368: <Text dimColor italic> 369: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" /> 370: </Text> 371: </Box> 372: </Box>; 373: } 374: if (isClaudeAIClearingAuth) { 375: return <Box flexDirection="column" gap={1} padding={1}> 376: <Text color="claude">Clear authentication for {server.name}</Text> 377: {claudeAIClearAuthBrowserOpened ? <> 378: <Text> 379: Find the MCP server in the browser and click 380: &quot;Disconnect&quot;. 381: </Text> 382: {claudeAIClearAuthUrl && <Box flexDirection="column"> 383: <Box> 384: <Text dimColor> 385: If your browser didn&apos;t open automatically, copy this 386: URL manually{' '} 387: </Text> 388: {urlCopied ? <Text color="success">(Copied!)</Text> : <Text dimColor> 389: <KeyboardShortcutHint shortcut="c" action="copy" parens /> 390: </Text>} 391: </Box> 392: <Link url={claudeAIClearAuthUrl} /> 393: </Box>} 394: <Box marginLeft={3} flexDirection="column"> 395: <Text color="permission"> 396: Press <Text bold>Enter</Text> when done. 397: </Text> 398: <Text dimColor italic> 399: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" /> 400: </Text> 401: </Box> 402: </> : <> 403: <Text> 404: This will open claude.ai in the browser. Find the MCP server in 405: the list and click &quot;Disconnect&quot;. 406: </Text> 407: <Box marginLeft={3} flexDirection="column"> 408: <Text color="permission"> 409: Press <Text bold>Enter</Text> to open the browser. 410: </Text> 411: <Text dimColor italic> 412: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" /> 413: </Text> 414: </Box> 415: </>} 416: </Box>; 417: } 418: if (isReconnecting) { 419: return <Box flexDirection="column" gap={1} padding={1}> 420: <Text color="text"> 421: Connecting to <Text bold>{server.name}</Text>… 422: </Text> 423: <Box> 424: <Spinner /> 425: <Text> Establishing connection to MCP server</Text> 426: </Box> 427: <Text dimColor>This may take a few moments.</Text> 428: </Box>; 429: } 430: const menuOptions = []; 431: // If server is disabled, show Enable first as the primary action 432: if (server.client.type === 'disabled') { 433: menuOptions.push({ 434: label: 'Enable', 435: value: 'toggle-enabled' 436: }); 437: } 438: if (server.client.type === 'connected' && serverToolsCount > 0) { 439: menuOptions.push({ 440: label: 'View tools', 441: value: 'tools' 442: }); 443: } 444: if (server.config.type === 'claudeai-proxy') { 445: if (server.client.type === 'connected') { 446: menuOptions.push({ 447: label: 'Clear authentication', 448: value: 'claudeai-clear-auth' 449: }); 450: } else if (server.client.type !== 'disabled') { 451: menuOptions.push({ 452: label: 'Authenticate', 453: value: 'claudeai-auth' 454: }); 455: } 456: } else { 457: if (isEffectivelyAuthenticated) { 458: menuOptions.push({ 459: label: 'Re-authenticate', 460: value: 'reauth' 461: }); 462: menuOptions.push({ 463: label: 'Clear authentication', 464: value: 'clear-auth' 465: }); 466: } 467: if (!isEffectivelyAuthenticated) { 468: menuOptions.push({ 469: label: 'Authenticate', 470: value: 'auth' 471: }); 472: } 473: } 474: if (server.client.type !== 'disabled') { 475: if (server.client.type !== 'needs-auth') { 476: menuOptions.push({ 477: label: 'Reconnect', 478: value: 'reconnectMcpServer' 479: }); 480: } 481: menuOptions.push({ 482: label: 'Disable', 483: value: 'toggle-enabled' 484: }); 485: } 486: if (menuOptions.length === 0) { 487: menuOptions.push({ 488: label: 'Back', 489: value: 'back' 490: }); 491: } 492: return <Box flexDirection="column"> 493: <Box flexDirection="column" paddingX={1} borderStyle={borderless ? undefined : 'round'}> 494: <Box marginBottom={1}> 495: <Text bold>{capitalizedServerName} MCP Server</Text> 496: </Box> 497: <Box flexDirection="column" gap={0}> 498: <Box> 499: <Text bold>Status: </Text> 500: {server.client.type === 'disabled' ? <Text>{color('inactive', theme)(figures.radioOff)} disabled</Text> : server.client.type === 'connected' ? <Text>{color('success', theme)(figures.tick)} connected</Text> : server.client.type === 'pending' ? <> 501: <Text dimColor>{figures.radioOff}</Text> 502: <Text> connecting…</Text> 503: </> : server.client.type === 'needs-auth' ? <Text> 504: {color('warning', theme)(figures.triangleUpOutline)} needs 505: authentication 506: </Text> : <Text>{color('error', theme)(figures.cross)} failed</Text>} 507: </Box> 508: {server.transport !== 'claudeai-proxy' && <Box> 509: <Text bold>Auth: </Text> 510: {isEffectivelyAuthenticated ? <Text> 511: {color('success', theme)(figures.tick)} authenticated 512: </Text> : <Text> 513: {color('error', theme)(figures.cross)} not authenticated 514: </Text>} 515: </Box>} 516: <Box> 517: <Text bold>URL: </Text> 518: <Text dimColor>{server.config.url}</Text> 519: </Box> 520: <Box> 521: <Text bold>Config location: </Text> 522: <Text dimColor>{describeMcpConfigFilePath(server.scope)}</Text> 523: </Box> 524: {server.client.type === 'connected' && <CapabilitiesSection serverToolsCount={serverToolsCount} serverPromptsCount={serverCommandsCount} serverResourcesCount={mcp.resources[server.name]?.length || 0} />} 525: {server.client.type === 'connected' && serverToolsCount > 0 && <Box> 526: <Text bold>Tools: </Text> 527: <Text dimColor>{serverToolsCount} tools</Text> 528: </Box>} 529: </Box> 530: {error && <Box marginTop={1}> 531: <Text color="error">Error: {error}</Text> 532: </Box>} 533: {menuOptions.length > 0 && <Box marginTop={1}> 534: <Select options={menuOptions} onChange={async value_0 => { 535: switch (value_0) { 536: case 'tools': 537: onViewTools(); 538: break; 539: case 'auth': 540: case 'reauth': 541: await handleAuthenticate(); 542: break; 543: case 'clear-auth': 544: await handleClearAuth(); 545: break; 546: case 'claudeai-auth': 547: await handleClaudeAIAuth(); 548: break; 549: case 'claudeai-clear-auth': 550: handleClaudeAIClearAuth(); 551: break; 552: case 'reconnectMcpServer': 553: setIsReconnecting(true); 554: try { 555: const result_1 = await reconnectMcpServer(server.name); 556: if (server.config.type === 'claudeai-proxy') { 557: logEvent('tengu_claudeai_mcp_reconnect', { 558: success: result_1.client.type === 'connected' 559: }); 560: } 561: const { 562: message: message_0 563: } = handleReconnectResult(result_1, server.name); 564: onComplete?.(message_0); 565: } catch (err_2) { 566: if (server.config.type === 'claudeai-proxy') { 567: logEvent('tengu_claudeai_mcp_reconnect', { 568: success: false 569: }); 570: } 571: onComplete?.(handleReconnectError(err_2, server.name)); 572: } finally { 573: setIsReconnecting(false); 574: } 575: break; 576: case 'toggle-enabled': 577: await handleToggleEnabled(); 578: break; 579: case 'back': 580: onCancel(); 581: break; 582: } 583: }} onCancel={onCancel} /> 584: </Box>} 585: </Box> 586: <Box marginTop={1}> 587: <Text dimColor italic> 588: {exitState.pending ? <>Press {exitState.keyName} again to exit</> : <Byline> 589: <KeyboardShortcutHint shortcut="↑↓" action="navigate" /> 590: <KeyboardShortcutHint shortcut="Enter" action="select" /> 591: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" /> 592: </Byline>} 593: </Text> 594: </Box> 595: </Box>; 596: }

File: src/components/mcp/MCPSettings.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useEffect, useMemo } from 'react'; 3: import type { CommandResultDisplay } from '../../commands.js'; 4: import { ClaudeAuthProvider } from '../../services/mcp/auth.js'; 5: import type { McpClaudeAIProxyServerConfig, McpHTTPServerConfig, McpSSEServerConfig, McpStdioServerConfig } from '../../services/mcp/types.js'; 6: import { extractAgentMcpServers, filterToolsByServer } from '../../services/mcp/utils.js'; 7: import { useAppState } from '../../state/AppState.js'; 8: import { getSessionIngressAuthToken } from '../../utils/sessionIngressAuth.js'; 9: import { MCPAgentServerMenu } from './MCPAgentServerMenu.js'; 10: import { MCPListPanel } from './MCPListPanel.js'; 11: import { MCPRemoteServerMenu } from './MCPRemoteServerMenu.js'; 12: import { MCPStdioServerMenu } from './MCPStdioServerMenu.js'; 13: import { MCPToolDetailView } from './MCPToolDetailView.js'; 14: import { MCPToolListView } from './MCPToolListView.js'; 15: import type { AgentMcpServerInfo, MCPViewState, ServerInfo } from './types.js'; 16: type Props = { 17: onComplete: (result?: string, options?: { 18: display?: CommandResultDisplay; 19: }) => void; 20: }; 21: export function MCPSettings(t0) { 22: const $ = _c(66); 23: const { 24: onComplete 25: } = t0; 26: const mcp = useAppState(_temp); 27: const agentDefinitions = useAppState(_temp2); 28: const mcpClients = mcp.clients; 29: let t1; 30: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 31: t1 = { 32: type: "list" 33: }; 34: $[0] = t1; 35: } else { 36: t1 = $[0]; 37: } 38: const [viewState, setViewState] = React.useState(t1); 39: let t2; 40: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 41: t2 = []; 42: $[1] = t2; 43: } else { 44: t2 = $[1]; 45: } 46: const [servers, setServers] = React.useState(t2); 47: let t3; 48: if ($[2] !== agentDefinitions.allAgents) { 49: t3 = extractAgentMcpServers(agentDefinitions.allAgents); 50: $[2] = agentDefinitions.allAgents; 51: $[3] = t3; 52: } else { 53: t3 = $[3]; 54: } 55: const agentMcpServers = t3; 56: let t4; 57: if ($[4] !== mcpClients) { 58: t4 = mcpClients.filter(_temp3).sort(_temp4); 59: $[4] = mcpClients; 60: $[5] = t4; 61: } else { 62: t4 = $[5]; 63: } 64: const filteredClients = t4; 65: let t5; 66: let t6; 67: if ($[6] !== filteredClients || $[7] !== mcp.tools) { 68: t5 = () => { 69: let cancelled = false; 70: const prepareServers = async function prepareServers() { 71: const serverInfos = await Promise.all(filteredClients.map(async client_0 => { 72: const scope = client_0.config.scope; 73: const isSSE = client_0.config.type === "sse"; 74: const isHTTP = client_0.config.type === "http"; 75: const isClaudeAIProxy = client_0.config.type === "claudeai-proxy"; 76: let isAuthenticated = undefined; 77: if (isSSE || isHTTP) { 78: const authProvider = new ClaudeAuthProvider(client_0.name, client_0.config as McpSSEServerConfig | McpHTTPServerConfig); 79: const tokens = await authProvider.tokens(); 80: const hasSessionAuth = getSessionIngressAuthToken() !== null && client_0.type === "connected"; 81: const hasToolsAndConnected = client_0.type === "connected" && filterToolsByServer(mcp.tools, client_0.name).length > 0; 82: isAuthenticated = Boolean(tokens) || hasSessionAuth || hasToolsAndConnected; 83: } 84: const baseInfo = { 85: name: client_0.name, 86: client: client_0, 87: scope 88: }; 89: if (isClaudeAIProxy) { 90: return { 91: ...baseInfo, 92: transport: "claudeai-proxy" as const, 93: isAuthenticated: false, 94: config: client_0.config as McpClaudeAIProxyServerConfig 95: }; 96: } else { 97: if (isSSE) { 98: return { 99: ...baseInfo, 100: transport: "sse" as const, 101: isAuthenticated, 102: config: client_0.config as McpSSEServerConfig 103: }; 104: } else { 105: if (isHTTP) { 106: return { 107: ...baseInfo, 108: transport: "http" as const, 109: isAuthenticated, 110: config: client_0.config as McpHTTPServerConfig 111: }; 112: } else { 113: return { 114: ...baseInfo, 115: transport: "stdio" as const, 116: config: client_0.config as McpStdioServerConfig 117: }; 118: } 119: } 120: } 121: })); 122: if (cancelled) { 123: return; 124: } 125: setServers(serverInfos); 126: }; 127: prepareServers(); 128: return () => { 129: cancelled = true; 130: }; 131: }; 132: t6 = [filteredClients, mcp.tools]; 133: $[6] = filteredClients; 134: $[7] = mcp.tools; 135: $[8] = t5; 136: $[9] = t6; 137: } else { 138: t5 = $[8]; 139: t6 = $[9]; 140: } 141: React.useEffect(t5, t6); 142: let t7; 143: let t8; 144: if ($[10] !== agentMcpServers.length || $[11] !== filteredClients.length || $[12] !== onComplete || $[13] !== servers.length) { 145: t7 = () => { 146: if (servers.length === 0 && filteredClients.length > 0) { 147: return; 148: } 149: if (servers.length === 0 && agentMcpServers.length === 0) { 150: onComplete("No MCP servers configured. Please run /doctor if this is unexpected. Otherwise, run `claude mcp --help` or visit https://code.claude.com/docs/en/mcp to learn more."); 151: } 152: }; 153: t8 = [servers.length, filteredClients.length, agentMcpServers.length, onComplete]; 154: $[10] = agentMcpServers.length; 155: $[11] = filteredClients.length; 156: $[12] = onComplete; 157: $[13] = servers.length; 158: $[14] = t7; 159: $[15] = t8; 160: } else { 161: t7 = $[14]; 162: t8 = $[15]; 163: } 164: useEffect(t7, t8); 165: switch (viewState.type) { 166: case "list": 167: { 168: let t10; 169: let t9; 170: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 171: t9 = server => setViewState({ 172: type: "server-menu", 173: server 174: }); 175: t10 = agentServer => setViewState({ 176: type: "agent-server-menu", 177: agentServer 178: }); 179: $[16] = t10; 180: $[17] = t9; 181: } else { 182: t10 = $[16]; 183: t9 = $[17]; 184: } 185: let t11; 186: if ($[18] !== agentMcpServers || $[19] !== onComplete || $[20] !== servers || $[21] !== viewState.defaultTab) { 187: t11 = <MCPListPanel servers={servers} agentServers={agentMcpServers} onSelectServer={t9} onSelectAgentServer={t10} onComplete={onComplete} defaultTab={viewState.defaultTab} />; 188: $[18] = agentMcpServers; 189: $[19] = onComplete; 190: $[20] = servers; 191: $[21] = viewState.defaultTab; 192: $[22] = t11; 193: } else { 194: t11 = $[22]; 195: } 196: return t11; 197: } 198: case "server-menu": 199: { 200: let t9; 201: if ($[23] !== mcp.tools || $[24] !== viewState.server.name) { 202: t9 = filterToolsByServer(mcp.tools, viewState.server.name); 203: $[23] = mcp.tools; 204: $[24] = viewState.server.name; 205: $[25] = t9; 206: } else { 207: t9 = $[25]; 208: } 209: const serverTools_0 = t9; 210: const defaultTab = viewState.server.transport === "claudeai-proxy" ? "claude.ai" : "Claude Code"; 211: if (viewState.server.transport === "stdio") { 212: let t10; 213: if ($[26] !== viewState.server) { 214: t10 = () => setViewState({ 215: type: "server-tools", 216: server: viewState.server 217: }); 218: $[26] = viewState.server; 219: $[27] = t10; 220: } else { 221: t10 = $[27]; 222: } 223: let t11; 224: if ($[28] !== defaultTab) { 225: t11 = () => setViewState({ 226: type: "list", 227: defaultTab 228: }); 229: $[28] = defaultTab; 230: $[29] = t11; 231: } else { 232: t11 = $[29]; 233: } 234: let t12; 235: if ($[30] !== onComplete || $[31] !== serverTools_0.length || $[32] !== t10 || $[33] !== t11 || $[34] !== viewState.server) { 236: t12 = <MCPStdioServerMenu server={viewState.server} serverToolsCount={serverTools_0.length} onViewTools={t10} onCancel={t11} onComplete={onComplete} />; 237: $[30] = onComplete; 238: $[31] = serverTools_0.length; 239: $[32] = t10; 240: $[33] = t11; 241: $[34] = viewState.server; 242: $[35] = t12; 243: } else { 244: t12 = $[35]; 245: } 246: return t12; 247: } else { 248: let t10; 249: if ($[36] !== viewState.server) { 250: t10 = () => setViewState({ 251: type: "server-tools", 252: server: viewState.server 253: }); 254: $[36] = viewState.server; 255: $[37] = t10; 256: } else { 257: t10 = $[37]; 258: } 259: let t11; 260: if ($[38] !== defaultTab) { 261: t11 = () => setViewState({ 262: type: "list", 263: defaultTab 264: }); 265: $[38] = defaultTab; 266: $[39] = t11; 267: } else { 268: t11 = $[39]; 269: } 270: let t12; 271: if ($[40] !== onComplete || $[41] !== serverTools_0.length || $[42] !== t10 || $[43] !== t11 || $[44] !== viewState.server) { 272: t12 = <MCPRemoteServerMenu server={viewState.server} serverToolsCount={serverTools_0.length} onViewTools={t10} onCancel={t11} onComplete={onComplete} />; 273: $[40] = onComplete; 274: $[41] = serverTools_0.length; 275: $[42] = t10; 276: $[43] = t11; 277: $[44] = viewState.server; 278: $[45] = t12; 279: } else { 280: t12 = $[45]; 281: } 282: return t12; 283: } 284: } 285: case "server-tools": 286: { 287: let t10; 288: let t9; 289: if ($[46] !== viewState.server) { 290: t9 = (_, index) => setViewState({ 291: type: "server-tool-detail", 292: server: viewState.server, 293: toolIndex: index 294: }); 295: t10 = () => setViewState({ 296: type: "server-menu", 297: server: viewState.server 298: }); 299: $[46] = viewState.server; 300: $[47] = t10; 301: $[48] = t9; 302: } else { 303: t10 = $[47]; 304: t9 = $[48]; 305: } 306: let t11; 307: if ($[49] !== t10 || $[50] !== t9 || $[51] !== viewState.server) { 308: t11 = <MCPToolListView server={viewState.server} onSelectTool={t9} onBack={t10} />; 309: $[49] = t10; 310: $[50] = t9; 311: $[51] = viewState.server; 312: $[52] = t11; 313: } else { 314: t11 = $[52]; 315: } 316: return t11; 317: } 318: case "server-tool-detail": 319: { 320: let t9; 321: if ($[53] !== mcp.tools || $[54] !== viewState.server.name) { 322: t9 = filterToolsByServer(mcp.tools, viewState.server.name); 323: $[53] = mcp.tools; 324: $[54] = viewState.server.name; 325: $[55] = t9; 326: } else { 327: t9 = $[55]; 328: } 329: const serverTools = t9; 330: const tool = serverTools[viewState.toolIndex]; 331: if (!tool) { 332: setViewState({ 333: type: "server-tools", 334: server: viewState.server 335: }); 336: return null; 337: } 338: let t10; 339: if ($[56] !== viewState.server) { 340: t10 = () => setViewState({ 341: type: "server-tools", 342: server: viewState.server 343: }); 344: $[56] = viewState.server; 345: $[57] = t10; 346: } else { 347: t10 = $[57]; 348: } 349: let t11; 350: if ($[58] !== t10 || $[59] !== tool || $[60] !== viewState.server) { 351: t11 = <MCPToolDetailView tool={tool} server={viewState.server} onBack={t10} />; 352: $[58] = t10; 353: $[59] = tool; 354: $[60] = viewState.server; 355: $[61] = t11; 356: } else { 357: t11 = $[61]; 358: } 359: return t11; 360: } 361: case "agent-server-menu": 362: { 363: let t9; 364: if ($[62] === Symbol.for("react.memo_cache_sentinel")) { 365: t9 = () => setViewState({ 366: type: "list", 367: defaultTab: "Agents" 368: }); 369: $[62] = t9; 370: } else { 371: t9 = $[62]; 372: } 373: let t10; 374: if ($[63] !== onComplete || $[64] !== viewState.agentServer) { 375: t10 = <MCPAgentServerMenu agentServer={viewState.agentServer} onCancel={t9} onComplete={onComplete} />; 376: $[63] = onComplete; 377: $[64] = viewState.agentServer; 378: $[65] = t10; 379: } else { 380: t10 = $[65]; 381: } 382: return t10; 383: } 384: } 385: } 386: function _temp4(a, b) { 387: return a.name.localeCompare(b.name); 388: } 389: function _temp3(client) { 390: return client.name !== "ide"; 391: } 392: function _temp2(s_0) { 393: return s_0.agentDefinitions; 394: } 395: function _temp(s) { 396: return s.mcp; 397: }

File: src/components/mcp/MCPStdioServerMenu.tsx

typescript 1: import figures from 'figures'; 2: import React, { useState } from 'react'; 3: import type { CommandResultDisplay } from '../../commands.js'; 4: import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 5: import { Box, color, Text, useTheme } from '../../ink.js'; 6: import { getMcpConfigByName } from '../../services/mcp/config.js'; 7: import { useMcpReconnect, useMcpToggleEnabled } from '../../services/mcp/MCPConnectionManager.js'; 8: import { describeMcpConfigFilePath, filterMcpPromptsByServer } from '../../services/mcp/utils.js'; 9: import { useAppState } from '../../state/AppState.js'; 10: import { errorMessage } from '../../utils/errors.js'; 11: import { capitalize } from '../../utils/stringUtils.js'; 12: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 13: import { Select } from '../CustomSelect/index.js'; 14: import { Byline } from '../design-system/Byline.js'; 15: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 16: import { Spinner } from '../Spinner.js'; 17: import { CapabilitiesSection } from './CapabilitiesSection.js'; 18: import type { StdioServerInfo } from './types.js'; 19: import { handleReconnectError, handleReconnectResult } from './utils/reconnectHelpers.js'; 20: type Props = { 21: server: StdioServerInfo; 22: serverToolsCount: number; 23: onViewTools: () => void; 24: onCancel: () => void; 25: onComplete: (result?: string, options?: { 26: display?: CommandResultDisplay; 27: }) => void; 28: borderless?: boolean; 29: }; 30: export function MCPStdioServerMenu({ 31: server, 32: serverToolsCount, 33: onViewTools, 34: onCancel, 35: onComplete, 36: borderless = false 37: }: Props): React.ReactNode { 38: const [theme] = useTheme(); 39: const exitState = useExitOnCtrlCDWithKeybindings(); 40: const mcp = useAppState(s => s.mcp); 41: const reconnectMcpServer = useMcpReconnect(); 42: const toggleMcpServer = useMcpToggleEnabled(); 43: const [isReconnecting, setIsReconnecting] = useState(false); 44: const handleToggleEnabled = React.useCallback(async () => { 45: const wasEnabled = server.client.type !== 'disabled'; 46: try { 47: await toggleMcpServer(server.name); 48: onCancel(); 49: } catch (err) { 50: const action = wasEnabled ? 'disable' : 'enable'; 51: onComplete(`Failed to ${action} MCP server '${server.name}': ${errorMessage(err)}`); 52: } 53: }, [server.client.type, server.name, toggleMcpServer, onCancel, onComplete]); 54: const capitalizedServerName = capitalize(String(server.name)); 55: const serverCommandsCount = filterMcpPromptsByServer(mcp.commands, server.name).length; 56: const menuOptions = []; 57: if (server.client.type !== 'disabled' && serverToolsCount > 0) { 58: menuOptions.push({ 59: label: 'View tools', 60: value: 'tools' 61: }); 62: } 63: if (server.client.type !== 'disabled') { 64: menuOptions.push({ 65: label: 'Reconnect', 66: value: 'reconnectMcpServer' 67: }); 68: } 69: menuOptions.push({ 70: label: server.client.type !== 'disabled' ? 'Disable' : 'Enable', 71: value: 'toggle-enabled' 72: }); 73: if (menuOptions.length === 0) { 74: menuOptions.push({ 75: label: 'Back', 76: value: 'back' 77: }); 78: } 79: if (isReconnecting) { 80: return <Box flexDirection="column" gap={1} padding={1}> 81: <Text color="text"> 82: Reconnecting to <Text bold>{server.name}</Text> 83: </Text> 84: <Box> 85: <Spinner /> 86: <Text> Restarting MCP server process</Text> 87: </Box> 88: <Text dimColor>This may take a few moments.</Text> 89: </Box>; 90: } 91: return <Box flexDirection="column"> 92: <Box flexDirection="column" paddingX={1} borderStyle={borderless ? undefined : 'round'}> 93: <Box marginBottom={1}> 94: <Text bold>{capitalizedServerName} MCP Server</Text> 95: </Box> 96: <Box flexDirection="column" gap={0}> 97: <Box> 98: <Text bold>Status: </Text> 99: {server.client.type === 'disabled' ? <Text>{color('inactive', theme)(figures.radioOff)} disabled</Text> : server.client.type === 'connected' ? <Text>{color('success', theme)(figures.tick)} connected</Text> : server.client.type === 'pending' ? <> 100: <Text dimColor>{figures.radioOff}</Text> 101: <Text> connecting…</Text> 102: </> : <Text>{color('error', theme)(figures.cross)} failed</Text>} 103: </Box> 104: <Box> 105: <Text bold>Command: </Text> 106: <Text dimColor>{server.config.command}</Text> 107: </Box> 108: {server.config.args && server.config.args.length > 0 && <Box> 109: <Text bold>Args: </Text> 110: <Text dimColor>{server.config.args.join(' ')}</Text> 111: </Box>} 112: <Box> 113: <Text bold>Config location: </Text> 114: <Text dimColor> 115: {describeMcpConfigFilePath(getMcpConfigByName(server.name)?.scope ?? 'dynamic')} 116: </Text> 117: </Box> 118: {server.client.type === 'connected' && <CapabilitiesSection serverToolsCount={serverToolsCount} serverPromptsCount={serverCommandsCount} serverResourcesCount={mcp.resources[server.name]?.length || 0} />} 119: {server.client.type === 'connected' && serverToolsCount > 0 && <Box> 120: <Text bold>Tools: </Text> 121: <Text dimColor>{serverToolsCount} tools</Text> 122: </Box>} 123: </Box> 124: {menuOptions.length > 0 && <Box marginTop={1}> 125: <Select options={menuOptions} onChange={async value => { 126: if (value === 'tools') { 127: onViewTools(); 128: } else if (value === 'reconnectMcpServer') { 129: setIsReconnecting(true); 130: try { 131: const result = await reconnectMcpServer(server.name); 132: const { 133: message 134: } = handleReconnectResult(result, server.name); 135: onComplete?.(message); 136: } catch (err_0) { 137: onComplete?.(handleReconnectError(err_0, server.name)); 138: } finally { 139: setIsReconnecting(false); 140: } 141: } else if (value === 'toggle-enabled') { 142: await handleToggleEnabled(); 143: } else if (value === 'back') { 144: onCancel(); 145: } 146: }} onCancel={onCancel} /> 147: </Box>} 148: </Box> 149: <Box marginTop={1}> 150: <Text dimColor italic> 151: {exitState.pending ? <>Press {exitState.keyName} again to exit</> : <Byline> 152: <KeyboardShortcutHint shortcut="↑↓" action="navigate" /> 153: <KeyboardShortcutHint shortcut="Enter" action="select" /> 154: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" /> 155: </Byline>} 156: </Text> 157: </Box> 158: </Box>; 159: }

File: src/components/mcp/MCPToolDetailView.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { extractMcpToolDisplayName, getMcpDisplayName } from '../../services/mcp/mcpStringUtils.js'; 5: import type { Tool } from '../../Tool.js'; 6: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 7: import { Dialog } from '../design-system/Dialog.js'; 8: import type { ServerInfo } from './types.js'; 9: type Props = { 10: tool: Tool; 11: server: ServerInfo; 12: onBack: () => void; 13: }; 14: export function MCPToolDetailView(t0) { 15: const $ = _c(44); 16: const { 17: tool, 18: server, 19: onBack 20: } = t0; 21: const [toolDescription, setToolDescription] = React.useState(""); 22: let t1; 23: let toolName; 24: if ($[0] !== server.name || $[1] !== tool) { 25: toolName = getMcpDisplayName(tool.name, server.name); 26: const fullDisplayName = tool.userFacingName ? tool.userFacingName({}) : toolName; 27: t1 = extractMcpToolDisplayName(fullDisplayName); 28: $[0] = server.name; 29: $[1] = tool; 30: $[2] = t1; 31: $[3] = toolName; 32: } else { 33: t1 = $[2]; 34: toolName = $[3]; 35: } 36: const displayName = t1; 37: let t2; 38: if ($[4] !== tool) { 39: t2 = tool.isReadOnly?.({}) ?? false; 40: $[4] = tool; 41: $[5] = t2; 42: } else { 43: t2 = $[5]; 44: } 45: const isReadOnly = t2; 46: let t3; 47: if ($[6] !== tool) { 48: t3 = tool.isDestructive?.({}) ?? false; 49: $[6] = tool; 50: $[7] = t3; 51: } else { 52: t3 = $[7]; 53: } 54: const isDestructive = t3; 55: let t4; 56: if ($[8] !== tool) { 57: t4 = tool.isOpenWorld?.({}) ?? false; 58: $[8] = tool; 59: $[9] = t4; 60: } else { 61: t4 = $[9]; 62: } 63: const isOpenWorld = t4; 64: let t5; 65: let t6; 66: if ($[10] !== tool) { 67: t5 = () => { 68: const loadDescription = async function loadDescription() { 69: try { 70: const desc = await tool.description({}, { 71: isNonInteractiveSession: false, 72: toolPermissionContext: { 73: mode: "default" as const, 74: additionalWorkingDirectories: new Map(), 75: alwaysAllowRules: {}, 76: alwaysDenyRules: {}, 77: alwaysAskRules: {}, 78: isBypassPermissionsModeAvailable: false 79: }, 80: tools: [] 81: }); 82: setToolDescription(desc); 83: } catch { 84: setToolDescription("Failed to load description"); 85: } 86: }; 87: loadDescription(); 88: }; 89: t6 = [tool]; 90: $[10] = tool; 91: $[11] = t5; 92: $[12] = t6; 93: } else { 94: t5 = $[11]; 95: t6 = $[12]; 96: } 97: React.useEffect(t5, t6); 98: let t7; 99: if ($[13] !== isReadOnly) { 100: t7 = isReadOnly && <Text color="success"> [read-only]</Text>; 101: $[13] = isReadOnly; 102: $[14] = t7; 103: } else { 104: t7 = $[14]; 105: } 106: let t8; 107: if ($[15] !== isDestructive) { 108: t8 = isDestructive && <Text color="error"> [destructive]</Text>; 109: $[15] = isDestructive; 110: $[16] = t8; 111: } else { 112: t8 = $[16]; 113: } 114: let t9; 115: if ($[17] !== isOpenWorld) { 116: t9 = isOpenWorld && <Text dimColor={true}> [open-world]</Text>; 117: $[17] = isOpenWorld; 118: $[18] = t9; 119: } else { 120: t9 = $[18]; 121: } 122: let t10; 123: if ($[19] !== displayName || $[20] !== t7 || $[21] !== t8 || $[22] !== t9) { 124: t10 = <>{displayName}{t7}{t8}{t9}</>; 125: $[19] = displayName; 126: $[20] = t7; 127: $[21] = t8; 128: $[22] = t9; 129: $[23] = t10; 130: } else { 131: t10 = $[23]; 132: } 133: const titleContent = t10; 134: let t11; 135: if ($[24] === Symbol.for("react.memo_cache_sentinel")) { 136: t11 = <Text bold={true}>Tool name: </Text>; 137: $[24] = t11; 138: } else { 139: t11 = $[24]; 140: } 141: let t12; 142: if ($[25] !== toolName) { 143: t12 = <Box>{t11}<Text dimColor={true}>{toolName}</Text></Box>; 144: $[25] = toolName; 145: $[26] = t12; 146: } else { 147: t12 = $[26]; 148: } 149: let t13; 150: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 151: t13 = <Text bold={true}>Full name: </Text>; 152: $[27] = t13; 153: } else { 154: t13 = $[27]; 155: } 156: let t14; 157: if ($[28] !== tool.name) { 158: t14 = <Box>{t13}<Text dimColor={true}>{tool.name}</Text></Box>; 159: $[28] = tool.name; 160: $[29] = t14; 161: } else { 162: t14 = $[29]; 163: } 164: let t15; 165: if ($[30] !== toolDescription) { 166: t15 = toolDescription && <Box flexDirection="column" marginTop={1}><Text bold={true}>Description:</Text><Text wrap="wrap">{toolDescription}</Text></Box>; 167: $[30] = toolDescription; 168: $[31] = t15; 169: } else { 170: t15 = $[31]; 171: } 172: let t16; 173: if ($[32] !== tool.inputJSONSchema) { 174: t16 = tool.inputJSONSchema && tool.inputJSONSchema.properties && Object.keys(tool.inputJSONSchema.properties).length > 0 && <Box flexDirection="column" marginTop={1}><Text bold={true}>Parameters:</Text><Box marginLeft={2} flexDirection="column">{Object.entries(tool.inputJSONSchema.properties).map(t17 => { 175: const [key, value] = t17; 176: const required = tool.inputJSONSchema?.required as string[] | undefined; 177: const isRequired = required?.includes(key); 178: return <Text key={key}>• {key}{isRequired && <Text dimColor={true}> (required)</Text>}:{" "}<Text dimColor={true}>{typeof value === "object" && value && "type" in value ? String(value.type) : "unknown"}</Text>{typeof value === "object" && value && "description" in value && <Text dimColor={true}> - {String(value.description)}</Text>}</Text>; 179: })}</Box></Box>; 180: $[32] = tool.inputJSONSchema; 181: $[33] = t16; 182: } else { 183: t16 = $[33]; 184: } 185: let t17; 186: if ($[34] !== t12 || $[35] !== t14 || $[36] !== t15 || $[37] !== t16) { 187: t17 = <Box flexDirection="column">{t12}{t14}{t15}{t16}</Box>; 188: $[34] = t12; 189: $[35] = t14; 190: $[36] = t15; 191: $[37] = t16; 192: $[38] = t17; 193: } else { 194: t17 = $[38]; 195: } 196: let t18; 197: if ($[39] !== onBack || $[40] !== server.name || $[41] !== t17 || $[42] !== titleContent) { 198: t18 = <Dialog title={titleContent} subtitle={server.name} onCancel={onBack} inputGuide={_temp}>{t17}</Dialog>; 199: $[39] = onBack; 200: $[40] = server.name; 201: $[41] = t17; 202: $[42] = titleContent; 203: $[43] = t18; 204: } else { 205: t18 = $[43]; 206: } 207: return t18; 208: } 209: function _temp(exitState) { 210: return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />; 211: }

File: src/components/mcp/MCPToolListView.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Text } from '../../ink.js'; 4: import { extractMcpToolDisplayName, getMcpDisplayName } from '../../services/mcp/mcpStringUtils.js'; 5: import { filterToolsByServer } from '../../services/mcp/utils.js'; 6: import { useAppState } from '../../state/AppState.js'; 7: import type { Tool } from '../../Tool.js'; 8: import { plural } from '../../utils/stringUtils.js'; 9: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 10: import { Select } from '../CustomSelect/index.js'; 11: import { Byline } from '../design-system/Byline.js'; 12: import { Dialog } from '../design-system/Dialog.js'; 13: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 14: import type { ServerInfo } from './types.js'; 15: type Props = { 16: server: ServerInfo; 17: onSelectTool: (tool: Tool, index: number) => void; 18: onBack: () => void; 19: }; 20: export function MCPToolListView(t0) { 21: const $ = _c(21); 22: const { 23: server, 24: onSelectTool, 25: onBack 26: } = t0; 27: const mcpTools = useAppState(_temp); 28: let t1; 29: bb0: { 30: if (server.client.type !== "connected") { 31: let t2; 32: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 33: t2 = []; 34: $[0] = t2; 35: } else { 36: t2 = $[0]; 37: } 38: t1 = t2; 39: break bb0; 40: } 41: let t2; 42: if ($[1] !== mcpTools || $[2] !== server.name) { 43: t2 = filterToolsByServer(mcpTools, server.name); 44: $[1] = mcpTools; 45: $[2] = server.name; 46: $[3] = t2; 47: } else { 48: t2 = $[3]; 49: } 50: t1 = t2; 51: } 52: const serverTools = t1; 53: let t2; 54: if ($[4] !== server.name || $[5] !== serverTools) { 55: let t3; 56: if ($[7] !== server.name) { 57: t3 = (tool, index) => { 58: const toolName = getMcpDisplayName(tool.name, server.name); 59: const fullDisplayName = tool.userFacingName ? tool.userFacingName({}) : toolName; 60: const displayName = extractMcpToolDisplayName(fullDisplayName); 61: const isReadOnly = tool.isReadOnly?.({}) ?? false; 62: const isDestructive = tool.isDestructive?.({}) ?? false; 63: const isOpenWorld = tool.isOpenWorld?.({}) ?? false; 64: const annotations = []; 65: if (isReadOnly) { 66: annotations.push("read-only"); 67: } 68: if (isDestructive) { 69: annotations.push("destructive"); 70: } 71: if (isOpenWorld) { 72: annotations.push("open-world"); 73: } 74: return { 75: label: displayName, 76: value: index.toString(), 77: description: annotations.length > 0 ? annotations.join(", ") : undefined, 78: descriptionColor: isDestructive ? "error" : isReadOnly ? "success" : undefined 79: }; 80: }; 81: $[7] = server.name; 82: $[8] = t3; 83: } else { 84: t3 = $[8]; 85: } 86: t2 = serverTools.map(t3); 87: $[4] = server.name; 88: $[5] = serverTools; 89: $[6] = t2; 90: } else { 91: t2 = $[6]; 92: } 93: const toolOptions = t2; 94: const t3 = `Tools for ${server.name}`; 95: const t4 = serverTools.length; 96: let t5; 97: if ($[9] !== serverTools.length) { 98: t5 = plural(serverTools.length, "tool"); 99: $[9] = serverTools.length; 100: $[10] = t5; 101: } else { 102: t5 = $[10]; 103: } 104: const t6 = `${t4} ${t5}`; 105: let t7; 106: if ($[11] !== onBack || $[12] !== onSelectTool || $[13] !== serverTools || $[14] !== toolOptions) { 107: t7 = serverTools.length === 0 ? <Text dimColor={true}>No tools available</Text> : <Select options={toolOptions} onChange={value => { 108: const index_0 = parseInt(value); 109: const tool_0 = serverTools[index_0]; 110: if (tool_0) { 111: onSelectTool(tool_0, index_0); 112: } 113: }} onCancel={onBack} />; 114: $[11] = onBack; 115: $[12] = onSelectTool; 116: $[13] = serverTools; 117: $[14] = toolOptions; 118: $[15] = t7; 119: } else { 120: t7 = $[15]; 121: } 122: let t8; 123: if ($[16] !== onBack || $[17] !== t3 || $[18] !== t6 || $[19] !== t7) { 124: t8 = <Dialog title={t3} subtitle={t6} onCancel={onBack} inputGuide={_temp2}>{t7}</Dialog>; 125: $[16] = onBack; 126: $[17] = t3; 127: $[18] = t6; 128: $[19] = t7; 129: $[20] = t8; 130: } else { 131: t8 = $[20]; 132: } 133: return t8; 134: } 135: function _temp2(exitState) { 136: return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" /></Byline>; 137: } 138: function _temp(s) { 139: return s.mcp.tools; 140: }

File: src/components/memory/MemoryFileSelector.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import chalk from 'chalk'; 4: import { mkdir } from 'fs/promises'; 5: import { join } from 'path'; 6: import * as React from 'react'; 7: import { use, useEffect, useState } from 'react'; 8: import { getOriginalCwd } from '../../bootstrap/state.js'; 9: import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 10: import { Box, Text } from '../../ink.js'; 11: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 12: import { getAutoMemPath, isAutoMemoryEnabled } from '../../memdir/paths.js'; 13: import { logEvent } from '../../services/analytics/index.js'; 14: import { isAutoDreamEnabled } from '../../services/autoDream/config.js'; 15: import { readLastConsolidatedAt } from '../../services/autoDream/consolidationLock.js'; 16: import { useAppState } from '../../state/AppState.js'; 17: import { getAgentMemoryDir } from '../../tools/AgentTool/agentMemory.js'; 18: import { openPath } from '../../utils/browser.js'; 19: import { getMemoryFiles, type MemoryFileInfo } from '../../utils/claudemd.js'; 20: import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'; 21: import { getDisplayPath } from '../../utils/file.js'; 22: import { formatRelativeTimeAgo } from '../../utils/format.js'; 23: import { projectIsInGitRepo } from '../../utils/memory/versions.js'; 24: import { updateSettingsForSource } from '../../utils/settings/settings.js'; 25: import { Select } from '../CustomSelect/index.js'; 26: import { ListItem } from '../design-system/ListItem.js'; 27: const teamMemPaths = feature('TEAMMEM') ? require('../../memdir/teamMemPaths.js') as typeof import('../../memdir/teamMemPaths.js') : null; 28: interface ExtendedMemoryFileInfo extends MemoryFileInfo { 29: isNested?: boolean; 30: exists: boolean; 31: } 32: let lastSelectedPath: string | undefined; 33: const OPEN_FOLDER_PREFIX = '__open_folder__'; 34: type Props = { 35: onSelect: (path: string) => void; 36: onCancel: () => void; 37: }; 38: export function MemoryFileSelector(t0) { 39: const $ = _c(58); 40: const { 41: onSelect, 42: onCancel 43: } = t0; 44: const existingMemoryFiles = use(getMemoryFiles()); 45: const userMemoryPath = join(getClaudeConfigHomeDir(), "CLAUDE.md"); 46: const projectMemoryPath = join(getOriginalCwd(), "CLAUDE.md"); 47: const hasUserMemory = existingMemoryFiles.some(f => f.path === userMemoryPath); 48: const hasProjectMemory = existingMemoryFiles.some(f_0 => f_0.path === projectMemoryPath); 49: const allMemoryFiles = [...existingMemoryFiles.filter(_temp).map(_temp2), ...(hasUserMemory ? [] : [{ 50: path: userMemoryPath, 51: type: "User" as const, 52: content: "", 53: exists: false 54: }]), ...(hasProjectMemory ? [] : [{ 55: path: projectMemoryPath, 56: type: "Project" as const, 57: content: "", 58: exists: false 59: }])]; 60: const depths = new Map(); 61: const memoryOptions = allMemoryFiles.map(file => { 62: const displayPath = getDisplayPath(file.path); 63: const existsLabel = file.exists ? "" : " (new)"; 64: const depth = file.parent ? (depths.get(file.parent) ?? 0) + 1 : 0; 65: depths.set(file.path, depth); 66: const indent = depth > 0 ? " ".repeat(depth - 1) : ""; 67: let label; 68: if (file.type === "User" && !file.isNested && file.path === userMemoryPath) { 69: label = "User memory"; 70: } else { 71: if (file.type === "Project" && !file.isNested && file.path === projectMemoryPath) { 72: label = "Project memory"; 73: } else { 74: if (depth > 0) { 75: label = `${indent}L ${displayPath}${existsLabel}`; 76: } else { 77: label = `${displayPath}`; 78: } 79: } 80: } 81: let description; 82: const isGit = projectIsInGitRepo(getOriginalCwd()); 83: if (file.type === "User" && !file.isNested) { 84: description = "Saved in ~/.claude/CLAUDE.md"; 85: } else { 86: if (file.type === "Project" && !file.isNested && file.path === projectMemoryPath) { 87: description = `${isGit ? "Checked in at" : "Saved in"} ./CLAUDE.md`; 88: } else { 89: if (file.parent) { 90: description = "@-imported"; 91: } else { 92: if (file.isNested) { 93: description = "dynamically loaded"; 94: } else { 95: description = ""; 96: } 97: } 98: } 99: } 100: return { 101: label, 102: value: file.path, 103: description 104: }; 105: }); 106: const folderOptions = []; 107: const agentDefinitions = useAppState(_temp3); 108: if (isAutoMemoryEnabled()) { 109: let t1; 110: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 111: t1 = { 112: label: "Open auto-memory folder", 113: value: `${OPEN_FOLDER_PREFIX}${getAutoMemPath()}`, 114: description: "" 115: }; 116: $[0] = t1; 117: } else { 118: t1 = $[0]; 119: } 120: folderOptions.push(t1); 121: if (feature("TEAMMEM") && teamMemPaths.isTeamMemoryEnabled()) { 122: let t2; 123: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 124: t2 = { 125: label: "Open team memory folder", 126: value: `${OPEN_FOLDER_PREFIX}${teamMemPaths.getTeamMemPath()}`, 127: description: "" 128: }; 129: $[1] = t2; 130: } else { 131: t2 = $[1]; 132: } 133: folderOptions.push(t2); 134: } 135: for (const agent of agentDefinitions.activeAgents) { 136: if (agent.memory) { 137: const agentDir = getAgentMemoryDir(agent.agentType, agent.memory); 138: folderOptions.push({ 139: label: `Open ${chalk.bold(agent.agentType)} agent memory`, 140: value: `${OPEN_FOLDER_PREFIX}${agentDir}`, 141: description: `${agent.memory} scope` 142: }); 143: } 144: } 145: } 146: memoryOptions.push(...folderOptions); 147: let t1; 148: if ($[2] !== memoryOptions) { 149: t1 = lastSelectedPath && memoryOptions.some(_temp4) ? lastSelectedPath : memoryOptions[0]?.value || ""; 150: $[2] = memoryOptions; 151: $[3] = t1; 152: } else { 153: t1 = $[3]; 154: } 155: const initialPath = t1; 156: const [autoMemoryOn, setAutoMemoryOn] = useState(isAutoMemoryEnabled); 157: const [autoDreamOn, setAutoDreamOn] = useState(isAutoDreamEnabled); 158: const [showDreamRow] = useState(isAutoMemoryEnabled); 159: const isDreamRunning = useAppState(_temp6); 160: const [lastDreamAt, setLastDreamAt] = useState(null); 161: let t2; 162: if ($[4] !== showDreamRow) { 163: t2 = () => { 164: if (!showDreamRow) { 165: return; 166: } 167: readLastConsolidatedAt().then(setLastDreamAt); 168: }; 169: $[4] = showDreamRow; 170: $[5] = t2; 171: } else { 172: t2 = $[5]; 173: } 174: let t3; 175: if ($[6] !== isDreamRunning || $[7] !== showDreamRow) { 176: t3 = [showDreamRow, isDreamRunning]; 177: $[6] = isDreamRunning; 178: $[7] = showDreamRow; 179: $[8] = t3; 180: } else { 181: t3 = $[8]; 182: } 183: useEffect(t2, t3); 184: let t4; 185: if ($[9] !== isDreamRunning || $[10] !== lastDreamAt) { 186: t4 = isDreamRunning ? "running" : lastDreamAt === null ? "" : lastDreamAt === 0 ? "never" : `last ran ${formatRelativeTimeAgo(new Date(lastDreamAt))}`; 187: $[9] = isDreamRunning; 188: $[10] = lastDreamAt; 189: $[11] = t4; 190: } else { 191: t4 = $[11]; 192: } 193: const dreamStatus = t4; 194: const [focusedToggle, setFocusedToggle] = useState(null); 195: const toggleFocused = focusedToggle !== null; 196: const lastToggleIndex = showDreamRow ? 1 : 0; 197: let t5; 198: if ($[12] !== autoMemoryOn) { 199: t5 = function handleToggleAutoMemory() { 200: const newValue = !autoMemoryOn; 201: updateSettingsForSource("userSettings", { 202: autoMemoryEnabled: newValue 203: }); 204: setAutoMemoryOn(newValue); 205: logEvent("tengu_auto_memory_toggled", { 206: enabled: newValue 207: }); 208: }; 209: $[12] = autoMemoryOn; 210: $[13] = t5; 211: } else { 212: t5 = $[13]; 213: } 214: const handleToggleAutoMemory = t5; 215: let t6; 216: if ($[14] !== autoDreamOn) { 217: t6 = function handleToggleAutoDream() { 218: const newValue_0 = !autoDreamOn; 219: updateSettingsForSource("userSettings", { 220: autoDreamEnabled: newValue_0 221: }); 222: setAutoDreamOn(newValue_0); 223: logEvent("tengu_auto_dream_toggled", { 224: enabled: newValue_0 225: }); 226: }; 227: $[14] = autoDreamOn; 228: $[15] = t6; 229: } else { 230: t6 = $[15]; 231: } 232: const handleToggleAutoDream = t6; 233: useExitOnCtrlCDWithKeybindings(); 234: let t7; 235: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 236: t7 = { 237: context: "Confirmation" 238: }; 239: $[16] = t7; 240: } else { 241: t7 = $[16]; 242: } 243: useKeybinding("confirm:no", onCancel, t7); 244: let t8; 245: if ($[17] !== focusedToggle || $[18] !== handleToggleAutoDream || $[19] !== handleToggleAutoMemory) { 246: t8 = () => { 247: if (focusedToggle === 0) { 248: handleToggleAutoMemory(); 249: } else { 250: if (focusedToggle === 1) { 251: handleToggleAutoDream(); 252: } 253: } 254: }; 255: $[17] = focusedToggle; 256: $[18] = handleToggleAutoDream; 257: $[19] = handleToggleAutoMemory; 258: $[20] = t8; 259: } else { 260: t8 = $[20]; 261: } 262: let t9; 263: if ($[21] !== toggleFocused) { 264: t9 = { 265: context: "Confirmation", 266: isActive: toggleFocused 267: }; 268: $[21] = toggleFocused; 269: $[22] = t9; 270: } else { 271: t9 = $[22]; 272: } 273: useKeybinding("confirm:yes", t8, t9); 274: let t10; 275: if ($[23] !== lastToggleIndex) { 276: t10 = () => { 277: setFocusedToggle(prev => prev !== null && prev < lastToggleIndex ? prev + 1 : null); 278: }; 279: $[23] = lastToggleIndex; 280: $[24] = t10; 281: } else { 282: t10 = $[24]; 283: } 284: let t11; 285: if ($[25] !== toggleFocused) { 286: t11 = { 287: context: "Select", 288: isActive: toggleFocused 289: }; 290: $[25] = toggleFocused; 291: $[26] = t11; 292: } else { 293: t11 = $[26]; 294: } 295: useKeybinding("select:next", t10, t11); 296: let t12; 297: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 298: t12 = () => { 299: setFocusedToggle(_temp7); 300: }; 301: $[27] = t12; 302: } else { 303: t12 = $[27]; 304: } 305: let t13; 306: if ($[28] !== toggleFocused) { 307: t13 = { 308: context: "Select", 309: isActive: toggleFocused 310: }; 311: $[28] = toggleFocused; 312: $[29] = t13; 313: } else { 314: t13 = $[29]; 315: } 316: useKeybinding("select:previous", t12, t13); 317: const t14 = focusedToggle === 0; 318: const t15 = autoMemoryOn ? "on" : "off"; 319: let t16; 320: if ($[30] !== t15) { 321: t16 = <Text>Auto-memory: {t15}</Text>; 322: $[30] = t15; 323: $[31] = t16; 324: } else { 325: t16 = $[31]; 326: } 327: let t17; 328: if ($[32] !== t14 || $[33] !== t16) { 329: t17 = <ListItem isFocused={t14}>{t16}</ListItem>; 330: $[32] = t14; 331: $[33] = t16; 332: $[34] = t17; 333: } else { 334: t17 = $[34]; 335: } 336: let t18; 337: if ($[35] !== autoDreamOn || $[36] !== dreamStatus || $[37] !== focusedToggle || $[38] !== isDreamRunning || $[39] !== showDreamRow) { 338: t18 = showDreamRow && <ListItem isFocused={focusedToggle === 1} styled={false}><Text color={focusedToggle === 1 ? "suggestion" : undefined}>Auto-dream: {autoDreamOn ? "on" : "off"}{dreamStatus && <Text dimColor={true}> · {dreamStatus}</Text>}{!isDreamRunning && autoDreamOn && <Text dimColor={true}> · /dream to run</Text>}</Text></ListItem>; 339: $[35] = autoDreamOn; 340: $[36] = dreamStatus; 341: $[37] = focusedToggle; 342: $[38] = isDreamRunning; 343: $[39] = showDreamRow; 344: $[40] = t18; 345: } else { 346: t18 = $[40]; 347: } 348: let t19; 349: if ($[41] !== t17 || $[42] !== t18) { 350: t19 = <Box flexDirection="column" marginBottom={1}>{t17}{t18}</Box>; 351: $[41] = t17; 352: $[42] = t18; 353: $[43] = t19; 354: } else { 355: t19 = $[43]; 356: } 357: let t20; 358: if ($[44] !== onSelect) { 359: t20 = value => { 360: if (value.startsWith(OPEN_FOLDER_PREFIX)) { 361: const folderPath = value.slice(OPEN_FOLDER_PREFIX.length); 362: mkdir(folderPath, { 363: recursive: true 364: }).catch(_temp8).then(() => openPath(folderPath)); 365: return; 366: } 367: lastSelectedPath = value; 368: onSelect(value); 369: }; 370: $[44] = onSelect; 371: $[45] = t20; 372: } else { 373: t20 = $[45]; 374: } 375: let t21; 376: if ($[46] !== lastToggleIndex) { 377: t21 = () => setFocusedToggle(lastToggleIndex); 378: $[46] = lastToggleIndex; 379: $[47] = t21; 380: } else { 381: t21 = $[47]; 382: } 383: let t22; 384: if ($[48] !== initialPath || $[49] !== memoryOptions || $[50] !== onCancel || $[51] !== t20 || $[52] !== t21 || $[53] !== toggleFocused) { 385: t22 = <Select defaultFocusValue={initialPath} options={memoryOptions} isDisabled={toggleFocused} onChange={t20} onCancel={onCancel} onUpFromFirstItem={t21} />; 386: $[48] = initialPath; 387: $[49] = memoryOptions; 388: $[50] = onCancel; 389: $[51] = t20; 390: $[52] = t21; 391: $[53] = toggleFocused; 392: $[54] = t22; 393: } else { 394: t22 = $[54]; 395: } 396: let t23; 397: if ($[55] !== t19 || $[56] !== t22) { 398: t23 = <Box flexDirection="column" width="100%">{t19}{t22}</Box>; 399: $[55] = t19; 400: $[56] = t22; 401: $[57] = t23; 402: } else { 403: t23 = $[57]; 404: } 405: return t23; 406: } 407: function _temp8() {} 408: function _temp7(prev_0) { 409: return prev_0 !== null && prev_0 > 0 ? prev_0 - 1 : prev_0; 410: } 411: function _temp6(s_0) { 412: return Object.values(s_0.tasks).some(_temp5); 413: } 414: function _temp5(t) { 415: return t.type === "dream" && t.status === "running"; 416: } 417: function _temp4(opt) { 418: return opt.value === lastSelectedPath; 419: } 420: function _temp3(s) { 421: return s.agentDefinitions; 422: } 423: function _temp2(f_2) { 424: return { 425: ...f_2, 426: exists: true 427: }; 428: } 429: function _temp(f_1) { 430: return f_1.type !== "AutoMem" && f_1.type !== "TeamMem"; 431: }

File: src/components/memory/MemoryUpdateNotification.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { homedir } from 'os'; 3: import { relative } from 'path'; 4: import React from 'react'; 5: import { Box, Text } from '../../ink.js'; 6: import { getCwd } from '../../utils/cwd.js'; 7: export function getRelativeMemoryPath(path: string): string { 8: const homeDir = homedir(); 9: const cwd = getCwd(); 10: const relativeToHome = path.startsWith(homeDir) ? '~' + path.slice(homeDir.length) : null; 11: const relativeToCwd = path.startsWith(cwd) ? './' + relative(cwd, path) : null; 12: if (relativeToHome && relativeToCwd) { 13: return relativeToHome.length <= relativeToCwd.length ? relativeToHome : relativeToCwd; 14: } 15: return relativeToHome || relativeToCwd || path; 16: } 17: export function MemoryUpdateNotification(t0) { 18: const $ = _c(4); 19: const { 20: memoryPath 21: } = t0; 22: let t1; 23: if ($[0] !== memoryPath) { 24: t1 = getRelativeMemoryPath(memoryPath); 25: $[0] = memoryPath; 26: $[1] = t1; 27: } else { 28: t1 = $[1]; 29: } 30: const displayPath = t1; 31: let t2; 32: if ($[2] !== displayPath) { 33: t2 = <Box flexDirection="column" flexGrow={1}><Text color="text">Memory updated in {displayPath} · /memory to edit</Text></Box>; 34: $[2] = displayPath; 35: $[3] = t2; 36: } else { 37: t2 = $[3]; 38: } 39: return t2; 40: }

File: src/components/messages/UserToolResultMessage/RejectedPlanMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Markdown } from 'src/components/Markdown.js'; 4: import { MessageResponse } from 'src/components/MessageResponse.js'; 5: import { Box, Text } from '../../../ink.js'; 6: type Props = { 7: plan: string; 8: }; 9: export function RejectedPlanMessage(t0) { 10: const $ = _c(3); 11: const { 12: plan 13: } = t0; 14: let t1; 15: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 16: t1 = <Text color="subtle">User rejected Claude's plan:</Text>; 17: $[0] = t1; 18: } else { 19: t1 = $[0]; 20: } 21: let t2; 22: if ($[1] !== plan) { 23: t2 = <MessageResponse><Box flexDirection="column">{t1}<Box borderStyle="round" borderColor="planMode" paddingX={1} overflow="hidden"><Markdown>{plan}</Markdown></Box></Box></MessageResponse>; 24: $[1] = plan; 25: $[2] = t2; 26: } else { 27: t2 = $[2]; 28: } 29: return t2; 30: }

File: src/components/messages/UserToolResultMessage/RejectedToolUseMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Text } from '../../../ink.js'; 4: import { MessageResponse } from '../../MessageResponse.js'; 5: export function RejectedToolUseMessage() { 6: const $ = _c(1); 7: let t0; 8: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 9: t0 = <MessageResponse height={1}><Text dimColor={true}>Tool use rejected</Text></MessageResponse>; 10: $[0] = t0; 11: } else { 12: t0 = $[0]; 13: } 14: return t0; 15: }

File: src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { InterruptedByUser } from 'src/components/InterruptedByUser.js'; 4: import { MessageResponse } from 'src/components/MessageResponse.js'; 5: export function UserToolCanceledMessage() { 6: const $ = _c(1); 7: let t0; 8: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 9: t0 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>; 10: $[0] = t0; 11: } else { 12: t0 = $[0]; 13: } 14: return t0; 15: }

File: src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 4: import * as React from 'react'; 5: import { BULLET_OPERATOR } from '../../../constants/figures.js'; 6: import { Text } from '../../../ink.js'; 7: import { filterToolProgressMessages, type Tool, type Tools } from '../../../Tool.js'; 8: import type { ProgressMessage } from '../../../types/message.js'; 9: import { INTERRUPT_MESSAGE_FOR_TOOL_USE, isClassifierDenial, PLAN_REJECTION_PREFIX, REJECT_MESSAGE_WITH_REASON_PREFIX } from '../../../utils/messages.js'; 10: import { FallbackToolUseErrorMessage } from '../../FallbackToolUseErrorMessage.js'; 11: import { InterruptedByUser } from '../../InterruptedByUser.js'; 12: import { MessageResponse } from '../../MessageResponse.js'; 13: import { RejectedPlanMessage } from './RejectedPlanMessage.js'; 14: import { RejectedToolUseMessage } from './RejectedToolUseMessage.js'; 15: type Props = { 16: progressMessagesForMessage: ProgressMessage[]; 17: tool?: Tool; 18: tools: Tools; 19: param: ToolResultBlockParam; 20: verbose: boolean; 21: isTranscriptMode?: boolean; 22: }; 23: export function UserToolErrorMessage(t0) { 24: const $ = _c(14); 25: const { 26: progressMessagesForMessage, 27: tool, 28: tools, 29: param, 30: verbose, 31: isTranscriptMode 32: } = t0; 33: if (typeof param.content === "string" && param.content.includes(INTERRUPT_MESSAGE_FOR_TOOL_USE)) { 34: let t1; 35: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 36: t1 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>; 37: $[0] = t1; 38: } else { 39: t1 = $[0]; 40: } 41: return t1; 42: } 43: if (typeof param.content === "string" && param.content.startsWith(PLAN_REJECTION_PREFIX)) { 44: let t1; 45: if ($[1] !== param.content) { 46: t1 = param.content.substring(PLAN_REJECTION_PREFIX.length); 47: $[1] = param.content; 48: $[2] = t1; 49: } else { 50: t1 = $[2]; 51: } 52: const planContent = t1; 53: let t2; 54: if ($[3] !== planContent) { 55: t2 = <RejectedPlanMessage plan={planContent} />; 56: $[3] = planContent; 57: $[4] = t2; 58: } else { 59: t2 = $[4]; 60: } 61: return t2; 62: } 63: if (typeof param.content === "string" && param.content.startsWith(REJECT_MESSAGE_WITH_REASON_PREFIX)) { 64: let t1; 65: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 66: t1 = <RejectedToolUseMessage />; 67: $[5] = t1; 68: } else { 69: t1 = $[5]; 70: } 71: return t1; 72: } 73: if (feature("TRANSCRIPT_CLASSIFIER") && typeof param.content === "string" && isClassifierDenial(param.content)) { 74: let t1; 75: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 76: t1 = <MessageResponse height={1}><Text dimColor={true}>Denied by auto mode classifier {BULLET_OPERATOR} /feedback if incorrect</Text></MessageResponse>; 77: $[6] = t1; 78: } else { 79: t1 = $[6]; 80: } 81: return t1; 82: } 83: let t1; 84: if ($[7] !== isTranscriptMode || $[8] !== param.content || $[9] !== progressMessagesForMessage || $[10] !== tool || $[11] !== tools || $[12] !== verbose) { 85: t1 = tool?.renderToolUseErrorMessage?.(param.content, { 86: progressMessagesForMessage: filterToolProgressMessages(progressMessagesForMessage), 87: tools, 88: verbose, 89: isTranscriptMode 90: }) ?? <FallbackToolUseErrorMessage result={param.content} verbose={verbose} />; 91: $[7] = isTranscriptMode; 92: $[8] = param.content; 93: $[9] = progressMessagesForMessage; 94: $[10] = tool; 95: $[11] = tools; 96: $[12] = verbose; 97: $[13] = t1; 98: } else { 99: t1 = $[13]; 100: } 101: return t1; 102: }

File: src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useTerminalSize } from '../../../hooks/useTerminalSize.js'; 4: import { useTheme } from '../../../ink.js'; 5: import { filterToolProgressMessages, type Tool, type Tools } from '../../../Tool.js'; 6: import type { ProgressMessage } from '../../../types/message.js'; 7: import type { buildMessageLookups } from '../../../utils/messages.js'; 8: import { FallbackToolUseRejectedMessage } from '../../FallbackToolUseRejectedMessage.js'; 9: type Props = { 10: input: { 11: [key: string]: unknown; 12: }; 13: progressMessagesForMessage: ProgressMessage[]; 14: style?: 'condensed'; 15: tool?: Tool; 16: tools: Tools; 17: lookups: ReturnType<typeof buildMessageLookups>; 18: verbose: boolean; 19: isTranscriptMode?: boolean; 20: }; 21: export function UserToolRejectMessage(t0) { 22: const $ = _c(13); 23: const { 24: input, 25: progressMessagesForMessage, 26: style, 27: tool, 28: tools, 29: verbose, 30: isTranscriptMode 31: } = t0; 32: const { 33: columns 34: } = useTerminalSize(); 35: const [theme] = useTheme(); 36: if (!tool || !tool.renderToolUseRejectedMessage) { 37: let t1; 38: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 39: t1 = <FallbackToolUseRejectedMessage />; 40: $[0] = t1; 41: } else { 42: t1 = $[0]; 43: } 44: return t1; 45: } 46: const t1 = tool.inputSchema; 47: let t2; 48: let t3; 49: if ($[1] !== columns || $[2] !== input || $[3] !== isTranscriptMode || $[4] !== progressMessagesForMessage || $[5] !== style || $[6] !== theme || $[7] !== tool || $[8] !== tools || $[9] !== verbose) { 50: t3 = Symbol.for("react.early_return_sentinel"); 51: bb0: { 52: const parsedInput = t1.safeParse(input); 53: if (!parsedInput.success) { 54: let t4; 55: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 56: t4 = <FallbackToolUseRejectedMessage />; 57: $[12] = t4; 58: } else { 59: t4 = $[12]; 60: } 61: t3 = t4; 62: break bb0; 63: } 64: t2 = tool.renderToolUseRejectedMessage(parsedInput.data, { 65: columns, 66: messages: [], 67: tools, 68: verbose, 69: progressMessagesForMessage: filterToolProgressMessages(progressMessagesForMessage), 70: style, 71: theme, 72: isTranscriptMode 73: }) ?? <FallbackToolUseRejectedMessage />; 74: } 75: $[1] = columns; 76: $[2] = input; 77: $[3] = isTranscriptMode; 78: $[4] = progressMessagesForMessage; 79: $[5] = style; 80: $[6] = theme; 81: $[7] = tool; 82: $[8] = tools; 83: $[9] = verbose; 84: $[10] = t2; 85: $[11] = t3; 86: } else { 87: t2 = $[10]; 88: t3 = $[11]; 89: } 90: if (t3 !== Symbol.for("react.early_return_sentinel")) { 91: return t3; 92: } 93: return t2; 94: }

File: src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import * as React from 'react'; 4: import type { Tools } from '../../../Tool.js'; 5: import type { NormalizedUserMessage, ProgressMessage } from '../../../types/message.js'; 6: import { type buildMessageLookups, CANCEL_MESSAGE, INTERRUPT_MESSAGE_FOR_TOOL_USE, REJECT_MESSAGE } from '../../../utils/messages.js'; 7: import { UserToolCanceledMessage } from './UserToolCanceledMessage.js'; 8: import { UserToolErrorMessage } from './UserToolErrorMessage.js'; 9: import { UserToolRejectMessage } from './UserToolRejectMessage.js'; 10: import { UserToolSuccessMessage } from './UserToolSuccessMessage.js'; 11: import { useGetToolFromMessages } from './utils.js'; 12: type Props = { 13: param: ToolResultBlockParam; 14: message: NormalizedUserMessage; 15: lookups: ReturnType<typeof buildMessageLookups>; 16: progressMessagesForMessage: ProgressMessage[]; 17: style?: 'condensed'; 18: tools: Tools; 19: verbose: boolean; 20: width: number | string; 21: isTranscriptMode?: boolean; 22: }; 23: export function UserToolResultMessage(t0) { 24: const $ = _c(28); 25: const { 26: param, 27: message, 28: lookups, 29: progressMessagesForMessage, 30: style, 31: tools, 32: verbose, 33: width, 34: isTranscriptMode 35: } = t0; 36: const toolUse = useGetToolFromMessages(param.tool_use_id, tools, lookups); 37: if (!toolUse) { 38: return null; 39: } 40: if (typeof param.content === "string" && param.content.startsWith(CANCEL_MESSAGE)) { 41: let t1; 42: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 43: t1 = <UserToolCanceledMessage />; 44: $[0] = t1; 45: } else { 46: t1 = $[0]; 47: } 48: return t1; 49: } 50: if (typeof param.content === "string" && param.content.startsWith(REJECT_MESSAGE) || param.content === INTERRUPT_MESSAGE_FOR_TOOL_USE) { 51: const t1 = toolUse.toolUse.input as { 52: [key: string]: unknown; 53: }; 54: let t2; 55: if ($[1] !== isTranscriptMode || $[2] !== lookups || $[3] !== progressMessagesForMessage || $[4] !== style || $[5] !== t1 || $[6] !== toolUse.tool || $[7] !== tools || $[8] !== verbose) { 56: t2 = <UserToolRejectMessage input={t1} progressMessagesForMessage={progressMessagesForMessage} tool={toolUse.tool} tools={tools} lookups={lookups} style={style} verbose={verbose} isTranscriptMode={isTranscriptMode} />; 57: $[1] = isTranscriptMode; 58: $[2] = lookups; 59: $[3] = progressMessagesForMessage; 60: $[4] = style; 61: $[5] = t1; 62: $[6] = toolUse.tool; 63: $[7] = tools; 64: $[8] = verbose; 65: $[9] = t2; 66: } else { 67: t2 = $[9]; 68: } 69: return t2; 70: } 71: if (param.is_error) { 72: let t1; 73: if ($[10] !== isTranscriptMode || $[11] !== param || $[12] !== progressMessagesForMessage || $[13] !== toolUse.tool || $[14] !== tools || $[15] !== verbose) { 74: t1 = <UserToolErrorMessage progressMessagesForMessage={progressMessagesForMessage} tool={toolUse.tool} tools={tools} param={param} verbose={verbose} isTranscriptMode={isTranscriptMode} />; 75: $[10] = isTranscriptMode; 76: $[11] = param; 77: $[12] = progressMessagesForMessage; 78: $[13] = toolUse.tool; 79: $[14] = tools; 80: $[15] = verbose; 81: $[16] = t1; 82: } else { 83: t1 = $[16]; 84: } 85: return t1; 86: } 87: let t1; 88: if ($[17] !== isTranscriptMode || $[18] !== lookups || $[19] !== message || $[20] !== progressMessagesForMessage || $[21] !== style || $[22] !== toolUse.tool || $[23] !== toolUse.toolUse.id || $[24] !== tools || $[25] !== verbose || $[26] !== width) { 89: t1 = <UserToolSuccessMessage message={message} lookups={lookups} toolUseID={toolUse.toolUse.id} progressMessagesForMessage={progressMessagesForMessage} style={style} tool={toolUse.tool} tools={tools} verbose={verbose} width={width} isTranscriptMode={isTranscriptMode} />; 90: $[17] = isTranscriptMode; 91: $[18] = lookups; 92: $[19] = message; 93: $[20] = progressMessagesForMessage; 94: $[21] = style; 95: $[22] = toolUse.tool; 96: $[23] = toolUse.toolUse.id; 97: $[24] = tools; 98: $[25] = verbose; 99: $[26] = width; 100: $[27] = t1; 101: } else { 102: t1 = $[27]; 103: } 104: return t1; 105: }

File: src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx

typescript 1: import { feature } from 'bun:bundle'; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { SentryErrorBoundary } from 'src/components/SentryErrorBoundary.js'; 5: import { Box, Text, useTheme } from '../../../ink.js'; 6: import { useAppState } from '../../../state/AppState.js'; 7: import { filterToolProgressMessages, type Tool, type Tools } from '../../../Tool.js'; 8: import type { NormalizedUserMessage, ProgressMessage } from '../../../types/message.js'; 9: import { deleteClassifierApproval, getClassifierApproval, getYoloClassifierApproval } from '../../../utils/classifierApprovals.js'; 10: import type { buildMessageLookups } from '../../../utils/messages.js'; 11: import { MessageResponse } from '../../MessageResponse.js'; 12: import { HookProgressMessage } from '../HookProgressMessage.js'; 13: type Props = { 14: message: NormalizedUserMessage; 15: lookups: ReturnType<typeof buildMessageLookups>; 16: toolUseID: string; 17: progressMessagesForMessage: ProgressMessage[]; 18: style?: 'condensed'; 19: tool?: Tool; 20: tools: Tools; 21: verbose: boolean; 22: width: number | string; 23: isTranscriptMode?: boolean; 24: }; 25: export function UserToolSuccessMessage({ 26: message, 27: lookups, 28: toolUseID, 29: progressMessagesForMessage, 30: style, 31: tool, 32: tools, 33: verbose, 34: width, 35: isTranscriptMode 36: }: Props): React.ReactNode { 37: const [theme] = useTheme(); 38: const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ? 39: useAppState(s => s.isBriefOnly) : false; 40: const [classifierRule] = React.useState(() => getClassifierApproval(toolUseID)); 41: const [yoloReason] = React.useState(() => getYoloClassifierApproval(toolUseID)); 42: React.useEffect(() => { 43: deleteClassifierApproval(toolUseID); 44: }, [toolUseID]); 45: if (!message.toolUseResult || !tool) { 46: return null; 47: } 48: const parsedOutput = tool.outputSchema?.safeParse(message.toolUseResult); 49: if (parsedOutput && !parsedOutput.success) { 50: return null; 51: } 52: const toolResult = parsedOutput?.data ?? message.toolUseResult; 53: const renderedMessage = tool.renderToolResultMessage?.(toolResult as never, filterToolProgressMessages(progressMessagesForMessage), { 54: style, 55: theme, 56: tools, 57: verbose, 58: isTranscriptMode, 59: isBriefOnly, 60: input: lookups.toolUseByToolUseID.get(toolUseID)?.input 61: }) ?? null; 62: if (renderedMessage === null) { 63: return null; 64: } 65: const rendersAsAssistantText = tool.userFacingName(undefined) === ''; 66: return <Box flexDirection="column"> 67: <Box flexDirection="column" width={rendersAsAssistantText ? undefined : width}> 68: {renderedMessage} 69: {feature('BASH_CLASSIFIER') ? classifierRule && <MessageResponse height={1}> 70: <Text dimColor> 71: <Text color="success">{figures.tick}</Text> 72: {' Auto-approved \u00b7 matched '} 73: {`"${classifierRule}"`} 74: </Text> 75: </MessageResponse> : null} 76: {feature('TRANSCRIPT_CLASSIFIER') ? yoloReason && <MessageResponse height={1}> 77: <Text dimColor>Allowed by auto mode classifier</Text> 78: </MessageResponse> : null} 79: </Box> 80: <SentryErrorBoundary> 81: <HookProgressMessage hookEvent="PostToolUse" lookups={lookups} toolUseID={toolUseID} verbose={verbose} isTranscriptMode={isTranscriptMode} /> 82: </SentryErrorBoundary> 83: </Box>; 84: }

File: src/components/messages/UserToolResultMessage/utils.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import { useMemo } from 'react'; 4: import { findToolByName, type Tool, type Tools } from '../../../Tool.js'; 5: import type { buildMessageLookups } from '../../../utils/messages.js'; 6: export function useGetToolFromMessages(toolUseID, tools, lookups) { 7: const $ = _c(7); 8: let t0; 9: if ($[0] !== lookups.toolUseByToolUseID || $[1] !== toolUseID || $[2] !== tools) { 10: bb0: { 11: const toolUse = lookups.toolUseByToolUseID.get(toolUseID); 12: if (!toolUse) { 13: t0 = null; 14: break bb0; 15: } 16: const tool = findToolByName(tools, toolUse.name); 17: if (!tool) { 18: t0 = null; 19: break bb0; 20: } 21: let t1; 22: if ($[4] !== tool || $[5] !== toolUse) { 23: t1 = { 24: tool, 25: toolUse 26: }; 27: $[4] = tool; 28: $[5] = toolUse; 29: $[6] = t1; 30: } else { 31: t1 = $[6]; 32: } 33: t0 = t1; 34: } 35: $[0] = lookups.toolUseByToolUseID; 36: $[1] = toolUseID; 37: $[2] = tools; 38: $[3] = t0; 39: } else { 40: t0 = $[3]; 41: } 42: return t0; 43: }

File: src/components/messages/AdvisorMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React from 'react'; 4: import { Box, Text } from '../../ink.js'; 5: import type { AdvisorBlock } from '../../utils/advisor.js'; 6: import { renderModelName } from '../../utils/model/model.js'; 7: import { jsonStringify } from '../../utils/slowOperations.js'; 8: import { CtrlOToExpand } from '../CtrlOToExpand.js'; 9: import { MessageResponse } from '../MessageResponse.js'; 10: import { ToolUseLoader } from '../ToolUseLoader.js'; 11: type Props = { 12: block: AdvisorBlock; 13: addMargin: boolean; 14: resolvedToolUseIDs: Set<string>; 15: erroredToolUseIDs: Set<string>; 16: shouldAnimate: boolean; 17: verbose: boolean; 18: advisorModel?: string; 19: }; 20: export function AdvisorMessage(t0) { 21: const $ = _c(30); 22: const { 23: block, 24: addMargin, 25: resolvedToolUseIDs, 26: erroredToolUseIDs, 27: shouldAnimate, 28: verbose, 29: advisorModel 30: } = t0; 31: if (block.type === "server_tool_use") { 32: let t1; 33: if ($[0] !== block.input) { 34: t1 = block.input && Object.keys(block.input).length > 0 ? jsonStringify(block.input) : null; 35: $[0] = block.input; 36: $[1] = t1; 37: } else { 38: t1 = $[1]; 39: } 40: const input = t1; 41: const t2 = addMargin ? 1 : 0; 42: let t3; 43: if ($[2] !== block.id || $[3] !== resolvedToolUseIDs) { 44: t3 = resolvedToolUseIDs.has(block.id); 45: $[2] = block.id; 46: $[3] = resolvedToolUseIDs; 47: $[4] = t3; 48: } else { 49: t3 = $[4]; 50: } 51: const t4 = !t3; 52: let t5; 53: if ($[5] !== block.id || $[6] !== erroredToolUseIDs) { 54: t5 = erroredToolUseIDs.has(block.id); 55: $[5] = block.id; 56: $[6] = erroredToolUseIDs; 57: $[7] = t5; 58: } else { 59: t5 = $[7]; 60: } 61: let t6; 62: if ($[8] !== shouldAnimate || $[9] !== t4 || $[10] !== t5) { 63: t6 = <ToolUseLoader shouldAnimate={shouldAnimate} isUnresolved={t4} isError={t5} />; 64: $[8] = shouldAnimate; 65: $[9] = t4; 66: $[10] = t5; 67: $[11] = t6; 68: } else { 69: t6 = $[11]; 70: } 71: let t7; 72: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 73: t7 = <Text bold={true}>Advising</Text>; 74: $[12] = t7; 75: } else { 76: t7 = $[12]; 77: } 78: let t8; 79: if ($[13] !== advisorModel) { 80: t8 = advisorModel ? <Text dimColor={true}> using {renderModelName(advisorModel)}</Text> : null; 81: $[13] = advisorModel; 82: $[14] = t8; 83: } else { 84: t8 = $[14]; 85: } 86: let t9; 87: if ($[15] !== input) { 88: t9 = input ? <Text dimColor={true}> · {input}</Text> : null; 89: $[15] = input; 90: $[16] = t9; 91: } else { 92: t9 = $[16]; 93: } 94: let t10; 95: if ($[17] !== t2 || $[18] !== t6 || $[19] !== t8 || $[20] !== t9) { 96: t10 = <Box marginTop={t2} paddingRight={2} flexDirection="row">{t6}{t7}{t8}{t9}</Box>; 97: $[17] = t2; 98: $[18] = t6; 99: $[19] = t8; 100: $[20] = t9; 101: $[21] = t10; 102: } else { 103: t10 = $[21]; 104: } 105: return t10; 106: } 107: let body; 108: bb0: switch (block.content.type) { 109: case "advisor_tool_result_error": 110: { 111: let t1; 112: if ($[22] !== block.content.error_code) { 113: t1 = <Text color="error">Advisor unavailable ({block.content.error_code})</Text>; 114: $[22] = block.content.error_code; 115: $[23] = t1; 116: } else { 117: t1 = $[23]; 118: } 119: body = t1; 120: break bb0; 121: } 122: case "advisor_result": 123: { 124: let t1; 125: if ($[24] !== block.content.text || $[25] !== verbose) { 126: t1 = verbose ? <Text dimColor={true}>{block.content.text}</Text> : <Text dimColor={true}>{figures.tick} Advisor has reviewed the conversation and will apply the feedback <CtrlOToExpand /></Text>; 127: $[24] = block.content.text; 128: $[25] = verbose; 129: $[26] = t1; 130: } else { 131: t1 = $[26]; 132: } 133: body = t1; 134: break bb0; 135: } 136: case "advisor_redacted_result": 137: { 138: let t1; 139: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 140: t1 = <Text dimColor={true}>{figures.tick} Advisor has reviewed the conversation and will apply the feedback</Text>; 141: $[27] = t1; 142: } else { 143: t1 = $[27]; 144: } 145: body = t1; 146: } 147: } 148: let t1; 149: if ($[28] !== body) { 150: t1 = <Box paddingRight={2}><MessageResponse>{body}</MessageResponse></Box>; 151: $[28] = body; 152: $[29] = t1; 153: } else { 154: t1 = $[29]; 155: } 156: return t1; 157: }

File: src/components/messages/AssistantRedactedThinkingMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: type Props = { 5: addMargin: boolean; 6: }; 7: export function AssistantRedactedThinkingMessage(t0) { 8: const $ = _c(3); 9: const { 10: addMargin: t1 11: } = t0; 12: const addMargin = t1 === undefined ? false : t1; 13: const t2 = addMargin ? 1 : 0; 14: let t3; 15: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 16: t3 = <Text dimColor={true} italic={true}>✻ Thinking…</Text>; 17: $[0] = t3; 18: } else { 19: t3 = $[0]; 20: } 21: let t4; 22: if ($[1] !== t2) { 23: t4 = <Box marginTop={t2}>{t3}</Box>; 24: $[1] = t2; 25: $[2] = t4; 26: } else { 27: t4 = $[2]; 28: } 29: return t4; 30: }

File: src/components/messages/AssistantTextMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import React, { useContext } from 'react'; 4: import { ERROR_MESSAGE_USER_ABORT } from 'src/services/compact/compact.js'; 5: import { isRateLimitErrorMessage } from 'src/services/rateLimitMessages.js'; 6: import { BLACK_CIRCLE } from '../../constants/figures.js'; 7: import { Box, NoSelect, Text } from '../../ink.js'; 8: import { API_ERROR_MESSAGE_PREFIX, API_TIMEOUT_ERROR_MESSAGE, CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE, CUSTOM_OFF_SWITCH_MESSAGE, INVALID_API_KEY_ERROR_MESSAGE, INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL, ORG_DISABLED_ERROR_MESSAGE_ENV_KEY, ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH, PROMPT_TOO_LONG_ERROR_MESSAGE, startsWithApiErrorPrefix, TOKEN_REVOKED_ERROR_MESSAGE } from '../../services/api/errors.js'; 9: import { isEmptyMessageText, NO_RESPONSE_REQUESTED } from '../../utils/messages.js'; 10: import { getUpgradeMessage } from '../../utils/model/contextWindowUpgradeCheck.js'; 11: import { getDefaultSonnetModel, renderModelName } from '../../utils/model/model.js'; 12: import { isMacOsKeychainLocked } from '../../utils/secureStorage/macOsKeychainStorage.js'; 13: import { CtrlOToExpand } from '../CtrlOToExpand.js'; 14: import { InterruptedByUser } from '../InterruptedByUser.js'; 15: import { Markdown } from '../Markdown.js'; 16: import { MessageResponse } from '../MessageResponse.js'; 17: import { MessageActionsSelectedContext } from '../messageActions.js'; 18: import { RateLimitMessage } from './RateLimitMessage.js'; 19: const MAX_API_ERROR_CHARS = 1000; 20: type Props = { 21: param: TextBlockParam; 22: addMargin: boolean; 23: shouldShowDot: boolean; 24: verbose: boolean; 25: width?: number | string; 26: onOpenRateLimitOptions?: () => void; 27: }; 28: function InvalidApiKeyMessage() { 29: const $ = _c(2); 30: let t0; 31: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 32: t0 = isMacOsKeychainLocked(); 33: $[0] = t0; 34: } else { 35: t0 = $[0]; 36: } 37: const isKeychainLocked = t0; 38: let t1; 39: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 40: t1 = <MessageResponse><Box flexDirection="column"><Text color="error">{INVALID_API_KEY_ERROR_MESSAGE}</Text>{isKeychainLocked && <Text dimColor={true}>· Run in another terminal: security unlock-keychain</Text>}</Box></MessageResponse>; 41: $[1] = t1; 42: } else { 43: t1 = $[1]; 44: } 45: return t1; 46: } 47: export function AssistantTextMessage(t0) { 48: const $ = _c(34); 49: const { 50: param: t1, 51: addMargin, 52: shouldShowDot, 53: verbose, 54: onOpenRateLimitOptions 55: } = t0; 56: const { 57: text 58: } = t1; 59: const isSelected = useContext(MessageActionsSelectedContext); 60: if (isEmptyMessageText(text)) { 61: return null; 62: } 63: if (isRateLimitErrorMessage(text)) { 64: let t2; 65: if ($[0] !== onOpenRateLimitOptions || $[1] !== text) { 66: t2 = <RateLimitMessage text={text} onOpenRateLimitOptions={onOpenRateLimitOptions} />; 67: $[0] = onOpenRateLimitOptions; 68: $[1] = text; 69: $[2] = t2; 70: } else { 71: t2 = $[2]; 72: } 73: return t2; 74: } 75: switch (text) { 76: case NO_RESPONSE_REQUESTED: 77: { 78: return null; 79: } 80: case PROMPT_TOO_LONG_ERROR_MESSAGE: 81: { 82: let t2; 83: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 84: t2 = getUpgradeMessage("warning"); 85: $[3] = t2; 86: } else { 87: t2 = $[3]; 88: } 89: const upgradeHint = t2; 90: let t3; 91: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 92: t3 = <MessageResponse height={1}><Text color="error">Context limit reached · /compact or /clear to continue{upgradeHint ? ` · ${upgradeHint}` : ""}</Text></MessageResponse>; 93: $[4] = t3; 94: } else { 95: t3 = $[4]; 96: } 97: return t3; 98: } 99: case CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE: 100: { 101: let t2; 102: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 103: t2 = <MessageResponse height={1}><Text color="error">Credit balance too low · Add funds: https: 104: $[5] = t2; 105: } else { 106: t2 = $[5]; 107: } 108: return t2; 109: } 110: case INVALID_API_KEY_ERROR_MESSAGE: 111: { 112: let t2; 113: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 114: t2 = <InvalidApiKeyMessage />; 115: $[6] = t2; 116: } else { 117: t2 = $[6]; 118: } 119: return t2; 120: } 121: case INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL: 122: { 123: let t2; 124: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 125: t2 = <MessageResponse height={1}><Text color="error">{INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL}</Text></MessageResponse>; 126: $[7] = t2; 127: } else { 128: t2 = $[7]; 129: } 130: return t2; 131: } 132: case ORG_DISABLED_ERROR_MESSAGE_ENV_KEY: 133: case ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH: 134: { 135: let t2; 136: if ($[8] !== text) { 137: t2 = <MessageResponse><Text color="error">{text}</Text></MessageResponse>; 138: $[8] = text; 139: $[9] = t2; 140: } else { 141: t2 = $[9]; 142: } 143: return t2; 144: } 145: case TOKEN_REVOKED_ERROR_MESSAGE: 146: { 147: let t2; 148: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 149: t2 = <MessageResponse height={1}><Text color="error">{TOKEN_REVOKED_ERROR_MESSAGE}</Text></MessageResponse>; 150: $[10] = t2; 151: } else { 152: t2 = $[10]; 153: } 154: return t2; 155: } 156: case API_TIMEOUT_ERROR_MESSAGE: 157: { 158: let t2; 159: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 160: t2 = <MessageResponse height={1}><Text color="error">{API_TIMEOUT_ERROR_MESSAGE}{process.env.API_TIMEOUT_MS && <>{" "}(API_TIMEOUT_MS={process.env.API_TIMEOUT_MS}ms, try increasing it)</>}</Text></MessageResponse>; 161: $[11] = t2; 162: } else { 163: t2 = $[11]; 164: } 165: return t2; 166: } 167: case CUSTOM_OFF_SWITCH_MESSAGE: 168: { 169: let t2; 170: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 171: t2 = <Text color="error">We are experiencing high demand for Opus 4.</Text>; 172: $[12] = t2; 173: } else { 174: t2 = $[12]; 175: } 176: let t3; 177: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 178: t3 = <MessageResponse><Box flexDirection="column" gap={1}>{t2}<Text>To continue immediately, use /model to switch to{" "}{renderModelName(getDefaultSonnetModel())} and continue coding.</Text></Box></MessageResponse>; 179: $[13] = t3; 180: } else { 181: t3 = $[13]; 182: } 183: return t3; 184: } 185: case ERROR_MESSAGE_USER_ABORT: 186: { 187: let t2; 188: if ($[14] === Symbol.for("react.memo_cache_sentinel")) { 189: t2 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>; 190: $[14] = t2; 191: } else { 192: t2 = $[14]; 193: } 194: return t2; 195: } 196: default: 197: { 198: if (startsWithApiErrorPrefix(text)) { 199: const truncated = !verbose && text.length > MAX_API_ERROR_CHARS; 200: const t2 = text === API_ERROR_MESSAGE_PREFIX ? `${API_ERROR_MESSAGE_PREFIX}: Please wait a moment and try again.` : truncated ? text.slice(0, MAX_API_ERROR_CHARS) + "\u2026" : text; 201: let t3; 202: if ($[15] !== t2) { 203: t3 = <Text color="error">{t2}</Text>; 204: $[15] = t2; 205: $[16] = t3; 206: } else { 207: t3 = $[16]; 208: } 209: let t4; 210: if ($[17] !== truncated) { 211: t4 = truncated && <CtrlOToExpand />; 212: $[17] = truncated; 213: $[18] = t4; 214: } else { 215: t4 = $[18]; 216: } 217: let t5; 218: if ($[19] !== t3 || $[20] !== t4) { 219: t5 = <MessageResponse><Box flexDirection="column">{t3}{t4}</Box></MessageResponse>; 220: $[19] = t3; 221: $[20] = t4; 222: $[21] = t5; 223: } else { 224: t5 = $[21]; 225: } 226: return t5; 227: } 228: const t2 = addMargin ? 1 : 0; 229: const t3 = isSelected ? "messageActionsBackground" : undefined; 230: let t4; 231: if ($[22] !== isSelected || $[23] !== shouldShowDot) { 232: t4 = shouldShowDot && <NoSelect fromLeftEdge={true} minWidth={2}><Text color={isSelected ? "suggestion" : "text"}>{BLACK_CIRCLE}</Text></NoSelect>; 233: $[22] = isSelected; 234: $[23] = shouldShowDot; 235: $[24] = t4; 236: } else { 237: t4 = $[24]; 238: } 239: let t5; 240: if ($[25] !== text) { 241: t5 = <Box flexDirection="column"><Markdown>{text}</Markdown></Box>; 242: $[25] = text; 243: $[26] = t5; 244: } else { 245: t5 = $[26]; 246: } 247: let t6; 248: if ($[27] !== t4 || $[28] !== t5) { 249: t6 = <Box flexDirection="row">{t4}{t5}</Box>; 250: $[27] = t4; 251: $[28] = t5; 252: $[29] = t6; 253: } else { 254: t6 = $[29]; 255: } 256: let t7; 257: if ($[30] !== t2 || $[31] !== t3 || $[32] !== t6) { 258: t7 = <Box alignItems="flex-start" flexDirection="row" justifyContent="space-between" marginTop={t2} width="100%" backgroundColor={t3}>{t6}</Box>; 259: $[30] = t2; 260: $[31] = t3; 261: $[32] = t6; 262: $[33] = t7; 263: } else { 264: t7 = $[33]; 265: } 266: return t7; 267: } 268: } 269: }

File: src/components/messages/AssistantThinkingMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { ThinkingBlock, ThinkingBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import React from 'react'; 4: import { Box, Text } from '../../ink.js'; 5: import { CtrlOToExpand } from '../CtrlOToExpand.js'; 6: import { Markdown } from '../Markdown.js'; 7: type Props = { 8: param: ThinkingBlock | ThinkingBlockParam | { 9: type: 'thinking'; 10: thinking: string; 11: }; 12: addMargin: boolean; 13: isTranscriptMode: boolean; 14: verbose: boolean; 15: hideInTranscript?: boolean; 16: }; 17: export function AssistantThinkingMessage(t0) { 18: const $ = _c(9); 19: const { 20: param: t1, 21: addMargin: t2, 22: isTranscriptMode, 23: verbose, 24: hideInTranscript: t3 25: } = t0; 26: const { 27: thinking 28: } = t1; 29: const addMargin = t2 === undefined ? false : t2; 30: const hideInTranscript = t3 === undefined ? false : t3; 31: if (!thinking) { 32: return null; 33: } 34: if (hideInTranscript) { 35: return null; 36: } 37: const shouldShowFullThinking = isTranscriptMode || verbose; 38: if (!shouldShowFullThinking) { 39: const t4 = addMargin ? 1 : 0; 40: let t5; 41: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 42: t5 = <Text dimColor={true} italic={true}>{"\u2234 Thinking"} <CtrlOToExpand /></Text>; 43: $[0] = t5; 44: } else { 45: t5 = $[0]; 46: } 47: let t6; 48: if ($[1] !== t4) { 49: t6 = <Box marginTop={t4}>{t5}</Box>; 50: $[1] = t4; 51: $[2] = t6; 52: } else { 53: t6 = $[2]; 54: } 55: return t6; 56: } 57: const t4 = addMargin ? 1 : 0; 58: let t5; 59: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 60: t5 = <Text dimColor={true} italic={true}>{"\u2234 Thinking"}…</Text>; 61: $[3] = t5; 62: } else { 63: t5 = $[3]; 64: } 65: let t6; 66: if ($[4] !== thinking) { 67: t6 = <Box paddingLeft={2}><Markdown dimColor={true}>{thinking}</Markdown></Box>; 68: $[4] = thinking; 69: $[5] = t6; 70: } else { 71: t6 = $[5]; 72: } 73: let t7; 74: if ($[6] !== t4 || $[7] !== t6) { 75: t7 = <Box flexDirection="column" gap={1} marginTop={t4} width="100%">{t5}{t6}</Box>; 76: $[6] = t4; 77: $[7] = t6; 78: $[8] = t7; 79: } else { 80: t7 = $[8]; 81: } 82: return t7; 83: }

File: src/components/messages/AssistantToolUseMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import React, { useMemo } from 'react'; 4: import { useTerminalSize } from 'src/hooks/useTerminalSize.js'; 5: import type { ThemeName } from 'src/utils/theme.js'; 6: import type { Command } from '../../commands.js'; 7: import { BLACK_CIRCLE } from '../../constants/figures.js'; 8: import { stringWidth } from '../../ink/stringWidth.js'; 9: import { Box, Text, useTheme } from '../../ink.js'; 10: import { useAppStateMaybeOutsideOfProvider } from '../../state/AppState.js'; 11: import { findToolByName, type Tool, type ToolProgressData, type Tools } from '../../Tool.js'; 12: import type { ProgressMessage } from '../../types/message.js'; 13: import { useIsClassifierChecking } from '../../utils/classifierApprovalsHook.js'; 14: import { logError } from '../../utils/log.js'; 15: import type { buildMessageLookups } from '../../utils/messages.js'; 16: import { MessageResponse } from '../MessageResponse.js'; 17: import { useSelectedMessageBg } from '../messageActions.js'; 18: import { SentryErrorBoundary } from '../SentryErrorBoundary.js'; 19: import { ToolUseLoader } from '../ToolUseLoader.js'; 20: import { HookProgressMessage } from './HookProgressMessage.js'; 21: type Props = { 22: param: ToolUseBlockParam; 23: addMargin: boolean; 24: tools: Tools; 25: commands: Command[]; 26: verbose: boolean; 27: inProgressToolUseIDs: Set<string>; 28: progressMessagesForMessage: ProgressMessage[]; 29: shouldAnimate: boolean; 30: shouldShowDot: boolean; 31: inProgressToolCallCount?: number; 32: lookups: ReturnType<typeof buildMessageLookups>; 33: isTranscriptMode?: boolean; 34: }; 35: export function AssistantToolUseMessage(t0) { 36: const $ = _c(81); 37: const { 38: param, 39: addMargin, 40: tools, 41: commands, 42: verbose, 43: inProgressToolUseIDs, 44: progressMessagesForMessage, 45: shouldAnimate, 46: shouldShowDot, 47: inProgressToolCallCount, 48: lookups, 49: isTranscriptMode 50: } = t0; 51: const terminalSize = useTerminalSize(); 52: const [theme] = useTheme(); 53: const bg = useSelectedMessageBg(); 54: const pendingWorkerRequest = useAppStateMaybeOutsideOfProvider(_temp); 55: const isClassifierCheckingRaw = useIsClassifierChecking(param.id); 56: const permissionMode = useAppStateMaybeOutsideOfProvider(_temp2); 57: const hasStrippedRules = useAppStateMaybeOutsideOfProvider(_temp3); 58: const isAutoClassifier = permissionMode === "auto" || permissionMode === "plan" && hasStrippedRules; 59: const isClassifierChecking = false && isClassifierCheckingRaw && permissionMode !== "auto"; 60: let t1; 61: if ($[0] !== param.input || $[1] !== param.name || $[2] !== tools) { 62: bb0: { 63: if (!tools) { 64: t1 = null; 65: break bb0; 66: } 67: const tool = findToolByName(tools, param.name); 68: if (!tool) { 69: t1 = null; 70: break bb0; 71: } 72: const input = tool.inputSchema.safeParse(param.input); 73: const data = input.success ? input.data : undefined; 74: t1 = { 75: tool, 76: input, 77: userFacingToolName: tool.userFacingName(data), 78: userFacingToolNameBackgroundColor: tool.userFacingNameBackgroundColor?.(data), 79: isTransparentWrapper: tool.isTransparentWrapper?.() ?? false 80: }; 81: } 82: $[0] = param.input; 83: $[1] = param.name; 84: $[2] = tools; 85: $[3] = t1; 86: } else { 87: t1 = $[3]; 88: } 89: const parsed = t1; 90: if (!parsed) { 91: logError(new Error(tools ? `Tool ${param.name} not found` : `Tools array is undefined for tool ${param.name}`)); 92: return null; 93: } 94: const { 95: tool: tool_0, 96: input: input_0, 97: userFacingToolName, 98: userFacingToolNameBackgroundColor, 99: isTransparentWrapper 100: } = parsed; 101: let t2; 102: if ($[4] !== lookups.resolvedToolUseIDs || $[5] !== param.id) { 103: t2 = lookups.resolvedToolUseIDs.has(param.id); 104: $[4] = lookups.resolvedToolUseIDs; 105: $[5] = param.id; 106: $[6] = t2; 107: } else { 108: t2 = $[6]; 109: } 110: const isResolved = t2; 111: let t3; 112: if ($[7] !== inProgressToolUseIDs || $[8] !== isResolved || $[9] !== param.id) { 113: t3 = !inProgressToolUseIDs.has(param.id) && !isResolved; 114: $[7] = inProgressToolUseIDs; 115: $[8] = isResolved; 116: $[9] = param.id; 117: $[10] = t3; 118: } else { 119: t3 = $[10]; 120: } 121: const isQueued = t3; 122: const isWaitingForPermission = pendingWorkerRequest?.toolUseId === param.id; 123: if (isTransparentWrapper) { 124: if (isQueued || isResolved) { 125: return null; 126: } 127: let t4; 128: if ($[11] !== inProgressToolCallCount || $[12] !== isTranscriptMode || $[13] !== lookups || $[14] !== param.id || $[15] !== progressMessagesForMessage || $[16] !== terminalSize || $[17] !== tool_0 || $[18] !== tools || $[19] !== verbose) { 129: t4 = renderToolUseProgressMessage(tool_0, tools, lookups, param.id, progressMessagesForMessage, { 130: verbose, 131: inProgressToolCallCount, 132: isTranscriptMode 133: }, terminalSize); 134: $[11] = inProgressToolCallCount; 135: $[12] = isTranscriptMode; 136: $[13] = lookups; 137: $[14] = param.id; 138: $[15] = progressMessagesForMessage; 139: $[16] = terminalSize; 140: $[17] = tool_0; 141: $[18] = tools; 142: $[19] = verbose; 143: $[20] = t4; 144: } else { 145: t4 = $[20]; 146: } 147: let t5; 148: if ($[21] !== bg || $[22] !== t4) { 149: t5 = <Box flexDirection="column" width="100%" backgroundColor={bg}>{t4}</Box>; 150: $[21] = bg; 151: $[22] = t4; 152: $[23] = t5; 153: } else { 154: t5 = $[23]; 155: } 156: return t5; 157: } 158: if (userFacingToolName === "") { 159: return null; 160: } 161: let t4; 162: if ($[24] !== commands || $[25] !== input_0.data || $[26] !== input_0.success || $[27] !== theme || $[28] !== tool_0 || $[29] !== verbose) { 163: t4 = input_0.success ? renderToolUseMessage(tool_0, input_0.data, { 164: theme, 165: verbose, 166: commands 167: }) : null; 168: $[24] = commands; 169: $[25] = input_0.data; 170: $[26] = input_0.success; 171: $[27] = theme; 172: $[28] = tool_0; 173: $[29] = verbose; 174: $[30] = t4; 175: } else { 176: t4 = $[30]; 177: } 178: const renderedToolUseMessage = t4; 179: if (renderedToolUseMessage === null) { 180: return null; 181: } 182: const t5 = addMargin ? 1 : 0; 183: const t6 = stringWidth(userFacingToolName) + (shouldShowDot ? 2 : 0); 184: let t7; 185: if ($[31] !== isQueued || $[32] !== isResolved || $[33] !== lookups.erroredToolUseIDs || $[34] !== param.id || $[35] !== shouldAnimate || $[36] !== shouldShowDot) { 186: t7 = shouldShowDot && (isQueued ? <Box minWidth={2}><Text dimColor={isQueued}>{BLACK_CIRCLE}</Text></Box> : <ToolUseLoader shouldAnimate={shouldAnimate} isUnresolved={!isResolved} isError={lookups.erroredToolUseIDs.has(param.id)} />); 187: $[31] = isQueued; 188: $[32] = isResolved; 189: $[33] = lookups.erroredToolUseIDs; 190: $[34] = param.id; 191: $[35] = shouldAnimate; 192: $[36] = shouldShowDot; 193: $[37] = t7; 194: } else { 195: t7 = $[37]; 196: } 197: const t8 = userFacingToolNameBackgroundColor ? "inverseText" : undefined; 198: let t9; 199: if ($[38] !== t8 || $[39] !== userFacingToolName || $[40] !== userFacingToolNameBackgroundColor) { 200: t9 = <Box flexShrink={0}><Text bold={true} wrap="truncate-end" backgroundColor={userFacingToolNameBackgroundColor} color={t8}>{userFacingToolName}</Text></Box>; 201: $[38] = t8; 202: $[39] = userFacingToolName; 203: $[40] = userFacingToolNameBackgroundColor; 204: $[41] = t9; 205: } else { 206: t9 = $[41]; 207: } 208: let t10; 209: if ($[42] !== renderedToolUseMessage) { 210: t10 = renderedToolUseMessage !== "" && <Box flexWrap="nowrap"><Text>({renderedToolUseMessage})</Text></Box>; 211: $[42] = renderedToolUseMessage; 212: $[43] = t10; 213: } else { 214: t10 = $[43]; 215: } 216: let t11; 217: if ($[44] !== input_0.data || $[45] !== input_0.success || $[46] !== tool_0) { 218: t11 = input_0.success && tool_0.renderToolUseTag && tool_0.renderToolUseTag(input_0.data); 219: $[44] = input_0.data; 220: $[45] = input_0.success; 221: $[46] = tool_0; 222: $[47] = t11; 223: } else { 224: t11 = $[47]; 225: } 226: let t12; 227: if ($[48] !== t10 || $[49] !== t11 || $[50] !== t6 || $[51] !== t7 || $[52] !== t9) { 228: t12 = <Box flexDirection="row" flexWrap="nowrap" minWidth={t6}>{t7}{t9}{t10}{t11}</Box>; 229: $[48] = t10; 230: $[49] = t11; 231: $[50] = t6; 232: $[51] = t7; 233: $[52] = t9; 234: $[53] = t12; 235: } else { 236: t12 = $[53]; 237: } 238: let t13; 239: if ($[54] !== inProgressToolCallCount || $[55] !== isAutoClassifier || $[56] !== isClassifierChecking || $[57] !== isQueued || $[58] !== isResolved || $[59] !== isTranscriptMode || $[60] !== isWaitingForPermission || $[61] !== lookups || $[62] !== param.id || $[63] !== progressMessagesForMessage || $[64] !== terminalSize || $[65] !== tool_0 || $[66] !== tools || $[67] !== verbose) { 240: t13 = !isResolved && !isQueued && (isClassifierChecking ? <MessageResponse height={1}><Text dimColor={true}>{isAutoClassifier ? "Auto classifier checking\u2026" : "Bash classifier checking\u2026"}</Text></MessageResponse> : isWaitingForPermission ? <MessageResponse height={1}><Text dimColor={true}>Waiting for permission…</Text></MessageResponse> : renderToolUseProgressMessage(tool_0, tools, lookups, param.id, progressMessagesForMessage, { 241: verbose, 242: inProgressToolCallCount, 243: isTranscriptMode 244: }, terminalSize)); 245: $[54] = inProgressToolCallCount; 246: $[55] = isAutoClassifier; 247: $[56] = isClassifierChecking; 248: $[57] = isQueued; 249: $[58] = isResolved; 250: $[59] = isTranscriptMode; 251: $[60] = isWaitingForPermission; 252: $[61] = lookups; 253: $[62] = param.id; 254: $[63] = progressMessagesForMessage; 255: $[64] = terminalSize; 256: $[65] = tool_0; 257: $[66] = tools; 258: $[67] = verbose; 259: $[68] = t13; 260: } else { 261: t13 = $[68]; 262: } 263: let t14; 264: if ($[69] !== isQueued || $[70] !== isResolved || $[71] !== tool_0) { 265: t14 = !isResolved && isQueued && renderToolUseQueuedMessage(tool_0); 266: $[69] = isQueued; 267: $[70] = isResolved; 268: $[71] = tool_0; 269: $[72] = t14; 270: } else { 271: t14 = $[72]; 272: } 273: let t15; 274: if ($[73] !== t12 || $[74] !== t13 || $[75] !== t14) { 275: t15 = <Box flexDirection="column">{t12}{t13}{t14}</Box>; 276: $[73] = t12; 277: $[74] = t13; 278: $[75] = t14; 279: $[76] = t15; 280: } else { 281: t15 = $[76]; 282: } 283: let t16; 284: if ($[77] !== bg || $[78] !== t15 || $[79] !== t5) { 285: t16 = <Box flexDirection="row" justifyContent="space-between" marginTop={t5} width="100%" backgroundColor={bg}>{t15}</Box>; 286: $[77] = bg; 287: $[78] = t15; 288: $[79] = t5; 289: $[80] = t16; 290: } else { 291: t16 = $[80]; 292: } 293: return t16; 294: } 295: function _temp3(state_1) { 296: return !!state_1.toolPermissionContext.strippedDangerousRules; 297: } 298: function _temp2(state_0) { 299: return state_0.toolPermissionContext.mode; 300: } 301: function _temp(state) { 302: return state.pendingWorkerRequest; 303: } 304: function renderToolUseMessage(tool: Tool, input: unknown, { 305: theme, 306: verbose, 307: commands 308: }: { 309: theme: ThemeName; 310: verbose: boolean; 311: commands: Command[]; 312: }): React.ReactNode { 313: try { 314: const parsed = tool.inputSchema.safeParse(input); 315: if (!parsed.success) { 316: return ''; 317: } 318: return tool.renderToolUseMessage(parsed.data, { 319: theme, 320: verbose, 321: commands 322: }); 323: } catch (error) { 324: logError(new Error(`Error rendering tool use message for ${tool.name}: ${error}`)); 325: return ''; 326: } 327: } 328: function renderToolUseProgressMessage(tool: Tool, tools: Tools, lookups: ReturnType<typeof buildMessageLookups>, toolUseID: string, progressMessagesForMessage: ProgressMessage[], { 329: verbose, 330: inProgressToolCallCount, 331: isTranscriptMode 332: }: { 333: verbose: boolean; 334: inProgressToolCallCount?: number; 335: isTranscriptMode?: boolean; 336: }, terminalSize: { 337: columns: number; 338: rows: number; 339: }): React.ReactNode { 340: const toolProgressMessages = progressMessagesForMessage.filter((msg): msg is ProgressMessage<ToolProgressData> => msg.data.type !== 'hook_progress'); 341: try { 342: const toolMessages = tool.renderToolUseProgressMessage?.(toolProgressMessages, { 343: tools, 344: verbose, 345: terminalSize, 346: inProgressToolCallCount: inProgressToolCallCount ?? 1, 347: isTranscriptMode 348: }) ?? null; 349: return <> 350: <SentryErrorBoundary> 351: <HookProgressMessage hookEvent="PreToolUse" lookups={lookups} toolUseID={toolUseID} verbose={verbose} isTranscriptMode={isTranscriptMode} /> 352: </SentryErrorBoundary> 353: {toolMessages} 354: </>; 355: } catch (error) { 356: logError(new Error(`Error rendering tool use progress message for ${tool.name}: ${error}`)); 357: return null; 358: } 359: } 360: function renderToolUseQueuedMessage(tool: Tool): React.ReactNode { 361: try { 362: return tool.renderToolUseQueuedMessage?.(); 363: } catch (error) { 364: logError(new Error(`Error rendering tool use queued message for ${tool.name}: ${error}`)); 365: return null; 366: } 367: }

File: src/components/messages/AttachmentMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useMemo } from 'react'; 3: import { Ansi, Box, Text } from '../../ink.js'; 4: import type { Attachment } from 'src/utils/attachments.js'; 5: import type { NullRenderingAttachmentType } from './nullRenderingAttachments.js'; 6: import { useAppState } from '../../state/AppState.js'; 7: import { getDisplayPath } from 'src/utils/file.js'; 8: import { formatFileSize } from 'src/utils/format.js'; 9: import { MessageResponse } from '../MessageResponse.js'; 10: import { basename, sep } from 'path'; 11: import { UserTextMessage } from './UserTextMessage.js'; 12: import { DiagnosticsDisplay } from '../DiagnosticsDisplay.js'; 13: import { getContentText } from 'src/utils/messages.js'; 14: import type { Theme } from 'src/utils/theme.js'; 15: import { UserImageMessage } from './UserImageMessage.js'; 16: import { toInkColor } from '../../utils/ink.js'; 17: import { jsonParse } from '../../utils/slowOperations.js'; 18: import { plural } from '../../utils/stringUtils.js'; 19: import { isEnvTruthy } from '../../utils/envUtils.js'; 20: import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'; 21: import { tryRenderPlanApprovalMessage, formatTeammateMessageContent } from './PlanApprovalMessage.js'; 22: import { BLACK_CIRCLE } from '../../constants/figures.js'; 23: import { TeammateMessageContent } from './UserTeammateMessage.js'; 24: import { isShutdownApproved } from '../../utils/teammateMailbox.js'; 25: import { CtrlOToExpand } from '../CtrlOToExpand.js'; 26: import { FilePathLink } from '../FilePathLink.js'; 27: import { feature } from 'bun:bundle'; 28: import { useSelectedMessageBg } from '../messageActions.js'; 29: type Props = { 30: addMargin: boolean; 31: attachment: Attachment; 32: verbose: boolean; 33: isTranscriptMode?: boolean; 34: }; 35: export function AttachmentMessage({ 36: attachment, 37: addMargin, 38: verbose, 39: isTranscriptMode 40: }: Props): React.ReactNode { 41: const bg = useSelectedMessageBg(); 42: const isDemoEnv = feature('EXPERIMENTAL_SKILL_SEARCH') ? 43: useMemo(() => isEnvTruthy(process.env.IS_DEMO), []) : false; 44: if (isAgentSwarmsEnabled() && attachment.type === 'teammate_mailbox') { 45: const visibleMessages = attachment.messages.filter(msg => { 46: if (isShutdownApproved(msg.text)) { 47: return false; 48: } 49: try { 50: const parsed = jsonParse(msg.text); 51: return parsed?.type !== 'idle_notification' && parsed?.type !== 'teammate_terminated'; 52: } catch { 53: return true; 54: } 55: }); 56: if (visibleMessages.length === 0) { 57: return null; 58: } 59: return <Box flexDirection="column"> 60: {visibleMessages.map((msg_0, idx) => { 61: let parsedMsg: { 62: type?: string; 63: taskId?: string; 64: subject?: string; 65: assignedBy?: string; 66: } | null = null; 67: try { 68: parsedMsg = jsonParse(msg_0.text); 69: } catch { 70: } 71: if (parsedMsg?.type === 'task_assignment') { 72: return <Box key={idx} paddingLeft={2}> 73: <Text>{BLACK_CIRCLE} </Text> 74: <Text>Task assigned: </Text> 75: <Text bold>#{parsedMsg.taskId}</Text> 76: <Text> - {parsedMsg.subject}</Text> 77: <Text dimColor> (from {parsedMsg.assignedBy || msg_0.from})</Text> 78: </Box>; 79: } 80: const planApprovalElement = tryRenderPlanApprovalMessage(msg_0.text, msg_0.from); 81: if (planApprovalElement) { 82: return <React.Fragment key={idx}>{planApprovalElement}</React.Fragment>; 83: } 84: const inkColor = toInkColor(msg_0.color); 85: const formattedContent = formatTeammateMessageContent(msg_0.text) ?? msg_0.text; 86: return <TeammateMessageContent key={idx} displayName={msg_0.from} inkColor={inkColor} content={formattedContent} summary={msg_0.summary} isTranscriptMode={isTranscriptMode} />; 87: })} 88: </Box>; 89: } 90: if (feature('EXPERIMENTAL_SKILL_SEARCH')) { 91: if (attachment.type === 'skill_discovery') { 92: if (attachment.skills.length === 0) return null; 93: const names = attachment.skills.map(s => s.shortId ? `${s.name} [${s.shortId}]` : s.name).join(', '); 94: const firstId = attachment.skills[0]?.shortId; 95: const hint = "external" === 'ant' && !isDemoEnv && firstId ? ` · /skill-feedback ${firstId} 1=wrong 2=noisy 3=good [comment]` : ''; 96: return <Line> 97: <Text bold>{attachment.skills.length}</Text> relevant{' '} 98: {plural(attachment.skills.length, 'skill')}: {names} 99: {hint && <Text dimColor>{hint}</Text>} 100: </Line>; 101: } 102: } 103: switch (attachment.type) { 104: case 'directory': 105: return <Line> 106: Listed directory <Text bold>{attachment.displayPath + sep}</Text> 107: </Line>; 108: case 'file': 109: case 'already_read_file': 110: if (attachment.content.type === 'notebook') { 111: return <Line> 112: Read <Text bold>{attachment.displayPath}</Text> ( 113: {attachment.content.file.cells.length} cells) 114: </Line>; 115: } 116: if (attachment.content.type === 'file_unchanged') { 117: return <Line> 118: Read <Text bold>{attachment.displayPath}</Text> (unchanged) 119: </Line>; 120: } 121: return <Line> 122: Read <Text bold>{attachment.displayPath}</Text> ( 123: {attachment.content.type === 'text' ? `${attachment.content.file.numLines}${attachment.truncated ? '+' : ''} lines` : formatFileSize(attachment.content.file.originalSize)} 124: ) 125: </Line>; 126: case 'compact_file_reference': 127: return <Line> 128: Referenced file <Text bold>{attachment.displayPath}</Text> 129: </Line>; 130: case 'pdf_reference': 131: return <Line> 132: Referenced PDF <Text bold>{attachment.displayPath}</Text> ( 133: {attachment.pageCount} pages) 134: </Line>; 135: case 'selected_lines_in_ide': 136: return <Line> 137: ⧉ Selected{' '} 138: <Text bold>{attachment.lineEnd - attachment.lineStart + 1}</Text>{' '} 139: lines from <Text bold>{attachment.displayPath}</Text> in{' '} 140: {attachment.ideName} 141: </Line>; 142: case 'nested_memory': 143: return <Line> 144: Loaded <Text bold>{attachment.displayPath}</Text> 145: </Line>; 146: case 'relevant_memories': 147: return <Box flexDirection="column" marginTop={addMargin ? 1 : 0} backgroundColor={bg}> 148: <Box flexDirection="row"> 149: <Box minWidth={2} /> 150: <Text dimColor> 151: Recalled <Text bold>{attachment.memories.length}</Text>{' '} 152: {attachment.memories.length === 1 ? 'memory' : 'memories'} 153: {!isTranscriptMode && <> 154: {' '} 155: <CtrlOToExpand /> 156: </>} 157: </Text> 158: </Box> 159: {(verbose || isTranscriptMode) && attachment.memories.map(m => <Box key={m.path} flexDirection="column"> 160: <MessageResponse> 161: <Text dimColor> 162: <FilePathLink filePath={m.path}> 163: {basename(m.path)} 164: </FilePathLink> 165: </Text> 166: </MessageResponse> 167: {isTranscriptMode && <Box paddingLeft={5}> 168: <Text> 169: <Ansi>{m.content}</Ansi> 170: </Text> 171: </Box>} 172: </Box>)} 173: </Box>; 174: case 'dynamic_skill': 175: { 176: const skillCount = attachment.skillNames.length; 177: return <Line> 178: Loaded{' '} 179: <Text bold> 180: {skillCount} {plural(skillCount, 'skill')} 181: </Text>{' '} 182: from <Text bold>{attachment.displayPath}</Text> 183: </Line>; 184: } 185: case 'skill_listing': 186: { 187: if (attachment.isInitial) { 188: return null; 189: } 190: return <Line> 191: <Text bold>{attachment.skillCount}</Text>{' '} 192: {plural(attachment.skillCount, 'skill')} available 193: </Line>; 194: } 195: case 'agent_listing_delta': 196: { 197: if (attachment.isInitial || attachment.addedTypes.length === 0) { 198: return null; 199: } 200: const count = attachment.addedTypes.length; 201: return <Line> 202: <Text bold>{count}</Text> agent {plural(count, 'type')} available 203: </Line>; 204: } 205: case 'queued_command': 206: { 207: const text = typeof attachment.prompt === 'string' ? attachment.prompt : getContentText(attachment.prompt) || ''; 208: const hasImages = attachment.imagePasteIds && attachment.imagePasteIds.length > 0; 209: return <Box flexDirection="column"> 210: <UserTextMessage addMargin={addMargin} param={{ 211: text, 212: type: 'text' 213: }} verbose={verbose} isTranscriptMode={isTranscriptMode} /> 214: {hasImages && attachment.imagePasteIds?.map(id => <UserImageMessage key={id} imageId={id} />)} 215: </Box>; 216: } 217: case 'plan_file_reference': 218: return <Line> 219: Plan file referenced ({getDisplayPath(attachment.planFilePath)}) 220: </Line>; 221: case 'invoked_skills': 222: { 223: if (attachment.skills.length === 0) { 224: return null; 225: } 226: const skillNames = attachment.skills.map(s_0 => s_0.name).join(', '); 227: return <Line>Skills restored ({skillNames})</Line>; 228: } 229: case 'diagnostics': 230: return <DiagnosticsDisplay attachment={attachment} verbose={verbose} />; 231: case 'mcp_resource': 232: return <Line> 233: Read MCP resource <Text bold>{attachment.name}</Text> from{' '} 234: {attachment.server} 235: </Line>; 236: case 'command_permissions': 237: return null; 238: case 'async_hook_response': 239: { 240: if (attachment.hookEvent === 'SessionStart' && !verbose) { 241: return null; 242: } 243: if (!verbose && !isTranscriptMode) { 244: return null; 245: } 246: return <Line> 247: Async hook <Text bold>{attachment.hookEvent}</Text> completed 248: </Line>; 249: } 250: case 'hook_blocking_error': 251: { 252: if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') { 253: return null; 254: } 255: const stderr = attachment.blockingError.blockingError.trim(); 256: return <> 257: <Line color="error"> 258: {attachment.hookName} hook returned blocking error 259: </Line> 260: {stderr ? <Line color="error">{stderr}</Line> : null} 261: </>; 262: } 263: case 'hook_non_blocking_error': 264: { 265: if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') { 266: return null; 267: } 268: return <Line color="error">{attachment.hookName} hook error</Line>; 269: } 270: case 'hook_error_during_execution': 271: if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') { 272: return null; 273: } 274: return <Line>{attachment.hookName} hook warning</Line>; 275: case 'hook_success': 276: return null; 277: case 'hook_stopped_continuation': 278: if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') { 279: return null; 280: } 281: return <Line color="warning"> 282: {attachment.hookName} hook stopped continuation: {attachment.message} 283: </Line>; 284: case 'hook_system_message': 285: return <Line> 286: {attachment.hookName} says: {attachment.content} 287: </Line>; 288: case 'hook_permission_decision': 289: { 290: const action = attachment.decision === 'allow' ? 'Allowed' : 'Denied'; 291: return <Line> 292: {action} by <Text bold>{attachment.hookEvent}</Text> hook 293: </Line>; 294: } 295: case 'task_status': 296: return <TaskStatusMessage attachment={attachment} />; 297: case 'teammate_shutdown_batch': 298: return <Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}> 299: <Text dimColor>{BLACK_CIRCLE} </Text> 300: <Text dimColor> 301: {attachment.count} {plural(attachment.count, 'teammate')} shut down 302: gracefully 303: </Text> 304: </Box>; 305: default: 306: attachment.type satisfies NullRenderingAttachmentType | 'skill_discovery' | 'teammate_mailbox'; 307: return null; 308: } 309: } 310: type TaskStatusAttachment = Extract<Attachment, { 311: type: 'task_status'; 312: }>; 313: function TaskStatusMessage(t0) { 314: const $ = _c(4); 315: const { 316: attachment 317: } = t0; 318: if (false && attachment.status === "killed") { 319: return null; 320: } 321: if (isAgentSwarmsEnabled() && attachment.taskType === "in_process_teammate") { 322: let t1; 323: if ($[0] !== attachment) { 324: t1 = <TeammateTaskStatus attachment={attachment} />; 325: $[0] = attachment; 326: $[1] = t1; 327: } else { 328: t1 = $[1]; 329: } 330: return t1; 331: } 332: let t1; 333: if ($[2] !== attachment) { 334: t1 = <GenericTaskStatus attachment={attachment} />; 335: $[2] = attachment; 336: $[3] = t1; 337: } else { 338: t1 = $[3]; 339: } 340: return t1; 341: } 342: function GenericTaskStatus(t0) { 343: const $ = _c(9); 344: const { 345: attachment 346: } = t0; 347: const bg = useSelectedMessageBg(); 348: const statusText = attachment.status === "completed" ? "completed in background" : attachment.status === "killed" ? "stopped" : attachment.status === "running" ? "still running in background" : attachment.status; 349: let t1; 350: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 351: t1 = <Text dimColor={true}>{BLACK_CIRCLE} </Text>; 352: $[0] = t1; 353: } else { 354: t1 = $[0]; 355: } 356: let t2; 357: if ($[1] !== attachment.description) { 358: t2 = <Text bold={true}>{attachment.description}</Text>; 359: $[1] = attachment.description; 360: $[2] = t2; 361: } else { 362: t2 = $[2]; 363: } 364: let t3; 365: if ($[3] !== statusText || $[4] !== t2) { 366: t3 = <Text dimColor={true}>Task "{t2}" {statusText}</Text>; 367: $[3] = statusText; 368: $[4] = t2; 369: $[5] = t3; 370: } else { 371: t3 = $[5]; 372: } 373: let t4; 374: if ($[6] !== bg || $[7] !== t3) { 375: t4 = <Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>{t1}{t3}</Box>; 376: $[6] = bg; 377: $[7] = t3; 378: $[8] = t4; 379: } else { 380: t4 = $[8]; 381: } 382: return t4; 383: } 384: function TeammateTaskStatus(t0) { 385: const $ = _c(16); 386: const { 387: attachment 388: } = t0; 389: const bg = useSelectedMessageBg(); 390: let t1; 391: if ($[0] !== attachment.taskId) { 392: t1 = s => s.tasks[attachment.taskId]; 393: $[0] = attachment.taskId; 394: $[1] = t1; 395: } else { 396: t1 = $[1]; 397: } 398: const task = useAppState(t1); 399: if (task?.type !== "in_process_teammate") { 400: let t2; 401: if ($[2] !== attachment) { 402: t2 = <GenericTaskStatus attachment={attachment} />; 403: $[2] = attachment; 404: $[3] = t2; 405: } else { 406: t2 = $[3]; 407: } 408: return t2; 409: } 410: let t2; 411: if ($[4] !== task.identity.color) { 412: t2 = toInkColor(task.identity.color); 413: $[4] = task.identity.color; 414: $[5] = t2; 415: } else { 416: t2 = $[5]; 417: } 418: const agentColor = t2; 419: const statusText = attachment.status === "completed" ? "shut down gracefully" : attachment.status; 420: let t3; 421: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 422: t3 = <Text dimColor={true}>{BLACK_CIRCLE} </Text>; 423: $[6] = t3; 424: } else { 425: t3 = $[6]; 426: } 427: let t4; 428: if ($[7] !== agentColor || $[8] !== task.identity.agentName) { 429: t4 = <Text color={agentColor} bold={true} dimColor={false}>@{task.identity.agentName}</Text>; 430: $[7] = agentColor; 431: $[8] = task.identity.agentName; 432: $[9] = t4; 433: } else { 434: t4 = $[9]; 435: } 436: let t5; 437: if ($[10] !== statusText || $[11] !== t4) { 438: t5 = <Text dimColor={true}>Teammate{" "}{t4}{" "}{statusText}</Text>; 439: $[10] = statusText; 440: $[11] = t4; 441: $[12] = t5; 442: } else { 443: t5 = $[12]; 444: } 445: let t6; 446: if ($[13] !== bg || $[14] !== t5) { 447: t6 = <Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>{t3}{t5}</Box>; 448: $[13] = bg; 449: $[14] = t5; 450: $[15] = t6; 451: } else { 452: t6 = $[15]; 453: } 454: return t6; 455: } 456: function Line(t0) { 457: const $ = _c(7); 458: const { 459: dimColor: t1, 460: children, 461: color 462: } = t0; 463: const dimColor = t1 === undefined ? true : t1; 464: const bg = useSelectedMessageBg(); 465: let t2; 466: if ($[0] !== children || $[1] !== color || $[2] !== dimColor) { 467: t2 = <MessageResponse><Text color={color} dimColor={dimColor} wrap="wrap">{children}</Text></MessageResponse>; 468: $[0] = children; 469: $[1] = color; 470: $[2] = dimColor; 471: $[3] = t2; 472: } else { 473: t2 = $[3]; 474: } 475: let t3; 476: if ($[4] !== bg || $[5] !== t2) { 477: t3 = <Box backgroundColor={bg}>{t2}</Box>; 478: $[4] = bg; 479: $[5] = t2; 480: $[6] = t3; 481: } else { 482: t3 = $[6]; 483: } 484: return t3; 485: }

File: src/components/messages/CollapsedReadSearchContent.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import { basename } from 'path'; 4: import React, { useRef } from 'react'; 5: import { useMinDisplayTime } from '../../hooks/useMinDisplayTime.js'; 6: import { Ansi, Box, Text, useTheme } from '../../ink.js'; 7: import { findToolByName, type Tools } from '../../Tool.js'; 8: import { getReplPrimitiveTools } from '../../tools/REPLTool/primitiveTools.js'; 9: import type { CollapsedReadSearchGroup, NormalizedAssistantMessage } from '../../types/message.js'; 10: import { uniq } from '../../utils/array.js'; 11: import { getToolUseIdsFromCollapsedGroup } from '../../utils/collapseReadSearch.js'; 12: import { getDisplayPath } from '../../utils/file.js'; 13: import { formatDuration, formatSecondsShort } from '../../utils/format.js'; 14: import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; 15: import type { buildMessageLookups } from '../../utils/messages.js'; 16: import type { ThemeName } from '../../utils/theme.js'; 17: import { CtrlOToExpand } from '../CtrlOToExpand.js'; 18: import { useSelectedMessageBg } from '../messageActions.js'; 19: import { PrBadge } from '../PrBadge.js'; 20: import { ToolUseLoader } from '../ToolUseLoader.js'; 21: const teamMemCollapsed = feature('TEAMMEM') ? require('./teamMemCollapsed.js') as typeof import('./teamMemCollapsed.js') : null; 22: const MIN_HINT_DISPLAY_MS = 700; 23: type Props = { 24: message: CollapsedReadSearchGroup; 25: inProgressToolUseIDs: Set<string>; 26: shouldAnimate: boolean; 27: verbose: boolean; 28: tools: Tools; 29: lookups: ReturnType<typeof buildMessageLookups>; 30: isActiveGroup?: boolean; 31: }; 32: function VerboseToolUse(t0) { 33: const $ = _c(24); 34: const { 35: content, 36: tools, 37: lookups, 38: inProgressToolUseIDs, 39: shouldAnimate, 40: theme 41: } = t0; 42: const bg = useSelectedMessageBg(); 43: let t1; 44: let t2; 45: if ($[0] !== bg || $[1] !== content.id || $[2] !== content.input || $[3] !== content.name || $[4] !== inProgressToolUseIDs || $[5] !== lookups || $[6] !== shouldAnimate || $[7] !== theme || $[8] !== tools) { 46: t2 = Symbol.for("react.early_return_sentinel"); 47: bb0: { 48: const tool = findToolByName(tools, content.name) ?? findToolByName(getReplPrimitiveTools(), content.name); 49: if (!tool) { 50: t2 = null; 51: break bb0; 52: } 53: let t3; 54: if ($[11] !== content.id || $[12] !== lookups.resolvedToolUseIDs) { 55: t3 = lookups.resolvedToolUseIDs.has(content.id); 56: $[11] = content.id; 57: $[12] = lookups.resolvedToolUseIDs; 58: $[13] = t3; 59: } else { 60: t3 = $[13]; 61: } 62: const isResolved = t3; 63: let t4; 64: if ($[14] !== content.id || $[15] !== lookups.erroredToolUseIDs) { 65: t4 = lookups.erroredToolUseIDs.has(content.id); 66: $[14] = content.id; 67: $[15] = lookups.erroredToolUseIDs; 68: $[16] = t4; 69: } else { 70: t4 = $[16]; 71: } 72: const isError = t4; 73: let t5; 74: if ($[17] !== content.id || $[18] !== inProgressToolUseIDs) { 75: t5 = inProgressToolUseIDs.has(content.id); 76: $[17] = content.id; 77: $[18] = inProgressToolUseIDs; 78: $[19] = t5; 79: } else { 80: t5 = $[19]; 81: } 82: const isInProgress = t5; 83: const resultMsg = lookups.toolResultByToolUseID.get(content.id); 84: const rawToolResult = resultMsg?.type === "user" ? resultMsg.toolUseResult : undefined; 85: const parsedOutput = tool.outputSchema?.safeParse(rawToolResult); 86: const toolResult = parsedOutput?.success ? parsedOutput.data : undefined; 87: const parsedInput = tool.inputSchema.safeParse(content.input); 88: const input = parsedInput.success ? parsedInput.data : undefined; 89: const userFacingName = tool.userFacingName(input); 90: const toolUseMessage = input ? tool.renderToolUseMessage(input, { 91: theme, 92: verbose: true 93: }) : null; 94: const t6 = shouldAnimate && isInProgress; 95: const t7 = !isResolved; 96: let t8; 97: if ($[20] !== isError || $[21] !== t6 || $[22] !== t7) { 98: t8 = <ToolUseLoader shouldAnimate={t6} isUnresolved={t7} isError={isError} />; 99: $[20] = isError; 100: $[21] = t6; 101: $[22] = t7; 102: $[23] = t8; 103: } else { 104: t8 = $[23]; 105: } 106: t1 = <Box key={content.id} flexDirection="column" marginTop={1} backgroundColor={bg}><Box flexDirection="row">{t8}<Text><Text bold={true}>{userFacingName}</Text>{toolUseMessage && <Text>({toolUseMessage})</Text>}</Text>{input && tool.renderToolUseTag?.(input)}</Box>{isResolved && !isError && toolResult !== undefined && <Box>{tool.renderToolResultMessage?.(toolResult, [], { 107: verbose: true, 108: tools, 109: theme 110: })}</Box>}</Box>; 111: } 112: $[0] = bg; 113: $[1] = content.id; 114: $[2] = content.input; 115: $[3] = content.name; 116: $[4] = inProgressToolUseIDs; 117: $[5] = lookups; 118: $[6] = shouldAnimate; 119: $[7] = theme; 120: $[8] = tools; 121: $[9] = t1; 122: $[10] = t2; 123: } else { 124: t1 = $[9]; 125: t2 = $[10]; 126: } 127: if (t2 !== Symbol.for("react.early_return_sentinel")) { 128: return t2; 129: } 130: return t1; 131: } 132: export function CollapsedReadSearchContent({ 133: message, 134: inProgressToolUseIDs, 135: shouldAnimate, 136: verbose, 137: tools, 138: lookups, 139: isActiveGroup 140: }: Props): React.ReactNode { 141: const bg = useSelectedMessageBg(); 142: const { 143: searchCount: rawSearchCount, 144: readCount: rawReadCount, 145: listCount: rawListCount, 146: replCount, 147: memorySearchCount, 148: memoryReadCount, 149: memoryWriteCount, 150: messages: groupMessages 151: } = message; 152: const [theme] = useTheme(); 153: const toolUseIds = getToolUseIdsFromCollapsedGroup(message); 154: const anyError = toolUseIds.some(id => lookups.erroredToolUseIDs.has(id)); 155: const hasMemoryOps = memorySearchCount > 0 || memoryReadCount > 0 || memoryWriteCount > 0; 156: const hasTeamMemoryOps = feature('TEAMMEM') ? teamMemCollapsed!.checkHasTeamMemOps(message) : false; 157: const maxReadCountRef = useRef(0); 158: const maxSearchCountRef = useRef(0); 159: const maxListCountRef = useRef(0); 160: const maxMcpCountRef = useRef(0); 161: const maxBashCountRef = useRef(0); 162: maxReadCountRef.current = Math.max(maxReadCountRef.current, rawReadCount); 163: maxSearchCountRef.current = Math.max(maxSearchCountRef.current, rawSearchCount); 164: maxListCountRef.current = Math.max(maxListCountRef.current, rawListCount); 165: maxMcpCountRef.current = Math.max(maxMcpCountRef.current, message.mcpCallCount ?? 0); 166: maxBashCountRef.current = Math.max(maxBashCountRef.current, message.bashCount ?? 0); 167: const readCount = maxReadCountRef.current; 168: const searchCount = maxSearchCountRef.current; 169: const listCount = maxListCountRef.current; 170: const mcpCallCount = maxMcpCountRef.current; 171: const gitOpBashCount = message.gitOpBashCount ?? 0; 172: const bashCount = isFullscreenEnvEnabled() ? Math.max(0, maxBashCountRef.current - gitOpBashCount) : 0; 173: const hasNonMemoryOps = searchCount > 0 || readCount > 0 || listCount > 0 || replCount > 0 || mcpCallCount > 0 || bashCount > 0 || gitOpBashCount > 0; 174: const readPaths = message.readFilePaths; 175: const searchArgs = message.searchArgs; 176: let incomingHint = message.latestDisplayHint; 177: if (incomingHint === undefined) { 178: const lastSearchRaw = searchArgs?.at(-1); 179: const lastSearch = lastSearchRaw !== undefined ? `"${lastSearchRaw}"` : undefined; 180: const lastRead = readPaths?.at(-1); 181: incomingHint = lastRead !== undefined ? getDisplayPath(lastRead) : lastSearch; 182: } 183: if (isActiveGroup) { 184: for (const id_0 of toolUseIds) { 185: if (!inProgressToolUseIDs.has(id_0)) continue; 186: const latest = lookups.progressMessagesByToolUseID.get(id_0)?.at(-1)?.data; 187: if (latest?.type === 'repl_tool_call' && latest.phase === 'start') { 188: const input = latest.toolInput as { 189: command?: string; 190: pattern?: string; 191: file_path?: string; 192: }; 193: incomingHint = input.file_path ?? (input.pattern ? `"${input.pattern}"` : undefined) ?? input.command ?? latest.toolName; 194: } 195: } 196: } 197: const displayedHint = useMinDisplayTime(incomingHint, MIN_HINT_DISPLAY_MS); 198: if (verbose) { 199: const toolUses: NormalizedAssistantMessage[] = []; 200: for (const msg of groupMessages) { 201: if (msg.type === 'assistant') { 202: toolUses.push(msg); 203: } else if (msg.type === 'grouped_tool_use') { 204: toolUses.push(...msg.messages); 205: } 206: } 207: return <Box flexDirection="column"> 208: {toolUses.map(msg_0 => { 209: const content = msg_0.message.content[0]; 210: if (content?.type !== 'tool_use') return null; 211: return <VerboseToolUse key={content.id} content={content} tools={tools} lookups={lookups} inProgressToolUseIDs={inProgressToolUseIDs} shouldAnimate={shouldAnimate} theme={theme} />; 212: })} 213: {message.hookInfos && message.hookInfos.length > 0 && <> 214: <Text dimColor> 215: {' ⎿ '}Ran {message.hookCount} PreToolUse{' '} 216: {message.hookCount === 1 ? 'hook' : 'hooks'} ( 217: {formatSecondsShort(message.hookTotalMs ?? 0)}) 218: </Text> 219: {message.hookInfos.map((info, idx) => <Text key={`hook-${idx}`} dimColor> 220: {' ⎿ '} 221: {info.command} ({formatSecondsShort(info.durationMs ?? 0)}) 222: </Text>)} 223: </>} 224: {message.relevantMemories?.map(m => <Box key={m.path} flexDirection="column" marginTop={1}> 225: <Text dimColor> 226: {' ⎿ '}Recalled {basename(m.path)} 227: </Text> 228: <Box paddingLeft={5}> 229: <Text> 230: <Ansi>{m.content}</Ansi> 231: </Text> 232: </Box> 233: </Box>)} 234: </Box>; 235: } 236: if (!hasMemoryOps && !hasTeamMemoryOps && !hasNonMemoryOps) { 237: return null; 238: } 239: let shellProgressSuffix = ''; 240: if (isFullscreenEnvEnabled() && isActiveGroup) { 241: let elapsed: number | undefined; 242: let lines = 0; 243: for (const id_1 of toolUseIds) { 244: if (!inProgressToolUseIDs.has(id_1)) continue; 245: const data = lookups.progressMessagesByToolUseID.get(id_1)?.at(-1)?.data; 246: if (data?.type !== 'bash_progress' && data?.type !== 'powershell_progress') { 247: continue; 248: } 249: if (elapsed === undefined || data.elapsedTimeSeconds > elapsed) { 250: elapsed = data.elapsedTimeSeconds; 251: lines = data.totalLines; 252: } 253: } 254: if (elapsed !== undefined && elapsed >= 2) { 255: const time = formatDuration(elapsed * 1000); 256: shellProgressSuffix = lines > 0 ? ` (${time} · ${lines} ${lines === 1 ? 'line' : 'lines'})` : ` (${time})`; 257: } 258: } 259: const nonMemParts: React.ReactNode[] = []; 260: function pushPart(key: string, verb: string, body: React.ReactNode): void { 261: const isFirst = nonMemParts.length === 0; 262: if (!isFirst) nonMemParts.push(<Text key={`comma-${key}`}>, </Text>); 263: nonMemParts.push(<Text key={key}> 264: {isFirst ? verb[0]!.toUpperCase() + verb.slice(1) : verb} {body} 265: </Text>); 266: } 267: if (isFullscreenEnvEnabled() && message.commits?.length) { 268: const byKind = { 269: committed: 'committed', 270: amended: 'amended commit', 271: 'cherry-picked': 'cherry-picked' 272: }; 273: for (const kind of ['committed', 'amended', 'cherry-picked'] as const) { 274: const shas = message.commits.filter(c => c.kind === kind).map(c_0 => c_0.sha); 275: if (shas.length) { 276: pushPart(kind, byKind[kind], <Text bold>{shas.join(', ')}</Text>); 277: } 278: } 279: } 280: if (isFullscreenEnvEnabled() && message.pushes?.length) { 281: const branches = uniq(message.pushes.map(p => p.branch)); 282: pushPart('push', 'pushed to', <Text bold>{branches.join(', ')}</Text>); 283: } 284: if (isFullscreenEnvEnabled() && message.branches?.length) { 285: const byAction = { 286: merged: 'merged', 287: rebased: 'rebased onto' 288: }; 289: for (const b of message.branches) { 290: pushPart(`br-${b.action}-${b.ref}`, byAction[b.action], <Text bold>{b.ref}</Text>); 291: } 292: } 293: if (isFullscreenEnvEnabled() && message.prs?.length) { 294: const verbs = { 295: created: 'created', 296: edited: 'edited', 297: merged: 'merged', 298: commented: 'commented on', 299: closed: 'closed', 300: ready: 'marked ready' 301: }; 302: for (const pr of message.prs) { 303: pushPart(`pr-${pr.action}-${pr.number}`, verbs[pr.action], pr.url ? <PrBadge number={pr.number} url={pr.url} bold /> : <Text bold>PR #{pr.number}</Text>); 304: } 305: } 306: if (searchCount > 0) { 307: const isFirst_0 = nonMemParts.length === 0; 308: const searchVerb = isActiveGroup ? isFirst_0 ? 'Searching for' : 'searching for' : isFirst_0 ? 'Searched for' : 'searched for'; 309: if (!isFirst_0) { 310: nonMemParts.push(<Text key="comma-s">, </Text>); 311: } 312: nonMemParts.push(<Text key="search"> 313: {searchVerb} <Text bold>{searchCount}</Text>{' '} 314: {searchCount === 1 ? 'pattern' : 'patterns'} 315: </Text>); 316: } 317: if (readCount > 0) { 318: const isFirst_1 = nonMemParts.length === 0; 319: const readVerb = isActiveGroup ? isFirst_1 ? 'Reading' : 'reading' : isFirst_1 ? 'Read' : 'read'; 320: if (!isFirst_1) { 321: nonMemParts.push(<Text key="comma-r">, </Text>); 322: } 323: nonMemParts.push(<Text key="read"> 324: {readVerb} <Text bold>{readCount}</Text>{' '} 325: {readCount === 1 ? 'file' : 'files'} 326: </Text>); 327: } 328: if (listCount > 0) { 329: const isFirst_2 = nonMemParts.length === 0; 330: const listVerb = isActiveGroup ? isFirst_2 ? 'Listing' : 'listing' : isFirst_2 ? 'Listed' : 'listed'; 331: if (!isFirst_2) { 332: nonMemParts.push(<Text key="comma-l">, </Text>); 333: } 334: nonMemParts.push(<Text key="list"> 335: {listVerb} <Text bold>{listCount}</Text>{' '} 336: {listCount === 1 ? 'directory' : 'directories'} 337: </Text>); 338: } 339: if (replCount > 0) { 340: const replVerb = isActiveGroup ? "REPL'ing" : "REPL'd"; 341: if (nonMemParts.length > 0) { 342: nonMemParts.push(<Text key="comma-repl">, </Text>); 343: } 344: nonMemParts.push(<Text key="repl"> 345: {replVerb} <Text bold>{replCount}</Text>{' '} 346: {replCount === 1 ? 'time' : 'times'} 347: </Text>); 348: } 349: if (mcpCallCount > 0) { 350: const serverLabel = message.mcpServerNames?.map(n => n.replace(/^claude\.ai /, '')).join(', ') || 'MCP'; 351: const isFirst_3 = nonMemParts.length === 0; 352: const verb_0 = isActiveGroup ? isFirst_3 ? 'Querying' : 'querying' : isFirst_3 ? 'Queried' : 'queried'; 353: if (!isFirst_3) { 354: nonMemParts.push(<Text key="comma-mcp">, </Text>); 355: } 356: nonMemParts.push(<Text key="mcp"> 357: {verb_0} {serverLabel} 358: {mcpCallCount > 1 && <> 359: {' '} 360: <Text bold>{mcpCallCount}</Text> times 361: </>} 362: </Text>); 363: } 364: if (isFullscreenEnvEnabled() && bashCount > 0) { 365: const isFirst_4 = nonMemParts.length === 0; 366: const verb_1 = isActiveGroup ? isFirst_4 ? 'Running' : 'running' : isFirst_4 ? 'Ran' : 'ran'; 367: if (!isFirst_4) { 368: nonMemParts.push(<Text key="comma-bash">, </Text>); 369: } 370: nonMemParts.push(<Text key="bash"> 371: {verb_1} <Text bold>{bashCount}</Text> bash{' '} 372: {bashCount === 1 ? 'command' : 'commands'} 373: </Text>); 374: } 375: const hasPrecedingNonMem = nonMemParts.length > 0; 376: const memParts: React.ReactNode[] = []; 377: if (memoryReadCount > 0) { 378: const isFirst_5 = !hasPrecedingNonMem && memParts.length === 0; 379: const verb_2 = isActiveGroup ? isFirst_5 ? 'Recalling' : 'recalling' : isFirst_5 ? 'Recalled' : 'recalled'; 380: if (!isFirst_5) { 381: memParts.push(<Text key="comma-mr">, </Text>); 382: } 383: memParts.push(<Text key="mem-read"> 384: {verb_2} <Text bold>{memoryReadCount}</Text>{' '} 385: {memoryReadCount === 1 ? 'memory' : 'memories'} 386: </Text>); 387: } 388: if (memorySearchCount > 0) { 389: const isFirst_6 = !hasPrecedingNonMem && memParts.length === 0; 390: const verb_3 = isActiveGroup ? isFirst_6 ? 'Searching' : 'searching' : isFirst_6 ? 'Searched' : 'searched'; 391: if (!isFirst_6) { 392: memParts.push(<Text key="comma-ms">, </Text>); 393: } 394: memParts.push(<Text key="mem-search">{`${verb_3} memories`}</Text>); 395: } 396: if (memoryWriteCount > 0) { 397: const isFirst_7 = !hasPrecedingNonMem && memParts.length === 0; 398: const verb_4 = isActiveGroup ? isFirst_7 ? 'Writing' : 'writing' : isFirst_7 ? 'Wrote' : 'wrote'; 399: if (!isFirst_7) { 400: memParts.push(<Text key="comma-mw">, </Text>); 401: } 402: memParts.push(<Text key="mem-write"> 403: {verb_4} <Text bold>{memoryWriteCount}</Text>{' '} 404: {memoryWriteCount === 1 ? 'memory' : 'memories'} 405: </Text>); 406: } 407: return <Box flexDirection="column" marginTop={1} backgroundColor={bg}> 408: <Box flexDirection="row"> 409: {isActiveGroup ? <ToolUseLoader shouldAnimate isUnresolved isError={anyError} /> : <Box minWidth={2} />} 410: <Text dimColor={!isActiveGroup}> 411: {nonMemParts} 412: {memParts} 413: {feature('TEAMMEM') ? teamMemCollapsed!.TeamMemCountParts({ 414: message, 415: isActiveGroup, 416: hasPrecedingParts: hasPrecedingNonMem || memParts.length > 0 417: }) : null} 418: {isActiveGroup && <Text key="ellipsis">…</Text>} <CtrlOToExpand /> 419: </Text> 420: </Box> 421: {isActiveGroup && displayedHint !== undefined && 422: <Box flexDirection="row"> 423: <Box width={5} flexShrink={0}> 424: <Text dimColor>{' ⎿ '}</Text> 425: </Box> 426: <Box flexDirection="column" flexGrow={1}> 427: {displayedHint.split('\n').map((line, i, arr) => <Text key={`hint-${i}`} dimColor> 428: {line} 429: {i === arr.length - 1 && shellProgressSuffix} 430: </Text>)} 431: </Box> 432: </Box>} 433: {message.hookTotalMs !== undefined && message.hookTotalMs > 0 && <Text dimColor> 434: {' ⎿ '}Ran {message.hookCount} PreToolUse{' '} 435: {message.hookCount === 1 ? 'hook' : 'hooks'} ( 436: {formatSecondsShort(message.hookTotalMs)}) 437: </Text>} 438: </Box>; 439: }

File: src/components/messages/CompactBoundaryMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'; 5: export function CompactBoundaryMessage() { 6: const $ = _c(2); 7: const historyShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o"); 8: let t0; 9: if ($[0] !== historyShortcut) { 10: t0 = <Box marginY={1}><Text dimColor={true}>✻ Conversation compacted ({historyShortcut} for history)</Text></Box>; 11: $[0] = historyShortcut; 12: $[1] = t0; 13: } else { 14: t0 = $[1]; 15: } 16: return t0; 17: }

File: src/components/messages/GroupedToolUseContent.tsx

typescript 1: import type { ToolResultBlockParam, ToolUseBlockParam } from '@anthropic-ai/sdk/resources/messages/messages.mjs'; 2: import * as React from 'react'; 3: import { filterToolProgressMessages, findToolByName, type Tools } from '../../Tool.js'; 4: import type { GroupedToolUseMessage } from '../../types/message.js'; 5: import type { buildMessageLookups } from '../../utils/messages.js'; 6: type Props = { 7: message: GroupedToolUseMessage; 8: tools: Tools; 9: lookups: ReturnType<typeof buildMessageLookups>; 10: inProgressToolUseIDs: Set<string>; 11: shouldAnimate: boolean; 12: }; 13: export function GroupedToolUseContent({ 14: message, 15: tools, 16: lookups, 17: inProgressToolUseIDs, 18: shouldAnimate 19: }: Props): React.ReactNode { 20: const tool = findToolByName(tools, message.toolName); 21: if (!tool?.renderGroupedToolUse) { 22: return null; 23: } 24: const resultsByToolUseId = new Map<string, { 25: param: ToolResultBlockParam; 26: output: unknown; 27: }>(); 28: for (const resultMsg of message.results) { 29: for (const content of resultMsg.message.content) { 30: if (content.type === 'tool_result') { 31: resultsByToolUseId.set(content.tool_use_id, { 32: param: content, 33: output: resultMsg.toolUseResult 34: }); 35: } 36: } 37: } 38: const toolUsesData = message.messages.map(msg => { 39: const content = msg.message.content[0]; 40: const result = resultsByToolUseId.get(content.id); 41: return { 42: param: content as ToolUseBlockParam, 43: isResolved: lookups.resolvedToolUseIDs.has(content.id), 44: isError: lookups.erroredToolUseIDs.has(content.id), 45: isInProgress: inProgressToolUseIDs.has(content.id), 46: progressMessages: filterToolProgressMessages(lookups.progressMessagesByToolUseID.get(content.id) ?? []), 47: result 48: }; 49: }); 50: const anyInProgress = toolUsesData.some(d => d.isInProgress); 51: return tool.renderGroupedToolUse(toolUsesData, { 52: shouldAnimate: shouldAnimate && anyInProgress, 53: tools 54: }); 55: }

File: src/components/messages/HighlightedThinkingText.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { useContext } from 'react'; 5: import { useQueuedMessage } from '../../context/QueuedMessageContext.js'; 6: import { Box, Text } from '../../ink.js'; 7: import { formatBriefTimestamp } from '../../utils/formatBriefTimestamp.js'; 8: import { findThinkingTriggerPositions, getRainbowColor, isUltrathinkEnabled } from '../../utils/thinking.js'; 9: import { MessageActionsSelectedContext } from '../messageActions.js'; 10: type Props = { 11: text: string; 12: useBriefLayout?: boolean; 13: timestamp?: string; 14: }; 15: export function HighlightedThinkingText(t0) { 16: const $ = _c(31); 17: const { 18: text, 19: useBriefLayout, 20: timestamp 21: } = t0; 22: const isQueued = useQueuedMessage()?.isQueued ?? false; 23: const isSelected = useContext(MessageActionsSelectedContext); 24: const pointerColor = isSelected ? "suggestion" : "subtle"; 25: if (useBriefLayout) { 26: let t1; 27: if ($[0] !== timestamp) { 28: t1 = timestamp ? formatBriefTimestamp(timestamp) : ""; 29: $[0] = timestamp; 30: $[1] = t1; 31: } else { 32: t1 = $[1]; 33: } 34: const ts = t1; 35: const t2 = isQueued ? "subtle" : "briefLabelYou"; 36: let t3; 37: if ($[2] !== t2) { 38: t3 = <Text color={t2}>You</Text>; 39: $[2] = t2; 40: $[3] = t3; 41: } else { 42: t3 = $[3]; 43: } 44: let t4; 45: if ($[4] !== ts) { 46: t4 = ts ? <Text dimColor={true}> {ts}</Text> : null; 47: $[4] = ts; 48: $[5] = t4; 49: } else { 50: t4 = $[5]; 51: } 52: let t5; 53: if ($[6] !== t3 || $[7] !== t4) { 54: t5 = <Box flexDirection="row">{t3}{t4}</Box>; 55: $[6] = t3; 56: $[7] = t4; 57: $[8] = t5; 58: } else { 59: t5 = $[8]; 60: } 61: const t6 = isQueued ? "subtle" : "text"; 62: let t7; 63: if ($[9] !== t6 || $[10] !== text) { 64: t7 = <Text color={t6}>{text}</Text>; 65: $[9] = t6; 66: $[10] = text; 67: $[11] = t7; 68: } else { 69: t7 = $[11]; 70: } 71: let t8; 72: if ($[12] !== t5 || $[13] !== t7) { 73: t8 = <Box flexDirection="column" paddingLeft={2}>{t5}{t7}</Box>; 74: $[12] = t5; 75: $[13] = t7; 76: $[14] = t8; 77: } else { 78: t8 = $[14]; 79: } 80: return t8; 81: } 82: let parts; 83: let t1; 84: if ($[15] !== pointerColor || $[16] !== text) { 85: t1 = Symbol.for("react.early_return_sentinel"); 86: bb0: { 87: const triggers = isUltrathinkEnabled() ? findThinkingTriggerPositions(text) : []; 88: if (triggers.length === 0) { 89: let t2; 90: if ($[19] !== pointerColor) { 91: t2 = <Text color={pointerColor}>{figures.pointer} </Text>; 92: $[19] = pointerColor; 93: $[20] = t2; 94: } else { 95: t2 = $[20]; 96: } 97: let t3; 98: if ($[21] !== text) { 99: t3 = <Text color="text">{text}</Text>; 100: $[21] = text; 101: $[22] = t3; 102: } else { 103: t3 = $[22]; 104: } 105: let t4; 106: if ($[23] !== t2 || $[24] !== t3) { 107: t4 = <Text>{t2}{t3}</Text>; 108: $[23] = t2; 109: $[24] = t3; 110: $[25] = t4; 111: } else { 112: t4 = $[25]; 113: } 114: t1 = t4; 115: break bb0; 116: } 117: parts = []; 118: let cursor = 0; 119: for (const t of triggers) { 120: if (t.start > cursor) { 121: parts.push(<Text key={`plain-${cursor}`} color="text">{text.slice(cursor, t.start)}</Text>); 122: } 123: for (let i = t.start; i < t.end; i++) { 124: parts.push(<Text key={`rb-${i}`} color={getRainbowColor(i - t.start)}>{text[i]}</Text>); 125: } 126: cursor = t.end; 127: } 128: if (cursor < text.length) { 129: parts.push(<Text key={`plain-${cursor}`} color="text">{text.slice(cursor)}</Text>); 130: } 131: } 132: $[15] = pointerColor; 133: $[16] = text; 134: $[17] = parts; 135: $[18] = t1; 136: } else { 137: parts = $[17]; 138: t1 = $[18]; 139: } 140: if (t1 !== Symbol.for("react.early_return_sentinel")) { 141: return t1; 142: } 143: let t2; 144: if ($[26] !== pointerColor) { 145: t2 = <Text color={pointerColor}>{figures.pointer} </Text>; 146: $[26] = pointerColor; 147: $[27] = t2; 148: } else { 149: t2 = $[27]; 150: } 151: let t3; 152: if ($[28] !== parts || $[29] !== t2) { 153: t3 = <Text>{t2}{parts}</Text>; 154: $[28] = parts; 155: $[29] = t2; 156: $[30] = t3; 157: } else { 158: t3 = $[30]; 159: } 160: return t3; 161: }

File: src/components/messages/HookProgressMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'; 4: import type { buildMessageLookups } from 'src/utils/messages.js'; 5: import { Box, Text } from '../../ink.js'; 6: import { MessageResponse } from '../MessageResponse.js'; 7: type Props = { 8: hookEvent: HookEvent; 9: lookups: ReturnType<typeof buildMessageLookups>; 10: toolUseID: string; 11: verbose: boolean; 12: isTranscriptMode?: boolean; 13: }; 14: export function HookProgressMessage(t0) { 15: const $ = _c(22); 16: const { 17: hookEvent, 18: lookups, 19: toolUseID, 20: isTranscriptMode 21: } = t0; 22: let t1; 23: if ($[0] !== hookEvent || $[1] !== lookups.inProgressHookCounts || $[2] !== toolUseID) { 24: t1 = lookups.inProgressHookCounts.get(toolUseID)?.get(hookEvent) ?? 0; 25: $[0] = hookEvent; 26: $[1] = lookups.inProgressHookCounts; 27: $[2] = toolUseID; 28: $[3] = t1; 29: } else { 30: t1 = $[3]; 31: } 32: const inProgressHookCount = t1; 33: const resolvedHookCount = lookups.resolvedHookCounts.get(toolUseID)?.get(hookEvent) ?? 0; 34: if (inProgressHookCount === 0) { 35: return null; 36: } 37: if (hookEvent === "PreToolUse" || hookEvent === "PostToolUse") { 38: if (isTranscriptMode) { 39: let t2; 40: if ($[4] !== inProgressHookCount) { 41: t2 = <Text dimColor={true}>{inProgressHookCount} </Text>; 42: $[4] = inProgressHookCount; 43: $[5] = t2; 44: } else { 45: t2 = $[5]; 46: } 47: let t3; 48: if ($[6] !== hookEvent) { 49: t3 = <Text dimColor={true} bold={true}>{hookEvent}</Text>; 50: $[6] = hookEvent; 51: $[7] = t3; 52: } else { 53: t3 = $[7]; 54: } 55: const t4 = inProgressHookCount === 1 ? " hook" : " hooks"; 56: let t5; 57: if ($[8] !== t4) { 58: t5 = <Text dimColor={true}>{t4} ran</Text>; 59: $[8] = t4; 60: $[9] = t5; 61: } else { 62: t5 = $[9]; 63: } 64: let t6; 65: if ($[10] !== t2 || $[11] !== t3 || $[12] !== t5) { 66: t6 = <MessageResponse><Box flexDirection="row">{t2}{t3}{t5}</Box></MessageResponse>; 67: $[10] = t2; 68: $[11] = t3; 69: $[12] = t5; 70: $[13] = t6; 71: } else { 72: t6 = $[13]; 73: } 74: return t6; 75: } 76: return null; 77: } 78: if (resolvedHookCount === inProgressHookCount) { 79: return null; 80: } 81: let t2; 82: if ($[14] === Symbol.for("react.memo_cache_sentinel")) { 83: t2 = <Text dimColor={true}>Running </Text>; 84: $[14] = t2; 85: } else { 86: t2 = $[14]; 87: } 88: let t3; 89: if ($[15] !== hookEvent) { 90: t3 = <Text dimColor={true} bold={true}>{hookEvent}</Text>; 91: $[15] = hookEvent; 92: $[16] = t3; 93: } else { 94: t3 = $[16]; 95: } 96: const t4 = inProgressHookCount === 1 ? " hook\u2026" : " hooks\u2026"; 97: let t5; 98: if ($[17] !== t4) { 99: t5 = <Text dimColor={true}>{t4}</Text>; 100: $[17] = t4; 101: $[18] = t5; 102: } else { 103: t5 = $[18]; 104: } 105: let t6; 106: if ($[19] !== t3 || $[20] !== t5) { 107: t6 = <MessageResponse><Box flexDirection="row">{t2}{t3}{t5}</Box></MessageResponse>; 108: $[19] = t3; 109: $[20] = t5; 110: $[21] = t6; 111: } else { 112: t6 = $[21]; 113: } 114: return t6; 115: }

File: src/components/messages/nullRenderingAttachments.ts

typescript 1: import type { Attachment } from 'src/utils/attachments.js' 2: import type { Message, NormalizedMessage } from '../../types/message.js' 3: const NULL_RENDERING_TYPES = [ 4: 'hook_success', 5: 'hook_additional_context', 6: 'hook_cancelled', 7: 'command_permissions', 8: 'agent_mention', 9: 'budget_usd', 10: 'critical_system_reminder', 11: 'edited_image_file', 12: 'edited_text_file', 13: 'opened_file_in_ide', 14: 'output_style', 15: 'plan_mode', 16: 'plan_mode_exit', 17: 'plan_mode_reentry', 18: 'structured_output', 19: 'team_context', 20: 'todo_reminder', 21: 'context_efficiency', 22: 'deferred_tools_delta', 23: 'mcp_instructions_delta', 24: 'companion_intro', 25: 'token_usage', 26: 'ultrathink_effort', 27: 'max_turns_reached', 28: 'task_reminder', 29: 'auto_mode', 30: 'auto_mode_exit', 31: 'output_token_usage', 32: 'pen_mode_enter', 33: 'pen_mode_exit', 34: 'verify_plan_reminder', 35: 'current_session_memory', 36: 'compaction_reminder', 37: 'date_change', 38: ] as const satisfies readonly Attachment['type'][] 39: export type NullRenderingAttachmentType = (typeof NULL_RENDERING_TYPES)[number] 40: const NULL_RENDERING_ATTACHMENT_TYPES: ReadonlySet<Attachment['type']> = 41: new Set(NULL_RENDERING_TYPES) 42: export function isNullRenderingAttachment( 43: msg: Message | NormalizedMessage, 44: ): boolean { 45: return ( 46: msg.type === 'attachment' && 47: NULL_RENDERING_ATTACHMENT_TYPES.has(msg.attachment.type) 48: ) 49: }

File: src/components/messages/PlanApprovalMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Markdown } from '../../components/Markdown.js'; 4: import { Box, Text } from '../../ink.js'; 5: import { jsonParse } from '../../utils/slowOperations.js'; 6: import { type IdleNotificationMessage, isIdleNotification, isPlanApprovalRequest, isPlanApprovalResponse, type PlanApprovalRequestMessage, type PlanApprovalResponseMessage } from '../../utils/teammateMailbox.js'; 7: import { getShutdownMessageSummary } from './ShutdownMessage.js'; 8: import { getTaskAssignmentSummary } from './TaskAssignmentMessage.js'; 9: type PlanApprovalRequestProps = { 10: request: PlanApprovalRequestMessage; 11: }; 12: export function PlanApprovalRequestDisplay(t0) { 13: const $ = _c(10); 14: const { 15: request 16: } = t0; 17: let t1; 18: if ($[0] !== request.from) { 19: t1 = <Box marginBottom={1}><Text color="planMode" bold={true}>Plan Approval Request from {request.from}</Text></Box>; 20: $[0] = request.from; 21: $[1] = t1; 22: } else { 23: t1 = $[1]; 24: } 25: let t2; 26: if ($[2] !== request.planContent) { 27: t2 = <Box borderStyle="dashed" borderColor="subtle" borderLeft={false} borderRight={false} flexDirection="column" paddingX={1} marginBottom={1}><Markdown>{request.planContent}</Markdown></Box>; 28: $[2] = request.planContent; 29: $[3] = t2; 30: } else { 31: t2 = $[3]; 32: } 33: let t3; 34: if ($[4] !== request.planFilePath) { 35: t3 = <Text dimColor={true}>Plan file: {request.planFilePath}</Text>; 36: $[4] = request.planFilePath; 37: $[5] = t3; 38: } else { 39: t3 = $[5]; 40: } 41: let t4; 42: if ($[6] !== t1 || $[7] !== t2 || $[8] !== t3) { 43: t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="planMode" flexDirection="column" paddingX={1}>{t1}{t2}{t3}</Box></Box>; 44: $[6] = t1; 45: $[7] = t2; 46: $[8] = t3; 47: $[9] = t4; 48: } else { 49: t4 = $[9]; 50: } 51: return t4; 52: } 53: type PlanApprovalResponseProps = { 54: response: PlanApprovalResponseMessage; 55: senderName: string; 56: }; 57: export function PlanApprovalResponseDisplay(t0) { 58: const $ = _c(13); 59: const { 60: response, 61: senderName 62: } = t0; 63: if (response.approved) { 64: let t1; 65: if ($[0] !== senderName) { 66: t1 = <Box><Text color="success" bold={true}>✓ Plan Approved by {senderName}</Text></Box>; 67: $[0] = senderName; 68: $[1] = t1; 69: } else { 70: t1 = $[1]; 71: } 72: let t2; 73: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 74: t2 = <Box marginTop={1}><Text>You can now proceed with implementation. Your plan mode restrictions have been lifted.</Text></Box>; 75: $[2] = t2; 76: } else { 77: t2 = $[2]; 78: } 79: let t3; 80: if ($[3] !== t1) { 81: t3 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="success" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}</Box></Box>; 82: $[3] = t1; 83: $[4] = t3; 84: } else { 85: t3 = $[4]; 86: } 87: return t3; 88: } 89: let t1; 90: if ($[5] !== senderName) { 91: t1 = <Box><Text color="error" bold={true}>✗ Plan Rejected by {senderName}</Text></Box>; 92: $[5] = senderName; 93: $[6] = t1; 94: } else { 95: t1 = $[6]; 96: } 97: let t2; 98: if ($[7] !== response.feedback) { 99: t2 = response.feedback && <Box marginTop={1} borderStyle="dashed" borderColor="subtle" borderLeft={false} borderRight={false} paddingX={1}><Text>Feedback: {response.feedback}</Text></Box>; 100: $[7] = response.feedback; 101: $[8] = t2; 102: } else { 103: t2 = $[8]; 104: } 105: let t3; 106: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 107: t3 = <Box marginTop={1}><Text dimColor={true}>Please revise your plan based on the feedback and call ExitPlanMode again.</Text></Box>; 108: $[9] = t3; 109: } else { 110: t3 = $[9]; 111: } 112: let t4; 113: if ($[10] !== t1 || $[11] !== t2) { 114: t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="error" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}{t3}</Box></Box>; 115: $[10] = t1; 116: $[11] = t2; 117: $[12] = t4; 118: } else { 119: t4 = $[12]; 120: } 121: return t4; 122: } 123: export function tryRenderPlanApprovalMessage(content: string, senderName: string): React.ReactNode | null { 124: const request = isPlanApprovalRequest(content); 125: if (request) { 126: return <PlanApprovalRequestDisplay request={request} />; 127: } 128: const response = isPlanApprovalResponse(content); 129: if (response) { 130: return <PlanApprovalResponseDisplay response={response} senderName={senderName} />; 131: } 132: return null; 133: } 134: function getPlanApprovalSummary(content: string): string | null { 135: const request = isPlanApprovalRequest(content); 136: if (request) { 137: return `[Plan Approval Request from ${request.from}]`; 138: } 139: const response = isPlanApprovalResponse(content); 140: if (response) { 141: if (response.approved) { 142: return '[Plan Approved] You can now proceed with implementation'; 143: } else { 144: return `[Plan Rejected] ${response.feedback || 'Please revise your plan'}`; 145: } 146: } 147: return null; 148: } 149: function getIdleNotificationSummary(msg: IdleNotificationMessage): string { 150: const parts: string[] = ['Agent idle']; 151: if (msg.completedTaskId) { 152: const status = msg.completedStatus || 'completed'; 153: parts.push(`Task ${msg.completedTaskId} ${status}`); 154: } 155: if (msg.summary) { 156: parts.push(`Last DM: ${msg.summary}`); 157: } 158: return parts.join(' · '); 159: } 160: export function formatTeammateMessageContent(content: string): string { 161: const planSummary = getPlanApprovalSummary(content); 162: if (planSummary) { 163: return planSummary; 164: } 165: const shutdownSummary = getShutdownMessageSummary(content); 166: if (shutdownSummary) { 167: return shutdownSummary; 168: } 169: const idleMsg = isIdleNotification(content); 170: if (idleMsg) { 171: return getIdleNotificationSummary(idleMsg); 172: } 173: const taskAssignmentSummary = getTaskAssignmentSummary(content); 174: if (taskAssignmentSummary) { 175: return taskAssignmentSummary; 176: } 177: try { 178: const parsed = jsonParse(content) as { 179: type?: string; 180: message?: string; 181: }; 182: if (parsed?.type === 'teammate_terminated' && parsed.message) { 183: return parsed.message; 184: } 185: } catch { 186: } 187: return content; 188: }

File: src/components/messages/RateLimitMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useEffect, useMemo, useState } from 'react'; 3: import { extraUsage } from 'src/commands/extra-usage/index.js'; 4: import { Box, Text } from 'src/ink.js'; 5: import { useClaudeAiLimits } from 'src/services/claudeAiLimitsHook.js'; 6: import { shouldProcessMockLimits } from 'src/services/rateLimitMocking.js'; 7: import { getRateLimitTier, getSubscriptionType, isClaudeAISubscriber } from 'src/utils/auth.js'; 8: import { hasClaudeAiBillingAccess } from 'src/utils/billing.js'; 9: import { MessageResponse } from '../MessageResponse.js'; 10: type UpsellParams = { 11: shouldShowUpsell: boolean; 12: isMax20x: boolean; 13: isExtraUsageCommandEnabled: boolean; 14: shouldAutoOpenRateLimitOptionsMenu: boolean; 15: isTeamOrEnterprise: boolean; 16: hasBillingAccess: boolean; 17: }; 18: export function getUpsellMessage({ 19: shouldShowUpsell, 20: isMax20x, 21: isExtraUsageCommandEnabled, 22: shouldAutoOpenRateLimitOptionsMenu, 23: isTeamOrEnterprise, 24: hasBillingAccess 25: }: UpsellParams): string | null { 26: if (!shouldShowUpsell) return null; 27: if (isMax20x) { 28: if (isExtraUsageCommandEnabled) { 29: return '/extra-usage to finish what you\u2019re working on.'; 30: } 31: return '/login to switch to an API usage-billed account.'; 32: } 33: if (shouldAutoOpenRateLimitOptionsMenu) { 34: return 'Opening your options\u2026'; 35: } 36: if (!isTeamOrEnterprise && !isExtraUsageCommandEnabled) { 37: return '/upgrade to increase your usage limit.'; 38: } 39: if (isTeamOrEnterprise) { 40: if (!isExtraUsageCommandEnabled) return null; 41: if (hasBillingAccess) { 42: return '/extra-usage to finish what you\u2019re working on.'; 43: } 44: return '/extra-usage to request more usage from your admin.'; 45: } 46: return '/upgrade or /extra-usage to finish what you\u2019re working on.'; 47: } 48: type RateLimitMessageProps = { 49: text: string; 50: onOpenRateLimitOptions?: () => void; 51: }; 52: export function RateLimitMessage(t0) { 53: const $ = _c(16); 54: const { 55: text, 56: onOpenRateLimitOptions 57: } = t0; 58: let t1; 59: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 60: t1 = getSubscriptionType(); 61: $[0] = t1; 62: } else { 63: t1 = $[0]; 64: } 65: const subscriptionType = t1; 66: let t2; 67: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 68: t2 = getRateLimitTier(); 69: $[1] = t2; 70: } else { 71: t2 = $[1]; 72: } 73: const rateLimitTier = t2; 74: const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise"; 75: const isMax20x = rateLimitTier === "default_claude_max_20x"; 76: let t3; 77: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 78: t3 = shouldProcessMockLimits() || isClaudeAISubscriber(); 79: $[2] = t3; 80: } else { 81: t3 = $[2]; 82: } 83: const shouldShowUpsell = t3; 84: const canSeeRateLimitOptionsUpsell = shouldShowUpsell && !isMax20x; 85: const [hasOpenedInteractiveMenu, setHasOpenedInteractiveMenu] = useState(false); 86: const claudeAiLimits = useClaudeAiLimits(); 87: const isCurrentlyRateLimited = claudeAiLimits.status === "rejected" && claudeAiLimits.resetsAt !== undefined && !claudeAiLimits.isUsingOverage; 88: const shouldAutoOpenRateLimitOptionsMenu = canSeeRateLimitOptionsUpsell && !hasOpenedInteractiveMenu && isCurrentlyRateLimited && onOpenRateLimitOptions; 89: let t4; 90: let t5; 91: if ($[3] !== onOpenRateLimitOptions || $[4] !== shouldAutoOpenRateLimitOptionsMenu) { 92: t4 = () => { 93: if (shouldAutoOpenRateLimitOptionsMenu) { 94: setHasOpenedInteractiveMenu(true); 95: onOpenRateLimitOptions(); 96: } 97: }; 98: t5 = [shouldAutoOpenRateLimitOptionsMenu, onOpenRateLimitOptions]; 99: $[3] = onOpenRateLimitOptions; 100: $[4] = shouldAutoOpenRateLimitOptionsMenu; 101: $[5] = t4; 102: $[6] = t5; 103: } else { 104: t4 = $[5]; 105: t5 = $[6]; 106: } 107: useEffect(t4, t5); 108: let t6; 109: bb0: { 110: let t7; 111: if ($[7] !== shouldAutoOpenRateLimitOptionsMenu) { 112: t7 = getUpsellMessage({ 113: shouldShowUpsell, 114: isMax20x, 115: isExtraUsageCommandEnabled: extraUsage.isEnabled(), 116: shouldAutoOpenRateLimitOptionsMenu: !!shouldAutoOpenRateLimitOptionsMenu, 117: isTeamOrEnterprise, 118: hasBillingAccess: hasClaudeAiBillingAccess() 119: }); 120: $[7] = shouldAutoOpenRateLimitOptionsMenu; 121: $[8] = t7; 122: } else { 123: t7 = $[8]; 124: } 125: const message = t7; 126: if (!message) { 127: t6 = null; 128: break bb0; 129: } 130: let t8; 131: if ($[9] !== message) { 132: t8 = <Text dimColor={true}>{message}</Text>; 133: $[9] = message; 134: $[10] = t8; 135: } else { 136: t8 = $[10]; 137: } 138: t6 = t8; 139: } 140: const upsell = t6; 141: let t7; 142: if ($[11] !== text) { 143: t7 = <Text color="error">{text}</Text>; 144: $[11] = text; 145: $[12] = t7; 146: } else { 147: t7 = $[12]; 148: } 149: const t8 = hasOpenedInteractiveMenu ? null : upsell; 150: let t9; 151: if ($[13] !== t7 || $[14] !== t8) { 152: t9 = <MessageResponse><Box flexDirection="column">{t7}{t8}</Box></MessageResponse>; 153: $[13] = t7; 154: $[14] = t8; 155: $[15] = t9; 156: } else { 157: t9 = $[15]; 158: } 159: return t9; 160: }

File: src/components/messages/ShutdownMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { isShutdownApproved, isShutdownRejected, isShutdownRequest, type ShutdownRejectedMessage, type ShutdownRequestMessage } from '../../utils/teammateMailbox.js'; 5: type ShutdownRequestProps = { 6: request: ShutdownRequestMessage; 7: }; 8: export function ShutdownRequestDisplay(t0) { 9: const $ = _c(7); 10: const { 11: request 12: } = t0; 13: let t1; 14: if ($[0] !== request.from) { 15: t1 = <Box marginBottom={1}><Text color="warning" bold={true}>Shutdown request from {request.from}</Text></Box>; 16: $[0] = request.from; 17: $[1] = t1; 18: } else { 19: t1 = $[1]; 20: } 21: let t2; 22: if ($[2] !== request.reason) { 23: t2 = request.reason && <Box><Text>Reason: {request.reason}</Text></Box>; 24: $[2] = request.reason; 25: $[3] = t2; 26: } else { 27: t2 = $[3]; 28: } 29: let t3; 30: if ($[4] !== t1 || $[5] !== t2) { 31: t3 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="warning" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}</Box></Box>; 32: $[4] = t1; 33: $[5] = t2; 34: $[6] = t3; 35: } else { 36: t3 = $[6]; 37: } 38: return t3; 39: } 40: type ShutdownRejectedProps = { 41: response: ShutdownRejectedMessage; 42: }; 43: export function ShutdownRejectedDisplay(t0) { 44: const $ = _c(8); 45: const { 46: response 47: } = t0; 48: let t1; 49: if ($[0] !== response.from) { 50: t1 = <Text color="subtle" bold={true}>Shutdown rejected by {response.from}</Text>; 51: $[0] = response.from; 52: $[1] = t1; 53: } else { 54: t1 = $[1]; 55: } 56: let t2; 57: if ($[2] !== response.reason) { 58: t2 = <Box marginTop={1} borderStyle="dashed" borderColor="subtle" borderLeft={false} borderRight={false} paddingX={1}><Text>Reason: {response.reason}</Text></Box>; 59: $[2] = response.reason; 60: $[3] = t2; 61: } else { 62: t2 = $[3]; 63: } 64: let t3; 65: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 66: t3 = <Box marginTop={1}><Text dimColor={true}>Teammate is continuing to work. You may request shutdown again later.</Text></Box>; 67: $[4] = t3; 68: } else { 69: t3 = $[4]; 70: } 71: let t4; 72: if ($[5] !== t1 || $[6] !== t2) { 73: t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="subtle" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}{t3}</Box></Box>; 74: $[5] = t1; 75: $[6] = t2; 76: $[7] = t4; 77: } else { 78: t4 = $[7]; 79: } 80: return t4; 81: } 82: export function tryRenderShutdownMessage(content: string): React.ReactNode | null { 83: const request = isShutdownRequest(content); 84: if (request) { 85: return <ShutdownRequestDisplay request={request} />; 86: } 87: if (isShutdownApproved(content)) { 88: return null; 89: } 90: const rejected = isShutdownRejected(content); 91: if (rejected) { 92: return <ShutdownRejectedDisplay response={rejected} />; 93: } 94: return null; 95: } 96: export function getShutdownMessageSummary(content: string): string | null { 97: const request = isShutdownRequest(content); 98: if (request) { 99: return `[Shutdown Request from ${request.from}]${request.reason ? ` ${request.reason}` : ''}`; 100: } 101: const approved = isShutdownApproved(content); 102: if (approved) { 103: return `[Shutdown Approved] ${approved.from} is now exiting`; 104: } 105: const rejected = isShutdownRejected(content); 106: if (rejected) { 107: return `[Shutdown Rejected] ${rejected.from}: ${rejected.reason}`; 108: } 109: return null; 110: }

File: src/components/messages/SystemAPIErrorMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useState } from 'react'; 4: import { Box, Text } from 'src/ink.js'; 5: import { formatAPIError } from 'src/services/api/errorUtils.js'; 6: import type { SystemAPIErrorMessage } from 'src/types/message.js'; 7: import { useInterval } from 'usehooks-ts'; 8: import { CtrlOToExpand } from '../CtrlOToExpand.js'; 9: import { MessageResponse } from '../MessageResponse.js'; 10: const MAX_API_ERROR_CHARS = 1000; 11: type Props = { 12: message: SystemAPIErrorMessage; 13: verbose: boolean; 14: }; 15: export function SystemAPIErrorMessage(t0) { 16: const $ = _c(33); 17: const { 18: message: t1, 19: verbose 20: } = t0; 21: const { 22: retryAttempt, 23: error, 24: retryInMs, 25: maxRetries 26: } = t1; 27: const hidden = true && retryAttempt < 4; 28: const [countdownMs, setCountdownMs] = useState(0); 29: const done = countdownMs >= retryInMs; 30: let t2; 31: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 32: t2 = () => setCountdownMs(_temp); 33: $[0] = t2; 34: } else { 35: t2 = $[0]; 36: } 37: useInterval(t2, hidden || done ? null : 1000); 38: if (hidden) { 39: return null; 40: } 41: let t3; 42: if ($[1] !== countdownMs || $[2] !== retryInMs) { 43: t3 = Math.round((retryInMs - countdownMs) / 1000); 44: $[1] = countdownMs; 45: $[2] = retryInMs; 46: $[3] = t3; 47: } else { 48: t3 = $[3]; 49: } 50: const retryInSecondsLive = Math.max(0, t3); 51: let T0; 52: let T1; 53: let T2; 54: let t4; 55: let t5; 56: let t6; 57: let truncated; 58: if ($[4] !== error || $[5] !== verbose) { 59: const formatted = formatAPIError(error); 60: truncated = !verbose && formatted.length > MAX_API_ERROR_CHARS; 61: T2 = MessageResponse; 62: T1 = Box; 63: t6 = "column"; 64: T0 = Text; 65: t4 = "error"; 66: t5 = truncated ? formatted.slice(0, MAX_API_ERROR_CHARS) + "\u2026" : formatted; 67: $[4] = error; 68: $[5] = verbose; 69: $[6] = T0; 70: $[7] = T1; 71: $[8] = T2; 72: $[9] = t4; 73: $[10] = t5; 74: $[11] = t6; 75: $[12] = truncated; 76: } else { 77: T0 = $[6]; 78: T1 = $[7]; 79: T2 = $[8]; 80: t4 = $[9]; 81: t5 = $[10]; 82: t6 = $[11]; 83: truncated = $[12]; 84: } 85: let t7; 86: if ($[13] !== T0 || $[14] !== t4 || $[15] !== t5) { 87: t7 = <T0 color={t4}>{t5}</T0>; 88: $[13] = T0; 89: $[14] = t4; 90: $[15] = t5; 91: $[16] = t7; 92: } else { 93: t7 = $[16]; 94: } 95: let t8; 96: if ($[17] !== truncated) { 97: t8 = truncated && <CtrlOToExpand />; 98: $[17] = truncated; 99: $[18] = t8; 100: } else { 101: t8 = $[18]; 102: } 103: const t9 = retryInSecondsLive === 1 ? "second" : "seconds"; 104: let t10; 105: if ($[19] !== maxRetries || $[20] !== retryAttempt || $[21] !== retryInSecondsLive || $[22] !== t9) { 106: t10 = <Text dimColor={true}>Retrying in {retryInSecondsLive}{" "}{t9}… (attempt{" "}{retryAttempt}/{maxRetries}){process.env.API_TIMEOUT_MS ? ` · API_TIMEOUT_MS=${process.env.API_TIMEOUT_MS}ms, try increasing it` : ""}</Text>; 107: $[19] = maxRetries; 108: $[20] = retryAttempt; 109: $[21] = retryInSecondsLive; 110: $[22] = t9; 111: $[23] = t10; 112: } else { 113: t10 = $[23]; 114: } 115: let t11; 116: if ($[24] !== T1 || $[25] !== t10 || $[26] !== t6 || $[27] !== t7 || $[28] !== t8) { 117: t11 = <T1 flexDirection={t6}>{t7}{t8}{t10}</T1>; 118: $[24] = T1; 119: $[25] = t10; 120: $[26] = t6; 121: $[27] = t7; 122: $[28] = t8; 123: $[29] = t11; 124: } else { 125: t11 = $[29]; 126: } 127: let t12; 128: if ($[30] !== T2 || $[31] !== t11) { 129: t12 = <T2>{t11}</T2>; 130: $[30] = T2; 131: $[31] = t11; 132: $[32] = t12; 133: } else { 134: t12 = $[32]; 135: } 136: return t12; 137: } 138: function _temp(ms) { 139: return ms + 1000; 140: }

File: src/components/messages/SystemTextMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { Box, Text, type TextProps } from '../../ink.js'; 3: import { feature } from 'bun:bundle'; 4: import * as React from 'react'; 5: import { useState } from 'react'; 6: import sample from 'lodash-es/sample.js'; 7: import { BLACK_CIRCLE, REFERENCE_MARK, TEARDROP_ASTERISK } from '../../constants/figures.js'; 8: import figures from 'figures'; 9: import { basename } from 'path'; 10: import { MessageResponse } from '../MessageResponse.js'; 11: import { FilePathLink } from '../FilePathLink.js'; 12: import { openPath } from '../../utils/browser.js'; 13: const teamMemSaved = feature('TEAMMEM') ? require('./teamMemSaved.js') as typeof import('./teamMemSaved.js') : null; 14: import { TURN_COMPLETION_VERBS } from '../../constants/turnCompletionVerbs.js'; 15: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 16: import type { SystemMessage, SystemStopHookSummaryMessage, SystemBridgeStatusMessage, SystemTurnDurationMessage, SystemThinkingMessage, SystemMemorySavedMessage } from '../../types/message.js'; 17: import { SystemAPIErrorMessage } from './SystemAPIErrorMessage.js'; 18: import { formatDuration, formatNumber, formatSecondsShort } from '../../utils/format.js'; 19: import { getGlobalConfig } from '../../utils/config.js'; 20: import Link from '../../ink/components/Link.js'; 21: import ThemedText from '../design-system/ThemedText.js'; 22: import { CtrlOToExpand } from '../CtrlOToExpand.js'; 23: import { useAppStateStore } from '../../state/AppState.js'; 24: import { isBackgroundTask, type TaskState } from '../../tasks/types.js'; 25: import { getPillLabel } from '../../tasks/pillLabel.js'; 26: import { useSelectedMessageBg } from '../messageActions.js'; 27: type Props = { 28: message: SystemMessage; 29: addMargin: boolean; 30: verbose: boolean; 31: isTranscriptMode?: boolean; 32: }; 33: export function SystemTextMessage(t0) { 34: const $ = _c(51); 35: const { 36: message, 37: addMargin, 38: verbose, 39: isTranscriptMode 40: } = t0; 41: const bg = useSelectedMessageBg(); 42: if (message.subtype === "turn_duration") { 43: let t1; 44: if ($[0] !== addMargin || $[1] !== message) { 45: t1 = <TurnDurationMessage message={message} addMargin={addMargin} />; 46: $[0] = addMargin; 47: $[1] = message; 48: $[2] = t1; 49: } else { 50: t1 = $[2]; 51: } 52: return t1; 53: } 54: if (message.subtype === "memory_saved") { 55: let t1; 56: if ($[3] !== addMargin || $[4] !== message) { 57: t1 = <MemorySavedMessage message={message} addMargin={addMargin} />; 58: $[3] = addMargin; 59: $[4] = message; 60: $[5] = t1; 61: } else { 62: t1 = $[5]; 63: } 64: return t1; 65: } 66: if (message.subtype === "away_summary") { 67: const t1 = addMargin ? 1 : 0; 68: let t2; 69: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 70: t2 = <Box minWidth={2}><Text dimColor={true}>{REFERENCE_MARK}</Text></Box>; 71: $[6] = t2; 72: } else { 73: t2 = $[6]; 74: } 75: let t3; 76: if ($[7] !== message.content) { 77: t3 = <Text dimColor={true}>{message.content}</Text>; 78: $[7] = message.content; 79: $[8] = t3; 80: } else { 81: t3 = $[8]; 82: } 83: let t4; 84: if ($[9] !== bg || $[10] !== t1 || $[11] !== t3) { 85: t4 = <Box flexDirection="row" marginTop={t1} backgroundColor={bg} width="100%">{t2}{t3}</Box>; 86: $[9] = bg; 87: $[10] = t1; 88: $[11] = t3; 89: $[12] = t4; 90: } else { 91: t4 = $[12]; 92: } 93: return t4; 94: } 95: if (message.subtype === "agents_killed") { 96: const t1 = addMargin ? 1 : 0; 97: let t2; 98: let t3; 99: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 100: t2 = <Box minWidth={2}><Text color="error">{BLACK_CIRCLE}</Text></Box>; 101: t3 = <Text dimColor={true}>All background agents stopped</Text>; 102: $[13] = t2; 103: $[14] = t3; 104: } else { 105: t2 = $[13]; 106: t3 = $[14]; 107: } 108: let t4; 109: if ($[15] !== bg || $[16] !== t1) { 110: t4 = <Box flexDirection="row" marginTop={t1} backgroundColor={bg} width="100%">{t2}{t3}</Box>; 111: $[15] = bg; 112: $[16] = t1; 113: $[17] = t4; 114: } else { 115: t4 = $[17]; 116: } 117: return t4; 118: } 119: if (message.subtype === "thinking") { 120: return null; 121: } 122: if (message.subtype === "bridge_status") { 123: let t1; 124: if ($[18] !== addMargin || $[19] !== message) { 125: t1 = <BridgeStatusMessage message={message} addMargin={addMargin} />; 126: $[18] = addMargin; 127: $[19] = message; 128: $[20] = t1; 129: } else { 130: t1 = $[20]; 131: } 132: return t1; 133: } 134: if (message.subtype === "scheduled_task_fire") { 135: const t1 = addMargin ? 1 : 0; 136: let t2; 137: if ($[21] !== message.content) { 138: t2 = <Text dimColor={true}>{TEARDROP_ASTERISK} {message.content}</Text>; 139: $[21] = message.content; 140: $[22] = t2; 141: } else { 142: t2 = $[22]; 143: } 144: let t3; 145: if ($[23] !== bg || $[24] !== t1 || $[25] !== t2) { 146: t3 = <Box marginTop={t1} backgroundColor={bg} width="100%">{t2}</Box>; 147: $[23] = bg; 148: $[24] = t1; 149: $[25] = t2; 150: $[26] = t3; 151: } else { 152: t3 = $[26]; 153: } 154: return t3; 155: } 156: if (message.subtype === "permission_retry") { 157: const t1 = addMargin ? 1 : 0; 158: let t2; 159: let t3; 160: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 161: t2 = <Text dimColor={true}>{TEARDROP_ASTERISK} </Text>; 162: t3 = <Text>Allowed </Text>; 163: $[27] = t2; 164: $[28] = t3; 165: } else { 166: t2 = $[27]; 167: t3 = $[28]; 168: } 169: let t4; 170: if ($[29] !== message.commands) { 171: t4 = message.commands.join(", "); 172: $[29] = message.commands; 173: $[30] = t4; 174: } else { 175: t4 = $[30]; 176: } 177: let t5; 178: if ($[31] !== t4) { 179: t5 = <Text bold={true}>{t4}</Text>; 180: $[31] = t4; 181: $[32] = t5; 182: } else { 183: t5 = $[32]; 184: } 185: let t6; 186: if ($[33] !== bg || $[34] !== t1 || $[35] !== t5) { 187: t6 = <Box marginTop={t1} backgroundColor={bg} width="100%">{t2}{t3}{t5}</Box>; 188: $[33] = bg; 189: $[34] = t1; 190: $[35] = t5; 191: $[36] = t6; 192: } else { 193: t6 = $[36]; 194: } 195: return t6; 196: } 197: const isStopHookSummary = message.subtype === "stop_hook_summary"; 198: if (!isStopHookSummary && !verbose && message.level === "info") { 199: return null; 200: } 201: if (message.subtype === "api_error") { 202: let t1; 203: if ($[37] !== message || $[38] !== verbose) { 204: t1 = <SystemAPIErrorMessage message={message} verbose={verbose} />; 205: $[37] = message; 206: $[38] = verbose; 207: $[39] = t1; 208: } else { 209: t1 = $[39]; 210: } 211: return t1; 212: } 213: if (message.subtype === "stop_hook_summary") { 214: let t1; 215: if ($[40] !== addMargin || $[41] !== isTranscriptMode || $[42] !== message || $[43] !== verbose) { 216: t1 = <StopHookSummaryMessage message={message} addMargin={addMargin} verbose={verbose} isTranscriptMode={isTranscriptMode} />; 217: $[40] = addMargin; 218: $[41] = isTranscriptMode; 219: $[42] = message; 220: $[43] = verbose; 221: $[44] = t1; 222: } else { 223: t1 = $[44]; 224: } 225: return t1; 226: } 227: const content = message.content; 228: if (typeof content !== "string") { 229: return null; 230: } 231: const t1 = message.level !== "info"; 232: const t2 = message.level === "warning" ? "warning" : undefined; 233: const t3 = message.level === "info"; 234: let t4; 235: if ($[45] !== addMargin || $[46] !== content || $[47] !== t1 || $[48] !== t2 || $[49] !== t3) { 236: t4 = <Box flexDirection="row" width="100%"><SystemTextMessageInner content={content} addMargin={addMargin} dot={t1} color={t2} dimColor={t3} /></Box>; 237: $[45] = addMargin; 238: $[46] = content; 239: $[47] = t1; 240: $[48] = t2; 241: $[49] = t3; 242: $[50] = t4; 243: } else { 244: t4 = $[50]; 245: } 246: return t4; 247: } 248: function StopHookSummaryMessage(t0) { 249: const $ = _c(47); 250: const { 251: message, 252: addMargin, 253: verbose, 254: isTranscriptMode 255: } = t0; 256: const bg = useSelectedMessageBg(); 257: const { 258: hookCount, 259: hookInfos, 260: hookErrors, 261: preventedContinuation, 262: stopReason 263: } = message; 264: const { 265: columns 266: } = useTerminalSize(); 267: let t1; 268: if ($[0] !== hookInfos || $[1] !== message.totalDurationMs) { 269: t1 = message.totalDurationMs ?? hookInfos.reduce(_temp, 0); 270: $[0] = hookInfos; 271: $[1] = message.totalDurationMs; 272: $[2] = t1; 273: } else { 274: t1 = $[2]; 275: } 276: const totalDurationMs = t1; 277: if (hookErrors.length === 0 && !preventedContinuation && !message.hookLabel) { 278: if (true || totalDurationMs < HOOK_TIMING_DISPLAY_THRESHOLD_MS) { 279: return null; 280: } 281: } 282: let t2; 283: if ($[3] !== totalDurationMs) { 284: t2 = false && totalDurationMs > 0 ? ` (${formatSecondsShort(totalDurationMs)})` : ""; 285: $[3] = totalDurationMs; 286: $[4] = t2; 287: } else { 288: t2 = $[4]; 289: } 290: const totalStr = t2; 291: if (message.hookLabel) { 292: const t3 = hookCount === 1 ? "hook" : "hooks"; 293: let t4; 294: if ($[5] !== hookCount || $[6] !== message.hookLabel || $[7] !== t3 || $[8] !== totalStr) { 295: t4 = <Text dimColor={true}>{" \u23BF "}Ran {hookCount} {message.hookLabel}{" "}{t3}{totalStr}</Text>; 296: $[5] = hookCount; 297: $[6] = message.hookLabel; 298: $[7] = t3; 299: $[8] = totalStr; 300: $[9] = t4; 301: } else { 302: t4 = $[9]; 303: } 304: let t5; 305: if ($[10] !== hookInfos || $[11] !== isTranscriptMode) { 306: t5 = isTranscriptMode && hookInfos.map(_temp2); 307: $[10] = hookInfos; 308: $[11] = isTranscriptMode; 309: $[12] = t5; 310: } else { 311: t5 = $[12]; 312: } 313: let t6; 314: if ($[13] !== t4 || $[14] !== t5) { 315: t6 = <Box flexDirection="column" width="100%">{t4}{t5}</Box>; 316: $[13] = t4; 317: $[14] = t5; 318: $[15] = t6; 319: } else { 320: t6 = $[15]; 321: } 322: return t6; 323: } 324: const t3 = addMargin ? 1 : 0; 325: let t4; 326: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 327: t4 = <Box minWidth={2}><Text>{BLACK_CIRCLE}</Text></Box>; 328: $[16] = t4; 329: } else { 330: t4 = $[16]; 331: } 332: const t5 = columns - 10; 333: let t6; 334: if ($[17] !== hookCount) { 335: t6 = <Text bold={true}>{hookCount}</Text>; 336: $[17] = hookCount; 337: $[18] = t6; 338: } else { 339: t6 = $[18]; 340: } 341: const t7 = message.hookLabel ?? "stop"; 342: const t8 = hookCount === 1 ? "hook" : "hooks"; 343: let t9; 344: if ($[19] !== hookInfos || $[20] !== verbose) { 345: t9 = !verbose && hookInfos.length > 0 && <>{" "}<CtrlOToExpand /></>; 346: $[19] = hookInfos; 347: $[20] = verbose; 348: $[21] = t9; 349: } else { 350: t9 = $[21]; 351: } 352: let t10; 353: if ($[22] !== t6 || $[23] !== t7 || $[24] !== t8 || $[25] !== t9 || $[26] !== totalStr) { 354: t10 = <Text>Ran {t6} {t7}{" "}{t8}{totalStr}{t9}</Text>; 355: $[22] = t6; 356: $[23] = t7; 357: $[24] = t8; 358: $[25] = t9; 359: $[26] = totalStr; 360: $[27] = t10; 361: } else { 362: t10 = $[27]; 363: } 364: let t11; 365: if ($[28] !== hookInfos || $[29] !== verbose) { 366: t11 = verbose && hookInfos.length > 0 && hookInfos.map(_temp3); 367: $[28] = hookInfos; 368: $[29] = verbose; 369: $[30] = t11; 370: } else { 371: t11 = $[30]; 372: } 373: let t12; 374: if ($[31] !== preventedContinuation || $[32] !== stopReason) { 375: t12 = preventedContinuation && stopReason && <Text><Text dimColor={true}>⎿  </Text>{stopReason}</Text>; 376: $[31] = preventedContinuation; 377: $[32] = stopReason; 378: $[33] = t12; 379: } else { 380: t12 = $[33]; 381: } 382: let t13; 383: if ($[34] !== hookErrors || $[35] !== message.hookLabel) { 384: t13 = hookErrors.length > 0 && hookErrors.map((err, idx_1) => <Text key={idx_1}><Text dimColor={true}>⎿  </Text>{message.hookLabel ?? "Stop"} hook error: {err}</Text>); 385: $[34] = hookErrors; 386: $[35] = message.hookLabel; 387: $[36] = t13; 388: } else { 389: t13 = $[36]; 390: } 391: let t14; 392: if ($[37] !== t10 || $[38] !== t11 || $[39] !== t12 || $[40] !== t13 || $[41] !== t5) { 393: t14 = <Box flexDirection="column" width={t5}>{t10}{t11}{t12}{t13}</Box>; 394: $[37] = t10; 395: $[38] = t11; 396: $[39] = t12; 397: $[40] = t13; 398: $[41] = t5; 399: $[42] = t14; 400: } else { 401: t14 = $[42]; 402: } 403: let t15; 404: if ($[43] !== bg || $[44] !== t14 || $[45] !== t3) { 405: t15 = <Box flexDirection="row" marginTop={t3} backgroundColor={bg} width="100%">{t4}{t14}</Box>; 406: $[43] = bg; 407: $[44] = t14; 408: $[45] = t3; 409: $[46] = t15; 410: } else { 411: t15 = $[46]; 412: } 413: return t15; 414: } 415: function _temp3(info_0, idx_0) { 416: const durationStr_0 = false && info_0.durationMs !== undefined ? ` (${formatSecondsShort(info_0.durationMs)})` : ""; 417: return <Text key={`cmd-${idx_0}`} dimColor={true}>⎿  {info_0.command === "prompt" ? `prompt: ${info_0.promptText || ""}` : info_0.command}{durationStr_0}</Text>; 418: } 419: function _temp2(info, idx) { 420: const durationStr = false && info.durationMs !== undefined ? ` (${formatSecondsShort(info.durationMs)})` : ""; 421: return <Text key={`cmd-${idx}`} dimColor={true}>{" \u23BF "}{info.command === "prompt" ? `prompt: ${info.promptText || ""}` : info.command}{durationStr}</Text>; 422: } 423: function _temp(sum, h) { 424: return sum + (h.durationMs ?? 0); 425: } 426: function SystemTextMessageInner(t0) { 427: const $ = _c(18); 428: const { 429: content, 430: addMargin, 431: dot, 432: color, 433: dimColor 434: } = t0; 435: const { 436: columns 437: } = useTerminalSize(); 438: const bg = useSelectedMessageBg(); 439: const t1 = addMargin ? 1 : 0; 440: let t2; 441: if ($[0] !== color || $[1] !== dimColor || $[2] !== dot) { 442: t2 = dot && <Box minWidth={2}><Text color={color} dimColor={dimColor}>{BLACK_CIRCLE}</Text></Box>; 443: $[0] = color; 444: $[1] = dimColor; 445: $[2] = dot; 446: $[3] = t2; 447: } else { 448: t2 = $[3]; 449: } 450: const t3 = columns - 10; 451: let t4; 452: if ($[4] !== content) { 453: t4 = content.trim(); 454: $[4] = content; 455: $[5] = t4; 456: } else { 457: t4 = $[5]; 458: } 459: let t5; 460: if ($[6] !== color || $[7] !== dimColor || $[8] !== t4) { 461: t5 = <Text color={color} dimColor={dimColor} wrap="wrap">{t4}</Text>; 462: $[6] = color; 463: $[7] = dimColor; 464: $[8] = t4; 465: $[9] = t5; 466: } else { 467: t5 = $[9]; 468: } 469: let t6; 470: if ($[10] !== t3 || $[11] !== t5) { 471: t6 = <Box flexDirection="column" width={t3}>{t5}</Box>; 472: $[10] = t3; 473: $[11] = t5; 474: $[12] = t6; 475: } else { 476: t6 = $[12]; 477: } 478: let t7; 479: if ($[13] !== bg || $[14] !== t1 || $[15] !== t2 || $[16] !== t6) { 480: t7 = <Box flexDirection="row" marginTop={t1} backgroundColor={bg} width="100%">{t2}{t6}</Box>; 481: $[13] = bg; 482: $[14] = t1; 483: $[15] = t2; 484: $[16] = t6; 485: $[17] = t7; 486: } else { 487: t7 = $[17]; 488: } 489: return t7; 490: } 491: function TurnDurationMessage(t0) { 492: const $ = _c(17); 493: const { 494: message, 495: addMargin 496: } = t0; 497: const bg = useSelectedMessageBg(); 498: const [verb] = useState(_temp4); 499: const store = useAppStateStore(); 500: let t1; 501: if ($[0] !== store) { 502: t1 = () => { 503: const tasks = store.getState().tasks; 504: const running = (Object.values(tasks ?? {}) as TaskState[]).filter(isBackgroundTask); 505: return running.length > 0 ? getPillLabel(running) : null; 506: }; 507: $[0] = store; 508: $[1] = t1; 509: } else { 510: t1 = $[1]; 511: } 512: const [backgroundTaskSummary] = useState(t1); 513: let t2; 514: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 515: t2 = getGlobalConfig().showTurnDuration ?? true; 516: $[2] = t2; 517: } else { 518: t2 = $[2]; 519: } 520: const showTurnDuration = t2; 521: let t3; 522: if ($[3] !== message.durationMs) { 523: t3 = formatDuration(message.durationMs); 524: $[3] = message.durationMs; 525: $[4] = t3; 526: } else { 527: t3 = $[4]; 528: } 529: const duration = t3; 530: const hasBudget = message.budgetLimit !== undefined; 531: let t4; 532: bb0: { 533: if (!hasBudget) { 534: t4 = ""; 535: break bb0; 536: } 537: const tokens = message.budgetTokens; 538: const limit = message.budgetLimit; 539: let t5; 540: if ($[5] !== limit || $[6] !== tokens) { 541: t5 = tokens >= limit ? `${formatNumber(tokens)} used (${formatNumber(limit)} min ${figures.tick})` : `${formatNumber(tokens)} / ${formatNumber(limit)} (${Math.round(tokens / limit * 100)}%)`; 542: $[5] = limit; 543: $[6] = tokens; 544: $[7] = t5; 545: } else { 546: t5 = $[7]; 547: } 548: const usage = t5; 549: const nudges = message.budgetNudges > 0 ? ` \u00B7 ${message.budgetNudges} ${message.budgetNudges === 1 ? "nudge" : "nudges"}` : ""; 550: t4 = `${showTurnDuration ? " \xB7 " : ""}${usage}${nudges}`; 551: } 552: const budgetSuffix = t4; 553: if (!showTurnDuration && !hasBudget) { 554: return null; 555: } 556: const t5 = addMargin ? 1 : 0; 557: let t6; 558: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 559: t6 = <Box minWidth={2}><Text dimColor={true}>{TEARDROP_ASTERISK}</Text></Box>; 560: $[8] = t6; 561: } else { 562: t6 = $[8]; 563: } 564: const t7 = showTurnDuration && `${verb} for ${duration}`; 565: const t8 = backgroundTaskSummary && ` \u00B7 ${backgroundTaskSummary} still running`; 566: let t9; 567: if ($[9] !== budgetSuffix || $[10] !== t7 || $[11] !== t8) { 568: t9 = <Text dimColor={true}>{t7}{budgetSuffix}{t8}</Text>; 569: $[9] = budgetSuffix; 570: $[10] = t7; 571: $[11] = t8; 572: $[12] = t9; 573: } else { 574: t9 = $[12]; 575: } 576: let t10; 577: if ($[13] !== bg || $[14] !== t5 || $[15] !== t9) { 578: t10 = <Box flexDirection="row" marginTop={t5} backgroundColor={bg} width="100%">{t6}{t9}</Box>; 579: $[13] = bg; 580: $[14] = t5; 581: $[15] = t9; 582: $[16] = t10; 583: } else { 584: t10 = $[16]; 585: } 586: return t10; 587: } 588: function _temp4() { 589: return sample(TURN_COMPLETION_VERBS) ?? "Worked"; 590: } 591: function MemorySavedMessage(t0) { 592: const $ = _c(16); 593: const { 594: message, 595: addMargin 596: } = t0; 597: const bg = useSelectedMessageBg(); 598: const { 599: writtenPaths 600: } = message; 601: let t1; 602: if ($[0] !== message) { 603: t1 = feature("TEAMMEM") ? teamMemSaved.teamMemSavedPart(message) : null; 604: $[0] = message; 605: $[1] = t1; 606: } else { 607: t1 = $[1]; 608: } 609: const team = t1; 610: const privateCount = writtenPaths.length - (team?.count ?? 0); 611: const t2 = privateCount > 0 ? `${privateCount} ${privateCount === 1 ? "memory" : "memories"}` : null; 612: const t3 = team?.segment; 613: let t4; 614: if ($[2] !== t2 || $[3] !== t3) { 615: t4 = [t2, t3].filter(Boolean); 616: $[2] = t2; 617: $[3] = t3; 618: $[4] = t4; 619: } else { 620: t4 = $[4]; 621: } 622: const parts = t4; 623: const t5 = addMargin ? 1 : 0; 624: let t6; 625: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 626: t6 = <Box minWidth={2}><Text dimColor={true}>{BLACK_CIRCLE}</Text></Box>; 627: $[5] = t6; 628: } else { 629: t6 = $[5]; 630: } 631: const t7 = message.verb ?? "Saved"; 632: const t8 = parts.join(" \xB7 "); 633: let t9; 634: if ($[6] !== t7 || $[7] !== t8) { 635: t9 = <Box flexDirection="row">{t6}<Text>{t7} {t8}</Text></Box>; 636: $[6] = t7; 637: $[7] = t8; 638: $[8] = t9; 639: } else { 640: t9 = $[8]; 641: } 642: let t10; 643: if ($[9] !== writtenPaths) { 644: t10 = writtenPaths.map(_temp5); 645: $[9] = writtenPaths; 646: $[10] = t10; 647: } else { 648: t10 = $[10]; 649: } 650: let t11; 651: if ($[11] !== bg || $[12] !== t10 || $[13] !== t5 || $[14] !== t9) { 652: t11 = <Box flexDirection="column" marginTop={t5} backgroundColor={bg}>{t9}{t10}</Box>; 653: $[11] = bg; 654: $[12] = t10; 655: $[13] = t5; 656: $[14] = t9; 657: $[15] = t11; 658: } else { 659: t11 = $[15]; 660: } 661: return t11; 662: } 663: function _temp5(p) { 664: return <MemoryFileRow key={p} path={p} />; 665: } 666: function MemoryFileRow(t0) { 667: const $ = _c(16); 668: const { 669: path 670: } = t0; 671: const [hover, setHover] = useState(false); 672: let t1; 673: if ($[0] !== path) { 674: t1 = () => void openPath(path); 675: $[0] = path; 676: $[1] = t1; 677: } else { 678: t1 = $[1]; 679: } 680: let t2; 681: let t3; 682: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 683: t2 = () => setHover(true); 684: t3 = () => setHover(false); 685: $[2] = t2; 686: $[3] = t3; 687: } else { 688: t2 = $[2]; 689: t3 = $[3]; 690: } 691: const t4 = !hover; 692: let t5; 693: if ($[4] !== path) { 694: t5 = basename(path); 695: $[4] = path; 696: $[5] = t5; 697: } else { 698: t5 = $[5]; 699: } 700: let t6; 701: if ($[6] !== path || $[7] !== t5) { 702: t6 = <FilePathLink filePath={path}>{t5}</FilePathLink>; 703: $[6] = path; 704: $[7] = t5; 705: $[8] = t6; 706: } else { 707: t6 = $[8]; 708: } 709: let t7; 710: if ($[9] !== hover || $[10] !== t4 || $[11] !== t6) { 711: t7 = <Text dimColor={t4} underline={hover}>{t6}</Text>; 712: $[9] = hover; 713: $[10] = t4; 714: $[11] = t6; 715: $[12] = t7; 716: } else { 717: t7 = $[12]; 718: } 719: let t8; 720: if ($[13] !== t1 || $[14] !== t7) { 721: t8 = <MessageResponse><Box onClick={t1} onMouseEnter={t2} onMouseLeave={t3}>{t7}</Box></MessageResponse>; 722: $[13] = t1; 723: $[14] = t7; 724: $[15] = t8; 725: } else { 726: t8 = $[15]; 727: } 728: return t8; 729: } 730: function ThinkingMessage(t0) { 731: const $ = _c(7); 732: const { 733: message, 734: addMargin 735: } = t0; 736: const bg = useSelectedMessageBg(); 737: const t1 = addMargin ? 1 : 0; 738: let t2; 739: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 740: t2 = <Box minWidth={2}><Text dimColor={true}>{TEARDROP_ASTERISK}</Text></Box>; 741: $[0] = t2; 742: } else { 743: t2 = $[0]; 744: } 745: let t3; 746: if ($[1] !== message.content) { 747: t3 = <Text dimColor={true}>{message.content}</Text>; 748: $[1] = message.content; 749: $[2] = t3; 750: } else { 751: t3 = $[2]; 752: } 753: let t4; 754: if ($[3] !== bg || $[4] !== t1 || $[5] !== t3) { 755: t4 = <Box flexDirection="row" marginTop={t1} backgroundColor={bg} width="100%">{t2}{t3}</Box>; 756: $[3] = bg; 757: $[4] = t1; 758: $[5] = t3; 759: $[6] = t4; 760: } else { 761: t4 = $[6]; 762: } 763: return t4; 764: } 765: function BridgeStatusMessage(t0) { 766: const $ = _c(13); 767: const { 768: message, 769: addMargin 770: } = t0; 771: const bg = useSelectedMessageBg(); 772: const t1 = addMargin ? 1 : 0; 773: let t2; 774: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 775: t2 = <Box minWidth={2} />; 776: $[0] = t2; 777: } else { 778: t2 = $[0]; 779: } 780: let t3; 781: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 782: t3 = <Text><ThemedText color="suggestion">/remote-control</ThemedText> is active. Code in CLI or at</Text>; 783: $[1] = t3; 784: } else { 785: t3 = $[1]; 786: } 787: let t4; 788: if ($[2] !== message.url) { 789: t4 = <Link url={message.url}>{message.url}</Link>; 790: $[2] = message.url; 791: $[3] = t4; 792: } else { 793: t4 = $[3]; 794: } 795: let t5; 796: if ($[4] !== message.upgradeNudge) { 797: t5 = message.upgradeNudge && <Text dimColor={true}>⎿ {message.upgradeNudge}</Text>; 798: $[4] = message.upgradeNudge; 799: $[5] = t5; 800: } else { 801: t5 = $[5]; 802: } 803: let t6; 804: if ($[6] !== t4 || $[7] !== t5) { 805: t6 = <Box flexDirection="column">{t3}{t4}{t5}</Box>; 806: $[6] = t4; 807: $[7] = t5; 808: $[8] = t6; 809: } else { 810: t6 = $[8]; 811: } 812: let t7; 813: if ($[9] !== bg || $[10] !== t1 || $[11] !== t6) { 814: t7 = <Box flexDirection="row" marginTop={t1} backgroundColor={bg} width={999}>{t2}{t6}</Box>; 815: $[9] = bg; 816: $[10] = t1; 817: $[11] = t6; 818: $[12] = t7; 819: } else { 820: t7 = $[12]; 821: } 822: return t7; 823: }

File: src/components/messages/TaskAssignmentMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { isTaskAssignment, type TaskAssignmentMessage } from '../../utils/teammateMailbox.js'; 5: type Props = { 6: assignment: TaskAssignmentMessage; 7: }; 8: export function TaskAssignmentDisplay(t0) { 9: const $ = _c(11); 10: const { 11: assignment 12: } = t0; 13: let t1; 14: if ($[0] !== assignment.assignedBy || $[1] !== assignment.taskId) { 15: t1 = <Box marginBottom={1}><Text color="cyan_FOR_SUBAGENTS_ONLY" bold={true}>Task #{assignment.taskId} assigned by {assignment.assignedBy}</Text></Box>; 16: $[0] = assignment.assignedBy; 17: $[1] = assignment.taskId; 18: $[2] = t1; 19: } else { 20: t1 = $[2]; 21: } 22: let t2; 23: if ($[3] !== assignment.subject) { 24: t2 = <Box><Text bold={true}>{assignment.subject}</Text></Box>; 25: $[3] = assignment.subject; 26: $[4] = t2; 27: } else { 28: t2 = $[4]; 29: } 30: let t3; 31: if ($[5] !== assignment.description) { 32: t3 = assignment.description && <Box marginTop={1}><Text dimColor={true}>{assignment.description}</Text></Box>; 33: $[5] = assignment.description; 34: $[6] = t3; 35: } else { 36: t3 = $[6]; 37: } 38: let t4; 39: if ($[7] !== t1 || $[8] !== t2 || $[9] !== t3) { 40: t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="cyan_FOR_SUBAGENTS_ONLY" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}{t3}</Box></Box>; 41: $[7] = t1; 42: $[8] = t2; 43: $[9] = t3; 44: $[10] = t4; 45: } else { 46: t4 = $[10]; 47: } 48: return t4; 49: } 50: export function tryRenderTaskAssignmentMessage(content: string): React.ReactNode | null { 51: const assignment = isTaskAssignment(content); 52: if (assignment) { 53: return <TaskAssignmentDisplay assignment={assignment} />; 54: } 55: return null; 56: } 57: export function getTaskAssignmentSummary(content: string): string | null { 58: const assignment = isTaskAssignment(content); 59: if (assignment) { 60: return `[Task Assigned] #${assignment.taskId} - ${assignment.subject}`; 61: } 62: return null; 63: }

File: src/components/messages/teamMemCollapsed.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Text } from '../../ink.js'; 4: import type { CollapsedReadSearchGroup } from '../../types/message.js'; 5: export function checkHasTeamMemOps(message: CollapsedReadSearchGroup): boolean { 6: return (message.teamMemorySearchCount ?? 0) > 0 || (message.teamMemoryReadCount ?? 0) > 0 || (message.teamMemoryWriteCount ?? 0) > 0; 7: } 8: export function TeamMemCountParts(t0) { 9: const $ = _c(23); 10: const { 11: message, 12: isActiveGroup, 13: hasPrecedingParts 14: } = t0; 15: const tmReadCount = message.teamMemoryReadCount ?? 0; 16: const tmSearchCount = message.teamMemorySearchCount ?? 0; 17: const tmWriteCount = message.teamMemoryWriteCount ?? 0; 18: if (tmReadCount === 0 && tmSearchCount === 0 && tmWriteCount === 0) { 19: return null; 20: } 21: let t1; 22: if ($[0] !== hasPrecedingParts || $[1] !== isActiveGroup || $[2] !== tmReadCount || $[3] !== tmSearchCount || $[4] !== tmWriteCount) { 23: const nodes = []; 24: let count = hasPrecedingParts ? 1 : 0; 25: if (tmReadCount > 0) { 26: const verb = isActiveGroup ? count === 0 ? "Recalling" : "recalling" : count === 0 ? "Recalled" : "recalled"; 27: if (count > 0) { 28: let t2; 29: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 30: t2 = <Text key="comma-tmr">, </Text>; 31: $[6] = t2; 32: } else { 33: t2 = $[6]; 34: } 35: nodes.push(t2); 36: } 37: let t2; 38: if ($[7] !== tmReadCount) { 39: t2 = <Text bold={true}>{tmReadCount}</Text>; 40: $[7] = tmReadCount; 41: $[8] = t2; 42: } else { 43: t2 = $[8]; 44: } 45: const t3 = tmReadCount === 1 ? "memory" : "memories"; 46: let t4; 47: if ($[9] !== t2 || $[10] !== t3 || $[11] !== verb) { 48: t4 = <Text key="team-mem-read">{verb} {t2} team{" "}{t3}</Text>; 49: $[9] = t2; 50: $[10] = t3; 51: $[11] = verb; 52: $[12] = t4; 53: } else { 54: t4 = $[12]; 55: } 56: nodes.push(t4); 57: count++; 58: } 59: if (tmSearchCount > 0) { 60: const verb_0 = isActiveGroup ? count === 0 ? "Searching" : "searching" : count === 0 ? "Searched" : "searched"; 61: if (count > 0) { 62: let t2; 63: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 64: t2 = <Text key="comma-tms">, </Text>; 65: $[13] = t2; 66: } else { 67: t2 = $[13]; 68: } 69: nodes.push(t2); 70: } 71: const t2 = `${verb_0} team memories`; 72: let t3; 73: if ($[14] !== t2) { 74: t3 = <Text key="team-mem-search">{t2}</Text>; 75: $[14] = t2; 76: $[15] = t3; 77: } else { 78: t3 = $[15]; 79: } 80: nodes.push(t3); 81: count++; 82: } 83: if (tmWriteCount > 0) { 84: const verb_1 = isActiveGroup ? count === 0 ? "Writing" : "writing" : count === 0 ? "Wrote" : "wrote"; 85: if (count > 0) { 86: let t2; 87: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 88: t2 = <Text key="comma-tmw">, </Text>; 89: $[16] = t2; 90: } else { 91: t2 = $[16]; 92: } 93: nodes.push(t2); 94: } 95: let t2; 96: if ($[17] !== tmWriteCount) { 97: t2 = <Text bold={true}>{tmWriteCount}</Text>; 98: $[17] = tmWriteCount; 99: $[18] = t2; 100: } else { 101: t2 = $[18]; 102: } 103: const t3 = tmWriteCount === 1 ? "memory" : "memories"; 104: let t4; 105: if ($[19] !== t2 || $[20] !== t3 || $[21] !== verb_1) { 106: t4 = <Text key="team-mem-write">{verb_1} {t2} team{" "}{t3}</Text>; 107: $[19] = t2; 108: $[20] = t3; 109: $[21] = verb_1; 110: $[22] = t4; 111: } else { 112: t4 = $[22]; 113: } 114: nodes.push(t4); 115: } 116: t1 = <>{nodes}</>; 117: $[0] = hasPrecedingParts; 118: $[1] = isActiveGroup; 119: $[2] = tmReadCount; 120: $[3] = tmSearchCount; 121: $[4] = tmWriteCount; 122: $[5] = t1; 123: } else { 124: t1 = $[5]; 125: } 126: return t1; 127: }

File: src/components/messages/teamMemSaved.ts

typescript 1: import type { SystemMemorySavedMessage } from '../../types/message.js' 2: export function teamMemSavedPart( 3: message: SystemMemorySavedMessage, 4: ): { segment: string; count: number } | null { 5: const count = message.teamCount ?? 0 6: if (count === 0) return null 7: return { 8: segment: `${count} team ${count === 1 ? 'memory' : 'memories'}`, 9: count, 10: } 11: }

File: src/components/messages/UserAgentNotificationMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import * as React from 'react'; 4: import { BLACK_CIRCLE } from '../../constants/figures.js'; 5: import { Box, Text, type TextProps } from '../../ink.js'; 6: import { extractTag } from '../../utils/messages.js'; 7: type Props = { 8: addMargin: boolean; 9: param: TextBlockParam; 10: }; 11: function getStatusColor(status: string | null): TextProps['color'] { 12: switch (status) { 13: case 'completed': 14: return 'success'; 15: case 'failed': 16: return 'error'; 17: case 'killed': 18: return 'warning'; 19: default: 20: return 'text'; 21: } 22: } 23: export function UserAgentNotificationMessage(t0) { 24: const $ = _c(12); 25: const { 26: addMargin, 27: param: t1 28: } = t0; 29: const { 30: text 31: } = t1; 32: let t2; 33: if ($[0] !== text) { 34: t2 = extractTag(text, "summary"); 35: $[0] = text; 36: $[1] = t2; 37: } else { 38: t2 = $[1]; 39: } 40: const summary = t2; 41: if (!summary) { 42: return null; 43: } 44: let t3; 45: if ($[2] !== text) { 46: const status = extractTag(text, "status"); 47: t3 = getStatusColor(status); 48: $[2] = text; 49: $[3] = t3; 50: } else { 51: t3 = $[3]; 52: } 53: const color = t3; 54: const t4 = addMargin ? 1 : 0; 55: let t5; 56: if ($[4] !== color) { 57: t5 = <Text color={color}>{BLACK_CIRCLE}</Text>; 58: $[4] = color; 59: $[5] = t5; 60: } else { 61: t5 = $[5]; 62: } 63: let t6; 64: if ($[6] !== summary || $[7] !== t5) { 65: t6 = <Text>{t5} {summary}</Text>; 66: $[6] = summary; 67: $[7] = t5; 68: $[8] = t6; 69: } else { 70: t6 = $[8]; 71: } 72: let t7; 73: if ($[9] !== t4 || $[10] !== t6) { 74: t7 = <Box marginTop={t4}>{t6}</Box>; 75: $[9] = t4; 76: $[10] = t6; 77: $[11] = t7; 78: } else { 79: t7 = $[11]; 80: } 81: return t7; 82: }

File: src/components/messages/UserBashInputMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import * as React from 'react'; 4: import { Box, Text } from '../../ink.js'; 5: import { extractTag } from '../../utils/messages.js'; 6: type Props = { 7: addMargin: boolean; 8: param: TextBlockParam; 9: }; 10: export function UserBashInputMessage(t0) { 11: const $ = _c(8); 12: const { 13: param: t1, 14: addMargin 15: } = t0; 16: const { 17: text 18: } = t1; 19: let t2; 20: if ($[0] !== text) { 21: t2 = extractTag(text, "bash-input"); 22: $[0] = text; 23: $[1] = t2; 24: } else { 25: t2 = $[1]; 26: } 27: const input = t2; 28: if (!input) { 29: return null; 30: } 31: const t3 = addMargin ? 1 : 0; 32: let t4; 33: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 34: t4 = <Text color="bashBorder">! </Text>; 35: $[2] = t4; 36: } else { 37: t4 = $[2]; 38: } 39: let t5; 40: if ($[3] !== input) { 41: t5 = <Text color="text">{input}</Text>; 42: $[3] = input; 43: $[4] = t5; 44: } else { 45: t5 = $[4]; 46: } 47: let t6; 48: if ($[5] !== t3 || $[6] !== t5) { 49: t6 = <Box flexDirection="row" marginTop={t3} backgroundColor="bashMessageBackgroundColor" paddingRight={1}>{t4}{t5}</Box>; 50: $[5] = t3; 51: $[6] = t5; 52: $[7] = t6; 53: } else { 54: t6 = $[7]; 55: } 56: return t6; 57: }

File: src/components/messages/UserBashOutputMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import BashToolResultMessage from '../../tools/BashTool/BashToolResultMessage.js'; 4: import { extractTag } from '../../utils/messages.js'; 5: export function UserBashOutputMessage(t0) { 6: const $ = _c(10); 7: const { 8: content, 9: verbose 10: } = t0; 11: let t1; 12: if ($[0] !== content) { 13: const rawStdout = extractTag(content, "bash-stdout") ?? ""; 14: t1 = extractTag(rawStdout, "persisted-output") ?? rawStdout; 15: $[0] = content; 16: $[1] = t1; 17: } else { 18: t1 = $[1]; 19: } 20: const stdout = t1; 21: let t2; 22: if ($[2] !== content) { 23: t2 = extractTag(content, "bash-stderr") ?? ""; 24: $[2] = content; 25: $[3] = t2; 26: } else { 27: t2 = $[3]; 28: } 29: const stderr = t2; 30: let t3; 31: if ($[4] !== stderr || $[5] !== stdout) { 32: t3 = { 33: stdout, 34: stderr 35: }; 36: $[4] = stderr; 37: $[5] = stdout; 38: $[6] = t3; 39: } else { 40: t3 = $[6]; 41: } 42: const t4 = !!verbose; 43: let t5; 44: if ($[7] !== t3 || $[8] !== t4) { 45: t5 = <BashToolResultMessage content={t3} verbose={t4} />; 46: $[7] = t3; 47: $[8] = t4; 48: $[9] = t5; 49: } else { 50: t5 = $[9]; 51: } 52: return t5; 53: }

File: src/components/messages/UserChannelMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import * as React from 'react'; 4: import { CHANNEL_ARROW } from '../../constants/figures.js'; 5: import { CHANNEL_TAG } from '../../constants/xml.js'; 6: import { Box, Text } from '../../ink.js'; 7: import { truncateToWidth } from '../../utils/format.js'; 8: type Props = { 9: addMargin: boolean; 10: param: TextBlockParam; 11: }; 12: const CHANNEL_RE = new RegExp(`<${CHANNEL_TAG}\\s+source="([^"]+)"([^>]*)>\\n?([\\s\\S]*?)\\n?</${CHANNEL_TAG}>`); 13: const USER_ATTR_RE = /\buser="([^"]+)"/; 14: // Plugin-provided servers get names like plugin:slack-channel:slack via 15: // addPluginScopeToServers — show just the leaf. Matches the suffix-match 16: // logic in isServerInChannels. 17: function displayServerName(name: string): string { 18: const i = name.lastIndexOf(':'); 19: return i === -1 ? name : name.slice(i + 1); 20: } 21: const TRUNCATE_AT = 60; 22: export function UserChannelMessage(t0) { 23: const $ = _c(29); 24: const { 25: addMargin, 26: param: t1 27: } = t0; 28: const { 29: text 30: } = t1; 31: let T0; 32: let T1; 33: let T2; 34: let t2; 35: let t3; 36: let t4; 37: let t5; 38: let t6; 39: let t7; 40: let truncated; 41: let user; 42: if ($[0] !== addMargin || $[1] !== text) { 43: t7 = Symbol.for("react.early_return_sentinel"); 44: bb0: { 45: const m = CHANNEL_RE.exec(text); 46: if (!m) { 47: t7 = null; 48: break bb0; 49: } 50: const [, source, attrs, content] = m; 51: user = USER_ATTR_RE.exec(attrs ?? "")?.[1]; 52: const body = (content ?? "").trim().replace(/\s+/g, " "); 53: truncated = truncateToWidth(body, TRUNCATE_AT); 54: T2 = Box; 55: t6 = addMargin ? 1 : 0; 56: T1 = Text; 57: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 58: t4 = <Text color="suggestion">{CHANNEL_ARROW}</Text>; 59: $[13] = t4; 60: } else { 61: t4 = $[13]; 62: } 63: t5 = " "; 64: T0 = Text; 65: t2 = true; 66: t3 = displayServerName(source ?? ""); 67: } 68: $[0] = addMargin; 69: $[1] = text; 70: $[2] = T0; 71: $[3] = T1; 72: $[4] = T2; 73: $[5] = t2; 74: $[6] = t3; 75: $[7] = t4; 76: $[8] = t5; 77: $[9] = t6; 78: $[10] = t7; 79: $[11] = truncated; 80: $[12] = user; 81: } else { 82: T0 = $[2]; 83: T1 = $[3]; 84: T2 = $[4]; 85: t2 = $[5]; 86: t3 = $[6]; 87: t4 = $[7]; 88: t5 = $[8]; 89: t6 = $[9]; 90: t7 = $[10]; 91: truncated = $[11]; 92: user = $[12]; 93: } 94: if (t7 !== Symbol.for("react.early_return_sentinel")) { 95: return t7; 96: } 97: const t8 = user ? ` \u00b7 ${user}` : ""; 98: let t9; 99: if ($[14] !== T0 || $[15] !== t2 || $[16] !== t3 || $[17] !== t8) { 100: t9 = <T0 dimColor={t2}>{t3}{t8}:</T0>; 101: $[14] = T0; 102: $[15] = t2; 103: $[16] = t3; 104: $[17] = t8; 105: $[18] = t9; 106: } else { 107: t9 = $[18]; 108: } 109: let t10; 110: if ($[19] !== T1 || $[20] !== t4 || $[21] !== t5 || $[22] !== t9 || $[23] !== truncated) { 111: t10 = <T1>{t4}{t5}{t9}{" "}{truncated}</T1>; 112: $[19] = T1; 113: $[20] = t4; 114: $[21] = t5; 115: $[22] = t9; 116: $[23] = truncated; 117: $[24] = t10; 118: } else { 119: t10 = $[24]; 120: } 121: let t11; 122: if ($[25] !== T2 || $[26] !== t10 || $[27] !== t6) { 123: t11 = <T2 marginTop={t6}>{t10}</T2>; 124: $[25] = T2; 125: $[26] = t10; 126: $[27] = t6; 127: $[28] = t11; 128: } else { 129: t11 = $[28]; 130: } 131: return t11; 132: }

File: src/components/messages/UserCommandMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import figures from 'figures'; 4: import * as React from 'react'; 5: import { COMMAND_MESSAGE_TAG } from '../../constants/xml.js'; 6: import { Box, Text } from '../../ink.js'; 7: import { extractTag } from '../../utils/messages.js'; 8: type Props = { 9: addMargin: boolean; 10: param: TextBlockParam; 11: }; 12: export function UserCommandMessage(t0) { 13: const $ = _c(19); 14: const { 15: addMargin, 16: param: t1 17: } = t0; 18: const { 19: text 20: } = t1; 21: let t2; 22: if ($[0] !== text) { 23: t2 = extractTag(text, COMMAND_MESSAGE_TAG); 24: $[0] = text; 25: $[1] = t2; 26: } else { 27: t2 = $[1]; 28: } 29: const commandMessage = t2; 30: let t3; 31: if ($[2] !== text) { 32: t3 = extractTag(text, "command-args"); 33: $[2] = text; 34: $[3] = t3; 35: } else { 36: t3 = $[3]; 37: } 38: const args = t3; 39: const isSkillFormat = extractTag(text, "skill-format") === "true"; 40: if (!commandMessage) { 41: return null; 42: } 43: if (isSkillFormat) { 44: const t4 = addMargin ? 1 : 0; 45: let t5; 46: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 47: t5 = <Text color="subtle">{figures.pointer} </Text>; 48: $[4] = t5; 49: } else { 50: t5 = $[4]; 51: } 52: let t6; 53: if ($[5] !== commandMessage) { 54: t6 = <Text>{t5}<Text color="text">Skill({commandMessage})</Text></Text>; 55: $[5] = commandMessage; 56: $[6] = t6; 57: } else { 58: t6 = $[6]; 59: } 60: let t7; 61: if ($[7] !== t4 || $[8] !== t6) { 62: t7 = <Box flexDirection="column" marginTop={t4} backgroundColor="userMessageBackground" paddingRight={1}>{t6}</Box>; 63: $[7] = t4; 64: $[8] = t6; 65: $[9] = t7; 66: } else { 67: t7 = $[9]; 68: } 69: return t7; 70: } 71: let t4; 72: if ($[10] !== args || $[11] !== commandMessage) { 73: t4 = [commandMessage, args].filter(Boolean); 74: $[10] = args; 75: $[11] = commandMessage; 76: $[12] = t4; 77: } else { 78: t4 = $[12]; 79: } 80: const content = `/${t4.join(" ")}`; 81: const t5 = addMargin ? 1 : 0; 82: let t6; 83: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 84: t6 = <Text color="subtle">{figures.pointer} </Text>; 85: $[13] = t6; 86: } else { 87: t6 = $[13]; 88: } 89: let t7; 90: if ($[14] !== content) { 91: t7 = <Text>{t6}<Text color="text">{content}</Text></Text>; 92: $[14] = content; 93: $[15] = t7; 94: } else { 95: t7 = $[15]; 96: } 97: let t8; 98: if ($[16] !== t5 || $[17] !== t7) { 99: t8 = <Box flexDirection="column" marginTop={t5} backgroundColor="userMessageBackground" paddingRight={1}>{t7}</Box>; 100: $[16] = t5; 101: $[17] = t7; 102: $[18] = t8; 103: } else { 104: t8 = $[18]; 105: } 106: return t8; 107: }

File: src/components/messages/UserImageMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { pathToFileURL } from 'url'; 4: import Link from '../../ink/components/Link.js'; 5: import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js'; 6: import { Box, Text } from '../../ink.js'; 7: import { getStoredImagePath } from '../../utils/imageStore.js'; 8: import { MessageResponse } from '../MessageResponse.js'; 9: type Props = { 10: imageId?: number; 11: addMargin?: boolean; 12: }; 13: export function UserImageMessage(t0) { 14: const $ = _c(7); 15: const { 16: imageId, 17: addMargin 18: } = t0; 19: const label = imageId ? `[Image #${imageId}]` : "[Image]"; 20: let t1; 21: if ($[0] !== imageId || $[1] !== label) { 22: const imagePath = imageId ? getStoredImagePath(imageId) : null; 23: t1 = imagePath && supportsHyperlinks() ? <Link url={pathToFileURL(imagePath).href}><Text>{label}</Text></Link> : <Text>{label}</Text>; 24: $[0] = imageId; 25: $[1] = label; 26: $[2] = t1; 27: } else { 28: t1 = $[2]; 29: } 30: const content = t1; 31: if (addMargin) { 32: let t2; 33: if ($[3] !== content) { 34: t2 = <Box marginTop={1}>{content}</Box>; 35: $[3] = content; 36: $[4] = t2; 37: } else { 38: t2 = $[4]; 39: } 40: return t2; 41: } 42: let t2; 43: if ($[5] !== content) { 44: t2 = <MessageResponse>{content}</MessageResponse>; 45: $[5] = content; 46: $[6] = t2; 47: } else { 48: t2 = $[6]; 49: } 50: return t2; 51: }

File: src/components/messages/UserLocalCommandOutputMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'; 4: import { NO_CONTENT_MESSAGE } from '../../constants/messages.js'; 5: import { Box, Text } from '../../ink.js'; 6: import { extractTag } from '../../utils/messages.js'; 7: import { Markdown } from '../Markdown.js'; 8: import { MessageResponse } from '../MessageResponse.js'; 9: type Props = { 10: content: string; 11: }; 12: export function UserLocalCommandOutputMessage(t0) { 13: const $ = _c(4); 14: const { 15: content 16: } = t0; 17: let lines; 18: let t1; 19: if ($[0] !== content) { 20: t1 = Symbol.for("react.early_return_sentinel"); 21: bb0: { 22: const stdout = extractTag(content, "local-command-stdout"); 23: const stderr = extractTag(content, "local-command-stderr"); 24: if (!stdout && !stderr) { 25: let t2; 26: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 27: t2 = <MessageResponse><Text dimColor={true}>{NO_CONTENT_MESSAGE}</Text></MessageResponse>; 28: $[3] = t2; 29: } else { 30: t2 = $[3]; 31: } 32: t1 = t2; 33: break bb0; 34: } 35: lines = []; 36: if (stdout?.trim()) { 37: lines.push(<IndentedContent key="stdout">{stdout.trim()}</IndentedContent>); 38: } 39: if (stderr?.trim()) { 40: lines.push(<IndentedContent key="stderr">{stderr.trim()}</IndentedContent>); 41: } 42: } 43: $[0] = content; 44: $[1] = lines; 45: $[2] = t1; 46: } else { 47: lines = $[1]; 48: t1 = $[2]; 49: } 50: if (t1 !== Symbol.for("react.early_return_sentinel")) { 51: return t1; 52: } 53: return lines; 54: } 55: function IndentedContent(t0) { 56: const $ = _c(5); 57: const { 58: children 59: } = t0; 60: if (children.startsWith(`${DIAMOND_OPEN} `) || children.startsWith(`${DIAMOND_FILLED} `)) { 61: let t1; 62: if ($[0] !== children) { 63: t1 = <CloudLaunchContent>{children}</CloudLaunchContent>; 64: $[0] = children; 65: $[1] = t1; 66: } else { 67: t1 = $[1]; 68: } 69: return t1; 70: } 71: let t1; 72: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 73: t1 = <Text dimColor={true}>{" \u23BF "}</Text>; 74: $[2] = t1; 75: } else { 76: t1 = $[2]; 77: } 78: let t2; 79: if ($[3] !== children) { 80: t2 = <Box flexDirection="row">{t1}<Box flexDirection="column" flexGrow={1}><Markdown>{children}</Markdown></Box></Box>; 81: $[3] = children; 82: $[4] = t2; 83: } else { 84: t2 = $[4]; 85: } 86: return t2; 87: } 88: function CloudLaunchContent(t0) { 89: const $ = _c(19); 90: const { 91: children 92: } = t0; 93: const diamond = children[0]; 94: let label; 95: let rest; 96: let t1; 97: if ($[0] !== children) { 98: const nl = children.indexOf("\n"); 99: const header = nl === -1 ? children.slice(2) : children.slice(2, nl); 100: rest = nl === -1 ? "" : children.slice(nl + 1).trim(); 101: const sep = header.indexOf(" \xB7 "); 102: label = sep === -1 ? header : header.slice(0, sep); 103: t1 = sep === -1 ? "" : header.slice(sep); 104: $[0] = children; 105: $[1] = label; 106: $[2] = rest; 107: $[3] = t1; 108: } else { 109: label = $[1]; 110: rest = $[2]; 111: t1 = $[3]; 112: } 113: const suffix = t1; 114: let t2; 115: if ($[4] !== diamond) { 116: t2 = <Text color="background">{diamond} </Text>; 117: $[4] = diamond; 118: $[5] = t2; 119: } else { 120: t2 = $[5]; 121: } 122: let t3; 123: if ($[6] !== label) { 124: t3 = <Text bold={true}>{label}</Text>; 125: $[6] = label; 126: $[7] = t3; 127: } else { 128: t3 = $[7]; 129: } 130: let t4; 131: if ($[8] !== suffix) { 132: t4 = suffix && <Text dimColor={true}>{suffix}</Text>; 133: $[8] = suffix; 134: $[9] = t4; 135: } else { 136: t4 = $[9]; 137: } 138: let t5; 139: if ($[10] !== t2 || $[11] !== t3 || $[12] !== t4) { 140: t5 = <Text>{t2}{t3}{t4}</Text>; 141: $[10] = t2; 142: $[11] = t3; 143: $[12] = t4; 144: $[13] = t5; 145: } else { 146: t5 = $[13]; 147: } 148: let t6; 149: if ($[14] !== rest) { 150: t6 = rest && <Box flexDirection="row"><Text dimColor={true}>{" \u23BF "}</Text><Text dimColor={true}>{rest}</Text></Box>; 151: $[14] = rest; 152: $[15] = t6; 153: } else { 154: t6 = $[15]; 155: } 156: let t7; 157: if ($[16] !== t5 || $[17] !== t6) { 158: t7 = <Box flexDirection="column">{t5}{t6}</Box>; 159: $[16] = t5; 160: $[17] = t6; 161: $[18] = t7; 162: } else { 163: t7 = $[18]; 164: } 165: return t7; 166: }

File: src/components/messages/UserMemoryInputMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import sample from 'lodash-es/sample.js'; 3: import * as React from 'react'; 4: import { useMemo } from 'react'; 5: import { Box, Text } from '../../ink.js'; 6: import { extractTag } from '../../utils/messages.js'; 7: import { MessageResponse } from '../MessageResponse.js'; 8: function getSavingMessage(): string { 9: return sample(['Got it.', 'Good to know.', 'Noted.']); 10: } 11: type Props = { 12: addMargin: boolean; 13: text: string; 14: }; 15: export function UserMemoryInputMessage(t0) { 16: const $ = _c(10); 17: const { 18: text, 19: addMargin 20: } = t0; 21: let t1; 22: if ($[0] !== text) { 23: t1 = extractTag(text, "user-memory-input"); 24: $[0] = text; 25: $[1] = t1; 26: } else { 27: t1 = $[1]; 28: } 29: const input = t1; 30: let t2; 31: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 32: t2 = getSavingMessage(); 33: $[2] = t2; 34: } else { 35: t2 = $[2]; 36: } 37: const savingText = t2; 38: if (!input) { 39: return null; 40: } 41: const t3 = addMargin ? 1 : 0; 42: let t4; 43: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 44: t4 = <Text color="remember" backgroundColor="memoryBackgroundColor">#</Text>; 45: $[3] = t4; 46: } else { 47: t4 = $[3]; 48: } 49: let t5; 50: if ($[4] !== input) { 51: t5 = <Box>{t4}<Text backgroundColor="memoryBackgroundColor" color="text">{" "}{input}{" "}</Text></Box>; 52: $[4] = input; 53: $[5] = t5; 54: } else { 55: t5 = $[5]; 56: } 57: let t6; 58: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 59: t6 = <MessageResponse height={1}><Text dimColor={true}>{savingText}</Text></MessageResponse>; 60: $[6] = t6; 61: } else { 62: t6 = $[6]; 63: } 64: let t7; 65: if ($[7] !== t3 || $[8] !== t5) { 66: t7 = <Box flexDirection="column" marginTop={t3} width="100%">{t5}{t6}</Box>; 67: $[7] = t3; 68: $[8] = t5; 69: $[9] = t7; 70: } else { 71: t7 = $[9]; 72: } 73: return t7; 74: }

File: src/components/messages/UserPlanMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { Markdown } from '../Markdown.js'; 5: type Props = { 6: addMargin: boolean; 7: planContent: string; 8: }; 9: export function UserPlanMessage(t0) { 10: const $ = _c(6); 11: const { 12: addMargin, 13: planContent 14: } = t0; 15: const t1 = addMargin ? 1 : 0; 16: let t2; 17: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 18: t2 = <Box marginBottom={1}><Text bold={true} color="planMode">Plan to implement</Text></Box>; 19: $[0] = t2; 20: } else { 21: t2 = $[0]; 22: } 23: let t3; 24: if ($[1] !== planContent) { 25: t3 = <Markdown>{planContent}</Markdown>; 26: $[1] = planContent; 27: $[2] = t3; 28: } else { 29: t3 = $[2]; 30: } 31: let t4; 32: if ($[3] !== t1 || $[4] !== t3) { 33: t4 = <Box flexDirection="column" borderStyle="round" borderColor="planMode" marginTop={t1} paddingX={1}>{t2}{t3}</Box>; 34: $[3] = t1; 35: $[4] = t3; 36: $[5] = t4; 37: } else { 38: t4 = $[5]; 39: } 40: return t4; 41: }

File: src/components/messages/UserPromptMessage.tsx

typescript 1: import { feature } from 'bun:bundle'; 2: import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import React, { useContext, useMemo } from 'react'; 4: import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js'; 5: import { Box } from '../../ink.js'; 6: import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; 7: import { useAppState } from '../../state/AppState.js'; 8: import { isEnvTruthy } from '../../utils/envUtils.js'; 9: import { logError } from '../../utils/log.js'; 10: import { countCharInString } from '../../utils/stringUtils.js'; 11: import { MessageActionsSelectedContext } from '../messageActions.js'; 12: import { HighlightedThinkingText } from './HighlightedThinkingText.js'; 13: type Props = { 14: addMargin: boolean; 15: param: TextBlockParam; 16: isTranscriptMode?: boolean; 17: timestamp?: string; 18: }; 19: const MAX_DISPLAY_CHARS = 10_000; 20: const TRUNCATE_HEAD_CHARS = 2_500; 21: const TRUNCATE_TAIL_CHARS = 2_500; 22: export function UserPromptMessage({ 23: addMargin, 24: param: { 25: text 26: }, 27: isTranscriptMode, 28: timestamp 29: }: Props): React.ReactNode { 30: const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ? 31: useAppState(s => s.isBriefOnly) : false; 32: const viewingAgentTaskId = feature('KAIROS') || feature('KAIROS_BRIEF') ? 33: useAppState(s_0 => s_0.viewingAgentTaskId) : null; 34: const briefEnvEnabled = feature('KAIROS') || feature('KAIROS_BRIEF') ? 35: useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_BRIEF), []) : false; 36: const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ? (getKairosActive() || getUserMsgOptIn() && (briefEnvEnabled || getFeatureValue_CACHED_MAY_BE_STALE('tengu_kairos_brief', false))) && isBriefOnly && !isTranscriptMode && !viewingAgentTaskId : false; 37: const displayText = useMemo(() => { 38: if (text.length <= MAX_DISPLAY_CHARS) return text; 39: const head = text.slice(0, TRUNCATE_HEAD_CHARS); 40: const tail = text.slice(-TRUNCATE_TAIL_CHARS); 41: const hiddenLines = countCharInString(text, '\n', TRUNCATE_HEAD_CHARS) - countCharInString(tail, '\n'); 42: return `${head}\n… +${hiddenLines} lines …\n${tail}`; 43: }, [text]); 44: const isSelected = useContext(MessageActionsSelectedContext); 45: if (!text) { 46: logError(new Error('No content found in user prompt message')); 47: return null; 48: } 49: return <Box flexDirection="column" marginTop={addMargin ? 1 : 0} backgroundColor={isSelected ? 'messageActionsBackground' : useBriefLayout ? undefined : 'userMessageBackground'} paddingRight={useBriefLayout ? 0 : 1}> 50: <HighlightedThinkingText text={displayText} useBriefLayout={useBriefLayout} timestamp={useBriefLayout ? timestamp : undefined} /> 51: </Box>; 52: }

File: src/components/messages/UserResourceUpdateMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import * as React from 'react'; 4: import { REFRESH_ARROW } from '../../constants/figures.js'; 5: import { Box, Text } from '../../ink.js'; 6: type Props = { 7: addMargin: boolean; 8: param: TextBlockParam; 9: }; 10: type ParsedUpdate = { 11: kind: 'resource' | 'polling'; 12: server: string; 13: target: string; 14: reason?: string; 15: }; 16: function parseUpdates(text: string): ParsedUpdate[] { 17: const updates: ParsedUpdate[] = []; 18: const resourceRegex = /<mcp-resource-update\s+server="([^"]+)"\s+uri="([^"]+)"[^>]*>(?:[\s\S]*?<reason>([^<]+)<\/reason>)?/g; 19: let match; 20: while ((match = resourceRegex.exec(text)) !== null) { 21: updates.push({ 22: kind: 'resource', 23: server: match[1] ?? '', 24: target: match[2] ?? '', 25: reason: match[3] 26: }); 27: } 28: // Match <mcp-polling-update type="tool" server="..." tool="..."> 29: const pollingRegex = /<mcp-polling-update\s+type="([^"]+)"\s+server="([^"]+)"\s+tool="([^"]+)"[^>]*>(?:[\s\S]*?<reason>([^<]+)<\/reason>)?/g; 30: while ((match = pollingRegex.exec(text)) !== null) { 31: updates.push({ 32: kind: 'polling', 33: server: match[2] ?? '', 34: target: match[3] ?? '', 35: reason: match[4] 36: }); 37: } 38: return updates; 39: } 40: // Format URI for display - show just the meaningful part 41: function formatUri(uri: string): string { 42: // For file:// URIs, show just the filename 43: if (uri.startsWith('file: 44: const path = uri.slice(7); 45: const parts = path.split('/'); 46: return parts[parts.length - 1] || path; 47: } 48: if (uri.length > 40) { 49: return uri.slice(0, 39) + '\u2026'; 50: } 51: return uri; 52: } 53: export function UserResourceUpdateMessage(t0) { 54: const $ = _c(12); 55: const { 56: addMargin, 57: param: t1 58: } = t0; 59: const { 60: text 61: } = t1; 62: let T0; 63: let t2; 64: let t3; 65: let t4; 66: let t5; 67: if ($[0] !== addMargin || $[1] !== text) { 68: t5 = Symbol.for("react.early_return_sentinel"); 69: bb0: { 70: const updates = parseUpdates(text); 71: if (updates.length === 0) { 72: t5 = null; 73: break bb0; 74: } 75: T0 = Box; 76: t2 = "column"; 77: t3 = addMargin ? 1 : 0; 78: t4 = updates.map(_temp); 79: } 80: $[0] = addMargin; 81: $[1] = text; 82: $[2] = T0; 83: $[3] = t2; 84: $[4] = t3; 85: $[5] = t4; 86: $[6] = t5; 87: } else { 88: T0 = $[2]; 89: t2 = $[3]; 90: t3 = $[4]; 91: t4 = $[5]; 92: t5 = $[6]; 93: } 94: if (t5 !== Symbol.for("react.early_return_sentinel")) { 95: return t5; 96: } 97: let t6; 98: if ($[7] !== T0 || $[8] !== t2 || $[9] !== t3 || $[10] !== t4) { 99: t6 = <T0 flexDirection={t2} marginTop={t3}>{t4}</T0>; 100: $[7] = T0; 101: $[8] = t2; 102: $[9] = t3; 103: $[10] = t4; 104: $[11] = t6; 105: } else { 106: t6 = $[11]; 107: } 108: return t6; 109: } 110: function _temp(update, i) { 111: return <Box key={i}><Text><Text color="success">{REFRESH_ARROW}</Text>{" "}<Text dimColor={true}>{update.server}:</Text>{" "}<Text color="suggestion">{update.kind === "resource" ? formatUri(update.target) : update.target}</Text>{update.reason && <Text dimColor={true}> · {update.reason}</Text>}</Text></Box>; 112: }

File: src/components/messages/UserTeammateMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 3: import figures from 'figures'; 4: import * as React from 'react'; 5: import { TEAMMATE_MESSAGE_TAG } from '../../constants/xml.js'; 6: import { Ansi, Box, Text, type TextProps } from '../../ink.js'; 7: import { toInkColor } from '../../utils/ink.js'; 8: import { jsonParse } from '../../utils/slowOperations.js'; 9: import { isShutdownApproved } from '../../utils/teammateMailbox.js'; 10: import { MessageResponse } from '../MessageResponse.js'; 11: import { tryRenderPlanApprovalMessage } from './PlanApprovalMessage.js'; 12: import { tryRenderShutdownMessage } from './ShutdownMessage.js'; 13: import { tryRenderTaskAssignmentMessage } from './TaskAssignmentMessage.js'; 14: type Props = { 15: addMargin: boolean; 16: param: TextBlockParam; 17: isTranscriptMode?: boolean; 18: }; 19: type ParsedMessage = { 20: teammateId: string; 21: content: string; 22: color?: string; 23: summary?: string; 24: }; 25: const TEAMMATE_MSG_REGEX = new RegExp(`<${TEAMMATE_MESSAGE_TAG}\\s+teammate_id="([^"]+)"(?:\\s+color="([^"]+)")?(?:\\s+summary="([^"]+)")?>\\n?([\\s\\S]*?)\\n?<\\/${TEAMMATE_MESSAGE_TAG}>`, 'g'); 26: function parseTeammateMessages(text: string): ParsedMessage[] { 27: const messages: ParsedMessage[] = []; 28: for (const match of text.matchAll(TEAMMATE_MSG_REGEX)) { 29: if (match[1] && match[4]) { 30: messages.push({ 31: teammateId: match[1], 32: color: match[2], 33: summary: match[3], 34: content: match[4].trim() 35: }); 36: } 37: } 38: return messages; 39: } 40: function getDisplayName(teammateId: string): string { 41: if (teammateId === 'leader') { 42: return 'leader'; 43: } 44: return teammateId; 45: } 46: export function UserTeammateMessage({ 47: addMargin, 48: param: { 49: text 50: }, 51: isTranscriptMode 52: }: Props): React.ReactNode { 53: const messages = parseTeammateMessages(text).filter(msg => { 54: if (isShutdownApproved(msg.content)) { 55: return false; 56: } 57: try { 58: const parsed = jsonParse(msg.content); 59: if (parsed?.type === 'teammate_terminated') return false; 60: } catch { 61: } 62: return true; 63: }); 64: if (messages.length === 0) { 65: return null; 66: } 67: return <Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%"> 68: {messages.map((msg_0, index) => { 69: const inkColor = toInkColor(msg_0.color); 70: const displayName = getDisplayName(msg_0.teammateId); 71: const planApprovalElement = tryRenderPlanApprovalMessage(msg_0.content, displayName); 72: if (planApprovalElement) { 73: return <React.Fragment key={index}>{planApprovalElement}</React.Fragment>; 74: } 75: const shutdownElement = tryRenderShutdownMessage(msg_0.content); 76: if (shutdownElement) { 77: return <React.Fragment key={index}>{shutdownElement}</React.Fragment>; 78: } 79: const taskAssignmentElement = tryRenderTaskAssignmentMessage(msg_0.content); 80: if (taskAssignmentElement) { 81: return <React.Fragment key={index}>{taskAssignmentElement}</React.Fragment>; 82: } 83: let parsedIdleNotification: { 84: type?: string; 85: } | null = null; 86: try { 87: parsedIdleNotification = jsonParse(msg_0.content); 88: } catch { 89: } 90: if (parsedIdleNotification?.type === 'idle_notification') { 91: return null; 92: } 93: if (parsedIdleNotification?.type === 'task_completed') { 94: const taskCompleted = parsedIdleNotification as { 95: type: string; 96: from: string; 97: taskId: string; 98: taskSubject?: string; 99: }; 100: return <Box key={index} flexDirection="column" marginTop={1}> 101: <Text color={inkColor}>{`@${displayName}${figures.pointer}`}</Text> 102: <MessageResponse> 103: <Text color="success">✓</Text> 104: <Text> 105: {' '} 106: Completed task #{taskCompleted.taskId} 107: {taskCompleted.taskSubject && <Text dimColor> ({taskCompleted.taskSubject})</Text>} 108: </Text> 109: </MessageResponse> 110: </Box>; 111: } 112: return <TeammateMessageContent key={index} displayName={displayName} inkColor={inkColor} content={msg_0.content} summary={msg_0.summary} isTranscriptMode={isTranscriptMode} />; 113: })} 114: </Box>; 115: } 116: type TeammateMessageContentProps = { 117: displayName: string; 118: inkColor: TextProps['color']; 119: content: string; 120: summary?: string; 121: isTranscriptMode?: boolean; 122: }; 123: export function TeammateMessageContent(t0) { 124: const $ = _c(14); 125: const { 126: displayName, 127: inkColor, 128: content, 129: summary, 130: isTranscriptMode 131: } = t0; 132: const t1 = `@${displayName}${figures.pointer}`; 133: let t2; 134: if ($[0] !== inkColor || $[1] !== t1) { 135: t2 = <Text color={inkColor}>{t1}</Text>; 136: $[0] = inkColor; 137: $[1] = t1; 138: $[2] = t2; 139: } else { 140: t2 = $[2]; 141: } 142: let t3; 143: if ($[3] !== summary) { 144: t3 = summary && <Text> {summary}</Text>; 145: $[3] = summary; 146: $[4] = t3; 147: } else { 148: t3 = $[4]; 149: } 150: let t4; 151: if ($[5] !== t2 || $[6] !== t3) { 152: t4 = <Box>{t2}{t3}</Box>; 153: $[5] = t2; 154: $[6] = t3; 155: $[7] = t4; 156: } else { 157: t4 = $[7]; 158: } 159: let t5; 160: if ($[8] !== content || $[9] !== isTranscriptMode) { 161: t5 = isTranscriptMode && <Box paddingLeft={2}><Text><Ansi>{content}</Ansi></Text></Box>; 162: $[8] = content; 163: $[9] = isTranscriptMode; 164: $[10] = t5; 165: } else { 166: t5 = $[10]; 167: } 168: let t6; 169: if ($[11] !== t4 || $[12] !== t5) { 170: t6 = <Box flexDirection="column" marginTop={1}>{t4}{t5}</Box>; 171: $[11] = t4; 172: $[12] = t5; 173: $[13] = t6; 174: } else { 175: t6 = $[13]; 176: } 177: return t6; 178: }

File: src/components/messages/UserTextMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; 4: import * as React from 'react'; 5: import { NO_CONTENT_MESSAGE } from '../../constants/messages.js'; 6: import { COMMAND_MESSAGE_TAG, LOCAL_COMMAND_CAVEAT_TAG, TASK_NOTIFICATION_TAG, TEAMMATE_MESSAGE_TAG, TICK_TAG } from '../../constants/xml.js'; 7: import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'; 8: import { extractTag, INTERRUPT_MESSAGE, INTERRUPT_MESSAGE_FOR_TOOL_USE } from '../../utils/messages.js'; 9: import { InterruptedByUser } from '../InterruptedByUser.js'; 10: import { MessageResponse } from '../MessageResponse.js'; 11: import { UserAgentNotificationMessage } from './UserAgentNotificationMessage.js'; 12: import { UserBashInputMessage } from './UserBashInputMessage.js'; 13: import { UserBashOutputMessage } from './UserBashOutputMessage.js'; 14: import { UserCommandMessage } from './UserCommandMessage.js'; 15: import { UserLocalCommandOutputMessage } from './UserLocalCommandOutputMessage.js'; 16: import { UserMemoryInputMessage } from './UserMemoryInputMessage.js'; 17: import { UserPlanMessage } from './UserPlanMessage.js'; 18: import { UserPromptMessage } from './UserPromptMessage.js'; 19: import { UserResourceUpdateMessage } from './UserResourceUpdateMessage.js'; 20: import { UserTeammateMessage } from './UserTeammateMessage.js'; 21: type Props = { 22: addMargin: boolean; 23: param: TextBlockParam; 24: verbose: boolean; 25: planContent?: string; 26: isTranscriptMode?: boolean; 27: timestamp?: string; 28: }; 29: export function UserTextMessage(t0) { 30: const $ = _c(49); 31: const { 32: addMargin, 33: param, 34: verbose, 35: planContent, 36: isTranscriptMode, 37: timestamp 38: } = t0; 39: if (param.text.trim() === NO_CONTENT_MESSAGE) { 40: return null; 41: } 42: if (planContent) { 43: let t1; 44: if ($[0] !== addMargin || $[1] !== planContent) { 45: t1 = <UserPlanMessage addMargin={addMargin} planContent={planContent} />; 46: $[0] = addMargin; 47: $[1] = planContent; 48: $[2] = t1; 49: } else { 50: t1 = $[2]; 51: } 52: return t1; 53: } 54: if (extractTag(param.text, TICK_TAG)) { 55: return null; 56: } 57: if (param.text.includes(`<${LOCAL_COMMAND_CAVEAT_TAG}>`)) { 58: return null; 59: } 60: if (param.text.startsWith("<bash-stdout") || param.text.startsWith("<bash-stderr")) { 61: let t1; 62: if ($[3] !== param.text || $[4] !== verbose) { 63: t1 = <UserBashOutputMessage content={param.text} verbose={verbose} />; 64: $[3] = param.text; 65: $[4] = verbose; 66: $[5] = t1; 67: } else { 68: t1 = $[5]; 69: } 70: return t1; 71: } 72: if (param.text.startsWith("<local-command-stdout") || param.text.startsWith("<local-command-stderr")) { 73: let t1; 74: if ($[6] !== param.text) { 75: t1 = <UserLocalCommandOutputMessage content={param.text} />; 76: $[6] = param.text; 77: $[7] = t1; 78: } else { 79: t1 = $[7]; 80: } 81: return t1; 82: } 83: if (param.text === INTERRUPT_MESSAGE || param.text === INTERRUPT_MESSAGE_FOR_TOOL_USE) { 84: let t1; 85: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 86: t1 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>; 87: $[8] = t1; 88: } else { 89: t1 = $[8]; 90: } 91: return t1; 92: } 93: if (feature("KAIROS_GITHUB_WEBHOOKS")) { 94: if (param.text.startsWith("<github-webhook-activity>")) { 95: let t1; 96: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 97: t1 = require("./UserGitHubWebhookMessage.js"); 98: $[9] = t1; 99: } else { 100: t1 = $[9]; 101: } 102: const { 103: UserGitHubWebhookMessage 104: } = t1 as typeof import('./UserGitHubWebhookMessage.js'); 105: let t2; 106: if ($[10] !== addMargin || $[11] !== param) { 107: t2 = <UserGitHubWebhookMessage addMargin={addMargin} param={param} />; 108: $[10] = addMargin; 109: $[11] = param; 110: $[12] = t2; 111: } else { 112: t2 = $[12]; 113: } 114: return t2; 115: } 116: } 117: if (param.text.includes("<bash-input>")) { 118: let t1; 119: if ($[13] !== addMargin || $[14] !== param) { 120: t1 = <UserBashInputMessage addMargin={addMargin} param={param} />; 121: $[13] = addMargin; 122: $[14] = param; 123: $[15] = t1; 124: } else { 125: t1 = $[15]; 126: } 127: return t1; 128: } 129: if (param.text.includes(`<${COMMAND_MESSAGE_TAG}>`)) { 130: let t1; 131: if ($[16] !== addMargin || $[17] !== param) { 132: t1 = <UserCommandMessage addMargin={addMargin} param={param} />; 133: $[16] = addMargin; 134: $[17] = param; 135: $[18] = t1; 136: } else { 137: t1 = $[18]; 138: } 139: return t1; 140: } 141: if (param.text.includes("<user-memory-input>")) { 142: let t1; 143: if ($[19] !== addMargin || $[20] !== param.text) { 144: t1 = <UserMemoryInputMessage addMargin={addMargin} text={param.text} />; 145: $[19] = addMargin; 146: $[20] = param.text; 147: $[21] = t1; 148: } else { 149: t1 = $[21]; 150: } 151: return t1; 152: } 153: if (isAgentSwarmsEnabled() && param.text.includes(`<${TEAMMATE_MESSAGE_TAG}`)) { 154: let t1; 155: if ($[22] !== addMargin || $[23] !== isTranscriptMode || $[24] !== param) { 156: t1 = <UserTeammateMessage addMargin={addMargin} param={param} isTranscriptMode={isTranscriptMode} />; 157: $[22] = addMargin; 158: $[23] = isTranscriptMode; 159: $[24] = param; 160: $[25] = t1; 161: } else { 162: t1 = $[25]; 163: } 164: return t1; 165: } 166: if (param.text.includes(`<${TASK_NOTIFICATION_TAG}`)) { 167: let t1; 168: if ($[26] !== addMargin || $[27] !== param) { 169: t1 = <UserAgentNotificationMessage addMargin={addMargin} param={param} />; 170: $[26] = addMargin; 171: $[27] = param; 172: $[28] = t1; 173: } else { 174: t1 = $[28]; 175: } 176: return t1; 177: } 178: if (param.text.includes("<mcp-resource-update") || param.text.includes("<mcp-polling-update")) { 179: let t1; 180: if ($[29] !== addMargin || $[30] !== param) { 181: t1 = <UserResourceUpdateMessage addMargin={addMargin} param={param} />; 182: $[29] = addMargin; 183: $[30] = param; 184: $[31] = t1; 185: } else { 186: t1 = $[31]; 187: } 188: return t1; 189: } 190: if (feature("FORK_SUBAGENT")) { 191: if (param.text.includes("<fork-boilerplate>")) { 192: let t1; 193: if ($[32] === Symbol.for("react.memo_cache_sentinel")) { 194: t1 = require("./UserForkBoilerplateMessage.js"); 195: $[32] = t1; 196: } else { 197: t1 = $[32]; 198: } 199: const { 200: UserForkBoilerplateMessage 201: } = t1 as typeof import('./UserForkBoilerplateMessage.js'); 202: let t2; 203: if ($[33] !== addMargin || $[34] !== param) { 204: t2 = <UserForkBoilerplateMessage addMargin={addMargin} param={param} />; 205: $[33] = addMargin; 206: $[34] = param; 207: $[35] = t2; 208: } else { 209: t2 = $[35]; 210: } 211: return t2; 212: } 213: } 214: if (feature("UDS_INBOX")) { 215: if (param.text.includes("<cross-session-message")) { 216: let t1; 217: if ($[36] === Symbol.for("react.memo_cache_sentinel")) { 218: t1 = require("./UserCrossSessionMessage.js"); 219: $[36] = t1; 220: } else { 221: t1 = $[36]; 222: } 223: const { 224: UserCrossSessionMessage 225: } = t1 as typeof import('./UserCrossSessionMessage.js'); 226: let t2; 227: if ($[37] !== addMargin || $[38] !== param) { 228: t2 = <UserCrossSessionMessage addMargin={addMargin} param={param} />; 229: $[37] = addMargin; 230: $[38] = param; 231: $[39] = t2; 232: } else { 233: t2 = $[39]; 234: } 235: return t2; 236: } 237: } 238: if (feature("KAIROS") || feature("KAIROS_CHANNELS")) { 239: if (param.text.includes("<channel source=\"")) { 240: let t1; 241: if ($[40] === Symbol.for("react.memo_cache_sentinel")) { 242: t1 = require("./UserChannelMessage.js"); 243: $[40] = t1; 244: } else { 245: t1 = $[40]; 246: } 247: const { 248: UserChannelMessage 249: } = t1 as typeof import('./UserChannelMessage.js'); 250: let t2; 251: if ($[41] !== addMargin || $[42] !== param) { 252: t2 = <UserChannelMessage addMargin={addMargin} param={param} />; 253: $[41] = addMargin; 254: $[42] = param; 255: $[43] = t2; 256: } else { 257: t2 = $[43]; 258: } 259: return t2; 260: } 261: } 262: let t1; 263: if ($[44] !== addMargin || $[45] !== isTranscriptMode || $[46] !== param || $[47] !== timestamp) { 264: t1 = <UserPromptMessage addMargin={addMargin} param={param} isTranscriptMode={isTranscriptMode} timestamp={timestamp} />; 265: $[44] = addMargin; 266: $[45] = isTranscriptMode; 267: $[46] = param; 268: $[47] = timestamp; 269: $[48] = t1; 270: } else { 271: t1 = $[48]; 272: } 273: return t1; 274: }

File: src/components/Passes/Passes.tsx

typescript 1: import * as React from 'react'; 2: import { useCallback, useEffect, useState } from 'react'; 3: import type { CommandResultDisplay } from '../../commands.js'; 4: import { TEARDROP_ASTERISK } from '../../constants/figures.js'; 5: import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 6: import { setClipboard } from '../../ink/termio/osc.js'; 7: import { Box, Link, Text, useInput } from '../../ink.js'; 8: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 9: import { logEvent } from '../../services/analytics/index.js'; 10: import { fetchReferralRedemptions, formatCreditAmount, getCachedOrFetchPassesEligibility } from '../../services/api/referral.js'; 11: import type { ReferralRedemptionsResponse, ReferrerRewardInfo } from '../../services/oauth/types.js'; 12: import { count } from '../../utils/array.js'; 13: import { logError } from '../../utils/log.js'; 14: import { Pane } from '../design-system/Pane.js'; 15: type PassStatus = { 16: passNumber: number; 17: isAvailable: boolean; 18: }; 19: type Props = { 20: onDone: (result?: string, options?: { 21: display?: CommandResultDisplay; 22: }) => void; 23: }; 24: export function Passes({ 25: onDone 26: }: Props): React.ReactNode { 27: const [loading, setLoading] = useState(true); 28: const [passStatuses, setPassStatuses] = useState<PassStatus[]>([]); 29: const [isAvailable, setIsAvailable] = useState(false); 30: const [referralLink, setReferralLink] = useState<string | null>(null); 31: const [referrerReward, setReferrerReward] = useState<ReferrerRewardInfo | null | undefined>(undefined); 32: const exitState = useExitOnCtrlCDWithKeybindings(() => onDone('Guest passes dialog dismissed', { 33: display: 'system' 34: })); 35: const handleCancel = useCallback(() => { 36: onDone('Guest passes dialog dismissed', { 37: display: 'system' 38: }); 39: }, [onDone]); 40: useKeybinding('confirm:no', handleCancel, { 41: context: 'Confirmation' 42: }); 43: useInput((_input, key) => { 44: if (key.return && referralLink) { 45: void setClipboard(referralLink).then(raw => { 46: if (raw) process.stdout.write(raw); 47: logEvent('tengu_guest_passes_link_copied', {}); 48: onDone(`Referral link copied to clipboard!`); 49: }); 50: } 51: }); 52: useEffect(() => { 53: async function loadPassesData() { 54: try { 55: const eligibilityData = await getCachedOrFetchPassesEligibility(); 56: if (!eligibilityData || !eligibilityData.eligible) { 57: setIsAvailable(false); 58: setLoading(false); 59: return; 60: } 61: setIsAvailable(true); 62: if (eligibilityData.referral_code_details?.referral_link) { 63: setReferralLink(eligibilityData.referral_code_details.referral_link); 64: } 65: setReferrerReward(eligibilityData.referrer_reward); 66: const campaign = eligibilityData.referral_code_details?.campaign ?? 'claude_code_guest_pass'; 67: let redemptionsData: ReferralRedemptionsResponse; 68: try { 69: redemptionsData = await fetchReferralRedemptions(campaign); 70: } catch (err_0) { 71: logError(err_0 as Error); 72: setIsAvailable(false); 73: setLoading(false); 74: return; 75: } 76: const redemptions = redemptionsData.redemptions || []; 77: const maxRedemptions = redemptionsData.limit || 3; 78: const statuses: PassStatus[] = []; 79: for (let i = 0; i < maxRedemptions; i++) { 80: const redemption = redemptions[i]; 81: statuses.push({ 82: passNumber: i + 1, 83: isAvailable: !redemption 84: }); 85: } 86: setPassStatuses(statuses); 87: setLoading(false); 88: } catch (err) { 89: logError(err as Error); 90: setIsAvailable(false); 91: setLoading(false); 92: } 93: } 94: void loadPassesData(); 95: }, []); 96: if (loading) { 97: return <Pane> 98: <Box flexDirection="column" gap={1}> 99: <Text dimColor>Loading guest pass information…</Text> 100: <Text dimColor italic> 101: {exitState.pending ? <>Press {exitState.keyName} again to exit</> : <>Esc to cancel</>} 102: </Text> 103: </Box> 104: </Pane>; 105: } 106: if (!isAvailable) { 107: return <Pane> 108: <Box flexDirection="column" gap={1}> 109: <Text>Guest passes are not currently available.</Text> 110: <Text dimColor italic> 111: {exitState.pending ? <>Press {exitState.keyName} again to exit</> : <>Esc to cancel</>} 112: </Text> 113: </Box> 114: </Pane>; 115: } 116: const availableCount = count(passStatuses, p => p.isAvailable); 117: const sortedPasses = [...passStatuses].sort((a, b) => +b.isAvailable - +a.isAvailable); 118: const renderTicket = (pass: PassStatus) => { 119: const isRedeemed = !pass.isAvailable; 120: if (isRedeemed) { 121: return <Box key={pass.passNumber} flexDirection="column" marginRight={1}> 122: <Text dimColor>{'┌─────────╱'}</Text> 123: <Text dimColor>{` ) CC ${TEARDROP_ASTERISK} ┊╱`}</Text> 124: <Text dimColor>{'└───────╱'}</Text> 125: </Box>; 126: } 127: return <Box key={pass.passNumber} flexDirection="column" marginRight={1}> 128: <Text>{'┌──────────┐'}</Text> 129: <Text> 130: {' ) CC '} 131: <Text color="claude">{TEARDROP_ASTERISK}</Text> 132: {' ┊ ( '} 133: </Text> 134: <Text>{'└──────────┘'}</Text> 135: </Box>; 136: }; 137: return <Pane> 138: <Box flexDirection="column" gap={1}> 139: <Text color="permission">Guest passes · {availableCount} left</Text> 140: <Box flexDirection="row" marginLeft={2}> 141: {sortedPasses.slice(0, 3).map(pass_0 => renderTicket(pass_0))} 142: </Box> 143: {referralLink && <Box marginLeft={2}> 144: <Text>{referralLink}</Text> 145: </Box>} 146: <Box flexDirection="column" marginLeft={2}> 147: <Text dimColor> 148: {referrerReward ? `Share a free week of Claude Code with friends. If they love it and subscribe, you'll get ${formatCreditAmount(referrerReward)} of extra usage to keep building. ` : 'Share a free week of Claude Code with friends. '} 149: <Link url={referrerReward ? 'https://support.claude.com/en/articles/13456702-claude-code-guest-passes' : 'https://support.claude.com/en/articles/12875061-claude-code-guest-passes'}> 150: Terms apply. 151: </Link> 152: </Text> 153: </Box> 154: <Box> 155: <Text dimColor italic> 156: {exitState.pending ? <>Press {exitState.keyName} again to exit</> : <>Enter to copy link · Esc to cancel</>} 157: </Text> 158: </Box> 159: </Box> 160: </Pane>; 161: }

File: src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { Base64ImageSource, ImageBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'; 3: import React, { Suspense, use, useCallback, useMemo, useRef, useState } from 'react'; 4: import { useSettings } from '../../../hooks/useSettings.js'; 5: import { useTerminalSize } from '../../../hooks/useTerminalSize.js'; 6: import { stringWidth } from '../../../ink/stringWidth.js'; 7: import { useTheme } from '../../../ink.js'; 8: import { useKeybindings } from '../../../keybindings/useKeybinding.js'; 9: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js'; 10: import { useAppState } from '../../../state/AppState.js'; 11: import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'; 12: import { AskUserQuestionTool } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'; 13: import { type CliHighlight, getCliHighlightPromise } from '../../../utils/cliHighlight.js'; 14: import type { PastedContent } from '../../../utils/config.js'; 15: import type { ImageDimensions } from '../../../utils/imageResizer.js'; 16: import { maybeResizeAndDownsampleImageBlock } from '../../../utils/imageResizer.js'; 17: import { cacheImagePath, storeImage } from '../../../utils/imageStore.js'; 18: import { logError } from '../../../utils/log.js'; 19: import { applyMarkdown } from '../../../utils/markdown.js'; 20: import { isPlanModeInterviewPhaseEnabled } from '../../../utils/planModeV2.js'; 21: import { getPlanFilePath } from '../../../utils/plans.js'; 22: import type { PermissionRequestProps } from '../PermissionRequest.js'; 23: import { QuestionView } from './QuestionView.js'; 24: import { SubmitQuestionsView } from './SubmitQuestionsView.js'; 25: import { useMultipleChoiceState } from './use-multiple-choice-state.js'; 26: const MIN_CONTENT_HEIGHT = 12; 27: const MIN_CONTENT_WIDTH = 40; 28: const CONTENT_CHROME_OVERHEAD = 15; 29: export function AskUserQuestionPermissionRequest(props) { 30: const $ = _c(4); 31: const settings = useSettings(); 32: if (settings.syntaxHighlightingDisabled) { 33: let t0; 34: if ($[0] !== props) { 35: t0 = <AskUserQuestionPermissionRequestBody {...props} highlight={null} />; 36: $[0] = props; 37: $[1] = t0; 38: } else { 39: t0 = $[1]; 40: } 41: return t0; 42: } 43: let t0; 44: if ($[2] !== props) { 45: t0 = <Suspense fallback={<AskUserQuestionPermissionRequestBody {...props} highlight={null} />}><AskUserQuestionWithHighlight {...props} /></Suspense>; 46: $[2] = props; 47: $[3] = t0; 48: } else { 49: t0 = $[3]; 50: } 51: return t0; 52: } 53: function AskUserQuestionWithHighlight(props) { 54: const $ = _c(4); 55: let t0; 56: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 57: t0 = getCliHighlightPromise(); 58: $[0] = t0; 59: } else { 60: t0 = $[0]; 61: } 62: const highlight = use(t0); 63: let t1; 64: if ($[1] !== highlight || $[2] !== props) { 65: t1 = <AskUserQuestionPermissionRequestBody {...props} highlight={highlight} />; 66: $[1] = highlight; 67: $[2] = props; 68: $[3] = t1; 69: } else { 70: t1 = $[3]; 71: } 72: return t1; 73: } 74: function AskUserQuestionPermissionRequestBody(t0) { 75: const $ = _c(115); 76: const { 77: toolUseConfirm, 78: onDone, 79: onReject, 80: highlight 81: } = t0; 82: let t1; 83: if ($[0] !== toolUseConfirm.input) { 84: t1 = AskUserQuestionTool.inputSchema.safeParse(toolUseConfirm.input); 85: $[0] = toolUseConfirm.input; 86: $[1] = t1; 87: } else { 88: t1 = $[1]; 89: } 90: const result = t1; 91: let t2; 92: if ($[2] !== result.data || $[3] !== result.success) { 93: t2 = result.success ? result.data.questions || [] : []; 94: $[2] = result.data; 95: $[3] = result.success; 96: $[4] = t2; 97: } else { 98: t2 = $[4]; 99: } 100: const questions = t2; 101: const { 102: rows: terminalRows 103: } = useTerminalSize(); 104: const [theme] = useTheme(); 105: let maxHeight = 0; 106: let maxWidth = 0; 107: const maxAllowedHeight = Math.max(MIN_CONTENT_HEIGHT, terminalRows - CONTENT_CHROME_OVERHEAD); 108: if ($[5] !== highlight || $[6] !== maxAllowedHeight || $[7] !== maxHeight || $[8] !== maxWidth || $[9] !== questions || $[10] !== theme) { 109: for (const q of questions) { 110: const hasPreview = q.options.some(_temp); 111: if (hasPreview) { 112: const maxPreviewContentLines = Math.max(1, maxAllowedHeight - 11); 113: let maxPreviewBoxHeight = 0; 114: for (const opt_0 of q.options) { 115: if (opt_0.preview) { 116: const rendered = applyMarkdown(opt_0.preview, theme, highlight); 117: const previewLines = rendered.split("\n"); 118: const isTruncated = previewLines.length > maxPreviewContentLines; 119: const displayedLines = isTruncated ? maxPreviewContentLines : previewLines.length; 120: maxPreviewBoxHeight = Math.max(maxPreviewBoxHeight, displayedLines + (isTruncated ? 1 : 0) + 2); 121: for (const line of previewLines) { 122: maxWidth = Math.max(maxWidth, stringWidth(line)); 123: } 124: } 125: } 126: const rightPanelHeight = maxPreviewBoxHeight + 2; 127: const leftPanelHeight = q.options.length + 2; 128: const sideByHeight = Math.max(leftPanelHeight, rightPanelHeight); 129: maxHeight = Math.max(maxHeight, sideByHeight + 7); 130: } else { 131: maxHeight = Math.max(maxHeight, q.options.length + 3 + 7); 132: } 133: } 134: $[5] = highlight; 135: $[6] = maxAllowedHeight; 136: $[7] = maxHeight; 137: $[8] = maxWidth; 138: $[9] = questions; 139: $[10] = theme; 140: $[11] = maxHeight; 141: } else { 142: maxHeight = $[11]; 143: } 144: const t3 = Math.min(Math.max(maxHeight, MIN_CONTENT_HEIGHT), maxAllowedHeight); 145: const t4 = Math.max(maxWidth, MIN_CONTENT_WIDTH); 146: let t5; 147: if ($[12] !== t3 || $[13] !== t4) { 148: t5 = { 149: globalContentHeight: t3, 150: globalContentWidth: t4 151: }; 152: $[12] = t3; 153: $[13] = t4; 154: $[14] = t5; 155: } else { 156: t5 = $[14]; 157: } 158: const { 159: globalContentHeight, 160: globalContentWidth 161: } = t5; 162: const metadataSource = result.success ? result.data.metadata?.source : undefined; 163: let t6; 164: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 165: t6 = {}; 166: $[15] = t6; 167: } else { 168: t6 = $[15]; 169: } 170: const [pastedContentsByQuestion, setPastedContentsByQuestion] = useState(t6); 171: const nextPasteIdRef = useRef(0); 172: let t7; 173: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 174: t7 = function onImagePaste(questionText, base64Image, mediaType, filename, dimensions, _sourcePath) { 175: nextPasteIdRef.current = nextPasteIdRef.current + 1; 176: const pasteId = nextPasteIdRef.current; 177: const newContent = { 178: id: pasteId, 179: type: "image", 180: content: base64Image, 181: mediaType: mediaType || "image/png", 182: filename: filename || "Pasted image", 183: dimensions 184: }; 185: cacheImagePath(newContent); 186: storeImage(newContent); 187: setPastedContentsByQuestion(prev => ({ 188: ...prev, 189: [questionText]: { 190: ...(prev[questionText] ?? {}), 191: [pasteId]: newContent 192: } 193: })); 194: }; 195: $[16] = t7; 196: } else { 197: t7 = $[16]; 198: } 199: const onImagePaste = t7; 200: let t8; 201: if ($[17] === Symbol.for("react.memo_cache_sentinel")) { 202: t8 = (questionText_0, id) => { 203: setPastedContentsByQuestion(prev_0 => { 204: const questionContents = { 205: ...(prev_0[questionText_0] ?? {}) 206: }; 207: delete questionContents[id]; 208: return { 209: ...prev_0, 210: [questionText_0]: questionContents 211: }; 212: }); 213: }; 214: $[17] = t8; 215: } else { 216: t8 = $[17]; 217: } 218: const onRemoveImage = t8; 219: let t9; 220: if ($[18] !== pastedContentsByQuestion) { 221: t9 = Object.values(pastedContentsByQuestion).flatMap(_temp2).filter(_temp3); 222: $[18] = pastedContentsByQuestion; 223: $[19] = t9; 224: } else { 225: t9 = $[19]; 226: } 227: const allImageAttachments = t9; 228: const toolPermissionContextMode = useAppState(_temp4); 229: const isInPlanMode = toolPermissionContextMode === "plan"; 230: let t10; 231: if ($[20] !== isInPlanMode) { 232: t10 = isInPlanMode ? getPlanFilePath() : undefined; 233: $[20] = isInPlanMode; 234: $[21] = t10; 235: } else { 236: t10 = $[21]; 237: } 238: const planFilePath = t10; 239: const state = useMultipleChoiceState(); 240: const { 241: currentQuestionIndex, 242: answers, 243: questionStates, 244: isInTextInput, 245: nextQuestion, 246: prevQuestion, 247: updateQuestionState, 248: setAnswer, 249: setTextInputMode 250: } = state; 251: const currentQuestion = currentQuestionIndex < (questions?.length || 0) ? questions?.[currentQuestionIndex] : null; 252: const isInSubmitView = currentQuestionIndex === (questions?.length || 0); 253: let t11; 254: if ($[22] !== answers || $[23] !== questions) { 255: t11 = questions?.every(q_0 => q_0?.question && !!answers[q_0.question]) ?? false; 256: $[22] = answers; 257: $[23] = questions; 258: $[24] = t11; 259: } else { 260: t11 = $[24]; 261: } 262: const allQuestionsAnswered = t11; 263: const hideSubmitTab = questions.length === 1 && !questions[0]?.multiSelect; 264: let t12; 265: if ($[25] !== isInPlanMode || $[26] !== metadataSource || $[27] !== onDone || $[28] !== onReject || $[29] !== questions.length || $[30] !== toolUseConfirm) { 266: t12 = () => { 267: if (metadataSource) { 268: logEvent("tengu_ask_user_question_rejected", { 269: source: metadataSource as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 270: questionCount: questions.length, 271: isInPlanMode, 272: interviewPhaseEnabled: isInPlanMode && isPlanModeInterviewPhaseEnabled() 273: }); 274: } 275: onDone(); 276: onReject(); 277: toolUseConfirm.onReject(); 278: }; 279: $[25] = isInPlanMode; 280: $[26] = metadataSource; 281: $[27] = onDone; 282: $[28] = onReject; 283: $[29] = questions.length; 284: $[30] = toolUseConfirm; 285: $[31] = t12; 286: } else { 287: t12 = $[31]; 288: } 289: const handleCancel = t12; 290: let t13; 291: if ($[32] !== allImageAttachments || $[33] !== answers || $[34] !== isInPlanMode || $[35] !== metadataSource || $[36] !== onDone || $[37] !== questions || $[38] !== toolUseConfirm) { 292: t13 = async () => { 293: const questionsWithAnswers = questions.map(q_1 => { 294: const answer = answers[q_1.question]; 295: if (answer) { 296: return `- "${q_1.question}"\n Answer: ${answer}`; 297: } 298: return `- "${q_1.question}"\n (No answer provided)`; 299: }).join("\n"); 300: const feedback = `The user wants to clarify these questions. 301: This means they may have additional information, context or questions for you. 302: Take their response into account and then reformulate the questions if appropriate. 303: Start by asking them what they would like to clarify. 304: Questions asked:\n${questionsWithAnswers}`; 305: if (metadataSource) { 306: logEvent("tengu_ask_user_question_respond_to_claude", { 307: source: metadataSource as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 308: questionCount: questions.length, 309: isInPlanMode, 310: interviewPhaseEnabled: isInPlanMode && isPlanModeInterviewPhaseEnabled() 311: }); 312: } 313: const imageBlocks = await convertImagesToBlocks(allImageAttachments); 314: onDone(); 315: toolUseConfirm.onReject(feedback, imageBlocks && imageBlocks.length > 0 ? imageBlocks : undefined); 316: }; 317: $[32] = allImageAttachments; 318: $[33] = answers; 319: $[34] = isInPlanMode; 320: $[35] = metadataSource; 321: $[36] = onDone; 322: $[37] = questions; 323: $[38] = toolUseConfirm; 324: $[39] = t13; 325: } else { 326: t13 = $[39]; 327: } 328: const handleRespondToClaude = t13; 329: let t14; 330: if ($[40] !== allImageAttachments || $[41] !== answers || $[42] !== isInPlanMode || $[43] !== metadataSource || $[44] !== onDone || $[45] !== questions || $[46] !== toolUseConfirm) { 331: t14 = async () => { 332: const questionsWithAnswers_0 = questions.map(q_2 => { 333: const answer_0 = answers[q_2.question]; 334: if (answer_0) { 335: return `- "${q_2.question}"\n Answer: ${answer_0}`; 336: } 337: return `- "${q_2.question}"\n (No answer provided)`; 338: }).join("\n"); 339: const feedback_0 = `The user has indicated they have provided enough answers for the plan interview. 340: Stop asking clarifying questions and proceed to finish the plan with the information you have. 341: Questions asked and answers provided:\n${questionsWithAnswers_0}`; 342: if (metadataSource) { 343: logEvent("tengu_ask_user_question_finish_plan_interview", { 344: source: metadataSource as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 345: questionCount: questions.length, 346: isInPlanMode, 347: interviewPhaseEnabled: isInPlanMode && isPlanModeInterviewPhaseEnabled() 348: }); 349: } 350: const imageBlocks_0 = await convertImagesToBlocks(allImageAttachments); 351: onDone(); 352: toolUseConfirm.onReject(feedback_0, imageBlocks_0 && imageBlocks_0.length > 0 ? imageBlocks_0 : undefined); 353: }; 354: $[40] = allImageAttachments; 355: $[41] = answers; 356: $[42] = isInPlanMode; 357: $[43] = metadataSource; 358: $[44] = onDone; 359: $[45] = questions; 360: $[46] = toolUseConfirm; 361: $[47] = t14; 362: } else { 363: t14 = $[47]; 364: } 365: const handleFinishPlanInterview = t14; 366: let t15; 367: if ($[48] !== allImageAttachments || $[49] !== isInPlanMode || $[50] !== metadataSource || $[51] !== onDone || $[52] !== questionStates || $[53] !== questions || $[54] !== toolUseConfirm) { 368: t15 = async answersToSubmit => { 369: if (metadataSource) { 370: logEvent("tengu_ask_user_question_accepted", { 371: source: metadataSource as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 372: questionCount: questions.length, 373: answerCount: Object.keys(answersToSubmit).length, 374: isInPlanMode, 375: interviewPhaseEnabled: isInPlanMode && isPlanModeInterviewPhaseEnabled() 376: }); 377: } 378: const annotations = {}; 379: for (const q_3 of questions) { 380: const answer_1 = answersToSubmit[q_3.question]; 381: const notes = questionStates[q_3.question]?.textInputValue; 382: const selectedOption = answer_1 ? q_3.options.find(opt_1 => opt_1.label === answer_1) : undefined; 383: const preview = selectedOption?.preview; 384: if (preview || notes?.trim()) { 385: annotations[q_3.question] = { 386: ...(preview && { 387: preview 388: }), 389: ...(notes?.trim() && { 390: notes: notes.trim() 391: }) 392: }; 393: } 394: } 395: const updatedInput = { 396: ...toolUseConfirm.input, 397: answers: answersToSubmit, 398: ...(Object.keys(annotations).length > 0 && { 399: annotations 400: }) 401: }; 402: const contentBlocks = await convertImagesToBlocks(allImageAttachments); 403: onDone(); 404: toolUseConfirm.onAllow(updatedInput, [], undefined, contentBlocks && contentBlocks.length > 0 ? contentBlocks : undefined); 405: }; 406: $[48] = allImageAttachments; 407: $[49] = isInPlanMode; 408: $[50] = metadataSource; 409: $[51] = onDone; 410: $[52] = questionStates; 411: $[53] = questions; 412: $[54] = toolUseConfirm; 413: $[55] = t15; 414: } else { 415: t15 = $[55]; 416: } 417: const submitAnswers = t15; 418: let t16; 419: if ($[56] !== answers || $[57] !== pastedContentsByQuestion || $[58] !== questions.length || $[59] !== setAnswer || $[60] !== submitAnswers) { 420: t16 = (questionText_1, label, textInput, t17) => { 421: const shouldAdvance = t17 === undefined ? true : t17; 422: let answer_2; 423: const isMultiSelect = Array.isArray(label); 424: if (isMultiSelect) { 425: answer_2 = label.join(", "); 426: } else { 427: if (textInput) { 428: const questionImages = Object.values(pastedContentsByQuestion[questionText_1] ?? {}).filter(_temp5); 429: answer_2 = questionImages.length > 0 ? `${textInput} (Image attached)` : textInput; 430: } else { 431: if (label === "__other__") { 432: const questionImages_0 = Object.values(pastedContentsByQuestion[questionText_1] ?? {}).filter(_temp6); 433: answer_2 = questionImages_0.length > 0 ? "(Image attached)" : label; 434: } else { 435: answer_2 = label; 436: } 437: } 438: } 439: const isSingleQuestion = questions.length === 1; 440: if (!isMultiSelect && isSingleQuestion && shouldAdvance) { 441: const updatedAnswers = { 442: ...answers, 443: [questionText_1]: answer_2 444: }; 445: submitAnswers(updatedAnswers).catch(logError); 446: return; 447: } 448: setAnswer(questionText_1, answer_2, shouldAdvance); 449: }; 450: $[56] = answers; 451: $[57] = pastedContentsByQuestion; 452: $[58] = questions.length; 453: $[59] = setAnswer; 454: $[60] = submitAnswers; 455: $[61] = t16; 456: } else { 457: t16 = $[61]; 458: } 459: const handleQuestionAnswer = t16; 460: let t17; 461: if ($[62] !== answers || $[63] !== handleCancel || $[64] !== submitAnswers) { 462: t17 = function handleFinalResponse(value) { 463: if (value === "cancel") { 464: handleCancel(); 465: return; 466: } 467: if (value === "submit") { 468: submitAnswers(answers).catch(logError); 469: } 470: }; 471: $[62] = answers; 472: $[63] = handleCancel; 473: $[64] = submitAnswers; 474: $[65] = t17; 475: } else { 476: t17 = $[65]; 477: } 478: const handleFinalResponse = t17; 479: const maxIndex = hideSubmitTab ? (questions?.length || 1) - 1 : questions?.length || 0; 480: let t18; 481: if ($[66] !== currentQuestionIndex || $[67] !== prevQuestion) { 482: t18 = () => { 483: if (currentQuestionIndex > 0) { 484: prevQuestion(); 485: } 486: }; 487: $[66] = currentQuestionIndex; 488: $[67] = prevQuestion; 489: $[68] = t18; 490: } else { 491: t18 = $[68]; 492: } 493: const handleTabPrev = t18; 494: let t19; 495: if ($[69] !== currentQuestionIndex || $[70] !== maxIndex || $[71] !== nextQuestion) { 496: t19 = () => { 497: if (currentQuestionIndex < maxIndex) { 498: nextQuestion(); 499: } 500: }; 501: $[69] = currentQuestionIndex; 502: $[70] = maxIndex; 503: $[71] = nextQuestion; 504: $[72] = t19; 505: } else { 506: t19 = $[72]; 507: } 508: const handleTabNext = t19; 509: let t20; 510: if ($[73] !== handleTabNext || $[74] !== handleTabPrev) { 511: t20 = { 512: "tabs:previous": handleTabPrev, 513: "tabs:next": handleTabNext 514: }; 515: $[73] = handleTabNext; 516: $[74] = handleTabPrev; 517: $[75] = t20; 518: } else { 519: t20 = $[75]; 520: } 521: const t21 = !(isInTextInput && !isInSubmitView); 522: let t22; 523: if ($[76] !== t21) { 524: t22 = { 525: context: "Tabs", 526: isActive: t21 527: }; 528: $[76] = t21; 529: $[77] = t22; 530: } else { 531: t22 = $[77]; 532: } 533: useKeybindings(t20, t22); 534: if (currentQuestion) { 535: let t23; 536: if ($[78] !== currentQuestion.question) { 537: t23 = (base64, mediaType_0, filename_0, dims, path) => onImagePaste(currentQuestion.question, base64, mediaType_0, filename_0, dims, path); 538: $[78] = currentQuestion.question; 539: $[79] = t23; 540: } else { 541: t23 = $[79]; 542: } 543: let t24; 544: if ($[80] !== currentQuestion.question || $[81] !== pastedContentsByQuestion) { 545: t24 = pastedContentsByQuestion[currentQuestion.question] ?? {}; 546: $[80] = currentQuestion.question; 547: $[81] = pastedContentsByQuestion; 548: $[82] = t24; 549: } else { 550: t24 = $[82]; 551: } 552: let t25; 553: if ($[83] !== currentQuestion.question) { 554: t25 = id_0 => onRemoveImage(currentQuestion.question, id_0); 555: $[83] = currentQuestion.question; 556: $[84] = t25; 557: } else { 558: t25 = $[84]; 559: } 560: let t26; 561: if ($[85] !== answers || $[86] !== currentQuestion || $[87] !== currentQuestionIndex || $[88] !== globalContentHeight || $[89] !== globalContentWidth || $[90] !== handleCancel || $[91] !== handleFinishPlanInterview || $[92] !== handleQuestionAnswer || $[93] !== handleRespondToClaude || $[94] !== handleTabNext || $[95] !== handleTabPrev || $[96] !== hideSubmitTab || $[97] !== nextQuestion || $[98] !== planFilePath || $[99] !== questionStates || $[100] !== questions || $[101] !== setTextInputMode || $[102] !== t23 || $[103] !== t24 || $[104] !== t25 || $[105] !== updateQuestionState) { 562: t26 = <><QuestionView question={currentQuestion} questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} questionStates={questionStates} hideSubmitTab={hideSubmitTab} minContentHeight={globalContentHeight} minContentWidth={globalContentWidth} planFilePath={planFilePath} onUpdateQuestionState={updateQuestionState} onAnswer={handleQuestionAnswer} onTextInputFocus={setTextInputMode} onCancel={handleCancel} onSubmit={nextQuestion} onTabPrev={handleTabPrev} onTabNext={handleTabNext} onRespondToClaude={handleRespondToClaude} onFinishPlanInterview={handleFinishPlanInterview} onImagePaste={t23} pastedContents={t24} onRemoveImage={t25} /></>; 563: $[85] = answers; 564: $[86] = currentQuestion; 565: $[87] = currentQuestionIndex; 566: $[88] = globalContentHeight; 567: $[89] = globalContentWidth; 568: $[90] = handleCancel; 569: $[91] = handleFinishPlanInterview; 570: $[92] = handleQuestionAnswer; 571: $[93] = handleRespondToClaude; 572: $[94] = handleTabNext; 573: $[95] = handleTabPrev; 574: $[96] = hideSubmitTab; 575: $[97] = nextQuestion; 576: $[98] = planFilePath; 577: $[99] = questionStates; 578: $[100] = questions; 579: $[101] = setTextInputMode; 580: $[102] = t23; 581: $[103] = t24; 582: $[104] = t25; 583: $[105] = updateQuestionState; 584: $[106] = t26; 585: } else { 586: t26 = $[106]; 587: } 588: return t26; 589: } 590: if (isInSubmitView) { 591: let t23; 592: if ($[107] !== allQuestionsAnswered || $[108] !== answers || $[109] !== currentQuestionIndex || $[110] !== globalContentHeight || $[111] !== handleFinalResponse || $[112] !== questions || $[113] !== toolUseConfirm.permissionResult) { 593: t23 = <><SubmitQuestionsView questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} allQuestionsAnswered={allQuestionsAnswered} permissionResult={toolUseConfirm.permissionResult} minContentHeight={globalContentHeight} onFinalResponse={handleFinalResponse} /></>; 594: $[107] = allQuestionsAnswered; 595: $[108] = answers; 596: $[109] = currentQuestionIndex; 597: $[110] = globalContentHeight; 598: $[111] = handleFinalResponse; 599: $[112] = questions; 600: $[113] = toolUseConfirm.permissionResult; 601: $[114] = t23; 602: } else { 603: t23 = $[114]; 604: } 605: return t23; 606: } 607: return null; 608: } 609: function _temp6(c_1) { 610: return c_1.type === "image"; 611: } 612: function _temp5(c_0) { 613: return c_0.type === "image"; 614: } 615: function _temp4(s) { 616: return s.toolPermissionContext.mode; 617: } 618: function _temp3(c) { 619: return c.type === "image"; 620: } 621: function _temp2(contents) { 622: return Object.values(contents); 623: } 624: function _temp(opt) { 625: return opt.preview; 626: } 627: async function convertImagesToBlocks(images: PastedContent[]): Promise<ImageBlockParam[] | undefined> { 628: if (images.length === 0) return undefined; 629: return Promise.all(images.map(async img => { 630: const block: ImageBlockParam = { 631: type: 'image', 632: source: { 633: type: 'base64', 634: media_type: (img.mediaType || 'image/png') as Base64ImageSource['media_type'], 635: data: img.content 636: } 637: }; 638: const resized = await maybeResizeAndDownsampleImageBlock(block); 639: return resized.block; 640: })); 641: }

File: src/components/permissions/AskUserQuestionPermissionRequest/PreviewBox.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { Suspense, use, useMemo } from 'react'; 3: import { useSettings } from '../../../hooks/useSettings.js'; 4: import { useTerminalSize } from '../../../hooks/useTerminalSize.js'; 5: import { stringWidth } from '../../../ink/stringWidth.js'; 6: import { Ansi, Box, Text, useTheme } from '../../../ink.js'; 7: import { type CliHighlight, getCliHighlightPromise } from '../../../utils/cliHighlight.js'; 8: import { applyMarkdown } from '../../../utils/markdown.js'; 9: import sliceAnsi from '../../../utils/sliceAnsi.js'; 10: type PreviewBoxProps = { 11: content: string; 12: maxLines?: number; 13: minHeight?: number; 14: minWidth?: number; 15: maxWidth?: number; 16: }; 17: const BOX_CHARS = { 18: topLeft: '┌', 19: topRight: '┐', 20: bottomLeft: '└', 21: bottomRight: '┘', 22: horizontal: '─', 23: vertical: '│', 24: teeLeft: '├', 25: teeRight: '┤' 26: }; 27: export function PreviewBox(props) { 28: const $ = _c(4); 29: const settings = useSettings(); 30: if (settings.syntaxHighlightingDisabled) { 31: let t0; 32: if ($[0] !== props) { 33: t0 = <PreviewBoxBody {...props} highlight={null} />; 34: $[0] = props; 35: $[1] = t0; 36: } else { 37: t0 = $[1]; 38: } 39: return t0; 40: } 41: let t0; 42: if ($[2] !== props) { 43: t0 = <Suspense fallback={<PreviewBoxBody {...props} highlight={null} />}><PreviewBoxWithHighlight {...props} /></Suspense>; 44: $[2] = props; 45: $[3] = t0; 46: } else { 47: t0 = $[3]; 48: } 49: return t0; 50: } 51: function PreviewBoxWithHighlight(props) { 52: const $ = _c(4); 53: let t0; 54: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 55: t0 = getCliHighlightPromise(); 56: $[0] = t0; 57: } else { 58: t0 = $[0]; 59: } 60: const highlight = use(t0); 61: let t1; 62: if ($[1] !== highlight || $[2] !== props) { 63: t1 = <PreviewBoxBody {...props} highlight={highlight} />; 64: $[1] = highlight; 65: $[2] = props; 66: $[3] = t1; 67: } else { 68: t1 = $[3]; 69: } 70: return t1; 71: } 72: function PreviewBoxBody(t0) { 73: const $ = _c(34); 74: const { 75: content, 76: maxLines, 77: minHeight, 78: minWidth: t1, 79: maxWidth, 80: highlight 81: } = t0; 82: const minWidth = t1 === undefined ? 40 : t1; 83: const { 84: columns: terminalWidth 85: } = useTerminalSize(); 86: const [theme] = useTheme(); 87: const effectiveMaxWidth = maxWidth ?? terminalWidth - 4; 88: const effectiveMaxLines = maxLines ?? 20; 89: let t2; 90: if ($[0] !== content || $[1] !== highlight || $[2] !== theme) { 91: t2 = applyMarkdown(content, theme, highlight); 92: $[0] = content; 93: $[1] = highlight; 94: $[2] = theme; 95: $[3] = t2; 96: } else { 97: t2 = $[3]; 98: } 99: const rendered = t2; 100: let T0; 101: let bottomBorder; 102: let t3; 103: let t4; 104: let t5; 105: let truncationBar; 106: if ($[4] !== effectiveMaxLines || $[5] !== effectiveMaxWidth || $[6] !== minHeight || $[7] !== minWidth || $[8] !== rendered) { 107: const contentLines = rendered.split("\n"); 108: const isTruncated = contentLines.length > effectiveMaxLines; 109: const truncatedLines = isTruncated ? contentLines.slice(0, effectiveMaxLines) : contentLines; 110: const effectiveMinHeight = Math.min(minHeight ?? 0, effectiveMaxLines); 111: const paddingNeeded = Math.max(0, effectiveMinHeight - truncatedLines.length - (isTruncated ? 1 : 0)); 112: const lines = paddingNeeded > 0 ? [...truncatedLines, ...Array(paddingNeeded).fill("")] : truncatedLines; 113: const contentWidth = Math.max(minWidth, ...lines.map(_temp)); 114: const boxWidth = Math.min(contentWidth + 4, effectiveMaxWidth); 115: const innerWidth = boxWidth - 4; 116: let t6; 117: if ($[15] !== boxWidth) { 118: t6 = BOX_CHARS.horizontal.repeat(boxWidth - 2); 119: $[15] = boxWidth; 120: $[16] = t6; 121: } else { 122: t6 = $[16]; 123: } 124: const topBorder = `${BOX_CHARS.topLeft}${t6}${BOX_CHARS.topRight}`; 125: let t7; 126: if ($[17] !== boxWidth) { 127: t7 = BOX_CHARS.horizontal.repeat(boxWidth - 2); 128: $[17] = boxWidth; 129: $[18] = t7; 130: } else { 131: t7 = $[18]; 132: } 133: bottomBorder = `${BOX_CHARS.bottomLeft}${t7}${BOX_CHARS.bottomRight}`; 134: truncationBar = isTruncated ? (() => { 135: const hiddenCount = contentLines.length - effectiveMaxLines; 136: const label = `${BOX_CHARS.horizontal.repeat(3)} \u2702 ${BOX_CHARS.horizontal.repeat(3)} ${hiddenCount} lines hidden `; 137: const labelWidth = stringWidth(label); 138: const fillWidth = Math.max(0, boxWidth - 2 - labelWidth); 139: return `${BOX_CHARS.teeLeft}${label}${BOX_CHARS.horizontal.repeat(fillWidth)}${BOX_CHARS.teeRight}`; 140: })() : null; 141: T0 = Box; 142: t3 = "column"; 143: if ($[19] !== topBorder) { 144: t4 = <Text dimColor={true}>{topBorder}</Text>; 145: $[19] = topBorder; 146: $[20] = t4; 147: } else { 148: t4 = $[20]; 149: } 150: let t8; 151: if ($[21] !== innerWidth) { 152: t8 = (line_0, index) => { 153: const lineWidth = stringWidth(line_0); 154: const displayLine = lineWidth > innerWidth ? sliceAnsi(line_0, 0, innerWidth) : line_0; 155: const padding = " ".repeat(Math.max(0, innerWidth - stringWidth(displayLine))); 156: return <Box key={index} flexDirection="row"><Text dimColor={true}>{BOX_CHARS.vertical} </Text><Ansi>{displayLine}</Ansi><Text dimColor={true}>{padding} {BOX_CHARS.vertical}</Text></Box>; 157: }; 158: $[21] = innerWidth; 159: $[22] = t8; 160: } else { 161: t8 = $[22]; 162: } 163: t5 = lines.map(t8); 164: $[4] = effectiveMaxLines; 165: $[5] = effectiveMaxWidth; 166: $[6] = minHeight; 167: $[7] = minWidth; 168: $[8] = rendered; 169: $[9] = T0; 170: $[10] = bottomBorder; 171: $[11] = t3; 172: $[12] = t4; 173: $[13] = t5; 174: $[14] = truncationBar; 175: } else { 176: T0 = $[9]; 177: bottomBorder = $[10]; 178: t3 = $[11]; 179: t4 = $[12]; 180: t5 = $[13]; 181: truncationBar = $[14]; 182: } 183: let t6; 184: if ($[23] !== truncationBar) { 185: t6 = truncationBar && <Text color="warning">{truncationBar}</Text>; 186: $[23] = truncationBar; 187: $[24] = t6; 188: } else { 189: t6 = $[24]; 190: } 191: let t7; 192: if ($[25] !== bottomBorder) { 193: t7 = <Text dimColor={true}>{bottomBorder}</Text>; 194: $[25] = bottomBorder; 195: $[26] = t7; 196: } else { 197: t7 = $[26]; 198: } 199: let t8; 200: if ($[27] !== T0 || $[28] !== t3 || $[29] !== t4 || $[30] !== t5 || $[31] !== t6 || $[32] !== t7) { 201: t8 = <T0 flexDirection={t3}>{t4}{t5}{t6}{t7}</T0>; 202: $[27] = T0; 203: $[28] = t3; 204: $[29] = t4; 205: $[30] = t5; 206: $[31] = t6; 207: $[32] = t7; 208: $[33] = t8; 209: } else { 210: t8 = $[33]; 211: } 212: return t8; 213: } 214: function _temp(line) { 215: return stringWidth(line); 216: }

File: src/components/permissions/AskUserQuestionPermissionRequest/PreviewQuestionView.tsx

typescript 1: import figures from 'figures'; 2: import React, { useCallback, useMemo, useRef, useState } from 'react'; 3: import { useTerminalSize } from '../../../hooks/useTerminalSize.js'; 4: import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'; 5: import { Box, Text } from '../../../ink.js'; 6: import { useKeybinding, useKeybindings } from '../../../keybindings/useKeybinding.js'; 7: import { useAppState } from '../../../state/AppState.js'; 8: import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'; 9: import { getExternalEditor } from '../../../utils/editor.js'; 10: import { toIDEDisplayName } from '../../../utils/ide.js'; 11: import { editPromptInEditor } from '../../../utils/promptEditor.js'; 12: import { Divider } from '../../design-system/Divider.js'; 13: import TextInput from '../../TextInput.js'; 14: import { PermissionRequestTitle } from '../PermissionRequestTitle.js'; 15: import { PreviewBox } from './PreviewBox.js'; 16: import { QuestionNavigationBar } from './QuestionNavigationBar.js'; 17: import type { QuestionState } from './use-multiple-choice-state.js'; 18: type Props = { 19: question: Question; 20: questions: Question[]; 21: currentQuestionIndex: number; 22: answers: Record<string, string>; 23: questionStates: Record<string, QuestionState>; 24: hideSubmitTab?: boolean; 25: minContentHeight?: number; 26: minContentWidth?: number; 27: onUpdateQuestionState: (questionText: string, updates: Partial<QuestionState>, isMultiSelect: boolean) => void; 28: onAnswer: (questionText: string, label: string | string[], textInput?: string, shouldAdvance?: boolean) => void; 29: onTextInputFocus: (isInInput: boolean) => void; 30: onCancel: () => void; 31: onTabPrev?: () => void; 32: onTabNext?: () => void; 33: onRespondToClaude: () => void; 34: onFinishPlanInterview: () => void; 35: }; 36: export function PreviewQuestionView({ 37: question, 38: questions, 39: currentQuestionIndex, 40: answers, 41: questionStates, 42: hideSubmitTab = false, 43: minContentHeight, 44: minContentWidth, 45: onUpdateQuestionState, 46: onAnswer, 47: onTextInputFocus, 48: onCancel, 49: onTabPrev, 50: onTabNext, 51: onRespondToClaude, 52: onFinishPlanInterview 53: }: Props): React.ReactNode { 54: const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan'; 55: const [isFooterFocused, setIsFooterFocused] = useState(false); 56: const [footerIndex, setFooterIndex] = useState(0); 57: const [isInNotesInput, setIsInNotesInput] = useState(false); 58: const [cursorOffset, setCursorOffset] = useState(0); 59: const editor = getExternalEditor(); 60: const editorName = editor ? toIDEDisplayName(editor) : null; 61: const questionText = question.question; 62: const questionState = questionStates[questionText]; 63: const allOptions = question.options; 64: const [focusedIndex, setFocusedIndex] = useState(0); 65: const prevQuestionText = useRef(questionText); 66: if (prevQuestionText.current !== questionText) { 67: prevQuestionText.current = questionText; 68: const selected = questionState?.selectedValue as string | undefined; 69: const idx = selected ? allOptions.findIndex(opt => opt.label === selected) : -1; 70: setFocusedIndex(idx >= 0 ? idx : 0); 71: } 72: const focusedOption = allOptions[focusedIndex]; 73: const selectedValue = questionState?.selectedValue as string | undefined; 74: const notesValue = questionState?.textInputValue || ''; 75: const handleSelectOption = useCallback((index: number) => { 76: const option = allOptions[index]; 77: if (!option) return; 78: setFocusedIndex(index); 79: onUpdateQuestionState(questionText, { 80: selectedValue: option.label 81: }, false); 82: onAnswer(questionText, option.label); 83: }, [allOptions, questionText, onUpdateQuestionState, onAnswer]); 84: const handleNavigate = useCallback((direction: 'up' | 'down' | number) => { 85: if (isInNotesInput) return; 86: let newIndex: number; 87: if (typeof direction === 'number') { 88: newIndex = direction; 89: } else if (direction === 'up') { 90: newIndex = focusedIndex > 0 ? focusedIndex - 1 : focusedIndex; 91: } else { 92: newIndex = focusedIndex < allOptions.length - 1 ? focusedIndex + 1 : focusedIndex; 93: } 94: if (newIndex >= 0 && newIndex < allOptions.length) { 95: setFocusedIndex(newIndex); 96: } 97: }, [focusedIndex, allOptions.length, isInNotesInput]); 98: useKeybinding('chat:externalEditor', async () => { 99: const currentValue = questionState?.textInputValue || ''; 100: const result = await editPromptInEditor(currentValue); 101: if (result.content !== null && result.content !== currentValue) { 102: onUpdateQuestionState(questionText, { 103: textInputValue: result.content 104: }, false); 105: } 106: }, { 107: context: 'Chat', 108: isActive: isInNotesInput && !!editor 109: }); 110: useKeybindings({ 111: 'tabs:previous': () => onTabPrev?.(), 112: 'tabs:next': () => onTabNext?.() 113: }, { 114: context: 'Tabs', 115: isActive: !isInNotesInput && !isFooterFocused 116: }); 117: const handleNotesExit = useCallback(() => { 118: setIsInNotesInput(false); 119: onTextInputFocus(false); 120: if (selectedValue) { 121: onAnswer(questionText, selectedValue); 122: } 123: }, [selectedValue, questionText, onAnswer, onTextInputFocus]); 124: const handleDownFromPreview = useCallback(() => { 125: setIsFooterFocused(true); 126: }, []); 127: const handleUpFromFooter = useCallback(() => { 128: setIsFooterFocused(false); 129: }, []); 130: const handleKeyDown = useCallback((e: KeyboardEvent) => { 131: if (isFooterFocused) { 132: if (e.key === 'up' || e.ctrl && e.key === 'p') { 133: e.preventDefault(); 134: if (footerIndex === 0) { 135: handleUpFromFooter(); 136: } else { 137: setFooterIndex(0); 138: } 139: return; 140: } 141: if (e.key === 'down' || e.ctrl && e.key === 'n') { 142: e.preventDefault(); 143: if (isInPlanMode && footerIndex === 0) { 144: setFooterIndex(1); 145: } 146: return; 147: } 148: if (e.key === 'return') { 149: e.preventDefault(); 150: if (footerIndex === 0) { 151: onRespondToClaude(); 152: } else { 153: onFinishPlanInterview(); 154: } 155: return; 156: } 157: if (e.key === 'escape') { 158: e.preventDefault(); 159: onCancel(); 160: } 161: return; 162: } 163: if (isInNotesInput) { 164: if (e.key === 'escape') { 165: e.preventDefault(); 166: handleNotesExit(); 167: } 168: return; 169: } 170: if (e.key === 'up' || e.ctrl && e.key === 'p') { 171: e.preventDefault(); 172: if (focusedIndex > 0) { 173: handleNavigate('up'); 174: } 175: } else if (e.key === 'down' || e.ctrl && e.key === 'n') { 176: e.preventDefault(); 177: if (focusedIndex === allOptions.length - 1) { 178: handleDownFromPreview(); 179: } else { 180: handleNavigate('down'); 181: } 182: } else if (e.key === 'return') { 183: e.preventDefault(); 184: handleSelectOption(focusedIndex); 185: } else if (e.key === 'n' && !e.ctrl && !e.meta) { 186: e.preventDefault(); 187: setIsInNotesInput(true); 188: onTextInputFocus(true); 189: } else if (e.key === 'escape') { 190: e.preventDefault(); 191: onCancel(); 192: } else if (e.key.length === 1 && e.key >= '1' && e.key <= '9') { 193: e.preventDefault(); 194: const idx_0 = parseInt(e.key, 10) - 1; 195: if (idx_0 < allOptions.length) { 196: handleNavigate(idx_0); 197: } 198: } 199: }, [isFooterFocused, footerIndex, isInPlanMode, isInNotesInput, focusedIndex, allOptions.length, handleUpFromFooter, handleDownFromPreview, handleNavigate, handleSelectOption, handleNotesExit, onRespondToClaude, onFinishPlanInterview, onCancel, onTextInputFocus]); 200: const previewContent = focusedOption?.preview || null; 201: const LEFT_PANEL_WIDTH = 30; 202: const GAP = 4; 203: const { 204: columns 205: } = useTerminalSize(); 206: const previewMaxWidth = columns - LEFT_PANEL_WIDTH - GAP; 207: const PREVIEW_OVERHEAD = 11; 208: const previewMaxLines = useMemo(() => { 209: return minContentHeight ? Math.max(1, minContentHeight - PREVIEW_OVERHEAD) : undefined; 210: }, [minContentHeight]); 211: return <Box flexDirection="column" marginTop={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}> 212: <Divider color="inactive" /> 213: <Box flexDirection="column" paddingTop={0}> 214: <QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} hideSubmitTab={hideSubmitTab} /> 215: <PermissionRequestTitle title={question.question} color={'text'} /> 216: <Box flexDirection="column" minHeight={minContentHeight}> 217: {} 218: <Box marginTop={1} flexDirection="row" gap={4}> 219: {} 220: <Box flexDirection="column" width={30}> 221: {allOptions.map((option_0, index_0) => { 222: const isFocused = focusedIndex === index_0; 223: const isSelected = selectedValue === option_0.label; 224: return <Box key={option_0.label} flexDirection="row"> 225: {isFocused ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>} 226: <Text dimColor> {index_0 + 1}.</Text> 227: <Text color={isSelected ? 'success' : isFocused ? 'suggestion' : undefined} bold={isFocused}> 228: {' '} 229: {option_0.label} 230: </Text> 231: {isSelected && <Text color="success"> {figures.tick}</Text>} 232: </Box>; 233: })} 234: </Box> 235: {} 236: <Box flexDirection="column" flexGrow={1}> 237: <PreviewBox content={previewContent || 'No preview available'} maxLines={previewMaxLines} minWidth={minContentWidth} maxWidth={previewMaxWidth} /> 238: <Box marginTop={1} flexDirection="row" gap={1}> 239: <Text color="suggestion">Notes:</Text> 240: {isInNotesInput ? <TextInput value={notesValue} placeholder="Add notes on this design…" onChange={value => { 241: onUpdateQuestionState(questionText, { 242: textInputValue: value 243: }, false); 244: }} onSubmit={handleNotesExit} onExit={handleNotesExit} focus={true} showCursor={true} columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /> : <Text dimColor italic> 245: {notesValue || 'press n to add notes'} 246: </Text>} 247: </Box> 248: </Box> 249: </Box> 250: {} 251: <Box flexDirection="column" marginTop={1}> 252: <Divider color="inactive" /> 253: <Box flexDirection="row" gap={1}> 254: {isFooterFocused && footerIndex === 0 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>} 255: <Text color={isFooterFocused && footerIndex === 0 ? 'suggestion' : undefined}> 256: Chat about this 257: </Text> 258: </Box> 259: {isInPlanMode && <Box flexDirection="row" gap={1}> 260: {isFooterFocused && footerIndex === 1 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>} 261: <Text color={isFooterFocused && footerIndex === 1 ? 'suggestion' : undefined}> 262: Skip interview and plan immediately 263: </Text> 264: </Box>} 265: </Box> 266: <Box marginTop={1}> 267: <Text color="inactive" dimColor> 268: Enter to select · {figures.arrowUp}/{figures.arrowDown} to 269: navigate · n to add notes 270: {questions.length > 1 && <> · Tab to switch questions</>} 271: {isInNotesInput && editorName && <> · ctrl+g to edit in {editorName}</>}{' '} 272: · Esc to cancel 273: </Text> 274: </Box> 275: </Box> 276: </Box> 277: </Box>; 278: }

File: src/components/permissions/AskUserQuestionPermissionRequest/QuestionNavigationBar.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React, { useMemo } from 'react'; 4: import { useTerminalSize } from '../../../hooks/useTerminalSize.js'; 5: import { stringWidth } from '../../../ink/stringWidth.js'; 6: import { Box, Text } from '../../../ink.js'; 7: import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'; 8: import { truncateToWidth } from '../../../utils/format.js'; 9: type Props = { 10: questions: Question[]; 11: currentQuestionIndex: number; 12: answers: Record<string, string>; 13: hideSubmitTab?: boolean; 14: }; 15: export function QuestionNavigationBar(t0) { 16: const $ = _c(39); 17: const { 18: questions, 19: currentQuestionIndex, 20: answers, 21: hideSubmitTab: t1 22: } = t0; 23: const hideSubmitTab = t1 === undefined ? false : t1; 24: const { 25: columns 26: } = useTerminalSize(); 27: let t2; 28: if ($[0] !== columns || $[1] !== currentQuestionIndex || $[2] !== hideSubmitTab || $[3] !== questions) { 29: bb0: { 30: const submitText = hideSubmitTab ? "" : ` ${figures.tick} Submit `; 31: const fixedWidth = stringWidth("\u2190 ") + stringWidth(" \u2192") + stringWidth(submitText); 32: const availableForTabs = columns - fixedWidth; 33: if (availableForTabs <= 0) { 34: let t3; 35: if ($[5] !== currentQuestionIndex || $[6] !== questions) { 36: let t4; 37: if ($[8] !== currentQuestionIndex) { 38: t4 = (q, index) => { 39: const header = q?.header || `Q${index + 1}`; 40: return index === currentQuestionIndex ? header.slice(0, 3) : ""; 41: }; 42: $[8] = currentQuestionIndex; 43: $[9] = t4; 44: } else { 45: t4 = $[9]; 46: } 47: t3 = questions.map(t4); 48: $[5] = currentQuestionIndex; 49: $[6] = questions; 50: $[7] = t3; 51: } else { 52: t3 = $[7]; 53: } 54: t2 = t3; 55: break bb0; 56: } 57: const tabHeaders = questions.map(_temp); 58: const idealWidths = tabHeaders.map(_temp2); 59: const totalIdealWidth = idealWidths.reduce(_temp3, 0); 60: if (totalIdealWidth <= availableForTabs) { 61: t2 = tabHeaders; 62: break bb0; 63: } 64: const currentHeader = tabHeaders[currentQuestionIndex] || ""; 65: const currentIdealWidth = 4 + stringWidth(currentHeader); 66: const currentTabWidth = Math.min(currentIdealWidth, availableForTabs / 2); 67: const remainingWidth = availableForTabs - currentTabWidth; 68: const otherTabCount = questions.length - 1; 69: const widthPerOtherTab = Math.max(6, Math.floor(remainingWidth / Math.max(otherTabCount, 1))); 70: let t3; 71: if ($[10] !== currentQuestionIndex || $[11] !== currentTabWidth || $[12] !== widthPerOtherTab) { 72: t3 = (header_1, index_1) => { 73: if (index_1 === currentQuestionIndex) { 74: const maxTextWidth = currentTabWidth - 2 - 2; 75: return truncateToWidth(header_1, maxTextWidth); 76: } else { 77: const maxTextWidth_0 = widthPerOtherTab - 2 - 2; 78: return truncateToWidth(header_1, maxTextWidth_0); 79: } 80: }; 81: $[10] = currentQuestionIndex; 82: $[11] = currentTabWidth; 83: $[12] = widthPerOtherTab; 84: $[13] = t3; 85: } else { 86: t3 = $[13]; 87: } 88: t2 = tabHeaders.map(t3); 89: } 90: $[0] = columns; 91: $[1] = currentQuestionIndex; 92: $[2] = hideSubmitTab; 93: $[3] = questions; 94: $[4] = t2; 95: } else { 96: t2 = $[4]; 97: } 98: const tabDisplayTexts = t2; 99: const hideArrows = questions.length === 1 && hideSubmitTab; 100: let t3; 101: if ($[14] !== currentQuestionIndex || $[15] !== hideArrows) { 102: t3 = !hideArrows && <Text color={currentQuestionIndex === 0 ? "inactive" : undefined}>←{" "}</Text>; 103: $[14] = currentQuestionIndex; 104: $[15] = hideArrows; 105: $[16] = t3; 106: } else { 107: t3 = $[16]; 108: } 109: let t4; 110: if ($[17] !== answers || $[18] !== currentQuestionIndex || $[19] !== questions || $[20] !== tabDisplayTexts) { 111: let t5; 112: if ($[22] !== answers || $[23] !== currentQuestionIndex || $[24] !== tabDisplayTexts) { 113: t5 = (q_1, index_2) => { 114: const isSelected = index_2 === currentQuestionIndex; 115: const isAnswered = q_1?.question && !!answers[q_1.question]; 116: const checkbox = isAnswered ? figures.checkboxOn : figures.checkboxOff; 117: const displayText = tabDisplayTexts[index_2] || q_1?.header || `Q${index_2 + 1}`; 118: return <Box key={q_1?.question || `question-${index_2}`}>{isSelected ? <Text backgroundColor="permission" color="inverseText">{" "}{checkbox} {displayText}{" "}</Text> : <Text>{" "}{checkbox} {displayText}{" "}</Text>}</Box>; 119: }; 120: $[22] = answers; 121: $[23] = currentQuestionIndex; 122: $[24] = tabDisplayTexts; 123: $[25] = t5; 124: } else { 125: t5 = $[25]; 126: } 127: t4 = questions.map(t5); 128: $[17] = answers; 129: $[18] = currentQuestionIndex; 130: $[19] = questions; 131: $[20] = tabDisplayTexts; 132: $[21] = t4; 133: } else { 134: t4 = $[21]; 135: } 136: let t5; 137: if ($[26] !== currentQuestionIndex || $[27] !== hideSubmitTab || $[28] !== questions.length) { 138: t5 = !hideSubmitTab && <Box key="submit">{currentQuestionIndex === questions.length ? <Text backgroundColor="permission" color="inverseText">{" "}{figures.tick} Submit{" "}</Text> : <Text> {figures.tick} Submit </Text>}</Box>; 139: $[26] = currentQuestionIndex; 140: $[27] = hideSubmitTab; 141: $[28] = questions.length; 142: $[29] = t5; 143: } else { 144: t5 = $[29]; 145: } 146: let t6; 147: if ($[30] !== currentQuestionIndex || $[31] !== hideArrows || $[32] !== questions.length) { 148: t6 = !hideArrows && <Text color={currentQuestionIndex === questions.length ? "inactive" : undefined}>{" "}→</Text>; 149: $[30] = currentQuestionIndex; 150: $[31] = hideArrows; 151: $[32] = questions.length; 152: $[33] = t6; 153: } else { 154: t6 = $[33]; 155: } 156: let t7; 157: if ($[34] !== t3 || $[35] !== t4 || $[36] !== t5 || $[37] !== t6) { 158: t7 = <Box flexDirection="row" marginBottom={1}>{t3}{t4}{t5}{t6}</Box>; 159: $[34] = t3; 160: $[35] = t4; 161: $[36] = t5; 162: $[37] = t6; 163: $[38] = t7; 164: } else { 165: t7 = $[38]; 166: } 167: return t7; 168: } 169: function _temp3(sum, w) { 170: return sum + w; 171: } 172: function _temp2(header_0) { 173: return 4 + stringWidth(header_0); 174: } 175: function _temp(q_0, index_0) { 176: return q_0?.header || `Q${index_0 + 1}`; 177: }

File: src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React, { useCallback, useState } from 'react'; 4: import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'; 5: import { Box, Text } from '../../../ink.js'; 6: import { useAppState } from '../../../state/AppState.js'; 7: import type { Question, QuestionOption } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'; 8: import type { PastedContent } from '../../../utils/config.js'; 9: import { getExternalEditor } from '../../../utils/editor.js'; 10: import { toIDEDisplayName } from '../../../utils/ide.js'; 11: import type { ImageDimensions } from '../../../utils/imageResizer.js'; 12: import { editPromptInEditor } from '../../../utils/promptEditor.js'; 13: import { type OptionWithDescription, Select, SelectMulti } from '../../CustomSelect/index.js'; 14: import { Divider } from '../../design-system/Divider.js'; 15: import { FilePathLink } from '../../FilePathLink.js'; 16: import { PermissionRequestTitle } from '../PermissionRequestTitle.js'; 17: import { PreviewQuestionView } from './PreviewQuestionView.js'; 18: import { QuestionNavigationBar } from './QuestionNavigationBar.js'; 19: import type { QuestionState } from './use-multiple-choice-state.js'; 20: type Props = { 21: question: Question; 22: questions: Question[]; 23: currentQuestionIndex: number; 24: answers: Record<string, string>; 25: questionStates: Record<string, QuestionState>; 26: hideSubmitTab?: boolean; 27: planFilePath?: string; 28: pastedContents?: Record<number, PastedContent>; 29: minContentHeight?: number; 30: minContentWidth?: number; 31: onUpdateQuestionState: (questionText: string, updates: Partial<QuestionState>, isMultiSelect: boolean) => void; 32: onAnswer: (questionText: string, label: string | string[], textInput?: string, shouldAdvance?: boolean) => void; 33: onTextInputFocus: (isInInput: boolean) => void; 34: onCancel: () => void; 35: onSubmit: () => void; 36: onTabPrev?: () => void; 37: onTabNext?: () => void; 38: onRespondToClaude: () => void; 39: onFinishPlanInterview: () => void; 40: onImagePaste?: (base64Image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) => void; 41: onRemoveImage?: (id: number) => void; 42: }; 43: export function QuestionView(t0) { 44: const $ = _c(114); 45: const { 46: question, 47: questions, 48: currentQuestionIndex, 49: answers, 50: questionStates, 51: hideSubmitTab: t1, 52: planFilePath, 53: minContentHeight, 54: minContentWidth, 55: onUpdateQuestionState, 56: onAnswer, 57: onTextInputFocus, 58: onCancel, 59: onSubmit, 60: onTabPrev, 61: onTabNext, 62: onRespondToClaude, 63: onFinishPlanInterview, 64: onImagePaste, 65: pastedContents, 66: onRemoveImage 67: } = t0; 68: const hideSubmitTab = t1 === undefined ? false : t1; 69: const isInPlanMode = useAppState(_temp) === "plan"; 70: const [isFooterFocused, setIsFooterFocused] = useState(false); 71: const [footerIndex, setFooterIndex] = useState(0); 72: const [isOtherFocused, setIsOtherFocused] = useState(false); 73: let t2; 74: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 75: const editor = getExternalEditor(); 76: t2 = editor ? toIDEDisplayName(editor) : null; 77: $[0] = t2; 78: } else { 79: t2 = $[0]; 80: } 81: const editorName = t2; 82: let t3; 83: if ($[1] !== onTextInputFocus) { 84: t3 = value => { 85: const isOther = value === "__other__"; 86: setIsOtherFocused(isOther); 87: onTextInputFocus(isOther); 88: }; 89: $[1] = onTextInputFocus; 90: $[2] = t3; 91: } else { 92: t3 = $[2]; 93: } 94: const handleFocus = t3; 95: let t4; 96: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 97: t4 = () => { 98: setIsFooterFocused(true); 99: }; 100: $[3] = t4; 101: } else { 102: t4 = $[3]; 103: } 104: const handleDownFromLastItem = t4; 105: let t5; 106: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 107: t5 = () => { 108: setIsFooterFocused(false); 109: }; 110: $[4] = t5; 111: } else { 112: t5 = $[4]; 113: } 114: const handleUpFromFooter = t5; 115: let t6; 116: if ($[5] !== footerIndex || $[6] !== isFooterFocused || $[7] !== isInPlanMode || $[8] !== onCancel || $[9] !== onFinishPlanInterview || $[10] !== onRespondToClaude) { 117: t6 = e => { 118: if (!isFooterFocused) { 119: return; 120: } 121: if (e.key === "up" || e.ctrl && e.key === "p") { 122: e.preventDefault(); 123: if (footerIndex === 0) { 124: handleUpFromFooter(); 125: } else { 126: setFooterIndex(0); 127: } 128: return; 129: } 130: if (e.key === "down" || e.ctrl && e.key === "n") { 131: e.preventDefault(); 132: if (isInPlanMode && footerIndex === 0) { 133: setFooterIndex(1); 134: } 135: return; 136: } 137: if (e.key === "return") { 138: e.preventDefault(); 139: if (footerIndex === 0) { 140: onRespondToClaude(); 141: } else { 142: onFinishPlanInterview(); 143: } 144: return; 145: } 146: if (e.key === "escape") { 147: e.preventDefault(); 148: onCancel(); 149: } 150: }; 151: $[5] = footerIndex; 152: $[6] = isFooterFocused; 153: $[7] = isInPlanMode; 154: $[8] = onCancel; 155: $[9] = onFinishPlanInterview; 156: $[10] = onRespondToClaude; 157: $[11] = t6; 158: } else { 159: t6 = $[11]; 160: } 161: const handleKeyDown = t6; 162: let handleOpenEditor; 163: let questionText; 164: let t7; 165: if ($[12] !== onUpdateQuestionState || $[13] !== question || $[14] !== questionStates) { 166: const textOptions = question.options.map(_temp2); 167: questionText = question.question; 168: const questionState = questionStates[questionText]; 169: let t8; 170: if ($[18] !== onUpdateQuestionState || $[19] !== question.multiSelect || $[20] !== questionText) { 171: t8 = async (currentValue, setValue) => { 172: const result = await editPromptInEditor(currentValue); 173: if (result.content !== null && result.content !== currentValue) { 174: setValue(result.content); 175: onUpdateQuestionState(questionText, { 176: textInputValue: result.content 177: }, question.multiSelect ?? false); 178: } 179: }; 180: $[18] = onUpdateQuestionState; 181: $[19] = question.multiSelect; 182: $[20] = questionText; 183: $[21] = t8; 184: } else { 185: t8 = $[21]; 186: } 187: handleOpenEditor = t8; 188: const t9 = question.multiSelect ? "Type something" : "Type something."; 189: const t10 = questionState?.textInputValue ?? ""; 190: let t11; 191: if ($[22] !== onUpdateQuestionState || $[23] !== question.multiSelect || $[24] !== questionText) { 192: t11 = value_0 => { 193: onUpdateQuestionState(questionText, { 194: textInputValue: value_0 195: }, question.multiSelect ?? false); 196: }; 197: $[22] = onUpdateQuestionState; 198: $[23] = question.multiSelect; 199: $[24] = questionText; 200: $[25] = t11; 201: } else { 202: t11 = $[25]; 203: } 204: let t12; 205: if ($[26] !== t10 || $[27] !== t11 || $[28] !== t9) { 206: t12 = { 207: type: "input" as const, 208: value: "__other__", 209: label: "Other", 210: placeholder: t9, 211: initialValue: t10, 212: onChange: t11 213: }; 214: $[26] = t10; 215: $[27] = t11; 216: $[28] = t9; 217: $[29] = t12; 218: } else { 219: t12 = $[29]; 220: } 221: const otherOption = t12; 222: t7 = [...textOptions, otherOption]; 223: $[12] = onUpdateQuestionState; 224: $[13] = question; 225: $[14] = questionStates; 226: $[15] = handleOpenEditor; 227: $[16] = questionText; 228: $[17] = t7; 229: } else { 230: handleOpenEditor = $[15]; 231: questionText = $[16]; 232: t7 = $[17]; 233: } 234: const options = t7; 235: const hasAnyPreview = !question.multiSelect && question.options.some(_temp3); 236: if (hasAnyPreview) { 237: let t8; 238: if ($[30] !== answers || $[31] !== currentQuestionIndex || $[32] !== hideSubmitTab || $[33] !== minContentHeight || $[34] !== minContentWidth || $[35] !== onAnswer || $[36] !== onCancel || $[37] !== onFinishPlanInterview || $[38] !== onRespondToClaude || $[39] !== onTabNext || $[40] !== onTabPrev || $[41] !== onTextInputFocus || $[42] !== onUpdateQuestionState || $[43] !== question || $[44] !== questionStates || $[45] !== questions) { 239: t8 = <PreviewQuestionView question={question} questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} questionStates={questionStates} hideSubmitTab={hideSubmitTab} minContentHeight={minContentHeight} minContentWidth={minContentWidth} onUpdateQuestionState={onUpdateQuestionState} onAnswer={onAnswer} onTextInputFocus={onTextInputFocus} onCancel={onCancel} onTabPrev={onTabPrev} onTabNext={onTabNext} onRespondToClaude={onRespondToClaude} onFinishPlanInterview={onFinishPlanInterview} />; 240: $[30] = answers; 241: $[31] = currentQuestionIndex; 242: $[32] = hideSubmitTab; 243: $[33] = minContentHeight; 244: $[34] = minContentWidth; 245: $[35] = onAnswer; 246: $[36] = onCancel; 247: $[37] = onFinishPlanInterview; 248: $[38] = onRespondToClaude; 249: $[39] = onTabNext; 250: $[40] = onTabPrev; 251: $[41] = onTextInputFocus; 252: $[42] = onUpdateQuestionState; 253: $[43] = question; 254: $[44] = questionStates; 255: $[45] = questions; 256: $[46] = t8; 257: } else { 258: t8 = $[46]; 259: } 260: return t8; 261: } 262: let t8; 263: if ($[47] !== isInPlanMode || $[48] !== planFilePath) { 264: t8 = isInPlanMode && planFilePath && <Box flexDirection="column" gap={0}><Divider color="inactive" /><Text color="inactive">Planning: <FilePathLink filePath={planFilePath} /></Text></Box>; 265: $[47] = isInPlanMode; 266: $[48] = planFilePath; 267: $[49] = t8; 268: } else { 269: t8 = $[49]; 270: } 271: let t9; 272: if ($[50] === Symbol.for("react.memo_cache_sentinel")) { 273: t9 = <Box marginTop={-1}><Divider color="inactive" /></Box>; 274: $[50] = t9; 275: } else { 276: t9 = $[50]; 277: } 278: let t10; 279: if ($[51] !== answers || $[52] !== currentQuestionIndex || $[53] !== hideSubmitTab || $[54] !== questions) { 280: t10 = <QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} hideSubmitTab={hideSubmitTab} />; 281: $[51] = answers; 282: $[52] = currentQuestionIndex; 283: $[53] = hideSubmitTab; 284: $[54] = questions; 285: $[55] = t10; 286: } else { 287: t10 = $[55]; 288: } 289: let t11; 290: if ($[56] !== question.question) { 291: t11 = <PermissionRequestTitle title={question.question} color="text" />; 292: $[56] = question.question; 293: $[57] = t11; 294: } else { 295: t11 = $[57]; 296: } 297: let t12; 298: if ($[58] !== currentQuestionIndex || $[59] !== handleFocus || $[60] !== handleOpenEditor || $[61] !== isFooterFocused || $[62] !== onAnswer || $[63] !== onCancel || $[64] !== onImagePaste || $[65] !== onRemoveImage || $[66] !== onSubmit || $[67] !== onUpdateQuestionState || $[68] !== options || $[69] !== pastedContents || $[70] !== question.multiSelect || $[71] !== question.question || $[72] !== questionStates || $[73] !== questionText || $[74] !== questions.length) { 299: t12 = <Box marginTop={1}>{question.multiSelect ? <SelectMulti key={question.question} options={options} defaultValue={questionStates[question.question]?.selectedValue as string[] | undefined} onChange={values => { 300: onUpdateQuestionState(questionText, { 301: selectedValue: values 302: }, true); 303: const textInput = values.includes("__other__") ? questionStates[questionText]?.textInputValue : undefined; 304: const finalValues = values.filter(_temp4).concat(textInput ? [textInput] : []); 305: onAnswer(questionText, finalValues, undefined, false); 306: }} onFocus={handleFocus} onCancel={onCancel} submitButtonText={currentQuestionIndex === questions.length - 1 ? "Submit" : "Next"} onSubmit={onSubmit} onDownFromLastItem={handleDownFromLastItem} isDisabled={isFooterFocused} onOpenEditor={handleOpenEditor} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage} /> : <Select key={question.question} options={options} defaultValue={questionStates[question.question]?.selectedValue as string | undefined} onChange={value_1 => { 307: onUpdateQuestionState(questionText, { 308: selectedValue: value_1 309: }, false); 310: const textInput_0 = value_1 === "__other__" ? questionStates[questionText]?.textInputValue : undefined; 311: onAnswer(questionText, value_1, textInput_0); 312: }} onFocus={handleFocus} onCancel={onCancel} onDownFromLastItem={handleDownFromLastItem} isDisabled={isFooterFocused} layout="compact-vertical" onOpenEditor={handleOpenEditor} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage} />}</Box>; 313: $[58] = currentQuestionIndex; 314: $[59] = handleFocus; 315: $[60] = handleOpenEditor; 316: $[61] = isFooterFocused; 317: $[62] = onAnswer; 318: $[63] = onCancel; 319: $[64] = onImagePaste; 320: $[65] = onRemoveImage; 321: $[66] = onSubmit; 322: $[67] = onUpdateQuestionState; 323: $[68] = options; 324: $[69] = pastedContents; 325: $[70] = question.multiSelect; 326: $[71] = question.question; 327: $[72] = questionStates; 328: $[73] = questionText; 329: $[74] = questions.length; 330: $[75] = t12; 331: } else { 332: t12 = $[75]; 333: } 334: let t13; 335: if ($[76] === Symbol.for("react.memo_cache_sentinel")) { 336: t13 = <Divider color="inactive" />; 337: $[76] = t13; 338: } else { 339: t13 = $[76]; 340: } 341: let t14; 342: if ($[77] !== footerIndex || $[78] !== isFooterFocused) { 343: t14 = isFooterFocused && footerIndex === 0 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>; 344: $[77] = footerIndex; 345: $[78] = isFooterFocused; 346: $[79] = t14; 347: } else { 348: t14 = $[79]; 349: } 350: const t15 = isFooterFocused && footerIndex === 0 ? "suggestion" : undefined; 351: const t16 = options.length + 1; 352: let t17; 353: if ($[80] !== t15 || $[81] !== t16) { 354: t17 = <Text color={t15}>{t16}. Chat about this</Text>; 355: $[80] = t15; 356: $[81] = t16; 357: $[82] = t17; 358: } else { 359: t17 = $[82]; 360: } 361: let t18; 362: if ($[83] !== t14 || $[84] !== t17) { 363: t18 = <Box flexDirection="row" gap={1}>{t14}{t17}</Box>; 364: $[83] = t14; 365: $[84] = t17; 366: $[85] = t18; 367: } else { 368: t18 = $[85]; 369: } 370: let t19; 371: if ($[86] !== footerIndex || $[87] !== isFooterFocused || $[88] !== isInPlanMode || $[89] !== options.length) { 372: t19 = isInPlanMode && <Box flexDirection="row" gap={1}>{isFooterFocused && footerIndex === 1 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}<Text color={isFooterFocused && footerIndex === 1 ? "suggestion" : undefined}>{options.length + 2}. Skip interview and plan immediately</Text></Box>; 373: $[86] = footerIndex; 374: $[87] = isFooterFocused; 375: $[88] = isInPlanMode; 376: $[89] = options.length; 377: $[90] = t19; 378: } else { 379: t19 = $[90]; 380: } 381: let t20; 382: if ($[91] !== t18 || $[92] !== t19) { 383: t20 = <Box flexDirection="column">{t13}{t18}{t19}</Box>; 384: $[91] = t18; 385: $[92] = t19; 386: $[93] = t20; 387: } else { 388: t20 = $[93]; 389: } 390: let t21; 391: if ($[94] !== questions.length) { 392: t21 = questions.length === 1 ? <>{figures.arrowUp}/{figures.arrowDown} to navigate</> : "Tab/Arrow keys to navigate"; 393: $[94] = questions.length; 394: $[95] = t21; 395: } else { 396: t21 = $[95]; 397: } 398: let t22; 399: if ($[96] !== isOtherFocused) { 400: t22 = isOtherFocused && editorName && <> · ctrl+g to edit in {editorName}</>; 401: $[96] = isOtherFocused; 402: $[97] = t22; 403: } else { 404: t22 = $[97]; 405: } 406: let t23; 407: if ($[98] !== t21 || $[99] !== t22) { 408: t23 = <Box marginTop={1}><Text color="inactive" dimColor={true}>Enter to select ·{" "}{t21}{t22}{" "}· Esc to cancel</Text></Box>; 409: $[98] = t21; 410: $[99] = t22; 411: $[100] = t23; 412: } else { 413: t23 = $[100]; 414: } 415: let t24; 416: if ($[101] !== minContentHeight || $[102] !== t12 || $[103] !== t20 || $[104] !== t23) { 417: t24 = <Box flexDirection="column" minHeight={minContentHeight}>{t12}{t20}{t23}</Box>; 418: $[101] = minContentHeight; 419: $[102] = t12; 420: $[103] = t20; 421: $[104] = t23; 422: $[105] = t24; 423: } else { 424: t24 = $[105]; 425: } 426: let t25; 427: if ($[106] !== t10 || $[107] !== t11 || $[108] !== t24) { 428: t25 = <Box flexDirection="column" paddingTop={0}>{t10}{t11}{t24}</Box>; 429: $[106] = t10; 430: $[107] = t11; 431: $[108] = t24; 432: $[109] = t25; 433: } else { 434: t25 = $[109]; 435: } 436: let t26; 437: if ($[110] !== handleKeyDown || $[111] !== t25 || $[112] !== t8) { 438: t26 = <Box flexDirection="column" marginTop={0} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t8}{t9}{t25}</Box>; 439: $[110] = handleKeyDown; 440: $[111] = t25; 441: $[112] = t8; 442: $[113] = t26; 443: } else { 444: t26 = $[113]; 445: } 446: return t26; 447: } 448: function _temp4(v) { 449: return v !== "__other__"; 450: } 451: function _temp3(opt_0) { 452: return opt_0.preview; 453: } 454: function _temp2(opt) { 455: return { 456: type: "text" as const, 457: value: opt.label, 458: label: opt.label, 459: description: opt.description 460: }; 461: } 462: function _temp(s) { 463: return s.toolPermissionContext.mode; 464: }

File: src/components/permissions/AskUserQuestionPermissionRequest/SubmitQuestionsView.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React from 'react'; 4: import { Box, Text } from '../../../ink.js'; 5: import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'; 6: import type { PermissionDecision } from '../../../utils/permissions/PermissionResult.js'; 7: import { Select } from '../../CustomSelect/index.js'; 8: import { Divider } from '../../design-system/Divider.js'; 9: import { PermissionRequestTitle } from '../PermissionRequestTitle.js'; 10: import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'; 11: import { QuestionNavigationBar } from './QuestionNavigationBar.js'; 12: type Props = { 13: questions: Question[]; 14: currentQuestionIndex: number; 15: answers: Record<string, string>; 16: allQuestionsAnswered: boolean; 17: permissionResult: PermissionDecision; 18: minContentHeight?: number; 19: onFinalResponse: (value: 'submit' | 'cancel') => void; 20: }; 21: export function SubmitQuestionsView(t0) { 22: const $ = _c(27); 23: const { 24: questions, 25: currentQuestionIndex, 26: answers, 27: allQuestionsAnswered, 28: permissionResult, 29: minContentHeight, 30: onFinalResponse 31: } = t0; 32: let t1; 33: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 34: t1 = <Divider color="inactive" />; 35: $[0] = t1; 36: } else { 37: t1 = $[0]; 38: } 39: let t2; 40: if ($[1] !== answers || $[2] !== currentQuestionIndex || $[3] !== questions) { 41: t2 = <QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} />; 42: $[1] = answers; 43: $[2] = currentQuestionIndex; 44: $[3] = questions; 45: $[4] = t2; 46: } else { 47: t2 = $[4]; 48: } 49: let t3; 50: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 51: t3 = <PermissionRequestTitle title="Review your answers" color="text" />; 52: $[5] = t3; 53: } else { 54: t3 = $[5]; 55: } 56: let t4; 57: if ($[6] !== allQuestionsAnswered) { 58: t4 = !allQuestionsAnswered && <Box marginBottom={1}><Text color="warning">{figures.warning} You have not answered all questions</Text></Box>; 59: $[6] = allQuestionsAnswered; 60: $[7] = t4; 61: } else { 62: t4 = $[7]; 63: } 64: let t5; 65: if ($[8] !== answers || $[9] !== questions) { 66: t5 = Object.keys(answers).length > 0 && <Box flexDirection="column" marginBottom={1}>{questions.filter(q => q?.question && answers[q.question]).map(q_0 => { 67: const answer = answers[q_0?.question]; 68: return <Box key={q_0?.question || "answer"} flexDirection="column" marginLeft={1}><Text>{figures.bullet} {q_0?.question || "Question"}</Text><Box marginLeft={2}><Text color="success">{figures.arrowRight} {answer}</Text></Box></Box>; 69: })}</Box>; 70: $[8] = answers; 71: $[9] = questions; 72: $[10] = t5; 73: } else { 74: t5 = $[10]; 75: } 76: let t6; 77: if ($[11] !== permissionResult) { 78: t6 = <PermissionRuleExplanation permissionResult={permissionResult} toolType="tool" />; 79: $[11] = permissionResult; 80: $[12] = t6; 81: } else { 82: t6 = $[12]; 83: } 84: let t7; 85: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 86: t7 = <Text color="inactive">Ready to submit your answers?</Text>; 87: $[13] = t7; 88: } else { 89: t7 = $[13]; 90: } 91: let t8; 92: if ($[14] === Symbol.for("react.memo_cache_sentinel")) { 93: t8 = { 94: type: "text" as const, 95: label: "Submit answers", 96: value: "submit" 97: }; 98: $[14] = t8; 99: } else { 100: t8 = $[14]; 101: } 102: let t9; 103: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 104: t9 = [t8, { 105: type: "text" as const, 106: label: "Cancel", 107: value: "cancel" 108: }]; 109: $[15] = t9; 110: } else { 111: t9 = $[15]; 112: } 113: let t10; 114: if ($[16] !== onFinalResponse) { 115: t10 = <Box marginTop={1}><Select options={t9} onChange={value => onFinalResponse(value as 'submit' | 'cancel')} onCancel={() => onFinalResponse("cancel")} /></Box>; 116: $[16] = onFinalResponse; 117: $[17] = t10; 118: } else { 119: t10 = $[17]; 120: } 121: let t11; 122: if ($[18] !== minContentHeight || $[19] !== t10 || $[20] !== t4 || $[21] !== t5 || $[22] !== t6) { 123: t11 = <Box flexDirection="column" marginTop={1} minHeight={minContentHeight}>{t4}{t5}{t6}{t7}{t10}</Box>; 124: $[18] = minContentHeight; 125: $[19] = t10; 126: $[20] = t4; 127: $[21] = t5; 128: $[22] = t6; 129: $[23] = t11; 130: } else { 131: t11 = $[23]; 132: } 133: let t12; 134: if ($[24] !== t11 || $[25] !== t2) { 135: t12 = <Box flexDirection="column" marginTop={1}>{t1}<Box flexDirection="column" borderTop={true} borderColor="inactive" paddingTop={0}>{t2}{t3}{t11}</Box></Box>; 136: $[24] = t11; 137: $[25] = t2; 138: $[26] = t12; 139: } else { 140: t12 = $[26]; 141: } 142: return t12; 143: }

File: src/components/permissions/AskUserQuestionPermissionRequest/use-multiple-choice-state.ts

typescript 1: import { useCallback, useReducer } from 'react' 2: export type AnswerValue = string 3: export type QuestionState = { 4: selectedValue?: string | string[] 5: textInputValue: string 6: } 7: type State = { 8: currentQuestionIndex: number 9: answers: Record<string, AnswerValue> 10: questionStates: Record<string, QuestionState> 11: isInTextInput: boolean 12: } 13: type Action = 14: | { type: 'next-question' } 15: | { type: 'prev-question' } 16: | { 17: type: 'update-question-state' 18: questionText: string 19: updates: Partial<QuestionState> 20: isMultiSelect: boolean 21: } 22: | { 23: type: 'set-answer' 24: questionText: string 25: answer: string 26: shouldAdvance: boolean 27: } 28: | { type: 'set-text-input-mode'; isInInput: boolean } 29: function reducer(state: State, action: Action): State { 30: switch (action.type) { 31: case 'next-question': 32: return { 33: ...state, 34: currentQuestionIndex: state.currentQuestionIndex + 1, 35: isInTextInput: false, 36: } 37: case 'prev-question': 38: return { 39: ...state, 40: currentQuestionIndex: Math.max(0, state.currentQuestionIndex - 1), 41: isInTextInput: false, 42: } 43: case 'update-question-state': { 44: const existing = state.questionStates[action.questionText] 45: const newState: QuestionState = { 46: selectedValue: 47: action.updates.selectedValue ?? 48: existing?.selectedValue ?? 49: (action.isMultiSelect ? [] : undefined), 50: textInputValue: 51: action.updates.textInputValue ?? existing?.textInputValue ?? '', 52: } 53: return { 54: ...state, 55: questionStates: { 56: ...state.questionStates, 57: [action.questionText]: newState, 58: }, 59: } 60: } 61: case 'set-answer': { 62: const newState = { 63: ...state, 64: answers: { 65: ...state.answers, 66: [action.questionText]: action.answer, 67: }, 68: } 69: if (action.shouldAdvance) { 70: return { 71: ...newState, 72: currentQuestionIndex: newState.currentQuestionIndex + 1, 73: isInTextInput: false, 74: } 75: } 76: return newState 77: } 78: case 'set-text-input-mode': 79: return { 80: ...state, 81: isInTextInput: action.isInInput, 82: } 83: } 84: } 85: const INITIAL_STATE: State = { 86: currentQuestionIndex: 0, 87: answers: {}, 88: questionStates: {}, 89: isInTextInput: false, 90: } 91: export type MultipleChoiceState = { 92: currentQuestionIndex: number 93: answers: Record<string, AnswerValue> 94: questionStates: Record<string, QuestionState> 95: isInTextInput: boolean 96: nextQuestion: () => void 97: prevQuestion: () => void 98: updateQuestionState: ( 99: questionText: string, 100: updates: Partial<QuestionState>, 101: isMultiSelect: boolean, 102: ) => void 103: setAnswer: ( 104: questionText: string, 105: answer: string, 106: shouldAdvance?: boolean, 107: ) => void 108: setTextInputMode: (isInInput: boolean) => void 109: } 110: export function useMultipleChoiceState(): MultipleChoiceState { 111: const [state, dispatch] = useReducer(reducer, INITIAL_STATE) 112: const nextQuestion = useCallback(() => { 113: dispatch({ type: 'next-question' }) 114: }, []) 115: const prevQuestion = useCallback(() => { 116: dispatch({ type: 'prev-question' }) 117: }, []) 118: const updateQuestionState = useCallback( 119: ( 120: questionText: string, 121: updates: Partial<QuestionState>, 122: isMultiSelect: boolean, 123: ) => { 124: dispatch({ 125: type: 'update-question-state', 126: questionText, 127: updates, 128: isMultiSelect, 129: }) 130: }, 131: [], 132: ) 133: const setAnswer = useCallback( 134: (questionText: string, answer: string, shouldAdvance: boolean = true) => { 135: dispatch({ 136: type: 'set-answer', 137: questionText, 138: answer, 139: shouldAdvance, 140: }) 141: }, 142: [], 143: ) 144: const setTextInputMode = useCallback((isInInput: boolean) => { 145: dispatch({ type: 'set-text-input-mode', isInInput }) 146: }, []) 147: return { 148: currentQuestionIndex: state.currentQuestionIndex, 149: answers: state.answers, 150: questionStates: state.questionStates, 151: isInTextInput: state.isInTextInput, 152: nextQuestion, 153: prevQuestion, 154: updateQuestionState, 155: setAnswer, 156: setTextInputMode, 157: } 158: }

File: src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import figures from 'figures'; 4: import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 5: import { Box, Text, useTheme } from '../../../ink.js'; 6: import { useKeybinding } from '../../../keybindings/useKeybinding.js'; 7: import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js'; 8: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js'; 9: import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'; 10: import { useAppState } from '../../../state/AppState.js'; 11: import { BashTool } from '../../../tools/BashTool/BashTool.js'; 12: import { getFirstWordPrefix, getSimpleCommandPrefix } from '../../../tools/BashTool/bashPermissions.js'; 13: import { getDestructiveCommandWarning } from '../../../tools/BashTool/destructiveCommandWarning.js'; 14: import { parseSedEditCommand } from '../../../tools/BashTool/sedEditParser.js'; 15: import { shouldUseSandbox } from '../../../tools/BashTool/shouldUseSandbox.js'; 16: import { getCompoundCommandPrefixesStatic } from '../../../utils/bash/prefix.js'; 17: import { createPromptRuleContent, generateGenericDescription, getBashPromptAllowDescriptions, isClassifierPermissionsEnabled } from '../../../utils/permissions/bashClassifier.js'; 18: import { extractRules } from '../../../utils/permissions/PermissionUpdate.js'; 19: import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'; 20: import { SandboxManager } from '../../../utils/sandbox/sandbox-adapter.js'; 21: import { Select } from '../../CustomSelect/select.js'; 22: import { ShimmerChar } from '../../Spinner/ShimmerChar.js'; 23: import { useShimmerAnimation } from '../../Spinner/useShimmerAnimation.js'; 24: import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'; 25: import { PermissionDecisionDebugInfo } from '../PermissionDecisionDebugInfo.js'; 26: import { PermissionDialog } from '../PermissionDialog.js'; 27: import { PermissionExplainerContent, usePermissionExplainerUI } from '../PermissionExplanation.js'; 28: import type { PermissionRequestProps } from '../PermissionRequest.js'; 29: import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'; 30: import { SedEditPermissionRequest } from '../SedEditPermissionRequest/SedEditPermissionRequest.js'; 31: import { useShellPermissionFeedback } from '../useShellPermissionFeedback.js'; 32: import { logUnaryPermissionEvent } from '../utils.js'; 33: import { bashToolUseOptions } from './bashToolUseOptions.js'; 34: const CHECKING_TEXT = 'Attempting to auto-approve\u2026'; 35: function ClassifierCheckingSubtitle() { 36: const $ = _c(6); 37: const [ref, glimmerIndex] = useShimmerAnimation("requesting", CHECKING_TEXT, false); 38: let t0; 39: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 40: t0 = [...CHECKING_TEXT]; 41: $[0] = t0; 42: } else { 43: t0 = $[0]; 44: } 45: let t1; 46: if ($[1] !== glimmerIndex) { 47: t1 = <Text>{t0.map((char, i) => <ShimmerChar key={i} char={char} index={i} glimmerIndex={glimmerIndex} messageColor="inactive" shimmerColor="subtle" />)}</Text>; 48: $[1] = glimmerIndex; 49: $[2] = t1; 50: } else { 51: t1 = $[2]; 52: } 53: let t2; 54: if ($[3] !== ref || $[4] !== t1) { 55: t2 = <Box ref={ref}>{t1}</Box>; 56: $[3] = ref; 57: $[4] = t1; 58: $[5] = t2; 59: } else { 60: t2 = $[5]; 61: } 62: return t2; 63: } 64: export function BashPermissionRequest(props) { 65: const $ = _c(21); 66: const { 67: toolUseConfirm, 68: toolUseContext, 69: onDone, 70: onReject, 71: verbose, 72: workerBadge 73: } = props; 74: let command; 75: let description; 76: let t0; 77: if ($[0] !== toolUseConfirm.input) { 78: ({ 79: command, 80: description 81: } = BashTool.inputSchema.parse(toolUseConfirm.input)); 82: t0 = parseSedEditCommand(command); 83: $[0] = toolUseConfirm.input; 84: $[1] = command; 85: $[2] = description; 86: $[3] = t0; 87: } else { 88: command = $[1]; 89: description = $[2]; 90: t0 = $[3]; 91: } 92: const sedInfo = t0; 93: if (sedInfo) { 94: let t1; 95: if ($[4] !== onDone || $[5] !== onReject || $[6] !== sedInfo || $[7] !== toolUseConfirm || $[8] !== toolUseContext || $[9] !== verbose || $[10] !== workerBadge) { 96: t1 = <SedEditPermissionRequest toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} sedInfo={sedInfo} />; 97: $[4] = onDone; 98: $[5] = onReject; 99: $[6] = sedInfo; 100: $[7] = toolUseConfirm; 101: $[8] = toolUseContext; 102: $[9] = verbose; 103: $[10] = workerBadge; 104: $[11] = t1; 105: } else { 106: t1 = $[11]; 107: } 108: return t1; 109: } 110: let t1; 111: if ($[12] !== command || $[13] !== description || $[14] !== onDone || $[15] !== onReject || $[16] !== toolUseConfirm || $[17] !== toolUseContext || $[18] !== verbose || $[19] !== workerBadge) { 112: t1 = <BashPermissionRequestInner toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} command={command} description={description} />; 113: $[12] = command; 114: $[13] = description; 115: $[14] = onDone; 116: $[15] = onReject; 117: $[16] = toolUseConfirm; 118: $[17] = toolUseContext; 119: $[18] = verbose; 120: $[19] = workerBadge; 121: $[20] = t1; 122: } else { 123: t1 = $[20]; 124: } 125: return t1; 126: } 127: function BashPermissionRequestInner({ 128: toolUseConfirm, 129: toolUseContext, 130: onDone, 131: onReject, 132: verbose: _verbose, 133: workerBadge, 134: command, 135: description 136: }: PermissionRequestProps & { 137: command: string; 138: description?: string; 139: }): React.ReactNode { 140: const [theme] = useTheme(); 141: const toolPermissionContext = useAppState(s => s.toolPermissionContext); 142: const explainerState = usePermissionExplainerUI({ 143: toolName: toolUseConfirm.tool.name, 144: toolInput: toolUseConfirm.input, 145: toolDescription: toolUseConfirm.description, 146: messages: toolUseContext.messages 147: }); 148: const { 149: yesInputMode, 150: noInputMode, 151: yesFeedbackModeEntered, 152: noFeedbackModeEntered, 153: acceptFeedback, 154: rejectFeedback, 155: setAcceptFeedback, 156: setRejectFeedback, 157: focusedOption, 158: handleInputModeToggle, 159: handleReject, 160: handleFocus 161: } = useShellPermissionFeedback({ 162: toolUseConfirm, 163: onDone, 164: onReject, 165: explainerVisible: explainerState.visible 166: }); 167: const [showPermissionDebug, setShowPermissionDebug] = useState(false); 168: const [classifierDescription, setClassifierDescription] = useState(description || ''); 169: // Track whether the initial description (from prop or async generation) was empty. 170: // Once we receive a non-empty description, this stays false. 171: const [initialClassifierDescriptionEmpty, setInitialClassifierDescriptionEmpty] = useState(!description?.trim()); 172: // Asynchronously generate a generic description for the classifier 173: useEffect(() => { 174: if (!isClassifierPermissionsEnabled()) return; 175: const abortController = new AbortController(); 176: generateGenericDescription(command, description, abortController.signal).then(generic => { 177: if (generic && !abortController.signal.aborted) { 178: setClassifierDescription(generic); 179: setInitialClassifierDescriptionEmpty(false); 180: } 181: }).catch(() => {}); // Keep original on error 182: return () => abortController.abort(); 183: }, [command, description]); 184: // GH#11380: For compound commands (cd src && git status && npm test), the 185: // backend already computed correct per-subcommand suggestions via tree-sitter 186: // split + per-subcommand permission checks. decisionReason.type === 187: // 'subcommandResults' marks this path. The sync prefix heuristics below 188: const isCompound = toolUseConfirm.permissionResult.decisionReason?.type === 'subcommandResults'; 189: const [editablePrefix, setEditablePrefix] = useState<string | undefined>(() => { 190: if (isCompound) { 191: const backendBashRules = extractRules('suggestions' in toolUseConfirm.permissionResult ? toolUseConfirm.permissionResult.suggestions : undefined).filter(r => r.toolName === BashTool.name && r.ruleContent); 192: return backendBashRules.length === 1 ? backendBashRules[0]!.ruleContent : undefined; 193: } 194: const two = getSimpleCommandPrefix(command); 195: if (two) return `${two}:*`; 196: const one = getFirstWordPrefix(command); 197: if (one) return `${one}:*`; 198: return command; 199: }); 200: const hasUserEditedPrefix = useRef(false); 201: const onEditablePrefixChange = useCallback((value: string) => { 202: hasUserEditedPrefix.current = true; 203: setEditablePrefix(value); 204: }, []); 205: useEffect(() => { 206: if (isCompound) return; 207: let cancelled = false; 208: getCompoundCommandPrefixesStatic(command, subcmd => BashTool.isReadOnly({ 209: command: subcmd 210: })).then(prefixes => { 211: if (cancelled || hasUserEditedPrefix.current) return; 212: if (prefixes.length > 0) { 213: setEditablePrefix(`${prefixes[0]}:*`); 214: } 215: }).catch(() => {}); 216: return () => { 217: cancelled = true; 218: }; 219: }, [command, isCompound]); 220: const [classifierWasChecking] = useState(feature('BASH_CLASSIFIER') ? !!toolUseConfirm.classifierCheckInProgress : false); 221: const { 222: destructiveWarning: destructiveWarning_0, 223: sandboxingEnabled: sandboxingEnabled_0, 224: isSandboxed: isSandboxed_0 225: } = useMemo(() => { 226: const destructiveWarning = getFeatureValue_CACHED_MAY_BE_STALE('tengu_destructive_command_warning', false) ? getDestructiveCommandWarning(command) : null; 227: const sandboxingEnabled = SandboxManager.isSandboxingEnabled(); 228: const isSandboxed = sandboxingEnabled && shouldUseSandbox(toolUseConfirm.input); 229: return { 230: destructiveWarning, 231: sandboxingEnabled, 232: isSandboxed 233: }; 234: }, [command, toolUseConfirm.input]); 235: const unaryEvent = useMemo<UnaryEvent>(() => ({ 236: completion_type: 'tool_use_single', 237: language_name: 'none' 238: }), []); 239: usePermissionRequestLogging(toolUseConfirm, unaryEvent); 240: const existingAllowDescriptions = useMemo(() => getBashPromptAllowDescriptions(toolPermissionContext), [toolPermissionContext]); 241: const options = useMemo(() => bashToolUseOptions({ 242: suggestions: toolUseConfirm.permissionResult.behavior === 'ask' ? toolUseConfirm.permissionResult.suggestions : undefined, 243: decisionReason: toolUseConfirm.permissionResult.decisionReason, 244: onRejectFeedbackChange: setRejectFeedback, 245: onAcceptFeedbackChange: setAcceptFeedback, 246: onClassifierDescriptionChange: setClassifierDescription, 247: classifierDescription, 248: initialClassifierDescriptionEmpty, 249: existingAllowDescriptions, 250: yesInputMode, 251: noInputMode, 252: editablePrefix, 253: onEditablePrefixChange 254: }), [toolUseConfirm, classifierDescription, initialClassifierDescriptionEmpty, existingAllowDescriptions, yesInputMode, noInputMode, editablePrefix, onEditablePrefixChange]); 255: const handleToggleDebug = useCallback(() => { 256: setShowPermissionDebug(prev => !prev); 257: }, []); 258: useKeybinding('permission:toggleDebug', handleToggleDebug, { 259: context: 'Confirmation' 260: }); 261: const handleDismissCheckmark = useCallback(() => { 262: toolUseConfirm.onDismissCheckmark?.(); 263: }, [toolUseConfirm]); 264: useKeybinding('confirm:no', handleDismissCheckmark, { 265: context: 'Confirmation', 266: isActive: feature('BASH_CLASSIFIER') ? !!toolUseConfirm.classifierAutoApproved : false 267: }); 268: function onSelect(value_0: string) { 269: let optionIndex: Record<string, number> = { 270: yes: 1, 271: 'yes-apply-suggestions': 2, 272: 'yes-prefix-edited': 2, 273: no: 3 274: }; 275: if (feature('BASH_CLASSIFIER')) { 276: optionIndex = { 277: yes: 1, 278: 'yes-apply-suggestions': 2, 279: 'yes-prefix-edited': 2, 280: 'yes-classifier-reviewed': 3, 281: no: 4 282: }; 283: } 284: logEvent('tengu_permission_request_option_selected', { 285: option_index: optionIndex[value_0], 286: explainer_visible: explainerState.visible 287: }); 288: const toolNameForAnalytics = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS; 289: if (value_0 === 'yes-prefix-edited') { 290: const trimmedPrefix = (editablePrefix ?? '').trim(); 291: logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept'); 292: if (!trimmedPrefix) { 293: toolUseConfirm.onAllow(toolUseConfirm.input, []); 294: } else { 295: const prefixUpdates: PermissionUpdate[] = [{ 296: type: 'addRules', 297: rules: [{ 298: toolName: BashTool.name, 299: ruleContent: trimmedPrefix 300: }], 301: behavior: 'allow', 302: destination: 'localSettings' 303: }]; 304: toolUseConfirm.onAllow(toolUseConfirm.input, prefixUpdates); 305: } 306: onDone(); 307: return; 308: } 309: if (feature('BASH_CLASSIFIER') && value_0 === 'yes-classifier-reviewed') { 310: const trimmedDescription = classifierDescription.trim(); 311: logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept'); 312: if (!trimmedDescription) { 313: toolUseConfirm.onAllow(toolUseConfirm.input, []); 314: } else { 315: const permissionUpdates: PermissionUpdate[] = [{ 316: type: 'addRules', 317: rules: [{ 318: toolName: BashTool.name, 319: ruleContent: createPromptRuleContent(trimmedDescription) 320: }], 321: behavior: 'allow', 322: destination: 'session' 323: }]; 324: toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates); 325: } 326: onDone(); 327: return; 328: } 329: switch (value_0) { 330: case 'yes': 331: { 332: const trimmedFeedback_0 = acceptFeedback.trim(); 333: logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept'); 334: logEvent('tengu_accept_submitted', { 335: toolName: toolNameForAnalytics, 336: isMcp: toolUseConfirm.tool.isMcp ?? false, 337: has_instructions: !!trimmedFeedback_0, 338: instructions_length: trimmedFeedback_0.length, 339: entered_feedback_mode: yesFeedbackModeEntered 340: }); 341: toolUseConfirm.onAllow(toolUseConfirm.input, [], trimmedFeedback_0 || undefined); 342: onDone(); 343: break; 344: } 345: case 'yes-apply-suggestions': 346: { 347: logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept'); 348: const permissionUpdates_0 = 'suggestions' in toolUseConfirm.permissionResult ? toolUseConfirm.permissionResult.suggestions || [] : []; 349: toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates_0); 350: onDone(); 351: break; 352: } 353: case 'no': 354: { 355: const trimmedFeedback = rejectFeedback.trim(); 356: logEvent('tengu_reject_submitted', { 357: toolName: toolNameForAnalytics, 358: isMcp: toolUseConfirm.tool.isMcp ?? false, 359: has_instructions: !!trimmedFeedback, 360: instructions_length: trimmedFeedback.length, 361: entered_feedback_mode: noFeedbackModeEntered 362: }); 363: handleReject(trimmedFeedback || undefined); 364: break; 365: } 366: } 367: } 368: const classifierSubtitle = feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved ? <Text> 369: <Text color="success">{figures.tick} Auto-approved</Text> 370: {toolUseConfirm.classifierMatchedRule && <Text dimColor> 371: {' \u00b7 matched "'} 372: {toolUseConfirm.classifierMatchedRule} 373: {'"'} 374: </Text>} 375: </Text> : toolUseConfirm.classifierCheckInProgress ? <ClassifierCheckingSubtitle /> : classifierWasChecking ? <Text dimColor>Requires manual approval</Text> : undefined : undefined; 376: return <PermissionDialog workerBadge={workerBadge} title={sandboxingEnabled_0 && !isSandboxed_0 ? 'Bash command (unsandboxed)' : 'Bash command'} subtitle={classifierSubtitle}> 377: <Box flexDirection="column" paddingX={2} paddingY={1}> 378: <Text dimColor={explainerState.visible}> 379: {BashTool.renderToolUseMessage({ 380: command, 381: description 382: }, { 383: theme, 384: verbose: true 385: } 386: )} 387: </Text> 388: {!explainerState.visible && <Text dimColor>{toolUseConfirm.description}</Text>} 389: <PermissionExplainerContent visible={explainerState.visible} promise={explainerState.promise} /> 390: </Box> 391: {showPermissionDebug ? <> 392: <PermissionDecisionDebugInfo permissionResult={toolUseConfirm.permissionResult} toolName="Bash" /> 393: {toolUseContext.options.debug && <Box justifyContent="flex-end" marginTop={1}> 394: <Text dimColor>Ctrl-D to hide debug info</Text> 395: </Box>} 396: </> : <> 397: <Box flexDirection="column"> 398: <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="command" /> 399: {destructiveWarning_0 && <Box marginBottom={1}> 400: <Text color="warning" dimColor={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved : false}> 401: {destructiveWarning_0} 402: </Text> 403: </Box>} 404: <Text dimColor={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved : false}> 405: Do you want to proceed? 406: </Text> 407: <Select options={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved ? options.map(o => ({ 408: ...o, 409: disabled: true 410: })) : options : options} isDisabled={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved : false} inlineDescriptions onChange={onSelect} onCancel={() => handleReject()} onFocus={handleFocus} onInputModeToggle={handleInputModeToggle} /> 411: </Box> 412: <Box justifyContent="space-between" marginTop={1}> 413: <Text dimColor> 414: Esc to cancel 415: {(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'} 416: {explainerState.enabled && ` · ctrl+e to ${explainerState.visible ? 'hide' : 'explain'}`} 417: </Text> 418: {toolUseContext.options.debug && <Text dimColor>Ctrl+d to show debug info</Text>} 419: </Box> 420: </>} 421: </PermissionDialog>; 422: }

File: src/components/permissions/BashPermissionRequest/bashToolUseOptions.tsx

typescript 1: import { BASH_TOOL_NAME } from '../../../tools/BashTool/toolName.js'; 2: import { extractOutputRedirections } from '../../../utils/bash/commands.js'; 3: import { isClassifierPermissionsEnabled } from '../../../utils/permissions/bashClassifier.js'; 4: import type { PermissionDecisionReason } from '../../../utils/permissions/PermissionResult.js'; 5: import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'; 6: import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'; 7: import type { OptionWithDescription } from '../../CustomSelect/select.js'; 8: import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js'; 9: export type BashToolUseOption = 'yes' | 'yes-apply-suggestions' | 'yes-prefix-edited' | 'yes-classifier-reviewed' | 'no'; 10: function descriptionAlreadyExists(description: string, existingDescriptions: string[]): boolean { 11: const normalized = description.toLowerCase().trimEnd(); 12: return existingDescriptions.some(existing => existing.toLowerCase().trimEnd() === normalized); 13: } 14: function stripBashRedirections(command: string): string { 15: const { 16: commandWithoutRedirections, 17: redirections 18: } = extractOutputRedirections(command); 19: return redirections.length > 0 ? commandWithoutRedirections : command; 20: } 21: export function bashToolUseOptions({ 22: suggestions = [], 23: decisionReason, 24: onRejectFeedbackChange, 25: onAcceptFeedbackChange, 26: onClassifierDescriptionChange, 27: classifierDescription, 28: initialClassifierDescriptionEmpty = false, 29: existingAllowDescriptions = [], 30: yesInputMode = false, 31: noInputMode = false, 32: editablePrefix, 33: onEditablePrefixChange 34: }: { 35: suggestions?: PermissionUpdate[]; 36: decisionReason?: PermissionDecisionReason; 37: onRejectFeedbackChange: (value: string) => void; 38: onAcceptFeedbackChange: (value: string) => void; 39: onClassifierDescriptionChange?: (value: string) => void; 40: classifierDescription?: string; 41: initialClassifierDescriptionEmpty?: boolean; 42: existingAllowDescriptions?: string[]; 43: yesInputMode?: boolean; 44: noInputMode?: boolean; 45: editablePrefix?: string; 46: onEditablePrefixChange?: (value: string) => void; 47: }): OptionWithDescription<BashToolUseOption>[] { 48: const options: OptionWithDescription<BashToolUseOption>[] = []; 49: if (yesInputMode) { 50: options.push({ 51: type: 'input', 52: label: 'Yes', 53: value: 'yes', 54: placeholder: 'and tell Claude what to do next', 55: onChange: onAcceptFeedbackChange, 56: allowEmptySubmitToCancel: true 57: }); 58: } else { 59: options.push({ 60: label: 'Yes', 61: value: 'yes' 62: }); 63: } 64: if (shouldShowAlwaysAllowOptions()) { 65: const hasNonBashSuggestions = suggestions.some(s => s.type === 'addDirectories' || s.type === 'addRules' && s.rules?.some(r => r.toolName !== BASH_TOOL_NAME)); 66: if (editablePrefix !== undefined && onEditablePrefixChange && !hasNonBashSuggestions && suggestions.length > 0) { 67: options.push({ 68: type: 'input', 69: label: 'Yes, and don\u2019t ask again for', 70: value: 'yes-prefix-edited', 71: placeholder: 'command prefix (e.g., npm run:*)', 72: initialValue: editablePrefix, 73: onChange: onEditablePrefixChange, 74: allowEmptySubmitToCancel: true, 75: showLabelWithValue: true, 76: labelValueSeparator: ': ', 77: resetCursorOnUpdate: true 78: }); 79: } else if (suggestions.length > 0) { 80: const label = generateShellSuggestionsLabel(suggestions, BASH_TOOL_NAME, stripBashRedirections); 81: if (label) { 82: options.push({ 83: label, 84: value: 'yes-apply-suggestions' 85: }); 86: } 87: } 88: const editablePrefixShown = options.some(o => o.value === 'yes-prefix-edited'); 89: if ("external" === 'ant' && !editablePrefixShown && isClassifierPermissionsEnabled() && onClassifierDescriptionChange && !initialClassifierDescriptionEmpty && !descriptionAlreadyExists(classifierDescription ?? '', existingAllowDescriptions) && decisionReason?.type !== 'classifier') { 90: options.push({ 91: type: 'input', 92: label: 'Yes, and don\u2019t ask again for', 93: value: 'yes-classifier-reviewed', 94: placeholder: 'describe what to allow...', 95: initialValue: classifierDescription ?? '', 96: onChange: onClassifierDescriptionChange, 97: allowEmptySubmitToCancel: true, 98: showLabelWithValue: true, 99: labelValueSeparator: ': ', 100: resetCursorOnUpdate: true 101: }); 102: } 103: } 104: if (noInputMode) { 105: options.push({ 106: type: 'input', 107: label: 'No', 108: value: 'no', 109: placeholder: 'and tell Claude what to do differently', 110: onChange: onRejectFeedbackChange, 111: allowEmptySubmitToCancel: true 112: }); 113: } else { 114: options.push({ 115: label: 'No', 116: value: 'no' 117: }); 118: } 119: return options; 120: }

File: src/components/permissions/ComputerUseApproval/ComputerUseApproval.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { getSentinelCategory } from '@ant/computer-use-mcp/sentinelApps'; 3: import type { CuPermissionRequest, CuPermissionResponse } from '@ant/computer-use-mcp/types'; 4: import { DEFAULT_GRANT_FLAGS } from '@ant/computer-use-mcp/types'; 5: import figures from 'figures'; 6: import * as React from 'react'; 7: import { useMemo, useState } from 'react'; 8: import { Box, Text } from '../../../ink.js'; 9: import { execFileNoThrow } from '../../../utils/execFileNoThrow.js'; 10: import { plural } from '../../../utils/stringUtils.js'; 11: import type { OptionWithDescription } from '../../CustomSelect/select.js'; 12: import { Select } from '../../CustomSelect/select.js'; 13: import { Dialog } from '../../design-system/Dialog.js'; 14: type ComputerUseApprovalProps = { 15: request: CuPermissionRequest; 16: onDone: (response: CuPermissionResponse) => void; 17: }; 18: const DENY_ALL_RESPONSE: CuPermissionResponse = { 19: granted: [], 20: denied: [], 21: flags: DEFAULT_GRANT_FLAGS 22: }; 23: export function ComputerUseApproval(t0) { 24: const $ = _c(3); 25: const { 26: request, 27: onDone 28: } = t0; 29: let t1; 30: if ($[0] !== onDone || $[1] !== request) { 31: t1 = request.tccState ? <ComputerUseTccPanel tccState={request.tccState} onDone={() => onDone(DENY_ALL_RESPONSE)} /> : <ComputerUseAppListPanel request={request} onDone={onDone} />; 32: $[0] = onDone; 33: $[1] = request; 34: $[2] = t1; 35: } else { 36: t1 = $[2]; 37: } 38: return t1; 39: } 40: type TccOption = 'open_accessibility' | 'open_screen_recording' | 'retry'; 41: function ComputerUseTccPanel(t0) { 42: const $ = _c(26); 43: const { 44: tccState, 45: onDone 46: } = t0; 47: let opts; 48: if ($[0] !== tccState.accessibility || $[1] !== tccState.screenRecording) { 49: opts = []; 50: if (!tccState.accessibility) { 51: let t1; 52: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 53: t1 = { 54: label: "Open System Settings \u2192 Accessibility", 55: value: "open_accessibility" 56: }; 57: $[3] = t1; 58: } else { 59: t1 = $[3]; 60: } 61: opts.push(t1); 62: } 63: if (!tccState.screenRecording) { 64: let t1; 65: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 66: t1 = { 67: label: "Open System Settings \u2192 Screen Recording", 68: value: "open_screen_recording" 69: }; 70: $[4] = t1; 71: } else { 72: t1 = $[4]; 73: } 74: opts.push(t1); 75: } 76: let t1; 77: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 78: t1 = { 79: label: "Try again", 80: value: "retry" 81: }; 82: $[5] = t1; 83: } else { 84: t1 = $[5]; 85: } 86: opts.push(t1); 87: $[0] = tccState.accessibility; 88: $[1] = tccState.screenRecording; 89: $[2] = opts; 90: } else { 91: opts = $[2]; 92: } 93: const options = opts; 94: let t1; 95: if ($[6] !== onDone) { 96: t1 = function onChange(value) { 97: switch (value) { 98: case "open_accessibility": 99: { 100: execFileNoThrow("open", ["x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"], { 101: useCwd: false 102: }); 103: return; 104: } 105: case "open_screen_recording": 106: { 107: execFileNoThrow("open", ["x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"], { 108: useCwd: false 109: }); 110: return; 111: } 112: case "retry": 113: { 114: onDone(); 115: return; 116: } 117: } 118: }; 119: $[6] = onDone; 120: $[7] = t1; 121: } else { 122: t1 = $[7]; 123: } 124: const onChange = t1; 125: const t2 = tccState.accessibility ? `${figures.tick} granted` : `${figures.cross} not granted`; 126: let t3; 127: if ($[8] !== t2) { 128: t3 = <Text>Accessibility:{" "}{t2}</Text>; 129: $[8] = t2; 130: $[9] = t3; 131: } else { 132: t3 = $[9]; 133: } 134: const t4 = tccState.screenRecording ? `${figures.tick} granted` : `${figures.cross} not granted`; 135: let t5; 136: if ($[10] !== t4) { 137: t5 = <Text>Screen Recording:{" "}{t4}</Text>; 138: $[10] = t4; 139: $[11] = t5; 140: } else { 141: t5 = $[11]; 142: } 143: let t6; 144: if ($[12] !== t3 || $[13] !== t5) { 145: t6 = <Box flexDirection="column">{t3}{t5}</Box>; 146: $[12] = t3; 147: $[13] = t5; 148: $[14] = t6; 149: } else { 150: t6 = $[14]; 151: } 152: let t7; 153: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 154: t7 = <Text dimColor={true}>Grant the missing permissions in System Settings, then select "Try again". macOS may require you to restart Claude Code after granting Screen Recording.</Text>; 155: $[15] = t7; 156: } else { 157: t7 = $[15]; 158: } 159: let t8; 160: if ($[16] !== onChange || $[17] !== onDone || $[18] !== options) { 161: t8 = <Select options={options} onChange={onChange} onCancel={onDone} />; 162: $[16] = onChange; 163: $[17] = onDone; 164: $[18] = options; 165: $[19] = t8; 166: } else { 167: t8 = $[19]; 168: } 169: let t9; 170: if ($[20] !== t6 || $[21] !== t8) { 171: t9 = <Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>{t6}{t7}{t8}</Box>; 172: $[20] = t6; 173: $[21] = t8; 174: $[22] = t9; 175: } else { 176: t9 = $[22]; 177: } 178: let t10; 179: if ($[23] !== onDone || $[24] !== t9) { 180: t10 = <Dialog title="Computer Use needs macOS permissions" onCancel={onDone}>{t9}</Dialog>; 181: $[23] = onDone; 182: $[24] = t9; 183: $[25] = t10; 184: } else { 185: t10 = $[25]; 186: } 187: return t10; 188: } 189: type AppListOption = 'allow_all' | 'deny'; 190: const SENTINEL_WARNING: Record<NonNullable<ReturnType<typeof getSentinelCategory>>, string> = { 191: shell: 'equivalent to shell access', 192: filesystem: 'can read/write any file', 193: system_settings: 'can change system settings' 194: }; 195: function ComputerUseAppListPanel(t0) { 196: const $ = _c(48); 197: const { 198: request, 199: onDone 200: } = t0; 201: let t1; 202: if ($[0] !== request.apps) { 203: t1 = () => new Set(request.apps.flatMap(_temp)); 204: $[0] = request.apps; 205: $[1] = t1; 206: } else { 207: t1 = $[1]; 208: } 209: const [checked] = useState(t1); 210: let t2; 211: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 212: t2 = ["clipboardRead", "clipboardWrite", "systemKeyCombos"]; 213: $[2] = t2; 214: } else { 215: t2 = $[2]; 216: } 217: const ALL_FLAG_KEYS = t2; 218: let t3; 219: if ($[3] !== request.requestedFlags) { 220: t3 = ALL_FLAG_KEYS.filter(k => request.requestedFlags[k]); 221: $[3] = request.requestedFlags; 222: $[4] = t3; 223: } else { 224: t3 = $[4]; 225: } 226: const requestedFlagKeys = t3; 227: const t4 = checked.size; 228: let t5; 229: if ($[5] !== checked.size) { 230: t5 = plural(checked.size, "app"); 231: $[5] = checked.size; 232: $[6] = t5; 233: } else { 234: t5 = $[6]; 235: } 236: const t6 = `Allow for this session (${t4} ${t5})`; 237: let t7; 238: if ($[7] !== t6) { 239: t7 = { 240: label: t6, 241: value: "allow_all" 242: }; 243: $[7] = t6; 244: $[8] = t7; 245: } else { 246: t7 = $[8]; 247: } 248: let t8; 249: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 250: t8 = { 251: label: <Text>Deny, and tell Claude what to do differently <Text bold={true}>(esc)</Text></Text>, 252: value: "deny" 253: }; 254: $[9] = t8; 255: } else { 256: t8 = $[9]; 257: } 258: let t9; 259: if ($[10] !== t7) { 260: t9 = [t7, t8]; 261: $[10] = t7; 262: $[11] = t9; 263: } else { 264: t9 = $[11]; 265: } 266: const options = t9; 267: let t10; 268: if ($[12] !== checked || $[13] !== onDone || $[14] !== request.apps || $[15] !== requestedFlagKeys) { 269: t10 = function respond(allow) { 270: if (!allow) { 271: onDone(DENY_ALL_RESPONSE); 272: return; 273: } 274: const now = Date.now(); 275: const granted = request.apps.flatMap(a_0 => a_0.resolved && checked.has(a_0.resolved.bundleId) ? [{ 276: bundleId: a_0.resolved.bundleId, 277: displayName: a_0.resolved.displayName, 278: grantedAt: now 279: }] : []); 280: const denied = request.apps.filter(a_1 => !a_1.resolved || !checked.has(a_1.resolved.bundleId)).map(_temp2); 281: const flags = { 282: ...DEFAULT_GRANT_FLAGS, 283: ...Object.fromEntries(requestedFlagKeys.map(_temp3)) 284: }; 285: onDone({ 286: granted, 287: denied, 288: flags 289: }); 290: }; 291: $[12] = checked; 292: $[13] = onDone; 293: $[14] = request.apps; 294: $[15] = requestedFlagKeys; 295: $[16] = t10; 296: } else { 297: t10 = $[16]; 298: } 299: const respond = t10; 300: let t11; 301: if ($[17] !== respond) { 302: t11 = () => respond(false); 303: $[17] = respond; 304: $[18] = t11; 305: } else { 306: t11 = $[18]; 307: } 308: let t12; 309: if ($[19] !== request.reason) { 310: t12 = request.reason ? <Text dimColor={true}>{request.reason}</Text> : null; 311: $[19] = request.reason; 312: $[20] = t12; 313: } else { 314: t12 = $[20]; 315: } 316: let t13; 317: if ($[21] !== checked || $[22] !== request.apps) { 318: let t14; 319: if ($[24] !== checked) { 320: t14 = a_3 => { 321: const resolved = a_3.resolved; 322: if (!resolved) { 323: return <Text key={a_3.requestedName} dimColor={true}>{" "}{figures.circle} {a_3.requestedName}{" "}<Text dimColor={true}>(not installed)</Text></Text>; 324: } 325: if (a_3.alreadyGranted) { 326: return <Text key={resolved.bundleId} dimColor={true}>{" "}{figures.tick} {resolved.displayName}{" "}<Text dimColor={true}>(already granted)</Text></Text>; 327: } 328: const sentinel = getSentinelCategory(resolved.bundleId); 329: const isChecked = checked.has(resolved.bundleId); 330: return <Box key={resolved.bundleId} flexDirection="column"><Text>{" "}{isChecked ? figures.circleFilled : figures.circle}{" "}{resolved.displayName}</Text>{sentinel ? <Text bold={true}>{" "}{figures.warning} {SENTINEL_WARNING[sentinel]}</Text> : null}</Box>; 331: }; 332: $[24] = checked; 333: $[25] = t14; 334: } else { 335: t14 = $[25]; 336: } 337: t13 = request.apps.map(t14); 338: $[21] = checked; 339: $[22] = request.apps; 340: $[23] = t13; 341: } else { 342: t13 = $[23]; 343: } 344: let t14; 345: if ($[26] !== t13) { 346: t14 = <Box flexDirection="column">{t13}</Box>; 347: $[26] = t13; 348: $[27] = t14; 349: } else { 350: t14 = $[27]; 351: } 352: let t15; 353: if ($[28] !== requestedFlagKeys) { 354: t15 = requestedFlagKeys.length > 0 ? <Box flexDirection="column"><Text dimColor={true}>Also requested:</Text>{requestedFlagKeys.map(_temp4)}</Box> : null; 355: $[28] = requestedFlagKeys; 356: $[29] = t15; 357: } else { 358: t15 = $[29]; 359: } 360: let t16; 361: if ($[30] !== request.willHide) { 362: t16 = request.willHide && request.willHide.length > 0 ? <Text dimColor={true}>{request.willHide.length} other{" "}{plural(request.willHide.length, "app")} will be hidden while Claude works.</Text> : null; 363: $[30] = request.willHide; 364: $[31] = t16; 365: } else { 366: t16 = $[31]; 367: } 368: let t17; 369: let t18; 370: if ($[32] !== respond) { 371: t17 = v => respond(v === "allow_all"); 372: t18 = () => respond(false); 373: $[32] = respond; 374: $[33] = t17; 375: $[34] = t18; 376: } else { 377: t17 = $[33]; 378: t18 = $[34]; 379: } 380: let t19; 381: if ($[35] !== options || $[36] !== t17 || $[37] !== t18) { 382: t19 = <Select options={options} onChange={t17} onCancel={t18} />; 383: $[35] = options; 384: $[36] = t17; 385: $[37] = t18; 386: $[38] = t19; 387: } else { 388: t19 = $[38]; 389: } 390: let t20; 391: if ($[39] !== t12 || $[40] !== t14 || $[41] !== t15 || $[42] !== t16 || $[43] !== t19) { 392: t20 = <Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>{t12}{t14}{t15}{t16}{t19}</Box>; 393: $[39] = t12; 394: $[40] = t14; 395: $[41] = t15; 396: $[42] = t16; 397: $[43] = t19; 398: $[44] = t20; 399: } else { 400: t20 = $[44]; 401: } 402: let t21; 403: if ($[45] !== t11 || $[46] !== t20) { 404: t21 = <Dialog title="Computer Use wants to control these apps" onCancel={t11}>{t20}</Dialog>; 405: $[45] = t11; 406: $[46] = t20; 407: $[47] = t21; 408: } else { 409: t21 = $[47]; 410: } 411: return t21; 412: } 413: function _temp4(flag) { 414: return <Text key={flag} dimColor={true}>{" "}· {flag}</Text>; 415: } 416: function _temp3(k_0) { 417: return [k_0, true] as const; 418: } 419: function _temp2(a_2) { 420: return { 421: bundleId: a_2.resolved?.bundleId ?? a_2.requestedName, 422: reason: a_2.resolved ? "user_denied" as const : "not_installed" as const 423: }; 424: } 425: function _temp(a) { 426: return a.resolved && !a.alreadyGranted ? [a.resolved.bundleId] : []; 427: }

File: src/components/permissions/EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { handlePlanModeTransition } from '../../../bootstrap/state.js'; 4: import { Box, Text } from '../../../ink.js'; 5: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js'; 6: import { useAppState } from '../../../state/AppState.js'; 7: import { isPlanModeInterviewPhaseEnabled } from '../../../utils/planModeV2.js'; 8: import { Select } from '../../CustomSelect/index.js'; 9: import { PermissionDialog } from '../PermissionDialog.js'; 10: import type { PermissionRequestProps } from '../PermissionRequest.js'; 11: export function EnterPlanModePermissionRequest(t0) { 12: const $ = _c(18); 13: const { 14: toolUseConfirm, 15: onDone, 16: onReject, 17: workerBadge 18: } = t0; 19: const toolPermissionContextMode = useAppState(_temp); 20: let t1; 21: if ($[0] !== onDone || $[1] !== onReject || $[2] !== toolPermissionContextMode || $[3] !== toolUseConfirm) { 22: t1 = function handleResponse(value) { 23: if (value === "yes") { 24: logEvent("tengu_plan_enter", { 25: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 26: entryMethod: "tool" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 27: }); 28: handlePlanModeTransition(toolPermissionContextMode, "plan"); 29: onDone(); 30: toolUseConfirm.onAllow({}, [{ 31: type: "setMode", 32: mode: "plan", 33: destination: "session" 34: }]); 35: } else { 36: onDone(); 37: onReject(); 38: toolUseConfirm.onReject(); 39: } 40: }; 41: $[0] = onDone; 42: $[1] = onReject; 43: $[2] = toolPermissionContextMode; 44: $[3] = toolUseConfirm; 45: $[4] = t1; 46: } else { 47: t1 = $[4]; 48: } 49: const handleResponse = t1; 50: let t2; 51: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 52: t2 = <Text>Claude wants to enter plan mode to explore and design an implementation approach.</Text>; 53: $[5] = t2; 54: } else { 55: t2 = $[5]; 56: } 57: let t3; 58: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 59: t3 = <Box marginTop={1} flexDirection="column"><Text dimColor={true}>In plan mode, Claude will:</Text><Text dimColor={true}> · Explore the codebase thoroughly</Text><Text dimColor={true}> · Identify existing patterns</Text><Text dimColor={true}> · Design an implementation strategy</Text><Text dimColor={true}> · Present a plan for your approval</Text></Box>; 60: $[6] = t3; 61: } else { 62: t3 = $[6]; 63: } 64: let t4; 65: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 66: t4 = <Box marginTop={1}><Text dimColor={true}>No code changes will be made until you approve the plan.</Text></Box>; 67: $[7] = t4; 68: } else { 69: t4 = $[7]; 70: } 71: let t5; 72: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 73: t5 = { 74: label: "Yes, enter plan mode", 75: value: "yes" as const 76: }; 77: $[8] = t5; 78: } else { 79: t5 = $[8]; 80: } 81: let t6; 82: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 83: t6 = [t5, { 84: label: "No, start implementing now", 85: value: "no" as const 86: }]; 87: $[9] = t6; 88: } else { 89: t6 = $[9]; 90: } 91: let t7; 92: if ($[10] !== handleResponse) { 93: t7 = () => handleResponse("no"); 94: $[10] = handleResponse; 95: $[11] = t7; 96: } else { 97: t7 = $[11]; 98: } 99: let t8; 100: if ($[12] !== handleResponse || $[13] !== t7) { 101: t8 = <Box flexDirection="column" marginTop={1} paddingX={1}>{t2}{t3}{t4}<Box marginTop={1}><Select options={t6} onChange={handleResponse} onCancel={t7} /></Box></Box>; 102: $[12] = handleResponse; 103: $[13] = t7; 104: $[14] = t8; 105: } else { 106: t8 = $[14]; 107: } 108: let t9; 109: if ($[15] !== t8 || $[16] !== workerBadge) { 110: t9 = <PermissionDialog color="planMode" title="Enter plan mode?" workerBadge={workerBadge}>{t8}</PermissionDialog>; 111: $[15] = t8; 112: $[16] = workerBadge; 113: $[17] = t9; 114: } else { 115: t9 = $[17]; 116: } 117: return t9; 118: } 119: function _temp(s) { 120: return s.toolPermissionContext.mode; 121: }

File: src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx

typescript 1: import { feature } from 'bun:bundle'; 2: import type { UUID } from 'crypto'; 3: import figures from 'figures'; 4: import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; 5: import { useNotifications } from 'src/context/notifications.js'; 6: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; 7: import { useAppState, useAppStateStore, useSetAppState } from 'src/state/AppState.js'; 8: import { getSdkBetas, getSessionId, isSessionPersistenceDisabled, setHasExitedPlanMode, setNeedsAutoModeExitAttachment, setNeedsPlanModeExitAttachment } from '../../../bootstrap/state.js'; 9: import { generateSessionName } from '../../../commands/rename/generateSessionName.js'; 10: import { launchUltraplan } from '../../../commands/ultraplan.js'; 11: import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'; 12: import { Box, Text } from '../../../ink.js'; 13: import type { AppState } from '../../../state/AppStateStore.js'; 14: import { AGENT_TOOL_NAME } from '../../../tools/AgentTool/constants.js'; 15: import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../../tools/ExitPlanModeTool/constants.js'; 16: import type { AllowedPrompt } from '../../../tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'; 17: import { TEAM_CREATE_TOOL_NAME } from '../../../tools/TeamCreateTool/constants.js'; 18: import { isAgentSwarmsEnabled } from '../../../utils/agentSwarmsEnabled.js'; 19: import { calculateContextPercentages, getContextWindowForModel } from '../../../utils/context.js'; 20: import { getExternalEditor } from '../../../utils/editor.js'; 21: import { getDisplayPath } from '../../../utils/file.js'; 22: import { toIDEDisplayName } from '../../../utils/ide.js'; 23: import { logError } from '../../../utils/log.js'; 24: import { enqueuePendingNotification } from '../../../utils/messageQueueManager.js'; 25: import { createUserMessage } from '../../../utils/messages.js'; 26: import { getMainLoopModel, getRuntimeMainLoopModel } from '../../../utils/model/model.js'; 27: import { createPromptRuleContent, isClassifierPermissionsEnabled, PROMPT_PREFIX } from '../../../utils/permissions/bashClassifier.js'; 28: import { type PermissionMode, toExternalPermissionMode } from '../../../utils/permissions/PermissionMode.js'; 29: import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'; 30: import { isAutoModeGateEnabled, restoreDangerousPermissions, stripDangerousPermissionsForAutoMode } from '../../../utils/permissions/permissionSetup.js'; 31: import { getPewterLedgerVariant, isPlanModeInterviewPhaseEnabled } from '../../../utils/planModeV2.js'; 32: import { getPlan, getPlanFilePath } from '../../../utils/plans.js'; 33: import { editFileInEditor, editPromptInEditor } from '../../../utils/promptEditor.js'; 34: import { getCurrentSessionTitle, getTranscriptPath, saveAgentName, saveCustomTitle } from '../../../utils/sessionStorage.js'; 35: import { getSettings_DEPRECATED } from '../../../utils/settings/settings.js'; 36: import { type OptionWithDescription, Select } from '../../CustomSelect/index.js'; 37: import { Markdown } from '../../Markdown.js'; 38: import { PermissionDialog } from '../PermissionDialog.js'; 39: import type { PermissionRequestProps } from '../PermissionRequest.js'; 40: import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'; 41: const autoModeStateModule = feature('TRANSCRIPT_CLASSIFIER') ? require('../../../utils/permissions/autoModeState.js') as typeof import('../../../utils/permissions/autoModeState.js') : null; 42: import type { Base64ImageSource, ImageBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'; 43: import type { PastedContent } from '../../../utils/config.js'; 44: import type { ImageDimensions } from '../../../utils/imageResizer.js'; 45: import { maybeResizeAndDownsampleImageBlock } from '../../../utils/imageResizer.js'; 46: import { cacheImagePath, storeImage } from '../../../utils/imageStore.js'; 47: 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'; 48: export function buildPermissionUpdates(mode: PermissionMode, allowedPrompts?: AllowedPrompt[]): PermissionUpdate[] { 49: const updates: PermissionUpdate[] = [{ 50: type: 'setMode', 51: mode: toExternalPermissionMode(mode), 52: destination: 'session' 53: }]; 54: if (isClassifierPermissionsEnabled() && allowedPrompts && allowedPrompts.length > 0) { 55: updates.push({ 56: type: 'addRules', 57: rules: allowedPrompts.map(p => ({ 58: toolName: p.tool, 59: ruleContent: createPromptRuleContent(p.prompt) 60: })), 61: behavior: 'allow', 62: destination: 'session' 63: }); 64: } 65: return updates; 66: } 67: export function autoNameSessionFromPlan(plan: string, setAppState: (updater: (prev: AppState) => AppState) => void, isClearContext: boolean): void { 68: if (isSessionPersistenceDisabled() || getSettings_DEPRECATED()?.cleanupPeriodDays === 0) { 69: return; 70: } 71: if (!isClearContext && getCurrentSessionTitle(getSessionId())) return; 72: void generateSessionName( 73: [createUserMessage({ 74: content: plan.slice(0, 1000) 75: })], new AbortController().signal).then(async name => { 76: if (!name || getCurrentSessionTitle(getSessionId())) return; 77: const sessionId = getSessionId() as UUID; 78: const fullPath = getTranscriptPath(); 79: await saveCustomTitle(sessionId, name, fullPath, 'auto'); 80: await saveAgentName(sessionId, name, fullPath, 'auto'); 81: setAppState(prev => { 82: if (prev.standaloneAgentContext?.name === name) return prev; 83: return { 84: ...prev, 85: standaloneAgentContext: { 86: ...prev.standaloneAgentContext, 87: name 88: } 89: }; 90: }); 91: }).catch(logError); 92: } 93: export function ExitPlanModePermissionRequest({ 94: toolUseConfirm, 95: onDone, 96: onReject, 97: workerBadge, 98: setStickyFooter 99: }: PermissionRequestProps): React.ReactNode { 100: const toolPermissionContext = useAppState(s => s.toolPermissionContext); 101: const setAppState = useSetAppState(); 102: const store = useAppStateStore(); 103: const { 104: addNotification 105: } = useNotifications(); 106: const [planFeedback, setPlanFeedback] = useState(''); 107: const [pastedContents, setPastedContents] = useState<Record<number, PastedContent>>({}); 108: const nextPasteIdRef = useRef(0); 109: const showClearContext = useAppState(s => s.settings.showClearContextOnPlanAccept) ?? false; 110: const ultraplanSessionUrl = useAppState(s => s.ultraplanSessionUrl); 111: const ultraplanLaunching = useAppState(s => s.ultraplanLaunching); 112: // Hide the Ultraplan button while a session is active or launching — 113: // selecting it would dismiss the dialog and reject locally before 114: // launchUltraplan can notice the session exists and return "already polling". 115: // feature() must sit directly in an if/ternary (bun:bundle DCE constraint). 116: const showUltraplan = feature('ULTRAPLAN') ? !ultraplanSessionUrl && !ultraplanLaunching : false; 117: const usage = toolUseConfirm.assistantMessage.message.usage; 118: const { 119: mode, 120: isAutoModeAvailable, 121: isBypassPermissionsModeAvailable 122: } = toolPermissionContext; 123: const options = useMemo(() => buildPlanApprovalOptions({ 124: showClearContext, 125: showUltraplan, 126: usedPercent: showClearContext ? getContextUsedPercent(usage, mode) : null, 127: isAutoModeAvailable, 128: isBypassPermissionsModeAvailable, 129: onFeedbackChange: setPlanFeedback 130: }), [showClearContext, showUltraplan, usage, mode, isAutoModeAvailable, isBypassPermissionsModeAvailable]); 131: function onImagePaste(base64Image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, _sourcePath?: string) { 132: const pasteId = nextPasteIdRef.current++; 133: const newContent: PastedContent = { 134: id: pasteId, 135: type: 'image', 136: content: base64Image, 137: mediaType: mediaType || 'image/png', 138: filename: filename || 'Pasted image', 139: dimensions 140: }; 141: cacheImagePath(newContent); 142: void storeImage(newContent); 143: setPastedContents(prev => ({ 144: ...prev, 145: [pasteId]: newContent 146: })); 147: } 148: const onRemoveImage = useCallback((id: number) => { 149: setPastedContents(prev => { 150: const next = { 151: ...prev 152: }; 153: delete next[id]; 154: return next; 155: }); 156: }, []); 157: const imageAttachments = Object.values(pastedContents).filter(c => c.type === 'image'); 158: const hasImages = imageAttachments.length > 0; 159: const isV2 = toolUseConfirm.tool.name === EXIT_PLAN_MODE_V2_TOOL_NAME; 160: const inputPlan = isV2 ? undefined : toolUseConfirm.input.plan as string | undefined; 161: const planFilePath = isV2 ? getPlanFilePath() : undefined; 162: const allowedPrompts = toolUseConfirm.input.allowedPrompts as AllowedPrompt[] | undefined; 163: const rawPlan = inputPlan ?? getPlan(); 164: const isEmpty = !rawPlan || rawPlan.trim() === ''; 165: // Capture the variant once on mount. GrowthBook reads from a disk cache 166: // so the value is stable across a single planning session. undefined = 167: // control arm. The variant is a fixed 3-value enum of short literals, 168: // not user input. 169: const [planStructureVariant] = useState(() => (getPewterLedgerVariant() ?? undefined) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS); 170: const [currentPlan, setCurrentPlan] = useState(() => { 171: if (inputPlan) return inputPlan; 172: const plan = getPlan(); 173: return plan ?? 'No plan found. Please write your plan to the plan file first.'; 174: }); 175: const [showSaveMessage, setShowSaveMessage] = useState(false); 176: // Track Ctrl+G local edits so updatedInput can include the plan (the tool 177: // only echoes the plan in tool_result when input.plan is set — otherwise 178: // the model already has it in context from writing the plan file). 179: const [planEditedLocally, setPlanEditedLocally] = useState(false); 180: // Auto-hide save message after 5 seconds 181: useEffect(() => { 182: if (showSaveMessage) { 183: const timer = setTimeout(setShowSaveMessage, 5000, false); 184: return () => clearTimeout(timer); 185: } 186: }, [showSaveMessage]); 187: // Handle Ctrl+G to edit plan in $EDITOR, Shift+Tab for auto-accept edits 188: const handleKeyDown = (e: KeyboardEvent): void => { 189: if (e.ctrl && e.key === 'g') { 190: e.preventDefault(); 191: logEvent('tengu_plan_external_editor_used', {}); 192: void (async () => { 193: if (isV2 && planFilePath) { 194: const result = await editFileInEditor(planFilePath); 195: if (result.error) { 196: addNotification({ 197: key: 'external-editor-error', 198: text: result.error, 199: color: 'warning', 200: priority: 'high' 201: }); 202: } 203: if (result.content !== null) { 204: if (result.content !== currentPlan) setPlanEditedLocally(true); 205: setCurrentPlan(result.content); 206: setShowSaveMessage(true); 207: } 208: } else { 209: const result = await editPromptInEditor(currentPlan); 210: if (result.error) { 211: addNotification({ 212: key: 'external-editor-error', 213: text: result.error, 214: color: 'warning', 215: priority: 'high' 216: }); 217: } 218: if (result.content !== null && result.content !== currentPlan) { 219: setCurrentPlan(result.content); 220: setShowSaveMessage(true); 221: } 222: } 223: })(); 224: return; 225: } 226: if (e.shift && e.key === 'tab') { 227: e.preventDefault(); 228: void handleResponse(showClearContext ? 'yes-accept-edits' : 'yes-accept-edits-keep-context'); 229: return; 230: } 231: }; 232: async function handleResponse(value: ResponseValue): Promise<void> { 233: const trimmedFeedback = planFeedback.trim(); 234: const acceptFeedback = trimmedFeedback || undefined; 235: if (value === 'ultraplan') { 236: logEvent('tengu_plan_exit', { 237: planLengthChars: currentPlan.length, 238: outcome: 'ultraplan' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 239: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 240: planStructureVariant 241: }); 242: onDone(); 243: onReject(); 244: toolUseConfirm.onReject('Plan being refined via Ultraplan — please wait for the result.'); 245: void launchUltraplan({ 246: blurb: '', 247: seedPlan: currentPlan, 248: getAppState: store.getState, 249: setAppState: store.setState, 250: signal: new AbortController().signal 251: }).then(msg => enqueuePendingNotification({ 252: value: msg, 253: mode: 'task-notification' 254: })).catch(logError); 255: return; 256: } 257: const updatedInput = isV2 && !planEditedLocally ? {} : { 258: plan: currentPlan 259: }; 260: if (feature('TRANSCRIPT_CLASSIFIER')) { 261: const goingToAuto = (value === 'yes-resume-auto-mode' || value === 'yes-auto-clear-context') && isAutoModeGateEnabled(); 262: const autoWasUsedDuringPlan = autoModeStateModule?.isAutoModeActive() ?? false; 263: if (value !== 'no' && !goingToAuto && autoWasUsedDuringPlan) { 264: autoModeStateModule?.setAutoModeActive(false); 265: setNeedsAutoModeExitAttachment(true); 266: setAppState(prev => ({ 267: ...prev, 268: toolPermissionContext: { 269: ...restoreDangerousPermissions(prev.toolPermissionContext), 270: prePlanMode: undefined 271: } 272: })); 273: } 274: } 275: const isResumeAutoOption = feature('TRANSCRIPT_CLASSIFIER') ? value === 'yes-resume-auto-mode' : false; 276: const isKeepContextOption = value === 'yes-accept-edits-keep-context' || value === 'yes-default-keep-context' || isResumeAutoOption; 277: if (value !== 'no') { 278: autoNameSessionFromPlan(currentPlan, setAppState, !isKeepContextOption); 279: } 280: if (value !== 'no' && !isKeepContextOption) { 281: let mode: PermissionMode = 'default'; 282: if (value === 'yes-bypass-permissions') { 283: mode = 'bypassPermissions'; 284: } else if (value === 'yes-accept-edits') { 285: mode = 'acceptEdits'; 286: } else if (feature('TRANSCRIPT_CLASSIFIER') && value === 'yes-auto-clear-context' && isAutoModeGateEnabled()) { 287: mode = 'auto'; 288: autoModeStateModule?.setAutoModeActive(true); 289: } 290: logEvent('tengu_plan_exit', { 291: planLengthChars: currentPlan.length, 292: outcome: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 293: clearContext: true, 294: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 295: planStructureVariant, 296: hasFeedback: !!acceptFeedback 297: }); 298: 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.` : ''; 299: // Capture the transcript path before context is cleared (session ID will be regenerated) 300: const transcriptPath = getTranscriptPath(); 301: 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}`; 302: 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.` : ''; 303: const feedbackSuffix = acceptFeedback ? `\n\nUser feedback on this plan: ${acceptFeedback}` : ''; 304: setAppState(prev => ({ 305: ...prev, 306: initialMessage: { 307: message: { 308: ...createUserMessage({ 309: content: `Implement the following plan:\n\n${currentPlan}${verificationInstruction}${transcriptHint}${teamHint}${feedbackSuffix}` 310: }), 311: planContent: currentPlan 312: }, 313: clearContext: true, 314: mode, 315: allowedPrompts 316: } 317: })); 318: setHasExitedPlanMode(true); 319: onDone(); 320: onReject(); 321: // Reject the tool use to unblock the query loop 322: // The REPL will see pendingInitialQuery and trigger fresh query 323: toolUseConfirm.onReject(); 324: return; 325: } 326: // Handle auto keep-context option — needs special handling because 327: // buildPermissionUpdates maps auto to 'default' via toExternalPermissionMode. 328: if (feature('TRANSCRIPT_CLASSIFIER') && value === 'yes-resume-auto-mode' && isAutoModeGateEnabled()) { 329: logEvent('tengu_plan_exit', { 330: planLengthChars: currentPlan.length, 331: outcome: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 332: clearContext: false, 333: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 334: planStructureVariant, 335: hasFeedback: !!acceptFeedback 336: }); 337: setHasExitedPlanMode(true); 338: setNeedsPlanModeExitAttachment(true); 339: autoModeStateModule?.setAutoModeActive(true); 340: setAppState(prev => ({ 341: ...prev, 342: toolPermissionContext: stripDangerousPermissionsForAutoMode({ 343: ...prev.toolPermissionContext, 344: mode: 'auto', 345: prePlanMode: undefined 346: }) 347: })); 348: onDone(); 349: toolUseConfirm.onAllow(updatedInput, [], acceptFeedback); 350: return; 351: } 352: const keepContextModes: Record<string, PermissionMode> = { 353: 'yes-accept-edits-keep-context': toolPermissionContext.isBypassPermissionsModeAvailable ? 'bypassPermissions' : 'acceptEdits', 354: 'yes-default-keep-context': 'default', 355: ...(feature('TRANSCRIPT_CLASSIFIER') ? { 356: 'yes-resume-auto-mode': 'default' as const 357: } : {}) 358: }; 359: const keepContextMode = keepContextModes[value]; 360: if (keepContextMode) { 361: logEvent('tengu_plan_exit', { 362: planLengthChars: currentPlan.length, 363: outcome: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 364: clearContext: false, 365: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 366: planStructureVariant, 367: hasFeedback: !!acceptFeedback 368: }); 369: setHasExitedPlanMode(true); 370: setNeedsPlanModeExitAttachment(true); 371: onDone(); 372: toolUseConfirm.onAllow(updatedInput, buildPermissionUpdates(keepContextMode, allowedPrompts), acceptFeedback); 373: return; 374: } 375: const standardModes: Record<string, PermissionMode> = { 376: 'yes-bypass-permissions': 'bypassPermissions', 377: 'yes-accept-edits': 'acceptEdits' 378: }; 379: const standardMode = standardModes[value]; 380: if (standardMode) { 381: logEvent('tengu_plan_exit', { 382: planLengthChars: currentPlan.length, 383: outcome: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 384: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 385: planStructureVariant, 386: hasFeedback: !!acceptFeedback 387: }); 388: setHasExitedPlanMode(true); 389: setNeedsPlanModeExitAttachment(true); 390: onDone(); 391: toolUseConfirm.onAllow(updatedInput, buildPermissionUpdates(standardMode, allowedPrompts), acceptFeedback); 392: return; 393: } 394: if (value === 'no') { 395: if (!trimmedFeedback && !hasImages) { 396: return; 397: } 398: logEvent('tengu_plan_exit', { 399: planLengthChars: currentPlan.length, 400: outcome: 'no' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 401: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 402: planStructureVariant 403: }); 404: let imageBlocks: ImageBlockParam[] | undefined; 405: if (hasImages) { 406: imageBlocks = await Promise.all(imageAttachments.map(async img => { 407: const block: ImageBlockParam = { 408: type: 'image', 409: source: { 410: type: 'base64', 411: media_type: (img.mediaType || 'image/png') as Base64ImageSource['media_type'], 412: data: img.content 413: } 414: }; 415: const resized = await maybeResizeAndDownsampleImageBlock(block); 416: return resized.block; 417: })); 418: } 419: onDone(); 420: onReject(); 421: toolUseConfirm.onReject(trimmedFeedback || (hasImages ? '(See attached image)' : undefined), imageBlocks && imageBlocks.length > 0 ? imageBlocks : undefined); 422: } 423: } 424: const editor = getExternalEditor(); 425: const editorName = editor ? toIDEDisplayName(editor) : null; 426: const handleResponseRef = useRef(handleResponse); 427: handleResponseRef.current = handleResponse; 428: const handleCancelRef = useRef<() => void>(undefined); 429: handleCancelRef.current = () => { 430: logEvent('tengu_plan_exit', { 431: planLengthChars: currentPlan.length, 432: outcome: 'no' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 433: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 434: planStructureVariant 435: }); 436: onDone(); 437: onReject(); 438: toolUseConfirm.onReject(); 439: }; 440: const useStickyFooter = !isEmpty && !!setStickyFooter; 441: useLayoutEffect(() => { 442: if (!useStickyFooter) return; 443: setStickyFooter(<Box flexDirection="column" borderStyle="round" borderColor="planMode" borderLeft={false} borderRight={false} borderBottom={false} paddingX={1}> 444: <Text dimColor>Would you like to proceed?</Text> 445: <Box marginTop={1}> 446: <Select options={options} onChange={v => void handleResponseRef.current(v)} onCancel={() => handleCancelRef.current?.()} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage} /> 447: </Box> 448: {editorName && <Box flexDirection="row" gap={1} marginTop={1}> 449: <Text dimColor>ctrl-g to edit in </Text> 450: <Text bold dimColor> 451: {editorName} 452: </Text> 453: {isV2 && planFilePath && <Text dimColor> · {getDisplayPath(planFilePath)}</Text>} 454: {showSaveMessage && <> 455: <Text dimColor>{' · '}</Text> 456: <Text color="success">{figures.tick}Plan saved!</Text> 457: </>} 458: </Box>} 459: </Box>); 460: return () => setStickyFooter(null); 461: }, [useStickyFooter, setStickyFooter, options, pastedContents, editorName, isV2, planFilePath, showSaveMessage]); 462: if (isEmpty) { 463: function handleEmptyPlanResponse(value: 'yes' | 'no'): void { 464: if (value === 'yes') { 465: logEvent('tengu_plan_exit', { 466: planLengthChars: 0, 467: outcome: 'yes-default' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 468: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 469: planStructureVariant 470: }); 471: if (feature('TRANSCRIPT_CLASSIFIER')) { 472: const autoWasUsedDuringPlan = autoModeStateModule?.isAutoModeActive() ?? false; 473: if (autoWasUsedDuringPlan) { 474: autoModeStateModule?.setAutoModeActive(false); 475: setNeedsAutoModeExitAttachment(true); 476: setAppState(prev => ({ 477: ...prev, 478: toolPermissionContext: { 479: ...restoreDangerousPermissions(prev.toolPermissionContext), 480: prePlanMode: undefined 481: } 482: })); 483: } 484: } 485: setHasExitedPlanMode(true); 486: setNeedsPlanModeExitAttachment(true); 487: onDone(); 488: toolUseConfirm.onAllow({}, [{ 489: type: 'setMode', 490: mode: 'default', 491: destination: 'session' 492: }]); 493: } else { 494: logEvent('tengu_plan_exit', { 495: planLengthChars: 0, 496: outcome: 'no' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 497: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 498: planStructureVariant 499: }); 500: onDone(); 501: onReject(); 502: toolUseConfirm.onReject(); 503: } 504: } 505: return <PermissionDialog color="planMode" title="Exit plan mode?" workerBadge={workerBadge}> 506: <Box flexDirection="column" paddingX={1} marginTop={1}> 507: <Text>Claude wants to exit plan mode</Text> 508: <Box marginTop={1}> 509: <Select options={[{ 510: label: 'Yes', 511: value: 'yes' as const 512: }, { 513: label: 'No', 514: value: 'no' as const 515: }]} onChange={handleEmptyPlanResponse} onCancel={() => { 516: logEvent('tengu_plan_exit', { 517: planLengthChars: 0, 518: outcome: 'no' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 519: interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(), 520: planStructureVariant 521: }); 522: onDone(); 523: onReject(); 524: toolUseConfirm.onReject(); 525: }} /> 526: </Box> 527: </Box> 528: </PermissionDialog>; 529: } 530: return <Box flexDirection="column" tabIndex={0} autoFocus onKeyDown={handleKeyDown}> 531: <PermissionDialog color="planMode" title="Ready to code?" innerPaddingX={0} workerBadge={workerBadge}> 532: <Box flexDirection="column" marginTop={1}> 533: <Box paddingX={1} flexDirection="column"> 534: <Text>Here is Claude&apos;s plan:</Text> 535: </Box> 536: <Box borderColor="subtle" borderStyle="dashed" flexDirection="column" borderLeft={false} borderRight={false} paddingX={1} marginBottom={1} 537: overflow="hidden"> 538: <Markdown>{currentPlan}</Markdown> 539: </Box> 540: <Box flexDirection="column" paddingX={1}> 541: <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" /> 542: {isClassifierPermissionsEnabled() && allowedPrompts && allowedPrompts.length > 0 && <Box flexDirection="column" marginBottom={1}> 543: <Text bold>Requested permissions:</Text> 544: {allowedPrompts.map((p, i) => <Text key={i} dimColor> 545: {' '}· {p.tool}({PROMPT_PREFIX} {p.prompt}) 546: </Text>)} 547: </Box>} 548: {!useStickyFooter && <> 549: <Text dimColor> 550: Claude has written up a plan and is ready to execute. Would 551: you like to proceed? 552: </Text> 553: <Box marginTop={1}> 554: <Select options={options} onChange={handleResponse} onCancel={() => handleCancelRef.current?.()} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage} /> 555: </Box> 556: </>} 557: </Box> 558: </Box> 559: </PermissionDialog> 560: {!useStickyFooter && editorName && <Box flexDirection="row" gap={1} paddingX={1} marginTop={1}> 561: <Box> 562: <Text dimColor>ctrl-g to edit in </Text> 563: <Text bold dimColor> 564: {editorName} 565: </Text> 566: {isV2 && planFilePath && <Text dimColor> · {getDisplayPath(planFilePath)}</Text>} 567: </Box> 568: {showSaveMessage && <Box> 569: <Text dimColor>{' · '}</Text> 570: <Text color="success">{figures.tick}Plan saved!</Text> 571: </Box>} 572: </Box>} 573: </Box>; 574: } 575: export function buildPlanApprovalOptions({ 576: showClearContext, 577: showUltraplan, 578: usedPercent, 579: isAutoModeAvailable, 580: isBypassPermissionsModeAvailable, 581: onFeedbackChange 582: }: { 583: showClearContext: boolean; 584: showUltraplan: boolean; 585: usedPercent: number | null; 586: isAutoModeAvailable: boolean | undefined; 587: isBypassPermissionsModeAvailable: boolean | undefined; 588: onFeedbackChange: (v: string) => void; 589: }): OptionWithDescription<ResponseValue>[] { 590: const options: OptionWithDescription<ResponseValue>[] = []; 591: const usedLabel = usedPercent !== null ? ` (${usedPercent}% used)` : ''; 592: if (showClearContext) { 593: if (feature('TRANSCRIPT_CLASSIFIER') && isAutoModeAvailable) { 594: options.push({ 595: label: `Yes, clear context${usedLabel} and use auto mode`, 596: value: 'yes-auto-clear-context' 597: }); 598: } else if (isBypassPermissionsModeAvailable) { 599: options.push({ 600: label: `Yes, clear context${usedLabel} and bypass permissions`, 601: value: 'yes-bypass-permissions' 602: }); 603: } else { 604: options.push({ 605: label: `Yes, clear context${usedLabel} and auto-accept edits`, 606: value: 'yes-accept-edits' 607: }); 608: } 609: } 610: if (feature('TRANSCRIPT_CLASSIFIER') && isAutoModeAvailable) { 611: options.push({ 612: label: 'Yes, and use auto mode', 613: value: 'yes-resume-auto-mode' 614: }); 615: } else if (isBypassPermissionsModeAvailable) { 616: options.push({ 617: label: 'Yes, and bypass permissions', 618: value: 'yes-accept-edits-keep-context' 619: }); 620: } else { 621: options.push({ 622: label: 'Yes, auto-accept edits', 623: value: 'yes-accept-edits-keep-context' 624: }); 625: } 626: options.push({ 627: label: 'Yes, manually approve edits', 628: value: 'yes-default-keep-context' 629: }); 630: if (showUltraplan) { 631: options.push({ 632: label: 'No, refine with Ultraplan on Claude Code on the web', 633: value: 'ultraplan' 634: }); 635: } 636: options.push({ 637: type: 'input', 638: label: 'No, keep planning', 639: value: 'no', 640: placeholder: 'Tell Claude what to change', 641: description: 'shift+tab to approve with this feedback', 642: onChange: onFeedbackChange 643: }); 644: return options; 645: } 646: function getContextUsedPercent(usage: { 647: input_tokens: number; 648: cache_creation_input_tokens?: number | null; 649: cache_read_input_tokens?: number | null; 650: } | undefined, permissionMode: PermissionMode): number | null { 651: if (!usage) return null; 652: const runtimeModel = getRuntimeMainLoopModel({ 653: permissionMode, 654: mainLoopModel: getMainLoopModel(), 655: exceeds200kTokens: false 656: }); 657: const contextWindowSize = getContextWindowForModel(runtimeModel, getSdkBetas()); 658: const { 659: used 660: } = calculateContextPercentages({ 661: input_tokens: usage.input_tokens, 662: cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0, 663: cache_read_input_tokens: usage.cache_read_input_tokens ?? 0 664: }, contextWindowSize); 665: return used; 666: }

File: src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { basename, relative } from 'path'; 3: import React from 'react'; 4: import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js'; 5: import { getCwd } from 'src/utils/cwd.js'; 6: import type { z } from 'zod/v4'; 7: import { Text } from '../../../ink.js'; 8: import { FileEditTool } from '../../../tools/FileEditTool/FileEditTool.js'; 9: import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'; 10: import { createSingleEditDiffConfig, type FileEdit, type IDEDiffSupport } from '../FilePermissionDialog/ideDiffConfig.js'; 11: import type { PermissionRequestProps } from '../PermissionRequest.js'; 12: type FileEditInput = z.infer<typeof FileEditTool.inputSchema>; 13: const ideDiffSupport: IDEDiffSupport<FileEditInput> = { 14: getConfig: (input: FileEditInput) => createSingleEditDiffConfig(input.file_path, input.old_string, input.new_string, input.replace_all), 15: applyChanges: (input: FileEditInput, modifiedEdits: FileEdit[]) => { 16: const firstEdit = modifiedEdits[0]; 17: if (firstEdit) { 18: return { 19: ...input, 20: old_string: firstEdit.old_string, 21: new_string: firstEdit.new_string, 22: replace_all: firstEdit.replace_all 23: }; 24: } 25: return input; 26: } 27: }; 28: export function FileEditPermissionRequest(props) { 29: const $ = _c(51); 30: const parseInput = _temp; 31: let T0; 32: let T1; 33: let T2; 34: let file_path; 35: let new_string; 36: let old_string; 37: let replace_all; 38: let t0; 39: let t1; 40: let t10; 41: let t2; 42: let t3; 43: let t4; 44: let t5; 45: let t6; 46: let t7; 47: let t8; 48: let t9; 49: if ($[0] !== props.onDone || $[1] !== props.onReject || $[2] !== props.toolUseConfirm || $[3] !== props.toolUseContext || $[4] !== props.workerBadge) { 50: const parsed = parseInput(props.toolUseConfirm.input); 51: ({ 52: file_path, 53: old_string, 54: new_string, 55: replace_all 56: } = parsed); 57: T2 = FilePermissionDialog; 58: t4 = props.toolUseConfirm; 59: t5 = props.toolUseContext; 60: t6 = props.onDone; 61: t7 = props.onReject; 62: t8 = props.workerBadge; 63: t9 = "Edit file"; 64: t10 = relative(getCwd(), file_path); 65: T1 = Text; 66: t2 = "Do you want to make this edit to"; 67: t3 = " "; 68: T0 = Text; 69: t0 = true; 70: t1 = basename(file_path); 71: $[0] = props.onDone; 72: $[1] = props.onReject; 73: $[2] = props.toolUseConfirm; 74: $[3] = props.toolUseContext; 75: $[4] = props.workerBadge; 76: $[5] = T0; 77: $[6] = T1; 78: $[7] = T2; 79: $[8] = file_path; 80: $[9] = new_string; 81: $[10] = old_string; 82: $[11] = replace_all; 83: $[12] = t0; 84: $[13] = t1; 85: $[14] = t10; 86: $[15] = t2; 87: $[16] = t3; 88: $[17] = t4; 89: $[18] = t5; 90: $[19] = t6; 91: $[20] = t7; 92: $[21] = t8; 93: $[22] = t9; 94: } else { 95: T0 = $[5]; 96: T1 = $[6]; 97: T2 = $[7]; 98: file_path = $[8]; 99: new_string = $[9]; 100: old_string = $[10]; 101: replace_all = $[11]; 102: t0 = $[12]; 103: t1 = $[13]; 104: t10 = $[14]; 105: t2 = $[15]; 106: t3 = $[16]; 107: t4 = $[17]; 108: t5 = $[18]; 109: t6 = $[19]; 110: t7 = $[20]; 111: t8 = $[21]; 112: t9 = $[22]; 113: } 114: let t11; 115: if ($[23] !== T0 || $[24] !== t0 || $[25] !== t1) { 116: t11 = <T0 bold={t0}>{t1}</T0>; 117: $[23] = T0; 118: $[24] = t0; 119: $[25] = t1; 120: $[26] = t11; 121: } else { 122: t11 = $[26]; 123: } 124: let t12; 125: if ($[27] !== T1 || $[28] !== t11 || $[29] !== t2 || $[30] !== t3) { 126: t12 = <T1>{t2}{t3}{t11}?</T1>; 127: $[27] = T1; 128: $[28] = t11; 129: $[29] = t2; 130: $[30] = t3; 131: $[31] = t12; 132: } else { 133: t12 = $[31]; 134: } 135: const t13 = replace_all || false; 136: let t14; 137: if ($[32] !== new_string || $[33] !== old_string || $[34] !== t13) { 138: t14 = [{ 139: old_string, 140: new_string, 141: replace_all: t13 142: }]; 143: $[32] = new_string; 144: $[33] = old_string; 145: $[34] = t13; 146: $[35] = t14; 147: } else { 148: t14 = $[35]; 149: } 150: let t15; 151: if ($[36] !== file_path || $[37] !== t14) { 152: t15 = <FileEditToolDiff file_path={file_path} edits={t14} />; 153: $[36] = file_path; 154: $[37] = t14; 155: $[38] = t15; 156: } else { 157: t15 = $[38]; 158: } 159: let t16; 160: if ($[39] !== T2 || $[40] !== file_path || $[41] !== t10 || $[42] !== t12 || $[43] !== t15 || $[44] !== t4 || $[45] !== t5 || $[46] !== t6 || $[47] !== t7 || $[48] !== t8 || $[49] !== t9) { 161: t16 = <T2 toolUseConfirm={t4} toolUseContext={t5} onDone={t6} onReject={t7} workerBadge={t8} title={t9} subtitle={t10} question={t12} content={t15} path={file_path} completionType="str_replace_single" parseInput={parseInput} ideDiffSupport={ideDiffSupport} />; 162: $[39] = T2; 163: $[40] = file_path; 164: $[41] = t10; 165: $[42] = t12; 166: $[43] = t15; 167: $[44] = t4; 168: $[45] = t5; 169: $[46] = t6; 170: $[47] = t7; 171: $[48] = t8; 172: $[49] = t9; 173: $[50] = t16; 174: } else { 175: t16 = $[50]; 176: } 177: return t16; 178: } 179: function _temp(input) { 180: return FileEditTool.inputSchema.parse(input); 181: }

File: src/components/permissions/FilePermissionDialog/FilePermissionDialog.tsx

typescript 1: import { relative } from 'path'; 2: import React, { useMemo } from 'react'; 3: import { useDiffInIDE } from '../../../hooks/useDiffInIDE.js'; 4: import { Box, Text } from '../../../ink.js'; 5: import type { ToolUseContext } from '../../../Tool.js'; 6: import { getLanguageName } from '../../../utils/cliHighlight.js'; 7: import { getCwd } from '../../../utils/cwd.js'; 8: import { getFsImplementation, safeResolvePath } from '../../../utils/fsOperations.js'; 9: import { expandPath } from '../../../utils/path.js'; 10: import type { CompletionType } from '../../../utils/unaryLogging.js'; 11: import { Select } from '../../CustomSelect/index.js'; 12: import { ShowInIDEPrompt } from '../../ShowInIDEPrompt.js'; 13: import { usePermissionRequestLogging } from '../hooks.js'; 14: import { PermissionDialog } from '../PermissionDialog.js'; 15: import type { ToolUseConfirm } from '../PermissionRequest.js'; 16: import type { WorkerBadgeProps } from '../WorkerBadge.js'; 17: import type { IDEDiffSupport } from './ideDiffConfig.js'; 18: import type { FileOperationType, PermissionOption } from './permissionOptions.js'; 19: import { type ToolInput, useFilePermissionDialog } from './useFilePermissionDialog.js'; 20: export type FilePermissionDialogProps<T extends ToolInput = ToolInput> = { 21: toolUseConfirm: ToolUseConfirm; 22: toolUseContext: ToolUseContext; 23: onDone: () => void; 24: onReject: () => void; 25: title: string; 26: subtitle?: React.ReactNode; 27: question?: string | React.ReactNode; 28: content?: React.ReactNode; 29: completionType?: CompletionType; 30: languageName?: string; 31: path: string | null; 32: parseInput: (input: unknown) => T; 33: operationType?: FileOperationType; 34: ideDiffSupport?: IDEDiffSupport<T>; 35: workerBadge: WorkerBadgeProps | undefined; 36: }; 37: export function FilePermissionDialog<T extends ToolInput = ToolInput>({ 38: toolUseConfirm, 39: toolUseContext, 40: onDone, 41: onReject, 42: title, 43: subtitle, 44: question = 'Do you want to proceed?', 45: content, 46: completionType = 'tool_use_single', 47: path, 48: parseInput, 49: operationType = 'write', 50: ideDiffSupport, 51: workerBadge, 52: languageName: languageNameOverride 53: }: FilePermissionDialogProps<T>): React.ReactNode { 54: const languageName = useMemo(() => languageNameOverride ?? (path ? getLanguageName(path) : 'none'), [languageNameOverride, path]); 55: const unaryEvent = useMemo(() => ({ 56: completion_type: completionType, 57: language_name: languageName 58: }), [completionType, languageName]); 59: usePermissionRequestLogging(toolUseConfirm, unaryEvent); 60: const symlinkTarget = useMemo(() => { 61: if (!path || operationType === 'read') { 62: return null; 63: } 64: const expandedPath = expandPath(path); 65: const fs = getFsImplementation(); 66: const { 67: resolvedPath, 68: isSymlink 69: } = safeResolvePath(fs, expandedPath); 70: if (isSymlink) { 71: return resolvedPath; 72: } 73: return null; 74: }, [path, operationType]); 75: const fileDialogResult = useFilePermissionDialog({ 76: filePath: path || '', 77: completionType, 78: languageName, 79: toolUseConfirm, 80: onDone, 81: onReject, 82: parseInput, 83: operationType 84: }); 85: // Use file dialog results for options 86: const { 87: options, 88: acceptFeedback, 89: rejectFeedback, 90: setFocusedOption, 91: handleInputModeToggle, 92: focusedOption, 93: yesInputMode, 94: noInputMode 95: } = fileDialogResult; 96: // Parse input using the provided parser 97: const parsedInput = parseInput(toolUseConfirm.input); 98: // Set up IDE diff support if enabled. Memoized: getConfig may do disk I/O 99: // (FileWrite's getConfig calls readFileSync for the old-content diff). 100: const ideDiffConfig = useMemo(() => ideDiffSupport ? ideDiffSupport.getConfig(parseInput(toolUseConfirm.input)) : null, [ideDiffSupport, toolUseConfirm.input]); 101: const diffParams = ideDiffConfig ? { 102: onChange: (option: PermissionOption, input: { 103: file_path: string; 104: edits: Array<{ 105: old_string: string; 106: new_string: string; 107: replace_all?: boolean; 108: }>; 109: }) => { 110: const transformedInput = ideDiffSupport!.applyChanges(parsedInput, input.edits); 111: fileDialogResult.onChange(option, transformedInput); 112: }, 113: toolUseContext, 114: filePath: ideDiffConfig.filePath, 115: edits: (ideDiffConfig.edits || []).map(e => ({ 116: old_string: e.old_string, 117: new_string: e.new_string, 118: replace_all: e.replace_all || false 119: })), 120: editMode: ideDiffConfig.editMode || 'single' 121: } : { 122: onChange: () => {}, 123: toolUseContext, 124: filePath: '', 125: edits: [], 126: editMode: 'single' as const 127: }; 128: const { 129: closeTabInIDE, 130: showingDiffInIDE, 131: ideName 132: } = useDiffInIDE(diffParams); 133: const onChange = (option_0: PermissionOption, feedback?: string) => { 134: closeTabInIDE?.(); 135: fileDialogResult.onChange(option_0, parsedInput, feedback?.trim()); 136: }; 137: if (showingDiffInIDE && ideDiffConfig && path) { 138: return <ShowInIDEPrompt onChange={(option_1: PermissionOption, _input, feedback_0?: string) => onChange(option_1, feedback_0)} options={options} filePath={path} input={parsedInput} ideName={ideName} symlinkTarget={symlinkTarget} rejectFeedback={rejectFeedback} acceptFeedback={acceptFeedback} setFocusedOption={setFocusedOption} onInputModeToggle={handleInputModeToggle} focusedOption={focusedOption} yesInputMode={yesInputMode} noInputMode={noInputMode} />; 139: } 140: const isSymlinkOutsideCwd = symlinkTarget != null && relative(getCwd(), symlinkTarget).startsWith('..'); 141: const symlinkWarning = symlinkTarget ? <Box paddingX={1} marginBottom={1}> 142: <Text color="warning"> 143: {isSymlinkOutsideCwd ? `This will modify ${symlinkTarget} (outside working directory) via a symlink` : `Symlink target: ${symlinkTarget}`} 144: </Text> 145: </Box> : null; 146: return <> 147: <PermissionDialog title={title} subtitle={subtitle} innerPaddingX={0} workerBadge={workerBadge}> 148: {symlinkWarning} 149: {content} 150: <Box flexDirection="column" paddingX={1}> 151: {typeof question === 'string' ? <Text>{question}</Text> : question} 152: <Select options={options} inlineDescriptions onChange={value => { 153: const selected = options.find(opt => opt.value === value); 154: if (selected) { 155: if (selected.option.type === 'reject') { 156: const trimmedFeedback = rejectFeedback.trim(); 157: onChange(selected.option, trimmedFeedback || undefined); 158: return; 159: } 160: if (selected.option.type === 'accept-once') { 161: const trimmedFeedback_0 = acceptFeedback.trim(); 162: onChange(selected.option, trimmedFeedback_0 || undefined); 163: return; 164: } 165: onChange(selected.option); 166: } 167: }} onCancel={() => onChange({ 168: type: 'reject' 169: })} onFocus={value_0 => setFocusedOption(value_0)} onInputModeToggle={handleInputModeToggle} /> 170: </Box> 171: </PermissionDialog> 172: <Box paddingX={1} marginTop={1}> 173: <Text dimColor> 174: Esc to cancel 175: {(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'} 176: </Text> 177: </Box> 178: </>; 179: }

File: src/components/permissions/FilePermissionDialog/ideDiffConfig.ts

typescript 1: import type { ToolInput } from './useFilePermissionDialog.js' 2: export interface FileEdit { 3: old_string: string 4: new_string: string 5: replace_all?: boolean 6: } 7: export interface IDEDiffConfig { 8: filePath: string 9: edits?: FileEdit[] 10: editMode?: 'single' | 'multiple' 11: } 12: export interface IDEDiffChangeInput { 13: file_path: string 14: edits: FileEdit[] 15: } 16: export interface IDEDiffSupport<TInput extends ToolInput> { 17: getConfig(input: TInput): IDEDiffConfig 18: applyChanges(input: TInput, modifiedEdits: FileEdit[]): TInput 19: } 20: export function createSingleEditDiffConfig( 21: filePath: string, 22: oldString: string, 23: newString: string, 24: replaceAll?: boolean, 25: ): IDEDiffConfig { 26: return { 27: filePath, 28: edits: [ 29: { 30: old_string: oldString, 31: new_string: newString, 32: replace_all: replaceAll, 33: }, 34: ], 35: editMode: 'single', 36: } 37: }

File: src/components/permissions/FilePermissionDialog/permissionOptions.tsx

typescript 1: import { homedir } from 'os'; 2: import { basename, join, sep } from 'path'; 3: import React, { type ReactNode } from 'react'; 4: import { getOriginalCwd } from '../../../bootstrap/state.js'; 5: import { Text } from '../../../ink.js'; 6: import { getShortcutDisplay } from '../../../keybindings/shortcutFormat.js'; 7: import type { ToolPermissionContext } from '../../../Tool.js'; 8: import { expandPath, getDirectoryForPath } from '../../../utils/path.js'; 9: import { normalizeCaseForComparison, pathInAllowedWorkingPath } from '../../../utils/permissions/filesystem.js'; 10: import type { OptionWithDescription } from '../../CustomSelect/select.js'; 11: export function isInClaudeFolder(filePath: string): boolean { 12: const absolutePath = expandPath(filePath); 13: const claudeFolderPath = expandPath(`${getOriginalCwd()}/.claude`); 14: const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath); 15: const normalizedClaudeFolderPath = normalizeCaseForComparison(claudeFolderPath); 16: return normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + sep.toLowerCase()) || 17: normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + '/'); 18: } 19: export function isInGlobalClaudeFolder(filePath: string): boolean { 20: const absolutePath = expandPath(filePath); 21: const globalClaudeFolderPath = join(homedir(), '.claude'); 22: const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath); 23: const normalizedGlobalClaudeFolderPath = normalizeCaseForComparison(globalClaudeFolderPath); 24: return normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + sep.toLowerCase()) || normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + '/'); 25: } 26: export type PermissionOption = { 27: type: 'accept-once'; 28: } | { 29: type: 'accept-session'; 30: scope?: 'claude-folder' | 'global-claude-folder'; 31: } | { 32: type: 'reject'; 33: }; 34: export type PermissionOptionWithLabel = OptionWithDescription<string> & { 35: option: PermissionOption; 36: }; 37: export type FileOperationType = 'read' | 'write' | 'create'; 38: export function getFilePermissionOptions({ 39: filePath, 40: toolPermissionContext, 41: operationType = 'write', 42: onRejectFeedbackChange, 43: onAcceptFeedbackChange, 44: yesInputMode = false, 45: noInputMode = false 46: }: { 47: filePath: string; 48: toolPermissionContext: ToolPermissionContext; 49: operationType?: FileOperationType; 50: onRejectFeedbackChange?: (value: string) => void; 51: onAcceptFeedbackChange?: (value: string) => void; 52: yesInputMode?: boolean; 53: noInputMode?: boolean; 54: }): PermissionOptionWithLabel[] { 55: const options: PermissionOptionWithLabel[] = []; 56: const modeCycleShortcut = getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab'); 57: if (yesInputMode && onAcceptFeedbackChange) { 58: options.push({ 59: type: 'input', 60: label: 'Yes', 61: value: 'yes', 62: placeholder: 'and tell Claude what to do next', 63: onChange: onAcceptFeedbackChange, 64: allowEmptySubmitToCancel: true, 65: option: { 66: type: 'accept-once' 67: } 68: }); 69: } else { 70: options.push({ 71: label: 'Yes', 72: value: 'yes', 73: option: { 74: type: 'accept-once' 75: } 76: }); 77: } 78: const inAllowedPath = pathInAllowedWorkingPath(filePath, toolPermissionContext); 79: const inClaudeFolder = isInClaudeFolder(filePath); 80: const inGlobalClaudeFolder = isInGlobalClaudeFolder(filePath); 81: if ((inClaudeFolder || inGlobalClaudeFolder) && operationType !== 'read') { 82: options.push({ 83: label: 'Yes, and allow Claude to edit its own settings for this session', 84: value: 'yes-claude-folder', 85: option: { 86: type: 'accept-session', 87: scope: inGlobalClaudeFolder ? 'global-claude-folder' : 'claude-folder' 88: } 89: }); 90: } else { 91: let sessionLabel: ReactNode; 92: if (inAllowedPath) { 93: if (operationType === 'read') { 94: sessionLabel = 'Yes, during this session'; 95: } else { 96: sessionLabel = <Text> 97: Yes, allow all edits during this session{' '} 98: <Text bold>({modeCycleShortcut})</Text> 99: </Text>; 100: } 101: } else { 102: const dirPath = getDirectoryForPath(filePath); 103: const dirName = basename(dirPath) || 'this directory'; 104: if (operationType === 'read') { 105: sessionLabel = <Text> 106: Yes, allow reading from <Text bold>{dirName}/</Text> during this 107: session 108: </Text>; 109: } else { 110: sessionLabel = <Text> 111: Yes, allow all edits in <Text bold>{dirName}/</Text> during this 112: session <Text bold>({modeCycleShortcut})</Text> 113: </Text>; 114: } 115: } 116: options.push({ 117: label: sessionLabel, 118: value: 'yes-session', 119: option: { 120: type: 'accept-session' 121: } 122: }); 123: } 124: if (noInputMode && onRejectFeedbackChange) { 125: options.push({ 126: type: 'input', 127: label: 'No', 128: value: 'no', 129: placeholder: 'and tell Claude what to do differently', 130: onChange: onRejectFeedbackChange, 131: allowEmptySubmitToCancel: true, 132: option: { 133: type: 'reject' 134: } 135: }); 136: } else { 137: options.push({ 138: label: 'No', 139: value: 'no', 140: option: { 141: type: 'reject' 142: } 143: }); 144: } 145: return options; 146: }

File: src/components/permissions/FilePermissionDialog/useFilePermissionDialog.ts

typescript 1: import { useCallback, useMemo, useState } from 'react' 2: import { useAppState } from 'src/state/AppState.js' 3: import { useKeybindings } from '../../../keybindings/useKeybinding.js' 4: import { 5: type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 6: logEvent, 7: } from '../../../services/analytics/index.js' 8: import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js' 9: import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js' 10: import type { CompletionType } from '../../../utils/unaryLogging.js' 11: import type { ToolUseConfirm } from '../PermissionRequest.js' 12: import { 13: type FileOperationType, 14: getFilePermissionOptions, 15: type PermissionOption, 16: type PermissionOptionWithLabel, 17: } from './permissionOptions.js' 18: import { 19: PERMISSION_HANDLERS, 20: type PermissionHandlerParams, 21: } from './usePermissionHandler.js' 22: export interface ToolInput { 23: [key: string]: unknown 24: } 25: export type UseFilePermissionDialogProps<T extends ToolInput> = { 26: filePath: string 27: completionType: CompletionType 28: languageName: string | Promise<string> 29: toolUseConfirm: ToolUseConfirm 30: onDone: () => void 31: onReject: () => void 32: parseInput: (input: unknown) => T 33: operationType?: FileOperationType 34: } 35: export type UseFilePermissionDialogResult<T> = { 36: options: PermissionOptionWithLabel[] 37: onChange: (option: PermissionOption, input: T, feedback?: string) => void 38: acceptFeedback: string 39: rejectFeedback: string 40: focusedOption: string 41: setFocusedOption: (option: string) => void 42: handleInputModeToggle: (value: string) => void 43: yesInputMode: boolean 44: noInputMode: boolean 45: } 46: export function useFilePermissionDialog<T extends ToolInput>({ 47: filePath, 48: completionType, 49: languageName, 50: toolUseConfirm, 51: onDone, 52: onReject, 53: parseInput, 54: operationType = 'write', 55: }: UseFilePermissionDialogProps<T>): UseFilePermissionDialogResult<T> { 56: const toolPermissionContext = useAppState(s => s.toolPermissionContext) 57: const [acceptFeedback, setAcceptFeedback] = useState('') 58: const [rejectFeedback, setRejectFeedback] = useState('') 59: const [focusedOption, setFocusedOption] = useState('yes') 60: const [yesInputMode, setYesInputMode] = useState(false) 61: const [noInputMode, setNoInputMode] = useState(false) 62: const [yesFeedbackModeEntered, setYesFeedbackModeEntered] = useState(false) 63: const [noFeedbackModeEntered, setNoFeedbackModeEntered] = useState(false) 64: const options = useMemo( 65: () => 66: getFilePermissionOptions({ 67: filePath, 68: toolPermissionContext, 69: operationType, 70: onRejectFeedbackChange: setRejectFeedback, 71: onAcceptFeedbackChange: setAcceptFeedback, 72: yesInputMode, 73: noInputMode, 74: }), 75: [filePath, toolPermissionContext, operationType, yesInputMode, noInputMode], 76: ) 77: const onChange = useCallback( 78: (option: PermissionOption, input: T, feedback?: string) => { 79: const params: PermissionHandlerParams = { 80: messageId: toolUseConfirm.assistantMessage.message.id, 81: path: filePath, 82: toolUseConfirm, 83: toolPermissionContext, 84: onDone, 85: onReject, 86: completionType, 87: languageName, 88: operationType, 89: } 90: const originalOnAllow = toolUseConfirm.onAllow 91: toolUseConfirm.onAllow = ( 92: _input: unknown, 93: permissionUpdates: PermissionUpdate[], 94: feedback?: string, 95: ) => { 96: originalOnAllow(input, permissionUpdates, feedback) 97: } 98: const handler = PERMISSION_HANDLERS[option.type] 99: handler(params, { 100: feedback, 101: hasFeedback: !!feedback, 102: enteredFeedbackMode: 103: option.type === 'accept-once' 104: ? yesFeedbackModeEntered 105: : noFeedbackModeEntered, 106: scope: option.type === 'accept-session' ? option.scope : undefined, 107: }) 108: }, 109: [ 110: filePath, 111: completionType, 112: languageName, 113: toolUseConfirm, 114: toolPermissionContext, 115: onDone, 116: onReject, 117: operationType, 118: yesFeedbackModeEntered, 119: noFeedbackModeEntered, 120: ], 121: ) 122: const handleCycleMode = useCallback(() => { 123: const sessionOption = options.find(o => o.option.type === 'accept-session') 124: if (sessionOption) { 125: const parsedInput = parseInput(toolUseConfirm.input) 126: onChange(sessionOption.option, parsedInput) 127: } 128: }, [options, parseInput, toolUseConfirm.input, onChange]) 129: useKeybindings( 130: { 'confirm:cycleMode': handleCycleMode }, 131: { context: 'Confirmation' }, 132: ) 133: const handleFocusedOptionChange = useCallback( 134: (value: string) => { 135: if (value !== 'yes' && yesInputMode && !acceptFeedback.trim()) { 136: setYesInputMode(false) 137: } 138: if (value !== 'no' && noInputMode && !rejectFeedback.trim()) { 139: setNoInputMode(false) 140: } 141: setFocusedOption(value) 142: }, 143: [yesInputMode, noInputMode, acceptFeedback, rejectFeedback], 144: ) 145: const handleInputModeToggle = useCallback( 146: (value: string) => { 147: const analyticsProps = { 148: toolName: sanitizeToolNameForAnalytics( 149: toolUseConfirm.tool.name, 150: ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 151: isMcp: toolUseConfirm.tool.isMcp ?? false, 152: } 153: if (value === 'yes') { 154: if (yesInputMode) { 155: setYesInputMode(false) 156: logEvent('tengu_accept_feedback_mode_collapsed', analyticsProps) 157: } else { 158: setYesInputMode(true) 159: setYesFeedbackModeEntered(true) 160: logEvent('tengu_accept_feedback_mode_entered', analyticsProps) 161: } 162: } else if (value === 'no') { 163: if (noInputMode) { 164: setNoInputMode(false) 165: logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps) 166: } else { 167: setNoInputMode(true) 168: setNoFeedbackModeEntered(true) 169: logEvent('tengu_reject_feedback_mode_entered', analyticsProps) 170: } 171: } 172: }, 173: [yesInputMode, noInputMode, toolUseConfirm], 174: ) 175: return { 176: options, 177: onChange, 178: acceptFeedback, 179: rejectFeedback, 180: focusedOption, 181: setFocusedOption: handleFocusedOptionChange, 182: handleInputModeToggle, 183: yesInputMode, 184: noInputMode, 185: } 186: }

File: src/components/permissions/FilePermissionDialog/usePermissionHandler.ts

typescript 1: import { 2: type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 3: logEvent, 4: } from '../../../services/analytics/index.js' 5: import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js' 6: import type { ToolPermissionContext } from '../../../Tool.js' 7: import { 8: CLAUDE_FOLDER_PERMISSION_PATTERN, 9: FILE_EDIT_TOOL_NAME, 10: GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN, 11: } from '../../../tools/FileEditTool/constants.js' 12: import { env } from '../../../utils/env.js' 13: import { generateSuggestions } from '../../../utils/permissions/filesystem.js' 14: import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js' 15: import { 16: type CompletionType, 17: logUnaryEvent, 18: } from '../../../utils/unaryLogging.js' 19: import type { ToolUseConfirm } from '../PermissionRequest.js' 20: import type { 21: FileOperationType, 22: PermissionOption, 23: } from './permissionOptions.js' 24: function logPermissionEvent( 25: event: 'accept' | 'reject', 26: completionType: CompletionType, 27: languageName: string | Promise<string>, 28: messageId: string, 29: hasFeedback?: boolean, 30: ): void { 31: void logUnaryEvent({ 32: completion_type: completionType, 33: event, 34: metadata: { 35: language_name: languageName, 36: message_id: messageId, 37: platform: env.platform, 38: hasFeedback: hasFeedback ?? false, 39: }, 40: }) 41: } 42: export type PermissionHandlerParams = { 43: messageId: string 44: path: string | null 45: toolUseConfirm: ToolUseConfirm 46: toolPermissionContext: ToolPermissionContext 47: onDone: () => void 48: onReject: () => void 49: completionType: CompletionType 50: languageName: string | Promise<string> 51: operationType: FileOperationType 52: } 53: export type PermissionHandlerOptions = { 54: hasFeedback?: boolean 55: feedback?: string 56: enteredFeedbackMode?: boolean 57: scope?: 'claude-folder' | 'global-claude-folder' 58: } 59: function handleAcceptOnce( 60: params: PermissionHandlerParams, 61: options?: PermissionHandlerOptions, 62: ): void { 63: const { messageId, toolUseConfirm, onDone, completionType, languageName } = 64: params 65: logPermissionEvent('accept', completionType, languageName, messageId) 66: logEvent('tengu_accept_submitted', { 67: toolName: sanitizeToolNameForAnalytics( 68: toolUseConfirm.tool.name, 69: ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 70: isMcp: toolUseConfirm.tool.isMcp ?? false, 71: has_instructions: !!options?.feedback, 72: instructions_length: options?.feedback?.length ?? 0, 73: entered_feedback_mode: options?.enteredFeedbackMode ?? false, 74: }) 75: onDone() 76: toolUseConfirm.onAllow(toolUseConfirm.input, [], options?.feedback) 77: } 78: function handleAcceptSession( 79: params: PermissionHandlerParams, 80: options?: PermissionHandlerOptions, 81: ): void { 82: const { 83: messageId, 84: path, 85: toolUseConfirm, 86: toolPermissionContext, 87: onDone, 88: completionType, 89: languageName, 90: operationType, 91: } = params 92: logPermissionEvent('accept', completionType, languageName, messageId) 93: if ( 94: options?.scope === 'claude-folder' || 95: options?.scope === 'global-claude-folder' 96: ) { 97: const pattern = 98: options.scope === 'global-claude-folder' 99: ? GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN 100: : CLAUDE_FOLDER_PERMISSION_PATTERN 101: const suggestions: PermissionUpdate[] = [ 102: { 103: type: 'addRules', 104: rules: [ 105: { 106: toolName: FILE_EDIT_TOOL_NAME, 107: ruleContent: pattern, 108: }, 109: ], 110: behavior: 'allow', 111: destination: 'session', 112: }, 113: ] 114: onDone() 115: toolUseConfirm.onAllow(toolUseConfirm.input, suggestions) 116: return 117: } 118: const suggestions = path 119: ? generateSuggestions(path, operationType, toolPermissionContext) 120: : [] 121: onDone() 122: toolUseConfirm.onAllow(toolUseConfirm.input, suggestions) 123: } 124: function handleReject( 125: params: PermissionHandlerParams, 126: options?: PermissionHandlerOptions, 127: ): void { 128: const { 129: messageId, 130: toolUseConfirm, 131: onDone, 132: onReject, 133: completionType, 134: languageName, 135: } = params 136: logPermissionEvent( 137: 'reject', 138: completionType, 139: languageName, 140: messageId, 141: options?.hasFeedback, 142: ) 143: logEvent('tengu_reject_submitted', { 144: toolName: sanitizeToolNameForAnalytics( 145: toolUseConfirm.tool.name, 146: ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 147: isMcp: toolUseConfirm.tool.isMcp ?? false, 148: has_instructions: !!options?.feedback, 149: instructions_length: options?.feedback?.length ?? 0, 150: entered_feedback_mode: options?.enteredFeedbackMode ?? false, 151: }) 152: onDone() 153: onReject() 154: toolUseConfirm.onReject(options?.feedback) 155: } 156: export const PERMISSION_HANDLERS: Record< 157: PermissionOption['type'], 158: (params: PermissionHandlerParams, options?: PermissionHandlerOptions) => void 159: > = { 160: 'accept-once': handleAcceptOnce, 161: 'accept-session': handleAcceptSession, 162: reject: handleReject, 163: }

File: src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, Text, useTheme } from '../../../ink.js'; 4: import { FallbackPermissionRequest } from '../FallbackPermissionRequest.js'; 5: import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'; 6: import type { ToolInput } from '../FilePermissionDialog/useFilePermissionDialog.js'; 7: import type { PermissionRequestProps, ToolUseConfirm } from '../PermissionRequest.js'; 8: function pathFromToolUse(toolUseConfirm: ToolUseConfirm): string | null { 9: const tool = toolUseConfirm.tool; 10: if ('getPath' in tool && typeof tool.getPath === 'function') { 11: try { 12: return tool.getPath(toolUseConfirm.input); 13: } catch { 14: return null; 15: } 16: } 17: return null; 18: } 19: export function FilesystemPermissionRequest(t0) { 20: const $ = _c(30); 21: const { 22: toolUseConfirm, 23: onDone, 24: onReject, 25: verbose, 26: toolUseContext, 27: workerBadge 28: } = t0; 29: const [theme] = useTheme(); 30: let t1; 31: if ($[0] !== toolUseConfirm) { 32: t1 = pathFromToolUse(toolUseConfirm); 33: $[0] = toolUseConfirm; 34: $[1] = t1; 35: } else { 36: t1 = $[1]; 37: } 38: const path = t1; 39: let t2; 40: if ($[2] !== toolUseConfirm.input || $[3] !== toolUseConfirm.tool) { 41: t2 = toolUseConfirm.tool.userFacingName(toolUseConfirm.input as never); 42: $[2] = toolUseConfirm.input; 43: $[3] = toolUseConfirm.tool; 44: $[4] = t2; 45: } else { 46: t2 = $[4]; 47: } 48: const userFacingName = t2; 49: const isReadOnly = toolUseConfirm.tool.isReadOnly(toolUseConfirm.input); 50: const userFacingReadOrEdit = isReadOnly ? "Read" : "Edit"; 51: const title = `${userFacingReadOrEdit} file`; 52: const parseInput = _temp; 53: if (!path) { 54: let t3; 55: if ($[5] !== onDone || $[6] !== onReject || $[7] !== toolUseConfirm || $[8] !== toolUseContext || $[9] !== verbose || $[10] !== workerBadge) { 56: t3 = <FallbackPermissionRequest toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} />; 57: $[5] = onDone; 58: $[6] = onReject; 59: $[7] = toolUseConfirm; 60: $[8] = toolUseContext; 61: $[9] = verbose; 62: $[10] = workerBadge; 63: $[11] = t3; 64: } else { 65: t3 = $[11]; 66: } 67: return t3; 68: } 69: let t3; 70: if ($[12] !== theme || $[13] !== toolUseConfirm.input || $[14] !== toolUseConfirm.tool || $[15] !== verbose) { 71: t3 = toolUseConfirm.tool.renderToolUseMessage(toolUseConfirm.input as never, { 72: theme, 73: verbose 74: }); 75: $[12] = theme; 76: $[13] = toolUseConfirm.input; 77: $[14] = toolUseConfirm.tool; 78: $[15] = verbose; 79: $[16] = t3; 80: } else { 81: t3 = $[16]; 82: } 83: let t4; 84: if ($[17] !== t3 || $[18] !== userFacingName) { 85: t4 = <Box flexDirection="column" paddingX={2} paddingY={1}><Text>{userFacingName}({t3})</Text></Box>; 86: $[17] = t3; 87: $[18] = userFacingName; 88: $[19] = t4; 89: } else { 90: t4 = $[19]; 91: } 92: const content = t4; 93: const t5 = isReadOnly ? "read" : "write"; 94: let t6; 95: if ($[20] !== content || $[21] !== onDone || $[22] !== onReject || $[23] !== path || $[24] !== t5 || $[25] !== title || $[26] !== toolUseConfirm || $[27] !== toolUseContext || $[28] !== workerBadge) { 96: t6 = <FilePermissionDialog toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} workerBadge={workerBadge} title={title} content={content} path={path} parseInput={parseInput} operationType={t5} completionType="tool_use_single" />; 97: $[20] = content; 98: $[21] = onDone; 99: $[22] = onReject; 100: $[23] = path; 101: $[24] = t5; 102: $[25] = title; 103: $[26] = toolUseConfirm; 104: $[27] = toolUseContext; 105: $[28] = workerBadge; 106: $[29] = t6; 107: } else { 108: t6 = $[29]; 109: } 110: return t6; 111: } 112: function _temp(input) { 113: return input as ToolInput; 114: }

File: src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { basename, relative } from 'path'; 3: import React, { useMemo } from 'react'; 4: import type { z } from 'zod/v4'; 5: import { Text } from '../../../ink.js'; 6: import { FileWriteTool } from '../../../tools/FileWriteTool/FileWriteTool.js'; 7: import { getCwd } from '../../../utils/cwd.js'; 8: import { isENOENT } from '../../../utils/errors.js'; 9: import { readFileSync } from '../../../utils/fileRead.js'; 10: import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'; 11: import { createSingleEditDiffConfig, type FileEdit, type IDEDiffSupport } from '../FilePermissionDialog/ideDiffConfig.js'; 12: import type { PermissionRequestProps } from '../PermissionRequest.js'; 13: import { FileWriteToolDiff } from './FileWriteToolDiff.js'; 14: type FileWriteToolInput = z.infer<typeof FileWriteTool.inputSchema>; 15: const ideDiffSupport: IDEDiffSupport<FileWriteToolInput> = { 16: getConfig: (input: FileWriteToolInput) => { 17: let oldContent: string; 18: try { 19: oldContent = readFileSync(input.file_path); 20: } catch (e) { 21: if (!isENOENT(e)) throw e; 22: oldContent = ''; 23: } 24: return createSingleEditDiffConfig(input.file_path, oldContent, input.content, false 25: ); 26: }, 27: applyChanges: (input: FileWriteToolInput, modifiedEdits: FileEdit[]) => { 28: const firstEdit = modifiedEdits[0]; 29: if (firstEdit) { 30: return { 31: ...input, 32: content: firstEdit.new_string 33: }; 34: } 35: return input; 36: } 37: }; 38: export function FileWritePermissionRequest(props) { 39: const $ = _c(30); 40: const parseInput = _temp; 41: let t0; 42: if ($[0] !== props.toolUseConfirm.input) { 43: t0 = parseInput(props.toolUseConfirm.input); 44: $[0] = props.toolUseConfirm.input; 45: $[1] = t0; 46: } else { 47: t0 = $[1]; 48: } 49: const parsed = t0; 50: const { 51: file_path, 52: content 53: } = parsed; 54: let t1; 55: if ($[2] !== file_path) { 56: ; 57: try { 58: t1 = { 59: fileExists: true, 60: oldContent: readFileSync(file_path) 61: }; 62: } catch (t2) { 63: const e = t2; 64: if (!isENOENT(e)) { 65: throw e; 66: } 67: let t3; 68: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 69: t3 = { 70: fileExists: false, 71: oldContent: "" 72: }; 73: $[4] = t3; 74: } else { 75: t3 = $[4]; 76: } 77: t1 = t3; 78: } 79: $[2] = file_path; 80: $[3] = t1; 81: } else { 82: t1 = $[3]; 83: } 84: const { 85: fileExists, 86: oldContent 87: } = t1; 88: const actionText = fileExists ? "overwrite" : "create"; 89: const t2 = props.toolUseConfirm; 90: const t3 = props.toolUseContext; 91: const t4 = props.onDone; 92: const t5 = props.onReject; 93: const t6 = props.workerBadge; 94: const t7 = fileExists ? "Overwrite file" : "Create file"; 95: let t8; 96: if ($[5] !== file_path) { 97: t8 = relative(getCwd(), file_path); 98: $[5] = file_path; 99: $[6] = t8; 100: } else { 101: t8 = $[6]; 102: } 103: let t9; 104: if ($[7] !== file_path) { 105: t9 = basename(file_path); 106: $[7] = file_path; 107: $[8] = t9; 108: } else { 109: t9 = $[8]; 110: } 111: let t10; 112: if ($[9] !== t9) { 113: t10 = <Text bold={true}>{t9}</Text>; 114: $[9] = t9; 115: $[10] = t10; 116: } else { 117: t10 = $[10]; 118: } 119: let t11; 120: if ($[11] !== actionText || $[12] !== t10) { 121: t11 = <Text>Do you want to {actionText} {t10}?</Text>; 122: $[11] = actionText; 123: $[12] = t10; 124: $[13] = t11; 125: } else { 126: t11 = $[13]; 127: } 128: let t12; 129: if ($[14] !== content || $[15] !== fileExists || $[16] !== file_path || $[17] !== oldContent) { 130: t12 = <FileWriteToolDiff file_path={file_path} content={content} fileExists={fileExists} oldContent={oldContent} />; 131: $[14] = content; 132: $[15] = fileExists; 133: $[16] = file_path; 134: $[17] = oldContent; 135: $[18] = t12; 136: } else { 137: t12 = $[18]; 138: } 139: let t13; 140: if ($[19] !== file_path || $[20] !== props.onDone || $[21] !== props.onReject || $[22] !== props.toolUseConfirm || $[23] !== props.toolUseContext || $[24] !== props.workerBadge || $[25] !== t11 || $[26] !== t12 || $[27] !== t7 || $[28] !== t8) { 141: t13 = <FilePermissionDialog toolUseConfirm={t2} toolUseContext={t3} onDone={t4} onReject={t5} workerBadge={t6} title={t7} subtitle={t8} question={t11} content={t12} path={file_path} completionType="write_file_single" parseInput={parseInput} ideDiffSupport={ideDiffSupport} />; 142: $[19] = file_path; 143: $[20] = props.onDone; 144: $[21] = props.onReject; 145: $[22] = props.toolUseConfirm; 146: $[23] = props.toolUseContext; 147: $[24] = props.workerBadge; 148: $[25] = t11; 149: $[26] = t12; 150: $[27] = t7; 151: $[28] = t8; 152: $[29] = t13; 153: } else { 154: t13 = $[29]; 155: } 156: return t13; 157: } 158: function _temp(input) { 159: return FileWriteTool.inputSchema.parse(input); 160: }

File: src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useMemo } from 'react'; 4: import { useTerminalSize } from '../../../hooks/useTerminalSize.js'; 5: import { Box, NoSelect, Text } from '../../../ink.js'; 6: import { intersperse } from '../../../utils/array.js'; 7: import { getPatchForDisplay } from '../../../utils/diff.js'; 8: import { HighlightedCode } from '../../HighlightedCode.js'; 9: import { StructuredDiff } from '../../StructuredDiff.js'; 10: type Props = { 11: file_path: string; 12: content: string; 13: fileExists: boolean; 14: oldContent: string; 15: }; 16: export function FileWriteToolDiff(t0) { 17: const $ = _c(15); 18: const { 19: file_path, 20: content, 21: fileExists, 22: oldContent 23: } = t0; 24: const { 25: columns 26: } = useTerminalSize(); 27: let t1; 28: bb0: { 29: if (!fileExists) { 30: t1 = null; 31: break bb0; 32: } 33: let t2; 34: if ($[0] !== content || $[1] !== file_path || $[2] !== oldContent) { 35: t2 = getPatchForDisplay({ 36: filePath: file_path, 37: fileContents: oldContent, 38: edits: [{ 39: old_string: oldContent, 40: new_string: content, 41: replace_all: false 42: }] 43: }); 44: $[0] = content; 45: $[1] = file_path; 46: $[2] = oldContent; 47: $[3] = t2; 48: } else { 49: t2 = $[3]; 50: } 51: t1 = t2; 52: } 53: const hunks = t1; 54: let t2; 55: if ($[4] !== content) { 56: t2 = content.split("\n")[0] ?? null; 57: $[4] = content; 58: $[5] = t2; 59: } else { 60: t2 = $[5]; 61: } 62: const firstLine = t2; 63: let t3; 64: if ($[6] !== columns || $[7] !== content || $[8] !== file_path || $[9] !== firstLine || $[10] !== hunks || $[11] !== oldContent) { 65: t3 = hunks ? intersperse(hunks.map(_ => <StructuredDiff key={_.newStart} patch={_} dim={false} filePath={file_path} firstLine={firstLine} fileContent={oldContent} width={columns - 2} />), _temp) : <HighlightedCode code={content || "(No content)"} filePath={file_path} />; 66: $[6] = columns; 67: $[7] = content; 68: $[8] = file_path; 69: $[9] = firstLine; 70: $[10] = hunks; 71: $[11] = oldContent; 72: $[12] = t3; 73: } else { 74: t3 = $[12]; 75: } 76: let t4; 77: if ($[13] !== t3) { 78: t4 = <Box flexDirection="column"><Box borderColor="subtle" borderStyle="dashed" flexDirection="column" borderLeft={false} borderRight={false} paddingX={1}>{t3}</Box></Box>; 79: $[13] = t3; 80: $[14] = t4; 81: } else { 82: t4 = $[14]; 83: } 84: return t4; 85: } 86: function _temp(i) { 87: return <NoSelect fromLeftEdge={true} key={`ellipsis-${i}`}><Text dimColor={true}>...</Text></NoSelect>; 88: }

File: src/components/permissions/NotebookEditPermissionRequest/NotebookEditPermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { basename } from 'path'; 3: import React from 'react'; 4: import type { z } from 'zod/v4'; 5: import { Text } from '../../../ink.js'; 6: import { NotebookEditTool } from '../../../tools/NotebookEditTool/NotebookEditTool.js'; 7: import { logError } from '../../../utils/log.js'; 8: import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'; 9: import type { PermissionRequestProps } from '../PermissionRequest.js'; 10: import { NotebookEditToolDiff } from './NotebookEditToolDiff.js'; 11: type NotebookEditInput = z.infer<typeof NotebookEditTool.inputSchema>; 12: export function NotebookEditPermissionRequest(props) { 13: const $ = _c(52); 14: const parseInput = _temp; 15: let T0; 16: let T1; 17: let T2; 18: let language; 19: let notebook_path; 20: let parsed; 21: let t0; 22: let t1; 23: let t10; 24: let t2; 25: let t3; 26: let t4; 27: let t5; 28: let t6; 29: let t7; 30: let t8; 31: let t9; 32: if ($[0] !== props.onDone || $[1] !== props.onReject || $[2] !== props.toolUseConfirm || $[3] !== props.toolUseContext || $[4] !== props.workerBadge) { 33: parsed = parseInput(props.toolUseConfirm.input); 34: const { 35: notebook_path: t11, 36: edit_mode, 37: cell_type 38: } = parsed; 39: notebook_path = t11; 40: language = cell_type === "markdown" ? "markdown" : "python"; 41: const editTypeText = edit_mode === "insert" ? "insert this cell into" : edit_mode === "delete" ? "delete this cell from" : "make this edit to"; 42: T2 = FilePermissionDialog; 43: t5 = props.toolUseConfirm; 44: t6 = props.toolUseContext; 45: t7 = props.onDone; 46: t8 = props.onReject; 47: t9 = props.workerBadge; 48: t10 = "Edit notebook"; 49: T1 = Text; 50: t2 = "Do you want to "; 51: t3 = editTypeText; 52: t4 = " "; 53: T0 = Text; 54: t0 = true; 55: t1 = basename(notebook_path); 56: $[0] = props.onDone; 57: $[1] = props.onReject; 58: $[2] = props.toolUseConfirm; 59: $[3] = props.toolUseContext; 60: $[4] = props.workerBadge; 61: $[5] = T0; 62: $[6] = T1; 63: $[7] = T2; 64: $[8] = language; 65: $[9] = notebook_path; 66: $[10] = parsed; 67: $[11] = t0; 68: $[12] = t1; 69: $[13] = t10; 70: $[14] = t2; 71: $[15] = t3; 72: $[16] = t4; 73: $[17] = t5; 74: $[18] = t6; 75: $[19] = t7; 76: $[20] = t8; 77: $[21] = t9; 78: } else { 79: T0 = $[5]; 80: T1 = $[6]; 81: T2 = $[7]; 82: language = $[8]; 83: notebook_path = $[9]; 84: parsed = $[10]; 85: t0 = $[11]; 86: t1 = $[12]; 87: t10 = $[13]; 88: t2 = $[14]; 89: t3 = $[15]; 90: t4 = $[16]; 91: t5 = $[17]; 92: t6 = $[18]; 93: t7 = $[19]; 94: t8 = $[20]; 95: t9 = $[21]; 96: } 97: let t11; 98: if ($[22] !== T0 || $[23] !== t0 || $[24] !== t1) { 99: t11 = <T0 bold={t0}>{t1}</T0>; 100: $[22] = T0; 101: $[23] = t0; 102: $[24] = t1; 103: $[25] = t11; 104: } else { 105: t11 = $[25]; 106: } 107: let t12; 108: if ($[26] !== T1 || $[27] !== t11 || $[28] !== t2 || $[29] !== t3 || $[30] !== t4) { 109: t12 = <T1>{t2}{t3}{t4}{t11}?</T1>; 110: $[26] = T1; 111: $[27] = t11; 112: $[28] = t2; 113: $[29] = t3; 114: $[30] = t4; 115: $[31] = t12; 116: } else { 117: t12 = $[31]; 118: } 119: const t13 = props.verbose ? 120 : 80; 120: let t14; 121: if ($[32] !== parsed.cell_id || $[33] !== parsed.cell_type || $[34] !== parsed.edit_mode || $[35] !== parsed.new_source || $[36] !== parsed.notebook_path || $[37] !== props.verbose || $[38] !== t13) { 122: t14 = <NotebookEditToolDiff notebook_path={parsed.notebook_path} cell_id={parsed.cell_id} new_source={parsed.new_source} cell_type={parsed.cell_type} edit_mode={parsed.edit_mode} verbose={props.verbose} width={t13} />; 123: $[32] = parsed.cell_id; 124: $[33] = parsed.cell_type; 125: $[34] = parsed.edit_mode; 126: $[35] = parsed.new_source; 127: $[36] = parsed.notebook_path; 128: $[37] = props.verbose; 129: $[38] = t13; 130: $[39] = t14; 131: } else { 132: t14 = $[39]; 133: } 134: let t15; 135: if ($[40] !== T2 || $[41] !== language || $[42] !== notebook_path || $[43] !== t10 || $[44] !== t12 || $[45] !== t14 || $[46] !== t5 || $[47] !== t6 || $[48] !== t7 || $[49] !== t8 || $[50] !== t9) { 136: t15 = <T2 toolUseConfirm={t5} toolUseContext={t6} onDone={t7} onReject={t8} workerBadge={t9} title={t10} question={t12} content={t14} path={notebook_path} completionType="tool_use_single" languageName={language} parseInput={parseInput} />; 137: $[40] = T2; 138: $[41] = language; 139: $[42] = notebook_path; 140: $[43] = t10; 141: $[44] = t12; 142: $[45] = t14; 143: $[46] = t5; 144: $[47] = t6; 145: $[48] = t7; 146: $[49] = t8; 147: $[50] = t9; 148: $[51] = t15; 149: } else { 150: t15 = $[51]; 151: } 152: return t15; 153: } 154: function _temp(input) { 155: const result = NotebookEditTool.inputSchema.safeParse(input); 156: if (!result.success) { 157: logError(new Error(`Failed to parse notebook edit input: ${result.error.message}`)); 158: return { 159: notebook_path: "", 160: new_source: "", 161: cell_id: "" 162: } as NotebookEditInput; 163: } 164: return result.data; 165: }

File: src/components/permissions/NotebookEditPermissionRequest/NotebookEditToolDiff.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { relative } from 'path'; 3: import * as React from 'react'; 4: import { Suspense, use, useMemo } from 'react'; 5: import { Box, NoSelect, Text } from '../../../ink.js'; 6: import type { NotebookCellType, NotebookContent } from '../../../types/notebook.js'; 7: import { intersperse } from '../../../utils/array.js'; 8: import { getCwd } from '../../../utils/cwd.js'; 9: import { getPatchForDisplay } from '../../../utils/diff.js'; 10: import { getFsImplementation } from '../../../utils/fsOperations.js'; 11: import { safeParseJSON } from '../../../utils/json.js'; 12: import { parseCellId } from '../../../utils/notebook.js'; 13: import { HighlightedCode } from '../../HighlightedCode.js'; 14: import { StructuredDiff } from '../../StructuredDiff.js'; 15: type Props = { 16: notebook_path: string; 17: cell_id: string | undefined; 18: new_source: string; 19: cell_type?: NotebookCellType; 20: edit_mode?: string; 21: verbose: boolean; 22: width: number; 23: }; 24: type InnerProps = { 25: notebook_path: string; 26: cell_id: string | undefined; 27: new_source: string; 28: cell_type?: NotebookCellType; 29: edit_mode?: string; 30: verbose: boolean; 31: width: number; 32: promise: Promise<NotebookContent | null>; 33: }; 34: export function NotebookEditToolDiff(props) { 35: const $ = _c(5); 36: let t0; 37: if ($[0] !== props.notebook_path) { 38: t0 = getFsImplementation().readFile(props.notebook_path, { 39: encoding: "utf-8" 40: }).then(_temp).catch(_temp2); 41: $[0] = props.notebook_path; 42: $[1] = t0; 43: } else { 44: t0 = $[1]; 45: } 46: const notebookDataPromise = t0; 47: let t1; 48: if ($[2] !== notebookDataPromise || $[3] !== props) { 49: t1 = <Suspense fallback={null}><NotebookEditToolDiffInner {...props} promise={notebookDataPromise} /></Suspense>; 50: $[2] = notebookDataPromise; 51: $[3] = props; 52: $[4] = t1; 53: } else { 54: t1 = $[4]; 55: } 56: return t1; 57: } 58: function _temp2() { 59: return null; 60: } 61: function _temp(content) { 62: return safeParseJSON(content) as NotebookContent | null; 63: } 64: function NotebookEditToolDiffInner(t0) { 65: const $ = _c(34); 66: const { 67: notebook_path, 68: cell_id, 69: new_source, 70: cell_type, 71: edit_mode: t1, 72: verbose, 73: width, 74: promise 75: } = t0; 76: const edit_mode = t1 === undefined ? "replace" : t1; 77: const notebookData = use(promise); 78: let t2; 79: if ($[0] !== cell_id || $[1] !== notebookData) { 80: bb0: { 81: if (!notebookData || !cell_id) { 82: t2 = ""; 83: break bb0; 84: } 85: const cellIndex = parseCellId(cell_id); 86: if (cellIndex !== undefined) { 87: if (notebookData.cells[cellIndex]) { 88: const source = notebookData.cells[cellIndex].source; 89: let t3; 90: if ($[3] !== source) { 91: t3 = Array.isArray(source) ? source.join("") : source; 92: $[3] = source; 93: $[4] = t3; 94: } else { 95: t3 = $[4]; 96: } 97: t2 = t3; 98: break bb0; 99: } 100: t2 = ""; 101: break bb0; 102: } 103: let t3; 104: if ($[5] !== cell_id) { 105: t3 = cell => cell.id === cell_id; 106: $[5] = cell_id; 107: $[6] = t3; 108: } else { 109: t3 = $[6]; 110: } 111: const cell_0 = notebookData.cells.find(t3); 112: if (!cell_0) { 113: t2 = ""; 114: break bb0; 115: } 116: t2 = Array.isArray(cell_0.source) ? cell_0.source.join("") : cell_0.source; 117: } 118: $[0] = cell_id; 119: $[1] = notebookData; 120: $[2] = t2; 121: } else { 122: t2 = $[2]; 123: } 124: const oldSource = t2; 125: let t3; 126: bb1: { 127: if (!notebookData || edit_mode === "insert" || edit_mode === "delete") { 128: t3 = null; 129: break bb1; 130: } 131: let t4; 132: if ($[7] !== new_source || $[8] !== notebook_path || $[9] !== oldSource) { 133: t4 = getPatchForDisplay({ 134: filePath: notebook_path, 135: fileContents: oldSource, 136: edits: [{ 137: old_string: oldSource, 138: new_string: new_source, 139: replace_all: false 140: }], 141: ignoreWhitespace: false 142: }); 143: $[7] = new_source; 144: $[8] = notebook_path; 145: $[9] = oldSource; 146: $[10] = t4; 147: } else { 148: t4 = $[10]; 149: } 150: t3 = t4; 151: } 152: const hunks = t3; 153: let editTypeDescription; 154: bb2: switch (edit_mode) { 155: case "insert": 156: { 157: editTypeDescription = "Insert new cell"; 158: break bb2; 159: } 160: case "delete": 161: { 162: editTypeDescription = "Delete cell"; 163: break bb2; 164: } 165: default: 166: { 167: editTypeDescription = "Replace cell contents"; 168: } 169: } 170: let t4; 171: if ($[11] !== notebook_path || $[12] !== verbose) { 172: t4 = verbose ? notebook_path : relative(getCwd(), notebook_path); 173: $[11] = notebook_path; 174: $[12] = verbose; 175: $[13] = t4; 176: } else { 177: t4 = $[13]; 178: } 179: let t5; 180: if ($[14] !== t4) { 181: t5 = <Text bold={true}>{t4}</Text>; 182: $[14] = t4; 183: $[15] = t5; 184: } else { 185: t5 = $[15]; 186: } 187: const t6 = cell_type ? ` (${cell_type})` : ""; 188: let t7; 189: if ($[16] !== cell_id || $[17] !== editTypeDescription || $[18] !== t6) { 190: t7 = <Text dimColor={true}>{editTypeDescription} for cell {cell_id}{t6}</Text>; 191: $[16] = cell_id; 192: $[17] = editTypeDescription; 193: $[18] = t6; 194: $[19] = t7; 195: } else { 196: t7 = $[19]; 197: } 198: let t8; 199: if ($[20] !== t5 || $[21] !== t7) { 200: t8 = <Box paddingBottom={1} flexDirection="column">{t5}{t7}</Box>; 201: $[20] = t5; 202: $[21] = t7; 203: $[22] = t8; 204: } else { 205: t8 = $[22]; 206: } 207: let t9; 208: if ($[23] !== cell_type || $[24] !== edit_mode || $[25] !== hunks || $[26] !== new_source || $[27] !== notebook_path || $[28] !== oldSource || $[29] !== width) { 209: t9 = edit_mode === "delete" ? <Box flexDirection="column" paddingLeft={2}><HighlightedCode code={oldSource} filePath={notebook_path} /></Box> : edit_mode === "insert" ? <Box flexDirection="column" paddingLeft={2}><HighlightedCode code={new_source} filePath={cell_type === "markdown" ? "file.md" : notebook_path} /></Box> : hunks ? intersperse(hunks.map(_ => <StructuredDiff key={_.newStart} patch={_} dim={false} width={width} filePath={notebook_path} firstLine={new_source.split("\n")[0] ?? null} fileContent={oldSource} />), _temp3) : <HighlightedCode code={new_source} filePath={cell_type === "markdown" ? "file.md" : notebook_path} />; 210: $[23] = cell_type; 211: $[24] = edit_mode; 212: $[25] = hunks; 213: $[26] = new_source; 214: $[27] = notebook_path; 215: $[28] = oldSource; 216: $[29] = width; 217: $[30] = t9; 218: } else { 219: t9 = $[30]; 220: } 221: let t10; 222: if ($[31] !== t8 || $[32] !== t9) { 223: t10 = <Box flexDirection="column"><Box borderStyle="round" flexDirection="column" paddingX={1}>{t8}{t9}</Box></Box>; 224: $[31] = t8; 225: $[32] = t9; 226: $[33] = t10; 227: } else { 228: t10 = $[33]; 229: } 230: return t10; 231: } 232: function _temp3(i) { 233: return <NoSelect fromLeftEdge={true} key={`ellipsis-${i}`}><Text dimColor={true}>...</Text></NoSelect>; 234: }

File: src/components/permissions/PowerShellPermissionRequest/PowerShellPermissionRequest.tsx

typescript 1: import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 2: import { Box, Text, useTheme } from '../../../ink.js'; 3: import { useKeybinding } from '../../../keybindings/useKeybinding.js'; 4: import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js'; 5: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js'; 6: import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'; 7: import { getDestructiveCommandWarning } from '../../../tools/PowerShellTool/destructiveCommandWarning.js'; 8: import { PowerShellTool } from '../../../tools/PowerShellTool/PowerShellTool.js'; 9: import { isAllowlistedCommand } from '../../../tools/PowerShellTool/readOnlyValidation.js'; 10: import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'; 11: import { getCompoundCommandPrefixesStatic } from '../../../utils/powershell/staticPrefix.js'; 12: import { Select } from '../../CustomSelect/select.js'; 13: import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'; 14: import { PermissionDecisionDebugInfo } from '../PermissionDecisionDebugInfo.js'; 15: import { PermissionDialog } from '../PermissionDialog.js'; 16: import { PermissionExplainerContent, usePermissionExplainerUI } from '../PermissionExplanation.js'; 17: import type { PermissionRequestProps } from '../PermissionRequest.js'; 18: import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'; 19: import { useShellPermissionFeedback } from '../useShellPermissionFeedback.js'; 20: import { logUnaryPermissionEvent } from '../utils.js'; 21: import { powershellToolUseOptions } from './powershellToolUseOptions.js'; 22: export function PowerShellPermissionRequest(props: PermissionRequestProps): React.ReactNode { 23: const { 24: toolUseConfirm, 25: toolUseContext, 26: onDone, 27: onReject, 28: workerBadge 29: } = props; 30: const { 31: command, 32: description 33: } = PowerShellTool.inputSchema.parse(toolUseConfirm.input); 34: const [theme] = useTheme(); 35: const explainerState = usePermissionExplainerUI({ 36: toolName: toolUseConfirm.tool.name, 37: toolInput: toolUseConfirm.input, 38: toolDescription: toolUseConfirm.description, 39: messages: toolUseContext.messages 40: }); 41: const { 42: yesInputMode, 43: noInputMode, 44: yesFeedbackModeEntered, 45: noFeedbackModeEntered, 46: acceptFeedback, 47: rejectFeedback, 48: setAcceptFeedback, 49: setRejectFeedback, 50: focusedOption, 51: handleInputModeToggle, 52: handleReject, 53: handleFocus 54: } = useShellPermissionFeedback({ 55: toolUseConfirm, 56: onDone, 57: onReject, 58: explainerVisible: explainerState.visible 59: }); 60: const destructiveWarning = getFeatureValue_CACHED_MAY_BE_STALE('tengu_destructive_command_warning', false) ? getDestructiveCommandWarning(command) : null; 61: const [showPermissionDebug, setShowPermissionDebug] = useState(false); 62: const [editablePrefix, setEditablePrefix] = useState<string | undefined>(command.includes('\n') ? undefined : command); 63: const hasUserEditedPrefix = useRef(false); 64: useEffect(() => { 65: let cancelled = false; 66: getCompoundCommandPrefixesStatic(command, element => isAllowlistedCommand(element, element.text)).then(prefixes => { 67: if (cancelled || hasUserEditedPrefix.current) return; 68: if (prefixes.length > 0) { 69: setEditablePrefix(`${prefixes[0]}:*`); 70: } 71: }).catch(() => {}); 72: return () => { 73: cancelled = true; 74: }; 75: }, [command]); 76: const onEditablePrefixChange = useCallback((value: string) => { 77: hasUserEditedPrefix.current = true; 78: setEditablePrefix(value); 79: }, []); 80: const unaryEvent = useMemo<UnaryEvent>(() => ({ 81: completion_type: 'tool_use_single', 82: language_name: 'none' 83: }), []); 84: usePermissionRequestLogging(toolUseConfirm, unaryEvent); 85: const options = useMemo(() => powershellToolUseOptions({ 86: suggestions: toolUseConfirm.permissionResult.behavior === 'ask' ? toolUseConfirm.permissionResult.suggestions : undefined, 87: onRejectFeedbackChange: setRejectFeedback, 88: onAcceptFeedbackChange: setAcceptFeedback, 89: yesInputMode, 90: noInputMode, 91: editablePrefix, 92: onEditablePrefixChange 93: }), [toolUseConfirm, yesInputMode, noInputMode, editablePrefix, onEditablePrefixChange]); 94: const handleToggleDebug = useCallback(() => { 95: setShowPermissionDebug(prev => !prev); 96: }, []); 97: useKeybinding('permission:toggleDebug', handleToggleDebug, { 98: context: 'Confirmation' 99: }); 100: function onSelect(value: string) { 101: const optionIndex: Record<string, number> = { 102: yes: 1, 103: 'yes-apply-suggestions': 2, 104: 'yes-prefix-edited': 2, 105: no: 3 106: }; 107: logEvent('tengu_permission_request_option_selected', { 108: option_index: optionIndex[value], 109: explainer_visible: explainerState.visible 110: }); 111: const toolNameForAnalytics = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS; 112: if (value === 'yes-prefix-edited') { 113: const trimmedPrefix = (editablePrefix ?? '').trim(); 114: logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept'); 115: if (!trimmedPrefix) { 116: toolUseConfirm.onAllow(toolUseConfirm.input, []); 117: } else { 118: const prefixUpdates: PermissionUpdate[] = [{ 119: type: 'addRules', 120: rules: [{ 121: toolName: PowerShellTool.name, 122: ruleContent: trimmedPrefix 123: }], 124: behavior: 'allow', 125: destination: 'localSettings' 126: }]; 127: toolUseConfirm.onAllow(toolUseConfirm.input, prefixUpdates); 128: } 129: onDone(); 130: return; 131: } 132: switch (value) { 133: case 'yes': 134: { 135: const trimmedFeedback = acceptFeedback.trim(); 136: logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept'); 137: logEvent('tengu_accept_submitted', { 138: toolName: toolNameForAnalytics, 139: isMcp: toolUseConfirm.tool.isMcp ?? false, 140: has_instructions: !!trimmedFeedback, 141: instructions_length: trimmedFeedback.length, 142: entered_feedback_mode: yesFeedbackModeEntered 143: }); 144: toolUseConfirm.onAllow(toolUseConfirm.input, [], trimmedFeedback || undefined); 145: onDone(); 146: break; 147: } 148: case 'yes-apply-suggestions': 149: { 150: logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept'); 151: const permissionUpdates = 'suggestions' in toolUseConfirm.permissionResult ? toolUseConfirm.permissionResult.suggestions || [] : []; 152: toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates); 153: onDone(); 154: break; 155: } 156: case 'no': 157: { 158: const trimmedFeedback = rejectFeedback.trim(); 159: logEvent('tengu_reject_submitted', { 160: toolName: toolNameForAnalytics, 161: isMcp: toolUseConfirm.tool.isMcp ?? false, 162: has_instructions: !!trimmedFeedback, 163: instructions_length: trimmedFeedback.length, 164: entered_feedback_mode: noFeedbackModeEntered 165: }); 166: handleReject(trimmedFeedback || undefined); 167: break; 168: } 169: } 170: } 171: return <PermissionDialog workerBadge={workerBadge} title="PowerShell command"> 172: <Box flexDirection="column" paddingX={2} paddingY={1}> 173: <Text dimColor={explainerState.visible}> 174: {PowerShellTool.renderToolUseMessage({ 175: command, 176: description 177: }, { 178: theme, 179: verbose: true 180: } 181: )} 182: </Text> 183: {!explainerState.visible && <Text dimColor>{toolUseConfirm.description}</Text>} 184: <PermissionExplainerContent visible={explainerState.visible} promise={explainerState.promise} /> 185: </Box> 186: {showPermissionDebug ? <> 187: <PermissionDecisionDebugInfo permissionResult={toolUseConfirm.permissionResult} toolName="PowerShell" /> 188: {toolUseContext.options.debug && <Box justifyContent="flex-end" marginTop={1}> 189: <Text dimColor>Ctrl-D to hide debug info</Text> 190: </Box>} 191: </> : <> 192: <Box flexDirection="column"> 193: <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="command" /> 194: {destructiveWarning && <Box marginBottom={1}> 195: <Text color="warning">{destructiveWarning}</Text> 196: </Box>} 197: <Text>Do you want to proceed?</Text> 198: <Select options={options} inlineDescriptions onChange={onSelect} onCancel={() => handleReject()} onFocus={handleFocus} onInputModeToggle={handleInputModeToggle} /> 199: </Box> 200: <Box justifyContent="space-between" marginTop={1}> 201: <Text dimColor> 202: Esc to cancel 203: {(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'} 204: {explainerState.enabled && ` · ctrl+e to ${explainerState.visible ? 'hide' : 'explain'}`} 205: </Text> 206: {toolUseContext.options.debug && <Text dimColor>Ctrl+d to show debug info</Text>} 207: </Box> 208: </>} 209: </PermissionDialog>; 210: }

File: src/components/permissions/PowerShellPermissionRequest/powershellToolUseOptions.tsx

typescript 1: import { POWERSHELL_TOOL_NAME } from '../../../tools/PowerShellTool/toolName.js'; 2: import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'; 3: import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'; 4: import type { OptionWithDescription } from '../../CustomSelect/select.js'; 5: import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js'; 6: export type PowerShellToolUseOption = 'yes' | 'yes-apply-suggestions' | 'yes-prefix-edited' | 'no'; 7: export function powershellToolUseOptions({ 8: suggestions = [], 9: onRejectFeedbackChange, 10: onAcceptFeedbackChange, 11: yesInputMode = false, 12: noInputMode = false, 13: editablePrefix, 14: onEditablePrefixChange 15: }: { 16: suggestions?: PermissionUpdate[]; 17: onRejectFeedbackChange: (value: string) => void; 18: onAcceptFeedbackChange: (value: string) => void; 19: yesInputMode?: boolean; 20: noInputMode?: boolean; 21: editablePrefix?: string; 22: onEditablePrefixChange?: (value: string) => void; 23: }): OptionWithDescription<PowerShellToolUseOption>[] { 24: const options: OptionWithDescription<PowerShellToolUseOption>[] = []; 25: if (yesInputMode) { 26: options.push({ 27: type: 'input', 28: label: 'Yes', 29: value: 'yes', 30: placeholder: 'and tell Claude what to do next', 31: onChange: onAcceptFeedbackChange, 32: allowEmptySubmitToCancel: true 33: }); 34: } else { 35: options.push({ 36: label: 'Yes', 37: value: 'yes' 38: }); 39: } 40: if (shouldShowAlwaysAllowOptions() && suggestions.length > 0) { 41: const hasNonPowerShellSuggestions = suggestions.some(s => s.type === 'addDirectories' || s.type === 'addRules' && s.rules?.some(r => r.toolName !== POWERSHELL_TOOL_NAME)); 42: if (editablePrefix !== undefined && onEditablePrefixChange && !hasNonPowerShellSuggestions) { 43: options.push({ 44: type: 'input', 45: label: 'Yes, and don\u2019t ask again for', 46: value: 'yes-prefix-edited', 47: placeholder: 'command prefix (e.g., Get-Process:*)', 48: initialValue: editablePrefix, 49: onChange: onEditablePrefixChange, 50: allowEmptySubmitToCancel: true, 51: showLabelWithValue: true, 52: labelValueSeparator: ': ', 53: resetCursorOnUpdate: true 54: }); 55: } else { 56: const label = generateShellSuggestionsLabel(suggestions, POWERSHELL_TOOL_NAME); 57: if (label) { 58: options.push({ 59: label, 60: value: 'yes-apply-suggestions' 61: }); 62: } 63: } 64: } 65: if (noInputMode) { 66: options.push({ 67: type: 'input', 68: label: 'No', 69: value: 'no', 70: placeholder: 'and tell Claude what to do differently', 71: onChange: onRejectFeedbackChange, 72: allowEmptySubmitToCancel: true 73: }); 74: } else { 75: options.push({ 76: label: 'No', 77: value: 'no' 78: }); 79: } 80: return options; 81: }

File: src/components/permissions/rules/AddPermissionRules.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useCallback } from 'react'; 4: import { Select } from '../../../components/CustomSelect/select.js'; 5: import { Box, Text } from '../../../ink.js'; 6: import type { ToolPermissionContext } from '../../../Tool.js'; 7: import type { PermissionBehavior, PermissionRule, PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js'; 8: import { applyPermissionUpdate, persistPermissionUpdate } from '../../../utils/permissions/PermissionUpdate.js'; 9: import { permissionRuleValueToString } from '../../../utils/permissions/permissionRuleParser.js'; 10: import { detectUnreachableRules, type UnreachableRule } from '../../../utils/permissions/shadowedRuleDetection.js'; 11: import { SandboxManager } from '../../../utils/sandbox/sandbox-adapter.js'; 12: import { type EditableSettingSource, SOURCES } from '../../../utils/settings/constants.js'; 13: import { getRelativeSettingsFilePathForSource } from '../../../utils/settings/settings.js'; 14: import { plural } from '../../../utils/stringUtils.js'; 15: import type { OptionWithDescription } from '../../CustomSelect/select.js'; 16: import { Dialog } from '../../design-system/Dialog.js'; 17: import { PermissionRuleDescription } from './PermissionRuleDescription.js'; 18: export function optionForPermissionSaveDestination(saveDestination: EditableSettingSource): OptionWithDescription { 19: switch (saveDestination) { 20: case 'localSettings': 21: return { 22: label: 'Project settings (local)', 23: description: `Saved in ${getRelativeSettingsFilePathForSource('localSettings')}`, 24: value: saveDestination 25: }; 26: case 'projectSettings': 27: return { 28: label: 'Project settings', 29: description: `Checked in at ${getRelativeSettingsFilePathForSource('projectSettings')}`, 30: value: saveDestination 31: }; 32: case 'userSettings': 33: return { 34: label: 'User settings', 35: description: `Saved in at ~/.claude/settings.json`, 36: value: saveDestination 37: }; 38: } 39: } 40: type Props = { 41: onAddRules: (rules: PermissionRule[], unreachable?: UnreachableRule[]) => void; 42: onCancel: () => void; 43: ruleValues: PermissionRuleValue[]; 44: ruleBehavior: PermissionBehavior; 45: initialContext: ToolPermissionContext; 46: setToolPermissionContext: (newContext: ToolPermissionContext) => void; 47: }; 48: export function AddPermissionRules(t0) { 49: const $ = _c(26); 50: const { 51: onAddRules, 52: onCancel, 53: ruleValues, 54: ruleBehavior, 55: initialContext, 56: setToolPermissionContext 57: } = t0; 58: let t1; 59: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 60: t1 = SOURCES.map(optionForPermissionSaveDestination); 61: $[0] = t1; 62: } else { 63: t1 = $[0]; 64: } 65: const allOptions = t1; 66: let t2; 67: if ($[1] !== initialContext || $[2] !== onAddRules || $[3] !== onCancel || $[4] !== ruleBehavior || $[5] !== ruleValues || $[6] !== setToolPermissionContext) { 68: t2 = selectedValue => { 69: if (selectedValue === "cancel") { 70: onCancel(); 71: return; 72: } else { 73: if ((SOURCES as readonly string[]).includes(selectedValue)) { 74: const destination = selectedValue as EditableSettingSource; 75: const updatedContext = applyPermissionUpdate(initialContext, { 76: type: "addRules", 77: rules: ruleValues, 78: behavior: ruleBehavior, 79: destination 80: }); 81: persistPermissionUpdate({ 82: type: "addRules", 83: rules: ruleValues, 84: behavior: ruleBehavior, 85: destination 86: }); 87: setToolPermissionContext(updatedContext); 88: const rules = ruleValues.map(ruleValue => ({ 89: ruleValue, 90: ruleBehavior, 91: source: destination 92: })); 93: const sandboxAutoAllowEnabled = SandboxManager.isSandboxingEnabled() && SandboxManager.isAutoAllowBashIfSandboxedEnabled(); 94: const allUnreachable = detectUnreachableRules(updatedContext, { 95: sandboxAutoAllowEnabled 96: }); 97: const newUnreachable = allUnreachable.filter(u => ruleValues.some(rv => rv.toolName === u.rule.ruleValue.toolName && rv.ruleContent === u.rule.ruleValue.ruleContent)); 98: onAddRules(rules, newUnreachable.length > 0 ? newUnreachable : undefined); 99: } 100: } 101: }; 102: $[1] = initialContext; 103: $[2] = onAddRules; 104: $[3] = onCancel; 105: $[4] = ruleBehavior; 106: $[5] = ruleValues; 107: $[6] = setToolPermissionContext; 108: $[7] = t2; 109: } else { 110: t2 = $[7]; 111: } 112: const onSelect = t2; 113: let t3; 114: if ($[8] !== ruleValues.length) { 115: t3 = plural(ruleValues.length, "rule"); 116: $[8] = ruleValues.length; 117: $[9] = t3; 118: } else { 119: t3 = $[9]; 120: } 121: const title = `Add ${ruleBehavior} permission ${t3}`; 122: let t4; 123: if ($[10] !== ruleValues) { 124: t4 = ruleValues.map(_temp); 125: $[10] = ruleValues; 126: $[11] = t4; 127: } else { 128: t4 = $[11]; 129: } 130: let t5; 131: if ($[12] !== t4) { 132: t5 = <Box flexDirection="column" paddingX={2}>{t4}</Box>; 133: $[12] = t4; 134: $[13] = t5; 135: } else { 136: t5 = $[13]; 137: } 138: const t6 = ruleValues.length === 1 ? "Where should this rule be saved?" : "Where should these rules be saved?"; 139: let t7; 140: if ($[14] !== t6) { 141: t7 = <Text>{t6}</Text>; 142: $[14] = t6; 143: $[15] = t7; 144: } else { 145: t7 = $[15]; 146: } 147: let t8; 148: if ($[16] !== onSelect) { 149: t8 = <Select options={allOptions} onChange={onSelect} />; 150: $[16] = onSelect; 151: $[17] = t8; 152: } else { 153: t8 = $[17]; 154: } 155: let t9; 156: if ($[18] !== t7 || $[19] !== t8) { 157: t9 = <Box flexDirection="column" marginY={1}>{t7}{t8}</Box>; 158: $[18] = t7; 159: $[19] = t8; 160: $[20] = t9; 161: } else { 162: t9 = $[20]; 163: } 164: let t10; 165: if ($[21] !== onCancel || $[22] !== t5 || $[23] !== t9 || $[24] !== title) { 166: t10 = <Dialog title={title} onCancel={onCancel} color="permission">{t5}{t9}</Dialog>; 167: $[21] = onCancel; 168: $[22] = t5; 169: $[23] = t9; 170: $[24] = title; 171: $[25] = t10; 172: } else { 173: t10 = $[25]; 174: } 175: return t10; 176: } 177: function _temp(ruleValue_0) { 178: return <Box flexDirection="column" key={permissionRuleValueToString(ruleValue_0)}><Text bold={true}>{permissionRuleValueToString(ruleValue_0)}</Text><PermissionRuleDescription ruleValue={ruleValue_0} /></Box>; 179: }

File: src/components/permissions/rules/AddWorkspaceDirectory.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { useCallback, useEffect, useMemo, useState } from 'react'; 5: import { useDebounceCallback } from 'usehooks-ts'; 6: import { addDirHelpMessage, validateDirectoryForWorkspace } from '../../../commands/add-dir/validation.js'; 7: import TextInput from '../../../components/TextInput.js'; 8: import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'; 9: import { Box, Text } from '../../../ink.js'; 10: import { useKeybinding } from '../../../keybindings/useKeybinding.js'; 11: import type { ToolPermissionContext } from '../../../Tool.js'; 12: import { getDirectoryCompletions } from '../../../utils/suggestions/directoryCompletion.js'; 13: import { ConfigurableShortcutHint } from '../../ConfigurableShortcutHint.js'; 14: import { Select } from '../../CustomSelect/select.js'; 15: import { Byline } from '../../design-system/Byline.js'; 16: import { Dialog } from '../../design-system/Dialog.js'; 17: import { KeyboardShortcutHint } from '../../design-system/KeyboardShortcutHint.js'; 18: import { PromptInputFooterSuggestions, type SuggestionItem } from '../../PromptInput/PromptInputFooterSuggestions.js'; 19: type Props = { 20: onAddDirectory: (path: string, remember?: boolean) => void; 21: onCancel: () => void; 22: permissionContext: ToolPermissionContext; 23: directoryPath?: string; 24: }; 25: type RememberDirectoryOption = 'yes-session' | 'yes-remember' | 'no'; 26: const REMEMBER_DIRECTORY_OPTIONS: Array<{ 27: value: RememberDirectoryOption; 28: label: string; 29: }> = [{ 30: value: 'yes-session', 31: label: 'Yes, for this session' 32: }, { 33: value: 'yes-remember', 34: label: 'Yes, and remember this directory' 35: }, { 36: value: 'no', 37: label: 'No' 38: }]; 39: function PermissionDescription() { 40: const $ = _c(1); 41: let t0; 42: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 43: t0 = <Text dimColor={true}>Claude Code will be able to read files in this directory and make edits when auto-accept edits is on.</Text>; 44: $[0] = t0; 45: } else { 46: t0 = $[0]; 47: } 48: return t0; 49: } 50: function DirectoryDisplay(t0) { 51: const $ = _c(5); 52: const { 53: path 54: } = t0; 55: let t1; 56: if ($[0] !== path) { 57: t1 = <Text color="permission">{path}</Text>; 58: $[0] = path; 59: $[1] = t1; 60: } else { 61: t1 = $[1]; 62: } 63: let t2; 64: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 65: t2 = <PermissionDescription />; 66: $[2] = t2; 67: } else { 68: t2 = $[2]; 69: } 70: let t3; 71: if ($[3] !== t1) { 72: t3 = <Box flexDirection="column" paddingX={2} gap={1}>{t1}{t2}</Box>; 73: $[3] = t1; 74: $[4] = t3; 75: } else { 76: t3 = $[4]; 77: } 78: return t3; 79: } 80: function DirectoryInput(t0) { 81: const $ = _c(14); 82: const { 83: value, 84: onChange, 85: onSubmit, 86: error, 87: suggestions, 88: selectedSuggestion 89: } = t0; 90: let t1; 91: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 92: t1 = <Text>Enter the path to the directory:</Text>; 93: $[0] = t1; 94: } else { 95: t1 = $[0]; 96: } 97: let t2; 98: if ($[1] !== onChange || $[2] !== onSubmit || $[3] !== value) { 99: t2 = <Box borderDimColor={true} borderStyle="round" marginY={1} paddingLeft={1}><TextInput showCursor={true} placeholder={`Directory path${figures.ellipsis}`} value={value} onChange={onChange} onSubmit={onSubmit} columns={80} cursorOffset={value.length} onChangeCursorOffset={_temp} /></Box>; 100: $[1] = onChange; 101: $[2] = onSubmit; 102: $[3] = value; 103: $[4] = t2; 104: } else { 105: t2 = $[4]; 106: } 107: let t3; 108: if ($[5] !== selectedSuggestion || $[6] !== suggestions) { 109: t3 = suggestions.length > 0 && <Box marginBottom={1}><PromptInputFooterSuggestions suggestions={suggestions} selectedSuggestion={selectedSuggestion} /></Box>; 110: $[5] = selectedSuggestion; 111: $[6] = suggestions; 112: $[7] = t3; 113: } else { 114: t3 = $[7]; 115: } 116: let t4; 117: if ($[8] !== error) { 118: t4 = error && <Text color="error">{error}</Text>; 119: $[8] = error; 120: $[9] = t4; 121: } else { 122: t4 = $[9]; 123: } 124: let t5; 125: if ($[10] !== t2 || $[11] !== t3 || $[12] !== t4) { 126: t5 = <Box flexDirection="column">{t1}{t2}{t3}{t4}</Box>; 127: $[10] = t2; 128: $[11] = t3; 129: $[12] = t4; 130: $[13] = t5; 131: } else { 132: t5 = $[13]; 133: } 134: return t5; 135: } 136: function _temp() {} 137: export function AddWorkspaceDirectory(t0) { 138: const $ = _c(34); 139: const { 140: onAddDirectory, 141: onCancel, 142: permissionContext, 143: directoryPath 144: } = t0; 145: const [directoryInput, setDirectoryInput] = useState(""); 146: const [error, setError] = useState(null); 147: let t1; 148: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 149: t1 = []; 150: $[0] = t1; 151: } else { 152: t1 = $[0]; 153: } 154: const [suggestions, setSuggestions] = useState(t1); 155: const [selectedSuggestion, setSelectedSuggestion] = useState(0); 156: let t2; 157: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 158: t2 = async path => { 159: if (!path) { 160: setSuggestions([]); 161: setSelectedSuggestion(0); 162: return; 163: } 164: const completions = await getDirectoryCompletions(path); 165: setSuggestions(completions); 166: setSelectedSuggestion(0); 167: }; 168: $[1] = t2; 169: } else { 170: t2 = $[1]; 171: } 172: const fetchSuggestions = t2; 173: const debouncedFetchSuggestions = useDebounceCallback(fetchSuggestions, 100); 174: let t3; 175: let t4; 176: if ($[2] !== debouncedFetchSuggestions || $[3] !== directoryInput) { 177: t3 = () => { 178: debouncedFetchSuggestions(directoryInput); 179: }; 180: t4 = [directoryInput, debouncedFetchSuggestions]; 181: $[2] = debouncedFetchSuggestions; 182: $[3] = directoryInput; 183: $[4] = t3; 184: $[5] = t4; 185: } else { 186: t3 = $[4]; 187: t4 = $[5]; 188: } 189: useEffect(t3, t4); 190: let t5; 191: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 192: t5 = suggestion => { 193: const newPath = suggestion.id + "/"; 194: setDirectoryInput(newPath); 195: setError(null); 196: }; 197: $[6] = t5; 198: } else { 199: t5 = $[6]; 200: } 201: const applySuggestion = t5; 202: let t6; 203: if ($[7] !== onAddDirectory || $[8] !== permissionContext) { 204: t6 = async newPath_0 => { 205: const result = await validateDirectoryForWorkspace(newPath_0, permissionContext); 206: if (result.resultType === "success") { 207: onAddDirectory(result.absolutePath, false); 208: } else { 209: setError(addDirHelpMessage(result)); 210: } 211: }; 212: $[7] = onAddDirectory; 213: $[8] = permissionContext; 214: $[9] = t6; 215: } else { 216: t6 = $[9]; 217: } 218: const handleSubmit = t6; 219: let t7; 220: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 221: t7 = { 222: context: "Settings" 223: }; 224: $[10] = t7; 225: } else { 226: t7 = $[10]; 227: } 228: useKeybinding("confirm:no", onCancel, t7); 229: let t8; 230: if ($[11] !== handleSubmit || $[12] !== selectedSuggestion || $[13] !== suggestions) { 231: t8 = e => { 232: if (suggestions.length > 0) { 233: if (e.key === "tab") { 234: e.preventDefault(); 235: const suggestion_0 = suggestions[selectedSuggestion]; 236: if (suggestion_0) { 237: applySuggestion(suggestion_0); 238: } 239: return; 240: } 241: if (e.key === "return") { 242: e.preventDefault(); 243: const suggestion_1 = suggestions[selectedSuggestion]; 244: if (suggestion_1) { 245: handleSubmit(suggestion_1.id + "/"); 246: } 247: return; 248: } 249: if (e.key === "up" || e.ctrl && e.key === "p") { 250: e.preventDefault(); 251: setSelectedSuggestion(prev => prev <= 0 ? suggestions.length - 1 : prev - 1); 252: return; 253: } 254: if (e.key === "down" || e.ctrl && e.key === "n") { 255: e.preventDefault(); 256: setSelectedSuggestion(prev_0 => prev_0 >= suggestions.length - 1 ? 0 : prev_0 + 1); 257: return; 258: } 259: } 260: }; 261: $[11] = handleSubmit; 262: $[12] = selectedSuggestion; 263: $[13] = suggestions; 264: $[14] = t8; 265: } else { 266: t8 = $[14]; 267: } 268: const handleKeyDown = t8; 269: let t9; 270: if ($[15] !== directoryPath || $[16] !== onAddDirectory || $[17] !== onCancel) { 271: t9 = value => { 272: if (!directoryPath) { 273: return; 274: } 275: const selectionValue = value as RememberDirectoryOption; 276: bb64: switch (selectionValue) { 277: case "yes-session": 278: { 279: onAddDirectory(directoryPath, false); 280: break bb64; 281: } 282: case "yes-remember": 283: { 284: onAddDirectory(directoryPath, true); 285: break bb64; 286: } 287: case "no": 288: { 289: onCancel(); 290: } 291: } 292: }; 293: $[15] = directoryPath; 294: $[16] = onAddDirectory; 295: $[17] = onCancel; 296: $[18] = t9; 297: } else { 298: t9 = $[18]; 299: } 300: const handleSelect = t9; 301: const t10 = directoryPath ? undefined : _temp2; 302: let t11; 303: if ($[19] !== directoryInput || $[20] !== directoryPath || $[21] !== error || $[22] !== handleSelect || $[23] !== handleSubmit || $[24] !== selectedSuggestion || $[25] !== suggestions) { 304: t11 = directoryPath ? <Box flexDirection="column" gap={1}><DirectoryDisplay path={directoryPath} /><Select options={REMEMBER_DIRECTORY_OPTIONS} onChange={handleSelect} onCancel={() => handleSelect("no")} /></Box> : <Box flexDirection="column" gap={1} marginX={2}><PermissionDescription /><DirectoryInput value={directoryInput} onChange={setDirectoryInput} onSubmit={handleSubmit} error={error} suggestions={suggestions} selectedSuggestion={selectedSuggestion} /></Box>; 305: $[19] = directoryInput; 306: $[20] = directoryPath; 307: $[21] = error; 308: $[22] = handleSelect; 309: $[23] = handleSubmit; 310: $[24] = selectedSuggestion; 311: $[25] = suggestions; 312: $[26] = t11; 313: } else { 314: t11 = $[26]; 315: } 316: let t12; 317: if ($[27] !== onCancel || $[28] !== t10 || $[29] !== t11) { 318: t12 = <Dialog title="Add directory to workspace" onCancel={onCancel} color="permission" isCancelActive={false} inputGuide={t10}>{t11}</Dialog>; 319: $[27] = onCancel; 320: $[28] = t10; 321: $[29] = t11; 322: $[30] = t12; 323: } else { 324: t12 = $[30]; 325: } 326: let t13; 327: if ($[31] !== handleKeyDown || $[32] !== t12) { 328: t13 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t12}</Box>; 329: $[31] = handleKeyDown; 330: $[32] = t12; 331: $[33] = t13; 332: } else { 333: t13 = $[33]; 334: } 335: return t13; 336: } 337: function _temp2(exitState) { 338: return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut="Tab" action="complete" /><KeyboardShortcutHint shortcut="Enter" action="add" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" /></Byline>; 339: }

File: src/components/permissions/rules/PermissionRuleDescription.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Text } from '../../../ink.js'; 4: import { BashTool } from '../../../tools/BashTool/BashTool.js'; 5: import type { PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js'; 6: type RuleSubtitleProps = { 7: ruleValue: PermissionRuleValue; 8: }; 9: export function PermissionRuleDescription(t0) { 10: const $ = _c(9); 11: const { 12: ruleValue 13: } = t0; 14: switch (ruleValue.toolName) { 15: case BashTool.name: 16: { 17: if (ruleValue.ruleContent) { 18: if (ruleValue.ruleContent.endsWith(":*")) { 19: let t1; 20: if ($[0] !== ruleValue.ruleContent) { 21: t1 = ruleValue.ruleContent.slice(0, -2); 22: $[0] = ruleValue.ruleContent; 23: $[1] = t1; 24: } else { 25: t1 = $[1]; 26: } 27: let t2; 28: if ($[2] !== t1) { 29: t2 = <Text dimColor={true}>Any Bash command starting with{" "}<Text bold={true}>{t1}</Text></Text>; 30: $[2] = t1; 31: $[3] = t2; 32: } else { 33: t2 = $[3]; 34: } 35: return t2; 36: } else { 37: let t1; 38: if ($[4] !== ruleValue.ruleContent) { 39: t1 = <Text dimColor={true}>The Bash command <Text bold={true}>{ruleValue.ruleContent}</Text></Text>; 40: $[4] = ruleValue.ruleContent; 41: $[5] = t1; 42: } else { 43: t1 = $[5]; 44: } 45: return t1; 46: } 47: } else { 48: let t1; 49: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 50: t1 = <Text dimColor={true}>Any Bash command</Text>; 51: $[6] = t1; 52: } else { 53: t1 = $[6]; 54: } 55: return t1; 56: } 57: } 58: default: 59: { 60: if (!ruleValue.ruleContent) { 61: let t1; 62: if ($[7] !== ruleValue.toolName) { 63: t1 = <Text dimColor={true}>Any use of the <Text bold={true}>{ruleValue.toolName}</Text> tool</Text>; 64: $[7] = ruleValue.toolName; 65: $[8] = t1; 66: } else { 67: t1 = $[8]; 68: } 69: return t1; 70: } else { 71: return null; 72: } 73: } 74: } 75: }

File: src/components/permissions/rules/PermissionRuleInput.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { useState } from 'react'; 5: import TextInput from '../../../components/TextInput.js'; 6: import { useExitOnCtrlCDWithKeybindings } from '../../../hooks/useExitOnCtrlCDWithKeybindings.js'; 7: import { useTerminalSize } from '../../../hooks/useTerminalSize.js'; 8: import { Box, Newline, Text } from '../../../ink.js'; 9: import { useKeybinding } from '../../../keybindings/useKeybinding.js'; 10: import { BashTool } from '../../../tools/BashTool/BashTool.js'; 11: import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js'; 12: import type { PermissionBehavior, PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js'; 13: import { permissionRuleValueFromString, permissionRuleValueToString } from '../../../utils/permissions/permissionRuleParser.js'; 14: export type PermissionRuleInputProps = { 15: onCancel: () => void; 16: onSubmit: (ruleValue: PermissionRuleValue, ruleBehavior: PermissionBehavior) => void; 17: ruleBehavior: PermissionBehavior; 18: }; 19: export function PermissionRuleInput(t0) { 20: const $ = _c(24); 21: const { 22: onCancel, 23: onSubmit, 24: ruleBehavior 25: } = t0; 26: const [inputValue, setInputValue] = useState(""); 27: const [cursorOffset, setCursorOffset] = useState(0); 28: const exitState = useExitOnCtrlCDWithKeybindings(); 29: let t1; 30: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 31: t1 = { 32: context: "Settings" 33: }; 34: $[0] = t1; 35: } else { 36: t1 = $[0]; 37: } 38: useKeybinding("confirm:no", onCancel, t1); 39: const { 40: columns 41: } = useTerminalSize(); 42: const textInputColumns = columns - 6; 43: let t2; 44: if ($[1] !== onSubmit || $[2] !== ruleBehavior) { 45: t2 = value => { 46: const trimmedValue = value.trim(); 47: if (trimmedValue.length === 0) { 48: return; 49: } 50: const ruleValue = permissionRuleValueFromString(trimmedValue); 51: onSubmit(ruleValue, ruleBehavior); 52: }; 53: $[1] = onSubmit; 54: $[2] = ruleBehavior; 55: $[3] = t2; 56: } else { 57: t2 = $[3]; 58: } 59: const handleSubmit = t2; 60: let t3; 61: if ($[4] !== ruleBehavior) { 62: t3 = <Text bold={true} color="permission">Add {ruleBehavior} permission rule</Text>; 63: $[4] = ruleBehavior; 64: $[5] = t3; 65: } else { 66: t3 = $[5]; 67: } 68: let t4; 69: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 70: t4 = <Newline />; 71: $[6] = t4; 72: } else { 73: t4 = $[6]; 74: } 75: let t5; 76: let t6; 77: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 78: t5 = <Text bold={true}>{permissionRuleValueToString({ 79: toolName: WebFetchTool.name 80: })}</Text>; 81: t6 = <Text bold={false}> or </Text>; 82: $[7] = t5; 83: $[8] = t6; 84: } else { 85: t5 = $[7]; 86: t6 = $[8]; 87: } 88: let t7; 89: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 90: t7 = <Text>Permission rules are a tool name, optionally followed by a specifier in parentheses.{t4}e.g.,{" "}{t5}{t6}<Text bold={true}>{permissionRuleValueToString({ 91: toolName: BashTool.name, 92: ruleContent: "ls:*" 93: })}</Text></Text>; 94: $[9] = t7; 95: } else { 96: t7 = $[9]; 97: } 98: let t8; 99: if ($[10] !== cursorOffset || $[11] !== handleSubmit || $[12] !== inputValue || $[13] !== textInputColumns) { 100: t8 = <Box flexDirection="column">{t7}<Box borderDimColor={true} borderStyle="round" marginY={1} paddingLeft={1}><TextInput showCursor={true} value={inputValue} onChange={setInputValue} onSubmit={handleSubmit} placeholder={`Enter permission rule${figures.ellipsis}`} columns={textInputColumns} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /></Box></Box>; 101: $[10] = cursorOffset; 102: $[11] = handleSubmit; 103: $[12] = inputValue; 104: $[13] = textInputColumns; 105: $[14] = t8; 106: } else { 107: t8 = $[14]; 108: } 109: let t9; 110: if ($[15] !== t3 || $[16] !== t8) { 111: t9 = <Box flexDirection="column" gap={1} borderStyle="round" paddingLeft={1} paddingRight={1} borderColor="permission">{t3}{t8}</Box>; 112: $[15] = t3; 113: $[16] = t8; 114: $[17] = t9; 115: } else { 116: t9 = $[17]; 117: } 118: let t10; 119: if ($[18] !== exitState.keyName || $[19] !== exitState.pending) { 120: t10 = <Box marginLeft={3}>{exitState.pending ? <Text dimColor={true}>Press {exitState.keyName} again to exit</Text> : <Text dimColor={true}>Enter to submit · Esc to cancel</Text>}</Box>; 121: $[18] = exitState.keyName; 122: $[19] = exitState.pending; 123: $[20] = t10; 124: } else { 125: t10 = $[20]; 126: } 127: let t11; 128: if ($[21] !== t10 || $[22] !== t9) { 129: t11 = <>{t9}{t10}</>; 130: $[21] = t10; 131: $[22] = t9; 132: $[23] = t11; 133: } else { 134: t11 = $[23]; 135: } 136: return t11; 137: }

File: src/components/permissions/rules/PermissionRuleList.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import chalk from 'chalk'; 3: import figures from 'figures'; 4: import * as React from 'react'; 5: import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 6: import { useAppState, useSetAppState } from 'src/state/AppState.js'; 7: import { applyPermissionUpdate, persistPermissionUpdate } from 'src/utils/permissions/PermissionUpdate.js'; 8: import type { PermissionUpdateDestination } from 'src/utils/permissions/PermissionUpdateSchema.js'; 9: import type { CommandResultDisplay } from '../../../commands.js'; 10: import { Select } from '../../../components/CustomSelect/select.js'; 11: import { useExitOnCtrlCDWithKeybindings } from '../../../hooks/useExitOnCtrlCDWithKeybindings.js'; 12: import { useSearchInput } from '../../../hooks/useSearchInput.js'; 13: import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'; 14: import { Box, Text, useTerminalFocus } from '../../../ink.js'; 15: import { useKeybinding } from '../../../keybindings/useKeybinding.js'; 16: import { type AutoModeDenial, getAutoModeDenials } from '../../../utils/autoModeDenials.js'; 17: import type { PermissionBehavior, PermissionRule, PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js'; 18: import { permissionRuleValueToString } from '../../../utils/permissions/permissionRuleParser.js'; 19: import { deletePermissionRule, getAllowRules, getAskRules, getDenyRules, permissionRuleSourceDisplayString } from '../../../utils/permissions/permissions.js'; 20: import type { UnreachableRule } from '../../../utils/permissions/shadowedRuleDetection.js'; 21: import { jsonStringify } from '../../../utils/slowOperations.js'; 22: import { Pane } from '../../design-system/Pane.js'; 23: import { Tab, Tabs, useTabHeaderFocus, useTabsWidth } from '../../design-system/Tabs.js'; 24: import { SearchBox } from '../../SearchBox.js'; 25: import type { Option } from '../../ui/option.js'; 26: import { AddPermissionRules } from './AddPermissionRules.js'; 27: import { AddWorkspaceDirectory } from './AddWorkspaceDirectory.js'; 28: import { PermissionRuleDescription } from './PermissionRuleDescription.js'; 29: import { PermissionRuleInput } from './PermissionRuleInput.js'; 30: import { RecentDenialsTab } from './RecentDenialsTab.js'; 31: import { RemoveWorkspaceDirectory } from './RemoveWorkspaceDirectory.js'; 32: import { WorkspaceTab } from './WorkspaceTab.js'; 33: type TabType = 'recent' | 'allow' | 'ask' | 'deny' | 'workspace'; 34: type RuleSourceTextProps = { 35: rule: PermissionRule; 36: }; 37: function RuleSourceText(t0) { 38: const $ = _c(4); 39: const { 40: rule 41: } = t0; 42: let t1; 43: if ($[0] !== rule.source) { 44: t1 = permissionRuleSourceDisplayString(rule.source); 45: $[0] = rule.source; 46: $[1] = t1; 47: } else { 48: t1 = $[1]; 49: } 50: const t2 = `From ${t1}`; 51: let t3; 52: if ($[2] !== t2) { 53: t3 = <Text dimColor={true}>{t2}</Text>; 54: $[2] = t2; 55: $[3] = t3; 56: } else { 57: t3 = $[3]; 58: } 59: return t3; 60: } 61: function getRuleBehaviorLabel(ruleBehavior: PermissionBehavior): string { 62: switch (ruleBehavior) { 63: case 'allow': 64: return 'allowed'; 65: case 'deny': 66: return 'denied'; 67: case 'ask': 68: return 'ask'; 69: } 70: } 71: function RuleDetails(t0) { 72: const $ = _c(42); 73: const { 74: rule, 75: onDelete, 76: onCancel 77: } = t0; 78: const exitState = useExitOnCtrlCDWithKeybindings(); 79: let t1; 80: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 81: t1 = { 82: context: "Confirmation" 83: }; 84: $[0] = t1; 85: } else { 86: t1 = $[0]; 87: } 88: useKeybinding("confirm:no", onCancel, t1); 89: let t2; 90: if ($[1] !== rule.ruleValue) { 91: t2 = permissionRuleValueToString(rule.ruleValue); 92: $[1] = rule.ruleValue; 93: $[2] = t2; 94: } else { 95: t2 = $[2]; 96: } 97: let t3; 98: if ($[3] !== t2) { 99: t3 = <Text bold={true}>{t2}</Text>; 100: $[3] = t2; 101: $[4] = t3; 102: } else { 103: t3 = $[4]; 104: } 105: let t4; 106: if ($[5] !== rule.ruleValue) { 107: t4 = <PermissionRuleDescription ruleValue={rule.ruleValue} />; 108: $[5] = rule.ruleValue; 109: $[6] = t4; 110: } else { 111: t4 = $[6]; 112: } 113: let t5; 114: if ($[7] !== rule) { 115: t5 = <RuleSourceText rule={rule} />; 116: $[7] = rule; 117: $[8] = t5; 118: } else { 119: t5 = $[8]; 120: } 121: let t6; 122: if ($[9] !== t3 || $[10] !== t4 || $[11] !== t5) { 123: t6 = <Box flexDirection="column" marginX={2}>{t3}{t4}{t5}</Box>; 124: $[9] = t3; 125: $[10] = t4; 126: $[11] = t5; 127: $[12] = t6; 128: } else { 129: t6 = $[12]; 130: } 131: const ruleDescription = t6; 132: let t7; 133: if ($[13] !== exitState.keyName || $[14] !== exitState.pending) { 134: t7 = <Box marginLeft={3}>{exitState.pending ? <Text dimColor={true}>Press {exitState.keyName} again to exit</Text> : <Text dimColor={true}>Esc to cancel</Text>}</Box>; 135: $[13] = exitState.keyName; 136: $[14] = exitState.pending; 137: $[15] = t7; 138: } else { 139: t7 = $[15]; 140: } 141: const footer = t7; 142: if (rule.source === "policySettings") { 143: let t8; 144: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 145: t8 = <Text bold={true} color="permission">Rule details</Text>; 146: $[16] = t8; 147: } else { 148: t8 = $[16]; 149: } 150: let t9; 151: if ($[17] === Symbol.for("react.memo_cache_sentinel")) { 152: t9 = <Text italic={true}>This rule is configured by managed settings and cannot be modified.{"\n"}Contact your system administrator for more information.</Text>; 153: $[17] = t9; 154: } else { 155: t9 = $[17]; 156: } 157: let t10; 158: if ($[18] !== ruleDescription) { 159: t10 = <Box flexDirection="column" gap={1} borderStyle="round" paddingLeft={1} paddingRight={1} borderColor="permission">{t8}{ruleDescription}{t9}</Box>; 160: $[18] = ruleDescription; 161: $[19] = t10; 162: } else { 163: t10 = $[19]; 164: } 165: let t11; 166: if ($[20] !== footer || $[21] !== t10) { 167: t11 = <>{t10}{footer}</>; 168: $[20] = footer; 169: $[21] = t10; 170: $[22] = t11; 171: } else { 172: t11 = $[22]; 173: } 174: return t11; 175: } 176: let t8; 177: if ($[23] !== rule.ruleBehavior) { 178: t8 = getRuleBehaviorLabel(rule.ruleBehavior); 179: $[23] = rule.ruleBehavior; 180: $[24] = t8; 181: } else { 182: t8 = $[24]; 183: } 184: let t9; 185: if ($[25] !== t8) { 186: t9 = <Text bold={true} color="error">Delete {t8} tool?</Text>; 187: $[25] = t8; 188: $[26] = t9; 189: } else { 190: t9 = $[26]; 191: } 192: let t10; 193: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 194: t10 = <Text>Are you sure you want to delete this permission rule?</Text>; 195: $[27] = t10; 196: } else { 197: t10 = $[27]; 198: } 199: let t11; 200: if ($[28] !== onCancel || $[29] !== onDelete) { 201: t11 = _ => _ === "yes" ? onDelete() : onCancel(); 202: $[28] = onCancel; 203: $[29] = onDelete; 204: $[30] = t11; 205: } else { 206: t11 = $[30]; 207: } 208: let t12; 209: if ($[31] === Symbol.for("react.memo_cache_sentinel")) { 210: t12 = [{ 211: label: "Yes", 212: value: "yes" 213: }, { 214: label: "No", 215: value: "no" 216: }]; 217: $[31] = t12; 218: } else { 219: t12 = $[31]; 220: } 221: let t13; 222: if ($[32] !== onCancel || $[33] !== t11) { 223: t13 = <Select onChange={t11} onCancel={onCancel} options={t12} />; 224: $[32] = onCancel; 225: $[33] = t11; 226: $[34] = t13; 227: } else { 228: t13 = $[34]; 229: } 230: let t14; 231: if ($[35] !== ruleDescription || $[36] !== t13 || $[37] !== t9) { 232: t14 = <Box flexDirection="column" gap={1} borderStyle="round" paddingLeft={1} paddingRight={1} borderColor="error">{t9}{ruleDescription}{t10}{t13}</Box>; 233: $[35] = ruleDescription; 234: $[36] = t13; 235: $[37] = t9; 236: $[38] = t14; 237: } else { 238: t14 = $[38]; 239: } 240: let t15; 241: if ($[39] !== footer || $[40] !== t14) { 242: t15 = <>{t14}{footer}</>; 243: $[39] = footer; 244: $[40] = t14; 245: $[41] = t15; 246: } else { 247: t15 = $[41]; 248: } 249: return t15; 250: } 251: type RulesTabContentProps = { 252: options: Option[]; 253: searchQuery: string; 254: isSearchMode: boolean; 255: isFocused: boolean; 256: onSelect: (value: string) => void; 257: onCancel: () => void; 258: lastFocusedRuleKey: string | undefined; 259: cursorOffset?: number; 260: onHeaderFocusChange?: (focused: boolean) => void; 261: }; 262: function RulesTabContent(props) { 263: const $ = _c(26); 264: const { 265: options, 266: searchQuery, 267: isSearchMode, 268: isFocused, 269: onSelect, 270: onCancel, 271: lastFocusedRuleKey, 272: cursorOffset, 273: onHeaderFocusChange 274: } = props; 275: const tabWidth = useTabsWidth(); 276: const { 277: headerFocused, 278: focusHeader, 279: blurHeader 280: } = useTabHeaderFocus(); 281: let t0; 282: let t1; 283: if ($[0] !== blurHeader || $[1] !== headerFocused || $[2] !== isSearchMode) { 284: t0 = () => { 285: if (isSearchMode && headerFocused) { 286: blurHeader(); 287: } 288: }; 289: t1 = [isSearchMode, headerFocused, blurHeader]; 290: $[0] = blurHeader; 291: $[1] = headerFocused; 292: $[2] = isSearchMode; 293: $[3] = t0; 294: $[4] = t1; 295: } else { 296: t0 = $[3]; 297: t1 = $[4]; 298: } 299: useEffect(t0, t1); 300: let t2; 301: let t3; 302: if ($[5] !== headerFocused || $[6] !== onHeaderFocusChange) { 303: t2 = () => { 304: onHeaderFocusChange?.(headerFocused); 305: }; 306: t3 = [headerFocused, onHeaderFocusChange]; 307: $[5] = headerFocused; 308: $[6] = onHeaderFocusChange; 309: $[7] = t2; 310: $[8] = t3; 311: } else { 312: t2 = $[7]; 313: t3 = $[8]; 314: } 315: useEffect(t2, t3); 316: const t4 = isSearchMode && !headerFocused; 317: let t5; 318: if ($[9] !== cursorOffset || $[10] !== isFocused || $[11] !== searchQuery || $[12] !== t4 || $[13] !== tabWidth) { 319: t5 = <Box marginBottom={1} flexDirection="column"><SearchBox query={searchQuery} isFocused={t4} isTerminalFocused={isFocused} width={tabWidth} cursorOffset={cursorOffset} /></Box>; 320: $[9] = cursorOffset; 321: $[10] = isFocused; 322: $[11] = searchQuery; 323: $[12] = t4; 324: $[13] = tabWidth; 325: $[14] = t5; 326: } else { 327: t5 = $[14]; 328: } 329: const t6 = Math.min(10, options.length); 330: const t7 = isSearchMode || headerFocused; 331: let t8; 332: if ($[15] !== focusHeader || $[16] !== lastFocusedRuleKey || $[17] !== onCancel || $[18] !== onSelect || $[19] !== options || $[20] !== t6 || $[21] !== t7) { 333: t8 = <Select options={options} onChange={onSelect} onCancel={onCancel} visibleOptionCount={t6} isDisabled={t7} defaultFocusValue={lastFocusedRuleKey} onUpFromFirstItem={focusHeader} />; 334: $[15] = focusHeader; 335: $[16] = lastFocusedRuleKey; 336: $[17] = onCancel; 337: $[18] = onSelect; 338: $[19] = options; 339: $[20] = t6; 340: $[21] = t7; 341: $[22] = t8; 342: } else { 343: t8 = $[22]; 344: } 345: let t9; 346: if ($[23] !== t5 || $[24] !== t8) { 347: t9 = <Box flexDirection="column">{t5}{t8}</Box>; 348: $[23] = t5; 349: $[24] = t8; 350: $[25] = t9; 351: } else { 352: t9 = $[25]; 353: } 354: return t9; 355: } 356: function PermissionRulesTab(t0) { 357: const $ = _c(27); 358: let T0; 359: let T1; 360: let handleToolSelect; 361: let rulesProps; 362: let t1; 363: let t2; 364: let t3; 365: let t4; 366: let tab; 367: if ($[0] !== t0) { 368: const { 369: tab: t5, 370: getRulesOptions, 371: handleToolSelect: t6, 372: ...t7 373: } = t0; 374: tab = t5; 375: handleToolSelect = t6; 376: rulesProps = t7; 377: T1 = Box; 378: t2 = "column"; 379: t3 = tab === "allow" ? 0 : undefined; 380: let t8; 381: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 382: t8 = { 383: allow: "Claude Code won't ask before using allowed tools.", 384: ask: "Claude Code will always ask for confirmation before using these tools.", 385: deny: "Claude Code will always reject requests to use denied tools." 386: }; 387: $[10] = t8; 388: } else { 389: t8 = $[10]; 390: } 391: const t9 = t8[tab]; 392: if ($[11] !== t9) { 393: t4 = <Text>{t9}</Text>; 394: $[11] = t9; 395: $[12] = t4; 396: } else { 397: t4 = $[12]; 398: } 399: T0 = RulesTabContent; 400: t1 = getRulesOptions(tab, rulesProps.searchQuery); 401: $[0] = t0; 402: $[1] = T0; 403: $[2] = T1; 404: $[3] = handleToolSelect; 405: $[4] = rulesProps; 406: $[5] = t1; 407: $[6] = t2; 408: $[7] = t3; 409: $[8] = t4; 410: $[9] = tab; 411: } else { 412: T0 = $[1]; 413: T1 = $[2]; 414: handleToolSelect = $[3]; 415: rulesProps = $[4]; 416: t1 = $[5]; 417: t2 = $[6]; 418: t3 = $[7]; 419: t4 = $[8]; 420: tab = $[9]; 421: } 422: let t5; 423: if ($[13] !== handleToolSelect || $[14] !== tab) { 424: t5 = v => handleToolSelect(v, tab); 425: $[13] = handleToolSelect; 426: $[14] = tab; 427: $[15] = t5; 428: } else { 429: t5 = $[15]; 430: } 431: let t6; 432: if ($[16] !== T0 || $[17] !== rulesProps || $[18] !== t1.options || $[19] !== t5) { 433: t6 = <T0 options={t1.options} onSelect={t5} {...rulesProps} />; 434: $[16] = T0; 435: $[17] = rulesProps; 436: $[18] = t1.options; 437: $[19] = t5; 438: $[20] = t6; 439: } else { 440: t6 = $[20]; 441: } 442: let t7; 443: if ($[21] !== T1 || $[22] !== t2 || $[23] !== t3 || $[24] !== t4 || $[25] !== t6) { 444: t7 = <T1 flexDirection={t2} flexShrink={t3}>{t4}{t6}</T1>; 445: $[21] = T1; 446: $[22] = t2; 447: $[23] = t3; 448: $[24] = t4; 449: $[25] = t6; 450: $[26] = t7; 451: } else { 452: t7 = $[26]; 453: } 454: return t7; 455: } 456: type Props = { 457: onExit: (result?: string, options?: { 458: display?: CommandResultDisplay; 459: shouldQuery?: boolean; 460: metaMessages?: string[]; 461: }) => void; 462: initialTab?: TabType; 463: onRetryDenials?: (commands: string[]) => void; 464: }; 465: export function PermissionRuleList(t0) { 466: const $ = _c(113); 467: const { 468: onExit, 469: initialTab, 470: onRetryDenials 471: } = t0; 472: let t1; 473: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 474: t1 = getAutoModeDenials(); 475: $[0] = t1; 476: } else { 477: t1 = $[0]; 478: } 479: const hasDenials = t1.length > 0; 480: const defaultTab = initialTab ?? (hasDenials ? "recent" : "allow"); 481: let t2; 482: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 483: t2 = []; 484: $[1] = t2; 485: } else { 486: t2 = $[1]; 487: } 488: const [changes, setChanges] = useState(t2); 489: const toolPermissionContext = useAppState(_temp); 490: const setAppState = useSetAppState(); 491: const isTerminalFocused = useTerminalFocus(); 492: let t3; 493: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 494: t3 = { 495: approved: new Set(), 496: retry: new Set(), 497: denials: [] 498: }; 499: $[2] = t3; 500: } else { 501: t3 = $[2]; 502: } 503: const denialStateRef = useRef(t3); 504: let t4; 505: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 506: t4 = s_0 => { 507: denialStateRef.current = s_0; 508: }; 509: $[3] = t4; 510: } else { 511: t4 = $[3]; 512: } 513: const handleDenialStateChange = t4; 514: const [selectedRule, setSelectedRule] = useState(); 515: const [lastFocusedRuleKey, setLastFocusedRuleKey] = useState(); 516: const [addingRuleToTab, setAddingRuleToTab] = useState(null); 517: const [validatedRule, setValidatedRule] = useState(null); 518: const [isAddingWorkspaceDirectory, setIsAddingWorkspaceDirectory] = useState(false); 519: const [removingDirectory, setRemovingDirectory] = useState(null); 520: const [isSearchMode, setIsSearchMode] = useState(false); 521: const [headerFocused, setHeaderFocused] = useState(true); 522: let t5; 523: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 524: t5 = focused => { 525: setHeaderFocused(focused); 526: }; 527: $[4] = t5; 528: } else { 529: t5 = $[4]; 530: } 531: const handleHeaderFocusChange = t5; 532: let map; 533: if ($[5] !== toolPermissionContext) { 534: map = new Map(); 535: getAllowRules(toolPermissionContext).forEach(rule => { 536: map.set(jsonStringify(rule), rule); 537: }); 538: $[5] = toolPermissionContext; 539: $[6] = map; 540: } else { 541: map = $[6]; 542: } 543: const allowRulesByKey = map; 544: let map_0; 545: if ($[7] !== toolPermissionContext) { 546: map_0 = new Map(); 547: getDenyRules(toolPermissionContext).forEach(rule_0 => { 548: map_0.set(jsonStringify(rule_0), rule_0); 549: }); 550: $[7] = toolPermissionContext; 551: $[8] = map_0; 552: } else { 553: map_0 = $[8]; 554: } 555: const denyRulesByKey = map_0; 556: let map_1; 557: if ($[9] !== toolPermissionContext) { 558: map_1 = new Map(); 559: getAskRules(toolPermissionContext).forEach(rule_1 => { 560: map_1.set(jsonStringify(rule_1), rule_1); 561: }); 562: $[9] = toolPermissionContext; 563: $[10] = map_1; 564: } else { 565: map_1 = $[10]; 566: } 567: const askRulesByKey = map_1; 568: let t6; 569: if ($[11] !== allowRulesByKey || $[12] !== askRulesByKey || $[13] !== denyRulesByKey) { 570: t6 = (tab, t7) => { 571: const query = t7 === undefined ? "" : t7; 572: const rulesByKey = (() => { 573: switch (tab) { 574: case "allow": 575: { 576: return allowRulesByKey; 577: } 578: case "deny": 579: { 580: return denyRulesByKey; 581: } 582: case "ask": 583: { 584: return askRulesByKey; 585: } 586: case "workspace": 587: case "recent": 588: { 589: return new Map(); 590: } 591: } 592: })(); 593: const options = []; 594: if (tab !== "workspace" && tab !== "recent" && !query) { 595: options.push({ 596: label: `Add a new rule${figures.ellipsis}`, 597: value: "add-new-rule" 598: }); 599: } 600: const sortedRuleKeys = Array.from(rulesByKey.keys()).sort((a, b) => { 601: const ruleA = rulesByKey.get(a); 602: const ruleB = rulesByKey.get(b); 603: if (ruleA && ruleB) { 604: const ruleAString = permissionRuleValueToString(ruleA.ruleValue).toLowerCase(); 605: const ruleBString = permissionRuleValueToString(ruleB.ruleValue).toLowerCase(); 606: return ruleAString.localeCompare(ruleBString); 607: } 608: return 0; 609: }); 610: const lowerQuery = query.toLowerCase(); 611: for (const ruleKey of sortedRuleKeys) { 612: const rule_2 = rulesByKey.get(ruleKey); 613: if (rule_2) { 614: const ruleString = permissionRuleValueToString(rule_2.ruleValue); 615: if (query && !ruleString.toLowerCase().includes(lowerQuery)) { 616: continue; 617: } 618: options.push({ 619: label: ruleString, 620: value: ruleKey 621: }); 622: } 623: } 624: return { 625: options, 626: rulesByKey 627: }; 628: }; 629: $[11] = allowRulesByKey; 630: $[12] = askRulesByKey; 631: $[13] = denyRulesByKey; 632: $[14] = t6; 633: } else { 634: t6 = $[14]; 635: } 636: const getRulesOptions = t6; 637: const exitState = useExitOnCtrlCDWithKeybindings(); 638: const isSearchModeActive = !selectedRule && !addingRuleToTab && !validatedRule && !isAddingWorkspaceDirectory && !removingDirectory; 639: const t7 = isSearchModeActive && isSearchMode; 640: let t8; 641: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 642: t8 = () => { 643: setIsSearchMode(false); 644: }; 645: $[15] = t8; 646: } else { 647: t8 = $[15]; 648: } 649: let t9; 650: if ($[16] !== t7) { 651: t9 = { 652: isActive: t7, 653: onExit: t8 654: }; 655: $[16] = t7; 656: $[17] = t9; 657: } else { 658: t9 = $[17]; 659: } 660: const { 661: query: searchQuery, 662: setQuery: setSearchQuery, 663: cursorOffset: searchCursorOffset 664: } = useSearchInput(t9); 665: let t10; 666: if ($[18] !== isSearchMode || $[19] !== isSearchModeActive || $[20] !== setSearchQuery) { 667: t10 = e => { 668: if (!isSearchModeActive) { 669: return; 670: } 671: if (isSearchMode) { 672: return; 673: } 674: if (e.ctrl || e.meta) { 675: return; 676: } 677: if (e.key === "/") { 678: e.preventDefault(); 679: setIsSearchMode(true); 680: setSearchQuery(""); 681: } else { 682: if (e.key.length === 1 && e.key !== "j" && e.key !== "k" && e.key !== "m" && e.key !== "i" && e.key !== "r" && e.key !== " ") { 683: e.preventDefault(); 684: setIsSearchMode(true); 685: setSearchQuery(e.key); 686: } 687: } 688: }; 689: $[18] = isSearchMode; 690: $[19] = isSearchModeActive; 691: $[20] = setSearchQuery; 692: $[21] = t10; 693: } else { 694: t10 = $[21]; 695: } 696: const handleKeyDown = t10; 697: let t11; 698: if ($[22] !== getRulesOptions) { 699: t11 = (selectedValue, tab_0) => { 700: const { 701: rulesByKey: rulesByKey_0 702: } = getRulesOptions(tab_0); 703: if (selectedValue === "add-new-rule") { 704: setAddingRuleToTab(tab_0); 705: return; 706: } else { 707: setSelectedRule(rulesByKey_0.get(selectedValue)); 708: return; 709: } 710: }; 711: $[22] = getRulesOptions; 712: $[23] = t11; 713: } else { 714: t11 = $[23]; 715: } 716: const handleToolSelect = t11; 717: let t12; 718: if ($[24] === Symbol.for("react.memo_cache_sentinel")) { 719: t12 = () => { 720: setAddingRuleToTab(null); 721: }; 722: $[24] = t12; 723: } else { 724: t12 = $[24]; 725: } 726: const handleRuleInputCancel = t12; 727: let t13; 728: if ($[25] === Symbol.for("react.memo_cache_sentinel")) { 729: t13 = (ruleValue, ruleBehavior) => { 730: setValidatedRule({ 731: ruleValue, 732: ruleBehavior 733: }); 734: setAddingRuleToTab(null); 735: }; 736: $[25] = t13; 737: } else { 738: t13 = $[25]; 739: } 740: const handleRuleInputSubmit = t13; 741: let t14; 742: if ($[26] === Symbol.for("react.memo_cache_sentinel")) { 743: t14 = (rules, unreachable) => { 744: setValidatedRule(null); 745: for (const rule_3 of rules) { 746: setChanges(prev => [...prev, `Added ${rule_3.ruleBehavior} rule ${chalk.bold(permissionRuleValueToString(rule_3.ruleValue))}`]); 747: } 748: if (unreachable && unreachable.length > 0) { 749: for (const u of unreachable) { 750: const severity = u.shadowType === "deny" ? "blocked" : "shadowed"; 751: setChanges(prev_0 => [...prev_0, chalk.yellow(`${figures.warning} Warning: ${permissionRuleValueToString(u.rule.ruleValue)} is ${severity}`), chalk.dim(` ${u.reason}`), chalk.dim(` Fix: ${u.fix}`)]); 752: } 753: } 754: }; 755: $[26] = t14; 756: } else { 757: t14 = $[26]; 758: } 759: const handleAddRulesSuccess = t14; 760: let t15; 761: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 762: t15 = () => { 763: setValidatedRule(null); 764: }; 765: $[27] = t15; 766: } else { 767: t15 = $[27]; 768: } 769: const handleAddRuleCancel = t15; 770: let t16; 771: if ($[28] === Symbol.for("react.memo_cache_sentinel")) { 772: t16 = () => setIsAddingWorkspaceDirectory(true); 773: $[28] = t16; 774: } else { 775: t16 = $[28]; 776: } 777: const handleRequestAddDirectory = t16; 778: let t17; 779: if ($[29] === Symbol.for("react.memo_cache_sentinel")) { 780: t17 = path => setRemovingDirectory(path); 781: $[29] = t17; 782: } else { 783: t17 = $[29]; 784: } 785: const handleRequestRemoveDirectory = t17; 786: let t18; 787: if ($[30] !== changes || $[31] !== onExit || $[32] !== onRetryDenials) { 788: t18 = () => { 789: const s_1 = denialStateRef.current; 790: const denialsFor = set => Array.from(set).map(idx => s_1.denials[idx]).filter(_temp2); 791: const retryDenials = denialsFor(s_1.retry); 792: if (retryDenials.length > 0) { 793: const commands = retryDenials.map(_temp3); 794: onRetryDenials?.(commands); 795: onExit(undefined, { 796: shouldQuery: true, 797: metaMessages: [`Permission granted for: ${commands.join(", ")}. You may now retry ${commands.length === 1 ? "this command" : "these commands"} if you would like.`] 798: }); 799: return; 800: } 801: const approvedDenials = denialsFor(s_1.approved); 802: if (approvedDenials.length > 0 || changes.length > 0) { 803: const approvedMsg = approvedDenials.length > 0 ? [`Approved ${approvedDenials.map(_temp4).join(", ")}`] : []; 804: onExit([...approvedMsg, ...changes].join("\n")); 805: } else { 806: onExit("Permissions dialog dismissed", { 807: display: "system" 808: }); 809: } 810: }; 811: $[30] = changes; 812: $[31] = onExit; 813: $[32] = onRetryDenials; 814: $[33] = t18; 815: } else { 816: t18 = $[33]; 817: } 818: const handleRulesCancel = t18; 819: const t19 = isSearchModeActive && !isSearchMode; 820: let t20; 821: if ($[34] !== t19) { 822: t20 = { 823: context: "Settings", 824: isActive: t19 825: }; 826: $[34] = t19; 827: $[35] = t20; 828: } else { 829: t20 = $[35]; 830: } 831: useKeybinding("confirm:no", handleRulesCancel, t20); 832: let t21; 833: if ($[36] !== getRulesOptions || $[37] !== selectedRule || $[38] !== setAppState || $[39] !== toolPermissionContext) { 834: t21 = () => { 835: if (!selectedRule) { 836: return; 837: } 838: const { 839: options: options_0 840: } = getRulesOptions(selectedRule.ruleBehavior as TabType); 841: const selectedKey = jsonStringify(selectedRule); 842: const ruleKeys = options_0.filter(_temp5).map(_temp6); 843: const currentIndex = ruleKeys.indexOf(selectedKey); 844: let nextFocusKey; 845: if (currentIndex !== -1) { 846: if (currentIndex < ruleKeys.length - 1) { 847: nextFocusKey = ruleKeys[currentIndex + 1]; 848: } else { 849: if (currentIndex > 0) { 850: nextFocusKey = ruleKeys[currentIndex - 1]; 851: } 852: } 853: } 854: setLastFocusedRuleKey(nextFocusKey); 855: deletePermissionRule({ 856: rule: selectedRule, 857: initialContext: toolPermissionContext, 858: setToolPermissionContext(toolPermissionContext_0) { 859: setAppState(prev_1 => ({ 860: ...prev_1, 861: toolPermissionContext: toolPermissionContext_0 862: })); 863: } 864: }); 865: setChanges(prev_2 => [...prev_2, `Deleted ${selectedRule.ruleBehavior} rule ${chalk.bold(permissionRuleValueToString(selectedRule.ruleValue))}`]); 866: setSelectedRule(undefined); 867: }; 868: $[36] = getRulesOptions; 869: $[37] = selectedRule; 870: $[38] = setAppState; 871: $[39] = toolPermissionContext; 872: $[40] = t21; 873: } else { 874: t21 = $[40]; 875: } 876: const handleDeleteRule = t21; 877: if (selectedRule) { 878: let t22; 879: if ($[41] === Symbol.for("react.memo_cache_sentinel")) { 880: t22 = () => setSelectedRule(undefined); 881: $[41] = t22; 882: } else { 883: t22 = $[41]; 884: } 885: let t23; 886: if ($[42] !== handleDeleteRule || $[43] !== selectedRule) { 887: t23 = <RuleDetails rule={selectedRule} onDelete={handleDeleteRule} onCancel={t22} />; 888: $[42] = handleDeleteRule; 889: $[43] = selectedRule; 890: $[44] = t23; 891: } else { 892: t23 = $[44]; 893: } 894: return t23; 895: } 896: if (addingRuleToTab && addingRuleToTab !== "workspace" && addingRuleToTab !== "recent") { 897: let t22; 898: if ($[45] !== addingRuleToTab) { 899: t22 = <PermissionRuleInput onCancel={handleRuleInputCancel} onSubmit={handleRuleInputSubmit} ruleBehavior={addingRuleToTab} />; 900: $[45] = addingRuleToTab; 901: $[46] = t22; 902: } else { 903: t22 = $[46]; 904: } 905: return t22; 906: } 907: if (validatedRule) { 908: let t22; 909: if ($[47] !== validatedRule.ruleValue) { 910: t22 = [validatedRule.ruleValue]; 911: $[47] = validatedRule.ruleValue; 912: $[48] = t22; 913: } else { 914: t22 = $[48]; 915: } 916: let t23; 917: if ($[49] !== setAppState) { 918: t23 = toolPermissionContext_1 => { 919: setAppState(prev_3 => ({ 920: ...prev_3, 921: toolPermissionContext: toolPermissionContext_1 922: })); 923: }; 924: $[49] = setAppState; 925: $[50] = t23; 926: } else { 927: t23 = $[50]; 928: } 929: let t24; 930: if ($[51] !== t22 || $[52] !== t23 || $[53] !== toolPermissionContext || $[54] !== validatedRule.ruleBehavior) { 931: t24 = <AddPermissionRules onAddRules={handleAddRulesSuccess} onCancel={handleAddRuleCancel} ruleValues={t22} ruleBehavior={validatedRule.ruleBehavior} initialContext={toolPermissionContext} setToolPermissionContext={t23} />; 932: $[51] = t22; 933: $[52] = t23; 934: $[53] = toolPermissionContext; 935: $[54] = validatedRule.ruleBehavior; 936: $[55] = t24; 937: } else { 938: t24 = $[55]; 939: } 940: return t24; 941: } 942: if (isAddingWorkspaceDirectory) { 943: let t22; 944: if ($[56] !== setAppState || $[57] !== toolPermissionContext) { 945: t22 = (path_0, remember) => { 946: const destination = remember ? "localSettings" : "session"; 947: const permissionUpdate = { 948: type: "addDirectories" as const, 949: directories: [path_0], 950: destination 951: }; 952: const updatedContext = applyPermissionUpdate(toolPermissionContext, permissionUpdate); 953: setAppState(prev_4 => ({ 954: ...prev_4, 955: toolPermissionContext: updatedContext 956: })); 957: if (remember) { 958: persistPermissionUpdate(permissionUpdate); 959: } 960: setChanges(prev_5 => [...prev_5, `Added directory ${chalk.bold(path_0)} to workspace${remember ? " and saved to local settings" : " for this session"}`]); 961: setIsAddingWorkspaceDirectory(false); 962: }; 963: $[56] = setAppState; 964: $[57] = toolPermissionContext; 965: $[58] = t22; 966: } else { 967: t22 = $[58]; 968: } 969: let t23; 970: if ($[59] === Symbol.for("react.memo_cache_sentinel")) { 971: t23 = () => setIsAddingWorkspaceDirectory(false); 972: $[59] = t23; 973: } else { 974: t23 = $[59]; 975: } 976: let t24; 977: if ($[60] !== t22 || $[61] !== toolPermissionContext) { 978: t24 = <AddWorkspaceDirectory onAddDirectory={t22} onCancel={t23} permissionContext={toolPermissionContext} />; 979: $[60] = t22; 980: $[61] = toolPermissionContext; 981: $[62] = t24; 982: } else { 983: t24 = $[62]; 984: } 985: return t24; 986: } 987: if (removingDirectory) { 988: let t22; 989: if ($[63] !== removingDirectory) { 990: t22 = () => { 991: setChanges(prev_6 => [...prev_6, `Removed directory ${chalk.bold(removingDirectory)} from workspace`]); 992: setRemovingDirectory(null); 993: }; 994: $[63] = removingDirectory; 995: $[64] = t22; 996: } else { 997: t22 = $[64]; 998: } 999: let t23; 1000: if ($[65] === Symbol.for("react.memo_cache_sentinel")) { 1001: t23 = () => setRemovingDirectory(null); 1002: $[65] = t23; 1003: } else { 1004: t23 = $[65]; 1005: } 1006: let t24; 1007: if ($[66] !== setAppState) { 1008: t24 = toolPermissionContext_2 => { 1009: setAppState(prev_7 => ({ 1010: ...prev_7, 1011: toolPermissionContext: toolPermissionContext_2 1012: })); 1013: }; 1014: $[66] = setAppState; 1015: $[67] = t24; 1016: } else { 1017: t24 = $[67]; 1018: } 1019: let t25; 1020: if ($[68] !== removingDirectory || $[69] !== t22 || $[70] !== t24 || $[71] !== toolPermissionContext) { 1021: t25 = <RemoveWorkspaceDirectory directoryPath={removingDirectory} onRemove={t22} onCancel={t23} permissionContext={toolPermissionContext} setPermissionContext={t24} />; 1022: $[68] = removingDirectory; 1023: $[69] = t22; 1024: $[70] = t24; 1025: $[71] = toolPermissionContext; 1026: $[72] = t25; 1027: } else { 1028: t25 = $[72]; 1029: } 1030: return t25; 1031: } 1032: let t22; 1033: if ($[73] !== getRulesOptions || $[74] !== handleRulesCancel || $[75] !== handleToolSelect || $[76] !== isSearchMode || $[77] !== isTerminalFocused || $[78] !== lastFocusedRuleKey || $[79] !== searchCursorOffset || $[80] !== searchQuery) { 1034: t22 = { 1035: searchQuery, 1036: isSearchMode, 1037: isFocused: isTerminalFocused, 1038: onCancel: handleRulesCancel, 1039: lastFocusedRuleKey, 1040: cursorOffset: searchCursorOffset, 1041: getRulesOptions, 1042: handleToolSelect, 1043: onHeaderFocusChange: handleHeaderFocusChange 1044: }; 1045: $[73] = getRulesOptions; 1046: $[74] = handleRulesCancel; 1047: $[75] = handleToolSelect; 1048: $[76] = isSearchMode; 1049: $[77] = isTerminalFocused; 1050: $[78] = lastFocusedRuleKey; 1051: $[79] = searchCursorOffset; 1052: $[80] = searchQuery; 1053: $[81] = t22; 1054: } else { 1055: t22 = $[81]; 1056: } 1057: const sharedRulesProps = t22; 1058: const isHidden = !!selectedRule || !!addingRuleToTab || !!validatedRule || isAddingWorkspaceDirectory || !!removingDirectory; 1059: const t23 = !isSearchMode; 1060: let t24; 1061: if ($[82] === Symbol.for("react.memo_cache_sentinel")) { 1062: t24 = <Tab id="recent" title="Recently denied"><RecentDenialsTab onHeaderFocusChange={handleHeaderFocusChange} onStateChange={handleDenialStateChange} /></Tab>; 1063: $[82] = t24; 1064: } else { 1065: t24 = $[82]; 1066: } 1067: let t25; 1068: if ($[83] !== sharedRulesProps) { 1069: t25 = <Tab id="allow" title="Allow"><PermissionRulesTab tab="allow" {...sharedRulesProps} /></Tab>; 1070: $[83] = sharedRulesProps; 1071: $[84] = t25; 1072: } else { 1073: t25 = $[84]; 1074: } 1075: let t26; 1076: if ($[85] !== sharedRulesProps) { 1077: t26 = <Tab id="ask" title="Ask"><PermissionRulesTab tab="ask" {...sharedRulesProps} /></Tab>; 1078: $[85] = sharedRulesProps; 1079: $[86] = t26; 1080: } else { 1081: t26 = $[86]; 1082: } 1083: let t27; 1084: if ($[87] !== sharedRulesProps) { 1085: t27 = <Tab id="deny" title="Deny"><PermissionRulesTab tab="deny" {...sharedRulesProps} /></Tab>; 1086: $[87] = sharedRulesProps; 1087: $[88] = t27; 1088: } else { 1089: t27 = $[88]; 1090: } 1091: let t28; 1092: if ($[89] === Symbol.for("react.memo_cache_sentinel")) { 1093: t28 = <Text>Claude Code can read files in the workspace, and make edits when auto-accept edits is on.</Text>; 1094: $[89] = t28; 1095: } else { 1096: t28 = $[89]; 1097: } 1098: let t29; 1099: if ($[90] !== onExit || $[91] !== toolPermissionContext) { 1100: t29 = <Tab id="workspace" title="Workspace"><Box flexDirection="column">{t28}<WorkspaceTab onExit={onExit} toolPermissionContext={toolPermissionContext} onRequestAddDirectory={handleRequestAddDirectory} onRequestRemoveDirectory={handleRequestRemoveDirectory} onHeaderFocusChange={handleHeaderFocusChange} /></Box></Tab>; 1101: $[90] = onExit; 1102: $[91] = toolPermissionContext; 1103: $[92] = t29; 1104: } else { 1105: t29 = $[92]; 1106: } 1107: let t30; 1108: if ($[93] !== defaultTab || $[94] !== isHidden || $[95] !== t23 || $[96] !== t25 || $[97] !== t26 || $[98] !== t27 || $[99] !== t29) { 1109: t30 = <Tabs title="Permissions:" color="permission" defaultTab={defaultTab} hidden={isHidden} initialHeaderFocused={!hasDenials} navFromContent={t23}>{t24}{t25}{t26}{t27}{t29}</Tabs>; 1110: $[93] = defaultTab; 1111: $[94] = isHidden; 1112: $[95] = t23; 1113: $[96] = t25; 1114: $[97] = t26; 1115: $[98] = t27; 1116: $[99] = t29; 1117: $[100] = t30; 1118: } else { 1119: t30 = $[100]; 1120: } 1121: let t31; 1122: if ($[101] !== defaultTab || $[102] !== exitState.keyName || $[103] !== exitState.pending || $[104] !== headerFocused || $[105] !== isSearchMode) { 1123: t31 = <Box marginTop={1} paddingLeft={1}><Text dimColor={true}>{exitState.pending ? <>Press {exitState.keyName} again to exit</> : headerFocused ? <>←/→ tab switch · ↓ return · Esc cancel</> : isSearchMode ? <>Type to filter · Enter/↓ select · ↑ tabs · Esc clear</> : hasDenials && defaultTab === "recent" ? <>Enter approve · r retry · ↑↓ navigate · ←/→ switch · Esc cancel</> : <>↑↓ navigate · Enter select · Type to search · ←/→ switch · Esc cancel</>}</Text></Box>; 1124: $[101] = defaultTab; 1125: $[102] = exitState.keyName; 1126: $[103] = exitState.pending; 1127: $[104] = headerFocused; 1128: $[105] = isSearchMode; 1129: $[106] = t31; 1130: } else { 1131: t31 = $[106]; 1132: } 1133: let t32; 1134: if ($[107] !== t30 || $[108] !== t31) { 1135: t32 = <Pane color="permission">{t30}{t31}</Pane>; 1136: $[107] = t30; 1137: $[108] = t31; 1138: $[109] = t32; 1139: } else { 1140: t32 = $[109]; 1141: } 1142: let t33; 1143: if ($[110] !== handleKeyDown || $[111] !== t32) { 1144: t33 = <Box flexDirection="column" onKeyDown={handleKeyDown}>{t32}</Box>; 1145: $[110] = handleKeyDown; 1146: $[111] = t32; 1147: $[112] = t33; 1148: } else { 1149: t33 = $[112]; 1150: } 1151: return t33; 1152: } 1153: function _temp6(opt_0) { 1154: return opt_0.value; 1155: } 1156: function _temp5(opt) { 1157: return opt.value !== "add-new-rule"; 1158: } 1159: function _temp4(d_1) { 1160: return chalk.bold(d_1.display); 1161: } 1162: function _temp3(d_0) { 1163: return d_0.display; 1164: } 1165: function _temp2(d) { 1166: return d !== undefined; 1167: } 1168: function _temp(s) { 1169: return s.toolPermissionContext; 1170: }

File: src/components/permissions/rules/RecentDenialsTab.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useCallback, useEffect, useState } from 'react'; 4: import { Box, Text, useInput } from '../../../ink.js'; 5: import { type AutoModeDenial, getAutoModeDenials } from '../../../utils/autoModeDenials.js'; 6: import { Select } from '../../CustomSelect/select.js'; 7: import { StatusIcon } from '../../design-system/StatusIcon.js'; 8: import { useTabHeaderFocus } from '../../design-system/Tabs.js'; 9: type Props = { 10: onHeaderFocusChange?: (focused: boolean) => void; 11: onStateChange: (state: { 12: approved: Set<number>; 13: retry: Set<number>; 14: denials: readonly AutoModeDenial[]; 15: }) => void; 16: }; 17: export function RecentDenialsTab(t0) { 18: const $ = _c(30); 19: const { 20: onHeaderFocusChange, 21: onStateChange 22: } = t0; 23: const { 24: headerFocused, 25: focusHeader 26: } = useTabHeaderFocus(); 27: let t1; 28: let t2; 29: if ($[0] !== headerFocused || $[1] !== onHeaderFocusChange) { 30: t1 = () => { 31: onHeaderFocusChange?.(headerFocused); 32: }; 33: t2 = [headerFocused, onHeaderFocusChange]; 34: $[0] = headerFocused; 35: $[1] = onHeaderFocusChange; 36: $[2] = t1; 37: $[3] = t2; 38: } else { 39: t1 = $[2]; 40: t2 = $[3]; 41: } 42: useEffect(t1, t2); 43: const [denials] = useState(_temp); 44: const [approved, setApproved] = useState(_temp2); 45: const [retry, setRetry] = useState(_temp3); 46: const [focusedIdx, setFocusedIdx] = useState(0); 47: let t3; 48: let t4; 49: if ($[4] !== approved || $[5] !== denials || $[6] !== onStateChange || $[7] !== retry) { 50: t3 = () => { 51: onStateChange({ 52: approved, 53: retry, 54: denials 55: }); 56: }; 57: t4 = [approved, retry, denials, onStateChange]; 58: $[4] = approved; 59: $[5] = denials; 60: $[6] = onStateChange; 61: $[7] = retry; 62: $[8] = t3; 63: $[9] = t4; 64: } else { 65: t3 = $[8]; 66: t4 = $[9]; 67: } 68: useEffect(t3, t4); 69: let t5; 70: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 71: t5 = value => { 72: const idx = Number(value); 73: setApproved(prev => { 74: const next = new Set(prev); 75: if (next.has(idx)) { 76: next.delete(idx); 77: } else { 78: next.add(idx); 79: } 80: return next; 81: }); 82: }; 83: $[10] = t5; 84: } else { 85: t5 = $[10]; 86: } 87: const handleSelect = t5; 88: let t6; 89: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 90: t6 = value_0 => { 91: setFocusedIdx(Number(value_0)); 92: }; 93: $[11] = t6; 94: } else { 95: t6 = $[11]; 96: } 97: const handleFocus = t6; 98: let t7; 99: if ($[12] !== focusedIdx) { 100: t7 = (input, _key) => { 101: if (input === "r") { 102: setRetry(prev_0 => { 103: const next_0 = new Set(prev_0); 104: if (next_0.has(focusedIdx)) { 105: next_0.delete(focusedIdx); 106: } else { 107: next_0.add(focusedIdx); 108: } 109: return next_0; 110: }); 111: setApproved(prev_1 => { 112: if (prev_1.has(focusedIdx)) { 113: return prev_1; 114: } 115: const next_1 = new Set(prev_1); 116: next_1.add(focusedIdx); 117: return next_1; 118: }); 119: } 120: }; 121: $[12] = focusedIdx; 122: $[13] = t7; 123: } else { 124: t7 = $[13]; 125: } 126: const t8 = denials.length > 0; 127: let t9; 128: if ($[14] !== t8) { 129: t9 = { 130: isActive: t8 131: }; 132: $[14] = t8; 133: $[15] = t9; 134: } else { 135: t9 = $[15]; 136: } 137: useInput(t7, t9); 138: if (denials.length === 0) { 139: let t10; 140: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 141: t10 = <Text dimColor={true}>No recent denials. Commands denied by the auto mode classifier will appear here.</Text>; 142: $[16] = t10; 143: } else { 144: t10 = $[16]; 145: } 146: return t10; 147: } 148: let t10; 149: if ($[17] !== approved || $[18] !== denials || $[19] !== retry) { 150: let t11; 151: if ($[21] !== approved || $[22] !== retry) { 152: t11 = (d, idx_0) => { 153: const isApproved = approved.has(idx_0); 154: const suffix = retry.has(idx_0) ? " (retry)" : ""; 155: return { 156: label: <Text><StatusIcon status={isApproved ? "success" : "error"} withSpace={true} />{d.display}<Text dimColor={true}>{suffix}</Text></Text>, 157: value: String(idx_0) 158: }; 159: }; 160: $[21] = approved; 161: $[22] = retry; 162: $[23] = t11; 163: } else { 164: t11 = $[23]; 165: } 166: t10 = denials.map(t11); 167: $[17] = approved; 168: $[18] = denials; 169: $[19] = retry; 170: $[20] = t10; 171: } else { 172: t10 = $[20]; 173: } 174: const options = t10; 175: let t11; 176: if ($[24] === Symbol.for("react.memo_cache_sentinel")) { 177: t11 = <Text>Commands recently denied by the auto mode classifier.</Text>; 178: $[24] = t11; 179: } else { 180: t11 = $[24]; 181: } 182: const t12 = Math.min(10, options.length); 183: let t13; 184: if ($[25] !== focusHeader || $[26] !== headerFocused || $[27] !== options || $[28] !== t12) { 185: t13 = <Box flexDirection="column">{t11}<Box marginTop={1}><Select options={options} onChange={handleSelect} onFocus={handleFocus} visibleOptionCount={t12} isDisabled={headerFocused} onUpFromFirstItem={focusHeader} /></Box></Box>; 186: $[25] = focusHeader; 187: $[26] = headerFocused; 188: $[27] = options; 189: $[28] = t12; 190: $[29] = t13; 191: } else { 192: t13 = $[29]; 193: } 194: return t13; 195: } 196: function _temp3() { 197: return new Set(); 198: } 199: function _temp2() { 200: return new Set(); 201: } 202: function _temp() { 203: return getAutoModeDenials(); 204: }

File: src/components/permissions/rules/RemoveWorkspaceDirectory.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useCallback } from 'react'; 4: import { Select } from '../../../components/CustomSelect/select.js'; 5: import { Box, Text } from '../../../ink.js'; 6: import type { ToolPermissionContext } from '../../../Tool.js'; 7: import { applyPermissionUpdate } from '../../../utils/permissions/PermissionUpdate.js'; 8: import { Dialog } from '../../design-system/Dialog.js'; 9: type Props = { 10: directoryPath: string; 11: onRemove: () => void; 12: onCancel: () => void; 13: permissionContext: ToolPermissionContext; 14: setPermissionContext: (context: ToolPermissionContext) => void; 15: }; 16: export function RemoveWorkspaceDirectory(t0) { 17: const $ = _c(19); 18: const { 19: directoryPath, 20: onRemove, 21: onCancel, 22: permissionContext, 23: setPermissionContext 24: } = t0; 25: let t1; 26: if ($[0] !== directoryPath || $[1] !== onRemove || $[2] !== permissionContext || $[3] !== setPermissionContext) { 27: t1 = () => { 28: const updatedContext = applyPermissionUpdate(permissionContext, { 29: type: "removeDirectories", 30: directories: [directoryPath], 31: destination: "session" 32: }); 33: setPermissionContext(updatedContext); 34: onRemove(); 35: }; 36: $[0] = directoryPath; 37: $[1] = onRemove; 38: $[2] = permissionContext; 39: $[3] = setPermissionContext; 40: $[4] = t1; 41: } else { 42: t1 = $[4]; 43: } 44: const handleRemove = t1; 45: let t2; 46: if ($[5] !== handleRemove || $[6] !== onCancel) { 47: t2 = value => { 48: if (value === "yes") { 49: handleRemove(); 50: } else { 51: onCancel(); 52: } 53: }; 54: $[5] = handleRemove; 55: $[6] = onCancel; 56: $[7] = t2; 57: } else { 58: t2 = $[7]; 59: } 60: const handleSelect = t2; 61: let t3; 62: if ($[8] !== directoryPath) { 63: t3 = <Box marginX={2} flexDirection="column"><Text bold={true}>{directoryPath}</Text></Box>; 64: $[8] = directoryPath; 65: $[9] = t3; 66: } else { 67: t3 = $[9]; 68: } 69: let t4; 70: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 71: t4 = <Text>Claude Code will no longer have access to files in this directory.</Text>; 72: $[10] = t4; 73: } else { 74: t4 = $[10]; 75: } 76: let t5; 77: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 78: t5 = [{ 79: label: "Yes", 80: value: "yes" 81: }, { 82: label: "No", 83: value: "no" 84: }]; 85: $[11] = t5; 86: } else { 87: t5 = $[11]; 88: } 89: let t6; 90: if ($[12] !== handleSelect || $[13] !== onCancel) { 91: t6 = <Select onChange={handleSelect} onCancel={onCancel} options={t5} />; 92: $[12] = handleSelect; 93: $[13] = onCancel; 94: $[14] = t6; 95: } else { 96: t6 = $[14]; 97: } 98: let t7; 99: if ($[15] !== onCancel || $[16] !== t3 || $[17] !== t6) { 100: t7 = <Dialog title="Remove directory from workspace?" onCancel={onCancel} color="error">{t3}{t4}{t6}</Dialog>; 101: $[15] = onCancel; 102: $[16] = t3; 103: $[17] = t6; 104: $[18] = t7; 105: } else { 106: t7 = $[18]; 107: } 108: return t7; 109: }

File: src/components/permissions/rules/WorkspaceTab.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { useCallback, useEffect } from 'react'; 5: import { getOriginalCwd } from '../../../bootstrap/state.js'; 6: import type { CommandResultDisplay } from '../../../commands.js'; 7: import { Select } from '../../../components/CustomSelect/select.js'; 8: import { Box, Text } from '../../../ink.js'; 9: import type { ToolPermissionContext } from '../../../Tool.js'; 10: import { useTabHeaderFocus } from '../../design-system/Tabs.js'; 11: type Props = { 12: onExit: (result?: string, options?: { 13: display?: CommandResultDisplay; 14: }) => void; 15: toolPermissionContext: ToolPermissionContext; 16: onRequestAddDirectory: () => void; 17: onRequestRemoveDirectory: (path: string) => void; 18: onHeaderFocusChange?: (focused: boolean) => void; 19: }; 20: type DirectoryItem = { 21: path: string; 22: isCurrent: boolean; 23: isDeletable: boolean; 24: }; 25: export function WorkspaceTab(t0) { 26: const $ = _c(23); 27: const { 28: onExit, 29: toolPermissionContext, 30: onRequestAddDirectory, 31: onRequestRemoveDirectory, 32: onHeaderFocusChange 33: } = t0; 34: const { 35: headerFocused, 36: focusHeader 37: } = useTabHeaderFocus(); 38: let t1; 39: let t2; 40: if ($[0] !== headerFocused || $[1] !== onHeaderFocusChange) { 41: t1 = () => { 42: onHeaderFocusChange?.(headerFocused); 43: }; 44: t2 = [headerFocused, onHeaderFocusChange]; 45: $[0] = headerFocused; 46: $[1] = onHeaderFocusChange; 47: $[2] = t1; 48: $[3] = t2; 49: } else { 50: t1 = $[2]; 51: t2 = $[3]; 52: } 53: useEffect(t1, t2); 54: let t3; 55: if ($[4] !== toolPermissionContext.additionalWorkingDirectories) { 56: t3 = Array.from(toolPermissionContext.additionalWorkingDirectories.keys()).map(_temp); 57: $[4] = toolPermissionContext.additionalWorkingDirectories; 58: $[5] = t3; 59: } else { 60: t3 = $[5]; 61: } 62: const additionalDirectories = t3; 63: let t4; 64: if ($[6] !== additionalDirectories || $[7] !== onRequestAddDirectory || $[8] !== onRequestRemoveDirectory) { 65: t4 = selectedValue => { 66: if (selectedValue === "add-directory") { 67: onRequestAddDirectory(); 68: return; 69: } 70: const directory = additionalDirectories.find(d => d.path === selectedValue); 71: if (directory && directory.isDeletable) { 72: onRequestRemoveDirectory(directory.path); 73: } 74: }; 75: $[6] = additionalDirectories; 76: $[7] = onRequestAddDirectory; 77: $[8] = onRequestRemoveDirectory; 78: $[9] = t4; 79: } else { 80: t4 = $[9]; 81: } 82: const handleDirectorySelect = t4; 83: let t5; 84: if ($[10] !== onExit) { 85: t5 = () => onExit("Workspace dialog dismissed", { 86: display: "system" 87: }); 88: $[10] = onExit; 89: $[11] = t5; 90: } else { 91: t5 = $[11]; 92: } 93: const handleCancel = t5; 94: let opts; 95: if ($[12] !== additionalDirectories) { 96: opts = additionalDirectories.map(_temp2); 97: let t6; 98: if ($[14] === Symbol.for("react.memo_cache_sentinel")) { 99: t6 = { 100: label: `Add directory${figures.ellipsis}`, 101: value: "add-directory" 102: }; 103: $[14] = t6; 104: } else { 105: t6 = $[14]; 106: } 107: opts.push(t6); 108: $[12] = additionalDirectories; 109: $[13] = opts; 110: } else { 111: opts = $[13]; 112: } 113: const options = opts; 114: let t6; 115: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 116: t6 = <Box flexDirection="row" marginTop={1} marginLeft={2} gap={1}><Text>{`- ${getOriginalCwd()}`}</Text><Text dimColor={true}>(Original working directory)</Text></Box>; 117: $[15] = t6; 118: } else { 119: t6 = $[15]; 120: } 121: const t7 = Math.min(10, options.length); 122: let t8; 123: if ($[16] !== focusHeader || $[17] !== handleCancel || $[18] !== handleDirectorySelect || $[19] !== headerFocused || $[20] !== options || $[21] !== t7) { 124: t8 = <Box flexDirection="column" marginBottom={1}>{t6}<Select options={options} onChange={handleDirectorySelect} onCancel={handleCancel} visibleOptionCount={t7} onUpFromFirstItem={focusHeader} isDisabled={headerFocused} /></Box>; 125: $[16] = focusHeader; 126: $[17] = handleCancel; 127: $[18] = handleDirectorySelect; 128: $[19] = headerFocused; 129: $[20] = options; 130: $[21] = t7; 131: $[22] = t8; 132: } else { 133: t8 = $[22]; 134: } 135: return t8; 136: } 137: function _temp2(dir) { 138: return { 139: label: dir.path, 140: value: dir.path 141: }; 142: } 143: function _temp(path) { 144: return { 145: path, 146: isCurrent: false, 147: isDeletable: true 148: }; 149: }

File: src/components/permissions/SedEditPermissionRequest/SedEditPermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { basename, relative } from 'path'; 3: import React, { Suspense, use, useMemo } from 'react'; 4: import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js'; 5: import { getCwd } from 'src/utils/cwd.js'; 6: import { isENOENT } from 'src/utils/errors.js'; 7: import { detectEncodingForResolvedPath } from 'src/utils/fileRead.js'; 8: import { getFsImplementation } from 'src/utils/fsOperations.js'; 9: import { Text } from '../../../ink.js'; 10: import { BashTool } from '../../../tools/BashTool/BashTool.js'; 11: import { applySedSubstitution, type SedEditInfo } from '../../../tools/BashTool/sedEditParser.js'; 12: import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'; 13: import type { PermissionRequestProps } from '../PermissionRequest.js'; 14: type SedEditPermissionRequestProps = PermissionRequestProps & { 15: sedInfo: SedEditInfo; 16: }; 17: type FileReadResult = { 18: oldContent: string; 19: fileExists: boolean; 20: }; 21: export function SedEditPermissionRequest(t0) { 22: const $ = _c(9); 23: let props; 24: let sedInfo; 25: if ($[0] !== t0) { 26: ({ 27: sedInfo, 28: ...props 29: } = t0); 30: $[0] = t0; 31: $[1] = props; 32: $[2] = sedInfo; 33: } else { 34: props = $[1]; 35: sedInfo = $[2]; 36: } 37: const { 38: filePath 39: } = sedInfo; 40: let t1; 41: if ($[3] !== filePath) { 42: t1 = (async () => { 43: const encoding = detectEncodingForResolvedPath(filePath); 44: const raw = await getFsImplementation().readFile(filePath, { 45: encoding 46: }); 47: return { 48: oldContent: raw.replaceAll("\r\n", "\n"), 49: fileExists: true 50: }; 51: })().catch(_temp); 52: $[3] = filePath; 53: $[4] = t1; 54: } else { 55: t1 = $[4]; 56: } 57: const contentPromise = t1; 58: let t2; 59: if ($[5] !== contentPromise || $[6] !== props || $[7] !== sedInfo) { 60: t2 = <Suspense fallback={null}><SedEditPermissionRequestInner sedInfo={sedInfo} contentPromise={contentPromise} {...props} /></Suspense>; 61: $[5] = contentPromise; 62: $[6] = props; 63: $[7] = sedInfo; 64: $[8] = t2; 65: } else { 66: t2 = $[8]; 67: } 68: return t2; 69: } 70: function _temp(e) { 71: if (!isENOENT(e)) { 72: throw e; 73: } 74: return { 75: oldContent: "", 76: fileExists: false 77: }; 78: } 79: function SedEditPermissionRequestInner(t0) { 80: const $ = _c(35); 81: let contentPromise; 82: let props; 83: let sedInfo; 84: if ($[0] !== t0) { 85: ({ 86: sedInfo, 87: contentPromise, 88: ...props 89: } = t0); 90: $[0] = t0; 91: $[1] = contentPromise; 92: $[2] = props; 93: $[3] = sedInfo; 94: } else { 95: contentPromise = $[1]; 96: props = $[2]; 97: sedInfo = $[3]; 98: } 99: const { 100: filePath 101: } = sedInfo; 102: const { 103: oldContent, 104: fileExists 105: } = use(contentPromise); 106: let t1; 107: if ($[4] !== oldContent || $[5] !== sedInfo) { 108: t1 = applySedSubstitution(oldContent, sedInfo); 109: $[4] = oldContent; 110: $[5] = sedInfo; 111: $[6] = t1; 112: } else { 113: t1 = $[6]; 114: } 115: const newContent = t1; 116: let t2; 117: bb0: { 118: if (oldContent === newContent) { 119: let t3; 120: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 121: t3 = []; 122: $[7] = t3; 123: } else { 124: t3 = $[7]; 125: } 126: t2 = t3; 127: break bb0; 128: } 129: let t3; 130: if ($[8] !== newContent || $[9] !== oldContent) { 131: t3 = [{ 132: old_string: oldContent, 133: new_string: newContent, 134: replace_all: false 135: }]; 136: $[8] = newContent; 137: $[9] = oldContent; 138: $[10] = t3; 139: } else { 140: t3 = $[10]; 141: } 142: t2 = t3; 143: } 144: const edits = t2; 145: let t3; 146: bb1: { 147: if (!fileExists) { 148: t3 = "File does not exist"; 149: break bb1; 150: } 151: t3 = "Pattern did not match any content"; 152: } 153: const noChangesMessage = t3; 154: let t4; 155: if ($[11] !== filePath || $[12] !== newContent) { 156: t4 = input => { 157: const parsed = BashTool.inputSchema.parse(input); 158: return { 159: ...parsed, 160: _simulatedSedEdit: { 161: filePath, 162: newContent 163: } 164: }; 165: }; 166: $[11] = filePath; 167: $[12] = newContent; 168: $[13] = t4; 169: } else { 170: t4 = $[13]; 171: } 172: const parseInput = t4; 173: const t5 = props.toolUseConfirm; 174: const t6 = props.toolUseContext; 175: const t7 = props.onDone; 176: const t8 = props.onReject; 177: let t9; 178: if ($[14] !== filePath) { 179: t9 = relative(getCwd(), filePath); 180: $[14] = filePath; 181: $[15] = t9; 182: } else { 183: t9 = $[15]; 184: } 185: let t10; 186: if ($[16] !== filePath) { 187: t10 = basename(filePath); 188: $[16] = filePath; 189: $[17] = t10; 190: } else { 191: t10 = $[17]; 192: } 193: let t11; 194: if ($[18] !== t10) { 195: t11 = <Text>Do you want to make this edit to{" "}<Text bold={true}>{t10}</Text>?</Text>; 196: $[18] = t10; 197: $[19] = t11; 198: } else { 199: t11 = $[19]; 200: } 201: let t12; 202: if ($[20] !== edits || $[21] !== filePath || $[22] !== noChangesMessage) { 203: t12 = edits.length > 0 ? <FileEditToolDiff file_path={filePath} edits={edits} /> : <Text dimColor={true}>{noChangesMessage}</Text>; 204: $[20] = edits; 205: $[21] = filePath; 206: $[22] = noChangesMessage; 207: $[23] = t12; 208: } else { 209: t12 = $[23]; 210: } 211: let t13; 212: if ($[24] !== filePath || $[25] !== parseInput || $[26] !== props.onDone || $[27] !== props.onReject || $[28] !== props.toolUseConfirm || $[29] !== props.toolUseContext || $[30] !== props.workerBadge || $[31] !== t11 || $[32] !== t12 || $[33] !== t9) { 213: t13 = <FilePermissionDialog toolUseConfirm={t5} toolUseContext={t6} onDone={t7} onReject={t8} title="Edit file" subtitle={t9} question={t11} content={t12} path={filePath} completionType="str_replace_single" parseInput={parseInput} workerBadge={props.workerBadge} />; 214: $[24] = filePath; 215: $[25] = parseInput; 216: $[26] = props.onDone; 217: $[27] = props.onReject; 218: $[28] = props.toolUseConfirm; 219: $[29] = props.toolUseContext; 220: $[30] = props.workerBadge; 221: $[31] = t11; 222: $[32] = t12; 223: $[33] = t9; 224: $[34] = t13; 225: } else { 226: t13 = $[34]; 227: } 228: return t13; 229: }

File: src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useCallback, useMemo } from 'react'; 3: import { logError } from 'src/utils/log.js'; 4: import { getOriginalCwd } from '../../../bootstrap/state.js'; 5: import { Box, Text } from '../../../ink.js'; 6: import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'; 7: import { SKILL_TOOL_NAME } from '../../../tools/SkillTool/constants.js'; 8: import { SkillTool } from '../../../tools/SkillTool/SkillTool.js'; 9: import { env } from '../../../utils/env.js'; 10: import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'; 11: import { logUnaryEvent } from '../../../utils/unaryLogging.js'; 12: import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'; 13: import { PermissionDialog } from '../PermissionDialog.js'; 14: import { PermissionPrompt, type PermissionPromptOption, type ToolAnalyticsContext } from '../PermissionPrompt.js'; 15: import type { PermissionRequestProps } from '../PermissionRequest.js'; 16: import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'; 17: type SkillOptionValue = 'yes' | 'yes-exact' | 'yes-prefix' | 'no'; 18: export function SkillPermissionRequest(props) { 19: const $ = _c(51); 20: const { 21: toolUseConfirm, 22: onDone, 23: onReject, 24: workerBadge 25: } = props; 26: const parseInput = _temp; 27: let t0; 28: if ($[0] !== toolUseConfirm.input) { 29: t0 = parseInput(toolUseConfirm.input); 30: $[0] = toolUseConfirm.input; 31: $[1] = t0; 32: } else { 33: t0 = $[1]; 34: } 35: const skill = t0; 36: const commandObj = toolUseConfirm.permissionResult.behavior === "ask" && toolUseConfirm.permissionResult.metadata && "command" in toolUseConfirm.permissionResult.metadata ? toolUseConfirm.permissionResult.metadata.command : undefined; 37: let t1; 38: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 39: t1 = { 40: completion_type: "tool_use_single", 41: language_name: "none" 42: }; 43: $[2] = t1; 44: } else { 45: t1 = $[2]; 46: } 47: const unaryEvent = t1; 48: usePermissionRequestLogging(toolUseConfirm, unaryEvent); 49: let t2; 50: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 51: t2 = getOriginalCwd(); 52: $[3] = t2; 53: } else { 54: t2 = $[3]; 55: } 56: const originalCwd = t2; 57: let t3; 58: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 59: t3 = shouldShowAlwaysAllowOptions(); 60: $[4] = t3; 61: } else { 62: t3 = $[4]; 63: } 64: const showAlwaysAllowOptions = t3; 65: let t4; 66: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 67: t4 = [{ 68: label: "Yes", 69: value: "yes", 70: feedbackConfig: { 71: type: "accept" 72: } 73: }]; 74: $[5] = t4; 75: } else { 76: t4 = $[5]; 77: } 78: const baseOptions = t4; 79: let alwaysAllowOptions; 80: if ($[6] !== skill) { 81: alwaysAllowOptions = []; 82: if (showAlwaysAllowOptions) { 83: const t5 = <Text bold={true}>{skill}</Text>; 84: let t6; 85: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 86: t6 = <Text bold={true}>{originalCwd}</Text>; 87: $[8] = t6; 88: } else { 89: t6 = $[8]; 90: } 91: let t7; 92: if ($[9] !== t5) { 93: t7 = { 94: label: <Text>Yes, and don't ask again for {t5} in{" "}{t6}</Text>, 95: value: "yes-exact" 96: }; 97: $[9] = t5; 98: $[10] = t7; 99: } else { 100: t7 = $[10]; 101: } 102: alwaysAllowOptions.push(t7); 103: const spaceIndex = skill.indexOf(" "); 104: if (spaceIndex > 0) { 105: const commandPrefix = skill.substring(0, spaceIndex); 106: const t8 = commandPrefix + ":*"; 107: let t9; 108: if ($[11] !== t8) { 109: t9 = <Text bold={true}>{t8}</Text>; 110: $[11] = t8; 111: $[12] = t9; 112: } else { 113: t9 = $[12]; 114: } 115: let t10; 116: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 117: t10 = <Text bold={true}>{originalCwd}</Text>; 118: $[13] = t10; 119: } else { 120: t10 = $[13]; 121: } 122: let t11; 123: if ($[14] !== t9) { 124: t11 = { 125: label: <Text>Yes, and don't ask again for{" "}{t9} commands in{" "}{t10}</Text>, 126: value: "yes-prefix" 127: }; 128: $[14] = t9; 129: $[15] = t11; 130: } else { 131: t11 = $[15]; 132: } 133: alwaysAllowOptions.push(t11); 134: } 135: } 136: $[6] = skill; 137: $[7] = alwaysAllowOptions; 138: } else { 139: alwaysAllowOptions = $[7]; 140: } 141: let t5; 142: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 143: t5 = { 144: label: "No", 145: value: "no", 146: feedbackConfig: { 147: type: "reject" 148: } 149: }; 150: $[16] = t5; 151: } else { 152: t5 = $[16]; 153: } 154: const noOption = t5; 155: let t6; 156: if ($[17] !== alwaysAllowOptions) { 157: t6 = [...baseOptions, ...alwaysAllowOptions, noOption]; 158: $[17] = alwaysAllowOptions; 159: $[18] = t6; 160: } else { 161: t6 = $[18]; 162: } 163: const options = t6; 164: let t7; 165: if ($[19] !== toolUseConfirm.tool.name) { 166: t7 = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name); 167: $[19] = toolUseConfirm.tool.name; 168: $[20] = t7; 169: } else { 170: t7 = $[20]; 171: } 172: const t8 = toolUseConfirm.tool.isMcp ?? false; 173: let t9; 174: if ($[21] !== t7 || $[22] !== t8) { 175: t9 = { 176: toolName: t7, 177: isMcp: t8 178: }; 179: $[21] = t7; 180: $[22] = t8; 181: $[23] = t9; 182: } else { 183: t9 = $[23]; 184: } 185: const toolAnalyticsContext = t9; 186: let t10; 187: if ($[24] !== onDone || $[25] !== onReject || $[26] !== skill || $[27] !== toolUseConfirm) { 188: t10 = (value, feedback) => { 189: bb33: switch (value) { 190: case "yes": 191: { 192: logUnaryEvent({ 193: completion_type: "tool_use_single", 194: event: "accept", 195: metadata: { 196: language_name: "none", 197: message_id: toolUseConfirm.assistantMessage.message.id, 198: platform: env.platform 199: } 200: }); 201: toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback); 202: onDone(); 203: break bb33; 204: } 205: case "yes-exact": 206: { 207: logUnaryEvent({ 208: completion_type: "tool_use_single", 209: event: "accept", 210: metadata: { 211: language_name: "none", 212: message_id: toolUseConfirm.assistantMessage.message.id, 213: platform: env.platform 214: } 215: }); 216: toolUseConfirm.onAllow(toolUseConfirm.input, [{ 217: type: "addRules", 218: rules: [{ 219: toolName: SKILL_TOOL_NAME, 220: ruleContent: skill 221: }], 222: behavior: "allow", 223: destination: "localSettings" 224: }]); 225: onDone(); 226: break bb33; 227: } 228: case "yes-prefix": 229: { 230: logUnaryEvent({ 231: completion_type: "tool_use_single", 232: event: "accept", 233: metadata: { 234: language_name: "none", 235: message_id: toolUseConfirm.assistantMessage.message.id, 236: platform: env.platform 237: } 238: }); 239: const spaceIndex_0 = skill.indexOf(" "); 240: const commandPrefix_0 = spaceIndex_0 > 0 ? skill.substring(0, spaceIndex_0) : skill; 241: toolUseConfirm.onAllow(toolUseConfirm.input, [{ 242: type: "addRules", 243: rules: [{ 244: toolName: SKILL_TOOL_NAME, 245: ruleContent: `${commandPrefix_0}:*` 246: }], 247: behavior: "allow", 248: destination: "localSettings" 249: }]); 250: onDone(); 251: break bb33; 252: } 253: case "no": 254: { 255: logUnaryEvent({ 256: completion_type: "tool_use_single", 257: event: "reject", 258: metadata: { 259: language_name: "none", 260: message_id: toolUseConfirm.assistantMessage.message.id, 261: platform: env.platform 262: } 263: }); 264: toolUseConfirm.onReject(feedback); 265: onReject(); 266: onDone(); 267: } 268: } 269: }; 270: $[24] = onDone; 271: $[25] = onReject; 272: $[26] = skill; 273: $[27] = toolUseConfirm; 274: $[28] = t10; 275: } else { 276: t10 = $[28]; 277: } 278: const handleSelect = t10; 279: let t11; 280: if ($[29] !== onDone || $[30] !== onReject || $[31] !== toolUseConfirm) { 281: t11 = () => { 282: logUnaryEvent({ 283: completion_type: "tool_use_single", 284: event: "reject", 285: metadata: { 286: language_name: "none", 287: message_id: toolUseConfirm.assistantMessage.message.id, 288: platform: env.platform 289: } 290: }); 291: toolUseConfirm.onReject(); 292: onReject(); 293: onDone(); 294: }; 295: $[29] = onDone; 296: $[30] = onReject; 297: $[31] = toolUseConfirm; 298: $[32] = t11; 299: } else { 300: t11 = $[32]; 301: } 302: const handleCancel = t11; 303: const t12 = `Use skill "${skill}"?`; 304: let t13; 305: if ($[33] === Symbol.for("react.memo_cache_sentinel")) { 306: t13 = <Text>Claude may use instructions, code, or files from this Skill.</Text>; 307: $[33] = t13; 308: } else { 309: t13 = $[33]; 310: } 311: const t14 = commandObj?.description; 312: let t15; 313: if ($[34] !== t14) { 314: t15 = <Box flexDirection="column" paddingX={2} paddingY={1}><Text dimColor={true}>{t14}</Text></Box>; 315: $[34] = t14; 316: $[35] = t15; 317: } else { 318: t15 = $[35]; 319: } 320: let t16; 321: if ($[36] !== toolUseConfirm.permissionResult) { 322: t16 = <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" />; 323: $[36] = toolUseConfirm.permissionResult; 324: $[37] = t16; 325: } else { 326: t16 = $[37]; 327: } 328: let t17; 329: if ($[38] !== handleCancel || $[39] !== handleSelect || $[40] !== options || $[41] !== toolAnalyticsContext) { 330: t17 = <PermissionPrompt options={options} onSelect={handleSelect} onCancel={handleCancel} toolAnalyticsContext={toolAnalyticsContext} />; 331: $[38] = handleCancel; 332: $[39] = handleSelect; 333: $[40] = options; 334: $[41] = toolAnalyticsContext; 335: $[42] = t17; 336: } else { 337: t17 = $[42]; 338: } 339: let t18; 340: if ($[43] !== t16 || $[44] !== t17) { 341: t18 = <Box flexDirection="column">{t16}{t17}</Box>; 342: $[43] = t16; 343: $[44] = t17; 344: $[45] = t18; 345: } else { 346: t18 = $[45]; 347: } 348: let t19; 349: if ($[46] !== t12 || $[47] !== t15 || $[48] !== t18 || $[49] !== workerBadge) { 350: t19 = <PermissionDialog title={t12} workerBadge={workerBadge}>{t13}{t15}{t18}</PermissionDialog>; 351: $[46] = t12; 352: $[47] = t15; 353: $[48] = t18; 354: $[49] = workerBadge; 355: $[50] = t19; 356: } else { 357: t19 = $[50]; 358: } 359: return t19; 360: } 361: function _temp(input) { 362: const result = SkillTool.inputSchema.safeParse(input); 363: if (!result.success) { 364: logError(new Error(`Failed to parse skill tool input: ${result.error.message}`)); 365: return ""; 366: } 367: return result.data.skill; 368: }

File: src/components/permissions/WebFetchPermissionRequest/WebFetchPermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useMemo } from 'react'; 3: import { Box, Text, useTheme } from '../../../ink.js'; 4: import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js'; 5: import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'; 6: import { type OptionWithDescription, Select } from '../../CustomSelect/select.js'; 7: import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'; 8: import { PermissionDialog } from '../PermissionDialog.js'; 9: import type { PermissionRequestProps } from '../PermissionRequest.js'; 10: import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'; 11: import { logUnaryPermissionEvent } from '../utils.js'; 12: function inputToPermissionRuleContent(input: { 13: [k: string]: unknown; 14: }): string { 15: try { 16: const parsedInput = WebFetchTool.inputSchema.safeParse(input); 17: if (!parsedInput.success) { 18: return `input:${input.toString()}`; 19: } 20: const { 21: url 22: } = parsedInput.data; 23: const hostname = new URL(url).hostname; 24: return `domain:${hostname}`; 25: } catch { 26: return `input:${input.toString()}`; 27: } 28: } 29: export function WebFetchPermissionRequest(t0) { 30: const $ = _c(41); 31: const { 32: toolUseConfirm, 33: onDone, 34: onReject, 35: verbose, 36: workerBadge 37: } = t0; 38: const [theme] = useTheme(); 39: const { 40: url 41: } = toolUseConfirm.input as { 42: url: string; 43: }; 44: let t1; 45: if ($[0] !== url) { 46: t1 = new URL(url); 47: $[0] = url; 48: $[1] = t1; 49: } else { 50: t1 = $[1]; 51: } 52: const hostname = t1.hostname; 53: let t2; 54: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 55: t2 = { 56: completion_type: "tool_use_single", 57: language_name: "none" 58: }; 59: $[2] = t2; 60: } else { 61: t2 = $[2]; 62: } 63: const unaryEvent = t2; 64: usePermissionRequestLogging(toolUseConfirm, unaryEvent); 65: let t3; 66: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 67: t3 = shouldShowAlwaysAllowOptions(); 68: $[3] = t3; 69: } else { 70: t3 = $[3]; 71: } 72: const showAlwaysAllowOptions = t3; 73: let t4; 74: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 75: t4 = { 76: label: "Yes", 77: value: "yes" 78: }; 79: $[4] = t4; 80: } else { 81: t4 = $[4]; 82: } 83: let result; 84: if ($[5] !== hostname) { 85: result = [t4]; 86: if (showAlwaysAllowOptions) { 87: const t5 = <Text bold={true}>{hostname}</Text>; 88: let t6; 89: if ($[7] !== t5) { 90: t6 = { 91: label: <Text>Yes, and don't ask again for {t5}</Text>, 92: value: "yes-dont-ask-again-domain" 93: }; 94: $[7] = t5; 95: $[8] = t6; 96: } else { 97: t6 = $[8]; 98: } 99: result.push(t6); 100: } 101: let t5; 102: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 103: t5 = { 104: label: <Text>No, and tell Claude what to do differently <Text bold={true}>(esc)</Text></Text>, 105: value: "no" 106: }; 107: $[9] = t5; 108: } else { 109: t5 = $[9]; 110: } 111: result.push(t5); 112: $[5] = hostname; 113: $[6] = result; 114: } else { 115: result = $[6]; 116: } 117: const options = result; 118: let t5; 119: if ($[10] !== onDone || $[11] !== onReject || $[12] !== toolUseConfirm) { 120: t5 = function onChange(newValue) { 121: bb8: switch (newValue) { 122: case "yes": 123: { 124: logUnaryPermissionEvent("tool_use_single", toolUseConfirm, "accept"); 125: toolUseConfirm.onAllow(toolUseConfirm.input, []); 126: onDone(); 127: break bb8; 128: } 129: case "yes-dont-ask-again-domain": 130: { 131: logUnaryPermissionEvent("tool_use_single", toolUseConfirm, "accept"); 132: const ruleContent = inputToPermissionRuleContent(toolUseConfirm.input); 133: const ruleValue = { 134: toolName: toolUseConfirm.tool.name, 135: ruleContent 136: }; 137: toolUseConfirm.onAllow(toolUseConfirm.input, [{ 138: type: "addRules", 139: rules: [ruleValue], 140: behavior: "allow", 141: destination: "localSettings" 142: }]); 143: onDone(); 144: break bb8; 145: } 146: case "no": 147: { 148: logUnaryPermissionEvent("tool_use_single", toolUseConfirm, "reject"); 149: toolUseConfirm.onReject(); 150: onReject(); 151: onDone(); 152: } 153: } 154: }; 155: $[10] = onDone; 156: $[11] = onReject; 157: $[12] = toolUseConfirm; 158: $[13] = t5; 159: } else { 160: t5 = $[13]; 161: } 162: const onChange = t5; 163: let t6; 164: if ($[14] !== theme || $[15] !== toolUseConfirm.input || $[16] !== verbose) { 165: t6 = WebFetchTool.renderToolUseMessage(toolUseConfirm.input as { 166: url: string; 167: prompt: string; 168: }, { 169: theme, 170: verbose 171: }); 172: $[14] = theme; 173: $[15] = toolUseConfirm.input; 174: $[16] = verbose; 175: $[17] = t6; 176: } else { 177: t6 = $[17]; 178: } 179: let t7; 180: if ($[18] !== t6) { 181: t7 = <Text>{t6}</Text>; 182: $[18] = t6; 183: $[19] = t7; 184: } else { 185: t7 = $[19]; 186: } 187: let t8; 188: if ($[20] !== toolUseConfirm.description) { 189: t8 = <Text dimColor={true}>{toolUseConfirm.description}</Text>; 190: $[20] = toolUseConfirm.description; 191: $[21] = t8; 192: } else { 193: t8 = $[21]; 194: } 195: let t9; 196: if ($[22] !== t7 || $[23] !== t8) { 197: t9 = <Box flexDirection="column" paddingX={2} paddingY={1}>{t7}{t8}</Box>; 198: $[22] = t7; 199: $[23] = t8; 200: $[24] = t9; 201: } else { 202: t9 = $[24]; 203: } 204: let t10; 205: if ($[25] !== toolUseConfirm.permissionResult) { 206: t10 = <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" />; 207: $[25] = toolUseConfirm.permissionResult; 208: $[26] = t10; 209: } else { 210: t10 = $[26]; 211: } 212: let t11; 213: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 214: t11 = <Text>Do you want to allow Claude to fetch this content?</Text>; 215: $[27] = t11; 216: } else { 217: t11 = $[27]; 218: } 219: let t12; 220: if ($[28] !== onChange) { 221: t12 = () => onChange("no"); 222: $[28] = onChange; 223: $[29] = t12; 224: } else { 225: t12 = $[29]; 226: } 227: let t13; 228: if ($[30] !== onChange || $[31] !== options || $[32] !== t12) { 229: t13 = <Select options={options} onChange={onChange} onCancel={t12} />; 230: $[30] = onChange; 231: $[31] = options; 232: $[32] = t12; 233: $[33] = t13; 234: } else { 235: t13 = $[33]; 236: } 237: let t14; 238: if ($[34] !== t10 || $[35] !== t13) { 239: t14 = <Box flexDirection="column">{t10}{t11}{t13}</Box>; 240: $[34] = t10; 241: $[35] = t13; 242: $[36] = t14; 243: } else { 244: t14 = $[36]; 245: } 246: let t15; 247: if ($[37] !== t14 || $[38] !== t9 || $[39] !== workerBadge) { 248: t15 = <PermissionDialog title="Fetch" workerBadge={workerBadge}>{t9}{t14}</PermissionDialog>; 249: $[37] = t14; 250: $[38] = t9; 251: $[39] = workerBadge; 252: $[40] = t15; 253: } else { 254: t15 = $[40]; 255: } 256: return t15; 257: }

File: src/components/permissions/FallbackPermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useCallback, useMemo } from 'react'; 3: import { getOriginalCwd } from '../../bootstrap/state.js'; 4: import { Box, Text, useTheme } from '../../ink.js'; 5: import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js'; 6: import { env } from '../../utils/env.js'; 7: import { shouldShowAlwaysAllowOptions } from '../../utils/permissions/permissionsLoader.js'; 8: import { truncateToLines } from '../../utils/stringUtils.js'; 9: import { logUnaryEvent } from '../../utils/unaryLogging.js'; 10: import { type UnaryEvent, usePermissionRequestLogging } from './hooks.js'; 11: import { PermissionDialog } from './PermissionDialog.js'; 12: import { PermissionPrompt, type PermissionPromptOption, type ToolAnalyticsContext } from './PermissionPrompt.js'; 13: import type { PermissionRequestProps } from './PermissionRequest.js'; 14: import { PermissionRuleExplanation } from './PermissionRuleExplanation.js'; 15: type FallbackOptionValue = 'yes' | 'yes-dont-ask-again' | 'no'; 16: export function FallbackPermissionRequest(t0) { 17: const $ = _c(58); 18: const { 19: toolUseConfirm, 20: onDone, 21: onReject, 22: workerBadge 23: } = t0; 24: const [theme] = useTheme(); 25: let originalUserFacingName; 26: let t1; 27: if ($[0] !== toolUseConfirm.input || $[1] !== toolUseConfirm.tool) { 28: originalUserFacingName = toolUseConfirm.tool.userFacingName(toolUseConfirm.input as never); 29: t1 = originalUserFacingName.endsWith(" (MCP)") ? originalUserFacingName.slice(0, -6) : originalUserFacingName; 30: $[0] = toolUseConfirm.input; 31: $[1] = toolUseConfirm.tool; 32: $[2] = originalUserFacingName; 33: $[3] = t1; 34: } else { 35: originalUserFacingName = $[2]; 36: t1 = $[3]; 37: } 38: const userFacingName = t1; 39: let t2; 40: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 41: t2 = { 42: completion_type: "tool_use_single", 43: language_name: "none" 44: }; 45: $[4] = t2; 46: } else { 47: t2 = $[4]; 48: } 49: const unaryEvent = t2; 50: usePermissionRequestLogging(toolUseConfirm, unaryEvent); 51: let t3; 52: if ($[5] !== onDone || $[6] !== onReject || $[7] !== toolUseConfirm) { 53: t3 = (value, feedback) => { 54: bb8: switch (value) { 55: case "yes": 56: { 57: logUnaryEvent({ 58: completion_type: "tool_use_single", 59: event: "accept", 60: metadata: { 61: language_name: "none", 62: message_id: toolUseConfirm.assistantMessage.message.id, 63: platform: env.platform 64: } 65: }); 66: toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback); 67: onDone(); 68: break bb8; 69: } 70: case "yes-dont-ask-again": 71: { 72: logUnaryEvent({ 73: completion_type: "tool_use_single", 74: event: "accept", 75: metadata: { 76: language_name: "none", 77: message_id: toolUseConfirm.assistantMessage.message.id, 78: platform: env.platform 79: } 80: }); 81: toolUseConfirm.onAllow(toolUseConfirm.input, [{ 82: type: "addRules", 83: rules: [{ 84: toolName: toolUseConfirm.tool.name 85: }], 86: behavior: "allow", 87: destination: "localSettings" 88: }]); 89: onDone(); 90: break bb8; 91: } 92: case "no": 93: { 94: logUnaryEvent({ 95: completion_type: "tool_use_single", 96: event: "reject", 97: metadata: { 98: language_name: "none", 99: message_id: toolUseConfirm.assistantMessage.message.id, 100: platform: env.platform 101: } 102: }); 103: toolUseConfirm.onReject(feedback); 104: onReject(); 105: onDone(); 106: } 107: } 108: }; 109: $[5] = onDone; 110: $[6] = onReject; 111: $[7] = toolUseConfirm; 112: $[8] = t3; 113: } else { 114: t3 = $[8]; 115: } 116: const handleSelect = t3; 117: let t4; 118: if ($[9] !== onDone || $[10] !== onReject || $[11] !== toolUseConfirm) { 119: t4 = () => { 120: logUnaryEvent({ 121: completion_type: "tool_use_single", 122: event: "reject", 123: metadata: { 124: language_name: "none", 125: message_id: toolUseConfirm.assistantMessage.message.id, 126: platform: env.platform 127: } 128: }); 129: toolUseConfirm.onReject(); 130: onReject(); 131: onDone(); 132: }; 133: $[9] = onDone; 134: $[10] = onReject; 135: $[11] = toolUseConfirm; 136: $[12] = t4; 137: } else { 138: t4 = $[12]; 139: } 140: const handleCancel = t4; 141: let t5; 142: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 143: t5 = getOriginalCwd(); 144: $[13] = t5; 145: } else { 146: t5 = $[13]; 147: } 148: const originalCwd = t5; 149: let t6; 150: if ($[14] === Symbol.for("react.memo_cache_sentinel")) { 151: t6 = shouldShowAlwaysAllowOptions(); 152: $[14] = t6; 153: } else { 154: t6 = $[14]; 155: } 156: const showAlwaysAllowOptions = t6; 157: let t7; 158: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 159: t7 = { 160: label: "Yes", 161: value: "yes", 162: feedbackConfig: { 163: type: "accept" 164: } 165: }; 166: $[15] = t7; 167: } else { 168: t7 = $[15]; 169: } 170: let result; 171: if ($[16] !== userFacingName) { 172: result = [t7]; 173: if (showAlwaysAllowOptions) { 174: const t8 = <Text bold={true}>{userFacingName}</Text>; 175: let t9; 176: if ($[18] === Symbol.for("react.memo_cache_sentinel")) { 177: t9 = <Text bold={true}>{originalCwd}</Text>; 178: $[18] = t9; 179: } else { 180: t9 = $[18]; 181: } 182: let t10; 183: if ($[19] !== t8) { 184: t10 = { 185: label: <Text>Yes, and don't ask again for {t8}{" "}commands in {t9}</Text>, 186: value: "yes-dont-ask-again" 187: }; 188: $[19] = t8; 189: $[20] = t10; 190: } else { 191: t10 = $[20]; 192: } 193: result.push(t10); 194: } 195: let t8; 196: if ($[21] === Symbol.for("react.memo_cache_sentinel")) { 197: t8 = { 198: label: "No", 199: value: "no", 200: feedbackConfig: { 201: type: "reject" 202: } 203: }; 204: $[21] = t8; 205: } else { 206: t8 = $[21]; 207: } 208: result.push(t8); 209: $[16] = userFacingName; 210: $[17] = result; 211: } else { 212: result = $[17]; 213: } 214: const options = result; 215: let t8; 216: if ($[22] !== toolUseConfirm.tool.name) { 217: t8 = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name); 218: $[22] = toolUseConfirm.tool.name; 219: $[23] = t8; 220: } else { 221: t8 = $[23]; 222: } 223: const t9 = toolUseConfirm.tool.isMcp ?? false; 224: let t10; 225: if ($[24] !== t8 || $[25] !== t9) { 226: t10 = { 227: toolName: t8, 228: isMcp: t9 229: }; 230: $[24] = t8; 231: $[25] = t9; 232: $[26] = t10; 233: } else { 234: t10 = $[26]; 235: } 236: const toolAnalyticsContext = t10; 237: let t11; 238: if ($[27] !== theme || $[28] !== toolUseConfirm.input || $[29] !== toolUseConfirm.tool) { 239: t11 = toolUseConfirm.tool.renderToolUseMessage(toolUseConfirm.input as never, { 240: theme, 241: verbose: true 242: }); 243: $[27] = theme; 244: $[28] = toolUseConfirm.input; 245: $[29] = toolUseConfirm.tool; 246: $[30] = t11; 247: } else { 248: t11 = $[30]; 249: } 250: let t12; 251: if ($[31] !== originalUserFacingName) { 252: t12 = originalUserFacingName.endsWith(" (MCP)") ? <Text dimColor={true}> (MCP)</Text> : ""; 253: $[31] = originalUserFacingName; 254: $[32] = t12; 255: } else { 256: t12 = $[32]; 257: } 258: let t13; 259: if ($[33] !== t11 || $[34] !== t12 || $[35] !== userFacingName) { 260: t13 = <Text>{userFacingName}({t11}){t12}</Text>; 261: $[33] = t11; 262: $[34] = t12; 263: $[35] = userFacingName; 264: $[36] = t13; 265: } else { 266: t13 = $[36]; 267: } 268: let t14; 269: if ($[37] !== toolUseConfirm.description) { 270: t14 = truncateToLines(toolUseConfirm.description, 3); 271: $[37] = toolUseConfirm.description; 272: $[38] = t14; 273: } else { 274: t14 = $[38]; 275: } 276: let t15; 277: if ($[39] !== t14) { 278: t15 = <Text dimColor={true}>{t14}</Text>; 279: $[39] = t14; 280: $[40] = t15; 281: } else { 282: t15 = $[40]; 283: } 284: let t16; 285: if ($[41] !== t13 || $[42] !== t15) { 286: t16 = <Box flexDirection="column" paddingX={2} paddingY={1}>{t13}{t15}</Box>; 287: $[41] = t13; 288: $[42] = t15; 289: $[43] = t16; 290: } else { 291: t16 = $[43]; 292: } 293: let t17; 294: if ($[44] !== toolUseConfirm.permissionResult) { 295: t17 = <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" />; 296: $[44] = toolUseConfirm.permissionResult; 297: $[45] = t17; 298: } else { 299: t17 = $[45]; 300: } 301: let t18; 302: if ($[46] !== handleCancel || $[47] !== handleSelect || $[48] !== options || $[49] !== toolAnalyticsContext) { 303: t18 = <PermissionPrompt options={options} onSelect={handleSelect} onCancel={handleCancel} toolAnalyticsContext={toolAnalyticsContext} />; 304: $[46] = handleCancel; 305: $[47] = handleSelect; 306: $[48] = options; 307: $[49] = toolAnalyticsContext; 308: $[50] = t18; 309: } else { 310: t18 = $[50]; 311: } 312: let t19; 313: if ($[51] !== t17 || $[52] !== t18) { 314: t19 = <Box flexDirection="column">{t17}{t18}</Box>; 315: $[51] = t17; 316: $[52] = t18; 317: $[53] = t19; 318: } else { 319: t19 = $[53]; 320: } 321: let t20; 322: if ($[54] !== t16 || $[55] !== t19 || $[56] !== workerBadge) { 323: t20 = <PermissionDialog title="Tool use" workerBadge={workerBadge}>{t16}{t19}</PermissionDialog>; 324: $[54] = t16; 325: $[55] = t19; 326: $[56] = workerBadge; 327: $[57] = t20; 328: } else { 329: t20 = $[57]; 330: } 331: return t20; 332: }

File: src/components/permissions/hooks.ts

typescript 1: import { feature } from 'bun:bundle' 2: import { useEffect, useRef } from 'react' 3: import { 4: type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 5: logEvent, 6: } from 'src/services/analytics/index.js' 7: import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js' 8: import { BashTool } from 'src/tools/BashTool/BashTool.js' 9: import { splitCommand_DEPRECATED } from 'src/utils/bash/commands.js' 10: import type { 11: PermissionDecisionReason, 12: PermissionResult, 13: } from 'src/utils/permissions/PermissionResult.js' 14: import { 15: extractRules, 16: hasRules, 17: } from 'src/utils/permissions/PermissionUpdate.js' 18: import { permissionRuleValueToString } from 'src/utils/permissions/permissionRuleParser.js' 19: import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js' 20: import type { ToolUseConfirm } from '../../components/permissions/PermissionRequest.js' 21: import { useSetAppState } from '../../state/AppState.js' 22: import { env } from '../../utils/env.js' 23: import { jsonStringify } from '../../utils/slowOperations.js' 24: import { type CompletionType, logUnaryEvent } from '../../utils/unaryLogging.js' 25: export type UnaryEvent = { 26: completion_type: CompletionType 27: language_name: string | Promise<string> 28: } 29: function permissionResultToLog(permissionResult: PermissionResult): string { 30: switch (permissionResult.behavior) { 31: case 'allow': 32: return 'allow' 33: case 'ask': { 34: const rules = extractRules(permissionResult.suggestions) 35: const suggestions = 36: rules.length > 0 37: ? rules.map(r => permissionRuleValueToString(r)).join(', ') 38: : 'none' 39: return `ask: ${permissionResult.message}, 40: suggestions: ${suggestions} 41: reason: ${decisionReasonToString(permissionResult.decisionReason)}` 42: } 43: case 'deny': 44: return `deny: ${permissionResult.message}, 45: reason: ${decisionReasonToString(permissionResult.decisionReason)}` 46: case 'passthrough': { 47: const rules = extractRules(permissionResult.suggestions) 48: const suggestions = 49: rules.length > 0 50: ? rules.map(r => permissionRuleValueToString(r)).join(', ') 51: : 'none' 52: return `passthrough: ${permissionResult.message}, 53: suggestions: ${suggestions} 54: reason: ${decisionReasonToString(permissionResult.decisionReason)}` 55: } 56: } 57: } 58: function decisionReasonToString( 59: decisionReason: PermissionDecisionReason | undefined, 60: ): string { 61: if (!decisionReason) { 62: return 'No decision reason' 63: } 64: if ( 65: (feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && 66: decisionReason.type === 'classifier' 67: ) { 68: return `Classifier: ${decisionReason.classifier}, Reason: ${decisionReason.reason}` 69: } 70: switch (decisionReason.type) { 71: case 'rule': 72: return `Rule: ${permissionRuleValueToString(decisionReason.rule.ruleValue)}` 73: case 'mode': 74: return `Mode: ${decisionReason.mode}` 75: case 'subcommandResults': 76: return `Subcommand Results: ${Array.from(decisionReason.reasons.entries()) 77: .map(([key, value]) => `${key}: ${permissionResultToLog(value)}`) 78: .join(', \n')}` 79: case 'permissionPromptTool': 80: return `Permission Tool: ${decisionReason.permissionPromptToolName}, Result: ${jsonStringify(decisionReason.toolResult)}` 81: case 'hook': 82: return `Hook: ${decisionReason.hookName}${decisionReason.reason ? `, Reason: ${decisionReason.reason}` : ''}` 83: case 'workingDir': 84: return `Working Directory: ${decisionReason.reason}` 85: case 'safetyCheck': 86: return `Safety check: ${decisionReason.reason}` 87: case 'other': 88: return `Other: ${decisionReason.reason}` 89: default: 90: return jsonStringify(decisionReason, null, 2) 91: } 92: } 93: export function usePermissionRequestLogging( 94: toolUseConfirm: ToolUseConfirm, 95: unaryEvent: UnaryEvent, 96: ): void { 97: const setAppState = useSetAppState() 98: const loggedToolUseID = useRef<string | null>(null) 99: useEffect(() => { 100: if (loggedToolUseID.current === toolUseConfirm.toolUseID) { 101: return 102: } 103: loggedToolUseID.current = toolUseConfirm.toolUseID 104: setAppState(prev => ({ 105: ...prev, 106: attribution: { 107: ...prev.attribution, 108: permissionPromptCount: prev.attribution.permissionPromptCount + 1, 109: }, 110: })) 111: logEvent('tengu_tool_use_show_permission_request', { 112: messageID: toolUseConfirm.assistantMessage.message 113: .id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 114: toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name), 115: isMcp: toolUseConfirm.tool.isMcp ?? false, 116: decisionReasonType: toolUseConfirm.permissionResult.decisionReason 117: ?.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 118: sandboxEnabled: SandboxManager.isSandboxingEnabled(), 119: }) 120: if (process.env.USER_TYPE === 'ant') { 121: const permissionResult = toolUseConfirm.permissionResult 122: if ( 123: toolUseConfirm.tool.name === BashTool.name && 124: permissionResult.behavior === 'ask' && 125: !hasRules(permissionResult.suggestions) 126: ) { 127: logEvent('tengu_internal_tool_use_permission_request_no_always_allow', { 128: messageID: toolUseConfirm.assistantMessage.message 129: .id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 130: toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name), 131: isMcp: toolUseConfirm.tool.isMcp ?? false, 132: decisionReasonType: (permissionResult.decisionReason?.type ?? 133: 'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 134: sandboxEnabled: SandboxManager.isSandboxingEnabled(), 135: decisionReasonDetails: decisionReasonToString( 136: permissionResult.decisionReason, 137: ) as never, 138: }) 139: } 140: } 141: if (process.env.USER_TYPE === 'ant') { 142: const parsedInput = BashTool.inputSchema.safeParse(toolUseConfirm.input) 143: if ( 144: toolUseConfirm.tool.name === BashTool.name && 145: toolUseConfirm.permissionResult.behavior === 'ask' && 146: parsedInput.success 147: ) { 148: let split = [parsedInput.data.command] 149: try { 150: split = splitCommand_DEPRECATED(parsedInput.data.command) 151: } catch { 152: } 153: logEvent('tengu_internal_bash_tool_use_permission_request', { 154: parts: jsonStringify( 155: split, 156: ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 157: input: jsonStringify( 158: toolUseConfirm.input, 159: ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 160: decisionReasonType: toolUseConfirm.permissionResult.decisionReason 161: ?.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 162: decisionReason: decisionReasonToString( 163: toolUseConfirm.permissionResult.decisionReason, 164: ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 165: }) 166: } 167: } 168: void logUnaryEvent({ 169: completion_type: unaryEvent.completion_type, 170: event: 'response', 171: metadata: { 172: language_name: unaryEvent.language_name, 173: message_id: toolUseConfirm.assistantMessage.message.id, 174: platform: env.platform, 175: }, 176: }) 177: }, [toolUseConfirm, unaryEvent, setAppState]) 178: }

File: src/components/permissions/PermissionDecisionDebugInfo.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import chalk from 'chalk'; 4: import figures from 'figures'; 5: import React, { useMemo } from 'react'; 6: import { Ansi, Box, color, Text, useTheme } from '../../ink.js'; 7: import { useAppState } from '../../state/AppState.js'; 8: import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'; 9: import { permissionModeTitle } from '../../utils/permissions/PermissionMode.js'; 10: import type { PermissionDecision, PermissionDecisionReason } from '../../utils/permissions/PermissionResult.js'; 11: import { extractRules } from '../../utils/permissions/PermissionUpdate.js'; 12: import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'; 13: import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js'; 14: import { detectUnreachableRules } from '../../utils/permissions/shadowedRuleDetection.js'; 15: import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'; 16: import { getSettingSourceDisplayNameLowercase } from '../../utils/settings/constants.js'; 17: type PermissionDecisionInfoItemProps = { 18: title?: string; 19: decisionReason: PermissionDecisionReason; 20: }; 21: function decisionReasonDisplayString(decisionReason: PermissionDecisionReason & { 22: type: Exclude<PermissionDecisionReason['type'], 'subcommandResults'>; 23: }): string { 24: if ((feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && decisionReason.type === 'classifier') { 25: return `${chalk.bold(decisionReason.classifier)} classifier: ${decisionReason.reason}`; 26: } 27: switch (decisionReason.type) { 28: case 'rule': 29: return `${chalk.bold(permissionRuleValueToString(decisionReason.rule.ruleValue))} rule from ${getSettingSourceDisplayNameLowercase(decisionReason.rule.source)}`; 30: case 'mode': 31: return `${permissionModeTitle(decisionReason.mode)} mode`; 32: case 'sandboxOverride': 33: return 'Requires permission to bypass sandbox'; 34: case 'workingDir': 35: return decisionReason.reason; 36: case 'safetyCheck': 37: case 'other': 38: return decisionReason.reason; 39: case 'permissionPromptTool': 40: return `${chalk.bold(decisionReason.permissionPromptToolName)} permission prompt tool`; 41: case 'hook': 42: return decisionReason.reason ? `${chalk.bold(decisionReason.hookName)} hook: ${decisionReason.reason}` : `${chalk.bold(decisionReason.hookName)} hook`; 43: case 'asyncAgent': 44: return decisionReason.reason; 45: default: 46: return ''; 47: } 48: } 49: function PermissionDecisionInfoItem(t0) { 50: const $ = _c(10); 51: const { 52: title, 53: decisionReason 54: } = t0; 55: const [theme] = useTheme(); 56: let t1; 57: if ($[0] !== decisionReason || $[1] !== theme) { 58: t1 = function formatDecisionReason() { 59: switch (decisionReason.type) { 60: case "subcommandResults": 61: { 62: return <Box flexDirection="column">{Array.from(decisionReason.reasons.entries()).map(t2 => { 63: const [subcommand, result] = t2; 64: const icon = result.behavior === "allow" ? color("success", theme)(figures.tick) : color("error", theme)(figures.cross); 65: return <Box flexDirection="column" key={subcommand}><Text>{icon} {subcommand}</Text>{result.decisionReason !== undefined && result.decisionReason.type !== "subcommandResults" && <Text><Text dimColor={true}>{" "}⎿{" "}</Text><Ansi>{decisionReasonDisplayString(result.decisionReason)}</Ansi></Text>}{result.behavior === "ask" && <SuggestedRules suggestions={result.suggestions} />}</Box>; 66: })}</Box>; 67: } 68: default: 69: { 70: return <Text><Ansi>{decisionReasonDisplayString(decisionReason)}</Ansi></Text>; 71: } 72: } 73: }; 74: $[0] = decisionReason; 75: $[1] = theme; 76: $[2] = t1; 77: } else { 78: t1 = $[2]; 79: } 80: const formatDecisionReason = t1; 81: let t2; 82: if ($[3] !== title) { 83: t2 = title && <Text>{title}</Text>; 84: $[3] = title; 85: $[4] = t2; 86: } else { 87: t2 = $[4]; 88: } 89: let t3; 90: if ($[5] !== formatDecisionReason) { 91: t3 = formatDecisionReason(); 92: $[5] = formatDecisionReason; 93: $[6] = t3; 94: } else { 95: t3 = $[6]; 96: } 97: let t4; 98: if ($[7] !== t2 || $[8] !== t3) { 99: t4 = <Box flexDirection="column">{t2}{t3}</Box>; 100: $[7] = t2; 101: $[8] = t3; 102: $[9] = t4; 103: } else { 104: t4 = $[9]; 105: } 106: return t4; 107: } 108: function SuggestedRules(t0) { 109: const $ = _c(18); 110: const { 111: suggestions 112: } = t0; 113: let T0; 114: let T1; 115: let t1; 116: let t2; 117: let t3; 118: let t4; 119: let t5; 120: if ($[0] !== suggestions) { 121: t5 = Symbol.for("react.early_return_sentinel"); 122: bb0: { 123: const rules = extractRules(suggestions); 124: if (rules.length === 0) { 125: t5 = null; 126: break bb0; 127: } 128: T1 = Text; 129: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 130: t2 = <Text dimColor={true}>{" "}⎿{" "}</Text>; 131: $[8] = t2; 132: } else { 133: t2 = $[8]; 134: } 135: t3 = "Suggested rules:"; 136: t4 = " "; 137: T0 = Ansi; 138: t1 = rules.map(_temp).join(", "); 139: } 140: $[0] = suggestions; 141: $[1] = T0; 142: $[2] = T1; 143: $[3] = t1; 144: $[4] = t2; 145: $[5] = t3; 146: $[6] = t4; 147: $[7] = t5; 148: } else { 149: T0 = $[1]; 150: T1 = $[2]; 151: t1 = $[3]; 152: t2 = $[4]; 153: t3 = $[5]; 154: t4 = $[6]; 155: t5 = $[7]; 156: } 157: if (t5 !== Symbol.for("react.early_return_sentinel")) { 158: return t5; 159: } 160: let t6; 161: if ($[9] !== T0 || $[10] !== t1) { 162: t6 = <T0>{t1}</T0>; 163: $[9] = T0; 164: $[10] = t1; 165: $[11] = t6; 166: } else { 167: t6 = $[11]; 168: } 169: let t7; 170: if ($[12] !== T1 || $[13] !== t2 || $[14] !== t3 || $[15] !== t4 || $[16] !== t6) { 171: t7 = <T1>{t2}{t3}{t4}{t6}</T1>; 172: $[12] = T1; 173: $[13] = t2; 174: $[14] = t3; 175: $[15] = t4; 176: $[16] = t6; 177: $[17] = t7; 178: } else { 179: t7 = $[17]; 180: } 181: return t7; 182: } 183: function _temp(rule) { 184: return chalk.bold(permissionRuleValueToString(rule)); 185: } 186: type Props = { 187: permissionResult: PermissionDecision; 188: toolName?: string; // Filter unreachable rules to this tool 189: }; 190: // Helper function to extract directories from permission updates 191: function extractDirectories(updates: PermissionUpdate[] | undefined): string[] { 192: if (!updates) return []; 193: return updates.flatMap(update => { 194: switch (update.type) { 195: case 'addDirectories': 196: return update.directories; 197: default: 198: return []; 199: } 200: }); 201: } 202: function extractMode(updates: PermissionUpdate[] | undefined): PermissionMode | undefined { 203: if (!updates) return undefined; 204: const update = updates.findLast(u => u.type === 'setMode'); 205: return update?.type === 'setMode' ? update.mode : undefined; 206: } 207: function SuggestionDisplay(t0) { 208: const $ = _c(22); 209: const { 210: suggestions, 211: width 212: } = t0; 213: if (!suggestions || suggestions.length === 0) { 214: let t1; 215: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 216: t1 = <Text dimColor={true}>Suggestions </Text>; 217: $[0] = t1; 218: } else { 219: t1 = $[0]; 220: } 221: let t2; 222: if ($[1] !== width) { 223: t2 = <Box justifyContent="flex-end" minWidth={width}>{t1}</Box>; 224: $[1] = width; 225: $[2] = t2; 226: } else { 227: t2 = $[2]; 228: } 229: let t3; 230: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 231: t3 = <Text>None</Text>; 232: $[3] = t3; 233: } else { 234: t3 = $[3]; 235: } 236: let t4; 237: if ($[4] !== t2) { 238: t4 = <Box flexDirection="row">{t2}{t3}</Box>; 239: $[4] = t2; 240: $[5] = t4; 241: } else { 242: t4 = $[5]; 243: } 244: return t4; 245: } 246: let t1; 247: let t2; 248: if ($[6] !== suggestions || $[7] !== width) { 249: t2 = Symbol.for("react.early_return_sentinel"); 250: bb0: { 251: const rules = extractRules(suggestions); 252: const directories = extractDirectories(suggestions); 253: const mode = extractMode(suggestions); 254: if (rules.length === 0 && directories.length === 0 && !mode) { 255: let t3; 256: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 257: t3 = <Text dimColor={true}>Suggestion </Text>; 258: $[10] = t3; 259: } else { 260: t3 = $[10]; 261: } 262: let t4; 263: if ($[11] !== width) { 264: t4 = <Box justifyContent="flex-end" minWidth={width}>{t3}</Box>; 265: $[11] = width; 266: $[12] = t4; 267: } else { 268: t4 = $[12]; 269: } 270: let t5; 271: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 272: t5 = <Text>None</Text>; 273: $[13] = t5; 274: } else { 275: t5 = $[13]; 276: } 277: let t6; 278: if ($[14] !== t4) { 279: t6 = <Box flexDirection="row">{t4}{t5}</Box>; 280: $[14] = t4; 281: $[15] = t6; 282: } else { 283: t6 = $[15]; 284: } 285: t2 = t6; 286: break bb0; 287: } 288: let t3; 289: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 290: t3 = <Text dimColor={true}>Suggestions </Text>; 291: $[16] = t3; 292: } else { 293: t3 = $[16]; 294: } 295: let t4; 296: if ($[17] !== width) { 297: t4 = <Box justifyContent="flex-end" minWidth={width}>{t3}</Box>; 298: $[17] = width; 299: $[18] = t4; 300: } else { 301: t4 = $[18]; 302: } 303: let t5; 304: if ($[19] === Symbol.for("react.memo_cache_sentinel")) { 305: t5 = <Text> </Text>; 306: $[19] = t5; 307: } else { 308: t5 = $[19]; 309: } 310: let t6; 311: if ($[20] !== t4) { 312: t6 = <Box flexDirection="row">{t4}{t5}</Box>; 313: $[20] = t4; 314: $[21] = t6; 315: } else { 316: t6 = $[21]; 317: } 318: t1 = <Box flexDirection="column">{t6}{rules.length > 0 && <Box flexDirection="row"><Box justifyContent="flex-end" minWidth={width}><Text dimColor={true}> Rules </Text></Box><Box flexDirection="column">{rules.map(_temp2)}</Box></Box>}{directories.length > 0 && <Box flexDirection="row"><Box justifyContent="flex-end" minWidth={width}><Text dimColor={true}> Directories </Text></Box><Box flexDirection="column">{directories.map(_temp3)}</Box></Box>}{mode && <Box flexDirection="row"><Box justifyContent="flex-end" minWidth={width}><Text dimColor={true}> Mode </Text></Box><Text>{permissionModeTitle(mode)}</Text></Box>}</Box>; 319: } 320: $[6] = suggestions; 321: $[7] = width; 322: $[8] = t1; 323: $[9] = t2; 324: } else { 325: t1 = $[8]; 326: t2 = $[9]; 327: } 328: if (t2 !== Symbol.for("react.early_return_sentinel")) { 329: return t2; 330: } 331: return t1; 332: } 333: function _temp3(dir, index_0) { 334: return <Text key={index_0}>{figures.bullet} {dir}</Text>; 335: } 336: function _temp2(rule, index) { 337: return <Text key={index}>{figures.bullet} {permissionRuleValueToString(rule)}</Text>; 338: } 339: export function PermissionDecisionDebugInfo(t0) { 340: const $ = _c(25); 341: const { 342: permissionResult, 343: toolName 344: } = t0; 345: const toolPermissionContext = useAppState(_temp4); 346: const decisionReason = permissionResult.decisionReason; 347: const suggestions = "suggestions" in permissionResult ? permissionResult.suggestions : undefined; 348: let t1; 349: if ($[0] !== suggestions || $[1] !== toolName || $[2] !== toolPermissionContext) { 350: bb0: { 351: const sandboxAutoAllowEnabled = SandboxManager.isSandboxingEnabled() && SandboxManager.isAutoAllowBashIfSandboxedEnabled(); 352: const all = detectUnreachableRules(toolPermissionContext, { 353: sandboxAutoAllowEnabled 354: }); 355: const suggestedRules = extractRules(suggestions); 356: if (suggestedRules.length > 0) { 357: t1 = all.filter(u => suggestedRules.some(suggested => suggested.toolName === u.rule.ruleValue.toolName && suggested.ruleContent === u.rule.ruleValue.ruleContent)); 358: break bb0; 359: } 360: if (toolName) { 361: let t2; 362: if ($[4] !== toolName) { 363: t2 = u_0 => u_0.rule.ruleValue.toolName === toolName; 364: $[4] = toolName; 365: $[5] = t2; 366: } else { 367: t2 = $[5]; 368: } 369: t1 = all.filter(t2); 370: break bb0; 371: } 372: t1 = all; 373: } 374: $[0] = suggestions; 375: $[1] = toolName; 376: $[2] = toolPermissionContext; 377: $[3] = t1; 378: } else { 379: t1 = $[3]; 380: } 381: const unreachableRules = t1; 382: let t2; 383: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 384: t2 = <Box justifyContent="flex-end" minWidth={10}><Text dimColor={true}>Behavior </Text></Box>; 385: $[6] = t2; 386: } else { 387: t2 = $[6]; 388: } 389: let t3; 390: if ($[7] !== permissionResult.behavior) { 391: t3 = <Box flexDirection="row">{t2}<Text>{permissionResult.behavior}</Text></Box>; 392: $[7] = permissionResult.behavior; 393: $[8] = t3; 394: } else { 395: t3 = $[8]; 396: } 397: let t4; 398: if ($[9] !== permissionResult.behavior || $[10] !== permissionResult.message) { 399: t4 = permissionResult.behavior !== "allow" && <Box flexDirection="row"><Box justifyContent="flex-end" minWidth={10}><Text dimColor={true}>Message </Text></Box><Text>{permissionResult.message}</Text></Box>; 400: $[9] = permissionResult.behavior; 401: $[10] = permissionResult.message; 402: $[11] = t4; 403: } else { 404: t4 = $[11]; 405: } 406: let t5; 407: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 408: t5 = <Box justifyContent="flex-end" minWidth={10}><Text dimColor={true}>Reason </Text></Box>; 409: $[12] = t5; 410: } else { 411: t5 = $[12]; 412: } 413: let t6; 414: if ($[13] !== decisionReason) { 415: t6 = <Box flexDirection="row">{t5}{decisionReason === undefined ? <Text>undefined</Text> : <PermissionDecisionInfoItem decisionReason={decisionReason} />}</Box>; 416: $[13] = decisionReason; 417: $[14] = t6; 418: } else { 419: t6 = $[14]; 420: } 421: let t7; 422: if ($[15] !== suggestions) { 423: t7 = <SuggestionDisplay suggestions={suggestions} width={10} />; 424: $[15] = suggestions; 425: $[16] = t7; 426: } else { 427: t7 = $[16]; 428: } 429: let t8; 430: if ($[17] !== unreachableRules) { 431: t8 = unreachableRules.length > 0 && <Box flexDirection="column" marginTop={1}><Text color="warning">{figures.warning} Unreachable Rules ({unreachableRules.length})</Text>{unreachableRules.map(_temp5)}</Box>; 432: $[17] = unreachableRules; 433: $[18] = t8; 434: } else { 435: t8 = $[18]; 436: } 437: let t9; 438: if ($[19] !== t3 || $[20] !== t4 || $[21] !== t6 || $[22] !== t7 || $[23] !== t8) { 439: t9 = <Box flexDirection="column">{t3}{t4}{t6}{t7}{t8}</Box>; 440: $[19] = t3; 441: $[20] = t4; 442: $[21] = t6; 443: $[22] = t7; 444: $[23] = t8; 445: $[24] = t9; 446: } else { 447: t9 = $[24]; 448: } 449: return t9; 450: } 451: function _temp5(u_1, i) { 452: return <Box key={i} flexDirection="column" marginLeft={2}><Text color="warning">{permissionRuleValueToString(u_1.rule.ruleValue)}</Text><Text dimColor={true}>{" "}{u_1.reason}</Text><Text dimColor={true}>{" "}Fix: {u_1.fix}</Text></Box>; 453: } 454: function _temp4(s) { 455: return s.toolPermissionContext; 456: }

File: src/components/permissions/PermissionDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box } from '../../ink.js'; 4: import type { Theme } from '../../utils/theme.js'; 5: import { PermissionRequestTitle } from './PermissionRequestTitle.js'; 6: import type { WorkerBadgeProps } from './WorkerBadge.js'; 7: type Props = { 8: title: string; 9: subtitle?: React.ReactNode; 10: color?: keyof Theme; 11: titleColor?: keyof Theme; 12: innerPaddingX?: number; 13: workerBadge?: WorkerBadgeProps; 14: titleRight?: React.ReactNode; 15: children: React.ReactNode; 16: }; 17: export function PermissionDialog(t0) { 18: const $ = _c(15); 19: const { 20: title, 21: subtitle, 22: color: t1, 23: titleColor, 24: innerPaddingX: t2, 25: workerBadge, 26: titleRight, 27: children 28: } = t0; 29: const color = t1 === undefined ? "permission" : t1; 30: const innerPaddingX = t2 === undefined ? 1 : t2; 31: let t3; 32: if ($[0] !== subtitle || $[1] !== title || $[2] !== titleColor || $[3] !== workerBadge) { 33: t3 = <PermissionRequestTitle title={title} subtitle={subtitle} color={titleColor} workerBadge={workerBadge} />; 34: $[0] = subtitle; 35: $[1] = title; 36: $[2] = titleColor; 37: $[3] = workerBadge; 38: $[4] = t3; 39: } else { 40: t3 = $[4]; 41: } 42: let t4; 43: if ($[5] !== t3 || $[6] !== titleRight) { 44: t4 = <Box paddingX={1} flexDirection="column"><Box justifyContent="space-between">{t3}{titleRight}</Box></Box>; 45: $[5] = t3; 46: $[6] = titleRight; 47: $[7] = t4; 48: } else { 49: t4 = $[7]; 50: } 51: let t5; 52: if ($[8] !== children || $[9] !== innerPaddingX) { 53: t5 = <Box flexDirection="column" paddingX={innerPaddingX}>{children}</Box>; 54: $[8] = children; 55: $[9] = innerPaddingX; 56: $[10] = t5; 57: } else { 58: t5 = $[10]; 59: } 60: let t6; 61: if ($[11] !== color || $[12] !== t4 || $[13] !== t5) { 62: t6 = <Box flexDirection="column" borderStyle="round" borderColor={color} borderLeft={false} borderRight={false} borderBottom={false} marginTop={1}>{t4}{t5}</Box>; 63: $[11] = color; 64: $[12] = t4; 65: $[13] = t5; 66: $[14] = t6; 67: } else { 68: t6 = $[14]; 69: } 70: return t6; 71: }

File: src/components/permissions/PermissionExplanation.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { Suspense, use, useState } from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 5: import { logEvent } from '../../services/analytics/index.js'; 6: import type { Message } from '../../types/message.js'; 7: import { generatePermissionExplanation, isPermissionExplainerEnabled, type PermissionExplanation as PermissionExplanationType, type RiskLevel } from '../../utils/permissions/permissionExplainer.js'; 8: import { ShimmerChar } from '../Spinner/ShimmerChar.js'; 9: import { useShimmerAnimation } from '../Spinner/useShimmerAnimation.js'; 10: const LOADING_MESSAGE = 'Loading explanation…'; 11: function ShimmerLoadingText() { 12: const $ = _c(7); 13: const [ref, glimmerIndex] = useShimmerAnimation("responding", LOADING_MESSAGE, false); 14: let t0; 15: if ($[0] !== glimmerIndex) { 16: t0 = LOADING_MESSAGE.split("").map((char, index) => <ShimmerChar key={index} char={char} index={index} glimmerIndex={glimmerIndex} messageColor="inactive" shimmerColor="text" />); 17: $[0] = glimmerIndex; 18: $[1] = t0; 19: } else { 20: t0 = $[1]; 21: } 22: let t1; 23: if ($[2] !== t0) { 24: t1 = <Text>{t0}</Text>; 25: $[2] = t0; 26: $[3] = t1; 27: } else { 28: t1 = $[3]; 29: } 30: let t2; 31: if ($[4] !== ref || $[5] !== t1) { 32: t2 = <Box ref={ref}>{t1}</Box>; 33: $[4] = ref; 34: $[5] = t1; 35: $[6] = t2; 36: } else { 37: t2 = $[6]; 38: } 39: return t2; 40: } 41: function getRiskColor(riskLevel: RiskLevel): 'success' | 'warning' | 'error' { 42: switch (riskLevel) { 43: case 'LOW': 44: return 'success'; 45: case 'MEDIUM': 46: return 'warning'; 47: case 'HIGH': 48: return 'error'; 49: } 50: } 51: function getRiskLabel(riskLevel: RiskLevel): string { 52: switch (riskLevel) { 53: case 'LOW': 54: return 'Low risk'; 55: case 'MEDIUM': 56: return 'Med risk'; 57: case 'HIGH': 58: return 'High risk'; 59: } 60: } 61: type PermissionExplanationProps = { 62: toolName: string; 63: toolInput: unknown; 64: toolDescription?: string; 65: messages?: Message[]; 66: }; 67: type ExplainerState = { 68: visible: boolean; 69: enabled: boolean; 70: promise: Promise<PermissionExplanationType | null> | null; 71: }; 72: function createExplanationPromise(props: PermissionExplanationProps): Promise<PermissionExplanationType | null> { 73: return generatePermissionExplanation({ 74: toolName: props.toolName, 75: toolInput: props.toolInput, 76: toolDescription: props.toolDescription, 77: messages: props.messages, 78: signal: new AbortController().signal 79: }).catch(() => null); 80: } 81: export function usePermissionExplainerUI(props) { 82: const $ = _c(9); 83: let t0; 84: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 85: t0 = isPermissionExplainerEnabled(); 86: $[0] = t0; 87: } else { 88: t0 = $[0]; 89: } 90: const enabled = t0; 91: const [visible, setVisible] = useState(false); 92: const [promise, setPromise] = useState(null); 93: let t1; 94: if ($[1] !== promise || $[2] !== props || $[3] !== visible) { 95: t1 = () => { 96: if (!visible) { 97: logEvent("tengu_permission_explainer_shortcut_used", {}); 98: if (!promise) { 99: setPromise(createExplanationPromise(props)); 100: } 101: } 102: setVisible(_temp); 103: }; 104: $[1] = promise; 105: $[2] = props; 106: $[3] = visible; 107: $[4] = t1; 108: } else { 109: t1 = $[4]; 110: } 111: let t2; 112: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 113: t2 = { 114: context: "Confirmation", 115: isActive: enabled 116: }; 117: $[5] = t2; 118: } else { 119: t2 = $[5]; 120: } 121: useKeybinding("confirm:toggleExplanation", t1, t2); 122: let t3; 123: if ($[6] !== promise || $[7] !== visible) { 124: t3 = { 125: visible, 126: enabled, 127: promise 128: }; 129: $[6] = promise; 130: $[7] = visible; 131: $[8] = t3; 132: } else { 133: t3 = $[8]; 134: } 135: return t3; 136: } 137: function _temp(v) { 138: return !v; 139: } 140: function ExplanationResult(t0) { 141: const $ = _c(21); 142: const { 143: promise 144: } = t0; 145: const explanation = use(promise); 146: if (!explanation) { 147: let t1; 148: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 149: t1 = <Box marginTop={1}><Text dimColor={true}>Explanation unavailable</Text></Box>; 150: $[0] = t1; 151: } else { 152: t1 = $[0]; 153: } 154: return t1; 155: } 156: let t1; 157: if ($[1] !== explanation.explanation) { 158: t1 = <Text>{explanation.explanation}</Text>; 159: $[1] = explanation.explanation; 160: $[2] = t1; 161: } else { 162: t1 = $[2]; 163: } 164: let t2; 165: if ($[3] !== explanation.reasoning) { 166: t2 = <Box marginTop={1}><Text>{explanation.reasoning}</Text></Box>; 167: $[3] = explanation.reasoning; 168: $[4] = t2; 169: } else { 170: t2 = $[4]; 171: } 172: let t3; 173: if ($[5] !== explanation.riskLevel) { 174: t3 = getRiskColor(explanation.riskLevel); 175: $[5] = explanation.riskLevel; 176: $[6] = t3; 177: } else { 178: t3 = $[6]; 179: } 180: let t4; 181: if ($[7] !== explanation.riskLevel) { 182: t4 = getRiskLabel(explanation.riskLevel); 183: $[7] = explanation.riskLevel; 184: $[8] = t4; 185: } else { 186: t4 = $[8]; 187: } 188: let t5; 189: if ($[9] !== t3 || $[10] !== t4) { 190: t5 = <Text color={t3}>{t4}:</Text>; 191: $[9] = t3; 192: $[10] = t4; 193: $[11] = t5; 194: } else { 195: t5 = $[11]; 196: } 197: let t6; 198: if ($[12] !== explanation.risk) { 199: t6 = <Text> {explanation.risk}</Text>; 200: $[12] = explanation.risk; 201: $[13] = t6; 202: } else { 203: t6 = $[13]; 204: } 205: let t7; 206: if ($[14] !== t5 || $[15] !== t6) { 207: t7 = <Box marginTop={1}><Text>{t5}{t6}</Text></Box>; 208: $[14] = t5; 209: $[15] = t6; 210: $[16] = t7; 211: } else { 212: t7 = $[16]; 213: } 214: let t8; 215: if ($[17] !== t1 || $[18] !== t2 || $[19] !== t7) { 216: t8 = <Box flexDirection="column" marginTop={1}>{t1}{t2}{t7}</Box>; 217: $[17] = t1; 218: $[18] = t2; 219: $[19] = t7; 220: $[20] = t8; 221: } else { 222: t8 = $[20]; 223: } 224: return t8; 225: } 226: export function PermissionExplainerContent(t0) { 227: const $ = _c(3); 228: const { 229: visible, 230: promise 231: } = t0; 232: if (!visible || !promise) { 233: return null; 234: } 235: let t1; 236: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 237: t1 = <Box marginTop={1}><ShimmerLoadingText /></Box>; 238: $[0] = t1; 239: } else { 240: t1 = $[0]; 241: } 242: let t2; 243: if ($[1] !== promise) { 244: t2 = <Suspense fallback={t1}><ExplanationResult promise={promise} /></Suspense>; 245: $[1] = promise; 246: $[2] = t2; 247: } else { 248: t2 = $[2]; 249: } 250: return t2; 251: }

File: src/components/permissions/PermissionPrompt.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { type ReactNode, useCallback, useMemo, useState } from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import type { KeybindingAction } from '../../keybindings/types.js'; 5: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 6: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; 7: import { useSetAppState } from '../../state/AppState.js'; 8: import { type OptionWithDescription, Select } from '../CustomSelect/select.js'; 9: export type FeedbackType = 'accept' | 'reject'; 10: export type PermissionPromptOption<T extends string> = { 11: value: T; 12: label: ReactNode; 13: feedbackConfig?: { 14: type: FeedbackType; 15: placeholder?: string; 16: }; 17: keybinding?: KeybindingAction; 18: }; 19: export type ToolAnalyticsContext = { 20: toolName: string; 21: isMcp: boolean; 22: }; 23: export type PermissionPromptProps<T extends string> = { 24: options: PermissionPromptOption<T>[]; 25: onSelect: (value: T, feedback?: string) => void; 26: onCancel?: () => void; 27: question?: string | ReactNode; 28: toolAnalyticsContext?: ToolAnalyticsContext; 29: }; 30: const DEFAULT_PLACEHOLDERS: Record<FeedbackType, string> = { 31: accept: 'tell Claude what to do next', 32: reject: 'tell Claude what to do differently' 33: }; 34: export function PermissionPrompt(t0) { 35: const $ = _c(54); 36: const { 37: options, 38: onSelect, 39: onCancel, 40: question: t1, 41: toolAnalyticsContext 42: } = t0; 43: const question = t1 === undefined ? "Do you want to proceed?" : t1; 44: const setAppState = useSetAppState(); 45: const [acceptFeedback, setAcceptFeedback] = useState(""); 46: const [rejectFeedback, setRejectFeedback] = useState(""); 47: const [acceptInputMode, setAcceptInputMode] = useState(false); 48: const [rejectInputMode, setRejectInputMode] = useState(false); 49: const [focusedValue, setFocusedValue] = useState(null); 50: const [acceptFeedbackModeEntered, setAcceptFeedbackModeEntered] = useState(false); 51: const [rejectFeedbackModeEntered, setRejectFeedbackModeEntered] = useState(false); 52: let t2; 53: if ($[0] !== focusedValue || $[1] !== options) { 54: let t3; 55: if ($[3] !== focusedValue) { 56: t3 = opt => opt.value === focusedValue; 57: $[3] = focusedValue; 58: $[4] = t3; 59: } else { 60: t3 = $[4]; 61: } 62: t2 = options.find(t3); 63: $[0] = focusedValue; 64: $[1] = options; 65: $[2] = t2; 66: } else { 67: t2 = $[2]; 68: } 69: const focusedOption = t2; 70: const focusedFeedbackType = focusedOption?.feedbackConfig?.type; 71: const showTabHint = focusedFeedbackType === "accept" && !acceptInputMode || focusedFeedbackType === "reject" && !rejectInputMode; 72: let t3; 73: if ($[5] !== acceptInputMode || $[6] !== options || $[7] !== rejectInputMode) { 74: let t4; 75: if ($[9] !== acceptInputMode || $[10] !== rejectInputMode) { 76: t4 = opt_0 => { 77: const { 78: value, 79: label, 80: feedbackConfig 81: } = opt_0; 82: if (!feedbackConfig) { 83: return { 84: label, 85: value 86: }; 87: } 88: const { 89: type, 90: placeholder 91: } = feedbackConfig; 92: const isInputMode = type === "accept" ? acceptInputMode : rejectInputMode; 93: const onChange = type === "accept" ? setAcceptFeedback : setRejectFeedback; 94: const defaultPlaceholder = DEFAULT_PLACEHOLDERS[type]; 95: if (isInputMode) { 96: return { 97: type: "input" as const, 98: label, 99: value, 100: placeholder: placeholder ?? defaultPlaceholder, 101: onChange, 102: allowEmptySubmitToCancel: true 103: }; 104: } 105: return { 106: label, 107: value 108: }; 109: }; 110: $[9] = acceptInputMode; 111: $[10] = rejectInputMode; 112: $[11] = t4; 113: } else { 114: t4 = $[11]; 115: } 116: t3 = options.map(t4); 117: $[5] = acceptInputMode; 118: $[6] = options; 119: $[7] = rejectInputMode; 120: $[8] = t3; 121: } else { 122: t3 = $[8]; 123: } 124: const selectOptions = t3; 125: let t4; 126: if ($[12] !== acceptInputMode || $[13] !== options || $[14] !== rejectInputMode || $[15] !== toolAnalyticsContext?.isMcp || $[16] !== toolAnalyticsContext?.toolName) { 127: t4 = value_0 => { 128: const option = options.find(opt_1 => opt_1.value === value_0); 129: if (!option?.feedbackConfig) { 130: return; 131: } 132: const { 133: type: type_0 134: } = option.feedbackConfig; 135: const analyticsProps = { 136: toolName: toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 137: isMcp: toolAnalyticsContext?.isMcp ?? false 138: }; 139: if (type_0 === "accept") { 140: if (acceptInputMode) { 141: setAcceptInputMode(false); 142: logEvent("tengu_accept_feedback_mode_collapsed", analyticsProps); 143: } else { 144: setAcceptInputMode(true); 145: setAcceptFeedbackModeEntered(true); 146: logEvent("tengu_accept_feedback_mode_entered", analyticsProps); 147: } 148: } else { 149: if (type_0 === "reject") { 150: if (rejectInputMode) { 151: setRejectInputMode(false); 152: logEvent("tengu_reject_feedback_mode_collapsed", analyticsProps); 153: } else { 154: setRejectInputMode(true); 155: setRejectFeedbackModeEntered(true); 156: logEvent("tengu_reject_feedback_mode_entered", analyticsProps); 157: } 158: } 159: } 160: }; 161: $[12] = acceptInputMode; 162: $[13] = options; 163: $[14] = rejectInputMode; 164: $[15] = toolAnalyticsContext?.isMcp; 165: $[16] = toolAnalyticsContext?.toolName; 166: $[17] = t4; 167: } else { 168: t4 = $[17]; 169: } 170: const handleInputModeToggle = t4; 171: let t5; 172: if ($[18] !== acceptFeedback || $[19] !== acceptFeedbackModeEntered || $[20] !== onSelect || $[21] !== options || $[22] !== rejectFeedback || $[23] !== rejectFeedbackModeEntered || $[24] !== toolAnalyticsContext?.isMcp || $[25] !== toolAnalyticsContext?.toolName) { 173: t5 = value_1 => { 174: const option_0 = options.find(opt_2 => opt_2.value === value_1); 175: if (!option_0) { 176: return; 177: } 178: let feedback; 179: if (option_0.feedbackConfig) { 180: const rawFeedback = option_0.feedbackConfig.type === "accept" ? acceptFeedback : rejectFeedback; 181: const trimmedFeedback = rawFeedback.trim(); 182: if (trimmedFeedback) { 183: feedback = trimmedFeedback; 184: } 185: const analyticsProps_0 = { 186: toolName: toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 187: isMcp: toolAnalyticsContext?.isMcp ?? false, 188: has_instructions: !!trimmedFeedback, 189: instructions_length: trimmedFeedback?.length ?? 0, 190: entered_feedback_mode: option_0.feedbackConfig.type === "accept" ? acceptFeedbackModeEntered : rejectFeedbackModeEntered 191: }; 192: if (option_0.feedbackConfig.type === "accept") { 193: logEvent("tengu_accept_submitted", analyticsProps_0); 194: } else { 195: if (option_0.feedbackConfig.type === "reject") { 196: logEvent("tengu_reject_submitted", analyticsProps_0); 197: } 198: } 199: } 200: onSelect(value_1, feedback); 201: }; 202: $[18] = acceptFeedback; 203: $[19] = acceptFeedbackModeEntered; 204: $[20] = onSelect; 205: $[21] = options; 206: $[22] = rejectFeedback; 207: $[23] = rejectFeedbackModeEntered; 208: $[24] = toolAnalyticsContext?.isMcp; 209: $[25] = toolAnalyticsContext?.toolName; 210: $[26] = t5; 211: } else { 212: t5 = $[26]; 213: } 214: const handleSelect = t5; 215: let handlers; 216: if ($[27] !== handleSelect || $[28] !== options) { 217: handlers = {}; 218: for (const opt_3 of options) { 219: if (opt_3.keybinding) { 220: handlers[opt_3.keybinding] = () => handleSelect(opt_3.value); 221: } 222: } 223: $[27] = handleSelect; 224: $[28] = options; 225: $[29] = handlers; 226: } else { 227: handlers = $[29]; 228: } 229: const keybindingHandlers = handlers; 230: let t6; 231: if ($[30] === Symbol.for("react.memo_cache_sentinel")) { 232: t6 = { 233: context: "Confirmation" 234: }; 235: $[30] = t6; 236: } else { 237: t6 = $[30]; 238: } 239: useKeybindings(keybindingHandlers, t6); 240: let t7; 241: if ($[31] !== onCancel || $[32] !== setAppState) { 242: t7 = () => { 243: logEvent("tengu_permission_request_escape", {}); 244: setAppState(_temp); 245: onCancel?.(); 246: }; 247: $[31] = onCancel; 248: $[32] = setAppState; 249: $[33] = t7; 250: } else { 251: t7 = $[33]; 252: } 253: const handleCancel = t7; 254: let t8; 255: if ($[34] !== question) { 256: t8 = typeof question === "string" ? <Text>{question}</Text> : question; 257: $[34] = question; 258: $[35] = t8; 259: } else { 260: t8 = $[35]; 261: } 262: let t9; 263: if ($[36] !== acceptFeedback || $[37] !== acceptInputMode || $[38] !== options || $[39] !== rejectFeedback || $[40] !== rejectInputMode) { 264: t9 = value_2 => { 265: const newOption = options.find(opt_4 => opt_4.value === value_2); 266: if (newOption?.feedbackConfig?.type !== "accept" && acceptInputMode && !acceptFeedback.trim()) { 267: setAcceptInputMode(false); 268: } 269: if (newOption?.feedbackConfig?.type !== "reject" && rejectInputMode && !rejectFeedback.trim()) { 270: setRejectInputMode(false); 271: } 272: setFocusedValue(value_2); 273: }; 274: $[36] = acceptFeedback; 275: $[37] = acceptInputMode; 276: $[38] = options; 277: $[39] = rejectFeedback; 278: $[40] = rejectInputMode; 279: $[41] = t9; 280: } else { 281: t9 = $[41]; 282: } 283: let t10; 284: if ($[42] !== handleCancel || $[43] !== handleInputModeToggle || $[44] !== handleSelect || $[45] !== selectOptions || $[46] !== t9) { 285: t10 = <Select options={selectOptions} inlineDescriptions={true} onChange={handleSelect} onCancel={handleCancel} onFocus={t9} onInputModeToggle={handleInputModeToggle} />; 286: $[42] = handleCancel; 287: $[43] = handleInputModeToggle; 288: $[44] = handleSelect; 289: $[45] = selectOptions; 290: $[46] = t9; 291: $[47] = t10; 292: } else { 293: t10 = $[47]; 294: } 295: const t11 = showTabHint && " \xB7 Tab to amend"; 296: let t12; 297: if ($[48] !== t11) { 298: t12 = <Box marginTop={1}><Text dimColor={true}>Esc to cancel{t11}</Text></Box>; 299: $[48] = t11; 300: $[49] = t12; 301: } else { 302: t12 = $[49]; 303: } 304: let t13; 305: if ($[50] !== t10 || $[51] !== t12 || $[52] !== t8) { 306: t13 = <Box flexDirection="column">{t8}{t10}{t12}</Box>; 307: $[50] = t10; 308: $[51] = t12; 309: $[52] = t8; 310: $[53] = t13; 311: } else { 312: t13 = $[53]; 313: } 314: return t13; 315: } 316: function _temp(prev) { 317: return { 318: ...prev, 319: attribution: { 320: ...prev.attribution, 321: escapeCount: prev.attribution.escapeCount + 1 322: } 323: }; 324: }

File: src/components/permissions/PermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import * as React from 'react'; 4: import { EnterPlanModeTool } from 'src/tools/EnterPlanModeTool/EnterPlanModeTool.js'; 5: import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'; 6: import { useNotifyAfterTimeout } from '../../hooks/useNotifyAfterTimeout.js'; 7: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 8: import type { AnyObject, Tool, ToolUseContext } from '../../Tool.js'; 9: import { AskUserQuestionTool } from '../../tools/AskUserQuestionTool/AskUserQuestionTool.js'; 10: import { BashTool } from '../../tools/BashTool/BashTool.js'; 11: import { FileEditTool } from '../../tools/FileEditTool/FileEditTool.js'; 12: import { FileReadTool } from '../../tools/FileReadTool/FileReadTool.js'; 13: import { FileWriteTool } from '../../tools/FileWriteTool/FileWriteTool.js'; 14: import { GlobTool } from '../../tools/GlobTool/GlobTool.js'; 15: import { GrepTool } from '../../tools/GrepTool/GrepTool.js'; 16: import { NotebookEditTool } from '../../tools/NotebookEditTool/NotebookEditTool.js'; 17: import { PowerShellTool } from '../../tools/PowerShellTool/PowerShellTool.js'; 18: import { SkillTool } from '../../tools/SkillTool/SkillTool.js'; 19: import { WebFetchTool } from '../../tools/WebFetchTool/WebFetchTool.js'; 20: import type { AssistantMessage } from '../../types/message.js'; 21: import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'; 22: import { AskUserQuestionPermissionRequest } from './AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.js'; 23: import { BashPermissionRequest } from './BashPermissionRequest/BashPermissionRequest.js'; 24: import { EnterPlanModePermissionRequest } from './EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.js'; 25: import { ExitPlanModePermissionRequest } from './ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.js'; 26: import { FallbackPermissionRequest } from './FallbackPermissionRequest.js'; 27: import { FileEditPermissionRequest } from './FileEditPermissionRequest/FileEditPermissionRequest.js'; 28: import { FilesystemPermissionRequest } from './FilesystemPermissionRequest/FilesystemPermissionRequest.js'; 29: import { FileWritePermissionRequest } from './FileWritePermissionRequest/FileWritePermissionRequest.js'; 30: import { NotebookEditPermissionRequest } from './NotebookEditPermissionRequest/NotebookEditPermissionRequest.js'; 31: import { PowerShellPermissionRequest } from './PowerShellPermissionRequest/PowerShellPermissionRequest.js'; 32: import { SkillPermissionRequest } from './SkillPermissionRequest/SkillPermissionRequest.js'; 33: import { WebFetchPermissionRequest } from './WebFetchPermissionRequest/WebFetchPermissionRequest.js'; 34: const ReviewArtifactTool = feature('REVIEW_ARTIFACT') ? (require('../../tools/ReviewArtifactTool/ReviewArtifactTool.js') as typeof import('../../tools/ReviewArtifactTool/ReviewArtifactTool.js')).ReviewArtifactTool : null; 35: const ReviewArtifactPermissionRequest = feature('REVIEW_ARTIFACT') ? (require('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js') as typeof import('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js')).ReviewArtifactPermissionRequest : null; 36: const WorkflowTool = feature('WORKFLOW_SCRIPTS') ? (require('../../tools/WorkflowTool/WorkflowTool.js') as typeof import('../../tools/WorkflowTool/WorkflowTool.js')).WorkflowTool : null; 37: const WorkflowPermissionRequest = feature('WORKFLOW_SCRIPTS') ? (require('../../tools/WorkflowTool/WorkflowPermissionRequest.js') as typeof import('../../tools/WorkflowTool/WorkflowPermissionRequest.js')).WorkflowPermissionRequest : null; 38: const MonitorTool = feature('MONITOR_TOOL') ? (require('../../tools/MonitorTool/MonitorTool.js') as typeof import('../../tools/MonitorTool/MonitorTool.js')).MonitorTool : null; 39: const MonitorPermissionRequest = feature('MONITOR_TOOL') ? (require('./MonitorPermissionRequest/MonitorPermissionRequest.js') as typeof import('./MonitorPermissionRequest/MonitorPermissionRequest.js')).MonitorPermissionRequest : null; 40: import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'; 41: import type { z } from 'zod/v4'; 42: import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'; 43: import type { WorkerBadgeProps } from './WorkerBadge.js'; 44: function permissionComponentForTool(tool: Tool): React.ComponentType<PermissionRequestProps> { 45: switch (tool) { 46: case FileEditTool: 47: return FileEditPermissionRequest; 48: case FileWriteTool: 49: return FileWritePermissionRequest; 50: case BashTool: 51: return BashPermissionRequest; 52: case PowerShellTool: 53: return PowerShellPermissionRequest; 54: case ReviewArtifactTool: 55: return ReviewArtifactPermissionRequest ?? FallbackPermissionRequest; 56: case WebFetchTool: 57: return WebFetchPermissionRequest; 58: case NotebookEditTool: 59: return NotebookEditPermissionRequest; 60: case ExitPlanModeV2Tool: 61: return ExitPlanModePermissionRequest; 62: case EnterPlanModeTool: 63: return EnterPlanModePermissionRequest; 64: case SkillTool: 65: return SkillPermissionRequest; 66: case AskUserQuestionTool: 67: return AskUserQuestionPermissionRequest; 68: case WorkflowTool: 69: return WorkflowPermissionRequest ?? FallbackPermissionRequest; 70: case MonitorTool: 71: return MonitorPermissionRequest ?? FallbackPermissionRequest; 72: case GlobTool: 73: case GrepTool: 74: case FileReadTool: 75: return FilesystemPermissionRequest; 76: default: 77: return FallbackPermissionRequest; 78: } 79: } 80: export type PermissionRequestProps<Input extends AnyObject = AnyObject> = { 81: toolUseConfirm: ToolUseConfirm<Input>; 82: toolUseContext: ToolUseContext; 83: onDone(): void; 84: onReject(): void; 85: verbose: boolean; 86: workerBadge: WorkerBadgeProps | undefined; 87: setStickyFooter?: (jsx: React.ReactNode | null) => void; 88: }; 89: export type ToolUseConfirm<Input extends AnyObject = AnyObject> = { 90: assistantMessage: AssistantMessage; 91: tool: Tool<Input>; 92: description: string; 93: input: z.infer<Input>; 94: toolUseContext: ToolUseContext; 95: toolUseID: string; 96: permissionResult: PermissionDecision; 97: permissionPromptStartTimeMs: number; 98: classifierCheckInProgress?: boolean; 99: classifierAutoApproved?: boolean; 100: classifierMatchedRule?: string; 101: workerBadge?: WorkerBadgeProps; 102: onUserInteraction(): void; 103: onAbort(): void; 104: onDismissCheckmark?(): void; 105: onAllow(updatedInput: z.infer<Input>, permissionUpdates: PermissionUpdate[], feedback?: string, contentBlocks?: ContentBlockParam[]): void; 106: onReject(feedback?: string, contentBlocks?: ContentBlockParam[]): void; 107: recheckPermission(): Promise<void>; 108: }; 109: function getNotificationMessage(toolUseConfirm: ToolUseConfirm): string { 110: const toolName = toolUseConfirm.tool.userFacingName(toolUseConfirm.input as never); 111: if (toolUseConfirm.tool === ExitPlanModeV2Tool) { 112: return 'Claude Code needs your approval for the plan'; 113: } 114: if (toolUseConfirm.tool === EnterPlanModeTool) { 115: return 'Claude Code wants to enter plan mode'; 116: } 117: if (feature('REVIEW_ARTIFACT') && toolUseConfirm.tool === ReviewArtifactTool) { 118: return 'Claude needs your approval for a review artifact'; 119: } 120: if (!toolName || toolName.trim() === '') { 121: return 'Claude Code needs your attention'; 122: } 123: return `Claude needs your permission to use ${toolName}`; 124: } 125: export function PermissionRequest(t0) { 126: const $ = _c(18); 127: const { 128: toolUseConfirm, 129: toolUseContext, 130: onDone, 131: onReject, 132: verbose, 133: workerBadge, 134: setStickyFooter 135: } = t0; 136: let t1; 137: if ($[0] !== onDone || $[1] !== onReject || $[2] !== toolUseConfirm) { 138: t1 = () => { 139: onDone(); 140: onReject(); 141: toolUseConfirm.onReject(); 142: }; 143: $[0] = onDone; 144: $[1] = onReject; 145: $[2] = toolUseConfirm; 146: $[3] = t1; 147: } else { 148: t1 = $[3]; 149: } 150: let t2; 151: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 152: t2 = { 153: context: "Confirmation" 154: }; 155: $[4] = t2; 156: } else { 157: t2 = $[4]; 158: } 159: useKeybinding("app:interrupt", t1, t2); 160: let t3; 161: if ($[5] !== toolUseConfirm) { 162: t3 = getNotificationMessage(toolUseConfirm); 163: $[5] = toolUseConfirm; 164: $[6] = t3; 165: } else { 166: t3 = $[6]; 167: } 168: const notificationMessage = t3; 169: useNotifyAfterTimeout(notificationMessage, "permission_prompt"); 170: let t4; 171: if ($[7] !== toolUseConfirm.tool) { 172: t4 = permissionComponentForTool(toolUseConfirm.tool); 173: $[7] = toolUseConfirm.tool; 174: $[8] = t4; 175: } else { 176: t4 = $[8]; 177: } 178: const PermissionComponent = t4; 179: let t5; 180: if ($[9] !== PermissionComponent || $[10] !== onDone || $[11] !== onReject || $[12] !== setStickyFooter || $[13] !== toolUseConfirm || $[14] !== toolUseContext || $[15] !== verbose || $[16] !== workerBadge) { 181: t5 = <PermissionComponent toolUseContext={toolUseContext} toolUseConfirm={toolUseConfirm} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} setStickyFooter={setStickyFooter} />; 182: $[9] = PermissionComponent; 183: $[10] = onDone; 184: $[11] = onReject; 185: $[12] = setStickyFooter; 186: $[13] = toolUseConfirm; 187: $[14] = toolUseContext; 188: $[15] = verbose; 189: $[16] = workerBadge; 190: $[17] = t5; 191: } else { 192: t5 = $[17]; 193: } 194: return t5; 195: }

File: src/components/permissions/PermissionRequestTitle.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import type { Theme } from '../../utils/theme.js'; 5: import type { WorkerBadgeProps } from './WorkerBadge.js'; 6: type Props = { 7: title: string; 8: subtitle?: React.ReactNode; 9: color?: keyof Theme; 10: workerBadge?: WorkerBadgeProps; 11: }; 12: export function PermissionRequestTitle(t0) { 13: const $ = _c(13); 14: const { 15: title, 16: subtitle, 17: color: t1, 18: workerBadge 19: } = t0; 20: const color = t1 === undefined ? "permission" : t1; 21: let t2; 22: if ($[0] !== color || $[1] !== title) { 23: t2 = <Text bold={true} color={color}>{title}</Text>; 24: $[0] = color; 25: $[1] = title; 26: $[2] = t2; 27: } else { 28: t2 = $[2]; 29: } 30: let t3; 31: if ($[3] !== workerBadge) { 32: t3 = workerBadge && <Text dimColor={true}>{"\xB7 "}@{workerBadge.name}</Text>; 33: $[3] = workerBadge; 34: $[4] = t3; 35: } else { 36: t3 = $[4]; 37: } 38: let t4; 39: if ($[5] !== t2 || $[6] !== t3) { 40: t4 = <Box flexDirection="row" gap={1}>{t2}{t3}</Box>; 41: $[5] = t2; 42: $[6] = t3; 43: $[7] = t4; 44: } else { 45: t4 = $[7]; 46: } 47: let t5; 48: if ($[8] !== subtitle) { 49: t5 = subtitle != null && (typeof subtitle === "string" ? <Text dimColor={true} wrap="truncate-start">{subtitle}</Text> : subtitle); 50: $[8] = subtitle; 51: $[9] = t5; 52: } else { 53: t5 = $[9]; 54: } 55: let t6; 56: if ($[10] !== t4 || $[11] !== t5) { 57: t6 = <Box flexDirection="column">{t4}{t5}</Box>; 58: $[10] = t4; 59: $[11] = t5; 60: $[12] = t6; 61: } else { 62: t6 = $[12]; 63: } 64: return t6; 65: }

File: src/components/permissions/PermissionRuleExplanation.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import chalk from 'chalk'; 4: import React from 'react'; 5: import { Ansi, Box, Text } from '../../ink.js'; 6: import { useAppState } from '../../state/AppState.js'; 7: import type { PermissionDecision, PermissionDecisionReason } from '../../utils/permissions/PermissionResult.js'; 8: import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js'; 9: import type { Theme } from '../../utils/theme.js'; 10: import ThemedText from '../design-system/ThemedText.js'; 11: export type PermissionRuleExplanationProps = { 12: permissionResult: PermissionDecision; 13: toolType: 'tool' | 'command' | 'edit' | 'read'; 14: }; 15: type DecisionReasonStrings = { 16: reasonString: string; 17: configString?: string; 18: themeColor?: keyof Theme; 19: }; 20: function stringsForDecisionReason(reason: PermissionDecisionReason | undefined, toolType: 'tool' | 'command' | 'edit' | 'read'): DecisionReasonStrings | null { 21: if (!reason) { 22: return null; 23: } 24: if ((feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && reason.type === 'classifier') { 25: if (reason.classifier === 'auto-mode') { 26: return { 27: reasonString: `Auto mode classifier requires confirmation for this ${toolType}.\n${reason.reason}`, 28: configString: undefined, 29: themeColor: 'error' 30: }; 31: } 32: return { 33: reasonString: `Classifier ${chalk.bold(reason.classifier)} requires confirmation for this ${toolType}.\n${reason.reason}`, 34: configString: undefined 35: }; 36: } 37: switch (reason.type) { 38: case 'rule': 39: return { 40: reasonString: `Permission rule ${chalk.bold(permissionRuleValueToString(reason.rule.ruleValue))} requires confirmation for this ${toolType}.`, 41: configString: reason.rule.source === 'policySettings' ? undefined : '/permissions to update rules' 42: }; 43: case 'hook': 44: { 45: const hookReasonString = reason.reason ? `:\n${reason.reason}` : '.'; 46: const sourceLabel = reason.hookSource ? ` ${chalk.dim(`[${reason.hookSource}]`)}` : ''; 47: return { 48: reasonString: `Hook ${chalk.bold(reason.hookName)} requires confirmation for this ${toolType}${hookReasonString}${sourceLabel}`, 49: configString: '/hooks to update' 50: }; 51: } 52: case 'safetyCheck': 53: case 'other': 54: return { 55: reasonString: reason.reason, 56: configString: undefined 57: }; 58: case 'workingDir': 59: return { 60: reasonString: reason.reason, 61: configString: '/permissions to update rules' 62: }; 63: default: 64: return null; 65: } 66: } 67: export function PermissionRuleExplanation(t0) { 68: const $ = _c(11); 69: const { 70: permissionResult, 71: toolType 72: } = t0; 73: const permissionMode = useAppState(_temp); 74: const t1 = permissionResult?.decisionReason; 75: let t2; 76: if ($[0] !== t1 || $[1] !== toolType) { 77: t2 = stringsForDecisionReason(t1, toolType); 78: $[0] = t1; 79: $[1] = toolType; 80: $[2] = t2; 81: } else { 82: t2 = $[2]; 83: } 84: const strings = t2; 85: if (!strings) { 86: return null; 87: } 88: const themeColor = strings.themeColor ?? (permissionResult?.decisionReason?.type === "hook" && permissionMode === "auto" ? "warning" : undefined); 89: let t3; 90: if ($[3] !== strings.reasonString || $[4] !== themeColor) { 91: t3 = themeColor ? <ThemedText color={themeColor}>{strings.reasonString}</ThemedText> : <Text><Ansi>{strings.reasonString}</Ansi></Text>; 92: $[3] = strings.reasonString; 93: $[4] = themeColor; 94: $[5] = t3; 95: } else { 96: t3 = $[5]; 97: } 98: let t4; 99: if ($[6] !== strings.configString) { 100: t4 = strings.configString && <Text dimColor={true}>{strings.configString}</Text>; 101: $[6] = strings.configString; 102: $[7] = t4; 103: } else { 104: t4 = $[7]; 105: } 106: let t5; 107: if ($[8] !== t3 || $[9] !== t4) { 108: t5 = <Box marginBottom={1} flexDirection="column">{t3}{t4}</Box>; 109: $[8] = t3; 110: $[9] = t4; 111: $[10] = t5; 112: } else { 113: t5 = $[10]; 114: } 115: return t5; 116: } 117: function _temp(s) { 118: return s.toolPermissionContext.mode; 119: }

File: src/components/permissions/SandboxPermissionRequest.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from 'src/ink.js'; 4: import { type NetworkHostPattern, shouldAllowManagedSandboxDomainsOnly } from 'src/utils/sandbox/sandbox-adapter.js'; 5: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; 6: import { Select } from '../CustomSelect/select.js'; 7: import { PermissionDialog } from './PermissionDialog.js'; 8: export type SandboxPermissionRequestProps = { 9: hostPattern: NetworkHostPattern; 10: onUserResponse: (response: { 11: allow: boolean; 12: persistToSettings: boolean; 13: }) => void; 14: }; 15: export function SandboxPermissionRequest(t0) { 16: const $ = _c(22); 17: const { 18: hostPattern: t1, 19: onUserResponse 20: } = t0; 21: const { 22: host 23: } = t1; 24: let t2; 25: if ($[0] !== onUserResponse) { 26: t2 = function onSelect(value) { 27: bb4: switch (value) { 28: case "yes": 29: { 30: onUserResponse({ 31: allow: true, 32: persistToSettings: false 33: }); 34: break bb4; 35: } 36: case "yes-dont-ask-again": 37: { 38: onUserResponse({ 39: allow: true, 40: persistToSettings: true 41: }); 42: break bb4; 43: } 44: case "no": 45: { 46: onUserResponse({ 47: allow: false, 48: persistToSettings: false 49: }); 50: } 51: } 52: }; 53: $[0] = onUserResponse; 54: $[1] = t2; 55: } else { 56: t2 = $[1]; 57: } 58: const onSelect = t2; 59: let t3; 60: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 61: t3 = shouldAllowManagedSandboxDomainsOnly(); 62: $[2] = t3; 63: } else { 64: t3 = $[2]; 65: } 66: const managedDomainsOnly = t3; 67: let t4; 68: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 69: t4 = { 70: label: "Yes", 71: value: "yes" 72: }; 73: $[3] = t4; 74: } else { 75: t4 = $[3]; 76: } 77: let t5; 78: if ($[4] !== host) { 79: t5 = !managedDomainsOnly ? [{ 80: label: <Text>Yes, and don't ask again for <Text bold={true}>{host}</Text></Text>, 81: value: "yes-dont-ask-again" 82: }] : []; 83: $[4] = host; 84: $[5] = t5; 85: } else { 86: t5 = $[5]; 87: } 88: let t6; 89: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 90: t6 = { 91: label: <Text>No, and tell Claude what to do differently <Text bold={true}>(esc)</Text></Text>, 92: value: "no" 93: }; 94: $[6] = t6; 95: } else { 96: t6 = $[6]; 97: } 98: let t7; 99: if ($[7] !== t5) { 100: t7 = [t4, ...t5, t6]; 101: $[7] = t5; 102: $[8] = t7; 103: } else { 104: t7 = $[8]; 105: } 106: const options = t7; 107: let t8; 108: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 109: t8 = <Text dimColor={true}>Host:</Text>; 110: $[9] = t8; 111: } else { 112: t8 = $[9]; 113: } 114: let t9; 115: if ($[10] !== host) { 116: t9 = <Box>{t8}<Text> {host}</Text></Box>; 117: $[10] = host; 118: $[11] = t9; 119: } else { 120: t9 = $[11]; 121: } 122: let t10; 123: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 124: t10 = <Box marginTop={1}><Text>Do you want to allow this connection?</Text></Box>; 125: $[12] = t10; 126: } else { 127: t10 = $[12]; 128: } 129: let t11; 130: if ($[13] !== onUserResponse) { 131: t11 = () => { 132: onUserResponse({ 133: allow: false, 134: persistToSettings: false 135: }); 136: }; 137: $[13] = onUserResponse; 138: $[14] = t11; 139: } else { 140: t11 = $[14]; 141: } 142: let t12; 143: if ($[15] !== onSelect || $[16] !== options || $[17] !== t11) { 144: t12 = <Box><Select options={options} onChange={onSelect} onCancel={t11} /></Box>; 145: $[15] = onSelect; 146: $[16] = options; 147: $[17] = t11; 148: $[18] = t12; 149: } else { 150: t12 = $[18]; 151: } 152: let t13; 153: if ($[19] !== t12 || $[20] !== t9) { 154: t13 = <PermissionDialog title="Network request outside of sandbox"><Box flexDirection="column" paddingX={2} paddingY={1}>{t9}{t10}{t12}</Box></PermissionDialog>; 155: $[19] = t12; 156: $[20] = t9; 157: $[21] = t13; 158: } else { 159: t13 = $[21]; 160: } 161: return t13; 162: }

File: src/components/permissions/shellPermissionHelpers.tsx

typescript 1: import { basename, sep } from 'path'; 2: import React, { type ReactNode } from 'react'; 3: import { getOriginalCwd } from '../../bootstrap/state.js'; 4: import { Text } from '../../ink.js'; 5: import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'; 6: import { permissionRuleExtractPrefix } from '../../utils/permissions/shellRuleMatching.js'; 7: function commandListDisplay(commands: string[]): ReactNode { 8: switch (commands.length) { 9: case 0: 10: return ''; 11: case 1: 12: return <Text bold>{commands[0]}</Text>; 13: case 2: 14: return <Text> 15: <Text bold>{commands[0]}</Text> and <Text bold>{commands[1]}</Text> 16: </Text>; 17: default: 18: return <Text> 19: <Text bold>{commands.slice(0, -1).join(', ')}</Text>, and{' '} 20: <Text bold>{commands.slice(-1)[0]}</Text> 21: </Text>; 22: } 23: } 24: function commandListDisplayTruncated(commands: string[]): ReactNode { 25: // Check if the plain text representation would be too long 26: const plainText = commands.join(', '); 27: if (plainText.length > 50) { 28: return 'similar'; 29: } 30: return commandListDisplay(commands); 31: } 32: function formatPathList(paths: string[]): ReactNode { 33: if (paths.length === 0) return ''; 34: // Extract directory names from paths 35: const names = paths.map(p => basename(p) || p); 36: if (names.length === 1) { 37: return <Text> 38: <Text bold>{names[0]}</Text> 39: {sep} 40: </Text>; 41: } 42: if (names.length === 2) { 43: return <Text> 44: <Text bold>{names[0]}</Text> 45: {sep} and <Text bold>{names[1]}</Text> 46: {sep} 47: </Text>; 48: } 49: // For 3+, show first two with "and N more" 50: return <Text> 51: <Text bold>{names[0]}</Text> 52: {sep}, <Text bold>{names[1]}</Text> 53: {sep} and {paths.length - 2} more 54: </Text>; 55: } 56: /** 57: * Generate the label for the "Yes, and apply suggestions" option in shell 58: * permission dialogs (Bash, PowerShell). Parametrized by the shell tool name 59: * and an optional command transform (e.g., Bash strips output redirections so 60: * filenames don't show as commands). 61: */ 62: export function generateShellSuggestionsLabel(suggestions: PermissionUpdate[], shellToolName: string, commandTransform?: (command: string) => string): ReactNode | null { 63: const allRules = suggestions.filter(s => s.type === 'addRules').flatMap(s => s.rules || []); 64: const readRules = allRules.filter(r => r.toolName === 'Read'); 65: const shellRules = allRules.filter(r => r.toolName === shellToolName); 66: const directories = suggestions.filter(s => s.type === 'addDirectories').flatMap(s => s.directories || []); 67: const readPaths = readRules.map(r => r.ruleContent?.replace('/**', '') || '').filter(p => p); 68: // Extract shell command prefixes, optionally transforming for display 69: const shellCommands = [...new Set(shellRules.flatMap(rule => { 70: if (!rule.ruleContent) return []; 71: const command = permissionRuleExtractPrefix(rule.ruleContent) ?? rule.ruleContent; 72: return commandTransform ? commandTransform(command) : command; 73: }))]; 74: // Check what we have 75: const hasDirectories = directories.length > 0; 76: const hasReadPaths = readPaths.length > 0; 77: const hasCommands = shellCommands.length > 0; 78: // Handle single type cases 79: if (hasReadPaths && !hasDirectories && !hasCommands) { 80: // Only Read rules - use "reading from" language 81: if (readPaths.length === 1) { 82: const firstPath = readPaths[0]!; 83: const dirName = basename(firstPath) || firstPath; 84: return <Text> 85: Yes, allow reading from <Text bold>{dirName}</Text> 86: {sep} from this project 87: </Text>; 88: } 89: // Multiple read paths 90: return <Text> 91: Yes, allow reading from {formatPathList(readPaths)} from this project 92: </Text>; 93: } 94: if (hasDirectories && !hasReadPaths && !hasCommands) { 95: // Only directory permissions - use "access to" language 96: if (directories.length === 1) { 97: const firstDir = directories[0]!; 98: const dirName = basename(firstDir) || firstDir; 99: return <Text> 100: Yes, and always allow access to <Text bold>{dirName}</Text> 101: {sep} from this project 102: </Text>; 103: } 104: // Multiple directories 105: return <Text> 106: Yes, and always allow access to {formatPathList(directories)} from this 107: project 108: </Text>; 109: } 110: if (hasCommands && !hasDirectories && !hasReadPaths) { 111: // Only shell command permissions 112: return <Text> 113: {"Yes, and don't ask again for "} 114: {commandListDisplayTruncated(shellCommands)} commands in{' '} 115: <Text bold>{getOriginalCwd()}</Text> 116: </Text>; 117: } 118: // Handle mixed cases 119: if ((hasDirectories || hasReadPaths) && !hasCommands) { 120: // Combine directories and read paths since they're both path access 121: const allPaths = [...directories, ...readPaths]; 122: if (hasDirectories && hasReadPaths) { 123: // Mixed - use generic "access to" 124: return <Text> 125: Yes, and always allow access to {formatPathList(allPaths)} from this 126: project 127: </Text>; 128: } 129: } 130: if ((hasDirectories || hasReadPaths) && hasCommands) { 131: const allPaths = [...directories, ...readPaths]; 132: if (allPaths.length === 1 && shellCommands.length === 1) { 133: return <Text> 134: Yes, and allow access to {formatPathList(allPaths)} and{' '} 135: {commandListDisplayTruncated(shellCommands)} commands 136: </Text>; 137: } 138: return <Text> 139: Yes, and allow {formatPathList(allPaths)} access and{' '} 140: {commandListDisplayTruncated(shellCommands)} commands 141: </Text>; 142: } 143: return null; 144: }

File: src/components/permissions/useShellPermissionFeedback.ts

typescript 1: import { useState } from 'react' 2: import { 3: type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 4: logEvent, 5: } from '../../services/analytics/index.js' 6: import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js' 7: import { useSetAppState } from '../../state/AppState.js' 8: import type { ToolUseConfirm } from './PermissionRequest.js' 9: import { logUnaryPermissionEvent } from './utils.js' 10: export function useShellPermissionFeedback({ 11: toolUseConfirm, 12: onDone, 13: onReject, 14: explainerVisible, 15: }: { 16: toolUseConfirm: ToolUseConfirm 17: onDone: () => void 18: onReject: () => void 19: explainerVisible: boolean 20: }): { 21: yesInputMode: boolean 22: noInputMode: boolean 23: yesFeedbackModeEntered: boolean 24: noFeedbackModeEntered: boolean 25: acceptFeedback: string 26: rejectFeedback: string 27: setAcceptFeedback: (v: string) => void 28: setRejectFeedback: (v: string) => void 29: focusedOption: string 30: handleInputModeToggle: (option: string) => void 31: handleReject: (feedback?: string) => void 32: handleFocus: (value: string) => void 33: } { 34: const setAppState = useSetAppState() 35: const [rejectFeedback, setRejectFeedback] = useState('') 36: const [acceptFeedback, setAcceptFeedback] = useState('') 37: const [yesInputMode, setYesInputMode] = useState(false) 38: const [noInputMode, setNoInputMode] = useState(false) 39: const [focusedOption, setFocusedOption] = useState('yes') 40: const [yesFeedbackModeEntered, setYesFeedbackModeEntered] = useState(false) 41: const [noFeedbackModeEntered, setNoFeedbackModeEntered] = useState(false) 42: function handleInputModeToggle(option: string) { 43: toolUseConfirm.onUserInteraction() 44: const analyticsProps = { 45: toolName: sanitizeToolNameForAnalytics( 46: toolUseConfirm.tool.name, 47: ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 48: isMcp: toolUseConfirm.tool.isMcp ?? false, 49: } 50: if (option === 'yes') { 51: if (yesInputMode) { 52: setYesInputMode(false) 53: logEvent('tengu_accept_feedback_mode_collapsed', analyticsProps) 54: } else { 55: setYesInputMode(true) 56: setYesFeedbackModeEntered(true) 57: logEvent('tengu_accept_feedback_mode_entered', analyticsProps) 58: } 59: } else if (option === 'no') { 60: if (noInputMode) { 61: setNoInputMode(false) 62: logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps) 63: } else { 64: setNoInputMode(true) 65: setNoFeedbackModeEntered(true) 66: logEvent('tengu_reject_feedback_mode_entered', analyticsProps) 67: } 68: } 69: } 70: function handleReject(feedback?: string) { 71: const trimmedFeedback = feedback?.trim() 72: const hasFeedback = !!trimmedFeedback 73: if (!hasFeedback) { 74: logEvent('tengu_permission_request_escape', { 75: explainer_visible: explainerVisible, 76: }) 77: setAppState(prev => ({ 78: ...prev, 79: attribution: { 80: ...prev.attribution, 81: escapeCount: prev.attribution.escapeCount + 1, 82: }, 83: })) 84: } 85: logUnaryPermissionEvent( 86: 'tool_use_single', 87: toolUseConfirm, 88: 'reject', 89: hasFeedback, 90: ) 91: if (trimmedFeedback) { 92: toolUseConfirm.onReject(trimmedFeedback) 93: } else { 94: toolUseConfirm.onReject() 95: } 96: onReject() 97: onDone() 98: } 99: function handleFocus(value: string) { 100: if (value !== focusedOption) { 101: toolUseConfirm.onUserInteraction() 102: } 103: if (value !== 'yes' && yesInputMode && !acceptFeedback.trim()) { 104: setYesInputMode(false) 105: } 106: if (value !== 'no' && noInputMode && !rejectFeedback.trim()) { 107: setNoInputMode(false) 108: } 109: setFocusedOption(value) 110: } 111: return { 112: yesInputMode, 113: noInputMode, 114: yesFeedbackModeEntered, 115: noFeedbackModeEntered, 116: acceptFeedback, 117: rejectFeedback, 118: setAcceptFeedback, 119: setRejectFeedback, 120: focusedOption, 121: handleInputModeToggle, 122: handleReject, 123: handleFocus, 124: } 125: }

File: src/components/permissions/utils.ts

typescript 1: import { getHostPlatformForAnalytics } from '../../utils/env.js' 2: import { type CompletionType, logUnaryEvent } from '../../utils/unaryLogging.js' 3: import type { ToolUseConfirm } from './PermissionRequest.js' 4: export function logUnaryPermissionEvent( 5: completion_type: CompletionType, 6: { 7: assistantMessage: { 8: message: { id: message_id }, 9: }, 10: }: ToolUseConfirm, 11: event: 'accept' | 'reject', 12: hasFeedback?: boolean, 13: ): void { 14: void logUnaryEvent({ 15: completion_type, 16: event, 17: metadata: { 18: language_name: 'none', 19: message_id, 20: platform: getHostPlatformForAnalytics(), 21: hasFeedback: hasFeedback ?? false, 22: }, 23: }) 24: }

File: src/components/permissions/WorkerBadge.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { BLACK_CIRCLE } from '../../constants/figures.js'; 4: import { Box, Text } from '../../ink.js'; 5: import { toInkColor } from '../../utils/ink.js'; 6: export type WorkerBadgeProps = { 7: name: string; 8: color: string; 9: }; 10: export function WorkerBadge(t0) { 11: const $ = _c(7); 12: const { 13: name, 14: color 15: } = t0; 16: let t1; 17: if ($[0] !== color) { 18: t1 = toInkColor(color); 19: $[0] = color; 20: $[1] = t1; 21: } else { 22: t1 = $[1]; 23: } 24: const inkColor = t1; 25: let t2; 26: if ($[2] !== name) { 27: t2 = <Text bold={true}>@{name}</Text>; 28: $[2] = name; 29: $[3] = t2; 30: } else { 31: t2 = $[3]; 32: } 33: let t3; 34: if ($[4] !== inkColor || $[5] !== t2) { 35: t3 = <Box flexDirection="row" gap={1}><Text color={inkColor}>{BLACK_CIRCLE} {t2}</Text></Box>; 36: $[4] = inkColor; 37: $[5] = t2; 38: $[6] = t3; 39: } else { 40: t3 = $[6]; 41: } 42: return t3; 43: }

File: src/components/permissions/WorkerPendingPermission.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { getAgentName, getTeammateColor, getTeamName } from '../../utils/teammate.js'; 5: import { Spinner } from '../Spinner.js'; 6: import { WorkerBadge } from './WorkerBadge.js'; 7: type Props = { 8: toolName: string; 9: description: string; 10: }; 11: export function WorkerPendingPermission(t0) { 12: const $ = _c(15); 13: const { 14: toolName, 15: description 16: } = t0; 17: let t1; 18: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 19: t1 = getTeamName(); 20: $[0] = t1; 21: } else { 22: t1 = $[0]; 23: } 24: const teamName = t1; 25: let t2; 26: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 27: t2 = getAgentName(); 28: $[1] = t2; 29: } else { 30: t2 = $[1]; 31: } 32: const agentName = t2; 33: let t3; 34: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 35: t3 = getTeammateColor(); 36: $[2] = t3; 37: } else { 38: t3 = $[2]; 39: } 40: const agentColor = t3; 41: let t4; 42: let t5; 43: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 44: t4 = <Box marginBottom={1}><Spinner /><Text color="warning" bold={true}>{" "}Waiting for team lead approval</Text></Box>; 45: t5 = agentName && agentColor && <Box marginBottom={1}><WorkerBadge name={agentName} color={agentColor} /></Box>; 46: $[3] = t4; 47: $[4] = t5; 48: } else { 49: t4 = $[3]; 50: t5 = $[4]; 51: } 52: let t6; 53: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 54: t6 = <Text dimColor={true}>Tool: </Text>; 55: $[5] = t6; 56: } else { 57: t6 = $[5]; 58: } 59: let t7; 60: if ($[6] !== toolName) { 61: t7 = <Box>{t6}<Text>{toolName}</Text></Box>; 62: $[6] = toolName; 63: $[7] = t7; 64: } else { 65: t7 = $[7]; 66: } 67: let t8; 68: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 69: t8 = <Text dimColor={true}>Action: </Text>; 70: $[8] = t8; 71: } else { 72: t8 = $[8]; 73: } 74: let t9; 75: if ($[9] !== description) { 76: t9 = <Box>{t8}<Text>{description}</Text></Box>; 77: $[9] = description; 78: $[10] = t9; 79: } else { 80: t9 = $[10]; 81: } 82: let t10; 83: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 84: t10 = teamName && <Box marginTop={1}><Text dimColor={true}>Permission request sent to team {"\""}{teamName}{"\""} leader</Text></Box>; 85: $[11] = t10; 86: } else { 87: t10 = $[11]; 88: } 89: let t11; 90: if ($[12] !== t7 || $[13] !== t9) { 91: t11 = <Box flexDirection="column" borderStyle="round" borderColor="warning" paddingX={1}>{t4}{t5}{t7}{t9}{t10}</Box>; 92: $[12] = t7; 93: $[13] = t9; 94: $[14] = t11; 95: } else { 96: t11 = $[14]; 97: } 98: return t11; 99: }

File: src/components/PromptInput/HistorySearchInput.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { stringWidth } from '../../ink/stringWidth.js'; 4: import { Box, Text } from '../../ink.js'; 5: import TextInput from '../TextInput.js'; 6: type Props = { 7: value: string; 8: onChange: (value: string) => void; 9: historyFailedMatch: boolean; 10: }; 11: function HistorySearchInput(t0) { 12: const $ = _c(9); 13: const { 14: value, 15: onChange, 16: historyFailedMatch 17: } = t0; 18: const t1 = historyFailedMatch ? "no matching prompt:" : "search prompts:"; 19: let t2; 20: if ($[0] !== t1) { 21: t2 = <Text dimColor={true}>{t1}</Text>; 22: $[0] = t1; 23: $[1] = t2; 24: } else { 25: t2 = $[1]; 26: } 27: const t3 = stringWidth(value) + 1; 28: let t4; 29: if ($[2] !== onChange || $[3] !== t3 || $[4] !== value) { 30: t4 = <TextInput value={value} onChange={onChange} cursorOffset={value.length} onChangeCursorOffset={_temp} columns={t3} focus={true} showCursor={true} multiline={false} dimColor={true} />; 31: $[2] = onChange; 32: $[3] = t3; 33: $[4] = value; 34: $[5] = t4; 35: } else { 36: t4 = $[5]; 37: } 38: let t5; 39: if ($[6] !== t2 || $[7] !== t4) { 40: t5 = <Box gap={1}>{t2}{t4}</Box>; 41: $[6] = t2; 42: $[7] = t4; 43: $[8] = t5; 44: } else { 45: t5 = $[8]; 46: } 47: return t5; 48: } 49: function _temp() {} 50: export default HistorySearchInput;

File: src/components/PromptInput/inputModes.ts

typescript 1: import type { HistoryMode } from 'src/hooks/useArrowKeyHistory.js' 2: import type { PromptInputMode } from 'src/types/textInputTypes.js' 3: export function prependModeCharacterToInput( 4: input: string, 5: mode: PromptInputMode, 6: ): string { 7: switch (mode) { 8: case 'bash': 9: return `!${input}` 10: default: 11: return input 12: } 13: } 14: export function getModeFromInput(input: string): HistoryMode { 15: if (input.startsWith('!')) { 16: return 'bash' 17: } 18: return 'prompt' 19: } 20: export function getValueFromInput(input: string): string { 21: const mode = getModeFromInput(input) 22: if (mode === 'prompt') { 23: return input 24: } 25: return input.slice(1) 26: } 27: export function isInputModeCharacter(input: string): boolean { 28: return input === '!' 29: }

File: src/components/PromptInput/inputPaste.ts

typescript 1: import { getPastedTextRefNumLines } from 'src/history.js' 2: import type { PastedContent } from 'src/utils/config.js' 3: const TRUNCATION_THRESHOLD = 10000 4: const PREVIEW_LENGTH = 1000 5: type TruncatedMessage = { 6: truncatedText: string 7: placeholderContent: string 8: } 9: export function maybeTruncateMessageForInput( 10: text: string, 11: nextPasteId: number, 12: ): TruncatedMessage { 13: if (text.length <= TRUNCATION_THRESHOLD) { 14: return { 15: truncatedText: text, 16: placeholderContent: '', 17: } 18: } 19: // Calculate how much text to keep from start and end 20: const startLength = Math.floor(PREVIEW_LENGTH / 2) 21: const endLength = Math.floor(PREVIEW_LENGTH / 2) 22: // Extract the portions we'll keep 23: const startText = text.slice(0, startLength) 24: const endText = text.slice(-endLength) 25: const placeholderContent = text.slice(startLength, -endLength) 26: const truncatedLines = getPastedTextRefNumLines(placeholderContent) 27: const placeholderId = nextPasteId 28: const placeholderRef = formatTruncatedTextRef(placeholderId, truncatedLines) 29: const truncatedText = startText + placeholderRef + endText 30: return { 31: truncatedText, 32: placeholderContent, 33: } 34: } 35: function formatTruncatedTextRef(id: number, numLines: number): string { 36: return `[...Truncated text #${id} +${numLines} lines...]` 37: } 38: export function maybeTruncateInput( 39: input: string, 40: pastedContents: Record<number, PastedContent>, 41: ): { newInput: string; newPastedContents: Record<number, PastedContent> } { 42: const existingIds = Object.keys(pastedContents).map(Number) 43: const nextPasteId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1 44: const { truncatedText, placeholderContent } = maybeTruncateMessageForInput( 45: input, 46: nextPasteId, 47: ) 48: if (!placeholderContent) { 49: return { newInput: input, newPastedContents: pastedContents } 50: } 51: return { 52: newInput: truncatedText, 53: newPastedContents: { 54: ...pastedContents, 55: [nextPasteId]: { 56: id: nextPasteId, 57: type: 'text', 58: content: placeholderContent, 59: }, 60: }, 61: } 62: }

File: src/components/PromptInput/IssueFlagBanner.tsx

typescript 1: import * as React from 'react'; 2: import { FLAG_ICON } from '../../constants/figures.js'; 3: import { Box, Text } from '../../ink.js'; 4: export function IssueFlagBanner() { 5: return null; 6: }

File: src/components/PromptInput/Notifications.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import * as React from 'react'; 4: import { type ReactNode, useEffect, useMemo, useState } from 'react'; 5: import { type Notification, useNotifications } from 'src/context/notifications.js'; 6: import { logEvent } from 'src/services/analytics/index.js'; 7: import { useAppState } from 'src/state/AppState.js'; 8: import { useVoiceState } from '../../context/voice.js'; 9: import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'; 10: import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js'; 11: import type { IDESelection } from '../../hooks/useIdeSelection.js'; 12: import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; 13: import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'; 14: import { Box, Text } from '../../ink.js'; 15: import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'; 16: import { calculateTokenWarningState } from '../../services/compact/autoCompact.js'; 17: import type { MCPServerConnection } from '../../services/mcp/types.js'; 18: import type { Message } from '../../types/message.js'; 19: import { getApiKeyHelperElapsedMs, getConfiguredApiKeyHelper, getSubscriptionType } from '../../utils/auth.js'; 20: import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'; 21: import { getExternalEditor } from '../../utils/editor.js'; 22: import { isEnvTruthy } from '../../utils/envUtils.js'; 23: import { formatDuration } from '../../utils/format.js'; 24: import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js'; 25: import { toIDEDisplayName } from '../../utils/ide.js'; 26: import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'; 27: import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js'; 28: import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js'; 29: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 30: import { IdeStatusIndicator } from '../IdeStatusIndicator.js'; 31: import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js'; 32: import { SentryErrorBoundary } from '../SentryErrorBoundary.js'; 33: import { TokenWarning } from '../TokenWarning.js'; 34: import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js'; 35: const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator = feature('VOICE_MODE') ? require('./VoiceIndicator.js').VoiceIndicator : () => null; 36: export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000; 37: type Props = { 38: apiKeyStatus: VerificationStatus; 39: autoUpdaterResult: AutoUpdaterResult | null; 40: isAutoUpdating: boolean; 41: debug: boolean; 42: verbose: boolean; 43: messages: Message[]; 44: onAutoUpdaterResult: (result: AutoUpdaterResult) => void; 45: onChangeIsUpdating: (isUpdating: boolean) => void; 46: ideSelection: IDESelection | undefined; 47: mcpClients?: MCPServerConnection[]; 48: isInputWrapped?: boolean; 49: isNarrow?: boolean; 50: }; 51: export function Notifications(t0) { 52: const $ = _c(34); 53: const { 54: apiKeyStatus, 55: autoUpdaterResult, 56: debug, 57: isAutoUpdating, 58: verbose, 59: messages, 60: onAutoUpdaterResult, 61: onChangeIsUpdating, 62: ideSelection, 63: mcpClients, 64: isInputWrapped: t1, 65: isNarrow: t2 66: } = t0; 67: const isInputWrapped = t1 === undefined ? false : t1; 68: const isNarrow = t2 === undefined ? false : t2; 69: let t3; 70: if ($[0] !== messages) { 71: const messagesForTokenCount = getMessagesAfterCompactBoundary(messages); 72: t3 = tokenCountFromLastAPIResponse(messagesForTokenCount); 73: $[0] = messages; 74: $[1] = t3; 75: } else { 76: t3 = $[1]; 77: } 78: const tokenUsage = t3; 79: const mainLoopModel = useMainLoopModel(); 80: let t4; 81: if ($[2] !== mainLoopModel || $[3] !== tokenUsage) { 82: t4 = calculateTokenWarningState(tokenUsage, mainLoopModel); 83: $[2] = mainLoopModel; 84: $[3] = tokenUsage; 85: $[4] = t4; 86: } else { 87: t4 = $[4]; 88: } 89: const isShowingCompactMessage = t4.isAboveWarningThreshold; 90: const { 91: status: ideStatus 92: } = useIdeConnectionStatus(mcpClients); 93: const notifications = useAppState(_temp); 94: const { 95: addNotification, 96: removeNotification 97: } = useNotifications(); 98: const claudeAiLimits = useClaudeAiLimits(); 99: let t5; 100: let t6; 101: if ($[5] !== addNotification) { 102: t5 = () => { 103: setEnvHookNotifier((text, isError) => { 104: addNotification({ 105: key: "env-hook", 106: text, 107: color: isError ? "error" : undefined, 108: priority: isError ? "medium" : "low", 109: timeoutMs: isError ? 8000 : 5000 110: }); 111: }); 112: return _temp2; 113: }; 114: t6 = [addNotification]; 115: $[5] = addNotification; 116: $[6] = t5; 117: $[7] = t6; 118: } else { 119: t5 = $[6]; 120: t6 = $[7]; 121: } 122: useEffect(t5, t6); 123: const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0); 124: const shouldShowAutoUpdater = !shouldShowIdeSelection || isAutoUpdating || autoUpdaterResult?.status !== "success"; 125: const isInOverageMode = claudeAiLimits.isUsingOverage; 126: let t7; 127: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 128: t7 = getSubscriptionType(); 129: $[8] = t7; 130: } else { 131: t7 = $[8]; 132: } 133: const subscriptionType = t7; 134: const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise"; 135: let t8; 136: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 137: t8 = getExternalEditor(); 138: $[9] = t8; 139: } else { 140: t8 = $[9]; 141: } 142: const editor = t8; 143: const shouldShowExternalEditorHint = isInputWrapped && !isShowingCompactMessage && apiKeyStatus !== "invalid" && apiKeyStatus !== "missing" && editor !== undefined; 144: let t10; 145: let t9; 146: if ($[10] !== addNotification || $[11] !== removeNotification || $[12] !== shouldShowExternalEditorHint) { 147: t9 = () => { 148: if (shouldShowExternalEditorHint && editor) { 149: logEvent("tengu_external_editor_hint_shown", {}); 150: addNotification({ 151: key: "external-editor-hint", 152: jsx: <Text dimColor={true}><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description={`edit in ${toIDEDisplayName(editor)}`} /></Text>, 153: priority: "immediate", 154: timeoutMs: 5000 155: }); 156: } else { 157: removeNotification("external-editor-hint"); 158: } 159: }; 160: t10 = [shouldShowExternalEditorHint, editor, addNotification, removeNotification]; 161: $[10] = addNotification; 162: $[11] = removeNotification; 163: $[12] = shouldShowExternalEditorHint; 164: $[13] = t10; 165: $[14] = t9; 166: } else { 167: t10 = $[13]; 168: t9 = $[14]; 169: } 170: useEffect(t9, t10); 171: const t11 = isNarrow ? "flex-start" : "flex-end"; 172: const t12 = isInOverageMode ?? false; 173: let t13; 174: if ($[15] !== apiKeyStatus || $[16] !== autoUpdaterResult || $[17] !== debug || $[18] !== ideSelection || $[19] !== isAutoUpdating || $[20] !== isShowingCompactMessage || $[21] !== mainLoopModel || $[22] !== mcpClients || $[23] !== notifications || $[24] !== onAutoUpdaterResult || $[25] !== onChangeIsUpdating || $[26] !== shouldShowAutoUpdater || $[27] !== t12 || $[28] !== tokenUsage || $[29] !== verbose) { 175: t13 = <NotificationContent ideSelection={ideSelection} mcpClients={mcpClients} notifications={notifications} isInOverageMode={t12} isTeamOrEnterprise={isTeamOrEnterprise} apiKeyStatus={apiKeyStatus} debug={debug} verbose={verbose} tokenUsage={tokenUsage} mainLoopModel={mainLoopModel} shouldShowAutoUpdater={shouldShowAutoUpdater} autoUpdaterResult={autoUpdaterResult} isAutoUpdating={isAutoUpdating} isShowingCompactMessage={isShowingCompactMessage} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} />; 176: $[15] = apiKeyStatus; 177: $[16] = autoUpdaterResult; 178: $[17] = debug; 179: $[18] = ideSelection; 180: $[19] = isAutoUpdating; 181: $[20] = isShowingCompactMessage; 182: $[21] = mainLoopModel; 183: $[22] = mcpClients; 184: $[23] = notifications; 185: $[24] = onAutoUpdaterResult; 186: $[25] = onChangeIsUpdating; 187: $[26] = shouldShowAutoUpdater; 188: $[27] = t12; 189: $[28] = tokenUsage; 190: $[29] = verbose; 191: $[30] = t13; 192: } else { 193: t13 = $[30]; 194: } 195: let t14; 196: if ($[31] !== t11 || $[32] !== t13) { 197: t14 = <SentryErrorBoundary><Box flexDirection="column" alignItems={t11} flexShrink={0} overflowX="hidden">{t13}</Box></SentryErrorBoundary>; 198: $[31] = t11; 199: $[32] = t13; 200: $[33] = t14; 201: } else { 202: t14 = $[33]; 203: } 204: return t14; 205: } 206: function _temp2() { 207: return setEnvHookNotifier(null); 208: } 209: function _temp(s) { 210: return s.notifications; 211: } 212: function NotificationContent({ 213: ideSelection, 214: mcpClients, 215: notifications, 216: isInOverageMode, 217: isTeamOrEnterprise, 218: apiKeyStatus, 219: debug, 220: verbose, 221: tokenUsage, 222: mainLoopModel, 223: shouldShowAutoUpdater, 224: autoUpdaterResult, 225: isAutoUpdating, 226: isShowingCompactMessage, 227: onAutoUpdaterResult, 228: onChangeIsUpdating 229: }: { 230: ideSelection: IDESelection | undefined; 231: mcpClients?: MCPServerConnection[]; 232: notifications: { 233: current: Notification | null; 234: queue: Notification[]; 235: }; 236: isInOverageMode: boolean; 237: isTeamOrEnterprise: boolean; 238: apiKeyStatus: VerificationStatus; 239: debug: boolean; 240: verbose: boolean; 241: tokenUsage: number; 242: mainLoopModel: string; 243: shouldShowAutoUpdater: boolean; 244: autoUpdaterResult: AutoUpdaterResult | null; 245: isAutoUpdating: boolean; 246: isShowingCompactMessage: boolean; 247: onAutoUpdaterResult: (result: AutoUpdaterResult) => void; 248: onChangeIsUpdating: (isUpdating: boolean) => void; 249: }): ReactNode { 250: const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState<string | null>(null); 251: useEffect(() => { 252: if (!getConfiguredApiKeyHelper()) return; 253: const interval = setInterval((setSlow: React.Dispatch<React.SetStateAction<string | null>>) => { 254: const ms = getApiKeyHelperElapsedMs(); 255: const next = ms >= 10_000 ? formatDuration(ms) : null; 256: setSlow(prev => next === prev ? prev : next); 257: }, 1000, setApiKeyHelperSlow); 258: return () => clearInterval(interval); 259: }, []); 260: const voiceState = feature('VOICE_MODE') ? 261: useVoiceState(s => s.voiceState) : 'idle' as const; 262: const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false; 263: const voiceError = feature('VOICE_MODE') ? 264: useVoiceState(s_0 => s_0.voiceError) : null; 265: const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ? 266: useAppState(s_1 => s_1.isBriefOnly) : false; 267: if (feature('VOICE_MODE') && voiceEnabled && (voiceState === 'recording' || voiceState === 'processing')) { 268: return <VoiceIndicator voiceState={voiceState} />; 269: } 270: return <> 271: <IdeStatusIndicator ideSelection={ideSelection} mcpClients={mcpClients} /> 272: {notifications.current && ('jsx' in notifications.current ? <Text wrap="truncate" key={notifications.current.key}> 273: {notifications.current.jsx} 274: </Text> : <Text color={notifications.current.color} dimColor={!notifications.current.color} wrap="truncate"> 275: {notifications.current.text} 276: </Text>)} 277: {isInOverageMode && !isTeamOrEnterprise && <Box> 278: <Text dimColor wrap="truncate"> 279: Now using extra usage 280: </Text> 281: </Box>} 282: {apiKeyHelperSlow && <Box> 283: <Text color="warning" wrap="truncate"> 284: apiKeyHelper is taking a while{' '} 285: </Text> 286: <Text dimColor wrap="truncate"> 287: ({apiKeyHelperSlow}) 288: </Text> 289: </Box>} 290: {(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && <Box> 291: <Text color="error" wrap="truncate"> 292: {isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ? 'Authentication error · Try again' : 'Not logged in · Run /login'} 293: </Text> 294: </Box>} 295: {debug && <Box> 296: <Text color="warning" wrap="truncate"> 297: Debug mode 298: </Text> 299: </Box>} 300: {apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && <Box> 301: <Text dimColor wrap="truncate"> 302: {tokenUsage} tokens 303: </Text> 304: </Box>} 305: {!isBriefOnly && <TokenWarning tokenUsage={tokenUsage} model={mainLoopModel} />} 306: {shouldShowAutoUpdater && <AutoUpdaterWrapper verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isAutoUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={!isShowingCompactMessage} />} 307: {feature('VOICE_MODE') ? voiceEnabled && voiceError && <Box> 308: <Text color="error" wrap="truncate"> 309: {voiceError} 310: </Text> 311: </Box> : null} 312: <MemoryUsageIndicator /> 313: <SandboxPromptFooterHint /> 314: </>; 315: }

File: src/components/PromptInput/PromptInput.tsx

typescript 1: import { feature } from 'bun:bundle'; 2: import chalk from 'chalk'; 3: import * as path from 'path'; 4: import * as React from 'react'; 5: import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'; 6: import { useNotifications } from 'src/context/notifications.js'; 7: import { useCommandQueue } from 'src/hooks/useCommandQueue.js'; 8: import { type IDEAtMentioned, useIdeAtMentioned } from 'src/hooks/useIdeAtMentioned.js'; 9: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; 10: import { type AppState, useAppState, useAppStateStore, useSetAppState } from 'src/state/AppState.js'; 11: import type { FooterItem } from 'src/state/AppStateStore.js'; 12: import { getCwd } from 'src/utils/cwd.js'; 13: import { isQueuedCommandEditable, popAllEditable } from 'src/utils/messageQueueManager.js'; 14: import stripAnsi from 'strip-ansi'; 15: import { companionReservedColumns } from '../../buddy/CompanionSprite.js'; 16: import { findBuddyTriggerPositions, useBuddyNotification } from '../../buddy/useBuddyNotification.js'; 17: import { FastModePicker } from '../../commands/fast/fast.js'; 18: import { isUltrareviewEnabled } from '../../commands/review/ultrareviewEnabled.js'; 19: import { getNativeCSIuTerminalDisplayName } from '../../commands/terminalSetup/terminalSetup.js'; 20: import { type Command, hasCommand } from '../../commands.js'; 21: import { useIsModalOverlayActive } from '../../context/overlayContext.js'; 22: import { useSetPromptOverlayDialog } from '../../context/promptOverlayContext.js'; 23: import { formatImageRef, formatPastedTextRef, getPastedTextRefNumLines, parseReferences } from '../../history.js'; 24: import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'; 25: import { type HistoryMode, useArrowKeyHistory } from '../../hooks/useArrowKeyHistory.js'; 26: import { useDoublePress } from '../../hooks/useDoublePress.js'; 27: import { useHistorySearch } from '../../hooks/useHistorySearch.js'; 28: import type { IDESelection } from '../../hooks/useIdeSelection.js'; 29: import { useInputBuffer } from '../../hooks/useInputBuffer.js'; 30: import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; 31: import { usePromptSuggestion } from '../../hooks/usePromptSuggestion.js'; 32: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 33: import { useTypeahead } from '../../hooks/useTypeahead.js'; 34: import type { BorderTextOptions } from '../../ink/render-border.js'; 35: import { stringWidth } from '../../ink/stringWidth.js'; 36: import { Box, type ClickEvent, type Key, Text, useInput } from '../../ink.js'; 37: import { useOptionalKeybindingContext } from '../../keybindings/KeybindingContext.js'; 38: import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js'; 39: import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js'; 40: import type { MCPServerConnection } from '../../services/mcp/types.js'; 41: import { abortPromptSuggestion, logSuggestionSuppressed } from '../../services/PromptSuggestion/promptSuggestion.js'; 42: import { type ActiveSpeculationState, abortSpeculation } from '../../services/PromptSuggestion/speculation.js'; 43: import { getActiveAgentForInput, getViewedTeammateTask } from '../../state/selectors.js'; 44: import { enterTeammateView, exitTeammateView, stopOrDismissAgent } from '../../state/teammateViewHelpers.js'; 45: import type { ToolPermissionContext } from '../../Tool.js'; 46: import { getRunningTeammatesSorted } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js'; 47: import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'; 48: import { isPanelAgentTask, type LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js'; 49: import { isBackgroundTask } from '../../tasks/types.js'; 50: import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js'; 51: import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js'; 52: import type { Message } from '../../types/message.js'; 53: import type { PermissionMode } from '../../types/permissions.js'; 54: import type { BaseTextInputProps, PromptInputMode, VimMode } from '../../types/textInputTypes.js'; 55: import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'; 56: import { count } from '../../utils/array.js'; 57: import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'; 58: import { Cursor } from '../../utils/Cursor.js'; 59: import { getGlobalConfig, type PastedContent, saveGlobalConfig } from '../../utils/config.js'; 60: import { logForDebugging } from '../../utils/debug.js'; 61: import { parseDirectMemberMessage, sendDirectMemberMessage } from '../../utils/directMemberMessage.js'; 62: import type { EffortLevel } from '../../utils/effort.js'; 63: import { env } from '../../utils/env.js'; 64: import { errorMessage } from '../../utils/errors.js'; 65: import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'; 66: import { getFastModeUnavailableReason, isFastModeAvailable, isFastModeCooldown, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js'; 67: import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; 68: import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js'; 69: import { getImageFromClipboard, PASTE_THRESHOLD } from '../../utils/imagePaste.js'; 70: import type { ImageDimensions } from '../../utils/imageResizer.js'; 71: import { cacheImagePath, storeImage } from '../../utils/imageStore.js'; 72: import { isMacosOptionChar, MACOS_OPTION_SPECIAL_CHARS } from '../../utils/keyboardShortcuts.js'; 73: import { logError } from '../../utils/log.js'; 74: import { isOpus1mMergeEnabled, modelDisplayString } from '../../utils/model/model.js'; 75: import { setAutoModeActive } from '../../utils/permissions/autoModeState.js'; 76: import { cyclePermissionMode, getNextPermissionMode } from '../../utils/permissions/getNextPermissionMode.js'; 77: import { transitionPermissionMode } from '../../utils/permissions/permissionSetup.js'; 78: import { getPlatform } from '../../utils/platform.js'; 79: import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js'; 80: import { editPromptInEditor } from '../../utils/promptEditor.js'; 81: import { hasAutoModeOptIn } from '../../utils/settings/settings.js'; 82: import { findBtwTriggerPositions } from '../../utils/sideQuestion.js'; 83: import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js'; 84: import { findSlackChannelPositions, getKnownChannelsVersion, hasSlackMcpServer, subscribeKnownChannels } from '../../utils/suggestions/slackChannelSuggestions.js'; 85: import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js'; 86: import { syncTeammateMode } from '../../utils/swarm/teamHelpers.js'; 87: import type { TeamSummary } from '../../utils/teamDiscovery.js'; 88: import { getTeammateColor } from '../../utils/teammate.js'; 89: import { isInProcessTeammate } from '../../utils/teammateContext.js'; 90: import { writeToMailbox } from '../../utils/teammateMailbox.js'; 91: import type { TextHighlight } from '../../utils/textHighlighting.js'; 92: import type { Theme } from '../../utils/theme.js'; 93: import { findThinkingTriggerPositions, getRainbowColor, isUltrathinkEnabled } from '../../utils/thinking.js'; 94: import { findTokenBudgetPositions } from '../../utils/tokenBudget.js'; 95: import { findUltraplanTriggerPositions, findUltrareviewTriggerPositions } from '../../utils/ultraplan/keyword.js'; 96: import { AutoModeOptInDialog } from '../AutoModeOptInDialog.js'; 97: import { BridgeDialog } from '../BridgeDialog.js'; 98: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 99: import { getVisibleAgentTasks, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js'; 100: import { getEffortNotificationText } from '../EffortIndicator.js'; 101: import { getFastIconString } from '../FastIcon.js'; 102: import { GlobalSearchDialog } from '../GlobalSearchDialog.js'; 103: import { HistorySearchDialog } from '../HistorySearchDialog.js'; 104: import { ModelPicker } from '../ModelPicker.js'; 105: import { QuickOpenDialog } from '../QuickOpenDialog.js'; 106: import TextInput from '../TextInput.js'; 107: import { ThinkingToggle } from '../ThinkingToggle.js'; 108: import { BackgroundTasksDialog } from '../tasks/BackgroundTasksDialog.js'; 109: import { shouldHideTasksFooter } from '../tasks/taskStatusUtils.js'; 110: import { TeamsDialog } from '../teams/TeamsDialog.js'; 111: import VimTextInput from '../VimTextInput.js'; 112: import { getModeFromInput, getValueFromInput } from './inputModes.js'; 113: import { FOOTER_TEMPORARY_STATUS_TIMEOUT, Notifications } from './Notifications.js'; 114: import PromptInputFooter from './PromptInputFooter.js'; 115: import type { SuggestionItem } from './PromptInputFooterSuggestions.js'; 116: import { PromptInputModeIndicator } from './PromptInputModeIndicator.js'; 117: import { PromptInputQueuedCommands } from './PromptInputQueuedCommands.js'; 118: import { PromptInputStashNotice } from './PromptInputStashNotice.js'; 119: import { useMaybeTruncateInput } from './useMaybeTruncateInput.js'; 120: import { usePromptInputPlaceholder } from './usePromptInputPlaceholder.js'; 121: import { useShowFastIconHint } from './useShowFastIconHint.js'; 122: import { useSwarmBanner } from './useSwarmBanner.js'; 123: import { isNonSpacePrintable, isVimModeEnabled } from './utils.js'; 124: type Props = { 125: debug: boolean; 126: ideSelection: IDESelection | undefined; 127: toolPermissionContext: ToolPermissionContext; 128: setToolPermissionContext: (ctx: ToolPermissionContext) => void; 129: apiKeyStatus: VerificationStatus; 130: commands: Command[]; 131: agents: AgentDefinition[]; 132: isLoading: boolean; 133: verbose: boolean; 134: messages: Message[]; 135: onAutoUpdaterResult: (result: AutoUpdaterResult) => void; 136: autoUpdaterResult: AutoUpdaterResult | null; 137: input: string; 138: onInputChange: (value: string) => void; 139: mode: PromptInputMode; 140: onModeChange: (mode: PromptInputMode) => void; 141: stashedPrompt: { 142: text: string; 143: cursorOffset: number; 144: pastedContents: Record<number, PastedContent>; 145: } | undefined; 146: setStashedPrompt: (value: { 147: text: string; 148: cursorOffset: number; 149: pastedContents: Record<number, PastedContent>; 150: } | undefined) => void; 151: submitCount: number; 152: onShowMessageSelector: () => void; 153: onMessageActionsEnter?: () => void; 154: mcpClients: MCPServerConnection[]; 155: pastedContents: Record<number, PastedContent>; 156: setPastedContents: React.Dispatch<React.SetStateAction<Record<number, PastedContent>>>; 157: vimMode: VimMode; 158: setVimMode: (mode: VimMode) => void; 159: showBashesDialog: string | boolean; 160: setShowBashesDialog: (show: string | boolean) => void; 161: onExit: () => void; 162: getToolUseContext: (messages: Message[], newMessages: Message[], abortController: AbortController, mainLoopModel: string) => ProcessUserInputContext; 163: onSubmit: (input: string, helpers: PromptInputHelpers, speculationAccept?: { 164: state: ActiveSpeculationState; 165: speculationSessionTimeSavedMs: number; 166: setAppState: (f: (prev: AppState) => AppState) => void; 167: }, options?: { 168: fromKeybinding?: boolean; 169: }) => Promise<void>; 170: onAgentSubmit?: (input: string, task: InProcessTeammateTaskState | LocalAgentTaskState, helpers: PromptInputHelpers) => Promise<void>; 171: isSearchingHistory: boolean; 172: setIsSearchingHistory: (isSearching: boolean) => void; 173: onDismissSideQuestion?: () => void; 174: isSideQuestionVisible?: boolean; 175: helpOpen: boolean; 176: setHelpOpen: React.Dispatch<React.SetStateAction<boolean>>; 177: hasSuppressedDialogs?: boolean; 178: isLocalJSXCommandActive?: boolean; 179: insertTextRef?: React.MutableRefObject<{ 180: insert: (text: string) => void; 181: setInputWithCursor: (value: string, cursor: number) => void; 182: cursorOffset: number; 183: } | null>; 184: voiceInterimRange?: { 185: start: number; 186: end: number; 187: } | null; 188: }; 189: const PROMPT_FOOTER_LINES = 5; 190: const MIN_INPUT_VIEWPORT_LINES = 3; 191: function PromptInput({ 192: debug, 193: ideSelection, 194: toolPermissionContext, 195: setToolPermissionContext, 196: apiKeyStatus, 197: commands, 198: agents, 199: isLoading, 200: verbose, 201: messages, 202: onAutoUpdaterResult, 203: autoUpdaterResult, 204: input, 205: onInputChange, 206: mode, 207: onModeChange, 208: stashedPrompt, 209: setStashedPrompt, 210: submitCount, 211: onShowMessageSelector, 212: onMessageActionsEnter, 213: mcpClients, 214: pastedContents, 215: setPastedContents, 216: vimMode, 217: setVimMode, 218: showBashesDialog, 219: setShowBashesDialog, 220: onExit, 221: getToolUseContext, 222: onSubmit: onSubmitProp, 223: onAgentSubmit, 224: isSearchingHistory, 225: setIsSearchingHistory, 226: onDismissSideQuestion, 227: isSideQuestionVisible, 228: helpOpen, 229: setHelpOpen, 230: hasSuppressedDialogs, 231: isLocalJSXCommandActive = false, 232: insertTextRef, 233: voiceInterimRange 234: }: Props): React.ReactNode { 235: const mainLoopModel = useMainLoopModel(); 236: const isModalOverlayActive = useIsModalOverlayActive() || isLocalJSXCommandActive; 237: const [isAutoUpdating, setIsAutoUpdating] = useState(false); 238: const [exitMessage, setExitMessage] = useState<{ 239: show: boolean; 240: key?: string; 241: }>({ 242: show: false 243: }); 244: const [cursorOffset, setCursorOffset] = useState<number>(input.length); 245: const lastInternalInputRef = React.useRef(input); 246: if (input !== lastInternalInputRef.current) { 247: setCursorOffset(input.length); 248: lastInternalInputRef.current = input; 249: } 250: const trackAndSetInput = React.useCallback((value: string) => { 251: lastInternalInputRef.current = value; 252: onInputChange(value); 253: }, [onInputChange]); 254: if (insertTextRef) { 255: insertTextRef.current = { 256: cursorOffset, 257: insert: (text: string) => { 258: const needsSpace = cursorOffset === input.length && input.length > 0 && !/\s$/.test(input); 259: const insertText = needsSpace ? ' ' + text : text; 260: const newValue = input.slice(0, cursorOffset) + insertText + input.slice(cursorOffset); 261: lastInternalInputRef.current = newValue; 262: onInputChange(newValue); 263: setCursorOffset(cursorOffset + insertText.length); 264: }, 265: setInputWithCursor: (value: string, cursor: number) => { 266: lastInternalInputRef.current = value; 267: onInputChange(value); 268: setCursorOffset(cursor); 269: } 270: }; 271: } 272: const store = useAppStateStore(); 273: const setAppState = useSetAppState(); 274: const tasks = useAppState(s => s.tasks); 275: const replBridgeConnected = useAppState(s => s.replBridgeConnected); 276: const replBridgeExplicit = useAppState(s => s.replBridgeExplicit); 277: const replBridgeReconnecting = useAppState(s => s.replBridgeReconnecting); 278: const bridgeFooterVisible = replBridgeConnected && (replBridgeExplicit || replBridgeReconnecting); 279: const hasTungstenSession = useAppState(s => "external" === 'ant' && s.tungstenActiveSession !== undefined); 280: const tmuxFooterVisible = "external" === 'ant' && hasTungstenSession; 281: const bagelFooterVisible = useAppState(s => false); 282: const teamContext = useAppState(s => s.teamContext); 283: const queuedCommands = useCommandQueue(); 284: const promptSuggestionState = useAppState(s => s.promptSuggestion); 285: const speculation = useAppState(s => s.speculation); 286: const speculationSessionTimeSavedMs = useAppState(s => s.speculationSessionTimeSavedMs); 287: const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId); 288: const viewSelectionMode = useAppState(s => s.viewSelectionMode); 289: const showSpinnerTree = useAppState(s => s.expandedView) === 'teammates'; 290: const { 291: companion: _companion, 292: companionMuted 293: } = feature('BUDDY') ? getGlobalConfig() : { 294: companion: undefined, 295: companionMuted: undefined 296: }; 297: const companionFooterVisible = !!_companion && !companionMuted; 298: const briefOwnsGap = feature('KAIROS') || feature('KAIROS_BRIEF') ? 299: useAppState(s => s.isBriefOnly) && !viewingAgentTaskId : false; 300: const mainLoopModel_ = useAppState(s => s.mainLoopModel); 301: const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession); 302: const thinkingEnabled = useAppState(s => s.thinkingEnabled); 303: const isFastMode = useAppState(s => isFastModeEnabled() ? s.fastMode : false); 304: const effortValue = useAppState(s => s.effortValue); 305: const viewedTeammate = getViewedTeammateTask(store.getState()); 306: const viewingAgentName = viewedTeammate?.identity.agentName; 307: const viewingAgentColor = viewedTeammate?.identity.color && AGENT_COLORS.includes(viewedTeammate.identity.color as AgentColorName) ? viewedTeammate.identity.color as AgentColorName : undefined; 308: const inProcessTeammates = useMemo(() => getRunningTeammatesSorted(tasks), [tasks]); 309: const isTeammateMode = inProcessTeammates.length > 0 || viewedTeammate !== undefined; 310: const effectiveToolPermissionContext = useMemo((): ToolPermissionContext => { 311: if (viewedTeammate) { 312: return { 313: ...toolPermissionContext, 314: mode: viewedTeammate.permissionMode 315: }; 316: } 317: return toolPermissionContext; 318: }, [viewedTeammate, toolPermissionContext]); 319: const { 320: historyQuery, 321: setHistoryQuery, 322: historyMatch, 323: historyFailedMatch 324: } = useHistorySearch(entry => { 325: setPastedContents(entry.pastedContents); 326: void onSubmit(entry.display); 327: }, input, trackAndSetInput, setCursorOffset, cursorOffset, onModeChange, mode, isSearchingHistory, setIsSearchingHistory, setPastedContents, pastedContents); 328: const nextPasteIdRef = useRef(-1); 329: if (nextPasteIdRef.current === -1) { 330: nextPasteIdRef.current = getInitialPasteId(messages); 331: } 332: const pendingSpaceAfterPillRef = useRef(false); 333: const [showTeamsDialog, setShowTeamsDialog] = useState(false); 334: const [showBridgeDialog, setShowBridgeDialog] = useState(false); 335: const [teammateFooterIndex, setTeammateFooterIndex] = useState(0); 336: const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex); 337: const setCoordinatorTaskIndex = useCallback((v: number | ((prev: number) => number)) => setAppState(prev => { 338: const next = typeof v === 'function' ? v(prev.coordinatorTaskIndex) : v; 339: if (next === prev.coordinatorTaskIndex) return prev; 340: return { 341: ...prev, 342: coordinatorTaskIndex: next 343: }; 344: }), [setAppState]); 345: const coordinatorTaskCount = useCoordinatorTaskCount(); 346: const hasBgTaskPill = useMemo(() => Object.values(tasks).some(t => isBackgroundTask(t) && !("external" === 'ant' && isPanelAgentTask(t))), [tasks]); 347: const minCoordinatorIndex = hasBgTaskPill ? -1 : 0; 348: useEffect(() => { 349: if (coordinatorTaskIndex >= coordinatorTaskCount) { 350: setCoordinatorTaskIndex(Math.max(minCoordinatorIndex, coordinatorTaskCount - 1)); 351: } else if (coordinatorTaskIndex < minCoordinatorIndex) { 352: setCoordinatorTaskIndex(minCoordinatorIndex); 353: } 354: }, [coordinatorTaskCount, coordinatorTaskIndex, minCoordinatorIndex]); 355: const [isPasting, setIsPasting] = useState(false); 356: const [isExternalEditorActive, setIsExternalEditorActive] = useState(false); 357: const [showModelPicker, setShowModelPicker] = useState(false); 358: const [showQuickOpen, setShowQuickOpen] = useState(false); 359: const [showGlobalSearch, setShowGlobalSearch] = useState(false); 360: const [showHistoryPicker, setShowHistoryPicker] = useState(false); 361: const [showFastModePicker, setShowFastModePicker] = useState(false); 362: const [showThinkingToggle, setShowThinkingToggle] = useState(false); 363: const [showAutoModeOptIn, setShowAutoModeOptIn] = useState(false); 364: const [previousModeBeforeAuto, setPreviousModeBeforeAuto] = useState<PermissionMode | null>(null); 365: const autoModeOptInTimeoutRef = useRef<NodeJS.Timeout | null>(null); 366: const isCursorOnFirstLine = useMemo(() => { 367: const firstNewlineIndex = input.indexOf('\n'); 368: if (firstNewlineIndex === -1) { 369: return true; 370: } 371: return cursorOffset <= firstNewlineIndex; 372: }, [input, cursorOffset]); 373: const isCursorOnLastLine = useMemo(() => { 374: const lastNewlineIndex = input.lastIndexOf('\n'); 375: if (lastNewlineIndex === -1) { 376: return true; 377: } 378: return cursorOffset > lastNewlineIndex; 379: }, [input, cursorOffset]); 380: const cachedTeams: TeamSummary[] = useMemo(() => { 381: if (!isAgentSwarmsEnabled()) return []; 382: if (isInProcessEnabled()) return []; 383: if (!teamContext) { 384: return []; 385: } 386: const teammateCount = count(Object.values(teamContext.teammates), t => t.name !== 'team-lead'); 387: return [{ 388: name: teamContext.teamName, 389: memberCount: teammateCount, 390: runningCount: 0, 391: idleCount: 0 392: }]; 393: }, [teamContext]); 394: const runningTaskCount = useMemo(() => count(Object.values(tasks), t => t.status === 'running'), [tasks]); 395: const tasksFooterVisible = (runningTaskCount > 0 || "external" === 'ant' && coordinatorTaskCount > 0) && !shouldHideTasksFooter(tasks, showSpinnerTree); 396: const teamsFooterVisible = cachedTeams.length > 0; 397: const footerItems = useMemo(() => [tasksFooterVisible && 'tasks', tmuxFooterVisible && 'tmux', bagelFooterVisible && 'bagel', teamsFooterVisible && 'teams', bridgeFooterVisible && 'bridge', companionFooterVisible && 'companion'].filter(Boolean) as FooterItem[], [tasksFooterVisible, tmuxFooterVisible, bagelFooterVisible, teamsFooterVisible, bridgeFooterVisible, companionFooterVisible]); 398: const rawFooterSelection = useAppState(s => s.footerSelection); 399: const footerItemSelected = rawFooterSelection && footerItems.includes(rawFooterSelection) ? rawFooterSelection : null; 400: useEffect(() => { 401: if (rawFooterSelection && !footerItemSelected) { 402: setAppState(prev => prev.footerSelection === null ? prev : { 403: ...prev, 404: footerSelection: null 405: }); 406: } 407: }, [rawFooterSelection, footerItemSelected, setAppState]); 408: const tasksSelected = footerItemSelected === 'tasks'; 409: const tmuxSelected = footerItemSelected === 'tmux'; 410: const bagelSelected = footerItemSelected === 'bagel'; 411: const teamsSelected = footerItemSelected === 'teams'; 412: const bridgeSelected = footerItemSelected === 'bridge'; 413: function selectFooterItem(item: FooterItem | null): void { 414: setAppState(prev => prev.footerSelection === item ? prev : { 415: ...prev, 416: footerSelection: item 417: }); 418: if (item === 'tasks') { 419: setTeammateFooterIndex(0); 420: setCoordinatorTaskIndex(minCoordinatorIndex); 421: } 422: } 423: function navigateFooter(delta: 1 | -1, exitAtStart = false): boolean { 424: const idx = footerItemSelected ? footerItems.indexOf(footerItemSelected) : -1; 425: const next = footerItems[idx + delta]; 426: if (next) { 427: selectFooterItem(next); 428: return true; 429: } 430: if (delta < 0 && exitAtStart) { 431: selectFooterItem(null); 432: return true; 433: } 434: return false; 435: } 436: const { 437: suggestion: promptSuggestion, 438: markAccepted, 439: logOutcomeAtSubmission, 440: markShown 441: } = usePromptSuggestion({ 442: inputValue: input, 443: isAssistantResponding: isLoading 444: }); 445: const displayedValue = useMemo(() => isSearchingHistory && historyMatch ? getValueFromInput(typeof historyMatch === 'string' ? historyMatch : historyMatch.display) : input, [isSearchingHistory, historyMatch, input]); 446: const thinkTriggers = useMemo(() => findThinkingTriggerPositions(displayedValue), [displayedValue]); 447: const ultraplanSessionUrl = useAppState(s => s.ultraplanSessionUrl); 448: const ultraplanLaunching = useAppState(s => s.ultraplanLaunching); 449: const ultraplanTriggers = useMemo(() => feature('ULTRAPLAN') && !ultraplanSessionUrl && !ultraplanLaunching ? findUltraplanTriggerPositions(displayedValue) : [], [displayedValue, ultraplanSessionUrl, ultraplanLaunching]); 450: const ultrareviewTriggers = useMemo(() => isUltrareviewEnabled() ? findUltrareviewTriggerPositions(displayedValue) : [], [displayedValue]); 451: const btwTriggers = useMemo(() => findBtwTriggerPositions(displayedValue), [displayedValue]); 452: const buddyTriggers = useMemo(() => findBuddyTriggerPositions(displayedValue), [displayedValue]); 453: const slashCommandTriggers = useMemo(() => { 454: const positions = findSlashCommandPositions(displayedValue); 455: return positions.filter(pos => { 456: const commandName = displayedValue.slice(pos.start + 1, pos.end); 457: return hasCommand(commandName, commands); 458: }); 459: }, [displayedValue, commands]); 460: const tokenBudgetTriggers = useMemo(() => feature('TOKEN_BUDGET') ? findTokenBudgetPositions(displayedValue) : [], [displayedValue]); 461: const knownChannelsVersion = useSyncExternalStore(subscribeKnownChannels, getKnownChannelsVersion); 462: const slackChannelTriggers = useMemo(() => hasSlackMcpServer(store.getState().mcp.clients) ? findSlackChannelPositions(displayedValue) : [], 463: [displayedValue, knownChannelsVersion]); 464: const memberMentionHighlights = useMemo((): Array<{ 465: start: number; 466: end: number; 467: themeColor: keyof Theme; 468: }> => { 469: if (!isAgentSwarmsEnabled()) return []; 470: if (!teamContext?.teammates) return []; 471: const highlights: Array<{ 472: start: number; 473: end: number; 474: themeColor: keyof Theme; 475: }> = []; 476: const members = teamContext.teammates; 477: if (!members) return highlights; 478: const regex = /(^|\s)@([\w-]+)/g; 479: const memberValues = Object.values(members); 480: let match; 481: while ((match = regex.exec(displayedValue)) !== null) { 482: const leadingSpace = match[1] ?? ''; 483: const nameStart = match.index + leadingSpace.length; 484: const fullMatch = match[0].trimStart(); 485: const name = match[2]; 486: // Check if this name matches a team member 487: const member = memberValues.find(t => t.name === name); 488: if (member?.color) { 489: const themeColor = AGENT_COLOR_TO_THEME_COLOR[member.color as AgentColorName]; 490: if (themeColor) { 491: highlights.push({ 492: start: nameStart, 493: end: nameStart + fullMatch.length, 494: themeColor 495: }); 496: } 497: } 498: } 499: return highlights; 500: }, [displayedValue, teamContext]); 501: const imageRefPositions = useMemo(() => parseReferences(displayedValue).filter(r => r.match.startsWith('[Image')).map(r => ({ 502: start: r.index, 503: end: r.index + r.match.length 504: })), [displayedValue]); 505: const cursorAtImageChip = imageRefPositions.some(r => r.start === cursorOffset); 506: useEffect(() => { 507: const inside = imageRefPositions.find(r => cursorOffset > r.start && cursorOffset < r.end); 508: if (inside) { 509: const mid = (inside.start + inside.end) / 2; 510: setCursorOffset(cursorOffset < mid ? inside.start : inside.end); 511: } 512: }, [cursorOffset, imageRefPositions, setCursorOffset]); 513: const combinedHighlights = useMemo((): TextHighlight[] => { 514: const highlights: TextHighlight[] = []; 515: for (const ref of imageRefPositions) { 516: if (cursorOffset === ref.start) { 517: highlights.push({ 518: start: ref.start, 519: end: ref.end, 520: color: undefined, 521: inverse: true, 522: priority: 8 523: }); 524: } 525: } 526: if (isSearchingHistory && historyMatch && !historyFailedMatch) { 527: highlights.push({ 528: start: cursorOffset, 529: end: cursorOffset + historyQuery.length, 530: color: 'warning', 531: priority: 20 532: }); 533: } 534: for (const trigger of btwTriggers) { 535: highlights.push({ 536: start: trigger.start, 537: end: trigger.end, 538: color: 'warning', 539: priority: 15 540: }); 541: } 542: for (const trigger of slashCommandTriggers) { 543: highlights.push({ 544: start: trigger.start, 545: end: trigger.end, 546: color: 'suggestion', 547: priority: 5 548: }); 549: } 550: for (const trigger of tokenBudgetTriggers) { 551: highlights.push({ 552: start: trigger.start, 553: end: trigger.end, 554: color: 'suggestion', 555: priority: 5 556: }); 557: } 558: for (const trigger of slackChannelTriggers) { 559: highlights.push({ 560: start: trigger.start, 561: end: trigger.end, 562: color: 'suggestion', 563: priority: 5 564: }); 565: } 566: for (const mention of memberMentionHighlights) { 567: highlights.push({ 568: start: mention.start, 569: end: mention.end, 570: color: mention.themeColor, 571: priority: 5 572: }); 573: } 574: if (voiceInterimRange) { 575: highlights.push({ 576: start: voiceInterimRange.start, 577: end: voiceInterimRange.end, 578: color: undefined, 579: dimColor: true, 580: priority: 1 581: }); 582: } 583: if (isUltrathinkEnabled()) { 584: for (const trigger of thinkTriggers) { 585: for (let i = trigger.start; i < trigger.end; i++) { 586: highlights.push({ 587: start: i, 588: end: i + 1, 589: color: getRainbowColor(i - trigger.start), 590: shimmerColor: getRainbowColor(i - trigger.start, true), 591: priority: 10 592: }); 593: } 594: } 595: } 596: if (feature('ULTRAPLAN')) { 597: for (const trigger of ultraplanTriggers) { 598: for (let i = trigger.start; i < trigger.end; i++) { 599: highlights.push({ 600: start: i, 601: end: i + 1, 602: color: getRainbowColor(i - trigger.start), 603: shimmerColor: getRainbowColor(i - trigger.start, true), 604: priority: 10 605: }); 606: } 607: } 608: } 609: for (const trigger of ultrareviewTriggers) { 610: for (let i = trigger.start; i < trigger.end; i++) { 611: highlights.push({ 612: start: i, 613: end: i + 1, 614: color: getRainbowColor(i - trigger.start), 615: shimmerColor: getRainbowColor(i - trigger.start, true), 616: priority: 10 617: }); 618: } 619: } 620: for (const trigger of buddyTriggers) { 621: for (let i = trigger.start; i < trigger.end; i++) { 622: highlights.push({ 623: start: i, 624: end: i + 1, 625: color: getRainbowColor(i - trigger.start), 626: shimmerColor: getRainbowColor(i - trigger.start, true), 627: priority: 10 628: }); 629: } 630: } 631: return highlights; 632: }, [isSearchingHistory, historyQuery, historyMatch, historyFailedMatch, cursorOffset, btwTriggers, imageRefPositions, memberMentionHighlights, slashCommandTriggers, tokenBudgetTriggers, slackChannelTriggers, displayedValue, voiceInterimRange, thinkTriggers, ultraplanTriggers, ultrareviewTriggers, buddyTriggers]); 633: const { 634: addNotification, 635: removeNotification 636: } = useNotifications(); 637: useEffect(() => { 638: if (thinkTriggers.length && isUltrathinkEnabled()) { 639: addNotification({ 640: key: 'ultrathink-active', 641: text: 'Effort set to high for this turn', 642: priority: 'immediate', 643: timeoutMs: 5000 644: }); 645: } else { 646: removeNotification('ultrathink-active'); 647: } 648: }, [addNotification, removeNotification, thinkTriggers.length]); 649: useEffect(() => { 650: if (feature('ULTRAPLAN') && ultraplanTriggers.length) { 651: addNotification({ 652: key: 'ultraplan-active', 653: text: 'This prompt will launch an ultraplan session in Claude Code on the web', 654: priority: 'immediate', 655: timeoutMs: 5000 656: }); 657: } else { 658: removeNotification('ultraplan-active'); 659: } 660: }, [addNotification, removeNotification, ultraplanTriggers.length]); 661: useEffect(() => { 662: if (isUltrareviewEnabled() && ultrareviewTriggers.length) { 663: addNotification({ 664: key: 'ultrareview-active', 665: text: 'Run /ultrareview after Claude finishes to review these changes in the cloud', 666: priority: 'immediate', 667: timeoutMs: 5000 668: }); 669: } 670: }, [addNotification, ultrareviewTriggers.length]); 671: const prevInputLengthRef = useRef(input.length); 672: const peakInputLengthRef = useRef(input.length); 673: const dismissStashHint = useCallback(() => { 674: removeNotification('stash-hint'); 675: }, [removeNotification]); 676: useEffect(() => { 677: const prevLength = prevInputLengthRef.current; 678: const peakLength = peakInputLengthRef.current; 679: const currentLength = input.length; 680: prevInputLengthRef.current = currentLength; 681: if (currentLength > peakLength) { 682: peakInputLengthRef.current = currentLength; 683: return; 684: } 685: if (currentLength === 0) { 686: peakInputLengthRef.current = 0; 687: return; 688: } 689: const clearedSubstantialInput = peakLength >= 20 && currentLength <= 5; 690: const wasRapidClear = prevLength >= 20 && currentLength <= 5; 691: if (clearedSubstantialInput && !wasRapidClear) { 692: const config = getGlobalConfig(); 693: if (!config.hasUsedStash) { 694: addNotification({ 695: key: 'stash-hint', 696: jsx: <Text dimColor> 697: Tip:{' '} 698: <ConfigurableShortcutHint action="chat:stash" context="Chat" fallback="ctrl+s" description="stash" /> 699: </Text>, 700: priority: 'immediate', 701: timeoutMs: FOOTER_TEMPORARY_STATUS_TIMEOUT 702: }); 703: } 704: peakInputLengthRef.current = currentLength; 705: } 706: }, [input.length, addNotification]); 707: const { 708: pushToBuffer, 709: undo, 710: canUndo, 711: clearBuffer 712: } = useInputBuffer({ 713: maxBufferSize: 50, 714: debounceMs: 1000 715: }); 716: useMaybeTruncateInput({ 717: input, 718: pastedContents, 719: onInputChange: trackAndSetInput, 720: setCursorOffset, 721: setPastedContents 722: }); 723: const defaultPlaceholder = usePromptInputPlaceholder({ 724: input, 725: submitCount, 726: viewingAgentName 727: }); 728: const onChange = useCallback((value: string) => { 729: if (value === '?') { 730: logEvent('tengu_help_toggled', {}); 731: setHelpOpen(v => !v); 732: return; 733: } 734: setHelpOpen(false); 735: dismissStashHint(); 736: abortPromptSuggestion(); 737: abortSpeculation(setAppState); 738: const isSingleCharInsertion = value.length === input.length + 1; 739: const insertedAtStart = cursorOffset === 0; 740: const mode = getModeFromInput(value); 741: if (insertedAtStart && mode !== 'prompt') { 742: if (isSingleCharInsertion) { 743: onModeChange(mode); 744: return; 745: } 746: if (input.length === 0) { 747: onModeChange(mode); 748: const valueWithoutMode = getValueFromInput(value).replaceAll('\t', ' '); 749: pushToBuffer(input, cursorOffset, pastedContents); 750: trackAndSetInput(valueWithoutMode); 751: setCursorOffset(valueWithoutMode.length); 752: return; 753: } 754: } 755: const processedValue = value.replaceAll('\t', ' '); 756: if (input !== processedValue) { 757: pushToBuffer(input, cursorOffset, pastedContents); 758: } 759: setAppState(prev => prev.footerSelection === null ? prev : { 760: ...prev, 761: footerSelection: null 762: }); 763: trackAndSetInput(processedValue); 764: }, [trackAndSetInput, onModeChange, input, cursorOffset, pushToBuffer, pastedContents, dismissStashHint, setAppState]); 765: const { 766: resetHistory, 767: onHistoryUp, 768: onHistoryDown, 769: dismissSearchHint, 770: historyIndex 771: } = useArrowKeyHistory((value: string, historyMode: HistoryMode, pastedContents: Record<number, PastedContent>) => { 772: onChange(value); 773: onModeChange(historyMode); 774: setPastedContents(pastedContents); 775: }, input, pastedContents, setCursorOffset, mode); 776: useEffect(() => { 777: if (isSearchingHistory) { 778: dismissSearchHint(); 779: } 780: }, [isSearchingHistory, dismissSearchHint]); 781: function handleHistoryUp() { 782: if (suggestions.length > 1) { 783: return; 784: } 785: if (!isCursorOnFirstLine) { 786: return; 787: } 788: const hasEditableCommand = queuedCommands.some(isQueuedCommandEditable); 789: if (hasEditableCommand) { 790: void popAllCommandsFromQueue(); 791: return; 792: } 793: onHistoryUp(); 794: } 795: function handleHistoryDown() { 796: if (suggestions.length > 1) { 797: return; 798: } 799: if (!isCursorOnLastLine) { 800: return; 801: } 802: if (onHistoryDown() && footerItems.length > 0) { 803: const first = footerItems[0]!; 804: selectFooterItem(first); 805: if (first === 'tasks' && !getGlobalConfig().hasSeenTasksHint) { 806: saveGlobalConfig(c => c.hasSeenTasksHint ? c : { 807: ...c, 808: hasSeenTasksHint: true 809: }); 810: } 811: } 812: } 813: const [suggestionsState, setSuggestionsStateRaw] = useState<{ 814: suggestions: SuggestionItem[]; 815: selectedSuggestion: number; 816: commandArgumentHint?: string; 817: }>({ 818: suggestions: [], 819: selectedSuggestion: -1, 820: commandArgumentHint: undefined 821: }); 822: const setSuggestionsState = useCallback((updater: typeof suggestionsState | ((prev: typeof suggestionsState) => typeof suggestionsState)) => { 823: setSuggestionsStateRaw(prev => typeof updater === 'function' ? updater(prev) : updater); 824: }, []); 825: const onSubmit = useCallback(async (inputParam: string, isSubmittingSlashCommand = false) => { 826: inputParam = inputParam.trimEnd(); 827: const state = store.getState(); 828: if (state.footerSelection && footerItems.includes(state.footerSelection)) { 829: return; 830: } 831: if (state.viewSelectionMode === 'selecting-agent') { 832: return; 833: } 834: const hasImages = Object.values(pastedContents).some(c => c.type === 'image'); 835: const suggestionText = promptSuggestionState.text; 836: const inputMatchesSuggestion = inputParam.trim() === '' || inputParam === suggestionText; 837: if (inputMatchesSuggestion && suggestionText && !hasImages && !state.viewingAgentTaskId) { 838: // If speculation is active, inject messages immediately as they stream 839: if (speculation.status === 'active') { 840: markAccepted(); 841: logOutcomeAtSubmission(suggestionText, { 842: skipReset: true 843: }); 844: void onSubmitProp(suggestionText, { 845: setCursorOffset, 846: clearBuffer, 847: resetHistory 848: }, { 849: state: speculation, 850: speculationSessionTimeSavedMs: speculationSessionTimeSavedMs, 851: setAppState 852: }); 853: return; 854: } 855: if (promptSuggestionState.shownAt > 0) { 856: markAccepted(); 857: inputParam = suggestionText; 858: } 859: } 860: if (isAgentSwarmsEnabled()) { 861: const directMessage = parseDirectMemberMessage(inputParam); 862: if (directMessage) { 863: const result = await sendDirectMemberMessage(directMessage.recipientName, directMessage.message, teamContext, writeToMailbox); 864: if (result.success) { 865: addNotification({ 866: key: 'direct-message-sent', 867: text: `Sent to @${result.recipientName}`, 868: priority: 'immediate', 869: timeoutMs: 3000 870: }); 871: trackAndSetInput(''); 872: setCursorOffset(0); 873: clearBuffer(); 874: resetHistory(); 875: return; 876: } else if (result.error === 'no_team_context') { 877: } else { 878: } 879: } 880: } 881: if (inputParam.trim() === '' && !hasImages) { 882: return; 883: } 884: // PromptInput UX: Check if suggestions dropdown is showing 885: // For directory suggestions, allow submission (Tab is used for completion) 886: const hasDirectorySuggestions = suggestionsState.suggestions.length > 0 && suggestionsState.suggestions.every(s => s.description === 'directory'); 887: if (suggestionsState.suggestions.length > 0 && !isSubmittingSlashCommand && !hasDirectorySuggestions) { 888: logForDebugging(`[onSubmit] early return: suggestions showing (count=${suggestionsState.suggestions.length})`); 889: return; 890: } 891: if (promptSuggestionState.text && promptSuggestionState.shownAt > 0) { 892: logOutcomeAtSubmission(inputParam); 893: } 894: removeNotification('stash-hint'); 895: const activeAgent = getActiveAgentForInput(store.getState()); 896: if (activeAgent.type !== 'leader' && onAgentSubmit) { 897: logEvent('tengu_transcript_input_to_teammate', {}); 898: await onAgentSubmit(inputParam, activeAgent.task, { 899: setCursorOffset, 900: clearBuffer, 901: resetHistory 902: }); 903: return; 904: } 905: await onSubmitProp(inputParam, { 906: setCursorOffset, 907: clearBuffer, 908: resetHistory 909: }); 910: }, [promptSuggestionState, speculation, speculationSessionTimeSavedMs, teamContext, store, footerItems, suggestionsState.suggestions, onSubmitProp, onAgentSubmit, clearBuffer, resetHistory, logOutcomeAtSubmission, setAppState, markAccepted, pastedContents, removeNotification]); 911: const { 912: suggestions, 913: selectedSuggestion, 914: commandArgumentHint, 915: inlineGhostText, 916: maxColumnWidth 917: } = useTypeahead({ 918: commands, 919: onInputChange: trackAndSetInput, 920: onSubmit, 921: setCursorOffset, 922: input, 923: cursorOffset, 924: mode, 925: agents, 926: setSuggestionsState, 927: suggestionsState, 928: suppressSuggestions: isSearchingHistory || historyIndex > 0, 929: markAccepted, 930: onModeChange 931: }); 932: const showPromptSuggestion = mode === 'prompt' && suggestions.length === 0 && promptSuggestion && !viewingAgentTaskId; 933: if (showPromptSuggestion) { 934: markShown(); 935: } 936: if (promptSuggestionState.text && !promptSuggestion && promptSuggestionState.shownAt === 0 && !viewingAgentTaskId) { 937: logSuggestionSuppressed('timing', promptSuggestionState.text); 938: setAppState(prev => ({ 939: ...prev, 940: promptSuggestion: { 941: text: null, 942: promptId: null, 943: shownAt: 0, 944: acceptedAt: 0, 945: generationRequestId: null 946: } 947: })); 948: } 949: function onImagePaste(image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) { 950: logEvent('tengu_paste_image', {}); 951: onModeChange('prompt'); 952: const pasteId = nextPasteIdRef.current++; 953: const newContent: PastedContent = { 954: id: pasteId, 955: type: 'image', 956: content: image, 957: mediaType: mediaType || 'image/png', 958: filename: filename || 'Pasted image', 959: dimensions, 960: sourcePath 961: }; 962: cacheImagePath(newContent); 963: void storeImage(newContent); 964: setPastedContents(prev => ({ 965: ...prev, 966: [pasteId]: newContent 967: })); 968: const prefix = pendingSpaceAfterPillRef.current ? ' ' : ''; 969: insertTextAtCursor(prefix + formatImageRef(pasteId)); 970: pendingSpaceAfterPillRef.current = true; 971: } 972: // Prune images whose [Image #N] placeholder is no longer in the input text. 973: // Covers pill backspace, Ctrl+U, char-by-char deletion — any edit that drops 974: // the ref. onImagePaste batches setPastedContents + insertTextAtCursor in the 975: // same event, so this effect sees the placeholder already present. 976: useEffect(() => { 977: const referencedIds = new Set(parseReferences(input).map(r => r.id)); 978: setPastedContents(prev => { 979: const orphaned = Object.values(prev).filter(c => c.type === 'image' && !referencedIds.has(c.id)); 980: if (orphaned.length === 0) return prev; 981: const next = { 982: ...prev 983: }; 984: for (const img of orphaned) delete next[img.id]; 985: return next; 986: }); 987: }, [input, setPastedContents]); 988: function onTextPaste(rawText: string) { 989: pendingSpaceAfterPillRef.current = false; 990: let text = stripAnsi(rawText).replace(/\r/g, '\n').replaceAll('\t', ' '); 991: if (input.length === 0) { 992: const pastedMode = getModeFromInput(text); 993: if (pastedMode !== 'prompt') { 994: onModeChange(pastedMode); 995: text = getValueFromInput(text); 996: } 997: } 998: const numLines = getPastedTextRefNumLines(text); 999: const maxLines = Math.min(rows - 10, 2); 1000: if (text.length > PASTE_THRESHOLD || numLines > maxLines) { 1001: const pasteId = nextPasteIdRef.current++; 1002: const newContent: PastedContent = { 1003: id: pasteId, 1004: type: 'text', 1005: content: text 1006: }; 1007: setPastedContents(prev => ({ 1008: ...prev, 1009: [pasteId]: newContent 1010: })); 1011: insertTextAtCursor(formatPastedTextRef(pasteId, numLines)); 1012: } else { 1013: insertTextAtCursor(text); 1014: } 1015: } 1016: const lazySpaceInputFilter = useCallback((input: string, key: Key): string => { 1017: if (!pendingSpaceAfterPillRef.current) return input; 1018: pendingSpaceAfterPillRef.current = false; 1019: if (isNonSpacePrintable(input, key)) return ' ' + input; 1020: return input; 1021: }, []); 1022: function insertTextAtCursor(text: string) { 1023: pushToBuffer(input, cursorOffset, pastedContents); 1024: const newInput = input.slice(0, cursorOffset) + text + input.slice(cursorOffset); 1025: trackAndSetInput(newInput); 1026: setCursorOffset(cursorOffset + text.length); 1027: } 1028: const doublePressEscFromEmpty = useDoublePress(() => {}, () => onShowMessageSelector()); 1029: const popAllCommandsFromQueue = useCallback((): boolean => { 1030: const result = popAllEditable(input, cursorOffset); 1031: if (!result) { 1032: return false; 1033: } 1034: trackAndSetInput(result.text); 1035: onModeChange('prompt'); 1036: setCursorOffset(result.cursorOffset); 1037: if (result.images.length > 0) { 1038: setPastedContents(prev => { 1039: const newContents = { 1040: ...prev 1041: }; 1042: for (const image of result.images) { 1043: newContents[image.id] = image; 1044: } 1045: return newContents; 1046: }); 1047: } 1048: return true; 1049: }, [trackAndSetInput, onModeChange, input, cursorOffset, setPastedContents]); 1050: const onIdeAtMentioned = function (atMentioned: IDEAtMentioned) { 1051: logEvent('tengu_ext_at_mentioned', {}); 1052: let atMentionedText: string; 1053: const relativePath = path.relative(getCwd(), atMentioned.filePath); 1054: if (atMentioned.lineStart && atMentioned.lineEnd) { 1055: atMentionedText = atMentioned.lineStart === atMentioned.lineEnd ? `@${relativePath}#L${atMentioned.lineStart} ` : `@${relativePath}#L${atMentioned.lineStart}-${atMentioned.lineEnd} `; 1056: } else { 1057: atMentionedText = `@${relativePath} `; 1058: } 1059: const cursorChar = input[cursorOffset - 1] ?? ' '; 1060: if (!/\s/.test(cursorChar)) { 1061: atMentionedText = ` ${atMentionedText}`; 1062: } 1063: insertTextAtCursor(atMentionedText); 1064: }; 1065: useIdeAtMentioned(mcpClients, onIdeAtMentioned); 1066: const handleUndo = useCallback(() => { 1067: if (canUndo) { 1068: const previousState = undo(); 1069: if (previousState) { 1070: trackAndSetInput(previousState.text); 1071: setCursorOffset(previousState.cursorOffset); 1072: setPastedContents(previousState.pastedContents); 1073: } 1074: } 1075: }, [canUndo, undo, trackAndSetInput, setPastedContents]); 1076: const handleNewline = useCallback(() => { 1077: pushToBuffer(input, cursorOffset, pastedContents); 1078: const newInput = input.slice(0, cursorOffset) + '\n' + input.slice(cursorOffset); 1079: trackAndSetInput(newInput); 1080: setCursorOffset(cursorOffset + 1); 1081: }, [input, cursorOffset, trackAndSetInput, setCursorOffset, pushToBuffer, pastedContents]); 1082: const handleExternalEditor = useCallback(async () => { 1083: logEvent('tengu_external_editor_used', {}); 1084: setIsExternalEditorActive(true); 1085: try { 1086: const result = await editPromptInEditor(input, pastedContents); 1087: if (result.error) { 1088: addNotification({ 1089: key: 'external-editor-error', 1090: text: result.error, 1091: color: 'warning', 1092: priority: 'high' 1093: }); 1094: } 1095: if (result.content !== null && result.content !== input) { 1096: pushToBuffer(input, cursorOffset, pastedContents); 1097: trackAndSetInput(result.content); 1098: setCursorOffset(result.content.length); 1099: } 1100: } catch (err) { 1101: if (err instanceof Error) { 1102: logError(err); 1103: } 1104: addNotification({ 1105: key: 'external-editor-error', 1106: text: `External editor failed: ${errorMessage(err)}`, 1107: color: 'warning', 1108: priority: 'high' 1109: }); 1110: } finally { 1111: setIsExternalEditorActive(false); 1112: } 1113: }, [input, cursorOffset, pastedContents, pushToBuffer, trackAndSetInput, addNotification]); 1114: const handleStash = useCallback(() => { 1115: if (input.trim() === '' && stashedPrompt !== undefined) { 1116: // Pop stash when input is empty 1117: trackAndSetInput(stashedPrompt.text); 1118: setCursorOffset(stashedPrompt.cursorOffset); 1119: setPastedContents(stashedPrompt.pastedContents); 1120: setStashedPrompt(undefined); 1121: } else if (input.trim() !== '') { 1122: // Push to stash (save text, cursor position, and pasted contents) 1123: setStashedPrompt({ 1124: text: input, 1125: cursorOffset, 1126: pastedContents 1127: }); 1128: trackAndSetInput(''); 1129: setCursorOffset(0); 1130: setPastedContents({}); 1131: // Track usage for /discover and stop showing hint 1132: saveGlobalConfig(c => { 1133: if (c.hasUsedStash) return c; 1134: return { 1135: ...c, 1136: hasUsedStash: true 1137: }; 1138: }); 1139: } 1140: }, [input, cursorOffset, stashedPrompt, trackAndSetInput, setStashedPrompt, pastedContents, setPastedContents]); 1141: // Handler for chat:modelPicker - toggle model picker 1142: const handleModelPicker = useCallback(() => { 1143: setShowModelPicker(prev => !prev); 1144: if (helpOpen) { 1145: setHelpOpen(false); 1146: } 1147: }, [helpOpen]); 1148: // Handler for chat:fastMode - toggle fast mode picker 1149: const handleFastModePicker = useCallback(() => { 1150: setShowFastModePicker(prev => !prev); 1151: if (helpOpen) { 1152: setHelpOpen(false); 1153: } 1154: }, [helpOpen]); 1155: // Handler for chat:thinkingToggle - toggle thinking mode 1156: const handleThinkingToggle = useCallback(() => { 1157: setShowThinkingToggle(prev => !prev); 1158: if (helpOpen) { 1159: setHelpOpen(false); 1160: } 1161: }, [helpOpen]); 1162: // Handler for chat:cycleMode - cycle through permission modes 1163: const handleCycleMode = useCallback(() => { 1164: // When viewing a teammate, cycle their mode instead of the leader's 1165: if (isAgentSwarmsEnabled() && viewedTeammate && viewingAgentTaskId) { 1166: const teammateContext: ToolPermissionContext = { 1167: ...toolPermissionContext, 1168: mode: viewedTeammate.permissionMode 1169: }; 1170: const nextMode = getNextPermissionMode(teammateContext, undefined); 1171: logEvent('tengu_mode_cycle', { 1172: to: nextMode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 1173: }); 1174: const teammateTaskId = viewingAgentTaskId; 1175: setAppState(prev => { 1176: const task = prev.tasks[teammateTaskId]; 1177: if (!task || task.type !== 'in_process_teammate') { 1178: return prev; 1179: } 1180: if (task.permissionMode === nextMode) { 1181: return prev; 1182: } 1183: return { 1184: ...prev, 1185: tasks: { 1186: ...prev.tasks, 1187: [teammateTaskId]: { 1188: ...task, 1189: permissionMode: nextMode 1190: } 1191: } 1192: }; 1193: }); 1194: if (helpOpen) { 1195: setHelpOpen(false); 1196: } 1197: return; 1198: } 1199: logForDebugging(`[auto-mode] handleCycleMode: currentMode=${toolPermissionContext.mode} isAutoModeAvailable=${toolPermissionContext.isAutoModeAvailable} showAutoModeOptIn=${showAutoModeOptIn} timeoutPending=${!!autoModeOptInTimeoutRef.current}`); 1200: const nextMode = getNextPermissionMode(toolPermissionContext, teamContext); 1201: let isEnteringAutoModeFirstTime = false; 1202: if (feature('TRANSCRIPT_CLASSIFIER')) { 1203: isEnteringAutoModeFirstTime = nextMode === 'auto' && toolPermissionContext.mode !== 'auto' && !hasAutoModeOptIn() && !viewingAgentTaskId; 1204: } 1205: if (feature('TRANSCRIPT_CLASSIFIER')) { 1206: if (isEnteringAutoModeFirstTime) { 1207: setPreviousModeBeforeAuto(toolPermissionContext.mode); 1208: setAppState(prev => ({ 1209: ...prev, 1210: toolPermissionContext: { 1211: ...prev.toolPermissionContext, 1212: mode: 'auto' 1213: } 1214: })); 1215: setToolPermissionContext({ 1216: ...toolPermissionContext, 1217: mode: 'auto' 1218: }); 1219: if (autoModeOptInTimeoutRef.current) { 1220: clearTimeout(autoModeOptInTimeoutRef.current); 1221: } 1222: autoModeOptInTimeoutRef.current = setTimeout((setShowAutoModeOptIn, autoModeOptInTimeoutRef) => { 1223: setShowAutoModeOptIn(true); 1224: autoModeOptInTimeoutRef.current = null; 1225: }, 400, setShowAutoModeOptIn, autoModeOptInTimeoutRef); 1226: if (helpOpen) { 1227: setHelpOpen(false); 1228: } 1229: return; 1230: } 1231: } 1232: if (feature('TRANSCRIPT_CLASSIFIER')) { 1233: if (showAutoModeOptIn || autoModeOptInTimeoutRef.current) { 1234: if (showAutoModeOptIn) { 1235: logEvent('tengu_auto_mode_opt_in_dialog_decline', {}); 1236: } 1237: setShowAutoModeOptIn(false); 1238: if (autoModeOptInTimeoutRef.current) { 1239: clearTimeout(autoModeOptInTimeoutRef.current); 1240: autoModeOptInTimeoutRef.current = null; 1241: } 1242: setPreviousModeBeforeAuto(null); 1243: } 1244: } 1245: const { 1246: context: preparedContext 1247: } = cyclePermissionMode(toolPermissionContext, teamContext); 1248: logEvent('tengu_mode_cycle', { 1249: to: nextMode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 1250: }); 1251: if (nextMode === 'plan') { 1252: saveGlobalConfig(current => ({ 1253: ...current, 1254: lastPlanModeUse: Date.now() 1255: })); 1256: } 1257: setAppState(prev => ({ 1258: ...prev, 1259: toolPermissionContext: { 1260: ...preparedContext, 1261: mode: nextMode 1262: } 1263: })); 1264: setToolPermissionContext({ 1265: ...preparedContext, 1266: mode: nextMode 1267: }); 1268: syncTeammateMode(nextMode, teamContext?.teamName); 1269: if (helpOpen) { 1270: setHelpOpen(false); 1271: } 1272: }, [toolPermissionContext, teamContext, viewingAgentTaskId, viewedTeammate, setAppState, setToolPermissionContext, helpOpen, showAutoModeOptIn]); 1273: const handleAutoModeOptInAccept = useCallback(() => { 1274: if (feature('TRANSCRIPT_CLASSIFIER')) { 1275: setShowAutoModeOptIn(false); 1276: setPreviousModeBeforeAuto(null); 1277: const strippedContext = transitionPermissionMode(previousModeBeforeAuto ?? toolPermissionContext.mode, 'auto', toolPermissionContext); 1278: setAppState(prev => ({ 1279: ...prev, 1280: toolPermissionContext: { 1281: ...strippedContext, 1282: mode: 'auto' 1283: } 1284: })); 1285: setToolPermissionContext({ 1286: ...strippedContext, 1287: mode: 'auto' 1288: }); 1289: if (helpOpen) { 1290: setHelpOpen(false); 1291: } 1292: } 1293: }, [helpOpen, setHelpOpen, previousModeBeforeAuto, toolPermissionContext, setAppState, setToolPermissionContext]); 1294: const handleAutoModeOptInDecline = useCallback(() => { 1295: if (feature('TRANSCRIPT_CLASSIFIER')) { 1296: logForDebugging(`[auto-mode] handleAutoModeOptInDecline: reverting to ${previousModeBeforeAuto}, setting isAutoModeAvailable=false`); 1297: setShowAutoModeOptIn(false); 1298: if (autoModeOptInTimeoutRef.current) { 1299: clearTimeout(autoModeOptInTimeoutRef.current); 1300: autoModeOptInTimeoutRef.current = null; 1301: } 1302: if (previousModeBeforeAuto) { 1303: setAutoModeActive(false); 1304: setAppState(prev => ({ 1305: ...prev, 1306: toolPermissionContext: { 1307: ...prev.toolPermissionContext, 1308: mode: previousModeBeforeAuto, 1309: isAutoModeAvailable: false 1310: } 1311: })); 1312: setToolPermissionContext({ 1313: ...toolPermissionContext, 1314: mode: previousModeBeforeAuto, 1315: isAutoModeAvailable: false 1316: }); 1317: setPreviousModeBeforeAuto(null); 1318: } 1319: } 1320: }, [previousModeBeforeAuto, toolPermissionContext, setAppState, setToolPermissionContext]); 1321: const handleImagePaste = useCallback(() => { 1322: void getImageFromClipboard().then(imageData => { 1323: if (imageData) { 1324: onImagePaste(imageData.base64, imageData.mediaType); 1325: } else { 1326: const shortcutDisplay = getShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v'); 1327: const message = env.isSSH() ? "No image found in clipboard. You're SSH'd; try scp?" : `No image found in clipboard. Use ${shortcutDisplay} to paste images.`; 1328: addNotification({ 1329: key: 'no-image-in-clipboard', 1330: text: message, 1331: priority: 'immediate', 1332: timeoutMs: 1000 1333: }); 1334: } 1335: }); 1336: }, [addNotification, onImagePaste]); 1337: const keybindingContext = useOptionalKeybindingContext(); 1338: useEffect(() => { 1339: if (!keybindingContext || isModalOverlayActive) return; 1340: return keybindingContext.registerHandler({ 1341: action: 'chat:submit', 1342: context: 'Chat', 1343: handler: () => { 1344: void onSubmit(input); 1345: } 1346: }); 1347: }, [keybindingContext, isModalOverlayActive, onSubmit, input]); 1348: const chatHandlers = useMemo(() => ({ 1349: 'chat:undo': handleUndo, 1350: 'chat:newline': handleNewline, 1351: 'chat:externalEditor': handleExternalEditor, 1352: 'chat:stash': handleStash, 1353: 'chat:modelPicker': handleModelPicker, 1354: 'chat:thinkingToggle': handleThinkingToggle, 1355: 'chat:cycleMode': handleCycleMode, 1356: 'chat:imagePaste': handleImagePaste 1357: }), [handleUndo, handleNewline, handleExternalEditor, handleStash, handleModelPicker, handleThinkingToggle, handleCycleMode, handleImagePaste]); 1358: useKeybindings(chatHandlers, { 1359: context: 'Chat', 1360: isActive: !isModalOverlayActive 1361: }); 1362: useKeybinding('chat:messageActions', () => onMessageActionsEnter?.(), { 1363: context: 'Chat', 1364: isActive: !isModalOverlayActive && !isSearchingHistory 1365: }); 1366: useKeybinding('chat:fastMode', handleFastModePicker, { 1367: context: 'Chat', 1368: isActive: !isModalOverlayActive && isFastModeEnabled() && isFastModeAvailable() 1369: }); 1370: useKeybinding('help:dismiss', () => { 1371: setHelpOpen(false); 1372: }, { 1373: context: 'Help', 1374: isActive: helpOpen 1375: }); 1376: const quickSearchActive = feature('QUICK_SEARCH') ? !isModalOverlayActive : false; 1377: useKeybinding('app:quickOpen', () => { 1378: if (feature('QUICK_SEARCH')) { 1379: setShowQuickOpen(true); 1380: setHelpOpen(false); 1381: } 1382: }, { 1383: context: 'Global', 1384: isActive: quickSearchActive 1385: }); 1386: useKeybinding('app:globalSearch', () => { 1387: if (feature('QUICK_SEARCH')) { 1388: setShowGlobalSearch(true); 1389: setHelpOpen(false); 1390: } 1391: }, { 1392: context: 'Global', 1393: isActive: quickSearchActive 1394: }); 1395: useKeybinding('history:search', () => { 1396: if (feature('HISTORY_PICKER')) { 1397: setShowHistoryPicker(true); 1398: setHelpOpen(false); 1399: } 1400: }, { 1401: context: 'Global', 1402: isActive: feature('HISTORY_PICKER') ? !isModalOverlayActive : false 1403: }); 1404: useKeybinding('app:interrupt', () => { 1405: abortSpeculation(setAppState); 1406: }, { 1407: context: 'Global', 1408: isActive: !isLoading && speculation.status === 'active' 1409: }); 1410: useKeybindings({ 1411: 'footer:up': () => { 1412: if (tasksSelected && "external" === 'ant' && coordinatorTaskCount > 0 && coordinatorTaskIndex > minCoordinatorIndex) { 1413: setCoordinatorTaskIndex(prev => prev - 1); 1414: return; 1415: } 1416: navigateFooter(-1, true); 1417: }, 1418: 'footer:down': () => { 1419: if (tasksSelected && "external" === 'ant' && coordinatorTaskCount > 0) { 1420: if (coordinatorTaskIndex < coordinatorTaskCount - 1) { 1421: setCoordinatorTaskIndex(prev => prev + 1); 1422: } 1423: return; 1424: } 1425: if (tasksSelected && !isTeammateMode) { 1426: setShowBashesDialog(true); 1427: selectFooterItem(null); 1428: return; 1429: } 1430: navigateFooter(1); 1431: }, 1432: 'footer:next': () => { 1433: if (tasksSelected && isTeammateMode) { 1434: const totalAgents = 1 + inProcessTeammates.length; 1435: setTeammateFooterIndex(prev => (prev + 1) % totalAgents); 1436: return; 1437: } 1438: navigateFooter(1); 1439: }, 1440: 'footer:previous': () => { 1441: if (tasksSelected && isTeammateMode) { 1442: const totalAgents = 1 + inProcessTeammates.length; 1443: setTeammateFooterIndex(prev => (prev - 1 + totalAgents) % totalAgents); 1444: return; 1445: } 1446: navigateFooter(-1); 1447: }, 1448: 'footer:openSelected': () => { 1449: if (viewSelectionMode === 'selecting-agent') { 1450: return; 1451: } 1452: switch (footerItemSelected) { 1453: case 'companion': 1454: if (feature('BUDDY')) { 1455: selectFooterItem(null); 1456: void onSubmit('/buddy'); 1457: } 1458: break; 1459: case 'tasks': 1460: if (isTeammateMode) { 1461: if (teammateFooterIndex === 0) { 1462: exitTeammateView(setAppState); 1463: } else { 1464: const teammate = inProcessTeammates[teammateFooterIndex - 1]; 1465: if (teammate) enterTeammateView(teammate.id, setAppState); 1466: } 1467: } else if (coordinatorTaskIndex === 0 && coordinatorTaskCount > 0) { 1468: exitTeammateView(setAppState); 1469: } else { 1470: const selectedTaskId = getVisibleAgentTasks(tasks)[coordinatorTaskIndex - 1]?.id; 1471: if (selectedTaskId) { 1472: enterTeammateView(selectedTaskId, setAppState); 1473: } else { 1474: setShowBashesDialog(true); 1475: selectFooterItem(null); 1476: } 1477: } 1478: break; 1479: case 'tmux': 1480: if ("external" === 'ant') { 1481: setAppState(prev => prev.tungstenPanelAutoHidden ? { 1482: ...prev, 1483: tungstenPanelAutoHidden: false 1484: } : { 1485: ...prev, 1486: tungstenPanelVisible: !(prev.tungstenPanelVisible ?? true) 1487: }); 1488: } 1489: break; 1490: case 'bagel': 1491: break; 1492: case 'teams': 1493: setShowTeamsDialog(true); 1494: selectFooterItem(null); 1495: break; 1496: case 'bridge': 1497: setShowBridgeDialog(true); 1498: selectFooterItem(null); 1499: break; 1500: } 1501: }, 1502: 'footer:clearSelection': () => { 1503: selectFooterItem(null); 1504: }, 1505: 'footer:close': () => { 1506: if (tasksSelected && coordinatorTaskIndex >= 1) { 1507: const task = getVisibleAgentTasks(tasks)[coordinatorTaskIndex - 1]; 1508: if (!task) return false; 1509: if (viewSelectionMode === 'viewing-agent' && task.id === viewingAgentTaskId) { 1510: onChange(input.slice(0, cursorOffset) + 'x' + input.slice(cursorOffset)); 1511: setCursorOffset(cursorOffset + 1); 1512: return; 1513: } 1514: stopOrDismissAgent(task.id, setAppState); 1515: if (task.status !== 'running') { 1516: setCoordinatorTaskIndex(i => Math.max(minCoordinatorIndex, i - 1)); 1517: } 1518: return; 1519: } 1520: return false; 1521: } 1522: }, { 1523: context: 'Footer', 1524: isActive: !!footerItemSelected && !isModalOverlayActive 1525: }); 1526: useInput((char, key) => { 1527: if (showTeamsDialog || showQuickOpen || showGlobalSearch || showHistoryPicker) { 1528: return; 1529: } 1530: if (getPlatform() === 'macos' && isMacosOptionChar(char)) { 1531: const shortcut = MACOS_OPTION_SPECIAL_CHARS[char]; 1532: const terminalName = getNativeCSIuTerminalDisplayName(); 1533: const jsx = terminalName ? <Text dimColor> 1534: To enable {shortcut}, set <Text bold>Option as Meta</Text> in{' '} 1535: {terminalName} preferences (⌘,) 1536: </Text> : <Text dimColor>To enable {shortcut}, run /terminal-setup</Text>; 1537: addNotification({ 1538: key: 'option-meta-hint', 1539: jsx, 1540: priority: 'immediate', 1541: timeoutMs: 5000 1542: }); 1543: } 1544: if (footerItemSelected && char && !key.ctrl && !key.meta && !key.escape && !key.return) { 1545: onChange(input.slice(0, cursorOffset) + char + input.slice(cursorOffset)); 1546: setCursorOffset(cursorOffset + char.length); 1547: return; 1548: } 1549: if (cursorOffset === 0 && (key.escape || key.backspace || key.delete || key.ctrl && char === 'u')) { 1550: onModeChange('prompt'); 1551: setHelpOpen(false); 1552: } 1553: if (helpOpen && input === '' && (key.backspace || key.delete)) { 1554: setHelpOpen(false); 1555: } 1556: // esc is a little overloaded: 1557: // - when we're loading a response, it's used to cancel the request 1558: if (key.escape) { 1559: if (speculation.status === 'active') { 1560: abortSpeculation(setAppState); 1561: return; 1562: } 1563: if (isSideQuestionVisible && onDismissSideQuestion) { 1564: onDismissSideQuestion(); 1565: return; 1566: } 1567: if (helpOpen) { 1568: setHelpOpen(false); 1569: return; 1570: } 1571: if (footerItemSelected) { 1572: return; 1573: } 1574: const hasEditableCommand = queuedCommands.some(isQueuedCommandEditable); 1575: if (hasEditableCommand) { 1576: void popAllCommandsFromQueue(); 1577: return; 1578: } 1579: if (messages.length > 0 && !input && !isLoading) { 1580: doublePressEscFromEmpty(); 1581: } 1582: } 1583: if (key.return && helpOpen) { 1584: setHelpOpen(false); 1585: } 1586: }); 1587: const swarmBanner = useSwarmBanner(); 1588: const fastModeCooldown = isFastModeEnabled() ? isFastModeCooldown() : false; 1589: const showFastIcon = isFastModeEnabled() ? isFastMode && (isFastModeAvailable() || fastModeCooldown) : false; 1590: const showFastIconHint = useShowFastIconHint(showFastIcon ?? false); 1591: const effortNotificationText = briefOwnsGap ? undefined : getEffortNotificationText(effortValue, mainLoopModel); 1592: useEffect(() => { 1593: if (!effortNotificationText) { 1594: removeNotification('effort-level'); 1595: return; 1596: } 1597: addNotification({ 1598: key: 'effort-level', 1599: text: effortNotificationText, 1600: priority: 'high', 1601: timeoutMs: 12_000 1602: }); 1603: }, [effortNotificationText, addNotification, removeNotification]); 1604: useBuddyNotification(); 1605: const companionSpeaking = feature('BUDDY') ? 1606: useAppState(s => s.companionReaction !== undefined) : false; 1607: const { 1608: columns, 1609: rows 1610: } = useTerminalSize(); 1611: const textInputColumns = columns - 3 - companionReservedColumns(columns, companionSpeaking); 1612: const maxVisibleLines = isFullscreenEnvEnabled() ? Math.max(MIN_INPUT_VIEWPORT_LINES, Math.floor(rows / 2) - PROMPT_FOOTER_LINES) : undefined; 1613: const handleInputClick = useCallback((e: ClickEvent) => { 1614: if (!input || isSearchingHistory) return; 1615: const c = Cursor.fromText(input, textInputColumns, cursorOffset); 1616: const viewportStart = c.getViewportStartLine(maxVisibleLines); 1617: const offset = c.measuredText.getOffsetFromPosition({ 1618: line: e.localRow + viewportStart, 1619: column: e.localCol 1620: }); 1621: setCursorOffset(offset); 1622: }, [input, textInputColumns, isSearchingHistory, cursorOffset, maxVisibleLines]); 1623: const handleOpenTasksDialog = useCallback((taskId?: string) => setShowBashesDialog(taskId ?? true), [setShowBashesDialog]); 1624: const placeholder = showPromptSuggestion && promptSuggestion ? promptSuggestion : defaultPlaceholder; 1625: const isInputWrapped = useMemo(() => input.includes('\n'), [input]); 1626: const handleModelSelect = useCallback((model: string | null, _effort: EffortLevel | undefined) => { 1627: let wasFastModeDisabled = false; 1628: setAppState(prev => { 1629: wasFastModeDisabled = isFastModeEnabled() && !isFastModeSupportedByModel(model) && !!prev.fastMode; 1630: return { 1631: ...prev, 1632: mainLoopModel: model, 1633: mainLoopModelForSession: null, 1634: ...(wasFastModeDisabled && { 1635: fastMode: false 1636: }) 1637: }; 1638: }); 1639: setShowModelPicker(false); 1640: const effectiveFastMode = (isFastMode ?? false) && !wasFastModeDisabled; 1641: let message = `Model set to ${modelDisplayString(model)}`; 1642: if (isBilledAsExtraUsage(model, effectiveFastMode, isOpus1mMergeEnabled())) { 1643: message += ' · Billed as extra usage'; 1644: } 1645: if (wasFastModeDisabled) { 1646: message += ' · Fast mode OFF'; 1647: } 1648: addNotification({ 1649: key: 'model-switched', 1650: jsx: <Text>{message}</Text>, 1651: priority: 'immediate', 1652: timeoutMs: 3000 1653: }); 1654: logEvent('tengu_model_picker_hotkey', { 1655: model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 1656: }); 1657: }, [setAppState, addNotification, isFastMode]); 1658: const handleModelCancel = useCallback(() => { 1659: setShowModelPicker(false); 1660: }, []); 1661: const modelPickerElement = useMemo(() => { 1662: if (!showModelPicker) return null; 1663: return <Box flexDirection="column" marginTop={1}> 1664: <ModelPicker initial={mainLoopModel_} sessionModel={mainLoopModelForSession} onSelect={handleModelSelect} onCancel={handleModelCancel} isStandaloneCommand showFastModeNotice={isFastModeEnabled() && isFastMode && isFastModeSupportedByModel(mainLoopModel_) && isFastModeAvailable()} /> 1665: </Box>; 1666: }, [showModelPicker, mainLoopModel_, mainLoopModelForSession, handleModelSelect, handleModelCancel]); 1667: const handleFastModeSelect = useCallback((result?: string) => { 1668: setShowFastModePicker(false); 1669: if (result) { 1670: addNotification({ 1671: key: 'fast-mode-toggled', 1672: jsx: <Text>{result}</Text>, 1673: priority: 'immediate', 1674: timeoutMs: 3000 1675: }); 1676: } 1677: }, [addNotification]); 1678: const fastModePickerElement = useMemo(() => { 1679: if (!showFastModePicker) return null; 1680: return <Box flexDirection="column" marginTop={1}> 1681: <FastModePicker onDone={handleFastModeSelect} unavailableReason={getFastModeUnavailableReason()} /> 1682: </Box>; 1683: }, [showFastModePicker, handleFastModeSelect]); 1684: const handleThinkingSelect = useCallback((enabled: boolean) => { 1685: setAppState(prev => ({ 1686: ...prev, 1687: thinkingEnabled: enabled 1688: })); 1689: setShowThinkingToggle(false); 1690: logEvent('tengu_thinking_toggled_hotkey', { 1691: enabled 1692: }); 1693: addNotification({ 1694: key: 'thinking-toggled-hotkey', 1695: jsx: <Text color={enabled ? 'suggestion' : undefined} dimColor={!enabled}> 1696: Thinking {enabled ? 'on' : 'off'} 1697: </Text>, 1698: priority: 'immediate', 1699: timeoutMs: 3000 1700: }); 1701: }, [setAppState, addNotification]); 1702: const handleThinkingCancel = useCallback(() => { 1703: setShowThinkingToggle(false); 1704: }, []); 1705: const thinkingToggleElement = useMemo(() => { 1706: if (!showThinkingToggle) return null; 1707: return <Box flexDirection="column" marginTop={1}> 1708: <ThinkingToggle currentValue={thinkingEnabled ?? true} onSelect={handleThinkingSelect} onCancel={handleThinkingCancel} isMidConversation={messages.some(m => m.type === 'assistant')} /> 1709: </Box>; 1710: }, [showThinkingToggle, thinkingEnabled, handleThinkingSelect, handleThinkingCancel, messages.length]); 1711: const autoModeOptInDialog = useMemo(() => feature('TRANSCRIPT_CLASSIFIER') && showAutoModeOptIn ? <AutoModeOptInDialog onAccept={handleAutoModeOptInAccept} onDecline={handleAutoModeOptInDecline} /> : null, [showAutoModeOptIn, handleAutoModeOptInAccept, handleAutoModeOptInDecline]); 1712: useSetPromptOverlayDialog(isFullscreenEnvEnabled() ? autoModeOptInDialog : null); 1713: if (showBashesDialog) { 1714: return <BackgroundTasksDialog onDone={() => setShowBashesDialog(false)} toolUseContext={getToolUseContext(messages, [], new AbortController(), mainLoopModel)} initialDetailTaskId={typeof showBashesDialog === 'string' ? showBashesDialog : undefined} />; 1715: } 1716: if (isAgentSwarmsEnabled() && showTeamsDialog) { 1717: return <TeamsDialog initialTeams={cachedTeams} onDone={() => { 1718: setShowTeamsDialog(false); 1719: }} />; 1720: } 1721: if (feature('QUICK_SEARCH')) { 1722: const insertWithSpacing = (text: string) => { 1723: const cursorChar = input[cursorOffset - 1] ?? ' '; 1724: insertTextAtCursor(/\s/.test(cursorChar) ? text : ` ${text}`); 1725: }; 1726: if (showQuickOpen) { 1727: return <QuickOpenDialog onDone={() => setShowQuickOpen(false)} onInsert={insertWithSpacing} />; 1728: } 1729: if (showGlobalSearch) { 1730: return <GlobalSearchDialog onDone={() => setShowGlobalSearch(false)} onInsert={insertWithSpacing} />; 1731: } 1732: } 1733: if (feature('HISTORY_PICKER') && showHistoryPicker) { 1734: return <HistorySearchDialog initialQuery={input} onSelect={entry => { 1735: const entryMode = getModeFromInput(entry.display); 1736: const value = getValueFromInput(entry.display); 1737: onModeChange(entryMode); 1738: trackAndSetInput(value); 1739: setPastedContents(entry.pastedContents); 1740: setCursorOffset(value.length); 1741: setShowHistoryPicker(false); 1742: }} onCancel={() => setShowHistoryPicker(false)} />; 1743: } 1744: if (modelPickerElement) { 1745: return modelPickerElement; 1746: } 1747: if (fastModePickerElement) { 1748: return fastModePickerElement; 1749: } 1750: if (thinkingToggleElement) { 1751: return thinkingToggleElement; 1752: } 1753: if (showBridgeDialog) { 1754: return <BridgeDialog onDone={() => { 1755: setShowBridgeDialog(false); 1756: selectFooterItem(null); 1757: }} />; 1758: } 1759: const baseProps: BaseTextInputProps = { 1760: multiline: true, 1761: onSubmit, 1762: onChange, 1763: value: historyMatch ? getValueFromInput(typeof historyMatch === 'string' ? historyMatch : historyMatch.display) : input, 1764: onHistoryUp: handleHistoryUp, 1765: onHistoryDown: handleHistoryDown, 1766: onHistoryReset: resetHistory, 1767: placeholder, 1768: onExit, 1769: onExitMessage: (show, key) => setExitMessage({ 1770: show, 1771: key 1772: }), 1773: onImagePaste, 1774: columns: textInputColumns, 1775: maxVisibleLines, 1776: disableCursorMovementForUpDownKeys: suggestions.length > 0 || !!footerItemSelected, 1777: disableEscapeDoublePress: suggestions.length > 0, 1778: cursorOffset, 1779: onChangeCursorOffset: setCursorOffset, 1780: onPaste: onTextPaste, 1781: onIsPastingChange: setIsPasting, 1782: focus: !isSearchingHistory && !isModalOverlayActive && !footerItemSelected, 1783: showCursor: !footerItemSelected && !isSearchingHistory && !cursorAtImageChip, 1784: argumentHint: commandArgumentHint, 1785: onUndo: canUndo ? () => { 1786: const previousState = undo(); 1787: if (previousState) { 1788: trackAndSetInput(previousState.text); 1789: setCursorOffset(previousState.cursorOffset); 1790: setPastedContents(previousState.pastedContents); 1791: } 1792: } : undefined, 1793: highlights: combinedHighlights, 1794: inlineGhostText, 1795: inputFilter: lazySpaceInputFilter 1796: }; 1797: const getBorderColor = (): keyof Theme => { 1798: const modeColors: Record<string, keyof Theme> = { 1799: bash: 'bashBorder' 1800: }; 1801: if (modeColors[mode]) { 1802: return modeColors[mode]; 1803: } 1804: if (isInProcessTeammate()) { 1805: return 'promptBorder'; 1806: } 1807: const teammateColorName = getTeammateColor(); 1808: if (teammateColorName && AGENT_COLORS.includes(teammateColorName as AgentColorName)) { 1809: return AGENT_COLOR_TO_THEME_COLOR[teammateColorName as AgentColorName]; 1810: } 1811: return 'promptBorder'; 1812: }; 1813: if (isExternalEditorActive) { 1814: return <Box flexDirection="row" alignItems="center" justifyContent="center" borderColor={getBorderColor()} borderStyle="round" borderLeft={false} borderRight={false} borderBottom width="100%"> 1815: <Text dimColor italic> 1816: Save and close editor to continue... 1817: </Text> 1818: </Box>; 1819: } 1820: const textInputElement = isVimModeEnabled() ? <VimTextInput {...baseProps} initialMode={vimMode} onModeChange={setVimMode} /> : <TextInput {...baseProps} />; 1821: return <Box flexDirection="column" marginTop={briefOwnsGap ? 0 : 1}> 1822: {!isFullscreenEnvEnabled() && <PromptInputQueuedCommands />} 1823: {hasSuppressedDialogs && <Box marginTop={1} marginLeft={2}> 1824: <Text dimColor>Waiting for permission…</Text> 1825: </Box>} 1826: <PromptInputStashNotice hasStash={stashedPrompt !== undefined} /> 1827: {swarmBanner ? <> 1828: <Text color={swarmBanner.bgColor}> 1829: {swarmBanner.text ? <> 1830: {'─'.repeat(Math.max(0, columns - stringWidth(swarmBanner.text) - 4))} 1831: <Text backgroundColor={swarmBanner.bgColor} color="inverseText"> 1832: {' '} 1833: {swarmBanner.text}{' '} 1834: </Text> 1835: {'──'} 1836: </> : '─'.repeat(columns)} 1837: </Text> 1838: <Box flexDirection="row" width="100%"> 1839: <PromptInputModeIndicator mode={mode} isLoading={isLoading} viewingAgentName={viewingAgentName} viewingAgentColor={viewingAgentColor} /> 1840: <Box flexGrow={1} flexShrink={1} onClick={handleInputClick}> 1841: {textInputElement} 1842: </Box> 1843: </Box> 1844: <Text color={swarmBanner.bgColor}>{'─'.repeat(columns)}</Text> 1845: </> : <Box flexDirection="row" alignItems="flex-start" justifyContent="flex-start" borderColor={getBorderColor()} borderStyle="round" borderLeft={false} borderRight={false} borderBottom width="100%" borderText={buildBorderText(showFastIcon ?? false, showFastIconHint, fastModeCooldown)}> 1846: <PromptInputModeIndicator mode={mode} isLoading={isLoading} viewingAgentName={viewingAgentName} viewingAgentColor={viewingAgentColor} /> 1847: <Box flexGrow={1} flexShrink={1} onClick={handleInputClick}> 1848: {textInputElement} 1849: </Box> 1850: </Box>} 1851: <PromptInputFooter apiKeyStatus={apiKeyStatus} debug={debug} exitMessage={exitMessage} vimMode={isVimModeEnabled() ? vimMode : undefined} mode={mode} autoUpdaterResult={autoUpdaterResult} isAutoUpdating={isAutoUpdating} verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={setIsAutoUpdating} suggestions={suggestions} selectedSuggestion={selectedSuggestion} maxColumnWidth={maxColumnWidth} toolPermissionContext={effectiveToolPermissionContext} helpOpen={helpOpen} suppressHint={input.length > 0} isLoading={isLoading} tasksSelected={tasksSelected} teamsSelected={teamsSelected} bridgeSelected={bridgeSelected} tmuxSelected={tmuxSelected} teammateFooterIndex={teammateFooterIndex} ideSelection={ideSelection} mcpClients={mcpClients} isPasting={isPasting} isInputWrapped={isInputWrapped} messages={messages} isSearching={isSearchingHistory} historyQuery={historyQuery} setHistoryQuery={setHistoryQuery} historyFailedMatch={historyFailedMatch} onOpenTasksDialog={isFullscreenEnvEnabled() ? handleOpenTasksDialog : undefined} /> 1852: {isFullscreenEnvEnabled() ? null : autoModeOptInDialog} 1853: {isFullscreenEnvEnabled() ? 1854: <Box position="absolute" marginTop={briefOwnsGap ? -2 : -1} height={suggestions.length === 0 && !showAutoModeOptIn ? 1 : 0} width="100%" paddingLeft={2} paddingRight={1} flexDirection="column" justifyContent="flex-end" overflow="hidden"> 1855: <Notifications apiKeyStatus={apiKeyStatus} autoUpdaterResult={autoUpdaterResult} debug={debug} isAutoUpdating={isAutoUpdating} verbose={verbose} messages={messages} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={setIsAutoUpdating} ideSelection={ideSelection} mcpClients={mcpClients} isInputWrapped={isInputWrapped} /> 1856: </Box> : null} 1857: </Box>; 1858: } 1859: function getInitialPasteId(messages: Message[]): number { 1860: let maxId = 0; 1861: for (const message of messages) { 1862: if (message.type === 'user') { 1863: if (message.imagePasteIds) { 1864: for (const id of message.imagePasteIds) { 1865: if (id > maxId) maxId = id; 1866: } 1867: } 1868: if (Array.isArray(message.message.content)) { 1869: for (const block of message.message.content) { 1870: if (block.type === 'text') { 1871: const refs = parseReferences(block.text); 1872: for (const ref of refs) { 1873: if (ref.id > maxId) maxId = ref.id; 1874: } 1875: } 1876: } 1877: } 1878: } 1879: } 1880: return maxId + 1; 1881: } 1882: function buildBorderText(showFastIcon: boolean, showFastIconHint: boolean, fastModeCooldown: boolean): BorderTextOptions | undefined { 1883: if (!showFastIcon) return undefined; 1884: const fastSeg = showFastIconHint ? `${getFastIconString(true, fastModeCooldown)} ${chalk.dim('/fast')}` : getFastIconString(true, fastModeCooldown); 1885: return { 1886: content: ` ${fastSeg} `, 1887: position: 'top', 1888: align: 'end', 1889: offset: 0 1890: }; 1891: } 1892: export default React.memo(PromptInput);

File: src/components/PromptInput/PromptInputFooter.tsx

typescript 1: import { feature } from 'bun:bundle'; 2: import * as React from 'react'; 3: import { memo, type ReactNode, useMemo, useRef } from 'react'; 4: import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'; 5: import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js'; 6: import { useSetPromptOverlay } from '../../context/promptOverlayContext.js'; 7: import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'; 8: import type { IDESelection } from '../../hooks/useIdeSelection.js'; 9: import { useSettings } from '../../hooks/useSettings.js'; 10: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 11: import { Box, Text } from '../../ink.js'; 12: import type { MCPServerConnection } from '../../services/mcp/types.js'; 13: import { useAppState } from '../../state/AppState.js'; 14: import type { ToolPermissionContext } from '../../Tool.js'; 15: import type { Message } from '../../types/message.js'; 16: import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js'; 17: import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'; 18: import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; 19: import { isUndercover } from '../../utils/undercover.js'; 20: import { CoordinatorTaskPanel, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js'; 21: import { getLastAssistantMessageId, StatusLine, statusLineShouldDisplay } from '../StatusLine.js'; 22: import { Notifications } from './Notifications.js'; 23: import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js'; 24: import { PromptInputFooterSuggestions, type SuggestionItem } from './PromptInputFooterSuggestions.js'; 25: import { PromptInputHelpMenu } from './PromptInputHelpMenu.js'; 26: type Props = { 27: apiKeyStatus: VerificationStatus; 28: debug: boolean; 29: exitMessage: { 30: show: boolean; 31: key?: string; 32: }; 33: vimMode: VimMode | undefined; 34: mode: PromptInputMode; 35: autoUpdaterResult: AutoUpdaterResult | null; 36: isAutoUpdating: boolean; 37: verbose: boolean; 38: onAutoUpdaterResult: (result: AutoUpdaterResult) => void; 39: onChangeIsUpdating: (isUpdating: boolean) => void; 40: suggestions: SuggestionItem[]; 41: selectedSuggestion: number; 42: maxColumnWidth?: number; 43: toolPermissionContext: ToolPermissionContext; 44: helpOpen: boolean; 45: suppressHint: boolean; 46: isLoading: boolean; 47: tasksSelected: boolean; 48: teamsSelected: boolean; 49: bridgeSelected: boolean; 50: tmuxSelected: boolean; 51: teammateFooterIndex?: number; 52: ideSelection: IDESelection | undefined; 53: mcpClients?: MCPServerConnection[]; 54: isPasting?: boolean; 55: isInputWrapped?: boolean; 56: messages: Message[]; 57: isSearching: boolean; 58: historyQuery: string; 59: setHistoryQuery: (query: string) => void; 60: historyFailedMatch: boolean; 61: onOpenTasksDialog?: (taskId?: string) => void; 62: }; 63: function PromptInputFooter({ 64: apiKeyStatus, 65: debug, 66: exitMessage, 67: vimMode, 68: mode, 69: autoUpdaterResult, 70: isAutoUpdating, 71: verbose, 72: onAutoUpdaterResult, 73: onChangeIsUpdating, 74: suggestions, 75: selectedSuggestion, 76: maxColumnWidth, 77: toolPermissionContext, 78: helpOpen, 79: suppressHint: suppressHintFromProps, 80: isLoading, 81: tasksSelected, 82: teamsSelected, 83: bridgeSelected, 84: tmuxSelected, 85: teammateFooterIndex, 86: ideSelection, 87: mcpClients, 88: isPasting = false, 89: isInputWrapped = false, 90: messages, 91: isSearching, 92: historyQuery, 93: setHistoryQuery, 94: historyFailedMatch, 95: onOpenTasksDialog 96: }: Props): ReactNode { 97: const settings = useSettings(); 98: const { 99: columns, 100: rows 101: } = useTerminalSize(); 102: const messagesRef = useRef(messages); 103: messagesRef.current = messages; 104: const lastAssistantMessageId = useMemo(() => getLastAssistantMessageId(messages), [messages]); 105: const isNarrow = columns < 80; 106: const isFullscreen = isFullscreenEnvEnabled(); 107: const isShort = isFullscreen && rows < 24; 108: const coordinatorTaskCount = useCoordinatorTaskCount(); 109: const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex); 110: const pillSelected = tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0); 111: const suppressHint = suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching; 112: const overlayData = useMemo(() => isFullscreen && suggestions.length ? { 113: suggestions, 114: selectedSuggestion, 115: maxColumnWidth 116: } : null, [isFullscreen, suggestions, selectedSuggestion, maxColumnWidth]); 117: useSetPromptOverlay(overlayData); 118: if (suggestions.length && !isFullscreen) { 119: return <Box paddingX={2} paddingY={0}> 120: <PromptInputFooterSuggestions suggestions={suggestions} selectedSuggestion={selectedSuggestion} maxColumnWidth={maxColumnWidth} /> 121: </Box>; 122: } 123: if (helpOpen) { 124: return <PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} />; 125: } 126: return <> 127: <Box flexDirection={isNarrow ? 'column' : 'row'} justifyContent={isNarrow ? 'flex-start' : 'space-between'} paddingX={2} gap={isNarrow ? 0 : 1}> 128: <Box flexDirection="column" flexShrink={isNarrow ? 0 : 1}> 129: {mode === 'prompt' && !isShort && !exitMessage.show && !isPasting && statusLineShouldDisplay(settings) && <StatusLine messagesRef={messagesRef} lastAssistantMessageId={lastAssistantMessageId} vimMode={vimMode} />} 130: <PromptInputFooterLeftSide exitMessage={exitMessage} vimMode={vimMode} mode={mode} toolPermissionContext={toolPermissionContext} suppressHint={suppressHint} isLoading={isLoading} tasksSelected={pillSelected} teamsSelected={teamsSelected} teammateFooterIndex={teammateFooterIndex} tmuxSelected={tmuxSelected} isPasting={isPasting} isSearching={isSearching} historyQuery={historyQuery} setHistoryQuery={setHistoryQuery} historyFailedMatch={historyFailedMatch} onOpenTasksDialog={onOpenTasksDialog} /> 131: </Box> 132: <Box flexShrink={1} gap={1}> 133: {isFullscreen ? null : <Notifications apiKeyStatus={apiKeyStatus} autoUpdaterResult={autoUpdaterResult} debug={debug} isAutoUpdating={isAutoUpdating} verbose={verbose} messages={messages} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} ideSelection={ideSelection} mcpClients={mcpClients} isInputWrapped={isInputWrapped} isNarrow={isNarrow} />} 134: {"external" === 'ant' && isUndercover() && <Text dimColor>undercover</Text>} 135: <BridgeStatusIndicator bridgeSelected={bridgeSelected} /> 136: </Box> 137: </Box> 138: {"external" === 'ant' && <CoordinatorTaskPanel />} 139: </>; 140: } 141: export default memo(PromptInputFooter); 142: type BridgeStatusProps = { 143: bridgeSelected: boolean; 144: }; 145: function BridgeStatusIndicator({ 146: bridgeSelected 147: }: BridgeStatusProps): React.ReactNode { 148: if (!feature('BRIDGE_MODE')) return null; 149: const enabled = useAppState(s => s.replBridgeEnabled); 150: const connected = useAppState(s_0 => s_0.replBridgeConnected); 151: const sessionActive = useAppState(s_1 => s_1.replBridgeSessionActive); 152: const reconnecting = useAppState(s_2 => s_2.replBridgeReconnecting); 153: const explicit = useAppState(s_3 => s_3.replBridgeExplicit); 154: if (!isBridgeEnabled() || !enabled) return null; 155: const status = getBridgeStatus({ 156: error: undefined, 157: connected, 158: sessionActive, 159: reconnecting 160: }); 161: if (!explicit && status.label !== 'Remote Control reconnecting') { 162: return null; 163: } 164: return <Text color={bridgeSelected ? 'background' : status.color} inverse={bridgeSelected} wrap="truncate"> 165: {status.label} 166: {bridgeSelected && <Text dimColor> · Enter to view</Text>} 167: </Text>; 168: }

File: src/components/PromptInput/PromptInputFooterLeftSide.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: const coordinatorModule = feature('COORDINATOR_MODE') ? require('../../coordinator/coordinatorMode.js') as typeof import('../../coordinator/coordinatorMode.js') : undefined; 4: import { Box, Text, Link } from '../../ink.js'; 5: import * as React from 'react'; 6: import figures from 'figures'; 7: import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'; 8: import type { VimMode, PromptInputMode } from '../../types/textInputTypes.js'; 9: import type { ToolPermissionContext } from '../../Tool.js'; 10: import { isVimModeEnabled } from './utils.js'; 11: import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'; 12: import { isDefaultMode, permissionModeSymbol, permissionModeTitle, getModeColor } from '../../utils/permissions/PermissionMode.js'; 13: import { BackgroundTaskStatus } from '../tasks/BackgroundTaskStatus.js'; 14: import { isBackgroundTask } from '../../tasks/types.js'; 15: import { isPanelAgentTask } from '../../tasks/LocalAgentTask/LocalAgentTask.js'; 16: import { getVisibleAgentTasks } from '../CoordinatorAgentStatus.js'; 17: import { count } from '../../utils/array.js'; 18: import { shouldHideTasksFooter } from '../tasks/taskStatusUtils.js'; 19: import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'; 20: import { TeamStatus } from '../teams/TeamStatus.js'; 21: import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js'; 22: import { useAppState, useAppStateStore } from 'src/state/AppState.js'; 23: import { getIsRemoteMode } from '../../bootstrap/state.js'; 24: import HistorySearchInput from './HistorySearchInput.js'; 25: import { usePrStatus } from '../../hooks/usePrStatus.js'; 26: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 27: import { Byline } from '../design-system/Byline.js'; 28: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 29: import { useTasksV2 } from '../../hooks/useTasksV2.js'; 30: import { formatDuration } from '../../utils/format.js'; 31: import { VoiceWarmupHint } from './VoiceIndicator.js'; 32: import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'; 33: import { useVoiceState } from '../../context/voice.js'; 34: import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; 35: import { isXtermJs } from '../../ink/terminal.js'; 36: import { useHasSelection, useSelection } from '../../ink/hooks/use-selection.js'; 37: import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; 38: import { getPlatform } from '../../utils/platform.js'; 39: import { PrBadge } from '../PrBadge.js'; 40: const proactiveModule = feature('PROACTIVE') || feature('KAIROS') ? require('../../proactive/index.js') : null; 41: const NO_OP_SUBSCRIBE = (_cb: () => void) => () => {}; 42: const NULL = () => null; 43: const MAX_VOICE_HINT_SHOWS = 3; 44: type Props = { 45: exitMessage: { 46: show: boolean; 47: key?: string; 48: }; 49: vimMode: VimMode | undefined; 50: mode: PromptInputMode; 51: toolPermissionContext: ToolPermissionContext; 52: suppressHint: boolean; 53: isLoading: boolean; 54: showMemoryTypeSelector?: boolean; 55: tasksSelected: boolean; 56: teamsSelected: boolean; 57: tmuxSelected: boolean; 58: teammateFooterIndex?: number; 59: isPasting?: boolean; 60: isSearching: boolean; 61: historyQuery: string; 62: setHistoryQuery: (query: string) => void; 63: historyFailedMatch: boolean; 64: onOpenTasksDialog?: (taskId?: string) => void; 65: }; 66: function ProactiveCountdown() { 67: const $ = _c(7); 68: const nextTickAt = useSyncExternalStore(proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE, proactiveModule?.getNextTickAt ?? NULL, NULL); 69: const [remainingSeconds, setRemainingSeconds] = useState(null); 70: let t0; 71: let t1; 72: if ($[0] !== nextTickAt) { 73: t0 = () => { 74: if (nextTickAt === null) { 75: setRemainingSeconds(null); 76: return; 77: } 78: const update = function update() { 79: const remaining = Math.max(0, Math.ceil((nextTickAt - Date.now()) / 1000)); 80: setRemainingSeconds(remaining); 81: }; 82: update(); 83: const interval = setInterval(update, 1000); 84: return () => clearInterval(interval); 85: }; 86: t1 = [nextTickAt]; 87: $[0] = nextTickAt; 88: $[1] = t0; 89: $[2] = t1; 90: } else { 91: t0 = $[1]; 92: t1 = $[2]; 93: } 94: useEffect(t0, t1); 95: if (remainingSeconds === null) { 96: return null; 97: } 98: const t2 = remainingSeconds * 1000; 99: let t3; 100: if ($[3] !== t2) { 101: t3 = formatDuration(t2, { 102: mostSignificantOnly: true 103: }); 104: $[3] = t2; 105: $[4] = t3; 106: } else { 107: t3 = $[4]; 108: } 109: let t4; 110: if ($[5] !== t3) { 111: t4 = <Text dimColor={true}>waiting{" "}{t3}</Text>; 112: $[5] = t3; 113: $[6] = t4; 114: } else { 115: t4 = $[6]; 116: } 117: return t4; 118: } 119: export function PromptInputFooterLeftSide(t0) { 120: const $ = _c(27); 121: const { 122: exitMessage, 123: vimMode, 124: mode, 125: toolPermissionContext, 126: suppressHint, 127: isLoading, 128: tasksSelected, 129: teamsSelected, 130: tmuxSelected, 131: teammateFooterIndex, 132: isPasting, 133: isSearching, 134: historyQuery, 135: setHistoryQuery, 136: historyFailedMatch, 137: onOpenTasksDialog 138: } = t0; 139: if (exitMessage.show) { 140: let t1; 141: if ($[0] !== exitMessage.key) { 142: t1 = <Text dimColor={true} key="exit-message">Press {exitMessage.key} again to exit</Text>; 143: $[0] = exitMessage.key; 144: $[1] = t1; 145: } else { 146: t1 = $[1]; 147: } 148: return t1; 149: } 150: if (isPasting) { 151: let t1; 152: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 153: t1 = <Text dimColor={true} key="pasting-message">Pasting text…</Text>; 154: $[2] = t1; 155: } else { 156: t1 = $[2]; 157: } 158: return t1; 159: } 160: let t1; 161: if ($[3] !== isSearching || $[4] !== vimMode) { 162: t1 = isVimModeEnabled() && vimMode === "INSERT" && !isSearching; 163: $[3] = isSearching; 164: $[4] = vimMode; 165: $[5] = t1; 166: } else { 167: t1 = $[5]; 168: } 169: const showVim = t1; 170: let t2; 171: if ($[6] !== historyFailedMatch || $[7] !== historyQuery || $[8] !== isSearching || $[9] !== setHistoryQuery) { 172: t2 = isSearching && <HistorySearchInput value={historyQuery} onChange={setHistoryQuery} historyFailedMatch={historyFailedMatch} />; 173: $[6] = historyFailedMatch; 174: $[7] = historyQuery; 175: $[8] = isSearching; 176: $[9] = setHistoryQuery; 177: $[10] = t2; 178: } else { 179: t2 = $[10]; 180: } 181: let t3; 182: if ($[11] !== showVim) { 183: t3 = showVim ? <Text dimColor={true} key="vim-insert">-- INSERT --</Text> : null; 184: $[11] = showVim; 185: $[12] = t3; 186: } else { 187: t3 = $[12]; 188: } 189: const t4 = !suppressHint && !showVim; 190: let t5; 191: if ($[13] !== isLoading || $[14] !== mode || $[15] !== onOpenTasksDialog || $[16] !== t4 || $[17] !== tasksSelected || $[18] !== teammateFooterIndex || $[19] !== teamsSelected || $[20] !== tmuxSelected || $[21] !== toolPermissionContext) { 192: t5 = <ModeIndicator mode={mode} toolPermissionContext={toolPermissionContext} showHint={t4} isLoading={isLoading} tasksSelected={tasksSelected} teamsSelected={teamsSelected} teammateFooterIndex={teammateFooterIndex} tmuxSelected={tmuxSelected} onOpenTasksDialog={onOpenTasksDialog} />; 193: $[13] = isLoading; 194: $[14] = mode; 195: $[15] = onOpenTasksDialog; 196: $[16] = t4; 197: $[17] = tasksSelected; 198: $[18] = teammateFooterIndex; 199: $[19] = teamsSelected; 200: $[20] = tmuxSelected; 201: $[21] = toolPermissionContext; 202: $[22] = t5; 203: } else { 204: t5 = $[22]; 205: } 206: let t6; 207: if ($[23] !== t2 || $[24] !== t3 || $[25] !== t5) { 208: t6 = <Box justifyContent="flex-start" gap={1}>{t2}{t3}{t5}</Box>; 209: $[23] = t2; 210: $[24] = t3; 211: $[25] = t5; 212: $[26] = t6; 213: } else { 214: t6 = $[26]; 215: } 216: return t6; 217: } 218: type ModeIndicatorProps = { 219: mode: PromptInputMode; 220: toolPermissionContext: ToolPermissionContext; 221: showHint: boolean; 222: isLoading: boolean; 223: tasksSelected: boolean; 224: teamsSelected: boolean; 225: tmuxSelected: boolean; 226: teammateFooterIndex?: number; 227: onOpenTasksDialog?: (taskId?: string) => void; 228: }; 229: function ModeIndicator({ 230: mode, 231: toolPermissionContext, 232: showHint, 233: isLoading, 234: tasksSelected, 235: teamsSelected, 236: tmuxSelected, 237: teammateFooterIndex, 238: onOpenTasksDialog 239: }: ModeIndicatorProps): React.ReactNode { 240: const { 241: columns 242: } = useTerminalSize(); 243: const modeCycleShortcut = useShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab'); 244: const tasks = useAppState(s => s.tasks); 245: const teamContext = useAppState(s_0 => s_0.teamContext); 246: const store = useAppStateStore(); 247: const [remoteSessionUrl] = useState(() => store.getState().remoteSessionUrl); 248: const viewSelectionMode = useAppState(s_1 => s_1.viewSelectionMode); 249: const viewingAgentTaskId = useAppState(s_2 => s_2.viewingAgentTaskId); 250: const expandedView = useAppState(s_3 => s_3.expandedView); 251: const showSpinnerTree = expandedView === 'teammates'; 252: const prStatus = usePrStatus(isLoading, isPrStatusEnabled()); 253: const hasTmuxSession = useAppState(s_4 => "external" === 'ant' && s_4.tungstenActiveSession !== undefined); 254: const nextTickAt = useSyncExternalStore(proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE, proactiveModule?.getNextTickAt ?? NULL, NULL); 255: const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false; 256: const voiceState = feature('VOICE_MODE') ? 257: useVoiceState(s_5 => s_5.voiceState) : 'idle' as const; 258: const voiceWarmingUp = feature('VOICE_MODE') ? 259: useVoiceState(s_6 => s_6.voiceWarmingUp) : false; 260: const hasSelection = useHasSelection(); 261: const selGetState = useSelection().getState; 262: const hasNextTick = nextTickAt !== null; 263: const isCoordinator = feature('COORDINATOR_MODE') ? coordinatorModule?.isCoordinatorMode() === true : false; 264: const runningTaskCount = useMemo(() => count(Object.values(tasks), t => isBackgroundTask(t) && !("external" === 'ant' && isPanelAgentTask(t))), [tasks]); 265: const tasksV2 = useTasksV2(); 266: const hasTaskItems = tasksV2 !== undefined && tasksV2.length > 0; 267: const escShortcut = useShortcutDisplay('chat:cancel', 'Chat', 'esc').toLowerCase(); 268: const todosShortcut = useShortcutDisplay('app:toggleTodos', 'Global', 'ctrl+t'); 269: const killAgentsShortcut = useShortcutDisplay('chat:killAgents', 'Chat', 'ctrl+x ctrl+k'); 270: const voiceKeyShortcut = feature('VOICE_MODE') ? 271: useShortcutDisplay('voice:pushToTalk', 'Chat', 'Space') : ''; 272: // Captured at mount so the hint doesn't flicker mid-session if another 273: const [voiceHintUnderCap] = feature('VOICE_MODE') ? 274: useState(() => (getGlobalConfig().voiceFooterHintSeenCount ?? 0) < MAX_VOICE_HINT_SHOWS) : [false]; 275: const voiceHintIncrementedRef = feature('VOICE_MODE') ? useRef(false) : null; 276: useEffect(() => { 277: if (feature('VOICE_MODE')) { 278: if (!voiceEnabled || !voiceHintUnderCap) return; 279: if (voiceHintIncrementedRef?.current) return; 280: if (voiceHintIncrementedRef) voiceHintIncrementedRef.current = true; 281: const newCount = (getGlobalConfig().voiceFooterHintSeenCount ?? 0) + 1; 282: saveGlobalConfig(prev => { 283: if ((prev.voiceFooterHintSeenCount ?? 0) >= newCount) return prev; 284: return { 285: ...prev, 286: voiceFooterHintSeenCount: newCount 287: }; 288: }); 289: } 290: }, [voiceEnabled, voiceHintUnderCap]); 291: const isKillAgentsConfirmShowing = useAppState(s_7 => s_7.notifications.current?.key === 'kill-agents-confirm'); 292: const hasTeams = isAgentSwarmsEnabled() && !isInProcessEnabled() && teamContext !== undefined && count(Object.values(teamContext.teammates), t_0 => t_0.name !== 'team-lead') > 0; 293: if (mode === 'bash') { 294: return <Text color="bashBorder">! for bash mode</Text>; 295: } 296: const currentMode = toolPermissionContext?.mode; 297: const hasActiveMode = !isDefaultMode(currentMode); 298: const viewedTask = viewingAgentTaskId ? tasks[viewingAgentTaskId] : undefined; 299: const isViewingTeammate = viewSelectionMode === 'viewing-agent' && viewedTask?.type === 'in_process_teammate'; 300: const isViewingCompletedTeammate = isViewingTeammate && viewedTask != null && viewedTask.status !== 'running'; 301: const hasBackgroundTasks = runningTaskCount > 0 || isViewingTeammate; 302: const primaryItemCount = (isCoordinator || hasActiveMode ? 1 : 0) + (hasBackgroundTasks ? 1 : 0) + (hasTeams ? 1 : 0); 303: const shouldShowPrStatus = isPrStatusEnabled() && prStatus.number !== null && prStatus.reviewState !== null && prStatus.url !== null && primaryItemCount < 2 && (primaryItemCount === 0 || columns >= 80); 304: const shouldShowModeHint = primaryItemCount < 2; 305: const hasInProcessTeammates = !showSpinnerTree && hasBackgroundTasks && Object.values(tasks).some(t_1 => t_1.type === 'in_process_teammate'); 306: const hasTeammatePills = hasInProcessTeammates || !showSpinnerTree && isViewingTeammate; 307: const modePart = currentMode && hasActiveMode && !getIsRemoteMode() ? <Text color={getModeColor(currentMode)} key="mode"> 308: {permissionModeSymbol(currentMode)}{' '} 309: {permissionModeTitle(currentMode).toLowerCase()} on 310: {shouldShowModeHint && <Text dimColor> 311: {' '} 312: <KeyboardShortcutHint shortcut={modeCycleShortcut} action="cycle" parens /> 313: </Text>} 314: </Text> : null; 315: const parts = [ 316: ...(remoteSessionUrl ? [<Link url={remoteSessionUrl} key="remote"> 317: <Text color="ide">{figures.circleDouble} remote</Text> 318: </Link>] : []), 319: ...("external" === 'ant' && hasTmuxSession ? [<TungstenPill key="tmux" selected={tmuxSelected} />] : []), ...(isAgentSwarmsEnabled() && hasTeams ? [<TeamStatus key="teams" teamsSelected={teamsSelected} showHint={showHint && !hasBackgroundTasks} />] : []), ...(shouldShowPrStatus ? [<PrBadge key="pr-status" number={prStatus.number!} url={prStatus.url!} reviewState={prStatus.reviewState!} />] : [])]; 320: const hasAnyInProcessTeammates = Object.values(tasks).some(t_2 => t_2.type === 'in_process_teammate' && t_2.status === 'running'); 321: const hasRunningAgentTasks = Object.values(tasks).some(t_3 => t_3.type === 'local_agent' && t_3.status === 'running'); 322: const hintParts = showHint ? getSpinnerHintParts(isLoading, escShortcut, todosShortcut, killAgentsShortcut, hasTaskItems, expandedView, hasAnyInProcessTeammates, hasRunningAgentTasks, isKillAgentsConfirmShowing) : []; 323: if (isViewingCompletedTeammate) { 324: parts.push(<Text dimColor key="esc-return"> 325: <KeyboardShortcutHint shortcut={escShortcut} action="return to team lead" /> 326: </Text>); 327: } else if ((feature('PROACTIVE') || feature('KAIROS')) && hasNextTick) { 328: parts.push(<ProactiveCountdown key="proactive" />); 329: } else if (!hasTeammatePills && showHint) { 330: parts.push(...hintParts); 331: } 332: if (hasTeammatePills) { 333: const otherParts = [...(modePart ? [modePart] : []), ...parts, ...(isViewingCompletedTeammate ? [] : hintParts)]; 334: return <Box flexDirection="column"> 335: <Box> 336: <BackgroundTaskStatus tasksSelected={tasksSelected} isViewingTeammate={isViewingTeammate} teammateFooterIndex={teammateFooterIndex} isLeaderIdle={!isLoading} onOpenDialog={onOpenTasksDialog} /> 337: </Box> 338: {otherParts.length > 0 && <Box> 339: <Byline>{otherParts}</Byline> 340: </Box>} 341: </Box>; 342: } 343: const hasCoordinatorTasks = "external" === 'ant' && getVisibleAgentTasks(tasks).length > 0; 344: const tasksPart = hasBackgroundTasks && !hasTeammatePills && !shouldHideTasksFooter(tasks, showSpinnerTree) ? <BackgroundTaskStatus tasksSelected={tasksSelected} isViewingTeammate={isViewingTeammate} teammateFooterIndex={teammateFooterIndex} isLeaderIdle={!isLoading} onOpenDialog={onOpenTasksDialog} /> : null; 345: if (parts.length === 0 && !tasksPart && !modePart && showHint) { 346: parts.push(<Text dimColor key="shortcuts-hint"> 347: ? for shortcuts 348: </Text>); 349: } 350: const copyOnSelect = getGlobalConfig().copyOnSelect ?? true; 351: const selectionHintHasContent = hasSelection && (!copyOnSelect || isXtermJs()); 352: if (feature('VOICE_MODE') && voiceEnabled && voiceWarmingUp) { 353: parts.push(<VoiceWarmupHint key="voice-warmup" />); 354: } else if (isFullscreenEnvEnabled() && selectionHintHasContent) { 355: const isMac = getPlatform() === 'macos'; 356: const altClickFailed = isMac && (selGetState()?.lastPressHadAlt ?? false); 357: parts.push(<Text dimColor key="selection-copy"> 358: <Byline> 359: {!copyOnSelect && <KeyboardShortcutHint shortcut="ctrl+c" action="copy" />} 360: {isXtermJs() && (altClickFailed ? <Text>set macOptionClickForcesSelection in VS Code settings</Text> : <KeyboardShortcutHint shortcut={isMac ? 'option+click' : 'shift+click'} action="native select" />)} 361: </Byline> 362: </Text>); 363: } else if (feature('VOICE_MODE') && parts.length > 0 && showHint && voiceEnabled && voiceState === 'idle' && hintParts.length === 0 && voiceHintUnderCap) { 364: parts.push(<Text dimColor key="voice-hint"> 365: hold {voiceKeyShortcut} to speak 366: </Text>); 367: } 368: if ((tasksPart || hasCoordinatorTasks) && showHint && !hasTeams) { 369: parts.push(<Text dimColor key="manage-tasks"> 370: {tasksSelected ? <KeyboardShortcutHint shortcut="Enter" action="view tasks" /> : <KeyboardShortcutHint shortcut="↓" action="manage" />} 371: </Text>); 372: } 373: if (parts.length === 0 && !tasksPart && !modePart) { 374: return isFullscreenEnvEnabled() ? <Text> </Text> : null; 375: } 376: return <Box height={1} overflow="hidden"> 377: {modePart && <Box flexShrink={0}> 378: {modePart} 379: {(tasksPart || parts.length > 0) && <Text dimColor> · </Text>} 380: </Box>} 381: {tasksPart && <Box flexShrink={0}> 382: {tasksPart} 383: {parts.length > 0 && <Text dimColor> · </Text>} 384: </Box>} 385: {parts.length > 0 && <Text wrap="truncate"> 386: <Byline>{parts}</Byline> 387: </Text>} 388: </Box>; 389: } 390: function getSpinnerHintParts(isLoading: boolean, escShortcut: string, todosShortcut: string, killAgentsShortcut: string, hasTaskItems: boolean, expandedView: 'none' | 'tasks' | 'teammates', hasTeammates: boolean, hasRunningAgentTasks: boolean, isKillAgentsConfirmShowing: boolean): React.ReactElement[] { 391: let toggleAction: string; 392: if (hasTeammates) { 393: switch (expandedView) { 394: case 'none': 395: toggleAction = 'show tasks'; 396: break; 397: case 'tasks': 398: toggleAction = 'show teammates'; 399: break; 400: case 'teammates': 401: toggleAction = 'hide'; 402: break; 403: } 404: } else { 405: toggleAction = expandedView === 'tasks' ? 'hide tasks' : 'show tasks'; 406: } 407: const showToggleHint = hasTaskItems || hasTeammates; 408: return [...(isLoading ? [<Text dimColor key="esc"> 409: <KeyboardShortcutHint shortcut={escShortcut} action="interrupt" /> 410: </Text>] : []), ...(!isLoading && hasRunningAgentTasks && !isKillAgentsConfirmShowing ? [<Text dimColor key="kill-agents"> 411: <KeyboardShortcutHint shortcut={killAgentsShortcut} action="stop agents" /> 412: </Text>] : []), ...(showToggleHint ? [<Text dimColor key="toggle-tasks"> 413: <KeyboardShortcutHint shortcut={todosShortcut} action={toggleAction} /> 414: </Text>] : [])]; 415: } 416: function isPrStatusEnabled(): boolean { 417: return getGlobalConfig().prStatusFooterEnabled ?? true; 418: }

File: src/components/PromptInput/PromptInputFooterSuggestions.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { memo, type ReactNode } from 'react'; 4: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 5: import { stringWidth } from '../../ink/stringWidth.js'; 6: import { Box, Text } from '../../ink.js'; 7: import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js'; 8: import type { Theme } from '../../utils/theme.js'; 9: export type SuggestionItem = { 10: id: string; 11: displayText: string; 12: tag?: string; 13: description?: string; 14: metadata?: unknown; 15: color?: keyof Theme; 16: }; 17: export type SuggestionType = 'command' | 'file' | 'directory' | 'agent' | 'shell' | 'custom-title' | 'slack-channel' | 'none'; 18: export const OVERLAY_MAX_ITEMS = 5; 19: function getIcon(itemId: string): string { 20: if (itemId.startsWith('file-')) return '+'; 21: if (itemId.startsWith('mcp-resource-')) return '◇'; 22: if (itemId.startsWith('agent-')) return '*'; 23: return '+'; 24: } 25: function isUnifiedSuggestion(itemId: string): boolean { 26: return itemId.startsWith('file-') || itemId.startsWith('mcp-resource-') || itemId.startsWith('agent-'); 27: } 28: const SuggestionItemRow = memo(function SuggestionItemRow(t0) { 29: const $ = _c(36); 30: const { 31: item, 32: maxColumnWidth, 33: isSelected 34: } = t0; 35: const columns = useTerminalSize().columns; 36: const isUnified = isUnifiedSuggestion(item.id); 37: if (isUnified) { 38: let t1; 39: if ($[0] !== item.id) { 40: t1 = getIcon(item.id); 41: $[0] = item.id; 42: $[1] = t1; 43: } else { 44: t1 = $[1]; 45: } 46: const icon = t1; 47: const textColor = isSelected ? "suggestion" : undefined; 48: const dimColor = !isSelected; 49: const isFile = item.id.startsWith("file-"); 50: const isMcpResource = item.id.startsWith("mcp-resource-"); 51: const separatorWidth = item.description ? 3 : 0; 52: let displayText; 53: if (isFile) { 54: let t2; 55: if ($[2] !== item.description) { 56: t2 = item.description ? Math.min(20, stringWidth(item.description)) : 0; 57: $[2] = item.description; 58: $[3] = t2; 59: } else { 60: t2 = $[3]; 61: } 62: const descReserve = t2; 63: const maxPathLength = columns - 2 - 4 - separatorWidth - descReserve; 64: let t3; 65: if ($[4] !== item.displayText || $[5] !== maxPathLength) { 66: t3 = truncatePathMiddle(item.displayText, maxPathLength); 67: $[4] = item.displayText; 68: $[5] = maxPathLength; 69: $[6] = t3; 70: } else { 71: t3 = $[6]; 72: } 73: displayText = t3; 74: } else { 75: if (isMcpResource) { 76: let t2; 77: if ($[7] !== item.displayText) { 78: t2 = truncateToWidth(item.displayText, 30); 79: $[7] = item.displayText; 80: $[8] = t2; 81: } else { 82: t2 = $[8]; 83: } 84: displayText = t2; 85: } else { 86: displayText = item.displayText; 87: } 88: } 89: const availableWidth = columns - 2 - stringWidth(displayText) - separatorWidth - 4; 90: let lineContent; 91: if (item.description) { 92: const maxDescLength = Math.max(0, availableWidth); 93: let t2; 94: if ($[9] !== item.description || $[10] !== maxDescLength) { 95: t2 = truncateToWidth(item.description.replace(/\s+/g, " "), maxDescLength); 96: $[9] = item.description; 97: $[10] = maxDescLength; 98: $[11] = t2; 99: } else { 100: t2 = $[11]; 101: } 102: const truncatedDesc = t2; 103: lineContent = `${icon} ${displayText} – ${truncatedDesc}`; 104: } else { 105: lineContent = `${icon} ${displayText}`; 106: } 107: let t2; 108: if ($[12] !== dimColor || $[13] !== lineContent || $[14] !== textColor) { 109: t2 = <Text color={textColor} dimColor={dimColor} wrap="truncate">{lineContent}</Text>; 110: $[12] = dimColor; 111: $[13] = lineContent; 112: $[14] = textColor; 113: $[15] = t2; 114: } else { 115: t2 = $[15]; 116: } 117: return t2; 118: } 119: const maxNameWidth = Math.floor(columns * 0.4); 120: const displayTextWidth = Math.min(maxColumnWidth ?? stringWidth(item.displayText) + 5, maxNameWidth); 121: const textColor_0 = item.color || (isSelected ? "suggestion" : undefined); 122: const shouldDim = !isSelected; 123: let displayText_0 = item.displayText; 124: if (stringWidth(displayText_0) > displayTextWidth - 2) { 125: const t1 = displayTextWidth - 2; 126: let t2; 127: if ($[16] !== displayText_0 || $[17] !== t1) { 128: t2 = truncateToWidth(displayText_0, t1); 129: $[16] = displayText_0; 130: $[17] = t1; 131: $[18] = t2; 132: } else { 133: t2 = $[18]; 134: } 135: displayText_0 = t2; 136: } 137: const paddedDisplayText = displayText_0 + " ".repeat(Math.max(0, displayTextWidth - stringWidth(displayText_0))); 138: const tagText = item.tag ? `[${item.tag}] ` : ""; 139: const tagWidth = stringWidth(tagText); 140: const descriptionWidth = Math.max(0, columns - displayTextWidth - tagWidth - 4); 141: let t1; 142: if ($[19] !== descriptionWidth || $[20] !== item.description) { 143: t1 = item.description ? truncateToWidth(item.description.replace(/\s+/g, " "), descriptionWidth) : ""; 144: $[19] = descriptionWidth; 145: $[20] = item.description; 146: $[21] = t1; 147: } else { 148: t1 = $[21]; 149: } 150: const truncatedDescription = t1; 151: let t2; 152: if ($[22] !== paddedDisplayText || $[23] !== shouldDim || $[24] !== textColor_0) { 153: t2 = <Text color={textColor_0} dimColor={shouldDim}>{paddedDisplayText}</Text>; 154: $[22] = paddedDisplayText; 155: $[23] = shouldDim; 156: $[24] = textColor_0; 157: $[25] = t2; 158: } else { 159: t2 = $[25]; 160: } 161: let t3; 162: if ($[26] !== tagText) { 163: t3 = tagText ? <Text dimColor={true}>{tagText}</Text> : null; 164: $[26] = tagText; 165: $[27] = t3; 166: } else { 167: t3 = $[27]; 168: } 169: const t4 = isSelected ? "suggestion" : undefined; 170: const t5 = !isSelected; 171: let t6; 172: if ($[28] !== t4 || $[29] !== t5 || $[30] !== truncatedDescription) { 173: t6 = <Text color={t4} dimColor={t5}>{truncatedDescription}</Text>; 174: $[28] = t4; 175: $[29] = t5; 176: $[30] = truncatedDescription; 177: $[31] = t6; 178: } else { 179: t6 = $[31]; 180: } 181: let t7; 182: if ($[32] !== t2 || $[33] !== t3 || $[34] !== t6) { 183: t7 = <Text wrap="truncate">{t2}{t3}{t6}</Text>; 184: $[32] = t2; 185: $[33] = t3; 186: $[34] = t6; 187: $[35] = t7; 188: } else { 189: t7 = $[35]; 190: } 191: return t7; 192: }); 193: type Props = { 194: suggestions: SuggestionItem[]; 195: selectedSuggestion: number; 196: maxColumnWidth?: number; 197: overlay?: boolean; 198: }; 199: export function PromptInputFooterSuggestions(t0) { 200: const $ = _c(22); 201: const { 202: suggestions, 203: selectedSuggestion, 204: maxColumnWidth: maxColumnWidthProp, 205: overlay 206: } = t0; 207: const { 208: rows 209: } = useTerminalSize(); 210: const maxVisibleItems = overlay ? OVERLAY_MAX_ITEMS : Math.min(6, Math.max(1, rows - 3)); 211: if (suggestions.length === 0) { 212: return null; 213: } 214: let t1; 215: if ($[0] !== maxColumnWidthProp || $[1] !== suggestions) { 216: t1 = maxColumnWidthProp ?? Math.max(...suggestions.map(_temp)) + 5; 217: $[0] = maxColumnWidthProp; 218: $[1] = suggestions; 219: $[2] = t1; 220: } else { 221: t1 = $[2]; 222: } 223: const maxColumnWidth = t1; 224: const startIndex = Math.max(0, Math.min(selectedSuggestion - Math.floor(maxVisibleItems / 2), suggestions.length - maxVisibleItems)); 225: const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length); 226: let T0; 227: let t2; 228: let t3; 229: let t4; 230: if ($[3] !== endIndex || $[4] !== maxColumnWidth || $[5] !== overlay || $[6] !== selectedSuggestion || $[7] !== startIndex || $[8] !== suggestions) { 231: const visibleItems = suggestions.slice(startIndex, endIndex); 232: T0 = Box; 233: t2 = "column"; 234: t3 = overlay ? undefined : "flex-end"; 235: let t5; 236: if ($[13] !== maxColumnWidth || $[14] !== selectedSuggestion || $[15] !== suggestions) { 237: t5 = item_0 => <SuggestionItemRow key={item_0.id} item={item_0} maxColumnWidth={maxColumnWidth} isSelected={item_0.id === suggestions[selectedSuggestion]?.id} />; 238: $[13] = maxColumnWidth; 239: $[14] = selectedSuggestion; 240: $[15] = suggestions; 241: $[16] = t5; 242: } else { 243: t5 = $[16]; 244: } 245: t4 = visibleItems.map(t5); 246: $[3] = endIndex; 247: $[4] = maxColumnWidth; 248: $[5] = overlay; 249: $[6] = selectedSuggestion; 250: $[7] = startIndex; 251: $[8] = suggestions; 252: $[9] = T0; 253: $[10] = t2; 254: $[11] = t3; 255: $[12] = t4; 256: } else { 257: T0 = $[9]; 258: t2 = $[10]; 259: t3 = $[11]; 260: t4 = $[12]; 261: } 262: let t5; 263: if ($[17] !== T0 || $[18] !== t2 || $[19] !== t3 || $[20] !== t4) { 264: t5 = <T0 flexDirection={t2} justifyContent={t3}>{t4}</T0>; 265: $[17] = T0; 266: $[18] = t2; 267: $[19] = t3; 268: $[20] = t4; 269: $[21] = t5; 270: } else { 271: t5 = $[21]; 272: } 273: return t5; 274: } 275: function _temp(item) { 276: return stringWidth(item.displayText); 277: } 278: export default memo(PromptInputFooterSuggestions);

File: src/components/PromptInput/PromptInputHelpMenu.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import * as React from 'react'; 4: import { Box, Text } from 'src/ink.js'; 5: import { getPlatform } from 'src/utils/platform.js'; 6: import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'; 7: import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'; 8: import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; 9: import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'; 10: import { getNewlineInstructions } from './utils.js'; 11: function formatShortcut(shortcut: string): string { 12: return shortcut.replace(/\+/g, ' + '); 13: } 14: type Props = { 15: dimColor?: boolean; 16: fixedWidth?: boolean; 17: gap?: number; 18: paddingX?: number; 19: }; 20: export function PromptInputHelpMenu(props) { 21: const $ = _c(99); 22: const { 23: dimColor, 24: fixedWidth, 25: gap, 26: paddingX 27: } = props; 28: const t0 = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o"); 29: let t1; 30: if ($[0] !== t0) { 31: t1 = formatShortcut(t0); 32: $[0] = t0; 33: $[1] = t1; 34: } else { 35: t1 = $[1]; 36: } 37: const transcriptShortcut = t1; 38: const t2 = useShortcutDisplay("app:toggleTodos", "Global", "ctrl+t"); 39: let t3; 40: if ($[2] !== t2) { 41: t3 = formatShortcut(t2); 42: $[2] = t2; 43: $[3] = t3; 44: } else { 45: t3 = $[3]; 46: } 47: const todosShortcut = t3; 48: const t4 = useShortcutDisplay("chat:undo", "Chat", "ctrl+_"); 49: let t5; 50: if ($[4] !== t4) { 51: t5 = formatShortcut(t4); 52: $[4] = t4; 53: $[5] = t5; 54: } else { 55: t5 = $[5]; 56: } 57: const undoShortcut = t5; 58: const t6 = useShortcutDisplay("chat:stash", "Chat", "ctrl+s"); 59: let t7; 60: if ($[6] !== t6) { 61: t7 = formatShortcut(t6); 62: $[6] = t6; 63: $[7] = t7; 64: } else { 65: t7 = $[7]; 66: } 67: const stashShortcut = t7; 68: const t8 = useShortcutDisplay("chat:cycleMode", "Chat", "shift+tab"); 69: let t9; 70: if ($[8] !== t8) { 71: t9 = formatShortcut(t8); 72: $[8] = t8; 73: $[9] = t9; 74: } else { 75: t9 = $[9]; 76: } 77: const cycleModeShortcut = t9; 78: const t10 = useShortcutDisplay("chat:modelPicker", "Chat", "alt+p"); 79: let t11; 80: if ($[10] !== t10) { 81: t11 = formatShortcut(t10); 82: $[10] = t10; 83: $[11] = t11; 84: } else { 85: t11 = $[11]; 86: } 87: const modelPickerShortcut = t11; 88: const t12 = useShortcutDisplay("chat:fastMode", "Chat", "alt+o"); 89: let t13; 90: if ($[12] !== t12) { 91: t13 = formatShortcut(t12); 92: $[12] = t12; 93: $[13] = t13; 94: } else { 95: t13 = $[13]; 96: } 97: const fastModeShortcut = t13; 98: const t14 = useShortcutDisplay("chat:externalEditor", "Chat", "ctrl+g"); 99: let t15; 100: if ($[14] !== t14) { 101: t15 = formatShortcut(t14); 102: $[14] = t14; 103: $[15] = t15; 104: } else { 105: t15 = $[15]; 106: } 107: const externalEditorShortcut = t15; 108: const t16 = useShortcutDisplay("app:toggleTerminal", "Global", "meta+j"); 109: let t17; 110: if ($[16] !== t16) { 111: t17 = formatShortcut(t16); 112: $[16] = t16; 113: $[17] = t17; 114: } else { 115: t17 = $[17]; 116: } 117: const terminalShortcut = t17; 118: const t18 = useShortcutDisplay("chat:imagePaste", "Chat", "ctrl+v"); 119: let t19; 120: if ($[18] !== t18) { 121: t19 = formatShortcut(t18); 122: $[18] = t18; 123: $[19] = t19; 124: } else { 125: t19 = $[19]; 126: } 127: const imagePasteShortcut = t19; 128: let t20; 129: if ($[20] !== dimColor || $[21] !== terminalShortcut) { 130: t20 = feature("TERMINAL_PANEL") ? getFeatureValue_CACHED_MAY_BE_STALE("tengu_terminal_panel", false) ? <Box><Text dimColor={dimColor}>{terminalShortcut} for terminal</Text></Box> : null : null; 131: $[20] = dimColor; 132: $[21] = terminalShortcut; 133: $[22] = t20; 134: } else { 135: t20 = $[22]; 136: } 137: const terminalShortcutElement = t20; 138: const t21 = fixedWidth ? 24 : undefined; 139: let t22; 140: if ($[23] !== dimColor) { 141: t22 = <Box><Text dimColor={dimColor}>! for bash mode</Text></Box>; 142: $[23] = dimColor; 143: $[24] = t22; 144: } else { 145: t22 = $[24]; 146: } 147: let t23; 148: if ($[25] !== dimColor) { 149: t23 = <Box><Text dimColor={dimColor}>/ for commands</Text></Box>; 150: $[25] = dimColor; 151: $[26] = t23; 152: } else { 153: t23 = $[26]; 154: } 155: let t24; 156: if ($[27] !== dimColor) { 157: t24 = <Box><Text dimColor={dimColor}>@ for file paths</Text></Box>; 158: $[27] = dimColor; 159: $[28] = t24; 160: } else { 161: t24 = $[28]; 162: } 163: let t25; 164: if ($[29] !== dimColor) { 165: t25 = <Box><Text dimColor={dimColor}>{"& for background"}</Text></Box>; 166: $[29] = dimColor; 167: $[30] = t25; 168: } else { 169: t25 = $[30]; 170: } 171: let t26; 172: if ($[31] !== dimColor) { 173: t26 = <Box><Text dimColor={dimColor}>/btw for side question</Text></Box>; 174: $[31] = dimColor; 175: $[32] = t26; 176: } else { 177: t26 = $[32]; 178: } 179: let t27; 180: if ($[33] !== t21 || $[34] !== t22 || $[35] !== t23 || $[36] !== t24 || $[37] !== t25 || $[38] !== t26) { 181: t27 = <Box flexDirection="column" width={t21}>{t22}{t23}{t24}{t25}{t26}</Box>; 182: $[33] = t21; 183: $[34] = t22; 184: $[35] = t23; 185: $[36] = t24; 186: $[37] = t25; 187: $[38] = t26; 188: $[39] = t27; 189: } else { 190: t27 = $[39]; 191: } 192: const t28 = fixedWidth ? 35 : undefined; 193: let t29; 194: if ($[40] !== dimColor) { 195: t29 = <Box><Text dimColor={dimColor}>double tap esc to clear input</Text></Box>; 196: $[40] = dimColor; 197: $[41] = t29; 198: } else { 199: t29 = $[41]; 200: } 201: let t30; 202: if ($[42] !== cycleModeShortcut || $[43] !== dimColor) { 203: t30 = <Box><Text dimColor={dimColor}>{cycleModeShortcut}{" "}{false ? "to cycle modes" : "to auto-accept edits"}</Text></Box>; 204: $[42] = cycleModeShortcut; 205: $[43] = dimColor; 206: $[44] = t30; 207: } else { 208: t30 = $[44]; 209: } 210: let t31; 211: if ($[45] !== dimColor || $[46] !== transcriptShortcut) { 212: t31 = <Box><Text dimColor={dimColor}>{transcriptShortcut} for verbose output</Text></Box>; 213: $[45] = dimColor; 214: $[46] = transcriptShortcut; 215: $[47] = t31; 216: } else { 217: t31 = $[47]; 218: } 219: let t32; 220: if ($[48] !== dimColor || $[49] !== todosShortcut) { 221: t32 = <Box><Text dimColor={dimColor}>{todosShortcut} to toggle tasks</Text></Box>; 222: $[48] = dimColor; 223: $[49] = todosShortcut; 224: $[50] = t32; 225: } else { 226: t32 = $[50]; 227: } 228: let t33; 229: if ($[51] === Symbol.for("react.memo_cache_sentinel")) { 230: t33 = getNewlineInstructions(); 231: $[51] = t33; 232: } else { 233: t33 = $[51]; 234: } 235: let t34; 236: if ($[52] !== dimColor) { 237: t34 = <Box><Text dimColor={dimColor}>{t33}</Text></Box>; 238: $[52] = dimColor; 239: $[53] = t34; 240: } else { 241: t34 = $[53]; 242: } 243: let t35; 244: if ($[54] !== t28 || $[55] !== t29 || $[56] !== t30 || $[57] !== t31 || $[58] !== t32 || $[59] !== t34 || $[60] !== terminalShortcutElement) { 245: t35 = <Box flexDirection="column" width={t28}>{t29}{t30}{t31}{t32}{terminalShortcutElement}{t34}</Box>; 246: $[54] = t28; 247: $[55] = t29; 248: $[56] = t30; 249: $[57] = t31; 250: $[58] = t32; 251: $[59] = t34; 252: $[60] = terminalShortcutElement; 253: $[61] = t35; 254: } else { 255: t35 = $[61]; 256: } 257: let t36; 258: if ($[62] !== dimColor || $[63] !== undoShortcut) { 259: t36 = <Box><Text dimColor={dimColor}>{undoShortcut} to undo</Text></Box>; 260: $[62] = dimColor; 261: $[63] = undoShortcut; 262: $[64] = t36; 263: } else { 264: t36 = $[64]; 265: } 266: let t37; 267: if ($[65] !== dimColor) { 268: t37 = getPlatform() !== "windows" && <Box><Text dimColor={dimColor}>ctrl + z to suspend</Text></Box>; 269: $[65] = dimColor; 270: $[66] = t37; 271: } else { 272: t37 = $[66]; 273: } 274: let t38; 275: if ($[67] !== dimColor || $[68] !== imagePasteShortcut) { 276: t38 = <Box><Text dimColor={dimColor}>{imagePasteShortcut} to paste images</Text></Box>; 277: $[67] = dimColor; 278: $[68] = imagePasteShortcut; 279: $[69] = t38; 280: } else { 281: t38 = $[69]; 282: } 283: let t39; 284: if ($[70] !== dimColor || $[71] !== modelPickerShortcut) { 285: t39 = <Box><Text dimColor={dimColor}>{modelPickerShortcut} to switch model</Text></Box>; 286: $[70] = dimColor; 287: $[71] = modelPickerShortcut; 288: $[72] = t39; 289: } else { 290: t39 = $[72]; 291: } 292: let t40; 293: if ($[73] !== dimColor || $[74] !== fastModeShortcut) { 294: t40 = isFastModeEnabled() && isFastModeAvailable() && <Box><Text dimColor={dimColor}>{fastModeShortcut} to toggle fast mode</Text></Box>; 295: $[73] = dimColor; 296: $[74] = fastModeShortcut; 297: $[75] = t40; 298: } else { 299: t40 = $[75]; 300: } 301: let t41; 302: if ($[76] !== dimColor || $[77] !== stashShortcut) { 303: t41 = <Box><Text dimColor={dimColor}>{stashShortcut} to stash prompt</Text></Box>; 304: $[76] = dimColor; 305: $[77] = stashShortcut; 306: $[78] = t41; 307: } else { 308: t41 = $[78]; 309: } 310: let t42; 311: if ($[79] !== dimColor || $[80] !== externalEditorShortcut) { 312: t42 = <Box><Text dimColor={dimColor}>{externalEditorShortcut} to edit in $EDITOR</Text></Box>; 313: $[79] = dimColor; 314: $[80] = externalEditorShortcut; 315: $[81] = t42; 316: } else { 317: t42 = $[81]; 318: } 319: let t43; 320: if ($[82] !== dimColor) { 321: t43 = isKeybindingCustomizationEnabled() && <Box><Text dimColor={dimColor}>/keybindings to customize</Text></Box>; 322: $[82] = dimColor; 323: $[83] = t43; 324: } else { 325: t43 = $[83]; 326: } 327: let t44; 328: if ($[84] !== t36 || $[85] !== t37 || $[86] !== t38 || $[87] !== t39 || $[88] !== t40 || $[89] !== t41 || $[90] !== t42 || $[91] !== t43) { 329: t44 = <Box flexDirection="column">{t36}{t37}{t38}{t39}{t40}{t41}{t42}{t43}</Box>; 330: $[84] = t36; 331: $[85] = t37; 332: $[86] = t38; 333: $[87] = t39; 334: $[88] = t40; 335: $[89] = t41; 336: $[90] = t42; 337: $[91] = t43; 338: $[92] = t44; 339: } else { 340: t44 = $[92]; 341: } 342: let t45; 343: if ($[93] !== gap || $[94] !== paddingX || $[95] !== t27 || $[96] !== t35 || $[97] !== t44) { 344: t45 = <Box paddingX={paddingX} flexDirection="row" gap={gap}>{t27}{t35}{t44}</Box>; 345: $[93] = gap; 346: $[94] = paddingX; 347: $[95] = t27; 348: $[96] = t35; 349: $[97] = t44; 350: $[98] = t45; 351: } else { 352: t45 = $[98]; 353: } 354: return t45; 355: }

File: src/components/PromptInput/PromptInputModeIndicator.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { Box, Text } from 'src/ink.js'; 5: import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from 'src/tools/AgentTool/agentColorManager.js'; 6: import type { PromptInputMode } from 'src/types/textInputTypes.js'; 7: import { getTeammateColor } from 'src/utils/teammate.js'; 8: import type { Theme } from 'src/utils/theme.js'; 9: import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'; 10: type Props = { 11: mode: PromptInputMode; 12: isLoading: boolean; 13: viewingAgentName?: string; 14: viewingAgentColor?: AgentColorName; 15: }; 16: function getTeammateThemeColor(): keyof Theme | undefined { 17: if (!isAgentSwarmsEnabled()) { 18: return undefined; 19: } 20: const colorName = getTeammateColor(); 21: if (!colorName) { 22: return undefined; 23: } 24: if (AGENT_COLORS.includes(colorName as AgentColorName)) { 25: return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]; 26: } 27: return undefined; 28: } 29: type PromptCharProps = { 30: isLoading: boolean; 31: themeColor?: keyof Theme; 32: }; 33: function PromptChar(t0) { 34: const $ = _c(3); 35: const { 36: isLoading, 37: themeColor 38: } = t0; 39: const teammateColor = themeColor; 40: const color = teammateColor ?? (false ? "subtle" : undefined); 41: let t1; 42: if ($[0] !== color || $[1] !== isLoading) { 43: t1 = <Text color={color} dimColor={isLoading}>{figures.pointer} </Text>; 44: $[0] = color; 45: $[1] = isLoading; 46: $[2] = t1; 47: } else { 48: t1 = $[2]; 49: } 50: return t1; 51: } 52: export function PromptInputModeIndicator(t0) { 53: const $ = _c(6); 54: const { 55: mode, 56: isLoading, 57: viewingAgentName, 58: viewingAgentColor 59: } = t0; 60: let t1; 61: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 62: t1 = getTeammateThemeColor(); 63: $[0] = t1; 64: } else { 65: t1 = $[0]; 66: } 67: const teammateColor = t1; 68: const viewedTeammateThemeColor = viewingAgentColor ? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor] : undefined; 69: let t2; 70: if ($[1] !== isLoading || $[2] !== mode || $[3] !== viewedTeammateThemeColor || $[4] !== viewingAgentName) { 71: t2 = <Box alignItems="flex-start" alignSelf="flex-start" flexWrap="nowrap" justifyContent="flex-start">{viewingAgentName ? <PromptChar isLoading={isLoading} themeColor={viewedTeammateThemeColor} /> : mode === "bash" ? <Text color="bashBorder" dimColor={isLoading}>! </Text> : <PromptChar isLoading={isLoading} themeColor={isAgentSwarmsEnabled() ? teammateColor : undefined} />}</Box>; 72: $[1] = isLoading; 73: $[2] = mode; 74: $[3] = viewedTeammateThemeColor; 75: $[4] = viewingAgentName; 76: $[5] = t2; 77: } else { 78: t2 = $[5]; 79: } 80: return t2; 81: }

File: src/components/PromptInput/PromptInputQueuedCommands.tsx

typescript 1: import { feature } from 'bun:bundle'; 2: import * as React from 'react'; 3: import { useMemo } from 'react'; 4: import { Box } from 'src/ink.js'; 5: import { useAppState } from 'src/state/AppState.js'; 6: import { STATUS_TAG, SUMMARY_TAG, TASK_NOTIFICATION_TAG } from '../../constants/xml.js'; 7: import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'; 8: import { useCommandQueue } from '../../hooks/useCommandQueue.js'; 9: import type { QueuedCommand } from '../../types/textInputTypes.js'; 10: import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js'; 11: import { createUserMessage, EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js'; 12: import { jsonParse } from '../../utils/slowOperations.js'; 13: import { Message } from '../Message.js'; 14: const EMPTY_SET = new Set<string>(); 15: function isIdleNotification(value: string): boolean { 16: try { 17: const parsed = jsonParse(value); 18: return parsed?.type === 'idle_notification'; 19: } catch { 20: return false; 21: } 22: } 23: const MAX_VISIBLE_NOTIFICATIONS = 3; 24: function createOverflowNotificationMessage(count: number): string { 25: return `<${TASK_NOTIFICATION_TAG}> 26: <${SUMMARY_TAG}>+${count} more tasks completed</${SUMMARY_TAG}> 27: <${STATUS_TAG}>completed</${STATUS_TAG}> 28: </${TASK_NOTIFICATION_TAG}>`; 29: } 30: function processQueuedCommands(queuedCommands: QueuedCommand[]): QueuedCommand[] { 31: const filteredCommands = queuedCommands.filter(cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value)); 32: const taskNotifications = filteredCommands.filter(cmd => cmd.mode === 'task-notification'); 33: const otherCommands = filteredCommands.filter(cmd => cmd.mode !== 'task-notification'); 34: if (taskNotifications.length <= MAX_VISIBLE_NOTIFICATIONS) { 35: return [...otherCommands, ...taskNotifications]; 36: } 37: const visibleNotifications = taskNotifications.slice(0, MAX_VISIBLE_NOTIFICATIONS - 1); 38: const overflowCount = taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1); 39: const overflowCommand: QueuedCommand = { 40: value: createOverflowNotificationMessage(overflowCount), 41: mode: 'task-notification' 42: }; 43: return [...otherCommands, ...visibleNotifications, overflowCommand]; 44: } 45: function PromptInputQueuedCommandsImpl(): React.ReactNode { 46: const queuedCommands = useCommandQueue(); 47: const viewingAgent = useAppState(s => !!s.viewingAgentTaskId); 48: const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ? 49: useAppState(s_0 => s_0.isBriefOnly) : false; 50: const messages = useMemo(() => { 51: if (queuedCommands.length === 0) return null; 52: const visibleCommands = queuedCommands.filter(isQueuedCommandVisible); 53: if (visibleCommands.length === 0) return null; 54: const processedCommands = processQueuedCommands(visibleCommands); 55: return normalizeMessages(processedCommands.map(cmd => { 56: let content = cmd.value; 57: if (cmd.mode === 'bash' && typeof content === 'string') { 58: content = `<bash-input>${content}</bash-input>`; 59: } 60: return createUserMessage({ 61: content 62: }); 63: })); 64: }, [queuedCommands]); 65: if (viewingAgent || messages === null) { 66: return null; 67: } 68: return <Box marginTop={1} flexDirection="column"> 69: {messages.map((message, i) => <QueuedMessageProvider key={i} isFirst={i === 0} useBriefLayout={useBriefLayout}> 70: <Message message={message} lookups={EMPTY_LOOKUPS} addMargin={false} tools={[]} commands={[]} verbose={false} inProgressToolUseIDs={EMPTY_SET} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} isTranscriptMode={false} isStatic={true} /> 71: </QueuedMessageProvider>)} 72: </Box>; 73: } 74: export const PromptInputQueuedCommands = React.memo(PromptInputQueuedCommandsImpl);

File: src/components/PromptInput/PromptInputStashNotice.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { Box, Text } from 'src/ink.js'; 5: type Props = { 6: hasStash: boolean; 7: }; 8: export function PromptInputStashNotice(t0) { 9: const $ = _c(1); 10: const { 11: hasStash 12: } = t0; 13: if (!hasStash) { 14: return null; 15: } 16: let t1; 17: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 18: t1 = <Box paddingLeft={2}><Text dimColor={true}>{figures.pointerSmall} Stashed (auto-restores after submit)</Text></Box>; 19: $[0] = t1; 20: } else { 21: t1 = $[0]; 22: } 23: return t1; 24: }

File: src/components/PromptInput/SandboxPromptFooterHint.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { type ReactNode, useEffect, useRef, useState } from 'react'; 4: import { Box, Text } from '../../ink.js'; 5: import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'; 6: import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'; 7: export function SandboxPromptFooterHint() { 8: const $ = _c(6); 9: const [recentViolationCount, setRecentViolationCount] = useState(0); 10: const timerRef = useRef(null); 11: const detailsShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o"); 12: let t0; 13: let t1; 14: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 15: t0 = () => { 16: if (!SandboxManager.isSandboxingEnabled()) { 17: return; 18: } 19: const store = SandboxManager.getSandboxViolationStore(); 20: let lastCount = store.getTotalCount(); 21: const unsubscribe = store.subscribe(() => { 22: const currentCount = store.getTotalCount(); 23: const newViolations = currentCount - lastCount; 24: if (newViolations > 0) { 25: setRecentViolationCount(newViolations); 26: lastCount = currentCount; 27: if (timerRef.current) { 28: clearTimeout(timerRef.current); 29: } 30: timerRef.current = setTimeout(setRecentViolationCount, 5000, 0); 31: } 32: }); 33: return () => { 34: unsubscribe(); 35: if (timerRef.current) { 36: clearTimeout(timerRef.current); 37: } 38: }; 39: }; 40: t1 = []; 41: $[0] = t0; 42: $[1] = t1; 43: } else { 44: t0 = $[0]; 45: t1 = $[1]; 46: } 47: useEffect(t0, t1); 48: if (!SandboxManager.isSandboxingEnabled() || recentViolationCount === 0) { 49: return null; 50: } 51: const t2 = recentViolationCount === 1 ? "operation" : "operations"; 52: let t3; 53: if ($[2] !== detailsShortcut || $[3] !== recentViolationCount || $[4] !== t2) { 54: t3 = <Box paddingX={0} paddingY={0}><Text color="inactive" wrap="truncate">⧈ Sandbox blocked {recentViolationCount}{" "}{t2} ·{" "}{detailsShortcut} for details · /sandbox to disable</Text></Box>; 55: $[2] = detailsShortcut; 56: $[3] = recentViolationCount; 57: $[4] = t2; 58: $[5] = t3; 59: } else { 60: t3 = $[5]; 61: } 62: return t3; 63: }

File: src/components/PromptInput/ShimmeredInput.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js'; 4: import { segmentTextByHighlights, type TextHighlight } from '../../utils/textHighlighting.js'; 5: import { ShimmerChar } from '../Spinner/ShimmerChar.js'; 6: type Props = { 7: text: string; 8: highlights: TextHighlight[]; 9: }; 10: type LinePart = { 11: text: string; 12: highlight: TextHighlight | undefined; 13: start: number; 14: }; 15: export function HighlightedInput(t0) { 16: const $ = _c(23); 17: const { 18: text, 19: highlights 20: } = t0; 21: let lines; 22: if ($[0] !== highlights || $[1] !== text) { 23: const segments = segmentTextByHighlights(text, highlights); 24: lines = [[]]; 25: let pos = 0; 26: for (const segment of segments) { 27: const parts = segment.text.split("\n"); 28: for (let i = 0; i < parts.length; i++) { 29: if (i > 0) { 30: lines.push([]); 31: pos = pos + 1; 32: } 33: const part = parts[i]; 34: if (part.length > 0) { 35: lines[lines.length - 1].push({ 36: text: part, 37: highlight: segment.highlight, 38: start: pos 39: }); 40: } 41: pos = pos + part.length; 42: } 43: } 44: $[0] = highlights; 45: $[1] = text; 46: $[2] = lines; 47: } else { 48: lines = $[2]; 49: } 50: let t1; 51: if ($[3] !== highlights) { 52: t1 = highlights.some(_temp); 53: $[3] = highlights; 54: $[4] = t1; 55: } else { 56: t1 = $[4]; 57: } 58: const hasShimmer = t1; 59: let sweepStart = 0; 60: let cycleLength = 1; 61: if (hasShimmer) { 62: let lo = Infinity; 63: let hi = -Infinity; 64: if ($[5] !== hi || $[6] !== highlights || $[7] !== lo) { 65: for (const h_0 of highlights) { 66: if (h_0.shimmerColor) { 67: lo = Math.min(lo, h_0.start); 68: hi = Math.max(hi, h_0.end); 69: } 70: } 71: $[5] = hi; 72: $[6] = highlights; 73: $[7] = lo; 74: $[8] = lo; 75: $[9] = hi; 76: } else { 77: lo = $[8]; 78: hi = $[9]; 79: } 80: sweepStart = lo - 10; 81: cycleLength = hi - lo + 20; 82: } 83: let t2; 84: if ($[10] !== cycleLength || $[11] !== hasShimmer || $[12] !== lines || $[13] !== sweepStart) { 85: t2 = { 86: lines, 87: hasShimmer, 88: sweepStart, 89: cycleLength 90: }; 91: $[10] = cycleLength; 92: $[11] = hasShimmer; 93: $[12] = lines; 94: $[13] = sweepStart; 95: $[14] = t2; 96: } else { 97: t2 = $[14]; 98: } 99: const { 100: lines: lines_0, 101: hasShimmer: hasShimmer_0, 102: sweepStart: sweepStart_0, 103: cycleLength: cycleLength_0 104: } = t2; 105: const [ref, time] = useAnimationFrame(hasShimmer_0 ? 50 : null); 106: const glimmerIndex = hasShimmer_0 ? sweepStart_0 + Math.floor(time / 50) % cycleLength_0 : -100; 107: let t3; 108: if ($[15] !== glimmerIndex || $[16] !== lines_0) { 109: let t4; 110: if ($[18] !== glimmerIndex) { 111: t4 = (lineParts, lineIndex) => <Box key={lineIndex}>{lineParts.length === 0 ? <Text> </Text> : lineParts.map((part_0, partIndex) => { 112: if (part_0.highlight?.shimmerColor && part_0.highlight.color) { 113: return <Text key={partIndex}>{part_0.text.split("").map((char, charIndex) => <ShimmerChar key={charIndex} char={char} index={part_0.start + charIndex} glimmerIndex={glimmerIndex} messageColor={part_0.highlight.color} shimmerColor={part_0.highlight.shimmerColor} />)}</Text>; 114: } 115: return <Text key={partIndex} color={part_0.highlight?.color} dimColor={part_0.highlight?.dimColor} inverse={part_0.highlight?.inverse}><Ansi>{part_0.text}</Ansi></Text>; 116: })}</Box>; 117: $[18] = glimmerIndex; 118: $[19] = t4; 119: } else { 120: t4 = $[19]; 121: } 122: t3 = lines_0.map(t4); 123: $[15] = glimmerIndex; 124: $[16] = lines_0; 125: $[17] = t3; 126: } else { 127: t3 = $[17]; 128: } 129: let t4; 130: if ($[20] !== ref || $[21] !== t3) { 131: t4 = <Box ref={ref} flexDirection="column">{t3}</Box>; 132: $[20] = ref; 133: $[21] = t3; 134: $[22] = t4; 135: } else { 136: t4 = $[22]; 137: } 138: return t4; 139: } 140: function _temp(h) { 141: return h.shimmerColor; 142: }

File: src/components/PromptInput/useMaybeTruncateInput.ts

typescript 1: import { useEffect, useState } from 'react' 2: import type { PastedContent } from 'src/utils/config.js' 3: import { maybeTruncateInput } from './inputPaste.js' 4: type Props = { 5: input: string 6: pastedContents: Record<number, PastedContent> 7: onInputChange: (input: string) => void 8: setCursorOffset: (offset: number) => void 9: setPastedContents: (contents: Record<number, PastedContent>) => void 10: } 11: export function useMaybeTruncateInput({ 12: input, 13: pastedContents, 14: onInputChange, 15: setCursorOffset, 16: setPastedContents, 17: }: Props) { 18: const [hasAppliedTruncationToInput, setHasAppliedTruncationToInput] = 19: useState(false) 20: useEffect(() => { 21: if (hasAppliedTruncationToInput) { 22: return 23: } 24: if (input.length <= 10_000) { 25: return 26: } 27: const { newInput, newPastedContents } = maybeTruncateInput( 28: input, 29: pastedContents, 30: ) 31: onInputChange(newInput) 32: setCursorOffset(newInput.length) 33: setPastedContents(newPastedContents) 34: setHasAppliedTruncationToInput(true) 35: }, [ 36: input, 37: hasAppliedTruncationToInput, 38: pastedContents, 39: onInputChange, 40: setPastedContents, 41: setCursorOffset, 42: ]) 43: useEffect(() => { 44: if (input === '') { 45: setHasAppliedTruncationToInput(false) 46: } 47: }, [input]) 48: }

File: src/components/PromptInput/usePromptInputPlaceholder.ts

typescript 1: import { feature } from 'bun:bundle' 2: import { useMemo } from 'react' 3: import { useCommandQueue } from 'src/hooks/useCommandQueue.js' 4: import { useAppState } from 'src/state/AppState.js' 5: import { getGlobalConfig } from 'src/utils/config.js' 6: import { getExampleCommandFromCache } from 'src/utils/exampleCommands.js' 7: import { isQueuedCommandEditable } from 'src/utils/messageQueueManager.js' 8: const proactiveModule = 9: feature('PROACTIVE') || feature('KAIROS') 10: ? require('../../proactive/index.js') 11: : null 12: type Props = { 13: input: string 14: submitCount: number 15: viewingAgentName?: string 16: } 17: const NUM_TIMES_QUEUE_HINT_SHOWN = 3 18: const MAX_TEAMMATE_NAME_LENGTH = 20 19: export function usePromptInputPlaceholder({ 20: input, 21: submitCount, 22: viewingAgentName, 23: }: Props): string | undefined { 24: const queuedCommands = useCommandQueue() 25: const promptSuggestionEnabled = useAppState(s => s.promptSuggestionEnabled) 26: const placeholder = useMemo(() => { 27: if (input !== '') { 28: return 29: } 30: // Show teammate hint when viewing teammate 31: if (viewingAgentName) { 32: const displayName = 33: viewingAgentName.length > MAX_TEAMMATE_NAME_LENGTH 34: ? viewingAgentName.slice(0, MAX_TEAMMATE_NAME_LENGTH - 3) + '...' 35: : viewingAgentName 36: return `Message @${displayName}…` 37: } 38: // Show queue hint if user has not seen it yet. 39: // Only count user-editable commands — task-notification and isMeta 40: // are hidden from the prompt area (see PromptInputQueuedCommands). 41: if ( 42: queuedCommands.some(isQueuedCommandEditable) && 43: (getGlobalConfig().queuedCommandUpHintCount || 0) < 44: NUM_TIMES_QUEUE_HINT_SHOWN 45: ) { 46: return 'Press up to edit queued messages' 47: } 48: if ( 49: submitCount < 1 && 50: promptSuggestionEnabled && 51: !proactiveModule?.isProactiveActive() 52: ) { 53: return getExampleCommandFromCache() 54: } 55: }, [ 56: input, 57: queuedCommands, 58: submitCount, 59: promptSuggestionEnabled, 60: viewingAgentName, 61: ]) 62: return placeholder 63: }

File: src/components/PromptInput/useShowFastIconHint.ts

typescript 1: import { useEffect, useState } from 'react' 2: const HINT_DISPLAY_DURATION_MS = 5000 3: let hasShownThisSession = false 4: export function useShowFastIconHint(showFastIcon: boolean): boolean { 5: const [showHint, setShowHint] = useState(false) 6: useEffect(() => { 7: if (hasShownThisSession || !showFastIcon) { 8: return 9: } 10: hasShownThisSession = true 11: setShowHint(true) 12: const timer = setTimeout(setShowHint, HINT_DISPLAY_DURATION_MS, false) 13: return () => { 14: clearTimeout(timer) 15: setShowHint(false) 16: } 17: }, [showFastIcon]) 18: return showHint 19: }

File: src/components/PromptInput/useSwarmBanner.ts

typescript 1: import * as React from 'react' 2: import { useAppState, useAppStateStore } from '../../state/AppState.js' 3: import { 4: getActiveAgentForInput, 5: getViewedTeammateTask, 6: } from '../../state/selectors.js' 7: import { 8: AGENT_COLOR_TO_THEME_COLOR, 9: AGENT_COLORS, 10: type AgentColorName, 11: getAgentColor, 12: } from '../../tools/AgentTool/agentColorManager.js' 13: import { getStandaloneAgentName } from '../../utils/standaloneAgent.js' 14: import { isInsideTmux } from '../../utils/swarm/backends/detection.js' 15: import { 16: getCachedDetectionResult, 17: isInProcessEnabled, 18: } from '../../utils/swarm/backends/registry.js' 19: import { getSwarmSocketName } from '../../utils/swarm/constants.js' 20: import { 21: getAgentName, 22: getTeammateColor, 23: getTeamName, 24: isTeammate, 25: } from '../../utils/teammate.js' 26: import { isInProcessTeammate } from '../../utils/teammateContext.js' 27: import type { Theme } from '../../utils/theme.js' 28: type SwarmBannerInfo = { 29: text: string 30: bgColor: keyof Theme 31: } | null 32: export function useSwarmBanner(): SwarmBannerInfo { 33: const teamContext = useAppState(s => s.teamContext) 34: const standaloneAgentContext = useAppState(s => s.standaloneAgentContext) 35: const agent = useAppState(s => s.agent) 36: useAppState(s => s.viewingAgentTaskId) 37: const store = useAppStateStore() 38: const [insideTmux, setInsideTmux] = React.useState<boolean | null>(null) 39: React.useEffect(() => { 40: void isInsideTmux().then(setInsideTmux) 41: }, []) 42: const state = store.getState() 43: if (isTeammate() && !isInProcessTeammate()) { 44: const agentName = getAgentName() 45: if (agentName && getTeamName()) { 46: return { 47: text: `@${agentName}`, 48: bgColor: toThemeColor( 49: teamContext?.selfAgentColor ?? getTeammateColor(), 50: ), 51: } 52: } 53: } 54: const hasTeammates = 55: teamContext?.teamName && 56: teamContext.teammates && 57: Object.keys(teamContext.teammates).length > 0 58: if (hasTeammates) { 59: const viewedTeammate = getViewedTeammateTask(state) 60: const viewedColor = toThemeColor(viewedTeammate?.identity.color) 61: const inProcessMode = isInProcessEnabled() 62: const nativePanes = getCachedDetectionResult()?.isNative ?? false 63: if (insideTmux === false && !inProcessMode && !nativePanes) { 64: return { 65: text: `View teammates: \`tmux -L ${getSwarmSocketName()} a\``, 66: bgColor: viewedColor, 67: } 68: } 69: if ( 70: (insideTmux === true || inProcessMode || nativePanes) && 71: viewedTeammate 72: ) { 73: return { 74: text: `@${viewedTeammate.identity.agentName}`, 75: bgColor: viewedColor, 76: } 77: } 78: } 79: const active = getActiveAgentForInput(state) 80: if (active.type === 'named_agent') { 81: const task = active.task 82: let name: string | undefined 83: for (const [n, id] of state.agentNameRegistry) { 84: if (id === task.id) { 85: name = n 86: break 87: } 88: } 89: return { 90: text: name ? `@${name}` : task.description, 91: bgColor: getAgentColor(task.agentType) ?? 'cyan_FOR_SUBAGENTS_ONLY', 92: } 93: } 94: const standaloneName = getStandaloneAgentName(state) 95: const standaloneColor = standaloneAgentContext?.color 96: if (standaloneName || standaloneColor) { 97: return { 98: text: standaloneName ?? '', 99: bgColor: toThemeColor(standaloneColor), 100: } 101: } 102: // --agent CLI flag (when not handled above). 103: if (agent) { 104: const agentDef = state.agentDefinitions.activeAgents.find( 105: a => a.agentType === agent, 106: ) 107: return { 108: text: agent, 109: bgColor: toThemeColor(agentDef?.color, 'promptBorder'), 110: } 111: } 112: return null 113: } 114: function toThemeColor( 115: colorName: string | undefined, 116: fallback: keyof Theme = 'cyan_FOR_SUBAGENTS_ONLY', 117: ): keyof Theme { 118: return colorName && AGENT_COLORS.includes(colorName as AgentColorName) 119: ? AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName] 120: : fallback 121: }

File: src/components/PromptInput/utils.ts

typescript 1: import { 2: hasUsedBackslashReturn, 3: isShiftEnterKeyBindingInstalled, 4: } from '../../commands/terminalSetup/terminalSetup.js' 5: import type { Key } from '../../ink.js' 6: import { getGlobalConfig } from '../../utils/config.js' 7: import { env } from '../../utils/env.js' 8: export function isVimModeEnabled(): boolean { 9: const config = getGlobalConfig() 10: return config.editorMode === 'vim' 11: } 12: export function getNewlineInstructions(): string { 13: if (env.terminal === 'Apple_Terminal' && process.platform === 'darwin') { 14: return 'shift + ⏎ for newline' 15: } 16: if (isShiftEnterKeyBindingInstalled()) { 17: return 'shift + ⏎ for newline' 18: } 19: return hasUsedBackslashReturn() 20: ? '\\⏎ for newline' 21: : 'backslash (\\) + return (⏎) for newline' 22: } 23: export function isNonSpacePrintable(input: string, key: Key): boolean { 24: if ( 25: key.ctrl || 26: key.meta || 27: key.escape || 28: key.return || 29: key.tab || 30: key.backspace || 31: key.delete || 32: key.upArrow || 33: key.downArrow || 34: key.leftArrow || 35: key.rightArrow || 36: key.pageUp || 37: key.pageDown || 38: key.home || 39: key.end 40: ) { 41: return false 42: } 43: return input.length > 0 && !/^\s/.test(input) && !input.startsWith('\x1b') 44: }

File: src/components/PromptInput/VoiceIndicator.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import * as React from 'react'; 4: import { useSettings } from '../../hooks/useSettings.js'; 5: import { Box, Text, useAnimationFrame } from '../../ink.js'; 6: import { interpolateColor, toRGBColor } from '../Spinner/utils.js'; 7: type Props = { 8: voiceState: 'idle' | 'recording' | 'processing'; 9: }; 10: const PROCESSING_DIM = { 11: r: 153, 12: g: 153, 13: b: 153 14: }; 15: const PROCESSING_BRIGHT = { 16: r: 185, 17: g: 185, 18: b: 185 19: }; 20: const PULSE_PERIOD_S = 2; 21: export function VoiceIndicator(props) { 22: const $ = _c(2); 23: if (!feature("VOICE_MODE")) { 24: return null; 25: } 26: let t0; 27: if ($[0] !== props) { 28: t0 = <VoiceIndicatorImpl {...props} />; 29: $[0] = props; 30: $[1] = t0; 31: } else { 32: t0 = $[1]; 33: } 34: return t0; 35: } 36: function VoiceIndicatorImpl(t0) { 37: const $ = _c(2); 38: const { 39: voiceState 40: } = t0; 41: switch (voiceState) { 42: case "recording": 43: { 44: let t1; 45: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 46: t1 = <Text dimColor={true}>listening…</Text>; 47: $[0] = t1; 48: } else { 49: t1 = $[0]; 50: } 51: return t1; 52: } 53: case "processing": 54: { 55: let t1; 56: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 57: t1 = <ProcessingShimmer />; 58: $[1] = t1; 59: } else { 60: t1 = $[1]; 61: } 62: return t1; 63: } 64: case "idle": 65: { 66: return null; 67: } 68: } 69: } 70: export function VoiceWarmupHint() { 71: const $ = _c(1); 72: if (!feature("VOICE_MODE")) { 73: return null; 74: } 75: let t0; 76: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 77: t0 = <Text dimColor={true}>keep holding…</Text>; 78: $[0] = t0; 79: } else { 80: t0 = $[0]; 81: } 82: return t0; 83: } 84: function ProcessingShimmer() { 85: const $ = _c(8); 86: const settings = useSettings(); 87: const reducedMotion = settings.prefersReducedMotion ?? false; 88: const [ref, time] = useAnimationFrame(reducedMotion ? null : 50); 89: if (reducedMotion) { 90: let t0; 91: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 92: t0 = <Text color="warning">Voice: processing…</Text>; 93: $[0] = t0; 94: } else { 95: t0 = $[0]; 96: } 97: return t0; 98: } 99: const elapsedSec = time / 1000; 100: const opacity = (Math.sin(elapsedSec * Math.PI * 2 / PULSE_PERIOD_S) + 1) / 2; 101: let t0; 102: if ($[1] !== opacity) { 103: t0 = toRGBColor(interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity)); 104: $[1] = opacity; 105: $[2] = t0; 106: } else { 107: t0 = $[2]; 108: } 109: const color = t0; 110: let t1; 111: if ($[3] !== color) { 112: t1 = <Text color={color}>Voice: processing…</Text>; 113: $[3] = color; 114: $[4] = t1; 115: } else { 116: t1 = $[4]; 117: } 118: let t2; 119: if ($[5] !== ref || $[6] !== t1) { 120: t2 = <Box ref={ref}>{t1}</Box>; 121: $[5] = ref; 122: $[6] = t1; 123: $[7] = t2; 124: } else { 125: t2 = $[7]; 126: } 127: return t2; 128: }

File: src/components/sandbox/SandboxConfigTab.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { SandboxManager, shouldAllowManagedSandboxDomainsOnly } from '../../utils/sandbox/sandbox-adapter.js'; 5: export function SandboxConfigTab() { 6: const $ = _c(3); 7: const isEnabled = SandboxManager.isSandboxingEnabled(); 8: let t0; 9: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 10: const depCheck = SandboxManager.checkDependencies(); 11: t0 = depCheck.warnings.length > 0 ? <Box marginTop={1} flexDirection="column">{depCheck.warnings.map(_temp)}</Box> : null; 12: $[0] = t0; 13: } else { 14: t0 = $[0]; 15: } 16: const warningsNote = t0; 17: if (!isEnabled) { 18: let t1; 19: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 20: t1 = <Box flexDirection="column" paddingY={1}><Text color="subtle">Sandbox is not enabled</Text>{warningsNote}</Box>; 21: $[1] = t1; 22: } else { 23: t1 = $[1]; 24: } 25: return t1; 26: } 27: let t1; 28: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 29: const fsReadConfig = SandboxManager.getFsReadConfig(); 30: const fsWriteConfig = SandboxManager.getFsWriteConfig(); 31: const networkConfig = SandboxManager.getNetworkRestrictionConfig(); 32: const allowUnixSockets = SandboxManager.getAllowUnixSockets(); 33: const excludedCommands = SandboxManager.getExcludedCommands(); 34: const globPatternWarnings = SandboxManager.getLinuxGlobPatternWarnings(); 35: t1 = <Box flexDirection="column" paddingY={1}><Box flexDirection="column"><Text bold={true} color="permission">Excluded Commands:</Text><Text dimColor={true}>{excludedCommands.length > 0 ? excludedCommands.join(", ") : "None"}</Text></Box>{fsReadConfig.denyOnly.length > 0 && <Box marginTop={1} flexDirection="column"><Text bold={true} color="permission">Filesystem Read Restrictions:</Text><Text dimColor={true}>Denied: {fsReadConfig.denyOnly.join(", ")}</Text>{fsReadConfig.allowWithinDeny && fsReadConfig.allowWithinDeny.length > 0 && <Text dimColor={true}>Allowed within denied: {fsReadConfig.allowWithinDeny.join(", ")}</Text>}</Box>}{fsWriteConfig.allowOnly.length > 0 && <Box marginTop={1} flexDirection="column"><Text bold={true} color="permission">Filesystem Write Restrictions:</Text><Text dimColor={true}>Allowed: {fsWriteConfig.allowOnly.join(", ")}</Text>{fsWriteConfig.denyWithinAllow.length > 0 && <Text dimColor={true}>Denied within allowed: {fsWriteConfig.denyWithinAllow.join(", ")}</Text>}</Box>}{(networkConfig.allowedHosts && networkConfig.allowedHosts.length > 0 || networkConfig.deniedHosts && networkConfig.deniedHosts.length > 0) && <Box marginTop={1} flexDirection="column"><Text bold={true} color="permission">Network Restrictions{shouldAllowManagedSandboxDomainsOnly() ? " (Managed)" : ""}:</Text>{networkConfig.allowedHosts && networkConfig.allowedHosts.length > 0 && <Text dimColor={true}>Allowed: {networkConfig.allowedHosts.join(", ")}</Text>}{networkConfig.deniedHosts && networkConfig.deniedHosts.length > 0 && <Text dimColor={true}>Denied: {networkConfig.deniedHosts.join(", ")}</Text>}</Box>}{allowUnixSockets && allowUnixSockets.length > 0 && <Box marginTop={1} flexDirection="column"><Text bold={true} color="permission">Allowed Unix Sockets:</Text><Text dimColor={true}>{allowUnixSockets.join(", ")}</Text></Box>}{globPatternWarnings.length > 0 && <Box marginTop={1} flexDirection="column"><Text bold={true} color="warning">⚠ Warning: Glob patterns not fully supported on Linux</Text><Text dimColor={true}>The following patterns will be ignored:{" "}{globPatternWarnings.slice(0, 3).join(", ")}{globPatternWarnings.length > 3 && ` (${globPatternWarnings.length - 3} more)`}</Text></Box>}{warningsNote}</Box>; 36: $[2] = t1; 37: } else { 38: t1 = $[2]; 39: } 40: return t1; 41: } 42: function _temp(w, i) { 43: return <Text key={i} dimColor={true}>{w}</Text>; 44: }

File: src/components/sandbox/SandboxDependenciesTab.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { getPlatform } from '../../utils/platform.js'; 5: import type { SandboxDependencyCheck } from '../../utils/sandbox/sandbox-adapter.js'; 6: type Props = { 7: depCheck: SandboxDependencyCheck; 8: }; 9: export function SandboxDependenciesTab(t0) { 10: const $ = _c(24); 11: const { 12: depCheck 13: } = t0; 14: let t1; 15: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 16: t1 = getPlatform(); 17: $[0] = t1; 18: } else { 19: t1 = $[0]; 20: } 21: const platform = t1; 22: const isMac = platform === "macos"; 23: let t2; 24: if ($[1] !== depCheck.errors) { 25: t2 = depCheck.errors.some(_temp); 26: $[1] = depCheck.errors; 27: $[2] = t2; 28: } else { 29: t2 = $[2]; 30: } 31: const rgMissing = t2; 32: let t3; 33: if ($[3] !== depCheck.errors) { 34: t3 = depCheck.errors.some(_temp2); 35: $[3] = depCheck.errors; 36: $[4] = t3; 37: } else { 38: t3 = $[4]; 39: } 40: const bwrapMissing = t3; 41: let t4; 42: if ($[5] !== depCheck.errors) { 43: t4 = depCheck.errors.some(_temp3); 44: $[5] = depCheck.errors; 45: $[6] = t4; 46: } else { 47: t4 = $[6]; 48: } 49: const socatMissing = t4; 50: const seccompMissing = depCheck.warnings.length > 0; 51: let t5; 52: if ($[7] !== bwrapMissing || $[8] !== depCheck.errors || $[9] !== rgMissing || $[10] !== seccompMissing || $[11] !== socatMissing) { 53: const otherErrors = depCheck.errors.filter(_temp4); 54: const rgInstallHint = isMac ? "brew install ripgrep" : "apt install ripgrep"; 55: let t6; 56: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 57: t6 = isMac && <Box flexDirection="column"><Text>seatbelt: <Text color="success">built-in (macOS)</Text></Text></Box>; 58: $[13] = t6; 59: } else { 60: t6 = $[13]; 61: } 62: let t7; 63: let t8; 64: if ($[14] !== rgMissing) { 65: t7 = <Text>ripgrep (rg):{" "}{rgMissing ? <Text color="error">not found</Text> : <Text color="success">found</Text>}</Text>; 66: t8 = rgMissing && <Text dimColor={true}>{" "}· {rgInstallHint}</Text>; 67: $[14] = rgMissing; 68: $[15] = t7; 69: $[16] = t8; 70: } else { 71: t7 = $[15]; 72: t8 = $[16]; 73: } 74: let t9; 75: if ($[17] !== t7 || $[18] !== t8) { 76: t9 = <Box flexDirection="column">{t7}{t8}</Box>; 77: $[17] = t7; 78: $[18] = t8; 79: $[19] = t9; 80: } else { 81: t9 = $[19]; 82: } 83: let t10; 84: if ($[20] !== bwrapMissing || $[21] !== seccompMissing || $[22] !== socatMissing) { 85: t10 = !isMac && <><Box flexDirection="column"><Text>bubblewrap (bwrap):{" "}{bwrapMissing ? <Text color="error">not installed</Text> : <Text color="success">installed</Text>}</Text>{bwrapMissing && <Text dimColor={true}>{" "}· apt install bubblewrap</Text>}</Box><Box flexDirection="column"><Text>socat:{" "}{socatMissing ? <Text color="error">not installed</Text> : <Text color="success">installed</Text>}</Text>{socatMissing && <Text dimColor={true}>{" "}· apt install socat</Text>}</Box><Box flexDirection="column"><Text>seccomp filter:{" "}{seccompMissing ? <Text color="warning">not installed</Text> : <Text color="success">installed</Text>}{seccompMissing && <Text dimColor={true}> (required to block unix domain sockets)</Text>}</Text>{seccompMissing && <Box flexDirection="column"><Text dimColor={true}>{" "}· npm install -g @anthropic-ai/sandbox-runtime</Text><Text dimColor={true}>{" "}· or copy vendor/seccomp

File: src/components/sandbox/SandboxDoctorSection.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'; 5: export function SandboxDoctorSection() { 6: const $ = _c(2); 7: if (!SandboxManager.isSupportedPlatform()) { 8: return null; 9: } 10: if (!SandboxManager.isSandboxEnabledInSettings()) { 11: return null; 12: } 13: let t0; 14: let t1; 15: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 16: t1 = Symbol.for("react.early_return_sentinel"); 17: bb0: { 18: const depCheck = SandboxManager.checkDependencies(); 19: const hasErrors = depCheck.errors.length > 0; 20: const hasWarnings = depCheck.warnings.length > 0; 21: if (!hasErrors && !hasWarnings) { 22: t1 = null; 23: break bb0; 24: } 25: const statusColor = hasErrors ? "error" as const : "warning" as const; 26: const statusText = hasErrors ? "Missing dependencies" : "Available (with warnings)"; 27: t0 = <Box flexDirection="column"><Text bold={true}>Sandbox</Text><Text>└ Status: <Text color={statusColor}>{statusText}</Text></Text>{depCheck.errors.map(_temp)}{depCheck.warnings.map(_temp2)}{hasErrors && <Text dimColor={true}>└ Run /sandbox for install instructions</Text>}</Box>; 28: } 29: $[0] = t0; 30: $[1] = t1; 31: } else { 32: t0 = $[0]; 33: t1 = $[1]; 34: } 35: if (t1 !== Symbol.for("react.early_return_sentinel")) { 36: return t1; 37: } 38: return t0; 39: } 40: function _temp2(w, i_0) { 41: return <Text key={i_0} color="warning">└ {w}</Text>; 42: } 43: function _temp(e, i) { 44: return <Text key={i} color="error">└ {e}</Text>; 45: }

File: src/components/sandbox/SandboxOverridesTab.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, color, Link, Text, useTheme } from '../../ink.js'; 4: import type { CommandResultDisplay } from '../../types/command.js'; 5: import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'; 6: import { Select } from '../CustomSelect/select.js'; 7: import { useTabHeaderFocus } from '../design-system/Tabs.js'; 8: type Props = { 9: onComplete: (result?: string, options?: { 10: display?: CommandResultDisplay; 11: }) => void; 12: }; 13: type OverrideMode = 'open' | 'closed'; 14: export function SandboxOverridesTab(t0) { 15: const $ = _c(5); 16: const { 17: onComplete 18: } = t0; 19: const isEnabled = SandboxManager.isSandboxingEnabled(); 20: const isLocked = SandboxManager.areSandboxSettingsLockedByPolicy(); 21: const currentAllowUnsandboxed = SandboxManager.areUnsandboxedCommandsAllowed(); 22: if (!isEnabled) { 23: let t1; 24: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 25: t1 = <Box flexDirection="column" paddingY={1}><Text color="subtle">Sandbox is not enabled. Enable sandbox to configure override settings.</Text></Box>; 26: $[0] = t1; 27: } else { 28: t1 = $[0]; 29: } 30: return t1; 31: } 32: if (isLocked) { 33: let t1; 34: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 35: t1 = <Text color="subtle">Override settings are managed by a higher-priority configuration and cannot be changed locally.</Text>; 36: $[1] = t1; 37: } else { 38: t1 = $[1]; 39: } 40: let t2; 41: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 42: t2 = <Box flexDirection="column" paddingY={1}>{t1}<Box marginTop={1}><Text dimColor={true}>Current setting:{" "}{currentAllowUnsandboxed ? "Allow unsandboxed fallback" : "Strict sandbox mode"}</Text></Box></Box>; 43: $[2] = t2; 44: } else { 45: t2 = $[2]; 46: } 47: return t2; 48: } 49: let t1; 50: if ($[3] !== onComplete) { 51: t1 = <OverridesSelect onComplete={onComplete} currentMode={currentAllowUnsandboxed ? "open" : "closed"} />; 52: $[3] = onComplete; 53: $[4] = t1; 54: } else { 55: t1 = $[4]; 56: } 57: return t1; 58: } 59: function OverridesSelect(t0) { 60: const $ = _c(25); 61: const { 62: onComplete, 63: currentMode 64: } = t0; 65: const [theme] = useTheme(); 66: const { 67: headerFocused, 68: focusHeader 69: } = useTabHeaderFocus(); 70: let t1; 71: if ($[0] !== theme) { 72: t1 = color("success", theme)("(current)"); 73: $[0] = theme; 74: $[1] = t1; 75: } else { 76: t1 = $[1]; 77: } 78: const currentIndicator = t1; 79: const t2 = currentMode === "open" ? `Allow unsandboxed fallback ${currentIndicator}` : "Allow unsandboxed fallback"; 80: let t3; 81: if ($[2] !== t2) { 82: t3 = { 83: label: t2, 84: value: "open" 85: }; 86: $[2] = t2; 87: $[3] = t3; 88: } else { 89: t3 = $[3]; 90: } 91: const t4 = currentMode === "closed" ? `Strict sandbox mode ${currentIndicator}` : "Strict sandbox mode"; 92: let t5; 93: if ($[4] !== t4) { 94: t5 = { 95: label: t4, 96: value: "closed" 97: }; 98: $[4] = t4; 99: $[5] = t5; 100: } else { 101: t5 = $[5]; 102: } 103: let t6; 104: if ($[6] !== t3 || $[7] !== t5) { 105: t6 = [t3, t5]; 106: $[6] = t3; 107: $[7] = t5; 108: $[8] = t6; 109: } else { 110: t6 = $[8]; 111: } 112: const options = t6; 113: let t7; 114: if ($[9] !== onComplete) { 115: t7 = async function handleSelect(value) { 116: const mode = value as OverrideMode; 117: await SandboxManager.setSandboxSettings({ 118: allowUnsandboxedCommands: mode === "open" 119: }); 120: const message = mode === "open" ? "\u2713 Unsandboxed fallback allowed - commands can run outside sandbox when necessary" : "\u2713 Strict sandbox mode - all commands must run in sandbox or be excluded via the `excludedCommands` option"; 121: onComplete(message); 122: }; 123: $[9] = onComplete; 124: $[10] = t7; 125: } else { 126: t7 = $[10]; 127: } 128: const handleSelect = t7; 129: let t8; 130: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 131: t8 = <Box marginBottom={1}><Text bold={true}>Configure Overrides:</Text></Box>; 132: $[11] = t8; 133: } else { 134: t8 = $[11]; 135: } 136: let t9; 137: if ($[12] !== onComplete) { 138: t9 = () => onComplete(undefined, { 139: display: "skip" 140: }); 141: $[12] = onComplete; 142: $[13] = t9; 143: } else { 144: t9 = $[13]; 145: } 146: let t10; 147: if ($[14] !== focusHeader || $[15] !== handleSelect || $[16] !== headerFocused || $[17] !== options || $[18] !== t9) { 148: t10 = <Select options={options} onChange={handleSelect} onCancel={t9} onUpFromFirstItem={focusHeader} isDisabled={headerFocused} />; 149: $[14] = focusHeader; 150: $[15] = handleSelect; 151: $[16] = headerFocused; 152: $[17] = options; 153: $[18] = t9; 154: $[19] = t10; 155: } else { 156: t10 = $[19]; 157: } 158: let t11; 159: if ($[20] === Symbol.for("react.memo_cache_sentinel")) { 160: t11 = <Text dimColor={true}><Text bold={true} dimColor={true}>Allow unsandboxed fallback:</Text>{" "}When a command fails due to sandbox restrictions, Claude can retry with dangerouslyDisableSandbox to run outside the sandbox (falling back to default permissions).</Text>; 161: $[20] = t11; 162: } else { 163: t11 = $[20]; 164: } 165: let t12; 166: if ($[21] === Symbol.for("react.memo_cache_sentinel")) { 167: t12 = <Text dimColor={true}><Text bold={true} dimColor={true}>Strict sandbox mode:</Text>{" "}All bash commands invoked by the model must run in the sandbox unless they are explicitly listed in excludedCommands.</Text>; 168: $[21] = t12; 169: } else { 170: t12 = $[21]; 171: } 172: let t13; 173: if ($[22] === Symbol.for("react.memo_cache_sentinel")) { 174: t13 = <Box flexDirection="column" marginTop={1} gap={1}>{t11}{t12}<Text dimColor={true}>Learn more:{" "}<Link url="https://code.claude.com/docs/en/sandboxing#configure-sandboxing">code.claude.com/docs/en/sandboxing#configure-sandboxing</Link></Text></Box>; 175: $[22] = t13; 176: } else { 177: t13 = $[22]; 178: } 179: let t14; 180: if ($[23] !== t10) { 181: t14 = <Box flexDirection="column" paddingY={1}>{t8}{t10}{t13}</Box>; 182: $[23] = t10; 183: $[24] = t14; 184: } else { 185: t14 = $[24]; 186: } 187: return t14; 188: }

File: src/components/sandbox/SandboxSettings.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, color, Link, Text, useTheme } from '../../ink.js'; 4: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 5: import type { CommandResultDisplay } from '../../types/command.js'; 6: import type { SandboxDependencyCheck } from '../../utils/sandbox/sandbox-adapter.js'; 7: import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'; 8: import { getSettings_DEPRECATED } from '../../utils/settings/settings.js'; 9: import { Select } from '../CustomSelect/select.js'; 10: import { Pane } from '../design-system/Pane.js'; 11: import { Tab, Tabs, useTabHeaderFocus } from '../design-system/Tabs.js'; 12: import { SandboxConfigTab } from './SandboxConfigTab.js'; 13: import { SandboxDependenciesTab } from './SandboxDependenciesTab.js'; 14: import { SandboxOverridesTab } from './SandboxOverridesTab.js'; 15: type Props = { 16: onComplete: (result?: string, options?: { 17: display?: CommandResultDisplay; 18: }) => void; 19: depCheck: SandboxDependencyCheck; 20: }; 21: type SandboxMode = 'auto-allow' | 'regular' | 'disabled'; 22: export function SandboxSettings(t0) { 23: const $ = _c(34); 24: const { 25: onComplete, 26: depCheck 27: } = t0; 28: const [theme] = useTheme(); 29: const currentEnabled = SandboxManager.isSandboxingEnabled(); 30: const currentAutoAllow = SandboxManager.isAutoAllowBashIfSandboxedEnabled(); 31: const hasWarnings = depCheck.warnings.length > 0; 32: let t1; 33: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 34: t1 = getSettings_DEPRECATED(); 35: $[0] = t1; 36: } else { 37: t1 = $[0]; 38: } 39: const settings = t1; 40: const allowAllUnixSockets = settings.sandbox?.network?.allowAllUnixSockets; 41: const showSocketWarning = hasWarnings && !allowAllUnixSockets; 42: const getCurrentMode = () => { 43: if (!currentEnabled) { 44: return "disabled"; 45: } 46: if (currentAutoAllow) { 47: return "auto-allow"; 48: } 49: return "regular"; 50: }; 51: const currentMode = getCurrentMode(); 52: let t2; 53: if ($[1] !== theme) { 54: t2 = color("success", theme)("(current)"); 55: $[1] = theme; 56: $[2] = t2; 57: } else { 58: t2 = $[2]; 59: } 60: const currentIndicator = t2; 61: const t3 = currentMode === "auto-allow" ? `Sandbox BashTool, with auto-allow ${currentIndicator}` : "Sandbox BashTool, with auto-allow"; 62: let t4; 63: if ($[3] !== t3) { 64: t4 = { 65: label: t3, 66: value: "auto-allow" 67: }; 68: $[3] = t3; 69: $[4] = t4; 70: } else { 71: t4 = $[4]; 72: } 73: const t5 = currentMode === "regular" ? `Sandbox BashTool, with regular permissions ${currentIndicator}` : "Sandbox BashTool, with regular permissions"; 74: let t6; 75: if ($[5] !== t5) { 76: t6 = { 77: label: t5, 78: value: "regular" 79: }; 80: $[5] = t5; 81: $[6] = t6; 82: } else { 83: t6 = $[6]; 84: } 85: const t7 = currentMode === "disabled" ? `No Sandbox ${currentIndicator}` : "No Sandbox"; 86: let t8; 87: if ($[7] !== t7) { 88: t8 = { 89: label: t7, 90: value: "disabled" 91: }; 92: $[7] = t7; 93: $[8] = t8; 94: } else { 95: t8 = $[8]; 96: } 97: let t9; 98: if ($[9] !== t4 || $[10] !== t6 || $[11] !== t8) { 99: t9 = [t4, t6, t8]; 100: $[9] = t4; 101: $[10] = t6; 102: $[11] = t8; 103: $[12] = t9; 104: } else { 105: t9 = $[12]; 106: } 107: const options = t9; 108: let t10; 109: if ($[13] !== onComplete) { 110: t10 = async function handleSelect(value) { 111: const mode = value as SandboxMode; 112: bb33: switch (mode) { 113: case "auto-allow": 114: { 115: await SandboxManager.setSandboxSettings({ 116: enabled: true, 117: autoAllowBashIfSandboxed: true 118: }); 119: onComplete("\u2713 Sandbox enabled with auto-allow for bash commands"); 120: break bb33; 121: } 122: case "regular": 123: { 124: await SandboxManager.setSandboxSettings({ 125: enabled: true, 126: autoAllowBashIfSandboxed: false 127: }); 128: onComplete("\u2713 Sandbox enabled with regular bash permissions"); 129: break bb33; 130: } 131: case "disabled": 132: { 133: await SandboxManager.setSandboxSettings({ 134: enabled: false, 135: autoAllowBashIfSandboxed: false 136: }); 137: onComplete("\u25CB Sandbox disabled"); 138: } 139: } 140: }; 141: $[13] = onComplete; 142: $[14] = t10; 143: } else { 144: t10 = $[14]; 145: } 146: const handleSelect = t10; 147: let t11; 148: if ($[15] !== onComplete) { 149: t11 = { 150: "confirm:no": () => onComplete(undefined, { 151: display: "skip" 152: }) 153: }; 154: $[15] = onComplete; 155: $[16] = t11; 156: } else { 157: t11 = $[16]; 158: } 159: let t12; 160: if ($[17] === Symbol.for("react.memo_cache_sentinel")) { 161: t12 = { 162: context: "Settings" 163: }; 164: $[17] = t12; 165: } else { 166: t12 = $[17]; 167: } 168: useKeybindings(t11, t12); 169: let t13; 170: if ($[18] !== handleSelect || $[19] !== onComplete || $[20] !== options || $[21] !== showSocketWarning) { 171: t13 = <Tab key="mode" title="Mode"><SandboxModeTab showSocketWarning={showSocketWarning} options={options} onSelect={handleSelect} onComplete={onComplete} /></Tab>; 172: $[18] = handleSelect; 173: $[19] = onComplete; 174: $[20] = options; 175: $[21] = showSocketWarning; 176: $[22] = t13; 177: } else { 178: t13 = $[22]; 179: } 180: const modeTab = t13; 181: let t14; 182: if ($[23] !== onComplete) { 183: t14 = <Tab key="overrides" title="Overrides"><SandboxOverridesTab onComplete={onComplete} /></Tab>; 184: $[23] = onComplete; 185: $[24] = t14; 186: } else { 187: t14 = $[24]; 188: } 189: const overridesTab = t14; 190: let t15; 191: if ($[25] === Symbol.for("react.memo_cache_sentinel")) { 192: t15 = <Tab key="config" title="Config"><SandboxConfigTab /></Tab>; 193: $[25] = t15; 194: } else { 195: t15 = $[25]; 196: } 197: const configTab = t15; 198: const hasErrors = depCheck.errors.length > 0; 199: let t16; 200: if ($[26] !== depCheck || $[27] !== hasErrors || $[28] !== hasWarnings || $[29] !== modeTab || $[30] !== overridesTab) { 201: t16 = hasErrors ? [<Tab key="dependencies" title="Dependencies"><SandboxDependenciesTab depCheck={depCheck} /></Tab>] : [modeTab, ...(hasWarnings ? [<Tab key="dependencies" title="Dependencies"><SandboxDependenciesTab depCheck={depCheck} /></Tab>] : []), overridesTab, configTab]; 202: $[26] = depCheck; 203: $[27] = hasErrors; 204: $[28] = hasWarnings; 205: $[29] = modeTab; 206: $[30] = overridesTab; 207: $[31] = t16; 208: } else { 209: t16 = $[31]; 210: } 211: const tabs = t16; 212: let t17; 213: if ($[32] !== tabs) { 214: t17 = <Pane color="permission"><Tabs title="Sandbox:" color="permission" defaultTab="Mode">{tabs}</Tabs></Pane>; 215: $[32] = tabs; 216: $[33] = t17; 217: } else { 218: t17 = $[33]; 219: } 220: return t17; 221: } 222: function SandboxModeTab(t0) { 223: const $ = _c(16); 224: const { 225: showSocketWarning, 226: options, 227: onSelect, 228: onComplete 229: } = t0; 230: const { 231: headerFocused, 232: focusHeader 233: } = useTabHeaderFocus(); 234: let t1; 235: if ($[0] !== showSocketWarning) { 236: t1 = showSocketWarning && <Box marginBottom={1}><Text color="warning">Cannot block unix domain sockets (see Dependencies tab)</Text></Box>; 237: $[0] = showSocketWarning; 238: $[1] = t1; 239: } else { 240: t1 = $[1]; 241: } 242: let t2; 243: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 244: t2 = <Box marginBottom={1}><Text bold={true}>Configure Mode:</Text></Box>; 245: $[2] = t2; 246: } else { 247: t2 = $[2]; 248: } 249: let t3; 250: if ($[3] !== onComplete) { 251: t3 = () => onComplete(undefined, { 252: display: "skip" 253: }); 254: $[3] = onComplete; 255: $[4] = t3; 256: } else { 257: t3 = $[4]; 258: } 259: let t4; 260: if ($[5] !== focusHeader || $[6] !== headerFocused || $[7] !== onSelect || $[8] !== options || $[9] !== t3) { 261: t4 = <Select options={options} onChange={onSelect} onCancel={t3} onUpFromFirstItem={focusHeader} isDisabled={headerFocused} />; 262: $[5] = focusHeader; 263: $[6] = headerFocused; 264: $[7] = onSelect; 265: $[8] = options; 266: $[9] = t3; 267: $[10] = t4; 268: } else { 269: t4 = $[10]; 270: } 271: let t5; 272: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 273: t5 = <Text dimColor={true}><Text bold={true} dimColor={true}>Auto-allow mode:</Text>{" "}Commands will try to run in the sandbox automatically, and attempts to run outside of the sandbox fallback to regular permissions. Explicit ask/deny rules are always respected.</Text>; 274: $[11] = t5; 275: } else { 276: t5 = $[11]; 277: } 278: let t6; 279: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 280: t6 = <Box flexDirection="column" marginTop={1} gap={1}>{t5}<Text dimColor={true}>Learn more:{" "}<Link url="https://code.claude.com/docs/en/sandboxing">code.claude.com/docs/en/sandboxing</Link></Text></Box>; 281: $[12] = t6; 282: } else { 283: t6 = $[12]; 284: } 285: let t7; 286: if ($[13] !== t1 || $[14] !== t4) { 287: t7 = <Box flexDirection="column" paddingY={1}>{t1}{t2}{t4}{t6}</Box>; 288: $[13] = t1; 289: $[14] = t4; 290: $[15] = t7; 291: } else { 292: t7 = $[15]; 293: } 294: return t7; 295: }

File: src/components/Settings/Config.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import { Box, Text, useTheme, useThemeSetting, useTerminalFocus } from '../../ink.js'; 4: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 5: import * as React from 'react'; 6: import { useState, useCallback } from 'react'; 7: import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js'; 8: import figures from 'figures'; 9: import { type GlobalConfig, saveGlobalConfig, getCurrentProjectConfig, type OutputStyle } from '../../utils/config.js'; 10: import { normalizeApiKeyForConfig } from '../../utils/authPortable.js'; 11: import { getGlobalConfig, getAutoUpdaterDisabledReason, formatAutoUpdaterDisabledReason, getRemoteControlAtStartup } from '../../utils/config.js'; 12: import chalk from 'chalk'; 13: import { permissionModeTitle, permissionModeFromString, toExternalPermissionMode, isExternalPermissionMode, EXTERNAL_PERMISSION_MODES, PERMISSION_MODES, type ExternalPermissionMode, type PermissionMode } from '../../utils/permissions/PermissionMode.js'; 14: import { getAutoModeEnabledState, hasAutoModeOptInAnySource, transitionPlanAutoMode } from '../../utils/permissions/permissionSetup.js'; 15: import { logError } from '../../utils/log.js'; 16: import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/index.js'; 17: import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'; 18: import { ThemePicker } from '../ThemePicker.js'; 19: import { useAppState, useSetAppState, useAppStateStore } from '../../state/AppState.js'; 20: import { ModelPicker } from '../ModelPicker.js'; 21: import { modelDisplayString, isOpus1mMergeEnabled } from '../../utils/model/model.js'; 22: import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'; 23: import { ClaudeMdExternalIncludesDialog } from '../ClaudeMdExternalIncludesDialog.js'; 24: import { ChannelDowngradeDialog, type ChannelDowngradeChoice } from '../ChannelDowngradeDialog.js'; 25: import { Dialog } from '../design-system/Dialog.js'; 26: import { Select } from '../CustomSelect/index.js'; 27: import { OutputStylePicker } from '../OutputStylePicker.js'; 28: import { LanguagePicker } from '../LanguagePicker.js'; 29: import { getExternalClaudeMdIncludes, getMemoryFiles, hasExternalClaudeMdIncludes } from 'src/utils/claudemd.js'; 30: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 31: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 32: import { Byline } from '../design-system/Byline.js'; 33: import { useTabHeaderFocus } from '../design-system/Tabs.js'; 34: import { useIsInsideModal } from '../../context/modalContext.js'; 35: import { SearchBox } from '../SearchBox.js'; 36: import { isSupportedTerminal, hasAccessToIDEExtensionDiffFeature } from '../../utils/ide.js'; 37: import { getInitialSettings, getSettingsForSource, updateSettingsForSource } from '../../utils/settings/settings.js'; 38: import { getUserMsgOptIn, setUserMsgOptIn } from '../../bootstrap/state.js'; 39: import { DEFAULT_OUTPUT_STYLE_NAME } from 'src/constants/outputStyles.js'; 40: import { isEnvTruthy, isRunningOnHomespace } from 'src/utils/envUtils.js'; 41: import type { LocalJSXCommandContext, CommandResultDisplay } from '../../commands.js'; 42: import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; 43: import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'; 44: import { getCliTeammateModeOverride, clearCliTeammateModeOverride } from '../../utils/swarm/backends/teammateModeSnapshot.js'; 45: import { getHardcodedTeammateModelFallback } from '../../utils/swarm/teammateModel.js'; 46: import { useSearchInput } from '../../hooks/useSearchInput.js'; 47: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 48: import { clearFastModeCooldown, FAST_MODE_MODEL_DISPLAY, isFastModeAvailable, isFastModeEnabled, getFastModeModel, isFastModeSupportedByModel } from '../../utils/fastMode.js'; 49: import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; 50: type Props = { 51: onClose: (result?: string, options?: { 52: display?: CommandResultDisplay; 53: }) => void; 54: context: LocalJSXCommandContext; 55: setTabsHidden: (hidden: boolean) => void; 56: onIsSearchModeChange?: (inSearchMode: boolean) => void; 57: contentHeight?: number; 58: }; 59: type SettingBase = { 60: id: string; 61: label: string; 62: } | { 63: id: string; 64: label: React.ReactNode; 65: searchText: string; 66: }; 67: type Setting = (SettingBase & { 68: value: boolean; 69: onChange(value: boolean): void; 70: type: 'boolean'; 71: }) | (SettingBase & { 72: value: string; 73: options: string[]; 74: onChange(value: string): void; 75: type: 'enum'; 76: }) | (SettingBase & { 77: value: string; 78: onChange(value: string): void; 79: type: 'managedEnum'; 80: }); 81: type SubMenu = 'Theme' | 'Model' | 'TeammateModel' | 'ExternalIncludes' | 'OutputStyle' | 'ChannelDowngrade' | 'Language' | 'EnableAutoUpdates'; 82: export function Config({ 83: onClose, 84: context, 85: setTabsHidden, 86: onIsSearchModeChange, 87: contentHeight 88: }: Props): React.ReactNode { 89: const { 90: headerFocused, 91: focusHeader 92: } = useTabHeaderFocus(); 93: const insideModal = useIsInsideModal(); 94: const [, setTheme] = useTheme(); 95: const themeSetting = useThemeSetting(); 96: const [globalConfig, setGlobalConfig] = useState(getGlobalConfig()); 97: const initialConfig = React.useRef(getGlobalConfig()); 98: const [settingsData, setSettingsData] = useState(getInitialSettings()); 99: const initialSettingsData = React.useRef(getInitialSettings()); 100: const [currentOutputStyle, setCurrentOutputStyle] = useState<OutputStyle>(settingsData?.outputStyle || DEFAULT_OUTPUT_STYLE_NAME); 101: const initialOutputStyle = React.useRef(currentOutputStyle); 102: const [currentLanguage, setCurrentLanguage] = useState<string | undefined>(settingsData?.language); 103: const initialLanguage = React.useRef(currentLanguage); 104: const [selectedIndex, setSelectedIndex] = useState(0); 105: const [scrollOffset, setScrollOffset] = useState(0); 106: const [isSearchMode, setIsSearchMode] = useState(true); 107: const isTerminalFocused = useTerminalFocus(); 108: const { 109: rows 110: } = useTerminalSize(); 111: const paneCap = contentHeight ?? Math.min(Math.floor(rows * 0.8), 30); 112: const maxVisible = Math.max(5, paneCap - 10); 113: const mainLoopModel = useAppState(s => s.mainLoopModel); 114: const verbose = useAppState(s_0 => s_0.verbose); 115: const thinkingEnabled = useAppState(s_1 => s_1.thinkingEnabled); 116: const isFastMode = useAppState(s_2 => isFastModeEnabled() ? s_2.fastMode : false); 117: const promptSuggestionEnabled = useAppState(s_3 => s_3.promptSuggestionEnabled); 118: const showAutoInDefaultModePicker = feature('TRANSCRIPT_CLASSIFIER') ? hasAutoModeOptInAnySource() || getAutoModeEnabledState() === 'enabled' : false; 119: const showDefaultViewPicker = feature('KAIROS') || feature('KAIROS_BRIEF') ? (require('../../tools/BriefTool/BriefTool.js') as typeof import('../../tools/BriefTool/BriefTool.js')).isBriefEntitled() : false; 120: const setAppState = useSetAppState(); 121: const [changes, setChanges] = useState<{ 122: [key: string]: unknown; 123: }>({}); 124: const initialThinkingEnabled = React.useRef(thinkingEnabled); 125: const [initialLocalSettings] = useState(() => getSettingsForSource('localSettings')); 126: const [initialUserSettings] = useState(() => getSettingsForSource('userSettings')); 127: const initialThemeSetting = React.useRef(themeSetting); 128: const store = useAppStateStore(); 129: const [initialAppState] = useState(() => { 130: const s_4 = store.getState(); 131: return { 132: mainLoopModel: s_4.mainLoopModel, 133: mainLoopModelForSession: s_4.mainLoopModelForSession, 134: verbose: s_4.verbose, 135: thinkingEnabled: s_4.thinkingEnabled, 136: fastMode: s_4.fastMode, 137: promptSuggestionEnabled: s_4.promptSuggestionEnabled, 138: isBriefOnly: s_4.isBriefOnly, 139: replBridgeEnabled: s_4.replBridgeEnabled, 140: replBridgeOutboundOnly: s_4.replBridgeOutboundOnly, 141: settings: s_4.settings 142: }; 143: }); 144: const [initialUserMsgOptIn] = useState(() => getUserMsgOptIn()); 145: const isDirty = React.useRef(false); 146: const [showThinkingWarning, setShowThinkingWarning] = useState(false); 147: const [showSubmenu, setShowSubmenu] = useState<SubMenu | null>(null); 148: const { 149: query: searchQuery, 150: setQuery: setSearchQuery, 151: cursorOffset: searchCursorOffset 152: } = useSearchInput({ 153: isActive: isSearchMode && showSubmenu === null && !headerFocused, 154: onExit: () => setIsSearchMode(false), 155: onExitUp: focusHeader, 156: passthroughCtrlKeys: ['c', 'd'] 157: }); 158: const ownsEsc = isSearchMode && !headerFocused; 159: React.useEffect(() => { 160: onIsSearchModeChange?.(ownsEsc); 161: }, [ownsEsc, onIsSearchModeChange]); 162: const isConnectedToIde = hasAccessToIDEExtensionDiffFeature(context.options.mcpClients); 163: const isFileCheckpointingAvailable = !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING); 164: const memoryFiles = React.use(getMemoryFiles(true)); 165: const shouldShowExternalIncludesToggle = hasExternalClaudeMdIncludes(memoryFiles); 166: const autoUpdaterDisabledReason = getAutoUpdaterDisabledReason(); 167: function onChangeMainModelConfig(value: string | null): void { 168: const previousModel = mainLoopModel; 169: logEvent('tengu_config_model_changed', { 170: from_model: previousModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 171: to_model: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 172: }); 173: setAppState(prev => ({ 174: ...prev, 175: mainLoopModel: value, 176: mainLoopModelForSession: null 177: })); 178: setChanges(prev_0 => { 179: const valStr = modelDisplayString(value) + (isBilledAsExtraUsage(value, false, isOpus1mMergeEnabled()) ? ' · Billed as extra usage' : ''); 180: if ('model' in prev_0) { 181: const { 182: model, 183: ...rest 184: } = prev_0; 185: return { 186: ...rest, 187: model: valStr 188: }; 189: } 190: return { 191: ...prev_0, 192: model: valStr 193: }; 194: }); 195: } 196: function onChangeVerbose(value_0: boolean): void { 197: saveGlobalConfig(current => ({ 198: ...current, 199: verbose: value_0 200: })); 201: setGlobalConfig({ 202: ...getGlobalConfig(), 203: verbose: value_0 204: }); 205: setAppState(prev_1 => ({ 206: ...prev_1, 207: verbose: value_0 208: })); 209: setChanges(prev_2 => { 210: if ('verbose' in prev_2) { 211: const { 212: verbose: verbose_0, 213: ...rest_0 214: } = prev_2; 215: return rest_0; 216: } 217: return { 218: ...prev_2, 219: verbose: value_0 220: }; 221: }); 222: } 223: const settingsItems: Setting[] = [ 224: { 225: id: 'autoCompactEnabled', 226: label: 'Auto-compact', 227: value: globalConfig.autoCompactEnabled, 228: type: 'boolean' as const, 229: onChange(autoCompactEnabled: boolean) { 230: saveGlobalConfig(current_0 => ({ 231: ...current_0, 232: autoCompactEnabled 233: })); 234: setGlobalConfig({ 235: ...getGlobalConfig(), 236: autoCompactEnabled 237: }); 238: logEvent('tengu_auto_compact_setting_changed', { 239: enabled: autoCompactEnabled 240: }); 241: } 242: }, { 243: id: 'spinnerTipsEnabled', 244: label: 'Show tips', 245: value: settingsData?.spinnerTipsEnabled ?? true, 246: type: 'boolean' as const, 247: onChange(spinnerTipsEnabled: boolean) { 248: updateSettingsForSource('localSettings', { 249: spinnerTipsEnabled 250: }); 251: setSettingsData(prev_3 => ({ 252: ...prev_3, 253: spinnerTipsEnabled 254: })); 255: logEvent('tengu_tips_setting_changed', { 256: enabled: spinnerTipsEnabled 257: }); 258: } 259: }, { 260: id: 'prefersReducedMotion', 261: label: 'Reduce motion', 262: value: settingsData?.prefersReducedMotion ?? false, 263: type: 'boolean' as const, 264: onChange(prefersReducedMotion: boolean) { 265: updateSettingsForSource('localSettings', { 266: prefersReducedMotion 267: }); 268: setSettingsData(prev_4 => ({ 269: ...prev_4, 270: prefersReducedMotion 271: })); 272: setAppState(prev_5 => ({ 273: ...prev_5, 274: settings: { 275: ...prev_5.settings, 276: prefersReducedMotion 277: } 278: })); 279: logEvent('tengu_reduce_motion_setting_changed', { 280: enabled: prefersReducedMotion 281: }); 282: } 283: }, { 284: id: 'thinkingEnabled', 285: label: 'Thinking mode', 286: value: thinkingEnabled ?? true, 287: type: 'boolean' as const, 288: onChange(enabled: boolean) { 289: setAppState(prev_6 => ({ 290: ...prev_6, 291: thinkingEnabled: enabled 292: })); 293: updateSettingsForSource('userSettings', { 294: alwaysThinkingEnabled: enabled ? undefined : false 295: }); 296: logEvent('tengu_thinking_toggled', { 297: enabled 298: }); 299: } 300: }, 301: ...(isFastModeEnabled() && isFastModeAvailable() ? [{ 302: id: 'fastMode', 303: label: `Fast mode (${FAST_MODE_MODEL_DISPLAY} only)`, 304: value: !!isFastMode, 305: type: 'boolean' as const, 306: onChange(enabled_0: boolean) { 307: clearFastModeCooldown(); 308: updateSettingsForSource('userSettings', { 309: fastMode: enabled_0 ? true : undefined 310: }); 311: if (enabled_0) { 312: setAppState(prev_7 => ({ 313: ...prev_7, 314: mainLoopModel: getFastModeModel(), 315: mainLoopModelForSession: null, 316: fastMode: true 317: })); 318: setChanges(prev_8 => ({ 319: ...prev_8, 320: model: getFastModeModel(), 321: 'Fast mode': 'ON' 322: })); 323: } else { 324: setAppState(prev_9 => ({ 325: ...prev_9, 326: fastMode: false 327: })); 328: setChanges(prev_10 => ({ 329: ...prev_10, 330: 'Fast mode': 'OFF' 331: })); 332: } 333: } 334: }] : []), ...(getFeatureValue_CACHED_MAY_BE_STALE('tengu_chomp_inflection', false) ? [{ 335: id: 'promptSuggestionEnabled', 336: label: 'Prompt suggestions', 337: value: promptSuggestionEnabled, 338: type: 'boolean' as const, 339: onChange(enabled_1: boolean) { 340: setAppState(prev_11 => ({ 341: ...prev_11, 342: promptSuggestionEnabled: enabled_1 343: })); 344: updateSettingsForSource('userSettings', { 345: promptSuggestionEnabled: enabled_1 ? undefined : false 346: }); 347: } 348: }] : []), 349: ...("external" === 'ant' ? [{ 350: id: 'speculationEnabled', 351: label: 'Speculative execution', 352: value: globalConfig.speculationEnabled ?? true, 353: type: 'boolean' as const, 354: onChange(enabled_2: boolean) { 355: saveGlobalConfig(current_1 => { 356: if (current_1.speculationEnabled === enabled_2) return current_1; 357: return { 358: ...current_1, 359: speculationEnabled: enabled_2 360: }; 361: }); 362: setGlobalConfig({ 363: ...getGlobalConfig(), 364: speculationEnabled: enabled_2 365: }); 366: logEvent('tengu_speculation_setting_changed', { 367: enabled: enabled_2 368: }); 369: } 370: }] : []), ...(isFileCheckpointingAvailable ? [{ 371: id: 'fileCheckpointingEnabled', 372: label: 'Rewind code (checkpoints)', 373: value: globalConfig.fileCheckpointingEnabled, 374: type: 'boolean' as const, 375: onChange(enabled_3: boolean) { 376: saveGlobalConfig(current_2 => ({ 377: ...current_2, 378: fileCheckpointingEnabled: enabled_3 379: })); 380: setGlobalConfig({ 381: ...getGlobalConfig(), 382: fileCheckpointingEnabled: enabled_3 383: }); 384: logEvent('tengu_file_history_snapshots_setting_changed', { 385: enabled: enabled_3 386: }); 387: } 388: }] : []), { 389: id: 'verbose', 390: label: 'Verbose output', 391: value: verbose, 392: type: 'boolean', 393: onChange: onChangeVerbose 394: }, { 395: id: 'terminalProgressBarEnabled', 396: label: 'Terminal progress bar', 397: value: globalConfig.terminalProgressBarEnabled, 398: type: 'boolean' as const, 399: onChange(terminalProgressBarEnabled: boolean) { 400: saveGlobalConfig(current_3 => ({ 401: ...current_3, 402: terminalProgressBarEnabled 403: })); 404: setGlobalConfig({ 405: ...getGlobalConfig(), 406: terminalProgressBarEnabled 407: }); 408: logEvent('tengu_terminal_progress_bar_setting_changed', { 409: enabled: terminalProgressBarEnabled 410: }); 411: } 412: }, ...(getFeatureValue_CACHED_MAY_BE_STALE('tengu_terminal_sidebar', false) ? [{ 413: id: 'showStatusInTerminalTab', 414: label: 'Show status in terminal tab', 415: value: globalConfig.showStatusInTerminalTab ?? false, 416: type: 'boolean' as const, 417: onChange(showStatusInTerminalTab: boolean) { 418: saveGlobalConfig(current_4 => ({ 419: ...current_4, 420: showStatusInTerminalTab 421: })); 422: setGlobalConfig({ 423: ...getGlobalConfig(), 424: showStatusInTerminalTab 425: }); 426: logEvent('tengu_terminal_tab_status_setting_changed', { 427: enabled: showStatusInTerminalTab 428: }); 429: } 430: }] : []), { 431: id: 'showTurnDuration', 432: label: 'Show turn duration', 433: value: globalConfig.showTurnDuration, 434: type: 'boolean' as const, 435: onChange(showTurnDuration: boolean) { 436: saveGlobalConfig(current_5 => ({ 437: ...current_5, 438: showTurnDuration 439: })); 440: setGlobalConfig({ 441: ...getGlobalConfig(), 442: showTurnDuration 443: }); 444: logEvent('tengu_show_turn_duration_setting_changed', { 445: enabled: showTurnDuration 446: }); 447: } 448: }, { 449: id: 'defaultPermissionMode', 450: label: 'Default permission mode', 451: value: settingsData?.permissions?.defaultMode || 'default', 452: options: (() => { 453: const priorityOrder: PermissionMode[] = ['default', 'plan']; 454: const allModes: readonly PermissionMode[] = feature('TRANSCRIPT_CLASSIFIER') ? PERMISSION_MODES : EXTERNAL_PERMISSION_MODES; 455: const excluded: PermissionMode[] = ['bypassPermissions']; 456: if (feature('TRANSCRIPT_CLASSIFIER') && !showAutoInDefaultModePicker) { 457: excluded.push('auto'); 458: } 459: return [...priorityOrder, ...allModes.filter(m => !priorityOrder.includes(m) && !excluded.includes(m))]; 460: })(), 461: type: 'enum' as const, 462: onChange(mode: string) { 463: const parsedMode = permissionModeFromString(mode); 464: const validatedMode = isExternalPermissionMode(parsedMode) ? toExternalPermissionMode(parsedMode) : parsedMode; 465: const result = updateSettingsForSource('userSettings', { 466: permissions: { 467: ...settingsData?.permissions, 468: defaultMode: validatedMode as ExternalPermissionMode 469: } 470: }); 471: if (result.error) { 472: logError(result.error); 473: return; 474: } 475: setSettingsData(prev_12 => ({ 476: ...prev_12, 477: permissions: { 478: ...prev_12?.permissions, 479: defaultMode: validatedMode as (typeof PERMISSION_MODES)[number] 480: } 481: })); 482: setChanges(prev_13 => ({ 483: ...prev_13, 484: defaultPermissionMode: mode 485: })); 486: logEvent('tengu_config_changed', { 487: setting: 'defaultPermissionMode' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 488: value: mode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 489: }); 490: } 491: }, ...(feature('TRANSCRIPT_CLASSIFIER') && showAutoInDefaultModePicker ? [{ 492: id: 'useAutoModeDuringPlan', 493: label: 'Use auto mode during plan', 494: value: (settingsData as { 495: useAutoModeDuringPlan?: boolean; 496: } | undefined)?.useAutoModeDuringPlan ?? true, 497: type: 'boolean' as const, 498: onChange(useAutoModeDuringPlan: boolean) { 499: updateSettingsForSource('userSettings', { 500: useAutoModeDuringPlan 501: }); 502: setSettingsData(prev_14 => ({ 503: ...prev_14, 504: useAutoModeDuringPlan 505: })); 506: setAppState(prev_15 => { 507: const next = transitionPlanAutoMode(prev_15.toolPermissionContext); 508: if (next === prev_15.toolPermissionContext) return prev_15; 509: return { 510: ...prev_15, 511: toolPermissionContext: next 512: }; 513: }); 514: setChanges(prev_16 => ({ 515: ...prev_16, 516: 'Use auto mode during plan': useAutoModeDuringPlan 517: })); 518: } 519: }] : []), { 520: id: 'respectGitignore', 521: label: 'Respect .gitignore in file picker', 522: value: globalConfig.respectGitignore, 523: type: 'boolean' as const, 524: onChange(respectGitignore: boolean) { 525: saveGlobalConfig(current_6 => ({ 526: ...current_6, 527: respectGitignore 528: })); 529: setGlobalConfig({ 530: ...getGlobalConfig(), 531: respectGitignore 532: }); 533: logEvent('tengu_respect_gitignore_setting_changed', { 534: enabled: respectGitignore 535: }); 536: } 537: }, { 538: id: 'copyFullResponse', 539: label: 'Always copy full response (skip /copy picker)', 540: value: globalConfig.copyFullResponse, 541: type: 'boolean' as const, 542: onChange(copyFullResponse: boolean) { 543: saveGlobalConfig(current_7 => ({ 544: ...current_7, 545: copyFullResponse 546: })); 547: setGlobalConfig({ 548: ...getGlobalConfig(), 549: copyFullResponse 550: }); 551: logEvent('tengu_config_changed', { 552: setting: 'copyFullResponse' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 553: value: String(copyFullResponse) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 554: }); 555: } 556: }, 557: ...(isFullscreenEnvEnabled() ? [{ 558: id: 'copyOnSelect', 559: label: 'Copy on select', 560: value: globalConfig.copyOnSelect ?? true, 561: type: 'boolean' as const, 562: onChange(copyOnSelect: boolean) { 563: saveGlobalConfig(current_8 => ({ 564: ...current_8, 565: copyOnSelect 566: })); 567: setGlobalConfig({ 568: ...getGlobalConfig(), 569: copyOnSelect 570: }); 571: logEvent('tengu_config_changed', { 572: setting: 'copyOnSelect' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 573: value: String(copyOnSelect) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 574: }); 575: } 576: }] : []), 577: autoUpdaterDisabledReason ? { 578: id: 'autoUpdatesChannel', 579: label: 'Auto-update channel', 580: value: 'disabled', 581: type: 'managedEnum' as const, 582: onChange() {} 583: } : { 584: id: 'autoUpdatesChannel', 585: label: 'Auto-update channel', 586: value: settingsData?.autoUpdatesChannel ?? 'latest', 587: type: 'managedEnum' as const, 588: onChange() { 589: } 590: }, { 591: id: 'theme', 592: label: 'Theme', 593: value: themeSetting, 594: type: 'managedEnum', 595: onChange: setTheme 596: }, { 597: id: 'notifChannel', 598: label: feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION') ? 'Local notifications' : 'Notifications', 599: value: globalConfig.preferredNotifChannel, 600: options: ['auto', 'iterm2', 'terminal_bell', 'iterm2_with_bell', 'kitty', 'ghostty', 'notifications_disabled'], 601: type: 'enum', 602: onChange(notifChannel: GlobalConfig['preferredNotifChannel']) { 603: saveGlobalConfig(current_9 => ({ 604: ...current_9, 605: preferredNotifChannel: notifChannel 606: })); 607: setGlobalConfig({ 608: ...getGlobalConfig(), 609: preferredNotifChannel: notifChannel 610: }); 611: } 612: }, ...(feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION') ? [{ 613: id: 'taskCompleteNotifEnabled', 614: label: 'Push when idle', 615: value: globalConfig.taskCompleteNotifEnabled ?? false, 616: type: 'boolean' as const, 617: onChange(taskCompleteNotifEnabled: boolean) { 618: saveGlobalConfig(current_10 => ({ 619: ...current_10, 620: taskCompleteNotifEnabled 621: })); 622: setGlobalConfig({ 623: ...getGlobalConfig(), 624: taskCompleteNotifEnabled 625: }); 626: } 627: }, { 628: id: 'inputNeededNotifEnabled', 629: label: 'Push when input needed', 630: value: globalConfig.inputNeededNotifEnabled ?? false, 631: type: 'boolean' as const, 632: onChange(inputNeededNotifEnabled: boolean) { 633: saveGlobalConfig(current_11 => ({ 634: ...current_11, 635: inputNeededNotifEnabled 636: })); 637: setGlobalConfig({ 638: ...getGlobalConfig(), 639: inputNeededNotifEnabled 640: }); 641: } 642: }, { 643: id: 'agentPushNotifEnabled', 644: label: 'Push when Claude decides', 645: value: globalConfig.agentPushNotifEnabled ?? false, 646: type: 'boolean' as const, 647: onChange(agentPushNotifEnabled: boolean) { 648: saveGlobalConfig(current_12 => ({ 649: ...current_12, 650: agentPushNotifEnabled 651: })); 652: setGlobalConfig({ 653: ...getGlobalConfig(), 654: agentPushNotifEnabled 655: }); 656: } 657: }] : []), { 658: id: 'outputStyle', 659: label: 'Output style', 660: value: currentOutputStyle, 661: type: 'managedEnum' as const, 662: onChange: () => {} 663: }, ...(showDefaultViewPicker ? [{ 664: id: 'defaultView', 665: label: 'What you see by default', 666: value: settingsData?.defaultView === undefined ? 'default' : String(settingsData.defaultView), 667: options: ['transcript', 'chat', 'default'], 668: type: 'enum' as const, 669: onChange(selected: string) { 670: const defaultView = selected === 'default' ? undefined : selected as 'chat' | 'transcript'; 671: updateSettingsForSource('localSettings', { 672: defaultView 673: }); 674: setSettingsData(prev_17 => ({ 675: ...prev_17, 676: defaultView 677: })); 678: const nextBrief = defaultView === 'chat'; 679: setAppState(prev_18 => { 680: if (prev_18.isBriefOnly === nextBrief) return prev_18; 681: return { 682: ...prev_18, 683: isBriefOnly: nextBrief 684: }; 685: }); 686: setUserMsgOptIn(nextBrief); 687: setChanges(prev_19 => ({ 688: ...prev_19, 689: 'Default view': selected 690: })); 691: logEvent('tengu_default_view_setting_changed', { 692: value: (defaultView ?? 'unset') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 693: }); 694: } 695: }] : []), { 696: id: 'language', 697: label: 'Language', 698: value: currentLanguage ?? 'Default (English)', 699: type: 'managedEnum' as const, 700: onChange: () => {} 701: }, { 702: id: 'editorMode', 703: label: 'Editor mode', 704: value: globalConfig.editorMode === 'emacs' ? 'normal' : globalConfig.editorMode || 'normal', 705: options: ['normal', 'vim'], 706: type: 'enum', 707: onChange(value_1: string) { 708: saveGlobalConfig(current_13 => ({ 709: ...current_13, 710: editorMode: value_1 as GlobalConfig['editorMode'] 711: })); 712: setGlobalConfig({ 713: ...getGlobalConfig(), 714: editorMode: value_1 as GlobalConfig['editorMode'] 715: }); 716: logEvent('tengu_editor_mode_changed', { 717: mode: value_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 718: source: 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 719: }); 720: } 721: }, { 722: id: 'prStatusFooterEnabled', 723: label: 'Show PR status footer', 724: value: globalConfig.prStatusFooterEnabled ?? true, 725: type: 'boolean' as const, 726: onChange(enabled_4: boolean) { 727: saveGlobalConfig(current_14 => { 728: if (current_14.prStatusFooterEnabled === enabled_4) return current_14; 729: return { 730: ...current_14, 731: prStatusFooterEnabled: enabled_4 732: }; 733: }); 734: setGlobalConfig({ 735: ...getGlobalConfig(), 736: prStatusFooterEnabled: enabled_4 737: }); 738: logEvent('tengu_pr_status_footer_setting_changed', { 739: enabled: enabled_4 740: }); 741: } 742: }, { 743: id: 'model', 744: label: 'Model', 745: value: mainLoopModel === null ? 'Default (recommended)' : mainLoopModel, 746: type: 'managedEnum' as const, 747: onChange: onChangeMainModelConfig 748: }, ...(isConnectedToIde ? [{ 749: id: 'diffTool', 750: label: 'Diff tool', 751: value: globalConfig.diffTool ?? 'auto', 752: options: ['terminal', 'auto'], 753: type: 'enum' as const, 754: onChange(diffTool: string) { 755: saveGlobalConfig(current_15 => ({ 756: ...current_15, 757: diffTool: diffTool as GlobalConfig['diffTool'] 758: })); 759: setGlobalConfig({ 760: ...getGlobalConfig(), 761: diffTool: diffTool as GlobalConfig['diffTool'] 762: }); 763: logEvent('tengu_diff_tool_changed', { 764: tool: diffTool as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 765: source: 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 766: }); 767: } 768: }] : []), ...(!isSupportedTerminal() ? [{ 769: id: 'autoConnectIde', 770: label: 'Auto-connect to IDE (external terminal)', 771: value: globalConfig.autoConnectIde ?? false, 772: type: 'boolean' as const, 773: onChange(autoConnectIde: boolean) { 774: saveGlobalConfig(current_16 => ({ 775: ...current_16, 776: autoConnectIde 777: })); 778: setGlobalConfig({ 779: ...getGlobalConfig(), 780: autoConnectIde 781: }); 782: logEvent('tengu_auto_connect_ide_changed', { 783: enabled: autoConnectIde, 784: source: 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 785: }); 786: } 787: }] : []), ...(isSupportedTerminal() ? [{ 788: id: 'autoInstallIdeExtension', 789: label: 'Auto-install IDE extension', 790: value: globalConfig.autoInstallIdeExtension ?? true, 791: type: 'boolean' as const, 792: onChange(autoInstallIdeExtension: boolean) { 793: saveGlobalConfig(current_17 => ({ 794: ...current_17, 795: autoInstallIdeExtension 796: })); 797: setGlobalConfig({ 798: ...getGlobalConfig(), 799: autoInstallIdeExtension 800: }); 801: logEvent('tengu_auto_install_ide_extension_changed', { 802: enabled: autoInstallIdeExtension, 803: source: 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 804: }); 805: } 806: }] : []), { 807: id: 'claudeInChromeDefaultEnabled', 808: label: 'Claude in Chrome enabled by default', 809: value: globalConfig.claudeInChromeDefaultEnabled ?? true, 810: type: 'boolean' as const, 811: onChange(enabled_5: boolean) { 812: saveGlobalConfig(current_18 => ({ 813: ...current_18, 814: claudeInChromeDefaultEnabled: enabled_5 815: })); 816: setGlobalConfig({ 817: ...getGlobalConfig(), 818: claudeInChromeDefaultEnabled: enabled_5 819: }); 820: logEvent('tengu_claude_in_chrome_setting_changed', { 821: enabled: enabled_5 822: }); 823: } 824: }, 825: ...(isAgentSwarmsEnabled() ? (() => { 826: const cliOverride = getCliTeammateModeOverride(); 827: const label = cliOverride ? `Teammate mode [overridden: ${cliOverride}]` : 'Teammate mode'; 828: return [{ 829: id: 'teammateMode', 830: label, 831: value: globalConfig.teammateMode ?? 'auto', 832: options: ['auto', 'tmux', 'in-process'], 833: type: 'enum' as const, 834: onChange(mode_0: string) { 835: if (mode_0 !== 'auto' && mode_0 !== 'tmux' && mode_0 !== 'in-process') { 836: return; 837: } 838: clearCliTeammateModeOverride(mode_0); 839: saveGlobalConfig(current_19 => ({ 840: ...current_19, 841: teammateMode: mode_0 842: })); 843: setGlobalConfig({ 844: ...getGlobalConfig(), 845: teammateMode: mode_0 846: }); 847: logEvent('tengu_teammate_mode_changed', { 848: mode: mode_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 849: }); 850: } 851: }, { 852: id: 'teammateDefaultModel', 853: label: 'Default teammate model', 854: value: teammateModelDisplayString(globalConfig.teammateDefaultModel), 855: type: 'managedEnum' as const, 856: onChange() {} 857: }]; 858: })() : []), 859: ...(feature('BRIDGE_MODE') && isBridgeEnabled() ? [{ 860: id: 'remoteControlAtStartup', 861: label: 'Enable Remote Control for all sessions', 862: value: globalConfig.remoteControlAtStartup === undefined ? 'default' : String(globalConfig.remoteControlAtStartup), 863: options: ['true', 'false', 'default'], 864: type: 'enum' as const, 865: onChange(selected_0: string) { 866: if (selected_0 === 'default') { 867: saveGlobalConfig(current_20 => { 868: if (current_20.remoteControlAtStartup === undefined) return current_20; 869: const next_0 = { 870: ...current_20 871: }; 872: delete next_0.remoteControlAtStartup; 873: return next_0; 874: }); 875: setGlobalConfig({ 876: ...getGlobalConfig(), 877: remoteControlAtStartup: undefined 878: }); 879: } else { 880: const enabled_6 = selected_0 === 'true'; 881: saveGlobalConfig(current_21 => { 882: if (current_21.remoteControlAtStartup === enabled_6) return current_21; 883: return { 884: ...current_21, 885: remoteControlAtStartup: enabled_6 886: }; 887: }); 888: setGlobalConfig({ 889: ...getGlobalConfig(), 890: remoteControlAtStartup: enabled_6 891: }); 892: } 893: const resolved = getRemoteControlAtStartup(); 894: setAppState(prev_20 => { 895: if (prev_20.replBridgeEnabled === resolved && !prev_20.replBridgeOutboundOnly) return prev_20; 896: return { 897: ...prev_20, 898: replBridgeEnabled: resolved, 899: replBridgeOutboundOnly: false 900: }; 901: }); 902: } 903: }] : []), ...(shouldShowExternalIncludesToggle ? [{ 904: id: 'showExternalIncludesDialog', 905: label: 'External CLAUDE.md includes', 906: value: (() => { 907: const projectConfig = getCurrentProjectConfig(); 908: if (projectConfig.hasClaudeMdExternalIncludesApproved) { 909: return 'true'; 910: } else { 911: return 'false'; 912: } 913: })(), 914: type: 'managedEnum' as const, 915: onChange() { 916: } 917: }] : []), ...(process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace() ? [{ 918: id: 'apiKey', 919: label: <Text> 920: Use custom API key:{' '} 921: <Text bold> 922: {normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY)} 923: </Text> 924: </Text>, 925: searchText: 'Use custom API key', 926: value: Boolean(process.env.ANTHROPIC_API_KEY && globalConfig.customApiKeyResponses?.approved?.includes(normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY))), 927: type: 'boolean' as const, 928: onChange(useCustomKey: boolean) { 929: saveGlobalConfig(current_22 => { 930: const updated = { 931: ...current_22 932: }; 933: if (!updated.customApiKeyResponses) { 934: updated.customApiKeyResponses = { 935: approved: [], 936: rejected: [] 937: }; 938: } 939: if (!updated.customApiKeyResponses.approved) { 940: updated.customApiKeyResponses = { 941: ...updated.customApiKeyResponses, 942: approved: [] 943: }; 944: } 945: if (!updated.customApiKeyResponses.rejected) { 946: updated.customApiKeyResponses = { 947: ...updated.customApiKeyResponses, 948: rejected: [] 949: }; 950: } 951: if (process.env.ANTHROPIC_API_KEY) { 952: const truncatedKey = normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY); 953: if (useCustomKey) { 954: updated.customApiKeyResponses = { 955: ...updated.customApiKeyResponses, 956: approved: [...(updated.customApiKeyResponses.approved ?? []).filter(k => k !== truncatedKey), truncatedKey], 957: rejected: (updated.customApiKeyResponses.rejected ?? []).filter(k_0 => k_0 !== truncatedKey) 958: }; 959: } else { 960: updated.customApiKeyResponses = { 961: ...updated.customApiKeyResponses, 962: approved: (updated.customApiKeyResponses.approved ?? []).filter(k_1 => k_1 !== truncatedKey), 963: rejected: [...(updated.customApiKeyResponses.rejected ?? []).filter(k_2 => k_2 !== truncatedKey), truncatedKey] 964: }; 965: } 966: } 967: return updated; 968: }); 969: setGlobalConfig(getGlobalConfig()); 970: } 971: }] : [])]; 972: const filteredSettingsItems = React.useMemo(() => { 973: if (!searchQuery) return settingsItems; 974: const lowerQuery = searchQuery.toLowerCase(); 975: return settingsItems.filter(setting => { 976: if (setting.id.toLowerCase().includes(lowerQuery)) return true; 977: const searchableText = 'searchText' in setting ? setting.searchText : setting.label; 978: return searchableText.toLowerCase().includes(lowerQuery); 979: }); 980: }, [settingsItems, searchQuery]); 981: React.useEffect(() => { 982: if (selectedIndex >= filteredSettingsItems.length) { 983: const newIndex = Math.max(0, filteredSettingsItems.length - 1); 984: setSelectedIndex(newIndex); 985: setScrollOffset(Math.max(0, newIndex - maxVisible + 1)); 986: return; 987: } 988: setScrollOffset(prev_21 => { 989: if (selectedIndex < prev_21) return selectedIndex; 990: if (selectedIndex >= prev_21 + maxVisible) return selectedIndex - maxVisible + 1; 991: return prev_21; 992: }); 993: }, [filteredSettingsItems.length, selectedIndex, maxVisible]); 994: const adjustScrollOffset = useCallback((newIndex_0: number) => { 995: setScrollOffset(prev_22 => { 996: if (newIndex_0 < prev_22) return newIndex_0; 997: if (newIndex_0 >= prev_22 + maxVisible) return newIndex_0 - maxVisible + 1; 998: return prev_22; 999: }); 1000: }, [maxVisible]); 1001: const handleSaveAndClose = useCallback(() => { 1002: if (showSubmenu !== null) { 1003: return; 1004: } 1005: const formattedChanges: string[] = Object.entries(changes).map(([key, value_2]) => { 1006: logEvent('tengu_config_changed', { 1007: key: key as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 1008: value: value_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 1009: }); 1010: return `Set ${key} to ${chalk.bold(value_2)}`; 1011: }); 1012: const effectiveApiKey = isRunningOnHomespace() ? undefined : process.env.ANTHROPIC_API_KEY; 1013: const initialUsingCustomKey = Boolean(effectiveApiKey && initialConfig.current.customApiKeyResponses?.approved?.includes(normalizeApiKeyForConfig(effectiveApiKey))); 1014: const currentUsingCustomKey = Boolean(effectiveApiKey && globalConfig.customApiKeyResponses?.approved?.includes(normalizeApiKeyForConfig(effectiveApiKey))); 1015: if (initialUsingCustomKey !== currentUsingCustomKey) { 1016: formattedChanges.push(`${currentUsingCustomKey ? 'Enabled' : 'Disabled'} custom API key`); 1017: logEvent('tengu_config_changed', { 1018: key: 'env.ANTHROPIC_API_KEY' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 1019: value: currentUsingCustomKey as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 1020: }); 1021: } 1022: if (globalConfig.theme !== initialConfig.current.theme) { 1023: formattedChanges.push(`Set theme to ${chalk.bold(globalConfig.theme)}`); 1024: } 1025: if (globalConfig.preferredNotifChannel !== initialConfig.current.preferredNotifChannel) { 1026: formattedChanges.push(`Set notifications to ${chalk.bold(globalConfig.preferredNotifChannel)}`); 1027: } 1028: if (currentOutputStyle !== initialOutputStyle.current) { 1029: formattedChanges.push(`Set output style to ${chalk.bold(currentOutputStyle)}`); 1030: } 1031: if (currentLanguage !== initialLanguage.current) { 1032: formattedChanges.push(`Set response language to ${chalk.bold(currentLanguage ?? 'Default (English)')}`); 1033: } 1034: if (globalConfig.editorMode !== initialConfig.current.editorMode) { 1035: formattedChanges.push(`Set editor mode to ${chalk.bold(globalConfig.editorMode || 'emacs')}`); 1036: } 1037: if (globalConfig.diffTool !== initialConfig.current.diffTool) { 1038: formattedChanges.push(`Set diff tool to ${chalk.bold(globalConfig.diffTool)}`); 1039: } 1040: if (globalConfig.autoConnectIde !== initialConfig.current.autoConnectIde) { 1041: formattedChanges.push(`${globalConfig.autoConnectIde ? 'Enabled' : 'Disabled'} auto-connect to IDE`); 1042: } 1043: if (globalConfig.autoInstallIdeExtension !== initialConfig.current.autoInstallIdeExtension) { 1044: formattedChanges.push(`${globalConfig.autoInstallIdeExtension ? 'Enabled' : 'Disabled'} auto-install IDE extension`); 1045: } 1046: if (globalConfig.autoCompactEnabled !== initialConfig.current.autoCompactEnabled) { 1047: formattedChanges.push(`${globalConfig.autoCompactEnabled ? 'Enabled' : 'Disabled'} auto-compact`); 1048: } 1049: if (globalConfig.respectGitignore !== initialConfig.current.respectGitignore) { 1050: formattedChanges.push(`${globalConfig.respectGitignore ? 'Enabled' : 'Disabled'} respect .gitignore in file picker`); 1051: } 1052: if (globalConfig.copyFullResponse !== initialConfig.current.copyFullResponse) { 1053: formattedChanges.push(`${globalConfig.copyFullResponse ? 'Enabled' : 'Disabled'} always copy full response`); 1054: } 1055: if (globalConfig.copyOnSelect !== initialConfig.current.copyOnSelect) { 1056: formattedChanges.push(`${globalConfig.copyOnSelect ? 'Enabled' : 'Disabled'} copy on select`); 1057: } 1058: if (globalConfig.terminalProgressBarEnabled !== initialConfig.current.terminalProgressBarEnabled) { 1059: formattedChanges.push(`${globalConfig.terminalProgressBarEnabled ? 'Enabled' : 'Disabled'} terminal progress bar`); 1060: } 1061: if (globalConfig.showStatusInTerminalTab !== initialConfig.current.showStatusInTerminalTab) { 1062: formattedChanges.push(`${globalConfig.showStatusInTerminalTab ? 'Enabled' : 'Disabled'} terminal tab status`); 1063: } 1064: if (globalConfig.showTurnDuration !== initialConfig.current.showTurnDuration) { 1065: formattedChanges.push(`${globalConfig.showTurnDuration ? 'Enabled' : 'Disabled'} turn duration`); 1066: } 1067: if (globalConfig.remoteControlAtStartup !== initialConfig.current.remoteControlAtStartup) { 1068: const remoteLabel = globalConfig.remoteControlAtStartup === undefined ? 'Reset Remote Control to default' : `${globalConfig.remoteControlAtStartup ? 'Enabled' : 'Disabled'} Remote Control for all sessions`; 1069: formattedChanges.push(remoteLabel); 1070: } 1071: if (settingsData?.autoUpdatesChannel !== initialSettingsData.current?.autoUpdatesChannel) { 1072: formattedChanges.push(`Set auto-update channel to ${chalk.bold(settingsData?.autoUpdatesChannel ?? 'latest')}`); 1073: } 1074: if (formattedChanges.length > 0) { 1075: onClose(formattedChanges.join('\n')); 1076: } else { 1077: onClose('Config dialog dismissed', { 1078: display: 'system' 1079: }); 1080: } 1081: }, [showSubmenu, changes, globalConfig, mainLoopModel, currentOutputStyle, currentLanguage, settingsData?.autoUpdatesChannel, isFastModeEnabled() ? (settingsData as Record<string, unknown> | undefined)?.fastMode : undefined, onClose]); 1082: const revertChanges = useCallback(() => { 1083: if (themeSetting !== initialThemeSetting.current) { 1084: setTheme(initialThemeSetting.current); 1085: } 1086: saveGlobalConfig(() => initialConfig.current); 1087: const il = initialLocalSettings; 1088: updateSettingsForSource('localSettings', { 1089: spinnerTipsEnabled: il?.spinnerTipsEnabled, 1090: prefersReducedMotion: il?.prefersReducedMotion, 1091: defaultView: il?.defaultView, 1092: outputStyle: il?.outputStyle 1093: }); 1094: const iu = initialUserSettings; 1095: updateSettingsForSource('userSettings', { 1096: alwaysThinkingEnabled: iu?.alwaysThinkingEnabled, 1097: fastMode: iu?.fastMode, 1098: promptSuggestionEnabled: iu?.promptSuggestionEnabled, 1099: autoUpdatesChannel: iu?.autoUpdatesChannel, 1100: minimumVersion: iu?.minimumVersion, 1101: language: iu?.language, 1102: ...(feature('TRANSCRIPT_CLASSIFIER') ? { 1103: useAutoModeDuringPlan: (iu as { 1104: useAutoModeDuringPlan?: boolean; 1105: } | undefined)?.useAutoModeDuringPlan 1106: } : {}), 1107: syntaxHighlightingDisabled: iu?.syntaxHighlightingDisabled, 1108: permissions: iu?.permissions === undefined ? undefined : { 1109: ...iu.permissions, 1110: defaultMode: iu.permissions.defaultMode 1111: } 1112: }); 1113: const ia = initialAppState; 1114: setAppState(prev_23 => ({ 1115: ...prev_23, 1116: mainLoopModel: ia.mainLoopModel, 1117: mainLoopModelForSession: ia.mainLoopModelForSession, 1118: verbose: ia.verbose, 1119: thinkingEnabled: ia.thinkingEnabled, 1120: fastMode: ia.fastMode, 1121: promptSuggestionEnabled: ia.promptSuggestionEnabled, 1122: isBriefOnly: ia.isBriefOnly, 1123: replBridgeEnabled: ia.replBridgeEnabled, 1124: replBridgeOutboundOnly: ia.replBridgeOutboundOnly, 1125: settings: ia.settings, 1126: toolPermissionContext: transitionPlanAutoMode(prev_23.toolPermissionContext) 1127: })); 1128: if (getUserMsgOptIn() !== initialUserMsgOptIn) { 1129: setUserMsgOptIn(initialUserMsgOptIn); 1130: } 1131: }, [themeSetting, setTheme, initialLocalSettings, initialUserSettings, initialAppState, initialUserMsgOptIn, setAppState]); 1132: const handleEscape = useCallback(() => { 1133: if (showSubmenu !== null) { 1134: return; 1135: } 1136: if (isDirty.current) { 1137: revertChanges(); 1138: } 1139: onClose('Config dialog dismissed', { 1140: display: 'system' 1141: }); 1142: }, [showSubmenu, revertChanges, onClose]); 1143: useKeybinding('confirm:no', handleEscape, { 1144: context: 'Settings', 1145: isActive: showSubmenu === null && !isSearchMode && !headerFocused 1146: }); 1147: useKeybinding('settings:close', handleSaveAndClose, { 1148: context: 'Settings', 1149: isActive: showSubmenu === null && !isSearchMode && !headerFocused 1150: }); 1151: const toggleSetting = useCallback(() => { 1152: const setting_0 = filteredSettingsItems[selectedIndex]; 1153: if (!setting_0 || !setting_0.onChange) { 1154: return; 1155: } 1156: if (setting_0.type === 'boolean') { 1157: isDirty.current = true; 1158: setting_0.onChange(!setting_0.value); 1159: if (setting_0.id === 'thinkingEnabled') { 1160: const newValue = !setting_0.value; 1161: const backToInitial = newValue === initialThinkingEnabled.current; 1162: if (backToInitial) { 1163: setShowThinkingWarning(false); 1164: } else if (context.messages.some(m_0 => m_0.type === 'assistant')) { 1165: setShowThinkingWarning(true); 1166: } 1167: } 1168: return; 1169: } 1170: if (setting_0.id === 'theme' || setting_0.id === 'model' || setting_0.id === 'teammateDefaultModel' || setting_0.id === 'showExternalIncludesDialog' || setting_0.id === 'outputStyle' || setting_0.id === 'language') { 1171: switch (setting_0.id) { 1172: case 'theme': 1173: setShowSubmenu('Theme'); 1174: setTabsHidden(true); 1175: return; 1176: case 'model': 1177: setShowSubmenu('Model'); 1178: setTabsHidden(true); 1179: return; 1180: case 'teammateDefaultModel': 1181: setShowSubmenu('TeammateModel'); 1182: setTabsHidden(true); 1183: return; 1184: case 'showExternalIncludesDialog': 1185: setShowSubmenu('ExternalIncludes'); 1186: setTabsHidden(true); 1187: return; 1188: case 'outputStyle': 1189: setShowSubmenu('OutputStyle'); 1190: setTabsHidden(true); 1191: return; 1192: case 'language': 1193: setShowSubmenu('Language'); 1194: setTabsHidden(true); 1195: return; 1196: } 1197: } 1198: if (setting_0.id === 'autoUpdatesChannel') { 1199: if (autoUpdaterDisabledReason) { 1200: setShowSubmenu('EnableAutoUpdates'); 1201: setTabsHidden(true); 1202: return; 1203: } 1204: const currentChannel = settingsData?.autoUpdatesChannel ?? 'latest'; 1205: if (currentChannel === 'latest') { 1206: setShowSubmenu('ChannelDowngrade'); 1207: setTabsHidden(true); 1208: } else { 1209: isDirty.current = true; 1210: updateSettingsForSource('userSettings', { 1211: autoUpdatesChannel: 'latest', 1212: minimumVersion: undefined 1213: }); 1214: setSettingsData(prev_24 => ({ 1215: ...prev_24, 1216: autoUpdatesChannel: 'latest', 1217: minimumVersion: undefined 1218: })); 1219: logEvent('tengu_autoupdate_channel_changed', { 1220: channel: 'latest' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 1221: }); 1222: } 1223: return; 1224: } 1225: if (setting_0.type === 'enum') { 1226: isDirty.current = true; 1227: const currentIndex = setting_0.options.indexOf(setting_0.value); 1228: const nextIndex = (currentIndex + 1) % setting_0.options.length; 1229: setting_0.onChange(setting_0.options[nextIndex]!); 1230: return; 1231: } 1232: }, [autoUpdaterDisabledReason, filteredSettingsItems, selectedIndex, settingsData?.autoUpdatesChannel, setTabsHidden]); 1233: const moveSelection = (delta: -1 | 1): void => { 1234: setShowThinkingWarning(false); 1235: const newIndex_1 = Math.max(0, Math.min(filteredSettingsItems.length - 1, selectedIndex + delta)); 1236: setSelectedIndex(newIndex_1); 1237: adjustScrollOffset(newIndex_1); 1238: }; 1239: useKeybindings({ 1240: 'select:previous': () => { 1241: if (selectedIndex === 0) { 1242: setShowThinkingWarning(false); 1243: setIsSearchMode(true); 1244: setScrollOffset(0); 1245: } else { 1246: moveSelection(-1); 1247: } 1248: }, 1249: 'select:next': () => moveSelection(1), 1250: 'scroll:lineUp': () => moveSelection(-1), 1251: 'scroll:lineDown': () => moveSelection(1), 1252: 'select:accept': toggleSetting, 1253: 'settings:search': () => { 1254: setIsSearchMode(true); 1255: setSearchQuery(''); 1256: } 1257: }, { 1258: context: 'Settings', 1259: isActive: showSubmenu === null && !isSearchMode && !headerFocused 1260: }); 1261: const handleKeyDown = useCallback((e: KeyboardEvent) => { 1262: if (showSubmenu !== null) return; 1263: if (headerFocused) return; 1264: if (isSearchMode) { 1265: if (e.key === 'escape') { 1266: e.preventDefault(); 1267: if (searchQuery.length > 0) { 1268: setSearchQuery(''); 1269: } else { 1270: setIsSearchMode(false); 1271: } 1272: return; 1273: } 1274: if (e.key === 'return' || e.key === 'down' || e.key === 'wheeldown') { 1275: e.preventDefault(); 1276: setIsSearchMode(false); 1277: setSelectedIndex(0); 1278: setScrollOffset(0); 1279: } 1280: return; 1281: } 1282: if (e.key === 'left' || e.key === 'right' || e.key === 'tab') { 1283: e.preventDefault(); 1284: toggleSetting(); 1285: return; 1286: } 1287: if (e.ctrl || e.meta) return; 1288: if (e.key === 'j' || e.key === 'k' || e.key === '/') return; 1289: if (e.key.length === 1 && e.key !== ' ') { 1290: e.preventDefault(); 1291: setIsSearchMode(true); 1292: setSearchQuery(e.key); 1293: } 1294: }, [showSubmenu, headerFocused, isSearchMode, searchQuery, setSearchQuery, toggleSetting]); 1295: return <Box flexDirection="column" width="100%" tabIndex={0} autoFocus onKeyDown={handleKeyDown}> 1296: {showSubmenu === 'Theme' ? <> 1297: <ThemePicker onThemeSelect={setting_1 => { 1298: isDirty.current = true; 1299: setTheme(setting_1); 1300: setShowSubmenu(null); 1301: setTabsHidden(false); 1302: }} onCancel={() => { 1303: setShowSubmenu(null); 1304: setTabsHidden(false); 1305: }} hideEscToCancel skipExitHandling={true} 1306: /> 1307: <Box> 1308: <Text dimColor italic> 1309: <Byline> 1310: <KeyboardShortcutHint shortcut="Enter" action="select" /> 1311: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /> 1312: </Byline> 1313: </Text> 1314: </Box> 1315: </> : showSubmenu === 'Model' ? <> 1316: <ModelPicker initial={mainLoopModel} onSelect={(model_0, _effort) => { 1317: isDirty.current = true; 1318: onChangeMainModelConfig(model_0); 1319: setShowSubmenu(null); 1320: setTabsHidden(false); 1321: }} onCancel={() => { 1322: setShowSubmenu(null); 1323: setTabsHidden(false); 1324: }} showFastModeNotice={isFastModeEnabled() ? isFastMode && isFastModeSupportedByModel(mainLoopModel) && isFastModeAvailable() : false} /> 1325: <Text dimColor> 1326: <Byline> 1327: <KeyboardShortcutHint shortcut="Enter" action="confirm" /> 1328: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /> 1329: </Byline> 1330: </Text> 1331: </> : showSubmenu === 'TeammateModel' ? <> 1332: <ModelPicker initial={globalConfig.teammateDefaultModel ?? null} skipSettingsWrite headerText="Default model for newly spawned teammates. The leader can override via the tool call's model parameter." onSelect={(model_1, _effort_0) => { 1333: setShowSubmenu(null); 1334: setTabsHidden(false); 1335: if (globalConfig.teammateDefaultModel === undefined && model_1 === null) { 1336: return; 1337: } 1338: isDirty.current = true; 1339: saveGlobalConfig(current_23 => current_23.teammateDefaultModel === model_1 ? current_23 : { 1340: ...current_23, 1341: teammateDefaultModel: model_1 1342: }); 1343: setGlobalConfig({ 1344: ...getGlobalConfig(), 1345: teammateDefaultModel: model_1 1346: }); 1347: setChanges(prev_25 => ({ 1348: ...prev_25, 1349: teammateDefaultModel: teammateModelDisplayString(model_1) 1350: })); 1351: logEvent('tengu_teammate_default_model_changed', { 1352: model: model_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 1353: }); 1354: }} onCancel={() => { 1355: setShowSubmenu(null); 1356: setTabsHidden(false); 1357: }} /> 1358: <Text dimColor> 1359: <Byline> 1360: <KeyboardShortcutHint shortcut="Enter" action="confirm" /> 1361: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /> 1362: </Byline> 1363: </Text> 1364: </> : showSubmenu === 'ExternalIncludes' ? <> 1365: <ClaudeMdExternalIncludesDialog onDone={() => { 1366: setShowSubmenu(null); 1367: setTabsHidden(false); 1368: }} externalIncludes={getExternalClaudeMdIncludes(memoryFiles)} /> 1369: <Text dimColor> 1370: <Byline> 1371: <KeyboardShortcutHint shortcut="Enter" action="confirm" /> 1372: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="disable external includes" /> 1373: </Byline> 1374: </Text> 1375: </> : showSubmenu === 'OutputStyle' ? <> 1376: <OutputStylePicker initialStyle={currentOutputStyle} onComplete={style => { 1377: isDirty.current = true; 1378: setCurrentOutputStyle(style ?? DEFAULT_OUTPUT_STYLE_NAME); 1379: setShowSubmenu(null); 1380: setTabsHidden(false); 1381: updateSettingsForSource('localSettings', { 1382: outputStyle: style 1383: }); 1384: void logEvent('tengu_output_style_changed', { 1385: style: (style ?? DEFAULT_OUTPUT_STYLE_NAME) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 1386: source: 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 1387: settings_source: 'localSettings' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 1388: }); 1389: }} onCancel={() => { 1390: setShowSubmenu(null); 1391: setTabsHidden(false); 1392: }} /> 1393: <Text dimColor> 1394: <Byline> 1395: <KeyboardShortcutHint shortcut="Enter" action="confirm" /> 1396: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /> 1397: </Byline> 1398: </Text> 1399: </> : showSubmenu === 'Language' ? <> 1400: <LanguagePicker initialLanguage={currentLanguage} onComplete={language => { 1401: isDirty.current = true; 1402: setCurrentLanguage(language); 1403: setShowSubmenu(null); 1404: setTabsHidden(false); 1405: updateSettingsForSource('userSettings', { 1406: language 1407: }); 1408: void logEvent('tengu_language_changed', { 1409: language: (language ?? 'default') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 1410: source: 'config_panel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 1411: }); 1412: }} onCancel={() => { 1413: setShowSubmenu(null); 1414: setTabsHidden(false); 1415: }} /> 1416: <Text dimColor> 1417: <Byline> 1418: <KeyboardShortcutHint shortcut="Enter" action="confirm" /> 1419: <ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" /> 1420: </Byline> 1421: </Text> 1422: </> : showSubmenu === 'EnableAutoUpdates' ? <Dialog title="Enable Auto-Updates" onCancel={() => { 1423: setShowSubmenu(null); 1424: setTabsHidden(false); 1425: }} hideBorder hideInputGuide> 1426: {autoUpdaterDisabledReason?.type !== 'config' ? <> 1427: <Text> 1428: {autoUpdaterDisabledReason?.type === 'env' ? 'Auto-updates are controlled by an environment variable and cannot be changed here.' : 'Auto-updates are disabled in development builds.'} 1429: </Text> 1430: {autoUpdaterDisabledReason?.type === 'env' && <Text dimColor> 1431: Unset {autoUpdaterDisabledReason.envVar} to re-enable 1432: auto-updates. 1433: </Text>} 1434: </> : <Select options={[{ 1435: label: 'Enable with latest channel', 1436: value: 'latest' 1437: }, { 1438: label: 'Enable with stable channel', 1439: value: 'stable' 1440: }]} onChange={(channel: string) => { 1441: isDirty.current = true; 1442: setShowSubmenu(null); 1443: setTabsHidden(false); 1444: saveGlobalConfig(current_24 => ({ 1445: ...current_24, 1446: autoUpdates: true 1447: })); 1448: setGlobalConfig({ 1449: ...getGlobalConfig(), 1450: autoUpdates: true 1451: }); 1452: updateSettingsForSource('userSettings', { 1453: autoUpdatesChannel: channel as 'latest' | 'stable', 1454: minimumVersion: undefined 1455: }); 1456: setSettingsData(prev_26 => ({ 1457: ...prev_26, 1458: autoUpdatesChannel: channel as 'latest' | 'stable', 1459: minimumVersion: undefined 1460: })); 1461: logEvent('tengu_autoupdate_enabled', { 1462: channel: channel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 1463: }); 1464: }} />} 1465: </Dialog> : showSubmenu === 'ChannelDowngrade' ? <ChannelDowngradeDialog currentVersion={MACRO.VERSION} onChoice={(choice: ChannelDowngradeChoice) => { 1466: setShowSubmenu(null); 1467: setTabsHidden(false); 1468: if (choice === 'cancel') { 1469: return; 1470: } 1471: isDirty.current = true; 1472: const newSettings: { 1473: autoUpdatesChannel: 'stable'; 1474: minimumVersion?: string; 1475: } = { 1476: autoUpdatesChannel: 'stable' 1477: }; 1478: if (choice === 'stay') { 1479: newSettings.minimumVersion = MACRO.VERSION; 1480: } 1481: updateSettingsForSource('userSettings', newSettings); 1482: setSettingsData(prev_27 => ({ 1483: ...prev_27, 1484: ...newSettings 1485: })); 1486: logEvent('tengu_autoupdate_channel_changed', { 1487: channel: 'stable' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 1488: minimum_version_set: choice === 'stay' 1489: }); 1490: }} /> : <Box flexDirection="column" gap={1} marginY={insideModal ? undefined : 1}> 1491: <SearchBox query={searchQuery} isFocused={isSearchMode && !headerFocused} isTerminalFocused={isTerminalFocused} cursorOffset={searchCursorOffset} placeholder="Search settings…" /> 1492: <Box flexDirection="column"> 1493: {filteredSettingsItems.length === 0 ? <Text dimColor italic> 1494: No settings match &quot;{searchQuery}&quot; 1495: </Text> : <> 1496: {scrollOffset > 0 && <Text dimColor> 1497: {figures.arrowUp} {scrollOffset} more above 1498: </Text>} 1499: {filteredSettingsItems.slice(scrollOffset, scrollOffset + maxVisible).map((setting_2, i) => { 1500: const actualIndex = scrollOffset + i; 1501: const isSelected = actualIndex === selectedIndex && !headerFocused && !isSearchMode; 1502: return <React.Fragment key={setting_2.id}> 1503: <Box> 1504: <Box width={44}> 1505: <Text color={isSelected ? 'suggestion' : undefined}> 1506: {isSelected ? figures.pointer : ' '}{' '} 1507: {setting_2.label} 1508: </Text> 1509: </Box> 1510: <Box key={isSelected ? 'selected' : 'unselected'}> 1511: {setting_2.type === 'boolean' ? <> 1512: <Text color={isSelected ? 'suggestion' : undefined}> 1513: {setting_2.value.toString()} 1514: </Text> 1515: {showThinkingWarning && setting_2.id === 'thinkingEnabled' && <Text color="warning"> 1516: {' '} 1517: Changing thinking mode mid-conversation 1518: will increase latency and may reduce 1519: quality. 1520: </Text>} 1521: </> : setting_2.id === 'theme' ? <Text color={isSelected ? 'suggestion' : undefined}> 1522: {THEME_LABELS[setting_2.value.toString()] ?? setting_2.value.toString()} 1523: </Text> : setting_2.id === 'notifChannel' ? <Text color={isSelected ? 'suggestion' : undefined}> 1524: <NotifChannelLabel value={setting_2.value.toString()} /> 1525: </Text> : setting_2.id === 'defaultPermissionMode' ? <Text color={isSelected ? 'suggestion' : undefined}> 1526: {permissionModeTitle(setting_2.value as PermissionMode)} 1527: </Text> : setting_2.id === 'autoUpdatesChannel' && autoUpdaterDisabledReason ? <Box flexDirection="column"> 1528: <Text color={isSelected ? 'suggestion' : undefined}> 1529: disabled 1530: </Text> 1531: <Text dimColor> 1532: ( 1533: {formatAutoUpdaterDisabledReason(autoUpdaterDisabledReason)} 1534: ) 1535: </Text> 1536: </Box> : <Text color={isSelected ? 'suggestion' : undefined}> 1537: {setting_2.value.toString()} 1538: </Text>} 1539: </Box> 1540: </Box> 1541: </React.Fragment>; 1542: })} 1543: {scrollOffset + maxVisible < filteredSettingsItems.length && <Text dimColor> 1544: {figures.arrowDown}{' '} 1545: {filteredSettingsItems.length - scrollOffset - maxVisible}{' '} 1546: more below 1547: </Text>} 1548: </>} 1549: </Box> 1550: {headerFocused ? <Text dimColor> 1551: <Byline> 1552: <KeyboardShortcutHint shortcut="←/→ tab" action="switch" /> 1553: <KeyboardShortcutHint shortcut="↓" action="return" /> 1554: <ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="close" /> 1555: </Byline> 1556: </Text> : isSearchMode ? <Text dimColor> 1557: <Byline> 1558: <Text>Type to filter</Text> 1559: <KeyboardShortcutHint shortcut="Enter/↓" action="select" /> 1560: <KeyboardShortcutHint shortcut="↑" action="tabs" /> 1561: <ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="clear" /> 1562: </Byline> 1563: </Text> : <Text dimColor> 1564: <Byline> 1565: <ConfigurableShortcutHint action="select:accept" context="Settings" fallback="Space" description="change" /> 1566: <ConfigurableShortcutHint action="settings:close" context="Settings" fallback="Enter" description="save" /> 1567: <ConfigurableShortcutHint action="settings:search" context="Settings" fallback="/" description="search" /> 1568: <ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" /> 1569: </Byline> 1570: </Text>} 1571: </Box>} 1572: </Box>; 1573: } 1574: function teammateModelDisplayString(value: string | null | undefined): string { 1575: if (value === undefined) { 1576: return modelDisplayString(getHardcodedTeammateModelFallback()); 1577: } 1578: if (value === null) return "Default (leader's model)"; 1579: return modelDisplayString(value); 1580: } 1581: const THEME_LABELS: Record<string, string> = { 1582: auto: 'Auto (match terminal)', 1583: dark: 'Dark mode', 1584: light: 'Light mode', 1585: 'dark-daltonized': 'Dark mode (colorblind-friendly)', 1586: 'light-daltonized': 'Light mode (colorblind-friendly)', 1587: 'dark-ansi': 'Dark mode (ANSI colors only)', 1588: 'light-ansi': 'Light mode (ANSI colors only)' 1589: }; 1590: function NotifChannelLabel(t0) { 1591: const $ = _c(4); 1592: const { 1593: value 1594: } = t0; 1595: switch (value) { 1596: case "auto": 1597: { 1598: return "Auto"; 1599: } 1600: case "iterm2": 1601: { 1602: let t1; 1603: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 1604: t1 = <Text>iTerm2 <Text dimColor={true}>(OSC 9)</Text></Text>; 1605: $[0] = t1; 1606: } else { 1607: t1 = $[0]; 1608: } 1609: return t1; 1610: } 1611: case "terminal_bell": 1612: { 1613: let t1; 1614: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 1615: t1 = <Text>Terminal Bell <Text dimColor={true}>(\a)</Text></Text>; 1616: $[1] = t1; 1617: } else { 1618: t1 = $[1]; 1619: } 1620: return t1; 1621: } 1622: case "kitty": 1623: { 1624: let t1; 1625: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 1626: t1 = <Text>Kitty <Text dimColor={true}>(OSC 99)</Text></Text>; 1627: $[2] = t1; 1628: } else { 1629: t1 = $[2]; 1630: } 1631: return t1; 1632: } 1633: case "ghostty": 1634: { 1635: let t1; 1636: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 1637: t1 = <Text>Ghostty <Text dimColor={true}>(OSC 777)</Text></Text>; 1638: $[3] = t1; 1639: } else { 1640: t1 = $[3]; 1641: } 1642: return t1; 1643: } 1644: case "iterm2_with_bell": 1645: { 1646: return "iTerm2 w/ Bell"; 1647: } 1648: case "notifications_disabled": 1649: { 1650: return "Disabled"; 1651: } 1652: default: 1653: { 1654: return value; 1655: } 1656: } 1657: }

File: src/components/Settings/Settings.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Suspense, useState } from 'react'; 4: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 5: import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 6: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 7: import { useIsInsideModal, useModalOrTerminalSize } from '../../context/modalContext.js'; 8: import { Pane } from '../design-system/Pane.js'; 9: import { Tabs, Tab } from '../design-system/Tabs.js'; 10: import { Status, buildDiagnostics } from './Status.js'; 11: import { Config } from './Config.js'; 12: import { Usage } from './Usage.js'; 13: import type { LocalJSXCommandContext, CommandResultDisplay } from '../../commands.js'; 14: type Props = { 15: onClose: (result?: string, options?: { 16: display?: CommandResultDisplay; 17: }) => void; 18: context: LocalJSXCommandContext; 19: defaultTab: 'Status' | 'Config' | 'Usage' | 'Gates'; 20: }; 21: export function Settings(t0) { 22: const $ = _c(25); 23: const { 24: onClose, 25: context, 26: defaultTab 27: } = t0; 28: const [selectedTab, setSelectedTab] = useState(defaultTab); 29: const [tabsHidden, setTabsHidden] = useState(false); 30: const [configOwnsEsc, setConfigOwnsEsc] = useState(false); 31: const [gatesOwnsEsc, setGatesOwnsEsc] = useState(false); 32: const insideModal = useIsInsideModal(); 33: const { 34: rows 35: } = useModalOrTerminalSize(useTerminalSize()); 36: const contentHeight = insideModal ? rows + 1 : Math.max(15, Math.min(Math.floor(rows * 0.8), 30)); 37: const [diagnosticsPromise] = useState(_temp2); 38: useExitOnCtrlCDWithKeybindings(); 39: let t1; 40: if ($[0] !== onClose || $[1] !== tabsHidden) { 41: t1 = () => { 42: if (tabsHidden) { 43: return; 44: } 45: onClose("Status dialog dismissed", { 46: display: "system" 47: }); 48: }; 49: $[0] = onClose; 50: $[1] = tabsHidden; 51: $[2] = t1; 52: } else { 53: t1 = $[2]; 54: } 55: const handleEscape = t1; 56: const t2 = !tabsHidden && !(selectedTab === "Config" && configOwnsEsc) && !(selectedTab === "Gates" && gatesOwnsEsc); 57: let t3; 58: if ($[3] !== t2) { 59: t3 = { 60: context: "Settings", 61: isActive: t2 62: }; 63: $[3] = t2; 64: $[4] = t3; 65: } else { 66: t3 = $[4]; 67: } 68: useKeybinding("confirm:no", handleEscape, t3); 69: let t4; 70: if ($[5] !== context || $[6] !== diagnosticsPromise) { 71: t4 = <Tab key="status" title="Status"><Status context={context} diagnosticsPromise={diagnosticsPromise} /></Tab>; 72: $[5] = context; 73: $[6] = diagnosticsPromise; 74: $[7] = t4; 75: } else { 76: t4 = $[7]; 77: } 78: let t5; 79: if ($[8] !== contentHeight || $[9] !== context || $[10] !== onClose) { 80: t5 = <Tab key="config" title="Config"><Suspense fallback={null}><Config context={context} onClose={onClose} setTabsHidden={setTabsHidden} onIsSearchModeChange={setConfigOwnsEsc} contentHeight={contentHeight} /></Suspense></Tab>; 81: $[8] = contentHeight; 82: $[9] = context; 83: $[10] = onClose; 84: $[11] = t5; 85: } else { 86: t5 = $[11]; 87: } 88: let t6; 89: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 90: t6 = <Tab key="usage" title="Usage"><Usage /></Tab>; 91: $[12] = t6; 92: } else { 93: t6 = $[12]; 94: } 95: let t7; 96: if ($[13] !== contentHeight) { 97: t7 = false ? [<Tab key="gates" title="Gates"><Gates onOwnsEscChange={setGatesOwnsEsc} contentHeight={contentHeight} /></Tab>] : []; 98: $[13] = contentHeight; 99: $[14] = t7; 100: } else { 101: t7 = $[14]; 102: } 103: let t8; 104: if ($[15] !== t4 || $[16] !== t5 || $[17] !== t7) { 105: t8 = [t4, t5, t6, ...t7]; 106: $[15] = t4; 107: $[16] = t5; 108: $[17] = t7; 109: $[18] = t8; 110: } else { 111: t8 = $[18]; 112: } 113: const tabs = t8; 114: const t9 = defaultTab !== "Config" && defaultTab !== "Gates"; 115: const t10 = tabsHidden || insideModal ? undefined : contentHeight; 116: let t11; 117: if ($[19] !== selectedTab || $[20] !== t10 || $[21] !== t9 || $[22] !== tabs || $[23] !== tabsHidden) { 118: t11 = <Pane color="permission"><Tabs color="permission" selectedTab={selectedTab} onTabChange={setSelectedTab} hidden={tabsHidden} initialHeaderFocused={t9} contentHeight={t10}>{tabs}</Tabs></Pane>; 119: $[19] = selectedTab; 120: $[20] = t10; 121: $[21] = t9; 122: $[22] = tabs; 123: $[23] = tabsHidden; 124: $[24] = t11; 125: } else { 126: t11 = $[24]; 127: } 128: return t11; 129: } 130: function _temp2() { 131: return buildDiagnostics().catch(_temp); 132: } 133: function _temp() { 134: return []; 135: }

File: src/components/Settings/Status.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { Suspense, use } from 'react'; 5: import { getSessionId } from '../../bootstrap/state.js'; 6: import type { LocalJSXCommandContext } from '../../commands.js'; 7: import { useIsInsideModal } from '../../context/modalContext.js'; 8: import { Box, Text, useTheme } from '../../ink.js'; 9: import { type AppState, useAppState } from '../../state/AppState.js'; 10: import { getCwd } from '../../utils/cwd.js'; 11: import { getCurrentSessionTitle } from '../../utils/sessionStorage.js'; 12: import { buildAccountProperties, buildAPIProviderProperties, buildIDEProperties, buildInstallationDiagnostics, buildInstallationHealthDiagnostics, buildMcpProperties, buildMemoryDiagnostics, buildSandboxProperties, buildSettingSourcesProperties, type Diagnostic, getModelDisplayLabel, type Property } from '../../utils/status.js'; 13: import type { ThemeName } from '../../utils/theme.js'; 14: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 15: type Props = { 16: context: LocalJSXCommandContext; 17: diagnosticsPromise: Promise<Diagnostic[]>; 18: }; 19: function buildPrimarySection(): Property[] { 20: const sessionId = getSessionId(); 21: const customTitle = getCurrentSessionTitle(sessionId); 22: const nameValue = customTitle ?? <Text dimColor>/rename to add a name</Text>; 23: return [{ 24: label: 'Version', 25: value: MACRO.VERSION 26: }, { 27: label: 'Session name', 28: value: nameValue 29: }, { 30: label: 'Session ID', 31: value: sessionId 32: }, { 33: label: 'cwd', 34: value: getCwd() 35: }, ...buildAccountProperties(), ...buildAPIProviderProperties()]; 36: } 37: function buildSecondarySection({ 38: mainLoopModel, 39: mcp, 40: theme, 41: context 42: }: { 43: mainLoopModel: AppState['mainLoopModel']; 44: mcp: AppState['mcp']; 45: theme: ThemeName; 46: context: LocalJSXCommandContext; 47: }): Property[] { 48: const modelLabel = getModelDisplayLabel(mainLoopModel); 49: return [{ 50: label: 'Model', 51: value: modelLabel 52: }, ...buildIDEProperties(mcp.clients, context.options.ideInstallationStatus, theme), ...buildMcpProperties(mcp.clients, theme), ...buildSandboxProperties(), ...buildSettingSourcesProperties()]; 53: } 54: export async function buildDiagnostics(): Promise<Diagnostic[]> { 55: return [...(await buildInstallationDiagnostics()), ...(await buildInstallationHealthDiagnostics()), ...(await buildMemoryDiagnostics())]; 56: } 57: function PropertyValue(t0) { 58: const $ = _c(8); 59: const { 60: value 61: } = t0; 62: if (Array.isArray(value)) { 63: let t1; 64: if ($[0] !== value) { 65: let t2; 66: if ($[2] !== value.length) { 67: t2 = (item, i) => <Text key={i}>{item}{i < value.length - 1 ? "," : ""}</Text>; 68: $[2] = value.length; 69: $[3] = t2; 70: } else { 71: t2 = $[3]; 72: } 73: t1 = value.map(t2); 74: $[0] = value; 75: $[1] = t1; 76: } else { 77: t1 = $[1]; 78: } 79: let t2; 80: if ($[4] !== t1) { 81: t2 = <Box flexWrap="wrap" columnGap={1} flexShrink={99}>{t1}</Box>; 82: $[4] = t1; 83: $[5] = t2; 84: } else { 85: t2 = $[5]; 86: } 87: return t2; 88: } 89: if (typeof value === "string") { 90: let t1; 91: if ($[6] !== value) { 92: t1 = <Text>{value}</Text>; 93: $[6] = value; 94: $[7] = t1; 95: } else { 96: t1 = $[7]; 97: } 98: return t1; 99: } 100: return value; 101: } 102: export function Status(t0) { 103: const $ = _c(20); 104: const { 105: context, 106: diagnosticsPromise 107: } = t0; 108: const mainLoopModel = useAppState(_temp); 109: const mcp = useAppState(_temp2); 110: const [theme] = useTheme(); 111: let t1; 112: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 113: t1 = buildPrimarySection(); 114: $[0] = t1; 115: } else { 116: t1 = $[0]; 117: } 118: let t2; 119: if ($[1] !== context || $[2] !== mainLoopModel || $[3] !== mcp || $[4] !== theme) { 120: t2 = buildSecondarySection({ 121: mainLoopModel, 122: mcp, 123: theme, 124: context 125: }); 126: $[1] = context; 127: $[2] = mainLoopModel; 128: $[3] = mcp; 129: $[4] = theme; 130: $[5] = t2; 131: } else { 132: t2 = $[5]; 133: } 134: let t3; 135: if ($[6] !== t2) { 136: t3 = [t1, t2]; 137: $[6] = t2; 138: $[7] = t3; 139: } else { 140: t3 = $[7]; 141: } 142: const sections = t3; 143: const grow = useIsInsideModal() ? 1 : undefined; 144: let t4; 145: if ($[8] !== sections) { 146: t4 = sections.map(_temp4); 147: $[8] = sections; 148: $[9] = t4; 149: } else { 150: t4 = $[9]; 151: } 152: let t5; 153: if ($[10] !== diagnosticsPromise) { 154: t5 = <Suspense fallback={null}><Diagnostics promise={diagnosticsPromise} /></Suspense>; 155: $[10] = diagnosticsPromise; 156: $[11] = t5; 157: } else { 158: t5 = $[11]; 159: } 160: let t6; 161: if ($[12] !== grow || $[13] !== t4 || $[14] !== t5) { 162: t6 = <Box flexDirection="column" gap={1} flexGrow={grow}>{t4}{t5}</Box>; 163: $[12] = grow; 164: $[13] = t4; 165: $[14] = t5; 166: $[15] = t6; 167: } else { 168: t6 = $[15]; 169: } 170: let t7; 171: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 172: t7 = <Text dimColor={true}><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" /></Text>; 173: $[16] = t7; 174: } else { 175: t7 = $[16]; 176: } 177: let t8; 178: if ($[17] !== grow || $[18] !== t6) { 179: t8 = <Box flexDirection="column" flexGrow={grow}>{t6}{t7}</Box>; 180: $[17] = grow; 181: $[18] = t6; 182: $[19] = t8; 183: } else { 184: t8 = $[19]; 185: } 186: return t8; 187: } 188: function _temp4(properties, i) { 189: return properties.length > 0 && <Box key={i} flexDirection="column">{properties.map(_temp3)}</Box>; 190: } 191: function _temp3(t0, j) { 192: const { 193: label, 194: value 195: } = t0; 196: return <Box key={j} flexDirection="row" gap={1} flexShrink={0}>{label !== undefined && <Text bold={true}>{label}:</Text>}<PropertyValue value={value} /></Box>; 197: } 198: function _temp2(s_0) { 199: return s_0.mcp; 200: } 201: function _temp(s) { 202: return s.mainLoopModel; 203: } 204: function Diagnostics(t0) { 205: const $ = _c(5); 206: const { 207: promise 208: } = t0; 209: const diagnostics = use(promise); 210: if (diagnostics.length === 0) { 211: return null; 212: } 213: let t1; 214: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 215: t1 = <Text bold={true}>System Diagnostics</Text>; 216: $[0] = t1; 217: } else { 218: t1 = $[0]; 219: } 220: let t2; 221: if ($[1] !== diagnostics) { 222: t2 = diagnostics.map(_temp5); 223: $[1] = diagnostics; 224: $[2] = t2; 225: } else { 226: t2 = $[2]; 227: } 228: let t3; 229: if ($[3] !== t2) { 230: t3 = <Box flexDirection="column" paddingBottom={1}>{t1}{t2}</Box>; 231: $[3] = t2; 232: $[4] = t3; 233: } else { 234: t3 = $[4]; 235: } 236: return t3; 237: } 238: function _temp5(diagnostic, i) { 239: return <Box key={i} flexDirection="row" gap={1} paddingX={1}><Text color="error">{figures.warning}</Text>{typeof diagnostic === "string" ? <Text wrap="wrap">{diagnostic}</Text> : diagnostic}</Box>; 240: }

File: src/components/Settings/Usage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useEffect, useState } from 'react'; 4: import { extraUsage as extraUsageCommand } from 'src/commands/extra-usage/index.js'; 5: import { formatCost } from 'src/cost-tracker.js'; 6: import { getSubscriptionType } from 'src/utils/auth.js'; 7: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 8: import { Box, Text } from '../../ink.js'; 9: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 10: import { type ExtraUsage, fetchUtilization, type RateLimit, type Utilization } from '../../services/api/usage.js'; 11: import { formatResetText } from '../../utils/format.js'; 12: import { logError } from '../../utils/log.js'; 13: import { jsonStringify } from '../../utils/slowOperations.js'; 14: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 15: import { Byline } from '../design-system/Byline.js'; 16: import { ProgressBar } from '../design-system/ProgressBar.js'; 17: import { isEligibleForOverageCreditGrant, OverageCreditUpsell } from '../LogoV2/OverageCreditUpsell.js'; 18: type LimitBarProps = { 19: title: string; 20: limit: RateLimit; 21: maxWidth: number; 22: showTimeInReset?: boolean; 23: extraSubtext?: string; 24: }; 25: function LimitBar(t0) { 26: const $ = _c(34); 27: const { 28: title, 29: limit, 30: maxWidth, 31: showTimeInReset: t1, 32: extraSubtext 33: } = t0; 34: const showTimeInReset = t1 === undefined ? true : t1; 35: const { 36: utilization, 37: resets_at 38: } = limit; 39: if (utilization === null) { 40: return null; 41: } 42: const usedText = `${Math.floor(utilization)}% used`; 43: let subtext; 44: if (resets_at) { 45: let t2; 46: if ($[0] !== resets_at || $[1] !== showTimeInReset) { 47: t2 = formatResetText(resets_at, true, showTimeInReset); 48: $[0] = resets_at; 49: $[1] = showTimeInReset; 50: $[2] = t2; 51: } else { 52: t2 = $[2]; 53: } 54: subtext = `Resets ${t2}`; 55: } 56: if (extraSubtext) { 57: if (subtext) { 58: subtext = `${extraSubtext} · ${subtext}`; 59: } else { 60: subtext = extraSubtext; 61: } 62: } 63: if (maxWidth >= 62) { 64: let t2; 65: if ($[3] !== title) { 66: t2 = <Text bold={true}>{title}</Text>; 67: $[3] = title; 68: $[4] = t2; 69: } else { 70: t2 = $[4]; 71: } 72: const t3 = utilization / 100; 73: let t4; 74: if ($[5] !== t3) { 75: t4 = <ProgressBar ratio={t3} width={50} fillColor="rate_limit_fill" emptyColor="rate_limit_empty" />; 76: $[5] = t3; 77: $[6] = t4; 78: } else { 79: t4 = $[6]; 80: } 81: let t5; 82: if ($[7] !== usedText) { 83: t5 = <Text>{usedText}</Text>; 84: $[7] = usedText; 85: $[8] = t5; 86: } else { 87: t5 = $[8]; 88: } 89: let t6; 90: if ($[9] !== t4 || $[10] !== t5) { 91: t6 = <Box flexDirection="row" gap={1}>{t4}{t5}</Box>; 92: $[9] = t4; 93: $[10] = t5; 94: $[11] = t6; 95: } else { 96: t6 = $[11]; 97: } 98: let t7; 99: if ($[12] !== subtext) { 100: t7 = subtext && <Text dimColor={true}>{subtext}</Text>; 101: $[12] = subtext; 102: $[13] = t7; 103: } else { 104: t7 = $[13]; 105: } 106: let t8; 107: if ($[14] !== t2 || $[15] !== t6 || $[16] !== t7) { 108: t8 = <Box flexDirection="column">{t2}{t6}{t7}</Box>; 109: $[14] = t2; 110: $[15] = t6; 111: $[16] = t7; 112: $[17] = t8; 113: } else { 114: t8 = $[17]; 115: } 116: return t8; 117: } else { 118: let t2; 119: if ($[18] !== title) { 120: t2 = <Text bold={true}>{title}</Text>; 121: $[18] = title; 122: $[19] = t2; 123: } else { 124: t2 = $[19]; 125: } 126: let t3; 127: if ($[20] !== subtext) { 128: t3 = subtext && <><Text> </Text><Text dimColor={true}>· {subtext}</Text></>; 129: $[20] = subtext; 130: $[21] = t3; 131: } else { 132: t3 = $[21]; 133: } 134: let t4; 135: if ($[22] !== t2 || $[23] !== t3) { 136: t4 = <Text>{t2}{t3}</Text>; 137: $[22] = t2; 138: $[23] = t3; 139: $[24] = t4; 140: } else { 141: t4 = $[24]; 142: } 143: const t5 = utilization / 100; 144: let t6; 145: if ($[25] !== maxWidth || $[26] !== t5) { 146: t6 = <ProgressBar ratio={t5} width={maxWidth} fillColor="rate_limit_fill" emptyColor="rate_limit_empty" />; 147: $[25] = maxWidth; 148: $[26] = t5; 149: $[27] = t6; 150: } else { 151: t6 = $[27]; 152: } 153: let t7; 154: if ($[28] !== usedText) { 155: t7 = <Text>{usedText}</Text>; 156: $[28] = usedText; 157: $[29] = t7; 158: } else { 159: t7 = $[29]; 160: } 161: let t8; 162: if ($[30] !== t4 || $[31] !== t6 || $[32] !== t7) { 163: t8 = <Box flexDirection="column">{t4}{t6}{t7}</Box>; 164: $[30] = t4; 165: $[31] = t6; 166: $[32] = t7; 167: $[33] = t8; 168: } else { 169: t8 = $[33]; 170: } 171: return t8; 172: } 173: } 174: export function Usage(): React.ReactNode { 175: const [utilization, setUtilization] = useState<Utilization | null>(null); 176: const [error, setError] = useState<string | null>(null); 177: const [isLoading, setIsLoading] = useState(true); 178: const { 179: columns 180: } = useTerminalSize(); 181: const availableWidth = columns - 2; 182: const maxWidth = Math.min(availableWidth, 80); 183: const loadUtilization = React.useCallback(async () => { 184: setIsLoading(true); 185: setError(null); 186: try { 187: const data = await fetchUtilization(); 188: setUtilization(data); 189: } catch (err) { 190: logError(err as Error); 191: const axiosError = err as { 192: response?: { 193: data?: unknown; 194: }; 195: }; 196: const responseBody = axiosError.response?.data ? jsonStringify(axiosError.response.data) : undefined; 197: setError(responseBody ? `Failed to load usage data: ${responseBody}` : 'Failed to load usage data'); 198: } finally { 199: setIsLoading(false); 200: } 201: }, []); 202: useEffect(() => { 203: void loadUtilization(); 204: }, [loadUtilization]); 205: useKeybinding('settings:retry', () => { 206: void loadUtilization(); 207: }, { 208: context: 'Settings', 209: isActive: !!error && !isLoading 210: }); 211: if (error) { 212: return <Box flexDirection="column" gap={1}> 213: <Text color="error">Error: {error}</Text> 214: <Text dimColor> 215: <Byline> 216: <ConfigurableShortcutHint action="settings:retry" context="Settings" fallback="r" description="retry" /> 217: <ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" /> 218: </Byline> 219: </Text> 220: </Box>; 221: } 222: if (!utilization) { 223: return <Box flexDirection="column" gap={1}> 224: <Text dimColor>Loading usage data…</Text> 225: <Text dimColor> 226: <ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" /> 227: </Text> 228: </Box>; 229: } 230: const subscriptionType = getSubscriptionType(); 231: const showSonnetBar = subscriptionType === 'max' || subscriptionType === 'team' || subscriptionType === null; 232: const limits = [{ 233: title: 'Current session', 234: limit: utilization.five_hour 235: }, { 236: title: 'Current week (all models)', 237: limit: utilization.seven_day 238: }, ...(showSonnetBar ? [{ 239: title: 'Current week (Sonnet only)', 240: limit: utilization.seven_day_sonnet 241: }] : [])]; 242: return <Box flexDirection="column" gap={1} width="100%"> 243: {limits.some(({ 244: limit 245: }) => limit) || <Text dimColor>/usage is only available for subscription plans.</Text>} 246: {limits.map(({ 247: title, 248: limit: limit_0 249: }) => limit_0 && <LimitBar key={title} title={title} limit={limit_0} maxWidth={maxWidth} />)} 250: {utilization.extra_usage && <ExtraUsageSection extraUsage={utilization.extra_usage} maxWidth={maxWidth} />} 251: {isEligibleForOverageCreditGrant() && <OverageCreditUpsell maxWidth={maxWidth} />} 252: <Text dimColor> 253: <ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" /> 254: </Text> 255: </Box>; 256: } 257: type ExtraUsageSectionProps = { 258: extraUsage: ExtraUsage; 259: maxWidth: number; 260: }; 261: const EXTRA_USAGE_SECTION_TITLE = 'Extra usage'; 262: function ExtraUsageSection(t0) { 263: const $ = _c(20); 264: const { 265: extraUsage, 266: maxWidth 267: } = t0; 268: const subscriptionType = getSubscriptionType(); 269: const isProOrMax = subscriptionType === "pro" || subscriptionType === "max"; 270: if (!isProOrMax) { 271: return false; 272: } 273: if (!extraUsage.is_enabled) { 274: if (extraUsageCommand.isEnabled()) { 275: let t1; 276: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 277: t1 = <Box flexDirection="column"><Text bold={true}>{EXTRA_USAGE_SECTION_TITLE}</Text><Text dimColor={true}>Extra usage not enabled · /extra-usage to enable</Text></Box>; 278: $[0] = t1; 279: } else { 280: t1 = $[0]; 281: } 282: return t1; 283: } 284: return null; 285: } 286: if (extraUsage.monthly_limit === null) { 287: let t1; 288: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 289: t1 = <Box flexDirection="column"><Text bold={true}>{EXTRA_USAGE_SECTION_TITLE}</Text><Text dimColor={true}>Unlimited</Text></Box>; 290: $[1] = t1; 291: } else { 292: t1 = $[1]; 293: } 294: return t1; 295: } 296: if (typeof extraUsage.used_credits !== "number" || typeof extraUsage.utilization !== "number") { 297: return null; 298: } 299: const t1 = extraUsage.used_credits / 100; 300: let t2; 301: if ($[2] !== t1) { 302: t2 = formatCost(t1, 2); 303: $[2] = t1; 304: $[3] = t2; 305: } else { 306: t2 = $[3]; 307: } 308: const formattedUsedCredits = t2; 309: const t3 = extraUsage.monthly_limit / 100; 310: let t4; 311: if ($[4] !== t3) { 312: t4 = formatCost(t3, 2); 313: $[4] = t3; 314: $[5] = t4; 315: } else { 316: t4 = $[5]; 317: } 318: const formattedMonthlyLimit = t4; 319: let T0; 320: let t5; 321: let t6; 322: let t7; 323: if ($[6] !== extraUsage.utilization) { 324: const now = new Date(); 325: const oneMonthReset = new Date(now.getFullYear(), now.getMonth() + 1, 1); 326: T0 = LimitBar; 327: t7 = EXTRA_USAGE_SECTION_TITLE; 328: t5 = extraUsage.utilization; 329: t6 = oneMonthReset.toISOString(); 330: $[6] = extraUsage.utilization; 331: $[7] = T0; 332: $[8] = t5; 333: $[9] = t6; 334: $[10] = t7; 335: } else { 336: T0 = $[7]; 337: t5 = $[8]; 338: t6 = $[9]; 339: t7 = $[10]; 340: } 341: let t8; 342: if ($[11] !== t5 || $[12] !== t6) { 343: t8 = { 344: utilization: t5, 345: resets_at: t6 346: }; 347: $[11] = t5; 348: $[12] = t6; 349: $[13] = t8; 350: } else { 351: t8 = $[13]; 352: } 353: const t9 = `${formattedUsedCredits} / ${formattedMonthlyLimit} spent`; 354: let t10; 355: if ($[14] !== T0 || $[15] !== maxWidth || $[16] !== t7 || $[17] !== t8 || $[18] !== t9) { 356: t10 = <T0 title={t7} limit={t8} showTimeInReset={false} extraSubtext={t9} maxWidth={maxWidth} />; 357: $[14] = T0; 358: $[15] = maxWidth; 359: $[16] = t7; 360: $[17] = t8; 361: $[18] = t9; 362: $[19] = t10; 363: } else { 364: t10 = $[19]; 365: } 366: return t10; 367: }

File: src/components/shell/ExpandShellOutputContext.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useContext } from 'react'; 4: const ExpandShellOutputContext = React.createContext(false); 5: export function ExpandShellOutputProvider(t0) { 6: const $ = _c(2); 7: const { 8: children 9: } = t0; 10: let t1; 11: if ($[0] !== children) { 12: t1 = <ExpandShellOutputContext.Provider value={true}>{children}</ExpandShellOutputContext.Provider>; 13: $[0] = children; 14: $[1] = t1; 15: } else { 16: t1 = $[1]; 17: } 18: return t1; 19: } 20: export function useExpandShellOutput() { 21: return useContext(ExpandShellOutputContext); 22: }

File: src/components/shell/OutputLine.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { useMemo } from 'react'; 4: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 5: import { Ansi, Text } from '../../ink.js'; 6: import { createHyperlink } from '../../utils/hyperlink.js'; 7: import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'; 8: import { renderTruncatedContent } from '../../utils/terminal.js'; 9: import { MessageResponse } from '../MessageResponse.js'; 10: import { InVirtualListContext } from '../messageActions.js'; 11: import { useExpandShellOutput } from './ExpandShellOutputContext.js'; 12: export function tryFormatJson(line: string): string { 13: try { 14: const parsed = jsonParse(line); 15: const stringified = jsonStringify(parsed); 16: const normalizedOriginal = line.replace(/\\\//g, '/').replace(/\s+/g, ''); 17: const normalizedStringified = stringified.replace(/\s+/g, ''); 18: if (normalizedOriginal !== normalizedStringified) { 19: // Precision loss detected - return original line unformatted 20: return line; 21: } 22: return jsonStringify(parsed, null, 2); 23: } catch { 24: return line; 25: } 26: } 27: const MAX_JSON_FORMAT_LENGTH = 10_000; 28: export function tryJsonFormatContent(content: string): string { 29: if (content.length > MAX_JSON_FORMAT_LENGTH) { 30: return content; 31: } 32: const allLines = content.split('\n'); 33: return allLines.map(tryFormatJson).join('\n'); 34: } 35: const URL_IN_JSON = /https?:\/\/[^\s"'<>\\]+/g; 36: export function linkifyUrlsInText(content: string): string { 37: return content.replace(URL_IN_JSON, url => createHyperlink(url)); 38: } 39: export function OutputLine(t0) { 40: const $ = _c(11); 41: const { 42: content, 43: verbose, 44: isError, 45: isWarning, 46: linkifyUrls 47: } = t0; 48: const { 49: columns 50: } = useTerminalSize(); 51: const expandShellOutput = useExpandShellOutput(); 52: const inVirtualList = React.useContext(InVirtualListContext); 53: const shouldShowFull = verbose || expandShellOutput; 54: let t1; 55: if ($[0] !== columns || $[1] !== content || $[2] !== inVirtualList || $[3] !== linkifyUrls || $[4] !== shouldShowFull) { 56: bb0: { 57: let formatted = tryJsonFormatContent(content); 58: if (linkifyUrls) { 59: formatted = linkifyUrlsInText(formatted); 60: } 61: if (shouldShowFull) { 62: t1 = stripUnderlineAnsi(formatted); 63: break bb0; 64: } 65: t1 = stripUnderlineAnsi(renderTruncatedContent(formatted, columns, inVirtualList)); 66: } 67: $[0] = columns; 68: $[1] = content; 69: $[2] = inVirtualList; 70: $[3] = linkifyUrls; 71: $[4] = shouldShowFull; 72: $[5] = t1; 73: } else { 74: t1 = $[5]; 75: } 76: const formattedContent = t1; 77: const color = isError ? "error" : isWarning ? "warning" : undefined; 78: let t2; 79: if ($[6] !== formattedContent) { 80: t2 = <Ansi>{formattedContent}</Ansi>; 81: $[6] = formattedContent; 82: $[7] = t2; 83: } else { 84: t2 = $[7]; 85: } 86: let t3; 87: if ($[8] !== color || $[9] !== t2) { 88: t3 = <MessageResponse><Text color={color}>{t2}</Text></MessageResponse>; 89: $[8] = color; 90: $[9] = t2; 91: $[10] = t3; 92: } else { 93: t3 = $[10]; 94: } 95: return t3; 96: } 97: /** 98: * Underline ANSI codes in particular tend to leak out for some reason. I wasn't 99: * able to figure out why, or why emitting a reset ANSI code wasn't enough to 100: * prevent them from leaking. I also didn't want to strip all ANSI codes with 101: * stripAnsi(), because we used to do that and people complained about losing 102: * all formatting. So we just strip the underline ANSI codes specifically. 103: */ 104: export function stripUnderlineAnsi(content: string): string { 105: return content.replace( 106: /\u001b\[([0-9]+;)*4(;[0-9]+)*m|\u001b\[4(;[0-9]+)*m|\u001b\[([0-9]+;)*4m/g, ''); 107: }

File: src/components/shell/ShellProgressMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import stripAnsi from 'strip-ansi'; 4: import { Box, Text } from '../../ink.js'; 5: import { formatFileSize } from '../../utils/format.js'; 6: import { MessageResponse } from '../MessageResponse.js'; 7: import { OffscreenFreeze } from '../OffscreenFreeze.js'; 8: import { ShellTimeDisplay } from './ShellTimeDisplay.js'; 9: type Props = { 10: output: string; 11: fullOutput: string; 12: elapsedTimeSeconds?: number; 13: totalLines?: number; 14: totalBytes?: number; 15: timeoutMs?: number; 16: taskId?: string; 17: verbose: boolean; 18: }; 19: export function ShellProgressMessage(t0) { 20: const $ = _c(30); 21: const { 22: output, 23: fullOutput, 24: elapsedTimeSeconds, 25: totalLines, 26: totalBytes, 27: timeoutMs, 28: verbose 29: } = t0; 30: let t1; 31: if ($[0] !== fullOutput) { 32: t1 = stripAnsi(fullOutput.trim()); 33: $[0] = fullOutput; 34: $[1] = t1; 35: } else { 36: t1 = $[1]; 37: } 38: const strippedFullOutput = t1; 39: let lines; 40: let t2; 41: if ($[2] !== output || $[3] !== strippedFullOutput || $[4] !== verbose) { 42: const strippedOutput = stripAnsi(output.trim()); 43: lines = strippedOutput.split("\n").filter(_temp); 44: t2 = verbose ? strippedFullOutput : lines.slice(-5).join("\n"); 45: $[2] = output; 46: $[3] = strippedFullOutput; 47: $[4] = verbose; 48: $[5] = lines; 49: $[6] = t2; 50: } else { 51: lines = $[5]; 52: t2 = $[6]; 53: } 54: const displayLines = t2; 55: if (!lines.length) { 56: let t3; 57: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 58: t3 = <Text dimColor={true}>Running… </Text>; 59: $[7] = t3; 60: } else { 61: t3 = $[7]; 62: } 63: let t4; 64: if ($[8] !== elapsedTimeSeconds || $[9] !== timeoutMs) { 65: t4 = <MessageResponse><OffscreenFreeze>{t3}<ShellTimeDisplay elapsedTimeSeconds={elapsedTimeSeconds} timeoutMs={timeoutMs} /></OffscreenFreeze></MessageResponse>; 66: $[8] = elapsedTimeSeconds; 67: $[9] = timeoutMs; 68: $[10] = t4; 69: } else { 70: t4 = $[10]; 71: } 72: return t4; 73: } 74: const extraLines = totalLines ? Math.max(0, totalLines - 5) : 0; 75: let lineStatus = ""; 76: if (!verbose && totalBytes && totalLines) { 77: lineStatus = `~${totalLines} lines`; 78: } else { 79: if (!verbose && extraLines > 0) { 80: lineStatus = `+${extraLines} lines`; 81: } 82: } 83: const t3 = verbose ? undefined : Math.min(5, lines.length); 84: let t4; 85: if ($[11] !== displayLines) { 86: t4 = <Text dimColor={true}>{displayLines}</Text>; 87: $[11] = displayLines; 88: $[12] = t4; 89: } else { 90: t4 = $[12]; 91: } 92: let t5; 93: if ($[13] !== t3 || $[14] !== t4) { 94: t5 = <Box height={t3} flexDirection="column" overflow="hidden">{t4}</Box>; 95: $[13] = t3; 96: $[14] = t4; 97: $[15] = t5; 98: } else { 99: t5 = $[15]; 100: } 101: let t6; 102: if ($[16] !== lineStatus) { 103: t6 = lineStatus ? <Text dimColor={true}>{lineStatus}</Text> : null; 104: $[16] = lineStatus; 105: $[17] = t6; 106: } else { 107: t6 = $[17]; 108: } 109: let t7; 110: if ($[18] !== elapsedTimeSeconds || $[19] !== timeoutMs) { 111: t7 = <ShellTimeDisplay elapsedTimeSeconds={elapsedTimeSeconds} timeoutMs={timeoutMs} />; 112: $[18] = elapsedTimeSeconds; 113: $[19] = timeoutMs; 114: $[20] = t7; 115: } else { 116: t7 = $[20]; 117: } 118: let t8; 119: if ($[21] !== totalBytes) { 120: t8 = totalBytes ? <Text dimColor={true}>{formatFileSize(totalBytes)}</Text> : null; 121: $[21] = totalBytes; 122: $[22] = t8; 123: } else { 124: t8 = $[22]; 125: } 126: let t9; 127: if ($[23] !== t6 || $[24] !== t7 || $[25] !== t8) { 128: t9 = <Box flexDirection="row" gap={1}>{t6}{t7}{t8}</Box>; 129: $[23] = t6; 130: $[24] = t7; 131: $[25] = t8; 132: $[26] = t9; 133: } else { 134: t9 = $[26]; 135: } 136: let t10; 137: if ($[27] !== t5 || $[28] !== t9) { 138: t10 = <MessageResponse><OffscreenFreeze><Box flexDirection="column">{t5}{t9}</Box></OffscreenFreeze></MessageResponse>; 139: $[27] = t5; 140: $[28] = t9; 141: $[29] = t10; 142: } else { 143: t10 = $[29]; 144: } 145: return t10; 146: } 147: function _temp(line) { 148: return line; 149: }

File: src/components/shell/ShellTimeDisplay.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Text } from '../../ink.js'; 4: import { formatDuration } from '../../utils/format.js'; 5: type Props = { 6: elapsedTimeSeconds?: number; 7: timeoutMs?: number; 8: }; 9: export function ShellTimeDisplay(t0) { 10: const $ = _c(10); 11: const { 12: elapsedTimeSeconds, 13: timeoutMs 14: } = t0; 15: if (elapsedTimeSeconds === undefined && !timeoutMs) { 16: return null; 17: } 18: let t1; 19: if ($[0] !== timeoutMs) { 20: t1 = timeoutMs ? formatDuration(timeoutMs, { 21: hideTrailingZeros: true 22: }) : undefined; 23: $[0] = timeoutMs; 24: $[1] = t1; 25: } else { 26: t1 = $[1]; 27: } 28: const timeout = t1; 29: if (elapsedTimeSeconds === undefined) { 30: const t2 = `(timeout ${timeout})`; 31: let t3; 32: if ($[2] !== t2) { 33: t3 = <Text dimColor={true}>{t2}</Text>; 34: $[2] = t2; 35: $[3] = t3; 36: } else { 37: t3 = $[3]; 38: } 39: return t3; 40: } 41: const t2 = elapsedTimeSeconds * 1000; 42: let t3; 43: if ($[4] !== t2) { 44: t3 = formatDuration(t2); 45: $[4] = t2; 46: $[5] = t3; 47: } else { 48: t3 = $[5]; 49: } 50: const elapsed = t3; 51: if (timeout) { 52: const t4 = `(${elapsed} · timeout ${timeout})`; 53: let t5; 54: if ($[6] !== t4) { 55: t5 = <Text dimColor={true}>{t4}</Text>; 56: $[6] = t4; 57: $[7] = t5; 58: } else { 59: t5 = $[7]; 60: } 61: return t5; 62: } 63: const t4 = `(${elapsed})`; 64: let t5; 65: if ($[8] !== t4) { 66: t5 = <Text dimColor={true}>{t4}</Text>; 67: $[8] = t4; 68: $[9] = t5; 69: } else { 70: t5 = $[9]; 71: } 72: return t5; 73: }

File: src/components/skills/SkillsMenu.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import capitalize from 'lodash-es/capitalize.js'; 3: import * as React from 'react'; 4: import { useMemo } from 'react'; 5: import { type Command, type CommandBase, type CommandResultDisplay, getCommandName, type PromptCommand } from '../../commands.js'; 6: import { Box, Text } from '../../ink.js'; 7: import { estimateSkillFrontmatterTokens, getSkillsPath } from '../../skills/loadSkillsDir.js'; 8: import { getDisplayPath } from '../../utils/file.js'; 9: import { formatTokens } from '../../utils/format.js'; 10: import { getSettingSourceName, type SettingSource } from '../../utils/settings/constants.js'; 11: import { plural } from '../../utils/stringUtils.js'; 12: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 13: import { Dialog } from '../design-system/Dialog.js'; 14: type SkillCommand = CommandBase & PromptCommand; 15: type SkillSource = SettingSource | 'plugin' | 'mcp'; 16: type Props = { 17: onExit: (result?: string, options?: { 18: display?: CommandResultDisplay; 19: }) => void; 20: commands: Command[]; 21: }; 22: function getSourceTitle(source: SkillSource): string { 23: if (source === 'plugin') { 24: return 'Plugin skills'; 25: } 26: if (source === 'mcp') { 27: return 'MCP skills'; 28: } 29: return `${capitalize(getSettingSourceName(source))} skills`; 30: } 31: function getSourceSubtitle(source: SkillSource, skills: SkillCommand[]): string | undefined { 32: if (source === 'mcp') { 33: const servers = [...new Set(skills.map(s => { 34: const idx = s.name.indexOf(':'); 35: return idx > 0 ? s.name.slice(0, idx) : null; 36: }).filter((n): n is string => n != null))]; 37: return servers.length > 0 ? servers.join(', ') : undefined; 38: } 39: const skillsPath = getDisplayPath(getSkillsPath(source, 'skills')); 40: const hasCommandsSkills = skills.some(s => s.loadedFrom === 'commands_DEPRECATED'); 41: return hasCommandsSkills ? `${skillsPath}, ${getDisplayPath(getSkillsPath(source, 'commands'))}` : skillsPath; 42: } 43: export function SkillsMenu(t0) { 44: const $ = _c(35); 45: const { 46: onExit, 47: commands 48: } = t0; 49: let t1; 50: if ($[0] !== commands) { 51: t1 = commands.filter(_temp); 52: $[0] = commands; 53: $[1] = t1; 54: } else { 55: t1 = $[1]; 56: } 57: const skills = t1; 58: let groups; 59: if ($[2] !== skills) { 60: groups = { 61: policySettings: [], 62: userSettings: [], 63: projectSettings: [], 64: localSettings: [], 65: flagSettings: [], 66: plugin: [], 67: mcp: [] 68: }; 69: for (const skill of skills) { 70: const source = skill.source as SkillSource; 71: if (source in groups) { 72: groups[source].push(skill); 73: } 74: } 75: for (const group of Object.values(groups)) { 76: group.sort(_temp2); 77: } 78: $[2] = skills; 79: $[3] = groups; 80: } else { 81: groups = $[3]; 82: } 83: const skillsBySource = groups; 84: let t2; 85: if ($[4] !== onExit) { 86: t2 = () => { 87: onExit("Skills dialog dismissed", { 88: display: "system" 89: }); 90: }; 91: $[4] = onExit; 92: $[5] = t2; 93: } else { 94: t2 = $[5]; 95: } 96: const handleCancel = t2; 97: if (skills.length === 0) { 98: let t3; 99: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 100: t3 = <Text dimColor={true}>Create skills in .claude/skills/ or ~/.claude/skills/</Text>; 101: $[6] = t3; 102: } else { 103: t3 = $[6]; 104: } 105: let t4; 106: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 107: t4 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>; 108: $[7] = t4; 109: } else { 110: t4 = $[7]; 111: } 112: let t5; 113: if ($[8] !== handleCancel) { 114: t5 = <Dialog title="Skills" subtitle="No skills found" onCancel={handleCancel} hideInputGuide={true}>{t3}{t4}</Dialog>; 115: $[8] = handleCancel; 116: $[9] = t5; 117: } else { 118: t5 = $[9]; 119: } 120: return t5; 121: } 122: const renderSkill = _temp3; 123: let t3; 124: if ($[10] !== skillsBySource) { 125: t3 = source_0 => { 126: const groupSkills = skillsBySource[source_0]; 127: if (groupSkills.length === 0) { 128: return null; 129: } 130: const title = getSourceTitle(source_0); 131: const subtitle = getSourceSubtitle(source_0, groupSkills); 132: return <Box flexDirection="column" key={source_0}><Box><Text bold={true} dimColor={true}>{title}</Text>{subtitle && <Text dimColor={true}> ({subtitle})</Text>}</Box>{groupSkills.map(skill_1 => renderSkill(skill_1))}</Box>; 133: }; 134: $[10] = skillsBySource; 135: $[11] = t3; 136: } else { 137: t3 = $[11]; 138: } 139: const renderSkillGroup = t3; 140: const t4 = skills.length; 141: let t5; 142: if ($[12] !== skills.length) { 143: t5 = plural(skills.length, "skill"); 144: $[12] = skills.length; 145: $[13] = t5; 146: } else { 147: t5 = $[13]; 148: } 149: const t6 = `${t4} ${t5}`; 150: let t7; 151: if ($[14] !== renderSkillGroup) { 152: t7 = renderSkillGroup("projectSettings"); 153: $[14] = renderSkillGroup; 154: $[15] = t7; 155: } else { 156: t7 = $[15]; 157: } 158: let t8; 159: if ($[16] !== renderSkillGroup) { 160: t8 = renderSkillGroup("userSettings"); 161: $[16] = renderSkillGroup; 162: $[17] = t8; 163: } else { 164: t8 = $[17]; 165: } 166: let t9; 167: if ($[18] !== renderSkillGroup) { 168: t9 = renderSkillGroup("policySettings"); 169: $[18] = renderSkillGroup; 170: $[19] = t9; 171: } else { 172: t9 = $[19]; 173: } 174: let t10; 175: if ($[20] !== renderSkillGroup) { 176: t10 = renderSkillGroup("plugin"); 177: $[20] = renderSkillGroup; 178: $[21] = t10; 179: } else { 180: t10 = $[21]; 181: } 182: let t11; 183: if ($[22] !== renderSkillGroup) { 184: t11 = renderSkillGroup("mcp"); 185: $[22] = renderSkillGroup; 186: $[23] = t11; 187: } else { 188: t11 = $[23]; 189: } 190: let t12; 191: if ($[24] !== t10 || $[25] !== t11 || $[26] !== t7 || $[27] !== t8 || $[28] !== t9) { 192: t12 = <Box flexDirection="column" gap={1}>{t7}{t8}{t9}{t10}{t11}</Box>; 193: $[24] = t10; 194: $[25] = t11; 195: $[26] = t7; 196: $[27] = t8; 197: $[28] = t9; 198: $[29] = t12; 199: } else { 200: t12 = $[29]; 201: } 202: let t13; 203: if ($[30] === Symbol.for("react.memo_cache_sentinel")) { 204: t13 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>; 205: $[30] = t13; 206: } else { 207: t13 = $[30]; 208: } 209: let t14; 210: if ($[31] !== handleCancel || $[32] !== t12 || $[33] !== t6) { 211: t14 = <Dialog title="Skills" subtitle={t6} onCancel={handleCancel} hideInputGuide={true}>{t12}{t13}</Dialog>; 212: $[31] = handleCancel; 213: $[32] = t12; 214: $[33] = t6; 215: $[34] = t14; 216: } else { 217: t14 = $[34]; 218: } 219: return t14; 220: } 221: function _temp3(skill_0) { 222: const estimatedTokens = estimateSkillFrontmatterTokens(skill_0); 223: const tokenDisplay = `~${formatTokens(estimatedTokens)}`; 224: const pluginName = skill_0.source === "plugin" ? skill_0.pluginInfo?.pluginManifest.name : undefined; 225: return <Box key={`${skill_0.name}-${skill_0.source}`}><Text>{getCommandName(skill_0)}</Text><Text dimColor={true}>{pluginName ? ` · ${pluginName}` : ""} · {tokenDisplay} description tokens</Text></Box>; 226: } 227: function _temp2(a, b) { 228: return getCommandName(a).localeCompare(getCommandName(b)); 229: } 230: function _temp(cmd) { 231: return cmd.type === "prompt" && (cmd.loadedFrom === "skills" || cmd.loadedFrom === "commands_DEPRECATED" || cmd.loadedFrom === "plugin" || cmd.loadedFrom === "mcp"); 232: }

File: src/components/Spinner/FlashingChar.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Text, useTheme } from '../../ink.js'; 4: import { getTheme, type Theme } from '../../utils/theme.js'; 5: import { interpolateColor, parseRGB, toRGBColor } from './utils.js'; 6: type Props = { 7: char: string; 8: flashOpacity: number; 9: messageColor: keyof Theme; 10: shimmerColor: keyof Theme; 11: }; 12: export function FlashingChar(t0) { 13: const $ = _c(9); 14: const { 15: char, 16: flashOpacity, 17: messageColor, 18: shimmerColor 19: } = t0; 20: const [themeName] = useTheme(); 21: let t1; 22: if ($[0] !== char || $[1] !== flashOpacity || $[2] !== messageColor || $[3] !== shimmerColor || $[4] !== themeName) { 23: t1 = Symbol.for("react.early_return_sentinel"); 24: bb0: { 25: const theme = getTheme(themeName); 26: const baseColorStr = theme[messageColor]; 27: const shimmerColorStr = theme[shimmerColor]; 28: const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null; 29: const shimmerRGB = shimmerColorStr ? parseRGB(shimmerColorStr) : null; 30: if (baseRGB && shimmerRGB) { 31: const interpolated = interpolateColor(baseRGB, shimmerRGB, flashOpacity); 32: t1 = <Text color={toRGBColor(interpolated)}>{char}</Text>; 33: break bb0; 34: } 35: } 36: $[0] = char; 37: $[1] = flashOpacity; 38: $[2] = messageColor; 39: $[3] = shimmerColor; 40: $[4] = themeName; 41: $[5] = t1; 42: } else { 43: t1 = $[5]; 44: } 45: if (t1 !== Symbol.for("react.early_return_sentinel")) { 46: return t1; 47: } 48: const shouldUseShimmer = flashOpacity > 0.5; 49: const t2 = shouldUseShimmer ? shimmerColor : messageColor; 50: let t3; 51: if ($[6] !== char || $[7] !== t2) { 52: t3 = <Text color={t2}>{char}</Text>; 53: $[6] = char; 54: $[7] = t2; 55: $[8] = t3; 56: } else { 57: t3 = $[8]; 58: } 59: return t3; 60: }

File: src/components/Spinner/GlimmerMessage.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { stringWidth } from '../../ink/stringWidth.js'; 4: import { Text, useTheme } from '../../ink.js'; 5: import { getGraphemeSegmenter } from '../../utils/intl.js'; 6: import { getTheme, type Theme } from '../../utils/theme.js'; 7: import type { SpinnerMode } from './types.js'; 8: import { interpolateColor, parseRGB, toRGBColor } from './utils.js'; 9: type Props = { 10: message: string; 11: mode: SpinnerMode; 12: messageColor: keyof Theme; 13: glimmerIndex: number; 14: flashOpacity: number; 15: shimmerColor: keyof Theme; 16: stalledIntensity?: number; 17: }; 18: const ERROR_RED = { 19: r: 171, 20: g: 43, 21: b: 63 22: }; 23: export function GlimmerMessage(t0) { 24: const $ = _c(75); 25: const { 26: message, 27: mode, 28: messageColor, 29: glimmerIndex, 30: flashOpacity, 31: shimmerColor, 32: stalledIntensity: t1 33: } = t0; 34: const stalledIntensity = t1 === undefined ? 0 : t1; 35: const [themeName] = useTheme(); 36: let messageWidth; 37: let segments; 38: let t2; 39: if ($[0] !== flashOpacity || $[1] !== message || $[2] !== messageColor || $[3] !== mode || $[4] !== shimmerColor || $[5] !== stalledIntensity || $[6] !== themeName) { 40: t2 = Symbol.for("react.early_return_sentinel"); 41: bb0: { 42: const theme = getTheme(themeName); 43: let segs; 44: if ($[10] !== message) { 45: segs = []; 46: for (const { 47: segment 48: } of getGraphemeSegmenter().segment(message)) { 49: segs.push({ 50: segment, 51: width: stringWidth(segment) 52: }); 53: } 54: $[10] = message; 55: $[11] = segs; 56: } else { 57: segs = $[11]; 58: } 59: let t3; 60: if ($[12] !== message) { 61: t3 = stringWidth(message); 62: $[12] = message; 63: $[13] = t3; 64: } else { 65: t3 = $[13]; 66: } 67: let t4; 68: if ($[14] !== segs || $[15] !== t3) { 69: t4 = { 70: segments: segs, 71: messageWidth: t3 72: }; 73: $[14] = segs; 74: $[15] = t3; 75: $[16] = t4; 76: } else { 77: t4 = $[16]; 78: } 79: ({ 80: segments, 81: messageWidth 82: } = t4); 83: if (!message) { 84: t2 = null; 85: break bb0; 86: } 87: if (stalledIntensity > 0) { 88: const baseColorStr = theme[messageColor]; 89: const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null; 90: if (baseRGB) { 91: const interpolated = interpolateColor(baseRGB, ERROR_RED, stalledIntensity); 92: const color = toRGBColor(interpolated); 93: let t5; 94: if ($[17] !== color) { 95: t5 = <Text color={color}> </Text>; 96: $[17] = color; 97: $[18] = t5; 98: } else { 99: t5 = $[18]; 100: } 101: t2 = <><Text color={color}>{message}</Text>{t5}</>; 102: break bb0; 103: } 104: const color_0 = stalledIntensity > 0.5 ? "error" : messageColor; 105: let t5; 106: if ($[19] !== color_0 || $[20] !== message) { 107: t5 = <Text color={color_0}>{message}</Text>; 108: $[19] = color_0; 109: $[20] = message; 110: $[21] = t5; 111: } else { 112: t5 = $[21]; 113: } 114: let t6; 115: if ($[22] !== color_0) { 116: t6 = <Text color={color_0}> </Text>; 117: $[22] = color_0; 118: $[23] = t6; 119: } else { 120: t6 = $[23]; 121: } 122: let t7; 123: if ($[24] !== t5 || $[25] !== t6) { 124: t7 = <>{t5}{t6}</>; 125: $[24] = t5; 126: $[25] = t6; 127: $[26] = t7; 128: } else { 129: t7 = $[26]; 130: } 131: t2 = t7; 132: break bb0; 133: } 134: if (mode === "tool-use") { 135: const baseColorStr_0 = theme[messageColor]; 136: const shimmerColorStr = theme[shimmerColor]; 137: const baseRGB_0 = baseColorStr_0 ? parseRGB(baseColorStr_0) : null; 138: const shimmerRGB = shimmerColorStr ? parseRGB(shimmerColorStr) : null; 139: if (baseRGB_0 && shimmerRGB) { 140: const interpolated_0 = interpolateColor(baseRGB_0, shimmerRGB, flashOpacity); 141: const t5 = <Text color={toRGBColor(interpolated_0)}>{message}</Text>; 142: let t6; 143: if ($[27] !== messageColor) { 144: t6 = <Text color={messageColor}> </Text>; 145: $[27] = messageColor; 146: $[28] = t6; 147: } else { 148: t6 = $[28]; 149: } 150: let t7; 151: if ($[29] !== t5 || $[30] !== t6) { 152: t7 = <>{t5}{t6}</>; 153: $[29] = t5; 154: $[30] = t6; 155: $[31] = t7; 156: } else { 157: t7 = $[31]; 158: } 159: t2 = t7; 160: break bb0; 161: } 162: const color_1 = flashOpacity > 0.5 ? shimmerColor : messageColor; 163: let t5; 164: if ($[32] !== color_1 || $[33] !== message) { 165: t5 = <Text color={color_1}>{message}</Text>; 166: $[32] = color_1; 167: $[33] = message; 168: $[34] = t5; 169: } else { 170: t5 = $[34]; 171: } 172: let t6; 173: if ($[35] !== messageColor) { 174: t6 = <Text color={messageColor}> </Text>; 175: $[35] = messageColor; 176: $[36] = t6; 177: } else { 178: t6 = $[36]; 179: } 180: let t7; 181: if ($[37] !== t5 || $[38] !== t6) { 182: t7 = <>{t5}{t6}</>; 183: $[37] = t5; 184: $[38] = t6; 185: $[39] = t7; 186: } else { 187: t7 = $[39]; 188: } 189: t2 = t7; 190: break bb0; 191: } 192: } 193: $[0] = flashOpacity; 194: $[1] = message; 195: $[2] = messageColor; 196: $[3] = mode; 197: $[4] = shimmerColor; 198: $[5] = stalledIntensity; 199: $[6] = themeName; 200: $[7] = messageWidth; 201: $[8] = segments; 202: $[9] = t2; 203: } else { 204: messageWidth = $[7]; 205: segments = $[8]; 206: t2 = $[9]; 207: } 208: if (t2 !== Symbol.for("react.early_return_sentinel")) { 209: return t2; 210: } 211: const shimmerStart = glimmerIndex - 1; 212: const shimmerEnd = glimmerIndex + 1; 213: if (shimmerStart >= messageWidth || shimmerEnd < 0) { 214: let t3; 215: if ($[40] !== message || $[41] !== messageColor) { 216: t3 = <Text color={messageColor}>{message}</Text>; 217: $[40] = message; 218: $[41] = messageColor; 219: $[42] = t3; 220: } else { 221: t3 = $[42]; 222: } 223: let t4; 224: if ($[43] !== messageColor) { 225: t4 = <Text color={messageColor}> </Text>; 226: $[43] = messageColor; 227: $[44] = t4; 228: } else { 229: t4 = $[44]; 230: } 231: let t5; 232: if ($[45] !== t3 || $[46] !== t4) { 233: t5 = <>{t3}{t4}</>; 234: $[45] = t3; 235: $[46] = t4; 236: $[47] = t5; 237: } else { 238: t5 = $[47]; 239: } 240: return t5; 241: } 242: const clampedStart = Math.max(0, shimmerStart); 243: let colPos = 0; 244: let before = ""; 245: let shim = ""; 246: let after = ""; 247: if ($[48] !== after || $[49] !== before || $[50] !== clampedStart || $[51] !== colPos || $[52] !== segments || $[53] !== shim || $[54] !== shimmerEnd) { 248: for (const { 249: segment: segment_0, 250: width 251: } of segments) { 252: if (colPos + width <= clampedStart) { 253: before = before + segment_0; 254: } else { 255: if (colPos > shimmerEnd) { 256: after = after + segment_0; 257: } else { 258: shim = shim + segment_0; 259: } 260: } 261: colPos = colPos + width; 262: } 263: $[48] = after; 264: $[49] = before; 265: $[50] = clampedStart; 266: $[51] = colPos; 267: $[52] = segments; 268: $[53] = shim; 269: $[54] = shimmerEnd; 270: $[55] = before; 271: $[56] = after; 272: $[57] = shim; 273: $[58] = colPos; 274: } else { 275: before = $[55]; 276: after = $[56]; 277: shim = $[57]; 278: colPos = $[58]; 279: } 280: let t3; 281: if ($[59] !== before || $[60] !== messageColor) { 282: t3 = before && <Text color={messageColor}>{before}</Text>; 283: $[59] = before; 284: $[60] = messageColor; 285: $[61] = t3; 286: } else { 287: t3 = $[61]; 288: } 289: let t4; 290: if ($[62] !== shim || $[63] !== shimmerColor) { 291: t4 = <Text color={shimmerColor}>{shim}</Text>; 292: $[62] = shim; 293: $[63] = shimmerColor; 294: $[64] = t4; 295: } else { 296: t4 = $[64]; 297: } 298: let t5; 299: if ($[65] !== after || $[66] !== messageColor) { 300: t5 = after && <Text color={messageColor}>{after}</Text>; 301: $[65] = after; 302: $[66] = messageColor; 303: $[67] = t5; 304: } else { 305: t5 = $[67]; 306: } 307: let t6; 308: if ($[68] !== messageColor) { 309: t6 = <Text color={messageColor}> </Text>; 310: $[68] = messageColor; 311: $[69] = t6; 312: } else { 313: t6 = $[69]; 314: } 315: let t7; 316: if ($[70] !== t3 || $[71] !== t4 || $[72] !== t5 || $[73] !== t6) { 317: t7 = <>{t3}{t4}{t5}{t6}</>; 318: $[70] = t3; 319: $[71] = t4; 320: $[72] = t5; 321: $[73] = t6; 322: $[74] = t7; 323: } else { 324: t7 = $[74]; 325: } 326: return t7; 327: }

File: src/components/Spinner/index.ts

typescript 1: export { FlashingChar } from './FlashingChar.js' 2: export { GlimmerMessage } from './GlimmerMessage.js' 3: export { ShimmerChar } from './ShimmerChar.js' 4: export { SpinnerGlyph } from './SpinnerGlyph.js' 5: export type { SpinnerMode } from './types.js' 6: export { useShimmerAnimation } from './useShimmerAnimation.js' 7: export { useStalledAnimation } from './useStalledAnimation.js' 8: export { getDefaultCharacters, interpolateColor } from './utils.js'

File: src/components/Spinner/ShimmerChar.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Text } from '../../ink.js'; 4: import type { Theme } from '../../utils/theme.js'; 5: type Props = { 6: char: string; 7: index: number; 8: glimmerIndex: number; 9: messageColor: keyof Theme; 10: shimmerColor: keyof Theme; 11: }; 12: export function ShimmerChar(t0) { 13: const $ = _c(3); 14: const { 15: char, 16: index, 17: glimmerIndex, 18: messageColor, 19: shimmerColor 20: } = t0; 21: const isHighlighted = index === glimmerIndex; 22: const isNearHighlight = Math.abs(index - glimmerIndex) === 1; 23: const shouldUseShimmer = isHighlighted || isNearHighlight; 24: const t1 = shouldUseShimmer ? shimmerColor : messageColor; 25: let t2; 26: if ($[0] !== char || $[1] !== t1) { 27: t2 = <Text color={t1}>{char}</Text>; 28: $[0] = char; 29: $[1] = t1; 30: $[2] = t2; 31: } else { 32: t2 = $[2]; 33: } 34: return t2; 35: }

File: src/components/Spinner/SpinnerAnimationRow.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { useMemo, useRef } from 'react'; 5: import { stringWidth } from '../../ink/stringWidth.js'; 6: import { Box, Text, useAnimationFrame } from '../../ink.js'; 7: import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'; 8: import { formatDuration, formatNumber } from '../../utils/format.js'; 9: import { toInkColor } from '../../utils/ink.js'; 10: import type { Theme } from '../../utils/theme.js'; 11: import { Byline } from '../design-system/Byline.js'; 12: import { GlimmerMessage } from './GlimmerMessage.js'; 13: import { SpinnerGlyph } from './SpinnerGlyph.js'; 14: import type { SpinnerMode } from './types.js'; 15: import { useStalledAnimation } from './useStalledAnimation.js'; 16: import { interpolateColor, toRGBColor } from './utils.js'; 17: const SEP_WIDTH = stringWidth(' · '); 18: const THINKING_BARE_WIDTH = stringWidth('thinking'); 19: const SHOW_TOKENS_AFTER_MS = 30_000; 20: const THINKING_INACTIVE = { 21: r: 153, 22: g: 153, 23: b: 153 24: }; 25: const THINKING_INACTIVE_SHIMMER = { 26: r: 185, 27: g: 185, 28: b: 185 29: }; 30: const THINKING_DELAY_MS = 3000; 31: const THINKING_GLOW_PERIOD_S = 2; 32: export type SpinnerAnimationRowProps = { 33: mode: SpinnerMode; 34: reducedMotion: boolean; 35: hasActiveTools: boolean; 36: responseLengthRef: React.RefObject<number>; 37: message: string; 38: messageColor: keyof Theme; 39: shimmerColor: keyof Theme; 40: overrideColor?: keyof Theme | null; 41: loadingStartTimeRef: React.RefObject<number>; 42: totalPausedMsRef: React.RefObject<number>; 43: pauseStartTimeRef: React.RefObject<number | null>; 44: spinnerSuffix?: string | null; 45: verbose: boolean; 46: columns: number; 47: hasRunningTeammates: boolean; 48: teammateTokens: number; 49: foregroundedTeammate: InProcessTeammateTaskState | undefined; 50: leaderIsIdle?: boolean; 51: thinkingStatus: 'thinking' | number | null; 52: effortSuffix: string; 53: }; 54: export function SpinnerAnimationRow({ 55: mode, 56: reducedMotion, 57: hasActiveTools, 58: responseLengthRef, 59: message, 60: messageColor, 61: shimmerColor, 62: overrideColor, 63: loadingStartTimeRef, 64: totalPausedMsRef, 65: pauseStartTimeRef, 66: spinnerSuffix, 67: verbose, 68: columns, 69: hasRunningTeammates, 70: teammateTokens, 71: foregroundedTeammate, 72: leaderIsIdle = false, 73: thinkingStatus, 74: effortSuffix 75: }: SpinnerAnimationRowProps): React.ReactNode { 76: const [viewportRef, time] = useAnimationFrame(reducedMotion ? null : 50); 77: const now = Date.now(); 78: const elapsedTimeMs = pauseStartTimeRef.current !== null ? pauseStartTimeRef.current - loadingStartTimeRef.current - totalPausedMsRef.current : now - loadingStartTimeRef.current - totalPausedMsRef.current; 79: const derivedStart = now - elapsedTimeMs; 80: const turnStartRef = useRef(derivedStart); 81: if (!hasRunningTeammates || derivedStart < turnStartRef.current) { 82: turnStartRef.current = derivedStart; 83: } 84: const currentResponseLength = responseLengthRef.current; 85: const { 86: isStalled, 87: stalledIntensity 88: } = useStalledAnimation(time, currentResponseLength, hasActiveTools || leaderIsIdle, reducedMotion); 89: const frame = reducedMotion ? 0 : Math.floor(time / 120); 90: const glimmerSpeed = mode === 'requesting' ? 50 : 200; 91: const glimmerMessageWidth = useMemo(() => stringWidth(message), [message]); 92: const cycleLength = glimmerMessageWidth + 20; 93: const cyclePosition = Math.floor(time / glimmerSpeed); 94: const glimmerIndex = reducedMotion ? -100 : isStalled ? -100 : mode === 'requesting' ? cyclePosition % cycleLength - 10 : glimmerMessageWidth + 10 - cyclePosition % cycleLength; 95: const flashOpacity = reducedMotion ? 0 : mode === 'tool-use' ? (Math.sin(time / 1000 * Math.PI) + 1) / 2 : 0; 96: const tokenCounterRef = useRef(currentResponseLength); 97: if (reducedMotion) { 98: tokenCounterRef.current = currentResponseLength; 99: } else { 100: const gap = currentResponseLength - tokenCounterRef.current; 101: if (gap > 0) { 102: let increment; 103: if (gap < 70) { 104: increment = 3; 105: } else if (gap < 200) { 106: increment = Math.max(8, Math.ceil(gap * 0.15)); 107: } else { 108: increment = 50; 109: } 110: tokenCounterRef.current = Math.min(tokenCounterRef.current + increment, currentResponseLength); 111: } 112: } 113: const displayedResponseLength = tokenCounterRef.current; 114: const leaderTokens = Math.round(displayedResponseLength / 4); 115: const effectiveElapsedMs = hasRunningTeammates ? Math.max(elapsedTimeMs, now - turnStartRef.current) : elapsedTimeMs; 116: const timerText = formatDuration(effectiveElapsedMs); 117: const timerWidth = stringWidth(timerText); 118: const totalTokens = foregroundedTeammate && !foregroundedTeammate.isIdle ? foregroundedTeammate.progress?.tokenCount ?? 0 : leaderTokens + teammateTokens; 119: const tokenCount = formatNumber(totalTokens); 120: const tokensText = hasRunningTeammates ? `${tokenCount} tokens` : `${figures.arrowDown} ${tokenCount} tokens`; 121: const tokensWidth = stringWidth(tokensText); 122: let thinkingText = thinkingStatus === 'thinking' ? `thinking${effortSuffix}` : typeof thinkingStatus === 'number' ? `thought for ${Math.max(1, Math.round(thinkingStatus / 1000))}s` : null; 123: let thinkingWidthValue = thinkingText ? stringWidth(thinkingText) : 0; 124: const messageWidth = glimmerMessageWidth + 2; 125: const sep = SEP_WIDTH; 126: const wantsThinking = thinkingStatus !== null; 127: const wantsTimerAndTokens = verbose || hasRunningTeammates || effectiveElapsedMs > SHOW_TOKENS_AFTER_MS; 128: const availableSpace = columns - messageWidth - 5; 129: let showThinking = wantsThinking && availableSpace > thinkingWidthValue; 130: if (!showThinking && wantsThinking && thinkingStatus === 'thinking' && effortSuffix) { 131: if (availableSpace > THINKING_BARE_WIDTH) { 132: thinkingText = 'thinking'; 133: thinkingWidthValue = THINKING_BARE_WIDTH; 134: showThinking = true; 135: } 136: } 137: const usedAfterThinking = showThinking ? thinkingWidthValue + sep : 0; 138: const showTimer = wantsTimerAndTokens && availableSpace > usedAfterThinking + timerWidth; 139: const usedAfterTimer = usedAfterThinking + (showTimer ? timerWidth + sep : 0); 140: const showTokens = wantsTimerAndTokens && totalTokens > 0 && availableSpace > usedAfterTimer + tokensWidth; 141: const thinkingOnly = showThinking && thinkingStatus === 'thinking' && !spinnerSuffix && !showTimer && !showTokens && true; 142: const thinkingElapsedSec = (time - THINKING_DELAY_MS) / 1000; 143: const thinkingOpacity = time < THINKING_DELAY_MS ? 0 : (Math.sin(thinkingElapsedSec * Math.PI * 2 / THINKING_GLOW_PERIOD_S) + 1) / 2; 144: const thinkingShimmerColor = toRGBColor(interpolateColor(THINKING_INACTIVE, THINKING_INACTIVE_SHIMMER, thinkingOpacity)); 145: const parts = [...(spinnerSuffix ? [<Text dimColor key="suffix"> 146: {spinnerSuffix} 147: </Text>] : []), ...(showTimer ? [<Text dimColor key="elapsedTime"> 148: {timerText} 149: </Text>] : []), ...(showTokens ? [<Box flexDirection="row" key="tokens"> 150: {!hasRunningTeammates && <SpinnerModeGlyph mode={mode} />} 151: <Text dimColor>{tokenCount} tokens</Text> 152: </Box>] : []), ...(showThinking && thinkingText ? [thinkingStatus === 'thinking' && !reducedMotion ? <Text key="thinking" color={thinkingShimmerColor}> 153: {thinkingOnly ? `(${thinkingText})` : thinkingText} 154: </Text> : <Text dimColor key="thinking"> 155: {thinkingText} 156: </Text>] : [])]; 157: const status = foregroundedTeammate && !foregroundedTeammate.isIdle ? <> 158: <Text dimColor>(esc to interrupt </Text> 159: <Text color={toInkColor(foregroundedTeammate.identity.color)}> 160: {foregroundedTeammate.identity.agentName} 161: </Text> 162: <Text dimColor>)</Text> 163: </> : !foregroundedTeammate && parts.length > 0 ? thinkingOnly ? <Byline>{parts}</Byline> : <> 164: <Text dimColor>(</Text> 165: <Byline>{parts}</Byline> 166: <Text dimColor>)</Text> 167: </> : null; 168: return <Box ref={viewportRef} flexDirection="row" flexWrap="wrap" marginTop={1} width="100%"> 169: <SpinnerGlyph frame={frame} messageColor={messageColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} reducedMotion={reducedMotion} time={time} /> 170: <GlimmerMessage message={message} mode={mode} messageColor={messageColor} glimmerIndex={glimmerIndex} flashOpacity={flashOpacity} shimmerColor={shimmerColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} /> 171: {status} 172: </Box>; 173: } 174: function SpinnerModeGlyph(t0) { 175: const $ = _c(2); 176: const { 177: mode 178: } = t0; 179: switch (mode) { 180: case "tool-input": 181: case "tool-use": 182: case "responding": 183: case "thinking": 184: { 185: let t1; 186: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 187: t1 = <Box width={2}><Text dimColor={true}>{figures.arrowDown}</Text></Box>; 188: $[0] = t1; 189: } else { 190: t1 = $[0]; 191: } 192: return t1; 193: } 194: case "requesting": 195: { 196: let t1; 197: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 198: t1 = <Box width={2}><Text dimColor={true}>{figures.arrowUp}</Text></Box>; 199: $[1] = t1; 200: } else { 201: t1 = $[1]; 202: } 203: return t1; 204: } 205: } 206: }

File: src/components/Spinner/SpinnerGlyph.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text, useTheme } from '../../ink.js'; 4: import { getTheme, type Theme } from '../../utils/theme.js'; 5: import { getDefaultCharacters, interpolateColor, parseRGB, toRGBColor } from './utils.js'; 6: const DEFAULT_CHARACTERS = getDefaultCharacters(); 7: const SPINNER_FRAMES = [...DEFAULT_CHARACTERS, ...[...DEFAULT_CHARACTERS].reverse()]; 8: const REDUCED_MOTION_DOT = '●'; 9: const REDUCED_MOTION_CYCLE_MS = 2000; 10: const ERROR_RED = { 11: r: 171, 12: g: 43, 13: b: 63 14: }; 15: type Props = { 16: frame: number; 17: messageColor: keyof Theme; 18: stalledIntensity?: number; 19: reducedMotion?: boolean; 20: time?: number; 21: }; 22: export function SpinnerGlyph(t0) { 23: const $ = _c(9); 24: const { 25: frame, 26: messageColor, 27: stalledIntensity: t1, 28: reducedMotion: t2, 29: time: t3 30: } = t0; 31: const stalledIntensity = t1 === undefined ? 0 : t1; 32: const reducedMotion = t2 === undefined ? false : t2; 33: const time = t3 === undefined ? 0 : t3; 34: const [themeName] = useTheme(); 35: const theme = getTheme(themeName); 36: if (reducedMotion) { 37: const isDim = Math.floor(time / (REDUCED_MOTION_CYCLE_MS / 2)) % 2 === 1; 38: let t4; 39: if ($[0] !== isDim || $[1] !== messageColor) { 40: t4 = <Box flexWrap="wrap" height={1} width={2}><Text color={messageColor} dimColor={isDim}>{REDUCED_MOTION_DOT}</Text></Box>; 41: $[0] = isDim; 42: $[1] = messageColor; 43: $[2] = t4; 44: } else { 45: t4 = $[2]; 46: } 47: return t4; 48: } 49: const spinnerChar = SPINNER_FRAMES[frame % SPINNER_FRAMES.length]; 50: if (stalledIntensity > 0) { 51: const baseColorStr = theme[messageColor]; 52: const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null; 53: if (baseRGB) { 54: const interpolated = interpolateColor(baseRGB, ERROR_RED, stalledIntensity); 55: return <Box flexWrap="wrap" height={1} width={2}><Text color={toRGBColor(interpolated)}>{spinnerChar}</Text></Box>; 56: } 57: const color = stalledIntensity > 0.5 ? "error" : messageColor; 58: let t4; 59: if ($[3] !== color || $[4] !== spinnerChar) { 60: t4 = <Box flexWrap="wrap" height={1} width={2}><Text color={color}>{spinnerChar}</Text></Box>; 61: $[3] = color; 62: $[4] = spinnerChar; 63: $[5] = t4; 64: } else { 65: t4 = $[5]; 66: } 67: return t4; 68: } 69: let t4; 70: if ($[6] !== messageColor || $[7] !== spinnerChar) { 71: t4 = <Box flexWrap="wrap" height={1} width={2}><Text color={messageColor}>{spinnerChar}</Text></Box>; 72: $[6] = messageColor; 73: $[7] = spinnerChar; 74: $[8] = t4; 75: } else { 76: t4 = $[8]; 77: } 78: return t4; 79: }

File: src/components/Spinner/teammateSelectHint.ts

typescript 1: export const TEAMMATE_SELECT_HINT = 'shift + ↑/↓ to select'

File: src/components/Spinner/TeammateSpinnerLine.tsx

typescript 1: import figures from 'figures'; 2: import sample from 'lodash-es/sample.js'; 3: import * as React from 'react'; 4: import { useRef, useState } from 'react'; 5: import { getSpinnerVerbs } from '../../constants/spinnerVerbs.js'; 6: import { TURN_COMPLETION_VERBS } from '../../constants/turnCompletionVerbs.js'; 7: import { useElapsedTime } from '../../hooks/useElapsedTime.js'; 8: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 9: import { stringWidth } from '../../ink/stringWidth.js'; 10: import { Box, Text } from '../../ink.js'; 11: import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'; 12: import { summarizeRecentActivities } from '../../utils/collapseReadSearch.js'; 13: import { formatDuration, formatNumber, truncateToWidth } from '../../utils/format.js'; 14: import { toInkColor } from '../../utils/ink.js'; 15: import { TEAMMATE_SELECT_HINT } from './teammateSelectHint.js'; 16: type Props = { 17: teammate: InProcessTeammateTaskState; 18: isLast: boolean; 19: isSelected?: boolean; 20: isForegrounded?: boolean; 21: allIdle?: boolean; 22: showPreview?: boolean; 23: }; 24: function getMessagePreview(messages: InProcessTeammateTaskState['messages']): string[] { 25: if (!messages?.length) return []; 26: const allLines: string[] = []; 27: const maxLineLength = 80; 28: for (let i = messages.length - 1; i >= 0 && allLines.length < 3; i--) { 29: const msg = messages[i]; 30: if (!msg || msg.type !== 'user' && msg.type !== 'assistant' || !msg.message?.content?.length) { 31: continue; 32: } 33: const content = msg.message.content; 34: for (const block of content) { 35: if (allLines.length >= 3) break; 36: if (!block || typeof block !== 'object') continue; 37: if ('type' in block && block.type === 'tool_use' && 'name' in block) { 38: const input = 'input' in block ? block.input as Record<string, unknown> : null; 39: let toolLine = `Using ${block.name}…`; 40: if (input) { 41: const desc = input.description as string | undefined || input.prompt as string | undefined || input.command as string | undefined || input.query as string | undefined || input.pattern as string | undefined; 42: if (desc) { 43: toolLine = desc.split('\n')[0] ?? toolLine; 44: } 45: } 46: allLines.push(truncateToWidth(toolLine, maxLineLength)); 47: } else if ('type' in block && block.type === 'text' && 'text' in block) { 48: const textLines = (block.text as string).split('\n').filter(l => l.trim()); 49: for (let j = textLines.length - 1; j >= 0 && allLines.length < 3; j--) { 50: const line = textLines[j]; 51: if (!line) continue; 52: allLines.push(truncateToWidth(line, maxLineLength)); 53: } 54: } 55: } 56: } 57: return allLines.reverse(); 58: } 59: export function TeammateSpinnerLine({ 60: teammate, 61: isLast, 62: isSelected, 63: isForegrounded, 64: allIdle, 65: showPreview 66: }: Props): React.ReactNode { 67: const [randomVerb] = useState(() => teammate.spinnerVerb ?? sample(getSpinnerVerbs())); 68: const [pastTenseVerb] = useState(() => teammate.pastTenseVerb ?? sample(TURN_COMPLETION_VERBS)); 69: const isHighlighted = isSelected || isForegrounded; 70: const treeChar = isHighlighted ? isLast ? '╘═' : '╞═' : isLast ? '└─' : '├─'; 71: const nameColor = toInkColor(teammate.identity.color); 72: const { 73: columns 74: } = useTerminalSize(); 75: const idleStartRef = useRef<number | null>(null); 76: const frozenDurationRef = useRef<string | null>(null); 77: if (teammate.isIdle && idleStartRef.current === null) { 78: idleStartRef.current = Date.now(); 79: } else if (!teammate.isIdle) { 80: idleStartRef.current = null; 81: } 82: if (!allIdle && frozenDurationRef.current !== null) { 83: frozenDurationRef.current = null; 84: } 85: const idleElapsedTime = useElapsedTime(idleStartRef.current ?? Date.now(), teammate.isIdle && !allIdle); 86: if (allIdle && frozenDurationRef.current === null) { 87: frozenDurationRef.current = formatDuration(Math.max(0, Date.now() - teammate.startTime - (teammate.totalPausedMs ?? 0))); 88: } 89: const displayTime = allIdle ? frozenDurationRef.current ?? (() => { 90: throw new Error(`frozenDurationRef is null for idle teammate ${teammate.identity.agentName}`); 91: })() : idleElapsedTime; 92: const basePrefix = 8; 93: const fullAgentName = `@${teammate.identity.agentName}`; 94: const fullNameWidth = stringWidth(fullAgentName); 95: const toolUseCount = teammate.progress?.toolUseCount ?? 0; 96: const tokenCount = teammate.progress?.tokenCount ?? 0; 97: const statsText = ` · ${toolUseCount} tool ${toolUseCount === 1 ? 'use' : 'uses'} · ${formatNumber(tokenCount)} tokens`; 98: const statsWidth = stringWidth(statsText); 99: const selectHintText = ` · ${TEAMMATE_SELECT_HINT}`; 100: const selectHintWidth = stringWidth(selectHintText); 101: const viewHintText = ' · enter to view'; 102: const viewHintWidth = stringWidth(viewHintText); 103: const minActivityWidth = 25; 104: const spaceWithFullName = columns - basePrefix - fullNameWidth - 2; 105: const showName = columns >= 60 && spaceWithFullName >= minActivityWidth; 106: const nameWidth = showName ? fullNameWidth + 2 : 0; 107: const availableForActivity = columns - basePrefix - nameWidth; 108: const showViewHint = isSelected && !isForegrounded && availableForActivity > viewHintWidth + statsWidth + minActivityWidth + 5; 109: const showSelectHint = isHighlighted && availableForActivity > selectHintWidth + (showViewHint ? viewHintWidth : 0) + statsWidth + minActivityWidth + 5; 110: const showStats = availableForActivity > statsWidth + minActivityWidth + 5; 111: const extrasCost = (showStats ? statsWidth : 0) + (showSelectHint ? selectHintWidth : 0) + (showViewHint ? viewHintWidth : 0); 112: const activityMaxWidth = Math.max(minActivityWidth, availableForActivity - extrasCost - 1); 113: const activityText = (() => { 114: const activities = teammate.progress?.recentActivities; 115: if (activities && activities.length > 0) { 116: const summary = summarizeRecentActivities(activities); 117: if (summary) return truncateToWidth(summary, activityMaxWidth); 118: } 119: const desc = teammate.progress?.lastActivity?.activityDescription; 120: if (desc) return truncateToWidth(desc, activityMaxWidth); 121: return randomVerb; 122: })(); 123: const renderStatus = (): React.ReactNode => { 124: if (teammate.shutdownRequested) { 125: return <Text dimColor>[stopping]</Text>; 126: } 127: if (teammate.awaitingPlanApproval) { 128: return <Text color="warning">[awaiting approval]</Text>; 129: } 130: if (teammate.isIdle) { 131: if (allIdle) { 132: return <Text dimColor> 133: {pastTenseVerb} for {displayTime} 134: </Text>; 135: } 136: return <Text dimColor>Idle for {idleElapsedTime}</Text>; 137: } 138: if (isHighlighted) { 139: return null; 140: } 141: return <Text dimColor> 142: {activityText?.endsWith('…') ? activityText : `${activityText}…`} 143: </Text>; 144: }; 145: const previewLines = showPreview ? getMessagePreview(teammate.messages) : []; 146: const previewTreeChar = isLast ? ' ' : '│ '; 147: return <Box flexDirection="column"> 148: <Box paddingLeft={3}> 149: {} 150: <Text color={isSelected ? 'suggestion' : undefined} bold={isSelected}> 151: {isSelected ? figures.pointer : ' '} 152: </Text> 153: <Text dimColor={!isSelected}>{treeChar} </Text> 154: {} 155: {showName && <Text color={isSelected ? 'suggestion' : nameColor}> 156: @{teammate.identity.agentName} 157: </Text>} 158: {showName && <Text dimColor={!isSelected}>: </Text>} 159: {renderStatus()} 160: {} 161: {showStats && <Text dimColor> 162: {' '} 163: · {toolUseCount} tool {toolUseCount === 1 ? 'use' : 'uses'} ·{' '} 164: {formatNumber(tokenCount)} tokens 165: </Text>} 166: {} 167: {showSelectHint && <Text dimColor> · {TEAMMATE_SELECT_HINT}</Text>} 168: {showViewHint && <Text dimColor> · enter to view</Text>} 169: </Box> 170: {} 171: {previewLines.map((line, idx) => <Box key={idx} paddingLeft={3}> 172: <Text dimColor> </Text> 173: <Text dimColor>{previewTreeChar} </Text> 174: <Text dimColor>{line}</Text> 175: </Box>)} 176: </Box>; 177: }

File: src/components/Spinner/TeammateSpinnerTree.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { Box, Text, type TextProps } from '../../ink.js'; 5: import { useAppState } from '../../state/AppState.js'; 6: import { getRunningTeammatesSorted } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js'; 7: import { formatNumber } from '../../utils/format.js'; 8: import { TeammateSpinnerLine } from './TeammateSpinnerLine.js'; 9: import { TEAMMATE_SELECT_HINT } from './teammateSelectHint.js'; 10: type Props = { 11: selectedIndex?: number; 12: isInSelectionMode?: boolean; 13: allIdle?: boolean; 14: leaderVerb?: string; 15: leaderTokenCount?: number; 16: leaderIdleText?: string; 17: }; 18: export function TeammateSpinnerTree(t0) { 19: const $ = _c(61); 20: const { 21: selectedIndex, 22: isInSelectionMode, 23: allIdle, 24: leaderVerb, 25: leaderTokenCount, 26: leaderIdleText 27: } = t0; 28: const tasks = useAppState(_temp); 29: const viewingAgentTaskId = useAppState(_temp2); 30: const showTeammateMessagePreview = useAppState(_temp3); 31: let T0; 32: let isHideSelected; 33: let t1; 34: let t2; 35: let t3; 36: let t4; 37: let t5; 38: if ($[0] !== allIdle || $[1] !== isInSelectionMode || $[2] !== leaderIdleText || $[3] !== leaderTokenCount || $[4] !== leaderVerb || $[5] !== selectedIndex || $[6] !== showTeammateMessagePreview || $[7] !== tasks || $[8] !== viewingAgentTaskId) { 39: t5 = Symbol.for("react.early_return_sentinel"); 40: bb0: { 41: const teammateTasks = getRunningTeammatesSorted(tasks); 42: if (teammateTasks.length === 0) { 43: t5 = null; 44: break bb0; 45: } 46: const isLeaderForegrounded = viewingAgentTaskId === undefined; 47: const isLeaderSelected = isInSelectionMode && selectedIndex === -1; 48: const isLeaderHighlighted = isLeaderForegrounded || isLeaderSelected; 49: isHideSelected = isInSelectionMode === true && selectedIndex === teammateTasks.length; 50: T0 = Box; 51: t1 = "column"; 52: t2 = 1; 53: const t6 = isLeaderSelected ? "suggestion" : undefined; 54: const t7 = isLeaderSelected ? figures.pointer : " "; 55: let t8; 56: if ($[16] !== isLeaderHighlighted || $[17] !== t6 || $[18] !== t7) { 57: t8 = <Text color={t6} bold={isLeaderHighlighted}>{t7}</Text>; 58: $[16] = isLeaderHighlighted; 59: $[17] = t6; 60: $[18] = t7; 61: $[19] = t8; 62: } else { 63: t8 = $[19]; 64: } 65: const t9 = !isLeaderHighlighted; 66: const t10 = isLeaderHighlighted ? "\u2552\u2550" : "\u250C\u2500"; 67: let t11; 68: if ($[20] !== isLeaderHighlighted || $[21] !== t10 || $[22] !== t9) { 69: t11 = <Text dimColor={t9} bold={isLeaderHighlighted}>{t10}{" "}</Text>; 70: $[20] = isLeaderHighlighted; 71: $[21] = t10; 72: $[22] = t9; 73: $[23] = t11; 74: } else { 75: t11 = $[23]; 76: } 77: const t12 = isLeaderSelected ? "suggestion" : "cyan_FOR_SUBAGENTS_ONLY"; 78: let t13; 79: if ($[24] !== isLeaderHighlighted || $[25] !== t12) { 80: t13 = <Text bold={isLeaderHighlighted} color={t12}>team-lead</Text>; 81: $[24] = isLeaderHighlighted; 82: $[25] = t12; 83: $[26] = t13; 84: } else { 85: t13 = $[26]; 86: } 87: let t14; 88: if ($[27] !== isLeaderForegrounded || $[28] !== leaderVerb) { 89: t14 = !isLeaderForegrounded && leaderVerb && <Text dimColor={true}>: {leaderVerb}…</Text>; 90: $[27] = isLeaderForegrounded; 91: $[28] = leaderVerb; 92: $[29] = t14; 93: } else { 94: t14 = $[29]; 95: } 96: let t15; 97: if ($[30] !== isLeaderForegrounded || $[31] !== leaderIdleText || $[32] !== leaderVerb) { 98: t15 = !isLeaderForegrounded && !leaderVerb && leaderIdleText && <Text dimColor={true}>: {leaderIdleText}</Text>; 99: $[30] = isLeaderForegrounded; 100: $[31] = leaderIdleText; 101: $[32] = leaderVerb; 102: $[33] = t15; 103: } else { 104: t15 = $[33]; 105: } 106: let t16; 107: if ($[34] !== isLeaderHighlighted || $[35] !== leaderTokenCount) { 108: t16 = leaderTokenCount !== undefined && leaderTokenCount > 0 && <Text dimColor={!isLeaderHighlighted}>{" "}· {formatNumber(leaderTokenCount)} tokens</Text>; 109: $[34] = isLeaderHighlighted; 110: $[35] = leaderTokenCount; 111: $[36] = t16; 112: } else { 113: t16 = $[36]; 114: } 115: let t17; 116: if ($[37] !== isLeaderHighlighted) { 117: t17 = isLeaderHighlighted && <Text dimColor={true}> · {TEAMMATE_SELECT_HINT}</Text>; 118: $[37] = isLeaderHighlighted; 119: $[38] = t17; 120: } else { 121: t17 = $[38]; 122: } 123: let t18; 124: if ($[39] !== isLeaderForegrounded || $[40] !== isLeaderSelected) { 125: t18 = isLeaderSelected && !isLeaderForegrounded && <Text dimColor={true}> · enter to view</Text>; 126: $[39] = isLeaderForegrounded; 127: $[40] = isLeaderSelected; 128: $[41] = t18; 129: } else { 130: t18 = $[41]; 131: } 132: if ($[42] !== t11 || $[43] !== t13 || $[44] !== t14 || $[45] !== t15 || $[46] !== t16 || $[47] !== t17 || $[48] !== t18 || $[49] !== t8) { 133: t3 = <Box paddingLeft={3}>{t8}{t11}{t13}{t14}{t15}{t16}{t17}{t18}</Box>; 134: $[42] = t11; 135: $[43] = t13; 136: $[44] = t14; 137: $[45] = t15; 138: $[46] = t16; 139: $[47] = t17; 140: $[48] = t18; 141: $[49] = t8; 142: $[50] = t3; 143: } else { 144: t3 = $[50]; 145: } 146: t4 = teammateTasks.map((teammate, index) => <TeammateSpinnerLine key={teammate.id} teammate={teammate} isLast={!isInSelectionMode && index === teammateTasks.length - 1} isSelected={isInSelectionMode && selectedIndex === index} isForegrounded={viewingAgentTaskId === teammate.id} allIdle={allIdle} showPreview={showTeammateMessagePreview} />); 147: } 148: $[0] = allIdle; 149: $[1] = isInSelectionMode; 150: $[2] = leaderIdleText; 151: $[3] = leaderTokenCount; 152: $[4] = leaderVerb; 153: $[5] = selectedIndex; 154: $[6] = showTeammateMessagePreview; 155: $[7] = tasks; 156: $[8] = viewingAgentTaskId; 157: $[9] = T0; 158: $[10] = isHideSelected; 159: $[11] = t1; 160: $[12] = t2; 161: $[13] = t3; 162: $[14] = t4; 163: $[15] = t5; 164: } else { 165: T0 = $[9]; 166: isHideSelected = $[10]; 167: t1 = $[11]; 168: t2 = $[12]; 169: t3 = $[13]; 170: t4 = $[14]; 171: t5 = $[15]; 172: } 173: if (t5 !== Symbol.for("react.early_return_sentinel")) { 174: return t5; 175: } 176: let t6; 177: if ($[51] !== isHideSelected || $[52] !== isInSelectionMode) { 178: t6 = isInSelectionMode && <HideRow isSelected={isHideSelected} />; 179: $[51] = isHideSelected; 180: $[52] = isInSelectionMode; 181: $[53] = t6; 182: } else { 183: t6 = $[53]; 184: } 185: let t7; 186: if ($[54] !== T0 || $[55] !== t1 || $[56] !== t2 || $[57] !== t3 || $[58] !== t4 || $[59] !== t6) { 187: t7 = <T0 flexDirection={t1} marginTop={t2}>{t3}{t4}{t6}</T0>; 188: $[54] = T0; 189: $[55] = t1; 190: $[56] = t2; 191: $[57] = t3; 192: $[58] = t4; 193: $[59] = t6; 194: $[60] = t7; 195: } else { 196: t7 = $[60]; 197: } 198: return t7; 199: } 200: function _temp3(s_1) { 201: return s_1.showTeammateMessagePreview; 202: } 203: function _temp2(s_0) { 204: return s_0.viewingAgentTaskId; 205: } 206: function _temp(s) { 207: return s.tasks; 208: } 209: function HideRow(t0) { 210: const $ = _c(18); 211: const { 212: isSelected 213: } = t0; 214: const t1 = isSelected ? "suggestion" : undefined; 215: const t2 = isSelected ? figures.pointer : " "; 216: let t3; 217: if ($[0] !== isSelected || $[1] !== t1 || $[2] !== t2) { 218: t3 = <Text color={t1} bold={isSelected}>{t2}</Text>; 219: $[0] = isSelected; 220: $[1] = t1; 221: $[2] = t2; 222: $[3] = t3; 223: } else { 224: t3 = $[3]; 225: } 226: const t4 = !isSelected; 227: const t5 = isSelected ? "\u2558\u2550" : "\u2514\u2500"; 228: let t6; 229: if ($[4] !== isSelected || $[5] !== t4 || $[6] !== t5) { 230: t6 = <Text dimColor={t4} bold={isSelected}>{t5}{" "}</Text>; 231: $[4] = isSelected; 232: $[5] = t4; 233: $[6] = t5; 234: $[7] = t6; 235: } else { 236: t6 = $[7]; 237: } 238: const t7 = !isSelected; 239: let t8; 240: if ($[8] !== isSelected || $[9] !== t7) { 241: t8 = <Text dimColor={t7} bold={isSelected}>hide</Text>; 242: $[8] = isSelected; 243: $[9] = t7; 244: $[10] = t8; 245: } else { 246: t8 = $[10]; 247: } 248: let t9; 249: if ($[11] !== isSelected) { 250: t9 = isSelected && <Text dimColor={true}> · enter to collapse</Text>; 251: $[11] = isSelected; 252: $[12] = t9; 253: } else { 254: t9 = $[12]; 255: } 256: let t10; 257: if ($[13] !== t3 || $[14] !== t6 || $[15] !== t8 || $[16] !== t9) { 258: t10 = <Box paddingLeft={3}>{t3}{t6}{t8}{t9}</Box>; 259: $[13] = t3; 260: $[14] = t6; 261: $[15] = t8; 262: $[16] = t9; 263: $[17] = t10; 264: } else { 265: t10 = $[17]; 266: } 267: return t10; 268: }

File: src/components/Spinner/useShimmerAnimation.ts

typescript 1: import { useMemo } from 'react' 2: import { stringWidth } from '../../ink/stringWidth.js' 3: import { type DOMElement, useAnimationFrame } from '../../ink.js' 4: import type { SpinnerMode } from './types.js' 5: export function useShimmerAnimation( 6: mode: SpinnerMode, 7: message: string, 8: isStalled: boolean, 9: ): [ref: (element: DOMElement | null) => void, glimmerIndex: number] { 10: const glimmerSpeed = mode === 'requesting' ? 50 : 200 11: const [ref, time] = useAnimationFrame(isStalled ? null : glimmerSpeed) 12: const messageWidth = useMemo(() => stringWidth(message), [message]) 13: if (isStalled) { 14: return [ref, -100] 15: } 16: const cyclePosition = Math.floor(time / glimmerSpeed) 17: const cycleLength = messageWidth + 20 18: if (mode === 'requesting') { 19: return [ref, (cyclePosition % cycleLength) - 10] 20: } 21: return [ref, messageWidth + 10 - (cyclePosition % cycleLength)] 22: }

File: src/components/Spinner/useStalledAnimation.ts

typescript 1: import { useRef } from 'react' 2: export function useStalledAnimation( 3: time: number, 4: currentResponseLength: number, 5: hasActiveTools = false, 6: reducedMotion = false, 7: ): { 8: isStalled: boolean 9: stalledIntensity: number 10: } { 11: const lastTokenTime = useRef(time) 12: const lastResponseLength = useRef(currentResponseLength) 13: const mountTime = useRef(time) 14: const stalledIntensityRef = useRef(0) 15: const lastSmoothTime = useRef(time) 16: if (currentResponseLength > lastResponseLength.current) { 17: lastTokenTime.current = time 18: lastResponseLength.current = currentResponseLength 19: stalledIntensityRef.current = 0 20: lastSmoothTime.current = time 21: } 22: let timeSinceLastToken: number 23: if (hasActiveTools) { 24: timeSinceLastToken = 0 25: lastTokenTime.current = time 26: } else if (currentResponseLength > 0) { 27: timeSinceLastToken = time - lastTokenTime.current 28: } else { 29: timeSinceLastToken = time - mountTime.current 30: } 31: const isStalled = timeSinceLastToken > 3000 && !hasActiveTools 32: const intensity = isStalled 33: ? Math.min((timeSinceLastToken - 3000) / 2000, 1) 34: : 0 35: if (!reducedMotion && (intensity > 0 || stalledIntensityRef.current > 0)) { 36: const dt = time - lastSmoothTime.current 37: if (dt >= 50) { 38: const steps = Math.floor(dt / 50) 39: let current = stalledIntensityRef.current 40: for (let i = 0; i < steps; i++) { 41: const diff = intensity - current 42: if (Math.abs(diff) < 0.01) { 43: current = intensity 44: break 45: } 46: current += diff * 0.1 47: } 48: stalledIntensityRef.current = current 49: lastSmoothTime.current = time 50: } 51: } else { 52: stalledIntensityRef.current = intensity 53: lastSmoothTime.current = time 54: } 55: const effectiveIntensity = reducedMotion 56: ? intensity 57: : stalledIntensityRef.current 58: return { isStalled, stalledIntensity: effectiveIntensity } 59: }

File: src/components/Spinner/utils.ts

typescript 1: import type { RGBColor as RGBColorString } from '../../ink/styles.js' 2: import type { RGBColor as RGBColorType } from './types.js' 3: export function getDefaultCharacters(): string[] { 4: if (process.env.TERM === 'xterm-ghostty') { 5: return ['·', '✢', '✳', '✶', '✻', '*'] 6: } 7: return process.platform === 'darwin' 8: ? ['·', '✢', '✳', '✶', '✻', '✽'] 9: : ['·', '✢', '*', '✶', '✻', '✽'] 10: } 11: export function interpolateColor( 12: color1: RGBColorType, 13: color2: RGBColorType, 14: t: number, 15: ): RGBColorType { 16: return { 17: r: Math.round(color1.r + (color2.r - color1.r) * t), 18: g: Math.round(color1.g + (color2.g - color1.g) * t), 19: b: Math.round(color1.b + (color2.b - color1.b) * t), 20: } 21: } 22: export function toRGBColor(color: RGBColorType): RGBColorString { 23: return `rgb(${color.r},${color.g},${color.b})` 24: } 25: export function hueToRgb(hue: number): RGBColorType { 26: const h = ((hue % 360) + 360) % 360 27: const s = 0.7 28: const l = 0.6 29: const c = (1 - Math.abs(2 * l - 1)) * s 30: const x = c * (1 - Math.abs(((h / 60) % 2) - 1)) 31: const m = l - c / 2 32: let r = 0 33: let g = 0 34: let b = 0 35: if (h < 60) { 36: r = c 37: g = x 38: } else if (h < 120) { 39: r = x 40: g = c 41: } else if (h < 180) { 42: g = c 43: b = x 44: } else if (h < 240) { 45: g = x 46: b = c 47: } else if (h < 300) { 48: r = x 49: b = c 50: } else { 51: r = c 52: b = x 53: } 54: return { 55: r: Math.round((r + m) * 255), 56: g: Math.round((g + m) * 255), 57: b: Math.round((b + m) * 255), 58: } 59: } 60: const RGB_CACHE = new Map<string, RGBColorType | null>() 61: export function parseRGB(colorStr: string): RGBColorType | null { 62: const cached = RGB_CACHE.get(colorStr) 63: if (cached !== undefined) return cached 64: const match = colorStr.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/) 65: const result = match 66: ? { 67: r: parseInt(match[1]!, 10), 68: g: parseInt(match[2]!, 10), 69: b: parseInt(match[3]!, 10), 70: } 71: : null 72: RGB_CACHE.set(colorStr, result) 73: return result 74: }

File: src/components/StructuredDiff/colorDiff.ts

typescript 1: import { 2: ColorDiff, 3: ColorFile, 4: getSyntaxTheme as nativeGetSyntaxTheme, 5: type SyntaxTheme, 6: } from 'color-diff-napi' 7: import { isEnvDefinedFalsy } from '../../utils/envUtils.js' 8: export type ColorModuleUnavailableReason = 'env' 9: export function getColorModuleUnavailableReason(): ColorModuleUnavailableReason | null { 10: if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_SYNTAX_HIGHLIGHT)) { 11: return 'env' 12: } 13: return null 14: } 15: export function expectColorDiff(): typeof ColorDiff | null { 16: return getColorModuleUnavailableReason() === null ? ColorDiff : null 17: } 18: export function expectColorFile(): typeof ColorFile | null { 19: return getColorModuleUnavailableReason() === null ? ColorFile : null 20: } 21: export function getSyntaxTheme(themeName: string): SyntaxTheme | null { 22: return getColorModuleUnavailableReason() === null 23: ? nativeGetSyntaxTheme(themeName) 24: : null 25: }

File: src/components/StructuredDiff/Fallback.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { diffWordsWithSpace, type StructuredPatchHunk } from 'diff'; 3: import * as React from 'react'; 4: import { useMemo } from 'react'; 5: import type { ThemeName } from 'src/utils/theme.js'; 6: import { stringWidth } from '../../ink/stringWidth.js'; 7: import { Box, NoSelect, Text, useTheme, wrapText } from '../../ink.js'; 8: interface DiffLine { 9: code: string; 10: type: 'add' | 'remove' | 'nochange'; 11: i: number; 12: originalCode: string; 13: wordDiff?: boolean; 14: matchedLine?: DiffLine; 15: } 16: export interface LineObject { 17: code: string; 18: i: number; 19: type: 'add' | 'remove' | 'nochange'; 20: originalCode: string; 21: wordDiff?: boolean; 22: matchedLine?: LineObject; 23: } 24: interface DiffPart { 25: added?: boolean; 26: removed?: boolean; 27: value: string; 28: } 29: type Props = { 30: patch: StructuredPatchHunk; 31: dim: boolean; 32: width: number; 33: }; 34: const CHANGE_THRESHOLD = 0.4; 35: export function StructuredDiffFallback(t0) { 36: const $ = _c(10); 37: const { 38: patch, 39: dim, 40: width 41: } = t0; 42: const [theme] = useTheme(); 43: let t1; 44: if ($[0] !== dim || $[1] !== patch.lines || $[2] !== patch.oldStart || $[3] !== theme || $[4] !== width) { 45: t1 = formatDiff(patch.lines, patch.oldStart, width, dim, theme); 46: $[0] = dim; 47: $[1] = patch.lines; 48: $[2] = patch.oldStart; 49: $[3] = theme; 50: $[4] = width; 51: $[5] = t1; 52: } else { 53: t1 = $[5]; 54: } 55: const diff = t1; 56: let t2; 57: if ($[6] !== diff) { 58: t2 = diff.map(_temp); 59: $[6] = diff; 60: $[7] = t2; 61: } else { 62: t2 = $[7]; 63: } 64: let t3; 65: if ($[8] !== t2) { 66: t3 = <Box flexDirection="column" flexGrow={1}>{t2}</Box>; 67: $[8] = t2; 68: $[9] = t3; 69: } else { 70: t3 = $[9]; 71: } 72: return t3; 73: } 74: function _temp(node, i) { 75: return <Box key={i}>{node}</Box>; 76: } 77: export function transformLinesToObjects(lines: string[]): LineObject[] { 78: return lines.map(code => { 79: if (code.startsWith('+')) { 80: return { 81: code: code.slice(1), 82: i: 0, 83: type: 'add', 84: originalCode: code.slice(1) 85: }; 86: } 87: if (code.startsWith('-')) { 88: return { 89: code: code.slice(1), 90: i: 0, 91: type: 'remove', 92: originalCode: code.slice(1) 93: }; 94: } 95: return { 96: code: code.slice(1), 97: i: 0, 98: type: 'nochange', 99: originalCode: code.slice(1) 100: }; 101: }); 102: } 103: export function processAdjacentLines(lineObjects: LineObject[]): LineObject[] { 104: const processedLines: LineObject[] = []; 105: let i = 0; 106: while (i < lineObjects.length) { 107: const current = lineObjects[i]; 108: if (!current) { 109: i++; 110: continue; 111: } 112: if (current.type === 'remove') { 113: const removeLines: LineObject[] = [current]; 114: let j = i + 1; 115: while (j < lineObjects.length && lineObjects[j]?.type === 'remove') { 116: const line = lineObjects[j]; 117: if (line) { 118: removeLines.push(line); 119: } 120: j++; 121: } 122: const addLines: LineObject[] = []; 123: while (j < lineObjects.length && lineObjects[j]?.type === 'add') { 124: const line = lineObjects[j]; 125: if (line) { 126: addLines.push(line); 127: } 128: j++; 129: } 130: if (removeLines.length > 0 && addLines.length > 0) { 131: const pairCount = Math.min(removeLines.length, addLines.length); 132: for (let k = 0; k < pairCount; k++) { 133: const removeLine = removeLines[k]; 134: const addLine = addLines[k]; 135: if (removeLine && addLine) { 136: removeLine.wordDiff = true; 137: addLine.wordDiff = true; 138: removeLine.matchedLine = addLine; 139: addLine.matchedLine = removeLine; 140: } 141: } 142: processedLines.push(...removeLines.filter(Boolean)); 143: processedLines.push(...addLines.filter(Boolean)); 144: i = j; 145: } else { 146: processedLines.push(current); 147: i++; 148: } 149: } else { 150: processedLines.push(current); 151: i++; 152: } 153: } 154: return processedLines; 155: } 156: export function calculateWordDiffs(oldText: string, newText: string): DiffPart[] { 157: const result = diffWordsWithSpace(oldText, newText, { 158: ignoreCase: false 159: }); 160: return result; 161: } 162: function generateWordDiffElements(item: DiffLine, width: number, maxWidth: number, dim: boolean, overrideTheme?: ThemeName): React.ReactNode[] | null { 163: const { 164: type, 165: i, 166: wordDiff, 167: matchedLine, 168: originalCode 169: } = item; 170: if (!wordDiff || !matchedLine) { 171: return null; 172: } 173: const removedLineText = type === 'remove' ? originalCode : matchedLine.originalCode; 174: const addedLineText = type === 'remove' ? matchedLine.originalCode : originalCode; 175: const wordDiffs = calculateWordDiffs(removedLineText, addedLineText); 176: const totalLength = removedLineText.length + addedLineText.length; 177: const changedLength = wordDiffs.filter(part => part.added || part.removed).reduce((sum, part) => sum + part.value.length, 0); 178: const changeRatio = changedLength / totalLength; 179: if (changeRatio > CHANGE_THRESHOLD || dim) { 180: return null; 181: } 182: const diffPrefix = type === 'add' ? '+' : '-'; 183: const diffPrefixWidth = diffPrefix.length; 184: const availableContentWidth = Math.max(1, width - maxWidth - 1 - diffPrefixWidth); 185: const wrappedLines: { 186: content: React.ReactNode[]; 187: contentWidth: number; 188: }[] = []; 189: let currentLine: React.ReactNode[] = []; 190: let currentLineWidth = 0; 191: wordDiffs.forEach((part, partIndex) => { 192: let shouldShow = false; 193: let partBgColor: 'diffAddedWord' | 'diffRemovedWord' | undefined; 194: if (type === 'add') { 195: if (part.added) { 196: shouldShow = true; 197: partBgColor = 'diffAddedWord'; 198: } else if (!part.removed) { 199: shouldShow = true; 200: } 201: } else if (type === 'remove') { 202: if (part.removed) { 203: shouldShow = true; 204: partBgColor = 'diffRemovedWord'; 205: } else if (!part.added) { 206: shouldShow = true; 207: } 208: } 209: if (!shouldShow) return; 210: const partWrapped = wrapText(part.value, availableContentWidth, 'wrap'); 211: const partLines = partWrapped.split('\n'); 212: partLines.forEach((partLine, lineIdx) => { 213: if (!partLine) return; 214: if (lineIdx > 0 || currentLineWidth + stringWidth(partLine) > availableContentWidth) { 215: if (currentLine.length > 0) { 216: wrappedLines.push({ 217: content: [...currentLine], 218: contentWidth: currentLineWidth 219: }); 220: currentLine = []; 221: currentLineWidth = 0; 222: } 223: } 224: currentLine.push(<Text key={`part-${partIndex}-${lineIdx}`} backgroundColor={partBgColor}> 225: {partLine} 226: </Text>); 227: currentLineWidth += stringWidth(partLine); 228: }); 229: }); 230: if (currentLine.length > 0) { 231: wrappedLines.push({ 232: content: currentLine, 233: contentWidth: currentLineWidth 234: }); 235: } 236: return wrappedLines.map(({ 237: content, 238: contentWidth 239: }, lineIndex) => { 240: const key = `${type}-${i}-${lineIndex}`; 241: const lineBgColor = type === 'add' ? dim ? 'diffAddedDimmed' : 'diffAdded' : dim ? 'diffRemovedDimmed' : 'diffRemoved'; 242: const lineNum = lineIndex === 0 ? i : undefined; 243: const lineNumStr = (lineNum !== undefined ? lineNum.toString().padStart(maxWidth) : ' '.repeat(maxWidth)) + ' '; 244: const usedWidth = lineNumStr.length + diffPrefixWidth + contentWidth; 245: const padding = Math.max(0, width - usedWidth); 246: return <Box key={key} flexDirection="row"> 247: <NoSelect fromLeftEdge> 248: <Text color={overrideTheme ? 'text' : undefined} backgroundColor={lineBgColor} dimColor={dim}> 249: {lineNumStr} 250: {diffPrefix} 251: </Text> 252: </NoSelect> 253: <Text color={overrideTheme ? 'text' : undefined} backgroundColor={lineBgColor} dimColor={dim}> 254: {content} 255: {' '.repeat(padding)} 256: </Text> 257: </Box>; 258: }); 259: } 260: function formatDiff(lines: string[], startingLineNumber: number, width: number, dim: boolean, overrideTheme?: ThemeName): React.ReactNode[] { 261: const safeWidth = Math.max(1, Math.floor(width)); 262: const lineObjects = transformLinesToObjects(lines); 263: const processedLines = processAdjacentLines(lineObjects); 264: const ls = numberDiffLines(processedLines, startingLineNumber); 265: const maxLineNumber = Math.max(...ls.map(({ 266: i 267: }) => i), 0); 268: const maxWidth = Math.max(maxLineNumber.toString().length + 1, 0); 269: return ls.flatMap((item): React.ReactNode[] => { 270: const { 271: type, 272: code, 273: i, 274: wordDiff, 275: matchedLine 276: } = item; 277: if (wordDiff && matchedLine) { 278: const wordDiffElements = generateWordDiffElements(item, safeWidth, maxWidth, dim, overrideTheme); 279: if (wordDiffElements !== null) { 280: return wordDiffElements; 281: } 282: } 283: const diffPrefixWidth = 2; 284: const availableContentWidth = Math.max(1, safeWidth - maxWidth - 1 - diffPrefixWidth); 285: const wrappedText = wrapText(code, availableContentWidth, 'wrap'); 286: const wrappedLines = wrappedText.split('\n'); 287: return wrappedLines.map((line, lineIndex) => { 288: const key = `${type}-${i}-${lineIndex}`; 289: const lineNum = lineIndex === 0 ? i : undefined; 290: const lineNumStr = (lineNum !== undefined ? lineNum.toString().padStart(maxWidth) : ' '.repeat(maxWidth)) + ' '; 291: const sigil = type === 'add' ? '+' : type === 'remove' ? '-' : ' '; 292: const contentWidth = lineNumStr.length + 1 + stringWidth(line); 293: const padding = Math.max(0, safeWidth - contentWidth); 294: const bgColor = type === 'add' ? dim ? 'diffAddedDimmed' : 'diffAdded' : type === 'remove' ? dim ? 'diffRemovedDimmed' : 'diffRemoved' : undefined; 295: return <Box key={key} flexDirection="row"> 296: <NoSelect fromLeftEdge> 297: <Text color={overrideTheme ? 'text' : undefined} backgroundColor={bgColor} dimColor={dim || type === 'nochange'}> 298: {lineNumStr} 299: {sigil} 300: </Text> 301: </NoSelect> 302: <Text color={overrideTheme ? 'text' : undefined} backgroundColor={bgColor} dimColor={dim}> 303: {line} 304: {' '.repeat(padding)} 305: </Text> 306: </Box>; 307: }); 308: }); 309: } 310: export function numberDiffLines(diff: LineObject[], startLine: number): DiffLine[] { 311: let i = startLine; 312: const result: DiffLine[] = []; 313: const queue = [...diff]; 314: while (queue.length > 0) { 315: const current = queue.shift()!; 316: const { 317: code, 318: type, 319: originalCode, 320: wordDiff, 321: matchedLine 322: } = current; 323: const line = { 324: code, 325: type, 326: i, 327: originalCode, 328: wordDiff, 329: matchedLine 330: }; 331: switch (type) { 332: case 'nochange': 333: i++; 334: result.push(line); 335: break; 336: case 'add': 337: i++; 338: result.push(line); 339: break; 340: case 'remove': 341: { 342: result.push(line); 343: let numRemoved = 0; 344: while (queue[0]?.type === 'remove') { 345: i++; 346: const current = queue.shift()!; 347: const { 348: code, 349: type, 350: originalCode, 351: wordDiff, 352: matchedLine 353: } = current; 354: const line = { 355: code, 356: type, 357: i, 358: originalCode, 359: wordDiff, 360: matchedLine 361: }; 362: result.push(line); 363: numRemoved++; 364: } 365: i -= numRemoved; 366: break; 367: } 368: } 369: } 370: return result; 371: }

File: src/components/tasks/AsyncAgentDetailDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useMemo } from 'react'; 3: import type { DeepImmutable } from 'src/types/utils.js'; 4: import { useElapsedTime } from '../../hooks/useElapsedTime.js'; 5: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 6: import { Box, Text, useTheme } from '../../ink.js'; 7: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 8: import { getEmptyToolPermissionContext } from '../../Tool.js'; 9: import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js'; 10: import { getTools } from '../../tools.js'; 11: import { formatNumber } from '../../utils/format.js'; 12: import { extractTag } from '../../utils/messages.js'; 13: import { Byline } from '../design-system/Byline.js'; 14: import { Dialog } from '../design-system/Dialog.js'; 15: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 16: import { UserPlanMessage } from '../messages/UserPlanMessage.js'; 17: import { renderToolActivity } from './renderToolActivity.js'; 18: import { getTaskStatusColor, getTaskStatusIcon } from './taskStatusUtils.js'; 19: type Props = { 20: agent: DeepImmutable<LocalAgentTaskState>; 21: onDone: () => void; 22: onKillAgent?: () => void; 23: onBack?: () => void; 24: }; 25: export function AsyncAgentDetailDialog(t0) { 26: const $ = _c(54); 27: const { 28: agent, 29: onDone, 30: onKillAgent, 31: onBack 32: } = t0; 33: const [theme] = useTheme(); 34: let t1; 35: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 36: t1 = getTools(getEmptyToolPermissionContext()); 37: $[0] = t1; 38: } else { 39: t1 = $[0]; 40: } 41: const tools = t1; 42: const elapsedTime = useElapsedTime(agent.startTime, agent.status === "running", 1000, agent.totalPausedMs ?? 0); 43: let t2; 44: if ($[1] !== onDone) { 45: t2 = { 46: "confirm:yes": onDone 47: }; 48: $[1] = onDone; 49: $[2] = t2; 50: } else { 51: t2 = $[2]; 52: } 53: let t3; 54: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 55: t3 = { 56: context: "Confirmation" 57: }; 58: $[3] = t3; 59: } else { 60: t3 = $[3]; 61: } 62: useKeybindings(t2, t3); 63: let t4; 64: if ($[4] !== agent.status || $[5] !== onBack || $[6] !== onDone || $[7] !== onKillAgent) { 65: t4 = e => { 66: if (e.key === " ") { 67: e.preventDefault(); 68: onDone(); 69: } else { 70: if (e.key === "left" && onBack) { 71: e.preventDefault(); 72: onBack(); 73: } else { 74: if (e.key === "x" && agent.status === "running" && onKillAgent) { 75: e.preventDefault(); 76: onKillAgent(); 77: } 78: } 79: } 80: }; 81: $[4] = agent.status; 82: $[5] = onBack; 83: $[6] = onDone; 84: $[7] = onKillAgent; 85: $[8] = t4; 86: } else { 87: t4 = $[8]; 88: } 89: const handleKeyDown = t4; 90: let t5; 91: if ($[9] !== agent.prompt) { 92: t5 = extractTag(agent.prompt, "plan"); 93: $[9] = agent.prompt; 94: $[10] = t5; 95: } else { 96: t5 = $[10]; 97: } 98: const planContent = t5; 99: const displayPrompt = agent.prompt.length > 300 ? agent.prompt.substring(0, 297) + "\u2026" : agent.prompt; 100: const tokenCount = agent.result?.totalTokens ?? agent.progress?.tokenCount; 101: const toolUseCount = agent.result?.totalToolUseCount ?? agent.progress?.toolUseCount; 102: const t6 = agent.selectedAgent?.agentType ?? "agent"; 103: const t7 = agent.description || "Async agent"; 104: let t8; 105: if ($[11] !== t6 || $[12] !== t7) { 106: t8 = <Text>{t6} ›{" "}{t7}</Text>; 107: $[11] = t6; 108: $[12] = t7; 109: $[13] = t8; 110: } else { 111: t8 = $[13]; 112: } 113: const title = t8; 114: let t9; 115: if ($[14] !== agent.status) { 116: t9 = agent.status !== "running" && <Text color={getTaskStatusColor(agent.status)}>{getTaskStatusIcon(agent.status)}{" "}{agent.status === "completed" ? "Completed" : agent.status === "failed" ? "Failed" : "Stopped"}{" \xB7 "}</Text>; 117: $[14] = agent.status; 118: $[15] = t9; 119: } else { 120: t9 = $[15]; 121: } 122: let t10; 123: if ($[16] !== tokenCount) { 124: t10 = tokenCount !== undefined && tokenCount > 0 && <> · {formatNumber(tokenCount)} tokens</>; 125: $[16] = tokenCount; 126: $[17] = t10; 127: } else { 128: t10 = $[17]; 129: } 130: let t11; 131: if ($[18] !== toolUseCount) { 132: t11 = toolUseCount !== undefined && toolUseCount > 0 && <>{" "}· {toolUseCount} {toolUseCount === 1 ? "tool" : "tools"}</>; 133: $[18] = toolUseCount; 134: $[19] = t11; 135: } else { 136: t11 = $[19]; 137: } 138: let t12; 139: if ($[20] !== elapsedTime || $[21] !== t10 || $[22] !== t11) { 140: t12 = <Text dimColor={true}>{elapsedTime}{t10}{t11}</Text>; 141: $[20] = elapsedTime; 142: $[21] = t10; 143: $[22] = t11; 144: $[23] = t12; 145: } else { 146: t12 = $[23]; 147: } 148: let t13; 149: if ($[24] !== t12 || $[25] !== t9) { 150: t13 = <Text>{t9}{t12}</Text>; 151: $[24] = t12; 152: $[25] = t9; 153: $[26] = t13; 154: } else { 155: t13 = $[26]; 156: } 157: const subtitle = t13; 158: let t14; 159: if ($[27] !== agent.status || $[28] !== onBack || $[29] !== onKillAgent) { 160: t14 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{agent.status === "running" && onKillAgent && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>; 161: $[27] = agent.status; 162: $[28] = onBack; 163: $[29] = onKillAgent; 164: $[30] = t14; 165: } else { 166: t14 = $[30]; 167: } 168: let t15; 169: if ($[31] !== agent.progress || $[32] !== agent.status || $[33] !== theme) { 170: t15 = agent.status === "running" && agent.progress?.recentActivities && agent.progress.recentActivities.length > 0 && <Box flexDirection="column"><Text bold={true} dimColor={true}>Progress</Text>{agent.progress.recentActivities.map((activity, i) => <Text key={i} dimColor={i < agent.progress.recentActivities.length - 1} wrap="truncate-end">{i === agent.progress.recentActivities.length - 1 ? "\u203A " : " "}{renderToolActivity(activity, tools, theme)}</Text>)}</Box>; 171: $[31] = agent.progress; 172: $[32] = agent.status; 173: $[33] = theme; 174: $[34] = t15; 175: } else { 176: t15 = $[34]; 177: } 178: let t16; 179: if ($[35] !== displayPrompt || $[36] !== planContent) { 180: t16 = planContent ? <Box marginTop={1}><UserPlanMessage addMargin={false} planContent={planContent} /></Box> : <Box flexDirection="column" marginTop={1}><Text bold={true} dimColor={true}>Prompt</Text><Text wrap="wrap">{displayPrompt}</Text></Box>; 181: $[35] = displayPrompt; 182: $[36] = planContent; 183: $[37] = t16; 184: } else { 185: t16 = $[37]; 186: } 187: let t17; 188: if ($[38] !== agent.error || $[39] !== agent.status) { 189: t17 = agent.status === "failed" && agent.error && <Box flexDirection="column" marginTop={1}><Text bold={true} color="error">Error</Text><Text color="error" wrap="wrap">{agent.error}</Text></Box>; 190: $[38] = agent.error; 191: $[39] = agent.status; 192: $[40] = t17; 193: } else { 194: t17 = $[40]; 195: } 196: let t18; 197: if ($[41] !== t15 || $[42] !== t16 || $[43] !== t17) { 198: t18 = <Box flexDirection="column">{t15}{t16}{t17}</Box>; 199: $[41] = t15; 200: $[42] = t16; 201: $[43] = t17; 202: $[44] = t18; 203: } else { 204: t18 = $[44]; 205: } 206: let t19; 207: if ($[45] !== onDone || $[46] !== subtitle || $[47] !== t14 || $[48] !== t18 || $[49] !== title) { 208: t19 = <Dialog title={title} subtitle={subtitle} onCancel={onDone} color="background" inputGuide={t14}>{t18}</Dialog>; 209: $[45] = onDone; 210: $[46] = subtitle; 211: $[47] = t14; 212: $[48] = t18; 213: $[49] = title; 214: $[50] = t19; 215: } else { 216: t19 = $[50]; 217: } 218: let t20; 219: if ($[51] !== handleKeyDown || $[52] !== t19) { 220: t20 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t19}</Box>; 221: $[51] = handleKeyDown; 222: $[52] = t19; 223: $[53] = t20; 224: } else { 225: t20 = $[53]; 226: } 227: return t20; 228: }

File: src/components/tasks/BackgroundTask.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Text } from 'src/ink.js'; 4: import type { BackgroundTaskState } from 'src/tasks/types.js'; 5: import type { DeepImmutable } from 'src/types/utils.js'; 6: import { truncate } from 'src/utils/format.js'; 7: import { toInkColor } from 'src/utils/ink.js'; 8: import { plural } from 'src/utils/stringUtils.js'; 9: import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'; 10: import { RemoteSessionProgress } from './RemoteSessionProgress.js'; 11: import { ShellProgress, TaskStatusText } from './ShellProgress.js'; 12: import { describeTeammateActivity } from './taskStatusUtils.js'; 13: type Props = { 14: task: DeepImmutable<BackgroundTaskState>; 15: maxActivityWidth?: number; 16: }; 17: export function BackgroundTask(t0) { 18: const $ = _c(92); 19: const { 20: task, 21: maxActivityWidth 22: } = t0; 23: const activityLimit = maxActivityWidth ?? 40; 24: switch (task.type) { 25: case "local_bash": 26: { 27: const t1 = task.kind === "monitor" ? task.description : task.command; 28: let t2; 29: if ($[0] !== activityLimit || $[1] !== t1) { 30: t2 = truncate(t1, activityLimit, true); 31: $[0] = activityLimit; 32: $[1] = t1; 33: $[2] = t2; 34: } else { 35: t2 = $[2]; 36: } 37: let t3; 38: if ($[3] !== task) { 39: t3 = <ShellProgress shell={task} />; 40: $[3] = task; 41: $[4] = t3; 42: } else { 43: t3 = $[4]; 44: } 45: let t4; 46: if ($[5] !== t2 || $[6] !== t3) { 47: t4 = <Text>{t2}{" "}{t3}</Text>; 48: $[5] = t2; 49: $[6] = t3; 50: $[7] = t4; 51: } else { 52: t4 = $[7]; 53: } 54: return t4; 55: } 56: case "remote_agent": 57: { 58: if (task.isRemoteReview) { 59: let t1; 60: if ($[8] !== task) { 61: t1 = <Text><RemoteSessionProgress session={task} /></Text>; 62: $[8] = task; 63: $[9] = t1; 64: } else { 65: t1 = $[9]; 66: } 67: return t1; 68: } 69: const running = task.status === "running" || task.status === "pending"; 70: const t1 = running ? DIAMOND_OPEN : DIAMOND_FILLED; 71: let t2; 72: if ($[10] !== t1) { 73: t2 = <Text dimColor={true}>{t1} </Text>; 74: $[10] = t1; 75: $[11] = t2; 76: } else { 77: t2 = $[11]; 78: } 79: let t3; 80: if ($[12] !== activityLimit || $[13] !== task.title) { 81: t3 = truncate(task.title, activityLimit, true); 82: $[12] = activityLimit; 83: $[13] = task.title; 84: $[14] = t3; 85: } else { 86: t3 = $[14]; 87: } 88: let t4; 89: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 90: t4 = <Text dimColor={true}> · </Text>; 91: $[15] = t4; 92: } else { 93: t4 = $[15]; 94: } 95: let t5; 96: if ($[16] !== task) { 97: t5 = <RemoteSessionProgress session={task} />; 98: $[16] = task; 99: $[17] = t5; 100: } else { 101: t5 = $[17]; 102: } 103: let t6; 104: if ($[18] !== t2 || $[19] !== t3 || $[20] !== t5) { 105: t6 = <Text>{t2}{t3}{t4}{t5}</Text>; 106: $[18] = t2; 107: $[19] = t3; 108: $[20] = t5; 109: $[21] = t6; 110: } else { 111: t6 = $[21]; 112: } 113: return t6; 114: } 115: case "local_agent": 116: { 117: let t1; 118: if ($[22] !== activityLimit || $[23] !== task.description) { 119: t1 = truncate(task.description, activityLimit, true); 120: $[22] = activityLimit; 121: $[23] = task.description; 122: $[24] = t1; 123: } else { 124: t1 = $[24]; 125: } 126: const t2 = task.status === "completed" ? "done" : undefined; 127: const t3 = task.status === "completed" && !task.notified ? ", unread" : undefined; 128: let t4; 129: if ($[25] !== t2 || $[26] !== t3 || $[27] !== task.status) { 130: t4 = <TaskStatusText status={task.status} label={t2} suffix={t3} />; 131: $[25] = t2; 132: $[26] = t3; 133: $[27] = task.status; 134: $[28] = t4; 135: } else { 136: t4 = $[28]; 137: } 138: let t5; 139: if ($[29] !== t1 || $[30] !== t4) { 140: t5 = <Text>{t1}{" "}{t4}</Text>; 141: $[29] = t1; 142: $[30] = t4; 143: $[31] = t5; 144: } else { 145: t5 = $[31]; 146: } 147: return t5; 148: } 149: case "in_process_teammate": 150: { 151: let T0; 152: let T1; 153: let t1; 154: let t2; 155: let t3; 156: let t4; 157: if ($[32] !== activityLimit || $[33] !== task) { 158: const activity = describeTeammateActivity(task); 159: T1 = Text; 160: let t5; 161: if ($[40] !== task.identity.color) { 162: t5 = toInkColor(task.identity.color); 163: $[40] = task.identity.color; 164: $[41] = t5; 165: } else { 166: t5 = $[41]; 167: } 168: if ($[42] !== t5 || $[43] !== task.identity.agentName) { 169: t4 = <Text color={t5}>@{task.identity.agentName}</Text>; 170: $[42] = t5; 171: $[43] = task.identity.agentName; 172: $[44] = t4; 173: } else { 174: t4 = $[44]; 175: } 176: T0 = Text; 177: t1 = true; 178: t2 = ": "; 179: t3 = truncate(activity, activityLimit, true); 180: $[32] = activityLimit; 181: $[33] = task; 182: $[34] = T0; 183: $[35] = T1; 184: $[36] = t1; 185: $[37] = t2; 186: $[38] = t3; 187: $[39] = t4; 188: } else { 189: T0 = $[34]; 190: T1 = $[35]; 191: t1 = $[36]; 192: t2 = $[37]; 193: t3 = $[38]; 194: t4 = $[39]; 195: } 196: let t5; 197: if ($[45] !== T0 || $[46] !== t1 || $[47] !== t2 || $[48] !== t3) { 198: t5 = <T0 dimColor={t1}>{t2}{t3}</T0>; 199: $[45] = T0; 200: $[46] = t1; 201: $[47] = t2; 202: $[48] = t3; 203: $[49] = t5; 204: } else { 205: t5 = $[49]; 206: } 207: let t6; 208: if ($[50] !== T1 || $[51] !== t4 || $[52] !== t5) { 209: t6 = <T1>{t4}{t5}</T1>; 210: $[50] = T1; 211: $[51] = t4; 212: $[52] = t5; 213: $[53] = t6; 214: } else { 215: t6 = $[53]; 216: } 217: return t6; 218: } 219: case "local_workflow": 220: { 221: const t1 = task.workflowName ?? task.summary ?? task.description; 222: let t2; 223: if ($[54] !== activityLimit || $[55] !== t1) { 224: t2 = truncate(t1, activityLimit, true); 225: $[54] = activityLimit; 226: $[55] = t1; 227: $[56] = t2; 228: } else { 229: t2 = $[56]; 230: } 231: let t3; 232: if ($[57] !== task.agentCount || $[58] !== task.status) { 233: t3 = task.status === "running" ? `${task.agentCount} ${plural(task.agentCount, "agent")}` : task.status === "completed" ? "done" : undefined; 234: $[57] = task.agentCount; 235: $[58] = task.status; 236: $[59] = t3; 237: } else { 238: t3 = $[59]; 239: } 240: const t4 = task.status === "completed" && !task.notified ? ", unread" : undefined; 241: let t5; 242: if ($[60] !== t3 || $[61] !== t4 || $[62] !== task.status) { 243: t5 = <TaskStatusText status={task.status} label={t3} suffix={t4} />; 244: $[60] = t3; 245: $[61] = t4; 246: $[62] = task.status; 247: $[63] = t5; 248: } else { 249: t5 = $[63]; 250: } 251: let t6; 252: if ($[64] !== t2 || $[65] !== t5) { 253: t6 = <Text>{t2}{" "}{t5}</Text>; 254: $[64] = t2; 255: $[65] = t5; 256: $[66] = t6; 257: } else { 258: t6 = $[66]; 259: } 260: return t6; 261: } 262: case "monitor_mcp": 263: { 264: let t1; 265: if ($[67] !== activityLimit || $[68] !== task.description) { 266: t1 = truncate(task.description, activityLimit, true); 267: $[67] = activityLimit; 268: $[68] = task.description; 269: $[69] = t1; 270: } else { 271: t1 = $[69]; 272: } 273: const t2 = task.status === "completed" ? "done" : undefined; 274: const t3 = task.status === "completed" && !task.notified ? ", unread" : undefined; 275: let t4; 276: if ($[70] !== t2 || $[71] !== t3 || $[72] !== task.status) { 277: t4 = <TaskStatusText status={task.status} label={t2} suffix={t3} />; 278: $[70] = t2; 279: $[71] = t3; 280: $[72] = task.status; 281: $[73] = t4; 282: } else { 283: t4 = $[73]; 284: } 285: let t5; 286: if ($[74] !== t1 || $[75] !== t4) { 287: t5 = <Text>{t1}{" "}{t4}</Text>; 288: $[74] = t1; 289: $[75] = t4; 290: $[76] = t5; 291: } else { 292: t5 = $[76]; 293: } 294: return t5; 295: } 296: case "dream": 297: { 298: const n = task.filesTouched.length; 299: let t1; 300: if ($[77] !== n || $[78] !== task.phase || $[79] !== task.sessionsReviewing) { 301: t1 = task.phase === "updating" && n > 0 ? `${n} ${plural(n, "file")}` : `${task.sessionsReviewing} ${plural(task.sessionsReviewing, "session")}`; 302: $[77] = n; 303: $[78] = task.phase; 304: $[79] = task.sessionsReviewing; 305: $[80] = t1; 306: } else { 307: t1 = $[80]; 308: } 309: const detail = t1; 310: let t2; 311: if ($[81] !== detail || $[82] !== task.phase) { 312: t2 = <Text dimColor={true}>· {task.phase} · {detail}</Text>; 313: $[81] = detail; 314: $[82] = task.phase; 315: $[83] = t2; 316: } else { 317: t2 = $[83]; 318: } 319: const t3 = task.status === "completed" ? "done" : undefined; 320: const t4 = task.status === "completed" && !task.notified ? ", unread" : undefined; 321: let t5; 322: if ($[84] !== t3 || $[85] !== t4 || $[86] !== task.status) { 323: t5 = <TaskStatusText status={task.status} label={t3} suffix={t4} />; 324: $[84] = t3; 325: $[85] = t4; 326: $[86] = task.status; 327: $[87] = t5; 328: } else { 329: t5 = $[87]; 330: } 331: let t6; 332: if ($[88] !== t2 || $[89] !== t5 || $[90] !== task.description) { 333: t6 = <Text>{task.description}{" "}{t2}{" "}{t5}</Text>; 334: $[88] = t2; 335: $[89] = t5; 336: $[90] = task.description; 337: $[91] = t6; 338: } else { 339: t6 = $[91]; 340: } 341: return t6; 342: } 343: } 344: }

File: src/components/tasks/BackgroundTasksDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import figures from 'figures'; 4: import React, { type ReactNode, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react'; 5: import { isCoordinatorMode } from 'src/coordinator/coordinatorMode.js'; 6: import { useTerminalSize } from 'src/hooks/useTerminalSize.js'; 7: import { useAppState, useSetAppState } from 'src/state/AppState.js'; 8: import { enterTeammateView, exitTeammateView } from 'src/state/teammateViewHelpers.js'; 9: import type { ToolUseContext } from 'src/Tool.js'; 10: import { DreamTask, type DreamTaskState } from 'src/tasks/DreamTask/DreamTask.js'; 11: import { InProcessTeammateTask } from 'src/tasks/InProcessTeammateTask/InProcessTeammateTask.js'; 12: import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js'; 13: import type { LocalAgentTaskState } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'; 14: import { LocalAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'; 15: import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js'; 16: import { LocalShellTask } from 'src/tasks/LocalShellTask/LocalShellTask.js'; 17: import type { LocalWorkflowTaskState } from 'src/tasks/LocalWorkflowTask/LocalWorkflowTask.js'; 18: import type { MonitorMcpTaskState } from 'src/tasks/MonitorMcpTask/MonitorMcpTask.js'; 19: import { RemoteAgentTask, type RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'; 20: import { type BackgroundTaskState, isBackgroundTask, type TaskState } from 'src/tasks/types.js'; 21: import type { DeepImmutable } from 'src/types/utils.js'; 22: import { intersperse } from 'src/utils/array.js'; 23: import { TEAM_LEAD_NAME } from 'src/utils/swarm/constants.js'; 24: import { stopUltraplan } from '../../commands/ultraplan.js'; 25: import type { CommandResultDisplay } from '../../commands.js'; 26: import { useRegisterOverlay } from '../../context/overlayContext.js'; 27: import type { ExitState } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 28: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 29: import { Box, Text } from '../../ink.js'; 30: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 31: import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'; 32: import { count } from '../../utils/array.js'; 33: import { Byline } from '../design-system/Byline.js'; 34: import { Dialog } from '../design-system/Dialog.js'; 35: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 36: import { AsyncAgentDetailDialog } from './AsyncAgentDetailDialog.js'; 37: import { BackgroundTask as BackgroundTaskComponent } from './BackgroundTask.js'; 38: import { DreamDetailDialog } from './DreamDetailDialog.js'; 39: import { InProcessTeammateDetailDialog } from './InProcessTeammateDetailDialog.js'; 40: import { RemoteSessionDetailDialog } from './RemoteSessionDetailDialog.js'; 41: import { ShellDetailDialog } from './ShellDetailDialog.js'; 42: type ViewState = { 43: mode: 'list'; 44: } | { 45: mode: 'detail'; 46: itemId: string; 47: }; 48: type Props = { 49: onDone: (result?: string, options?: { 50: display?: CommandResultDisplay; 51: }) => void; 52: toolUseContext: ToolUseContext; 53: initialDetailTaskId?: string; 54: }; 55: type ListItem = { 56: id: string; 57: type: 'local_bash'; 58: label: string; 59: status: string; 60: task: DeepImmutable<LocalShellTaskState>; 61: } | { 62: id: string; 63: type: 'remote_agent'; 64: label: string; 65: status: string; 66: task: DeepImmutable<RemoteAgentTaskState>; 67: } | { 68: id: string; 69: type: 'local_agent'; 70: label: string; 71: status: string; 72: task: DeepImmutable<LocalAgentTaskState>; 73: } | { 74: id: string; 75: type: 'in_process_teammate'; 76: label: string; 77: status: string; 78: task: DeepImmutable<InProcessTeammateTaskState>; 79: } | { 80: id: string; 81: type: 'local_workflow'; 82: label: string; 83: status: string; 84: task: DeepImmutable<LocalWorkflowTaskState>; 85: } | { 86: id: string; 87: type: 'monitor_mcp'; 88: label: string; 89: status: string; 90: task: DeepImmutable<MonitorMcpTaskState>; 91: } | { 92: id: string; 93: type: 'dream'; 94: label: string; 95: status: string; 96: task: DeepImmutable<DreamTaskState>; 97: } | { 98: id: string; 99: type: 'leader'; 100: label: string; 101: status: 'running'; 102: }; 103: const WorkflowDetailDialog = feature('WORKFLOW_SCRIPTS') ? (require('./WorkflowDetailDialog.js') as typeof import('./WorkflowDetailDialog.js')).WorkflowDetailDialog : null; 104: const workflowTaskModule = feature('WORKFLOW_SCRIPTS') ? require('src/tasks/LocalWorkflowTask/LocalWorkflowTask.js') as typeof import('src/tasks/LocalWorkflowTask/LocalWorkflowTask.js') : null; 105: const killWorkflowTask = workflowTaskModule?.killWorkflowTask ?? null; 106: const skipWorkflowAgent = workflowTaskModule?.skipWorkflowAgent ?? null; 107: const retryWorkflowAgent = workflowTaskModule?.retryWorkflowAgent ?? null; 108: const monitorMcpModule = feature('MONITOR_TOOL') ? require('../../tasks/MonitorMcpTask/MonitorMcpTask.js') as typeof import('../../tasks/MonitorMcpTask/MonitorMcpTask.js') : null; 109: const killMonitorMcp = monitorMcpModule?.killMonitorMcp ?? null; 110: const MonitorMcpDetailDialog = feature('MONITOR_TOOL') ? (require('./MonitorMcpDetailDialog.js') as typeof import('./MonitorMcpDetailDialog.js')).MonitorMcpDetailDialog : null; 111: function getSelectableBackgroundTasks(tasks: Record<string, TaskState> | undefined, foregroundedTaskId: string | undefined): TaskState[] { 112: const backgroundTasks = Object.values(tasks ?? {}).filter(isBackgroundTask); 113: return backgroundTasks.filter(task => !(task.type === 'local_agent' && task.id === foregroundedTaskId)); 114: } 115: export function BackgroundTasksDialog({ 116: onDone, 117: toolUseContext, 118: initialDetailTaskId 119: }: Props): React.ReactNode { 120: const tasks = useAppState(s => s.tasks); 121: const foregroundedTaskId = useAppState(s_0 => s_0.foregroundedTaskId); 122: const showSpinnerTree = useAppState(s_1 => s_1.expandedView) === 'teammates'; 123: const setAppState = useSetAppState(); 124: const killAgentsShortcut = useShortcutDisplay('chat:killAgents', 'Chat', 'ctrl+x ctrl+k'); 125: const typedTasks = tasks as Record<string, TaskState> | undefined; 126: const skippedListOnMount = useRef(false); 127: const [viewState, setViewState] = useState<ViewState>(() => { 128: if (initialDetailTaskId) { 129: skippedListOnMount.current = true; 130: return { 131: mode: 'detail', 132: itemId: initialDetailTaskId 133: }; 134: } 135: const allItems = getSelectableBackgroundTasks(typedTasks, foregroundedTaskId); 136: if (allItems.length === 1) { 137: skippedListOnMount.current = true; 138: return { 139: mode: 'detail', 140: itemId: allItems[0]!.id 141: }; 142: } 143: return { 144: mode: 'list' 145: }; 146: }); 147: const [selectedIndex, setSelectedIndex] = useState<number>(0); 148: useRegisterOverlay('background-tasks-dialog'); 149: const { 150: bashTasks, 151: remoteSessions, 152: agentTasks, 153: teammateTasks, 154: workflowTasks, 155: mcpMonitors, 156: dreamTasks: dreamTasks_0, 157: allSelectableItems 158: } = useMemo(() => { 159: const backgroundTasks = Object.values(typedTasks ?? {}).filter(isBackgroundTask); 160: const allItems_0 = backgroundTasks.map(toListItem); 161: const sorted = allItems_0.sort((a, b) => { 162: const aStatus = a.status; 163: const bStatus = b.status; 164: if (aStatus === 'running' && bStatus !== 'running') return -1; 165: if (aStatus !== 'running' && bStatus === 'running') return 1; 166: const aTime = 'task' in a ? a.task.startTime : 0; 167: const bTime = 'task' in b ? b.task.startTime : 0; 168: return bTime - aTime; 169: }); 170: const bash = sorted.filter(item => item.type === 'local_bash'); 171: const remote = sorted.filter(item_0 => item_0.type === 'remote_agent'); 172: const agent = sorted.filter(item_1 => item_1.type === 'local_agent' && item_1.id !== foregroundedTaskId); 173: const workflows = sorted.filter(item_2 => item_2.type === 'local_workflow'); 174: const monitorMcp = sorted.filter(item_3 => item_3.type === 'monitor_mcp'); 175: const dreamTasks = sorted.filter(item_4 => item_4.type === 'dream'); 176: const teammates = showSpinnerTree ? [] : sorted.filter(item_5 => item_5.type === 'in_process_teammate'); 177: const leaderItem: ListItem[] = teammates.length > 0 ? [{ 178: id: '__leader__', 179: type: 'leader', 180: label: `@${TEAM_LEAD_NAME}`, 181: status: 'running' 182: }] : []; 183: return { 184: bashTasks: bash, 185: remoteSessions: remote, 186: agentTasks: agent, 187: workflowTasks: workflows, 188: mcpMonitors: monitorMcp, 189: dreamTasks, 190: teammateTasks: [...leaderItem, ...teammates], 191: allSelectableItems: [...leaderItem, ...teammates, ...bash, ...monitorMcp, ...remote, ...agent, ...workflows, ...dreamTasks] 192: }; 193: }, [typedTasks, foregroundedTaskId, showSpinnerTree]); 194: const currentSelection = allSelectableItems[selectedIndex] ?? null; 195: useKeybindings({ 196: 'confirm:previous': () => setSelectedIndex(prev => Math.max(0, prev - 1)), 197: 'confirm:next': () => setSelectedIndex(prev_0 => Math.min(allSelectableItems.length - 1, prev_0 + 1)), 198: 'confirm:yes': () => { 199: const current = allSelectableItems[selectedIndex]; 200: if (current) { 201: if (current.type === 'leader') { 202: exitTeammateView(setAppState); 203: onDone('Viewing leader', { 204: display: 'system' 205: }); 206: } else { 207: setViewState({ 208: mode: 'detail', 209: itemId: current.id 210: }); 211: } 212: } 213: } 214: }, { 215: context: 'Confirmation', 216: isActive: viewState.mode === 'list' 217: }); 218: const handleKeyDown = (e: KeyboardEvent) => { 219: if (viewState.mode !== 'list') return; 220: if (e.key === 'left') { 221: e.preventDefault(); 222: onDone('Background tasks dialog dismissed', { 223: display: 'system' 224: }); 225: return; 226: } 227: const currentSelection_0 = allSelectableItems[selectedIndex]; 228: if (!currentSelection_0) return; 229: if (e.key === 'x') { 230: e.preventDefault(); 231: if (currentSelection_0.type === 'local_bash' && currentSelection_0.status === 'running') { 232: void killShellTask(currentSelection_0.id); 233: } else if (currentSelection_0.type === 'local_agent' && currentSelection_0.status === 'running') { 234: void killAgentTask(currentSelection_0.id); 235: } else if (currentSelection_0.type === 'in_process_teammate' && currentSelection_0.status === 'running') { 236: void killTeammateTask(currentSelection_0.id); 237: } else if (currentSelection_0.type === 'local_workflow' && currentSelection_0.status === 'running' && killWorkflowTask) { 238: killWorkflowTask(currentSelection_0.id, setAppState); 239: } else if (currentSelection_0.type === 'monitor_mcp' && currentSelection_0.status === 'running' && killMonitorMcp) { 240: killMonitorMcp(currentSelection_0.id, setAppState); 241: } else if (currentSelection_0.type === 'dream' && currentSelection_0.status === 'running') { 242: void killDreamTask(currentSelection_0.id); 243: } else if (currentSelection_0.type === 'remote_agent' && currentSelection_0.status === 'running') { 244: if (currentSelection_0.task.isUltraplan) { 245: void stopUltraplan(currentSelection_0.id, currentSelection_0.task.sessionId, setAppState); 246: } else { 247: void killRemoteAgentTask(currentSelection_0.id); 248: } 249: } 250: } 251: if (e.key === 'f') { 252: if (currentSelection_0.type === 'in_process_teammate' && currentSelection_0.status === 'running') { 253: e.preventDefault(); 254: enterTeammateView(currentSelection_0.id, setAppState); 255: onDone('Viewing teammate', { 256: display: 'system' 257: }); 258: } else if (currentSelection_0.type === 'leader') { 259: e.preventDefault(); 260: exitTeammateView(setAppState); 261: onDone('Viewing leader', { 262: display: 'system' 263: }); 264: } 265: } 266: }; 267: async function killShellTask(taskId: string): Promise<void> { 268: await LocalShellTask.kill(taskId, setAppState); 269: } 270: async function killAgentTask(taskId_0: string): Promise<void> { 271: await LocalAgentTask.kill(taskId_0, setAppState); 272: } 273: async function killTeammateTask(taskId_1: string): Promise<void> { 274: await InProcessTeammateTask.kill(taskId_1, setAppState); 275: } 276: async function killDreamTask(taskId_2: string): Promise<void> { 277: await DreamTask.kill(taskId_2, setAppState); 278: } 279: async function killRemoteAgentTask(taskId_3: string): Promise<void> { 280: await RemoteAgentTask.kill(taskId_3, setAppState); 281: } 282: const onDoneEvent = useEffectEvent(onDone); 283: useEffect(() => { 284: if (viewState.mode !== 'list') { 285: const task = (typedTasks ?? {})[viewState.itemId]; 286: if (!task || task.type !== 'local_workflow' && !isBackgroundTask(task)) { 287: if (skippedListOnMount.current) { 288: onDoneEvent('Background tasks dialog dismissed', { 289: display: 'system' 290: }); 291: } else { 292: setViewState({ 293: mode: 'list' 294: }); 295: } 296: } 297: } 298: const totalItems = allSelectableItems.length; 299: if (selectedIndex >= totalItems && totalItems > 0) { 300: setSelectedIndex(totalItems - 1); 301: } 302: }, [viewState, typedTasks, selectedIndex, allSelectableItems, onDoneEvent]); 303: const goBackToList = () => { 304: if (skippedListOnMount.current && allSelectableItems.length <= 1) { 305: onDone('Background tasks dialog dismissed', { 306: display: 'system' 307: }); 308: } else { 309: skippedListOnMount.current = false; 310: setViewState({ 311: mode: 'list' 312: }); 313: } 314: }; 315: if (viewState.mode !== 'list' && typedTasks) { 316: const task_0 = typedTasks[viewState.itemId]; 317: if (!task_0) { 318: return null; 319: } 320: switch (task_0.type) { 321: case 'local_bash': 322: return <ShellDetailDialog shell={task_0} onDone={onDone} onKillShell={() => void killShellTask(task_0.id)} onBack={goBackToList} key={`shell-${task_0.id}`} />; 323: case 'local_agent': 324: return <AsyncAgentDetailDialog agent={task_0} onDone={onDone} onKillAgent={() => void killAgentTask(task_0.id)} onBack={goBackToList} key={`agent-${task_0.id}`} />; 325: case 'remote_agent': 326: return <RemoteSessionDetailDialog session={task_0} onDone={onDone} toolUseContext={toolUseContext} onBack={goBackToList} onKill={task_0.status !== 'running' ? undefined : task_0.isUltraplan ? () => void stopUltraplan(task_0.id, task_0.sessionId, setAppState) : () => void killRemoteAgentTask(task_0.id)} key={`session-${task_0.id}`} />; 327: case 'in_process_teammate': 328: return <InProcessTeammateDetailDialog teammate={task_0} onDone={onDone} onKill={task_0.status === 'running' ? () => void killTeammateTask(task_0.id) : undefined} onBack={goBackToList} onForeground={task_0.status === 'running' ? () => { 329: enterTeammateView(task_0.id, setAppState); 330: onDone('Viewing teammate', { 331: display: 'system' 332: }); 333: } : undefined} key={`teammate-${task_0.id}`} />; 334: case 'local_workflow': 335: if (!WorkflowDetailDialog) return null; 336: return <WorkflowDetailDialog workflow={task_0} onDone={onDone} onKill={task_0.status === 'running' && killWorkflowTask ? () => killWorkflowTask(task_0.id, setAppState) : undefined} onSkipAgent={task_0.status === 'running' && skipWorkflowAgent ? agentId => skipWorkflowAgent(task_0.id, agentId, setAppState) : undefined} onRetryAgent={task_0.status === 'running' && retryWorkflowAgent ? agentId_0 => retryWorkflowAgent(task_0.id, agentId_0, setAppState) : undefined} onBack={goBackToList} key={`workflow-${task_0.id}`} />; 337: case 'monitor_mcp': 338: if (!MonitorMcpDetailDialog) return null; 339: return <MonitorMcpDetailDialog task={task_0} onKill={task_0.status === 'running' && killMonitorMcp ? () => killMonitorMcp(task_0.id, setAppState) : undefined} onBack={goBackToList} key={`monitor-mcp-${task_0.id}`} />; 340: case 'dream': 341: return <DreamDetailDialog task={task_0} onDone={() => onDone('Background tasks dialog dismissed', { 342: display: 'system' 343: })} onBack={goBackToList} onKill={task_0.status === 'running' ? () => void killDreamTask(task_0.id) : undefined} key={`dream-${task_0.id}`} />; 344: } 345: } 346: const runningBashCount = count(bashTasks, _ => _.status === 'running'); 347: const runningAgentCount = count(remoteSessions, __0 => __0.status === 'running' || __0.status === 'pending') + count(agentTasks, __1 => __1.status === 'running'); 348: const runningTeammateCount = count(teammateTasks, __2 => __2.status === 'running'); 349: const subtitle = intersperse([...(runningTeammateCount > 0 ? [<Text key="teammates"> 350: {runningTeammateCount}{' '} 351: {runningTeammateCount !== 1 ? 'agents' : 'agent'} 352: </Text>] : []), ...(runningBashCount > 0 ? [<Text key="shells"> 353: {runningBashCount}{' '} 354: {runningBashCount !== 1 ? 'active shells' : 'active shell'} 355: </Text>] : []), ...(runningAgentCount > 0 ? [<Text key="agents"> 356: {runningAgentCount}{' '} 357: {runningAgentCount !== 1 ? 'active agents' : 'active agent'} 358: </Text>] : [])], index => <Text key={`separator-${index}`}> · </Text>); 359: const actions = [<KeyboardShortcutHint key="upDown" shortcut="↑/↓" action="select" />, <KeyboardShortcutHint key="enter" shortcut="Enter" action="view" />, ...(currentSelection?.type === 'in_process_teammate' && currentSelection.status === 'running' ? [<KeyboardShortcutHint key="foreground" shortcut="f" action="foreground" />] : []), ...((currentSelection?.type === 'local_bash' || currentSelection?.type === 'local_agent' || currentSelection?.type === 'in_process_teammate' || currentSelection?.type === 'local_workflow' || currentSelection?.type === 'monitor_mcp' || currentSelection?.type === 'dream' || currentSelection?.type === 'remote_agent') && currentSelection.status === 'running' ? [<KeyboardShortcutHint key="kill" shortcut="x" action="stop" />] : []), ...(agentTasks.some(t => t.status === 'running') ? [<KeyboardShortcutHint key="kill-all" shortcut={killAgentsShortcut} action="stop all agents" />] : []), <KeyboardShortcutHint key="esc" shortcut="←/Esc" action="close" />]; 360: const handleCancel = () => onDone('Background tasks dialog dismissed', { 361: display: 'system' 362: }); 363: function renderInputGuide(exitState: ExitState): React.ReactNode { 364: if (exitState.pending) { 365: return <Text>Press {exitState.keyName} again to exit</Text>; 366: } 367: return <Byline>{actions}</Byline>; 368: } 369: return <Box flexDirection="column" tabIndex={0} autoFocus onKeyDown={handleKeyDown}> 370: <Dialog title="Background tasks" subtitle={<>{subtitle}</>} onCancel={handleCancel} color="background" inputGuide={renderInputGuide}> 371: {allSelectableItems.length === 0 ? <Text dimColor>No tasks currently running</Text> : <Box flexDirection="column"> 372: {teammateTasks.length > 0 && <Box flexDirection="column"> 373: {(bashTasks.length > 0 || remoteSessions.length > 0 || agentTasks.length > 0) && <Text dimColor> 374: <Text bold>{' '}Agents</Text> ( 375: {count(teammateTasks, i => i.type !== 'leader')}) 376: </Text>} 377: <Box flexDirection="column"> 378: <TeammateTaskGroups teammateTasks={teammateTasks} currentSelectionId={currentSelection?.id} /> 379: </Box> 380: </Box>} 381: {bashTasks.length > 0 && <Box flexDirection="column" marginTop={teammateTasks.length > 0 ? 1 : 0}> 382: {(teammateTasks.length > 0 || remoteSessions.length > 0 || agentTasks.length > 0) && <Text dimColor> 383: <Text bold>{' '}Shells</Text> ({bashTasks.length}) 384: </Text>} 385: <Box flexDirection="column"> 386: {bashTasks.map(item_6 => <Item key={item_6.id} item={item_6} isSelected={item_6.id === currentSelection?.id} />)} 387: </Box> 388: </Box>} 389: {mcpMonitors.length > 0 && <Box flexDirection="column" marginTop={teammateTasks.length > 0 || bashTasks.length > 0 ? 1 : 0}> 390: <Text dimColor> 391: <Text bold>{' '}Monitors</Text> ({mcpMonitors.length}) 392: </Text> 393: <Box flexDirection="column"> 394: {mcpMonitors.map(item_7 => <Item key={item_7.id} item={item_7} isSelected={item_7.id === currentSelection?.id} />)} 395: </Box> 396: </Box>} 397: {remoteSessions.length > 0 && <Box flexDirection="column" marginTop={teammateTasks.length > 0 || bashTasks.length > 0 || mcpMonitors.length > 0 ? 1 : 0}> 398: <Text dimColor> 399: <Text bold>{' '}Remote agents</Text> ({remoteSessions.length} 400: ) 401: </Text> 402: <Box flexDirection="column"> 403: {remoteSessions.map(item_8 => <Item key={item_8.id} item={item_8} isSelected={item_8.id === currentSelection?.id} />)} 404: </Box> 405: </Box>} 406: {agentTasks.length > 0 && <Box flexDirection="column" marginTop={teammateTasks.length > 0 || bashTasks.length > 0 || mcpMonitors.length > 0 || remoteSessions.length > 0 ? 1 : 0}> 407: <Text dimColor> 408: <Text bold>{' '}Local agents</Text> ({agentTasks.length}) 409: </Text> 410: <Box flexDirection="column"> 411: {agentTasks.map(item_9 => <Item key={item_9.id} item={item_9} isSelected={item_9.id === currentSelection?.id} />)} 412: </Box> 413: </Box>} 414: {workflowTasks.length > 0 && <Box flexDirection="column" marginTop={teammateTasks.length > 0 || bashTasks.length > 0 || mcpMonitors.length > 0 || remoteSessions.length > 0 || agentTasks.length > 0 ? 1 : 0}> 415: <Text dimColor> 416: <Text bold>{' '}Workflows</Text> ({workflowTasks.length}) 417: </Text> 418: <Box flexDirection="column"> 419: {workflowTasks.map(item_10 => <Item key={item_10.id} item={item_10} isSelected={item_10.id === currentSelection?.id} />)} 420: </Box> 421: </Box>} 422: {dreamTasks_0.length > 0 && <Box flexDirection="column" marginTop={teammateTasks.length > 0 || bashTasks.length > 0 || mcpMonitors.length > 0 || remoteSessions.length > 0 || agentTasks.length > 0 || workflowTasks.length > 0 ? 1 : 0}> 423: <Box flexDirection="column"> 424: {dreamTasks_0.map(item_11 => <Item key={item_11.id} item={item_11} isSelected={item_11.id === currentSelection?.id} />)} 425: </Box> 426: </Box>} 427: </Box>} 428: </Dialog> 429: </Box>; 430: } 431: function toListItem(task: BackgroundTaskState): ListItem { 432: switch (task.type) { 433: case 'local_bash': 434: return { 435: id: task.id, 436: type: 'local_bash', 437: label: task.kind === 'monitor' ? task.description : task.command, 438: status: task.status, 439: task 440: }; 441: case 'remote_agent': 442: return { 443: id: task.id, 444: type: 'remote_agent', 445: label: task.title, 446: status: task.status, 447: task 448: }; 449: case 'local_agent': 450: return { 451: id: task.id, 452: type: 'local_agent', 453: label: task.description, 454: status: task.status, 455: task 456: }; 457: case 'in_process_teammate': 458: return { 459: id: task.id, 460: type: 'in_process_teammate', 461: label: `@${task.identity.agentName}`, 462: status: task.status, 463: task 464: }; 465: case 'local_workflow': 466: return { 467: id: task.id, 468: type: 'local_workflow', 469: label: task.summary ?? task.description, 470: status: task.status, 471: task 472: }; 473: case 'monitor_mcp': 474: return { 475: id: task.id, 476: type: 'monitor_mcp', 477: label: task.description, 478: status: task.status, 479: task 480: }; 481: case 'dream': 482: return { 483: id: task.id, 484: type: 'dream', 485: label: task.description, 486: status: task.status, 487: task 488: }; 489: } 490: } 491: function Item(t0) { 492: const $ = _c(14); 493: const { 494: item, 495: isSelected 496: } = t0; 497: const { 498: columns 499: } = useTerminalSize(); 500: const maxActivityWidth = Math.max(30, columns - 26); 501: let t1; 502: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 503: t1 = isCoordinatorMode(); 504: $[0] = t1; 505: } else { 506: t1 = $[0]; 507: } 508: const useGreyPointer = t1; 509: const t2 = useGreyPointer && isSelected; 510: const t3 = isSelected ? figures.pointer + " " : " "; 511: let t4; 512: if ($[1] !== t2 || $[2] !== t3) { 513: t4 = <Text dimColor={t2}>{t3}</Text>; 514: $[1] = t2; 515: $[2] = t3; 516: $[3] = t4; 517: } else { 518: t4 = $[3]; 519: } 520: const t5 = isSelected && !useGreyPointer ? "suggestion" : undefined; 521: let t6; 522: if ($[4] !== item.task || $[5] !== item.type || $[6] !== maxActivityWidth) { 523: t6 = item.type === "leader" ? <Text>@{TEAM_LEAD_NAME}</Text> : <BackgroundTaskComponent task={item.task} maxActivityWidth={maxActivityWidth} />; 524: $[4] = item.task; 525: $[5] = item.type; 526: $[6] = maxActivityWidth; 527: $[7] = t6; 528: } else { 529: t6 = $[7]; 530: } 531: let t7; 532: if ($[8] !== t5 || $[9] !== t6) { 533: t7 = <Text color={t5}>{t6}</Text>; 534: $[8] = t5; 535: $[9] = t6; 536: $[10] = t7; 537: } else { 538: t7 = $[10]; 539: } 540: let t8; 541: if ($[11] !== t4 || $[12] !== t7) { 542: t8 = <Box flexDirection="row">{t4}{t7}</Box>; 543: $[11] = t4; 544: $[12] = t7; 545: $[13] = t8; 546: } else { 547: t8 = $[13]; 548: } 549: return t8; 550: } 551: function TeammateTaskGroups(t0) { 552: const $ = _c(3); 553: const { 554: teammateTasks, 555: currentSelectionId 556: } = t0; 557: let t1; 558: if ($[0] !== currentSelectionId || $[1] !== teammateTasks) { 559: const leaderItems = teammateTasks.filter(_temp); 560: const teammateItems = teammateTasks.filter(_temp2); 561: const teams = new Map(); 562: for (const item of teammateItems) { 563: const teamName = item.task.identity.teamName; 564: const group = teams.get(teamName); 565: if (group) { 566: group.push(item); 567: } else { 568: teams.set(teamName, [item]); 569: } 570: } 571: const teamEntries = [...teams.entries()]; 572: t1 = <>{teamEntries.map(t2 => { 573: const [teamName_0, items] = t2; 574: const memberCount = items.length + leaderItems.length; 575: return <Box key={teamName_0} flexDirection="column"><Text dimColor={true}>{" "}Team: {teamName_0} ({memberCount})</Text>{leaderItems.map(item_0 => <Item key={`${item_0.id}-${teamName_0}`} item={item_0} isSelected={item_0.id === currentSelectionId} />)}{items.map(item_1 => <Item key={item_1.id} item={item_1} isSelected={item_1.id === currentSelectionId} />)}</Box>; 576: })}</>; 577: $[0] = currentSelectionId; 578: $[1] = teammateTasks; 579: $[2] = t1; 580: } else { 581: t1 = $[2]; 582: } 583: return t1; 584: } 585: function _temp2(i_0) { 586: return i_0.type === "in_process_teammate"; 587: } 588: function _temp(i) { 589: return i.type === "leader"; 590: }

File: src/components/tasks/BackgroundTaskStatus.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { useMemo, useState } from 'react'; 5: import { useTerminalSize } from 'src/hooks/useTerminalSize.js'; 6: import { stringWidth } from 'src/ink/stringWidth.js'; 7: import { useAppState, useSetAppState } from 'src/state/AppState.js'; 8: import { enterTeammateView, exitTeammateView } from 'src/state/teammateViewHelpers.js'; 9: import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'; 10: import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js'; 11: import { type BackgroundTaskState, isBackgroundTask, type TaskState } from 'src/tasks/types.js'; 12: import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js'; 13: import { Box, Text } from '../../ink.js'; 14: import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js'; 15: import type { Theme } from '../../utils/theme.js'; 16: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 17: import { shouldHideTasksFooter } from './taskStatusUtils.js'; 18: type Props = { 19: tasksSelected: boolean; 20: isViewingTeammate?: boolean; 21: teammateFooterIndex?: number; 22: isLeaderIdle?: boolean; 23: onOpenDialog?: (taskId?: string) => void; 24: }; 25: export function BackgroundTaskStatus(t0) { 26: const $ = _c(48); 27: const { 28: tasksSelected, 29: isViewingTeammate, 30: teammateFooterIndex: t1, 31: isLeaderIdle: t2, 32: onOpenDialog 33: } = t0; 34: const teammateFooterIndex = t1 === undefined ? 0 : t1; 35: const isLeaderIdle = t2 === undefined ? false : t2; 36: const setAppState = useSetAppState(); 37: const { 38: columns 39: } = useTerminalSize(); 40: const tasks = useAppState(_temp); 41: const viewingAgentTaskId = useAppState(_temp2); 42: let t3; 43: if ($[0] !== tasks) { 44: t3 = (Object.values(tasks ?? {}) as TaskState[]).filter(_temp3); 45: $[0] = tasks; 46: $[1] = t3; 47: } else { 48: t3 = $[1]; 49: } 50: const runningTasks = t3; 51: const expandedView = useAppState(_temp4); 52: const showSpinnerTree = expandedView === "teammates"; 53: const allTeammates = !showSpinnerTree && runningTasks.length > 0 && runningTasks.every(_temp5); 54: let t4; 55: if ($[2] !== runningTasks) { 56: t4 = runningTasks.filter(_temp6).sort(_temp7); 57: $[2] = runningTasks; 58: $[3] = t4; 59: } else { 60: t4 = $[3]; 61: } 62: const teammateEntries = t4; 63: let t5; 64: if ($[4] !== isLeaderIdle) { 65: t5 = { 66: name: "main", 67: color: undefined as keyof Theme | undefined, 68: isIdle: isLeaderIdle, 69: taskId: undefined as string | undefined 70: }; 71: $[4] = isLeaderIdle; 72: $[5] = t5; 73: } else { 74: t5 = $[5]; 75: } 76: const mainPill = t5; 77: let t6; 78: if ($[6] !== mainPill || $[7] !== tasksSelected || $[8] !== teammateEntries) { 79: const teammatePills = teammateEntries.map(_temp8); 80: if (!tasksSelected) { 81: teammatePills.sort(_temp9); 82: } 83: const pills = [mainPill, ...teammatePills]; 84: t6 = pills.map(_temp0); 85: $[6] = mainPill; 86: $[7] = tasksSelected; 87: $[8] = teammateEntries; 88: $[9] = t6; 89: } else { 90: t6 = $[9]; 91: } 92: const allPills = t6; 93: let t7; 94: if ($[10] !== allPills) { 95: t7 = allPills.map(_temp1); 96: $[10] = allPills; 97: $[11] = t7; 98: } else { 99: t7 = $[11]; 100: } 101: const pillWidths = t7; 102: if (allTeammates || !showSpinnerTree && isViewingTeammate) { 103: const selectedIdx = tasksSelected ? teammateFooterIndex : -1; 104: let t8; 105: if ($[12] !== teammateEntries || $[13] !== viewingAgentTaskId) { 106: t8 = viewingAgentTaskId ? teammateEntries.findIndex(t_3 => t_3.id === viewingAgentTaskId) + 1 : 0; 107: $[12] = teammateEntries; 108: $[13] = viewingAgentTaskId; 109: $[14] = t8; 110: } else { 111: t8 = $[14]; 112: } 113: const viewedIdx = t8; 114: const availableWidth = Math.max(20, columns - 20 - 4); 115: const t9 = selectedIdx >= 0 ? selectedIdx : 0; 116: let t10; 117: if ($[15] !== availableWidth || $[16] !== pillWidths || $[17] !== t9) { 118: t10 = calculateHorizontalScrollWindow(pillWidths, availableWidth, 2, t9); 119: $[15] = availableWidth; 120: $[16] = pillWidths; 121: $[17] = t9; 122: $[18] = t10; 123: } else { 124: t10 = $[18]; 125: } 126: const { 127: startIndex, 128: endIndex, 129: showLeftArrow, 130: showRightArrow 131: } = t10; 132: let t11; 133: if ($[19] !== allPills || $[20] !== endIndex || $[21] !== startIndex) { 134: t11 = allPills.slice(startIndex, endIndex); 135: $[19] = allPills; 136: $[20] = endIndex; 137: $[21] = startIndex; 138: $[22] = t11; 139: } else { 140: t11 = $[22]; 141: } 142: const visiblePills = t11; 143: let t12; 144: if ($[23] !== showLeftArrow) { 145: t12 = showLeftArrow && <Text dimColor={true}>{figures.arrowLeft} </Text>; 146: $[23] = showLeftArrow; 147: $[24] = t12; 148: } else { 149: t12 = $[24]; 150: } 151: let t13; 152: if ($[25] !== selectedIdx || $[26] !== setAppState || $[27] !== viewedIdx || $[28] !== visiblePills) { 153: t13 = visiblePills.map((pill_1, i_1) => { 154: const needsSeparator = i_1 > 0; 155: return <React.Fragment key={pill_1.name}>{needsSeparator && <Text> </Text>}<AgentPill name={pill_1.name} color={pill_1.color} isSelected={selectedIdx === pill_1.idx} isViewed={viewedIdx === pill_1.idx} isIdle={pill_1.isIdle} onClick={() => pill_1.taskId ? enterTeammateView(pill_1.taskId, setAppState) : exitTeammateView(setAppState)} /></React.Fragment>; 156: }); 157: $[25] = selectedIdx; 158: $[26] = setAppState; 159: $[27] = viewedIdx; 160: $[28] = visiblePills; 161: $[29] = t13; 162: } else { 163: t13 = $[29]; 164: } 165: let t14; 166: if ($[30] !== showRightArrow) { 167: t14 = showRightArrow && <Text dimColor={true}> {figures.arrowRight}</Text>; 168: $[30] = showRightArrow; 169: $[31] = t14; 170: } else { 171: t14 = $[31]; 172: } 173: let t15; 174: if ($[32] === Symbol.for("react.memo_cache_sentinel")) { 175: t15 = <Text dimColor={true}>{" \xB7 "}<KeyboardShortcutHint shortcut={"shift + \u2193"} action="expand" /></Text>; 176: $[32] = t15; 177: } else { 178: t15 = $[32]; 179: } 180: let t16; 181: if ($[33] !== t12 || $[34] !== t13 || $[35] !== t14) { 182: t16 = <>{t12}{t13}{t14}{t15}</>; 183: $[33] = t12; 184: $[34] = t13; 185: $[35] = t14; 186: $[36] = t16; 187: } else { 188: t16 = $[36]; 189: } 190: return t16; 191: } 192: if (shouldHideTasksFooter(tasks ?? {}, showSpinnerTree)) { 193: return null; 194: } 195: if (runningTasks.length === 0) { 196: return null; 197: } 198: let t8; 199: if ($[37] !== runningTasks) { 200: t8 = getPillLabel(runningTasks); 201: $[37] = runningTasks; 202: $[38] = t8; 203: } else { 204: t8 = $[38]; 205: } 206: let t9; 207: if ($[39] !== onOpenDialog || $[40] !== t8 || $[41] !== tasksSelected) { 208: t9 = <SummaryPill selected={tasksSelected} onClick={onOpenDialog}>{t8}</SummaryPill>; 209: $[39] = onOpenDialog; 210: $[40] = t8; 211: $[41] = tasksSelected; 212: $[42] = t9; 213: } else { 214: t9 = $[42]; 215: } 216: let t10; 217: if ($[43] !== runningTasks) { 218: t10 = pillNeedsCta(runningTasks) && <Text dimColor={true}> · {figures.arrowDown} to view</Text>; 219: $[43] = runningTasks; 220: $[44] = t10; 221: } else { 222: t10 = $[44]; 223: } 224: let t11; 225: if ($[45] !== t10 || $[46] !== t9) { 226: t11 = <>{t9}{t10}</>; 227: $[45] = t10; 228: $[46] = t9; 229: $[47] = t11; 230: } else { 231: t11 = $[47]; 232: } 233: return t11; 234: } 235: function _temp1(pill_0, i_0) { 236: const pillText = `@${pill_0.name}`; 237: return stringWidth(pillText) + (i_0 > 0 ? 1 : 0); 238: } 239: function _temp0(pill, i) { 240: return { 241: ...pill, 242: idx: i 243: }; 244: } 245: function _temp9(a_0, b_0) { 246: if (a_0.isIdle !== b_0.isIdle) { 247: return a_0.isIdle ? 1 : -1; 248: } 249: return 0; 250: } 251: function _temp8(t_2) { 252: return { 253: name: t_2.identity.agentName, 254: color: getAgentThemeColor(t_2.identity.color), 255: isIdle: t_2.isIdle, 256: taskId: t_2.id 257: }; 258: } 259: function _temp7(a, b) { 260: return a.identity.agentName.localeCompare(b.identity.agentName); 261: } 262: function _temp6(t_1) { 263: return t_1.type === "in_process_teammate"; 264: } 265: function _temp5(t_0) { 266: return t_0.type === "in_process_teammate"; 267: } 268: function _temp4(s_1) { 269: return s_1.expandedView; 270: } 271: function _temp3(t) { 272: return isBackgroundTask(t) && !(false && isPanelAgentTask(t)); 273: } 274: function _temp2(s_0) { 275: return s_0.viewingAgentTaskId; 276: } 277: function _temp(s) { 278: return s.tasks; 279: } 280: type AgentPillProps = { 281: name: string; 282: color?: keyof Theme; 283: isSelected: boolean; 284: isViewed: boolean; 285: isIdle: boolean; 286: onClick?: () => void; 287: }; 288: function AgentPill(t0) { 289: const $ = _c(19); 290: const { 291: name, 292: color, 293: isSelected, 294: isViewed, 295: isIdle, 296: onClick 297: } = t0; 298: const [hover, setHover] = useState(false); 299: const highlighted = isSelected || hover; 300: let label; 301: if (highlighted) { 302: let t1; 303: if ($[0] !== color || $[1] !== isViewed || $[2] !== name) { 304: t1 = color ? <Text backgroundColor={color} color="inverseText" bold={isViewed}>@{name}</Text> : <Text color="background" inverse={true} bold={isViewed}>@{name}</Text>; 305: $[0] = color; 306: $[1] = isViewed; 307: $[2] = name; 308: $[3] = t1; 309: } else { 310: t1 = $[3]; 311: } 312: label = t1; 313: } else { 314: if (isIdle) { 315: let t1; 316: if ($[4] !== isViewed || $[5] !== name) { 317: t1 = <Text dimColor={true} bold={isViewed}>@{name}</Text>; 318: $[4] = isViewed; 319: $[5] = name; 320: $[6] = t1; 321: } else { 322: t1 = $[6]; 323: } 324: label = t1; 325: } else { 326: if (isViewed) { 327: let t1; 328: if ($[7] !== color || $[8] !== name) { 329: t1 = <Text color={color} bold={true}>@{name}</Text>; 330: $[7] = color; 331: $[8] = name; 332: $[9] = t1; 333: } else { 334: t1 = $[9]; 335: } 336: label = t1; 337: } else { 338: const t1 = !color; 339: let t2; 340: if ($[10] !== color || $[11] !== name || $[12] !== t1) { 341: t2 = <Text color={color} dimColor={t1}>@{name}</Text>; 342: $[10] = color; 343: $[11] = name; 344: $[12] = t1; 345: $[13] = t2; 346: } else { 347: t2 = $[13]; 348: } 349: label = t2; 350: } 351: } 352: } 353: if (!onClick) { 354: return label; 355: } 356: let t1; 357: let t2; 358: if ($[14] === Symbol.for("react.memo_cache_sentinel")) { 359: t1 = () => setHover(true); 360: t2 = () => setHover(false); 361: $[14] = t1; 362: $[15] = t2; 363: } else { 364: t1 = $[14]; 365: t2 = $[15]; 366: } 367: let t3; 368: if ($[16] !== label || $[17] !== onClick) { 369: t3 = <Box onClick={onClick} onMouseEnter={t1} onMouseLeave={t2}>{label}</Box>; 370: $[16] = label; 371: $[17] = onClick; 372: $[18] = t3; 373: } else { 374: t3 = $[18]; 375: } 376: return t3; 377: } 378: function SummaryPill(t0) { 379: const $ = _c(8); 380: const { 381: selected, 382: onClick, 383: children 384: } = t0; 385: const [hover, setHover] = useState(false); 386: const t1 = selected || hover; 387: let t2; 388: if ($[0] !== children || $[1] !== t1) { 389: t2 = <Text color="background" inverse={t1}>{children}</Text>; 390: $[0] = children; 391: $[1] = t1; 392: $[2] = t2; 393: } else { 394: t2 = $[2]; 395: } 396: const label = t2; 397: if (!onClick) { 398: return label; 399: } 400: let t3; 401: let t4; 402: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 403: t3 = () => setHover(true); 404: t4 = () => setHover(false); 405: $[3] = t3; 406: $[4] = t4; 407: } else { 408: t3 = $[3]; 409: t4 = $[4]; 410: } 411: let t5; 412: if ($[5] !== label || $[6] !== onClick) { 413: t5 = <Box onClick={onClick} onMouseEnter={t3} onMouseLeave={t4}>{label}</Box>; 414: $[5] = label; 415: $[6] = onClick; 416: $[7] = t5; 417: } else { 418: t5 = $[7]; 419: } 420: return t5; 421: } 422: function getAgentThemeColor(colorName: string | undefined): keyof Theme | undefined { 423: if (!colorName) return undefined; 424: if (AGENT_COLORS.includes(colorName as AgentColorName)) { 425: return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]; 426: } 427: return undefined; 428: }

File: src/components/tasks/DreamDetailDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import type { DeepImmutable } from 'src/types/utils.js'; 4: import { useElapsedTime } from '../../hooks/useElapsedTime.js'; 5: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 6: import { Box, Text } from '../../ink.js'; 7: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 8: import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js'; 9: import { plural } from '../../utils/stringUtils.js'; 10: import { Byline } from '../design-system/Byline.js'; 11: import { Dialog } from '../design-system/Dialog.js'; 12: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 13: type Props = { 14: task: DeepImmutable<DreamTaskState>; 15: onDone: () => void; 16: onBack?: () => void; 17: onKill?: () => void; 18: }; 19: const VISIBLE_TURNS = 6; 20: export function DreamDetailDialog(t0) { 21: const $ = _c(70); 22: const { 23: task, 24: onDone, 25: onBack, 26: onKill 27: } = t0; 28: const elapsedTime = useElapsedTime(task.startTime, task.status === "running", 1000, 0); 29: let t1; 30: if ($[0] !== onDone) { 31: t1 = { 32: "confirm:yes": onDone 33: }; 34: $[0] = onDone; 35: $[1] = t1; 36: } else { 37: t1 = $[1]; 38: } 39: let t2; 40: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 41: t2 = { 42: context: "Confirmation" 43: }; 44: $[2] = t2; 45: } else { 46: t2 = $[2]; 47: } 48: useKeybindings(t1, t2); 49: let t3; 50: if ($[3] !== onBack || $[4] !== onDone || $[5] !== onKill || $[6] !== task.status) { 51: t3 = e => { 52: if (e.key === " ") { 53: e.preventDefault(); 54: onDone(); 55: } else { 56: if (e.key === "left" && onBack) { 57: e.preventDefault(); 58: onBack(); 59: } else { 60: if (e.key === "x" && task.status === "running" && onKill) { 61: e.preventDefault(); 62: onKill(); 63: } 64: } 65: } 66: }; 67: $[3] = onBack; 68: $[4] = onDone; 69: $[5] = onKill; 70: $[6] = task.status; 71: $[7] = t3; 72: } else { 73: t3 = $[7]; 74: } 75: const handleKeyDown = t3; 76: let T0; 77: let T1; 78: let T2; 79: let t10; 80: let t11; 81: let t12; 82: let t13; 83: let t14; 84: let t15; 85: let t16; 86: let t4; 87: let t5; 88: let t6; 89: let t7; 90: let t8; 91: let t9; 92: if ($[8] !== elapsedTime || $[9] !== handleKeyDown || $[10] !== onBack || $[11] !== onDone || $[12] !== onKill || $[13] !== task.filesTouched.length || $[14] !== task.sessionsReviewing || $[15] !== task.status || $[16] !== task.turns) { 93: const visibleTurns = task.turns.filter(_temp); 94: const shown = visibleTurns.slice(-VISIBLE_TURNS); 95: const hidden = visibleTurns.length - shown.length; 96: T2 = Box; 97: t13 = "column"; 98: t14 = 0; 99: t15 = true; 100: t16 = handleKeyDown; 101: T1 = Dialog; 102: t8 = "Memory consolidation"; 103: const t17 = task.sessionsReviewing; 104: let t18; 105: if ($[33] !== task.sessionsReviewing) { 106: t18 = plural(task.sessionsReviewing, "session"); 107: $[33] = task.sessionsReviewing; 108: $[34] = t18; 109: } else { 110: t18 = $[34]; 111: } 112: let t19; 113: if ($[35] !== task.filesTouched.length) { 114: t19 = task.filesTouched.length > 0 && <>{" "}· {task.filesTouched.length}{" "}{plural(task.filesTouched.length, "file")} touched</>; 115: $[35] = task.filesTouched.length; 116: $[36] = t19; 117: } else { 118: t19 = $[36]; 119: } 120: if ($[37] !== elapsedTime || $[38] !== t18 || $[39] !== t19 || $[40] !== task.sessionsReviewing) { 121: t9 = <Text dimColor={true}>{elapsedTime} · reviewing {t17}{" "}{t18}{t19}</Text>; 122: $[37] = elapsedTime; 123: $[38] = t18; 124: $[39] = t19; 125: $[40] = task.sessionsReviewing; 126: $[41] = t9; 127: } else { 128: t9 = $[41]; 129: } 130: t10 = onDone; 131: t11 = "background"; 132: if ($[42] !== onBack || $[43] !== onKill || $[44] !== task.status) { 133: t12 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{task.status === "running" && onKill && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>; 134: $[42] = onBack; 135: $[43] = onKill; 136: $[44] = task.status; 137: $[45] = t12; 138: } else { 139: t12 = $[45]; 140: } 141: T0 = Box; 142: t4 = "column"; 143: t5 = 1; 144: let t20; 145: if ($[46] === Symbol.for("react.memo_cache_sentinel")) { 146: t20 = <Text bold={true}>Status:</Text>; 147: $[46] = t20; 148: } else { 149: t20 = $[46]; 150: } 151: if ($[47] !== task.status) { 152: t6 = <Text>{t20}{" "}{task.status === "running" ? <Text color="background">running</Text> : task.status === "completed" ? <Text color="success">{task.status}</Text> : <Text color="error">{task.status}</Text>}</Text>; 153: $[47] = task.status; 154: $[48] = t6; 155: } else { 156: t6 = $[48]; 157: } 158: t7 = shown.length === 0 ? <Text dimColor={true}>{task.status === "running" ? "Starting\u2026" : "(no text output)"}</Text> : <>{hidden > 0 && <Text dimColor={true}>({hidden} earlier {plural(hidden, "turn")})</Text>}{shown.map(_temp2)}</>; 159: $[8] = elapsedTime; 160: $[9] = handleKeyDown; 161: $[10] = onBack; 162: $[11] = onDone; 163: $[12] = onKill; 164: $[13] = task.filesTouched.length; 165: $[14] = task.sessionsReviewing; 166: $[15] = task.status; 167: $[16] = task.turns; 168: $[17] = T0; 169: $[18] = T1; 170: $[19] = T2; 171: $[20] = t10; 172: $[21] = t11; 173: $[22] = t12; 174: $[23] = t13; 175: $[24] = t14; 176: $[25] = t15; 177: $[26] = t16; 178: $[27] = t4; 179: $[28] = t5; 180: $[29] = t6; 181: $[30] = t7; 182: $[31] = t8; 183: $[32] = t9; 184: } else { 185: T0 = $[17]; 186: T1 = $[18]; 187: T2 = $[19]; 188: t10 = $[20]; 189: t11 = $[21]; 190: t12 = $[22]; 191: t13 = $[23]; 192: t14 = $[24]; 193: t15 = $[25]; 194: t16 = $[26]; 195: t4 = $[27]; 196: t5 = $[28]; 197: t6 = $[29]; 198: t7 = $[30]; 199: t8 = $[31]; 200: t9 = $[32]; 201: } 202: let t17; 203: if ($[49] !== T0 || $[50] !== t4 || $[51] !== t5 || $[52] !== t6 || $[53] !== t7) { 204: t17 = <T0 flexDirection={t4} gap={t5}>{t6}{t7}</T0>; 205: $[49] = T0; 206: $[50] = t4; 207: $[51] = t5; 208: $[52] = t6; 209: $[53] = t7; 210: $[54] = t17; 211: } else { 212: t17 = $[54]; 213: } 214: let t18; 215: if ($[55] !== T1 || $[56] !== t10 || $[57] !== t11 || $[58] !== t12 || $[59] !== t17 || $[60] !== t8 || $[61] !== t9) { 216: t18 = <T1 title={t8} subtitle={t9} onCancel={t10} color={t11} inputGuide={t12}>{t17}</T1>; 217: $[55] = T1; 218: $[56] = t10; 219: $[57] = t11; 220: $[58] = t12; 221: $[59] = t17; 222: $[60] = t8; 223: $[61] = t9; 224: $[62] = t18; 225: } else { 226: t18 = $[62]; 227: } 228: let t19; 229: if ($[63] !== T2 || $[64] !== t13 || $[65] !== t14 || $[66] !== t15 || $[67] !== t16 || $[68] !== t18) { 230: t19 = <T2 flexDirection={t13} tabIndex={t14} autoFocus={t15} onKeyDown={t16}>{t18}</T2>; 231: $[63] = T2; 232: $[64] = t13; 233: $[65] = t14; 234: $[66] = t15; 235: $[67] = t16; 236: $[68] = t18; 237: $[69] = t19; 238: } else { 239: t19 = $[69]; 240: } 241: return t19; 242: } 243: function _temp2(turn, i) { 244: return <Box key={i} flexDirection="column"><Text wrap="wrap">{turn.text}</Text>{turn.toolUseCount > 0 && <Text dimColor={true}>{" "}({turn.toolUseCount}{" "}{plural(turn.toolUseCount, "tool")})</Text>}</Box>; 245: } 246: function _temp(t) { 247: return t.text !== ""; 248: }

File: src/components/tasks/InProcessTeammateDetailDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useMemo } from 'react'; 3: import type { DeepImmutable } from 'src/types/utils.js'; 4: import { useElapsedTime } from '../../hooks/useElapsedTime.js'; 5: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 6: import { Box, Text, useTheme } from '../../ink.js'; 7: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 8: import { getEmptyToolPermissionContext } from '../../Tool.js'; 9: import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'; 10: import { getTools } from '../../tools.js'; 11: import { formatNumber, truncateToWidth } from '../../utils/format.js'; 12: import { toInkColor } from '../../utils/ink.js'; 13: import { Byline } from '../design-system/Byline.js'; 14: import { Dialog } from '../design-system/Dialog.js'; 15: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 16: import { renderToolActivity } from './renderToolActivity.js'; 17: import { describeTeammateActivity } from './taskStatusUtils.js'; 18: type Props = { 19: teammate: DeepImmutable<InProcessTeammateTaskState>; 20: onDone: () => void; 21: onKill?: () => void; 22: onBack?: () => void; 23: onForeground?: () => void; 24: }; 25: export function InProcessTeammateDetailDialog(t0) { 26: const $ = _c(63); 27: const { 28: teammate, 29: onDone, 30: onKill, 31: onBack, 32: onForeground 33: } = t0; 34: const [theme] = useTheme(); 35: let t1; 36: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 37: t1 = getTools(getEmptyToolPermissionContext()); 38: $[0] = t1; 39: } else { 40: t1 = $[0]; 41: } 42: const tools = t1; 43: const elapsedTime = useElapsedTime(teammate.startTime, teammate.status === "running", 1000, teammate.totalPausedMs ?? 0); 44: let t2; 45: if ($[1] !== onDone) { 46: t2 = { 47: "confirm:yes": onDone 48: }; 49: $[1] = onDone; 50: $[2] = t2; 51: } else { 52: t2 = $[2]; 53: } 54: let t3; 55: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 56: t3 = { 57: context: "Confirmation" 58: }; 59: $[3] = t3; 60: } else { 61: t3 = $[3]; 62: } 63: useKeybindings(t2, t3); 64: let t4; 65: if ($[4] !== onBack || $[5] !== onDone || $[6] !== onForeground || $[7] !== onKill || $[8] !== teammate.status) { 66: t4 = e => { 67: if (e.key === " ") { 68: e.preventDefault(); 69: onDone(); 70: } else { 71: if (e.key === "left" && onBack) { 72: e.preventDefault(); 73: onBack(); 74: } else { 75: if (e.key === "x" && teammate.status === "running" && onKill) { 76: e.preventDefault(); 77: onKill(); 78: } else { 79: if (e.key === "f" && teammate.status === "running" && onForeground) { 80: e.preventDefault(); 81: onForeground(); 82: } 83: } 84: } 85: } 86: }; 87: $[4] = onBack; 88: $[5] = onDone; 89: $[6] = onForeground; 90: $[7] = onKill; 91: $[8] = teammate.status; 92: $[9] = t4; 93: } else { 94: t4 = $[9]; 95: } 96: const handleKeyDown = t4; 97: let t5; 98: if ($[10] !== teammate) { 99: t5 = describeTeammateActivity(teammate); 100: $[10] = teammate; 101: $[11] = t5; 102: } else { 103: t5 = $[11]; 104: } 105: const activity = t5; 106: const tokenCount = teammate.result?.totalTokens ?? teammate.progress?.tokenCount; 107: const toolUseCount = teammate.result?.totalToolUseCount ?? teammate.progress?.toolUseCount; 108: let t6; 109: if ($[12] !== teammate.prompt) { 110: t6 = truncateToWidth(teammate.prompt, 300); 111: $[12] = teammate.prompt; 112: $[13] = t6; 113: } else { 114: t6 = $[13]; 115: } 116: const displayPrompt = t6; 117: let t7; 118: if ($[14] !== teammate.identity.color) { 119: t7 = toInkColor(teammate.identity.color); 120: $[14] = teammate.identity.color; 121: $[15] = t7; 122: } else { 123: t7 = $[15]; 124: } 125: let t8; 126: if ($[16] !== t7 || $[17] !== teammate.identity.agentName) { 127: t8 = <Text color={t7}>@{teammate.identity.agentName}</Text>; 128: $[16] = t7; 129: $[17] = teammate.identity.agentName; 130: $[18] = t8; 131: } else { 132: t8 = $[18]; 133: } 134: let t9; 135: if ($[19] !== activity) { 136: t9 = activity && <Text dimColor={true}> ({activity})</Text>; 137: $[19] = activity; 138: $[20] = t9; 139: } else { 140: t9 = $[20]; 141: } 142: let t10; 143: if ($[21] !== t8 || $[22] !== t9) { 144: t10 = <Text>{t8}{t9}</Text>; 145: $[21] = t8; 146: $[22] = t9; 147: $[23] = t10; 148: } else { 149: t10 = $[23]; 150: } 151: const title = t10; 152: let t11; 153: if ($[24] !== teammate.status) { 154: t11 = teammate.status !== "running" && <Text color={teammate.status === "completed" ? "success" : teammate.status === "killed" ? "warning" : "error"}>{teammate.status === "completed" ? "Completed" : teammate.status === "failed" ? "Failed" : "Stopped"}{" \xB7 "}</Text>; 155: $[24] = teammate.status; 156: $[25] = t11; 157: } else { 158: t11 = $[25]; 159: } 160: let t12; 161: if ($[26] !== tokenCount) { 162: t12 = tokenCount !== undefined && tokenCount > 0 && <> · {formatNumber(tokenCount)} tokens</>; 163: $[26] = tokenCount; 164: $[27] = t12; 165: } else { 166: t12 = $[27]; 167: } 168: let t13; 169: if ($[28] !== toolUseCount) { 170: t13 = toolUseCount !== undefined && toolUseCount > 0 && <>{" "}· {toolUseCount} {toolUseCount === 1 ? "tool" : "tools"}</>; 171: $[28] = toolUseCount; 172: $[29] = t13; 173: } else { 174: t13 = $[29]; 175: } 176: let t14; 177: if ($[30] !== elapsedTime || $[31] !== t12 || $[32] !== t13) { 178: t14 = <Text dimColor={true}>{elapsedTime}{t12}{t13}</Text>; 179: $[30] = elapsedTime; 180: $[31] = t12; 181: $[32] = t13; 182: $[33] = t14; 183: } else { 184: t14 = $[33]; 185: } 186: let t15; 187: if ($[34] !== t11 || $[35] !== t14) { 188: t15 = <Text>{t11}{t14}</Text>; 189: $[34] = t11; 190: $[35] = t14; 191: $[36] = t15; 192: } else { 193: t15 = $[36]; 194: } 195: const subtitle = t15; 196: let t16; 197: if ($[37] !== onBack || $[38] !== onForeground || $[39] !== onKill || $[40] !== teammate.status) { 198: t16 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{teammate.status === "running" && onKill && <KeyboardShortcutHint shortcut="x" action="stop" />}{teammate.status === "running" && onForeground && <KeyboardShortcutHint shortcut="f" action="foreground" />}</Byline>; 199: $[37] = onBack; 200: $[38] = onForeground; 201: $[39] = onKill; 202: $[40] = teammate.status; 203: $[41] = t16; 204: } else { 205: t16 = $[41]; 206: } 207: let t17; 208: if ($[42] !== teammate.progress || $[43] !== teammate.status || $[44] !== theme) { 209: t17 = teammate.status === "running" && teammate.progress?.recentActivities && teammate.progress.recentActivities.length > 0 && <Box flexDirection="column"><Text bold={true} dimColor={true}>Progress</Text>{teammate.progress.recentActivities.map((activity_0, i) => <Text key={i} dimColor={i < teammate.progress.recentActivities.length - 1} wrap="truncate-end">{i === teammate.progress.recentActivities.length - 1 ? "\u203A " : " "}{renderToolActivity(activity_0, tools, theme)}</Text>)}</Box>; 210: $[42] = teammate.progress; 211: $[43] = teammate.status; 212: $[44] = theme; 213: $[45] = t17; 214: } else { 215: t17 = $[45]; 216: } 217: let t18; 218: if ($[46] === Symbol.for("react.memo_cache_sentinel")) { 219: t18 = <Text bold={true} dimColor={true}>Prompt</Text>; 220: $[46] = t18; 221: } else { 222: t18 = $[46]; 223: } 224: let t19; 225: if ($[47] !== displayPrompt) { 226: t19 = <Box flexDirection="column" marginTop={1}>{t18}<Text wrap="wrap">{displayPrompt}</Text></Box>; 227: $[47] = displayPrompt; 228: $[48] = t19; 229: } else { 230: t19 = $[48]; 231: } 232: let t20; 233: if ($[49] !== teammate.error || $[50] !== teammate.status) { 234: t20 = teammate.status === "failed" && teammate.error && <Box flexDirection="column" marginTop={1}><Text bold={true} color="error">Error</Text><Text color="error" wrap="wrap">{teammate.error}</Text></Box>; 235: $[49] = teammate.error; 236: $[50] = teammate.status; 237: $[51] = t20; 238: } else { 239: t20 = $[51]; 240: } 241: let t21; 242: if ($[52] !== onDone || $[53] !== subtitle || $[54] !== t16 || $[55] !== t17 || $[56] !== t19 || $[57] !== t20 || $[58] !== title) { 243: t21 = <Dialog title={title} subtitle={subtitle} onCancel={onDone} color="background" inputGuide={t16}>{t17}{t19}{t20}</Dialog>; 244: $[52] = onDone; 245: $[53] = subtitle; 246: $[54] = t16; 247: $[55] = t17; 248: $[56] = t19; 249: $[57] = t20; 250: $[58] = title; 251: $[59] = t21; 252: } else { 253: t21 = $[59]; 254: } 255: let t22; 256: if ($[60] !== handleKeyDown || $[61] !== t21) { 257: t22 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t21}</Box>; 258: $[60] = handleKeyDown; 259: $[61] = t21; 260: $[62] = t22; 261: } else { 262: t22 = $[62]; 263: } 264: return t22; 265: }

File: src/components/tasks/RemoteSessionDetailDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import React, { useMemo, useState } from 'react'; 4: import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js'; 5: import type { ToolUseContext } from 'src/Tool.js'; 6: import type { DeepImmutable } from 'src/types/utils.js'; 7: import type { CommandResultDisplay } from '../../commands.js'; 8: import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'; 9: import { useElapsedTime } from '../../hooks/useElapsedTime.js'; 10: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 11: import { Box, Link, Text } from '../../ink.js'; 12: import type { RemoteAgentTaskState } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js'; 13: import { getRemoteTaskSessionUrl } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js'; 14: import { AGENT_TOOL_NAME, LEGACY_AGENT_TOOL_NAME } from '../../tools/AgentTool/constants.js'; 15: import { ASK_USER_QUESTION_TOOL_NAME } from '../../tools/AskUserQuestionTool/prompt.js'; 16: import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../tools/ExitPlanModeTool/constants.js'; 17: import { openBrowser } from '../../utils/browser.js'; 18: import { errorMessage } from '../../utils/errors.js'; 19: import { formatDuration, truncateToWidth } from '../../utils/format.js'; 20: import { toInternalMessages } from '../../utils/messages/mappers.js'; 21: import { EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js'; 22: import { plural } from '../../utils/stringUtils.js'; 23: import { teleportResumeCodeSession } from '../../utils/teleport.js'; 24: import { Select } from '../CustomSelect/select.js'; 25: import { Byline } from '../design-system/Byline.js'; 26: import { Dialog } from '../design-system/Dialog.js'; 27: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 28: import { Message } from '../Message.js'; 29: import { formatReviewStageCounts, RemoteSessionProgress } from './RemoteSessionProgress.js'; 30: type Props = { 31: session: DeepImmutable<RemoteAgentTaskState>; 32: toolUseContext: ToolUseContext; 33: onDone: (result?: string, options?: { 34: display?: CommandResultDisplay; 35: }) => void; 36: onBack?: () => void; 37: onKill?: () => void; 38: }; 39: export function formatToolUseSummary(name: string, input: unknown): string { 40: if (name === EXIT_PLAN_MODE_V2_TOOL_NAME) { 41: return 'Review the plan in Claude Code on the web'; 42: } 43: if (!input || typeof input !== 'object') return name; 44: if (name === ASK_USER_QUESTION_TOOL_NAME && 'questions' in input) { 45: const qs = input.questions; 46: if (Array.isArray(qs) && qs[0] && typeof qs[0] === 'object') { 47: const q = 'question' in qs[0] && typeof qs[0].question === 'string' && qs[0].question ? qs[0].question : 'header' in qs[0] && typeof qs[0].header === 'string' ? qs[0].header : null; 48: if (q) { 49: const oneLine = q.replace(/\s+/g, ' ').trim(); 50: return `Answer in browser: ${truncateToWidth(oneLine, 50)}`; 51: } 52: } 53: } 54: for (const v of Object.values(input)) { 55: if (typeof v === 'string' && v.trim()) { 56: const oneLine = v.replace(/\s+/g, ' ').trim(); 57: return `${name} ${truncateToWidth(oneLine, 60)}`; 58: } 59: } 60: return name; 61: } 62: const PHASE_LABEL = { 63: needs_input: 'input required', 64: plan_ready: 'ready' 65: } as const; 66: const AGENT_VERB = { 67: needs_input: 'waiting', 68: plan_ready: 'done' 69: } as const; 70: function UltraplanSessionDetail(t0) { 71: const $ = _c(70); 72: const { 73: session, 74: onDone, 75: onBack, 76: onKill 77: } = t0; 78: const running = session.status === "running" || session.status === "pending"; 79: const phase = session.ultraplanPhase; 80: const statusText = running ? phase ? PHASE_LABEL[phase] : "running" : session.status; 81: const elapsedTime = useElapsedTime(session.startTime, running, 1000, 0, session.endTime); 82: let spawns = 0; 83: let calls = 0; 84: let lastBlock = null; 85: for (const msg of session.log) { 86: if (msg.type !== "assistant") { 87: continue; 88: } 89: for (const block of msg.message.content) { 90: if (block.type !== "tool_use") { 91: continue; 92: } 93: calls++; 94: lastBlock = block; 95: if (block.name === AGENT_TOOL_NAME || block.name === LEGACY_AGENT_TOOL_NAME) { 96: spawns++; 97: } 98: } 99: } 100: const t1 = 1 + spawns; 101: let t2; 102: if ($[0] !== lastBlock) { 103: t2 = lastBlock ? formatToolUseSummary(lastBlock.name, lastBlock.input) : null; 104: $[0] = lastBlock; 105: $[1] = t2; 106: } else { 107: t2 = $[1]; 108: } 109: let t3; 110: if ($[2] !== calls || $[3] !== t1 || $[4] !== t2) { 111: t3 = { 112: agentsWorking: t1, 113: toolCalls: calls, 114: lastToolCall: t2 115: }; 116: $[2] = calls; 117: $[3] = t1; 118: $[4] = t2; 119: $[5] = t3; 120: } else { 121: t3 = $[5]; 122: } 123: const { 124: agentsWorking, 125: toolCalls, 126: lastToolCall 127: } = t3; 128: let t4; 129: if ($[6] !== session.sessionId) { 130: t4 = getRemoteTaskSessionUrl(session.sessionId); 131: $[6] = session.sessionId; 132: $[7] = t4; 133: } else { 134: t4 = $[7]; 135: } 136: const sessionUrl = t4; 137: let t5; 138: if ($[8] !== onBack || $[9] !== onDone) { 139: t5 = onBack ?? (() => onDone("Remote session details dismissed", { 140: display: "system" 141: })); 142: $[8] = onBack; 143: $[9] = onDone; 144: $[10] = t5; 145: } else { 146: t5 = $[10]; 147: } 148: const goBackOrClose = t5; 149: const [confirmingStop, setConfirmingStop] = useState(false); 150: if (confirmingStop) { 151: let t6; 152: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 153: t6 = () => setConfirmingStop(false); 154: $[11] = t6; 155: } else { 156: t6 = $[11]; 157: } 158: let t7; 159: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 160: t7 = <Text dimColor={true}>This will terminate the Claude Code on the web session.</Text>; 161: $[12] = t7; 162: } else { 163: t7 = $[12]; 164: } 165: let t8; 166: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 167: t8 = { 168: label: "Terminate session", 169: value: "stop" as const 170: }; 171: $[13] = t8; 172: } else { 173: t8 = $[13]; 174: } 175: let t9; 176: if ($[14] === Symbol.for("react.memo_cache_sentinel")) { 177: t9 = [t8, { 178: label: "Back", 179: value: "back" as const 180: }]; 181: $[14] = t9; 182: } else { 183: t9 = $[14]; 184: } 185: let t10; 186: if ($[15] !== goBackOrClose || $[16] !== onKill) { 187: t10 = <Dialog title="Stop ultraplan?" onCancel={t6} color="background"><Box flexDirection="column" gap={1}>{t7}<Select options={t9} onChange={v => { 188: if (v === "stop") { 189: onKill?.(); 190: goBackOrClose(); 191: } else { 192: setConfirmingStop(false); 193: } 194: }} /></Box></Dialog>; 195: $[15] = goBackOrClose; 196: $[16] = onKill; 197: $[17] = t10; 198: } else { 199: t10 = $[17]; 200: } 201: return t10; 202: } 203: const t6 = phase === "plan_ready" ? DIAMOND_FILLED : DIAMOND_OPEN; 204: let t7; 205: if ($[18] !== t6) { 206: t7 = <Text color="background">{t6}{" "}</Text>; 207: $[18] = t6; 208: $[19] = t7; 209: } else { 210: t7 = $[19]; 211: } 212: let t8; 213: if ($[20] === Symbol.for("react.memo_cache_sentinel")) { 214: t8 = <Text bold={true}>ultraplan</Text>; 215: $[20] = t8; 216: } else { 217: t8 = $[20]; 218: } 219: let t9; 220: if ($[21] !== elapsedTime || $[22] !== statusText) { 221: t9 = <Text dimColor={true}>{" \xB7 "}{elapsedTime}{" \xB7 "}{statusText}</Text>; 222: $[21] = elapsedTime; 223: $[22] = statusText; 224: $[23] = t9; 225: } else { 226: t9 = $[23]; 227: } 228: let t10; 229: if ($[24] !== t7 || $[25] !== t9) { 230: t10 = <Text>{t7}{t8}{t9}</Text>; 231: $[24] = t7; 232: $[25] = t9; 233: $[26] = t10; 234: } else { 235: t10 = $[26]; 236: } 237: let t11; 238: if ($[27] !== phase) { 239: t11 = phase === "plan_ready" && <Text color="success">{figures.tick} </Text>; 240: $[27] = phase; 241: $[28] = t11; 242: } else { 243: t11 = $[28]; 244: } 245: let t12; 246: if ($[29] !== agentsWorking) { 247: t12 = plural(agentsWorking, "agent"); 248: $[29] = agentsWorking; 249: $[30] = t12; 250: } else { 251: t12 = $[30]; 252: } 253: const t13 = phase ? AGENT_VERB[phase] : "working"; 254: let t14; 255: if ($[31] !== toolCalls) { 256: t14 = plural(toolCalls, "call"); 257: $[31] = toolCalls; 258: $[32] = t14; 259: } else { 260: t14 = $[32]; 261: } 262: let t15; 263: if ($[33] !== agentsWorking || $[34] !== t11 || $[35] !== t12 || $[36] !== t13 || $[37] !== t14 || $[38] !== toolCalls) { 264: t15 = <Text>{t11}{agentsWorking} {t12}{" "}{t13} · {toolCalls} tool{" "}{t14}</Text>; 265: $[33] = agentsWorking; 266: $[34] = t11; 267: $[35] = t12; 268: $[36] = t13; 269: $[37] = t14; 270: $[38] = toolCalls; 271: $[39] = t15; 272: } else { 273: t15 = $[39]; 274: } 275: let t16; 276: if ($[40] !== lastToolCall) { 277: t16 = lastToolCall && <Text dimColor={true}>{lastToolCall}</Text>; 278: $[40] = lastToolCall; 279: $[41] = t16; 280: } else { 281: t16 = $[41]; 282: } 283: let t17; 284: if ($[42] !== sessionUrl) { 285: t17 = <Text dimColor={true}>{sessionUrl}</Text>; 286: $[42] = sessionUrl; 287: $[43] = t17; 288: } else { 289: t17 = $[43]; 290: } 291: let t18; 292: if ($[44] !== sessionUrl || $[45] !== t17) { 293: t18 = <Link url={sessionUrl}>{t17}</Link>; 294: $[44] = sessionUrl; 295: $[45] = t17; 296: $[46] = t18; 297: } else { 298: t18 = $[46]; 299: } 300: let t19; 301: if ($[47] === Symbol.for("react.memo_cache_sentinel")) { 302: t19 = { 303: label: "Review in Claude Code on the web", 304: value: "open" as const 305: }; 306: $[47] = t19; 307: } else { 308: t19 = $[47]; 309: } 310: let t20; 311: if ($[48] !== onKill || $[49] !== running) { 312: t20 = onKill && running ? [{ 313: label: "Stop ultraplan", 314: value: "stop" as const 315: }] : []; 316: $[48] = onKill; 317: $[49] = running; 318: $[50] = t20; 319: } else { 320: t20 = $[50]; 321: } 322: let t21; 323: if ($[51] === Symbol.for("react.memo_cache_sentinel")) { 324: t21 = { 325: label: "Back", 326: value: "back" as const 327: }; 328: $[51] = t21; 329: } else { 330: t21 = $[51]; 331: } 332: let t22; 333: if ($[52] !== t20) { 334: t22 = [t19, ...t20, t21]; 335: $[52] = t20; 336: $[53] = t22; 337: } else { 338: t22 = $[53]; 339: } 340: let t23; 341: if ($[54] !== goBackOrClose || $[55] !== onDone || $[56] !== sessionUrl) { 342: t23 = v_0 => { 343: switch (v_0) { 344: case "open": 345: { 346: openBrowser(sessionUrl); 347: onDone(); 348: return; 349: } 350: case "stop": 351: { 352: setConfirmingStop(true); 353: return; 354: } 355: case "back": 356: { 357: goBackOrClose(); 358: return; 359: } 360: } 361: }; 362: $[54] = goBackOrClose; 363: $[55] = onDone; 364: $[56] = sessionUrl; 365: $[57] = t23; 366: } else { 367: t23 = $[57]; 368: } 369: let t24; 370: if ($[58] !== t22 || $[59] !== t23) { 371: t24 = <Select options={t22} onChange={t23} />; 372: $[58] = t22; 373: $[59] = t23; 374: $[60] = t24; 375: } else { 376: t24 = $[60]; 377: } 378: let t25; 379: if ($[61] !== t15 || $[62] !== t16 || $[63] !== t18 || $[64] !== t24) { 380: t25 = <Box flexDirection="column" gap={1}>{t15}{t16}{t18}{t24}</Box>; 381: $[61] = t15; 382: $[62] = t16; 383: $[63] = t18; 384: $[64] = t24; 385: $[65] = t25; 386: } else { 387: t25 = $[65]; 388: } 389: let t26; 390: if ($[66] !== goBackOrClose || $[67] !== t10 || $[68] !== t25) { 391: t26 = <Dialog title={t10} onCancel={goBackOrClose} color="background">{t25}</Dialog>; 392: $[66] = goBackOrClose; 393: $[67] = t10; 394: $[68] = t25; 395: $[69] = t26; 396: } else { 397: t26 = $[69]; 398: } 399: return t26; 400: } 401: const STAGES = ['finding', 'verifying', 'synthesizing'] as const; 402: const STAGE_LABELS: Record<(typeof STAGES)[number], string> = { 403: finding: 'Find', 404: verifying: 'Verify', 405: synthesizing: 'Dedupe' 406: }; 407: function StagePipeline(t0) { 408: const $ = _c(15); 409: const { 410: stage, 411: completed, 412: hasProgress 413: } = t0; 414: let t1; 415: if ($[0] !== stage) { 416: t1 = stage ? STAGES.indexOf(stage) : -1; 417: $[0] = stage; 418: $[1] = t1; 419: } else { 420: t1 = $[1]; 421: } 422: const currentIdx = t1; 423: const inSetup = !completed && !hasProgress; 424: let t2; 425: if ($[2] !== inSetup) { 426: t2 = inSetup ? <Text color="background">Setup</Text> : <Text dimColor={true}>Setup</Text>; 427: $[2] = inSetup; 428: $[3] = t2; 429: } else { 430: t2 = $[3]; 431: } 432: let t3; 433: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 434: t3 = <Text dimColor={true}> → </Text>; 435: $[4] = t3; 436: } else { 437: t3 = $[4]; 438: } 439: let t4; 440: if ($[5] !== completed || $[6] !== currentIdx || $[7] !== inSetup) { 441: t4 = STAGES.map((s, i) => { 442: const isCurrent = !completed && !inSetup && i === currentIdx; 443: return <React.Fragment key={s}>{i > 0 && <Text dimColor={true}> → </Text>}{isCurrent ? <Text color="background">{STAGE_LABELS[s]}</Text> : <Text dimColor={true}>{STAGE_LABELS[s]}</Text>}</React.Fragment>; 444: }); 445: $[5] = completed; 446: $[6] = currentIdx; 447: $[7] = inSetup; 448: $[8] = t4; 449: } else { 450: t4 = $[8]; 451: } 452: let t5; 453: if ($[9] !== completed) { 454: t5 = completed && <Text color="success"> ✓</Text>; 455: $[9] = completed; 456: $[10] = t5; 457: } else { 458: t5 = $[10]; 459: } 460: let t6; 461: if ($[11] !== t2 || $[12] !== t4 || $[13] !== t5) { 462: t6 = <Text>{t2}{t3}{t4}{t5}</Text>; 463: $[11] = t2; 464: $[12] = t4; 465: $[13] = t5; 466: $[14] = t6; 467: } else { 468: t6 = $[14]; 469: } 470: return t6; 471: } 472: function reviewCountsLine(session: DeepImmutable<RemoteAgentTaskState>): string { 473: const p = session.reviewProgress; 474: if (!p) return session.status === 'completed' ? 'done' : 'setting up'; 475: const verified = p.bugsVerified; 476: const refuted = p.bugsRefuted ?? 0; 477: if (session.status === 'completed') { 478: const parts = [`${verified} ${plural(verified, 'finding')}`]; 479: if (refuted > 0) parts.push(`${refuted} refuted`); 480: return parts.join(' · '); 481: } 482: return formatReviewStageCounts(p.stage, p.bugsFound, verified, refuted); 483: } 484: type MenuAction = 'open' | 'stop' | 'back' | 'dismiss'; 485: function ReviewSessionDetail(t0) { 486: const $ = _c(56); 487: const { 488: session, 489: onDone, 490: onBack, 491: onKill 492: } = t0; 493: const completed = session.status === "completed"; 494: const running = session.status === "running" || session.status === "pending"; 495: const [confirmingStop, setConfirmingStop] = useState(false); 496: const elapsedTime = useElapsedTime(session.startTime, running, 1000, 0, session.endTime); 497: let t1; 498: if ($[0] !== onDone) { 499: t1 = () => onDone("Remote session details dismissed", { 500: display: "system" 501: }); 502: $[0] = onDone; 503: $[1] = t1; 504: } else { 505: t1 = $[1]; 506: } 507: const handleClose = t1; 508: const goBackOrClose = onBack ?? handleClose; 509: let t2; 510: if ($[2] !== session.sessionId) { 511: t2 = getRemoteTaskSessionUrl(session.sessionId); 512: $[2] = session.sessionId; 513: $[3] = t2; 514: } else { 515: t2 = $[3]; 516: } 517: const sessionUrl = t2; 518: const statusLabel = completed ? "ready" : running ? "running" : session.status; 519: if (confirmingStop) { 520: let t3; 521: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 522: t3 = () => setConfirmingStop(false); 523: $[4] = t3; 524: } else { 525: t3 = $[4]; 526: } 527: let t4; 528: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 529: t4 = <Text dimColor={true}>This archives the remote session and stops local tracking. The review will not complete and any findings so far are discarded.</Text>; 530: $[5] = t4; 531: } else { 532: t4 = $[5]; 533: } 534: let t5; 535: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 536: t5 = { 537: label: "Stop ultrareview", 538: value: "stop" as const 539: }; 540: $[6] = t5; 541: } else { 542: t5 = $[6]; 543: } 544: let t6; 545: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 546: t6 = [t5, { 547: label: "Back", 548: value: "back" as const 549: }]; 550: $[7] = t6; 551: } else { 552: t6 = $[7]; 553: } 554: let t7; 555: if ($[8] !== goBackOrClose || $[9] !== onKill) { 556: t7 = <Dialog title="Stop ultrareview?" onCancel={t3} color="background"><Box flexDirection="column" gap={1}>{t4}<Select options={t6} onChange={v => { 557: if (v === "stop") { 558: onKill?.(); 559: goBackOrClose(); 560: } else { 561: setConfirmingStop(false); 562: } 563: }} /></Box></Dialog>; 564: $[8] = goBackOrClose; 565: $[9] = onKill; 566: $[10] = t7; 567: } else { 568: t7 = $[10]; 569: } 570: return t7; 571: } 572: let t3; 573: if ($[11] !== completed || $[12] !== onKill || $[13] !== running) { 574: t3 = completed ? [{ 575: label: "Open in Claude Code on the web", 576: value: "open" 577: }, { 578: label: "Dismiss", 579: value: "dismiss" 580: }] : [{ 581: label: "Open in Claude Code on the web", 582: value: "open" 583: }, ...(onKill && running ? [{ 584: label: "Stop ultrareview", 585: value: "stop" as const 586: }] : []), { 587: label: "Back", 588: value: "back" 589: }]; 590: $[11] = completed; 591: $[12] = onKill; 592: $[13] = running; 593: $[14] = t3; 594: } else { 595: t3 = $[14]; 596: } 597: const options = t3; 598: let t4; 599: if ($[15] !== goBackOrClose || $[16] !== handleClose || $[17] !== onDone || $[18] !== sessionUrl) { 600: t4 = action => { 601: bb45: switch (action) { 602: case "open": 603: { 604: openBrowser(sessionUrl); 605: onDone(); 606: break bb45; 607: } 608: case "stop": 609: { 610: setConfirmingStop(true); 611: break bb45; 612: } 613: case "back": 614: { 615: goBackOrClose(); 616: break bb45; 617: } 618: case "dismiss": 619: { 620: handleClose(); 621: } 622: } 623: }; 624: $[15] = goBackOrClose; 625: $[16] = handleClose; 626: $[17] = onDone; 627: $[18] = sessionUrl; 628: $[19] = t4; 629: } else { 630: t4 = $[19]; 631: } 632: const handleSelect = t4; 633: const t5 = completed ? DIAMOND_FILLED : DIAMOND_OPEN; 634: let t6; 635: if ($[20] !== t5) { 636: t6 = <Text color="background">{t5}{" "}</Text>; 637: $[20] = t5; 638: $[21] = t6; 639: } else { 640: t6 = $[21]; 641: } 642: let t7; 643: if ($[22] === Symbol.for("react.memo_cache_sentinel")) { 644: t7 = <Text bold={true}>ultrareview</Text>; 645: $[22] = t7; 646: } else { 647: t7 = $[22]; 648: } 649: let t8; 650: if ($[23] !== elapsedTime || $[24] !== statusLabel) { 651: t8 = <Text dimColor={true}>{" \xB7 "}{elapsedTime}{" \xB7 "}{statusLabel}</Text>; 652: $[23] = elapsedTime; 653: $[24] = statusLabel; 654: $[25] = t8; 655: } else { 656: t8 = $[25]; 657: } 658: let t9; 659: if ($[26] !== t6 || $[27] !== t8) { 660: t9 = <Text>{t6}{t7}{t8}</Text>; 661: $[26] = t6; 662: $[27] = t8; 663: $[28] = t9; 664: } else { 665: t9 = $[28]; 666: } 667: const t10 = session.reviewProgress?.stage; 668: const t11 = !!session.reviewProgress; 669: let t12; 670: if ($[29] !== completed || $[30] !== t10 || $[31] !== t11) { 671: t12 = <StagePipeline stage={t10} completed={completed} hasProgress={t11} />; 672: $[29] = completed; 673: $[30] = t10; 674: $[31] = t11; 675: $[32] = t12; 676: } else { 677: t12 = $[32]; 678: } 679: let t13; 680: if ($[33] !== session) { 681: t13 = reviewCountsLine(session); 682: $[33] = session; 683: $[34] = t13; 684: } else { 685: t13 = $[34]; 686: } 687: let t14; 688: if ($[35] !== t13) { 689: t14 = <Text>{t13}</Text>; 690: $[35] = t13; 691: $[36] = t14; 692: } else { 693: t14 = $[36]; 694: } 695: let t15; 696: if ($[37] !== sessionUrl) { 697: t15 = <Text dimColor={true}>{sessionUrl}</Text>; 698: $[37] = sessionUrl; 699: $[38] = t15; 700: } else { 701: t15 = $[38]; 702: } 703: let t16; 704: if ($[39] !== sessionUrl || $[40] !== t15) { 705: t16 = <Link url={sessionUrl}>{t15}</Link>; 706: $[39] = sessionUrl; 707: $[40] = t15; 708: $[41] = t16; 709: } else { 710: t16 = $[41]; 711: } 712: let t17; 713: if ($[42] !== t14 || $[43] !== t16) { 714: t17 = <Box flexDirection="column">{t14}{t16}</Box>; 715: $[42] = t14; 716: $[43] = t16; 717: $[44] = t17; 718: } else { 719: t17 = $[44]; 720: } 721: let t18; 722: if ($[45] !== handleSelect || $[46] !== options) { 723: t18 = <Select options={options} onChange={handleSelect} />; 724: $[45] = handleSelect; 725: $[46] = options; 726: $[47] = t18; 727: } else { 728: t18 = $[47]; 729: } 730: let t19; 731: if ($[48] !== t12 || $[49] !== t17 || $[50] !== t18) { 732: t19 = <Box flexDirection="column" gap={1}>{t12}{t17}{t18}</Box>; 733: $[48] = t12; 734: $[49] = t17; 735: $[50] = t18; 736: $[51] = t19; 737: } else { 738: t19 = $[51]; 739: } 740: let t20; 741: if ($[52] !== goBackOrClose || $[53] !== t19 || $[54] !== t9) { 742: t20 = <Dialog title={t9} onCancel={goBackOrClose} color="background" inputGuide={_temp}>{t19}</Dialog>; 743: $[52] = goBackOrClose; 744: $[53] = t19; 745: $[54] = t9; 746: $[55] = t20; 747: } else { 748: t20 = $[55]; 749: } 750: return t20; 751: } 752: function _temp(exitState) { 753: return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut="Enter" action="select" /><KeyboardShortcutHint shortcut="Esc" action="go back" /></Byline>; 754: } 755: export function RemoteSessionDetailDialog({ 756: session, 757: toolUseContext, 758: onDone, 759: onBack, 760: onKill 761: }: Props): React.ReactNode { 762: const [isTeleporting, setIsTeleporting] = useState(false); 763: const [teleportError, setTeleportError] = useState<string | null>(null); 764: const lastMessages = useMemo(() => { 765: if (session.isUltraplan || session.isRemoteReview) return []; 766: return normalizeMessages(toInternalMessages(session.log as SDKMessage[])).filter(_ => _.type !== 'progress').slice(-3); 767: }, [session]); 768: if (session.isUltraplan) { 769: return <UltraplanSessionDetail session={session} onDone={onDone} onBack={onBack} onKill={onKill} />; 770: } 771: if (session.isRemoteReview) { 772: return <ReviewSessionDetail session={session} onDone={onDone} onBack={onBack} onKill={onKill} />; 773: } 774: const handleClose = () => onDone('Remote session details dismissed', { 775: display: 'system' 776: }); 777: const handleKeyDown = (e: KeyboardEvent) => { 778: if (e.key === ' ') { 779: e.preventDefault(); 780: onDone('Remote session details dismissed', { 781: display: 'system' 782: }); 783: } else if (e.key === 'left' && onBack) { 784: e.preventDefault(); 785: onBack(); 786: } else if (e.key === 't' && !isTeleporting) { 787: e.preventDefault(); 788: void handleTeleport(); 789: } else if (e.key === 'return') { 790: e.preventDefault(); 791: handleClose(); 792: } 793: }; 794: async function handleTeleport(): Promise<void> { 795: setIsTeleporting(true); 796: setTeleportError(null); 797: try { 798: await teleportResumeCodeSession(session.sessionId); 799: } catch (err) { 800: setTeleportError(errorMessage(err)); 801: } finally { 802: setIsTeleporting(false); 803: } 804: } 805: const displayTitle = truncateToWidth(session.title, 50); 806: const displayStatus = session.status === 'pending' ? 'starting' : session.status; 807: return <Box flexDirection="column" tabIndex={0} autoFocus onKeyDown={handleKeyDown}> 808: <Dialog title="Remote session details" onCancel={handleClose} color="background" inputGuide={exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline> 809: {onBack && <KeyboardShortcutHint shortcut="←" action="go back" />} 810: <KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" /> 811: {!isTeleporting && <KeyboardShortcutHint shortcut="t" action="teleport" />} 812: </Byline>}> 813: <Box flexDirection="column"> 814: <Text> 815: <Text bold>Status</Text>:{' '} 816: {displayStatus === 'running' || displayStatus === 'starting' ? <Text color="background">{displayStatus}</Text> : displayStatus === 'completed' ? <Text color="success">{displayStatus}</Text> : <Text color="error">{displayStatus}</Text>} 817: </Text> 818: <Text> 819: <Text bold>Runtime</Text>:{' '} 820: {formatDuration((session.endTime ?? Date.now()) - session.startTime)} 821: </Text> 822: <Text wrap="truncate-end"> 823: <Text bold>Title</Text>: {displayTitle} 824: </Text> 825: <Text> 826: <Text bold>Progress</Text>:{' '} 827: <RemoteSessionProgress session={session} /> 828: </Text> 829: <Text> 830: <Text bold>Session URL</Text>:{' '} 831: <Link url={getRemoteTaskSessionUrl(session.sessionId)}> 832: <Text dimColor>{getRemoteTaskSessionUrl(session.sessionId)}</Text> 833: </Link> 834: </Text> 835: </Box> 836: {} 837: {session.log.length > 0 && <Box flexDirection="column" marginTop={1}> 838: <Text> 839: <Text bold>Recent messages</Text>: 840: </Text> 841: <Box flexDirection="column" height={10} overflowY="hidden"> 842: {lastMessages.map((msg, i) => <Message key={i} message={msg} lookups={EMPTY_LOOKUPS} addMargin={i > 0} tools={toolUseContext.options.tools} commands={toolUseContext.options.commands} verbose={toolUseContext.options.verbose} inProgressToolUseIDs={new Set()} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} style="condensed" isTranscriptMode={false} isStatic={true} />)} 843: </Box> 844: <Box marginTop={1}> 845: <Text dimColor italic> 846: Showing last {lastMessages.length} of {session.log.length}{' '} 847: messages 848: </Text> 849: </Box> 850: </Box>} 851: {} 852: {teleportError && <Box marginTop={1}> 853: <Text color="error">Teleport failed: {teleportError}</Text> 854: </Box>} 855: {} 856: {isTeleporting && <Text color="background">Teleporting to session…</Text>} 857: </Dialog> 858: </Box>; 859: }

File: src/components/tasks/RemoteSessionProgress.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useRef } from 'react'; 3: import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'; 4: import type { DeepImmutable } from 'src/types/utils.js'; 5: import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'; 6: import { useSettings } from '../../hooks/useSettings.js'; 7: import { Text, useAnimationFrame } from '../../ink.js'; 8: import { count } from '../../utils/array.js'; 9: import { getRainbowColor } from '../../utils/thinking.js'; 10: const TICK_MS = 80; 11: type ReviewStage = NonNullable<NonNullable<RemoteAgentTaskState['reviewProgress']>['stage']>; 12: export function formatReviewStageCounts(stage: ReviewStage | undefined, found: number, verified: number, refuted: number): string { 13: if (!stage) return `${found} found · ${verified} verified`; 14: if (stage === 'synthesizing') { 15: const parts = [`${verified} verified`]; 16: if (refuted > 0) parts.push(`${refuted} refuted`); 17: parts.push('deduping'); 18: return parts.join(' · '); 19: } 20: if (stage === 'verifying') { 21: const parts = [`${found} found`, `${verified} verified`]; 22: if (refuted > 0) parts.push(`${refuted} refuted`); 23: return parts.join(' · '); 24: } 25: return found > 0 ? `${found} found` : 'finding'; 26: } 27: function RainbowText(t0) { 28: const $ = _c(5); 29: const { 30: text, 31: phase: t1 32: } = t0; 33: const phase = t1 === undefined ? 0 : t1; 34: let t2; 35: if ($[0] !== text) { 36: t2 = [...text]; 37: $[0] = text; 38: $[1] = t2; 39: } else { 40: t2 = $[1]; 41: } 42: let t3; 43: if ($[2] !== phase || $[3] !== t2) { 44: t3 = <>{t2.map((ch, i) => <Text key={i} color={getRainbowColor(i + phase)}>{ch}</Text>)}</>; 45: $[2] = phase; 46: $[3] = t2; 47: $[4] = t3; 48: } else { 49: t3 = $[4]; 50: } 51: return t3; 52: } 53: function useSmoothCount(target: number, time: number, snap: boolean): number { 54: const displayed = useRef(target); 55: const lastTick = useRef(time); 56: if (snap || target < displayed.current) { 57: displayed.current = target; 58: } else if (target > displayed.current && time !== lastTick.current) { 59: displayed.current += 1; 60: lastTick.current = time; 61: } 62: return displayed.current; 63: } 64: function ReviewRainbowLine(t0) { 65: const $ = _c(15); 66: const { 67: session 68: } = t0; 69: const settings = useSettings(); 70: const reducedMotion = settings.prefersReducedMotion ?? false; 71: const p = session.reviewProgress; 72: const running = session.status === "running"; 73: const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null); 74: const targetFound = p?.bugsFound ?? 0; 75: const targetVerified = p?.bugsVerified ?? 0; 76: const targetRefuted = p?.bugsRefuted ?? 0; 77: const snap = reducedMotion || !running; 78: const found = useSmoothCount(targetFound, time, snap); 79: const verified = useSmoothCount(targetVerified, time, snap); 80: const refuted = useSmoothCount(targetRefuted, time, snap); 81: const phase = Math.floor(time / (TICK_MS * 3)) % 7; 82: if (session.status === "completed") { 83: let t1; 84: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 85: t1 = <><Text color="background">{DIAMOND_FILLED} </Text><RainbowText text="ultrareview" phase={0} /><Text dimColor={true}> ready · shift+↓ to view</Text></>; 86: $[0] = t1; 87: } else { 88: t1 = $[0]; 89: } 90: return t1; 91: } 92: if (session.status === "failed") { 93: let t1; 94: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 95: t1 = <><Text color="background">{DIAMOND_FILLED} </Text><RainbowText text="ultrareview" phase={0} /><Text color="error" dimColor={true}>{" \xB7 "}error</Text></>; 96: $[1] = t1; 97: } else { 98: t1 = $[1]; 99: } 100: return t1; 101: } 102: let t1; 103: if ($[2] !== found || $[3] !== p || $[4] !== refuted || $[5] !== verified) { 104: t1 = !p ? "setting up" : formatReviewStageCounts(p.stage, found, verified, refuted); 105: $[2] = found; 106: $[3] = p; 107: $[4] = refuted; 108: $[5] = verified; 109: $[6] = t1; 110: } else { 111: t1 = $[6]; 112: } 113: const tail = t1; 114: let t2; 115: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 116: t2 = <Text color="background">{DIAMOND_OPEN} </Text>; 117: $[7] = t2; 118: } else { 119: t2 = $[7]; 120: } 121: const t3 = running ? phase : 0; 122: let t4; 123: if ($[8] !== t3) { 124: t4 = <RainbowText text="ultrareview" phase={t3} />; 125: $[8] = t3; 126: $[9] = t4; 127: } else { 128: t4 = $[9]; 129: } 130: let t5; 131: if ($[10] !== tail) { 132: t5 = <Text dimColor={true}> · {tail}</Text>; 133: $[10] = tail; 134: $[11] = t5; 135: } else { 136: t5 = $[11]; 137: } 138: let t6; 139: if ($[12] !== t4 || $[13] !== t5) { 140: t6 = <>{t2}{t4}{t5}</>; 141: $[12] = t4; 142: $[13] = t5; 143: $[14] = t6; 144: } else { 145: t6 = $[14]; 146: } 147: return t6; 148: } 149: export function RemoteSessionProgress(t0) { 150: const $ = _c(11); 151: const { 152: session 153: } = t0; 154: if (session.isRemoteReview) { 155: let t1; 156: if ($[0] !== session) { 157: t1 = <ReviewRainbowLine session={session} />; 158: $[0] = session; 159: $[1] = t1; 160: } else { 161: t1 = $[1]; 162: } 163: return t1; 164: } 165: if (session.status === "completed") { 166: let t1; 167: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 168: t1 = <Text bold={true} color="success" dimColor={true}>done</Text>; 169: $[2] = t1; 170: } else { 171: t1 = $[2]; 172: } 173: return t1; 174: } 175: if (session.status === "failed") { 176: let t1; 177: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 178: t1 = <Text bold={true} color="error" dimColor={true}>error</Text>; 179: $[3] = t1; 180: } else { 181: t1 = $[3]; 182: } 183: return t1; 184: } 185: if (!session.todoList.length) { 186: let t1; 187: if ($[4] !== session.status) { 188: t1 = <Text dimColor={true}>{session.status}…</Text>; 189: $[4] = session.status; 190: $[5] = t1; 191: } else { 192: t1 = $[5]; 193: } 194: return t1; 195: } 196: let t1; 197: if ($[6] !== session.todoList) { 198: t1 = count(session.todoList, _temp); 199: $[6] = session.todoList; 200: $[7] = t1; 201: } else { 202: t1 = $[7]; 203: } 204: const completed = t1; 205: const total = session.todoList.length; 206: let t2; 207: if ($[8] !== completed || $[9] !== total) { 208: t2 = <Text dimColor={true}>{completed}/{total}</Text>; 209: $[8] = completed; 210: $[9] = total; 211: $[10] = t2; 212: } else { 213: t2 = $[10]; 214: } 215: return t2; 216: } 217: function _temp(_) { 218: return _.status === "completed"; 219: }

File: src/components/tasks/renderToolActivity.tsx

typescript 1: import React from 'react'; 2: import { Text } from '../../ink.js'; 3: import type { Tools } from '../../Tool.js'; 4: import { findToolByName } from '../../Tool.js'; 5: import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js'; 6: import type { ThemeName } from '../../utils/theme.js'; 7: export function renderToolActivity(activity: ToolActivity, tools: Tools, theme: ThemeName): React.ReactNode { 8: const tool = findToolByName(tools, activity.toolName); 9: if (!tool) { 10: return activity.toolName; 11: } 12: try { 13: const parsed = tool.inputSchema.safeParse(activity.input); 14: const parsedInput = parsed.success ? parsed.data : {}; 15: const userFacingName = tool.userFacingName(parsedInput); 16: if (!userFacingName) { 17: return activity.toolName; 18: } 19: const toolArgs = tool.renderToolUseMessage(parsedInput, { 20: theme, 21: verbose: false 22: }); 23: if (toolArgs) { 24: return <Text> 25: {userFacingName}({toolArgs}) 26: </Text>; 27: } 28: return userFacingName; 29: } catch { 30: return activity.toolName; 31: } 32: }

File: src/components/tasks/ShellDetailDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { Suspense, use, useDeferredValue, useEffect, useState } from 'react'; 3: import type { DeepImmutable } from 'src/types/utils.js'; 4: import type { CommandResultDisplay } from '../../commands.js'; 5: import { useTerminalSize } from '../../hooks/useTerminalSize.js'; 6: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 7: import { Box, Text } from '../../ink.js'; 8: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 9: import type { LocalShellTaskState } from '../../tasks/LocalShellTask/guards.js'; 10: import { formatDuration, formatFileSize, truncateToWidth } from '../../utils/format.js'; 11: import { tailFile } from '../../utils/fsOperations.js'; 12: import { getTaskOutputPath } from '../../utils/task/diskOutput.js'; 13: import { Byline } from '../design-system/Byline.js'; 14: import { Dialog } from '../design-system/Dialog.js'; 15: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 16: type Props = { 17: shell: DeepImmutable<LocalShellTaskState>; 18: onDone: (result?: string, options?: { 19: display?: CommandResultDisplay; 20: }) => void; 21: onKillShell?: () => void; 22: onBack?: () => void; 23: }; 24: const SHELL_DETAIL_TAIL_BYTES = 8192; 25: type TaskOutputResult = { 26: content: string; 27: bytesTotal: number; 28: }; 29: async function getTaskOutput(shell: DeepImmutable<LocalShellTaskState>): Promise<TaskOutputResult> { 30: const path = getTaskOutputPath(shell.id); 31: try { 32: const result = await tailFile(path, SHELL_DETAIL_TAIL_BYTES); 33: return { 34: content: result.content, 35: bytesTotal: result.bytesTotal 36: }; 37: } catch { 38: return { 39: content: '', 40: bytesTotal: 0 41: }; 42: } 43: } 44: export function ShellDetailDialog(t0) { 45: const $ = _c(57); 46: const { 47: shell, 48: onDone, 49: onKillShell, 50: onBack 51: } = t0; 52: const { 53: columns 54: } = useTerminalSize(); 55: let t1; 56: if ($[0] !== shell) { 57: t1 = () => getTaskOutput(shell); 58: $[0] = shell; 59: $[1] = t1; 60: } else { 61: t1 = $[1]; 62: } 63: const [outputPromise, setOutputPromise] = useState(t1); 64: const deferredOutputPromise = useDeferredValue(outputPromise); 65: let t2; 66: if ($[2] !== shell) { 67: t2 = () => { 68: if (shell.status !== "running") { 69: return; 70: } 71: const timer = setInterval(_temp, 1000, setOutputPromise, shell); 72: return () => clearInterval(timer); 73: }; 74: $[2] = shell; 75: $[3] = t2; 76: } else { 77: t2 = $[3]; 78: } 79: let t3; 80: if ($[4] !== shell.id || $[5] !== shell.status) { 81: t3 = [shell.id, shell.status]; 82: $[4] = shell.id; 83: $[5] = shell.status; 84: $[6] = t3; 85: } else { 86: t3 = $[6]; 87: } 88: useEffect(t2, t3); 89: let t4; 90: if ($[7] !== onDone) { 91: t4 = () => onDone("Shell details dismissed", { 92: display: "system" 93: }); 94: $[7] = onDone; 95: $[8] = t4; 96: } else { 97: t4 = $[8]; 98: } 99: const handleClose = t4; 100: let t5; 101: if ($[9] !== handleClose) { 102: t5 = { 103: "confirm:yes": handleClose 104: }; 105: $[9] = handleClose; 106: $[10] = t5; 107: } else { 108: t5 = $[10]; 109: } 110: let t6; 111: if ($[11] === Symbol.for("react.memo_cache_sentinel")) { 112: t6 = { 113: context: "Confirmation" 114: }; 115: $[11] = t6; 116: } else { 117: t6 = $[11]; 118: } 119: useKeybindings(t5, t6); 120: let t7; 121: if ($[12] !== onBack || $[13] !== onDone || $[14] !== onKillShell || $[15] !== shell.status) { 122: t7 = e => { 123: if (e.key === " ") { 124: e.preventDefault(); 125: onDone("Shell details dismissed", { 126: display: "system" 127: }); 128: } else { 129: if (e.key === "left" && onBack) { 130: e.preventDefault(); 131: onBack(); 132: } else { 133: if (e.key === "x" && shell.status === "running" && onKillShell) { 134: e.preventDefault(); 135: onKillShell(); 136: } 137: } 138: } 139: }; 140: $[12] = onBack; 141: $[13] = onDone; 142: $[14] = onKillShell; 143: $[15] = shell.status; 144: $[16] = t7; 145: } else { 146: t7 = $[16]; 147: } 148: const handleKeyDown = t7; 149: const isMonitor = shell.kind === "monitor"; 150: let t8; 151: if ($[17] !== shell.command) { 152: t8 = truncateToWidth(shell.command, 280); 153: $[17] = shell.command; 154: $[18] = t8; 155: } else { 156: t8 = $[18]; 157: } 158: const displayCommand = t8; 159: const t9 = isMonitor ? "Monitor details" : "Shell details"; 160: let t10; 161: if ($[19] !== onBack || $[20] !== onKillShell || $[21] !== shell.status) { 162: t10 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{shell.status === "running" && onKillShell && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>; 163: $[19] = onBack; 164: $[20] = onKillShell; 165: $[21] = shell.status; 166: $[22] = t10; 167: } else { 168: t10 = $[22]; 169: } 170: let t11; 171: if ($[23] === Symbol.for("react.memo_cache_sentinel")) { 172: t11 = <Text bold={true}>Status:</Text>; 173: $[23] = t11; 174: } else { 175: t11 = $[23]; 176: } 177: let t12; 178: if ($[24] !== shell.result || $[25] !== shell.status) { 179: t12 = <Text>{t11}{" "}{shell.status === "running" ? <Text color="background">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text> : shell.status === "completed" ? <Text color="success">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text> : <Text color="error">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text>}</Text>; 180: $[24] = shell.result; 181: $[25] = shell.status; 182: $[26] = t12; 183: } else { 184: t12 = $[26]; 185: } 186: let t13; 187: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 188: t13 = <Text bold={true}>Runtime:</Text>; 189: $[27] = t13; 190: } else { 191: t13 = $[27]; 192: } 193: let t14; 194: if ($[28] !== shell.endTime) { 195: t14 = shell.endTime ?? Date.now(); 196: $[28] = shell.endTime; 197: $[29] = t14; 198: } else { 199: t14 = $[29]; 200: } 201: const t15 = t14 - shell.startTime; 202: let t16; 203: if ($[30] !== t15) { 204: t16 = formatDuration(t15); 205: $[30] = t15; 206: $[31] = t16; 207: } else { 208: t16 = $[31]; 209: } 210: let t17; 211: if ($[32] !== t16) { 212: t17 = <Text>{t13}{" "}{t16}</Text>; 213: $[32] = t16; 214: $[33] = t17; 215: } else { 216: t17 = $[33]; 217: } 218: const t18 = isMonitor ? "Script:" : "Command:"; 219: let t19; 220: if ($[34] !== t18) { 221: t19 = <Text bold={true}>{t18}</Text>; 222: $[34] = t18; 223: $[35] = t19; 224: } else { 225: t19 = $[35]; 226: } 227: let t20; 228: if ($[36] !== displayCommand || $[37] !== t19) { 229: t20 = <Text wrap="wrap">{t19}{" "}{displayCommand}</Text>; 230: $[36] = displayCommand; 231: $[37] = t19; 232: $[38] = t20; 233: } else { 234: t20 = $[38]; 235: } 236: let t21; 237: if ($[39] !== t12 || $[40] !== t17 || $[41] !== t20) { 238: t21 = <Box flexDirection="column">{t12}{t17}{t20}</Box>; 239: $[39] = t12; 240: $[40] = t17; 241: $[41] = t20; 242: $[42] = t21; 243: } else { 244: t21 = $[42]; 245: } 246: let t22; 247: if ($[43] === Symbol.for("react.memo_cache_sentinel")) { 248: t22 = <Text bold={true}>Output:</Text>; 249: $[43] = t22; 250: } else { 251: t22 = $[43]; 252: } 253: let t23; 254: if ($[44] === Symbol.for("react.memo_cache_sentinel")) { 255: t23 = <Text dimColor={true}>Loading output…</Text>; 256: $[44] = t23; 257: } else { 258: t23 = $[44]; 259: } 260: let t24; 261: if ($[45] !== columns || $[46] !== deferredOutputPromise) { 262: t24 = <Box flexDirection="column">{t22}<Suspense fallback={t23}><ShellOutputContent outputPromise={deferredOutputPromise} columns={columns} /></Suspense></Box>; 263: $[45] = columns; 264: $[46] = deferredOutputPromise; 265: $[47] = t24; 266: } else { 267: t24 = $[47]; 268: } 269: let t25; 270: if ($[48] !== handleClose || $[49] !== t10 || $[50] !== t21 || $[51] !== t24 || $[52] !== t9) { 271: t25 = <Dialog title={t9} onCancel={handleClose} color="background" inputGuide={t10}>{t21}{t24}</Dialog>; 272: $[48] = handleClose; 273: $[49] = t10; 274: $[50] = t21; 275: $[51] = t24; 276: $[52] = t9; 277: $[53] = t25; 278: } else { 279: t25 = $[53]; 280: } 281: let t26; 282: if ($[54] !== handleKeyDown || $[55] !== t25) { 283: t26 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t25}</Box>; 284: $[54] = handleKeyDown; 285: $[55] = t25; 286: $[56] = t26; 287: } else { 288: t26 = $[56]; 289: } 290: return t26; 291: } 292: function _temp(setOutputPromise_0, shell_0) { 293: return setOutputPromise_0(getTaskOutput(shell_0)); 294: } 295: type ShellOutputContentProps = { 296: outputPromise: Promise<TaskOutputResult>; 297: columns: number; 298: }; 299: function ShellOutputContent(t0) { 300: const $ = _c(19); 301: const { 302: outputPromise, 303: columns 304: } = t0; 305: const { 306: content, 307: bytesTotal 308: } = use(outputPromise); 309: if (!content) { 310: let t1; 311: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 312: t1 = <Text dimColor={true}>No output available</Text>; 313: $[0] = t1; 314: } else { 315: t1 = $[0]; 316: } 317: return t1; 318: } 319: let isIncomplete; 320: let rendered; 321: if ($[1] !== bytesTotal || $[2] !== content) { 322: const starts = []; 323: let pos = content.length; 324: for (let i = 0; i < 10 && pos > 0; i++) { 325: const prev = content.lastIndexOf("\n", pos - 1); 326: starts.push(prev + 1); 327: pos = prev; 328: } 329: starts.reverse(); 330: isIncomplete = bytesTotal > content.length; 331: rendered = []; 332: for (let i_0 = 0; i_0 < starts.length; i_0++) { 333: const start = starts[i_0]; 334: const end = i_0 < starts.length - 1 ? starts[i_0 + 1] - 1 : content.length; 335: const line = content.slice(start, end); 336: if (line) { 337: rendered.push(line); 338: } 339: } 340: $[1] = bytesTotal; 341: $[2] = content; 342: $[3] = isIncomplete; 343: $[4] = rendered; 344: } else { 345: isIncomplete = $[3]; 346: rendered = $[4]; 347: } 348: const t1 = columns - 6; 349: let t2; 350: if ($[5] !== rendered) { 351: t2 = rendered.map(_temp2); 352: $[5] = rendered; 353: $[6] = t2; 354: } else { 355: t2 = $[6]; 356: } 357: let t3; 358: if ($[7] !== t1 || $[8] !== t2) { 359: t3 = <Box borderStyle="round" paddingX={1} flexDirection="column" height={12} maxWidth={t1}>{t2}</Box>; 360: $[7] = t1; 361: $[8] = t2; 362: $[9] = t3; 363: } else { 364: t3 = $[9]; 365: } 366: const t4 = `Showing ${rendered.length} lines`; 367: let t5; 368: if ($[10] !== bytesTotal || $[11] !== isIncomplete) { 369: t5 = isIncomplete ? ` of ${formatFileSize(bytesTotal)}` : ""; 370: $[10] = bytesTotal; 371: $[11] = isIncomplete; 372: $[12] = t5; 373: } else { 374: t5 = $[12]; 375: } 376: let t6; 377: if ($[13] !== t4 || $[14] !== t5) { 378: t6 = <Text dimColor={true} italic={true}>{t4}{t5}</Text>; 379: $[13] = t4; 380: $[14] = t5; 381: $[15] = t6; 382: } else { 383: t6 = $[15]; 384: } 385: let t7; 386: if ($[16] !== t3 || $[17] !== t6) { 387: t7 = <>{t3}{t6}</>; 388: $[16] = t3; 389: $[17] = t6; 390: $[18] = t7; 391: } else { 392: t7 = $[18]; 393: } 394: return t7; 395: } 396: function _temp2(line_0, i_1) { 397: return <Text key={i_1} wrap="truncate-end">{line_0}</Text>; 398: }

File: src/components/tasks/ShellProgress.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import type { ReactNode } from 'react'; 3: import React from 'react'; 4: import { Text } from 'src/ink.js'; 5: import type { TaskStatus } from 'src/Task.js'; 6: import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js'; 7: import type { DeepImmutable } from 'src/types/utils.js'; 8: type TaskStatusTextProps = { 9: status: TaskStatus; 10: label?: string; 11: suffix?: string; 12: }; 13: export function TaskStatusText(t0) { 14: const $ = _c(4); 15: const { 16: status, 17: label, 18: suffix 19: } = t0; 20: const displayLabel = label ?? status; 21: const color = status === "completed" ? "success" : status === "failed" ? "error" : status === "killed" ? "warning" : undefined; 22: let t1; 23: if ($[0] !== color || $[1] !== displayLabel || $[2] !== suffix) { 24: t1 = <Text color={color} dimColor={true}>({displayLabel}{suffix})</Text>; 25: $[0] = color; 26: $[1] = displayLabel; 27: $[2] = suffix; 28: $[3] = t1; 29: } else { 30: t1 = $[3]; 31: } 32: return t1; 33: } 34: export function ShellProgress(t0) { 35: const $ = _c(4); 36: const { 37: shell 38: } = t0; 39: switch (shell.status) { 40: case "completed": 41: { 42: let t1; 43: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 44: t1 = <TaskStatusText status="completed" label="done" />; 45: $[0] = t1; 46: } else { 47: t1 = $[0]; 48: } 49: return t1; 50: } 51: case "failed": 52: { 53: let t1; 54: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 55: t1 = <TaskStatusText status="failed" label="error" />; 56: $[1] = t1; 57: } else { 58: t1 = $[1]; 59: } 60: return t1; 61: } 62: case "killed": 63: { 64: let t1; 65: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 66: t1 = <TaskStatusText status="killed" label="stopped" />; 67: $[2] = t1; 68: } else { 69: t1 = $[2]; 70: } 71: return t1; 72: } 73: case "running": 74: case "pending": 75: { 76: let t1; 77: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 78: t1 = <TaskStatusText status="running" />; 79: $[3] = t1; 80: } else { 81: t1 = $[3]; 82: } 83: return t1; 84: } 85: } 86: }

File: src/components/tasks/taskStatusUtils.tsx

typescript 1: import figures from 'figures'; 2: import type { TaskStatus } from 'src/Task.js'; 3: import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js'; 4: import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'; 5: import { isBackgroundTask, type TaskState } from 'src/tasks/types.js'; 6: import type { DeepImmutable } from 'src/types/utils.js'; 7: import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js'; 8: export function isTerminalStatus(status: TaskStatus): boolean { 9: return status === 'completed' || status === 'failed' || status === 'killed'; 10: } 11: export function getTaskStatusIcon(status: TaskStatus, options?: { 12: isIdle?: boolean; 13: awaitingApproval?: boolean; 14: hasError?: boolean; 15: shutdownRequested?: boolean; 16: }): string { 17: const { 18: isIdle, 19: awaitingApproval, 20: hasError, 21: shutdownRequested 22: } = options ?? {}; 23: if (hasError) return figures.cross; 24: if (awaitingApproval) return figures.questionMarkPrefix; 25: if (shutdownRequested) return figures.warning; 26: if (status === 'running') { 27: if (isIdle) return figures.ellipsis; 28: return figures.play; 29: } 30: if (status === 'completed') return figures.tick; 31: if (status === 'failed' || status === 'killed') return figures.cross; 32: return figures.bullet; 33: } 34: export function getTaskStatusColor(status: TaskStatus, options?: { 35: isIdle?: boolean; 36: awaitingApproval?: boolean; 37: hasError?: boolean; 38: shutdownRequested?: boolean; 39: }): 'success' | 'error' | 'warning' | 'background' { 40: const { 41: isIdle, 42: awaitingApproval, 43: hasError, 44: shutdownRequested 45: } = options ?? {}; 46: if (hasError) return 'error'; 47: if (awaitingApproval) return 'warning'; 48: if (shutdownRequested) return 'warning'; 49: if (isIdle) return 'background'; 50: if (status === 'completed') return 'success'; 51: if (status === 'failed') return 'error'; 52: if (status === 'killed') return 'warning'; 53: return 'background'; 54: } 55: export function describeTeammateActivity(t: DeepImmutable<InProcessTeammateTaskState>): string { 56: if (t.shutdownRequested) return 'stopping'; 57: if (t.awaitingPlanApproval) return 'awaiting approval'; 58: if (t.isIdle) return 'idle'; 59: return (t.progress?.recentActivities && summarizeRecentActivities(t.progress.recentActivities)) ?? t.progress?.lastActivity?.activityDescription ?? 'working'; 60: } 61: export function shouldHideTasksFooter(tasks: { 62: [taskId: string]: TaskState; 63: }, showSpinnerTree: boolean): boolean { 64: if (!showSpinnerTree) return false; 65: let hasVisibleTask = false; 66: for (const t of Object.values(tasks) as TaskState[]) { 67: if (!isBackgroundTask(t) || "external" === 'ant' && isPanelAgentTask(t)) { 68: continue; 69: } 70: hasVisibleTask = true; 71: if (t.type !== 'in_process_teammate') return false; 72: } 73: return hasVisibleTask; 74: }

File: src/components/teams/TeamsDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { randomUUID } from 'crypto'; 3: import figures from 'figures'; 4: import * as React from 'react'; 5: import { useCallback, useEffect, useMemo, useState } from 'react'; 6: import { useInterval } from 'usehooks-ts'; 7: import { useRegisterOverlay } from '../../context/overlayContext.js'; 8: import { stringWidth } from '../../ink/stringWidth.js'; 9: import { Box, Text, useInput } from '../../ink.js'; 10: import { useKeybindings } from '../../keybindings/useKeybinding.js'; 11: import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'; 12: import { type AppState, useAppState, useSetAppState } from '../../state/AppState.js'; 13: import { getEmptyToolPermissionContext } from '../../Tool.js'; 14: import { AGENT_COLOR_TO_THEME_COLOR } from '../../tools/AgentTool/agentColorManager.js'; 15: import { logForDebugging } from '../../utils/debug.js'; 16: import { execFileNoThrow } from '../../utils/execFileNoThrow.js'; 17: import { truncateToWidth } from '../../utils/format.js'; 18: import { getNextPermissionMode } from '../../utils/permissions/getNextPermissionMode.js'; 19: import { getModeColor, type PermissionMode, permissionModeFromString, permissionModeSymbol } from '../../utils/permissions/PermissionMode.js'; 20: import { jsonStringify } from '../../utils/slowOperations.js'; 21: import { IT2_COMMAND, isInsideTmuxSync } from '../../utils/swarm/backends/detection.js'; 22: import { ensureBackendsRegistered, getBackendByType, getCachedBackend } from '../../utils/swarm/backends/registry.js'; 23: import type { PaneBackendType } from '../../utils/swarm/backends/types.js'; 24: import { getSwarmSocketName, TMUX_COMMAND } from '../../utils/swarm/constants.js'; 25: import { addHiddenPaneId, removeHiddenPaneId, removeMemberFromTeam, setMemberMode, setMultipleMemberModes } from '../../utils/swarm/teamHelpers.js'; 26: import { listTasks, type Task, unassignTeammateTasks } from '../../utils/tasks.js'; 27: import { getTeammateStatuses, type TeammateStatus, type TeamSummary } from '../../utils/teamDiscovery.js'; 28: import { createModeSetRequestMessage, sendShutdownRequestToMailbox, writeToMailbox } from '../../utils/teammateMailbox.js'; 29: import { Dialog } from '../design-system/Dialog.js'; 30: import ThemedText from '../design-system/ThemedText.js'; 31: type Props = { 32: initialTeams?: TeamSummary[]; 33: onDone: () => void; 34: }; 35: type DialogLevel = { 36: type: 'teammateList'; 37: teamName: string; 38: } | { 39: type: 'teammateDetail'; 40: teamName: string; 41: memberName: string; 42: }; 43: export function TeamsDialog({ 44: initialTeams, 45: onDone 46: }: Props): React.ReactNode { 47: useRegisterOverlay('teams-dialog'); 48: const setAppState = useSetAppState(); 49: const firstTeamName = initialTeams?.[0]?.name ?? ''; 50: const [dialogLevel, setDialogLevel] = useState<DialogLevel>({ 51: type: 'teammateList', 52: teamName: firstTeamName 53: }); 54: const [selectedIndex, setSelectedIndex] = useState(0); 55: const [refreshKey, setRefreshKey] = useState(0); 56: const teammateStatuses = useMemo(() => { 57: return getTeammateStatuses(dialogLevel.teamName); 58: }, [dialogLevel.teamName, refreshKey]); 59: useInterval(() => { 60: setRefreshKey(k => k + 1); 61: }, 1000); 62: const currentTeammate = useMemo(() => { 63: if (dialogLevel.type !== 'teammateDetail') return null; 64: return teammateStatuses.find(t => t.name === dialogLevel.memberName) ?? null; 65: }, [dialogLevel, teammateStatuses]); 66: const isBypassAvailable = useAppState(s => s.toolPermissionContext.isBypassPermissionsModeAvailable); 67: const goBackToList = (): void => { 68: setDialogLevel({ 69: type: 'teammateList', 70: teamName: dialogLevel.teamName 71: }); 72: setSelectedIndex(0); 73: }; 74: const handleCycleMode = useCallback(() => { 75: if (dialogLevel.type === 'teammateDetail' && currentTeammate) { 76: cycleTeammateMode(currentTeammate, dialogLevel.teamName, isBypassAvailable); 77: setRefreshKey(k => k + 1); 78: } else if (dialogLevel.type === 'teammateList' && teammateStatuses.length > 0) { 79: cycleAllTeammateModes(teammateStatuses, dialogLevel.teamName, isBypassAvailable); 80: setRefreshKey(k => k + 1); 81: } 82: }, [dialogLevel, currentTeammate, teammateStatuses, isBypassAvailable]); 83: useKeybindings({ 84: 'confirm:cycleMode': handleCycleMode 85: }, { 86: context: 'Confirmation' 87: }); 88: useInput((input, key) => { 89: if (key.leftArrow) { 90: if (dialogLevel.type === 'teammateDetail') { 91: goBackToList(); 92: } 93: return; 94: } 95: if (key.upArrow || key.downArrow) { 96: const maxIndex = getMaxIndex(); 97: if (key.upArrow) { 98: setSelectedIndex(prev => Math.max(0, prev - 1)); 99: } else { 100: setSelectedIndex(prev => Math.min(maxIndex, prev + 1)); 101: } 102: return; 103: } 104: if (key.return) { 105: if (dialogLevel.type === 'teammateList' && teammateStatuses[selectedIndex]) { 106: setDialogLevel({ 107: type: 'teammateDetail', 108: teamName: dialogLevel.teamName, 109: memberName: teammateStatuses[selectedIndex].name 110: }); 111: } else if (dialogLevel.type === 'teammateDetail' && currentTeammate) { 112: void viewTeammateOutput(currentTeammate.tmuxPaneId, currentTeammate.backendType); 113: onDone(); 114: } 115: return; 116: } 117: if (input === 'k') { 118: if (dialogLevel.type === 'teammateList' && teammateStatuses[selectedIndex]) { 119: void killTeammate(teammateStatuses[selectedIndex].tmuxPaneId, teammateStatuses[selectedIndex].backendType, dialogLevel.teamName, teammateStatuses[selectedIndex].agentId, teammateStatuses[selectedIndex].name, setAppState).then(() => { 120: setRefreshKey(k => k + 1); 121: setSelectedIndex(prev => Math.max(0, Math.min(prev, teammateStatuses.length - 2))); 122: }); 123: } else if (dialogLevel.type === 'teammateDetail' && currentTeammate) { 124: void killTeammate(currentTeammate.tmuxPaneId, currentTeammate.backendType, dialogLevel.teamName, currentTeammate.agentId, currentTeammate.name, setAppState); 125: goBackToList(); 126: } 127: return; 128: } 129: if (input === 's') { 130: if (dialogLevel.type === 'teammateList' && teammateStatuses[selectedIndex]) { 131: const teammate = teammateStatuses[selectedIndex]; 132: void sendShutdownRequestToMailbox(teammate.name, dialogLevel.teamName, 'Graceful shutdown requested by team lead'); 133: } else if (dialogLevel.type === 'teammateDetail' && currentTeammate) { 134: void sendShutdownRequestToMailbox(currentTeammate.name, dialogLevel.teamName, 'Graceful shutdown requested by team lead'); 135: goBackToList(); 136: } 137: return; 138: } 139: if (input === 'h') { 140: const backend = getCachedBackend(); 141: const teammate = dialogLevel.type === 'teammateList' ? teammateStatuses[selectedIndex] : dialogLevel.type === 'teammateDetail' ? currentTeammate : null; 142: if (teammate && backend?.supportsHideShow) { 143: void toggleTeammateVisibility(teammate, dialogLevel.teamName).then(() => { 144: setRefreshKey(k => k + 1); 145: }); 146: if (dialogLevel.type === 'teammateDetail') { 147: goBackToList(); 148: } 149: } 150: return; 151: } 152: if (input === 'H' && dialogLevel.type === 'teammateList') { 153: const backend = getCachedBackend(); 154: if (backend?.supportsHideShow && teammateStatuses.length > 0) { 155: const anyVisible = teammateStatuses.some(t => !t.isHidden); 156: void Promise.all(teammateStatuses.map(t => anyVisible ? hideTeammate(t, dialogLevel.teamName) : showTeammate(t, dialogLevel.teamName))).then(() => { 157: setRefreshKey(k => k + 1); 158: }); 159: } 160: return; 161: } 162: if (input === 'p' && dialogLevel.type === 'teammateList') { 163: const idleTeammates = teammateStatuses.filter(t => t.status === 'idle'); 164: if (idleTeammates.length > 0) { 165: void Promise.all(idleTeammates.map(t => killTeammate(t.tmuxPaneId, t.backendType, dialogLevel.teamName, t.agentId, t.name, setAppState))).then(() => { 166: setRefreshKey(k => k + 1); 167: setSelectedIndex(prev => Math.max(0, Math.min(prev, teammateStatuses.length - idleTeammates.length - 1))); 168: }); 169: } 170: return; 171: } 172: }); 173: function getMaxIndex(): number { 174: if (dialogLevel.type === 'teammateList') { 175: return Math.max(0, teammateStatuses.length - 1); 176: } 177: return 0; 178: } 179: if (dialogLevel.type === 'teammateList') { 180: return <TeamDetailView teamName={dialogLevel.teamName} teammates={teammateStatuses} selectedIndex={selectedIndex} onCancel={onDone} />; 181: } 182: if (dialogLevel.type === 'teammateDetail' && currentTeammate) { 183: return <TeammateDetailView teammate={currentTeammate} teamName={dialogLevel.teamName} onCancel={goBackToList} />; 184: } 185: return null; 186: } 187: type TeamDetailViewProps = { 188: teamName: string; 189: teammates: TeammateStatus[]; 190: selectedIndex: number; 191: onCancel: () => void; 192: }; 193: function TeamDetailView(t0) { 194: const $ = _c(13); 195: const { 196: teamName, 197: teammates, 198: selectedIndex, 199: onCancel 200: } = t0; 201: const subtitle = `${teammates.length} ${teammates.length === 1 ? "teammate" : "teammates"}`; 202: const supportsHideShow = getCachedBackend()?.supportsHideShow ?? false; 203: const cycleModeShortcut = useShortcutDisplay("confirm:cycleMode", "Confirmation", "shift+tab"); 204: const t1 = `Team ${teamName}`; 205: let t2; 206: if ($[0] !== selectedIndex || $[1] !== teammates) { 207: t2 = teammates.length === 0 ? <Text dimColor={true}>No teammates</Text> : <Box flexDirection="column">{teammates.map((teammate, index) => <TeammateListItem key={teammate.agentId} teammate={teammate} isSelected={index === selectedIndex} />)}</Box>; 208: $[0] = selectedIndex; 209: $[1] = teammates; 210: $[2] = t2; 211: } else { 212: t2 = $[2]; 213: } 214: let t3; 215: if ($[3] !== onCancel || $[4] !== subtitle || $[5] !== t1 || $[6] !== t2) { 216: t3 = <Dialog title={t1} subtitle={subtitle} onCancel={onCancel} color="background" hideInputGuide={true}>{t2}</Dialog>; 217: $[3] = onCancel; 218: $[4] = subtitle; 219: $[5] = t1; 220: $[6] = t2; 221: $[7] = t3; 222: } else { 223: t3 = $[7]; 224: } 225: let t4; 226: if ($[8] !== cycleModeShortcut) { 227: t4 = <Box marginLeft={1}><Text dimColor={true}>{figures.arrowUp}/{figures.arrowDown} select · Enter view · k kill · s shutdown · p prune idle{supportsHideShow && " \xB7 h hide/show \xB7 H hide/show all"}{" \xB7 "}{cycleModeShortcut} sync cycle modes for all · Esc close</Text></Box>; 228: $[8] = cycleModeShortcut; 229: $[9] = t4; 230: } else { 231: t4 = $[9]; 232: } 233: let t5; 234: if ($[10] !== t3 || $[11] !== t4) { 235: t5 = <>{t3}{t4}</>; 236: $[10] = t3; 237: $[11] = t4; 238: $[12] = t5; 239: } else { 240: t5 = $[12]; 241: } 242: return t5; 243: } 244: type TeammateListItemProps = { 245: teammate: TeammateStatus; 246: isSelected: boolean; 247: }; 248: function TeammateListItem(t0) { 249: const $ = _c(21); 250: const { 251: teammate, 252: isSelected 253: } = t0; 254: const isIdle = teammate.status === "idle"; 255: const shouldDim = isIdle && !isSelected; 256: let modeSymbol; 257: let t1; 258: if ($[0] !== teammate.mode) { 259: const mode = teammate.mode ? permissionModeFromString(teammate.mode) : "default"; 260: modeSymbol = permissionModeSymbol(mode); 261: t1 = getModeColor(mode); 262: $[0] = teammate.mode; 263: $[1] = modeSymbol; 264: $[2] = t1; 265: } else { 266: modeSymbol = $[1]; 267: t1 = $[2]; 268: } 269: const modeColor = t1; 270: const t2 = isSelected ? "suggestion" : undefined; 271: const t3 = isSelected ? figures.pointer + " " : " "; 272: let t4; 273: if ($[3] !== teammate.isHidden) { 274: t4 = teammate.isHidden && <Text dimColor={true}>[hidden] </Text>; 275: $[3] = teammate.isHidden; 276: $[4] = t4; 277: } else { 278: t4 = $[4]; 279: } 280: let t5; 281: if ($[5] !== isIdle) { 282: t5 = isIdle && <Text dimColor={true}>[idle] </Text>; 283: $[5] = isIdle; 284: $[6] = t5; 285: } else { 286: t5 = $[6]; 287: } 288: let t6; 289: if ($[7] !== modeColor || $[8] !== modeSymbol) { 290: t6 = modeSymbol && <Text color={modeColor}>{modeSymbol} </Text>; 291: $[7] = modeColor; 292: $[8] = modeSymbol; 293: $[9] = t6; 294: } else { 295: t6 = $[9]; 296: } 297: let t7; 298: if ($[10] !== teammate.model) { 299: t7 = teammate.model && <Text dimColor={true}> ({teammate.model})</Text>; 300: $[10] = teammate.model; 301: $[11] = t7; 302: } else { 303: t7 = $[11]; 304: } 305: let t8; 306: if ($[12] !== shouldDim || $[13] !== t2 || $[14] !== t3 || $[15] !== t4 || $[16] !== t5 || $[17] !== t6 || $[18] !== t7 || $[19] !== teammate.name) { 307: t8 = <Text color={t2} dimColor={shouldDim}>{t3}{t4}{t5}{t6}@{teammate.name}{t7}</Text>; 308: $[12] = shouldDim; 309: $[13] = t2; 310: $[14] = t3; 311: $[15] = t4; 312: $[16] = t5; 313: $[17] = t6; 314: $[18] = t7; 315: $[19] = teammate.name; 316: $[20] = t8; 317: } else { 318: t8 = $[20]; 319: } 320: return t8; 321: } 322: type TeammateDetailViewProps = { 323: teammate: TeammateStatus; 324: teamName: string; 325: onCancel: () => void; 326: }; 327: function TeammateDetailView(t0) { 328: const $ = _c(39); 329: const { 330: teammate, 331: teamName, 332: onCancel 333: } = t0; 334: const [promptExpanded, setPromptExpanded] = useState(false); 335: const cycleModeShortcut = useShortcutDisplay("confirm:cycleMode", "Confirmation", "shift+tab"); 336: const themeColor = teammate.color ? AGENT_COLOR_TO_THEME_COLOR[teammate.color as keyof typeof AGENT_COLOR_TO_THEME_COLOR] : undefined; 337: let t1; 338: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 339: t1 = []; 340: $[0] = t1; 341: } else { 342: t1 = $[0]; 343: } 344: const [teammateTasks, setTeammateTasks] = useState(t1); 345: let t2; 346: let t3; 347: if ($[1] !== teamName || $[2] !== teammate.agentId || $[3] !== teammate.name) { 348: t2 = () => { 349: let cancelled = false; 350: listTasks(teamName).then(allTasks => { 351: if (cancelled) { 352: return; 353: } 354: setTeammateTasks(allTasks.filter(task => task.owner === teammate.agentId || task.owner === teammate.name)); 355: }); 356: return () => { 357: cancelled = true; 358: }; 359: }; 360: t3 = [teamName, teammate.agentId, teammate.name]; 361: $[1] = teamName; 362: $[2] = teammate.agentId; 363: $[3] = teammate.name; 364: $[4] = t2; 365: $[5] = t3; 366: } else { 367: t2 = $[4]; 368: t3 = $[5]; 369: } 370: useEffect(t2, t3); 371: let t4; 372: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 373: t4 = input => { 374: if (input === "p") { 375: setPromptExpanded(_temp); 376: } 377: }; 378: $[6] = t4; 379: } else { 380: t4 = $[6]; 381: } 382: useInput(t4); 383: const workingPath = teammate.worktreePath || teammate.cwd; 384: let subtitleParts; 385: if ($[7] !== teammate.model || $[8] !== teammate.worktreePath || $[9] !== workingPath) { 386: subtitleParts = []; 387: if (teammate.model) { 388: subtitleParts.push(teammate.model); 389: } 390: if (workingPath) { 391: subtitleParts.push(teammate.worktreePath ? `worktree: ${workingPath}` : workingPath); 392: } 393: $[7] = teammate.model; 394: $[8] = teammate.worktreePath; 395: $[9] = workingPath; 396: $[10] = subtitleParts; 397: } else { 398: subtitleParts = $[10]; 399: } 400: const subtitle = subtitleParts.join(" \xB7 ") || undefined; 401: let modeSymbol; 402: let t5; 403: if ($[11] !== teammate.mode) { 404: const mode = teammate.mode ? permissionModeFromString(teammate.mode) : "default"; 405: modeSymbol = permissionModeSymbol(mode); 406: t5 = getModeColor(mode); 407: $[11] = teammate.mode; 408: $[12] = modeSymbol; 409: $[13] = t5; 410: } else { 411: modeSymbol = $[12]; 412: t5 = $[13]; 413: } 414: const modeColor = t5; 415: let t6; 416: if ($[14] !== modeColor || $[15] !== modeSymbol) { 417: t6 = modeSymbol && <Text color={modeColor}>{modeSymbol} </Text>; 418: $[14] = modeColor; 419: $[15] = modeSymbol; 420: $[16] = t6; 421: } else { 422: t6 = $[16]; 423: } 424: let t7; 425: if ($[17] !== teammate.name || $[18] !== themeColor) { 426: t7 = themeColor ? <ThemedText color={themeColor}>{`@${teammate.name}`}</ThemedText> : `@${teammate.name}`; 427: $[17] = teammate.name; 428: $[18] = themeColor; 429: $[19] = t7; 430: } else { 431: t7 = $[19]; 432: } 433: let t8; 434: if ($[20] !== t6 || $[21] !== t7) { 435: t8 = <>{t6}{t7}</>; 436: $[20] = t6; 437: $[21] = t7; 438: $[22] = t8; 439: } else { 440: t8 = $[22]; 441: } 442: const title = t8; 443: let t9; 444: if ($[23] !== teammateTasks) { 445: t9 = teammateTasks.length > 0 && <Box flexDirection="column"><Text bold={true}>Tasks</Text>{teammateTasks.map(_temp2)}</Box>; 446: $[23] = teammateTasks; 447: $[24] = t9; 448: } else { 449: t9 = $[24]; 450: } 451: let t10; 452: if ($[25] !== promptExpanded || $[26] !== teammate.prompt) { 453: t10 = teammate.prompt && <Box flexDirection="column"><Text bold={true}>Prompt</Text><Text>{promptExpanded ? teammate.prompt : truncateToWidth(teammate.prompt, 80)}{stringWidth(teammate.prompt) > 80 && !promptExpanded && <Text dimColor={true}> (p to expand)</Text>}</Text></Box>; 454: $[25] = promptExpanded; 455: $[26] = teammate.prompt; 456: $[27] = t10; 457: } else { 458: t10 = $[27]; 459: } 460: let t11; 461: if ($[28] !== onCancel || $[29] !== subtitle || $[30] !== t10 || $[31] !== t9 || $[32] !== title) { 462: t11 = <Dialog title={title} subtitle={subtitle} onCancel={onCancel} color="background" hideInputGuide={true}>{t9}{t10}</Dialog>; 463: $[28] = onCancel; 464: $[29] = subtitle; 465: $[30] = t10; 466: $[31] = t9; 467: $[32] = title; 468: $[33] = t11; 469: } else { 470: t11 = $[33]; 471: } 472: let t12; 473: if ($[34] !== cycleModeShortcut) { 474: t12 = <Box marginLeft={1}><Text dimColor={true}>{figures.arrowLeft} back · Esc close · k kill · s shutdown{getCachedBackend()?.supportsHideShow && " \xB7 h hide/show"}{" \xB7 "}{cycleModeShortcut} cycle mode</Text></Box>; 475: $[34] = cycleModeShortcut; 476: $[35] = t12; 477: } else { 478: t12 = $[35]; 479: } 480: let t13; 481: if ($[36] !== t11 || $[37] !== t12) { 482: t13 = <>{t11}{t12}</>; 483: $[36] = t11; 484: $[37] = t12; 485: $[38] = t13; 486: } else { 487: t13 = $[38]; 488: } 489: return t13; 490: } 491: function _temp2(task_0) { 492: return <Text key={task_0.id} color={task_0.status === "completed" ? "success" : undefined}>{task_0.status === "completed" ? figures.tick : "\u25FC"}{" "}{task_0.subject}</Text>; 493: } 494: function _temp(prev) { 495: return !prev; 496: } 497: async function killTeammate(paneId: string, backendType: PaneBackendType | undefined, teamName: string, teammateId: string, teammateName: string, setAppState: (f: (prev: AppState) => AppState) => void): Promise<void> { 498: if (backendType) { 499: try { 500: await ensureBackendsRegistered(); 501: await getBackendByType(backendType).killPane(paneId, !isInsideTmuxSync()); 502: } catch (error) { 503: logForDebugging(`[TeamsDialog] Failed to kill pane ${paneId}: ${error}`); 504: } 505: } else { 506: logForDebugging(`[TeamsDialog] Skipping pane kill for ${paneId}: no backendType recorded`); 507: } 508: removeMemberFromTeam(teamName, paneId); 509: const { 510: notificationMessage 511: } = await unassignTeammateTasks(teamName, teammateId, teammateName, 'terminated'); 512: setAppState(prev => { 513: if (!prev.teamContext?.teammates) return prev; 514: if (!(teammateId in prev.teamContext.teammates)) return prev; 515: const { 516: [teammateId]: _, 517: ...remainingTeammates 518: } = prev.teamContext.teammates; 519: return { 520: ...prev, 521: teamContext: { 522: ...prev.teamContext, 523: teammates: remainingTeammates 524: }, 525: inbox: { 526: messages: [...prev.inbox.messages, { 527: id: randomUUID(), 528: from: 'system', 529: text: jsonStringify({ 530: type: 'teammate_terminated', 531: message: notificationMessage 532: }), 533: timestamp: new Date().toISOString(), 534: status: 'pending' as const 535: }] 536: } 537: }; 538: }); 539: logForDebugging(`[TeamsDialog] Removed ${teammateId} from teamContext`); 540: } 541: async function viewTeammateOutput(paneId: string, backendType: PaneBackendType | undefined): Promise<void> { 542: if (backendType === 'iterm2') { 543: await execFileNoThrow(IT2_COMMAND, ['session', 'focus', '-s', paneId]); 544: } else { 545: const args = isInsideTmuxSync() ? ['select-pane', '-t', paneId] : ['-L', getSwarmSocketName(), 'select-pane', '-t', paneId]; 546: await execFileNoThrow(TMUX_COMMAND, args); 547: } 548: } 549: async function toggleTeammateVisibility(teammate: TeammateStatus, teamName: string): Promise<void> { 550: if (teammate.isHidden) { 551: await showTeammate(teammate, teamName); 552: } else { 553: await hideTeammate(teammate, teamName); 554: } 555: } 556: async function hideTeammate(teammate: TeammateStatus, teamName: string): Promise<void> {} 557: async function showTeammate(teammate: TeammateStatus, teamName: string): Promise<void> {} 558: function sendModeChangeToTeammate(teammateName: string, teamName: string, targetMode: PermissionMode): void { 559: setMemberMode(teamName, teammateName, targetMode); 560: const message = createModeSetRequestMessage({ 561: mode: targetMode, 562: from: 'team-lead' 563: }); 564: void writeToMailbox(teammateName, { 565: from: 'team-lead', 566: text: jsonStringify(message), 567: timestamp: new Date().toISOString() 568: }, teamName); 569: logForDebugging(`[TeamsDialog] Sent mode change to ${teammateName}: ${targetMode}`); 570: } 571: function cycleTeammateMode(teammate: TeammateStatus, teamName: string, isBypassAvailable: boolean): void { 572: const currentMode = teammate.mode ? permissionModeFromString(teammate.mode) : 'default'; 573: const context = { 574: ...getEmptyToolPermissionContext(), 575: mode: currentMode, 576: isBypassPermissionsModeAvailable: isBypassAvailable 577: }; 578: const nextMode = getNextPermissionMode(context); 579: sendModeChangeToTeammate(teammate.name, teamName, nextMode); 580: } 581: function cycleAllTeammateModes(teammates: TeammateStatus[], teamName: string, isBypassAvailable: boolean): void { 582: if (teammates.length === 0) return; 583: const modes = teammates.map(t => t.mode ? permissionModeFromString(t.mode) : 'default'); 584: const allSame = modes.every(m => m === modes[0]); 585: const targetMode = !allSame ? 'default' : getNextPermissionMode({ 586: ...getEmptyToolPermissionContext(), 587: mode: modes[0] ?? 'default', 588: isBypassPermissionsModeAvailable: isBypassAvailable 589: }); 590: const modeUpdates = teammates.map(t => ({ 591: memberName: t.name, 592: mode: targetMode 593: })); 594: setMultipleMemberModes(teamName, modeUpdates); 595: for (const teammate of teammates) { 596: const message = createModeSetRequestMessage({ 597: mode: targetMode, 598: from: 'team-lead' 599: }); 600: void writeToMailbox(teammate.name, { 601: from: 'team-lead', 602: text: jsonStringify(message), 603: timestamp: new Date().toISOString() 604: }, teamName); 605: } 606: logForDebugging(`[TeamsDialog] Sent mode change to all ${teammates.length} teammates: ${targetMode}`); 607: }

File: src/components/teams/TeamStatus.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Text } from '../../ink.js'; 4: import { useAppState } from '../../state/AppState.js'; 5: type Props = { 6: teamsSelected: boolean; 7: showHint: boolean; 8: }; 9: export function TeamStatus(t0) { 10: const $ = _c(14); 11: const { 12: teamsSelected, 13: showHint 14: } = t0; 15: const teamContext = useAppState(_temp); 16: let t1; 17: if ($[0] !== teamContext) { 18: t1 = teamContext ? Object.values(teamContext.teammates).filter(_temp2).length : 0; 19: $[0] = teamContext; 20: $[1] = t1; 21: } else { 22: t1 = $[1]; 23: } 24: const totalTeammates = t1; 25: if (totalTeammates === 0) { 26: return null; 27: } 28: let t2; 29: if ($[2] !== showHint || $[3] !== teamsSelected) { 30: t2 = showHint && teamsSelected ? <><Text dimColor={true}>· </Text><Text dimColor={true}>Enter to view</Text></> : null; 31: $[2] = showHint; 32: $[3] = teamsSelected; 33: $[4] = t2; 34: } else { 35: t2 = $[4]; 36: } 37: const hint = t2; 38: const statusText = `${totalTeammates} ${totalTeammates === 1 ? "teammate" : "teammates"}`; 39: const t3 = teamsSelected ? "selected" : "normal"; 40: let t4; 41: if ($[5] !== statusText || $[6] !== t3 || $[7] !== teamsSelected) { 42: t4 = <Text key={t3} color="background" inverse={teamsSelected}>{statusText}</Text>; 43: $[5] = statusText; 44: $[6] = t3; 45: $[7] = teamsSelected; 46: $[8] = t4; 47: } else { 48: t4 = $[8]; 49: } 50: let t5; 51: if ($[9] !== hint) { 52: t5 = hint ? <Text> {hint}</Text> : null; 53: $[9] = hint; 54: $[10] = t5; 55: } else { 56: t5 = $[10]; 57: } 58: let t6; 59: if ($[11] !== t4 || $[12] !== t5) { 60: t6 = <>{t4}{t5}</>; 61: $[11] = t4; 62: $[12] = t5; 63: $[13] = t6; 64: } else { 65: t6 = $[13]; 66: } 67: return t6; 68: } 69: function _temp2(t) { 70: return t.name !== "team-lead"; 71: } 72: function _temp(s) { 73: return s.teamContext; 74: }

File: src/components/TrustDialog/TrustDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { homedir } from 'os'; 3: import React from 'react'; 4: import { logEvent } from 'src/services/analytics/index.js'; 5: import { setSessionTrustAccepted } from '../../bootstrap/state.js'; 6: import type { Command } from '../../commands.js'; 7: import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 8: import { Box, Link, Text } from '../../ink.js'; 9: import { useKeybinding } from '../../keybindings/useKeybinding.js'; 10: import { getMcpConfigsByScope } from '../../services/mcp/config.js'; 11: import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js'; 12: import { checkHasTrustDialogAccepted, saveCurrentProjectConfig } from '../../utils/config.js'; 13: import { getCwd } from '../../utils/cwd.js'; 14: import { getFsImplementation } from '../../utils/fsOperations.js'; 15: import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js'; 16: import { Select } from '../CustomSelect/index.js'; 17: import { PermissionDialog } from '../permissions/PermissionDialog.js'; 18: import { getApiKeyHelperSources, getAwsCommandsSources, getBashPermissionSources, getDangerousEnvVarsSources, getGcpCommandsSources, getHooksSources, getOtelHeadersHelperSources } from './utils.js'; 19: type Props = { 20: onDone(): void; 21: commands?: Command[]; 22: }; 23: export function TrustDialog(t0) { 24: const $ = _c(33); 25: const { 26: onDone, 27: commands 28: } = t0; 29: let t1; 30: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 31: t1 = getMcpConfigsByScope("project"); 32: $[0] = t1; 33: } else { 34: t1 = $[0]; 35: } 36: const { 37: servers: projectServers 38: } = t1; 39: let t2; 40: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 41: t2 = Object.keys(projectServers); 42: $[1] = t2; 43: } else { 44: t2 = $[1]; 45: } 46: const hasMcpServers = t2.length > 0; 47: let t3; 48: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 49: t3 = getHooksSources(); 50: $[2] = t3; 51: } else { 52: t3 = $[2]; 53: } 54: const hooksSettingSources = t3; 55: const hasHooks = hooksSettingSources.length > 0; 56: let t4; 57: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 58: t4 = getBashPermissionSources(); 59: $[3] = t4; 60: } else { 61: t4 = $[3]; 62: } 63: const bashSettingSources = t4; 64: let t5; 65: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 66: t5 = getApiKeyHelperSources(); 67: $[4] = t5; 68: } else { 69: t5 = $[4]; 70: } 71: const apiKeyHelperSources = t5; 72: const hasApiKeyHelper = apiKeyHelperSources.length > 0; 73: let t6; 74: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 75: t6 = getAwsCommandsSources(); 76: $[5] = t6; 77: } else { 78: t6 = $[5]; 79: } 80: const awsCommandsSources = t6; 81: const hasAwsCommands = awsCommandsSources.length > 0; 82: let t7; 83: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 84: t7 = getGcpCommandsSources(); 85: $[6] = t7; 86: } else { 87: t7 = $[6]; 88: } 89: const gcpCommandsSources = t7; 90: const hasGcpCommands = gcpCommandsSources.length > 0; 91: let t8; 92: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 93: t8 = getOtelHeadersHelperSources(); 94: $[7] = t8; 95: } else { 96: t8 = $[7]; 97: } 98: const otelHeadersHelperSources = t8; 99: const hasOtelHeadersHelper = otelHeadersHelperSources.length > 0; 100: let t9; 101: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 102: t9 = getDangerousEnvVarsSources(); 103: $[8] = t9; 104: } else { 105: t9 = $[8]; 106: } 107: const dangerousEnvVarsSources = t9; 108: const hasDangerousEnvVars = dangerousEnvVarsSources.length > 0; 109: let t10; 110: if ($[9] !== commands) { 111: t10 = commands?.some(_temp2) ?? false; 112: $[9] = commands; 113: $[10] = t10; 114: } else { 115: t10 = $[10]; 116: } 117: const hasSlashCommandBash = t10; 118: let t11; 119: if ($[11] !== commands) { 120: t11 = commands?.some(_temp4) ?? false; 121: $[11] = commands; 122: $[12] = t11; 123: } else { 124: t11 = $[12]; 125: } 126: const hasSkillsBash = t11; 127: const hasAnyBashExecution = bashSettingSources.length > 0 || hasSlashCommandBash || hasSkillsBash; 128: const hasTrustDialogAccepted = checkHasTrustDialogAccepted(); 129: let t12; 130: let t13; 131: if ($[13] !== hasAnyBashExecution) { 132: t12 = () => { 133: const isHomeDir = homedir() === getCwd(); 134: logEvent("tengu_trust_dialog_shown", { 135: isHomeDir, 136: hasMcpServers, 137: hasHooks, 138: hasBashExecution: hasAnyBashExecution, 139: hasApiKeyHelper, 140: hasAwsCommands, 141: hasGcpCommands, 142: hasOtelHeadersHelper, 143: hasDangerousEnvVars 144: }); 145: }; 146: t13 = [hasMcpServers, hasHooks, hasAnyBashExecution, hasApiKeyHelper, hasAwsCommands, hasGcpCommands, hasOtelHeadersHelper, hasDangerousEnvVars]; 147: $[13] = hasAnyBashExecution; 148: $[14] = t12; 149: $[15] = t13; 150: } else { 151: t12 = $[14]; 152: t13 = $[15]; 153: } 154: React.useEffect(t12, t13); 155: let t14; 156: if ($[16] !== hasAnyBashExecution || $[17] !== onDone) { 157: t14 = function onChange(value) { 158: if (value === "exit") { 159: gracefulShutdownSync(1); 160: return; 161: } 162: const isHomeDir_0 = homedir() === getCwd(); 163: logEvent("tengu_trust_dialog_accept", { 164: isHomeDir: isHomeDir_0, 165: hasMcpServers, 166: hasHooks, 167: hasBashExecution: hasAnyBashExecution, 168: hasApiKeyHelper, 169: hasAwsCommands, 170: hasGcpCommands, 171: hasOtelHeadersHelper, 172: hasDangerousEnvVars 173: }); 174: if (isHomeDir_0) { 175: setSessionTrustAccepted(true); 176: } else { 177: saveCurrentProjectConfig(_temp5); 178: } 179: onDone(); 180: }; 181: $[16] = hasAnyBashExecution; 182: $[17] = onDone; 183: $[18] = t14; 184: } else { 185: t14 = $[18]; 186: } 187: const onChange = t14; 188: const exitState = useExitOnCtrlCDWithKeybindings(_temp6); 189: let t15; 190: if ($[19] === Symbol.for("react.memo_cache_sentinel")) { 191: t15 = { 192: context: "Confirmation" 193: }; 194: $[19] = t15; 195: } else { 196: t15 = $[19]; 197: } 198: useKeybinding("confirm:no", _temp7, t15); 199: if (hasTrustDialogAccepted) { 200: setTimeout(onDone); 201: return null; 202: } 203: let t16; 204: let t17; 205: let t18; 206: if ($[20] === Symbol.for("react.memo_cache_sentinel")) { 207: t16 = <Text bold={true}>{getFsImplementation().cwd()}</Text>; 208: t17 = <Text>Quick safety check: Is this a project you created or one you trust? (Like your own code, a well-known open source project, or work from your team). If not, take a moment to review what{"'"}s in this folder first.</Text>; 209: t18 = <Text>Claude Code{"'"}ll be able to read, edit, and execute files here.</Text>; 210: $[20] = t16; 211: $[21] = t17; 212: $[22] = t18; 213: } else { 214: t16 = $[20]; 215: t17 = $[21]; 216: t18 = $[22]; 217: } 218: let t19; 219: if ($[23] === Symbol.for("react.memo_cache_sentinel")) { 220: t19 = <Text dimColor={true}><Link url="https://code.claude.com/docs/en/security">Security guide</Link></Text>; 221: $[23] = t19; 222: } else { 223: t19 = $[23]; 224: } 225: let t20; 226: if ($[24] === Symbol.for("react.memo_cache_sentinel")) { 227: t20 = [{ 228: label: "Yes, I trust this folder", 229: value: "enable_all" 230: }, { 231: label: "No, exit", 232: value: "exit" 233: }]; 234: $[24] = t20; 235: } else { 236: t20 = $[24]; 237: } 238: let t21; 239: if ($[25] !== onChange) { 240: t21 = <Select options={t20} onChange={value_0 => onChange(value_0 as 'enable_all' | 'exit')} onCancel={() => onChange("exit")} />; 241: $[25] = onChange; 242: $[26] = t21; 243: } else { 244: t21 = $[26]; 245: } 246: let t22; 247: if ($[27] !== exitState.keyName || $[28] !== exitState.pending) { 248: t22 = <Text dimColor={true}>{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <>Enter to confirm · Esc to cancel</>}</Text>; 249: $[27] = exitState.keyName; 250: $[28] = exitState.pending; 251: $[29] = t22; 252: } else { 253: t22 = $[29]; 254: } 255: let t23; 256: if ($[30] !== t21 || $[31] !== t22) { 257: t23 = <PermissionDialog color="warning" titleColor="warning" title="Accessing workspace:"><Box flexDirection="column" gap={1} paddingTop={1}>{t16}{t17}{t18}{t19}{t21}{t22}</Box></PermissionDialog>; 258: $[30] = t21; 259: $[31] = t22; 260: $[32] = t23; 261: } else { 262: t23 = $[32]; 263: } 264: return t23; 265: } 266: function _temp7() { 267: gracefulShutdownSync(0); 268: } 269: function _temp6() { 270: return gracefulShutdownSync(1); 271: } 272: function _temp5(current) { 273: return { 274: ...current, 275: hasTrustDialogAccepted: true 276: }; 277: } 278: function _temp4(command_0) { 279: return command_0.type === "prompt" && (command_0.loadedFrom === "skills" || command_0.loadedFrom === "plugin") && (command_0.source === "projectSettings" || command_0.source === "localSettings" || command_0.source === "plugin") && command_0.allowedTools?.some(_temp3); 280: } 281: function _temp3(tool_0) { 282: return tool_0 === BASH_TOOL_NAME || tool_0.startsWith(BASH_TOOL_NAME + "("); 283: } 284: function _temp2(command) { 285: return command.type === "prompt" && command.loadedFrom === "commands_DEPRECATED" && (command.source === "projectSettings" || command.source === "localSettings") && command.allowedTools?.some(_temp); 286: } 287: function _temp(tool) { 288: return tool === BASH_TOOL_NAME || tool.startsWith(BASH_TOOL_NAME + "("); 289: }

File: src/components/TrustDialog/utils.ts

typescript 1: import type { PermissionRule } from 'src/utils/permissions/PermissionRule.js' 2: import { getSettingsForSource } from 'src/utils/settings/settings.js' 3: import type { SettingsJson } from 'src/utils/settings/types.js' 4: import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js' 5: import { SAFE_ENV_VARS } from '../../utils/managedEnvConstants.js' 6: import { getPermissionRulesForSource } from '../../utils/permissions/permissionsLoader.js' 7: function hasHooks(settings: SettingsJson | null): boolean { 8: if (settings === null || settings.disableAllHooks) { 9: return false 10: } 11: if (settings.statusLine) { 12: return true 13: } 14: if (settings.fileSuggestion) { 15: return true 16: } 17: if (!settings.hooks) { 18: return false 19: } 20: for (const hookConfig of Object.values(settings.hooks)) { 21: if (hookConfig.length > 0) { 22: return true 23: } 24: } 25: return false 26: } 27: export function getHooksSources(): string[] { 28: const sources: string[] = [] 29: const projectSettings = getSettingsForSource('projectSettings') 30: if (hasHooks(projectSettings)) { 31: sources.push('.claude/settings.json') 32: } 33: const localSettings = getSettingsForSource('localSettings') 34: if (hasHooks(localSettings)) { 35: sources.push('.claude/settings.local.json') 36: } 37: return sources 38: } 39: function hasBashPermission(rules: PermissionRule[]): boolean { 40: return rules.some( 41: rule => 42: rule.ruleBehavior === 'allow' && 43: (rule.ruleValue.toolName === BASH_TOOL_NAME || 44: rule.ruleValue.toolName.startsWith(BASH_TOOL_NAME + '(')), 45: ) 46: } 47: export function getBashPermissionSources(): string[] { 48: const sources: string[] = [] 49: const projectRules = getPermissionRulesForSource('projectSettings') 50: if (hasBashPermission(projectRules)) { 51: sources.push('.claude/settings.json') 52: } 53: const localRules = getPermissionRulesForSource('localSettings') 54: if (hasBashPermission(localRules)) { 55: sources.push('.claude/settings.local.json') 56: } 57: return sources 58: } 59: export function formatListWithAnd(items: string[], limit?: number): string { 60: if (items.length === 0) return '' 61: // Ignore limit if it's 0 62: const effectiveLimit = limit === 0 ? undefined : limit 63: if (!effectiveLimit || items.length <= effectiveLimit) { 64: if (items.length === 1) return items[0]! 65: if (items.length === 2) return `${items[0]} and ${items[1]}` 66: const lastItem = items[items.length - 1]! 67: const allButLast = items.slice(0, -1) 68: return `${allButLast.join(', ')}, and ${lastItem}` 69: } 70: const shown = items.slice(0, effectiveLimit) 71: const remaining = items.length - effectiveLimit 72: if (shown.length === 1) { 73: return `${shown[0]} and ${remaining} more` 74: } 75: return `${shown.join(', ')}, and ${remaining} more` 76: } 77: function hasOtelHeadersHelper(settings: SettingsJson | null): boolean { 78: return !!settings?.otelHeadersHelper 79: } 80: export function getOtelHeadersHelperSources(): string[] { 81: const sources: string[] = [] 82: const projectSettings = getSettingsForSource('projectSettings') 83: if (hasOtelHeadersHelper(projectSettings)) { 84: sources.push('.claude/settings.json') 85: } 86: const localSettings = getSettingsForSource('localSettings') 87: if (hasOtelHeadersHelper(localSettings)) { 88: sources.push('.claude/settings.local.json') 89: } 90: return sources 91: } 92: function hasApiKeyHelper(settings: SettingsJson | null): boolean { 93: return !!settings?.apiKeyHelper 94: } 95: export function getApiKeyHelperSources(): string[] { 96: const sources: string[] = [] 97: const projectSettings = getSettingsForSource('projectSettings') 98: if (hasApiKeyHelper(projectSettings)) { 99: sources.push('.claude/settings.json') 100: } 101: const localSettings = getSettingsForSource('localSettings') 102: if (hasApiKeyHelper(localSettings)) { 103: sources.push('.claude/settings.local.json') 104: } 105: return sources 106: } 107: function hasAwsCommands(settings: SettingsJson | null): boolean { 108: return !!(settings?.awsAuthRefresh || settings?.awsCredentialExport) 109: } 110: export function getAwsCommandsSources(): string[] { 111: const sources: string[] = [] 112: const projectSettings = getSettingsForSource('projectSettings') 113: if (hasAwsCommands(projectSettings)) { 114: sources.push('.claude/settings.json') 115: } 116: const localSettings = getSettingsForSource('localSettings') 117: if (hasAwsCommands(localSettings)) { 118: sources.push('.claude/settings.local.json') 119: } 120: return sources 121: } 122: function hasGcpCommands(settings: SettingsJson | null): boolean { 123: return !!settings?.gcpAuthRefresh 124: } 125: export function getGcpCommandsSources(): string[] { 126: const sources: string[] = [] 127: const projectSettings = getSettingsForSource('projectSettings') 128: if (hasGcpCommands(projectSettings)) { 129: sources.push('.claude/settings.json') 130: } 131: const localSettings = getSettingsForSource('localSettings') 132: if (hasGcpCommands(localSettings)) { 133: sources.push('.claude/settings.local.json') 134: } 135: return sources 136: } 137: function hasDangerousEnvVars(settings: SettingsJson | null): boolean { 138: if (!settings?.env) { 139: return false 140: } 141: return Object.keys(settings.env).some( 142: key => !SAFE_ENV_VARS.has(key.toUpperCase()), 143: ) 144: } 145: export function getDangerousEnvVarsSources(): string[] { 146: const sources: string[] = [] 147: const projectSettings = getSettingsForSource('projectSettings') 148: if (hasDangerousEnvVars(projectSettings)) { 149: sources.push('.claude/settings.json') 150: } 151: const localSettings = getSettingsForSource('localSettings') 152: if (hasDangerousEnvVars(localSettings)) { 153: sources.push('.claude/settings.local.json') 154: } 155: return sources 156: }

File: src/components/ui/OrderedList.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { createContext, isValidElement, type ReactNode, useContext } from 'react'; 3: import { Box } from '../../ink.js'; 4: import { OrderedListItem, OrderedListItemContext } from './OrderedListItem.js'; 5: const OrderedListContext = createContext({ 6: marker: '' 7: }); 8: type OrderedListProps = { 9: children: ReactNode; 10: }; 11: function OrderedListComponent(t0) { 12: const $ = _c(9); 13: const { 14: children 15: } = t0; 16: const { 17: marker: parentMarker 18: } = useContext(OrderedListContext); 19: let numberOfItems = 0; 20: for (const child of React.Children.toArray(children)) { 21: if (!isValidElement(child) || child.type !== OrderedListItem) { 22: continue; 23: } 24: numberOfItems++; 25: } 26: const maxMarkerWidth = String(numberOfItems).length; 27: let t1; 28: if ($[0] !== children || $[1] !== maxMarkerWidth || $[2] !== parentMarker) { 29: let t2; 30: if ($[4] !== maxMarkerWidth || $[5] !== parentMarker) { 31: t2 = (child_0, index) => { 32: if (!isValidElement(child_0) || child_0.type !== OrderedListItem) { 33: return child_0; 34: } 35: const paddedMarker = `${String(index + 1).padStart(maxMarkerWidth)}.`; 36: const marker = `${parentMarker}${paddedMarker}`; 37: return <OrderedListContext.Provider value={{ 38: marker 39: }}><OrderedListItemContext.Provider value={{ 40: marker 41: }}>{child_0}</OrderedListItemContext.Provider></OrderedListContext.Provider>; 42: }; 43: $[4] = maxMarkerWidth; 44: $[5] = parentMarker; 45: $[6] = t2; 46: } else { 47: t2 = $[6]; 48: } 49: t1 = React.Children.map(children, t2); 50: $[0] = children; 51: $[1] = maxMarkerWidth; 52: $[2] = parentMarker; 53: $[3] = t1; 54: } else { 55: t1 = $[3]; 56: } 57: let t2; 58: if ($[7] !== t1) { 59: t2 = <Box flexDirection="column">{t1}</Box>; 60: $[7] = t1; 61: $[8] = t2; 62: } else { 63: t2 = $[8]; 64: } 65: return t2; 66: } 67: OrderedListComponent.Item = OrderedListItem; 68: export const OrderedList = OrderedListComponent;

File: src/components/ui/OrderedListItem.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { createContext, type ReactNode, useContext } from 'react'; 3: import { Box, Text } from '../../ink.js'; 4: export const OrderedListItemContext = createContext({ 5: marker: '' 6: }); 7: type OrderedListItemProps = { 8: children: ReactNode; 9: }; 10: export function OrderedListItem(t0) { 11: const $ = _c(7); 12: const { 13: children 14: } = t0; 15: const { 16: marker 17: } = useContext(OrderedListItemContext); 18: let t1; 19: if ($[0] !== marker) { 20: t1 = <Text dimColor={true}>{marker}</Text>; 21: $[0] = marker; 22: $[1] = t1; 23: } else { 24: t1 = $[1]; 25: } 26: let t2; 27: if ($[2] !== children) { 28: t2 = <Box flexDirection="column">{children}</Box>; 29: $[2] = children; 30: $[3] = t2; 31: } else { 32: t2 = $[3]; 33: } 34: let t3; 35: if ($[4] !== t1 || $[5] !== t2) { 36: t3 = <Box gap={1}>{t1}{t2}</Box>; 37: $[4] = t1; 38: $[5] = t2; 39: $[6] = t3; 40: } else { 41: t3 = $[6]; 42: } 43: return t3; 44: }

File: src/components/ui/TreeSelect.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; 4: import { Box } from '../../ink.js'; 5: import { type OptionWithDescription, Select } from '../CustomSelect/select.js'; 6: export type TreeNode<T> = { 7: id: string | number; 8: value: T; 9: label: string; 10: description?: string; 11: dimDescription?: boolean; 12: children?: TreeNode<T>[]; 13: metadata?: Record<string, unknown>; 14: }; 15: type FlattenedNode<T> = { 16: node: TreeNode<T>; 17: depth: number; 18: isExpanded: boolean; 19: hasChildren: boolean; 20: parentId?: string | number; 21: }; 22: export type TreeSelectProps<T> = { 23: readonly nodes: TreeNode<T>[]; 24: readonly onSelect: (node: TreeNode<T>) => void; 25: readonly onCancel?: () => void; 26: readonly onFocus?: (node: TreeNode<T>) => void; 27: readonly focusNodeId?: string | number; 28: readonly visibleOptionCount?: number; 29: readonly layout?: 'compact' | 'expanded' | 'compact-vertical'; 30: readonly isDisabled?: boolean; 31: readonly hideIndexes?: boolean; 32: readonly isNodeExpanded?: (nodeId: string | number) => boolean; 33: readonly onExpand?: (nodeId: string | number) => void; 34: readonly onCollapse?: (nodeId: string | number) => void; 35: readonly getParentPrefix?: (isExpanded: boolean) => string; 36: readonly getChildPrefix?: (depth: number) => string; 37: readonly onUpFromFirstItem?: () => void; 38: }; 39: export function TreeSelect(t0) { 40: const $ = _c(48); 41: const { 42: nodes, 43: onSelect, 44: onCancel, 45: onFocus, 46: focusNodeId, 47: visibleOptionCount, 48: layout: t1, 49: isDisabled: t2, 50: hideIndexes: t3, 51: isNodeExpanded, 52: onExpand, 53: onCollapse, 54: getParentPrefix, 55: getChildPrefix, 56: onUpFromFirstItem 57: } = t0; 58: const layout = t1 === undefined ? "expanded" : t1; 59: const isDisabled = t2 === undefined ? false : t2; 60: const hideIndexes = t3 === undefined ? false : t3; 61: let t4; 62: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 63: t4 = new Set(); 64: $[0] = t4; 65: } else { 66: t4 = $[0]; 67: } 68: const [internalExpandedIds, setInternalExpandedIds] = React.useState(t4); 69: const isProgrammaticFocusRef = React.useRef(false); 70: const lastFocusedIdRef = React.useRef(null); 71: let t5; 72: if ($[1] !== internalExpandedIds || $[2] !== isNodeExpanded) { 73: t5 = nodeId => { 74: if (isNodeExpanded) { 75: return isNodeExpanded(nodeId); 76: } 77: return internalExpandedIds.has(nodeId); 78: }; 79: $[1] = internalExpandedIds; 80: $[2] = isNodeExpanded; 81: $[3] = t5; 82: } else { 83: t5 = $[3]; 84: } 85: const isExpanded = t5; 86: let result; 87: if ($[4] !== isExpanded || $[5] !== nodes) { 88: result = []; 89: function traverse(node, depth, parentId) { 90: const hasChildren = !!node.children && node.children.length > 0; 91: const nodeIsExpanded = isExpanded(node.id); 92: result.push({ 93: node, 94: depth, 95: isExpanded: nodeIsExpanded, 96: hasChildren, 97: parentId 98: }); 99: if (hasChildren && nodeIsExpanded && node.children) { 100: for (const child of node.children) { 101: traverse(child, depth + 1, node.id); 102: } 103: } 104: } 105: for (const node_0 of nodes) { 106: traverse(node_0, 0); 107: } 108: $[4] = isExpanded; 109: $[5] = nodes; 110: $[6] = result; 111: } else { 112: result = $[6]; 113: } 114: const flattenedNodes = result; 115: const defaultGetParentPrefix = _temp; 116: const defaultGetChildPrefix = _temp2; 117: const parentPrefixFn = getParentPrefix ?? defaultGetParentPrefix; 118: const childPrefixFn = getChildPrefix ?? defaultGetChildPrefix; 119: let t6; 120: if ($[7] !== childPrefixFn || $[8] !== parentPrefixFn) { 121: t6 = flatNode => { 122: let prefix = ""; 123: if (flatNode.hasChildren) { 124: prefix = parentPrefixFn(flatNode.isExpanded); 125: } else { 126: if (flatNode.depth > 0) { 127: prefix = childPrefixFn(flatNode.depth); 128: } 129: } 130: return prefix + flatNode.node.label; 131: }; 132: $[7] = childPrefixFn; 133: $[8] = parentPrefixFn; 134: $[9] = t6; 135: } else { 136: t6 = $[9]; 137: } 138: const buildLabel = t6; 139: let t7; 140: if ($[10] !== buildLabel || $[11] !== flattenedNodes) { 141: t7 = flattenedNodes.map(flatNode_0 => ({ 142: label: buildLabel(flatNode_0), 143: description: flatNode_0.node.description, 144: dimDescription: flatNode_0.node.dimDescription ?? true, 145: value: flatNode_0.node.id 146: })); 147: $[10] = buildLabel; 148: $[11] = flattenedNodes; 149: $[12] = t7; 150: } else { 151: t7 = $[12]; 152: } 153: const options = t7; 154: let map; 155: if ($[13] !== flattenedNodes) { 156: map = new Map(); 157: flattenedNodes.forEach(fn => map.set(fn.node.id, fn.node)); 158: $[13] = flattenedNodes; 159: $[14] = map; 160: } else { 161: map = $[14]; 162: } 163: const nodeMap = map; 164: let t8; 165: if ($[15] !== flattenedNodes) { 166: t8 = nodeId_0 => flattenedNodes.find(fn_0 => fn_0.node.id === nodeId_0); 167: $[15] = flattenedNodes; 168: $[16] = t8; 169: } else { 170: t8 = $[16]; 171: } 172: const findFlattenedNode = t8; 173: let t9; 174: if ($[17] !== findFlattenedNode || $[18] !== onCollapse || $[19] !== onExpand) { 175: t9 = (nodeId_1, shouldExpand) => { 176: const flatNode_1 = findFlattenedNode(nodeId_1); 177: if (!flatNode_1 || !flatNode_1.hasChildren) { 178: return; 179: } 180: if (shouldExpand) { 181: if (onExpand) { 182: onExpand(nodeId_1); 183: } else { 184: setInternalExpandedIds(prev => new Set(prev).add(nodeId_1)); 185: } 186: } else { 187: if (onCollapse) { 188: onCollapse(nodeId_1); 189: } else { 190: setInternalExpandedIds(prev_0 => { 191: const newSet = new Set(prev_0); 192: newSet.delete(nodeId_1); 193: return newSet; 194: }); 195: } 196: } 197: }; 198: $[17] = findFlattenedNode; 199: $[18] = onCollapse; 200: $[19] = onExpand; 201: $[20] = t9; 202: } else { 203: t9 = $[20]; 204: } 205: const toggleExpand = t9; 206: let t10; 207: if ($[21] !== findFlattenedNode || $[22] !== focusNodeId || $[23] !== isDisabled || $[24] !== nodeMap || $[25] !== onFocus || $[26] !== toggleExpand) { 208: t10 = e => { 209: if (!focusNodeId || isDisabled) { 210: return; 211: } 212: const flatNode_2 = findFlattenedNode(focusNodeId); 213: if (!flatNode_2) { 214: return; 215: } 216: if (e.key === "right" && flatNode_2.hasChildren) { 217: e.preventDefault(); 218: toggleExpand(focusNodeId, true); 219: } else { 220: if (e.key === "left") { 221: if (flatNode_2.hasChildren && flatNode_2.isExpanded) { 222: e.preventDefault(); 223: toggleExpand(focusNodeId, false); 224: } else { 225: if (flatNode_2.parentId !== undefined) { 226: e.preventDefault(); 227: isProgrammaticFocusRef.current = true; 228: toggleExpand(flatNode_2.parentId, false); 229: if (onFocus) { 230: const parentNode = nodeMap.get(flatNode_2.parentId); 231: if (parentNode) { 232: onFocus(parentNode); 233: } 234: } 235: } 236: } 237: } 238: } 239: }; 240: $[21] = findFlattenedNode; 241: $[22] = focusNodeId; 242: $[23] = isDisabled; 243: $[24] = nodeMap; 244: $[25] = onFocus; 245: $[26] = toggleExpand; 246: $[27] = t10; 247: } else { 248: t10 = $[27]; 249: } 250: const handleKeyDown = t10; 251: let t11; 252: if ($[28] !== nodeMap || $[29] !== onSelect) { 253: t11 = nodeId_2 => { 254: const node_1 = nodeMap.get(nodeId_2); 255: if (!node_1) { 256: return; 257: } 258: onSelect(node_1); 259: }; 260: $[28] = nodeMap; 261: $[29] = onSelect; 262: $[30] = t11; 263: } else { 264: t11 = $[30]; 265: } 266: const handleChange = t11; 267: let t12; 268: if ($[31] !== nodeMap || $[32] !== onFocus) { 269: t12 = nodeId_3 => { 270: if (isProgrammaticFocusRef.current) { 271: isProgrammaticFocusRef.current = false; 272: return; 273: } 274: if (lastFocusedIdRef.current === nodeId_3) { 275: return; 276: } 277: lastFocusedIdRef.current = nodeId_3; 278: if (onFocus) { 279: const node_2 = nodeMap.get(nodeId_3); 280: if (node_2) { 281: onFocus(node_2); 282: } 283: } 284: }; 285: $[31] = nodeMap; 286: $[32] = onFocus; 287: $[33] = t12; 288: } else { 289: t12 = $[33]; 290: } 291: const handleFocus = t12; 292: let t13; 293: if ($[34] !== focusNodeId || $[35] !== handleChange || $[36] !== handleFocus || $[37] !== hideIndexes || $[38] !== isDisabled || $[39] !== layout || $[40] !== onCancel || $[41] !== onUpFromFirstItem || $[42] !== options || $[43] !== visibleOptionCount) { 294: t13 = <Select options={options} onChange={handleChange} onFocus={handleFocus} onCancel={onCancel} defaultFocusValue={focusNodeId} visibleOptionCount={visibleOptionCount} layout={layout} isDisabled={isDisabled} hideIndexes={hideIndexes} onUpFromFirstItem={onUpFromFirstItem} />; 295: $[34] = focusNodeId; 296: $[35] = handleChange; 297: $[36] = handleFocus; 298: $[37] = hideIndexes; 299: $[38] = isDisabled; 300: $[39] = layout; 301: $[40] = onCancel; 302: $[41] = onUpFromFirstItem; 303: $[42] = options; 304: $[43] = visibleOptionCount; 305: $[44] = t13; 306: } else { 307: t13 = $[44]; 308: } 309: let t14; 310: if ($[45] !== handleKeyDown || $[46] !== t13) { 311: t14 = <Box tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t13}</Box>; 312: $[45] = handleKeyDown; 313: $[46] = t13; 314: $[47] = t14; 315: } else { 316: t14 = $[47]; 317: } 318: return t14; 319: } 320: function _temp2(_depth) { 321: return " \u25B8 "; 322: } 323: function _temp(isExpanded_0) { 324: return isExpanded_0 ? "\u25BC " : "\u25B6 "; 325: }

File: src/components/wizard/index.ts

typescript 1: export type { 2: WizardContextValue, 3: WizardProviderProps, 4: WizardStepComponent, 5: } from './types.js' 6: export { useWizard } from './useWizard.js' 7: export { WizardDialogLayout } from './WizardDialogLayout.js' 8: export { WizardNavigationFooter } from './WizardNavigationFooter.js' 9: export { WizardProvider } from './WizardProvider.js'

File: src/components/wizard/useWizard.ts

typescript 1: import { useContext } from 'react' 2: import type { WizardContextValue } from './types.js' 3: import { WizardContext } from './WizardProvider.js' 4: export function useWizard< 5: T extends Record<string, unknown> = Record<string, unknown>, 6: >(): WizardContextValue<T> { 7: const context = useContext(WizardContext) as WizardContextValue<T> | null 8: if (!context) { 9: throw new Error('useWizard must be used within a WizardProvider') 10: } 11: return context 12: }

File: src/components/wizard/WizardDialogLayout.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { type ReactNode } from 'react'; 3: import type { Theme } from '../../utils/theme.js'; 4: import { Dialog } from '../design-system/Dialog.js'; 5: import { useWizard } from './useWizard.js'; 6: import { WizardNavigationFooter } from './WizardNavigationFooter.js'; 7: type Props = { 8: title?: string; 9: color?: keyof Theme; 10: children: ReactNode; 11: subtitle?: string; 12: footerText?: ReactNode; 13: }; 14: export function WizardDialogLayout(t0) { 15: const $ = _c(11); 16: const { 17: title: titleOverride, 18: color: t1, 19: children, 20: subtitle, 21: footerText 22: } = t0; 23: const color = t1 === undefined ? "suggestion" : t1; 24: const { 25: currentStepIndex, 26: totalSteps, 27: title: providerTitle, 28: showStepCounter, 29: goBack 30: } = useWizard(); 31: const title = titleOverride || providerTitle || "Wizard"; 32: const stepSuffix = showStepCounter !== false ? ` (${currentStepIndex + 1}/${totalSteps})` : ""; 33: const t2 = `${title}${stepSuffix}`; 34: let t3; 35: if ($[0] !== children || $[1] !== color || $[2] !== goBack || $[3] !== subtitle || $[4] !== t2) { 36: t3 = <Dialog title={t2} subtitle={subtitle} onCancel={goBack} color={color} hideInputGuide={true} isCancelActive={false}>{children}</Dialog>; 37: $[0] = children; 38: $[1] = color; 39: $[2] = goBack; 40: $[3] = subtitle; 41: $[4] = t2; 42: $[5] = t3; 43: } else { 44: t3 = $[5]; 45: } 46: let t4; 47: if ($[6] !== footerText) { 48: t4 = <WizardNavigationFooter instructions={footerText} />; 49: $[6] = footerText; 50: $[7] = t4; 51: } else { 52: t4 = $[7]; 53: } 54: let t5; 55: if ($[8] !== t3 || $[9] !== t4) { 56: t5 = <>{t3}{t4}</>; 57: $[8] = t3; 58: $[9] = t4; 59: $[10] = t5; 60: } else { 61: t5 = $[10]; 62: } 63: return t5; 64: }

File: src/components/wizard/WizardNavigationFooter.tsx

typescript 1: import React, { type ReactNode } from 'react'; 2: import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 3: import { Box, Text } from '../../ink.js'; 4: import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; 5: import { Byline } from '../design-system/Byline.js'; 6: import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; 7: type Props = { 8: instructions?: ReactNode; 9: }; 10: export function WizardNavigationFooter({ 11: instructions = <Byline> 12: <KeyboardShortcutHint shortcut="↑↓" action="navigate" /> 13: <KeyboardShortcutHint shortcut="Enter" action="select" /> 14: <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /> 15: </Byline> 16: }: Props): ReactNode { 17: const exitState = useExitOnCtrlCDWithKeybindings(); 18: return <Box marginLeft={3} marginTop={1}> 19: <Text dimColor> 20: {exitState.pending ? `Press ${exitState.keyName} again to exit` : instructions} 21: </Text> 22: </Box>; 23: }

File: src/components/wizard/WizardProvider.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { createContext, type ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; 3: import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; 4: import type { WizardContextValue, WizardProviderProps } from './types.js'; 5: export const WizardContext = createContext<WizardContextValue<any> | null>(null); 6: export function WizardProvider(t0) { 7: const $ = _c(38); 8: const { 9: steps, 10: initialData: t1, 11: onComplete, 12: onCancel, 13: children, 14: title, 15: showStepCounter: t2 16: } = t0; 17: let t3; 18: if ($[0] !== t1) { 19: t3 = t1 === undefined ? {} as T : t1; 20: $[0] = t1; 21: $[1] = t3; 22: } else { 23: t3 = $[1]; 24: } 25: const initialData = t3; 26: const showStepCounter = t2 === undefined ? true : t2; 27: const [currentStepIndex, setCurrentStepIndex] = useState(0); 28: const [wizardData, setWizardData] = useState(initialData); 29: const [isCompleted, setIsCompleted] = useState(false); 30: let t4; 31: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 32: t4 = []; 33: $[2] = t4; 34: } else { 35: t4 = $[2]; 36: } 37: const [navigationHistory, setNavigationHistory] = useState(t4); 38: useExitOnCtrlCDWithKeybindings(); 39: let t5; 40: let t6; 41: if ($[3] !== isCompleted || $[4] !== onComplete || $[5] !== wizardData) { 42: t5 = () => { 43: if (isCompleted) { 44: setNavigationHistory([]); 45: onComplete(wizardData); 46: } 47: }; 48: t6 = [isCompleted, wizardData, onComplete]; 49: $[3] = isCompleted; 50: $[4] = onComplete; 51: $[5] = wizardData; 52: $[6] = t5; 53: $[7] = t6; 54: } else { 55: t5 = $[6]; 56: t6 = $[7]; 57: } 58: useEffect(t5, t6); 59: let t7; 60: if ($[8] !== currentStepIndex || $[9] !== navigationHistory || $[10] !== steps.length) { 61: t7 = () => { 62: if (currentStepIndex < steps.length - 1) { 63: if (navigationHistory.length > 0) { 64: setNavigationHistory(prev => [...prev, currentStepIndex]); 65: } 66: setCurrentStepIndex(_temp); 67: } else { 68: setIsCompleted(true); 69: } 70: }; 71: $[8] = currentStepIndex; 72: $[9] = navigationHistory; 73: $[10] = steps.length; 74: $[11] = t7; 75: } else { 76: t7 = $[11]; 77: } 78: const goNext = t7; 79: let t8; 80: if ($[12] !== currentStepIndex || $[13] !== navigationHistory || $[14] !== onCancel) { 81: t8 = () => { 82: if (navigationHistory.length > 0) { 83: const previousStep = navigationHistory[navigationHistory.length - 1]; 84: if (previousStep !== undefined) { 85: setNavigationHistory(_temp2); 86: setCurrentStepIndex(previousStep); 87: } 88: } else { 89: if (currentStepIndex > 0) { 90: setCurrentStepIndex(_temp3); 91: } else { 92: if (onCancel) { 93: onCancel(); 94: } 95: } 96: } 97: }; 98: $[12] = currentStepIndex; 99: $[13] = navigationHistory; 100: $[14] = onCancel; 101: $[15] = t8; 102: } else { 103: t8 = $[15]; 104: } 105: const goBack = t8; 106: let t9; 107: if ($[16] !== currentStepIndex || $[17] !== steps.length) { 108: t9 = index => { 109: if (index >= 0 && index < steps.length) { 110: setNavigationHistory(prev_3 => [...prev_3, currentStepIndex]); 111: setCurrentStepIndex(index); 112: } 113: }; 114: $[16] = currentStepIndex; 115: $[17] = steps.length; 116: $[18] = t9; 117: } else { 118: t9 = $[18]; 119: } 120: const goToStep = t9; 121: let t10; 122: if ($[19] !== onCancel) { 123: t10 = () => { 124: setNavigationHistory([]); 125: if (onCancel) { 126: onCancel(); 127: } 128: }; 129: $[19] = onCancel; 130: $[20] = t10; 131: } else { 132: t10 = $[20]; 133: } 134: const cancel = t10; 135: let t11; 136: if ($[21] === Symbol.for("react.memo_cache_sentinel")) { 137: t11 = updates => { 138: setWizardData(prev_4 => ({ 139: ...prev_4, 140: ...updates 141: })); 142: }; 143: $[21] = t11; 144: } else { 145: t11 = $[21]; 146: } 147: const updateWizardData = t11; 148: let t12; 149: if ($[22] !== cancel || $[23] !== currentStepIndex || $[24] !== goBack || $[25] !== goNext || $[26] !== goToStep || $[27] !== showStepCounter || $[28] !== steps.length || $[29] !== title || $[30] !== wizardData) { 150: t12 = { 151: currentStepIndex, 152: totalSteps: steps.length, 153: wizardData, 154: setWizardData, 155: updateWizardData, 156: goNext, 157: goBack, 158: goToStep, 159: cancel, 160: title, 161: showStepCounter 162: }; 163: $[22] = cancel; 164: $[23] = currentStepIndex; 165: $[24] = goBack; 166: $[25] = goNext; 167: $[26] = goToStep; 168: $[27] = showStepCounter; 169: $[28] = steps.length; 170: $[29] = title; 171: $[30] = wizardData; 172: $[31] = t12; 173: } else { 174: t12 = $[31]; 175: } 176: const contextValue = t12; 177: const CurrentStepComponent = steps[currentStepIndex]; 178: if (!CurrentStepComponent || isCompleted) { 179: return null; 180: } 181: let t13; 182: if ($[32] !== CurrentStepComponent || $[33] !== children) { 183: t13 = children || <CurrentStepComponent />; 184: $[32] = CurrentStepComponent; 185: $[33] = children; 186: $[34] = t13; 187: } else { 188: t13 = $[34]; 189: } 190: let t14; 191: if ($[35] !== contextValue || $[36] !== t13) { 192: t14 = <WizardContext.Provider value={contextValue}>{t13}</WizardContext.Provider>; 193: $[35] = contextValue; 194: $[36] = t13; 195: $[37] = t14; 196: } else { 197: t14 = $[37]; 198: } 199: return t14; 200: } 201: function _temp3(prev_2) { 202: return prev_2 - 1; 203: } 204: function _temp2(prev_1) { 205: return prev_1.slice(0, -1); 206: } 207: function _temp(prev_0) { 208: return prev_0 + 1; 209: }

File: src/components/AgentProgressLine.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { Box, Text } from '../ink.js'; 4: import { formatNumber } from '../utils/format.js'; 5: import type { Theme } from '../utils/theme.js'; 6: type Props = { 7: agentType: string; 8: description?: string; 9: name?: string; 10: descriptionColor?: keyof Theme; 11: taskDescription?: string; 12: toolUseCount: number; 13: tokens: number | null; 14: color?: keyof Theme; 15: isLast: boolean; 16: isResolved: boolean; 17: isError: boolean; 18: isAsync?: boolean; 19: shouldAnimate: boolean; 20: lastToolInfo?: string | null; 21: hideType?: boolean; 22: }; 23: export function AgentProgressLine(t0) { 24: const $ = _c(32); 25: const { 26: agentType, 27: description, 28: name, 29: descriptionColor, 30: taskDescription, 31: toolUseCount, 32: tokens, 33: color, 34: isLast, 35: isResolved, 36: isAsync: t1, 37: lastToolInfo, 38: hideType: t2 39: } = t0; 40: const isAsync = t1 === undefined ? false : t1; 41: const hideType = t2 === undefined ? false : t2; 42: const treeChar = isLast ? "\u2514\u2500" : "\u251C\u2500"; 43: const isBackgrounded = isAsync && isResolved; 44: let t3; 45: if ($[0] !== isBackgrounded || $[1] !== isResolved || $[2] !== lastToolInfo || $[3] !== taskDescription) { 46: t3 = () => { 47: if (!isResolved) { 48: return lastToolInfo || "Initializing\u2026"; 49: } 50: if (isBackgrounded) { 51: return taskDescription ?? "Running in the background"; 52: } 53: return "Done"; 54: }; 55: $[0] = isBackgrounded; 56: $[1] = isResolved; 57: $[2] = lastToolInfo; 58: $[3] = taskDescription; 59: $[4] = t3; 60: } else { 61: t3 = $[4]; 62: } 63: const getStatusText = t3; 64: let t4; 65: if ($[5] !== treeChar) { 66: t4 = <Text dimColor={true}>{treeChar} </Text>; 67: $[5] = treeChar; 68: $[6] = t4; 69: } else { 70: t4 = $[6]; 71: } 72: const t5 = !isResolved; 73: let t6; 74: if ($[7] !== agentType || $[8] !== color || $[9] !== description || $[10] !== descriptionColor || $[11] !== hideType || $[12] !== name) { 75: t6 = hideType ? <><Text bold={true}>{name ?? description ?? agentType}</Text>{name && description && <Text dimColor={true}>: {description}</Text>}</> : <><Text bold={true} backgroundColor={color} color={color ? "inverseText" : undefined}>{agentType}</Text>{description && <>{" ("}<Text backgroundColor={descriptionColor} color={descriptionColor ? "inverseText" : undefined}>{description}</Text>{")"}</>}</>; 76: $[7] = agentType; 77: $[8] = color; 78: $[9] = description; 79: $[10] = descriptionColor; 80: $[11] = hideType; 81: $[12] = name; 82: $[13] = t6; 83: } else { 84: t6 = $[13]; 85: } 86: let t7; 87: if ($[14] !== isBackgrounded || $[15] !== tokens || $[16] !== toolUseCount) { 88: t7 = !isBackgrounded && <>{" \xB7 "}{toolUseCount} tool {toolUseCount === 1 ? "use" : "uses"}{tokens !== null && <> · {formatNumber(tokens)} tokens</>}</>; 89: $[14] = isBackgrounded; 90: $[15] = tokens; 91: $[16] = toolUseCount; 92: $[17] = t7; 93: } else { 94: t7 = $[17]; 95: } 96: let t8; 97: if ($[18] !== t5 || $[19] !== t6 || $[20] !== t7) { 98: t8 = <Text dimColor={t5}>{t6}{t7}</Text>; 99: $[18] = t5; 100: $[19] = t6; 101: $[20] = t7; 102: $[21] = t8; 103: } else { 104: t8 = $[21]; 105: } 106: let t9; 107: if ($[22] !== t4 || $[23] !== t8) { 108: t9 = <Box paddingLeft={3}>{t4}{t8}</Box>; 109: $[22] = t4; 110: $[23] = t8; 111: $[24] = t9; 112: } else { 113: t9 = $[24]; 114: } 115: let t10; 116: if ($[25] !== getStatusText || $[26] !== isBackgrounded || $[27] !== isLast) { 117: t10 = !isBackgrounded && <Box paddingLeft={3} flexDirection="row"><Text dimColor={true}>{isLast ? " \u23BF " : "\u2502 \u23BF "}</Text><Text dimColor={true}>{getStatusText()}</Text></Box>; 118: $[25] = getStatusText; 119: $[26] = isBackgrounded; 120: $[27] = isLast; 121: $[28] = t10; 122: } else { 123: t10 = $[28]; 124: } 125: let t11; 126: if ($[29] !== t10 || $[30] !== t9) { 127: t11 = <Box flexDirection="column">{t9}{t10}</Box>; 128: $[29] = t10; 129: $[30] = t9; 130: $[31] = t11; 131: } else { 132: t11 = $[31]; 133: } 134: return t11; 135: }

File: src/components/App.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { FpsMetricsProvider } from '../context/fpsMetrics.js'; 4: import { StatsProvider, type StatsStore } from '../context/stats.js'; 5: import { type AppState, AppStateProvider } from '../state/AppState.js'; 6: import { onChangeAppState } from '../state/onChangeAppState.js'; 7: import type { FpsMetrics } from '../utils/fpsTracker.js'; 8: type Props = { 9: getFpsMetrics: () => FpsMetrics | undefined; 10: stats?: StatsStore; 11: initialState: AppState; 12: children: React.ReactNode; 13: }; 14: export function App(t0) { 15: const $ = _c(9); 16: const { 17: getFpsMetrics, 18: stats, 19: initialState, 20: children 21: } = t0; 22: let t1; 23: if ($[0] !== children || $[1] !== initialState) { 24: t1 = <AppStateProvider initialState={initialState} onChangeAppState={onChangeAppState}>{children}</AppStateProvider>; 25: $[0] = children; 26: $[1] = initialState; 27: $[2] = t1; 28: } else { 29: t1 = $[2]; 30: } 31: let t2; 32: if ($[3] !== stats || $[4] !== t1) { 33: t2 = <StatsProvider store={stats}>{t1}</StatsProvider>; 34: $[3] = stats; 35: $[4] = t1; 36: $[5] = t2; 37: } else { 38: t2 = $[5]; 39: } 40: let t3; 41: if ($[6] !== getFpsMetrics || $[7] !== t2) { 42: t3 = <FpsMetricsProvider getFpsMetrics={getFpsMetrics}>{t2}</FpsMetricsProvider>; 43: $[6] = getFpsMetrics; 44: $[7] = t2; 45: $[8] = t3; 46: } else { 47: t3 = $[8]; 48: } 49: return t3; 50: }

File: src/components/ApproveApiKey.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Text } from '../ink.js'; 4: import { saveGlobalConfig } from '../utils/config.js'; 5: import { Select } from './CustomSelect/index.js'; 6: import { Dialog } from './design-system/Dialog.js'; 7: type Props = { 8: customApiKeyTruncated: string; 9: onDone(approved: boolean): void; 10: }; 11: export function ApproveApiKey(t0) { 12: const $ = _c(17); 13: const { 14: customApiKeyTruncated, 15: onDone 16: } = t0; 17: let t1; 18: if ($[0] !== customApiKeyTruncated || $[1] !== onDone) { 19: t1 = function onChange(value) { 20: bb2: switch (value) { 21: case "yes": 22: { 23: saveGlobalConfig(current_0 => ({ 24: ...current_0, 25: customApiKeyResponses: { 26: ...current_0.customApiKeyResponses, 27: approved: [...(current_0.customApiKeyResponses?.approved ?? []), customApiKeyTruncated] 28: } 29: })); 30: onDone(true); 31: break bb2; 32: } 33: case "no": 34: { 35: saveGlobalConfig(current => ({ 36: ...current, 37: customApiKeyResponses: { 38: ...current.customApiKeyResponses, 39: rejected: [...(current.customApiKeyResponses?.rejected ?? []), customApiKeyTruncated] 40: } 41: })); 42: onDone(false); 43: } 44: } 45: }; 46: $[0] = customApiKeyTruncated; 47: $[1] = onDone; 48: $[2] = t1; 49: } else { 50: t1 = $[2]; 51: } 52: const onChange = t1; 53: let t2; 54: if ($[3] !== onChange) { 55: t2 = () => onChange("no"); 56: $[3] = onChange; 57: $[4] = t2; 58: } else { 59: t2 = $[4]; 60: } 61: let t3; 62: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 63: t3 = <Text bold={true}>ANTHROPIC_API_KEY</Text>; 64: $[5] = t3; 65: } else { 66: t3 = $[5]; 67: } 68: let t4; 69: if ($[6] !== customApiKeyTruncated) { 70: t4 = <Text>{t3}<Text>: sk-ant-...{customApiKeyTruncated}</Text></Text>; 71: $[6] = customApiKeyTruncated; 72: $[7] = t4; 73: } else { 74: t4 = $[7]; 75: } 76: let t5; 77: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 78: t5 = <Text>Do you want to use this API key?</Text>; 79: $[8] = t5; 80: } else { 81: t5 = $[8]; 82: } 83: let t6; 84: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 85: t6 = { 86: label: "Yes", 87: value: "yes" 88: }; 89: $[9] = t6; 90: } else { 91: t6 = $[9]; 92: } 93: let t7; 94: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 95: t7 = [t6, { 96: label: <Text>No (<Text bold={true}>recommended</Text>)</Text>, 97: value: "no" 98: }]; 99: $[10] = t7; 100: } else { 101: t7 = $[10]; 102: } 103: let t8; 104: if ($[11] !== onChange) { 105: t8 = <Select defaultValue="no" defaultFocusValue="no" options={t7} onChange={value_0 => onChange(value_0 as 'yes' | 'no')} onCancel={() => onChange("no")} />; 106: $[11] = onChange; 107: $[12] = t8; 108: } else { 109: t8 = $[12]; 110: } 111: let t9; 112: if ($[13] !== t2 || $[14] !== t4 || $[15] !== t8) { 113: t9 = <Dialog title="Detected a custom API key in your environment" color="warning" onCancel={t2}>{t4}{t5}{t8}</Dialog>; 114: $[13] = t2; 115: $[14] = t4; 116: $[15] = t8; 117: $[16] = t9; 118: } else { 119: t9 = $[16]; 120: } 121: return t9; 122: }

File: src/components/AutoModeOptInDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { logEvent } from 'src/services/analytics/index.js'; 4: import { Box, Link, Text } from '../ink.js'; 5: import { updateSettingsForSource } from '../utils/settings/settings.js'; 6: import { Select } from './CustomSelect/index.js'; 7: import { Dialog } from './design-system/Dialog.js'; 8: export const AUTO_MODE_DESCRIPTION = "Auto mode lets Claude handle permission prompts automatically — Claude checks each tool call for risky actions and prompt injection before executing. Actions Claude identifies as safe are executed, while actions Claude identifies as risky are blocked and Claude may try a different approach. Ideal for long-running tasks. Sessions are slightly more expensive. Claude can make mistakes that allow harmful commands to run, it's recommended to only use in isolated environments. Shift+Tab to change mode."; 9: type Props = { 10: onAccept(): void; 11: onDecline(): void; 12: declineExits?: boolean; 13: }; 14: export function AutoModeOptInDialog(t0) { 15: const $ = _c(18); 16: const { 17: onAccept, 18: onDecline, 19: declineExits 20: } = t0; 21: let t1; 22: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 23: t1 = []; 24: $[0] = t1; 25: } else { 26: t1 = $[0]; 27: } 28: React.useEffect(_temp, t1); 29: let t2; 30: if ($[1] !== onAccept || $[2] !== onDecline) { 31: t2 = function onChange(value) { 32: bb3: switch (value) { 33: case "accept": 34: { 35: logEvent("tengu_auto_mode_opt_in_dialog_accept", {}); 36: updateSettingsForSource("userSettings", { 37: skipAutoPermissionPrompt: true 38: }); 39: onAccept(); 40: break bb3; 41: } 42: case "accept-default": 43: { 44: logEvent("tengu_auto_mode_opt_in_dialog_accept_default", {}); 45: updateSettingsForSource("userSettings", { 46: skipAutoPermissionPrompt: true, 47: permissions: { 48: defaultMode: "auto" 49: } 50: }); 51: onAccept(); 52: break bb3; 53: } 54: case "decline": 55: { 56: logEvent("tengu_auto_mode_opt_in_dialog_decline", {}); 57: onDecline(); 58: } 59: } 60: }; 61: $[1] = onAccept; 62: $[2] = onDecline; 63: $[3] = t2; 64: } else { 65: t2 = $[3]; 66: } 67: const onChange = t2; 68: let t3; 69: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 70: t3 = <Box flexDirection="column" gap={1}><Text>{AUTO_MODE_DESCRIPTION}</Text><Link url="https://code.claude.com/docs/en/security" /></Box>; 71: $[4] = t3; 72: } else { 73: t3 = $[4]; 74: } 75: let t4; 76: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 77: t4 = true ? [{ 78: label: "Yes, and make it my default mode", 79: value: "accept-default" as const 80: }] : []; 81: $[5] = t4; 82: } else { 83: t4 = $[5]; 84: } 85: let t5; 86: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 87: t5 = { 88: label: "Yes, enable auto mode", 89: value: "accept" as const 90: }; 91: $[6] = t5; 92: } else { 93: t5 = $[6]; 94: } 95: const t6 = declineExits ? "No, exit" : "No, go back"; 96: let t7; 97: if ($[7] !== t6) { 98: t7 = [...t4, t5, { 99: label: t6, 100: value: "decline" as const 101: }]; 102: $[7] = t6; 103: $[8] = t7; 104: } else { 105: t7 = $[8]; 106: } 107: let t8; 108: if ($[9] !== onChange) { 109: t8 = value_0 => onChange(value_0 as 'accept' | 'accept-default' | 'decline'); 110: $[9] = onChange; 111: $[10] = t8; 112: } else { 113: t8 = $[10]; 114: } 115: let t9; 116: if ($[11] !== onDecline || $[12] !== t7 || $[13] !== t8) { 117: t9 = <Select options={t7} onChange={t8} onCancel={onDecline} />; 118: $[11] = onDecline; 119: $[12] = t7; 120: $[13] = t8; 121: $[14] = t9; 122: } else { 123: t9 = $[14]; 124: } 125: let t10; 126: if ($[15] !== onDecline || $[16] !== t9) { 127: t10 = <Dialog title="Enable auto mode?" color="warning" onCancel={onDecline}>{t3}{t9}</Dialog>; 128: $[15] = onDecline; 129: $[16] = t9; 130: $[17] = t10; 131: } else { 132: t10 = $[17]; 133: } 134: return t10; 135: } 136: function _temp() { 137: logEvent("tengu_auto_mode_opt_in_dialog_shown", {}); 138: }

File: src/components/AutoUpdater.tsx

typescript 1: import * as React from 'react'; 2: import { useEffect, useRef, useState } from 'react'; 3: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; 4: import { useInterval } from 'usehooks-ts'; 5: import { useUpdateNotification } from '../hooks/useUpdateNotification.js'; 6: import { Box, Text } from '../ink.js'; 7: import { type AutoUpdaterResult, getLatestVersion, getMaxVersion, type InstallStatus, installGlobalPackage, shouldSkipVersion } from '../utils/autoUpdater.js'; 8: import { getGlobalConfig, isAutoUpdaterDisabled } from '../utils/config.js'; 9: import { logForDebugging } from '../utils/debug.js'; 10: import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js'; 11: import { installOrUpdateClaudePackage, localInstallationExists } from '../utils/localInstaller.js'; 12: import { removeInstalledSymlink } from '../utils/nativeInstaller/index.js'; 13: import { gt, gte } from '../utils/semver.js'; 14: import { getInitialSettings } from '../utils/settings/settings.js'; 15: type Props = { 16: isUpdating: boolean; 17: onChangeIsUpdating: (isUpdating: boolean) => void; 18: onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void; 19: autoUpdaterResult: AutoUpdaterResult | null; 20: showSuccessMessage: boolean; 21: verbose: boolean; 22: }; 23: export function AutoUpdater({ 24: isUpdating, 25: onChangeIsUpdating, 26: onAutoUpdaterResult, 27: autoUpdaterResult, 28: showSuccessMessage, 29: verbose 30: }: Props): React.ReactNode { 31: const [versions, setVersions] = useState<{ 32: global?: string | null; 33: latest?: string | null; 34: }>({}); 35: const [hasLocalInstall, setHasLocalInstall] = useState(false); 36: const updateSemver = useUpdateNotification(autoUpdaterResult?.version); 37: useEffect(() => { 38: void localInstallationExists().then(setHasLocalInstall); 39: }, []); 40: const isUpdatingRef = useRef(isUpdating); 41: isUpdatingRef.current = isUpdating; 42: const checkForUpdates = React.useCallback(async () => { 43: if (isUpdatingRef.current) { 44: return; 45: } 46: if ("production" === 'test' || "production" === 'development') { 47: logForDebugging('AutoUpdater: Skipping update check in test/dev environment'); 48: return; 49: } 50: const currentVersion = MACRO.VERSION; 51: const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'; 52: let latestVersion = await getLatestVersion(channel); 53: const isDisabled = isAutoUpdaterDisabled(); 54: const maxVersion = await getMaxVersion(); 55: if (maxVersion && latestVersion && gt(latestVersion, maxVersion)) { 56: logForDebugging(`AutoUpdater: maxVersion ${maxVersion} is set, capping update from ${latestVersion} to ${maxVersion}`); 57: if (gte(currentVersion, maxVersion)) { 58: logForDebugging(`AutoUpdater: current version ${currentVersion} is already at or above maxVersion ${maxVersion}, skipping update`); 59: setVersions({ 60: global: currentVersion, 61: latest: latestVersion 62: }); 63: return; 64: } 65: latestVersion = maxVersion; 66: } 67: setVersions({ 68: global: currentVersion, 69: latest: latestVersion 70: }); 71: if (!isDisabled && currentVersion && latestVersion && !gte(currentVersion, latestVersion) && !shouldSkipVersion(latestVersion)) { 72: const startTime = Date.now(); 73: onChangeIsUpdating(true); 74: const config = getGlobalConfig(); 75: if (config.installMethod !== 'native') { 76: await removeInstalledSymlink(); 77: } 78: const installationType = await getCurrentInstallationType(); 79: logForDebugging(`AutoUpdater: Detected installation type: ${installationType}`); 80: if (installationType === 'development') { 81: logForDebugging('AutoUpdater: Cannot auto-update development build'); 82: onChangeIsUpdating(false); 83: return; 84: } 85: let installStatus: InstallStatus; 86: let updateMethod: 'local' | 'global'; 87: if (installationType === 'npm-local') { 88: logForDebugging('AutoUpdater: Using local update method'); 89: updateMethod = 'local'; 90: installStatus = await installOrUpdateClaudePackage(channel); 91: } else if (installationType === 'npm-global') { 92: logForDebugging('AutoUpdater: Using global update method'); 93: updateMethod = 'global'; 94: installStatus = await installGlobalPackage(); 95: } else if (installationType === 'native') { 96: logForDebugging('AutoUpdater: Unexpected native installation in non-native updater'); 97: onChangeIsUpdating(false); 98: return; 99: } else { 100: logForDebugging(`AutoUpdater: Unknown installation type, falling back to config`); 101: const isMigrated = config.installMethod === 'local'; 102: updateMethod = isMigrated ? 'local' : 'global'; 103: if (isMigrated) { 104: installStatus = await installOrUpdateClaudePackage(channel); 105: } else { 106: installStatus = await installGlobalPackage(); 107: } 108: } 109: onChangeIsUpdating(false); 110: if (installStatus === 'success') { 111: logEvent('tengu_auto_updater_success', { 112: fromVersion: currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 113: toVersion: latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 114: durationMs: Date.now() - startTime, 115: wasMigrated: updateMethod === 'local', 116: installationType: installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 117: }); 118: } else { 119: logEvent('tengu_auto_updater_fail', { 120: fromVersion: currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 121: attemptedVersion: latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 122: status: installStatus as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 123: durationMs: Date.now() - startTime, 124: wasMigrated: updateMethod === 'local', 125: installationType: installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 126: }); 127: } 128: onAutoUpdaterResult({ 129: version: latestVersion, 130: status: installStatus 131: }); 132: } 133: }, [onAutoUpdaterResult]); 134: useEffect(() => { 135: void checkForUpdates(); 136: }, [checkForUpdates]); 137: useInterval(checkForUpdates, 30 * 60 * 1000); 138: if (!autoUpdaterResult?.version && (!versions.global || !versions.latest)) { 139: return null; 140: } 141: if (!autoUpdaterResult?.version && !isUpdating) { 142: return null; 143: } 144: return <Box flexDirection="row" gap={1}> 145: {verbose && <Text dimColor wrap="truncate"> 146: globalVersion: {versions.global} &middot; latestVersion:{' '} 147: {versions.latest} 148: </Text>} 149: {isUpdating ? <> 150: <Box> 151: <Text color="text" dimColor wrap="truncate"> 152: Auto-updating… 153: </Text> 154: </Box> 155: </> : autoUpdaterResult?.status === 'success' && showSuccessMessage && updateSemver && <Text color="success" wrap="truncate"> 156: ✓ Update installed · Restart to apply 157: </Text>} 158: {(autoUpdaterResult?.status === 'install_failed' || autoUpdaterResult?.status === 'no_permissions') && <Text color="error" wrap="truncate"> 159: ✗ Auto-update failed &middot; Try <Text bold>claude doctor</Text> or{' '} 160: <Text bold> 161: {hasLocalInstall ? `cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}` : `npm i -g ${MACRO.PACKAGE_URL}`} 162: </Text> 163: </Text>} 164: </Box>; 165: }

File: src/components/AutoUpdaterWrapper.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import * as React from 'react'; 4: import type { AutoUpdaterResult } from '../utils/autoUpdater.js'; 5: import { isAutoUpdaterDisabled } from '../utils/config.js'; 6: import { logForDebugging } from '../utils/debug.js'; 7: import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js'; 8: import { AutoUpdater } from './AutoUpdater.js'; 9: import { NativeAutoUpdater } from './NativeAutoUpdater.js'; 10: import { PackageManagerAutoUpdater } from './PackageManagerAutoUpdater.js'; 11: type Props = { 12: isUpdating: boolean; 13: onChangeIsUpdating: (isUpdating: boolean) => void; 14: onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void; 15: autoUpdaterResult: AutoUpdaterResult | null; 16: showSuccessMessage: boolean; 17: verbose: boolean; 18: }; 19: export function AutoUpdaterWrapper(t0) { 20: const $ = _c(17); 21: const { 22: isUpdating, 23: onChangeIsUpdating, 24: onAutoUpdaterResult, 25: autoUpdaterResult, 26: showSuccessMessage, 27: verbose 28: } = t0; 29: const [useNativeInstaller, setUseNativeInstaller] = React.useState(null); 30: const [isPackageManager, setIsPackageManager] = React.useState(null); 31: let t1; 32: let t2; 33: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 34: t1 = () => { 35: const checkInstallation = async function checkInstallation() { 36: if (feature("SKIP_DETECTION_WHEN_AUTOUPDATES_DISABLED") && isAutoUpdaterDisabled()) { 37: logForDebugging("AutoUpdaterWrapper: Skipping detection, auto-updates disabled"); 38: return; 39: } 40: const installationType = await getCurrentInstallationType(); 41: logForDebugging(`AutoUpdaterWrapper: Installation type: ${installationType}`); 42: setUseNativeInstaller(installationType === "native"); 43: setIsPackageManager(installationType === "package-manager"); 44: }; 45: checkInstallation(); 46: }; 47: t2 = []; 48: $[0] = t1; 49: $[1] = t2; 50: } else { 51: t1 = $[0]; 52: t2 = $[1]; 53: } 54: React.useEffect(t1, t2); 55: if (useNativeInstaller === null || isPackageManager === null) { 56: return null; 57: } 58: if (isPackageManager) { 59: let t3; 60: if ($[2] !== autoUpdaterResult || $[3] !== isUpdating || $[4] !== onAutoUpdaterResult || $[5] !== onChangeIsUpdating || $[6] !== showSuccessMessage || $[7] !== verbose) { 61: t3 = <PackageManagerAutoUpdater verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={showSuccessMessage} />; 62: $[2] = autoUpdaterResult; 63: $[3] = isUpdating; 64: $[4] = onAutoUpdaterResult; 65: $[5] = onChangeIsUpdating; 66: $[6] = showSuccessMessage; 67: $[7] = verbose; 68: $[8] = t3; 69: } else { 70: t3 = $[8]; 71: } 72: return t3; 73: } 74: const Updater = useNativeInstaller ? NativeAutoUpdater : AutoUpdater; 75: let t3; 76: if ($[9] !== Updater || $[10] !== autoUpdaterResult || $[11] !== isUpdating || $[12] !== onAutoUpdaterResult || $[13] !== onChangeIsUpdating || $[14] !== showSuccessMessage || $[15] !== verbose) { 77: t3 = <Updater verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={showSuccessMessage} />; 78: $[9] = Updater; 79: $[10] = autoUpdaterResult; 80: $[11] = isUpdating; 81: $[12] = onAutoUpdaterResult; 82: $[13] = onChangeIsUpdating; 83: $[14] = showSuccessMessage; 84: $[15] = verbose; 85: $[16] = t3; 86: } else { 87: t3 = $[16]; 88: } 89: return t3; 90: }

File: src/components/AwsAuthStatusBox.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useEffect, useState } from 'react'; 3: import { Box, Link, Text } from '../ink.js'; 4: import { type AwsAuthStatus, AwsAuthStatusManager } from '../utils/awsAuthStatusManager.js'; 5: const URL_RE = /https?:\/\/\S+/; 6: export function AwsAuthStatusBox() { 7: const $ = _c(11); 8: let t0; 9: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 10: t0 = AwsAuthStatusManager.getInstance().getStatus(); 11: $[0] = t0; 12: } else { 13: t0 = $[0]; 14: } 15: const [status, setStatus] = useState(t0); 16: let t1; 17: let t2; 18: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 19: t1 = () => { 20: const unsubscribe = AwsAuthStatusManager.getInstance().subscribe(setStatus); 21: return unsubscribe; 22: }; 23: t2 = []; 24: $[1] = t1; 25: $[2] = t2; 26: } else { 27: t1 = $[1]; 28: t2 = $[2]; 29: } 30: useEffect(t1, t2); 31: if (!status.isAuthenticating && !status.error && status.output.length === 0) { 32: return null; 33: } 34: if (!status.isAuthenticating && !status.error) { 35: return null; 36: } 37: let t3; 38: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 39: t3 = <Text bold={true} color="permission">Cloud Authentication</Text>; 40: $[3] = t3; 41: } else { 42: t3 = $[3]; 43: } 44: let t4; 45: if ($[4] !== status.output) { 46: t4 = status.output.length > 0 && <Box flexDirection="column" marginTop={1}>{status.output.slice(-5).map(_temp)}</Box>; 47: $[4] = status.output; 48: $[5] = t4; 49: } else { 50: t4 = $[5]; 51: } 52: let t5; 53: if ($[6] !== status.error) { 54: t5 = status.error && <Box marginTop={1}><Text color="error">{status.error}</Text></Box>; 55: $[6] = status.error; 56: $[7] = t5; 57: } else { 58: t5 = $[7]; 59: } 60: let t6; 61: if ($[8] !== t4 || $[9] !== t5) { 62: t6 = <Box flexDirection="column" borderStyle="round" borderColor="permission" paddingX={1} marginY={1}>{t3}{t4}{t5}</Box>; 63: $[8] = t4; 64: $[9] = t5; 65: $[10] = t6; 66: } else { 67: t6 = $[10]; 68: } 69: return t6; 70: } 71: function _temp(line, index) { 72: const m = line.match(URL_RE); 73: if (!m) { 74: return <Text key={index} dimColor={true}>{line}</Text>; 75: } 76: const url = m[0]; 77: const start = m.index ?? 0; 78: const before = line.slice(0, start); 79: const after = line.slice(start + url.length); 80: return <Text key={index} dimColor={true}>{before}<Link url={url}>{url}</Link>{after}</Text>; 81: }

File: src/components/BaseTextInput.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { renderPlaceholder } from '../hooks/renderPlaceholder.js'; 4: import { usePasteHandler } from '../hooks/usePasteHandler.js'; 5: import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js'; 6: import { Ansi, Box, Text, useInput } from '../ink.js'; 7: import type { BaseInputState, BaseTextInputProps } from '../types/textInputTypes.js'; 8: import type { TextHighlight } from '../utils/textHighlighting.js'; 9: import { HighlightedInput } from './PromptInput/ShimmeredInput.js'; 10: type BaseTextInputComponentProps = BaseTextInputProps & { 11: inputState: BaseInputState; 12: children?: React.ReactNode; 13: terminalFocus: boolean; 14: highlights?: TextHighlight[]; 15: invert?: (text: string) => string; 16: hidePlaceholderText?: boolean; 17: }; 18: export function BaseTextInput(t0) { 19: const $ = _c(14); 20: const { 21: inputState, 22: children, 23: terminalFocus, 24: invert, 25: hidePlaceholderText, 26: ...props 27: } = t0; 28: const { 29: onInput, 30: renderedValue, 31: cursorLine, 32: cursorColumn 33: } = inputState; 34: const t1 = Boolean(props.focus && props.showCursor && terminalFocus); 35: let t2; 36: if ($[0] !== cursorColumn || $[1] !== cursorLine || $[2] !== t1) { 37: t2 = { 38: line: cursorLine, 39: column: cursorColumn, 40: active: t1 41: }; 42: $[0] = cursorColumn; 43: $[1] = cursorLine; 44: $[2] = t1; 45: $[3] = t2; 46: } else { 47: t2 = $[3]; 48: } 49: const cursorRef = useDeclaredCursor(t2); 50: const { 51: wrappedOnInput, 52: isPasting: t3 53: } = usePasteHandler({ 54: onPaste: props.onPaste, 55: onInput: (input, key) => { 56: if (isPasting && key.return) { 57: return; 58: } 59: onInput(input, key); 60: }, 61: onImagePaste: props.onImagePaste 62: }); 63: const isPasting = t3; 64: const { 65: onIsPastingChange 66: } = props; 67: React.useEffect(() => { 68: if (onIsPastingChange) { 69: onIsPastingChange(isPasting); 70: } 71: }, [isPasting, onIsPastingChange]); 72: const { 73: showPlaceholder, 74: renderedPlaceholder 75: } = renderPlaceholder({ 76: placeholder: props.placeholder, 77: value: props.value, 78: showCursor: props.showCursor, 79: focus: props.focus, 80: terminalFocus, 81: invert, 82: hidePlaceholderText 83: }); 84: useInput(wrappedOnInput, { 85: isActive: props.focus 86: }); 87: const commandWithoutArgs = props.value && props.value.trim().indexOf(" ") === -1 || props.value && props.value.endsWith(" "); 88: const showArgumentHint = Boolean(props.argumentHint && props.value && commandWithoutArgs && props.value.startsWith("/")); 89: const cursorFiltered = props.showCursor && props.highlights ? props.highlights.filter(h => h.dimColor || props.cursorOffset < h.start || props.cursorOffset >= h.end) : props.highlights; 90: const { 91: viewportCharOffset, 92: viewportCharEnd 93: } = inputState; 94: const filteredHighlights = cursorFiltered && viewportCharOffset > 0 ? cursorFiltered.filter(h_0 => h_0.end > viewportCharOffset && h_0.start < viewportCharEnd).map(h_1 => ({ 95: ...h_1, 96: start: Math.max(0, h_1.start - viewportCharOffset), 97: end: h_1.end - viewportCharOffset 98: })) : cursorFiltered; 99: const hasHighlights = filteredHighlights && filteredHighlights.length > 0; 100: if (hasHighlights) { 101: return <Box ref={cursorRef}><HighlightedInput text={renderedValue} highlights={filteredHighlights} />{showArgumentHint && <Text dimColor={true}>{props.value?.endsWith(" ") ? "" : " "}{props.argumentHint}</Text>}{children}</Box>; 102: } 103: const T0 = Box; 104: const T1 = Text; 105: const t4 = "truncate-end"; 106: const t5 = showPlaceholder && props.placeholderElement ? props.placeholderElement : showPlaceholder && renderedPlaceholder ? <Ansi>{renderedPlaceholder}</Ansi> : <Ansi>{renderedValue}</Ansi>; 107: const t6 = showArgumentHint && <Text dimColor={true}>{props.value?.endsWith(" ") ? "" : " "}{props.argumentHint}</Text>; 108: let t7; 109: if ($[4] !== T1 || $[5] !== children || $[6] !== props || $[7] !== t5 || $[8] !== t6) { 110: t7 = <T1 wrap={t4} dimColor={props.dimColor}>{t5}{t6}{children}</T1>; 111: $[4] = T1; 112: $[5] = children; 113: $[6] = props; 114: $[7] = t5; 115: $[8] = t6; 116: $[9] = t7; 117: } else { 118: t7 = $[9]; 119: } 120: let t8; 121: if ($[10] !== T0 || $[11] !== cursorRef || $[12] !== t7) { 122: t8 = <T0 ref={cursorRef}>{t7}</T0>; 123: $[10] = T0; 124: $[11] = cursorRef; 125: $[12] = t7; 126: $[13] = t8; 127: } else { 128: t8 = $[13]; 129: } 130: return t8; 131: }

File: src/components/BashModeProgress.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box } from '../ink.js'; 4: import { BashTool } from '../tools/BashTool/BashTool.js'; 5: import type { ShellProgress } from '../types/tools.js'; 6: import { UserBashInputMessage } from './messages/UserBashInputMessage.js'; 7: import { ShellProgressMessage } from './shell/ShellProgressMessage.js'; 8: type Props = { 9: input: string; 10: progress: ShellProgress | null; 11: verbose: boolean; 12: }; 13: export function BashModeProgress(t0) { 14: const $ = _c(8); 15: const { 16: input, 17: progress, 18: verbose 19: } = t0; 20: const t1 = `<bash-input>${input}</bash-input>`; 21: let t2; 22: if ($[0] !== t1) { 23: t2 = <UserBashInputMessage addMargin={false} param={{ 24: text: t1, 25: type: "text" 26: }} />; 27: $[0] = t1; 28: $[1] = t2; 29: } else { 30: t2 = $[1]; 31: } 32: let t3; 33: if ($[2] !== progress || $[3] !== verbose) { 34: t3 = progress ? <ShellProgressMessage fullOutput={progress.fullOutput} output={progress.output} elapsedTimeSeconds={progress.elapsedTimeSeconds} totalLines={progress.totalLines} verbose={verbose} /> : BashTool.renderToolUseProgressMessage?.([], { 35: verbose, 36: tools: [], 37: terminalSize: undefined 38: }); 39: $[2] = progress; 40: $[3] = verbose; 41: $[4] = t3; 42: } else { 43: t3 = $[4]; 44: } 45: let t4; 46: if ($[5] !== t2 || $[6] !== t3) { 47: t4 = <Box flexDirection="column" marginTop={1}>{t2}{t3}</Box>; 48: $[5] = t2; 49: $[6] = t3; 50: $[7] = t4; 51: } else { 52: t4 = $[7]; 53: } 54: return t4; 55: }

File: src/components/BridgeDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { basename } from 'path'; 3: import { toString as qrToString } from 'qrcode'; 4: import * as React from 'react'; 5: import { useEffect, useState } from 'react'; 6: import { getOriginalCwd } from '../bootstrap/state.js'; 7: import { buildActiveFooterText, buildIdleFooterText, FAILED_FOOTER_TEXT, getBridgeStatus } from '../bridge/bridgeStatusUtil.js'; 8: import { BRIDGE_FAILED_INDICATOR, BRIDGE_READY_INDICATOR } from '../constants/figures.js'; 9: import { useRegisterOverlay } from '../context/overlayContext.js'; 10: import { Box, Text, useInput } from '../ink.js'; 11: import { useKeybindings } from '../keybindings/useKeybinding.js'; 12: import { useAppState, useSetAppState } from '../state/AppState.js'; 13: import { saveGlobalConfig } from '../utils/config.js'; 14: import { getBranch } from '../utils/git.js'; 15: import { Dialog } from './design-system/Dialog.js'; 16: type Props = { 17: onDone: () => void; 18: }; 19: export function BridgeDialog(t0) { 20: const $ = _c(87); 21: const { 22: onDone 23: } = t0; 24: useRegisterOverlay("bridge-dialog"); 25: const connected = useAppState(_temp); 26: const sessionActive = useAppState(_temp2); 27: const reconnecting = useAppState(_temp3); 28: const connectUrl = useAppState(_temp4); 29: const sessionUrl = useAppState(_temp5); 30: const error = useAppState(_temp6); 31: const explicit = useAppState(_temp7); 32: const environmentId = useAppState(_temp8); 33: const sessionId = useAppState(_temp9); 34: const verbose = useAppState(_temp0); 35: const setAppState = useSetAppState(); 36: const [showQR, setShowQR] = useState(false); 37: const [qrText, setQrText] = useState(""); 38: const [branchName, setBranchName] = useState(""); 39: let t1; 40: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 41: t1 = basename(getOriginalCwd()); 42: $[0] = t1; 43: } else { 44: t1 = $[0]; 45: } 46: const repoName = t1; 47: let t2; 48: let t3; 49: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 50: t2 = () => { 51: getBranch().then(setBranchName).catch(_temp1); 52: }; 53: t3 = []; 54: $[1] = t2; 55: $[2] = t3; 56: } else { 57: t2 = $[1]; 58: t3 = $[2]; 59: } 60: useEffect(t2, t3); 61: const displayUrl = sessionActive ? sessionUrl : connectUrl; 62: let t4; 63: let t5; 64: if ($[3] !== displayUrl || $[4] !== showQR) { 65: t4 = () => { 66: if (!showQR || !displayUrl) { 67: setQrText(""); 68: return; 69: } 70: qrToString(displayUrl, { 71: type: "utf8", 72: errorCorrectionLevel: "L", 73: small: true 74: }).then(setQrText).catch(() => setQrText("")); 75: }; 76: t5 = [showQR, displayUrl]; 77: $[3] = displayUrl; 78: $[4] = showQR; 79: $[5] = t4; 80: $[6] = t5; 81: } else { 82: t4 = $[5]; 83: t5 = $[6]; 84: } 85: useEffect(t4, t5); 86: let t6; 87: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 88: t6 = () => { 89: setShowQR(_temp10); 90: }; 91: $[7] = t6; 92: } else { 93: t6 = $[7]; 94: } 95: let t7; 96: if ($[8] !== onDone) { 97: t7 = { 98: "confirm:yes": onDone, 99: "confirm:toggle": t6 100: }; 101: $[8] = onDone; 102: $[9] = t7; 103: } else { 104: t7 = $[9]; 105: } 106: let t8; 107: if ($[10] === Symbol.for("react.memo_cache_sentinel")) { 108: t8 = { 109: context: "Confirmation" 110: }; 111: $[10] = t8; 112: } else { 113: t8 = $[10]; 114: } 115: useKeybindings(t7, t8); 116: let t9; 117: if ($[11] !== explicit || $[12] !== onDone || $[13] !== setAppState) { 118: t9 = input => { 119: if (input === "d") { 120: if (explicit) { 121: saveGlobalConfig(_temp11); 122: } 123: setAppState(_temp12); 124: onDone(); 125: } 126: }; 127: $[11] = explicit; 128: $[12] = onDone; 129: $[13] = setAppState; 130: $[14] = t9; 131: } else { 132: t9 = $[14]; 133: } 134: useInput(t9); 135: let t10; 136: if ($[15] !== connected || $[16] !== error || $[17] !== reconnecting || $[18] !== sessionActive) { 137: t10 = getBridgeStatus({ 138: error, 139: connected, 140: sessionActive, 141: reconnecting 142: }); 143: $[15] = connected; 144: $[16] = error; 145: $[17] = reconnecting; 146: $[18] = sessionActive; 147: $[19] = t10; 148: } else { 149: t10 = $[19]; 150: } 151: const { 152: label: statusLabel, 153: color: statusColor 154: } = t10; 155: const indicator = error ? BRIDGE_FAILED_INDICATOR : BRIDGE_READY_INDICATOR; 156: let T0; 157: let T1; 158: let footerText; 159: let t11; 160: let t12; 161: let t13; 162: let t14; 163: let t15; 164: let t16; 165: let t17; 166: if ($[20] !== branchName || $[21] !== displayUrl || $[22] !== environmentId || $[23] !== error || $[24] !== indicator || $[25] !== onDone || $[26] !== qrText || $[27] !== sessionActive || $[28] !== sessionId || $[29] !== showQR || $[30] !== statusColor || $[31] !== statusLabel || $[32] !== verbose) { 167: const qrLines = qrText ? qrText.split("\n").filter(_temp13) : []; 168: let contextParts; 169: if ($[43] !== branchName) { 170: contextParts = []; 171: if (repoName) { 172: contextParts.push(repoName); 173: } 174: if (branchName) { 175: contextParts.push(branchName); 176: } 177: $[43] = branchName; 178: $[44] = contextParts; 179: } else { 180: contextParts = $[44]; 181: } 182: const contextSuffix = contextParts.length > 0 ? " \xB7 " + contextParts.join(" \xB7 ") : ""; 183: let t18; 184: if ($[45] !== displayUrl || $[46] !== error || $[47] !== sessionActive) { 185: t18 = error ? FAILED_FOOTER_TEXT : displayUrl ? sessionActive ? buildActiveFooterText(displayUrl) : buildIdleFooterText(displayUrl) : undefined; 186: $[45] = displayUrl; 187: $[46] = error; 188: $[47] = sessionActive; 189: $[48] = t18; 190: } else { 191: t18 = $[48]; 192: } 193: footerText = t18; 194: T1 = Dialog; 195: t15 = "Remote Control"; 196: t16 = onDone; 197: t17 = true; 198: T0 = Box; 199: t11 = "column"; 200: t12 = 1; 201: let t19; 202: if ($[49] !== indicator || $[50] !== statusColor || $[51] !== statusLabel) { 203: t19 = <Text color={statusColor}>{indicator} {statusLabel}</Text>; 204: $[49] = indicator; 205: $[50] = statusColor; 206: $[51] = statusLabel; 207: $[52] = t19; 208: } else { 209: t19 = $[52]; 210: } 211: let t20; 212: if ($[53] !== contextSuffix) { 213: t20 = <Text dimColor={true}>{contextSuffix}</Text>; 214: $[53] = contextSuffix; 215: $[54] = t20; 216: } else { 217: t20 = $[54]; 218: } 219: let t21; 220: if ($[55] !== t19 || $[56] !== t20) { 221: t21 = <Text>{t19}{t20}</Text>; 222: $[55] = t19; 223: $[56] = t20; 224: $[57] = t21; 225: } else { 226: t21 = $[57]; 227: } 228: let t22; 229: if ($[58] !== error) { 230: t22 = error && <Text color="error">{error}</Text>; 231: $[58] = error; 232: $[59] = t22; 233: } else { 234: t22 = $[59]; 235: } 236: let t23; 237: if ($[60] !== environmentId || $[61] !== verbose) { 238: t23 = verbose && environmentId && <Text dimColor={true}>Environment: {environmentId}</Text>; 239: $[60] = environmentId; 240: $[61] = verbose; 241: $[62] = t23; 242: } else { 243: t23 = $[62]; 244: } 245: let t24; 246: if ($[63] !== sessionId || $[64] !== verbose) { 247: t24 = verbose && sessionId && <Text dimColor={true}>Session: {sessionId}</Text>; 248: $[63] = sessionId; 249: $[64] = verbose; 250: $[65] = t24; 251: } else { 252: t24 = $[65]; 253: } 254: if ($[66] !== t21 || $[67] !== t22 || $[68] !== t23 || $[69] !== t24) { 255: t13 = <Box flexDirection="column">{t21}{t22}{t23}{t24}</Box>; 256: $[66] = t21; 257: $[67] = t22; 258: $[68] = t23; 259: $[69] = t24; 260: $[70] = t13; 261: } else { 262: t13 = $[70]; 263: } 264: t14 = showQR && qrLines.length > 0 && <Box flexDirection="column">{qrLines.map(_temp14)}</Box>; 265: $[20] = branchName; 266: $[21] = displayUrl; 267: $[22] = environmentId; 268: $[23] = error; 269: $[24] = indicator; 270: $[25] = onDone; 271: $[26] = qrText; 272: $[27] = sessionActive; 273: $[28] = sessionId; 274: $[29] = showQR; 275: $[30] = statusColor; 276: $[31] = statusLabel; 277: $[32] = verbose; 278: $[33] = T0; 279: $[34] = T1; 280: $[35] = footerText; 281: $[36] = t11; 282: $[37] = t12; 283: $[38] = t13; 284: $[39] = t14; 285: $[40] = t15; 286: $[41] = t16; 287: $[42] = t17; 288: } else { 289: T0 = $[33]; 290: T1 = $[34]; 291: footerText = $[35]; 292: t11 = $[36]; 293: t12 = $[37]; 294: t13 = $[38]; 295: t14 = $[39]; 296: t15 = $[40]; 297: t16 = $[41]; 298: t17 = $[42]; 299: } 300: let t18; 301: if ($[71] !== footerText) { 302: t18 = footerText && <Text dimColor={true}>{footerText}</Text>; 303: $[71] = footerText; 304: $[72] = t18; 305: } else { 306: t18 = $[72]; 307: } 308: let t19; 309: if ($[73] === Symbol.for("react.memo_cache_sentinel")) { 310: t19 = <Text dimColor={true}>d to disconnect · space for QR code · Enter/Esc to close</Text>; 311: $[73] = t19; 312: } else { 313: t19 = $[73]; 314: } 315: let t20; 316: if ($[74] !== T0 || $[75] !== t11 || $[76] !== t12 || $[77] !== t13 || $[78] !== t14 || $[79] !== t18) { 317: t20 = <T0 flexDirection={t11} gap={t12}>{t13}{t14}{t18}{t19}</T0>; 318: $[74] = T0; 319: $[75] = t11; 320: $[76] = t12; 321: $[77] = t13; 322: $[78] = t14; 323: $[79] = t18; 324: $[80] = t20; 325: } else { 326: t20 = $[80]; 327: } 328: let t21; 329: if ($[81] !== T1 || $[82] !== t15 || $[83] !== t16 || $[84] !== t17 || $[85] !== t20) { 330: t21 = <T1 title={t15} onCancel={t16} hideInputGuide={t17}>{t20}</T1>; 331: $[81] = T1; 332: $[82] = t15; 333: $[83] = t16; 334: $[84] = t17; 335: $[85] = t20; 336: $[86] = t21; 337: } else { 338: t21 = $[86]; 339: } 340: return t21; 341: } 342: function _temp14(line, i) { 343: return <Text key={i}>{line}</Text>; 344: } 345: function _temp13(l) { 346: return l.length > 0; 347: } 348: function _temp12(prev_0) { 349: if (!prev_0.replBridgeEnabled) { 350: return prev_0; 351: } 352: return { 353: ...prev_0, 354: replBridgeEnabled: false 355: }; 356: } 357: function _temp11(current) { 358: if (current.remoteControlAtStartup === false) { 359: return current; 360: } 361: return { 362: ...current, 363: remoteControlAtStartup: false 364: }; 365: } 366: function _temp10(prev) { 367: return !prev; 368: } 369: function _temp1() {} 370: function _temp0(s_8) { 371: return s_8.verbose; 372: } 373: function _temp9(s_7) { 374: return s_7.replBridgeSessionId; 375: } 376: function _temp8(s_6) { 377: return s_6.replBridgeEnvironmentId; 378: } 379: function _temp7(s_5) { 380: return s_5.replBridgeExplicit; 381: } 382: function _temp6(s_4) { 383: return s_4.replBridgeError; 384: } 385: function _temp5(s_3) { 386: return s_3.replBridgeSessionUrl; 387: } 388: function _temp4(s_2) { 389: return s_2.replBridgeConnectUrl; 390: } 391: function _temp3(s_1) { 392: return s_1.replBridgeReconnecting; 393: } 394: function _temp2(s_0) { 395: return s_0.replBridgeSessionActive; 396: } 397: function _temp(s) { 398: return s.replBridgeConnected; 399: }

File: src/components/BypassPermissionsModeDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useCallback } from 'react'; 3: import { logEvent } from 'src/services/analytics/index.js'; 4: import { Box, Link, Newline, Text } from '../ink.js'; 5: import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'; 6: import { updateSettingsForSource } from '../utils/settings/settings.js'; 7: import { Select } from './CustomSelect/index.js'; 8: import { Dialog } from './design-system/Dialog.js'; 9: type Props = { 10: onAccept(): void; 11: }; 12: export function BypassPermissionsModeDialog(t0) { 13: const $ = _c(7); 14: const { 15: onAccept 16: } = t0; 17: let t1; 18: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 19: t1 = []; 20: $[0] = t1; 21: } else { 22: t1 = $[0]; 23: } 24: React.useEffect(_temp, t1); 25: let t2; 26: if ($[1] !== onAccept) { 27: t2 = function onChange(value) { 28: bb3: switch (value) { 29: case "accept": 30: { 31: logEvent("tengu_bypass_permissions_mode_dialog_accept", {}); 32: updateSettingsForSource("userSettings", { 33: skipDangerousModePermissionPrompt: true 34: }); 35: onAccept(); 36: break bb3; 37: } 38: case "decline": 39: { 40: gracefulShutdownSync(1); 41: } 42: } 43: }; 44: $[1] = onAccept; 45: $[2] = t2; 46: } else { 47: t2 = $[2]; 48: } 49: const onChange = t2; 50: const handleEscape = _temp2; 51: let t3; 52: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 53: t3 = <Box flexDirection="column" gap={1}><Text>In Bypass Permissions mode, Claude Code will not ask for your approval before running potentially dangerous commands.<Newline />This mode should only be used in a sandboxed container/VM that has restricted internet access and can easily be restored if damaged.</Text><Text>By proceeding, you accept all responsibility for actions taken while running in Bypass Permissions mode.</Text><Link url="https://code.claude.com/docs/en/security" /></Box>; 54: $[3] = t3; 55: } else { 56: t3 = $[3]; 57: } 58: let t4; 59: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 60: t4 = [{ 61: label: "No, exit", 62: value: "decline" 63: }, { 64: label: "Yes, I accept", 65: value: "accept" 66: }]; 67: $[4] = t4; 68: } else { 69: t4 = $[4]; 70: } 71: let t5; 72: if ($[5] !== onChange) { 73: t5 = <Dialog title="WARNING: Claude Code running in Bypass Permissions mode" color="error" onCancel={handleEscape}>{t3}<Select options={t4} onChange={value_0 => onChange(value_0 as 'accept' | 'decline')} /></Dialog>; 74: $[5] = onChange; 75: $[6] = t5; 76: } else { 77: t5 = $[6]; 78: } 79: return t5; 80: } 81: function _temp2() { 82: gracefulShutdownSync(0); 83: } 84: function _temp() { 85: logEvent("tengu_bypass_permissions_mode_dialog_shown", {}); 86: }

File: src/components/ChannelDowngradeDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Text } from '../ink.js'; 4: import { Select } from './CustomSelect/index.js'; 5: import { Dialog } from './design-system/Dialog.js'; 6: export type ChannelDowngradeChoice = 'downgrade' | 'stay' | 'cancel'; 7: type Props = { 8: currentVersion: string; 9: onChoice: (choice: ChannelDowngradeChoice) => void; 10: }; 11: export function ChannelDowngradeDialog(t0) { 12: const $ = _c(17); 13: const { 14: currentVersion, 15: onChoice 16: } = t0; 17: let t1; 18: if ($[0] !== onChoice) { 19: t1 = function handleSelect(value) { 20: onChoice(value); 21: }; 22: $[0] = onChoice; 23: $[1] = t1; 24: } else { 25: t1 = $[1]; 26: } 27: const handleSelect = t1; 28: let t2; 29: if ($[2] !== onChoice) { 30: t2 = function handleCancel() { 31: onChoice("cancel"); 32: }; 33: $[2] = onChoice; 34: $[3] = t2; 35: } else { 36: t2 = $[3]; 37: } 38: const handleCancel = t2; 39: let t3; 40: if ($[4] !== currentVersion) { 41: t3 = <Text>The stable channel may have an older version than what you're currently running ({currentVersion}).</Text>; 42: $[4] = currentVersion; 43: $[5] = t3; 44: } else { 45: t3 = $[5]; 46: } 47: let t4; 48: if ($[6] === Symbol.for("react.memo_cache_sentinel")) { 49: t4 = <Text dimColor={true}>How would you like to handle this?</Text>; 50: $[6] = t4; 51: } else { 52: t4 = $[6]; 53: } 54: let t5; 55: if ($[7] === Symbol.for("react.memo_cache_sentinel")) { 56: t5 = { 57: label: "Allow possible downgrade to stable version", 58: value: "downgrade" as ChannelDowngradeChoice 59: }; 60: $[7] = t5; 61: } else { 62: t5 = $[7]; 63: } 64: const t6 = `Stay on current version (${currentVersion}) until stable catches up`; 65: let t7; 66: if ($[8] !== t6) { 67: t7 = [t5, { 68: label: t6, 69: value: "stay" as ChannelDowngradeChoice 70: }]; 71: $[8] = t6; 72: $[9] = t7; 73: } else { 74: t7 = $[9]; 75: } 76: let t8; 77: if ($[10] !== handleSelect || $[11] !== t7) { 78: t8 = <Select options={t7} onChange={handleSelect} />; 79: $[10] = handleSelect; 80: $[11] = t7; 81: $[12] = t8; 82: } else { 83: t8 = $[12]; 84: } 85: let t9; 86: if ($[13] !== handleCancel || $[14] !== t3 || $[15] !== t8) { 87: t9 = <Dialog title="Switch to Stable Channel" onCancel={handleCancel} color="permission" hideBorder={true} hideInputGuide={true}>{t3}{t4}{t8}</Dialog>; 88: $[13] = handleCancel; 89: $[14] = t3; 90: $[15] = t8; 91: $[16] = t9; 92: } else { 93: t9 = $[16]; 94: } 95: return t9; 96: }

File: src/components/ClaudeInChromeOnboarding.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { logEvent } from 'src/services/analytics/index.js'; 4: import { Box, Link, Newline, Text, useInput } from '../ink.js'; 5: import { isChromeExtensionInstalled } from '../utils/claudeInChrome/setup.js'; 6: import { saveGlobalConfig } from '../utils/config.js'; 7: import { Dialog } from './design-system/Dialog.js'; 8: const CHROME_EXTENSION_URL = 'https://claude.ai/chrome'; 9: const CHROME_PERMISSIONS_URL = 'https://clau.de/chrome/permissions'; 10: type Props = { 11: onDone(): void; 12: }; 13: export function ClaudeInChromeOnboarding(t0) { 14: const $ = _c(20); 15: const { 16: onDone 17: } = t0; 18: const [isExtensionInstalled, setIsExtensionInstalled] = React.useState(false); 19: let t1; 20: let t2; 21: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 22: t1 = () => { 23: logEvent("tengu_claude_in_chrome_onboarding_shown", {}); 24: isChromeExtensionInstalled().then(setIsExtensionInstalled); 25: saveGlobalConfig(_temp); 26: }; 27: t2 = []; 28: $[0] = t1; 29: $[1] = t2; 30: } else { 31: t1 = $[0]; 32: t2 = $[1]; 33: } 34: React.useEffect(t1, t2); 35: let t3; 36: if ($[2] !== onDone) { 37: t3 = (_input, key) => { 38: if (key.return) { 39: onDone(); 40: } 41: }; 42: $[2] = onDone; 43: $[3] = t3; 44: } else { 45: t3 = $[3]; 46: } 47: useInput(t3); 48: let t4; 49: if ($[4] !== isExtensionInstalled) { 50: t4 = !isExtensionInstalled && <><Newline /><Newline />Requires the Chrome extension. Get started at{" "}<Link url={CHROME_EXTENSION_URL} /></>; 51: $[4] = isExtensionInstalled; 52: $[5] = t4; 53: } else { 54: t4 = $[5]; 55: } 56: let t5; 57: if ($[6] !== t4) { 58: t5 = <Text>Claude in Chrome works with the Chrome extension to let you control your browser directly from Claude Code. You can navigate websites, fill forms, capture screenshots, record GIFs, and debug with console logs and network requests.{t4}</Text>; 59: $[6] = t4; 60: $[7] = t5; 61: } else { 62: t5 = $[7]; 63: } 64: let t6; 65: if ($[8] !== isExtensionInstalled) { 66: t6 = isExtensionInstalled && <>{" "}(<Link url={CHROME_PERMISSIONS_URL} />)</>; 67: $[8] = isExtensionInstalled; 68: $[9] = t6; 69: } else { 70: t6 = $[9]; 71: } 72: let t7; 73: if ($[10] !== t6) { 74: t7 = <Text dimColor={true}>Site-level permissions are inherited from the Chrome extension. Manage permissions in the Chrome extension settings to control which sites Claude can browse, click, and type on{t6}.</Text>; 75: $[10] = t6; 76: $[11] = t7; 77: } else { 78: t7 = $[11]; 79: } 80: let t8; 81: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 82: t8 = <Text bold={true} color="chromeYellow">/chrome</Text>; 83: $[12] = t8; 84: } else { 85: t8 = $[12]; 86: } 87: let t9; 88: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 89: t9 = <Text dimColor={true}>For more info, use{" "}{t8}{" "}or visit <Link url="https://code.claude.com/docs/en/chrome" /></Text>; 90: $[13] = t9; 91: } else { 92: t9 = $[13]; 93: } 94: let t10; 95: if ($[14] !== t5 || $[15] !== t7) { 96: t10 = <Box flexDirection="column" gap={1}>{t5}{t7}{t9}</Box>; 97: $[14] = t5; 98: $[15] = t7; 99: $[16] = t10; 100: } else { 101: t10 = $[16]; 102: } 103: let t11; 104: if ($[17] !== onDone || $[18] !== t10) { 105: t11 = <Dialog title="Claude in Chrome (Beta)" onCancel={onDone} color="chromeYellow">{t10}</Dialog>; 106: $[17] = onDone; 107: $[18] = t10; 108: $[19] = t11; 109: } else { 110: t11 = $[19]; 111: } 112: return t11; 113: } 114: function _temp(current) { 115: return { 116: ...current, 117: hasCompletedClaudeInChromeOnboarding: true 118: }; 119: }

File: src/components/ClaudeMdExternalIncludesDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useCallback } from 'react'; 3: import { logEvent } from 'src/services/analytics/index.js'; 4: import { Box, Link, Text } from '../ink.js'; 5: import type { ExternalClaudeMdInclude } from '../utils/claudemd.js'; 6: import { saveCurrentProjectConfig } from '../utils/config.js'; 7: import { Select } from './CustomSelect/index.js'; 8: import { Dialog } from './design-system/Dialog.js'; 9: type Props = { 10: onDone(): void; 11: isStandaloneDialog?: boolean; 12: externalIncludes?: ExternalClaudeMdInclude[]; 13: }; 14: export function ClaudeMdExternalIncludesDialog(t0) { 15: const $ = _c(18); 16: const { 17: onDone, 18: isStandaloneDialog, 19: externalIncludes 20: } = t0; 21: let t1; 22: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 23: t1 = []; 24: $[0] = t1; 25: } else { 26: t1 = $[0]; 27: } 28: React.useEffect(_temp, t1); 29: let t2; 30: if ($[1] !== onDone) { 31: t2 = value => { 32: if (value === "no") { 33: logEvent("tengu_claude_md_external_includes_dialog_declined", {}); 34: saveCurrentProjectConfig(_temp2); 35: } else { 36: logEvent("tengu_claude_md_external_includes_dialog_accepted", {}); 37: saveCurrentProjectConfig(_temp3); 38: } 39: onDone(); 40: }; 41: $[1] = onDone; 42: $[2] = t2; 43: } else { 44: t2 = $[2]; 45: } 46: const handleSelection = t2; 47: let t3; 48: if ($[3] !== handleSelection) { 49: t3 = () => { 50: handleSelection("no"); 51: }; 52: $[3] = handleSelection; 53: $[4] = t3; 54: } else { 55: t3 = $[4]; 56: } 57: const handleEscape = t3; 58: const t4 = !isStandaloneDialog; 59: const t5 = !isStandaloneDialog; 60: let t6; 61: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 62: t6 = <Text>This project's CLAUDE.md imports files outside the current working directory. Never allow this for third-party repositories.</Text>; 63: $[5] = t6; 64: } else { 65: t6 = $[5]; 66: } 67: let t7; 68: if ($[6] !== externalIncludes) { 69: t7 = externalIncludes && externalIncludes.length > 0 && <Box flexDirection="column"><Text dimColor={true}>External imports:</Text>{externalIncludes.map(_temp4)}</Box>; 70: $[6] = externalIncludes; 71: $[7] = t7; 72: } else { 73: t7 = $[7]; 74: } 75: let t8; 76: if ($[8] === Symbol.for("react.memo_cache_sentinel")) { 77: t8 = <Text dimColor={true}>Important: Only use Claude Code with files you trust. Accessing untrusted files may pose security risks{" "}<Link url="https://code.claude.com/docs/en/security" />{" "}</Text>; 78: $[8] = t8; 79: } else { 80: t8 = $[8]; 81: } 82: let t9; 83: if ($[9] === Symbol.for("react.memo_cache_sentinel")) { 84: t9 = [{ 85: label: "Yes, allow external imports", 86: value: "yes" 87: }, { 88: label: "No, disable external imports", 89: value: "no" 90: }]; 91: $[9] = t9; 92: } else { 93: t9 = $[9]; 94: } 95: let t10; 96: if ($[10] !== handleSelection) { 97: t10 = <Select options={t9} onChange={value_0 => handleSelection(value_0 as 'yes' | 'no')} />; 98: $[10] = handleSelection; 99: $[11] = t10; 100: } else { 101: t10 = $[11]; 102: } 103: let t11; 104: if ($[12] !== handleEscape || $[13] !== t10 || $[14] !== t4 || $[15] !== t5 || $[16] !== t7) { 105: t11 = <Dialog title="Allow external CLAUDE.md file imports?" color="warning" onCancel={handleEscape} hideBorder={t4} hideInputGuide={t5}>{t6}{t7}{t8}{t10}</Dialog>; 106: $[12] = handleEscape; 107: $[13] = t10; 108: $[14] = t4; 109: $[15] = t5; 110: $[16] = t7; 111: $[17] = t11; 112: } else { 113: t11 = $[17]; 114: } 115: return t11; 116: } 117: function _temp4(include, i) { 118: return <Text key={i} dimColor={true}>{" "}{include.path}</Text>; 119: } 120: function _temp3(current_0) { 121: return { 122: ...current_0, 123: hasClaudeMdExternalIncludesApproved: true, 124: hasClaudeMdExternalIncludesWarningShown: true 125: }; 126: } 127: function _temp2(current) { 128: return { 129: ...current, 130: hasClaudeMdExternalIncludesApproved: false, 131: hasClaudeMdExternalIncludesWarningShown: true 132: }; 133: } 134: function _temp() { 135: logEvent("tengu_claude_md_includes_dialog_shown", {}); 136: }

File: src/components/ClickableImageRef.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { pathToFileURL } from 'url'; 4: import Link from '../ink/components/Link.js'; 5: import { supportsHyperlinks } from '../ink/supports-hyperlinks.js'; 6: import { Text } from '../ink.js'; 7: import { getStoredImagePath } from '../utils/imageStore.js'; 8: import type { Theme } from '../utils/theme.js'; 9: type Props = { 10: imageId: number; 11: backgroundColor?: keyof Theme; 12: isSelected?: boolean; 13: }; 14: export function ClickableImageRef(t0) { 15: const $ = _c(13); 16: const { 17: imageId, 18: backgroundColor, 19: isSelected: t1 20: } = t0; 21: const isSelected = t1 === undefined ? false : t1; 22: const imagePath = getStoredImagePath(imageId); 23: const displayText = `[Image #${imageId}]`; 24: if (imagePath && supportsHyperlinks()) { 25: const fileUrl = pathToFileURL(imagePath).href; 26: let t2; 27: let t3; 28: if ($[0] !== backgroundColor || $[1] !== displayText || $[2] !== isSelected) { 29: t2 = <Text backgroundColor={backgroundColor} inverse={isSelected}>{displayText}</Text>; 30: t3 = <Text backgroundColor={backgroundColor} inverse={isSelected} bold={isSelected}>{displayText}</Text>; 31: $[0] = backgroundColor; 32: $[1] = displayText; 33: $[2] = isSelected; 34: $[3] = t2; 35: $[4] = t3; 36: } else { 37: t2 = $[3]; 38: t3 = $[4]; 39: } 40: let t4; 41: if ($[5] !== fileUrl || $[6] !== t2 || $[7] !== t3) { 42: t4 = <Link url={fileUrl} fallback={t2}>{t3}</Link>; 43: $[5] = fileUrl; 44: $[6] = t2; 45: $[7] = t3; 46: $[8] = t4; 47: } else { 48: t4 = $[8]; 49: } 50: return t4; 51: } 52: let t2; 53: if ($[9] !== backgroundColor || $[10] !== displayText || $[11] !== isSelected) { 54: t2 = <Text backgroundColor={backgroundColor} inverse={isSelected}>{displayText}</Text>; 55: $[9] = backgroundColor; 56: $[10] = displayText; 57: $[11] = isSelected; 58: $[12] = t2; 59: } else { 60: t2 = $[12]; 61: } 62: return t2; 63: }

File: src/components/CompactSummary.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import { BLACK_CIRCLE } from '../constants/figures.js'; 4: import { Box, Text } from '../ink.js'; 5: import type { Screen } from '../screens/REPL.js'; 6: import type { NormalizedUserMessage } from '../types/message.js'; 7: import { getUserMessageText } from '../utils/messages.js'; 8: import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'; 9: import { MessageResponse } from './MessageResponse.js'; 10: type Props = { 11: message: NormalizedUserMessage; 12: screen: Screen; 13: }; 14: export function CompactSummary(t0) { 15: const $ = _c(24); 16: const { 17: message, 18: screen 19: } = t0; 20: const isTranscriptMode = screen === "transcript"; 21: let t1; 22: if ($[0] !== message) { 23: t1 = getUserMessageText(message) || ""; 24: $[0] = message; 25: $[1] = t1; 26: } else { 27: t1 = $[1]; 28: } 29: const textContent = t1; 30: const metadata = message.summarizeMetadata; 31: if (metadata) { 32: let t2; 33: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 34: t2 = <Box minWidth={2}><Text color="text">{BLACK_CIRCLE}</Text></Box>; 35: $[2] = t2; 36: } else { 37: t2 = $[2]; 38: } 39: let t3; 40: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 41: t3 = <Text bold={true}>Summarized conversation</Text>; 42: $[3] = t3; 43: } else { 44: t3 = $[3]; 45: } 46: let t4; 47: if ($[4] !== isTranscriptMode || $[5] !== metadata) { 48: t4 = !isTranscriptMode && <MessageResponse><Box flexDirection="column"><Text dimColor={true}>Summarized {metadata.messagesSummarized} messages{" "}{metadata.direction === "up_to" ? "up to this point" : "from this point"}</Text>{metadata.userContext && <Text dimColor={true}>Context: {"\u201C"}{metadata.userContext}{"\u201D"}</Text>}<Text dimColor={true}><ConfigurableShortcutHint action="app:toggleTranscript" context="Global" fallback="ctrl+o" description="expand history" parens={true} /></Text></Box></MessageResponse>; 49: $[4] = isTranscriptMode; 50: $[5] = metadata; 51: $[6] = t4; 52: } else { 53: t4 = $[6]; 54: } 55: let t5; 56: if ($[7] !== isTranscriptMode || $[8] !== textContent) { 57: t5 = isTranscriptMode && <MessageResponse><Text>{textContent}</Text></MessageResponse>; 58: $[7] = isTranscriptMode; 59: $[8] = textContent; 60: $[9] = t5; 61: } else { 62: t5 = $[9]; 63: } 64: let t6; 65: if ($[10] !== t4 || $[11] !== t5) { 66: t6 = <Box flexDirection="column" marginTop={1}><Box flexDirection="row">{t2}<Box flexDirection="column">{t3}{t4}{t5}</Box></Box></Box>; 67: $[10] = t4; 68: $[11] = t5; 69: $[12] = t6; 70: } else { 71: t6 = $[12]; 72: } 73: return t6; 74: } 75: let t2; 76: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 77: t2 = <Box minWidth={2}><Text color="text">{BLACK_CIRCLE}</Text></Box>; 78: $[13] = t2; 79: } else { 80: t2 = $[13]; 81: } 82: let t3; 83: if ($[14] !== isTranscriptMode) { 84: t3 = !isTranscriptMode && <Text dimColor={true}>{" "}<ConfigurableShortcutHint action="app:toggleTranscript" context="Global" fallback="ctrl+o" description="expand" parens={true} /></Text>; 85: $[14] = isTranscriptMode; 86: $[15] = t3; 87: } else { 88: t3 = $[15]; 89: } 90: let t4; 91: if ($[16] !== t3) { 92: t4 = <Box flexDirection="row">{t2}<Box flexDirection="column"><Text bold={true}>Compact summary{t3}</Text></Box></Box>; 93: $[16] = t3; 94: $[17] = t4; 95: } else { 96: t4 = $[17]; 97: } 98: let t5; 99: if ($[18] !== isTranscriptMode || $[19] !== textContent) { 100: t5 = isTranscriptMode && <MessageResponse><Text>{textContent}</Text></MessageResponse>; 101: $[18] = isTranscriptMode; 102: $[19] = textContent; 103: $[20] = t5; 104: } else { 105: t5 = $[20]; 106: } 107: let t6; 108: if ($[21] !== t4 || $[22] !== t5) { 109: t6 = <Box flexDirection="column" marginTop={1}>{t4}{t5}</Box>; 110: $[21] = t4; 111: $[22] = t5; 112: $[23] = t6; 113: } else { 114: t6 = $[23]; 115: } 116: return t6; 117: }

File: src/components/ConfigurableShortcutHint.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import * as React from 'react'; 3: import type { KeybindingAction, KeybindingContextName } from '../keybindings/types.js'; 4: import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js'; 5: import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; 6: type Props = { 7: action: KeybindingAction; 8: context: KeybindingContextName; 9: fallback: string; 10: description: string; 11: parens?: boolean; 12: bold?: boolean; 13: }; 14: export function ConfigurableShortcutHint(t0) { 15: const $ = _c(5); 16: const { 17: action, 18: context, 19: fallback, 20: description, 21: parens, 22: bold 23: } = t0; 24: const shortcut = useShortcutDisplay(action, context, fallback); 25: let t1; 26: if ($[0] !== bold || $[1] !== description || $[2] !== parens || $[3] !== shortcut) { 27: t1 = <KeyboardShortcutHint shortcut={shortcut} action={description} parens={parens} bold={bold} />; 28: $[0] = bold; 29: $[1] = description; 30: $[2] = parens; 31: $[3] = shortcut; 32: $[4] = t1; 33: } else { 34: t1 = $[4]; 35: } 36: return t1; 37: }

File: src/components/ConsoleOAuthFlow.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React, { useCallback, useEffect, useRef, useState } from 'react'; 3: import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; 4: import { installOAuthTokens } from '../cli/handlers/auth.js'; 5: import { useTerminalSize } from '../hooks/useTerminalSize.js'; 6: import { setClipboard } from '../ink/termio/osc.js'; 7: import { useTerminalNotification } from '../ink/useTerminalNotification.js'; 8: import { Box, Link, Text } from '../ink.js'; 9: import { useKeybinding } from '../keybindings/useKeybinding.js'; 10: import { getSSLErrorHint } from '../services/api/errorUtils.js'; 11: import { sendNotification } from '../services/notifier.js'; 12: import { OAuthService } from '../services/oauth/index.js'; 13: import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js'; 14: import { logError } from '../utils/log.js'; 15: import { getSettings_DEPRECATED } from '../utils/settings/settings.js'; 16: import { Select } from './CustomSelect/select.js'; 17: import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; 18: import { Spinner } from './Spinner.js'; 19: import TextInput from './TextInput.js'; 20: type Props = { 21: onDone(): void; 22: startingMessage?: string; 23: mode?: 'login' | 'setup-token'; 24: forceLoginMethod?: 'claudeai' | 'console'; 25: }; 26: type OAuthStatus = { 27: state: 'idle'; 28: } 29: | { 30: state: 'platform_setup'; 31: } 32: | { 33: state: 'ready_to_start'; 34: } 35: | { 36: state: 'waiting_for_login'; 37: url: string; 38: } 39: | { 40: state: 'creating_api_key'; 41: } 42: | { 43: state: 'about_to_retry'; 44: nextState: OAuthStatus; 45: } | { 46: state: 'success'; 47: token?: string; 48: } | { 49: state: 'error'; 50: message: string; 51: toRetry?: OAuthStatus; 52: }; 53: const PASTE_HERE_MSG = 'Paste code here if prompted > '; 54: export function ConsoleOAuthFlow({ 55: onDone, 56: startingMessage, 57: mode = 'login', 58: forceLoginMethod: forceLoginMethodProp 59: }: Props): React.ReactNode { 60: const settings = getSettings_DEPRECATED() || {}; 61: const forceLoginMethod = forceLoginMethodProp ?? settings.forceLoginMethod; 62: const orgUUID = settings.forceLoginOrgUUID; 63: const forcedMethodMessage = forceLoginMethod === 'claudeai' ? 'Login method pre-selected: Subscription Plan (Claude Pro/Max)' : forceLoginMethod === 'console' ? 'Login method pre-selected: API Usage Billing (Anthropic Console)' : null; 64: const terminal = useTerminalNotification(); 65: const [oauthStatus, setOAuthStatus] = useState<OAuthStatus>(() => { 66: if (mode === 'setup-token') { 67: return { 68: state: 'ready_to_start' 69: }; 70: } 71: if (forceLoginMethod === 'claudeai' || forceLoginMethod === 'console') { 72: return { 73: state: 'ready_to_start' 74: }; 75: } 76: return { 77: state: 'idle' 78: }; 79: }); 80: const [pastedCode, setPastedCode] = useState(''); 81: const [cursorOffset, setCursorOffset] = useState(0); 82: const [oauthService] = useState(() => new OAuthService()); 83: const [loginWithClaudeAi, setLoginWithClaudeAi] = useState(() => { 84: // Use Claude AI auth for setup-token mode to support user:inference scope 85: return mode === 'setup-token' || forceLoginMethod === 'claudeai'; 86: }); 87: const [showPastePrompt, setShowPastePrompt] = useState(false); 88: const [urlCopied, setUrlCopied] = useState(false); 89: const textInputColumns = useTerminalSize().columns - PASTE_HERE_MSG.length - 1; 90: useEffect(() => { 91: if (forceLoginMethod === 'claudeai') { 92: logEvent('tengu_oauth_claudeai_forced', {}); 93: } else if (forceLoginMethod === 'console') { 94: logEvent('tengu_oauth_console_forced', {}); 95: } 96: }, [forceLoginMethod]); 97: useEffect(() => { 98: if (oauthStatus.state === 'about_to_retry') { 99: const timer = setTimeout(setOAuthStatus, 1000, oauthStatus.nextState); 100: return () => clearTimeout(timer); 101: } 102: }, [oauthStatus]); 103: useKeybinding('confirm:yes', () => { 104: logEvent('tengu_oauth_success', { 105: loginWithClaudeAi 106: }); 107: onDone(); 108: }, { 109: context: 'Confirmation', 110: isActive: oauthStatus.state === 'success' && mode !== 'setup-token' 111: }); 112: useKeybinding('confirm:yes', () => { 113: setOAuthStatus({ 114: state: 'idle' 115: }); 116: }, { 117: context: 'Confirmation', 118: isActive: oauthStatus.state === 'platform_setup' 119: }); 120: useKeybinding('confirm:yes', () => { 121: if (oauthStatus.state === 'error' && oauthStatus.toRetry) { 122: setPastedCode(''); 123: setOAuthStatus({ 124: state: 'about_to_retry', 125: nextState: oauthStatus.toRetry 126: }); 127: } 128: }, { 129: context: 'Confirmation', 130: isActive: oauthStatus.state === 'error' && !!oauthStatus.toRetry 131: }); 132: useEffect(() => { 133: if (pastedCode === 'c' && oauthStatus.state === 'waiting_for_login' && showPastePrompt && !urlCopied) { 134: void setClipboard(oauthStatus.url).then(raw => { 135: if (raw) process.stdout.write(raw); 136: setUrlCopied(true); 137: setTimeout(setUrlCopied, 2000, false); 138: }); 139: setPastedCode(''); 140: } 141: }, [pastedCode, oauthStatus, showPastePrompt, urlCopied]); 142: async function handleSubmitCode(value: string, url: string) { 143: try { 144: // Expecting format "authorizationCode#state" from the authorization callback URL 145: const [authorizationCode, state] = value.split('#'); 146: if (!authorizationCode || !state) { 147: setOAuthStatus({ 148: state: 'error', 149: message: 'Invalid code. Please make sure the full code was copied', 150: toRetry: { 151: state: 'waiting_for_login', 152: url 153: } 154: }); 155: return; 156: } 157: logEvent('tengu_oauth_manual_entry', {}); 158: oauthService.handleManualAuthCodeInput({ 159: authorizationCode, 160: state 161: }); 162: } catch (err: unknown) { 163: logError(err); 164: setOAuthStatus({ 165: state: 'error', 166: message: (err as Error).message, 167: toRetry: { 168: state: 'waiting_for_login', 169: url 170: } 171: }); 172: } 173: } 174: const startOAuth = useCallback(async () => { 175: try { 176: logEvent('tengu_oauth_flow_start', { 177: loginWithClaudeAi 178: }); 179: const result = await oauthService.startOAuthFlow(async url_0 => { 180: setOAuthStatus({ 181: state: 'waiting_for_login', 182: url: url_0 183: }); 184: setTimeout(setShowPastePrompt, 3000, true); 185: }, { 186: loginWithClaudeAi, 187: inferenceOnly: mode === 'setup-token', 188: expiresIn: mode === 'setup-token' ? 365 * 24 * 60 * 60 : undefined, 189: orgUUID 190: }).catch(err_1 => { 191: const isTokenExchangeError = err_1.message.includes('Token exchange failed'); 192: const sslHint_0 = getSSLErrorHint(err_1); 193: setOAuthStatus({ 194: state: 'error', 195: message: sslHint_0 ?? (isTokenExchangeError ? 'Failed to exchange authorization code for access token. Please try again.' : err_1.message), 196: toRetry: mode === 'setup-token' ? { 197: state: 'ready_to_start' 198: } : { 199: state: 'idle' 200: } 201: }); 202: logEvent('tengu_oauth_token_exchange_error', { 203: error: err_1.message, 204: ssl_error: sslHint_0 !== null 205: }); 206: throw err_1; 207: }); 208: if (mode === 'setup-token') { 209: setOAuthStatus({ 210: state: 'success', 211: token: result.accessToken 212: }); 213: } else { 214: await installOAuthTokens(result); 215: const orgResult = await validateForceLoginOrg(); 216: if (!orgResult.valid) { 217: throw new Error(orgResult.message); 218: } 219: setOAuthStatus({ 220: state: 'success' 221: }); 222: void sendNotification({ 223: message: 'Claude Code login successful', 224: notificationType: 'auth_success' 225: }, terminal); 226: } 227: } catch (err_0) { 228: const errorMessage = (err_0 as Error).message; 229: const sslHint = getSSLErrorHint(err_0); 230: setOAuthStatus({ 231: state: 'error', 232: message: sslHint ?? errorMessage, 233: toRetry: { 234: state: mode === 'setup-token' ? 'ready_to_start' : 'idle' 235: } 236: }); 237: logEvent('tengu_oauth_error', { 238: error: errorMessage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 239: ssl_error: sslHint !== null 240: }); 241: } 242: }, [oauthService, setShowPastePrompt, loginWithClaudeAi, mode, orgUUID]); 243: const pendingOAuthStartRef = useRef(false); 244: useEffect(() => { 245: if (oauthStatus.state === 'ready_to_start' && !pendingOAuthStartRef.current) { 246: pendingOAuthStartRef.current = true; 247: process.nextTick((startOAuth_0: () => Promise<void>, pendingOAuthStartRef_0: React.MutableRefObject<boolean>) => { 248: void startOAuth_0(); 249: pendingOAuthStartRef_0.current = false; 250: }, startOAuth, pendingOAuthStartRef); 251: } 252: }, [oauthStatus.state, startOAuth]); 253: useEffect(() => { 254: if (mode === 'setup-token' && oauthStatus.state === 'success') { 255: const timer_0 = setTimeout((loginWithClaudeAi_0, onDone_0) => { 256: logEvent('tengu_oauth_success', { 257: loginWithClaudeAi: loginWithClaudeAi_0 258: }); 259: onDone_0(); 260: }, 500, loginWithClaudeAi, onDone); 261: return () => clearTimeout(timer_0); 262: } 263: }, [mode, oauthStatus, loginWithClaudeAi, onDone]); 264: useEffect(() => { 265: return () => { 266: oauthService.cleanup(); 267: }; 268: }, [oauthService]); 269: return <Box flexDirection="column" gap={1}> 270: {oauthStatus.state === 'waiting_for_login' && showPastePrompt && <Box flexDirection="column" key="urlToCopy" gap={1} paddingBottom={1}> 271: <Box paddingX={1}> 272: <Text dimColor> 273: Browser didn&apos;t open? Use the url below to sign in{' '} 274: </Text> 275: {urlCopied ? <Text color="success">(Copied!)</Text> : <Text dimColor> 276: <KeyboardShortcutHint shortcut="c" action="copy" parens /> 277: </Text>} 278: </Box> 279: <Link url={oauthStatus.url}> 280: <Text dimColor>{oauthStatus.url}</Text> 281: </Link> 282: </Box>} 283: {mode === 'setup-token' && oauthStatus.state === 'success' && oauthStatus.token && <Box key="tokenOutput" flexDirection="column" gap={1} paddingTop={1}> 284: <Text color="success"> 285: ✓ Long-lived authentication token created successfully! 286: </Text> 287: <Box flexDirection="column" gap={1}> 288: <Text>Your OAuth token (valid for 1 year):</Text> 289: <Text color="warning">{oauthStatus.token}</Text> 290: <Text dimColor> 291: Store this token securely. You won&apos;t be able to see it 292: again. 293: </Text> 294: <Text dimColor> 295: Use this token by setting: export 296: CLAUDE_CODE_OAUTH_TOKEN=&lt;token&gt; 297: </Text> 298: </Box> 299: </Box>} 300: <Box paddingLeft={1} flexDirection="column" gap={1}> 301: <OAuthStatusMessage oauthStatus={oauthStatus} mode={mode} startingMessage={startingMessage} forcedMethodMessage={forcedMethodMessage} showPastePrompt={showPastePrompt} pastedCode={pastedCode} setPastedCode={setPastedCode} cursorOffset={cursorOffset} setCursorOffset={setCursorOffset} textInputColumns={textInputColumns} handleSubmitCode={handleSubmitCode} setOAuthStatus={setOAuthStatus} setLoginWithClaudeAi={setLoginWithClaudeAi} /> 302: </Box> 303: </Box>; 304: } 305: type OAuthStatusMessageProps = { 306: oauthStatus: OAuthStatus; 307: mode: 'login' | 'setup-token'; 308: startingMessage: string | undefined; 309: forcedMethodMessage: string | null; 310: showPastePrompt: boolean; 311: pastedCode: string; 312: setPastedCode: (value: string) => void; 313: cursorOffset: number; 314: setCursorOffset: (offset: number) => void; 315: textInputColumns: number; 316: handleSubmitCode: (value: string, url: string) => void; 317: setOAuthStatus: (status: OAuthStatus) => void; 318: setLoginWithClaudeAi: (value: boolean) => void; 319: }; 320: function OAuthStatusMessage(t0) { 321: const $ = _c(51); 322: const { 323: oauthStatus, 324: mode, 325: startingMessage, 326: forcedMethodMessage, 327: showPastePrompt, 328: pastedCode, 329: setPastedCode, 330: cursorOffset, 331: setCursorOffset, 332: textInputColumns, 333: handleSubmitCode, 334: setOAuthStatus, 335: setLoginWithClaudeAi 336: } = t0; 337: switch (oauthStatus.state) { 338: case "idle": 339: { 340: const t1 = startingMessage ? startingMessage : "Claude Code can be used with your Claude subscription or billed based on API usage through your Console account."; 341: let t2; 342: if ($[0] !== t1) { 343: t2 = <Text bold={true}>{t1}</Text>; 344: $[0] = t1; 345: $[1] = t2; 346: } else { 347: t2 = $[1]; 348: } 349: let t3; 350: if ($[2] === Symbol.for("react.memo_cache_sentinel")) { 351: t3 = <Text>Select login method:</Text>; 352: $[2] = t3; 353: } else { 354: t3 = $[2]; 355: } 356: let t4; 357: if ($[3] === Symbol.for("react.memo_cache_sentinel")) { 358: t4 = { 359: label: <Text>Claude account with subscription ·{" "}<Text dimColor={true}>Pro, Max, Team, or Enterprise</Text>{false && <Text>{"\n"}<Text color="warning">[ANT-ONLY]</Text>{" "}<Text dimColor={true}>Please use this option unless you need to login to a special org for accessing sensitive data (e.g. customer data, HIPI data) with the Console option</Text></Text>}{"\n"}</Text>, 360: value: "claudeai" 361: }; 362: $[3] = t4; 363: } else { 364: t4 = $[3]; 365: } 366: let t5; 367: if ($[4] === Symbol.for("react.memo_cache_sentinel")) { 368: t5 = { 369: label: <Text>Anthropic Console account ·{" "}<Text dimColor={true}>API usage billing</Text>{"\n"}</Text>, 370: value: "console" 371: }; 372: $[4] = t5; 373: } else { 374: t5 = $[4]; 375: } 376: let t6; 377: if ($[5] === Symbol.for("react.memo_cache_sentinel")) { 378: t6 = [t4, t5, { 379: label: <Text>3rd-party platform ·{" "}<Text dimColor={true}>Amazon Bedrock, Microsoft Foundry, or Vertex AI</Text>{"\n"}</Text>, 380: value: "platform" 381: }]; 382: $[5] = t6; 383: } else { 384: t6 = $[5]; 385: } 386: let t7; 387: if ($[6] !== setLoginWithClaudeAi || $[7] !== setOAuthStatus) { 388: t7 = <Box><Select options={t6} onChange={value_0 => { 389: if (value_0 === "platform") { 390: logEvent("tengu_oauth_platform_selected", {}); 391: setOAuthStatus({ 392: state: "platform_setup" 393: }); 394: } else { 395: setOAuthStatus({ 396: state: "ready_to_start" 397: }); 398: if (value_0 === "claudeai") { 399: logEvent("tengu_oauth_claudeai_selected", {}); 400: setLoginWithClaudeAi(true); 401: } else { 402: logEvent("tengu_oauth_console_selected", {}); 403: setLoginWithClaudeAi(false); 404: } 405: } 406: }} /></Box>; 407: $[6] = setLoginWithClaudeAi; 408: $[7] = setOAuthStatus; 409: $[8] = t7; 410: } else { 411: t7 = $[8]; 412: } 413: let t8; 414: if ($[9] !== t2 || $[10] !== t7) { 415: t8 = <Box flexDirection="column" gap={1} marginTop={1}>{t2}{t3}{t7}</Box>; 416: $[9] = t2; 417: $[10] = t7; 418: $[11] = t8; 419: } else { 420: t8 = $[11]; 421: } 422: return t8; 423: } 424: case "platform_setup": 425: { 426: let t1; 427: if ($[12] === Symbol.for("react.memo_cache_sentinel")) { 428: t1 = <Text bold={true}>Using 3rd-party platforms</Text>; 429: $[12] = t1; 430: } else { 431: t1 = $[12]; 432: } 433: let t2; 434: let t3; 435: if ($[13] === Symbol.for("react.memo_cache_sentinel")) { 436: t2 = <Text>Claude Code supports Amazon Bedrock, Microsoft Foundry, and Vertex AI. Set the required environment variables, then restart Claude Code.</Text>; 437: t3 = <Text>If you are part of an enterprise organization, contact your administrator for setup instructions.</Text>; 438: $[13] = t2; 439: $[14] = t3; 440: } else { 441: t2 = $[13]; 442: t3 = $[14]; 443: } 444: let t4; 445: if ($[15] === Symbol.for("react.memo_cache_sentinel")) { 446: t4 = <Text bold={true}>Documentation:</Text>; 447: $[15] = t4; 448: } else { 449: t4 = $[15]; 450: } 451: let t5; 452: if ($[16] === Symbol.for("react.memo_cache_sentinel")) { 453: t5 = <Text>· Amazon Bedrock:{" "}<Link url="https://code.claude.com/docs/en/amazon-bedrock">https: 454: $[16] = t5; 455: } else { 456: t5 = $[16]; 457: } 458: let t6; 459: if ($[17] === Symbol.for("react.memo_cache_sentinel")) { 460: t6 = <Text>· Microsoft Foundry:{" "}<Link url="https://code.claude.com/docs/en/microsoft-foundry">https: 461: $[17] = t6; 462: } else { 463: t6 = $[17]; 464: } 465: let t7; 466: if ($[18] === Symbol.for("react.memo_cache_sentinel")) { 467: t7 = <Box flexDirection="column" marginTop={1}>{t4}{t5}{t6}<Text>· Vertex AI:{" "}<Link url="https://code.claude.com/docs/en/google-vertex-ai">https: 468: $[18] = t7; 469: } else { 470: t7 = $[18]; 471: } 472: let t8; 473: if ($[19] === Symbol.for("react.memo_cache_sentinel")) { 474: t8 = <Box flexDirection="column" gap={1} marginTop={1}>{t1}<Box flexDirection="column" gap={1}>{t2}{t3}{t7}<Box marginTop={1}><Text dimColor={true}>Press <Text bold={true}>Enter</Text> to go back to login options.</Text></Box></Box></Box>; 475: $[19] = t8; 476: } else { 477: t8 = $[19]; 478: } 479: return t8; 480: } 481: case "waiting_for_login": 482: { 483: let t1; 484: if ($[20] !== forcedMethodMessage) { 485: t1 = forcedMethodMessage && <Box><Text dimColor={true}>{forcedMethodMessage}</Text></Box>; 486: $[20] = forcedMethodMessage; 487: $[21] = t1; 488: } else { 489: t1 = $[21]; 490: } 491: let t2; 492: if ($[22] !== showPastePrompt) { 493: t2 = !showPastePrompt && <Box><Spinner /><Text>Opening browser to sign in…</Text></Box>; 494: $[22] = showPastePrompt; 495: $[23] = t2; 496: } else { 497: t2 = $[23]; 498: } 499: let t3; 500: if ($[24] !== cursorOffset || $[25] !== handleSubmitCode || $[26] !== oauthStatus.url || $[27] !== pastedCode || $[28] !== setCursorOffset || $[29] !== setPastedCode || $[30] !== showPastePrompt || $[31] !== textInputColumns) { 501: t3 = showPastePrompt && <Box><Text>{PASTE_HERE_MSG}</Text><TextInput value={pastedCode} onChange={setPastedCode} onSubmit={value => handleSubmitCode(value, oauthStatus.url)} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} columns={textInputColumns} mask="*" /></Box>; 502: $[24] = cursorOffset; 503: $[25] = handleSubmitCode; 504: $[26] = oauthStatus.url; 505: $[27] = pastedCode; 506: $[28] = setCursorOffset; 507: $[29] = setPastedCode; 508: $[30] = showPastePrompt; 509: $[31] = textInputColumns; 510: $[32] = t3; 511: } else { 512: t3 = $[32]; 513: } 514: let t4; 515: if ($[33] !== t1 || $[34] !== t2 || $[35] !== t3) { 516: t4 = <Box flexDirection="column" gap={1}>{t1}{t2}{t3}</Box>; 517: $[33] = t1; 518: $[34] = t2; 519: $[35] = t3; 520: $[36] = t4; 521: } else { 522: t4 = $[36]; 523: } 524: return t4; 525: } 526: case "creating_api_key": 527: { 528: let t1; 529: if ($[37] === Symbol.for("react.memo_cache_sentinel")) { 530: t1 = <Box flexDirection="column" gap={1}><Box><Spinner /><Text>Creating API key for Claude Code…</Text></Box></Box>; 531: $[37] = t1; 532: } else { 533: t1 = $[37]; 534: } 535: return t1; 536: } 537: case "about_to_retry": 538: { 539: let t1; 540: if ($[38] === Symbol.for("react.memo_cache_sentinel")) { 541: t1 = <Box flexDirection="column" gap={1}><Text color="permission">Retrying…</Text></Box>; 542: $[38] = t1; 543: } else { 544: t1 = $[38]; 545: } 546: return t1; 547: } 548: case "success": 549: { 550: let t1; 551: if ($[39] !== mode || $[40] !== oauthStatus.token) { 552: t1 = mode === "setup-token" && oauthStatus.token ? null : <>{getOauthAccountInfo()?.emailAddress ? <Text dimColor={true}>Logged in as{" "}<Text>{getOauthAccountInfo()?.emailAddress}</Text></Text> : null}<Text color="success">Login successful. Press <Text bold={true}>Enter</Text> to continue…</Text></>; 553: $[39] = mode; 554: $[40] = oauthStatus.token; 555: $[41] = t1; 556: } else { 557: t1 = $[41]; 558: } 559: let t2; 560: if ($[42] !== t1) { 561: t2 = <Box flexDirection="column">{t1}</Box>; 562: $[42] = t1; 563: $[43] = t2; 564: } else { 565: t2 = $[43]; 566: } 567: return t2; 568: } 569: case "error": 570: { 571: let t1; 572: if ($[44] !== oauthStatus.message) { 573: t1 = <Text color="error">OAuth error: {oauthStatus.message}</Text>; 574: $[44] = oauthStatus.message; 575: $[45] = t1; 576: } else { 577: t1 = $[45]; 578: } 579: let t2; 580: if ($[46] !== oauthStatus.toRetry) { 581: t2 = oauthStatus.toRetry && <Box marginTop={1}><Text color="permission">Press <Text bold={true}>Enter</Text> to retry.</Text></Box>; 582: $[46] = oauthStatus.toRetry; 583: $[47] = t2; 584: } else { 585: t2 = $[47]; 586: } 587: let t3; 588: if ($[48] !== t1 || $[49] !== t2) { 589: t3 = <Box flexDirection="column" gap={1}>{t1}{t2}</Box>; 590: $[48] = t1; 591: $[49] = t2; 592: $[50] = t3; 593: } else { 594: t3 = $[50]; 595: } 596: return t3; 597: } 598: default: 599: { 600: return null; 601: } 602: } 603: }

File: src/components/ContextSuggestions.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { Box, Text } from '../ink.js'; 5: import type { ContextSuggestion } from '../utils/contextSuggestions.js'; 6: import { formatTokens } from '../utils/format.js'; 7: import { StatusIcon } from './design-system/StatusIcon.js'; 8: type Props = { 9: suggestions: ContextSuggestion[]; 10: }; 11: export function ContextSuggestions(t0) { 12: const $ = _c(5); 13: const { 14: suggestions 15: } = t0; 16: if (suggestions.length === 0) { 17: return null; 18: } 19: let t1; 20: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 21: t1 = <Text bold={true}>Suggestions</Text>; 22: $[0] = t1; 23: } else { 24: t1 = $[0]; 25: } 26: let t2; 27: if ($[1] !== suggestions) { 28: t2 = suggestions.map(_temp); 29: $[1] = suggestions; 30: $[2] = t2; 31: } else { 32: t2 = $[2]; 33: } 34: let t3; 35: if ($[3] !== t2) { 36: t3 = <Box flexDirection="column" marginTop={1}>{t1}{t2}</Box>; 37: $[3] = t2; 38: $[4] = t3; 39: } else { 40: t3 = $[4]; 41: } 42: return t3; 43: } 44: function _temp(suggestion, i) { 45: return <Box key={i} flexDirection="column" marginTop={i === 0 ? 0 : 1}><Box><StatusIcon status={suggestion.severity} withSpace={true} /><Text bold={true}>{suggestion.title}</Text>{suggestion.savingsTokens ? <Text dimColor={true}>{" "}{figures.arrowRight} save ~{formatTokens(suggestion.savingsTokens)}</Text> : null}</Box><Box marginLeft={2}><Text dimColor={true}>{suggestion.detail}</Text></Box></Box>; 46: }

File: src/components/ContextVisualization.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import { feature } from 'bun:bundle'; 3: import * as React from 'react'; 4: import { Box, Text } from '../ink.js'; 5: import type { ContextData } from '../utils/analyzeContext.js'; 6: import { generateContextSuggestions } from '../utils/contextSuggestions.js'; 7: import { getDisplayPath } from '../utils/file.js'; 8: import { formatTokens } from '../utils/format.js'; 9: import { getSourceDisplayName, type SettingSource } from '../utils/settings/constants.js'; 10: import { plural } from '../utils/stringUtils.js'; 11: import { ContextSuggestions } from './ContextSuggestions.js'; 12: const RESERVED_CATEGORY_NAME = 'Autocompact buffer'; 13: function CollapseStatus() { 14: const $ = _c(2); 15: if (feature("CONTEXT_COLLAPSE")) { 16: let t0; 17: let t1; 18: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 19: t1 = Symbol.for("react.early_return_sentinel"); 20: bb0: { 21: const { 22: getStats, 23: isContextCollapseEnabled 24: } = require("../services/contextCollapse/index.js") as typeof import('../services/contextCollapse/index.js'); 25: if (!isContextCollapseEnabled()) { 26: t1 = null; 27: break bb0; 28: } 29: const s = getStats(); 30: const { 31: health: h 32: } = s; 33: const parts = []; 34: if (s.collapsedSpans > 0) { 35: parts.push(`${s.collapsedSpans} ${plural(s.collapsedSpans, "span")} summarized (${s.collapsedMessages} msgs)`); 36: } 37: if (s.stagedSpans > 0) { 38: parts.push(`${s.stagedSpans} staged`); 39: } 40: const summary = parts.length > 0 ? parts.join(", ") : h.totalSpawns > 0 ? `${h.totalSpawns} ${plural(h.totalSpawns, "spawn")}, nothing staged yet` : "waiting for first trigger"; 41: let line2 = null; 42: if (h.totalErrors > 0) { 43: line2 = <Text color="warning">Collapse errors: {h.totalErrors}/{h.totalSpawns} spawns failed{h.lastError ? ` (last: ${h.lastError.slice(0, 60)})` : ""}</Text>; 44: } else { 45: if (h.emptySpawnWarningEmitted) { 46: line2 = <Text color="warning">Collapse idle: {h.totalEmptySpawns} consecutive empty runs</Text>; 47: } 48: } 49: t0 = <><Text dimColor={true}>Context strategy: collapse ({summary})</Text>{line2}</>; 50: } 51: $[0] = t0; 52: $[1] = t1; 53: } else { 54: t0 = $[0]; 55: t1 = $[1]; 56: } 57: if (t1 !== Symbol.for("react.early_return_sentinel")) { 58: return t1; 59: } 60: return t0; 61: } 62: return null; 63: } 64: const SOURCE_DISPLAY_ORDER = ['Project', 'User', 'Managed', 'Plugin', 'Built-in']; 65: function groupBySource<T extends { 66: source: SettingSource | 'plugin' | 'built-in'; 67: tokens: number; 68: }>(items: T[]): Map<string, T[]> { 69: const groups = new Map<string, T[]>(); 70: for (const item of items) { 71: const key = getSourceDisplayName(item.source); 72: const existing = groups.get(key) || []; 73: existing.push(item); 74: groups.set(key, existing); 75: } 76: for (const [key, group] of groups.entries()) { 77: groups.set(key, group.sort((a, b) => b.tokens - a.tokens)); 78: } 79: const orderedGroups = new Map<string, T[]>(); 80: for (const source of SOURCE_DISPLAY_ORDER) { 81: const group = groups.get(source); 82: if (group) { 83: orderedGroups.set(source, group); 84: } 85: } 86: return orderedGroups; 87: } 88: interface Props { 89: data: ContextData; 90: } 91: export function ContextVisualization(t0) { 92: const $ = _c(87); 93: const { 94: data 95: } = t0; 96: const { 97: categories, 98: totalTokens, 99: rawMaxTokens, 100: percentage, 101: gridRows, 102: model, 103: memoryFiles, 104: mcpTools, 105: deferredBuiltinTools: t1, 106: systemTools, 107: systemPromptSections, 108: agents, 109: skills, 110: messageBreakdown 111: } = data; 112: let T0; 113: let T1; 114: let t2; 115: let t3; 116: let t4; 117: let t5; 118: let t6; 119: let t7; 120: let t8; 121: let t9; 122: if ($[0] !== categories || $[1] !== gridRows || $[2] !== mcpTools || $[3] !== model || $[4] !== percentage || $[5] !== rawMaxTokens || $[6] !== systemTools || $[7] !== t1 || $[8] !== totalTokens) { 123: const deferredBuiltinTools = t1 === undefined ? [] : t1; 124: const visibleCategories = categories.filter(_temp); 125: let t10; 126: if ($[19] !== categories) { 127: t10 = categories.some(_temp2); 128: $[19] = categories; 129: $[20] = t10; 130: } else { 131: t10 = $[20]; 132: } 133: const hasDeferredMcpTools = t10; 134: const hasDeferredBuiltinTools = deferredBuiltinTools.length > 0; 135: const autocompactCategory = categories.find(_temp3); 136: T1 = Box; 137: t6 = "column"; 138: t7 = 1; 139: if ($[21] === Symbol.for("react.memo_cache_sentinel")) { 140: t8 = <Text bold={true}>Context Usage</Text>; 141: $[21] = t8; 142: } else { 143: t8 = $[21]; 144: } 145: let t11; 146: if ($[22] !== gridRows) { 147: t11 = gridRows.map(_temp5); 148: $[22] = gridRows; 149: $[23] = t11; 150: } else { 151: t11 = $[23]; 152: } 153: let t12; 154: if ($[24] !== t11) { 155: t12 = <Box flexDirection="column" flexShrink={0}>{t11}</Box>; 156: $[24] = t11; 157: $[25] = t12; 158: } else { 159: t12 = $[25]; 160: } 161: let t13; 162: if ($[26] !== totalTokens) { 163: t13 = formatTokens(totalTokens); 164: $[26] = totalTokens; 165: $[27] = t13; 166: } else { 167: t13 = $[27]; 168: } 169: let t14; 170: if ($[28] !== rawMaxTokens) { 171: t14 = formatTokens(rawMaxTokens); 172: $[28] = rawMaxTokens; 173: $[29] = t14; 174: } else { 175: t14 = $[29]; 176: } 177: let t15; 178: if ($[30] !== model || $[31] !== percentage || $[32] !== t13 || $[33] !== t14) { 179: t15 = <Text dimColor={true}>{model} · {t13}/{t14}{" "}tokens ({percentage}%)</Text>; 180: $[30] = model; 181: $[31] = percentage; 182: $[32] = t13; 183: $[33] = t14; 184: $[34] = t15; 185: } else { 186: t15 = $[34]; 187: } 188: let t16; 189: let t17; 190: let t18; 191: if ($[35] === Symbol.for("react.memo_cache_sentinel")) { 192: t16 = <CollapseStatus />; 193: t17 = <Text> </Text>; 194: t18 = <Text dimColor={true} italic={true}>Estimated usage by category</Text>; 195: $[35] = t16; 196: $[36] = t17; 197: $[37] = t18; 198: } else { 199: t16 = $[35]; 200: t17 = $[36]; 201: t18 = $[37]; 202: } 203: let t19; 204: if ($[38] !== rawMaxTokens) { 205: t19 = (cat_2, index) => { 206: const tokenDisplay = formatTokens(cat_2.tokens); 207: const percentDisplay = cat_2.isDeferred ? "N/A" : `${(cat_2.tokens / rawMaxTokens * 100).toFixed(1)}%`; 208: const isReserved = cat_2.name === RESERVED_CATEGORY_NAME; 209: const displayName = cat_2.name; 210: const symbol = cat_2.isDeferred ? " " : isReserved ? "\u26DD" : "\u26C1"; 211: return <Box key={index}><Text color={cat_2.color}>{symbol}</Text><Text> {displayName}: </Text><Text dimColor={true}>{tokenDisplay} tokens ({percentDisplay})</Text></Box>; 212: }; 213: $[38] = rawMaxTokens; 214: $[39] = t19; 215: } else { 216: t19 = $[39]; 217: } 218: const t20 = visibleCategories.map(t19); 219: let t21; 220: if ($[40] !== categories || $[41] !== rawMaxTokens) { 221: t21 = (categories.find(_temp6)?.tokens ?? 0) > 0 && <Box><Text dimColor={true}>⛶</Text><Text> Free space: </Text><Text dimColor={true}>{formatTokens(categories.find(_temp7)?.tokens || 0)}{" "}({((categories.find(_temp8)?.tokens || 0) / rawMaxTokens * 100).toFixed(1)}%)</Text></Box>; 222: $[40] = categories; 223: $[41] = rawMaxTokens; 224: $[42] = t21; 225: } else { 226: t21 = $[42]; 227: } 228: const t22 = autocompactCategory && autocompactCategory.tokens > 0 && <Box><Text color={autocompactCategory.color}>⛝</Text><Text dimColor={true}> {autocompactCategory.name}: </Text><Text dimColor={true}>{formatTokens(autocompactCategory.tokens)} tokens ({(autocompactCategory.tokens / rawMaxTokens * 100).toFixed(1)}%)</Text></Box>; 229: let t23; 230: if ($[43] !== t15 || $[44] !== t20 || $[45] !== t21 || $[46] !== t22) { 231: t23 = <Box flexDirection="column" gap={0} flexShrink={0}>{t15}{t16}{t17}{t18}{t20}{t21}{t22}</Box>; 232: $[43] = t15; 233: $[44] = t20; 234: $[45] = t21; 235: $[46] = t22; 236: $[47] = t23; 237: } else { 238: t23 = $[47]; 239: } 240: if ($[48] !== t12 || $[49] !== t23) { 241: t9 = <Box flexDirection="row" gap={2}>{t12}{t23}</Box>; 242: $[48] = t12; 243: $[49] = t23; 244: $[50] = t9; 245: } else { 246: t9 = $[50]; 247: } 248: T0 = Box; 249: t2 = "column"; 250: t3 = -1; 251: if ($[51] !== hasDeferredMcpTools || $[52] !== mcpTools) { 252: t4 = mcpTools.length > 0 && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>MCP tools</Text><Text dimColor={true}>{" "}· /mcp{hasDeferredMcpTools ? " (loaded on-demand)" : ""}</Text></Box>{mcpTools.some(_temp9) && <Box flexDirection="column" marginTop={1}><Text dimColor={true}>Loaded</Text>{mcpTools.filter(_temp0).map(_temp1)}</Box>}{hasDeferredMcpTools && mcpTools.some(_temp10) && <Box flexDirection="column" marginTop={1}><Text dimColor={true}>Available</Text>{mcpTools.filter(_temp11).map(_temp12)}</Box>}{!hasDeferredMcpTools && mcpTools.map(_temp13)}</Box>; 253: $[51] = hasDeferredMcpTools; 254: $[52] = mcpTools; 255: $[53] = t4; 256: } else { 257: t4 = $[53]; 258: } 259: t5 = (systemTools && systemTools.length > 0 || hasDeferredBuiltinTools) && false && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>[ANT-ONLY] System tools</Text>{hasDeferredBuiltinTools && <Text dimColor={true}> (some loaded on-demand)</Text>}</Box><Box flexDirection="column" marginTop={1}><Text dimColor={true}>Loaded</Text>{systemTools?.map(_temp14)}{deferredBuiltinTools.filter(_temp15).map(_temp16)}</Box>{hasDeferredBuiltinTools && deferredBuiltinTools.some(_temp17) && <Box flexDirection="column" marginTop={1}><Text dimColor={true}>Available</Text>{deferredBuiltinTools.filter(_temp18).map(_temp19)}</Box>}</Box>; 260: $[0] = categories; 261: $[1] = gridRows; 262: $[2] = mcpTools; 263: $[3] = model; 264: $[4] = percentage; 265: $[5] = rawMaxTokens; 266: $[6] = systemTools; 267: $[7] = t1; 268: $[8] = totalTokens; 269: $[9] = T0; 270: $[10] = T1; 271: $[11] = t2; 272: $[12] = t3; 273: $[13] = t4; 274: $[14] = t5; 275: $[15] = t6; 276: $[16] = t7; 277: $[17] = t8; 278: $[18] = t9; 279: } else { 280: T0 = $[9]; 281: T1 = $[10]; 282: t2 = $[11]; 283: t3 = $[12]; 284: t4 = $[13]; 285: t5 = $[14]; 286: t6 = $[15]; 287: t7 = $[16]; 288: t8 = $[17]; 289: t9 = $[18]; 290: } 291: let t10; 292: if ($[54] !== systemPromptSections) { 293: t10 = systemPromptSections && systemPromptSections.length > 0 && false && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] System prompt sections</Text>{systemPromptSections.map(_temp20)}</Box>; 294: $[54] = systemPromptSections; 295: $[55] = t10; 296: } else { 297: t10 = $[55]; 298: } 299: let t11; 300: if ($[56] !== agents) { 301: t11 = agents.length > 0 && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>Custom agents</Text><Text dimColor={true}> · /agents</Text></Box>{Array.from(groupBySource(agents).entries()).map(_temp22)}</Box>; 302: $[56] = agents; 303: $[57] = t11; 304: } else { 305: t11 = $[57]; 306: } 307: let t12; 308: if ($[58] !== memoryFiles) { 309: t12 = memoryFiles.length > 0 && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>Memory files</Text><Text dimColor={true}> · /memory</Text></Box>{memoryFiles.map(_temp23)}</Box>; 310: $[58] = memoryFiles; 311: $[59] = t12; 312: } else { 313: t12 = $[59]; 314: } 315: let t13; 316: if ($[60] !== skills) { 317: t13 = skills && skills.tokens > 0 && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>Skills</Text><Text dimColor={true}> · /skills</Text></Box>{Array.from(groupBySource(skills.skillFrontmatter).entries()).map(_temp25)}</Box>; 318: $[60] = skills; 319: $[61] = t13; 320: } else { 321: t13 = $[61]; 322: } 323: let t14; 324: if ($[62] !== messageBreakdown) { 325: t14 = messageBreakdown && false && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] Message breakdown</Text><Box flexDirection="column" marginLeft={1}><Box><Text>Tool calls: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.toolCallTokens)} tokens</Text></Box><Box><Text>Tool results: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.toolResultTokens)} tokens</Text></Box><Box><Text>Attachments: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.attachmentTokens)} tokens</Text></Box><Box><Text>Assistant messages (non-tool): </Text><Text dimColor={true}>{formatTokens(messageBreakdown.assistantMessageTokens)} tokens</Text></Box><Box><Text>User messages (non-tool-result): </Text><Text dimColor={true}>{formatTokens(messageBreakdown.userMessageTokens)} tokens</Text></Box></Box>{messageBreakdown.toolCallsByType.length > 0 && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] Top tools</Text>{messageBreakdown.toolCallsByType.slice(0, 5).map(_temp26)}</Box>}{messageBreakdown.attachmentsByType.length > 0 && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] Top attachments</Text>{messageBreakdown.attachmentsByType.slice(0, 5).map(_temp27)}</Box>}</Box>; 326: $[62] = messageBreakdown; 327: $[63] = t14; 328: } else { 329: t14 = $[63]; 330: } 331: let t15; 332: if ($[64] !== T0 || $[65] !== t10 || $[66] !== t11 || $[67] !== t12 || $[68] !== t13 || $[69] !== t14 || $[70] !== t2 || $[71] !== t3 || $[72] !== t4 || $[73] !== t5) { 333: t15 = <T0 flexDirection={t2} marginLeft={t3}>{t4}{t5}{t10}{t11}{t12}{t13}{t14}</T0>; 334: $[64] = T0; 335: $[65] = t10; 336: $[66] = t11; 337: $[67] = t12; 338: $[68] = t13; 339: $[69] = t14; 340: $[70] = t2; 341: $[71] = t3; 342: $[72] = t4; 343: $[73] = t5; 344: $[74] = t15; 345: } else { 346: t15 = $[74]; 347: } 348: let t16; 349: if ($[75] !== data) { 350: t16 = generateContextSuggestions(data); 351: $[75] = data; 352: $[76] = t16; 353: } else { 354: t16 = $[76]; 355: } 356: let t17; 357: if ($[77] !== t16) { 358: t17 = <ContextSuggestions suggestions={t16} />; 359: $[77] = t16; 360: $[78] = t17; 361: } else { 362: t17 = $[78]; 363: } 364: let t18; 365: if ($[79] !== T1 || $[80] !== t15 || $[81] !== t17 || $[82] !== t6 || $[83] !== t7 || $[84] !== t8 || $[85] !== t9) { 366: t18 = <T1 flexDirection={t6} paddingLeft={t7}>{t8}{t9}{t15}{t17}</T1>; 367: $[79] = T1; 368: $[80] = t15; 369: $[81] = t17; 370: $[82] = t6; 371: $[83] = t7; 372: $[84] = t8; 373: $[85] = t9; 374: $[86] = t18; 375: } else { 376: t18 = $[86]; 377: } 378: return t18; 379: } 380: function _temp27(attachment, i_10) { 381: return <Box key={i_10} marginLeft={1}><Text>└ {attachment.name}: </Text><Text dimColor={true}>{formatTokens(attachment.tokens)} tokens</Text></Box>; 382: } 383: function _temp26(tool_5, i_9) { 384: return <Box key={i_9} marginLeft={1}><Text>└ {tool_5.name}: </Text><Text dimColor={true}>calls {formatTokens(tool_5.callTokens)}, results{" "}{formatTokens(tool_5.resultTokens)}</Text></Box>; 385: } 386: function _temp25(t0) { 387: const [sourceDisplay_0, sourceSkills] = t0; 388: return <Box key={sourceDisplay_0} flexDirection="column" marginTop={1}><Text dimColor={true}>{sourceDisplay_0}</Text>{sourceSkills.map(_temp24)}</Box>; 389: } 390: function _temp24(skill, i_8) { 391: return <Box key={i_8}><Text>└ {skill.name}: </Text><Text dimColor={true}>{formatTokens(skill.tokens)} tokens</Text></Box>; 392: } 393: function _temp23(file, i_7) { 394: return <Box key={i_7}><Text>└ {getDisplayPath(file.path)}: </Text><Text dimColor={true}>{formatTokens(file.tokens)} tokens</Text></Box>; 395: } 396: function _temp22(t0) { 397: const [sourceDisplay, sourceAgents] = t0; 398: return <Box key={sourceDisplay} flexDirection="column" marginTop={1}><Text dimColor={true}>{sourceDisplay}</Text>{sourceAgents.map(_temp21)}</Box>; 399: } 400: function _temp21(agent, i_6) { 401: return <Box key={i_6}><Text>└ {agent.agentType}: </Text><Text dimColor={true}>{formatTokens(agent.tokens)} tokens</Text></Box>; 402: } 403: function _temp20(section, i_5) { 404: return <Box key={i_5}><Text>└ {section.name}: </Text><Text dimColor={true}>{formatTokens(section.tokens)} tokens</Text></Box>; 405: } 406: function _temp19(tool_4, i_4) { 407: return <Box key={i_4}><Text dimColor={true}>└ {tool_4.name}</Text></Box>; 408: } 409: function _temp18(t_4) { 410: return !t_4.isLoaded; 411: } 412: function _temp17(t_5) { 413: return !t_5.isLoaded; 414: } 415: function _temp16(tool_3, i_3) { 416: return <Box key={`def-${i_3}`}><Text>└ {tool_3.name}: </Text><Text dimColor={true}>{formatTokens(tool_3.tokens)} tokens</Text></Box>; 417: } 418: function _temp15(t_3) { 419: return t_3.isLoaded; 420: } 421: function _temp14(tool_2, i_2) { 422: return <Box key={`sys-${i_2}`}><Text>└ {tool_2.name}: </Text><Text dimColor={true}>{formatTokens(tool_2.tokens)} tokens</Text></Box>; 423: } 424: function _temp13(tool_1, i_1) { 425: return <Box key={i_1}><Text>└ {tool_1.name}: </Text><Text dimColor={true}>{formatTokens(tool_1.tokens)} tokens</Text></Box>; 426: } 427: function _temp12(tool_0, i_0) { 428: return <Box key={i_0}><Text dimColor={true}>└ {tool_0.name}</Text></Box>; 429: } 430: function _temp11(t_1) { 431: return !t_1.isLoaded; 432: } 433: function _temp10(t_2) { 434: return !t_2.isLoaded; 435: } 436: function _temp1(tool, i) { 437: return <Box key={i}><Text>└ {tool.name}: </Text><Text dimColor={true}>{formatTokens(tool.tokens)} tokens</Text></Box>; 438: } 439: function _temp0(t) { 440: return t.isLoaded; 441: } 442: function _temp9(t_0) { 443: return t_0.isLoaded; 444: } 445: function _temp8(c_0) { 446: return c_0.name === "Free space"; 447: } 448: function _temp7(c) { 449: return c.name === "Free space"; 450: } 451: function _temp6(c_1) { 452: return c_1.name === "Free space"; 453: } 454: function _temp5(row, rowIndex) { 455: return <Box key={rowIndex} flexDirection="row" marginLeft={-1}>{row.map(_temp4)}</Box>; 456: } 457: function _temp4(square, colIndex) { 458: if (square.categoryName === "Free space") { 459: return <Text key={colIndex} dimColor={true}>{"\u26F6 "}</Text>; 460: } 461: if (square.categoryName === RESERVED_CATEGORY_NAME) { 462: return <Text key={colIndex} color={square.color}>{"\u26DD "}</Text>; 463: } 464: return <Text key={colIndex} color={square.color}>{square.squareFullness >= 0.7 ? "\u26C1 " : "\u26C0 "}</Text>; 465: } 466: function _temp3(cat_1) { 467: return cat_1.name === RESERVED_CATEGORY_NAME; 468: } 469: function _temp2(cat_0) { 470: return cat_0.isDeferred && cat_0.name.includes("MCP"); 471: } 472: function _temp(cat) { 473: return cat.tokens > 0 && cat.name !== "Free space" && cat.name !== RESERVED_CATEGORY_NAME && !cat.isDeferred; 474: }

File: src/components/CoordinatorAgentStatus.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import figures from 'figures'; 3: import * as React from 'react'; 4: import { BLACK_CIRCLE, PAUSE_ICON, PLAY_ICON } from '../constants/figures.js'; 5: import { useTerminalSize } from '../hooks/useTerminalSize.js'; 6: import { stringWidth } from '../ink/stringWidth.js'; 7: import { Box, Text, wrapText } from '../ink.js'; 8: import { type AppState, useAppState, useSetAppState } from '../state/AppState.js'; 9: import { enterTeammateView, exitTeammateView } from '../state/teammateViewHelpers.js'; 10: import { isPanelAgentTask, type LocalAgentTaskState } from '../tasks/LocalAgentTask/LocalAgentTask.js'; 11: import { formatDuration, formatNumber } from '../utils/format.js'; 12: import { evictTerminalTask } from '../utils/task/framework.js'; 13: import { isTerminalStatus } from './tasks/taskStatusUtils.js'; 14: export function getVisibleAgentTasks(tasks: AppState['tasks']): LocalAgentTaskState[] { 15: return Object.values(tasks).filter((t): t is LocalAgentTaskState => isPanelAgentTask(t) && t.evictAfter !== 0).sort((a, b) => a.startTime - b.startTime); 16: } 17: export function CoordinatorTaskPanel(): React.ReactNode { 18: const tasks = useAppState(s => s.tasks); 19: const viewingAgentTaskId = useAppState(s_0 => s_0.viewingAgentTaskId); 20: const agentNameRegistry = useAppState(s_1 => s_1.agentNameRegistry); 21: const coordinatorTaskIndex = useAppState(s_2 => s_2.coordinatorTaskIndex); 22: const tasksSelected = useAppState(s_3 => s_3.footerSelection === 'tasks'); 23: const selectedIndex = tasksSelected ? coordinatorTaskIndex : undefined; 24: const setAppState = useSetAppState(); 25: const visibleTasks = getVisibleAgentTasks(tasks); 26: const hasTasks = Object.values(tasks).some(isPanelAgentTask); 27: const tasksRef = React.useRef(tasks); 28: tasksRef.current = tasks; 29: const [, setTick] = React.useState(0); 30: React.useEffect(() => { 31: if (!hasTasks) return; 32: const interval = setInterval((tasksRef_0, setAppState_0, setTick_0) => { 33: const now = Date.now(); 34: for (const t of Object.values(tasksRef_0.current)) { 35: if (isPanelAgentTask(t) && (t.evictAfter ?? Infinity) <= now) { 36: evictTerminalTask(t.id, setAppState_0); 37: } 38: } 39: setTick_0((prev: number) => prev + 1); 40: }, 1000, tasksRef, setAppState, setTick); 41: return () => clearInterval(interval); 42: }, [hasTasks, setAppState]); 43: const nameByAgentId = React.useMemo(() => { 44: const inv = new Map<string, string>(); 45: for (const [n, id] of agentNameRegistry) inv.set(id, n); 46: return inv; 47: }, [agentNameRegistry]); 48: if (visibleTasks.length === 0) { 49: return null; 50: } 51: return <Box flexDirection="column" marginTop={1}> 52: <MainLine isSelected={selectedIndex === 0} isViewed={viewingAgentTaskId === undefined} onClick={() => exitTeammateView(setAppState)} /> 53: {visibleTasks.map((task, i) => <AgentLine key={task.id} task={task} name={nameByAgentId.get(task.id)} isSelected={selectedIndex === i + 1} isViewed={viewingAgentTaskId === task.id} onClick={() => enterTeammateView(task.id, setAppState)} />)} 54: </Box>; 55: } 56: export function useCoordinatorTaskCount() { 57: const tasks = useAppState(_temp); 58: let t0; 59: t0 = 0; 60: return t0; 61: } 62: function _temp(s) { 63: return s.tasks; 64: } 65: function MainLine(t0) { 66: const $ = _c(10); 67: const { 68: isSelected, 69: isViewed, 70: onClick 71: } = t0; 72: const [hover, setHover] = React.useState(false); 73: const prefix = isSelected || hover ? figures.pointer + " " : " "; 74: const bullet = isViewed ? BLACK_CIRCLE : figures.circle; 75: let t1; 76: let t2; 77: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 78: t1 = () => setHover(true); 79: t2 = () => setHover(false); 80: $[0] = t1; 81: $[1] = t2; 82: } else { 83: t1 = $[0]; 84: t2 = $[1]; 85: } 86: const t3 = !isSelected && !isViewed && !hover; 87: let t4; 88: if ($[2] !== bullet || $[3] !== isViewed || $[4] !== prefix || $[5] !== t3) { 89: t4 = <Text dimColor={t3} bold={isViewed}>{prefix}{bullet} main</Text>; 90: $[2] = bullet; 91: $[3] = isViewed; 92: $[4] = prefix; 93: $[5] = t3; 94: $[6] = t4; 95: } else { 96: t4 = $[6]; 97: } 98: let t5; 99: if ($[7] !== onClick || $[8] !== t4) { 100: t5 = <Box onClick={onClick} onMouseEnter={t1} onMouseLeave={t2}>{t4}</Box>; 101: $[7] = onClick; 102: $[8] = t4; 103: $[9] = t5; 104: } else { 105: t5 = $[9]; 106: } 107: return t5; 108: } 109: type AgentLineProps = { 110: task: LocalAgentTaskState; 111: name?: string; 112: isSelected?: boolean; 113: isViewed?: boolean; 114: onClick?: () => void; 115: }; 116: function AgentLine(t0) { 117: const $ = _c(32); 118: const { 119: task, 120: name, 121: isSelected, 122: isViewed, 123: onClick 124: } = t0; 125: const { 126: columns 127: } = useTerminalSize(); 128: const [hover, setHover] = React.useState(false); 129: const isRunning = !isTerminalStatus(task.status); 130: const pausedMs = task.totalPausedMs ?? 0; 131: const elapsedMs = Math.max(0, isRunning ? Date.now() - task.startTime - pausedMs : (task.endTime ?? task.startTime) - task.startTime - pausedMs); 132: let t1; 133: if ($[0] !== elapsedMs) { 134: t1 = formatDuration(elapsedMs); 135: $[0] = elapsedMs; 136: $[1] = t1; 137: } else { 138: t1 = $[1]; 139: } 140: const elapsed = t1; 141: const tokenCount = task.progress?.tokenCount; 142: const lastActivity = task.progress?.lastActivity; 143: const arrow = lastActivity ? figures.arrowDown : figures.arrowUp; 144: let t2; 145: if ($[2] !== arrow || $[3] !== tokenCount) { 146: t2 = tokenCount !== undefined && tokenCount > 0 ? ` · ${arrow} ${formatNumber(tokenCount)} tokens` : ""; 147: $[2] = arrow; 148: $[3] = tokenCount; 149: $[4] = t2; 150: } else { 151: t2 = $[4]; 152: } 153: const tokenText = t2; 154: const queuedCount = task.pendingMessages.length; 155: const queuedText = queuedCount > 0 ? ` · ${queuedCount} queued` : ""; 156: const displayDescription = task.progress?.summary || task.description; 157: const highlighted = isSelected || hover; 158: const prefix = highlighted ? figures.pointer + " " : " "; 159: const bullet = isViewed ? BLACK_CIRCLE : figures.circle; 160: const dim = !highlighted && !isViewed; 161: const sep = isRunning ? PLAY_ICON : PAUSE_ICON; 162: const namePart = name ? `${name}: ` : ""; 163: const hintPart = isSelected && !isViewed ? ` · x to ${isRunning ? "stop" : "clear"}` : ""; 164: const suffixPart = ` ${sep} ${elapsed}${tokenText}${queuedText}${hintPart}`; 165: const availableForDesc = columns - stringWidth(prefix) - stringWidth(`${bullet} `) - stringWidth(namePart) - stringWidth(suffixPart); 166: const t3 = Math.max(0, availableForDesc); 167: let t4; 168: if ($[5] !== displayDescription || $[6] !== t3) { 169: t4 = wrapText(displayDescription, t3, "truncate-end"); 170: $[5] = displayDescription; 171: $[6] = t3; 172: $[7] = t4; 173: } else { 174: t4 = $[7]; 175: } 176: const truncated = t4; 177: let t5; 178: if ($[8] !== name) { 179: t5 = name && <><Text dimColor={false} bold={true}>{name}</Text>{": "}</>; 180: $[8] = name; 181: $[9] = t5; 182: } else { 183: t5 = $[9]; 184: } 185: let t6; 186: if ($[10] !== queuedCount || $[11] !== queuedText) { 187: t6 = queuedCount > 0 && <Text color="warning">{queuedText}</Text>; 188: $[10] = queuedCount; 189: $[11] = queuedText; 190: $[12] = t6; 191: } else { 192: t6 = $[12]; 193: } 194: let t7; 195: if ($[13] !== hintPart) { 196: t7 = hintPart && <Text dimColor={true}>{hintPart}</Text>; 197: $[13] = hintPart; 198: $[14] = t7; 199: } else { 200: t7 = $[14]; 201: } 202: let t8; 203: if ($[15] !== bullet || $[16] !== dim || $[17] !== elapsed || $[18] !== isViewed || $[19] !== prefix || $[20] !== sep || $[21] !== t5 || $[22] !== t6 || $[23] !== t7 || $[24] !== tokenText || $[25] !== truncated) { 204: t8 = <Text dimColor={dim} bold={isViewed}>{prefix}{bullet}{" "}{t5}{truncated} {sep} {elapsed}{tokenText}{t6}{t7}</Text>; 205: $[15] = bullet; 206: $[16] = dim; 207: $[17] = elapsed; 208: $[18] = isViewed; 209: $[19] = prefix; 210: $[20] = sep; 211: $[21] = t5; 212: $[22] = t6; 213: $[23] = t7; 214: $[24] = tokenText; 215: $[25] = truncated; 216: $[26] = t8; 217: } else { 218: t8 = $[26]; 219: } 220: const line = t8; 221: if (!onClick) { 222: return line; 223: } 224: let t10; 225: let t9; 226: if ($[27] === Symbol.for("react.memo_cache_sentinel")) { 227: t9 = () => setHover(true); 228: t10 = () => setHover(false); 229: $[27] = t10; 230: $[28] = t9; 231: } else { 232: t10 = $[27]; 233: t9 = $[28]; 234: } 235: let t11; 236: if ($[29] !== line || $[30] !== onClick) { 237: t11 = <Box onClick={onClick} onMouseEnter={t9} onMouseLeave={t10}>{line}</Box>; 238: $[29] = line; 239: $[30] = onClick; 240: $[31] = t11; 241: } else { 242: t11 = $[31]; 243: } 244: return t11; 245: }

File: src/components/CostThresholdDialog.tsx

typescript 1: import { c as _c } from "react/compiler-runtime"; 2: import React from 'react'; 3: import { Box, Link, Text } from '../ink.js'; 4: import { Select } from './CustomSelect/index.js'; 5: import { Dialog } from './design-system/Dialog.js'; 6: type Props = { 7: onDone: () => void; 8: }; 9: export function CostThresholdDialog(t0) { 10: const $ = _c(7); 11: const { 12: onDone 13: } = t0; 14: let t1; 15: if ($[0] === Symbol.for("react.memo_cache_sentinel")) { 16: t1 = <Box flexDirection="column"><Text>Learn more about how to monitor your spending:</Text><Link url="https://code.claude.com/docs/en/costs" /></Box>; 17: $[0] = t1; 18: } else { 19: t1 = $[0]; 20: } 21: let t2; 22: if ($[1] === Symbol.for("react.memo_cache_sentinel")) { 23: t2 = [{ 24: value: "ok", 25: label: "Got it, thanks!" 26: }]; 27: $[1] = t2; 28: } else { 29: t2 = $[1]; 30: } 31: let t3; 32: if ($[2] !== onDone) { 33: t3 = <Select options={t2} onChange={onDone} />; 34: $[2] = onDone; 35: $[3] = t3; 36: } else { 37: t3 = $[3]; 38: } 39: let t4; 40: if ($[4] !== onDone || $[5] !== t3) { 41: t4 = <Dialog title="You've spent $5 on the Anthropic API this session." onCancel={onDone}>{t1}{t3}</Dialog>; 42: $[4] = onDone; 43: $[5] = t3; 44: $[6] = t4; 45: } else { 46: t4 = $[6]; 47: } 48: return t4; 49: }

File: src/components/CtrlOToExpand.tsx

````typescript