5b041d6b49
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>
239 lines
7.9 KiB
TypeScript
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`;
|
|
}
|