mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-21 15:55:50 +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,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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user