fix(copilot): add isLongRunning flag directly to StreamToolInputAvailable

Instead of sending a separate custom event, add isLongRunning boolean
to the existing StreamToolInputAvailable event. This is much simpler
and works with the AI SDK without needing custom event handling.

Backend changes:
- Add isLongRunning field to StreamToolInputAvailable
- Check tool.is_long_running in response_adapter and set the flag
- Remove separate StreamLongRunningStart emission

Frontend changes:
- Check part.isLongRunning directly on the tool part
- Remove message prop from ToolWrapper (no longer needed)
- Simplify detection logic

This approach piggybacks on the existing tool-input-available event
that the AI SDK already recognizes and adds to message.parts.
This commit is contained in:
Zamil Majdy
2026-02-21 19:13:19 +07:00
parent 6a7cd84b26
commit 04ef290273
5 changed files with 15 additions and 37 deletions

View File

@@ -148,6 +148,10 @@ class StreamToolInputAvailable(StreamBaseResponse):
input: dict[str, Any] = Field(
default_factory=dict, description="Tool input arguments"
)
isLongRunning: bool = Field(
default=False,
description="Whether this tool is long-running (triggers UI feedback)",
)
class StreamToolOutputAvailable(StreamBaseResponse):

View File

@@ -34,6 +34,7 @@ from backend.copilot.response_model import (
StreamToolInputStart,
StreamToolOutputAvailable,
)
from backend.copilot.tools import get_tool
from .tool_adapter import MCP_TOOL_PREFIX, pop_pending_tool_output
@@ -111,6 +112,10 @@ class SDKResponseAdapter:
# instead of "mcp__copilot__find_block".
tool_name = block.name.removeprefix(MCP_TOOL_PREFIX)
# Check if this is a long-running tool to trigger UI feedback
tool = get_tool(tool_name)
is_long_running = tool.is_long_running if tool else False
responses.append(
StreamToolInputStart(toolCallId=block.id, toolName=tool_name)
)
@@ -119,6 +124,7 @@ class SDKResponseAdapter:
toolCallId=block.id,
toolName=tool_name,
input=block.input,
isLongRunning=is_long_running,
)
)
self.current_tool_calls[block.id] = {"name": tool_name}

View File

@@ -25,14 +25,12 @@ from ..response_model import (
StreamFinish,
StreamFinishStep,
StreamHeartbeat,
StreamLongRunningStart,
StreamStart,
StreamTextDelta,
StreamToolInputAvailable,
StreamToolOutputAvailable,
)
from ..service import _build_system_prompt, _generate_session_title
from ..tools import get_tool
from ..tools.sandbox import WORKSPACE_PREFIX, make_session_path
from ..tracking import track_user_message
from .response_adapter import SDKResponseAdapter
@@ -733,24 +731,6 @@ async def stream_chat_completion_sdk(
yield response
# Emit long-running notification for tools with is_long_running=True
if isinstance(response, StreamToolInputAvailable):
tool = get_tool(response.toolName)
logger.info(
f"[SDK] Tool check: {response.toolName}, "
f"tool={tool}, is_long_running={tool.is_long_running if tool else 'N/A'}"
)
if tool and tool.is_long_running:
logger.info(
f"[SDK] Emitting StreamLongRunningStart for {response.toolName}"
)
yield StreamLongRunningStart(
data={
"toolCallId": response.toolCallId,
"toolName": response.toolName,
}
)
if isinstance(response, StreamTextDelta):
delta = response.delta or ""
# After tool results, start a new assistant

View File

@@ -212,8 +212,6 @@ export const ChatMessagesContainer = ({
<ToolWrapper
key={`${message.id}-${i}`}
part={part as ToolUIPart}
message={message}
message={message}
>
<FindBlocksTool part={part as ToolUIPart} />
</ToolWrapper>

View File

@@ -1,31 +1,21 @@
import type { ToolUIPart, UIDataTypes, UIMessage, UITools } from "ai";
import type { ToolUIPart } from "ai";
import { LongRunningToolDisplay } from "../LongRunningToolDisplay/LongRunningToolDisplay";
interface Props {
part: ToolUIPart;
message: UIMessage<unknown, UIDataTypes, UITools>;
children: React.ReactNode;
}
/**
* Wrapper for all tool components. Automatically shows UI feedback
* for long-running tools by detecting StreamLongRunningStart events from the backend.
* for long-running tools by detecting the isLongRunning flag on the tool part.
*/
export function ToolWrapper({ part, message, children }: Props) {
export function ToolWrapper({ part, children }: Props) {
const isStreaming =
part.state === "input-streaming" || part.state === "input-available";
// Check if this tool has a data-long-running-start event in the message
const isLongRunning = message.parts.some(
(p) =>
p.type === "data-long-running-start" &&
"data" in p &&
typeof p.data === "object" &&
p.data !== null &&
"toolCallId" in p.data &&
"toolCallId" in part &&
p.data.toolCallId === part.toolCallId,
);
// Check if this tool is marked as long-running in the part itself
const isLongRunning = "isLongRunning" in part && part.isLongRunning === true;
return (
<>