diff --git a/anycode/components/agent/AcpMessage.css b/anycode/components/agent/AcpMessage.css index 3d20bfa..0e2fcfc 100644 --- a/anycode/components/agent/AcpMessage.css +++ b/anycode/components/agent/AcpMessage.css @@ -240,6 +240,37 @@ color: var(--user-message-fg, var(--theme-accent-foreground, var(--text-color, #ffffff))); } +.acp-user-message-body { + min-width: 0; +} + +.acp-user-message-body-collapsed { + max-height: 180px; + overflow: hidden; +} + +.acp-user-message-toggle { + display: flex; + align-items: center; + gap: 5px; + margin: 5px auto -2px; + padding: 2px 8px; + border: 0; + border-radius: 999px; + background: rgba(0, 0, 0, 0.12); + color: inherit; + font: inherit; + font-size: 0.78em; + cursor: pointer; + opacity: 0.8; +} + +.acp-user-message-toggle:hover, +.acp-user-message-toggle:focus-visible { + opacity: 1; + background: rgba(0, 0, 0, 0.2); +} + .acp-message-assistant .acp-message-content { color: var(--theme-foreground, var(--text-color, #e0e0e0)); } diff --git a/anycode/components/agent/AcpMessage.tsx b/anycode/components/agent/AcpMessage.tsx index 25afac3..70bbe7b 100644 --- a/anycode/components/agent/AcpMessage.tsx +++ b/anycode/components/agent/AcpMessage.tsx @@ -27,6 +27,7 @@ interface AcpMessageProps { toolUpdates?: AcpToolUpdateMessage[]; isExpanded?: boolean; onToggle?: () => void; + onUserMessageToggle?: () => void; onUndo?: () => void; onOpenFile?: (path: string, line?: number, column?: number) => void; onOpenFileDiff?: (path: string, line?: number, column?: number) => void; @@ -943,65 +944,105 @@ const StreamingMarkdownContent: React.FC<{ const TextMessage: React.FC<{ message: AcpUserMessage | AcpAssistantMessage; + onUserMessageToggle?: () => void; onUndo?: () => void; onOpenFile?: (path: string, line?: number, column?: number) => void; onOpenFileDiff?: (path: string, line?: number, column?: number) => void; -}> = ({ message, onUndo, onOpenFile, onOpenFileDiff }) => ( -
-
- - {message.role === 'user' && Array.isArray(message.attachments) && message.attachments.length > 0 && ( -
- {message.attachments.map((attachment, index) => { - const src = `data:${attachment.mime_type};base64,${attachment.data_base64}`; - const isImage = attachment.mime_type.startsWith('image/'); - const isAudio = attachment.mime_type.startsWith('audio/'); - if (isImage) { - return ( - {attachment.name} - ); - } - if (isAudio) { +}> = ({ message, onUserMessageToggle, onUndo, onOpenFile, onOpenFileDiff }) => { + const [isExpanded, setIsExpanded] = React.useState(false); + const [isCollapsible, setIsCollapsible] = React.useState(false); + const bodyRef = React.useRef(null); + + React.useLayoutEffect(() => { + const body = bodyRef.current; + if (!body || message.role !== 'user') { + setIsCollapsible(false); + return; + } + + setIsExpanded(false); + const frame = requestAnimationFrame(() => { + setIsCollapsible(body.scrollHeight > body.clientHeight + 1); + }); + return () => cancelAnimationFrame(frame); + }, [message.content, message.role]); + + return ( +
+
+
+ +
+ {message.role === 'user' && isCollapsible && ( + + )} + {message.role === 'user' && Array.isArray(message.attachments) && message.attachments.length > 0 && ( +
+ {message.attachments.map((attachment, index) => { + const src = `data:${attachment.mime_type};base64,${attachment.data_base64}`; + const isImage = attachment.mime_type.startsWith('image/'); + const isAudio = attachment.mime_type.startsWith('audio/'); + if (isImage) { + return ( + {attachment.name} + ); + } + if (isAudio) { + return ( +
); - } - return ( -
- {attachment.name} -
- ); - })} -
- )} - {message.role === 'user' && onUndo && ( -
- -
- )} + })} +
+ )} + {message.role === 'user' && onUndo && ( +
+ +
+ )} +
-
-); + ); +}; const MediaMessage: React.FC<{ message: AcpMediaMessage; @@ -1089,6 +1130,7 @@ const AcpMessageComponent: React.FC = ({ toolUpdates, isExpanded = false, onToggle, + onUserMessageToggle, onUndo, onOpenFile, onOpenFileDiff, @@ -1128,6 +1170,7 @@ const AcpMessageComponent: React.FC = ({ return ( ( && previous.toolResult === next.toolResult && previous.toolUpdates === next.toolUpdates && previous.isExpanded === next.isExpanded + && previous.onUserMessageToggle === next.onUserMessageToggle && previous.onOpenFile === next.onOpenFile && previous.onOpenFileDiff === next.onOpenFileDiff )); diff --git a/anycode/components/agent/AcpMessages.tsx b/anycode/components/agent/AcpMessages.tsx index ada397e..38dc693 100644 --- a/anycode/components/agent/AcpMessages.tsx +++ b/anycode/components/agent/AcpMessages.tsx @@ -18,6 +18,7 @@ interface AcpMessagesProps { expandedThoughts: Set; activeSearchMessageIndex?: number; onWorkGroupExpansionChange?: () => void; + onUserMessageToggle?: () => void; onToggleToolCall: (index: number) => void; onToggleToolResult: (index: number) => void; onToggleThought: (index: number) => void; @@ -34,6 +35,7 @@ const AcpMessagesComponent: React.FC = ({ expandedThoughts, activeSearchMessageIndex, onWorkGroupExpansionChange, + onUserMessageToggle, onToggleToolCall, onToggleToolResult, onToggleThought, @@ -187,6 +189,7 @@ const AcpMessagesComponent: React.FC = ({ toolUpdates={toolUpdates} onOpenFile={onOpenFile} onOpenFileDiff={onOpenFileDiff} + onUserMessageToggle={onUserMessageToggle} onUndo={ message.role === 'user' && onUndoMessage ? () => onUndoMessage(message) diff --git a/anycode/components/agent/AcpSession.tsx b/anycode/components/agent/AcpSession.tsx index c3b57e6..e78bd8f 100644 --- a/anycode/components/agent/AcpSession.tsx +++ b/anycode/components/agent/AcpSession.tsx @@ -689,6 +689,7 @@ const AcpSessionComponent: React.FC = ({ expandedThoughts={searchExpandedThoughts} activeSearchMessageIndex={activeSearchMessageIndex} onWorkGroupExpansionChange={handleWorkGroupExpansionChange} + onUserMessageToggle={disableAutoScroll} onToggleToolCall={handleToggleToolCall} onToggleToolResult={handleToggleToolResult} onToggleThought={handleToggleThought} diff --git a/anycode/tests/demo.spec.ts b/anycode/tests/demo.spec.ts index 099e200..cf0061a 100644 --- a/anycode/tests/demo.spec.ts +++ b/anycode/tests/demo.spec.ts @@ -336,6 +336,26 @@ test.describe('Anycode Live Demo Mode E2E Tests', () => { } }); + test('should collapse and expand long user messages', async ({ page }) => { + const promptInput = page.getByPlaceholder('Ask anything...'); + if (await promptInput.isVisible({ timeout: 5000 }).catch(() => false)) { + const longPrompt = Array.from({ length: 24 }, (_, index) => `Log line ${index + 1}: repeated diagnostic output`).join('\n'); + await promptInput.fill(longPrompt); + await promptInput.press('Enter'); + + const userMessage = page.locator('.acp-message-user').filter({ hasText: 'Log line 24' }).last(); + const toggle = userMessage.getByRole('button', { name: 'Show more' }); + await expect(toggle).toBeVisible({ timeout: 10000 }); + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + await expect(userMessage.locator('.acp-user-message-body')).toHaveClass(/acp-user-message-body-collapsed/); + + await toggle.click(); + await expect(userMessage.getByRole('button', { name: 'Show less' })).toHaveAttribute('aria-expanded', 'true'); + await expect(userMessage.locator('.acp-user-message-body')).not.toHaveClass(/acp-user-message-body-collapsed/); + await expect(page.getByRole('button', { name: 'Enable auto-scroll' })).toBeVisible(); + } + }); + test('should search ACP text without expanding unrelated work groups', async ({ page }) => { const promptInput = page.getByPlaceholder('Ask anything...'); if (await promptInput.isVisible({ timeout: 5000 }).catch(() => false)) {