流出したclaude codeのソースコード2
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'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'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'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'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: "Disconnect".
381: </Text>
382: {claudeAIClearAuthUrl && <Box flexDirection="column">
383: <Box>
384: <Text dimColor>
385: If your browser didn'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 "Disconnect".
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'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 "{searchQuery}"
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} · 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 · 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'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'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=<token>
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