claude-mpm 3.4.27__py3-none-any.whl → 3.5.0__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.
Files changed (123) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +182 -299
  3. claude_mpm/agents/agent_loader.py +283 -57
  4. claude_mpm/agents/agent_loader_integration.py +6 -9
  5. claude_mpm/agents/base_agent.json +2 -1
  6. claude_mpm/agents/base_agent_loader.py +1 -1
  7. claude_mpm/cli/__init__.py +5 -7
  8. claude_mpm/cli/commands/__init__.py +0 -2
  9. claude_mpm/cli/commands/agents.py +1 -1
  10. claude_mpm/cli/commands/memory.py +1 -1
  11. claude_mpm/cli/commands/run.py +12 -0
  12. claude_mpm/cli/parser.py +0 -13
  13. claude_mpm/cli/utils.py +1 -1
  14. claude_mpm/config/__init__.py +44 -2
  15. claude_mpm/config/agent_config.py +348 -0
  16. claude_mpm/config/paths.py +322 -0
  17. claude_mpm/constants.py +0 -1
  18. claude_mpm/core/__init__.py +2 -5
  19. claude_mpm/core/agent_registry.py +63 -17
  20. claude_mpm/core/claude_runner.py +354 -43
  21. claude_mpm/core/config.py +7 -1
  22. claude_mpm/core/config_aliases.py +4 -3
  23. claude_mpm/core/config_paths.py +151 -0
  24. claude_mpm/core/factories.py +4 -50
  25. claude_mpm/core/logger.py +11 -13
  26. claude_mpm/core/service_registry.py +2 -2
  27. claude_mpm/dashboard/static/js/components/agent-inference.js +101 -25
  28. claude_mpm/dashboard/static/js/components/event-processor.js +3 -2
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +343 -83
  30. claude_mpm/hooks/memory_integration_hook.py +1 -1
  31. claude_mpm/init.py +37 -6
  32. claude_mpm/scripts/socketio_daemon.py +6 -2
  33. claude_mpm/services/__init__.py +71 -3
  34. claude_mpm/services/agents/__init__.py +85 -0
  35. claude_mpm/services/agents/deployment/__init__.py +21 -0
  36. claude_mpm/services/{agent_deployment.py → agents/deployment/agent_deployment.py} +192 -41
  37. claude_mpm/services/{agent_lifecycle_manager.py → agents/deployment/agent_lifecycle_manager.py} +11 -10
  38. claude_mpm/services/agents/loading/__init__.py +11 -0
  39. claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +9 -8
  40. claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +2 -2
  41. claude_mpm/services/{framework_agent_loader.py → agents/loading/framework_agent_loader.py} +116 -40
  42. claude_mpm/services/agents/management/__init__.py +9 -0
  43. claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +6 -5
  44. claude_mpm/services/agents/memory/__init__.py +21 -0
  45. claude_mpm/services/{agent_memory_manager.py → agents/memory/agent_memory_manager.py} +3 -3
  46. claude_mpm/services/agents/registry/__init__.py +29 -0
  47. claude_mpm/services/{agent_registry.py → agents/registry/agent_registry.py} +101 -16
  48. claude_mpm/services/{deployed_agent_discovery.py → agents/registry/deployed_agent_discovery.py} +12 -2
  49. claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +6 -5
  50. claude_mpm/services/async_session_logger.py +584 -0
  51. claude_mpm/services/claude_session_logger.py +299 -0
  52. claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
  53. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +17 -17
  54. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +3 -3
  55. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +1 -1
  56. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +1 -1
  57. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +19 -24
  58. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +1 -1
  59. claude_mpm/services/framework_claude_md_generator.py +4 -2
  60. claude_mpm/services/memory/__init__.py +17 -0
  61. claude_mpm/services/{memory_builder.py → memory/builder.py} +3 -3
  62. claude_mpm/services/memory/cache/__init__.py +14 -0
  63. claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +1 -1
  64. claude_mpm/services/memory/cache/simple_cache.py +317 -0
  65. claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +1 -1
  66. claude_mpm/services/{memory_router.py → memory/router.py} +1 -1
  67. claude_mpm/services/optimized_hook_service.py +542 -0
  68. claude_mpm/services/project_registry.py +14 -8
  69. claude_mpm/services/response_tracker.py +237 -0
  70. claude_mpm/services/ticketing_service_original.py +4 -2
  71. claude_mpm/services/version_control/branch_strategy.py +3 -1
  72. claude_mpm/utils/paths.py +12 -10
  73. claude_mpm/utils/session_logging.py +114 -0
  74. claude_mpm/validation/agent_validator.py +2 -1
  75. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/METADATA +26 -20
  76. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/RECORD +83 -106
  77. claude_mpm/cli/commands/ui.py +0 -57
  78. claude_mpm/core/simple_runner.py +0 -1046
  79. claude_mpm/hooks/builtin/__init__.py +0 -1
  80. claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
  81. claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
  82. claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
  83. claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
  84. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
  85. claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
  86. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
  87. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
  88. claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
  89. claude_mpm/orchestration/__init__.py +0 -6
  90. claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
  91. claude_mpm/orchestration/archive/factory.py +0 -215
  92. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
  93. claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
  94. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
  95. claude_mpm/orchestration/archive/orchestrator.py +0 -501
  96. claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
  97. claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
  98. claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
  99. claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
  100. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
  101. claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
  102. claude_mpm/schemas/workflow_validator.py +0 -411
  103. claude_mpm/services/parent_directory_manager/__init__.py +0 -577
  104. claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
  105. claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
  106. claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
  107. claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
  108. claude_mpm/services/parent_directory_manager/operations.py +0 -186
  109. claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
  110. claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
  111. claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
  112. claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
  113. claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
  114. claude_mpm/ui/__init__.py +0 -1
  115. claude_mpm/ui/rich_terminal_ui.py +0 -295
  116. claude_mpm/ui/terminal_ui.py +0 -328
  117. /claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +0 -0
  118. /claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +0 -0
  119. /claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +0 -0
  120. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/WHEEL +0 -0
  121. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/entry_points.txt +0 -0
  122. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/licenses/LICENSE +0 -0
  123. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/top_level.txt +0 -0
