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.
- core/__init__.py +18 -0
- core/application.py +578 -0
- core/cli.py +193 -0
- core/commands/__init__.py +43 -0
- core/commands/executor.py +277 -0
- core/commands/menu_renderer.py +319 -0
- core/commands/parser.py +186 -0
- core/commands/registry.py +331 -0
- core/commands/system_commands.py +479 -0
- core/config/__init__.py +7 -0
- core/config/llm_task_config.py +110 -0
- core/config/loader.py +501 -0
- core/config/manager.py +112 -0
- core/config/plugin_config_manager.py +346 -0
- core/config/plugin_schema.py +424 -0
- core/config/service.py +399 -0
- core/effects/__init__.py +1 -0
- core/events/__init__.py +12 -0
- core/events/bus.py +129 -0
- core/events/executor.py +154 -0
- core/events/models.py +258 -0
- core/events/processor.py +176 -0
- core/events/registry.py +289 -0
- core/fullscreen/__init__.py +19 -0
- core/fullscreen/command_integration.py +290 -0
- core/fullscreen/components/__init__.py +12 -0
- core/fullscreen/components/animation.py +258 -0
- core/fullscreen/components/drawing.py +160 -0
- core/fullscreen/components/matrix_components.py +177 -0
- core/fullscreen/manager.py +302 -0
- core/fullscreen/plugin.py +204 -0
- core/fullscreen/renderer.py +282 -0
- core/fullscreen/session.py +324 -0
- core/io/__init__.py +52 -0
- core/io/buffer_manager.py +362 -0
- core/io/config_status_view.py +272 -0
- core/io/core_status_views.py +410 -0
- core/io/input_errors.py +313 -0
- core/io/input_handler.py +2655 -0
- core/io/input_mode_manager.py +402 -0
- core/io/key_parser.py +344 -0
- core/io/layout.py +587 -0
- core/io/message_coordinator.py +204 -0
- core/io/message_renderer.py +601 -0
- core/io/modal_interaction_handler.py +315 -0
- core/io/raw_input_processor.py +946 -0
- core/io/status_renderer.py +845 -0
- core/io/terminal_renderer.py +586 -0
- core/io/terminal_state.py +551 -0
- core/io/visual_effects.py +734 -0
- core/llm/__init__.py +26 -0
- core/llm/api_communication_service.py +863 -0
- core/llm/conversation_logger.py +473 -0
- core/llm/conversation_manager.py +414 -0
- core/llm/file_operations_executor.py +1401 -0
- core/llm/hook_system.py +402 -0
- core/llm/llm_service.py +1629 -0
- core/llm/mcp_integration.py +386 -0
- core/llm/message_display_service.py +450 -0
- core/llm/model_router.py +214 -0
- core/llm/plugin_sdk.py +396 -0
- core/llm/response_parser.py +848 -0
- core/llm/response_processor.py +364 -0
- core/llm/tool_executor.py +520 -0
- core/logging/__init__.py +19 -0
- core/logging/setup.py +208 -0
- core/models/__init__.py +5 -0
- core/models/base.py +23 -0
- core/plugins/__init__.py +13 -0
- core/plugins/collector.py +212 -0
- core/plugins/discovery.py +386 -0
- core/plugins/factory.py +263 -0
- core/plugins/registry.py +152 -0
- core/storage/__init__.py +5 -0
- core/storage/state_manager.py +84 -0
- core/ui/__init__.py +6 -0
- core/ui/config_merger.py +176 -0
- core/ui/config_widgets.py +369 -0
- core/ui/live_modal_renderer.py +276 -0
- core/ui/modal_actions.py +162 -0
- core/ui/modal_overlay_renderer.py +373 -0
- core/ui/modal_renderer.py +591 -0
- core/ui/modal_state_manager.py +443 -0
- core/ui/widget_integration.py +222 -0
- core/ui/widgets/__init__.py +27 -0
- core/ui/widgets/base_widget.py +136 -0
- core/ui/widgets/checkbox.py +85 -0
- core/ui/widgets/dropdown.py +140 -0
- core/ui/widgets/label.py +78 -0
- core/ui/widgets/slider.py +185 -0
- core/ui/widgets/text_input.py +224 -0
- core/utils/__init__.py +11 -0
- core/utils/config_utils.py +656 -0
- core/utils/dict_utils.py +212 -0
- core/utils/error_utils.py +275 -0
- core/utils/key_reader.py +171 -0
- core/utils/plugin_utils.py +267 -0
- core/utils/prompt_renderer.py +151 -0
- kollabor-0.4.9.dist-info/METADATA +298 -0
- kollabor-0.4.9.dist-info/RECORD +128 -0
- kollabor-0.4.9.dist-info/WHEEL +5 -0
- kollabor-0.4.9.dist-info/entry_points.txt +2 -0
- kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
- kollabor-0.4.9.dist-info/top_level.txt +4 -0
- kollabor_cli_main.py +20 -0
- plugins/__init__.py +1 -0
- plugins/enhanced_input/__init__.py +18 -0
- plugins/enhanced_input/box_renderer.py +103 -0
- plugins/enhanced_input/box_styles.py +142 -0
- plugins/enhanced_input/color_engine.py +165 -0
- plugins/enhanced_input/config.py +150 -0
- plugins/enhanced_input/cursor_manager.py +72 -0
- plugins/enhanced_input/geometry.py +81 -0
- plugins/enhanced_input/state.py +130 -0
- plugins/enhanced_input/text_processor.py +115 -0
- plugins/enhanced_input_plugin.py +385 -0
- plugins/fullscreen/__init__.py +9 -0
- plugins/fullscreen/example_plugin.py +327 -0
- plugins/fullscreen/matrix_plugin.py +132 -0
- plugins/hook_monitoring_plugin.py +1299 -0
- plugins/query_enhancer_plugin.py +350 -0
- plugins/save_conversation_plugin.py +502 -0
- plugins/system_commands_plugin.py +93 -0
- plugins/tmux_plugin.py +795 -0
- plugins/workflow_enforcement_plugin.py +629 -0
- system_prompt/default.md +1286 -0
- system_prompt/default_win.md +265 -0
- system_prompt/example_with_trender.md +47 -0
core/ui/modal_actions.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Modal action handlers for save/cancel functionality."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, Any, List
|
|
5
|
+
from .config_merger import ConfigMerger
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModalActionHandler:
|
|
11
|
+
"""Handles modal actions like save/cancel with config persistence."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config_service):
|
|
14
|
+
"""Initialize action handler with config service.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
config_service: ConfigService instance for persistence.
|
|
18
|
+
"""
|
|
19
|
+
self.config_service = config_service
|
|
20
|
+
|
|
21
|
+
async def handle_action(self, action: str, widgets: List[Any]) -> Dict[str, Any]:
|
|
22
|
+
"""Handle modal action (save/cancel).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
action: Action to perform ("save" or "cancel").
|
|
26
|
+
widgets: List of widgets to collect values from.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Action result with success status and message.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
if action == "save":
|
|
33
|
+
return await self._handle_save_action(widgets)
|
|
34
|
+
elif action == "cancel":
|
|
35
|
+
return await self._handle_cancel_action()
|
|
36
|
+
else:
|
|
37
|
+
return {
|
|
38
|
+
"success": False,
|
|
39
|
+
"message": f"Unknown action: {action}",
|
|
40
|
+
"action": action
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error(f"Error handling modal action {action}: {e}")
|
|
45
|
+
return {
|
|
46
|
+
"success": False,
|
|
47
|
+
"message": f"Error handling action: {str(e)}",
|
|
48
|
+
"action": action
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async def _handle_save_action(self, widgets: List[Any]) -> Dict[str, Any]:
|
|
52
|
+
"""Handle save action with config persistence.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
widgets: List of widgets to save values from.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Save action result.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
logger.info(f"=== SAVE ACTION: Processing {len(widgets)} widgets ===")
|
|
62
|
+
for i, w in enumerate(widgets):
|
|
63
|
+
logger.info(f" Widget {i}: {w.__class__.__name__} path={getattr(w, 'config_path', 'N/A')} pending={getattr(w, '_pending_value', 'N/A')}")
|
|
64
|
+
|
|
65
|
+
# Collect widget changes
|
|
66
|
+
changes = ConfigMerger.collect_widget_changes(widgets)
|
|
67
|
+
logger.info(f"=== SAVE ACTION: Collected {len(changes)} changes: {changes} ===")
|
|
68
|
+
|
|
69
|
+
if not changes:
|
|
70
|
+
return {
|
|
71
|
+
"success": True,
|
|
72
|
+
"message": "No changes to save",
|
|
73
|
+
"action": "save",
|
|
74
|
+
"changes_count": 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Validate changes before applying
|
|
78
|
+
validation = ConfigMerger.validate_config_changes(self.config_service, changes)
|
|
79
|
+
if not validation["valid"]:
|
|
80
|
+
error_msg = f"Invalid configuration: {', '.join(validation['errors'])}"
|
|
81
|
+
logger.error(error_msg)
|
|
82
|
+
return {
|
|
83
|
+
"success": False,
|
|
84
|
+
"message": error_msg,
|
|
85
|
+
"action": "save",
|
|
86
|
+
"validation_errors": validation["errors"]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Apply changes
|
|
90
|
+
success = ConfigMerger.apply_widget_changes(self.config_service, changes)
|
|
91
|
+
|
|
92
|
+
if success:
|
|
93
|
+
logger.info(f"Successfully saved {len(changes)} configuration changes")
|
|
94
|
+
return {
|
|
95
|
+
"success": True,
|
|
96
|
+
"message": f"Saved {len(changes)} configuration changes",
|
|
97
|
+
"action": "save",
|
|
98
|
+
"changes_count": len(changes),
|
|
99
|
+
"changes": changes
|
|
100
|
+
}
|
|
101
|
+
else:
|
|
102
|
+
return {
|
|
103
|
+
"success": False,
|
|
104
|
+
"message": "Failed to save configuration changes",
|
|
105
|
+
"action": "save",
|
|
106
|
+
"changes_count": len(changes)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error in save action: {e}")
|
|
111
|
+
return {
|
|
112
|
+
"success": False,
|
|
113
|
+
"message": f"Save failed: {str(e)}",
|
|
114
|
+
"action": "save"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async def _handle_cancel_action(self) -> Dict[str, Any]:
|
|
118
|
+
"""Handle cancel action (no persistence).
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Cancel action result.
|
|
122
|
+
"""
|
|
123
|
+
logger.info("Modal cancelled - no changes saved")
|
|
124
|
+
return {
|
|
125
|
+
"success": True,
|
|
126
|
+
"message": "Configuration changes cancelled",
|
|
127
|
+
"action": "cancel"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def get_save_confirmation_message(self, changes: Dict[str, Any]) -> str:
|
|
131
|
+
"""Generate save confirmation message.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
changes: Dictionary of config changes.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Formatted confirmation message.
|
|
138
|
+
"""
|
|
139
|
+
if not changes:
|
|
140
|
+
return "No changes to save"
|
|
141
|
+
|
|
142
|
+
change_list = []
|
|
143
|
+
for path, value in changes.items():
|
|
144
|
+
# Truncate long values for display
|
|
145
|
+
display_value = str(value)
|
|
146
|
+
if len(display_value) > 30:
|
|
147
|
+
display_value = display_value[:27] + "..."
|
|
148
|
+
change_list.append(f"{path} = {display_value}")
|
|
149
|
+
|
|
150
|
+
changes_text = "\n".join(f" • {change}" for change in change_list[:5])
|
|
151
|
+
if len(changes) > 5:
|
|
152
|
+
changes_text += f"\n ... and {len(changes) - 5} more"
|
|
153
|
+
|
|
154
|
+
return f"Save {len(changes)} configuration changes?\n\n{changes_text}"
|
|
155
|
+
|
|
156
|
+
def get_cancel_confirmation_message(self) -> str:
|
|
157
|
+
"""Generate cancel confirmation message.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Formatted confirmation message.
|
|
161
|
+
"""
|
|
162
|
+
return "Cancel configuration changes?\n\nAll unsaved changes will be lost."
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Pure modal overlay renderer that bypasses chat message system.
|
|
2
|
+
|
|
3
|
+
This renderer provides true modal overlay functionality by:
|
|
4
|
+
1. Using direct terminal output (no conversation buffer)
|
|
5
|
+
2. Saving/restoring terminal state
|
|
6
|
+
3. Implementing proper screen buffer management
|
|
7
|
+
4. Providing isolated modal display that never accumulates in chat
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from ..io.visual_effects import ColorPalette
|
|
15
|
+
from ..io.terminal_state import TerminalState
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ModalState:
|
|
22
|
+
"""Represents saved terminal state for modal restoration."""
|
|
23
|
+
cursor_position: Tuple[int, int]
|
|
24
|
+
screen_lines: List[str]
|
|
25
|
+
cursor_visible: bool
|
|
26
|
+
terminal_size: Tuple[int, int]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ModalOverlayRenderer:
|
|
30
|
+
"""Pure modal overlay renderer with terminal state isolation.
|
|
31
|
+
|
|
32
|
+
This class provides modal display functionality that:
|
|
33
|
+
- Never interacts with conversation buffer or chat pipeline
|
|
34
|
+
- Uses direct terminal control sequences for overlay rendering
|
|
35
|
+
- Maintains complete isolation from message accumulation systems
|
|
36
|
+
- Implements proper save/restore of terminal state
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, terminal_state: TerminalState):
|
|
40
|
+
"""Initialize modal overlay renderer.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
terminal_state: TerminalState instance for direct terminal control.
|
|
44
|
+
"""
|
|
45
|
+
self.terminal_state = terminal_state
|
|
46
|
+
self.modal_active = False
|
|
47
|
+
self.saved_state: Optional[ModalState] = None
|
|
48
|
+
self.modal_lines: List[str] = []
|
|
49
|
+
self.modal_position = (0, 0) # (row, col) for modal positioning
|
|
50
|
+
|
|
51
|
+
def show_modal_overlay(self, modal_lines: List[str],
|
|
52
|
+
center_position: bool = True) -> bool:
|
|
53
|
+
"""Display modal as true overlay without affecting conversation.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
modal_lines: List of modal content lines to display.
|
|
57
|
+
center_position: Whether to center modal on screen.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
True if modal was displayed successfully.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
# Save current terminal state
|
|
64
|
+
if not self._save_terminal_state():
|
|
65
|
+
logger.error("Failed to save terminal state")
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# Calculate modal position
|
|
69
|
+
if center_position:
|
|
70
|
+
self.modal_position = self._calculate_center_position(modal_lines)
|
|
71
|
+
|
|
72
|
+
# Clear any existing modal content
|
|
73
|
+
self._clear_modal_area()
|
|
74
|
+
|
|
75
|
+
# Render modal overlay using direct terminal output
|
|
76
|
+
self._render_modal_direct(modal_lines)
|
|
77
|
+
|
|
78
|
+
# Store modal state
|
|
79
|
+
self.modal_lines = modal_lines.copy()
|
|
80
|
+
self.modal_active = True
|
|
81
|
+
|
|
82
|
+
logger.info(f"Modal overlay displayed with {len(modal_lines)} lines")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"Failed to show modal overlay: {e}")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def refresh_modal_display(self) -> bool:
|
|
90
|
+
"""Refresh modal display without accumulation.
|
|
91
|
+
|
|
92
|
+
This method re-renders the modal content without any interaction
|
|
93
|
+
with conversation buffers or message systems.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if refresh was successful.
|
|
97
|
+
"""
|
|
98
|
+
if not self.modal_active or not self.modal_lines:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Clear current modal area
|
|
103
|
+
self._clear_modal_area()
|
|
104
|
+
|
|
105
|
+
# Re-render modal content directly
|
|
106
|
+
self._render_modal_direct(self.modal_lines)
|
|
107
|
+
|
|
108
|
+
logger.debug("Modal display refreshed without accumulation")
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(f"Failed to refresh modal display: {e}")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
def hide_modal_overlay(self) -> bool:
|
|
116
|
+
"""Hide modal overlay and restore terminal state.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if modal was hidden successfully.
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
if not self.modal_active:
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
# Clear modal area
|
|
126
|
+
self._clear_modal_area()
|
|
127
|
+
|
|
128
|
+
# Restore terminal state if saved
|
|
129
|
+
if self.saved_state:
|
|
130
|
+
self._restore_terminal_state()
|
|
131
|
+
|
|
132
|
+
# Reset modal state
|
|
133
|
+
self.modal_active = False
|
|
134
|
+
self.modal_lines = []
|
|
135
|
+
self.saved_state = None
|
|
136
|
+
|
|
137
|
+
logger.info("Modal overlay hidden and terminal state restored")
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Failed to hide modal overlay: {e}")
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def update_modal_content(self, new_lines: List[str]) -> bool:
|
|
145
|
+
"""Update modal content and refresh display.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
new_lines: New modal content lines.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
True if update was successful.
|
|
152
|
+
"""
|
|
153
|
+
if not self.modal_active:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# Update content and refresh display
|
|
158
|
+
self.modal_lines = new_lines.copy()
|
|
159
|
+
return self.refresh_modal_display()
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(f"Failed to update modal content: {e}")
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
def _save_terminal_state(self) -> bool:
|
|
166
|
+
"""Save current terminal state for restoration.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
True if state was saved successfully.
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
# Get current terminal size
|
|
173
|
+
width, height = self.terminal_state.get_size()
|
|
174
|
+
|
|
175
|
+
# Create saved state (simplified - real implementation would capture screen)
|
|
176
|
+
self.saved_state = ModalState(
|
|
177
|
+
cursor_position=(0, 0), # Would query actual cursor position
|
|
178
|
+
screen_lines=[], # Would capture current screen content
|
|
179
|
+
cursor_visible=not self.terminal_state._cursor_hidden,
|
|
180
|
+
terminal_size=(width, height)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
logger.debug("Terminal state saved for modal")
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.error(f"Failed to save terminal state: {e}")
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
def _restore_terminal_state(self) -> bool:
|
|
191
|
+
"""Restore terminal state from saved state.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
True if state was restored successfully.
|
|
195
|
+
"""
|
|
196
|
+
if not self.saved_state:
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
# Restore cursor visibility
|
|
201
|
+
if self.saved_state.cursor_visible:
|
|
202
|
+
self.terminal_state.show_cursor()
|
|
203
|
+
else:
|
|
204
|
+
self.terminal_state.hide_cursor()
|
|
205
|
+
|
|
206
|
+
logger.debug("Terminal state restored after modal")
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Failed to restore terminal state: {e}")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def _calculate_center_position(self, modal_lines: List[str]) -> Tuple[int, int]:
|
|
214
|
+
"""Calculate center position for modal on screen.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
modal_lines: Modal content lines.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Tuple of (row, col) for modal position.
|
|
221
|
+
"""
|
|
222
|
+
width, height = self.terminal_state.get_size()
|
|
223
|
+
|
|
224
|
+
# Calculate modal dimensions
|
|
225
|
+
modal_height = len(modal_lines)
|
|
226
|
+
modal_width = max(len(line) for line in modal_lines) if modal_lines else 0
|
|
227
|
+
|
|
228
|
+
# Center position
|
|
229
|
+
start_row = max(0, (height - modal_height) // 2)
|
|
230
|
+
start_col = max(0, (width - modal_width) // 2)
|
|
231
|
+
|
|
232
|
+
return (start_row, start_col)
|
|
233
|
+
|
|
234
|
+
def _clear_modal_area(self) -> bool:
|
|
235
|
+
"""Clear the modal display area.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
True if area was cleared successfully.
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
if not self.modal_lines:
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
# Move to modal position and clear each line
|
|
245
|
+
row, col = self.modal_position
|
|
246
|
+
|
|
247
|
+
for i, line in enumerate(self.modal_lines):
|
|
248
|
+
# Move to line position
|
|
249
|
+
self.terminal_state.write_raw(f"\033[{row + i + 1};{col + 1}H")
|
|
250
|
+
# Clear the line content (overwrite with spaces)
|
|
251
|
+
spaces = " " * len(line)
|
|
252
|
+
self.terminal_state.write_raw(spaces)
|
|
253
|
+
|
|
254
|
+
logger.debug(f"Cleared modal area at position {self.modal_position}")
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.error(f"Failed to clear modal area: {e}")
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
def _render_modal_direct(self, modal_lines: List[str]) -> bool:
|
|
262
|
+
"""Render modal content using direct terminal output.
|
|
263
|
+
|
|
264
|
+
This method completely bypasses the message system and writes
|
|
265
|
+
directly to terminal using escape sequences.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
modal_lines: Modal content lines to render.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
True if rendering was successful.
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
if not modal_lines:
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
row, col = self.modal_position
|
|
278
|
+
|
|
279
|
+
# Render each line directly to terminal
|
|
280
|
+
for i, line in enumerate(modal_lines):
|
|
281
|
+
# Move cursor to position
|
|
282
|
+
terminal_row = row + i + 1 # 1-based positioning
|
|
283
|
+
terminal_col = col + 1 # 1-based positioning
|
|
284
|
+
|
|
285
|
+
# Use ANSI escape sequence to position cursor
|
|
286
|
+
self.terminal_state.write_raw(f"\033[{terminal_row};{terminal_col}H")
|
|
287
|
+
|
|
288
|
+
# Write line content directly
|
|
289
|
+
self.terminal_state.write_raw(line)
|
|
290
|
+
|
|
291
|
+
# Hide cursor for clean modal display
|
|
292
|
+
self.terminal_state.hide_cursor()
|
|
293
|
+
|
|
294
|
+
logger.debug(f"Modal rendered directly at position {self.modal_position}")
|
|
295
|
+
return True
|
|
296
|
+
|
|
297
|
+
except Exception as e:
|
|
298
|
+
logger.error(f"Failed to render modal directly: {e}")
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
def get_modal_status(self) -> Dict[str, Any]:
|
|
302
|
+
"""Get current modal overlay status.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Dictionary with modal status information.
|
|
306
|
+
"""
|
|
307
|
+
return {
|
|
308
|
+
"modal_active": self.modal_active,
|
|
309
|
+
"modal_lines_count": len(self.modal_lines),
|
|
310
|
+
"modal_position": self.modal_position,
|
|
311
|
+
"has_saved_state": self.saved_state is not None,
|
|
312
|
+
"terminal_size": self.terminal_state.get_size()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class ModalDisplayCoordinator:
|
|
317
|
+
"""Coordinates modal display with input system without chat interference.
|
|
318
|
+
|
|
319
|
+
This coordinator ensures modal display updates happen through
|
|
320
|
+
the overlay system rather than the conversation pipeline.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def __init__(self, modal_overlay_renderer: ModalOverlayRenderer):
|
|
324
|
+
"""Initialize modal display coordinator.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
modal_overlay_renderer: ModalOverlayRenderer instance.
|
|
328
|
+
"""
|
|
329
|
+
self.overlay_renderer = modal_overlay_renderer
|
|
330
|
+
self.event_handlers = {}
|
|
331
|
+
|
|
332
|
+
def register_modal_event_handler(self, event_type: str, handler) -> None:
|
|
333
|
+
"""Register event handler for modal interactions.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
event_type: Type of event to handle.
|
|
337
|
+
handler: Event handler function.
|
|
338
|
+
"""
|
|
339
|
+
self.event_handlers[event_type] = handler
|
|
340
|
+
|
|
341
|
+
def handle_modal_widget_change(self, widget_data: Dict[str, Any]) -> bool:
|
|
342
|
+
"""Handle widget state change in modal.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
widget_data: Widget state change information.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
True if change was handled successfully.
|
|
349
|
+
"""
|
|
350
|
+
try:
|
|
351
|
+
# Trigger modal refresh through overlay system (not chat system)
|
|
352
|
+
return self.overlay_renderer.refresh_modal_display()
|
|
353
|
+
|
|
354
|
+
except Exception as e:
|
|
355
|
+
logger.error(f"Failed to handle modal widget change: {e}")
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
def handle_modal_navigation(self, navigation_data: Dict[str, Any]) -> bool:
|
|
359
|
+
"""Handle navigation in modal (arrow keys, tab, etc.).
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
navigation_data: Navigation event information.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
True if navigation was handled successfully.
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
# Process navigation and refresh modal display
|
|
369
|
+
return self.overlay_renderer.refresh_modal_display()
|
|
370
|
+
|
|
371
|
+
except Exception as e:
|
|
372
|
+
logger.error(f"Failed to handle modal navigation: {e}")
|
|
373
|
+
return False
|