Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions anycode/components/agent/AcpMessage.css
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
148 changes: 96 additions & 52 deletions anycode/components/agent/AcpMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 }) => (
<div className={`acp-message acp-message-${message.role}`}>
<div className="acp-message-content acp-message-content-with-actions">
<StreamingMarkdownContent
content={message.content}
onOpenFile={onOpenFile}
onOpenFileDiff={onOpenFileDiff}
/>
{message.role === 'user' && Array.isArray(message.attachments) && message.attachments.length > 0 && (
<div className="acp-message-attachments">
{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 (
<img
key={`${attachment.name}-${index}`}
src={src}
alt={attachment.name}
className="acp-message-attachment-image"
/>
);
}
if (isAudio) {
}> = ({ message, onUserMessageToggle, onUndo, onOpenFile, onOpenFileDiff }) => {
const [isExpanded, setIsExpanded] = React.useState(false);
const [isCollapsible, setIsCollapsible] = React.useState(false);
const bodyRef = React.useRef<HTMLDivElement>(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 (
<div className={`acp-message acp-message-${message.role}`}>
<div className="acp-message-content acp-message-content-with-actions">
<div
ref={bodyRef}
className={`acp-user-message-body ${message.role === 'user' && !isExpanded ? 'acp-user-message-body-collapsed' : ''}`}
>
<StreamingMarkdownContent
content={message.content}
onOpenFile={onOpenFile}
onOpenFileDiff={onOpenFileDiff}
/>
</div>
{message.role === 'user' && isCollapsible && (
<button
type="button"
className="acp-user-message-toggle"
aria-expanded={isExpanded}
onClick={() => {
onUserMessageToggle?.();
setIsExpanded((expanded) => !expanded);
}}
>
<span aria-hidden="true">{isExpanded ? '▲' : '▼'}</span>
{isExpanded ? 'Show less' : 'Show more'}
</button>
)}
{message.role === 'user' && Array.isArray(message.attachments) && message.attachments.length > 0 && (
<div className="acp-message-attachments">
{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 (
<img
key={`${attachment.name}-${index}`}
src={src}
alt={attachment.name}
className="acp-message-attachment-image"
/>
);
}
if (isAudio) {
return (
<audio
key={`${attachment.name}-${index}`}
controls
src={src}
className="acp-message-attachment-audio"
/>
);
}
return (
<audio
<div
key={`${attachment.name}-${index}`}
controls
src={src}
className="acp-message-attachment-audio"
/>
className="acp-message-attachment-file"
title={attachment.mime_type}
>
{attachment.name}
</div>
);
}
return (
<div
key={`${attachment.name}-${index}`}
className="acp-message-attachment-file"
title={attachment.mime_type}
>
{attachment.name}
</div>
);
})}
</div>
)}
{message.role === 'user' && onUndo && (
<div className="acp-message-actions">
<button className="acp-undo-button" onClick={onUndo} title="Undo">
Undo
</button>
</div>
)}
})}
</div>
)}
{message.role === 'user' && onUndo && (
<div className="acp-message-actions">
<button className="acp-undo-button" onClick={onUndo} title="Undo">
Undo
</button>
</div>
)}
</div>
</div>
</div>
);
);
};

const MediaMessage: React.FC<{
message: AcpMediaMessage;
Expand Down Expand Up @@ -1089,6 +1130,7 @@ const AcpMessageComponent: React.FC<AcpMessageProps> = ({
toolUpdates,
isExpanded = false,
onToggle,
onUserMessageToggle,
onUndo,
onOpenFile,
onOpenFileDiff,
Expand Down Expand Up @@ -1128,6 +1170,7 @@ const AcpMessageComponent: React.FC<AcpMessageProps> = ({
return (
<TextMessage
message={message}
onUserMessageToggle={onUserMessageToggle}
onUndo={onUndo}
onOpenFile={onOpenFile}
onOpenFileDiff={onOpenFileDiff}
Expand Down Expand Up @@ -1170,6 +1213,7 @@ export const AcpMessage = React.memo(AcpMessageComponent, (previous, next) => (
&& previous.toolResult === next.toolResult
&& previous.toolUpdates === next.toolUpdates
&& previous.isExpanded === next.isExpanded
&& previous.onUserMessageToggle === next.onUserMessageToggle
&& previous.onOpenFile === next.onOpenFile
&& previous.onOpenFileDiff === next.onOpenFileDiff
));
3 changes: 3 additions & 0 deletions anycode/components/agent/AcpMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface AcpMessagesProps {
expandedThoughts: Set<number>;
activeSearchMessageIndex?: number;
onWorkGroupExpansionChange?: () => void;
onUserMessageToggle?: () => void;
onToggleToolCall: (index: number) => void;
onToggleToolResult: (index: number) => void;
onToggleThought: (index: number) => void;
Expand All @@ -34,6 +35,7 @@ const AcpMessagesComponent: React.FC<AcpMessagesProps> = ({
expandedThoughts,
activeSearchMessageIndex,
onWorkGroupExpansionChange,
onUserMessageToggle,
onToggleToolCall,
onToggleToolResult,
onToggleThought,
Expand Down Expand Up @@ -187,6 +189,7 @@ const AcpMessagesComponent: React.FC<AcpMessagesProps> = ({
toolUpdates={toolUpdates}
onOpenFile={onOpenFile}
onOpenFileDiff={onOpenFileDiff}
onUserMessageToggle={onUserMessageToggle}
onUndo={
message.role === 'user' && onUndoMessage
? () => onUndoMessage(message)
Expand Down
1 change: 1 addition & 0 deletions anycode/components/agent/AcpSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,7 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
expandedThoughts={searchExpandedThoughts}
activeSearchMessageIndex={activeSearchMessageIndex}
onWorkGroupExpansionChange={handleWorkGroupExpansionChange}
onUserMessageToggle={disableAutoScroll}
onToggleToolCall={handleToggleToolCall}
onToggleToolResult={handleToggleToolResult}
onToggleThought={handleToggleThought}
Expand Down
20 changes: 20 additions & 0 deletions anycode/tests/demo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down