Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 80 additions & 8 deletions src/services/context-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import path from 'path';
import { homedir } from 'os';
import { existsSync, readFileSync, unlinkSync } from 'fs';
import { existsSync, readFileSync, unlinkSync, readdirSync, statSync } from 'fs';
import { SessionStore } from './sqlite/SessionStore.js';
import {
OBSERVATION_TYPES,
Expand Down Expand Up @@ -41,6 +41,7 @@ interface ContextConfig {
fullObservationField: 'narrative' | 'facts';
showLastSummary: boolean;
showLastMessage: boolean;
showLastPlan: boolean;
}

/**
Expand Down Expand Up @@ -69,6 +70,7 @@ function loadContextConfig(): ContextConfig {
fullObservationField: settings.CLAUDE_MEM_CONTEXT_FULL_FIELD as 'narrative' | 'facts',
showLastSummary: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY === 'true',
showLastMessage: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE === 'true',
showLastPlan: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_PLAN === 'true',
};
} catch (error) {
logger.warn('WORKER', 'Failed to load context settings, using defaults', {}, error as Error);
Expand All @@ -86,6 +88,7 @@ function loadContextConfig(): ContextConfig {
fullObservationField: 'narrative' as const,
showLastSummary: true,
showLastMessage: false,
showLastPlan: false,
};
}
}
Expand Down Expand Up @@ -259,6 +262,53 @@ function extractPriorMessages(transcriptPath: string): { userMessage: string; as
}
}

/**
* Get the most recent plan file from ~/.claude/plans/
* Returns { filePath, content, title } or null if no plans exist
*/
function getMostRecentPlanFile(): { filePath: string; content: string; title: string } | null {
try {
const plansDir = path.join(homedir(), '.claude', 'plans');

if (!existsSync(plansDir)) {
return null;
}

const files = readdirSync(plansDir)
.filter((f: string) => f.endsWith('.md'))
.map((f: string) => {
const fullPath = path.join(plansDir, f);
const stat = statSync(fullPath);
return {
name: f,
path: fullPath,
mtime: stat.mtime.getTime()
};
})
.sort((a: { mtime: number }, b: { mtime: number }) => b.mtime - a.mtime);

if (files.length === 0) {
return null;
}

const mostRecent = files[0];
const content = readFileSync(mostRecent.path, 'utf-8');

// Extract title from H1 heading
const titleMatch = content.match(/^#\s+(.+)$/m);
const title = titleMatch?.[1] || mostRecent.name.replace('.md', '');
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern /^#\s+(.+)$/m will match the first H1 heading anywhere in the file, but it could match a heading with trailing whitespace. Consider trimming the captured title with .trim() to ensure clean titles, like: const title = titleMatch?.[1]?.trim() || mostRecent.name.replace('.md', '');

Suggested change
const title = titleMatch?.[1] || mostRecent.name.replace('.md', '');
const title = titleMatch?.[1]?.trim() || mostRecent.name.replace('.md', '');

Copilot uses AI. Check for mistakes.

return {
filePath: mostRecent.path,
content: content.trim(),
title
};
} catch (error) {
logger.warn('WORKER', 'Failed to read plan files', {}, error as Error);
return null;
}
}

/**
* Generate context for a project
*/
Expand Down Expand Up @@ -409,9 +459,9 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
const totalObservations = observations.length;
const totalReadTokens = observations.reduce((sum, obs) => {
const obsSize = (obs.title?.length || 0) +
(obs.subtitle?.length || 0) +
(obs.narrative?.length || 0) +
JSON.stringify(obs.facts || []).length;
(obs.subtitle?.length || 0) +
(obs.narrative?.length || 0) +
JSON.stringify(obs.facts || []).length;
return sum + Math.ceil(obsSize / CHARS_PER_TOKEN_ESTIMATE);
}, 0);
const totalDiscoveryTokens = observations.reduce((sum, obs) => sum + (obs.discovery_tokens || 0), 0);
Expand All @@ -421,7 +471,7 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
: 0;

const showContextEconomics = config.showReadTokens || config.showWorkTokens ||
config.showSavingsAmount || config.showSavingsPercent;
config.showSavingsAmount || config.showSavingsPercent;

if (showContextEconomics) {
if (useColors) {
Expand Down Expand Up @@ -585,9 +635,9 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
const icon = TYPE_ICON_MAP[obs.type as keyof typeof TYPE_ICON_MAP] || '•';

const obsSize = (obs.title?.length || 0) +
(obs.subtitle?.length || 0) +
(obs.narrative?.length || 0) +
JSON.stringify(obs.facts || []).length;
(obs.subtitle?.length || 0) +
(obs.narrative?.length || 0) +
JSON.stringify(obs.facts || []).length;
const readTokens = Math.ceil(obsSize / CHARS_PER_TOKEN_ESTIMATE);
const discoveryTokens = obs.discovery_tokens || 0;
const workEmoji = TYPE_WORK_EMOJI_MAP[obs.type as keyof typeof TYPE_WORK_EMOJI_MAP] || '🔍';
Expand Down Expand Up @@ -695,6 +745,28 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
output.push('');
}

// Include last plan if enabled
if (config.showLastPlan) {
const plan = getMostRecentPlanFile();
if (plan) {
output.push('');
output.push('---');
output.push('');
if (useColors) {
output.push(`${colors.bright}${colors.cyan}📋 Last Plan: ${plan.title}${colors.reset}`);
output.push(`${colors.dim}File: ${plan.filePath}${colors.reset}`);
output.push('');
output.push(plan.content);
} else {
output.push(`**📋 Last Plan: ${plan.title}**`);
output.push(`_File: ${plan.filePath}_`);
output.push('');
output.push(plan.content);
}
output.push('');
}
}

// Footer
if (showContextEconomics && totalDiscoveryTokens > 0 && savings > 0) {
const workTokensK = Math.round(totalDiscoveryTokens / 1000);
Expand Down
2 changes: 2 additions & 0 deletions src/services/worker/http/routes/SettingsRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export class SettingsRoutes extends BaseRouteHandler {
// Feature Toggles
'CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY',
'CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE',
'CLAUDE_MEM_CONTEXT_SHOW_LAST_PLAN',
];

for (const key of settingKeys) {
Expand Down Expand Up @@ -266,6 +267,7 @@ export class SettingsRoutes extends BaseRouteHandler {
'CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT',
'CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY',
'CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE',
'CLAUDE_MEM_CONTEXT_SHOW_LAST_PLAN',
];

for (const key of booleanSettings) {
Expand Down
2 changes: 2 additions & 0 deletions src/shared/SettingsDefaultsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface SettingsDefaults {
// Feature Toggles
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: string;
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: string;
CLAUDE_MEM_CONTEXT_SHOW_LAST_PLAN: string;
}

export class SettingsDefaultsManager {
Expand Down Expand Up @@ -67,6 +68,7 @@ export class SettingsDefaultsManager {
// Feature Toggles
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: 'true',
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
CLAUDE_MEM_CONTEXT_SHOW_LAST_PLAN: 'false',
};

/**
Expand Down
1 change: 1 addition & 0 deletions src/ui/viewer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface Settings {
// Feature Toggles
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY?: string;
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE?: string;
CLAUDE_MEM_CONTEXT_SHOW_LAST_PLAN?: string;
}

export interface WorkerStats {
Expand Down