Skip to content

Commit 10ecade

Browse files
yarin-magclaude
andcommitted
feat(conversation): add hover tooltips on token counts with system prompt hint
- ↑ and ↓ arrows show tooltip explaining input/output tokens on hover - Info icon appears on first user message (>50 input tokens) explaining system prompt inclusion - Switched from Tailwind named group variants to React useState for reliable hover behavior - cursor-help on all token spans to signal interactivity Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0222b20 commit 10ecade

1 file changed

Lines changed: 59 additions & 29 deletions

File tree

apps/web/src/features/agents/components/ConversationMessage.tsx

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useState } from 'react';
12
import { Bot, User, Info } from 'lucide-react';
23
import { cn, formatTime, formatTokens } from '../../../lib/utils';
34
import ReactMarkdown from 'react-markdown';
@@ -11,6 +12,7 @@ interface ConversationMessageProps {
1112

1213
export function ConversationMessage({ turn }: ConversationMessageProps) {
1314
const isUser = turn.role === "user";
15+
const [tooltip, setTooltip] = useState<'input' | 'output' | 'hint' | null>(null);
1416
const isFromWeb = turn.source === 'web';
1517

1618
return (
@@ -106,38 +108,66 @@ export function ConversationMessage({ turn }: ConversationMessageProps) {
106108
</div>
107109

108110
{/* Token annotation — both user and assistant messages */}
109-
{turn.tokens && (
110-
<div className={cn(
111-
'text-xs mt-1 flex items-center gap-1.5 flex-wrap',
112-
isUser ? 'opacity-70' : 'text-muted-foreground/60'
113-
)}>
114-
<span className="relative group/in flex items-center gap-1 cursor-default">
115-
<span>{formatTokens(turn.tokens.input_tokens ?? 0)}</span>
116-
<span className="pointer-events-none absolute bottom-full left-0 mb-1.5 w-44 rounded-md bg-popover border border-border px-2.5 py-1.5 text-[11px] leading-snug text-popover-foreground shadow-md opacity-0 group-hover/in:opacity-100 transition-opacity z-50">
117-
Input tokens — context sent to the model for this turn
111+
{turn.tokens && (() => {
112+
const inputTokens = turn.tokens.input_tokens ?? 0;
113+
const outputTokens = turn.tokens.output_tokens ?? 0;
114+
const showSystemHint = isUser && inputTokens > 50;
115+
return (
116+
<div className={cn(
117+
'text-xs mt-1 flex items-center gap-1.5 flex-wrap',
118+
isUser ? 'opacity-70' : 'text-muted-foreground/60'
119+
)}>
120+
{/* Input tokens */}
121+
<span
122+
className="relative cursor-help"
123+
onMouseEnter={() => setTooltip('input')}
124+
onMouseLeave={() => setTooltip(null)}
125+
>
126+
{formatTokens(inputTokens)}
127+
{tooltip === 'input' && (
128+
<span className="absolute bottom-full left-0 mb-2 w-48 rounded-md bg-popover border border-border px-2.5 py-1.5 text-[11px] leading-snug text-popover-foreground shadow-md z-50 whitespace-normal">
129+
Input tokens — context sent to the model for this turn
130+
</span>
131+
)}
118132
</span>
119-
</span>
120-
{isUser && (turn.tokens.input_tokens ?? 0) > 50 && (
121-
<span className="relative group/hint">
122-
<Info className="h-3 w-3 opacity-50 cursor-default" />
123-
<span className="pointer-events-none absolute bottom-full right-0 mb-1.5 w-52 rounded-md bg-popover border border-border px-2.5 py-1.5 text-[11px] leading-snug text-popover-foreground shadow-md opacity-0 group-hover/hint:opacity-100 transition-opacity z-50">
124-
Includes system prompt tokens sent with the first message of this run
133+
134+
{/* System prompt hint icon */}
135+
{showSystemHint && (
136+
<span
137+
className="relative cursor-help"
138+
onMouseEnter={() => setTooltip('hint')}
139+
onMouseLeave={() => setTooltip(null)}
140+
>
141+
<Info className="h-3 w-3 opacity-50" />
142+
{tooltip === 'hint' && (
143+
<span className="absolute bottom-full right-0 mb-2 w-56 rounded-md bg-popover border border-border px-2.5 py-1.5 text-[11px] leading-snug text-popover-foreground shadow-md z-50 whitespace-normal">
144+
Includes system prompt tokens — Claude Code injects its full instructions on the first message of every run
145+
</span>
146+
)}
125147
</span>
126-
</span>
127-
)}
128-
{(turn.tokens.output_tokens ?? 0) > 0 && (
129-
<>
130-
<span className="opacity-40">·</span>
131-
<span className="relative group/out flex items-center gap-1 cursor-default">
132-
<span>{formatTokens(turn.tokens.output_tokens ?? 0)}</span>
133-
<span className="pointer-events-none absolute bottom-full left-0 mb-1.5 w-44 rounded-md bg-popover border border-border px-2.5 py-1.5 text-[11px] leading-snug text-popover-foreground shadow-md opacity-0 group-hover/out:opacity-100 transition-opacity z-50">
134-
Output tokens — text generated by the model in this turn
148+
)}
149+
150+
{/* Output tokens */}
151+
{outputTokens > 0 && (
152+
<>
153+
<span className="opacity-40">·</span>
154+
<span
155+
className="relative cursor-help"
156+
onMouseEnter={() => setTooltip('output')}
157+
onMouseLeave={() => setTooltip(null)}
158+
>
159+
{formatTokens(outputTokens)}
160+
{tooltip === 'output' && (
161+
<span className="absolute bottom-full left-0 mb-2 w-48 rounded-md bg-popover border border-border px-2.5 py-1.5 text-[11px] leading-snug text-popover-foreground shadow-md z-50 whitespace-normal">
162+
Output tokens — text generated by the model in this turn
163+
</span>
164+
)}
135165
</span>
136-
</span>
137-
</>
138-
)}
139-
</div>
140-
)}
166+
</>
167+
)}
168+
</div>
169+
);
170+
})()}
141171
</div>
142172
</div>
143173

0 commit comments

Comments
 (0)