From 86214b93a99da0bb599ab366870258978e290a5f Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Tue, 21 Oct 2025 17:28:39 -0400 Subject: [PATCH] Add support for index view in context hook and refactor summary retrieval method --- src/bin/hooks/context-hook.ts | 7 +- src/hooks/context.ts | 359 ++++++++++++++++------------ src/services/sqlite/SessionStore.ts | 25 ++ 3 files changed, 232 insertions(+), 159 deletions(-) diff --git a/src/bin/hooks/context-hook.ts b/src/bin/hooks/context-hook.ts index 44496275..0c9ed2b5 100644 --- a/src/bin/hooks/context-hook.ts +++ b/src/bin/hooks/context-hook.ts @@ -8,9 +8,12 @@ import { contextHook } from '../../hooks/context.js'; import { stdin } from 'process'; try { + // Check for --index flag + const useIndexView = process.argv.includes('--index'); + if (stdin.isTTY) { // Running manually from terminal - print formatted output with colors - const contextOutput = contextHook(undefined, true); + const contextOutput = contextHook(undefined, true, useIndexView); console.log(contextOutput); process.exit(0); } else { @@ -19,7 +22,7 @@ try { stdin.on('data', (chunk) => input += chunk); stdin.on('end', () => { const parsed = input.trim() ? JSON.parse(input) : undefined; - const contextOutput = contextHook(parsed, false); + const contextOutput = contextHook(parsed, false, useIndexView); const result = { hookSpecificOutput: { hookEventName: "SessionStart", diff --git a/src/hooks/context.ts b/src/hooks/context.ts index 90009429..3599fc35 100644 --- a/src/hooks/context.ts +++ b/src/hooks/context.ts @@ -30,7 +30,7 @@ const colors = { * * Output: Returns formatted context string to be wrapped in hookSpecificOutput */ -export function contextHook(input?: SessionStartInput, useColors: boolean = false): string { +export function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): string { // v4.0.0: Ensure worker is running before loading context ensureWorkerRunning(); const cwd = input?.cwd ?? process.cwd(); @@ -39,215 +39,260 @@ export function contextHook(input?: SessionStartInput, useColors: boolean = fals const db = new SessionStore(); try { - const sessions = db.getRecentSessionsWithStatus(project, 3); + const summaries = db.getRecentSummariesWithSessionInfo(project, 3); - if (sessions.length === 0) { + if (summaries.length === 0) { if (useColors) { - return `\n${colors.bright}${colors.cyan}📝 Recent Session Context${colors.reset}\n\n${colors.dim}No previous sessions found for this project yet.${colors.reset}\n`; + return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous summaries found for this project yet.${colors.reset}\n`; } - return '# Recent Session Context\n\nNo previous sessions found for this project yet.'; + return `# [${project}] recent context\n\nNo previous summaries found for this project yet.`; } const output: string[] = []; - if (useColors) { - output.push(''); - output.push(`${colors.bright}${colors.cyan}📝 Recent Session Context${colors.reset}`); - output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); - output.push(`${colors.dim}Showing last ${sessions.length} session(s) for ${colors.reset}${colors.bright}${project}${colors.reset}`); - output.push(''); - } else { - output.push('# Recent Session Context'); - output.push(''); - output.push(`Showing last ${sessions.length} session(s) for **${project}**:`); - output.push(''); - } - - for (const session of sessions) { - if (!session.sdk_session_id) continue; - + // Index view: Show previous as index, latest in full at bottom (chat-style) + if (useIndexView) { if (useColors) { + output.push(''); + output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`); output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); output.push(''); } else { - output.push('---'); + output.push(`# [${project}] recent context`); output.push(''); } - // Check if session has a summary - if (session.has_summary) { - const summary = db.getSummaryForSession(session.sdk_session_id); - - if (summary) { - const promptLabel = summary.prompt_number ? ` (Prompt #${summary.prompt_number})` : ''; - - if (useColors) { - output.push(`${colors.bright}${colors.green}✓ Summary${promptLabel}${colors.reset}`); - output.push(''); - } else { - output.push(`**Summary${promptLabel}**`); - output.push(''); - } - - if (summary.request) { - if (useColors) { - output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`); - output.push(''); - } else { - output.push(`**Request:** ${summary.request}`); - } - } - - if (summary.learned) { - if (useColors) { - output.push(`${colors.bright}${colors.blue}Learned:${colors.reset} ${summary.learned}`); - output.push(''); - } else { - output.push(`**Learned:** ${summary.learned}`); - } - } - - if (summary.completed) { - if (useColors) { - output.push(`${colors.bright}${colors.green}Completed:${colors.reset} ${summary.completed}`); - output.push(''); - } else { - output.push(`**Completed:** ${summary.completed}`); - } - } - - if (summary.next_steps) { - if (useColors) { - output.push(`${colors.bright}${colors.magenta}Next Steps:${colors.reset} ${summary.next_steps}`); - output.push(''); - } else { - output.push(`**Next Steps:** ${summary.next_steps}`); - } - } - - // Get files from observations (not from summary which is never populated) - const sessionFiles = db.getFilesForSession(session.sdk_session_id); - - if (sessionFiles.filesRead.length > 0) { - if (useColors) { - output.push(`${colors.dim}Files Read: ${sessionFiles.filesRead.join(', ')}${colors.reset}`); - } else { - output.push(`**Files Read:** ${sessionFiles.filesRead.join(', ')}`); - } - } - - if (sessionFiles.filesModified.length > 0) { - if (useColors) { - output.push(`${colors.dim}Files Modified: ${sessionFiles.filesModified.join(', ')}${colors.reset}`); - } else { - output.push(`**Files Modified:** ${sessionFiles.filesModified.join(', ')}`); - } - } - - const dateTime = new Date(summary.created_at).toLocaleString(); - if (useColors) { - output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`); - } else { - output.push(`**Date:** ${dateTime}`); - } - } - } else if (session.status === 'active') { - // Active session without summary - show observation titles + // Show index of previous summaries (oldest to newest) + if (summaries.length > 1) { if (useColors) { - output.push(`${colors.bright}${colors.yellow}⏳ In Progress${colors.reset}`); + output.push(`${colors.bright}${colors.dim}Previous Requests:${colors.reset}`); output.push(''); } else { - output.push(`**In Progress**`); + output.push('**Previous Requests:**'); output.push(''); } - if (session.user_prompt) { + // Iterate backwards through array (skip first which is most recent) + for (let i = summaries.length - 1; i >= 1; i--) { + const prev = summaries[i]; + const prevDate = new Date(prev.created_at); + const dateTimeStr = prevDate.toLocaleString(); + if (useColors) { - output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${session.user_prompt}`); - output.push(''); + output.push(`${colors.dim}• ${dateTimeStr}:${colors.reset} ${prev.request || '(no request)'}`); } else { - output.push(`**Request:** ${session.user_prompt}`); + output.push(`- ${dateTimeStr}: ${prev.request || '(no request)'}`); } } - const observations = db.getObservationsForSession(session.sdk_session_id); - - if (observations.length > 0) { + if (useColors) { + output.push(''); + output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); output.push(''); - if (useColors) { - output.push(`${colors.bright}Observations (${observations.length}):${colors.reset}`); - for (const obs of observations) { - output.push(` ${colors.dim}•${colors.reset} ${obs.title}`); - } - output.push(''); - } else { - output.push(`**Observations (${observations.length}):**`); - for (const obs of observations) { - output.push(`- ${obs.title}`); - } - } } else { output.push(''); - if (useColors) { - output.push(`${colors.dim}No observations yet${colors.reset}`); - output.push(''); - } else { - output.push('*No observations yet*'); - } + output.push('---'); + output.push(''); } + } + // Show most recent summary in full at the bottom + const latest = summaries[0]; + + if (latest.request) { + if (useColors) { + output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${latest.request}`); + output.push(''); + } else { + output.push(`**Request:** ${latest.request}`); + output.push(''); + } + } + + if (latest.learned) { + if (useColors) { + output.push(`${colors.bright}${colors.blue}Learned:${colors.reset} ${latest.learned}`); + output.push(''); + } else { + output.push(`**Learned:** ${latest.learned}`); + output.push(''); + } + } + + if (latest.completed) { + if (useColors) { + output.push(`${colors.bright}${colors.green}Completed:${colors.reset} ${latest.completed}`); + output.push(''); + } else { + output.push(`**Completed:** ${latest.completed}`); + output.push(''); + } + } + + if (latest.next_steps) { + if (useColors) { + output.push(`${colors.bright}${colors.magenta}Next Steps:${colors.reset} ${latest.next_steps}`); + output.push(''); + } else { + output.push(`**Next Steps:** ${latest.next_steps}`); + output.push(''); + } + } + + // Get files for latest summary + const latestFiles = db.getFilesForSession(latest.sdk_session_id); + + if (latestFiles.filesRead.length > 0) { + if (useColors) { + output.push(`${colors.dim}Files Read: ${latestFiles.filesRead.join(', ')}${colors.reset}`); + } else { + output.push(`**Files Read:** ${latestFiles.filesRead.join(', ')}`); + } + } + + if (latestFiles.filesModified.length > 0) { + if (useColors) { + output.push(`${colors.dim}Files Modified: ${latestFiles.filesModified.join(', ')}${colors.reset}`); + } else { + output.push(`**Files Modified:** ${latestFiles.filesModified.join(', ')}`); + } + } + + const latestDate = new Date(latest.created_at).toLocaleString(); + if (useColors) { + output.push(`${colors.dim}Date: ${latestDate}${colors.reset}`); + } else { + output.push(`**Date:** ${latestDate}`); + } + + if (useColors) { output.push(''); - const activeDateTime = new Date(session.started_at).toLocaleString(); + output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); + } + + return output.join('\n'); + } + + if (useColors) { + output.push(''); + output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`); + output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); + } else { + output.push(`# [${project}] recent context`); + output.push(''); + } + + let previousSessionId: string | null = null; + let isFirstSummary = true; + + for (const summary of summaries) { + // Add session break indicator if this is a different session + const isNewSession = previousSessionId !== null && summary.sdk_session_id !== previousSessionId; + + if (isNewSession) { if (useColors) { - output.push(`${colors.dim}Status: Active - summary pending${colors.reset}`); - output.push(`${colors.dim}Date: ${activeDateTime}${colors.reset}`); + output.push(''); + output.push(`${colors.dim}${'─'.repeat(23)} New Session ${'─'.repeat(24)}${colors.reset}`); + output.push(''); } else { - output.push(`**Status:** Active - summary pending`); - output.push(`**Date:** ${activeDateTime}`); + output.push(''); + output.push('--- New Session ---'); + output.push(''); + } + } else if (!isFirstSummary) { + // Only show regular separator if not first summary and not showing "New Session" + if (useColors) { + output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); + output.push(''); + } else { + output.push('---'); + output.push(''); } } else { - // Failed or completed session without summary - const displayStatus = session.status === 'failed' ? 'stopped' : session.status; - const statusIcon = session.status === 'failed' ? '⚠️' : '○'; - + // First summary - just add a blank line after header if (useColors) { - const statusColor = session.status === 'failed' ? colors.yellow : colors.gray; - output.push(`${colors.bright}${statusColor}${statusIcon} ${displayStatus.charAt(0).toUpperCase() + displayStatus.slice(1)}${colors.reset}`); - output.push(''); - } else { - output.push(`**${displayStatus.charAt(0).toUpperCase() + displayStatus.slice(1)}**`); output.push(''); } + } - if (session.user_prompt) { - if (useColors) { - output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${session.user_prompt}`); - output.push(''); - } else { - output.push(`**Request:** ${session.user_prompt}`); - } - } + isFirstSummary = false; - output.push(''); - const failedDateTime = new Date(session.started_at).toLocaleString(); + if (summary.request) { if (useColors) { - output.push(`${colors.dim}Status: ${displayStatus} - no summary available${colors.reset}`); - output.push(`${colors.dim}Date: ${failedDateTime}${colors.reset}`); + output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`); + output.push(''); } else { - output.push(`**Status:** ${displayStatus} - no summary available`); - output.push(`**Date:** ${failedDateTime}`); + output.push(`**Request:** ${summary.request}`); + output.push(''); } } + if (summary.learned) { + if (useColors) { + output.push(`${colors.bright}${colors.blue}Learned:${colors.reset} ${summary.learned}`); + output.push(''); + } else { + output.push(`**Learned:** ${summary.learned}`); + output.push(''); + } + } + + if (summary.completed) { + if (useColors) { + output.push(`${colors.bright}${colors.green}Completed:${colors.reset} ${summary.completed}`); + output.push(''); + } else { + output.push(`**Completed:** ${summary.completed}`); + output.push(''); + } + } + + if (summary.next_steps) { + if (useColors) { + output.push(`${colors.bright}${colors.magenta}Next Steps:${colors.reset} ${summary.next_steps}`); + output.push(''); + } else { + output.push(`**Next Steps:** ${summary.next_steps}`); + output.push(''); + } + } + + // Get files from observations (not from summary which is never populated) + const sessionFiles = db.getFilesForSession(summary.sdk_session_id); + + if (sessionFiles.filesRead.length > 0) { + if (useColors) { + output.push(`${colors.dim}Files Read: ${sessionFiles.filesRead.join(', ')}${colors.reset}`); + } else { + output.push(`**Files Read:** ${sessionFiles.filesRead.join(', ')}`); + } + } + + if (sessionFiles.filesModified.length > 0) { + if (useColors) { + output.push(`${colors.dim}Files Modified: ${sessionFiles.filesModified.join(', ')}${colors.reset}`); + } else { + output.push(`**Files Modified:** ${sessionFiles.filesModified.join(', ')}`); + } + } + + const dateTime = new Date(summary.created_at).toLocaleString(); + if (useColors) { + output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`); + } else { + output.push(`**Date:** ${dateTime}`); + } + if (!useColors) { output.push(''); } + + previousSessionId = summary.sdk_session_id; } if (useColors) { output.push(''); output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); - output.push(''); } return output.join('\n'); diff --git a/src/services/sqlite/SessionStore.ts b/src/services/sqlite/SessionStore.ts index 8a3bd876..9a1c4d99 100644 --- a/src/services/sqlite/SessionStore.ts +++ b/src/services/sqlite/SessionStore.ts @@ -433,6 +433,31 @@ export class SessionStore { return stmt.all(project, limit) as any[]; } + /** + * Get recent summaries with session info for context display + */ + getRecentSummariesWithSessionInfo(project: string, limit: number = 3): Array<{ + sdk_session_id: string; + request: string | null; + learned: string | null; + completed: string | null; + next_steps: string | null; + prompt_number: number | null; + created_at: string; + }> { + const stmt = this.db.prepare(` + SELECT + sdk_session_id, request, learned, completed, next_steps, + prompt_number, created_at + FROM session_summaries + WHERE project = ? + ORDER BY created_at_epoch DESC + LIMIT ? + `); + + return stmt.all(project, limit) as any[]; + } + /** * Get recent observations for a project */