/** * FormattingService - Handles all formatting logic for search results * Uses table format matching context-generator style for visual consistency */ import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../sqlite/types.js'; import { ModeManager } from '../domain/ModeManager.js'; // Token estimation constant (matches context-generator) const CHARS_PER_TOKEN_ESTIMATE = 4; export class FormattingService { /** * Format search tips footer */ formatSearchTips(): string { return `\n--- 💡 Search Strategy: 1. Search with index to see titles, dates, IDs 2. Use timeline to get context around interesting results 3. Batch fetch full details: get_observations(ids=[...]) Tips: • Filter by type: obs_type="bugfix,feature" • Filter by date: dateStart="2025-01-01" • Sort: orderBy="date_desc" or "date_asc"`; } /** * Format time from epoch (matches context-generator formatTime) */ private formatTime(epoch: number): string { return new Date(epoch).toLocaleString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); } /** * Estimate read tokens for an observation */ private estimateReadTokens(obs: ObservationSearchResult): number { const size = (obs.title?.length || 0) + (obs.subtitle?.length || 0) + (obs.narrative?.length || 0) + (obs.facts?.length || 0); return Math.ceil(size / CHARS_PER_TOKEN_ESTIMATE); } /** * Format observation as table row * | ID | Time | T | Title | Read | Work | */ formatObservationIndex(obs: ObservationSearchResult, _index: number): string { const id = `#${obs.id}`; const time = this.formatTime(obs.created_at_epoch); const icon = ModeManager.getInstance().getTypeIcon(obs.type); const title = obs.title || 'Untitled'; const readTokens = this.estimateReadTokens(obs); const workEmoji = ModeManager.getInstance().getWorkEmoji(obs.type); const workTokens = obs.discovery_tokens || 0; const workDisplay = workTokens > 0 ? `${workEmoji} ${workTokens}` : '-'; return `| ${id} | ${time} | ${icon} | ${title} | ~${readTokens} | ${workDisplay} |`; } /** * Format session summary as table row * | ID | Time | T | Title | - | - | */ formatSessionIndex(session: SessionSummarySearchResult, _index: number): string { const id = `#S${session.id}`; const time = this.formatTime(session.created_at_epoch); const icon = '🎯'; const title = session.request || `Session ${session.sdk_session_id?.substring(0, 8) || 'unknown'}`; return `| ${id} | ${time} | ${icon} | ${title} | - | - |`; } /** * Format user prompt as table row * | ID | Time | T | Title | - | - | */ formatUserPromptIndex(prompt: UserPromptSearchResult, _index: number): string { const id = `#P${prompt.id}`; const time = this.formatTime(prompt.created_at_epoch); const icon = '💬'; // Truncate long prompts for table display const title = prompt.prompt_text.length > 60 ? prompt.prompt_text.substring(0, 57) + '...' : prompt.prompt_text; return `| ${id} | ${time} | ${icon} | ${title} | - | - |`; } /** * Generate table header for observations */ formatTableHeader(): string { return `| ID | Time | T | Title | Read | Work | |-----|------|---|-------|------|------|`; } /** * Generate table header for search results (no Work column) */ formatSearchTableHeader(): string { return `| ID | Time | T | Title | Read | |----|------|---|-------|------|`; } /** * Format observation as table row for search results (no Work column) */ formatObservationSearchRow(obs: ObservationSearchResult, lastTime: string): { row: string; time: string } { const id = `#${obs.id}`; const time = this.formatTime(obs.created_at_epoch); const icon = ModeManager.getInstance().getTypeIcon(obs.type); const title = obs.title || 'Untitled'; const readTokens = this.estimateReadTokens(obs); // Use ditto mark if same time as previous row const timeDisplay = time === lastTime ? '″' : time; return { row: `| ${id} | ${timeDisplay} | ${icon} | ${title} | ~${readTokens} |`, time }; } /** * Format session summary as table row for search results (no Work column) */ formatSessionSearchRow(session: SessionSummarySearchResult, lastTime: string): { row: string; time: string } { const id = `#S${session.id}`; const time = this.formatTime(session.created_at_epoch); const icon = '🎯'; const title = session.request || `Session ${session.sdk_session_id?.substring(0, 8) || 'unknown'}`; // Use ditto mark if same time as previous row const timeDisplay = time === lastTime ? '″' : time; return { row: `| ${id} | ${timeDisplay} | ${icon} | ${title} | - |`, time }; } /** * Format user prompt as table row for search results (no Work column) */ formatUserPromptSearchRow(prompt: UserPromptSearchResult, lastTime: string): { row: string; time: string } { const id = `#P${prompt.id}`; const time = this.formatTime(prompt.created_at_epoch); const icon = '💬'; // Truncate long prompts for table display const title = prompt.prompt_text.length > 60 ? prompt.prompt_text.substring(0, 57) + '...' : prompt.prompt_text; // Use ditto mark if same time as previous row const timeDisplay = time === lastTime ? '″' : time; return { row: `| ${id} | ${timeDisplay} | ${icon} | ${title} | - |`, time }; } }