Skip to content

Commit ce1a43f

Browse files
committed
Refactor message handling and enhance streaming support in Claude runner
- Updated the MessageObject structure to support new content block types, including text, tool use, and tool result blocks, improving the flexibility of message handling. - Replaced legacy tool message handling in the frontend with a new StreamMessage component to accommodate the updated message structure. - Enhanced the Claude runner to emit structured messages for streaming, allowing for smoother UI updates and better integration with the assistant's responses. - Removed obsolete fields related to tool usage and updated the logic for appending messages to ensure compatibility with the new structure. - Cleaned up the Dockerfile by removing the requirements.txt file and switching to a pyproject.toml for dependency management, streamlining the build process.
1 parent 67dd219 commit ce1a43f

File tree

15 files changed

+1634
-172
lines changed

15 files changed

+1634
-172
lines changed

.vscode/settings.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"makefile.configureOnOpen": false,
3+
"python.defaultInterpreterPath": "/Users/gkrumbac/Documents/vTeam/components/runners/claude-code-runner/.venv/bin/python",
4+
"python.analysis.extraPaths": [
5+
"/Users/gkrumbac/Documents/vTeam/components/runners/claude-code-runner"
6+
],
7+
"python.analysis.interpreter": [
8+
"/Users/gkrumbac/Documents/vTeam/components/runners/claude-code-runner/.venv/bin/python"
9+
]
10+
}

components/backend/main.go

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,22 @@ type GitConfig struct {
278278
}
279279

280280
type MessageObject struct {
281-
Content string `json:"content,omitempty"`
282-
ToolUseID string `json:"tool_use_id,omitempty"`
283-
ToolUseName string `json:"tool_use_name,omitempty"`
284-
ToolUseInput string `json:"tool_use_input,omitempty"`
285-
ToolUseIsError *bool `json:"tool_use_is_error,omitempty"`
281+
// New structured streaming fields (pass-through from runner)
282+
Type string `json:"type,omitempty"`
283+
Content interface{} `json:"content,omitempty"`
284+
Subtype string `json:"subtype,omitempty"`
285+
Data map[string]interface{} `json:"data,omitempty"`
286+
// Assistant fields
287+
Model string `json:"model,omitempty"`
288+
// Result fields
289+
DurationMs int `json:"duration_ms,omitempty"`
290+
DurationApiMs int `json:"duration_api_ms,omitempty"`
291+
IsError bool `json:"is_error,omitempty"`
292+
NumTurns int `json:"num_turns,omitempty"`
293+
SessionID string `json:"session_id,omitempty"`
294+
TotalCostUSD *float64 `json:"total_cost_usd,omitempty"`
295+
Usage map[string]interface{} `json:"usage,omitempty"`
296+
Result *string `json:"result,omitempty"`
286297
}
287298

