Compare commits

...

8 Commits

Author SHA1 Message Date
Alex Newman bc7e0ba3e0 chore: bump version to 6.4.9
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 22:06:50 -05:00
Alex Newman cbfc94bc26 Merge pull request #160 from thedotmack/feature/context-settings
feat: Add comprehensive context configuration and display settings
2025-12-01 22:03:30 -05:00
Alex Newman c768a80bf0 Refactor context configuration and settings handling
- Updated context configuration loading path from ~/.claude/settings.json to ~/.claude-mem/settings.json.
- Modified the extractPriorMessages function to focus on retrieving the last assistant message only, removing user message extraction.
- Enhanced output formatting for displaying prior assistant messages in the context hook.
- Added new settings related to token economics and observation filtering in the useSettings hook.
2025-12-01 19:26:33 -05:00
Alex Newman 6dc648f07c feat: add functionality to extract and display prior session messages
- Implemented `cwdToDashed` helper function to format current working directory for transcript file paths.
- Added `extractPriorMessages` function to read and parse the last user and assistant messages from a transcript file.
- Enhanced `contextHook` to retrieve and display prior session messages if enabled in the configuration.
- Updated output formatting to include a "Previously" section showing last messages from the prior session.
2025-12-01 18:10:55 -05:00
Alex Newman b116681529 refactor: improve context economics display logic and logging
- Updated logging category from 'CONTEXT' to 'HOOK' for context settings loading failure.
- Simplified the display logic for the Context Economics section to show only when relevant settings are enabled.
- Enhanced readability by consolidating repeated code for displaying token savings.
- Adjusted footer logic to conditionally display token savings message based on visibility of context economics.
2025-12-01 17:43:04 -05:00
Alex Newman d1876cb6e0 Refactor observation handling: centralize constants and improve context settings
- Introduced `observation-metadata.ts` to define valid observation types and concepts, along with their corresponding emoji mappings.
- Updated `context-hook.ts` to utilize new constants for observation types and concepts, enhancing maintainability.
- Refactored `worker-service.ts` to validate observation types and concepts against the new centralized constants.
- Consolidated settings management in `Sidebar.tsx` to streamline state handling for context settings.
- Improved error handling and logging for context loading failures.
2025-12-01 17:29:48 -05:00
Alex Newman e1017b483b 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.
2025-12-01 16:53:35 -05:00
Alex Newman 8d5b886f63 docs: Update CHANGELOG.md for v6.4.1
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 23:30:15 -05:00
15 changed files with 886 additions and 220 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [ "plugins": [
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "6.4.1", "version": "6.4.9",
"source": "./plugin", "source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions" "description": "Persistent memory system for Claude Code - context compression across sessions"
} }
+53
View File
@@ -4,6 +4,59 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [6.4.1] - 2025-12-01
## Hey there, claude-mem community! 👋
We're doing something new and exciting: **our first-ever Live AMA**!
### 🔴 When You'll See Us Live
**December 1st-5th, 2025**
**Daily from 5-7pm EST**
During these times, you'll see a live indicator (🔴) when you start a new session, letting you know we're available right now to answer questions, discuss ideas, or just chat about what you're building with claude-mem.
### What Changed in This Release
We've added a smart announcement system that:
- Shows upcoming AMA schedule before/after live hours
- Displays a live indicator (🔴) when we're actively available
- Automatically cleans up after the event ends
### Why We're Doing This
We want to hear from **you**! Whether you're:
- Just getting started with claude-mem
- A power user with feature ideas
- Curious about how memory compression works
- Running into any issues
- Or just want to say hi 👋
This is your chance to connect directly with the developer (@thedotmack) and fellow community members.
### Join the Community
Can't make the live times? No worries! Join our Discord to stay connected:
**https://discord.gg/J4wttp9vDu**
We're excited to meet you and hear what you're building!
---
## Technical Details
**Changed Files:**
- `src/hooks/user-message-hook.ts` - Added time-aware announcement logic
- Version bumped across all manifests (6.4.0 → 6.4.1)
**Built Artifacts:**
- `plugin/scripts/user-message-hook.js` - Updated compiled hook
---
Looking forward to seeing you at the AMA! 🎉
## [6.4.0] - 2025-12-01 ## [6.4.0] - 2025-12-01
## 🎯 Highlights ## 🎯 Highlights
+1 -1
View File
@@ -6,7 +6,7 @@
Claude-mem is a Claude Code plugin providing persistent memory across sessions. It captures tool usage, compresses observations using the Claude Agent SDK, and injects relevant context into future sessions. Claude-mem is a Claude Code plugin providing persistent memory across sessions. It captures tool usage, compresses observations using the Claude Agent SDK, and injects relevant context into future sessions.
**Current Version**: 6.4.1 **Current Version**: 6.4.9
## Architecture ## Architecture
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "6.4.1", "version": "6.4.9",
"description": "Memory compression system for Claude Code - persist context across sessions", "description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [ "keywords": [
"claude", "claude",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "claude-mem", "name": "claude-mem",
"version": "6.4.1", "version": "6.4.9",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions", "description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": { "author": {
"name": "Alex Newman" "name": "Alex Newman"
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
+68
View File
@@ -0,0 +1,68 @@
/**
* Observation metadata constants
* Shared across hooks, worker service, and UI components
*/
/**
* Valid observation types
*/
export const OBSERVATION_TYPES = [
'bugfix',
'feature',
'refactor',
'discovery',
'decision',
'change'
] as const;
export type ObservationType = typeof OBSERVATION_TYPES[number];
/**
* Valid observation concepts
*/
export const OBSERVATION_CONCEPTS = [
'how-it-works',
'why-it-exists',
'what-changed',
'problem-solution',
'gotcha',
'pattern',
'trade-off'
] as const;
export type ObservationConcept = typeof OBSERVATION_CONCEPTS[number];
/**
* Map observation types to emoji icons
*/
export const TYPE_ICON_MAP: Record<ObservationType | 'session-request', string> = {
'bugfix': '🔴',
'feature': '🟣',
'refactor': '🔄',
'change': '✅',
'discovery': '🔵',
'decision': '⚖️',
'session-request': '🎯'
};
/**
* Map observation types to work emoji (for token display)
*/
export const TYPE_WORK_EMOJI_MAP: Record<ObservationType, string> = {
'discovery': '🔍', // research/exploration
'change': '🛠️', // building/modifying
'feature': '🛠️', // building/modifying
'bugfix': '🛠️', // building/modifying
'refactor': '🛠️', // building/modifying
'decision': '⚖️' // decision-making
};
/**
* Default observation types (comma-separated string for settings)
*/
export const DEFAULT_OBSERVATION_TYPES_STRING = OBSERVATION_TYPES.join(',');
/**
* Default observation concepts (comma-separated string for settings)
*/
export const DEFAULT_OBSERVATION_CONCEPTS_STRING = OBSERVATION_CONCEPTS.join(',');
+347 -96
View File
@@ -10,6 +10,15 @@ import { stdin } from 'process';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname } from 'path'; import { dirname } from 'path';
import { SessionStore } from '../services/sqlite/SessionStore.js'; import { SessionStore } from '../services/sqlite/SessionStore.js';
import {
OBSERVATION_TYPES,
OBSERVATION_CONCEPTS,
TYPE_ICON_MAP,
TYPE_WORK_EMOJI_MAP,
DEFAULT_OBSERVATION_TYPES_STRING,
DEFAULT_OBSERVATION_CONCEPTS_STRING
} from '../constants/observation-metadata.js';
import { logger } from '../utils/logger.js';
// Get __dirname equivalent in ESM // Get __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -19,31 +28,82 @@ const __dirname = dirname(__filename);
// From src/hooks/ we need to go up to plugin root: ../../ // From src/hooks/ we need to go up to plugin root: ../../
const VERSION_MARKER_PATH = path.join(__dirname, '../../.install-version'); const VERSION_MARKER_PATH = path.join(__dirname, '../../.install-version');
/** interface ContextConfig {
* Get context depth from settings // Display counts
* Priority: ~/.claude/settings.json > env var > default totalObservationCount: number;
*/ fullObservationCount: number;
function getContextDepth(): number { sessionCount: number;
try {
const settingsPath = path.join(homedir(), '.claude', 'settings.json'); // Token display toggles
if (existsSync(settingsPath)) { showReadTokens: boolean;
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); showWorkTokens: boolean;
if (settings.env?.CLAUDE_MEM_CONTEXT_OBSERVATIONS) { showSavingsAmount: boolean;
const count = parseInt(settings.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10); showSavingsPercent: boolean;
if (!isNaN(count) && count > 0) {
return count; // Filters
} observationTypes: Set<string>;
} observationConcepts: Set<string>;
}
} catch { // Display options
// Fall through to env var or default fullObservationField: 'narrative' | 'facts';
} showLastSummary: boolean;
return parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50', 10); showLastMessage: boolean;
} }
// Configuration: Read from settings.json or environment /**
const DISPLAY_OBSERVATION_COUNT = getContextDepth(); * Load all context configuration settings
const DISPLAY_SESSION_COUNT = 10; // Recent sessions for timeline context * Priority: ~/.claude-mem/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(OBSERVATION_TYPES),
observationConcepts: new Set(OBSERVATION_CONCEPTS),
fullObservationField: 'narrative' as const,
showLastSummary: true,
showLastMessage: false,
};
try {
const settingsPath = path.join(homedir(), '.claude-mem', '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 || DEFAULT_OBSERVATION_TYPES_STRING)
.split(',').map((t: string) => t.trim()).filter(Boolean)
),
observationConcepts: new Set(
(env.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || DEFAULT_OBSERVATION_CONCEPTS_STRING)
.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 (error) {
logger.warn('HOOK', 'Failed to load context settings, using defaults', {}, error as Error);
return defaults;
}
}
// Configuration constants
const CHARS_PER_TOKEN_ESTIMATE = 4; // Rough estimate for token counting const CHARS_PER_TOKEN_ESTIMATE = 4; // Rough estimate for token counting
const SUMMARY_LOOKAHEAD = 1; // Fetch one extra summary for offset calculation const SUMMARY_LOOKAHEAD = 1; // Fetch one extra summary for offset calculation
@@ -159,10 +219,73 @@ function renderSummaryField(label: string, value: string | null, color: string,
return [`**${label}**: ${value}`, '']; return [`**${label}**: ${value}`, ''];
} }
// Helper: Convert cwd path to dashed format for transcript directory name
function cwdToDashed(cwd: string): string {
// Convert all slashes to dashes (including leading slash)
return cwd.replace(/\//g, '-');
}
// Helper: Extract last assistant message from transcript file
function extractPriorMessages(transcriptPath: string): { userMessage: string; assistantMessage: string } {
try {
if (!existsSync(transcriptPath)) {
return { userMessage: '', assistantMessage: '' };
}
const content = readFileSync(transcriptPath, 'utf-8').trim();
if (!content) {
return { userMessage: '', assistantMessage: '' };
}
const lines = content.split('\n').filter(line => line.trim());
// Find the last assistant message by filtering for assistant type and taking the last one
let lastAssistantMessage = '';
// Iterate backwards to find the most recent assistant message with text content
for (let i = lines.length - 1; i >= 0; i--) {
try {
const line = lines[i];
// Quick check if this line is an assistant message
if (!line.includes('"type":"assistant"')) {
continue;
}
const entry = JSON.parse(line);
if (entry.type === 'assistant' && entry.message?.content && Array.isArray(entry.message.content)) {
let text = '';
for (const block of entry.message.content) {
if (block.type === 'text') {
text += block.text;
}
}
// Remove system-reminder tags
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
if (text) {
lastAssistantMessage = text;
break; // Found it, stop searching
}
}
} catch (parseError) {
// Skip malformed lines
continue;
}
}
return { userMessage: '', assistantMessage: lastAssistantMessage };
} catch (error) {
logger.failure('HOOK', `Failed to extract prior messages from transcript`, { transcriptPath }, error as Error);
return { userMessage: '', assistantMessage: '' };
}
}
/** /**
* Context Hook Main Logic * Context Hook Main Logic
*/ */
async function contextHook(input?: SessionStartInput, useColors: boolean = false): Promise<string> { async function contextHook(input?: SessionStartInput, useColors: boolean = false): Promise<string> {
const config = loadContextConfig();
const cwd = input?.cwd ?? process.cwd(); const cwd = input?.cwd ?? process.cwd();
const project = cwd ? path.basename(cwd) : 'unknown-project'; const project = cwd ? path.basename(cwd) : 'unknown-project';
@@ -188,19 +311,32 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
throw error; throw error;
} }
// Get ALL recent observations for this project (not filtered by summaries) // Build SQL WHERE clause for observation types
const typeArray = Array.from(config.observationTypes);
const typePlaceholders = typeArray.map(() => '?').join(',');
// Build SQL WHERE clause for concepts
const conceptArray = Array.from(config.observationConcepts);
const conceptPlaceholders = conceptArray.map(() => '?').join(',');
// Get recent observations filtered by type and concepts at SQL level
// This ensures we show observations even when summaries haven't been generated // 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(` const observations = db.db.prepare(`
SELECT SELECT
id, sdk_session_id, type, title, subtitle, narrative, id, sdk_session_id, type, title, subtitle, narrative,
facts, concepts, files_read, files_modified, discovery_tokens, facts, concepts, files_read, files_modified, discovery_tokens,
created_at, created_at_epoch created_at, created_at_epoch
FROM observations FROM observations
WHERE project = ? WHERE project = ?
AND type IN (${typePlaceholders})
AND EXISTS (
SELECT 1 FROM json_each(concepts)
WHERE value IN (${conceptPlaceholders})
)
ORDER BY created_at_epoch DESC ORDER BY created_at_epoch DESC
LIMIT ? LIMIT ?
`).all(project, DISPLAY_OBSERVATION_COUNT) as Observation[]; `).all(project, ...typeArray, ...conceptArray, config.totalObservationCount) as Observation[];
// Get recent summaries (optional - may not exist for recent sessions) // Get recent summaries (optional - may not exist for recent sessions)
// Fetch one extra for offset calculation // Fetch one extra for offset calculation
@@ -210,10 +346,53 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
WHERE project = ? WHERE project = ?
ORDER BY created_at_epoch DESC ORDER BY created_at_epoch DESC
LIMIT ? LIMIT ?
`).all(project, DISPLAY_SESSION_COUNT + SUMMARY_LOOKAHEAD) as SessionSummary[]; `).all(project, config.sessionCount + SUMMARY_LOOKAHEAD) as SessionSummary[];
// Retrieve prior session messages if enabled
let priorUserMessage = '';
let priorAssistantMessage = '';
// let debugInfo: string[] = [];
if (config.showLastMessage && observations.length > 0) {
try {
const currentSessionId = input?.session_id;
// Find the first observation from a different session (the prior session)
const priorSessionObs = observations.find(obs => obs.sdk_session_id !== currentSessionId);
if (priorSessionObs) {
const priorSessionId = priorSessionObs.sdk_session_id;
// Construct transcript path: ~/.claude/projects/{dashed-cwd}/{session_id}.jsonl
const dashedCwd = cwdToDashed(cwd);
const transcriptPath = path.join(homedir(), '.claude', 'projects', dashedCwd, `${priorSessionId}.jsonl`);
// debugInfo.push(`📋 Prior Message Retrieval:`);
// debugInfo.push(` Session ID: ${priorSessionId}`);
// debugInfo.push(` Transcript: ${transcriptPath}`);
// debugInfo.push(` Exists: ${existsSync(transcriptPath)}`);
// Extract messages from transcript
const messages = extractPriorMessages(transcriptPath);
priorUserMessage = messages.userMessage;
priorAssistantMessage = messages.assistantMessage;
// if (!priorUserMessage && !priorAssistantMessage) {
// debugInfo.push(` ⚠️ No messages extracted from transcript`);
// } else {
// debugInfo.push(` ✅ Found user message: ${!!priorUserMessage}`);
// debugInfo.push(` ✅ Found assistant message: ${!!priorAssistantMessage}`);
// }
} // else {
// debugInfo.push(`📋 Prior Message Retrieval: No prior session found (all observations from current session)`);
// }
} catch (error) {
// debugInfo.push(`📋 Prior Message Retrieval Error: ${(error as Error).message}`);
}
}
// If we have neither observations nor summaries, show empty state // If we have neither observations nor summaries, show empty state
if (allObservations.length === 0 && recentSummaries.length === 0) { if (observations.length === 0 && recentSummaries.length === 0) {
db.close(); db.close();
if (useColors) { if (useColors) {
return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${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 sessions found for this project yet.${colors.reset}\n`;
@@ -221,11 +400,9 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
return `# [${project}] recent context\n\nNo previous sessions found for this project yet.`; return `# [${project}] recent context\n\nNo previous sessions found for this project yet.`;
} }
// Use observations for display (summaries are supplementary) const displaySummaries = recentSummaries.slice(0, config.sessionCount);
const observations = allObservations;
const displaySummaries = recentSummaries.slice(0, DISPLAY_SESSION_COUNT);
// All observations are shown in timeline (filtered by type, not concepts) // All filtered observations are shown in timeline
const timelineObs = observations; const timelineObs = observations;
// Build output // Build output
@@ -298,23 +475,44 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
? Math.round((savings / totalDiscoveryTokens) * 100) ? Math.round((savings / totalDiscoveryTokens) * 100)
: 0; : 0;
// Display Context Economics section // Display Context Economics section only if at least one token setting is enabled
if (useColors) { const showContextEconomics = config.showReadTokens || config.showWorkTokens ||
output.push(`${colors.bright}${colors.cyan}📊 Context Economics${colors.reset}`); config.showSavingsAmount || config.showSavingsPercent;
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 (showContextEconomics) {
if (totalDiscoveryTokens > 0) { if (useColors) {
output.push(`${colors.green} Your savings: ${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)${colors.reset}`); 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 && (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 && (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('');
} }
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)`);
}
output.push('');
} }
// Prepare summaries for timeline display // Prepare summaries for timeline display
@@ -341,6 +539,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 TimelineItem =
| { type: 'observation'; data: Observation } | { type: 'observation'; data: Observation }
| { type: 'summary'; data: SummaryTimelineItem }; | { type: 'summary'; data: SummaryTimelineItem };
@@ -449,29 +654,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
const title = obs.title || 'Untitled'; const title = obs.title || 'Untitled';
// Map observation type to emoji icon // Map observation type to emoji icon
let icon = '•'; const icon = TYPE_ICON_MAP[obs.type as keyof typeof TYPE_ICON_MAP] || '•';
switch (obs.type) {
case 'bugfix':
icon = '🔴';
break;
case 'feature':
icon = '🟣';
break;
case 'refactor':
icon = '🔄';
break;
case 'change':
icon = '✅';
break;
case 'discovery':
icon = '🔵';
break;
case 'decision':
icon = '⚖️';
break;
default:
icon = '•';
}
// Section 2: Calculate read tokens (estimate from observation size) // Section 2: Calculate read tokens (estimate from observation size)
const obsSize = (obs.title?.length || 0) + const obsSize = (obs.title?.length || 0) +
@@ -484,21 +667,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
const discoveryTokens = obs.discovery_tokens || 0; const discoveryTokens = obs.discovery_tokens || 0;
// Map observation type to work emoji // Map observation type to work emoji
let workEmoji = '🔍'; // default to research/discovery const workEmoji = TYPE_WORK_EMOJI_MAP[obs.type as keyof typeof TYPE_WORK_EMOJI_MAP] || '🔍';
switch (obs.type) {
case 'discovery':
workEmoji = '🔍'; // research/exploration
break;
case 'change':
case 'feature':
case 'bugfix':
case 'refactor':
workEmoji = '🛠️'; // building/modifying
break;
case 'decision':
workEmoji = '⚖️'; // decision-making
break;
}
const discoveryDisplay = discoveryTokens > 0 ? `${workEmoji} ${discoveryTokens.toLocaleString()}` : '-'; const discoveryDisplay = discoveryTokens > 0 ? `${workEmoji} ${discoveryTokens.toLocaleString()}` : '-';
@@ -506,13 +675,68 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
const timeDisplay = showTime ? time : ''; const timeDisplay = showTime ? time : '';
lastTime = time; lastTime = time;
if (useColors) { // Check if this observation should show full details
const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length); const shouldShowFull = fullObservationIds.has(obs.id);
const readPart = readTokens > 0 ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
const discoveryPart = discoveryTokens > 0 ? `${colors.dim}(${workEmoji} ${discoveryTokens.toLocaleString()}t)${colors.reset}` : ''; if (shouldShowFull) {
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${readPart} ${discoveryPart}`); // 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 = (config.showReadTokens && readTokens > 0) ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
const discoveryPart = (config.showWorkTokens && 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}`);
}
if (readPart || discoveryPart) {
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('');
}
const tokenParts: string[] = [];
if (config.showReadTokens) {
tokenParts.push(`Read: ~${readTokens}`);
}
if (config.showWorkTokens) {
tokenParts.push(`Work: ${discoveryDisplay}`);
}
if (tokenParts.length > 0) {
output.push(tokenParts.join(', '));
}
output.push('');
// Reopen table for next items if in same file
currentFile = null;
}
} else { } 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 = (config.showReadTokens && readTokens > 0) ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
const discoveryPart = (config.showWorkTokens && discoveryTokens > 0) ? `${colors.dim}(${workEmoji} ${discoveryTokens.toLocaleString()}t)${colors.reset}` : '';
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${readPart} ${discoveryPart}`);
} else {
const readCol = config.showReadTokens ? `~${readTokens}` : '';
const workCol = config.showWorkTokens ? discoveryDisplay : '';
output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ${readCol} | ${workCol} |`);
}
} }
} }
} }
@@ -528,7 +752,8 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
const mostRecentSummary = recentSummaries[0]; const mostRecentSummary = recentSummaries[0];
const mostRecentObservation = observations[0]; // observations are DESC by created_at_epoch 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) && (mostRecentSummary.investigated || mostRecentSummary.learned || mostRecentSummary.completed || mostRecentSummary.next_steps) &&
(!mostRecentObservation || mostRecentSummary.created_at_epoch > mostRecentObservation.created_at_epoch); (!mostRecentObservation || mostRecentSummary.created_at_epoch > mostRecentObservation.created_at_epoch);
@@ -539,8 +764,25 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
output.push(...renderSummaryField('Next Steps', mostRecentSummary.next_steps, colors.magenta, useColors)); output.push(...renderSummaryField('Next Steps', mostRecentSummary.next_steps, colors.magenta, useColors));
} }
// Footer with token savings message // Previously section (last assistant message from prior session) - positioned at bottom for chronological sense
if (totalDiscoveryTokens > 0 && savings > 0) { if (priorAssistantMessage) {
output.push('');
output.push('---');
output.push('');
if (useColors) {
output.push(`${colors.bright}${colors.magenta}📋 Previously${colors.reset}`);
output.push('');
output.push(`${colors.dim}A: ${priorAssistantMessage}${colors.reset}`);
} else {
output.push(`**📋 Previously**`);
output.push('');
output.push(`A: ${priorAssistantMessage}`);
}
output.push('');
}
// Footer with token savings message (only show if token economics is visible)
if (showContextEconomics && totalDiscoveryTokens > 0 && savings > 0) {
const workTokensK = Math.round(totalDiscoveryTokens / 1000); const workTokensK = Math.round(totalDiscoveryTokens / 1000);
output.push(''); output.push('');
if (useColors) { if (useColors) {
@@ -552,6 +794,15 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
} }
db.close(); db.close();
// Add debug info directly to output
// if (debugInfo.length > 0) {
// output.push('');
// output.push('---');
// output.push('');
// output.push(...debugInfo);
// }
return output.join('\n').trimEnd(); return output.join('\n').trimEnd();
} }
+144 -20
View File
@@ -30,6 +30,12 @@ import { SDKAgent } from './worker/SDKAgent.js';
import { PaginationHelper } from './worker/PaginationHelper.js'; import { PaginationHelper } from './worker/PaginationHelper.js';
import { SettingsManager } from './worker/SettingsManager.js'; import { SettingsManager } from './worker/SettingsManager.js';
import { getBranchInfo, switchBranch, pullUpdates, type BranchInfo, type SwitchResult } from './worker/BranchManager.js'; import { getBranchInfo, switchBranch, pullUpdates, type BranchInfo, type SwitchResult } from './worker/BranchManager.js';
import {
OBSERVATION_TYPES,
OBSERVATION_CONCEPTS,
DEFAULT_OBSERVATION_TYPES_STRING,
DEFAULT_OBSERVATION_CONCEPTS_STRING
} from '../constants/observation-metadata.js';
export class WorkerService { export class WorkerService {
private app: express.Application; private app: express.Application;
@@ -812,19 +818,100 @@ 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
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 && !OBSERVATION_TYPES.includes(type as any)) {
return { valid: false, error: `Invalid observation type: ${type}. Valid types: ${OBSERVATION_TYPES.join(', ')}` };
}
}
}
// Validate observation concepts
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 && !OBSERVATION_CONCEPTS.includes(concept as any)) {
return { valid: false, error: `Invalid observation concept: ${concept}. Valid concepts: ${OBSERVATION_CONCEPTS.join(', ')}` };
}
}
}
return { valid: true };
}
/** /**
* Get environment settings (from ~/.claude/settings.json) * Get environment settings (from ~/.claude/settings.json)
*/ */
private handleGetSettings(req: Request, res: Response): void { private handleGetSettings(req: Request, res: Response): void {
try { try {
const settingsPath = path.join(homedir(), '.claude', 'settings.json'); const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
if (!existsSync(settingsPath)) { if (!existsSync(settingsPath)) {
// Return defaults if file doesn't exist // Return defaults if file doesn't exist
res.json({ res.json({
CLAUDE_MEM_MODEL: 'claude-haiku-4-5', CLAUDE_MEM_MODEL: 'claude-haiku-4-5',
CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50', 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: DEFAULT_OBSERVATION_TYPES_STRING,
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: DEFAULT_OBSERVATION_CONCEPTS_STRING,
// 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; return;
} }
@@ -836,7 +923,22 @@ export class WorkerService {
res.json({ res.json({
CLAUDE_MEM_MODEL: env.CLAUDE_MEM_MODEL || 'claude-haiku-4-5', CLAUDE_MEM_MODEL: env.CLAUDE_MEM_MODEL || 'claude-haiku-4-5',
CLAUDE_MEM_CONTEXT_OBSERVATIONS: env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50', 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 || DEFAULT_OBSERVATION_TYPES_STRING,
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: env.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || DEFAULT_OBSERVATION_CONCEPTS_STRING,
// 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) { } catch (error) {
logger.failure('WORKER', 'Get settings failed', {}, error as Error); logger.failure('WORKER', 'Get settings failed', {}, error as Error);
@@ -849,11 +951,9 @@ export class WorkerService {
*/ */
private handleUpdateSettings(req: Request, res: Response): void { private handleUpdateSettings(req: Request, res: Response): void {
try { try {
const { CLAUDE_MEM_MODEL, CLAUDE_MEM_CONTEXT_OBSERVATIONS, CLAUDE_MEM_WORKER_PORT } = req.body; // Validate CLAUDE_MEM_CONTEXT_OBSERVATIONS
if (req.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
// Validate inputs const obsCount = parseInt(req.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
if (CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
const obsCount = parseInt(CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
if (isNaN(obsCount) || obsCount < 1 || obsCount > 200) { if (isNaN(obsCount) || obsCount < 1 || obsCount > 200) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
@@ -863,8 +963,9 @@ export class WorkerService {
} }
} }
if (CLAUDE_MEM_WORKER_PORT) { // Validate CLAUDE_MEM_WORKER_PORT
const port = parseInt(CLAUDE_MEM_WORKER_PORT, 10); if (req.body.CLAUDE_MEM_WORKER_PORT) {
const port = parseInt(req.body.CLAUDE_MEM_WORKER_PORT, 10);
if (isNaN(port) || port < 1024 || port > 65535) { if (isNaN(port) || port < 1024 || port > 65535) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
@@ -874,8 +975,18 @@ 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 // Read existing settings
const settingsPath = path.join(homedir(), '.claude', 'settings.json'); const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
let settings: any = { env: {} }; let settings: any = { env: {} };
if (existsSync(settingsPath)) { if (existsSync(settingsPath)) {
@@ -886,15 +997,28 @@ export class WorkerService {
} }
} }
// Update settings // Update all settings from request body
if (CLAUDE_MEM_MODEL) { const settingKeys = [
settings.env.CLAUDE_MEM_MODEL = CLAUDE_MEM_MODEL; 'CLAUDE_MEM_MODEL',
} 'CLAUDE_MEM_CONTEXT_OBSERVATIONS',
if (CLAUDE_MEM_CONTEXT_OBSERVATIONS) { 'CLAUDE_MEM_WORKER_PORT',
settings.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS = CLAUDE_MEM_CONTEXT_OBSERVATIONS; 'CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS',
} 'CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS',
if (CLAUDE_MEM_WORKER_PORT) { 'CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT',
settings.env.CLAUDE_MEM_WORKER_PORT = CLAUDE_MEM_WORKER_PORT; '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 // Write back
+125 -19
View File
@@ -19,21 +19,52 @@ interface SidebarProps {
} }
export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConnected, projects, currentFilter, onFilterChange, onSave, onClose, onRefreshStats }: SidebarProps) { export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConnected, projects, currentFilter, onFilterChange, onSave, onClose, onRefreshStats }: SidebarProps) {
// Settings form state // Consolidated settings form state
const [model, setModel] = useState(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL); const [formState, setFormState] = useState<Settings>({
const [contextObs, setContextObs] = useState(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS); CLAUDE_MEM_MODEL: settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL,
const [workerPort, setWorkerPort] = useState(settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT); CLAUDE_MEM_CONTEXT_OBSERVATIONS: settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS,
CLAUDE_MEM_WORKER_PORT: settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT,
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: settings.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: settings.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT,
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES,
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS,
CLAUDE_MEM_CONTEXT_FULL_COUNT: settings.CLAUDE_MEM_CONTEXT_FULL_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_COUNT,
CLAUDE_MEM_CONTEXT_FULL_FIELD: settings.CLAUDE_MEM_CONTEXT_FULL_FIELD || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_FIELD,
CLAUDE_MEM_CONTEXT_SESSION_COUNT: settings.CLAUDE_MEM_CONTEXT_SESSION_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SESSION_COUNT,
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY,
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE,
});
// MCP toggle state (separate from settings) // MCP toggle state (separate from settings)
const [mcpEnabled, setMcpEnabled] = useState(true); const [mcpEnabled, setMcpEnabled] = useState(true);
const [mcpToggling, setMcpToggling] = useState(false); const [mcpToggling, setMcpToggling] = useState(false);
const [mcpStatus, setMcpStatus] = useState(''); const [mcpStatus, setMcpStatus] = useState('');
// Update settings form state when settings change // Helper to update form state
const updateFormState = (field: keyof Settings, value: string) => {
setFormState(prev => ({ ...prev, [field]: value }));
};
// Update settings form state when settings prop changes
useEffect(() => { useEffect(() => {
setModel(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL); setFormState({
setContextObs(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS); CLAUDE_MEM_MODEL: settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL,
setWorkerPort(settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT); CLAUDE_MEM_CONTEXT_OBSERVATIONS: settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS,
CLAUDE_MEM_WORKER_PORT: settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT,
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: settings.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: settings.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT,
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES,
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS,
CLAUDE_MEM_CONTEXT_FULL_COUNT: settings.CLAUDE_MEM_CONTEXT_FULL_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_COUNT,
CLAUDE_MEM_CONTEXT_FULL_FIELD: settings.CLAUDE_MEM_CONTEXT_FULL_FIELD || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_FIELD,
CLAUDE_MEM_CONTEXT_SESSION_COUNT: settings.CLAUDE_MEM_CONTEXT_SESSION_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SESSION_COUNT,
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY,
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE,
});
}, [settings]); }, [settings]);
// Fetch MCP status on mount // Fetch MCP status on mount
@@ -52,11 +83,7 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
}, [isOpen, onRefreshStats]); }, [isOpen, onRefreshStats]);
const handleSave = () => { const handleSave = () => {
onSave({ onSave(formState);
CLAUDE_MEM_MODEL: model,
CLAUDE_MEM_CONTEXT_OBSERVATIONS: contextObs,
CLAUDE_MEM_WORKER_PORT: workerPort
});
}; };
const handleMcpToggle = async (enabled: boolean) => { const handleMcpToggle = async (enabled: boolean) => {
@@ -193,8 +220,8 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
</div> </div>
<select <select
id="model" id="model"
value={model} value={formState.CLAUDE_MEM_MODEL}
onChange={e => setModel(e.target.value)} onChange={e => updateFormState('CLAUDE_MEM_MODEL', e.target.value)}
> >
<option value="claude-haiku-4-5">claude-haiku-4-5</option> <option value="claude-haiku-4-5">claude-haiku-4-5</option>
<option value="claude-sonnet-4-5">claude-sonnet-4-5</option> <option value="claude-sonnet-4-5">claude-sonnet-4-5</option>
@@ -211,8 +238,8 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
id="contextObs" id="contextObs"
min="1" min="1"
max="200" max="200"
value={contextObs} value={formState.CLAUDE_MEM_CONTEXT_OBSERVATIONS}
onChange={e => setContextObs(e.target.value)} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_OBSERVATIONS', e.target.value)}
/> />
</div> </div>
<div className="form-group"> <div className="form-group">
@@ -225,10 +252,89 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
id="workerPort" id="workerPort"
min="1024" min="1024"
max="65535" max="65535"
value={workerPort} value={formState.CLAUDE_MEM_WORKER_PORT}
onChange={e => setWorkerPort(e.target.value)} onChange={e => updateFormState('CLAUDE_MEM_WORKER_PORT', e.target.value)}
/> />
</div> </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={formState.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS', e.target.checked ? 'true' : 'false')} />
Show read tokens
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={formState.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS', e.target.checked ? 'true' : 'false')} />
Show work tokens
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={formState.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT', e.target.checked ? 'true' : 'false')} />
Show savings amount
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={formState.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT', 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={formState.CLAUDE_MEM_CONTEXT_FULL_COUNT} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_FULL_COUNT', 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={formState.CLAUDE_MEM_CONTEXT_FULL_FIELD} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_FULL_FIELD', 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={formState.CLAUDE_MEM_CONTEXT_SESSION_COUNT} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SESSION_COUNT', 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={formState.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY', e.target.checked ? 'true' : 'false')} />
Show last session summary
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={formState.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE', e.target.checked ? 'true' : 'false')} />
Include last session message
</label>
</div>
</div>
{saveStatus && ( {saveStatus && (
<div className="save-status">{saveStatus}</div> <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_MODEL: 'claude-haiku-4-5',
CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50', CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50',
CLAUDE_MEM_WORKER_PORT: '37777', 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; } as const;
+20 -1
View File
@@ -17,7 +17,26 @@ export function useSettings() {
setSettings({ setSettings({
CLAUDE_MEM_MODEL: data.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL, CLAUDE_MEM_MODEL: data.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL,
CLAUDE_MEM_CONTEXT_OBSERVATIONS: data.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS, CLAUDE_MEM_CONTEXT_OBSERVATIONS: data.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS,
CLAUDE_MEM_WORKER_PORT: data.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT CLAUDE_MEM_WORKER_PORT: data.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT,
// Token Economics Display
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: data.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: data.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: data.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: data.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT,
// Observation Filtering
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: data.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES,
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: data.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS,
// Display Configuration
CLAUDE_MEM_CONTEXT_FULL_COUNT: data.CLAUDE_MEM_CONTEXT_FULL_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_COUNT,
CLAUDE_MEM_CONTEXT_FULL_FIELD: data.CLAUDE_MEM_CONTEXT_FULL_FIELD || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_FIELD,
CLAUDE_MEM_CONTEXT_SESSION_COUNT: data.CLAUDE_MEM_CONTEXT_SESSION_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SESSION_COUNT,
// Feature Toggles
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: data.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY,
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: data.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE,
}); });
}) })
.catch(error => { .catch(error => {
+19
View File
@@ -58,6 +58,25 @@ export interface Settings {
CLAUDE_MEM_MODEL: string; CLAUDE_MEM_MODEL: string;
CLAUDE_MEM_CONTEXT_OBSERVATIONS: string; CLAUDE_MEM_CONTEXT_OBSERVATIONS: string;
CLAUDE_MEM_WORKER_PORT: 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 { export interface WorkerStats {