docs: add hud config and replay tooling

This commit is contained in:
Jarrod Watts
2026-01-03 10:05:07 +11:00
parent 71f4a534db
commit cf80758302
6 changed files with 160 additions and 1 deletions

View File

@@ -17,6 +17,7 @@ bun run build # Build TypeScript
bun run dev # Watch mode for development
bun test # Run all tests
bun test <pattern> # Run specific test (e.g., bun test sparkline)
bun run replay:events -- --input ../tui/test-fixtures/hud-events.jsonl # Replay events
# Manual testing with a FIFO
mkfifo /tmp/test.fifo
@@ -77,6 +78,20 @@ Runtime files stored in `~/.claude/hud/`:
- `pids/<session_id>.pid` - Process ID for cleanup
- `logs/<session_id>.log` - Fallback output when split pane unavailable
## HUD Configuration
Optional HUD config lives at `~/.claude/hud/config.json`:
```json
{
"panelOrder": ["status", "context", "tools", "agents", "todos"],
"hiddenPanels": ["cost"],
"width": 56
}
```
Panel IDs: `status`, `context`, `cost`, `contextInfo`, `tools`, `agents`, `todos`.
## Dependencies
- **Runtime**: Node.js 18+ or Bun, jq (JSON parsing in hooks)

View File

@@ -2,6 +2,10 @@
Thanks for your interest in contributing! This guide will help you get started.
## Community Standards
Please read `CODE_OF_CONDUCT.md`. For security issues, see `SECURITY.md`.
## Development Setup
```bash
@@ -28,6 +32,17 @@ bun run typecheck
# Format code
bun run format
# Replay a fixture event stream
bun run replay:events -- --input ../tui/test-fixtures/hud-events.jsonl
```
### One-shot checks
From the repo root:
```bash
./scripts/check.sh
```
### Local Testing

23
docs/adr/005-hud-store.md Normal file
View File

@@ -0,0 +1,23 @@
# 005 - HUD Store and Reducer Architecture
## Status
Accepted
## Context
The HUD app was previously managing IO and state derivation inside a React hook.
That mixed FIFO reading, settings/config scanning, and UI state updates in one
place, which made testing and reuse difficult.
## Decision
Introduce a `HudStore` that owns IO and side effects, and a pure reducer for
state transitions.
- `tui/src/state/hud-store.ts` manages EventReader, trackers, and environment
refresh intervals.
- `tui/src/state/hud-reducer.ts` handles event-driven state transitions.
- `tui/src/state/hud-state.ts` defines public state and internal tracking.
## Consequences
- UI code subscribes to a stable store and stays focused on rendering.
- Event handling is testable in isolation via reducer tests.
- Future renderers can reuse the store without React.

View File

@@ -13,7 +13,8 @@
"lint:fix": "eslint src/ --fix",
"format": "prettier --write src/",
"format:check": "prettier --check src/",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"replay:events": "bun run scripts/replay-events.ts --"
},
"lint-staged": {
"*.{ts,tsx}": [

View File

@@ -0,0 +1,100 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { EventEmitter } from 'node:events';
import { HudStore } from '../src/state/hud-store.js';
import type { EventSource } from '../src/state/hud-store.js';
import { parseHudEvent } from '../src/lib/hud-event.js';
type Args = {
input?: string;
json?: boolean;
};
function parseArgs(argv: string[]): Args {
const args: Args = {};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--input') {
args.input = argv[i + 1];
i += 1;
} else if (arg === '--json') {
args.json = true;
}
}
return args;
}
function createEventSource() {
const emitter = new EventEmitter();
const source: EventSource = {
on(event, listener) {
emitter.on(event, listener);
},
getStatus() {
return 'connected';
},
close() {
emitter.removeAllListeners();
},
switchFifo() {
return;
},
};
return { emitter, source };
}
function usage(): void {
console.log('Usage: bun run replay:events -- --input path/to/events.jsonl [--json]');
}
const args = parseArgs(process.argv.slice(2));
if (!args.input) {
usage();
process.exit(1);
}
const inputPath = resolve(args.input);
const contents = readFileSync(inputPath, 'utf-8');
const lines = contents.split('\n').filter((line) => line.trim().length > 0);
const { emitter, source } = createEventSource();
const store = new HudStore({
fifoPath: 'replay',
clockIntervalMs: 0,
eventSourceFactory: () => source,
});
lines.forEach((line, index) => {
const event = parseHudEvent(line);
if (!event) {
console.warn(`Skipping invalid event line ${index + 1}`);
return;
}
emitter.emit('event', event);
const state = store.getState();
if (args.json) {
console.log(
JSON.stringify(
{
index: index + 1,
event: event.event,
tools: state.tools.length,
todos: state.todos.length,
agents: state.agents.length,
cost: state.cost.totalCost,
contextPercent: state.context.percent,
},
null,
2,
),
);
} else {
console.log(
`#${index + 1} ${event.event} tools=${state.tools.length} todos=${state.todos.length} agents=${state.agents.length} cost=$${state.cost.totalCost.toFixed(
2,
)}`,
);
}
});
store.dispose();

View File

@@ -0,0 +1,5 @@
{"event":"UserPromptSubmit","session":"fixture","ts":1,"prompt":"Hello"}
{"event":"PreToolUse","tool":"Read","toolUseId":"tool-1","input":{"file_path":"README.md"},"response":null,"session":"fixture","ts":2}
{"event":"PostToolUse","tool":"Read","toolUseId":"tool-1","input":null,"response":{"duration_ms":200},"session":"fixture","ts":2}
{"event":"PreToolUse","tool":"TodoWrite","toolUseId":"tool-2","input":{"todos":[{"content":"Ship HUD tests","status":"in_progress"}]},"response":null,"session":"fixture","ts":3}
{"event":"Stop","tool":null,"input":null,"response":null,"session":"fixture","ts":4}