288299
type AgenticSessionStatus struct {
@@ -750,26 +761,58 @@ func parseStatus(status map[string]interface{}) *AgenticSessionStatus {
750761
if msgMap, ok := msg.(map[string]interface{}); ok {
751762
messageObj := MessageObject{}
752763

753-
if content, ok := msgMap["content"].(string); ok {
754-
messageObj.Content = content
764+
// New structured fields (optional)
765+
if mt, ok := msgMap["type"].(string); ok {
766+
messageObj.Type = mt
755767
}
756-
757-
if toolUseID, ok := msgMap["tool_use_id"].(string); ok {
758-
messageObj.ToolUseID = toolUseID
768+
if cb, ok := msgMap["content"].([]interface{}); ok {
769+
messageObj.Content = cb
770+
}
771+
if st, ok := msgMap["subtype"].(string); ok {
772+
messageObj.Subtype = st
773+
}
774+
if data, ok := msgMap["data"].(map[string]interface{}); ok {
775+
messageObj.Data = data
759776
}
760777

761-
if toolUseName, ok := msgMap["tool_use_name"].(string); ok {
762-
messageObj.ToolUseName = toolUseName
778+
// Assistant fields
779+
if model, ok := msgMap["model"].(string); ok {
780+
messageObj.Model = model
763781
}
764782

765-
if toolUseInput, ok := msgMap["tool_use_input"].(string); ok {
766-
messageObj.ToolUseInput = toolUseInput
783+
// Result fields
784+
if dms, ok := msgMap["duration_ms"].(float64); ok {
785+
messageObj.DurationMs = int(dms)
786+
}
787+
if dams, ok := msgMap["duration_api_ms"].(float64); ok {
788+
messageObj.DurationApiMs = int(dams)
789+
}
790+
if isErr, ok := msgMap["is_error"].(bool); ok {
791+
messageObj.IsError = isErr
792+
}
793+
if nt, ok := msgMap["num_turns"].(float64); ok {
794+
messageObj.NumTurns = int(nt)
795+
}
796+
if sid, ok := msgMap["session_id"].(string); ok {
797+
messageObj.SessionID = sid
798+
}
799+
if tcu, ok := msgMap["total_cost_usd"].(float64); ok {
800+
messageObj.TotalCostUSD = &tcu
801+
}
802+
if usage, ok := msgMap["usage"].(map[string]interface{}); ok {
803+
messageObj.Usage = usage
804+
}
805+
if res, ok := msgMap["result"].(string); ok {
806+
messageObj.Result = &res
767807
}
768808

769-
if toolUseIsError, ok := msgMap["tool_use_is_error"].(bool); ok {
770-
messageObj.ToolUseIsError = &toolUseIsError
809+
// If content is a string (legacy runner), still store it (overwrites any array set above)
810+
if content, ok := msgMap["content"].(string); ok {
811+
messageObj.Content = content
771812
}
772813

814+
// legacy tool_* fields no longer supported
815+
773816
result.Messages[i] = messageObj
774817
}
775818
}

components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717

1818
// Custom components
1919
import { Message } from "@/components/ui/message";
20-
import { ToolMessage } from "@/components/ui/tool-message";
20+
import { StreamMessage } from "@/components/ui/stream-message";
2121

2222
// Markdown rendering
2323
import ReactMarkdown from "react-markdown";
@@ -489,23 +489,9 @@ export default function ProjectSessionDetailPage({ params }: { params: Promise<{
489489
<CardContent>
490490
<div className="max-h-96 overflow-y-auto space-y-4 bg-gray-50 rounded-lg p-4">
491491
{/* Display all existing messages */}
492-
{session.status?.messages?.map((message, index) => {
493-
const isToolMessage = message.tool_use_id || message.tool_use_name;
494-
if (isToolMessage) {
495-
return (
496-
<ToolMessage key={`tool-${index}-${message.tool_use_id}`} message={message} />
497-
);
498-
} else {
499-
return (
500-
<Message
501-
key={`text-${index}`}
502-
role="bot"
503-
content={message.content || ""}
504-
name="Claude AI"
505-
/>
506-
);
507-
}
508-
})}
492+
{session.status?.messages?.map((message, index) => (
493+
<StreamMessage key={`msg-${index}`} message={message as any} />
494+
))}
509495

510496
{/* Show loading message if still processing */}
511497
{(session.status?.phase === "Running" ||
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from "react";
2+
import { cn } from "@/lib/utils";
3+
import { Badge } from "@/components/ui/badge";
4+
import { CheckCircle2, XCircle } from "lucide-react";
5+
6+
export type ResultMessageProps = {
7+
duration_ms: number;
8+
duration_api_ms: number;
9+
is_error: boolean;
10+
num_turns: number;
11+
session_id: string;
12+
total_cost_usd?: number | null;
13+
usage?: Record<string, any> | null;
14+
result?: string | null;
15+
className?: string;
16+
};
17+
18+
export const ResultMessage: React.FC<ResultMessageProps> = (props) => {
19+
const {
20+
duration_ms,
21+
duration_api_ms,
22+
is_error,
23+
num_turns,
24+
session_id,
25+
total_cost_usd,
26+
usage,
27+
result,
28+
className,
29+
} = props;
30+
31+
return (
32+
<div className={cn("mb-4", className)}>
33+
<div className="bg-white rounded-lg border shadow-sm p-3">
34+
<div className="flex items-center justify-between mb-2">
35+
<Badge variant={is_error ? "destructive" : "secondary"} className="text-xs">
36+
{is_error ? (
37+
<span className="inline-flex items-center"><XCircle className="w-3 h-3 mr-1" /> Error</span>
38+
) : (
39+
<span className="inline-flex items-center"><CheckCircle2 className="w-3 h-3 mr-1" /> Success</span>
40+
)}
41+
</Badge>
42+
<span className="text-[10px] text-gray-500">{session_id}</span>
43+
</div>
44+
45+
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs text-gray-700">
46+
<div><span className="font-medium">Duration:</span> {duration_ms} ms</div>
47+
<div><span className="font-medium">API:</span> {duration_api_ms} ms</div>
48+
<div><span className="font-medium">Turns:</span> {num_turns}</div>
49+
{typeof total_cost_usd === "number" && <div><span className="font-medium">Cost:</span> ${total_cost_usd.toFixed(4)}</div>}
50+
</div>
51+
52+
{usage && (
53+
<div className="mt-2">
54+
<div className="text-[11px] text-gray-500 mb-1">Usage</div>
55+
<pre className="bg-gray-50 border rounded p-2 whitespace-pre-wrap break-words text-xs text-gray-800">
56+
{JSON.stringify(usage, null, 2)}
57+
</pre>
58+
</div>
59+
)}
60+
61+
{result && (
62+
<div className="mt-2">
63+
<div className="text-[11px] text-gray-500 mb-1">Result</div>
64+
<pre className="bg-gray-50 border rounded p-2 whitespace-pre-wrap break-words text-xs text-gray-800">
65+
{result}
66+
</pre>
67+
</div>
68+
)}
69+
</div>
70+
</div>
71+
);
72+
};
73+
74+
export default ResultMessage;
75+
76+
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from "react";
2+
import { MessageObject, ContentBlock } from "@/types/agentic-session";
3+
import { Message } from "@/components/ui/message";
4+
import { ToolMessage } from "@/components/ui/tool-message";
5+
import { ThinkingMessage } from "@/components/ui/thinking-message";
6+
import { SystemMessage } from "@/components/ui/system-message";
7+
import { ResultMessage } from "@/components/ui/result-message";
8+
9+
export type StreamMessageProps = {
10+
message: MessageObject;
11+
};
12+
13+
const hasToolBlocks = (blocks: ContentBlock[] | undefined) =>
14+
Array.isArray(blocks) && blocks.some((b) => b.type === "tool_use_block" || b.type === "tool_result_block");
15+
16+
const getTextFromAssistant = (blocks: ContentBlock[] | undefined): string => {
17+
if (!Array.isArray(blocks)) return "";
18+
const tb = blocks.find((b) => b.type === "text_block") as Extract<ContentBlock, { type: "text_block" }> | undefined;
19+
return tb?.text || "";
20+
};
21+
22+
export const StreamMessage: React.FC<StreamMessageProps> = ({ message }) => {
23+
switch (message.type) {
24+
case "user_message": {
25+
const text = typeof message.content === "string" ? message.content : "";
26+
return <Message role="user" content={text} name="You" />;
27+
}
28+
case "assistant_message": {
29+
const blocks = message.content;
30+
// Thinking (new): show above, expandable
31+
if (Array.isArray(blocks) && blocks.some((b) => b.type === "thinking_block")) {
32+
return (
33+
<>
34+
<ThinkingMessage blocks={blocks} />
35+
{hasToolBlocks(blocks) ? (
36+
<ToolMessage message={message as any} />
37+
) : (
38+
<Message role="bot" content={getTextFromAssistant(blocks)} name="Claude AI" />
39+
)}
40+
</>
41+
);
42+
}
43+
// Tool use/result
44+
if (hasToolBlocks(blocks)) {
45+
return <ToolMessage message={message as any} />;
46+
}
47+
// Plain text
48+
return <Message role="bot" content={getTextFromAssistant(blocks)} name="Claude AI" />;
49+
}
50+
case "system_message": {
51+
return <SystemMessage subtype={message.subtype} data={message.data} />;
52+
}
53+
case "result_message": {
54+
return (
55+
<ResultMessage
56+
duration_ms={message.duration_ms}
57+
duration_api_ms={message.duration_api_ms}
58+
is_error={message.is_error}
59+
num_turns={message.num_turns}
60+
session_id={message.session_id}
61+
total_cost_usd={message.total_cost_usd}
62+
usage={message.usage as any}
63+
result={message.result ?? undefined}
64+
/>
65+
);
66+
}
67+
default:
68+
return null;
69+
}
70+
};
71+
72+
export default StreamMessage;
73+
74+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from "react";
2+
import { cn } from "@/lib/utils";
3+
import { Badge } from "@/components/ui/badge";
4+
import { Info } from "lucide-react";
5+
6+
export type SystemMessageProps = {
7+
subtype: string;
8+
data: Record<string, any>;
9+
className?: string;
10+
};
11+
12+
export const SystemMessage: React.FC<SystemMessageProps> = ({ subtype, data, className }) => {
13+
return (
14+
<div className={cn("mb-4", className)}>
15+
<div className="flex items-start space-x-3">
16+
<div className="flex-shrink-0">
17+
<div className="w-8 h-8 rounded-full flex items-center justify-center bg-gray-600">
18+
<Info className="w-4 h-4 text-white" />
19+
</div>
20+
</div>
21+
22+
<div className="flex-1 min-w-0">
23+
<div className="bg-white rounded-lg border shadow-sm p-3">
24+
<div className="flex items-center justify-between mb-2">
25+
<Badge variant="secondary" className="text-xs">System</Badge>
26+
<span className="text-[10px] text-gray-500">{subtype}</span>
27+
</div>
28+
29+
<pre className="bg-gray-50 border rounded p-2 whitespace-pre-wrap break-words text-xs text-gray-800">
30+
{JSON.stringify(data ?? {}, null, 2)}
31+
</pre>
32+
</div>
33+
</div>
34+
</div>
35+
</div>
36+
);
37+
};
38+
39+
export default SystemMessage;
40+
41+

0 commit comments

Comments
 (0)