Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6708b3684 | |||
| 375dd1c3d6 | |||
| c78500cac2 | |||
| 2b683f99bb | |||
| 00e2b0c55f | |||
| fbd4df4285 | |||
| ca24048e15 | |||
| f9fd85fa4d |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "6.4.9",
|
||||
"version": "6.5.0",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
+21
-2065
File diff suppressed because it is too large
Load Diff
@@ -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.0
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -328,7 +328,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Generated
+27
-6
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "6.3.6",
|
||||
"version": "6.4.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-mem",
|
||||
"version": "6.3.6",
|
||||
"version": "6.4.9",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"express": "^4.18.2",
|
||||
"glob": "^11.0.3",
|
||||
@@ -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",
|
||||
@@ -2148,6 +2163,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 +2362,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 +3874,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 +4875,6 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "6.4.9",
|
||||
"version": "6.5.0",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -48,6 +48,7 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"express": "^4.18.2",
|
||||
"glob": "^11.0.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "6.4.9",
|
||||
"version": "6.5.0",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"mcpServers": {
|
||||
"claude-mem-search": {
|
||||
"type": "stdio",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.mjs"
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.cjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,12 +20,11 @@ export function Header({
|
||||
projects,
|
||||
currentFilter,
|
||||
onFilterChange,
|
||||
onSettingsToggle,
|
||||
sidebarOpen,
|
||||
isProcessing,
|
||||
queueDepth,
|
||||
themePreference,
|
||||
onThemeChange
|
||||
onThemeChange,
|
||||
onContextPreviewToggle
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<div className="header">
|
||||
@@ -41,51 +40,17 @@ export function Header({
|
||||
<span className="logo-text">claude-mem</span>
|
||||
</h1>
|
||||
<div className="status">
|
||||
<a
|
||||
href="https://docs.claude-mem.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Documentation"
|
||||
className="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://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>
|
||||
</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 +66,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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
Reference in New Issue
Block a user