Files
claude-mem/src/services/context/formatters/HumanFormatter.ts
T
Alex Newman 5b041d6b49 refactor: rename formatters to AgentFormatter/HumanFormatter for semantic clarity
ColorFormatter and MarkdownFormatter names obscured their actual purpose.
The formatters serve two distinct audiences: the AI agent (compressed,
token-efficient context) and the human (rich ANSI-colored terminal output).

- MarkdownFormatter → AgentFormatter (renderMarkdown* → renderAgent*)
- ColorFormatter → HumanFormatter (renderColor* → renderHuman*)
- useColors parameter → forHuman across the pipeline
- Import aliases Color/Markdown → Human/Agent
- API query param `colors=true` unchanged (backward compatible)

Pure rename refactor — no logic or behavior changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:50:41 -07:00

239 lines
7.9 KiB
TypeScript

/**
* HumanFormatter - Formats context output with ANSI colors for terminal
*
* Handles all colored formatting for context injection (terminal display).
*/
import type {
ContextConfig,
Observation,
TokenEconomics,
PriorMessages,
} from '../types.js';
import { colors } from '../types.js';
import { ModeManager } from '../../domain/ModeManager.js';
import { formatObservationTokenDisplay } from '../TokenCalculator.js';
/**
* Format current date/time for header display
*/
function formatHeaderDateTime(): string {
const now = new Date();
const date = now.toLocaleDateString('en-CA'); // YYYY-MM-DD format
const time = now.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).toLowerCase().replace(' ', '');
const tz = now.toLocaleTimeString('en-US', { timeZoneName: 'short' }).split(' ').pop();
return `${date} ${time} ${tz}`;
}
/**
* Render human-readable header
*/
export function renderHumanHeader(project: string): string[] {
return [
'',
`${colors.bright}${colors.cyan}[${project}] recent context, ${formatHeaderDateTime()}${colors.reset}`,
`${colors.gray}${'─'.repeat(60)}${colors.reset}`,
''
];
}
/**
* Render human-readable legend
*/
export function renderHumanLegend(): string[] {
const mode = ModeManager.getInstance().getActiveMode();
const typeLegendItems = mode.observation_types.map(t => `${t.emoji} ${t.id}`).join(' | ');
return [
`${colors.dim}Legend: session-request | ${typeLegendItems}${colors.reset}`,
''
];
}
/**
* Render human-readable column key
*/
export function renderHumanColumnKey(): string[] {
return [
`${colors.bright}Column Key${colors.reset}`,
`${colors.dim} Read: Tokens to read this observation (cost to learn it now)${colors.reset}`,
`${colors.dim} Work: Tokens spent on work that produced this record ( research, building, deciding)${colors.reset}`,
''
];
}
/**
* Render human-readable context index instructions
*/
export function renderHumanContextIndex(): string[] {
return [
`${colors.dim}Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${colors.reset}`,
'',
`${colors.dim}When you need implementation details, rationale, or debugging context:${colors.reset}`,
`${colors.dim} - Fetch by ID: get_observations([IDs]) for observations visible in this index${colors.reset}`,
`${colors.dim} - Search history: Use the mem-search skill for past decisions, bugs, and deeper research${colors.reset}`,
`${colors.dim} - Trust this index over re-reading code for past decisions and learnings${colors.reset}`,
''
];
}
/**
* Render human-readable context economics
*/
export function renderHumanContextEconomics(
economics: TokenEconomics,
config: ContextConfig
): string[] {
const output: string[] = [];
output.push(`${colors.bright}${colors.cyan}Context Economics${colors.reset}`);
output.push(`${colors.dim} Loading: ${economics.totalObservations} observations (${economics.totalReadTokens.toLocaleString()} tokens to read)${colors.reset}`);
output.push(`${colors.dim} Work investment: ${economics.totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions${colors.reset}`);
if (economics.totalDiscoveryTokens > 0 && (config.showSavingsAmount || config.showSavingsPercent)) {
let savingsLine = ' Your savings: ';
if (config.showSavingsAmount && config.showSavingsPercent) {
savingsLine += `${economics.savings.toLocaleString()} tokens (${economics.savingsPercent}% reduction from reuse)`;
} else if (config.showSavingsAmount) {
savingsLine += `${economics.savings.toLocaleString()} tokens`;
} else {
savingsLine += `${economics.savingsPercent}% reduction from reuse`;
}
output.push(`${colors.green}${savingsLine}${colors.reset}`);
}
output.push('');
return output;
}
/**
* Render human-readable day header
*/
export function renderHumanDayHeader(day: string): string[] {
return [
`${colors.bright}${colors.cyan}${day}${colors.reset}`,
''
];
}
/**
* Render human-readable file header
*/
export function renderHumanFileHeader(file: string): string[] {
return [
`${colors.dim}${file}${colors.reset}`
];
}
/**
* Render human-readable table row for observation
*/
export function renderHumanTableRow(
obs: Observation,
time: string,
showTime: boolean,
config: ContextConfig
): string {
const title = obs.title || 'Untitled';
const icon = ModeManager.getInstance().getTypeIcon(obs.type);
const { readTokens, discoveryTokens, workEmoji } = formatObservationTokenDisplay(obs, config);
const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length);
const readPart = (config.showReadTokens && readTokens > 0) ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
const discoveryPart = (config.showWorkTokens && discoveryTokens > 0) ? `${colors.dim}(${workEmoji} ${discoveryTokens.toLocaleString()}t)${colors.reset}` : '';
return ` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${readPart} ${discoveryPart}`;
}
/**
* Render human-readable full observation
*/
export function renderHumanFullObservation(
obs: Observation,
time: string,
showTime: boolean,
detailField: string | null,
config: ContextConfig
): string[] {
const output: string[] = [];
const title = obs.title || 'Untitled';
const icon = ModeManager.getInstance().getTypeIcon(obs.type);
const { readTokens, discoveryTokens, workEmoji } = formatObservationTokenDisplay(obs, config);
const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length);
const readPart = (config.showReadTokens && readTokens > 0) ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
const discoveryPart = (config.showWorkTokens && discoveryTokens > 0) ? `${colors.dim}(${workEmoji} ${discoveryTokens.toLocaleString()}t)${colors.reset}` : '';
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${colors.bright}${title}${colors.reset}`);
if (detailField) {
output.push(` ${colors.dim}${detailField}${colors.reset}`);
}
if (readPart || discoveryPart) {
output.push(` ${readPart} ${discoveryPart}`);
}
output.push('');
return output;
}
/**
* Render human-readable summary item in timeline
*/
export function renderHumanSummaryItem(
summary: { id: number; request: string | null },
formattedTime: string
): string[] {
const summaryTitle = `${summary.request || 'Session started'} (${formattedTime})`;
return [
`${colors.yellow}#S${summary.id}${colors.reset} ${summaryTitle}`,
''
];
}
/**
* Render human-readable summary field
*/
export function renderHumanSummaryField(label: string, value: string | null, color: string): string[] {
if (!value) return [];
return [`${color}${label}:${colors.reset} ${value}`, ''];
}
/**
* Render human-readable previously section
*/
export function renderHumanPreviouslySection(priorMessages: PriorMessages): string[] {
if (!priorMessages.assistantMessage) return [];
return [
'',
'---',
'',
`${colors.bright}${colors.magenta}Previously${colors.reset}`,
'',
`${colors.dim}A: ${priorMessages.assistantMessage}${colors.reset}`,
''
];
}
/**
* Render human-readable footer
*/
export function renderHumanFooter(totalDiscoveryTokens: number, totalReadTokens: number): string[] {
const workTokensK = Math.round(totalDiscoveryTokens / 1000);
return [
'',
`${colors.dim}Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use the claude-mem skill to access memories by ID.${colors.reset}`
];
}
/**
* Render human-readable empty state
*/
export function renderHumanEmptyState(project: string): string {
return `\n${colors.bright}${colors.cyan}[${project}] recent context, ${formatHeaderDateTime()}${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous sessions found for this project yet.${colors.reset}\n`;
}