Skip to content

Commit 0a70ac0

Browse files
committed
feat(ui): enhance message rendering with thinking widget and session improvements
- Add ThinkingWidget component for displaying AI reasoning content in collapsible interface - Improve session initialization by removing redundant event listener and enhancing ID extraction - Enhance StreamMessage component to handle diverse content structures and thinking content - Add comprehensive debug logging for better message structure understanding - Fix cost display logic to handle both cost_usd and total_cost_usd fields - Refactor user message rendering to support both nested and direct content structures
1 parent d8695c4 commit 0a70ac0

File tree

3 files changed

+100
-26
lines changed

3 files changed

+100
-26
lines changed

src/components/ClaudeCodeSession.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -285,16 +285,6 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
285285
// Set up event listeners before executing
286286
console.log('[ClaudeCodeSession] Setting up event listeners...');
287287

288-
// Listen for the session started event to get the Claude session ID
289-
const sessionStartedUnlisten = await listen<string>(`claude-session-started:*`, (event) => {
290-
const eventName = event.event;
291-
const sessionId = eventName.split(':')[1];
292-
if (sessionId && !claudeSessionId) {
293-
console.log('[ClaudeCodeSession] Received Claude session ID:', sessionId);
294-
setClaudeSessionId(sessionId);
295-
}
296-
});
297-
298288
// If we already have a Claude session ID, use isolated listeners
299289
const eventSuffix = claudeSessionId ? `:${claudeSessionId}` : '';
300290

@@ -315,14 +305,22 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
315305
});
316306

317307
// Extract session info from system init message
318-
if (message.type === "system" && message.subtype === "init" && message.session_id && !extractedSessionInfo) {
308+
if (message.type === "system" && message.subtype === "init" && message.session_id) {
319309
console.log('[ClaudeCodeSession] Extracting session info from init message');
320310
// Extract project ID from the project path
321311
const projectId = projectPath.replace(/[^a-zA-Z0-9]/g, '-');
322-
setExtractedSessionInfo({
323-
sessionId: message.session_id,
324-
projectId: projectId
325-
});
312+
313+
// Set both claudeSessionId and extractedSessionInfo
314+
if (!claudeSessionId) {
315+
setClaudeSessionId(message.session_id);
316+
}
317+
318+
if (!extractedSessionInfo) {
319+
setExtractedSessionInfo({
320+
sessionId: message.session_id,
321+
projectId: projectId
322+
});
323+
}
326324
}
327325
} catch (err) {
328326
console.error("Failed to parse message:", err, event.payload);
@@ -364,7 +362,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
364362
}
365363
});
366364

367-
unlistenRefs.current = [sessionStartedUnlisten, outputUnlisten, errorUnlisten, completeUnlisten];
365+
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
368366

369367
// Add the user message immediately to the UI (after setting up listeners)
370368
const userMessage: ClaudeStreamMessage = {

src/components/StreamMessage.tsx

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ import {
3333
SystemReminderWidget,
3434
SystemInitializedWidget,
3535
TaskWidget,
36-
LSResultWidget
36+
LSResultWidget,
37+
ThinkingWidget
3738
} from "./ToolWidgets";
3839

3940
interface StreamMessageProps {
@@ -73,6 +74,15 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
7374
if (!toolId) return null;
7475
return toolResults.get(toolId) || null;
7576
};
77+
78+
// Debug logging to understand message structure
79+
console.log('[StreamMessage] Rendering message:', {
80+
type: message.type,
81+
hasMessage: !!message.message,
82+
messageStructure: message.message ? Object.keys(message.message) : 'no message field',
83+
fullMessage: message
84+
});
85+
7686
try {
7787
// Skip rendering for meta messages that don't have meaningful content
7888
if (message.isMeta && !message.leafUuid && !message.summary) {
@@ -147,6 +157,19 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
147157
);
148158
}
149159

160+
// Thinking content - render with ThinkingWidget
161+
if (content.type === "thinking") {
162+
renderedSomething = true;
163+
return (
164+
<div key={idx}>
165+
<ThinkingWidget
166+
thinking={content.thinking || ''}
167+
signature={content.signature}
168+
/>
169+
</div>
170+
);
171+
}
172+
150173
// Tool use - render custom widgets based on tool name
151174
if (content.type === "tool_use") {
152175
const toolName = content.name?.toLowerCase();
@@ -258,6 +281,7 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
258281

259282
return null;
260283
})}
284+
261285
{msg.usage && (
262286
<div className="text-xs text-muted-foreground mt-2">
263287
Tokens: {msg.usage.input_tokens} in, {msg.usage.output_tokens} out
@@ -268,16 +292,18 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
268292
</CardContent>
269293
</Card>
270294
);
295+
271296
if (!renderedSomething) return null;
272297
return renderedCard;
273298
}
274299

