Compare commits

...

16 Commits

Author SHA1 Message Date
Alex Newman 213557dd6e chore: Bump version to 6.5.3
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 15:34:39 -05:00
Alex Newman e1d2ffeb02 fix: Hide console window on Windows when spawning child processes (#166)
* fix: Hide console window on Windows when spawning child processes

Add windowsHide: true to spawnSync and execSync calls to prevent
empty console windows from appearing on Windows when hooks execute.

Fixes two spawn points:
- worker-utils.ts: PM2 spawn when starting worker service
- user-message-hook.ts: Node spawn for context display

Reference: https://nodejs.org/api/child_process.html (windowsHide option)

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

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

* fix: Add windowsHide to remaining execSync calls for complete Windows console window hiding

This completes the Windows console window fix by adding `windowsHide: true` to all remaining `execSync` calls:

- src/services/worker-service.ts:220 - pgrep command for orphaned process detection
- src/services/worker-service.ts:226 - pkill command for process cleanup
- src/services/worker/SDKAgent.ts:414 - where/which claude command for finding executable

These operations are less frequent than the user-prompt hook, but should still avoid spawning console windows on Windows for a complete fix.

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

Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
2025-12-04 16:02:35 -05:00
Alex Newman 9e66a4843e docs: Update CHANGELOG.md from releases 2025-12-04 15:41:35 -05:00
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
38 changed files with 3276 additions and 928 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "6.4.9",
"version": "6.5.3",
"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"
]
}
}
}
+92
View File
@@ -4,6 +4,98 @@ 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.2] - 2025-12-04
## What's Changed
- **Upgraded better-sqlite3** from `^11.0.0` to `^12.5.0` for Node.js 25 compatibility
### Fixes
- Resolves compilation errors when installing on Node.js 25.x (#164)
## [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! 👋
+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.9
**Current Version**: 6.5.3
## 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.9",
"version": "6.5.3",
"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.9",
"version": "6.5.3",
"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
+1 -1
View File
@@ -407,7 +407,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${i.replace("project","s.project")}
ORDER BY up.created_at_epoch ASC
`;try{let g=this.db.prepare(_).all(p,c,...o),S=this.db.prepare(m).all(p,c,...o),u=this.db.prepare(T).all(p,c,...o);return{observations:g,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:u.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(g){return console.error("[SessionStore] Error querying timeline records:",g.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function K(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function L(a,e,s={}){let t=K(a,e,s);return JSON.stringify(t)}import A from"path";import{homedir as V}from"os";import{existsSync as y,readFileSync as q}from"fs";import{spawnSync as J}from"child_process";var Q=100,z=500,Z=10;function h(){try{let a=A.join(V(),".claude-mem","settings.json");if(y(a)){let e=JSON.parse(q(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function U(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(Q)})).ok}catch{return!1}}async function ee(){try{let a=N(),e=A.join(a,"ecosystem.config.cjs");if(!y(e))throw new Error(`Ecosystem config not found at ${e}`);let s=A.join(a,"node_modules",".bin","pm2"),t=process.platform==="win32"?s+".cmd":s,r=y(t)?t:"pm2",n=J(r,["start",e],{cwd:a,stdio:"pipe",encoding:"utf-8"});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed");for(let i=0;i<Z;i++)if(await new Promise(o=>setTimeout(o,z)),await U())return!0;return!1}catch{return!1}}async function w(){if(await U())return;if(!await ee()){let e=h(),s=N();throw new Error(`Worker service failed to start on port ${e}.
`;try{let g=this.db.prepare(_).all(p,c,...o),S=this.db.prepare(m).all(p,c,...o),u=this.db.prepare(T).all(p,c,...o);return{observations:g,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:u.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(g){return console.error("[SessionStore] Error querying timeline records:",g.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function K(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function L(a,e,s={}){let t=K(a,e,s);return JSON.stringify(t)}import A from"path";import{homedir as V}from"os";import{existsSync as y,readFileSync as q}from"fs";import{spawnSync as J}from"child_process";var Q=100,z=500,Z=10;function h(){try{let a=A.join(V(),".claude-mem","settings.json");if(y(a)){let e=JSON.parse(q(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function U(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(Q)})).ok}catch{return!1}}async function ee(){try{let a=N(),e=A.join(a,"ecosystem.config.cjs");if(!y(e))throw new Error(`Ecosystem config not found at ${e}`);let s=A.join(a,"node_modules",".bin","pm2"),t=process.platform==="win32"?s+".cmd":s,r=y(t)?t:"pm2",n=J(r,["start",e],{cwd:a,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed");for(let i=0;i<Z;i++)if(await new Promise(o=>setTimeout(o,z)),await U())return!0;return!1}catch{return!1}}async function w(){if(await U())return;if(!await ee()){let e=h(),s=N();throw new Error(`Worker service failed to start on port ${e}.
To start manually, run:
cd ${s}
+2 -2
View File
@@ -407,7 +407,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${i.replace("project","s.project")}
ORDER BY up.created_at_epoch ASC
`;try{let E=this.db.prepare(m).all(p,u,...o),g=this.db.prepare(T).all(p,u,...o),c=this.db.prepare(_).all(p,u,...o);return{observations:E,sessions:g.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:c.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(E){return console.error("[SessionStore] Error querying timeline records:",E.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function K(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(a,e,s={}){let t=K(a,e,s);return JSON.stringify(t)}import v from"path";import{homedir as V}from"os";import{existsSync as y,readFileSync as q}from"fs";import{spawnSync as J}from"child_process";var Q=100,z=500,Z=10;function N(){try{let a=v.join(V(),".claude-mem","settings.json");if(y(a)){let e=JSON.parse(q(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function M(){try{let a=N();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(Q)})).ok}catch{return!1}}async function ee(){try{let a=I(),e=v.join(a,"ecosystem.config.cjs");if(!y(e))throw new Error(`Ecosystem config not found at ${e}`);let s=v.join(a,"node_modules",".bin","pm2"),t=process.platform==="win32"?s+".cmd":s,r=y(t)?t:"pm2",n=J(r,["start",e],{cwd:a,stdio:"pipe",encoding:"utf-8"});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed");for(let i=0;i<Z;i++)if(await new Promise(o=>setTimeout(o,z)),await M())return!0;return!1}catch{return!1}}async function w(){if(await M())return;if(!await ee()){let e=N(),s=I();throw new Error(`Worker service failed to start on port ${e}.
`;try{let E=this.db.prepare(m).all(p,u,...o),g=this.db.prepare(T).all(p,u,...o),c=this.db.prepare(_).all(p,u,...o);return{observations:E,sessions:g.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:c.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(E){return console.error("[SessionStore] Error querying timeline records:",E.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function K(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(a,e,s={}){let t=K(a,e,s);return JSON.stringify(t)}import v from"path";import{homedir as V}from"os";import{existsSync as y,readFileSync as q}from"fs";import{spawnSync as J}from"child_process";var Q=100,z=500,Z=10;function N(){try{let a=v.join(V(),".claude-mem","settings.json");if(y(a)){let e=JSON.parse(q(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function w(){try{let a=N();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(Q)})).ok}catch{return!1}}async function ee(){try{let a=I(),e=v.join(a,"ecosystem.config.cjs");if(!y(e))throw new Error(`Ecosystem config not found at ${e}`);let s=v.join(a,"node_modules",".bin","pm2"),t=process.platform==="win32"?s+".cmd":s,r=y(t)?t:"pm2",n=J(r,["start",e],{cwd:a,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed");for(let i=0;i<Z;i++)if(await new Promise(o=>setTimeout(o,z)),await w())return!0;return!1}catch{return!1}}async function M(){if(await w())return;if(!await ee()){let e=N(),s=I();throw new Error(`Worker service failed to start on port ${e}.
To start manually, run:
cd ${s}
@@ -415,4 +415,4 @@ To start manually, run:
If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as se}from"fs";import{homedir as te}from"os";import{join as re}from"path";var oe=re(te(),".claude-mem","silent.log");function R(a,e,s=""){let t=new Date().toISOString(),o=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),p=o?`${o[1].split("/").pop()}:${o[2]}`:"unknown",u=`[${t}] [${p}] ${a}`;if(e!==void 0)try{u+=` ${JSON.stringify(e)}`}catch(m){u+=` [stringify error: ${m}]`}u+=`
`;try{se(oe,u)}catch(m){console.error("[silent-debug] Failed to write to log:",m)}return s}var F=100;function ne(a){let e=(a.match(/<private>/g)||[]).length,s=(a.match(/<claude-mem-context>/g)||[]).length;return e+s}function C(a){if(typeof a!="string")return R("[tag-stripping] received non-string for JSON context:",{type:typeof a}),"{}";let e=ne(a);return e>F&&R("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:F,contentLength:a.length}),a.replace(/<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g,"").replace(/<private>[\s\S]*?<\/private>/g,"").trim()}var ie=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function ae(a){if(!a)throw new Error("saveHook requires input");let{session_id:e,cwd:s,tool_name:t,tool_input:r,tool_response:n}=a;if(ie.has(t)){console.log(f("PostToolUse",!0));return}await w();let i=new h,o=i.createSDKSession(e,"",""),p=i.getPromptCounter(o),u=i.getUserPrompt(e,p);if(!u||u.trim()===""){R("[save-hook] Skipping observation - user prompt was entirely private",{session_id:e,promptNumber:p,tool_name:t}),i.close(),console.log(f("PostToolUse",!0));return}i.close();let m=S.formatTool(t,r),T=N();S.dataIn("HOOK",`PostToolUse: ${m}`,{sessionId:o,workerPort:T});try{let _="{}",E="{}";try{_=r!==void 0?C(JSON.stringify(r)):"{}"}catch(c){R("[save-hook] Failed to stringify tool_input:",{error:c,tool_name:t}),_='{"error": "Failed to serialize tool_input"}'}try{E=n!==void 0?C(JSON.stringify(n)):"{}"}catch(c){R("[save-hook] Failed to stringify tool_response:",{error:c,tool_name:t}),E='{"error": "Failed to serialize tool_response"}'}let g=await fetch(`http://127.0.0.1:${T}/sessions/${o}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:_,tool_response:E,prompt_number:p,cwd:s||""}),signal:AbortSignal.timeout(2e3)});if(!g.ok){let c=await g.text();throw S.failure("HOOK","Failed to send observation",{sessionId:o,status:g.status},c),new Error(`Failed to send observation to worker: ${g.status} ${c}`)}S.debug("HOOK","Observation sent successfully",{sessionId:o,toolName:t})}catch(_){throw _.cause?.code==="ECONNREFUSED"||_.name==="TimeoutError"||_.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):_}console.log(f("PostToolUse",!0))}var D="";X.on("data",a=>D+=a);X.on("end",async()=>{let a=D?JSON.parse(D):void 0;await ae(a)});
`;try{se(oe,u)}catch(m){console.error("[silent-debug] Failed to write to log:",m)}return s}var F=100;function ne(a){let e=(a.match(/<private>/g)||[]).length,s=(a.match(/<claude-mem-context>/g)||[]).length;return e+s}function C(a){if(typeof a!="string")return R("[tag-stripping] received non-string for JSON context:",{type:typeof a}),"{}";let e=ne(a);return e>F&&R("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:F,contentLength:a.length}),a.replace(/<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g,"").replace(/<private>[\s\S]*?<\/private>/g,"").trim()}var ie=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function ae(a){if(!a)throw new Error("saveHook requires input");let{session_id:e,cwd:s,tool_name:t,tool_input:r,tool_response:n}=a;if(ie.has(t)){console.log(f("PostToolUse",!0));return}await M();let i=new h,o=i.createSDKSession(e,"",""),p=i.getPromptCounter(o),u=i.getUserPrompt(e,p);if(!u||u.trim()===""){R("[save-hook] Skipping observation - user prompt was entirely private",{session_id:e,promptNumber:p,tool_name:t}),i.close(),console.log(f("PostToolUse",!0));return}i.close();let m=S.formatTool(t,r),T=N();S.dataIn("HOOK",`PostToolUse: ${m}`,{sessionId:o,workerPort:T});try{let _="{}",E="{}";try{_=r!==void 0?C(JSON.stringify(r)):"{}"}catch(c){R("[save-hook] Failed to stringify tool_input:",{error:c,tool_name:t}),_='{"error": "Failed to serialize tool_input"}'}try{E=n!==void 0?C(JSON.stringify(n)):"{}"}catch(c){R("[save-hook] Failed to stringify tool_response:",{error:c,tool_name:t}),E='{"error": "Failed to serialize tool_response"}'}let g=await fetch(`http://127.0.0.1:${T}/sessions/${o}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:_,tool_response:E,prompt_number:p,cwd:s||""}),signal:AbortSignal.timeout(2e3)});if(!g.ok){let c=await g.text();throw S.failure("HOOK","Failed to send observation",{sessionId:o,status:g.status},c),new Error(`Failed to send observation to worker: ${g.status} ${c}`)}S.debug("HOOK","Observation sent successfully",{sessionId:o,toolName:t})}catch(_){throw _.cause?.code==="ECONNREFUSED"||_.name==="TimeoutError"||_.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):_}console.log(f("PostToolUse",!0))}var D="";X.on("data",a=>D+=a);X.on("end",async()=>{let a=D?JSON.parse(D):void 0;await ae(a)});
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -407,7 +407,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
ORDER BY up.created_at_epoch ASC
`;try{let g=this.db.prepare(_).all(p,c,...i),b=this.db.prepare(u).all(p,c,...i),m=this.db.prepare(E).all(p,c,...i);return{observations:g,sessions:b.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:m.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(g){return console.error("[SessionStore] Error querying timeline records:",g.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function K(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function y(a,e,s={}){let t=K(a,e,s);return JSON.stringify(t)}import A from"path";import{homedir as q}from"os";import{existsSync as v,readFileSync as V}from"fs";import{spawnSync as J}from"child_process";var Q=100,z=500,Z=10;function h(){try{let a=A.join(q(),".claude-mem","settings.json");if(v(a)){let e=JSON.parse(V(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function U(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(Q)})).ok}catch{return!1}}async function ee(){try{let a=N(),e=A.join(a,"ecosystem.config.cjs");if(!v(e))throw new Error(`Ecosystem config not found at ${e}`);let s=A.join(a,"node_modules",".bin","pm2"),t=process.platform==="win32"?s+".cmd":s,r=v(t)?t:"pm2",n=J(r,["start",e],{cwd:a,stdio:"pipe",encoding:"utf-8"});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed");for(let o=0;o<Z;o++)if(await new Promise(i=>setTimeout(i,z)),await U())return!0;return!1}catch{return!1}}async function M(){if(await U())return;if(!await ee()){let e=h(),s=N();throw new Error(`Worker service failed to start on port ${e}.
`;try{let g=this.db.prepare(_).all(p,c,...i),b=this.db.prepare(u).all(p,c,...i),m=this.db.prepare(E).all(p,c,...i);return{observations:g,sessions:b.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:m.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(g){return console.error("[SessionStore] Error querying timeline records:",g.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function K(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function y(a,e,s={}){let t=K(a,e,s);return JSON.stringify(t)}import A from"path";import{homedir as q}from"os";import{existsSync as v,readFileSync as V}from"fs";import{spawnSync as J}from"child_process";var Q=100,z=500,Z=10;function h(){try{let a=A.join(q(),".claude-mem","settings.json");if(v(a)){let e=JSON.parse(V(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function U(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(Q)})).ok}catch{return!1}}async function ee(){try{let a=N(),e=A.join(a,"ecosystem.config.cjs");if(!v(e))throw new Error(`Ecosystem config not found at ${e}`);let s=A.join(a,"node_modules",".bin","pm2"),t=process.platform==="win32"?s+".cmd":s,r=v(t)?t:"pm2",n=J(r,["start",e],{cwd:a,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed");for(let o=0;o<Z;o++)if(await new Promise(i=>setTimeout(i,z)),await U())return!0;return!1}catch{return!1}}async function M(){if(await U())return;if(!await ee()){let e=h(),s=N();throw new Error(`Worker service failed to start on port ${e}.
To start manually, run:
cd ${s}
+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 _,basename as H}from"path";import{homedir as d}from"os";import{fileURLToPath as x}from"url";function E(){return typeof __dirname<"u"?__dirname:_(x(import.meta.url))}var j=E(),o=process.env.CLAUDE_MEM_DATA_DIR||t(d(),".claude-mem"),c=process.env.CLAUDE_CONFIG_DIR||t(d(),".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 l(){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",windowsHide:!0}),n=l(),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(),w=e.getUTCMonth(),y=e.getUTCFullYear()===2025&&w===11&&u>=1&&u<=5,T=p>=17&&p<19;y&&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>
+7 -4
View File
@@ -289,7 +289,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
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) {
@@ -393,7 +393,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
// If we have neither observations nor summaries, show empty state
if (observations.length === 0 && recentSummaries.length === 0) {
db.close();
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`;
}
@@ -623,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) {
@@ -793,7 +793,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
}
}
db.close();
db?.close();
// Add debug info directly to output
// if (debugInfo.length > 0) {
@@ -806,6 +806,9 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
return output.join('\n').trimEnd();
}
// Export for use by worker service
export { contextHook };
// Entry Point - handle stdin/stdout
const forceColors = process.argv.includes('--colors');
+20 -1
View File
@@ -44,7 +44,8 @@ try {
// Cross-platform path to context-hook.js in the installed plugin
const contextHookPath = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', 'scripts', 'context-hook.js');
const output = execSync(`node "${contextHookPath}" --colors`, {
encoding: 'utf8'
encoding: 'utf8',
windowsHide: true
});
const port = getWorkerPort();
@@ -53,6 +54,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 +97,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
+71 -2
View File
@@ -169,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));
@@ -202,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));
}
@@ -215,13 +217,13 @@ export class WorkerService {
// Find orphaned uvx processes (which spawn chroma servers)
try {
const processes = execSync('pgrep -fl uvx', { encoding: 'utf-8', stdio: 'pipe' }).trim();
const processes = execSync('pgrep -fl uvx', { encoding: 'utf-8', stdio: 'pipe', windowsHide: true }).trim();
if (processes) {
const processCount = processes.split('\n').length;
logger.info('WORKER', 'Cleaning up orphaned MCP processes', { count: processCount });
// Kill the processes
execSync('pkill -f uvx', { stdio: 'pipe' });
execSync('pkill -f uvx', { stdio: 'pipe', windowsHide: true });
logger.success('WORKER', `Cleaned up ${processCount} orphaned MCP server processes`);
}
} catch (error: any) {
@@ -818,6 +820,31 @@ 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
*/
@@ -1482,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
+1 -1
View File
@@ -411,7 +411,7 @@ export class SDKAgent {
*/
private findClaudeExecutable(): string {
const claudePath = process.env.CLAUDE_CODE_PATH ||
execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { encoding: 'utf8' })
execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { encoding: 'utf8', windowsHide: true })
.trim().split('\n')[0].trim();
if (!claudePath) {
+2 -1
View File
@@ -67,7 +67,8 @@ async function startWorker(): Promise<boolean> {
const result = spawnSync(pm2Command, ['start', ecosystemPath], {
cwd: pluginRoot,
stdio: 'pipe',
encoding: 'utf-8'
encoding: 'utf-8',
windowsHide: true
});
if (result.status !== 0) {
throw new Error(result.stderr || 'PM2 start failed');
+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">
@@ -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>
);
}
+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 };
}
+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`;
}