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:
Alex Newman
2026-03-21 11:50:41 -07:00
parent 9f529a30f5
commit 5b041d6b49
9 changed files with 247 additions and 305 deletions
+13 -13
View File
@@ -29,8 +29,8 @@ import { renderHeader } from './sections/HeaderRenderer.js';
import { renderTimeline } from './sections/TimelineRenderer.js'; import { renderTimeline } from './sections/TimelineRenderer.js';
import { shouldShowSummary, renderSummaryFields } from './sections/SummaryRenderer.js'; import { shouldShowSummary, renderSummaryFields } from './sections/SummaryRenderer.js';
import { renderPreviouslySection, renderFooter } from './sections/FooterRenderer.js'; import { renderPreviouslySection, renderFooter } from './sections/FooterRenderer.js';
import { renderMarkdownEmptyState } from './formatters/MarkdownFormatter.js'; import { renderAgentEmptyState } from './formatters/AgentFormatter.js';
import { renderColorEmptyState } from './formatters/ColorFormatter.js'; import { renderHumanEmptyState } from './formatters/HumanFormatter.js';
// Version marker path for native module error handling // Version marker path for native module error handling
const VERSION_MARKER_PATH = path.join( const VERSION_MARKER_PATH = path.join(
@@ -66,8 +66,8 @@ function initializeDatabase(): SessionStore | null {
/** /**
* Render empty state when no data exists * Render empty state when no data exists
*/ */
function renderEmptyState(project: string, useColors: boolean): string { function renderEmptyState(project: string, forHuman: boolean): string {
return useColors ? renderColorEmptyState(project) : renderMarkdownEmptyState(project); return forHuman ? renderHumanEmptyState(project) : renderAgentEmptyState(project);
} }
/** /**
@@ -80,7 +80,7 @@ function buildContextOutput(
config: ContextConfig, config: ContextConfig,
cwd: string, cwd: string,
sessionId: string | undefined, sessionId: string | undefined,
useColors: boolean forHuman: boolean
): string { ): string {
const output: string[] = []; const output: string[] = [];
@@ -88,7 +88,7 @@ function buildContextOutput(
const economics = calculateTokenEconomics(observations); const economics = calculateTokenEconomics(observations);
// Render header section // Render header section
output.push(...renderHeader(project, economics, config, useColors)); output.push(...renderHeader(project, economics, config, forHuman));
// Prepare timeline data // Prepare timeline data
const displaySummaries = summaries.slice(0, config.sessionCount); const displaySummaries = summaries.slice(0, config.sessionCount);
@@ -97,22 +97,22 @@ function buildContextOutput(
const fullObservationIds = getFullObservationIds(observations, config.fullObservationCount); const fullObservationIds = getFullObservationIds(observations, config.fullObservationCount);
// Render timeline // Render timeline
output.push(...renderTimeline(timeline, fullObservationIds, config, cwd, useColors)); output.push(...renderTimeline(timeline, fullObservationIds, config, cwd, forHuman));
// Render most recent summary if applicable // Render most recent summary if applicable
const mostRecentSummary = summaries[0]; const mostRecentSummary = summaries[0];
const mostRecentObservation = observations[0]; const mostRecentObservation = observations[0];
if (shouldShowSummary(config, mostRecentSummary, mostRecentObservation)) { if (shouldShowSummary(config, mostRecentSummary, mostRecentObservation)) {
output.push(...renderSummaryFields(mostRecentSummary, useColors)); output.push(...renderSummaryFields(mostRecentSummary, forHuman));
} }
// Render previously section (prior assistant message) // Render previously section (prior assistant message)
const priorMessages = getPriorSessionMessages(observations, config, sessionId, cwd); const priorMessages = getPriorSessionMessages(observations, config, sessionId, cwd);
output.push(...renderPreviouslySection(priorMessages, useColors)); output.push(...renderPreviouslySection(priorMessages, forHuman));
// Render footer // Render footer
output.push(...renderFooter(economics, config, useColors)); output.push(...renderFooter(economics, config, forHuman));
return output.join('\n').trimEnd(); return output.join('\n').trimEnd();
} }
@@ -125,7 +125,7 @@ function buildContextOutput(
*/ */
export async function generateContext( export async function generateContext(
input?: ContextInput, input?: ContextInput,
useColors: boolean = false forHuman: boolean = false
): Promise<string> { ): Promise<string> {
const config = loadContextConfig(); const config = loadContextConfig();
const cwd = input?.cwd ?? process.cwd(); const cwd = input?.cwd ?? process.cwd();
@@ -157,7 +157,7 @@ export async function generateContext(
// Handle empty state // Handle empty state
if (observations.length === 0 && summaries.length === 0) { if (observations.length === 0 && summaries.length === 0) {
return renderEmptyState(project, useColors); return renderEmptyState(project, forHuman);
} }
// Build and return context // Build and return context
@@ -168,7 +168,7 @@ export async function generateContext(
config, config,
cwd, cwd,
input?.session_id, input?.session_id,
useColors forHuman
); );
return output; return output;
@@ -1,8 +1,8 @@
/** /**
* MarkdownFormatter - Formats context output as compact markdown for LLM injection * AgentFormatter - Formats context output as compact markdown for LLM injection
* *
* Optimized for token efficiency: flat lines instead of tables, no repeated headers. * Optimized for token efficiency: flat lines instead of tables, no repeated headers.
* The colored terminal formatter (ColorFormatter.ts) handles human-readable display separately. * The human-readable terminal formatter (HumanFormatter.ts) handles human-readable display separately.
*/ */
import type { import type {
@@ -31,9 +31,9 @@ function formatHeaderDateTime(): string {
} }
/** /**
* Render markdown header * Render agent header
*/ */
export function renderMarkdownHeader(project: string): string[] { export function renderAgentHeader(project: string): string[] {
return [ return [
`# $CMEM ${project} ${formatHeaderDateTime()}`, `# $CMEM ${project} ${formatHeaderDateTime()}`,
'' ''
@@ -41,9 +41,9 @@ export function renderMarkdownHeader(project: string): string[] {
} }
/** /**
* Render markdown legend * Render agent legend
*/ */
export function renderMarkdownLegend(): string[] { export function renderAgentLegend(): string[] {
const mode = ModeManager.getInstance().getActiveMode(); const mode = ModeManager.getInstance().getActiveMode();
const typeLegendItems = mode.observation_types.map(t => `${t.emoji}${t.id}`).join(' '); const typeLegendItems = mode.observation_types.map(t => `${t.emoji}${t.id}`).join(' ');
@@ -56,23 +56,23 @@ export function renderMarkdownLegend(): string[] {
} }
/** /**
* Render markdown column key - no longer needed in compact format * Render agent column key - no longer needed in compact format
*/ */
export function renderMarkdownColumnKey(): string[] { export function renderAgentColumnKey(): string[] {
return []; return [];
} }
/** /**
* Render markdown context index instructions - folded into legend * Render agent context index instructions - folded into legend
*/ */
export function renderMarkdownContextIndex(): string[] { export function renderAgentContextIndex(): string[] {
return []; return [];
} }
/** /**
* Render markdown context economics * Render agent context economics
*/ */
export function renderMarkdownContextEconomics( export function renderAgentContextEconomics(
economics: TokenEconomics, economics: TokenEconomics,
config: ContextConfig config: ContextConfig
): string[] { ): string[] {
@@ -98,18 +98,18 @@ export function renderMarkdownContextEconomics(
} }
/** /**
* Render markdown day header * Render agent day header
*/ */
export function renderMarkdownDayHeader(day: string): string[] { export function renderAgentDayHeader(day: string): string[] {
return [ return [
`### ${day}`, `### ${day}`,
]; ];
} }
/** /**
* Render markdown file header - no longer renders table headers in compact format * Render agent file header - no longer renders table headers in compact format
*/ */
export function renderMarkdownFileHeader(_file: string): string[] { export function renderAgentFileHeader(_file: string): string[] {
// File grouping eliminated in compact format - file context is in observation titles // File grouping eliminated in compact format - file context is in observation titles
return []; return [];
} }
@@ -124,7 +124,7 @@ function compactTime(time: string): string {
/** /**
* Render compact flat line for observation (replaces table row) * Render compact flat line for observation (replaces table row)
*/ */
export function renderMarkdownTableRow( export function renderAgentTableRow(
obs: Observation, obs: Observation,
timeDisplay: string, timeDisplay: string,
_config: ContextConfig _config: ContextConfig
@@ -137,9 +137,9 @@ export function renderMarkdownTableRow(
} }
/** /**
* Render markdown full observation * Render agent full observation
*/ */
export function renderMarkdownFullObservation( export function renderAgentFullObservation(
obs: Observation, obs: Observation,
timeDisplay: string, timeDisplay: string,
detailField: string | null, detailField: string | null,
@@ -172,9 +172,9 @@ export function renderMarkdownFullObservation(
} }
/** /**
* Render markdown summary item in timeline * Render agent summary item in timeline
*/ */
export function renderMarkdownSummaryItem( export function renderAgentSummaryItem(
summary: { id: number; request: string | null }, summary: { id: number; request: string | null },
formattedTime: string formattedTime: string
): string[] { ): string[] {
@@ -184,17 +184,17 @@ export function renderMarkdownSummaryItem(
} }
/** /**
* Render markdown summary field * Render agent summary field
*/ */
export function renderMarkdownSummaryField(label: string, value: string | null): string[] { export function renderAgentSummaryField(label: string, value: string | null): string[] {
if (!value) return []; if (!value) return [];
return [`**${label}**: ${value}`, '']; return [`**${label}**: ${value}`, ''];
} }
/** /**
* Render markdown previously section * Render agent previously section
*/ */
export function renderMarkdownPreviouslySection(priorMessages: PriorMessages): string[] { export function renderAgentPreviouslySection(priorMessages: PriorMessages): string[] {
if (!priorMessages.assistantMessage) return []; if (!priorMessages.assistantMessage) return [];
return [ return [
@@ -209,9 +209,9 @@ export function renderMarkdownPreviouslySection(priorMessages: PriorMessages): s
} }
/** /**
* Render markdown footer * Render agent footer
*/ */
export function renderMarkdownFooter(totalDiscoveryTokens: number, totalReadTokens: number): string[] { export function renderAgentFooter(totalDiscoveryTokens: number, totalReadTokens: number): string[] {
const workTokensK = Math.round(totalDiscoveryTokens / 1000); const workTokensK = Math.round(totalDiscoveryTokens / 1000);
return [ return [
'', '',
@@ -220,8 +220,8 @@ export function renderMarkdownFooter(totalDiscoveryTokens: number, totalReadToke
} }
/** /**
* Render markdown empty state * Render agent empty state
*/ */
export function renderMarkdownEmptyState(project: string): string { export function renderAgentEmptyState(project: string): string {
return `# $CMEM ${project} ${formatHeaderDateTime()}\n\nNo previous sessions found.`; return `# $CMEM ${project} ${formatHeaderDateTime()}\n\nNo previous sessions found.`;
} }
@@ -1,5 +1,5 @@
/** /**
* ColorFormatter - Formats context output with ANSI colors for terminal * HumanFormatter - Formats context output with ANSI colors for terminal
* *
* Handles all colored formatting for context injection (terminal display). * Handles all colored formatting for context injection (terminal display).
*/ */
@@ -30,9 +30,9 @@ function formatHeaderDateTime(): string {
} }
/** /**
* Render colored header * Render human-readable header
*/ */
export function renderColorHeader(project: string): string[] { export function renderHumanHeader(project: string): string[] {
return [ return [
'', '',
`${colors.bright}${colors.cyan}[${project}] recent context, ${formatHeaderDateTime()}${colors.reset}`, `${colors.bright}${colors.cyan}[${project}] recent context, ${formatHeaderDateTime()}${colors.reset}`,
@@ -42,9 +42,9 @@ export function renderColorHeader(project: string): string[] {
} }
/** /**
* Render colored legend * Render human-readable legend
*/ */
export function renderColorLegend(): string[] { export function renderHumanLegend(): string[] {
const mode = ModeManager.getInstance().getActiveMode(); const mode = ModeManager.getInstance().getActiveMode();
const typeLegendItems = mode.observation_types.map(t => `${t.emoji} ${t.id}`).join(' | '); const typeLegendItems = mode.observation_types.map(t => `${t.emoji} ${t.id}`).join(' | ');
@@ -55,9 +55,9 @@ export function renderColorLegend(): string[] {
} }
/** /**
* Render colored column key * Render human-readable column key
*/ */
export function renderColorColumnKey(): string[] { export function renderHumanColumnKey(): string[] {
return [ return [
`${colors.bright}Column Key${colors.reset}`, `${colors.bright}Column Key${colors.reset}`,
`${colors.dim} Read: Tokens to read this observation (cost to learn it now)${colors.reset}`, `${colors.dim} Read: Tokens to read this observation (cost to learn it now)${colors.reset}`,
@@ -67,9 +67,9 @@ export function renderColorColumnKey(): string[] {
} }
/** /**
* Render colored context index instructions * Render human-readable context index instructions
*/ */
export function renderColorContextIndex(): string[] { export function renderHumanContextIndex(): string[] {
return [ return [
`${colors.dim}Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${colors.reset}`, `${colors.dim}Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${colors.reset}`,
'', '',
@@ -82,9 +82,9 @@ export function renderColorContextIndex(): string[] {
} }
/** /**
* Render colored context economics * Render human-readable context economics
*/ */
export function renderColorContextEconomics( export function renderHumanContextEconomics(
economics: TokenEconomics, economics: TokenEconomics,
config: ContextConfig config: ContextConfig
): string[] { ): string[] {
@@ -111,9 +111,9 @@ export function renderColorContextEconomics(
} }
/** /**
* Render colored day header * Render human-readable day header
*/ */
export function renderColorDayHeader(day: string): string[] { export function renderHumanDayHeader(day: string): string[] {
return [ return [
`${colors.bright}${colors.cyan}${day}${colors.reset}`, `${colors.bright}${colors.cyan}${day}${colors.reset}`,
'' ''
@@ -121,18 +121,18 @@ export function renderColorDayHeader(day: string): string[] {
} }
/** /**
* Render colored file header * Render human-readable file header
*/ */
export function renderColorFileHeader(file: string): string[] { export function renderHumanFileHeader(file: string): string[] {
return [ return [
`${colors.dim}${file}${colors.reset}` `${colors.dim}${file}${colors.reset}`
]; ];
} }
/** /**
* Render colored table row for observation * Render human-readable table row for observation
*/ */
export function renderColorTableRow( export function renderHumanTableRow(
obs: Observation, obs: Observation,
time: string, time: string,
showTime: boolean, showTime: boolean,
@@ -150,9 +150,9 @@ export function renderColorTableRow(
} }
/** /**
* Render colored full observation * Render human-readable full observation
*/ */
export function renderColorFullObservation( export function renderHumanFullObservation(
obs: Observation, obs: Observation,
time: string, time: string,
showTime: boolean, showTime: boolean,
@@ -181,9 +181,9 @@ export function renderColorFullObservation(
} }
/** /**
* Render colored summary item in timeline * Render human-readable summary item in timeline
*/ */
export function renderColorSummaryItem( export function renderHumanSummaryItem(
summary: { id: number; request: string | null }, summary: { id: number; request: string | null },
formattedTime: string formattedTime: string
): string[] { ): string[] {
@@ -195,17 +195,17 @@ export function renderColorSummaryItem(
} }
/** /**
* Render colored summary field * Render human-readable summary field
*/ */
export function renderColorSummaryField(label: string, value: string | null, color: string): string[] { export function renderHumanSummaryField(label: string, value: string | null, color: string): string[] {
if (!value) return []; if (!value) return [];
return [`${color}${label}:${colors.reset} ${value}`, '']; return [`${color}${label}:${colors.reset} ${value}`, ''];
} }
/** /**
* Render colored previously section * Render human-readable previously section
*/ */
export function renderColorPreviouslySection(priorMessages: PriorMessages): string[] { export function renderHumanPreviouslySection(priorMessages: PriorMessages): string[] {
if (!priorMessages.assistantMessage) return []; if (!priorMessages.assistantMessage) return [];
return [ return [
@@ -220,9 +220,9 @@ export function renderColorPreviouslySection(priorMessages: PriorMessages): stri
} }
/** /**
* Render colored footer * Render human-readable footer
*/ */
export function renderColorFooter(totalDiscoveryTokens: number, totalReadTokens: number): string[] { export function renderHumanFooter(totalDiscoveryTokens: number, totalReadTokens: number): string[] {
const workTokensK = Math.round(totalDiscoveryTokens / 1000); const workTokensK = Math.round(totalDiscoveryTokens / 1000);
return [ return [
'', '',
@@ -231,8 +231,8 @@ export function renderColorFooter(totalDiscoveryTokens: number, totalReadTokens:
} }
/** /**
* Render colored empty state * Render human-readable empty state
*/ */
export function renderColorEmptyState(project: string): string { 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`; 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`;
} }
+10 -10
View File
@@ -6,20 +6,20 @@
import type { ContextConfig, TokenEconomics, PriorMessages } from '../types.js'; import type { ContextConfig, TokenEconomics, PriorMessages } from '../types.js';
import { shouldShowContextEconomics } from '../TokenCalculator.js'; import { shouldShowContextEconomics } from '../TokenCalculator.js';
import * as Markdown from '../formatters/MarkdownFormatter.js'; import * as Agent from '../formatters/AgentFormatter.js';
import * as Color from '../formatters/ColorFormatter.js'; import * as Human from '../formatters/HumanFormatter.js';
/** /**
* Render the previously section (prior assistant message) * Render the previously section (prior assistant message)
*/ */
export function renderPreviouslySection( export function renderPreviouslySection(
priorMessages: PriorMessages, priorMessages: PriorMessages,
useColors: boolean forHuman: boolean
): string[] { ): string[] {
if (useColors) { if (forHuman) {
return Color.renderColorPreviouslySection(priorMessages); return Human.renderHumanPreviouslySection(priorMessages);
} }
return Markdown.renderMarkdownPreviouslySection(priorMessages); return Agent.renderAgentPreviouslySection(priorMessages);
} }
/** /**
@@ -28,15 +28,15 @@ export function renderPreviouslySection(
export function renderFooter( export function renderFooter(
economics: TokenEconomics, economics: TokenEconomics,
config: ContextConfig, config: ContextConfig,
useColors: boolean forHuman: boolean
): string[] { ): string[] {
// Only show footer if we have savings to display // Only show footer if we have savings to display
if (!shouldShowContextEconomics(config) || economics.totalDiscoveryTokens <= 0 || economics.savings <= 0) { if (!shouldShowContextEconomics(config) || economics.totalDiscoveryTokens <= 0 || economics.savings <= 0) {
return []; return [];
} }
if (useColors) { if (forHuman) {
return Color.renderColorFooter(economics.totalDiscoveryTokens, economics.totalReadTokens); return Human.renderHumanFooter(economics.totalDiscoveryTokens, economics.totalReadTokens);
} }
return Markdown.renderMarkdownFooter(economics.totalDiscoveryTokens, economics.totalReadTokens); return Agent.renderAgentFooter(economics.totalDiscoveryTokens, economics.totalReadTokens);
} }
+18 -18
View File
@@ -6,8 +6,8 @@
import type { ContextConfig, TokenEconomics } from '../types.js'; import type { ContextConfig, TokenEconomics } from '../types.js';
import { shouldShowContextEconomics } from '../TokenCalculator.js'; import { shouldShowContextEconomics } from '../TokenCalculator.js';
import * as Markdown from '../formatters/MarkdownFormatter.js'; import * as Agent from '../formatters/AgentFormatter.js';
import * as Color from '../formatters/ColorFormatter.js'; import * as Human from '../formatters/HumanFormatter.js';
/** /**
* Render the complete header section * Render the complete header section
@@ -16,44 +16,44 @@ export function renderHeader(
project: string, project: string,
economics: TokenEconomics, economics: TokenEconomics,
config: ContextConfig, config: ContextConfig,
useColors: boolean forHuman: boolean
): string[] { ): string[] {
const output: string[] = []; const output: string[] = [];
// Main header // Main header
if (useColors) { if (forHuman) {
output.push(...Color.renderColorHeader(project)); output.push(...Human.renderHumanHeader(project));
} else { } else {
output.push(...Markdown.renderMarkdownHeader(project)); output.push(...Agent.renderAgentHeader(project));
} }
// Legend // Legend
if (useColors) { if (forHuman) {
output.push(...Color.renderColorLegend()); output.push(...Human.renderHumanLegend());
} else { } else {
output.push(...Markdown.renderMarkdownLegend()); output.push(...Agent.renderAgentLegend());
} }
// Column key // Column key
if (useColors) { if (forHuman) {
output.push(...Color.renderColorColumnKey()); output.push(...Human.renderHumanColumnKey());
} else { } else {
output.push(...Markdown.renderMarkdownColumnKey()); output.push(...Agent.renderAgentColumnKey());
} }
// Context index instructions // Context index instructions
if (useColors) { if (forHuman) {
output.push(...Color.renderColorContextIndex()); output.push(...Human.renderHumanContextIndex());
} else { } else {
output.push(...Markdown.renderMarkdownContextIndex()); output.push(...Agent.renderAgentContextIndex());
} }
// Context economics // Context economics
if (shouldShowContextEconomics(config)) { if (shouldShowContextEconomics(config)) {
if (useColors) { if (forHuman) {
output.push(...Color.renderColorContextEconomics(economics, config)); output.push(...Human.renderHumanContextEconomics(economics, config));
} else { } else {
output.push(...Markdown.renderMarkdownContextEconomics(economics, config)); output.push(...Agent.renderAgentContextEconomics(economics, config));
} }
} }
@@ -6,8 +6,8 @@
import type { ContextConfig, Observation, SessionSummary } from '../types.js'; import type { ContextConfig, Observation, SessionSummary } from '../types.js';
import { colors } from '../types.js'; import { colors } from '../types.js';
import * as Markdown from '../formatters/MarkdownFormatter.js'; import * as Agent from '../formatters/AgentFormatter.js';
import * as Color from '../formatters/ColorFormatter.js'; import * as Human from '../formatters/HumanFormatter.js';
/** /**
* Check if summary should be displayed * Check if summary should be displayed
@@ -45,20 +45,20 @@ export function shouldShowSummary(
*/ */
export function renderSummaryFields( export function renderSummaryFields(
summary: SessionSummary, summary: SessionSummary,
useColors: boolean forHuman: boolean
): string[] { ): string[] {
const output: string[] = []; const output: string[] = [];
if (useColors) { if (forHuman) {
output.push(...Color.renderColorSummaryField('Investigated', summary.investigated, colors.blue)); output.push(...Human.renderHumanSummaryField('Investigated', summary.investigated, colors.blue));
output.push(...Color.renderColorSummaryField('Learned', summary.learned, colors.yellow)); output.push(...Human.renderHumanSummaryField('Learned', summary.learned, colors.yellow));
output.push(...Color.renderColorSummaryField('Completed', summary.completed, colors.green)); output.push(...Human.renderHumanSummaryField('Completed', summary.completed, colors.green));
output.push(...Color.renderColorSummaryField('Next Steps', summary.next_steps, colors.magenta)); output.push(...Human.renderHumanSummaryField('Next Steps', summary.next_steps, colors.magenta));
} else { } else {
output.push(...Markdown.renderMarkdownSummaryField('Investigated', summary.investigated)); output.push(...Agent.renderAgentSummaryField('Investigated', summary.investigated));
output.push(...Markdown.renderMarkdownSummaryField('Learned', summary.learned)); output.push(...Agent.renderAgentSummaryField('Learned', summary.learned));
output.push(...Markdown.renderMarkdownSummaryField('Completed', summary.completed)); output.push(...Agent.renderAgentSummaryField('Completed', summary.completed));
output.push(...Markdown.renderMarkdownSummaryField('Next Steps', summary.next_steps)); output.push(...Agent.renderAgentSummaryField('Next Steps', summary.next_steps));
} }
return output; return output;
@@ -1,8 +1,8 @@
/** /**
* TimelineRenderer - Renders the chronological timeline of observations and summaries * TimelineRenderer - Renders the chronological timeline of observations and summaries
* *
* Handles day grouping and rendering. In markdown (LLM) mode, uses flat compact lines. * Handles day grouping and rendering. In agent (LLM) mode, uses flat compact lines.
* In color (terminal) mode, uses file grouping with visual formatting. * In human (terminal) mode, uses file grouping with visual formatting.
*/ */
import type { import type {
@@ -12,8 +12,8 @@ import type {
SummaryTimelineItem, SummaryTimelineItem,
} from '../types.js'; } from '../types.js';
import { formatTime, formatDate, formatDateTime, extractFirstFile, parseJsonArray } from '../../../shared/timeline-formatting.js'; import { formatTime, formatDate, formatDateTime, extractFirstFile, parseJsonArray } from '../../../shared/timeline-formatting.js';
import * as Markdown from '../formatters/MarkdownFormatter.js'; import * as Agent from '../formatters/AgentFormatter.js';
import * as Color from '../formatters/ColorFormatter.js'; import * as Human from '../formatters/HumanFormatter.js';
/** /**
* Group timeline items by day * Group timeline items by day
@@ -51,9 +51,9 @@ function getDetailField(obs: Observation, config: ContextConfig): string | null
} }
/** /**
* Render a single day's timeline items (markdown/LLM mode - flat compact lines) * Render a single day's timeline items (agent/LLM mode - flat compact lines)
*/ */
function renderDayTimelineMarkdown( function renderDayTimelineAgent(
day: string, day: string,
dayItems: TimelineItem[], dayItems: TimelineItem[],
fullObservationIds: Set<number>, fullObservationIds: Set<number>,
@@ -61,17 +61,15 @@ function renderDayTimelineMarkdown(
): string[] { ): string[] {
const output: string[] = []; const output: string[] = [];
output.push(...Markdown.renderMarkdownDayHeader(day)); output.push(...Agent.renderAgentDayHeader(day));
let lastTime = ''; let lastTime = '';
for (const item of dayItems) { for (const item of dayItems) {
if (item.type === 'summary') { if (item.type === 'summary') {
lastTime = '';
const summary = item.data as SummaryTimelineItem; const summary = item.data as SummaryTimelineItem;
const formattedTime = formatDateTime(summary.displayTime); const formattedTime = formatDateTime(summary.displayTime);
output.push(...Markdown.renderMarkdownSummaryItem(summary, formattedTime)); output.push(...Agent.renderAgentSummaryItem(summary, formattedTime));
} else { } else {
const obs = item.data as Observation; const obs = item.data as Observation;
const time = formatTime(obs.created_at); const time = formatTime(obs.created_at);
@@ -83,9 +81,9 @@ function renderDayTimelineMarkdown(
if (shouldShowFull) { if (shouldShowFull) {
const detailField = getDetailField(obs, config); const detailField = getDetailField(obs, config);
output.push(...Markdown.renderMarkdownFullObservation(obs, timeDisplay, detailField, config)); output.push(...Agent.renderAgentFullObservation(obs, timeDisplay, detailField, config));
} else { } else {
output.push(Markdown.renderMarkdownTableRow(obs, timeDisplay, config)); output.push(Agent.renderAgentTableRow(obs, timeDisplay, config));
} }
} }
} }
@@ -94,9 +92,9 @@ function renderDayTimelineMarkdown(
} }
/** /**
* Render a single day's timeline items (color/terminal mode - file grouped with tables) * Render a single day's timeline items (human/terminal mode - file grouped with tables)
*/ */
function renderDayTimelineColor( function renderDayTimelineHuman(
day: string, day: string,
dayItems: TimelineItem[], dayItems: TimelineItem[],
fullObservationIds: Set<number>, fullObservationIds: Set<number>,
@@ -105,7 +103,7 @@ function renderDayTimelineColor(
): string[] { ): string[] {
const output: string[] = []; const output: string[] = [];
output.push(...Color.renderColorDayHeader(day)); output.push(...Human.renderHumanDayHeader(day));
let currentFile: string | null = null; let currentFile: string | null = null;
let lastTime = ''; let lastTime = '';
@@ -117,7 +115,7 @@ function renderDayTimelineColor(
const summary = item.data as SummaryTimelineItem; const summary = item.data as SummaryTimelineItem;
const formattedTime = formatDateTime(summary.displayTime); const formattedTime = formatDateTime(summary.displayTime);
output.push(...Color.renderColorSummaryItem(summary, formattedTime)); output.push(...Human.renderHumanSummaryItem(summary, formattedTime));
} else { } else {
const obs = item.data as Observation; const obs = item.data as Observation;
const file = extractFirstFile(obs.files_modified, cwd, obs.files_read); const file = extractFirstFile(obs.files_modified, cwd, obs.files_read);
@@ -129,15 +127,15 @@ function renderDayTimelineColor(
// Check if we need a new file section // Check if we need a new file section
if (file !== currentFile) { if (file !== currentFile) {
output.push(...Color.renderColorFileHeader(file)); output.push(...Human.renderHumanFileHeader(file));
currentFile = file; currentFile = file;
} }
if (shouldShowFull) { if (shouldShowFull) {
const detailField = getDetailField(obs, config); const detailField = getDetailField(obs, config);
output.push(...Color.renderColorFullObservation(obs, time, showTime, detailField, config)); output.push(...Human.renderHumanFullObservation(obs, time, showTime, detailField, config));
} else { } else {
output.push(Color.renderColorTableRow(obs, time, showTime, config)); output.push(Human.renderHumanTableRow(obs, time, showTime, config));
} }
} }
} }
@@ -156,12 +154,12 @@ export function renderDayTimeline(
fullObservationIds: Set<number>, fullObservationIds: Set<number>,
config: ContextConfig, config: ContextConfig,
cwd: string, cwd: string,
useColors: boolean forHuman: boolean
): string[] { ): string[] {
if (useColors) { if (forHuman) {
return renderDayTimelineColor(day, dayItems, fullObservationIds, config, cwd); return renderDayTimelineHuman(day, dayItems, fullObservationIds, config, cwd);
} }
return renderDayTimelineMarkdown(day, dayItems, fullObservationIds, config); return renderDayTimelineAgent(day, dayItems, fullObservationIds, config);
} }
/** /**
@@ -172,13 +170,13 @@ export function renderTimeline(
fullObservationIds: Set<number>, fullObservationIds: Set<number>,
config: ContextConfig, config: ContextConfig,
cwd: string, cwd: string,
useColors: boolean forHuman: boolean
): string[] { ): string[] {
const output: string[] = []; const output: string[] = [];
const itemsByDay = groupTimelineByDay(timeline); const itemsByDay = groupTimelineByDay(timeline);
for (const [day, dayItems] of itemsByDay) { for (const [day, dayItems] of itemsByDay) {
output.push(...renderDayTimeline(day, dayItems, fullObservationIds, config, cwd, useColors)); output.push(...renderDayTimeline(day, dayItems, fullObservationIds, config, cwd, forHuman));
} }
return output; return output;
@@ -185,7 +185,7 @@ export class SearchRoutes extends BaseRouteHandler {
session_id: 'preview-' + Date.now(), session_id: 'preview-' + Date.now(),
cwd: cwd cwd: cwd
}, },
true // useColors=true for ANSI terminal output true // forHuman=true for ANSI terminal output
); );
// Return as plain text // Return as plain text
@@ -207,7 +207,7 @@ export class SearchRoutes extends BaseRouteHandler {
private handleContextInject = this.wrapHandler(async (req: Request, res: Response): Promise<void> => { private handleContextInject = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
// Support both legacy `project` and new `projects` parameter // Support both legacy `project` and new `projects` parameter
const projectsParam = (req.query.projects as string) || (req.query.project as string); const projectsParam = (req.query.projects as string) || (req.query.project as string);
const useColors = req.query.colors === 'true'; const forHuman = req.query.colors === 'true';
const full = req.query.full === 'true'; const full = req.query.full === 'true';
if (!projectsParam) { if (!projectsParam) {
@@ -238,7 +238,7 @@ export class SearchRoutes extends BaseRouteHandler {
projects: projects, projects: projects,
full full
}, },
useColors forHuman
); );
// Return as plain text // Return as plain text
@@ -28,21 +28,21 @@ mock.module('../../../src/services/domain/ModeManager.js', () => ({
})); }));
import { import {
renderMarkdownHeader, renderAgentHeader,
renderMarkdownLegend, renderAgentLegend,
renderMarkdownColumnKey, renderAgentColumnKey,
renderMarkdownContextIndex, renderAgentContextIndex,
renderMarkdownContextEconomics, renderAgentContextEconomics,
renderMarkdownDayHeader, renderAgentDayHeader,
renderMarkdownFileHeader, renderAgentFileHeader,
renderMarkdownTableRow, renderAgentTableRow,
renderMarkdownFullObservation, renderAgentFullObservation,
renderMarkdownSummaryItem, renderAgentSummaryItem,
renderMarkdownSummaryField, renderAgentSummaryField,
renderMarkdownPreviouslySection, renderAgentPreviouslySection,
renderMarkdownFooter, renderAgentFooter,
renderMarkdownEmptyState, renderAgentEmptyState,
} from '../../../src/services/context/formatters/MarkdownFormatter.js'; } from '../../../src/services/context/formatters/AgentFormatter.js';
import type { Observation, TokenEconomics, ContextConfig, PriorMessages } from '../../../src/services/context/types.js'; import type { Observation, TokenEconomics, ContextConfig, PriorMessages } from '../../../src/services/context/types.js';
@@ -97,209 +97,164 @@ function createTestConfig(overrides: Partial<ContextConfig> = {}): ContextConfig
}; };
} }
describe('MarkdownFormatter', () => { describe('AgentFormatter', () => {
describe('renderMarkdownHeader', () => { describe('renderAgentHeader', () => {
it('should produce valid markdown header with project name', () => { it('should produce valid markdown header with project name', () => {
const result = renderMarkdownHeader('my-project'); const result = renderAgentHeader('my-project');
expect(result).toHaveLength(2); expect(result).toHaveLength(2);
expect(result[0]).toMatch(/^# \[my-project\] recent context, \d{4}-\d{2}-\d{2} \d{1,2}:\d{2}[ap]m [A-Z]{3,4}$/); expect(result[0]).toMatch(/^# \$CMEM my-project \d{4}-\d{2}-\d{2} \d{1,2}:\d{2}[ap]m [A-Z]{3,4}$/);
expect(result[1]).toBe(''); expect(result[1]).toBe('');
}); });
it('should handle special characters in project name', () => { it('should handle special characters in project name', () => {
const result = renderMarkdownHeader('project-with-special_chars.v2'); const result = renderAgentHeader('project-with-special_chars.v2');
expect(result[0]).toContain('project-with-special_chars.v2'); expect(result[0]).toContain('project-with-special_chars.v2');
}); });
it('should handle empty project name', () => { it('should handle empty project name', () => {
const result = renderMarkdownHeader(''); const result = renderAgentHeader('');
expect(result[0]).toMatch(/^# \[\] recent context, \d{4}-\d{2}-\d{2} \d{1,2}:\d{2}[ap]m [A-Z]{3,4}$/); expect(result[0]).toMatch(/^# \$CMEM \d{4}-\d{2}-\d{2} \d{1,2}:\d{2}[ap]m [A-Z]{3,4}$/);
}); });
}); });
describe('renderMarkdownLegend', () => { describe('renderAgentLegend', () => {
it('should produce legend with type items', () => { it('should produce legend with type items', () => {
const result = renderMarkdownLegend(); const result = renderAgentLegend();
expect(result).toHaveLength(2); expect(result).toHaveLength(4);
expect(result[0]).toContain('**Legend:**'); expect(result[0]).toContain('Legend:');
expect(result[1]).toBe(''); expect(result[3]).toBe('');
}); });
it('should include session-request in legend', () => { it('should include session in legend', () => {
const result = renderMarkdownLegend(); const result = renderAgentLegend();
expect(result[0]).toContain('session-request'); expect(result[0]).toContain('session');
}); });
}); });
describe('renderMarkdownColumnKey', () => { describe('renderAgentColumnKey', () => {
it('should produce column key explanation', () => { it('should return empty array in compact format', () => {
const result = renderMarkdownColumnKey(); const result = renderAgentColumnKey();
expect(result.length).toBeGreaterThan(0); expect(result).toHaveLength(0);
expect(result[0]).toContain('**Column Key**');
});
it('should explain Read column', () => {
const result = renderMarkdownColumnKey();
const joined = result.join('\n');
expect(joined).toContain('Read');
expect(joined).toContain('Tokens to read');
});
it('should explain Work column', () => {
const result = renderMarkdownColumnKey();
const joined = result.join('\n');
expect(joined).toContain('Work');
expect(joined).toContain('Tokens spent');
}); });
}); });
describe('renderMarkdownContextIndex', () => { describe('renderAgentContextIndex', () => {
it('should produce context index instructions', () => { it('should return empty array in compact format', () => {
const result = renderMarkdownContextIndex(); const result = renderAgentContextIndex();
expect(result.length).toBeGreaterThan(0); expect(result).toHaveLength(0);
expect(result[0]).toContain('**Context Index:**');
});
it('should mention mem-search skill', () => {
const result = renderMarkdownContextIndex();
const joined = result.join('\n');
expect(joined).toContain('mem-search');
}); });
}); });
describe('renderMarkdownContextEconomics', () => { describe('renderAgentContextEconomics', () => {
it('should include observation count', () => { it('should include observation count', () => {
const economics = createTestEconomics({ totalObservations: 25 }); const economics = createTestEconomics({ totalObservations: 25 });
const config = createTestConfig(); const config = createTestConfig();
const result = renderMarkdownContextEconomics(economics, config); const result = renderAgentContextEconomics(economics, config);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('25 observations'); expect(joined).toContain('25 obs');
}); });
it('should include read tokens', () => { it('should include read tokens', () => {
const economics = createTestEconomics({ totalReadTokens: 1500 }); const economics = createTestEconomics({ totalReadTokens: 1500 });
const config = createTestConfig(); const config = createTestConfig();
const result = renderMarkdownContextEconomics(economics, config); const result = renderAgentContextEconomics(economics, config);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('1,500 tokens'); expect(joined).toContain('1,500t read');
}); });
it('should include work investment', () => { it('should include work investment', () => {
const economics = createTestEconomics({ totalDiscoveryTokens: 10000 }); const economics = createTestEconomics({ totalDiscoveryTokens: 10000 });
const config = createTestConfig(); const config = createTestConfig();
const result = renderMarkdownContextEconomics(economics, config); const result = renderAgentContextEconomics(economics, config);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('10,000 tokens'); expect(joined).toContain('10,000t work');
}); });
it('should show savings when config has showSavingsAmount', () => { it('should show savings when config has showSavingsAmount', () => {
const economics = createTestEconomics({ savings: 4500, savingsPercent: 90, totalDiscoveryTokens: 5000 }); const economics = createTestEconomics({ savings: 4500, savingsPercent: 90, totalDiscoveryTokens: 5000 });
const config = createTestConfig({ showSavingsAmount: true, showSavingsPercent: false }); const config = createTestConfig({ showSavingsAmount: true, showSavingsPercent: false });
const result = renderMarkdownContextEconomics(economics, config); const result = renderAgentContextEconomics(economics, config);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('savings'); expect(joined).toContain('4,500t saved');
expect(joined).toContain('4,500 tokens');
}); });
it('should show savings percent when config has showSavingsPercent', () => { it('should show savings percent when config has showSavingsPercent', () => {
const economics = createTestEconomics({ savingsPercent: 85, totalDiscoveryTokens: 1000 }); const economics = createTestEconomics({ savingsPercent: 85, totalDiscoveryTokens: 1000 });
const config = createTestConfig({ showSavingsAmount: false, showSavingsPercent: true }); const config = createTestConfig({ showSavingsAmount: false, showSavingsPercent: true });
const result = renderMarkdownContextEconomics(economics, config); const result = renderAgentContextEconomics(economics, config);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('85%'); expect(joined).toContain('85% savings');
}); });
it('should not show savings when discovery tokens is 0', () => { it('should not show savings when discovery tokens is 0', () => {
const economics = createTestEconomics({ totalDiscoveryTokens: 0, savings: 0, savingsPercent: 0 }); const economics = createTestEconomics({ totalDiscoveryTokens: 0, savings: 0, savingsPercent: 0 });
const config = createTestConfig({ showSavingsAmount: true, showSavingsPercent: true }); const config = createTestConfig({ showSavingsAmount: true, showSavingsPercent: true });
const result = renderMarkdownContextEconomics(economics, config); const result = renderAgentContextEconomics(economics, config);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).not.toContain('Your savings'); expect(joined).not.toContain('savings');
}); });
}); });
describe('renderMarkdownDayHeader', () => { describe('renderAgentDayHeader', () => {
it('should render day as h3 heading', () => { it('should render day as h3 heading', () => {
const result = renderMarkdownDayHeader('2025-01-01'); const result = renderAgentDayHeader('2025-01-01');
expect(result).toHaveLength(2); expect(result).toHaveLength(1);
expect(result[0]).toBe('### 2025-01-01'); expect(result[0]).toBe('### 2025-01-01');
expect(result[1]).toBe('');
}); });
}); });
describe('renderMarkdownFileHeader', () => { describe('renderAgentFileHeader', () => {
it('should render file name in bold', () => { it('should return empty array in compact format', () => {
const result = renderMarkdownFileHeader('src/index.ts'); const result = renderAgentFileHeader('src/index.ts');
expect(result[0]).toBe('**src/index.ts**'); expect(result).toHaveLength(0);
});
it('should include table headers', () => {
const result = renderMarkdownFileHeader('test.ts');
const joined = result.join('\n');
expect(joined).toContain('| ID |');
expect(joined).toContain('| Time |');
expect(joined).toContain('| T |');
expect(joined).toContain('| Title |');
expect(joined).toContain('| Read |');
expect(joined).toContain('| Work |');
});
it('should include separator row', () => {
const result = renderMarkdownFileHeader('test.ts');
expect(result[2]).toContain('|----');
}); });
}); });
describe('renderMarkdownTableRow', () => { describe('renderAgentTableRow', () => {
it('should include observation ID with hash prefix', () => { it('should include observation ID', () => {
const obs = createTestObservation({ id: 42 }); const obs = createTestObservation({ id: 42 });
const config = createTestConfig(); const config = createTestConfig();
const result = renderMarkdownTableRow(obs, '10:30', config); const result = renderAgentTableRow(obs, '10:30 AM', config);
expect(result).toContain('#42'); expect(result).toContain('42');
}); });
it('should include time display', () => { it('should include compact time display', () => {
const obs = createTestObservation(); const obs = createTestObservation();
const config = createTestConfig(); const config = createTestConfig();
const result = renderMarkdownTableRow(obs, '14:30', config); const result = renderAgentTableRow(obs, '2:30 PM', config);
expect(result).toContain('14:30'); expect(result).toContain('2:30p');
}); });
it('should include title', () => { it('should include title', () => {
const obs = createTestObservation({ title: 'Important Discovery' }); const obs = createTestObservation({ title: 'Important Discovery' });
const config = createTestConfig(); const config = createTestConfig();
const result = renderMarkdownTableRow(obs, '10:00', config); const result = renderAgentTableRow(obs, '10:00 AM', config);
expect(result).toContain('Important Discovery'); expect(result).toContain('Important Discovery');
}); });
@@ -308,30 +263,18 @@ describe('MarkdownFormatter', () => {
const obs = createTestObservation({ title: null }); const obs = createTestObservation({ title: null });
const config = createTestConfig(); const config = createTestConfig();
const result = renderMarkdownTableRow(obs, '10:00', config); const result = renderAgentTableRow(obs, '10:00 AM', config);
expect(result).toContain('Untitled'); expect(result).toContain('Untitled');
}); });
it('should show read tokens when config enabled', () => { it('should produce flat format: ID TIME TYPE TITLE', () => {
const obs = createTestObservation(); const obs = createTestObservation({ id: 5 });
const config = createTestConfig({ showReadTokens: true }); const config = createTestConfig();
const result = renderMarkdownTableRow(obs, '10:00', config); const result = renderAgentTableRow(obs, '10:00 AM', config);
expect(result).toContain('~'); expect(result).toBe('5 10:00a I Test Observation');
});
it('should hide read tokens when config disabled', () => {
const obs = createTestObservation();
const config = createTestConfig({ showReadTokens: false });
const result = renderMarkdownTableRow(obs, '10:00', config);
// Row should have empty read column
const columns = result.split('|');
// Find the Read column (5th column, index 5)
expect(columns[5].trim()).toBe('');
}); });
it('should use quote mark for repeated time', () => { it('should use quote mark for repeated time', () => {
@@ -339,21 +282,21 @@ describe('MarkdownFormatter', () => {
const config = createTestConfig(); const config = createTestConfig();
// Empty string timeDisplay means "same as previous" // Empty string timeDisplay means "same as previous"
const result = renderMarkdownTableRow(obs, '', config); const result = renderAgentTableRow(obs, '', config);
expect(result).toContain('"'); expect(result).toContain('"');
}); });
}); });
describe('renderMarkdownFullObservation', () => { describe('renderAgentFullObservation', () => {
it('should include observation ID and title', () => { it('should include observation ID and title', () => {
const obs = createTestObservation({ id: 7, title: 'Full Observation' }); const obs = createTestObservation({ id: 7, title: 'Full Observation' });
const config = createTestConfig(); const config = createTestConfig();
const result = renderMarkdownFullObservation(obs, '10:00', 'Detail content', config); const result = renderAgentFullObservation(obs, '10:00 AM', 'Detail content', config);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('**#7**'); expect(joined).toContain('**7**');
expect(joined).toContain('**Full Observation**'); expect(joined).toContain('**Full Observation**');
}); });
@@ -361,7 +304,7 @@ describe('MarkdownFormatter', () => {
const obs = createTestObservation(); const obs = createTestObservation();
const config = createTestConfig(); const config = createTestConfig();
const result = renderMarkdownFullObservation(obs, '10:00', 'The detailed narrative here', config); const result = renderAgentFullObservation(obs, '10:00 AM', 'The detailed narrative here', config);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('The detailed narrative here'); expect(joined).toContain('The detailed narrative here');
@@ -371,7 +314,7 @@ describe('MarkdownFormatter', () => {
const obs = createTestObservation(); const obs = createTestObservation();
const config = createTestConfig(); const config = createTestConfig();
const result = renderMarkdownFullObservation(obs, '10:00', null, config); const result = renderAgentFullObservation(obs, '10:00 AM', null, config);
// Should not have an extra content block // Should not have an extra content block
expect(result.length).toBeLessThan(5); expect(result.length).toBeLessThan(5);
@@ -381,28 +324,30 @@ describe('MarkdownFormatter', () => {
const obs = createTestObservation({ discovery_tokens: 250 }); const obs = createTestObservation({ discovery_tokens: 250 });
const config = createTestConfig({ showReadTokens: true, showWorkTokens: true }); const config = createTestConfig({ showReadTokens: true, showWorkTokens: true });
const result = renderMarkdownFullObservation(obs, '10:00', null, config); const result = renderAgentFullObservation(obs, '10:00 AM', null, config);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('Read:'); // Compact format: "~{readTokens}t" and "W {discoveryTokens}"
expect(joined).toContain('Work:'); expect(joined).toContain('~');
expect(joined).toContain('t');
expect(joined).toContain('W 250');
}); });
}); });
describe('renderMarkdownSummaryItem', () => { describe('renderAgentSummaryItem', () => {
it('should include session ID with S prefix', () => { it('should include session ID with S prefix', () => {
const summary = { id: 5, request: 'Implement feature' }; const summary = { id: 5, request: 'Implement feature' };
const result = renderMarkdownSummaryItem(summary, '2025-01-01 10:00'); const result = renderAgentSummaryItem(summary, '2025-01-01 10:00');
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('**#S5**'); expect(joined).toContain('S5');
}); });
it('should include request text', () => { it('should include request text', () => {
const summary = { id: 1, request: 'Build authentication' }; const summary = { id: 1, request: 'Build authentication' };
const result = renderMarkdownSummaryItem(summary, '10:00'); const result = renderAgentSummaryItem(summary, '10:00');
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('Build authentication'); expect(joined).toContain('Build authentication');
@@ -411,16 +356,16 @@ describe('MarkdownFormatter', () => {
it('should use "Session started" when request is null', () => { it('should use "Session started" when request is null', () => {
const summary = { id: 1, request: null }; const summary = { id: 1, request: null };
const result = renderMarkdownSummaryItem(summary, '10:00'); const result = renderAgentSummaryItem(summary, '10:00');
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('Session started'); expect(joined).toContain('Session started');
}); });
}); });
describe('renderMarkdownSummaryField', () => { describe('renderAgentSummaryField', () => {
it('should render label and value in bold', () => { it('should render label and value in bold', () => {
const result = renderMarkdownSummaryField('Learned', 'How to test'); const result = renderAgentSummaryField('Learned', 'How to test');
expect(result).toHaveLength(2); expect(result).toHaveLength(2);
expect(result[0]).toBe('**Learned**: How to test'); expect(result[0]).toBe('**Learned**: How to test');
@@ -428,27 +373,27 @@ describe('MarkdownFormatter', () => {
}); });
it('should return empty array when value is null', () => { it('should return empty array when value is null', () => {
const result = renderMarkdownSummaryField('Learned', null); const result = renderAgentSummaryField('Learned', null);
expect(result).toHaveLength(0); expect(result).toHaveLength(0);
}); });
it('should return empty array when value is empty string', () => { it('should return empty array when value is empty string', () => {
const result = renderMarkdownSummaryField('Learned', ''); const result = renderAgentSummaryField('Learned', '');
// Empty string is falsy, so should return empty array // Empty string is falsy, so should return empty array
expect(result).toHaveLength(0); expect(result).toHaveLength(0);
}); });
}); });
describe('renderMarkdownPreviouslySection', () => { describe('renderAgentPreviouslySection', () => {
it('should render section when assistantMessage exists', () => { it('should render section when assistantMessage exists', () => {
const priorMessages: PriorMessages = { const priorMessages: PriorMessages = {
userMessage: '', userMessage: '',
assistantMessage: 'I completed the task successfully.', assistantMessage: 'I completed the task successfully.',
}; };
const result = renderMarkdownPreviouslySection(priorMessages); const result = renderAgentPreviouslySection(priorMessages);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('**Previously**'); expect(joined).toContain('**Previously**');
@@ -461,7 +406,7 @@ describe('MarkdownFormatter', () => {
assistantMessage: '', assistantMessage: '',
}; };
const result = renderMarkdownPreviouslySection(priorMessages); const result = renderAgentPreviouslySection(priorMessages);
expect(result).toHaveLength(0); expect(result).toHaveLength(0);
}); });
@@ -472,31 +417,30 @@ describe('MarkdownFormatter', () => {
assistantMessage: 'Some message', assistantMessage: 'Some message',
}; };
const result = renderMarkdownPreviouslySection(priorMessages); const result = renderAgentPreviouslySection(priorMessages);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('---'); expect(joined).toContain('---');
}); });
}); });
describe('renderMarkdownFooter', () => { describe('renderAgentFooter', () => {
it('should include token amounts', () => { it('should include work token amount in k', () => {
const result = renderMarkdownFooter(10000, 500); const result = renderAgentFooter(10000, 500);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('10k'); expect(joined).toContain('10k');
expect(joined).toContain('500');
}); });
it('should mention claude-mem skill', () => { it('should mention mem-search skill', () => {
const result = renderMarkdownFooter(5000, 100); const result = renderAgentFooter(5000, 100);
const joined = result.join('\n'); const joined = result.join('\n');
expect(joined).toContain('claude-mem'); expect(joined).toContain('mem-search skill');
}); });
it('should round work tokens to nearest thousand', () => { it('should round work tokens to nearest thousand', () => {
const result = renderMarkdownFooter(15500, 100); const result = renderAgentFooter(15500, 100);
const joined = result.join('\n'); const joined = result.join('\n');
// 15500 / 1000 = 15.5 -> rounds to 16 // 15500 / 1000 = 15.5 -> rounds to 16
@@ -504,25 +448,25 @@ describe('MarkdownFormatter', () => {
}); });
}); });
describe('renderMarkdownEmptyState', () => { describe('renderAgentEmptyState', () => {
it('should return helpful message with project name', () => { it('should return helpful message with project name', () => {
const result = renderMarkdownEmptyState('my-project'); const result = renderAgentEmptyState('my-project');
expect(result).toContain('# [my-project] recent context'); expect(result).toContain('# $CMEM my-project');
expect(result).toContain('No previous sessions found'); expect(result).toContain('No previous sessions found.');
}); });
it('should be valid markdown', () => { it('should be valid markdown', () => {
const result = renderMarkdownEmptyState('test'); const result = renderAgentEmptyState('test');
// Should start with h1 // Should start with h1
expect(result.startsWith('#')).toBe(true); expect(result.startsWith('#')).toBe(true);
}); });
it('should handle empty project name', () => { it('should handle empty project name', () => {
const result = renderMarkdownEmptyState(''); const result = renderAgentEmptyState('');
expect(result).toContain('# [] recent context'); expect(result).toContain('# $CMEM ');
}); });
}); });
}); });