Skip to content

Commit 004e31b

Browse files
committed
Refactor: split some components out of message-block
1 parent d94900c commit 004e31b

File tree

6 files changed

+557
-503
lines changed

6 files changed

+557
-503
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { memo, useCallback } from 'react'
2+
3+
import { pluralize } from '@codebuff/common/util/string'
4+
import { ToolCallItem } from '../tools/tool-call-item'
5+
import { useTheme } from '../../hooks/use-theme'
6+
import type { ContentBlock } from '../../types/chat'
7+
8+
interface AgentListBranchProps {
9+
agentListBlock: Extract<ContentBlock, { type: 'agent-list' }>
10+
keyPrefix: string
11+
collapsedAgents: Set<string>
12+
onToggleCollapsed: (id: string) => void
13+
}
14+
15+
export const AgentListBranch = memo(
16+
({
17+
agentListBlock,
18+
keyPrefix,
19+
collapsedAgents,
20+
onToggleCollapsed,
21+
}: AgentListBranchProps) => {
22+
const theme = useTheme()
23+
const isCollapsed = collapsedAgents.has(agentListBlock.id)
24+
const { agents } = agentListBlock
25+
26+
const sortedAgents = [...agents].sort((a, b) => {
27+
const displayNameComparison = (a.displayName || '')
28+
.toLowerCase()
29+
.localeCompare((b.displayName || '').toLowerCase())
30+
31+
return (
32+
displayNameComparison ||
33+
a.id.toLowerCase().localeCompare(b.id.toLowerCase())
34+
)
35+
})
36+
37+
const agentCount = sortedAgents.length
38+
39+
const formatIdentifier = useCallback(
40+
(agent: { id: string; displayName: string }) =>
41+
agent.displayName && agent.displayName !== agent.id
42+
? `${agent.displayName} (${agent.id})`
43+
: agent.displayName || agent.id,
44+
[],
45+
)
46+
47+
const headerText = pluralize(agentCount, 'local agent')
48+
49+
const handleToggle = useCallback(() => {
50+
onToggleCollapsed(agentListBlock.id)
51+
}, [onToggleCollapsed, agentListBlock.id])
52+
53+
return (
54+
<box key={keyPrefix}>
55+
<ToolCallItem
56+
name={headerText}
57+
content={
58+
<box style={{ flexDirection: 'column', gap: 0 }}>
59+
{sortedAgents.map((agent, idx) => {
60+
const identifier = formatIdentifier(agent)
61+
return (
62+
<text
63+
key={`agent-${idx}`}
64+
style={{ wrapMode: 'word', fg: theme.foreground }}
65+
>
66+
{`• ${identifier}`}
67+
</text>
68+
)
69+
})}
70+
</box>
71+
}
72+
isCollapsed={isCollapsed}
73+
isStreaming={false}
74+
streamingPreview=""
75+
finishedPreview=""
76+
onToggle={handleToggle}
77+
dense
78+
/>
79+
</box>
80+
)
81+
},
82+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { memo } from 'react'
2+
3+
import {
4+
renderMarkdown,
5+
renderStreamingMarkdown,
6+
hasMarkdown,
7+
type MarkdownPalette,
8+
} from '../../utils/markdown-renderer'
9+
10+
interface ContentWithMarkdownProps {
11+
content: string
12+
isStreaming: boolean
13+
codeBlockWidth: number
14+
palette: MarkdownPalette
15+
}
16+
17+
export const ContentWithMarkdown = memo(
18+
({
19+
content,
20+
isStreaming,
21+
codeBlockWidth,
22+
palette,
23+
}: ContentWithMarkdownProps) => {
24+
if (!hasMarkdown(content)) {
25+
return content
26+
}
27+
const options = { codeBlockWidth, palette }
28+
if (isStreaming) {
29+
return renderStreamingMarkdown(content, options)
30+
}
31+
return renderMarkdown(content, options)
32+
},
33+
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { memo, useCallback } from 'react'
2+
3+
import { Thinking } from '../thinking'
4+
import type { ContentBlock } from '../../types/chat'
5+
6+
interface ThinkingBlockProps {
7+
blocks: Extract<ContentBlock, { type: 'text' }>[]
8+
keyPrefix: string
9+
startIndex: number
10+
indentLevel: number
11+
collapsedAgents: Set<string>
12+
setCollapsedAgents: (value: (prev: Set<string>) => Set<string>) => void
13+
autoCollapsedAgents: Set<string>
14+
addAutoCollapsedAgent: (value: string) => void
15+
onToggleCollapsed: (id: string) => void
16+
availableWidth: number
17+
}
18+
19+
export const ThinkingBlock = memo(
20+
({
21+
blocks,
22+
keyPrefix,
23+
startIndex,
24+
indentLevel,
25+
collapsedAgents,
26+
setCollapsedAgents,
27+
autoCollapsedAgents,
28+
addAutoCollapsedAgent,
29+
onToggleCollapsed,
30+
availableWidth,
31+
}: ThinkingBlockProps) => {
32+
const thinkingId = `${keyPrefix}-thinking-${startIndex}`
33+
const combinedContent = blocks
34+
.map((b) => b.content)
35+
.join('')
36+
.trim()
37+
38+
if (!autoCollapsedAgents.has(thinkingId)) {
39+
addAutoCollapsedAgent(thinkingId)
40+
setCollapsedAgents((prev) => new Set(prev).add(thinkingId))
41+
}
42+
43+
const isCollapsed = collapsedAgents.has(thinkingId)
44+
const marginLeft = Math.max(0, indentLevel * 2)
45+
const availWidth = Math.max(10, availableWidth - marginLeft - 4)
46+
47+
const handleToggle = useCallback(() => {
48+
onToggleCollapsed(thinkingId)
49+
}, [onToggleCollapsed, thinkingId])
50+
51+
if (!combinedContent) {
52+
return null
53+
}
54+
55+
return (
56+
<box style={{ marginLeft }}>
57+
<Thinking
58+
content={combinedContent}
59+
isCollapsed={isCollapsed}
60+
onToggle={handleToggle}
61+
availableWidth={availWidth}
62+
/>
63+
</box>
64+
)
65+
},
66+
)
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { memo, useCallback } from 'react'
2+
3+
import { renderToolComponent } from '../tools/registry'
4+
import { ToolCallItem } from '../tools/tool-call-item'
5+
import { useTheme } from '../../hooks/use-theme'
6+
import { getToolDisplayInfo } from '../../utils/codebuff-client'
7+
import type { MarkdownPalette } from '../../utils/markdown-renderer'
8+
import { ContentWithMarkdown } from './content-with-markdown'
9+
import type { ContentBlock } from '../../types/chat'
10+
11+
interface ToolBranchProps {
12+
toolBlock: Extract<ContentBlock, { type: 'tool' }>
13+
indentLevel: number
14+
keyPrefix: string
15+
availableWidth: number
16+
collapsedAgents: Set<string>
17+
streamingAgents: Set<string>
18+
onToggleCollapsed: (id: string) => void
19+
markdownPalette: MarkdownPalette
20+
}
21+
22+
export const ToolBranch = memo(
23+
({
24+
toolBlock,
25+
indentLevel,
26+
keyPrefix,
27+
availableWidth,
28+
collapsedAgents,
29+
streamingAgents,
30+
onToggleCollapsed,
31+
markdownPalette,
32+
}: ToolBranchProps) => {
33+
const theme = useTheme()
34+
35+
const sanitizePreview = (value: string): string =>
36+
value.replace(/[#*_`~\[\]()]/g, '').trim()
37+
38+
if (toolBlock.toolName === 'end_turn') {
39+
return null
40+
}
41+
if ('includeToolCall' in toolBlock && toolBlock.includeToolCall === false) {
42+
return null
43+
}
44+
45+
const displayInfo = getToolDisplayInfo(toolBlock.toolName)
46+
const isCollapsed = collapsedAgents.has(toolBlock.toolCallId)
47+
const isStreaming = streamingAgents.has(toolBlock.toolCallId)
48+
49+
const inputContent = `\`\`\`json\n${JSON.stringify(toolBlock.input, null, 2)}\n\`\`\``
50+
const codeBlockLang =
51+
toolBlock.toolName === 'run_terminal_command' ? '' : 'yaml'
52+
const resultContent = toolBlock.output
53+
? `\n\n**Result:**\n\`\`\`${codeBlockLang}\n${toolBlock.output}\n\`\`\``
54+
: ''
55+
const fullContent = inputContent + resultContent
56+
57+
const lines = fullContent.split('\n').filter((line) => line.trim())
58+
const firstLine = lines[0] || ''
59+
const lastLine = lines[lines.length - 1] || firstLine
60+
const commandPreview =
61+
toolBlock.toolName === 'run_terminal_command' &&
62+
toolBlock.input &&
63+
typeof toolBlock.input === 'object' &&
64+
'command' in toolBlock.input &&
65+
typeof toolBlock.input.command === 'string'
66+
? `$ ${toolBlock.input.command.trim()}`
67+
: null
68+
69+
let toolRenderConfig = renderToolComponent(toolBlock, theme, {
70+
availableWidth,
71+
indentationOffset: 0,
72+
previewPrefix: '',
73+
labelWidth: 0,
74+
})
75+
76+
const streamingPreview = isStreaming
77+
? commandPreview ?? `${sanitizePreview(firstLine)}...`
78+
: ''
79+
80+
const getToolFinishedPreview = useCallback(
81+
(commandPrev: string | null, lastLn: string): string => {
82+
if (commandPrev) {
83+
return commandPrev
84+
}
85+
86+
if (toolBlock.toolName === 'run_terminal_command' && toolBlock.output) {
87+
const outputLines = toolBlock.output
88+
.split('\n')
89+
.filter((line) => line.trim())
90+
const lastThreeLines = outputLines.slice(-3)
91+
const hasMoreLines = outputLines.length > 3
92+
const preview = lastThreeLines.join('\n')
93+
return hasMoreLines ? `...\n${preview}` : preview
94+
}
95+
96+
return sanitizePreview(lastLn)
97+
},
98+
[toolBlock],
99+
)
100+
101+
const finishedPreview = !isStreaming
102+
? toolRenderConfig?.collapsedPreview ??
103+
getToolFinishedPreview(commandPreview, lastLine)
104+
: ''
105+
106+
const indentationOffset = indentLevel * 2
107+
const agentMarkdownOptions = {
108+
codeBlockWidth: Math.max(10, availableWidth - 12 - indentationOffset),
109+
palette: {
110+
...markdownPalette,
111+
codeTextFg: theme.foreground,
112+
},
113+
}
114+
115+
const displayContent = (
116+
<ContentWithMarkdown
117+
content={fullContent}
118+
isStreaming={false}
119+
codeBlockWidth={agentMarkdownOptions.codeBlockWidth}
120+
palette={agentMarkdownOptions.palette}
121+
/>
122+
)
123+
124+
const renderableDisplayContent =
125+
displayContent === null ||
126+
displayContent === undefined ||
127+
displayContent === false ||
128+
displayContent === '' ? null : (
129+
<text
130+
fg={theme.foreground}
131+
style={{ wrapMode: 'word' }}
132+
attributes={
133+
theme.messageTextAttributes && theme.messageTextAttributes !== 0
134+
? theme.messageTextAttributes
135+
: undefined
136+
}
137+
>
138+
{displayContent}
139+
</text>
140+
)
141+
142+
const headerName = displayInfo.name
143+
144+
const handleToggle = useCallback(() => {
145+
onToggleCollapsed(toolBlock.toolCallId)
146+
}, [onToggleCollapsed, toolBlock.toolCallId])
147+
148+
return (
149+
<box key={keyPrefix}>
150+
{toolRenderConfig ? (
151+
toolRenderConfig.content
152+
) : (
153+
<ToolCallItem
154+
name={headerName}
155+
content={renderableDisplayContent}
156+
isCollapsed={isCollapsed}
157+
isStreaming={isStreaming}
158+
streamingPreview={streamingPreview}
159+
finishedPreview={finishedPreview}
160+
onToggle={handleToggle}
161+
titleSuffix={undefined}
162+
/>
163+
)}
164+
</box>
165+
)
166+
},
167+
)

0 commit comments

Comments
 (0)