diff --git a/assets/images/ecc-logo.png b/assets/images/ecc-logo.png new file mode 100644 index 00000000..ef6e8ac8 Binary files /dev/null and b/assets/images/ecc-logo.png differ diff --git a/ecc_dashboard.py b/ecc_dashboard.py new file mode 100644 index 00000000..b2ea49d3 --- /dev/null +++ b/ecc_dashboard.py @@ -0,0 +1,914 @@ +#!/usr/bin/env python3 +""" +ECC Dashboard - Everything Claude Code GUI +Cross-platform TkInter application for managing ECC components +""" + +import tkinter as tk +from tkinter import ttk, scrolledtext, messagebox +import os +import json +from typing import Dict, List, Optional + +# ============================================================================ +# DATA LOADERS - Load ECC data from the project +# ============================================================================ + +def get_project_path() -> str: + """Get the ECC project path - assumes this script is run from the project dir""" + return os.path.dirname(os.path.abspath(__file__)) + +def load_agents(project_path: str) -> List[Dict]: + """Load agents from AGENTS.md""" + agents_file = os.path.join(project_path, "AGENTS.md") + agents = [] + + if os.path.exists(agents_file): + with open(agents_file, 'r', encoding='utf-8') as f: + content = f.read() + + # Parse agent table from AGENTS.md + lines = content.split('\n') + in_table = False + for line in lines: + if '| Agent | Purpose | When to Use |' in line: + in_table = True + continue + if in_table and line.startswith('|'): + parts = [p.strip() for p in line.split('|')] + if len(parts) >= 4 and parts[1] and parts[1] != 'Agent': + agents.append({ + 'name': parts[1], + 'purpose': parts[2], + 'when_to_use': parts[3] + }) + + # Fallback default agents if file not found + if not agents: + agents = [ + {'name': 'planner', 'purpose': 'Implementation planning', 'when_to_use': 'Complex features, refactoring'}, + {'name': 'architect', 'purpose': 'System design and scalability', 'when_to_use': 'Architectural decisions'}, + {'name': 'tdd-guide', 'purpose': 'Test-driven development', 'when_to_use': 'New features, bug fixes'}, + {'name': 'code-reviewer', 'purpose': 'Code quality and maintainability', 'when_to_use': 'After writing/modifying code'}, + {'name': 'security-reviewer', 'purpose': 'Vulnerability detection', 'when_to_use': 'Before commits, sensitive code'}, + {'name': 'build-error-resolver', 'purpose': 'Fix build/type errors', 'when_to_use': 'When build fails'}, + {'name': 'e2e-runner', 'purpose': 'End-to-end Playwright testing', 'when_to_use': 'Critical user flows'}, + {'name': 'refactor-cleaner', 'purpose': 'Dead code cleanup', 'when_to_use': 'Code maintenance'}, + {'name': 'doc-updater', 'purpose': 'Documentation and codemaps', 'when_to_use': 'Updating docs'}, + {'name': 'go-reviewer', 'purpose': 'Go code review', 'when_to_use': 'Go projects'}, + {'name': 'python-reviewer', 'purpose': 'Python code review', 'when_to_use': 'Python projects'}, + {'name': 'typescript-reviewer', 'purpose': 'TypeScript/JavaScript code review', 'when_to_use': 'TypeScript projects'}, + {'name': 'rust-reviewer', 'purpose': 'Rust code review', 'when_to_use': 'Rust projects'}, + {'name': 'java-reviewer', 'purpose': 'Java and Spring Boot code review', 'when_to_use': 'Java projects'}, + {'name': 'kotlin-reviewer', 'purpose': 'Kotlin code review', 'when_to_use': 'Kotlin projects'}, + {'name': 'cpp-reviewer', 'purpose': 'C/C++ code review', 'when_to_use': 'C/C++ projects'}, + {'name': 'database-reviewer', 'purpose': 'PostgreSQL/Supabase specialist', 'when_to_use': 'Database work'}, + {'name': 'loop-operator', 'purpose': 'Autonomous loop execution', 'when_to_use': 'Run loops safely'}, + {'name': 'harness-optimizer', 'purpose': 'Harness config tuning', 'when_to_use': 'Reliability, cost, throughput'}, + ] + + return agents + +def load_skills(project_path: str) -> List[Dict]: + """Load skills from skills directory""" + skills_dir = os.path.join(project_path, "skills") + skills = [] + + if os.path.exists(skills_dir): + for item in os.listdir(skills_dir): + skill_path = os.path.join(skills_dir, item) + if os.path.isdir(skill_path): + skill_file = os.path.join(skill_path, "SKILL.md") + description = item.replace('-', ' ').title() + + if os.path.exists(skill_file): + try: + with open(skill_file, 'r', encoding='utf-8') as f: + content = f.read() + # Extract description from first lines + lines = content.split('\n') + for line in lines: + if line.strip() and not line.startswith('#'): + description = line.strip()[:100] + break + if line.startswith('# '): + description = line[2:].strip()[:100] + break + except: + pass + + # Determine category + category = "General" + item_lower = item.lower() + if 'python' in item_lower or 'django' in item_lower: + category = "Python" + elif 'golang' in item_lower or 'go-' in item_lower: + category = "Go" + elif 'frontend' in item_lower or 'react' in item_lower: + category = "Frontend" + elif 'backend' in item_lower or 'api' in item_lower: + category = "Backend" + elif 'security' in item_lower: + category = "Security" + elif 'testing' in item_lower or 'tdd' in item_lower: + category = "Testing" + elif 'docker' in item_lower or 'deployment' in item_lower: + category = "DevOps" + elif 'swift' in item_lower or 'ios' in item_lower: + category = "iOS" + elif 'java' in item_lower or 'spring' in item_lower: + category = "Java" + elif 'rust' in item_lower: + category = "Rust" + + skills.append({ + 'name': item, + 'description': description, + 'category': category, + 'path': skill_path + }) + + # Fallback if directory doesn't exist + if not skills: + skills = [ + {'name': 'tdd-workflow', 'description': 'Test-driven development workflow', 'category': 'Testing'}, + {'name': 'coding-standards', 'description': 'Baseline coding conventions', 'category': 'General'}, + {'name': 'security-review', 'description': 'Security checklist and patterns', 'category': 'Security'}, + {'name': 'frontend-patterns', 'description': 'React and Next.js patterns', 'category': 'Frontend'}, + {'name': 'backend-patterns', 'description': 'API and database patterns', 'category': 'Backend'}, + {'name': 'api-design', 'description': 'REST API design patterns', 'category': 'Backend'}, + {'name': 'docker-patterns', 'description': 'Docker and container patterns', 'category': 'DevOps'}, + {'name': 'e2e-testing', 'description': 'Playwright E2E testing patterns', 'category': 'Testing'}, + {'name': 'verification-loop', 'description': 'Build, test, lint verification', 'category': 'General'}, + {'name': 'python-patterns', 'description': 'Python idioms and best practices', 'category': 'Python'}, + {'name': 'golang-patterns', 'description': 'Go idioms and best practices', 'category': 'Go'}, + {'name': 'django-patterns', 'description': 'Django patterns and best practices', 'category': 'Python'}, + {'name': 'springboot-patterns', 'description': 'Java Spring Boot patterns', 'category': 'Java'}, + {'name': 'laravel-patterns', 'description': 'Laravel architecture patterns', 'category': 'PHP'}, + ] + + return skills + +def load_commands(project_path: str) -> List[Dict]: + """Load commands from commands directory""" + commands_dir = os.path.join(project_path, "commands") + commands = [] + + if os.path.exists(commands_dir): + for item in os.listdir(commands_dir): + if item.endswith('.md'): + cmd_name = item[:-3] + description = "" + + try: + with open(os.path.join(commands_dir, item), 'r', encoding='utf-8') as f: + content = f.read() + lines = content.split('\n') + for line in lines: + if line.startswith('# '): + description = line[2:].strip() + break + except: + pass + + commands.append({ + 'name': cmd_name, + 'description': description or cmd_name.replace('-', ' ').title() + }) + + # Fallback commands + if not commands: + commands = [ + {'name': 'plan', 'description': 'Create implementation plan'}, + {'name': 'tdd', 'description': 'Test-driven development workflow'}, + {'name': 'code-review', 'description': 'Review code for quality and security'}, + {'name': 'build-fix', 'description': 'Fix build and TypeScript errors'}, + {'name': 'e2e', 'description': 'Generate and run E2E tests'}, + {'name': 'refactor-clean', 'description': 'Remove dead code'}, + {'name': 'verify', 'description': 'Run verification loop'}, + {'name': 'eval', 'description': 'Run evaluation against criteria'}, + {'name': 'security', 'description': 'Run comprehensive security review'}, + {'name': 'test-coverage', 'description': 'Analyze test coverage'}, + {'name': 'update-docs', 'description': 'Update documentation'}, + {'name': 'setup-pm', 'description': 'Configure package manager'}, + {'name': 'go-review', 'description': 'Go code review'}, + {'name': 'go-test', 'description': 'Go TDD workflow'}, + {'name': 'python-review', 'description': 'Python code review'}, + ] + + return commands + +def load_rules(project_path: str) -> List[Dict]: + """Load rules from rules directory""" + rules_dir = os.path.join(project_path, "rules") + rules = [] + + if os.path.exists(rules_dir): + for item in os.listdir(rules_dir): + item_path = os.path.join(rules_dir, item) + if os.path.isdir(item_path): + # Common rules + if item == "common": + for file in os.listdir(item_path): + if file.endswith('.md'): + rules.append({ + 'name': file[:-3], + 'language': 'Common', + 'path': os.path.join(item_path, file) + }) + else: + # Language-specific rules + for file in os.listdir(item_path): + if file.endswith('.md'): + rules.append({ + 'name': file[:-3], + 'language': item.title(), + 'path': os.path.join(item_path, file) + }) + + # Fallback rules + if not rules: + rules = [ + {'name': 'coding-style', 'language': 'Common', 'path': ''}, + {'name': 'git-workflow', 'language': 'Common', 'path': ''}, + {'name': 'testing', 'language': 'Common', 'path': ''}, + {'name': 'performance', 'language': 'Common', 'path': ''}, + {'name': 'patterns', 'language': 'Common', 'path': ''}, + {'name': 'security', 'language': 'Common', 'path': ''}, + {'name': 'typescript', 'language': 'TypeScript', 'path': ''}, + {'name': 'python', 'language': 'Python', 'path': ''}, + {'name': 'golang', 'language': 'Go', 'path': ''}, + {'name': 'swift', 'language': 'Swift', 'path': ''}, + {'name': 'php', 'language': 'PHP', 'path': ''}, + ] + + return rules + +# ============================================================================ +# MAIN APPLICATION +# ============================================================================ + +class ECCDashboard(tk.Tk): + """Main ECC Dashboard Application""" + + def __init__(self): + super().__init__() + + self.project_path = get_project_path() + self.title("ECC Dashboard - Everything Claude Code") + + self.state('zoomed') + + try: + self.icon_image = tk.PhotoImage(file='assets/images/ecc-logo.png') + self.iconphoto(True, self.icon_image) + except: + pass + + self.minsize(800, 600) + + # Load data + self.agents = load_agents(self.project_path) + self.skills = load_skills(self.project_path) + self.commands = load_commands(self.project_path) + self.rules = load_rules(self.project_path) + + # Settings + self.settings = { + 'project_path': self.project_path, + 'theme': 'light' + } + + # Setup UI + self.setup_styles() + self.create_widgets() + + # Center window + self.center_window() + + def setup_styles(self): + """Setup ttk styles for modern look""" + style = ttk.Style() + style.theme_use('clam') + + # Configure tab style + style.configure('TNotebook', background='#f0f0f0') + style.configure('TNotebook.Tab', padding=[10, 5], font=('Arial', 10)) + style.map('TNotebook.Tab', background=[('selected', '#ffffff')]) + + # Configure Treeview + style.configure('Treeview', font=('Arial', 10), rowheight=25) + style.configure('Treeview.Heading', font=('Arial', 10, 'bold')) + + # Configure buttons + style.configure('TButton', font=('Arial', 10), padding=5) + + def center_window(self): + """Center the window on screen""" + self.update_idletasks() + width = self.winfo_width() + height = self.winfo_height() + x = (self.winfo_screenwidth() // 2) - (width // 2) + y = (self.winfo_screenheight() // 2) - (height // 2) + self.geometry(f'{width}x{height}+{x}+{y}') + + def create_widgets(self): + """Create all UI widgets""" + # Main container + main_frame = ttk.Frame(self) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Header + header_frame = ttk.Frame(main_frame) + header_frame.pack(fill=tk.X, pady=(0, 10)) + + try: + self.logo_image = tk.PhotoImage(file='assets/images/ecc-logo.png') + self.logo_image = self.logo_image.subsample(2, 2) + ttk.Label(header_frame, image=self.logo_image).pack(side=tk.LEFT, padx=(0, 10)) + except: + pass + + self.title_label = ttk.Label(header_frame, text="ECC Dashboard", font=('Open Sans', 18, 'bold')) + self.title_label.pack(side=tk.LEFT) + self.version_label = ttk.Label(header_frame, text="v1.10.0", font=('Open Sans', 10), foreground='gray') + self.version_label.pack(side=tk.LEFT, padx=(10, 0)) + + # Notebook (tabs) + self.notebook = ttk.Notebook(main_frame) + self.notebook.pack(fill=tk.BOTH, expand=True) + + # Create tabs + self.create_agents_tab() + self.create_skills_tab() + self.create_commands_tab() + self.create_rules_tab() + self.create_settings_tab() + + # Status bar + status_frame = ttk.Frame(main_frame) + status_frame.pack(fill=tk.X, pady=(10, 0)) + + self.status_label = ttk.Label(status_frame, + text=f"Ready | Agents: {len(self.agents)} | Skills: {len(self.skills)} | Commands: {len(self.commands)}", + font=('Arial', 9), foreground='gray') + self.status_label.pack(side=tk.LEFT) + + # ========================================================================= + # AGENTS TAB + # ========================================================================= + + def create_agents_tab(self): + """Create Agents tab""" + frame = ttk.Frame(self.notebook) + self.notebook.add(frame, text=f"Agents ({len(self.agents)})") + + # Search bar + search_frame = ttk.Frame(frame) + search_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(search_frame, text="Search:").pack(side=tk.LEFT) + self.agent_search = ttk.Entry(search_frame, width=30) + self.agent_search.pack(side=tk.LEFT, padx=5) + self.agent_search.bind('', self.filter_agents) + + ttk.Label(search_frame, text="Count:").pack(side=tk.LEFT, padx=(20, 0)) + self.agent_count_label = ttk.Label(search_frame, text=str(len(self.agents))) + self.agent_count_label.pack(side=tk.LEFT) + + # Split pane: list + details + paned = ttk.PanedWindow(frame, orient=tk.HORIZONTAL) + paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) + + # Agent list + list_frame = ttk.Frame(paned) + paned.add(list_frame, weight=2) + + columns = ('name', 'purpose') + self.agent_tree = ttk.Treeview(list_frame, columns=columns, show='tree headings') + self.agent_tree.heading('#0', text='#') + self.agent_tree.heading('name', text='Agent Name') + self.agent_tree.heading('purpose', text='Purpose') + self.agent_tree.column('#0', width=40) + self.agent_tree.column('name', width=180) + self.agent_tree.column('purpose', width=250) + + self.agent_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # Scrollbar + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.agent_tree.yview) + self.agent_tree.configure(yscrollcommand=scrollbar.set) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Details panel + details_frame = ttk.Frame(paned) + paned.add(details_frame, weight=1) + + ttk.Label(details_frame, text="Details", font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=5) + + self.agent_details = scrolledtext.ScrolledText(details_frame, wrap=tk.WORD, height=15) + self.agent_details.pack(fill=tk.BOTH, expand=True) + + # Bind selection + self.agent_tree.bind('<>', self.on_agent_select) + + # Populate list + self.populate_agents(self.agents) + + def populate_agents(self, agents: List[Dict]): + """Populate agents list""" + for item in self.agent_tree.get_children(): + self.agent_tree.delete(item) + + for i, agent in enumerate(agents, 1): + self.agent_tree.insert('', tk.END, text=str(i), values=(agent['name'], agent['purpose'])) + + def filter_agents(self, event=None): + """Filter agents based on search""" + query = self.agent_search.get().lower() + + if not query: + filtered = self.agents + else: + filtered = [a for a in self.agents + if query in a['name'].lower() or query in a['purpose'].lower()] + + self.populate_agents(filtered) + self.agent_count_label.config(text=str(len(filtered))) + + def on_agent_select(self, event): + """Handle agent selection""" + selection = self.agent_tree.selection() + if not selection: + return + + item = self.agent_tree.item(selection[0]) + agent_name = item['values'][0] + + agent = next((a for a in self.agents if a['name'] == agent_name), None) + if agent: + details = f"""Agent: {agent['name']} + +Purpose: {agent['purpose']} + +When to Use: {agent['when_to_use']} + +--- +Usage in Claude Code: +Use the /{agent['name']} command or invoke via agent delegation.""" + self.agent_details.delete('1.0', tk.END) + self.agent_details.insert('1.0', details) + + # ========================================================================= + # SKILLS TAB + # ========================================================================= + + def create_skills_tab(self): + """Create Skills tab""" + frame = ttk.Frame(self.notebook) + self.notebook.add(frame, text=f"Skills ({len(self.skills)})") + + # Search and filter + filter_frame = ttk.Frame(frame) + filter_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(filter_frame, text="Search:").pack(side=tk.LEFT) + self.skill_search = ttk.Entry(filter_frame, width=25) + self.skill_search.pack(side=tk.LEFT, padx=5) + self.skill_search.bind('', self.filter_skills) + + ttk.Label(filter_frame, text="Category:").pack(side=tk.LEFT, padx=(20, 0)) + self.skill_category = ttk.Combobox(filter_frame, values=['All'] + self.get_categories(), width=15) + self.skill_category.set('All') + self.skill_category.pack(side=tk.LEFT, padx=5) + self.skill_category.bind('<>', self.filter_skills) + + ttk.Label(filter_frame, text="Count:").pack(side=tk.LEFT, padx=(20, 0)) + self.skill_count_label = ttk.Label(filter_frame, text=str(len(self.skills))) + self.skill_count_label.pack(side=tk.LEFT) + + # Split pane + paned = ttk.PanedWindow(frame, orient=tk.HORIZONTAL) + paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) + + # Skill list + list_frame = ttk.Frame(paned) + paned.add(list_frame, weight=1) + + columns = ('name', 'category', 'description') + self.skill_tree = ttk.Treeview(list_frame, columns=columns, show='tree headings') + self.skill_tree.heading('#0', text='#') + self.skill_tree.heading('name', text='Skill Name') + self.skill_tree.heading('category', text='Category') + self.skill_tree.heading('description', text='Description') + + self.skill_tree.column('#0', width=40) + self.skill_tree.column('name', width=180) + self.skill_tree.column('category', width=100) + self.skill_tree.column('description', width=300) + + self.skill_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.skill_tree.yview) + self.skill_tree.configure(yscrollcommand=scrollbar.set) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Details + details_frame = ttk.Frame(paned) + paned.add(details_frame, weight=1) + + ttk.Label(details_frame, text="Description", font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=5) + + self.skill_details = scrolledtext.ScrolledText(details_frame, wrap=tk.WORD, height=15) + self.skill_details.pack(fill=tk.BOTH, expand=True) + + self.skill_tree.bind('<>', self.on_skill_select) + + self.populate_skills(self.skills) + + def get_categories(self) -> List[str]: + """Get unique categories from skills""" + categories = set(s['category'] for s in self.skills) + return sorted(categories) + + def populate_skills(self, skills: List[Dict]): + """Populate skills list""" + for item in self.skill_tree.get_children(): + self.skill_tree.delete(item) + + for i, skill in enumerate(skills, 1): + self.skill_tree.insert('', tk.END, text=str(i), + values=(skill['name'], skill['category'], skill['description'])) + + def filter_skills(self, event=None): + """Filter skills based on search and category""" + search = self.skill_search.get().lower() + category = self.skill_category.get() + + filtered = self.skills + + if category != 'All': + filtered = [s for s in filtered if s['category'] == category] + + if search: + filtered = [s for s in filtered + if search in s['name'].lower() or search in s['description'].lower()] + + self.populate_skills(filtered) + self.skill_count_label.config(text=str(len(filtered))) + + def on_skill_select(self, event): + """Handle skill selection""" + selection = self.skill_tree.selection() + if not selection: + return + + item = self.skill_tree.item(selection[0]) + skill_name = item['values'][0] + + skill = next((s for s in self.skills if s['name'] == skill_name), None) + if skill: + details = f"""Skill: {skill['name']} + +Category: {skill['category']} + +Description: {skill['description']} + +Path: {skill['path']} + +--- +Usage: This skill is automatically activated when working with related technologies.""" + self.skill_details.delete('1.0', tk.END) + self.skill_details.insert('1.0', details) + + # ========================================================================= + # COMMANDS TAB + # ========================================================================= + + def create_commands_tab(self): + """Create Commands tab""" + frame = ttk.Frame(self.notebook) + self.notebook.add(frame, text=f"Commands ({len(self.commands)})") + + # Info + info_frame = ttk.Frame(frame) + info_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(info_frame, text="Slash Commands for Claude Code:", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + ttk.Label(info_frame, text="Use these commands in Claude Code by typing /command_name", + foreground='gray').pack(anchor=tk.W) + + # Commands list + list_frame = ttk.Frame(frame) + list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) + + columns = ('name', 'description') + self.command_tree = ttk.Treeview(list_frame, columns=columns, show='tree headings') + self.command_tree.heading('#0', text='#') + self.command_tree.heading('name', text='Command') + self.command_tree.heading('description', text='Description') + + self.command_tree.column('#0', width=40) + self.command_tree.column('name', width=150) + self.command_tree.column('description', width=400) + + self.command_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.command_tree.yview) + self.command_tree.configure(yscrollcommand=scrollbar.set) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Populate + for i, cmd in enumerate(self.commands, 1): + self.command_tree.insert('', tk.END, text=str(i), + values=('/' + cmd['name'], cmd['description'])) + + # ========================================================================= + # RULES TAB + # ========================================================================= + + def create_rules_tab(self): + """Create Rules tab""" + frame = ttk.Frame(self.notebook) + self.notebook.add(frame, text=f"Rules ({len(self.rules)})") + + # Info + info_frame = ttk.Frame(frame) + info_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(info_frame, text="Coding Rules by Language:", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + ttk.Label(info_frame, text="These rules are automatically applied in Claude Code", + foreground='gray').pack(anchor=tk.W) + + # Filter + filter_frame = ttk.Frame(frame) + filter_frame.pack(fill=tk.X, padx=10, pady=5) + + ttk.Label(filter_frame, text="Language:").pack(side=tk.LEFT) + self.rules_language = ttk.Combobox(filter_frame, + values=['All'] + self.get_rule_languages(), + width=15) + self.rules_language.set('All') + self.rules_language.pack(side=tk.LEFT, padx=5) + self.rules_language.bind('<>', self.filter_rules) + + # Rules list + list_frame = ttk.Frame(frame) + list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) + + columns = ('name', 'language') + self.rules_tree = ttk.Treeview(list_frame, columns=columns, show='tree headings') + self.rules_tree.heading('#0', text='#') + self.rules_tree.heading('name', text='Rule Name') + self.rules_tree.heading('language', text='Language') + + self.rules_tree.column('#0', width=40) + self.rules_tree.column('name', width=250) + self.rules_tree.column('language', width=100) + + self.rules_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.rules_tree.yview) + self.rules_tree.configure(yscrollcommand=scrollbar.set) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + self.populate_rules(self.rules) + + def get_rule_languages(self) -> List[str]: + """Get unique languages from rules""" + languages = set(r['language'] for r in self.rules) + return sorted(languages) + + def populate_rules(self, rules: List[Dict]): + """Populate rules list""" + for item in self.rules_tree.get_children(): + self.rules_tree.delete(item) + + for i, rule in enumerate(rules, 1): + self.rules_tree.insert('', tk.END, text=str(i), + values=(rule['name'], rule['language'])) + + def filter_rules(self, event=None): + """Filter rules by language""" + language = self.rules_language.get() + + if language == 'All': + filtered = self.rules + else: + filtered = [r for r in self.rules if r['language'] == language] + + self.populate_rules(filtered) + + # ========================================================================= + # SETTINGS TAB + # ========================================================================= + + def create_settings_tab(self): + """Create Settings tab""" + frame = ttk.Frame(self.notebook) + self.notebook.add(frame, text="Settings") + + # Project path + path_frame = ttk.LabelFrame(frame, text="Project Path", padding=10) + path_frame.pack(fill=tk.X, padx=10, pady=10) + + self.path_entry = ttk.Entry(path_frame, width=60) + self.path_entry.insert(0, self.project_path) + self.path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + ttk.Button(path_frame, text="Browse...", command=self.browse_path).pack(side=tk.LEFT, padx=5) + + # Theme + theme_frame = ttk.LabelFrame(frame, text="Appearance", padding=10) + theme_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(theme_frame, text="Theme:").pack(anchor=tk.W) + self.theme_var = tk.StringVar(value='light') + light_rb = ttk.Radiobutton(theme_frame, text="Light", variable=self.theme_var, + value='light', command=self.apply_theme) + light_rb.pack(anchor=tk.W) + dark_rb = ttk.Radiobutton(theme_frame, text="Dark", variable=self.theme_var, + value='dark', command=self.apply_theme) + dark_rb.pack(anchor=tk.W) + + font_frame = ttk.LabelFrame(frame, text="Font", padding=10) + font_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(font_frame, text="Font Family:").pack(anchor=tk.W) + self.font_var = tk.StringVar(value='Open Sans') + + fonts = ['Open Sans', 'Arial', 'Helvetica', 'Times New Roman', 'Courier New', 'Verdana', 'Georgia', 'Tahoma', 'Trebuchet MS'] + self.font_combo = ttk.Combobox(font_frame, textvariable=self.font_var, values=fonts, state='readonly') + self.font_combo.pack(anchor=tk.W, fill=tk.X, pady=(5, 0)) + self.font_combo.bind('<>', lambda e: self.apply_theme()) + + ttk.Label(font_frame, text="Font Size:").pack(anchor=tk.W, pady=(10, 0)) + self.size_var = tk.StringVar(value='10') + sizes = ['8', '9', '10', '11', '12', '14', '16', '18', '20'] + self.size_combo = ttk.Combobox(font_frame, textvariable=self.size_var, values=sizes, state='readonly', width=10) + self.size_combo.pack(anchor=tk.W, pady=(5, 0)) + self.size_combo.bind('<>', lambda e: self.apply_theme()) + + # Quick Actions + actions_frame = ttk.LabelFrame(frame, text="Quick Actions", padding=10) + actions_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + ttk.Button(actions_frame, text="Open Project in Terminal", + command=self.open_terminal).pack(fill=tk.X, pady=2) + ttk.Button(actions_frame, text="Open README", + command=self.open_readme).pack(fill=tk.X, pady=2) + ttk.Button(actions_frame, text="Open AGENTS.md", + command=self.open_agents).pack(fill=tk.X, pady=2) + ttk.Button(actions_frame, text="Refresh Data", + command=self.refresh_data).pack(fill=tk.X, pady=2) + + # About + about_frame = ttk.LabelFrame(frame, text="About", padding=10) + about_frame.pack(fill=tk.X, padx=10, pady=10) + + about_text = """ECC Dashboard v1.0.0 +Everything Claude Code GUI + +A cross-platform desktop application for +managing and exploring ECC components. + +Version: 1.10.0 +Project: github.com/affaan-m/everything-claude-code""" + + ttk.Label(about_frame, text=about_text, justify=tk.LEFT).pack(anchor=tk.W) + + def browse_path(self): + """Browse for project path""" + from tkinter import filedialog + path = filedialog.askdirectory(initialdir=self.project_path) + if path: + self.path_entry.delete(0, tk.END) + self.path_entry.insert(0, path) + + def open_terminal(self): + """Open terminal at project path""" + import subprocess + path = self.path_entry.get() + if os.name == 'nt': # Windows + subprocess.Popen(['cmd', '/c', 'start', 'cmd', '/k', f'cd /d "{path}"']) + elif os.uname().sysname == 'Darwin': # macOS + subprocess.Popen(['open', '-a', 'Terminal', path]) + else: # Linux + subprocess.Popen(['x-terminal-emulator', '-e', f'cd {path}']) + + def open_readme(self): + """Open README in default browser/reader""" + import subprocess + path = os.path.join(self.path_entry.get(), 'README.md') + if os.path.exists(path): + subprocess.Popen(['xdg-open' if os.name != 'nt' else 'start', path]) + else: + messagebox.showerror("Error", "README.md not found") + + def open_agents(self): + """Open AGENTS.md""" + import subprocess + path = os.path.join(self.path_entry.get(), 'AGENTS.md') + if os.path.exists(path): + subprocess.Popen(['xdg-open' if os.name != 'nt' else 'start', path]) + else: + messagebox.showerror("Error", "AGENTS.md not found") + + def refresh_data(self): + """Refresh all data""" + self.project_path = self.path_entry.get() + self.agents = load_agents(self.project_path) + self.skills = load_skills(self.project_path) + self.commands = load_commands(self.project_path) + self.rules = load_rules(self.project_path) + + # Update tabs + self.notebook.tab(0, text=f"Agents ({len(self.agents)})") + self.notebook.tab(1, text=f"Skills ({len(self.skills)})") + self.notebook.tab(2, text=f"Commands ({len(self.commands)})") + self.notebook.tab(3, text=f"Rules ({len(self.rules)})") + + # Repopulate + self.populate_agents(self.agents) + self.populate_skills(self.skills) + + # Update status + self.status_label.config( + text=f"Ready | Agents: {len(self.agents)} | Skills: {len(self.skills)} | Commands: {len(self.commands)}" + ) + + messagebox.showinfo("Success", "Data refreshed successfully!") + + def apply_theme(self): + theme = self.theme_var.get() + font_family = self.font_var.get() + font_size = int(self.size_var.get()) + font_tuple = (font_family, font_size) + + if theme == 'dark': + bg_color = '#2b2b2b' + fg_color = '#ffffff' + entry_bg = '#3c3c3c' + frame_bg = '#2b2b2b' + select_bg = '#0f5a9e' + else: + bg_color = '#f0f0f0' + fg_color = '#000000' + entry_bg = '#ffffff' + frame_bg = '#f0f0f0' + select_bg = '#e0e0e0' + + self.configure(background=bg_color) + + style = ttk.Style() + style.configure('.', background=bg_color, foreground=fg_color, font=font_tuple) + style.configure('TFrame', background=bg_color, font=font_tuple) + style.configure('TLabel', background=bg_color, foreground=fg_color, font=font_tuple) + style.configure('TNotebook', background=bg_color, font=font_tuple) + style.configure('TNotebook.Tab', background=frame_bg, foreground=fg_color, font=font_tuple) + style.map('TNotebook.Tab', background=[('selected', select_bg)]) + style.configure('Treeview', background=entry_bg, foreground=fg_color, fieldbackground=entry_bg, font=font_tuple) + style.configure('Treeview.Heading', background=frame_bg, foreground=fg_color, font=font_tuple) + style.configure('TEntry', fieldbackground=entry_bg, foreground=fg_color, font=font_tuple) + style.configure('TButton', background=frame_bg, foreground=fg_color, font=font_tuple) + + self.title_label.configure(font=(font_family, 18, 'bold')) + self.version_label.configure(font=(font_family, 10)) + + def update_widget_colors(widget): + try: + widget.configure(background=bg_color) + except: + pass + for child in widget.winfo_children(): + try: + child.configure(background=bg_color) + except: + pass + try: + update_widget_colors(child) + except: + pass + + try: + update_widget_colors(self) + except: + pass + + self.update() + + +# ============================================================================ +# MAIN +# ============================================================================ + +def main(): + """Main entry point""" + app = ECCDashboard() + app.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/package.json b/package.json index 1a569589..a9d9530f 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,8 @@ "test": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && node tests/run-all.js", "coverage": "c8 --all --include=\"scripts/**/*.js\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js", "build:opencode": "node scripts/build-opencode.js", - "prepack": "npm run build:opencode" + "prepack": "npm run build:opencode", + "dashboard": "python3 ./ecc_dashboard.py" }, "dependencies": { "@iarna/toml": "^2.2.5", @@ -137,4 +138,4 @@ "node": ">=18" }, "packageManager": "yarn@4.9.2+sha512.1fc009bc09d13cfd0e19efa44cbfc2b9cf6ca61482725eb35bbc5e257e093ebf4130db6dfe15d604ff4b79efd8e1e8e99b25fa7d0a6197c9f9826358d4d65c3c" -} +} \ No newline at end of file