更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)

* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files)

纯格式化:移除分号、React Compiler import、import 多行展开。
修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-2): 格式化 commands (79 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md

- README.md: 大幅重写,更详细版本历史和配置示例
- Run.ps1: 新增 Windows 启动脚本
- TODO.md: 新增包完成清单
- V6.md: 删除(架构重构规划已不适用)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复以前的问题

* fix: 修复 login 面板的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-04 23:24:27 +08:00
committed by GitHub
parent 02694918b5
commit 5b1a52b8e0
559 changed files with 103807 additions and 101817 deletions

View File

@@ -1,25 +1,29 @@
import { c as _c } from "react/compiler-runtime";
import React, { Suspense, use, useMemo } from 'react';
import { useSettings } from '../../../hooks/useSettings.js';
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
import { stringWidth } from '../../../ink/stringWidth.js';
import { Ansi, Box, Text, useTheme } from '../../../ink.js';
import { type CliHighlight, getCliHighlightPromise } from '../../../utils/cliHighlight.js';
import { applyMarkdown } from '../../../utils/markdown.js';
import sliceAnsi from '../../../utils/sliceAnsi.js';
import React, { Suspense, use, useMemo } from 'react'
import { useSettings } from '../../../hooks/useSettings.js'
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
import { stringWidth } from '../../../ink/stringWidth.js'
import { Ansi, Box, Text, useTheme } from '../../../ink.js'
import {
type CliHighlight,
getCliHighlightPromise,
} from '../../../utils/cliHighlight.js'
import { applyMarkdown } from '../../../utils/markdown.js'
import sliceAnsi from '../../../utils/sliceAnsi.js'
type PreviewBoxProps = {
/** The preview content to display. Markdown is rendered with syntax highlighting
* for code blocks (```ts, ```py, etc.). Also supports plain multi-line text. */
content: string;
content: string
/** Maximum number of lines to display before truncating. @default 20 */
maxLines?: number;
maxLines?: number
/** Minimum height (in lines) for the preview box. Content will be padded if shorter. */
minHeight?: number;
minHeight?: number
/** Minimum width for the preview box. @default 40 */
minWidth?: number;
minWidth?: number
/** Maximum width available for this box (e.g., the container width). */
maxWidth?: number;
};
maxWidth?: number
}
const BOX_CHARS = {
topLeft: '┌',
topRight: '┐',
@@ -28,201 +32,127 @@ const BOX_CHARS = {
horizontal: '─',
vertical: '│',
teeLeft: '├',
teeRight: '┤'
};
teeRight: '┤',
}
/**
* A bordered monospace box for displaying preview content.
* Truncates content that exceeds maxLines with an indicator.
* The parent component should pass maxLines based on its available height budget.
*/
export function PreviewBox(props) {
const $ = _c(4);
const settings = useSettings();
export function PreviewBox(props: PreviewBoxProps): React.ReactNode {
const settings = useSettings()
if (settings.syntaxHighlightingDisabled) {
let t0;
if ($[0] !== props) {
t0 = <PreviewBoxBody {...props} highlight={null} />;
$[0] = props;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
return <PreviewBoxBody {...props} highlight={null} />
}
let t0;
if ($[2] !== props) {
t0 = <Suspense fallback={<PreviewBoxBody {...props} highlight={null} />}><PreviewBoxWithHighlight {...props} /></Suspense>;
$[2] = props;
$[3] = t0;
} else {
t0 = $[3];
}
return t0;
return (
<Suspense fallback={<PreviewBoxBody {...props} highlight={null} />}>
<PreviewBoxWithHighlight {...props} />
</Suspense>
)
}
function PreviewBoxWithHighlight(props) {
const $ = _c(4);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = getCliHighlightPromise();
$[0] = t0;
} else {
t0 = $[0];
}
const highlight = use(t0);
let t1;
if ($[1] !== highlight || $[2] !== props) {
t1 = <PreviewBoxBody {...props} highlight={highlight} />;
$[1] = highlight;
$[2] = props;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
function PreviewBoxWithHighlight(props: PreviewBoxProps): React.ReactNode {
const highlight = use(getCliHighlightPromise())
return <PreviewBoxBody {...props} highlight={highlight} />
}
function PreviewBoxBody(t0) {
const $ = _c(34);
const {
content,
maxLines,
minHeight,
minWidth: t1,
maxWidth,
highlight
} = t0;
const minWidth = t1 === undefined ? 40 : t1;
const {
columns: terminalWidth
} = useTerminalSize();
const [theme] = useTheme();
const effectiveMaxWidth = maxWidth ?? terminalWidth - 4;
const effectiveMaxLines = maxLines ?? 20;
let t2;
if ($[0] !== content || $[1] !== highlight || $[2] !== theme) {
t2 = applyMarkdown(content, theme, highlight);
$[0] = content;
$[1] = highlight;
$[2] = theme;
$[3] = t2;
} else {
t2 = $[3];
}
const rendered = t2;
let T0;
let bottomBorder;
let t3;
let t4;
let t5;
let truncationBar;
if ($[4] !== effectiveMaxLines || $[5] !== effectiveMaxWidth || $[6] !== minHeight || $[7] !== minWidth || $[8] !== rendered) {
const contentLines = rendered.split("\n");
const isTruncated = contentLines.length > effectiveMaxLines;
const truncatedLines = isTruncated ? contentLines.slice(0, effectiveMaxLines) : contentLines;
const effectiveMinHeight = Math.min(minHeight ?? 0, effectiveMaxLines);
const paddingNeeded = Math.max(0, effectiveMinHeight - truncatedLines.length - (isTruncated ? 1 : 0));
const lines = paddingNeeded > 0 ? [...truncatedLines, ...Array(paddingNeeded).fill("")] : truncatedLines;
const contentWidth = Math.max(minWidth, ...lines.map(_temp));
const boxWidth = Math.min(contentWidth + 4, effectiveMaxWidth);
const innerWidth = boxWidth - 4;
let t6;
if ($[15] !== boxWidth) {
t6 = BOX_CHARS.horizontal.repeat(boxWidth - 2);
$[15] = boxWidth;
$[16] = t6;
} else {
t6 = $[16];
}
const topBorder = `${BOX_CHARS.topLeft}${t6}${BOX_CHARS.topRight}`;
let t7;
if ($[17] !== boxWidth) {
t7 = BOX_CHARS.horizontal.repeat(boxWidth - 2);
$[17] = boxWidth;
$[18] = t7;
} else {
t7 = $[18];
}
bottomBorder = `${BOX_CHARS.bottomLeft}${t7}${BOX_CHARS.bottomRight}`;
truncationBar = isTruncated ? (() => {
const hiddenCount = contentLines.length - effectiveMaxLines;
const label = `${BOX_CHARS.horizontal.repeat(3)} \u2702 ${BOX_CHARS.horizontal.repeat(3)} ${hiddenCount} lines hidden `;
const labelWidth = stringWidth(label);
const fillWidth = Math.max(0, boxWidth - 2 - labelWidth);
return `${BOX_CHARS.teeLeft}${label}${BOX_CHARS.horizontal.repeat(fillWidth)}${BOX_CHARS.teeRight}`;
})() : null;
T0 = Box;
t3 = "column";
if ($[19] !== topBorder) {
t4 = <Text dimColor={true}>{topBorder}</Text>;
$[19] = topBorder;
$[20] = t4;
} else {
t4 = $[20];
}
let t8;
if ($[21] !== innerWidth) {
t8 = (line_0, index) => {
const lineWidth = stringWidth(line_0);
const displayLine = lineWidth > innerWidth ? sliceAnsi(line_0, 0, innerWidth) : line_0;
const padding = " ".repeat(Math.max(0, innerWidth - stringWidth(displayLine)));
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>;
};
$[21] = innerWidth;
$[22] = t8;
} else {
t8 = $[22];
}
t5 = lines.map(t8);
$[4] = effectiveMaxLines;
$[5] = effectiveMaxWidth;
$[6] = minHeight;
$[7] = minWidth;
$[8] = rendered;
$[9] = T0;
$[10] = bottomBorder;
$[11] = t3;
$[12] = t4;
$[13] = t5;
$[14] = truncationBar;
} else {
T0 = $[9];
bottomBorder = $[10];
t3 = $[11];
t4 = $[12];
t5 = $[13];
truncationBar = $[14];
}
let t6;
if ($[23] !== truncationBar) {
t6 = truncationBar && <Text color="warning">{truncationBar}</Text>;
$[23] = truncationBar;
$[24] = t6;
} else {
t6 = $[24];
}
let t7;
if ($[25] !== bottomBorder) {
t7 = <Text dimColor={true}>{bottomBorder}</Text>;
$[25] = bottomBorder;
$[26] = t7;
} else {
t7 = $[26];
}
let t8;
if ($[27] !== T0 || $[28] !== t3 || $[29] !== t4 || $[30] !== t5 || $[31] !== t6 || $[32] !== t7) {
t8 = <T0 flexDirection={t3}>{t4}{t5}{t6}{t7}</T0>;
$[27] = T0;
$[28] = t3;
$[29] = t4;
$[30] = t5;
$[31] = t6;
$[32] = t7;
$[33] = t8;
} else {
t8 = $[33];
}
return t8;
}
function _temp(line) {
return stringWidth(line);
function PreviewBoxBody({
content,
maxLines,
minHeight,
minWidth = 40,
maxWidth,
highlight,
}: PreviewBoxProps & { highlight: CliHighlight | null }): React.ReactNode {
const { columns: terminalWidth } = useTerminalSize()
const [theme] = useTheme()
const effectiveMaxWidth = maxWidth ?? terminalWidth - 4
// Use provided maxLines, or a reasonable default
const effectiveMaxLines = maxLines ?? 20
// Render markdown with syntax highlighting for code blocks. applyMarkdown
// returns an ANSI-styled string (bold, colors, etc.) that we split into
// lines. stringWidth and sliceAnsi below correctly handle ANSI codes.
const rendered = useMemo(
() => applyMarkdown(content, theme, highlight),
[content, theme, highlight],
)
const contentLines = rendered.split('\n')
const isTruncated = contentLines.length > effectiveMaxLines
// Truncate to effectiveMaxLines
const truncatedLines = isTruncated
? contentLines.slice(0, effectiveMaxLines)
: contentLines
// Pad content with empty lines if shorter than minHeight, but never exceed
// the truncation limit — otherwise padding undoes the truncation
const effectiveMinHeight = Math.min(minHeight ?? 0, effectiveMaxLines)
const paddingNeeded = Math.max(
0,
effectiveMinHeight - truncatedLines.length - (isTruncated ? 1 : 0),
)
const lines =
paddingNeeded > 0
? [...truncatedLines, ...Array<string>(paddingNeeded).fill('')]
: truncatedLines
// Calculate content width (max visual line width, handling unicode/emoji/CJK)
const contentWidth = Math.max(
minWidth,
...lines.map(line => stringWidth(line)),
)
// Add 2 for border padding, cap at the container width to prevent line wrapping
const boxWidth = Math.min(contentWidth + 4, effectiveMaxWidth)
const innerWidth = boxWidth - 4 // Account for borders and padding
// Render top border
const topBorder = `${BOX_CHARS.topLeft}${BOX_CHARS.horizontal.repeat(boxWidth - 2)}${BOX_CHARS.topRight}`
// Render bottom border
const bottomBorder = `${BOX_CHARS.bottomLeft}${BOX_CHARS.horizontal.repeat(boxWidth - 2)}${BOX_CHARS.bottomRight}`
// Build the truncation separator bar (e.g. ├─── ✂ ─── 42 lines hidden ──────┤)
const truncationBar = isTruncated
? (() => {
const hiddenCount = contentLines.length - effectiveMaxLines
const label = `${BOX_CHARS.horizontal.repeat(3)} \u2702 ${BOX_CHARS.horizontal.repeat(3)} ${hiddenCount} lines hidden `
const labelWidth = stringWidth(label)
const fillWidth = Math.max(0, boxWidth - 2 - labelWidth)
return `${BOX_CHARS.teeLeft}${label}${BOX_CHARS.horizontal.repeat(fillWidth)}${BOX_CHARS.teeRight}`
})()
: null
return (
<Box flexDirection="column">
<Text dimColor>{topBorder}</Text>
{lines.map((line, index) => {
// Pad or truncate line to fit inner width (using visual width for unicode/emoji/CJK).
// sliceAnsi handles ANSI escape codes correctly; stringWidth strips them before measuring.
const lineWidth = stringWidth(line)
const displayLine =
lineWidth > innerWidth ? sliceAnsi(line, 0, innerWidth) : line
const padding = ' '.repeat(
Math.max(0, innerWidth - stringWidth(displayLine)),
)
return (
<Box key={index} flexDirection="row">
<Text dimColor>{BOX_CHARS.vertical} </Text>
<Ansi>{displayLine}</Ansi>
<Text dimColor>
{padding} {BOX_CHARS.vertical}
</Text>
</Box>
)
})}
{truncationBar && <Text color="warning">{truncationBar}</Text>}
<Text dimColor>{bottomBorder}</Text>
</Box>
)
}

View File

@@ -1,38 +1,51 @@
import figures from 'figures';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js';
import { Box, Text } from '../../../ink.js';
import { useKeybinding, useKeybindings } from '../../../keybindings/useKeybinding.js';
import { useAppState } from '../../../state/AppState.js';
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
import { getExternalEditor } from '../../../utils/editor.js';
import { toIDEDisplayName } from '../../../utils/ide.js';
import { editPromptInEditor } from '../../../utils/promptEditor.js';
import { Divider } from '../../design-system/Divider.js';
import TextInput from '../../TextInput.js';
import { PermissionRequestTitle } from '../PermissionRequestTitle.js';
import { PreviewBox } from './PreviewBox.js';
import { QuestionNavigationBar } from './QuestionNavigationBar.js';
import type { QuestionState } from './use-multiple-choice-state.js';
import figures from 'figures'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'
import { Box, Text } from '../../../ink.js'
import {
useKeybinding,
useKeybindings,
} from '../../../keybindings/useKeybinding.js'
import { useAppState } from '../../../state/AppState.js'
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
import { getExternalEditor } from '../../../utils/editor.js'
import { toIDEDisplayName } from '../../../utils/ide.js'
import { editPromptInEditor } from '../../../utils/promptEditor.js'
import { Divider } from '../../design-system/Divider.js'
import TextInput from '../../TextInput.js'
import { PermissionRequestTitle } from '../PermissionRequestTitle.js'
import { PreviewBox } from './PreviewBox.js'
import { QuestionNavigationBar } from './QuestionNavigationBar.js'
import type { QuestionState } from './use-multiple-choice-state.js'
type Props = {
question: Question;
questions: Question[];
currentQuestionIndex: number;
answers: Record<string, string>;
questionStates: Record<string, QuestionState>;
hideSubmitTab?: boolean;
minContentHeight?: number;
minContentWidth?: number;
onUpdateQuestionState: (questionText: string, updates: Partial<QuestionState>, isMultiSelect: boolean) => void;
onAnswer: (questionText: string, label: string | string[], textInput?: string, shouldAdvance?: boolean) => void;
onTextInputFocus: (isInInput: boolean) => void;
onCancel: () => void;
onTabPrev?: () => void;
onTabNext?: () => void;
onRespondToClaude: () => void;
onFinishPlanInterview: () => void;
};
question: Question
questions: Question[]
currentQuestionIndex: number
answers: Record<string, string>
questionStates: Record<string, QuestionState>
hideSubmitTab?: boolean
minContentHeight?: number
minContentWidth?: number
onUpdateQuestionState: (
questionText: string,
updates: Partial<QuestionState>,
isMultiSelect: boolean,
) => void
onAnswer: (
questionText: string,
label: string | string[],
textInput?: string,
shouldAdvance?: boolean,
) => void
onTextInputFocus: (isInInput: boolean) => void
onCancel: () => void
onTabPrev?: () => void
onTabNext?: () => void
onRespondToClaude: () => void
onFinishPlanInterview: () => void
}
/**
* A side-by-side question view for questions with preview content.
@@ -54,188 +67,235 @@ export function PreviewQuestionView({
onTabPrev,
onTabNext,
onRespondToClaude,
onFinishPlanInterview
onFinishPlanInterview,
}: Props): React.ReactNode {
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan';
const [isFooterFocused, setIsFooterFocused] = useState(false);
const [footerIndex, setFooterIndex] = useState(0);
const [isInNotesInput, setIsInNotesInput] = useState(false);
const [cursorOffset, setCursorOffset] = useState(0);
const editor = getExternalEditor();
const editorName = editor ? toIDEDisplayName(editor) : null;
const questionText = question.question;
const questionState = questionStates[questionText];
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan'
const [isFooterFocused, setIsFooterFocused] = useState(false)
const [footerIndex, setFooterIndex] = useState(0)
const [isInNotesInput, setIsInNotesInput] = useState(false)
const [cursorOffset, setCursorOffset] = useState(0)
const editor = getExternalEditor()
const editorName = editor ? toIDEDisplayName(editor) : null
const questionText = question.question
const questionState = questionStates[questionText]
// Only real options — no "Other" for preview questions
const allOptions = question.options;
const allOptions = question.options
// Track which option is focused (for preview display)
const [focusedIndex, setFocusedIndex] = useState(0);
const [focusedIndex, setFocusedIndex] = useState(0)
// Reset focusedIndex when navigating to a different question
const prevQuestionText = useRef(questionText);
const prevQuestionText = useRef(questionText)
if (prevQuestionText.current !== questionText) {
prevQuestionText.current = questionText;
const selected = questionState?.selectedValue as string | undefined;
const idx = selected ? allOptions.findIndex(opt => opt.label === selected) : -1;
setFocusedIndex(idx >= 0 ? idx : 0);
prevQuestionText.current = questionText
const selected = questionState?.selectedValue as string | undefined
const idx = selected
? allOptions.findIndex(opt => opt.label === selected)
: -1
setFocusedIndex(idx >= 0 ? idx : 0)
}
const focusedOption = allOptions[focusedIndex];
const selectedValue = questionState?.selectedValue as string | undefined;
const notesValue = questionState?.textInputValue || '';
const handleSelectOption = useCallback((index: number) => {
const option = allOptions[index];
if (!option) return;
setFocusedIndex(index);
onUpdateQuestionState(questionText, {
selectedValue: option.label
}, false);
onAnswer(questionText, option.label);
}, [allOptions, questionText, onUpdateQuestionState, onAnswer]);
const handleNavigate = useCallback((direction: 'up' | 'down' | number) => {
if (isInNotesInput) return;
let newIndex: number;
if (typeof direction === 'number') {
newIndex = direction;
} else if (direction === 'up') {
newIndex = focusedIndex > 0 ? focusedIndex - 1 : focusedIndex;
} else {
newIndex = focusedIndex < allOptions.length - 1 ? focusedIndex + 1 : focusedIndex;
}
if (newIndex >= 0 && newIndex < allOptions.length) {
setFocusedIndex(newIndex);
}
}, [focusedIndex, allOptions.length, isInNotesInput]);
const focusedOption = allOptions[focusedIndex]
const selectedValue = questionState?.selectedValue as string | undefined
const notesValue = questionState?.textInputValue || ''
const handleSelectOption = useCallback(
(index: number) => {
const option = allOptions[index]
if (!option) return
setFocusedIndex(index)
onUpdateQuestionState(
questionText,
{ selectedValue: option.label },
false,
)
onAnswer(questionText, option.label)
},
[allOptions, questionText, onUpdateQuestionState, onAnswer],
)
const handleNavigate = useCallback(
(direction: 'up' | 'down' | number) => {
if (isInNotesInput) return
let newIndex: number
if (typeof direction === 'number') {
newIndex = direction
} else if (direction === 'up') {
newIndex = focusedIndex > 0 ? focusedIndex - 1 : focusedIndex
} else {
newIndex =
focusedIndex < allOptions.length - 1 ? focusedIndex + 1 : focusedIndex
}
if (newIndex >= 0 && newIndex < allOptions.length) {
setFocusedIndex(newIndex)
}
},
[focusedIndex, allOptions.length, isInNotesInput],
)
// Handle ctrl+g to open external editor for notes
useKeybinding('chat:externalEditor', async () => {
const currentValue = questionState?.textInputValue || '';
const result = await editPromptInEditor(currentValue);
if (result.content !== null && result.content !== currentValue) {
onUpdateQuestionState(questionText, {
textInputValue: result.content
}, false);
}
}, {
context: 'Chat',
isActive: isInNotesInput && !!editor
});
useKeybinding(
'chat:externalEditor',
async () => {
const currentValue = questionState?.textInputValue || ''
const result = await editPromptInEditor(currentValue)
if (result.content !== null && result.content !== currentValue) {
onUpdateQuestionState(
questionText,
{ textInputValue: result.content },
false,
)
}
},
{ context: 'Chat', isActive: isInNotesInput && !!editor },
)
// Handle left/right arrow and tab for question navigation.
// This must be in the child component (not just the parent) because child useInput
// handlers register first on the event emitter and fire before parent handlers.
// Without this, the parent's useKeybindings may not fire reliably depending on
// listener ordering in the event emitter.
useKeybindings({
'tabs:previous': () => onTabPrev?.(),
'tabs:next': () => onTabNext?.()
}, {
context: 'Tabs',
isActive: !isInNotesInput && !isFooterFocused
});
useKeybindings(
{
'tabs:previous': () => onTabPrev?.(),
'tabs:next': () => onTabNext?.(),
},
{ context: 'Tabs', isActive: !isInNotesInput && !isFooterFocused },
)
// Re-submit the answer (plain label) when exiting notes input.
// Notes are stored in questionStates and collected at submit time via annotations.
const handleNotesExit = useCallback(() => {
setIsInNotesInput(false);
onTextInputFocus(false);
setIsInNotesInput(false)
onTextInputFocus(false)
if (selectedValue) {
onAnswer(questionText, selectedValue);
onAnswer(questionText, selectedValue)
}
}, [selectedValue, questionText, onAnswer, onTextInputFocus]);
}, [selectedValue, questionText, onAnswer, onTextInputFocus])
const handleDownFromPreview = useCallback(() => {
setIsFooterFocused(true);
}, []);
setIsFooterFocused(true)
}, [])
const handleUpFromFooter = useCallback(() => {
setIsFooterFocused(false);
}, []);
setIsFooterFocused(false)
}, [])
// Handle keyboard input for option/footer/notes navigation.
// Always active — the handler routes internally based on isFooterFocused/isInNotesInput.
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (isFooterFocused) {
if (e.key === 'up' || e.ctrl && e.key === 'p') {
e.preventDefault();
if (footerIndex === 0) {
handleUpFromFooter();
} else {
setFooterIndex(0);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (isFooterFocused) {
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
e.preventDefault()
if (footerIndex === 0) {
handleUpFromFooter()
} else {
setFooterIndex(0)
}
return
}
return;
}
if (e.key === 'down' || e.ctrl && e.key === 'n') {
e.preventDefault();
if (isInPlanMode && footerIndex === 0) {
setFooterIndex(1);
}
return;
}
if (e.key === 'return') {
e.preventDefault();
if (footerIndex === 0) {
onRespondToClaude();
} else {
onFinishPlanInterview();
}
return;
}
if (e.key === 'escape') {
e.preventDefault();
onCancel();
}
return;
}
if (isInNotesInput) {
// In notes input mode, handle escape to exit back to option navigation
if (e.key === 'escape') {
e.preventDefault();
handleNotesExit();
}
return;
}
// Handle option navigation (vertical)
if (e.key === 'up' || e.ctrl && e.key === 'p') {
e.preventDefault();
if (focusedIndex > 0) {
handleNavigate('up');
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
e.preventDefault()
if (isInPlanMode && footerIndex === 0) {
setFooterIndex(1)
}
return
}
if (e.key === 'return') {
e.preventDefault()
if (footerIndex === 0) {
onRespondToClaude()
} else {
onFinishPlanInterview()
}
return
}
if (e.key === 'escape') {
e.preventDefault()
onCancel()
}
return
}
} else if (e.key === 'down' || e.ctrl && e.key === 'n') {
e.preventDefault();
if (focusedIndex === allOptions.length - 1) {
// At bottom of options, go to footer
handleDownFromPreview();
} else {
handleNavigate('down');
if (isInNotesInput) {
// In notes input mode, handle escape to exit back to option navigation
if (e.key === 'escape') {
e.preventDefault()
handleNotesExit()
}
return
}
} else if (e.key === 'return') {
e.preventDefault();
handleSelectOption(focusedIndex);
} else if (e.key === 'n' && !e.ctrl && !e.meta) {
// Press 'n' to focus the notes input
e.preventDefault();
setIsInNotesInput(true);
onTextInputFocus(true);
} else if (e.key === 'escape') {
e.preventDefault();
onCancel();
} else if (e.key.length === 1 && e.key >= '1' && e.key <= '9') {
e.preventDefault();
const idx_0 = parseInt(e.key, 10) - 1;
if (idx_0 < allOptions.length) {
handleNavigate(idx_0);
// Handle option navigation (vertical)
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
e.preventDefault()
if (focusedIndex > 0) {
handleNavigate('up')
}
} else if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
e.preventDefault()
if (focusedIndex === allOptions.length - 1) {
// At bottom of options, go to footer
handleDownFromPreview()
} else {
handleNavigate('down')
}
} else if (e.key === 'return') {
e.preventDefault()
handleSelectOption(focusedIndex)
} else if (e.key === 'n' && !e.ctrl && !e.meta) {
// Press 'n' to focus the notes input
e.preventDefault()
setIsInNotesInput(true)
onTextInputFocus(true)
} else if (e.key === 'escape') {
e.preventDefault()
onCancel()
} else if (e.key.length === 1 && e.key >= '1' && e.key <= '9') {
e.preventDefault()
const idx = parseInt(e.key, 10) - 1
if (idx < allOptions.length) {
handleNavigate(idx)
}
}
}
}, [isFooterFocused, footerIndex, isInPlanMode, isInNotesInput, focusedIndex, allOptions.length, handleUpFromFooter, handleDownFromPreview, handleNavigate, handleSelectOption, handleNotesExit, onRespondToClaude, onFinishPlanInterview, onCancel, onTextInputFocus]);
const previewContent = focusedOption?.preview || null;
},
[
isFooterFocused,
footerIndex,
isInPlanMode,
isInNotesInput,
focusedIndex,
allOptions.length,
handleUpFromFooter,
handleDownFromPreview,
handleNavigate,
handleSelectOption,
handleNotesExit,
onRespondToClaude,
onFinishPlanInterview,
onCancel,
onTextInputFocus,
],
)
const previewContent = focusedOption?.preview || null
// The right panel's available width is terminal minus the left panel and gap.
const LEFT_PANEL_WIDTH = 30;
const GAP = 4;
const {
columns
} = useTerminalSize();
const previewMaxWidth = columns - LEFT_PANEL_WIDTH - GAP;
const LEFT_PANEL_WIDTH = 30
const GAP = 4
const { columns } = useTerminalSize()
const previewMaxWidth = columns - LEFT_PANEL_WIDTH - GAP
// Lines used within the content area that aren't preview content:
// 1: marginTop on side-by-side box
@@ -245,19 +305,34 @@ export function PreviewQuestionView({
// 1: "Chat about this" line
// 1: plan mode line (may or may not show)
// 2: help text (marginTop=1 + text)
const PREVIEW_OVERHEAD = 11;
const PREVIEW_OVERHEAD = 11
// Compute the max lines available for preview content from the parent's
// height budget to prevent terminal overflow. We do NOT pad shorter options
// to match the tallest — the outer box's minHeight handles cross-question
// layout consistency, and within-question shifts are acceptable.
const previewMaxLines = useMemo(() => {
return minContentHeight ? Math.max(1, minContentHeight - PREVIEW_OVERHEAD) : undefined;
}, [minContentHeight]);
return <Box flexDirection="column" marginTop={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
return minContentHeight
? Math.max(1, minContentHeight - PREVIEW_OVERHEAD)
: undefined
}, [minContentHeight])
return (
<Box
flexDirection="column"
marginTop={1}
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Divider color="inactive" />
<Box flexDirection="column" paddingTop={0}>
<QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} hideSubmitTab={hideSubmitTab} />
<QuestionNavigationBar
questions={questions}
currentQuestionIndex={currentQuestionIndex}
answers={answers}
hideSubmitTab={hideSubmitTab}
/>
<PermissionRequestTitle title={question.question} color={'text'} />
<Box flexDirection="column" minHeight={minContentHeight}>
@@ -265,33 +340,71 @@ export function PreviewQuestionView({
<Box marginTop={1} flexDirection="row" gap={4}>
{/* Left panel: vertical option list */}
<Box flexDirection="column" width={30}>
{allOptions.map((option_0, index_0) => {
const isFocused = focusedIndex === index_0;
const isSelected = selectedValue === option_0.label;
return <Box key={option_0.label} flexDirection="row">
{isFocused ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
<Text dimColor> {index_0 + 1}.</Text>
<Text color={isSelected ? 'success' : isFocused ? 'suggestion' : undefined} bold={isFocused}>
{allOptions.map((option, index) => {
const isFocused = focusedIndex === index
const isSelected = selectedValue === option.label
return (
<Box key={option.label} flexDirection="row">
{isFocused ? (
<Text color="suggestion">{figures.pointer}</Text>
) : (
<Text> </Text>
)}
<Text dimColor> {index + 1}.</Text>
<Text
color={
isSelected
? 'success'
: isFocused
? 'suggestion'
: undefined
}
bold={isFocused}
>
{' '}
{option_0.label}
{option.label}
</Text>
{isSelected && <Text color="success"> {figures.tick}</Text>}
</Box>;
})}
</Box>
)
})}
</Box>
{/* Right panel: preview + notes */}
<Box flexDirection="column" flexGrow={1}>
<PreviewBox content={previewContent || 'No preview available'} maxLines={previewMaxLines} minWidth={minContentWidth} maxWidth={previewMaxWidth} />
<PreviewBox
content={previewContent || 'No preview available'}
maxLines={previewMaxLines}
minWidth={minContentWidth}
maxWidth={previewMaxWidth}
/>
<Box marginTop={1} flexDirection="row" gap={1}>
<Text color="suggestion">Notes:</Text>
{isInNotesInput ? <TextInput value={notesValue} placeholder="Add notes on this design…" onChange={value => {
onUpdateQuestionState(questionText, {
textInputValue: value
}, false);
}} onSubmit={handleNotesExit} onExit={handleNotesExit} focus={true} showCursor={true} columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /> : <Text dimColor italic>
{isInNotesInput ? (
<TextInput
value={notesValue}
placeholder="Add notes on this design…"
onChange={value => {
onUpdateQuestionState(
questionText,
{ textInputValue: value },
false,
)
}}
onSubmit={handleNotesExit}
onExit={handleNotesExit}
focus={true}
showCursor={true}
columns={60}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
/>
) : (
<Text dimColor italic>
{notesValue || 'press n to add notes'}
</Text>}
</Text>
)}
</Box>
</Box>
</Box>
@@ -300,28 +413,53 @@ export function PreviewQuestionView({
<Box flexDirection="column" marginTop={1}>
<Divider color="inactive" />
<Box flexDirection="row" gap={1}>
{isFooterFocused && footerIndex === 0 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
<Text color={isFooterFocused && footerIndex === 0 ? 'suggestion' : undefined}>
{isFooterFocused && footerIndex === 0 ? (
<Text color="suggestion">{figures.pointer}</Text>
) : (
<Text> </Text>
)}
<Text
color={
isFooterFocused && footerIndex === 0
? 'suggestion'
: undefined
}
>
Chat about this
</Text>
</Box>
{isInPlanMode && <Box flexDirection="row" gap={1}>
{isFooterFocused && footerIndex === 1 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}
<Text color={isFooterFocused && footerIndex === 1 ? 'suggestion' : undefined}>
{isInPlanMode && (
<Box flexDirection="row" gap={1}>
{isFooterFocused && footerIndex === 1 ? (
<Text color="suggestion">{figures.pointer}</Text>
) : (
<Text> </Text>
)}
<Text
color={
isFooterFocused && footerIndex === 1
? 'suggestion'
: undefined
}
>
Skip interview and plan immediately
</Text>
</Box>}
</Box>
)}
</Box>
<Box marginTop={1}>
<Text color="inactive" dimColor>
Enter to select · {figures.arrowUp}/{figures.arrowDown} to
navigate · n to add notes
{questions.length > 1 && <> · Tab to switch questions</>}
{isInNotesInput && editorName && <> · ctrl+g to edit in {editorName}</>}{' '}
{isInNotesInput && editorName && (
<> · ctrl+g to edit in {editorName}</>
)}{' '}
· Esc to cancel
</Text>
</Box>
</Box>
</Box>
</Box>;
</Box>
)
}

View File

@@ -1,177 +1,151 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import React, { useMemo } from 'react';
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
import { stringWidth } from '../../../ink/stringWidth.js';
import { Box, Text } from '../../../ink.js';
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
import { truncateToWidth } from '../../../utils/format.js';
import figures from 'figures'
import React, { useMemo } from 'react'
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
import { stringWidth } from '../../../ink/stringWidth.js'
import { Box, Text } from '../../../ink.js'
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
import { truncateToWidth } from '../../../utils/format.js'
type Props = {
questions: Question[];
currentQuestionIndex: number;
answers: Record<string, string>;
hideSubmitTab?: boolean;
};
export function QuestionNavigationBar(t0) {
const $ = _c(39);
const {
questions,
currentQuestionIndex,
answers,
hideSubmitTab: t1
} = t0;
const hideSubmitTab = t1 === undefined ? false : t1;
const {
columns
} = useTerminalSize();
let t2;
if ($[0] !== columns || $[1] !== currentQuestionIndex || $[2] !== hideSubmitTab || $[3] !== questions) {
bb0: {
const submitText = hideSubmitTab ? "" : ` ${figures.tick} Submit `;
const fixedWidth = stringWidth("\u2190 ") + stringWidth(" \u2192") + stringWidth(submitText);
const availableForTabs = columns - fixedWidth;
if (availableForTabs <= 0) {
let t3;
if ($[5] !== currentQuestionIndex || $[6] !== questions) {
let t4;
if ($[8] !== currentQuestionIndex) {
t4 = (q, index) => {
const header = q?.header || `Q${index + 1}`;
return index === currentQuestionIndex ? header.slice(0, 3) : "";
};
$[8] = currentQuestionIndex;
$[9] = t4;
} else {
t4 = $[9];
}
t3 = questions.map(t4);
$[5] = currentQuestionIndex;
$[6] = questions;
$[7] = t3;
} else {
t3 = $[7];
}
t2 = t3;
break bb0;
}
const tabHeaders = questions.map(_temp);
const idealWidths = tabHeaders.map(_temp2);
const totalIdealWidth = idealWidths.reduce(_temp3, 0);
if (totalIdealWidth <= availableForTabs) {
t2 = tabHeaders;
break bb0;
}
const currentHeader = tabHeaders[currentQuestionIndex] || "";
const currentIdealWidth = 4 + stringWidth(currentHeader);
const currentTabWidth = Math.min(currentIdealWidth, availableForTabs / 2);
const remainingWidth = availableForTabs - currentTabWidth;
const otherTabCount = questions.length - 1;
const widthPerOtherTab = Math.max(6, Math.floor(remainingWidth / Math.max(otherTabCount, 1)));
let t3;
if ($[10] !== currentQuestionIndex || $[11] !== currentTabWidth || $[12] !== widthPerOtherTab) {
t3 = (header_1, index_1) => {
if (index_1 === currentQuestionIndex) {
const maxTextWidth = currentTabWidth - 2 - 2;
return truncateToWidth(header_1, maxTextWidth);
} else {
const maxTextWidth_0 = widthPerOtherTab - 2 - 2;
return truncateToWidth(header_1, maxTextWidth_0);
}
};
$[10] = currentQuestionIndex;
$[11] = currentTabWidth;
$[12] = widthPerOtherTab;
$[13] = t3;
questions: Question[]
currentQuestionIndex: number
answers: Record<string, string>
hideSubmitTab?: boolean
}
export function QuestionNavigationBar({
questions,
currentQuestionIndex,
answers,
hideSubmitTab = false,
}: Props): React.ReactNode {
const { columns } = useTerminalSize()
// Calculate the display text for each tab based on available width
const tabDisplayTexts = useMemo(() => {
// Calculate fixed width elements
const leftArrow = '← '
const rightArrow = ' →'
const submitText = hideSubmitTab ? '' : ` ${figures.tick} Submit `
const checkboxWidth = 2 // checkbox + space
const paddingPerTab = 2 // space before and after each tab text
const fixedWidth =
stringWidth(leftArrow) + stringWidth(rightArrow) + stringWidth(submitText)
// Available width for all question tabs
const availableForTabs = columns - fixedWidth
if (availableForTabs <= 0) {
// Terminal too narrow, fallback to minimal display
return questions.map((q: Question, index: number) => {
const header = q?.header || `Q${index + 1}`
return index === currentQuestionIndex ? header.slice(0, 3) : ''
})
}
// Calculate ideal width for each tab (checkbox + padding + text)
const tabHeaders = questions.map(
(q: Question, index: number) => q?.header || `Q${index + 1}`,
)
const idealWidths = tabHeaders.map(
header => checkboxWidth + paddingPerTab + stringWidth(header),
)
// Calculate total ideal width
const totalIdealWidth = idealWidths.reduce((sum, w) => sum + w, 0)
// If everything fits, use full headers
if (totalIdealWidth <= availableForTabs) {
return tabHeaders
}
// Need to truncate - prioritize current tab
const currentHeader = tabHeaders[currentQuestionIndex] || ''
const currentIdealWidth =
checkboxWidth + paddingPerTab + stringWidth(currentHeader)
// Minimum width for other tabs (checkbox + padding + 1 char + ellipsis)
const minWidthPerTab = checkboxWidth + paddingPerTab + 2 // "X…"
// Calculate space for current tab (try to show full text)
const currentTabWidth = Math.min(currentIdealWidth, availableForTabs / 2)
const remainingWidth = availableForTabs - currentTabWidth
// Calculate space for other tabs
const otherTabCount = questions.length - 1
const widthPerOtherTab = Math.max(
minWidthPerTab,
Math.floor(remainingWidth / Math.max(otherTabCount, 1)),
)
return tabHeaders.map((header, index) => {
if (index === currentQuestionIndex) {
// Current tab - show as much as possible
const maxTextWidth = currentTabWidth - checkboxWidth - paddingPerTab
return truncateToWidth(header, maxTextWidth)
} else {
t3 = $[13];
// Other tabs - truncate to fit
const maxTextWidth = widthPerOtherTab - checkboxWidth - paddingPerTab
return truncateToWidth(header, maxTextWidth)
}
t2 = tabHeaders.map(t3);
}
$[0] = columns;
$[1] = currentQuestionIndex;
$[2] = hideSubmitTab;
$[3] = questions;
$[4] = t2;
} else {
t2 = $[4];
}
const tabDisplayTexts = t2;
const hideArrows = questions.length === 1 && hideSubmitTab;
let t3;
if ($[14] !== currentQuestionIndex || $[15] !== hideArrows) {
t3 = !hideArrows && <Text color={currentQuestionIndex === 0 ? "inactive" : undefined}>{" "}</Text>;
$[14] = currentQuestionIndex;
$[15] = hideArrows;
$[16] = t3;
} else {
t3 = $[16];
}
let t4;
if ($[17] !== answers || $[18] !== currentQuestionIndex || $[19] !== questions || $[20] !== tabDisplayTexts) {
let t5;
if ($[22] !== answers || $[23] !== currentQuestionIndex || $[24] !== tabDisplayTexts) {
t5 = (q_1, index_2) => {
const isSelected = index_2 === currentQuestionIndex;
const isAnswered = q_1?.question && !!answers[q_1.question];
const checkbox = isAnswered ? figures.checkboxOn : figures.checkboxOff;
const displayText = tabDisplayTexts[index_2] || q_1?.header || `Q${index_2 + 1}`;
return <Box key={q_1?.question || `question-${index_2}`}>{isSelected ? <Text backgroundColor="permission" color="inverseText">{" "}{checkbox} {displayText}{" "}</Text> : <Text>{" "}{checkbox} {displayText}{" "}</Text>}</Box>;
};
$[22] = answers;
$[23] = currentQuestionIndex;
$[24] = tabDisplayTexts;
$[25] = t5;
} else {
t5 = $[25];
}
t4 = questions.map(t5);
$[17] = answers;
$[18] = currentQuestionIndex;
$[19] = questions;
$[20] = tabDisplayTexts;
$[21] = t4;
} else {
t4 = $[21];
}
let t5;
if ($[26] !== currentQuestionIndex || $[27] !== hideSubmitTab || $[28] !== questions.length) {
t5 = !hideSubmitTab && <Box key="submit">{currentQuestionIndex === questions.length ? <Text backgroundColor="permission" color="inverseText">{" "}{figures.tick} Submit{" "}</Text> : <Text> {figures.tick} Submit </Text>}</Box>;
$[26] = currentQuestionIndex;
$[27] = hideSubmitTab;
$[28] = questions.length;
$[29] = t5;
} else {
t5 = $[29];
}
let t6;
if ($[30] !== currentQuestionIndex || $[31] !== hideArrows || $[32] !== questions.length) {
t6 = !hideArrows && <Text color={currentQuestionIndex === questions.length ? "inactive" : undefined}>{" "}</Text>;
$[30] = currentQuestionIndex;
$[31] = hideArrows;
$[32] = questions.length;
$[33] = t6;
} else {
t6 = $[33];
}
let t7;
if ($[34] !== t3 || $[35] !== t4 || $[36] !== t5 || $[37] !== t6) {
t7 = <Box flexDirection="row" marginBottom={1}>{t3}{t4}{t5}{t6}</Box>;
$[34] = t3;
$[35] = t4;
$[36] = t5;
$[37] = t6;
$[38] = t7;
} else {
t7 = $[38];
}
return t7;
}
function _temp3(sum, w) {
return sum + w;
}
function _temp2(header_0) {
return 4 + stringWidth(header_0);
}
function _temp(q_0, index_0) {
return q_0?.header || `Q${index_0 + 1}`;
})
}, [questions, currentQuestionIndex, columns, hideSubmitTab])
const hideArrows = questions.length === 1 && hideSubmitTab
return (
<Box flexDirection="row" marginBottom={1}>
{!hideArrows && (
<Text color={currentQuestionIndex === 0 ? 'inactive' : undefined}>
{' '}
</Text>
)}
{questions.map((q: Question, index: number) => {
const isSelected = index === currentQuestionIndex
const isAnswered = q?.question && !!answers[q.question]
const checkbox = isAnswered ? figures.checkboxOn : figures.checkboxOff
const displayText =
tabDisplayTexts[index] || q?.header || `Q${index + 1}`
return (
<Box key={q?.question || `question-${index}`}>
{isSelected ? (
<Text backgroundColor="permission" color="inverseText">
{' '}
{checkbox} {displayText}{' '}
</Text>
) : (
<Text>
{' '}
{checkbox} {displayText}{' '}
</Text>
)}
</Box>
)
})}
{!hideSubmitTab && (
<Box key="submit">
{currentQuestionIndex === questions.length ? (
<Text backgroundColor="permission" color="inverseText">
{' '}
{figures.tick} Submit{' '}
</Text>
) : (
<Text> {figures.tick} Submit </Text>
)}
</Box>
)}
{!hideArrows && (
<Text
color={
currentQuestionIndex === questions.length ? 'inactive' : undefined
}
>
{' '}
</Text>
)}
</Box>
)
}

View File

@@ -1,464 +1,398 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import React, { useCallback, useState } from 'react';
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js';
import { Box, Text } from '../../../ink.js';
import { useAppState } from '../../../state/AppState.js';
import type { Question, QuestionOption } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
import type { PastedContent } from '../../../utils/config.js';
import { getExternalEditor } from '../../../utils/editor.js';
import { toIDEDisplayName } from '../../../utils/ide.js';
import type { ImageDimensions } from '../../../utils/imageResizer.js';
import { editPromptInEditor } from '../../../utils/promptEditor.js';
import { type OptionWithDescription, Select, SelectMulti } from '../../CustomSelect/index.js';
import { Divider } from '../../design-system/Divider.js';
import { FilePathLink } from '../../FilePathLink.js';
import { PermissionRequestTitle } from '../PermissionRequestTitle.js';
import { PreviewQuestionView } from './PreviewQuestionView.js';
import { QuestionNavigationBar } from './QuestionNavigationBar.js';
import type { QuestionState } from './use-multiple-choice-state.js';
import figures from 'figures'
import React, { useCallback, useState } from 'react'
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'
import { Box, Text } from '../../../ink.js'
import { useAppState } from '../../../state/AppState.js'
import type {
Question,
QuestionOption,
} from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
import type { PastedContent } from '../../../utils/config.js'
import { getExternalEditor } from '../../../utils/editor.js'
import { toIDEDisplayName } from '../../../utils/ide.js'
import type { ImageDimensions } from '../../../utils/imageResizer.js'
import { editPromptInEditor } from '../../../utils/promptEditor.js'
import {
type OptionWithDescription,
Select,
SelectMulti,
} from '../../CustomSelect/index.js'
import { Divider } from '../../design-system/Divider.js'
import { FilePathLink } from '../../FilePathLink.js'
import { PermissionRequestTitle } from '../PermissionRequestTitle.js'
import { PreviewQuestionView } from './PreviewQuestionView.js'
import { QuestionNavigationBar } from './QuestionNavigationBar.js'
import type { QuestionState } from './use-multiple-choice-state.js'
type Props = {
question: Question;
questions: Question[];
currentQuestionIndex: number;
answers: Record<string, string>;
questionStates: Record<string, QuestionState>;
hideSubmitTab?: boolean;
planFilePath?: string;
pastedContents?: Record<number, PastedContent>;
minContentHeight?: number;
minContentWidth?: number;
onUpdateQuestionState: (questionText: string, updates: Partial<QuestionState>, isMultiSelect: boolean) => void;
onAnswer: (questionText: string, label: string | string[], textInput?: string, shouldAdvance?: boolean) => void;
onTextInputFocus: (isInInput: boolean) => void;
onCancel: () => void;
onSubmit: () => void;
onTabPrev?: () => void;
onTabNext?: () => void;
onRespondToClaude: () => void;
onFinishPlanInterview: () => void;
onImagePaste?: (base64Image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) => void;
onRemoveImage?: (id: number) => void;
};
export function QuestionView(t0) {
const $ = _c(114);
const {
question,
questions,
currentQuestionIndex,
answers,
questionStates,
hideSubmitTab: t1,
planFilePath,
minContentHeight,
minContentWidth,
onUpdateQuestionState,
onAnswer,
onTextInputFocus,
onCancel,
onSubmit,
onTabPrev,
onTabNext,
onRespondToClaude,
onFinishPlanInterview,
onImagePaste,
pastedContents,
onRemoveImage
} = t0;
const hideSubmitTab = t1 === undefined ? false : t1;
const isInPlanMode = useAppState(_temp) === "plan";
const [isFooterFocused, setIsFooterFocused] = useState(false);
const [footerIndex, setFooterIndex] = useState(0);
const [isOtherFocused, setIsOtherFocused] = useState(false);
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const editor = getExternalEditor();
t2 = editor ? toIDEDisplayName(editor) : null;
$[0] = t2;
} else {
t2 = $[0];
}
const editorName = t2;
let t3;
if ($[1] !== onTextInputFocus) {
t3 = value => {
const isOther = value === "__other__";
setIsOtherFocused(isOther);
onTextInputFocus(isOther);
};
$[1] = onTextInputFocus;
$[2] = t3;
} else {
t3 = $[2];
}
const handleFocus = t3;
let t4;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t4 = () => {
setIsFooterFocused(true);
};
$[3] = t4;
} else {
t4 = $[3];
}
const handleDownFromLastItem = t4;
let t5;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t5 = () => {
setIsFooterFocused(false);
};
$[4] = t5;
} else {
t5 = $[4];
}
const handleUpFromFooter = t5;
let t6;
if ($[5] !== footerIndex || $[6] !== isFooterFocused || $[7] !== isInPlanMode || $[8] !== onCancel || $[9] !== onFinishPlanInterview || $[10] !== onRespondToClaude) {
t6 = e => {
if (!isFooterFocused) {
return;
}
if (e.key === "up" || e.ctrl && e.key === "p") {
e.preventDefault();
question: Question
questions: Question[]
currentQuestionIndex: number
answers: Record<string, string>
questionStates: Record<string, QuestionState>
hideSubmitTab?: boolean
planFilePath?: string
pastedContents?: Record<number, PastedContent>
minContentHeight?: number
minContentWidth?: number
onUpdateQuestionState: (
questionText: string,
updates: Partial<QuestionState>,
isMultiSelect: boolean,
) => void
onAnswer: (
questionText: string,
label: string | string[],
textInput?: string,
shouldAdvance?: boolean,
) => void
onTextInputFocus: (isInInput: boolean) => void
onCancel: () => void
onSubmit: () => void
onTabPrev?: () => void
onTabNext?: () => void
onRespondToClaude: () => void
onFinishPlanInterview: () => void
onImagePaste?: (
base64Image: string,
mediaType?: string,
filename?: string,
dimensions?: ImageDimensions,
sourcePath?: string,
) => void
onRemoveImage?: (id: number) => void
}
export function QuestionView({
question,
questions,
currentQuestionIndex,
answers,
questionStates,
hideSubmitTab = false,
planFilePath,
minContentHeight,
minContentWidth,
onUpdateQuestionState,
onAnswer,
onTextInputFocus,
onCancel,
onSubmit,
onTabPrev,
onTabNext,
onRespondToClaude,
onFinishPlanInterview,
onImagePaste,
pastedContents,
onRemoveImage,
}: Props): React.ReactNode {
const isInPlanMode = useAppState(s => s.toolPermissionContext.mode) === 'plan'
const [isFooterFocused, setIsFooterFocused] = useState(false)
const [footerIndex, setFooterIndex] = useState(0)
const [isOtherFocused, setIsOtherFocused] = useState(false)
const editor = getExternalEditor()
const editorName = editor ? toIDEDisplayName(editor) : null
const handleFocus = useCallback(
(value: string) => {
const isOther = value === '__other__'
setIsOtherFocused(isOther)
onTextInputFocus(isOther)
},
[onTextInputFocus],
)
const handleDownFromLastItem = useCallback(() => {
setIsFooterFocused(true)
}, [])
const handleUpFromFooter = useCallback(() => {
setIsFooterFocused(false)
}, [])
// Handle keyboard input when footer is focused
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (!isFooterFocused) return
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
e.preventDefault()
if (footerIndex === 0) {
handleUpFromFooter();
handleUpFromFooter()
} else {
setFooterIndex(0);
setFooterIndex(0)
}
return;
return
}
if (e.key === "down" || e.ctrl && e.key === "n") {
e.preventDefault();
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
e.preventDefault()
if (isInPlanMode && footerIndex === 0) {
setFooterIndex(1);
setFooterIndex(1)
}
return;
return
}
if (e.key === "return") {
e.preventDefault();
if (e.key === 'return') {
e.preventDefault()
if (footerIndex === 0) {
onRespondToClaude();
onRespondToClaude()
} else {
onFinishPlanInterview();
onFinishPlanInterview()
}
return;
return
}
if (e.key === "escape") {
e.preventDefault();
onCancel();
if (e.key === 'escape') {
e.preventDefault()
onCancel()
}
};
$[5] = footerIndex;
$[6] = isFooterFocused;
$[7] = isInPlanMode;
$[8] = onCancel;
$[9] = onFinishPlanInterview;
$[10] = onRespondToClaude;
$[11] = t6;
} else {
t6 = $[11];
},
[
isFooterFocused,
footerIndex,
isInPlanMode,
handleUpFromFooter,
onRespondToClaude,
onFinishPlanInterview,
onCancel,
],
)
const textOptions: OptionWithDescription<string>[] = question.options.map(
(opt: QuestionOption) => ({
type: 'text' as const,
value: opt.label,
label: opt.label,
description: opt.description,
}),
)
const questionText = question.question
const questionState = questionStates[questionText]
const handleOpenEditor = useCallback(
async (currentValue: string, setValue: (value: string) => void) => {
const result = await editPromptInEditor(currentValue)
if (result.content !== null && result.content !== currentValue) {
// Update the Select's internal state for immediate UI update
setValue(result.content)
// Also update the question state for persistence
onUpdateQuestionState(
questionText,
{ textInputValue: result.content },
question.multiSelect ?? false,
)
}
},
[questionText, onUpdateQuestionState, question.multiSelect],
)
const otherOption: OptionWithDescription<string> = {
type: 'input' as const,
value: '__other__',
label: 'Other',
placeholder: question.multiSelect ? 'Type something' : 'Type something.',
initialValue: questionState?.textInputValue ?? '',
onChange: (value: string) => {
onUpdateQuestionState(
questionText,
{ textInputValue: value },
question.multiSelect ?? false,
)
},
}
const handleKeyDown = t6;
let handleOpenEditor;
let questionText;
let t7;
if ($[12] !== onUpdateQuestionState || $[13] !== question || $[14] !== questionStates) {
const textOptions = question.options.map(_temp2);
questionText = question.question;
const questionState = questionStates[questionText];
let t8;
if ($[18] !== onUpdateQuestionState || $[19] !== question.multiSelect || $[20] !== questionText) {
t8 = async (currentValue, setValue) => {
const result = await editPromptInEditor(currentValue);
if (result.content !== null && result.content !== currentValue) {
setValue(result.content);
onUpdateQuestionState(questionText, {
textInputValue: result.content
}, question.multiSelect ?? false);
}
};
$[18] = onUpdateQuestionState;
$[19] = question.multiSelect;
$[20] = questionText;
$[21] = t8;
} else {
t8 = $[21];
}
handleOpenEditor = t8;
const t9 = question.multiSelect ? "Type something" : "Type something.";
const t10 = questionState?.textInputValue ?? "";
let t11;
if ($[22] !== onUpdateQuestionState || $[23] !== question.multiSelect || $[24] !== questionText) {
t11 = value_0 => {
onUpdateQuestionState(questionText, {
textInputValue: value_0
}, question.multiSelect ?? false);
};
$[22] = onUpdateQuestionState;
$[23] = question.multiSelect;
$[24] = questionText;
$[25] = t11;
} else {
t11 = $[25];
}
let t12;
if ($[26] !== t10 || $[27] !== t11 || $[28] !== t9) {
t12 = {
type: "input" as const,
value: "__other__",
label: "Other",
placeholder: t9,
initialValue: t10,
onChange: t11
};
$[26] = t10;
$[27] = t11;
$[28] = t9;
$[29] = t12;
} else {
t12 = $[29];
}
const otherOption = t12;
t7 = [...textOptions, otherOption];
$[12] = onUpdateQuestionState;
$[13] = question;
$[14] = questionStates;
$[15] = handleOpenEditor;
$[16] = questionText;
$[17] = t7;
} else {
handleOpenEditor = $[15];
questionText = $[16];
t7 = $[17];
}
const options = t7;
const hasAnyPreview = !question.multiSelect && question.options.some(_temp3);
const options = [...textOptions, otherOption]
// Check if any option has a preview and it's not multi-select
// Previews only supported for single-select questions
const hasAnyPreview =
!question.multiSelect && question.options.some(opt => opt.preview)
// Delegate to PreviewQuestionView for carousel-style preview mode
if (hasAnyPreview) {
let t8;
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) {
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} />;
$[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;
$[46] = t8;
} else {
t8 = $[46];
}
return t8;
return (
<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}
/>
)
}
let t8;
if ($[47] !== isInPlanMode || $[48] !== planFilePath) {
t8 = isInPlanMode && planFilePath && <Box flexDirection="column" gap={0}><Divider color="inactive" /><Text color="inactive">Planning: <FilePathLink filePath={planFilePath} /></Text></Box>;
$[47] = isInPlanMode;
$[48] = planFilePath;
$[49] = t8;
} else {
t8 = $[49];
}
let t9;
if ($[50] === Symbol.for("react.memo_cache_sentinel")) {
t9 = <Box marginTop={-1}><Divider color="inactive" /></Box>;
$[50] = t9;
} else {
t9 = $[50];
}
let t10;
if ($[51] !== answers || $[52] !== currentQuestionIndex || $[53] !== hideSubmitTab || $[54] !== questions) {
t10 = <QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} hideSubmitTab={hideSubmitTab} />;
$[51] = answers;
$[52] = currentQuestionIndex;
$[53] = hideSubmitTab;
$[54] = questions;
$[55] = t10;
} else {
t10 = $[55];
}
let t11;
if ($[56] !== question.question) {
t11 = <PermissionRequestTitle title={question.question} color="text" />;
$[56] = question.question;
$[57] = t11;
} else {
t11 = $[57];
}
let t12;
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) {
t12 = <Box marginTop={1}>{question.multiSelect ? <SelectMulti key={question.question} options={options} defaultValue={questionStates[question.question]?.selectedValue as string[] | undefined} onChange={values => {
onUpdateQuestionState(questionText, {
selectedValue: values
}, true);
const textInput = values.includes("__other__") ? questionStates[questionText]?.textInputValue : undefined;
const finalValues = values.filter(_temp4).concat(textInput ? [textInput] : []);
onAnswer(questionText, finalValues, undefined, false);
}} 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 => {
onUpdateQuestionState(questionText, {
selectedValue: value_1
}, false);
const textInput_0 = value_1 === "__other__" ? questionStates[questionText]?.textInputValue : undefined;
onAnswer(questionText, value_1, textInput_0);
}} onFocus={handleFocus} onCancel={onCancel} onDownFromLastItem={handleDownFromLastItem} isDisabled={isFooterFocused} layout="compact-vertical" onOpenEditor={handleOpenEditor} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage} />}</Box>;
$[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;
$[75] = t12;
} else {
t12 = $[75];
}
let t13;
if ($[76] === Symbol.for("react.memo_cache_sentinel")) {
t13 = <Divider color="inactive" />;
$[76] = t13;
} else {
t13 = $[76];
}
let t14;
if ($[77] !== footerIndex || $[78] !== isFooterFocused) {
t14 = isFooterFocused && footerIndex === 0 ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>;
$[77] = footerIndex;
$[78] = isFooterFocused;
$[79] = t14;
} else {
t14 = $[79];
}
const t15 = isFooterFocused && footerIndex === 0 ? "suggestion" : undefined;
const t16 = options.length + 1;
let t17;
if ($[80] !== t15 || $[81] !== t16) {
t17 = <Text color={t15}>{t16}. Chat about this</Text>;
$[80] = t15;
$[81] = t16;
$[82] = t17;
} else {
t17 = $[82];
}
let t18;
if ($[83] !== t14 || $[84] !== t17) {
t18 = <Box flexDirection="row" gap={1}>{t14}{t17}</Box>;
$[83] = t14;
$[84] = t17;
$[85] = t18;
} else {
t18 = $[85];
}
let t19;
if ($[86] !== footerIndex || $[87] !== isFooterFocused || $[88] !== isInPlanMode || $[89] !== options.length) {
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>;
$[86] = footerIndex;
$[87] = isFooterFocused;
$[88] = isInPlanMode;
$[89] = options.length;
$[90] = t19;
} else {
t19 = $[90];
}
let t20;
if ($[91] !== t18 || $[92] !== t19) {
t20 = <Box flexDirection="column">{t13}{t18}{t19}</Box>;
$[91] = t18;
$[92] = t19;
$[93] = t20;
} else {
t20 = $[93];
}
let t21;
if ($[94] !== questions.length) {
t21 = questions.length === 1 ? <>{figures.arrowUp}/{figures.arrowDown} to navigate</> : "Tab/Arrow keys to navigate";
$[94] = questions.length;
$[95] = t21;
} else {
t21 = $[95];
}
let t22;
if ($[96] !== isOtherFocused) {
t22 = isOtherFocused && editorName && <> · ctrl+g to edit in {editorName}</>;
$[96] = isOtherFocused;
$[97] = t22;
} else {
t22 = $[97];
}
let t23;
if ($[98] !== t21 || $[99] !== t22) {
t23 = <Box marginTop={1}><Text color="inactive" dimColor={true}>Enter to select ·{" "}{t21}{t22}{" "}· Esc to cancel</Text></Box>;
$[98] = t21;
$[99] = t22;
$[100] = t23;
} else {
t23 = $[100];
}
let t24;
if ($[101] !== minContentHeight || $[102] !== t12 || $[103] !== t20 || $[104] !== t23) {
t24 = <Box flexDirection="column" minHeight={minContentHeight}>{t12}{t20}{t23}</Box>;
$[101] = minContentHeight;
$[102] = t12;
$[103] = t20;
$[104] = t23;
$[105] = t24;
} else {
t24 = $[105];
}
let t25;
if ($[106] !== t10 || $[107] !== t11 || $[108] !== t24) {
t25 = <Box flexDirection="column" paddingTop={0}>{t10}{t11}{t24}</Box>;
$[106] = t10;
$[107] = t11;
$[108] = t24;
$[109] = t25;
} else {
t25 = $[109];
}
let t26;
if ($[110] !== handleKeyDown || $[111] !== t25 || $[112] !== t8) {
t26 = <Box flexDirection="column" marginTop={0} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t8}{t9}{t25}</Box>;
$[110] = handleKeyDown;
$[111] = t25;
$[112] = t8;
$[113] = t26;
} else {
t26 = $[113];
}
return t26;
}
function _temp4(v) {
return v !== "__other__";
}
function _temp3(opt_0) {
return opt_0.preview;
}
function _temp2(opt) {
return {
type: "text" as const,
value: opt.label,
label: opt.label,
description: opt.description
};
}
function _temp(s) {
return s.toolPermissionContext.mode;
return (
<Box
flexDirection="column"
marginTop={0}
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
{isInPlanMode && planFilePath && (
<Box flexDirection="column" gap={0}>
<Divider color="inactive" />
<Text color="inactive">
Planning: <FilePathLink filePath={planFilePath} />
</Text>
</Box>
)}
<Box marginTop={-1}>
<Divider color="inactive" />
</Box>
<Box flexDirection="column" paddingTop={0}>
<QuestionNavigationBar
questions={questions}
currentQuestionIndex={currentQuestionIndex}
answers={answers}
hideSubmitTab={hideSubmitTab}
/>
<PermissionRequestTitle title={question.question} color={'text'} />
<Box flexDirection="column" minHeight={minContentHeight}>
<Box marginTop={1}>
{question.multiSelect ? (
<SelectMulti
key={question.question}
options={options}
defaultValue={
questionStates[question.question]?.selectedValue as
| string[]
| undefined
}
onChange={(values: string[]) => {
onUpdateQuestionState(
questionText,
{ selectedValue: values },
true,
)
const textInput = values.includes('__other__')
? questionStates[questionText]?.textInputValue
: undefined
const finalValues = values
.filter(v => v !== '__other__')
.concat(textInput ? [textInput] : [])
onAnswer(questionText, finalValues, undefined, false)
}}
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: string) => {
onUpdateQuestionState(
questionText,
{ selectedValue: value },
false,
)
const textInput =
value === '__other__'
? questionStates[questionText]?.textInputValue
: undefined
onAnswer(questionText, value, textInput)
}}
onFocus={handleFocus}
onCancel={onCancel}
onDownFromLastItem={handleDownFromLastItem}
isDisabled={isFooterFocused}
layout="compact-vertical"
onOpenEditor={handleOpenEditor}
onImagePaste={onImagePaste}
pastedContents={pastedContents}
onRemoveImage={onRemoveImage}
/>
)}
</Box>
{/* Footer section - always visible, separate from Select */}
<Box flexDirection="column">
<Divider color="inactive" />
<Box flexDirection="row" gap={1}>
{isFooterFocused && footerIndex === 0 ? (
<Text color="suggestion">{figures.pointer}</Text>
) : (
<Text> </Text>
)}
<Text
color={
isFooterFocused && footerIndex === 0
? 'suggestion'
: undefined
}
>
{options.length + 1}. Chat about this
</Text>
</Box>
{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>
)}
</Box>
<Box marginTop={1}>
<Text color="inactive" dimColor>
Enter to select ·{' '}
{questions.length === 1 ? (
<>
{figures.arrowUp}/{figures.arrowDown} to navigate
</>
) : (
'Tab/Arrow keys to navigate'
)}
{isOtherFocused && editorName && (
<> · ctrl+g to edit in {editorName}</>
)}{' '}
· Esc to cancel
</Text>
</Box>
</Box>
</Box>
</Box>
)
}

View File

@@ -1,143 +1,104 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import React from 'react';
import { Box, Text } from '../../../ink.js';
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
import type { PermissionDecision } from '../../../utils/permissions/PermissionResult.js';
import { Select } from '../../CustomSelect/index.js';
import { Divider } from '../../design-system/Divider.js';
import { PermissionRequestTitle } from '../PermissionRequestTitle.js';
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js';
import { QuestionNavigationBar } from './QuestionNavigationBar.js';
import figures from 'figures'
import React from 'react'
import { Box, Text } from '../../../ink.js'
import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
import type { PermissionDecision } from '../../../utils/permissions/PermissionResult.js'
import { Select } from '../../CustomSelect/index.js'
import { Divider } from '../../design-system/Divider.js'
import { PermissionRequestTitle } from '../PermissionRequestTitle.js'
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
import { QuestionNavigationBar } from './QuestionNavigationBar.js'
type Props = {
questions: Question[];
currentQuestionIndex: number;
answers: Record<string, string>;
allQuestionsAnswered: boolean;
permissionResult: PermissionDecision;
minContentHeight?: number;
onFinalResponse: (value: 'submit' | 'cancel') => void;
};
export function SubmitQuestionsView(t0) {
const $ = _c(27);
const {
questions,
currentQuestionIndex,
answers,
allQuestionsAnswered,
permissionResult,
minContentHeight,
onFinalResponse
} = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Divider color="inactive" />;
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== answers || $[2] !== currentQuestionIndex || $[3] !== questions) {
t2 = <QuestionNavigationBar questions={questions} currentQuestionIndex={currentQuestionIndex} answers={answers} />;
$[1] = answers;
$[2] = currentQuestionIndex;
$[3] = questions;
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <PermissionRequestTitle title="Review your answers" color="text" />;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] !== allQuestionsAnswered) {
t4 = !allQuestionsAnswered && <Box marginBottom={1}><Text color="warning">{figures.warning} You have not answered all questions</Text></Box>;
$[6] = allQuestionsAnswered;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== answers || $[9] !== questions) {
t5 = Object.keys(answers).length > 0 && <Box flexDirection="column" marginBottom={1}>{questions.filter(q => q?.question && answers[q.question]).map(q_0 => {
const answer = answers[q_0?.question];
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>;
})}</Box>;
$[8] = answers;
$[9] = questions;
$[10] = t5;
} else {
t5 = $[10];
}
let t6;
if ($[11] !== permissionResult) {
t6 = <PermissionRuleExplanation permissionResult={permissionResult} toolType="tool" />;
$[11] = permissionResult;
$[12] = t6;
} else {
t6 = $[12];
}
let t7;
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
t7 = <Text color="inactive">Ready to submit your answers?</Text>;
$[13] = t7;
} else {
t7 = $[13];
}
let t8;
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
t8 = {
type: "text" as const,
label: "Submit answers",
value: "submit"
};
$[14] = t8;
} else {
t8 = $[14];
}
let t9;
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
t9 = [t8, {
type: "text" as const,
label: "Cancel",
value: "cancel"
}];
$[15] = t9;
} else {
t9 = $[15];
}
let t10;
if ($[16] !== onFinalResponse) {
t10 = <Box marginTop={1}><Select options={t9} onChange={value => onFinalResponse(value as 'submit' | 'cancel')} onCancel={() => onFinalResponse("cancel")} /></Box>;
$[16] = onFinalResponse;
$[17] = t10;
} else {
t10 = $[17];
}
let t11;
if ($[18] !== minContentHeight || $[19] !== t10 || $[20] !== t4 || $[21] !== t5 || $[22] !== t6) {
t11 = <Box flexDirection="column" marginTop={1} minHeight={minContentHeight}>{t4}{t5}{t6}{t7}{t10}</Box>;
$[18] = minContentHeight;
$[19] = t10;
$[20] = t4;
$[21] = t5;
$[22] = t6;
$[23] = t11;
} else {
t11 = $[23];
}
let t12;
if ($[24] !== t11 || $[25] !== t2) {
t12 = <Box flexDirection="column" marginTop={1}>{t1}<Box flexDirection="column" borderTop={true} borderColor="inactive" paddingTop={0}>{t2}{t3}{t11}</Box></Box>;
$[24] = t11;
$[25] = t2;
$[26] = t12;
} else {
t12 = $[26];
}
return t12;
questions: Question[]
currentQuestionIndex: number
answers: Record<string, string>
allQuestionsAnswered: boolean
permissionResult: PermissionDecision
minContentHeight?: number
onFinalResponse: (value: 'submit' | 'cancel') => void
}
export function SubmitQuestionsView({
questions,
currentQuestionIndex,
answers,
allQuestionsAnswered,
permissionResult,
minContentHeight,
onFinalResponse,
}: Props): React.ReactNode {
return (
<Box flexDirection="column" marginTop={1}>
<Divider color="inactive" />
<Box
flexDirection="column"
borderTop
borderColor="inactive"
paddingTop={0}
>
<QuestionNavigationBar
questions={questions}
currentQuestionIndex={currentQuestionIndex}
answers={answers}
/>
<PermissionRequestTitle title="Review your answers" color="text" />
<Box flexDirection="column" marginTop={1} minHeight={minContentHeight}>
{!allQuestionsAnswered && (
<Box marginBottom={1}>
<Text color="warning">
{figures.warning} You have not answered all questions
</Text>
</Box>
)}
{Object.keys(answers).length > 0 && (
<Box flexDirection="column" marginBottom={1}>
{questions
.filter((q: Question) => q?.question && answers[q.question])
.map((q: Question) => {
const answer = answers[q?.question]
return (
<Box
key={q?.question || 'answer'}
flexDirection="column"
marginLeft={1}
>
<Text>
{figures.bullet} {q?.question || 'Question'}
</Text>
<Box marginLeft={2}>
<Text color="success">
{figures.arrowRight} {answer}
</Text>
</Box>
</Box>
)
})}
</Box>
)}
<PermissionRuleExplanation
permissionResult={permissionResult}
toolType="tool"
/>
<Text color="inactive">Ready to submit your answers?</Text>
<Box marginTop={1}>
<Select
options={[
{
type: 'text' as const,
label: 'Submit answers',
value: 'submit',
},
{ type: 'text' as const, label: 'Cancel', value: 'cancel' },
]}
onChange={value => onFinalResponse(value as 'submit' | 'cancel')}
onCancel={() => onFinalResponse('cancel')}
/>
</Box>
</Box>
</Box>
</Box>
)
}

View File

@@ -1,37 +1,51 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import figures from 'figures';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Text, useTheme } from '../../../ink.js';
import { useKeybinding } from '../../../keybindings/useKeybinding.js';
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js';
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js';
import { useAppState } from '../../../state/AppState.js';
import { BashTool } from '../../../tools/BashTool/BashTool.js';
import { getFirstWordPrefix, getSimpleCommandPrefix } from '../../../tools/BashTool/bashPermissions.js';
import { getDestructiveCommandWarning } from '../../../tools/BashTool/destructiveCommandWarning.js';
import { parseSedEditCommand } from '../../../tools/BashTool/sedEditParser.js';
import { shouldUseSandbox } from '../../../tools/BashTool/shouldUseSandbox.js';
import { getCompoundCommandPrefixesStatic } from '../../../utils/bash/prefix.js';
import { createPromptRuleContent, generateGenericDescription, getBashPromptAllowDescriptions, isClassifierPermissionsEnabled } from '../../../utils/permissions/bashClassifier.js';
import { extractRules } from '../../../utils/permissions/PermissionUpdate.js';
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js';
import { SandboxManager } from '../../../utils/sandbox/sandbox-adapter.js';
import { Select } from '../../CustomSelect/select.js';
import { ShimmerChar } from '../../Spinner/ShimmerChar.js';
import { useShimmerAnimation } from '../../Spinner/useShimmerAnimation.js';
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js';
import { PermissionDecisionDebugInfo } from '../PermissionDecisionDebugInfo.js';
import { PermissionDialog } from '../PermissionDialog.js';
import { PermissionExplainerContent, usePermissionExplainerUI } from '../PermissionExplanation.js';
import type { PermissionRequestProps } from '../PermissionRequest.js';
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js';
import { SedEditPermissionRequest } from '../SedEditPermissionRequest/SedEditPermissionRequest.js';
import { useShellPermissionFeedback } from '../useShellPermissionFeedback.js';
import { logUnaryPermissionEvent } from '../utils.js';
import { bashToolUseOptions } from './bashToolUseOptions.js';
const CHECKING_TEXT = 'Attempting to auto-approve\u2026';
import { feature } from 'bun:bundle'
import figures from 'figures'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Box, Text, useTheme } from '../../../ink.js'
import { useKeybinding } from '../../../keybindings/useKeybinding.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../../services/analytics/index.js'
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'
import { useAppState } from '../../../state/AppState.js'
import { BashTool } from '../../../tools/BashTool/BashTool.js'
import {
getFirstWordPrefix,
getSimpleCommandPrefix,
} from '../../../tools/BashTool/bashPermissions.js'
import { getDestructiveCommandWarning } from '../../../tools/BashTool/destructiveCommandWarning.js'
import { parseSedEditCommand } from '../../../tools/BashTool/sedEditParser.js'
import { shouldUseSandbox } from '../../../tools/BashTool/shouldUseSandbox.js'
import { getCompoundCommandPrefixesStatic } from '../../../utils/bash/prefix.js'
import {
createPromptRuleContent,
generateGenericDescription,
getBashPromptAllowDescriptions,
isClassifierPermissionsEnabled,
} from '../../../utils/permissions/bashClassifier.js'
import { extractRules } from '../../../utils/permissions/PermissionUpdate.js'
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
import { SandboxManager } from '../../../utils/sandbox/sandbox-adapter.js'
import { Select } from '../../CustomSelect/select.js'
import { ShimmerChar } from '../../Spinner/ShimmerChar.js'
import { useShimmerAnimation } from '../../Spinner/useShimmerAnimation.js'
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'
import { PermissionDecisionDebugInfo } from '../PermissionDecisionDebugInfo.js'
import { PermissionDialog } from '../PermissionDialog.js'
import {
PermissionExplainerContent,
usePermissionExplainerUI,
} from '../PermissionExplanation.js'
import type { PermissionRequestProps } from '../PermissionRequest.js'
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
import { SedEditPermissionRequest } from '../SedEditPermissionRequest/SedEditPermissionRequest.js'
import { useShellPermissionFeedback } from '../useShellPermissionFeedback.js'
import { logUnaryPermissionEvent } from '../utils.js'
import { bashToolUseOptions } from './bashToolUseOptions.js'
const CHECKING_TEXT = 'Attempting to auto-approve\u2026'
// Isolates the 20fps shimmer clock from BashPermissionRequestInner. Before this
// extraction, useShimmerAnimation lived inside the 535-line Inner body, so every
@@ -39,97 +53,77 @@ const CHECKING_TEXT = 'Attempting to auto-approve\u2026';
// all children) for the ~1-3 seconds the classifier typically takes. Inner also
// has a Compiler bailout (see below), so nothing was auto-memoized — the full
// JSX tree was reconstructed 20-60 times per classifier check.
function ClassifierCheckingSubtitle() {
const $ = _c(6);
const [ref, glimmerIndex] = useShimmerAnimation("requesting", CHECKING_TEXT, false);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [...CHECKING_TEXT];
$[0] = t0;
} else {
t0 = $[0];
}
let t1;
if ($[1] !== glimmerIndex) {
t1 = <Text>{t0.map((char, i) => <ShimmerChar key={i} char={char} index={i} glimmerIndex={glimmerIndex} messageColor="inactive" shimmerColor="subtle" />)}</Text>;
$[1] = glimmerIndex;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== ref || $[4] !== t1) {
t2 = <Box ref={ref}>{t1}</Box>;
$[3] = ref;
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
function ClassifierCheckingSubtitle(): React.ReactNode {
const [ref, glimmerIndex] = useShimmerAnimation(
'requesting',
CHECKING_TEXT,
false,
)
return (
<Box ref={ref}>
<Text>
{[...CHECKING_TEXT].map((char, i) => (
<ShimmerChar
key={i}
char={char}
index={i}
glimmerIndex={glimmerIndex}
messageColor="inactive"
shimmerColor="subtle"
/>
))}
</Text>
</Box>
)
}
export function BashPermissionRequest(props) {
const $ = _c(21);
export function BashPermissionRequest(
props: PermissionRequestProps,
): React.ReactNode {
const {
toolUseConfirm,
toolUseContext,
onDone,
onReject,
verbose,
workerBadge
} = props;
let command;
let description;
let t0;
if ($[0] !== toolUseConfirm.input) {
({
command,
description
} = BashTool.inputSchema.parse(toolUseConfirm.input));
t0 = parseSedEditCommand(command);
$[0] = toolUseConfirm.input;
$[1] = command;
$[2] = description;
$[3] = t0;
} else {
command = $[1];
description = $[2];
t0 = $[3];
}
const sedInfo = t0;
workerBadge,
} = props
const { command, description } = BashTool.inputSchema.parse(
toolUseConfirm.input,
)
// Detect sed in-place edit commands and delegate to SedEditPermissionRequest
// This renders sed edits like file edits with a diff view
const sedInfo = parseSedEditCommand(command)
if (sedInfo) {
let t1;
if ($[4] !== onDone || $[5] !== onReject || $[6] !== sedInfo || $[7] !== toolUseConfirm || $[8] !== toolUseContext || $[9] !== verbose || $[10] !== workerBadge) {
t1 = <SedEditPermissionRequest toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} sedInfo={sedInfo} />;
$[4] = onDone;
$[5] = onReject;
$[6] = sedInfo;
$[7] = toolUseConfirm;
$[8] = toolUseContext;
$[9] = verbose;
$[10] = workerBadge;
$[11] = t1;
} else {
t1 = $[11];
}
return t1;
return (
<SedEditPermissionRequest
toolUseConfirm={toolUseConfirm}
toolUseContext={toolUseContext}
onDone={onDone}
onReject={onReject}
verbose={verbose}
workerBadge={workerBadge}
sedInfo={sedInfo}
/>
)
}
let t1;
if ($[12] !== command || $[13] !== description || $[14] !== onDone || $[15] !== onReject || $[16] !== toolUseConfirm || $[17] !== toolUseContext || $[18] !== verbose || $[19] !== workerBadge) {
t1 = <BashPermissionRequestInner toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} command={command} description={description} />;
$[12] = command;
$[13] = description;
$[14] = onDone;
$[15] = onReject;
$[16] = toolUseConfirm;
$[17] = toolUseContext;
$[18] = verbose;
$[19] = workerBadge;
$[20] = t1;
} else {
t1 = $[20];
}
return t1;
// Regular bash command - render with hooks
return (
<BashPermissionRequestInner
toolUseConfirm={toolUseConfirm}
toolUseContext={toolUseContext}
onDone={onDone}
onReject={onReject}
verbose={verbose}
workerBadge={workerBadge}
command={command}
description={description}
/>
)
}
// Inner component that uses hooks - only called for non-MCP CLI commands
@@ -141,19 +135,19 @@ function BashPermissionRequestInner({
verbose: _verbose,
workerBadge,
command,
description
description,
}: PermissionRequestProps & {
command: string;
description?: string;
command: string
description?: string
}): React.ReactNode {
const [theme] = useTheme();
const toolPermissionContext = useAppState(s => s.toolPermissionContext);
const [theme] = useTheme()
const toolPermissionContext = useAppState(s => s.toolPermissionContext)
const explainerState = usePermissionExplainerUI({
toolName: toolUseConfirm.tool.name,
toolInput: toolUseConfirm.input,
toolDescription: toolUseConfirm.description,
messages: toolUseContext.messages
});
messages: toolUseContext.messages,
})
const {
yesInputMode,
noInputMode,
@@ -166,31 +160,39 @@ function BashPermissionRequestInner({
focusedOption,
handleInputModeToggle,
handleReject,
handleFocus
handleFocus,
} = useShellPermissionFeedback({
toolUseConfirm,
onDone,
onReject,
explainerVisible: explainerState.visible
});
const [showPermissionDebug, setShowPermissionDebug] = useState(false);
const [classifierDescription, setClassifierDescription] = useState(description || '');
explainerVisible: explainerState.visible,
})
const [showPermissionDebug, setShowPermissionDebug] = useState(false)
const [classifierDescription, setClassifierDescription] = useState(
description || '',
)
// Track whether the initial description (from prop or async generation) was empty.
// Once we receive a non-empty description, this stays false.
const [initialClassifierDescriptionEmpty, setInitialClassifierDescriptionEmpty] = useState(!description?.trim());
const [
initialClassifierDescriptionEmpty,
setInitialClassifierDescriptionEmpty,
] = useState(!description?.trim())
// Asynchronously generate a generic description for the classifier
useEffect(() => {
if (!isClassifierPermissionsEnabled()) return;
const abortController = new AbortController();
generateGenericDescription(command, description, abortController.signal).then(generic => {
if (generic && !abortController.signal.aborted) {
setClassifierDescription(generic);
setInitialClassifierDescriptionEmpty(false);
}
}).catch(() => {}); // Keep original on error
return () => abortController.abort();
}, [command, description]);
if (!isClassifierPermissionsEnabled()) return
const abortController = new AbortController()
generateGenericDescription(command, description, abortController.signal)
.then(generic => {
if (generic && !abortController.signal.aborted) {
setClassifierDescription(generic)
setInitialClassifierDescriptionEmpty(false)
}
})
.catch(() => {}) // Keep original on error
return () => abortController.abort()
}, [command, description])
// GH#11380: For compound commands (cd src && git status && npm test), the
// backend already computed correct per-subcommand suggestions via tree-sitter
@@ -206,7 +208,8 @@ function BashPermissionRequestInner({
// from the backend rule. When compound with 2+ rules, editablePrefix stays
// undefined so bashToolUseOptions falls through to yes-apply-suggestions,
// which saves all per-subcommand rules atomically.
const isCompound = toolUseConfirm.permissionResult.decisionReason?.type === 'subcommandResults';
const isCompound =
toolUseConfirm.permissionResult.decisionReason?.type === 'subcommandResults'
// Editable prefix — initialize synchronously with the best prefix we can
// extract without tree-sitter, then refine via tree-sitter for compound
@@ -216,49 +219,63 @@ function BashPermissionRequestInner({
//
// Lazy initializer: this runs regex + split on every render if left in
// the render body; it's only needed for initial state.
const [editablePrefix, setEditablePrefix] = useState<string | undefined>(() => {
if (isCompound) {
// Backend suggestion is the source of truth for compound commands.
// Single rule → seed the editable input so the user can refine it.
// Multiple/zero rulesundefined → yes-apply-suggestions handles it.
const backendBashRules = extractRules('suggestions' in toolUseConfirm.permissionResult ? toolUseConfirm.permissionResult.suggestions : undefined).filter(r => r.toolName === BashTool.name && r.ruleContent);
return backendBashRules.length === 1 ? backendBashRules[0]!.ruleContent : undefined;
}
const two = getSimpleCommandPrefix(command);
if (two) return `${two}:*`;
const one = getFirstWordPrefix(command);
if (one) return `${one}:*`;
return command;
});
const hasUserEditedPrefix = useRef(false);
const [editablePrefix, setEditablePrefix] = useState<string | undefined>(
() => {
if (isCompound) {
// Backend suggestion is the source of truth for compound commands.
// Single rule → seed the editable input so the user can refine it.
// Multiple/zero rules → undefined → yes-apply-suggestions handles it.
const backendBashRules = extractRules(
'suggestions' in toolUseConfirm.permissionResult
? toolUseConfirm.permissionResult.suggestions
: undefined,
).filter(r => r.toolName === BashTool.name && r.ruleContent)
return backendBashRules.length === 1
? backendBashRules[0]!.ruleContent
: undefined
}
const two = getSimpleCommandPrefix(command)
if (two) return `${two}:*`
const one = getFirstWordPrefix(command)
if (one) return `${one}:*`
return command
},
)
const hasUserEditedPrefix = useRef(false)
const onEditablePrefixChange = useCallback((value: string) => {
hasUserEditedPrefix.current = true;
setEditablePrefix(value);
}, []);
hasUserEditedPrefix.current = true
setEditablePrefix(value)
}, [])
useEffect(() => {
// Skip async refinement for compound commands — the backend already ran
// the full per-subcommand analysis and its suggestion is correct.
if (isCompound) return;
let cancelled = false;
getCompoundCommandPrefixesStatic(command, subcmd => BashTool.isReadOnly({
command: subcmd
})).then(prefixes => {
if (cancelled || hasUserEditedPrefix.current) return;
if (prefixes.length > 0) {
setEditablePrefix(`${prefixes[0]}:*`);
}
}).catch(() => {}); // Keep sync prefix on tree-sitter failure
if (isCompound) return
let cancelled = false
getCompoundCommandPrefixesStatic(command, subcmd =>
BashTool.isReadOnly({ command: subcmd }),
)
.then(prefixes => {
if (cancelled || hasUserEditedPrefix.current) return
if (prefixes.length > 0) {
setEditablePrefix(`${prefixes[0]}:*`)
}
})
.catch(() => {}) // Keep sync prefix on tree-sitter failure
return () => {
cancelled = true;
};
}, [command, isCompound]);
cancelled = true
}
}, [command, isCompound])
// Track whether classifier check was ever in progress (persists after completion).
// classifierCheckInProgress is set once at queue-push time (interactiveHandler)
// and only ever transitions true→false, so capturing the mount-time value is
// sufficient — no latch/ref needed. The feature() ternary keeps the property
// read out of external builds (forbidden-string check).
const [classifierWasChecking] = useState(feature('BASH_CLASSIFIER') ? !!toolUseConfirm.classifierCheckInProgress : false);
const [classifierWasChecking] = useState(
feature('BASH_CLASSIFIER')
? !!toolUseConfirm.classifierCheckInProgress
: false,
)
// These derive solely from the tool input (fixed for the dialog lifetime).
// The shimmer clock used to live in this component and re-render it at 20fps
@@ -266,216 +283,330 @@ function BashPermissionRequestInner({
// extraction). React Compiler can't auto-memoize imported functions (can't
// prove side-effect freedom), so this useMemo still guards against any
// re-render source (e.g. Inner state updates). Same pattern as PR#20730.
const {
destructiveWarning: destructiveWarning_0,
sandboxingEnabled: sandboxingEnabled_0,
isSandboxed: isSandboxed_0
} = useMemo(() => {
const destructiveWarning = getFeatureValue_CACHED_MAY_BE_STALE('tengu_destructive_command_warning', false) ? getDestructiveCommandWarning(command) : null;
const sandboxingEnabled = SandboxManager.isSandboxingEnabled();
const isSandboxed = sandboxingEnabled && shouldUseSandbox(toolUseConfirm.input);
return {
destructiveWarning,
sandboxingEnabled,
isSandboxed
};
}, [command, toolUseConfirm.input]);
const unaryEvent = useMemo<UnaryEvent>(() => ({
completion_type: 'tool_use_single',
language_name: 'none'
}), []);
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
const existingAllowDescriptions = useMemo(() => getBashPromptAllowDescriptions(toolPermissionContext), [toolPermissionContext]);
const options = useMemo(() => bashToolUseOptions({
suggestions: toolUseConfirm.permissionResult.behavior === 'ask' ? toolUseConfirm.permissionResult.suggestions : undefined,
decisionReason: toolUseConfirm.permissionResult.decisionReason,
onRejectFeedbackChange: setRejectFeedback,
onAcceptFeedbackChange: setAcceptFeedback,
onClassifierDescriptionChange: setClassifierDescription,
classifierDescription,
initialClassifierDescriptionEmpty,
existingAllowDescriptions,
yesInputMode,
noInputMode,
editablePrefix,
onEditablePrefixChange
}), [toolUseConfirm, classifierDescription, initialClassifierDescriptionEmpty, existingAllowDescriptions, yesInputMode, noInputMode, editablePrefix, onEditablePrefixChange]);
const { destructiveWarning, sandboxingEnabled, isSandboxed } = useMemo(() => {
const destructiveWarning = getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_destructive_command_warning',
false,
)
? getDestructiveCommandWarning(command)
: null
const sandboxingEnabled = SandboxManager.isSandboxingEnabled()
const isSandboxed =
sandboxingEnabled && shouldUseSandbox(toolUseConfirm.input)
return { destructiveWarning, sandboxingEnabled, isSandboxed }
}, [command, toolUseConfirm.input])
const unaryEvent = useMemo<UnaryEvent>(
() => ({ completion_type: 'tool_use_single', language_name: 'none' }),
[],
)
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
const existingAllowDescriptions = useMemo(
() => getBashPromptAllowDescriptions(toolPermissionContext),
[toolPermissionContext],
)
const options = useMemo(
() =>
bashToolUseOptions({
suggestions:
toolUseConfirm.permissionResult.behavior === 'ask'
? toolUseConfirm.permissionResult.suggestions
: undefined,
decisionReason: toolUseConfirm.permissionResult.decisionReason,
onRejectFeedbackChange: setRejectFeedback,
onAcceptFeedbackChange: setAcceptFeedback,
onClassifierDescriptionChange: setClassifierDescription,
classifierDescription,
initialClassifierDescriptionEmpty,
existingAllowDescriptions,
yesInputMode,
noInputMode,
editablePrefix,
onEditablePrefixChange,
}),
[
toolUseConfirm,
classifierDescription,
initialClassifierDescriptionEmpty,
existingAllowDescriptions,
yesInputMode,
noInputMode,
editablePrefix,
onEditablePrefixChange,
],
)
// Toggle permission debug info with keybinding
const handleToggleDebug = useCallback(() => {
setShowPermissionDebug(prev => !prev);
}, []);
setShowPermissionDebug(prev => !prev)
}, [])
useKeybinding('permission:toggleDebug', handleToggleDebug, {
context: 'Confirmation'
});
context: 'Confirmation',
})
// Allow Esc to dismiss the checkmark after auto-approval
const handleDismissCheckmark = useCallback(() => {
toolUseConfirm.onDismissCheckmark?.();
}, [toolUseConfirm]);
toolUseConfirm.onDismissCheckmark?.()
}, [toolUseConfirm])
useKeybinding('confirm:no', handleDismissCheckmark, {
context: 'Confirmation',
isActive: feature('BASH_CLASSIFIER') ? !!toolUseConfirm.classifierAutoApproved : false
});
function onSelect(value_0: string) {
isActive: feature('BASH_CLASSIFIER')
? !!toolUseConfirm.classifierAutoApproved
: false,
})
function onSelect(value: string) {
// Map options to numeric values for analytics (strings not allowed in logEvent)
let optionIndex: Record<string, number> = {
yes: 1,
'yes-apply-suggestions': 2,
'yes-prefix-edited': 2,
no: 3
};
no: 3,
}
if (feature('BASH_CLASSIFIER')) {
optionIndex = {
yes: 1,
'yes-apply-suggestions': 2,
'yes-prefix-edited': 2,
'yes-classifier-reviewed': 3,
no: 4
};
no: 4,
}
}
logEvent('tengu_permission_request_option_selected', {
option_index: optionIndex[value_0],
explainer_visible: explainerState.visible
});
const toolNameForAnalytics = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS;
if (value_0 === 'yes-prefix-edited') {
const trimmedPrefix = (editablePrefix ?? '').trim();
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
option_index: optionIndex[value],
explainer_visible: explainerState.visible,
})
const toolNameForAnalytics = sanitizeToolNameForAnalytics(
toolUseConfirm.tool.name,
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
if (value === 'yes-prefix-edited') {
const trimmedPrefix = (editablePrefix ?? '').trim()
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
if (!trimmedPrefix) {
toolUseConfirm.onAllow(toolUseConfirm.input, []);
toolUseConfirm.onAllow(toolUseConfirm.input, [])
} else {
const prefixUpdates: PermissionUpdate[] = [{
type: 'addRules',
rules: [{
toolName: BashTool.name,
ruleContent: trimmedPrefix
}],
behavior: 'allow',
destination: 'localSettings'
}];
toolUseConfirm.onAllow(toolUseConfirm.input, prefixUpdates);
const prefixUpdates: PermissionUpdate[] = [
{
type: 'addRules',
rules: [
{
toolName: BashTool.name,
ruleContent: trimmedPrefix,
},
],
behavior: 'allow',
destination: 'localSettings',
},
]
toolUseConfirm.onAllow(toolUseConfirm.input, prefixUpdates)
}
onDone();
return;
onDone()
return
}
if (feature('BASH_CLASSIFIER') && value_0 === 'yes-classifier-reviewed') {
const trimmedDescription = classifierDescription.trim();
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
if (feature('BASH_CLASSIFIER') && value === 'yes-classifier-reviewed') {
const trimmedDescription = classifierDescription.trim()
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
if (!trimmedDescription) {
toolUseConfirm.onAllow(toolUseConfirm.input, []);
toolUseConfirm.onAllow(toolUseConfirm.input, [])
} else {
const permissionUpdates: PermissionUpdate[] = [{
type: 'addRules',
rules: [{
toolName: BashTool.name,
ruleContent: createPromptRuleContent(trimmedDescription)
}],
behavior: 'allow',
destination: 'session'
}];
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates);
const permissionUpdates: PermissionUpdate[] = [
{
type: 'addRules',
rules: [
{
toolName: BashTool.name,
ruleContent: createPromptRuleContent(trimmedDescription),
},
],
behavior: 'allow',
destination: 'session',
},
]
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates)
}
onDone();
return;
onDone()
return
}
switch (value_0) {
case 'yes':
{
const trimmedFeedback_0 = acceptFeedback.trim();
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
// Log accept submission with feedback context
logEvent('tengu_accept_submitted', {
toolName: toolNameForAnalytics,
isMcp: toolUseConfirm.tool.isMcp ?? false,
has_instructions: !!trimmedFeedback_0,
instructions_length: trimmedFeedback_0.length,
entered_feedback_mode: yesFeedbackModeEntered
});
toolUseConfirm.onAllow(toolUseConfirm.input, [], trimmedFeedback_0 || undefined);
onDone();
break;
}
case 'yes-apply-suggestions':
{
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
// Extract suggestions if present (works for both 'ask' and 'passthrough' behaviors)
const permissionUpdates_0 = 'suggestions' in toolUseConfirm.permissionResult ? toolUseConfirm.permissionResult.suggestions || [] : [];
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates_0);
onDone();
break;
}
case 'no':
{
const trimmedFeedback = rejectFeedback.trim();
// Log reject submission with feedback context
logEvent('tengu_reject_submitted', {
toolName: toolNameForAnalytics,
isMcp: toolUseConfirm.tool.isMcp ?? false,
has_instructions: !!trimmedFeedback,
instructions_length: trimmedFeedback.length,
entered_feedback_mode: noFeedbackModeEntered
});
switch (value) {
case 'yes': {
const trimmedFeedback = acceptFeedback.trim()
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
// Log accept submission with feedback context
logEvent('tengu_accept_submitted', {
toolName: toolNameForAnalytics,
isMcp: toolUseConfirm.tool.isMcp ?? false,
has_instructions: !!trimmedFeedback,
instructions_length: trimmedFeedback.length,
entered_feedback_mode: yesFeedbackModeEntered,
})
toolUseConfirm.onAllow(
toolUseConfirm.input,
[],
trimmedFeedback || undefined,
)
onDone()
break
}
case 'yes-apply-suggestions': {
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
// Extract suggestions if present (works for both 'ask' and 'passthrough' behaviors)
const permissionUpdates =
'suggestions' in toolUseConfirm.permissionResult
? toolUseConfirm.permissionResult.suggestions || []
: []
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates)
onDone()
break
}
case 'no': {
const trimmedFeedback = rejectFeedback.trim()
// Process rejection (with or without feedback)
handleReject(trimmedFeedback || undefined);
break;
}
// Log reject submission with feedback context
logEvent('tengu_reject_submitted', {
toolName: toolNameForAnalytics,
isMcp: toolUseConfirm.tool.isMcp ?? false,
has_instructions: !!trimmedFeedback,
instructions_length: trimmedFeedback.length,
entered_feedback_mode: noFeedbackModeEntered,
})
// Process rejection (with or without feedback)
handleReject(trimmedFeedback || undefined)
break
}
}
}
const classifierSubtitle = feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved ? <Text>
const classifierSubtitle = feature('BASH_CLASSIFIER') ? (
toolUseConfirm.classifierAutoApproved ? (
<Text>
<Text color="success">{figures.tick} Auto-approved</Text>
{toolUseConfirm.classifierMatchedRule && <Text dimColor>
{toolUseConfirm.classifierMatchedRule && (
<Text dimColor>
{' \u00b7 matched "'}
{toolUseConfirm.classifierMatchedRule}
{'"'}
</Text>}
</Text> : toolUseConfirm.classifierCheckInProgress ? <ClassifierCheckingSubtitle /> : classifierWasChecking ? <Text dimColor>Requires manual approval</Text> : undefined : undefined;
return <PermissionDialog workerBadge={workerBadge} title={sandboxingEnabled_0 && !isSandboxed_0 ? 'Bash command (unsandboxed)' : 'Bash command'} subtitle={classifierSubtitle}>
</Text>
)}
</Text>
) : toolUseConfirm.classifierCheckInProgress ? (
<ClassifierCheckingSubtitle />
) : classifierWasChecking ? (
<Text dimColor>Requires manual approval</Text>
) : undefined
) : undefined
return (
<PermissionDialog
workerBadge={workerBadge}
title={
sandboxingEnabled && !isSandboxed
? 'Bash command (unsandboxed)'
: 'Bash command'
}
subtitle={classifierSubtitle}
>
<Box flexDirection="column" paddingX={2} paddingY={1}>
<Text dimColor={explainerState.visible}>
{BashTool.renderToolUseMessage({
command,
description
}, {
theme,
verbose: true
} // always show the full command
)}
{BashTool.renderToolUseMessage(
{ command, description },
{ theme, verbose: true }, // always show the full command
)}
</Text>
{!explainerState.visible && <Text dimColor>{toolUseConfirm.description}</Text>}
<PermissionExplainerContent visible={explainerState.visible} promise={explainerState.promise} />
{!explainerState.visible && (
<Text dimColor>{toolUseConfirm.description}</Text>
)}
<PermissionExplainerContent
visible={explainerState.visible}
promise={explainerState.promise}
/>
</Box>
{showPermissionDebug ? <>
<PermissionDecisionDebugInfo permissionResult={toolUseConfirm.permissionResult} toolName="Bash" />
{toolUseContext.options.debug && <Box justifyContent="flex-end" marginTop={1}>
{showPermissionDebug ? (
<>
<PermissionDecisionDebugInfo
permissionResult={toolUseConfirm.permissionResult}
toolName="Bash"
/>
{toolUseContext.options.debug && (
<Box justifyContent="flex-end" marginTop={1}>
<Text dimColor>Ctrl-D to hide debug info</Text>
</Box>}
</> : <>
</Box>
)}
</>
) : (
<>
<Box flexDirection="column">
<PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="command" />
{destructiveWarning_0 && <Box marginBottom={1}>
<Text color="warning" dimColor={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved : false}>
{destructiveWarning_0}
<PermissionRuleExplanation
permissionResult={toolUseConfirm.permissionResult}
toolType="command"
/>
{destructiveWarning && (
<Box marginBottom={1}>
<Text
color="warning"
dimColor={
feature('BASH_CLASSIFIER')
? toolUseConfirm.classifierAutoApproved
: false
}
>
{destructiveWarning}
</Text>
</Box>}
<Text dimColor={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved : false}>
</Box>
)}
<Text
dimColor={
feature('BASH_CLASSIFIER')
? toolUseConfirm.classifierAutoApproved
: false
}
>
Do you want to proceed?
</Text>
<Select options={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved ? options.map(o => ({
...o,
disabled: true
})) : options : options} isDisabled={feature('BASH_CLASSIFIER') ? toolUseConfirm.classifierAutoApproved : false} inlineDescriptions onChange={onSelect} onCancel={() => handleReject()} onFocus={handleFocus} onInputModeToggle={handleInputModeToggle} />
<Select
options={
feature('BASH_CLASSIFIER')
? toolUseConfirm.classifierAutoApproved
? options.map(o => ({ ...o, disabled: true }))
: options
: options
}
isDisabled={
feature('BASH_CLASSIFIER')
? toolUseConfirm.classifierAutoApproved
: false
}
inlineDescriptions
onChange={onSelect}
onCancel={() => handleReject()}
onFocus={handleFocus}
onInputModeToggle={handleInputModeToggle}
/>
</Box>
<Box justifyContent="space-between" marginTop={1}>
<Text dimColor>
Esc to cancel
{(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'}
{explainerState.enabled && ` · ctrl+e to ${explainerState.visible ? 'hide' : 'explain'}`}
{((focusedOption === 'yes' && !yesInputMode) ||
(focusedOption === 'no' && !noInputMode)) &&
' · Tab to amend'}
{explainerState.enabled &&
` · ctrl+e to ${explainerState.visible ? 'hide' : 'explain'}`}
</Text>
{toolUseContext.options.debug && <Text dimColor>Ctrl+d to show debug info</Text>}
{toolUseContext.options.debug && (
<Text dimColor>Ctrl+d to show debug info</Text>
)}
</Box>
</>}
</PermissionDialog>;
</>
)}
</PermissionDialog>
)
}

View File

@@ -1,33 +1,43 @@
import { BASH_TOOL_NAME } from '../../../tools/BashTool/toolName.js';
import { extractOutputRedirections } from '../../../utils/bash/commands.js';
import { isClassifierPermissionsEnabled } from '../../../utils/permissions/bashClassifier.js';
import type { PermissionDecisionReason } from '../../../utils/permissions/PermissionResult.js';
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js';
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js';
import type { OptionWithDescription } from '../../CustomSelect/select.js';
import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js';
export type BashToolUseOption = 'yes' | 'yes-apply-suggestions' | 'yes-prefix-edited' | 'yes-classifier-reviewed' | 'no';
import { BASH_TOOL_NAME } from '../../../tools/BashTool/toolName.js'
import { extractOutputRedirections } from '../../../utils/bash/commands.js'
import { isClassifierPermissionsEnabled } from '../../../utils/permissions/bashClassifier.js'
import type { PermissionDecisionReason } from '../../../utils/permissions/PermissionResult.js'
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'
import type { OptionWithDescription } from '../../CustomSelect/select.js'
import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js'
export type BashToolUseOption =
| 'yes'
| 'yes-apply-suggestions'
| 'yes-prefix-edited'
| 'yes-classifier-reviewed'
| 'no'
/**
* Check if a description already exists in the allow list.
* Compares lowercase and trailing-whitespace-trimmed versions.
*/
function descriptionAlreadyExists(description: string, existingDescriptions: string[]): boolean {
const normalized = description.toLowerCase().trimEnd();
return existingDescriptions.some(existing => existing.toLowerCase().trimEnd() === normalized);
function descriptionAlreadyExists(
description: string,
existingDescriptions: string[],
): boolean {
const normalized = description.toLowerCase().trimEnd()
return existingDescriptions.some(
existing => existing.toLowerCase().trimEnd() === normalized,
)
}
/**
* Strip output redirections so filenames don't show as commands in the label.
*/
function stripBashRedirections(command: string): string {
const {
commandWithoutRedirections,
redirections
} = extractOutputRedirections(command);
const { commandWithoutRedirections, redirections } =
extractOutputRedirections(command)
// Only use stripped version if there were actual redirections
return redirections.length > 0 ? commandWithoutRedirections : command;
return redirections.length > 0 ? commandWithoutRedirections : command
}
export function bashToolUseOptions({
suggestions = [],
decisionReason,
@@ -40,25 +50,26 @@ export function bashToolUseOptions({
yesInputMode = false,
noInputMode = false,
editablePrefix,
onEditablePrefixChange
onEditablePrefixChange,
}: {
suggestions?: PermissionUpdate[];
decisionReason?: PermissionDecisionReason;
onRejectFeedbackChange: (value: string) => void;
onAcceptFeedbackChange: (value: string) => void;
onClassifierDescriptionChange?: (value: string) => void;
classifierDescription?: string;
suggestions?: PermissionUpdate[]
decisionReason?: PermissionDecisionReason
onRejectFeedbackChange: (value: string) => void
onAcceptFeedbackChange: (value: string) => void
onClassifierDescriptionChange?: (value: string) => void
classifierDescription?: string
/** Whether the initial classifier description was empty. When true, hides the option. */
initialClassifierDescriptionEmpty?: boolean;
existingAllowDescriptions?: string[];
yesInputMode?: boolean;
noInputMode?: boolean;
initialClassifierDescriptionEmpty?: boolean
existingAllowDescriptions?: string[]
yesInputMode?: boolean
noInputMode?: boolean
/** Editable prefix rule content (e.g., "npm run:*"). When set, replaces Haiku-based suggestions. */
editablePrefix?: string;
editablePrefix?: string
/** Callback when the user edits the prefix value. */
onEditablePrefixChange?: (value: string) => void;
onEditablePrefixChange?: (value: string) => void
}): OptionWithDescription<BashToolUseOption>[] {
const options: OptionWithDescription<BashToolUseOption>[] = [];
const options: OptionWithDescription<BashToolUseOption>[] = []
if (yesInputMode) {
options.push({
type: 'input',
@@ -66,13 +77,13 @@ export function bashToolUseOptions({
value: 'yes',
placeholder: 'and tell Claude what to do next',
onChange: onAcceptFeedbackChange,
allowEmptySubmitToCancel: true
});
allowEmptySubmitToCancel: true,
})
} else {
options.push({
label: 'Yes',
value: 'yes'
});
value: 'yes',
})
}
// Only show "always allow" options when not restricted by allowManagedPermissionRulesOnly
@@ -81,8 +92,18 @@ export function bashToolUseOptions({
// Haiku-generated suggestion label — but only when the suggestions
// don't contain non-Bash items (addDirectories, Read rules) that
// the editable prefix can't represent.
const hasNonBashSuggestions = suggestions.some(s => s.type === 'addDirectories' || s.type === 'addRules' && s.rules?.some(r => r.toolName !== BASH_TOOL_NAME));
if (editablePrefix !== undefined && onEditablePrefixChange && !hasNonBashSuggestions && suggestions.length > 0) {
const hasNonBashSuggestions = suggestions.some(
s =>
s.type === 'addDirectories' ||
(s.type === 'addRules' &&
s.rules?.some(r => r.toolName !== BASH_TOOL_NAME)),
)
if (
editablePrefix !== undefined &&
onEditablePrefixChange &&
!hasNonBashSuggestions &&
suggestions.length > 0
) {
options.push({
type: 'input',
label: 'Yes, and don\u2019t ask again for',
@@ -93,15 +114,20 @@ export function bashToolUseOptions({
allowEmptySubmitToCancel: true,
showLabelWithValue: true,
labelValueSeparator: ': ',
resetCursorOnUpdate: true
});
resetCursorOnUpdate: true,
})
} else if (suggestions.length > 0) {
const label = generateShellSuggestionsLabel(suggestions, BASH_TOOL_NAME, stripBashRedirections);
const label = generateShellSuggestionsLabel(
suggestions,
BASH_TOOL_NAME,
stripBashRedirections,
)
if (label) {
options.push({
label,
value: 'yes-apply-suggestions'
});
value: 'yes-apply-suggestions',
})
}
}
@@ -111,8 +137,21 @@ export function bashToolUseOptions({
// (prompt-based rules don't help when the server-side classifier triggers first).
// Skip when the editable prefix option is already shown — they serve the
// same role and having two identical-looking "don't ask again" inputs is confusing.
const editablePrefixShown = options.some(o => o.value === 'yes-prefix-edited');
if ((process.env.USER_TYPE) === 'ant' && !editablePrefixShown && isClassifierPermissionsEnabled() && onClassifierDescriptionChange && !initialClassifierDescriptionEmpty && !descriptionAlreadyExists(classifierDescription ?? '', existingAllowDescriptions) && decisionReason?.type !== 'classifier') {
const editablePrefixShown = options.some(
o => o.value === 'yes-prefix-edited',
)
if (
process.env.USER_TYPE === 'ant' &&
!editablePrefixShown &&
isClassifierPermissionsEnabled() &&
onClassifierDescriptionChange &&
!initialClassifierDescriptionEmpty &&
!descriptionAlreadyExists(
classifierDescription ?? '',
existingAllowDescriptions,
) &&
decisionReason?.type !== 'classifier'
) {
options.push({
type: 'input',
label: 'Yes, and don\u2019t ask again for',
@@ -123,10 +162,11 @@ export function bashToolUseOptions({
allowEmptySubmitToCancel: true,
showLabelWithValue: true,
labelValueSeparator: ': ',
resetCursorOnUpdate: true
});
resetCursorOnUpdate: true,
})
}
}
if (noInputMode) {
options.push({
type: 'input',
@@ -134,13 +174,14 @@ export function bashToolUseOptions({
value: 'no',
placeholder: 'and tell Claude what to do differently',
onChange: onRejectFeedbackChange,
allowEmptySubmitToCancel: true
});
allowEmptySubmitToCancel: true,
})
} else {
options.push({
label: 'No',
value: 'no'
});
value: 'no',
})
}
return options;
return options
}

View File

@@ -1,25 +1,29 @@
import { c as _c } from "react/compiler-runtime";
import { getSentinelCategory } from '@ant/computer-use-mcp/sentinelApps';
import type { CuPermissionRequest, CuPermissionResponse } from '@ant/computer-use-mcp/types';
import { DEFAULT_GRANT_FLAGS } from '@ant/computer-use-mcp/types';
import figures from 'figures';
import * as React from 'react';
import { useMemo, useState } from 'react';
import { Box, Text } from '../../../ink.js';
import { execFileNoThrow } from '../../../utils/execFileNoThrow.js';
import { plural } from '../../../utils/stringUtils.js';
import type { OptionWithDescription } from '../../CustomSelect/select.js';
import { Select } from '../../CustomSelect/select.js';
import { Dialog } from '../../design-system/Dialog.js';
import { getSentinelCategory } from '@ant/computer-use-mcp/sentinelApps'
import type {
CuPermissionRequest,
CuPermissionResponse,
} from '@ant/computer-use-mcp/types'
import { DEFAULT_GRANT_FLAGS } from '@ant/computer-use-mcp/types'
import figures from 'figures'
import * as React from 'react'
import { useMemo, useState } from 'react'
import { Box, Text } from '../../../ink.js'
import { execFileNoThrow } from '../../../utils/execFileNoThrow.js'
import { plural } from '../../../utils/stringUtils.js'
import type { OptionWithDescription } from '../../CustomSelect/select.js'
import { Select } from '../../CustomSelect/select.js'
import { Dialog } from '../../design-system/Dialog.js'
type ComputerUseApprovalProps = {
request: CuPermissionRequest;
onDone: (response: CuPermissionResponse) => void;
};
request: CuPermissionRequest
onDone: (response: CuPermissionResponse) => void
}
const DENY_ALL_RESPONSE: CuPermissionResponse = {
granted: [],
denied: [],
flags: DEFAULT_GRANT_FLAGS
};
flags: DEFAULT_GRANT_FLAGS,
}
/**
* Two-panel dispatcher. When `request.tccState` is present, macOS permissions
@@ -27,414 +31,271 @@ const DENY_ALL_RESPONSE: CuPermissionResponse = {
* irrelevant — show a TCC panel that opens System Settings. Otherwise show the
* app allowlist + grant-flags panel.
*/
export function ComputerUseApproval(t0) {
const $ = _c(3);
const {
request,
onDone
} = t0;
let t1;
if ($[0] !== onDone || $[1] !== request) {
t1 = request.tccState ? <ComputerUseTccPanel tccState={request.tccState} onDone={() => onDone(DENY_ALL_RESPONSE)} /> : <ComputerUseAppListPanel request={request} onDone={onDone} />;
$[0] = onDone;
$[1] = request;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
export function ComputerUseApproval({
request,
onDone,
}: ComputerUseApprovalProps): React.ReactNode {
return request.tccState ? (
<ComputerUseTccPanel
tccState={request.tccState}
onDone={() => onDone(DENY_ALL_RESPONSE)}
/>
) : (
<ComputerUseAppListPanel request={request} onDone={onDone} />
)
}
// ── TCC panel ─────────────────────────────────────────────────────────────
type TccOption = 'open_accessibility' | 'open_screen_recording' | 'retry';
function ComputerUseTccPanel(t0) {
const $ = _c(26);
const {
tccState,
onDone
} = t0;
let opts;
if ($[0] !== tccState.accessibility || $[1] !== tccState.screenRecording) {
opts = [];
type TccOption = 'open_accessibility' | 'open_screen_recording' | 'retry'
function ComputerUseTccPanel({
tccState,
onDone,
}: {
tccState: NonNullable<CuPermissionRequest['tccState']>
onDone: () => void
}): React.ReactNode {
const options = useMemo<OptionWithDescription<TccOption>[]>(() => {
const opts: OptionWithDescription<TccOption>[] = []
if (!tccState.accessibility) {
let t1;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {
label: "Open System Settings \u2192 Accessibility",
value: "open_accessibility"
};
$[3] = t1;
} else {
t1 = $[3];
}
opts.push(t1);
opts.push({
label: 'Open System Settings → Accessibility',
value: 'open_accessibility',
})
}
if (!tccState.screenRecording) {
let t1;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {
label: "Open System Settings \u2192 Screen Recording",
value: "open_screen_recording"
};
$[4] = t1;
} else {
t1 = $[4];
}
opts.push(t1);
opts.push({
label: 'Open System Settings → Screen Recording',
value: 'open_screen_recording',
})
}
let t1;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {
label: "Try again",
value: "retry"
};
$[5] = t1;
} else {
t1 = $[5];
opts.push({ label: 'Try again', value: 'retry' })
return opts
}, [tccState.accessibility, tccState.screenRecording])
function onChange(value: TccOption): void {
switch (value) {
case 'open_accessibility':
void execFileNoThrow(
'open',
[
'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility',
],
{ useCwd: false },
)
return
case 'open_screen_recording':
void execFileNoThrow(
'open',
[
'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
],
{ useCwd: false },
)
return
case 'retry':
// Resolve with deny-all — the model re-calls request_access, which
// re-checks TCC and renders the app list if now granted.
onDone()
return
}
opts.push(t1);
$[0] = tccState.accessibility;
$[1] = tccState.screenRecording;
$[2] = opts;
} else {
opts = $[2];
}
const options = opts;
let t1;
if ($[6] !== onDone) {
t1 = function onChange(value) {
switch (value) {
case "open_accessibility":
{
execFileNoThrow("open", ["x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"], {
useCwd: false
});
return;
}
case "open_screen_recording":
{
execFileNoThrow("open", ["x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"], {
useCwd: false
});
return;
}
case "retry":
{
onDone();
return;
}
}
};
$[6] = onDone;
$[7] = t1;
} else {
t1 = $[7];
}
const onChange = t1;
const t2 = tccState.accessibility ? `${figures.tick} granted` : `${figures.cross} not granted`;
let t3;
if ($[8] !== t2) {
t3 = <Text>Accessibility:{" "}{t2}</Text>;
$[8] = t2;
$[9] = t3;
} else {
t3 = $[9];
}
const t4 = tccState.screenRecording ? `${figures.tick} granted` : `${figures.cross} not granted`;
let t5;
if ($[10] !== t4) {
t5 = <Text>Screen Recording:{" "}{t4}</Text>;
$[10] = t4;
$[11] = t5;
} else {
t5 = $[11];
}
let t6;
if ($[12] !== t3 || $[13] !== t5) {
t6 = <Box flexDirection="column">{t3}{t5}</Box>;
$[12] = t3;
$[13] = t5;
$[14] = t6;
} else {
t6 = $[14];
}
let t7;
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
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>;
$[15] = t7;
} else {
t7 = $[15];
}
let t8;
if ($[16] !== onChange || $[17] !== onDone || $[18] !== options) {
t8 = <Select options={options} onChange={onChange} onCancel={onDone} />;
$[16] = onChange;
$[17] = onDone;
$[18] = options;
$[19] = t8;
} else {
t8 = $[19];
}
let t9;
if ($[20] !== t6 || $[21] !== t8) {
t9 = <Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>{t6}{t7}{t8}</Box>;
$[20] = t6;
$[21] = t8;
$[22] = t9;
} else {
t9 = $[22];
}
let t10;
if ($[23] !== onDone || $[24] !== t9) {
t10 = <Dialog title="Computer Use needs macOS permissions" onCancel={onDone}>{t9}</Dialog>;
$[23] = onDone;
$[24] = t9;
$[25] = t10;
} else {
t10 = $[25];
}
return t10;
return (
<Dialog title="Computer Use needs macOS permissions" onCancel={onDone}>
<Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>
<Box flexDirection="column">
<Text>
Accessibility:{' '}
{tccState.accessibility
? `${figures.tick} granted`
: `${figures.cross} not granted`}
</Text>
<Text>
Screen Recording:{' '}
{tccState.screenRecording
? `${figures.tick} granted`
: `${figures.cross} not granted`}
</Text>
</Box>
<Text dimColor>
Grant the missing permissions in System Settings, then select
&quot;Try again&quot;. macOS may require you to restart Claude Code
after granting Screen Recording.
</Text>
<Select options={options} onChange={onChange} onCancel={onDone} />
</Box>
</Dialog>
)
}
// ── App allowlist panel ───────────────────────────────────────────────────
type AppListOption = 'allow_all' | 'deny';
const SENTINEL_WARNING: Record<NonNullable<ReturnType<typeof getSentinelCategory>>, string> = {
type AppListOption = 'allow_all' | 'deny'
const SENTINEL_WARNING: Record<
NonNullable<ReturnType<typeof getSentinelCategory>>,
string
> = {
shell: 'equivalent to shell access',
filesystem: 'can read/write any file',
system_settings: 'can change system settings'
};
function ComputerUseAppListPanel(t0) {
const $ = _c(48);
const {
request,
onDone
} = t0;
let t1;
if ($[0] !== request.apps) {
t1 = () => new Set(request.apps.flatMap(_temp));
$[0] = request.apps;
$[1] = t1;
} else {
t1 = $[1];
}
const [checked] = useState(t1);
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = ["clipboardRead", "clipboardWrite", "systemKeyCombos"];
$[2] = t2;
} else {
t2 = $[2];
}
const ALL_FLAG_KEYS = t2;
let t3;
if ($[3] !== request.requestedFlags) {
t3 = ALL_FLAG_KEYS.filter(k => request.requestedFlags[k]);
$[3] = request.requestedFlags;
$[4] = t3;
} else {
t3 = $[4];
}
const requestedFlagKeys = t3;
const t4 = checked.size;
let t5;
if ($[5] !== checked.size) {
t5 = plural(checked.size, "app");
$[5] = checked.size;
$[6] = t5;
} else {
t5 = $[6];
}
const t6 = `Allow for this session (${t4} ${t5})`;
let t7;
if ($[7] !== t6) {
t7 = {
label: t6,
value: "allow_all"
};
$[7] = t6;
$[8] = t7;
} else {
t7 = $[8];
}
let t8;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t8 = {
label: <Text>Deny, and tell Claude what to do differently <Text bold={true}>(esc)</Text></Text>,
value: "deny"
};
$[9] = t8;
} else {
t8 = $[9];
}
let t9;
if ($[10] !== t7) {
t9 = [t7, t8];
$[10] = t7;
$[11] = t9;
} else {
t9 = $[11];
}
const options = t9;
let t10;
if ($[12] !== checked || $[13] !== onDone || $[14] !== request.apps || $[15] !== requestedFlagKeys) {
t10 = function respond(allow) {
if (!allow) {
onDone(DENY_ALL_RESPONSE);
return;
}
const now = Date.now();
const granted = request.apps.flatMap(a_0 => a_0.resolved && checked.has(a_0.resolved.bundleId) ? [{
bundleId: a_0.resolved.bundleId,
displayName: a_0.resolved.displayName,
grantedAt: now
}] : []);
const denied = request.apps.filter(a_1 => !a_1.resolved || !checked.has(a_1.resolved.bundleId)).map(_temp2);
const flags = {
...DEFAULT_GRANT_FLAGS,
...Object.fromEntries(requestedFlagKeys.map(_temp3))
};
onDone({
granted,
denied,
flags
});
};
$[12] = checked;
$[13] = onDone;
$[14] = request.apps;
$[15] = requestedFlagKeys;
$[16] = t10;
} else {
t10 = $[16];
}
const respond = t10;
let t11;
if ($[17] !== respond) {
t11 = () => respond(false);
$[17] = respond;
$[18] = t11;
} else {
t11 = $[18];
}
let t12;
if ($[19] !== request.reason) {
t12 = request.reason ? <Text dimColor={true}>{request.reason}</Text> : null;
$[19] = request.reason;
$[20] = t12;
} else {
t12 = $[20];
}
let t13;
if ($[21] !== checked || $[22] !== request.apps) {
let t14;
if ($[24] !== checked) {
t14 = a_3 => {
const resolved = a_3.resolved;
if (!resolved) {
return <Text key={a_3.requestedName} dimColor={true}>{" "}{figures.circle} {a_3.requestedName}{" "}<Text dimColor={true}>(not installed)</Text></Text>;
}
if (a_3.alreadyGranted) {
return <Text key={resolved.bundleId} dimColor={true}>{" "}{figures.tick} {resolved.displayName}{" "}<Text dimColor={true}>(already granted)</Text></Text>;
}
const sentinel = getSentinelCategory(resolved.bundleId);
const isChecked = checked.has(resolved.bundleId);
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>;
};
$[24] = checked;
$[25] = t14;
} else {
t14 = $[25];
system_settings: 'can change system settings',
}
function ComputerUseAppListPanel({
request,
onDone,
}: ComputerUseApprovalProps): React.ReactNode {
// Pre-check every resolved, not-yet-granted app. Sentinels stay checked
// too — the warning text is the signal, not an unchecked box.
// Per-item toggles are a follow-up; for now every resolved app is granted
// when the user accepts. `setChecked` is unused until then.
const [checked] = useState<ReadonlySet<string>>(
() =>
new Set(
request.apps.flatMap(a =>
a.resolved && !a.alreadyGranted ? [a.resolved.bundleId] : [],
),
),
)
type FlagKey = keyof typeof DEFAULT_GRANT_FLAGS
const ALL_FLAG_KEYS: FlagKey[] = [
'clipboardRead',
'clipboardWrite',
'systemKeyCombos',
]
const requestedFlagKeys = useMemo(
(): FlagKey[] => ALL_FLAG_KEYS.filter(k => request.requestedFlags[k]),
[request.requestedFlags],
)
const options = useMemo<OptionWithDescription<AppListOption>[]>(
() => [
{
label: `Allow for this session (${checked.size} ${plural(checked.size, 'app')})`,
value: 'allow_all',
},
{
label: (
<Text>
Deny, and tell Claude what to do differently <Text bold>(esc)</Text>
</Text>
),
value: 'deny',
},
],
[checked.size],
)
function respond(allow: boolean): void {
if (!allow) {
onDone(DENY_ALL_RESPONSE)
return
}
t13 = request.apps.map(t14);
$[21] = checked;
$[22] = request.apps;
$[23] = t13;
} else {
t13 = $[23];
const now = Date.now()
const granted = request.apps.flatMap(a =>
a.resolved && checked.has(a.resolved.bundleId)
? [
{
bundleId: a.resolved.bundleId,
displayName: a.resolved.displayName,
grantedAt: now,
},
]
: [],
)
const denied = request.apps
.filter(a => !a.resolved || !checked.has(a.resolved.bundleId))
.map(a => ({
bundleId: a.resolved?.bundleId ?? a.requestedName,
reason: a.resolved
? ('user_denied' as const)
: ('not_installed' as const),
}))
// Grant all requested flags on allow — per-flag toggles are a follow-up.
const flags = {
...DEFAULT_GRANT_FLAGS,
...Object.fromEntries(requestedFlagKeys.map(k => [k, true] as const)),
}
onDone({ granted, denied, flags })
}
let t14;
if ($[26] !== t13) {
t14 = <Box flexDirection="column">{t13}</Box>;
$[26] = t13;
$[27] = t14;
} else {
t14 = $[27];
}
let t15;
if ($[28] !== requestedFlagKeys) {
t15 = requestedFlagKeys.length > 0 ? <Box flexDirection="column"><Text dimColor={true}>Also requested:</Text>{requestedFlagKeys.map(_temp4)}</Box> : null;
$[28] = requestedFlagKeys;
$[29] = t15;
} else {
t15 = $[29];
}
let t16;
if ($[30] !== request.willHide) {
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;
$[30] = request.willHide;
$[31] = t16;
} else {
t16 = $[31];
}
let t17;
let t18;
if ($[32] !== respond) {
t17 = v => respond(v === "allow_all");
t18 = () => respond(false);
$[32] = respond;
$[33] = t17;
$[34] = t18;
} else {
t17 = $[33];
t18 = $[34];
}
let t19;
if ($[35] !== options || $[36] !== t17 || $[37] !== t18) {
t19 = <Select options={options} onChange={t17} onCancel={t18} />;
$[35] = options;
$[36] = t17;
$[37] = t18;
$[38] = t19;
} else {
t19 = $[38];
}
let t20;
if ($[39] !== t12 || $[40] !== t14 || $[41] !== t15 || $[42] !== t16 || $[43] !== t19) {
t20 = <Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>{t12}{t14}{t15}{t16}{t19}</Box>;
$[39] = t12;
$[40] = t14;
$[41] = t15;
$[42] = t16;
$[43] = t19;
$[44] = t20;
} else {
t20 = $[44];
}
let t21;
if ($[45] !== t11 || $[46] !== t20) {
t21 = <Dialog title="Computer Use wants to control these apps" onCancel={t11}>{t20}</Dialog>;
$[45] = t11;
$[46] = t20;
$[47] = t21;
} else {
t21 = $[47];
}
return t21;
}
function _temp4(flag) {
return <Text key={flag} dimColor={true}>{" "}· {flag}</Text>;
}
function _temp3(k_0) {
return [k_0, true] as const;
}
function _temp2(a_2) {
return {
bundleId: a_2.resolved?.bundleId ?? a_2.requestedName,
reason: a_2.resolved ? "user_denied" as const : "not_installed" as const
};
}
function _temp(a) {
return a.resolved && !a.alreadyGranted ? [a.resolved.bundleId] : [];
return (
<Dialog
title="Computer Use wants to control these apps"
onCancel={() => respond(false)}
>
<Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>
{request.reason ? <Text dimColor>{request.reason}</Text> : null}
<Box flexDirection="column">
{request.apps.map(a => {
const resolved = a.resolved
if (!resolved) {
return (
<Text key={a.requestedName} dimColor>
{' '}
{figures.circle} {a.requestedName}{' '}
<Text dimColor>(not installed)</Text>
</Text>
)
}
if (a.alreadyGranted) {
return (
<Text key={resolved.bundleId} dimColor>
{' '}
{figures.tick} {resolved.displayName}{' '}
<Text dimColor>(already granted)</Text>
</Text>
)
}
const sentinel = getSentinelCategory(resolved.bundleId)
const isChecked = checked.has(resolved.bundleId)
return (
<Box key={resolved.bundleId} flexDirection="column">
<Text>
{' '}
{isChecked ? figures.circleFilled : figures.circle}{' '}
{resolved.displayName}
</Text>
{sentinel ? (
<Text bold>
{' '}
{figures.warning} {SENTINEL_WARNING[sentinel]}
</Text>
) : null}
</Box>
)
})}
</Box>
{requestedFlagKeys.length > 0 ? (
<Box flexDirection="column">
<Text dimColor>Also requested:</Text>
{requestedFlagKeys.map(flag => (
<Text key={flag} dimColor>
{' '}· {flag}
</Text>
))}
</Box>
) : null}
{request.willHide && request.willHide.length > 0 ? (
<Text dimColor>
{request.willHide.length} other{' '}
{plural(request.willHide.length, 'app')} will be hidden while Claude
works.
</Text>
) : null}
<Select
options={options}
onChange={v => respond(v === 'allow_all')}
onCancel={() => respond(false)}
/>
</Box>
</Dialog>
)
}

View File

@@ -1,121 +1,82 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { handlePlanModeTransition } from '../../../bootstrap/state.js';
import { Box, Text } from '../../../ink.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js';
import { useAppState } from '../../../state/AppState.js';
import { isPlanModeInterviewPhaseEnabled } from '../../../utils/planModeV2.js';
import { Select } from '../../CustomSelect/index.js';
import { PermissionDialog } from '../PermissionDialog.js';
import type { PermissionRequestProps } from '../PermissionRequest.js';
export function EnterPlanModePermissionRequest(t0) {
const $ = _c(18);
const {
toolUseConfirm,
onDone,
onReject,
workerBadge
} = t0;
const toolPermissionContextMode = useAppState(_temp);
let t1;
if ($[0] !== onDone || $[1] !== onReject || $[2] !== toolPermissionContextMode || $[3] !== toolUseConfirm) {
t1 = function handleResponse(value) {
if (value === "yes") {
logEvent("tengu_plan_enter", {
interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(),
entryMethod: "tool" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
handlePlanModeTransition(toolPermissionContextMode, "plan");
onDone();
toolUseConfirm.onAllow({}, [{
type: "setMode",
mode: "plan",
destination: "session"
}]);
} else {
onDone();
onReject();
toolUseConfirm.onReject();
}
};
$[0] = onDone;
$[1] = onReject;
$[2] = toolPermissionContextMode;
$[3] = toolUseConfirm;
$[4] = t1;
} else {
t1 = $[4];
import React from 'react'
import { handlePlanModeTransition } from '../../../bootstrap/state.js'
import { Box, Text } from '../../../ink.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../../services/analytics/index.js'
import { useAppState } from '../../../state/AppState.js'
import { isPlanModeInterviewPhaseEnabled } from '../../../utils/planModeV2.js'
import { Select } from '../../CustomSelect/index.js'
import { PermissionDialog } from '../PermissionDialog.js'
import type { PermissionRequestProps } from '../PermissionRequest.js'
export function EnterPlanModePermissionRequest({
toolUseConfirm,
onDone,
onReject,
workerBadge,
}: PermissionRequestProps): React.ReactNode {
const toolPermissionContextMode = useAppState(
s => s.toolPermissionContext.mode,
)
function handleResponse(value: 'yes' | 'no'): void {
if (value === 'yes') {
logEvent('tengu_plan_enter', {
interviewPhaseEnabled: isPlanModeInterviewPhaseEnabled(),
entryMethod:
'tool' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
handlePlanModeTransition(toolPermissionContextMode, 'plan')
onDone()
toolUseConfirm.onAllow({}, [
{ type: 'setMode', mode: 'plan', destination: 'session' },
])
} else {
onDone()
onReject()
toolUseConfirm.onReject()
}
}
const handleResponse = t1;
let t2;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Text>Claude wants to enter plan mode to explore and design an implementation approach.</Text>;
$[5] = t2;
} else {
t2 = $[5];
}
let t3;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
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>;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Box marginTop={1}><Text dimColor={true}>No code changes will be made until you approve the plan.</Text></Box>;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t5 = {
label: "Yes, enter plan mode",
value: "yes" as const
};
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t6 = [t5, {
label: "No, start implementing now",
value: "no" as const
}];
$[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] !== handleResponse) {
t7 = () => handleResponse("no");
$[10] = handleResponse;
$[11] = t7;
} else {
t7 = $[11];
}
let t8;
if ($[12] !== handleResponse || $[13] !== t7) {
t8 = <Box flexDirection="column" marginTop={1} paddingX={1}>{t2}{t3}{t4}<Box marginTop={1}><Select options={t6} onChange={handleResponse} onCancel={t7} /></Box></Box>;
$[12] = handleResponse;
$[13] = t7;
$[14] = t8;
} else {
t8 = $[14];
}
let t9;
if ($[15] !== t8 || $[16] !== workerBadge) {
t9 = <PermissionDialog color="planMode" title="Enter plan mode?" workerBadge={workerBadge}>{t8}</PermissionDialog>;
$[15] = t8;
$[16] = workerBadge;
$[17] = t9;
} else {
t9 = $[17];
}
return t9;
}
function _temp(s) {
return s.toolPermissionContext.mode;
return (
<PermissionDialog
color="planMode"
title="Enter plan mode?"
workerBadge={workerBadge}
>
<Box flexDirection="column" marginTop={1} paddingX={1}>
<Text>
Claude wants to enter plan mode to explore and design an
implementation approach.
</Text>
<Box marginTop={1} flexDirection="column">
<Text dimColor>In plan mode, Claude will:</Text>
<Text dimColor> · Explore the codebase thoroughly</Text>
<Text dimColor> · Identify existing patterns</Text>
<Text dimColor> · Design an implementation strategy</Text>
<Text dimColor> · Present a plan for your approval</Text>
</Box>
<Box marginTop={1}>
<Text dimColor>
No code changes will be made until you approve the plan.
</Text>
</Box>
<Box marginTop={1}>
<Select
options={[
{ label: 'Yes, enter plan mode', value: 'yes' as const },
{ label: 'No, start implementing now', value: 'no' as const },
]}
onChange={handleResponse}
onCancel={() => handleResponse('no')}
/>
</Box>
</Box>
</PermissionDialog>
)
}

View File

@@ -1,332 +1,196 @@
import { c as _c } from "react/compiler-runtime";
import React, { useCallback, useMemo } from 'react';
import { getOriginalCwd } from '../../bootstrap/state.js';
import { Box, Text, useTheme } from '../../ink.js';
import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js';
import { env } from '../../utils/env.js';
import { shouldShowAlwaysAllowOptions } from '../../utils/permissions/permissionsLoader.js';
import { truncateToLines } from '../../utils/stringUtils.js';
import { logUnaryEvent } from '../../utils/unaryLogging.js';
import { type UnaryEvent, usePermissionRequestLogging } from './hooks.js';
import { PermissionDialog } from './PermissionDialog.js';
import { PermissionPrompt, type PermissionPromptOption, type ToolAnalyticsContext } from './PermissionPrompt.js';
import type { PermissionRequestProps } from './PermissionRequest.js';
import { PermissionRuleExplanation } from './PermissionRuleExplanation.js';
type FallbackOptionValue = 'yes' | 'yes-dont-ask-again' | 'no';
export function FallbackPermissionRequest(t0) {
const $ = _c(58);
const {
toolUseConfirm,
onDone,
onReject,
workerBadge
} = t0;
const [theme] = useTheme();
let originalUserFacingName;
let t1;
if ($[0] !== toolUseConfirm.input || $[1] !== toolUseConfirm.tool) {
originalUserFacingName = toolUseConfirm.tool.userFacingName(toolUseConfirm.input as never);
t1 = originalUserFacingName.endsWith(" (MCP)") ? originalUserFacingName.slice(0, -6) : originalUserFacingName;
$[0] = toolUseConfirm.input;
$[1] = toolUseConfirm.tool;
$[2] = originalUserFacingName;
$[3] = t1;
} else {
originalUserFacingName = $[2];
t1 = $[3];
}
const userFacingName = t1;
let t2;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t2 = {
completion_type: "tool_use_single",
language_name: "none"
};
$[4] = t2;
} else {
t2 = $[4];
}
const unaryEvent = t2;
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
let t3;
if ($[5] !== onDone || $[6] !== onReject || $[7] !== toolUseConfirm) {
t3 = (value, feedback) => {
bb8: switch (value) {
case "yes":
{
logUnaryEvent({
completion_type: "tool_use_single",
event: "accept",
metadata: {
language_name: "none",
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform
}
});
toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback);
onDone();
break bb8;
}
case "yes-dont-ask-again":
{
logUnaryEvent({
completion_type: "tool_use_single",
event: "accept",
metadata: {
language_name: "none",
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform
}
});
toolUseConfirm.onAllow(toolUseConfirm.input, [{
type: "addRules",
rules: [{
toolName: toolUseConfirm.tool.name
}],
behavior: "allow",
destination: "localSettings"
}]);
onDone();
break bb8;
}
case "no":
{
logUnaryEvent({
completion_type: "tool_use_single",
event: "reject",
metadata: {
language_name: "none",
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform
}
});
toolUseConfirm.onReject(feedback);
onReject();
onDone();
}
}
};
$[5] = onDone;
$[6] = onReject;
$[7] = toolUseConfirm;
$[8] = t3;
} else {
t3 = $[8];
}
const handleSelect = t3;
let t4;
if ($[9] !== onDone || $[10] !== onReject || $[11] !== toolUseConfirm) {
t4 = () => {
logUnaryEvent({
completion_type: "tool_use_single",
event: "reject",
metadata: {
language_name: "none",
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform
import React, { useCallback, useMemo } from 'react'
import { getOriginalCwd } from '../../bootstrap/state.js'
import { Box, Text, useTheme } from '../../ink.js'
import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js'
import { env } from '../../utils/env.js'
import { shouldShowAlwaysAllowOptions } from '../../utils/permissions/permissionsLoader.js'
import { truncateToLines } from '../../utils/stringUtils.js'
import { logUnaryEvent } from '../../utils/unaryLogging.js'
import { type UnaryEvent, usePermissionRequestLogging } from './hooks.js'
import { PermissionDialog } from './PermissionDialog.js'
import {
PermissionPrompt,
type PermissionPromptOption,
type ToolAnalyticsContext,
} from './PermissionPrompt.js'
import type { PermissionRequestProps } from './PermissionRequest.js'
import { PermissionRuleExplanation } from './PermissionRuleExplanation.js'
type FallbackOptionValue = 'yes' | 'yes-dont-ask-again' | 'no'
export function FallbackPermissionRequest({
toolUseConfirm,
onDone,
onReject,
verbose: _verbose,
workerBadge,
}: PermissionRequestProps): React.ReactNode {
const [theme] = useTheme()
// TODO: Avoid these special cases
const originalUserFacingName = toolUseConfirm.tool.userFacingName(
toolUseConfirm.input as never,
)
const userFacingName = originalUserFacingName.endsWith(' (MCP)')
? originalUserFacingName.slice(0, -6)
: originalUserFacingName
const unaryEvent = useMemo<UnaryEvent>(
() => ({
completion_type: 'tool_use_single',
language_name: 'none',
}),
[],
)
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
const handleSelect = useCallback(
(value: FallbackOptionValue, feedback?: string) => {
switch (value) {
case 'yes':
void logUnaryEvent({
completion_type: 'tool_use_single',
event: 'accept',
metadata: {
language_name: 'none',
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform,
},
})
toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback)
onDone()
break
case 'yes-dont-ask-again': {
void logUnaryEvent({
completion_type: 'tool_use_single',
event: 'accept',
metadata: {
language_name: 'none',
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform,
},
})
toolUseConfirm.onAllow(toolUseConfirm.input, [
{
type: 'addRules',
rules: [
{
toolName: toolUseConfirm.tool.name,
},
],
behavior: 'allow',
destination: 'localSettings',
},
])
onDone()
break
}
});
toolUseConfirm.onReject();
onReject();
onDone();
};
$[9] = onDone;
$[10] = onReject;
$[11] = toolUseConfirm;
$[12] = t4;
} else {
t4 = $[12];
}
const handleCancel = t4;
let t5;
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
t5 = getOriginalCwd();
$[13] = t5;
} else {
t5 = $[13];
}
const originalCwd = t5;
let t6;
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
t6 = shouldShowAlwaysAllowOptions();
$[14] = t6;
} else {
t6 = $[14];
}
const showAlwaysAllowOptions = t6;
let t7;
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
t7 = {
label: "Yes",
value: "yes",
feedbackConfig: {
type: "accept"
case 'no':
void logUnaryEvent({
completion_type: 'tool_use_single',
event: 'reject',
metadata: {
language_name: 'none',
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform,
},
})
toolUseConfirm.onReject(feedback)
onReject()
onDone()
break
}
};
$[15] = t7;
} else {
t7 = $[15];
}
let result;
if ($[16] !== userFacingName) {
result = [t7];
},
[toolUseConfirm, onDone, onReject],
)
const handleCancel = useCallback(() => {
void logUnaryEvent({
completion_type: 'tool_use_single',
event: 'reject',
metadata: {
language_name: 'none',
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform,
},
})
toolUseConfirm.onReject()
onReject()
onDone()
}, [toolUseConfirm, onDone, onReject])
const originalCwd = getOriginalCwd()
const showAlwaysAllowOptions = shouldShowAlwaysAllowOptions()
const options = useMemo((): PermissionPromptOption<FallbackOptionValue>[] => {
const result: PermissionPromptOption<FallbackOptionValue>[] = [
{
label: 'Yes',
value: 'yes',
feedbackConfig: { type: 'accept' },
},
]
if (showAlwaysAllowOptions) {
const t8 = <Text bold={true}>{userFacingName}</Text>;
let t9;
if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
t9 = <Text bold={true}>{originalCwd}</Text>;
$[18] = t9;
} else {
t9 = $[18];
}
let t10;
if ($[19] !== t8) {
t10 = {
label: <Text>Yes, and don't ask again for {t8}{" "}commands in {t9}</Text>,
value: "yes-dont-ask-again"
};
$[19] = t8;
$[20] = t10;
} else {
t10 = $[20];
}
result.push(t10);
result.push({
label: (
<Text>
Yes, and don&apos;t ask again for <Text bold>{userFacingName}</Text>{' '}
commands in <Text bold>{originalCwd}</Text>
</Text>
),
value: 'yes-dont-ask-again',
})
}
let t8;
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
t8 = {
label: "No",
value: "no",
feedbackConfig: {
type: "reject"
}
};
$[21] = t8;
} else {
t8 = $[21];
}
result.push(t8);
$[16] = userFacingName;
$[17] = result;
} else {
result = $[17];
}
const options = result;
let t8;
if ($[22] !== toolUseConfirm.tool.name) {
t8 = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name);
$[22] = toolUseConfirm.tool.name;
$[23] = t8;
} else {
t8 = $[23];
}
const t9 = toolUseConfirm.tool.isMcp ?? false;
let t10;
if ($[24] !== t8 || $[25] !== t9) {
t10 = {
toolName: t8,
isMcp: t9
};
$[24] = t8;
$[25] = t9;
$[26] = t10;
} else {
t10 = $[26];
}
const toolAnalyticsContext = t10;
let t11;
if ($[27] !== theme || $[28] !== toolUseConfirm.input || $[29] !== toolUseConfirm.tool) {
t11 = toolUseConfirm.tool.renderToolUseMessage(toolUseConfirm.input as never, {
theme,
verbose: true
});
$[27] = theme;
$[28] = toolUseConfirm.input;
$[29] = toolUseConfirm.tool;
$[30] = t11;
} else {
t11 = $[30];
}
let t12;
if ($[31] !== originalUserFacingName) {
t12 = originalUserFacingName.endsWith(" (MCP)") ? <Text dimColor={true}> (MCP)</Text> : "";
$[31] = originalUserFacingName;
$[32] = t12;
} else {
t12 = $[32];
}
let t13;
if ($[33] !== t11 || $[34] !== t12 || $[35] !== userFacingName) {
t13 = <Text>{userFacingName}({t11}){t12}</Text>;
$[33] = t11;
$[34] = t12;
$[35] = userFacingName;
$[36] = t13;
} else {
t13 = $[36];
}
let t14;
if ($[37] !== toolUseConfirm.description) {
t14 = truncateToLines(toolUseConfirm.description, 3);
$[37] = toolUseConfirm.description;
$[38] = t14;
} else {
t14 = $[38];
}
let t15;
if ($[39] !== t14) {
t15 = <Text dimColor={true}>{t14}</Text>;
$[39] = t14;
$[40] = t15;
} else {
t15 = $[40];
}
let t16;
if ($[41] !== t13 || $[42] !== t15) {
t16 = <Box flexDirection="column" paddingX={2} paddingY={1}>{t13}{t15}</Box>;
$[41] = t13;
$[42] = t15;
$[43] = t16;
} else {
t16 = $[43];
}
let t17;
if ($[44] !== toolUseConfirm.permissionResult) {
t17 = <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" />;
$[44] = toolUseConfirm.permissionResult;
$[45] = t17;
} else {
t17 = $[45];
}
let t18;
if ($[46] !== handleCancel || $[47] !== handleSelect || $[48] !== options || $[49] !== toolAnalyticsContext) {
t18 = <PermissionPrompt options={options} onSelect={handleSelect} onCancel={handleCancel} toolAnalyticsContext={toolAnalyticsContext} />;
$[46] = handleCancel;
$[47] = handleSelect;
$[48] = options;
$[49] = toolAnalyticsContext;
$[50] = t18;
} else {
t18 = $[50];
}
let t19;
if ($[51] !== t17 || $[52] !== t18) {
t19 = <Box flexDirection="column">{t17}{t18}</Box>;
$[51] = t17;
$[52] = t18;
$[53] = t19;
} else {
t19 = $[53];
}
let t20;
if ($[54] !== t16 || $[55] !== t19 || $[56] !== workerBadge) {
t20 = <PermissionDialog title="Tool use" workerBadge={workerBadge}>{t16}{t19}</PermissionDialog>;
$[54] = t16;
$[55] = t19;
$[56] = workerBadge;
$[57] = t20;
} else {
t20 = $[57];
}
return t20;
result.push({
label: 'No',
value: 'no',
feedbackConfig: { type: 'reject' },
})
return result
}, [userFacingName, originalCwd, showAlwaysAllowOptions])
const toolAnalyticsContext = useMemo(
(): ToolAnalyticsContext => ({
toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name),
isMcp: toolUseConfirm.tool.isMcp ?? false,
}),
[toolUseConfirm.tool.name, toolUseConfirm.tool.isMcp],
)
return (
<PermissionDialog title="Tool use" workerBadge={workerBadge}>
<Box flexDirection="column" paddingX={2} paddingY={1}>
<Text>
{userFacingName}(
{toolUseConfirm.tool.renderToolUseMessage(
toolUseConfirm.input as never,
{ theme, verbose: true },
)}
)
{originalUserFacingName.endsWith(' (MCP)') ? (
<Text dimColor> (MCP)</Text>
) : (
''
)}
</Text>
<Text dimColor>{truncateToLines(toolUseConfirm.description, 3)}</Text>
</Box>
<Box flexDirection="column">
<PermissionRuleExplanation
permissionResult={toolUseConfirm.permissionResult}
toolType="tool"
/>
<PermissionPrompt
options={options}
onSelect={handleSelect}
onCancel={handleCancel}
toolAnalyticsContext={toolAnalyticsContext}
/>
</Box>
</PermissionDialog>
)
}

View File

@@ -1,181 +1,79 @@
import { c as _c } from "react/compiler-runtime";
import { basename, relative } from 'path';
import React from 'react';
import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js';
import { getCwd } from 'src/utils/cwd.js';
import type { z } from 'zod/v4';
import { Text } from '../../../ink.js';
import { FileEditTool } from '../../../tools/FileEditTool/FileEditTool.js';
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js';
import { createSingleEditDiffConfig, type FileEdit, type IDEDiffSupport } from '../FilePermissionDialog/ideDiffConfig.js';
import type { PermissionRequestProps } from '../PermissionRequest.js';
type FileEditInput = z.infer<typeof FileEditTool.inputSchema>;
import { basename, relative } from 'path'
import React from 'react'
import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js'
import { getCwd } from 'src/utils/cwd.js'
import type { z } from 'zod/v4'
import { Text } from '../../../ink.js'
import { FileEditTool } from '../../../tools/FileEditTool/FileEditTool.js'
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'
import {
createSingleEditDiffConfig,
type FileEdit,
type IDEDiffSupport,
} from '../FilePermissionDialog/ideDiffConfig.js'
import type { PermissionRequestProps } from '../PermissionRequest.js'
type FileEditInput = z.infer<typeof FileEditTool.inputSchema>
const ideDiffSupport: IDEDiffSupport<FileEditInput> = {
getConfig: (input: FileEditInput) => createSingleEditDiffConfig(input.file_path, input.old_string, input.new_string, input.replace_all),
getConfig: (input: FileEditInput) =>
createSingleEditDiffConfig(
input.file_path,
input.old_string,
input.new_string,
input.replace_all,
),
applyChanges: (input: FileEditInput, modifiedEdits: FileEdit[]) => {
const firstEdit = modifiedEdits[0];
const firstEdit = modifiedEdits[0]
if (firstEdit) {
return {
...input,
old_string: firstEdit.old_string,
new_string: firstEdit.new_string,
replace_all: firstEdit.replace_all
};
replace_all: firstEdit.replace_all,
}
}
return input;
}
};
export function FileEditPermissionRequest(props) {
const $ = _c(51);
const parseInput = _temp;
let T0;
let T1;
let T2;
let file_path;
let new_string;
let old_string;
let replace_all;
let t0;
let t1;
let t10;
let t2;
let t3;
let t4;
let t5;
let t6;
let t7;
let t8;
let t9;
if ($[0] !== props.onDone || $[1] !== props.onReject || $[2] !== props.toolUseConfirm || $[3] !== props.toolUseContext || $[4] !== props.workerBadge) {
const parsed = parseInput(props.toolUseConfirm.input);
({
file_path,
old_string,
new_string,
replace_all
} = parsed);
T2 = FilePermissionDialog;
t4 = props.toolUseConfirm;
t5 = props.toolUseContext;
t6 = props.onDone;
t7 = props.onReject;
t8 = props.workerBadge;
t9 = "Edit file";
t10 = relative(getCwd(), file_path);
T1 = Text;
t2 = "Do you want to make this edit to";
t3 = " ";
T0 = Text;
t0 = true;
t1 = basename(file_path);
$[0] = props.onDone;
$[1] = props.onReject;
$[2] = props.toolUseConfirm;
$[3] = props.toolUseContext;
$[4] = props.workerBadge;
$[5] = T0;
$[6] = T1;
$[7] = T2;
$[8] = file_path;
$[9] = new_string;
$[10] = old_string;
$[11] = replace_all;
$[12] = t0;
$[13] = t1;
$[14] = t10;
$[15] = t2;
$[16] = t3;
$[17] = t4;
$[18] = t5;
$[19] = t6;
$[20] = t7;
$[21] = t8;
$[22] = t9;
} else {
T0 = $[5];
T1 = $[6];
T2 = $[7];
file_path = $[8];
new_string = $[9];
old_string = $[10];
replace_all = $[11];
t0 = $[12];
t1 = $[13];
t10 = $[14];
t2 = $[15];
t3 = $[16];
t4 = $[17];
t5 = $[18];
t6 = $[19];
t7 = $[20];
t8 = $[21];
t9 = $[22];
}
let t11;
if ($[23] !== T0 || $[24] !== t0 || $[25] !== t1) {
t11 = <T0 bold={t0}>{t1}</T0>;
$[23] = T0;
$[24] = t0;
$[25] = t1;
$[26] = t11;
} else {
t11 = $[26];
}
let t12;
if ($[27] !== T1 || $[28] !== t11 || $[29] !== t2 || $[30] !== t3) {
t12 = <T1>{t2}{t3}{t11}?</T1>;
$[27] = T1;
$[28] = t11;
$[29] = t2;
$[30] = t3;
$[31] = t12;
} else {
t12 = $[31];
}
const t13 = replace_all || false;
let t14;
if ($[32] !== new_string || $[33] !== old_string || $[34] !== t13) {
t14 = [{
old_string,
new_string,
replace_all: t13
}];
$[32] = new_string;
$[33] = old_string;
$[34] = t13;
$[35] = t14;
} else {
t14 = $[35];
}
let t15;
if ($[36] !== file_path || $[37] !== t14) {
t15 = <FileEditToolDiff file_path={file_path} edits={t14} />;
$[36] = file_path;
$[37] = t14;
$[38] = t15;
} else {
t15 = $[38];
}
let t16;
if ($[39] !== T2 || $[40] !== file_path || $[41] !== t10 || $[42] !== t12 || $[43] !== t15 || $[44] !== t4 || $[45] !== t5 || $[46] !== t6 || $[47] !== t7 || $[48] !== t8 || $[49] !== t9) {
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} />;
$[39] = T2;
$[40] = file_path;
$[41] = t10;
$[42] = t12;
$[43] = t15;
$[44] = t4;
$[45] = t5;
$[46] = t6;
$[47] = t7;
$[48] = t8;
$[49] = t9;
$[50] = t16;
} else {
t16 = $[50];
}
return t16;
return input
},
}
function _temp(input) {
return FileEditTool.inputSchema.parse(input);
export function FileEditPermissionRequest(
props: PermissionRequestProps,
): React.ReactNode {
const parseInput = (input: unknown): FileEditInput => {
return FileEditTool.inputSchema.parse(input)
}
const parsed = parseInput(props.toolUseConfirm.input)
const { file_path, old_string, new_string, replace_all } = parsed
return (
<FilePermissionDialog
toolUseConfirm={props.toolUseConfirm}
toolUseContext={props.toolUseContext}
onDone={props.onDone}
onReject={props.onReject}
workerBadge={props.workerBadge}
title="Edit file"
subtitle={relative(getCwd(), file_path)}
question={
<Text>
Do you want to make this edit to{' '}
<Text bold>{basename(file_path)}</Text>?
</Text>
}
content={
<FileEditToolDiff
file_path={file_path}
edits={[
{ old_string, new_string, replace_all: replace_all || false },
]}
/>
}
path={file_path}
completionType="str_replace_single"
parseInput={parseInput}
ideDiffSupport={ideDiffSupport}
/>
)
}

View File

@@ -1,50 +1,61 @@
import { relative } from 'path';
import React, { useMemo } from 'react';
import { useDiffInIDE } from '../../../hooks/useDiffInIDE.js';
import { Box, Text } from '../../../ink.js';
import type { ToolUseContext } from '../../../Tool.js';
import { getLanguageName } from '../../../utils/cliHighlight.js';
import { getCwd } from '../../../utils/cwd.js';
import { getFsImplementation, safeResolvePath } from '../../../utils/fsOperations.js';
import { expandPath } from '../../../utils/path.js';
import type { CompletionType } from '../../../utils/unaryLogging.js';
import { Select } from '../../CustomSelect/index.js';
import { ShowInIDEPrompt } from '../../ShowInIDEPrompt.js';
import { usePermissionRequestLogging } from '../hooks.js';
import { PermissionDialog } from '../PermissionDialog.js';
import type { ToolUseConfirm } from '../PermissionRequest.js';
import type { WorkerBadgeProps } from '../WorkerBadge.js';
import type { IDEDiffSupport } from './ideDiffConfig.js';
import type { FileOperationType, PermissionOption } from './permissionOptions.js';
import { type ToolInput, useFilePermissionDialog } from './useFilePermissionDialog.js';
import { relative } from 'path'
import React, { useMemo } from 'react'
import { useDiffInIDE } from '../../../hooks/useDiffInIDE.js'
import { Box, Text } from '../../../ink.js'
import type { ToolUseContext } from '../../../Tool.js'
import { getLanguageName } from '../../../utils/cliHighlight.js'
import { getCwd } from '../../../utils/cwd.js'
import {
getFsImplementation,
safeResolvePath,
} from '../../../utils/fsOperations.js'
import { expandPath } from '../../../utils/path.js'
import type { CompletionType } from '../../../utils/unaryLogging.js'
import { Select } from '../../CustomSelect/index.js'
import { ShowInIDEPrompt } from '../../ShowInIDEPrompt.js'
import { usePermissionRequestLogging } from '../hooks.js'
import { PermissionDialog } from '../PermissionDialog.js'
import type { ToolUseConfirm } from '../PermissionRequest.js'
import type { WorkerBadgeProps } from '../WorkerBadge.js'
import type { IDEDiffSupport } from './ideDiffConfig.js'
import type {
FileOperationType,
PermissionOption,
} from './permissionOptions.js'
import {
type ToolInput,
useFilePermissionDialog,
} from './useFilePermissionDialog.js'
export type FilePermissionDialogProps<T extends ToolInput = ToolInput> = {
// Required props from PermissionRequestProps
toolUseConfirm: ToolUseConfirm;
toolUseContext: ToolUseContext;
onDone: () => void;
onReject: () => void;
toolUseConfirm: ToolUseConfirm
toolUseContext: ToolUseContext
onDone: () => void
onReject: () => void
// Dialog customization
title: string;
subtitle?: React.ReactNode;
question?: string | React.ReactNode;
content?: React.ReactNode; // Can be general content or diff component
title: string
subtitle?: React.ReactNode
question?: string | React.ReactNode
content?: React.ReactNode // Can be general content or diff component
// Logging
completionType?: CompletionType;
languageName?: string; // override — derived from path when omitted
completionType?: CompletionType
languageName?: string // override — derived from path when omitted
// File/directory operations
path: string | null;
parseInput: (input: unknown) => T;
operationType?: FileOperationType;
path: string | null
parseInput: (input: unknown) => T
operationType?: FileOperationType
// IDE diff support
ideDiffSupport?: IDEDiffSupport<T>;
ideDiffSupport?: IDEDiffSupport<T>
// Worker badge for teammate permission requests
workerBadge: WorkerBadgeProps | undefined;
};
workerBadge: WorkerBadgeProps | undefined
}
export function FilePermissionDialog<T extends ToolInput = ToolInput>({
toolUseConfirm,
toolUseContext,
@@ -60,33 +71,38 @@ export function FilePermissionDialog<T extends ToolInput = ToolInput>({
operationType = 'write',
ideDiffSupport,
workerBadge,
languageName: languageNameOverride
languageName: languageNameOverride,
}: FilePermissionDialogProps<T>): React.ReactNode {
// Derive from path unless caller provided an explicit override (NotebookEdit
// passes 'python'/'markdown' from cell_type). getLanguageName is async;
// downstream UnaryEvent.language_name and logPermissionEvent already accept
// Promise<string>. useMemo keeps the promise stable across renders.
const languageName = useMemo(() => languageNameOverride ?? (path ? getLanguageName(path) : 'none'), [languageNameOverride, path]);
const unaryEvent = useMemo(() => ({
completion_type: completionType,
language_name: languageName
}), [completionType, languageName]);
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
const languageName = useMemo(
() => languageNameOverride ?? (path ? getLanguageName(path) : 'none'),
[languageNameOverride, path],
)
const unaryEvent = useMemo(
() => ({
completion_type: completionType,
language_name: languageName,
}),
[completionType, languageName],
)
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
const symlinkTarget = useMemo(() => {
if (!path || operationType === 'read') {
return null;
return null
}
const expandedPath = expandPath(path);
const fs = getFsImplementation();
const {
resolvedPath,
isSymlink
} = safeResolvePath(fs, expandedPath);
const expandedPath = expandPath(path)
const fs = getFsImplementation()
const { resolvedPath, isSymlink } = safeResolvePath(fs, expandedPath)
if (isSymlink) {
return resolvedPath;
return resolvedPath
}
return null;
}, [path, operationType]);
return null
}, [path, operationType])
const fileDialogResult = useFilePermissionDialog({
filePath: path || '',
completionType,
@@ -95,8 +111,8 @@ export function FilePermissionDialog<T extends ToolInput = ToolInput>({
onDone,
onReject,
parseInput,
operationType
});
operationType,
})
// Use file dialog results for options
const {
@@ -107,97 +123,150 @@ export function FilePermissionDialog<T extends ToolInput = ToolInput>({
handleInputModeToggle,
focusedOption,
yesInputMode,
noInputMode
} = fileDialogResult;
noInputMode,
} = fileDialogResult
// Parse input using the provided parser
const parsedInput = parseInput(toolUseConfirm.input);
const parsedInput = parseInput(toolUseConfirm.input)
// Set up IDE diff support if enabled. Memoized: getConfig may do disk I/O
// (FileWrite's getConfig calls readFileSync for the old-content diff).
// Keyed on the raw input — parseInput is a pure Zod parse whose result
// depends only on toolUseConfirm.input.
const ideDiffConfig = useMemo(() => ideDiffSupport ? ideDiffSupport.getConfig(parseInput(toolUseConfirm.input)) : null, [ideDiffSupport, toolUseConfirm.input]);
const ideDiffConfig = useMemo(
() =>
ideDiffSupport
? ideDiffSupport.getConfig(parseInput(toolUseConfirm.input))
: null,
[ideDiffSupport, toolUseConfirm.input],
)
// Create diff params based on whether IDE diff is available
const diffParams = ideDiffConfig ? {
onChange: (option: PermissionOption, input: {
file_path: string;
edits: Array<{
old_string: string;
new_string: string;
replace_all?: boolean;
}>;
}) => {
const transformedInput = ideDiffSupport!.applyChanges(parsedInput, input.edits);
fileDialogResult.onChange(option, transformedInput);
},
toolUseContext,
filePath: ideDiffConfig.filePath,
edits: (ideDiffConfig.edits || []).map(e => ({
old_string: e.old_string,
new_string: e.new_string,
replace_all: e.replace_all || false
})),
editMode: ideDiffConfig.editMode || 'single'
} : {
onChange: () => {},
toolUseContext,
filePath: '',
edits: [],
editMode: 'single' as const
};
const {
closeTabInIDE,
showingDiffInIDE,
ideName
} = useDiffInIDE(diffParams);
const onChange = (option_0: PermissionOption, feedback?: string) => {
closeTabInIDE?.();
fileDialogResult.onChange(option_0, parsedInput, feedback?.trim());
};
if (showingDiffInIDE && ideDiffConfig && path) {
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} />;
const diffParams = ideDiffConfig
? {
onChange: (
option: PermissionOption,
input: {
file_path: string
edits: Array<{
old_string: string
new_string: string
replace_all?: boolean
}>
},
) => {
const transformedInput = ideDiffSupport!.applyChanges(
parsedInput,
input.edits,
)
fileDialogResult.onChange(option, transformedInput)
},
toolUseContext,
filePath: ideDiffConfig.filePath,
edits: (ideDiffConfig.edits || []).map(e => ({
old_string: e.old_string,
new_string: e.new_string,
replace_all: e.replace_all || false,
})),
editMode: ideDiffConfig.editMode || 'single',
}
: {
onChange: () => {},
toolUseContext,
filePath: '',
edits: [],
editMode: 'single' as const,
}
const { closeTabInIDE, showingDiffInIDE, ideName } = useDiffInIDE(diffParams)
const onChange = (option: PermissionOption, feedback?: string) => {
closeTabInIDE?.()
fileDialogResult.onChange(option, parsedInput, feedback?.trim())
}
const isSymlinkOutsideCwd = symlinkTarget != null && relative(getCwd(), symlinkTarget).startsWith('..');
const symlinkWarning = symlinkTarget ? <Box paddingX={1} marginBottom={1}>
if (showingDiffInIDE && ideDiffConfig && path) {
return (
<ShowInIDEPrompt
onChange={(option: PermissionOption, _input, feedback?: string) =>
onChange(option, feedback)
}
options={options}
filePath={path}
input={parsedInput}
ideName={ideName}
symlinkTarget={symlinkTarget}
rejectFeedback={rejectFeedback}
acceptFeedback={acceptFeedback}
setFocusedOption={setFocusedOption}
onInputModeToggle={handleInputModeToggle}
focusedOption={focusedOption}
yesInputMode={yesInputMode}
noInputMode={noInputMode}
/>
)
}
const isSymlinkOutsideCwd =
symlinkTarget != null && relative(getCwd(), symlinkTarget).startsWith('..')
const symlinkWarning = symlinkTarget ? (
<Box paddingX={1} marginBottom={1}>
<Text color="warning">
{isSymlinkOutsideCwd ? `This will modify ${symlinkTarget} (outside working directory) via a symlink` : `Symlink target: ${symlinkTarget}`}
{isSymlinkOutsideCwd
? `This will modify ${symlinkTarget} (outside working directory) via a symlink`
: `Symlink target: ${symlinkTarget}`}
</Text>
</Box> : null;
return <>
<PermissionDialog title={title} subtitle={subtitle} innerPaddingX={0} workerBadge={workerBadge}>
</Box>
) : null
return (
<>
<PermissionDialog
title={title}
subtitle={subtitle}
innerPaddingX={0}
workerBadge={workerBadge}
>
{symlinkWarning}
{content}
<Box flexDirection="column" paddingX={1}>
{typeof question === 'string' ? <Text>{question}</Text> : question}
<Select options={options} inlineDescriptions onChange={value => {
const selected = options.find(opt => opt.value === value);
if (selected) {
// For reject option
if (selected.option.type === 'reject') {
const trimmedFeedback = rejectFeedback.trim();
onChange(selected.option, trimmedFeedback || undefined);
return;
}
// For accept-once option, pass accept feedback if present
if (selected.option.type === 'accept-once') {
const trimmedFeedback_0 = acceptFeedback.trim();
onChange(selected.option, trimmedFeedback_0 || undefined);
return;
}
onChange(selected.option);
}
}} onCancel={() => onChange({
type: 'reject'
})} onFocus={value_0 => setFocusedOption(value_0)} onInputModeToggle={handleInputModeToggle} />
<Select
options={options}
inlineDescriptions
onChange={value => {
const selected = options.find(opt => opt.value === value)
if (selected) {
// For reject option
if (selected.option.type === 'reject') {
const trimmedFeedback = rejectFeedback.trim()
onChange(selected.option, trimmedFeedback || undefined)
return
}
// For accept-once option, pass accept feedback if present
if (selected.option.type === 'accept-once') {
const trimmedFeedback = acceptFeedback.trim()
onChange(selected.option, trimmedFeedback || undefined)
return
}
onChange(selected.option)
}
}}
onCancel={() => onChange({ type: 'reject' })}
onFocus={value => setFocusedOption(value)}
onInputModeToggle={handleInputModeToggle}
/>
</Box>
</PermissionDialog>
<Box paddingX={1} marginTop={1}>
<Text dimColor>
Esc to cancel
{(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'}
{((focusedOption === 'yes' && !yesInputMode) ||
(focusedOption === 'no' && !noInputMode)) &&
' · Tab to amend'}
</Text>
</Box>
</>;
</>
)
}

View File

@@ -1,29 +1,37 @@
import { homedir } from 'os';
import { basename, join, sep } from 'path';
import React, { type ReactNode } from 'react';
import { getOriginalCwd } from '../../../bootstrap/state.js';
import { Text } from '../../../ink.js';
import { getShortcutDisplay } from '../../../keybindings/shortcutFormat.js';
import type { ToolPermissionContext } from '../../../Tool.js';
import { expandPath, getDirectoryForPath } from '../../../utils/path.js';
import { normalizeCaseForComparison, pathInAllowedWorkingPath } from '../../../utils/permissions/filesystem.js';
import type { OptionWithDescription } from '../../CustomSelect/select.js';
import { homedir } from 'os'
import { basename, join, sep } from 'path'
import React, { type ReactNode } from 'react'
import { getOriginalCwd } from '../../../bootstrap/state.js'
import { Text } from '../../../ink.js'
import { getShortcutDisplay } from '../../../keybindings/shortcutFormat.js'
import type { ToolPermissionContext } from '../../../Tool.js'
import { expandPath, getDirectoryForPath } from '../../../utils/path.js'
import {
normalizeCaseForComparison,
pathInAllowedWorkingPath,
} from '../../../utils/permissions/filesystem.js'
import type { OptionWithDescription } from '../../CustomSelect/select.js'
/**
* Check if a path is within the project's .claude/ folder.
* This is used to determine whether to show the special ".claude folder" permission option.
*/
export function isInClaudeFolder(filePath: string): boolean {
const absolutePath = expandPath(filePath);
const claudeFolderPath = expandPath(`${getOriginalCwd()}/.claude`);
const absolutePath = expandPath(filePath)
const claudeFolderPath = expandPath(`${getOriginalCwd()}/.claude`)
// Check if the path is within the project's .claude folder
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath);
const normalizedClaudeFolderPath = normalizeCaseForComparison(claudeFolderPath);
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath)
const normalizedClaudeFolderPath =
normalizeCaseForComparison(claudeFolderPath)
// Path must start with the .claude folder path (and be inside it, not just the folder itself)
return normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + sep.toLowerCase()) ||
// Also match case where sep is / on posix systems
normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + '/');
return (
normalizedAbsolutePath.startsWith(
normalizedClaudeFolderPath + sep.toLowerCase(),
) ||
// Also match case where sep is / on posix systems
normalizedAbsolutePath.startsWith(normalizedClaudeFolderPath + '/')
)
}
/**
@@ -32,24 +40,33 @@ export function isInClaudeFolder(filePath: string): boolean {
* for files in the user's home directory.
*/
export function isInGlobalClaudeFolder(filePath: string): boolean {
const absolutePath = expandPath(filePath);
const globalClaudeFolderPath = join(homedir(), '.claude');
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath);
const normalizedGlobalClaudeFolderPath = normalizeCaseForComparison(globalClaudeFolderPath);
return normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + sep.toLowerCase()) || normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + '/');
const absolutePath = expandPath(filePath)
const globalClaudeFolderPath = join(homedir(), '.claude')
const normalizedAbsolutePath = normalizeCaseForComparison(absolutePath)
const normalizedGlobalClaudeFolderPath = normalizeCaseForComparison(
globalClaudeFolderPath,
)
return (
normalizedAbsolutePath.startsWith(
normalizedGlobalClaudeFolderPath + sep.toLowerCase(),
) ||
normalizedAbsolutePath.startsWith(normalizedGlobalClaudeFolderPath + '/')
)
}
export type PermissionOption = {
type: 'accept-once';
} | {
type: 'accept-session';
scope?: 'claude-folder' | 'global-claude-folder';
} | {
type: 'reject';
};
export type PermissionOption =
| { type: 'accept-once' }
| { type: 'accept-session'; scope?: 'claude-folder' | 'global-claude-folder' }
| { type: 'reject' }
export type PermissionOptionWithLabel = OptionWithDescription<string> & {
option: PermissionOption;
};
export type FileOperationType = 'read' | 'write' | 'create';
option: PermissionOption
}
export type FileOperationType = 'read' | 'write' | 'create'
export function getFilePermissionOptions({
filePath,
toolPermissionContext,
@@ -57,18 +74,22 @@ export function getFilePermissionOptions({
onRejectFeedbackChange,
onAcceptFeedbackChange,
yesInputMode = false,
noInputMode = false
noInputMode = false,
}: {
filePath: string;
toolPermissionContext: ToolPermissionContext;
operationType?: FileOperationType;
onRejectFeedbackChange?: (value: string) => void;
onAcceptFeedbackChange?: (value: string) => void;
yesInputMode?: boolean;
noInputMode?: boolean;
filePath: string
toolPermissionContext: ToolPermissionContext
operationType?: FileOperationType
onRejectFeedbackChange?: (value: string) => void
onAcceptFeedbackChange?: (value: string) => void
yesInputMode?: boolean
noInputMode?: boolean
}): PermissionOptionWithLabel[] {
const options: PermissionOptionWithLabel[] = [];
const modeCycleShortcut = getShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab');
const options: PermissionOptionWithLabel[] = []
const modeCycleShortcut = getShortcutDisplay(
'chat:cycleMode',
'Chat',
'shift+tab',
)
// When in input mode, show input field
if (yesInputMode && onAcceptFeedbackChange) {
@@ -79,24 +100,24 @@ export function getFilePermissionOptions({
placeholder: 'and tell Claude what to do next',
onChange: onAcceptFeedbackChange,
allowEmptySubmitToCancel: true,
option: {
type: 'accept-once'
}
});
option: { type: 'accept-once' },
})
} else {
options.push({
label: 'Yes',
value: 'yes',
option: {
type: 'accept-once'
}
});
option: { type: 'accept-once' },
})
}
const inAllowedPath = pathInAllowedWorkingPath(filePath, toolPermissionContext);
const inAllowedPath = pathInAllowedWorkingPath(
filePath,
toolPermissionContext,
)
// Check if this is a .claude/ folder path (project or global)
const inClaudeFolder = isInClaudeFolder(filePath);
const inGlobalClaudeFolder = isInGlobalClaudeFolder(filePath);
const inClaudeFolder = isInClaudeFolder(filePath)
const inGlobalClaudeFolder = isInGlobalClaudeFolder(filePath)
// Option 2: For .claude/ folder, show special option instead of generic session option
// Note: Session-level options are always shown since they only affect in-memory state,
@@ -108,45 +129,52 @@ export function getFilePermissionOptions({
value: 'yes-claude-folder',
option: {
type: 'accept-session',
scope: inGlobalClaudeFolder ? 'global-claude-folder' : 'claude-folder'
}
});
scope: inGlobalClaudeFolder ? 'global-claude-folder' : 'claude-folder',
},
})
} else {
// Option 2: Allow all changes/reads during session
let sessionLabel: ReactNode;
let sessionLabel: ReactNode
if (inAllowedPath) {
// Inside working directory
if (operationType === 'read') {
sessionLabel = 'Yes, during this session';
sessionLabel = 'Yes, during this session'
} else {
sessionLabel = <Text>
sessionLabel = (
<Text>
Yes, allow all edits during this session{' '}
<Text bold>({modeCycleShortcut})</Text>
</Text>;
</Text>
)
}
} else {
// Outside working directory - include directory name
const dirPath = getDirectoryForPath(filePath);
const dirName = basename(dirPath) || 'this directory';
const dirPath = getDirectoryForPath(filePath)
const dirName = basename(dirPath) || 'this directory'
if (operationType === 'read') {
sessionLabel = <Text>
sessionLabel = (
<Text>
Yes, allow reading from <Text bold>{dirName}/</Text> during this
session
</Text>;
</Text>
)
} else {
sessionLabel = <Text>
sessionLabel = (
<Text>
Yes, allow all edits in <Text bold>{dirName}/</Text> during this
session <Text bold>({modeCycleShortcut})</Text>
</Text>;
</Text>
)
}
}
options.push({
label: sessionLabel,
value: 'yes-session',
option: {
type: 'accept-session'
}
});
option: { type: 'accept-session' },
})
}
// When in input mode, show input field for reject
@@ -158,19 +186,16 @@ export function getFilePermissionOptions({
placeholder: 'and tell Claude what to do differently',
onChange: onRejectFeedbackChange,
allowEmptySubmitToCancel: true,
option: {
type: 'reject'
}
});
option: { type: 'reject' },
})
} else {
// Not in input mode - simple option
options.push({
label: 'No',
value: 'no',
option: {
type: 'reject'
}
});
option: { type: 'reject' },
})
}
return options;
return options
}

View File

@@ -1,160 +1,101 @@
import { c as _c } from "react/compiler-runtime";
import { basename, relative } from 'path';
import React, { useMemo } from 'react';
import type { z } from 'zod/v4';
import { Text } from '../../../ink.js';
import { FileWriteTool } from '../../../tools/FileWriteTool/FileWriteTool.js';
import { getCwd } from '../../../utils/cwd.js';
import { isENOENT } from '../../../utils/errors.js';
import { readFileSync } from '../../../utils/fileRead.js';
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js';
import { createSingleEditDiffConfig, type FileEdit, type IDEDiffSupport } from '../FilePermissionDialog/ideDiffConfig.js';
import type { PermissionRequestProps } from '../PermissionRequest.js';
import { FileWriteToolDiff } from './FileWriteToolDiff.js';
type FileWriteToolInput = z.infer<typeof FileWriteTool.inputSchema>;
import { basename, relative } from 'path'
import React, { useMemo } from 'react'
import type { z } from 'zod/v4'
import { Text } from '../../../ink.js'
import { FileWriteTool } from '../../../tools/FileWriteTool/FileWriteTool.js'
import { getCwd } from '../../../utils/cwd.js'
import { isENOENT } from '../../../utils/errors.js'
import { readFileSync } from '../../../utils/fileRead.js'
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'
import {
createSingleEditDiffConfig,
type FileEdit,
type IDEDiffSupport,
} from '../FilePermissionDialog/ideDiffConfig.js'
import type { PermissionRequestProps } from '../PermissionRequest.js'
import { FileWriteToolDiff } from './FileWriteToolDiff.js'
type FileWriteToolInput = z.infer<typeof FileWriteTool.inputSchema>
const ideDiffSupport: IDEDiffSupport<FileWriteToolInput> = {
getConfig: (input: FileWriteToolInput) => {
let oldContent: string;
let oldContent: string
try {
oldContent = readFileSync(input.file_path);
oldContent = readFileSync(input.file_path)
} catch (e) {
if (!isENOENT(e)) throw e;
oldContent = '';
if (!isENOENT(e)) throw e
oldContent = ''
}
return createSingleEditDiffConfig(input.file_path, oldContent, input.content, false // For file writes, we replace the entire content
);
return createSingleEditDiffConfig(
input.file_path,
oldContent,
input.content,
false, // For file writes, we replace the entire content
)
},
applyChanges: (input: FileWriteToolInput, modifiedEdits: FileEdit[]) => {
const firstEdit = modifiedEdits[0];
const firstEdit = modifiedEdits[0]
if (firstEdit) {
return {
...input,
content: firstEdit.new_string
};
content: firstEdit.new_string,
}
}
return input;
return input
},
}
export function FileWritePermissionRequest(
props: PermissionRequestProps,
): React.ReactNode {
const parseInput = (input: unknown): FileWriteToolInput => {
return FileWriteTool.inputSchema.parse(input)
}
};
export function FileWritePermissionRequest(props) {
const $ = _c(30);
const parseInput = _temp;
let t0;
if ($[0] !== props.toolUseConfirm.input) {
t0 = parseInput(props.toolUseConfirm.input);
$[0] = props.toolUseConfirm.input;
$[1] = t0;
} else {
t0 = $[1];
}
const parsed = t0;
const {
file_path,
content
} = parsed;
let t1;
if ($[2] !== file_path) {
;
const parsed = parseInput(props.toolUseConfirm.input)
const { file_path, content } = parsed
// Single read drives both UI text ("Create" vs "Overwrite") and the diff
// shown by FileWriteToolDiff — avoids a redundant existsSync stat that would
// block first-mount commit on slow/networked filesystems.
const { fileExists, oldContent } = useMemo(() => {
try {
t1 = {
fileExists: true,
oldContent: readFileSync(file_path)
};
} catch (t2) {
const e = t2;
if (!isENOENT(e)) {
throw e;
}
let t3;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t3 = {
fileExists: false,
oldContent: ""
};
$[4] = t3;
} else {
t3 = $[4];
}
t1 = t3;
return { fileExists: true, oldContent: readFileSync(file_path) }
} catch (e) {
if (!isENOENT(e)) throw e
return { fileExists: false, oldContent: '' }
}
$[2] = file_path;
$[3] = t1;
} else {
t1 = $[3];
}
const {
fileExists,
oldContent
} = t1;
const actionText = fileExists ? "overwrite" : "create";
const t2 = props.toolUseConfirm;
const t3 = props.toolUseContext;
const t4 = props.onDone;
const t5 = props.onReject;
const t6 = props.workerBadge;
const t7 = fileExists ? "Overwrite file" : "Create file";
let t8;
if ($[5] !== file_path) {
t8 = relative(getCwd(), file_path);
$[5] = file_path;
$[6] = t8;
} else {
t8 = $[6];
}
let t9;
if ($[7] !== file_path) {
t9 = basename(file_path);
$[7] = file_path;
$[8] = t9;
} else {
t9 = $[8];
}
let t10;
if ($[9] !== t9) {
t10 = <Text bold={true}>{t9}</Text>;
$[9] = t9;
$[10] = t10;
} else {
t10 = $[10];
}
let t11;
if ($[11] !== actionText || $[12] !== t10) {
t11 = <Text>Do you want to {actionText} {t10}?</Text>;
$[11] = actionText;
$[12] = t10;
$[13] = t11;
} else {
t11 = $[13];
}
let t12;
if ($[14] !== content || $[15] !== fileExists || $[16] !== file_path || $[17] !== oldContent) {
t12 = <FileWriteToolDiff file_path={file_path} content={content} fileExists={fileExists} oldContent={oldContent} />;
$[14] = content;
$[15] = fileExists;
$[16] = file_path;
$[17] = oldContent;
$[18] = t12;
} else {
t12 = $[18];
}
let t13;
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) {
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} />;
$[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;
$[29] = t13;
} else {
t13 = $[29];
}
return t13;
}
function _temp(input) {
return FileWriteTool.inputSchema.parse(input);
}, [file_path])
const actionText = fileExists ? 'overwrite' : 'create'
return (
<FilePermissionDialog
toolUseConfirm={props.toolUseConfirm}
toolUseContext={props.toolUseContext}
onDone={props.onDone}
onReject={props.onReject}
workerBadge={props.workerBadge}
title={fileExists ? 'Overwrite file' : 'Create file'}
subtitle={relative(getCwd(), file_path)}
question={
<Text>
Do you want to {actionText} <Text bold>{basename(file_path)}</Text>?
</Text>
}
content={
<FileWriteToolDiff
file_path={file_path}
content={content}
fileExists={fileExists}
oldContent={oldContent}
/>
}
path={file_path}
completionType="write_file_single"
parseInput={parseInput}
ideDiffSupport={ideDiffSupport}
/>
)
}

View File

@@ -1,88 +1,82 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { useMemo } from 'react';
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
import { Box, NoSelect, Text } from '../../../ink.js';
import { intersperse } from '../../../utils/array.js';
import { getPatchForDisplay } from '../../../utils/diff.js';
import { HighlightedCode } from '../../HighlightedCode.js';
import { StructuredDiff } from '../../StructuredDiff.js';
import * as React from 'react'
import { useMemo } from 'react'
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
import { Box, NoSelect, Text } from '../../../ink.js'
import { intersperse } from '../../../utils/array.js'
import { getPatchForDisplay } from '../../../utils/diff.js'
import { HighlightedCode } from '../../HighlightedCode.js'
import { StructuredDiff } from '../../StructuredDiff.js'
type Props = {
file_path: string;
content: string;
fileExists: boolean;
oldContent: string;
};
export function FileWriteToolDiff(t0) {
const $ = _c(15);
const {
file_path,
content,
fileExists,
oldContent
} = t0;
const {
columns
} = useTerminalSize();
let t1;
bb0: {
file_path: string
content: string
fileExists: boolean
oldContent: string
}
export function FileWriteToolDiff({
file_path,
content,
fileExists,
oldContent,
}: Props): React.ReactNode {
const { columns } = useTerminalSize()
const hunks = useMemo(() => {
if (!fileExists) {
t1 = null;
break bb0;
return null
}
let t2;
if ($[0] !== content || $[1] !== file_path || $[2] !== oldContent) {
t2 = getPatchForDisplay({
filePath: file_path,
fileContents: oldContent,
edits: [{
return getPatchForDisplay({
filePath: file_path,
fileContents: oldContent,
edits: [
{
old_string: oldContent,
new_string: content,
replace_all: false
}]
});
$[0] = content;
$[1] = file_path;
$[2] = oldContent;
$[3] = t2;
} else {
t2 = $[3];
}
t1 = t2;
}
const hunks = t1;
let t2;
if ($[4] !== content) {
t2 = content.split("\n")[0] ?? null;
$[4] = content;
$[5] = t2;
} else {
t2 = $[5];
}
const firstLine = t2;
let t3;
if ($[6] !== columns || $[7] !== content || $[8] !== file_path || $[9] !== firstLine || $[10] !== hunks || $[11] !== oldContent) {
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} />;
$[6] = columns;
$[7] = content;
$[8] = file_path;
$[9] = firstLine;
$[10] = hunks;
$[11] = oldContent;
$[12] = t3;
} else {
t3 = $[12];
}
let t4;
if ($[13] !== t3) {
t4 = <Box flexDirection="column"><Box borderColor="subtle" borderStyle="dashed" flexDirection="column" borderLeft={false} borderRight={false} paddingX={1}>{t3}</Box></Box>;
$[13] = t3;
$[14] = t4;
} else {
t4 = $[14];
}
return t4;
}
function _temp(i) {
return <NoSelect fromLeftEdge={true} key={`ellipsis-${i}`}><Text dimColor={true}>...</Text></NoSelect>;
replace_all: false,
},
],
})
}, [fileExists, file_path, oldContent, content])
const firstLine = content.split('\n')[0] ?? null
const paddingX = 1
return (
<Box flexDirection="column">
<Box
borderColor="subtle"
borderStyle="dashed"
flexDirection="column"
borderLeft={false}
borderRight={false}
paddingX={paddingX}
>
{hunks ? (
intersperse(
hunks.map(_ => (
<StructuredDiff
key={_.newStart}
patch={_}
dim={false}
filePath={file_path}
firstLine={firstLine}
fileContent={oldContent}
width={columns - 2 * paddingX}
/>
)),
i => (
<NoSelect fromLeftEdge key={`ellipsis-${i}`}>
<Text dimColor>...</Text>
</NoSelect>
),
)
) : (
<HighlightedCode
code={content || '(No content)'}
filePath={file_path}
/>
)}
</Box>
</Box>
)
}

View File

@@ -1,114 +1,89 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { Box, Text, useTheme } from '../../../ink.js';
import { FallbackPermissionRequest } from '../FallbackPermissionRequest.js';
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js';
import type { ToolInput } from '../FilePermissionDialog/useFilePermissionDialog.js';
import type { PermissionRequestProps, ToolUseConfirm } from '../PermissionRequest.js';
import React from 'react'
import { Box, Text, useTheme } from '../../../ink.js'
import { FallbackPermissionRequest } from '../FallbackPermissionRequest.js'
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'
import type { ToolInput } from '../FilePermissionDialog/useFilePermissionDialog.js'
import type {
PermissionRequestProps,
ToolUseConfirm,
} from '../PermissionRequest.js'
function pathFromToolUse(toolUseConfirm: ToolUseConfirm): string | null {
const tool = toolUseConfirm.tool;
const tool = toolUseConfirm.tool
if ('getPath' in tool && typeof tool.getPath === 'function') {
try {
return tool.getPath(toolUseConfirm.input);
return tool.getPath(toolUseConfirm.input)
} catch {
return null;
return null
}
}
return null;
return null
}
export function FilesystemPermissionRequest(t0) {
const $ = _c(30);
const {
toolUseConfirm,
onDone,
onReject,
verbose,
toolUseContext,
workerBadge
} = t0;
const [theme] = useTheme();
let t1;
if ($[0] !== toolUseConfirm) {
t1 = pathFromToolUse(toolUseConfirm);
$[0] = toolUseConfirm;
$[1] = t1;
} else {
t1 = $[1];
}
const path = t1;
let t2;
if ($[2] !== toolUseConfirm.input || $[3] !== toolUseConfirm.tool) {
t2 = toolUseConfirm.tool.userFacingName(toolUseConfirm.input as never);
$[2] = toolUseConfirm.input;
$[3] = toolUseConfirm.tool;
$[4] = t2;
} else {
t2 = $[4];
}
const userFacingName = t2;
const isReadOnly = toolUseConfirm.tool.isReadOnly(toolUseConfirm.input);
const userFacingReadOrEdit = isReadOnly ? "Read" : "Edit";
const title = `${userFacingReadOrEdit} file`;
const parseInput = _temp;
export function FilesystemPermissionRequest({
toolUseConfirm,
onDone,
onReject,
verbose,
toolUseContext,
workerBadge,
}: PermissionRequestProps): React.ReactNode {
const [theme] = useTheme()
const path = pathFromToolUse(toolUseConfirm)
const userFacingName = toolUseConfirm.tool.userFacingName(
toolUseConfirm.input as never,
)
const isReadOnly = toolUseConfirm.tool.isReadOnly(toolUseConfirm.input)
const userFacingReadOrEdit = isReadOnly ? 'Read' : 'Edit'
// Use simple singular form - the actual operation details are shown in content
const title = `${userFacingReadOrEdit} file`
// Simple pass-through parser since we don't need to transform the input
const parseInput = (input: unknown): ToolInput => input as ToolInput
// Fall back to generic permission request if no path is found
if (!path) {
let t3;
if ($[5] !== onDone || $[6] !== onReject || $[7] !== toolUseConfirm || $[8] !== toolUseContext || $[9] !== verbose || $[10] !== workerBadge) {
t3 = <FallbackPermissionRequest toolUseConfirm={toolUseConfirm} toolUseContext={toolUseContext} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} />;
$[5] = onDone;
$[6] = onReject;
$[7] = toolUseConfirm;
$[8] = toolUseContext;
$[9] = verbose;
$[10] = workerBadge;
$[11] = t3;
} else {
t3 = $[11];
}
return t3;
return (
<FallbackPermissionRequest
toolUseConfirm={toolUseConfirm}
toolUseContext={toolUseContext}
onDone={onDone}
onReject={onReject}
verbose={verbose}
workerBadge={workerBadge}
/>
)
}
let t3;
if ($[12] !== theme || $[13] !== toolUseConfirm.input || $[14] !== toolUseConfirm.tool || $[15] !== verbose) {
t3 = toolUseConfirm.tool.renderToolUseMessage(toolUseConfirm.input as never, {
theme,
verbose
});
$[12] = theme;
$[13] = toolUseConfirm.input;
$[14] = toolUseConfirm.tool;
$[15] = verbose;
$[16] = t3;
} else {
t3 = $[16];
}
let t4;
if ($[17] !== t3 || $[18] !== userFacingName) {
t4 = <Box flexDirection="column" paddingX={2} paddingY={1}><Text>{userFacingName}({t3})</Text></Box>;
$[17] = t3;
$[18] = userFacingName;
$[19] = t4;
} else {
t4 = $[19];
}
const content = t4;
const t5 = isReadOnly ? "read" : "write";
let t6;
if ($[20] !== content || $[21] !== onDone || $[22] !== onReject || $[23] !== path || $[24] !== t5 || $[25] !== title || $[26] !== toolUseConfirm || $[27] !== toolUseContext || $[28] !== workerBadge) {
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" />;
$[20] = content;
$[21] = onDone;
$[22] = onReject;
$[23] = path;
$[24] = t5;
$[25] = title;
$[26] = toolUseConfirm;
$[27] = toolUseContext;
$[28] = workerBadge;
$[29] = t6;
} else {
t6 = $[29];
}
return t6;
}
function _temp(input) {
return input as ToolInput;
// Render tool use message content
const content = (
<Box flexDirection="column" paddingX={2} paddingY={1}>
<Text>
{userFacingName}(
{toolUseConfirm.tool.renderToolUseMessage(
toolUseConfirm.input as never,
{ theme, verbose },
)}
)
</Text>
</Box>
)
return (
<FilePermissionDialog
toolUseConfirm={toolUseConfirm}
toolUseContext={toolUseContext}
onDone={onDone}
onReject={onReject}
workerBadge={workerBadge}
title={title}
content={content}
path={path}
parseInput={parseInput}
operationType={isReadOnly ? 'read' : 'write'}
completionType="tool_use_single"
/>
)
}

View File

@@ -1,165 +1,77 @@
import { c as _c } from "react/compiler-runtime";
import { basename } from 'path';
import React from 'react';
import type { z } from 'zod/v4';
import { Text } from '../../../ink.js';
import { NotebookEditTool } from '../../../tools/NotebookEditTool/NotebookEditTool.js';
import { logError } from '../../../utils/log.js';
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js';
import type { PermissionRequestProps } from '../PermissionRequest.js';
import { NotebookEditToolDiff } from './NotebookEditToolDiff.js';
type NotebookEditInput = z.infer<typeof NotebookEditTool.inputSchema>;
export function NotebookEditPermissionRequest(props) {
const $ = _c(52);
const parseInput = _temp;
let T0;
let T1;
let T2;
let language;
let notebook_path;
let parsed;
let t0;
let t1;
let t10;
let t2;
let t3;
let t4;
let t5;
let t6;
let t7;
let t8;
let t9;
if ($[0] !== props.onDone || $[1] !== props.onReject || $[2] !== props.toolUseConfirm || $[3] !== props.toolUseContext || $[4] !== props.workerBadge) {
parsed = parseInput(props.toolUseConfirm.input);
const {
notebook_path: t11,
edit_mode,
cell_type
} = parsed;
notebook_path = t11;
language = cell_type === "markdown" ? "markdown" : "python";
const editTypeText = edit_mode === "insert" ? "insert this cell into" : edit_mode === "delete" ? "delete this cell from" : "make this edit to";
T2 = FilePermissionDialog;
t5 = props.toolUseConfirm;
t6 = props.toolUseContext;
t7 = props.onDone;
t8 = props.onReject;
t9 = props.workerBadge;
t10 = "Edit notebook";
T1 = Text;
t2 = "Do you want to ";
t3 = editTypeText;
t4 = " ";
T0 = Text;
t0 = true;
t1 = basename(notebook_path);
$[0] = props.onDone;
$[1] = props.onReject;
$[2] = props.toolUseConfirm;
$[3] = props.toolUseContext;
$[4] = props.workerBadge;
$[5] = T0;
$[6] = T1;
$[7] = T2;
$[8] = language;
$[9] = notebook_path;
$[10] = parsed;
$[11] = t0;
$[12] = t1;
$[13] = t10;
$[14] = t2;
$[15] = t3;
$[16] = t4;
$[17] = t5;
$[18] = t6;
$[19] = t7;
$[20] = t8;
$[21] = t9;
} else {
T0 = $[5];
T1 = $[6];
T2 = $[7];
language = $[8];
notebook_path = $[9];
parsed = $[10];
t0 = $[11];
t1 = $[12];
t10 = $[13];
t2 = $[14];
t3 = $[15];
t4 = $[16];
t5 = $[17];
t6 = $[18];
t7 = $[19];
t8 = $[20];
t9 = $[21];
import { basename } from 'path'
import React from 'react'
import type { z } from 'zod/v4'
import { Text } from '../../../ink.js'
import { NotebookEditTool } from '../../../tools/NotebookEditTool/NotebookEditTool.js'
import { logError } from '../../../utils/log.js'
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'
import type { PermissionRequestProps } from '../PermissionRequest.js'
import { NotebookEditToolDiff } from './NotebookEditToolDiff.js'
type NotebookEditInput = z.infer<typeof NotebookEditTool.inputSchema>
export function NotebookEditPermissionRequest(
props: PermissionRequestProps,
): React.ReactNode {
const parseInput = (input: unknown): NotebookEditInput => {
const result = NotebookEditTool.inputSchema.safeParse(input)
if (!result.success) {
logError(
new Error(
`Failed to parse notebook edit input: ${result.error.message}`,
),
)
// Return a default value to avoid crashing
return {
notebook_path: '',
new_source: '',
cell_id: '',
} as NotebookEditInput
}
return result.data
}
let t11;
if ($[22] !== T0 || $[23] !== t0 || $[24] !== t1) {
t11 = <T0 bold={t0}>{t1}</T0>;
$[22] = T0;
$[23] = t0;
$[24] = t1;
$[25] = t11;
} else {
t11 = $[25];
}
let t12;
if ($[26] !== T1 || $[27] !== t11 || $[28] !== t2 || $[29] !== t3 || $[30] !== t4) {
t12 = <T1>{t2}{t3}{t4}{t11}?</T1>;
$[26] = T1;
$[27] = t11;
$[28] = t2;
$[29] = t3;
$[30] = t4;
$[31] = t12;
} else {
t12 = $[31];
}
const t13 = props.verbose ? 120 : 80;
let t14;
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) {
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} />;
$[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;
$[39] = t14;
} else {
t14 = $[39];
}
let t15;
if ($[40] !== T2 || $[41] !== language || $[42] !== notebook_path || $[43] !== t10 || $[44] !== t12 || $[45] !== t14 || $[46] !== t5 || $[47] !== t6 || $[48] !== t7 || $[49] !== t8 || $[50] !== t9) {
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} />;
$[40] = T2;
$[41] = language;
$[42] = notebook_path;
$[43] = t10;
$[44] = t12;
$[45] = t14;
$[46] = t5;
$[47] = t6;
$[48] = t7;
$[49] = t8;
$[50] = t9;
$[51] = t15;
} else {
t15 = $[51];
}
return t15;
}
function _temp(input) {
const result = NotebookEditTool.inputSchema.safeParse(input);
if (!result.success) {
logError(new Error(`Failed to parse notebook edit input: ${result.error.message}`));
return {
notebook_path: "",
new_source: "",
cell_id: ""
} as NotebookEditInput;
}
return result.data;
const parsed = parseInput(props.toolUseConfirm.input)
const { notebook_path, edit_mode, cell_type } = parsed
const language = cell_type === 'markdown' ? 'markdown' : 'python'
const editTypeText =
edit_mode === 'insert'
? 'insert this cell into'
: edit_mode === 'delete'
? 'delete this cell from'
: 'make this edit to'
return (
<FilePermissionDialog
toolUseConfirm={props.toolUseConfirm}
toolUseContext={props.toolUseContext}
onDone={props.onDone}
onReject={props.onReject}
workerBadge={props.workerBadge}
title="Edit notebook"
question={
<Text>
Do you want to {editTypeText}{' '}
<Text bold>{basename(notebook_path)}</Text>?
</Text>
}
content={
<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={props.verbose ? 120 : 80}
/>
}
path={notebook_path}
completionType="tool_use_single"
languageName={language}
parseInput={parseInput}
/>
)
}

View File

@@ -1,234 +1,172 @@
import { c as _c } from "react/compiler-runtime";
import { relative } from 'path';
import * as React from 'react';
import { Suspense, use, useMemo } from 'react';
import { Box, NoSelect, Text } from '../../../ink.js';
import type { NotebookCellType, NotebookContent } from '../../../types/notebook.js';
import { intersperse } from '../../../utils/array.js';
import { getCwd } from '../../../utils/cwd.js';
import { getPatchForDisplay } from '../../../utils/diff.js';
import { getFsImplementation } from '../../../utils/fsOperations.js';
import { safeParseJSON } from '../../../utils/json.js';
import { parseCellId } from '../../../utils/notebook.js';
import { HighlightedCode } from '../../HighlightedCode.js';
import { StructuredDiff } from '../../StructuredDiff.js';
import { relative } from 'path'
import * as React from 'react'
import { Suspense, use, useMemo } from 'react'
import { Box, NoSelect, Text } from '../../../ink.js'
import type {
NotebookCellType,
NotebookContent,
} from '../../../types/notebook.js'
import { intersperse } from '../../../utils/array.js'
import { getCwd } from '../../../utils/cwd.js'
import { getPatchForDisplay } from '../../../utils/diff.js'
import { getFsImplementation } from '../../../utils/fsOperations.js'
import { safeParseJSON } from '../../../utils/json.js'
import { parseCellId } from '../../../utils/notebook.js'
import { HighlightedCode } from '../../HighlightedCode.js'
import { StructuredDiff } from '../../StructuredDiff.js'
type Props = {
notebook_path: string;
cell_id: string | undefined;
new_source: string;
cell_type?: NotebookCellType;
edit_mode?: string;
verbose: boolean;
width: number;
};
notebook_path: string
cell_id: string | undefined
new_source: string
cell_type?: NotebookCellType
edit_mode?: string
verbose: boolean
width: number
}
type InnerProps = {
notebook_path: string;
cell_id: string | undefined;
new_source: string;
cell_type?: NotebookCellType;
edit_mode?: string;
verbose: boolean;
width: number;
promise: Promise<NotebookContent | null>;
};
export function NotebookEditToolDiff(props: Props) {
const $ = _c(5);
let t0;
if ($[0] !== props.notebook_path) {
t0 = getFsImplementation().readFile(props.notebook_path, {
encoding: "utf-8"
}).then(_temp).catch(_temp2);
$[0] = props.notebook_path;
$[1] = t0;
} else {
t0 = $[1];
}
const notebookDataPromise = t0;
let t1;
if ($[2] !== notebookDataPromise || $[3] !== props) {
t1 = <Suspense fallback={null}><NotebookEditToolDiffInner {...props} promise={notebookDataPromise} /></Suspense>;
$[2] = notebookDataPromise;
$[3] = props;
$[4] = t1;
} else {
t1 = $[4];
}
return t1;
notebook_path: string
cell_id: string | undefined
new_source: string
cell_type?: NotebookCellType
edit_mode?: string
verbose: boolean
width: number
promise: Promise<NotebookContent | null>
}
function _temp2() {
return null;
export function NotebookEditToolDiff(props: Props): React.ReactNode {
// Create a promise that never rejects so we can handle errors inline.
// Memoized on notebook_path so we don't re-read on every render.
const notebookDataPromise = useMemo(
() =>
getFsImplementation()
.readFile(props.notebook_path, { encoding: 'utf-8' })
.then(content => safeParseJSON(content) as NotebookContent | null)
.catch(() => null),
[props.notebook_path],
)
return (
<Suspense fallback={null}>
<NotebookEditToolDiffInner {...props} promise={notebookDataPromise} />
</Suspense>
)
}
function _temp(content) {
return safeParseJSON(content) as NotebookContent | null;
}
function NotebookEditToolDiffInner(t0: InnerProps) {
const $ = _c(34);
const {
notebook_path,
cell_id,
new_source,
cell_type,
edit_mode: t1,
verbose,
width,
promise
} = t0;
const edit_mode = t1 === undefined ? "replace" : t1;
const notebookData = use(promise);
let t2;
if ($[0] !== cell_id || $[1] !== notebookData) {
bb0: {
if (!notebookData || !cell_id) {
t2 = "";
break bb0;
}
const cellIndex = parseCellId(cell_id);
if (cellIndex !== undefined) {
if (notebookData.cells[cellIndex]) {
const source = notebookData.cells[cellIndex].source;
let t3;
if ($[3] !== source) {
t3 = Array.isArray(source) ? source.join("") : source;
$[3] = source;
$[4] = t3;
} else {
t3 = $[4];
}
t2 = t3;
break bb0;
}
t2 = "";
break bb0;
}
let t3;
if ($[5] !== cell_id) {
t3 = cell => cell.id === cell_id;
$[5] = cell_id;
$[6] = t3;
} else {
t3 = $[6];
}
const cell_0 = notebookData.cells.find(t3);
if (!cell_0) {
t2 = "";
break bb0;
}
t2 = Array.isArray(cell_0.source) ? cell_0.source.join("") : cell_0.source;
function NotebookEditToolDiffInner({
notebook_path,
cell_id,
new_source,
cell_type,
edit_mode = 'replace',
verbose,
width,
promise,
}: InnerProps): React.ReactNode {
const notebookData = use(promise)
const oldSource = useMemo(() => {
if (!notebookData || !cell_id) {
return ''
}
$[0] = cell_id;
$[1] = notebookData;
$[2] = t2;
} else {
t2 = $[2];
}
const oldSource = t2;
let t3;
bb1: {
if (!notebookData || edit_mode === "insert" || edit_mode === "delete") {
t3 = null;
break bb1;
const cellIndex = parseCellId(cell_id)
if (cellIndex !== undefined) {
if (notebookData.cells[cellIndex]) {
const source = notebookData.cells[cellIndex].source
return Array.isArray(source) ? source.join('') : source
}
return ''
}
let t4;
if ($[7] !== new_source || $[8] !== notebook_path || $[9] !== oldSource) {
t4 = getPatchForDisplay({
filePath: notebook_path,
fileContents: oldSource,
edits: [{
const cell = notebookData.cells.find(cell => cell.id === cell_id)
if (!cell) {
return ''
}
return Array.isArray(cell.source) ? cell.source.join('') : cell.source
}, [notebookData, cell_id])
const hunks = useMemo(() => {
if (!notebookData || edit_mode === 'insert' || edit_mode === 'delete') {
return null
}
// Create a "fake" file content with just the cell source
// This allows us to use the regular diff mechanism
return getPatchForDisplay({
filePath: notebook_path,
fileContents: oldSource,
edits: [
{
old_string: oldSource,
new_string: new_source,
replace_all: false
}],
ignoreWhitespace: false
});
$[7] = new_source;
$[8] = notebook_path;
$[9] = oldSource;
$[10] = t4;
} else {
t4 = $[10];
}
t3 = t4;
}
const hunks = t3;
let editTypeDescription;
bb2: switch (edit_mode) {
case "insert":
{
editTypeDescription = "Insert new cell";
break bb2;
}
case "delete":
{
editTypeDescription = "Delete cell";
break bb2;
}
replace_all: false,
},
],
ignoreWhitespace: false,
})
}, [notebookData, notebook_path, oldSource, new_source, edit_mode])
let editTypeDescription: string
switch (edit_mode) {
case 'insert':
editTypeDescription = 'Insert new cell'
break
case 'delete':
editTypeDescription = 'Delete cell'
break
default:
{
editTypeDescription = "Replace cell contents";
}
editTypeDescription = 'Replace cell contents'
}
let t4;
if ($[11] !== notebook_path || $[12] !== verbose) {
t4 = verbose ? notebook_path : relative(getCwd(), notebook_path);
$[11] = notebook_path;
$[12] = verbose;
$[13] = t4;
} else {
t4 = $[13];
}
let t5;
if ($[14] !== t4) {
t5 = <Text bold={true}>{t4}</Text>;
$[14] = t4;
$[15] = t5;
} else {
t5 = $[15];
}
const t6 = cell_type ? ` (${cell_type})` : "";
let t7;
if ($[16] !== cell_id || $[17] !== editTypeDescription || $[18] !== t6) {
t7 = <Text dimColor={true}>{editTypeDescription} for cell {cell_id}{t6}</Text>;
$[16] = cell_id;
$[17] = editTypeDescription;
$[18] = t6;
$[19] = t7;
} else {
t7 = $[19];
}
let t8;
if ($[20] !== t5 || $[21] !== t7) {
t8 = <Box paddingBottom={1} flexDirection="column">{t5}{t7}</Box>;
$[20] = t5;
$[21] = t7;
$[22] = t8;
} else {
t8 = $[22];
}
let t9;
if ($[23] !== cell_type || $[24] !== edit_mode || $[25] !== hunks || $[26] !== new_source || $[27] !== notebook_path || $[28] !== oldSource || $[29] !== width) {
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} />;
$[23] = cell_type;
$[24] = edit_mode;
$[25] = hunks;
$[26] = new_source;
$[27] = notebook_path;
$[28] = oldSource;
$[29] = width;
$[30] = t9;
} else {
t9 = $[30];
}
let t10;
if ($[31] !== t8 || $[32] !== t9) {
t10 = <Box flexDirection="column"><Box borderStyle="round" flexDirection="column" paddingX={1}>{t8}{t9}</Box></Box>;
$[31] = t8;
$[32] = t9;
$[33] = t10;
} else {
t10 = $[33];
}
return t10;
}
function _temp3(i) {
return <NoSelect fromLeftEdge={true} key={`ellipsis-${i}`}><Text dimColor={true}>...</Text></NoSelect>;
return (
<Box flexDirection="column">
<Box borderStyle="round" flexDirection="column" paddingX={1}>
<Box paddingBottom={1} flexDirection="column">
<Text bold>
{verbose ? notebook_path : relative(getCwd(), notebook_path)}
</Text>
<Text dimColor>
{editTypeDescription} for cell {cell_id}
{cell_type ? ` (${cell_type})` : ''}
</Text>
</Box>
{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}
/>
)),
i => (
<NoSelect fromLeftEdge key={`ellipsis-${i}`}>
<Text dimColor>...</Text>
</NoSelect>
),
)
) : (
<HighlightedCode
code={new_source}
filePath={cell_type === 'markdown' ? 'file.md' : notebook_path}
/>
)}
</Box>
</Box>
)
}

View File

@@ -1,459 +1,350 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import chalk from 'chalk';
import figures from 'figures';
import React, { useMemo } from 'react';
import { Ansi, Box, color, Text, useTheme } from '../../ink.js';
import { useAppState } from '../../state/AppState.js';
import type { PermissionMode } from '../../utils/permissions/PermissionMode.js';
import { permissionModeTitle } from '../../utils/permissions/PermissionMode.js';
import type { PermissionDecision, PermissionDecisionReason } from '../../utils/permissions/PermissionResult.js';
import { extractRules } from '../../utils/permissions/PermissionUpdate.js';
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js';
import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js';
import { detectUnreachableRules } from '../../utils/permissions/shadowedRuleDetection.js';
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
import { getSettingSourceDisplayNameLowercase } from '../../utils/settings/constants.js';
import { feature } from 'bun:bundle'
import chalk from 'chalk'
import figures from 'figures'
import React, { useMemo } from 'react'
import { Ansi, Box, color, Text, useTheme } from '../../ink.js'
import { useAppState } from '../../state/AppState.js'
import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'
import { permissionModeTitle } from '../../utils/permissions/PermissionMode.js'
import type {
PermissionDecision,
PermissionDecisionReason,
} from '../../utils/permissions/PermissionResult.js'
import { extractRules } from '../../utils/permissions/PermissionUpdate.js'
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js'
import { detectUnreachableRules } from '../../utils/permissions/shadowedRuleDetection.js'
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
import { getSettingSourceDisplayNameLowercase } from '../../utils/settings/constants.js'
type PermissionDecisionInfoItemProps = {
title?: string;
decisionReason: PermissionDecisionReason;
};
function decisionReasonDisplayString(decisionReason: PermissionDecisionReason & {
type: Exclude<PermissionDecisionReason['type'], 'subcommandResults'>;
}): string {
if ((feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && decisionReason.type === 'classifier') {
return `${chalk.bold(decisionReason.classifier)} classifier: ${decisionReason.reason}`;
title?: string
decisionReason: PermissionDecisionReason
}
function decisionReasonDisplayString(
decisionReason: PermissionDecisionReason & {
type: Exclude<PermissionDecisionReason['type'], 'subcommandResults'>
},
): string {
if (
(feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) &&
decisionReason.type === 'classifier'
) {
return `${chalk.bold(decisionReason.classifier)} classifier: ${decisionReason.reason}`
}
switch (decisionReason.type) {
case 'rule':
return `${chalk.bold(permissionRuleValueToString(decisionReason.rule.ruleValue))} rule from ${getSettingSourceDisplayNameLowercase(decisionReason.rule.source)}`;
return `${chalk.bold(permissionRuleValueToString(decisionReason.rule.ruleValue))} rule from ${getSettingSourceDisplayNameLowercase(decisionReason.rule.source)}`
case 'mode':
return `${permissionModeTitle(decisionReason.mode)} mode`;
return `${permissionModeTitle(decisionReason.mode)} mode`
case 'sandboxOverride':
return 'Requires permission to bypass sandbox';
return 'Requires permission to bypass sandbox'
case 'workingDir':
return decisionReason.reason;
return decisionReason.reason
case 'safetyCheck':
case 'other':
return decisionReason.reason;
return decisionReason.reason
case 'permissionPromptTool':
return `${chalk.bold(decisionReason.permissionPromptToolName)} permission prompt tool`;
return `${chalk.bold(decisionReason.permissionPromptToolName)} permission prompt tool`
case 'hook':
return decisionReason.reason ? `${chalk.bold(decisionReason.hookName)} hook: ${decisionReason.reason}` : `${chalk.bold(decisionReason.hookName)} hook`;
return decisionReason.reason
? `${chalk.bold(decisionReason.hookName)} hook: ${decisionReason.reason}`
: `${chalk.bold(decisionReason.hookName)} hook`
case 'asyncAgent':
return decisionReason.reason;
return decisionReason.reason
default:
return '';
return ''
}
}
function PermissionDecisionInfoItem(t0) {
const $ = _c(10);
const {
title,
decisionReason
} = t0;
const [theme] = useTheme();
let t1;
if ($[0] !== decisionReason || $[1] !== theme) {
t1 = function formatDecisionReason() {
switch (decisionReason.type) {
case "subcommandResults":
{
return <Box flexDirection="column">{Array.from(decisionReason.reasons.entries()).map(t2 => {
const [subcommand, result] = t2 as [string, { behavior: string; decisionReason?: { type: string }; suggestions?: unknown }];
const icon = result.behavior === "allow" ? color("success", theme)(figures.tick) : color("error", theme)(figures.cross);
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 as any)}</Ansi></Text>}{result.behavior === "ask" && <SuggestedRules suggestions={result.suggestions} />}</Box>;
})}</Box>;
}
default:
{
return <Text><Ansi>{decisionReasonDisplayString(decisionReason)}</Ansi></Text>;
}
}
};
$[0] = decisionReason;
$[1] = theme;
$[2] = t1;
} else {
t1 = $[2];
}
const formatDecisionReason = t1;
let t2;
if ($[3] !== title) {
t2 = title && <Text>{title}</Text>;
$[3] = title;
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[5] !== formatDecisionReason) {
t3 = formatDecisionReason();
$[5] = formatDecisionReason;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] !== t2 || $[8] !== t3) {
t4 = <Box flexDirection="column">{t2}{t3}</Box>;
$[7] = t2;
$[8] = t3;
$[9] = t4;
} else {
t4 = $[9];
}
return t4;
}
function SuggestedRules(t0) {
const $ = _c(18);
const {
suggestions
} = t0;
let T0;
let T1;
let t1;
let t2;
let t3;
let t4;
let t5;
if ($[0] !== suggestions) {
t5 = Symbol.for("react.early_return_sentinel");
bb0: {
const rules = extractRules(suggestions);
if (rules.length === 0) {
t5 = null;
break bb0;
}
T1 = Text;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Text dimColor={true}>{" "}{" "}</Text>;
$[8] = t2;
} else {
t2 = $[8];
}
t3 = "Suggested rules:";
t4 = " ";
T0 = Ansi;
t1 = rules.map(_temp).join(", ");
function PermissionDecisionInfoItem({
title,
decisionReason,
}: PermissionDecisionInfoItemProps): React.ReactNode {
const [theme] = useTheme()
function formatDecisionReason(): React.ReactNode {
switch (decisionReason.type) {
case 'subcommandResults':
return (
<Box flexDirection="column">
{Array.from(decisionReason.reasons.entries()).map(
([subcommand, result]) => {
const icon =
result.behavior === 'allow'
? color('success', theme)(figures.tick)
: color('error', theme)(figures.cross)
return (
<Box flexDirection="column" key={subcommand}>
<Text>
{icon} {subcommand}
</Text>
{result.decisionReason !== undefined &&
result.decisionReason.type !== 'subcommandResults' && (
<Text>
<Text dimColor>
{' '}{' '}
</Text>
<Ansi>
{decisionReasonDisplayString(result.decisionReason)}
</Ansi>
</Text>
)}
{result.behavior === 'ask' && (
<SuggestedRules suggestions={result.suggestions} />
)}
</Box>
)
},
)}
</Box>
)
default:
return (
<Text>
<Ansi>{decisionReasonDisplayString(decisionReason)}</Ansi>
</Text>
)
}
$[0] = suggestions;
$[1] = T0;
$[2] = T1;
$[3] = t1;
$[4] = t2;
$[5] = t3;
$[6] = t4;
$[7] = t5;
} else {
T0 = $[1];
T1 = $[2];
t1 = $[3];
t2 = $[4];
t3 = $[5];
t4 = $[6];
t5 = $[7];
}
if (t5 !== Symbol.for("react.early_return_sentinel")) {
return t5;
}
let t6;
if ($[9] !== T0 || $[10] !== t1) {
t6 = <T0>{t1}</T0>;
$[9] = T0;
$[10] = t1;
$[11] = t6;
} else {
t6 = $[11];
}
let t7;
if ($[12] !== T1 || $[13] !== t2 || $[14] !== t3 || $[15] !== t4 || $[16] !== t6) {
t7 = <T1>{t2}{t3}{t4}{t6}</T1>;
$[12] = T1;
$[13] = t2;
$[14] = t3;
$[15] = t4;
$[16] = t6;
$[17] = t7;
} else {
t7 = $[17];
}
return t7;
return (
<Box flexDirection="column">
{title && <Text>{title}</Text>}
{formatDecisionReason()}
</Box>
)
}
function _temp(rule) {
return chalk.bold(permissionRuleValueToString(rule));
function SuggestedRules({
suggestions,
}: {
suggestions: PermissionUpdate[] | undefined
}): React.ReactNode {
const rules = extractRules(suggestions)
if (rules.length === 0) return null
return (
<Text>
<Text dimColor>
{' '}{' '}
</Text>
Suggested rules:{' '}
<Ansi>
{rules
.map(rule => chalk.bold(permissionRuleValueToString(rule)))
.join(', ')}
</Ansi>
</Text>
)
}
type Props = {
permissionResult: PermissionDecision;
toolName?: string; // Filter unreachable rules to this tool
};
permissionResult: PermissionDecision
toolName?: string // Filter unreachable rules to this tool
}
// Helper function to extract directories from permission updates
function extractDirectories(updates: PermissionUpdate[] | undefined): string[] {
if (!updates) return [];
if (!updates) return []
return updates.flatMap(update => {
switch (update.type) {
case 'addDirectories':
return update.directories;
return update.directories
default:
return [];
return []
}
});
})
}
// Helper function to extract mode from permission updates
function extractMode(updates: PermissionUpdate[] | undefined): PermissionMode | undefined {
if (!updates) return undefined;
const update = updates.findLast(u => u.type === 'setMode');
return update?.type === 'setMode' ? update.mode : undefined;
function extractMode(
updates: PermissionUpdate[] | undefined,
): PermissionMode | undefined {
if (!updates) return undefined
const update = updates.findLast(u => u.type === 'setMode')
return update?.type === 'setMode' ? update.mode : undefined
}
function SuggestionDisplay(t0) {
const $ = _c(22);
const {
suggestions,
width
} = t0;
function SuggestionDisplay({
suggestions,
width,
}: {
suggestions: PermissionUpdate[] | undefined
width: number
}): React.ReactNode {
if (!suggestions || suggestions.length === 0) {
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text dimColor={true}>Suggestions </Text>;
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== width) {
t2 = <Box justifyContent="flex-end" minWidth={width}>{t1}</Box>;
$[1] = width;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Text>None</Text>;
$[3] = t3;
} else {
t3 = $[3];
}
let t4;
if ($[4] !== t2) {
t4 = <Box flexDirection="row">{t2}{t3}</Box>;
$[4] = t2;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
return (
<Box flexDirection="row">
<Box justifyContent="flex-end" minWidth={width}>
<Text dimColor>Suggestions </Text>
</Box>
<Text>None</Text>
</Box>
)
}
let t1;
let t2;
if ($[6] !== suggestions || $[7] !== width) {
t2 = Symbol.for("react.early_return_sentinel");
bb0: {
const rules = extractRules(suggestions);
const directories = extractDirectories(suggestions);
const mode = extractMode(suggestions);
if (rules.length === 0 && directories.length === 0 && !mode) {
let t3;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Text dimColor={true}>Suggestion </Text>;
$[10] = t3;
} else {
t3 = $[10];
}
let t4;
if ($[11] !== width) {
t4 = <Box justifyContent="flex-end" minWidth={width}>{t3}</Box>;
$[11] = width;
$[12] = t4;
} else {
t4 = $[12];
}
let t5;
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text>None</Text>;
$[13] = t5;
} else {
t5 = $[13];
}
let t6;
if ($[14] !== t4) {
t6 = <Box flexDirection="row">{t4}{t5}</Box>;
$[14] = t4;
$[15] = t6;
} else {
t6 = $[15];
}
t2 = t6;
break bb0;
}
let t3;
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Text dimColor={true}>Suggestions </Text>;
$[16] = t3;
} else {
t3 = $[16];
}
let t4;
if ($[17] !== width) {
t4 = <Box justifyContent="flex-end" minWidth={width}>{t3}</Box>;
$[17] = width;
$[18] = t4;
} else {
t4 = $[18];
}
let t5;
if ($[19] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text> </Text>;
$[19] = t5;
} else {
t5 = $[19];
}
let t6;
if ($[20] !== t4) {
t6 = <Box flexDirection="row">{t4}{t5}</Box>;
$[20] = t4;
$[21] = t6;
} else {
t6 = $[21];
}
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>;
}
$[6] = suggestions;
$[7] = width;
$[8] = t1;
$[9] = t2;
} else {
t1 = $[8];
t2 = $[9];
const rules = extractRules(suggestions)
const directories = extractDirectories(suggestions)
const mode = extractMode(suggestions)
// If nothing to display, show None
if (rules.length === 0 && directories.length === 0 && !mode) {
return (
<Box flexDirection="row">
<Box justifyContent="flex-end" minWidth={width}>
<Text dimColor>Suggestion </Text>
</Box>
<Text>None</Text>
</Box>
)
}
if (t2 !== Symbol.for("react.early_return_sentinel")) {
return t2;
}
return t1;
return (
<Box flexDirection="column">
<Box flexDirection="row">
<Box justifyContent="flex-end" minWidth={width}>
<Text dimColor>Suggestions </Text>
</Box>
<Text> </Text>
</Box>
{/* Display rules */}
{rules.length > 0 && (
<Box flexDirection="row">
<Box justifyContent="flex-end" minWidth={width}>
<Text dimColor> Rules </Text>
</Box>
<Box flexDirection="column">
{rules.map((rule, index) => (
<Text key={index}>
{figures.bullet} {permissionRuleValueToString(rule)}
</Text>
))}
</Box>
</Box>
)}
{/* Display directories */}
{directories.length > 0 && (
<Box flexDirection="row">
<Box justifyContent="flex-end" minWidth={width}>
<Text dimColor> Directories </Text>
</Box>
<Box flexDirection="column">
{directories.map((dir, index) => (
<Text key={index}>
{figures.bullet} {dir}
</Text>
))}
</Box>
</Box>
)}
{/* Display mode change */}
{mode && (
<Box flexDirection="row">
<Box justifyContent="flex-end" minWidth={width}>
<Text dimColor> Mode </Text>
</Box>
<Text>{permissionModeTitle(mode)}</Text>
</Box>
)}
</Box>
)
}
function _temp3(dir, index_0) {
return <Text key={index_0}>{figures.bullet} {dir}</Text>;
}
function _temp2(rule, index) {
return <Text key={index}>{figures.bullet} {permissionRuleValueToString(rule)}</Text>;
}
export function PermissionDecisionDebugInfo(t0) {
const $ = _c(25);
const {
permissionResult,
toolName
} = t0;
const toolPermissionContext = useAppState(_temp4);
const decisionReason = permissionResult.decisionReason;
const suggestions = "suggestions" in permissionResult ? permissionResult.suggestions : undefined;
let t1;
if ($[0] !== suggestions || $[1] !== toolName || $[2] !== toolPermissionContext) {
bb0: {
const sandboxAutoAllowEnabled = SandboxManager.isSandboxingEnabled() && SandboxManager.isAutoAllowBashIfSandboxedEnabled();
const all = detectUnreachableRules(toolPermissionContext, {
sandboxAutoAllowEnabled
});
const suggestedRules = extractRules(suggestions);
if (suggestedRules.length > 0) {
t1 = all.filter(u => suggestedRules.some(suggested => suggested.toolName === u.rule.ruleValue.toolName && suggested.ruleContent === u.rule.ruleValue.ruleContent));
break bb0;
}
if (toolName) {
let t2;
if ($[4] !== toolName) {
t2 = u_0 => u_0.rule.ruleValue.toolName === toolName;
$[4] = toolName;
$[5] = t2;
} else {
t2 = $[5];
}
t1 = all.filter(t2);
break bb0;
}
t1 = all;
export function PermissionDecisionDebugInfo({
permissionResult,
toolName,
}: Props): React.ReactNode {
const toolPermissionContext = useAppState(s => s.toolPermissionContext)
const decisionReason = permissionResult.decisionReason
const suggestions =
'suggestions' in permissionResult ? permissionResult.suggestions : undefined
const unreachableRules = useMemo(() => {
const sandboxAutoAllowEnabled =
SandboxManager.isSandboxingEnabled() &&
SandboxManager.isAutoAllowBashIfSandboxedEnabled()
const all = detectUnreachableRules(toolPermissionContext, {
sandboxAutoAllowEnabled,
})
// Get the suggested rules from the permission result
const suggestedRules = extractRules(suggestions)
// Filter to rules that match any of the suggested rules
// A rule matches if it has the same toolName and ruleContent
if (suggestedRules.length > 0) {
return all.filter(u =>
suggestedRules.some(
suggested =>
suggested.toolName === u.rule.ruleValue.toolName &&
suggested.ruleContent === u.rule.ruleValue.ruleContent,
),
)
}
$[0] = suggestions;
$[1] = toolName;
$[2] = toolPermissionContext;
$[3] = t1;
} else {
t1 = $[3];
}
const unreachableRules = t1;
let t2;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Box justifyContent="flex-end" minWidth={10}><Text dimColor={true}>Behavior </Text></Box>;
$[6] = t2;
} else {
t2 = $[6];
}
let t3;
if ($[7] !== permissionResult.behavior) {
t3 = <Box flexDirection="row">{t2}<Text>{permissionResult.behavior}</Text></Box>;
$[7] = permissionResult.behavior;
$[8] = t3;
} else {
t3 = $[8];
}
let t4;
if ($[9] !== permissionResult.behavior || $[10] !== permissionResult.message) {
t4 = permissionResult.behavior !== "allow" && <Box flexDirection="row"><Box justifyContent="flex-end" minWidth={10}><Text dimColor={true}>Message </Text></Box><Text>{permissionResult.message}</Text></Box>;
$[9] = permissionResult.behavior;
$[10] = permissionResult.message;
$[11] = t4;
} else {
t4 = $[11];
}
let t5;
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Box justifyContent="flex-end" minWidth={10}><Text dimColor={true}>Reason </Text></Box>;
$[12] = t5;
} else {
t5 = $[12];
}
let t6;
if ($[13] !== decisionReason) {
t6 = <Box flexDirection="row">{t5}{decisionReason === undefined ? <Text>undefined</Text> : <PermissionDecisionInfoItem decisionReason={decisionReason} />}</Box>;
$[13] = decisionReason;
$[14] = t6;
} else {
t6 = $[14];
}
let t7;
if ($[15] !== suggestions) {
t7 = <SuggestionDisplay suggestions={suggestions} width={10} />;
$[15] = suggestions;
$[16] = t7;
} else {
t7 = $[16];
}
let t8;
if ($[17] !== unreachableRules) {
t8 = unreachableRules.length > 0 && <Box flexDirection="column" marginTop={1}><Text color="warning">{figures.warning} Unreachable Rules ({unreachableRules.length})</Text>{unreachableRules.map(_temp5)}</Box>;
$[17] = unreachableRules;
$[18] = t8;
} else {
t8 = $[18];
}
let t9;
if ($[19] !== t3 || $[20] !== t4 || $[21] !== t6 || $[22] !== t7 || $[23] !== t8) {
t9 = <Box flexDirection="column">{t3}{t4}{t6}{t7}{t8}</Box>;
$[19] = t3;
$[20] = t4;
$[21] = t6;
$[22] = t7;
$[23] = t8;
$[24] = t9;
} else {
t9 = $[24];
}
return t9;
}
function _temp5(u_1, i) {
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>;
}
function _temp4(s) {
return s.toolPermissionContext;
// Fallback: filter by tool name if specified
if (toolName) {
return all.filter(u => u.rule.ruleValue.toolName === toolName)
}
return all
}, [toolPermissionContext, toolName, suggestions])
const WIDTH = 10
return (
<Box flexDirection="column">
<Box flexDirection="row">
<Box justifyContent="flex-end" minWidth={WIDTH}>
<Text dimColor>Behavior </Text>
</Box>
<Text>{permissionResult.behavior}</Text>
</Box>
{permissionResult.behavior !== 'allow' && (
<Box flexDirection="row">
<Box justifyContent="flex-end" minWidth={WIDTH}>
<Text dimColor>Message </Text>
</Box>
<Text>{permissionResult.message}</Text>
</Box>
)}
<Box flexDirection="row">
<Box justifyContent="flex-end" minWidth={WIDTH}>
<Text dimColor>Reason </Text>
</Box>
{decisionReason === undefined ? (
<Text>undefined</Text>
) : (
<PermissionDecisionInfoItem decisionReason={decisionReason} />
)}
</Box>
<SuggestionDisplay suggestions={suggestions} width={WIDTH} />
{unreachableRules.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text color="warning">
{figures.warning} Unreachable Rules ({unreachableRules.length})
</Text>
{unreachableRules.map((u, i) => (
<Box key={i} flexDirection="column" marginLeft={2}>
<Text color="warning">
{permissionRuleValueToString(u.rule.ruleValue)}
</Text>
<Text dimColor>
{' '}
{u.reason}
</Text>
<Text dimColor>
{' '}Fix: {u.fix}
</Text>
</Box>
))}
</Box>
)}
</Box>
)
}

View File

@@ -1,71 +1,54 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { Box } from '../../ink.js';
import type { Theme } from '../../utils/theme.js';
import { PermissionRequestTitle } from './PermissionRequestTitle.js';
import type { WorkerBadgeProps } from './WorkerBadge.js';
import * as React from 'react'
import { Box } from '../../ink.js'
import type { Theme } from '../../utils/theme.js'
import { PermissionRequestTitle } from './PermissionRequestTitle.js'
import type { WorkerBadgeProps } from './WorkerBadge.js'
type Props = {
title: string;
subtitle?: React.ReactNode;
color?: keyof Theme;
titleColor?: keyof Theme;
innerPaddingX?: number;
workerBadge?: WorkerBadgeProps;
titleRight?: React.ReactNode;
children: React.ReactNode;
};
export function PermissionDialog(t0) {
const $ = _c(15);
const {
title,
subtitle,
color: t1,
titleColor,
innerPaddingX: t2,
workerBadge,
titleRight,
children
} = t0;
const color = t1 === undefined ? "permission" : t1;
const innerPaddingX = t2 === undefined ? 1 : t2;
let t3;
if ($[0] !== subtitle || $[1] !== title || $[2] !== titleColor || $[3] !== workerBadge) {
t3 = <PermissionRequestTitle title={title} subtitle={subtitle} color={titleColor} workerBadge={workerBadge} />;
$[0] = subtitle;
$[1] = title;
$[2] = titleColor;
$[3] = workerBadge;
$[4] = t3;
} else {
t3 = $[4];
}
let t4;
if ($[5] !== t3 || $[6] !== titleRight) {
t4 = <Box paddingX={1} flexDirection="column"><Box justifyContent="space-between">{t3}{titleRight}</Box></Box>;
$[5] = t3;
$[6] = titleRight;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== children || $[9] !== innerPaddingX) {
t5 = <Box flexDirection="column" paddingX={innerPaddingX}>{children}</Box>;
$[8] = children;
$[9] = innerPaddingX;
$[10] = t5;
} else {
t5 = $[10];
}
let t6;
if ($[11] !== color || $[12] !== t4 || $[13] !== t5) {
t6 = <Box flexDirection="column" borderStyle="round" borderColor={color} borderLeft={false} borderRight={false} borderBottom={false} marginTop={1}>{t4}{t5}</Box>;
$[11] = color;
$[12] = t4;
$[13] = t5;
$[14] = t6;
} else {
t6 = $[14];
}
return t6;
title: string
subtitle?: React.ReactNode
color?: keyof Theme
titleColor?: keyof Theme
innerPaddingX?: number
workerBadge?: WorkerBadgeProps
titleRight?: React.ReactNode
children: React.ReactNode
}
export function PermissionDialog({
title,
subtitle,
color = 'permission',
titleColor,
innerPaddingX = 1,
workerBadge,
titleRight,
children,
}: Props): React.ReactNode {
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={color}
borderLeft={false}
borderRight={false}
borderBottom={false}
marginTop={1}
>
<Box paddingX={1} flexDirection="column">
<Box justifyContent="space-between">
<PermissionRequestTitle
title={title}
subtitle={subtitle}
color={titleColor}
workerBadge={workerBadge}
/>
{titleRight}
</Box>
</Box>
<Box flexDirection="column" paddingX={innerPaddingX}>
{children}
</Box>
</Box>
)
}

View File

@@ -1,87 +1,93 @@
import { c as _c } from "react/compiler-runtime";
import React, { Suspense, use, useState } from 'react';
import { Box, Text } from '../../ink.js';
import { useKeybinding } from '../../keybindings/useKeybinding.js';
import { logEvent } from '../../services/analytics/index.js';
import type { Message } from '../../types/message.js';
import { generatePermissionExplanation, isPermissionExplainerEnabled, type PermissionExplanation as PermissionExplanationType, type RiskLevel } from '../../utils/permissions/permissionExplainer.js';
import { ShimmerChar } from '../Spinner/ShimmerChar.js';
import { useShimmerAnimation } from '../Spinner/useShimmerAnimation.js';
const LOADING_MESSAGE = 'Loading explanation…';
function ShimmerLoadingText() {
const $ = _c(7);
const [ref, glimmerIndex] = useShimmerAnimation("responding", LOADING_MESSAGE, false);
let t0;
if ($[0] !== glimmerIndex) {
t0 = LOADING_MESSAGE.split("").map((char, index) => <ShimmerChar key={index} char={char} index={index} glimmerIndex={glimmerIndex} messageColor="inactive" shimmerColor="text" />);
$[0] = glimmerIndex;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] !== t0) {
t1 = <Text>{t0}</Text>;
$[2] = t0;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== ref || $[5] !== t1) {
t2 = <Box ref={ref}>{t1}</Box>;
$[4] = ref;
$[5] = t1;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
import React, { Suspense, use, useState } from 'react'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import { logEvent } from '../../services/analytics/index.js'
import type { Message } from '../../types/message.js'
import {
generatePermissionExplanation,
isPermissionExplainerEnabled,
type PermissionExplanation as PermissionExplanationType,
type RiskLevel,
} from '../../utils/permissions/permissionExplainer.js'
import { ShimmerChar } from '../Spinner/ShimmerChar.js'
import { useShimmerAnimation } from '../Spinner/useShimmerAnimation.js'
const LOADING_MESSAGE = 'Loading explanation…'
function ShimmerLoadingText(): React.ReactNode {
const [ref, glimmerIndex] = useShimmerAnimation(
'responding',
LOADING_MESSAGE,
false,
)
return (
<Box ref={ref}>
<Text>
{LOADING_MESSAGE.split('').map((char, index) => (
<ShimmerChar
key={index}
char={char}
index={index}
glimmerIndex={glimmerIndex}
messageColor="inactive"
shimmerColor="text"
/>
))}
</Text>
</Box>
)
}
function getRiskColor(riskLevel: RiskLevel): 'success' | 'warning' | 'error' {
switch (riskLevel) {
case 'LOW':
return 'success';
return 'success'
case 'MEDIUM':
return 'warning';
return 'warning'
case 'HIGH':
return 'error';
return 'error'
}
}
function getRiskLabel(riskLevel: RiskLevel): string {
switch (riskLevel) {
case 'LOW':
return 'Low risk';
return 'Low risk'
case 'MEDIUM':
return 'Med risk';
return 'Med risk'
case 'HIGH':
return 'High risk';
return 'High risk'
}
}
type PermissionExplanationProps = {
toolName: string;
toolInput: unknown;
toolDescription?: string;
messages?: Message[];
};
toolName: string
toolInput: unknown
toolDescription?: string
messages?: Message[]
}
type ExplainerState = {
visible: boolean;
enabled: boolean;
promise: Promise<PermissionExplanationType | null> | null;
};
visible: boolean
enabled: boolean
promise: Promise<PermissionExplanationType | null> | null
}
/**
* Creates an explanation promise that never rejects.
* Errors are caught and returned as null.
*/
function createExplanationPromise(props: PermissionExplanationProps): Promise<PermissionExplanationType | null> {
function createExplanationPromise(
props: PermissionExplanationProps,
): Promise<PermissionExplanationType | null> {
return generatePermissionExplanation({
toolName: props.toolName,
toolInput: props.toolInput,
toolDescription: props.toolDescription,
messages: props.messages,
signal: new AbortController().signal // Won't abort - request is fast enough
}).catch(() => null);
signal: new AbortController().signal, // Won't abort - request is fast enough
}).catch(() => null)
}
/**
@@ -89,183 +95,93 @@ function createExplanationPromise(props: PermissionExplanationProps): Promise<Pe
* Creates the fetch promise lazily (only when user hits Ctrl+E)
* to avoid consuming tokens for explanations users never view.
*/
export function usePermissionExplainerUI(props) {
const $ = _c(9);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = isPermissionExplainerEnabled();
$[0] = t0;
} else {
t0 = $[0];
}
const enabled = t0;
const [visible, setVisible] = useState(false);
const [promise, setPromise] = useState(null);
let t1;
if ($[1] !== promise || $[2] !== props || $[3] !== visible) {
t1 = () => {
export function usePermissionExplainerUI(
props: PermissionExplanationProps,
): ExplainerState {
const enabled = isPermissionExplainerEnabled()
const [visible, setVisible] = useState(false)
const [promise, setPromise] =
useState<Promise<PermissionExplanationType | null> | null>(null)
// Use keybinding for ctrl+e toggle (configurable via keybindings.json)
useKeybinding(
'confirm:toggleExplanation',
() => {
if (!visible) {
logEvent("tengu_permission_explainer_shortcut_used", {});
logEvent('tengu_permission_explainer_shortcut_used', {})
// Only create the promise on first toggle (lazy loading)
if (!promise) {
setPromise(createExplanationPromise(props));
setPromise(createExplanationPromise(props))
}
}
setVisible(_temp);
};
$[1] = promise;
$[2] = props;
$[3] = visible;
$[4] = t1;
} else {
t1 = $[4];
}
let t2;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t2 = {
context: "Confirmation",
isActive: enabled
};
$[5] = t2;
} else {
t2 = $[5];
}
useKeybinding("confirm:toggleExplanation", t1, t2);
let t3;
if ($[6] !== promise || $[7] !== visible) {
t3 = {
visible,
enabled,
promise
};
$[6] = promise;
$[7] = visible;
$[8] = t3;
} else {
t3 = $[8];
}
return t3;
setVisible(v => !v)
},
{ context: 'Confirmation', isActive: enabled },
)
return { visible, enabled, promise }
}
/**
* Inner component that uses React 19's use() to read the promise.
* Suspends while loading, returns null on error.
*/
function _temp(v) {
return !v;
}
function ExplanationResult(t0) {
const $ = _c(21);
const {
promise
} = t0;
const explanation = use(promise) as PermissionExplanationType | null;
function ExplanationResult({
promise,
}: {
promise: Promise<PermissionExplanationType | null>
}): React.ReactNode {
const explanation = use(promise)
if (!explanation) {
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Box marginTop={1}><Text dimColor={true}>Explanation unavailable</Text></Box>;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
return (
<Box marginTop={1}>
<Text dimColor>Explanation unavailable</Text>
</Box>
)
}
let t1;
if ($[1] !== explanation.explanation) {
t1 = <Text>{explanation.explanation}</Text>;
$[1] = explanation.explanation;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== explanation.reasoning) {
t2 = <Box marginTop={1}><Text>{explanation.reasoning}</Text></Box>;
$[3] = explanation.reasoning;
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[5] !== explanation.riskLevel) {
t3 = getRiskColor(explanation.riskLevel);
$[5] = explanation.riskLevel;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] !== explanation.riskLevel) {
t4 = getRiskLabel(explanation.riskLevel);
$[7] = explanation.riskLevel;
$[8] = t4;
} else {
t4 = $[8];
}
let t5;
if ($[9] !== t3 || $[10] !== t4) {
t5 = <Text color={t3}>{t4}:</Text>;
$[9] = t3;
$[10] = t4;
$[11] = t5;
} else {
t5 = $[11];
}
let t6;
if ($[12] !== explanation.risk) {
t6 = <Text> {explanation.risk}</Text>;
$[12] = explanation.risk;
$[13] = t6;
} else {
t6 = $[13];
}
let t7;
if ($[14] !== t5 || $[15] !== t6) {
t7 = <Box marginTop={1}><Text>{t5}{t6}</Text></Box>;
$[14] = t5;
$[15] = t6;
$[16] = t7;
} else {
t7 = $[16];
}
let t8;
if ($[17] !== t1 || $[18] !== t2 || $[19] !== t7) {
t8 = <Box flexDirection="column" marginTop={1}>{t1}{t2}{t7}</Box>;
$[17] = t1;
$[18] = t2;
$[19] = t7;
$[20] = t8;
} else {
t8 = $[20];
}
return t8;
return (
<Box flexDirection="column" marginTop={1}>
<Text>{explanation.explanation}</Text>
<Box marginTop={1}>
<Text>{explanation.reasoning}</Text>
</Box>
<Box marginTop={1}>
<Text>
<Text color={getRiskColor(explanation.riskLevel)}>
{getRiskLabel(explanation.riskLevel)}:
</Text>
<Text> {explanation.risk}</Text>
</Text>
</Box>
</Box>
)
}
/**
* Content component - shows loading (via Suspense) or explanation when visible
*/
export function PermissionExplainerContent(t0) {
const $ = _c(3);
const {
visible,
promise
} = t0;
export function PermissionExplainerContent({
visible,
promise,
}: {
visible: boolean
promise: Promise<PermissionExplanationType | null> | null
}): React.ReactNode {
if (!visible || !promise) {
return null;
return null
}
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Box marginTop={1}><ShimmerLoadingText /></Box>;
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== promise) {
t2 = <Suspense fallback={t1}><ExplanationResult promise={promise} /></Suspense>;
$[1] = promise;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
return (
<Suspense
fallback={
<Box marginTop={1}>
<ShimmerLoadingText />
</Box>
}
>
<ExplanationResult promise={promise} />
</Suspense>
)
}

View File

@@ -1,36 +1,43 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode, useCallback, useMemo, useState } from 'react';
import { Box, Text } from '../../ink.js';
import type { KeybindingAction } from '../../keybindings/types.js';
import { useKeybindings } from '../../keybindings/useKeybinding.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
import { useSetAppState } from '../../state/AppState.js';
import { type OptionWithDescription, Select } from '../CustomSelect/select.js';
export type FeedbackType = 'accept' | 'reject';
import React, { type ReactNode, useCallback, useMemo, useState } from 'react'
import { Box, Text } from '../../ink.js'
import type { KeybindingAction } from '../../keybindings/types.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../services/analytics/index.js'
import { useSetAppState } from '../../state/AppState.js'
import { type OptionWithDescription, Select } from '../CustomSelect/select.js'
export type FeedbackType = 'accept' | 'reject'
export type PermissionPromptOption<T extends string> = {
value: T;
label: ReactNode;
value: T
label: ReactNode
feedbackConfig?: {
type: FeedbackType;
placeholder?: string;
};
keybinding?: KeybindingAction;
};
type: FeedbackType
placeholder?: string
}
keybinding?: KeybindingAction
}
export type ToolAnalyticsContext = {
toolName: string;
isMcp: boolean;
};
toolName: string
isMcp: boolean
}
export type PermissionPromptProps<T extends string> = {
options: PermissionPromptOption<T>[];
onSelect: (value: T, feedback?: string) => void;
onCancel?: () => void;
question?: string | ReactNode;
toolAnalyticsContext?: ToolAnalyticsContext;
};
options: PermissionPromptOption<T>[]
onSelect: (value: T, feedback?: string) => void
onCancel?: () => void
question?: string | ReactNode
toolAnalyticsContext?: ToolAnalyticsContext
}
const DEFAULT_PLACEHOLDERS: Record<FeedbackType, string> = {
accept: 'tell Claude what to do next',
reject: 'tell Claude what to do differently'
};
reject: 'tell Claude what to do differently',
}
/**
* Shared component for permission prompts with optional feedback input.
@@ -42,294 +49,219 @@ const DEFAULT_PLACEHOLDERS: Record<FeedbackType, string> = {
* - Analytics events for feedback interactions
* - Transforming options to Select-compatible format
*/
export function PermissionPrompt(t0) {
const $ = _c(54);
const {
options,
onSelect,
onCancel,
question: t1,
toolAnalyticsContext
} = t0;
const question = t1 === undefined ? "Do you want to proceed?" : t1;
const setAppState = useSetAppState();
const [acceptFeedback, setAcceptFeedback] = useState("");
const [rejectFeedback, setRejectFeedback] = useState("");
const [acceptInputMode, setAcceptInputMode] = useState(false);
const [rejectInputMode, setRejectInputMode] = useState(false);
const [focusedValue, setFocusedValue] = useState(null);
const [acceptFeedbackModeEntered, setAcceptFeedbackModeEntered] = useState(false);
const [rejectFeedbackModeEntered, setRejectFeedbackModeEntered] = useState(false);
let t2;
if ($[0] !== focusedValue || $[1] !== options) {
let t3;
if ($[3] !== focusedValue) {
t3 = opt => opt.value === focusedValue;
$[3] = focusedValue;
$[4] = t3;
} else {
t3 = $[4];
}
t2 = options.find(t3);
$[0] = focusedValue;
$[1] = options;
$[2] = t2;
} else {
t2 = $[2];
}
const focusedOption = t2;
const focusedFeedbackType = focusedOption?.feedbackConfig?.type;
const showTabHint = focusedFeedbackType === "accept" && !acceptInputMode || focusedFeedbackType === "reject" && !rejectInputMode;
let t3;
if ($[5] !== acceptInputMode || $[6] !== options || $[7] !== rejectInputMode) {
let t4;
if ($[9] !== acceptInputMode || $[10] !== rejectInputMode) {
t4 = opt_0 => {
const {
value,
label,
feedbackConfig
} = opt_0;
if (!feedbackConfig) {
return {
label,
value
};
}
const {
type,
placeholder
} = feedbackConfig;
const isInputMode = type === "accept" ? acceptInputMode : rejectInputMode;
const onChange = type === "accept" ? setAcceptFeedback : setRejectFeedback;
const defaultPlaceholder = DEFAULT_PLACEHOLDERS[type];
if (isInputMode) {
return {
type: "input" as const,
label,
value,
placeholder: placeholder ?? defaultPlaceholder,
onChange,
allowEmptySubmitToCancel: true
};
}
export function PermissionPrompt<T extends string>({
options,
onSelect,
onCancel,
question = 'Do you want to proceed?',
toolAnalyticsContext,
}: PermissionPromptProps<T>): React.ReactNode {
const setAppState = useSetAppState()
const [acceptFeedback, setAcceptFeedback] = useState('')
const [rejectFeedback, setRejectFeedback] = useState('')
const [acceptInputMode, setAcceptInputMode] = useState(false)
const [rejectInputMode, setRejectInputMode] = useState(false)
const [focusedValue, setFocusedValue] = useState<T | null>(null)
// Track whether user ever entered feedback mode (persists after collapse)
const [acceptFeedbackModeEntered, setAcceptFeedbackModeEntered] =
useState(false)
const [rejectFeedbackModeEntered, setRejectFeedbackModeEntered] =
useState(false)
// Find which option is focused and whether it has feedback config
const focusedOption = options.find(opt => opt.value === focusedValue)
const focusedFeedbackType = focusedOption?.feedbackConfig?.type
// Show Tab hint when focused on a feedback-enabled option that's not already in input mode
const showTabHint =
(focusedFeedbackType === 'accept' && !acceptInputMode) ||
(focusedFeedbackType === 'reject' && !rejectInputMode)
// Transform options to Select-compatible format
const selectOptions = useMemo((): OptionWithDescription<T>[] => {
return options.map(opt => {
const { value, label, feedbackConfig } = opt
// No feedback config = simple option
if (!feedbackConfig) {
return {
label,
value
};
};
$[9] = acceptInputMode;
$[10] = rejectInputMode;
$[11] = t4;
} else {
t4 = $[11];
}
t3 = options.map(t4);
$[5] = acceptInputMode;
$[6] = options;
$[7] = rejectInputMode;
$[8] = t3;
} else {
t3 = $[8];
}
const selectOptions = t3;
let t4;
if ($[12] !== acceptInputMode || $[13] !== options || $[14] !== rejectInputMode || $[15] !== toolAnalyticsContext?.isMcp || $[16] !== toolAnalyticsContext?.toolName) {
t4 = value_0 => {
const option = options.find(opt_1 => opt_1.value === value_0);
if (!option?.feedbackConfig) {
return;
value,
}
}
const {
type: type_0
} = option.feedbackConfig;
const { type, placeholder } = feedbackConfig
const isInputMode = type === 'accept' ? acceptInputMode : rejectInputMode
const onChange = type === 'accept' ? setAcceptFeedback : setRejectFeedback
const defaultPlaceholder = DEFAULT_PLACEHOLDERS[type]
// When in input mode, show input field
if (isInputMode) {
return {
type: 'input' as const,
label,
value,
placeholder: placeholder ?? defaultPlaceholder,
onChange,
allowEmptySubmitToCancel: true,
}
}
// Not in input mode - show simple option
return {
label,
value,
}
})
}, [options, acceptInputMode, rejectInputMode])
// Handle Tab key to toggle input mode
const handleInputModeToggle = useCallback(
(value: T) => {
const option = options.find(opt => opt.value === value)
if (!option?.feedbackConfig) return
const { type } = option.feedbackConfig
const analyticsProps = {
toolName: toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
isMcp: toolAnalyticsContext?.isMcp ?? false
};
if (type_0 === "accept") {
toolName:
toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
isMcp: toolAnalyticsContext?.isMcp ?? false,
}
if (type === 'accept') {
if (acceptInputMode) {
setAcceptInputMode(false);
logEvent("tengu_accept_feedback_mode_collapsed", analyticsProps);
setAcceptInputMode(false)
logEvent('tengu_accept_feedback_mode_collapsed', analyticsProps)
} else {
setAcceptInputMode(true);
setAcceptFeedbackModeEntered(true);
logEvent("tengu_accept_feedback_mode_entered", analyticsProps);
setAcceptInputMode(true)
setAcceptFeedbackModeEntered(true)
logEvent('tengu_accept_feedback_mode_entered', analyticsProps)
}
} else {
if (type_0 === "reject") {
if (rejectInputMode) {
setRejectInputMode(false);
logEvent("tengu_reject_feedback_mode_collapsed", analyticsProps);
} else {
setRejectInputMode(true);
setRejectFeedbackModeEntered(true);
logEvent("tengu_reject_feedback_mode_entered", analyticsProps);
}
} else if (type === 'reject') {
if (rejectInputMode) {
setRejectInputMode(false)
logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps)
} else {
setRejectInputMode(true)
setRejectFeedbackModeEntered(true)
logEvent('tengu_reject_feedback_mode_entered', analyticsProps)
}
}
};
$[12] = acceptInputMode;
$[13] = options;
$[14] = rejectInputMode;
$[15] = toolAnalyticsContext?.isMcp;
$[16] = toolAnalyticsContext?.toolName;
$[17] = t4;
} else {
t4 = $[17];
}
const handleInputModeToggle = t4;
let t5;
if ($[18] !== acceptFeedback || $[19] !== acceptFeedbackModeEntered || $[20] !== onSelect || $[21] !== options || $[22] !== rejectFeedback || $[23] !== rejectFeedbackModeEntered || $[24] !== toolAnalyticsContext?.isMcp || $[25] !== toolAnalyticsContext?.toolName) {
t5 = value_1 => {
const option_0 = options.find(opt_2 => opt_2.value === value_1);
if (!option_0) {
return;
}
let feedback;
if (option_0.feedbackConfig) {
const rawFeedback = option_0.feedbackConfig.type === "accept" ? acceptFeedback : rejectFeedback;
const trimmedFeedback = rawFeedback.trim();
},
[options, acceptInputMode, rejectInputMode, toolAnalyticsContext],
)
// Handle selection
const handleSelect = useCallback(
(value: T) => {
const option = options.find(opt => opt.value === value)
if (!option) return
// Get feedback if applicable
let feedback: string | undefined
if (option.feedbackConfig) {
const rawFeedback =
option.feedbackConfig.type === 'accept'
? acceptFeedback
: rejectFeedback
const trimmedFeedback = rawFeedback.trim()
if (trimmedFeedback) {
feedback = trimmedFeedback;
feedback = trimmedFeedback
}
const analyticsProps_0 = {
toolName: toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
// Log accept/reject submission with feedback context
const analyticsProps = {
toolName:
toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
isMcp: toolAnalyticsContext?.isMcp ?? false,
has_instructions: !!trimmedFeedback,
instructions_length: trimmedFeedback?.length ?? 0,
entered_feedback_mode: option_0.feedbackConfig.type === "accept" ? acceptFeedbackModeEntered : rejectFeedbackModeEntered
};
if (option_0.feedbackConfig.type === "accept") {
logEvent("tengu_accept_submitted", analyticsProps_0);
} else {
if (option_0.feedbackConfig.type === "reject") {
logEvent("tengu_reject_submitted", analyticsProps_0);
}
entered_feedback_mode:
option.feedbackConfig.type === 'accept'
? acceptFeedbackModeEntered
: rejectFeedbackModeEntered,
}
if (option.feedbackConfig.type === 'accept') {
logEvent('tengu_accept_submitted', analyticsProps)
} else if (option.feedbackConfig.type === 'reject') {
logEvent('tengu_reject_submitted', analyticsProps)
}
}
onSelect(value_1, feedback);
};
$[18] = acceptFeedback;
$[19] = acceptFeedbackModeEntered;
$[20] = onSelect;
$[21] = options;
$[22] = rejectFeedback;
$[23] = rejectFeedbackModeEntered;
$[24] = toolAnalyticsContext?.isMcp;
$[25] = toolAnalyticsContext?.toolName;
$[26] = t5;
} else {
t5 = $[26];
}
const handleSelect = t5;
let handlers;
if ($[27] !== handleSelect || $[28] !== options) {
handlers = {};
for (const opt_3 of options) {
if (opt_3.keybinding) {
handlers[opt_3.keybinding] = () => handleSelect(opt_3.value);
onSelect(value, feedback)
},
[
options,
acceptFeedback,
rejectFeedback,
onSelect,
toolAnalyticsContext,
acceptFeedbackModeEntered,
rejectFeedbackModeEntered,
],
)
// Register keybinding handlers for options that have a keybinding set
const keybindingHandlers = useMemo(() => {
const handlers: Record<string, () => void> = {}
for (const opt of options) {
if (opt.keybinding) {
handlers[opt.keybinding] = () => handleSelect(opt.value)
}
}
$[27] = handleSelect;
$[28] = options;
$[29] = handlers;
} else {
handlers = $[29];
}
const keybindingHandlers = handlers;
let t6;
if ($[30] === Symbol.for("react.memo_cache_sentinel")) {
t6 = {
context: "Confirmation"
};
$[30] = t6;
} else {
t6 = $[30];
}
useKeybindings(keybindingHandlers, t6);
let t7;
if ($[31] !== onCancel || $[32] !== setAppState) {
t7 = () => {
logEvent("tengu_permission_request_escape", {});
setAppState(_temp);
onCancel?.();
};
$[31] = onCancel;
$[32] = setAppState;
$[33] = t7;
} else {
t7 = $[33];
}
const handleCancel = t7;
let t8;
if ($[34] !== question) {
t8 = typeof question === "string" ? <Text>{question}</Text> : question;
$[34] = question;
$[35] = t8;
} else {
t8 = $[35];
}
let t9;
if ($[36] !== acceptFeedback || $[37] !== acceptInputMode || $[38] !== options || $[39] !== rejectFeedback || $[40] !== rejectInputMode) {
t9 = value_2 => {
const newOption = options.find(opt_4 => opt_4.value === value_2);
if (newOption?.feedbackConfig?.type !== "accept" && acceptInputMode && !acceptFeedback.trim()) {
setAcceptInputMode(false);
}
if (newOption?.feedbackConfig?.type !== "reject" && rejectInputMode && !rejectFeedback.trim()) {
setRejectInputMode(false);
}
setFocusedValue(value_2);
};
$[36] = acceptFeedback;
$[37] = acceptInputMode;
$[38] = options;
$[39] = rejectFeedback;
$[40] = rejectInputMode;
$[41] = t9;
} else {
t9 = $[41];
}
let t10;
if ($[42] !== handleCancel || $[43] !== handleInputModeToggle || $[44] !== handleSelect || $[45] !== selectOptions || $[46] !== t9) {
t10 = <Select options={selectOptions} inlineDescriptions={true} onChange={handleSelect} onCancel={handleCancel} onFocus={t9} onInputModeToggle={handleInputModeToggle} />;
$[42] = handleCancel;
$[43] = handleInputModeToggle;
$[44] = handleSelect;
$[45] = selectOptions;
$[46] = t9;
$[47] = t10;
} else {
t10 = $[47];
}
const t11 = showTabHint && " \xB7 Tab to amend";
let t12;
if ($[48] !== t11) {
t12 = <Box marginTop={1}><Text dimColor={true}>Esc to cancel{t11}</Text></Box>;
$[48] = t11;
$[49] = t12;
} else {
t12 = $[49];
}
let t13;
if ($[50] !== t10 || $[51] !== t12 || $[52] !== t8) {
t13 = <Box flexDirection="column">{t8}{t10}{t12}</Box>;
$[50] = t10;
$[51] = t12;
$[52] = t8;
$[53] = t13;
} else {
t13 = $[53];
}
return t13;
}
function _temp(prev) {
return {
...prev,
attribution: {
...prev.attribution,
escapeCount: prev.attribution.escapeCount + 1
}
};
return handlers
}, [options, handleSelect])
useKeybindings(keybindingHandlers, { context: 'Confirmation' })
// Handle cancel (Esc)
const handleCancel = useCallback(() => {
logEvent('tengu_permission_request_escape', {})
// Increment escape count for attribution tracking
setAppState(prev => ({
...prev,
attribution: {
...prev.attribution,
escapeCount: prev.attribution.escapeCount + 1,
},
}))
onCancel?.()
}, [onCancel, setAppState])
return (
<Box flexDirection="column">
{typeof question === 'string' ? <Text>{question}</Text> : question}
<Select
options={selectOptions}
inlineDescriptions
onChange={handleSelect}
onCancel={handleCancel}
onFocus={value => {
// Reset input mode when navigating away, but only if no text typed
const newOption = options.find(opt => opt.value === value)
if (
newOption?.feedbackConfig?.type !== 'accept' &&
acceptInputMode &&
!acceptFeedback.trim()
) {
setAcceptInputMode(false)
}
if (
newOption?.feedbackConfig?.type !== 'reject' &&
rejectInputMode &&
!rejectFeedback.trim()
) {
setRejectInputMode(false)
}
setFocusedValue(value)
}}
onInputModeToggle={handleInputModeToggle}
/>
<Box marginTop={1}>
<Text dimColor>Esc to cancel{showTabHint && ' · Tab to amend'}</Text>
</Box>
</Box>
)
}

View File

@@ -1,92 +1,125 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import * as React from 'react';
import { EnterPlanModeTool } from 'src/tools/EnterPlanModeTool/EnterPlanModeTool.js';
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js';
import { useNotifyAfterTimeout } from '../../hooks/useNotifyAfterTimeout.js';
import { useKeybinding } from '../../keybindings/useKeybinding.js';
import type { AnyObject, Tool, ToolUseContext } from '../../Tool.js';
import { AskUserQuestionTool } from '../../tools/AskUserQuestionTool/AskUserQuestionTool.js';
import { BashTool } from '../../tools/BashTool/BashTool.js';
import { FileEditTool } from '../../tools/FileEditTool/FileEditTool.js';
import { FileReadTool } from '../../tools/FileReadTool/FileReadTool.js';
import { FileWriteTool } from '../../tools/FileWriteTool/FileWriteTool.js';
import { GlobTool } from '../../tools/GlobTool/GlobTool.js';
import { GrepTool } from '../../tools/GrepTool/GrepTool.js';
import { NotebookEditTool } from '../../tools/NotebookEditTool/NotebookEditTool.js';
import { PowerShellTool } from '../../tools/PowerShellTool/PowerShellTool.js';
import { SkillTool } from '../../tools/SkillTool/SkillTool.js';
import { WebFetchTool } from '../../tools/WebFetchTool/WebFetchTool.js';
import type { AssistantMessage } from '../../types/message.js';
import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js';
import { AskUserQuestionPermissionRequest } from './AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.js';
import { BashPermissionRequest } from './BashPermissionRequest/BashPermissionRequest.js';
import { EnterPlanModePermissionRequest } from './EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.js';
import { ExitPlanModePermissionRequest } from './ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.js';
import { FallbackPermissionRequest } from './FallbackPermissionRequest.js';
import { FileEditPermissionRequest } from './FileEditPermissionRequest/FileEditPermissionRequest.js';
import { FilesystemPermissionRequest } from './FilesystemPermissionRequest/FilesystemPermissionRequest.js';
import { FileWritePermissionRequest } from './FileWritePermissionRequest/FileWritePermissionRequest.js';
import { NotebookEditPermissionRequest } from './NotebookEditPermissionRequest/NotebookEditPermissionRequest.js';
import { PowerShellPermissionRequest } from './PowerShellPermissionRequest/PowerShellPermissionRequest.js';
import { SkillPermissionRequest } from './SkillPermissionRequest/SkillPermissionRequest.js';
import { WebFetchPermissionRequest } from './WebFetchPermissionRequest/WebFetchPermissionRequest.js';
import { feature } from 'bun:bundle'
import * as React from 'react'
import { EnterPlanModeTool } from 'src/tools/EnterPlanModeTool/EnterPlanModeTool.js'
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
import { useNotifyAfterTimeout } from '../../hooks/useNotifyAfterTimeout.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import type { AnyObject, Tool, ToolUseContext } from '../../Tool.js'
import { AskUserQuestionTool } from '../../tools/AskUserQuestionTool/AskUserQuestionTool.js'
import { BashTool } from '../../tools/BashTool/BashTool.js'
import { FileEditTool } from '../../tools/FileEditTool/FileEditTool.js'
import { FileReadTool } from '../../tools/FileReadTool/FileReadTool.js'
import { FileWriteTool } from '../../tools/FileWriteTool/FileWriteTool.js'
import { GlobTool } from '../../tools/GlobTool/GlobTool.js'
import { GrepTool } from '../../tools/GrepTool/GrepTool.js'
import { NotebookEditTool } from '../../tools/NotebookEditTool/NotebookEditTool.js'
import { PowerShellTool } from '../../tools/PowerShellTool/PowerShellTool.js'
import { SkillTool } from '../../tools/SkillTool/SkillTool.js'
import { WebFetchTool } from '../../tools/WebFetchTool/WebFetchTool.js'
import type { AssistantMessage } from '../../types/message.js'
import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'
import { AskUserQuestionPermissionRequest } from './AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.js'
import { BashPermissionRequest } from './BashPermissionRequest/BashPermissionRequest.js'
import { EnterPlanModePermissionRequest } from './EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.js'
import { ExitPlanModePermissionRequest } from './ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.js'
import { FallbackPermissionRequest } from './FallbackPermissionRequest.js'
import { FileEditPermissionRequest } from './FileEditPermissionRequest/FileEditPermissionRequest.js'
import { FilesystemPermissionRequest } from './FilesystemPermissionRequest/FilesystemPermissionRequest.js'
import { FileWritePermissionRequest } from './FileWritePermissionRequest/FileWritePermissionRequest.js'
import { NotebookEditPermissionRequest } from './NotebookEditPermissionRequest/NotebookEditPermissionRequest.js'
import { PowerShellPermissionRequest } from './PowerShellPermissionRequest/PowerShellPermissionRequest.js'
import { SkillPermissionRequest } from './SkillPermissionRequest/SkillPermissionRequest.js'
import { WebFetchPermissionRequest } from './WebFetchPermissionRequest/WebFetchPermissionRequest.js'
/* eslint-disable @typescript-eslint/no-require-imports */
const ReviewArtifactTool = feature('REVIEW_ARTIFACT') ? (require('../../tools/ReviewArtifactTool/ReviewArtifactTool.js') as typeof import('../../tools/ReviewArtifactTool/ReviewArtifactTool.js')).ReviewArtifactTool : null;
const ReviewArtifactPermissionRequest = feature('REVIEW_ARTIFACT') ? (require('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js') as typeof import('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js')).ReviewArtifactPermissionRequest : null;
const WorkflowTool = feature('WORKFLOW_SCRIPTS') ? (require('../../tools/WorkflowTool/WorkflowTool.js') as typeof import('../../tools/WorkflowTool/WorkflowTool.js')).WorkflowTool : null;
const WorkflowPermissionRequest = feature('WORKFLOW_SCRIPTS') ? (require('../../tools/WorkflowTool/WorkflowPermissionRequest.js') as typeof import('../../tools/WorkflowTool/WorkflowPermissionRequest.js')).WorkflowPermissionRequest : null;
const MonitorTool = feature('MONITOR_TOOL') ? (require('../../tools/MonitorTool/MonitorTool.js') as typeof import('../../tools/MonitorTool/MonitorTool.js')).MonitorTool : null;
const MonitorPermissionRequest = feature('MONITOR_TOOL') ? (require('./MonitorPermissionRequest/MonitorPermissionRequest.js') as typeof import('./MonitorPermissionRequest/MonitorPermissionRequest.js')).MonitorPermissionRequest : null;
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs';
const ReviewArtifactTool = feature('REVIEW_ARTIFACT')
? (
require('../../tools/ReviewArtifactTool/ReviewArtifactTool.js') as typeof import('../../tools/ReviewArtifactTool/ReviewArtifactTool.js')
).ReviewArtifactTool
: null
const ReviewArtifactPermissionRequest = feature('REVIEW_ARTIFACT')
? (
require('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js') as typeof import('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js')
).ReviewArtifactPermissionRequest
: null
const WorkflowTool = feature('WORKFLOW_SCRIPTS')
? (
require('../../tools/WorkflowTool/WorkflowTool.js') as typeof import('../../tools/WorkflowTool/WorkflowTool.js')
).WorkflowTool
: null
const WorkflowPermissionRequest = feature('WORKFLOW_SCRIPTS')
? (
require('../../tools/WorkflowTool/WorkflowPermissionRequest.js') as typeof import('../../tools/WorkflowTool/WorkflowPermissionRequest.js')
).WorkflowPermissionRequest
: null
const MonitorTool = feature('MONITOR_TOOL')
? (
require('../../tools/MonitorTool/MonitorTool.js') as typeof import('../../tools/MonitorTool/MonitorTool.js')
).MonitorTool
: null
const MonitorPermissionRequest = feature('MONITOR_TOOL')
? (
require('./MonitorPermissionRequest/MonitorPermissionRequest.js') as typeof import('./MonitorPermissionRequest/MonitorPermissionRequest.js')
).MonitorPermissionRequest
: null
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
/* eslint-enable @typescript-eslint/no-require-imports */
import type { z } from 'zod/v4';
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js';
import type { WorkerBadgeProps } from './WorkerBadge.js';
function permissionComponentForTool(tool: Tool): React.ComponentType<PermissionRequestProps> {
import type { z } from 'zod/v4'
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
import type { WorkerBadgeProps } from './WorkerBadge.js'
function permissionComponentForTool(
tool: Tool,
): React.ComponentType<PermissionRequestProps> {
switch (tool) {
case FileEditTool:
return FileEditPermissionRequest;
return FileEditPermissionRequest
case FileWriteTool:
return FileWritePermissionRequest;
return FileWritePermissionRequest
case BashTool:
return BashPermissionRequest;
return BashPermissionRequest
case PowerShellTool:
return PowerShellPermissionRequest;
return PowerShellPermissionRequest
case ReviewArtifactTool:
return ReviewArtifactPermissionRequest ?? FallbackPermissionRequest;
return ReviewArtifactPermissionRequest ?? FallbackPermissionRequest
case WebFetchTool:
return WebFetchPermissionRequest;
return WebFetchPermissionRequest
case NotebookEditTool:
return NotebookEditPermissionRequest;
return NotebookEditPermissionRequest
case ExitPlanModeV2Tool:
return ExitPlanModePermissionRequest;
return ExitPlanModePermissionRequest
case EnterPlanModeTool:
return EnterPlanModePermissionRequest;
return EnterPlanModePermissionRequest
case SkillTool:
return SkillPermissionRequest;
return SkillPermissionRequest
case AskUserQuestionTool:
return AskUserQuestionPermissionRequest;
return AskUserQuestionPermissionRequest
case WorkflowTool:
return WorkflowPermissionRequest ?? FallbackPermissionRequest;
return WorkflowPermissionRequest ?? FallbackPermissionRequest
case MonitorTool:
return MonitorPermissionRequest ?? FallbackPermissionRequest;
return MonitorPermissionRequest ?? FallbackPermissionRequest
case GlobTool:
case GrepTool:
case FileReadTool:
return FilesystemPermissionRequest;
return FilesystemPermissionRequest
default:
return FallbackPermissionRequest;
return FallbackPermissionRequest
}
}
export type PermissionRequestProps<Input extends AnyObject = AnyObject> = {
toolUseConfirm: ToolUseConfirm<Input>;
toolUseContext: ToolUseContext;
onDone(): void;
onReject(): void;
verbose: boolean;
workerBadge: WorkerBadgeProps | undefined;
toolUseConfirm: ToolUseConfirm<Input>
toolUseContext: ToolUseContext
onDone(): void
onReject(): void
verbose: boolean
workerBadge: WorkerBadgeProps | undefined
/**
* Register JSX to render in a sticky footer below the scrollable area.
* Fullscreen mode only (non-fullscreen has no sticky area — terminal
@@ -98,119 +131,102 @@ export type PermissionRequestProps<Input extends AnyObject = AnyObject> = {
* to avoid stale closures (React reconciles the JSX, preserving Select's
* internal focus/input state).
*/
setStickyFooter?: (jsx: React.ReactNode | null) => void;
};
setStickyFooter?: (jsx: React.ReactNode | null) => void
}
export type ToolUseConfirm<Input extends AnyObject = AnyObject> = {
assistantMessage: AssistantMessage;
tool: Tool<Input>;
description: string;
input: z.infer<Input>;
toolUseContext: ToolUseContext;
toolUseID: string;
permissionResult: PermissionDecision;
permissionPromptStartTimeMs: number;
assistantMessage: AssistantMessage
tool: Tool<Input>
description: string
input: z.infer<Input>
toolUseContext: ToolUseContext
toolUseID: string
permissionResult: PermissionDecision
permissionPromptStartTimeMs: number
/**
* Called when user interacts with the permission dialog (e.g., arrow keys, tab, typing).
* This prevents async auto-approval mechanisms (like the bash classifier) from
* dismissing the dialog while the user is actively engaging with it.
*/
classifierCheckInProgress?: boolean;
classifierAutoApproved?: boolean;
classifierMatchedRule?: string;
workerBadge?: WorkerBadgeProps;
onUserInteraction(): void;
onAbort(): void;
onDismissCheckmark?(): void;
onAllow(updatedInput: z.infer<Input>, permissionUpdates: PermissionUpdate[], feedback?: string, contentBlocks?: ContentBlockParam[]): void;
onReject(feedback?: string, contentBlocks?: ContentBlockParam[]): void;
recheckPermission(): Promise<void>;
};
classifierCheckInProgress?: boolean
classifierAutoApproved?: boolean
classifierMatchedRule?: string
workerBadge?: WorkerBadgeProps
onUserInteraction(): void
onAbort(): void
onDismissCheckmark?(): void
onAllow(
updatedInput: z.infer<Input>,
permissionUpdates: PermissionUpdate[],
feedback?: string,
contentBlocks?: ContentBlockParam[],
): void
onReject(feedback?: string, contentBlocks?: ContentBlockParam[]): void
recheckPermission(): Promise<void>
}
function getNotificationMessage(toolUseConfirm: ToolUseConfirm): string {
const toolName = toolUseConfirm.tool.userFacingName(toolUseConfirm.input as never);
const toolName = toolUseConfirm.tool.userFacingName(
toolUseConfirm.input as never,
)
if (toolUseConfirm.tool === ExitPlanModeV2Tool) {
return 'Claude Code needs your approval for the plan';
return 'Claude Code needs your approval for the plan'
}
if (toolUseConfirm.tool === EnterPlanModeTool) {
return 'Claude Code wants to enter plan mode';
return 'Claude Code wants to enter plan mode'
}
if (feature('REVIEW_ARTIFACT') && toolUseConfirm.tool === ReviewArtifactTool) {
return 'Claude needs your approval for a review artifact';
if (
feature('REVIEW_ARTIFACT') &&
toolUseConfirm.tool === ReviewArtifactTool
) {
return 'Claude needs your approval for a review artifact'
}
if (!toolName || toolName.trim() === '') {
return 'Claude Code needs your attention';
return 'Claude Code needs your attention'
}
return `Claude needs your permission to use ${toolName}`;
return `Claude needs your permission to use ${toolName}`
}
// TODO: Move this to Tool.renderPermissionRequest
export function PermissionRequest(t0) {
const $ = _c(18);
const {
toolUseConfirm,
toolUseContext,
onDone,
onReject,
verbose,
workerBadge,
setStickyFooter
} = t0;
let t1;
if ($[0] !== onDone || $[1] !== onReject || $[2] !== toolUseConfirm) {
t1 = () => {
onDone();
onReject();
toolUseConfirm.onReject();
};
$[0] = onDone;
$[1] = onReject;
$[2] = toolUseConfirm;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t2 = {
context: "Confirmation"
};
$[4] = t2;
} else {
t2 = $[4];
}
useKeybinding("app:interrupt", t1, t2);
let t3;
if ($[5] !== toolUseConfirm) {
t3 = getNotificationMessage(toolUseConfirm);
$[5] = toolUseConfirm;
$[6] = t3;
} else {
t3 = $[6];
}
const notificationMessage = t3;
useNotifyAfterTimeout(notificationMessage, "permission_prompt");
let t4;
if ($[7] !== toolUseConfirm.tool) {
t4 = permissionComponentForTool(toolUseConfirm.tool);
$[7] = toolUseConfirm.tool;
$[8] = t4;
} else {
t4 = $[8];
}
const PermissionComponent = t4;
let t5;
if ($[9] !== PermissionComponent || $[10] !== onDone || $[11] !== onReject || $[12] !== setStickyFooter || $[13] !== toolUseConfirm || $[14] !== toolUseContext || $[15] !== verbose || $[16] !== workerBadge) {
t5 = <PermissionComponent toolUseContext={toolUseContext} toolUseConfirm={toolUseConfirm} onDone={onDone} onReject={onReject} verbose={verbose} workerBadge={workerBadge} setStickyFooter={setStickyFooter} />;
$[9] = PermissionComponent;
$[10] = onDone;
$[11] = onReject;
$[12] = setStickyFooter;
$[13] = toolUseConfirm;
$[14] = toolUseContext;
$[15] = verbose;
$[16] = workerBadge;
$[17] = t5;
} else {
t5 = $[17];
}
return t5;
export function PermissionRequest({
toolUseConfirm,
toolUseContext,
onDone,
onReject,
verbose,
workerBadge,
setStickyFooter,
}: PermissionRequestProps): React.ReactNode {
// Handle Ctrl+C (app:interrupt) to reject
useKeybinding(
'app:interrupt',
() => {
onDone()
onReject()
toolUseConfirm.onReject()
},
{ context: 'Confirmation' },
)
const notificationMessage = getNotificationMessage(toolUseConfirm)
useNotifyAfterTimeout(notificationMessage, 'permission_prompt')
const PermissionComponent = permissionComponentForTool(toolUseConfirm.tool)
return (
<PermissionComponent
toolUseContext={toolUseContext}
toolUseConfirm={toolUseConfirm}
onDone={onDone}
onReject={onReject}
verbose={verbose}
workerBadge={workerBadge}
setStickyFooter={setStickyFooter}
/>
)
}

View File

@@ -1,65 +1,41 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { Box, Text } from '../../ink.js';
import type { Theme } from '../../utils/theme.js';
import type { WorkerBadgeProps } from './WorkerBadge.js';
import * as React from 'react'
import { Box, Text } from '../../ink.js'
import type { Theme } from '../../utils/theme.js'
import type { WorkerBadgeProps } from './WorkerBadge.js'
type Props = {
title: string;
subtitle?: React.ReactNode;
color?: keyof Theme;
workerBadge?: WorkerBadgeProps;
};
export function PermissionRequestTitle(t0) {
const $ = _c(13);
const {
title,
subtitle,
color: t1,
workerBadge
} = t0;
const color = t1 === undefined ? "permission" : t1;
let t2;
if ($[0] !== color || $[1] !== title) {
t2 = <Text bold={true} color={color}>{title}</Text>;
$[0] = color;
$[1] = title;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] !== workerBadge) {
t3 = workerBadge && <Text dimColor={true}>{"\xB7 "}@{workerBadge.name}</Text>;
$[3] = workerBadge;
$[4] = t3;
} else {
t3 = $[4];
}
let t4;
if ($[5] !== t2 || $[6] !== t3) {
t4 = <Box flexDirection="row" gap={1}>{t2}{t3}</Box>;
$[5] = t2;
$[6] = t3;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== subtitle) {
t5 = subtitle != null && (typeof subtitle === "string" ? <Text dimColor={true} wrap="truncate-start">{subtitle}</Text> : subtitle);
$[8] = subtitle;
$[9] = t5;
} else {
t5 = $[9];
}
let t6;
if ($[10] !== t4 || $[11] !== t5) {
t6 = <Box flexDirection="column">{t4}{t5}</Box>;
$[10] = t4;
$[11] = t5;
$[12] = t6;
} else {
t6 = $[12];
}
return t6;
title: string
subtitle?: React.ReactNode
color?: keyof Theme
workerBadge?: WorkerBadgeProps
}
export function PermissionRequestTitle({
title,
subtitle,
color = 'permission',
workerBadge,
}: Props): React.ReactNode {
return (
<Box flexDirection="column">
<Box flexDirection="row" gap={1}>
<Text bold color={color}>
{title}
</Text>
{workerBadge && (
<Text dimColor>
{'· '}@{workerBadge.name}
</Text>
)}
</Box>
{subtitle != null &&
(typeof subtitle === 'string' ? (
<Text dimColor wrap="truncate-start">
{subtitle}
</Text>
) : (
subtitle
))}
</Box>
)
}

View File

@@ -1,120 +1,118 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import chalk from 'chalk';
import React from 'react';
import { Ansi, Box, Text } from '../../ink.js';
import { useAppState } from '../../state/AppState.js';
import type { PermissionDecision, PermissionDecisionReason } from '../../utils/permissions/PermissionResult.js';
import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js';
import type { Theme } from '../../utils/theme.js';
import ThemedText from '../design-system/ThemedText.js';
import { feature } from 'bun:bundle'
import chalk from 'chalk'
import React from 'react'
import { Ansi, Box, Text } from '../../ink.js'
import { useAppState } from '../../state/AppState.js'
import type {
PermissionDecision,
PermissionDecisionReason,
} from '../../utils/permissions/PermissionResult.js'
import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js'
import type { Theme } from '../../utils/theme.js'
import ThemedText from '../design-system/ThemedText.js'
export type PermissionRuleExplanationProps = {
permissionResult: PermissionDecision;
toolType: 'tool' | 'command' | 'edit' | 'read';
};
permissionResult: PermissionDecision
toolType: 'tool' | 'command' | 'edit' | 'read'
}
type DecisionReasonStrings = {
reasonString: string;
configString?: string;
reasonString: string
configString?: string
/** When set, reasonString is plain text rendered with this theme color instead of <Ansi>. */
themeColor?: keyof Theme;
};
function stringsForDecisionReason(reason: PermissionDecisionReason | undefined, toolType: 'tool' | 'command' | 'edit' | 'read'): DecisionReasonStrings | null {
themeColor?: keyof Theme
}
function stringsForDecisionReason(
reason: PermissionDecisionReason | undefined,
toolType: 'tool' | 'command' | 'edit' | 'read',
): DecisionReasonStrings | null {
if (!reason) {
return null;
return null
}
if ((feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && reason.type === 'classifier') {
if (
(feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) &&
reason.type === 'classifier'
) {
if (reason.classifier === 'auto-mode') {
return {
reasonString: `Auto mode classifier requires confirmation for this ${toolType}.\n${reason.reason}`,
configString: undefined,
themeColor: 'error'
};
themeColor: 'error',
}
}
return {
reasonString: `Classifier ${chalk.bold(reason.classifier)} requires confirmation for this ${toolType}.\n${reason.reason}`,
configString: undefined
};
configString: undefined,
}
}
switch (reason.type) {
case 'rule':
return {
reasonString: `Permission rule ${chalk.bold(permissionRuleValueToString(reason.rule.ruleValue))} requires confirmation for this ${toolType}.`,
configString: reason.rule.source === 'policySettings' ? undefined : '/permissions to update rules'
};
case 'hook':
{
const hookReasonString = reason.reason ? `:\n${reason.reason}` : '.';
const sourceLabel = reason.hookSource ? ` ${chalk.dim(`[${reason.hookSource}]`)}` : '';
return {
reasonString: `Hook ${chalk.bold(reason.hookName)} requires confirmation for this ${toolType}${hookReasonString}${sourceLabel}`,
configString: '/hooks to update'
};
reasonString: `Permission rule ${chalk.bold(
permissionRuleValueToString(reason.rule.ruleValue),
)} requires confirmation for this ${toolType}.`,
configString:
reason.rule.source === 'policySettings'
? undefined
: '/permissions to update rules',
}
case 'hook': {
const hookReasonString = reason.reason ? `:\n${reason.reason}` : '.'
const sourceLabel = reason.hookSource
? ` ${chalk.dim(`[${reason.hookSource}]`)}`
: ''
return {
reasonString: `Hook ${chalk.bold(reason.hookName)} requires confirmation for this ${toolType}${hookReasonString}${sourceLabel}`,
configString: '/hooks to update',
}
}
case 'safetyCheck':
case 'other':
return {
reasonString: reason.reason,
configString: undefined
};
configString: undefined,
}
case 'workingDir':
return {
reasonString: reason.reason,
configString: '/permissions to update rules'
};
configString: '/permissions to update rules',
}
default:
return null;
return null
}
}
export function PermissionRuleExplanation(t0) {
const $ = _c(11);
const {
permissionResult,
toolType
} = t0;
const permissionMode = useAppState(_temp);
const t1 = permissionResult?.decisionReason;
let t2;
if ($[0] !== t1 || $[1] !== toolType) {
t2 = stringsForDecisionReason(t1, toolType);
$[0] = t1;
$[1] = toolType;
$[2] = t2;
} else {
t2 = $[2];
}
const strings = t2;
export function PermissionRuleExplanation({
permissionResult,
toolType,
}: PermissionRuleExplanationProps): React.ReactNode {
const permissionMode = useAppState(s => s.toolPermissionContext.mode)
const strings = stringsForDecisionReason(
permissionResult?.decisionReason,
toolType,
)
if (!strings) {
return null;
return null
}
const themeColor = strings.themeColor ?? (permissionResult?.decisionReason?.type === "hook" && permissionMode === "auto" ? "warning" : undefined);
let t3;
if ($[3] !== strings.reasonString || $[4] !== themeColor) {
t3 = themeColor ? <ThemedText color={themeColor}>{strings.reasonString}</ThemedText> : <Text><Ansi>{strings.reasonString}</Ansi></Text>;
$[3] = strings.reasonString;
$[4] = themeColor;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] !== strings.configString) {
t4 = strings.configString && <Text dimColor={true}>{strings.configString}</Text>;
$[6] = strings.configString;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== t3 || $[9] !== t4) {
t5 = <Box marginBottom={1} flexDirection="column">{t3}{t4}</Box>;
$[8] = t3;
$[9] = t4;
$[10] = t5;
} else {
t5 = $[10];
}
return t5;
}
function _temp(s) {
return s.toolPermissionContext.mode;
const themeColor =
strings.themeColor ??
(permissionResult?.decisionReason?.type === 'hook' &&
permissionMode === 'auto'
? 'warning'
: undefined)
return (
<Box marginBottom={1} flexDirection="column">
{themeColor ? (
<ThemedText color={themeColor}>{strings.reasonString}</ThemedText>
) : (
<Text>
<Ansi>{strings.reasonString}</Ansi>
</Text>
)}
{strings.configString && <Text dimColor>{strings.configString}</Text>}
</Box>
)
}

View File

@@ -1,43 +1,48 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Text, useTheme } from '../../../ink.js';
import { useKeybinding } from '../../../keybindings/useKeybinding.js';
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../../services/analytics/index.js';
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js';
import { getDestructiveCommandWarning } from '../../../tools/PowerShellTool/destructiveCommandWarning.js';
import { PowerShellTool } from '../../../tools/PowerShellTool/PowerShellTool.js';
import { isAllowlistedCommand } from '../../../tools/PowerShellTool/readOnlyValidation.js';
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js';
import { getCompoundCommandPrefixesStatic } from '../../../utils/powershell/staticPrefix.js';
import { Select } from '../../CustomSelect/select.js';
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js';
import { PermissionDecisionDebugInfo } from '../PermissionDecisionDebugInfo.js';
import { PermissionDialog } from '../PermissionDialog.js';
import { PermissionExplainerContent, usePermissionExplainerUI } from '../PermissionExplanation.js';
import type { PermissionRequestProps } from '../PermissionRequest.js';
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js';
import { useShellPermissionFeedback } from '../useShellPermissionFeedback.js';
import { logUnaryPermissionEvent } from '../utils.js';
import { powershellToolUseOptions } from './powershellToolUseOptions.js';
export function PowerShellPermissionRequest(props: PermissionRequestProps): React.ReactNode {
const {
toolUseConfirm,
toolUseContext,
onDone,
onReject,
workerBadge
} = props;
const {
command,
description
} = PowerShellTool.inputSchema.parse(toolUseConfirm.input);
const [theme] = useTheme();
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Box, Text, useTheme } from '../../../ink.js'
import { useKeybinding } from '../../../keybindings/useKeybinding.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../../services/analytics/index.js'
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'
import { getDestructiveCommandWarning } from '../../../tools/PowerShellTool/destructiveCommandWarning.js'
import { PowerShellTool } from '../../../tools/PowerShellTool/PowerShellTool.js'
import { isAllowlistedCommand } from '../../../tools/PowerShellTool/readOnlyValidation.js'
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
import { getCompoundCommandPrefixesStatic } from '../../../utils/powershell/staticPrefix.js'
import { Select } from '../../CustomSelect/select.js'
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'
import { PermissionDecisionDebugInfo } from '../PermissionDecisionDebugInfo.js'
import { PermissionDialog } from '../PermissionDialog.js'
import {
PermissionExplainerContent,
usePermissionExplainerUI,
} from '../PermissionExplanation.js'
import type { PermissionRequestProps } from '../PermissionRequest.js'
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
import { useShellPermissionFeedback } from '../useShellPermissionFeedback.js'
import { logUnaryPermissionEvent } from '../utils.js'
import { powershellToolUseOptions } from './powershellToolUseOptions.js'
export function PowerShellPermissionRequest(
props: PermissionRequestProps,
): React.ReactNode {
const { toolUseConfirm, toolUseContext, onDone, onReject, workerBadge } =
props
const { command, description } = PowerShellTool.inputSchema.parse(
toolUseConfirm.input,
)
const [theme] = useTheme()
const explainerState = usePermissionExplainerUI({
toolName: toolUseConfirm.tool.name,
toolInput: toolUseConfirm.input,
toolDescription: toolUseConfirm.description,
messages: toolUseContext.messages
});
messages: toolUseContext.messages,
})
const {
yesInputMode,
noInputMode,
@@ -50,15 +55,21 @@ export function PowerShellPermissionRequest(props: PermissionRequestProps): Reac
focusedOption,
handleInputModeToggle,
handleReject,
handleFocus
handleFocus,
} = useShellPermissionFeedback({
toolUseConfirm,
onDone,
onReject,
explainerVisible: explainerState.visible
});
const destructiveWarning = getFeatureValue_CACHED_MAY_BE_STALE('tengu_destructive_command_warning', false) ? getDestructiveCommandWarning(command) : null;
const [showPermissionDebug, setShowPermissionDebug] = useState(false);
explainerVisible: explainerState.visible,
})
const destructiveWarning = getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_destructive_command_warning',
false,
)
? getDestructiveCommandWarning(command)
: null
const [showPermissionDebug, setShowPermissionDebug] = useState(false)
// Editable prefix — compute static prefix locally (no LLM call).
// Initialize synchronously to the raw command for single-line commands so
@@ -69,166 +80,233 @@ export function PowerShellPermissionRequest(props: PermissionRequestProps): Reac
// corpus shows 14 multiline rules, zero match twice). For compound commands,
// computes a prefix per subcommand, excluding subcommands that are already
// auto-allowed (read-only).
const [editablePrefix, setEditablePrefix] = useState<string | undefined>(command.includes('\n') ? undefined : command);
const hasUserEditedPrefix = useRef(false);
const [editablePrefix, setEditablePrefix] = useState<string | undefined>(
command.includes('\n') ? undefined : command,
)
const hasUserEditedPrefix = useRef(false)
useEffect(() => {
let cancelled = false;
let cancelled = false
// Filter receives ParsedCommandElement — isAllowlistedCommand works from
// element.name/nameType/args directly. isReadOnlyCommand(text) would need
// to reparse (pwsh.exe spawn per subcommand) and returns false without the
// full parsed AST, making the filter a no-op.
getCompoundCommandPrefixesStatic(command, element => isAllowlistedCommand(element, element.text)).then(prefixes => {
if (cancelled || hasUserEditedPrefix.current) return;
if (prefixes.length > 0) {
setEditablePrefix(`${prefixes[0]}:*`);
}
}).catch(() => {});
getCompoundCommandPrefixesStatic(command, element =>
isAllowlistedCommand(element, element.text),
)
.then(prefixes => {
if (cancelled || hasUserEditedPrefix.current) return
if (prefixes.length > 0) {
setEditablePrefix(`${prefixes[0]}:*`)
}
})
.catch(() => {})
return () => {
cancelled = true;
};
cancelled = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [command]);
}, [command])
const onEditablePrefixChange = useCallback((value: string) => {
hasUserEditedPrefix.current = true;
setEditablePrefix(value);
}, []);
const unaryEvent = useMemo<UnaryEvent>(() => ({
completion_type: 'tool_use_single',
language_name: 'none'
}), []);
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
const options = useMemo(() => powershellToolUseOptions({
suggestions: toolUseConfirm.permissionResult.behavior === 'ask' ? toolUseConfirm.permissionResult.suggestions : undefined,
onRejectFeedbackChange: setRejectFeedback,
onAcceptFeedbackChange: setAcceptFeedback,
yesInputMode,
noInputMode,
editablePrefix,
onEditablePrefixChange
}), [toolUseConfirm, yesInputMode, noInputMode, editablePrefix, onEditablePrefixChange]);
hasUserEditedPrefix.current = true
setEditablePrefix(value)
}, [])
const unaryEvent = useMemo<UnaryEvent>(
() => ({ completion_type: 'tool_use_single', language_name: 'none' }),
[],
)
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
const options = useMemo(
() =>
powershellToolUseOptions({
suggestions:
toolUseConfirm.permissionResult.behavior === 'ask'
? toolUseConfirm.permissionResult.suggestions
: undefined,
onRejectFeedbackChange: setRejectFeedback,
onAcceptFeedbackChange: setAcceptFeedback,
yesInputMode,
noInputMode,
editablePrefix,
onEditablePrefixChange,
}),
[
toolUseConfirm,
yesInputMode,
noInputMode,
editablePrefix,
onEditablePrefixChange,
],
)
// Toggle permission debug info with keybinding
const handleToggleDebug = useCallback(() => {
setShowPermissionDebug(prev => !prev);
}, []);
setShowPermissionDebug(prev => !prev)
}, [])
useKeybinding('permission:toggleDebug', handleToggleDebug, {
context: 'Confirmation'
});
context: 'Confirmation',
})
function onSelect(value: string) {
// Map options to numeric values for analytics (strings not allowed in logEvent)
const optionIndex: Record<string, number> = {
yes: 1,
'yes-apply-suggestions': 2,
'yes-prefix-edited': 2,
no: 3
};
no: 3,
}
logEvent('tengu_permission_request_option_selected', {
option_index: optionIndex[value],
explainer_visible: explainerState.visible
});
const toolNameForAnalytics = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS;
explainer_visible: explainerState.visible,
})
const toolNameForAnalytics = sanitizeToolNameForAnalytics(
toolUseConfirm.tool.name,
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
if (value === 'yes-prefix-edited') {
const trimmedPrefix = (editablePrefix ?? '').trim();
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
const trimmedPrefix = (editablePrefix ?? '').trim()
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
if (!trimmedPrefix) {
toolUseConfirm.onAllow(toolUseConfirm.input, []);
toolUseConfirm.onAllow(toolUseConfirm.input, [])
} else {
const prefixUpdates: PermissionUpdate[] = [{
type: 'addRules',
rules: [{
toolName: PowerShellTool.name,
ruleContent: trimmedPrefix
}],
behavior: 'allow',
destination: 'localSettings'
}];
toolUseConfirm.onAllow(toolUseConfirm.input, prefixUpdates);
const prefixUpdates: PermissionUpdate[] = [
{
type: 'addRules',
rules: [
{
toolName: PowerShellTool.name,
ruleContent: trimmedPrefix,
},
],
behavior: 'allow',
destination: 'localSettings',
},
]
toolUseConfirm.onAllow(toolUseConfirm.input, prefixUpdates)
}
onDone();
return;
onDone()
return
}
switch (value) {
case 'yes':
{
const trimmedFeedback = acceptFeedback.trim();
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
// Log accept submission with feedback context
logEvent('tengu_accept_submitted', {
toolName: toolNameForAnalytics,
isMcp: toolUseConfirm.tool.isMcp ?? false,
has_instructions: !!trimmedFeedback,
instructions_length: trimmedFeedback.length,
entered_feedback_mode: yesFeedbackModeEntered
});
toolUseConfirm.onAllow(toolUseConfirm.input, [], trimmedFeedback || undefined);
onDone();
break;
}
case 'yes-apply-suggestions':
{
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept');
// Extract suggestions if present (works for both 'ask' and 'passthrough' behaviors)
const permissionUpdates = 'suggestions' in toolUseConfirm.permissionResult ? toolUseConfirm.permissionResult.suggestions || [] : [];
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates);
onDone();
break;
}
case 'no':
{
const trimmedFeedback = rejectFeedback.trim();
case 'yes': {
const trimmedFeedback = acceptFeedback.trim()
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
// Log accept submission with feedback context
logEvent('tengu_accept_submitted', {
toolName: toolNameForAnalytics,
isMcp: toolUseConfirm.tool.isMcp ?? false,
has_instructions: !!trimmedFeedback,
instructions_length: trimmedFeedback.length,
entered_feedback_mode: yesFeedbackModeEntered,
})
toolUseConfirm.onAllow(
toolUseConfirm.input,
[],
trimmedFeedback || undefined,
)
onDone()
break
}
case 'yes-apply-suggestions': {
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
// Extract suggestions if present (works for both 'ask' and 'passthrough' behaviors)
const permissionUpdates =
'suggestions' in toolUseConfirm.permissionResult
? toolUseConfirm.permissionResult.suggestions || []
: []
toolUseConfirm.onAllow(toolUseConfirm.input, permissionUpdates)
onDone()
break
}
case 'no': {
const trimmedFeedback = rejectFeedback.trim()
// Log reject submission with feedback context
logEvent('tengu_reject_submitted', {
toolName: toolNameForAnalytics,
isMcp: toolUseConfirm.tool.isMcp ?? false,
has_instructions: !!trimmedFeedback,
instructions_length: trimmedFeedback.length,
entered_feedback_mode: noFeedbackModeEntered
});
// Log reject submission with feedback context
logEvent('tengu_reject_submitted', {
toolName: toolNameForAnalytics,
isMcp: toolUseConfirm.tool.isMcp ?? false,
has_instructions: !!trimmedFeedback,
instructions_length: trimmedFeedback.length,
entered_feedback_mode: noFeedbackModeEntered,
})
// Process rejection (with or without feedback)
handleReject(trimmedFeedback || undefined);
break;
}
// Process rejection (with or without feedback)
handleReject(trimmedFeedback || undefined)
break
}
}
}
return <PermissionDialog workerBadge={workerBadge} title="PowerShell command">
return (
<PermissionDialog workerBadge={workerBadge} title="PowerShell command">
<Box flexDirection="column" paddingX={2} paddingY={1}>
<Text dimColor={explainerState.visible}>
{PowerShellTool.renderToolUseMessage({
command,
description
}, {
theme,
verbose: true
} // always show the full command
)}
{PowerShellTool.renderToolUseMessage(
{ command, description },
{ theme, verbose: true }, // always show the full command
)}
</Text>
{!explainerState.visible && <Text dimColor>{toolUseConfirm.description}</Text>}
<PermissionExplainerContent visible={explainerState.visible} promise={explainerState.promise} />
{!explainerState.visible && (
<Text dimColor>{toolUseConfirm.description}</Text>
)}
<PermissionExplainerContent
visible={explainerState.visible}
promise={explainerState.promise}
/>
</Box>
{showPermissionDebug ? <>
<PermissionDecisionDebugInfo permissionResult={toolUseConfirm.permissionResult} toolName="PowerShell" />
{toolUseContext.options.debug && <Box justifyContent="flex-end" marginTop={1}>
{showPermissionDebug ? (
<>
<PermissionDecisionDebugInfo
permissionResult={toolUseConfirm.permissionResult}
toolName="PowerShell"
/>
{toolUseContext.options.debug && (
<Box justifyContent="flex-end" marginTop={1}>
<Text dimColor>Ctrl-D to hide debug info</Text>
</Box>}
</> : <>
</Box>
)}
</>
) : (
<>
<Box flexDirection="column">
<PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="command" />
{destructiveWarning && <Box marginBottom={1}>
<PermissionRuleExplanation
permissionResult={toolUseConfirm.permissionResult}
toolType="command"
/>
{destructiveWarning && (
<Box marginBottom={1}>
<Text color="warning">{destructiveWarning}</Text>
</Box>}
</Box>
)}
<Text>Do you want to proceed?</Text>
<Select options={options} inlineDescriptions onChange={onSelect} onCancel={() => handleReject()} onFocus={handleFocus} onInputModeToggle={handleInputModeToggle} />
<Select
options={options}
inlineDescriptions
onChange={onSelect}
onCancel={() => handleReject()}
onFocus={handleFocus}
onInputModeToggle={handleInputModeToggle}
/>
</Box>
<Box justifyContent="space-between" marginTop={1}>
<Text dimColor>
Esc to cancel
{(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'}
{explainerState.enabled && ` · ctrl+e to ${explainerState.visible ? 'hide' : 'explain'}`}
{((focusedOption === 'yes' && !yesInputMode) ||
(focusedOption === 'no' && !noInputMode)) &&
' · Tab to amend'}
{explainerState.enabled &&
` · ctrl+e to ${explainerState.visible ? 'hide' : 'explain'}`}
</Text>
{toolUseContext.options.debug && <Text dimColor>Ctrl+d to show debug info</Text>}
{toolUseContext.options.debug && (
<Text dimColor>Ctrl+d to show debug info</Text>
)}
</Box>
</>}
</PermissionDialog>;
</>
)}
</PermissionDialog>
)
}

View File

@@ -1,9 +1,15 @@
import { POWERSHELL_TOOL_NAME } from '../../../tools/PowerShellTool/toolName.js';
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js';
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js';
import type { OptionWithDescription } from '../../CustomSelect/select.js';
import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js';
export type PowerShellToolUseOption = 'yes' | 'yes-apply-suggestions' | 'yes-prefix-edited' | 'no';
import { POWERSHELL_TOOL_NAME } from '../../../tools/PowerShellTool/toolName.js'
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'
import type { OptionWithDescription } from '../../CustomSelect/select.js'
import { generateShellSuggestionsLabel } from '../shellPermissionHelpers.js'
export type PowerShellToolUseOption =
| 'yes'
| 'yes-apply-suggestions'
| 'yes-prefix-edited'
| 'no'
export function powershellToolUseOptions({
suggestions = [],
onRejectFeedbackChange,
@@ -11,17 +17,18 @@ export function powershellToolUseOptions({
yesInputMode = false,
noInputMode = false,
editablePrefix,
onEditablePrefixChange
onEditablePrefixChange,
}: {
suggestions?: PermissionUpdate[];
onRejectFeedbackChange: (value: string) => void;
onAcceptFeedbackChange: (value: string) => void;
yesInputMode?: boolean;
noInputMode?: boolean;
editablePrefix?: string;
onEditablePrefixChange?: (value: string) => void;
suggestions?: PermissionUpdate[]
onRejectFeedbackChange: (value: string) => void
onAcceptFeedbackChange: (value: string) => void
yesInputMode?: boolean
noInputMode?: boolean
editablePrefix?: string
onEditablePrefixChange?: (value: string) => void
}): OptionWithDescription<PowerShellToolUseOption>[] {
const options: OptionWithDescription<PowerShellToolUseOption>[] = [];
const options: OptionWithDescription<PowerShellToolUseOption>[] = []
if (yesInputMode) {
options.push({
type: 'input',
@@ -29,13 +36,13 @@ export function powershellToolUseOptions({
value: 'yes',
placeholder: 'and tell Claude what to do next',
onChange: onAcceptFeedbackChange,
allowEmptySubmitToCancel: true
});
allowEmptySubmitToCancel: true,
})
} else {
options.push({
label: 'Yes',
value: 'yes'
});
value: 'yes',
})
}
// Note: No sandbox toggle for PowerShell - sandbox is not supported on Windows
@@ -47,8 +54,17 @@ export function powershellToolUseOptions({
// directory permissions or Read-tool rules, so fall back to the label when
// those are present.
if (shouldShowAlwaysAllowOptions() && suggestions.length > 0) {
const hasNonPowerShellSuggestions = suggestions.some(s => s.type === 'addDirectories' || s.type === 'addRules' && s.rules?.some(r => r.toolName !== POWERSHELL_TOOL_NAME));
if (editablePrefix !== undefined && onEditablePrefixChange && !hasNonPowerShellSuggestions) {
const hasNonPowerShellSuggestions = suggestions.some(
s =>
s.type === 'addDirectories' ||
(s.type === 'addRules' &&
s.rules?.some(r => r.toolName !== POWERSHELL_TOOL_NAME)),
)
if (
editablePrefix !== undefined &&
onEditablePrefixChange &&
!hasNonPowerShellSuggestions
) {
options.push({
type: 'input',
label: 'Yes, and don\u2019t ask again for',
@@ -59,18 +75,22 @@ export function powershellToolUseOptions({
allowEmptySubmitToCancel: true,
showLabelWithValue: true,
labelValueSeparator: ': ',
resetCursorOnUpdate: true
});
resetCursorOnUpdate: true,
})
} else {
const label = generateShellSuggestionsLabel(suggestions, POWERSHELL_TOOL_NAME);
const label = generateShellSuggestionsLabel(
suggestions,
POWERSHELL_TOOL_NAME,
)
if (label) {
options.push({
label,
value: 'yes-apply-suggestions'
});
value: 'yes-apply-suggestions',
})
}
}
}
if (noInputMode) {
options.push({
type: 'input',
@@ -78,13 +98,14 @@ export function powershellToolUseOptions({
value: 'no',
placeholder: 'and tell Claude what to do differently',
onChange: onRejectFeedbackChange,
allowEmptySubmitToCancel: true
});
allowEmptySubmitToCancel: true,
})
} else {
options.push({
label: 'No',
value: 'no'
});
value: 'no',
})
}
return options;
return options
}

View File

@@ -1,162 +1,106 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { Box, Text } from 'src/ink.js';
import { type NetworkHostPattern, shouldAllowManagedSandboxDomainsOnly } from 'src/utils/sandbox/sandbox-adapter.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
import { Select } from '../CustomSelect/select.js';
import { PermissionDialog } from './PermissionDialog.js';
import * as React from 'react'
import { Box, Text } from 'src/ink.js'
import {
type NetworkHostPattern,
shouldAllowManagedSandboxDomainsOnly,
} from 'src/utils/sandbox/sandbox-adapter.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../services/analytics/index.js'
import { Select } from '../CustomSelect/select.js'
import { PermissionDialog } from './PermissionDialog.js'
export type SandboxPermissionRequestProps = {
hostPattern: NetworkHostPattern;
hostPattern: NetworkHostPattern
onUserResponse: (response: {
allow: boolean;
persistToSettings: boolean;
}) => void;
};
export function SandboxPermissionRequest(t0) {
const $ = _c(22);
const {
hostPattern: t1,
onUserResponse
} = t0;
const {
host
} = t1;
let t2;
if ($[0] !== onUserResponse) {
t2 = function onSelect(value) {
bb4: switch (value) {
case "yes":
{
onUserResponse({
allow: true,
persistToSettings: false
});
break bb4;
}
case "yes-dont-ask-again":
{
onUserResponse({
allow: true,
persistToSettings: true
});
break bb4;
}
case "no":
{
onUserResponse({
allow: false,
persistToSettings: false
});
}
}
};
$[0] = onUserResponse;
$[1] = t2;
} else {
t2 = $[1];
}
const onSelect = t2;
let t3;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t3 = shouldAllowManagedSandboxDomainsOnly();
$[2] = t3;
} else {
t3 = $[2];
}
const managedDomainsOnly = t3;
let t4;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t4 = {
label: "Yes",
value: "yes"
};
$[3] = t4;
} else {
t4 = $[3];
}
let t5;
if ($[4] !== host) {
t5 = !managedDomainsOnly ? [{
label: <Text>Yes, and don't ask again for <Text bold={true}>{host}</Text></Text>,
value: "yes-dont-ask-again"
}] : [];
$[4] = host;
$[5] = t5;
} else {
t5 = $[5];
}
let t6;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t6 = {
label: <Text>No, and tell Claude what to do differently <Text bold={true}>(esc)</Text></Text>,
value: "no"
};
$[6] = t6;
} else {
t6 = $[6];
}
let t7;
if ($[7] !== t5) {
t7 = [t4, ...t5, t6];
$[7] = t5;
$[8] = t7;
} else {
t7 = $[8];
}
const options = t7;
let t8;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t8 = <Text dimColor={true}>Host:</Text>;
$[9] = t8;
} else {
t8 = $[9];
}
let t9;
if ($[10] !== host) {
t9 = <Box>{t8}<Text> {host}</Text></Box>;
$[10] = host;
$[11] = t9;
} else {
t9 = $[11];
}
let t10;
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
t10 = <Box marginTop={1}><Text>Do you want to allow this connection?</Text></Box>;
$[12] = t10;
} else {
t10 = $[12];
}
let t11;
if ($[13] !== onUserResponse) {
t11 = () => {
onUserResponse({
allow: false,
persistToSettings: false
});
};
$[13] = onUserResponse;
$[14] = t11;
} else {
t11 = $[14];
}
let t12;
if ($[15] !== onSelect || $[16] !== options || $[17] !== t11) {
t12 = <Box><Select options={options} onChange={onSelect} onCancel={t11} /></Box>;
$[15] = onSelect;
$[16] = options;
$[17] = t11;
$[18] = t12;
} else {
t12 = $[18];
}
let t13;
if ($[19] !== t12 || $[20] !== t9) {
t13 = <PermissionDialog title="Network request outside of sandbox"><Box flexDirection="column" paddingX={2} paddingY={1}>{t9}{t10}{t12}</Box></PermissionDialog>;
$[19] = t12;
$[20] = t9;
$[21] = t13;
} else {
t13 = $[21];
}
return t13;
allow: boolean
persistToSettings: boolean
}) => void
}
export function SandboxPermissionRequest({
hostPattern: { host },
onUserResponse,
}: SandboxPermissionRequestProps): React.ReactNode {
function onSelect(value: string) {
// We may want to better unify this dialog with other permission dialogs
// and use their logging, but this is slightly different and we don't have
// the tool context here. For now, just use basic logging for basic data.
if (process.env.USER_TYPE === 'ant') {
logEvent('tengu_sandbox_network_dialog_result', {
host: host as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
result:
value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
}
switch (value) {
case 'yes':
onUserResponse({ allow: true, persistToSettings: false })
break
case 'yes-dont-ask-again':
onUserResponse({ allow: true, persistToSettings: true })
break
case 'no':
onUserResponse({ allow: false, persistToSettings: false })
break
}
}
const managedDomainsOnly = shouldAllowManagedSandboxDomainsOnly()
const options = [
{ label: 'Yes', value: 'yes' },
...(!managedDomainsOnly
? [
{
label: (
<Text>
Yes, and don&apos;t ask again for <Text bold>{host}</Text>
</Text>
),
value: 'yes-dont-ask-again',
},
]
: []),
{
label: (
<Text>
No, and tell Claude what to do differently <Text bold>(esc)</Text>
</Text>
),
value: 'no',
},
]
return (
<PermissionDialog title="Network request outside of sandbox">
<Box flexDirection="column" paddingX={2} paddingY={1}>
<Box>
<Text dimColor>Host:</Text>
<Text> {host}</Text>
</Box>
<Box marginTop={1}>
<Text>Do you want to allow this connection?</Text>
</Box>
<Box>
<Select
options={options}
onChange={onSelect}
onCancel={() => {
if (process.env.USER_TYPE === 'ant') {
logEvent('tengu_sandbox_network_dialog_result', {
host: host as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
result:
'cancel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
}
onUserResponse({ allow: false, persistToSettings: false })
}}
/>
</Box>
</Box>
</PermissionDialog>
)
}

View File

@@ -1,229 +1,139 @@
import { c as _c } from "react/compiler-runtime";
import { basename, relative } from 'path';
import React, { Suspense, use, useMemo } from 'react';
import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js';
import { getCwd } from 'src/utils/cwd.js';
import { isENOENT } from 'src/utils/errors.js';
import { detectEncodingForResolvedPath } from 'src/utils/fileRead.js';
import { getFsImplementation } from 'src/utils/fsOperations.js';
import { Text } from '../../../ink.js';
import { BashTool } from '../../../tools/BashTool/BashTool.js';
import { applySedSubstitution, type SedEditInfo } from '../../../tools/BashTool/sedEditParser.js';
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js';
import type { PermissionRequestProps } from '../PermissionRequest.js';
import { basename, relative } from 'path'
import React, { Suspense, use, useMemo } from 'react'
import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js'
import { getCwd } from 'src/utils/cwd.js'
import { isENOENT } from 'src/utils/errors.js'
import { detectEncodingForResolvedPath } from 'src/utils/fileRead.js'
import { getFsImplementation } from 'src/utils/fsOperations.js'
import { Text } from '../../../ink.js'
import { BashTool } from '../../../tools/BashTool/BashTool.js'
import {
applySedSubstitution,
type SedEditInfo,
} from '../../../tools/BashTool/sedEditParser.js'
import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js'
import type { PermissionRequestProps } from '../PermissionRequest.js'
type SedEditPermissionRequestProps = PermissionRequestProps & {
sedInfo: SedEditInfo;
};
type FileReadResult = {
oldContent: string;
fileExists: boolean;
};
export function SedEditPermissionRequest(t0) {
const $ = _c(9);
let props;
let sedInfo;
if ($[0] !== t0) {
({
sedInfo,
...props
} = t0);
$[0] = t0;
$[1] = props;
$[2] = sedInfo;
} else {
props = $[1];
sedInfo = $[2];
}
const {
filePath
} = sedInfo;
let t1;
if ($[3] !== filePath) {
t1 = (async () => {
const encoding = detectEncodingForResolvedPath(filePath);
const raw = await getFsImplementation().readFile(filePath, {
encoding
});
return {
oldContent: raw.replaceAll("\r\n", "\n"),
fileExists: true
};
})().catch(_temp);
$[3] = filePath;
$[4] = t1;
} else {
t1 = $[4];
}
const contentPromise = t1;
let t2;
if ($[5] !== contentPromise || $[6] !== props || $[7] !== sedInfo) {
t2 = <Suspense fallback={null}><SedEditPermissionRequestInner sedInfo={sedInfo} contentPromise={contentPromise} {...props} /></Suspense>;
$[5] = contentPromise;
$[6] = props;
$[7] = sedInfo;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
sedInfo: SedEditInfo
}
function _temp(e) {
if (!isENOENT(e)) {
throw e;
}
return {
oldContent: "",
fileExists: false
};
type FileReadResult = { oldContent: string; fileExists: boolean }
export function SedEditPermissionRequest({
sedInfo,
...props
}: SedEditPermissionRequestProps): React.ReactNode {
const { filePath } = sedInfo
// Read file content async so mount doesn't block React commit on disk I/O.
// Large files would otherwise hang the dialog before it renders.
// Memoized on filePath so we don't re-read on every render.
const contentPromise = useMemo(
() =>
(async (): Promise<FileReadResult> => {
// Detect encoding first (sync 4KB read — negligible) so UTF-16LE BOMs
// render correctly. This matches what readFileSync did before the
// async conversion.
const encoding = detectEncodingForResolvedPath(filePath)
const raw = await getFsImplementation().readFile(filePath, { encoding })
return {
oldContent: raw.replaceAll('\r\n', '\n'),
fileExists: true,
}
})().catch((e: unknown): FileReadResult => {
if (!isENOENT(e)) throw e
return { oldContent: '', fileExists: false }
}),
[filePath],
)
return (
<Suspense fallback={null}>
<SedEditPermissionRequestInner
sedInfo={sedInfo}
contentPromise={contentPromise}
{...props}
/>
</Suspense>
)
}
function SedEditPermissionRequestInner(t0) {
const $ = _c(35);
let contentPromise;
let props;
let sedInfo;
if ($[0] !== t0) {
({
sedInfo,
contentPromise,
...props
} = t0);
$[0] = t0;
$[1] = contentPromise;
$[2] = props;
$[3] = sedInfo;
} else {
contentPromise = $[1];
props = $[2];
sedInfo = $[3];
}
const {
filePath
} = sedInfo;
const {
oldContent,
fileExists
} = use(contentPromise) as any;
let t1;
if ($[4] !== oldContent || $[5] !== sedInfo) {
t1 = applySedSubstitution(oldContent, sedInfo);
$[4] = oldContent;
$[5] = sedInfo;
$[6] = t1;
} else {
t1 = $[6];
}
const newContent = t1;
let t2;
bb0: {
function SedEditPermissionRequestInner({
sedInfo,
contentPromise,
...props
}: SedEditPermissionRequestProps & {
contentPromise: Promise<FileReadResult>
}): React.ReactNode {
const { filePath } = sedInfo
const { oldContent, fileExists } = use(contentPromise)
// Compute the new content by applying the sed substitution
const newContent = useMemo(() => {
return applySedSubstitution(oldContent, sedInfo)
}, [oldContent, sedInfo])
// Create the edit representation for the diff
const edits = useMemo(() => {
if (oldContent === newContent) {
let t3;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t3 = [];
$[7] = t3;
} else {
t3 = $[7];
}
t2 = t3;
break bb0;
return []
}
let t3;
if ($[8] !== newContent || $[9] !== oldContent) {
t3 = [{
return [
{
old_string: oldContent,
new_string: newContent,
replace_all: false
}];
$[8] = newContent;
$[9] = oldContent;
$[10] = t3;
} else {
t3 = $[10];
}
t2 = t3;
}
const edits = t2;
let t3;
bb1: {
replace_all: false,
},
]
}, [oldContent, newContent])
// Determine appropriate message when no changes
const noChangesMessage = useMemo(() => {
if (!fileExists) {
t3 = "File does not exist";
break bb1;
return 'File does not exist'
}
return 'Pattern did not match any content'
}, [fileExists])
// Parse input and add _simulatedSedEdit to ensure what user previewed
// is exactly what gets written (prevents sed/JS regex differences)
const parseInput = (input: unknown) => {
const parsed = BashTool.inputSchema.parse(input)
return {
...parsed,
_simulatedSedEdit: {
filePath,
newContent,
},
}
t3 = "Pattern did not match any content";
}
const noChangesMessage = t3;
let t4;
if ($[11] !== filePath || $[12] !== newContent) {
t4 = input => {
const parsed = BashTool.inputSchema.parse(input);
return {
...parsed,
_simulatedSedEdit: {
filePath,
newContent
}
};
};
$[11] = filePath;
$[12] = newContent;
$[13] = t4;
} else {
t4 = $[13];
}
const parseInput = t4;
const t5 = props.toolUseConfirm;
const t6 = props.toolUseContext;
const t7 = props.onDone;
const t8 = props.onReject;
let t9;
if ($[14] !== filePath) {
t9 = relative(getCwd(), filePath);
$[14] = filePath;
$[15] = t9;
} else {
t9 = $[15];
}
let t10;
if ($[16] !== filePath) {
t10 = basename(filePath);
$[16] = filePath;
$[17] = t10;
} else {
t10 = $[17];
}
let t11;
if ($[18] !== t10) {
t11 = <Text>Do you want to make this edit to{" "}<Text bold={true}>{t10}</Text>?</Text>;
$[18] = t10;
$[19] = t11;
} else {
t11 = $[19];
}
let t12;
if ($[20] !== edits || $[21] !== filePath || $[22] !== noChangesMessage) {
t12 = edits.length > 0 ? <FileEditToolDiff file_path={filePath} edits={edits} /> : <Text dimColor={true}>{noChangesMessage}</Text>;
$[20] = edits;
$[21] = filePath;
$[22] = noChangesMessage;
$[23] = t12;
} else {
t12 = $[23];
}
let t13;
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) {
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} />;
$[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;
$[34] = t13;
} else {
t13 = $[34];
}
return t13;
return (
<FilePermissionDialog
toolUseConfirm={props.toolUseConfirm}
toolUseContext={props.toolUseContext}
onDone={props.onDone}
onReject={props.onReject}
title="Edit file"
subtitle={relative(getCwd(), filePath)}
question={
<Text>
Do you want to make this edit to{' '}
<Text bold>{basename(filePath)}</Text>?
</Text>
}
content={
edits.length > 0 ? (
<FileEditToolDiff file_path={filePath} edits={edits} />
) : (
<Text dimColor>{noChangesMessage}</Text>
)
}
path={filePath}
completionType="str_replace_single"
parseInput={parseInput}
workerBadge={props.workerBadge}
/>
)
}

View File

@@ -1,368 +1,253 @@
import { c as _c } from "react/compiler-runtime";
import React, { useCallback, useMemo } from 'react';
import { logError } from 'src/utils/log.js';
import { getOriginalCwd } from '../../../bootstrap/state.js';
import { Box, Text } from '../../../ink.js';
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js';
import { SKILL_TOOL_NAME } from '../../../tools/SkillTool/constants.js';
import { SkillTool } from '../../../tools/SkillTool/SkillTool.js';
import { env } from '../../../utils/env.js';
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js';
import { logUnaryEvent } from '../../../utils/unaryLogging.js';
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js';
import { PermissionDialog } from '../PermissionDialog.js';
import { PermissionPrompt, type PermissionPromptOption, type ToolAnalyticsContext } from '../PermissionPrompt.js';
import type { PermissionRequestProps } from '../PermissionRequest.js';
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js';
type SkillOptionValue = 'yes' | 'yes-exact' | 'yes-prefix' | 'no';
export function SkillPermissionRequest(props) {
const $ = _c(51);
import React, { useCallback, useMemo } from 'react'
import { logError } from 'src/utils/log.js'
import { getOriginalCwd } from '../../../bootstrap/state.js'
import { Box, Text } from '../../../ink.js'
import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js'
import { SKILL_TOOL_NAME } from '../../../tools/SkillTool/constants.js'
import { SkillTool } from '../../../tools/SkillTool/SkillTool.js'
import { env } from '../../../utils/env.js'
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'
import { logUnaryEvent } from '../../../utils/unaryLogging.js'
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'
import { PermissionDialog } from '../PermissionDialog.js'
import {
PermissionPrompt,
type PermissionPromptOption,
type ToolAnalyticsContext,
} from '../PermissionPrompt.js'
import type { PermissionRequestProps } from '../PermissionRequest.js'
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
type SkillOptionValue = 'yes' | 'yes-exact' | 'yes-prefix' | 'no'
export function SkillPermissionRequest(
props: PermissionRequestProps,
): React.ReactNode {
const {
toolUseConfirm,
onDone,
onReject,
workerBadge
} = props;
const parseInput = _temp;
let t0;
if ($[0] !== toolUseConfirm.input) {
t0 = parseInput(toolUseConfirm.input);
$[0] = toolUseConfirm.input;
$[1] = t0;
} else {
t0 = $[1];
verbose: _verbose,
workerBadge,
} = props
const parseInput = (input: unknown): string => {
const result = SkillTool.inputSchema.safeParse(input)
if (!result.success) {
logError(
new Error(`Failed to parse skill tool input: ${result.error.message}`),
)
return ''
}
return result.data.skill
}
const skill = t0;
const commandObj = toolUseConfirm.permissionResult.behavior === "ask" && toolUseConfirm.permissionResult.metadata && "command" in toolUseConfirm.permissionResult.metadata ? toolUseConfirm.permissionResult.metadata.command : undefined;
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {
completion_type: "tool_use_single",
language_name: "none"
};
$[2] = t1;
} else {
t1 = $[2];
}
const unaryEvent = t1;
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = getOriginalCwd();
$[3] = t2;
} else {
t2 = $[3];
}
const originalCwd = t2;
let t3;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t3 = shouldShowAlwaysAllowOptions();
$[4] = t3;
} else {
t3 = $[4];
}
const showAlwaysAllowOptions = t3;
let t4;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t4 = [{
label: "Yes",
value: "yes",
feedbackConfig: {
type: "accept"
}
}];
$[5] = t4;
} else {
t4 = $[5];
}
const baseOptions = t4;
let alwaysAllowOptions;
if ($[6] !== skill) {
alwaysAllowOptions = [];
const skill = parseInput(toolUseConfirm.input)
// Check if this is a command using metadata from checkPermissions
const commandObj =
toolUseConfirm.permissionResult.behavior === 'ask' &&
toolUseConfirm.permissionResult.metadata &&
'command' in toolUseConfirm.permissionResult.metadata
? toolUseConfirm.permissionResult.metadata.command
: undefined
const unaryEvent = useMemo<UnaryEvent>(
() => ({
completion_type: 'tool_use_single',
language_name: 'none',
}),
[],
)
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
const originalCwd = getOriginalCwd()
const showAlwaysAllowOptions = shouldShowAlwaysAllowOptions()
const options = useMemo((): PermissionPromptOption<SkillOptionValue>[] => {
const baseOptions: PermissionPromptOption<SkillOptionValue>[] = [
{
label: 'Yes',
value: 'yes',
feedbackConfig: { type: 'accept' },
},
]
// Only add "always allow" options when not restricted by allowManagedPermissionRulesOnly
const alwaysAllowOptions: PermissionPromptOption<SkillOptionValue>[] = []
if (showAlwaysAllowOptions) {
const t5 = <Text bold={true}>{skill}</Text>;
let t6;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t6 = <Text bold={true}>{originalCwd}</Text>;
$[8] = t6;
} else {
t6 = $[8];
}
let t7;
if ($[9] !== t5) {
t7 = {
label: <Text>Yes, and don't ask again for {t5} in{" "}{t6}</Text>,
value: "yes-exact"
};
$[9] = t5;
$[10] = t7;
} else {
t7 = $[10];
}
alwaysAllowOptions.push(t7);
const spaceIndex = skill.indexOf(" ");
// Add exact match option
alwaysAllowOptions.push({
label: (
<Text>
Yes, and don&apos;t ask again for <Text bold>{skill}</Text> in{' '}
<Text bold>{originalCwd}</Text>
</Text>
),
value: 'yes-exact',
})
// Add prefix option if the skill has arguments
const spaceIndex = skill.indexOf(' ')
if (spaceIndex > 0) {
const commandPrefix = skill.substring(0, spaceIndex);
const t8 = commandPrefix + ":*";
let t9;
if ($[11] !== t8) {
t9 = <Text bold={true}>{t8}</Text>;
$[11] = t8;
$[12] = t9;
} else {
t9 = $[12];
}
let t10;
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
t10 = <Text bold={true}>{originalCwd}</Text>;
$[13] = t10;
} else {
t10 = $[13];
}
let t11;
if ($[14] !== t9) {
t11 = {
label: <Text>Yes, and don't ask again for{" "}{t9} commands in{" "}{t10}</Text>,
value: "yes-prefix"
};
$[14] = t9;
$[15] = t11;
} else {
t11 = $[15];
}
alwaysAllowOptions.push(t11);
const commandPrefix = skill.substring(0, spaceIndex)
alwaysAllowOptions.push({
label: (
<Text>
Yes, and don&apos;t ask again for{' '}
<Text bold>{commandPrefix + ':*'}</Text> commands in{' '}
<Text bold>{originalCwd}</Text>
</Text>
),
value: 'yes-prefix',
})
}
}
$[6] = skill;
$[7] = alwaysAllowOptions;
} else {
alwaysAllowOptions = $[7];
}
let t5;
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
t5 = {
label: "No",
value: "no",
feedbackConfig: {
type: "reject"
}
};
$[16] = t5;
} else {
t5 = $[16];
}
const noOption = t5;
let t6;
if ($[17] !== alwaysAllowOptions) {
t6 = [...baseOptions, ...alwaysAllowOptions, noOption];
$[17] = alwaysAllowOptions;
$[18] = t6;
} else {
t6 = $[18];
}
const options = t6;
let t7;
if ($[19] !== toolUseConfirm.tool.name) {
t7 = sanitizeToolNameForAnalytics(toolUseConfirm.tool.name);
$[19] = toolUseConfirm.tool.name;
$[20] = t7;
} else {
t7 = $[20];
}
const t8 = toolUseConfirm.tool.isMcp ?? false;
let t9;
if ($[21] !== t7 || $[22] !== t8) {
t9 = {
toolName: t7,
isMcp: t8
};
$[21] = t7;
$[22] = t8;
$[23] = t9;
} else {
t9 = $[23];
}
const toolAnalyticsContext = t9;
let t10;
if ($[24] !== onDone || $[25] !== onReject || $[26] !== skill || $[27] !== toolUseConfirm) {
t10 = (value, feedback) => {
bb33: switch (value) {
case "yes":
{
logUnaryEvent({
completion_type: "tool_use_single",
event: "accept",
metadata: {
language_name: "none",
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform
}
});
toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback);
onDone();
break bb33;
}
case "yes-exact":
{
logUnaryEvent({
completion_type: "tool_use_single",
event: "accept",
metadata: {
language_name: "none",
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform
}
});
toolUseConfirm.onAllow(toolUseConfirm.input, [{
type: "addRules",
rules: [{
toolName: SKILL_TOOL_NAME,
ruleContent: skill
}],
behavior: "allow",
destination: "localSettings"
}]);
onDone();
break bb33;
}
case "yes-prefix":
{
logUnaryEvent({
completion_type: "tool_use_single",
event: "accept",
metadata: {
language_name: "none",
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform
}
});
const spaceIndex_0 = skill.indexOf(" ");
const commandPrefix_0 = spaceIndex_0 > 0 ? skill.substring(0, spaceIndex_0) : skill;
toolUseConfirm.onAllow(toolUseConfirm.input, [{
type: "addRules",
rules: [{
toolName: SKILL_TOOL_NAME,
ruleContent: `${commandPrefix_0}:*`
}],
behavior: "allow",
destination: "localSettings"
}]);
onDone();
break bb33;
}
case "no":
{
logUnaryEvent({
completion_type: "tool_use_single",
event: "reject",
metadata: {
language_name: "none",
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform
}
});
toolUseConfirm.onReject(feedback);
onReject();
onDone();
}
}
};
$[24] = onDone;
$[25] = onReject;
$[26] = skill;
$[27] = toolUseConfirm;
$[28] = t10;
} else {
t10 = $[28];
}
const handleSelect = t10;
let t11;
if ($[29] !== onDone || $[30] !== onReject || $[31] !== toolUseConfirm) {
t11 = () => {
logUnaryEvent({
completion_type: "tool_use_single",
event: "reject",
metadata: {
language_name: "none",
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform
const noOption: PermissionPromptOption<SkillOptionValue> = {
label: 'No',
value: 'no',
feedbackConfig: { type: 'reject' },
}
return [...baseOptions, ...alwaysAllowOptions, noOption]
}, [skill, originalCwd, showAlwaysAllowOptions])
const toolAnalyticsContext = useMemo(
(): ToolAnalyticsContext => ({
toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name),
isMcp: toolUseConfirm.tool.isMcp ?? false,
}),
[toolUseConfirm.tool.name, toolUseConfirm.tool.isMcp],
)
const handleSelect = useCallback(
(value: SkillOptionValue, feedback?: string) => {
switch (value) {
case 'yes':
void logUnaryEvent({
completion_type: 'tool_use_single',
event: 'accept',
metadata: {
language_name: 'none',
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform,
},
})
toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback)
onDone()
break
case 'yes-exact': {
void logUnaryEvent({
completion_type: 'tool_use_single',
event: 'accept',
metadata: {
language_name: 'none',
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform,
},
})
toolUseConfirm.onAllow(toolUseConfirm.input, [
{
type: 'addRules',
rules: [
{
toolName: SKILL_TOOL_NAME,
ruleContent: skill,
},
],
behavior: 'allow',
destination: 'localSettings',
},
])
onDone()
break
}
});
toolUseConfirm.onReject();
onReject();
onDone();
};
$[29] = onDone;
$[30] = onReject;
$[31] = toolUseConfirm;
$[32] = t11;
} else {
t11 = $[32];
}
const handleCancel = t11;
const t12 = `Use skill "${skill}"?`;
let t13;
if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
t13 = <Text>Claude may use instructions, code, or files from this Skill.</Text>;
$[33] = t13;
} else {
t13 = $[33];
}
const t14 = commandObj?.description;
let t15;
if ($[34] !== t14) {
t15 = <Box flexDirection="column" paddingX={2} paddingY={1}><Text dimColor={true}>{t14}</Text></Box>;
$[34] = t14;
$[35] = t15;
} else {
t15 = $[35];
}
let t16;
if ($[36] !== toolUseConfirm.permissionResult) {
t16 = <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" />;
$[36] = toolUseConfirm.permissionResult;
$[37] = t16;
} else {
t16 = $[37];
}
let t17;
if ($[38] !== handleCancel || $[39] !== handleSelect || $[40] !== options || $[41] !== toolAnalyticsContext) {
t17 = <PermissionPrompt options={options} onSelect={handleSelect} onCancel={handleCancel} toolAnalyticsContext={toolAnalyticsContext} />;
$[38] = handleCancel;
$[39] = handleSelect;
$[40] = options;
$[41] = toolAnalyticsContext;
$[42] = t17;
} else {
t17 = $[42];
}
let t18;
if ($[43] !== t16 || $[44] !== t17) {
t18 = <Box flexDirection="column">{t16}{t17}</Box>;
$[43] = t16;
$[44] = t17;
$[45] = t18;
} else {
t18 = $[45];
}
let t19;
if ($[46] !== t12 || $[47] !== t15 || $[48] !== t18 || $[49] !== workerBadge) {
t19 = <PermissionDialog title={t12} workerBadge={workerBadge}>{t13}{t15}{t18}</PermissionDialog>;
$[46] = t12;
$[47] = t15;
$[48] = t18;
$[49] = workerBadge;
$[50] = t19;
} else {
t19 = $[50];
}
return t19;
}
function _temp(input) {
const result = SkillTool.inputSchema.safeParse(input);
if (!result.success) {
logError(new Error(`Failed to parse skill tool input: ${result.error.message}`));
return "";
}
return result.data.skill;
case 'yes-prefix': {
void logUnaryEvent({
completion_type: 'tool_use_single',
event: 'accept',
metadata: {
language_name: 'none',
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform,
},
})
// Extract the skill prefix (everything before the first space)
const spaceIndex = skill.indexOf(' ')
const commandPrefix =
spaceIndex > 0 ? skill.substring(0, spaceIndex) : skill
toolUseConfirm.onAllow(toolUseConfirm.input, [
{
type: 'addRules',
rules: [
{
toolName: SKILL_TOOL_NAME,
ruleContent: `${commandPrefix}:*`,
},
],
behavior: 'allow',
destination: 'localSettings',
},
])
onDone()
break
}
case 'no':
void logUnaryEvent({
completion_type: 'tool_use_single',
event: 'reject',
metadata: {
language_name: 'none',
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform,
},
})
toolUseConfirm.onReject(feedback)
onReject()
onDone()
break
}
},
[toolUseConfirm, onDone, onReject, skill],
)
const handleCancel = useCallback(() => {
void logUnaryEvent({
completion_type: 'tool_use_single',
event: 'reject',
metadata: {
language_name: 'none',
message_id: toolUseConfirm.assistantMessage.message.id,
platform: env.platform,
},
})
toolUseConfirm.onReject()
onReject()
onDone()
}, [toolUseConfirm, onDone, onReject])
return (
<PermissionDialog title={`Use skill "${skill}"?`} workerBadge={workerBadge}>
<Text>Claude may use instructions, code, or files from this Skill.</Text>
<Box flexDirection="column" paddingX={2} paddingY={1}>
<Text dimColor>{commandObj?.description}</Text>
</Box>
<Box flexDirection="column">
<PermissionRuleExplanation
permissionResult={toolUseConfirm.permissionResult}
toolType="tool"
/>
<PermissionPrompt
options={options}
onSelect={handleSelect}
onCancel={handleCancel}
toolAnalyticsContext={toolAnalyticsContext}
/>
</Box>
</PermissionDialog>
)
}

View File

@@ -1,257 +1,148 @@
import { c as _c } from "react/compiler-runtime";
import React, { useMemo } from 'react';
import { Box, Text, useTheme } from '../../../ink.js';
import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js';
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js';
import { type OptionWithDescription, Select } from '../../CustomSelect/select.js';
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js';
import { PermissionDialog } from '../PermissionDialog.js';
import type { PermissionRequestProps } from '../PermissionRequest.js';
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js';
import { logUnaryPermissionEvent } from '../utils.js';
function inputToPermissionRuleContent(input: {
[k: string]: unknown;
}): string {
import React, { useMemo } from 'react'
import { Box, Text, useTheme } from '../../../ink.js'
import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js'
import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js'
import {
type OptionWithDescription,
Select,
} from '../../CustomSelect/select.js'
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'
import { PermissionDialog } from '../PermissionDialog.js'
import type { PermissionRequestProps } from '../PermissionRequest.js'
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
import { logUnaryPermissionEvent } from '../utils.js'
function inputToPermissionRuleContent(input: { [k: string]: unknown }): string {
try {
const parsedInput = WebFetchTool.inputSchema.safeParse(input);
const parsedInput = WebFetchTool.inputSchema.safeParse(input)
if (!parsedInput.success) {
return `input:${input.toString()}`;
return `input:${input.toString()}`
}
const {
url
} = parsedInput.data;
const hostname = new URL(url).hostname;
return `domain:${hostname}`;
const { url } = parsedInput.data
const hostname = new URL(url).hostname
return `domain:${hostname}`
} catch {
return `input:${input.toString()}`;
return `input:${input.toString()}`
}
}
export function WebFetchPermissionRequest(t0) {
const $ = _c(41);
const {
toolUseConfirm,
onDone,
onReject,
verbose,
workerBadge
} = t0;
const [theme] = useTheme();
const {
url
} = toolUseConfirm.input as {
url: string;
};
let t1;
if ($[0] !== url) {
t1 = new URL(url);
$[0] = url;
$[1] = t1;
} else {
t1 = $[1];
}
const hostname = t1.hostname;
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = {
completion_type: "tool_use_single",
language_name: "none"
};
$[2] = t2;
} else {
t2 = $[2];
}
const unaryEvent = t2;
usePermissionRequestLogging(toolUseConfirm, unaryEvent);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = shouldShowAlwaysAllowOptions();
$[3] = t3;
} else {
t3 = $[3];
}
const showAlwaysAllowOptions = t3;
let t4;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t4 = {
label: "Yes",
value: "yes"
};
$[4] = t4;
} else {
t4 = $[4];
}
let result;
if ($[5] !== hostname) {
result = [t4];
export function WebFetchPermissionRequest({
toolUseConfirm,
onDone,
onReject,
verbose,
workerBadge,
}: PermissionRequestProps): React.ReactNode {
const [theme] = useTheme()
// url is already validated by the input schema
const { url } = toolUseConfirm.input as { url: string }
// Extract hostname from URL
const hostname = new URL(url).hostname
const unaryEvent = useMemo<UnaryEvent>(
() => ({ completion_type: 'tool_use_single', language_name: 'none' }),
[],
)
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
// Generate permission options specific to domains
const showAlwaysAllowOptions = shouldShowAlwaysAllowOptions()
const options = useMemo((): OptionWithDescription<string>[] => {
const result: OptionWithDescription<string>[] = [
{
label: 'Yes',
value: 'yes',
},
]
if (showAlwaysAllowOptions) {
const t5 = <Text bold={true}>{hostname}</Text>;
let t6;
if ($[7] !== t5) {
t6 = {
label: <Text>Yes, and don't ask again for {t5}</Text>,
value: "yes-dont-ask-again-domain"
};
$[7] = t5;
$[8] = t6;
} else {
t6 = $[8];
}
result.push(t6);
result.push({
label: (
<Text>
Yes, and don&apos;t ask again for <Text bold>{hostname}</Text>
</Text>
),
value: 'yes-dont-ask-again-domain',
})
}
let t5;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t5 = {
label: <Text>No, and tell Claude what to do differently <Text bold={true}>(esc)</Text></Text>,
value: "no"
};
$[9] = t5;
} else {
t5 = $[9];
}
result.push(t5);
$[5] = hostname;
$[6] = result;
} else {
result = $[6];
}
const options = result;
let t5;
if ($[10] !== onDone || $[11] !== onReject || $[12] !== toolUseConfirm) {
t5 = function onChange(newValue) {
bb8: switch (newValue) {
case "yes":
result.push({
label: (
<Text>
No, and tell Claude what to do differently <Text bold>(esc)</Text>
</Text>
),
value: 'no',
})
return result
}, [hostname, showAlwaysAllowOptions])
function onChange(newValue: string) {
switch (newValue) {
case 'yes':
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
toolUseConfirm.onAllow(toolUseConfirm.input, [])
onDone()
break
case 'yes-dont-ask-again-domain': {
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
const ruleContent = inputToPermissionRuleContent(toolUseConfirm.input)
const ruleValue = {
toolName: toolUseConfirm.tool.name,
ruleContent,
}
// Pass permission update directly to onAllow
toolUseConfirm.onAllow(toolUseConfirm.input, [
{
logUnaryPermissionEvent("tool_use_single", toolUseConfirm, "accept");
toolUseConfirm.onAllow(toolUseConfirm.input, []);
onDone();
break bb8;
}
case "yes-dont-ask-again-domain":
{
logUnaryPermissionEvent("tool_use_single", toolUseConfirm, "accept");
const ruleContent = inputToPermissionRuleContent(toolUseConfirm.input);
const ruleValue = {
toolName: toolUseConfirm.tool.name,
ruleContent
};
toolUseConfirm.onAllow(toolUseConfirm.input, [{
type: "addRules",
rules: [ruleValue],
behavior: "allow",
destination: "localSettings"
}]);
onDone();
break bb8;
}
case "no":
{
logUnaryPermissionEvent("tool_use_single", toolUseConfirm, "reject");
toolUseConfirm.onReject();
onReject();
onDone();
}
type: 'addRules',
rules: [ruleValue],
behavior: 'allow',
destination: 'localSettings',
},
])
onDone()
break
}
};
$[10] = onDone;
$[11] = onReject;
$[12] = toolUseConfirm;
$[13] = t5;
} else {
t5 = $[13];
case 'no':
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'reject')
toolUseConfirm.onReject()
onReject()
onDone()
break
}
}
const onChange = t5;
let t6;
if ($[14] !== theme || $[15] !== toolUseConfirm.input || $[16] !== verbose) {
t6 = WebFetchTool.renderToolUseMessage(toolUseConfirm.input as {
url: string;
prompt: string;
}, {
theme,
verbose
});
$[14] = theme;
$[15] = toolUseConfirm.input;
$[16] = verbose;
$[17] = t6;
} else {
t6 = $[17];
}
let t7;
if ($[18] !== t6) {
t7 = <Text>{t6}</Text>;
$[18] = t6;
$[19] = t7;
} else {
t7 = $[19];
}
let t8;
if ($[20] !== toolUseConfirm.description) {
t8 = <Text dimColor={true}>{toolUseConfirm.description}</Text>;
$[20] = toolUseConfirm.description;
$[21] = t8;
} else {
t8 = $[21];
}
let t9;
if ($[22] !== t7 || $[23] !== t8) {
t9 = <Box flexDirection="column" paddingX={2} paddingY={1}>{t7}{t8}</Box>;
$[22] = t7;
$[23] = t8;
$[24] = t9;
} else {
t9 = $[24];
}
let t10;
if ($[25] !== toolUseConfirm.permissionResult) {
t10 = <PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" />;
$[25] = toolUseConfirm.permissionResult;
$[26] = t10;
} else {
t10 = $[26];
}
let t11;
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
t11 = <Text>Do you want to allow Claude to fetch this content?</Text>;
$[27] = t11;
} else {
t11 = $[27];
}
let t12;
if ($[28] !== onChange) {
t12 = () => onChange("no");
$[28] = onChange;
$[29] = t12;
} else {
t12 = $[29];
}
let t13;
if ($[30] !== onChange || $[31] !== options || $[32] !== t12) {
t13 = <Select options={options} onChange={onChange} onCancel={t12} />;
$[30] = onChange;
$[31] = options;
$[32] = t12;
$[33] = t13;
} else {
t13 = $[33];
}
let t14;
if ($[34] !== t10 || $[35] !== t13) {
t14 = <Box flexDirection="column">{t10}{t11}{t13}</Box>;
$[34] = t10;
$[35] = t13;
$[36] = t14;
} else {
t14 = $[36];
}
let t15;
if ($[37] !== t14 || $[38] !== t9 || $[39] !== workerBadge) {
t15 = <PermissionDialog title="Fetch" workerBadge={workerBadge}>{t9}{t14}</PermissionDialog>;
$[37] = t14;
$[38] = t9;
$[39] = workerBadge;
$[40] = t15;
} else {
t15 = $[40];
}
return t15;
return (
<PermissionDialog title="Fetch" workerBadge={workerBadge}>
<Box flexDirection="column" paddingX={2} paddingY={1}>
<Text>
{WebFetchTool.renderToolUseMessage(
toolUseConfirm.input as { url: string; prompt: string },
{
theme,
verbose,
},
)}
</Text>
<Text dimColor>{toolUseConfirm.description}</Text>
</Box>
<Box flexDirection="column">
<PermissionRuleExplanation
permissionResult={toolUseConfirm.permissionResult}
toolType="tool"
/>
<Text>Do you want to allow Claude to fetch this content?</Text>
<Select
options={options}
onChange={onChange}
onCancel={() => onChange('no')}
/>
</Box>
</PermissionDialog>
)
}

View File

@@ -1,48 +1,27 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { BLACK_CIRCLE } from '../../constants/figures.js';
import { Box, Text } from '../../ink.js';
import { toInkColor } from '../../utils/ink.js';
import * as React from 'react'
import { BLACK_CIRCLE } from '../../constants/figures.js'
import { Box, Text } from '../../ink.js'
import { toInkColor } from '../../utils/ink.js'
export type WorkerBadgeProps = {
name: string;
color: string;
};
name: string
color: string
}
/**
* Renders a colored badge showing the worker's name for permission prompts.
* Used to indicate which swarm worker is requesting the permission.
*/
export function WorkerBadge(t0) {
const $ = _c(7);
const {
name,
color
} = t0;
let t1;
if ($[0] !== color) {
t1 = toInkColor(color);
$[0] = color;
$[1] = t1;
} else {
t1 = $[1];
}
const inkColor = t1;
let t2;
if ($[2] !== name) {
t2 = <Text bold={true}>@{name}</Text>;
$[2] = name;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== inkColor || $[5] !== t2) {
t3 = <Box flexDirection="row" gap={1}><Text color={inkColor}>{BLACK_CIRCLE} {t2}</Text></Box>;
$[4] = inkColor;
$[5] = t2;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
export function WorkerBadge({
name,
color,
}: WorkerBadgeProps): React.ReactNode {
const inkColor = toInkColor(color)
return (
<Box flexDirection="row" gap={1}>
<Text color={inkColor}>
{BLACK_CIRCLE} <Text bold>@{name}</Text>
</Text>
</Box>
)
}

View File

@@ -1,104 +1,70 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { Box, Text } from '../../ink.js';
import { getAgentName, getTeammateColor, getTeamName } from '../../utils/teammate.js';
import { Spinner } from '../Spinner.js';
import { WorkerBadge } from './WorkerBadge.js';
import * as React from 'react'
import { Box, Text } from '../../ink.js'
import {
getAgentName,
getTeammateColor,
getTeamName,
} from '../../utils/teammate.js'
import { Spinner } from '../Spinner.js'
import { WorkerBadge } from './WorkerBadge.js'
type Props = {
toolName: string;
description: string;
};
toolName: string
description: string
}
/**
* Visual indicator shown on workers while waiting for leader to approve a permission request.
* Displays the pending tool with a spinner and information about what's being requested.
*/
export function WorkerPendingPermission(t0) {
const $ = _c(15);
const {
toolName,
description
} = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = getTeamName();
$[0] = t1;
} else {
t1 = $[0];
}
const teamName = t1;
let t2;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t2 = getAgentName();
$[1] = t2;
} else {
t2 = $[1];
}
const agentName = t2;
let t3;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t3 = getTeammateColor();
$[2] = t3;
} else {
t3 = $[2];
}
const agentColor = t3;
let t4;
let t5;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Box marginBottom={1}><Spinner /><Text color="warning" bold={true}>{" "}Waiting for team lead approval</Text></Box>;
t5 = agentName && agentColor && <Box marginBottom={1}><WorkerBadge name={agentName} color={agentColor} /></Box>;
$[3] = t4;
$[4] = t5;
} else {
t4 = $[3];
t5 = $[4];
}
let t6;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t6 = <Text dimColor={true}>Tool: </Text>;
$[5] = t6;
} else {
t6 = $[5];
}
let t7;
if ($[6] !== toolName) {
t7 = <Box>{t6}<Text>{toolName}</Text></Box>;
$[6] = toolName;
$[7] = t7;
} else {
t7 = $[7];
}
let t8;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t8 = <Text dimColor={true}>Action: </Text>;
$[8] = t8;
} else {
t8 = $[8];
}
let t9;
if ($[9] !== description) {
t9 = <Box>{t8}<Text>{description}</Text></Box>;
$[9] = description;
$[10] = t9;
} else {
t9 = $[10];
}
let t10;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t10 = teamName && <Box marginTop={1}><Text dimColor={true}>Permission request sent to team {"\""}{teamName}{"\""} leader</Text></Box>;
$[11] = t10;
} else {
t10 = $[11];
}
let t11;
if ($[12] !== t7 || $[13] !== t9) {
t11 = <Box flexDirection="column" borderStyle="round" borderColor="warning" paddingX={1}>{t4}{t5}{t7}{t9}{t10}</Box>;
$[12] = t7;
$[13] = t9;
$[14] = t11;
} else {
t11 = $[14];
}
return t11;
export function WorkerPendingPermission({
toolName,
description,
}: Props): React.ReactNode {
const teamName = getTeamName()
const agentName = getAgentName()
const agentColor = getTeammateColor()
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor="warning"
paddingX={1}
>
<Box marginBottom={1}>
<Spinner />
<Text color="warning" bold>
{' '}
Waiting for team lead approval
</Text>
</Box>
{agentName && agentColor && (
<Box marginBottom={1}>
<WorkerBadge name={agentName} color={agentColor} />
</Box>
)}
<Box>
<Text dimColor>Tool: </Text>
<Text>{toolName}</Text>
</Box>
<Box>
<Text dimColor>Action: </Text>
<Text>{description}</Text>
</Box>
{teamName && (
<Box marginTop={1}>
<Text dimColor>
Permission request sent to team {'"'}
{teamName}
{'"'} leader
</Text>
</Box>
)}
</Box>
)
}

View File

@@ -1,179 +1,165 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { useCallback } from 'react';
import { Select } from '../../../components/CustomSelect/select.js';
import { Box, Text } from '../../../ink.js';
import type { ToolPermissionContext } from '../../../Tool.js';
import type { PermissionBehavior, PermissionRule, PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js';
import { applyPermissionUpdate, persistPermissionUpdate } from '../../../utils/permissions/PermissionUpdate.js';
import { permissionRuleValueToString } from '../../../utils/permissions/permissionRuleParser.js';
import { detectUnreachableRules, type UnreachableRule } from '../../../utils/permissions/shadowedRuleDetection.js';
import { SandboxManager } from '../../../utils/sandbox/sandbox-adapter.js';
import { type EditableSettingSource, SOURCES } from '../../../utils/settings/constants.js';
import { getRelativeSettingsFilePathForSource } from '../../../utils/settings/settings.js';
import { plural } from '../../../utils/stringUtils.js';
import type { OptionWithDescription } from '../../CustomSelect/select.js';
import { Dialog } from '../../design-system/Dialog.js';
import { PermissionRuleDescription } from './PermissionRuleDescription.js';
export function optionForPermissionSaveDestination(saveDestination: EditableSettingSource): OptionWithDescription {
import * as React from 'react'
import { useCallback } from 'react'
import { Select } from '../../../components/CustomSelect/select.js'
import { Box, Text } from '../../../ink.js'
import type { ToolPermissionContext } from '../../../Tool.js'
import type {
PermissionBehavior,
PermissionRule,
PermissionRuleValue,
} from '../../../utils/permissions/PermissionRule.js'
import {
applyPermissionUpdate,
persistPermissionUpdate,
} from '../../../utils/permissions/PermissionUpdate.js'
import { permissionRuleValueToString } from '../../../utils/permissions/permissionRuleParser.js'
import {
detectUnreachableRules,
type UnreachableRule,
} from '../../../utils/permissions/shadowedRuleDetection.js'
import { SandboxManager } from '../../../utils/sandbox/sandbox-adapter.js'
import {
type EditableSettingSource,
SOURCES,
} from '../../../utils/settings/constants.js'
import { getRelativeSettingsFilePathForSource } from '../../../utils/settings/settings.js'
import { plural } from '../../../utils/stringUtils.js'
import type { OptionWithDescription } from '../../CustomSelect/select.js'
import { Dialog } from '../../design-system/Dialog.js'
import { PermissionRuleDescription } from './PermissionRuleDescription.js'
export function optionForPermissionSaveDestination(
saveDestination: EditableSettingSource,
): OptionWithDescription {
switch (saveDestination) {
case 'localSettings':
return {
label: 'Project settings (local)',
description: `Saved in ${getRelativeSettingsFilePathForSource('localSettings')}`,
value: saveDestination
};
value: saveDestination,
}
case 'projectSettings':
return {
label: 'Project settings',
description: `Checked in at ${getRelativeSettingsFilePathForSource('projectSettings')}`,
value: saveDestination
};
value: saveDestination,
}
case 'userSettings':
return {
label: 'User settings',
description: `Saved in at ~/.claude/settings.json`,
value: saveDestination
};
}
}
type Props = {
onAddRules: (rules: PermissionRule[], unreachable?: UnreachableRule[]) => void;
onCancel: () => void;
ruleValues: PermissionRuleValue[];
ruleBehavior: PermissionBehavior;
initialContext: ToolPermissionContext;
setToolPermissionContext: (newContext: ToolPermissionContext) => void;
};
export function AddPermissionRules(t0) {
const $ = _c(26);
const {
onAddRules,
onCancel,
ruleValues,
ruleBehavior,
initialContext,
setToolPermissionContext
} = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = SOURCES.map(optionForPermissionSaveDestination);
$[0] = t1;
} else {
t1 = $[0];
}
const allOptions = t1;
let t2;
if ($[1] !== initialContext || $[2] !== onAddRules || $[3] !== onCancel || $[4] !== ruleBehavior || $[5] !== ruleValues || $[6] !== setToolPermissionContext) {
t2 = selectedValue => {
if (selectedValue === "cancel") {
onCancel();
return;
} else {
if ((SOURCES as readonly string[]).includes(selectedValue)) {
const destination = selectedValue as EditableSettingSource;
const updatedContext = applyPermissionUpdate(initialContext, {
type: "addRules",
rules: ruleValues,
behavior: ruleBehavior,
destination
});
persistPermissionUpdate({
type: "addRules",
rules: ruleValues,
behavior: ruleBehavior,
destination
});
setToolPermissionContext(updatedContext);
const rules = ruleValues.map(ruleValue => ({
ruleValue,
ruleBehavior,
source: destination
}));
const sandboxAutoAllowEnabled = SandboxManager.isSandboxingEnabled() && SandboxManager.isAutoAllowBashIfSandboxedEnabled();
const allUnreachable = detectUnreachableRules(updatedContext, {
sandboxAutoAllowEnabled
});
const newUnreachable = allUnreachable.filter(u => ruleValues.some(rv => rv.toolName === u.rule.ruleValue.toolName && rv.ruleContent === u.rule.ruleValue.ruleContent));
onAddRules(rules, newUnreachable.length > 0 ? newUnreachable : undefined);
}
value: saveDestination,
}
};
$[1] = initialContext;
$[2] = onAddRules;
$[3] = onCancel;
$[4] = ruleBehavior;
$[5] = ruleValues;
$[6] = setToolPermissionContext;
$[7] = t2;
} else {
t2 = $[7];
}
const onSelect = t2;
let t3;
if ($[8] !== ruleValues.length) {
t3 = plural(ruleValues.length, "rule");
$[8] = ruleValues.length;
$[9] = t3;
} else {
t3 = $[9];
}
const title = `Add ${ruleBehavior} permission ${t3}`;
let t4;
if ($[10] !== ruleValues) {
t4 = ruleValues.map(_temp);
$[10] = ruleValues;
$[11] = t4;
} else {
t4 = $[11];
}
let t5;
if ($[12] !== t4) {
t5 = <Box flexDirection="column" paddingX={2}>{t4}</Box>;
$[12] = t4;
$[13] = t5;
} else {
t5 = $[13];
}
const t6 = ruleValues.length === 1 ? "Where should this rule be saved?" : "Where should these rules be saved?";
let t7;
if ($[14] !== t6) {
t7 = <Text>{t6}</Text>;
$[14] = t6;
$[15] = t7;
} else {
t7 = $[15];
}
let t8;
if ($[16] !== onSelect) {
t8 = <Select options={allOptions} onChange={onSelect} />;
$[16] = onSelect;
$[17] = t8;
} else {
t8 = $[17];
}
let t9;
if ($[18] !== t7 || $[19] !== t8) {
t9 = <Box flexDirection="column" marginY={1}>{t7}{t8}</Box>;
$[18] = t7;
$[19] = t8;
$[20] = t9;
} else {
t9 = $[20];
}
let t10;
if ($[21] !== onCancel || $[22] !== t5 || $[23] !== t9 || $[24] !== title) {
t10 = <Dialog title={title} onCancel={onCancel} color="permission">{t5}{t9}</Dialog>;
$[21] = onCancel;
$[22] = t5;
$[23] = t9;
$[24] = title;
$[25] = t10;
} else {
t10 = $[25];
}
return t10;
}
function _temp(ruleValue_0) {
return <Box flexDirection="column" key={permissionRuleValueToString(ruleValue_0)}><Text bold={true}>{permissionRuleValueToString(ruleValue_0)}</Text><PermissionRuleDescription ruleValue={ruleValue_0} /></Box>;
type Props = {
onAddRules: (rules: PermissionRule[], unreachable?: UnreachableRule[]) => void
onCancel: () => void
ruleValues: PermissionRuleValue[]
ruleBehavior: PermissionBehavior
initialContext: ToolPermissionContext
setToolPermissionContext: (newContext: ToolPermissionContext) => void
}
export function AddPermissionRules({
onAddRules,
onCancel,
ruleValues,
ruleBehavior,
initialContext,
setToolPermissionContext,
}: Props): React.ReactNode {
const allOptions = SOURCES.map(optionForPermissionSaveDestination)
const onSelect = useCallback(
(selectedValue: string) => {
if (selectedValue === 'cancel') {
onCancel()
return
} else if ((SOURCES as readonly string[]).includes(selectedValue)) {
const destination = selectedValue as EditableSettingSource
const updatedContext = applyPermissionUpdate(initialContext, {
type: 'addRules',
rules: ruleValues,
behavior: ruleBehavior,
destination,
})
// Persist to settings
persistPermissionUpdate({
type: 'addRules',
rules: ruleValues,
behavior: ruleBehavior,
destination,
})
setToolPermissionContext(updatedContext)
const rules: PermissionRule[] = ruleValues.map(ruleValue => ({
ruleValue,
ruleBehavior,
source: destination,
}))
// Check for unreachable rules among the ones we just added
const sandboxAutoAllowEnabled =
SandboxManager.isSandboxingEnabled() &&
SandboxManager.isAutoAllowBashIfSandboxedEnabled()
const allUnreachable = detectUnreachableRules(updatedContext, {
sandboxAutoAllowEnabled,
})
// Filter to only rules we just added
const newUnreachable = allUnreachable.filter(u =>
ruleValues.some(
rv =>
rv.toolName === u.rule.ruleValue.toolName &&
rv.ruleContent === u.rule.ruleValue.ruleContent,
),
)
onAddRules(
rules,
newUnreachable.length > 0 ? newUnreachable : undefined,
)
}
},
[
onAddRules,
onCancel,
ruleValues,
ruleBehavior,
initialContext,
setToolPermissionContext,
],
)
const title = `Add ${ruleBehavior} permission ${plural(ruleValues.length, 'rule')}`
return (
<Dialog title={title} onCancel={onCancel} color="permission">
<Box flexDirection="column" paddingX={2}>
{ruleValues.map(ruleValue => (
<Box
flexDirection="column"
key={permissionRuleValueToString(ruleValue)}
>
<Text bold>{permissionRuleValueToString(ruleValue)}</Text>
<PermissionRuleDescription ruleValue={ruleValue} />
</Box>
))}
</Box>
<Box flexDirection="column" marginY={1}>
<Text>
{ruleValues.length === 1
? 'Where should this rule be saved?'
: 'Where should these rules be saved?'}
</Text>
<Select options={allOptions} onChange={onSelect} />
</Box>
</Dialog>
)
}

View File

@@ -1,339 +1,292 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDebounceCallback } from 'usehooks-ts';
import { addDirHelpMessage, validateDirectoryForWorkspace } from '../../../commands/add-dir/validation.js';
import TextInput from '../../../components/TextInput.js';
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js';
import { Box, Text } from '../../../ink.js';
import { useKeybinding } from '../../../keybindings/useKeybinding.js';
import type { ToolPermissionContext } from '../../../Tool.js';
import { getDirectoryCompletions } from '../../../utils/suggestions/directoryCompletion.js';
import { ConfigurableShortcutHint } from '../../ConfigurableShortcutHint.js';
import { Select } from '../../CustomSelect/select.js';
import { Byline } from '../../design-system/Byline.js';
import { Dialog } from '../../design-system/Dialog.js';
import { KeyboardShortcutHint } from '../../design-system/KeyboardShortcutHint.js';
import { PromptInputFooterSuggestions, type SuggestionItem } from '../../PromptInput/PromptInputFooterSuggestions.js';
import figures from 'figures'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useDebounceCallback } from 'usehooks-ts'
import {
addDirHelpMessage,
validateDirectoryForWorkspace,
} from '../../../commands/add-dir/validation.js'
import TextInput from '../../../components/TextInput.js'
import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js'
import { Box, Text } from '../../../ink.js'
import { useKeybinding } from '../../../keybindings/useKeybinding.js'
import type { ToolPermissionContext } from '../../../Tool.js'
import { getDirectoryCompletions } from '../../../utils/suggestions/directoryCompletion.js'
import { ConfigurableShortcutHint } from '../../ConfigurableShortcutHint.js'
import { Select } from '../../CustomSelect/select.js'
import { Byline } from '../../design-system/Byline.js'
import { Dialog } from '../../design-system/Dialog.js'
import { KeyboardShortcutHint } from '../../design-system/KeyboardShortcutHint.js'
import {
PromptInputFooterSuggestions,
type SuggestionItem,
} from '../../PromptInput/PromptInputFooterSuggestions.js'
type Props = {
onAddDirectory: (path: string, remember?: boolean) => void;
onCancel: () => void;
permissionContext: ToolPermissionContext;
directoryPath?: string; // When directoryPath is provided, show selection options instead of input
};
type RememberDirectoryOption = 'yes-session' | 'yes-remember' | 'no';
onAddDirectory: (path: string, remember?: boolean) => void
onCancel: () => void
permissionContext: ToolPermissionContext
directoryPath?: string // When directoryPath is provided, show selection options instead of input
}
type RememberDirectoryOption = 'yes-session' | 'yes-remember' | 'no'
const REMEMBER_DIRECTORY_OPTIONS: Array<{
value: RememberDirectoryOption;
label: string;
}> = [{
value: 'yes-session',
label: 'Yes, for this session'
}, {
value: 'yes-remember',
label: 'Yes, and remember this directory'
}, {
value: 'no',
label: 'No'
}];
function PermissionDescription() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
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>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
value: RememberDirectoryOption
label: string
}> = [
{
value: 'yes-session',
label: 'Yes, for this session',
},
{
value: 'yes-remember',
label: 'Yes, and remember this directory',
},
{
value: 'no',
label: 'No',
},
]
function PermissionDescription(): React.ReactNode {
return (
<Text dimColor>
Claude Code will be able to read files in this directory and make edits
when auto-accept edits is on.
</Text>
)
}
function DirectoryDisplay(t0) {
const $ = _c(5);
const {
path
} = t0;
let t1;
if ($[0] !== path) {
t1 = <Text color="permission">{path}</Text>;
$[0] = path;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <PermissionDescription />;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] !== t1) {
t3 = <Box flexDirection="column" paddingX={2} gap={1}>{t1}{t2}</Box>;
$[3] = t1;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
function DirectoryDisplay({ path }: { path: string }): React.ReactNode {
return (
<Box flexDirection="column" paddingX={2} gap={1}>
<Text color="permission">{path}</Text>
<PermissionDescription />
</Box>
)
}
function DirectoryInput(t0) {
const $ = _c(14);
const {
value,
onChange,
onSubmit,
error,
suggestions,
selectedSuggestion
} = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text>Enter the path to the directory:</Text>;
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== onChange || $[2] !== onSubmit || $[3] !== value) {
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>;
$[1] = onChange;
$[2] = onSubmit;
$[3] = value;
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[5] !== selectedSuggestion || $[6] !== suggestions) {
t3 = suggestions.length > 0 && <Box marginBottom={1}><PromptInputFooterSuggestions suggestions={suggestions} selectedSuggestion={selectedSuggestion} /></Box>;
$[5] = selectedSuggestion;
$[6] = suggestions;
$[7] = t3;
} else {
t3 = $[7];
}
let t4;
if ($[8] !== error) {
t4 = error && <Text color="error">{error}</Text>;
$[8] = error;
$[9] = t4;
} else {
t4 = $[9];
}
let t5;
if ($[10] !== t2 || $[11] !== t3 || $[12] !== t4) {
t5 = <Box flexDirection="column">{t1}{t2}{t3}{t4}</Box>;
$[10] = t2;
$[11] = t3;
$[12] = t4;
$[13] = t5;
} else {
t5 = $[13];
}
return t5;
function DirectoryInput({
value,
onChange,
onSubmit,
error,
suggestions,
selectedSuggestion,
}: {
value: string
onChange: (value: string) => void
onSubmit: (value: string) => void
error: string | null
suggestions: SuggestionItem[]
selectedSuggestion: number
}): React.ReactNode {
return (
<Box flexDirection="column">
<Text>Enter the path to the directory:</Text>
<Box borderDimColor borderStyle="round" marginY={1} paddingLeft={1}>
<TextInput
showCursor
placeholder={`Directory path${figures.ellipsis}`}
value={value}
onChange={onChange}
onSubmit={onSubmit}
columns={80}
cursorOffset={value.length}
onChangeCursorOffset={() => {}}
/>
</Box>
{suggestions.length > 0 && (
<Box marginBottom={1}>
<PromptInputFooterSuggestions
suggestions={suggestions}
selectedSuggestion={selectedSuggestion}
/>
</Box>
)}
{error && <Text color="error">{error}</Text>}
</Box>
)
}
function _temp() {}
export function AddWorkspaceDirectory(t0) {
const $ = _c(34);
const {
onAddDirectory,
onCancel,
permissionContext,
directoryPath
} = t0;
const [directoryInput, setDirectoryInput] = useState("");
const [error, setError] = useState(null);
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[0] = t1;
} else {
t1 = $[0];
}
const [suggestions, setSuggestions] = useState(t1);
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
let t2;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t2 = async path => {
if (!path) {
setSuggestions([]);
setSelectedSuggestion(0);
return;
}
const completions = await getDirectoryCompletions(path);
setSuggestions(completions);
setSelectedSuggestion(0);
};
$[1] = t2;
} else {
t2 = $[1];
}
const fetchSuggestions = t2;
const debouncedFetchSuggestions = useDebounceCallback(fetchSuggestions, 100);
let t3;
let t4;
if ($[2] !== debouncedFetchSuggestions || $[3] !== directoryInput) {
t3 = () => {
debouncedFetchSuggestions(directoryInput);
};
t4 = [directoryInput, debouncedFetchSuggestions];
$[2] = debouncedFetchSuggestions;
$[3] = directoryInput;
$[4] = t3;
$[5] = t4;
} else {
t3 = $[4];
t4 = $[5];
}
useEffect(t3, t4);
let t5;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t5 = suggestion => {
const newPath = suggestion.id + "/";
setDirectoryInput(newPath);
setError(null);
};
$[6] = t5;
} else {
t5 = $[6];
}
const applySuggestion = t5;
let t6;
if ($[7] !== onAddDirectory || $[8] !== permissionContext) {
t6 = async newPath_0 => {
const result = await validateDirectoryForWorkspace(newPath_0, permissionContext);
if (result.resultType === "success") {
onAddDirectory(result.absolutePath, false);
export function AddWorkspaceDirectory({
onAddDirectory,
onCancel,
permissionContext,
directoryPath,
}: Props): React.ReactNode {
const [directoryInput, setDirectoryInput] = useState('')
const [error, setError] = useState<string | null>(null)
const [suggestions, setSuggestions] = useState<SuggestionItem[]>([])
const [selectedSuggestion, setSelectedSuggestion] = useState(0)
const options = useMemo(() => REMEMBER_DIRECTORY_OPTIONS, [])
// Fetch directory completions
const fetchSuggestions = useCallback(async (path: string) => {
if (!path) {
setSuggestions([])
setSelectedSuggestion(0)
return
}
const completions = await getDirectoryCompletions(path)
setSuggestions(completions)
setSelectedSuggestion(0)
}, [])
const debouncedFetchSuggestions = useDebounceCallback(fetchSuggestions, 100)
useEffect(() => {
void debouncedFetchSuggestions(directoryInput)
}, [directoryInput, debouncedFetchSuggestions])
const applySuggestion = useCallback((suggestion: SuggestionItem) => {
const newPath = suggestion.id + '/'
setDirectoryInput(newPath)
setError(null)
// Suggestions will update via the useEffect
}, [])
// Handle directory submission from input
const handleSubmit = useCallback(
async (newPath: string) => {
const result = await validateDirectoryForWorkspace(
newPath,
permissionContext,
)
if (result.resultType === 'success') {
onAddDirectory(result.absolutePath, false)
} else {
setError(addDirHelpMessage(result));
setError(addDirHelpMessage(result))
}
};
$[7] = onAddDirectory;
$[8] = permissionContext;
$[9] = t6;
} else {
t6 = $[9];
}
const handleSubmit = t6;
let t7;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t7 = {
context: "Settings"
};
$[10] = t7;
} else {
t7 = $[10];
}
useKeybinding("confirm:no", onCancel, t7);
let t8;
if ($[11] !== handleSubmit || $[12] !== selectedSuggestion || $[13] !== suggestions) {
t8 = e => {
},
[permissionContext, onAddDirectory],
)
// Handle Esc to cancel (Ctrl+C handled by global keybindings)
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
useKeybinding('confirm:no', onCancel, { context: 'Settings' })
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (suggestions.length > 0) {
if (e.key === "tab") {
e.preventDefault();
const suggestion_0 = suggestions[selectedSuggestion];
if (suggestion_0) {
applySuggestion(suggestion_0);
// Tab: accept selected suggestion and continue (for drilling into subdirs)
if (e.key === 'tab') {
e.preventDefault()
const suggestion = suggestions[selectedSuggestion]
if (suggestion) {
applySuggestion(suggestion)
}
return;
return
}
if (e.key === "return") {
e.preventDefault();
const suggestion_1 = suggestions[selectedSuggestion];
if (suggestion_1) {
handleSubmit(suggestion_1.id + "/");
// Enter: apply selected suggestion and submit
if (e.key === 'return') {
e.preventDefault()
const suggestion = suggestions[selectedSuggestion]
if (suggestion) {
void handleSubmit(suggestion.id + '/')
}
return;
return
}
if (e.key === "up" || e.ctrl && e.key === "p") {
e.preventDefault();
setSelectedSuggestion(prev => prev <= 0 ? suggestions.length - 1 : prev - 1);
return;
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
e.preventDefault()
setSelectedSuggestion(prev =>
prev <= 0 ? suggestions.length - 1 : prev - 1,
)
return
}
if (e.key === "down" || e.ctrl && e.key === "n") {
e.preventDefault();
setSelectedSuggestion(prev_0 => prev_0 >= suggestions.length - 1 ? 0 : prev_0 + 1);
return;
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
e.preventDefault()
setSelectedSuggestion(prev =>
prev >= suggestions.length - 1 ? 0 : prev + 1,
)
return
}
}
};
$[11] = handleSubmit;
$[12] = selectedSuggestion;
$[13] = suggestions;
$[14] = t8;
} else {
t8 = $[14];
}
const handleKeyDown = t8;
let t9;
if ($[15] !== directoryPath || $[16] !== onAddDirectory || $[17] !== onCancel) {
t9 = value => {
if (!directoryPath) {
return;
},
[suggestions, selectedSuggestion, applySuggestion, handleSubmit],
)
const handleSelect = useCallback(
(value: string) => {
if (!directoryPath) return
const selectionValue = value as RememberDirectoryOption
switch (selectionValue) {
case 'yes-session':
onAddDirectory(directoryPath, false)
break
case 'yes-remember':
onAddDirectory(directoryPath, true)
break
case 'no':
onCancel()
break
}
const selectionValue = value as RememberDirectoryOption;
bb64: switch (selectionValue) {
case "yes-session":
{
onAddDirectory(directoryPath, false);
break bb64;
}
case "yes-remember":
{
onAddDirectory(directoryPath, true);
break bb64;
}
case "no":
{
onCancel();
}
}
};
$[15] = directoryPath;
$[16] = onAddDirectory;
$[17] = onCancel;
$[18] = t9;
} else {
t9 = $[18];
}
const handleSelect = t9;
const t10 = directoryPath ? undefined : _temp2;
let t11;
if ($[19] !== directoryInput || $[20] !== directoryPath || $[21] !== error || $[22] !== handleSelect || $[23] !== handleSubmit || $[24] !== selectedSuggestion || $[25] !== suggestions) {
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>;
$[19] = directoryInput;
$[20] = directoryPath;
$[21] = error;
$[22] = handleSelect;
$[23] = handleSubmit;
$[24] = selectedSuggestion;
$[25] = suggestions;
$[26] = t11;
} else {
t11 = $[26];
}
let t12;
if ($[27] !== onCancel || $[28] !== t10 || $[29] !== t11) {
t12 = <Dialog title="Add directory to workspace" onCancel={onCancel} color="permission" isCancelActive={false} inputGuide={t10}>{t11}</Dialog>;
$[27] = onCancel;
$[28] = t10;
$[29] = t11;
$[30] = t12;
} else {
t12 = $[30];
}
let t13;
if ($[31] !== handleKeyDown || $[32] !== t12) {
t13 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t12}</Box>;
$[31] = handleKeyDown;
$[32] = t12;
$[33] = t13;
} else {
t13 = $[33];
}
return t13;
}
function _temp2(exitState) {
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>;
},
[directoryPath, onAddDirectory, onCancel],
)
return (
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Dialog
title="Add directory to workspace"
onCancel={onCancel}
color="permission"
isCancelActive={false}
inputGuide={
directoryPath
? undefined
: exitState =>
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>
)
}
>
{directoryPath ? (
<Box flexDirection="column" gap={1}>
<DirectoryDisplay path={directoryPath} />
<Select
options={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>
)}
</Dialog>
</Box>
)
}

View File

@@ -1,75 +1,46 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { Text } from '../../../ink.js';
import { BashTool } from '../../../tools/BashTool/BashTool.js';
import type { PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js';
import * as React from 'react'
import { Text } from '../../../ink.js'
import { BashTool } from '../../../tools/BashTool/BashTool.js'
import type { PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js'
type RuleSubtitleProps = {
ruleValue: PermissionRuleValue;
};
export function PermissionRuleDescription(t0) {
const $ = _c(9);
const {
ruleValue
} = t0;
ruleValue: PermissionRuleValue
}
export function PermissionRuleDescription({
ruleValue,
}: RuleSubtitleProps): React.ReactNode {
switch (ruleValue.toolName) {
case BashTool.name:
{
if (ruleValue.ruleContent) {
if (ruleValue.ruleContent.endsWith(":*")) {
let t1;
if ($[0] !== ruleValue.ruleContent) {
t1 = ruleValue.ruleContent.slice(0, -2);
$[0] = ruleValue.ruleContent;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1) {
t2 = <Text dimColor={true}>Any Bash command starting with{" "}<Text bold={true}>{t1}</Text></Text>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
} else {
let t1;
if ($[4] !== ruleValue.ruleContent) {
t1 = <Text dimColor={true}>The Bash command <Text bold={true}>{ruleValue.ruleContent}</Text></Text>;
$[4] = ruleValue.ruleContent;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
case BashTool.name: {
if (ruleValue.ruleContent) {
if (ruleValue.ruleContent.endsWith(':*')) {
return (
<Text dimColor>
Any Bash command starting with{' '}
<Text bold>{ruleValue.ruleContent.slice(0, -2)}</Text>
</Text>
)
} else {
let t1;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text dimColor={true}>Any Bash command</Text>;
$[6] = t1;
} else {
t1 = $[6];
}
return t1;
return (
<Text dimColor>
The Bash command <Text bold>{ruleValue.ruleContent}</Text>
</Text>
)
}
} else {
return <Text dimColor>Any Bash command</Text>
}
default:
{
if (!ruleValue.ruleContent) {
let t1;
if ($[7] !== ruleValue.toolName) {
t1 = <Text dimColor={true}>Any use of the <Text bold={true}>{ruleValue.toolName}</Text> tool</Text>;
$[7] = ruleValue.toolName;
$[8] = t1;
} else {
t1 = $[8];
}
return t1;
} else {
return null;
}
}
default: {
if (!ruleValue.ruleContent) {
return (
<Text dimColor>
Any use of the <Text bold>{ruleValue.toolName}</Text> tool
</Text>
)
} else {
return null
}
}
}
}

View File

@@ -1,137 +1,107 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import * as React from 'react';
import { useState } from 'react';
import TextInput from '../../../components/TextInput.js';
import { useExitOnCtrlCDWithKeybindings } from '../../../hooks/useExitOnCtrlCDWithKeybindings.js';
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
import { Box, Newline, Text } from '../../../ink.js';
import { useKeybinding } from '../../../keybindings/useKeybinding.js';
import { BashTool } from '../../../tools/BashTool/BashTool.js';
import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js';
import type { PermissionBehavior, PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js';
import { permissionRuleValueFromString, permissionRuleValueToString } from '../../../utils/permissions/permissionRuleParser.js';
import figures from 'figures'
import * as React from 'react'
import { useState } from 'react'
import TextInput from '../../../components/TextInput.js'
import { useExitOnCtrlCDWithKeybindings } from '../../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
import { Box, Newline, Text } from '../../../ink.js'
import { useKeybinding } from '../../../keybindings/useKeybinding.js'
import { BashTool } from '../../../tools/BashTool/BashTool.js'
import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js'
import type {
PermissionBehavior,
PermissionRuleValue,
} from '../../../utils/permissions/PermissionRule.js'
import {
permissionRuleValueFromString,
permissionRuleValueToString,
} from '../../../utils/permissions/permissionRuleParser.js'
export type PermissionRuleInputProps = {
onCancel: () => void;
onSubmit: (ruleValue: PermissionRuleValue, ruleBehavior: PermissionBehavior) => void;
ruleBehavior: PermissionBehavior;
};
export function PermissionRuleInput(t0) {
const $ = _c(24);
const {
onCancel,
onSubmit,
ruleBehavior
} = t0;
const [inputValue, setInputValue] = useState("");
const [cursorOffset, setCursorOffset] = useState(0);
const exitState = useExitOnCtrlCDWithKeybindings();
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {
context: "Settings"
};
$[0] = t1;
} else {
t1 = $[0];
}
useKeybinding("confirm:no", onCancel, t1);
const {
columns
} = useTerminalSize();
const textInputColumns = columns - 6;
let t2;
if ($[1] !== onSubmit || $[2] !== ruleBehavior) {
t2 = value => {
const trimmedValue = value.trim();
if (trimmedValue.length === 0) {
return;
}
const ruleValue = permissionRuleValueFromString(trimmedValue);
onSubmit(ruleValue, ruleBehavior);
};
$[1] = onSubmit;
$[2] = ruleBehavior;
$[3] = t2;
} else {
t2 = $[3];
}
const handleSubmit = t2;
let t3;
if ($[4] !== ruleBehavior) {
t3 = <Text bold={true} color="permission">Add {ruleBehavior} permission rule</Text>;
$[4] = ruleBehavior;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Newline />;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
let t6;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text bold={true}>{permissionRuleValueToString({
toolName: WebFetchTool.name
})}</Text>;
t6 = <Text bold={false}> or </Text>;
$[7] = t5;
$[8] = t6;
} else {
t5 = $[7];
t6 = $[8];
}
let t7;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t7 = <Text>Permission rules are a tool name, optionally followed by a specifier in parentheses.{t4}e.g.,{" "}{t5}{t6}<Text bold={true}>{permissionRuleValueToString({
toolName: BashTool.name,
ruleContent: "ls:*"
})}</Text></Text>;
$[9] = t7;
} else {
t7 = $[9];
}
let t8;
if ($[10] !== cursorOffset || $[11] !== handleSubmit || $[12] !== inputValue || $[13] !== textInputColumns) {
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>;
$[10] = cursorOffset;
$[11] = handleSubmit;
$[12] = inputValue;
$[13] = textInputColumns;
$[14] = t8;
} else {
t8 = $[14];
}
let t9;
if ($[15] !== t3 || $[16] !== t8) {
t9 = <Box flexDirection="column" gap={1} borderStyle="round" paddingLeft={1} paddingRight={1} borderColor="permission">{t3}{t8}</Box>;
$[15] = t3;
$[16] = t8;
$[17] = t9;
} else {
t9 = $[17];
}
let t10;
if ($[18] !== exitState.keyName || $[19] !== exitState.pending) {
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>;
$[18] = exitState.keyName;
$[19] = exitState.pending;
$[20] = t10;
} else {
t10 = $[20];
}
let t11;
if ($[21] !== t10 || $[22] !== t9) {
t11 = <>{t9}{t10}</>;
$[21] = t10;
$[22] = t9;
$[23] = t11;
} else {
t11 = $[23];
}
return t11;
onCancel: () => void
onSubmit: (
ruleValue: PermissionRuleValue,
ruleBehavior: PermissionBehavior,
) => void
ruleBehavior: PermissionBehavior
}
export function PermissionRuleInput({
onCancel,
onSubmit,
ruleBehavior,
}: PermissionRuleInputProps): React.ReactNode {
const [inputValue, setInputValue] = useState('')
const [cursorOffset, setCursorOffset] = useState(0)
const exitState = useExitOnCtrlCDWithKeybindings()
// Use configurable keybinding for ESC to cancel
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
useKeybinding('confirm:no', onCancel, { context: 'Settings' })
const { columns } = useTerminalSize()
const textInputColumns = columns - 6
const handleSubmit = (value: string) => {
const trimmedValue = value.trim()
if (trimmedValue.length === 0) {
return
}
const ruleValue = permissionRuleValueFromString(trimmedValue)
onSubmit(ruleValue, ruleBehavior)
}
return (
<>
<Box
flexDirection="column"
gap={1}
borderStyle="round"
paddingLeft={1}
paddingRight={1}
borderColor="permission"
>
<Text bold color="permission">
Add {ruleBehavior} permission rule
</Text>
<Box flexDirection="column">
<Text>
Permission rules are a tool name, optionally followed by a specifier
in parentheses.
<Newline />
e.g.,{' '}
<Text bold>
{permissionRuleValueToString({ toolName: WebFetchTool.name })}
</Text>
<Text bold={false}> or </Text>
<Text bold>
{permissionRuleValueToString({
toolName: BashTool.name,
ruleContent: 'ls:*',
})}
</Text>
</Text>
<Box borderDimColor borderStyle="round" marginY={1} paddingLeft={1}>
<TextInput
showCursor
value={inputValue}
onChange={setInputValue}
onSubmit={handleSubmit}
placeholder={`Enter permission rule${figures.ellipsis}`}
columns={textInputColumns}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
/>
</Box>
</Box>
</Box>
<Box marginLeft={3}>
{exitState.pending ? (
<Text dimColor>Press {exitState.keyName} again to exit</Text>
) : (
<Text dimColor>Enter to submit · Esc to cancel</Text>
)}
</Box>
</>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,206 +1,118 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- 'r' is a view-specific key, not a global keybinding
import { Box, Text, useInput } from '../../../ink.js';
import { type AutoModeDenial, getAutoModeDenials } from '../../../utils/autoModeDenials.js';
import { Select } from '../../CustomSelect/select.js';
import { StatusIcon } from '../../design-system/StatusIcon.js';
import { useTabHeaderFocus } from '../../design-system/Tabs.js';
import { Box, Text, useInput } from '../../../ink.js'
import {
type AutoModeDenial,
getAutoModeDenials,
} from '../../../utils/autoModeDenials.js'
import { Select } from '../../CustomSelect/select.js'
import { StatusIcon } from '../../design-system/StatusIcon.js'
import { useTabHeaderFocus } from '../../design-system/Tabs.js'
type Props = {
onHeaderFocusChange?: (focused: boolean) => void;
onHeaderFocusChange?: (focused: boolean) => void
/** Called when approved/retry state changes so parent can act on exit */
onStateChange: (state: {
approved: Set<number>;
retry: Set<number>;
denials: readonly AutoModeDenial[];
}) => void;
};
export function RecentDenialsTab(t0) {
const $ = _c(30);
const {
onHeaderFocusChange,
onStateChange
} = t0;
const {
headerFocused,
focusHeader
} = useTabHeaderFocus();
let t1;
let t2;
if ($[0] !== headerFocused || $[1] !== onHeaderFocusChange) {
t1 = () => {
onHeaderFocusChange?.(headerFocused);
};
t2 = [headerFocused, onHeaderFocusChange];
$[0] = headerFocused;
$[1] = onHeaderFocusChange;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
const [denials] = useState(_temp);
const [approved, setApproved] = useState(_temp2);
const [retry, setRetry] = useState(_temp3);
const [focusedIdx, setFocusedIdx] = useState(0);
let t3;
let t4;
if ($[4] !== approved || $[5] !== denials || $[6] !== onStateChange || $[7] !== retry) {
t3 = () => {
onStateChange({
approved,
retry,
denials
});
};
t4 = [approved, retry, denials, onStateChange];
$[4] = approved;
$[5] = denials;
$[6] = onStateChange;
$[7] = retry;
$[8] = t3;
$[9] = t4;
} else {
t3 = $[8];
t4 = $[9];
}
useEffect(t3, t4);
let t5;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t5 = value => {
const idx = Number(value);
setApproved(prev => {
const next = new Set(prev);
if (next.has(idx)) {
next.delete(idx);
} else {
next.add(idx);
}
return next;
});
};
$[10] = t5;
} else {
t5 = $[10];
}
const handleSelect = t5;
let t6;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t6 = value_0 => {
setFocusedIdx(Number(value_0));
};
$[11] = t6;
} else {
t6 = $[11];
}
const handleFocus = t6;
let t7;
if ($[12] !== focusedIdx) {
t7 = (input, _key) => {
if (input === "r") {
setRetry(prev_0 => {
const next_0 = new Set(prev_0);
if (next_0.has(focusedIdx)) {
next_0.delete(focusedIdx);
} else {
next_0.add(focusedIdx);
}
return next_0;
});
setApproved(prev_1 => {
if (prev_1.has(focusedIdx)) {
return prev_1;
}
const next_1 = new Set(prev_1);
next_1.add(focusedIdx);
return next_1;
});
approved: Set<number>
retry: Set<number>
denials: readonly AutoModeDenial[]
}) => void
}
export function RecentDenialsTab({
onHeaderFocusChange,
onStateChange,
}: Props): React.ReactNode {
const { headerFocused, focusHeader } = useTabHeaderFocus()
useEffect(() => {
onHeaderFocusChange?.(headerFocused)
}, [headerFocused, onHeaderFocusChange])
// Snapshot on mount — approved/retry Sets key by index, and the live store
// prepends. A concurrent denial would shift all indices mid-edit.
const [denials] = useState(() => getAutoModeDenials())
const [approved, setApproved] = useState<Set<number>>(() => new Set())
const [retry, setRetry] = useState<Set<number>>(() => new Set())
const [focusedIdx, setFocusedIdx] = useState(0)
useEffect(() => {
onStateChange({ approved, retry, denials })
}, [approved, retry, denials, onStateChange])
const handleSelect = useCallback((value: string) => {
const idx = Number(value)
setApproved(prev => {
const next = new Set(prev)
if (next.has(idx)) next.delete(idx)
else next.add(idx)
return next
})
}, [])
const handleFocus = useCallback((value: string) => {
setFocusedIdx(Number(value))
}, [])
useInput(
(input, _key) => {
if (input === 'r') {
setRetry(prev => {
const next = new Set(prev)
if (next.has(focusedIdx)) next.delete(focusedIdx)
else next.add(focusedIdx)
return next
})
// Retry implies approve
setApproved(prev => {
if (prev.has(focusedIdx)) return prev
const next = new Set(prev)
next.add(focusedIdx)
return next
})
}
};
$[12] = focusedIdx;
$[13] = t7;
} else {
t7 = $[13];
}
const t8 = denials.length > 0;
let t9;
if ($[14] !== t8) {
t9 = {
isActive: t8
};
$[14] = t8;
$[15] = t9;
} else {
t9 = $[15];
}
useInput(t7, t9);
},
{ isActive: denials.length > 0 },
)
if (denials.length === 0) {
let t10;
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
t10 = <Text dimColor={true}>No recent denials. Commands denied by the auto mode classifier will appear here.</Text>;
$[16] = t10;
} else {
t10 = $[16];
return (
<Text dimColor>
No recent denials. Commands denied by the auto mode classifier will
appear here.
</Text>
)
}
const options = denials.map((d, idx) => {
const isApproved = approved.has(idx)
const suffix = retry.has(idx) ? ' (retry)' : ''
return {
label: (
<Text>
<StatusIcon status={isApproved ? 'success' : 'error'} withSpace />
{d.display}
<Text dimColor>{suffix}</Text>
</Text>
),
value: String(idx),
}
return t10;
}
let t10;
if ($[17] !== approved || $[18] !== denials || $[19] !== retry) {
let t11;
if ($[21] !== approved || $[22] !== retry) {
t11 = (d, idx_0) => {
const isApproved = approved.has(idx_0);
const suffix = retry.has(idx_0) ? " (retry)" : "";
return {
label: <Text><StatusIcon status={isApproved ? "success" : "error"} withSpace={true} />{d.display}<Text dimColor={true}>{suffix}</Text></Text>,
value: String(idx_0)
};
};
$[21] = approved;
$[22] = retry;
$[23] = t11;
} else {
t11 = $[23];
}
t10 = denials.map(t11);
$[17] = approved;
$[18] = denials;
$[19] = retry;
$[20] = t10;
} else {
t10 = $[20];
}
const options = t10;
let t11;
if ($[24] === Symbol.for("react.memo_cache_sentinel")) {
t11 = <Text>Commands recently denied by the auto mode classifier.</Text>;
$[24] = t11;
} else {
t11 = $[24];
}
const t12 = Math.min(10, options.length);
let t13;
if ($[25] !== focusHeader || $[26] !== headerFocused || $[27] !== options || $[28] !== t12) {
t13 = <Box flexDirection="column">{t11}<Box marginTop={1}><Select options={options} onChange={handleSelect} onFocus={handleFocus} visibleOptionCount={t12} isDisabled={headerFocused} onUpFromFirstItem={focusHeader} /></Box></Box>;
$[25] = focusHeader;
$[26] = headerFocused;
$[27] = options;
$[28] = t12;
$[29] = t13;
} else {
t13 = $[29];
}
return t13;
}
function _temp3() {
return new Set();
}
function _temp2() {
return new Set();
}
function _temp() {
return getAutoModeDenials();
})
return (
<Box flexDirection="column">
<Text>Commands recently denied by the auto mode classifier.</Text>
<Box marginTop={1}>
<Select
options={options}
onChange={handleSelect}
onFocus={handleFocus}
visibleOptionCount={Math.min(10, options.length)}
isDisabled={headerFocused}
onUpFromFirstItem={focusHeader}
/>
</Box>
</Box>
)
}

View File

@@ -1,109 +1,68 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { useCallback } from 'react';
import { Select } from '../../../components/CustomSelect/select.js';
import { Box, Text } from '../../../ink.js';
import type { ToolPermissionContext } from '../../../Tool.js';
import { applyPermissionUpdate } from '../../../utils/permissions/PermissionUpdate.js';
import { Dialog } from '../../design-system/Dialog.js';
import * as React from 'react'
import { useCallback } from 'react'
import { Select } from '../../../components/CustomSelect/select.js'
import { Box, Text } from '../../../ink.js'
import type { ToolPermissionContext } from '../../../Tool.js'
import { applyPermissionUpdate } from '../../../utils/permissions/PermissionUpdate.js'
import { Dialog } from '../../design-system/Dialog.js'
type Props = {
directoryPath: string;
onRemove: () => void;
onCancel: () => void;
permissionContext: ToolPermissionContext;
setPermissionContext: (context: ToolPermissionContext) => void;
};
export function RemoveWorkspaceDirectory(t0) {
const $ = _c(19);
const {
directoryPath,
onRemove,
onCancel,
permissionContext,
setPermissionContext
} = t0;
let t1;
if ($[0] !== directoryPath || $[1] !== onRemove || $[2] !== permissionContext || $[3] !== setPermissionContext) {
t1 = () => {
const updatedContext = applyPermissionUpdate(permissionContext, {
type: "removeDirectories",
directories: [directoryPath],
destination: "session"
});
setPermissionContext(updatedContext);
onRemove();
};
$[0] = directoryPath;
$[1] = onRemove;
$[2] = permissionContext;
$[3] = setPermissionContext;
$[4] = t1;
} else {
t1 = $[4];
}
const handleRemove = t1;
let t2;
if ($[5] !== handleRemove || $[6] !== onCancel) {
t2 = value => {
if (value === "yes") {
handleRemove();
} else {
onCancel();
}
};
$[5] = handleRemove;
$[6] = onCancel;
$[7] = t2;
} else {
t2 = $[7];
}
const handleSelect = t2;
let t3;
if ($[8] !== directoryPath) {
t3 = <Box marginX={2} flexDirection="column"><Text bold={true}>{directoryPath}</Text></Box>;
$[8] = directoryPath;
$[9] = t3;
} else {
t3 = $[9];
}
let t4;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Text>Claude Code will no longer have access to files in this directory.</Text>;
$[10] = t4;
} else {
t4 = $[10];
}
let t5;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t5 = [{
label: "Yes",
value: "yes"
}, {
label: "No",
value: "no"
}];
$[11] = t5;
} else {
t5 = $[11];
}
let t6;
if ($[12] !== handleSelect || $[13] !== onCancel) {
t6 = <Select onChange={handleSelect} onCancel={onCancel} options={t5} />;
$[12] = handleSelect;
$[13] = onCancel;
$[14] = t6;
} else {
t6 = $[14];
}
let t7;
if ($[15] !== onCancel || $[16] !== t3 || $[17] !== t6) {
t7 = <Dialog title="Remove directory from workspace?" onCancel={onCancel} color="error">{t3}{t4}{t6}</Dialog>;
$[15] = onCancel;
$[16] = t3;
$[17] = t6;
$[18] = t7;
} else {
t7 = $[18];
}
return t7;
directoryPath: string
onRemove: () => void
onCancel: () => void
permissionContext: ToolPermissionContext
setPermissionContext: (context: ToolPermissionContext) => void
}
export function RemoveWorkspaceDirectory({
directoryPath,
onRemove,
onCancel,
permissionContext,
setPermissionContext,
}: Props): React.ReactNode {
const handleRemove = useCallback(() => {
const updatedContext = applyPermissionUpdate(permissionContext, {
type: 'removeDirectories',
directories: [directoryPath],
destination: 'session',
})
setPermissionContext(updatedContext)
onRemove()
}, [directoryPath, permissionContext, setPermissionContext, onRemove])
const handleSelect = useCallback(
(value: string) => {
if (value === 'yes') {
handleRemove()
} else {
onCancel()
}
},
[handleRemove, onCancel],
)
return (
<Dialog
title="Remove directory from workspace?"
onCancel={onCancel}
color="error"
>
<Box marginX={2} flexDirection="column">
<Text bold>{directoryPath}</Text>
</Box>
<Text>
Claude Code will no longer have access to files in this directory.
</Text>
<Select
onChange={handleSelect}
onCancel={onCancel}
options={[
{ label: 'Yes', value: 'yes' },
{ label: 'No', value: 'no' },
]}
/>
</Dialog>
)
}

View File

@@ -1,149 +1,105 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import * as React from 'react';
import { useCallback, useEffect } from 'react';
import { getOriginalCwd } from '../../../bootstrap/state.js';
import type { CommandResultDisplay } from '../../../commands.js';
import { Select } from '../../../components/CustomSelect/select.js';
import { Box, Text } from '../../../ink.js';
import type { ToolPermissionContext } from '../../../Tool.js';
import { useTabHeaderFocus } from '../../design-system/Tabs.js';
import figures from 'figures'
import * as React from 'react'
import { useCallback, useEffect } from 'react'
import { getOriginalCwd } from '../../../bootstrap/state.js'
import type { CommandResultDisplay } from '../../../commands.js'
import { Select } from '../../../components/CustomSelect/select.js'
import { Box, Text } from '../../../ink.js'
import type { ToolPermissionContext } from '../../../Tool.js'
import { useTabHeaderFocus } from '../../design-system/Tabs.js'
type Props = {
onExit: (result?: string, options?: {
display?: CommandResultDisplay;
}) => void;
toolPermissionContext: ToolPermissionContext;
onRequestAddDirectory: () => void;
onRequestRemoveDirectory: (path: string) => void;
onHeaderFocusChange?: (focused: boolean) => void;
};
onExit: (
result?: string,
options?: { display?: CommandResultDisplay },
) => void
toolPermissionContext: ToolPermissionContext
onRequestAddDirectory: () => void
onRequestRemoveDirectory: (path: string) => void
onHeaderFocusChange?: (focused: boolean) => void
}
type DirectoryItem = {
path: string;
isCurrent: boolean;
isDeletable: boolean;
};
export function WorkspaceTab(t0) {
const $ = _c(23);
const {
onExit,
toolPermissionContext,
onRequestAddDirectory,
onRequestRemoveDirectory,
onHeaderFocusChange
} = t0;
const {
headerFocused,
focusHeader
} = useTabHeaderFocus();
let t1;
let t2;
if ($[0] !== headerFocused || $[1] !== onHeaderFocusChange) {
t1 = () => {
onHeaderFocusChange?.(headerFocused);
};
t2 = [headerFocused, onHeaderFocusChange];
$[0] = headerFocused;
$[1] = onHeaderFocusChange;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] !== toolPermissionContext.additionalWorkingDirectories) {
t3 = Array.from(toolPermissionContext.additionalWorkingDirectories.keys()).map(_temp);
$[4] = toolPermissionContext.additionalWorkingDirectories;
$[5] = t3;
} else {
t3 = $[5];
}
const additionalDirectories = t3;
let t4;
if ($[6] !== additionalDirectories || $[7] !== onRequestAddDirectory || $[8] !== onRequestRemoveDirectory) {
t4 = selectedValue => {
if (selectedValue === "add-directory") {
onRequestAddDirectory();
return;
path: string
isCurrent: boolean
isDeletable: boolean
}
export function WorkspaceTab({
onExit,
toolPermissionContext,
onRequestAddDirectory,
onRequestRemoveDirectory,
onHeaderFocusChange,
}: Props): React.ReactNode {
const { headerFocused, focusHeader } = useTabHeaderFocus()
useEffect(() => {
onHeaderFocusChange?.(headerFocused)
}, [headerFocused, onHeaderFocusChange])
// Get only additional workspace directories (not the current working directory)
const additionalDirectories = React.useMemo((): DirectoryItem[] => {
return Array.from(
toolPermissionContext.additionalWorkingDirectories.keys(),
).map(path => ({
path,
isCurrent: false,
isDeletable: true,
}))
}, [toolPermissionContext.additionalWorkingDirectories])
const handleDirectorySelect = useCallback(
(selectedValue: string) => {
if (selectedValue === 'add-directory') {
onRequestAddDirectory()
return
}
const directory = additionalDirectories.find(d => d.path === selectedValue);
const directory = additionalDirectories.find(
d => d.path === selectedValue,
)
if (directory && directory.isDeletable) {
onRequestRemoveDirectory(directory.path);
onRequestRemoveDirectory(directory.path)
}
};
$[6] = additionalDirectories;
$[7] = onRequestAddDirectory;
$[8] = onRequestRemoveDirectory;
$[9] = t4;
} else {
t4 = $[9];
}
const handleDirectorySelect = t4;
let t5;
if ($[10] !== onExit) {
t5 = () => onExit("Workspace dialog dismissed", {
display: "system"
});
$[10] = onExit;
$[11] = t5;
} else {
t5 = $[11];
}
const handleCancel = t5;
let opts;
if ($[12] !== additionalDirectories) {
opts = additionalDirectories.map(_temp2);
let t6;
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
t6 = {
label: `Add directory${figures.ellipsis}`,
value: "add-directory"
};
$[14] = t6;
} else {
t6 = $[14];
}
opts.push(t6);
$[12] = additionalDirectories;
$[13] = opts;
} else {
opts = $[13];
}
const options = opts;
let t6;
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
t6 = <Box flexDirection="row" marginTop={1} marginLeft={2} gap={1}><Text>{`- ${getOriginalCwd()}`}</Text><Text dimColor={true}>(Original working directory)</Text></Box>;
$[15] = t6;
} else {
t6 = $[15];
}
const t7 = Math.min(10, options.length);
let t8;
if ($[16] !== focusHeader || $[17] !== handleCancel || $[18] !== handleDirectorySelect || $[19] !== headerFocused || $[20] !== options || $[21] !== t7) {
t8 = <Box flexDirection="column" marginBottom={1}>{t6}<Select options={options} onChange={handleDirectorySelect} onCancel={handleCancel} visibleOptionCount={t7} onUpFromFirstItem={focusHeader} isDisabled={headerFocused} /></Box>;
$[16] = focusHeader;
$[17] = handleCancel;
$[18] = handleDirectorySelect;
$[19] = headerFocused;
$[20] = options;
$[21] = t7;
$[22] = t8;
} else {
t8 = $[22];
}
return t8;
}
function _temp2(dir) {
return {
label: dir.path,
value: dir.path
};
}
function _temp(path) {
return {
path,
isCurrent: false,
isDeletable: true
};
},
[additionalDirectories, onRequestAddDirectory, onRequestRemoveDirectory],
)
const handleCancel = useCallback(
() => onExit('Workspace dialog dismissed', { display: 'system' }),
[onExit],
)
// Main list view options
const options = React.useMemo(() => {
const opts = additionalDirectories.map(dir => ({
label: dir.path,
value: dir.path,
}))
opts.push({
label: `Add directory${figures.ellipsis}`,
value: 'add-directory',
})
return opts
}, [additionalDirectories])
// Main list view
return (
<Box flexDirection="column" marginBottom={1}>
{/* Current working directory section */}
<Box flexDirection="row" marginTop={1} marginLeft={2} gap={1}>
<Text>{`- ${getOriginalCwd()}`}</Text>
<Text dimColor>(Original working directory)</Text>
</Box>
<Select
options={options}
onChange={handleDirectorySelect}
onCancel={handleCancel}
visibleOptionCount={Math.min(10, options.length)}
onUpFromFirstItem={focusHeader}
isDisabled={headerFocused}
/>
</Box>
)
}

View File

@@ -1,59 +1,73 @@
import { basename, sep } from 'path';
import React, { type ReactNode } from 'react';
import { getOriginalCwd } from '../../bootstrap/state.js';
import { Text } from '../../ink.js';
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js';
import { permissionRuleExtractPrefix } from '../../utils/permissions/shellRuleMatching.js';
import { basename, sep } from 'path'
import React, { type ReactNode } from 'react'
import { getOriginalCwd } from '../../bootstrap/state.js'
import { Text } from '../../ink.js'
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
import { permissionRuleExtractPrefix } from '../../utils/permissions/shellRuleMatching.js'
function commandListDisplay(commands: string[]): ReactNode {
switch (commands.length) {
case 0:
return '';
return ''
case 1:
return <Text bold>{commands[0]}</Text>;
return <Text bold>{commands[0]}</Text>
case 2:
return <Text>
return (
<Text>
<Text bold>{commands[0]}</Text> and <Text bold>{commands[1]}</Text>
</Text>;
</Text>
)
default:
return <Text>
return (
<Text>
<Text bold>{commands.slice(0, -1).join(', ')}</Text>, and{' '}
<Text bold>{commands.slice(-1)[0]}</Text>
</Text>;
</Text>
)
}
}
function commandListDisplayTruncated(commands: string[]): ReactNode {
// Check if the plain text representation would be too long
const plainText = commands.join(', ');
const plainText = commands.join(', ')
if (plainText.length > 50) {
return 'similar';
return 'similar'
}
return commandListDisplay(commands);
return commandListDisplay(commands)
}
function formatPathList(paths: string[]): ReactNode {
if (paths.length === 0) return '';
if (paths.length === 0) return ''
// Extract directory names from paths
const names = paths.map(p => basename(p) || p);
const names = paths.map(p => basename(p) || p)
if (names.length === 1) {
return <Text>
return (
<Text>
<Text bold>{names[0]}</Text>
{sep}
</Text>;
</Text>
)
}
if (names.length === 2) {
return <Text>
return (
<Text>
<Text bold>{names[0]}</Text>
{sep} and <Text bold>{names[1]}</Text>
{sep}
</Text>;
</Text>
)
}
// For 3+, show first two with "and N more"
return <Text>
return (
<Text>
<Text bold>{names[0]}</Text>
{sep}, <Text bold>{names[1]}</Text>
{sep} and {paths.length - 2} more
</Text>;
</Text>
)
}
/**
@@ -62,102 +76,138 @@ function formatPathList(paths: string[]): ReactNode {
* and an optional command transform (e.g., Bash strips output redirections so
* filenames don't show as commands).
*/
export function generateShellSuggestionsLabel(suggestions: PermissionUpdate[], shellToolName: string, commandTransform?: (command: string) => string): ReactNode | null {
export function generateShellSuggestionsLabel(
suggestions: PermissionUpdate[],
shellToolName: string,
commandTransform?: (command: string) => string,
): ReactNode | null {
// Collect all rules for display
const allRules = suggestions.filter(s => s.type === 'addRules').flatMap(s => s.rules || []);
const allRules = suggestions
.filter(s => s.type === 'addRules')
.flatMap(s => s.rules || [])
// Separate Read rules from shell rules
const readRules = allRules.filter(r => r.toolName === 'Read');
const shellRules = allRules.filter(r => r.toolName === shellToolName);
const readRules = allRules.filter(r => r.toolName === 'Read')
const shellRules = allRules.filter(r => r.toolName === shellToolName)
// Get directory info
const directories = suggestions.filter(s => s.type === 'addDirectories').flatMap(s => s.directories || []);
const directories = suggestions
.filter(s => s.type === 'addDirectories')
.flatMap(s => s.directories || [])
// Extract paths from Read rules (keep separate from directories)
const readPaths = readRules.map(r => r.ruleContent?.replace('/**', '') || '').filter(p => p);
const readPaths = readRules
.map(r => r.ruleContent?.replace('/**', '') || '')
.filter(p => p)
// Extract shell command prefixes, optionally transforming for display
const shellCommands = [...new Set(shellRules.flatMap(rule => {
if (!rule.ruleContent) return [];
const command = permissionRuleExtractPrefix(rule.ruleContent) ?? rule.ruleContent;
return commandTransform ? commandTransform(command) : command;
}))];
const shellCommands = [
...new Set(
shellRules.flatMap(rule => {
if (!rule.ruleContent) return []
const command =
permissionRuleExtractPrefix(rule.ruleContent) ?? rule.ruleContent
return commandTransform ? commandTransform(command) : command
}),
),
]
// Check what we have
const hasDirectories = directories.length > 0;
const hasReadPaths = readPaths.length > 0;
const hasCommands = shellCommands.length > 0;
const hasDirectories = directories.length > 0
const hasReadPaths = readPaths.length > 0
const hasCommands = shellCommands.length > 0
// Handle single type cases
if (hasReadPaths && !hasDirectories && !hasCommands) {
// Only Read rules - use "reading from" language
if (readPaths.length === 1) {
const firstPath = readPaths[0]!;
const dirName = basename(firstPath) || firstPath;
return <Text>
const firstPath = readPaths[0]!
const dirName = basename(firstPath) || firstPath
return (
<Text>
Yes, allow reading from <Text bold>{dirName}</Text>
{sep} from this project
</Text>;
</Text>
)
}
// Multiple read paths
return <Text>
return (
<Text>
Yes, allow reading from {formatPathList(readPaths)} from this project
</Text>;
</Text>
)
}
if (hasDirectories && !hasReadPaths && !hasCommands) {
// Only directory permissions - use "access to" language
if (directories.length === 1) {
const firstDir = directories[0]!;
const dirName = basename(firstDir) || firstDir;
return <Text>
const firstDir = directories[0]!
const dirName = basename(firstDir) || firstDir
return (
<Text>
Yes, and always allow access to <Text bold>{dirName}</Text>
{sep} from this project
</Text>;
</Text>
)
}
// Multiple directories
return <Text>
return (
<Text>
Yes, and always allow access to {formatPathList(directories)} from this
project
</Text>;
</Text>
)
}
if (hasCommands && !hasDirectories && !hasReadPaths) {
// Only shell command permissions
return <Text>
return (
<Text>
{"Yes, and don't ask again for "}
{commandListDisplayTruncated(shellCommands)} commands in{' '}
<Text bold>{getOriginalCwd()}</Text>
</Text>;
</Text>
)
}
// Handle mixed cases
if ((hasDirectories || hasReadPaths) && !hasCommands) {
// Combine directories and read paths since they're both path access
const allPaths = [...directories, ...readPaths];
const allPaths = [...directories, ...readPaths]
if (hasDirectories && hasReadPaths) {
// Mixed - use generic "access to"
return <Text>
return (
<Text>
Yes, and always allow access to {formatPathList(allPaths)} from this
project
</Text>;
</Text>
)
}
}
if ((hasDirectories || hasReadPaths) && hasCommands) {
// Build descriptive message for both types
const allPaths = [...directories, ...readPaths];
const allPaths = [...directories, ...readPaths]
// Keep it concise but informative
if (allPaths.length === 1 && shellCommands.length === 1) {
return <Text>
return (
<Text>
Yes, and allow access to {formatPathList(allPaths)} and{' '}
{commandListDisplayTruncated(shellCommands)} commands
</Text>;
</Text>
)
}
return <Text>
return (
<Text>
Yes, and allow {formatPathList(allPaths)} access and{' '}
{commandListDisplayTruncated(shellCommands)} commands
</Text>;
</Text>
)
}
return null;
return null
}