mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
更新大量 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:
@@ -1,157 +1,85 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { AdvisorBlock } from '../../utils/advisor.js';
|
||||
import { renderModelName } from '../../utils/model/model.js';
|
||||
import { jsonStringify } from '../../utils/slowOperations.js';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js';
|
||||
import figures from 'figures'
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { AdvisorBlock } from '../../utils/advisor.js'
|
||||
import { renderModelName } from '../../utils/model/model.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js'
|
||||
|
||||
type Props = {
|
||||
block: AdvisorBlock;
|
||||
addMargin: boolean;
|
||||
resolvedToolUseIDs: Set<string>;
|
||||
erroredToolUseIDs: Set<string>;
|
||||
shouldAnimate: boolean;
|
||||
verbose: boolean;
|
||||
advisorModel?: string;
|
||||
};
|
||||
export function AdvisorMessage(t0) {
|
||||
const $ = _c(30);
|
||||
const {
|
||||
block,
|
||||
addMargin,
|
||||
resolvedToolUseIDs,
|
||||
erroredToolUseIDs,
|
||||
shouldAnimate,
|
||||
verbose,
|
||||
advisorModel
|
||||
} = t0;
|
||||
if (block.type === "server_tool_use") {
|
||||
let t1;
|
||||
if ($[0] !== block.input) {
|
||||
t1 = block.input && Object.keys(block.input).length > 0 ? jsonStringify(block.input) : null;
|
||||
$[0] = block.input;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const input = t1;
|
||||
const t2 = addMargin ? 1 : 0;
|
||||
let t3;
|
||||
if ($[2] !== block.id || $[3] !== resolvedToolUseIDs) {
|
||||
t3 = resolvedToolUseIDs.has(block.id);
|
||||
$[2] = block.id;
|
||||
$[3] = resolvedToolUseIDs;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const t4 = !t3;
|
||||
let t5;
|
||||
if ($[5] !== block.id || $[6] !== erroredToolUseIDs) {
|
||||
t5 = erroredToolUseIDs.has(block.id);
|
||||
$[5] = block.id;
|
||||
$[6] = erroredToolUseIDs;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== shouldAnimate || $[9] !== t4 || $[10] !== t5) {
|
||||
t6 = <ToolUseLoader shouldAnimate={shouldAnimate} isUnresolved={t4} isError={t5} />;
|
||||
$[8] = shouldAnimate;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
let t7;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text bold={true}>Advising</Text>;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
let t8;
|
||||
if ($[13] !== advisorModel) {
|
||||
t8 = advisorModel ? <Text dimColor={true}> using {renderModelName(advisorModel)}</Text> : null;
|
||||
$[13] = advisorModel;
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
}
|
||||
let t9;
|
||||
if ($[15] !== input) {
|
||||
t9 = input ? <Text dimColor={true}> · {input}</Text> : null;
|
||||
$[15] = input;
|
||||
$[16] = t9;
|
||||
} else {
|
||||
t9 = $[16];
|
||||
}
|
||||
let t10;
|
||||
if ($[17] !== t2 || $[18] !== t6 || $[19] !== t8 || $[20] !== t9) {
|
||||
t10 = <Box marginTop={t2} paddingRight={2} flexDirection="row">{t6}{t7}{t8}{t9}</Box>;
|
||||
$[17] = t2;
|
||||
$[18] = t6;
|
||||
$[19] = t8;
|
||||
$[20] = t9;
|
||||
$[21] = t10;
|
||||
} else {
|
||||
t10 = $[21];
|
||||
}
|
||||
return t10;
|
||||
}
|
||||
let body;
|
||||
bb0: switch (block.content.type) {
|
||||
case "advisor_tool_result_error":
|
||||
{
|
||||
let t1;
|
||||
if ($[22] !== block.content.error_code) {
|
||||
t1 = <Text color="error">Advisor unavailable ({block.content.error_code})</Text>;
|
||||
$[22] = block.content.error_code;
|
||||
$[23] = t1;
|
||||
} else {
|
||||
t1 = $[23];
|
||||
}
|
||||
body = t1;
|
||||
break bb0;
|
||||
}
|
||||
case "advisor_result":
|
||||
{
|
||||
let t1;
|
||||
if ($[24] !== block.content.text || $[25] !== verbose) {
|
||||
t1 = verbose ? <Text dimColor={true}>{block.content.text}</Text> : <Text dimColor={true}>{figures.tick} Advisor has reviewed the conversation and will apply the feedback <CtrlOToExpand /></Text>;
|
||||
$[24] = block.content.text;
|
||||
$[25] = verbose;
|
||||
$[26] = t1;
|
||||
} else {
|
||||
t1 = $[26];
|
||||
}
|
||||
body = t1;
|
||||
break bb0;
|
||||
}
|
||||
case "advisor_redacted_result":
|
||||
{
|
||||
let t1;
|
||||
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>{figures.tick} Advisor has reviewed the conversation and will apply the feedback</Text>;
|
||||
$[27] = t1;
|
||||
} else {
|
||||
t1 = $[27];
|
||||
}
|
||||
body = t1;
|
||||
}
|
||||
}
|
||||
let t1;
|
||||
if ($[28] !== body) {
|
||||
t1 = <Box paddingRight={2}><MessageResponse>{body}</MessageResponse></Box>;
|
||||
$[28] = body;
|
||||
$[29] = t1;
|
||||
} else {
|
||||
t1 = $[29];
|
||||
}
|
||||
return t1;
|
||||
block: AdvisorBlock
|
||||
addMargin: boolean
|
||||
resolvedToolUseIDs: Set<string>
|
||||
erroredToolUseIDs: Set<string>
|
||||
shouldAnimate: boolean
|
||||
verbose: boolean
|
||||
advisorModel?: string
|
||||
}
|
||||
|
||||
export function AdvisorMessage({
|
||||
block,
|
||||
addMargin,
|
||||
resolvedToolUseIDs,
|
||||
erroredToolUseIDs,
|
||||
shouldAnimate,
|
||||
verbose,
|
||||
advisorModel,
|
||||
}: Props): React.ReactNode {
|
||||
if (block.type === 'server_tool_use') {
|
||||
const input =
|
||||
block.input && Object.keys(block.input).length > 0
|
||||
? jsonStringify(block.input)
|
||||
: null
|
||||
return (
|
||||
<Box marginTop={addMargin ? 1 : 0} paddingRight={2} flexDirection="row">
|
||||
<ToolUseLoader
|
||||
shouldAnimate={shouldAnimate}
|
||||
isUnresolved={!resolvedToolUseIDs.has(block.id)}
|
||||
isError={erroredToolUseIDs.has(block.id)}
|
||||
/>
|
||||
<Text bold>Advising</Text>
|
||||
{advisorModel ? (
|
||||
<Text dimColor> using {renderModelName(advisorModel)}</Text>
|
||||
) : null}
|
||||
{input ? <Text dimColor> · {input}</Text> : null}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
let body: React.ReactNode
|
||||
switch (block.content.type) {
|
||||
case 'advisor_tool_result_error':
|
||||
body = (
|
||||
<Text color="error">
|
||||
Advisor unavailable ({block.content.error_code})
|
||||
</Text>
|
||||
)
|
||||
break
|
||||
case 'advisor_result':
|
||||
body = verbose ? (
|
||||
<Text dimColor>{block.content.text}</Text>
|
||||
) : (
|
||||
<Text dimColor>
|
||||
{figures.tick} Advisor has reviewed the conversation and will apply
|
||||
the feedback <CtrlOToExpand />
|
||||
</Text>
|
||||
)
|
||||
break
|
||||
case 'advisor_redacted_result':
|
||||
body = (
|
||||
<Text dimColor>
|
||||
{figures.tick} Advisor has reviewed the conversation and will apply
|
||||
the feedback
|
||||
</Text>
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<Box paddingRight={2}>
|
||||
<MessageResponse>{body}</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
};
|
||||
export function AssistantRedactedThinkingMessage(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
addMargin: t1
|
||||
} = t0;
|
||||
const addMargin = t1 === undefined ? false : t1;
|
||||
const t2 = addMargin ? 1 : 0;
|
||||
let t3;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text dimColor={true} italic={true}>✻ Thinking…</Text>;
|
||||
$[0] = t3;
|
||||
} else {
|
||||
t3 = $[0];
|
||||
}
|
||||
let t4;
|
||||
if ($[1] !== t2) {
|
||||
t4 = <Box marginTop={t2}>{t3}</Box>;
|
||||
$[1] = t2;
|
||||
$[2] = t4;
|
||||
} else {
|
||||
t4 = $[2];
|
||||
}
|
||||
return t4;
|
||||
addMargin: boolean
|
||||
}
|
||||
|
||||
export function AssistantRedactedThinkingMessage({
|
||||
addMargin = false,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<Box marginTop={addMargin ? 1 : 0}>
|
||||
<Text dimColor italic>
|
||||
✻ Thinking…
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,269 +1,222 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import React, { useContext } from 'react';
|
||||
import { ERROR_MESSAGE_USER_ABORT } from 'src/services/compact/compact.js';
|
||||
import { isRateLimitErrorMessage } from 'src/services/rateLimitMessages.js';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { Box, NoSelect, Text } from '../../ink.js';
|
||||
import { API_ERROR_MESSAGE_PREFIX, API_TIMEOUT_ERROR_MESSAGE, CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE, CUSTOM_OFF_SWITCH_MESSAGE, INVALID_API_KEY_ERROR_MESSAGE, INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL, ORG_DISABLED_ERROR_MESSAGE_ENV_KEY, ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH, PROMPT_TOO_LONG_ERROR_MESSAGE, startsWithApiErrorPrefix, TOKEN_REVOKED_ERROR_MESSAGE } from '../../services/api/errors.js';
|
||||
import { isEmptyMessageText, NO_RESPONSE_REQUESTED } from '../../utils/messages.js';
|
||||
import { getUpgradeMessage } from '../../utils/model/contextWindowUpgradeCheck.js';
|
||||
import { getDefaultSonnetModel, renderModelName } from '../../utils/model/model.js';
|
||||
import { isMacOsKeychainLocked } from '../../utils/secureStorage/macOsKeychainStorage.js';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { InterruptedByUser } from '../InterruptedByUser.js';
|
||||
import { Markdown } from '../Markdown.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js';
|
||||
import { RateLimitMessage } from './RateLimitMessage.js';
|
||||
const MAX_API_ERROR_CHARS = 1000;
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import React, { useContext } from 'react'
|
||||
import { ERROR_MESSAGE_USER_ABORT } from 'src/services/compact/compact.js'
|
||||
import { isRateLimitErrorMessage } from 'src/services/rateLimitMessages.js'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||
import { Box, NoSelect, Text } from '../../ink.js'
|
||||
import {
|
||||
API_ERROR_MESSAGE_PREFIX,
|
||||
API_TIMEOUT_ERROR_MESSAGE,
|
||||
CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE,
|
||||
CUSTOM_OFF_SWITCH_MESSAGE,
|
||||
INVALID_API_KEY_ERROR_MESSAGE,
|
||||
INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL,
|
||||
ORG_DISABLED_ERROR_MESSAGE_ENV_KEY,
|
||||
ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH,
|
||||
PROMPT_TOO_LONG_ERROR_MESSAGE,
|
||||
startsWithApiErrorPrefix,
|
||||
TOKEN_REVOKED_ERROR_MESSAGE,
|
||||
} from '../../services/api/errors.js'
|
||||
import {
|
||||
isEmptyMessageText,
|
||||
NO_RESPONSE_REQUESTED,
|
||||
} from '../../utils/messages.js'
|
||||
import { getUpgradeMessage } from '../../utils/model/contextWindowUpgradeCheck.js'
|
||||
import {
|
||||
getDefaultSonnetModel,
|
||||
renderModelName,
|
||||
} from '../../utils/model/model.js'
|
||||
import { isMacOsKeychainLocked } from '../../utils/secureStorage/macOsKeychainStorage.js'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { InterruptedByUser } from '../InterruptedByUser.js'
|
||||
import { Markdown } from '../Markdown.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js'
|
||||
import { RateLimitMessage } from './RateLimitMessage.js'
|
||||
|
||||
const MAX_API_ERROR_CHARS = 1000
|
||||
|
||||
type Props = {
|
||||
param: TextBlockParam;
|
||||
addMargin: boolean;
|
||||
shouldShowDot: boolean;
|
||||
verbose: boolean;
|
||||
width?: number | string;
|
||||
onOpenRateLimitOptions?: () => void;
|
||||
};
|
||||
function InvalidApiKeyMessage() {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = isMacOsKeychainLocked();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const isKeychainLocked = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <MessageResponse><Box flexDirection="column"><Text color="error">{INVALID_API_KEY_ERROR_MESSAGE}</Text>{isKeychainLocked && <Text dimColor={true}>· Run in another terminal: security unlock-keychain</Text>}</Box></MessageResponse>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
param: TextBlockParam
|
||||
addMargin: boolean
|
||||
shouldShowDot: boolean
|
||||
verbose: boolean
|
||||
width?: number | string
|
||||
onOpenRateLimitOptions?: () => void
|
||||
}
|
||||
export function AssistantTextMessage(t0) {
|
||||
const $ = _c(34);
|
||||
const {
|
||||
param: t1,
|
||||
addMargin,
|
||||
shouldShowDot,
|
||||
verbose,
|
||||
onOpenRateLimitOptions
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
const isSelected = useContext(MessageActionsSelectedContext);
|
||||
|
||||
function InvalidApiKeyMessage(): React.ReactNode {
|
||||
const isKeychainLocked = isMacOsKeychainLocked()
|
||||
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">{INVALID_API_KEY_ERROR_MESSAGE}</Text>
|
||||
{isKeychainLocked && (
|
||||
<Text dimColor>
|
||||
· Run in another terminal: security unlock-keychain
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
export function AssistantTextMessage({
|
||||
param: { text },
|
||||
addMargin,
|
||||
shouldShowDot,
|
||||
verbose,
|
||||
onOpenRateLimitOptions,
|
||||
}: Props): React.ReactNode {
|
||||
const isSelected = useContext(MessageActionsSelectedContext)
|
||||
if (isEmptyMessageText(text)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Handle all rate limit error messages from getRateLimitErrorMessage
|
||||
// Use the exported function to avoid fragile string coupling
|
||||
if (isRateLimitErrorMessage(text)) {
|
||||
let t2;
|
||||
if ($[0] !== onOpenRateLimitOptions || $[1] !== text) {
|
||||
t2 = <RateLimitMessage text={text} onOpenRateLimitOptions={onOpenRateLimitOptions} />;
|
||||
$[0] = onOpenRateLimitOptions;
|
||||
$[1] = text;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
return (
|
||||
<RateLimitMessage
|
||||
text={text}
|
||||
onOpenRateLimitOptions={onOpenRateLimitOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
switch (text) {
|
||||
// Local JSX commands don't need a response, but we still want Claude to see them
|
||||
// Tool results render their own interrupt messages
|
||||
case NO_RESPONSE_REQUESTED:
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case PROMPT_TOO_LONG_ERROR_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = getUpgradeMessage("warning");
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const upgradeHint = t2;
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <MessageResponse height={1}><Text color="error">Context limit reached · /compact or /clear to continue{upgradeHint ? ` · ${upgradeHint}` : ""}</Text></MessageResponse>;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
return null
|
||||
|
||||
case PROMPT_TOO_LONG_ERROR_MESSAGE: {
|
||||
const upgradeHint = getUpgradeMessage('warning')
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text color="error">
|
||||
Context limit reached · /compact or /clear to continue
|
||||
{upgradeHint ? ` · ${upgradeHint}` : ''}
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
case CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse height={1}><Text color="error">Credit balance too low · Add funds: https://platform.claude.com/settings/billing</Text></MessageResponse>;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text color="error">
|
||||
Credit balance too low · Add funds:
|
||||
https://platform.claude.com/settings/billing
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
case INVALID_API_KEY_ERROR_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <InvalidApiKeyMessage />;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return <InvalidApiKeyMessage />
|
||||
|
||||
case INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL:
|
||||
{
|
||||
let t2;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse height={1}><Text color="error">{INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL}</Text></MessageResponse>;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text color="error">{INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
case ORG_DISABLED_ERROR_MESSAGE_ENV_KEY:
|
||||
case ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH:
|
||||
{
|
||||
let t2;
|
||||
if ($[8] !== text) {
|
||||
t2 = <MessageResponse><Text color="error">{text}</Text></MessageResponse>;
|
||||
$[8] = text;
|
||||
$[9] = t2;
|
||||
} else {
|
||||
t2 = $[9];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Text color="error">{text}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
case TOKEN_REVOKED_ERROR_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse height={1}><Text color="error">{TOKEN_REVOKED_ERROR_MESSAGE}</Text></MessageResponse>;
|
||||
$[10] = t2;
|
||||
} else {
|
||||
t2 = $[10];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text color="error">{TOKEN_REVOKED_ERROR_MESSAGE}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
case API_TIMEOUT_ERROR_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse height={1}><Text color="error">{API_TIMEOUT_ERROR_MESSAGE}{process.env.API_TIMEOUT_MS && <>{" "}(API_TIMEOUT_MS={process.env.API_TIMEOUT_MS}ms, try increasing it)</>}</Text></MessageResponse>;
|
||||
$[11] = t2;
|
||||
} else {
|
||||
t2 = $[11];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text color="error">
|
||||
{API_TIMEOUT_ERROR_MESSAGE}
|
||||
{process.env.API_TIMEOUT_MS && (
|
||||
<>
|
||||
{' '}
|
||||
(API_TIMEOUT_MS={process.env.API_TIMEOUT_MS}ms, try increasing
|
||||
it)
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
case CUSTOM_OFF_SWITCH_MESSAGE:
|
||||
{
|
||||
let t2;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text color="error">We are experiencing high demand for Opus 4.</Text>;
|
||||
$[12] = t2;
|
||||
} else {
|
||||
t2 = $[12];
|
||||
}
|
||||
let t3;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <MessageResponse><Box flexDirection="column" gap={1}>{t2}<Text>To continue immediately, use /model to switch to{" "}{renderModelName(getDefaultSonnetModel())} and continue coding.</Text></Box></MessageResponse>;
|
||||
$[13] = t3;
|
||||
} else {
|
||||
t3 = $[13];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="error">
|
||||
We are experiencing high demand for Opus 4.
|
||||
</Text>
|
||||
<Text>
|
||||
To continue immediately, use /model to switch to{' '}
|
||||
{renderModelName(getDefaultSonnetModel())} and continue coding.
|
||||
</Text>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
// TODO: Move this to a user turn
|
||||
case ERROR_MESSAGE_USER_ABORT:
|
||||
{
|
||||
let t2;
|
||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>;
|
||||
$[14] = t2;
|
||||
} else {
|
||||
t2 = $[14];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<InterruptedByUser />
|
||||
</MessageResponse>
|
||||
)
|
||||
|
||||
default:
|
||||
{
|
||||
if (startsWithApiErrorPrefix(text)) {
|
||||
const truncated = !verbose && text.length > MAX_API_ERROR_CHARS;
|
||||
const t2 = text === API_ERROR_MESSAGE_PREFIX ? `${API_ERROR_MESSAGE_PREFIX}: Please wait a moment and try again.` : truncated ? text.slice(0, MAX_API_ERROR_CHARS) + "\u2026" : text;
|
||||
let t3;
|
||||
if ($[15] !== t2) {
|
||||
t3 = <Text color="error">{t2}</Text>;
|
||||
$[15] = t2;
|
||||
$[16] = t3;
|
||||
} else {
|
||||
t3 = $[16];
|
||||
}
|
||||
let t4;
|
||||
if ($[17] !== truncated) {
|
||||
t4 = truncated && <CtrlOToExpand />;
|
||||
$[17] = truncated;
|
||||
$[18] = t4;
|
||||
} else {
|
||||
t4 = $[18];
|
||||
}
|
||||
let t5;
|
||||
if ($[19] !== t3 || $[20] !== t4) {
|
||||
t5 = <MessageResponse><Box flexDirection="column">{t3}{t4}</Box></MessageResponse>;
|
||||
$[19] = t3;
|
||||
$[20] = t4;
|
||||
$[21] = t5;
|
||||
} else {
|
||||
t5 = $[21];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
const t2 = addMargin ? 1 : 0;
|
||||
const t3 = isSelected ? "messageActionsBackground" : undefined;
|
||||
let t4;
|
||||
if ($[22] !== isSelected || $[23] !== shouldShowDot) {
|
||||
t4 = shouldShowDot && <NoSelect fromLeftEdge={true} minWidth={2}><Text color={isSelected ? "suggestion" : "text"}>{BLACK_CIRCLE}</Text></NoSelect>;
|
||||
$[22] = isSelected;
|
||||
$[23] = shouldShowDot;
|
||||
$[24] = t4;
|
||||
} else {
|
||||
t4 = $[24];
|
||||
}
|
||||
let t5;
|
||||
if ($[25] !== text) {
|
||||
t5 = <Box flexDirection="column"><Markdown>{text}</Markdown></Box>;
|
||||
$[25] = text;
|
||||
$[26] = t5;
|
||||
} else {
|
||||
t5 = $[26];
|
||||
}
|
||||
let t6;
|
||||
if ($[27] !== t4 || $[28] !== t5) {
|
||||
t6 = <Box flexDirection="row">{t4}{t5}</Box>;
|
||||
$[27] = t4;
|
||||
$[28] = t5;
|
||||
$[29] = t6;
|
||||
} else {
|
||||
t6 = $[29];
|
||||
}
|
||||
let t7;
|
||||
if ($[30] !== t2 || $[31] !== t3 || $[32] !== t6) {
|
||||
t7 = <Box alignItems="flex-start" flexDirection="row" justifyContent="space-between" marginTop={t2} width="100%" backgroundColor={t3}>{t6}</Box>;
|
||||
$[30] = t2;
|
||||
$[31] = t3;
|
||||
$[32] = t6;
|
||||
$[33] = t7;
|
||||
} else {
|
||||
t7 = $[33];
|
||||
}
|
||||
return t7;
|
||||
if (startsWithApiErrorPrefix(text)) {
|
||||
const truncated = !verbose && text.length > MAX_API_ERROR_CHARS
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">
|
||||
{text === API_ERROR_MESSAGE_PREFIX
|
||||
? `${API_ERROR_MESSAGE_PREFIX}: Please wait a moment and try again.`
|
||||
: truncated
|
||||
? text.slice(0, MAX_API_ERROR_CHARS) + '…'
|
||||
: text}
|
||||
</Text>
|
||||
{truncated && <CtrlOToExpand />}
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
alignItems="flex-start"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
width="100%"
|
||||
backgroundColor={isSelected ? 'messageActionsBackground' : undefined}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
{shouldShowDot && (
|
||||
<NoSelect fromLeftEdge minWidth={2}>
|
||||
<Text color={isSelected ? 'suggestion' : 'text'}>
|
||||
{BLACK_CIRCLE}
|
||||
</Text>
|
||||
</NoSelect>
|
||||
)}
|
||||
<Box flexDirection="column">
|
||||
<Markdown>{text}</Markdown>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,66 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { ThinkingBlock, ThinkingBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { Markdown } from '../Markdown.js';
|
||||
import type {
|
||||
ThinkingBlock,
|
||||
ThinkingBlockParam,
|
||||
} from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { Markdown } from '../Markdown.js'
|
||||
|
||||
type Props = {
|
||||
// Accept either full ThinkingBlock/ThinkingBlockParam or a minimal shape with just type and thinking
|
||||
param: ThinkingBlock | ThinkingBlockParam | {
|
||||
type: 'thinking';
|
||||
thinking: string;
|
||||
};
|
||||
addMargin: boolean;
|
||||
isTranscriptMode: boolean;
|
||||
verbose: boolean;
|
||||
param:
|
||||
| ThinkingBlock
|
||||
| ThinkingBlockParam
|
||||
| { type: 'thinking'; thinking: string }
|
||||
addMargin: boolean
|
||||
isTranscriptMode: boolean
|
||||
verbose: boolean
|
||||
/** When true, hide this thinking block entirely (used for past thinking in transcript mode) */
|
||||
hideInTranscript?: boolean;
|
||||
};
|
||||
export function AssistantThinkingMessage(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
param: t1,
|
||||
addMargin: t2,
|
||||
isTranscriptMode,
|
||||
verbose,
|
||||
hideInTranscript: t3
|
||||
} = t0;
|
||||
const {
|
||||
thinking
|
||||
} = t1;
|
||||
const addMargin = t2 === undefined ? false : t2;
|
||||
const hideInTranscript = t3 === undefined ? false : t3;
|
||||
if (!thinking) {
|
||||
return null;
|
||||
}
|
||||
if (hideInTranscript) {
|
||||
return null;
|
||||
}
|
||||
const shouldShowFullThinking = isTranscriptMode || verbose;
|
||||
if (!shouldShowFullThinking) {
|
||||
const t4 = addMargin ? 1 : 0;
|
||||
let t5;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text dimColor={true} italic={true}>{"\u2234 Thinking"} <CtrlOToExpand /></Text>;
|
||||
$[0] = t5;
|
||||
} else {
|
||||
t5 = $[0];
|
||||
}
|
||||
let t6;
|
||||
if ($[1] !== t4) {
|
||||
t6 = <Box marginTop={t4}>{t5}</Box>;
|
||||
$[1] = t4;
|
||||
$[2] = t6;
|
||||
} else {
|
||||
t6 = $[2];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
const t4 = addMargin ? 1 : 0;
|
||||
let t5;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text dimColor={true} italic={true}>{"\u2234 Thinking"}…</Text>;
|
||||
$[3] = t5;
|
||||
} else {
|
||||
t5 = $[3];
|
||||
}
|
||||
let t6;
|
||||
if ($[4] !== thinking) {
|
||||
t6 = <Box paddingLeft={2}><Markdown dimColor={true}>{thinking}</Markdown></Box>;
|
||||
$[4] = thinking;
|
||||
$[5] = t6;
|
||||
} else {
|
||||
t6 = $[5];
|
||||
}
|
||||
let t7;
|
||||
if ($[6] !== t4 || $[7] !== t6) {
|
||||
t7 = <Box flexDirection="column" gap={1} marginTop={t4} width="100%">{t5}{t6}</Box>;
|
||||
$[6] = t4;
|
||||
$[7] = t6;
|
||||
$[8] = t7;
|
||||
} else {
|
||||
t7 = $[8];
|
||||
}
|
||||
return t7;
|
||||
hideInTranscript?: boolean
|
||||
}
|
||||
|
||||
export function AssistantThinkingMessage({
|
||||
param: { thinking },
|
||||
addMargin = false,
|
||||
isTranscriptMode,
|
||||
verbose,
|
||||
hideInTranscript = false,
|
||||
}: Props): React.ReactNode {
|
||||
if (!thinking) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (hideInTranscript) {
|
||||
return null
|
||||
}
|
||||
|
||||
const shouldShowFullThinking = isTranscriptMode || verbose
|
||||
const label = '∴ Thinking'
|
||||
|
||||
if (!shouldShowFullThinking) {
|
||||
return (
|
||||
<Box marginTop={addMargin ? 1 : 0}>
|
||||
<Text dimColor italic>
|
||||
{label} <CtrlOToExpand />
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
width="100%"
|
||||
>
|
||||
<Text dimColor italic>
|
||||
{label}…
|
||||
</Text>
|
||||
<Box paddingLeft={2}>
|
||||
<Markdown dimColor>{thinking}</Markdown>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,367 +1,326 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTerminalSize } from 'src/hooks/useTerminalSize.js';
|
||||
import type { ThemeName } from 'src/utils/theme.js';
|
||||
import type { Command } from '../../commands.js';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Box, Text, useTheme } from '../../ink.js';
|
||||
import { useAppStateMaybeOutsideOfProvider } from '../../state/AppState.js';
|
||||
import { findToolByName, type Tool, type ToolProgressData, type Tools } from '../../Tool.js';
|
||||
import type { ProgressMessage } from '../../types/message.js';
|
||||
import { useIsClassifierChecking } from '../../utils/classifierApprovalsHook.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import type { buildMessageLookups } from '../../utils/messages.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { useSelectedMessageBg } from '../messageActions.js';
|
||||
import { SentryErrorBoundary } from '../SentryErrorBoundary.js';
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js';
|
||||
import { HookProgressMessage } from './HookProgressMessage.js';
|
||||
import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTerminalSize } from 'src/hooks/useTerminalSize.js'
|
||||
import type { ThemeName } from 'src/utils/theme.js'
|
||||
import type { Command } from '../../commands.js'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Box, Text, useTheme } from '../../ink.js'
|
||||
import { useAppStateMaybeOutsideOfProvider } from '../../state/AppState.js'
|
||||
import {
|
||||
findToolByName,
|
||||
type Tool,
|
||||
type ToolProgressData,
|
||||
type Tools,
|
||||
} from '../../Tool.js'
|
||||
import type { ProgressMessage } from '../../types/message.js'
|
||||
import { useIsClassifierChecking } from '../../utils/classifierApprovalsHook.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import type { buildMessageLookups } from '../../utils/messages.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { useSelectedMessageBg } from '../messageActions.js'
|
||||
import { SentryErrorBoundary } from '../SentryErrorBoundary.js'
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js'
|
||||
import { HookProgressMessage } from './HookProgressMessage.js'
|
||||
|
||||
type Props = {
|
||||
param: ToolUseBlockParam;
|
||||
addMargin: boolean;
|
||||
tools: Tools;
|
||||
commands: Command[];
|
||||
verbose: boolean;
|
||||
inProgressToolUseIDs: Set<string>;
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
shouldAnimate: boolean;
|
||||
shouldShowDot: boolean;
|
||||
inProgressToolCallCount?: number;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function AssistantToolUseMessage(t0) {
|
||||
const $ = _c(81);
|
||||
const {
|
||||
param,
|
||||
addMargin,
|
||||
tools,
|
||||
commands,
|
||||
verbose,
|
||||
inProgressToolUseIDs,
|
||||
progressMessagesForMessage,
|
||||
shouldAnimate,
|
||||
shouldShowDot,
|
||||
inProgressToolCallCount,
|
||||
lookups,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
const terminalSize = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
const bg = useSelectedMessageBg();
|
||||
const pendingWorkerRequest = useAppStateMaybeOutsideOfProvider(_temp);
|
||||
const isClassifierCheckingRaw = useIsClassifierChecking(param.id);
|
||||
const permissionMode = useAppStateMaybeOutsideOfProvider(_temp2);
|
||||
const hasStrippedRules = useAppStateMaybeOutsideOfProvider(_temp3);
|
||||
const isAutoClassifier = permissionMode === "auto" || permissionMode === "plan" && hasStrippedRules;
|
||||
const isClassifierChecking = false && isClassifierCheckingRaw && permissionMode !== "auto";
|
||||
let t1;
|
||||
if ($[0] !== param.input || $[1] !== param.name || $[2] !== tools) {
|
||||
bb0: {
|
||||
if (!tools) {
|
||||
t1 = null;
|
||||
break bb0;
|
||||
}
|
||||
const tool = findToolByName(tools, param.name);
|
||||
if (!tool) {
|
||||
t1 = null;
|
||||
break bb0;
|
||||
}
|
||||
const input = tool.inputSchema.safeParse(param.input);
|
||||
const data = input.success ? input.data : undefined;
|
||||
t1 = {
|
||||
tool,
|
||||
input,
|
||||
userFacingToolName: tool.userFacingName(data),
|
||||
userFacingToolNameBackgroundColor: tool.userFacingNameBackgroundColor?.(data),
|
||||
isTransparentWrapper: tool.isTransparentWrapper?.() ?? false
|
||||
};
|
||||
param: ToolUseBlockParam
|
||||
addMargin: boolean
|
||||
tools: Tools
|
||||
commands: Command[]
|
||||
verbose: boolean
|
||||
inProgressToolUseIDs: Set<string>
|
||||
progressMessagesForMessage: ProgressMessage[]
|
||||
shouldAnimate: boolean
|
||||
shouldShowDot: boolean
|
||||
inProgressToolCallCount?: number
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function AssistantToolUseMessage({
|
||||
param,
|
||||
addMargin,
|
||||
tools,
|
||||
commands,
|
||||
verbose,
|
||||
inProgressToolUseIDs,
|
||||
progressMessagesForMessage,
|
||||
shouldAnimate,
|
||||
shouldShowDot,
|
||||
inProgressToolCallCount,
|
||||
lookups,
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const terminalSize = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
const bg = useSelectedMessageBg()
|
||||
const pendingWorkerRequest = useAppStateMaybeOutsideOfProvider(
|
||||
state => state.pendingWorkerRequest,
|
||||
)
|
||||
const isClassifierCheckingRaw = useIsClassifierChecking(param.id)
|
||||
const permissionMode = useAppStateMaybeOutsideOfProvider(
|
||||
state => state.toolPermissionContext.mode,
|
||||
)
|
||||
// strippedDangerousRules is set by stripDangerousPermissionsForAutoMode
|
||||
// (even to {}) whenever auto is active, and cleared by restoreDangerousPermissions
|
||||
// on deactivation — a reliable proxy for isAutoModeActive() during plan.
|
||||
// prePlanMode would be stale after transitionPlanAutoMode deactivates mid-plan.
|
||||
const hasStrippedRules = useAppStateMaybeOutsideOfProvider(
|
||||
state => !!state.toolPermissionContext.strippedDangerousRules,
|
||||
)
|
||||
const isAutoClassifier =
|
||||
permissionMode === 'auto' || (permissionMode === 'plan' && hasStrippedRules)
|
||||
const isClassifierChecking =
|
||||
process.env.USER_TYPE === 'ant' &&
|
||||
isClassifierCheckingRaw &&
|
||||
permissionMode !== 'auto'
|
||||
|
||||
// Memoize on param identity (stable — from the persisted message object).
|
||||
// Zod safeParse allocates per call, and some tools' userFacingName()
|
||||
// (BashTool → shouldUseSandbox → shell-quote parse) are expensive. Without
|
||||
// this, ~50 bash messages × shell-quote-per-render pushed transition
|
||||
// render past the shimmer tick → abort → infinite retry (#21605).
|
||||
const parsed = useMemo(() => {
|
||||
if (!tools) return null
|
||||
const tool = findToolByName(tools, param.name)
|
||||
if (!tool) return null
|
||||
const input = tool.inputSchema.safeParse(param.input)
|
||||
const data = input.success ? input.data : undefined
|
||||
return {
|
||||
tool,
|
||||
input,
|
||||
userFacingToolName: tool.userFacingName(data),
|
||||
userFacingToolNameBackgroundColor:
|
||||
tool.userFacingNameBackgroundColor?.(data),
|
||||
isTransparentWrapper: tool.isTransparentWrapper?.() ?? false,
|
||||
}
|
||||
$[0] = param.input;
|
||||
$[1] = param.name;
|
||||
$[2] = tools;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const parsed = t1;
|
||||
}, [tools, param])
|
||||
|
||||
if (!parsed) {
|
||||
logError(new Error(tools ? `Tool ${param.name} not found` : `Tools array is undefined for tool ${param.name}`));
|
||||
return null;
|
||||
// Guard against undefined tools (required prop) or unknown tool name
|
||||
logError(
|
||||
new Error(
|
||||
tools
|
||||
? `Tool ${param.name} not found`
|
||||
: `Tools array is undefined for tool ${param.name}`,
|
||||
),
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const {
|
||||
tool: tool_0,
|
||||
input: input_0,
|
||||
tool,
|
||||
input,
|
||||
userFacingToolName,
|
||||
userFacingToolNameBackgroundColor,
|
||||
isTransparentWrapper
|
||||
} = parsed;
|
||||
let t2;
|
||||
if ($[4] !== lookups.resolvedToolUseIDs || $[5] !== param.id) {
|
||||
t2 = lookups.resolvedToolUseIDs.has(param.id);
|
||||
$[4] = lookups.resolvedToolUseIDs;
|
||||
$[5] = param.id;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
const isResolved = t2;
|
||||
let t3;
|
||||
if ($[7] !== inProgressToolUseIDs || $[8] !== isResolved || $[9] !== param.id) {
|
||||
t3 = !inProgressToolUseIDs.has(param.id) && !isResolved;
|
||||
$[7] = inProgressToolUseIDs;
|
||||
$[8] = isResolved;
|
||||
$[9] = param.id;
|
||||
$[10] = t3;
|
||||
} else {
|
||||
t3 = $[10];
|
||||
}
|
||||
const isQueued = t3;
|
||||
const isWaitingForPermission = pendingWorkerRequest?.toolUseId === param.id;
|
||||
isTransparentWrapper,
|
||||
} = parsed
|
||||
|
||||
const isResolved = lookups.resolvedToolUseIDs.has(param.id)
|
||||
const isQueued = !inProgressToolUseIDs.has(param.id) && !isResolved
|
||||
const isWaitingForPermission = pendingWorkerRequest?.toolUseId === param.id
|
||||
|
||||
if (isTransparentWrapper) {
|
||||
if (isQueued || isResolved) {
|
||||
return null;
|
||||
}
|
||||
let t4;
|
||||
if ($[11] !== inProgressToolCallCount || $[12] !== isTranscriptMode || $[13] !== lookups || $[14] !== param.id || $[15] !== progressMessagesForMessage || $[16] !== terminalSize || $[17] !== tool_0 || $[18] !== tools || $[19] !== verbose) {
|
||||
t4 = renderToolUseProgressMessage(tool_0, tools, lookups, param.id, progressMessagesForMessage, {
|
||||
verbose,
|
||||
inProgressToolCallCount,
|
||||
isTranscriptMode
|
||||
}, terminalSize);
|
||||
$[11] = inProgressToolCallCount;
|
||||
$[12] = isTranscriptMode;
|
||||
$[13] = lookups;
|
||||
$[14] = param.id;
|
||||
$[15] = progressMessagesForMessage;
|
||||
$[16] = terminalSize;
|
||||
$[17] = tool_0;
|
||||
$[18] = tools;
|
||||
$[19] = verbose;
|
||||
$[20] = t4;
|
||||
} else {
|
||||
t4 = $[20];
|
||||
}
|
||||
let t5;
|
||||
if ($[21] !== bg || $[22] !== t4) {
|
||||
t5 = <Box flexDirection="column" width="100%" backgroundColor={bg}>{t4}</Box>;
|
||||
$[21] = bg;
|
||||
$[22] = t4;
|
||||
$[23] = t5;
|
||||
} else {
|
||||
t5 = $[23];
|
||||
}
|
||||
return t5;
|
||||
if (isQueued || isResolved) return null
|
||||
return (
|
||||
<Box flexDirection="column" width="100%" backgroundColor={bg}>
|
||||
{renderToolUseProgressMessage(
|
||||
tool,
|
||||
tools,
|
||||
lookups,
|
||||
param.id,
|
||||
progressMessagesForMessage,
|
||||
{ verbose, inProgressToolCallCount, isTranscriptMode },
|
||||
terminalSize,
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
if (userFacingToolName === "") {
|
||||
return null;
|
||||
|
||||
if (userFacingToolName === '') {
|
||||
return null
|
||||
}
|
||||
let t4;
|
||||
if ($[24] !== commands || $[25] !== input_0.data || $[26] !== input_0.success || $[27] !== theme || $[28] !== tool_0 || $[29] !== verbose) {
|
||||
t4 = input_0.success ? renderToolUseMessage(tool_0, input_0.data, {
|
||||
theme,
|
||||
verbose,
|
||||
commands
|
||||
}) : null;
|
||||
$[24] = commands;
|
||||
$[25] = input_0.data;
|
||||
$[26] = input_0.success;
|
||||
$[27] = theme;
|
||||
$[28] = tool_0;
|
||||
$[29] = verbose;
|
||||
$[30] = t4;
|
||||
} else {
|
||||
t4 = $[30];
|
||||
}
|
||||
const renderedToolUseMessage = t4;
|
||||
|
||||
const renderedToolUseMessage = input.success
|
||||
? renderToolUseMessage(tool, input.data, { theme, verbose, commands })
|
||||
: null
|
||||
if (renderedToolUseMessage === null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const t5 = addMargin ? 1 : 0;
|
||||
const t6 = stringWidth(userFacingToolName) + (shouldShowDot ? 2 : 0);
|
||||
let t7;
|
||||
if ($[31] !== isQueued || $[32] !== isResolved || $[33] !== lookups.erroredToolUseIDs || $[34] !== param.id || $[35] !== shouldAnimate || $[36] !== shouldShowDot) {
|
||||
t7 = shouldShowDot && (isQueued ? <Box minWidth={2}><Text dimColor={isQueued}>{BLACK_CIRCLE}</Text></Box> : <ToolUseLoader shouldAnimate={shouldAnimate} isUnresolved={!isResolved} isError={lookups.erroredToolUseIDs.has(param.id)} />);
|
||||
$[31] = isQueued;
|
||||
$[32] = isResolved;
|
||||
$[33] = lookups.erroredToolUseIDs;
|
||||
$[34] = param.id;
|
||||
$[35] = shouldAnimate;
|
||||
$[36] = shouldShowDot;
|
||||
$[37] = t7;
|
||||
} else {
|
||||
t7 = $[37];
|
||||
}
|
||||
const t8 = userFacingToolNameBackgroundColor ? "inverseText" : undefined;
|
||||
let t9;
|
||||
if ($[38] !== t8 || $[39] !== userFacingToolName || $[40] !== userFacingToolNameBackgroundColor) {
|
||||
t9 = <Box flexShrink={0}><Text bold={true} wrap="truncate-end" backgroundColor={userFacingToolNameBackgroundColor} color={t8}>{userFacingToolName}</Text></Box>;
|
||||
$[38] = t8;
|
||||
$[39] = userFacingToolName;
|
||||
$[40] = userFacingToolNameBackgroundColor;
|
||||
$[41] = t9;
|
||||
} else {
|
||||
t9 = $[41];
|
||||
}
|
||||
let t10;
|
||||
if ($[42] !== renderedToolUseMessage) {
|
||||
t10 = renderedToolUseMessage !== "" && <Box flexWrap="nowrap"><Text>({renderedToolUseMessage})</Text></Box>;
|
||||
$[42] = renderedToolUseMessage;
|
||||
$[43] = t10;
|
||||
} else {
|
||||
t10 = $[43];
|
||||
}
|
||||
let t11;
|
||||
if ($[44] !== input_0.data || $[45] !== input_0.success || $[46] !== tool_0) {
|
||||
t11 = input_0.success && tool_0.renderToolUseTag && tool_0.renderToolUseTag(input_0.data);
|
||||
$[44] = input_0.data;
|
||||
$[45] = input_0.success;
|
||||
$[46] = tool_0;
|
||||
$[47] = t11;
|
||||
} else {
|
||||
t11 = $[47];
|
||||
}
|
||||
let t12;
|
||||
if ($[48] !== t10 || $[49] !== t11 || $[50] !== t6 || $[51] !== t7 || $[52] !== t9) {
|
||||
t12 = <Box flexDirection="row" flexWrap="nowrap" minWidth={t6}>{t7}{t9}{t10}{t11}</Box>;
|
||||
$[48] = t10;
|
||||
$[49] = t11;
|
||||
$[50] = t6;
|
||||
$[51] = t7;
|
||||
$[52] = t9;
|
||||
$[53] = t12;
|
||||
} else {
|
||||
t12 = $[53];
|
||||
}
|
||||
let t13;
|
||||
if ($[54] !== inProgressToolCallCount || $[55] !== isAutoClassifier || $[56] !== isClassifierChecking || $[57] !== isQueued || $[58] !== isResolved || $[59] !== isTranscriptMode || $[60] !== isWaitingForPermission || $[61] !== lookups || $[62] !== param.id || $[63] !== progressMessagesForMessage || $[64] !== terminalSize || $[65] !== tool_0 || $[66] !== tools || $[67] !== verbose) {
|
||||
t13 = !isResolved && !isQueued && (isClassifierChecking ? <MessageResponse height={1}><Text dimColor={true}>{isAutoClassifier ? "Auto classifier checking\u2026" : "Bash classifier checking\u2026"}</Text></MessageResponse> : isWaitingForPermission ? <MessageResponse height={1}><Text dimColor={true}>Waiting for permission…</Text></MessageResponse> : renderToolUseProgressMessage(tool_0, tools, lookups, param.id, progressMessagesForMessage, {
|
||||
verbose,
|
||||
inProgressToolCallCount,
|
||||
isTranscriptMode
|
||||
}, terminalSize));
|
||||
$[54] = inProgressToolCallCount;
|
||||
$[55] = isAutoClassifier;
|
||||
$[56] = isClassifierChecking;
|
||||
$[57] = isQueued;
|
||||
$[58] = isResolved;
|
||||
$[59] = isTranscriptMode;
|
||||
$[60] = isWaitingForPermission;
|
||||
$[61] = lookups;
|
||||
$[62] = param.id;
|
||||
$[63] = progressMessagesForMessage;
|
||||
$[64] = terminalSize;
|
||||
$[65] = tool_0;
|
||||
$[66] = tools;
|
||||
$[67] = verbose;
|
||||
$[68] = t13;
|
||||
} else {
|
||||
t13 = $[68];
|
||||
}
|
||||
let t14;
|
||||
if ($[69] !== isQueued || $[70] !== isResolved || $[71] !== tool_0) {
|
||||
t14 = !isResolved && isQueued && renderToolUseQueuedMessage(tool_0);
|
||||
$[69] = isQueued;
|
||||
$[70] = isResolved;
|
||||
$[71] = tool_0;
|
||||
$[72] = t14;
|
||||
} else {
|
||||
t14 = $[72];
|
||||
}
|
||||
let t15;
|
||||
if ($[73] !== t12 || $[74] !== t13 || $[75] !== t14) {
|
||||
t15 = <Box flexDirection="column">{t12}{t13}{t14}</Box>;
|
||||
$[73] = t12;
|
||||
$[74] = t13;
|
||||
$[75] = t14;
|
||||
$[76] = t15;
|
||||
} else {
|
||||
t15 = $[76];
|
||||
}
|
||||
let t16;
|
||||
if ($[77] !== bg || $[78] !== t15 || $[79] !== t5) {
|
||||
t16 = <Box flexDirection="row" justifyContent="space-between" marginTop={t5} width="100%" backgroundColor={bg}>{t15}</Box>;
|
||||
$[77] = bg;
|
||||
$[78] = t15;
|
||||
$[79] = t5;
|
||||
$[80] = t16;
|
||||
} else {
|
||||
t16 = $[80];
|
||||
}
|
||||
return t16;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
width="100%"
|
||||
backgroundColor={bg}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Box
|
||||
flexDirection="row"
|
||||
flexWrap="nowrap"
|
||||
minWidth={stringWidth(userFacingToolName) + (shouldShowDot ? 2 : 0)}
|
||||
>
|
||||
{shouldShowDot &&
|
||||
(isQueued ? (
|
||||
<Box minWidth={2}>
|
||||
<Text dimColor={isQueued}>{BLACK_CIRCLE}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
// WARNING: The code here and in ToolUseLoader is particularly
|
||||
// sensitive to what *should* just be trivial refactorings. See
|
||||
// the comment in ToolUseLoader for more details.
|
||||
<ToolUseLoader
|
||||
shouldAnimate={shouldAnimate}
|
||||
isUnresolved={!isResolved}
|
||||
isError={lookups.erroredToolUseIDs.has(param.id)}
|
||||
/>
|
||||
))}
|
||||
<Box flexShrink={0}>
|
||||
<Text
|
||||
bold
|
||||
wrap="truncate-end"
|
||||
backgroundColor={userFacingToolNameBackgroundColor}
|
||||
color={
|
||||
userFacingToolNameBackgroundColor ? 'inverseText' : undefined
|
||||
}
|
||||
>
|
||||
{userFacingToolName}
|
||||
</Text>
|
||||
</Box>
|
||||
{renderedToolUseMessage !== '' && (
|
||||
<Box flexWrap="nowrap">
|
||||
<Text>({renderedToolUseMessage})</Text>
|
||||
</Box>
|
||||
)}
|
||||
{/* Render tool-specific tags (timeout, model, resume ID, etc.) */}
|
||||
{input.success &&
|
||||
tool.renderToolUseTag &&
|
||||
tool.renderToolUseTag(input.data)}
|
||||
</Box>
|
||||
{!isResolved &&
|
||||
!isQueued &&
|
||||
(isClassifierChecking ? (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>
|
||||
{isAutoClassifier
|
||||
? 'Auto classifier checking\u2026'
|
||||
: 'Bash classifier checking\u2026'}
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
) : isWaitingForPermission ? (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Waiting for permission…</Text>
|
||||
</MessageResponse>
|
||||
) : (
|
||||
renderToolUseProgressMessage(
|
||||
tool,
|
||||
tools,
|
||||
lookups,
|
||||
param.id,
|
||||
progressMessagesForMessage,
|
||||
{
|
||||
verbose,
|
||||
inProgressToolCallCount,
|
||||
isTranscriptMode,
|
||||
},
|
||||
terminalSize,
|
||||
)
|
||||
))}
|
||||
{!isResolved && isQueued && renderToolUseQueuedMessage(tool)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp3(state_1) {
|
||||
return !!state_1.toolPermissionContext.strippedDangerousRules;
|
||||
}
|
||||
function _temp2(state_0) {
|
||||
return state_0.toolPermissionContext.mode;
|
||||
}
|
||||
function _temp(state) {
|
||||
return state.pendingWorkerRequest;
|
||||
}
|
||||
function renderToolUseMessage(tool: Tool, input: unknown, {
|
||||
theme,
|
||||
verbose,
|
||||
commands
|
||||
}: {
|
||||
theme: ThemeName;
|
||||
verbose: boolean;
|
||||
commands: Command[];
|
||||
}): React.ReactNode {
|
||||
|
||||
function renderToolUseMessage(
|
||||
tool: Tool,
|
||||
input: unknown,
|
||||
{
|
||||
theme,
|
||||
verbose,
|
||||
commands,
|
||||
}: { theme: ThemeName; verbose: boolean; commands: Command[] },
|
||||
): React.ReactNode {
|
||||
try {
|
||||
const parsed = tool.inputSchema.safeParse(input);
|
||||
const parsed = tool.inputSchema.safeParse(input)
|
||||
if (!parsed.success) {
|
||||
return '';
|
||||
return ''
|
||||
}
|
||||
return tool.renderToolUseMessage(parsed.data, {
|
||||
theme,
|
||||
verbose,
|
||||
commands
|
||||
});
|
||||
return tool.renderToolUseMessage(parsed.data, { theme, verbose, commands })
|
||||
} catch (error) {
|
||||
logError(new Error(`Error rendering tool use message for ${tool.name}: ${error}`));
|
||||
return '';
|
||||
logError(
|
||||
new Error(`Error rendering tool use message for ${tool.name}: ${error}`),
|
||||
)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
function renderToolUseProgressMessage(tool: Tool, tools: Tools, lookups: ReturnType<typeof buildMessageLookups>, toolUseID: string, progressMessagesForMessage: ProgressMessage[], {
|
||||
verbose,
|
||||
inProgressToolCallCount,
|
||||
isTranscriptMode
|
||||
}: {
|
||||
verbose: boolean;
|
||||
inProgressToolCallCount?: number;
|
||||
isTranscriptMode?: boolean;
|
||||
}, terminalSize: {
|
||||
columns: number;
|
||||
rows: number;
|
||||
}): React.ReactNode {
|
||||
const toolProgressMessages = progressMessagesForMessage.filter((msg): msg is ProgressMessage<ToolProgressData> => (msg.data as { type?: string }).type !== 'hook_progress');
|
||||
|
||||
function renderToolUseProgressMessage(
|
||||
tool: Tool,
|
||||
tools: Tools,
|
||||
lookups: ReturnType<typeof buildMessageLookups>,
|
||||
toolUseID: string,
|
||||
progressMessagesForMessage: ProgressMessage[],
|
||||
{
|
||||
verbose,
|
||||
inProgressToolCallCount,
|
||||
isTranscriptMode,
|
||||
}: {
|
||||
verbose: boolean
|
||||
inProgressToolCallCount?: number
|
||||
isTranscriptMode?: boolean
|
||||
},
|
||||
terminalSize: { columns: number; rows: number },
|
||||
): React.ReactNode {
|
||||
const toolProgressMessages = progressMessagesForMessage.filter(
|
||||
(msg): msg is ProgressMessage<ToolProgressData> =>
|
||||
msg.data.type !== 'hook_progress',
|
||||
)
|
||||
try {
|
||||
const toolMessages = tool.renderToolUseProgressMessage?.(toolProgressMessages, {
|
||||
tools,
|
||||
verbose,
|
||||
terminalSize,
|
||||
inProgressToolCallCount: inProgressToolCallCount ?? 1,
|
||||
isTranscriptMode
|
||||
}) ?? null;
|
||||
return <>
|
||||
const toolMessages =
|
||||
tool.renderToolUseProgressMessage?.(toolProgressMessages, {
|
||||
tools,
|
||||
verbose,
|
||||
terminalSize,
|
||||
inProgressToolCallCount: inProgressToolCallCount ?? 1,
|
||||
isTranscriptMode,
|
||||
}) ?? null
|
||||
return (
|
||||
<>
|
||||
<SentryErrorBoundary>
|
||||
<HookProgressMessage hookEvent="PreToolUse" lookups={lookups} toolUseID={toolUseID} verbose={verbose} isTranscriptMode={isTranscriptMode} />
|
||||
<HookProgressMessage
|
||||
hookEvent="PreToolUse"
|
||||
lookups={lookups}
|
||||
toolUseID={toolUseID}
|
||||
verbose={verbose}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
</SentryErrorBoundary>
|
||||
{toolMessages}
|
||||
</>;
|
||||
</>
|
||||
)
|
||||
} catch (error) {
|
||||
logError(new Error(`Error rendering tool use progress message for ${tool.name}: ${error}`));
|
||||
return null;
|
||||
logError(
|
||||
new Error(
|
||||
`Error rendering tool use progress message for ${tool.name}: ${error}`,
|
||||
),
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function renderToolUseQueuedMessage(tool: Tool): React.ReactNode {
|
||||
try {
|
||||
return tool.renderToolUseQueuedMessage?.();
|
||||
return tool.renderToolUseQueuedMessage?.()
|
||||
} catch (error) {
|
||||
logError(new Error(`Error rendering tool use queued message for ${tool.name}: ${error}`));
|
||||
return null;
|
||||
logError(
|
||||
new Error(
|
||||
`Error rendering tool use queued message for ${tool.name}: ${error}`,
|
||||
),
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +1,134 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
|
||||
import React, { useMemo } from 'react';
|
||||
import { Ansi, Box, Text } from '../../ink.js';
|
||||
import type { Attachment } from 'src/utils/attachments.js';
|
||||
import type { NullRenderingAttachmentType } from './nullRenderingAttachments.js';
|
||||
import { type AppState, useAppState } from '../../state/AppState.js';
|
||||
import type { TaskState } from '../../tasks/types.js';
|
||||
import { getDisplayPath } from 'src/utils/file.js';
|
||||
import { formatFileSize } from 'src/utils/format.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { basename, sep } from 'path';
|
||||
import { UserTextMessage } from './UserTextMessage.js';
|
||||
import { DiagnosticsDisplay } from '../DiagnosticsDisplay.js';
|
||||
import { getContentText } from 'src/utils/messages.js';
|
||||
import type { Theme } from 'src/utils/theme.js';
|
||||
import { UserImageMessage } from './UserImageMessage.js';
|
||||
import { toInkColor } from '../../utils/ink.js';
|
||||
import { jsonParse } from '../../utils/slowOperations.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
|
||||
import { tryRenderPlanApprovalMessage, formatTeammateMessageContent } from './PlanApprovalMessage.js';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { TeammateMessageContent } from './UserTeammateMessage.js';
|
||||
import { isShutdownApproved } from '../../utils/teammateMailbox.js';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { FilePathLink } from '../FilePathLink.js';
|
||||
import { feature } from 'bun:bundle';
|
||||
import { useSelectedMessageBg } from '../messageActions.js';
|
||||
import React, { useMemo } from 'react'
|
||||
import { Ansi, Box, Text } from '../../ink.js'
|
||||
import type { Attachment } from 'src/utils/attachments.js'
|
||||
import type { NullRenderingAttachmentType } from './nullRenderingAttachments.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import { getDisplayPath } from 'src/utils/file.js'
|
||||
import { formatFileSize } from 'src/utils/format.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { basename, sep } from 'path'
|
||||
import { UserTextMessage } from './UserTextMessage.js'
|
||||
import { DiagnosticsDisplay } from '../DiagnosticsDisplay.js'
|
||||
import { getContentText } from 'src/utils/messages.js'
|
||||
import type { Theme } from 'src/utils/theme.js'
|
||||
import { UserImageMessage } from './UserImageMessage.js'
|
||||
import { toInkColor } from '../../utils/ink.js'
|
||||
import { jsonParse } from '../../utils/slowOperations.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
import {
|
||||
tryRenderPlanApprovalMessage,
|
||||
formatTeammateMessageContent,
|
||||
} from './PlanApprovalMessage.js'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||
import { TeammateMessageContent } from './UserTeammateMessage.js'
|
||||
import { isShutdownApproved } from '../../utils/teammateMailbox.js'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { FilePathLink } from '../FilePathLink.js'
|
||||
import { feature } from 'bun:bundle'
|
||||
import { useSelectedMessageBg } from '../messageActions.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
attachment: Attachment;
|
||||
verbose: boolean;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
addMargin: boolean
|
||||
attachment: Attachment
|
||||
verbose: boolean
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function AttachmentMessage({
|
||||
attachment,
|
||||
addMargin,
|
||||
verbose,
|
||||
isTranscriptMode
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const bg = useSelectedMessageBg();
|
||||
const bg = useSelectedMessageBg()
|
||||
// Hoisted to mount-time — per-message component, re-renders on every scroll.
|
||||
const isDemoEnv = feature('EXPERIMENTAL_SKILL_SEARCH') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useMemo(() => isEnvTruthy(process.env.IS_DEMO), []) : false;
|
||||
const isDemoEnv = feature('EXPERIMENTAL_SKILL_SEARCH')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useMemo(() => isEnvTruthy(process.env.IS_DEMO), [])
|
||||
: false
|
||||
// Handle teammate_mailbox BEFORE switch
|
||||
if (isAgentSwarmsEnabled() && attachment.type === 'teammate_mailbox') {
|
||||
// Filter out idle notifications BEFORE counting - they are hidden in the UI
|
||||
// so showing them in the count would be confusing ("2 messages in mailbox:" with nothing shown)
|
||||
const visibleMessages = attachment.messages.filter(msg => {
|
||||
if (isShutdownApproved(msg.text)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const parsed = jsonParse(msg.text);
|
||||
return parsed?.type !== 'idle_notification' && parsed?.type !== 'teammate_terminated';
|
||||
const parsed = jsonParse(msg.text)
|
||||
return (
|
||||
parsed?.type !== 'idle_notification' &&
|
||||
parsed?.type !== 'teammate_terminated'
|
||||
)
|
||||
} catch {
|
||||
return true; // Non-JSON messages are visible
|
||||
return true // Non-JSON messages are visible
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (visibleMessages.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Box flexDirection="column">
|
||||
{visibleMessages.map((msg_0, idx) => {
|
||||
// Try to parse as JSON for task_assignment messages
|
||||
let parsedMsg: {
|
||||
type?: string;
|
||||
taskId?: string;
|
||||
subject?: string;
|
||||
assignedBy?: string;
|
||||
} | null = null;
|
||||
try {
|
||||
parsedMsg = jsonParse(msg_0.text);
|
||||
} catch {
|
||||
// Not JSON, treat as plain text
|
||||
}
|
||||
if (parsedMsg?.type === 'task_assignment') {
|
||||
return <Box key={idx} paddingLeft={2}>
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{visibleMessages.map((msg, idx) => {
|
||||
// Try to parse as JSON for task_assignment messages
|
||||
let parsedMsg: {
|
||||
type?: string
|
||||
taskId?: string
|
||||
subject?: string
|
||||
assignedBy?: string
|
||||
} | null = null
|
||||
try {
|
||||
parsedMsg = jsonParse(msg.text)
|
||||
} catch {
|
||||
// Not JSON, treat as plain text
|
||||
}
|
||||
|
||||
if (parsedMsg?.type === 'task_assignment') {
|
||||
return (
|
||||
<Box key={idx} paddingLeft={2}>
|
||||
<Text>{BLACK_CIRCLE} </Text>
|
||||
<Text>Task assigned: </Text>
|
||||
<Text bold>#{parsedMsg.taskId}</Text>
|
||||
<Text> - {parsedMsg.subject}</Text>
|
||||
<Text dimColor> (from {parsedMsg.assignedBy || msg_0.from})</Text>
|
||||
</Box>;
|
||||
}
|
||||
<Text dimColor> (from {parsedMsg.assignedBy || msg.from})</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Note: idle_notification messages already filtered out above
|
||||
// Note: idle_notification messages already filtered out above
|
||||
|
||||
// Try to render as plan approval message (request or response)
|
||||
const planApprovalElement = tryRenderPlanApprovalMessage(msg_0.text, msg_0.from);
|
||||
if (planApprovalElement) {
|
||||
return <React.Fragment key={idx}>{planApprovalElement}</React.Fragment>;
|
||||
}
|
||||
// Try to render as plan approval message (request or response)
|
||||
const planApprovalElement = tryRenderPlanApprovalMessage(
|
||||
msg.text,
|
||||
msg.from,
|
||||
)
|
||||
if (planApprovalElement) {
|
||||
return (
|
||||
<React.Fragment key={idx}>{planApprovalElement}</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// Plain text message - sender header with chevron, truncated content
|
||||
const inkColor = toInkColor(msg_0.color);
|
||||
const formattedContent = formatTeammateMessageContent(msg_0.text) ?? msg_0.text;
|
||||
return <TeammateMessageContent key={idx} displayName={msg_0.from} inkColor={inkColor} content={formattedContent} summary={msg_0.summary} isTranscriptMode={isTranscriptMode} />;
|
||||
})}
|
||||
</Box>;
|
||||
// Plain text message - sender header with chevron, truncated content
|
||||
const inkColor = toInkColor(msg.color)
|
||||
const formattedContent =
|
||||
formatTeammateMessageContent(msg.text) ?? msg.text
|
||||
return (
|
||||
<TeammateMessageContent
|
||||
key={idx}
|
||||
displayName={msg.from}
|
||||
inkColor={inkColor}
|
||||
content={formattedContent}
|
||||
summary={msg.summary}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// skill_discovery rendered here (not in the switch) so the 'skill_discovery'
|
||||
@@ -108,83 +136,117 @@ export function AttachmentMessage({
|
||||
// be conditionally eliminated; an if-body can.
|
||||
if (feature('EXPERIMENTAL_SKILL_SEARCH')) {
|
||||
if (attachment.type === 'skill_discovery') {
|
||||
if (attachment.skills.length === 0) return null;
|
||||
if (attachment.skills.length === 0) return null
|
||||
// Ant users get shortIds inline so they can /skill-feedback while the
|
||||
// turn is still fresh. External users (when this un-gates) just see
|
||||
// names — shortId is undefined outside ant builds anyway.
|
||||
const names = attachment.skills.map(s => s.shortId ? `${s.name} [${s.shortId}]` : s.name).join(', ');
|
||||
const firstId = attachment.skills[0]?.shortId;
|
||||
const hint = (process.env.USER_TYPE) === 'ant' && !isDemoEnv && firstId ? ` · /skill-feedback ${firstId} 1=wrong 2=noisy 3=good [comment]` : '';
|
||||
return <Line>
|
||||
const names = attachment.skills
|
||||
.map(s => (s.shortId ? `${s.name} [${s.shortId}]` : s.name))
|
||||
.join(', ')
|
||||
const firstId = attachment.skills[0]?.shortId
|
||||
const hint =
|
||||
process.env.USER_TYPE === 'ant' && !isDemoEnv && firstId
|
||||
? ` · /skill-feedback ${firstId} 1=wrong 2=noisy 3=good [comment]`
|
||||
: ''
|
||||
return (
|
||||
<Line>
|
||||
<Text bold>{attachment.skills.length}</Text> relevant{' '}
|
||||
{plural(attachment.skills.length, 'skill')}: {names}
|
||||
{hint && <Text dimColor>{hint}</Text>}
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- teammate_mailbox/skill_discovery handled before switch
|
||||
switch (attachment.type) {
|
||||
case 'directory':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Listed directory <Text bold>{attachment.displayPath + sep}</Text>
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'file':
|
||||
case 'already_read_file':
|
||||
if (attachment.content.type === 'notebook') {
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Read <Text bold>{attachment.displayPath}</Text> (
|
||||
{attachment.content.file.cells.length} cells)
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
if (attachment.content.type === 'file_unchanged') {
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Read <Text bold>{attachment.displayPath}</Text> (unchanged)
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Read <Text bold>{attachment.displayPath}</Text> (
|
||||
{attachment.content.type === 'text' ? `${attachment.content.file.numLines}${attachment.truncated ? '+' : ''} lines` : formatFileSize(attachment.content.file.originalSize)}
|
||||
{attachment.content.type === 'text'
|
||||
? `${attachment.content.file.numLines}${attachment.truncated ? '+' : ''} lines`
|
||||
: formatFileSize(attachment.content.file.originalSize)}
|
||||
)
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'compact_file_reference':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Referenced file <Text bold>{attachment.displayPath}</Text>
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'pdf_reference':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Referenced PDF <Text bold>{attachment.displayPath}</Text> (
|
||||
{attachment.pageCount} pages)
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'selected_lines_in_ide':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
⧉ Selected{' '}
|
||||
<Text bold>{attachment.lineEnd - attachment.lineStart + 1}</Text>{' '}
|
||||
lines from <Text bold>{attachment.displayPath}</Text> in{' '}
|
||||
{attachment.ideName}
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'nested_memory':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Loaded <Text bold>{attachment.displayPath}</Text>
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'relevant_memories':
|
||||
// Usually absorbed into a CollapsedReadSearchGroup (collapseReadSearch.ts)
|
||||
// so this only renders when the preceding tool was non-collapsible (Edit,
|
||||
// Write) and no group was open. Match CollapsedReadSearchContent's style:
|
||||
// 2-space gutter, dim text, count only — filenames/content in ctrl+o.
|
||||
return <Box flexDirection="column" marginTop={addMargin ? 1 : 0} backgroundColor={bg}>
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
backgroundColor={bg}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
<Box minWidth={2} />
|
||||
<Text dimColor>
|
||||
Recalled <Text bold>{attachment.memories.length}</Text>{' '}
|
||||
{attachment.memories.length === 1 ? 'memory' : 'memories'}
|
||||
{!isTranscriptMode && <>
|
||||
{!isTranscriptMode && (
|
||||
<>
|
||||
{' '}
|
||||
<CtrlOToExpand />
|
||||
</>}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
{(verbose || isTranscriptMode) && attachment.memories.map(m => <Box key={m.path} flexDirection="column">
|
||||
{(verbose || isTranscriptMode) &&
|
||||
attachment.memories.map(m => (
|
||||
<Box key={m.path} flexDirection="column">
|
||||
<MessageResponse>
|
||||
<Text dimColor>
|
||||
<FilePathLink filePath={m.path}>
|
||||
@@ -192,156 +254,201 @@ export function AttachmentMessage({
|
||||
</FilePathLink>
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
{isTranscriptMode && <Box paddingLeft={5}>
|
||||
{isTranscriptMode && (
|
||||
<Box paddingLeft={5}>
|
||||
<Text>
|
||||
<Ansi>{m.content}</Ansi>
|
||||
</Text>
|
||||
</Box>}
|
||||
</Box>)}
|
||||
</Box>;
|
||||
case 'dynamic_skill':
|
||||
{
|
||||
const skillCount = attachment.skillNames.length;
|
||||
return <Line>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
case 'dynamic_skill': {
|
||||
const skillCount = attachment.skillNames.length
|
||||
return (
|
||||
<Line>
|
||||
Loaded{' '}
|
||||
<Text bold>
|
||||
{skillCount} {plural(skillCount, 'skill')}
|
||||
</Text>{' '}
|
||||
from <Text bold>{attachment.displayPath}</Text>
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
case 'skill_listing': {
|
||||
if (attachment.isInitial) {
|
||||
return null
|
||||
}
|
||||
case 'skill_listing':
|
||||
{
|
||||
if (attachment.isInitial) {
|
||||
return null;
|
||||
}
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
<Text bold>{attachment.skillCount}</Text>{' '}
|
||||
{plural(attachment.skillCount, 'skill')} available
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
case 'agent_listing_delta': {
|
||||
if (attachment.isInitial || attachment.addedTypes.length === 0) {
|
||||
return null
|
||||
}
|
||||
case 'agent_listing_delta':
|
||||
{
|
||||
if (attachment.isInitial || attachment.addedTypes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const count = attachment.addedTypes.length;
|
||||
return <Line>
|
||||
const count = attachment.addedTypes.length
|
||||
return (
|
||||
<Line>
|
||||
<Text bold>{count}</Text> agent {plural(count, 'type')} available
|
||||
</Line>;
|
||||
}
|
||||
case 'queued_command':
|
||||
{
|
||||
const text = typeof attachment.prompt === 'string' ? attachment.prompt : getContentText(attachment.prompt) || '';
|
||||
const hasImages = attachment.imagePasteIds && attachment.imagePasteIds.length > 0;
|
||||
return <Box flexDirection="column">
|
||||
<UserTextMessage addMargin={addMargin} param={{
|
||||
text,
|
||||
type: 'text'
|
||||
}} verbose={verbose} isTranscriptMode={isTranscriptMode} />
|
||||
{hasImages && attachment.imagePasteIds?.map(id => <UserImageMessage key={id} imageId={id} />)}
|
||||
</Box>;
|
||||
}
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
case 'queued_command': {
|
||||
const text =
|
||||
typeof attachment.prompt === 'string'
|
||||
? attachment.prompt
|
||||
: getContentText(attachment.prompt) || ''
|
||||
const hasImages =
|
||||
attachment.imagePasteIds && attachment.imagePasteIds.length > 0
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<UserTextMessage
|
||||
addMargin={addMargin}
|
||||
param={{ text, type: 'text' }}
|
||||
verbose={verbose}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
{hasImages &&
|
||||
attachment.imagePasteIds?.map(id => (
|
||||
<UserImageMessage key={id} imageId={id} />
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
case 'plan_file_reference':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Plan file referenced ({getDisplayPath(attachment.planFilePath)})
|
||||
</Line>;
|
||||
case 'invoked_skills':
|
||||
{
|
||||
if (attachment.skills.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const skillNames = attachment.skills.map(s_0 => s_0.name).join(', ');
|
||||
return <Line>Skills restored ({skillNames})</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'invoked_skills': {
|
||||
if (attachment.skills.length === 0) {
|
||||
return null
|
||||
}
|
||||
const skillNames = attachment.skills.map(s => s.name).join(', ')
|
||||
return <Line>Skills restored ({skillNames})</Line>
|
||||
}
|
||||
case 'diagnostics':
|
||||
return <DiagnosticsDisplay attachment={attachment} verbose={verbose} />;
|
||||
return <DiagnosticsDisplay attachment={attachment} verbose={verbose} />
|
||||
case 'mcp_resource':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
Read MCP resource <Text bold>{attachment.name}</Text> from{' '}
|
||||
{attachment.server}
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'command_permissions':
|
||||
// The skill success message is rendered by SkillTool's renderToolResultMessage,
|
||||
// so we don't render anything here to avoid duplicate messages.
|
||||
return null;
|
||||
case 'async_hook_response':
|
||||
{
|
||||
// SessionStart hook completions are only shown in verbose mode
|
||||
if (attachment.hookEvent === 'SessionStart' && !verbose) {
|
||||
return null;
|
||||
}
|
||||
// Generally hide async hook completion messages unless in verbose mode
|
||||
if (!verbose && !isTranscriptMode) {
|
||||
return null;
|
||||
}
|
||||
return <Line>
|
||||
Async hook <Text bold>{attachment.hookEvent}</Text> completed
|
||||
</Line>;
|
||||
return null
|
||||
case 'async_hook_response': {
|
||||
// SessionStart hook completions are only shown in verbose mode
|
||||
if (attachment.hookEvent === 'SessionStart' && !verbose) {
|
||||
return null
|
||||
}
|
||||
case 'hook_blocking_error':
|
||||
{
|
||||
// Stop hooks are rendered as a summary in SystemStopHookSummaryMessage
|
||||
if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') {
|
||||
return null;
|
||||
}
|
||||
// Show stderr to the user so they can understand why the hook blocked
|
||||
const stderr = attachment.blockingError.blockingError.trim();
|
||||
return <>
|
||||
// Generally hide async hook completion messages unless in verbose mode
|
||||
if (!verbose && !isTranscriptMode) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Line>
|
||||
Async hook <Text bold>{attachment.hookEvent}</Text> completed
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
case 'hook_blocking_error': {
|
||||
// Stop hooks are rendered as a summary in SystemStopHookSummaryMessage
|
||||
if (
|
||||
attachment.hookEvent === 'Stop' ||
|
||||
attachment.hookEvent === 'SubagentStop'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
// Show stderr to the user so they can understand why the hook blocked
|
||||
const stderr = attachment.blockingError.blockingError.trim()
|
||||
return (
|
||||
<>
|
||||
<Line color="error">
|
||||
{attachment.hookName} hook returned blocking error
|
||||
</Line>
|
||||
{stderr ? <Line color="error">{stderr}</Line> : null}
|
||||
</>;
|
||||
}
|
||||
case 'hook_non_blocking_error':
|
||||
{
|
||||
// Stop hooks are rendered as a summary in SystemStopHookSummaryMessage
|
||||
if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') {
|
||||
return null;
|
||||
}
|
||||
// Full hook output is logged to debug log via hookEvents.ts
|
||||
return <Line color="error">{attachment.hookName} hook error</Line>;
|
||||
</>
|
||||
)
|
||||
}
|
||||
case 'hook_non_blocking_error': {
|
||||
// Stop hooks are rendered as a summary in SystemStopHookSummaryMessage
|
||||
if (
|
||||
attachment.hookEvent === 'Stop' ||
|
||||
attachment.hookEvent === 'SubagentStop'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
// Full hook output is logged to debug log via hookEvents.ts
|
||||
return <Line color="error">{attachment.hookName} hook error</Line>
|
||||
}
|
||||
case 'hook_error_during_execution':
|
||||
// Stop hooks are rendered as a summary in SystemStopHookSummaryMessage
|
||||
if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') {
|
||||
return null;
|
||||
if (
|
||||
attachment.hookEvent === 'Stop' ||
|
||||
attachment.hookEvent === 'SubagentStop'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
// Full hook output is logged to debug log via hookEvents.ts
|
||||
return <Line>{attachment.hookName} hook warning</Line>;
|
||||
return <Line>{attachment.hookName} hook warning</Line>
|
||||
case 'hook_success':
|
||||
// Full hook output is logged to debug log via hookEvents.ts
|
||||
return null;
|
||||
return null
|
||||
case 'hook_stopped_continuation':
|
||||
// Stop hooks are rendered as a summary in SystemStopHookSummaryMessage
|
||||
if (attachment.hookEvent === 'Stop' || attachment.hookEvent === 'SubagentStop') {
|
||||
return null;
|
||||
if (
|
||||
attachment.hookEvent === 'Stop' ||
|
||||
attachment.hookEvent === 'SubagentStop'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return <Line color="warning">
|
||||
return (
|
||||
<Line color="warning">
|
||||
{attachment.hookName} hook stopped continuation: {attachment.message}
|
||||
</Line>;
|
||||
</Line>
|
||||
)
|
||||
case 'hook_system_message':
|
||||
return <Line>
|
||||
return (
|
||||
<Line>
|
||||
{attachment.hookName} says: {attachment.content}
|
||||
</Line>;
|
||||
case 'hook_permission_decision':
|
||||
{
|
||||
const action = attachment.decision === 'allow' ? 'Allowed' : 'Denied';
|
||||
return <Line>
|
||||
</Line>
|
||||
)
|
||||
case 'hook_permission_decision': {
|
||||
const action = attachment.decision === 'allow' ? 'Allowed' : 'Denied'
|
||||
return (
|
||||
<Line>
|
||||
{action} by <Text bold>{attachment.hookEvent}</Text> hook
|
||||
</Line>;
|
||||
}
|
||||
</Line>
|
||||
)
|
||||
}
|
||||
case 'task_status':
|
||||
return <TaskStatusMessage attachment={attachment} />;
|
||||
return <TaskStatusMessage attachment={attachment} />
|
||||
case 'teammate_shutdown_batch':
|
||||
return <Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>
|
||||
return (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
width="100%"
|
||||
marginTop={1}
|
||||
backgroundColor={bg}
|
||||
>
|
||||
<Text dimColor>{BLACK_CIRCLE} </Text>
|
||||
<Text dimColor>
|
||||
{attachment.count} {plural(attachment.count, 'teammate')} shut down
|
||||
gracefully
|
||||
</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
default:
|
||||
// Exhaustiveness: every type reaching here must be in NULL_RENDERING_TYPES.
|
||||
// If TS errors, a new Attachment type was added without a case above AND
|
||||
@@ -352,185 +459,110 @@ export function AttachmentMessage({
|
||||
// skill_discovery and teammate_mailbox are handled BEFORE the switch in
|
||||
// runtime-gated blocks (feature() / isAgentSwarmsEnabled()) that TS can't
|
||||
// narrow through — excluded here via type union (compile-time only, no emit).
|
||||
attachment.type satisfies NullRenderingAttachmentType | 'skill_discovery' | 'teammate_mailbox' | 'bagel_console';
|
||||
return null;
|
||||
attachment.type satisfies
|
||||
| NullRenderingAttachmentType
|
||||
| 'skill_discovery'
|
||||
| 'teammate_mailbox'
|
||||
return null
|
||||
}
|
||||
}
|
||||
type TaskStatusAttachment = Extract<Attachment, {
|
||||
type: 'task_status';
|
||||
}>;
|
||||
function TaskStatusMessage(t0) {
|
||||
const $ = _c(4);
|
||||
const {
|
||||
attachment
|
||||
} = t0;
|
||||
if (false && attachment.status === "killed") {
|
||||
return null;
|
||||
|
||||
type TaskStatusAttachment = Extract<Attachment, { type: 'task_status' }>
|
||||
|
||||
function TaskStatusMessage({
|
||||
attachment,
|
||||
}: {
|
||||
attachment: TaskStatusAttachment
|
||||
}): React.ReactNode {
|
||||
// For ants, killed task status is shown in the CoordinatorTaskPanel.
|
||||
// Don't render it again in the chat.
|
||||
if (process.env.USER_TYPE === 'ant' && attachment.status === 'killed') {
|
||||
return null
|
||||
}
|
||||
if (isAgentSwarmsEnabled() && attachment.taskType === "in_process_teammate") {
|
||||
let t1;
|
||||
if ($[0] !== attachment) {
|
||||
t1 = <TeammateTaskStatus attachment={attachment} />;
|
||||
$[0] = attachment;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
|
||||
// Only access teammate-specific code when swarms are enabled.
|
||||
// TeammateTaskStatus subscribes to AppState; by gating the mount we
|
||||
// avoid adding a store listener for every non-teammate attachment.
|
||||
if (isAgentSwarmsEnabled() && attachment.taskType === 'in_process_teammate') {
|
||||
return <TeammateTaskStatus attachment={attachment} />
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== attachment) {
|
||||
t1 = <GenericTaskStatus attachment={attachment} />;
|
||||
$[2] = attachment;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
|
||||
return <GenericTaskStatus attachment={attachment} />
|
||||
}
|
||||
function GenericTaskStatus(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
attachment
|
||||
} = t0;
|
||||
const bg = useSelectedMessageBg();
|
||||
const statusText = attachment.status === "completed" ? "completed in background" : attachment.status === "killed" ? "stopped" : attachment.status === "running" ? "still running in background" : attachment.status;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>{BLACK_CIRCLE} </Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== attachment.description) {
|
||||
t2 = <Text bold={true}>{attachment.description}</Text>;
|
||||
$[1] = attachment.description;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== statusText || $[4] !== t2) {
|
||||
t3 = <Text dimColor={true}>Task "{t2}" {statusText}</Text>;
|
||||
$[3] = statusText;
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== bg || $[7] !== t3) {
|
||||
t4 = <Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>{t1}{t3}</Box>;
|
||||
$[6] = bg;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
return t4;
|
||||
|
||||
function GenericTaskStatus({
|
||||
attachment,
|
||||
}: {
|
||||
attachment: TaskStatusAttachment
|
||||
}): React.ReactNode {
|
||||
const bg = useSelectedMessageBg()
|
||||
const statusText =
|
||||
attachment.status === 'completed'
|
||||
? 'completed in background'
|
||||
: attachment.status === 'killed'
|
||||
? 'stopped'
|
||||
: attachment.status === 'running'
|
||||
? 'still running in background'
|
||||
: attachment.status
|
||||
return (
|
||||
<Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>
|
||||
<Text dimColor>{BLACK_CIRCLE} </Text>
|
||||
<Text dimColor>
|
||||
Task "<Text bold>{attachment.description}</Text>" {statusText}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function TeammateTaskStatus(t0: { attachment: TaskStatusAttachment }) {
|
||||
const $ = _c(16);
|
||||
const {
|
||||
attachment
|
||||
} = t0;
|
||||
const bg = useSelectedMessageBg();
|
||||
let t1: (s: AppState) => TaskState;
|
||||
if ($[0] !== attachment.taskId) {
|
||||
t1 = s => s.tasks[attachment.taskId];
|
||||
$[0] = attachment.taskId;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1] as (s: AppState) => TaskState;
|
||||
|
||||
function TeammateTaskStatus({
|
||||
attachment,
|
||||
}: {
|
||||
attachment: TaskStatusAttachment
|
||||
}): React.ReactNode {
|
||||
const bg = useSelectedMessageBg()
|
||||
// Narrow selector: only re-render when this specific task changes.
|
||||
const task = useAppState(s => s.tasks[attachment.taskId])
|
||||
if (task?.type !== 'in_process_teammate') {
|
||||
// Fall through to generic rendering (task not yet in store, or wrong type)
|
||||
return <GenericTaskStatus attachment={attachment} />
|
||||
}
|
||||
const task = useAppState(t1);
|
||||
if (task?.type !== "in_process_teammate") {
|
||||
let t2;
|
||||
if ($[2] !== attachment) {
|
||||
t2 = <GenericTaskStatus attachment={attachment} />;
|
||||
$[2] = attachment;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== task.identity.color) {
|
||||
t2 = toInkColor(task.identity.color);
|
||||
$[4] = task.identity.color;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const agentColor = t2;
|
||||
const statusText = attachment.status === "completed" ? "shut down gracefully" : attachment.status;
|
||||
let t3;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text dimColor={true}>{BLACK_CIRCLE} </Text>;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== agentColor || $[8] !== task.identity.agentName) {
|
||||
t4 = <Text color={agentColor} bold={true} dimColor={false}>@{task.identity.agentName}</Text>;
|
||||
$[7] = agentColor;
|
||||
$[8] = task.identity.agentName;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== statusText || $[11] !== t4) {
|
||||
t5 = <Text dimColor={true}>Teammate{" "}{t4}{" "}{statusText}</Text>;
|
||||
$[10] = statusText;
|
||||
$[11] = t4;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
let t6;
|
||||
if ($[13] !== bg || $[14] !== t5) {
|
||||
t6 = <Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>{t3}{t5}</Box>;
|
||||
$[13] = bg;
|
||||
$[14] = t5;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
return t6;
|
||||
const agentColor = toInkColor(task.identity.color)
|
||||
const statusText =
|
||||
attachment.status === 'completed'
|
||||
? 'shut down gracefully'
|
||||
: attachment.status
|
||||
return (
|
||||
<Box flexDirection="row" width="100%" marginTop={1} backgroundColor={bg}>
|
||||
<Text dimColor>{BLACK_CIRCLE} </Text>
|
||||
<Text dimColor>
|
||||
Teammate{' '}
|
||||
<Text color={agentColor} bold dimColor={false}>
|
||||
@{task.identity.agentName}
|
||||
</Text>{' '}
|
||||
{statusText}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
// We allow setting dimColor to false here to help work around the dim-bold bug.
|
||||
// https://github.com/chalk/chalk/issues/290
|
||||
function Line(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
dimColor: t1,
|
||||
children,
|
||||
color
|
||||
} = t0;
|
||||
const dimColor = t1 === undefined ? true : t1;
|
||||
const bg = useSelectedMessageBg();
|
||||
let t2;
|
||||
if ($[0] !== children || $[1] !== color || $[2] !== dimColor) {
|
||||
t2 = <MessageResponse><Text color={color} dimColor={dimColor} wrap="wrap">{children}</Text></MessageResponse>;
|
||||
$[0] = children;
|
||||
$[1] = color;
|
||||
$[2] = dimColor;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== bg || $[5] !== t2) {
|
||||
t3 = <Box backgroundColor={bg}>{t2}</Box>;
|
||||
$[4] = bg;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
function Line({
|
||||
dimColor = true,
|
||||
children,
|
||||
color,
|
||||
}: {
|
||||
dimColor?: boolean
|
||||
children: React.ReactNode
|
||||
color?: keyof Theme
|
||||
}): React.ReactNode {
|
||||
const bg = useSelectedMessageBg()
|
||||
return (
|
||||
<Box backgroundColor={bg}>
|
||||
<MessageResponse>
|
||||
<Text color={color} dimColor={dimColor} wrap="wrap">
|
||||
{children}
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,144 +1,122 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import { basename } from 'path';
|
||||
import React, { useRef } from 'react';
|
||||
import { useMinDisplayTime } from '../../hooks/useMinDisplayTime.js';
|
||||
import { Ansi, Box, Text, useTheme } from '../../ink.js';
|
||||
import { findToolByName, type Tools } from '../../Tool.js';
|
||||
import { getReplPrimitiveTools } from '../../tools/REPLTool/primitiveTools.js';
|
||||
import type { CollapsedReadSearchGroup, NormalizedAssistantMessage } from '../../types/message.js';
|
||||
import { uniq } from '../../utils/array.js';
|
||||
import { getToolUseIdsFromCollapsedGroup } from '../../utils/collapseReadSearch.js';
|
||||
import { getDisplayPath } from '../../utils/file.js';
|
||||
import { formatDuration, formatSecondsShort } from '../../utils/format.js';
|
||||
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
|
||||
import type { buildMessageLookups } from '../../utils/messages.js';
|
||||
import type { ThemeName } from '../../utils/theme.js';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { useSelectedMessageBg } from '../messageActions.js';
|
||||
import { PrBadge } from '../PrBadge.js';
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import { basename } from 'path'
|
||||
import React, { useRef } from 'react'
|
||||
import { useMinDisplayTime } from '../../hooks/useMinDisplayTime.js'
|
||||
import { Ansi, Box, Text, useTheme } from '../../ink.js'
|
||||
import { findToolByName, type Tools } from '../../Tool.js'
|
||||
import { getReplPrimitiveTools } from '../../tools/REPLTool/primitiveTools.js'
|
||||
import type {
|
||||
CollapsedReadSearchGroup,
|
||||
NormalizedAssistantMessage,
|
||||
} from '../../types/message.js'
|
||||
import { uniq } from '../../utils/array.js'
|
||||
import { getToolUseIdsFromCollapsedGroup } from '../../utils/collapseReadSearch.js'
|
||||
import { getDisplayPath } from '../../utils/file.js'
|
||||
import { formatDuration, formatSecondsShort } from '../../utils/format.js'
|
||||
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
|
||||
import type { buildMessageLookups } from '../../utils/messages.js'
|
||||
import type { ThemeName } from '../../utils/theme.js'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { useSelectedMessageBg } from '../messageActions.js'
|
||||
import { PrBadge } from '../PrBadge.js'
|
||||
import { ToolUseLoader } from '../ToolUseLoader.js'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const teamMemCollapsed = feature('TEAMMEM') ? require('./teamMemCollapsed.js') as typeof import('./teamMemCollapsed.js') : null;
|
||||
const teamMemCollapsed = feature('TEAMMEM')
|
||||
? (require('./teamMemCollapsed.js') as typeof import('./teamMemCollapsed.js'))
|
||||
: null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
// Hold each ⤿ hint for a minimum duration so fast-completing tool calls
|
||||
// (bash commands, file reads, search patterns) are actually readable instead
|
||||
// of flickering past in a single frame.
|
||||
const MIN_HINT_DISPLAY_MS = 700;
|
||||
const MIN_HINT_DISPLAY_MS = 700
|
||||
|
||||
type Props = {
|
||||
message: CollapsedReadSearchGroup;
|
||||
inProgressToolUseIDs: Set<string>;
|
||||
shouldAnimate: boolean;
|
||||
verbose: boolean;
|
||||
tools: Tools;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
message: CollapsedReadSearchGroup
|
||||
inProgressToolUseIDs: Set<string>
|
||||
shouldAnimate: boolean
|
||||
verbose: boolean
|
||||
tools: Tools
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
/** True if this is the currently active collapsed group (last one, still loading) */
|
||||
isActiveGroup?: boolean;
|
||||
};
|
||||
isActiveGroup?: boolean
|
||||
}
|
||||
|
||||
/** Render a single tool use in verbose mode */
|
||||
function VerboseToolUse(t0) {
|
||||
const $ = _c(24);
|
||||
const {
|
||||
content,
|
||||
tools,
|
||||
lookups,
|
||||
inProgressToolUseIDs,
|
||||
shouldAnimate,
|
||||
theme
|
||||
} = t0;
|
||||
const bg = useSelectedMessageBg();
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== bg || $[1] !== content.id || $[2] !== content.input || $[3] !== content.name || $[4] !== inProgressToolUseIDs || $[5] !== lookups || $[6] !== shouldAnimate || $[7] !== theme || $[8] !== tools) {
|
||||
t2 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const tool = findToolByName(tools, content.name) ?? findToolByName(getReplPrimitiveTools(), content.name);
|
||||
if (!tool) {
|
||||
t2 = null;
|
||||
break bb0;
|
||||
}
|
||||
let t3;
|
||||
if ($[11] !== content.id || $[12] !== lookups.resolvedToolUseIDs) {
|
||||
t3 = lookups.resolvedToolUseIDs.has(content.id);
|
||||
$[11] = content.id;
|
||||
$[12] = lookups.resolvedToolUseIDs;
|
||||
$[13] = t3;
|
||||
} else {
|
||||
t3 = $[13];
|
||||
}
|
||||
const isResolved = t3;
|
||||
let t4;
|
||||
if ($[14] !== content.id || $[15] !== lookups.erroredToolUseIDs) {
|
||||
t4 = lookups.erroredToolUseIDs.has(content.id);
|
||||
$[14] = content.id;
|
||||
$[15] = lookups.erroredToolUseIDs;
|
||||
$[16] = t4;
|
||||
} else {
|
||||
t4 = $[16];
|
||||
}
|
||||
const isError = t4;
|
||||
let t5;
|
||||
if ($[17] !== content.id || $[18] !== inProgressToolUseIDs) {
|
||||
t5 = inProgressToolUseIDs.has(content.id);
|
||||
$[17] = content.id;
|
||||
$[18] = inProgressToolUseIDs;
|
||||
$[19] = t5;
|
||||
} else {
|
||||
t5 = $[19];
|
||||
}
|
||||
const isInProgress = t5;
|
||||
const resultMsg = lookups.toolResultByToolUseID.get(content.id);
|
||||
const rawToolResult = resultMsg?.type === "user" ? resultMsg.toolUseResult : undefined;
|
||||
const parsedOutput = tool.outputSchema?.safeParse(rawToolResult);
|
||||
const toolResult = parsedOutput?.success ? parsedOutput.data : undefined;
|
||||
const parsedInput = tool.inputSchema.safeParse(content.input);
|
||||
const input = parsedInput.success ? parsedInput.data : undefined;
|
||||
const userFacingName = tool.userFacingName(input);
|
||||
const toolUseMessage = input ? tool.renderToolUseMessage(input, {
|
||||
theme,
|
||||
verbose: true
|
||||
}) : null;
|
||||
const t6 = shouldAnimate && isInProgress;
|
||||
const t7 = !isResolved;
|
||||
let t8;
|
||||
if ($[20] !== isError || $[21] !== t6 || $[22] !== t7) {
|
||||
t8 = <ToolUseLoader shouldAnimate={t6} isUnresolved={t7} isError={isError} />;
|
||||
$[20] = isError;
|
||||
$[21] = t6;
|
||||
$[22] = t7;
|
||||
$[23] = t8;
|
||||
} else {
|
||||
t8 = $[23];
|
||||
}
|
||||
t1 = <Box key={content.id} flexDirection="column" marginTop={1} backgroundColor={bg}><Box flexDirection="row">{t8}<Text><Text bold={true}>{userFacingName}</Text>{toolUseMessage && <Text>({toolUseMessage})</Text>}</Text>{input && tool.renderToolUseTag?.(input)}</Box>{isResolved && !isError && toolResult !== undefined && <Box>{tool.renderToolResultMessage?.(toolResult, [], {
|
||||
function VerboseToolUse({
|
||||
content,
|
||||
tools,
|
||||
lookups,
|
||||
inProgressToolUseIDs,
|
||||
shouldAnimate,
|
||||
theme,
|
||||
}: {
|
||||
content: { type: 'tool_use'; id: string; name: string; input: unknown }
|
||||
tools: Tools
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
inProgressToolUseIDs: Set<string>
|
||||
shouldAnimate: boolean
|
||||
theme: ThemeName
|
||||
}): React.ReactNode {
|
||||
const bg = useSelectedMessageBg()
|
||||
// Same REPL-primitive fallback as getToolSearchOrReadInfo — REPL mode strips
|
||||
// these from the execution tools list, but virtual messages still need them
|
||||
// to render in verbose mode.
|
||||
const tool =
|
||||
findToolByName(tools, content.name) ??
|
||||
findToolByName(getReplPrimitiveTools(), content.name)
|
||||
if (!tool) return null
|
||||
|
||||
const isResolved = lookups.resolvedToolUseIDs.has(content.id)
|
||||
const isError = lookups.erroredToolUseIDs.has(content.id)
|
||||
const isInProgress = inProgressToolUseIDs.has(content.id)
|
||||
|
||||
const resultMsg = lookups.toolResultByToolUseID.get(content.id)
|
||||
const rawToolResult =
|
||||
resultMsg?.type === 'user' ? resultMsg.toolUseResult : undefined
|
||||
const parsedOutput = tool.outputSchema?.safeParse(rawToolResult)
|
||||
const toolResult = parsedOutput?.success ? parsedOutput.data : undefined
|
||||
|
||||
const parsedInput = tool.inputSchema.safeParse(content.input)
|
||||
const input = parsedInput.success ? parsedInput.data : undefined
|
||||
const userFacingName = tool.userFacingName(input)
|
||||
const toolUseMessage = input
|
||||
? tool.renderToolUseMessage(input, { theme, verbose: true })
|
||||
: null
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={content.id}
|
||||
flexDirection="column"
|
||||
marginTop={1}
|
||||
backgroundColor={bg}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
<ToolUseLoader
|
||||
shouldAnimate={shouldAnimate && isInProgress}
|
||||
isUnresolved={!isResolved}
|
||||
isError={isError}
|
||||
/>
|
||||
<Text>
|
||||
<Text bold>{userFacingName}</Text>
|
||||
{toolUseMessage && <Text>({toolUseMessage})</Text>}
|
||||
</Text>
|
||||
{input && tool.renderToolUseTag?.(input)}
|
||||
</Box>
|
||||
{isResolved && !isError && toolResult !== undefined && (
|
||||
<Box>
|
||||
{tool.renderToolResultMessage?.(toolResult, [], {
|
||||
verbose: true,
|
||||
tools,
|
||||
theme
|
||||
})}</Box>}</Box>;
|
||||
}
|
||||
$[0] = bg;
|
||||
$[1] = content.id;
|
||||
$[2] = content.input;
|
||||
$[3] = content.name;
|
||||
$[4] = inProgressToolUseIDs;
|
||||
$[5] = lookups;
|
||||
$[6] = shouldAnimate;
|
||||
$[7] = theme;
|
||||
$[8] = tools;
|
||||
$[9] = t1;
|
||||
$[10] = t2;
|
||||
} else {
|
||||
t1 = $[9];
|
||||
t2 = $[10];
|
||||
}
|
||||
if (t2 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t2;
|
||||
}
|
||||
return t1;
|
||||
theme,
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function CollapsedReadSearchContent({
|
||||
message,
|
||||
inProgressToolUseIDs,
|
||||
@@ -146,9 +124,9 @@ export function CollapsedReadSearchContent({
|
||||
verbose,
|
||||
tools,
|
||||
lookups,
|
||||
isActiveGroup
|
||||
isActiveGroup,
|
||||
}: Props): React.ReactNode {
|
||||
const bg = useSelectedMessageBg();
|
||||
const bg = useSelectedMessageBg()
|
||||
const {
|
||||
searchCount: rawSearchCount,
|
||||
readCount: rawReadCount,
|
||||
@@ -157,94 +135,141 @@ export function CollapsedReadSearchContent({
|
||||
memorySearchCount,
|
||||
memoryReadCount,
|
||||
memoryWriteCount,
|
||||
messages: groupMessages
|
||||
} = message;
|
||||
const [theme] = useTheme();
|
||||
const toolUseIds = getToolUseIdsFromCollapsedGroup(message);
|
||||
const anyError = toolUseIds.some(id => lookups.erroredToolUseIDs.has(id));
|
||||
const hasMemoryOps = memorySearchCount > 0 || memoryReadCount > 0 || memoryWriteCount > 0;
|
||||
const hasTeamMemoryOps = feature('TEAMMEM') ? teamMemCollapsed!.checkHasTeamMemOps(message) : false;
|
||||
messages: groupMessages,
|
||||
} = message
|
||||
const [theme] = useTheme()
|
||||
const toolUseIds = getToolUseIdsFromCollapsedGroup(message)
|
||||
const anyError = toolUseIds.some(id => lookups.erroredToolUseIDs.has(id))
|
||||
const hasMemoryOps =
|
||||
memorySearchCount > 0 || memoryReadCount > 0 || memoryWriteCount > 0
|
||||
const hasTeamMemoryOps = feature('TEAMMEM')
|
||||
? teamMemCollapsed!.checkHasTeamMemOps(message)
|
||||
: false
|
||||
|
||||
// Track the max seen counts so they only ever increase. The debounce timer
|
||||
// causes extra re-renders at arbitrary times; during a brief "invisible window"
|
||||
// in the streaming executor the group count can dip, which causes jitter.
|
||||
const maxReadCountRef = useRef(0);
|
||||
const maxSearchCountRef = useRef(0);
|
||||
const maxListCountRef = useRef(0);
|
||||
const maxMcpCountRef = useRef(0);
|
||||
const maxBashCountRef = useRef(0);
|
||||
maxReadCountRef.current = Math.max(maxReadCountRef.current, rawReadCount);
|
||||
maxSearchCountRef.current = Math.max(maxSearchCountRef.current, rawSearchCount);
|
||||
maxListCountRef.current = Math.max(maxListCountRef.current, rawListCount);
|
||||
maxMcpCountRef.current = Math.max(maxMcpCountRef.current, message.mcpCallCount ?? 0);
|
||||
maxBashCountRef.current = Math.max(maxBashCountRef.current, message.bashCount ?? 0);
|
||||
const readCount = maxReadCountRef.current;
|
||||
const searchCount = maxSearchCountRef.current;
|
||||
const listCount = maxListCountRef.current;
|
||||
const mcpCallCount = maxMcpCountRef.current;
|
||||
const maxReadCountRef = useRef(0)
|
||||
const maxSearchCountRef = useRef(0)
|
||||
const maxListCountRef = useRef(0)
|
||||
const maxMcpCountRef = useRef(0)
|
||||
const maxBashCountRef = useRef(0)
|
||||
maxReadCountRef.current = Math.max(maxReadCountRef.current, rawReadCount)
|
||||
maxSearchCountRef.current = Math.max(
|
||||
maxSearchCountRef.current,
|
||||
rawSearchCount,
|
||||
)
|
||||
maxListCountRef.current = Math.max(maxListCountRef.current, rawListCount)
|
||||
maxMcpCountRef.current = Math.max(
|
||||
maxMcpCountRef.current,
|
||||
message.mcpCallCount ?? 0,
|
||||
)
|
||||
maxBashCountRef.current = Math.max(
|
||||
maxBashCountRef.current,
|
||||
message.bashCount ?? 0,
|
||||
)
|
||||
const readCount = maxReadCountRef.current
|
||||
const searchCount = maxSearchCountRef.current
|
||||
const listCount = maxListCountRef.current
|
||||
const mcpCallCount = maxMcpCountRef.current
|
||||
// Subtract commands surfaced as "Committed …" / "Created PR …" so the
|
||||
// same command isn't counted twice. gitOpBashCount is read live (no max-ref
|
||||
// needed — it's 0 until results arrive, then only grows).
|
||||
const gitOpBashCount = message.gitOpBashCount ?? 0;
|
||||
const bashCount = isFullscreenEnvEnabled() ? Math.max(0, maxBashCountRef.current - gitOpBashCount) : 0;
|
||||
const hasNonMemoryOps = searchCount > 0 || readCount > 0 || listCount > 0 || replCount > 0 || mcpCallCount > 0 || bashCount > 0 || gitOpBashCount > 0;
|
||||
const readPaths = message.readFilePaths;
|
||||
const searchArgs = message.searchArgs;
|
||||
let incomingHint = message.latestDisplayHint;
|
||||
const gitOpBashCount = message.gitOpBashCount ?? 0
|
||||
const bashCount = isFullscreenEnvEnabled()
|
||||
? Math.max(0, maxBashCountRef.current - gitOpBashCount)
|
||||
: 0
|
||||
|
||||
const hasNonMemoryOps =
|
||||
searchCount > 0 ||
|
||||
readCount > 0 ||
|
||||
listCount > 0 ||
|
||||
replCount > 0 ||
|
||||
mcpCallCount > 0 ||
|
||||
bashCount > 0 ||
|
||||
gitOpBashCount > 0
|
||||
|
||||
const readPaths = message.readFilePaths
|
||||
const searchArgs = message.searchArgs
|
||||
let incomingHint = message.latestDisplayHint
|
||||
if (incomingHint === undefined) {
|
||||
const lastSearchRaw = searchArgs?.at(-1);
|
||||
const lastSearch = lastSearchRaw !== undefined ? `"${lastSearchRaw}"` : undefined;
|
||||
const lastRead = readPaths?.at(-1);
|
||||
incomingHint = lastRead !== undefined ? getDisplayPath(lastRead) : lastSearch;
|
||||
const lastSearchRaw = searchArgs?.at(-1)
|
||||
const lastSearch =
|
||||
lastSearchRaw !== undefined ? `"${lastSearchRaw}"` : undefined
|
||||
const lastRead = readPaths?.at(-1)
|
||||
incomingHint =
|
||||
lastRead !== undefined ? getDisplayPath(lastRead) : lastSearch
|
||||
}
|
||||
|
||||
// Active REPL calls emit repl_tool_call progress with the current inner
|
||||
// tool's name+input. Virtual messages don't arrive until REPL completes,
|
||||
// so this is the only source of a live hint during execution.
|
||||
if (isActiveGroup) {
|
||||
for (const id_0 of toolUseIds) {
|
||||
if (!inProgressToolUseIDs.has(id_0)) continue;
|
||||
const latest = lookups.progressMessagesByToolUseID.get(id_0)?.at(-1)?.data as { type?: string; phase?: string; toolInput?: unknown; toolName?: string } | undefined;
|
||||
for (const id of toolUseIds) {
|
||||
if (!inProgressToolUseIDs.has(id)) continue
|
||||
const latest = lookups.progressMessagesByToolUseID.get(id)?.at(-1)?.data
|
||||
if (latest?.type === 'repl_tool_call' && latest.phase === 'start') {
|
||||
const input = latest.toolInput as {
|
||||
command?: string;
|
||||
pattern?: string;
|
||||
file_path?: string;
|
||||
};
|
||||
incomingHint = input.file_path ?? (input.pattern ? `"${input.pattern}"` : undefined) ?? input.command ?? latest.toolName;
|
||||
command?: string
|
||||
pattern?: string
|
||||
file_path?: string
|
||||
}
|
||||
incomingHint =
|
||||
input.file_path ??
|
||||
(input.pattern ? `"${input.pattern}"` : undefined) ??
|
||||
input.command ??
|
||||
latest.toolName
|
||||
}
|
||||
}
|
||||
}
|
||||
const displayedHint = useMinDisplayTime(incomingHint, MIN_HINT_DISPLAY_MS);
|
||||
|
||||
const displayedHint = useMinDisplayTime(incomingHint, MIN_HINT_DISPLAY_MS)
|
||||
|
||||
// In verbose mode, render each tool use with its 1-line result summary
|
||||
if (verbose) {
|
||||
const toolUses: NormalizedAssistantMessage[] = [];
|
||||
const toolUses: NormalizedAssistantMessage[] = []
|
||||
for (const msg of groupMessages) {
|
||||
if (msg.type === 'assistant') {
|
||||
toolUses.push(msg);
|
||||
toolUses.push(msg)
|
||||
} else if (msg.type === 'grouped_tool_use') {
|
||||
toolUses.push(...msg.messages);
|
||||
toolUses.push(...msg.messages)
|
||||
}
|
||||
}
|
||||
return <Box flexDirection="column">
|
||||
{toolUses.map(msg_0 => {
|
||||
const content = msg_0.message.content[0];
|
||||
if (!content || typeof content === 'string' || content?.type !== 'tool_use') return null;
|
||||
return <VerboseToolUse key={content.id} content={content} tools={tools} lookups={lookups} inProgressToolUseIDs={inProgressToolUseIDs} shouldAnimate={shouldAnimate} theme={theme} />;
|
||||
})}
|
||||
{message.hookInfos && message.hookInfos.length > 0 && <>
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{toolUses.map(msg => {
|
||||
const content = msg.message.content[0]
|
||||
if (content?.type !== 'tool_use') return null
|
||||
return (
|
||||
<VerboseToolUse
|
||||
key={content.id}
|
||||
content={content}
|
||||
tools={tools}
|
||||
lookups={lookups}
|
||||
inProgressToolUseIDs={inProgressToolUseIDs}
|
||||
shouldAnimate={shouldAnimate}
|
||||
theme={theme}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{message.hookInfos && message.hookInfos.length > 0 && (
|
||||
<>
|
||||
<Text dimColor>
|
||||
{' ⎿ '}Ran {message.hookCount} PreToolUse{' '}
|
||||
{message.hookCount === 1 ? 'hook' : 'hooks'} (
|
||||
{formatSecondsShort(message.hookTotalMs ?? 0)})
|
||||
</Text>
|
||||
{message.hookInfos.map((info, idx) => <Text key={`hook-${idx}`} dimColor>
|
||||
{message.hookInfos.map((info, idx) => (
|
||||
<Text key={`hook-${idx}`} dimColor>
|
||||
{' ⎿ '}
|
||||
{info.command} ({formatSecondsShort(info.durationMs ?? 0)})
|
||||
</Text>)}
|
||||
</>}
|
||||
{message.relevantMemories?.map(m => <Box key={m.path} flexDirection="column" marginTop={1}>
|
||||
</Text>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{message.relevantMemories?.map(m => (
|
||||
<Box key={m.path} flexDirection="column" marginTop={1}>
|
||||
<Text dimColor>
|
||||
{' ⎿ '}Recalled {basename(m.path)}
|
||||
</Text>
|
||||
@@ -253,8 +278,10 @@ export function CollapsedReadSearchContent({
|
||||
<Ansi>{m.content}</Ansi>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>)}
|
||||
</Box>;
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Non-verbose mode: Show counts with blinking grey dot while active, green dot when finalized
|
||||
@@ -263,70 +290,79 @@ export function CollapsedReadSearchContent({
|
||||
// Defensive: If all counts are 0, don't render the collapsed group
|
||||
// This shouldn't happen in normal operation, but handles edge cases
|
||||
if (!hasMemoryOps && !hasTeamMemoryOps && !hasNonMemoryOps) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Find the slowest in-progress shell command in this group. BashTool yields
|
||||
// progress every second but the collapsed renderer never showed it — long
|
||||
// commands (npm install, tests) looked frozen. Shown after 2s so fast
|
||||
// commands stay clean; the ticking counter reassures that slow ones aren't stuck.
|
||||
let shellProgressSuffix = '';
|
||||
let shellProgressSuffix = ''
|
||||
if (isFullscreenEnvEnabled() && isActiveGroup) {
|
||||
let elapsed: number | undefined;
|
||||
let lines = 0;
|
||||
for (const id_1 of toolUseIds) {
|
||||
if (!inProgressToolUseIDs.has(id_1)) continue;
|
||||
const data = lookups.progressMessagesByToolUseID.get(id_1)?.at(-1)?.data as { type?: string; elapsedTimeSeconds?: number; totalLines?: number } | undefined;
|
||||
if (data?.type !== 'bash_progress' && data?.type !== 'powershell_progress') {
|
||||
continue;
|
||||
let elapsed: number | undefined
|
||||
let lines = 0
|
||||
for (const id of toolUseIds) {
|
||||
if (!inProgressToolUseIDs.has(id)) continue
|
||||
const data = lookups.progressMessagesByToolUseID.get(id)?.at(-1)?.data
|
||||
if (
|
||||
data?.type !== 'bash_progress' &&
|
||||
data?.type !== 'powershell_progress'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (elapsed === undefined || (data.elapsedTimeSeconds ?? 0) > elapsed) {
|
||||
elapsed = data.elapsedTimeSeconds ?? 0;
|
||||
lines = data.totalLines ?? 0;
|
||||
if (elapsed === undefined || data.elapsedTimeSeconds > elapsed) {
|
||||
elapsed = data.elapsedTimeSeconds
|
||||
lines = data.totalLines
|
||||
}
|
||||
}
|
||||
if (elapsed !== undefined && elapsed >= 2) {
|
||||
const time = formatDuration(elapsed * 1000);
|
||||
shellProgressSuffix = lines > 0 ? ` (${time} · ${lines} ${lines === 1 ? 'line' : 'lines'})` : ` (${time})`;
|
||||
const time = formatDuration(elapsed * 1000)
|
||||
shellProgressSuffix =
|
||||
lines > 0
|
||||
? ` (${time} · ${lines} ${lines === 1 ? 'line' : 'lines'})`
|
||||
: ` (${time})`
|
||||
}
|
||||
}
|
||||
|
||||
// Build non-memory parts first (search, read, repl, mcp, bash) — these render
|
||||
// before memory so the line reads "Ran 3 bash commands, recalled 1 memory".
|
||||
const nonMemParts: React.ReactNode[] = [];
|
||||
const nonMemParts: React.ReactNode[] = []
|
||||
|
||||
// Git operations lead the line — they're the load-bearing outcome.
|
||||
function pushPart(key: string, verb: string, body: React.ReactNode): void {
|
||||
const isFirst = nonMemParts.length === 0;
|
||||
if (!isFirst) nonMemParts.push(<Text key={`comma-${key}`}>, </Text>);
|
||||
nonMemParts.push(<Text key={key}>
|
||||
const isFirst = nonMemParts.length === 0
|
||||
if (!isFirst) nonMemParts.push(<Text key={`comma-${key}`}>, </Text>)
|
||||
nonMemParts.push(
|
||||
<Text key={key}>
|
||||
{isFirst ? verb[0]!.toUpperCase() + verb.slice(1) : verb} {body}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
if (isFullscreenEnvEnabled() && message.commits?.length) {
|
||||
const byKind = {
|
||||
committed: 'committed',
|
||||
amended: 'amended commit',
|
||||
'cherry-picked': 'cherry-picked'
|
||||
};
|
||||
'cherry-picked': 'cherry-picked',
|
||||
}
|
||||
for (const kind of ['committed', 'amended', 'cherry-picked'] as const) {
|
||||
const shas = message.commits.filter(c => c.kind === kind).map(c_0 => c_0.sha);
|
||||
const shas = message.commits.filter(c => c.kind === kind).map(c => c.sha)
|
||||
if (shas.length) {
|
||||
pushPart(kind, byKind[kind], <Text bold>{shas.join(', ')}</Text>);
|
||||
pushPart(kind, byKind[kind], <Text bold>{shas.join(', ')}</Text>)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isFullscreenEnvEnabled() && message.pushes?.length) {
|
||||
const branches = uniq(message.pushes.map(p => p.branch));
|
||||
pushPart('push', 'pushed to', <Text bold>{branches.join(', ')}</Text>);
|
||||
const branches = uniq(message.pushes.map(p => p.branch))
|
||||
pushPart('push', 'pushed to', <Text bold>{branches.join(', ')}</Text>)
|
||||
}
|
||||
if (isFullscreenEnvEnabled() && message.branches?.length) {
|
||||
const byAction = {
|
||||
merged: 'merged',
|
||||
rebased: 'rebased onto'
|
||||
};
|
||||
const byAction = { merged: 'merged', rebased: 'rebased onto' }
|
||||
for (const b of message.branches) {
|
||||
pushPart(`br-${b.action}-${b.ref}`, byAction[b.action], <Text bold>{b.ref}</Text>);
|
||||
pushPart(
|
||||
`br-${b.action}-${b.ref}`,
|
||||
byAction[b.action],
|
||||
<Text bold>{b.ref}</Text>,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isFullscreenEnvEnabled() && message.prs?.length) {
|
||||
@@ -336,148 +372,248 @@ export function CollapsedReadSearchContent({
|
||||
merged: 'merged',
|
||||
commented: 'commented on',
|
||||
closed: 'closed',
|
||||
ready: 'marked ready'
|
||||
};
|
||||
ready: 'marked ready',
|
||||
}
|
||||
for (const pr of message.prs) {
|
||||
pushPart(`pr-${pr.action}-${pr.number}`, verbs[pr.action], pr.url ? <PrBadge number={pr.number} url={pr.url} bold /> : <Text bold>PR #{pr.number}</Text>);
|
||||
pushPart(
|
||||
`pr-${pr.action}-${pr.number}`,
|
||||
verbs[pr.action],
|
||||
pr.url ? (
|
||||
<PrBadge number={pr.number} url={pr.url} bold />
|
||||
) : (
|
||||
<Text bold>PR #{pr.number}</Text>
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (searchCount > 0) {
|
||||
const isFirst_0 = nonMemParts.length === 0;
|
||||
const searchVerb = isActiveGroup ? isFirst_0 ? 'Searching for' : 'searching for' : isFirst_0 ? 'Searched for' : 'searched for';
|
||||
if (!isFirst_0) {
|
||||
nonMemParts.push(<Text key="comma-s">, </Text>);
|
||||
const isFirst = nonMemParts.length === 0
|
||||
const searchVerb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Searching for'
|
||||
: 'searching for'
|
||||
: isFirst
|
||||
? 'Searched for'
|
||||
: 'searched for'
|
||||
if (!isFirst) {
|
||||
nonMemParts.push(<Text key="comma-s">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="search">
|
||||
nonMemParts.push(
|
||||
<Text key="search">
|
||||
{searchVerb} <Text bold>{searchCount}</Text>{' '}
|
||||
{searchCount === 1 ? 'pattern' : 'patterns'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (readCount > 0) {
|
||||
const isFirst_1 = nonMemParts.length === 0;
|
||||
const readVerb = isActiveGroup ? isFirst_1 ? 'Reading' : 'reading' : isFirst_1 ? 'Read' : 'read';
|
||||
if (!isFirst_1) {
|
||||
nonMemParts.push(<Text key="comma-r">, </Text>);
|
||||
const isFirst = nonMemParts.length === 0
|
||||
const readVerb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Reading'
|
||||
: 'reading'
|
||||
: isFirst
|
||||
? 'Read'
|
||||
: 'read'
|
||||
if (!isFirst) {
|
||||
nonMemParts.push(<Text key="comma-r">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="read">
|
||||
nonMemParts.push(
|
||||
<Text key="read">
|
||||
{readVerb} <Text bold>{readCount}</Text>{' '}
|
||||
{readCount === 1 ? 'file' : 'files'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (listCount > 0) {
|
||||
const isFirst_2 = nonMemParts.length === 0;
|
||||
const listVerb = isActiveGroup ? isFirst_2 ? 'Listing' : 'listing' : isFirst_2 ? 'Listed' : 'listed';
|
||||
if (!isFirst_2) {
|
||||
nonMemParts.push(<Text key="comma-l">, </Text>);
|
||||
const isFirst = nonMemParts.length === 0
|
||||
const listVerb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Listing'
|
||||
: 'listing'
|
||||
: isFirst
|
||||
? 'Listed'
|
||||
: 'listed'
|
||||
if (!isFirst) {
|
||||
nonMemParts.push(<Text key="comma-l">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="list">
|
||||
nonMemParts.push(
|
||||
<Text key="list">
|
||||
{listVerb} <Text bold>{listCount}</Text>{' '}
|
||||
{listCount === 1 ? 'directory' : 'directories'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (replCount > 0) {
|
||||
const replVerb = isActiveGroup ? "REPL'ing" : "REPL'd";
|
||||
const replVerb = isActiveGroup ? "REPL'ing" : "REPL'd"
|
||||
if (nonMemParts.length > 0) {
|
||||
nonMemParts.push(<Text key="comma-repl">, </Text>);
|
||||
nonMemParts.push(<Text key="comma-repl">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="repl">
|
||||
nonMemParts.push(
|
||||
<Text key="repl">
|
||||
{replVerb} <Text bold>{replCount}</Text>{' '}
|
||||
{replCount === 1 ? 'time' : 'times'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (mcpCallCount > 0) {
|
||||
const serverLabel = message.mcpServerNames?.map(n => n.replace(/^claude\.ai /, '')).join(', ') || 'MCP';
|
||||
const isFirst_3 = nonMemParts.length === 0;
|
||||
const verb_0 = isActiveGroup ? isFirst_3 ? 'Querying' : 'querying' : isFirst_3 ? 'Queried' : 'queried';
|
||||
if (!isFirst_3) {
|
||||
nonMemParts.push(<Text key="comma-mcp">, </Text>);
|
||||
const serverLabel =
|
||||
message.mcpServerNames
|
||||
?.map(n => n.replace(/^claude\.ai /, ''))
|
||||
.join(', ') || 'MCP'
|
||||
const isFirst = nonMemParts.length === 0
|
||||
const verb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Querying'
|
||||
: 'querying'
|
||||
: isFirst
|
||||
? 'Queried'
|
||||
: 'queried'
|
||||
if (!isFirst) {
|
||||
nonMemParts.push(<Text key="comma-mcp">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="mcp">
|
||||
{verb_0} {serverLabel}
|
||||
{mcpCallCount > 1 && <>
|
||||
nonMemParts.push(
|
||||
<Text key="mcp">
|
||||
{verb} {serverLabel}
|
||||
{mcpCallCount > 1 && (
|
||||
<>
|
||||
{' '}
|
||||
<Text bold>{mcpCallCount}</Text> times
|
||||
</>}
|
||||
</Text>);
|
||||
</>
|
||||
)}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (isFullscreenEnvEnabled() && bashCount > 0) {
|
||||
const isFirst_4 = nonMemParts.length === 0;
|
||||
const verb_1 = isActiveGroup ? isFirst_4 ? 'Running' : 'running' : isFirst_4 ? 'Ran' : 'ran';
|
||||
if (!isFirst_4) {
|
||||
nonMemParts.push(<Text key="comma-bash">, </Text>);
|
||||
const isFirst = nonMemParts.length === 0
|
||||
const verb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Running'
|
||||
: 'running'
|
||||
: isFirst
|
||||
? 'Ran'
|
||||
: 'ran'
|
||||
if (!isFirst) {
|
||||
nonMemParts.push(<Text key="comma-bash">, </Text>)
|
||||
}
|
||||
nonMemParts.push(<Text key="bash">
|
||||
{verb_1} <Text bold>{bashCount}</Text> bash{' '}
|
||||
nonMemParts.push(
|
||||
<Text key="bash">
|
||||
{verb} <Text bold>{bashCount}</Text> bash{' '}
|
||||
{bashCount === 1 ? 'command' : 'commands'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
// Build memory parts (auto-memory) — rendered after nonMemParts
|
||||
const hasPrecedingNonMem = nonMemParts.length > 0;
|
||||
const memParts: React.ReactNode[] = [];
|
||||
const hasPrecedingNonMem = nonMemParts.length > 0
|
||||
const memParts: React.ReactNode[] = []
|
||||
|
||||
if (memoryReadCount > 0) {
|
||||
const isFirst_5 = !hasPrecedingNonMem && memParts.length === 0;
|
||||
const verb_2 = isActiveGroup ? isFirst_5 ? 'Recalling' : 'recalling' : isFirst_5 ? 'Recalled' : 'recalled';
|
||||
if (!isFirst_5) {
|
||||
memParts.push(<Text key="comma-mr">, </Text>);
|
||||
const isFirst = !hasPrecedingNonMem && memParts.length === 0
|
||||
const verb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Recalling'
|
||||
: 'recalling'
|
||||
: isFirst
|
||||
? 'Recalled'
|
||||
: 'recalled'
|
||||
if (!isFirst) {
|
||||
memParts.push(<Text key="comma-mr">, </Text>)
|
||||
}
|
||||
memParts.push(<Text key="mem-read">
|
||||
{verb_2} <Text bold>{memoryReadCount}</Text>{' '}
|
||||
memParts.push(
|
||||
<Text key="mem-read">
|
||||
{verb} <Text bold>{memoryReadCount}</Text>{' '}
|
||||
{memoryReadCount === 1 ? 'memory' : 'memories'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
if (memorySearchCount > 0) {
|
||||
const isFirst_6 = !hasPrecedingNonMem && memParts.length === 0;
|
||||
const verb_3 = isActiveGroup ? isFirst_6 ? 'Searching' : 'searching' : isFirst_6 ? 'Searched' : 'searched';
|
||||
if (!isFirst_6) {
|
||||
memParts.push(<Text key="comma-ms">, </Text>);
|
||||
const isFirst = !hasPrecedingNonMem && memParts.length === 0
|
||||
const verb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Searching'
|
||||
: 'searching'
|
||||
: isFirst
|
||||
? 'Searched'
|
||||
: 'searched'
|
||||
if (!isFirst) {
|
||||
memParts.push(<Text key="comma-ms">, </Text>)
|
||||
}
|
||||
memParts.push(<Text key="mem-search">{`${verb_3} memories`}</Text>);
|
||||
memParts.push(<Text key="mem-search">{`${verb} memories`}</Text>)
|
||||
}
|
||||
|
||||
if (memoryWriteCount > 0) {
|
||||
const isFirst_7 = !hasPrecedingNonMem && memParts.length === 0;
|
||||
const verb_4 = isActiveGroup ? isFirst_7 ? 'Writing' : 'writing' : isFirst_7 ? 'Wrote' : 'wrote';
|
||||
if (!isFirst_7) {
|
||||
memParts.push(<Text key="comma-mw">, </Text>);
|
||||
const isFirst = !hasPrecedingNonMem && memParts.length === 0
|
||||
const verb = isActiveGroup
|
||||
? isFirst
|
||||
? 'Writing'
|
||||
: 'writing'
|
||||
: isFirst
|
||||
? 'Wrote'
|
||||
: 'wrote'
|
||||
if (!isFirst) {
|
||||
memParts.push(<Text key="comma-mw">, </Text>)
|
||||
}
|
||||
memParts.push(<Text key="mem-write">
|
||||
{verb_4} <Text bold>{memoryWriteCount}</Text>{' '}
|
||||
memParts.push(
|
||||
<Text key="mem-write">
|
||||
{verb} <Text bold>{memoryWriteCount}</Text>{' '}
|
||||
{memoryWriteCount === 1 ? 'memory' : 'memories'}
|
||||
</Text>);
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
return <Box flexDirection="column" marginTop={1} backgroundColor={bg}>
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1} backgroundColor={bg}>
|
||||
<Box flexDirection="row">
|
||||
{isActiveGroup ? <ToolUseLoader shouldAnimate isUnresolved isError={anyError} /> : <Box minWidth={2} />}
|
||||
{isActiveGroup ? (
|
||||
<ToolUseLoader shouldAnimate isUnresolved isError={anyError} />
|
||||
) : (
|
||||
<Box minWidth={2} />
|
||||
)}
|
||||
<Text dimColor={!isActiveGroup}>
|
||||
{nonMemParts}
|
||||
{memParts}
|
||||
{feature('TEAMMEM') ? teamMemCollapsed!.TeamMemCountParts({
|
||||
message,
|
||||
isActiveGroup,
|
||||
hasPrecedingParts: hasPrecedingNonMem || memParts.length > 0
|
||||
}) : null}
|
||||
{feature('TEAMMEM')
|
||||
? teamMemCollapsed!.TeamMemCountParts({
|
||||
message,
|
||||
isActiveGroup,
|
||||
hasPrecedingParts: hasPrecedingNonMem || memParts.length > 0,
|
||||
})
|
||||
: null}
|
||||
{isActiveGroup && <Text key="ellipsis">…</Text>} <CtrlOToExpand />
|
||||
</Text>
|
||||
</Box>
|
||||
{isActiveGroup && displayedHint !== undefined &&
|
||||
// Row layout: 5-wide gutter for ⎿, then a flex column for the text.
|
||||
// Ink's wrap stays inside the right column so continuation lines
|
||||
// indent under ⎿. MAX_HINT_CHARS in commandAsHint caps total at ~5 lines.
|
||||
<Box flexDirection="row">
|
||||
{isActiveGroup && displayedHint !== undefined && (
|
||||
// Row layout: 5-wide gutter for ⎿, then a flex column for the text.
|
||||
// Ink's wrap stays inside the right column so continuation lines
|
||||
// indent under ⎿. MAX_HINT_CHARS in commandAsHint caps total at ~5 lines.
|
||||
<Box flexDirection="row">
|
||||
<Box width={5} flexShrink={0}>
|
||||
<Text dimColor>{' ⎿ '}</Text>
|
||||
</Box>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{displayedHint.split('\n').map((line, i, arr) => <Text key={`hint-${i}`} dimColor>
|
||||
{displayedHint.split('\n').map((line, i, arr) => (
|
||||
<Text key={`hint-${i}`} dimColor>
|
||||
{line}
|
||||
{i === arr.length - 1 && shellProgressSuffix}
|
||||
</Text>)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</Box>}
|
||||
{message.hookTotalMs !== undefined && message.hookTotalMs > 0 && <Text dimColor>
|
||||
</Box>
|
||||
)}
|
||||
{message.hookTotalMs !== undefined && message.hookTotalMs > 0 && (
|
||||
<Text dimColor>
|
||||
{' ⎿ '}Ran {message.hookCount} PreToolUse{' '}
|
||||
{message.hookCount === 1 ? 'hook' : 'hooks'} (
|
||||
{formatSecondsShort(message.hookTotalMs)})
|
||||
</Text>}
|
||||
</Box>;
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
|
||||
export function CompactBoundaryMessage() {
|
||||
const $ = _c(2);
|
||||
const historyShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
|
||||
let t0;
|
||||
if ($[0] !== historyShortcut) {
|
||||
t0 = <Box marginY={1}><Text dimColor={true}>✻ Conversation compacted ({historyShortcut} for history)</Text></Box>;
|
||||
$[0] = historyShortcut;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
|
||||
|
||||
export function CompactBoundaryMessage(): React.ReactNode {
|
||||
const historyShortcut = useShortcutDisplay(
|
||||
'app:toggleTranscript',
|
||||
'Global',
|
||||
'ctrl+o',
|
||||
)
|
||||
|
||||
return (
|
||||
<Box marginY={1}>
|
||||
<Text dimColor>
|
||||
✻ Conversation compacted ({historyShortcut} for history)
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,62 +1,71 @@
|
||||
import type { ToolResultBlockParam, ToolUseBlockParam } from '@anthropic-ai/sdk/resources/messages/messages.mjs';
|
||||
import * as React from 'react';
|
||||
import { filterToolProgressMessages, findToolByName, type Tools } from '../../Tool.js';
|
||||
import type { GroupedToolUseMessage } from '../../types/message.js';
|
||||
import type { buildMessageLookups } from '../../utils/messages.js';
|
||||
import type {
|
||||
ToolResultBlockParam,
|
||||
ToolUseBlockParam,
|
||||
} from '@anthropic-ai/sdk/resources/messages/messages.mjs'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
filterToolProgressMessages,
|
||||
findToolByName,
|
||||
type Tools,
|
||||
} from '../../Tool.js'
|
||||
import type { GroupedToolUseMessage } from '../../types/message.js'
|
||||
import type { buildMessageLookups } from '../../utils/messages.js'
|
||||
|
||||
type Props = {
|
||||
message: GroupedToolUseMessage;
|
||||
tools: Tools;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
inProgressToolUseIDs: Set<string>;
|
||||
shouldAnimate: boolean;
|
||||
};
|
||||
message: GroupedToolUseMessage
|
||||
tools: Tools
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
inProgressToolUseIDs: Set<string>
|
||||
shouldAnimate: boolean
|
||||
}
|
||||
|
||||
export function GroupedToolUseContent({
|
||||
message,
|
||||
tools,
|
||||
lookups,
|
||||
inProgressToolUseIDs,
|
||||
shouldAnimate
|
||||
shouldAnimate,
|
||||
}: Props): React.ReactNode {
|
||||
const tool = findToolByName(tools, message.toolName);
|
||||
const tool = findToolByName(tools, message.toolName)
|
||||
if (!tool?.renderGroupedToolUse) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Build a map from tool_use_id to result data
|
||||
const resultsByToolUseId = new Map<string, {
|
||||
param: ToolResultBlockParam;
|
||||
output: unknown;
|
||||
}>();
|
||||
const resultsByToolUseId = new Map<
|
||||
string,
|
||||
{ param: ToolResultBlockParam; output: unknown }
|
||||
>()
|
||||
for (const resultMsg of message.results) {
|
||||
const contentArr = resultMsg.message.content;
|
||||
if (!Array.isArray(contentArr)) continue;
|
||||
for (const content of contentArr) {
|
||||
if (typeof content === 'string') continue;
|
||||
for (const content of resultMsg.message.content) {
|
||||
if (content.type === 'tool_result') {
|
||||
resultsByToolUseId.set((content as ToolResultBlockParam).tool_use_id, {
|
||||
param: content as ToolResultBlockParam,
|
||||
output: resultMsg.toolUseResult
|
||||
});
|
||||
resultsByToolUseId.set(content.tool_use_id, {
|
||||
param: content,
|
||||
output: resultMsg.toolUseResult,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toolUsesData = message.messages.map(msg => {
|
||||
const contentArr = msg.message.content;
|
||||
const rawContent = Array.isArray(contentArr) ? contentArr[0] : undefined;
|
||||
const content = rawContent as ToolUseBlockParam;
|
||||
const result = resultsByToolUseId.get(content.id);
|
||||
const content = msg.message.content[0]
|
||||
const result = resultsByToolUseId.get(content.id)
|
||||
return {
|
||||
param: content,
|
||||
param: content as ToolUseBlockParam,
|
||||
isResolved: lookups.resolvedToolUseIDs.has(content.id),
|
||||
isError: lookups.erroredToolUseIDs.has(content.id),
|
||||
isInProgress: inProgressToolUseIDs.has(content.id),
|
||||
progressMessages: filterToolProgressMessages(lookups.progressMessagesByToolUseID.get(content.id) ?? []),
|
||||
result
|
||||
};
|
||||
});
|
||||
const anyInProgress = toolUsesData.some(d => d.isInProgress);
|
||||
progressMessages: filterToolProgressMessages(
|
||||
lookups.progressMessagesByToolUseID.get(content.id) ?? [],
|
||||
),
|
||||
result,
|
||||
}
|
||||
})
|
||||
|
||||
const anyInProgress = toolUsesData.some(d => d.isInProgress)
|
||||
|
||||
return tool.renderGroupedToolUse(toolUsesData, {
|
||||
shouldAnimate: shouldAnimate && anyInProgress,
|
||||
tools
|
||||
});
|
||||
tools,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,161 +1,91 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { useQueuedMessage } from '../../context/QueuedMessageContext.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { formatBriefTimestamp } from '../../utils/formatBriefTimestamp.js';
|
||||
import { findThinkingTriggerPositions, getRainbowColor, isUltrathinkEnabled } from '../../utils/thinking.js';
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useContext } from 'react'
|
||||
import { useQueuedMessage } from '../../context/QueuedMessageContext.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { formatBriefTimestamp } from '../../utils/formatBriefTimestamp.js'
|
||||
import {
|
||||
findThinkingTriggerPositions,
|
||||
getRainbowColor,
|
||||
isUltrathinkEnabled,
|
||||
} from '../../utils/thinking.js'
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js'
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
useBriefLayout?: boolean;
|
||||
timestamp?: string;
|
||||
};
|
||||
export function HighlightedThinkingText(t0) {
|
||||
const $ = _c(31);
|
||||
const {
|
||||
text,
|
||||
useBriefLayout,
|
||||
timestamp
|
||||
} = t0;
|
||||
const isQueued = useQueuedMessage()?.isQueued ?? false;
|
||||
const isSelected = useContext(MessageActionsSelectedContext);
|
||||
const pointerColor = isSelected ? "suggestion" : "subtle";
|
||||
if (useBriefLayout) {
|
||||
let t1;
|
||||
if ($[0] !== timestamp) {
|
||||
t1 = timestamp ? formatBriefTimestamp(timestamp) : "";
|
||||
$[0] = timestamp;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const ts = t1;
|
||||
const t2 = isQueued ? "subtle" : "briefLabelYou";
|
||||
let t3;
|
||||
if ($[2] !== t2) {
|
||||
t3 = <Text color={t2}>You</Text>;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== ts) {
|
||||
t4 = ts ? <Text dimColor={true}> {ts}</Text> : null;
|
||||
$[4] = ts;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
let t5;
|
||||
if ($[6] !== t3 || $[7] !== t4) {
|
||||
t5 = <Box flexDirection="row">{t3}{t4}</Box>;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
const t6 = isQueued ? "subtle" : "text";
|
||||
let t7;
|
||||
if ($[9] !== t6 || $[10] !== text) {
|
||||
t7 = <Text color={t6}>{text}</Text>;
|
||||
$[9] = t6;
|
||||
$[10] = text;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t7 = $[11];
|
||||
}
|
||||
let t8;
|
||||
if ($[12] !== t5 || $[13] !== t7) {
|
||||
t8 = <Box flexDirection="column" paddingLeft={2}>{t5}{t7}</Box>;
|
||||
$[12] = t5;
|
||||
$[13] = t7;
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
}
|
||||
return t8;
|
||||
}
|
||||
let parts;
|
||||
let t1;
|
||||
if ($[15] !== pointerColor || $[16] !== text) {
|
||||
t1 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const triggers = isUltrathinkEnabled() ? findThinkingTriggerPositions(text) : [];
|
||||
if (triggers.length === 0) {
|
||||
let t2;
|
||||
if ($[19] !== pointerColor) {
|
||||
t2 = <Text color={pointerColor}>{figures.pointer} </Text>;
|
||||
$[19] = pointerColor;
|
||||
$[20] = t2;
|
||||
} else {
|
||||
t2 = $[20];
|
||||
}
|
||||
let t3;
|
||||
if ($[21] !== text) {
|
||||
t3 = <Text color="text">{text}</Text>;
|
||||
$[21] = text;
|
||||
$[22] = t3;
|
||||
} else {
|
||||
t3 = $[22];
|
||||
}
|
||||
let t4;
|
||||
if ($[23] !== t2 || $[24] !== t3) {
|
||||
t4 = <Text>{t2}{t3}</Text>;
|
||||
$[23] = t2;
|
||||
$[24] = t3;
|
||||
$[25] = t4;
|
||||
} else {
|
||||
t4 = $[25];
|
||||
}
|
||||
t1 = t4;
|
||||
break bb0;
|
||||
}
|
||||
parts = [];
|
||||
let cursor = 0;
|
||||
for (const t of triggers) {
|
||||
if (t.start > cursor) {
|
||||
parts.push(<Text key={`plain-${cursor}`} color="text">{text.slice(cursor, t.start)}</Text>);
|
||||
}
|
||||
for (let i = t.start; i < t.end; i++) {
|
||||
parts.push(<Text key={`rb-${i}`} color={getRainbowColor(i - t.start)}>{text[i]}</Text>);
|
||||
}
|
||||
cursor = t.end;
|
||||
}
|
||||
if (cursor < text.length) {
|
||||
parts.push(<Text key={`plain-${cursor}`} color="text">{text.slice(cursor)}</Text>);
|
||||
}
|
||||
}
|
||||
$[15] = pointerColor;
|
||||
$[16] = text;
|
||||
$[17] = parts;
|
||||
$[18] = t1;
|
||||
} else {
|
||||
parts = $[17];
|
||||
t1 = $[18];
|
||||
}
|
||||
if (t1 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t1;
|
||||
}
|
||||
let t2;
|
||||
if ($[26] !== pointerColor) {
|
||||
t2 = <Text color={pointerColor}>{figures.pointer} </Text>;
|
||||
$[26] = pointerColor;
|
||||
$[27] = t2;
|
||||
} else {
|
||||
t2 = $[27];
|
||||
}
|
||||
let t3;
|
||||
if ($[28] !== parts || $[29] !== t2) {
|
||||
t3 = <Text>{t2}{parts}</Text>;
|
||||
$[28] = parts;
|
||||
$[29] = t2;
|
||||
$[30] = t3;
|
||||
} else {
|
||||
t3 = $[30];
|
||||
}
|
||||
return t3;
|
||||
text: string
|
||||
useBriefLayout?: boolean
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
export function HighlightedThinkingText({
|
||||
text,
|
||||
useBriefLayout,
|
||||
timestamp,
|
||||
}: Props): React.ReactNode {
|
||||
// Brief/assistant mode: chat-style "You" label instead of the ❯ highlight.
|
||||
// Parent drops its backgroundColor when this is true, so no grey shows
|
||||
// through. No manual wrap needed — Ink wraps inside the parent Box.
|
||||
const isQueued = useQueuedMessage()?.isQueued ?? false
|
||||
const isSelected = useContext(MessageActionsSelectedContext)
|
||||
const pointerColor = isSelected ? 'suggestion' : 'subtle'
|
||||
if (useBriefLayout) {
|
||||
const ts = timestamp ? formatBriefTimestamp(timestamp) : ''
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={isQueued ? 'subtle' : 'briefLabelYou'}>You</Text>
|
||||
{ts ? <Text dimColor> {ts}</Text> : null}
|
||||
</Box>
|
||||
<Text color={isQueued ? 'subtle' : 'text'}>{text}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const triggers = isUltrathinkEnabled()
|
||||
? findThinkingTriggerPositions(text)
|
||||
: []
|
||||
|
||||
if (triggers.length === 0) {
|
||||
return (
|
||||
<Text>
|
||||
<Text color={pointerColor}>{figures.pointer} </Text>
|
||||
<Text color="text">{text}</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// Static rainbow (no shimmer — transcript messages don't animate)
|
||||
const parts: React.ReactNode[] = []
|
||||
let cursor = 0
|
||||
for (const t of triggers) {
|
||||
if (t.start > cursor) {
|
||||
parts.push(
|
||||
<Text key={`plain-${cursor}`} color="text">
|
||||
{text.slice(cursor, t.start)}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
for (let i = t.start; i < t.end; i++) {
|
||||
parts.push(
|
||||
<Text key={`rb-${i}`} color={getRainbowColor(i - t.start)}>
|
||||
{text[i]}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
cursor = t.end
|
||||
}
|
||||
if (cursor < text.length) {
|
||||
parts.push(
|
||||
<Text key={`plain-${cursor}`} color="text">
|
||||
{text.slice(cursor)}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Text>
|
||||
<Text color={pointerColor}>{figures.pointer} </Text>
|
||||
{parts}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,115 +1,67 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js';
|
||||
import type { buildMessageLookups } from 'src/utils/messages.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import * as React from 'react'
|
||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
||||
import type { buildMessageLookups } from 'src/utils/messages.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
type Props = {
|
||||
hookEvent: HookEvent;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
toolUseID: string;
|
||||
verbose: boolean;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function HookProgressMessage(t0) {
|
||||
const $ = _c(22);
|
||||
const {
|
||||
hookEvent,
|
||||
lookups,
|
||||
toolUseID,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== hookEvent || $[1] !== lookups.inProgressHookCounts || $[2] !== toolUseID) {
|
||||
t1 = lookups.inProgressHookCounts.get(toolUseID)?.get(hookEvent) ?? 0;
|
||||
$[0] = hookEvent;
|
||||
$[1] = lookups.inProgressHookCounts;
|
||||
$[2] = toolUseID;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const inProgressHookCount = t1;
|
||||
const resolvedHookCount = lookups.resolvedHookCounts.get(toolUseID)?.get(hookEvent) ?? 0;
|
||||
if (inProgressHookCount === 0) {
|
||||
return null;
|
||||
}
|
||||
if (hookEvent === "PreToolUse" || hookEvent === "PostToolUse") {
|
||||
if (isTranscriptMode) {
|
||||
let t2;
|
||||
if ($[4] !== inProgressHookCount) {
|
||||
t2 = <Text dimColor={true}>{inProgressHookCount} </Text>;
|
||||
$[4] = inProgressHookCount;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== hookEvent) {
|
||||
t3 = <Text dimColor={true} bold={true}>{hookEvent}</Text>;
|
||||
$[6] = hookEvent;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
const t4 = inProgressHookCount === 1 ? " hook" : " hooks";
|
||||
let t5;
|
||||
if ($[8] !== t4) {
|
||||
t5 = <Text dimColor={true}>{t4} ran</Text>;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
let t6;
|
||||
if ($[10] !== t2 || $[11] !== t3 || $[12] !== t5) {
|
||||
t6 = <MessageResponse><Box flexDirection="row">{t2}{t3}{t5}</Box></MessageResponse>;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
$[12] = t5;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (resolvedHookCount === inProgressHookCount) {
|
||||
return null;
|
||||
}
|
||||
let t2;
|
||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text dimColor={true}>Running </Text>;
|
||||
$[14] = t2;
|
||||
} else {
|
||||
t2 = $[14];
|
||||
}
|
||||
let t3;
|
||||
if ($[15] !== hookEvent) {
|
||||
t3 = <Text dimColor={true} bold={true}>{hookEvent}</Text>;
|
||||
$[15] = hookEvent;
|
||||
$[16] = t3;
|
||||
} else {
|
||||
t3 = $[16];
|
||||
}
|
||||
const t4 = inProgressHookCount === 1 ? " hook\u2026" : " hooks\u2026";
|
||||
let t5;
|
||||
if ($[17] !== t4) {
|
||||
t5 = <Text dimColor={true}>{t4}</Text>;
|
||||
$[17] = t4;
|
||||
$[18] = t5;
|
||||
} else {
|
||||
t5 = $[18];
|
||||
}
|
||||
let t6;
|
||||
if ($[19] !== t3 || $[20] !== t5) {
|
||||
t6 = <MessageResponse><Box flexDirection="row">{t2}{t3}{t5}</Box></MessageResponse>;
|
||||
$[19] = t3;
|
||||
$[20] = t5;
|
||||
$[21] = t6;
|
||||
} else {
|
||||
t6 = $[21];
|
||||
}
|
||||
return t6;
|
||||
hookEvent: HookEvent
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
toolUseID: string
|
||||
verbose: boolean
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function HookProgressMessage({
|
||||
hookEvent,
|
||||
lookups,
|
||||
toolUseID,
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const inProgressHookCount =
|
||||
lookups.inProgressHookCounts.get(toolUseID)?.get(hookEvent) ?? 0
|
||||
const resolvedHookCount =
|
||||
lookups.resolvedHookCounts.get(toolUseID)?.get(hookEvent) ?? 0
|
||||
if (inProgressHookCount === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (hookEvent === 'PreToolUse' || hookEvent === 'PostToolUse') {
|
||||
// In transcript mode, show a static summary since messages never re-render
|
||||
// (so a transient "Running..." would get stuck).
|
||||
if (isTranscriptMode) {
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor>{inProgressHookCount} </Text>
|
||||
<Text dimColor bold>
|
||||
{hookEvent}
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{inProgressHookCount === 1 ? ' hook' : ' hooks'} ran
|
||||
</Text>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
// Outside transcript mode, hide — completion info is shown via
|
||||
// async_hook_response attachments instead.
|
||||
return null
|
||||
}
|
||||
|
||||
if (resolvedHookCount === inProgressHookCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor>Running </Text>
|
||||
<Text dimColor bold>
|
||||
{hookEvent}
|
||||
</Text>
|
||||
<Text dimColor>{inProgressHookCount === 1 ? ' hook…' : ' hooks…'}</Text>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,149 +1,158 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Markdown } from '../../components/Markdown.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { jsonParse } from '../../utils/slowOperations.js';
|
||||
import { type IdleNotificationMessage, isIdleNotification, isPlanApprovalRequest, isPlanApprovalResponse, type PlanApprovalRequestMessage, type PlanApprovalResponseMessage } from '../../utils/teammateMailbox.js';
|
||||
import { getShutdownMessageSummary } from './ShutdownMessage.js';
|
||||
import { getTaskAssignmentSummary } from './TaskAssignmentMessage.js';
|
||||
import * as React from 'react'
|
||||
import { Markdown } from '../../components/Markdown.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { jsonParse } from '../../utils/slowOperations.js'
|
||||
import {
|
||||
type IdleNotificationMessage,
|
||||
isIdleNotification,
|
||||
isPlanApprovalRequest,
|
||||
isPlanApprovalResponse,
|
||||
type PlanApprovalRequestMessage,
|
||||
type PlanApprovalResponseMessage,
|
||||
} from '../../utils/teammateMailbox.js'
|
||||
import { getShutdownMessageSummary } from './ShutdownMessage.js'
|
||||
import { getTaskAssignmentSummary } from './TaskAssignmentMessage.js'
|
||||
|
||||
type PlanApprovalRequestProps = {
|
||||
request: PlanApprovalRequestMessage;
|
||||
};
|
||||
request: PlanApprovalRequestMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a plan approval request with a planMode-colored border,
|
||||
* showing the plan content and instructions for approving/rejecting.
|
||||
*/
|
||||
export function PlanApprovalRequestDisplay(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
request
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== request.from) {
|
||||
t1 = <Box marginBottom={1}><Text color="planMode" bold={true}>Plan Approval Request from {request.from}</Text></Box>;
|
||||
$[0] = request.from;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== request.planContent) {
|
||||
t2 = <Box borderStyle="dashed" borderColor="subtle" borderLeft={false} borderRight={false} flexDirection="column" paddingX={1} marginBottom={1}><Markdown>{request.planContent}</Markdown></Box>;
|
||||
$[2] = request.planContent;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== request.planFilePath) {
|
||||
t3 = <Text dimColor={true}>Plan file: {request.planFilePath}</Text>;
|
||||
$[4] = request.planFilePath;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== t1 || $[7] !== t2 || $[8] !== t3) {
|
||||
t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="planMode" flexDirection="column" paddingX={1}>{t1}{t2}{t3}</Box></Box>;
|
||||
$[6] = t1;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
return t4;
|
||||
export function PlanApprovalRequestDisplay({
|
||||
request,
|
||||
}: PlanApprovalRequestProps): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="planMode"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="planMode" bold>
|
||||
Plan Approval Request from {request.from}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
borderStyle="dashed"
|
||||
borderColor="subtle"
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
marginBottom={1}
|
||||
>
|
||||
<Markdown>{request.planContent}</Markdown>
|
||||
</Box>
|
||||
<Text dimColor>Plan file: {request.planFilePath}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type PlanApprovalResponseProps = {
|
||||
response: PlanApprovalResponseMessage;
|
||||
senderName: string;
|
||||
};
|
||||
response: PlanApprovalResponseMessage
|
||||
senderName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a plan approval response with a success (green) or error (red) border.
|
||||
*/
|
||||
export function PlanApprovalResponseDisplay(t0) {
|
||||
const $ = _c(13);
|
||||
const {
|
||||
response,
|
||||
senderName
|
||||
} = t0;
|
||||
export function PlanApprovalResponseDisplay({
|
||||
response,
|
||||
senderName,
|
||||
}: PlanApprovalResponseProps): React.ReactNode {
|
||||
if (response.approved) {
|
||||
let t1;
|
||||
if ($[0] !== senderName) {
|
||||
t1 = <Box><Text color="success" bold={true}>✓ Plan Approved by {senderName}</Text></Box>;
|
||||
$[0] = senderName;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box marginTop={1}><Text>You can now proceed with implementation. Your plan mode restrictions have been lifted.</Text></Box>;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== t1) {
|
||||
t3 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="success" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}</Box></Box>;
|
||||
$[3] = t1;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="success"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
>
|
||||
<Box>
|
||||
<Text color="success" bold>
|
||||
✓ Plan Approved by {senderName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
You can now proceed with implementation. Your plan mode
|
||||
restrictions have been lifted.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t1;
|
||||
if ($[5] !== senderName) {
|
||||
t1 = <Box><Text color="error" bold={true}>✗ Plan Rejected by {senderName}</Text></Box>;
|
||||
$[5] = senderName;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
let t2;
|
||||
if ($[7] !== response.feedback) {
|
||||
t2 = response.feedback && <Box marginTop={1} borderStyle="dashed" borderColor="subtle" borderLeft={false} borderRight={false} paddingX={1}><Text>Feedback: {response.feedback}</Text></Box>;
|
||||
$[7] = response.feedback;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
let t3;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box marginTop={1}><Text dimColor={true}>Please revise your plan based on the feedback and call ExitPlanMode again.</Text></Box>;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
let t4;
|
||||
if ($[10] !== t1 || $[11] !== t2) {
|
||||
t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="error" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}{t3}</Box></Box>;
|
||||
$[10] = t1;
|
||||
$[11] = t2;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
return t4;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="error"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
>
|
||||
<Box>
|
||||
<Text color="error" bold>
|
||||
✗ Plan Rejected by {senderName}
|
||||
</Text>
|
||||
</Box>
|
||||
{response.feedback && (
|
||||
<Box
|
||||
marginTop={1}
|
||||
borderStyle="dashed"
|
||||
borderColor="subtle"
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
paddingX={1}
|
||||
>
|
||||
<Text>Feedback: {response.feedback}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Please revise your plan based on the feedback and call ExitPlanMode
|
||||
again.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse and render a plan approval message from raw content.
|
||||
* Returns the rendered component if it's a plan approval message, null otherwise.
|
||||
*/
|
||||
export function tryRenderPlanApprovalMessage(content: string, senderName: string): React.ReactNode | null {
|
||||
const request = isPlanApprovalRequest(content);
|
||||
export function tryRenderPlanApprovalMessage(
|
||||
content: string,
|
||||
senderName: string,
|
||||
): React.ReactNode | null {
|
||||
const request = isPlanApprovalRequest(content)
|
||||
if (request) {
|
||||
return <PlanApprovalRequestDisplay request={request} />;
|
||||
return <PlanApprovalRequestDisplay request={request} />
|
||||
}
|
||||
const response = isPlanApprovalResponse(content);
|
||||
|
||||
const response = isPlanApprovalResponse(content)
|
||||
if (response) {
|
||||
return <PlanApprovalResponseDisplay response={response} senderName={senderName} />;
|
||||
return (
|
||||
<PlanApprovalResponseDisplay
|
||||
response={response}
|
||||
senderName={senderName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,34 +161,36 @@ export function tryRenderPlanApprovalMessage(content: string, senderName: string
|
||||
* Returns null if the content is not a plan approval message.
|
||||
*/
|
||||
function getPlanApprovalSummary(content: string): string | null {
|
||||
const request = isPlanApprovalRequest(content);
|
||||
const request = isPlanApprovalRequest(content)
|
||||
if (request) {
|
||||
return `[Plan Approval Request from ${request.from}]`;
|
||||
return `[Plan Approval Request from ${request.from}]`
|
||||
}
|
||||
const response = isPlanApprovalResponse(content);
|
||||
|
||||
const response = isPlanApprovalResponse(content)
|
||||
if (response) {
|
||||
if (response.approved) {
|
||||
return '[Plan Approved] You can now proceed with implementation';
|
||||
return '[Plan Approved] You can now proceed with implementation'
|
||||
} else {
|
||||
return `[Plan Rejected] ${response.feedback || 'Please revise your plan'}`;
|
||||
return `[Plan Rejected] ${response.feedback || 'Please revise your plan'}`
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a brief summary text for an idle notification.
|
||||
*/
|
||||
function getIdleNotificationSummary(msg: IdleNotificationMessage): string {
|
||||
const parts: string[] = ['Agent idle'];
|
||||
const parts: string[] = ['Agent idle']
|
||||
if (msg.completedTaskId) {
|
||||
const status = msg.completedStatus || 'completed';
|
||||
parts.push(`Task ${msg.completedTaskId} ${status}`);
|
||||
const status = msg.completedStatus || 'completed'
|
||||
parts.push(`Task ${msg.completedTaskId} ${status}`)
|
||||
}
|
||||
if (msg.summary) {
|
||||
parts.push(`Last DM: ${msg.summary}`);
|
||||
parts.push(`Last DM: ${msg.summary}`)
|
||||
}
|
||||
return parts.join(' · ');
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,34 +199,35 @@ function getIdleNotificationSummary(msg: IdleNotificationMessage): string {
|
||||
* Otherwise returns the original content.
|
||||
*/
|
||||
export function formatTeammateMessageContent(content: string): string {
|
||||
const planSummary = getPlanApprovalSummary(content);
|
||||
const planSummary = getPlanApprovalSummary(content)
|
||||
if (planSummary) {
|
||||
return planSummary;
|
||||
return planSummary
|
||||
}
|
||||
const shutdownSummary = getShutdownMessageSummary(content);
|
||||
|
||||
const shutdownSummary = getShutdownMessageSummary(content)
|
||||
if (shutdownSummary) {
|
||||
return shutdownSummary;
|
||||
return shutdownSummary
|
||||
}
|
||||
const idleMsg = isIdleNotification(content);
|
||||
|
||||
const idleMsg = isIdleNotification(content)
|
||||
if (idleMsg) {
|
||||
return getIdleNotificationSummary(idleMsg);
|
||||
return getIdleNotificationSummary(idleMsg)
|
||||
}
|
||||
const taskAssignmentSummary = getTaskAssignmentSummary(content);
|
||||
|
||||
const taskAssignmentSummary = getTaskAssignmentSummary(content)
|
||||
if (taskAssignmentSummary) {
|
||||
return taskAssignmentSummary;
|
||||
return taskAssignmentSummary
|
||||
}
|
||||
|
||||
// Check for teammate_terminated message
|
||||
try {
|
||||
const parsed = jsonParse(content) as {
|
||||
type?: string;
|
||||
message?: string;
|
||||
};
|
||||
const parsed = jsonParse(content) as { type?: string; message?: string }
|
||||
if (parsed?.type === 'teammate_terminated' && parsed.message) {
|
||||
return parsed.message;
|
||||
return parsed.message
|
||||
}
|
||||
} catch {
|
||||
// Not JSON
|
||||
}
|
||||
return content;
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -1,160 +1,131 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { extraUsage } from 'src/commands/extra-usage/index.js';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import { useClaudeAiLimits } from 'src/services/claudeAiLimitsHook.js';
|
||||
import { shouldProcessMockLimits } from 'src/services/rateLimitMocking.js'; // Used for /mock-limits command
|
||||
import { getRateLimitTier, getSubscriptionType, isClaudeAISubscriber } from 'src/utils/auth.js';
|
||||
import { hasClaudeAiBillingAccess } from 'src/utils/billing.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { extraUsage } from 'src/commands/extra-usage/index.js'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
import { useClaudeAiLimits } from 'src/services/claudeAiLimitsHook.js'
|
||||
import { shouldProcessMockLimits } from 'src/services/rateLimitMocking.js' // Used for /mock-limits command
|
||||
import {
|
||||
getRateLimitTier,
|
||||
getSubscriptionType,
|
||||
isClaudeAISubscriber,
|
||||
} from 'src/utils/auth.js'
|
||||
import { hasClaudeAiBillingAccess } from 'src/utils/billing.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
type UpsellParams = {
|
||||
shouldShowUpsell: boolean;
|
||||
isMax20x: boolean;
|
||||
isExtraUsageCommandEnabled: boolean;
|
||||
shouldAutoOpenRateLimitOptionsMenu: boolean;
|
||||
isTeamOrEnterprise: boolean;
|
||||
hasBillingAccess: boolean;
|
||||
};
|
||||
shouldShowUpsell: boolean
|
||||
isMax20x: boolean
|
||||
isExtraUsageCommandEnabled: boolean
|
||||
shouldAutoOpenRateLimitOptionsMenu: boolean
|
||||
isTeamOrEnterprise: boolean
|
||||
hasBillingAccess: boolean
|
||||
}
|
||||
|
||||
export function getUpsellMessage({
|
||||
shouldShowUpsell,
|
||||
isMax20x,
|
||||
isExtraUsageCommandEnabled,
|
||||
shouldAutoOpenRateLimitOptionsMenu,
|
||||
isTeamOrEnterprise,
|
||||
hasBillingAccess
|
||||
hasBillingAccess,
|
||||
}: UpsellParams): string | null {
|
||||
if (!shouldShowUpsell) return null;
|
||||
if (!shouldShowUpsell) return null
|
||||
|
||||
if (isMax20x) {
|
||||
if (isExtraUsageCommandEnabled) {
|
||||
return '/extra-usage to finish what you\u2019re working on.';
|
||||
return '/extra-usage to finish what you\u2019re working on.'
|
||||
}
|
||||
return '/login to switch to an API usage-billed account.';
|
||||
return '/login to switch to an API usage-billed account.'
|
||||
}
|
||||
|
||||
if (shouldAutoOpenRateLimitOptionsMenu) {
|
||||
return 'Opening your options\u2026';
|
||||
return 'Opening your options\u2026'
|
||||
}
|
||||
|
||||
if (!isTeamOrEnterprise && !isExtraUsageCommandEnabled) {
|
||||
return '/upgrade to increase your usage limit.';
|
||||
return '/upgrade to increase your usage limit.'
|
||||
}
|
||||
|
||||
if (isTeamOrEnterprise) {
|
||||
if (!isExtraUsageCommandEnabled) return null;
|
||||
if (!isExtraUsageCommandEnabled) return null
|
||||
|
||||
if (hasBillingAccess) {
|
||||
return '/extra-usage to finish what you\u2019re working on.';
|
||||
return '/extra-usage to finish what you\u2019re working on.'
|
||||
}
|
||||
return '/extra-usage to request more usage from your admin.';
|
||||
|
||||
return '/extra-usage to request more usage from your admin.'
|
||||
}
|
||||
return '/upgrade or /extra-usage to finish what you\u2019re working on.';
|
||||
|
||||
return '/upgrade or /extra-usage to finish what you\u2019re working on.'
|
||||
}
|
||||
|
||||
type RateLimitMessageProps = {
|
||||
text: string;
|
||||
onOpenRateLimitOptions?: () => void;
|
||||
};
|
||||
export function RateLimitMessage(t0) {
|
||||
const $ = _c(16);
|
||||
const {
|
||||
text,
|
||||
onOpenRateLimitOptions
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getSubscriptionType();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const subscriptionType = t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = getRateLimitTier();
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const rateLimitTier = t2;
|
||||
const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise";
|
||||
const isMax20x = rateLimitTier === "default_claude_max_20x";
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = shouldProcessMockLimits() || isClaudeAISubscriber();
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
const shouldShowUpsell = t3;
|
||||
const canSeeRateLimitOptionsUpsell = shouldShowUpsell && !isMax20x;
|
||||
const [hasOpenedInteractiveMenu, setHasOpenedInteractiveMenu] = useState(false);
|
||||
const claudeAiLimits = useClaudeAiLimits();
|
||||
const isCurrentlyRateLimited = claudeAiLimits.status === "rejected" && claudeAiLimits.resetsAt !== undefined && !claudeAiLimits.isUsingOverage;
|
||||
const shouldAutoOpenRateLimitOptionsMenu = canSeeRateLimitOptionsUpsell && !hasOpenedInteractiveMenu && isCurrentlyRateLimited && onOpenRateLimitOptions;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[3] !== onOpenRateLimitOptions || $[4] !== shouldAutoOpenRateLimitOptionsMenu) {
|
||||
t4 = () => {
|
||||
if (shouldAutoOpenRateLimitOptionsMenu) {
|
||||
setHasOpenedInteractiveMenu(true);
|
||||
onOpenRateLimitOptions();
|
||||
}
|
||||
};
|
||||
t5 = [shouldAutoOpenRateLimitOptionsMenu, onOpenRateLimitOptions];
|
||||
$[3] = onOpenRateLimitOptions;
|
||||
$[4] = shouldAutoOpenRateLimitOptionsMenu;
|
||||
$[5] = t4;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
t5 = $[6];
|
||||
}
|
||||
useEffect(t4, t5);
|
||||
let t6;
|
||||
bb0: {
|
||||
let t7;
|
||||
if ($[7] !== shouldAutoOpenRateLimitOptionsMenu) {
|
||||
t7 = getUpsellMessage({
|
||||
shouldShowUpsell,
|
||||
isMax20x,
|
||||
isExtraUsageCommandEnabled: extraUsage.isEnabled(),
|
||||
shouldAutoOpenRateLimitOptionsMenu: !!shouldAutoOpenRateLimitOptionsMenu,
|
||||
isTeamOrEnterprise,
|
||||
hasBillingAccess: hasClaudeAiBillingAccess()
|
||||
});
|
||||
$[7] = shouldAutoOpenRateLimitOptionsMenu;
|
||||
$[8] = t7;
|
||||
} else {
|
||||
t7 = $[8];
|
||||
}
|
||||
const message = t7;
|
||||
if (!message) {
|
||||
t6 = null;
|
||||
break bb0;
|
||||
}
|
||||
let t8;
|
||||
if ($[9] !== message) {
|
||||
t8 = <Text dimColor={true}>{message}</Text>;
|
||||
$[9] = message;
|
||||
$[10] = t8;
|
||||
} else {
|
||||
t8 = $[10];
|
||||
}
|
||||
t6 = t8;
|
||||
}
|
||||
const upsell = t6;
|
||||
let t7;
|
||||
if ($[11] !== text) {
|
||||
t7 = <Text color="error">{text}</Text>;
|
||||
$[11] = text;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
const t8 = hasOpenedInteractiveMenu ? null : upsell;
|
||||
let t9;
|
||||
if ($[13] !== t7 || $[14] !== t8) {
|
||||
t9 = <MessageResponse><Box flexDirection="column">{t7}{t8}</Box></MessageResponse>;
|
||||
$[13] = t7;
|
||||
$[14] = t8;
|
||||
$[15] = t9;
|
||||
} else {
|
||||
t9 = $[15];
|
||||
}
|
||||
return t9;
|
||||
text: string
|
||||
onOpenRateLimitOptions?: () => void
|
||||
}
|
||||
|
||||
export function RateLimitMessage({
|
||||
text,
|
||||
onOpenRateLimitOptions,
|
||||
}: RateLimitMessageProps): React.ReactNode {
|
||||
const subscriptionType = getSubscriptionType()
|
||||
const rateLimitTier = getRateLimitTier()
|
||||
const isTeamOrEnterprise =
|
||||
subscriptionType === 'team' || subscriptionType === 'enterprise'
|
||||
const isMax20x = rateLimitTier === 'default_claude_max_20x'
|
||||
// Always show upsell when using /mock-limits command, otherwise show for subscribers
|
||||
const shouldShowUpsell = shouldProcessMockLimits() || isClaudeAISubscriber()
|
||||
|
||||
const canSeeRateLimitOptionsUpsell = shouldShowUpsell && !isMax20x
|
||||
|
||||
const [hasOpenedInteractiveMenu, setHasOpenedInteractiveMenu] =
|
||||
useState(false)
|
||||
|
||||
// Check actual rate limit status - only auto-open if user is currently rate limited
|
||||
// AND we've verified this with the API (resetsAt is only set after API response).
|
||||
// This prevents false alerts when resuming sessions with old rate limit messages.
|
||||
const claudeAiLimits = useClaudeAiLimits()
|
||||
const isCurrentlyRateLimited =
|
||||
claudeAiLimits.status === 'rejected' &&
|
||||
claudeAiLimits.resetsAt !== undefined &&
|
||||
!claudeAiLimits.isUsingOverage
|
||||
|
||||
const shouldAutoOpenRateLimitOptionsMenu =
|
||||
canSeeRateLimitOptionsUpsell &&
|
||||
!hasOpenedInteractiveMenu &&
|
||||
isCurrentlyRateLimited &&
|
||||
onOpenRateLimitOptions
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoOpenRateLimitOptionsMenu) {
|
||||
setHasOpenedInteractiveMenu(true)
|
||||
onOpenRateLimitOptions()
|
||||
}
|
||||
}, [shouldAutoOpenRateLimitOptionsMenu, onOpenRateLimitOptions])
|
||||
|
||||
const upsell = useMemo(() => {
|
||||
const message = getUpsellMessage({
|
||||
shouldShowUpsell,
|
||||
isMax20x,
|
||||
isExtraUsageCommandEnabled: extraUsage.isEnabled(),
|
||||
shouldAutoOpenRateLimitOptionsMenu: !!shouldAutoOpenRateLimitOptionsMenu,
|
||||
isTeamOrEnterprise,
|
||||
hasBillingAccess: hasClaudeAiBillingAccess(),
|
||||
})
|
||||
if (!message) return null
|
||||
return <Text dimColor>{message}</Text>
|
||||
}, [
|
||||
shouldShowUpsell,
|
||||
isMax20x,
|
||||
isTeamOrEnterprise,
|
||||
shouldAutoOpenRateLimitOptionsMenu,
|
||||
])
|
||||
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">{text}</Text>
|
||||
{hasOpenedInteractiveMenu ? null : upsell}
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,112 +1,113 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { isShutdownApproved, isShutdownRejected, isShutdownRequest, type ShutdownRejectedMessage, type ShutdownRequestMessage } from '../../utils/teammateMailbox.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
isShutdownApproved,
|
||||
isShutdownRejected,
|
||||
isShutdownRequest,
|
||||
type ShutdownRejectedMessage,
|
||||
type ShutdownRequestMessage,
|
||||
} from '../../utils/teammateMailbox.js'
|
||||
|
||||
type ShutdownRequestProps = {
|
||||
request: ShutdownRequestMessage;
|
||||
};
|
||||
request: ShutdownRequestMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a shutdown request with a warning-colored border.
|
||||
*/
|
||||
export function ShutdownRequestDisplay(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
request
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== request.from) {
|
||||
t1 = <Box marginBottom={1}><Text color="warning" bold={true}>Shutdown request from {request.from}</Text></Box>;
|
||||
$[0] = request.from;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== request.reason) {
|
||||
t2 = request.reason && <Box><Text>Reason: {request.reason}</Text></Box>;
|
||||
$[2] = request.reason;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== t1 || $[5] !== t2) {
|
||||
t3 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="warning" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}</Box></Box>;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
export function ShutdownRequestDisplay({
|
||||
request,
|
||||
}: ShutdownRequestProps): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="warning"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="warning" bold>
|
||||
Shutdown request from {request.from}
|
||||
</Text>
|
||||
</Box>
|
||||
{request.reason && (
|
||||
<Box>
|
||||
<Text>Reason: {request.reason}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type ShutdownRejectedProps = {
|
||||
response: ShutdownRejectedMessage;
|
||||
};
|
||||
response: ShutdownRejectedMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a shutdown rejected message with a subtle (grey) border.
|
||||
*/
|
||||
export function ShutdownRejectedDisplay(t0) {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
response
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== response.from) {
|
||||
t1 = <Text color="subtle" bold={true}>Shutdown rejected by {response.from}</Text>;
|
||||
$[0] = response.from;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== response.reason) {
|
||||
t2 = <Box marginTop={1} borderStyle="dashed" borderColor="subtle" borderLeft={false} borderRight={false} paddingX={1}><Text>Reason: {response.reason}</Text></Box>;
|
||||
$[2] = response.reason;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box marginTop={1}><Text dimColor={true}>Teammate is continuing to work. You may request shutdown again later.</Text></Box>;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== t1 || $[6] !== t2) {
|
||||
t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="subtle" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}{t3}</Box></Box>;
|
||||
$[5] = t1;
|
||||
$[6] = t2;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
return t4;
|
||||
export function ShutdownRejectedDisplay({
|
||||
response,
|
||||
}: ShutdownRejectedProps): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="subtle"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
>
|
||||
<Text color="subtle" bold>
|
||||
Shutdown rejected by {response.from}
|
||||
</Text>
|
||||
<Box
|
||||
marginTop={1}
|
||||
borderStyle="dashed"
|
||||
borderColor="subtle"
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
paddingX={1}
|
||||
>
|
||||
<Text>Reason: {response.reason}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Teammate is continuing to work. You may request shutdown again
|
||||
later.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse and render a shutdown message from raw content.
|
||||
* Returns the rendered component if it's a shutdown message, null otherwise.
|
||||
*/
|
||||
export function tryRenderShutdownMessage(content: string): React.ReactNode | null {
|
||||
const request = isShutdownRequest(content);
|
||||
export function tryRenderShutdownMessage(
|
||||
content: string,
|
||||
): React.ReactNode | null {
|
||||
const request = isShutdownRequest(content)
|
||||
if (request) {
|
||||
return <ShutdownRequestDisplay request={request} />;
|
||||
return <ShutdownRequestDisplay request={request} />
|
||||
}
|
||||
|
||||
// Shutdown approved is handled inline by the caller — skip it here
|
||||
if (isShutdownApproved(content)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const rejected = isShutdownRejected(content);
|
||||
|
||||
const rejected = isShutdownRejected(content)
|
||||
if (rejected) {
|
||||
return <ShutdownRejectedDisplay response={rejected} />;
|
||||
return <ShutdownRejectedDisplay response={rejected} />
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,17 +116,20 @@ export function tryRenderShutdownMessage(content: string): React.ReactNode | nul
|
||||
* Returns null if the content is not a shutdown message.
|
||||
*/
|
||||
export function getShutdownMessageSummary(content: string): string | null {
|
||||
const request = isShutdownRequest(content);
|
||||
const request = isShutdownRequest(content)
|
||||
if (request) {
|
||||
return `[Shutdown Request from ${request.from}]${request.reason ? ` ${request.reason}` : ''}`;
|
||||
return `[Shutdown Request from ${request.from}]${request.reason ? ` ${request.reason}` : ''}`
|
||||
}
|
||||
const approved = isShutdownApproved(content);
|
||||
|
||||
const approved = isShutdownApproved(content)
|
||||
if (approved) {
|
||||
return `[Shutdown Approved] ${approved.from} is now exiting`;
|
||||
return `[Shutdown Approved] ${approved.from} is now exiting`
|
||||
}
|
||||
const rejected = isShutdownRejected(content);
|
||||
|
||||
const rejected = isShutdownRejected(content)
|
||||
if (rejected) {
|
||||
return `[Shutdown Rejected] ${rejected.from}: ${rejected.reason}`;
|
||||
return `[Shutdown Rejected] ${rejected.from}: ${rejected.reason}`
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,140 +1,64 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import { formatAPIError } from 'src/services/api/errorUtils.js';
|
||||
import type { SystemAPIErrorMessage } from 'src/types/message.js';
|
||||
import { useInterval } from 'usehooks-ts';
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
const MAX_API_ERROR_CHARS = 1000;
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
import { formatAPIError } from 'src/services/api/errorUtils.js'
|
||||
import type { SystemAPIErrorMessage } from 'src/types/message.js'
|
||||
import { useInterval } from 'usehooks-ts'
|
||||
import { CtrlOToExpand } from '../CtrlOToExpand.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
const MAX_API_ERROR_CHARS = 1000
|
||||
|
||||
type Props = {
|
||||
message: SystemAPIErrorMessage;
|
||||
verbose: boolean;
|
||||
};
|
||||
export function SystemAPIErrorMessage(t0) {
|
||||
const $ = _c(33);
|
||||
const {
|
||||
message: t1,
|
||||
verbose
|
||||
} = t0;
|
||||
const {
|
||||
retryAttempt,
|
||||
error,
|
||||
retryInMs,
|
||||
maxRetries
|
||||
} = t1;
|
||||
const hidden = true && retryAttempt < 4;
|
||||
const [countdownMs, setCountdownMs] = useState(0);
|
||||
const done = countdownMs >= retryInMs;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => setCountdownMs(_temp);
|
||||
$[0] = t2;
|
||||
} else {
|
||||
t2 = $[0];
|
||||
}
|
||||
useInterval(t2, hidden || done ? null : 1000);
|
||||
message: SystemAPIErrorMessage
|
||||
verbose: boolean
|
||||
}
|
||||
|
||||
export function SystemAPIErrorMessage({
|
||||
message: { retryAttempt, error, retryInMs, maxRetries },
|
||||
verbose,
|
||||
}: Props): React.ReactNode {
|
||||
// Hidden for early retries on external builds to avoid noise. Compute before
|
||||
// useInterval so we never register a timer that just drives a null render.
|
||||
const hidden = process.env.USER_TYPE === 'external' && retryAttempt < 4
|
||||
|
||||
const [countdownMs, setCountdownMs] = useState(0)
|
||||
const done = countdownMs >= retryInMs
|
||||
useInterval(
|
||||
() => setCountdownMs(ms => ms + 1000),
|
||||
hidden || done ? null : 1000,
|
||||
)
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t3;
|
||||
if ($[1] !== countdownMs || $[2] !== retryInMs) {
|
||||
t3 = Math.round((retryInMs - countdownMs) / 1000);
|
||||
$[1] = countdownMs;
|
||||
$[2] = retryInMs;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const retryInSecondsLive = Math.max(0, t3);
|
||||
let T0;
|
||||
let T1;
|
||||
let T2;
|
||||
let t4;
|
||||
let t5;
|
||||
let t6;
|
||||
let truncated;
|
||||
if ($[4] !== error || $[5] !== verbose) {
|
||||
const formatted = formatAPIError(error);
|
||||
truncated = !verbose && formatted.length > MAX_API_ERROR_CHARS;
|
||||
T2 = MessageResponse;
|
||||
T1 = Box;
|
||||
t6 = "column";
|
||||
T0 = Text;
|
||||
t4 = "error";
|
||||
t5 = truncated ? formatted.slice(0, MAX_API_ERROR_CHARS) + "\u2026" : formatted;
|
||||
$[4] = error;
|
||||
$[5] = verbose;
|
||||
$[6] = T0;
|
||||
$[7] = T1;
|
||||
$[8] = T2;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
$[12] = truncated;
|
||||
} else {
|
||||
T0 = $[6];
|
||||
T1 = $[7];
|
||||
T2 = $[8];
|
||||
t4 = $[9];
|
||||
t5 = $[10];
|
||||
t6 = $[11];
|
||||
truncated = $[12];
|
||||
}
|
||||
let t7;
|
||||
if ($[13] !== T0 || $[14] !== t4 || $[15] !== t5) {
|
||||
t7 = <T0 color={t4}>{t5}</T0>;
|
||||
$[13] = T0;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
let t8;
|
||||
if ($[17] !== truncated) {
|
||||
t8 = truncated && <CtrlOToExpand />;
|
||||
$[17] = truncated;
|
||||
$[18] = t8;
|
||||
} else {
|
||||
t8 = $[18];
|
||||
}
|
||||
const t9 = retryInSecondsLive === 1 ? "second" : "seconds";
|
||||
let t10;
|
||||
if ($[19] !== maxRetries || $[20] !== retryAttempt || $[21] !== retryInSecondsLive || $[22] !== t9) {
|
||||
t10 = <Text dimColor={true}>Retrying in {retryInSecondsLive}{" "}{t9}… (attempt{" "}{retryAttempt}/{maxRetries}){process.env.API_TIMEOUT_MS ? ` · API_TIMEOUT_MS=${process.env.API_TIMEOUT_MS}ms, try increasing it` : ""}</Text>;
|
||||
$[19] = maxRetries;
|
||||
$[20] = retryAttempt;
|
||||
$[21] = retryInSecondsLive;
|
||||
$[22] = t9;
|
||||
$[23] = t10;
|
||||
} else {
|
||||
t10 = $[23];
|
||||
}
|
||||
let t11;
|
||||
if ($[24] !== T1 || $[25] !== t10 || $[26] !== t6 || $[27] !== t7 || $[28] !== t8) {
|
||||
t11 = <T1 flexDirection={t6}>{t7}{t8}{t10}</T1>;
|
||||
$[24] = T1;
|
||||
$[25] = t10;
|
||||
$[26] = t6;
|
||||
$[27] = t7;
|
||||
$[28] = t8;
|
||||
$[29] = t11;
|
||||
} else {
|
||||
t11 = $[29];
|
||||
}
|
||||
let t12;
|
||||
if ($[30] !== T2 || $[31] !== t11) {
|
||||
t12 = <T2>{t11}</T2>;
|
||||
$[30] = T2;
|
||||
$[31] = t11;
|
||||
$[32] = t12;
|
||||
} else {
|
||||
t12 = $[32];
|
||||
}
|
||||
return t12;
|
||||
}
|
||||
function _temp(ms) {
|
||||
return ms + 1000;
|
||||
|
||||
const retryInSecondsLive = Math.max(
|
||||
0,
|
||||
Math.round((retryInMs - countdownMs) / 1000),
|
||||
)
|
||||
|
||||
const formatted = formatAPIError(error)
|
||||
const truncated = !verbose && formatted.length > MAX_API_ERROR_CHARS
|
||||
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">
|
||||
{truncated
|
||||
? formatted.slice(0, MAX_API_ERROR_CHARS) + '…'
|
||||
: formatted}
|
||||
</Text>
|
||||
{truncated && <CtrlOToExpand />}
|
||||
<Text dimColor>
|
||||
Retrying in {retryInSecondsLive}{' '}
|
||||
{retryInSecondsLive === 1 ? 'second' : 'seconds'}… (attempt{' '}
|
||||
{retryAttempt}/{maxRetries})
|
||||
{process.env.API_TIMEOUT_MS
|
||||
? ` · API_TIMEOUT_MS=${process.env.API_TIMEOUT_MS}ms, try increasing it`
|
||||
: ''}
|
||||
</Text>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,75 +1,65 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { isTaskAssignment, type TaskAssignmentMessage } from '../../utils/teammateMailbox.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
isTaskAssignment,
|
||||
type TaskAssignmentMessage,
|
||||
} from '../../utils/teammateMailbox.js'
|
||||
|
||||
type Props = {
|
||||
assignment: TaskAssignmentMessage;
|
||||
};
|
||||
assignment: TaskAssignmentMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a task assignment with a cyan border (team-related color).
|
||||
*/
|
||||
export function TaskAssignmentDisplay(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
assignment
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== assignment.assignedBy || $[1] !== assignment.taskId) {
|
||||
t1 = <Box marginBottom={1}><Text color="cyan_FOR_SUBAGENTS_ONLY" bold={true}>Task #{assignment.taskId} assigned by {assignment.assignedBy}</Text></Box>;
|
||||
$[0] = assignment.assignedBy;
|
||||
$[1] = assignment.taskId;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== assignment.subject) {
|
||||
t2 = <Box><Text bold={true}>{assignment.subject}</Text></Box>;
|
||||
$[3] = assignment.subject;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== assignment.description) {
|
||||
t3 = assignment.description && <Box marginTop={1}><Text dimColor={true}>{assignment.description}</Text></Box>;
|
||||
$[5] = assignment.description;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== t1 || $[8] !== t2 || $[9] !== t3) {
|
||||
t4 = <Box flexDirection="column" marginY={1}><Box borderStyle="round" borderColor="cyan_FOR_SUBAGENTS_ONLY" flexDirection="column" paddingX={1} paddingY={1}>{t1}{t2}{t3}</Box></Box>;
|
||||
$[7] = t1;
|
||||
$[8] = t2;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
return t4;
|
||||
export function TaskAssignmentDisplay({ assignment }: Props): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="cyan_FOR_SUBAGENTS_ONLY"
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="cyan_FOR_SUBAGENTS_ONLY" bold>
|
||||
Task #{assignment.taskId} assigned by {assignment.assignedBy}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text bold>{assignment.subject}</Text>
|
||||
</Box>
|
||||
{assignment.description && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>{assignment.description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse and render a task assignment message from raw content.
|
||||
*/
|
||||
export function tryRenderTaskAssignmentMessage(content: string): React.ReactNode | null {
|
||||
const assignment = isTaskAssignment(content);
|
||||
export function tryRenderTaskAssignmentMessage(
|
||||
content: string,
|
||||
): React.ReactNode | null {
|
||||
const assignment = isTaskAssignment(content)
|
||||
if (assignment) {
|
||||
return <TaskAssignmentDisplay assignment={assignment} />;
|
||||
return <TaskAssignmentDisplay assignment={assignment} />
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a brief summary text for a task assignment message.
|
||||
*/
|
||||
export function getTaskAssignmentSummary(content: string): string | null {
|
||||
const assignment = isTaskAssignment(content);
|
||||
const assignment = isTaskAssignment(content)
|
||||
if (assignment) {
|
||||
return `[Task Assigned] #${assignment.taskId} - ${assignment.subject}`;
|
||||
return `[Task Assigned] #${assignment.taskId} - ${assignment.subject}`
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,82 +1,42 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { Box, Text, type TextProps } from '../../ink.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||
import { Box, Text, type TextProps } from '../../ink.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
};
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
}
|
||||
|
||||
function getStatusColor(status: string | null): TextProps['color'] {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'success';
|
||||
return 'success'
|
||||
case 'failed':
|
||||
return 'error';
|
||||
return 'error'
|
||||
case 'killed':
|
||||
return 'warning';
|
||||
return 'warning'
|
||||
default:
|
||||
return 'text';
|
||||
return 'text'
|
||||
}
|
||||
}
|
||||
export function UserAgentNotificationMessage(t0) {
|
||||
const $ = _c(12);
|
||||
const {
|
||||
addMargin,
|
||||
param: t1
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
let t2;
|
||||
if ($[0] !== text) {
|
||||
t2 = extractTag(text, "summary");
|
||||
$[0] = text;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const summary = t2;
|
||||
if (!summary) {
|
||||
return null;
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== text) {
|
||||
const status = extractTag(text, "status");
|
||||
t3 = getStatusColor(status);
|
||||
$[2] = text;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const color = t3;
|
||||
const t4 = addMargin ? 1 : 0;
|
||||
let t5;
|
||||
if ($[4] !== color) {
|
||||
t5 = <Text color={color}>{BLACK_CIRCLE}</Text>;
|
||||
$[4] = color;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
let t6;
|
||||
if ($[6] !== summary || $[7] !== t5) {
|
||||
t6 = <Text>{t5} {summary}</Text>;
|
||||
$[6] = summary;
|
||||
$[7] = t5;
|
||||
$[8] = t6;
|
||||
} else {
|
||||
t6 = $[8];
|
||||
}
|
||||
let t7;
|
||||
if ($[9] !== t4 || $[10] !== t6) {
|
||||
t7 = <Box marginTop={t4}>{t6}</Box>;
|
||||
$[9] = t4;
|
||||
$[10] = t6;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t7 = $[11];
|
||||
}
|
||||
return t7;
|
||||
|
||||
export function UserAgentNotificationMessage({
|
||||
addMargin,
|
||||
param: { text },
|
||||
}: Props): React.ReactNode {
|
||||
const summary = extractTag(text, 'summary')
|
||||
if (!summary) return null
|
||||
|
||||
const status = extractTag(text, 'status')
|
||||
const color = getStatusColor(status)
|
||||
|
||||
return (
|
||||
<Box marginTop={addMargin ? 1 : 0}>
|
||||
<Text>
|
||||
<Text color={color}>{BLACK_CIRCLE}</Text> {summary}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,57 +1,30 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
};
|
||||
export function UserBashInputMessage(t0) {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
param: t1,
|
||||
addMargin
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
let t2;
|
||||
if ($[0] !== text) {
|
||||
t2 = extractTag(text, "bash-input");
|
||||
$[0] = text;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const input = t2;
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
const t3 = addMargin ? 1 : 0;
|
||||
let t4;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text color="bashBorder">! </Text>;
|
||||
$[2] = t4;
|
||||
} else {
|
||||
t4 = $[2];
|
||||
}
|
||||
let t5;
|
||||
if ($[3] !== input) {
|
||||
t5 = <Text color="text">{input}</Text>;
|
||||
$[3] = input;
|
||||
$[4] = t5;
|
||||
} else {
|
||||
t5 = $[4];
|
||||
}
|
||||
let t6;
|
||||
if ($[5] !== t3 || $[6] !== t5) {
|
||||
t6 = <Box flexDirection="row" marginTop={t3} backgroundColor="bashMessageBackgroundColor" paddingRight={1}>{t4}{t5}</Box>;
|
||||
$[5] = t3;
|
||||
$[6] = t5;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t6 = $[7];
|
||||
}
|
||||
return t6;
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
}
|
||||
|
||||
export function UserBashInputMessage({
|
||||
param: { text },
|
||||
addMargin,
|
||||
}: Props): React.ReactNode {
|
||||
const input = extractTag(text, 'bash-input')
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
backgroundColor="bashMessageBackgroundColor"
|
||||
paddingRight={1}
|
||||
>
|
||||
<Text color="bashBorder">! </Text>
|
||||
<Text color="text">{input}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,53 +1,20 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import BashToolResultMessage from '../../tools/BashTool/BashToolResultMessage.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
export function UserBashOutputMessage(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
content,
|
||||
verbose
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== content) {
|
||||
const rawStdout = extractTag(content, "bash-stdout") ?? "";
|
||||
t1 = extractTag(rawStdout, "persisted-output") ?? rawStdout;
|
||||
$[0] = content;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const stdout = t1;
|
||||
let t2;
|
||||
if ($[2] !== content) {
|
||||
t2 = extractTag(content, "bash-stderr") ?? "";
|
||||
$[2] = content;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const stderr = t2;
|
||||
let t3;
|
||||
if ($[4] !== stderr || $[5] !== stdout) {
|
||||
t3 = {
|
||||
stdout,
|
||||
stderr
|
||||
};
|
||||
$[4] = stderr;
|
||||
$[5] = stdout;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
const t4 = !!verbose;
|
||||
let t5;
|
||||
if ($[7] !== t3 || $[8] !== t4) {
|
||||
t5 = <BashToolResultMessage content={t3} verbose={t4} />;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
return t5;
|
||||
import * as React from 'react'
|
||||
import BashToolResultMessage from '../../tools/BashTool/BashToolResultMessage.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
|
||||
export function UserBashOutputMessage({
|
||||
content,
|
||||
verbose,
|
||||
}: {
|
||||
content: string
|
||||
verbose?: boolean
|
||||
}): React.ReactNode {
|
||||
const rawStdout = extractTag(content, 'bash-stdout') ?? ''
|
||||
// Unwrap <persisted-output> if present — keep the inner content (file path +
|
||||
// preview) for the user; the wrapper tag itself is model-facing signaling.
|
||||
const stdout = extractTag(rawStdout, 'persisted-output') ?? rawStdout
|
||||
const stderr = extractTag(content, 'bash-stderr') ?? ''
|
||||
return (
|
||||
<BashToolResultMessage content={{ stdout, stderr }} verbose={!!verbose} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,136 +1,52 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { CHANNEL_ARROW } from '../../constants/figures.js';
|
||||
import { CHANNEL_TAG } from '../../constants/xml.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { truncateToWidth } from '../../utils/format.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { CHANNEL_ARROW } from '../../constants/figures.js'
|
||||
import { CHANNEL_TAG } from '../../constants/xml.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { truncateToWidth } from '../../utils/format.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
};
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
}
|
||||
|
||||
// <channel source="..." user="..." chat_id="...">content</channel>
|
||||
// source is always first (wrapChannelMessage writes it), user is optional.
|
||||
const CHANNEL_RE = new RegExp(`<${CHANNEL_TAG}\\s+source="([^"]+)"([^>]*)>\\n?([\\s\\S]*?)\\n?</${CHANNEL_TAG}>`);
|
||||
const USER_ATTR_RE = /\buser="([^"]+)"/;
|
||||
const CHANNEL_RE = new RegExp(
|
||||
`<${CHANNEL_TAG}\\s+source="([^"]+)"([^>]*)>\\n?([\\s\\S]*?)\\n?</${CHANNEL_TAG}>`,
|
||||
)
|
||||
const USER_ATTR_RE = /\buser="([^"]+)"/
|
||||
|
||||
// Plugin-provided servers get names like plugin:slack-channel:slack via
|
||||
// addPluginScopeToServers — show just the leaf. Matches the suffix-match
|
||||
// logic in isServerInChannels.
|
||||
function displayServerName(name: string): string {
|
||||
const i = name.lastIndexOf(':');
|
||||
return i === -1 ? name : name.slice(i + 1);
|
||||
const i = name.lastIndexOf(':')
|
||||
return i === -1 ? name : name.slice(i + 1)
|
||||
}
|
||||
const TRUNCATE_AT = 60;
|
||||
export function UserChannelMessage(t0) {
|
||||
const $ = _c(29);
|
||||
const {
|
||||
addMargin,
|
||||
param: t1
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
let T0;
|
||||
let T1;
|
||||
let T2;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
let t6;
|
||||
let t7;
|
||||
let truncated;
|
||||
let user;
|
||||
if ($[0] !== addMargin || $[1] !== text) {
|
||||
t7 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const m = CHANNEL_RE.exec(text);
|
||||
if (!m) {
|
||||
t7 = null;
|
||||
break bb0;
|
||||
}
|
||||
const [, source, attrs, content] = m;
|
||||
user = USER_ATTR_RE.exec(attrs ?? "")?.[1];
|
||||
const body = (content ?? "").trim().replace(/\s+/g, " ");
|
||||
truncated = truncateToWidth(body, TRUNCATE_AT);
|
||||
T2 = Box;
|
||||
t6 = addMargin ? 1 : 0;
|
||||
T1 = Text;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text color="suggestion">{CHANNEL_ARROW}</Text>;
|
||||
$[13] = t4;
|
||||
} else {
|
||||
t4 = $[13];
|
||||
}
|
||||
t5 = " ";
|
||||
T0 = Text;
|
||||
t2 = true;
|
||||
t3 = displayServerName(source ?? "");
|
||||
}
|
||||
$[0] = addMargin;
|
||||
$[1] = text;
|
||||
$[2] = T0;
|
||||
$[3] = T1;
|
||||
$[4] = T2;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
$[8] = t5;
|
||||
$[9] = t6;
|
||||
$[10] = t7;
|
||||
$[11] = truncated;
|
||||
$[12] = user;
|
||||
} else {
|
||||
T0 = $[2];
|
||||
T1 = $[3];
|
||||
T2 = $[4];
|
||||
t2 = $[5];
|
||||
t3 = $[6];
|
||||
t4 = $[7];
|
||||
t5 = $[8];
|
||||
t6 = $[9];
|
||||
t7 = $[10];
|
||||
truncated = $[11];
|
||||
user = $[12];
|
||||
}
|
||||
if (t7 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t7;
|
||||
}
|
||||
const t8 = user ? ` \u00b7 ${user}` : "";
|
||||
let t9;
|
||||
if ($[14] !== T0 || $[15] !== t2 || $[16] !== t3 || $[17] !== t8) {
|
||||
t9 = <T0 dimColor={t2}>{t3}{t8}:</T0>;
|
||||
$[14] = T0;
|
||||
$[15] = t2;
|
||||
$[16] = t3;
|
||||
$[17] = t8;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
let t10;
|
||||
if ($[19] !== T1 || $[20] !== t4 || $[21] !== t5 || $[22] !== t9 || $[23] !== truncated) {
|
||||
t10 = <T1>{t4}{t5}{t9}{" "}{truncated}</T1>;
|
||||
$[19] = T1;
|
||||
$[20] = t4;
|
||||
$[21] = t5;
|
||||
$[22] = t9;
|
||||
$[23] = truncated;
|
||||
$[24] = t10;
|
||||
} else {
|
||||
t10 = $[24];
|
||||
}
|
||||
let t11;
|
||||
if ($[25] !== T2 || $[26] !== t10 || $[27] !== t6) {
|
||||
t11 = <T2 marginTop={t6}>{t10}</T2>;
|
||||
$[25] = T2;
|
||||
$[26] = t10;
|
||||
$[27] = t6;
|
||||
$[28] = t11;
|
||||
} else {
|
||||
t11 = $[28];
|
||||
}
|
||||
return t11;
|
||||
|
||||
const TRUNCATE_AT = 60
|
||||
|
||||
export function UserChannelMessage({
|
||||
addMargin,
|
||||
param: { text },
|
||||
}: Props): React.ReactNode {
|
||||
const m = CHANNEL_RE.exec(text)
|
||||
if (!m) return null
|
||||
const [, source, attrs, content] = m
|
||||
const user = USER_ATTR_RE.exec(attrs ?? '')?.[1]
|
||||
const body = (content ?? '').trim().replace(/\s+/g, ' ')
|
||||
const truncated = truncateToWidth(body, TRUNCATE_AT)
|
||||
return (
|
||||
<Box marginTop={addMargin ? 1 : 0}>
|
||||
<Text>
|
||||
<Text color="suggestion">{CHANNEL_ARROW}</Text>{' '}
|
||||
<Text dimColor>
|
||||
{displayServerName(source ?? '')}
|
||||
{user ? ` \u00b7 ${user}` : ''}:
|
||||
</Text>{' '}
|
||||
{truncated}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,107 +1,57 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { COMMAND_MESSAGE_TAG } from '../../constants/xml.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { COMMAND_MESSAGE_TAG } from '../../constants/xml.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
};
|
||||
export function UserCommandMessage(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
addMargin,
|
||||
param: t1
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
let t2;
|
||||
if ($[0] !== text) {
|
||||
t2 = extractTag(text, COMMAND_MESSAGE_TAG);
|
||||
$[0] = text;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const commandMessage = t2;
|
||||
let t3;
|
||||
if ($[2] !== text) {
|
||||
t3 = extractTag(text, "command-args");
|
||||
$[2] = text;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const args = t3;
|
||||
const isSkillFormat = extractTag(text, "skill-format") === "true";
|
||||
if (!commandMessage) {
|
||||
return null;
|
||||
}
|
||||
if (isSkillFormat) {
|
||||
const t4 = addMargin ? 1 : 0;
|
||||
let t5;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text color="subtle">{figures.pointer} </Text>;
|
||||
$[4] = t5;
|
||||
} else {
|
||||
t5 = $[4];
|
||||
}
|
||||
let t6;
|
||||
if ($[5] !== commandMessage) {
|
||||
t6 = <Text>{t5}<Text color="text">Skill({commandMessage})</Text></Text>;
|
||||
$[5] = commandMessage;
|
||||
$[6] = t6;
|
||||
} else {
|
||||
t6 = $[6];
|
||||
}
|
||||
let t7;
|
||||
if ($[7] !== t4 || $[8] !== t6) {
|
||||
t7 = <Box flexDirection="column" marginTop={t4} backgroundColor="userMessageBackground" paddingRight={1}>{t6}</Box>;
|
||||
$[7] = t4;
|
||||
$[8] = t6;
|
||||
$[9] = t7;
|
||||
} else {
|
||||
t7 = $[9];
|
||||
}
|
||||
return t7;
|
||||
}
|
||||
let t4;
|
||||
if ($[10] !== args || $[11] !== commandMessage) {
|
||||
t4 = [commandMessage, args].filter(Boolean);
|
||||
$[10] = args;
|
||||
$[11] = commandMessage;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
const content = `/${t4.join(" ")}`;
|
||||
const t5 = addMargin ? 1 : 0;
|
||||
let t6;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Text color="subtle">{figures.pointer} </Text>;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
let t7;
|
||||
if ($[14] !== content) {
|
||||
t7 = <Text>{t6}<Text color="text">{content}</Text></Text>;
|
||||
$[14] = content;
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
}
|
||||
let t8;
|
||||
if ($[16] !== t5 || $[17] !== t7) {
|
||||
t8 = <Box flexDirection="column" marginTop={t5} backgroundColor="userMessageBackground" paddingRight={1}>{t7}</Box>;
|
||||
$[16] = t5;
|
||||
$[17] = t7;
|
||||
$[18] = t8;
|
||||
} else {
|
||||
t8 = $[18];
|
||||
}
|
||||
return t8;
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
}
|
||||
|
||||
export function UserCommandMessage({
|
||||
addMargin,
|
||||
param: { text },
|
||||
}: Props): React.ReactNode {
|
||||
const commandMessage = extractTag(text, COMMAND_MESSAGE_TAG)
|
||||
const args = extractTag(text, 'command-args')
|
||||
const isSkillFormat = extractTag(text, 'skill-format') === 'true'
|
||||
|
||||
if (!commandMessage) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Skills use "Skill(name)" format
|
||||
if (isSkillFormat) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
backgroundColor="userMessageBackground"
|
||||
paddingRight={1}
|
||||
>
|
||||
<Text>
|
||||
<Text color="subtle">{figures.pointer} </Text>
|
||||
<Text color="text">Skill({commandMessage})</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Slash command format: show as "❯ /command args"
|
||||
const content = `/${[commandMessage, args].filter(Boolean).join(' ')}`
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
backgroundColor="userMessageBackground"
|
||||
paddingRight={1}
|
||||
>
|
||||
<Text>
|
||||
<Text color="subtle">{figures.pointer} </Text>
|
||||
<Text color="text">{content}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { pathToFileURL } from 'url';
|
||||
import Link from '../../ink/components/Link.js';
|
||||
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { getStoredImagePath } from '../../utils/imageStore.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import * as React from 'react'
|
||||
import { pathToFileURL } from 'url'
|
||||
import Link from '../../ink/components/Link.js'
|
||||
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { getStoredImagePath } from '../../utils/imageStore.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
type Props = {
|
||||
imageId?: number;
|
||||
addMargin?: boolean;
|
||||
};
|
||||
imageId?: number
|
||||
addMargin?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an image attachment in user messages.
|
||||
@@ -17,42 +17,27 @@ type Props = {
|
||||
* Uses MessageResponse styling to appear connected to the message above,
|
||||
* unless addMargin is true (image starts a new user turn without text).
|
||||
*/
|
||||
export function UserImageMessage(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
imageId,
|
||||
addMargin
|
||||
} = t0;
|
||||
const label = imageId ? `[Image #${imageId}]` : "[Image]";
|
||||
let t1;
|
||||
if ($[0] !== imageId || $[1] !== label) {
|
||||
const imagePath = imageId ? getStoredImagePath(imageId) : null;
|
||||
t1 = imagePath && supportsHyperlinks() ? <Link url={pathToFileURL(imagePath).href}><Text>{label}</Text></Link> : <Text>{label}</Text>;
|
||||
$[0] = imageId;
|
||||
$[1] = label;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const content = t1;
|
||||
export function UserImageMessage({
|
||||
imageId,
|
||||
addMargin,
|
||||
}: Props): React.ReactNode {
|
||||
const label = imageId ? `[Image #${imageId}]` : '[Image]'
|
||||
const imagePath = imageId ? getStoredImagePath(imageId) : null
|
||||
|
||||
const content =
|
||||
imagePath && supportsHyperlinks() ? (
|
||||
<Link url={pathToFileURL(imagePath).href}>
|
||||
<Text>{label}</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<Text>{label}</Text>
|
||||
)
|
||||
|
||||
// When this image starts a new user turn (no text before it),
|
||||
// show with margin instead of the connected line style
|
||||
if (addMargin) {
|
||||
let t2;
|
||||
if ($[3] !== content) {
|
||||
t2 = <Box marginTop={1}>{content}</Box>;
|
||||
$[3] = content;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
return <Box marginTop={1}>{content}</Box>
|
||||
}
|
||||
let t2;
|
||||
if ($[5] !== content) {
|
||||
t2 = <MessageResponse>{content}</MessageResponse>;
|
||||
$[5] = content;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
|
||||
return <MessageResponse>{content}</MessageResponse>
|
||||
}
|
||||
|
||||
@@ -1,166 +1,80 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
|
||||
import { NO_CONTENT_MESSAGE } from '../../constants/messages.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import { Markdown } from '../Markdown.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import * as React from 'react'
|
||||
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
|
||||
import { NO_CONTENT_MESSAGE } from '../../constants/messages.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
import { Markdown } from '../Markdown.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
};
|
||||
export function UserLocalCommandOutputMessage(t0) {
|
||||
const $ = _c(4);
|
||||
const {
|
||||
content
|
||||
} = t0;
|
||||
let lines;
|
||||
let t1;
|
||||
if ($[0] !== content) {
|
||||
t1 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const stdout = extractTag(content, "local-command-stdout");
|
||||
const stderr = extractTag(content, "local-command-stderr");
|
||||
if (!stdout && !stderr) {
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <MessageResponse><Text dimColor={true}>{NO_CONTENT_MESSAGE}</Text></MessageResponse>;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
t1 = t2;
|
||||
break bb0;
|
||||
}
|
||||
lines = [];
|
||||
if (stdout?.trim()) {
|
||||
lines.push(<IndentedContent key="stdout">{stdout.trim()}</IndentedContent>);
|
||||
}
|
||||
if (stderr?.trim()) {
|
||||
lines.push(<IndentedContent key="stderr">{stderr.trim()}</IndentedContent>);
|
||||
}
|
||||
}
|
||||
$[0] = content;
|
||||
$[1] = lines;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
lines = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
if (t1 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t1;
|
||||
}
|
||||
return lines;
|
||||
content: string
|
||||
}
|
||||
function IndentedContent(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
children
|
||||
} = t0;
|
||||
if (children.startsWith(`${DIAMOND_OPEN} `) || children.startsWith(`${DIAMOND_FILLED} `)) {
|
||||
let t1;
|
||||
if ($[0] !== children) {
|
||||
t1 = <CloudLaunchContent>{children}</CloudLaunchContent>;
|
||||
$[0] = children;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
|
||||
export function UserLocalCommandOutputMessage({
|
||||
content,
|
||||
}: Props): React.ReactNode {
|
||||
const stdout = extractTag(content, 'local-command-stdout')
|
||||
const stderr = extractTag(content, 'local-command-stderr')
|
||||
if (!stdout && !stderr) {
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Text dimColor>{NO_CONTENT_MESSAGE}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>{" \u23BF "}</Text>;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
|
||||
const lines: React.ReactNode[] = []
|
||||
if (stdout?.trim()) {
|
||||
lines.push(<IndentedContent key="stdout">{stdout.trim()}</IndentedContent>)
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== children) {
|
||||
t2 = <Box flexDirection="row">{t1}<Box flexDirection="column" flexGrow={1}><Markdown>{children}</Markdown></Box></Box>;
|
||||
$[3] = children;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
if (stderr?.trim()) {
|
||||
lines.push(<IndentedContent key="stderr">{stderr.trim()}</IndentedContent>)
|
||||
}
|
||||
return t2;
|
||||
return lines
|
||||
}
|
||||
function CloudLaunchContent(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
children
|
||||
} = t0;
|
||||
const diamond = children[0];
|
||||
let label;
|
||||
let rest;
|
||||
let t1;
|
||||
if ($[0] !== children) {
|
||||
const nl = children.indexOf("\n");
|
||||
const header = nl === -1 ? children.slice(2) : children.slice(2, nl);
|
||||
rest = nl === -1 ? "" : children.slice(nl + 1).trim();
|
||||
const sep = header.indexOf(" \xB7 ");
|
||||
label = sep === -1 ? header : header.slice(0, sep);
|
||||
t1 = sep === -1 ? "" : header.slice(sep);
|
||||
$[0] = children;
|
||||
$[1] = label;
|
||||
$[2] = rest;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
label = $[1];
|
||||
rest = $[2];
|
||||
t1 = $[3];
|
||||
|
||||
function IndentedContent({ children }: { children: string }): React.ReactNode {
|
||||
if (
|
||||
children.startsWith(`${DIAMOND_OPEN} `) ||
|
||||
children.startsWith(`${DIAMOND_FILLED} `)
|
||||
) {
|
||||
return <CloudLaunchContent>{children}</CloudLaunchContent>
|
||||
}
|
||||
const suffix = t1;
|
||||
let t2;
|
||||
if ($[4] !== diamond) {
|
||||
t2 = <Text color="background">{diamond} </Text>;
|
||||
$[4] = diamond;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== label) {
|
||||
t3 = <Text bold={true}>{label}</Text>;
|
||||
$[6] = label;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== suffix) {
|
||||
t4 = suffix && <Text dimColor={true}>{suffix}</Text>;
|
||||
$[8] = suffix;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== t2 || $[11] !== t3 || $[12] !== t4) {
|
||||
t5 = <Text>{t2}{t3}{t4}</Text>;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
$[12] = t4;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}
|
||||
let t6;
|
||||
if ($[14] !== rest) {
|
||||
t6 = rest && <Box flexDirection="row"><Text dimColor={true}>{" \u23BF "}</Text><Text dimColor={true}>{rest}</Text></Box>;
|
||||
$[14] = rest;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
let t7;
|
||||
if ($[16] !== t5 || $[17] !== t6) {
|
||||
t7 = <Box flexDirection="column">{t5}{t6}</Box>;
|
||||
$[16] = t5;
|
||||
$[17] = t6;
|
||||
$[18] = t7;
|
||||
} else {
|
||||
t7 = $[18];
|
||||
}
|
||||
return t7;
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor>{' ⎿ '}</Text>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<Markdown>{children}</Markdown>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function CloudLaunchContent({
|
||||
children,
|
||||
}: {
|
||||
children: string
|
||||
}): React.ReactNode {
|
||||
const diamond = children[0]!
|
||||
const nl = children.indexOf('\n')
|
||||
const header = nl === -1 ? children.slice(2) : children.slice(2, nl)
|
||||
const rest = nl === -1 ? '' : children.slice(nl + 1).trim()
|
||||
const sep = header.indexOf(' · ')
|
||||
const label = sep === -1 ? header : header.slice(0, sep)
|
||||
const suffix = sep === -1 ? '' : header.slice(sep)
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
<Text color="background">{diamond} </Text>
|
||||
<Text bold>{label}</Text>
|
||||
{suffix && <Text dimColor>{suffix}</Text>}
|
||||
</Text>
|
||||
{rest && (
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor>{' ⎿ '}</Text>
|
||||
<Text dimColor>{rest}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,74 +1,44 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import sample from 'lodash-es/sample.js';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import sample from 'lodash-es/sample.js'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
|
||||
function getSavingMessage(): string {
|
||||
return sample(['Got it.', 'Good to know.', 'Noted.']);
|
||||
return sample(['Got it.', 'Good to know.', 'Noted.'])
|
||||
}
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
text: string;
|
||||
};
|
||||
export function UserMemoryInputMessage(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
text,
|
||||
addMargin
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== text) {
|
||||
t1 = extractTag(text, "user-memory-input");
|
||||
$[0] = text;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const input = t1;
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = getSavingMessage();
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const savingText = t2;
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
const t3 = addMargin ? 1 : 0;
|
||||
let t4;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text color="remember" backgroundColor="memoryBackgroundColor">#</Text>;
|
||||
$[3] = t4;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
}
|
||||
let t5;
|
||||
if ($[4] !== input) {
|
||||
t5 = <Box>{t4}<Text backgroundColor="memoryBackgroundColor" color="text">{" "}{input}{" "}</Text></Box>;
|
||||
$[4] = input;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
let t6;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <MessageResponse height={1}><Text dimColor={true}>{savingText}</Text></MessageResponse>;
|
||||
$[6] = t6;
|
||||
} else {
|
||||
t6 = $[6];
|
||||
}
|
||||
let t7;
|
||||
if ($[7] !== t3 || $[8] !== t5) {
|
||||
t7 = <Box flexDirection="column" marginTop={t3} width="100%">{t5}{t6}</Box>;
|
||||
$[7] = t3;
|
||||
$[8] = t5;
|
||||
$[9] = t7;
|
||||
} else {
|
||||
t7 = $[9];
|
||||
}
|
||||
return t7;
|
||||
addMargin: boolean
|
||||
text: string
|
||||
}
|
||||
|
||||
export function UserMemoryInputMessage({
|
||||
text,
|
||||
addMargin,
|
||||
}: Props): React.ReactNode {
|
||||
const input = extractTag(text, 'user-memory-input')
|
||||
const savingText = useMemo(() => getSavingMessage(), [])
|
||||
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
||||
<Box>
|
||||
<Text color="remember" backgroundColor="memoryBackgroundColor">
|
||||
#
|
||||
</Text>
|
||||
<Text backgroundColor="memoryBackgroundColor" color="text">
|
||||
{' '}
|
||||
{input}{' '}
|
||||
</Text>
|
||||
</Box>
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>{savingText}</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,30 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { Markdown } from '../Markdown.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { Markdown } from '../Markdown.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
planContent: string;
|
||||
};
|
||||
export function UserPlanMessage(t0) {
|
||||
const $ = _c(6);
|
||||
const {
|
||||
addMargin,
|
||||
planContent
|
||||
} = t0;
|
||||
const t1 = addMargin ? 1 : 0;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box marginBottom={1}><Text bold={true} color="planMode">Plan to implement</Text></Box>;
|
||||
$[0] = t2;
|
||||
} else {
|
||||
t2 = $[0];
|
||||
}
|
||||
let t3;
|
||||
if ($[1] !== planContent) {
|
||||
t3 = <Markdown>{planContent}</Markdown>;
|
||||
$[1] = planContent;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
let t4;
|
||||
if ($[3] !== t1 || $[4] !== t3) {
|
||||
t4 = <Box flexDirection="column" borderStyle="round" borderColor="planMode" marginTop={t1} paddingX={1}>{t2}{t3}</Box>;
|
||||
$[3] = t1;
|
||||
$[4] = t3;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
addMargin: boolean
|
||||
planContent: string
|
||||
}
|
||||
|
||||
export function UserPlanMessage({
|
||||
addMargin,
|
||||
planContent,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="planMode"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
paddingX={1}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color="planMode">
|
||||
Plan to implement
|
||||
</Text>
|
||||
</Box>
|
||||
<Markdown>{planContent}</Markdown>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js';
|
||||
import { Box } from '../../ink.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { countCharInString } from '../../utils/stringUtils.js';
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js';
|
||||
import { HighlightedThinkingText } from './HighlightedThinkingText.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js'
|
||||
import { Box } from '../../ink.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { countCharInString } from '../../utils/stringUtils.js'
|
||||
import { MessageActionsSelectedContext } from '../messageActions.js'
|
||||
import { HighlightedThinkingText } from './HighlightedThinkingText.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
isTranscriptMode?: boolean;
|
||||
timestamp?: string;
|
||||
};
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
isTranscriptMode?: boolean
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
// Hard cap on displayed prompt text. Piping large files via stdin
|
||||
// (e.g. `cat 11k-line-file | claude`) creates a single user message whose
|
||||
@@ -25,16 +26,15 @@ type Props = {
|
||||
// avoids this via <Static> (print-and-forget to terminal scrollback).
|
||||
// Head+tail because `{ cat file; echo prompt; } | claude` puts the user's
|
||||
// actual question at the end.
|
||||
const MAX_DISPLAY_CHARS = 10_000;
|
||||
const TRUNCATE_HEAD_CHARS = 2_500;
|
||||
const TRUNCATE_TAIL_CHARS = 2_500;
|
||||
const MAX_DISPLAY_CHARS = 10_000
|
||||
const TRUNCATE_HEAD_CHARS = 2_500
|
||||
const TRUNCATE_TAIL_CHARS = 2_500
|
||||
|
||||
export function UserPromptMessage({
|
||||
addMargin,
|
||||
param: {
|
||||
text
|
||||
},
|
||||
param: { text },
|
||||
isTranscriptMode,
|
||||
timestamp
|
||||
timestamp,
|
||||
}: Props): React.ReactNode {
|
||||
// REPL.tsx passes isBriefOnly={viewedTeammateTask ? false : isBriefOnly}
|
||||
// but that prop isn't threaded this deep — replicate the override by
|
||||
@@ -48,32 +48,72 @@ export function UserPromptMessage({
|
||||
// bypasses React.memo). Runtime-gated like isBriefEnabled() but inlined
|
||||
// to avoid pulling BriefTool.ts → prompt.ts tool-name strings into
|
||||
// external builds.
|
||||
const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly) : false;
|
||||
const viewingAgentTaskId = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s_0 => s_0.viewingAgentTaskId) : null;
|
||||
const isBriefOnly =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly)
|
||||
: false
|
||||
const viewingAgentTaskId =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.viewingAgentTaskId)
|
||||
: null
|
||||
// Hoisted to mount-time — per-message component, re-renders on every scroll.
|
||||
const briefEnvEnabled = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_BRIEF), []) : false;
|
||||
const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ? (getKairosActive() || getUserMsgOptIn() && (briefEnvEnabled || getFeatureValue_CACHED_MAY_BE_STALE('tengu_kairos_brief', false))) && isBriefOnly && !isTranscriptMode && !viewingAgentTaskId : false;
|
||||
const briefEnvEnabled =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_BRIEF), [])
|
||||
: false
|
||||
const useBriefLayout =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? (getKairosActive() ||
|
||||
(getUserMsgOptIn() &&
|
||||
(briefEnvEnabled ||
|
||||
getFeatureValue_CACHED_MAY_BE_STALE(
|
||||
'tengu_kairos_brief',
|
||||
false,
|
||||
)))) &&
|
||||
isBriefOnly &&
|
||||
!isTranscriptMode &&
|
||||
!viewingAgentTaskId
|
||||
: false
|
||||
|
||||
// Truncate before the early return so the hook order is stable.
|
||||
const displayText = useMemo(() => {
|
||||
if (text.length <= MAX_DISPLAY_CHARS) return text;
|
||||
const head = text.slice(0, TRUNCATE_HEAD_CHARS);
|
||||
const tail = text.slice(-TRUNCATE_TAIL_CHARS);
|
||||
const hiddenLines = countCharInString(text, '\n', TRUNCATE_HEAD_CHARS) - countCharInString(tail, '\n');
|
||||
return `${head}\n… +${hiddenLines} lines …\n${tail}`;
|
||||
}, [text]);
|
||||
const isSelected = useContext(MessageActionsSelectedContext);
|
||||
if (text.length <= MAX_DISPLAY_CHARS) return text
|
||||
const head = text.slice(0, TRUNCATE_HEAD_CHARS)
|
||||
const tail = text.slice(-TRUNCATE_TAIL_CHARS)
|
||||
const hiddenLines =
|
||||
countCharInString(text, '\n', TRUNCATE_HEAD_CHARS) -
|
||||
countCharInString(tail, '\n')
|
||||
return `${head}\n… +${hiddenLines} lines …\n${tail}`
|
||||
}, [text])
|
||||
|
||||
const isSelected = useContext(MessageActionsSelectedContext)
|
||||
|
||||
if (!text) {
|
||||
logError(new Error('No content found in user prompt message'));
|
||||
return null;
|
||||
logError(new Error('No content found in user prompt message'))
|
||||
return null
|
||||
}
|
||||
return <Box flexDirection="column" marginTop={addMargin ? 1 : 0} backgroundColor={isSelected ? 'messageActionsBackground' : useBriefLayout ? undefined : 'userMessageBackground'} paddingRight={useBriefLayout ? 0 : 1}>
|
||||
<HighlightedThinkingText text={displayText} useBriefLayout={useBriefLayout} timestamp={useBriefLayout ? timestamp : undefined} />
|
||||
</Box>;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={addMargin ? 1 : 0}
|
||||
backgroundColor={
|
||||
isSelected
|
||||
? 'messageActionsBackground'
|
||||
: useBriefLayout
|
||||
? undefined
|
||||
: 'userMessageBackground'
|
||||
}
|
||||
paddingRight={useBriefLayout ? 0 : 1}
|
||||
>
|
||||
<HighlightedThinkingText
|
||||
text={displayText}
|
||||
useBriefLayout={useBriefLayout}
|
||||
timestamp={useBriefLayout ? timestamp : undefined}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,120 +1,91 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { REFRESH_ARROW } from '../../constants/figures.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { REFRESH_ARROW } from '../../constants/figures.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
};
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
}
|
||||
|
||||
type ParsedUpdate = {
|
||||
kind: 'resource' | 'polling';
|
||||
server: string;
|
||||
kind: 'resource' | 'polling'
|
||||
server: string
|
||||
/** URI for resource updates, tool name for polling updates */
|
||||
target: string;
|
||||
reason?: string;
|
||||
};
|
||||
target: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
// Parse resource and polling updates from XML format
|
||||
function parseUpdates(text: string): ParsedUpdate[] {
|
||||
const updates: ParsedUpdate[] = [];
|
||||
const updates: ParsedUpdate[] = []
|
||||
|
||||
// Match <mcp-resource-update server="..." uri="...">
|
||||
const resourceRegex = /<mcp-resource-update\s+server="([^"]+)"\s+uri="([^"]+)"[^>]*>(?:[\s\S]*?<reason>([^<]+)<\/reason>)?/g;
|
||||
let match;
|
||||
const resourceRegex =
|
||||
/<mcp-resource-update\s+server="([^"]+)"\s+uri="([^"]+)"[^>]*>(?:[\s\S]*?<reason>([^<]+)<\/reason>)?/g
|
||||
let match
|
||||
while ((match = resourceRegex.exec(text)) !== null) {
|
||||
updates.push({
|
||||
kind: 'resource',
|
||||
server: match[1] ?? '',
|
||||
target: match[2] ?? '',
|
||||
reason: match[3]
|
||||
});
|
||||
reason: match[3],
|
||||
})
|
||||
}
|
||||
|
||||
// Match <mcp-polling-update type="tool" server="..." tool="...">
|
||||
const pollingRegex = /<mcp-polling-update\s+type="([^"]+)"\s+server="([^"]+)"\s+tool="([^"]+)"[^>]*>(?:[\s\S]*?<reason>([^<]+)<\/reason>)?/g;
|
||||
const pollingRegex =
|
||||
/<mcp-polling-update\s+type="([^"]+)"\s+server="([^"]+)"\s+tool="([^"]+)"[^>]*>(?:[\s\S]*?<reason>([^<]+)<\/reason>)?/g
|
||||
while ((match = pollingRegex.exec(text)) !== null) {
|
||||
updates.push({
|
||||
kind: 'polling',
|
||||
server: match[2] ?? '',
|
||||
target: match[3] ?? '',
|
||||
reason: match[4]
|
||||
});
|
||||
reason: match[4],
|
||||
})
|
||||
}
|
||||
return updates;
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
// Format URI for display - show just the meaningful part
|
||||
function formatUri(uri: string): string {
|
||||
// For file:// URIs, show just the filename
|
||||
if (uri.startsWith('file://')) {
|
||||
const path = uri.slice(7);
|
||||
const parts = path.split('/');
|
||||
return parts[parts.length - 1] || path;
|
||||
const path = uri.slice(7)
|
||||
const parts = path.split('/')
|
||||
return parts[parts.length - 1] || path
|
||||
}
|
||||
// For other URIs, show the whole thing but truncated
|
||||
if (uri.length > 40) {
|
||||
return uri.slice(0, 39) + '\u2026';
|
||||
return uri.slice(0, 39) + '\u2026'
|
||||
}
|
||||
return uri;
|
||||
return uri
|
||||
}
|
||||
export function UserResourceUpdateMessage(t0) {
|
||||
const $ = _c(12);
|
||||
const {
|
||||
addMargin,
|
||||
param: t1
|
||||
} = t0;
|
||||
const {
|
||||
text
|
||||
} = t1;
|
||||
let T0;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[0] !== addMargin || $[1] !== text) {
|
||||
t5 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const updates = parseUpdates(text);
|
||||
if (updates.length === 0) {
|
||||
t5 = null;
|
||||
break bb0;
|
||||
}
|
||||
T0 = Box;
|
||||
t2 = "column";
|
||||
t3 = addMargin ? 1 : 0;
|
||||
t4 = updates.map(_temp);
|
||||
}
|
||||
$[0] = addMargin;
|
||||
$[1] = text;
|
||||
$[2] = T0;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
$[5] = t4;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
T0 = $[2];
|
||||
t2 = $[3];
|
||||
t3 = $[4];
|
||||
t4 = $[5];
|
||||
t5 = $[6];
|
||||
}
|
||||
if (t5 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t5;
|
||||
}
|
||||
let t6;
|
||||
if ($[7] !== T0 || $[8] !== t2 || $[9] !== t3 || $[10] !== t4) {
|
||||
t6 = <T0 flexDirection={t2} marginTop={t3}>{t4}</T0>;
|
||||
$[7] = T0;
|
||||
$[8] = t2;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
function _temp(update, i) {
|
||||
return <Box key={i}><Text><Text color="success">{REFRESH_ARROW}</Text>{" "}<Text dimColor={true}>{update.server}:</Text>{" "}<Text color="suggestion">{update.kind === "resource" ? formatUri(update.target) : update.target}</Text>{update.reason && <Text dimColor={true}> · {update.reason}</Text>}</Text></Box>;
|
||||
|
||||
export function UserResourceUpdateMessage({
|
||||
addMargin,
|
||||
param: { text },
|
||||
}: Props): React.ReactNode {
|
||||
const updates = parseUpdates(text)
|
||||
if (updates.length === 0) return null
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={addMargin ? 1 : 0}>
|
||||
{updates.map((update, i) => (
|
||||
<Box key={i}>
|
||||
<Text>
|
||||
<Text color="success">{REFRESH_ARROW}</Text>{' '}
|
||||
<Text dimColor>{update.server}:</Text>{' '}
|
||||
<Text color="suggestion">
|
||||
{update.kind === 'resource'
|
||||
? formatUri(update.target)
|
||||
: update.target}
|
||||
</Text>
|
||||
{update.reason && <Text dimColor> · {update.reason}</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { TEAMMATE_MESSAGE_TAG } from '../../constants/xml.js';
|
||||
import { Ansi, Box, Text, type TextProps } from '../../ink.js';
|
||||
import { toInkColor } from '../../utils/ink.js';
|
||||
import { jsonParse } from '../../utils/slowOperations.js';
|
||||
import { isShutdownApproved } from '../../utils/teammateMailbox.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { tryRenderPlanApprovalMessage } from './PlanApprovalMessage.js';
|
||||
import { tryRenderShutdownMessage } from './ShutdownMessage.js';
|
||||
import { tryRenderTaskAssignmentMessage } from './TaskAssignmentMessage.js';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { TEAMMATE_MESSAGE_TAG } from '../../constants/xml.js'
|
||||
import { Ansi, Box, Text, type TextProps } from '../../ink.js'
|
||||
import { toInkColor } from '../../utils/ink.js'
|
||||
import { jsonParse } from '../../utils/slowOperations.js'
|
||||
import { isShutdownApproved } from '../../utils/teammateMailbox.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { tryRenderPlanApprovalMessage } from './PlanApprovalMessage.js'
|
||||
import { tryRenderShutdownMessage } from './ShutdownMessage.js'
|
||||
import { tryRenderTaskAssignmentMessage } from './TaskAssignmentMessage.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
type ParsedMessage = {
|
||||
teammateId: string;
|
||||
content: string;
|
||||
color?: string;
|
||||
summary?: string;
|
||||
};
|
||||
const TEAMMATE_MSG_REGEX = new RegExp(`<${TEAMMATE_MESSAGE_TAG}\\s+teammate_id="([^"]+)"(?:\\s+color="([^"]+)")?(?:\\s+summary="([^"]+)")?>\\n?([\\s\\S]*?)\\n?<\\/${TEAMMATE_MESSAGE_TAG}>`, 'g');
|
||||
teammateId: string
|
||||
content: string
|
||||
color?: string
|
||||
summary?: string
|
||||
}
|
||||
|
||||
const TEAMMATE_MSG_REGEX = new RegExp(
|
||||
`<${TEAMMATE_MESSAGE_TAG}\\s+teammate_id="([^"]+)"(?:\\s+color="([^"]+)")?(?:\\s+summary="([^"]+)")?>\\n?([\\s\\S]*?)\\n?<\\/${TEAMMATE_MESSAGE_TAG}>`,
|
||||
'g',
|
||||
)
|
||||
|
||||
/**
|
||||
* Parse all teammate messages from XML format:
|
||||
@@ -30,176 +35,169 @@ const TEAMMATE_MSG_REGEX = new RegExp(`<${TEAMMATE_MESSAGE_TAG}\\s+teammate_id="
|
||||
* Supports multiple messages in a single text block.
|
||||
*/
|
||||
function parseTeammateMessages(text: string): ParsedMessage[] {
|
||||
const messages: ParsedMessage[] = [];
|
||||
const messages: ParsedMessage[] = []
|
||||
// Use matchAll to find all matches (this is a RegExp method, not child_process)
|
||||
for (const match of text.matchAll(TEAMMATE_MSG_REGEX)) {
|
||||
if (match[1] && match[4]) {
|
||||
messages.push({
|
||||
teammateId: match[1],
|
||||
color: match[2],
|
||||
// may be undefined
|
||||
summary: match[3],
|
||||
// may be undefined
|
||||
content: match[4].trim()
|
||||
});
|
||||
color: match[2], // may be undefined
|
||||
summary: match[3], // may be undefined
|
||||
content: match[4].trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
function getDisplayName(teammateId: string): string {
|
||||
if (teammateId === 'leader') {
|
||||
return 'leader';
|
||||
return 'leader'
|
||||
}
|
||||
return teammateId;
|
||||
return teammateId
|
||||
}
|
||||
|
||||
export function UserTeammateMessage({
|
||||
addMargin,
|
||||
param: {
|
||||
text
|
||||
},
|
||||
isTranscriptMode
|
||||
param: { text },
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const messages = parseTeammateMessages(text).filter(msg => {
|
||||
// Pre-filter shutdown lifecycle messages to avoid empty wrapper
|
||||
// Box elements creating blank lines between model turns
|
||||
if (isShutdownApproved(msg.content)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const parsed = jsonParse(msg.content);
|
||||
if (parsed?.type === 'teammate_terminated') return false;
|
||||
const parsed = jsonParse(msg.content)
|
||||
if (parsed?.type === 'teammate_terminated') return false
|
||||
} catch {
|
||||
// Not JSON, keep the message
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return true
|
||||
})
|
||||
if (messages.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
||||
{messages.map((msg_0, index) => {
|
||||
const inkColor = toInkColor(msg_0.color);
|
||||
const displayName = getDisplayName(msg_0.teammateId);
|
||||
|
||||
// Try to render as plan approval message (request or response)
|
||||
const planApprovalElement = tryRenderPlanApprovalMessage(msg_0.content, displayName);
|
||||
if (planApprovalElement) {
|
||||
return <React.Fragment key={index}>{planApprovalElement}</React.Fragment>;
|
||||
}
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
|
||||
{messages.map((msg, index) => {
|
||||
const inkColor = toInkColor(msg.color)
|
||||
const displayName = getDisplayName(msg.teammateId)
|
||||
|
||||
// Try to render as shutdown message (request or rejected)
|
||||
const shutdownElement = tryRenderShutdownMessage(msg_0.content);
|
||||
if (shutdownElement) {
|
||||
return <React.Fragment key={index}>{shutdownElement}</React.Fragment>;
|
||||
}
|
||||
// Try to render as plan approval message (request or response)
|
||||
const planApprovalElement = tryRenderPlanApprovalMessage(
|
||||
msg.content,
|
||||
displayName,
|
||||
)
|
||||
if (planApprovalElement) {
|
||||
return (
|
||||
<React.Fragment key={index}>{planApprovalElement}</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// Try to render as task assignment message
|
||||
const taskAssignmentElement = tryRenderTaskAssignmentMessage(msg_0.content);
|
||||
if (taskAssignmentElement) {
|
||||
return <React.Fragment key={index}>{taskAssignmentElement}</React.Fragment>;
|
||||
}
|
||||
// Try to render as shutdown message (request or rejected)
|
||||
const shutdownElement = tryRenderShutdownMessage(msg.content)
|
||||
if (shutdownElement) {
|
||||
return <React.Fragment key={index}>{shutdownElement}</React.Fragment>
|
||||
}
|
||||
|
||||
// Try to parse as structured JSON message
|
||||
let parsedIdleNotification: {
|
||||
type?: string;
|
||||
} | null = null;
|
||||
try {
|
||||
parsedIdleNotification = jsonParse(msg_0.content);
|
||||
} catch {
|
||||
// Not JSON
|
||||
}
|
||||
// Try to render as task assignment message
|
||||
const taskAssignmentElement = tryRenderTaskAssignmentMessage(
|
||||
msg.content,
|
||||
)
|
||||
if (taskAssignmentElement) {
|
||||
return (
|
||||
<React.Fragment key={index}>{taskAssignmentElement}</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// Hide idle notifications - they are processed silently
|
||||
if (parsedIdleNotification?.type === 'idle_notification') {
|
||||
return null;
|
||||
}
|
||||
// Try to parse as structured JSON message
|
||||
let parsedIdleNotification: { type?: string } | null = null
|
||||
try {
|
||||
parsedIdleNotification = jsonParse(msg.content)
|
||||
} catch {
|
||||
// Not JSON
|
||||
}
|
||||
|
||||
// Task completed notification - show which task was completed
|
||||
if (parsedIdleNotification?.type === 'task_completed') {
|
||||
const taskCompleted = parsedIdleNotification as {
|
||||
type: string;
|
||||
from: string;
|
||||
taskId: string;
|
||||
taskSubject?: string;
|
||||
};
|
||||
return <Box key={index} flexDirection="column" marginTop={1}>
|
||||
<Text color={inkColor}>{`@${displayName}${figures.pointer}`}</Text>
|
||||
// Hide idle notifications - they are processed silently
|
||||
if (parsedIdleNotification?.type === 'idle_notification') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Task completed notification - show which task was completed
|
||||
if (parsedIdleNotification?.type === 'task_completed') {
|
||||
const taskCompleted = parsedIdleNotification as {
|
||||
type: string
|
||||
from: string
|
||||
taskId: string
|
||||
taskSubject?: string
|
||||
}
|
||||
return (
|
||||
<Box key={index} flexDirection="column" marginTop={1}>
|
||||
<Text
|
||||
color={inkColor}
|
||||
>{`@${displayName}${figures.pointer}`}</Text>
|
||||
<MessageResponse>
|
||||
<Text color="success">✓</Text>
|
||||
<Text>
|
||||
{' '}
|
||||
Completed task #{taskCompleted.taskId}
|
||||
{taskCompleted.taskSubject && <Text dimColor> ({taskCompleted.taskSubject})</Text>}
|
||||
{taskCompleted.taskSubject && (
|
||||
<Text dimColor> ({taskCompleted.taskSubject})</Text>
|
||||
)}
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
</Box>;
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Default: plain text message (truncated)
|
||||
return <TeammateMessageContent key={index} displayName={displayName} inkColor={inkColor} content={msg_0.content} summary={msg_0.summary} isTranscriptMode={isTranscriptMode} />;
|
||||
})}
|
||||
</Box>;
|
||||
// Default: plain text message (truncated)
|
||||
return (
|
||||
<TeammateMessageContent
|
||||
key={index}
|
||||
displayName={displayName}
|
||||
inkColor={inkColor}
|
||||
content={msg.content}
|
||||
summary={msg.summary}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type TeammateMessageContentProps = {
|
||||
displayName: string;
|
||||
inkColor: TextProps['color'];
|
||||
content: string;
|
||||
summary?: string;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function TeammateMessageContent(t0) {
|
||||
const $ = _c(14);
|
||||
const {
|
||||
displayName,
|
||||
inkColor,
|
||||
content,
|
||||
summary,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
const t1 = `@${displayName}${figures.pointer}`;
|
||||
let t2;
|
||||
if ($[0] !== inkColor || $[1] !== t1) {
|
||||
t2 = <Text color={inkColor}>{t1}</Text>;
|
||||
$[0] = inkColor;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== summary) {
|
||||
t3 = summary && <Text> {summary}</Text>;
|
||||
$[3] = summary;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== t2 || $[6] !== t3) {
|
||||
t4 = <Box>{t2}{t3}</Box>;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== content || $[9] !== isTranscriptMode) {
|
||||
t5 = isTranscriptMode && <Box paddingLeft={2}><Text><Ansi>{content}</Ansi></Text></Box>;
|
||||
$[8] = content;
|
||||
$[9] = isTranscriptMode;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
let t6;
|
||||
if ($[11] !== t4 || $[12] !== t5) {
|
||||
t6 = <Box flexDirection="column" marginTop={1}>{t4}{t5}</Box>;
|
||||
$[11] = t4;
|
||||
$[12] = t5;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
return t6;
|
||||
displayName: string
|
||||
inkColor: TextProps['color']
|
||||
content: string
|
||||
summary?: string
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function TeammateMessageContent({
|
||||
displayName,
|
||||
inkColor,
|
||||
content,
|
||||
summary,
|
||||
isTranscriptMode,
|
||||
}: TeammateMessageContentProps): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box>
|
||||
<Text color={inkColor}>{`@${displayName}${figures.pointer}`}</Text>
|
||||
{summary && <Text> {summary}</Text>}
|
||||
</Box>
|
||||
{isTranscriptMode && (
|
||||
<Box paddingLeft={2}>
|
||||
<Text>
|
||||
<Ansi>{content}</Ansi>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,274 +1,197 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { NO_CONTENT_MESSAGE } from '../../constants/messages.js';
|
||||
import { COMMAND_MESSAGE_TAG, LOCAL_COMMAND_CAVEAT_TAG, TASK_NOTIFICATION_TAG, TEAMMATE_MESSAGE_TAG, TICK_TAG } from '../../constants/xml.js';
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
|
||||
import { extractTag, INTERRUPT_MESSAGE, INTERRUPT_MESSAGE_FOR_TOOL_USE } from '../../utils/messages.js';
|
||||
import { InterruptedByUser } from '../InterruptedByUser.js';
|
||||
import { MessageResponse } from '../MessageResponse.js';
|
||||
import { UserAgentNotificationMessage } from './UserAgentNotificationMessage.js';
|
||||
import { UserBashInputMessage } from './UserBashInputMessage.js';
|
||||
import { UserBashOutputMessage } from './UserBashOutputMessage.js';
|
||||
import { UserCommandMessage } from './UserCommandMessage.js';
|
||||
import { UserLocalCommandOutputMessage } from './UserLocalCommandOutputMessage.js';
|
||||
import { UserMemoryInputMessage } from './UserMemoryInputMessage.js';
|
||||
import { UserPlanMessage } from './UserPlanMessage.js';
|
||||
import { UserPromptMessage } from './UserPromptMessage.js';
|
||||
import { UserResourceUpdateMessage } from './UserResourceUpdateMessage.js';
|
||||
import { UserTeammateMessage } from './UserTeammateMessage.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { NO_CONTENT_MESSAGE } from '../../constants/messages.js'
|
||||
import {
|
||||
COMMAND_MESSAGE_TAG,
|
||||
LOCAL_COMMAND_CAVEAT_TAG,
|
||||
TASK_NOTIFICATION_TAG,
|
||||
TEAMMATE_MESSAGE_TAG,
|
||||
TICK_TAG,
|
||||
} from '../../constants/xml.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
import {
|
||||
extractTag,
|
||||
INTERRUPT_MESSAGE,
|
||||
INTERRUPT_MESSAGE_FOR_TOOL_USE,
|
||||
} from '../../utils/messages.js'
|
||||
import { InterruptedByUser } from '../InterruptedByUser.js'
|
||||
import { MessageResponse } from '../MessageResponse.js'
|
||||
import { UserAgentNotificationMessage } from './UserAgentNotificationMessage.js'
|
||||
import { UserBashInputMessage } from './UserBashInputMessage.js'
|
||||
import { UserBashOutputMessage } from './UserBashOutputMessage.js'
|
||||
import { UserCommandMessage } from './UserCommandMessage.js'
|
||||
import { UserLocalCommandOutputMessage } from './UserLocalCommandOutputMessage.js'
|
||||
import { UserMemoryInputMessage } from './UserMemoryInputMessage.js'
|
||||
import { UserPlanMessage } from './UserPlanMessage.js'
|
||||
import { UserPromptMessage } from './UserPromptMessage.js'
|
||||
import { UserResourceUpdateMessage } from './UserResourceUpdateMessage.js'
|
||||
import { UserTeammateMessage } from './UserTeammateMessage.js'
|
||||
|
||||
type Props = {
|
||||
addMargin: boolean;
|
||||
param: TextBlockParam;
|
||||
verbose: boolean;
|
||||
planContent?: string;
|
||||
isTranscriptMode?: boolean;
|
||||
timestamp?: string;
|
||||
};
|
||||
export function UserTextMessage(t0) {
|
||||
const $ = _c(49);
|
||||
const {
|
||||
addMargin,
|
||||
param,
|
||||
verbose,
|
||||
planContent,
|
||||
isTranscriptMode,
|
||||
timestamp
|
||||
} = t0;
|
||||
if (param.text.trim() === NO_CONTENT_MESSAGE) {
|
||||
return null;
|
||||
}
|
||||
if (planContent) {
|
||||
let t1;
|
||||
if ($[0] !== addMargin || $[1] !== planContent) {
|
||||
t1 = <UserPlanMessage addMargin={addMargin} planContent={planContent} />;
|
||||
$[0] = addMargin;
|
||||
$[1] = planContent;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (extractTag(param.text, TICK_TAG)) {
|
||||
return null;
|
||||
}
|
||||
if (param.text.includes(`<${LOCAL_COMMAND_CAVEAT_TAG}>`)) {
|
||||
return null;
|
||||
}
|
||||
if (param.text.startsWith("<bash-stdout") || param.text.startsWith("<bash-stderr")) {
|
||||
let t1;
|
||||
if ($[3] !== param.text || $[4] !== verbose) {
|
||||
t1 = <UserBashOutputMessage content={param.text} verbose={verbose} />;
|
||||
$[3] = param.text;
|
||||
$[4] = verbose;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (param.text.startsWith("<local-command-stdout") || param.text.startsWith("<local-command-stderr")) {
|
||||
let t1;
|
||||
if ($[6] !== param.text) {
|
||||
t1 = <UserLocalCommandOutputMessage content={param.text} />;
|
||||
$[6] = param.text;
|
||||
$[7] = t1;
|
||||
} else {
|
||||
t1 = $[7];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (param.text === INTERRUPT_MESSAGE || param.text === INTERRUPT_MESSAGE_FOR_TOOL_USE) {
|
||||
let t1;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>;
|
||||
$[8] = t1;
|
||||
} else {
|
||||
t1 = $[8];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (feature("KAIROS_GITHUB_WEBHOOKS")) {
|
||||
if (param.text.startsWith("<github-webhook-activity>")) {
|
||||
let t1;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = require("./UserGitHubWebhookMessage.js");
|
||||
$[9] = t1;
|
||||
} else {
|
||||
t1 = $[9];
|
||||
}
|
||||
const {
|
||||
UserGitHubWebhookMessage
|
||||
} = t1 as typeof import('./UserGitHubWebhookMessage.js');
|
||||
let t2;
|
||||
if ($[10] !== addMargin || $[11] !== param) {
|
||||
t2 = <UserGitHubWebhookMessage addMargin={addMargin} param={param} />;
|
||||
$[10] = addMargin;
|
||||
$[11] = param;
|
||||
$[12] = t2;
|
||||
} else {
|
||||
t2 = $[12];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
}
|
||||
if (param.text.includes("<bash-input>")) {
|
||||
let t1;
|
||||
if ($[13] !== addMargin || $[14] !== param) {
|
||||
t1 = <UserBashInputMessage addMargin={addMargin} param={param} />;
|
||||
$[13] = addMargin;
|
||||
$[14] = param;
|
||||
$[15] = t1;
|
||||
} else {
|
||||
t1 = $[15];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (param.text.includes(`<${COMMAND_MESSAGE_TAG}>`)) {
|
||||
let t1;
|
||||
if ($[16] !== addMargin || $[17] !== param) {
|
||||
t1 = <UserCommandMessage addMargin={addMargin} param={param} />;
|
||||
$[16] = addMargin;
|
||||
$[17] = param;
|
||||
$[18] = t1;
|
||||
} else {
|
||||
t1 = $[18];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (param.text.includes("<user-memory-input>")) {
|
||||
let t1;
|
||||
if ($[19] !== addMargin || $[20] !== param.text) {
|
||||
t1 = <UserMemoryInputMessage addMargin={addMargin} text={param.text} />;
|
||||
$[19] = addMargin;
|
||||
$[20] = param.text;
|
||||
$[21] = t1;
|
||||
} else {
|
||||
t1 = $[21];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (isAgentSwarmsEnabled() && param.text.includes(`<${TEAMMATE_MESSAGE_TAG}`)) {
|
||||
let t1;
|
||||
if ($[22] !== addMargin || $[23] !== isTranscriptMode || $[24] !== param) {
|
||||
t1 = <UserTeammateMessage addMargin={addMargin} param={param} isTranscriptMode={isTranscriptMode} />;
|
||||
$[22] = addMargin;
|
||||
$[23] = isTranscriptMode;
|
||||
$[24] = param;
|
||||
$[25] = t1;
|
||||
} else {
|
||||
t1 = $[25];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (param.text.includes(`<${TASK_NOTIFICATION_TAG}`)) {
|
||||
let t1;
|
||||
if ($[26] !== addMargin || $[27] !== param) {
|
||||
t1 = <UserAgentNotificationMessage addMargin={addMargin} param={param} />;
|
||||
$[26] = addMargin;
|
||||
$[27] = param;
|
||||
$[28] = t1;
|
||||
} else {
|
||||
t1 = $[28];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (param.text.includes("<mcp-resource-update") || param.text.includes("<mcp-polling-update")) {
|
||||
let t1;
|
||||
if ($[29] !== addMargin || $[30] !== param) {
|
||||
t1 = <UserResourceUpdateMessage addMargin={addMargin} param={param} />;
|
||||
$[29] = addMargin;
|
||||
$[30] = param;
|
||||
$[31] = t1;
|
||||
} else {
|
||||
t1 = $[31];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (feature("FORK_SUBAGENT")) {
|
||||
if (param.text.includes("<fork-boilerplate>")) {
|
||||
let t1;
|
||||
if ($[32] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = require("./UserForkBoilerplateMessage.js");
|
||||
$[32] = t1;
|
||||
} else {
|
||||
t1 = $[32];
|
||||
}
|
||||
const {
|
||||
UserForkBoilerplateMessage
|
||||
} = t1 as typeof import('./UserForkBoilerplateMessage.js');
|
||||
let t2;
|
||||
if ($[33] !== addMargin || $[34] !== param) {
|
||||
t2 = <UserForkBoilerplateMessage addMargin={addMargin} param={param} />;
|
||||
$[33] = addMargin;
|
||||
$[34] = param;
|
||||
$[35] = t2;
|
||||
} else {
|
||||
t2 = $[35];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
}
|
||||
if (feature("UDS_INBOX")) {
|
||||
if (param.text.includes("<cross-session-message")) {
|
||||
let t1;
|
||||
if ($[36] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = require("./UserCrossSessionMessage.js");
|
||||
$[36] = t1;
|
||||
} else {
|
||||
t1 = $[36];
|
||||
}
|
||||
const {
|
||||
UserCrossSessionMessage
|
||||
} = t1 as typeof import('./UserCrossSessionMessage.js');
|
||||
let t2;
|
||||
if ($[37] !== addMargin || $[38] !== param) {
|
||||
t2 = <UserCrossSessionMessage addMargin={addMargin} param={param} />;
|
||||
$[37] = addMargin;
|
||||
$[38] = param;
|
||||
$[39] = t2;
|
||||
} else {
|
||||
t2 = $[39];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
}
|
||||
if (feature("KAIROS") || feature("KAIROS_CHANNELS")) {
|
||||
if (param.text.includes("<channel source=\"")) {
|
||||
let t1;
|
||||
if ($[40] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = require("./UserChannelMessage.js");
|
||||
$[40] = t1;
|
||||
} else {
|
||||
t1 = $[40];
|
||||
}
|
||||
const {
|
||||
UserChannelMessage
|
||||
} = t1 as typeof import('./UserChannelMessage.js');
|
||||
let t2;
|
||||
if ($[41] !== addMargin || $[42] !== param) {
|
||||
t2 = <UserChannelMessage addMargin={addMargin} param={param} />;
|
||||
$[41] = addMargin;
|
||||
$[42] = param;
|
||||
$[43] = t2;
|
||||
} else {
|
||||
t2 = $[43];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
}
|
||||
let t1;
|
||||
if ($[44] !== addMargin || $[45] !== isTranscriptMode || $[46] !== param || $[47] !== timestamp) {
|
||||
t1 = <UserPromptMessage addMargin={addMargin} param={param} isTranscriptMode={isTranscriptMode} timestamp={timestamp} />;
|
||||
$[44] = addMargin;
|
||||
$[45] = isTranscriptMode;
|
||||
$[46] = param;
|
||||
$[47] = timestamp;
|
||||
$[48] = t1;
|
||||
} else {
|
||||
t1 = $[48];
|
||||
}
|
||||
return t1;
|
||||
addMargin: boolean
|
||||
param: TextBlockParam
|
||||
verbose: boolean
|
||||
planContent?: string
|
||||
isTranscriptMode?: boolean
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
export function UserTextMessage({
|
||||
addMargin,
|
||||
param,
|
||||
verbose,
|
||||
planContent,
|
||||
isTranscriptMode,
|
||||
timestamp,
|
||||
}: Props): React.ReactNode {
|
||||
if (param.text.trim() === NO_CONTENT_MESSAGE) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Plan to implement message (cleared context flow)
|
||||
if (planContent) {
|
||||
return <UserPlanMessage addMargin={addMargin} planContent={planContent} />
|
||||
}
|
||||
|
||||
if (extractTag(param.text, TICK_TAG)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Hide synthetic caveat messages (should be filtered by isMeta, this is defensive)
|
||||
if (param.text.includes(`<${LOCAL_COMMAND_CAVEAT_TAG}>`)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Show bash output
|
||||
if (
|
||||
param.text.startsWith('<bash-stdout') ||
|
||||
param.text.startsWith('<bash-stderr')
|
||||
) {
|
||||
return <UserBashOutputMessage content={param.text} verbose={verbose} />
|
||||
}
|
||||
|
||||
// Show command output
|
||||
if (
|
||||
param.text.startsWith('<local-command-stdout') ||
|
||||
param.text.startsWith('<local-command-stderr')
|
||||
) {
|
||||
return <UserLocalCommandOutputMessage content={param.text} />
|
||||
}
|
||||
|
||||
// Handle interruption messages specially
|
||||
if (
|
||||
param.text === INTERRUPT_MESSAGE ||
|
||||
param.text === INTERRUPT_MESSAGE_FOR_TOOL_USE
|
||||
) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<InterruptedByUser />
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
// GitHub webhook events (check_run, review comments, pushes) delivered via
|
||||
// bound-session routing after /subscribe-pr. The tag constant is stripped
|
||||
// from external builds — inline the literal so the import doesn't fail.
|
||||
// The require() below DCEs when both flags are off. startsWith (not
|
||||
// includes) and before the includes-checks below: defense-in-depth if
|
||||
// the sanitizer were ever weakened.
|
||||
if (feature('KAIROS_GITHUB_WEBHOOKS')) {
|
||||
if (param.text.startsWith('<github-webhook-activity>')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { UserGitHubWebhookMessage } =
|
||||
require('./UserGitHubWebhookMessage.js') as typeof import('./UserGitHubWebhookMessage.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
return <UserGitHubWebhookMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
}
|
||||
|
||||
// Bash inputs!
|
||||
if (param.text.includes('<bash-input>')) {
|
||||
return <UserBashInputMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
|
||||
// Slash commands/
|
||||
if (param.text.includes(`<${COMMAND_MESSAGE_TAG}>`)) {
|
||||
return <UserCommandMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
|
||||
if (param.text.includes('<user-memory-input>')) {
|
||||
return <UserMemoryInputMessage addMargin={addMargin} text={param.text} />
|
||||
}
|
||||
|
||||
// Teammate messages - only check when swarms enabled
|
||||
if (
|
||||
isAgentSwarmsEnabled() &&
|
||||
param.text.includes(`<${TEAMMATE_MESSAGE_TAG}`)
|
||||
) {
|
||||
return (
|
||||
<UserTeammateMessage
|
||||
addMargin={addMargin}
|
||||
param={param}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Task notifications (agent completions, bash completions, etc.)
|
||||
if (param.text.includes(`<${TASK_NOTIFICATION_TAG}`)) {
|
||||
return <UserAgentNotificationMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
|
||||
// MCP resource and polling update notifications
|
||||
if (
|
||||
param.text.includes('<mcp-resource-update') ||
|
||||
param.text.includes('<mcp-polling-update')
|
||||
) {
|
||||
return <UserResourceUpdateMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
|
||||
// Fork child's first message: collapse the rules/format boilerplate, show
|
||||
// only the directive. FORK_BOILERPLATE_TAG is inlined so the import doesn't
|
||||
// ship in external builds where feature('FORK_SUBAGENT') is false.
|
||||
if (feature('FORK_SUBAGENT')) {
|
||||
if (param.text.includes('<fork-boilerplate>')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { UserForkBoilerplateMessage } =
|
||||
require('./UserForkBoilerplateMessage.js') as typeof import('./UserForkBoilerplateMessage.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
return <UserForkBoilerplateMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-session UDS message (from another Claude session's SendMessage).
|
||||
// CROSS_SESSION_MESSAGE_TAG is inlined so the import doesn't ship in
|
||||
// external builds where feature('UDS_INBOX') is false.
|
||||
if (feature('UDS_INBOX')) {
|
||||
if (param.text.includes('<cross-session-message')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { UserCrossSessionMessage } =
|
||||
require('./UserCrossSessionMessage.js') as typeof import('./UserCrossSessionMessage.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
return <UserCrossSessionMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
}
|
||||
|
||||
// Inbound channel message (MCP server push).
|
||||
if (feature('KAIROS') || feature('KAIROS_CHANNELS')) {
|
||||
if (param.text.includes('<channel source="')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { UserChannelMessage } =
|
||||
require('./UserChannelMessage.js') as typeof import('./UserChannelMessage.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
return <UserChannelMessage addMargin={addMargin} param={param} />
|
||||
}
|
||||
}
|
||||
|
||||
// User prompts>
|
||||
return (
|
||||
<UserPromptMessage
|
||||
addMargin={addMargin}
|
||||
param={param}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Markdown } from 'src/components/Markdown.js';
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js';
|
||||
import { Box, Text } from '../../../ink.js';
|
||||
import * as React from 'react'
|
||||
import { Markdown } from 'src/components/Markdown.js'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
import { Box, Text } from '../../../ink.js'
|
||||
|
||||
type Props = {
|
||||
plan: string;
|
||||
};
|
||||
export function RejectedPlanMessage(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
plan
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text color="subtle">User rejected Claude's plan:</Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== plan) {
|
||||
t2 = <MessageResponse><Box flexDirection="column">{t1}<Box borderStyle="round" borderColor="planMode" paddingX={1} overflow="hidden"><Markdown>{plan}</Markdown></Box></Box></MessageResponse>;
|
||||
$[1] = plan;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
plan: string
|
||||
}
|
||||
|
||||
export function RejectedPlanMessage({ plan }: Props): React.ReactNode {
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
<Text color="subtle">User rejected Claude's plan:</Text>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor="planMode"
|
||||
paddingX={1}
|
||||
// Necessary for Windows Terminal to render properly
|
||||
overflow="hidden"
|
||||
>
|
||||
<Markdown>{plan}</Markdown>
|
||||
</Box>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { MessageResponse } from '../../MessageResponse.js';
|
||||
export function RejectedToolUseMessage() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <MessageResponse height={1}><Text dimColor={true}>Tool use rejected</Text></MessageResponse>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
import * as React from 'react'
|
||||
import { Text } from '../../../ink.js'
|
||||
import { MessageResponse } from '../../MessageResponse.js'
|
||||
|
||||
export function RejectedToolUseMessage(): React.ReactNode {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Tool use rejected</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { InterruptedByUser } from 'src/components/InterruptedByUser.js';
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js';
|
||||
export function UserToolCanceledMessage() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
import * as React from 'react'
|
||||
import { InterruptedByUser } from 'src/components/InterruptedByUser.js'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
|
||||
export function UserToolCanceledMessage(): React.ReactNode {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<InterruptedByUser />
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,102 +1,95 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import { BULLET_OPERATOR } from '../../../constants/figures.js';
|
||||
import { Text } from '../../../ink.js';
|
||||
import { filterToolProgressMessages, type Tool, type Tools } from '../../../Tool.js';
|
||||
import type { ProgressMessage } from '../../../types/message.js';
|
||||
import { INTERRUPT_MESSAGE_FOR_TOOL_USE, isClassifierDenial, PLAN_REJECTION_PREFIX, REJECT_MESSAGE_WITH_REASON_PREFIX } from '../../../utils/messages.js';
|
||||
import { FallbackToolUseErrorMessage } from '../../FallbackToolUseErrorMessage.js';
|
||||
import { InterruptedByUser } from '../../InterruptedByUser.js';
|
||||
import { MessageResponse } from '../../MessageResponse.js';
|
||||
import { RejectedPlanMessage } from './RejectedPlanMessage.js';
|
||||
import { RejectedToolUseMessage } from './RejectedToolUseMessage.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import { BULLET_OPERATOR } from '../../../constants/figures.js'
|
||||
import { Text } from '../../../ink.js'
|
||||
import {
|
||||
filterToolProgressMessages,
|
||||
type Tool,
|
||||
type Tools,
|
||||
} from '../../../Tool.js'
|
||||
import type { ProgressMessage } from '../../../types/message.js'
|
||||
import {
|
||||
INTERRUPT_MESSAGE_FOR_TOOL_USE,
|
||||
isClassifierDenial,
|
||||
PLAN_REJECTION_PREFIX,
|
||||
REJECT_MESSAGE_WITH_REASON_PREFIX,
|
||||
} from '../../../utils/messages.js'
|
||||
import { FallbackToolUseErrorMessage } from '../../FallbackToolUseErrorMessage.js'
|
||||
import { InterruptedByUser } from '../../InterruptedByUser.js'
|
||||
import { MessageResponse } from '../../MessageResponse.js'
|
||||
import { RejectedPlanMessage } from './RejectedPlanMessage.js'
|
||||
import { RejectedToolUseMessage } from './RejectedToolUseMessage.js'
|
||||
|
||||
type Props = {
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
tool?: Tool; // undefined when resuming an old conversation that uses an old tool
|
||||
tools: Tools;
|
||||
param: ToolResultBlockParam;
|
||||
verbose: boolean;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function UserToolErrorMessage(t0) {
|
||||
const $ = _c(14);
|
||||
const {
|
||||
progressMessagesForMessage,
|
||||
tool,
|
||||
tools,
|
||||
param,
|
||||
verbose,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
if (typeof param.content === "string" && param.content.includes(INTERRUPT_MESSAGE_FOR_TOOL_USE)) {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
progressMessagesForMessage: ProgressMessage[]
|
||||
tool?: Tool // undefined when resuming an old conversation that uses an old tool
|
||||
tools: Tools
|
||||
param: ToolResultBlockParam
|
||||
verbose: boolean
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function UserToolErrorMessage({
|
||||
progressMessagesForMessage,
|
||||
tool,
|
||||
tools,
|
||||
param,
|
||||
verbose,
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
if (
|
||||
typeof param.content === 'string' &&
|
||||
param.content.includes(INTERRUPT_MESSAGE_FOR_TOOL_USE)
|
||||
) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<InterruptedByUser />
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
if (typeof param.content === "string" && param.content.startsWith(PLAN_REJECTION_PREFIX)) {
|
||||
let t1;
|
||||
if ($[1] !== param.content) {
|
||||
t1 = param.content.substring(PLAN_REJECTION_PREFIX.length);
|
||||
$[1] = param.content;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const planContent = t1;
|
||||
let t2;
|
||||
if ($[3] !== planContent) {
|
||||
t2 = <RejectedPlanMessage plan={planContent} />;
|
||||
$[3] = planContent;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
|
||||
if (
|
||||
typeof param.content === 'string' &&
|
||||
param.content.startsWith(PLAN_REJECTION_PREFIX)
|
||||
) {
|
||||
// Extract the plan content from the error message
|
||||
const planContent = param.content.substring(PLAN_REJECTION_PREFIX.length)
|
||||
return <RejectedPlanMessage plan={planContent} />
|
||||
}
|
||||
if (typeof param.content === "string" && param.content.startsWith(REJECT_MESSAGE_WITH_REASON_PREFIX)) {
|
||||
let t1;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <RejectedToolUseMessage />;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
|
||||
if (
|
||||
typeof param.content === 'string' &&
|
||||
param.content.startsWith(REJECT_MESSAGE_WITH_REASON_PREFIX)
|
||||
) {
|
||||
return <RejectedToolUseMessage />
|
||||
}
|
||||
if (feature("TRANSCRIPT_CLASSIFIER") && typeof param.content === "string" && isClassifierDenial(param.content)) {
|
||||
let t1;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <MessageResponse height={1}><Text dimColor={true}>Denied by auto mode classifier {BULLET_OPERATOR} /feedback if incorrect</Text></MessageResponse>;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
return t1;
|
||||
|
||||
if (
|
||||
feature('TRANSCRIPT_CLASSIFIER') &&
|
||||
typeof param.content === 'string' &&
|
||||
isClassifierDenial(param.content)
|
||||
) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>
|
||||
Denied by auto mode classifier {BULLET_OPERATOR} /feedback if
|
||||
incorrect
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
let t1;
|
||||
if ($[7] !== isTranscriptMode || $[8] !== param.content || $[9] !== progressMessagesForMessage || $[10] !== tool || $[11] !== tools || $[12] !== verbose) {
|
||||
t1 = tool?.renderToolUseErrorMessage?.(param.content, {
|
||||
progressMessagesForMessage: filterToolProgressMessages(progressMessagesForMessage),
|
||||
|
||||
return (
|
||||
tool?.renderToolUseErrorMessage?.(param.content, {
|
||||
progressMessagesForMessage: filterToolProgressMessages(
|
||||
progressMessagesForMessage,
|
||||
),
|
||||
tools,
|
||||
verbose,
|
||||
isTranscriptMode
|
||||
}) ?? <FallbackToolUseErrorMessage result={param.content} verbose={verbose} />;
|
||||
$[7] = isTranscriptMode;
|
||||
$[8] = param.content;
|
||||
$[9] = progressMessagesForMessage;
|
||||
$[10] = tool;
|
||||
$[11] = tools;
|
||||
$[12] = verbose;
|
||||
$[13] = t1;
|
||||
} else {
|
||||
t1 = $[13];
|
||||
}
|
||||
return t1;
|
||||
isTranscriptMode,
|
||||
}) ?? (
|
||||
<FallbackToolUseErrorMessage result={param.content} verbose={verbose} />
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,94 +1,59 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
|
||||
import { useTheme } from '../../../ink.js';
|
||||
import { filterToolProgressMessages, type Tool, type Tools } from '../../../Tool.js';
|
||||
import type { ProgressMessage } from '../../../types/message.js';
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js';
|
||||
import { FallbackToolUseRejectedMessage } from '../../FallbackToolUseRejectedMessage.js';
|
||||
import * as React from 'react'
|
||||
import { useTerminalSize } from '../../../hooks/useTerminalSize.js'
|
||||
import { useTheme } from '../../../ink.js'
|
||||
import {
|
||||
filterToolProgressMessages,
|
||||
type Tool,
|
||||
type Tools,
|
||||
} from '../../../Tool.js'
|
||||
import type { ProgressMessage } from '../../../types/message.js'
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js'
|
||||
import { FallbackToolUseRejectedMessage } from '../../FallbackToolUseRejectedMessage.js'
|
||||
|
||||
type Props = {
|
||||
input: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
style?: 'condensed';
|
||||
tool?: Tool;
|
||||
tools: Tools;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
verbose: boolean;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function UserToolRejectMessage(t0) {
|
||||
const $ = _c(13);
|
||||
const {
|
||||
input,
|
||||
progressMessagesForMessage,
|
||||
style,
|
||||
tool,
|
||||
tools,
|
||||
verbose,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
if (!tool || !tool.renderToolUseRejectedMessage) {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <FallbackToolUseRejectedMessage />;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
const t1 = tool.inputSchema;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[1] !== columns || $[2] !== input || $[3] !== isTranscriptMode || $[4] !== progressMessagesForMessage || $[5] !== style || $[6] !== theme || $[7] !== tool || $[8] !== tools || $[9] !== verbose) {
|
||||
t3 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const parsedInput = t1.safeParse(input);
|
||||
if (!parsedInput.success) {
|
||||
let t4;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <FallbackToolUseRejectedMessage />;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
t3 = t4;
|
||||
break bb0;
|
||||
}
|
||||
t2 = tool.renderToolUseRejectedMessage(parsedInput.data, {
|
||||
columns,
|
||||
messages: [],
|
||||
tools,
|
||||
verbose,
|
||||
progressMessagesForMessage: filterToolProgressMessages(progressMessagesForMessage),
|
||||
style,
|
||||
theme,
|
||||
isTranscriptMode
|
||||
}) ?? <FallbackToolUseRejectedMessage />;
|
||||
}
|
||||
$[1] = columns;
|
||||
$[2] = input;
|
||||
$[3] = isTranscriptMode;
|
||||
$[4] = progressMessagesForMessage;
|
||||
$[5] = style;
|
||||
$[6] = theme;
|
||||
$[7] = tool;
|
||||
$[8] = tools;
|
||||
$[9] = verbose;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
} else {
|
||||
t2 = $[10];
|
||||
t3 = $[11];
|
||||
}
|
||||
if (t3 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t3;
|
||||
}
|
||||
return t2;
|
||||
input: { [key: string]: unknown }
|
||||
progressMessagesForMessage: ProgressMessage[]
|
||||
style?: 'condensed'
|
||||
tool?: Tool
|
||||
tools: Tools
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
verbose: boolean
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function UserToolRejectMessage({
|
||||
input,
|
||||
progressMessagesForMessage,
|
||||
style,
|
||||
tool,
|
||||
tools,
|
||||
verbose,
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const { columns } = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
|
||||
if (!tool || !tool.renderToolUseRejectedMessage) {
|
||||
return <FallbackToolUseRejectedMessage />
|
||||
}
|
||||
|
||||
const parsedInput = tool.inputSchema.safeParse(input)
|
||||
if (!parsedInput.success) {
|
||||
return <FallbackToolUseRejectedMessage />
|
||||
}
|
||||
|
||||
return (
|
||||
tool.renderToolUseRejectedMessage(parsedInput.data, {
|
||||
columns,
|
||||
messages: [],
|
||||
tools,
|
||||
verbose,
|
||||
progressMessagesForMessage: filterToolProgressMessages(
|
||||
progressMessagesForMessage,
|
||||
),
|
||||
style,
|
||||
theme,
|
||||
isTranscriptMode,
|
||||
}) ?? <FallbackToolUseRejectedMessage />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,105 +1,101 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import * as React from 'react';
|
||||
import type { Tools } from '../../../Tool.js';
|
||||
import type { NormalizedUserMessage, ProgressMessage } from '../../../types/message.js';
|
||||
import { type buildMessageLookups, CANCEL_MESSAGE, INTERRUPT_MESSAGE_FOR_TOOL_USE, REJECT_MESSAGE } from '../../../utils/messages.js';
|
||||
import { UserToolCanceledMessage } from './UserToolCanceledMessage.js';
|
||||
import { UserToolErrorMessage } from './UserToolErrorMessage.js';
|
||||
import { UserToolRejectMessage } from './UserToolRejectMessage.js';
|
||||
import { UserToolSuccessMessage } from './UserToolSuccessMessage.js';
|
||||
import { useGetToolFromMessages } from './utils.js';
|
||||
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import * as React from 'react'
|
||||
import type { Tools } from '../../../Tool.js'
|
||||
import type {
|
||||
NormalizedUserMessage,
|
||||
ProgressMessage,
|
||||
} from '../../../types/message.js'
|
||||
import {
|
||||
type buildMessageLookups,
|
||||
CANCEL_MESSAGE,
|
||||
INTERRUPT_MESSAGE_FOR_TOOL_USE,
|
||||
REJECT_MESSAGE,
|
||||
} from '../../../utils/messages.js'
|
||||
import { UserToolCanceledMessage } from './UserToolCanceledMessage.js'
|
||||
import { UserToolErrorMessage } from './UserToolErrorMessage.js'
|
||||
import { UserToolRejectMessage } from './UserToolRejectMessage.js'
|
||||
import { UserToolSuccessMessage } from './UserToolSuccessMessage.js'
|
||||
import { useGetToolFromMessages } from './utils.js'
|
||||
|
||||
type Props = {
|
||||
param: ToolResultBlockParam;
|
||||
message: NormalizedUserMessage;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
style?: 'condensed';
|
||||
tools: Tools;
|
||||
verbose: boolean;
|
||||
width: number | string;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
export function UserToolResultMessage(t0) {
|
||||
const $ = _c(28);
|
||||
const {
|
||||
param,
|
||||
message,
|
||||
lookups,
|
||||
progressMessagesForMessage,
|
||||
style,
|
||||
tools,
|
||||
verbose,
|
||||
width,
|
||||
isTranscriptMode
|
||||
} = t0;
|
||||
const toolUse = useGetToolFromMessages(param.tool_use_id, tools, lookups);
|
||||
if (!toolUse) {
|
||||
return null;
|
||||
}
|
||||
if (typeof param.content === "string" && param.content.startsWith(CANCEL_MESSAGE)) {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <UserToolCanceledMessage />;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
if (typeof param.content === "string" && param.content.startsWith(REJECT_MESSAGE) || param.content === INTERRUPT_MESSAGE_FOR_TOOL_USE) {
|
||||
const t1 = toolUse.toolUse.input as {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
let t2;
|
||||
if ($[1] !== isTranscriptMode || $[2] !== lookups || $[3] !== progressMessagesForMessage || $[4] !== style || $[5] !== t1 || $[6] !== toolUse.tool || $[7] !== tools || $[8] !== verbose) {
|
||||
t2 = <UserToolRejectMessage input={t1} progressMessagesForMessage={progressMessagesForMessage} tool={toolUse.tool} tools={tools} lookups={lookups} style={style} verbose={verbose} isTranscriptMode={isTranscriptMode} />;
|
||||
$[1] = isTranscriptMode;
|
||||
$[2] = lookups;
|
||||
$[3] = progressMessagesForMessage;
|
||||
$[4] = style;
|
||||
$[5] = t1;
|
||||
$[6] = toolUse.tool;
|
||||
$[7] = tools;
|
||||
$[8] = verbose;
|
||||
$[9] = t2;
|
||||
} else {
|
||||
t2 = $[9];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
if (param.is_error) {
|
||||
let t1;
|
||||
if ($[10] !== isTranscriptMode || $[11] !== param || $[12] !== progressMessagesForMessage || $[13] !== toolUse.tool || $[14] !== tools || $[15] !== verbose) {
|
||||
t1 = <UserToolErrorMessage progressMessagesForMessage={progressMessagesForMessage} tool={toolUse.tool} tools={tools} param={param} verbose={verbose} isTranscriptMode={isTranscriptMode} />;
|
||||
$[10] = isTranscriptMode;
|
||||
$[11] = param;
|
||||
$[12] = progressMessagesForMessage;
|
||||
$[13] = toolUse.tool;
|
||||
$[14] = tools;
|
||||
$[15] = verbose;
|
||||
$[16] = t1;
|
||||
} else {
|
||||
t1 = $[16];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
let t1;
|
||||
if ($[17] !== isTranscriptMode || $[18] !== lookups || $[19] !== message || $[20] !== progressMessagesForMessage || $[21] !== style || $[22] !== toolUse.tool || $[23] !== toolUse.toolUse.id || $[24] !== tools || $[25] !== verbose || $[26] !== width) {
|
||||
t1 = <UserToolSuccessMessage message={message} lookups={lookups} toolUseID={toolUse.toolUse.id} progressMessagesForMessage={progressMessagesForMessage} style={style} tool={toolUse.tool} tools={tools} verbose={verbose} width={width} isTranscriptMode={isTranscriptMode} />;
|
||||
$[17] = isTranscriptMode;
|
||||
$[18] = lookups;
|
||||
$[19] = message;
|
||||
$[20] = progressMessagesForMessage;
|
||||
$[21] = style;
|
||||
$[22] = toolUse.tool;
|
||||
$[23] = toolUse.toolUse.id;
|
||||
$[24] = tools;
|
||||
$[25] = verbose;
|
||||
$[26] = width;
|
||||
$[27] = t1;
|
||||
} else {
|
||||
t1 = $[27];
|
||||
}
|
||||
return t1;
|
||||
param: ToolResultBlockParam
|
||||
message: NormalizedUserMessage
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
progressMessagesForMessage: ProgressMessage[]
|
||||
style?: 'condensed'
|
||||
tools: Tools
|
||||
verbose: boolean
|
||||
width: number | string
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function UserToolResultMessage({
|
||||
param,
|
||||
message,
|
||||
lookups,
|
||||
progressMessagesForMessage,
|
||||
style,
|
||||
tools,
|
||||
verbose,
|
||||
width,
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const toolUse = useGetToolFromMessages(param.tool_use_id, tools, lookups)
|
||||
if (!toolUse) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
typeof param.content === 'string' &&
|
||||
param.content.startsWith(CANCEL_MESSAGE)
|
||||
) {
|
||||
return <UserToolCanceledMessage />
|
||||
}
|
||||
|
||||
if (
|
||||
(typeof param.content === 'string' &&
|
||||
param.content.startsWith(REJECT_MESSAGE)) ||
|
||||
param.content === INTERRUPT_MESSAGE_FOR_TOOL_USE
|
||||
) {
|
||||
return (
|
||||
<UserToolRejectMessage
|
||||
input={toolUse.toolUse.input as { [key: string]: unknown }}
|
||||
progressMessagesForMessage={progressMessagesForMessage}
|
||||
tool={toolUse.tool}
|
||||
tools={tools}
|
||||
lookups={lookups}
|
||||
style={style}
|
||||
verbose={verbose}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (param.is_error) {
|
||||
return (
|
||||
<UserToolErrorMessage
|
||||
progressMessagesForMessage={progressMessagesForMessage}
|
||||
tool={toolUse.tool}
|
||||
tools={tools}
|
||||
param={param}
|
||||
verbose={verbose}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<UserToolSuccessMessage
|
||||
message={message}
|
||||
lookups={lookups}
|
||||
toolUseID={toolUse.toolUse.id}
|
||||
progressMessagesForMessage={progressMessagesForMessage}
|
||||
style={style}
|
||||
tool={toolUse.tool}
|
||||
tools={tools}
|
||||
verbose={verbose}
|
||||
width={width}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { SentryErrorBoundary } from 'src/components/SentryErrorBoundary.js';
|
||||
import { Box, Text, useTheme } from '../../../ink.js';
|
||||
import { useAppState } from '../../../state/AppState.js';
|
||||
import { filterToolProgressMessages, type Tool, type Tools } from '../../../Tool.js';
|
||||
import type { NormalizedUserMessage, ProgressMessage } from '../../../types/message.js';
|
||||
import { deleteClassifierApproval, getClassifierApproval, getYoloClassifierApproval } from '../../../utils/classifierApprovals.js';
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js';
|
||||
import { MessageResponse } from '../../MessageResponse.js';
|
||||
import { HookProgressMessage } from '../HookProgressMessage.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { SentryErrorBoundary } from 'src/components/SentryErrorBoundary.js'
|
||||
import { Box, Text, useTheme } from '../../../ink.js'
|
||||
import { useAppState } from '../../../state/AppState.js'
|
||||
import {
|
||||
filterToolProgressMessages,
|
||||
type Tool,
|
||||
type Tools,
|
||||
} from '../../../Tool.js'
|
||||
import type {
|
||||
NormalizedUserMessage,
|
||||
ProgressMessage,
|
||||
} from '../../../types/message.js'
|
||||
import {
|
||||
deleteClassifierApproval,
|
||||
getClassifierApproval,
|
||||
getYoloClassifierApproval,
|
||||
} from '../../../utils/classifierApprovals.js'
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js'
|
||||
import { MessageResponse } from '../../MessageResponse.js'
|
||||
import { HookProgressMessage } from '../HookProgressMessage.js'
|
||||
|
||||
type Props = {
|
||||
message: NormalizedUserMessage;
|
||||
lookups: ReturnType<typeof buildMessageLookups>;
|
||||
toolUseID: string;
|
||||
progressMessagesForMessage: ProgressMessage[];
|
||||
style?: 'condensed';
|
||||
tool?: Tool;
|
||||
tools: Tools;
|
||||
verbose: boolean;
|
||||
width: number | string;
|
||||
isTranscriptMode?: boolean;
|
||||
};
|
||||
message: NormalizedUserMessage
|
||||
lookups: ReturnType<typeof buildMessageLookups>
|
||||
toolUseID: string
|
||||
progressMessagesForMessage: ProgressMessage[]
|
||||
style?: 'condensed'
|
||||
tool?: Tool
|
||||
tools: Tools
|
||||
verbose: boolean
|
||||
width: number | string
|
||||
isTranscriptMode?: boolean
|
||||
}
|
||||
|
||||
export function UserToolSuccessMessage({
|
||||
message,
|
||||
lookups,
|
||||
@@ -32,72 +45,105 @@ export function UserToolSuccessMessage({
|
||||
tools,
|
||||
verbose,
|
||||
width,
|
||||
isTranscriptMode
|
||||
isTranscriptMode,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme();
|
||||
const [theme] = useTheme()
|
||||
// Hook stays inside feature() ternary so external builds don't pay a
|
||||
// per-scrollback-message store subscription — same pattern as
|
||||
// UserPromptMessage.tsx.
|
||||
const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly) : false;
|
||||
const isBriefOnly =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly)
|
||||
: false
|
||||
|
||||
// Capture classifier approval once on mount, then delete from Map to prevent linear growth.
|
||||
// useState lazy initializer ensures the value persists across re-renders.
|
||||
const [classifierRule] = React.useState(() => getClassifierApproval(toolUseID));
|
||||
const [yoloReason] = React.useState(() => getYoloClassifierApproval(toolUseID));
|
||||
const [classifierRule] = React.useState(() =>
|
||||
getClassifierApproval(toolUseID),
|
||||
)
|
||||
const [yoloReason] = React.useState(() =>
|
||||
getYoloClassifierApproval(toolUseID),
|
||||
)
|
||||
React.useEffect(() => {
|
||||
deleteClassifierApproval(toolUseID);
|
||||
}, [toolUseID]);
|
||||
deleteClassifierApproval(toolUseID)
|
||||
}, [toolUseID])
|
||||
|
||||
if (!message.toolUseResult || !tool) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Resumed transcripts deserialize toolUseResult via raw JSON.parse with no
|
||||
// validation (parseJSONL). A partial/corrupt/old-format result crashes
|
||||
// renderToolResultMessage on first field access (anthropics/claude-code#39817).
|
||||
// Validate against outputSchema before rendering — mirrors CollapsedReadSearchContent.
|
||||
const parsedOutput = tool.outputSchema?.safeParse(message.toolUseResult);
|
||||
const parsedOutput = tool.outputSchema?.safeParse(message.toolUseResult)
|
||||
if (parsedOutput && !parsedOutput.success) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const toolResult = parsedOutput?.data ?? message.toolUseResult;
|
||||
const renderedMessage = tool.renderToolResultMessage?.(toolResult as never, filterToolProgressMessages(progressMessagesForMessage), {
|
||||
style,
|
||||
theme,
|
||||
tools,
|
||||
verbose,
|
||||
isTranscriptMode,
|
||||
isBriefOnly,
|
||||
input: lookups.toolUseByToolUseID.get(toolUseID)?.input
|
||||
}) ?? null;
|
||||
const toolResult = parsedOutput?.data ?? message.toolUseResult
|
||||
|
||||
const renderedMessage =
|
||||
tool.renderToolResultMessage?.(
|
||||
toolResult as never,
|
||||
filterToolProgressMessages(progressMessagesForMessage),
|
||||
{
|
||||
style,
|
||||
theme,
|
||||
tools,
|
||||
verbose,
|
||||
isTranscriptMode,
|
||||
isBriefOnly,
|
||||
input: lookups.toolUseByToolUseID.get(toolUseID)?.input,
|
||||
},
|
||||
) ?? null
|
||||
|
||||
// Don't render anything if the tool result message is null
|
||||
if (renderedMessage === null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Tools that return '' from userFacingName opt out of tool chrome and
|
||||
// render like plain assistant text. Skip the tool-result width constraint
|
||||
// so MarkdownTable's SAFETY_MARGIN=4 (tuned for the assistant-text 2-col
|
||||
// dot gutter) holds — otherwise tables wrap their box-drawing chars.
|
||||
const rendersAsAssistantText = tool.userFacingName(undefined) === '';
|
||||
return <Box flexDirection="column">
|
||||
<Box flexDirection="column" width={rendersAsAssistantText ? undefined : width}>
|
||||
const rendersAsAssistantText = tool.userFacingName(undefined) === ''
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box
|
||||
flexDirection="column"
|
||||
width={rendersAsAssistantText ? undefined : width}
|
||||
>
|
||||
{renderedMessage}
|
||||
{feature('BASH_CLASSIFIER') ? classifierRule && <MessageResponse height={1}>
|
||||
{feature('BASH_CLASSIFIER')
|
||||
? classifierRule && (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>
|
||||
<Text color="success">{figures.tick}</Text>
|
||||
{' Auto-approved \u00b7 matched '}
|
||||
{`"${classifierRule}"`}
|
||||
</Text>
|
||||
</MessageResponse> : null}
|
||||
{feature('TRANSCRIPT_CLASSIFIER') ? yoloReason && <MessageResponse height={1}>
|
||||
</MessageResponse>
|
||||
)
|
||||
: null}
|
||||
{feature('TRANSCRIPT_CLASSIFIER')
|
||||
? yoloReason && (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Allowed by auto mode classifier</Text>
|
||||
</MessageResponse> : null}
|
||||
</MessageResponse>
|
||||
)
|
||||
: null}
|
||||
</Box>
|
||||
<SentryErrorBoundary>
|
||||
<HookProgressMessage hookEvent="PostToolUse" lookups={lookups} toolUseID={toolUseID} verbose={verbose} isTranscriptMode={isTranscriptMode} />
|
||||
<HookProgressMessage
|
||||
hookEvent="PostToolUse"
|
||||
lookups={lookups}
|
||||
toolUseID={toolUseID}
|
||||
verbose={verbose}
|
||||
isTranscriptMode={isTranscriptMode}
|
||||
/>
|
||||
</SentryErrorBoundary>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,43 +1,22 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import { useMemo } from 'react';
|
||||
import { findToolByName, type Tool, type Tools } from '../../../Tool.js';
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js';
|
||||
export function useGetToolFromMessages(toolUseID, tools, lookups) {
|
||||
const $ = _c(7);
|
||||
let t0;
|
||||
if ($[0] !== lookups.toolUseByToolUseID || $[1] !== toolUseID || $[2] !== tools) {
|
||||
bb0: {
|
||||
const toolUse = lookups.toolUseByToolUseID.get(toolUseID);
|
||||
if (!toolUse) {
|
||||
t0 = null;
|
||||
break bb0;
|
||||
}
|
||||
const tool = findToolByName(tools, toolUse.name);
|
||||
if (!tool) {
|
||||
t0 = null;
|
||||
break bb0;
|
||||
}
|
||||
let t1;
|
||||
if ($[4] !== tool || $[5] !== toolUse) {
|
||||
t1 = {
|
||||
tool,
|
||||
toolUse
|
||||
};
|
||||
$[4] = tool;
|
||||
$[5] = toolUse;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
t0 = t1;
|
||||
import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import { useMemo } from 'react'
|
||||
import { findToolByName, type Tool, type Tools } from '../../../Tool.js'
|
||||
import type { buildMessageLookups } from '../../../utils/messages.js'
|
||||
|
||||
export function useGetToolFromMessages(
|
||||
toolUseID: string,
|
||||
tools: Tools,
|
||||
lookups: ReturnType<typeof buildMessageLookups>,
|
||||
): { tool: Tool; toolUse: ToolUseBlockParam } | null {
|
||||
return useMemo(() => {
|
||||
const toolUse = lookups.toolUseByToolUseID.get(toolUseID)
|
||||
if (!toolUse) {
|
||||
return null
|
||||
}
|
||||
$[0] = lookups.toolUseByToolUseID;
|
||||
$[1] = toolUseID;
|
||||
$[2] = tools;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
}
|
||||
return t0;
|
||||
const tool = findToolByName(tools, toolUse.name)
|
||||
if (!tool) {
|
||||
return null
|
||||
}
|
||||
return { tool, toolUse }
|
||||
}, [toolUseID, lookups, tools])
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
import type { CollapsedReadSearchGroup } from '../../types/message.js';
|
||||
import React from 'react'
|
||||
import { Text } from '../../ink.js'
|
||||
import type { CollapsedReadSearchGroup } from '../../types/message.js'
|
||||
|
||||
/**
|
||||
* Plain function (not a React component) so the React Compiler won't
|
||||
@@ -9,7 +8,11 @@ import type { CollapsedReadSearchGroup } from '../../types/message.js';
|
||||
* is only loaded when feature('TEAMMEM') is true.
|
||||
*/
|
||||
export function checkHasTeamMemOps(message: CollapsedReadSearchGroup): boolean {
|
||||
return (message.teamMemorySearchCount ?? 0) > 0 || (message.teamMemoryReadCount ?? 0) > 0 || (message.teamMemoryWriteCount ?? 0) > 0;
|
||||
return (
|
||||
(message.teamMemorySearchCount ?? 0) > 0 ||
|
||||
(message.teamMemoryReadCount ?? 0) > 0 ||
|
||||
(message.teamMemoryWriteCount ?? 0) > 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,123 +20,79 @@ export function checkHasTeamMemOps(message: CollapsedReadSearchGroup): boolean {
|
||||
* This module is only loaded when feature('TEAMMEM') is true,
|
||||
* so DCE removes it entirely from external builds.
|
||||
*/
|
||||
export function TeamMemCountParts(t0) {
|
||||
const $ = _c(23);
|
||||
const {
|
||||
message,
|
||||
isActiveGroup,
|
||||
hasPrecedingParts
|
||||
} = t0;
|
||||
const tmReadCount = message.teamMemoryReadCount ?? 0;
|
||||
const tmSearchCount = message.teamMemorySearchCount ?? 0;
|
||||
const tmWriteCount = message.teamMemoryWriteCount ?? 0;
|
||||
export function TeamMemCountParts({
|
||||
message,
|
||||
isActiveGroup,
|
||||
hasPrecedingParts,
|
||||
}: {
|
||||
message: CollapsedReadSearchGroup
|
||||
isActiveGroup: boolean | undefined
|
||||
hasPrecedingParts: boolean
|
||||
}): React.ReactNode {
|
||||
const tmReadCount = message.teamMemoryReadCount ?? 0
|
||||
const tmSearchCount = message.teamMemorySearchCount ?? 0
|
||||
const tmWriteCount = message.teamMemoryWriteCount ?? 0
|
||||
|
||||
if (tmReadCount === 0 && tmSearchCount === 0 && tmWriteCount === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t1;
|
||||
if ($[0] !== hasPrecedingParts || $[1] !== isActiveGroup || $[2] !== tmReadCount || $[3] !== tmSearchCount || $[4] !== tmWriteCount) {
|
||||
const nodes = [];
|
||||
let count = hasPrecedingParts ? 1 : 0;
|
||||
if (tmReadCount > 0) {
|
||||
const verb = isActiveGroup ? count === 0 ? "Recalling" : "recalling" : count === 0 ? "Recalled" : "recalled";
|
||||
if (count > 0) {
|
||||
let t2;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text key="comma-tmr">, </Text>;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
nodes.push(t2);
|
||||
}
|
||||
let t2;
|
||||
if ($[7] !== tmReadCount) {
|
||||
t2 = <Text bold={true}>{tmReadCount}</Text>;
|
||||
$[7] = tmReadCount;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
const t3 = tmReadCount === 1 ? "memory" : "memories";
|
||||
let t4;
|
||||
if ($[9] !== t2 || $[10] !== t3 || $[11] !== verb) {
|
||||
t4 = <Text key="team-mem-read">{verb} {t2} team{" "}{t3}</Text>;
|
||||
$[9] = t2;
|
||||
$[10] = t3;
|
||||
$[11] = verb;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
nodes.push(t4);
|
||||
count++;
|
||||
|
||||
const nodes: React.ReactNode[] = []
|
||||
let count = hasPrecedingParts ? 1 : 0
|
||||
|
||||
if (tmReadCount > 0) {
|
||||
const verb = isActiveGroup
|
||||
? count === 0
|
||||
? 'Recalling'
|
||||
: 'recalling'
|
||||
: count === 0
|
||||
? 'Recalled'
|
||||
: 'recalled'
|
||||
if (count > 0) {
|
||||
nodes.push(<Text key="comma-tmr">, </Text>)
|
||||
}
|
||||
if (tmSearchCount > 0) {
|
||||
const verb_0 = isActiveGroup ? count === 0 ? "Searching" : "searching" : count === 0 ? "Searched" : "searched";
|
||||
if (count > 0) {
|
||||
let t2;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text key="comma-tms">, </Text>;
|
||||
$[13] = t2;
|
||||
} else {
|
||||
t2 = $[13];
|
||||
}
|
||||
nodes.push(t2);
|
||||
}
|
||||
const t2 = `${verb_0} team memories`;
|
||||
let t3;
|
||||
if ($[14] !== t2) {
|
||||
t3 = <Text key="team-mem-search">{t2}</Text>;
|
||||
$[14] = t2;
|
||||
$[15] = t3;
|
||||
} else {
|
||||
t3 = $[15];
|
||||
}
|
||||
nodes.push(t3);
|
||||
count++;
|
||||
}
|
||||
if (tmWriteCount > 0) {
|
||||
const verb_1 = isActiveGroup ? count === 0 ? "Writing" : "writing" : count === 0 ? "Wrote" : "wrote";
|
||||
if (count > 0) {
|
||||
let t2;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text key="comma-tmw">, </Text>;
|
||||
$[16] = t2;
|
||||
} else {
|
||||
t2 = $[16];
|
||||
}
|
||||
nodes.push(t2);
|
||||
}
|
||||
let t2;
|
||||
if ($[17] !== tmWriteCount) {
|
||||
t2 = <Text bold={true}>{tmWriteCount}</Text>;
|
||||
$[17] = tmWriteCount;
|
||||
$[18] = t2;
|
||||
} else {
|
||||
t2 = $[18];
|
||||
}
|
||||
const t3 = tmWriteCount === 1 ? "memory" : "memories";
|
||||
let t4;
|
||||
if ($[19] !== t2 || $[20] !== t3 || $[21] !== verb_1) {
|
||||
t4 = <Text key="team-mem-write">{verb_1} {t2} team{" "}{t3}</Text>;
|
||||
$[19] = t2;
|
||||
$[20] = t3;
|
||||
$[21] = verb_1;
|
||||
$[22] = t4;
|
||||
} else {
|
||||
t4 = $[22];
|
||||
}
|
||||
nodes.push(t4);
|
||||
}
|
||||
t1 = <>{nodes}</>;
|
||||
$[0] = hasPrecedingParts;
|
||||
$[1] = isActiveGroup;
|
||||
$[2] = tmReadCount;
|
||||
$[3] = tmSearchCount;
|
||||
$[4] = tmWriteCount;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
nodes.push(
|
||||
<Text key="team-mem-read">
|
||||
{verb} <Text bold>{tmReadCount}</Text> team{' '}
|
||||
{tmReadCount === 1 ? 'memory' : 'memories'}
|
||||
</Text>,
|
||||
)
|
||||
count++
|
||||
}
|
||||
return t1;
|
||||
|
||||
if (tmSearchCount > 0) {
|
||||
const verb = isActiveGroup
|
||||
? count === 0
|
||||
? 'Searching'
|
||||
: 'searching'
|
||||
: count === 0
|
||||
? 'Searched'
|
||||
: 'searched'
|
||||
if (count > 0) {
|
||||
nodes.push(<Text key="comma-tms">, </Text>)
|
||||
}
|
||||
nodes.push(<Text key="team-mem-search">{`${verb} team memories`}</Text>)
|
||||
count++
|
||||
}
|
||||
|
||||
if (tmWriteCount > 0) {
|
||||
const verb = isActiveGroup
|
||||
? count === 0
|
||||
? 'Writing'
|
||||
: 'writing'
|
||||
: count === 0
|
||||
? 'Wrote'
|
||||
: 'wrote'
|
||||
if (count > 0) {
|
||||
nodes.push(<Text key="comma-tmw">, </Text>)
|
||||
}
|
||||
nodes.push(
|
||||
<Text key="team-mem-write">
|
||||
{verb} <Text bold>{tmWriteCount}</Text> team{' '}
|
||||
{tmWriteCount === 1 ? 'memory' : 'memories'}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
|
||||
return <>{nodes}</>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user