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 (
-

- );
- }
- 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 (
+

+ );
+ }
+ if (isAudio) {
+ return (
+
+ );
+ }
return (
-
+ className="acp-message-attachment-file"
+ title={attachment.mime_type}
+ >
+ {attachment.name}
+
);
- }
- 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)) {