@@ -1,295 +0,0 @@
1
- """Rich terminal UI for claude-mpm with live updates."""
2
-
3
- import asyncio
4
- import subprocess
5
- import threading
6
- from datetime import datetime
7
- from pathlib import Path
8
- from typing import Optional, List, Dict, Any
9
- import json
10
-
11
- from rich.console import Console
12
- from rich.layout import Layout
13
- from rich.live import Live
14
- from rich.panel import Panel
15
- from rich.table import Table
16
- from rich.text import Text
17
- from rich.syntax import Syntax
18
- from rich.align import Align
19
- from rich.box import ROUNDED
20
- from rich import print as rprint
21
-
22
- try:
23
- from ..services.ticket_manager import TicketManager
24
- from ..core.logger import get_logger
25
- except ImportError:
26
- from claude_mpm.services.ticket_manager import TicketManager
27
- from claude_mpm.core.logger import get_logger
28
-
29
-
30
- class RichTerminalUI:
31
- """Rich terminal UI with live updates."""
32
-
33
- def __init__(self):
34
- self.console = Console()
35
- self.logger = get_logger("rich_ui")
36
- self.ticket_manager = None
37
- self.todos = []
38
- self.tickets = []
39
- self.claude_output = []
40
- self.claude_process = None
41
- self.running = True
42
-
43
- # Try to initialize ticket manager
44
- try:
45
- self.ticket_manager = TicketManager()
46
- except Exception as e:
47
- self.logger.warning(f"Ticket manager not available: {e}")
48
-
49
- # Create layout
50
- self.layout = Layout()
51
- self._setup_layout()
52
-
53
- def _setup_layout(self):
54
- """Setup the layout structure."""
55
- self.layout.split_column(
56
- Layout(name="header", size=3),
57
- Layout(name="body"),
58
- Layout(name="footer", size=3)
59
- )
60
-
61
- # Split body into two columns
62
- self.layout["body"].split_row(
63
- Layout(name="main", ratio=2),
64
- Layout(name="sidebar", ratio=1)
65
- )
66
-
67
- # Split sidebar into two sections
68
- self.layout["sidebar"].split_column(
69
- Layout(name="todos"),
70
- Layout(name="tickets")
71
- )
72
-
73
- def _make_header(self) -> Panel:
74
- """Create header panel."""
75
- grid = Table.grid(expand=True)
76
- grid.add_column(justify="left")
77
- grid.add_column(justify="center")
78
- grid.add_column(justify="right")
79
-
80
- grid.add_row(
81
- "[bold blue]Claude MPM[/bold blue]",
82
- "[yellow]Terminal UI[/yellow]",
83
- datetime.now().strftime("%Y-%m-%d %H:%M:%S")
84
- )
85
-
86
- return Panel(grid, style="white on blue", box=ROUNDED)
87
-
88
- def _make_footer(self) -> Panel:
89
- """Create footer panel."""
90
- text = Text()
91
- text.append("[F5]", style="bold yellow")
92
- text.append(" Refresh ", style="white")
93
- text.append("[Ctrl+N]", style="bold yellow")
94
- text.append(" New Ticket ", style="white")
95
- text.append("[Ctrl+C]", style="bold yellow")
96
- text.append(" Quit", style="white")
97
-
98
- return Panel(Align.center(text), style="white on blue", box=ROUNDED)
99
-
100
- def _make_claude_panel(self) -> Panel:
101
- """Create Claude output panel."""
102
- # Show last 30 lines
103
- lines = self.claude_output[-30:] if self.claude_output else ["Waiting for Claude to start..."]
104
- content = "\n".join(lines)
105
-
106
- return Panel(
107
- content,
108
- title="[bold]Claude Output[/bold]",
109
- border_style="green",
110
- box=ROUNDED
111
- )
112
-
113
- def _make_todos_panel(self) -> Panel:
114
- """Create todos panel."""
115
- table = Table(show_header=True, header_style="bold cyan", box=None)
116
- table.add_column("", width=2)
117
- table.add_column("Priority", width=8)
118
- table.add_column("Task", overflow="fold")
119
-
120
- for todo in self.todos[:10]: # Show top 10
121
- status_icon = "✓" if todo.get('status') == 'completed' else "○"
122
- priority = todo.get('priority', 'medium')
123
- content = todo.get('content', '')
124
-
125
- # Color based on priority
126
- if priority == 'high':
127
- style = "bold red"
128
- elif priority == 'low':
129
- style = "dim"
130
- else:
131
- style = "white"
132
-
133
- if todo.get('status') == 'completed':
134
- style = "green"
135
-
136
- table.add_row(status_icon, priority.upper(), content, style=style)
137
-
138
- return Panel(
139
- table,
140
- title=f"[bold]ToDo List ({len(self.todos)})[/bold]",
141
- border_style="blue",
142
- box=ROUNDED
143
- )
144
-
145
- def _make_tickets_panel(self) -> Panel:
146
- """Create tickets panel."""
147
- table = Table(show_header=True, header_style="bold magenta", box=None)
148
- table.add_column("ID", width=10)
149
- table.add_column("Priority", width=8)
150
- table.add_column("Title", overflow="fold")
151
-
152
- for ticket in self.tickets[:10]: # Show top 10
153
- ticket_id = ticket.get('id', 'N/A')
154
- priority = ticket.get('priority', 'medium')
155
- title = ticket.get('title', 'No title')
156
-
157
- # Color based on priority
158
- if priority == 'high':
159
- style = "bold red"
160
- elif priority == 'low':
161
- style = "dim"
162
- else:
163
- style = "white"
164
-
165
- table.add_row(ticket_id, priority.upper(), title, style=style)
166
-
167
- return Panel(
168
- table,
169
- title=f"[bold]Tickets ({len(self.tickets)})[/bold]",
170
- border_style="magenta",
171
- box=ROUNDED
172
- )
173
-
174
- def _update_layout(self):
175
- """Update all layout panels."""
176
- self.layout["header"].update(self._make_header())
177
- self.layout["footer"].update(self._make_footer())
178
- self.layout["main"].update(self._make_claude_panel())
179
- self.layout["todos"].update(self._make_todos_panel())
180
- self.layout["tickets"].update(self._make_tickets_panel())
181
-
182
- def _start_claude(self):
183
- """Start Claude process in a thread."""
184
- def run_claude():
185
- try:
186
- # Load system instructions
187
- from ..core.simple_runner import SimpleClaudeRunner
188
- runner = SimpleClaudeRunner(enable_tickets=False)
189
- system_prompt = runner._create_system_prompt()
190
-
191
- cmd = ["claude", "--model", "opus", "--dangerously-skip-permissions"]
192
- if system_prompt:
193
- cmd.extend(["--append-system-prompt", system_prompt])
194
-
195
- self.claude_process = subprocess.Popen(
196
- cmd,
197
- stdout=subprocess.PIPE,
198
- stderr=subprocess.STDOUT,
199
- stdin=subprocess.PIPE,
200
- text=True,
201
- bufsize=1
202
- )
203
-
204
- # Read output
205
- for line in iter(self.claude_process.stdout.readline, ''):
206
- if line and self.running:
207
- self.claude_output.append(line.rstrip())
208
- # Keep last 1000 lines
209
- if len(self.claude_output) > 1000:
210
- self.claude_output = self.claude_output[-1000:]
211
-
212
- except Exception as e:
213
- self.claude_output.append(f"Error starting Claude: {e}")
214
-
215
- thread = threading.Thread(target=run_claude, daemon=True)
216
- thread.start()
217
-
218
- def _load_todos(self):
219
- """Load ToDo items from Claude's todo file."""
220
- try:
221
- # Look for Claude's todo files
222
- todo_dir = Path.home() / ".claude" / "todos"
223
- if todo_dir.exists():
224
- todos = []
225
- for todo_file in todo_dir.glob("*.json"):
226
- try:
227
- with open(todo_file, 'r') as f:
228
- data = json.load(f)
229
- if isinstance(data, list):
230
- todos.extend(data)
231
- except:
232
- pass
233
-
234
- # Sort by priority and status
235
- priority_order = {'high': 0, 'medium': 1, 'low': 2}
236
- todos.sort(key=lambda x: (
237
- x.get('status') == 'completed',
238
- priority_order.get(x.get('priority', 'medium'), 1)
239
- ))
240
- self.todos = todos
241
- except Exception as e:
242
- self.logger.error(f"Error loading todos: {e}")
243
-
244
- def _load_tickets(self):
245
- """Load tickets from ticket manager."""
246
- if self.ticket_manager:
247
- try:
248
- self.tickets = self.ticket_manager.list_recent_tickets(limit=20)
249
- except Exception as e:
250
- self.logger.error(f"Error loading tickets: {e}")
251
-
252
- async def _refresh_data(self):
253
- """Refresh data periodically."""
254
- while self.running:
255
- self._load_todos()
256
- self._load_tickets()
257
- await asyncio.sleep(5) # Refresh every 5 seconds
258
-
259
- async def run(self):
260
- """Run the rich terminal UI."""
261
- # Start Claude
262
- self._start_claude()
263
-
264
- # Initial data load
265
- self._load_todos()
266
- self._load_tickets()
267
-
268
- # Start refresh task
269
- refresh_task = asyncio.create_task(self._refresh_data())
270
-
271
- try:
272
- with Live(self.layout, refresh_per_second=2, screen=True) as live:
273
- while self.running:
274
- self._update_layout()
275
- await asyncio.sleep(0.5)
276
- except KeyboardInterrupt:
277
- self.running = False
278
- finally:
279
- refresh_task.cancel()
280
- if self.claude_process:
281
- self.claude_process.terminate()
282
-
283
- def stop(self):
284
- """Stop the UI."""
285
- self.running = False
286
-
287
-
288
- def main():
289
- """Run the rich terminal UI."""
290
- ui = RichTerminalUI()
291
- asyncio.run(ui.run())
292
-
293
-
294
- if __name__ == "__main__":
295
- main()
@@ -1,328 +0,0 @@
1
- """Terminal UI for claude-mpm with multiple panes."""
2
-
3
- import curses
4
- import subprocess
5
- import threading
6
- import queue
7
- import time
8
- from pathlib import Path
9
- from typing import Optional, List, Dict, Any
10
- import json
11
- from datetime import datetime
12
-
13
- try:
14
- from ..services.ticket_manager import TicketManager
15
- from ..core.logger import get_logger
16
- except ImportError:
17
- from claude_mpm.services.ticket_manager import TicketManager
18
- from claude_mpm.core.logger import get_logger
19
-
20
-
21
- class TerminalUI:
22
- """Multi-pane terminal UI for claude-mpm."""
23
-
24
- def __init__(self):
25
- self.logger = get_logger("terminal_ui")
26
- self.ticket_manager = None
27
- self.todos = []
28
- self.tickets = []
29
- self.claude_output = []
30
- self.active_pane = 0 # 0=claude, 1=todos, 2=tickets
31
- self.claude_process = None
32
- self.output_queue = queue.Queue()
33
-
34
- # Try to initialize ticket manager
35
- try:
36
- self.ticket_manager = TicketManager()
37
- except Exception as e:
38
- self.logger.warning(f"Ticket manager not available: {e}")
39
-
40
- def run(self):
41
- """Run the terminal UI."""
42
- curses.wrapper(self._main)
43
-
44
- def _main(self, stdscr):
45
- """Main curses loop."""
46
- # Setup
47
- curses.curs_set(0) # Hide cursor
48
- stdscr.nodelay(1) # Non-blocking input
49
- stdscr.timeout(100) # Refresh every 100ms
50
-
51
- # Initialize color pairs
52
- curses.start_color()
53
- curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Header
54
- curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE) # Active pane
55
- curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) # Success
56
- curses.init_pair(4, curses.COLOR_RED, curses.COLOR_BLACK) # Error
57
-
58
- # Start Claude process
59
- self._start_claude()
60
-
61
- # Load initial data
62
- self._load_todos()
63
- self._load_tickets()
64
-
65
- while True:
66
- # Get terminal size
67
- height, width = stdscr.getmaxyx()
68
-
69
- # Clear screen
70
- stdscr.clear()
71
-
72
- # Draw UI
73
- self._draw_header(stdscr, width)
74
- self._draw_panes(stdscr, height, width)
75
- self._draw_footer(stdscr, height, width)
76
-
77
- # Handle input
78
- key = stdscr.getch()
79
- if key == ord('q') or key == ord('Q'):
80
- if self._confirm_quit(stdscr):
81
- break
82
- elif key == ord('\t'): # Tab to switch panes
83
- self.active_pane = (self.active_pane + 1) % 3
84
- elif key == curses.KEY_F5: # Refresh
85
- self._load_todos()
86
- self._load_tickets()
87
- elif key == ord('n') or key == ord('N'): # New ticket
88
- if self.active_pane == 2:
89
- self._create_ticket(stdscr)
90
-
91
- # Update Claude output
92
- self._update_claude_output()
93
-
94
- # Refresh display
95
- stdscr.refresh()
96
-
97
- def _draw_header(self, stdscr, width):
98
- """Draw the header."""
99
- header = " Claude MPM Terminal UI "
100
- padding = (width - len(header)) // 2
101
- stdscr.attron(curses.color_pair(1))
102
- stdscr.addstr(0, 0, " " * width)
103
- stdscr.addstr(0, padding, header)
104
- stdscr.attroff(curses.color_pair(1))
105
-
106
- def _draw_panes(self, stdscr, height, width):
107
- """Draw the three panes."""
108
- # Calculate pane dimensions
109
- pane_height = height - 3 # Minus header and footer
110
- claude_width = width // 2
111
- side_width = width - claude_width - 1
112
-
113
- # Draw Claude pane (left half)
114
- self._draw_claude_pane(stdscr, 1, 0, pane_height, claude_width)
115
-
116
- # Draw vertical separator
117
- for y in range(1, height - 1):
118
- stdscr.addch(y, claude_width, '│')
119
-
120
- # Draw ToDo pane (top right)
121
- todo_height = pane_height // 2
122
- self._draw_todo_pane(stdscr, 1, claude_width + 1, todo_height, side_width)
123
-
124
- # Draw horizontal separator
125
- stdscr.hline(todo_height + 1, claude_width + 1, '─', side_width)
126
-
127
- # Draw Tickets pane (bottom right)
128
- ticket_height = pane_height - todo_height - 1
129
- self._draw_ticket_pane(stdscr, todo_height + 2, claude_width + 1, ticket_height, side_width)
130
-
131
- def _draw_claude_pane(self, stdscr, y, x, height, width):
132
- """Draw Claude output pane."""
133
- # Title
134
- title = " Claude Output "
135
- if self.active_pane == 0:
136
- stdscr.attron(curses.color_pair(2))
137
- stdscr.addstr(y, x, title + " " * (width - len(title)))
138
- if self.active_pane == 0:
139
- stdscr.attroff(curses.color_pair(2))
140
-
141
- # Content
142
- content_height = height - 2
143
- start_line = max(0, len(self.claude_output) - content_height)
144
-
145
- for i, line in enumerate(self.claude_output[start_line:start_line + content_height]):
146
- if y + i + 2 < y + height:
147
- truncated = line[:width-2] if len(line) > width-2 else line
148
- try:
149
- stdscr.addstr(y + i + 2, x + 1, truncated)
150
- except curses.error:
151
- pass # Ignore if we can't write (edge of screen)
152
-
153
- def _draw_todo_pane(self, stdscr, y, x, height, width):
154
- """Draw ToDo list pane."""
155
- # Title
156
- title = f" ToDo List ({len(self.todos)}) "
157
- if self.active_pane == 1:
158
- stdscr.attron(curses.color_pair(2))
159
- stdscr.addstr(y, x, title + " " * (width - len(title)))
160
- if self.active_pane == 1:
161
- stdscr.attroff(curses.color_pair(2))
162
-
163
- # Content
164
- content_height = height - 2
165
- for i, todo in enumerate(self.todos[:content_height]):
166
- if y + i + 2 < y + height:
167
- status_icon = "✓" if todo.get('status') == 'completed' else "○"
168
- priority = todo.get('priority', 'medium')[0].upper()
169
- text = f"{status_icon} [{priority}] {todo.get('content', '')}"
170
- truncated = text[:width-2] if len(text) > width-2 else text
171
-
172
- # Color based on status
173
- if todo.get('status') == 'completed':
174
- stdscr.attron(curses.color_pair(3))
175
- try:
176
- stdscr.addstr(y + i + 2, x + 1, truncated)
177
- except curses.error:
178
- pass
179
- if todo.get('status') == 'completed':
180
- stdscr.attroff(curses.color_pair(3))
181
-
182
- def _draw_ticket_pane(self, stdscr, y, x, height, width):
183
- """Draw Tickets pane."""
184
- # Title
185
- title = f" Tickets ({len(self.tickets)}) [N]ew "
186
- if self.active_pane == 2:
187
- stdscr.attron(curses.color_pair(2))
188
- stdscr.addstr(y, x, title + " " * (width - len(title)))
189
- if self.active_pane == 2:
190
- stdscr.attroff(curses.color_pair(2))
191
-
192
- # Content
193
- content_height = height - 2
194
- for i, ticket in enumerate(self.tickets[:content_height]):
195
- if y + i + 2 < y + height:
196
- ticket_id = ticket.get('id', 'N/A')
197
- title = ticket.get('title', 'No title')
198
- text = f"[{ticket_id}] {title}"
199
- truncated = text[:width-2] if len(text) > width-2 else text
200
- try:
201
- stdscr.addstr(y + i + 2, x + 1, truncated)
202
- except curses.error:
203
- pass
204
-
205
- def _draw_footer(self, stdscr, height, width):
206
- """Draw the footer."""
207
- footer = " [Tab] Switch Pane | [F5] Refresh | [Q] Quit "
208
- stdscr.attron(curses.color_pair(1))
209
- stdscr.addstr(height - 1, 0, " " * width)
210
- stdscr.addstr(height - 1, 0, footer)
211
- stdscr.attroff(curses.color_pair(1))
212
-
213
- def _start_claude(self):
214
- """Start Claude process in a thread."""
215
- def run_claude():
216
- try:
217
- cmd = ["claude", "--model", "opus", "--dangerously-skip-permissions"]
218
- self.claude_process = subprocess.Popen(
219
- cmd,
220
- stdout=subprocess.PIPE,
221
- stderr=subprocess.STDOUT,
222
- stdin=subprocess.PIPE,
223
- text=True,
224
- bufsize=1
225
- )
226
-
227
- # Read output
228
- for line in iter(self.claude_process.stdout.readline, ''):
229
- if line:
230
- self.output_queue.put(line.rstrip())
231
-
232
- except Exception as e:
233
- self.output_queue.put(f"Error starting Claude: {e}")
234
-
235
- thread = threading.Thread(target=run_claude, daemon=True)
236
- thread.start()
237
-
238
- def _update_claude_output(self):
239
- """Update Claude output from queue."""
240
- try:
241
- while True:
242
- line = self.output_queue.get_nowait()
243
- self.claude_output.append(line)
244
- # Keep last 1000 lines
245
- if len(self.claude_output) > 1000:
246
- self.claude_output = self.claude_output[-1000:]
247
- except queue.Empty:
248
- pass
249
-
250
- def _load_todos(self):
251
- """Load ToDo items from Claude's todo file."""
252
- try:
253
- # Look for Claude's todo files
254
- todo_dir = Path.home() / ".claude" / "todos"
255
- if todo_dir.exists():
256
- todos = []
257
- for todo_file in todo_dir.glob("*.json"):
258
- try:
259
- with open(todo_file, 'r') as f:
260
- data = json.load(f)
261
- if isinstance(data, list):
262
- todos.extend(data)
263
- except:
264
- pass
265
-
266
- # Sort by priority and status
267
- priority_order = {'high': 0, 'medium': 1, 'low': 2}
268
- todos.sort(key=lambda x: (
269
- x.get('status') == 'completed',
270
- priority_order.get(x.get('priority', 'medium'), 1)
271
- ))
272
- self.todos = todos[:20] # Keep top 20
273
- except Exception as e:
274
- self.logger.error(f"Error loading todos: {e}")
275
-
276
- def _load_tickets(self):
277
- """Load tickets from ticket manager."""
278
- if self.ticket_manager:
279
- try:
280
- self.tickets = self.ticket_manager.list_recent_tickets(limit=20)
281
- except Exception as e:
282
- self.logger.error(f"Error loading tickets: {e}")
283
-
284
- def _create_ticket(self, stdscr):
285
- """Create a new ticket."""
286
- if not self.ticket_manager:
287
- return
288
-
289
- # Simple input dialog
290
- curses.echo()
291
- stdscr.addstr(10, 10, "Enter ticket title: ")
292
- stdscr.refresh()
293
- title = stdscr.getstr(10, 30, 60).decode('utf-8')
294
- curses.noecho()
295
-
296
- if title:
297
- try:
298
- ticket = self.ticket_manager.create_ticket(
299
- title=title,
300
- description="Created from Terminal UI",
301
- priority="medium"
302
- )
303
- self._load_tickets()
304
- except Exception as e:
305
- self.logger.error(f"Error creating ticket: {e}")
306
-
307
- def _confirm_quit(self, stdscr):
308
- """Confirm quit dialog."""
309
- height, width = stdscr.getmaxyx()
310
- msg = "Really quit? (y/n)"
311
- y = height // 2
312
- x = (width - len(msg)) // 2
313
-
314
- stdscr.addstr(y, x, msg)
315
- stdscr.refresh()
316
-
317
- key = stdscr.getch()
318
- return key == ord('y') or key == ord('Y')
319
-
320
-
321
- def main():
322
- """Run the terminal UI."""
323
- ui = TerminalUI()
324
- ui.run()
325
-
326
-
327
- if __name__ == "__main__":
328
- main()