更新大量 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,136 +1,226 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import * as React from 'react';
import { getAllowedChannels, getQuestionPreviewFormat } from 'src/bootstrap/state.js';
import { MessageResponse } from 'src/components/MessageResponse.js';
import { BLACK_CIRCLE } from 'src/constants/figures.js';
import { getModeColor } from 'src/utils/permissions/PermissionMode.js';
import { z } from 'zod/v4';
import { Box, Text } from '../../ink.js';
import type { Tool } from '../../Tool.js';
import { buildTool, type ToolDef } from '../../Tool.js';
import { lazySchema } from '../../utils/lazySchema.js';
import { ASK_USER_QUESTION_TOOL_CHIP_WIDTH, ASK_USER_QUESTION_TOOL_NAME, ASK_USER_QUESTION_TOOL_PROMPT, DESCRIPTION, PREVIEW_FEATURE_PROMPT } from './prompt.js';
const questionOptionSchema = lazySchema(() => z.object({
label: z.string().describe('The display text for this option that the user will see and select. Should be concise (1-5 words) and clearly describe the choice.'),
description: z.string().describe('Explanation of what this option means or what will happen if chosen. Useful for providing context about trade-offs or implications.'),
preview: z.string().optional().describe('Optional preview content rendered when this option is focused. Use for mockups, code snippets, or visual comparisons that help users compare options. See the tool description for the expected content format.')
}));
const questionSchema = lazySchema(() => z.object({
question: z.string().describe('The complete question to ask the user. Should be clear, specific, and end with a question mark. Example: "Which library should we use for date formatting?" If multiSelect is true, phrase it accordingly, e.g. "Which features do you want to enable?"'),
header: z.string().describe(`Very short label displayed as a chip/tag (max ${ASK_USER_QUESTION_TOOL_CHIP_WIDTH} chars). Examples: "Auth method", "Library", "Approach".`),
options: z.array(questionOptionSchema()).min(2).max(4).describe(`The available choices for this question. Must have 2-4 options. Each option should be a distinct, mutually exclusive choice (unless multiSelect is enabled). There should be no 'Other' option, that will be provided automatically.`),
multiSelect: z.boolean().default(false).describe('Set to true to allow the user to select multiple options instead of just one. Use when choices are not mutually exclusive.')
}));
import { feature } from 'bun:bundle'
import * as React from 'react'
import {
getAllowedChannels,
getQuestionPreviewFormat,
} from 'src/bootstrap/state.js'
import { MessageResponse } from 'src/components/MessageResponse.js'
import { BLACK_CIRCLE } from 'src/constants/figures.js'
import { getModeColor } from 'src/utils/permissions/PermissionMode.js'
import { z } from 'zod/v4'
import { Box, Text } from '../../ink.js'
import type { Tool } from '../../Tool.js'
import { buildTool, type ToolDef } from '../../Tool.js'
import { lazySchema } from '../../utils/lazySchema.js'
import {
ASK_USER_QUESTION_TOOL_CHIP_WIDTH,
ASK_USER_QUESTION_TOOL_NAME,
ASK_USER_QUESTION_TOOL_PROMPT,
DESCRIPTION,
PREVIEW_FEATURE_PROMPT,
} from './prompt.js'
const questionOptionSchema = lazySchema(() =>
z.object({
label: z
.string()
.describe(
'The display text for this option that the user will see and select. Should be concise (1-5 words) and clearly describe the choice.',
),
description: z
.string()
.describe(
'Explanation of what this option means or what will happen if chosen. Useful for providing context about trade-offs or implications.',
),
preview: z
.string()
.optional()
.describe(
'Optional preview content rendered when this option is focused. Use for mockups, code snippets, or visual comparisons that help users compare options. See the tool description for the expected content format.',
),
}),
)
const questionSchema = lazySchema(() =>
z.object({
question: z
.string()
.describe(
'The complete question to ask the user. Should be clear, specific, and end with a question mark. Example: "Which library should we use for date formatting?" If multiSelect is true, phrase it accordingly, e.g. "Which features do you want to enable?"',
),
header: z
.string()
.describe(
`Very short label displayed as a chip/tag (max ${ASK_USER_QUESTION_TOOL_CHIP_WIDTH} chars). Examples: "Auth method", "Library", "Approach".`,
),
options: z
.array(questionOptionSchema())
.min(2)
.max(4)
.describe(
`The available choices for this question. Must have 2-4 options. Each option should be a distinct, mutually exclusive choice (unless multiSelect is enabled). There should be no 'Other' option, that will be provided automatically.`,
),
multiSelect: z
.boolean()
.default(false)
.describe(
'Set to true to allow the user to select multiple options instead of just one. Use when choices are not mutually exclusive.',
),
}),
)
const annotationsSchema = lazySchema(() => {
const annotationSchema = z.object({
preview: z.string().optional().describe('The preview content of the selected option, if the question used previews.'),
notes: z.string().optional().describe('Free-text notes the user added to their selection.')
});
return z.record(z.string(), annotationSchema).optional().describe('Optional per-question annotations from the user (e.g., notes on preview selections). Keyed by question text.');
});
preview: z
.string()
.optional()
.describe(
'The preview content of the selected option, if the question used previews.',
),
notes: z
.string()
.optional()
.describe('Free-text notes the user added to their selection.'),
})
return z
.record(z.string(), annotationSchema)
.optional()
.describe(
'Optional per-question annotations from the user (e.g., notes on preview selections). Keyed by question text.',
)
})
const UNIQUENESS_REFINE = {
check: (data: {
questions: {
question: string;
options: {
label: string;
}[];
}[];
questions: { question: string; options: { label: string }[] }[]
}) => {
const questions = data.questions.map(q => q.question);
const questions = data.questions.map(q => q.question)
if (questions.length !== new Set(questions).size) {
return false;
return false
}
for (const question of data.questions) {
const labels = question.options.map(opt => opt.label);
const labels = question.options.map(opt => opt.label)
if (labels.length !== new Set(labels).size) {
return false;
return false
}
}
return true;
return true
},
message: 'Question texts must be unique, option labels must be unique within each question'
} as const;
message:
'Question texts must be unique, option labels must be unique within each question',
} as const
const commonFields = lazySchema(() => ({
answers: z.record(z.string(), z.string()).optional().describe('User answers collected by the permission component'),
answers: z
.record(z.string(), z.string())
.optional()
.describe('User answers collected by the permission component'),
annotations: annotationsSchema(),
metadata: z.object({
source: z.string().optional().describe('Optional identifier for the source of this question (e.g., "remember" for /remember command). Used for analytics tracking.')
}).optional().describe('Optional metadata for tracking and analytics purposes. Not displayed to user.')
}));
const inputSchema = lazySchema(() => z.strictObject({
questions: z.array(questionSchema()).min(1).max(4).describe('Questions to ask the user (1-4 questions)'),
...commonFields()
}).refine(UNIQUENESS_REFINE.check, {
message: UNIQUENESS_REFINE.message
}));
type InputSchema = ReturnType<typeof inputSchema>;
const outputSchema = lazySchema(() => z.object({
questions: z.array(questionSchema()).describe('The questions that were asked'),
answers: z.record(z.string(), z.string()).describe('The answers provided by the user (question text -> answer string; multi-select answers are comma-separated)'),
annotations: annotationsSchema()
}));
type OutputSchema = ReturnType<typeof outputSchema>;
metadata: z
.object({
source: z
.string()
.optional()
.describe(
'Optional identifier for the source of this question (e.g., "remember" for /remember command). Used for analytics tracking.',
),
})
.optional()
.describe(
'Optional metadata for tracking and analytics purposes. Not displayed to user.',
),
}))
const inputSchema = lazySchema(() =>
z
.strictObject({
questions: z
.array(questionSchema())
.min(1)
.max(4)
.describe('Questions to ask the user (1-4 questions)'),
...commonFields(),
})
.refine(UNIQUENESS_REFINE.check, {
message: UNIQUENESS_REFINE.message,
}),
)
type InputSchema = ReturnType<typeof inputSchema>
const outputSchema = lazySchema(() =>
z.object({
questions: z
.array(questionSchema())
.describe('The questions that were asked'),
answers: z
.record(z.string(), z.string())
.describe(
'The answers provided by the user (question text -> answer string; multi-select answers are comma-separated)',
),
annotations: annotationsSchema(),
}),
)
type OutputSchema = ReturnType<typeof outputSchema>
// SDK schemas are identical to internal schemas now that `preview` and
// `annotations` are public (configurable via `toolConfig.askUserQuestion`).
export const _sdkInputSchema = inputSchema;
export const _sdkOutputSchema = outputSchema;
export type Question = z.infer<ReturnType<typeof questionSchema>>;
export type QuestionOption = z.infer<ReturnType<typeof questionOptionSchema>>;
export type Output = z.infer<OutputSchema>;
function AskUserQuestionResultMessage(t0) {
const $ = _c(3);
const {
answers
} = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Box flexDirection="row"><Text color={getModeColor("default")}>{BLACK_CIRCLE} </Text><Text>User answered Claude's questions:</Text></Box>;
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== answers) {
t2 = <Box flexDirection="column" marginTop={1}>{t1}<MessageResponse><Box flexDirection="column">{Object.entries(answers).map(_temp)}</Box></MessageResponse></Box>;
$[1] = answers;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
}
function _temp(t0) {
const [questionText, answer] = t0;
return <Text key={questionText} color="inactive">· {questionText} → {answer}</Text>;
export const _sdkInputSchema = inputSchema
export const _sdkOutputSchema = outputSchema
export type Question = z.infer<ReturnType<typeof questionSchema>>
export type QuestionOption = z.infer<ReturnType<typeof questionOptionSchema>>
export type Output = z.infer<OutputSchema>
function AskUserQuestionResultMessage({
answers,
}: {
answers: Output['answers']
}): React.ReactNode {
return (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection="row">
<Text color={getModeColor('default')}>{BLACK_CIRCLE}&nbsp;</Text>
<Text>User answered Claude&apos;s questions:</Text>
</Box>
<MessageResponse>
<Box flexDirection="column">
{Object.entries(answers).map(([questionText, answer]) => (
<Text key={questionText} color="inactive">
· {questionText} {answer}
</Text>
))}
</Box>
</MessageResponse>
</Box>
)
}
export const AskUserQuestionTool: Tool<InputSchema, Output> = buildTool({
name: ASK_USER_QUESTION_TOOL_NAME,
searchHint: 'prompt the user with a multiple-choice question',
maxResultSizeChars: 100_000,
shouldDefer: true,
async description() {
return DESCRIPTION;
return DESCRIPTION
},
async prompt() {
const format = getQuestionPreviewFormat();
const format = getQuestionPreviewFormat()
if (format === undefined) {
// SDK consumer that hasn't opted into a preview format — omit preview
// guidance (they may not render the field at all).
return ASK_USER_QUESTION_TOOL_PROMPT;
return ASK_USER_QUESTION_TOOL_PROMPT
}
return ASK_USER_QUESTION_TOOL_PROMPT + PREVIEW_FEATURE_PROMPT[format];
return ASK_USER_QUESTION_TOOL_PROMPT + PREVIEW_FEATURE_PROMPT[format]
},
get inputSchema(): InputSchema {
return inputSchema();
return inputSchema()
},
get outputSchema(): OutputSchema {
return outputSchema();
return outputSchema()
},
userFacingName() {
return '';
return ''
},
isEnabled() {
// When --channels is active the user is likely on Telegram/Discord, not
@@ -138,128 +228,115 @@ export const AskUserQuestionTool: Tool<InputSchema, Output> = buildTool({
// the keyboard. Channel permission relay already skips
// requiresUserInteraction() tools (interactiveHandler.ts) so there's
// no alternate approval path.
if ((feature('KAIROS') || feature('KAIROS_CHANNELS')) && getAllowedChannels().length > 0) {
return false;
if (
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
getAllowedChannels().length > 0
) {
return false
}
return true;
return true
},
isConcurrencySafe() {
return true;
return true
},
isReadOnly() {
return true;
return true
},
toAutoClassifierInput(input) {
return input.questions.map(q => q.question).join(' | ');
return input.questions.map(q => q.question).join(' | ')
},
requiresUserInteraction() {
return true;
return true
},
async validateInput({
questions
}) {
async validateInput({ questions }) {
if (getQuestionPreviewFormat() !== 'html') {
return {
result: true
};
return { result: true }
}
for (const q of questions) {
for (const opt of q.options) {
const err = validateHtmlPreview(opt.preview);
const err = validateHtmlPreview(opt.preview)
if (err) {
return {
result: false,
message: `Option "${opt.label}" in question "${q.question}": ${err}`,
errorCode: 1
};
errorCode: 1,
}
}
}
}
return {
result: true
};
return { result: true }
},
async checkPermissions(input) {
return {
behavior: 'ask' as const,
message: 'Answer questions?',
updatedInput: input
};
updatedInput: input,
}
},
renderToolUseMessage() {
return null;
return null
},
renderToolUseProgressMessage() {
return null;
return null
},
renderToolResultMessage({
answers
}, _toolUseID) {
return <AskUserQuestionResultMessage answers={answers} />;
renderToolResultMessage({ answers }, _toolUseID) {
return <AskUserQuestionResultMessage answers={answers} />
},
renderToolUseRejectedMessage() {
return <Box flexDirection="row" marginTop={1}>
return (
<Box flexDirection="row" marginTop={1}>
<Text color={getModeColor('default')}>{BLACK_CIRCLE}&nbsp;</Text>
<Text>User declined to answer questions</Text>
</Box>;
</Box>
)
},
renderToolUseErrorMessage() {
return null;
return null
},
async call({
questions,
answers = {},
annotations
}, _context) {
async call({ questions, answers = {}, annotations }, _context) {
return {
data: {
questions,
answers,
...(annotations && {
annotations
})
}
};
data: { questions, answers, ...(annotations && { annotations }) },
}
},
mapToolResultToToolResultBlockParam({
answers,
annotations
}, toolUseID) {
const answersText = Object.entries(answers).map(([questionText, answer]) => {
const annotation = annotations?.[questionText];
const parts = [`"${questionText}"="${answer}"`];
if (annotation?.preview) {
parts.push(`selected preview:\n${annotation.preview}`);
}
if (annotation?.notes) {
parts.push(`user notes: ${annotation.notes}`);
}
return parts.join(' ');
}).join(', ');
mapToolResultToToolResultBlockParam({ answers, annotations }, toolUseID) {
const answersText = Object.entries(answers)
.map(([questionText, answer]) => {
const annotation = annotations?.[questionText]
const parts = [`"${questionText}"="${answer}"`]
if (annotation?.preview) {
parts.push(`selected preview:\n${annotation.preview}`)
}
if (annotation?.notes) {
parts.push(`user notes: ${annotation.notes}`)
}
return parts.join(' ')
})
.join(', ')
return {
type: 'tool_result',
content: `User has answered your questions: ${answersText}. You can now continue with the user's answers in mind.`,
tool_use_id: toolUseID
};
}
} satisfies ToolDef<InputSchema, Output>);
tool_use_id: toolUseID,
}
},
} satisfies ToolDef<InputSchema, Output>)
// Lightweight HTML fragment check. Not a parser — HTML5 parsers are
// error-recovering by spec and accept anything. We're checking model intent
// (did it emit HTML?) and catching the specific things we told it not to do.
function validateHtmlPreview(preview: string | undefined): string | null {
if (preview === undefined) return null;
if (preview === undefined) return null
if (/<\s*(html|body|!doctype)\b/i.test(preview)) {
return 'preview must be an HTML fragment, not a full document (no <html>, <body>, or <!DOCTYPE>)';
return 'preview must be an HTML fragment, not a full document (no <html>, <body>, or <!DOCTYPE>)'
}
// SDK consumers typically set this via innerHTML — disallow executable/style
// tags so a preview can't run code or restyle the host page. Inline event
// handlers (onclick etc.) are still possible; consumers should sanitize.
if (/<\s*(script|style)\b/i.test(preview)) {
return 'preview must not contain <script> or <style> tags. Use inline styles via the style attribute if needed.';
return 'preview must not contain <script> or <style> tags. Use inline styles via the style attribute if needed.'
}
if (!/<[a-z][^>]*>/i.test(preview)) {
return 'preview must contain HTML (previewFormat is set to "html"). Wrap content in a tag like <div> or <pre>.';
return 'preview must contain HTML (previewFormat is set to "html"). Wrap content in a tag like <div> or <pre>.'
}
return null;
return null
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,43 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { removeSandboxViolationTags } from 'src/utils/sandbox/sandbox-ui-utils.js';
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
import { MessageResponse } from '../../components/MessageResponse.js';
import { OutputLine } from '../../components/shell/OutputLine.js';
import { ShellTimeDisplay } from '../../components/shell/ShellTimeDisplay.js';
import { Box, Text } from '../../ink.js';
import type { Out as BashOut } from './BashTool.js';
import React from 'react'
import { removeSandboxViolationTags } from 'src/utils/sandbox/sandbox-ui-utils.js'
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { OutputLine } from '../../components/shell/OutputLine.js'
import { ShellTimeDisplay } from '../../components/shell/ShellTimeDisplay.js'
import { Box, Text } from '../../ink.js'
import type { Out as BashOut } from './BashTool.js'
type Props = {
content: Omit<BashOut, 'interrupted'>;
verbose: boolean;
timeoutMs?: number;
};
content: Omit<BashOut, 'interrupted'>
verbose: boolean
timeoutMs?: number
}
// Pattern to match "Shell cwd was reset to <path>" message
// Use (?:^|\n) to match either start of string or after a newline
const SHELL_CWD_RESET_PATTERN = /(?:^|\n)(Shell cwd was reset to .+)$/;
const SHELL_CWD_RESET_PATTERN = /(?:^|\n)(Shell cwd was reset to .+)$/
/**
* Extracts sandbox violations from stderr if present
* Returns both the cleaned stderr and the violations content
*/
function extractSandboxViolations(stderr: string): {
cleanedStderr: string;
cleanedStderr: string
} {
const violationsMatch = stderr.match(/<sandbox_violations>([\s\S]*?)<\/sandbox_violations>/);
const violationsMatch = stderr.match(
/<sandbox_violations>([\s\S]*?)<\/sandbox_violations>/,
)
if (!violationsMatch) {
return {
cleanedStderr: stderr
};
return { cleanedStderr: stderr }
}
// Remove the sandbox violations section from stderr
const cleanedStderr = removeSandboxViolationTags(stderr).trim();
const cleanedStderr = removeSandboxViolationTags(stderr).trim()
return {
cleanedStderr
};
cleanedStderr,
}
}
/**
@@ -43,148 +45,85 @@ function extractSandboxViolations(stderr: string): {
* Returns the cleaned stderr and the warning message separately
*/
function extractCwdResetWarning(stderr: string): {
cleanedStderr: string;
cwdResetWarning: string | null;
cleanedStderr: string
cwdResetWarning: string | null
} {
const match = stderr.match(SHELL_CWD_RESET_PATTERN);
const match = stderr.match(SHELL_CWD_RESET_PATTERN)
if (!match) {
return {
cleanedStderr: stderr,
cwdResetWarning: null
};
return { cleanedStderr: stderr, cwdResetWarning: null }
}
// Extract the warning message from capture group 1
const cwdResetWarning = match[1] ?? null;
const cwdResetWarning = match[1] ?? null
// Remove the warning from stderr (replace the full match)
const cleanedStderr = stderr.replace(SHELL_CWD_RESET_PATTERN, '').trim();
return {
cleanedStderr,
cwdResetWarning
};
const cleanedStderr = stderr.replace(SHELL_CWD_RESET_PATTERN, '').trim()
return { cleanedStderr, cwdResetWarning }
}
export default function BashToolResultMessage(t0) {
const $ = _c(34);
const {
content: t1,
verbose,
timeoutMs
} = t0;
const {
stdout: t2,
stderr: t3,
export default function BashToolResultMessage({
content: {
stdout = '',
stderr: stdErrWithViolations = '',
isImage,
returnCodeInterpretation,
noOutputExpected,
backgroundTaskId
} = t1;
const stdout = t2 === undefined ? "" : t2;
const stdErrWithViolations = t3 === undefined ? "" : t3;
let T0;
let cwdResetWarning;
let stderr;
let t4;
let t5;
let t6;
let t7;
if ($[0] !== isImage || $[1] !== stdErrWithViolations || $[2] !== stdout || $[3] !== verbose) {
t7 = Symbol.for("react.early_return_sentinel");
bb0: {
const {
cleanedStderr: stderrWithoutViolations
} = extractSandboxViolations(stdErrWithViolations);
({
cleanedStderr: stderr,
cwdResetWarning
} = extractCwdResetWarning(stderrWithoutViolations));
if (isImage) {
let t8;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t8 = <MessageResponse height={1}><Text dimColor={true}>[Image data detected and sent to Claude]</Text></MessageResponse>;
$[11] = t8;
} else {
t8 = $[11];
}
t7 = t8;
break bb0;
}
T0 = Box;
t4 = "column";
if ($[12] !== stdout || $[13] !== verbose) {
t5 = stdout !== "" ? <OutputLine content={stdout} verbose={verbose} /> : null;
$[12] = stdout;
$[13] = verbose;
$[14] = t5;
} else {
t5 = $[14];
}
t6 = stderr.trim() !== "" ? <OutputLine content={stderr} verbose={verbose} isError={true} /> : null;
}
$[0] = isImage;
$[1] = stdErrWithViolations;
$[2] = stdout;
$[3] = verbose;
$[4] = T0;
$[5] = cwdResetWarning;
$[6] = stderr;
$[7] = t4;
$[8] = t5;
$[9] = t6;
$[10] = t7;
} else {
T0 = $[4];
cwdResetWarning = $[5];
stderr = $[6];
t4 = $[7];
t5 = $[8];
t6 = $[9];
t7 = $[10];
backgroundTaskId,
},
verbose,
timeoutMs,
}: Props): React.ReactNode {
// Extract sandbox violations from stderr as it feels cleaner on the UI
// We want the model to see the violations, so it can explain what went wrong, and the
// user can access them in the violation logs
const { cleanedStderr: stderrWithoutViolations } =
extractSandboxViolations(stdErrWithViolations)
// Extract "Shell cwd was reset" warning to render it with warning color instead of error
const { cleanedStderr: stderr, cwdResetWarning } = extractCwdResetWarning(
stderrWithoutViolations,
)
// If this is an image, we don't want to truncate it in the UI
if (isImage) {
return (
<MessageResponse height={1}>
<Text dimColor>[Image data detected and sent to Claude]</Text>
</MessageResponse>
)
}
if (t7 !== Symbol.for("react.early_return_sentinel")) {
return t7;
}
let t8;
if ($[15] !== cwdResetWarning) {
t8 = cwdResetWarning ? <MessageResponse><Text dimColor={true}>{cwdResetWarning}</Text></MessageResponse> : null;
$[15] = cwdResetWarning;
$[16] = t8;
} else {
t8 = $[16];
}
let t9;
if ($[17] !== backgroundTaskId || $[18] !== cwdResetWarning || $[19] !== noOutputExpected || $[20] !== returnCodeInterpretation || $[21] !== stderr || $[22] !== stdout) {
t9 = stdout === "" && stderr.trim() === "" && !cwdResetWarning ? <MessageResponse height={1}><Text dimColor={true}>{backgroundTaskId ? <>Running in the background{" "}<KeyboardShortcutHint shortcut={"\u2193"} action="manage" parens={true} /></> : returnCodeInterpretation || (noOutputExpected ? "Done" : "(No output)")}</Text></MessageResponse> : null;
$[17] = backgroundTaskId;
$[18] = cwdResetWarning;
$[19] = noOutputExpected;
$[20] = returnCodeInterpretation;
$[21] = stderr;
$[22] = stdout;
$[23] = t9;
} else {
t9 = $[23];
}
let t10;
if ($[24] !== timeoutMs) {
t10 = timeoutMs && <MessageResponse><ShellTimeDisplay timeoutMs={timeoutMs} /></MessageResponse>;
$[24] = timeoutMs;
$[25] = t10;
} else {
t10 = $[25];
}
let t11;
if ($[26] !== T0 || $[27] !== t10 || $[28] !== t4 || $[29] !== t5 || $[30] !== t6 || $[31] !== t8 || $[32] !== t9) {
t11 = <T0 flexDirection={t4}>{t5}{t6}{t8}{t9}{t10}</T0>;
$[26] = T0;
$[27] = t10;
$[28] = t4;
$[29] = t5;
$[30] = t6;
$[31] = t8;
$[32] = t9;
$[33] = t11;
} else {
t11 = $[33];
}
return t11;
return (
<Box flexDirection="column">
{stdout !== '' ? <OutputLine content={stdout} verbose={verbose} /> : null}
{stderr.trim() !== '' ? (
<OutputLine content={stderr} verbose={verbose} isError />
) : null}
{cwdResetWarning ? (
<MessageResponse>
<Text dimColor>{cwdResetWarning}</Text>
</MessageResponse>
) : null}
{stdout === '' && stderr.trim() === '' && !cwdResetWarning ? (
<MessageResponse height={1}>
<Text dimColor>
{backgroundTaskId ? (
<>
Running in the background{' '}
<KeyboardShortcutHint shortcut="↓" action="manage" parens />
</>
) : (
returnCodeInterpretation ||
(noOutputExpected ? 'Done' : '(No output)')
)}
</Text>
</MessageResponse>
) : null}
{timeoutMs && (
<MessageResponse>
<ShellTimeDisplay timeoutMs={timeoutMs} />
</MessageResponse>
)}
</Box>
)
}

View File

@@ -1,184 +1,213 @@
import { c as _c } from "react/compiler-runtime";
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import * as React from 'react';
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js';
import { MessageResponse } from '../../components/MessageResponse.js';
import { ShellProgressMessage } from '../../components/shell/ShellProgressMessage.js';
import { Box, Text } from '../../ink.js';
import { useKeybinding } from '../../keybindings/useKeybinding.js';
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
import { useAppStateStore, useSetAppState } from '../../state/AppState.js';
import type { Tool } from '../../Tool.js';
import { backgroundAll } from '../../tasks/LocalShellTask/LocalShellTask.js';
import type { ProgressMessage } from '../../types/message.js';
import { env } from '../../utils/env.js';
import { isEnvTruthy } from '../../utils/envUtils.js';
import { getDisplayPath } from '../../utils/file.js';
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
import type { ThemeName } from '../../utils/theme.js';
import type { BashProgress, BashToolInput, Out } from './BashTool.js';
import BashToolResultMessage from './BashToolResultMessage.js';
import { extractBashCommentLabel } from './commentLabel.js';
import { parseSedEditCommand } from './sedEditParser.js';
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import * as React from 'react'
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { ShellProgressMessage } from '../../components/shell/ShellProgressMessage.js'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
import { useAppStateStore, useSetAppState } from '../../state/AppState.js'
import type { Tool } from '../../Tool.js'
import { backgroundAll } from '../../tasks/LocalShellTask/LocalShellTask.js'
import type { ProgressMessage } from '../../types/message.js'
import { env } from '../../utils/env.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { getDisplayPath } from '../../utils/file.js'
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
import type { ThemeName } from '../../utils/theme.js'
import type { BashProgress, BashToolInput, Out } from './BashTool.js'
import BashToolResultMessage from './BashToolResultMessage.js'
import { extractBashCommentLabel } from './commentLabel.js'
import { parseSedEditCommand } from './sedEditParser.js'
// Constants for command display
const MAX_COMMAND_DISPLAY_LINES = 2;
const MAX_COMMAND_DISPLAY_CHARS = 160;
const MAX_COMMAND_DISPLAY_LINES = 2
const MAX_COMMAND_DISPLAY_CHARS = 160
// Simple component to show background hint and handle ctrl+b
// When ctrl+b is pressed, backgrounds ALL running foreground commands
export function BackgroundHint(t0) {
const $ = _c(9);
let t1;
if ($[0] !== t0) {
t1 = t0 === undefined ? {} : t0;
$[0] = t0;
$[1] = t1;
} else {
t1 = $[1];
}
const {
onBackground
} = t1;
const store = useAppStateStore();
const setAppState = useSetAppState();
let t2;
if ($[2] !== onBackground || $[3] !== setAppState || $[4] !== store) {
t2 = () => {
backgroundAll(() => store.getState(), setAppState);
onBackground?.();
};
$[2] = onBackground;
$[3] = setAppState;
$[4] = store;
$[5] = t2;
} else {
t2 = $[5];
}
const handleBackground = t2;
let t3;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t3 = {
context: "Task"
};
$[6] = t3;
} else {
t3 = $[6];
}
useKeybinding("task:background", handleBackground, t3);
const baseShortcut = useShortcutDisplay("task:background", "Task", "ctrl+b");
const shortcut = env.terminal === "tmux" && baseShortcut === "ctrl+b" ? "ctrl+b ctrl+b (twice)" : baseShortcut;
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS)) {
return null;
}
let t4;
if ($[7] !== shortcut) {
t4 = <Box paddingLeft={5}><Text dimColor={true}><KeyboardShortcutHint shortcut={shortcut} action="run in background" parens={true} /></Text></Box>;
$[7] = shortcut;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
}
export function renderToolUseMessage(input: Partial<BashToolInput>, {
verbose,
theme: _theme
export function BackgroundHint({
onBackground,
}: {
verbose: boolean;
theme: ThemeName;
}): React.ReactNode {
const {
command
} = input;
onBackground?: () => void
} = {}): React.ReactElement | null {
const store = useAppStateStore()
const setAppState = useSetAppState()
// Handler for task:background - background all foreground tasks
const handleBackground = React.useCallback(() => {
// Background ALL foreground bash tasks
backgroundAll(() => store.getState(), setAppState)
// Also call the optional callback (used for non-bash tasks like agents)
onBackground?.()
}, [store, setAppState, onBackground])
useKeybinding('task:background', handleBackground, {
context: 'Task',
})
// Get the configured shortcut for task:background
const baseShortcut = useShortcutDisplay('task:background', 'Task', 'ctrl+b')
// In tmux, ctrl+b is the prefix key, so users need to press it twice to send ctrl+b
const shortcut =
env.terminal === 'tmux' && baseShortcut === 'ctrl+b'
? 'ctrl+b ctrl+b (twice)'
: baseShortcut
// Don't show background hint if background tasks are disabled
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS)) {
return null
}
return (
<Box paddingLeft={5}>
<Text dimColor>
<KeyboardShortcutHint
shortcut={shortcut}
action="run in background"
parens
/>
</Text>
</Box>
)
}
export function renderToolUseMessage(
input: Partial<BashToolInput>,
{ verbose, theme: _theme }: { verbose: boolean; theme: ThemeName },
): React.ReactNode {
const { command } = input
if (!command) {
return null;
return null
}
// Render sed in-place edits like file edits (show file path only)
const sedInfo = parseSedEditCommand(command);
const sedInfo = parseSedEditCommand(command)
if (sedInfo) {
return verbose ? sedInfo.filePath : getDisplayPath(sedInfo.filePath);
return verbose ? sedInfo.filePath : getDisplayPath(sedInfo.filePath)
}
if (!verbose) {
const lines = command.split('\n');
const lines = command.split('\n')
if (isFullscreenEnvEnabled()) {
const label = extractBashCommentLabel(command);
const label = extractBashCommentLabel(command)
if (label) {
return label.length > MAX_COMMAND_DISPLAY_CHARS ? label.slice(0, MAX_COMMAND_DISPLAY_CHARS) + '…' : label;
return label.length > MAX_COMMAND_DISPLAY_CHARS
? label.slice(0, MAX_COMMAND_DISPLAY_CHARS) + '…'
: label
}
}
const needsLineTruncation = lines.length > MAX_COMMAND_DISPLAY_LINES;
const needsCharTruncation = command.length > MAX_COMMAND_DISPLAY_CHARS;
const needsLineTruncation = lines.length > MAX_COMMAND_DISPLAY_LINES
const needsCharTruncation = command.length > MAX_COMMAND_DISPLAY_CHARS
if (needsLineTruncation || needsCharTruncation) {
let truncated = command;
let truncated = command
// First truncate by lines if needed
if (needsLineTruncation) {
truncated = lines.slice(0, MAX_COMMAND_DISPLAY_LINES).join('\n');
truncated = lines.slice(0, MAX_COMMAND_DISPLAY_LINES).join('\n')
}
// Then truncate by chars if still too long
if (truncated.length > MAX_COMMAND_DISPLAY_CHARS) {
truncated = truncated.slice(0, MAX_COMMAND_DISPLAY_CHARS);
truncated = truncated.slice(0, MAX_COMMAND_DISPLAY_CHARS)
}
return <Text>{truncated.trim()}</Text>;
return <Text>{truncated.trim()}</Text>
}
}
return command;
return command
}
export function renderToolUseProgressMessage(progressMessagesForMessage: ProgressMessage<BashProgress>[], {
verbose,
tools: _tools,
terminalSize: _terminalSize,
inProgressToolCallCount: _inProgressToolCallCount
}: {
tools: Tool[];
verbose: boolean;
terminalSize?: {
columns: number;
rows: number;
};
inProgressToolCallCount?: number;
}): React.ReactNode {
const lastProgress = progressMessagesForMessage.at(-1);
export function renderToolUseProgressMessage(
progressMessagesForMessage: ProgressMessage<BashProgress>[],
{
verbose,
tools: _tools,
terminalSize: _terminalSize,
inProgressToolCallCount: _inProgressToolCallCount,
}: {
tools: Tool[]
verbose: boolean
terminalSize?: { columns: number; rows: number }
inProgressToolCallCount?: number
},
): React.ReactNode {
const lastProgress = progressMessagesForMessage.at(-1)
if (!lastProgress || !lastProgress.data) {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>Running</Text>
</MessageResponse>;
</MessageResponse>
)
}
const data = lastProgress.data;
return <ShellProgressMessage fullOutput={data.fullOutput} output={data.output} elapsedTimeSeconds={data.elapsedTimeSeconds} totalLines={data.totalLines} totalBytes={data.totalBytes} timeoutMs={data.timeoutMs} taskId={data.taskId} verbose={verbose} />;
const data = lastProgress.data
return (
<ShellProgressMessage
fullOutput={data.fullOutput}
output={data.output}
elapsedTimeSeconds={data.elapsedTimeSeconds}
totalLines={data.totalLines}
totalBytes={data.totalBytes}
timeoutMs={data.timeoutMs}
taskId={data.taskId}
verbose={verbose}
/>
)
}
export function renderToolUseQueuedMessage(): React.ReactNode {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>Waiting</Text>
</MessageResponse>;
</MessageResponse>
)
}
export function renderToolResultMessage(content: Out, progressMessagesForMessage: ProgressMessage<BashProgress>[], {
verbose,
theme: _theme,
tools: _tools,
style: _style
}: {
verbose: boolean;
theme: ThemeName;
tools: Tool[];
style?: 'condensed';
}): React.ReactNode {
const lastProgress = progressMessagesForMessage.at(-1);
const timeoutMs = lastProgress?.data?.timeoutMs;
return <BashToolResultMessage content={content} verbose={verbose} timeoutMs={timeoutMs} />;
export function renderToolResultMessage(
content: Out,
progressMessagesForMessage: ProgressMessage<BashProgress>[],
{
verbose,
theme: _theme,
tools: _tools,
style: _style,
}: {
verbose: boolean
theme: ThemeName
tools: Tool[]
style?: 'condensed'
},
): React.ReactNode {
const lastProgress = progressMessagesForMessage.at(-1)
const timeoutMs = lastProgress?.data?.timeoutMs
return (
<BashToolResultMessage
content={content}
verbose={verbose}
timeoutMs={timeoutMs}
/>
)
}
export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], {
verbose,
progressMessagesForMessage: _progressMessagesForMessage,
tools: _tools
}: {
verbose: boolean;
progressMessagesForMessage: ProgressMessage<BashProgress>[];
tools: Tool[];
}): React.ReactNode {
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
{
verbose,
progressMessagesForMessage: _progressMessagesForMessage,
tools: _tools,
}: {
verbose: boolean
progressMessagesForMessage: ProgressMessage<BashProgress>[]
tools: Tool[]
},
): React.ReactNode {
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
}

