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>
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* 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`;
|
||||
}
|
||||
Reference in New Issue
Block a user