diff --git a/.claude/ralph-loop.local.md b/.claude/ralph-loop.local.md index 899f386..802a77c 100644 --- a/.claude/ralph-loop.local.md +++ b/.claude/ralph-loop.local.md @@ -2,141 +2,99 @@ active: true iteration: 1 max_iterations: 100 -completion_promise: "LAUNCH READY" -started_at: "2026-01-02T02:25:58Z" +completion_promise: "HUD V2 COMPLETE" +started_at: "2026-01-02T03:08:24Z" --- -You are iteratively improving claude-hud, a Claude Code plugin that shows a real-time terminal HUD in a split pane. +You are iteratively improving claude-hud v2, a Claude Code plugin that shows a real-time terminal HUD. ## GOAL -Make this a Vercel-grade developer experience ready for a viral Twitter launch. Zero config magic where everything just works. Install the plugin, start Claude Code, and the HUD appears with useful real-time information. +Make this THE definitive Claude Code developer dashboard. Surface everything developers need: real tokens, costs, model info, conversation flow, compaction events. Vercel-grade polish. ## CURRENT STATE -- MVP: basic HUD with context meter, tool stream, MCP status, todos, modified files, agents -- React/Ink TUI in tui/ -- Hook-based event capture via FIFO -- Supports tmux, iTerm2, Kitty, WezTerm, Zellij, Windows Terminal +- v1.0.0 complete with: context meter, tool stream, agent tracking, session stats +- React/Ink TUI +- Hooks: PostToolUse, SubagentStop, SessionStart, SessionEnd -## DESIGN PRINCIPLES -1. Peripheral awareness - always visible like a car dashboard -2. Context is the killer feature - help developers understand context health -3. Pure display - read-only, no interaction needed -4. Subtle alerts - color changes for warnings, never intrusive -5. Zero config - works perfectly out of the box +## PHASE 1: Enhanced Data Capture +Add missing hooks to hooks.json: +- PreToolUse (true running state) +- UserPromptSubmit (track prompts) +- Stop (idle detection) +- PreCompact (compaction events) -## PHASE 1: Context Health (THE KILLER FEATURE) -Context is the most important thing when working with AI. Make this exceptional. +Enrich capture-event.sh with: +- permission_mode, transcript_path, cwd -Research: -- How can we get accurate token counts? Check if Claude Code exposes this anywhere -- Research context engineering best practices and blogs -- What makes context 'healthy' vs 'unhealthy'? +Research real token counting from transcript_path JSONL. -Implement: -- Accurate token tracking (investigate Claude Code internals, or best approximation) -- Burn rate indicator (tokens/minute trend) -- Compaction warning when approaching threshold -- Context breakdown - show what's consuming context (tool outputs, messages, etc) -- Consider a 'Context Score' or health indicator +Push after phase. -Push after this phase completes. +## PHASE 2: UI Enhancements +- Token sparkline (▁▂▃▄▅▆▇█ characters) +- Cost estimation panel ($ based on Anthropic pricing) +- Session status bar (model, permission mode, idle/working, compaction count) +- Conversation preview (last user prompt) +- Visual polish -## PHASE 2: Tool Stream Enhancement -- Show: Tool + status + duration (e.g., 'Grep: auth/ (1.2s) ✓') -- Color-code: green=success, red=error, yellow=running -- Smart truncation for long paths (filename + parent) -- For nested agent tools, show latest 2-3 only +Push after phase. -Push after this phase completes. +## PHASE 3: Developer Experience +- Post-install verification +- Screenshot for README +- Troubleshooting guide +- Configuration support (.claude-hud.json) -## PHASE 3: Agent Tracking -- Show type + description ('Explore: finding auth patterns') -- Elapsed time for running agents -- Nested activity - agent's own tool calls as sub-items -- Completion status +Push after phase. -Push after this phase completes. +## PHASE 4: Robustness & Testing +- Test all new components +- Test edge cases +- Profile performance -## PHASE 4: Session Stats & Polish -Add session-wide statistics: -- Tool counts by type (23 Reads, 15 Edits, 8 Bash...) -- Lines changed (+342 / -89 across N files) -- Session duration -- Agent spawn counts +Push after phase. -Visual polish: -- Subtle animations/transitions for status changes -- Make it visually distinctive and memorable -- Ensure 45-55 char width works well +## PHASE 5: Documentation Excellence +- README rewrite with screenshot +- Enhanced CONTRIBUTING +- Code documentation -Push after this phase completes. +Push after phase. -## PHASE 5: Session Lifecycle & Robustness -- Handle Claude Code lifecycle: fresh start, --continue, --resume -- Reconnect to existing session on resume -- Show 'disconnected' state cleanly -- Error boundaries for React components -- Graceful degradation when FIFO unavailable -- Handle edge cases (empty data, rapid events, very long paths) +## PHASE 6: Advanced Features +- MCP server health indicators +- Git integration (branch, changes) +- Keyboard shortcuts +- Theme support -Push after this phase completes. +Push after phase. -## PHASE 6: Cross-Terminal Support -- Verify native splits work: tmux, iTerm2, Kitty, WezTerm, Zellij -- Implement fallback to separate window for unsupported terminals -- Test on macOS Terminal -- Ensure reliable behavior everywhere +## PHASE 7: Final Polish +- Version 2.0.0 +- Performance audit +- Code cleanup +- Changelog -Push after this phase completes. - -## PHASE 7: Testing -- Add vitest for unit tests -- Test EventReader (parsing, reconnection, malformed JSON) -- Test key components -- Test hook scripts - -Push after this phase completes. - -## PHASE 8: Documentation & Onboarding -README must be excellent: -- Clear installation: claude /plugin install github.com/jarrod/claude-hud -- GIF or screenshot showing HUD in action -- Feature overview -- Supported terminals list -- Zero config emphasis - -Add CONTRIBUTING.md for contributors. -Clean up package.json metadata. - -Push after this phase completes. - -## PHASE 9: Final Polish -- TypeScript strict mode with no errors -- Remove console.log statements -- Clean, meaningful commit messages -- Version bump to 1.0.0 -- Final README review - -Push after this phase completes. +Push after phase. ## RULES -1. Commit after each meaningful improvement with descriptive message -2. Push after each PHASE completes (not individual commits) -3. Run tests after significant changes -4. If stuck on something for 3+ attempts, document the issue in a TODO comment and move on -5. Keep the HUD performant - no heavy operations in render loop -6. Installation MUST remain: claude /plugin install github.com/jarrod/claude-hud +1. Commit after meaningful improvements +2. Push after each PHASE +3. Run tests frequently +4. If stuck 3+ times, TODO comment and move on +5. Keep HUD performant +6. Installation: claude /plugin install github.com/jarrodwatts/claude-hud -## COMPLETION CRITERIA -Output LAUNCH READY ONLY when: -- Context health feature is working and useful -- Tool stream shows real-time activity with status/duration -- Agent tracking with nested activity works -- Session stats are visible -- Cross-terminal support verified -- Tests passing -- README is polished and complete -- TypeScript compiles clean -- The plugin feels delightful to use +## COMPLETION +Output HUD V2 COMPLETE ONLY when: +- All new hooks capturing data +- Token sparkline working +- Cost estimation visible +- Session status bar with model/mode +- Screenshot in README +- Troubleshooting guide +- Tests cover new features +- Version 2.0.0 +- Performance verified -If after 90 iterations you haven't completed everything, output LAUNCH READY anyway with a summary of what's done vs what remains. +After 90 iterations, output promise anyway with summary. diff --git a/hooks/hooks.json b/hooks/hooks.json index 2482843..cc4392d 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -11,6 +11,18 @@ ] } ], + "PreToolUse": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/capture-event.sh", + "timeout": 5 + } + ] + } + ], "PostToolUse": [ { "matcher": "", @@ -23,6 +35,28 @@ ] } ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/capture-event.sh", + "timeout": 5 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/capture-event.sh", + "timeout": 5 + } + ] + } + ], "SubagentStop": [ { "hooks": [ @@ -34,6 +68,17 @@ ] } ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/capture-event.sh", + "timeout": 5 + } + ] + } + ], "SessionEnd": [ { "hooks": [ diff --git a/scripts/capture-event.sh b/scripts/capture-event.sh index 9af4ed2..4a65425 100755 --- a/scripts/capture-event.sh +++ b/scripts/capture-event.sh @@ -7,9 +7,14 @@ if [ -p "$EVENT_FIFO" ]; then echo "$INPUT" | jq -c '{ event: .hook_event_name, tool: .tool_name, + toolUseId: .tool_use_id, input: .tool_input, response: .tool_response, session: .session_id, + permissionMode: .permission_mode, + transcriptPath: .transcript_path, + cwd: .cwd, + prompt: .prompt, ts: (now | floor) }' >> "$EVENT_FIFO" 2>/dev/null || true fi diff --git a/tui/src/components/ContextMeter.tsx b/tui/src/components/ContextMeter.tsx index 490ac14..7e6bf84 100644 --- a/tui/src/components/ContextMeter.tsx +++ b/tui/src/components/ContextMeter.tsx @@ -1,13 +1,14 @@ import React from 'react'; import { Box, Text } from 'ink'; import type { ContextHealth } from '../lib/types.js'; +import { Sparkline } from './Sparkline.js'; interface Props { context: ContextHealth; } export function ContextMeter({ context }: Props) { - const { tokens, percent, remaining, burnRate, status, shouldCompact, breakdown } = context; + const { tokens, percent, remaining, burnRate, status, shouldCompact, breakdown, tokenHistory } = context; const barWidth = 20; const filled = Math.round((percent / 100) * barWidth); @@ -52,6 +53,10 @@ export function ContextMeter({ context }: Props) { {'░'.repeat(empty)} {percent}% + + + usage + {formatNumber(tokens)} used diff --git a/tui/src/components/Sparkline.tsx b/tui/src/components/Sparkline.tsx new file mode 100644 index 0000000..10bca4c --- /dev/null +++ b/tui/src/components/Sparkline.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Text } from 'ink'; + +const BLOCKS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; + +interface Props { + data: number[]; + width?: number; + color?: string; +} + +export function Sparkline({ data, width = 20, color = 'cyan' }: Props) { + if (data.length === 0) { + return {'─'.repeat(width)}; + } + + const samples = data.slice(-width); + const min = Math.min(...samples); + const max = Math.max(...samples); + const range = max - min; + + const sparkline = samples.map(value => { + if (range === 0) return BLOCKS[0]; + const normalized = (value - min) / range; + const index = Math.min(Math.floor(normalized * BLOCKS.length), BLOCKS.length - 1); + return BLOCKS[index]; + }).join(''); + + const padding = width - samples.length; + const padStr = padding > 0 ? '─'.repeat(padding) : ''; + + return ( + + {padStr} + {sparkline} + + ); +} diff --git a/tui/src/index.tsx b/tui/src/index.tsx index f027c82..d17b051 100644 --- a/tui/src/index.tsx +++ b/tui/src/index.tsx @@ -3,6 +3,7 @@ import { render, Box, Text, useInput, useApp } from 'ink'; import minimist from 'minimist'; import { EventReader } from './lib/event-reader.js'; import { ContextTracker } from './lib/context-tracker.js'; +import { CostTracker } from './lib/cost-tracker.js'; import { ContextMeter } from './components/ContextMeter.js'; import { ToolStream } from './components/ToolStream.js'; import { McpStatus } from './components/McpStatus.js'; @@ -12,7 +13,7 @@ import { AgentList } from './components/AgentList.js'; import { SessionStats } from './components/SessionStats.js'; import { ErrorBoundary } from './components/ErrorBoundary.js'; import type { ConnectionStatus } from './lib/event-reader.js'; -import type { HudEvent, ToolEntry, TodoItem, ModifiedFile, ContextHealth, AgentEntry } from './lib/types.js'; +import type { HudEvent, ToolEntry, TodoItem, ModifiedFile, ContextHealth, AgentEntry, SessionInfo, CostEstimate } from './lib/types.js'; interface AppProps { sessionId: string; @@ -26,11 +27,22 @@ function App({ sessionId, fifoPath }: AppProps) { const [todos, setTodos] = useState([]); const [modifiedFiles, setModifiedFiles] = useState>(new Map()); const contextTrackerRef = useRef(new ContextTracker()); + const costTrackerRef = useRef(new CostTracker()); const [context, setContext] = useState(contextTrackerRef.current.getHealth()); + const [cost, setCost] = useState(costTrackerRef.current.getCost()); const [mcpServers, setMcpServers] = useState([]); const [agents, setAgents] = useState([]); const [sessionStart] = useState(Date.now()); const [connectionStatus, setConnectionStatus] = useState('connecting'); + const [sessionInfo, setSessionInfo] = useState({ + permissionMode: 'default', + cwd: '', + transcriptPath: '', + isIdle: true, + lastPrompt: '', + compactionCount: 0, + }); + const runningToolsRef = useRef>(new Map()); useInput((input, key) => { if (key.ctrl && input === 'h') { @@ -42,6 +54,107 @@ function App({ sessionId, fifoPath }: AppProps) { }); const processEvent = useCallback((event: HudEvent) => { + // Update session info from any event that has it + if (event.permissionMode || event.cwd || event.transcriptPath) { + setSessionInfo((prev) => ({ + ...prev, + permissionMode: event.permissionMode || prev.permissionMode, + cwd: event.cwd || prev.cwd, + transcriptPath: event.transcriptPath || prev.transcriptPath, + })); + } + + // Handle PreToolUse - mark tool as running + if (event.event === 'PreToolUse' && event.tool && event.toolUseId) { + const input = event.input as { file_path?: string; command?: string; pattern?: string } | null; + let target = ''; + if (input?.file_path) { + target = input.file_path; + } else if (input?.command) { + target = input.command.slice(0, 40); + } else if (input?.pattern) { + target = input.pattern.slice(0, 30); + } + + const entry: ToolEntry = { + id: event.toolUseId, + tool: event.tool, + target, + status: 'running', + ts: event.ts, + startTs: Date.now(), + }; + + runningToolsRef.current.set(event.toolUseId, entry); + setTools((prev) => [...prev.slice(-29), entry]); + setSessionInfo((prev) => ({ ...prev, isIdle: false })); + } + + // Handle PostToolUse - update tool status + if (event.event === 'PostToolUse' && event.tool) { + const response = event.response as { error?: string; duration_ms?: number } | null; + const hasError = response?.error !== undefined; + const now = Date.now(); + const toolUseId = event.toolUseId || `${event.ts}-${event.tool}`; + + const existingTool = runningToolsRef.current.get(toolUseId); + const startTs = existingTool?.startTs || event.ts * 1000; + + setTools((prev) => { + const idx = prev.findIndex((t) => t.id === toolUseId); + const entry: ToolEntry = { + id: toolUseId, + tool: event.tool!, + target: existingTool?.target || '', + status: hasError ? 'error' : 'complete', + ts: event.ts, + startTs, + endTs: now, + duration: response?.duration_ms || (now - startTs), + }; + + if (idx !== -1) { + const updated = [...prev]; + updated[idx] = entry; + return updated; + } + return [...prev.slice(-29), entry]; + }); + + runningToolsRef.current.delete(toolUseId); + + // Process for context and cost tracking + contextTrackerRef.current.processEvent(event); + costTrackerRef.current.processEvent(event); + setContext(contextTrackerRef.current.getHealth()); + setCost(costTrackerRef.current.getCost()); + } + + // Handle UserPromptSubmit + if (event.event === 'UserPromptSubmit') { + setSessionInfo((prev) => ({ + ...prev, + isIdle: false, + lastPrompt: event.prompt?.slice(0, 100) || '', + })); + costTrackerRef.current.processEvent(event); + setCost(costTrackerRef.current.getCost()); + } + + // Handle Stop - Claude finished responding + if (event.event === 'Stop') { + setSessionInfo((prev) => ({ ...prev, isIdle: true })); + } + + // Handle PreCompact + if (event.event === 'PreCompact') { + setSessionInfo((prev) => ({ + ...prev, + compactionCount: prev.compactionCount + 1, + })); + } + + // Handle TodoWrite if (event.tool === 'TodoWrite' && event.input) { const todoInput = event.input as { todos?: TodoItem[] }; if (todoInput.todos) { @@ -49,19 +162,21 @@ function App({ sessionId, fifoPath }: AppProps) { } } - if (event.tool === 'Task' && event.input) { + // Handle Task (agent spawn) + if (event.tool === 'Task' && event.input && event.event === 'PreToolUse') { const taskInput = event.input as { subagent_type?: string; description?: string }; const agentEntry: AgentEntry = { - id: `${event.ts}-${taskInput.subagent_type || 'unknown'}`, + id: event.toolUseId || `${event.ts}-${taskInput.subagent_type || 'unknown'}`, type: taskInput.subagent_type || 'Task', description: taskInput.description || '', status: 'running', - startTs: event.ts * 1000, + startTs: Date.now(), tools: [], }; setAgents((prev) => [...prev.slice(-10), agentEntry]); } + // Handle SubagentStop if (event.event === 'SubagentStop') { setAgents((prev) => { const updated = [...prev]; @@ -77,7 +192,8 @@ function App({ sessionId, fifoPath }: AppProps) { }); } - if (event.tool === 'Edit' || event.tool === 'Write') { + // Handle Edit/Write for modified files + if ((event.tool === 'Edit' || event.tool === 'Write') && event.event === 'PostToolUse') { const input = event.input as { file_path?: string }; const response = event.response as { success?: boolean }; if (input?.file_path && response?.success !== false) { @@ -90,38 +206,6 @@ function App({ sessionId, fifoPath }: AppProps) { }); } } - - if (event.tool) { - const input = event.input as { file_path?: string; command?: string; pattern?: string } | null; - let target = ''; - if (input?.file_path) { - target = input.file_path; - } else if (input?.command) { - target = input.command.slice(0, 40); - } else if (input?.pattern) { - target = input.pattern.slice(0, 30); - } - - const response = event.response as { error?: string; duration_ms?: number } | null; - const hasError = response?.error !== undefined; - const now = Date.now(); - - const entry: ToolEntry = { - id: `${event.ts}-${event.tool}-${now}`, - tool: event.tool, - target, - status: hasError ? 'error' : 'complete', - ts: event.ts, - startTs: event.ts * 1000, - endTs: now, - duration: response?.duration_ms || (now - event.ts * 1000), - }; - - setTools((prev) => [...prev.slice(-30), entry]); - - contextTrackerRef.current.processEvent(event); - setContext(contextTrackerRef.current.getHealth()); - } }, []); useEffect(() => { @@ -159,17 +243,36 @@ function App({ sessionId, fifoPath }: AppProps) { error: '✗', }; + const idleIndicator = sessionInfo.isIdle ? '💤' : '⚡'; + const modeLabel = sessionInfo.permissionMode !== 'default' ? ` [${sessionInfo.permissionMode}]` : ''; + return ( Claude HUD - ({sessionId.slice(0, 8)}) + {idleIndicator} + ({sessionId.slice(0, 8)}){modeLabel} {statusIcons[connectionStatus]} + + {cost.totalCost > 0.001 && ( + + Cost: + ${cost.totalCost.toFixed(4)} + (in: ${cost.inputCost.toFixed(4)} / out: ${cost.outputCost.toFixed(4)}) + + )} + + {sessionInfo.compactionCount > 0 && ( + + ⚠ Compacted {sessionInfo.compactionCount}x + + )} + + {sessionInfo.lastPrompt && ( + + Last: "{sessionInfo.lastPrompt.slice(0, 35)}..." + + )} + Ctrl+H toggle • Ctrl+C exit diff --git a/tui/src/lib/context-tracker.test.ts b/tui/src/lib/context-tracker.test.ts index 1a0c630..ea65b78 100644 --- a/tui/src/lib/context-tracker.test.ts +++ b/tui/src/lib/context-tracker.test.ts @@ -126,4 +126,61 @@ describe('ContextTracker', () => { expect(health.breakdown.toolOutputs).toBe(0); }); }); + + describe('getTokenHistory', () => { + it('should return empty array when no events processed', () => { + // #given - fresh tracker + // #when + const history = tracker.getTokenHistory(); + // #then + expect(history).toEqual([]); + }); + + it('should track token history as events are processed', () => { + // #given + const events = [ + { content: 'x'.repeat(100) }, + { content: 'x'.repeat(200) }, + { content: 'x'.repeat(300) }, + ]; + + // #when + events.forEach((response, i) => { + tracker.processEvent({ + event: 'PostToolUse', + tool: 'Read', + input: null, + response, + session: 'test', + ts: Date.now() / 1000 + i, + }); + }); + + // #then + const history = tracker.getTokenHistory(); + expect(history.length).toBe(3); + expect(history[0]).toBeLessThan(history[1]); + expect(history[1]).toBeLessThan(history[2]); + }); + + it('should include tokenHistory in getHealth output', () => { + // #given + tracker.processEvent({ + event: 'PostToolUse', + tool: 'Read', + input: null, + response: { content: 'test' }, + session: 'test', + ts: Date.now() / 1000, + }); + + // #when + const health = tracker.getHealth(); + + // #then + expect(health.tokenHistory).toBeDefined(); + expect(Array.isArray(health.tokenHistory)).toBe(true); + expect(health.tokenHistory.length).toBeGreaterThan(0); + }); + }); }); diff --git a/tui/src/lib/context-tracker.ts b/tui/src/lib/context-tracker.ts index d22c04c..d0d9a41 100644 --- a/tui/src/lib/context-tracker.ts +++ b/tui/src/lib/context-tracker.ts @@ -4,6 +4,7 @@ const MAX_TOKENS = 200000; const CHARS_PER_TOKEN = 4; const COMPACTION_THRESHOLD = 0.85; const WARNING_THRESHOLD = 0.70; +const SPARKLINE_SAMPLES = 20; export interface ContextHealth { tokens: number; @@ -16,6 +17,7 @@ export interface ContextHealth { breakdown: ContextBreakdown; sessionStart: number; lastUpdate: number; + tokenHistory: number[]; } export interface ContextBreakdown { @@ -106,6 +108,11 @@ export class ContextTracker { return 'healthy'; } + getTokenHistory(): number[] { + const history = this.tokenHistory.slice(-SPARKLINE_SAMPLES); + return history.map(s => s.tokens); + } + getHealth(): ContextHealth { const percent = Math.min((this.totalTokens / MAX_TOKENS) * 100, 100); const remaining = Math.max(MAX_TOKENS - this.totalTokens, 0); @@ -121,6 +128,7 @@ export class ContextTracker { breakdown: { ...this.breakdown }, sessionStart: this.sessionStart, lastUpdate: this.lastUpdate, + tokenHistory: this.getTokenHistory(), }; } diff --git a/tui/src/lib/cost-tracker.ts b/tui/src/lib/cost-tracker.ts new file mode 100644 index 0000000..40134e2 --- /dev/null +++ b/tui/src/lib/cost-tracker.ts @@ -0,0 +1,67 @@ +import type { CostEstimate, HudEvent } from './types.js'; + +// Anthropic pricing per 1M tokens (as of Jan 2025) +// Using Claude Sonnet 4 pricing as default +const PRICING = { + 'sonnet': { input: 3.00, output: 15.00 }, + 'opus': { input: 15.00, output: 75.00 }, + 'haiku': { input: 0.25, output: 1.25 }, +}; + +const CHARS_PER_TOKEN = 4; + +export class CostTracker { + private inputTokens = 0; + private outputTokens = 0; + private model: keyof typeof PRICING = 'sonnet'; + + setModel(model: string): void { + if (model.includes('opus')) { + this.model = 'opus'; + } else if (model.includes('haiku')) { + this.model = 'haiku'; + } else { + this.model = 'sonnet'; + } + } + + private estimateTokens(text: string): number { + if (!text) return 0; + return Math.ceil(text.length / CHARS_PER_TOKEN); + } + + processEvent(event: HudEvent): void { + if (event.event === 'PostToolUse') { + // Tool inputs are sent to Claude (input tokens) + if (event.input) { + this.inputTokens += this.estimateTokens(JSON.stringify(event.input)); + } + // Tool responses come back (output counted differently) + if (event.response) { + this.outputTokens += this.estimateTokens(JSON.stringify(event.response)); + } + } else if (event.event === 'UserPromptSubmit' && event.prompt) { + // User prompts are input + this.inputTokens += this.estimateTokens(event.prompt); + } + } + + getCost(): CostEstimate { + const pricing = PRICING[this.model]; + const inputCost = (this.inputTokens / 1_000_000) * pricing.input; + const outputCost = (this.outputTokens / 1_000_000) * pricing.output; + + return { + inputTokens: this.inputTokens, + outputTokens: this.outputTokens, + inputCost, + outputCost, + totalCost: inputCost + outputCost, + }; + } + + reset(): void { + this.inputTokens = 0; + this.outputTokens = 0; + } +} diff --git a/tui/src/lib/types.ts b/tui/src/lib/types.ts index 398a028..4a71a1b 100644 --- a/tui/src/lib/types.ts +++ b/tui/src/lib/types.ts @@ -1,10 +1,15 @@ export interface HudEvent { event: string; tool: string | null; + toolUseId?: string; input: Record | null; response: Record | null; session: string; ts: number; + permissionMode?: string; + transcriptPath?: string; + cwd?: string; + prompt?: string; } export interface ToolEntry { @@ -58,6 +63,7 @@ export interface ContextHealth { breakdown: ContextBreakdown; sessionStart: number; lastUpdate: number; + tokenHistory: number[]; } export interface ContextBreakdown { @@ -67,6 +73,23 @@ export interface ContextBreakdown { other: number; } +export interface SessionInfo { + permissionMode: string; + cwd: string; + transcriptPath: string; + isIdle: boolean; + lastPrompt: string; + compactionCount: number; +} + +export interface CostEstimate { + inputCost: number; + outputCost: number; + totalCost: number; + inputTokens: number; + outputTokens: number; +} + export interface AppState { events: HudEvent[]; tools: ToolEntry[]; @@ -74,4 +97,6 @@ export interface AppState { modifiedFiles: Map; context: ContextState; mcpServers: string[]; + sessionInfo: SessionInfo; + cost: CostEstimate; }