kollabor 0.4.9__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 (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. system_prompt/example_with_trender.md +47 -0
plugins/tmux_plugin.py ADDED
@@ -0,0 +1,795 @@
1
+ """Tmux integration plugin for managing and viewing tmux sessions.
2
+
3
+ Provides commands to:
4
+ - Create new tmux sessions with commands
5
+ - View live tmux session output in alt buffer
6
+ - List active sessions
7
+ - Kill sessions
8
+ """
9
+
10
+ import asyncio
11
+ import subprocess
12
+ import logging
13
+ import shutil
14
+ from typing import Dict, Any, List, Optional
15
+ from dataclasses import dataclass, field
16
+ from datetime import datetime
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class TmuxSession:
23
+ """Represents a managed tmux session."""
24
+ name: str
25
+ command: str
26
+ created_at: datetime = field(default_factory=datetime.now)
27
+ pid: Optional[int] = None
28
+
29
+ def is_alive(self) -> bool:
30
+ """Check if the tmux session is still running."""
31
+ try:
32
+ result = subprocess.run(
33
+ ["tmux", "has-session", "-t", self.name],
34
+ capture_output=True
35
+ )
36
+ return result.returncode == 0
37
+ except Exception:
38
+ return False
39
+
40
+
41
+ class TmuxPlugin:
42
+ """Plugin for tmux session management and live viewing."""
43
+
44
+ def __init__(self, name: str = "tmux", state_manager=None, event_bus=None,
45
+ renderer=None, config=None):
46
+ """Initialize the tmux plugin.
47
+
48
+ Args:
49
+ name: Plugin name.
50
+ state_manager: State management system.
51
+ event_bus: Event bus for hook registration.
52
+ renderer: Terminal renderer.
53
+ config: Configuration manager.
54
+ """
55
+ self.name = name
56
+ self.version = "1.0.0"
57
+ self.description = "Manage and view tmux sessions"
58
+ self.enabled = True
59
+
60
+ self.sessions: Dict[str, TmuxSession] = {}
61
+ self.state_manager = state_manager
62
+ self.event_bus = event_bus
63
+ self.renderer = renderer
64
+ self.config = config
65
+ self.command_registry = None
66
+ self.input_handler = None
67
+ self._current_session: Optional[str] = None # Currently viewing session
68
+ self._last_arrow_time: float = 0.0 # For double-arrow detection
69
+ self._last_arrow_dir: Optional[str] = None # "Left" or "Right"
70
+ self._double_arrow_threshold: float = 0.3 # seconds
71
+
72
+ self.logger = logger
73
+
74
+ @staticmethod
75
+ def get_default_config() -> Dict[str, Any]:
76
+ """Get default configuration for tmux plugin."""
77
+ return {
78
+ "plugins": {
79
+ "tmux": {
80
+ "enabled": True,
81
+ "show_status": True,
82
+ "refresh_rate": 0.1, # Live view refresh rate in seconds
83
+ "default_shell": "/bin/bash"
84
+ }
85
+ }
86
+ }
87
+
88
+ async def initialize(self, event_bus, config, **kwargs) -> None:
89
+ """Initialize the plugin.
90
+
91
+ Args:
92
+ event_bus: Application event bus.
93
+ config: Configuration manager.
94
+ **kwargs: Additional parameters including command_registry.
95
+ """
96
+ try:
97
+ self.event_bus = event_bus
98
+ self.config = config
99
+ self.command_registry = kwargs.get('command_registry')
100
+ self.input_handler = kwargs.get('input_handler')
101
+ self.renderer = kwargs.get('renderer')
102
+
103
+ # Check if tmux is available
104
+ if not self._check_tmux_available():
105
+ self.logger.warning("tmux not found in PATH - tmux plugin disabled")
106
+ self.enabled = False
107
+ return
108
+
109
+ # Register commands
110
+ if self.command_registry:
111
+ self._register_commands()
112
+
113
+ # Discover existing sessions
114
+ self._discover_existing_sessions()
115
+
116
+ # Register status view
117
+ await self._register_status_view()
118
+
119
+ self.logger.info("Tmux plugin initialized successfully")
120
+
121
+ except Exception as e:
122
+ self.logger.error(f"Error initializing tmux plugin: {e}")
123
+ raise
124
+
125
+ def _check_tmux_available(self) -> bool:
126
+ """Check if tmux is available on the system."""
127
+ return shutil.which("tmux") is not None
128
+
129
+ def _register_commands(self):
130
+ """Register tmux commands with the command registry."""
131
+ from core.events.models import (
132
+ CommandDefinition, CommandMode, CommandCategory
133
+ )
134
+
135
+ # /terminal - manage tmux sessions
136
+ terminal_cmd = CommandDefinition(
137
+ name="terminal",
138
+ description="Manage tmux sessions (new/view/list/kill)",
139
+ handler=self._handle_tmux_command,
140
+ plugin_name=self.name,
141
+ category=CommandCategory.SYSTEM,
142
+ mode=CommandMode.INSTANT,
143
+ aliases=["term", "tmux", "t"],
144
+ icon="[>_]"
145
+ )
146
+ self.command_registry.register_command(terminal_cmd)
147
+
148
+ self.logger.info("Tmux commands registered")
149
+
150
+ async def _handle_tmux_command(self, command) -> "CommandResult":
151
+ """Handle /tmux command with subcommands.
152
+
153
+ Usage:
154
+ /tmux new <command> <session_name> - Create new session
155
+ /tmux view <session_name> - Live view session
156
+ /tmux list - List sessions
157
+ /tmux kill <session_name> - Kill session
158
+ /tmux attach <session_name> - Attach to session (exits kollabor)
159
+ """
160
+ from core.events.models import CommandResult
161
+
162
+ args = command.args if command.args else []
163
+
164
+ if not args:
165
+ # No args - go directly to view mode
166
+ return await self._handle_view_session([])
167
+
168
+ subcommand = args[0].lower()
169
+
170
+ if subcommand == "new":
171
+ return await self._handle_new_session(args[1:])
172
+ elif subcommand == "view":
173
+ return await self._handle_view_session(args[1:])
174
+ elif subcommand == "list" or subcommand == "ls":
175
+ return await self._handle_list_sessions()
176
+ elif subcommand == "kill":
177
+ return await self._handle_kill_session(args[1:])
178
+ elif subcommand == "attach":
179
+ return await self._handle_attach_session(args[1:])
180
+ elif subcommand == "help" or subcommand == "--help" or subcommand == "-h":
181
+ return CommandResult(
182
+ success=True,
183
+ message=self._get_help_text(),
184
+ display_type="info"
185
+ )
186
+ else:
187
+ return CommandResult(
188
+ success=False,
189
+ message=f"Unknown subcommand: {subcommand}\n\n{self._get_help_text()}",
190
+ display_type="error"
191
+ )
192
+
193
+ def _get_help_text(self) -> str:
194
+ """Get help text for terminal command."""
195
+ return """Terminal Session Manager
196
+
197
+ Usage:
198
+ /terminal new <name> <command> Create new session running command
199
+ /terminal view [name] Live view session (</> to cycle)
200
+ /terminal list List all sessions
201
+ /terminal kill <name> Kill a session
202
+ /terminal attach <name> Attach to session (leaves kollabor)
203
+
204
+ Examples:
205
+ /terminal new myserver python -m http.server 8080
206
+ /terminal new logs tail -f /var/log/syslog
207
+ /terminal view
208
+ /terminal kill myserver
209
+
210
+ Aliases: /t, /term, /tmux"""
211
+
212
+ async def _handle_new_session(self, args: List[str]) -> "CommandResult":
213
+ """Create a new tmux session."""
214
+ from core.events.models import CommandResult
215
+
216
+ if len(args) < 1:
217
+ return CommandResult(
218
+ success=False,
219
+ message="Usage: /terminal new <session_name> [command]",
220
+ display_type="error"
221
+ )
222
+
223
+ session_name = args[0]
224
+ command = " ".join(args[1:]) if len(args) > 1 else None
225
+
226
+ # Check if session already exists
227
+ if session_name in self.sessions and self.sessions[session_name].is_alive():
228
+ return CommandResult(
229
+ success=False,
230
+ message=f"Session '{session_name}' already exists",
231
+ display_type="error"
232
+ )
233
+
234
+ try:
235
+ # Create detached tmux session with interactive bash shell
236
+ result = subprocess.run(
237
+ ["tmux", "new-session", "-d", "-s", session_name, "bash", "-i"],
238
+ capture_output=True,
239
+ text=True
240
+ )
241
+
242
+ if result.returncode != 0:
243
+ return CommandResult(
244
+ success=False,
245
+ message=f"Failed to create session: {result.stderr}",
246
+ display_type="error"
247
+ )
248
+
249
+ # Send the command to the new session (if provided)
250
+ if command:
251
+ send_result = subprocess.run(
252
+ ["tmux", "send-keys", "-t", session_name, command, "Enter"],
253
+ capture_output=True,
254
+ text=True
255
+ )
256
+
257
+ if send_result.returncode != 0:
258
+ self.logger.warning(f"Session created but failed to send command: {send_result.stderr}")
259
+
260
+ # Track the session
261
+ self.sessions[session_name] = TmuxSession(
262
+ name=session_name,
263
+ command=command or "bash"
264
+ )
265
+
266
+ msg = f"Created session '{session_name}'"
267
+ if command:
268
+ msg += f" running: {command}"
269
+ return CommandResult(
270
+ success=True,
271
+ message=msg,
272
+ display_type="success"
273
+ )
274
+
275
+ except Exception as e:
276
+ return CommandResult(
277
+ success=False,
278
+ message=f"Error creating session: {e}",
279
+ display_type="error"
280
+ )
281
+
282
+ async def _handle_view_session(self, args: List[str]) -> "CommandResult":
283
+ """View a tmux session live in alt buffer."""
284
+ from core.events.models import CommandResult
285
+
286
+ # If no args, get first available session
287
+ if not args:
288
+ sessions = self._get_all_tmux_sessions()
289
+ if not sessions:
290
+ return CommandResult(
291
+ success=False,
292
+ message="No tmux sessions found. Use '/tmux new <name> <command>' to create one.",
293
+ display_type="error"
294
+ )
295
+ session_name = sessions[0]
296
+ else:
297
+ session_name = args[0]
298
+ # Check if session exists (in tmux, not just our tracking)
299
+ if not self._session_exists(session_name):
300
+ return CommandResult(
301
+ success=False,
302
+ message=f"Session '{session_name}' not found",
303
+ display_type="error"
304
+ )
305
+
306
+ # Emit live modal trigger event
307
+ if self.event_bus:
308
+ from core.ui.live_modal_renderer import LiveModalConfig
309
+ from core.events.models import EventType
310
+
311
+ # Content generator - uses self._current_session for dynamic switching
312
+ async def get_tmux_content() -> List[str]:
313
+ # Show session name as header line
314
+ sessions = self._get_all_tmux_sessions()
315
+ session_idx = sessions.index(self._current_session) + 1 if self._current_session in sessions else 1
316
+ header = f"[Session: {self._current_session}] ({session_idx}/{len(sessions)})"
317
+ content = self._capture_tmux_pane(self._current_session)
318
+ return [header, "─" * len(header)] + content
319
+
320
+ # Input callback for passthrough
321
+ async def handle_input(key_press) -> bool:
322
+ if key_press.name == "Escape":
323
+ return True # Exit
324
+
325
+ # Key8776 (≈) kills the current session
326
+ if key_press.code == 8776:
327
+ try:
328
+ subprocess.run(
329
+ ["tmux", "kill-session", "-t", self._current_session],
330
+ capture_output=True,
331
+ text=True,
332
+ timeout=2
333
+ )
334
+ logger.info(f"Killed tmux session: {self._current_session}")
335
+ return True # Exit modal after killing session
336
+ except Exception as e:
337
+ logger.error(f"Failed to kill tmux session {self._current_session}: {e}")
338
+ return False
339
+
340
+ # Use Opt+Right (Alt+ArrowRight) and Opt+Left (Alt+ArrowLeft) to cycle sessions
341
+ # Support both Alt+Arrow and Alt+letter sequences
342
+
343
+ if key_press.name == "Alt+ArrowRight":
344
+ new_session = self._cycle_session(forward=True)
345
+ if new_session:
346
+ self._current_session = new_session
347
+ return False
348
+ elif key_press.name == "Alt+ArrowLeft":
349
+ new_session = self._cycle_session(forward=False)
350
+ if new_session:
351
+ self._current_session = new_session
352
+ return False
353
+ # Also support Alt+f and Alt+b for terminals that use these sequences
354
+ elif key_press.name == "Alt+f":
355
+ new_session = self._cycle_session(forward=True)
356
+ if new_session:
357
+ self._current_session = new_session
358
+ return False
359
+ elif key_press.name == "Alt+b":
360
+ new_session = self._cycle_session(forward=False)
361
+ if new_session:
362
+ self._current_session = new_session
363
+ return False
364
+
365
+ # Only forward keys if we have an active session
366
+ if self._current_session is None:
367
+ return False
368
+
369
+ # Forward arrow keys to tmux
370
+ if key_press.name in ("ArrowLeft", "ArrowRight"):
371
+ self._send_keys_to_tmux(self._current_session, key_press.name.replace("Arrow", ""))
372
+ return False
373
+
374
+ # Handle Ctrl+C - send to tmux (interrupt)
375
+ if key_press.char and ord(key_press.char) == 3:
376
+ self._send_keys_to_tmux(self._current_session, "C-c")
377
+ return False
378
+
379
+ # Map special keys to tmux key names
380
+ key_map = {
381
+ "Enter": "Enter",
382
+ "Backspace": "BSpace",
383
+ "Tab": "Tab",
384
+ "ArrowUp": "Up",
385
+ "ArrowDown": "Down",
386
+ "Home": "Home",
387
+ "End": "End",
388
+ "PageUp": "PageUp",
389
+ "PageDown": "PageDown",
390
+ "Delete": "DC",
391
+ }
392
+
393
+ # Forward special keys
394
+ if key_press.name in key_map:
395
+ self._send_keys_to_tmux(self._current_session, key_map[key_press.name])
396
+ # Forward regular character keys
397
+ elif key_press.char:
398
+ self._send_keys_to_tmux(self._current_session, key_press.char)
399
+
400
+ return False
401
+
402
+ # Configure the live modal with streaming-friendly refresh rate
403
+ config = LiveModalConfig(
404
+ title="terminal",
405
+ footer="Esc: exit | Opt+Left/Right: cycle | Opt+x: kills session",
406
+ refresh_rate=self.config.get("plugins.tmux.refresh_rate", 2.0), # 2 seconds - much slower
407
+ passthrough_input=True
408
+ )
409
+
410
+ # Store current session
411
+ self._current_session = session_name
412
+
413
+ # Emit event to trigger live modal (input handler will handle it)
414
+ await self.event_bus.emit_with_hooks(
415
+ EventType.LIVE_MODAL_TRIGGER,
416
+ {
417
+ "content_generator": get_tmux_content,
418
+ "config": config,
419
+ "input_callback": handle_input
420
+ },
421
+ "live_modal"
422
+ )
423
+
424
+ return CommandResult(
425
+ success=True,
426
+ message="", # No message needed, modal takes over
427
+ display_type="none"
428
+ )
429
+ else:
430
+ return CommandResult(
431
+ success=False,
432
+ message="Live view not available - event bus not configured",
433
+ display_type="error"
434
+ )
435
+
436
+ def _capture_tmux_pane(self, session_name: str) -> List[str]:
437
+ """Capture current content of a tmux pane."""
438
+ try:
439
+ # Capture only the last 5 lines to reduce overhead
440
+ result = subprocess.run(
441
+ ["tmux", "capture-pane", "-p", "-S", "-5", "-t", session_name],
442
+ capture_output=True,
443
+ text=True,
444
+ timeout=2 # Add timeout to prevent hanging
445
+ )
446
+ if result.returncode == 0:
447
+ # Split on \n and handle empty lines properly
448
+ content = result.stdout
449
+ # Remove trailing empty lines but keep internal empty lines
450
+ while content.endswith('\n'):
451
+ content = content[:-1]
452
+
453
+ lines = content.split('\n')
454
+
455
+ # Clean up captured content for display
456
+ cleaned = []
457
+ for line in lines:
458
+ # Strip ANSI escape sequences and trailing whitespace
459
+ import re
460
+ line = re.sub(r'\x1b\[[0-9;]*m', '', line) # Remove ANSI colors
461
+ line = line.rstrip()
462
+ # Only add non-empty lines or empty lines that have meaningful content
463
+ if line or (cleaned and cleaned[-1] and not cleaned[-1].isspace()):
464
+ cleaned.append(line)
465
+ return cleaned
466
+ else:
467
+ return [f"Error capturing pane: {result.stderr}"]
468
+ except subprocess.TimeoutExpired:
469
+ return [f"Session capture timed out - session might be unresponsive"]
470
+ except Exception as e:
471
+ return [f"Error: {e}"]
472
+
473
+ def _send_keys_to_tmux(self, session_name: str, keys: str):
474
+ """Send keys to a tmux session."""
475
+ try:
476
+ subprocess.run(
477
+ ["tmux", "send-keys", "-t", session_name, keys],
478
+ capture_output=True
479
+ )
480
+ except Exception as e:
481
+ self.logger.error(f"Error sending keys to tmux: {e}")
482
+
483
+ async def _handle_list_sessions(self) -> "CommandResult":
484
+ """List all tmux sessions (both managed and discovered)."""
485
+ from core.events.models import CommandResult
486
+
487
+ # Discover all existing tmux sessions
488
+ all_sessions = self._get_all_tmux_sessions()
489
+
490
+ if not all_sessions:
491
+ return CommandResult(
492
+ success=True,
493
+ message="No tmux sessions found. Use '/tmux new <name> <command>' to create one.",
494
+ display_type="info"
495
+ )
496
+
497
+ # Build list of sessions, one per line
498
+ lines = ["Terminal Sessions:"]
499
+ for session_name in all_sessions:
500
+ # Check if it's a managed session
501
+ if session_name in self.sessions:
502
+ session = self.sessions[session_name]
503
+ status = "[MANAGED]" if session.is_alive() else "[MANAGED-DEAD]"
504
+ lines.append(f"{status} {session_name}")
505
+ else:
506
+ lines.append(f"[EXTERNAL] {session_name}")
507
+
508
+ message = "\n".join(lines)
509
+
510
+ return CommandResult(
511
+ success=True,
512
+ message=message,
513
+ display_type="info"
514
+ )
515
+
516
+ async def _handle_kill_session(self, args: List[str]) -> "CommandResult":
517
+ """Kill a tmux session."""
518
+ from core.events.models import CommandResult
519
+
520
+ if not args:
521
+ return CommandResult(
522
+ success=False,
523
+ message="Usage: /tmux kill <session_name>",
524
+ display_type="error"
525
+ )
526
+
527
+ session_name = args[0]
528
+
529
+ try:
530
+ result = subprocess.run(
531
+ ["tmux", "kill-session", "-t", session_name],
532
+ capture_output=True,
533
+ text=True
534
+ )
535
+
536
+ if result.returncode == 0:
537
+ # Remove from tracking
538
+ if session_name in self.sessions:
539
+ del self.sessions[session_name]
540
+ return CommandResult(
541
+ success=True,
542
+ message=f"Killed session '{session_name}'",
543
+ display_type="info"
544
+ )
545
+ else:
546
+ return CommandResult(
547
+ success=False,
548
+ message=f"Failed to kill session: {result.stderr}",
549
+ display_type="error"
550
+ )
551
+
552
+ except Exception as e:
553
+ return CommandResult(
554
+ success=False,
555
+ message=f"Error killing session: {e}",
556
+ display_type="error"
557
+ )
558
+
559
+ async def _handle_attach_session(self, args: List[str]) -> "CommandResult":
560
+ """Attach to a tmux session (exits kollabor)."""
561
+ from core.events.models import CommandResult
562
+
563
+ if not args:
564
+ return CommandResult(
565
+ success=False,
566
+ message="Usage: /tmux attach <session_name>",
567
+ display_type="error"
568
+ )
569
+
570
+ session_name = args[0]
571
+
572
+ if not self._session_exists(session_name):
573
+ return CommandResult(
574
+ success=False,
575
+ message=f"Session '{session_name}' not found",
576
+ display_type="error"
577
+ )
578
+
579
+ return CommandResult(
580
+ success=True,
581
+ message=f"To attach to '{session_name}', exit kollabor and run:\n tmux attach -t {session_name}",
582
+ display_type="info"
583
+ )
584
+
585
+ def _session_exists(self, session_name: str) -> bool:
586
+ """Check if a tmux session exists."""
587
+ try:
588
+ result = subprocess.run(
589
+ ["tmux", "has-session", "-t", session_name],
590
+ capture_output=True
591
+ )
592
+ return result.returncode == 0
593
+ except Exception:
594
+ return False
595
+
596
+ def _cycle_session(self, forward: bool = True) -> Optional[str]:
597
+ """Cycle to next/previous tmux session.
598
+
599
+ Args:
600
+ forward: True for next session, False for previous.
601
+
602
+ Returns:
603
+ New session name, or None if no other sessions.
604
+ """
605
+ sessions = self._get_all_tmux_sessions()
606
+ if len(sessions) <= 1:
607
+ return None
608
+
609
+ try:
610
+ current_idx = sessions.index(self._current_session)
611
+ if forward:
612
+ new_idx = (current_idx + 1) % len(sessions)
613
+ else:
614
+ new_idx = (current_idx - 1) % len(sessions)
615
+ return sessions[new_idx]
616
+ except ValueError:
617
+ # Current session not in list, return first
618
+ return sessions[0] if sessions else None
619
+
620
+ def _refresh_session_status(self):
621
+ """Refresh status of all tracked sessions."""
622
+ dead_sessions = []
623
+ for name, session in self.sessions.items():
624
+ if not session.is_alive():
625
+ dead_sessions.append(name)
626
+
627
+ # Optionally remove dead sessions from tracking
628
+ # For now, keep them but show as [DEAD]
629
+
630
+ def _get_all_tmux_sessions(self) -> List[str]:
631
+ """Get list of all existing tmux sessions."""
632
+ try:
633
+ result = subprocess.run(
634
+ ["tmux", "list-sessions", "-F", "#{session_name}"],
635
+ capture_output=True,
636
+ text=True,
637
+ check=True
638
+ )
639
+ if result.returncode == 0:
640
+ # Parse output: each line is a session name
641
+ sessions = []
642
+ for line in result.stdout.strip().split('\n'):
643
+ if line:
644
+ sessions.append(line)
645
+ return sessions
646
+ return []
647
+ except subprocess.CalledProcessError:
648
+ # No sessions or tmux not running
649
+ return []
650
+ except Exception as e:
651
+ self.logger.error(f"Error listing tmux sessions: {e}")
652
+ return []
653
+
654
+ def _discover_existing_sessions(self):
655
+ """Discover existing tmux sessions (optional bootstrap)."""
656
+ try:
657
+ result = subprocess.run(
658
+ ["tmux", "list-sessions", "-F", "#{session_name}"],
659
+ capture_output=True,
660
+ text=True
661
+ )
662
+ if result.returncode == 0:
663
+ existing = result.stdout.strip().split("\n")
664
+ self.logger.debug(f"Found existing tmux sessions: {existing}")
665
+ except Exception:
666
+ pass
667
+
668
+ async def _register_status_view(self) -> None:
669
+ """Register tmux sessions status view."""
670
+ try:
671
+ # Check if renderer has status registry
672
+ if (hasattr(self.renderer, 'status_renderer') and
673
+ self.renderer.status_renderer and
674
+ hasattr(self.renderer.status_renderer, 'status_registry') and
675
+ self.renderer.status_renderer.status_registry):
676
+
677
+ from core.io.status_renderer import StatusViewConfig, BlockConfig
678
+
679
+ # Create tmux sessions view
680
+ tmux_view = StatusViewConfig(
681
+ name="Tmux Sessions",
682
+ plugin_source="tmux",
683
+ priority=500, # Between core views and plugin views
684
+ blocks=[
685
+ BlockConfig(
686
+ width_fraction=1.0,
687
+ content_provider=self._get_tmux_sessions_content,
688
+ title="Tmux Sessions",
689
+ priority=100
690
+ )
691
+ ],
692
+ )
693
+
694
+ registry = self.renderer.status_renderer.status_registry
695
+ registry.register_status_view("tmux", tmux_view)
696
+ logger.info("Registered 'Tmux Sessions' status view")
697
+
698
+ else:
699
+ logger.debug("Status registry not available - cannot register status view")
700
+
701
+ except Exception as e:
702
+ logger.error(f"Failed to register tmux status view: {e}")
703
+
704
+ def _get_tmux_sessions_content(self) -> List[str]:
705
+ """Get tmux sessions content for status view."""
706
+ try:
707
+ if not self.enabled:
708
+ return ["Tmux: Disabled"]
709
+
710
+ # Get all tmux sessions (both managed and discovered)
711
+ all_sessions = self._get_all_tmux_sessions()
712
+
713
+ if not all_sessions:
714
+ return ["Sessions: 0 active"]
715
+
716
+ lines = []
717
+ session_count = len(all_sessions)
718
+
719
+ # Header line
720
+ lines.append(f"Sessions: {session_count} active | /terminal to view")
721
+
722
+ # Display sessions in 3-column layout
723
+ max_display = 9 # Show max 9 sessions (3 rows x 3 columns)
724
+ display_sessions = all_sessions[:max_display]
725
+
726
+ # Group into rows of 3
727
+ rows = []
728
+ for i in range(0, len(display_sessions), 3):
729
+ row = display_sessions[i:i+3]
730
+ rows.append(row)
731
+
732
+ # Format each row
733
+ for row in rows:
734
+ # Pad row to 3 columns
735
+ while len(row) < 3:
736
+ row.append("")
737
+
738
+ # Format with fixed width columns
739
+ formatted_row = f"{row[0]:<30}{row[1]:<30}{row[2]:<30}"
740
+ lines.append(formatted_row.rstrip())
741
+
742
+ # Show overflow count if there are more sessions
743
+ if session_count > max_display:
744
+ overflow = session_count - max_display
745
+ lines.append(f"({overflow} more...)")
746
+
747
+ return lines
748
+
749
+ except Exception as e:
750
+ logger.error(f"Error getting tmux sessions content: {e}")
751
+ return ["Tmux: Error"]
752
+
753
+ def get_status_line(self) -> Dict[str, List[str]]:
754
+ """Get status line (no longer used - using status view instead)."""
755
+ # Return empty - we use the dedicated status view now
756
+ return {"A": [], "B": [], "C": []}
757
+
758
+ async def shutdown(self) -> None:
759
+ """Shutdown the plugin."""
760
+ try:
761
+ # Clear session tracking
762
+ self._current_session = None
763
+ self.logger.info("Tmux plugin shutdown completed")
764
+
765
+ except Exception as e:
766
+ self.logger.error(f"Error shutting down tmux plugin: {e}")
767
+
768
+ async def register_hooks(self) -> None:
769
+ """Register event hooks."""
770
+ # Could register hooks for live modal input handling
771
+ pass
772
+
773
+ @staticmethod
774
+ def get_config_widgets() -> Optional[Dict[str, Any]]:
775
+ """Get configuration widgets for the config modal."""
776
+ return {
777
+ "title": "Tmux Settings",
778
+ "widgets": [
779
+ {
780
+ "type": "checkbox",
781
+ "label": "Show Status",
782
+ "config_path": "plugins.tmux.show_status",
783
+ "help": "Show tmux session count in status bar"
784
+ },
785
+ {
786
+ "type": "slider",
787
+ "label": "Refresh Rate (ms)",
788
+ "config_path": "plugins.tmux.refresh_rate_ms",
789
+ "min_value": 50,
790
+ "max_value": 1000,
791
+ "step": 50,
792
+ "help": "Live view refresh rate in milliseconds"
793
+ }
794
+ ]
795
+ }