feat(web): add CLI output message block support with layout improvements

Implement CLI output message type for displaying command output from user/assistant messages. Adds CliOutputBlock component and type with detection logic based on message metadata and CLI tags. Includes merging of adjacent CLI output blocks for cleaner presentation. Enhance layout throughout components with proper overflow handling and width constraints for improved text wrapping and scrolling behavior.
This commit is contained in:
weishu
2025-12-21 09:16:46 +08:00
parent 1028547a3a
commit afe0e8b892
16 changed files with 385 additions and 31 deletions
+98 -2
View File
@@ -1,7 +1,11 @@
import type { AgentState } from '@/types/api'
import type { AgentEvent, ChatBlock, ChatToolCall, NormalizedMessage, ToolCallBlock, ToolPermission, UsageData } from '@/chat/types'
import type { AgentEvent, ChatBlock, ChatToolCall, CliOutputBlock, NormalizedMessage, ToolCallBlock, ToolPermission, UsageData } from '@/chat/types'
import { traceMessages, type TracedMessage } from '@/chat/tracer'
const CLI_TAG_REGEX = /<(?:local-command-[a-z-]+|command-(?:name|message|args))>/i
const CLI_COMMAND_NAME_REGEX = /<command-name>/i
const CLI_COMMAND_STDOUT_REGEX = /<local-command-stdout>/i
// Calculate context size from usage data
function calculateContextSize(usage: UsageData): number {
return (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0) + usage.input_tokens
@@ -161,6 +165,76 @@ function getPermissions(agentState: AgentState | null | undefined): Map<string,
return map
}
function getMetaSentFrom(meta: unknown): string | null {
if (!meta || typeof meta !== 'object') return null
const sentFrom = (meta as { sentFrom?: unknown }).sentFrom
return typeof sentFrom === 'string' ? sentFrom : null
}
function hasCliOutputTags(text: string): boolean {
return CLI_TAG_REGEX.test(text)
}
function hasCommandNameTag(text: string): boolean {
return CLI_COMMAND_NAME_REGEX.test(text)
}
function hasLocalCommandStdoutTag(text: string): boolean {
return CLI_COMMAND_STDOUT_REGEX.test(text)
}
function isCliOutputText(text: string, meta: unknown): boolean {
return getMetaSentFrom(meta) === 'cli' && hasCliOutputTags(text)
}
function createCliOutputBlock(props: {
id: string
localId: string | null
createdAt: number
text: string
source: CliOutputBlock['source']
meta?: unknown
}): CliOutputBlock {
return {
kind: 'cli-output',
id: props.id,
localId: props.localId,
createdAt: props.createdAt,
text: props.text,
source: props.source,
meta: props.meta
}
}
function mergeCliOutputBlocks(blocks: ChatBlock[]): ChatBlock[] {
const merged: ChatBlock[] = []
for (const block of blocks) {
if (block.kind !== 'cli-output') {
merged.push(block)
continue
}
const prev = merged[merged.length - 1]
if (
prev
&& prev.kind === 'cli-output'
&& prev.source === block.source
&& hasCommandNameTag(prev.text)
&& !hasLocalCommandStdoutTag(prev.text)
&& hasLocalCommandStdoutTag(block.text)
) {
const separator = prev.text.endsWith('\n') || block.text.startsWith('\n') ? '' : '\n'
merged[merged.length - 1] = { ...prev, text: `${prev.text}${separator}${block.text}` }
continue
}
merged.push(block)
}
return merged
}
function ensureToolBlock(
blocks: ChatBlock[],
toolBlocksById: Map<string, ToolCallBlock>,
@@ -296,6 +370,17 @@ function reduceTimeline(
}
if (msg.role === 'user') {
if (isCliOutputText(msg.content.text, msg.meta)) {
blocks.push(createCliOutputBlock({
id: msg.id,
localId: msg.localId,
createdAt: msg.createdAt,
text: msg.content.text,
source: 'user',
meta: msg.meta
}))
continue
}
blocks.push({
kind: 'user-text',
id: msg.id,
@@ -313,6 +398,17 @@ function reduceTimeline(
for (let idx = 0; idx < msg.content.length; idx += 1) {
const c = msg.content[idx]
if (c.type === 'text') {
if (isCliOutputText(c.text, msg.meta)) {
blocks.push(createCliOutputBlock({
id: `${msg.id}:${idx}`,
localId: msg.localId,
createdAt: msg.createdAt,
text: c.text,
source: 'assistant',
meta: msg.meta
}))
continue
}
blocks.push({
kind: 'agent-text',
id: `${msg.id}:${idx}`,
@@ -447,7 +543,7 @@ function reduceTimeline(
}
}
return { blocks, toolBlocksById, hasReadyEvent }
return { blocks: mergeCliOutputBlocks(blocks), toolBlocksById, hasReadyEvent }
}
export type LatestUsage = {