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:
Alex Newman
2025-12-01 16:53:35 -05:00
parent 8d5b886f63
commit e1017b483b
8 changed files with 576 additions and 125 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+200 -22
View File
@@ -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
View File
@@ -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
+115 -1
View File
@@ -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>
)}
+19
View File
@@ -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;
+19
View File
@@ -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 {