Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
9 changes: 8 additions & 1 deletion src/components/tools/StatusSetToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ interface StatusSetToolCallProps {

export const StatusSetToolCall: React.FC<StatusSetToolCallProps> = ({
args,
result: _result,
result,
status = "pending",
}) => {
const statusDisplay = getStatusDisplay(status);

// Show error message if validation failed
const errorMessage =
status === "failed" && result && typeof result === "object" && "error" in result
? String(result.error)
: undefined;

return (
<ToolContainer expanded={false}>
<ToolHeader>
Expand All @@ -25,6 +31,7 @@ export const StatusSetToolCall: React.FC<StatusSetToolCallProps> = ({
<Tooltip>status_set</Tooltip>
</TooltipWrapper>
<span className="text-muted-foreground italic">{args.message}</span>
{errorMessage && <span className="text-error-foreground text-sm">({errorMessage})</span>}
<StatusIndicator status={status}>{statusDisplay}</StatusIndicator>
</ToolHeader>
</ToolContainer>
Expand Down
237 changes: 237 additions & 0 deletions src/utils/messages/StreamingMessageAggregator.status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,241 @@ describe("StreamingMessageAggregator - Agent Status", () => {
// Status should be cleared on new stream start
expect(aggregator.getAgentStatus()).toBeUndefined();
});

it("should show 'failed' status in UI when status_set validation fails", () => {
const aggregator = new StreamingMessageAggregator("2024-01-01T00:00:00.000Z");
const messageId = "msg1";

// Start a stream
aggregator.handleStreamStart({
type: "stream-start",
workspaceId: "workspace1",
messageId,
model: "test-model",
historySequence: 1,
});

// Add a status_set tool call with invalid emoji
aggregator.handleToolCallStart({
type: "tool-call-start",
workspaceId: "workspace1",
messageId,
toolCallId: "tool1",
toolName: "status_set",
args: { emoji: "not-an-emoji", message: "test" },
tokens: 10,
timestamp: Date.now(),
});

// Complete with validation failure
aggregator.handleToolCallEnd({
type: "tool-call-end",
workspaceId: "workspace1",
messageId,
toolCallId: "tool1",
toolName: "status_set",
result: { success: false, error: "emoji must be a single emoji character" },
});

// End the stream to finalize message
aggregator.handleStreamEnd({
type: "stream-end",
workspaceId: "workspace1",
messageId,
metadata: { model: "test-model" },
parts: [],
});

// Check that the tool message shows 'failed' status in the UI
const displayedMessages = aggregator.getDisplayedMessages();
const toolMessage = displayedMessages.find((m) => m.type === "tool");
expect(toolMessage).toBeDefined();
expect(toolMessage?.type).toBe("tool");
if (toolMessage?.type === "tool") {
expect(toolMessage.status).toBe("failed");
expect(toolMessage.toolName).toBe("status_set");
}

// And status should NOT be updated in aggregator
expect(aggregator.getAgentStatus()).toBeUndefined();
});

it("should show 'completed' status in UI when status_set validation succeeds", () => {
const aggregator = new StreamingMessageAggregator("2024-01-01T00:00:00.000Z");
const messageId = "msg1";

// Start a stream
aggregator.handleStreamStart({
type: "stream-start",
workspaceId: "workspace1",
messageId,
model: "test-model",
historySequence: 1,
});

// Add a successful status_set tool call
aggregator.handleToolCallStart({
type: "tool-call-start",
workspaceId: "workspace1",
messageId,
toolCallId: "tool1",
toolName: "status_set",
args: { emoji: "πŸ”", message: "Analyzing code" },
tokens: 10,
timestamp: Date.now(),
});

// Complete successfully
aggregator.handleToolCallEnd({
type: "tool-call-end",
workspaceId: "workspace1",
messageId,
toolCallId: "tool1",
toolName: "status_set",
result: { success: true, emoji: "πŸ”", message: "Analyzing code" },
});

// End the stream to finalize message
aggregator.handleStreamEnd({
type: "stream-end",
workspaceId: "workspace1",
messageId,
metadata: { model: "test-model" },
parts: [],
});

// Check that the tool message shows 'completed' status in the UI
const displayedMessages = aggregator.getDisplayedMessages();
const toolMessage = displayedMessages.find((m) => m.type === "tool");
expect(toolMessage).toBeDefined();
expect(toolMessage?.type).toBe("tool");
if (toolMessage?.type === "tool") {
expect(toolMessage.status).toBe("completed");
expect(toolMessage.toolName).toBe("status_set");
}

// And status SHOULD be updated in aggregator
const status = aggregator.getAgentStatus();
expect(status).toBeDefined();
expect(status?.emoji).toBe("πŸ”");
expect(status?.message).toBe("Analyzing code");
});

it("should reconstruct agentStatus when loading historical messages", () => {
const aggregator = new StreamingMessageAggregator("2024-01-01T00:00:00.000Z");

// Create historical messages with a completed status_set tool call
const historicalMessages = [
{
id: "msg1",
role: "user" as const,
parts: [{ type: "text" as const, text: "Hello" }],
metadata: { timestamp: Date.now(), historySequence: 1 },
},
{
id: "msg2",
role: "assistant" as const,
parts: [
{ type: "text" as const, text: "Working on it..." },
{
type: "dynamic-tool" as const,
toolCallId: "tool1",
toolName: "status_set",
state: "output-available" as const,
input: { emoji: "πŸ”", message: "Analyzing code" },
output: { success: true, emoji: "πŸ”", message: "Analyzing code" },
timestamp: Date.now(),
},
],
metadata: { timestamp: Date.now(), historySequence: 2 },
},
];

// Load historical messages
aggregator.loadHistoricalMessages(historicalMessages);

// Status should be reconstructed from the historical tool call
const status = aggregator.getAgentStatus();
expect(status).toBeDefined();
expect(status?.emoji).toBe("πŸ”");
expect(status?.message).toBe("Analyzing code");
});

it("should use most recent status_set when loading multiple historical messages", () => {
const aggregator = new StreamingMessageAggregator("2024-01-01T00:00:00.000Z");

// Create historical messages with multiple status_set calls
const historicalMessages = [
{
id: "msg1",
role: "assistant" as const,
parts: [
{
type: "dynamic-tool" as const,
toolCallId: "tool1",
toolName: "status_set",
state: "output-available" as const,
input: { emoji: "πŸ”", message: "First status" },
output: { success: true, emoji: "πŸ”", message: "First status" },
timestamp: Date.now(),
},
],
metadata: { timestamp: Date.now(), historySequence: 1 },
},
{
id: "msg2",
role: "assistant" as const,
parts: [
{
type: "dynamic-tool" as const,
toolCallId: "tool2",
toolName: "status_set",
state: "output-available" as const,
input: { emoji: "πŸ“", message: "Second status" },
output: { success: true, emoji: "πŸ“", message: "Second status" },
timestamp: Date.now(),
},
],
metadata: { timestamp: Date.now(), historySequence: 2 },
},
];

// Load historical messages
aggregator.loadHistoricalMessages(historicalMessages);

// Should use the most recent (last processed) status
const status = aggregator.getAgentStatus();
expect(status?.emoji).toBe("πŸ“");
expect(status?.message).toBe("Second status");
});

it("should not reconstruct status from failed status_set in historical messages", () => {
const aggregator = new StreamingMessageAggregator("2024-01-01T00:00:00.000Z");

// Create historical message with failed status_set
const historicalMessages = [
{
id: "msg1",
role: "assistant" as const,
parts: [
{
type: "dynamic-tool" as const,
toolCallId: "tool1",
toolName: "status_set",
state: "output-available" as const,
input: { emoji: "not-emoji", message: "test" },
output: { success: false, error: "emoji must be a single emoji character" },
timestamp: Date.now(),
},
],
metadata: { timestamp: Date.now(), historySequence: 1 },
},
];

// Load historical messages
aggregator.loadHistoricalMessages(historicalMessages);

// Status should remain undefined (failed validation)
expect(aggregator.getAgentStatus()).toBeUndefined();
});
});
Loading
Loading