A React Native app for the ultimate thinking partner.
1/**
2 * Message Label Utilities
3 *
4 * Computes human-readable labels for message groups based on:
5 * - Message type (assistant, tool_call, etc.)
6 * - Streaming state
7 * - Tool name (for tool calls)
8 * - Content presence (reasoning-only vs full message)
9 *
10 * Used by MessageGroupBubble to show unified "(co <action>)" labels.
11 */
12
13import type { MessageGroup } from '../hooks/useMessageGroups';
14
15/**
16 * Extract a specific argument from tool call arguments
17 * Expects Python-style format: tool(query="value", num=10)
18 */
19function extractToolArgument(group: MessageGroup, argName: string): string | null {
20 const argsStr = group.toolCall?.args || group.content || '';
21
22 // Extract from Python-style string: tool(arg="value", ...)
23 const regex = new RegExp(`${argName}=["']([^"']+)["']`);
24 const match = argsStr.match(regex);
25 if (match) {
26 return match[1];
27 }
28
29 return null;
30}
31
32/**
33 * Tool name to human-readable action mapping
34 */
35const TOOL_ACTIONS: Record<string, { past: string; present: string }> = {
36 web_search: { past: 'searched the web', present: 'is searching the web' },
37 open_files: { past: 'opened files', present: 'is opening files' },
38 memory: { past: 'recalled', present: 'is recalling' },
39 conversation_search: { past: 'searched the conversation', present: 'is searching the conversation' },
40 grep_files: { past: 'searched files', present: 'is searching files' },
41 memory_replace: { past: 'updated memory', present: 'is updating memory' },
42 memory_insert: { past: 'added to memory', present: 'is adding to memory' },
43 fetch_webpage: { past: 'fetched a webpage', present: 'is fetching a webpage' },
44 semantic_search_files: { past: 'searched files', present: 'is searching files' },
45};
46
47/**
48 * Get human-readable label for a message group
49 *
50 * Examples:
51 * - "(co said)" - assistant message with content
52 * - "(co thought)" - assistant message with only reasoning
53 * - "(co searched the web)" - completed web_search tool call
54 * - "(co is thinking)" - streaming with only reasoning
55 * - "(co is searching the web)" - streaming web_search tool call
56 */
57export function getMessageLabel(group: MessageGroup): string {
58 const isStreaming = group.isStreaming === true;
59
60 // TOOL CALL MESSAGE
61 if (group.type === 'tool_call') {
62 const toolName = group.toolCall?.name || 'unknown_tool';
63
64 // Special handling for memory tool with str_replace command
65 if (toolName === 'memory') {
66 // Try to detect str_replace command in the arguments
67 const args = group.toolCall?.args || group.content || '';
68 if (args.includes('str_replace') || args.includes('command: str_replace')) {
69 return isStreaming ? '(co is updating its memory)' : '(co updated its memory)';
70 }
71 }
72
73 // Special handling for web_search - include query
74 if (toolName === 'web_search') {
75 const query = extractToolArgument(group, 'query');
76 if (query) {
77 return isStreaming ? `(co is searching for ${query})` : `(co searched for ${query})`;
78 }
79 }
80
81 const action = TOOL_ACTIONS[toolName];
82
83 if (action) {
84 return isStreaming ? `(co ${action.present})` : `(co ${action.past})`;
85 }
86
87 // Fallback for unknown tools
88 return isStreaming ? `(co is using ${toolName})` : `(co used ${toolName})`;
89 }
90
91 // ASSISTANT MESSAGE
92 if (group.type === 'assistant') {
93 const hasContent = group.content && group.content.trim().length > 0;
94 const hasReasoningOnly = group.reasoning && !hasContent;
95
96 if (hasReasoningOnly) {
97 // Only reasoning, no assistant message content
98 return isStreaming ? '(co is thinking)' : '(co thought)';
99 }
100
101 // Has assistant message content (with or without reasoning)
102 return isStreaming ? '(co is saying)' : '(co said)';
103 }
104
105 // USER MESSAGE (shouldn't need label, but handle gracefully)
106 if (group.type === 'user') {
107 return ''; // User messages don't show labels
108 }
109
110 // COMPACTION (shouldn't need label)
111 if (group.type === 'compaction') {
112 return ''; // Compaction bars have their own styling
113 }
114
115 // ORPHANED TOOL RETURN (defensive)
116 if (group.type === 'tool_return_orphaned') {
117 return '(tool result)';
118 }
119
120 // Fallback
121 return '(co)';
122}