View File

@@ -1,30 +1,36 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import React from 'react';
import { Markdown } from '../../components/Markdown.js';
import { BLACK_CIRCLE } from '../../constants/figures.js';
import { Box, Text } from '../../ink.js';
import type { ProgressMessage } from '../../types/message.js';
import { getDisplayPath } from '../../utils/file.js';
import { formatFileSize } from '../../utils/format.js';
import { formatBriefTimestamp } from '../../utils/formatBriefTimestamp.js';
import type { Output } from './BriefTool.js';
import figures from 'figures'
import React from 'react'
import { Markdown } from '../../components/Markdown.js'
import { BLACK_CIRCLE } from '../../constants/figures.js'
import { Box, Text } from '../../ink.js'
import type { ProgressMessage } from '../../types/message.js'
import { getDisplayPath } from '../../utils/file.js'
import { formatFileSize } from '../../utils/format.js'
import { formatBriefTimestamp } from '../../utils/formatBriefTimestamp.js'
import type { Output } from './BriefTool.js'
export function renderToolUseMessage(): React.ReactNode {
return '';
return ''
}
export function renderToolResultMessage(output: Output, _progressMessages: ProgressMessage[], options?: {
isTranscriptMode?: boolean;
isBriefOnly?: boolean;
}): React.ReactNode {
const hasAttachments = (output.attachments?.length ?? 0) > 0;
export function renderToolResultMessage(
output: Output,
_progressMessages: ProgressMessage[],
options?: {
isTranscriptMode?: boolean
isBriefOnly?: boolean
},
): React.ReactNode {
const hasAttachments = (output.attachments?.length ?? 0) > 0
if (!output.message && !hasAttachments) {
return null;
return null
}
// In transcript mode (ctrl+o), model text is NOT filtered — keep the ⏺ so
// SendUserMessage is visually distinct from the surrounding text blocks.
if (options?.isTranscriptMode) {
return <Box flexDirection="row" marginTop={1}>
return (
<Box flexDirection="row" marginTop={1}>
<Box minWidth={2}>
<Text color="text">{BLACK_CIRCLE}</Text>
</Box>
@@ -32,15 +38,17 @@ export function renderToolResultMessage(output: Output, _progressMessages: Progr
{output.message ? <Markdown>{output.message}</Markdown> : null}
<AttachmentList attachments={output.attachments} />
</Box>
</Box>;
</Box>
)
}
// Brief-only (chat) view: "Claude" label + 2-col indent, matching the "You"
// label UserPromptMessage applies to user input (#20889). The "N in background"
// spinner status lives in BriefSpinner (Spinner.tsx) — stateless label here.
if (options?.isBriefOnly) {
const ts = output.sentAt ? formatBriefTimestamp(output.sentAt) : '';
return <Box flexDirection="column" marginTop={1} paddingLeft={2}>
const ts = output.sentAt ? formatBriefTimestamp(output.sentAt) : ''
return (
<Box flexDirection="column" marginTop={1} paddingLeft={2}>
<Box flexDirection="row">
<Text color="briefLabelClaude">Claude</Text>
{ts ? <Text dimColor> {ts}</Text> : null}
@@ -49,7 +57,8 @@ export function renderToolResultMessage(output: Output, _progressMessages: Progr
{output.message ? <Markdown>{output.message}</Markdown> : null}
<AttachmentList attachments={output.attachments} />
</Box>
</Box>;
</Box>
)
}
// Default view: dropTextInBriefTurns (Messages.tsx) hides the redundant
@@ -58,43 +67,38 @@ export function renderToolResultMessage(output: Output, _progressMessages: Progr
// userFacingName() returns '' so UserToolSuccessMessage drops its columns-5
// width constraint and AssistantToolUseMessage renders null (no tool chrome).
// Empty minWidth={2} box mirrors AssistantTextMessage's ⏺ gutter spacing.
return <Box flexDirection="row" marginTop={1}>
return (
<Box flexDirection="row" marginTop={1}>
<Box minWidth={2} />
<Box flexDirection="column">
{output.message ? <Markdown>{output.message}</Markdown> : null}
<AttachmentList attachments={output.attachments} />
</Box>
</Box>;
</Box>
)
}
type AttachmentListProps = {
attachments: Output['attachments'];
};
export function AttachmentList(t0) {
const $ = _c(4);
const {
attachments
} = t0;
attachments: Output['attachments']
}
export function AttachmentList({
attachments,
}: AttachmentListProps): React.ReactNode {
if (!attachments || attachments.length === 0) {
return null;
return null
}
let t1;
if ($[0] !== attachments) {
t1 = attachments.map(_temp);
$[0] = attachments;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1) {
t2 = <Box flexDirection="column" marginTop={1}>{t1}</Box>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp(att) {
return <Box key={att.path} flexDirection="row"><Text dimColor={true}>{figures.pointerSmall} {att.isImage ? "[image]" : "[file]"}{" "}</Text><Text>{getDisplayPath(att.path)}</Text><Text dimColor={true}> ({formatFileSize(att.size)})</Text></Box>;
return (
<Box flexDirection="column" marginTop={1}>
{attachments.map(att => (
<Box key={att.path} flexDirection="row">
<Text dimColor>
{figures.pointerSmall} {att.isImage ? '[image]' : '[file]'}{' '}
</Text>
<Text>{getDisplayPath(att.path)}</Text>
<Text dimColor> ({formatFileSize(att.size)})</Text>
</Box>
))}
</Box>
)
}

View File

@@ -1,37 +1,48 @@
import React from 'react';
import { MessageResponse } from '../../components/MessageResponse.js';
import { Text } from '../../ink.js';
import { jsonStringify } from '../../utils/slowOperations.js';
import type { Input, Output } from './ConfigTool.js';
import React from 'react'
import { MessageResponse } from '../../components/MessageResponse.js'
import { Text } from '../../ink.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import type { Input, Output } from './ConfigTool.js'
export function renderToolUseMessage(input: Partial<Input>): React.ReactNode {
if (!input.setting) return null;
if (!input.setting) return null
if (input.value === undefined) {
return <Text dimColor>Getting {input.setting}</Text>;
return <Text dimColor>Getting {input.setting}</Text>
}
return <Text dimColor>
return (
<Text dimColor>
Setting {input.setting} to {jsonStringify(input.value)}
</Text>;
</Text>
)
}
export function renderToolResultMessage(content: Output): React.ReactNode {
if (!content.success) {
return <MessageResponse>
return (
<MessageResponse>
<Text color="error">Failed: {content.error}</Text>
</MessageResponse>;
</MessageResponse>
)
}
if (content.operation === 'get') {
return <MessageResponse>
return (
<MessageResponse>
<Text>
<Text bold>{content.setting}</Text> = {jsonStringify(content.value)}
</Text>
</MessageResponse>;
</MessageResponse>
)
}
return <MessageResponse>
return (
<MessageResponse>
<Text>
Set <Text bold>{content.setting}</Text> to{' '}
<Text bold>{jsonStringify(content.newValue)}</Text>
</Text>
</MessageResponse>;
</MessageResponse>
)
}
export function renderToolUseRejectedMessage(): React.ReactNode {
return <Text color="warning">Config change rejected</Text>;
return <Text color="warning">Config change rejected</Text>
}

View File

@@ -1,18 +1,23 @@
import * as React from 'react';
import { BLACK_CIRCLE } from 'src/constants/figures.js';
import { getModeColor } from 'src/utils/permissions/PermissionMode.js';
import { Box, Text } from '../../ink.js';
import type { ToolProgressData } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import type { ThemeName } from '../../utils/theme.js';
import type { Output } from './EnterPlanModeTool.js';
import * as React from 'react'
import { BLACK_CIRCLE } from 'src/constants/figures.js'
import { getModeColor } from 'src/utils/permissions/PermissionMode.js'
import { Box, Text } from '../../ink.js'
import type { ToolProgressData } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import type { ThemeName } from '../../utils/theme.js'
import type { Output } from './EnterPlanModeTool.js'
export function renderToolUseMessage(): React.ReactNode {
return null;
return null
}
export function renderToolResultMessage(_output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], _options: {
theme: ThemeName;
}): React.ReactNode {
return <Box flexDirection="column" marginTop={1}>
export function renderToolResultMessage(
_output: Output,
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
_options: { theme: ThemeName },
): React.ReactNode {
return (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection="row">
<Text color={getModeColor('plan')}>{BLACK_CIRCLE}</Text>
<Text> Entered plan mode</Text>
@@ -22,11 +27,15 @@ export function renderToolResultMessage(_output: Output, _progressMessagesForMes
Claude is now exploring and designing an implementation approach.
</Text>
</Box>
</Box>;
</Box>
)
}
export function renderToolUseRejectedMessage(): React.ReactNode {
return <Box flexDirection="row" marginTop={1}>
return (
<Box flexDirection="row" marginTop={1}>
<Text color={getModeColor('default')}>{BLACK_CIRCLE}</Text>
<Text> User declined to enter plan mode</Text>
</Box>;
</Box>
)
}

View File

@@ -1,19 +1,25 @@
import * as React from 'react';
import { Box, Text } from '../../ink.js';
import type { ToolProgressData } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import type { ThemeName } from '../../utils/theme.js';
import type { Output } from './EnterWorktreeTool.js';
import * as React from 'react'
import { Box, Text } from '../../ink.js'
import type { ToolProgressData } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import type { ThemeName } from '../../utils/theme.js'
import type { Output } from './EnterWorktreeTool.js'
export function renderToolUseMessage(): React.ReactNode {
return 'Creating worktree…';
return 'Creating worktree…'
}
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], _options: {
theme: ThemeName;
}): React.ReactNode {
return <Box flexDirection="column">
export function renderToolResultMessage(
output: Output,
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
_options: { theme: ThemeName },
): React.ReactNode {
return (
<Box flexDirection="column">
<Text>
Switched to worktree on branch <Text bold>{output.worktreeBranch}</Text>
</Text>
<Text dimColor>{output.worktreePath}</Text>
</Box>;
</Box>
)
}

View File

@@ -1,45 +1,47 @@
import * as React from 'react';
import { Markdown } from 'src/components/Markdown.js';
import { MessageResponse } from 'src/components/MessageResponse.js';
import { RejectedPlanMessage } from 'src/components/messages/UserToolResultMessage/RejectedPlanMessage.js';
import { BLACK_CIRCLE } from 'src/constants/figures.js';
import { getModeColor } from 'src/utils/permissions/PermissionMode.js';
import { Box, Text } from '../../ink.js';
import type { ToolProgressData } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import { getDisplayPath } from '../../utils/file.js';
import { getPlan } from '../../utils/plans.js';
import type { ThemeName } from '../../utils/theme.js';
import type { Output } from './ExitPlanModeV2Tool.js';
import * as React from 'react'
import { Markdown } from 'src/components/Markdown.js'
import { MessageResponse } from 'src/components/MessageResponse.js'
import { RejectedPlanMessage } from 'src/components/messages/UserToolResultMessage/RejectedPlanMessage.js'
import { BLACK_CIRCLE } from 'src/constants/figures.js'
import { getModeColor } from 'src/utils/permissions/PermissionMode.js'
import { Box, Text } from '../../ink.js'
import type { ToolProgressData } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import { getDisplayPath } from '../../utils/file.js'
import { getPlan } from '../../utils/plans.js'
import type { ThemeName } from '../../utils/theme.js'
import type { Output } from './ExitPlanModeV2Tool.js'
export function renderToolUseMessage(): React.ReactNode {
return null;
return null
}
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], {
theme: _theme
}: {
theme: ThemeName;
}): React.ReactNode {
const {
plan,
filePath
} = output;
const isEmpty = !plan || plan.trim() === '';
const displayPath = filePath ? getDisplayPath(filePath) : '';
const awaitingLeaderApproval = output.awaitingLeaderApproval;
export function renderToolResultMessage(
output: Output,
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
{ theme: _theme }: { theme: ThemeName },
): React.ReactNode {
const { plan, filePath } = output
const isEmpty = !plan || plan.trim() === ''
const displayPath = filePath ? getDisplayPath(filePath) : ''
const awaitingLeaderApproval = output.awaitingLeaderApproval
// Simplified message for empty plans
if (isEmpty) {
return <Box flexDirection="column" marginTop={1}>
return (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection="row">
<Text color={getModeColor('plan')}>{BLACK_CIRCLE}</Text>
<Text> Exited plan mode</Text>
</Box>
</Box>;
</Box>
)
}
// When awaiting leader approval, show a different message
if (awaitingLeaderApproval) {
return <Box flexDirection="column" marginTop={1}>
return (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection="row">
<Text color={getModeColor('plan')}>{BLACK_CIRCLE}</Text>
<Text> Plan submitted for team lead approval</Text>
@@ -50,32 +52,37 @@ export function renderToolResultMessage(output: Output, _progressMessagesForMess
<Text dimColor>Waiting for team lead to review and approve...</Text>
</Box>
</MessageResponse>
</Box>;
</Box>
)
}
return <Box flexDirection="column" marginTop={1}>
return (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection="row">
<Text color={getModeColor('plan')}>{BLACK_CIRCLE}</Text>
<Text> User approved Claude&apos;s plan</Text>
</Box>
<MessageResponse>
<Box flexDirection="column">
{filePath && <Text dimColor>Plan saved to: {displayPath} · /plan to edit</Text>}
{filePath && (
<Text dimColor>Plan saved to: {displayPath} · /plan to edit</Text>
)}
<Markdown>{plan}</Markdown>
</Box>
</MessageResponse>
</Box>;
</Box>
)
}
export function renderToolUseRejectedMessage({
plan
}: {
plan?: string;
}, {
theme: _theme
}: {
theme: ThemeName;
}): React.ReactNode {
const planContent = plan ?? getPlan() ?? 'No plan found';
return <Box flexDirection="column">
export function renderToolUseRejectedMessage(
{ plan }: { plan?: string },
{ theme: _theme }: { theme: ThemeName },
): React.ReactNode {
const planContent = plan ?? getPlan() ?? 'No plan found'
return (
<Box flexDirection="column">
<RejectedPlanMessage plan={planContent} />
</Box>;
</Box>
)
}

View File

@@ -1,24 +1,33 @@
import * as React from 'react';
import { Box, Text } from '../../ink.js';
import type { ToolProgressData } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import type { ThemeName } from '../../utils/theme.js';
import type { Output } from './ExitWorktreeTool.js';
import * as React from 'react'
import { Box, Text } from '../../ink.js'
import type { ToolProgressData } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import type { ThemeName } from '../../utils/theme.js'
import type { Output } from './ExitWorktreeTool.js'
export function renderToolUseMessage(): React.ReactNode {
return 'Exiting worktree…';
return 'Exiting worktree…'
}
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], _options: {
theme: ThemeName;
}): React.ReactNode {
const actionLabel = output.action === 'keep' ? 'Kept worktree' : 'Removed worktree';
return <Box flexDirection="column">
export function renderToolResultMessage(
output: Output,
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
_options: { theme: ThemeName },
): React.ReactNode {
const actionLabel =
output.action === 'keep' ? 'Kept worktree' : 'Removed worktree'
return (
<Box flexDirection="column">
<Text>
{actionLabel}
{output.worktreeBranch ? <>
{output.worktreeBranch ? (
<>
{' '}
(branch <Text bold>{output.worktreeBranch}</Text>)
</> : null}
</>
) : null}
</Text>
<Text dimColor>Returned to {output.originalCwd}</Text>
</Box>;
</Box>
)
}

View File

@@ -1,288 +1,322 @@
import { c as _c } from "react/compiler-runtime";
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import type { StructuredPatchHunk } from 'diff';
import * as React from 'react';
import { Suspense, use, useState } from 'react';
import { FileEditToolUseRejectedMessage } from 'src/components/FileEditToolUseRejectedMessage.js';
import { MessageResponse } from 'src/components/MessageResponse.js';
import { extractTag } from 'src/utils/messages.js';
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js';
import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage.js';
import { FilePathLink } from '../../components/FilePathLink.js';
import { Text } from '../../ink.js';
import type { Tools } from '../../Tool.js';
import type { Message, ProgressMessage } from '../../types/message.js';
import { adjustHunkLineNumbers, CONTEXT_LINES } from '../../utils/diff.js';
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js';
import { logError } from '../../utils/log.js';
import { getPlansDirectory } from '../../utils/plans.js';
import { readEditContext } from '../../utils/readEditContext.js';
import { firstLineOf } from '../../utils/stringUtils.js';
import type { ThemeName } from '../../utils/theme.js';
import type { FileEditOutput } from './types.js';
import { findActualString, getPatchForEdit, preserveQuoteStyle } from './utils.js';
export function userFacingName(input: Partial<{
file_path: string;
old_string: string;
new_string: string;
replace_all: boolean;
edits: unknown[];
}> | undefined): string {
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import type { StructuredPatchHunk } from 'diff'
import * as React from 'react'
import { Suspense, use, useState } from 'react'
import { FileEditToolUseRejectedMessage } from 'src/components/FileEditToolUseRejectedMessage.js'
import { MessageResponse } from 'src/components/MessageResponse.js'
import { extractTag } from 'src/utils/messages.js'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage.js'
import { FilePathLink } from '../../components/FilePathLink.js'
import { Text } from '../../ink.js'
import type { Tools } from '../../Tool.js'
import type { Message, ProgressMessage } from '../../types/message.js'
import { adjustHunkLineNumbers, CONTEXT_LINES } from '../../utils/diff.js'
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js'
import { logError } from '../../utils/log.js'
import { getPlansDirectory } from '../../utils/plans.js'
import { readEditContext } from '../../utils/readEditContext.js'
import { firstLineOf } from '../../utils/stringUtils.js'
import type { ThemeName } from '../../utils/theme.js'
import type { FileEditOutput } from './types.js'
import {
findActualString,
getPatchForEdit,
preserveQuoteStyle,
} from './utils.js'
export function userFacingName(
input:
| Partial<{
file_path: string
old_string: string
new_string: string
replace_all: boolean
edits: unknown[]
}>
| undefined,
): string {
if (!input) {
return 'Update';
return 'Update'
}
if (input.file_path?.startsWith(getPlansDirectory())) {
return 'Updated plan';
return 'Updated plan'
}
// Hashline edits always modify an existing file (line-ref based)
if (input.edits != null) {
return 'Update';
return 'Update'
}
if (input.old_string === '') {
return 'Create';
return 'Create'
}
return 'Update';
return 'Update'
}
export function getToolUseSummary(input: Partial<{
file_path: string;
old_string: string;
new_string: string;
replace_all: boolean;
}> | undefined): string | null {
export function getToolUseSummary(
input:
| Partial<{
file_path: string
old_string: string
new_string: string
replace_all: boolean
}>
| undefined,
): string | null {
if (!input?.file_path) {
return null;
return null
}
return getDisplayPath(input.file_path);
return getDisplayPath(input.file_path)
}
export function renderToolUseMessage({
file_path
}: {
file_path?: string;
}, {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
export function renderToolUseMessage(
{ file_path }: { file_path?: string },
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!file_path) {
return null;
return null
}
// For plan files, path is already in userFacingName
if (file_path.startsWith(getPlansDirectory())) {
return '';
return ''
}
return <FilePathLink filePath={file_path}>
return (
<FilePathLink filePath={file_path}>
{verbose ? file_path : getDisplayPath(file_path)}
</FilePathLink>;
</FilePathLink>
)
}
export function renderToolResultMessage({
filePath,
structuredPatch,
originalFile
}: FileEditOutput, _progressMessagesForMessage: ProgressMessage[], {
style,
verbose
}: {
style?: 'condensed';
verbose: boolean;
}): React.ReactNode {
export function renderToolResultMessage(
{ filePath, structuredPatch, originalFile }: FileEditOutput,
_progressMessagesForMessage: ProgressMessage[],
{ style, verbose }: { style?: 'condensed'; verbose: boolean },
): React.ReactNode {
// For plan files, show /plan hint above the diff
const isPlanFile = filePath.startsWith(getPlansDirectory());
return <FileEditToolUpdatedMessage filePath={filePath} structuredPatch={structuredPatch} firstLine={originalFile.split('\n')[0] ?? null} fileContent={originalFile} style={style} verbose={verbose} previewHint={isPlanFile ? '/plan to preview' : undefined} />;
const isPlanFile = filePath.startsWith(getPlansDirectory())
return (
<FileEditToolUpdatedMessage
filePath={filePath}
structuredPatch={structuredPatch}
firstLine={originalFile.split('\n')[0] ?? null}
fileContent={originalFile}
style={style}
verbose={verbose}
previewHint={isPlanFile ? '/plan to preview' : undefined}
/>
)
}
export function renderToolUseRejectedMessage(input: {
file_path: string;
old_string?: string;
new_string?: string;
replace_all?: boolean;
edits?: unknown[];
}, options: {
columns: number;
messages: Message[];
progressMessagesForMessage: ProgressMessage[];
style?: 'condensed';
theme: ThemeName;
tools: Tools;
verbose: boolean;
}): React.ReactElement {
const {
style,
verbose
} = options;
const filePath = input.file_path;
const oldString = input.old_string ?? '';
const newString = input.new_string ?? '';
const replaceAll = input.replace_all ?? false;
export function renderToolUseRejectedMessage(
input: {
file_path: string
old_string?: string
new_string?: string
replace_all?: boolean
edits?: unknown[]
},
options: {
columns: number
messages: Message[]
progressMessagesForMessage: ProgressMessage[]
style?: 'condensed'
theme: ThemeName
tools: Tools
verbose: boolean
},
): React.ReactElement {
const { style, verbose } = options
const filePath = input.file_path
const oldString = input.old_string ?? ''
const newString = input.new_string ?? ''
const replaceAll = input.replace_all ?? false
// Defensive: if input has an unexpected shape, show a simple rejection message
if ('edits' in input && input.edits != null) {
return <FileEditToolUseRejectedMessage file_path={filePath} operation="update" firstLine={null} verbose={verbose} />;
return (
<FileEditToolUseRejectedMessage
file_path={filePath}
operation="update"
firstLine={null}
verbose={verbose}
/>
)
}
const isNewFile = oldString === '';
const isNewFile = oldString === ''
// For new file creation, show content preview instead of diff
if (isNewFile) {
return <FileEditToolUseRejectedMessage file_path={filePath} operation="write" content={newString} firstLine={firstLineOf(newString)} verbose={verbose} />;
return (
<FileEditToolUseRejectedMessage
file_path={filePath}
operation="write"
content={newString}
firstLine={firstLineOf(newString)}
verbose={verbose}
/>
)
}
return <EditRejectionDiff filePath={filePath} oldString={oldString} newString={newString} replaceAll={replaceAll} style={style} verbose={verbose} />;
return (
<EditRejectionDiff
filePath={filePath}
oldString={oldString}
newString={newString}
replaceAll={replaceAll}
style={style}
verbose={verbose}
/>
)
}
export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], options: {
progressMessagesForMessage: ProgressMessage[];
tools: Tools;
verbose: boolean;
}): React.ReactElement {
const {
verbose
} = options;
if (!verbose && typeof result === 'string' && extractTag(result, 'tool_use_error')) {
const errorMessage = extractTag(result, 'tool_use_error');
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
options: {
progressMessagesForMessage: ProgressMessage[]
tools: Tools
verbose: boolean
},
): React.ReactElement {
const { verbose } = options
if (
!verbose &&
typeof result === 'string' &&
extractTag(result, 'tool_use_error')
) {
const errorMessage = extractTag(result, 'tool_use_error')
// Show a less scary message for intended behavior
if (errorMessage?.includes('File has not been read yet')) {
return <MessageResponse>
return (
<MessageResponse>
<Text dimColor>File must be read first</Text>
</MessageResponse>;
</MessageResponse>
)
}
if (errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)) {
return <MessageResponse>
return (
<MessageResponse>
<Text color="error">File not found</Text>
</MessageResponse>;
</MessageResponse>
)
}
return <MessageResponse>
return (
<MessageResponse>
<Text color="error">Error editing file</Text>
</MessageResponse>;
</MessageResponse>
)
}
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
}
type RejectionDiffData = {
patch: StructuredPatchHunk[];
firstLine: string | null;
fileContent: string | undefined;
};
function EditRejectionDiff(t0) {
const $ = _c(16);
const {
filePath,
oldString,
newString,
replaceAll,
style,
verbose
} = t0;
let t1;
if ($[0] !== filePath || $[1] !== newString || $[2] !== oldString || $[3] !== replaceAll) {
t1 = () => loadRejectionDiff(filePath, oldString, newString, replaceAll);
$[0] = filePath;
$[1] = newString;
$[2] = oldString;
$[3] = replaceAll;
$[4] = t1;
} else {
t1 = $[4];
}
const [dataPromise] = useState(t1);
let t2;
if ($[5] !== filePath || $[6] !== verbose) {
t2 = <FileEditToolUseRejectedMessage file_path={filePath} operation="update" firstLine={null} verbose={verbose} />;
$[5] = filePath;
$[6] = verbose;
$[7] = t2;
} else {
t2 = $[7];
}
let t3;
if ($[8] !== dataPromise || $[9] !== filePath || $[10] !== style || $[11] !== verbose) {
t3 = <EditRejectionBody promise={dataPromise} filePath={filePath} style={style} verbose={verbose} />;
$[8] = dataPromise;
$[9] = filePath;
$[10] = style;
$[11] = verbose;
$[12] = t3;
} else {
t3 = $[12];
}
let t4;
if ($[13] !== t2 || $[14] !== t3) {
t4 = <Suspense fallback={t2}>{t3}</Suspense>;
$[13] = t2;
$[14] = t3;
$[15] = t4;
} else {
t4 = $[15];
}
return t4;
patch: StructuredPatchHunk[]
firstLine: string | null
fileContent: string | undefined
}
function EditRejectionBody(t0) {
const $ = _c(7);
const {
promise,
filePath,
style,
verbose
} = t0;
const {
patch,
firstLine,
fileContent
} = use(promise) as any;
let t1;
if ($[0] !== fileContent || $[1] !== filePath || $[2] !== firstLine || $[3] !== patch || $[4] !== style || $[5] !== verbose) {
t1 = <FileEditToolUseRejectedMessage file_path={filePath} operation="update" patch={patch} firstLine={firstLine} fileContent={fileContent} style={style} verbose={verbose} />;
$[0] = fileContent;
$[1] = filePath;
$[2] = firstLine;
$[3] = patch;
$[4] = style;
$[5] = verbose;
$[6] = t1;
} else {
t1 = $[6];
}
return t1;
function EditRejectionDiff({
filePath,
oldString,
newString,
replaceAll,
style,
verbose,
}: {
filePath: string
oldString: string
newString: string
replaceAll: boolean
style?: 'condensed'
verbose: boolean
}): React.ReactNode {
const [dataPromise] = useState(() =>
loadRejectionDiff(filePath, oldString, newString, replaceAll),
)
return (
<Suspense
fallback={
<FileEditToolUseRejectedMessage
file_path={filePath}
operation="update"
firstLine={null}
verbose={verbose}
/>
}
>
<EditRejectionBody
promise={dataPromise}
filePath={filePath}
style={style}
verbose={verbose}
/>
</Suspense>
)
}
async function loadRejectionDiff(filePath: string, oldString: string, newString: string, replaceAll: boolean): Promise<RejectionDiffData> {
function EditRejectionBody({
promise,
filePath,
style,
verbose,
}: {
promise: Promise<RejectionDiffData>
filePath: string
style?: 'condensed'
verbose: boolean
}): React.ReactNode {
const { patch, firstLine, fileContent } = use(promise)
return (
<FileEditToolUseRejectedMessage
file_path={filePath}
operation="update"
patch={patch}
firstLine={firstLine}
fileContent={fileContent}
style={style}
verbose={verbose}
/>
)
}
async function loadRejectionDiff(
filePath: string,
oldString: string,
newString: string,
replaceAll: boolean,
): Promise<RejectionDiffData> {
try {
// Chunked read — context window around the first occurrence. replaceAll
// still shows matches *within* the window via getPatchForEdit; we accept
// losing the all-occurrences view to keep the read bounded.
const ctx = await readEditContext(filePath, oldString, CONTEXT_LINES);
const ctx = await readEditContext(filePath, oldString, CONTEXT_LINES)
if (ctx === null || ctx.truncated || ctx.content === '') {
// ENOENT / not found / truncated — diff just the tool inputs.
const {
patch
} = getPatchForEdit({
const { patch } = getPatchForEdit({
filePath,
fileContents: oldString,
oldString,
newString
});
return {
patch,
firstLine: null,
fileContent: undefined
};
newString,
})
return { patch, firstLine: null, fileContent: undefined }
}
const actualOld = findActualString(ctx.content, oldString) || oldString;
const actualNew = preserveQuoteStyle(oldString, actualOld, newString);
const {
patch
} = getPatchForEdit({
const actualOld = findActualString(ctx.content, oldString) || oldString
const actualNew = preserveQuoteStyle(oldString, actualOld, newString)
const { patch } = getPatchForEdit({
filePath,
fileContents: ctx.content,
oldString: actualOld,
newString: actualNew,
replaceAll
});
replaceAll,
})
return {
patch: adjustHunkLineNumbers(patch, ctx.lineOffset - 1),
firstLine: ctx.lineOffset === 1 ? firstLineOf(ctx.content) : null,
fileContent: ctx.content
};
fileContent: ctx.content,
}
} catch (e) {
// User may have manually applied the change while the diff was shown.
logError(e as Error);
return {
patch: [],
firstLine: null,
fileContent: undefined
};
logError(e as Error)
return { patch: [], firstLine: null, fileContent: undefined }
}
}

View File

@@ -1,184 +1,201 @@
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import * as React from 'react';
import { extractTag } from 'src/utils/messages.js';
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js';
import { FilePathLink } from '../../components/FilePathLink.js';
import { MessageResponse } from '../../components/MessageResponse.js';
import { Text } from '../../ink.js';
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js';
import { formatFileSize } from '../../utils/format.js';
import { getPlansDirectory } from '../../utils/plans.js';
import { getTaskOutputDir } from '../../utils/task/diskOutput.js';
import type { Input, Output } from './FileReadTool.js';
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import * as React from 'react'
import { extractTag } from 'src/utils/messages.js'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { FilePathLink } from '../../components/FilePathLink.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { Text } from '../../ink.js'
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js'
import { formatFileSize } from '../../utils/format.js'
import { getPlansDirectory } from '../../utils/plans.js'
import { getTaskOutputDir } from '../../utils/task/diskOutput.js'
import type { Input, Output } from './FileReadTool.js'
/**
* Check if a file path is an agent output file and extract the task ID.
* Agent output files follow the pattern: {projectTempDir}/tasks/{taskId}.output
*/
function getAgentOutputTaskId(filePath: string): string | null {
const prefix = `${getTaskOutputDir()}/`;
const suffix = '.output';
const prefix = `${getTaskOutputDir()}/`
const suffix = '.output'
if (filePath.startsWith(prefix) && filePath.endsWith(suffix)) {
const taskId = filePath.slice(prefix.length, -suffix.length);
const taskId = filePath.slice(prefix.length, -suffix.length)
// Validate it looks like a task ID (alphanumeric, reasonable length)
if (taskId.length > 0 && taskId.length <= 20 && /^[a-zA-Z0-9_-]+$/.test(taskId)) {
return taskId;
if (
taskId.length > 0 &&
taskId.length <= 20 &&
/^[a-zA-Z0-9_-]+$/.test(taskId)
) {
return taskId
}
}
return null;
return null
}
export function renderToolUseMessage({
file_path,
offset,
limit,
pages
}: Partial<Input>, {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
export function renderToolUseMessage(
{ file_path, offset, limit, pages }: Partial<Input>,
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!file_path) {
return null;
return null
}
// For agent output files, return empty string so no parentheses are shown
// The task ID is displayed separately by AssistantToolUseMessage
if (getAgentOutputTaskId(file_path)) {
return '';
return ''
}
const displayPath = verbose ? file_path : getDisplayPath(file_path);
const displayPath = verbose ? file_path : getDisplayPath(file_path)
if (pages) {
return <>
return (
<>
<FilePathLink filePath={file_path}>{displayPath}</FilePathLink>
{` · pages ${pages}`}
</>;
</>
)
}
if (verbose && (offset || limit)) {
const startLine = offset ?? 1;
const lineRange = limit ? `lines ${startLine}-${startLine + limit - 1}` : `from line ${startLine}`;
return <>
const startLine = offset ?? 1
const lineRange = limit
? `lines ${startLine}-${startLine + limit - 1}`
: `from line ${startLine}`
return (
<>
<FilePathLink filePath={file_path}>{displayPath}</FilePathLink>
{` · ${lineRange}`}
</>;
</>
)
}
return <FilePathLink filePath={file_path}>{displayPath}</FilePathLink>;
return <FilePathLink filePath={file_path}>{displayPath}</FilePathLink>
}
export function renderToolUseTag({
file_path
file_path,
}: Partial<Input>): React.ReactNode {
const agentTaskId = file_path ? getAgentOutputTaskId(file_path) : null;
const agentTaskId = file_path ? getAgentOutputTaskId(file_path) : null
// Show agent task ID for Read tool when reading agent output
if (!agentTaskId) {
return null;
return null
}
return <Text dimColor> {agentTaskId}</Text>;
return <Text dimColor> {agentTaskId}</Text>
}
export function renderToolResultMessage(output: Output): React.ReactNode {
// TODO: Render recursively
switch (output.type) {
case 'image':
{
const {
originalSize
} = output.file;
const formattedSize = formatFileSize(originalSize);
return <MessageResponse height={1}>
case 'image': {
const { originalSize } = output.file
const formattedSize = formatFileSize(originalSize)
return (
<MessageResponse height={1}>
<Text>Read image ({formattedSize})</Text>
</MessageResponse>;
</MessageResponse>
)
}
case 'notebook': {
const { cells } = output.file
if (!cells || cells.length < 1) {
return <Text color="error">No cells found in notebook</Text>
}
case 'notebook':
{
const {
cells
} = output.file;
if (!cells || cells.length < 1) {
return <Text color="error">No cells found in notebook</Text>;
}
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text>
Read <Text bold>{cells.length}</Text> cells
</Text>
</MessageResponse>;
}
case 'pdf':
{
const {
originalSize
} = output.file;
const formattedSize = formatFileSize(originalSize);
return <MessageResponse height={1}>
</MessageResponse>
)
}
case 'pdf': {
const { originalSize } = output.file
const formattedSize = formatFileSize(originalSize)
return (
<MessageResponse height={1}>
<Text>Read PDF ({formattedSize})</Text>
</MessageResponse>;
}
case 'parts':
{
return <MessageResponse height={1}>
</MessageResponse>
)
}
case 'parts': {
return (
<MessageResponse height={1}>
<Text>
Read <Text bold>{output.file.count}</Text>{' '}
{output.file.count === 1 ? 'page' : 'pages'} (
{formatFileSize(output.file.originalSize)})
</Text>
</MessageResponse>;
}
case 'text':
{
const {
numLines
} = output.file;
return <MessageResponse height={1}>
</MessageResponse>
)
}
case 'text': {
const { numLines } = output.file
return (
<MessageResponse height={1}>
<Text>
Read <Text bold>{numLines}</Text>{' '}
{numLines === 1 ? 'line' : 'lines'}
</Text>
</MessageResponse>;
}
case 'file_unchanged':
{
return <MessageResponse height={1}>
</MessageResponse>
)
}
case 'file_unchanged': {
return (
<MessageResponse height={1}>
<Text dimColor>Unchanged since last read</Text>
</MessageResponse>;
}
</MessageResponse>
)
}
}
}
export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!verbose && typeof result === 'string') {
// FileReadTool throws from call() so errors lack <tool_use_error> wrapping —
// check the raw string directly for the cwd note marker.
if (result.includes(FILE_NOT_FOUND_CWD_NOTE)) {
return <MessageResponse>
return (
<MessageResponse>
<Text color="error">File not found</Text>
</MessageResponse>;
</MessageResponse>
)
}
if (extractTag(result, 'tool_use_error')) {
return <MessageResponse>
return (
<MessageResponse>
<Text color="error">Error reading file</Text>
</MessageResponse>;
</MessageResponse>
)
}
}
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
}
export function userFacingName(input: Partial<Input> | undefined): string {
if (input?.file_path?.startsWith(getPlansDirectory())) {
return 'Reading Plan';
return 'Reading Plan'
}
if (input?.file_path && getAgentOutputTaskId(input.file_path)) {
return 'Read agent output';
return 'Read agent output'
}
return 'Read';
return 'Read'
}
export function getToolUseSummary(input: Partial<Input> | undefined): string | null {
export function getToolUseSummary(
input: Partial<Input> | undefined,
): string | null {
if (!input?.file_path) {
return null;
return null
}
// For agent output files, just show the task ID
const agentTaskId = getAgentOutputTaskId(input.file_path);
const agentTaskId = getAgentOutputTaskId(input.file_path)
if (agentTaskId) {
return agentTaskId;
return agentTaskId
}
return getDisplayPath(input.file_path);
return getDisplayPath(input.file_path)
}

View File

@@ -1,404 +1,335 @@
import { c as _c } from "react/compiler-runtime";
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import type { StructuredPatchHunk } from 'diff';
import { isAbsolute, relative, resolve } from 'path';
import * as React from 'react';
import { Suspense, use, useState } from 'react';
import { MessageResponse } from 'src/components/MessageResponse.js';
import { extractTag } from 'src/utils/messages.js';
import { CtrlOToExpand } from '../../components/CtrlOToExpand.js';
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js';
import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage.js';
import { FileEditToolUseRejectedMessage } from '../../components/FileEditToolUseRejectedMessage.js';
import { FilePathLink } from '../../components/FilePathLink.js';
import { HighlightedCode } from '../../components/HighlightedCode.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { Box, Text } from '../../ink.js';
import type { ToolProgressData } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import { getCwd } from '../../utils/cwd.js';
import { getPatchForDisplay } from '../../utils/diff.js';
import { getDisplayPath } from '../../utils/file.js';
import { logError } from '../../utils/log.js';
import { getPlansDirectory } from '../../utils/plans.js';
import { openForScan, readCapped } from '../../utils/readEditContext.js';
import type { Output } from './FileWriteTool.js';
const MAX_LINES_TO_RENDER = 10;
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import type { StructuredPatchHunk } from 'diff'
import { isAbsolute, relative, resolve } from 'path'
import * as React from 'react'
import { Suspense, use, useState } from 'react'
import { MessageResponse } from 'src/components/MessageResponse.js'
import { extractTag } from 'src/utils/messages.js'
import { CtrlOToExpand } from '../../components/CtrlOToExpand.js'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage.js'
import { FileEditToolUseRejectedMessage } from '../../components/FileEditToolUseRejectedMessage.js'
import { FilePathLink } from '../../components/FilePathLink.js'
import { HighlightedCode } from '../../components/HighlightedCode.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { Box, Text } from '../../ink.js'
import type { ToolProgressData } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import { getCwd } from '../../utils/cwd.js'
import { getPatchForDisplay } from '../../utils/diff.js'
import { getDisplayPath } from '../../utils/file.js'
import { logError } from '../../utils/log.js'
import { getPlansDirectory } from '../../utils/plans.js'
import { openForScan, readCapped } from '../../utils/readEditContext.js'
import type { Output } from './FileWriteTool.js'
const MAX_LINES_TO_RENDER = 10
// Model output uses \n regardless of platform, so always split on \n.
// os.EOL is \r\n on Windows, which would give numLines=1 for all files.
const EOL = '\n';
const EOL = '\n'
/**
* Count visible lines in file content. A trailing newline is treated as a
* line terminator (not a new empty line), matching editor line numbering.
*/
export function countLines(content: string): number {
const parts = content.split(EOL);
return content.endsWith(EOL) ? parts.length - 1 : parts.length;
const parts = content.split(EOL)
return content.endsWith(EOL) ? parts.length - 1 : parts.length
}
function FileWriteToolCreatedMessage(t0) {
const $ = _c(25);
const {
filePath,
content,
verbose
} = t0;
const {
columns
} = useTerminalSize();
const contentWithFallback = content || "(No content)";
const numLines = countLines(content);
const plusLines = numLines - MAX_LINES_TO_RENDER;
let t1;
if ($[0] !== numLines) {
t1 = <Text bold={true}>{numLines}</Text>;
$[0] = numLines;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== filePath || $[3] !== verbose) {
t2 = verbose ? filePath : relative(getCwd(), filePath);
$[2] = filePath;
$[3] = verbose;
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[5] !== t2) {
t3 = <Text bold={true}>{t2}</Text>;
$[5] = t2;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] !== t1 || $[8] !== t3) {
t4 = <Text>Wrote {t1} lines to{" "}{t3}</Text>;
$[7] = t1;
$[8] = t3;
$[9] = t4;
} else {
t4 = $[9];
}
let t5;
if ($[10] !== contentWithFallback || $[11] !== verbose) {
t5 = verbose ? contentWithFallback : contentWithFallback.split("\n").slice(0, MAX_LINES_TO_RENDER).join("\n");
$[10] = contentWithFallback;
$[11] = verbose;
$[12] = t5;
} else {
t5 = $[12];
}
const t6 = columns - 12;
let t7;
if ($[13] !== filePath || $[14] !== t5 || $[15] !== t6) {
t7 = <Box flexDirection="column"><HighlightedCode code={t5} filePath={filePath} width={t6} /></Box>;
$[13] = filePath;
$[14] = t5;
$[15] = t6;
$[16] = t7;
} else {
t7 = $[16];
}
let t8;
if ($[17] !== numLines || $[18] !== plusLines || $[19] !== verbose) {
t8 = !verbose && plusLines > 0 && <Text dimColor={true}> +{plusLines} {plusLines === 1 ? "line" : "lines"}{" "}{numLines > 0 && <CtrlOToExpand />}</Text>;
$[17] = numLines;
$[18] = plusLines;
$[19] = verbose;
$[20] = t8;
} else {
t8 = $[20];
}
let t9;
if ($[21] !== t4 || $[22] !== t7 || $[23] !== t8) {
t9 = <MessageResponse><Box flexDirection="column">{t4}{t7}{t8}</Box></MessageResponse>;
$[21] = t4;
$[22] = t7;
$[23] = t8;
$[24] = t9;
} else {
t9 = $[24];
}
return t9;
function FileWriteToolCreatedMessage({
filePath,
content,
verbose,
}: {
filePath: string
content: string
verbose: boolean
}): React.ReactNode {
const { columns } = useTerminalSize()
const contentWithFallback = content || '(No content)'
const numLines = countLines(content)
const plusLines = numLines - MAX_LINES_TO_RENDER
return (
<MessageResponse>
<Box flexDirection="column">
<Text>
Wrote <Text bold>{numLines}</Text> lines to{' '}
<Text bold>{verbose ? filePath : relative(getCwd(), filePath)}</Text>
</Text>
<Box flexDirection="column">
<HighlightedCode
code={
verbose
? contentWithFallback
: contentWithFallback
.split('\n')
.slice(0, MAX_LINES_TO_RENDER)
.join('\n')
}
filePath={filePath}
width={columns - 12}
/>
</Box>
{!verbose && plusLines > 0 && (
<Text dimColor>
+{plusLines} {plusLines === 1 ? 'line' : 'lines'}{' '}
{numLines > 0 && <CtrlOToExpand />}
</Text>
)}
</Box>
</MessageResponse>
)
}
export function userFacingName(input: Partial<{
file_path: string;
content: string;
}> | undefined): string {
export function userFacingName(
input: Partial<{ file_path: string; content: string }> | undefined,
): string {
if (input?.file_path?.startsWith(getPlansDirectory())) {
return 'Updated plan';
return 'Updated plan'
}
return 'Write';
return 'Write'
}
/** Gates fullscreen click-to-expand. Only `create` truncates (to
* MAX_LINES_TO_RENDER); `update` renders the full diff regardless of verbose.
* Called per visible message on hover/scroll, so early-exit after finding the
* (MAX+1)th line instead of splitting the whole (possibly huge) content. */
export function isResultTruncated({
type,
content
}: Output): boolean {
if (type !== 'create') return false;
let pos = 0;
export function isResultTruncated({ type, content }: Output): boolean {
if (type !== 'create') return false
let pos = 0
for (let i = 0; i < MAX_LINES_TO_RENDER; i++) {
pos = content.indexOf(EOL, pos);
if (pos === -1) return false;
pos++;
pos = content.indexOf(EOL, pos)
if (pos === -1) return false
pos++
}
// countLines treats a trailing EOL as a terminator, not a new line
return pos < content.length;
return pos < content.length
}
export function getToolUseSummary(input: Partial<{
file_path: string;
content: string;
}> | undefined): string | null {
export function getToolUseSummary(
input: Partial<{ file_path: string; content: string }> | undefined,
): string | null {
if (!input?.file_path) {
return null;
return null
}
return getDisplayPath(input.file_path);
return getDisplayPath(input.file_path)
}
export function renderToolUseMessage(input: Partial<{
file_path: string;
content: string;
}>, {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
export function renderToolUseMessage(
input: Partial<{ file_path: string; content: string }>,
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!input.file_path) {
return null;
return null
}
// For plan files, path is already in userFacingName
if (input.file_path.startsWith(getPlansDirectory())) {
return '';
return ''
}
return <FilePathLink filePath={input.file_path}>
return (
<FilePathLink filePath={input.file_path}>
{verbose ? input.file_path : getDisplayPath(input.file_path)}
</FilePathLink>;
</FilePathLink>
)
}
export function renderToolUseRejectedMessage({
file_path,
content
}: {
file_path: string;
content: string;
}, {
export function renderToolUseRejectedMessage(
{ file_path, content }: { file_path: string; content: string },
{ style, verbose }: { style?: 'condensed'; verbose: boolean },
): React.ReactNode {
return (
<WriteRejectionDiff
filePath={file_path}
content={content}
style={style}
verbose={verbose}
/>
)
}
type RejectionDiffData =
| { type: 'create' }
| { type: 'update'; patch: StructuredPatchHunk[]; oldContent: string }
| { type: 'error' }
function WriteRejectionDiff({
filePath,
content,
style,
verbose
verbose,
}: {
style?: 'condensed';
verbose: boolean;
filePath: string
content: string
style?: 'condensed'
verbose: boolean
}): React.ReactNode {
return <WriteRejectionDiff filePath={file_path} content={content} style={style} verbose={verbose} />;
const [dataPromise] = useState(() => loadRejectionDiff(filePath, content))
const firstLine = content.split('\n')[0] ?? null
const createFallback = (
<FileEditToolUseRejectedMessage
file_path={filePath}
operation="write"
content={content}
firstLine={firstLine}
verbose={verbose}
/>
)
return (
<Suspense fallback={createFallback}>
<WriteRejectionBody
promise={dataPromise}
filePath={filePath}
firstLine={firstLine}
createFallback={createFallback}
style={style}
verbose={verbose}
/>
</Suspense>
)
}
type RejectionDiffData = {
type: 'create';
} | {
type: 'update';
patch: StructuredPatchHunk[];
oldContent: string;
} | {
type: 'error';
};
function WriteRejectionDiff(t0) {
const $ = _c(20);
const {
filePath,
content,
style,
verbose
} = t0;
let t1;
if ($[0] !== content || $[1] !== filePath) {
t1 = () => loadRejectionDiff(filePath, content);
$[0] = content;
$[1] = filePath;
$[2] = t1;
} else {
t1 = $[2];
function WriteRejectionBody({
promise,
filePath,
firstLine,
createFallback,
style,
verbose,
}: {
promise: Promise<RejectionDiffData>
filePath: string
firstLine: string | null
createFallback: React.ReactNode
style?: 'condensed'
verbose: boolean
}): React.ReactNode {
const data = use(promise)
if (data.type === 'create') return createFallback
if (data.type === 'error') {
return (
<MessageResponse>
<Text>(No changes)</Text>
</MessageResponse>
)
}
const [dataPromise] = useState(t1);
let t2;
if ($[3] !== content) {
t2 = content.split("\n")[0] ?? null;
$[3] = content;
$[4] = t2;
} else {
t2 = $[4];
}
const firstLine = t2;
let t3;
if ($[5] !== content || $[6] !== filePath || $[7] !== firstLine || $[8] !== verbose) {
t3 = <FileEditToolUseRejectedMessage file_path={filePath} operation="write" content={content} firstLine={firstLine} verbose={verbose} />;
$[5] = content;
$[6] = filePath;
$[7] = firstLine;
$[8] = verbose;
$[9] = t3;
} else {
t3 = $[9];
}
const createFallback = t3;
let t4;
if ($[10] !== createFallback || $[11] !== dataPromise || $[12] !== filePath || $[13] !== firstLine || $[14] !== style || $[15] !== verbose) {
t4 = <WriteRejectionBody promise={dataPromise} filePath={filePath} firstLine={firstLine} createFallback={createFallback} style={style} verbose={verbose} />;
$[10] = createFallback;
$[11] = dataPromise;
$[12] = filePath;
$[13] = firstLine;
$[14] = style;
$[15] = verbose;
$[16] = t4;
} else {
t4 = $[16];
}
let t5;
if ($[17] !== createFallback || $[18] !== t4) {
t5 = <Suspense fallback={createFallback}>{t4}</Suspense>;
$[17] = createFallback;
$[18] = t4;
$[19] = t5;
} else {
t5 = $[19];
}
return t5;
return (
<FileEditToolUseRejectedMessage
file_path={filePath}
operation="update"
patch={data.patch}
firstLine={firstLine}
fileContent={data.oldContent}
style={style}
verbose={verbose}
/>
)
}
function WriteRejectionBody(t0) {
const $ = _c(8);
const {
promise,
filePath,
firstLine,
createFallback,
style,
verbose
} = t0;
const data = use(promise) as RejectionDiffData;
if (data.type === "create") {
return createFallback;
}
if (data.type === "error") {
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <MessageResponse><Text>(No changes)</Text></MessageResponse>;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
}
let t1;
if ($[1] !== data.oldContent || $[2] !== data.patch || $[3] !== filePath || $[4] !== firstLine || $[5] !== style || $[6] !== verbose) {
t1 = <FileEditToolUseRejectedMessage file_path={filePath} operation="update" patch={data.patch} firstLine={firstLine} fileContent={data.oldContent} style={style} verbose={verbose} />;
$[1] = data.oldContent;
$[2] = data.patch;
$[3] = filePath;
$[4] = firstLine;
$[5] = style;
$[6] = verbose;
$[7] = t1;
} else {
t1 = $[7];
}
return t1;
}
async function loadRejectionDiff(filePath: string, content: string): Promise<RejectionDiffData> {
async function loadRejectionDiff(
filePath: string,
content: string,
): Promise<RejectionDiffData> {
try {
const fullFilePath = isAbsolute(filePath) ? filePath : resolve(getCwd(), filePath);
const handle = await openForScan(fullFilePath);
if (handle === null) return {
type: 'create'
};
let oldContent: string | null;
const fullFilePath = isAbsolute(filePath)
? filePath
: resolve(getCwd(), filePath)
const handle = await openForScan(fullFilePath)
if (handle === null) return { type: 'create' }
let oldContent: string | null
try {
oldContent = await readCapped(handle);
oldContent = await readCapped(handle)
} finally {
await handle.close();
await handle.close()
}
// File exceeds MAX_SCAN_BYTES — fall back to the create view rather than
// OOMing on a diff of a multi-GB file.
if (oldContent === null) return {
type: 'create'
};
if (oldContent === null) return { type: 'create' }
const patch = getPatchForDisplay({
filePath,
fileContents: oldContent,
edits: [{
old_string: oldContent,
new_string: content,
replace_all: false
}]
});
return {
type: 'update',
patch,
oldContent
};
edits: [
{ old_string: oldContent, new_string: content, replace_all: false },
],
})
return { type: 'update', patch, oldContent }
} catch (e) {
// User may have manually applied the change while the diff was shown.
logError(e as Error);
return {
type: 'error'
};
logError(e as Error)
return { type: 'error' }
}
}
export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
if (!verbose && typeof result === 'string' && extractTag(result, 'tool_use_error')) {
return <MessageResponse>
<Text color="error">Error writing file</Text>
</MessageResponse>;
}
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
}
export function renderToolResultMessage({
filePath,
content,
structuredPatch,
type,
originalFile
}: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], {
style,
verbose
}: {
style?: 'condensed';
verbose: boolean;
}): React.ReactNode {
switch (type) {
case 'create':
{
const isPlanFile = filePath.startsWith(getPlansDirectory());
// Plan files: invert condensed behavior
// - Regular mode: just show hint (user can type /plan to see full content)
// - Condensed mode (subagent view): show full content
if (isPlanFile && !verbose) {
if (style !== 'condensed') {
return <MessageResponse>
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (
!verbose &&
typeof result === 'string' &&
extractTag(result, 'tool_use_error')
) {
return (
<MessageResponse>
<Text color="error">Error writing file</Text>
</MessageResponse>
)
}
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
}
export function renderToolResultMessage(
{ filePath, content, structuredPatch, type, originalFile }: Output,
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
{ style, verbose }: { style?: 'condensed'; verbose: boolean },
): React.ReactNode {
switch (type) {
case 'create': {
const isPlanFile = filePath.startsWith(getPlansDirectory())
// Plan files: invert condensed behavior
// - Regular mode: just show hint (user can type /plan to see full content)
// - Condensed mode (subagent view): show full content
if (isPlanFile && !verbose) {
if (style !== 'condensed') {
return (
<MessageResponse>
<Text dimColor>/plan to preview</Text>
</MessageResponse>;
}
} else if (style === 'condensed' && !verbose) {
const numLines = countLines(content);
return <Text>
</MessageResponse>
)
}
} else if (style === 'condensed' && !verbose) {
const numLines = countLines(content)
return (
<Text>
Wrote <Text bold>{numLines}</Text> lines to{' '}
<Text bold>{relative(getCwd(), filePath)}</Text>
</Text>;
}
return <FileWriteToolCreatedMessage filePath={filePath} content={content} verbose={verbose} />;
}
case 'update':
{
const isPlanFile = filePath.startsWith(getPlansDirectory());
return <FileEditToolUpdatedMessage filePath={filePath} structuredPatch={structuredPatch} firstLine={content.split('\n')[0] ?? null} fileContent={originalFile ?? undefined} style={style} verbose={verbose} previewHint={isPlanFile ? '/plan to preview' : undefined} />;
</Text>
)
}
return (
<FileWriteToolCreatedMessage
filePath={filePath}
content={content}
verbose={verbose}
/>
)
}
case 'update': {
const isPlanFile = filePath.startsWith(getPlansDirectory())
return (
<FileEditToolUpdatedMessage
filePath={filePath}
structuredPatch={structuredPatch}
firstLine={content.split('\n')[0] ?? null}
fileContent={originalFile ?? undefined}
style={style}
verbose={verbose}
previewHint={isPlanFile ? '/plan to preview' : undefined}
/>
)
}
}
}

View File

@@ -1,62 +1,65 @@
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import React from 'react';
import { MessageResponse } from 'src/components/MessageResponse.js';
import { extractTag } from 'src/utils/messages.js';
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js';
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js';
import { Text } from '../../ink.js';
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js';
import { truncate } from '../../utils/format.js';
import { GrepTool } from '../GrepTool/GrepTool.js';
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import React from 'react'
import { MessageResponse } from 'src/components/MessageResponse.js'
import { extractTag } from 'src/utils/messages.js'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js'
import { Text } from '../../ink.js'
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js'
import { truncate } from '../../utils/format.js'
import { GrepTool } from '../GrepTool/GrepTool.js'
export function userFacingName(): string {
return 'Search';
return 'Search'
}
export function renderToolUseMessage({
pattern,
path
}: Partial<{
pattern: string;
path: string;
}>, {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
export function renderToolUseMessage(
{ pattern, path }: Partial<{ pattern: string; path: string }>,
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!pattern) {
return null;
return null
}
if (!path) {
return `pattern: "${pattern}"`;
return `pattern: "${pattern}"`
}
return `pattern: "${pattern}", path: "${verbose ? path : getDisplayPath(path)}"`;
return `pattern: "${pattern}", path: "${verbose ? path : getDisplayPath(path)}"`
}
export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
if (!verbose && typeof result === 'string' && extractTag(result, 'tool_use_error')) {
const errorMessage = extractTag(result, 'tool_use_error');
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (
!verbose &&
typeof result === 'string' &&
extractTag(result, 'tool_use_error')
) {
const errorMessage = extractTag(result, 'tool_use_error')
if (errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)) {
return <MessageResponse>
return (
<MessageResponse>
<Text color="error">File not found</Text>
</MessageResponse>;
</MessageResponse>
)
}
return <MessageResponse>
return (
<MessageResponse>
<Text color="error">Error searching files</Text>
</MessageResponse>;
</MessageResponse>
)
}
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
}
// Note: GlobTool reuses GrepTool's renderToolResultMessage
export const renderToolResultMessage = GrepTool.renderToolResultMessage;
export function getToolUseSummary(input: Partial<{
pattern: string;
path: string;
}> | undefined): string | null {
export const renderToolResultMessage = GrepTool.renderToolResultMessage
export function getToolUseSummary(
input: Partial<{ pattern: string; path: string }> | undefined,
): string | null {
if (!input?.pattern) {
return null;
return null
}
return truncate(input.pattern, TOOL_SUMMARY_MAX_LENGTH);
return truncate(input.pattern, TOOL_SUMMARY_MAX_LENGTH)
}

View File

@@ -1,200 +1,190 @@
import { c as _c } from "react/compiler-runtime";
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import React from 'react';
import { CtrlOToExpand } from '../../components/CtrlOToExpand.js';
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js';
import { MessageResponse } from '../../components/MessageResponse.js';
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js';
import { Box, Text } from '../../ink.js';
import type { ToolProgressData } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js';
import { truncate } from '../../utils/format.js';
import { extractTag } from '../../utils/messages.js';
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import React from 'react'
import { CtrlOToExpand } from '../../components/CtrlOToExpand.js'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js'
import { Box, Text } from '../../ink.js'
import type { ToolProgressData } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js'
import { truncate } from '../../utils/format.js'
import { extractTag } from '../../utils/messages.js'
// Reusable component for search result summaries
function SearchResultSummary(t0) {
const $ = _c(26);
const {
count,
countLabel,
secondaryCount,
secondaryLabel,
content,
verbose
} = t0;
let t1;
if ($[0] !== count) {
t1 = <Text bold={true}>{count} </Text>;
$[0] = count;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== count || $[3] !== countLabel) {
t2 = count === 0 || count > 1 ? countLabel : countLabel.slice(0, -1);
$[2] = count;
$[3] = countLabel;
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[5] !== t1 || $[6] !== t2) {
t3 = <Text>Found {t1}{t2}</Text>;
$[5] = t1;
$[6] = t2;
$[7] = t3;
} else {
t3 = $[7];
}
const primaryText = t3;
let t4;
if ($[8] !== secondaryCount || $[9] !== secondaryLabel) {
t4 = secondaryCount !== undefined && secondaryLabel ? <Text>{" "}across <Text bold={true}>{secondaryCount} </Text>{secondaryCount === 0 || secondaryCount > 1 ? secondaryLabel : secondaryLabel.slice(0, -1)}</Text> : null;
$[8] = secondaryCount;
$[9] = secondaryLabel;
$[10] = t4;
} else {
t4 = $[10];
}
const secondaryText = t4;
if (verbose) {
let t5;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text dimColor={true}>    </Text>;
$[11] = t5;
} else {
t5 = $[11];
}
let t6;
if ($[12] !== primaryText || $[13] !== secondaryText) {
t6 = <Box flexDirection="row"><Text>{t5}{primaryText}{secondaryText}</Text></Box>;
$[12] = primaryText;
$[13] = secondaryText;
$[14] = t6;
} else {
t6 = $[14];
}
let t7;
if ($[15] !== content) {
t7 = <Box marginLeft={5}><Text>{content}</Text></Box>;
$[15] = content;
$[16] = t7;
} else {
t7 = $[16];
}
let t8;
if ($[17] !== t6 || $[18] !== t7) {
t8 = <Box flexDirection="column">{t6}{t7}</Box>;
$[17] = t6;
$[18] = t7;
$[19] = t8;
} else {
t8 = $[19];
}
return t8;
}
let t5;
if ($[20] !== count) {
t5 = count > 0 && <CtrlOToExpand />;
$[20] = count;
$[21] = t5;
} else {
t5 = $[21];
}
let t6;
if ($[22] !== primaryText || $[23] !== secondaryText || $[24] !== t5) {
t6 = <MessageResponse height={1}><Text>{primaryText}{secondaryText} {t5}</Text></MessageResponse>;
$[22] = primaryText;
$[23] = secondaryText;
$[24] = t5;
$[25] = t6;
} else {
t6 = $[25];
}
return t6;
}
type Output = {
mode?: 'content' | 'files_with_matches' | 'count';
numFiles: number;
filenames: string[];
content?: string;
numLines?: number; // For content mode
numMatches?: number; // For count mode
};
export function renderToolUseMessage({
pattern,
path
}: Partial<{
pattern: string;
path?: string;
}>, {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
if (!pattern) {
return null;
}
const parts = [`pattern: "${pattern}"`];
if (path) {
parts.push(`path: "${verbose ? path : getDisplayPath(path)}"`);
}
return parts.join(', ');
}
export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
if (!verbose && typeof result === 'string' && extractTag(result, 'tool_use_error')) {
const errorMessage = extractTag(result, 'tool_use_error');
if (errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)) {
return <MessageResponse>
<Text color="error">File not found</Text>
</MessageResponse>;
}
return <MessageResponse>
<Text color="error">Error searching files</Text>
</MessageResponse>;
}
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
}
export function renderToolResultMessage({
mode = 'files_with_matches',
filenames,
numFiles,
function SearchResultSummary({
count,
countLabel,
secondaryCount,
secondaryLabel,
content,
numLines,
numMatches
}: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], {
verbose
verbose,
}: {
verbose: boolean;
count: number
countLabel: string
secondaryCount?: number
secondaryLabel?: string
content?: string
verbose: boolean
}): React.ReactNode {
if (mode === 'content') {
return <SearchResultSummary count={numLines ?? 0} countLabel="lines" content={content} verbose={verbose} />;
const primaryText = (
<Text>
Found <Text bold>{count} </Text>
{count === 0 || count > 1 ? countLabel : countLabel.slice(0, -1)}
</Text>
)
const secondaryText =
secondaryCount !== undefined && secondaryLabel ? (
<Text>
{' '}
across <Text bold>{secondaryCount} </Text>
{secondaryCount === 0 || secondaryCount > 1
? secondaryLabel
: secondaryLabel.slice(0, -1)}
</Text>
) : null
if (verbose) {
return (
<Box flexDirection="column">
<Box flexDirection="row">
<Text>
<Text dimColor>&nbsp;&nbsp; &nbsp;</Text>
{primaryText}
{secondaryText}
</Text>
</Box>
<Box marginLeft={5}>
<Text>{content}</Text>
</Box>
</Box>
)
}
return (
<MessageResponse height={1}>
<Text>
{primaryText}
{secondaryText} {count > 0 && <CtrlOToExpand />}
</Text>
</MessageResponse>
)
}
type Output = {
mode?: 'content' | 'files_with_matches' | 'count'
numFiles: number
filenames: string[]
content?: string
numLines?: number // For content mode
numMatches?: number // For count mode
}
export function renderToolUseMessage(
{ pattern, path }: Partial<{ pattern: string; path?: string }>,
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!pattern) {
return null
}
const parts = [`pattern: "${pattern}"`]
if (path) {
parts.push(`path: "${verbose ? path : getDisplayPath(path)}"`)
}
return parts.join(', ')
}
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (
!verbose &&
typeof result === 'string' &&
extractTag(result, 'tool_use_error')
) {
const errorMessage = extractTag(result, 'tool_use_error')
if (errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)) {
return (
<MessageResponse>
<Text color="error">File not found</Text>
</MessageResponse>
)
}
return (
<MessageResponse>
<Text color="error">Error searching files</Text>
</MessageResponse>
)
}
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
}
export function renderToolResultMessage(
{
mode = 'files_with_matches',
filenames,
numFiles,
content,
numLines,
numMatches,
}: Output,
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (mode === 'content') {
return (
<SearchResultSummary
count={numLines ?? 0}
countLabel="lines"
content={content}
verbose={verbose}
/>
)
}
if (mode === 'count') {
return <SearchResultSummary count={numMatches ?? 0} countLabel="matches" secondaryCount={numFiles} secondaryLabel="files" content={content} verbose={verbose} />;
return (
<SearchResultSummary
count={numMatches ?? 0}
countLabel="matches"
secondaryCount={numFiles}
secondaryLabel="files"
content={content}
verbose={verbose}
/>
)
}
// files_with_matches mode
const fileListContent = filenames.map(filename => filename).join('\n');
return <SearchResultSummary count={numFiles} countLabel="files" content={fileListContent} verbose={verbose} />;
const fileListContent = filenames.map(filename => filename).join('\n')
return (
<SearchResultSummary
count={numFiles}
countLabel="files"
content={fileListContent}
verbose={verbose}
/>
)
}
export function getToolUseSummary(input: Partial<{
pattern: string;
path?: string;
glob?: string;
type?: string;
output_mode?: 'content' | 'files_with_matches' | 'count';
head_limit?: number;
}> | undefined): string | null {
export function getToolUseSummary(
input:
| Partial<{
pattern: string
path?: string
glob?: string
type?: string
output_mode?: 'content' | 'files_with_matches' | 'count'
head_limit?: number
}>
| undefined,
): string | null {
if (!input?.pattern) {
return null;
return null
}
return truncate(input.pattern, TOOL_SUMMARY_MAX_LENGTH);
return truncate(input.pattern, TOOL_SUMMARY_MAX_LENGTH)
}

View File

@@ -1,227 +1,203 @@
import { c as _c } from "react/compiler-runtime";
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import React from 'react';
import { CtrlOToExpand } from '../../components/CtrlOToExpand.js';
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js';
import { MessageResponse } from '../../components/MessageResponse.js';
import { Box, Text } from '../../ink.js';
import { getDisplayPath } from '../../utils/file.js';
import { extractTag } from '../../utils/messages.js';
import type { Input, Output } from './LSPTool.js';
import { getSymbolAtPosition } from './symbolContext.js';
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import React from 'react'
import { CtrlOToExpand } from '../../components/CtrlOToExpand.js'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { Box, Text } from '../../ink.js'
import { getDisplayPath } from '../../utils/file.js'
import { extractTag } from '../../utils/messages.js'
import type { Input, Output } from './LSPTool.js'
import { getSymbolAtPosition } from './symbolContext.js'
// Lookup map for operation-specific labels
const OPERATION_LABELS: Record<Input['operation'], {
singular: string;
plural: string;
special?: string;
}> = {
goToDefinition: {
singular: 'definition',
plural: 'definitions'
},
findReferences: {
singular: 'reference',
plural: 'references'
},
documentSymbol: {
singular: 'symbol',
plural: 'symbols'
},
workspaceSymbol: {
singular: 'symbol',
plural: 'symbols'
},
hover: {
singular: 'hover info',
plural: 'hover info',
special: 'available'
},
goToImplementation: {
singular: 'implementation',
plural: 'implementations'
},
prepareCallHierarchy: {
singular: 'call item',
plural: 'call items'
},
incomingCalls: {
singular: 'caller',
plural: 'callers'
},
outgoingCalls: {
singular: 'callee',
plural: 'callees'
}
};
const OPERATION_LABELS: Record<
Input['operation'],
{ singular: string; plural: string; special?: string }
> = {
goToDefinition: { singular: 'definition', plural: 'definitions' },
findReferences: { singular: 'reference', plural: 'references' },
documentSymbol: { singular: 'symbol', plural: 'symbols' },
workspaceSymbol: { singular: 'symbol', plural: 'symbols' },
hover: { singular: 'hover info', plural: 'hover info', special: 'available' },
goToImplementation: { singular: 'implementation', plural: 'implementations' },
prepareCallHierarchy: { singular: 'call item', plural: 'call items' },
incomingCalls: { singular: 'caller', plural: 'callers' },
outgoingCalls: { singular: 'callee', plural: 'callees' },
}
/**
* Reusable component for LSP result summaries with collapsed/expanded views
*/
function LSPResultSummary(t0) {
const $ = _c(24);
const {
operation,
resultCount,
fileCount,
content,
verbose
} = t0;
let t1;
if ($[0] !== operation) {
t1 = OPERATION_LABELS[operation] || {
singular: "result",
plural: "results"
};
$[0] = operation;
$[1] = t1;
} else {
t1 = $[1];
}
const labelConfig = t1;
const countLabel = resultCount === 1 ? labelConfig.singular : labelConfig.plural;
let t2;
if ($[2] !== countLabel || $[3] !== labelConfig.special || $[4] !== operation || $[5] !== resultCount) {
t2 = operation === "hover" && resultCount > 0 && labelConfig.special ? <Text>Hover info {labelConfig.special}</Text> : <Text>Found <Text bold={true}>{resultCount} </Text>{countLabel}</Text>;
$[2] = countLabel;
$[3] = labelConfig.special;
$[4] = operation;
$[5] = resultCount;
$[6] = t2;
} else {
t2 = $[6];
}
const primaryText = t2;
let t3;
if ($[7] !== fileCount) {
t3 = fileCount > 1 ? <Text>{" "}across <Text bold={true}>{fileCount} </Text>files</Text> : null;
$[7] = fileCount;
$[8] = t3;
} else {
t3 = $[8];
}
const secondaryText = t3;
if (verbose) {
let t4;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Text dimColor={true}>    </Text>;
$[9] = t4;
} else {
t4 = $[9];
}
let t5;
if ($[10] !== primaryText || $[11] !== secondaryText) {
t5 = <Box flexDirection="row"><Text>{t4}{primaryText}{secondaryText}</Text></Box>;
$[10] = primaryText;
$[11] = secondaryText;
$[12] = t5;
} else {
t5 = $[12];
}
let t6;
if ($[13] !== content) {
t6 = <Box marginLeft={5}><Text>{content}</Text></Box>;
$[13] = content;
$[14] = t6;
} else {
t6 = $[14];
}
let t7;
if ($[15] !== t5 || $[16] !== t6) {
t7 = <Box flexDirection="column">{t5}{t6}</Box>;
$[15] = t5;
$[16] = t6;
$[17] = t7;
} else {
t7 = $[17];
}
return t7;
}
let t4;
if ($[18] !== resultCount) {
t4 = resultCount > 0 && <CtrlOToExpand />;
$[18] = resultCount;
$[19] = t4;
} else {
t4 = $[19];
}
let t5;
if ($[20] !== primaryText || $[21] !== secondaryText || $[22] !== t4) {
t5 = <MessageResponse height={1}><Text>{primaryText}{secondaryText} {t4}</Text></MessageResponse>;
$[20] = primaryText;
$[21] = secondaryText;
$[22] = t4;
$[23] = t5;
} else {
t5 = $[23];
}
return t5;
}
export function userFacingName(): string {
return 'LSP';
}
export function renderToolUseMessage(input: Partial<Input>, {
verbose
function LSPResultSummary({
operation,
resultCount,
fileCount,
content,
verbose,
}: {
verbose: boolean;
operation: Input['operation']
resultCount: number
fileCount: number
content: string
verbose: boolean
}): React.ReactNode {
if (!input.operation) {
return null;
// Get label configuration for this operation
const labelConfig = OPERATION_LABELS[operation] || {
singular: 'result',
plural: 'results',
}
const parts: string[] = [];
const countLabel =
resultCount === 1 ? labelConfig.singular : labelConfig.plural
const primaryText =
operation === 'hover' && resultCount > 0 && labelConfig.special ? (
<Text>Hover info {labelConfig.special}</Text>
) : (
<Text>
Found <Text bold>{resultCount} </Text>
{countLabel}
</Text>
)
const secondaryText =
fileCount > 1 ? (
<Text>
{' '}
across <Text bold>{fileCount} </Text>
files
</Text>
) : null
if (verbose) {
return (
<Box flexDirection="column">
<Box flexDirection="row">
<Text>
<Text dimColor>&nbsp;&nbsp; &nbsp;</Text>
{primaryText}
{secondaryText}
</Text>
</Box>
<Box marginLeft={5}>
<Text>{content}</Text>
</Box>
</Box>
)
}
return (
<MessageResponse height={1}>
<Text>
{primaryText}
{secondaryText} {resultCount > 0 && <CtrlOToExpand />}
</Text>
</MessageResponse>
)
}
export function userFacingName(): string {
return 'LSP'
}
export function renderToolUseMessage(
input: Partial<Input>,
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!input.operation) {
return null
}
const parts: string[] = []
// For position-based operations (goToDefinition, findReferences, hover, goToImplementation),
// show the symbol at the position for better context
if ((input.operation === 'goToDefinition' || input.operation === 'findReferences' || input.operation === 'hover' || input.operation === 'goToImplementation') && input.filePath && input.line !== undefined && input.character !== undefined) {
if (
(input.operation === 'goToDefinition' ||
input.operation === 'findReferences' ||
input.operation === 'hover' ||
input.operation === 'goToImplementation') &&
input.filePath &&
input.line !== undefined &&
input.character !== undefined
) {
// Convert from 1-based (user input) to 0-based (internal file reading)
const symbol = getSymbolAtPosition(input.filePath, input.line - 1, input.character - 1);
const displayPath = verbose ? input.filePath : getDisplayPath(input.filePath);
const symbol = getSymbolAtPosition(
input.filePath,
input.line - 1,
input.character - 1,
)
const displayPath = verbose
? input.filePath
: getDisplayPath(input.filePath)
if (symbol) {
parts.push(`operation: "${input.operation}"`);
parts.push(`symbol: "${symbol}"`);
parts.push(`in: "${displayPath}"`);
parts.push(`operation: "${input.operation}"`)
parts.push(`symbol: "${symbol}"`)
parts.push(`in: "${displayPath}"`)
} else {
parts.push(`operation: "${input.operation}"`);
parts.push(`file: "${displayPath}"`);
parts.push(`position: ${input.line}:${input.character}`);
parts.push(`operation: "${input.operation}"`)
parts.push(`file: "${displayPath}"`)
parts.push(`position: ${input.line}:${input.character}`)
}
return parts.join(', ');
return parts.join(', ')
}
// For other operations (documentSymbol, workspaceSymbol),
// show operation and file without position details
parts.push(`operation: "${input.operation}"`);
parts.push(`operation: "${input.operation}"`)
if (input.filePath) {
const displayPath = verbose ? input.filePath : getDisplayPath(input.filePath);
parts.push(`file: "${displayPath}"`);
const displayPath = verbose
? input.filePath
: getDisplayPath(input.filePath)
parts.push(`file: "${displayPath}"`)
}
return parts.join(', ');
return parts.join(', ')
}
export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
if (!verbose && typeof result === 'string' && extractTag(result, 'tool_use_error')) {
return <MessageResponse>
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (
!verbose &&
typeof result === 'string' &&
extractTag(result, 'tool_use_error')
) {
return (
<MessageResponse>
<Text color="error">LSP operation failed</Text>
</MessageResponse>;
</MessageResponse>
)
}
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
}
export function renderToolResultMessage(output: Output, _progressMessages: unknown[], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
export function renderToolResultMessage(
output: Output,
_progressMessages: unknown[],
{ verbose }: { verbose: boolean },
): React.ReactNode {
// Use collapsed/expanded view if we have count information
if (output.resultCount !== undefined && output.fileCount !== undefined) {
return <LSPResultSummary operation={output.operation} resultCount={output.resultCount} fileCount={output.fileCount} content={output.result} verbose={verbose} />;
return (
<LSPResultSummary
operation={output.operation}
resultCount={output.resultCount}
fileCount={output.fileCount}
content={output.result}
verbose={verbose}
/>
)
}
// Fallback for error cases where counts aren't available
// (e.g., LSP server initialization failures, request errors)
return <MessageResponse>
return (
<MessageResponse>
<Text>{output.result}</Text>
</MessageResponse>;
</MessageResponse>
)
}

View File

@@ -1,28 +1,35 @@
import * as React from 'react';
import { MessageResponse } from '../../components/MessageResponse.js';
import { OutputLine } from '../../components/shell/OutputLine.js';
import { Text } from '../../ink.js';
import type { ToolProgressData } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import { jsonStringify } from '../../utils/slowOperations.js';
import type { Output } from './ListMcpResourcesTool.js';
export function renderToolUseMessage(input: Partial<{
server?: string;
}>): React.ReactNode {
return input.server ? `List MCP resources from server "${input.server}"` : `List all MCP resources`;
import * as React from 'react'
import { MessageResponse } from '../../components/MessageResponse.js'
import { OutputLine } from '../../components/shell/OutputLine.js'
import { Text } from '../../ink.js'
import type { ToolProgressData } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import type { Output } from './ListMcpResourcesTool.js'
export function renderToolUseMessage(
input: Partial<{ server?: string }>,
): React.ReactNode {
return input.server
? `List MCP resources from server "${input.server}"`
: `List all MCP resources`
}
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
export function renderToolResultMessage(
output: Output,
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!output || output.length === 0) {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>(No resources found)</Text>
</MessageResponse>;
</MessageResponse>
)
}
// eslint-disable-next-line no-restricted-syntax -- human-facing UI, not tool_result
const formattedOutput = jsonStringify(output, null, 2);
return <OutputLine content={formattedOutput} verbose={verbose} />;
const formattedOutput = jsonStringify(output, null, 2)
return <OutputLine content={formattedOutput} verbose={verbose} />
}

View File

@@ -1,80 +1,99 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import figures from 'figures';
import * as React from 'react';
import type { z } from 'zod/v4';
import { ProgressBar } from '../../components/design-system/ProgressBar.js';
import { MessageResponse } from '../../components/MessageResponse.js';
import { linkifyUrlsInText, OutputLine } from '../../components/shell/OutputLine.js';
import { stringWidth } from '../../ink/stringWidth.js';
import { Ansi, Box, Text } from '../../ink.js';
import type { ToolProgressData } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import type { MCPProgress } from '../../types/tools.js';
import { formatNumber } from '../../utils/format.js';
import { createHyperlink } from '../../utils/hyperlink.js';
import { getContentSizeEstimate, type MCPToolResult } from '../../utils/mcpValidation.js';
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js';
import type { inputSchema } from './MCPTool.js';
import { feature } from 'bun:bundle'
import figures from 'figures'
import * as React from 'react'
import type { z } from 'zod/v4'
import { ProgressBar } from '../../components/design-system/ProgressBar.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import {
linkifyUrlsInText,
OutputLine,
} from '../../components/shell/OutputLine.js'
import { stringWidth } from '../../ink/stringWidth.js'
import { Ansi, Box, Text } from '../../ink.js'
import type { ToolProgressData } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import type { MCPProgress } from '../../types/tools.js'
import { formatNumber } from '../../utils/format.js'
import { createHyperlink } from '../../utils/hyperlink.js'
import {
getContentSizeEstimate,
type MCPToolResult,
} from '../../utils/mcpValidation.js'
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'
import type { inputSchema } from './MCPTool.js'
// Threshold for displaying warning about large MCP responses
const MCP_OUTPUT_WARNING_THRESHOLD_TOKENS = 10_000;
const MCP_OUTPUT_WARNING_THRESHOLD_TOKENS = 10_000
// In non-verbose mode, truncate individual input values to keep the header
// compact. Matches BashTool's philosophy of showing enough to identify the
// call without dumping the entire payload inline.
const MAX_INPUT_VALUE_CHARS = 80;
const MAX_INPUT_VALUE_CHARS = 80
// Max number of top-level keys before we fall back to raw JSON display.
// Beyond this a flat k:v list is more noise than help.
const MAX_FLAT_JSON_KEYS = 12;
const MAX_FLAT_JSON_KEYS = 12
// Don't attempt flat-object parsing for large blobs.
const MAX_FLAT_JSON_CHARS = 5_000;
const MAX_FLAT_JSON_CHARS = 5_000
// Don't attempt to parse JSON blobs larger than this (perf safety).
const MAX_JSON_PARSE_CHARS = 200_000;
const MAX_JSON_PARSE_CHARS = 200_000
// A string value is "dominant text payload" if it has newlines or is
// long enough that inline display would be worse than unwrapping.
const UNWRAP_MIN_STRING_LEN = 200;
export function renderToolUseMessage(input: z.infer<ReturnType<typeof inputSchema>>, {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
const UNWRAP_MIN_STRING_LEN = 200
export function renderToolUseMessage(
input: z.infer<ReturnType<typeof inputSchema>>,
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (Object.keys(input).length === 0) {
return '';
return ''
}
return Object.entries(input).map(([key, value]) => {
let rendered = jsonStringify(value);
if (feature('MCP_RICH_OUTPUT') && !verbose && rendered.length > MAX_INPUT_VALUE_CHARS) {
rendered = rendered.slice(0, MAX_INPUT_VALUE_CHARS).trimEnd() + '…';
}
return `${key}: ${rendered}`;
}).join(', ');
return Object.entries(input)
.map(([key, value]) => {
let rendered = jsonStringify(value)
if (
feature('MCP_RICH_OUTPUT') &&
!verbose &&
rendered.length > MAX_INPUT_VALUE_CHARS
) {
rendered = rendered.slice(0, MAX_INPUT_VALUE_CHARS).trimEnd() + '…'
}
return `${key}: ${rendered}`
})
.join(', ')
}
export function renderToolUseProgressMessage(progressMessagesForMessage: ProgressMessage<MCPProgress>[]): React.ReactNode {
const lastProgress = progressMessagesForMessage.at(-1);
export function renderToolUseProgressMessage(
progressMessagesForMessage: ProgressMessage<MCPProgress>[],
): React.ReactNode {
const lastProgress = progressMessagesForMessage.at(-1)
if (!lastProgress?.data) {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>Running</Text>
</MessageResponse>;
</MessageResponse>
)
}
const {
progress,
total,
progressMessage
} = lastProgress.data;
const { progress, total, progressMessage } = lastProgress.data
if (progress === undefined) {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>Running</Text>
</MessageResponse>;
</MessageResponse>
)
}
if (total !== undefined && total > 0) {
const ratio = Math.min(1, Math.max(0, progress / total));
const percentage = Math.round(ratio * 100);
return <MessageResponse>
const ratio = Math.min(1, Math.max(0, progress / total))
const percentage = Math.round(ratio * 100)
return (
<MessageResponse>
<Box flexDirection="column">
{progressMessage && <Text dimColor>{progressMessage}</Text>}
<Box flexDirection="row" gap={1}>
@@ -82,71 +101,110 @@ export function renderToolUseProgressMessage(progressMessagesForMessage: Progres
<Text dimColor>{percentage}%</Text>
</Box>
</Box>
</MessageResponse>;
</MessageResponse>
)
}
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>{progressMessage ?? `Processing… ${progress}`}</Text>
</MessageResponse>;
</MessageResponse>
)
}
export function renderToolResultMessage(output: string | MCPToolResult, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], {
verbose,
input
}: {
verbose: boolean;
input?: unknown;
}): React.ReactNode {
const mcpOutput = output as MCPToolResult;
export function renderToolResultMessage(
output: string | MCPToolResult,
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
{ verbose, input }: { verbose: boolean; input?: unknown },
): React.ReactNode {
const mcpOutput = output as MCPToolResult
if (!verbose) {
const slackSend = trySlackSendCompact(mcpOutput, input);
const slackSend = trySlackSendCompact(mcpOutput, input)
if (slackSend !== null) {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text>
Sent a message to{' '}
<Ansi>{createHyperlink(slackSend.url, slackSend.channel)}</Ansi>
</Text>
</MessageResponse>;
</MessageResponse>
)
}
}
const estimatedTokens = getContentSizeEstimate(mcpOutput);
const showWarning = estimatedTokens > MCP_OUTPUT_WARNING_THRESHOLD_TOKENS;
const warningMessage = showWarning ? `${figures.warning} Large MCP response (~${formatNumber(estimatedTokens)} tokens), this can fill up context quickly` : null;
let contentElement: React.ReactNode;
const estimatedTokens = getContentSizeEstimate(mcpOutput)
const showWarning = estimatedTokens > MCP_OUTPUT_WARNING_THRESHOLD_TOKENS
const warningMessage = showWarning
? `${figures.warning} Large MCP response (~${formatNumber(estimatedTokens)} tokens), this can fill up context quickly`
: null
let contentElement: React.ReactNode
if (Array.isArray(mcpOutput)) {
const contentBlocks = mcpOutput.map((item, i) => {
if (item.type === 'image') {
return <Box key={i} justifyContent="space-between" overflowX="hidden" width="100%">
return (
<Box
key={i}
justifyContent="space-between"
overflowX="hidden"
width="100%"
>
<MessageResponse height={1}>
<Text>[Image]</Text>
</MessageResponse>
</Box>;
</Box>
)
}
// For text blocks and any other block types, extract text if available
const textContent = item.type === 'text' && 'text' in item && item.text !== null && item.text !== undefined ? String(item.text) : '';
return feature('MCP_RICH_OUTPUT') ? <MCPTextOutput key={i} content={textContent} verbose={verbose} /> : <OutputLine key={i} content={textContent} verbose={verbose} />;
});
const textContent =
item.type === 'text' &&
'text' in item &&
item.text !== null &&
item.text !== undefined
? String(item.text)
: ''
return feature('MCP_RICH_OUTPUT') ? (
<MCPTextOutput key={i} content={textContent} verbose={verbose} />
) : (
<OutputLine key={i} content={textContent} verbose={verbose} />
)
})
// Wrap array content in a column layout
contentElement = <Box flexDirection="column" width="100%">
contentElement = (
<Box flexDirection="column" width="100%">
{contentBlocks}
</Box>;
</Box>
)
} else if (!mcpOutput) {
contentElement = <Box justifyContent="space-between" overflowX="hidden" width="100%">
contentElement = (
<Box justifyContent="space-between" overflowX="hidden" width="100%">
<MessageResponse height={1}>
<Text dimColor>(No content)</Text>
</MessageResponse>
</Box>;
</Box>
)
} else {
contentElement = feature('MCP_RICH_OUTPUT') ? <MCPTextOutput content={mcpOutput} verbose={verbose} /> : <OutputLine content={mcpOutput} verbose={verbose} />;
contentElement = feature('MCP_RICH_OUTPUT') ? (
<MCPTextOutput content={mcpOutput} verbose={verbose} />
) : (
<OutputLine content={mcpOutput} verbose={verbose} />
)
}
if (warningMessage) {
return <Box flexDirection="column">
return (
<Box flexDirection="column">
<MessageResponse height={1}>
<Text color="warning">{warningMessage}</Text>
</MessageResponse>
{contentElement}
</Box>;
</Box>
)
}
return contentElement;
return contentElement
}
/**
@@ -156,138 +214,73 @@ export function renderToolResultMessage(output: string | MCPToolResult, _progres
* 2. If JSON is a small flat-ish object, render as aligned key: value.
* 3. Otherwise fall through to OutputLine (pretty-print + truncate).
*/
function MCPTextOutput(t0) {
const $ = _c(18);
const {
content,
verbose
} = t0;
let t1;
if ($[0] !== content || $[1] !== verbose) {
t1 = Symbol.for("react.early_return_sentinel");
bb0: {
const unwrapped = tryUnwrapTextPayload(content);
if (unwrapped !== null) {
const t2 = unwrapped.extras.length > 0 && <Text dimColor={true}>{unwrapped.extras.map(_temp).join(" \xB7 ")}</Text>;
let t3;
if ($[3] !== unwrapped || $[4] !== verbose) {
t3 = <OutputLine content={unwrapped.body} verbose={verbose} linkifyUrls={true} />;
$[3] = unwrapped;
$[4] = verbose;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] !== t2 || $[7] !== t3) {
t4 = <MessageResponse><Box flexDirection="column">{t2}{t3}</Box></MessageResponse>;
$[6] = t2;
$[7] = t3;
$[8] = t4;
} else {
t4 = $[8];
}
t1 = t4;
break bb0;
}
}
$[0] = content;
$[1] = verbose;
$[2] = t1;
} else {
t1 = $[2];
function MCPTextOutput({
content,
verbose,
}: {
content: string
verbose: boolean
}): React.ReactNode {
const unwrapped = tryUnwrapTextPayload(content)
if (unwrapped !== null) {
return (
<MessageResponse>
<Box flexDirection="column">
{unwrapped.extras.length > 0 && (
<Text dimColor>
{unwrapped.extras.map(([k, v]) => `${k}: ${v}`).join(' · ')}
</Text>
)}
<OutputLine content={unwrapped.body} verbose={verbose} linkifyUrls />
</Box>
</MessageResponse>
)
}
if (t1 !== Symbol.for("react.early_return_sentinel")) {
return t1;
const flat = tryFlattenJson(content)
if (flat !== null) {
const maxKeyWidth = Math.max(...flat.map(([k]) => stringWidth(k)))
return (
<MessageResponse>
<Box flexDirection="column">
{flat.map(([key, value], i) => (
<Text key={i}>
<Text dimColor>{key.padEnd(maxKeyWidth)}: </Text>
<Ansi>{linkifyUrlsInText(value)}</Ansi>
</Text>
))}
</Box>
</MessageResponse>
)
}
let t2;
if ($[9] !== content) {
t2 = Symbol.for("react.early_return_sentinel");
bb1: {
const flat = tryFlattenJson(content);
if (flat !== null) {
const maxKeyWidth = Math.max(...flat.map(_temp2));
let t3;
if ($[11] !== maxKeyWidth) {
t3 = (t4, i) => {
const [key, value] = t4;
return <Text key={i}><Text dimColor={true}>{key.padEnd(maxKeyWidth)}: </Text><Ansi>{linkifyUrlsInText(value)}</Ansi></Text>;
};
$[11] = maxKeyWidth;
$[12] = t3;
} else {
t3 = $[12];
}
const t4 = <Box flexDirection="column">{flat.map(t3)}</Box>;
let t5;
if ($[13] !== t4) {
t5 = <MessageResponse>{t4}</MessageResponse>;
$[13] = t4;
$[14] = t5;
} else {
t5 = $[14];
}
t2 = t5;
break bb1;
}
}
$[9] = content;
$[10] = t2;
} else {
t2 = $[10];
}
if (t2 !== Symbol.for("react.early_return_sentinel")) {
return t2;
}
let t3;
if ($[15] !== content || $[16] !== verbose) {
t3 = <OutputLine content={content} verbose={verbose} linkifyUrls={true} />;
$[15] = content;
$[16] = verbose;
$[17] = t3;
} else {
t3 = $[17];
}
return t3;
return <OutputLine content={content} verbose={verbose} linkifyUrls />
}
/**
* Parse content as a JSON object and return its entries. Null if content
* doesn't parse, isn't an object, is too large, or has 0/too-many keys.
*/
function _temp2(t0) {
const [k_0] = t0;
return stringWidth(k_0);
}
function _temp(t0) {
const [k, v] = t0;
return `${k}: ${v}`;
}
function parseJsonEntries(content: string, {
maxChars,
maxKeys
}: {
maxChars: number;
maxKeys: number;
}): [string, unknown][] | null {
const trimmed = content.trim();
function parseJsonEntries(
content: string,
{ maxChars, maxKeys }: { maxChars: number; maxKeys: number },
): [string, unknown][] | null {
const trimmed = content.trim()
if (trimmed.length === 0 || trimmed.length > maxChars || trimmed[0] !== '{') {
return null;
return null
}
let parsed: unknown;
let parsed: unknown
try {
parsed = jsonParse(trimmed);
parsed = jsonParse(trimmed)
} catch {
return null;
return null
}
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
return null;
return null
}
const entries = Object.entries(parsed);
const entries = Object.entries(parsed)
if (entries.length === 0 || entries.length > maxKeys) {
return null;
return null
}
return entries;
return entries
}
/**
@@ -298,24 +291,28 @@ function parseJsonEntries(content: string, {
export function tryFlattenJson(content: string): [string, string][] | null {
const entries = parseJsonEntries(content, {
maxChars: MAX_FLAT_JSON_CHARS,
maxKeys: MAX_FLAT_JSON_KEYS
});
if (entries === null) return null;
const result: [string, string][] = [];
maxKeys: MAX_FLAT_JSON_KEYS,
})
if (entries === null) return null
const result: [string, string][] = []
for (const [key, value] of entries) {
if (typeof value === 'string') {
result.push([key, value]);
} else if (value === null || typeof value === 'number' || typeof value === 'boolean') {
result.push([key, String(value)]);
result.push([key, value])
} else if (
value === null ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
result.push([key, String(value)])
} else if (typeof value === 'object') {
const compact = jsonStringify(value);
if (compact.length > 120) return null;
result.push([key, compact]);
const compact = jsonStringify(value)
if (compact.length > 120) return null
result.push([key, compact])
} else {
return null;
return null
}
}
return result;
return result
}
/**
@@ -324,43 +321,46 @@ export function tryFlattenJson(content: string): [string, string][] | null {
* handles the common MCP pattern of {"messages":"line1\nline2..."} where
* pretty-printing keeps \n escaped but we want real line breaks + truncation.
*/
export function tryUnwrapTextPayload(content: string): {
body: string;
extras: [string, string][];
} | null {
export function tryUnwrapTextPayload(
content: string,
): { body: string; extras: [string, string][] } | null {
const entries = parseJsonEntries(content, {
maxChars: MAX_JSON_PARSE_CHARS,
maxKeys: 4
});
if (entries === null) return null;
maxKeys: 4,
})
if (entries === null) return null
// Find the one dominant string payload. Trim first: a trailing \n on a
// short sibling (e.g. pagination hints) shouldn't make it "dominant".
let body: string | null = null;
const extras: [string, string][] = [];
let body: string | null = null
const extras: [string, string][] = []
for (const [key, value] of entries) {
if (typeof value === 'string') {
const t = value.trimEnd();
const isDominant = t.length > UNWRAP_MIN_STRING_LEN || t.includes('\n') && t.length > 50;
const t = value.trimEnd()
const isDominant =
t.length > UNWRAP_MIN_STRING_LEN || (t.includes('\n') && t.length > 50)
if (isDominant) {
if (body !== null) return null; // two big strings — ambiguous
body = t;
continue;
if (body !== null) return null // two big strings — ambiguous
body = t
continue
}
if (t.length > 150) return null;
extras.push([key, t.replace(/\s+/g, ' ')]);
} else if (value === null || typeof value === 'number' || typeof value === 'boolean') {
extras.push([key, String(value)]);
if (t.length > 150) return null
extras.push([key, t.replace(/\s+/g, ' ')])
} else if (
value === null ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
extras.push([key, String(value)])
} else {
return null; // nested object/array — use flat or pretty-print path
return null // nested object/array — use flat or pretty-print path
}
}
if (body === null) return null;
return {
body,
extras
};
if (body === null) return null
return { body, extras }
}
const SLACK_ARCHIVES_RE = /^https:\/\/[a-z0-9-]+\.slack\.com\/archives\/([A-Z0-9]+)\/p\d+$/;
const SLACK_ARCHIVES_RE =
/^https:\/\/[a-z0-9-]+\.slack\.com\/archives\/([A-Z0-9]+)\/p\d+$/
/**
* Detect a Slack send-message result and return a compact {channel, url} pair.
@@ -369,34 +369,27 @@ const SLACK_ARCHIVES_RE = /^https:\/\/[a-z0-9-]+\.slack\.com\/archives\/([A-Z0-9
* tool input (may be a name like "#foo" or an ID like "C09EVDAN1NK") and
* falls back to the ID parsed from the archives URL.
*/
export function trySlackSendCompact(output: string | MCPToolResult, input: unknown): {
channel: string;
url: string;
} | null {
let text: unknown = output;
export function trySlackSendCompact(
output: string | MCPToolResult,
input: unknown,
): { channel: string; url: string } | null {
let text: unknown = output
if (Array.isArray(output)) {
const block = output.find(b => b.type === 'text');
text = block && 'text' in block ? block.text : undefined;
const block = output.find(b => b.type === 'text')
text = block && 'text' in block ? block.text : undefined
}
if (typeof text !== 'string' || !text.includes('"message_link"')) {
return null;
return null
}
const entries = parseJsonEntries(text, {
maxChars: 2000,
maxKeys: 6
});
const url = entries?.find(([k]) => k === 'message_link')?.[1];
if (typeof url !== 'string') return null;
const m = SLACK_ARCHIVES_RE.exec(url);
if (!m) return null;
const inp = input as {
channel_id?: unknown;
channel?: unknown;
} | undefined;
const raw = inp?.channel_id ?? inp?.channel ?? m[1];
const label = typeof raw === 'string' && raw ? raw : 'slack';
return {
channel: label.startsWith('#') ? label : `#${label}`,
url
};
const entries = parseJsonEntries(text, { maxChars: 2000, maxKeys: 6 })
const url = entries?.find(([k]) => k === 'message_link')?.[1]
if (typeof url !== 'string') return null
const m = SLACK_ARCHIVES_RE.exec(url)
if (!m) return null
const inp = input as { channel_id?: unknown; channel?: unknown } | undefined
const raw = inp?.channel_id ?? inp?.channel ?? m[1]
const label = typeof raw === 'string' && raw ? raw : 'slack'
return { channel: label.startsWith('#') ? label : `#${label}`, url }
}

View File

@@ -1,85 +1,116 @@
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import * as React from 'react';
import type { Message, ProgressMessage } from 'src/types/message.js';
import { extractTag } from 'src/utils/messages.js';
import type { ThemeName } from 'src/utils/theme.js';
import type { z } from 'zod/v4';
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js';
import { FilePathLink } from '../../components/FilePathLink.js';
import { HighlightedCode } from '../../components/HighlightedCode.js';
import { MessageResponse } from '../../components/MessageResponse.js';
import { NotebookEditToolUseRejectedMessage } from '../../components/NotebookEditToolUseRejectedMessage.js';
import { Box, Text } from '../../ink.js';
import type { Tools } from '../../Tool.js';
import { getDisplayPath } from '../../utils/file.js';
import type { inputSchema, Output } from './NotebookEditTool.js';
export function getToolUseSummary(input: Partial<z.infer<ReturnType<typeof inputSchema>>> | undefined): string | null {
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import * as React from 'react'
import type { Message, ProgressMessage } from 'src/types/message.js'
import { extractTag } from 'src/utils/messages.js'
import type { ThemeName } from 'src/utils/theme.js'
import type { z } from 'zod/v4'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { FilePathLink } from '../../components/FilePathLink.js'
import { HighlightedCode } from '../../components/HighlightedCode.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { NotebookEditToolUseRejectedMessage } from '../../components/NotebookEditToolUseRejectedMessage.js'
import { Box, Text } from '../../ink.js'
import type { Tools } from '../../Tool.js'
import { getDisplayPath } from '../../utils/file.js'
import type { inputSchema, Output } from './NotebookEditTool.js'
export function getToolUseSummary(
input: Partial<z.infer<ReturnType<typeof inputSchema>>> | undefined,
): string | null {
if (!input?.notebook_path) {
return null;
return null
}
return getDisplayPath(input.notebook_path);
return getDisplayPath(input.notebook_path)
}
export function renderToolUseMessage({
notebook_path,
cell_id,
new_source,
cell_type,
edit_mode
}: Partial<z.infer<ReturnType<typeof inputSchema>>>, {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
export function renderToolUseMessage(
{
notebook_path,
cell_id,
new_source,
cell_type,
edit_mode,
}: Partial<z.infer<ReturnType<typeof inputSchema>>>,
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!notebook_path || !new_source || !cell_type) {
return null;
return null
}
const displayPath = verbose ? notebook_path : getDisplayPath(notebook_path);
const displayPath = verbose ? notebook_path : getDisplayPath(notebook_path)
if (verbose) {
return <>
return (
<>
<FilePathLink filePath={notebook_path}>{displayPath}</FilePathLink>
{`@${cell_id}, content: ${new_source.slice(0, 30)}…, cell_type: ${cell_type}, edit_mode: ${edit_mode ?? 'replace'}`}
</>;
</>
)
}
return <>
return (
<>
<FilePathLink filePath={notebook_path}>{displayPath}</FilePathLink>
{`@${cell_id}`}
</>;
</>
)
}
export function renderToolUseRejectedMessage(input: z.infer<ReturnType<typeof inputSchema>>, {
verbose
}: {
columns?: number;
messages?: Message[];
progressMessagesForMessage?: ProgressMessage[];
theme?: ThemeName;
tools?: Tools;
verbose: boolean;
}): React.ReactNode {
return <NotebookEditToolUseRejectedMessage notebook_path={input.notebook_path} cell_id={input.cell_id} new_source={input.new_source} cell_type={input.cell_type} edit_mode={input.edit_mode} verbose={verbose} />;
export function renderToolUseRejectedMessage(
input: z.infer<ReturnType<typeof inputSchema>>,
{
verbose,
}: {
columns?: number
messages?: Message[]
progressMessagesForMessage?: ProgressMessage[]
theme?: ThemeName
tools?: Tools
verbose: boolean
},
): React.ReactNode {
return (
<NotebookEditToolUseRejectedMessage
notebook_path={input.notebook_path}
cell_id={input.cell_id}
new_source={input.new_source}
cell_type={input.cell_type}
edit_mode={input.edit_mode}
verbose={verbose}
/>
)
}
export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
if (!verbose && typeof result === 'string' && extractTag(result, 'tool_use_error')) {
return <MessageResponse>
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (
!verbose &&
typeof result === 'string' &&
extractTag(result, 'tool_use_error')
) {
return (
<MessageResponse>
<Text color="error">Error editing notebook</Text>
</MessageResponse>;
</MessageResponse>
)
}
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
}
export function renderToolResultMessage({
cell_id,
new_source,
error
error,
}: Output): React.ReactNode {
if (error) {
return <MessageResponse>
return (
<MessageResponse>
<Text color="error">{error}</Text>
</MessageResponse>;
</MessageResponse>
)
}
return <MessageResponse>
return (
<MessageResponse>
<Box flexDirection="column">
<Text>
Updated cell <Text bold>{cell_id}</Text>:
@@ -88,5 +119,6 @@ export function renderToolResultMessage({
<HighlightedCode code={new_source} filePath="notebook.py" />
</Box>
</Box>
</MessageResponse>;
</MessageResponse>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,130 +1,181 @@
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import * as React from 'react';
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js';
import { MessageResponse } from '../../components/MessageResponse.js';
import { OutputLine } from '../../components/shell/OutputLine.js';
import { ShellProgressMessage } from '../../components/shell/ShellProgressMessage.js';
import { ShellTimeDisplay } from '../../components/shell/ShellTimeDisplay.js';
import { Box, Text } from '../../ink.js';
import type { Tool } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import type { PowerShellProgress } from '../../types/tools.js';
import type { ThemeName } from '../../utils/theme.js';
import type { Out, PowerShellToolInput } from './PowerShellTool.js';
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import * as React from 'react'
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { OutputLine } from '../../components/shell/OutputLine.js'
import { ShellProgressMessage } from '../../components/shell/ShellProgressMessage.js'
import { ShellTimeDisplay } from '../../components/shell/ShellTimeDisplay.js'
import { Box, Text } from '../../ink.js'
import type { Tool } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import type { PowerShellProgress } from '../../types/tools.js'
import type { ThemeName } from '../../utils/theme.js'
import type { Out, PowerShellToolInput } from './PowerShellTool.js'
// Constants for command display
const MAX_COMMAND_DISPLAY_LINES = 2;
const MAX_COMMAND_DISPLAY_CHARS = 160;
export function renderToolUseMessage(input: Partial<PowerShellToolInput>, {
verbose,
theme: _theme
}: {
verbose: boolean;
theme: ThemeName;
}): React.ReactNode {
const {
command
} = input;
const MAX_COMMAND_DISPLAY_LINES = 2
const MAX_COMMAND_DISPLAY_CHARS = 160
export function renderToolUseMessage(
input: Partial<PowerShellToolInput>,
{ verbose, theme: _theme }: { verbose: boolean; theme: ThemeName },
): React.ReactNode {
const { command } = input
if (!command) {
return null;
return null
}
const displayCommand = command;
const displayCommand = command
if (!verbose) {
const lines = displayCommand.split('\n');
const needsLineTruncation = lines.length > MAX_COMMAND_DISPLAY_LINES;
const needsCharTruncation = displayCommand.length > MAX_COMMAND_DISPLAY_CHARS;
const lines = displayCommand.split('\n')
const needsLineTruncation = lines.length > MAX_COMMAND_DISPLAY_LINES
const needsCharTruncation =
displayCommand.length > MAX_COMMAND_DISPLAY_CHARS
if (needsLineTruncation || needsCharTruncation) {
let truncated = displayCommand;
let truncated = displayCommand
if (needsLineTruncation) {
truncated = lines.slice(0, MAX_COMMAND_DISPLAY_LINES).join('\n');
truncated = lines.slice(0, MAX_COMMAND_DISPLAY_LINES).join('\n')
}
if (truncated.length > MAX_COMMAND_DISPLAY_CHARS) {
truncated = truncated.slice(0, MAX_COMMAND_DISPLAY_CHARS);
truncated = truncated.slice(0, MAX_COMMAND_DISPLAY_CHARS)
}
return <Text>{truncated.trim()}</Text>;
return <Text>{truncated.trim()}</Text>
}
}
return displayCommand;
return displayCommand
}
export function renderToolUseProgressMessage(progressMessagesForMessage: ProgressMessage<PowerShellProgress>[], {
verbose,
tools: _tools,
terminalSize: _terminalSize,
inProgressToolCallCount: _inProgressToolCallCount
}: {
tools: Tool[];
verbose: boolean;
terminalSize?: {
columns: number;
rows: number;
};
inProgressToolCallCount?: number;
}): React.ReactNode {
const lastProgress = progressMessagesForMessage.at(-1);
export function renderToolUseProgressMessage(
progressMessagesForMessage: ProgressMessage<PowerShellProgress>[],
{
verbose,
tools: _tools,
terminalSize: _terminalSize,
inProgressToolCallCount: _inProgressToolCallCount,
}: {
tools: Tool[]
verbose: boolean
terminalSize?: { columns: number; rows: number }
inProgressToolCallCount?: number
},
): React.ReactNode {
const lastProgress = progressMessagesForMessage.at(-1)
if (!lastProgress || !lastProgress.data) {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>Running</Text>
</MessageResponse>;
</MessageResponse>
)
}
const data = lastProgress.data;
return <ShellProgressMessage fullOutput={data.fullOutput} output={data.output} elapsedTimeSeconds={data.elapsedTimeSeconds} totalLines={data.totalLines} totalBytes={data.totalBytes} timeoutMs={data.timeoutMs} taskId={data.taskId} verbose={verbose} />;
const data = lastProgress.data
return (
<ShellProgressMessage
fullOutput={data.fullOutput}
output={data.output}
elapsedTimeSeconds={data.elapsedTimeSeconds}
totalLines={data.totalLines}
totalBytes={data.totalBytes}
timeoutMs={data.timeoutMs}
taskId={data.taskId}
verbose={verbose}
/>
)
}
export function renderToolUseQueuedMessage(): React.ReactNode {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>Waiting</Text>
</MessageResponse>;
</MessageResponse>
)
}
export function renderToolResultMessage(content: Out, progressMessagesForMessage: ProgressMessage<PowerShellProgress>[], {
verbose,
theme: _theme,
tools: _tools,
style: _style
}: {
verbose: boolean;
theme: ThemeName;
tools: Tool[];
style?: 'condensed';
}): React.ReactNode {
const lastProgress = progressMessagesForMessage.at(-1);
const timeoutMs = lastProgress?.data?.timeoutMs;
export function renderToolResultMessage(
content: Out,
progressMessagesForMessage: ProgressMessage<PowerShellProgress>[],
{
verbose,
theme: _theme,
tools: _tools,
style: _style,
}: {
verbose: boolean
theme: ThemeName
tools: Tool[]
style?: 'condensed'
},
): React.ReactNode {
const lastProgress = progressMessagesForMessage.at(-1)
const timeoutMs = lastProgress?.data?.timeoutMs
const {
stdout,
stderr,
interrupted,
returnCodeInterpretation,
isImage,
backgroundTaskId
} = content;
backgroundTaskId,
} = content
if (isImage) {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>[Image data detected and sent to Claude]</Text>
</MessageResponse>;
</MessageResponse>
)
}
return <Box flexDirection="column">
return (
<Box flexDirection="column">
{stdout !== '' ? <OutputLine content={stdout} verbose={verbose} /> : null}
{stderr.trim() !== '' ? <OutputLine content={stderr} verbose={verbose} isError /> : null}
{stdout === '' && stderr.trim() === '' ? <MessageResponse height={1}>
{stderr.trim() !== '' ? (
<OutputLine content={stderr} verbose={verbose} isError />
) : null}
{stdout === '' && stderr.trim() === '' ? (
<MessageResponse height={1}>
<Text dimColor>
{backgroundTaskId ? <>
{backgroundTaskId ? (
<>
Running in the background{' '}
<KeyboardShortcutHint shortcut="↓" action="manage" parens />
</> : interrupted ? 'Interrupted' : returnCodeInterpretation || '(No output)'}
</>
) : interrupted ? (
'Interrupted'
) : (
returnCodeInterpretation || '(No output)'
)}
</Text>
</MessageResponse> : null}
{timeoutMs ? <MessageResponse>
</MessageResponse>
) : null}
{timeoutMs ? (
<MessageResponse>
<ShellTimeDisplay timeoutMs={timeoutMs} />
</MessageResponse> : null}
</Box>;
</MessageResponse>
) : null}
</Box>
)
}
export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], {
verbose,
progressMessagesForMessage: _progressMessagesForMessage,
tools: _tools
}: {
verbose: boolean;
progressMessagesForMessage: ProgressMessage<PowerShellProgress>[];
tools: Tool[];
}): React.ReactNode {
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />;
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
{
verbose,
progressMessagesForMessage: _progressMessagesForMessage,
tools: _tools,
}: {
verbose: boolean
progressMessagesForMessage: ProgressMessage<PowerShellProgress>[]
tools: Tool[]
},
): React.ReactNode {
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
}

View File

@@ -1,36 +1,44 @@
import * as React from 'react';
import type { z } from 'zod/v4';
import { MessageResponse } from '../../components/MessageResponse.js';
import { OutputLine } from '../../components/shell/OutputLine.js';
import { Box, Text } from '../../ink.js';
import type { ToolProgressData } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import { jsonStringify } from '../../utils/slowOperations.js';
import type { inputSchema, Output } from './ReadMcpResourceTool.js';
export function renderToolUseMessage(input: Partial<z.infer<ReturnType<typeof inputSchema>>>): React.ReactNode {
import * as React from 'react'
import type { z } from 'zod/v4'
import { MessageResponse } from '../../components/MessageResponse.js'
import { OutputLine } from '../../components/shell/OutputLine.js'
import { Box, Text } from '../../ink.js'
import type { ToolProgressData } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import type { inputSchema, Output } from './ReadMcpResourceTool.js'
export function renderToolUseMessage(
input: Partial<z.infer<ReturnType<typeof inputSchema>>>,
): React.ReactNode {
if (!input.uri || !input.server) {
return null;
return null
}
return `Read resource "${input.uri}" from server "${input.server}"`;
return `Read resource "${input.uri}" from server "${input.server}"`
}
export function userFacingName(): string {
return 'readMcpResource';
return 'readMcpResource'
}
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
export function renderToolResultMessage(
output: Output,
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!output || !output.contents || output.contents.length === 0) {
return <Box justifyContent="space-between" overflowX="hidden" width="100%">
return (
<Box justifyContent="space-between" overflowX="hidden" width="100%">
<MessageResponse height={1}>
<Text dimColor>(No content)</Text>
</MessageResponse>
</Box>;
</Box>
)
}
// Format as JSON for better readability
// eslint-disable-next-line no-restricted-syntax -- human-facing UI, not tool_result
const formattedOutput = jsonStringify(output, null, 2);
return <OutputLine content={formattedOutput} verbose={verbose} />;
const formattedOutput = jsonStringify(output, null, 2)
return <OutputLine content={formattedOutput} verbose={verbose} />
}

View File

@@ -1,16 +1,20 @@
import React from 'react';
import { MessageResponse } from '../../components/MessageResponse.js';
import { Text } from '../../ink.js';
import { countCharInString } from '../../utils/stringUtils.js';
import type { Input, Output } from './RemoteTriggerTool.js';
import React from 'react'
import { MessageResponse } from '../../components/MessageResponse.js'
import { Text } from '../../ink.js'
import { countCharInString } from '../../utils/stringUtils.js'
import type { Input, Output } from './RemoteTriggerTool.js'
export function renderToolUseMessage(input: Partial<Input>): React.ReactNode {
return `${input.action ?? ''}${input.trigger_id ? ` ${input.trigger_id}` : ''}`;
return `${input.action ?? ''}${input.trigger_id ? ` ${input.trigger_id}` : ''}`
}
export function renderToolResultMessage(output: Output): React.ReactNode {
const lines = countCharInString(output.json, '\n') + 1;
return <MessageResponse>
const lines = countCharInString(output.json, '\n') + 1
return (
<MessageResponse>
<Text>
HTTP {output.status} <Text dimColor>({lines} lines)</Text>
</Text>
</MessageResponse>;
</MessageResponse>
)
}

View File

@@ -1,59 +1,75 @@
import React from 'react';
import { MessageResponse } from '../../components/MessageResponse.js';
import { Text } from '../../ink.js';
import { truncate } from '../../utils/format.js';
import type { CreateOutput } from './CronCreateTool.js';
import type { DeleteOutput } from './CronDeleteTool.js';
import type { ListOutput } from './CronListTool.js';
import React from 'react'
import { MessageResponse } from '../../components/MessageResponse.js'
import { Text } from '../../ink.js'
import { truncate } from '../../utils/format.js'
import type { CreateOutput } from './CronCreateTool.js'
import type { DeleteOutput } from './CronDeleteTool.js'
import type { ListOutput } from './CronListTool.js'
// --- CronCreate -------------------------------------------------------------
export function renderCreateToolUseMessage(input: Partial<{
cron: string;
prompt: string;
}>): React.ReactNode {
return `${input.cron ?? ''}${input.prompt ? `: ${truncate(input.prompt, 60, true)}` : ''}`;
export function renderCreateToolUseMessage(
input: Partial<{ cron: string; prompt: string }>,
): React.ReactNode {
return `${input.cron ?? ''}${input.prompt ? `: ${truncate(input.prompt, 60, true)}` : ''}`
}
export function renderCreateResultMessage(output: CreateOutput): React.ReactNode {
return <MessageResponse>
export function renderCreateResultMessage(
output: CreateOutput,
): React.ReactNode {
return (
<MessageResponse>
<Text>
Scheduled <Text bold>{output.id}</Text>{' '}
<Text dimColor>({output.humanSchedule})</Text>
</Text>
</MessageResponse>;
</MessageResponse>
)
}
// --- CronDelete -------------------------------------------------------------
export function renderDeleteToolUseMessage(input: Partial<{
id: string;
}>): React.ReactNode {
return input.id ?? '';
export function renderDeleteToolUseMessage(
input: Partial<{ id: string }>,
): React.ReactNode {
return input.id ?? ''
}
export function renderDeleteResultMessage(output: DeleteOutput): React.ReactNode {
return <MessageResponse>
export function renderDeleteResultMessage(
output: DeleteOutput,
): React.ReactNode {
return (
<MessageResponse>
<Text>
Cancelled <Text bold>{output.id}</Text>
</Text>
</MessageResponse>;
</MessageResponse>
)
}
// --- CronList ---------------------------------------------------------------
export function renderListToolUseMessage(): React.ReactNode {
return '';
return ''
}
export function renderListResultMessage(output: ListOutput): React.ReactNode {
if (output.jobs.length === 0) {
return <MessageResponse>
return (
<MessageResponse>
<Text dimColor>No scheduled jobs</Text>
</MessageResponse>;
</MessageResponse>
)
}
return <MessageResponse>
{output.jobs.map(j => <Text key={j.id}>
return (
<MessageResponse>
{output.jobs.map(j => (
<Text key={j.id}>
<Text bold>{j.id}</Text> <Text dimColor>{j.humanSchedule}</Text>
</Text>)}
</MessageResponse>;
</Text>
))}
</MessageResponse>
)
}
// --- Shared -----------------------------------------------------------------

View File

@@ -1,30 +1,40 @@
import React from 'react';
import { MessageResponse } from '../../components/MessageResponse.js';
import { Text } from '../../ink.js';
import { jsonParse } from '../../utils/slowOperations.js';
import type { Input, SendMessageToolOutput } from './SendMessageTool.js';
import React from 'react'
import { MessageResponse } from '../../components/MessageResponse.js'
import { Text } from '../../ink.js'
import { jsonParse } from '../../utils/slowOperations.js'
import type { Input, SendMessageToolOutput } from './SendMessageTool.js'
export function renderToolUseMessage(input: Partial<Input>): React.ReactNode {
if (typeof input.message !== 'object' || input.message === null) {
return null;
return null
}
if (input.message.type === 'plan_approval_response') {
return input.message.approve ? `approve plan from: ${input.to}` : `reject plan from: ${input.to}`;
return input.message.approve
? `approve plan from: ${input.to}`
: `reject plan from: ${input.to}`
}
return null;
return null
}
export function renderToolResultMessage(content: SendMessageToolOutput | string, _progressMessages: unknown, {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
const result: SendMessageToolOutput = typeof content === 'string' ? jsonParse(content) : content;
export function renderToolResultMessage(
content: SendMessageToolOutput | string,
_progressMessages: unknown,
{ verbose }: { verbose: boolean },
): React.ReactNode {
const result: SendMessageToolOutput =
typeof content === 'string' ? jsonParse(content) : content
if ('routing' in result && result.routing) {
return null;
return null
}
if ('request_id' in result && 'target' in result) {
return null;
return null
}
return <MessageResponse>
return (
<MessageResponse>
<Text dimColor>{result.message}</Text>
</MessageResponse>;
</MessageResponse>
)
}

View File

@@ -1,127 +1,181 @@
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import * as React from 'react';
import { SubAgentProvider } from 'src/components/CtrlOToExpand.js';
import { FallbackToolUseErrorMessage } from 'src/components/FallbackToolUseErrorMessage.js';
import { FallbackToolUseRejectedMessage } from 'src/components/FallbackToolUseRejectedMessage.js';
import type { z } from 'zod/v4';
import type { Command } from '../../commands.js';
import { Byline } from '../../components/design-system/Byline.js';
import { Message as MessageComponent } from '../../components/Message.js';
import { MessageResponse } from '../../components/MessageResponse.js';
import { Box, Text } from '../../ink.js';
import type { Tools } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import { buildSubagentLookups, EMPTY_LOOKUPS } from '../../utils/messages.js';
import { plural } from '../../utils/stringUtils.js';
import type { inputSchema, Output, Progress } from './SkillTool.js';
type Input = z.infer<ReturnType<typeof inputSchema>>;
const MAX_PROGRESS_MESSAGES_TO_SHOW = 3;
const INITIALIZING_TEXT = 'Initializing…';
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import * as React from 'react'
import { SubAgentProvider } from 'src/components/CtrlOToExpand.js'
import { FallbackToolUseErrorMessage } from 'src/components/FallbackToolUseErrorMessage.js'
import { FallbackToolUseRejectedMessage } from 'src/components/FallbackToolUseRejectedMessage.js'
import type { z } from 'zod/v4'
import type { Command } from '../../commands.js'
import { Byline } from '../../components/design-system/Byline.js'
import { Message as MessageComponent } from '../../components/Message.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { Box, Text } from '../../ink.js'
import type { Tools } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import { buildSubagentLookups, EMPTY_LOOKUPS } from '../../utils/messages.js'
import { plural } from '../../utils/stringUtils.js'
import type { inputSchema, Output, Progress } from './SkillTool.js'
type Input = z.infer<ReturnType<typeof inputSchema>>
const MAX_PROGRESS_MESSAGES_TO_SHOW = 3
const INITIALIZING_TEXT = 'Initializing…'
export function renderToolResultMessage(output: Output): React.ReactNode {
// Handle forked skill result
if ('status' in output && output.status === 'forked') {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text>
<Byline>{['Done']}</Byline>
</Text>
</MessageResponse>;
</MessageResponse>
)
}
const parts: string[] = ['Successfully loaded skill'];
const parts: string[] = ['Successfully loaded skill']
// Show tools count (only for inline skills)
if ('allowedTools' in output && output.allowedTools && output.allowedTools.length > 0) {
const count = output.allowedTools.length;
parts.push(`${count} ${plural(count, 'tool')} allowed`);
if (
'allowedTools' in output &&
output.allowedTools &&
output.allowedTools.length > 0
) {
const count = output.allowedTools.length
parts.push(`${count} ${plural(count, 'tool')} allowed`)
}
// Show model if non-default (only for inline skills)
if ('model' in output && output.model) {
parts.push(output.model);
parts.push(output.model)
}
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text>
<Byline>{parts}</Byline>
</Text>
</MessageResponse>;
</MessageResponse>
)
}
export function renderToolUseMessage({
skill
}: Partial<Input>, {
commands
}: {
commands?: Command[];
}): React.ReactNode {
export function renderToolUseMessage(
{ skill }: Partial<Input>,
{ commands }: { commands?: Command[] },
): React.ReactNode {
if (!skill) {
return null;
return null
}
// Look up the command to check if it came from the legacy /commands folder
const command = commands?.find(c => c.name === skill);
const displayName = command?.loadedFrom === 'commands_DEPRECATED' ? `/${skill}` : skill;
return displayName;
const command = commands?.find(c => c.name === skill)
const displayName =
command?.loadedFrom === 'commands_DEPRECATED' ? `/${skill}` : skill
return displayName
}
export function renderToolUseProgressMessage(progressMessages: ProgressMessage<Progress>[], {
tools,
verbose
}: {
tools: Tools;
verbose: boolean;
}): React.ReactNode {
export function renderToolUseProgressMessage(
progressMessages: ProgressMessage<Progress>[],
{
tools,
verbose,
}: {
tools: Tools
verbose: boolean
},
): React.ReactNode {
if (!progressMessages.length) {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>{INITIALIZING_TEXT}</Text>
</MessageResponse>;
</MessageResponse>
)
}
// Take only the last few messages for display in non-verbose mode
const displayedMessages = verbose ? progressMessages : progressMessages.slice(-MAX_PROGRESS_MESSAGES_TO_SHOW);
const hiddenCount = progressMessages.length - displayedMessages.length;
const {
inProgressToolUseIDs
} = buildSubagentLookups(progressMessages.map(pm => pm.data));
return <MessageResponse>
const displayedMessages = verbose
? progressMessages
: progressMessages.slice(-MAX_PROGRESS_MESSAGES_TO_SHOW)
const hiddenCount = progressMessages.length - displayedMessages.length
const { inProgressToolUseIDs } = buildSubagentLookups(
progressMessages.map(pm => pm.data),
)
return (
<MessageResponse>
<Box flexDirection="column">
<SubAgentProvider>
{displayedMessages.map(progressMessage => <Box key={progressMessage.uuid} height={1} overflow="hidden">
<MessageComponent message={progressMessage.data.message} lookups={EMPTY_LOOKUPS} addMargin={false} tools={tools} commands={[]} verbose={verbose} inProgressToolUseIDs={inProgressToolUseIDs} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} style="condensed" isTranscriptMode={false} isStatic={true} />
</Box>)}
{displayedMessages.map(progressMessage => (
<Box key={progressMessage.uuid} height={1} overflow="hidden">
<MessageComponent
message={progressMessage.data.message}
lookups={EMPTY_LOOKUPS}
addMargin={false}
tools={tools}
commands={[]}
verbose={verbose}
inProgressToolUseIDs={inProgressToolUseIDs}
progressMessagesForMessage={[]}
shouldAnimate={false}
shouldShowDot={false}
style="condensed"
isTranscriptMode={false}
isStatic={true}
/>
</Box>
))}
</SubAgentProvider>
{hiddenCount > 0 && <Text dimColor>
{hiddenCount > 0 && (
<Text dimColor>
+{hiddenCount} more tool {plural(hiddenCount, 'use')}
</Text>}
</Text>
)}
</Box>
</MessageResponse>;
</MessageResponse>
)
}
export function renderToolUseRejectedMessage(_input: Input, {
progressMessagesForMessage,
tools,
verbose
}: {
progressMessagesForMessage: ProgressMessage<Progress>[];
tools: Tools;
verbose: boolean;
}): React.ReactNode {
return <>
export function renderToolUseRejectedMessage(
_input: Input,
{
progressMessagesForMessage,
tools,
verbose,
}: {
progressMessagesForMessage: ProgressMessage<Progress>[]
tools: Tools
verbose: boolean
},
): React.ReactNode {
return (
<>
{renderToolUseProgressMessage(progressMessagesForMessage, {
tools,
verbose
})}
tools,
verbose,
})}
<FallbackToolUseRejectedMessage />
</>;
</>
)
}
export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], {
progressMessagesForMessage,
tools,
verbose
}: {
progressMessagesForMessage: ProgressMessage<Progress>[];
tools: Tools;
verbose: boolean;
}): React.ReactNode {
return <>
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
{
progressMessagesForMessage,
tools,
verbose,
}: {
progressMessagesForMessage: ProgressMessage<Progress>[]
tools: Tools
verbose: boolean
},
): React.ReactNode {
return (
<>
{renderToolUseProgressMessage(progressMessagesForMessage, {
tools,
verbose
})}
tools,
verbose,
})}
<FallbackToolUseErrorMessage result={result} verbose={verbose} />
</>;
</>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,51 @@
import React from 'react';
import { MessageResponse } from '../../components/MessageResponse.js';
import { stringWidth } from '../../ink/stringWidth.js';
import { Text } from '../../ink.js';
import { truncateToWidthNoEllipsis } from '../../utils/format.js';
import type { Output } from './TaskStopTool.js';
import React from 'react'
import { MessageResponse } from '../../components/MessageResponse.js'
import { stringWidth } from '../../ink/stringWidth.js'
import { Text } from '../../ink.js'
import { truncateToWidthNoEllipsis } from '../../utils/format.js'
import type { Output } from './TaskStopTool.js'
export function renderToolUseMessage(): React.ReactNode {
return '';
return ''
}
const MAX_COMMAND_DISPLAY_LINES = 2;
const MAX_COMMAND_DISPLAY_CHARS = 160;
const MAX_COMMAND_DISPLAY_LINES = 2
const MAX_COMMAND_DISPLAY_CHARS = 160
function truncateCommand(command: string): string {
const lines = command.split('\n');
let truncated = command;
const lines = command.split('\n')
let truncated = command
if (lines.length > MAX_COMMAND_DISPLAY_LINES) {
truncated = lines.slice(0, MAX_COMMAND_DISPLAY_LINES).join('\n');
truncated = lines.slice(0, MAX_COMMAND_DISPLAY_LINES).join('\n')
}
if (stringWidth(truncated) > MAX_COMMAND_DISPLAY_CHARS) {
truncated = truncateToWidthNoEllipsis(truncated, MAX_COMMAND_DISPLAY_CHARS);
truncated = truncateToWidthNoEllipsis(truncated, MAX_COMMAND_DISPLAY_CHARS)
}
return truncated.trim();
return truncated.trim()
}
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: unknown[], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
if ((process.env.USER_TYPE) === 'ant') {
return null;
export function renderToolResultMessage(
output: Output,
_progressMessagesForMessage: unknown[],
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (process.env.USER_TYPE === 'ant') {
return null
}
const rawCommand = output.command ?? '';
const command = verbose ? rawCommand : truncateCommand(rawCommand);
const suffix = command !== rawCommand ? '… · stopped' : ' · stopped';
return <MessageResponse>
const rawCommand = output.command ?? ''
const command = verbose ? rawCommand : truncateCommand(rawCommand)
const suffix = command !== rawCommand ? '… · stopped' : ' · stopped'
return (
<MessageResponse>
<Text>
{command}
{suffix}
</Text>
</MessageResponse>;
</MessageResponse>
)
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import type { Input } from './TeamCreateTool.js';
import React from 'react'
import type { Input } from './TeamCreateTool.js'
export function renderToolUseMessage(input: Partial<Input>): React.ReactNode {
return `create team: ${input.team_name}`;
return `create team: ${input.team_name}`
}

View File

@@ -1,19 +1,25 @@
import React from 'react';
import { jsonParse } from '../../utils/slowOperations.js';
import type { Output } from './TeamDeleteTool.js';
export function renderToolUseMessage(_input: Record<string, unknown>): React.ReactNode {
return 'cleanup team: current';
import React from 'react'
import { jsonParse } from '../../utils/slowOperations.js'
import type { Output } from './TeamDeleteTool.js'
export function renderToolUseMessage(
_input: Record<string, unknown>,
): React.ReactNode {
return 'cleanup team: current'
}
export function renderToolResultMessage(content: Output | string, _progressMessages: unknown, {
verbose: _verbose
}: {
verbose: boolean;
}): React.ReactNode {
const result: Output = typeof content === 'string' ? jsonParse(content) : content;
export function renderToolResultMessage(
content: Output | string,
_progressMessages: unknown,
{ verbose: _verbose }: { verbose: boolean },
): React.ReactNode {
const result: Output =
typeof content === 'string' ? jsonParse(content) : content
// Suppress cleanup result - the batched shutdown message covers this
if ('success' in result && 'team_name' in result && 'message' in result) {
return null;
return null
}
return null;
return null
}

View File

@@ -1,49 +1,42 @@
import React from 'react';
import { MessageResponse } from '../../components/MessageResponse.js';
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js';
import { Box, Text } from '../../ink.js';
import type { ToolProgressData } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import { formatFileSize, truncate } from '../../utils/format.js';
import type { Output } from './WebFetchTool.js';
export function renderToolUseMessage({
url,
prompt
}: Partial<{
url: string;
prompt: string;
}>, {
verbose
}: {
theme?: string;
verbose: boolean;
}): React.ReactNode {
import React from 'react'
import { MessageResponse } from '../../components/MessageResponse.js'
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js'
import { Box, Text } from '../../ink.js'
import type { ToolProgressData } from '../../Tool.js'
import type { ProgressMessage } from '../../types/message.js'
import { formatFileSize, truncate } from '../../utils/format.js'
import type { Output } from './WebFetchTool.js'
export function renderToolUseMessage(
{ url, prompt }: Partial<{ url: string; prompt: string }>,
{ verbose }: { theme?: string; verbose: boolean },
): React.ReactNode {
if (!url) {
return null;
return null
}
if (verbose) {
return `url: "${url}"${verbose && prompt ? `, prompt: "${prompt}"` : ''}`;
return `url: "${url}"${verbose && prompt ? `, prompt: "${prompt}"` : ''}`
}
return url;
return url
}
export function renderToolUseProgressMessage(): React.ReactNode {
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text dimColor>Fetching</Text>
</MessageResponse>;
</MessageResponse>
)
}
export function renderToolResultMessage({
bytes,
code,
codeText,
result
}: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
const formattedSize = formatFileSize(bytes);
export function renderToolResultMessage(
{ bytes, code, codeText, result }: Output,
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
{ verbose }: { verbose: boolean },
): React.ReactNode {
const formattedSize = formatFileSize(bytes)
if (verbose) {
return <Box flexDirection="column">
return (
<Box flexDirection="column">
<MessageResponse height={1}>
<Text>
Received <Text bold>{formattedSize}</Text> ({code} {codeText})
@@ -52,20 +45,23 @@ export function renderToolResultMessage({
<Box flexDirection="column">
<Text>{result}</Text>
</Box>
</Box>;
</Box>
)
}
return <MessageResponse height={1}>
return (
<MessageResponse height={1}>
<Text>
Received <Text bold>{formattedSize}</Text> ({code} {codeText})
</Text>
</MessageResponse>;
</MessageResponse>
)
}
export function getToolUseSummary(input: Partial<{
url: string;
prompt: string;
}> | undefined): string | null {
export function getToolUseSummary(
input: Partial<{ url: string; prompt: string }> | undefined,
): string | null {
if (!input?.url) {
return null;
return null
}
return truncate(input.url, TOOL_SUMMARY_MAX_LENGTH);
return truncate(input.url, TOOL_SUMMARY_MAX_LENGTH)
}

View File

@@ -1,100 +1,127 @@
import React from 'react';
import { MessageResponse } from '../../components/MessageResponse.js';
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js';
import { Box, Text } from '../../ink.js';
import type { ProgressMessage } from '../../types/message.js';
import { truncate } from '../../utils/format.js';
import type { Output, SearchResult, WebSearchProgress } from './WebSearchTool.js';
function getSearchSummary(results: (SearchResult | string | null | undefined)[]): {
searchCount: number;
totalResultCount: number;
import React from 'react'
import { MessageResponse } from '../../components/MessageResponse.js'
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js'
import { Box, Text } from '../../ink.js'
import type { ProgressMessage } from '../../types/message.js'
import { truncate } from '../../utils/format.js'
import type {
Output,
SearchResult,
WebSearchProgress,
} from './WebSearchTool.js'
function getSearchSummary(
results: (SearchResult | string | null | undefined)[],
): {
searchCount: number
totalResultCount: number
} {
let searchCount = 0;
let totalResultCount = 0;
let searchCount = 0
let totalResultCount = 0
for (const result of results) {
if (result != null && typeof result !== 'string') {
searchCount++;
totalResultCount += result.content?.length ?? 0;
searchCount++
totalResultCount += result.content?.length ?? 0
}
}
return {
searchCount,
totalResultCount
};
return { searchCount, totalResultCount }
}
export function renderToolUseMessage({
query,
allowed_domains,
blocked_domains
}: Partial<{
query: string;
allowed_domains?: string[];
blocked_domains?: string[];
}>, {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
export function renderToolUseMessage(
{
query,
allowed_domains,
blocked_domains,
}: Partial<{
query: string
allowed_domains?: string[]
blocked_domains?: string[]
}>,
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!query) {
return null;
return null
}
let message = '';
let message = ''
if (query) {
message += `"${query}"`;
message += `"${query}"`
}
if (verbose) {
if (allowed_domains && allowed_domains.length > 0) {
message += `, only allowing domains: ${allowed_domains.join(', ')}`;
message += `, only allowing domains: ${allowed_domains.join(', ')}`
}
if (blocked_domains && blocked_domains.length > 0) {
message += `, blocking domains: ${blocked_domains.join(', ')}`;
message += `, blocking domains: ${blocked_domains.join(', ')}`
}
}
return message;
return message
}
export function renderToolUseProgressMessage(progressMessages: ProgressMessage<WebSearchProgress>[]): React.ReactNode {
export function renderToolUseProgressMessage(
progressMessages: ProgressMessage<WebSearchProgress>[],
): React.ReactNode {
if (progressMessages.length === 0) {
return null;
return null
}
const lastProgress = progressMessages[progressMessages.length - 1];
const lastProgress = progressMessages[progressMessages.length - 1]
if (!lastProgress?.data) {
return null;
return null
}
const data = lastProgress.data;
const data = lastProgress.data
switch (data.type) {
case 'query_update':
return <MessageResponse>
return (
<MessageResponse>
<Text dimColor>Searching: {data.query}</Text>
</MessageResponse>;
</MessageResponse>
)
case 'search_results_received':
return <MessageResponse>
return (
<MessageResponse>
<Text dimColor>
Found {data.resultCount} results for &quot;{data.query}&quot;
</Text>
</MessageResponse>;
</MessageResponse>
)
default:
return null;
return null
}
}
export function renderToolResultMessage(output: Output): React.ReactNode {
const {
searchCount
} = getSearchSummary(output.results ?? []);
const timeDisplay = output.durationSeconds >= 1 ? `${Math.round(output.durationSeconds)}s` : `${Math.round(output.durationSeconds * 1000)}ms`;
return <Box justifyContent="space-between" width="100%">
const { searchCount } = getSearchSummary(output.results ?? [])
const timeDisplay =
output.durationSeconds >= 1
? `${Math.round(output.durationSeconds)}s`
: `${Math.round(output.durationSeconds * 1000)}ms`
return (
<Box justifyContent="space-between" width="100%">
<MessageResponse height={1}>
<Text>
Did {searchCount} search
{searchCount !== 1 ? 'es' : ''} in {timeDisplay}
</Text>
</MessageResponse>
</Box>;
</Box>
)
}
export function getToolUseSummary(input: Partial<{
query: string;
}> | undefined): string | null {
export function getToolUseSummary(
input: Partial<{ query: string }> | undefined,
): string | null {
if (!input?.query) {
return null;
return null
}
return truncate(input.query, TOOL_SUMMARY_MAX_LENGTH);
return truncate(input.query, TOOL_SUMMARY_MAX_LENGTH)
}

View File

@@ -2,72 +2,75 @@
* This testing-only tool will always pop up a permission dialog when called by
* the model.
*/
import { z } from 'zod/v4';
import type { Tool } from '../../Tool.js';
import { buildTool, type ToolDef } from '../../Tool.js';
import { lazySchema } from '../../utils/lazySchema.js';
const NAME = 'TestingPermission';
const inputSchema = lazySchema(() => z.strictObject({}));
type InputSchema = ReturnType<typeof inputSchema>;
import { z } from 'zod/v4'
import type { Tool } from '../../Tool.js'
import { buildTool, type ToolDef } from '../../Tool.js'
import { lazySchema } from '../../utils/lazySchema.js'
const NAME = 'TestingPermission'
const inputSchema = lazySchema(() => z.strictObject({}))
type InputSchema = ReturnType<typeof inputSchema>
export const TestingPermissionTool: Tool<InputSchema, string> = buildTool({
name: NAME,
maxResultSizeChars: 100_000,
async description() {
return 'Test tool that always asks for permission';
return 'Test tool that always asks for permission'
},
async prompt() {
return 'Test tool that always asks for permission before executing. Used for end-to-end testing.';
return 'Test tool that always asks for permission before executing. Used for end-to-end testing.'
},
get inputSchema(): InputSchema {
return inputSchema();
return inputSchema()
},
userFacingName() {
return 'TestingPermission';
return 'TestingPermission'
},
isEnabled() {
return ("production" as string) === 'test';
return "production" === 'test'
},
isConcurrencySafe() {
return true;
return true
},
isReadOnly() {
return true;
return true
},
async checkPermissions() {
// This tool always requires permission
return {
behavior: 'ask' as const,
message: `Run test?`
};
message: `Run test?`,
}
},
renderToolUseMessage() {
return null;
return null
},
renderToolUseProgressMessage() {
return null;
return null
},
renderToolUseQueuedMessage() {
return null;
return null
},
renderToolUseRejectedMessage() {
return null;
return null
},
renderToolResultMessage() {
return null;
return null
},
renderToolUseErrorMessage() {
return null;
return null
},
async call() {
return {
data: `${NAME} executed successfully`
};
data: `${NAME} executed successfully`,
}
},
mapToolResultToToolResultBlockParam(result, toolUseID) {
return {
type: 'tool_result',
content: String(result),
tool_use_id: toolUseID
};
}
} satisfies ToolDef<InputSchema, string>);
tool_use_id: toolUseID,
}
},
} satisfies ToolDef<InputSchema, string>)