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:
@@ -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;
|
||||
|
||||
+29
-29
@@ -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.`;
|
||||
}
|
||||
+29
-29
@@ -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`;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+110
-166
@@ -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 ');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user