Files
everything-claude-code/ecc_dashboard.py
2026-04-14 19:44:32 -07:00

914 lines
38 KiB
Python

#!/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
import subprocess
from typing import Dict, List, Optional
from scripts.lib.ecc_dashboard_runtime import build_terminal_launch, maximize_window
# ============================================================================
# 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")
maximize_window(self)
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('<KeyRelease>', 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('<<TreeviewSelect>>', 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('<KeyRelease>', 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('<<ComboboxSelected>>', 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('<<TreeviewSelect>>', 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('<<ComboboxSelected>>', 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('<<ComboboxSelected>>', 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('<<ComboboxSelected>>', 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"""
path = self.path_entry.get()
argv, kwargs = build_terminal_launch(path)
subprocess.Popen(argv, **kwargs)
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()