diff --git a/agent/dashboard/message_adapter.py b/agent/dashboard/message_adapter.py index 1180fcdf..d6314eca 100644 --- a/agent/dashboard/message_adapter.py +++ b/agent/dashboard/message_adapter.py @@ -36,6 +36,10 @@ def _message_type(message: dict[str, Any]) -> str: def _tool_kind(name: str) -> str: lowered = name.lower() + if lowered == "slack_thread_reply": + return "slack" + if lowered == "linear_comment": + return "linear" if lowered in _EDIT_TOOLS or any(token in lowered for token in ("edit", "write", "replace")): return "edit" if lowered in _EXECUTE_TOOLS: diff --git a/tests/test_dashboard_message_adapter.py b/tests/test_dashboard_message_adapter.py index b8e112c1..415f437f 100644 --- a/tests/test_dashboard_message_adapter.py +++ b/tests/test_dashboard_message_adapter.py @@ -39,6 +39,39 @@ def test_state_messages_to_ui_maps_user_and_tool_calls() -> None: assert tool_chunk["output"] == "print('hi')" +def test_state_messages_to_ui_tags_slack_and_linear_replies() -> None: + messages = [ + {"type": "human", "id": "u1", "content": "ping"}, + { + "type": "ai", + "id": "a1", + "content": "", + "tool_calls": [ + { + "id": "call-slack", + "name": "slack_thread_reply", + "args": {"message": "Done! Opened a PR."}, + }, + { + "id": "call-linear", + "name": "linear_comment", + "args": {"comment_body": "Done! Opened a PR.", "ticket_id": "abc"}, + }, + ], + }, + ] + + ui = state_messages_to_ui(messages) + + chunks = ui[-1]["chunks"] + slack_chunk = next(c for c in chunks if c["toolCallId"] == "call-slack") + linear_chunk = next(c for c in chunks if c["toolCallId"] == "call-linear") + assert slack_chunk["toolKind"] == "slack" + assert slack_chunk["input"]["message"] == "Done! Opened a PR." + assert linear_chunk["toolKind"] == "linear" + assert linear_chunk["input"]["comment_body"] == "Done! Opened a PR." + + def test_state_messages_to_ui_merges_agent_turn_and_hides_internal_tools() -> None: messages = [ {"type": "human", "id": "u1", "content": "hello"}, diff --git a/ui/src/components/agents/ported/MessageView.tsx b/ui/src/components/agents/ported/MessageView.tsx index 4934d097..64541f25 100644 --- a/ui/src/components/agents/ported/MessageView.tsx +++ b/ui/src/components/agents/ported/MessageView.tsx @@ -8,6 +8,7 @@ import { CodeBlock } from "./CodeBlock"; import { Markdown } from "./Markdown"; import { ToolExecution } from "./ToolExecution"; import { ShellCommand } from "./ShellCommand"; +import { ReplyCard } from "./ReplyCard"; import type { Chunk, Message, @@ -21,6 +22,7 @@ type RenderItem = | { type: "explored-group"; key: string; id: string; chunks: ToolExecutionChunk[] } | { type: "edit-item"; key: string; chunk: ToolExecutionChunk } | { type: "shell-item"; key: string; chunk: ToolExecutionChunk } + | { type: "reply-item"; key: string; chunk: ToolExecutionChunk } | { type: "tool-item"; key: string; chunk: ToolExecutionChunk }; function getChunkRenderKey(chunk: Chunk, sourceIndex: number): string { @@ -61,6 +63,10 @@ function isShellTool(chunk: ToolExecutionChunk): boolean { return chunk.toolKind === "execute"; } +function isReplyTool(chunk: ToolExecutionChunk): boolean { + return chunk.toolKind === "slack" || chunk.toolKind === "linear"; +} + function buildRenderItems(chunks: Chunk[]): RenderItem[] { const items: RenderItem[] = []; let exploredBuffer: ToolExecutionChunk[] = []; @@ -96,6 +102,8 @@ function buildRenderItems(chunks: Chunk[]): RenderItem[] { items.push({ type: "edit-item", key: `tool-${chunk.toolCallId}`, chunk }); } else if (isShellTool(chunk)) { items.push({ type: "shell-item", key: `tool-${chunk.toolCallId}`, chunk }); + } else if (isReplyTool(chunk)) { + items.push({ type: "reply-item", key: `tool-${chunk.toolCallId}`, chunk }); } else { items.push({ type: "tool-item", key: `tool-${chunk.toolCallId}`, chunk }); } @@ -578,6 +586,13 @@ function AgentMessage({ ); + case "reply-item": + return ( +