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 { shouldShowSummary, renderSummaryFields } from './sections/SummaryRenderer.js';
import { renderPreviouslySection, renderFooter } from './sections/FooterRenderer.js';
import { renderMarkdownEmptyState } from './formatters/MarkdownFormatter.js';
import { renderColorEmptyState } from './formatters/ColorFormatter.js';
import { renderAgentEmptyState } from './formatters/AgentFormatter.js';
import { renderHumanEmptyState } from './formatters/HumanFormatter.js';
// Version marker path for native module error handling
const VERSION_MARKER_PATH = path.join(
@@ -66,8 +66,8 @@ function initializeDatabase(): SessionStore | null {
/**
* Render empty state when no data exists
*/
function renderEmptyState(project: string, useColors: boolean): string {
return useColors ? renderColorEmptyState(project) : renderMarkdownEmptyState(project);
function renderEmptyState(project: string, forHuman: boolean): string {
return forHuman ? renderHumanEmptyState(project) : renderAgentEmptyState(project);
}
/**
@@ -80,7 +80,7 @@ function buildContextOutput(
config: ContextConfig,
cwd: string,
sessionId: string | undefined,
useColors: boolean
forHuman: boolean
): string {
const output: string[] = [];
@@ -88,7 +88,7 @@ function buildContextOutput(
const economics = calculateTokenEconomics(observations);
// Render header section
output.push(...renderHeader(project, economics, config, useColors));
output.push(...renderHeader(project, economics, config, forHuman));
// Prepare timeline data
const displaySummaries = summaries.slice(0, config.sessionCount);
@@ -97,22 +97,22 @@ function buildContextOutput(
const fullObservationIds = getFullObservationIds(observations, config.fullObservationCount);
// Render timeline
output.push(...renderTimeline(timeline, fullObservationIds, config, cwd, useColors));
output.push(...renderTimeline(timeline, fullObservationIds, config, cwd, forHuman));
// Render most recent summary if applicable
const mostRecentSummary = summaries[0];
const mostRecentObservation = observations[0];
if (shouldShowSummary(config, mostRecentSummary, mostRecentObservation)) {
output.push(...renderSummaryFields(mostRecentSummary, useColors));
output.push(...renderSummaryFields(mostRecentSummary, forHuman));
}
// Render previously section (prior assistant message)
const priorMessages = getPriorSessionMessages(observations, config, sessionId, cwd);
output.push(...renderPreviouslySection(priorMessages, useColors));
output.push(...renderPreviouslySection(priorMessages, forHuman));
// Render footer
output.push(...renderFooter(economics, config, useColors));
output.push(...renderFooter(economics, config, forHuman));
return output.join('\n').trimEnd();
}
@@ -125,7 +125,7 @@ function buildContextOutput(
*/
export async function generateContext(
input?: ContextInput,
useColors: boolean = false
forHuman: boolean = false
): Promise<string> {
const config = loadContextConfig();
const cwd = input?.cwd ?? process.cwd();
@@ -157,7 +157,7 @@ export async function generateContext(
// Handle empty state
if (observations.length === 0 && summaries.length === 0) {
return renderEmptyState(project, useColors);
return renderEmptyState(project, forHuman);
}
// Build and return context
@@ -168,7 +168,7 @@ export async function generateContext(
config,
cwd,
input?.session_id,
useColors
forHuman
);
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.
* 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 {
@@ -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 [
`# $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 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 [];
}
/**
* 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 [];
}
/**
* Render markdown context economics
* Render agent context economics
*/
export function renderMarkdownContextEconomics(
export function renderAgentContextEconomics(
economics: TokenEconomics,
config: ContextConfig
): 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 [
`### ${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
return [];
}
@@ -124,7 +124,7 @@ function compactTime(time: string): string {
/**
* Render compact flat line for observation (replaces table row)
*/
export function renderMarkdownTableRow(
export function renderAgentTableRow(
obs: Observation,
timeDisplay: string,
_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,
timeDisplay: string,
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 },
formattedTime: 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 [];
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 [];
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);
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.`;
}
@@ -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).
*/
@@ -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 [
'',
`${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 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 [
`${colors.bright}Column Key${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 [
`${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,
config: ContextConfig
): 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 [
`${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 [
`${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,
time: string,
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,
time: string,
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 },
formattedTime: 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 [];
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 [];
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);
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`;
}
+10 -10
View File
@@ -6,20 +6,20 @@
import type { ContextConfig, TokenEconomics, PriorMessages } from '../types.js';
import { shouldShowContextEconomics } from '../TokenCalculator.js';
import * as Markdown from '../formatters/MarkdownFormatter.js';
import * as Color from '../formatters/ColorFormatter.js';
import * as Agent from '../formatters/AgentFormatter.js';
import * as Human from '../formatters/HumanFormatter.js';
/**
* Render the previously section (prior assistant message)
*/
export function renderPreviouslySection(
priorMessages: PriorMessages,
useColors: boolean
forHuman: boolean
): string[] {
if (useColors) {
return Color.renderColorPreviouslySection(priorMessages);
if (forHuman) {
return Human.renderHumanPreviouslySection(priorMessages);
}
return Markdown.renderMarkdownPreviouslySection(priorMessages);
return Agent.renderAgentPreviouslySection(priorMessages);
}
/**
@@ -28,15 +28,15 @@ export function renderPreviouslySection(
export function renderFooter(
economics: TokenEconomics,
config: ContextConfig,
useColors: boolean
forHuman: boolean
): string[] {
// Only show footer if we have savings to display
if (!shouldShowContextEconomics(config) || economics.totalDiscoveryTokens <= 0 || economics.savings <= 0) {
return [];
}
if (useColors) {
return Color.renderColorFooter(economics.totalDiscoveryTokens, economics.totalReadTokens);
if (forHuman) {
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 { shouldShowContextEconomics } from '../TokenCalculator.js';
import * as Markdown from '../formatters/MarkdownFormatter.js';
import * as Color from '../formatters/ColorFormatter.js';
import * as Agent from '../formatters/AgentFormatter.js';
import * as Human from '../formatters/HumanFormatter.js';
/**
* Render the complete header section
@@ -16,44 +16,44 @@ export function renderHeader(
project: string,
economics: TokenEconomics,
config: ContextConfig,
useColors: boolean
forHuman: boolean
): string[] {
const output: string[] = [];
// Main header
if (useColors) {
output.push(...Color.renderColorHeader(project));
if (forHuman) {
output.push(...Human.renderHumanHeader(project));
} else {
output.push(...Markdown.renderMarkdownHeader(project));
output.push(...Agent.renderAgentHeader(project));
}
// Legend
if (useColors) {
output.push(...Color.renderColorLegend());
if (forHuman) {
output.push(...Human.renderHumanLegend());
} else {
output.push(...Markdown.renderMarkdownLegend());
output.push(...Agent.renderAgentLegend());
}
// Column key
if (useColors) {
output.push(...Color.renderColorColumnKey());
if (forHuman) {
output.push(...Human.renderHumanColumnKey());
} else {
output.push(...Markdown.renderMarkdownColumnKey());
output.push(...Agent.renderAgentColumnKey());
}
// Context index instructions
if (useColors) {
output.push(...Color.renderColorContextIndex());
if (forHuman) {
output.push(...Human.renderHumanContextIndex());
} else {
output.push(...Markdown.renderMarkdownContextIndex());
output.push(...Agent.renderAgentContextIndex());
}
// Context economics
if (shouldShowContextEconomics(config)) {
if (useColors) {
output.push(...Color.renderColorContextEconomics(economics, config));
if (forHuman) {
output.push(...Human.renderHumanContextEconomics(economics, config));
} 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 { colors } from '../types.js';
import * as Markdown from '../formatters/MarkdownFormatter.js';
import * as Color from '../formatters/ColorFormatter.js';
import * as Agent from '../formatters/AgentFormatter.js';
import * as Human from '../formatters/HumanFormatter.js';
/**
* Check if summary should be displayed
@@ -45,20 +45,20 @@ export function shouldShowSummary(
*/
export function renderSummaryFields(
summary: SessionSummary,
useColors: boolean
forHuman: boolean
): string[] {
const output: string[] = [];
if (useColors) {
output.push(...Color.renderColorSummaryField('Investigated', summary.investigated, colors.blue));
output.push(...Color.renderColorSummaryField('Learned', summary.learned, colors.yellow));
output.push(...Color.renderColorSummaryField('Completed', summary.completed, colors.green));
output.push(...Color.renderColorSummaryField('Next Steps', summary.next_steps, colors.magenta));
if (forHuman) {
output.push(...Human.renderHumanSummaryField('Investigated', summary.investigated, colors.blue));
output.push(...Human.renderHumanSummaryField('Learned', summary.learned, colors.yellow));
output.push(...Human.renderHumanSummaryField('Completed', summary.completed, colors.green));
output.push(...Human.renderHumanSummaryField('Next Steps', summary.next_steps, colors.magenta));
} else {
output.push(...Markdown.renderMarkdownSummaryField('Investigated', summary.investigated));
output.push(...Markdown.renderMarkdownSummaryField('Learned', summary.learned));
output.push(...Markdown.renderMarkdownSummaryField('Completed', summary.completed));
output.push(...Markdown.renderMarkdownSummaryField('Next Steps', summary.next_steps));
output.push(...Agent.renderAgentSummaryField('Investigated', summary.investigated));
output.push(...Agent.renderAgentSummaryField('Learned', summary.learned));
output.push(...Agent.renderAgentSummaryField('Completed', summary.completed));
output.push(...Agent.renderAgentSummaryField('Next Steps', summary.next_steps));
}
return output;
@@ -1,8 +1,8 @@
/**
* TimelineRenderer - Renders the chronological timeline of observations and summaries
*
* Handles day grouping and rendering. In markdown (LLM) mode, uses flat compact lines.
* In color (terminal) mode, uses file grouping with visual formatting.
* Handles day grouping and rendering. In agent (LLM) mode, uses flat compact lines.
* In human (terminal) mode, uses file grouping with visual formatting.
*/
import type {
@@ -12,8 +12,8 @@ import type {
SummaryTimelineItem,
} from '../types.js';
import { formatTime, formatDate, formatDateTime, extractFirstFile, parseJsonArray } from '../../../shared/timeline-formatting.js';
import * as Markdown from '../formatters/MarkdownFormatter.js';
import * as Color from '../formatters/ColorFormatter.js';
import * as Agent from '../formatters/AgentFormatter.js';
import * as Human from '../formatters/HumanFormatter.js';
/**
* 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,
dayItems: TimelineItem[],
fullObservationIds: Set<number>,
@@ -61,17 +61,15 @@ function renderDayTimelineMarkdown(
): string[] {
const output: string[] = [];
output.push(...Markdown.renderMarkdownDayHeader(day));
output.push(...Agent.renderAgentDayHeader(day));
let lastTime = '';
for (const item of dayItems) {
if (item.type === 'summary') {
lastTime = '';
const summary = item.data as SummaryTimelineItem;
const formattedTime = formatDateTime(summary.displayTime);
output.push(...Markdown.renderMarkdownSummaryItem(summary, formattedTime));
output.push(...Agent.renderAgentSummaryItem(summary, formattedTime));
} else {
const obs = item.data as Observation;
const time = formatTime(obs.created_at);
@@ -83,9 +81,9 @@ function renderDayTimelineMarkdown(
if (shouldShowFull) {
const detailField = getDetailField(obs, config);
output.push(...Markdown.renderMarkdownFullObservation(obs, timeDisplay, detailField, config));
output.push(...Agent.renderAgentFullObservation(obs, timeDisplay, detailField, config));
} 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,
dayItems: TimelineItem[],
fullObservationIds: Set<number>,
@@ -105,7 +103,7 @@ function renderDayTimelineColor(
): string[] {
const output: string[] = [];
output.push(...Color.renderColorDayHeader(day));
output.push(...Human.renderHumanDayHeader(day));
let currentFile: string | null = null;
let lastTime = '';
@@ -117,7 +115,7 @@ function renderDayTimelineColor(
const summary = item.data as SummaryTimelineItem;
const formattedTime = formatDateTime(summary.displayTime);
output.push(...Color.renderColorSummaryItem(summary, formattedTime));
output.push(...Human.renderHumanSummaryItem(summary, formattedTime));
} else {
const obs = item.data as Observation;
const file = extractFirstFile(obs.files_modified, cwd, obs.files_read);
@@ -129,15 +127,15 @@ function renderDayTimelineColor(
// Check if we need a new file section
if (file !== currentFile) {
output.push(...Color.renderColorFileHeader(file));
output.push(...Human.renderHumanFileHeader(file));
currentFile = file;
}
if (shouldShowFull) {
const detailField = getDetailField(obs, config);
output.push(...Color.renderColorFullObservation(obs, time, showTime, detailField, config));
output.push(...Human.renderHumanFullObservation(obs, time, showTime, detailField, config));
} 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>,
config: ContextConfig,
cwd: string,
useColors: boolean
forHuman: boolean
): string[] {
if (useColors) {
return renderDayTimelineColor(day, dayItems, fullObservationIds, config, cwd);
if (forHuman) {
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>,
config: ContextConfig,
cwd: string,
useColors: boolean
forHuman: boolean
): string[] {
const output: string[] = [];
const itemsByDay = groupTimelineByDay(timeline);
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;
@@ -185,7 +185,7 @@ export class SearchRoutes extends BaseRouteHandler {
session_id: 'preview-' + Date.now(),
cwd: cwd
},
true // useColors=true for ANSI terminal output
true // forHuman=true for ANSI terminal output
);
// Return as plain text
@@ -207,7 +207,7 @@ export class SearchRoutes extends BaseRouteHandler {
private handleContextInject = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
// Support both legacy `project` and new `projects` parameter
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';
if (!projectsParam) {
@@ -238,7 +238,7 @@ export class SearchRoutes extends BaseRouteHandler {
projects: projects,
full
},
useColors
forHuman
);
// Return as plain text
@@ -28,21 +28,21 @@ mock.module('../../../src/services/domain/ModeManager.js', () => ({
}));
import {
renderMarkdownHeader,
renderMarkdownLegend,
renderMarkdownColumnKey,
renderMarkdownContextIndex,
renderMarkdownContextEconomics,
renderMarkdownDayHeader,
renderMarkdownFileHeader,
renderMarkdownTableRow,
renderMarkdownFullObservation,
renderMarkdownSummaryItem,
renderMarkdownSummaryField,
renderMarkdownPreviouslySection,
renderMarkdownFooter,
renderMarkdownEmptyState,
} from '../../../src/services/context/formatters/MarkdownFormatter.js';
renderAgentHeader,
renderAgentLegend,
renderAgentColumnKey,
renderAgentContextIndex,
renderAgentContextEconomics,
renderAgentDayHeader,
renderAgentFileHeader,
renderAgentTableRow,
renderAgentFullObservation,
renderAgentSummaryItem,
renderAgentSummaryField,
renderAgentPreviouslySection,
renderAgentFooter,
renderAgentEmptyState,
} from '../../../src/services/context/formatters/AgentFormatter.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('renderMarkdownHeader', () => {
describe('AgentFormatter', () => {
describe('renderAgentHeader', () => {
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[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('');
});
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');
});
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', () => {
const result = renderMarkdownLegend();
const result = renderAgentLegend();
expect(result).toHaveLength(2);
expect(result[0]).toContain('**Legend:**');
expect(result[1]).toBe('');
expect(result).toHaveLength(4);
expect(result[0]).toContain('Legend:');
expect(result[3]).toBe('');
});
it('should include session-request in legend', () => {
const result = renderMarkdownLegend();
it('should include session in legend', () => {
const result = renderAgentLegend();
expect(result[0]).toContain('session-request');
expect(result[0]).toContain('session');
});
});
describe('renderMarkdownColumnKey', () => {
it('should produce column key explanation', () => {
const result = renderMarkdownColumnKey();
describe('renderAgentColumnKey', () => {
it('should return empty array in compact format', () => {
const result = renderAgentColumnKey();
expect(result.length).toBeGreaterThan(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');
expect(result).toHaveLength(0);
});
});
describe('renderMarkdownContextIndex', () => {
it('should produce context index instructions', () => {
const result = renderMarkdownContextIndex();
describe('renderAgentContextIndex', () => {
it('should return empty array in compact format', () => {
const result = renderAgentContextIndex();
expect(result.length).toBeGreaterThan(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');
expect(result).toHaveLength(0);
});
});
describe('renderMarkdownContextEconomics', () => {
describe('renderAgentContextEconomics', () => {
it('should include observation count', () => {
const economics = createTestEconomics({ totalObservations: 25 });
const config = createTestConfig();
const result = renderMarkdownContextEconomics(economics, config);
const result = renderAgentContextEconomics(economics, config);
const joined = result.join('\n');
expect(joined).toContain('25 observations');
expect(joined).toContain('25 obs');
});
it('should include read tokens', () => {
const economics = createTestEconomics({ totalReadTokens: 1500 });
const config = createTestConfig();
const result = renderMarkdownContextEconomics(economics, config);
const result = renderAgentContextEconomics(economics, config);
const joined = result.join('\n');
expect(joined).toContain('1,500 tokens');
expect(joined).toContain('1,500t read');
});
it('should include work investment', () => {
const economics = createTestEconomics({ totalDiscoveryTokens: 10000 });
const config = createTestConfig();
const result = renderMarkdownContextEconomics(economics, config);
const result = renderAgentContextEconomics(economics, config);
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', () => {
const economics = createTestEconomics({ savings: 4500, savingsPercent: 90, totalDiscoveryTokens: 5000 });
const config = createTestConfig({ showSavingsAmount: true, showSavingsPercent: false });
const result = renderMarkdownContextEconomics(economics, config);
const result = renderAgentContextEconomics(economics, config);
const joined = result.join('\n');
expect(joined).toContain('savings');
expect(joined).toContain('4,500 tokens');
expect(joined).toContain('4,500t saved');
});
it('should show savings percent when config has showSavingsPercent', () => {
const economics = createTestEconomics({ savingsPercent: 85, totalDiscoveryTokens: 1000 });
const config = createTestConfig({ showSavingsAmount: false, showSavingsPercent: true });
const result = renderMarkdownContextEconomics(economics, config);
const result = renderAgentContextEconomics(economics, config);
const joined = result.join('\n');
expect(joined).toContain('85%');
expect(joined).toContain('85% savings');
});
it('should not show savings when discovery tokens is 0', () => {
const economics = createTestEconomics({ totalDiscoveryTokens: 0, savings: 0, savingsPercent: 0 });
const config = createTestConfig({ showSavingsAmount: true, showSavingsPercent: true });
const result = renderMarkdownContextEconomics(economics, config);
const result = renderAgentContextEconomics(economics, config);
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', () => {
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[1]).toBe('');
});
});
describe('renderMarkdownFileHeader', () => {
it('should render file name in bold', () => {
const result = renderMarkdownFileHeader('src/index.ts');
describe('renderAgentFileHeader', () => {
it('should return empty array in compact format', () => {
const result = renderAgentFileHeader('src/index.ts');
expect(result[0]).toBe('**src/index.ts**');
});
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('|----');
expect(result).toHaveLength(0);
});
});
describe('renderMarkdownTableRow', () => {
it('should include observation ID with hash prefix', () => {
describe('renderAgentTableRow', () => {
it('should include observation ID', () => {
const obs = createTestObservation({ id: 42 });
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 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', () => {
const obs = createTestObservation({ title: 'Important Discovery' });
const config = createTestConfig();
const result = renderMarkdownTableRow(obs, '10:00', config);
const result = renderAgentTableRow(obs, '10:00 AM', config);
expect(result).toContain('Important Discovery');
});
@@ -308,30 +263,18 @@ describe('MarkdownFormatter', () => {
const obs = createTestObservation({ title: null });
const config = createTestConfig();
const result = renderMarkdownTableRow(obs, '10:00', config);
const result = renderAgentTableRow(obs, '10:00 AM', config);
expect(result).toContain('Untitled');
});
it('should show read tokens when config enabled', () => {
const obs = createTestObservation();
const config = createTestConfig({ showReadTokens: true });
it('should produce flat format: ID TIME TYPE TITLE', () => {
const obs = createTestObservation({ id: 5 });
const config = createTestConfig();
const result = renderMarkdownTableRow(obs, '10:00', config);
const result = renderAgentTableRow(obs, '10:00 AM', config);
expect(result).toContain('~');
});
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('');
expect(result).toBe('5 10:00a I Test Observation');
});
it('should use quote mark for repeated time', () => {
@@ -339,21 +282,21 @@ describe('MarkdownFormatter', () => {
const config = createTestConfig();
// Empty string timeDisplay means "same as previous"
const result = renderMarkdownTableRow(obs, '', config);
const result = renderAgentTableRow(obs, '', config);
expect(result).toContain('"');
});
});
describe('renderMarkdownFullObservation', () => {
describe('renderAgentFullObservation', () => {
it('should include observation ID and title', () => {
const obs = createTestObservation({ id: 7, title: 'Full Observation' });
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');
expect(joined).toContain('**#7**');
expect(joined).toContain('**7**');
expect(joined).toContain('**Full Observation**');
});
@@ -361,7 +304,7 @@ describe('MarkdownFormatter', () => {
const obs = createTestObservation();
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');
expect(joined).toContain('The detailed narrative here');
@@ -371,7 +314,7 @@ describe('MarkdownFormatter', () => {
const obs = createTestObservation();
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
expect(result.length).toBeLessThan(5);
@@ -381,28 +324,30 @@ describe('MarkdownFormatter', () => {
const obs = createTestObservation({ discovery_tokens: 250 });
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');
expect(joined).toContain('Read:');
expect(joined).toContain('Work:');
// Compact format: "~{readTokens}t" and "W {discoveryTokens}"
expect(joined).toContain('~');
expect(joined).toContain('t');
expect(joined).toContain('W 250');
});
});
describe('renderMarkdownSummaryItem', () => {
describe('renderAgentSummaryItem', () => {
it('should include session ID with S prefix', () => {
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');
expect(joined).toContain('**#S5**');
expect(joined).toContain('S5');
});
it('should include request text', () => {
const summary = { id: 1, request: 'Build authentication' };
const result = renderMarkdownSummaryItem(summary, '10:00');
const result = renderAgentSummaryItem(summary, '10:00');
const joined = result.join('\n');
expect(joined).toContain('Build authentication');
@@ -411,16 +356,16 @@ describe('MarkdownFormatter', () => {
it('should use "Session started" when request is null', () => {
const summary = { id: 1, request: null };
const result = renderMarkdownSummaryItem(summary, '10:00');
const result = renderAgentSummaryItem(summary, '10:00');
const joined = result.join('\n');
expect(joined).toContain('Session started');
});
});
describe('renderMarkdownSummaryField', () => {
describe('renderAgentSummaryField', () => {
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[0]).toBe('**Learned**: How to test');
@@ -428,27 +373,27 @@ describe('MarkdownFormatter', () => {
});
it('should return empty array when value is null', () => {
const result = renderMarkdownSummaryField('Learned', null);
const result = renderAgentSummaryField('Learned', null);
expect(result).toHaveLength(0);
});
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
expect(result).toHaveLength(0);
});
});
describe('renderMarkdownPreviouslySection', () => {
describe('renderAgentPreviouslySection', () => {
it('should render section when assistantMessage exists', () => {
const priorMessages: PriorMessages = {
userMessage: '',
assistantMessage: 'I completed the task successfully.',
};
const result = renderMarkdownPreviouslySection(priorMessages);
const result = renderAgentPreviouslySection(priorMessages);
const joined = result.join('\n');
expect(joined).toContain('**Previously**');
@@ -461,7 +406,7 @@ describe('MarkdownFormatter', () => {
assistantMessage: '',
};
const result = renderMarkdownPreviouslySection(priorMessages);
const result = renderAgentPreviouslySection(priorMessages);
expect(result).toHaveLength(0);
});
@@ -472,31 +417,30 @@ describe('MarkdownFormatter', () => {
assistantMessage: 'Some message',
};
const result = renderMarkdownPreviouslySection(priorMessages);
const result = renderAgentPreviouslySection(priorMessages);
const joined = result.join('\n');
expect(joined).toContain('---');
});
});
describe('renderMarkdownFooter', () => {
it('should include token amounts', () => {
const result = renderMarkdownFooter(10000, 500);
describe('renderAgentFooter', () => {
it('should include work token amount in k', () => {
const result = renderAgentFooter(10000, 500);
const joined = result.join('\n');
expect(joined).toContain('10k');
expect(joined).toContain('500');
});
it('should mention claude-mem skill', () => {
const result = renderMarkdownFooter(5000, 100);
it('should mention mem-search skill', () => {
const result = renderAgentFooter(5000, 100);
const joined = result.join('\n');
expect(joined).toContain('claude-mem');
expect(joined).toContain('mem-search skill');
});
it('should round work tokens to nearest thousand', () => {
const result = renderMarkdownFooter(15500, 100);
const result = renderAgentFooter(15500, 100);
const joined = result.join('\n');
// 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', () => {
const result = renderMarkdownEmptyState('my-project');
const result = renderAgentEmptyState('my-project');
expect(result).toContain('# [my-project] recent context');
expect(result).toContain('No previous sessions found');
expect(result).toContain('# $CMEM my-project');
expect(result).toContain('No previous sessions found.');
});
it('should be valid markdown', () => {
const result = renderMarkdownEmptyState('test');
const result = renderAgentEmptyState('test');
// Should start with h1
expect(result.startsWith('#')).toBe(true);
});
it('should handle empty project name', () => {
const result = renderMarkdownEmptyState('');
const result = renderAgentEmptyState('');
expect(result).toContain('# [] recent context');
expect(result).toContain('# $CMEM ');
});
});
});