feat: Enhance context settings with validation and UI options
- Added ContextConfig interface and loadContextConfig function to manage context settings. - Implemented validation for context settings in WorkerService. - Updated Sidebar component to include new context settings for token economics, observation filtering, display configuration, and feature toggles. - Introduced default settings for new context features. - Adjusted types to accommodate new settings in the application state.
This commit is contained in:
+200
-22
@@ -41,6 +41,80 @@ function getContextDepth(): number {
|
||||
return parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50', 10);
|
||||
}
|
||||
|
||||
interface ContextConfig {
|
||||
// Display counts
|
||||
totalObservationCount: number;
|
||||
fullObservationCount: number;
|
||||
sessionCount: number;
|
||||
|
||||
// Token display toggles
|
||||
showReadTokens: boolean;
|
||||
showWorkTokens: boolean;
|
||||
showSavingsAmount: boolean;
|
||||
showSavingsPercent: boolean;
|
||||
|
||||
// Filters
|
||||
observationTypes: Set<string>;
|
||||
observationConcepts: Set<string>;
|
||||
|
||||
// Display options
|
||||
fullObservationField: 'narrative' | 'facts';
|
||||
showLastSummary: boolean;
|
||||
showLastMessage: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all context configuration settings
|
||||
* Priority: ~/.claude/settings.json > env var > defaults
|
||||
*/
|
||||
function loadContextConfig(): ContextConfig {
|
||||
const defaults = {
|
||||
totalObservationCount: parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50', 10),
|
||||
fullObservationCount: 5,
|
||||
sessionCount: 10,
|
||||
showReadTokens: true,
|
||||
showWorkTokens: true,
|
||||
showSavingsAmount: true,
|
||||
showSavingsPercent: true,
|
||||
observationTypes: new Set(['bugfix', 'feature', 'refactor', 'discovery', 'decision', 'change']),
|
||||
observationConcepts: new Set(['how-it-works', 'why-it-exists', 'what-changed', 'problem-solution', 'gotcha', 'pattern', 'trade-off']),
|
||||
fullObservationField: 'narrative' as const,
|
||||
showLastSummary: true,
|
||||
showLastMessage: false,
|
||||
};
|
||||
|
||||
try {
|
||||
const settingsPath = path.join(homedir(), '.claude', 'settings.json');
|
||||
if (!existsSync(settingsPath)) return defaults;
|
||||
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
const env = settings.env || {};
|
||||
|
||||
return {
|
||||
totalObservationCount: parseInt(env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50', 10),
|
||||
fullObservationCount: parseInt(env.CLAUDE_MEM_CONTEXT_FULL_COUNT || '5', 10),
|
||||
sessionCount: parseInt(env.CLAUDE_MEM_CONTEXT_SESSION_COUNT || '10', 10),
|
||||
showReadTokens: env.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS !== 'false',
|
||||
showWorkTokens: env.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS !== 'false',
|
||||
showSavingsAmount: env.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT !== 'false',
|
||||
showSavingsPercent: env.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT !== 'false',
|
||||
observationTypes: new Set(
|
||||
(env.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES || 'bugfix,feature,refactor,discovery,decision,change')
|
||||
.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
),
|
||||
observationConcepts: new Set(
|
||||
(env.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || 'how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off')
|
||||
.split(',').map((c: string) => c.trim()).filter(Boolean)
|
||||
),
|
||||
fullObservationField: (env.CLAUDE_MEM_CONTEXT_FULL_FIELD || 'narrative') as 'narrative' | 'facts',
|
||||
showLastSummary: env.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY !== 'false',
|
||||
showLastMessage: env.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE === 'true',
|
||||
};
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration: Read from settings.json or environment
|
||||
const DISPLAY_OBSERVATION_COUNT = getContextDepth();
|
||||
const DISPLAY_SESSION_COUNT = 10; // Recent sessions for timeline context
|
||||
@@ -163,6 +237,7 @@ function renderSummaryField(label: string, value: string | null, color: string,
|
||||
* Context Hook Main Logic
|
||||
*/
|
||||
async function contextHook(input?: SessionStartInput, useColors: boolean = false): Promise<string> {
|
||||
const config = loadContextConfig();
|
||||
const cwd = input?.cwd ?? process.cwd();
|
||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
||||
|
||||
@@ -190,7 +265,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
|
||||
// Get ALL recent observations for this project (not filtered by summaries)
|
||||
// This ensures we show observations even when summaries haven't been generated
|
||||
// Configurable via CLAUDE_MEM_CONTEXT_OBSERVATIONS env var (default: 50)
|
||||
// Configurable via settings (default: 50)
|
||||
const allObservations = db.db.prepare(`
|
||||
SELECT
|
||||
id, sdk_session_id, type, title, subtitle, narrative,
|
||||
@@ -200,7 +275,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(project, DISPLAY_OBSERVATION_COUNT) as Observation[];
|
||||
`).all(project, config.totalObservationCount) as Observation[];
|
||||
|
||||
// Get recent summaries (optional - may not exist for recent sessions)
|
||||
// Fetch one extra for offset calculation
|
||||
@@ -210,7 +285,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(project, DISPLAY_SESSION_COUNT + SUMMARY_LOOKAHEAD) as SessionSummary[];
|
||||
`).all(project, config.sessionCount + SUMMARY_LOOKAHEAD) as SessionSummary[];
|
||||
|
||||
// If we have neither observations nor summaries, show empty state
|
||||
if (allObservations.length === 0 && recentSummaries.length === 0) {
|
||||
@@ -221,11 +296,19 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
return `# [${project}] recent context\n\nNo previous sessions found for this project yet.`;
|
||||
}
|
||||
|
||||
// Use observations for display (summaries are supplementary)
|
||||
const observations = allObservations;
|
||||
const displaySummaries = recentSummaries.slice(0, DISPLAY_SESSION_COUNT);
|
||||
// Filter observations by type
|
||||
let observations = allObservations.filter(obs => config.observationTypes.has(obs.type));
|
||||
|
||||
// All observations are shown in timeline (filtered by type, not concepts)
|
||||
// Filter by concepts (include if observation has at least one matching concept)
|
||||
observations = observations.filter(obs => {
|
||||
if (config.observationConcepts.size === 0) return true;
|
||||
const obsConcepts = parseJsonArray(obs.concepts);
|
||||
return obsConcepts.some(c => config.observationConcepts.has(c));
|
||||
});
|
||||
|
||||
const displaySummaries = recentSummaries.slice(0, config.sessionCount);
|
||||
|
||||
// All filtered observations are shown in timeline
|
||||
const timelineObs = observations;
|
||||
|
||||
// Build output
|
||||
@@ -301,18 +384,42 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
// Display Context Economics section
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.cyan}📊 Context Economics${colors.reset}`);
|
||||
output.push(`${colors.dim} Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)${colors.reset}`);
|
||||
output.push(`${colors.dim} Work investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions${colors.reset}`);
|
||||
if (totalDiscoveryTokens > 0) {
|
||||
output.push(`${colors.green} Your savings: ${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)${colors.reset}`);
|
||||
if (config.showReadTokens) {
|
||||
output.push(`${colors.dim} Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)${colors.reset}`);
|
||||
}
|
||||
if (config.showWorkTokens) {
|
||||
output.push(`${colors.dim} Work investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions${colors.reset}`);
|
||||
}
|
||||
if (totalDiscoveryTokens > 0 && (config.showSavingsAmount || config.showSavingsPercent)) {
|
||||
let savingsLine = ' Your savings: ';
|
||||
if (config.showSavingsAmount && config.showSavingsPercent) {
|
||||
savingsLine += `${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)`;
|
||||
} else if (config.showSavingsAmount) {
|
||||
savingsLine += `${savings.toLocaleString()} tokens`;
|
||||
} else {
|
||||
savingsLine += `${savingsPercent}% reduction from reuse`;
|
||||
}
|
||||
output.push(`${colors.green}${savingsLine}${colors.reset}`);
|
||||
}
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`📊 **Context Economics**:`);
|
||||
output.push(`- Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)`);
|
||||
output.push(`- Work investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions`);
|
||||
if (totalDiscoveryTokens > 0) {
|
||||
output.push(`- Your savings: ${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)`);
|
||||
if (config.showReadTokens) {
|
||||
output.push(`- Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)`);
|
||||
}
|
||||
if (config.showWorkTokens) {
|
||||
output.push(`- Work investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions`);
|
||||
}
|
||||
if (totalDiscoveryTokens > 0 && (config.showSavingsAmount || config.showSavingsPercent)) {
|
||||
let savingsLine = '- Your savings: ';
|
||||
if (config.showSavingsAmount && config.showSavingsPercent) {
|
||||
savingsLine += `${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)`;
|
||||
} else if (config.showSavingsAmount) {
|
||||
savingsLine += `${savings.toLocaleString()} tokens`;
|
||||
} else {
|
||||
savingsLine += `${savingsPercent}% reduction from reuse`;
|
||||
}
|
||||
output.push(savingsLine);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
@@ -341,6 +448,13 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
};
|
||||
});
|
||||
|
||||
// Identify which observations should show full details (most recent N)
|
||||
const fullObservationIds = new Set(
|
||||
observations
|
||||
.slice(0, config.fullObservationCount)
|
||||
.map(obs => obs.id)
|
||||
);
|
||||
|
||||
type TimelineItem =
|
||||
| { type: 'observation'; data: Observation }
|
||||
| { type: 'summary'; data: SummaryTimelineItem };
|
||||
@@ -506,13 +620,55 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
const timeDisplay = showTime ? time : '';
|
||||
lastTime = time;
|
||||
|
||||
if (useColors) {
|
||||
const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length);
|
||||
const readPart = readTokens > 0 ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
|
||||
const discoveryPart = discoveryTokens > 0 ? `${colors.dim}(${workEmoji} ${discoveryTokens.toLocaleString()}t)${colors.reset}` : '';
|
||||
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${readPart} ${discoveryPart}`);
|
||||
// Check if this observation should show full details
|
||||
const shouldShowFull = fullObservationIds.has(obs.id);
|
||||
|
||||
if (shouldShowFull) {
|
||||
// Render with full details (narrative or facts)
|
||||
const detailField = config.fullObservationField === 'narrative'
|
||||
? obs.narrative
|
||||
: (obs.facts ? parseJsonArray(obs.facts).join('\n') : null);
|
||||
|
||||
if (useColors) {
|
||||
const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length);
|
||||
const readPart = readTokens > 0 ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
|
||||
const discoveryPart = discoveryTokens > 0 ? `${colors.dim}(${workEmoji} ${discoveryTokens.toLocaleString()}t)${colors.reset}` : '';
|
||||
|
||||
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${colors.bright}${title}${colors.reset}`);
|
||||
if (detailField) {
|
||||
output.push(` ${colors.dim}${detailField}${colors.reset}`);
|
||||
}
|
||||
output.push(` ${readPart} ${discoveryPart}`);
|
||||
output.push('');
|
||||
} else {
|
||||
// Close table for full observation
|
||||
if (tableOpen) {
|
||||
output.push('');
|
||||
tableOpen = false;
|
||||
}
|
||||
|
||||
output.push(`**#${obs.id}** ${timeDisplay || '″'} ${icon} **${title}**`);
|
||||
if (detailField) {
|
||||
output.push('');
|
||||
output.push(detailField);
|
||||
output.push('');
|
||||
}
|
||||
output.push(`Read: ~${readTokens}, Work: ${discoveryDisplay}`);
|
||||
output.push('');
|
||||
|
||||
// Reopen table for next items if in same file
|
||||
currentFile = null;
|
||||
}
|
||||
} else {
|
||||
output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${readTokens} | ${discoveryDisplay} |`);
|
||||
// Compact index rendering (existing code)
|
||||
if (useColors) {
|
||||
const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length);
|
||||
const readPart = readTokens > 0 ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
|
||||
const discoveryPart = discoveryTokens > 0 ? `${colors.dim}(${workEmoji} ${discoveryTokens.toLocaleString()}t)${colors.reset}` : '';
|
||||
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${readPart} ${discoveryPart}`);
|
||||
} else {
|
||||
output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${readTokens} | ${discoveryDisplay} |`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -528,7 +684,8 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
const mostRecentSummary = recentSummaries[0];
|
||||
const mostRecentObservation = observations[0]; // observations are DESC by created_at_epoch
|
||||
|
||||
const shouldShowSummary = mostRecentSummary &&
|
||||
const shouldShowSummary = config.showLastSummary &&
|
||||
mostRecentSummary &&
|
||||
(mostRecentSummary.investigated || mostRecentSummary.learned || mostRecentSummary.completed || mostRecentSummary.next_steps) &&
|
||||
(!mostRecentObservation || mostRecentSummary.created_at_epoch > mostRecentObservation.created_at_epoch);
|
||||
|
||||
@@ -539,6 +696,27 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
output.push(...renderSummaryField('Next Steps', mostRecentSummary.next_steps, colors.magenta, useColors));
|
||||
}
|
||||
|
||||
// Show last message from previous session if enabled
|
||||
// Note: last_assistant_message field would need to be added to session_summaries table
|
||||
// For now, this is a placeholder for the feature
|
||||
if (config.showLastMessage && mostRecentSummary) {
|
||||
// This would require the last_assistant_message field to be populated
|
||||
// The field exists but may not be populated yet in the current implementation
|
||||
const lastMessage = (mostRecentSummary as any).last_assistant_message;
|
||||
if (lastMessage) {
|
||||
output.push('');
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.magenta}💬 Last Message from Previous Session${colors.reset}`);
|
||||
output.push(`${colors.dim}${lastMessage}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**💬 Last Message from Previous Session**`);
|
||||
output.push('');
|
||||
output.push(lastMessage);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// Footer with token savings message
|
||||
if (totalDiscoveryTokens > 0 && savings > 0) {
|
||||
const workTokensK = Math.round(totalDiscoveryTokens / 1000);
|
||||
|
||||
+138
-18
@@ -812,6 +812,74 @@ export class WorkerService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate context settings from request body
|
||||
*/
|
||||
private validateContextSettings(settings: any): { valid: boolean; error?: string } {
|
||||
// Validate boolean string values
|
||||
const booleanSettings = [
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE',
|
||||
];
|
||||
|
||||
for (const key of booleanSettings) {
|
||||
if (settings[key] && !['true', 'false'].includes(settings[key])) {
|
||||
return { valid: false, error: `${key} must be "true" or "false"` };
|
||||
}
|
||||
}
|
||||
|
||||
// Validate FULL_COUNT (0-20)
|
||||
if (settings.CLAUDE_MEM_CONTEXT_FULL_COUNT) {
|
||||
const count = parseInt(settings.CLAUDE_MEM_CONTEXT_FULL_COUNT, 10);
|
||||
if (isNaN(count) || count < 0 || count > 20) {
|
||||
return { valid: false, error: 'CLAUDE_MEM_CONTEXT_FULL_COUNT must be between 0 and 20' };
|
||||
}
|
||||
}
|
||||
|
||||
// Validate SESSION_COUNT (1-50)
|
||||
if (settings.CLAUDE_MEM_CONTEXT_SESSION_COUNT) {
|
||||
const count = parseInt(settings.CLAUDE_MEM_CONTEXT_SESSION_COUNT, 10);
|
||||
if (isNaN(count) || count < 1 || count > 50) {
|
||||
return { valid: false, error: 'CLAUDE_MEM_CONTEXT_SESSION_COUNT must be between 1 and 50' };
|
||||
}
|
||||
}
|
||||
|
||||
// Validate FULL_FIELD
|
||||
if (settings.CLAUDE_MEM_CONTEXT_FULL_FIELD) {
|
||||
if (!['narrative', 'facts'].includes(settings.CLAUDE_MEM_CONTEXT_FULL_FIELD)) {
|
||||
return { valid: false, error: 'CLAUDE_MEM_CONTEXT_FULL_FIELD must be "narrative" or "facts"' };
|
||||
}
|
||||
}
|
||||
|
||||
// Validate observation types
|
||||
const validTypes = ['bugfix', 'feature', 'refactor', 'discovery', 'decision', 'change'];
|
||||
if (settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES) {
|
||||
const types = settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES.split(',').map((t: string) => t.trim());
|
||||
for (const type of types) {
|
||||
if (type && !validTypes.includes(type)) {
|
||||
return { valid: false, error: `Invalid observation type: ${type}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate observation concepts
|
||||
const validConcepts = ['how-it-works', 'why-it-exists', 'what-changed', 'problem-solution', 'gotcha', 'pattern', 'trade-off'];
|
||||
if (settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS) {
|
||||
const concepts = settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS.split(',').map((c: string) => c.trim());
|
||||
for (const concept of concepts) {
|
||||
if (concept && !validConcepts.includes(concept)) {
|
||||
return { valid: false, error: `Invalid observation concept: ${concept}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment settings (from ~/.claude/settings.json)
|
||||
*/
|
||||
@@ -824,7 +892,22 @@ export class WorkerService {
|
||||
res.json({
|
||||
CLAUDE_MEM_MODEL: 'claude-haiku-4-5',
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50',
|
||||
CLAUDE_MEM_WORKER_PORT: '37777'
|
||||
CLAUDE_MEM_WORKER_PORT: '37777',
|
||||
// Token Economics
|
||||
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: 'true',
|
||||
// Observation Filtering
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: 'bugfix,feature,refactor,discovery,decision,change',
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: 'how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off',
|
||||
// Display Configuration
|
||||
CLAUDE_MEM_CONTEXT_FULL_COUNT: '5',
|
||||
CLAUDE_MEM_CONTEXT_FULL_FIELD: 'narrative',
|
||||
CLAUDE_MEM_CONTEXT_SESSION_COUNT: '10',
|
||||
// Feature Toggles
|
||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -836,7 +919,22 @@ export class WorkerService {
|
||||
res.json({
|
||||
CLAUDE_MEM_MODEL: env.CLAUDE_MEM_MODEL || 'claude-haiku-4-5',
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50',
|
||||
CLAUDE_MEM_WORKER_PORT: env.CLAUDE_MEM_WORKER_PORT || '37777'
|
||||
CLAUDE_MEM_WORKER_PORT: env.CLAUDE_MEM_WORKER_PORT || '37777',
|
||||
// Token Economics
|
||||
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: env.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS || 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: env.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS || 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: env.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT || 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: env.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT || 'true',
|
||||
// Observation Filtering
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: env.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES || 'bugfix,feature,refactor,discovery,decision,change',
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: env.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || 'how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off',
|
||||
// Display Configuration
|
||||
CLAUDE_MEM_CONTEXT_FULL_COUNT: env.CLAUDE_MEM_CONTEXT_FULL_COUNT || '5',
|
||||
CLAUDE_MEM_CONTEXT_FULL_FIELD: env.CLAUDE_MEM_CONTEXT_FULL_FIELD || 'narrative',
|
||||
CLAUDE_MEM_CONTEXT_SESSION_COUNT: env.CLAUDE_MEM_CONTEXT_SESSION_COUNT || '10',
|
||||
// Feature Toggles
|
||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: env.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: env.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || 'false',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Get settings failed', {}, error as Error);
|
||||
@@ -849,11 +947,9 @@ export class WorkerService {
|
||||
*/
|
||||
private handleUpdateSettings(req: Request, res: Response): void {
|
||||
try {
|
||||
const { CLAUDE_MEM_MODEL, CLAUDE_MEM_CONTEXT_OBSERVATIONS, CLAUDE_MEM_WORKER_PORT } = req.body;
|
||||
|
||||
// Validate inputs
|
||||
if (CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
||||
const obsCount = parseInt(CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
||||
// Validate CLAUDE_MEM_CONTEXT_OBSERVATIONS
|
||||
if (req.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
||||
const obsCount = parseInt(req.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
||||
if (isNaN(obsCount) || obsCount < 1 || obsCount > 200) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
@@ -863,8 +959,9 @@ export class WorkerService {
|
||||
}
|
||||
}
|
||||
|
||||
if (CLAUDE_MEM_WORKER_PORT) {
|
||||
const port = parseInt(CLAUDE_MEM_WORKER_PORT, 10);
|
||||
// Validate CLAUDE_MEM_WORKER_PORT
|
||||
if (req.body.CLAUDE_MEM_WORKER_PORT) {
|
||||
const port = parseInt(req.body.CLAUDE_MEM_WORKER_PORT, 10);
|
||||
if (isNaN(port) || port < 1024 || port > 65535) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
@@ -874,6 +971,16 @@ export class WorkerService {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate context settings
|
||||
const validation = this.validateContextSettings(req.body);
|
||||
if (!validation.valid) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: validation.error
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Read existing settings
|
||||
const settingsPath = path.join(homedir(), '.claude', 'settings.json');
|
||||
let settings: any = { env: {} };
|
||||
@@ -886,15 +993,28 @@ export class WorkerService {
|
||||
}
|
||||
}
|
||||
|
||||
// Update settings
|
||||
if (CLAUDE_MEM_MODEL) {
|
||||
settings.env.CLAUDE_MEM_MODEL = CLAUDE_MEM_MODEL;
|
||||
}
|
||||
if (CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
||||
settings.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS = CLAUDE_MEM_CONTEXT_OBSERVATIONS;
|
||||
}
|
||||
if (CLAUDE_MEM_WORKER_PORT) {
|
||||
settings.env.CLAUDE_MEM_WORKER_PORT = CLAUDE_MEM_WORKER_PORT;
|
||||
// Update all settings from request body
|
||||
const settingKeys = [
|
||||
'CLAUDE_MEM_MODEL',
|
||||
'CLAUDE_MEM_CONTEXT_OBSERVATIONS',
|
||||
'CLAUDE_MEM_WORKER_PORT',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT',
|
||||
'CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES',
|
||||
'CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS',
|
||||
'CLAUDE_MEM_CONTEXT_FULL_COUNT',
|
||||
'CLAUDE_MEM_CONTEXT_FULL_FIELD',
|
||||
'CLAUDE_MEM_CONTEXT_SESSION_COUNT',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY',
|
||||
'CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE',
|
||||
];
|
||||
|
||||
for (const key of settingKeys) {
|
||||
if (req.body[key] !== undefined) {
|
||||
settings.env[key] = req.body[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Write back
|
||||
|
||||
@@ -24,6 +24,19 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
|
||||
const [contextObs, setContextObs] = useState(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS);
|
||||
const [workerPort, setWorkerPort] = useState(settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT);
|
||||
|
||||
// Context settings state
|
||||
const [showReadTokens, setShowReadTokens] = useState(settings.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS);
|
||||
const [showWorkTokens, setShowWorkTokens] = useState(settings.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS);
|
||||
const [showSavingsAmount, setShowSavingsAmount] = useState(settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT);
|
||||
const [showSavingsPercent, setShowSavingsPercent] = useState(settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT);
|
||||
const [obsTypes, setObsTypes] = useState(settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES);
|
||||
const [obsConcepts, setObsConcepts] = useState(settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS);
|
||||
const [fullCount, setFullCount] = useState(settings.CLAUDE_MEM_CONTEXT_FULL_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_COUNT);
|
||||
const [fullField, setFullField] = useState(settings.CLAUDE_MEM_CONTEXT_FULL_FIELD || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_FIELD);
|
||||
const [sessionCount, setSessionCount] = useState(settings.CLAUDE_MEM_CONTEXT_SESSION_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SESSION_COUNT);
|
||||
const [showLastSummary, setShowLastSummary] = useState(settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY);
|
||||
const [showLastMessage, setShowLastMessage] = useState(settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE);
|
||||
|
||||
// MCP toggle state (separate from settings)
|
||||
const [mcpEnabled, setMcpEnabled] = useState(true);
|
||||
const [mcpToggling, setMcpToggling] = useState(false);
|
||||
@@ -34,6 +47,17 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
|
||||
setModel(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
|
||||
setContextObs(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS);
|
||||
setWorkerPort(settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT);
|
||||
setShowReadTokens(settings.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS);
|
||||
setShowWorkTokens(settings.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS);
|
||||
setShowSavingsAmount(settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT);
|
||||
setShowSavingsPercent(settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT);
|
||||
setObsTypes(settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES);
|
||||
setObsConcepts(settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS);
|
||||
setFullCount(settings.CLAUDE_MEM_CONTEXT_FULL_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_COUNT);
|
||||
setFullField(settings.CLAUDE_MEM_CONTEXT_FULL_FIELD || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_FIELD);
|
||||
setSessionCount(settings.CLAUDE_MEM_CONTEXT_SESSION_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SESSION_COUNT);
|
||||
setShowLastSummary(settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY);
|
||||
setShowLastMessage(settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE);
|
||||
}, [settings]);
|
||||
|
||||
// Fetch MCP status on mount
|
||||
@@ -55,7 +79,18 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
|
||||
onSave({
|
||||
CLAUDE_MEM_MODEL: model,
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: contextObs,
|
||||
CLAUDE_MEM_WORKER_PORT: workerPort
|
||||
CLAUDE_MEM_WORKER_PORT: workerPort,
|
||||
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: showReadTokens,
|
||||
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: showWorkTokens,
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: showSavingsAmount,
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: showSavingsPercent,
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: obsTypes,
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: obsConcepts,
|
||||
CLAUDE_MEM_CONTEXT_FULL_COUNT: fullCount,
|
||||
CLAUDE_MEM_CONTEXT_FULL_FIELD: fullField,
|
||||
CLAUDE_MEM_CONTEXT_SESSION_COUNT: sessionCount,
|
||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: showLastSummary,
|
||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: showLastMessage,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -229,6 +264,85 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
|
||||
onChange={e => setWorkerPort(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Token Economics Display */}
|
||||
<div className="form-group">
|
||||
<label>Token Economics Display</label>
|
||||
<div className="setting-description">
|
||||
Choose which token metrics to show in session start context.
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={showReadTokens === 'true'} onChange={e => setShowReadTokens(e.target.checked ? 'true' : 'false')} />
|
||||
Show read tokens
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={showWorkTokens === 'true'} onChange={e => setShowWorkTokens(e.target.checked ? 'true' : 'false')} />
|
||||
Show work tokens
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={showSavingsAmount === 'true'} onChange={e => setShowSavingsAmount(e.target.checked ? 'true' : 'false')} />
|
||||
Show savings amount
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={showSavingsPercent === 'true'} onChange={e => setShowSavingsPercent(e.target.checked ? 'true' : 'false')} />
|
||||
Show savings percentage
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Display Configuration */}
|
||||
<div className="form-group">
|
||||
<label>Display Configuration</label>
|
||||
<div className="setting-description">
|
||||
Control how observations are displayed in the timeline.
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '8px' }}>
|
||||
<div>
|
||||
<label htmlFor="fullCount" style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>
|
||||
Full observation count (0-20)
|
||||
</label>
|
||||
<input type="number" id="fullCount" min="0" max="20" value={fullCount} onChange={e => setFullCount(e.target.value)} style={{ width: '100%' }} />
|
||||
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
||||
Number of most recent observations to show with full details
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="fullField" style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>
|
||||
Full observation field
|
||||
</label>
|
||||
<select id="fullField" value={fullField} onChange={e => setFullField(e.target.value)} style={{ width: '100%' }}>
|
||||
<option value="narrative">Narrative</option>
|
||||
<option value="facts">Facts</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="sessionCount" style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>
|
||||
Session summary count (1-50)
|
||||
</label>
|
||||
<input type="number" id="sessionCount" min="1" max="50" value={sessionCount} onChange={e => setSessionCount(e.target.value)} style={{ width: '100%' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Toggles */}
|
||||
<div className="form-group">
|
||||
<label>Context Features</label>
|
||||
<div className="setting-description">
|
||||
Toggle additional features in session start context.
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={showLastSummary === 'true'} onChange={e => setShowLastSummary(e.target.checked ? 'true' : 'false')} />
|
||||
Show last session summary
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={showLastMessage === 'true'} onChange={e => setShowLastMessage(e.target.checked ? 'true' : 'false')} />
|
||||
Include last session message
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{saveStatus && (
|
||||
<div className="save-status">{saveStatus}</div>
|
||||
)}
|
||||
|
||||
@@ -6,4 +6,23 @@ export const DEFAULT_SETTINGS = {
|
||||
CLAUDE_MEM_MODEL: 'claude-haiku-4-5',
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50',
|
||||
CLAUDE_MEM_WORKER_PORT: '37777',
|
||||
|
||||
// Token Economics (all true for backwards compatibility)
|
||||
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: 'true',
|
||||
|
||||
// Observation Filtering (all types and concepts)
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: 'bugfix,feature,refactor,discovery,decision,change',
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: 'how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off',
|
||||
|
||||
// Display Configuration
|
||||
CLAUDE_MEM_CONTEXT_FULL_COUNT: '5',
|
||||
CLAUDE_MEM_CONTEXT_FULL_FIELD: 'narrative',
|
||||
CLAUDE_MEM_CONTEXT_SESSION_COUNT: '10',
|
||||
|
||||
// Feature Toggles
|
||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: 'true',
|
||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
|
||||
} as const;
|
||||
|
||||
@@ -58,6 +58,25 @@ export interface Settings {
|
||||
CLAUDE_MEM_MODEL: string;
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: string;
|
||||
CLAUDE_MEM_WORKER_PORT: string;
|
||||
|
||||
// Token Economics Display
|
||||
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS?: string;
|
||||
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS?: string;
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT?: string;
|
||||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT?: string;
|
||||
|
||||
// Observation Filtering
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES?: string;
|
||||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS?: string;
|
||||
|
||||
// Display Configuration
|
||||
CLAUDE_MEM_CONTEXT_FULL_COUNT?: string;
|
||||
CLAUDE_MEM_CONTEXT_FULL_FIELD?: string;
|
||||
CLAUDE_MEM_CONTEXT_SESSION_COUNT?: string;
|
||||
|
||||
// Feature Toggles
|
||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY?: string;
|
||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE?: string;
|
||||
}
|
||||
|
||||
export interface WorkerStats {
|
||||
|
||||
Reference in New Issue
Block a user