Compare commits

...

21 Commits

Author SHA1 Message Date
Alex Newman 0a3b50c875 chore: Upgrade better-sqlite3 to v12.5.0 for Node.js 25 compatibility
Fixes #164

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 15:23:24 -05:00
Copilot a8d31d465f Upgrade better-sqlite3 to ^12.5.0 for Node.js 25 compatibility (#165)
* Initial plan

* Initial plan for better-sqlite3 upgrade

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

* Upgrade better-sqlite3 to ^12.5.0 for Node.js 25 compatibility

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
2025-12-04 15:22:30 -05:00
Alex Newman 186f54b3fd docs: Update CHANGELOG.md from releases 2025-12-04 13:31:00 -05:00
Alex Newman be28c095e2 Release v6.5.1: Product Hunt Launch Day UI Updates
- Decorative Product Hunt announcement in terminal with rocket borders
- Product Hunt badge in viewer header with theme-aware switching
- Badge uses separate tracking URL for analytics

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 13:29:56 -05:00
Alex Newman 28305f73bb docs: Update CHANGELOG.md from releases
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 21:48:50 -05:00
Alex Newman c6708b3684 Release v6.5.0: Documentation Overhaul
Comprehensive documentation update with current features:
- Updated "What's New" section to highlight v6.4.x features
- Added privacy tags and context configuration to key features
- Fixed default model (claude-haiku-4-5)
- Clarified lifecycle hook count (5 events, 6 scripts)
- Removed outdated MCP server references
- Updated version numbers across all docs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 21:47:59 -05:00
Alex Newman 375dd1c3d6 feat: Add Context Settings Modal with Terminal Preview and UI Enhancements (#161)
* feat: Add Context Injection Settings modal with terminal preview

Adds a new settings modal accessible from the viewer UI header that allows users to configure context injection parameters with a live terminal preview showing how observations will appear.

Changes:
- New ContextSettingsModal component with auto-saving settings
- TerminalPreview component for live context visualization
- useContextPreview hook for fetching preview data
- Modal positioned to left of color mode button
- Settings sync with backend via worker service API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Add demo data and modify contextHook for cm_demo_content project

- Introduced DEMO_OBSERVATIONS and DEMO_SUMMARIES for the cm_demo_content project to provide mock data for testing and demonstration purposes.
- Updated contextHook to utilize demo data when the project is cm_demo_content, filtering observations based on configured types and concepts.
- Adjusted the worker service to use the contextHook with demo data, ensuring ANSI rendering for terminal output.
- Enhanced error handling and ensured proper closure of database connections.

* feat: add GitHub stars button with dynamic star count

- Implemented a new GitHubStarsButton component that fetches and displays the star count for a specified GitHub repository.
- Added useGitHubStars hook to handle API requests and state management for star count.
- Created formatStarCount utility function to format the star count into compact notation (k/M suffixes).
- Styled the GitHub stars button to match existing UI components, including hover and active states.
- Updated Header component to include the new GitHubStarsButton, replacing the static GitHub link.
- Added responsive styles to hide the GitHub stars button on mobile devices.

* feat: add API endpoint to fetch distinct projects and update context settings modal

- Implemented a new API endpoint `/api/projects` in `worker-service.ts` to retrieve a list of distinct projects from the observations.
- Modified `ContextSettingsModal.tsx` to replace the current project display with a dropdown for selecting projects, utilizing the fetched project list.
- Updated `useContextPreview.ts` to fetch projects on mount and manage the selected project state.
- Removed the `currentProject` prop from `ContextSettingsModal` and `App` components as it is now managed internally within the modal.

* Enhance Context Settings Modal and Terminal Preview

- Updated the styling of the Context Settings Modal for a modern clean design, including improved backdrop, header, and body layout.
- Introduced responsive design adjustments for smaller screens.
- Added custom scrollbar styles for better user experience.
- Refactored the TerminalPreview component to utilize `ansi-to-html` for rendering ANSI content, improving text display.
- Implemented new font variables for terminal styling across the application.
- Enhanced checkbox and input styles in the settings panel for better usability and aesthetics.
- Improved the layout and structure of settings groups and chips for a more organized appearance.

* Refactor UI components for compact design and enhance MCP toggle functionality

- Updated grid layout in viewer.html and viewer-template.html for better space utilization.
- Reduced padding and font sizes in settings groups, filter chips, and form controls for a more compact appearance.
- Implemented MCP toggle state management in ContextSettingsModal with API integration for status fetching and toggling.
- Reorganized settings groups for clarity, renaming and consolidating sections for improved user experience.
- Added feedback mechanism for MCP toggle status to inform users of changes and errors.

* feat: add collapsible sections, chip groups, form fields with tooltips, and toggle switches in settings modal

- Implemented collapsible sections for better organization of settings.
- Added chip groups with select all/none functionality for observation types and concepts.
- Enhanced form fields with optional tooltips for better user guidance.
- Introduced toggle switches for various settings, improving user interaction.
- Updated styles for new components to ensure consistency and responsiveness.
- Refactored ContextSettingsModal to utilize new components and improve readability.
- Improved TerminalPreview component styling for better layout and usability.

* Refactor modal header and preview selector styles; enhance terminal preview functionality

- Updated modal header padding and added gap for better spacing.
- Introduced a new header-controls section to include a project preview selector.
- Enhanced the preview selector styles for improved usability and aesthetics.
- Adjusted the preview column styles for a cleaner look.
- Implemented word wrap toggle functionality in the TerminalPreview component, allowing users to switch between wrapped and scrollable text.
- Improved scroll position handling in TerminalPreview to maintain user experience during content updates.

* feat: enhance modal settings with new icon links and update header controls

- Added new modal icon links for documentation and social media in ContextSettingsModal.
- Updated the header to remove sidebar toggle functionality and replaced it with context preview toggle.
- Refactored styles for modal icon links to improve UI/UX.
- Removed sidebar component from App and adjusted related state management.

* chore: remove abandoned cm_demo_content demo data approach

The demo data feature was prototyped but didn't work out. Removes:
- DEMO_OBSERVATIONS and DEMO_SUMMARIES arrays
- Conditional logic that bypassed DB for demo project
- Demo mode check in prior message extraction

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 21:34:41 -05:00
Alex Newman c78500cac2 Merge pull request #163 from thedotmack/copilot/fix-search-server-build-references 2025-12-03 17:21:04 -05:00
copilot-swe-agent[bot] 2b683f99bb fix: Update search-server references from .mjs to .cjs to match actual build output
- Update plugin/.mcp.json to reference search-server.cjs
- Update docs/public/configuration.mdx to reference search-server.cjs
- Update docs/public/development.mdx to reference search-server.cjs
- Remove stale plugin/scripts/search-server.mjs file

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
2025-12-03 22:17:12 +00:00
copilot-swe-agent[bot] 00e2b0c55f Initial plan 2025-12-03 22:13:59 +00:00
Alex Newman fbd4df4285 Merge branch 'feature/restore-when-to-skip-guidance' - Add WHEN TO SKIP guidance to observation prompt 2025-12-01 23:06:26 -05:00
Alex Newman ca24048e15 feat: Restore "WHEN TO SKIP" guidance to continuation prompt
Restores observation skip guidance that was removed in commit 68290a9
for token reduction. The removal caused the observer agent to forget
skip criteria after the first prompt, leading to more verbose
observations of routine operations.

Changes:
- Added WHEN TO SKIP section back to buildContinuationPrompt
- Added condensed CRITICAL reminder about what to record
- Maintains token efficiency by using condensed guidance vs full examples

This balances token usage with observation quality by keeping the
essential skip criteria without the full WHAT TO RECORD examples.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 23:04:51 -05:00
Alex Newman f9fd85fa4d chore: update CHANGELOG.md from releases
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 22:08:34 -05:00
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
38 changed files with 4087 additions and 1084 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "6.4.1",
"version": "6.5.2",
"source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions"
}
+14
View File
@@ -0,0 +1,14 @@
{
"mcpServers": {
"old-claude-mem": {
"command": "uvx",
"args": [
"chroma-mcp",
"--client-type",
"persistent",
"--data-dir",
"/Users/alexnewman/.claude-mem/backups/chroma-backup-20251005-222403"
]
}
}
}
+136
View File
@@ -4,6 +4,142 @@ 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/).
## [6.5.1] - 2025-12-04
## What's New
- Decorative Product Hunt announcement in terminal with rocket borders
- Product Hunt badge in viewer header with theme-aware switching (light/dark)
- Badge uses separate tracking URL for analytics
## Changes
This is a temporary launch day update. The announcement will auto-expire at midnight EST.
## [6.5.0] - 2025-12-04
## Documentation Overhaul
This release brings comprehensive documentation updates to reflect all features added in v6.4.x and standardize version references across the codebase.
### Changes
**Updated "What's New" Sections:**
- Highlights v6.4.9 Context Configuration Settings (11 new settings)
- Highlights v6.4.0 Dual-Tag Privacy System (`<private>` tags)
- Highlights v6.3.0 Version Channel (beta toggle in UI)
**Key Features Updated:**
- Added 🔒 Privacy Control (`<private>` tags)
- Added ⚙️ Context Configuration settings
**Clarifications:**
- Fixed lifecycle hook count: 5 lifecycle events with 6 hook scripts
- Fixed default model: `claude-haiku-4-5` (not sonnet)
- Removed outdated MCP search server references (replaced by skills in v5.4.0)
**Files Updated:**
- README.md - version badge, features, What's New, default model
- docs/public/introduction.mdx - features, hook count, What's New
- docs/public/installation.mdx - removed MCP reference
- docs/public/configuration.mdx - default model corrections
- plugin/skills/mem-search/operations/help.md - version references
---
📚 Full documentation available at [docs.claude-mem.ai](https://docs.claude-mem.ai)
## [6.4.9] - 2025-12-02
## New Features
This release adds comprehensive context configuration settings, giving users fine-grained control over how memory context is injected at session start.
### Context Configuration (11 new settings)
**Token Economics Display:**
- Control visibility of read tokens, work tokens, savings amount, and savings percentage
**Observation Filtering:**
- Filter by observation types (bugfix, feature, refactor, discovery, decision, change)
- Filter by observation concepts (how-it-works, why-it-exists, what-changed, problem-solution, gotcha, pattern, trade-off)
**Display Configuration:**
- Configure number of full observations to include
- Choose which field to show in full (narrative/facts)
- Set number of recent sessions to include
**Feature Toggles:**
- Control inclusion of last session summary
- Control inclusion of final messages from prior session
All settings have sensible defaults and are fully backwards compatible.
### What's Next
**Settings UI enhancements coming very shortly in the next release!** We're working on improving the settings interface for even better user experience.
## Technical Details
- 10 files changed (+825, -212)
- New centralized observation metadata constants
- Enhanced context hook with SQL-based filtering
- Worker service settings validation
- Viewer UI controls for all settings
## [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
## 🎯 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.
**Current Version**: 6.4.1
**Current Version**: 6.5.2
## Architecture
+18 -17
View File
@@ -17,7 +17,7 @@
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.3.0-green.svg" alt="Version">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg" alt="Node">
@@ -71,6 +71,8 @@ Restart Claude Code. Context from previous sessions will automatically appear in
- 📊 **Progressive Disclosure** - Layered memory retrieval with token cost visibility
- 🔍 **Skill-Based Search** - Query your project history with mem-search skill (~2,250 token savings)
- 🖥️ **Web Viewer UI** - Real-time memory stream at http://localhost:37777
- 🔒 **Privacy Control** - Use `<private>` tags to exclude sensitive content from storage
- ⚙️ **Context Configuration** - Fine-grained control over what context gets injected
- 🤖 **Automatic Operation** - No manual intervention required
- 🔗 **Citations** - Reference past decisions with `claude-mem://` URIs
- 🧪 **Beta Channel** - Try experimental features like Endless Mode via version switching
@@ -144,7 +146,7 @@ npx mintlify dev
**Core Components:**
1. **6 Lifecycle Hooks** - context-hook, user-message-hook, new-hook, save-hook, summary-hook, cleanup-hook
1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd (6 hook scripts)
2. **Smart Install** - Cached dependency checker (pre-hook script, not a lifecycle hook)
3. **Worker Service** - HTTP API on port 37777 with web viewer UI and 10 search endpoints, managed by PM2
4. **SQLite Database** - Stores sessions, observations, summaries with FTS5 full-text search
@@ -229,28 +231,27 @@ See [Beta Features Documentation](https://docs.claude-mem.ai/beta-features) for
---
## What's New in v6.0.0
## What's New
**🚀 Major Session Management & Transcript Processing Improvements:**
**v6.4.9 - Context Configuration Settings:**
- 11 new settings for fine-grained control over context injection
- Configure token economics display, observation filtering by type/concept
- Control number of observations and which fields to display
- **Enhanced Session Initialization**: Accept userPrompt and promptNumber for better context tracking
- **Live UserPrompt Updates**: Multi-turn conversation support with real-time prompt tracking
- **Improved SessionManager**: Better context handling and observation processing
- **Comprehensive Transcript Processing**: New scripts and utilities for analyzing Claude Code transcripts
- **Rich Context Extraction**: Advanced parsing utilities for extracting meaningful context from sessions
- **Refactored Architecture**: Improved hooks and SDKAgent for more reliable observation handling
- **Silent Debug Logging**: Better debugging capabilities without cluttering output
- **Enhanced Error Handling**: More robust error recovery and debugging tools
**v6.4.0 - Dual-Tag Privacy System:**
- `<private>` tags for user-controlled privacy - wrap sensitive content to exclude from storage
- System-level `<claude-mem-context>` tags prevent recursive observation storage
- Edge processing ensures private content never reaches database
**Breaking Changes**: Significant architectural changes in session management and observation handling. Existing sessions continue to work, but internal APIs have evolved.
**v6.3.0 - Version Channel:**
- Switch between stable and beta versions from the web viewer UI
- Try experimental features like Endless Mode without manual git operations
**Previous Highlights:**
- **v6.0.0**: Major session management & transcript processing improvements
- **v5.5.0**: mem-search skill enhancement with 100% effectiveness rate
- **v5.4.0**: Skill-based search architecture (~2,250 tokens saved per session)
- **v5.1.2**: Theme toggle for light/dark mode in viewer UI
- **v5.1.0**: Web-based viewer UI with real-time updates
- **v5.0.3**: Smart install caching (2-5s → 10ms)
- **v5.0.0**: Hybrid search with Chroma vector database
See [CHANGELOG.md](CHANGELOG.md) for complete version history.
@@ -313,7 +314,7 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
**Environment Variables:**
- `CLAUDE_MEM_MODEL` - AI model for processing (default: claude-sonnet-4-5)
- `CLAUDE_MEM_MODEL` - AI model for processing (default: claude-haiku-4-5)
- `CLAUDE_MEM_WORKER_PORT` - Worker port (default: 37777)
- `CLAUDE_MEM_DATA_DIR` - Data directory override (dev only)
+100 -23
View File
@@ -13,7 +13,7 @@ description: "Environment variables and settings for Claude-Mem"
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem/` | Data directory (production default) |
| `CLAUDE_CODE_PATH` | Auto-detected | Path to Claude Code CLI (for Windows) |
| `CLAUDE_MEM_WORKER_PORT` | `37777` | Worker service port |
| `CLAUDE_MEM_MODEL` | `claude-sonnet-4-5` | AI model for processing observations |
| `CLAUDE_MEM_MODEL` | `claude-haiku-4-5` | AI model for processing observations |
| `CLAUDE_MEM_CONTEXT_OBSERVATIONS` | `50` | Number of observations to inject |
| `NODE_ENV` | `production` | Environment mode |
| `FORCE_COLOR` | `1` | Enable colored logs |
@@ -24,8 +24,8 @@ Configure which AI model processes your observations.
### Available Models
- `claude-haiku-4-5` - Fast, cost-efficient
- `claude-sonnet-4-5` - Balanced (default)
- `claude-haiku-4-5` - Fast, cost-efficient (default)
- `claude-sonnet-4-5` - Balanced
- `claude-opus-4` - Most capable
- `claude-3-7-sonnet` - Alternative version
@@ -43,7 +43,7 @@ Edit `~/.claude/settings.json`:
```json
{
"CLAUDE_MEM_MODEL": "claude-sonnet-4-5"
"CLAUDE_MEM_MODEL": "claude-haiku-4-5"
}
```
@@ -82,7 +82,7 @@ ${CLAUDE_PLUGIN_ROOT}/
│ ├── summary-hook.js # Summary generation hook
│ ├── cleanup-hook.js # Session cleanup hook
│ ├── worker-service.cjs # Worker service (CJS)
│ └── search-server.mjs # MCP search server (ESM)
│ └── search-server.cjs # MCP search server (CJS)
└── ui/
└── viewer.html # Web viewer UI bundle
```
@@ -201,33 +201,110 @@ module.exports = {
## Context Injection Configuration
### CLAUDE_MEM_CONTEXT_OBSERVATIONS
Claude-Mem injects past observations into each new session, giving Claude awareness of recent work. You can configure exactly what gets injected using the **Context Settings Modal**.
Controls how many observations are injected into each new session for context continuity.
### Context Settings Modal
**Default**: 50 observations
Access the settings modal from the web viewer at http://localhost:37777:
**What it does**:
- Fetches the most recent N observations from the database
- Injects them as context at SessionStart
- Allows Claude to maintain awareness of recent work across sessions
1. Click the **gear icon** in the header
2. Adjust settings in the right panel
3. See changes reflected live in the **Terminal Preview** on the left
4. Settings auto-save as you change them
**Configuration** in `~/.claude/settings.json`:
The Terminal Preview shows exactly what will be injected at the start of your next Claude Code session for the selected project.
```json
{
"env": {
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "100"
}
}
```
### Loading Settings
Control how many observations are injected:
| Setting | Default | Range | Description |
|---------|---------|-------|-------------|
| **Observations** | 50 | 1-200 | Total number of recent observations to include |
| **Sessions** | 10 | 1-50 | Number of recent sessions to pull observations from |
**Considerations**:
- **Higher values** = More context but slower SessionStart and more tokens used
- **Lower values** = Faster SessionStart but less historical awareness
- Default of 50 balances context richness with performance
- Default of 50 observations from 10 sessions balances context richness with performance
**Note**: This injects individual observations, not entire sessions. Each observation represents a single tool execution (Read, Write, Edit, etc.) that was compressed into a semantic learning.
### Filter Settings
Control which observation types and concepts are included:
**Types** (select any combination):
- `bugfix` - Bug fixes and error resolutions
- `feature` - New functionality additions
- `refactor` - Code restructuring
- `discovery` - Learnings about how code works
- `decision` - Architectural or design decisions
- `change` - General code changes
**Concepts** (select any combination):
- `how-it-works` - System behavior explanations
- `why-it-exists` - Rationale for code/design
- `what-changed` - Change summaries
- `problem-solution` - Problem/solution pairs
- `gotcha` - Edge cases and pitfalls
- `pattern` - Recurring patterns
- `trade-off` - Design trade-offs
Use "All" or "None" buttons to quickly select/deselect all options.
### Display Settings
Control how observations appear in the context:
**Full Observations**:
| Setting | Default | Options | Description |
|---------|---------|---------|-------------|
| **Count** | 5 | 0-20 | How many observations show expanded details |
| **Field** | narrative | narrative, facts | Which field to expand |
The most recent N observations (set by Count) show their full narrative or facts. Remaining observations show only title, type, and token counts in a compact table format.
**Token Economics** (toggles):
| Setting | Default | Description |
|---------|---------|-------------|
| **Read cost** | true | Show tokens to read each observation |
| **Work investment** | true | Show tokens spent creating the observation |
| **Savings** | true | Show total tokens saved by reusing context |
Token economics help you understand the value of cached observations vs. re-reading files.
### Advanced Settings
| Setting | Default | Description |
|---------|---------|-------------|
| **Model** | claude-haiku-4-5 | AI model for generating observations |
| **Worker Port** | 37777 | Port for background worker service |
| **MCP search server** | true | Enable Model Context Protocol search tools |
| **Include last summary** | false | Add previous session's summary to context |
| **Include last message** | false | Add previous session's final message |
### Manual Configuration
Settings are stored in `~/.claude-mem/settings.json`. You can also configure via environment variables in `~/.claude/settings.json`:
```json
{
"env": {
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "100",
"CLAUDE_MEM_CONTEXT_SESSION_COUNT": "20",
"CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES": "bugfix,decision,discovery",
"CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS": "how-it-works,gotcha",
"CLAUDE_MEM_CONTEXT_FULL_COUNT": "10",
"CLAUDE_MEM_CONTEXT_FULL_FIELD": "narrative",
"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_LAST_SUMMARY": "false",
"CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE": "false"
}
}
```
**Note**: The Context Settings Modal is the recommended way to configure these settings, as it provides live preview of changes.
## Customization
@@ -328,7 +405,7 @@ npm run worker:logs
### Invalid Model Name
If you specify an invalid model name, the worker will fall back to `claude-sonnet-4-5` and log a warning.
If you specify an invalid model name, the worker will fall back to `claude-haiku-4-5` and log a warning.
Valid models:
- claude-haiku-4-5
+2 -2
View File
@@ -33,7 +33,7 @@ The build process uses esbuild to compile TypeScript:
1. Compiles TypeScript to JavaScript
2. Creates standalone executables for each hook in `plugin/scripts/`
3. Bundles MCP search server to `plugin/scripts/search-server.mjs`
3. Bundles MCP search server to `plugin/scripts/search-server.cjs`
4. Bundles worker service to `plugin/scripts/worker-service.cjs`
5. Bundles web viewer UI to `plugin/ui/viewer.html`
@@ -41,7 +41,7 @@ The build process uses esbuild to compile TypeScript:
- Hook executables: `*-hook.js` (ESM format)
- Smart installer: `smart-install.js` (ESM format)
- Worker service: `worker-service.cjs` (CJS format)
- Search server: `search-server.mjs` (ESM format)
- Search server: `search-server.cjs` (CJS format)
- Viewer UI: `viewer.html` (self-contained HTML bundle)
### Build Scripts
-1
View File
@@ -18,7 +18,6 @@ That's it! The plugin will automatically:
- Download prebuilt binaries (no compilation needed)
- Install all dependencies (including PM2 and SQLite binaries)
- Configure hooks for session lifecycle management
- Set up the MCP search server
- Auto-start the worker service on first session
Start a new Claude Code session and you'll see context from previous sessions automatically loaded.
+13 -14
View File
@@ -25,7 +25,8 @@ Restart Claude Code. Context from previous sessions will automatically appear in
- 🧠 **Persistent Memory** - Context survives across sessions
- 🔍 **mem-search Skill** - Query your project history with natural language (~2,250 token savings)
- 🌐 **Web Viewer UI** - Real-time memory stream visualization at http://localhost:37777
- 🎨 **Theme Toggle** - Light, dark, and system preference themes
- 🔒 **Privacy Control** - Use `<private>` tags to exclude sensitive content from storage
- ⚙️ **Context Configuration** - Fine-grained control over what context gets injected
- 🤖 **Automatic Operation** - No manual intervention required
- 📊 **FTS5 Search** - Fast full-text search across observations
- 🔗 **Citations** - Reference past decisions with `claude-mem://` URIs
@@ -55,7 +56,7 @@ Restart Claude Code. Context from previous sessions will automatically appear in
```
**Core Components:**
1. **6 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd, UserMessage
1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd (6 hook scripts)
2. **Smart Install** - Cached dependency checker (pre-hook script)
3. **Worker Service** - HTTP API on port 37777 managed by PM2
4. **SQLite Database** - Stores sessions, observations, summaries with FTS5 search
@@ -71,25 +72,23 @@ See [Architecture Overview](architecture/overview) for details.
- **PM2**: Process manager (bundled - no global install required)
- **SQLite 3**: For persistent storage (bundled)
## What's New in v6.0.0
## What's New
**🚀 Major Session Management & Transcript Processing Improvements:**
**v6.4.9 - Context Configuration Settings:**
- 11 new settings for fine-grained control over context injection
- Configure token economics display, observation filtering by type/concept
- **Enhanced Session Initialization**: Accept userPrompt and promptNumber for better context tracking
- **Live UserPrompt Updates**: Multi-turn conversation support with real-time prompt tracking
- **Improved SessionManager**: Better context handling and observation processing
- **Comprehensive Transcript Processing**: New scripts and utilities for analyzing Claude Code transcripts
- **Rich Context Extraction**: Advanced parsing utilities for extracting meaningful context from sessions
- **Refactored Architecture**: Improved hooks and SDKAgent for more reliable observation handling
- **Silent Debug Logging**: Better debugging capabilities without cluttering output
- **Enhanced Error Handling**: More robust error recovery and debugging tools
**v6.4.0 - Dual-Tag Privacy System:**
- `<private>` tags for user-controlled privacy - wrap sensitive content to exclude from storage
- Edge processing ensures private content never reaches database
**Breaking Changes**: Significant architectural changes in session management and observation handling. Existing sessions continue to work, but internal APIs have evolved.
**v6.3.0 - Version Channel:**
- Switch between stable and beta versions from the web viewer UI
**Previous Highlights:**
- **v6.0.0**: Major session management & transcript processing improvements
- **v5.5.0**: mem-search skill enhancement with 100% effectiveness rate
- **v5.4.0**: Skill-based search architecture (~2,250 tokens saved per session)
- **v5.1.2**: Theme toggle for light/dark mode in viewer UI
## Next Steps
+34 -8
View File
@@ -1,17 +1,18 @@
{
"name": "claude-mem",
"version": "6.3.6",
"version": "6.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-mem",
"version": "6.3.6",
"version": "6.5.0",
"license": "AGPL-3.0",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
"@modelcontextprotocol/sdk": "^1.20.1",
"better-sqlite3": "^11.0.0",
"ansi-to-html": "^0.7.2",
"better-sqlite3": "^12.5.0",
"express": "^4.18.2",
"glob": "^11.0.3",
"handlebars": "^4.7.8",
@@ -1484,7 +1485,6 @@
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -1615,6 +1615,21 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ansi-to-html": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz",
"integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==",
"license": "MIT",
"dependencies": {
"entities": "^2.2.0"
},
"bin": {
"ansi-to-html": "bin/ansi-to-html"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/ansis": {
"version": "4.0.0-node10",
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.0.0-node10.tgz",
@@ -1701,12 +1716,17 @@
}
},
"node_modules/better-sqlite3": {
"version": "11.10.0",
"version": "12.5.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz",
"integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
}
},
"node_modules/binary-extensions": {
@@ -2148,6 +2168,15 @@
"node": ">=8.6"
}
},
"node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"license": "BSD-2-Clause",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2338,7 +2367,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -3851,7 +3879,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -4853,7 +4880,6 @@
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "6.4.1",
"version": "6.5.2",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
@@ -48,7 +48,8 @@
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
"@modelcontextprotocol/sdk": "^1.20.1",
"better-sqlite3": "^11.0.0",
"ansi-to-html": "^0.7.2",
"better-sqlite3": "^12.5.0",
"express": "^4.18.2",
"glob": "^11.0.3",
"handlebars": "^4.7.8",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "6.4.1",
"version": "6.5.2",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": {
"name": "Alex Newman"
+1 -1
View File
@@ -2,7 +2,7 @@
"mcpServers": {
"claude-mem-search": {
"type": "stdio",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.mjs"
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.cjs"
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+14 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node
import{execSync as A}from"child_process";import{join as c}from"path";import{homedir as d}from"os";import{existsSync as C}from"fs";import x from"path";import{homedir as E}from"os";import{existsSync as k,readFileSync as R}from"fs";import{join as t,dirname as T,basename as U}from"path";import{homedir as u}from"os";import{fileURLToPath as y}from"url";function w(){return typeof __dirname<"u"?__dirname:T(y(import.meta.url))}var H=w(),e=process.env.CLAUDE_MEM_DATA_DIR||t(u(),".claude-mem"),a=process.env.CLAUDE_CONFIG_DIR||t(u(),".claude"),W=t(e,"archives"),j=t(e,"logs"),N=t(e,"trash"),$=t(e,"backups"),F=t(e,"settings.json"),K=t(e,"claude-mem.db"),B=t(e,"vector-db"),G=t(a,"settings.json"),V=t(a,"commands"),J=t(a,"CLAUDE.md");function l(){try{let o=x.join(E(),".claude-mem","settings.json");if(k(o)){let s=JSON.parse(R(o,"utf-8")),n=parseInt(s.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(n))return n}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}var P=c(d(),".claude","plugins","marketplaces","thedotmack"),S=c(P,"node_modules");C(S)||(console.error(`
import{execSync as P}from"child_process";import{join as a}from"path";import{homedir as f}from"os";import{existsSync as S}from"fs";import k from"path";import{homedir as A}from"os";import{existsSync as R,readFileSync as C}from"fs";import{join as t,dirname as w,basename as L}from"path";import{homedir as l}from"os";import{fileURLToPath as x}from"url";function E(){return typeof __dirname<"u"?__dirname:w(x(import.meta.url))}var j=E(),o=process.env.CLAUDE_MEM_DATA_DIR||t(l(),".claude-mem"),c=process.env.CLAUDE_CONFIG_DIR||t(l(),".claude"),N=t(o,"archives"),$=t(o,"logs"),F=t(o,"trash"),K=t(o,"backups"),B=t(o,"settings.json"),G=t(o,"claude-mem.db"),V=t(o,"vector-db"),J=t(c,"settings.json"),Y=t(c,"commands"),Z=t(c,"CLAUDE.md");function d(){try{let r=k.join(A(),".claude-mem","settings.json");if(R(r)){let s=JSON.parse(C(r,"utf-8")),n=parseInt(s.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(n))return n}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}var v=a(f(),".claude","plugins","marketplaces","thedotmack"),I=a(v,"node_modules");S(I)||(console.error(`
---
\u{1F389} Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
user messages in Claude Code UI until a better method is provided.
@@ -17,7 +17,17 @@ Dependencies have been installed in the background. This only happens once.
Thank you for installing Claude-Mem!
This message was not added to your startup context, so you can continue working as normal.
`),process.exit(3));try{let o=c(d(),".claude","plugins","marketplaces","thedotmack","plugin","scripts","context-hook.js"),s=A(`node "${o}" --colors`,{encoding:"utf8"}),n=l(),r=new Date,f=new Date("2025-12-06T00:00:00Z"),i="";if(r<f){let g=r.getUTCHours()*60+r.getUTCMinutes(),m=Math.floor((g-300+1440)%1440/60),p=r.getUTCDate(),h=r.getUTCMonth(),D=r.getUTCFullYear()===2025&&h===11&&p>=1&&p<=5,_=m>=17&&m<19;D&&_?i=`
`),process.exit(3));try{let r=a(f(),".claude","plugins","marketplaces","thedotmack","plugin","scripts","context-hook.js"),s=P(`node "${r}" --colors`,{encoding:"utf8"}),n=d(),e=new Date,g=new Date("2025-12-06T00:00:00Z"),h=new Date("2025-12-05T05:00:00Z"),m="";e<h&&(m=`
\u{1F680} \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u{1F680}
We launched on Product Hunt!
https://tinyurl.com/claude-mem-ph
\u2B50 Your upvote means the world - thank you!
\u{1F680} \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u{1F680}
`);let i="";if(e<g){let D=e.getUTCHours()*60+e.getUTCMinutes(),p=Math.floor((D-300+1440)%1440/60),u=e.getUTCDate(),y=e.getUTCMonth(),T=e.getUTCFullYear()===2025&&y===11&&u>=1&&u<=5,_=p>=17&&p<19;T&&_?i=`
\u{1F534} LIVE NOW: AMA w/ Dev (@thedotmack) until 7pm EST
`:i=`
\u2013 LIVE AMA w/ Dev (@thedotmack) Dec 1st\u20135th, 5pm to 7pm EST
@@ -30,6 +40,6 @@ This message was not added to your startup context, so you can continue working
\u{1F4A1} New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history.
\u{1F4AC} Community https://discord.gg/J4wttp9vDu`+i+`
\u{1F4AC} Community https://discord.gg/J4wttp9vDu`+m+i+`
\u{1F4FA} Watch live in browser http://localhost:${n}/
`)}catch(o){console.error(`\u274C Failed to load context display: ${o}`)}process.exit(3);
`)}catch(r){console.error(`\u274C Failed to load context display: ${r}`)}process.exit(3);
File diff suppressed because one or more lines are too long
+3 -7
View File
@@ -21,7 +21,7 @@ Returns complete API documentation:
```json
{
"version": "5.4.0",
"version": "6.5.0",
"base_url": "http://localhost:37777/api",
"endpoints": [
{
@@ -55,7 +55,7 @@ Returns complete API documentation:
Present as reference documentation:
```markdown
## claude-mem Search API Reference (v5.4.0)
## claude-mem Search API Reference
Base URL: `http://localhost:37777/api`
@@ -164,11 +164,7 @@ The help response includes version information:
```json
{
"version": "5.4.0",
"skill_migration": true,
"deprecated": {
"mcp_tools": "Replaced by HTTP API in v5.4.0"
}
"version": "6.5.0"
}
```
File diff suppressed because one or more lines are too long
+906
View File
@@ -80,6 +80,9 @@
--color-skeleton-highlight: #e8ecef;
--shadow-focus: 0 0 0 2px rgba(9, 105, 218, 0.3);
/* Font families */
--font-terminal: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
}
/* Theme Variables - Dark Mode */
@@ -146,6 +149,9 @@
--color-skeleton-highlight: #4a4540;
--shadow-focus: 0 0 0 2px rgba(88, 166, 255, 0.2);
/* Font families */
--font-terminal: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
}
/* System preference default */
@@ -213,6 +219,9 @@
--color-skeleton-highlight: #e8ecef;
--shadow-focus: 0 0 0 2px rgba(9, 105, 218, 0.3);
/* Font families */
--font-terminal: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
}
}
@@ -280,6 +289,9 @@
--color-skeleton-highlight: #505050;
--shadow-focus: 0 0 0 2px rgba(88, 166, 255, 0.2);
/* Font families */
--font-terminal: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
}
}
@@ -602,6 +614,61 @@
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
/* GitHub Stars Button - Similar to Community Button */
.github-stars-btn {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 0 14px;
height: 36px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.github-stars-btn:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.github-stars-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
/* Stars count animation */
.stars-count {
animation: countUp 0.6s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-block;
}
.stars-loading {
opacity: 0.5;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes countUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.icon-link {
display: flex;
align-items: center;
@@ -1453,6 +1520,11 @@
.community-btn {
display: none;
}
/* Hide GitHub stars button on mobile */
.github-stars-btn {
display: none;
}
}
/* Mobile Responsive Styles - 480px and below */
@@ -1590,6 +1662,840 @@
height: 44px;
}
}
/* Context Settings Modal - Modern Clean Design */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s ease-out;
padding: 20px;
}
.context-settings-modal {
background: var(--color-bg-primary);
border: 1px solid var(--color-border-primary);
border-radius: 12px;
width: 100%;
max-width: 1200px;
height: 90vh;
max-height: 800px;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.modal-header {
padding: 14px 20px;
border-bottom: 1px solid var(--color-border-primary);
background: var(--color-bg-header);
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-shrink: 0;
}
.modal-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-header);
letter-spacing: -0.01em;
flex-shrink: 0;
}
.header-controls {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
justify-content: flex-end;
}
.preview-selector {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--color-text-secondary);
white-space: nowrap;
}
.preview-selector select {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
color: var(--color-text-primary);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.preview-selector select:hover {
border-color: var(--color-border-focus);
background: var(--color-bg-card-hover);
}
.preview-selector select:focus {
outline: none;
border-color: var(--color-accent-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.modal-close-btn {
background: transparent;
border: 1px solid var(--color-border-primary);
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
padding: 0;
}
.modal-close-btn:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: scale(1.05);
}
.modal-close-btn:active {
transform: scale(0.95);
}
.modal-icon-link {
background: transparent;
border: 1px solid var(--color-border-primary);
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
padding: 0;
text-decoration: none;
}
.modal-icon-link:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: scale(1.05);
}
.modal-icon-link:active {
transform: scale(0.95);
}
.modal-body {
flex: 1;
display: grid;
grid-template-columns: 70fr 30fr;
gap: 0;
overflow: hidden;
min-height: 0;
}
/* Preview Column - Terminal Style */
.preview-column {
padding: 20px;
overflow: hidden;
border-right: none;
background: transparent;
display: flex;
flex-direction: column;
}
.preview-column-header {
padding: 16px 20px;
background: #141414;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
flex-shrink: 0;
}
.preview-column-header label {
display: block;
font-size: 11px;
font-weight: 600;
color: #888;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.preview-column-header select {
width: 100%;
background: #0a0a0a;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
padding: 8px 12px;
height: 36px;
font-size: 13px;
font-weight: 500;
color: #ddd;
cursor: pointer;
transition: all 0.2s;
}
.preview-column-header select:hover {
border-color: rgba(255, 255, 255, 0.2);
background: #111;
}
.preview-column-header select:focus {
outline: none;
border-color: var(--color-accent-primary);
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
}
.preview-content {
flex: 1;
overflow-y: auto;
padding: 20px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 13px;
line-height: 1.6;
color: #ccc;
}
.preview-content pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Settings Column */
.settings-column {
padding: 0;
overflow-y: auto;
background: var(--color-bg-primary);
position: relative;
}
/* Custom Scrollbar */
.settings-column::-webkit-scrollbar {
width: 8px;
}
.settings-column::-webkit-scrollbar-track {
background: transparent;
}
.settings-column::-webkit-scrollbar-thumb {
background: var(--color-bg-scrollbar-thumb);
border-radius: 4px;
}
.settings-column::-webkit-scrollbar-thumb:hover {
background: var(--color-bg-scrollbar-thumb-hover);
}
.preview-content::-webkit-scrollbar {
width: 8px;
}
.preview-content::-webkit-scrollbar-track {
background: transparent;
}
.preview-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
.preview-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
/* Settings Groups - Compact */
.settings-group {
padding: 14px 16px;
border-bottom: 1px solid var(--color-border-primary);
}
.settings-group:last-child {
border-bottom: none;
}
.settings-group h4 {
margin: 0 0 10px 0;
font-size: 10px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.8px;
}
/* Filter Chips - Compact */
.chips-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chip {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 5px 10px;
min-height: 28px;
border: 1px solid var(--color-border-primary);
border-radius: 4px;
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
background: var(--color-bg-card);
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
}
.chip:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-hover);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.chip:active {
transform: translateY(0);
}
.chip.selected {
background: linear-gradient(135deg, var(--color-bg-button) 0%, var(--color-accent-primary) 100%);
color: white;
border-color: var(--color-bg-button);
box-shadow: 0 2px 8px rgba(9, 105, 218, 0.25);
}
.chip.selected:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(9, 105, 218, 0.35);
}
/* Form Controls in Modal - Compact */
.settings-group input[type="number"],
.settings-group select {
width: 100%;
background: var(--color-bg-input);
border: 1px solid var(--color-border-primary);
border-radius: 4px;
padding: 6px 10px;
height: 32px;
font-size: 12px;
color: var(--color-text-primary);
transition: all 0.2s;
margin-top: 4px;
}
.settings-group input[type="number"]:hover,
.settings-group select:hover {
border-color: var(--color-border-hover);
}
.settings-group input[type="number"]:focus,
.settings-group select:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
}
.settings-group label {
display: block;
font-size: 11px;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: 4px;
}
/* Checkboxes - Compact */
.settings-group input[type="checkbox"] {
width: 14px;
height: 14px;
cursor: pointer;
margin-right: 6px;
accent-color: var(--color-accent-primary);
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 4px;
}
.checkbox-item {
display: flex;
align-items: center;
cursor: pointer;
padding: 4px 0;
}
.checkbox-item label {
margin: 0;
cursor: pointer;
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
}
.checkbox-item:hover label {
color: var(--color-text-primary);
}
/* Number Input Group - Compact */
.number-input-group {
margin-top: 6px;
}
.select-group {
margin-top: 6px;
}
.number-input-group + .number-input-group,
.select-group + .number-input-group,
.number-input-group + .select-group {
margin-top: 10px;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* ============================================
NEW: Collapsible Sections
============================================ */
.settings-section-collapsible {
border-bottom: 1px solid var(--color-border-primary);
}
.settings-section-collapsible:last-child {
border-bottom: none;
}
.section-header-btn {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
transition: background 0.15s ease;
}
.section-header-btn:hover {
background: var(--color-bg-card-hover);
}
.section-header-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
letter-spacing: -0.01em;
}
.section-description {
font-size: 11px;
color: var(--color-text-muted);
font-weight: 400;
}
.chevron-icon {
color: var(--color-text-muted);
transition: transform 0.2s ease;
flex-shrink: 0;
}
.chevron-icon.rotated {
transform: rotate(180deg);
}
.section-content {
padding: 0 16px 16px 16px;
}
/* ============================================
NEW: Chip Groups with All/None
============================================ */
.chip-group {
margin-bottom: 14px;
}
.chip-group:last-child {
margin-bottom: 0;
}
.chip-group-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.chip-group-label {
font-size: 11px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.chip-group-actions {
display: flex;
gap: 4px;
}
.chip-action {
padding: 2px 8px;
font-size: 10px;
font-weight: 500;
color: var(--color-text-muted);
background: transparent;
border: 1px solid var(--color-border-primary);
border-radius: 3px;
cursor: pointer;
transition: all 0.15s ease;
}
.chip-action:hover {
color: var(--color-text-primary);
border-color: var(--color-border-hover);
background: var(--color-bg-card-hover);
}
.chip-action.active {
color: var(--color-accent-primary);
border-color: var(--color-accent-primary);
background: var(--color-type-badge-bg);
}
/* ============================================
NEW: Form Fields with Tooltips
============================================ */
.form-field {
margin-bottom: 12px;
}
.form-field:last-child {
margin-bottom: 0;
}
.form-field-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: 6px;
}
.tooltip-trigger {
display: inline-flex;
align-items: center;
color: var(--color-text-muted);
cursor: help;
transition: color 0.15s ease;
}
.tooltip-trigger:hover {
color: var(--color-accent-primary);
}
.form-field input[type="number"],
.form-field select {
width: 100%;
background: var(--color-bg-input);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 8px 12px;
height: 36px;
font-size: 13px;
color: var(--color-text-primary);
transition: all 0.15s ease;
}
.form-field input[type="number"]:hover,
.form-field select:hover {
border-color: var(--color-border-hover);
}
.form-field input[type="number"]:focus,
.form-field select:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
}
/* ============================================
NEW: Toggle Switches
============================================ */
.toggle-group {
display: flex;
flex-direction: column;
gap: 2px;
}
.toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid var(--color-border-secondary);
}
.toggle-row:last-child {
border-bottom: none;
}
.toggle-info {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 0;
}
.toggle-label {
font-size: 12px;
font-weight: 500;
color: var(--color-text-primary);
cursor: pointer;
}
.toggle-description {
font-size: 11px;
color: var(--color-text-muted);
line-height: 1.3;
}
.toggle-switch {
position: relative;
width: 40px;
height: 22px;
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border-primary);
border-radius: 11px;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
margin-left: 12px;
padding: 0;
}
.toggle-switch:hover:not(.disabled) {
border-color: var(--color-border-hover);
}
.toggle-switch.on {
background: var(--color-accent-primary);
border-color: var(--color-accent-primary);
}
.toggle-switch.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.toggle-knob {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
transition: transform 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.toggle-switch.on .toggle-knob {
transform: translateX(18px);
}
/* ============================================
NEW: Display Subsections
============================================ */
.display-subsection {
padding: 12px 0;
border-bottom: 1px solid var(--color-border-secondary);
}
.display-subsection:first-child {
padding-top: 0;
}
.display-subsection:last-child {
border-bottom: none;
padding-bottom: 0;
}
.subsection-label {
display: block;
font-size: 11px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
/* ============================================
Improved Chip Styles
============================================ */
.chip {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 12px;
min-height: 30px;
border: 1px solid var(--color-border-primary);
border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
background: var(--color-bg-card);
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
}
.chip:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-accent-primary);
color: var(--color-text-primary);
}
.chip:active {
transform: scale(0.98);
}
.chip.selected {
background: var(--color-accent-primary);
color: white;
border-color: var(--color-accent-primary);
}
.chip.selected:hover {
background: var(--color-bg-button-hover);
border-color: var(--color-bg-button-hover);
}
/* Responsive Modal */
@media (max-width: 900px) {
.modal-body {
grid-template-columns: 1fr;
}
.preview-column {
display: none;
}
}
@media (max-width: 600px) {
.modal-backdrop {
padding: 0;
}
.context-settings-modal {
border-radius: 0;
height: 100vh;
max-height: none;
}
.modal-header {
padding: 12px 16px;
gap: 12px;
}
.preview-selector {
font-size: 11px;
gap: 6px;
}
.preview-selector select {
padding: 5px 10px;
font-size: 11px;
}
.settings-group {
padding: 14px 16px;
}
.section-header-btn {
padding: 12px 14px;
}
.section-content {
padding: 0 14px 14px 14px;
}
.toggle-row {
padding: 8px 0;
}
.toggle-switch {
width: 36px;
height: 20px;
}
.toggle-knob {
width: 14px;
height: 14px;
}
.toggle-switch.on .toggle-knob {
transform: translateX(16px);
}
}
</style>
</head>
+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(',');
+354 -100
View File
@@ -10,6 +10,15 @@ import { stdin } from 'process';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
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
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: ../../
const VERSION_MARKER_PATH = path.join(__dirname, '../../.install-version');
/**
* Get context depth from settings
* Priority: ~/.claude/settings.json > env var > default
*/
function getContextDepth(): number {
try {
const settingsPath = path.join(homedir(), '.claude', 'settings.json');
if (existsSync(settingsPath)) {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
if (settings.env?.CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
const count = parseInt(settings.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
if (!isNaN(count) && count > 0) {
return count;
}
}
}
} catch {
// Fall through to env var or default
}
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;
}
// Configuration: Read from settings.json or environment
const DISPLAY_OBSERVATION_COUNT = getContextDepth();
const DISPLAY_SESSION_COUNT = 10; // Recent sessions for timeline context
/**
* Load all context configuration settings
* 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 SUMMARY_LOOKAHEAD = 1; // Fetch one extra summary for offset calculation
@@ -159,14 +219,77 @@ function renderSummaryField(label: string, value: string | null, color: string,
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
*/
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';
let db: SessionStore;
let db: SessionStore | null = null;
try {
db = new SessionStore();
} catch (error: any) {
@@ -188,19 +311,32 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
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
// Configurable via CLAUDE_MEM_CONTEXT_OBSERVATIONS env var (default: 50)
const allObservations = db.db.prepare(`
// Configurable via settings (default: 50)
const observations = db.db.prepare(`
SELECT
id, sdk_session_id, type, title, subtitle, narrative,
facts, concepts, files_read, files_modified, discovery_tokens,
created_at, created_at_epoch
FROM observations
WHERE project = ?
AND type IN (${typePlaceholders})
AND EXISTS (
SELECT 1 FROM json_each(concepts)
WHERE value IN (${conceptPlaceholders})
)
ORDER BY created_at_epoch DESC
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)
// Fetch one extra for offset calculation
@@ -210,22 +346,63 @@ 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[];
// 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 (allObservations.length === 0 && recentSummaries.length === 0) {
db.close();
if (observations.length === 0 && recentSummaries.length === 0) {
db?.close();
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 `# [${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);
const displaySummaries = recentSummaries.slice(0, config.sessionCount);
// All observations are shown in timeline (filtered by type, not concepts)
// All filtered observations are shown in timeline
const timelineObs = observations;
// Build output
@@ -298,23 +475,44 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
? Math.round((savings / totalDiscoveryTokens) * 100)
: 0;
// 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}`);
// Display Context Economics section only if at least one token setting is enabled
const showContextEconomics = config.showReadTokens || config.showWorkTokens ||
config.showSavingsAmount || config.showSavingsPercent;
if (showContextEconomics) {
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 && (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
@@ -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: 'observation'; data: Observation }
| { type: 'summary'; data: SummaryTimelineItem };
@@ -418,7 +623,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
// Render observation
const obs = item.data;
const files = parseJsonArray(obs.files_modified);
const file = files.length > 0 ? toRelativePath(files[0], cwd) : 'General';
const file = (files.length > 0 && files[0]) ? toRelativePath(files[0], cwd) : 'General';
// Check if we need a new file section
if (file !== currentFile) {
@@ -449,29 +654,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
const title = obs.title || 'Untitled';
// Map observation type to emoji icon
let icon = '•';
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 = '•';
}
const icon = TYPE_ICON_MAP[obs.type as keyof typeof TYPE_ICON_MAP] || '•';
// Section 2: Calculate read tokens (estimate from observation size)
const obsSize = (obs.title?.length || 0) +
@@ -484,21 +667,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
const discoveryTokens = obs.discovery_tokens || 0;
// Map observation type to work emoji
let workEmoji = '🔍'; // default to research/discovery
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 workEmoji = TYPE_WORK_EMOJI_MAP[obs.type as keyof typeof TYPE_WORK_EMOJI_MAP] || '🔍';
const discoveryDisplay = discoveryTokens > 0 ? `${workEmoji} ${discoveryTokens.toLocaleString()}` : '-';
@@ -506,13 +675,68 @@ 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 = (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 {
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 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,8 +764,25 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
output.push(...renderSummaryField('Next Steps', mostRecentSummary.next_steps, colors.magenta, useColors));
}
// Footer with token savings message
if (totalDiscoveryTokens > 0 && savings > 0) {
// Previously section (last assistant message from prior session) - positioned at bottom for chronological sense
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);
output.push('');
if (useColors) {
@@ -551,10 +793,22 @@ 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();
}
// Export for use by worker service
export { contextHook };
// Entry Point - handle stdin/stdout
const forceColors = process.argv.includes('--colors');
+18
View File
@@ -53,6 +53,23 @@ try {
const now = new Date();
const amaEndDate = new Date('2025-12-06T00:00:00Z'); // Dec 5, 2025 7pm EST
// Product Hunt launch announcement - expires Dec 5, 2025 12am EST (05:00 UTC)
const phLaunchEndDate = new Date('2025-12-05T05:00:00Z');
let productHuntAnnouncement = "";
if (now < phLaunchEndDate) {
productHuntAnnouncement = `
🚀 🚀
We launched on Product Hunt!
https://tinyurl.com/claude-mem-ph
Your upvote means the world - thank you!
🚀 🚀
`;
}
let amaAnnouncement = "";
if (now < amaEndDate) {
// Check if we're during the live event (Dec 1-5, 5pm-7pm EST daily)
@@ -79,6 +96,7 @@ try {
output +
"\n\n💡 New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history.\n" +
"\n💬 Community https://discord.gg/J4wttp9vDu" +
productHuntAnnouncement +
amaAnnouncement +
`\n📺 Watch live in browser http://localhost:${port}/\n`
);
+12
View File
@@ -233,6 +233,18 @@ Hello memory agent, you are continuing to observe the primary Claude session.
You do not have access to tools. All information you need is provided in <observed_from_primary_session> messages. Create observations from what you observe - no investigation needed.
CRITICAL: Record what was LEARNED/BUILT/FIXED/DEPLOYED/CONFIGURED, not what you (the observer) are doing. Focus on deliverables and capabilities - what the system NOW DOES differently.
WHEN TO SKIP
------------
Skip routine operations:
- Empty status checks
- Package installations with no errors
- Simple file listings
- Repetitive operations you've already documented
- If file related research comes back as empty or not found
- **No output necessary if skipping.**
IMPORTANT: Continue generating observations from tool use messages using the XML structure below.
OUTPUT FORMAT
+213 -20
View File
@@ -30,6 +30,12 @@ import { SDKAgent } from './worker/SDKAgent.js';
import { PaginationHelper } from './worker/PaginationHelper.js';
import { SettingsManager } from './worker/SettingsManager.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 {
private app: express.Application;
@@ -163,6 +169,7 @@ export class WorkerService {
this.app.get('/api/prompt/:id', this.handleGetPromptById.bind(this));
this.app.get('/api/stats', this.handleGetStats.bind(this));
this.app.get('/api/projects', this.handleGetProjects.bind(this));
this.app.get('/api/processing-status', this.handleGetProcessingStatus.bind(this));
this.app.post('/api/processing', this.handleSetProcessing.bind(this));
@@ -196,6 +203,7 @@ export class WorkerService {
this.app.get('/api/search/by-type', this.handleSearchByType.bind(this));
this.app.get('/api/context/recent', this.handleGetRecentContext.bind(this));
this.app.get('/api/context/timeline', this.handleGetContextTimeline.bind(this));
this.app.get('/api/context/preview', this.handleContextPreview.bind(this));
this.app.get('/api/timeline/by-query', this.handleGetTimelineByQuery.bind(this));
this.app.get('/api/search/help', this.handleSearchHelp.bind(this));
}
@@ -812,19 +820,125 @@ export class WorkerService {
}
}
/**
* Get list of distinct projects from observations
* GET /api/projects
*/
private handleGetProjects(req: Request, res: Response): void {
try {
const db = this.dbManager.getSessionStore().db;
const rows = db.prepare(`
SELECT DISTINCT project
FROM observations
WHERE project IS NOT NULL
GROUP BY project
ORDER BY MAX(created_at_epoch) DESC
`).all() as Array<{ project: string }>;
const projects = rows.map(row => row.project);
res.json({ projects });
} catch (error) {
logger.failure('WORKER', 'Get projects failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
/**
* 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)
*/
private handleGetSettings(req: Request, res: Response): void {
try {
const settingsPath = path.join(homedir(), '.claude', 'settings.json');
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
if (!existsSync(settingsPath)) {
// Return defaults if file doesn't exist
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: 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;
}
@@ -836,7 +950,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 || 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) {
logger.failure('WORKER', 'Get settings failed', {}, error as Error);
@@ -849,11 +978,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 +990,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,8 +1002,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
const settingsPath = path.join(homedir(), '.claude', 'settings.json');
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
let settings: any = { env: {} };
if (existsSync(settingsPath)) {
@@ -886,15 +1024,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
@@ -1358,6 +1509,48 @@ export class WorkerService {
}
}
/**
* Generate context preview for settings modal
* GET /api/context/preview?project=...
*/
private async handleContextPreview(req: Request, res: Response): Promise<void> {
try {
// Dynamic import to use BUILT context-hook function
const packageRoot = getPackageRoot();
const contextHookPath = path.join(packageRoot, 'plugin', 'scripts', 'context-hook.js');
const { contextHook } = await import(contextHookPath);
// Get project from query parameter
const projectName = req.query.project as string;
if (!projectName) {
return res.status(400).json({ error: 'Project parameter is required' });
}
// Use project name as CWD (contextHook uses path.basename to get project)
const cwd = `/preview/${projectName}`;
// Generate preview context (with colors for terminal display)
const contextText = await contextHook(
{
session_id: 'preview-' + Date.now(),
cwd: cwd
},
true // useColors=true for ANSI terminal output
);
// Return as plain text
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.send(contextText);
} catch (error) {
logger.failure('WORKER', 'Context preview generation failed', {}, error as Error);
res.status(500).json({
error: 'Failed to generate context preview',
message: (error as Error).message
});
}
}
/**
* Get timeline by query (search first, then get timeline around best match)
* GET /api/timeline/by-query?query=...&mode=auto&depth_before=10&depth_after=10
+906
View File
@@ -80,6 +80,9 @@
--color-skeleton-highlight: #e8ecef;
--shadow-focus: 0 0 0 2px rgba(9, 105, 218, 0.3);
/* Font families */
--font-terminal: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
}
/* Theme Variables - Dark Mode */
@@ -146,6 +149,9 @@
--color-skeleton-highlight: #4a4540;
--shadow-focus: 0 0 0 2px rgba(88, 166, 255, 0.2);
/* Font families */
--font-terminal: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
}
/* System preference default */
@@ -213,6 +219,9 @@
--color-skeleton-highlight: #e8ecef;
--shadow-focus: 0 0 0 2px rgba(9, 105, 218, 0.3);
/* Font families */
--font-terminal: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
}
}
@@ -280,6 +289,9 @@
--color-skeleton-highlight: #505050;
--shadow-focus: 0 0 0 2px rgba(88, 166, 255, 0.2);
/* Font families */
--font-terminal: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
}
}
@@ -602,6 +614,61 @@
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
/* GitHub Stars Button - Similar to Community Button */
.github-stars-btn {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 0 14px;
height: 36px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.github-stars-btn:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.github-stars-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
/* Stars count animation */
.stars-count {
animation: countUp 0.6s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-block;
}
.stars-loading {
opacity: 0.5;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes countUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.icon-link {
display: flex;
align-items: center;
@@ -1453,6 +1520,11 @@
.community-btn {
display: none;
}
/* Hide GitHub stars button on mobile */
.github-stars-btn {
display: none;
}
}
/* Mobile Responsive Styles - 480px and below */
@@ -1590,6 +1662,840 @@
height: 44px;
}
}
/* Context Settings Modal - Modern Clean Design */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s ease-out;
padding: 20px;
}
.context-settings-modal {
background: var(--color-bg-primary);
border: 1px solid var(--color-border-primary);
border-radius: 12px;
width: 100%;
max-width: 1200px;
height: 90vh;
max-height: 800px;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.modal-header {
padding: 14px 20px;
border-bottom: 1px solid var(--color-border-primary);
background: var(--color-bg-header);
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-shrink: 0;
}
.modal-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-header);
letter-spacing: -0.01em;
flex-shrink: 0;
}
.header-controls {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
justify-content: flex-end;
}
.preview-selector {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--color-text-secondary);
white-space: nowrap;
}
.preview-selector select {
background: var(--color-bg-card);
border: 1px solid var(--color-border-primary);
color: var(--color-text-primary);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.preview-selector select:hover {
border-color: var(--color-border-focus);
background: var(--color-bg-card-hover);
}
.preview-selector select:focus {
outline: none;
border-color: var(--color-accent-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.modal-close-btn {
background: transparent;
border: 1px solid var(--color-border-primary);
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
padding: 0;
}
.modal-close-btn:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: scale(1.05);
}
.modal-close-btn:active {
transform: scale(0.95);
}
.modal-icon-link {
background: transparent;
border: 1px solid var(--color-border-primary);
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
padding: 0;
text-decoration: none;
}
.modal-icon-link:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-focus);
color: var(--color-text-primary);
transform: scale(1.05);
}
.modal-icon-link:active {
transform: scale(0.95);
}
.modal-body {
flex: 1;
display: grid;
grid-template-columns: 70fr 30fr;
gap: 0;
overflow: hidden;
min-height: 0;
}
/* Preview Column - Terminal Style */
.preview-column {
padding: 20px;
overflow: hidden;
border-right: none;
background: transparent;
display: flex;
flex-direction: column;
}
.preview-column-header {
padding: 16px 20px;
background: #141414;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
flex-shrink: 0;
}
.preview-column-header label {
display: block;
font-size: 11px;
font-weight: 600;
color: #888;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.preview-column-header select {
width: 100%;
background: #0a0a0a;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
padding: 8px 12px;
height: 36px;
font-size: 13px;
font-weight: 500;
color: #ddd;
cursor: pointer;
transition: all 0.2s;
}
.preview-column-header select:hover {
border-color: rgba(255, 255, 255, 0.2);
background: #111;
}
.preview-column-header select:focus {
outline: none;
border-color: var(--color-accent-primary);
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
}
.preview-content {
flex: 1;
overflow-y: auto;
padding: 20px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 13px;
line-height: 1.6;
color: #ccc;
}
.preview-content pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Settings Column */
.settings-column {
padding: 0;
overflow-y: auto;
background: var(--color-bg-primary);
position: relative;
}
/* Custom Scrollbar */
.settings-column::-webkit-scrollbar {
width: 8px;
}
.settings-column::-webkit-scrollbar-track {
background: transparent;
}
.settings-column::-webkit-scrollbar-thumb {
background: var(--color-bg-scrollbar-thumb);
border-radius: 4px;
}
.settings-column::-webkit-scrollbar-thumb:hover {
background: var(--color-bg-scrollbar-thumb-hover);
}
.preview-content::-webkit-scrollbar {
width: 8px;
}
.preview-content::-webkit-scrollbar-track {
background: transparent;
}
.preview-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
.preview-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
/* Settings Groups - Compact */
.settings-group {
padding: 14px 16px;
border-bottom: 1px solid var(--color-border-primary);
}
.settings-group:last-child {
border-bottom: none;
}
.settings-group h4 {
margin: 0 0 10px 0;
font-size: 10px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.8px;
}
/* Filter Chips - Compact */
.chips-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chip {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 5px 10px;
min-height: 28px;
border: 1px solid var(--color-border-primary);
border-radius: 4px;
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
background: var(--color-bg-card);
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
}
.chip:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-border-hover);
color: var(--color-text-primary);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.chip:active {
transform: translateY(0);
}
.chip.selected {
background: linear-gradient(135deg, var(--color-bg-button) 0%, var(--color-accent-primary) 100%);
color: white;
border-color: var(--color-bg-button);
box-shadow: 0 2px 8px rgba(9, 105, 218, 0.25);
}
.chip.selected:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(9, 105, 218, 0.35);
}
/* Form Controls in Modal - Compact */
.settings-group input[type="number"],
.settings-group select {
width: 100%;
background: var(--color-bg-input);
border: 1px solid var(--color-border-primary);
border-radius: 4px;
padding: 6px 10px;
height: 32px;
font-size: 12px;
color: var(--color-text-primary);
transition: all 0.2s;
margin-top: 4px;
}
.settings-group input[type="number"]:hover,
.settings-group select:hover {
border-color: var(--color-border-hover);
}
.settings-group input[type="number"]:focus,
.settings-group select:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
}
.settings-group label {
display: block;
font-size: 11px;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: 4px;
}
/* Checkboxes - Compact */
.settings-group input[type="checkbox"] {
width: 14px;
height: 14px;
cursor: pointer;
margin-right: 6px;
accent-color: var(--color-accent-primary);
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 4px;
}
.checkbox-item {
display: flex;
align-items: center;
cursor: pointer;
padding: 4px 0;
}
.checkbox-item label {
margin: 0;
cursor: pointer;
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
}
.checkbox-item:hover label {
color: var(--color-text-primary);
}
/* Number Input Group - Compact */
.number-input-group {
margin-top: 6px;
}
.select-group {
margin-top: 6px;
}
.number-input-group + .number-input-group,
.select-group + .number-input-group,
.number-input-group + .select-group {
margin-top: 10px;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* ============================================
NEW: Collapsible Sections
============================================ */
.settings-section-collapsible {
border-bottom: 1px solid var(--color-border-primary);
}
.settings-section-collapsible:last-child {
border-bottom: none;
}
.section-header-btn {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
transition: background 0.15s ease;
}
.section-header-btn:hover {
background: var(--color-bg-card-hover);
}
.section-header-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
letter-spacing: -0.01em;
}
.section-description {
font-size: 11px;
color: var(--color-text-muted);
font-weight: 400;
}
.chevron-icon {
color: var(--color-text-muted);
transition: transform 0.2s ease;
flex-shrink: 0;
}
.chevron-icon.rotated {
transform: rotate(180deg);
}
.section-content {
padding: 0 16px 16px 16px;
}
/* ============================================
NEW: Chip Groups with All/None
============================================ */
.chip-group {
margin-bottom: 14px;
}
.chip-group:last-child {
margin-bottom: 0;
}
.chip-group-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.chip-group-label {
font-size: 11px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.chip-group-actions {
display: flex;
gap: 4px;
}
.chip-action {
padding: 2px 8px;
font-size: 10px;
font-weight: 500;
color: var(--color-text-muted);
background: transparent;
border: 1px solid var(--color-border-primary);
border-radius: 3px;
cursor: pointer;
transition: all 0.15s ease;
}
.chip-action:hover {
color: var(--color-text-primary);
border-color: var(--color-border-hover);
background: var(--color-bg-card-hover);
}
.chip-action.active {
color: var(--color-accent-primary);
border-color: var(--color-accent-primary);
background: var(--color-type-badge-bg);
}
/* ============================================
NEW: Form Fields with Tooltips
============================================ */
.form-field {
margin-bottom: 12px;
}
.form-field:last-child {
margin-bottom: 0;
}
.form-field-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: 6px;
}
.tooltip-trigger {
display: inline-flex;
align-items: center;
color: var(--color-text-muted);
cursor: help;
transition: color 0.15s ease;
}
.tooltip-trigger:hover {
color: var(--color-accent-primary);
}
.form-field input[type="number"],
.form-field select {
width: 100%;
background: var(--color-bg-input);
border: 1px solid var(--color-border-primary);
border-radius: 6px;
padding: 8px 12px;
height: 36px;
font-size: 13px;
color: var(--color-text-primary);
transition: all 0.15s ease;
}
.form-field input[type="number"]:hover,
.form-field select:hover {
border-color: var(--color-border-hover);
}
.form-field input[type="number"]:focus,
.form-field select:focus {
outline: none;
border-color: var(--color-border-focus);
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
}
/* ============================================
NEW: Toggle Switches
============================================ */
.toggle-group {
display: flex;
flex-direction: column;
gap: 2px;
}
.toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid var(--color-border-secondary);
}
.toggle-row:last-child {
border-bottom: none;
}
.toggle-info {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 0;
}
.toggle-label {
font-size: 12px;
font-weight: 500;
color: var(--color-text-primary);
cursor: pointer;
}
.toggle-description {
font-size: 11px;
color: var(--color-text-muted);
line-height: 1.3;
}
.toggle-switch {
position: relative;
width: 40px;
height: 22px;
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border-primary);
border-radius: 11px;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
margin-left: 12px;
padding: 0;
}
.toggle-switch:hover:not(.disabled) {
border-color: var(--color-border-hover);
}
.toggle-switch.on {
background: var(--color-accent-primary);
border-color: var(--color-accent-primary);
}
.toggle-switch.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.toggle-knob {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
transition: transform 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.toggle-switch.on .toggle-knob {
transform: translateX(18px);
}
/* ============================================
NEW: Display Subsections
============================================ */
.display-subsection {
padding: 12px 0;
border-bottom: 1px solid var(--color-border-secondary);
}
.display-subsection:first-child {
padding-top: 0;
}
.display-subsection:last-child {
border-bottom: none;
padding-bottom: 0;
}
.subsection-label {
display: block;
font-size: 11px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
/* ============================================
Improved Chip Styles
============================================ */
.chip {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 12px;
min-height: 30px;
border: 1px solid var(--color-border-primary);
border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
background: var(--color-bg-card);
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
}
.chip:hover {
background: var(--color-bg-card-hover);
border-color: var(--color-accent-primary);
color: var(--color-text-primary);
}
.chip:active {
transform: scale(0.98);
}
.chip.selected {
background: var(--color-accent-primary);
color: white;
border-color: var(--color-accent-primary);
}
.chip.selected:hover {
background: var(--color-bg-button-hover);
border-color: var(--color-bg-button-hover);
}
/* Responsive Modal */
@media (max-width: 900px) {
.modal-body {
grid-template-columns: 1fr;
}
.preview-column {
display: none;
}
}
@media (max-width: 600px) {
.modal-backdrop {
padding: 0;
}
.context-settings-modal {
border-radius: 0;
height: 100vh;
max-height: none;
}
.modal-header {
padding: 12px 16px;
gap: 12px;
}
.preview-selector {
font-size: 11px;
gap: 6px;
}
.preview-selector select {
padding: 5px 10px;
font-size: 11px;
}
.settings-group {
padding: 14px 16px;
}
.section-header-btn {
padding: 12px 14px;
}
.section-content {
padding: 0 14px 14px 14px;
}
.toggle-row {
padding: 8px 0;
}
.toggle-switch {
width: 36px;
height: 20px;
}
.toggle-knob {
width: 14px;
height: 14px;
}
.toggle-switch.on .toggle-knob {
transform: translateX(16px);
}
}
</style>
</head>
+10 -19
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Header } from './components/Header';
import { Feed } from './components/Feed';
import { Sidebar } from './components/Sidebar';
import { ContextSettingsModal } from './components/ContextSettingsModal';
import { useSSE } from './hooks/useSSE';
import { useSettings } from './hooks/useSettings';
import { useStats } from './hooks/useStats';
@@ -12,7 +12,7 @@ import { mergeAndDeduplicateByProject } from './utils/data';
export function App() {
const [currentFilter, setCurrentFilter] = useState('');
const [sidebarOpen, setSidebarOpen] = useState(false);
const [contextPreviewOpen, setContextPreviewOpen] = useState(false);
const [paginatedObservations, setPaginatedObservations] = useState<Observation[]>([]);
const [paginatedSummaries, setPaginatedSummaries] = useState<Summary[]>([]);
const [paginatedPrompts, setPaginatedPrompts] = useState<UserPrompt[]>([]);
@@ -48,9 +48,9 @@ export function App() {
return mergeAndDeduplicateByProject(prompts, paginatedPrompts);
}, [prompts, paginatedPrompts, currentFilter]);
// Toggle sidebar
const toggleSidebar = useCallback(() => {
setSidebarOpen(prev => !prev);
// Toggle context preview modal
const toggleContextPreview = useCallback(() => {
setContextPreviewOpen(prev => !prev);
}, []);
// Handle loading more data
@@ -92,15 +92,13 @@ export function App() {
projects={projects}
currentFilter={currentFilter}
onFilterChange={setCurrentFilter}
onSettingsToggle={toggleSidebar}
sidebarOpen={sidebarOpen}
isProcessing={isProcessing}
queueDepth={queueDepth}
themePreference={preference}
onThemeChange={setThemePreference}
onContextPreviewToggle={toggleContextPreview}
/>
<Feed
observations={allObservations}
summaries={allSummaries}
@@ -110,20 +108,13 @@ export function App() {
hasMore={pagination.observations.hasMore || pagination.summaries.hasMore || pagination.prompts.hasMore}
/>
<Sidebar
isOpen={sidebarOpen}
<ContextSettingsModal
isOpen={contextPreviewOpen}
onClose={toggleContextPreview}
settings={settings}
stats={stats}
onSave={saveSettings}
isSaving={isSaving}
saveStatus={saveStatus}
isConnected={isConnected}
projects={projects}
currentFilter={currentFilter}
onFilterChange={setCurrentFilter}
onSave={saveSettings}
onClose={toggleSidebar}
onRefreshStats={refreshStats}
/>
</>
);
@@ -0,0 +1,552 @@
import React, { useState, useCallback, useEffect } from 'react';
import type { Settings } from '../types';
import { TerminalPreview } from './TerminalPreview';
import { useContextPreview } from '../hooks/useContextPreview';
interface ContextSettingsModalProps {
isOpen: boolean;
onClose: () => void;
settings: Settings;
onSave: (settings: Settings) => void;
isSaving: boolean;
saveStatus: string;
}
// Simple debounce helper
function debounce<T extends (...args: any[]) => any>(fn: T, ms: number): T {
let timeoutId: NodeJS.Timeout;
return ((...args: any[]) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), ms);
}) as T;
}
// Collapsible section component
function CollapsibleSection({
title,
description,
children,
defaultOpen = true
}: {
title: string;
description?: string;
children: React.ReactNode;
defaultOpen?: boolean;
}) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className={`settings-section-collapsible ${isOpen ? 'open' : ''}`}>
<button
className="section-header-btn"
onClick={() => setIsOpen(!isOpen)}
type="button"
>
<div className="section-header-content">
<span className="section-title">{title}</span>
{description && <span className="section-description">{description}</span>}
</div>
<svg
className={`chevron-icon ${isOpen ? 'rotated' : ''}`}
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
{isOpen && <div className="section-content">{children}</div>}
</div>
);
}
// Chip group with select all/none
function ChipGroup({
label,
options,
selectedValues,
onToggle,
onSelectAll,
onSelectNone
}: {
label: string;
options: string[];
selectedValues: string[];
onToggle: (value: string) => void;
onSelectAll: () => void;
onSelectNone: () => void;
}) {
const allSelected = options.every(opt => selectedValues.includes(opt));
const noneSelected = options.every(opt => !selectedValues.includes(opt));
return (
<div className="chip-group">
<div className="chip-group-header">
<span className="chip-group-label">{label}</span>
<div className="chip-group-actions">
<button
type="button"
className={`chip-action ${allSelected ? 'active' : ''}`}
onClick={onSelectAll}
>
All
</button>
<button
type="button"
className={`chip-action ${noneSelected ? 'active' : ''}`}
onClick={onSelectNone}
>
None
</button>
</div>
</div>
<div className="chips-container">
{options.map(option => (
<button
key={option}
type="button"
className={`chip ${selectedValues.includes(option) ? 'selected' : ''}`}
onClick={() => onToggle(option)}
>
{option}
</button>
))}
</div>
</div>
);
}
// Form field with optional tooltip
function FormField({
label,
tooltip,
children
}: {
label: string;
tooltip?: string;
children: React.ReactNode;
}) {
return (
<div className="form-field">
<label className="form-field-label">
{label}
{tooltip && (
<span className="tooltip-trigger" title={tooltip}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
</span>
)}
</label>
{children}
</div>
);
}
// Toggle switch component
function ToggleSwitch({
id,
label,
description,
checked,
onChange,
disabled
}: {
id: string;
label: string;
description?: string;
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
}) {
return (
<div className="toggle-row">
<div className="toggle-info">
<label htmlFor={id} className="toggle-label">{label}</label>
{description && <span className="toggle-description">{description}</span>}
</div>
<button
type="button"
id={id}
role="switch"
aria-checked={checked}
className={`toggle-switch ${checked ? 'on' : ''} ${disabled ? 'disabled' : ''}`}
onClick={() => !disabled && onChange(!checked)}
disabled={disabled}
>
<span className="toggle-knob" />
</button>
</div>
);
}
export function ContextSettingsModal({
isOpen,
onClose,
settings,
onSave,
isSaving,
saveStatus
}: ContextSettingsModalProps) {
const [formState, setFormState] = useState<Settings>(settings);
// MCP toggle state
const [mcpEnabled, setMcpEnabled] = useState(true);
const [mcpToggling, setMcpToggling] = useState(false);
const [mcpStatus, setMcpStatus] = useState('');
// Create debounced save function
const debouncedSave = useCallback(
debounce((newSettings: Settings) => {
onSave(newSettings);
}, 300),
[onSave]
);
// Update form state when settings prop changes
useEffect(() => {
setFormState(settings);
}, [settings]);
// Fetch MCP status on mount
useEffect(() => {
fetch('/api/mcp/status')
.then(res => res.json())
.then(data => setMcpEnabled(data.enabled))
.catch(error => console.error('Failed to load MCP status:', error));
}, []);
// Get context preview based on current form state
const { preview, isLoading, error, projects, selectedProject, setSelectedProject } = useContextPreview(formState);
const updateSetting = useCallback((key: keyof Settings, value: string) => {
const newState = { ...formState, [key]: value };
setFormState(newState);
debouncedSave(newState);
}, [formState, debouncedSave]);
const toggleBoolean = useCallback((key: keyof Settings) => {
const currentValue = formState[key];
const newValue = currentValue === 'true' ? 'false' : 'true';
updateSetting(key, newValue);
}, [formState, updateSetting]);
const toggleArrayValue = useCallback((key: keyof Settings, value: string) => {
const currentValue = formState[key] || '';
const currentArray = currentValue ? currentValue.split(',') : [];
const newArray = currentArray.includes(value)
? currentArray.filter(v => v !== value)
: [...currentArray, value];
updateSetting(key, newArray.join(','));
}, [formState, updateSetting]);
const getArrayValues = useCallback((key: keyof Settings): string[] => {
const currentValue = formState[key] || '';
return currentValue ? currentValue.split(',') : [];
}, [formState]);
const setAllArrayValues = useCallback((key: keyof Settings, values: string[]) => {
updateSetting(key, values.join(','));
}, [updateSetting]);
// Handle MCP toggle
const handleMcpToggle = async (enabled: boolean) => {
setMcpToggling(true);
setMcpStatus('Toggling...');
try {
const response = await fetch('/api/mcp/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled })
});
const result = await response.json();
if (result.success) {
setMcpEnabled(result.enabled);
setMcpStatus('Updated (restart to apply)');
setTimeout(() => setMcpStatus(''), 3000);
} else {
setMcpStatus(`Error: ${result.error}`);
setTimeout(() => setMcpStatus(''), 3000);
}
} catch (err) {
setMcpStatus(`Error: ${err instanceof Error ? err.message : 'Unknown error'}`);
setTimeout(() => setMcpStatus(''), 3000);
} finally {
setMcpToggling(false);
}
};
// Handle ESC key
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (isOpen) {
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}
}, [isOpen, onClose]);
if (!isOpen) return null;
const observationTypes = ['bugfix', 'feature', 'refactor', 'discovery', 'decision', 'change'];
const observationConcepts = ['how-it-works', 'why-it-exists', 'what-changed', 'problem-solution', 'gotcha', 'pattern', 'trade-off'];
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="context-settings-modal" onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div className="modal-header">
<h2>Settings</h2>
<div className="header-controls">
<a
href="https://docs.claude-mem.ai"
target="_blank"
rel="noopener noreferrer"
title="Documentation"
className="modal-icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
</a>
<a
href="https://x.com/Claude_Memory"
target="_blank"
rel="noopener noreferrer"
title="X (Twitter)"
className="modal-icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
<label className="preview-selector">
Preview for:
<select
value={selectedProject || ''}
onChange={(e) => setSelectedProject(e.target.value)}
>
{projects.map(project => (
<option key={project} value={project}>{project}</option>
))}
</select>
</label>
<button
onClick={onClose}
className="modal-close-btn"
title="Close (Esc)"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</div>
{/* Body - 2 columns */}
<div className="modal-body">
{/* Left column - Terminal Preview */}
<div className="preview-column">
<div className="preview-content">
{error ? (
<div style={{ color: '#ff6b6b' }}>
Error loading preview: {error}
</div>
) : (
<TerminalPreview content={preview} isLoading={isLoading} />
)}
</div>
</div>
{/* Right column - Settings Panel */}
<div className="settings-column">
{/* Section 1: Loading */}
<CollapsibleSection
title="Loading"
description="How many observations to inject"
>
<FormField
label="Observations"
tooltip="Number of recent observations to include in context (1-200)"
>
<input
type="number"
min="1"
max="200"
value={formState.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50'}
onChange={(e) => updateSetting('CLAUDE_MEM_CONTEXT_OBSERVATIONS', e.target.value)}
/>
</FormField>
<FormField
label="Sessions"
tooltip="Number of recent sessions to pull observations from (1-50)"
>
<input
type="number"
min="1"
max="50"
value={formState.CLAUDE_MEM_CONTEXT_SESSION_COUNT || '10'}
onChange={(e) => updateSetting('CLAUDE_MEM_CONTEXT_SESSION_COUNT', e.target.value)}
/>
</FormField>
</CollapsibleSection>
{/* Section 2: Filters */}
<CollapsibleSection
title="Filters"
description="Which observation types to include"
>
<ChipGroup
label="Type"
options={observationTypes}
selectedValues={getArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES')}
onToggle={(value) => toggleArrayValue('CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES', value)}
onSelectAll={() => setAllArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES', observationTypes)}
onSelectNone={() => setAllArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES', [])}
/>
<ChipGroup
label="Concept"
options={observationConcepts}
selectedValues={getArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS')}
onToggle={(value) => toggleArrayValue('CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS', value)}
onSelectAll={() => setAllArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS', observationConcepts)}
onSelectNone={() => setAllArrayValues('CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS', [])}
/>
</CollapsibleSection>
{/* Section 3: Display */}
<CollapsibleSection
title="Display"
description="What to show in context tables"
>
<div className="display-subsection">
<span className="subsection-label">Full Observations</span>
<FormField
label="Count"
tooltip="How many observations show expanded details (0-20)"
>
<input
type="number"
min="0"
max="20"
value={formState.CLAUDE_MEM_CONTEXT_FULL_COUNT || '5'}
onChange={(e) => updateSetting('CLAUDE_MEM_CONTEXT_FULL_COUNT', e.target.value)}
/>
</FormField>
<FormField
label="Field"
tooltip="Which field to expand for full observations"
>
<select
value={formState.CLAUDE_MEM_CONTEXT_FULL_FIELD || 'narrative'}
onChange={(e) => updateSetting('CLAUDE_MEM_CONTEXT_FULL_FIELD', e.target.value)}
>
<option value="narrative">Narrative</option>
<option value="facts">Facts</option>
</select>
</FormField>
</div>
<div className="display-subsection">
<span className="subsection-label">Token Economics</span>
<div className="toggle-group">
<ToggleSwitch
id="show-read-tokens"
label="Read cost"
description="Tokens to read this observation"
checked={formState.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS === 'true'}
onChange={() => toggleBoolean('CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS')}
/>
<ToggleSwitch
id="show-work-tokens"
label="Work investment"
description="Tokens spent creating this observation"
checked={formState.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS === 'true'}
onChange={() => toggleBoolean('CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS')}
/>
<ToggleSwitch
id="show-savings-amount"
label="Savings"
description="Total tokens saved by reusing context"
checked={formState.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT === 'true'}
onChange={() => toggleBoolean('CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT')}
/>
</div>
</div>
</CollapsibleSection>
{/* Section 4: Advanced */}
<CollapsibleSection
title="Advanced"
description="Model selection and integrations"
defaultOpen={false}
>
<FormField
label="Model"
tooltip="AI model used for generating observations"
>
<select
value={formState.CLAUDE_MEM_MODEL || 'claude-haiku-4-5'}
onChange={(e) => updateSetting('CLAUDE_MEM_MODEL', e.target.value)}
>
<option value="claude-haiku-4-5">claude-haiku-4-5 (fastest)</option>
<option value="claude-sonnet-4-5">claude-sonnet-4-5 (balanced)</option>
<option value="claude-opus-4">claude-opus-4 (highest quality)</option>
</select>
</FormField>
<FormField
label="Worker Port"
tooltip="Port for the background worker service"
>
<input
type="number"
min="1024"
max="65535"
value={formState.CLAUDE_MEM_WORKER_PORT || '37777'}
onChange={(e) => updateSetting('CLAUDE_MEM_WORKER_PORT', e.target.value)}
/>
</FormField>
<div className="toggle-group" style={{ marginTop: '12px' }}>
<ToggleSwitch
id="mcp-enabled"
label="MCP search server"
description={mcpStatus || "Enable Model Context Protocol search"}
checked={mcpEnabled}
onChange={handleMcpToggle}
disabled={mcpToggling}
/>
<ToggleSwitch
id="show-last-summary"
label="Include last summary"
description="Add previous session's summary to context"
checked={formState.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY === 'true'}
onChange={() => toggleBoolean('CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY')}
/>
<ToggleSwitch
id="show-last-message"
label="Include last message"
description="Add previous session's final message"
checked={formState.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE === 'true'}
onChange={() => toggleBoolean('CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE')}
/>
</div>
</CollapsibleSection>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,51 @@
import React from 'react';
import { useGitHubStars } from '../hooks/useGitHubStars';
import { formatStarCount } from '../utils/formatNumber';
interface GitHubStarsButtonProps {
username: string;
repo: string;
className?: string;
}
export function GitHubStarsButton({ username, repo, className = '' }: GitHubStarsButtonProps) {
const { stars, isLoading, error } = useGitHubStars(username, repo);
const repoUrl = `https://github.com/${username}/${repo}`;
// Graceful degradation: on error, show just the icon (like original static link)
if (error) {
return (
<a
href={repoUrl}
target="_blank"
rel="noopener noreferrer"
title="GitHub"
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
);
}
return (
<a
href={repoUrl}
target="_blank"
rel="noopener noreferrer"
className={`github-stars-btn ${className}`}
title={`Star us on GitHub${stars !== null ? ` (${stars.toLocaleString()} stars)` : ''}`}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ marginRight: '6px' }}>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" style={{ marginRight: '4px' }}>
<path d="M12 .587l3.668 7.431 8.2 1.192-5.934 5.787 1.4 8.166L12 18.896l-7.334 3.867 1.4-8.166-5.934-5.787 8.2-1.192z"/>
</svg>
<span className={isLoading ? 'stars-loading' : 'stars-count'}>
{isLoading ? '...' : (stars !== null ? formatStarCount(stars) : '—')}
</span>
</a>
);
}
+24 -39
View File
@@ -1,18 +1,18 @@
import React from 'react';
import { ThemeToggle } from './ThemeToggle';
import { ThemePreference } from '../hooks/useTheme';
import { GitHubStarsButton } from './GitHubStarsButton';
interface HeaderProps {
isConnected: boolean;
projects: string[];
currentFilter: string;
onFilterChange: (filter: string) => void;
onSettingsToggle: () => void;
sidebarOpen: boolean;
isProcessing: boolean;
queueDepth: number;
themePreference: ThemePreference;
onThemeChange: (theme: ThemePreference) => void;
onContextPreviewToggle: () => void;
}
export function Header({
@@ -20,13 +20,18 @@ export function Header({
projects,
currentFilter,
onFilterChange,
onSettingsToggle,
sidebarOpen,
isProcessing,
queueDepth,
themePreference,
onThemeChange
onThemeChange,
onContextPreviewToggle
}: HeaderProps) {
// Resolve effective theme for Product Hunt badge
const isDark = themePreference === 'dark' ||
(themePreference === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
const phBadgeTheme = isDark ? 'dark' : 'light';
const phBadgeUrl = `https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1045833&theme=${phBadgeTheme}`;
return (
<div className="header">
<h1>
@@ -42,50 +47,30 @@ export function Header({
</h1>
<div className="status">
<a
href="https://docs.claude-mem.ai"
href="https://www.producthunt.com/products/claude-mem?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-claude-mem"
target="_blank"
rel="noopener noreferrer"
title="Documentation"
className="icon-link"
style={{ display: 'flex', alignItems: 'center' }}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
</a>
<a
href="https://github.com/thedotmack/claude-mem/"
target="_blank"
rel="noopener noreferrer"
title="GitHub"
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
<a
href="https://x.com/Claude_Memory"
target="_blank"
rel="noopener noreferrer"
title="X (Twitter)"
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
<img
src={phBadgeUrl}
alt="Claude-Mem on Product Hunt"
style={{ width: '180px', height: '40px' }}
width="180"
height="40"
/>
</a>
<GitHubStarsButton username="thedotmack" repo="claude-mem" />
<a
href="https://discord.gg/J4wttp9vDu"
target="_blank"
rel="noopener noreferrer"
className="community-btn"
className="icon-link"
title="Join our Discord community"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ marginRight: '6px' }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
<span>Community</span>
</a>
<select
value={currentFilter}
@@ -101,8 +86,8 @@ export function Header({
onThemeChange={onThemeChange}
/>
<button
className={`settings-btn ${sidebarOpen ? 'active' : ''}`}
onClick={onSettingsToggle}
className="settings-btn"
onClick={onContextPreviewToggle}
title="Settings"
>
<svg className="settings-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+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) {
// Settings form state
const [model, setModel] = useState(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
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);
// Consolidated settings form state
const [formState, setFormState] = useState<Settings>({
CLAUDE_MEM_MODEL: settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL,
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)
const [mcpEnabled, setMcpEnabled] = useState(true);
const [mcpToggling, setMcpToggling] = useState(false);
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(() => {
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);
setFormState({
CLAUDE_MEM_MODEL: settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL,
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]);
// Fetch MCP status on mount
@@ -52,11 +83,7 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
}, [isOpen, onRefreshStats]);
const handleSave = () => {
onSave({
CLAUDE_MEM_MODEL: model,
CLAUDE_MEM_CONTEXT_OBSERVATIONS: contextObs,
CLAUDE_MEM_WORKER_PORT: workerPort
});
onSave(formState);
};
const handleMcpToggle = async (enabled: boolean) => {
@@ -193,8 +220,8 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
</div>
<select
id="model"
value={model}
onChange={e => setModel(e.target.value)}
value={formState.CLAUDE_MEM_MODEL}
onChange={e => updateFormState('CLAUDE_MEM_MODEL', e.target.value)}
>
<option value="claude-haiku-4-5">claude-haiku-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"
min="1"
max="200"
value={contextObs}
onChange={e => setContextObs(e.target.value)}
value={formState.CLAUDE_MEM_CONTEXT_OBSERVATIONS}
onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_OBSERVATIONS', e.target.value)}
/>
</div>
<div className="form-group">
@@ -225,10 +252,89 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
id="workerPort"
min="1024"
max="65535"
value={workerPort}
onChange={e => setWorkerPort(e.target.value)}
value={formState.CLAUDE_MEM_WORKER_PORT}
onChange={e => updateFormState('CLAUDE_MEM_WORKER_PORT', 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={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 && (
<div className="save-status">{saveStatus}</div>
)}
@@ -0,0 +1,136 @@
import React, { useMemo, useRef, useLayoutEffect, useState } from 'react';
import AnsiToHtml from 'ansi-to-html';
interface TerminalPreviewProps {
content: string;
isLoading?: boolean;
className?: string;
}
const ansiConverter = new AnsiToHtml({
fg: '#dcd6cc',
bg: '#252320',
newline: false,
escapeXML: true,
stream: false
});
export function TerminalPreview({ content, isLoading = false, className = '' }: TerminalPreviewProps) {
const preRef = useRef<HTMLPreElement>(null);
const scrollTopRef = useRef(0);
const [wordWrap, setWordWrap] = useState(true);
const html = useMemo(() => {
// Save scroll position before content changes
if (preRef.current) {
scrollTopRef.current = preRef.current.scrollTop;
}
if (!content) return '';
return ansiConverter.toHtml(content);
}, [content]);
// Restore scroll position after render
useLayoutEffect(() => {
if (preRef.current && scrollTopRef.current > 0) {
preRef.current.scrollTop = scrollTopRef.current;
}
}, [html]);
const preStyle: React.CSSProperties = {
padding: '16px',
margin: 0,
fontFamily: 'var(--font-terminal)',
fontSize: '12px',
lineHeight: '1.6',
overflow: 'auto',
color: 'var(--color-text-primary)',
backgroundColor: 'var(--color-bg-card)',
whiteSpace: wordWrap ? 'pre-wrap' : 'pre',
wordBreak: wordWrap ? 'break-word' : 'normal',
position: 'absolute',
inset: 0,
};
return (
<div
className={className}
style={{
backgroundColor: 'var(--color-bg-card)',
border: '1px solid var(--color-border-primary)',
borderRadius: '8px',
overflow: 'hidden',
height: '100%',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.3)'
}}
>
{/* Window chrome */}
<div
style={{
padding: '12px',
borderBottom: '1px solid var(--color-border-primary)',
display: 'flex',
gap: '6px',
alignItems: 'center',
backgroundColor: 'var(--color-bg-header)'
}}
>
<div style={{ width: '12px', height: '12px', borderRadius: '50%', backgroundColor: '#ff5f57' }} />
<div style={{ width: '12px', height: '12px', borderRadius: '50%', backgroundColor: '#ffbd2e' }} />
<div style={{ width: '12px', height: '12px', borderRadius: '50%', backgroundColor: '#28c840' }} />
<button
onClick={() => setWordWrap(!wordWrap)}
style={{
marginLeft: 'auto',
padding: '4px 8px',
fontSize: '11px',
fontWeight: 500,
color: wordWrap ? 'var(--color-text-secondary)' : 'var(--color-accent-primary)',
backgroundColor: 'transparent',
border: '1px solid',
borderColor: wordWrap ? 'var(--color-border-primary)' : 'var(--color-accent-primary)',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s',
whiteSpace: 'nowrap'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--color-accent-primary)';
e.currentTarget.style.color = 'var(--color-accent-primary)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = wordWrap ? 'var(--color-border-primary)' : 'var(--color-accent-primary)';
e.currentTarget.style.color = wordWrap ? 'var(--color-text-secondary)' : 'var(--color-accent-primary)';
}}
title={wordWrap ? 'Disable word wrap (scroll horizontally)' : 'Enable word wrap'}
>
{wordWrap ? '⤢ Wrap' : '⇄ Scroll'}
</button>
</div>
{/* Content area */}
{isLoading ? (
<div
style={{
padding: '16px',
fontFamily: 'var(--font-terminal)',
fontSize: '12px',
color: 'var(--color-text-secondary)'
}}
>
Loading preview...
</div>
) : (
<div style={{ position: 'relative', flex: 1, overflow: 'hidden' }}>
<pre
ref={preRef}
style={preStyle}
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
)}
</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;
+76
View File
@@ -0,0 +1,76 @@
import { useState, useEffect, useCallback } from 'react';
import type { Settings } from '../types';
interface UseContextPreviewResult {
preview: string;
isLoading: boolean;
error: string | null;
refresh: () => Promise<void>;
projects: string[];
selectedProject: string | null;
setSelectedProject: (project: string) => void;
}
export function useContextPreview(settings: Settings): UseContextPreviewResult {
const [preview, setPreview] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [projects, setProjects] = useState<string[]>([]);
const [selectedProject, setSelectedProject] = useState<string | null>(null);
// Fetch projects on mount
useEffect(() => {
async function fetchProjects() {
try {
const response = await fetch('/api/projects');
const data = await response.json();
if (data.projects && data.projects.length > 0) {
setProjects(data.projects);
setSelectedProject(data.projects[0]); // Default to first project
}
} catch (err) {
console.error('Failed to fetch projects:', err);
}
}
fetchProjects();
}, []);
const refresh = useCallback(async () => {
if (!selectedProject) {
setPreview('No project selected');
return;
}
setIsLoading(true);
setError(null);
try {
const params = new URLSearchParams({
project: selectedProject
});
const response = await fetch(`/api/context/preview?${params}`);
const text = await response.text();
if (response.ok) {
setPreview(text);
} else {
setError('Failed to load preview');
}
} catch (err) {
setError((err as Error).message);
} finally {
setIsLoading(false);
}
}, [selectedProject]);
// Debounced refresh when settings or selectedProject change
useEffect(() => {
const timeout = setTimeout(() => {
refresh();
}, 300);
return () => clearTimeout(timeout);
}, [settings, refresh]);
return { preview, isLoading, error, refresh, projects, selectedProject, setSelectedProject };
}
+46
View File
@@ -0,0 +1,46 @@
import { useState, useEffect, useCallback } from 'react';
export interface GitHubStarsData {
stargazers_count: number;
watchers_count: number;
forks_count: number;
}
export interface UseGitHubStarsReturn {
stars: number | null;
isLoading: boolean;
error: Error | null;
}
export function useGitHubStars(username: string, repo: string): UseGitHubStarsReturn {
const [stars, setStars] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchStars = useCallback(async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetch(`https://api.github.com/repos/${username}/${repo}`);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const data: GitHubStarsData = await response.json();
setStars(data.stargazers_count);
} catch (error) {
console.error('Failed to fetch GitHub stars:', error);
setError(error instanceof Error ? error : new Error('Unknown error'));
} finally {
setIsLoading(false);
}
}, [username, repo]);
useEffect(() => {
fetchStars();
}, [fetchStars]);
return { stars, isLoading, error };
}
+20 -1
View File
@@ -17,7 +17,26 @@ export function useSettings() {
setSettings({
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_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 => {
+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 {
+23
View File
@@ -0,0 +1,23 @@
/**
* Formats a number into compact notation with k/M suffixes
* Examples:
* 999 "999"
* 1234 "1.2k"
* 45678 "45.7k"
* 1234567 "1.2M"
*/
export function formatStarCount(count: number): string {
if (count < 1000) {
return count.toString();
}
if (count < 1000000) {
// Format as k (thousands)
const thousands = count / 1000;
return `${thousands.toFixed(1)}k`;
}
// Format as M (millions)
const millions = count / 1000000;
return `${millions.toFixed(1)}M`;
}