0a26bb18bf
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
242 lines
6.6 KiB
TypeScript
242 lines
6.6 KiB
TypeScript
/**
|
|
* MarkdownFormatter - Formats context output as markdown (non-colored mode)
|
|
*
|
|
* Handles all markdown formatting for context injection.
|
|
*/
|
|
|
|
import type {
|
|
ContextConfig,
|
|
Observation,
|
|
SessionSummary,
|
|
TokenEconomics,
|
|
PriorMessages,
|
|
} 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 markdown header
|
|
*/
|
|
export function renderMarkdownHeader(project: string): string[] {
|
|
return [
|
|
`# [${project}] recent context, ${formatHeaderDateTime()}`,
|
|
''
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render markdown legend
|
|
*/
|
|
export function renderMarkdownLegend(): string[] {
|
|
const mode = ModeManager.getInstance().getActiveMode();
|
|
const typeLegendItems = mode.observation_types.map(t => `${t.emoji} ${t.id}`).join(' | ');
|
|
|
|
return [
|
|
`**Legend:** session-request | ${typeLegendItems}`,
|
|
''
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render markdown column key
|
|
*/
|
|
export function renderMarkdownColumnKey(): string[] {
|
|
return [
|
|
`**Column Key**:`,
|
|
`- **Read**: Tokens to read this observation (cost to learn it now)`,
|
|
`- **Work**: Tokens spent on work that produced this record ( research, building, deciding)`,
|
|
''
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render markdown context index instructions
|
|
*/
|
|
export function renderMarkdownContextIndex(): string[] {
|
|
return [
|
|
`**Context Index:** This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.`,
|
|
'',
|
|
`When you need implementation details, rationale, or debugging context:`,
|
|
`- Fetch by ID: get_observations([IDs]) for observations visible in this index`,
|
|
`- Search history: Use the mem-search skill for past decisions, bugs, and deeper research`,
|
|
`- Trust this index over re-reading code for past decisions and learnings`,
|
|
''
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render markdown context economics
|
|
*/
|
|
export function renderMarkdownContextEconomics(
|
|
economics: TokenEconomics,
|
|
config: ContextConfig
|
|
): string[] {
|
|
const output: string[] = [];
|
|
|
|
output.push(`**Context Economics**:`);
|
|
output.push(`- Loading: ${economics.totalObservations} observations (${economics.totalReadTokens.toLocaleString()} tokens to read)`);
|
|
output.push(`- Work investment: ${economics.totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions`);
|
|
|
|
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(savingsLine);
|
|
}
|
|
output.push('');
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Render markdown day header
|
|
*/
|
|
export function renderMarkdownDayHeader(day: string): string[] {
|
|
return [
|
|
`### ${day}`,
|
|
''
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render markdown file header with table header
|
|
*/
|
|
export function renderMarkdownFileHeader(file: string): string[] {
|
|
return [
|
|
`**${file}**`,
|
|
`| ID | Time | T | Title | Read | Work |`,
|
|
`|----|------|---|-------|------|------|`
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render markdown table row for observation
|
|
*/
|
|
export function renderMarkdownTableRow(
|
|
obs: Observation,
|
|
timeDisplay: string,
|
|
config: ContextConfig
|
|
): string {
|
|
const title = obs.title || 'Untitled';
|
|
const icon = ModeManager.getInstance().getTypeIcon(obs.type);
|
|
const { readTokens, discoveryDisplay } = formatObservationTokenDisplay(obs, config);
|
|
|
|
const readCol = config.showReadTokens ? `~${readTokens}` : '';
|
|
const workCol = config.showWorkTokens ? discoveryDisplay : '';
|
|
|
|
return `| #${obs.id} | ${timeDisplay || '"'} | ${icon} | ${title} | ${readCol} | ${workCol} |`;
|
|
}
|
|
|
|
/**
|
|
* Render markdown full observation
|
|
*/
|
|
export function renderMarkdownFullObservation(
|
|
obs: Observation,
|
|
timeDisplay: string,
|
|
detailField: string | null,
|
|
config: ContextConfig
|
|
): string[] {
|
|
const output: string[] = [];
|
|
const title = obs.title || 'Untitled';
|
|
const icon = ModeManager.getInstance().getTypeIcon(obs.type);
|
|
const { readTokens, discoveryDisplay } = formatObservationTokenDisplay(obs, config);
|
|
|
|
output.push(`**#${obs.id}** ${timeDisplay || '"'} ${icon} **${title}**`);
|
|
if (detailField) {
|
|
output.push('');
|
|
output.push(detailField);
|
|
output.push('');
|
|
}
|
|
|
|
const tokenParts: string[] = [];
|
|
if (config.showReadTokens) {
|
|
tokenParts.push(`Read: ~${readTokens}`);
|
|
}
|
|
if (config.showWorkTokens) {
|
|
tokenParts.push(`Work: ${discoveryDisplay}`);
|
|
}
|
|
if (tokenParts.length > 0) {
|
|
output.push(tokenParts.join(', '));
|
|
}
|
|
output.push('');
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Render markdown summary item in timeline
|
|
*/
|
|
export function renderMarkdownSummaryItem(
|
|
summary: { id: number; request: string | null },
|
|
formattedTime: string
|
|
): string[] {
|
|
const summaryTitle = `${summary.request || 'Session started'} (${formattedTime})`;
|
|
return [
|
|
`**#S${summary.id}** ${summaryTitle}`,
|
|
''
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render markdown summary field
|
|
*/
|
|
export function renderMarkdownSummaryField(label: string, value: string | null): string[] {
|
|
if (!value) return [];
|
|
return [`**${label}**: ${value}`, ''];
|
|
}
|
|
|
|
/**
|
|
* Render markdown previously section
|
|
*/
|
|
export function renderMarkdownPreviouslySection(priorMessages: PriorMessages): string[] {
|
|
if (!priorMessages.assistantMessage) return [];
|
|
|
|
return [
|
|
'',
|
|
'---',
|
|
'',
|
|
`**Previously**`,
|
|
'',
|
|
`A: ${priorMessages.assistantMessage}`,
|
|
''
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render markdown footer
|
|
*/
|
|
export function renderMarkdownFooter(totalDiscoveryTokens: number, totalReadTokens: number): string[] {
|
|
const workTokensK = Math.round(totalDiscoveryTokens / 1000);
|
|
return [
|
|
'',
|
|
`Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use the claude-mem skill to access memories by ID.`
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Render markdown empty state
|
|
*/
|
|
export function renderMarkdownEmptyState(project: string): string {
|
|
return `# [${project}] recent context, ${formatHeaderDateTime()}\n\nNo previous sessions found for this project yet.`;
|
|
}
|