claude-mpm 4.3.22__py3-none-any.whl → 4.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/WORKFLOW.md +2 -14
- claude_mpm/cli/commands/configure.py +2 -29
- claude_mpm/cli/commands/doctor.py +2 -2
- claude_mpm/cli/commands/mpm_init.py +3 -3
- claude_mpm/cli/parsers/configure_parser.py +4 -15
- claude_mpm/core/framework/__init__.py +38 -0
- claude_mpm/core/framework/formatters/__init__.py +11 -0
- claude_mpm/core/framework/formatters/capability_generator.py +356 -0
- claude_mpm/core/framework/formatters/content_formatter.py +283 -0
- claude_mpm/core/framework/formatters/context_generator.py +180 -0
- claude_mpm/core/framework/loaders/__init__.py +13 -0
- claude_mpm/core/framework/loaders/agent_loader.py +202 -0
- claude_mpm/core/framework/loaders/file_loader.py +213 -0
- claude_mpm/core/framework/loaders/instruction_loader.py +151 -0
- claude_mpm/core/framework/loaders/packaged_loader.py +208 -0
- claude_mpm/core/framework/processors/__init__.py +11 -0
- claude_mpm/core/framework/processors/memory_processor.py +222 -0
- claude_mpm/core/framework/processors/metadata_processor.py +146 -0
- claude_mpm/core/framework/processors/template_processor.py +238 -0
- claude_mpm/core/framework_loader.py +277 -1798
- claude_mpm/hooks/__init__.py +9 -1
- claude_mpm/hooks/kuzu_memory_hook.py +352 -0
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/services/agents/memory/content_manager.py +5 -2
- claude_mpm/services/agents/memory/memory_file_service.py +1 -0
- claude_mpm/services/agents/memory/memory_limits_service.py +1 -0
- claude_mpm/services/core/path_resolver.py +1 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +1 -0
- claude_mpm/services/mcp_config_manager.py +67 -4
- claude_mpm/services/mcp_gateway/core/process_pool.py +281 -0
- claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
- claude_mpm/services/mcp_gateway/main.py +3 -13
- claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
- claude_mpm/services/mcp_gateway/tools/__init__.py +13 -2
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +36 -6
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +542 -0
- claude_mpm/services/shared/__init__.py +2 -1
- claude_mpm/services/shared/service_factory.py +8 -5
- claude_mpm/services/unified/__init__.py +65 -0
- claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
- claude_mpm/services/unified/config_strategies/__init__.py +190 -0
- claude_mpm/services/unified/config_strategies/config_schema.py +689 -0
- claude_mpm/services/unified/config_strategies/context_strategy.py +748 -0
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +999 -0
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +871 -0
- claude_mpm/services/unified/config_strategies/unified_config_service.py +802 -0
- claude_mpm/services/unified/config_strategies/validation_strategy.py +1105 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
- claude_mpm/services/unified/deployment_strategies/base.py +557 -0
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
- claude_mpm/services/unified/deployment_strategies/local.py +594 -0
- claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
- claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
- claude_mpm/services/unified/interfaces.py +499 -0
- claude_mpm/services/unified/migration.py +532 -0
- claude_mpm/services/unified/strategies.py +551 -0
- claude_mpm/services/unified/unified_analyzer.py +534 -0
- claude_mpm/services/unified/unified_config.py +688 -0
- claude_mpm/services/unified/unified_deployment.py +470 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/METADATA +15 -15
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/RECORD +71 -32
- claude_mpm/cli/commands/configure_tui.py +0 -1927
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/top_level.txt +0 -0
@@ -1,1927 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Full-screen terminal interface for claude-mpm configuration using Textual.
|
3
|
-
|
4
|
-
WHY: Provides a modern, user-friendly TUI for managing configurations with
|
5
|
-
keyboard navigation, mouse support, and responsive layouts.
|
6
|
-
|
7
|
-
DESIGN DECISIONS:
|
8
|
-
- Use Textual for modern full-screen terminal interface
|
9
|
-
- Implement multiple screens with sidebar navigation
|
10
|
-
- Support both keyboard and mouse interaction
|
11
|
-
- Provide live search and filtering capabilities
|
12
|
-
- Use modal dialogs for confirmations
|
13
|
-
- Maintain consistency with existing configuration logic
|
14
|
-
|
15
|
-
EVENT HANDLING FIX:
|
16
|
-
- ListView selection events are handled via on_list_view_selected method
|
17
|
-
- Multiple event handlers provide fallback for different interaction methods
|
18
|
-
- Enter key binding (action_select_nav) handles keyboard selection
|
19
|
-
- Debug logging helps diagnose event flow issues
|
20
|
-
- Index-based selection is most reliable for screen switching
|
21
|
-
"""
|
22
|
-
|
23
|
-
import json
|
24
|
-
import os
|
25
|
-
import sys
|
26
|
-
from datetime import datetime, timezone
|
27
|
-
from pathlib import Path
|
28
|
-
from typing import Any, Dict, List, Optional
|
29
|
-
|
30
|
-
from textual import on, work
|
31
|
-
from textual.app import App, ComposeResult
|
32
|
-
from textual.binding import Binding
|
33
|
-
from textual.containers import Container, Horizontal, Vertical
|
34
|
-
from textual.screen import ModalScreen
|
35
|
-
from textual.widgets import (
|
36
|
-
Button,
|
37
|
-
ContentSwitcher,
|
38
|
-
DataTable,
|
39
|
-
Footer,
|
40
|
-
Header,
|
41
|
-
Input,
|
42
|
-
Label,
|
43
|
-
ListItem,
|
44
|
-
ListView,
|
45
|
-
Rule,
|
46
|
-
Static,
|
47
|
-
TabbedContent,
|
48
|
-
TabPane,
|
49
|
-
TextArea,
|
50
|
-
Tree,
|
51
|
-
)
|
52
|
-
|
53
|
-
from ...services.version_service import VersionService
|
54
|
-
from ..shared import CommandResult
|
55
|
-
|
56
|
-
|
57
|
-
class AgentConfig:
|
58
|
-
"""Agent configuration model matching the existing implementation."""
|
59
|
-
|
60
|
-
def __init__(
|
61
|
-
self, name: str, description: str = "", dependencies: Optional[List[str]] = None
|
62
|
-
):
|
63
|
-
self.name = name
|
64
|
-
self.description = description
|
65
|
-
self.dependencies = dependencies or []
|
66
|
-
self.enabled = True
|
67
|
-
|
68
|
-
|
69
|
-
class SimpleAgentManager:
|
70
|
-
"""Agent manager matching the existing implementation."""
|
71
|
-
|
72
|
-
def __init__(self, config_dir: Path):
|
73
|
-
self.config_dir = config_dir
|
74
|
-
self.config_file = config_dir / "agent_states.json"
|
75
|
-
self.config_dir.mkdir(parents=True, exist_ok=True)
|
76
|
-
self._load_states()
|
77
|
-
self.templates_dir = (
|
78
|
-
Path(__file__).parent.parent.parent / "agents" / "templates"
|
79
|
-
)
|
80
|
-
|
81
|
-
def _load_states(self):
|
82
|
-
"""Load agent states from file."""
|
83
|
-
if self.config_file.exists():
|
84
|
-
with open(self.config_file) as f:
|
85
|
-
self.states = json.load(f)
|
86
|
-
else:
|
87
|
-
self.states = {}
|
88
|
-
|
89
|
-
def _save_states(self):
|
90
|
-
"""Save agent states to file."""
|
91
|
-
with open(self.config_file, "w") as f:
|
92
|
-
json.dump(self.states, f, indent=2)
|
93
|
-
|
94
|
-
def is_agent_enabled(self, agent_name: str) -> bool:
|
95
|
-
"""Check if an agent is enabled."""
|
96
|
-
return self.states.get(agent_name, {}).get("enabled", True)
|
97
|
-
|
98
|
-
def set_agent_enabled(self, agent_name: str, enabled: bool):
|
99
|
-
"""Set agent enabled state."""
|
100
|
-
if agent_name not in self.states:
|
101
|
-
self.states[agent_name] = {}
|
102
|
-
self.states[agent_name]["enabled"] = enabled
|
103
|
-
self._save_states()
|
104
|
-
|
105
|
-
def discover_agents(self) -> List[AgentConfig]:
|
106
|
-
"""Discover available agents from template JSON files."""
|
107
|
-
agents = []
|
108
|
-
|
109
|
-
if not self.templates_dir.exists():
|
110
|
-
return [
|
111
|
-
AgentConfig("engineer", "Engineering agent (templates not found)", []),
|
112
|
-
AgentConfig("research", "Research agent (templates not found)", []),
|
113
|
-
]
|
114
|
-
|
115
|
-
try:
|
116
|
-
for template_file in sorted(self.templates_dir.glob("*.json")):
|
117
|
-
if "backup" in template_file.name.lower():
|
118
|
-
continue
|
119
|
-
|
120
|
-
try:
|
121
|
-
with open(template_file) as f:
|
122
|
-
template_data = json.load(f)
|
123
|
-
|
124
|
-
agent_id = template_data.get("agent_id", template_file.stem)
|
125
|
-
metadata = template_data.get("metadata", {})
|
126
|
-
metadata.get("name", agent_id)
|
127
|
-
description = metadata.get(
|
128
|
-
"description", "No description available"
|
129
|
-
)
|
130
|
-
|
131
|
-
capabilities = template_data.get("capabilities", {})
|
132
|
-
tools = capabilities.get("tools", [])
|
133
|
-
display_tools = tools[:3] if len(tools) > 3 else tools
|
134
|
-
|
135
|
-
normalized_id = agent_id.replace("-agent", "").replace("_", "-")
|
136
|
-
|
137
|
-
agent = AgentConfig(
|
138
|
-
name=normalized_id,
|
139
|
-
description=(
|
140
|
-
description[:80] + "..."
|
141
|
-
if len(description) > 80
|
142
|
-
else description
|
143
|
-
),
|
144
|
-
dependencies=display_tools,
|
145
|
-
)
|
146
|
-
agent.enabled = self.is_agent_enabled(normalized_id)
|
147
|
-
agents.append(agent)
|
148
|
-
|
149
|
-
except (json.JSONDecodeError, KeyError):
|
150
|
-
continue
|
151
|
-
|
152
|
-
except Exception:
|
153
|
-
return [
|
154
|
-
AgentConfig("engineer", "Error loading templates", []),
|
155
|
-
AgentConfig("research", "Research agent", []),
|
156
|
-
]
|
157
|
-
|
158
|
-
agents.sort(key=lambda a: a.name)
|
159
|
-
return agents if agents else [AgentConfig("engineer", "No agents found", [])]
|
160
|
-
|
161
|
-
|
162
|
-
class ConfirmDialog(ModalScreen):
|
163
|
-
"""Modal dialog for confirmations."""
|
164
|
-
|
165
|
-
def __init__(self, message: str, title: str = "Confirm"):
|
166
|
-
super().__init__()
|
167
|
-
self.message = message
|
168
|
-
self.title = title
|
169
|
-
|
170
|
-
def compose(self) -> ComposeResult:
|
171
|
-
with Container(id="confirm-dialog"):
|
172
|
-
yield Label(self.title, id="confirm-title")
|
173
|
-
yield Label(self.message, id="confirm-message")
|
174
|
-
with Horizontal(id="confirm-buttons"):
|
175
|
-
yield Button("Yes", variant="primary", id="confirm-yes")
|
176
|
-
yield Button("No", variant="default", id="confirm-no")
|
177
|
-
|
178
|
-
@on(Button.Pressed, "#confirm-yes")
|
179
|
-
def on_yes(self):
|
180
|
-
self.dismiss(True)
|
181
|
-
|
182
|
-
@on(Button.Pressed, "#confirm-no")
|
183
|
-
def on_no(self):
|
184
|
-
self.dismiss(False)
|
185
|
-
|
186
|
-
|
187
|
-
class EditTemplateDialog(ModalScreen):
|
188
|
-
"""Modal dialog for template editing."""
|
189
|
-
|
190
|
-
def __init__(self, agent_name: str, template: Dict[str, Any]):
|
191
|
-
super().__init__()
|
192
|
-
self.agent_name = agent_name
|
193
|
-
self.template = template
|
194
|
-
|
195
|
-
def compose(self) -> ComposeResult:
|
196
|
-
with Container(id="edit-dialog"):
|
197
|
-
yield Label(f"Edit Template: {self.agent_name}", id="edit-title")
|
198
|
-
yield TextArea(json.dumps(self.template, indent=2), id="template-editor")
|
199
|
-
with Horizontal(id="edit-buttons"):
|
200
|
-
yield Button("Save", variant="primary", id="save-template")
|
201
|
-
yield Button("Cancel", variant="default", id="cancel-edit")
|
202
|
-
|
203
|
-
@on(Button.Pressed, "#save-template")
|
204
|
-
def on_save(self):
|
205
|
-
editor = self.query_one("#template-editor", TextArea)
|
206
|
-
try:
|
207
|
-
template = json.loads(editor.text)
|
208
|
-
self.dismiss(template)
|
209
|
-
except json.JSONDecodeError as e:
|
210
|
-
# Show error in the editor
|
211
|
-
self.notify(f"Invalid JSON: {e}", severity="error")
|
212
|
-
|
213
|
-
@on(Button.Pressed, "#cancel-edit")
|
214
|
-
def on_cancel(self):
|
215
|
-
self.dismiss(None)
|
216
|
-
|
217
|
-
|
218
|
-
class AgentInfo:
|
219
|
-
"""Extended agent information with deployment status."""
|
220
|
-
|
221
|
-
def __init__(
|
222
|
-
self,
|
223
|
-
name: str,
|
224
|
-
category: str,
|
225
|
-
template_path: Path,
|
226
|
-
description: str = "",
|
227
|
-
version: str = "1.0.0",
|
228
|
-
tools: Optional[List[str]] = None,
|
229
|
-
model: Optional[str] = None,
|
230
|
-
):
|
231
|
-
self.name = name
|
232
|
-
self.category = category # "system", "project", "user"
|
233
|
-
self.template_path = template_path
|
234
|
-
self.description = description
|
235
|
-
self.version = version
|
236
|
-
self.tools = tools or []
|
237
|
-
self.model = model
|
238
|
-
self.is_deployed = False
|
239
|
-
self.deployment_path = None
|
240
|
-
self.metadata = {}
|
241
|
-
self.last_modified = None
|
242
|
-
|
243
|
-
def check_deployment(self, project_dir: Path):
|
244
|
-
"""Check if this agent is deployed to .claude/agents/."""
|
245
|
-
claude_agents_dir = project_dir / ".claude" / "agents"
|
246
|
-
possible_names = [
|
247
|
-
f"{self.name}.md",
|
248
|
-
f"{self.name.replace('-', '_')}.md",
|
249
|
-
f"{self.name}-agent.md",
|
250
|
-
f"{self.name.replace('-', '_')}_agent.md",
|
251
|
-
]
|
252
|
-
|
253
|
-
for name in possible_names:
|
254
|
-
deployed_file = claude_agents_dir / name
|
255
|
-
if deployed_file.exists():
|
256
|
-
self.is_deployed = True
|
257
|
-
self.deployment_path = deployed_file
|
258
|
-
return True
|
259
|
-
|
260
|
-
self.is_deployed = False
|
261
|
-
self.deployment_path = None
|
262
|
-
return False
|
263
|
-
|
264
|
-
|
265
|
-
class AgentDiscovery:
|
266
|
-
"""Service to discover agents from all sources."""
|
267
|
-
|
268
|
-
def __init__(self, project_dir: Path):
|
269
|
-
self.project_dir = project_dir
|
270
|
-
# System agents from the package
|
271
|
-
self.system_templates_dir = (
|
272
|
-
Path(__file__).parent.parent.parent / "agents" / "templates"
|
273
|
-
)
|
274
|
-
# Project agents from .claude-mpm/agents/
|
275
|
-
self.project_agents_dir = project_dir / ".claude-mpm" / "agents"
|
276
|
-
# User agents from ~/.claude-mpm/agents/
|
277
|
-
self.user_agents_dir = Path.home() / ".claude-mpm" / "agents"
|
278
|
-
|
279
|
-
def discover_all_agents(self) -> Dict[str, List[AgentInfo]]:
|
280
|
-
"""Discover agents from all sources, categorized."""
|
281
|
-
agents = {
|
282
|
-
"system": self._discover_system_agents(),
|
283
|
-
"project": self._discover_project_agents(),
|
284
|
-
"user": self._discover_user_agents(),
|
285
|
-
}
|
286
|
-
|
287
|
-
# Check deployment status for all agents
|
288
|
-
for category in agents.values():
|
289
|
-
for agent in category:
|
290
|
-
agent.check_deployment(self.project_dir)
|
291
|
-
|
292
|
-
return agents
|
293
|
-
|
294
|
-
def _discover_system_agents(self) -> List[AgentInfo]:
|
295
|
-
"""Discover system agents from package templates."""
|
296
|
-
agents = []
|
297
|
-
if self.system_templates_dir.exists():
|
298
|
-
for template_file in sorted(self.system_templates_dir.glob("*.json")):
|
299
|
-
if "backup" not in template_file.name.lower():
|
300
|
-
agent = self._load_agent_from_template(template_file, "system")
|
301
|
-
if agent:
|
302
|
-
agents.append(agent)
|
303
|
-
return agents
|
304
|
-
|
305
|
-
def _discover_project_agents(self) -> List[AgentInfo]:
|
306
|
-
"""Discover project-specific agents."""
|
307
|
-
agents = []
|
308
|
-
if self.project_agents_dir.exists():
|
309
|
-
for template_file in sorted(self.project_agents_dir.glob("*.json")):
|
310
|
-
agent = self._load_agent_from_template(template_file, "project")
|
311
|
-
if agent:
|
312
|
-
agents.append(agent)
|
313
|
-
return agents
|
314
|
-
|
315
|
-
def _discover_user_agents(self) -> List[AgentInfo]:
|
316
|
-
"""Discover user-level agents."""
|
317
|
-
agents = []
|
318
|
-
if self.user_agents_dir.exists():
|
319
|
-
for template_file in sorted(self.user_agents_dir.glob("*.json")):
|
320
|
-
agent = self._load_agent_from_template(template_file, "user")
|
321
|
-
if agent:
|
322
|
-
agents.append(agent)
|
323
|
-
return agents
|
324
|
-
|
325
|
-
def _load_agent_from_template(
|
326
|
-
self, template_file: Path, category: str
|
327
|
-
) -> Optional[AgentInfo]:
|
328
|
-
"""Load agent information from a template file."""
|
329
|
-
try:
|
330
|
-
with open(template_file) as f:
|
331
|
-
data = json.load(f)
|
332
|
-
|
333
|
-
agent_id = data.get("agent_id", template_file.stem)
|
334
|
-
metadata = data.get("metadata", {})
|
335
|
-
capabilities = data.get("capabilities", {})
|
336
|
-
|
337
|
-
agent = AgentInfo(
|
338
|
-
name=agent_id.replace("-agent", "").replace("_", "-"),
|
339
|
-
category=category,
|
340
|
-
template_path=template_file,
|
341
|
-
description=metadata.get("description", "No description"),
|
342
|
-
version=data.get("agent_version", metadata.get("version", "1.0.0")),
|
343
|
-
tools=capabilities.get("tools", []),
|
344
|
-
model=metadata.get("model"),
|
345
|
-
)
|
346
|
-
|
347
|
-
agent.metadata = metadata
|
348
|
-
|
349
|
-
# Get file modification time
|
350
|
-
stat = template_file.stat()
|
351
|
-
agent.last_modified = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
|
352
|
-
|
353
|
-
return agent
|
354
|
-
|
355
|
-
except Exception:
|
356
|
-
return None
|
357
|
-
|
358
|
-
|
359
|
-
class ViewAgentPropertiesDialog(ModalScreen):
|
360
|
-
"""Modal dialog for viewing agent properties."""
|
361
|
-
|
362
|
-
def __init__(self, agent: AgentInfo):
|
363
|
-
super().__init__()
|
364
|
-
self.agent = agent
|
365
|
-
|
366
|
-
def compose(self) -> ComposeResult:
|
367
|
-
with Container(id="view-properties-dialog"):
|
368
|
-
yield Label(f"Agent Properties: {self.agent.name}", id="properties-title")
|
369
|
-
|
370
|
-
# Load and display the JSON template
|
371
|
-
try:
|
372
|
-
with open(self.agent.template_path) as f:
|
373
|
-
template_data = json.load(f)
|
374
|
-
json_text = json.dumps(template_data, indent=2)
|
375
|
-
except Exception as e:
|
376
|
-
json_text = f"Error loading template: {e}"
|
377
|
-
|
378
|
-
yield TextArea(json_text, read_only=True, id="properties-viewer")
|
379
|
-
|
380
|
-
with Horizontal(id="properties-buttons"):
|
381
|
-
yield Button("Close", variant="primary", id="close-properties")
|
382
|
-
|
383
|
-
@on(Button.Pressed, "#close-properties")
|
384
|
-
def on_close(self):
|
385
|
-
self.dismiss()
|
386
|
-
|
387
|
-
|
388
|
-
class AgentManagementScreen(Container):
|
389
|
-
"""Comprehensive 3-pane agent management screen."""
|
390
|
-
|
391
|
-
def __init__(
|
392
|
-
self, agent_manager: SimpleAgentManager, id: str = "agent-management-screen"
|
393
|
-
):
|
394
|
-
super().__init__(id=id)
|
395
|
-
self.agent_manager = agent_manager
|
396
|
-
self.project_dir = Path.cwd()
|
397
|
-
self.discovery = AgentDiscovery(self.project_dir)
|
398
|
-
self.all_agents = {}
|
399
|
-
self.current_category = "system"
|
400
|
-
self.selected_agent = None
|
401
|
-
self.current_pane_focus = 0 # 0=categories, 1=list, 2=details
|
402
|
-
|
403
|
-
def compose(self) -> ComposeResult:
|
404
|
-
# Simple vertical layout
|
405
|
-
yield Label("Agent Management", id="screen-title")
|
406
|
-
|
407
|
-
# Category tabs
|
408
|
-
with TabbedContent(id="agent-category-tabs"):
|
409
|
-
with TabPane("System", id="tab-system"):
|
410
|
-
yield Label("System agents")
|
411
|
-
with TabPane("Project", id="tab-project"):
|
412
|
-
yield Label("Project agents")
|
413
|
-
with TabPane("User", id="tab-user"):
|
414
|
-
yield Label("User agents")
|
415
|
-
|
416
|
-
# Search box
|
417
|
-
yield Input(placeholder="Search agents...", id="agent-search")
|
418
|
-
|
419
|
-
# Agent table with proper initialization
|
420
|
-
table = DataTable(id="agent-list-table", cursor_type="row")
|
421
|
-
table.add_columns("Name", "Status", "Version", "Path")
|
422
|
-
yield table
|
423
|
-
|
424
|
-
# Simple details area
|
425
|
-
yield Static("Select an agent to view details", id="agent-details")
|
426
|
-
|
427
|
-
# Action buttons in a horizontal container
|
428
|
-
with Horizontal(id="agent-action-buttons"):
|
429
|
-
yield Button("Deploy/Undeploy", id="toggle-deploy", variant="primary")
|
430
|
-
yield Button("View Properties", id="view-properties", variant="default")
|
431
|
-
yield Button("Edit", id="edit-agent", variant="warning")
|
432
|
-
yield Button("Delete", id="delete-agent", variant="error")
|
433
|
-
|
434
|
-
def on_mount(self):
|
435
|
-
"""Initialize when screen is mounted."""
|
436
|
-
# Load agents immediately since we've already set up columns in compose()
|
437
|
-
self.load_agents()
|
438
|
-
self.update_action_buttons()
|
439
|
-
|
440
|
-
def scroll_to_pane(self, pane_id: str):
|
441
|
-
"""Simplified scroll method - no longer needed with vertical layout."""
|
442
|
-
|
443
|
-
def focus_next_pane(self):
|
444
|
-
"""Simplified focus navigation - just focus table."""
|
445
|
-
try:
|
446
|
-
table = self.query_one("#agent-list-table", DataTable)
|
447
|
-
table.focus()
|
448
|
-
except Exception:
|
449
|
-
pass
|
450
|
-
|
451
|
-
def focus_previous_pane(self):
|
452
|
-
"""Simplified focus navigation - just focus table."""
|
453
|
-
try:
|
454
|
-
table = self.query_one("#agent-list-table", DataTable)
|
455
|
-
table.focus()
|
456
|
-
except Exception:
|
457
|
-
pass
|
458
|
-
|
459
|
-
def load_agents(self):
|
460
|
-
"""Load all agents from all sources."""
|
461
|
-
self.all_agents = self.discovery.discover_all_agents()
|
462
|
-
|
463
|
-
# Debug: log what we found
|
464
|
-
for category, agents in self.all_agents.items():
|
465
|
-
self.log(f"Found {len(agents)} {category} agents")
|
466
|
-
|
467
|
-
# Load the current category into the table
|
468
|
-
self.log(f"Loading initial category: {self.current_category}")
|
469
|
-
self.load_category_agents(self.current_category)
|
470
|
-
|
471
|
-
def load_category_agents(self, category: str):
|
472
|
-
"""Load agents from a specific category into the table."""
|
473
|
-
self.current_category = category
|
474
|
-
table = self.query_one("#agent-list-table", DataTable)
|
475
|
-
|
476
|
-
# Clear only the rows, not the columns
|
477
|
-
table.clear()
|
478
|
-
|
479
|
-
agents = self.all_agents.get(category, [])
|
480
|
-
self.log(f"Loading {len(agents)} agents in category '{category}'")
|
481
|
-
|
482
|
-
for agent in agents:
|
483
|
-
status = "✓ Deployed" if agent.is_deployed else "✗ Not Deployed"
|
484
|
-
# Show relative path for readability
|
485
|
-
try:
|
486
|
-
rel_path = agent.template_path.relative_to(Path.home())
|
487
|
-
path_str = f"~/{rel_path}"
|
488
|
-
except Exception:
|
489
|
-
try:
|
490
|
-
rel_path = agent.template_path.relative_to(self.project_dir)
|
491
|
-
path_str = f"./{rel_path}"
|
492
|
-
except Exception:
|
493
|
-
path_str = str(agent.template_path)
|
494
|
-
|
495
|
-
self.log(f"Adding row: {agent.name}, {status}, {agent.version}, {path_str}")
|
496
|
-
table.add_row(agent.name, status, agent.version, path_str, key=agent.name)
|
497
|
-
|
498
|
-
# Force a refresh of the table and ensure visibility
|
499
|
-
table.refresh()
|
500
|
-
table.visible = True
|
501
|
-
|
502
|
-
# Debug: Log the table state
|
503
|
-
self.log(f"Table now has {table.row_count} rows, visible={table.visible}")
|
504
|
-
if table.row_count == 0:
|
505
|
-
self.log("WARNING: No rows were added to the table!")
|
506
|
-
|
507
|
-
@on(TabbedContent.TabActivated)
|
508
|
-
def on_tab_changed(self, event: TabbedContent.TabActivated):
|
509
|
-
"""Handle category tab changes."""
|
510
|
-
tab_id = event.pane.id
|
511
|
-
if tab_id:
|
512
|
-
category = tab_id.replace("tab-", "")
|
513
|
-
self.load_category_agents(category)
|
514
|
-
self.selected_agent = None
|
515
|
-
self.update_agent_details()
|
516
|
-
self.update_action_buttons()
|
517
|
-
|
518
|
-
@on(DataTable.RowSelected)
|
519
|
-
def on_table_row_selected(self, event: DataTable.RowSelected):
|
520
|
-
"""Handle agent selection from table."""
|
521
|
-
if event.row_key:
|
522
|
-
agent_name = str(event.row_key.value)
|
523
|
-
# Find the agent in current category
|
524
|
-
agents = self.all_agents.get(self.current_category, [])
|
525
|
-
agent = next((a for a in agents if a.name == agent_name), None)
|
526
|
-
if agent:
|
527
|
-
self.selected_agent = agent
|
528
|
-
self.update_agent_details()
|
529
|
-
self.update_action_buttons()
|
530
|
-
|
531
|
-
def update_agent_details(self):
|
532
|
-
"""Update the agent details pane."""
|
533
|
-
details = self.query_one("#agent-details", Static)
|
534
|
-
|
535
|
-
if not self.selected_agent:
|
536
|
-
details.update("Select an agent to view details")
|
537
|
-
return
|
538
|
-
|
539
|
-
agent = self.selected_agent
|
540
|
-
|
541
|
-
# Build detailed information
|
542
|
-
details_text = f"""[bold]{agent.name}[/bold]
|
543
|
-
|
544
|
-
[bold]Category:[/bold] {agent.category.title()}
|
545
|
-
[bold]Version:[/bold] {agent.version}
|
546
|
-
[bold]Deployment:[/bold] {'✓ Deployed' if agent.is_deployed else '✗ Not Deployed'}
|
547
|
-
|
548
|
-
[bold]Description:[/bold]
|
549
|
-
{agent.description}
|
550
|
-
|
551
|
-
[bold]Template Path:[/bold]
|
552
|
-
{agent.template_path}
|
553
|
-
"""
|
554
|
-
|
555
|
-
if agent.is_deployed and agent.deployment_path:
|
556
|
-
details_text += f"\n[bold]Deployed To:[/bold]\n{agent.deployment_path}\n"
|
557
|
-
|
558
|
-
if agent.model:
|
559
|
-
details_text += f"\n[bold]Model:[/bold] {agent.model}\n"
|
560
|
-
|
561
|
-
if agent.tools:
|
562
|
-
details_text += "\n[bold]Tools:[/bold]\n"
|
563
|
-
for tool in agent.tools[:10]: # Show first 10 tools
|
564
|
-
details_text += f" • {tool}\n"
|
565
|
-
if len(agent.tools) > 10:
|
566
|
-
details_text += f" ... and {len(agent.tools) - 10} more\n"
|
567
|
-
|
568
|
-
if agent.last_modified:
|
569
|
-
details_text += f"\n[bold]Last Modified:[/bold] {agent.last_modified.strftime('%Y-%m-%d %H:%M')}\n"
|
570
|
-
|
571
|
-
details.update(details_text)
|
572
|
-
|
573
|
-
def update_action_buttons(self):
|
574
|
-
"""Update the state of action buttons based on selected agent."""
|
575
|
-
toggle_btn = self.query_one("#toggle-deploy", Button)
|
576
|
-
view_btn = self.query_one("#view-properties", Button)
|
577
|
-
edit_btn = self.query_one("#edit-agent", Button)
|
578
|
-
delete_btn = self.query_one("#delete-agent", Button)
|
579
|
-
|
580
|
-
if not self.selected_agent:
|
581
|
-
# Disable all buttons
|
582
|
-
toggle_btn.disabled = True
|
583
|
-
view_btn.disabled = True
|
584
|
-
edit_btn.disabled = True
|
585
|
-
delete_btn.disabled = True
|
586
|
-
toggle_btn.label = "Deploy/Undeploy"
|
587
|
-
else:
|
588
|
-
agent = self.selected_agent
|
589
|
-
|
590
|
-
# Toggle deploy button
|
591
|
-
toggle_btn.disabled = False
|
592
|
-
toggle_btn.label = "Undeploy" if agent.is_deployed else "Deploy"
|
593
|
-
|
594
|
-
# View properties always enabled
|
595
|
-
view_btn.disabled = False
|
596
|
-
|
597
|
-
# Edit button - only for project/user agents
|
598
|
-
edit_btn.disabled = agent.category == "system"
|
599
|
-
|
600
|
-
# Delete button - only for project/user agents
|
601
|
-
delete_btn.disabled = agent.category == "system"
|
602
|
-
|
603
|
-
@work
|
604
|
-
@on(Button.Pressed, "#toggle-deploy")
|
605
|
-
async def on_toggle_deploy(self):
|
606
|
-
"""Deploy or undeploy the selected agent."""
|
607
|
-
if not self.selected_agent:
|
608
|
-
return
|
609
|
-
|
610
|
-
agent = self.selected_agent
|
611
|
-
|
612
|
-
if agent.is_deployed:
|
613
|
-
# Undeploy
|
614
|
-
result = await self.app.push_screen_wait(
|
615
|
-
ConfirmDialog(
|
616
|
-
f"Undeploy agent '{agent.name}' from .claude/agents/?",
|
617
|
-
"Confirm Undeploy",
|
618
|
-
)
|
619
|
-
)
|
620
|
-
if result:
|
621
|
-
if agent.deployment_path and agent.deployment_path.exists():
|
622
|
-
try:
|
623
|
-
agent.deployment_path.unlink()
|
624
|
-
self.notify(f"Agent '{agent.name}' undeployed")
|
625
|
-
agent.is_deployed = False
|
626
|
-
agent.deployment_path = None
|
627
|
-
self.load_agents()
|
628
|
-
self.update_agent_details()
|
629
|
-
self.update_action_buttons()
|
630
|
-
except Exception as e:
|
631
|
-
self.notify(f"Failed to undeploy: {e}", severity="error")
|
632
|
-
else:
|
633
|
-
# Deploy
|
634
|
-
self.deploy_agent(agent)
|
635
|
-
|
636
|
-
def deploy_agent(self, agent: AgentInfo):
|
637
|
-
"""Deploy an agent to .claude/agents/."""
|
638
|
-
try:
|
639
|
-
# Create .claude/agents directory
|
640
|
-
claude_agents_dir = self.project_dir / ".claude" / "agents"
|
641
|
-
claude_agents_dir.mkdir(parents=True, exist_ok=True)
|
642
|
-
|
643
|
-
# Load template
|
644
|
-
with open(agent.template_path) as f:
|
645
|
-
template_data = json.load(f)
|
646
|
-
|
647
|
-
# Convert to YAML format (simplified for this example)
|
648
|
-
# In production, use the actual AgentFormatConverter
|
649
|
-
agent_content = self._build_agent_markdown(template_data)
|
650
|
-
|
651
|
-
# Write to .claude/agents/
|
652
|
-
target_file = claude_agents_dir / f"{agent.name}.md"
|
653
|
-
with open(target_file, "w") as f:
|
654
|
-
f.write(agent_content)
|
655
|
-
|
656
|
-
self.notify(f"Agent '{agent.name}' deployed to .claude/agents/")
|
657
|
-
agent.is_deployed = True
|
658
|
-
agent.deployment_path = target_file
|
659
|
-
self.load_agents()
|
660
|
-
self.update_agent_details()
|
661
|
-
self.update_action_buttons()
|
662
|
-
|
663
|
-
except Exception as e:
|
664
|
-
self.notify(f"Failed to deploy: {e}", severity="error")
|
665
|
-
|
666
|
-
def _build_agent_markdown(self, template_data: Dict[str, Any]) -> str:
|
667
|
-
"""Build a simple markdown representation of the agent."""
|
668
|
-
metadata = template_data.get("metadata", {})
|
669
|
-
capabilities = template_data.get("capabilities", {})
|
670
|
-
instructions = template_data.get("instructions", [])
|
671
|
-
|
672
|
-
content = f"""---
|
673
|
-
agent_id: {template_data.get('agent_id', 'unknown')}
|
674
|
-
name: {metadata.get('name', 'Unknown')}
|
675
|
-
version: {metadata.get('version', '1.0.0')}
|
676
|
-
model: {metadata.get('model', 'claude-3-5-sonnet-20241022')}
|
677
|
-
---
|
678
|
-
|
679
|
-
# {metadata.get('name', 'Agent')}
|
680
|
-
|
681
|
-
{metadata.get('description', 'No description')}
|
682
|
-
|
683
|
-
## Instructions
|
684
|
-
|
685
|
-
"""
|
686
|
-
|
687
|
-
for instruction in instructions:
|
688
|
-
if isinstance(instruction, dict):
|
689
|
-
content += f"- {instruction.get('content', '')}\n"
|
690
|
-
else:
|
691
|
-
content += f"- {instruction}\n"
|
692
|
-
|
693
|
-
if capabilities.get("tools"):
|
694
|
-
content += "\n## Tools\n\n"
|
695
|
-
for tool in capabilities["tools"]:
|
696
|
-
content += f"- {tool}\n"
|
697
|
-
|
698
|
-
return content
|
699
|
-
|
700
|
-
@work
|
701
|
-
@on(Button.Pressed, "#view-properties")
|
702
|
-
async def on_view_properties(self):
|
703
|
-
"""View the selected agent's properties."""
|
704
|
-
if self.selected_agent:
|
705
|
-
await self.app.push_screen_wait(
|
706
|
-
ViewAgentPropertiesDialog(self.selected_agent)
|
707
|
-
)
|
708
|
-
|
709
|
-
@work
|
710
|
-
@on(Button.Pressed, "#edit-agent")
|
711
|
-
async def on_edit_agent(self):
|
712
|
-
"""Edit the selected agent (project/user only)."""
|
713
|
-
if not self.selected_agent or self.selected_agent.category == "system":
|
714
|
-
return
|
715
|
-
|
716
|
-
try:
|
717
|
-
with open(self.selected_agent.template_path) as f:
|
718
|
-
template_data = json.load(f)
|
719
|
-
|
720
|
-
result = await self.app.push_screen_wait(
|
721
|
-
EditTemplateDialog(self.selected_agent.name, template_data)
|
722
|
-
)
|
723
|
-
|
724
|
-
if result:
|
725
|
-
# Save the edited template
|
726
|
-
with open(self.selected_agent.template_path, "w") as f:
|
727
|
-
json.dump(result, f, indent=2)
|
728
|
-
|
729
|
-
self.notify(f"Agent '{self.selected_agent.name}' updated")
|
730
|
-
self.load_agents()
|
731
|
-
self.update_agent_details()
|
732
|
-
|
733
|
-
except Exception as e:
|
734
|
-
self.notify(f"Failed to edit agent: {e}", severity="error")
|
735
|
-
|
736
|
-
@work
|
737
|
-
@on(Button.Pressed, "#delete-agent")
|
738
|
-
async def on_delete_agent(self):
|
739
|
-
"""Delete the selected agent (project/user only)."""
|
740
|
-
if not self.selected_agent or self.selected_agent.category == "system":
|
741
|
-
return
|
742
|
-
|
743
|
-
result = await self.app.push_screen_wait(
|
744
|
-
ConfirmDialog(
|
745
|
-
f"Delete agent '{self.selected_agent.name}'? This cannot be undone.",
|
746
|
-
"Confirm Delete",
|
747
|
-
)
|
748
|
-
)
|
749
|
-
|
750
|
-
if result:
|
751
|
-
try:
|
752
|
-
# If deployed, undeploy first
|
753
|
-
if (
|
754
|
-
self.selected_agent.is_deployed
|
755
|
-
and self.selected_agent.deployment_path
|
756
|
-
):
|
757
|
-
self.selected_agent.deployment_path.unlink(missing_ok=True)
|
758
|
-
|
759
|
-
# Delete the template file
|
760
|
-
self.selected_agent.template_path.unlink()
|
761
|
-
|
762
|
-
self.notify(f"Agent '{self.selected_agent.name}' deleted")
|
763
|
-
self.selected_agent = None
|
764
|
-
self.load_agents()
|
765
|
-
self.update_agent_details()
|
766
|
-
self.update_action_buttons()
|
767
|
-
|
768
|
-
except Exception as e:
|
769
|
-
self.notify(f"Failed to delete agent: {e}", severity="error")
|
770
|
-
|
771
|
-
@on(Input.Changed, "#agent-search")
|
772
|
-
def on_search_changed(self, event: Input.Changed):
|
773
|
-
"""Filter agents based on search input."""
|
774
|
-
search_term = event.value.lower()
|
775
|
-
table = self.query_one("#agent-list-table", DataTable)
|
776
|
-
table.clear()
|
777
|
-
|
778
|
-
agents = self.all_agents.get(self.current_category, [])
|
779
|
-
|
780
|
-
if search_term:
|
781
|
-
# Filter agents
|
782
|
-
filtered = [
|
783
|
-
agent
|
784
|
-
for agent in agents
|
785
|
-
if search_term in agent.name.lower()
|
786
|
-
or search_term in agent.description.lower()
|
787
|
-
]
|
788
|
-
else:
|
789
|
-
filtered = agents
|
790
|
-
|
791
|
-
# Repopulate table with filtered agents
|
792
|
-
for agent in filtered:
|
793
|
-
status = "✓ Deployed" if agent.is_deployed else "✗ Not Deployed"
|
794
|
-
try:
|
795
|
-
rel_path = agent.template_path.relative_to(Path.home())
|
796
|
-
path_str = f"~/{rel_path}"
|
797
|
-
except Exception:
|
798
|
-
try:
|
799
|
-
rel_path = agent.template_path.relative_to(self.project_dir)
|
800
|
-
path_str = f"./{rel_path}"
|
801
|
-
except Exception:
|
802
|
-
path_str = str(agent.template_path)
|
803
|
-
|
804
|
-
table.add_row(agent.name, status, agent.version, path_str, key=agent.name)
|
805
|
-
|
806
|
-
|
807
|
-
class TemplateEditingScreen(Container):
|
808
|
-
"""Screen for template editing."""
|
809
|
-
|
810
|
-
def __init__(
|
811
|
-
self,
|
812
|
-
agent_manager: SimpleAgentManager,
|
813
|
-
current_scope: str,
|
814
|
-
project_dir: Path,
|
815
|
-
id: str = "template-screen",
|
816
|
-
):
|
817
|
-
super().__init__(id=id)
|
818
|
-
self.agent_manager = agent_manager
|
819
|
-
self.current_scope = current_scope
|
820
|
-
self.project_dir = project_dir
|
821
|
-
self.templates = []
|
822
|
-
|
823
|
-
def compose(self) -> ComposeResult:
|
824
|
-
yield Label("Template Editing", id="screen-title")
|
825
|
-
|
826
|
-
with Horizontal(id="template-layout"):
|
827
|
-
# Template list
|
828
|
-
with Vertical(id="template-list-container"):
|
829
|
-
yield Label("Templates", classes="pane-title")
|
830
|
-
yield ListView(id="template-list")
|
831
|
-
|
832
|
-
# Template viewer
|
833
|
-
with Vertical(id="template-viewer-container"):
|
834
|
-
yield Label("Content", classes="pane-title")
|
835
|
-
yield TextArea("", read_only=True, id="template-viewer")
|
836
|
-
with Horizontal(id="template-actions"):
|
837
|
-
yield Button("Edit", id="edit-template", variant="primary")
|
838
|
-
yield Button("Create Copy", id="copy-template", variant="default")
|
839
|
-
yield Button("Reset", id="reset-template", variant="warning")
|
840
|
-
|
841
|
-
def on_mount(self):
|
842
|
-
"""Load templates when screen is mounted."""
|
843
|
-
# Use after_refresh to ensure UI is ready
|
844
|
-
self.call_after_refresh(self.load_templates)
|
845
|
-
|
846
|
-
def load_templates(self):
|
847
|
-
"""Load available templates."""
|
848
|
-
self.templates = []
|
849
|
-
agents = self.agent_manager.discover_agents()
|
850
|
-
|
851
|
-
list_view = self.query_one("#template-list", ListView)
|
852
|
-
# Clear existing items
|
853
|
-
list_view.clear()
|
854
|
-
|
855
|
-
# Create list items and append them
|
856
|
-
items_to_add = []
|
857
|
-
for agent in agents:
|
858
|
-
template_path = self._get_agent_template_path(agent.name)
|
859
|
-
is_custom = not str(template_path).startswith(
|
860
|
-
str(self.agent_manager.templates_dir)
|
861
|
-
)
|
862
|
-
|
863
|
-
label = f"{agent.name} {'(custom)' if is_custom else '(system)'}"
|
864
|
-
list_item = ListItem(Label(label))
|
865
|
-
list_item.data = {
|
866
|
-
"name": agent.name,
|
867
|
-
"path": template_path,
|
868
|
-
"is_custom": is_custom,
|
869
|
-
}
|
870
|
-
items_to_add.append(list_item)
|
871
|
-
self.templates.append((agent.name, template_path, is_custom))
|
872
|
-
|
873
|
-
# Batch append all items at once
|
874
|
-
for item in items_to_add:
|
875
|
-
list_view.append(item)
|
876
|
-
|
877
|
-
# Log what we loaded
|
878
|
-
self.log(f"Loaded {len(items_to_add)} templates")
|
879
|
-
|
880
|
-
def _get_agent_template_path(self, agent_name: str) -> Path:
|
881
|
-
"""Get the path to an agent's template file."""
|
882
|
-
if self.current_scope == "project":
|
883
|
-
config_dir = self.project_dir / ".claude-mpm" / "agents"
|
884
|
-
else:
|
885
|
-
config_dir = Path.home() / ".claude-mpm" / "agents"
|
886
|
-
|
887
|
-
config_dir.mkdir(parents=True, exist_ok=True)
|
888
|
-
custom_template = config_dir / f"{agent_name}.json"
|
889
|
-
|
890
|
-
if custom_template.exists():
|
891
|
-
return custom_template
|
892
|
-
|
893
|
-
possible_names = [
|
894
|
-
f"{agent_name}.json",
|
895
|
-
f"{agent_name.replace('-', '_')}.json",
|
896
|
-
f"{agent_name}-agent.json",
|
897
|
-
f"{agent_name.replace('-', '_')}_agent.json",
|
898
|
-
]
|
899
|
-
|
900
|
-
for name in possible_names:
|
901
|
-
system_template = self.agent_manager.templates_dir / name
|
902
|
-
if system_template.exists():
|
903
|
-
return system_template
|
904
|
-
|
905
|
-
return custom_template
|
906
|
-
|
907
|
-
@on(ListView.Selected)
|
908
|
-
def on_template_selected(self, event: ListView.Selected):
|
909
|
-
"""Display selected template."""
|
910
|
-
if event.item and hasattr(event.item, "data"):
|
911
|
-
data = event.item.data
|
912
|
-
template_path = data["path"]
|
913
|
-
|
914
|
-
if template_path.exists():
|
915
|
-
with open(template_path) as f:
|
916
|
-
template = json.load(f)
|
917
|
-
|
918
|
-
viewer = self.query_one("#template-viewer", TextArea)
|
919
|
-
viewer.text = json.dumps(template, indent=2)
|
920
|
-
|
921
|
-
# Update button states
|
922
|
-
edit_btn = self.query_one("#edit-template", Button)
|
923
|
-
copy_btn = self.query_one("#copy-template", Button)
|
924
|
-
reset_btn = self.query_one("#reset-template", Button)
|
925
|
-
|
926
|
-
if data["is_custom"]:
|
927
|
-
edit_btn.disabled = False
|
928
|
-
copy_btn.disabled = True
|
929
|
-
reset_btn.disabled = False
|
930
|
-
else:
|
931
|
-
edit_btn.disabled = True
|
932
|
-
copy_btn.disabled = False
|
933
|
-
reset_btn.disabled = True
|
934
|
-
|
935
|
-
@work
|
936
|
-
@on(Button.Pressed, "#edit-template")
|
937
|
-
async def on_edit_template(self):
|
938
|
-
"""Edit the selected template."""
|
939
|
-
list_view = self.query_one("#template-list", ListView)
|
940
|
-
if list_view.highlighted and hasattr(list_view.highlighted, "data"):
|
941
|
-
data = list_view.highlighted.data
|
942
|
-
viewer = self.query_one("#template-viewer", TextArea)
|
943
|
-
|
944
|
-
try:
|
945
|
-
template = json.loads(viewer.text)
|
946
|
-
result = await self.app.push_screen_wait(
|
947
|
-
EditTemplateDialog(data["name"], template)
|
948
|
-
)
|
949
|
-
|
950
|
-
if result:
|
951
|
-
# Save the edited template
|
952
|
-
with open(data["path"], "w") as f:
|
953
|
-
json.dump(result, f, indent=2)
|
954
|
-
|
955
|
-
viewer.text = json.dumps(result, indent=2)
|
956
|
-
self.notify(f"Template '{data['name']}' saved")
|
957
|
-
except json.JSONDecodeError:
|
958
|
-
self.notify("Invalid JSON in viewer", severity="error")
|
959
|
-
|
960
|
-
@work
|
961
|
-
@on(Button.Pressed, "#copy-template")
|
962
|
-
async def on_copy_template(self):
|
963
|
-
"""Create a custom copy of a system template."""
|
964
|
-
list_view = self.query_one("#template-list", ListView)
|
965
|
-
if list_view.highlighted and hasattr(list_view.highlighted, "data"):
|
966
|
-
data = list_view.highlighted.data
|
967
|
-
|
968
|
-
if not data["is_custom"]:
|
969
|
-
viewer = self.query_one("#template-viewer", TextArea)
|
970
|
-
try:
|
971
|
-
template = json.loads(viewer.text)
|
972
|
-
|
973
|
-
if self.current_scope == "project":
|
974
|
-
config_dir = self.project_dir / ".claude-mpm" / "agents"
|
975
|
-
else:
|
976
|
-
config_dir = Path.home() / ".claude-mpm" / "agents"
|
977
|
-
|
978
|
-
config_dir.mkdir(parents=True, exist_ok=True)
|
979
|
-
custom_path = config_dir / f"{data['name']}.json"
|
980
|
-
|
981
|
-
proceed = True
|
982
|
-
if custom_path.exists():
|
983
|
-
proceed = await self.app.push_screen_wait(
|
984
|
-
ConfirmDialog(
|
985
|
-
"Custom template already exists. Overwrite?",
|
986
|
-
"Confirm Overwrite",
|
987
|
-
)
|
988
|
-
)
|
989
|
-
|
990
|
-
if proceed:
|
991
|
-
with open(custom_path, "w") as f:
|
992
|
-
json.dump(template, f, indent=2)
|
993
|
-
|
994
|
-
self.load_templates()
|
995
|
-
self.notify(f"Created custom template for '{data['name']}'")
|
996
|
-
|
997
|
-
except json.JSONDecodeError:
|
998
|
-
self.notify("Invalid JSON in viewer", severity="error")
|
999
|
-
|
1000
|
-
@work
|
1001
|
-
@on(Button.Pressed, "#reset-template")
|
1002
|
-
async def on_reset_template(self):
|
1003
|
-
"""Reset a custom template to system defaults."""
|
1004
|
-
list_view = self.query_one("#template-list", ListView)
|
1005
|
-
if list_view.highlighted and hasattr(list_view.highlighted, "data"):
|
1006
|
-
data = list_view.highlighted.data
|
1007
|
-
|
1008
|
-
if data["is_custom"]:
|
1009
|
-
result = await self.app.push_screen_wait(
|
1010
|
-
ConfirmDialog(
|
1011
|
-
f"Reset '{data['name']}' to system defaults?", "Confirm Reset"
|
1012
|
-
)
|
1013
|
-
)
|
1014
|
-
|
1015
|
-
if result:
|
1016
|
-
data["path"].unlink(missing_ok=True)
|
1017
|
-
self.load_templates()
|
1018
|
-
self.notify(f"Template '{data['name']}' reset to defaults")
|
1019
|
-
|
1020
|
-
|
1021
|
-
class BehaviorFilesScreen(Container):
|
1022
|
-
"""Screen for behavior file management."""
|
1023
|
-
|
1024
|
-
def __init__(
|
1025
|
-
self, current_scope: str, project_dir: Path, id: str = "behavior-screen"
|
1026
|
-
):
|
1027
|
-
super().__init__(id=id)
|
1028
|
-
self.current_scope = current_scope
|
1029
|
-
self.project_dir = project_dir
|
1030
|
-
|
1031
|
-
def compose(self) -> ComposeResult:
|
1032
|
-
yield Label("Behavior Files", id="screen-title")
|
1033
|
-
|
1034
|
-
with Horizontal(id="behavior-layout"):
|
1035
|
-
# File tree
|
1036
|
-
with Vertical(id="file-tree-container"):
|
1037
|
-
yield Label("Files", classes="pane-title")
|
1038
|
-
tree = Tree("Behavior Files", id="behavior-tree")
|
1039
|
-
tree.root.expand()
|
1040
|
-
yield tree
|
1041
|
-
|
1042
|
-
# File editor
|
1043
|
-
with Vertical(id="file-editor-container"):
|
1044
|
-
yield Label("Content", classes="pane-title", id="editor-title")
|
1045
|
-
yield TextArea("", id="behavior-editor")
|
1046
|
-
with Horizontal(id="behavior-actions"):
|
1047
|
-
yield Button("Save", id="save-behavior", variant="primary")
|
1048
|
-
yield Button("Import", id="import-behavior", variant="default")
|
1049
|
-
yield Button("Export", id="export-behavior", variant="default")
|
1050
|
-
|
1051
|
-
def on_mount(self):
|
1052
|
-
"""Load behavior files when screen is mounted."""
|
1053
|
-
# Use after_refresh to ensure UI is ready
|
1054
|
-
self.call_after_refresh(self.load_behavior_files)
|
1055
|
-
|
1056
|
-
def load_behavior_files(self):
|
1057
|
-
"""Load and display behavior files."""
|
1058
|
-
if self.current_scope == "project":
|
1059
|
-
config_dir = self.project_dir / ".claude-mpm" / "behaviors"
|
1060
|
-
else:
|
1061
|
-
config_dir = Path.home() / ".claude-mpm" / "behaviors"
|
1062
|
-
|
1063
|
-
config_dir.mkdir(parents=True, exist_ok=True)
|
1064
|
-
|
1065
|
-
tree = self.query_one("#behavior-tree", Tree)
|
1066
|
-
tree.clear()
|
1067
|
-
|
1068
|
-
# Add identity and workflow files
|
1069
|
-
for filename in ["identity.yaml", "workflow.yaml"]:
|
1070
|
-
file_path = config_dir / filename
|
1071
|
-
node = tree.root.add(filename)
|
1072
|
-
node.data = file_path
|
1073
|
-
|
1074
|
-
if file_path.exists():
|
1075
|
-
node.set_label(f"{filename} ✓")
|
1076
|
-
else:
|
1077
|
-
node.set_label(f"{filename} ✗")
|
1078
|
-
|
1079
|
-
@on(Tree.NodeSelected)
|
1080
|
-
def on_node_selected(self, event: Tree.NodeSelected):
|
1081
|
-
"""Load file content when node is selected."""
|
1082
|
-
if event.node.data:
|
1083
|
-
file_path = event.node.data
|
1084
|
-
editor = self.query_one("#behavior-editor", TextArea)
|
1085
|
-
|
1086
|
-
if file_path.exists():
|
1087
|
-
with open(file_path) as f:
|
1088
|
-
editor.text = f.read()
|
1089
|
-
editor.read_only = False
|
1090
|
-
else:
|
1091
|
-
editor.text = f"# {file_path.name}\n# File does not exist yet\n"
|
1092
|
-
editor.read_only = False
|
1093
|
-
|
1094
|
-
# Update editor title
|
1095
|
-
title = self.query_one("#editor-title", Label)
|
1096
|
-
title.update(f"{file_path.name} ──────")
|
1097
|
-
|
1098
|
-
@on(Button.Pressed, "#save-behavior")
|
1099
|
-
def on_save_behavior(self):
|
1100
|
-
"""Save the current behavior file."""
|
1101
|
-
tree = self.query_one("#behavior-tree", Tree)
|
1102
|
-
if tree.cursor_node and tree.cursor_node.data:
|
1103
|
-
file_path = tree.cursor_node.data
|
1104
|
-
editor = self.query_one("#behavior-editor", TextArea)
|
1105
|
-
|
1106
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
1107
|
-
with open(file_path, "w") as f:
|
1108
|
-
f.write(editor.text)
|
1109
|
-
|
1110
|
-
# Update tree node
|
1111
|
-
tree.cursor_node.set_label(f"{file_path.name} ✓")
|
1112
|
-
self.notify(f"Saved {file_path.name}")
|
1113
|
-
|
1114
|
-
@on(Button.Pressed, "#import-behavior")
|
1115
|
-
async def on_import_behavior(self):
|
1116
|
-
"""Import a behavior file."""
|
1117
|
-
# In a real implementation, this would open a file dialog
|
1118
|
-
self.notify(
|
1119
|
-
"Import functionality would open a file dialog", severity="information"
|
1120
|
-
)
|
1121
|
-
|
1122
|
-
@on(Button.Pressed, "#export-behavior")
|
1123
|
-
async def on_export_behavior(self):
|
1124
|
-
"""Export a behavior file."""
|
1125
|
-
tree = self.query_one("#behavior-tree", Tree)
|
1126
|
-
if tree.cursor_node and tree.cursor_node.data:
|
1127
|
-
file_path = tree.cursor_node.data
|
1128
|
-
if file_path.exists():
|
1129
|
-
# In a real implementation, this would open a save dialog
|
1130
|
-
self.notify(
|
1131
|
-
f"Would export {file_path.name} to chosen location",
|
1132
|
-
severity="information",
|
1133
|
-
)
|
1134
|
-
else:
|
1135
|
-
self.notify("File does not exist", severity="error")
|
1136
|
-
|
1137
|
-
|
1138
|
-
class SettingsScreen(Container):
|
1139
|
-
"""Screen for settings and version information."""
|
1140
|
-
|
1141
|
-
def __init__(
|
1142
|
-
self, current_scope: str, project_dir: Path, id: str = "settings-screen"
|
1143
|
-
):
|
1144
|
-
super().__init__(id=id)
|
1145
|
-
self.current_scope = current_scope
|
1146
|
-
self.project_dir = project_dir
|
1147
|
-
self.version_service = VersionService()
|
1148
|
-
|
1149
|
-
def compose(self) -> ComposeResult:
|
1150
|
-
yield Label("Settings", id="screen-title")
|
1151
|
-
|
1152
|
-
with Vertical(id="settings-content"):
|
1153
|
-
# Scope settings
|
1154
|
-
with Container(id="scope-section", classes="settings-section"):
|
1155
|
-
yield Label("Configuration Scope", classes="section-title")
|
1156
|
-
with Horizontal(classes="setting-row"):
|
1157
|
-
yield Label("Current Scope:", classes="setting-label")
|
1158
|
-
yield Label(
|
1159
|
-
self.current_scope.upper(),
|
1160
|
-
id="current-scope",
|
1161
|
-
classes="setting-value",
|
1162
|
-
)
|
1163
|
-
yield Button("Switch", id="switch-scope", variant="default")
|
1164
|
-
|
1165
|
-
with Horizontal(classes="setting-row"):
|
1166
|
-
yield Label("Directory:", classes="setting-label")
|
1167
|
-
yield Label(
|
1168
|
-
str(self.project_dir), id="current-dir", classes="setting-value"
|
1169
|
-
)
|
1170
|
-
|
1171
|
-
# Version information
|
1172
|
-
with Container(id="version-section", classes="settings-section"):
|
1173
|
-
yield Label("Version Information", classes="section-title")
|
1174
|
-
yield Container(id="version-info")
|
1175
|
-
|
1176
|
-
# Export/Import
|
1177
|
-
with Container(id="export-section", classes="settings-section"):
|
1178
|
-
yield Label("Configuration Management", classes="section-title")
|
1179
|
-
with Horizontal(classes="setting-row"):
|
1180
|
-
yield Button(
|
1181
|
-
"Export Configuration", id="export-config", variant="primary"
|
1182
|
-
)
|
1183
|
-
yield Button(
|
1184
|
-
"Import Configuration", id="import-config", variant="default"
|
1185
|
-
)
|
1186
|
-
|
1187
|
-
def on_mount(self):
|
1188
|
-
"""Load version information when screen is mounted."""
|
1189
|
-
# Use after_refresh to ensure UI is ready
|
1190
|
-
self.call_after_refresh(self.load_version_info)
|
1191
|
-
|
1192
|
-
def load_version_info(self):
|
1193
|
-
"""Load and display version information."""
|
1194
|
-
mpm_version = self.version_service.get_version()
|
1195
|
-
build_number = self.version_service.get_build_number()
|
1196
|
-
|
1197
|
-
# Try to get Claude version
|
1198
|
-
claude_version = "Unknown"
|
1199
|
-
try:
|
1200
|
-
import subprocess
|
1201
|
-
|
1202
|
-
result = subprocess.run(
|
1203
|
-
["claude", "--version"],
|
1204
|
-
capture_output=True,
|
1205
|
-
text=True,
|
1206
|
-
timeout=5,
|
1207
|
-
check=False,
|
1208
|
-
)
|
1209
|
-
if result.returncode == 0:
|
1210
|
-
claude_version = result.stdout.strip()
|
1211
|
-
except Exception:
|
1212
|
-
pass
|
1213
|
-
|
1214
|
-
version_container = self.query_one("#version-info", Container)
|
1215
|
-
version_container.remove_children()
|
1216
|
-
|
1217
|
-
version_text = f"""Claude MPM: v{mpm_version} (build {build_number})
|
1218
|
-
Claude Code: {claude_version}
|
1219
|
-
Python: {sys.version.split()[0]}"""
|
1220
|
-
|
1221
|
-
for line in version_text.split("\n"):
|
1222
|
-
version_container.mount(Label(line, classes="version-line"))
|
1223
|
-
|
1224
|
-
@on(Button.Pressed, "#switch-scope")
|
1225
|
-
def on_switch_scope(self):
|
1226
|
-
"""Switch configuration scope."""
|
1227
|
-
self.current_scope = "user" if self.current_scope == "project" else "project"
|
1228
|
-
|
1229
|
-
scope_label = self.query_one("#current-scope", Label)
|
1230
|
-
scope_label.update(self.current_scope.upper())
|
1231
|
-
|
1232
|
-
# Update agent manager in the app and all screens
|
1233
|
-
if hasattr(self.app, "agent_manager"):
|
1234
|
-
if self.current_scope == "project":
|
1235
|
-
config_dir = self.project_dir / ".claude-mpm"
|
1236
|
-
else:
|
1237
|
-
config_dir = Path.home() / ".claude-mpm"
|
1238
|
-
self.app.agent_manager = SimpleAgentManager(config_dir)
|
1239
|
-
|
1240
|
-
# Update all screens with new scope
|
1241
|
-
try:
|
1242
|
-
switcher = self.app.query_one("#content-switcher", ContentSwitcher)
|
1243
|
-
|
1244
|
-
# Update each screen's scope
|
1245
|
-
for screen_id in ["agents", "templates", "behaviors", "settings"]:
|
1246
|
-
screen = switcher.get_child_by_id(screen_id)
|
1247
|
-
if screen and hasattr(screen, "current_scope"):
|
1248
|
-
screen.current_scope = self.current_scope
|
1249
|
-
if screen and hasattr(screen, "agent_manager"):
|
1250
|
-
screen.agent_manager = self.app.agent_manager
|
1251
|
-
|
1252
|
-
# Reload data in the current screen if it has a load method
|
1253
|
-
current_screen = switcher.get_child_by_id(self.app.current_screen_name)
|
1254
|
-
if current_screen:
|
1255
|
-
if hasattr(current_screen, "load_agents"):
|
1256
|
-
current_screen.load_agents()
|
1257
|
-
elif hasattr(current_screen, "load_templates"):
|
1258
|
-
current_screen.load_templates()
|
1259
|
-
elif hasattr(current_screen, "load_behavior_files"):
|
1260
|
-
current_screen.load_behavior_files()
|
1261
|
-
except Exception:
|
1262
|
-
pass
|
1263
|
-
|
1264
|
-
self.notify(f"Switched to {self.current_scope} scope")
|
1265
|
-
|
1266
|
-
@on(Button.Pressed, "#export-config")
|
1267
|
-
async def on_export_config(self):
|
1268
|
-
"""Export configuration."""
|
1269
|
-
# In a real implementation, this would open a save dialog
|
1270
|
-
self.notify(
|
1271
|
-
"Export functionality would save configuration to chosen file",
|
1272
|
-
severity="information",
|
1273
|
-
)
|
1274
|
-
|
1275
|
-
@on(Button.Pressed, "#import-config")
|
1276
|
-
async def on_import_config(self):
|
1277
|
-
"""Import configuration."""
|
1278
|
-
# In a real implementation, this would open a file dialog
|
1279
|
-
self.notify(
|
1280
|
-
"Import functionality would load configuration from chosen file",
|
1281
|
-
severity="information",
|
1282
|
-
)
|
1283
|
-
|
1284
|
-
|
1285
|
-
class ConfigureTUI(App):
|
1286
|
-
"""Main Textual application for configuration management."""
|
1287
|
-
|
1288
|
-
CSS = """
|
1289
|
-
/* Global styles */
|
1290
|
-
Container {
|
1291
|
-
background: $surface;
|
1292
|
-
}
|
1293
|
-
|
1294
|
-
#screen-title {
|
1295
|
-
text-style: bold;
|
1296
|
-
text-align: left;
|
1297
|
-
padding: 0 1;
|
1298
|
-
height: 1;
|
1299
|
-
background: $primary 30%;
|
1300
|
-
color: $text;
|
1301
|
-
margin-bottom: 1;
|
1302
|
-
border-bottom: solid $primary;
|
1303
|
-
}
|
1304
|
-
|
1305
|
-
/* Header styles */
|
1306
|
-
Header {
|
1307
|
-
background: $primary;
|
1308
|
-
border-bottom: solid $accent;
|
1309
|
-
}
|
1310
|
-
|
1311
|
-
/* Main layout */
|
1312
|
-
#main-layout {
|
1313
|
-
height: 100%;
|
1314
|
-
}
|
1315
|
-
|
1316
|
-
/* Sidebar navigation - Clean minimal style */
|
1317
|
-
#sidebar {
|
1318
|
-
width: 25;
|
1319
|
-
background: $panel;
|
1320
|
-
border-right: solid $primary;
|
1321
|
-
padding: 0;
|
1322
|
-
}
|
1323
|
-
|
1324
|
-
.sidebar-title {
|
1325
|
-
text-style: bold;
|
1326
|
-
padding: 0 1;
|
1327
|
-
height: 1;
|
1328
|
-
background: $primary 20%;
|
1329
|
-
text-align: left;
|
1330
|
-
margin-bottom: 0;
|
1331
|
-
border-bottom: solid $primary;
|
1332
|
-
}
|
1333
|
-
|
1334
|
-
#nav-list {
|
1335
|
-
height: 100%;
|
1336
|
-
padding: 0;
|
1337
|
-
margin: 0;
|
1338
|
-
}
|
1339
|
-
|
1340
|
-
/* Single-line list items with minimal styling */
|
1341
|
-
#nav-list > ListItem {
|
1342
|
-
padding: 0 2;
|
1343
|
-
margin: 0;
|
1344
|
-
height: 1; /* Single line height */
|
1345
|
-
background: transparent;
|
1346
|
-
}
|
1347
|
-
|
1348
|
-
#nav-list > ListItem Label {
|
1349
|
-
padding: 0;
|
1350
|
-
margin: 0;
|
1351
|
-
width: 100%;
|
1352
|
-
}
|
1353
|
-
|
1354
|
-
/* Hover state - light background */
|
1355
|
-
#nav-list > ListItem:hover {
|
1356
|
-
background: $boost;
|
1357
|
-
}
|
1358
|
-
|
1359
|
-
/* Highlighted/Selected state - accent background */
|
1360
|
-
#nav-list > ListItem.--highlight {
|
1361
|
-
background: $accent 30%;
|
1362
|
-
text-style: bold;
|
1363
|
-
}
|
1364
|
-
|
1365
|
-
/* Active selected state - primary background with bold text */
|
1366
|
-
#nav-list > ListItem.active {
|
1367
|
-
background: $primary 50%;
|
1368
|
-
text-style: bold;
|
1369
|
-
}
|
1370
|
-
|
1371
|
-
/* Main content area */
|
1372
|
-
#content-switcher {
|
1373
|
-
padding: 1;
|
1374
|
-
height: 100%;
|
1375
|
-
width: 100%;
|
1376
|
-
}
|
1377
|
-
|
1378
|
-
/* Content screens (Containers) */
|
1379
|
-
#agents, #templates, #behaviors, #settings {
|
1380
|
-
height: 100%;
|
1381
|
-
width: 100%;
|
1382
|
-
}
|
1383
|
-
|
1384
|
-
/* Agent Management simplified layout styles */
|
1385
|
-
#agent-management-screen {
|
1386
|
-
height: 100%;
|
1387
|
-
padding: 1;
|
1388
|
-
}
|
1389
|
-
|
1390
|
-
#screen-title {
|
1391
|
-
text-style: bold;
|
1392
|
-
padding: 0 1;
|
1393
|
-
height: 1;
|
1394
|
-
background: $primary 20%;
|
1395
|
-
text-align: left;
|
1396
|
-
margin-bottom: 1;
|
1397
|
-
border-bottom: solid $primary;
|
1398
|
-
}
|
1399
|
-
|
1400
|
-
/* Compact headers for all screens */
|
1401
|
-
#list-title, #viewer-title, #tree-title, #editor-title {
|
1402
|
-
text-style: bold;
|
1403
|
-
padding: 0 1;
|
1404
|
-
height: 1;
|
1405
|
-
background: $primary 20%;
|
1406
|
-
text-align: left;
|
1407
|
-
margin-bottom: 1;
|
1408
|
-
border-bottom: solid $primary;
|
1409
|
-
}
|
1410
|
-
|
1411
|
-
#agent-search {
|
1412
|
-
margin-bottom: 1;
|
1413
|
-
width: 100%;
|
1414
|
-
}
|
1415
|
-
|
1416
|
-
#agent-list-table {
|
1417
|
-
height: 20;
|
1418
|
-
min-height: 15;
|
1419
|
-
margin-bottom: 1;
|
1420
|
-
border: solid $primary;
|
1421
|
-
}
|
1422
|
-
|
1423
|
-
#agent-details {
|
1424
|
-
padding: 1;
|
1425
|
-
height: 10;
|
1426
|
-
border: solid $primary;
|
1427
|
-
margin-bottom: 1;
|
1428
|
-
}
|
1429
|
-
|
1430
|
-
#agent-action-buttons {
|
1431
|
-
height: 3;
|
1432
|
-
align: center middle;
|
1433
|
-
}
|
1434
|
-
|
1435
|
-
#agent-action-buttons Button {
|
1436
|
-
margin: 0 1;
|
1437
|
-
}
|
1438
|
-
|
1439
|
-
#agent-category-tabs {
|
1440
|
-
height: 3;
|
1441
|
-
margin-bottom: 1;
|
1442
|
-
}
|
1443
|
-
|
1444
|
-
#agent-category-tabs TabPane {
|
1445
|
-
padding: 0;
|
1446
|
-
}
|
1447
|
-
|
1448
|
-
#view-properties-dialog {
|
1449
|
-
align: center middle;
|
1450
|
-
background: $panel;
|
1451
|
-
border: thick $primary;
|
1452
|
-
padding: 2;
|
1453
|
-
margin: 2 4;
|
1454
|
-
width: 90%;
|
1455
|
-
height: 80%;
|
1456
|
-
}
|
1457
|
-
|
1458
|
-
#properties-title {
|
1459
|
-
text-style: bold;
|
1460
|
-
margin-bottom: 1;
|
1461
|
-
}
|
1462
|
-
|
1463
|
-
#properties-viewer {
|
1464
|
-
width: 100%;
|
1465
|
-
height: 100%;
|
1466
|
-
margin: 1 0;
|
1467
|
-
}
|
1468
|
-
|
1469
|
-
#properties-buttons {
|
1470
|
-
align: center middle;
|
1471
|
-
height: 3;
|
1472
|
-
margin-top: 1;
|
1473
|
-
}
|
1474
|
-
|
1475
|
-
/* Template screen styles */
|
1476
|
-
#template-layout {
|
1477
|
-
height: 100%;
|
1478
|
-
}
|
1479
|
-
|
1480
|
-
#template-list-container {
|
1481
|
-
width: 40%;
|
1482
|
-
border-right: solid $primary;
|
1483
|
-
padding-right: 1;
|
1484
|
-
}
|
1485
|
-
|
1486
|
-
#template-viewer-container {
|
1487
|
-
width: 60%;
|
1488
|
-
padding-left: 1;
|
1489
|
-
}
|
1490
|
-
|
1491
|
-
#template-viewer {
|
1492
|
-
height: 100%;
|
1493
|
-
}
|
1494
|
-
|
1495
|
-
#template-actions {
|
1496
|
-
align: center middle;
|
1497
|
-
height: 3;
|
1498
|
-
margin-top: 1;
|
1499
|
-
}
|
1500
|
-
|
1501
|
-
#template-actions Button {
|
1502
|
-
margin: 0 1;
|
1503
|
-
}
|
1504
|
-
|
1505
|
-
/* Behavior screen styles */
|
1506
|
-
#behavior-layout {
|
1507
|
-
height: 100%;
|
1508
|
-
}
|
1509
|
-
|
1510
|
-
#file-tree-container {
|
1511
|
-
width: 30%;
|
1512
|
-
border-right: solid $primary;
|
1513
|
-
padding-right: 1;
|
1514
|
-
}
|
1515
|
-
|
1516
|
-
#file-editor-container {
|
1517
|
-
width: 70%;
|
1518
|
-
padding-left: 1;
|
1519
|
-
}
|
1520
|
-
|
1521
|
-
#behavior-editor {
|
1522
|
-
height: 100%;
|
1523
|
-
}
|
1524
|
-
|
1525
|
-
#behavior-actions {
|
1526
|
-
align: center middle;
|
1527
|
-
height: 3;
|
1528
|
-
margin-top: 1;
|
1529
|
-
}
|
1530
|
-
|
1531
|
-
#behavior-actions Button {
|
1532
|
-
margin: 0 1;
|
1533
|
-
}
|
1534
|
-
|
1535
|
-
/* Settings screen styles */
|
1536
|
-
#settings-content {
|
1537
|
-
padding: 2;
|
1538
|
-
max-width: 80;
|
1539
|
-
}
|
1540
|
-
|
1541
|
-
.settings-section {
|
1542
|
-
margin-bottom: 2;
|
1543
|
-
border: solid $primary;
|
1544
|
-
padding: 1;
|
1545
|
-
}
|
1546
|
-
|
1547
|
-
.section-title {
|
1548
|
-
text-style: bold;
|
1549
|
-
padding: 0 1;
|
1550
|
-
height: 1;
|
1551
|
-
margin-bottom: 1;
|
1552
|
-
color: $primary;
|
1553
|
-
border-bottom: solid $primary;
|
1554
|
-
}
|
1555
|
-
|
1556
|
-
.setting-row {
|
1557
|
-
align: left middle;
|
1558
|
-
height: 3;
|
1559
|
-
}
|
1560
|
-
|
1561
|
-
.setting-label {
|
1562
|
-
width: 20;
|
1563
|
-
}
|
1564
|
-
|
1565
|
-
.setting-value {
|
1566
|
-
width: 40;
|
1567
|
-
color: $text-muted;
|
1568
|
-
}
|
1569
|
-
|
1570
|
-
.version-line {
|
1571
|
-
padding: 0 1;
|
1572
|
-
margin: 0;
|
1573
|
-
}
|
1574
|
-
|
1575
|
-
/* Modal dialog styles */
|
1576
|
-
#confirm-dialog, #edit-dialog {
|
1577
|
-
align: center middle;
|
1578
|
-
background: $panel;
|
1579
|
-
border: thick $primary;
|
1580
|
-
padding: 2;
|
1581
|
-
margin: 4 8;
|
1582
|
-
}
|
1583
|
-
|
1584
|
-
#confirm-title, #edit-title {
|
1585
|
-
text-style: bold;
|
1586
|
-
margin-bottom: 1;
|
1587
|
-
}
|
1588
|
-
|
1589
|
-
#confirm-message {
|
1590
|
-
margin-bottom: 2;
|
1591
|
-
}
|
1592
|
-
|
1593
|
-
#confirm-buttons, #edit-buttons {
|
1594
|
-
align: center middle;
|
1595
|
-
height: 3;
|
1596
|
-
}
|
1597
|
-
|
1598
|
-
#confirm-buttons Button, #edit-buttons Button {
|
1599
|
-
margin: 0 1;
|
1600
|
-
}
|
1601
|
-
|
1602
|
-
#template-editor {
|
1603
|
-
width: 80;
|
1604
|
-
height: 30;
|
1605
|
-
margin: 1 0;
|
1606
|
-
}
|
1607
|
-
|
1608
|
-
/* Footer styles */
|
1609
|
-
Footer {
|
1610
|
-
background: $panel;
|
1611
|
-
}
|
1612
|
-
"""
|
1613
|
-
|
1614
|
-
BINDINGS = [
|
1615
|
-
Binding("ctrl+a", "navigate('agents')", "Agents", key_display="^A"),
|
1616
|
-
Binding("ctrl+t", "navigate('templates')", "Templates", key_display="^T"),
|
1617
|
-
Binding("ctrl+b", "navigate('behaviors')", "Behaviors", key_display="^B"),
|
1618
|
-
Binding("ctrl+s", "navigate('settings')", "Settings", key_display="^S"),
|
1619
|
-
Binding("ctrl+q", "quit", "Quit", key_display="^Q"),
|
1620
|
-
Binding("f1", "help", "Help", key_display="F1"),
|
1621
|
-
Binding("enter", "select_nav", "Select", show=False),
|
1622
|
-
Binding("ctrl+right", "focus_next_pane", "Next Pane", show=False),
|
1623
|
-
Binding("ctrl+left", "focus_prev_pane", "Prev Pane", show=False),
|
1624
|
-
]
|
1625
|
-
|
1626
|
-
def __init__(
|
1627
|
-
self, current_scope: str = "project", project_dir: Optional[Path] = None
|
1628
|
-
):
|
1629
|
-
super().__init__()
|
1630
|
-
self.current_scope = current_scope
|
1631
|
-
self.project_dir = project_dir or Path.cwd()
|
1632
|
-
|
1633
|
-
# Initialize agent manager
|
1634
|
-
if self.current_scope == "project":
|
1635
|
-
config_dir = self.project_dir / ".claude-mpm"
|
1636
|
-
else:
|
1637
|
-
config_dir = Path.home() / ".claude-mpm"
|
1638
|
-
self.agent_manager = SimpleAgentManager(config_dir)
|
1639
|
-
|
1640
|
-
# Track current screen
|
1641
|
-
self.current_screen_name = "agents"
|
1642
|
-
|
1643
|
-
# Version service
|
1644
|
-
self.version_service = VersionService()
|
1645
|
-
|
1646
|
-
def compose(self) -> ComposeResult:
|
1647
|
-
"""Create the main application layout."""
|
1648
|
-
# Header with version info
|
1649
|
-
self.version_service.get_version()
|
1650
|
-
yield Header(show_clock=True)
|
1651
|
-
yield Rule(line_style="heavy")
|
1652
|
-
|
1653
|
-
with Horizontal(id="main-layout"):
|
1654
|
-
# Sidebar navigation
|
1655
|
-
with Container(id="sidebar"):
|
1656
|
-
# Use Static instead of Label for the header
|
1657
|
-
yield Static("MENU", classes="sidebar-title")
|
1658
|
-
# Create ListView with simple text items - no emojis, clean look
|
1659
|
-
yield ListView(
|
1660
|
-
ListItem(Label("Agents"), id="nav-agents"),
|
1661
|
-
ListItem(Label("Templates"), id="nav-templates"),
|
1662
|
-
ListItem(Label("Behaviors"), id="nav-behaviors"),
|
1663
|
-
ListItem(Label("Settings"), id="nav-settings"),
|
1664
|
-
id="nav-list",
|
1665
|
-
)
|
1666
|
-
|
1667
|
-
# Main content area with ContentSwitcher
|
1668
|
-
with ContentSwitcher(initial="agents", id="content-switcher"):
|
1669
|
-
# Create all screens with proper IDs for ContentSwitcher
|
1670
|
-
yield AgentManagementScreen(self.agent_manager, id="agents")
|
1671
|
-
yield TemplateEditingScreen(
|
1672
|
-
self.agent_manager,
|
1673
|
-
self.current_scope,
|
1674
|
-
self.project_dir,
|
1675
|
-
id="templates",
|
1676
|
-
)
|
1677
|
-
yield BehaviorFilesScreen(
|
1678
|
-
self.current_scope, self.project_dir, id="behaviors"
|
1679
|
-
)
|
1680
|
-
yield SettingsScreen(
|
1681
|
-
self.current_scope, self.project_dir, id="settings"
|
1682
|
-
)
|
1683
|
-
|
1684
|
-
# Footer with shortcuts
|
1685
|
-
yield Footer()
|
1686
|
-
|
1687
|
-
def on_mount(self):
|
1688
|
-
"""Initialize the application."""
|
1689
|
-
self.title = f"Claude MPM Configuration v{self.version_service.get_version()}"
|
1690
|
-
self.sub_title = f"Scope: {self.current_scope.upper()} | {self.project_dir}"
|
1691
|
-
|
1692
|
-
# Get the navigation list
|
1693
|
-
list_view = self.query_one("#nav-list", ListView)
|
1694
|
-
|
1695
|
-
# Highlight the first navigation item
|
1696
|
-
if list_view.children:
|
1697
|
-
first_item = list_view.children[0]
|
1698
|
-
if isinstance(first_item, ListItem):
|
1699
|
-
first_item.add_class("active")
|
1700
|
-
|
1701
|
-
# Set focus on the navigation list to enable keyboard navigation
|
1702
|
-
list_view.focus()
|
1703
|
-
|
1704
|
-
# Set initial index to 0 (highlight first item)
|
1705
|
-
list_view.index = 0
|
1706
|
-
|
1707
|
-
# Initialize all screens that are Containers in ContentSwitcher
|
1708
|
-
# since ContentSwitcher doesn't automatically call their on_mount
|
1709
|
-
def initialize_screens():
|
1710
|
-
try:
|
1711
|
-
# Initialize agent management screen
|
1712
|
-
agent_screen = self.query_one("#agents", AgentManagementScreen)
|
1713
|
-
agent_screen.on_mount()
|
1714
|
-
self.log("Initialized AgentManagementScreen")
|
1715
|
-
|
1716
|
-
# Initialize template screen
|
1717
|
-
template_screen = self.query_one("#templates", TemplateEditingScreen)
|
1718
|
-
template_screen.on_mount()
|
1719
|
-
self.log("Initialized TemplateEditingScreen")
|
1720
|
-
|
1721
|
-
# Initialize behavior screen
|
1722
|
-
behavior_screen = self.query_one("#behaviors", BehaviorFilesScreen)
|
1723
|
-
behavior_screen.on_mount()
|
1724
|
-
self.log("Initialized BehaviorFilesScreen")
|
1725
|
-
|
1726
|
-
# Initialize settings screen
|
1727
|
-
settings_screen = self.query_one("#settings", SettingsScreen)
|
1728
|
-
settings_screen.on_mount()
|
1729
|
-
self.log("Initialized SettingsScreen")
|
1730
|
-
|
1731
|
-
except Exception as e:
|
1732
|
-
self.log(f"Error initializing screens: {e}")
|
1733
|
-
|
1734
|
-
# Use call_after_refresh to ensure DOM is ready
|
1735
|
-
self.call_after_refresh(initialize_screens)
|
1736
|
-
|
1737
|
-
def _on_nav_index_changed(self, old_index: int, new_index: int) -> None:
|
1738
|
-
"""Watch for navigation list index changes as a fallback."""
|
1739
|
-
if new_index is not None:
|
1740
|
-
screens = ["agents", "templates", "behaviors", "settings"]
|
1741
|
-
if 0 <= new_index < len(screens):
|
1742
|
-
screen_name = screens[new_index]
|
1743
|
-
self.log(f"Index changed to {new_index}, switching to {screen_name}")
|
1744
|
-
# Only switch if it's a different screen
|
1745
|
-
if screen_name != self.current_screen_name:
|
1746
|
-
self.switch_screen(screen_name)
|
1747
|
-
|
1748
|
-
@on(ListView.Selected, "#nav-list")
|
1749
|
-
def on_nav_list_selected(self, event: ListView.Selected) -> None:
|
1750
|
-
"""Handle navigation ListView selection - primary handler."""
|
1751
|
-
self.log("Navigation ListView.Selected triggered")
|
1752
|
-
|
1753
|
-
# Map item IDs to screen names
|
1754
|
-
id_to_screen = {
|
1755
|
-
"nav-agents": "agents",
|
1756
|
-
"nav-templates": "templates",
|
1757
|
-
"nav-behaviors": "behaviors",
|
1758
|
-
"nav-settings": "settings",
|
1759
|
-
}
|
1760
|
-
|
1761
|
-
# Try to get screen name from item ID first
|
1762
|
-
if event.item and hasattr(event.item, "id") and event.item.id:
|
1763
|
-
screen_name = id_to_screen.get(event.item.id)
|
1764
|
-
if screen_name:
|
1765
|
-
self.log(f"Selected item by ID: {event.item.id} -> {screen_name}")
|
1766
|
-
self.switch_screen(screen_name)
|
1767
|
-
self.notify(f"Switched to {screen_name.title()}", timeout=1)
|
1768
|
-
return
|
1769
|
-
|
1770
|
-
# Fallback to index-based selection
|
1771
|
-
if event.list_view and event.list_view.index is not None:
|
1772
|
-
screens = ["agents", "templates", "behaviors", "settings"]
|
1773
|
-
index = event.list_view.index
|
1774
|
-
if 0 <= index < len(screens):
|
1775
|
-
screen_name = screens[index]
|
1776
|
-
self.log(f"Selected by index: {index} -> {screen_name}")
|
1777
|
-
self.switch_screen(screen_name)
|
1778
|
-
self.notify(f"Switched to {screen_name.title()}", timeout=1)
|
1779
|
-
|
1780
|
-
@on(ListView.Highlighted, "#nav-list")
|
1781
|
-
def on_nav_list_highlighted(self, event: ListView.Highlighted) -> None:
|
1782
|
-
"""Handle ListView highlight changes for mouse hover."""
|
1783
|
-
# This helps with mouse interaction - when user hovers over items
|
1784
|
-
if event.list_view and event.list_view.index is not None:
|
1785
|
-
self.log(f"Navigation item highlighted at index: {event.list_view.index}")
|
1786
|
-
|
1787
|
-
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
1788
|
-
"""Traditional method-name based handler as ultimate fallback."""
|
1789
|
-
self.log("on_list_view_selected (traditional handler) triggered")
|
1790
|
-
|
1791
|
-
# Try to get the navigation list
|
1792
|
-
try:
|
1793
|
-
nav_list = self.query_one("#nav-list", ListView)
|
1794
|
-
if nav_list and nav_list.index is not None:
|
1795
|
-
screens = ["agents", "templates", "behaviors", "settings"]
|
1796
|
-
if 0 <= nav_list.index < len(screens):
|
1797
|
-
screen_name = screens[nav_list.index]
|
1798
|
-
self.log(f"Traditional handler: switching to {screen_name}")
|
1799
|
-
self.switch_screen(screen_name)
|
1800
|
-
self.notify(f"Switched to {screen_name.title()}", timeout=1)
|
1801
|
-
except Exception as e:
|
1802
|
-
self.log(f"Error in traditional handler: {e}")
|
1803
|
-
|
1804
|
-
def switch_screen(self, screen_name: str):
|
1805
|
-
"""Switch to a different screen."""
|
1806
|
-
if screen_name == self.current_screen_name:
|
1807
|
-
return
|
1808
|
-
|
1809
|
-
try:
|
1810
|
-
# Use ContentSwitcher to switch screens
|
1811
|
-
switcher = self.query_one("#content-switcher", ContentSwitcher)
|
1812
|
-
switcher.current = screen_name
|
1813
|
-
self.current_screen_name = screen_name
|
1814
|
-
|
1815
|
-
# Update navigation highlight
|
1816
|
-
list_view = self.query_one("#nav-list", ListView)
|
1817
|
-
for item in list_view.children:
|
1818
|
-
if isinstance(item, ListItem):
|
1819
|
-
item.remove_class("active")
|
1820
|
-
if item.id == f"nav-{screen_name}":
|
1821
|
-
item.add_class("active")
|
1822
|
-
|
1823
|
-
except Exception as e:
|
1824
|
-
self.notify(f"Error switching screen: {e}", severity="error")
|
1825
|
-
|
1826
|
-
def action_select_nav(self):
|
1827
|
-
"""Handle Enter key on navigation list."""
|
1828
|
-
self.log("action_select_nav triggered")
|
1829
|
-
try:
|
1830
|
-
# Check if the navigation list has focus
|
1831
|
-
list_view = self.query_one("#nav-list", ListView)
|
1832
|
-
if self.focused == list_view and list_view.index is not None:
|
1833
|
-
screens = ["agents", "templates", "behaviors", "settings"]
|
1834
|
-
if 0 <= list_view.index < len(screens):
|
1835
|
-
self.log(f"Selecting screen via Enter: {screens[list_view.index]}")
|
1836
|
-
self.switch_screen(screens[list_view.index])
|
1837
|
-
except Exception as e:
|
1838
|
-
self.log(f"Error in action_select_nav: {e}")
|
1839
|
-
|
1840
|
-
def action_navigate(self, screen: str):
|
1841
|
-
"""Navigate to a specific screen via keyboard shortcut."""
|
1842
|
-
self.switch_screen(screen)
|
1843
|
-
|
1844
|
-
# Also update the ListView selection to match
|
1845
|
-
list_view = self.query_one("#nav-list", ListView)
|
1846
|
-
screens = ["agents", "templates", "behaviors", "settings"]
|
1847
|
-
if screen in screens:
|
1848
|
-
index = screens.index(screen)
|
1849
|
-
list_view.index = index
|
1850
|
-
|
1851
|
-
def action_help(self):
|
1852
|
-
"""Show help information."""
|
1853
|
-
self.notify(
|
1854
|
-
"Keyboard Shortcuts:\n"
|
1855
|
-
"Ctrl+A: Agent Management\n"
|
1856
|
-
"Ctrl+T: Template Editing\n"
|
1857
|
-
"Ctrl+B: Behavior Files\n"
|
1858
|
-
"Ctrl+S: Settings\n"
|
1859
|
-
"Ctrl+Q: Quit\n"
|
1860
|
-
"Tab: Navigate UI elements\n"
|
1861
|
-
"Ctrl+→/←: Navigate panes\n"
|
1862
|
-
"Enter: Select/Activate",
|
1863
|
-
title="Help",
|
1864
|
-
timeout=10,
|
1865
|
-
)
|
1866
|
-
|
1867
|
-
def action_focus_next_pane(self):
|
1868
|
-
"""Focus next pane in agent management screen."""
|
1869
|
-
try:
|
1870
|
-
current_screen = self.query_one(
|
1871
|
-
"#content-switcher", ContentSwitcher
|
1872
|
-
).current
|
1873
|
-
if current_screen == "agents":
|
1874
|
-
agent_screen = self.query_one("#agents", AgentManagementScreen)
|
1875
|
-
agent_screen.focus_next_pane()
|
1876
|
-
except Exception:
|
1877
|
-
pass
|
1878
|
-
|
1879
|
-
def action_focus_prev_pane(self):
|
1880
|
-
"""Focus previous pane in agent management screen."""
|
1881
|
-
try:
|
1882
|
-
current_screen = self.query_one(
|
1883
|
-
"#content-switcher", ContentSwitcher
|
1884
|
-
).current
|
1885
|
-
if current_screen == "agents":
|
1886
|
-
agent_screen = self.query_one("#agents", AgentManagementScreen)
|
1887
|
-
agent_screen.focus_previous_pane()
|
1888
|
-
except Exception:
|
1889
|
-
pass
|
1890
|
-
|
1891
|
-
|
1892
|
-
def can_use_tui() -> bool:
|
1893
|
-
"""Check if the terminal supports full-screen TUI mode."""
|
1894
|
-
# Check if we're in an interactive terminal
|
1895
|
-
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
1896
|
-
return False
|
1897
|
-
|
1898
|
-
# Check if we're in a supported terminal
|
1899
|
-
term = os.environ.get("TERM", "")
|
1900
|
-
if not term or term == "dumb":
|
1901
|
-
return False
|
1902
|
-
|
1903
|
-
# Check terminal size
|
1904
|
-
try:
|
1905
|
-
import shutil
|
1906
|
-
|
1907
|
-
cols, rows = shutil.get_terminal_size()
|
1908
|
-
if cols < 80 or rows < 24:
|
1909
|
-
return False
|
1910
|
-
except Exception:
|
1911
|
-
return False
|
1912
|
-
|
1913
|
-
return True
|
1914
|
-
|
1915
|
-
|
1916
|
-
def launch_tui(
|
1917
|
-
current_scope: str = "project", project_dir: Optional[Path] = None
|
1918
|
-
) -> CommandResult:
|
1919
|
-
"""Launch the Textual TUI application."""
|
1920
|
-
try:
|
1921
|
-
app = ConfigureTUI(current_scope, project_dir)
|
1922
|
-
app.run()
|
1923
|
-
return CommandResult.success_result("Configuration completed")
|
1924
|
-
except KeyboardInterrupt:
|
1925
|
-
return CommandResult.success_result("Configuration cancelled")
|
1926
|
-
except Exception as e:
|
1927
|
-
return CommandResult.error_result(f"TUI error: {e}")
|