diff --git a/src/services/context/ContextBuilder.ts b/src/services/context/ContextBuilder.ts index 3971bd36..1c6f2b9c 100644 --- a/src/services/context/ContextBuilder.ts +++ b/src/services/context/ContextBuilder.ts @@ -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 { 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; diff --git a/src/services/context/formatters/MarkdownFormatter.ts b/src/services/context/formatters/AgentFormatter.ts similarity index 72% rename from src/services/context/formatters/MarkdownFormatter.ts rename to src/services/context/formatters/AgentFormatter.ts index 50164e2f..54ff6c76 100644 --- a/src/services/context/formatters/MarkdownFormatter.ts +++ b/src/services/context/formatters/AgentFormatter.ts @@ -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.`; } diff --git a/src/services/context/formatters/ColorFormatter.ts b/src/services/context/formatters/HumanFormatter.ts similarity index 82% rename from src/services/context/formatters/ColorFormatter.ts rename to src/services/context/formatters/HumanFormatter.ts index 20061813..1abfa740 100644 --- a/src/services/context/formatters/ColorFormatter.ts +++ b/src/services/context/formatters/HumanFormatter.ts @@ -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`; } diff --git a/src/services/context/sections/FooterRenderer.ts b/src/services/context/sections/FooterRenderer.ts index a592a1af..af5773e8 100644 --- a/src/services/context/sections/FooterRenderer.ts +++ b/src/services/context/sections/FooterRenderer.ts @@ -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); } diff --git a/src/services/context/sections/HeaderRenderer.ts b/src/services/context/sections/HeaderRenderer.ts index 42ac201e..c94b9623 100644 --- a/src/services/context/sections/HeaderRenderer.ts +++ b/src/services/context/sections/HeaderRenderer.ts @@ -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)); } } diff --git a/src/services/context/sections/SummaryRenderer.ts b/src/services/context/sections/SummaryRenderer.ts index fcf51a9c..93b13aab 100644 --- a/src/services/context/sections/SummaryRenderer.ts +++ b/src/services/context/sections/SummaryRenderer.ts @@ -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; diff --git a/src/services/context/sections/TimelineRenderer.ts b/src/services/context/sections/TimelineRenderer.ts index 9cf6741e..b1f43724 100644 --- a/src/services/context/sections/TimelineRenderer.ts +++ b/src/services/context/sections/TimelineRenderer.ts @@ -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, @@ -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, @@ -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, 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, 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; diff --git a/src/services/worker/http/routes/SearchRoutes.ts b/src/services/worker/http/routes/SearchRoutes.ts index 3cea0e24..40d2581a 100644 --- a/src/services/worker/http/routes/SearchRoutes.ts +++ b/src/services/worker/http/routes/SearchRoutes.ts @@ -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 => { // 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 diff --git a/tests/context/formatters/markdown-formatter.test.ts b/tests/context/formatters/agent-formatter.test.ts similarity index 55% rename from tests/context/formatters/markdown-formatter.test.ts rename to tests/context/formatters/agent-formatter.test.ts index 49266acf..34d3f516 100644 --- a/tests/context/formatters/markdown-formatter.test.ts +++ b/tests/context/formatters/agent-formatter.test.ts @@ -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 }; } -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 '); }); }); });