mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-14 02:42:04 +08:00
feat: add support for Claude CLI tool and enhance memory features
- Added new CLI tool "Claude" with command handling in cli-executor.ts. - Implemented session discovery for Claude in native-session-discovery.ts. - Enhanced memory view with active memory controls, including sync functionality and configuration options. - Introduced zoom and fit view controls for memory graph visualization. - Updated i18n.js for new memory-related translations. - Improved error handling and migration for CLI history store.
This commit is contained in:
@@ -151,12 +151,12 @@ export function run(argv: string[]): void {
|
|||||||
// CLI command
|
// CLI command
|
||||||
program
|
program
|
||||||
.command('cli [subcommand] [args...]')
|
.command('cli [subcommand] [args...]')
|
||||||
.description('Unified CLI tool executor (gemini/qwen/codex)')
|
.description('Unified CLI tool executor (gemini/qwen/codex/claude)')
|
||||||
.option('--tool <tool>', 'CLI tool to use', 'gemini')
|
.option('--tool <tool>', 'CLI tool to use', 'gemini')
|
||||||
.option('--mode <mode>', 'Execution mode: analysis, write, auto', 'analysis')
|
.option('--mode <mode>', 'Execution mode: analysis, write, auto', 'analysis')
|
||||||
.option('--model <model>', 'Model override')
|
.option('--model <model>', 'Model override')
|
||||||
.option('--cd <path>', 'Working directory')
|
.option('--cd <path>', 'Working directory')
|
||||||
.option('--includeDirs <dirs>', 'Additional directories (--include-directories for gemini/qwen, --add-dir for codex)')
|
.option('--includeDirs <dirs>', 'Additional directories (--include-directories for gemini/qwen, --add-dir for codex/claude)')
|
||||||
.option('--timeout <ms>', 'Timeout in milliseconds', '300000')
|
.option('--timeout <ms>', 'Timeout in milliseconds', '300000')
|
||||||
.option('--no-stream', 'Disable streaming output')
|
.option('--no-stream', 'Disable streaming output')
|
||||||
.option('--limit <n>', 'History limit')
|
.option('--limit <n>', 'History limit')
|
||||||
|
|||||||
@@ -79,10 +79,11 @@ function normalizePath(filePath: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get project path from current working directory
|
* Get project path from hook data or current working directory
|
||||||
*/
|
*/
|
||||||
function getProjectPath(): string {
|
function getProjectPath(hookCwd?: string): string {
|
||||||
return process.cwd();
|
// Prefer hook's cwd (actual project workspace) over process.cwd()
|
||||||
|
return hookCwd || process.cwd();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,6 +91,7 @@ function getProjectPath(): string {
|
|||||||
*/
|
*/
|
||||||
async function trackAction(options: TrackOptions): Promise<void> {
|
async function trackAction(options: TrackOptions): Promise<void> {
|
||||||
let { type, action, value, session, stdin } = options;
|
let { type, action, value, session, stdin } = options;
|
||||||
|
let hookCwd: string | undefined;
|
||||||
|
|
||||||
// If --stdin flag is set, read from stdin (Claude Code hook format)
|
// If --stdin flag is set, read from stdin (Claude Code hook format)
|
||||||
if (stdin) {
|
if (stdin) {
|
||||||
@@ -98,6 +100,7 @@ async function trackAction(options: TrackOptions): Promise<void> {
|
|||||||
if (stdinData) {
|
if (stdinData) {
|
||||||
const hookData = JSON.parse(stdinData);
|
const hookData = JSON.parse(stdinData);
|
||||||
session = hookData.session_id || session;
|
session = hookData.session_id || session;
|
||||||
|
hookCwd = hookData.cwd; // Extract workspace path from hook
|
||||||
|
|
||||||
// Extract value based on hook event
|
// Extract value based on hook event
|
||||||
if (hookData.tool_input) {
|
if (hookData.tool_input) {
|
||||||
@@ -151,7 +154,7 @@ async function trackAction(options: TrackOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const projectPath = getProjectPath();
|
const projectPath = getProjectPath(hookCwd);
|
||||||
const store = getMemoryStore(projectPath);
|
const store = getMemoryStore(projectPath);
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const MODULE_FILES = [
|
|||||||
'dashboard-js/components/mcp-manager.js',
|
'dashboard-js/components/mcp-manager.js',
|
||||||
'dashboard-js/components/hook-manager.js',
|
'dashboard-js/components/hook-manager.js',
|
||||||
'dashboard-js/components/version-check.js',
|
'dashboard-js/components/version-check.js',
|
||||||
|
'dashboard-js/components/task-queue-sidebar.js',
|
||||||
// Views
|
// Views
|
||||||
'dashboard-js/views/home.js',
|
'dashboard-js/views/home.js',
|
||||||
'dashboard-js/views/project-overview.js',
|
'dashboard-js/views/project-overview.js',
|
||||||
|
|||||||
@@ -917,109 +917,6 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API: Memory Module - Get hotspot statistics
|
|
||||||
if (pathname === '/api/memory/stats') {
|
|
||||||
const projectPath = url.searchParams.get('path') || initialPath;
|
|
||||||
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
||||||
const type = url.searchParams.get('type') || null;
|
|
||||||
const sort = url.searchParams.get('sort') || 'heat';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const memoryStore = getMemoryStore(projectPath);
|
|
||||||
let hotEntities = memoryStore.getHotEntities(limit);
|
|
||||||
|
|
||||||
// Filter by type if specified
|
|
||||||
if (type) {
|
|
||||||
hotEntities = hotEntities.filter(e => e.type === type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by field
|
|
||||||
if (sort === 'reads') {
|
|
||||||
hotEntities.sort((a, b) => b.stats.read_count - a.stats.read_count);
|
|
||||||
} else if (sort === 'writes') {
|
|
||||||
hotEntities.sort((a, b) => b.stats.write_count - a.stats.write_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({
|
|
||||||
items: hotEntities.map(e => ({
|
|
||||||
value: e.value,
|
|
||||||
type: e.type,
|
|
||||||
read_count: e.stats.read_count,
|
|
||||||
write_count: e.stats.write_count,
|
|
||||||
mention_count: e.stats.mention_count,
|
|
||||||
heat_score: e.stats.heat_score
|
|
||||||
}))
|
|
||||||
}));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API: Memory Module - Get association graph
|
|
||||||
if (pathname === '/api/memory/graph') {
|
|
||||||
const projectPath = url.searchParams.get('path') || initialPath;
|
|
||||||
const center = url.searchParams.get('center');
|
|
||||||
const depth = parseInt(url.searchParams.get('depth') || '1', 10);
|
|
||||||
|
|
||||||
if (!center) {
|
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ error: 'center parameter is required' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const memoryStore = getMemoryStore(projectPath);
|
|
||||||
|
|
||||||
// Find the center entity (assume it's a file for now)
|
|
||||||
const entity = memoryStore.getEntity('file', center);
|
|
||||||
if (!entity) {
|
|
||||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ error: 'Entity not found' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get associations
|
|
||||||
const associations = memoryStore.getAssociations(entity.id!, 20);
|
|
||||||
const stats = memoryStore.getStats(entity.id!);
|
|
||||||
|
|
||||||
// Build graph structure
|
|
||||||
const nodes = [
|
|
||||||
{
|
|
||||||
id: entity.id!.toString(),
|
|
||||||
label: entity.value,
|
|
||||||
type: entity.type,
|
|
||||||
heat: stats?.heat_score || 0
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const links = [];
|
|
||||||
for (const assoc of associations) {
|
|
||||||
nodes.push({
|
|
||||||
id: assoc.target.id!.toString(),
|
|
||||||
label: assoc.target.value,
|
|
||||||
type: assoc.target.type,
|
|
||||||
heat: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
links.push({
|
|
||||||
source: entity.id!.toString(),
|
|
||||||
target: assoc.target.id!.toString(),
|
|
||||||
weight: assoc.weight
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ nodes, links }));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API: Memory Module - Track entity access
|
// API: Memory Module - Track entity access
|
||||||
if (pathname === '/api/memory/track' && req.method === 'POST') {
|
if (pathname === '/api/memory/track' && req.method === 'POST') {
|
||||||
handlePostRequest(req, res, async (body) => {
|
handlePostRequest(req, res, async (body) => {
|
||||||
@@ -1245,7 +1142,7 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|||||||
if (pathname === '/api/memory/insights/analyze' && req.method === 'POST') {
|
if (pathname === '/api/memory/insights/analyze' && req.method === 'POST') {
|
||||||
handlePostRequest(req, res, async (body: any) => {
|
handlePostRequest(req, res, async (body: any) => {
|
||||||
const projectPath = body.path || initialPath;
|
const projectPath = body.path || initialPath;
|
||||||
const tool = body.tool || 'gemini'; // gemini, qwen, codex
|
const tool = body.tool || 'gemini'; // gemini, qwen, codex, claude
|
||||||
const prompts = body.prompts || [];
|
const prompts = body.prompts || [];
|
||||||
const lang = body.lang || 'en'; // Language preference
|
const lang = body.lang || 'en'; // Language preference
|
||||||
|
|
||||||
@@ -1345,6 +1242,527 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: Memory Module - Get hotspot statistics
|
||||||
|
if (pathname === '/api/memory/stats') {
|
||||||
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
|
const filter = url.searchParams.get('filter') || 'all'; // today, week, all
|
||||||
|
const limit = parseInt(url.searchParams.get('limit') || '10', 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const memoryStore = getMemoryStore(projectPath);
|
||||||
|
const hotEntities = memoryStore.getHotEntities(limit * 4);
|
||||||
|
|
||||||
|
// Filter by time if needed
|
||||||
|
let filtered = hotEntities;
|
||||||
|
if (filter === 'today') {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
filtered = hotEntities.filter((e: any) => new Date(e.last_seen_at) >= today);
|
||||||
|
} else if (filter === 'week') {
|
||||||
|
const weekAgo = new Date();
|
||||||
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||||
|
filtered = hotEntities.filter((e: any) => new Date(e.last_seen_at) >= weekAgo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate into mostRead and mostEdited
|
||||||
|
const fileEntities = filtered.filter((e: any) => e.type === 'file');
|
||||||
|
|
||||||
|
const mostRead = fileEntities
|
||||||
|
.filter((e: any) => e.stats.read_count > 0)
|
||||||
|
.sort((a: any, b: any) => b.stats.read_count - a.stats.read_count)
|
||||||
|
.slice(0, limit)
|
||||||
|
.map((e: any) => ({
|
||||||
|
path: e.value,
|
||||||
|
file: e.value.split(/[/\\]/).pop(),
|
||||||
|
heat: e.stats.read_count,
|
||||||
|
count: e.stats.read_count,
|
||||||
|
lastSeen: e.last_seen_at
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mostEdited = fileEntities
|
||||||
|
.filter((e: any) => e.stats.write_count > 0)
|
||||||
|
.sort((a: any, b: any) => b.stats.write_count - a.stats.write_count)
|
||||||
|
.slice(0, limit)
|
||||||
|
.map((e: any) => ({
|
||||||
|
path: e.value,
|
||||||
|
file: e.value.split(/[/\\]/).pop(),
|
||||||
|
heat: e.stats.write_count,
|
||||||
|
count: e.stats.write_count,
|
||||||
|
lastSeen: e.last_seen_at
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ stats: { mostRead, mostEdited } }));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ stats: { mostRead: [], mostEdited: [] } }));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Memory Module - Get memory graph (file associations with modules and components)
|
||||||
|
if (pathname === '/api/memory/graph') {
|
||||||
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const memoryStore = getMemoryStore(projectPath);
|
||||||
|
const hotEntities = memoryStore.getHotEntities(100);
|
||||||
|
|
||||||
|
// Build file nodes from entities
|
||||||
|
const fileEntities = hotEntities.filter((e: any) => e.type === 'file');
|
||||||
|
const fileNodes = fileEntities.map((e: any) => {
|
||||||
|
const fileName = e.value.split(/[/\\]/).pop() || '';
|
||||||
|
// Detect component type based on file name patterns
|
||||||
|
const isComponent = /\.(tsx|jsx|vue|svelte)$/.test(fileName) ||
|
||||||
|
/^[A-Z][a-zA-Z]+\.(ts|js)$/.test(fileName) ||
|
||||||
|
fileName.includes('.component.') ||
|
||||||
|
fileName.includes('.controller.');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: e.value,
|
||||||
|
name: fileName,
|
||||||
|
path: e.value,
|
||||||
|
type: isComponent ? 'component' : 'file',
|
||||||
|
heat: Math.min(25, 8 + e.stats.heat_score / 10)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract unique modules (directories) from file paths
|
||||||
|
const moduleMap = new Map<string, { heat: number; files: string[] }>();
|
||||||
|
for (const file of fileEntities) {
|
||||||
|
const parts = file.value.split(/[/\\]/);
|
||||||
|
// Get parent directory as module (skip if root level)
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const modulePath = parts.slice(0, -1).join('/');
|
||||||
|
const moduleName = parts[parts.length - 2] || modulePath;
|
||||||
|
// Skip common non-module directories
|
||||||
|
if (['node_modules', '.git', 'dist', 'build', '.next', '.nuxt'].includes(moduleName)) continue;
|
||||||
|
|
||||||
|
if (!moduleMap.has(modulePath)) {
|
||||||
|
moduleMap.set(modulePath, { heat: 0, files: [] });
|
||||||
|
}
|
||||||
|
const mod = moduleMap.get(modulePath)!;
|
||||||
|
mod.heat += file.stats.heat_score / 20;
|
||||||
|
mod.files.push(file.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create module nodes (limit to top modules by heat)
|
||||||
|
const moduleNodes = Array.from(moduleMap.entries())
|
||||||
|
.sort((a, b) => b[1].heat - a[1].heat)
|
||||||
|
.slice(0, 15)
|
||||||
|
.map(([modulePath, data]) => ({
|
||||||
|
id: modulePath,
|
||||||
|
name: modulePath.split(/[/\\]/).pop() || modulePath,
|
||||||
|
path: modulePath,
|
||||||
|
type: 'module',
|
||||||
|
heat: Math.min(20, 12 + data.heat / 5),
|
||||||
|
fileCount: data.files.length
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Combine all nodes
|
||||||
|
const nodes = [...fileNodes, ...moduleNodes];
|
||||||
|
const nodeIds = new Set(nodes.map(n => n.id));
|
||||||
|
|
||||||
|
// Build edges from associations
|
||||||
|
const edges: any[] = [];
|
||||||
|
const edgeSet = new Set<string>(); // Prevent duplicate edges
|
||||||
|
|
||||||
|
// Add file-to-file associations
|
||||||
|
for (const entity of hotEntities) {
|
||||||
|
if (!entity.id || entity.type !== 'file') continue;
|
||||||
|
const associations = memoryStore.getAssociations(entity.id, 10);
|
||||||
|
for (const assoc of associations) {
|
||||||
|
if (assoc.target && nodeIds.has(assoc.target.value)) {
|
||||||
|
const edgeKey = [entity.value, assoc.target.value].sort().join('|');
|
||||||
|
if (!edgeSet.has(edgeKey)) {
|
||||||
|
edgeSet.add(edgeKey);
|
||||||
|
edges.push({
|
||||||
|
source: entity.value,
|
||||||
|
target: assoc.target.value,
|
||||||
|
weight: assoc.weight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file-to-module edges (files belong to their parent modules)
|
||||||
|
for (const [modulePath, data] of moduleMap.entries()) {
|
||||||
|
if (!nodeIds.has(modulePath)) continue;
|
||||||
|
for (const filePath of data.files) {
|
||||||
|
if (nodeIds.has(filePath)) {
|
||||||
|
const edgeKey = [modulePath, filePath].sort().join('|');
|
||||||
|
if (!edgeSet.has(edgeKey)) {
|
||||||
|
edgeSet.add(edgeKey);
|
||||||
|
edges.push({
|
||||||
|
source: modulePath,
|
||||||
|
target: filePath,
|
||||||
|
weight: 2 // Lower weight for structural relationships
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ graph: { nodes, edges } }));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ graph: { nodes: [], edges: [] } }));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Memory Module - Get recent context activities
|
||||||
|
if (pathname === '/api/memory/recent') {
|
||||||
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
|
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const memoryStore = getMemoryStore(projectPath);
|
||||||
|
|
||||||
|
// Get recent access logs with entity info - filter to file type only
|
||||||
|
const db = (memoryStore as any).db;
|
||||||
|
const recentLogs = db.prepare(`
|
||||||
|
SELECT a.*, e.type, e.value
|
||||||
|
FROM access_logs a
|
||||||
|
JOIN entities e ON a.entity_id = e.id
|
||||||
|
WHERE e.type = 'file'
|
||||||
|
ORDER BY a.timestamp DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(limit * 2) as any[]; // Fetch more to account for filtering
|
||||||
|
|
||||||
|
// Filter out invalid entries (JSON strings, error messages, etc.)
|
||||||
|
const validLogs = recentLogs.filter((log: any) => {
|
||||||
|
const value = log.value || '';
|
||||||
|
// Skip if value looks like JSON or contains error-like patterns
|
||||||
|
if (value.includes('"status"') || value.includes('"content"') ||
|
||||||
|
value.includes('"activeForm"') || value.startsWith('{') ||
|
||||||
|
value.startsWith('[') || value.includes('graph 400')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Must have a file extension or look like a valid path
|
||||||
|
const hasExtension = /\.[a-zA-Z0-9]{1,10}$/.test(value);
|
||||||
|
const looksLikePath = value.includes('/') || value.includes('\\');
|
||||||
|
return hasExtension || looksLikePath;
|
||||||
|
}).slice(0, limit);
|
||||||
|
|
||||||
|
const recent = validLogs.map((log: any) => ({
|
||||||
|
type: log.action, // read, write, mention
|
||||||
|
timestamp: log.timestamp,
|
||||||
|
prompt: log.context_summary || '',
|
||||||
|
files: [log.value],
|
||||||
|
description: `${log.action}: ${log.value.split(/[/\\]/).pop()}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ recent }));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ recent: [] }));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Active Memory - Get status
|
||||||
|
if (pathname === '/api/memory/active/status') {
|
||||||
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configPath = path.join(projectPath, '.claude', 'rules', 'active_memory.md');
|
||||||
|
const configJsonPath = path.join(projectPath, '.claude', 'rules', 'active_memory_config.json');
|
||||||
|
const enabled = fs.existsSync(configPath);
|
||||||
|
let lastSync: string | null = null;
|
||||||
|
let fileCount = 0;
|
||||||
|
let config = { interval: 'manual', tool: 'gemini' };
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
const stats = fs.statSync(configPath);
|
||||||
|
lastSync = stats.mtime.toISOString();
|
||||||
|
const content = fs.readFileSync(configPath, 'utf-8');
|
||||||
|
// Count file sections
|
||||||
|
fileCount = (content.match(/^## /gm) || []).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config if exists
|
||||||
|
if (fs.existsSync(configJsonPath)) {
|
||||||
|
try {
|
||||||
|
config = JSON.parse(fs.readFileSync(configJsonPath, 'utf-8'));
|
||||||
|
} catch (e) { /* ignore parse errors */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
enabled,
|
||||||
|
status: enabled ? { lastSync, fileCount } : null,
|
||||||
|
config
|
||||||
|
}));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ enabled: false, status: null, config: { interval: 'manual', tool: 'gemini' } }));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Active Memory - Toggle
|
||||||
|
if (pathname === '/api/memory/active/toggle' && req.method === 'POST') {
|
||||||
|
let body = '';
|
||||||
|
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
||||||
|
req.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const { enabled, config } = JSON.parse(body || '{}');
|
||||||
|
const projectPath = initialPath;
|
||||||
|
const rulesDir = path.join(projectPath, '.claude', 'rules');
|
||||||
|
const configPath = path.join(rulesDir, 'active_memory.md');
|
||||||
|
const configJsonPath = path.join(rulesDir, 'active_memory_config.json');
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
// Enable: Create directory and initial file
|
||||||
|
if (!fs.existsSync(rulesDir)) {
|
||||||
|
fs.mkdirSync(rulesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save config
|
||||||
|
if (config) {
|
||||||
|
fs.writeFileSync(configJsonPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create initial active_memory.md with header
|
||||||
|
const initialContent = `# Active Memory
|
||||||
|
|
||||||
|
> Auto-generated understanding of frequently accessed files.
|
||||||
|
> Last updated: ${new Date().toISOString()}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*No files analyzed yet. Click "Sync Now" to analyze hot files.*
|
||||||
|
`;
|
||||||
|
fs.writeFileSync(configPath, initialContent, 'utf-8');
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ enabled: true, message: 'Active Memory enabled' }));
|
||||||
|
} else {
|
||||||
|
// Disable: Remove the files
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
fs.unlinkSync(configPath);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(configJsonPath)) {
|
||||||
|
fs.unlinkSync(configJsonPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ enabled: false, message: 'Active Memory disabled' }));
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Active Memory - Update Config
|
||||||
|
if (pathname === '/api/memory/active/config' && req.method === 'POST') {
|
||||||
|
let body = '';
|
||||||
|
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
||||||
|
req.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const { config } = JSON.parse(body || '{}');
|
||||||
|
const projectPath = initialPath;
|
||||||
|
const rulesDir = path.join(projectPath, '.claude', 'rules');
|
||||||
|
const configJsonPath = path.join(rulesDir, 'active_memory_config.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(rulesDir)) {
|
||||||
|
fs.mkdirSync(rulesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(configJsonPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ success: true, config }));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Active Memory - Sync (analyze hot files using CLI and update active_memory.md)
|
||||||
|
if (pathname === '/api/memory/active/sync' && req.method === 'POST') {
|
||||||
|
let body = '';
|
||||||
|
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
||||||
|
req.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const { tool = 'gemini' } = JSON.parse(body || '{}');
|
||||||
|
const projectPath = initialPath;
|
||||||
|
const rulesDir = path.join(projectPath, '.claude', 'rules');
|
||||||
|
const configPath = path.join(rulesDir, 'active_memory.md');
|
||||||
|
|
||||||
|
// Get hot files from memory store
|
||||||
|
const memoryStore = getMemoryStore(projectPath);
|
||||||
|
const hotEntities = memoryStore.getHotEntities(20);
|
||||||
|
const hotFiles = hotEntities
|
||||||
|
.filter((e: any) => e.type === 'file')
|
||||||
|
.slice(0, 10); // Limit to top 10 files
|
||||||
|
|
||||||
|
// Build file list for CLI analysis
|
||||||
|
const filePaths = hotFiles.map((f: any) => {
|
||||||
|
const filePath = f.value;
|
||||||
|
return path.isAbsolute(filePath) ? filePath : path.join(projectPath, filePath);
|
||||||
|
}).filter((p: string) => fs.existsSync(p));
|
||||||
|
|
||||||
|
// Build the active memory content header
|
||||||
|
let content = `# Active Memory
|
||||||
|
|
||||||
|
> Auto-generated understanding of frequently accessed files using ${tool.toUpperCase()}.
|
||||||
|
> Last updated: ${new Date().toISOString()}
|
||||||
|
> Files analyzed: ${hotFiles.length}
|
||||||
|
> CLI Tool: ${tool}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Use CLI to analyze files if available
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
let cliOutput = '';
|
||||||
|
|
||||||
|
// Build CLI command based on tool
|
||||||
|
const cliPrompt = `PURPOSE: Analyze the following hot files and provide a concise understanding of each.
|
||||||
|
TASK: For each file, describe its purpose, key exports, dependencies, and how it relates to other files.
|
||||||
|
MODE: analysis
|
||||||
|
CONTEXT: ${filePaths.map((p: string) => '@' + p).join(' ')}
|
||||||
|
EXPECTED: Markdown format with ## headings for each file, bullet points for key information.
|
||||||
|
RULES: Be concise. Focus on practical understanding. Include function signatures for key exports.`;
|
||||||
|
|
||||||
|
const cliCmd = tool === 'qwen' ? 'qwen' : 'gemini';
|
||||||
|
const cliArgs = ['-p', cliPrompt];
|
||||||
|
|
||||||
|
// Try to execute CLI
|
||||||
|
try {
|
||||||
|
const cliProcess = spawn(cliCmd, cliArgs, {
|
||||||
|
cwd: projectPath,
|
||||||
|
shell: true,
|
||||||
|
timeout: 120000 // 2 minute timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
cliOutput = await new Promise<string>((resolve, reject) => {
|
||||||
|
let output = '';
|
||||||
|
let errorOutput = '';
|
||||||
|
|
||||||
|
cliProcess.stdout?.on('data', (data: Buffer) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
cliProcess.stderr?.on('data', (data: Buffer) => {
|
||||||
|
errorOutput += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
cliProcess.on('close', (code: number) => {
|
||||||
|
if (code === 0 || output.length > 100) {
|
||||||
|
resolve(output);
|
||||||
|
} else {
|
||||||
|
reject(new Error(errorOutput || 'CLI execution failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cliProcess.on('error', (err: Error) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout fallback
|
||||||
|
setTimeout(() => {
|
||||||
|
if (output.length > 0) {
|
||||||
|
resolve(output);
|
||||||
|
} else {
|
||||||
|
reject(new Error('CLI timeout'));
|
||||||
|
}
|
||||||
|
}, 120000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add CLI output to content
|
||||||
|
content += cliOutput + '\n\n---\n\n';
|
||||||
|
|
||||||
|
} catch (cliErr) {
|
||||||
|
// Fallback to basic analysis if CLI fails
|
||||||
|
console.warn('[Active Memory] CLI analysis failed, using basic analysis:', (cliErr as Error).message);
|
||||||
|
|
||||||
|
// Basic analysis fallback
|
||||||
|
for (const file of hotFiles) {
|
||||||
|
const fileName = file.value.split(/[/\\]/).pop() || file.value;
|
||||||
|
const filePath = file.value;
|
||||||
|
const heat = file.stats?.heat_score || 0;
|
||||||
|
const readCount = file.stats?.read_count || 0;
|
||||||
|
const writeCount = file.stats?.write_count || 0;
|
||||||
|
|
||||||
|
content += `## ${fileName}
|
||||||
|
|
||||||
|
- **Path**: \`${filePath}\`
|
||||||
|
- **Heat Score**: ${heat}
|
||||||
|
- **Access**: ${readCount} reads, ${writeCount} writes
|
||||||
|
- **Last Seen**: ${file.last_seen_at || 'Unknown'}
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Try to read file and generate summary
|
||||||
|
try {
|
||||||
|
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(projectPath, filePath);
|
||||||
|
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
const stat = fs.statSync(fullPath);
|
||||||
|
const ext = path.extname(fullPath).toLowerCase();
|
||||||
|
|
||||||
|
content += `- **Size**: ${(stat.size / 1024).toFixed(1)} KB\n`;
|
||||||
|
content += `- **Type**: ${ext || 'unknown'}\n`;
|
||||||
|
|
||||||
|
const textExts = ['.ts', '.js', '.tsx', '.jsx', '.md', '.json', '.css', '.html', '.vue', '.svelte', '.py', '.go', '.rs'];
|
||||||
|
if (textExts.includes(ext) && stat.size < 100000) {
|
||||||
|
const fileContent = fs.readFileSync(fullPath, 'utf-8');
|
||||||
|
const lines = fileContent.split('\n').slice(0, 30);
|
||||||
|
|
||||||
|
const exports = lines.filter(l =>
|
||||||
|
l.includes('export ') || l.includes('function ') ||
|
||||||
|
l.includes('class ') || l.includes('interface ')
|
||||||
|
).slice(0, 8);
|
||||||
|
|
||||||
|
if (exports.length > 0) {
|
||||||
|
content += `\n**Key Exports**:\n\`\`\`\n${exports.join('\n')}\n\`\`\`\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (fileErr) {
|
||||||
|
// Skip file analysis errors
|
||||||
|
}
|
||||||
|
|
||||||
|
content += '\n---\n\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!fs.existsSync(rulesDir)) {
|
||||||
|
fs.mkdirSync(rulesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the file
|
||||||
|
fs.writeFileSync(configPath, content, 'utf-8');
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
filesAnalyzed: hotFiles.length,
|
||||||
|
path: configPath,
|
||||||
|
usedCli: cliOutput.length > 0
|
||||||
|
}));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// API: Memory Module - Get conversations index
|
// API: Memory Module - Get conversations index
|
||||||
if (pathname === '/api/memory/conversations') {
|
if (pathname === '/api/memory/conversations') {
|
||||||
const projectPath = url.searchParams.get('path') || initialPath;
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
@@ -2985,7 +3403,7 @@ async function getFileContent(filePath) {
|
|||||||
/**
|
/**
|
||||||
* Trigger update-module-claude tool (async execution)
|
* Trigger update-module-claude tool (async execution)
|
||||||
* @param {string} targetPath - Directory path to update
|
* @param {string} targetPath - Directory path to update
|
||||||
* @param {string} tool - CLI tool to use (gemini, qwen, codex)
|
* @param {string} tool - CLI tool to use (gemini, qwen, codex, claude)
|
||||||
* @param {string} strategy - Update strategy (single-layer, multi-layer)
|
* @param {string} strategy - Update strategy (single-layer, multi-layer)
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1863,3 +1863,238 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
UPDATE TASKS SECTION - In CLI Tab
|
||||||
|
========================================== */
|
||||||
|
|
||||||
|
/* Section Container */
|
||||||
|
.update-tasks-section {
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
padding-bottom: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-tasks-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-tasks-title {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-tasks-clear-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-tasks-clear-btn:hover {
|
||||||
|
background: hsl(var(--destructive) / 0.1);
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-tasks-list {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-tasks-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-tasks-empty span {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-tasks-empty p {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CLI History Section */
|
||||||
|
.cli-history-section {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-history-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-history-title {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-history-list {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual Update Task Item */
|
||||||
|
.update-task-item {
|
||||||
|
background: hsl(var(--background));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-item.status-pending {
|
||||||
|
border-left: 3px solid hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-item.status-running {
|
||||||
|
border-left: 3px solid hsl(var(--warning));
|
||||||
|
background: hsl(var(--warning) / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-item.status-completed {
|
||||||
|
border-left: 3px solid hsl(var(--success));
|
||||||
|
background: hsl(var(--success) / 0.05);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-item.status-failed {
|
||||||
|
border-left: 3px solid hsl(var(--destructive));
|
||||||
|
background: hsl(var(--destructive) / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-status {
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-item.status-running .update-task-status {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-strategy {
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-cli-select {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 6px;
|
||||||
|
background: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-cli-select:hover:not(:disabled) {
|
||||||
|
border-color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-cli-select:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-start {
|
||||||
|
background: hsl(var(--primary));
|
||||||
|
color: hsl(var(--primary-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-start:hover {
|
||||||
|
background: hsl(var(--primary) / 0.9);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-remove {
|
||||||
|
background: transparent;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-remove:hover {
|
||||||
|
background: hsl(var(--destructive) / 0.1);
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-stop {
|
||||||
|
background: hsl(var(--warning));
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-stop:hover {
|
||||||
|
background: hsl(var(--warning) / 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-task-message {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -484,6 +484,11 @@
|
|||||||
color: hsl(142 71% 35%);
|
color: hsl(142 71% 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-tool-tag.tool-claude {
|
||||||
|
background: hsl(25 90% 50% / 0.12);
|
||||||
|
color: hsl(25 90% 40%);
|
||||||
|
}
|
||||||
|
|
||||||
.history-mode-tag {
|
.history-mode-tag {
|
||||||
font-size: 0.625rem;
|
font-size: 0.625rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -713,6 +718,14 @@
|
|||||||
border-color: hsl(142 71% 45% / 0.7);
|
border-color: hsl(142 71% 45% / 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cli-tool-card.tool-claude.available {
|
||||||
|
border-color: hsl(25 90% 50% / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-tool-card.tool-claude.available:hover {
|
||||||
|
border-color: hsl(25 90% 50% / 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
.cli-tool-card.unavailable {
|
.cli-tool-card.unavailable {
|
||||||
border-color: hsl(var(--border));
|
border-color: hsl(var(--border));
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
@@ -1006,6 +1019,11 @@
|
|||||||
color: hsl(142 71% 35%);
|
color: hsl(142 71% 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cli-tool-claude {
|
||||||
|
background: hsl(25 90% 50% / 0.12);
|
||||||
|
color: hsl(25 90% 40%);
|
||||||
|
}
|
||||||
|
|
||||||
.cli-history-time {
|
.cli-history-time {
|
||||||
font-size: 0.6875rem;
|
font-size: 0.6875rem;
|
||||||
color: hsl(var(--muted-foreground));
|
color: hsl(var(--muted-foreground));
|
||||||
@@ -3187,6 +3205,11 @@
|
|||||||
color: hsl(145 60% 35%);
|
color: hsl(145 60% 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cli-queue-tool-tag.cli-tool-claude {
|
||||||
|
background: hsl(25 90% 50% / 0.15);
|
||||||
|
color: hsl(25 90% 40%);
|
||||||
|
}
|
||||||
|
|
||||||
.cli-queue-status {
|
.cli-queue-status {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
.memory-view {
|
.memory-view {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
|
max-height: calc(100vh - 150px);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-view.loading {
|
.memory-view.loading {
|
||||||
@@ -18,11 +22,241 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Memory Header with Active Memory Toggle */
|
||||||
|
.memory-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-header-left h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Memory Controls Container */
|
||||||
|
.active-memory-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Memory Toggle */
|
||||||
|
.active-memory-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Memory Config */
|
||||||
|
.active-memory-config {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
border-left: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item select {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
background: hsl(var(--background));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item select:focus {
|
||||||
|
border-color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item select:hover {
|
||||||
|
border-color: hsl(var(--primary) / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Memory Actions */
|
||||||
|
.active-memory-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
border-left: 1px solid hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-sync {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: hsl(var(--muted));
|
||||||
|
border-radius: 24px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider::before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input:checked + .toggle-slider {
|
||||||
|
background-color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input:checked + .toggle-slider::before {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input:focus + .toggle-slider {
|
||||||
|
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: hsl(var(--muted) / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-status.active {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
color: hsl(142 76% 36%);
|
||||||
|
background: hsl(142 76% 36% / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auto-sync indicator */
|
||||||
|
.auto-sync-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
background: hsl(var(--primary) / 0.1);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-sync-indicator svg {
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sync Button */
|
||||||
|
.btn-sync {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: hsl(var(--primary) / 0.1);
|
||||||
|
border: 1px solid hsl(var(--primary) / 0.3);
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sync:hover {
|
||||||
|
background: hsl(var(--primary) / 0.2);
|
||||||
|
border-color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sync.syncing {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sync.syncing i {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
.memory-columns {
|
.memory-columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 280px 1fr 320px;
|
grid-template-columns: 280px 1fr 320px;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
height: 100%;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: calc(100vh - 230px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-column {
|
.memory-column {
|
||||||
@@ -33,6 +267,7 @@
|
|||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Memory Section inside columns */
|
/* Memory Section inside columns */
|
||||||
@@ -40,6 +275,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
@@ -110,10 +346,15 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotspot-list-container {
|
.hotspot-list-container {
|
||||||
margin-bottom: 1rem;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotspot-list-container:last-child {
|
.hotspot-list-container:last-child {
|
||||||
@@ -130,6 +371,7 @@
|
|||||||
margin: 0 0 0.5rem 0;
|
margin: 0 0 0.5rem 0;
|
||||||
padding-bottom: 0.375rem;
|
padding-bottom: 0.375rem;
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hotspot List Items */
|
/* Hotspot List Items */
|
||||||
@@ -267,8 +509,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.legend-dot.file { background: hsl(var(--primary)); }
|
.legend-dot.file { background: hsl(var(--primary)); }
|
||||||
.legend-dot.module { background: hsl(var(--muted-foreground)); }
|
.legend-dot.module { background: hsl(var(--muted-foreground)); border: 1px dashed hsl(var(--muted-foreground)); }
|
||||||
.legend-dot.component { background: hsl(var(--success)); }
|
.legend-dot.component { background: hsl(142 76% 36%); }
|
||||||
|
|
||||||
/* Graph Container */
|
/* Graph Container */
|
||||||
.graph-container,
|
.graph-container,
|
||||||
@@ -277,9 +519,11 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
background: hsl(var(--background));
|
background: hsl(var(--background));
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
|
max-height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-graph-container svg {
|
.memory-graph-container svg {
|
||||||
@@ -425,8 +669,10 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Context Timeline Card Style */
|
||||||
.timeline-item {
|
.timeline-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -436,10 +682,21 @@
|
|||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 60px;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-item:hover {
|
.timeline-item:hover {
|
||||||
border-color: hsl(var(--primary) / 0.3);
|
border-color: hsl(var(--primary) / 0.3);
|
||||||
|
background: hsl(var(--hover));
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item.expanded {
|
||||||
|
max-height: none;
|
||||||
|
background: hsl(var(--muted) / 0.3);
|
||||||
|
border-color: hsl(var(--primary) / 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-item:last-child {
|
.timeline-item:last-child {
|
||||||
@@ -641,6 +898,12 @@
|
|||||||
.memory-columns {
|
.memory-columns {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-column {
|
||||||
|
max-height: 500px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,6 +917,7 @@
|
|||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotspots-header {
|
.hotspots-header {
|
||||||
@@ -683,6 +947,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotspot-item {
|
.hotspot-item {
|
||||||
@@ -793,6 +1058,7 @@
|
|||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-header {
|
.graph-header {
|
||||||
@@ -828,7 +1094,9 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: hsl(var(--background));
|
background: hsl(var(--background));
|
||||||
min-height: 400px;
|
min-height: 300px;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* D3 Graph Elements */
|
/* D3 Graph Elements */
|
||||||
@@ -943,6 +1211,111 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Graph Zoom/Pan Styles */
|
||||||
|
.memory-graph-svg {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-graph-svg:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-content {
|
||||||
|
transition: transform 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graph Node Groups */
|
||||||
|
.graph-node-group {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-node-group:hover circle {
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-node-group.file circle {
|
||||||
|
fill: hsl(var(--primary));
|
||||||
|
stroke: hsl(var(--primary));
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-node-group.module circle {
|
||||||
|
fill: hsl(var(--muted));
|
||||||
|
stroke: hsl(var(--muted-foreground));
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-dasharray: 4 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-node-group.component circle {
|
||||||
|
fill: hsl(142 76% 36%);
|
||||||
|
stroke: hsl(142 76% 36%);
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual graph-node circles (for legacy support) */
|
||||||
|
.graph-node.file {
|
||||||
|
fill: hsl(var(--primary));
|
||||||
|
stroke: hsl(var(--primary));
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-node.module {
|
||||||
|
fill: hsl(var(--muted));
|
||||||
|
stroke: hsl(var(--muted-foreground));
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-dasharray: 4 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-node.component {
|
||||||
|
fill: hsl(142 76% 36%);
|
||||||
|
stroke: hsl(142 76% 36%);
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected Node */
|
||||||
|
.graph-node.selected {
|
||||||
|
stroke: hsl(var(--foreground));
|
||||||
|
stroke-width: 3;
|
||||||
|
filter: drop-shadow(0 0 8px hsl(var(--primary)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graph Labels */
|
||||||
|
.graph-label {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 11px;
|
||||||
|
fill: hsl(var(--foreground));
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
text-shadow:
|
||||||
|
1px 1px 2px hsl(var(--background)),
|
||||||
|
-1px -1px 2px hsl(var(--background)),
|
||||||
|
1px -1px 2px hsl(var(--background)),
|
||||||
|
-1px 1px 2px hsl(var(--background));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graph Controls */
|
||||||
|
.graph-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-controls .btn-icon {
|
||||||
|
padding: 0.375rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-controls .btn-icon:hover {
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
* Context Timeline (Right Column)
|
* Context Timeline (Right Column)
|
||||||
* ======================================== */
|
* ======================================== */
|
||||||
@@ -953,6 +1326,7 @@
|
|||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-header {
|
.context-header {
|
||||||
@@ -982,37 +1356,10 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-item {
|
/* Timeline item styles moved to Context Timeline Card Style section (line ~445) */
|
||||||
position: relative;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-left: 2px solid hsl(var(--border));
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-item:last-child {
|
|
||||||
border-left-color: transparent;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-item::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: -6px;
|
|
||||||
top: 0;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: hsl(var(--primary));
|
|
||||||
border: 2px solid hsl(var(--card));
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-item.recent::before {
|
|
||||||
background: hsl(0 84% 60%);
|
|
||||||
box-shadow: 0 0 8px hsl(0 84% 60% / 0.5);
|
|
||||||
animation: timelinePulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-timestamp {
|
.timeline-timestamp {
|
||||||
font-size: 0.6875rem;
|
font-size: 0.6875rem;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Displays CLI tool availability status and allows setting default tool
|
// Displays CLI tool availability status and allows setting default tool
|
||||||
|
|
||||||
// ========== CLI State ==========
|
// ========== CLI State ==========
|
||||||
let cliToolStatus = { gemini: {}, qwen: {}, codex: {} };
|
let cliToolStatus = { gemini: {}, qwen: {}, codex: {}, claude: {} };
|
||||||
let codexLensStatus = { ready: false };
|
let codexLensStatus = { ready: false };
|
||||||
let semanticStatus = { available: false };
|
let semanticStatus = { available: false };
|
||||||
let defaultCliTool = 'gemini';
|
let defaultCliTool = 'gemini';
|
||||||
@@ -105,16 +105,18 @@ function renderCliStatus() {
|
|||||||
const toolDescriptions = {
|
const toolDescriptions = {
|
||||||
gemini: 'Google AI for code analysis',
|
gemini: 'Google AI for code analysis',
|
||||||
qwen: 'Alibaba AI assistant',
|
qwen: 'Alibaba AI assistant',
|
||||||
codex: 'OpenAI code generation'
|
codex: 'OpenAI code generation',
|
||||||
|
claude: 'Anthropic AI assistant'
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolIcons = {
|
const toolIcons = {
|
||||||
gemini: 'sparkle',
|
gemini: 'sparkle',
|
||||||
qwen: 'bot',
|
qwen: 'bot',
|
||||||
codex: 'code-2'
|
codex: 'code-2',
|
||||||
|
claude: 'brain'
|
||||||
};
|
};
|
||||||
|
|
||||||
const tools = ['gemini', 'qwen', 'codex'];
|
const tools = ['gemini', 'qwen', 'codex', 'claude'];
|
||||||
|
|
||||||
const toolsHtml = tools.map(tool => {
|
const toolsHtml = tools.map(tool => {
|
||||||
const status = cliToolStatus[tool] || {};
|
const status = cliToolStatus[tool] || {};
|
||||||
@@ -270,7 +272,7 @@ function renderCliStatus() {
|
|||||||
<span class="cli-toggle-slider"></span>
|
<span class="cli-toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="cli-setting-desc">Use native tool resume (gemini -r, qwen --resume, codex resume)</p>
|
<p class="cli-setting-desc">Use native tool resume (gemini -r, qwen --resume, codex resume, claude --resume)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cli-setting-item ${!smartContextEnabled ? 'disabled' : ''}">
|
<div class="cli-setting-item ${!smartContextEnabled ? 'disabled' : ''}">
|
||||||
<label class="cli-setting-label">
|
<label class="cli-setting-label">
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ let cliQueueData = [];
|
|||||||
let currentQueueTab = 'tasks'; // 'tasks' | 'cli'
|
let currentQueueTab = 'tasks'; // 'tasks' | 'cli'
|
||||||
let cliCategoryFilter = 'all'; // 'all' | 'user' | 'internal' | 'insight'
|
let cliCategoryFilter = 'all'; // 'all' | 'user' | 'internal' | 'insight'
|
||||||
|
|
||||||
|
// Update task queue data (for CLAUDE.md updates from explorer)
|
||||||
|
let sidebarUpdateTasks = [];
|
||||||
|
let isSidebarTaskRunning = {}; // Track running tasks by id
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize task queue sidebar
|
* Initialize task queue sidebar
|
||||||
*/
|
*/
|
||||||
@@ -65,10 +69,36 @@ function initTaskQueueSidebar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="task-queue-content" id="cliQueueContent" style="display: none;">
|
<div class="task-queue-content" id="cliQueueContent" style="display: none;">
|
||||||
<div class="task-queue-empty-state">
|
<!-- Update Tasks Section -->
|
||||||
<div class="task-queue-empty-icon">⚡</div>
|
<div class="update-tasks-section" id="updateTasksSection">
|
||||||
<div class="task-queue-empty-text">No CLI executions</div>
|
<div class="update-tasks-header">
|
||||||
<div class="task-queue-empty-hint">CLI tool executions will appear here</div>
|
<span class="update-tasks-title">📝 ${t('taskQueue.title')}</span>
|
||||||
|
<button class="update-tasks-clear-btn" onclick="clearCompletedUpdateTasks()" title="${t('taskQueue.clearCompleted')}">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="update-tasks-list" id="updateTasksList">
|
||||||
|
<div class="update-tasks-empty">
|
||||||
|
<span>${t('taskQueue.noTasks')}</span>
|
||||||
|
<p>${t('taskQueue.noTasksHint')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CLI History Section -->
|
||||||
|
<div class="cli-history-section" id="cliHistorySection">
|
||||||
|
<div class="cli-history-header">
|
||||||
|
<span class="cli-history-title">⚡ ${t('title.cliHistory')}</span>
|
||||||
|
</div>
|
||||||
|
<div class="cli-history-list" id="cliHistoryList">
|
||||||
|
<div class="task-queue-empty-state">
|
||||||
|
<div class="task-queue-empty-icon">⚡</div>
|
||||||
|
<div class="task-queue-empty-text">No internal executions</div>
|
||||||
|
<div class="task-queue-empty-hint">CLI tool executions will appear here</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +119,7 @@ function initTaskQueueSidebar() {
|
|||||||
|
|
||||||
updateTaskQueueData();
|
updateTaskQueueData();
|
||||||
updateCliQueueData();
|
updateCliQueueData();
|
||||||
renderTaskQueue();
|
renderTaskQueueSidebar();
|
||||||
renderCliQueue();
|
renderCliQueue();
|
||||||
updateTaskQueueBadge();
|
updateTaskQueueBadge();
|
||||||
}
|
}
|
||||||
@@ -114,7 +144,7 @@ function toggleTaskQueueSidebar() {
|
|||||||
toggle.classList.add('hidden');
|
toggle.classList.add('hidden');
|
||||||
// Refresh data when opened
|
// Refresh data when opened
|
||||||
updateTaskQueueData();
|
updateTaskQueueData();
|
||||||
renderTaskQueue();
|
renderTaskQueueSidebar();
|
||||||
} else {
|
} else {
|
||||||
sidebar.classList.remove('open');
|
sidebar.classList.remove('open');
|
||||||
overlay.classList.remove('show');
|
overlay.classList.remove('show');
|
||||||
@@ -182,9 +212,10 @@ function updateTaskQueueData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render task queue list
|
* Render task queue list in sidebar
|
||||||
|
* Note: Named renderTaskQueueSidebar to avoid conflict with explorer.js renderTaskQueue
|
||||||
*/
|
*/
|
||||||
function renderTaskQueue(filter) {
|
function renderTaskQueueSidebar(filter) {
|
||||||
filter = filter || 'all';
|
filter = filter || 'all';
|
||||||
var contentEl = document.getElementById('taskQueueContent');
|
var contentEl = document.getElementById('taskQueueContent');
|
||||||
if (!contentEl) {
|
if (!contentEl) {
|
||||||
@@ -249,7 +280,7 @@ function filterTaskQueue(filter) {
|
|||||||
document.querySelectorAll('.task-filter-btn').forEach(btn => {
|
document.querySelectorAll('.task-filter-btn').forEach(btn => {
|
||||||
btn.classList.toggle('active', btn.dataset.filter === filter);
|
btn.classList.toggle('active', btn.dataset.filter === filter);
|
||||||
});
|
});
|
||||||
renderTaskQueue(filter);
|
renderTaskQueueSidebar(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -307,7 +338,7 @@ function updateTaskQueueBadge() {
|
|||||||
function refreshTaskQueue() {
|
function refreshTaskQueue() {
|
||||||
updateTaskQueueData();
|
updateTaskQueueData();
|
||||||
updateCliQueueData();
|
updateCliQueueData();
|
||||||
renderTaskQueue();
|
renderTaskQueueSidebar();
|
||||||
renderCliQueue();
|
renderCliQueue();
|
||||||
updateTaskQueueBadge();
|
updateTaskQueueBadge();
|
||||||
}
|
}
|
||||||
@@ -365,7 +396,7 @@ async function updateCliQueueData() {
|
|||||||
* Render CLI queue list
|
* Render CLI queue list
|
||||||
*/
|
*/
|
||||||
function renderCliQueue() {
|
function renderCliQueue() {
|
||||||
const contentEl = document.getElementById('cliQueueContent');
|
const contentEl = document.getElementById('cliHistoryList');
|
||||||
if (!contentEl) return;
|
if (!contentEl) return;
|
||||||
|
|
||||||
// Filter by category
|
// Filter by category
|
||||||
@@ -459,3 +490,227 @@ function getCliTimeAgo(date) {
|
|||||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
||||||
return `${Math.floor(seconds / 86400)}d`;
|
return `${Math.floor(seconds / 86400)}d`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// UPDATE TASK QUEUE - For CLAUDE.md Updates
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add update task to sidebar queue (called from explorer)
|
||||||
|
*/
|
||||||
|
function addUpdateTaskToSidebar(path, tool = 'gemini', strategy = 'single-layer') {
|
||||||
|
const task = {
|
||||||
|
id: Date.now(),
|
||||||
|
path,
|
||||||
|
tool,
|
||||||
|
strategy,
|
||||||
|
status: 'pending', // pending, running, completed, failed
|
||||||
|
message: '',
|
||||||
|
addedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
sidebarUpdateTasks.push(task);
|
||||||
|
renderSidebarUpdateTasks();
|
||||||
|
updateCliTabBadge();
|
||||||
|
|
||||||
|
// Open sidebar and switch to CLI tab if not visible
|
||||||
|
if (!isTaskQueueSidebarVisible) {
|
||||||
|
toggleTaskQueueSidebar();
|
||||||
|
}
|
||||||
|
switchQueueTab('cli');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove update task from queue
|
||||||
|
*/
|
||||||
|
function removeUpdateTask(taskId) {
|
||||||
|
sidebarUpdateTasks = sidebarUpdateTasks.filter(t => t.id !== taskId);
|
||||||
|
renderSidebarUpdateTasks();
|
||||||
|
updateCliTabBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear completed/failed update tasks
|
||||||
|
*/
|
||||||
|
function clearCompletedUpdateTasks() {
|
||||||
|
sidebarUpdateTasks = sidebarUpdateTasks.filter(t => t.status === 'pending' || t.status === 'running');
|
||||||
|
renderSidebarUpdateTasks();
|
||||||
|
updateCliTabBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update CLI tool for a specific task
|
||||||
|
*/
|
||||||
|
function updateSidebarTaskCliTool(taskId, tool) {
|
||||||
|
const task = sidebarUpdateTasks.find(t => t.id === taskId);
|
||||||
|
if (task && task.status === 'pending') {
|
||||||
|
task.tool = tool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a single update task
|
||||||
|
*/
|
||||||
|
async function executeSidebarUpdateTask(taskId) {
|
||||||
|
const task = sidebarUpdateTasks.find(t => t.id === taskId);
|
||||||
|
if (!task || task.status !== 'pending') return;
|
||||||
|
|
||||||
|
const folderName = task.path.split('/').pop() || task.path;
|
||||||
|
|
||||||
|
// Update status to running
|
||||||
|
task.status = 'running';
|
||||||
|
task.message = t('taskQueue.processing');
|
||||||
|
isSidebarTaskRunning[taskId] = true;
|
||||||
|
renderSidebarUpdateTasks();
|
||||||
|
|
||||||
|
if (typeof addGlobalNotification === 'function') {
|
||||||
|
addGlobalNotification('info', `Processing: ${folderName}`, `Strategy: ${task.strategy}, Tool: ${task.tool}`, 'Explorer');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/update-claude-md', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
path: task.path,
|
||||||
|
tool: task.tool,
|
||||||
|
strategy: task.strategy
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
task.status = 'completed';
|
||||||
|
task.message = t('taskQueue.updated');
|
||||||
|
if (typeof addGlobalNotification === 'function') {
|
||||||
|
addGlobalNotification('success', `Completed: ${folderName}`, result.message, 'Explorer');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
task.status = 'failed';
|
||||||
|
task.message = result.error || t('taskQueue.failed');
|
||||||
|
if (typeof addGlobalNotification === 'function') {
|
||||||
|
addGlobalNotification('error', `Failed: ${folderName}`, result.error || 'Unknown error', 'Explorer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
task.status = 'failed';
|
||||||
|
task.message = error.message;
|
||||||
|
if (typeof addGlobalNotification === 'function') {
|
||||||
|
addGlobalNotification('error', `Error: ${folderName}`, error.message, 'Explorer');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
delete isSidebarTaskRunning[taskId];
|
||||||
|
renderSidebarUpdateTasks();
|
||||||
|
updateCliTabBadge();
|
||||||
|
|
||||||
|
// Refresh tree to show updated CLAUDE.md files
|
||||||
|
if (typeof loadExplorerTree === 'function' && typeof explorerCurrentPath !== 'undefined') {
|
||||||
|
loadExplorerTree(explorerCurrentPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop/cancel a running update task (if possible)
|
||||||
|
*/
|
||||||
|
function stopSidebarUpdateTask(taskId) {
|
||||||
|
// Currently just removes the task - actual cancellation would need AbortController
|
||||||
|
const task = sidebarUpdateTasks.find(t => t.id === taskId);
|
||||||
|
if (task && task.status === 'running') {
|
||||||
|
task.status = 'failed';
|
||||||
|
task.message = 'Cancelled';
|
||||||
|
delete isSidebarTaskRunning[taskId];
|
||||||
|
renderSidebarUpdateTasks();
|
||||||
|
updateCliTabBadge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render update task queue list
|
||||||
|
*/
|
||||||
|
function renderSidebarUpdateTasks() {
|
||||||
|
const listEl = document.getElementById('updateTasksList');
|
||||||
|
if (!listEl) return;
|
||||||
|
|
||||||
|
if (sidebarUpdateTasks.length === 0) {
|
||||||
|
listEl.innerHTML = `
|
||||||
|
<div class="update-tasks-empty">
|
||||||
|
<span>${t('taskQueue.noTasks')}</span>
|
||||||
|
<p>${t('taskQueue.noTasksHint')}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listEl.innerHTML = sidebarUpdateTasks.map(task => {
|
||||||
|
const folderName = task.path.split('/').pop() || task.path;
|
||||||
|
const strategyIcon = task.strategy === 'multi-layer' ? '📂' : '📄';
|
||||||
|
const strategyLabel = task.strategy === 'multi-layer'
|
||||||
|
? t('taskQueue.withSubdirs')
|
||||||
|
: t('taskQueue.currentOnly');
|
||||||
|
|
||||||
|
const statusIcon = {
|
||||||
|
'pending': '⏳',
|
||||||
|
'running': '🔄',
|
||||||
|
'completed': '✅',
|
||||||
|
'failed': '❌'
|
||||||
|
}[task.status];
|
||||||
|
|
||||||
|
const isPending = task.status === 'pending';
|
||||||
|
const isRunning = task.status === 'running';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="update-task-item status-${task.status}" data-task-id="${task.id}">
|
||||||
|
<div class="update-task-header">
|
||||||
|
<span class="update-task-status">${statusIcon}</span>
|
||||||
|
<span class="update-task-name" title="${escapeHtml(task.path)}">${escapeHtml(folderName)}</span>
|
||||||
|
<span class="update-task-strategy" title="${strategyLabel}">${strategyIcon}</span>
|
||||||
|
</div>
|
||||||
|
<div class="update-task-controls">
|
||||||
|
<select class="update-task-cli-select"
|
||||||
|
onchange="updateSidebarTaskCliTool(${task.id}, this.value)"
|
||||||
|
${!isPending ? 'disabled' : ''}>
|
||||||
|
<option value="gemini" ${task.tool === 'gemini' ? 'selected' : ''}>Gemini</option>
|
||||||
|
<option value="qwen" ${task.tool === 'qwen' ? 'selected' : ''}>Qwen</option>
|
||||||
|
<option value="codex" ${task.tool === 'codex' ? 'selected' : ''}>Codex</option>
|
||||||
|
<option value="claude" ${task.tool === 'claude' ? 'selected' : ''}>Claude</option>
|
||||||
|
</select>
|
||||||
|
${isPending ? `
|
||||||
|
<button class="update-task-btn update-task-start" onclick="executeSidebarUpdateTask(${task.id})" title="${t('taskQueue.startAll')}">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<polygon points="5 3 19 12 5 21 5 3"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="update-task-btn update-task-remove" onclick="removeUpdateTask(${task.id})" title="Remove">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M18 6L6 18M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
${isRunning ? `
|
||||||
|
<button class="update-task-btn update-task-stop" onclick="stopSidebarUpdateTask(${task.id})" title="Stop">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<rect x="6" y="6" width="12" height="12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
${task.message ? `<div class="update-task-message">${escapeHtml(task.message)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update CLI tab badge with pending update tasks count
|
||||||
|
*/
|
||||||
|
function updateCliTabBadge() {
|
||||||
|
const pendingCount = sidebarUpdateTasks.filter(t => t.status === 'pending' || t.status === 'running').length;
|
||||||
|
const cliTabBadge = document.getElementById('cliTabBadge');
|
||||||
|
if (cliTabBadge) {
|
||||||
|
const totalCount = pendingCount + cliQueueData.length;
|
||||||
|
cliTabBadge.textContent = totalCount;
|
||||||
|
cliTabBadge.style.display = totalCount > 0 ? 'inline' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -569,6 +569,9 @@ const i18n = {
|
|||||||
'memory.memoryGraph': 'Memory Graph',
|
'memory.memoryGraph': 'Memory Graph',
|
||||||
'memory.nodes': 'nodes',
|
'memory.nodes': 'nodes',
|
||||||
'memory.resetView': 'Reset View',
|
'memory.resetView': 'Reset View',
|
||||||
|
'memory.zoomIn': 'Zoom In',
|
||||||
|
'memory.zoomOut': 'Zoom Out',
|
||||||
|
'memory.fitView': 'Fit to View',
|
||||||
'memory.file': 'File',
|
'memory.file': 'File',
|
||||||
'memory.module': 'Module',
|
'memory.module': 'Module',
|
||||||
'memory.component': 'Component',
|
'memory.component': 'Component',
|
||||||
@@ -580,6 +583,7 @@ const i18n = {
|
|||||||
'memory.noRecentActivity': 'No recent activity',
|
'memory.noRecentActivity': 'No recent activity',
|
||||||
'memory.reads': 'Reads',
|
'memory.reads': 'Reads',
|
||||||
'memory.edits': 'Edits',
|
'memory.edits': 'Edits',
|
||||||
|
'memory.mentions': 'Mentions',
|
||||||
'memory.prompts': 'Prompts',
|
'memory.prompts': 'Prompts',
|
||||||
'memory.nodeDetails': 'Node Details',
|
'memory.nodeDetails': 'Node Details',
|
||||||
'memory.heat': 'Heat',
|
'memory.heat': 'Heat',
|
||||||
@@ -590,6 +594,25 @@ const i18n = {
|
|||||||
'memory.justNow': 'Just now',
|
'memory.justNow': 'Just now',
|
||||||
'memory.minutesAgo': 'minutes ago',
|
'memory.minutesAgo': 'minutes ago',
|
||||||
'memory.hoursAgo': 'hours ago',
|
'memory.hoursAgo': 'hours ago',
|
||||||
|
'memory.title': 'Memory',
|
||||||
|
'memory.activeMemory': 'Active Memory',
|
||||||
|
'memory.active': 'Active',
|
||||||
|
'memory.inactive': 'Inactive',
|
||||||
|
'memory.syncNow': 'Sync Now',
|
||||||
|
'memory.syncComplete': 'Sync complete',
|
||||||
|
'memory.syncError': 'Sync failed',
|
||||||
|
'memory.filesAnalyzed': 'files analyzed',
|
||||||
|
'memory.activeMemoryEnabled': 'Active Memory enabled',
|
||||||
|
'memory.activeMemoryDisabled': 'Active Memory disabled',
|
||||||
|
'memory.activeMemoryError': 'Failed to toggle Active Memory',
|
||||||
|
'memory.interval': 'Interval',
|
||||||
|
'memory.intervalManual': 'Manual',
|
||||||
|
'memory.minutes': 'min',
|
||||||
|
'memory.cliTool': 'CLI',
|
||||||
|
'memory.lastSync': 'Last sync',
|
||||||
|
'memory.autoSyncActive': 'Auto-sync',
|
||||||
|
'memory.configUpdated': 'Configuration updated',
|
||||||
|
'memory.configError': 'Failed to update configuration',
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
'common.cancel': 'Cancel',
|
'common.cancel': 'Cancel',
|
||||||
@@ -1170,6 +1193,9 @@ const i18n = {
|
|||||||
'memory.memoryGraph': '记忆图谱',
|
'memory.memoryGraph': '记忆图谱',
|
||||||
'memory.nodes': '节点',
|
'memory.nodes': '节点',
|
||||||
'memory.resetView': '重置视图',
|
'memory.resetView': '重置视图',
|
||||||
|
'memory.zoomIn': '放大',
|
||||||
|
'memory.zoomOut': '缩小',
|
||||||
|
'memory.fitView': '自适应',
|
||||||
'memory.file': '文件',
|
'memory.file': '文件',
|
||||||
'memory.module': '模块',
|
'memory.module': '模块',
|
||||||
'memory.component': '组件',
|
'memory.component': '组件',
|
||||||
@@ -1181,6 +1207,7 @@ const i18n = {
|
|||||||
'memory.noRecentActivity': '无最近活动',
|
'memory.noRecentActivity': '无最近活动',
|
||||||
'memory.reads': '读取',
|
'memory.reads': '读取',
|
||||||
'memory.edits': '编辑',
|
'memory.edits': '编辑',
|
||||||
|
'memory.mentions': '提及',
|
||||||
'memory.prompts': '提示',
|
'memory.prompts': '提示',
|
||||||
'memory.nodeDetails': '节点详情',
|
'memory.nodeDetails': '节点详情',
|
||||||
'memory.heat': '热度',
|
'memory.heat': '热度',
|
||||||
@@ -1191,6 +1218,25 @@ const i18n = {
|
|||||||
'memory.justNow': '刚刚',
|
'memory.justNow': '刚刚',
|
||||||
'memory.minutesAgo': '分钟前',
|
'memory.minutesAgo': '分钟前',
|
||||||
'memory.hoursAgo': '小时前',
|
'memory.hoursAgo': '小时前',
|
||||||
|
'memory.title': '记忆',
|
||||||
|
'memory.activeMemory': '活动记忆',
|
||||||
|
'memory.active': '已启用',
|
||||||
|
'memory.inactive': '未启用',
|
||||||
|
'memory.syncNow': '立即同步',
|
||||||
|
'memory.syncComplete': '同步完成',
|
||||||
|
'memory.syncError': '同步失败',
|
||||||
|
'memory.filesAnalyzed': '个文件已分析',
|
||||||
|
'memory.activeMemoryEnabled': '活动记忆已启用',
|
||||||
|
'memory.activeMemoryDisabled': '活动记忆已禁用',
|
||||||
|
'memory.activeMemoryError': '切换活动记忆失败',
|
||||||
|
'memory.interval': '间隔',
|
||||||
|
'memory.intervalManual': '手动',
|
||||||
|
'memory.minutes': '分钟',
|
||||||
|
'memory.cliTool': 'CLI',
|
||||||
|
'memory.lastSync': '上次同步',
|
||||||
|
'memory.autoSyncActive': '自动同步',
|
||||||
|
'memory.configUpdated': '配置已更新',
|
||||||
|
'memory.configError': '配置更新失败',
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
'common.cancel': '取消',
|
'common.cancel': '取消',
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ async function renderExplorer() {
|
|||||||
<option value="gemini">Gemini</option>
|
<option value="gemini">Gemini</option>
|
||||||
<option value="qwen">Qwen</option>
|
<option value="qwen">Qwen</option>
|
||||||
<option value="codex">Codex</option>
|
<option value="codex">Codex</option>
|
||||||
|
<option value="claude">Claude</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-queue-actions">
|
<div class="task-queue-actions">
|
||||||
@@ -707,12 +708,17 @@ function addUpdateTask(path, tool = 'gemini', strategy = 'single-layer') {
|
|||||||
* Add task from folder context (right-click or button)
|
* Add task from folder context (right-click or button)
|
||||||
*/
|
*/
|
||||||
function addFolderToQueue(folderPath, strategy = 'single-layer') {
|
function addFolderToQueue(folderPath, strategy = 'single-layer') {
|
||||||
// Use the selected CLI tool from the queue panel
|
// Use the sidebar queue instead of floating panel
|
||||||
addUpdateTask(folderPath, defaultCliTool, strategy);
|
if (typeof addUpdateTaskToSidebar === 'function') {
|
||||||
|
addUpdateTaskToSidebar(folderPath, defaultCliTool, strategy);
|
||||||
|
} else {
|
||||||
|
// Fallback to local queue
|
||||||
|
addUpdateTask(folderPath, defaultCliTool, strategy);
|
||||||
|
|
||||||
// Show task queue if not visible
|
// Show task queue if not visible
|
||||||
if (!isTaskQueueVisible) {
|
if (!isTaskQueueVisible) {
|
||||||
toggleTaskQueue();
|
toggleTaskQueue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ var memoryGraphData = null;
|
|||||||
var recentContext = [];
|
var recentContext = [];
|
||||||
var memoryTimeFilter = 'all'; // 'today', 'week', 'all'
|
var memoryTimeFilter = 'all'; // 'today', 'week', 'all'
|
||||||
var selectedNode = null;
|
var selectedNode = null;
|
||||||
|
var activeMemoryEnabled = false;
|
||||||
|
var activeMemoryStatus = null;
|
||||||
|
var activeMemoryConfig = {
|
||||||
|
interval: 'manual', // manual, 5, 15, 30, 60 (minutes)
|
||||||
|
tool: 'gemini' // gemini, qwen
|
||||||
|
};
|
||||||
|
var activeMemorySyncTimer = null; // Timer for automatic periodic sync
|
||||||
|
|
||||||
// ========== Main Render Function ==========
|
// ========== Main Render Function ==========
|
||||||
async function renderMemoryView() {
|
async function renderMemoryView() {
|
||||||
@@ -29,11 +36,20 @@ async function renderMemoryView() {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadMemoryStats(),
|
loadMemoryStats(),
|
||||||
loadMemoryGraph(),
|
loadMemoryGraph(),
|
||||||
loadRecentContext()
|
loadRecentContext(),
|
||||||
|
loadActiveMemoryStatus()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Render three-column layout
|
// Render layout with Active Memory header
|
||||||
container.innerHTML = '<div class="memory-view">' +
|
container.innerHTML = '<div class="memory-view">' +
|
||||||
|
'<div class="memory-header">' +
|
||||||
|
'<div class="memory-header-left">' +
|
||||||
|
'<h2><i data-lucide="brain" class="w-5 h-5"></i> ' + t('memory.title') + '</h2>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="memory-header-right">' +
|
||||||
|
renderActiveMemoryControls() +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
'<div class="memory-columns">' +
|
'<div class="memory-columns">' +
|
||||||
'<div class="memory-column left" id="memory-hotspots"></div>' +
|
'<div class="memory-column left" id="memory-hotspots"></div>' +
|
||||||
'<div class="memory-column center" id="memory-graph"></div>' +
|
'<div class="memory-column center" id="memory-graph"></div>' +
|
||||||
@@ -50,6 +66,56 @@ async function renderMemoryView() {
|
|||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderActiveMemoryControls() {
|
||||||
|
var html = '<div class="active-memory-controls">' +
|
||||||
|
'<div class="active-memory-toggle">' +
|
||||||
|
'<span class="toggle-label">' + t('memory.activeMemory') + '</span>' +
|
||||||
|
'<label class="toggle-switch">' +
|
||||||
|
'<input type="checkbox" id="activeMemorySwitch" ' + (activeMemoryEnabled ? 'checked' : '') + ' onchange="toggleActiveMemory(this.checked)">' +
|
||||||
|
'<span class="toggle-slider"></span>' +
|
||||||
|
'</label>' +
|
||||||
|
(activeMemoryEnabled ? '<span class="toggle-status active"><i data-lucide="zap" class="w-3 h-3"></i> ' + t('memory.active') + '</span>' : '<span class="toggle-status">' + t('memory.inactive') + '</span>') +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
if (activeMemoryEnabled) {
|
||||||
|
var isAutoSync = activeMemoryConfig.interval !== 'manual';
|
||||||
|
html += '<div class="active-memory-config">' +
|
||||||
|
// Interval selector
|
||||||
|
'<div class="config-item">' +
|
||||||
|
'<label>' + t('memory.interval') + '</label>' +
|
||||||
|
'<select id="activeMemoryInterval" onchange="updateActiveMemoryConfig(\'interval\', this.value)">' +
|
||||||
|
'<option value="manual"' + (activeMemoryConfig.interval === 'manual' ? ' selected' : '') + '>' + t('memory.intervalManual') + '</option>' +
|
||||||
|
'<option value="5"' + (activeMemoryConfig.interval === '5' ? ' selected' : '') + '>5 ' + t('memory.minutes') + '</option>' +
|
||||||
|
'<option value="15"' + (activeMemoryConfig.interval === '15' ? ' selected' : '') + '>15 ' + t('memory.minutes') + '</option>' +
|
||||||
|
'<option value="30"' + (activeMemoryConfig.interval === '30' ? ' selected' : '') + '>30 ' + t('memory.minutes') + '</option>' +
|
||||||
|
'<option value="60"' + (activeMemoryConfig.interval === '60' ? ' selected' : '') + '>60 ' + t('memory.minutes') + '</option>' +
|
||||||
|
'</select>' +
|
||||||
|
'</div>' +
|
||||||
|
// CLI tool selector
|
||||||
|
'<div class="config-item">' +
|
||||||
|
'<label>' + t('memory.cliTool') + '</label>' +
|
||||||
|
'<select id="activeMemoryCli" onchange="updateActiveMemoryConfig(\'tool\', this.value)">' +
|
||||||
|
'<option value="gemini"' + (activeMemoryConfig.tool === 'gemini' ? ' selected' : '') + '>Gemini</option>' +
|
||||||
|
'<option value="qwen"' + (activeMemoryConfig.tool === 'qwen' ? ' selected' : '') + '>Qwen</option>' +
|
||||||
|
'</select>' +
|
||||||
|
'</div>' +
|
||||||
|
// Auto-sync indicator
|
||||||
|
(isAutoSync ? '<div class="auto-sync-indicator"><i data-lucide="timer" class="w-3 h-3"></i> ' + t('memory.autoSyncActive') + '</div>' : '') +
|
||||||
|
'</div>' +
|
||||||
|
// Sync button and status
|
||||||
|
'<div class="active-memory-actions">' +
|
||||||
|
'<button class="btn-icon btn-sync" onclick="syncActiveMemory()" title="' + t('memory.syncNow') + '">' +
|
||||||
|
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
|
||||||
|
'</button>' +
|
||||||
|
(activeMemoryStatus && activeMemoryStatus.lastSync ?
|
||||||
|
'<span class="last-sync">' + t('memory.lastSync') + ': ' + formatTimestamp(activeMemoryStatus.lastSync) + '</span>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Data Loading ==========
|
// ========== Data Loading ==========
|
||||||
async function loadMemoryStats() {
|
async function loadMemoryStats() {
|
||||||
try {
|
try {
|
||||||
@@ -93,6 +159,168 @@ async function loadRecentContext() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Active Memory Functions ==========
|
||||||
|
// Timer management for automatic sync
|
||||||
|
function startActiveMemorySyncTimer() {
|
||||||
|
// Clear any existing timer
|
||||||
|
stopActiveMemorySyncTimer();
|
||||||
|
|
||||||
|
// Only start timer if interval is not manual
|
||||||
|
if (activeMemoryConfig.interval === 'manual' || !activeMemoryEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var intervalMs = parseInt(activeMemoryConfig.interval, 10) * 60 * 1000; // Convert minutes to ms
|
||||||
|
console.log('[ActiveMemory] Starting auto-sync timer:', activeMemoryConfig.interval, 'minutes');
|
||||||
|
|
||||||
|
activeMemorySyncTimer = setInterval(function() {
|
||||||
|
console.log('[ActiveMemory] Auto-sync triggered');
|
||||||
|
syncActiveMemory();
|
||||||
|
}, intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopActiveMemorySyncTimer() {
|
||||||
|
if (activeMemorySyncTimer) {
|
||||||
|
console.log('[ActiveMemory] Stopping auto-sync timer');
|
||||||
|
clearInterval(activeMemorySyncTimer);
|
||||||
|
activeMemorySyncTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadActiveMemoryStatus() {
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/memory/active/status');
|
||||||
|
if (!response.ok) throw new Error('Failed to load active memory status');
|
||||||
|
var data = await response.json();
|
||||||
|
activeMemoryEnabled = data.enabled || false;
|
||||||
|
activeMemoryStatus = data.status || null;
|
||||||
|
// Load config if available
|
||||||
|
if (data.config) {
|
||||||
|
activeMemoryConfig = Object.assign(activeMemoryConfig, data.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start timer if active memory is enabled and interval is not manual
|
||||||
|
if (activeMemoryEnabled && activeMemoryConfig.interval !== 'manual') {
|
||||||
|
startActiveMemorySyncTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load active memory status:', err);
|
||||||
|
activeMemoryEnabled = false;
|
||||||
|
activeMemoryStatus = null;
|
||||||
|
return { enabled: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleActiveMemory(enabled) {
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/memory/active/toggle', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
enabled: enabled,
|
||||||
|
config: activeMemoryConfig
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to toggle active memory');
|
||||||
|
var data = await response.json();
|
||||||
|
activeMemoryEnabled = data.enabled;
|
||||||
|
|
||||||
|
// Manage auto-sync timer based on enabled state
|
||||||
|
if (activeMemoryEnabled) {
|
||||||
|
startActiveMemorySyncTimer();
|
||||||
|
} else {
|
||||||
|
stopActiveMemorySyncTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
if (window.showToast) {
|
||||||
|
showToast(enabled ? t('memory.activeMemoryEnabled') : t('memory.activeMemoryDisabled'), 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render the view to update UI
|
||||||
|
renderMemoryView();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to toggle active memory:', err);
|
||||||
|
if (window.showToast) {
|
||||||
|
showToast(t('memory.activeMemoryError'), 'error');
|
||||||
|
}
|
||||||
|
// Revert checkbox state
|
||||||
|
var checkbox = document.getElementById('activeMemorySwitch');
|
||||||
|
if (checkbox) checkbox.checked = !enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateActiveMemoryConfig(key, value) {
|
||||||
|
activeMemoryConfig[key] = value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/memory/active/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ config: activeMemoryConfig })
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to update config');
|
||||||
|
|
||||||
|
// Restart timer if interval changed and active memory is enabled
|
||||||
|
if (key === 'interval' && activeMemoryEnabled) {
|
||||||
|
startActiveMemorySyncTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.showToast) {
|
||||||
|
showToast(t('memory.configUpdated'), 'success');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update active memory config:', err);
|
||||||
|
if (window.showToast) {
|
||||||
|
showToast(t('memory.configError'), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncActiveMemory() {
|
||||||
|
var syncBtn = document.querySelector('.btn-sync');
|
||||||
|
if (syncBtn) {
|
||||||
|
syncBtn.classList.add('syncing');
|
||||||
|
syncBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await fetch('/api/memory/active/sync', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
tool: activeMemoryConfig.tool
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to sync active memory');
|
||||||
|
var data = await response.json();
|
||||||
|
|
||||||
|
if (window.showToast) {
|
||||||
|
showToast(t('memory.syncComplete') + ' (' + (data.filesAnalyzed || 0) + ' ' + t('memory.filesAnalyzed') + ')', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data and update last sync time
|
||||||
|
await loadActiveMemoryStatus();
|
||||||
|
// Update last sync display without full re-render
|
||||||
|
var lastSyncEl = document.querySelector('.last-sync');
|
||||||
|
if (lastSyncEl && activeMemoryStatus && activeMemoryStatus.lastSync) {
|
||||||
|
lastSyncEl.textContent = t('memory.lastSync') + ': ' + formatTimestamp(activeMemoryStatus.lastSync);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to sync active memory:', err);
|
||||||
|
if (window.showToast) {
|
||||||
|
showToast(t('memory.syncError'), 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (syncBtn) {
|
||||||
|
syncBtn.classList.remove('syncing');
|
||||||
|
syncBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Left Column: Context Hotspots ==========
|
// ========== Left Column: Context Hotspots ==========
|
||||||
function renderHotspotsColumn() {
|
function renderHotspotsColumn() {
|
||||||
var container = document.getElementById('memory-hotspots');
|
var container = document.getElementById('memory-hotspots');
|
||||||
@@ -163,6 +391,12 @@ function renderHotspotList(items, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== Center Column: Memory Graph ==========
|
// ========== Center Column: Memory Graph ==========
|
||||||
|
// Store graph state for zoom/pan
|
||||||
|
var graphZoom = null;
|
||||||
|
var graphSvg = null;
|
||||||
|
var graphGroup = null;
|
||||||
|
var graphSimulation = null;
|
||||||
|
|
||||||
function renderGraphColumn() {
|
function renderGraphColumn() {
|
||||||
var container = document.getElementById('memory-graph');
|
var container = document.getElementById('memory-graph');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -173,10 +407,19 @@ function renderGraphColumn() {
|
|||||||
'<h3><i data-lucide="network" class="w-4 h-4"></i> ' + t('memory.memoryGraph') + '</h3>' +
|
'<h3><i data-lucide="network" class="w-4 h-4"></i> ' + t('memory.memoryGraph') + '</h3>' +
|
||||||
'<span class="section-count">' + (memoryGraphData.nodes || []).length + ' ' + t('memory.nodes') + '</span>' +
|
'<span class="section-count">' + (memoryGraphData.nodes || []).length + ' ' + t('memory.nodes') + '</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="section-header-actions">' +
|
'<div class="section-header-actions graph-controls">' +
|
||||||
'<button class="btn-icon" onclick="resetGraphView()" title="' + t('memory.resetView') + '">' +
|
'<button class="btn-icon" onclick="zoomGraphIn()" title="' + t('memory.zoomIn') + '">' +
|
||||||
|
'<i data-lucide="zoom-in" class="w-4 h-4"></i>' +
|
||||||
|
'</button>' +
|
||||||
|
'<button class="btn-icon" onclick="zoomGraphOut()" title="' + t('memory.zoomOut') + '">' +
|
||||||
|
'<i data-lucide="zoom-out" class="w-4 h-4"></i>' +
|
||||||
|
'</button>' +
|
||||||
|
'<button class="btn-icon" onclick="fitGraphToView()" title="' + t('memory.fitView') + '">' +
|
||||||
'<i data-lucide="maximize-2" class="w-4 h-4"></i>' +
|
'<i data-lucide="maximize-2" class="w-4 h-4"></i>' +
|
||||||
'</button>' +
|
'</button>' +
|
||||||
|
'<button class="btn-icon" onclick="resetGraphView()" title="' + t('memory.resetView') + '">' +
|
||||||
|
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
|
||||||
|
'</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="memory-graph-container" id="memoryGraphSvg"></div>' +
|
'<div class="memory-graph-container" id="memoryGraphSvg"></div>' +
|
||||||
@@ -223,81 +466,140 @@ function renderMemoryGraph(graphData) {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
var width = container.clientWidth || 600;
|
var width = container.clientWidth || 600;
|
||||||
var height = container.clientHeight || 500;
|
var height = container.clientHeight || 400;
|
||||||
|
|
||||||
// Clear existing
|
// Clear existing
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
var svg = d3.select('#memoryGraphSvg')
|
// Filter and clean nodes - remove invalid names (like JSON data)
|
||||||
|
var cleanNodes = graphData.nodes.filter(function(node) {
|
||||||
|
var name = node.name || node.id || '';
|
||||||
|
// Filter out JSON-like data, error messages, and very long strings
|
||||||
|
if (name.length > 100) return false;
|
||||||
|
if (name.includes('"status"') || name.includes('"content"')) return false;
|
||||||
|
if (name.includes('"todos"') || name.includes('"activeForm"')) return false;
|
||||||
|
if (name.startsWith('{') || name.startsWith('[')) return false;
|
||||||
|
// Allow all valid node types: file, module, component
|
||||||
|
return true;
|
||||||
|
}).map(function(node) {
|
||||||
|
// Truncate long names for display
|
||||||
|
var displayName = node.name || node.id || 'Unknown';
|
||||||
|
if (displayName.length > 25) {
|
||||||
|
displayName = displayName.substring(0, 22) + '...';
|
||||||
|
}
|
||||||
|
return Object.assign({}, node, { displayName: displayName });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter edges to only include valid nodes
|
||||||
|
var nodeIds = new Set(cleanNodes.map(function(n) { return n.id; }));
|
||||||
|
var cleanEdges = graphData.edges.filter(function(edge) {
|
||||||
|
var sourceId = typeof edge.source === 'object' ? edge.source.id : edge.source;
|
||||||
|
var targetId = typeof edge.target === 'object' ? edge.target.id : edge.target;
|
||||||
|
return nodeIds.has(sourceId) && nodeIds.has(targetId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create SVG with zoom support
|
||||||
|
graphSvg = d3.select('#memoryGraphSvg')
|
||||||
.append('svg')
|
.append('svg')
|
||||||
.attr('width', width)
|
.attr('width', width)
|
||||||
.attr('height', height)
|
.attr('height', height)
|
||||||
.attr('class', 'memory-graph-svg');
|
.attr('class', 'memory-graph-svg')
|
||||||
|
.attr('viewBox', [0, 0, width, height]);
|
||||||
|
|
||||||
|
// Create a group for zoom/pan transformations
|
||||||
|
graphGroup = graphSvg.append('g').attr('class', 'graph-content');
|
||||||
|
|
||||||
|
// Setup zoom behavior
|
||||||
|
graphZoom = d3.zoom()
|
||||||
|
.scaleExtent([0.1, 4])
|
||||||
|
.on('zoom', function(event) {
|
||||||
|
graphGroup.attr('transform', event.transform);
|
||||||
|
});
|
||||||
|
|
||||||
|
graphSvg.call(graphZoom);
|
||||||
|
|
||||||
// Create force simulation
|
// Create force simulation
|
||||||
var simulation = d3.forceSimulation(graphData.nodes)
|
graphSimulation = d3.forceSimulation(cleanNodes)
|
||||||
.force('link', d3.forceLink(graphData.edges).id(function(d) { return d.id; }).distance(100))
|
.force('link', d3.forceLink(cleanEdges).id(function(d) { return d.id; }).distance(80))
|
||||||
.force('charge', d3.forceManyBody().strength(-300))
|
.force('charge', d3.forceManyBody().strength(-200))
|
||||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||||
.force('collision', d3.forceCollide().radius(function(d) { return (d.heat || 10) + 5; }));
|
.force('collision', d3.forceCollide().radius(function(d) { return Math.max(15, (d.heat || 10) + 10); }))
|
||||||
|
.force('x', d3.forceX(width / 2).strength(0.05))
|
||||||
|
.force('y', d3.forceY(height / 2).strength(0.05));
|
||||||
|
|
||||||
// Draw edges
|
// Draw edges
|
||||||
var link = svg.append('g')
|
var link = graphGroup.append('g')
|
||||||
|
.attr('class', 'graph-links')
|
||||||
.selectAll('line')
|
.selectAll('line')
|
||||||
.data(graphData.edges)
|
.data(cleanEdges)
|
||||||
.enter()
|
.enter()
|
||||||
.append('line')
|
.append('line')
|
||||||
.attr('class', 'graph-edge')
|
.attr('class', 'graph-edge')
|
||||||
.attr('stroke-width', function(d) { return Math.sqrt(d.weight || 1); });
|
.attr('stroke-width', function(d) { return Math.sqrt(d.weight || 1); });
|
||||||
|
|
||||||
// Draw nodes
|
// Draw nodes
|
||||||
var node = svg.append('g')
|
var node = graphGroup.append('g')
|
||||||
.selectAll('circle')
|
.attr('class', 'graph-nodes')
|
||||||
.data(graphData.nodes)
|
.selectAll('g')
|
||||||
|
.data(cleanNodes)
|
||||||
.enter()
|
.enter()
|
||||||
.append('circle')
|
.append('g')
|
||||||
.attr('class', function(d) { return 'graph-node ' + (d.type || 'file'); })
|
.attr('class', function(d) { return 'graph-node-group ' + (d.type || 'file'); })
|
||||||
.attr('r', function(d) { return (d.heat || 10); })
|
|
||||||
.attr('data-id', function(d) { return d.id; })
|
|
||||||
.call(d3.drag()
|
.call(d3.drag()
|
||||||
.on('start', dragstarted)
|
.on('start', dragstarted)
|
||||||
.on('drag', dragged)
|
.on('drag', dragged)
|
||||||
.on('end', dragended))
|
.on('end', dragended))
|
||||||
.on('click', function(event, d) {
|
.on('click', function(event, d) {
|
||||||
|
event.stopPropagation();
|
||||||
selectNode(d);
|
selectNode(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Node labels
|
// Add circles to nodes
|
||||||
var label = svg.append('g')
|
node.append('circle')
|
||||||
.selectAll('text')
|
.attr('class', function(d) { return 'graph-node ' + (d.type || 'file'); })
|
||||||
.data(graphData.nodes)
|
.attr('r', function(d) { return Math.max(8, Math.min(20, (d.heat || 10))); })
|
||||||
.enter()
|
.attr('data-id', function(d) { return d.id; });
|
||||||
.append('text')
|
|
||||||
|
// Add labels to nodes
|
||||||
|
node.append('text')
|
||||||
.attr('class', 'graph-label')
|
.attr('class', 'graph-label')
|
||||||
.text(function(d) { return d.name || d.id; })
|
.text(function(d) {
|
||||||
.attr('x', 8)
|
// Show file count for modules
|
||||||
.attr('y', 3);
|
if (d.type === 'module' && d.fileCount) {
|
||||||
|
return d.displayName + ' (' + d.fileCount + ')';
|
||||||
|
}
|
||||||
|
return d.displayName;
|
||||||
|
})
|
||||||
|
.attr('x', function(d) { return Math.max(10, (d.heat || 10)) + 4; })
|
||||||
|
.attr('y', 4)
|
||||||
|
.attr('font-size', '11px');
|
||||||
|
|
||||||
// Update positions on simulation tick
|
// Update positions on simulation tick
|
||||||
simulation.on('tick', function() {
|
graphSimulation.on('tick', function() {
|
||||||
link
|
link
|
||||||
.attr('x1', function(d) { return d.source.x; })
|
.attr('x1', function(d) { return d.source.x; })
|
||||||
.attr('y1', function(d) { return d.source.y; })
|
.attr('y1', function(d) { return d.source.y; })
|
||||||
.attr('x2', function(d) { return d.target.x; })
|
.attr('x2', function(d) { return d.target.x; })
|
||||||
.attr('y2', function(d) { return d.target.y; });
|
.attr('y2', function(d) { return d.target.y; });
|
||||||
|
|
||||||
node
|
node.attr('transform', function(d) {
|
||||||
.attr('cx', function(d) { return d.x; })
|
return 'translate(' + d.x + ',' + d.y + ')';
|
||||||
.attr('cy', function(d) { return d.y; });
|
});
|
||||||
|
|
||||||
label
|
|
||||||
.attr('x', function(d) { return d.x + 8; })
|
|
||||||
.attr('y', function(d) { return d.y + 3; });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-fit after simulation stabilizes
|
||||||
|
graphSimulation.on('end', function() {
|
||||||
|
fitGraphToView();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also fit after initial layout
|
||||||
|
setTimeout(function() {
|
||||||
|
fitGraphToView();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
// Drag functions
|
// Drag functions
|
||||||
function dragstarted(event, d) {
|
function dragstarted(event, d) {
|
||||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
if (!event.active) graphSimulation.alphaTarget(0.3).restart();
|
||||||
d.fx = d.x;
|
d.fx = d.x;
|
||||||
d.fy = d.y;
|
d.fy = d.y;
|
||||||
}
|
}
|
||||||
@@ -308,18 +610,94 @@ function renderMemoryGraph(graphData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dragended(event, d) {
|
function dragended(event, d) {
|
||||||
if (!event.active) simulation.alphaTarget(0);
|
if (!event.active) graphSimulation.alphaTarget(0);
|
||||||
d.fx = null;
|
d.fx = null;
|
||||||
d.fy = null;
|
d.fy = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Graph Zoom Controls ==========
|
||||||
|
function zoomGraphIn() {
|
||||||
|
if (graphSvg && graphZoom) {
|
||||||
|
graphSvg.transition().duration(300).call(graphZoom.scaleBy, 1.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomGraphOut() {
|
||||||
|
if (graphSvg && graphZoom) {
|
||||||
|
graphSvg.transition().duration(300).call(graphZoom.scaleBy, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitGraphToView() {
|
||||||
|
if (!graphSvg || !graphGroup || !graphZoom) return;
|
||||||
|
|
||||||
|
var container = document.getElementById('memoryGraphSvg');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
var width = container.clientWidth || 600;
|
||||||
|
var height = container.clientHeight || 400;
|
||||||
|
|
||||||
|
// Get the bounds of all nodes
|
||||||
|
var bounds = graphGroup.node().getBBox();
|
||||||
|
if (bounds.width === 0 || bounds.height === 0) return;
|
||||||
|
|
||||||
|
// Calculate scale to fit with padding
|
||||||
|
var padding = 40;
|
||||||
|
var scale = Math.min(
|
||||||
|
(width - padding * 2) / bounds.width,
|
||||||
|
(height - padding * 2) / bounds.height
|
||||||
|
);
|
||||||
|
scale = Math.min(Math.max(scale, 0.2), 2); // Clamp scale between 0.2 and 2
|
||||||
|
|
||||||
|
// Calculate translation to center
|
||||||
|
var tx = (width - bounds.width * scale) / 2 - bounds.x * scale;
|
||||||
|
var ty = (height - bounds.height * scale) / 2 - bounds.y * scale;
|
||||||
|
|
||||||
|
// Apply transform with animation
|
||||||
|
graphSvg.transition()
|
||||||
|
.duration(500)
|
||||||
|
.call(graphZoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
function centerGraphOnNode(nodeId) {
|
||||||
|
if (!graphSvg || !graphGroup || !graphZoom) return;
|
||||||
|
|
||||||
|
var container = document.getElementById('memoryGraphSvg');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
var width = container.clientWidth || 600;
|
||||||
|
var height = container.clientHeight || 400;
|
||||||
|
|
||||||
|
// Find the node
|
||||||
|
var nodeData = null;
|
||||||
|
graphGroup.selectAll('.graph-node-group').each(function(d) {
|
||||||
|
if (d.id === nodeId) nodeData = d;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!nodeData || nodeData.x === undefined) return;
|
||||||
|
|
||||||
|
// Calculate translation to center on node
|
||||||
|
var scale = 1.2;
|
||||||
|
var tx = width / 2 - nodeData.x * scale;
|
||||||
|
var ty = height / 2 - nodeData.y * scale;
|
||||||
|
|
||||||
|
graphSvg.transition()
|
||||||
|
.duration(500)
|
||||||
|
.call(graphZoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
|
||||||
|
}
|
||||||
|
|
||||||
function selectNode(node) {
|
function selectNode(node) {
|
||||||
selectedNode = node;
|
selectedNode = node;
|
||||||
|
|
||||||
// Highlight in graph
|
// Highlight in graph
|
||||||
d3.selectAll('.graph-node').classed('selected', false);
|
if (graphGroup) {
|
||||||
d3.selectAll('.graph-node[data-id="' + node.id + '"]').classed('selected', true);
|
graphGroup.selectAll('.graph-node').classed('selected', false);
|
||||||
|
graphGroup.selectAll('.graph-node[data-id="' + node.id + '"]').classed('selected', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center graph on selected node
|
||||||
|
centerGraphOnNode(node.id);
|
||||||
|
|
||||||
// Show node details in context column
|
// Show node details in context column
|
||||||
showNodeDetails(node);
|
showNodeDetails(node);
|
||||||
@@ -329,19 +707,15 @@ function highlightNode(path) {
|
|||||||
var node = memoryGraphData.nodes.find(function(n) { return n.path === path || n.id === path; });
|
var node = memoryGraphData.nodes.find(function(n) { return n.path === path || n.id === path; });
|
||||||
if (node) {
|
if (node) {
|
||||||
selectNode(node);
|
selectNode(node);
|
||||||
// Center graph on node if possible
|
|
||||||
if (typeof d3 !== 'undefined') {
|
|
||||||
var container = document.getElementById('memoryGraphSvg');
|
|
||||||
if (container) {
|
|
||||||
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetGraphView() {
|
function resetGraphView() {
|
||||||
selectedNode = null;
|
selectedNode = null;
|
||||||
d3.selectAll('.graph-node').classed('selected', false);
|
if (graphGroup) {
|
||||||
|
graphGroup.selectAll('.graph-node').classed('selected', false);
|
||||||
|
}
|
||||||
|
fitGraphToView();
|
||||||
renderContextColumn();
|
renderContextColumn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,13 +756,14 @@ function renderContextTimeline(prompts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return '<div class="context-timeline">' +
|
return '<div class="context-timeline">' +
|
||||||
prompts.map(function(item) {
|
prompts.map(function(item, index) {
|
||||||
var timestamp = item.timestamp ? formatTimestamp(item.timestamp) : 'Unknown time';
|
var timestamp = item.timestamp ? formatTimestamp(item.timestamp) : 'Unknown time';
|
||||||
var type = item.type || 'unknown';
|
var type = item.type || 'unknown';
|
||||||
var typeIcon = type === 'read' ? 'eye' : type === 'edit' ? 'pencil' : 'file-text';
|
var typeIcon = type === 'read' ? 'eye' : type === 'write' ? 'pencil' : type === 'edit' ? 'pencil' : 'file-text';
|
||||||
var files = item.files || [];
|
var files = item.files || [];
|
||||||
|
var description = item.prompt || item.description || 'No description';
|
||||||
|
|
||||||
return '<div class="timeline-item">' +
|
return '<div class="timeline-item" data-index="' + index + '" onclick="toggleTimelineItem(this)">' +
|
||||||
'<div class="timeline-icon ' + type + '">' +
|
'<div class="timeline-icon ' + type + '">' +
|
||||||
'<i data-lucide="' + typeIcon + '" class="w-3.5 h-3.5"></i>' +
|
'<i data-lucide="' + typeIcon + '" class="w-3.5 h-3.5"></i>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
@@ -397,14 +772,13 @@ function renderContextTimeline(prompts) {
|
|||||||
'<span class="timeline-type">' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)) + '</span>' +
|
'<span class="timeline-type">' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)) + '</span>' +
|
||||||
'<span class="timeline-time">' + timestamp + '</span>' +
|
'<span class="timeline-time">' + timestamp + '</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="timeline-prompt">' + escapeHtml(item.prompt || item.description || 'No description') + '</div>' +
|
'<div class="timeline-prompt">' + escapeHtml(description) + '</div>' +
|
||||||
(files.length > 0 ? '<div class="timeline-files">' +
|
(files.length > 0 ? '<div class="timeline-files">' +
|
||||||
files.slice(0, 3).map(function(f) {
|
files.map(function(f) {
|
||||||
return '<span class="file-tag" onclick="highlightNode(\'' + escapeHtml(f) + '\')">' +
|
return '<span class="file-tag" onclick="event.stopPropagation(); highlightNode(\'' + escapeHtml(f) + '\')">' +
|
||||||
'<i data-lucide="file" class="w-3 h-3"></i> ' + escapeHtml(f.split('/').pop().split('\\').pop()) +
|
'<i data-lucide="file" class="w-3 h-3"></i> ' + escapeHtml(f.split('/').pop().split('\\').pop()) +
|
||||||
'</span>';
|
'</span>';
|
||||||
}).join('') +
|
}).join('') +
|
||||||
(files.length > 3 ? '<span class="file-tag more">+' + (files.length - 3) + ' more</span>' : '') +
|
|
||||||
'</div>' : '') +
|
'</div>' : '') +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
@@ -412,10 +786,17 @@ function renderContextTimeline(prompts) {
|
|||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle timeline item expansion
|
||||||
|
*/
|
||||||
|
function toggleTimelineItem(element) {
|
||||||
|
element.classList.toggle('expanded');
|
||||||
|
}
|
||||||
|
|
||||||
function renderContextStats() {
|
function renderContextStats() {
|
||||||
var totalReads = recentContext.filter(function(c) { return c.type === 'read'; }).length;
|
var totalReads = recentContext.filter(function(c) { return c.type === 'read'; }).length;
|
||||||
var totalEdits = recentContext.filter(function(c) { return c.type === 'edit'; }).length;
|
var totalEdits = recentContext.filter(function(c) { return c.type === 'edit' || c.type === 'write'; }).length;
|
||||||
var totalPrompts = recentContext.filter(function(c) { return c.type === 'prompt'; }).length;
|
var totalMentions = recentContext.filter(function(c) { return c.type === 'mention'; }).length;
|
||||||
|
|
||||||
return '<div class="context-stats">' +
|
return '<div class="context-stats">' +
|
||||||
'<div class="context-stat-item">' +
|
'<div class="context-stat-item">' +
|
||||||
@@ -430,8 +811,8 @@ function renderContextStats() {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="context-stat-item">' +
|
'<div class="context-stat-item">' +
|
||||||
'<i data-lucide="message-square" class="w-4 h-4"></i>' +
|
'<i data-lucide="message-square" class="w-4 h-4"></i>' +
|
||||||
'<span class="stat-label">' + t('memory.prompts') + '</span>' +
|
'<span class="stat-label">' + t('memory.mentions') + '</span>' +
|
||||||
'<span class="stat-value">' + totalPrompts + '</span>' +
|
'<span class="stat-value">' + totalMentions + '</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,6 +299,35 @@ function buildCommand(params: {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'claude':
|
||||||
|
// Claude Code: claude -p "prompt" for non-interactive mode
|
||||||
|
args.push('-p'); // Print mode (non-interactive)
|
||||||
|
// Native resume: claude --resume <session-id> or --continue
|
||||||
|
if (nativeResume?.enabled) {
|
||||||
|
if (nativeResume.isLatest) {
|
||||||
|
args.push('--continue');
|
||||||
|
} else if (nativeResume.sessionId) {
|
||||||
|
args.push('--resume', nativeResume.sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (model) {
|
||||||
|
args.push('--model', model);
|
||||||
|
}
|
||||||
|
// Permission modes for write/auto
|
||||||
|
if (mode === 'write' || mode === 'auto') {
|
||||||
|
args.push('--dangerously-skip-permissions');
|
||||||
|
}
|
||||||
|
// Output format for better parsing
|
||||||
|
args.push('--output-format', 'text');
|
||||||
|
// Add directories
|
||||||
|
if (include) {
|
||||||
|
const dirs = include.split(',').map(d => d.trim()).filter(d => d);
|
||||||
|
for (const addDir of dirs) {
|
||||||
|
args.push('--add-dir', addDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown CLI tool: ${tool}`);
|
throw new Error(`Unknown CLI tool: ${tool}`);
|
||||||
}
|
}
|
||||||
@@ -1172,7 +1201,7 @@ export async function batchDeleteExecutionsAsync(baseDir: string, ids: string[])
|
|||||||
* Get status of all CLI tools
|
* Get status of all CLI tools
|
||||||
*/
|
*/
|
||||||
export async function getCliToolsStatus(): Promise<Record<string, ToolAvailability>> {
|
export async function getCliToolsStatus(): Promise<Record<string, ToolAvailability>> {
|
||||||
const tools = ['gemini', 'qwen', 'codex'];
|
const tools = ['gemini', 'qwen', 'codex', 'claude'];
|
||||||
const results: Record<string, ToolAvailability> = {};
|
const results: Record<string, ToolAvailability> = {};
|
||||||
|
|
||||||
await Promise.all(tools.map(async (tool) => {
|
await Promise.all(tools.map(async (tool) => {
|
||||||
|
|||||||
@@ -187,15 +187,27 @@ export class CliHistoryStore {
|
|||||||
* Migrate schema for existing databases
|
* Migrate schema for existing databases
|
||||||
*/
|
*/
|
||||||
private migrateSchema(): void {
|
private migrateSchema(): void {
|
||||||
// Check if category column exists
|
try {
|
||||||
const tableInfo = this.db.prepare('PRAGMA table_info(conversations)').all() as Array<{ name: string }>;
|
// Check if category column exists
|
||||||
const hasCategory = tableInfo.some(col => col.name === 'category');
|
const tableInfo = this.db.prepare('PRAGMA table_info(conversations)').all() as Array<{ name: string }>;
|
||||||
|
const hasCategory = tableInfo.some(col => col.name === 'category');
|
||||||
|
|
||||||
if (!hasCategory) {
|
if (!hasCategory) {
|
||||||
this.db.exec(`
|
console.log('[CLI History] Migrating database: adding category column...');
|
||||||
ALTER TABLE conversations ADD COLUMN category TEXT DEFAULT 'user';
|
this.db.exec(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_conversations_category ON conversations(category);
|
ALTER TABLE conversations ADD COLUMN category TEXT DEFAULT 'user';
|
||||||
`);
|
`);
|
||||||
|
// Create index separately to handle potential errors
|
||||||
|
try {
|
||||||
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_category ON conversations(category);`);
|
||||||
|
} catch (indexErr) {
|
||||||
|
console.warn('[CLI History] Category index creation warning:', (indexErr as Error).message);
|
||||||
|
}
|
||||||
|
console.log('[CLI History] Migration complete: category column added');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[CLI History] Migration error:', (err as Error).message);
|
||||||
|
// Don't throw - allow the store to continue working with existing schema
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -432,11 +432,103 @@ class CodexSessionDiscoverer extends SessionDiscoverer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude Code Session Discoverer
|
||||||
|
* Path: ~/.claude/projects/<projectHash>/sessions/*.jsonl
|
||||||
|
* Claude Code stores sessions with UUID-based session IDs
|
||||||
|
*/
|
||||||
|
class ClaudeSessionDiscoverer extends SessionDiscoverer {
|
||||||
|
tool = 'claude';
|
||||||
|
basePath = join(getHomePath(), '.claude', 'projects');
|
||||||
|
|
||||||
|
getSessions(options: SessionDiscoveryOptions = {}): NativeSession[] {
|
||||||
|
const { workingDir, limit, afterTimestamp } = options;
|
||||||
|
const sessions: NativeSession[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!existsSync(this.basePath)) return [];
|
||||||
|
|
||||||
|
// If workingDir provided, only look in that project's folder
|
||||||
|
let projectDirs: string[];
|
||||||
|
if (workingDir) {
|
||||||
|
const projectHash = calculateProjectHash(workingDir);
|
||||||
|
const projectPath = join(this.basePath, projectHash);
|
||||||
|
projectDirs = existsSync(projectPath) ? [projectHash] : [];
|
||||||
|
} else {
|
||||||
|
projectDirs = readdirSync(this.basePath).filter(d => {
|
||||||
|
const fullPath = join(this.basePath, d);
|
||||||
|
return statSync(fullPath).isDirectory();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const projectHash of projectDirs) {
|
||||||
|
const sessionsDir = join(this.basePath, projectHash, 'sessions');
|
||||||
|
if (!existsSync(sessionsDir)) continue;
|
||||||
|
|
||||||
|
const sessionFiles = readdirSync(sessionsDir)
|
||||||
|
.filter(f => f.endsWith('.jsonl') || f.endsWith('.json'))
|
||||||
|
.map(f => ({
|
||||||
|
name: f,
|
||||||
|
path: join(sessionsDir, f),
|
||||||
|
stat: statSync(join(sessionsDir, f))
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
|
||||||
|
|
||||||
|
for (const file of sessionFiles) {
|
||||||
|
if (afterTimestamp && file.stat.mtime <= afterTimestamp) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract session ID from filename or content
|
||||||
|
const uuidMatch = file.name.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
||||||
|
if (uuidMatch) {
|
||||||
|
sessions.push({
|
||||||
|
sessionId: uuidMatch[1],
|
||||||
|
tool: this.tool,
|
||||||
|
filePath: file.path,
|
||||||
|
projectHash,
|
||||||
|
createdAt: file.stat.birthtime,
|
||||||
|
updatedAt: file.stat.mtime
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Try reading first line for session metadata
|
||||||
|
const firstLine = readFileSync(file.path, 'utf8').split('\n')[0];
|
||||||
|
const meta = JSON.parse(firstLine);
|
||||||
|
if (meta.session_id) {
|
||||||
|
sessions.push({
|
||||||
|
sessionId: meta.session_id,
|
||||||
|
tool: this.tool,
|
||||||
|
filePath: file.path,
|
||||||
|
projectHash,
|
||||||
|
createdAt: new Date(meta.timestamp || file.stat.birthtime),
|
||||||
|
updatedAt: file.stat.mtime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip invalid files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
||||||
|
return limit ? sessions.slice(0, limit) : sessions;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findSessionById(sessionId: string): NativeSession | null {
|
||||||
|
const sessions = this.getSessions();
|
||||||
|
return sessions.find(s => s.sessionId === sessionId) || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Singleton discoverers
|
// Singleton discoverers
|
||||||
const discoverers: Record<string, SessionDiscoverer> = {
|
const discoverers: Record<string, SessionDiscoverer> = {
|
||||||
gemini: new GeminiSessionDiscoverer(),
|
gemini: new GeminiSessionDiscoverer(),
|
||||||
qwen: new QwenSessionDiscoverer(),
|
qwen: new QwenSessionDiscoverer(),
|
||||||
codex: new CodexSessionDiscoverer()
|
codex: new CodexSessionDiscoverer(),
|
||||||
|
claude: new ClaudeSessionDiscoverer()
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user