275-
// User message
276-
if (message.type === "user" && message.message) {
300+
// User message - handle both nested and direct content structures
301+
if (message.type === "user") {
277302
// Don't render meta messages, which are for system use
278303
if (message.isMeta) return null;
279304

280-
const msg = message.message;
305+
// Handle different message structures
306+
const msg = message.message || message;
281307

282308
let renderedSomething = false;
283309

@@ -288,9 +314,9 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
288314
<User className="h-5 w-5 text-muted-foreground mt-0.5" />
289315
<div className="flex-1 space-y-2 min-w-0">
290316
{/* Handle content that is a simple string (e.g. from user commands) */}
291-
{typeof msg.content === 'string' && (
317+
{(typeof msg.content === 'string' || (msg.content && !Array.isArray(msg.content))) && (
292318
(() => {
293-
const contentStr = msg.content as string;
319+
const contentStr = typeof msg.content === 'string' ? msg.content : String(msg.content);
294320
if (contentStr.trim() === '') return null;
295321
renderedSomething = true;
296322

@@ -316,9 +342,9 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
316342

317343
// Otherwise render as plain text
318344
return (
319-
<pre className="text-sm font-mono whitespace-pre-wrap text-muted-foreground">
345+
<div className="text-sm">
320346
{contentStr}
321-
</pre>
347+
</div>
322348
);
323349
})()
324350
)}
@@ -646,8 +672,8 @@ const StreamMessageComponent: React.FC<StreamMessageProps> = ({ message, classNa
646672
)}
647673

648674
<div className="text-xs text-muted-foreground space-y-1 mt-2">
649-
{message.cost_usd !== undefined && (
650-
<div>Cost: ${message.cost_usd.toFixed(4)} USD</div>
675+
{(message.cost_usd !== undefined || message.total_cost_usd !== undefined) && (
676+
<div>Cost: ${((message.cost_usd || message.total_cost_usd)!).toFixed(4)} USD</div>
651677
)}
652678
{message.duration_ms !== undefined && (
653679
<div>Duration: {(message.duration_ms / 1000).toFixed(2)}s</div>

src/components/ToolWidgets.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,3 +1869,53 @@ export const TaskWidget: React.FC<{
18691869
</div>
18701870
);
18711871
};
1872+
1873+
/**
1874+
* Widget for displaying AI thinking/reasoning content
1875+
* Collapsible and closed by default
1876+
*/
1877+
export const ThinkingWidget: React.FC<{
1878+
thinking: string;
1879+
signature?: string;
1880+
}> = ({ thinking, signature }) => {
1881+
const [isExpanded, setIsExpanded] = useState(false);
1882+
1883+
return (
1884+
<div className="rounded-lg border border-purple-500/20 bg-gradient-to-br from-purple-500/5 to-violet-500/5 overflow-hidden">
1885+
<button
1886+
onClick={() => setIsExpanded(!isExpanded)}
1887+
className="w-full px-4 py-3 flex items-center justify-between hover:bg-purple-500/10 transition-colors"
1888+
>
1889+
<div className="flex items-center gap-2">
1890+
<div className="relative">
1891+
<Bot className="h-4 w-4 text-purple-500" />
1892+
<Sparkles className="h-2.5 w-2.5 text-purple-400 absolute -top-1 -right-1 animate-pulse" />
1893+
</div>
1894+
<span className="text-sm font-medium text-purple-600 dark:text-purple-400">
1895+
Thinking...
1896+
</span>
1897+
</div>
1898+
<ChevronRight className={cn(
1899+
"h-4 w-4 text-purple-500 transition-transform",
1900+
isExpanded && "rotate-90"
1901+
)} />
1902+
</button>
1903+
1904+
{isExpanded && (
1905+
<div className="px-4 pb-4 pt-2 space-y-3 border-t border-purple-500/20">
1906+
<div className="prose prose-sm dark:prose-invert max-w-none">
1907+
<pre className="text-xs font-mono text-purple-700 dark:text-purple-300 whitespace-pre-wrap bg-purple-500/5 p-3 rounded-lg">
1908+
{thinking}
1909+
</pre>
1910+
</div>
1911+
1912+
{signature && (
1913+
<div className="text-xs text-purple-600/60 dark:text-purple-400/60 font-mono truncate">
1914+
Signature: {signature.slice(0, 16)}...
1915+
</div>
1916+
)}
1917+
</div>
1918+
)}
1919+
</div>
1920+
);
1921+
};

0 commit comments

Comments
 (0)