Skip to content

Commit 15e6b42

Browse files
committed
Add tool update streaming and improve ACP message rendering
add tool_update messages in backend and frontend types stream intermediate tool updates without persisting to history merge tool result/update into tool call display to reduce noise refine tool result parsing and thought rendering UX tweak ACP dialog/message styles and icons
1 parent a1fc341 commit 15e6b42

7 files changed

Lines changed: 380 additions & 70 deletions

File tree

anycode-backend/src/acp.rs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ pub struct AcpToolResult {
6060
pub result: Value,
6161
}
6262

63+
#[derive(Debug, Clone, Serialize, Deserialize)]
64+
pub struct AcpToolUpdate {
65+
pub id: String,
66+
pub update: Value,
67+
}
68+
6369
#[derive(Debug, Clone, Serialize, Deserialize)]
6470
pub struct AcpPromptState {
6571
pub is_processing: bool,
@@ -103,6 +109,8 @@ pub enum AcpMessage {
103109
ToolCall(AcpToolCall),
104110
#[serde(rename = "tool_result")]
105111
ToolResult(AcpToolResult),
112+
#[serde(rename = "tool_update")]
113+
ToolUpdate(AcpToolUpdate),
106114
#[serde(rename = "prompt_state")]
107115
PromptState(AcpPromptState),
108116
#[serde(rename = "permission_request")]
@@ -521,27 +529,35 @@ impl AcpClientImpl {
521529
info!("ToolCallUpdate received for agent {}: tool_call_id={:?}, status={:?}",
522530
self.agent_id, update.tool_call_id, update.fields.status);
523531

524-
// If tool call is completed, send tool result
525-
if let Some(acp::ToolCallStatus::Completed) = update.fields.status {
526-
let tool_call_id = update.tool_call_id.to_string();
527-
528-
// Serialize entire update to JSON string and put in result
529-
let result = serde_json::to_value(&update)
530-
.unwrap_or_else(|_| serde_json::json!({}));
531-
532-
let acp_tool_result = AcpToolResult {
533-
id: tool_call_id,
534-
result: result,
535-
};
536-
537-
// Add to history
538-
{
539-
let mut history = self.history.lock().await;
540-
history.push(AcpMessage::ToolResult(acp_tool_result.clone()));
532+
let tool_call_id = update.tool_call_id.to_string();
533+
534+
// Serialize entire update to JSON string and put in payload
535+
let payload = serde_json::to_value(&update)
536+
.unwrap_or_else(|_| serde_json::json!({}));
537+
538+
match update.fields.status {
539+
Some(acp::ToolCallStatus::Completed) => {
540+
let acp_tool_result = AcpToolResult {
541+
id: tool_call_id,
542+
result: payload,
543+
};
544+
545+
// Add to history only when completed
546+
{
547+
let mut history = self.history.lock().await;
548+
history.push(AcpMessage::ToolResult(acp_tool_result.clone()));
549+
}
550+
self.send_message(AcpMessage::ToolResult(acp_tool_result)).await;
551+
}
552+
Some(_) => {
553+
let acp_tool_update = AcpToolUpdate {
554+
id: tool_call_id,
555+
update: payload,
556+
};
557+
// Stream intermediate updates without persisting
558+
self.send_message(AcpMessage::ToolUpdate(acp_tool_update)).await;
541559
}
542-
543-
// Send to frontend
544-
self.send_message(AcpMessage::ToolResult(acp_tool_result)).await;
560+
None => {}
545561
}
546562
}
547563

anycode/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,8 +1078,8 @@ const App: React.FC = () => {
10781078
return;
10791079
}
10801080

1081-
// Handle tool_call, tool_result, and permission_request messages
1082-
if (data.item.role === 'tool_call' || data.item.role === 'tool_result' || data.item.role === 'permission_request') {
1081+
// Handle tool_call, tool_result, tool_update, and permission_request messages
1082+
if (data.item.role === 'tool_call' || data.item.role === 'tool_result' || data.item.role === 'tool_update' || data.item.role === 'permission_request') {
10831083
// Follow mode: open file when tool_call has locations
10841084
if (data.item.role === 'tool_call' && followEnabledRef.current) {
10851085
const toolCall = data.item as AcpToolCallMessage;

anycode/components/agent/AcpDialog.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@
185185

186186
.acp-diff-btn svg,
187187
.acp-follow-btn svg {
188-
width: 20px;
188+
width: 15px;
189189
height: 20px;
190190
}
191191

anycode/components/agent/AcpMessage.css

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@
9191
}
9292

9393
.acp-message-thought .acp-message-content {
94-
background-color: var(--assistant-message-bg, #2a2a2a);
94+
/* background-color: var(--assistant-message-bg, #2a2a2a); */
9595
color: var(--text-color-secondary, #888);
96-
border: 1px solid var(--border-color, #333);
96+
/* border: 1px solid var(--border-color, #333); */
9797
font-size: 11px;
9898
font-style: normal;
9999
}
@@ -105,7 +105,7 @@
105105
.acp-message-tool_call .acp-message-content {
106106
background-color: var(--assistant-message-bg, #2a2a2a);
107107
/* color: var(--text-color, #ccc); */
108-
border: 1px solid var(--border-color, #333);
108+
/* border: 1px solid var(--border-color, #333); */
109109
max-width: 100%;
110110
box-sizing: border-box;
111111
overflow-x: hidden;
@@ -124,25 +124,34 @@
124124
overflow-x: hidden;
125125
}
126126

127+
.acp-message-tool_update {
128+
align-items: flex-start;
129+
}
130+
131+
.acp-message-tool_update .acp-message-content {
132+
background-color: var(--assistant-message-bg, #2a2a2a);
133+
color: var(--text-color, #ccc);
134+
border: 1px solid var(--border-color, #333);
135+
max-width: 100%;
136+
box-sizing: border-box;
137+
overflow-x: hidden;
138+
}
139+
127140
.acp-tool-call-indicator,
128-
.acp-tool-result-indicator {
141+
.acp-tool-result-indicator,
142+
.acp-tool-update-indicator {
129143
font-size: 12px;
130144
font-weight: 600;
131145
cursor: pointer;
132146
user-select: none;
133147
}
134148

135149
.acp-tool-call-indicator:hover,
136-
.acp-tool-result-indicator:hover {
150+
.acp-tool-result-indicator:hover,
151+
.acp-tool-update-indicator:hover {
137152
color: var(--text-color, #ccc);
138153
}
139154

140-
.acp-tool-call-header {
141-
display: flex;
142-
align-items: center;
143-
font-size: 12px;
144-
}
145-
146155
.acp-tool-call-name {
147156
font-size: 11px;
148157
font-weight: 400;
@@ -172,7 +181,8 @@
172181

173182
.acp-tool-call-command,
174183
.acp-tool-call-args,
175-
.acp-tool-result-content {
184+
.acp-tool-result-content,
185+
.acp-tool-update-content {
176186
margin: 0;
177187
border-radius: 4px;
178188
font-size: 12px;
@@ -184,24 +194,63 @@
184194
white-space: pre-wrap;
185195
}
186196

197+
.acp-tool-result-details {
198+
display: flex;
199+
flex-direction: column;
200+
gap: 6px;
201+
}
202+
203+
.acp-tool-result-meta {
204+
display: flex;
205+
flex-direction: column;
206+
gap: 2px;
207+
font-size: 12px;
208+
}
209+
210+
.acp-tool-result-title {
211+
font-weight: 600;
212+
color: var(--text-color, #ccc);
213+
}
214+
215+
.acp-tool-result-status {
216+
font-size: 11px;
217+
color: var(--text-color-secondary, #888);
218+
text-transform: uppercase;
219+
}
220+
221+
.acp-tool-result-text {
222+
font-size: 12px;
223+
color: var(--text-color, #ccc);
224+
white-space: pre-wrap;
225+
}
226+
187227
.acp-tool-call-command {
188228
white-space: pre-wrap;
189229
word-break: break-all;
190230
}
191231

192-
.acp-thought-indicator {
193-
font-size: 11px;
194-
font-weight: 500;
195-
color: var(--text-color-secondary, #888);
196-
margin-bottom: 4px;
197-
font-style: normal;
232+
.acp-tool-call-indicator,
233+
.acp-permission-header {
234+
display: flex;
235+
align-items: center;
236+
gap: 6px;
198237
}
199238

200-
.acp-thought-content {
201-
margin-top: 8px;
239+
.acp-thought-text {
202240
font-size: 11px;
203241
color: var(--text-color-secondary, #888);
204242
line-height: 1.4;
243+
white-space: pre-wrap;
244+
}
245+
246+
.acp-thought-text-collapsed {
247+
white-space: nowrap;
248+
overflow: hidden;
249+
text-overflow: ellipsis;
250+
}
251+
252+
.acp-thought-toggle-inline {
253+
margin-right: 6px;
205254
}
206255

207256
/* Permission Request Styles */

0 commit comments

Comments
 (0)