mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-13 21:33:32 +08:00
feat: add dashboard GUI with theme, font customization, and logo
- Add ecc_dashboard.py - Tkinter-based GUI for exploring ECC components - Implement dark/light theme toggle in Settings tab - Add font family and size customization - Display project logo in header and taskbar - Open in maximized window with native title bar - Add 'dashboard' script to package.json - Update README with dashboard documentation Closes #XXX
This commit is contained in:
24
README.md
24
README.md
@@ -84,6 +84,7 @@ This repo is the raw code only. The guides explain everything.
|
||||
|
||||
### v1.10.0 — Surface Refresh, Operator Workflows, and ECC 2.0 Alpha (Apr 2026)
|
||||
|
||||
- **Dashboard GUI** — New Tkinter-based desktop application (`ecc_dashboard.py` or `npm run dashboard`) with dark/light theme toggle, font customization, and project logo in header and taskbar.
|
||||
- **Public surface synced to the live repo** — metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: 38 agents, 156 skills, and 72 legacy command shims.
|
||||
- **Operator and outbound workflow expansion** — `brand-voice`, `social-graph-ranker`, `connections-optimizer`, `customer-billing-ops`, `ecc-tools-cost-audit`, `google-workspace-ops`, `project-flow-ops`, and `workspace-surface-audit` round out the operator lane.
|
||||
- **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system.
|
||||
@@ -240,6 +241,23 @@ For manual install instructions see the README in the `rules/` folder. When copy
|
||||
|
||||
**That's it!** You now have access to 47 agents, 181 skills, and 79 legacy command shims.
|
||||
|
||||
### Dashboard GUI
|
||||
|
||||
Launch the desktop dashboard to visually explore ECC components:
|
||||
|
||||
```bash
|
||||
npm run dashboard
|
||||
# or
|
||||
python3 ./ecc_dashboard.py
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Tabbed interface: Agents, Skills, Commands, Rules, Settings
|
||||
- Dark/Light theme toggle
|
||||
- Font customization (family & size)
|
||||
- Project logo in header and taskbar
|
||||
- Search and filter across all components
|
||||
|
||||
### Multi-model commands require additional setup
|
||||
|
||||
> WARNING: `multi-*` commands are **not** covered by the base plugin/rules install above.
|
||||
@@ -500,6 +518,12 @@ everything-claude-code/
|
||||
|-- mcp-configs/ # MCP server configurations
|
||||
| |-- mcp-servers.json # GitHub, Supabase, Vercel, Railway, etc.
|
||||
|
|
||||
|-- ecc_dashboard.py # Desktop GUI dashboard (Tkinter)
|
||||
|
|
||||
|-- assets/ # Assets for dashboard
|
||||
| |-- images/
|
||||
| |-- ecc-logo.png
|
||||
|
|
||||
|-- marketplace.json # Self-hosted marketplace config (for /plugin marketplace add)
|
||||
```
|
||||
|
||||
|
||||
BIN
assets/images/ecc-logo.png
Normal file
BIN
assets/images/ecc-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
914
ecc_dashboard.py
Normal file
914
ecc_dashboard.py
Normal file
@@ -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('<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"""
|
||||
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()
|
||||
Reference in New Issue
Block a user