Skip to content

Commit 7bef7be

Browse files
committed
Refine ACP dialog UI
1 parent a076845 commit 7bef7be

10 files changed

Lines changed: 178 additions & 102 deletions

File tree

anycode/components/AcpDialog.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,17 @@ const ToolCallMessage: React.FC<ToolCallMessageProps> = ({ message, isExpanded,
6565
const hasArguments = message.arguments &&
6666
JSON.stringify(message.arguments) !== '{}' &&
6767
JSON.stringify(message.arguments) !== '[]';
68+
const displayCommand = message.command?.trim() || message.name;
6869

6970
return (
7071
<div className="acp-message acp-message-tool_call">
7172
<div className="acp-message-content">
72-
<div className="acp-tool-call-indicator" onClick={onToggle} style={{ cursor: 'pointer' }}>
73+
<div className="acp-tool-call-toggle" onClick={onToggle} style={{ cursor: 'pointer' }}>
7374
<div className="acp-tool-call-header">
7475
<span className="acp-toggle-icon">{isExpanded ? '▼' : '▶'}</span>
7576
Tool Call:
7677
</div>
77-
<div className="acp-tool-call-name">{message.name}</div>
78+
<div className="acp-tool-call-name">{displayCommand}</div>
7879
</div>
7980

8081
{isExpanded && (
@@ -183,8 +184,6 @@ const useAutoScroll = (messages: AcpMessage[]) => {
183184

184185
if (scrollDirection === 'up') {
185186
userScrolledUpRef.current = true;
186-
} else if (scrollDirection === 'down' && checkIfScrolledToBottom(contentElement)) {
187-
userScrolledUpRef.current = false;
188187
}
189188

190189
isScrolledToBottomRef.current = checkIfScrolledToBottom(contentElement);
@@ -436,4 +435,4 @@ export const AcpDialog: React.FC<AcpDialogProps> = ({
436435
</div>
437436
</div>
438437
);
439-
};
438+
};

anycode/components/agent/AcpAgentsList.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
background: transparent;
5656
outline: none;
5757
border: none;
58+
color: #fff;
5859
padding: 2px;
5960
cursor: pointer;
6061
display: flex;
@@ -103,4 +104,3 @@
103104
height: 20px;
104105
}
105106
}
106-

anycode/components/agent/AcpDialog.css

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,49 @@
228228
}
229229

230230
.acp-dialog-content {
231+
position: relative;
231232
flex: 1;
232-
overflow-y: auto;
233-
overflow-x: hidden;
234233
min-height: 0;
235-
scroll-behavior: smooth;
236234
}
237235

236+
.acp-scroll-to-bottom-btn {
237+
position: absolute;
238+
right: 16px;
239+
bottom: 16px;
240+
width: 36px;
241+
height: 36px;
242+
border: none;
243+
border-radius: 999px;
244+
display: flex;
245+
align-items: center;
246+
justify-content: center;
247+
cursor: pointer;
248+
color: var(--text-color, #ccc);
249+
background: rgba(45, 45, 45, 0.5);
250+
backdrop-filter: blur(2px);
251+
-webkit-backdrop-filter: blur(2px);
252+
transition: background-color 0.2s, transform 0.2s, opacity 0.2s, color 0.2s;
253+
z-index: 1;
254+
}
255+
256+
.acp-scroll-to-bottom-btn:hover {
257+
background-color: var(--hover-bg, #2a2a2a);
258+
transform: translateY(-1px);
259+
}
260+
261+
.acp-scroll-to-bottom-btn:focus,
262+
.acp-scroll-to-bottom-btn:active {
263+
outline: none;
264+
}
265+
266+
.acp-scroll-to-bottom-btn svg {
267+
width: 22px;
268+
height: 22px;
269+
}
270+
271+
@media (max-width: 768px) {
272+
.acp-scroll-to-bottom-btn {
273+
right: 12px;
274+
bottom: 12px;
275+
}
276+
}

anycode/components/agent/AcpDialog.tsx

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,31 @@ import { AcpInput } from './AcpInput';
1212
import { AcpMessages } from './AcpMessages';
1313
import { AcpIcons } from './AcpIcons';
1414

15-
const useAutoScroll = (messages: AcpMessage[]) => {
15+
const useAutoScroll = (messages: AcpMessage[], isProcessing: boolean) => {
1616
const contentRef = useRef<HTMLDivElement>(null);
17-
const isScrolledToBottomRef = useRef(true);
17+
const autoScrollEnabledRef = useRef(true);
1818
const lastScrollTopRef = useRef<number>(0);
19-
const userScrolledUpRef = useRef(false);
19+
const isProgrammaticScrollRef = useRef(false);
20+
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
2021

2122
const checkIfScrolledToBottom = (element: HTMLElement): boolean => {
22-
const threshold = 70;
23-
return element.scrollHeight - element.scrollTop - element.clientHeight <= threshold;
23+
return element.scrollHeight - element.scrollTop - element.clientHeight <= 48;
24+
};
25+
26+
const setAutoScroll = (enabled: boolean) => {
27+
autoScrollEnabledRef.current = enabled;
28+
setAutoScrollEnabled(prev => (prev === enabled ? prev : enabled));
29+
};
30+
31+
const scrollToBottom = (behavior: ScrollBehavior = 'auto') => {
32+
const element = contentRef.current;
33+
if (!element) return;
34+
35+
isProgrammaticScrollRef.current = true;
36+
element.scrollTo({
37+
top: element.scrollHeight,
38+
behavior,
39+
});
2440
};
2541

2642
useEffect(() => {
@@ -29,33 +45,48 @@ const useAutoScroll = (messages: AcpMessage[]) => {
2945

3046
const handleScroll = () => {
3147
const currentScrollTop = contentElement.scrollTop;
32-
const scrollDirection = currentScrollTop < lastScrollTopRef.current ? 'up' : 'down';
33-
48+
const delta = currentScrollTop - lastScrollTopRef.current;
49+
const scrollDirection = delta < -1 ? 'up' : delta > 1 ? 'down' : 'none';
50+
51+
if (isProgrammaticScrollRef.current) {
52+
if (checkIfScrolledToBottom(contentElement)) {
53+
isProgrammaticScrollRef.current = false;
54+
}
55+
lastScrollTopRef.current = currentScrollTop;
56+
return;
57+
}
58+
3459
if (scrollDirection === 'up') {
35-
userScrolledUpRef.current = true;
36-
} else if (scrollDirection === 'down' && checkIfScrolledToBottom(contentElement)) {
37-
userScrolledUpRef.current = false;
60+
setAutoScroll(false);
3861
}
39-
40-
isScrolledToBottomRef.current = checkIfScrolledToBottom(contentElement);
62+
4163
lastScrollTopRef.current = currentScrollTop;
4264
};
4365

4466
contentElement.addEventListener('scroll', handleScroll);
67+
lastScrollTopRef.current = contentElement.scrollTop;
4568
return () => contentElement.removeEventListener('scroll', handleScroll);
4669
}, []);
4770

4871
useEffect(() => {
49-
if (contentRef.current && isScrolledToBottomRef.current && !userScrolledUpRef.current) {
72+
if (!isProcessing) return;
73+
74+
if (autoScrollEnabledRef.current && contentRef.current) {
5075
requestAnimationFrame(() => {
51-
if (contentRef.current) {
52-
contentRef.current.scrollTop = contentRef.current.scrollHeight;
53-
}
76+
scrollToBottom('auto');
5477
});
5578
}
56-
}, [messages]);
79+
}, [messages, isProcessing]);
80+
81+
const enableAutoScroll = () => {
82+
setAutoScroll(true);
83+
84+
requestAnimationFrame(() => {
85+
scrollToBottom('auto');
86+
});
87+
};
5788

58-
return contentRef;
89+
return { contentRef, autoScrollEnabled, enableAutoScroll };
5990
};
6091

6192
const useExpandableItems = () => {
@@ -133,7 +164,7 @@ export const AcpDialog: React.FC<AcpDialogProps> = ({
133164
const { expanded: expandedToolResults, toggle: toggleToolResult } = useExpandableItems();
134165
const { expanded: expandedThoughts, toggle: toggleThought } = useExpandableItems();
135166
const { expanded: expandedPermissions, toggle: togglePermission } = useExpandableItems();
136-
const contentRef = useAutoScroll(messages);
167+
const { contentRef, autoScrollEnabled, enableAutoScroll } = useAutoScroll(messages, isProcessing);
137168

138169
if (!isOpen) return null;
139170

@@ -203,23 +234,35 @@ export const AcpDialog: React.FC<AcpDialogProps> = ({
203234
</div>
204235
</div>
205236

206-
<div className="acp-dialog-content" ref={contentRef}>
207-
<div className="acp-dialog-messages">
208-
<AcpMessages
209-
messages={messages}
210-
toolCalls={toolCalls}
211-
expandedToolCalls={expandedToolCalls}
212-
expandedToolResults={expandedToolResults}
213-
expandedThoughts={expandedThoughts}
214-
expandedPermissions={expandedPermissions}
215-
onToggleToolCall={toggleToolCall}
216-
onToggleToolResult={toggleToolResult}
217-
onToggleThought={toggleThought}
218-
onTogglePermission={togglePermission}
219-
onPermissionResponse={(permissionId, optionId) => onPermissionResponse(agentId, permissionId, optionId)}
220-
onUndoMessage={(message) => onUndoPrompt(agentId, message.checkpoint_id, message.content)}
221-
/>
237+
<div className="acp-dialog-content">
238+
<div className="acp-dialog-messages" ref={contentRef}>
239+
<div className="acp-dialog-messages-inner">
240+
<AcpMessages
241+
messages={messages}
242+
toolCalls={toolCalls}
243+
expandedToolCalls={expandedToolCalls}
244+
expandedToolResults={expandedToolResults}
245+
expandedThoughts={expandedThoughts}
246+
expandedPermissions={expandedPermissions}
247+
onToggleToolCall={toggleToolCall}
248+
onToggleToolResult={toggleToolResult}
249+
onToggleThought={toggleThought}
250+
onTogglePermission={togglePermission}
251+
onPermissionResponse={(permissionId, optionId) => onPermissionResponse(agentId, permissionId, optionId)}
252+
onUndoMessage={(message) => onUndoPrompt(agentId, message.checkpoint_id, message.content)}
253+
/>
254+
</div>
222255
</div>
256+
{!autoScrollEnabled && (
257+
<button
258+
className="acp-scroll-to-bottom-btn"
259+
onClick={enableAutoScroll}
260+
title="Enable auto-scroll"
261+
aria-label="Enable auto-scroll"
262+
>
263+
<AcpIcons.ScrollDown />
264+
</button>
265+
)}
223266
</div>
224267

225268
<AcpInput

anycode/components/agent/AcpIcons.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@ export const AcpIcons = {
1515
),
1616
Send: () => (
1717
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
18-
<circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="1.5" fill="none"/>
19-
<path d="M10 6V14M10 6L6 10M10 6L14 10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
18+
<path d="M10 15.5V5.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
19+
<path d="M5.5 9.5L10 5L14.5 9.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
2020
</svg>
2121
),
2222
Cancel: () => (
2323
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
24-
<circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="1.5" fill="none"/>
25-
<rect x="6" y="6" width="8" height="8" stroke="currentColor" strokeWidth="1.5" fill="none"/>
24+
<rect x="5.5" y="5.5" width="9" height="9" rx="1.5" fill="currentColor"/>
2625
</svg>
2726
),
2827
Close: () => (
@@ -51,5 +50,10 @@ export const AcpIcons = {
5150
<path d="M12,10 C12,11.105 11.105,12 10,12 C8.895,12 8,11.105 8,10 C8,8.895 8.895,8 10,8 C11.105,8 12,8.895 12,10 M10,14 C7.794,14 6,12.206 6,10 C6,7.794 7.794,6 10,6 C12.206,6 14,7.794 14,10 C14,12.206 12.206,14 10,14 M10,4 C6.686,4 4,6.686 4,10 C4,13.314 6.686,16 10,16 C13.314,16 16,13.314 16,10 C16,6.686 13.314,4 10,4 M10,18 C5.589,18 2,14.411 2,10 C2,5.589 5.589,2 10,2 C14.411,2 18,5.589 18,10 C18,14.411 14.411,18 10,18 M10,0 C4.477,0 0,4.477 0,10 C0,15.523 4.477,20 10,20 C15.523,20 20,15.523 20,10 C20,4.477 15.523,0 10,0"/>
5251
</svg>
5352
),
53+
ScrollDown: () => (
54+
<svg width="18" height="18" viewBox="0 0 20 20" fill="none">
55+
<path d="M10 4.5V14.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
56+
<path d="M5.5 10.5L10 15L14.5 10.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
57+
</svg>
58+
),
5459
};
55-

anycode/components/agent/AcpInput.css

Lines changed: 19 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
border: none;
1414

1515
padding: 8px;
16-
/* color: var(--text-color, #ccc); */
16+
color: #fff;
17+
-webkit-text-fill-color: #fff;
1718
font-family: inherit;
1819
font-size: 14px;
1920
resize: none;
@@ -31,48 +32,17 @@
3132
}
3233

3334
.acp-dialog-input textarea::placeholder {
35+
color: rgba(255, 255, 255, 0.55);
36+
-webkit-text-fill-color: rgba(255, 255, 255, 0.55);
3437
font-size: 12px;
3538
}
3639

37-
.acp-send-btn {
38-
background-color: transparent;
39-
color: var(--text-color, #ccc);
40-
border: none;
41-
padding: 0px;
42-
border-radius: 50%;
43-
cursor: pointer;
44-
font-size: 14px;
45-
font-weight: 500;
46-
display: flex;
47-
align-items: center;
48-
justify-content: center;
49-
flex-shrink: 0;
50-
outline: none;
51-
}
52-
53-
.acp-send-btn:focus,
54-
.acp-send-btn:active {
55-
outline: none;
56-
border: none;
57-
}
58-
59-
.acp-send-btn svg {
60-
width: 20px;
61-
height: 20px;
62-
}
63-
64-
.acp-send-btn:hover:not(:disabled) {
65-
background-color: var(--hover-bg, #2a2a2a);
66-
}
67-
68-
.acp-send-btn:disabled {
69-
opacity: 0.5;
70-
cursor: not-allowed;
71-
}
72-
40+
.acp-send-btn,
7341
.acp-stop-prompt-btn {
74-
background-color: transparent;
75-
color: var(--text-color, #ccc);
42+
background: rgba(45, 45, 45, 0.5);
43+
backdrop-filter: blur(2px);
44+
-webkit-backdrop-filter: blur(2px);
45+
color: #fff;
7646
border: none;
7747
padding: 0px;
7848
border-radius: 50%;
@@ -82,25 +52,34 @@
8252
display: flex;
8353
align-items: center;
8454
justify-content: center;
55+
width: 36px;
56+
height: 36px;
8557
flex-shrink: 0;
8658
outline: none;
59+
transition: background-color 0.2s, transform 0.2s, opacity 0.2s, color 0.2s;
8760
}
8861

62+
.acp-send-btn:focus,
63+
.acp-send-btn:active,
8964
.acp-stop-prompt-btn:focus,
9065
.acp-stop-prompt-btn:active {
9166
outline: none;
9267
border: none;
9368
}
9469

70+
.acp-send-btn svg,
9571
.acp-stop-prompt-btn svg {
9672
width: 20px;
9773
height: 20px;
9874
}
9975

76+
.acp-send-btn:hover:not(:disabled),
10077
.acp-stop-prompt-btn:hover:not(:disabled) {
10178
background-color: var(--hover-bg, #2a2a2a);
79+
transform: translateY(-1px);
10280
}
10381

82+
.acp-send-btn:disabled,
10483
.acp-stop-prompt-btn:disabled {
10584
opacity: 0.5;
10685
cursor: not-allowed;
@@ -111,4 +90,4 @@
11190
.acp-dialog-input textarea {
11291
font-size: 16px;
11392
}
114-
}
93+
}

0 commit comments

Comments
 (0)