kollabor 0.4.9__py3-none-any.whl → 0.4.15__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.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
core/ui/config_widgets.py
CHANGED
|
@@ -209,58 +209,158 @@ class ConfigWidgetDefinitions:
|
|
|
209
209
|
]
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
|
-
"title": "
|
|
212
|
+
"title": "Application Settings",
|
|
213
213
|
"widgets": [
|
|
214
214
|
{
|
|
215
215
|
"type": "text_input",
|
|
216
|
-
"label": "
|
|
217
|
-
"config_path": "
|
|
218
|
-
"placeholder": "
|
|
219
|
-
"help": "
|
|
216
|
+
"label": "Application Name",
|
|
217
|
+
"config_path": "application.name",
|
|
218
|
+
"placeholder": "Kollabor CLI",
|
|
219
|
+
"help": "Display name for the application"
|
|
220
220
|
},
|
|
221
221
|
{
|
|
222
222
|
"type": "text_input",
|
|
223
|
-
"label": "
|
|
224
|
-
"config_path": "
|
|
225
|
-
"placeholder": "
|
|
226
|
-
"help": "
|
|
223
|
+
"label": "Version",
|
|
224
|
+
"config_path": "application.version",
|
|
225
|
+
"placeholder": "1.0.0",
|
|
226
|
+
"help": "Current application version"
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"title": "LLM Settings",
|
|
232
|
+
"widgets": [
|
|
233
|
+
{
|
|
234
|
+
"type": "slider",
|
|
235
|
+
"label": "Max History",
|
|
236
|
+
"config_path": "core.llm.max_history",
|
|
237
|
+
"min_value": 10,
|
|
238
|
+
"max_value": 200,
|
|
239
|
+
"step": 10,
|
|
240
|
+
"help": "Maximum conversation history entries to keep"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"type": "slider",
|
|
244
|
+
"label": "Message History Limit",
|
|
245
|
+
"config_path": "core.llm.message_history_limit",
|
|
246
|
+
"min_value": 5,
|
|
247
|
+
"max_value": 100,
|
|
248
|
+
"step": 5,
|
|
249
|
+
"help": "Messages sent to API per request"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"type": "checkbox",
|
|
253
|
+
"label": "Enable Streaming",
|
|
254
|
+
"config_path": "core.llm.enable_streaming",
|
|
255
|
+
"help": "Stream responses as they arrive"
|
|
227
256
|
},
|
|
228
257
|
{
|
|
229
258
|
"type": "slider",
|
|
230
|
-
"label": "
|
|
231
|
-
"config_path": "core.llm.
|
|
259
|
+
"label": "Processing Delay (sec)",
|
|
260
|
+
"config_path": "core.llm.processing_delay",
|
|
232
261
|
"min_value": 0.0,
|
|
233
|
-
"max_value":
|
|
262
|
+
"max_value": 1.0,
|
|
234
263
|
"step": 0.1,
|
|
235
|
-
"help": "
|
|
264
|
+
"help": "Delay between processing steps"
|
|
236
265
|
},
|
|
237
266
|
{
|
|
238
267
|
"type": "slider",
|
|
239
|
-
"label": "
|
|
240
|
-
"config_path": "core.llm.
|
|
268
|
+
"label": "Thinking Delay (sec)",
|
|
269
|
+
"config_path": "core.llm.thinking_delay",
|
|
270
|
+
"min_value": 0.0,
|
|
271
|
+
"max_value": 1.0,
|
|
272
|
+
"step": 0.1,
|
|
273
|
+
"help": "Delay for thinking animation display"
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"title": "Tool Execution Timeouts",
|
|
279
|
+
"widgets": [
|
|
280
|
+
{
|
|
281
|
+
"type": "slider",
|
|
282
|
+
"label": "Terminal Timeout (sec)",
|
|
283
|
+
"config_path": "core.llm.terminal_timeout",
|
|
241
284
|
"min_value": 10,
|
|
242
|
-
"max_value":
|
|
285
|
+
"max_value": 300,
|
|
243
286
|
"step": 10,
|
|
244
|
-
"help": "
|
|
287
|
+
"help": "Timeout for terminal commands in seconds"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"type": "slider",
|
|
291
|
+
"label": "MCP Timeout (sec)",
|
|
292
|
+
"config_path": "core.llm.mcp_timeout",
|
|
293
|
+
"min_value": 10,
|
|
294
|
+
"max_value": 300,
|
|
295
|
+
"step": 10,
|
|
296
|
+
"help": "Timeout for MCP tool calls in seconds"
|
|
245
297
|
}
|
|
246
298
|
]
|
|
247
299
|
},
|
|
248
300
|
{
|
|
249
|
-
"title": "
|
|
301
|
+
"title": "Logging",
|
|
250
302
|
"widgets": [
|
|
251
303
|
{
|
|
252
|
-
"type": "
|
|
253
|
-
"label": "
|
|
254
|
-
"config_path": "
|
|
255
|
-
"
|
|
256
|
-
"help": "
|
|
304
|
+
"type": "dropdown",
|
|
305
|
+
"label": "Log Level",
|
|
306
|
+
"config_path": "logging.level",
|
|
307
|
+
"options": ["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
308
|
+
"help": "Application logging verbosity"
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"title": "Hooks",
|
|
314
|
+
"widgets": [
|
|
315
|
+
{
|
|
316
|
+
"type": "slider",
|
|
317
|
+
"label": "Default Timeout (sec)",
|
|
318
|
+
"config_path": "hooks.default_timeout",
|
|
319
|
+
"min_value": 5,
|
|
320
|
+
"max_value": 120,
|
|
321
|
+
"step": 5,
|
|
322
|
+
"help": "Default hook execution timeout"
|
|
257
323
|
},
|
|
258
324
|
{
|
|
259
|
-
"type": "
|
|
260
|
-
"label": "
|
|
261
|
-
"config_path": "
|
|
262
|
-
"
|
|
263
|
-
"
|
|
325
|
+
"type": "slider",
|
|
326
|
+
"label": "Default Retries",
|
|
327
|
+
"config_path": "hooks.default_retries",
|
|
328
|
+
"min_value": 0,
|
|
329
|
+
"max_value": 10,
|
|
330
|
+
"step": 1,
|
|
331
|
+
"help": "Number of retry attempts for failed hooks"
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"title": "Performance Thresholds",
|
|
337
|
+
"widgets": [
|
|
338
|
+
{
|
|
339
|
+
"type": "slider",
|
|
340
|
+
"label": "Failure Rate Warning",
|
|
341
|
+
"config_path": "performance.failure_rate_warning",
|
|
342
|
+
"min_value": 0.01,
|
|
343
|
+
"max_value": 0.5,
|
|
344
|
+
"step": 0.01,
|
|
345
|
+
"help": "Failure rate to trigger warning (0.05 = 5%)"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"type": "slider",
|
|
349
|
+
"label": "Failure Rate Critical",
|
|
350
|
+
"config_path": "performance.failure_rate_critical",
|
|
351
|
+
"min_value": 0.05,
|
|
352
|
+
"max_value": 0.5,
|
|
353
|
+
"step": 0.01,
|
|
354
|
+
"help": "Failure rate to trigger critical alert (0.15 = 15%)"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"type": "slider",
|
|
358
|
+
"label": "Degradation Threshold",
|
|
359
|
+
"config_path": "performance.degradation_threshold",
|
|
360
|
+
"min_value": 0.05,
|
|
361
|
+
"max_value": 0.5,
|
|
362
|
+
"step": 0.01,
|
|
363
|
+
"help": "Performance degradation threshold (0.15 = 15%)"
|
|
264
364
|
}
|
|
265
365
|
]
|
|
266
366
|
},
|
core/ui/live_modal_renderer.py
CHANGED
|
@@ -75,9 +75,10 @@ class LiveModalRenderer:
|
|
|
75
75
|
width, height = self.terminal_state.get_size()
|
|
76
76
|
|
|
77
77
|
# Create layout for fullscreen modal
|
|
78
|
+
# Use most of the screen, minimal margin for header/footer
|
|
78
79
|
layout = ModalLayout(
|
|
79
80
|
width=width - 4, # Leave margin
|
|
80
|
-
height=height -
|
|
81
|
+
height=height - 4, # Minimal margin for borders
|
|
81
82
|
start_row=1,
|
|
82
83
|
start_col=2,
|
|
83
84
|
center_horizontal=True,
|
core/ui/modal_actions.py
CHANGED
|
@@ -91,6 +91,11 @@ class ModalActionHandler:
|
|
|
91
91
|
|
|
92
92
|
if success:
|
|
93
93
|
logger.info(f"Successfully saved {len(changes)} configuration changes")
|
|
94
|
+
|
|
95
|
+
# Trigger hot reload for all registered services
|
|
96
|
+
self.config_service._notify_reload_callbacks()
|
|
97
|
+
logger.info("Hot reload callbacks triggered")
|
|
98
|
+
|
|
94
99
|
return {
|
|
95
100
|
"success": True,
|
|
96
101
|
"message": f"Saved {len(changes)} configuration changes",
|
|
@@ -311,63 +311,3 @@ class ModalOverlayRenderer:
|
|
|
311
311
|
"has_saved_state": self.saved_state is not None,
|
|
312
312
|
"terminal_size": self.terminal_state.get_size()
|
|
313
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
|
core/ui/modal_renderer.py
CHANGED
|
@@ -16,6 +16,9 @@ from .modal_state_manager import ModalStateManager, ModalLayout, ModalDisplayMod
|
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
|
+
# Maximum modal width to prevent overly wide modals
|
|
20
|
+
MAX_MODAL_WIDTH = 80
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
class ModalRenderer:
|
|
21
24
|
"""Modal overlay renderer using existing visual effects system."""
|
|
@@ -49,6 +52,11 @@ class ModalRenderer:
|
|
|
49
52
|
self.visible_height = 20 # Number of widget lines visible at once
|
|
50
53
|
self._save_confirm_active = False # For save confirmation prompt
|
|
51
54
|
|
|
55
|
+
# Command list selection (for modals with "commands" sections)
|
|
56
|
+
self.command_items: List[Dict] = [] # Flat list of all command items
|
|
57
|
+
self.selected_command_index = 0
|
|
58
|
+
self.has_command_sections = False
|
|
59
|
+
|
|
52
60
|
# Action handling
|
|
53
61
|
self.action_handler = ModalActionHandler(config_service) if config_service else None
|
|
54
62
|
|
|
@@ -62,6 +70,9 @@ class ModalRenderer:
|
|
|
62
70
|
Modal interaction result.
|
|
63
71
|
"""
|
|
64
72
|
try:
|
|
73
|
+
# Reset command selection state for fresh modal
|
|
74
|
+
self._command_selected = False
|
|
75
|
+
|
|
65
76
|
# FIXED: Use overlay system instead of chat pipeline clearing
|
|
66
77
|
# No more clear_active_area() - that only clears display, not buffers
|
|
67
78
|
|
|
@@ -130,9 +141,10 @@ class ModalRenderer:
|
|
|
130
141
|
border_color = ColorPalette.GREY
|
|
131
142
|
title_color = ColorPalette.BRIGHT_WHITE
|
|
132
143
|
footer_color = ColorPalette.GREY
|
|
133
|
-
# Use dynamic terminal width
|
|
144
|
+
# Use dynamic terminal width, capped at MAX_MODAL_WIDTH (80 cols)
|
|
134
145
|
terminal_width = getattr(self.terminal_renderer.terminal_state, 'width', 80) if self.terminal_renderer else 80
|
|
135
|
-
|
|
146
|
+
requested_width = int(ui_config.width or MAX_MODAL_WIDTH)
|
|
147
|
+
width = min(requested_width, terminal_width, MAX_MODAL_WIDTH)
|
|
136
148
|
title = ui_config.title or "Modal"
|
|
137
149
|
|
|
138
150
|
lines = []
|
|
@@ -176,6 +188,9 @@ class ModalRenderer:
|
|
|
176
188
|
Returns:
|
|
177
189
|
List of content lines with rendered widgets.
|
|
178
190
|
"""
|
|
191
|
+
# Store config for scroll calculations (needed for non-selectable items)
|
|
192
|
+
self._last_modal_config = modal_config
|
|
193
|
+
|
|
179
194
|
all_lines = [] # All content lines before pagination
|
|
180
195
|
border_color = ColorPalette.GREY # Modal border color
|
|
181
196
|
|
|
@@ -187,6 +202,12 @@ class ModalRenderer:
|
|
|
187
202
|
self.widgets = self._create_widgets(modal_config)
|
|
188
203
|
if self.widgets:
|
|
189
204
|
self.widgets[0].set_focus(True)
|
|
205
|
+
# Reset command selection for command-style modals
|
|
206
|
+
self.selected_command_index = 0
|
|
207
|
+
|
|
208
|
+
# Always rebuild command_items list (but preserve selected_command_index)
|
|
209
|
+
self.command_items = []
|
|
210
|
+
self.has_command_sections = False
|
|
190
211
|
|
|
191
212
|
# Build all content lines with widget indices
|
|
192
213
|
widget_index = 0
|
|
@@ -220,6 +241,103 @@ class ModalRenderer:
|
|
|
220
241
|
|
|
221
242
|
widget_index += 1
|
|
222
243
|
|
|
244
|
+
# Handle "commands" format (used by help modal, etc.)
|
|
245
|
+
section_commands = section.get("commands", [])
|
|
246
|
+
if section_commands and not section_widgets:
|
|
247
|
+
self.has_command_sections = True
|
|
248
|
+
for cmd_idx, cmd in enumerate(section_commands):
|
|
249
|
+
name = cmd.get("name", "")
|
|
250
|
+
description = cmd.get("description", "")
|
|
251
|
+
is_selectable = cmd.get("selectable", True) # Default to selectable
|
|
252
|
+
|
|
253
|
+
# Truncate name and description BEFORE adding ANSI codes
|
|
254
|
+
# Layout: " > name description" = ~34 chars prefix + description
|
|
255
|
+
max_name_len = 26
|
|
256
|
+
if len(name) > max_name_len:
|
|
257
|
+
name = name[:max_name_len - 3] + "..."
|
|
258
|
+
|
|
259
|
+
max_desc_len = width - 38 # Account for prefix, name padding, and borders
|
|
260
|
+
if len(description) > max_desc_len:
|
|
261
|
+
description = description[:max_desc_len - 3] + "..."
|
|
262
|
+
|
|
263
|
+
if is_selectable:
|
|
264
|
+
# Track command item with its global index (only for selectable items)
|
|
265
|
+
global_cmd_idx = len(self.command_items)
|
|
266
|
+
self.command_items.append(cmd)
|
|
267
|
+
|
|
268
|
+
# Check if this item is selected
|
|
269
|
+
is_selected = (global_cmd_idx == self.selected_command_index)
|
|
270
|
+
|
|
271
|
+
# Format command line with selection indicator
|
|
272
|
+
if is_selected:
|
|
273
|
+
# Highlight selected item with lime color
|
|
274
|
+
cmd_text = f" {ColorPalette.LIME_LIGHT}> {name:<26}{ColorPalette.RESET} {description}"
|
|
275
|
+
else:
|
|
276
|
+
cmd_text = f" {name:<26} {description}"
|
|
277
|
+
widget_line_map.append(-2) # Command entry (use -2 to distinguish from headers)
|
|
278
|
+
else:
|
|
279
|
+
# Non-selectable info item - render dimmed
|
|
280
|
+
cmd_text = f" {ColorPalette.DIM}{name:<26} {description}{ColorPalette.RESET}"
|
|
281
|
+
widget_line_map.append(-4) # Non-selectable info line
|
|
282
|
+
|
|
283
|
+
modal_line = f"│{self._pad_line_with_ansi(cmd_text, width-2)}│"
|
|
284
|
+
all_lines.append(f"{border_color}{modal_line}{ColorPalette.RESET}")
|
|
285
|
+
|
|
286
|
+
# Also handle "sessions" format (used by resume modal, etc.)
|
|
287
|
+
section_sessions = section.get("sessions", [])
|
|
288
|
+
if section_sessions and not section_widgets and not section_commands:
|
|
289
|
+
self.has_command_sections = True
|
|
290
|
+
for sess_idx, sess in enumerate(section_sessions):
|
|
291
|
+
# Track session item with its global index
|
|
292
|
+
global_sess_idx = len(self.command_items)
|
|
293
|
+
# Convert session format to command format for selection handling
|
|
294
|
+
cmd_item = {
|
|
295
|
+
"name": sess.get("title", sess.get("id", "Unknown")),
|
|
296
|
+
"description": sess.get("subtitle", ""),
|
|
297
|
+
"session_id": sess.get("id") or sess.get("metadata", {}).get("session_id", ""),
|
|
298
|
+
"action": sess.get("action", "resume_session"), # Use session's action or default
|
|
299
|
+
"exit_mode": sess.get("exit_mode", "normal"), # How to exit modal before handling
|
|
300
|
+
"metadata": sess.get("metadata", {})
|
|
301
|
+
}
|
|
302
|
+
self.command_items.append(cmd_item)
|
|
303
|
+
|
|
304
|
+
title = sess.get("title", sess.get("id", "Unknown"))
|
|
305
|
+
subtitle = sess.get("subtitle", "")
|
|
306
|
+
|
|
307
|
+
# Check if this item is selected
|
|
308
|
+
is_selected = (global_sess_idx == self.selected_command_index)
|
|
309
|
+
|
|
310
|
+
# Format session line with selection indicator
|
|
311
|
+
# Truncate title BEFORE adding ANSI codes to avoid mid-code truncation
|
|
312
|
+
max_title_len = width - 8 # Account for " > " prefix and borders
|
|
313
|
+
if len(title) > max_title_len:
|
|
314
|
+
title = title[:max_title_len - 3] + "..."
|
|
315
|
+
|
|
316
|
+
if is_selected:
|
|
317
|
+
# Highlight selected item with lime color
|
|
318
|
+
sess_text = f" {ColorPalette.LIME_LIGHT}> {title}{ColorPalette.RESET}"
|
|
319
|
+
else:
|
|
320
|
+
sess_text = f" {title}"
|
|
321
|
+
|
|
322
|
+
modal_line = f"│{self._pad_line_with_ansi(sess_text, width-2)}│"
|
|
323
|
+
all_lines.append(f"{border_color}{modal_line}{ColorPalette.RESET}")
|
|
324
|
+
widget_line_map.append(-2) # Session entry
|
|
325
|
+
|
|
326
|
+
# Add subtitle on a second line if present
|
|
327
|
+
if subtitle:
|
|
328
|
+
# Truncate subtitle BEFORE adding ANSI codes
|
|
329
|
+
max_sub_len = width - 10 # Account for " " prefix and borders
|
|
330
|
+
if len(subtitle) > max_sub_len:
|
|
331
|
+
subtitle = subtitle[:max_sub_len - 3] + "..."
|
|
332
|
+
|
|
333
|
+
if is_selected:
|
|
334
|
+
sub_text = f" {ColorPalette.GREY}{subtitle}{ColorPalette.RESET}"
|
|
335
|
+
else:
|
|
336
|
+
sub_text = f" {ColorPalette.DIM}{subtitle}{ColorPalette.RESET}"
|
|
337
|
+
sub_line = f"│{self._pad_line_with_ansi(sub_text, width-2)}│"
|
|
338
|
+
all_lines.append(f"{border_color}{sub_line}{ColorPalette.RESET}")
|
|
339
|
+
widget_line_map.append(-3) # Subtitle line (non-selectable)
|
|
340
|
+
|
|
223
341
|
# Add blank line after each section (except the last one)
|
|
224
342
|
if section_idx < len(sections) - 1:
|
|
225
343
|
blank_line = f"│{' ' * (width-2)}│"
|
|
@@ -251,10 +369,20 @@ class ModalRenderer:
|
|
|
251
369
|
|
|
252
370
|
# Apply scroll offset and return visible lines
|
|
253
371
|
total_lines = len(all_lines)
|
|
372
|
+
|
|
373
|
+
# Clamp scroll offset to valid range (fixes wrap-around from first to last item)
|
|
374
|
+
max_scroll = max(0, total_lines - self.visible_height)
|
|
375
|
+
self.scroll_offset = max(0, min(self.scroll_offset, max_scroll))
|
|
376
|
+
|
|
254
377
|
end_offset = min(self.scroll_offset + self.visible_height, total_lines)
|
|
255
378
|
visible_lines = all_lines[self.scroll_offset:end_offset]
|
|
256
379
|
|
|
257
|
-
#
|
|
380
|
+
# Pad to fixed height (visible_height) to prevent height changes when scrolling
|
|
381
|
+
while len(visible_lines) < self.visible_height:
|
|
382
|
+
empty_line = f"│{' ' * (width-2)}│"
|
|
383
|
+
visible_lines.append(f"{border_color}{empty_line}{ColorPalette.RESET}")
|
|
384
|
+
|
|
385
|
+
# Add scroll indicator if needed (always at the same position)
|
|
258
386
|
if total_lines > self.visible_height:
|
|
259
387
|
scroll_info = f" [{self.scroll_offset + 1}-{end_offset}/{total_lines}] "
|
|
260
388
|
if self.scroll_offset > 0:
|
|
@@ -263,6 +391,10 @@ class ModalRenderer:
|
|
|
263
391
|
scroll_info = f"{scroll_info}↓"
|
|
264
392
|
indicator_line = f"│{scroll_info.center(width-2)}│"
|
|
265
393
|
visible_lines.append(f"{ColorPalette.DIM}{indicator_line}{ColorPalette.RESET}")
|
|
394
|
+
else:
|
|
395
|
+
# Add empty indicator line to maintain fixed height
|
|
396
|
+
empty_indicator = f"│{' ' * (width-2)}│"
|
|
397
|
+
visible_lines.append(f"{border_color}{empty_indicator}{ColorPalette.RESET}")
|
|
266
398
|
|
|
267
399
|
return visible_lines
|
|
268
400
|
|
|
@@ -291,10 +423,12 @@ class ModalRenderer:
|
|
|
291
423
|
# This completely bypasses write_message() and conversation buffers
|
|
292
424
|
|
|
293
425
|
# Create modal layout configuration
|
|
294
|
-
|
|
295
|
-
|
|
426
|
+
# Use visible width (strip ANSI) for accurate layout calculation
|
|
427
|
+
visible_widths = [len(self._strip_ansi(line)) for line in lines] if lines else [MAX_MODAL_WIDTH]
|
|
428
|
+
content_width = max(visible_widths)
|
|
429
|
+
# Constrain to terminal width and MAX_MODAL_WIDTH (80 cols)
|
|
296
430
|
terminal_width = getattr(self.terminal_renderer.terminal_state, 'width', 80) if self.terminal_renderer else 80
|
|
297
|
-
width = min(content_width
|
|
431
|
+
width = min(content_width, terminal_width - 2, MAX_MODAL_WIDTH) # Cap at 80 cols
|
|
298
432
|
height = len(lines)
|
|
299
433
|
layout = ModalLayout(
|
|
300
434
|
width=width,
|
|
@@ -379,7 +513,8 @@ class ModalRenderer:
|
|
|
379
513
|
logger.error(f"Widget config missing 'type' field: {e}")
|
|
380
514
|
raise ValueError(f"Widget config missing required 'type' field: {config}")
|
|
381
515
|
|
|
382
|
-
|
|
516
|
+
# Support both "config_path" (for config-bound widgets) and "field" (for form modals)
|
|
517
|
+
config_path = config.get("config_path") or config.get("field", "core.ui.unknown")
|
|
383
518
|
|
|
384
519
|
# Get current value from config service if available
|
|
385
520
|
current_value = None
|
|
@@ -440,6 +575,68 @@ class ModalRenderer:
|
|
|
440
575
|
Returns:
|
|
441
576
|
True if navigation was handled.
|
|
442
577
|
"""
|
|
578
|
+
# Handle command-style modal navigation (no widgets, just command items)
|
|
579
|
+
if self.has_command_sections and self.command_items and not self.widgets:
|
|
580
|
+
old_index = self.selected_command_index
|
|
581
|
+
lines_per_item = 2 # Approximate lines per command item
|
|
582
|
+
|
|
583
|
+
# Calculate total content height (including non-selectable items)
|
|
584
|
+
# Use a larger estimate to account for section headers and non-selectable items
|
|
585
|
+
total_content_lines = self._estimate_total_content_lines()
|
|
586
|
+
max_scroll = max(0, total_content_lines - self.visible_height)
|
|
587
|
+
|
|
588
|
+
if key_press.name == "ArrowDown" or key_press.name == "Tab":
|
|
589
|
+
if self.selected_command_index < len(self.command_items) - 1:
|
|
590
|
+
# Move to next selectable item
|
|
591
|
+
self.selected_command_index += 1
|
|
592
|
+
elif self.scroll_offset < max_scroll:
|
|
593
|
+
# At last selectable item but more content below - just scroll
|
|
594
|
+
self.scroll_offset = min(self.scroll_offset + 2, max_scroll)
|
|
595
|
+
return True # Don't change selection, just scrolled
|
|
596
|
+
else:
|
|
597
|
+
# At bottom of content - wrap to top
|
|
598
|
+
self.selected_command_index = 0
|
|
599
|
+
self.scroll_offset = 0
|
|
600
|
+
return True
|
|
601
|
+
elif key_press.name == "ArrowUp":
|
|
602
|
+
if self.selected_command_index > 0:
|
|
603
|
+
# Move to previous selectable item
|
|
604
|
+
self.selected_command_index -= 1
|
|
605
|
+
elif self.scroll_offset > 0:
|
|
606
|
+
# At first selectable item but can scroll up - just scroll
|
|
607
|
+
self.scroll_offset = max(0, self.scroll_offset - 2)
|
|
608
|
+
return True # Don't change selection, just scrolled
|
|
609
|
+
else:
|
|
610
|
+
# At top of content - wrap to bottom
|
|
611
|
+
self.selected_command_index = len(self.command_items) - 1
|
|
612
|
+
self.scroll_offset = max_scroll
|
|
613
|
+
return True
|
|
614
|
+
elif key_press.name == "PageDown":
|
|
615
|
+
self.selected_command_index = min(self.selected_command_index + 10, len(self.command_items) - 1)
|
|
616
|
+
# Also scroll to bottom if at last item
|
|
617
|
+
if self.selected_command_index == len(self.command_items) - 1:
|
|
618
|
+
self.scroll_offset = max_scroll
|
|
619
|
+
elif key_press.name == "PageUp":
|
|
620
|
+
self.selected_command_index = max(self.selected_command_index - 10, 0)
|
|
621
|
+
# Also scroll to top if at first item
|
|
622
|
+
if self.selected_command_index == 0:
|
|
623
|
+
self.scroll_offset = 0
|
|
624
|
+
else:
|
|
625
|
+
return False
|
|
626
|
+
|
|
627
|
+
# Update scroll offset to keep selection visible
|
|
628
|
+
selected_line = self.selected_command_index * lines_per_item
|
|
629
|
+
|
|
630
|
+
# Scroll down if selection is below visible area
|
|
631
|
+
if selected_line >= self.scroll_offset + self.visible_height - 2:
|
|
632
|
+
self.scroll_offset = max(0, selected_line - self.visible_height + 4)
|
|
633
|
+
|
|
634
|
+
# Scroll up if selection is above visible area
|
|
635
|
+
if selected_line < self.scroll_offset:
|
|
636
|
+
self.scroll_offset = max(0, selected_line - 2)
|
|
637
|
+
|
|
638
|
+
return True
|
|
639
|
+
|
|
443
640
|
if not self.widgets:
|
|
444
641
|
return False
|
|
445
642
|
|
|
@@ -490,6 +687,17 @@ class ModalRenderer:
|
|
|
490
687
|
Returns:
|
|
491
688
|
True if input was handled by a widget.
|
|
492
689
|
"""
|
|
690
|
+
# Handle Enter key for command-style modals
|
|
691
|
+
logger.info(f"🔧 _handle_widget_input: has_command_sections={self.has_command_sections}, "
|
|
692
|
+
f"command_items={len(self.command_items) if self.command_items else 0}, "
|
|
693
|
+
f"widgets={len(self.widgets) if self.widgets else 0}")
|
|
694
|
+
if self.has_command_sections and self.command_items and not self.widgets:
|
|
695
|
+
if key_press.name == "Enter" or key_press.char == "\r":
|
|
696
|
+
# Mark that a command was selected
|
|
697
|
+
self._command_selected = True
|
|
698
|
+
logger.info(f"🎯 Command selected! _command_selected={self._command_selected}")
|
|
699
|
+
return True
|
|
700
|
+
return False
|
|
493
701
|
|
|
494
702
|
if not self.widgets or self.focused_widget_index >= len(self.widgets):
|
|
495
703
|
return False
|
|
@@ -499,6 +707,25 @@ class ModalRenderer:
|
|
|
499
707
|
result = focused_widget.handle_input(key_press)
|
|
500
708
|
return result
|
|
501
709
|
|
|
710
|
+
def get_selected_command(self) -> Optional[Dict]:
|
|
711
|
+
"""Get the currently selected command item.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
Selected command dict or None.
|
|
715
|
+
"""
|
|
716
|
+
if self.has_command_sections and self.command_items:
|
|
717
|
+
if 0 <= self.selected_command_index < len(self.command_items):
|
|
718
|
+
return self.command_items[self.selected_command_index]
|
|
719
|
+
return None
|
|
720
|
+
|
|
721
|
+
def was_command_selected(self) -> bool:
|
|
722
|
+
"""Check if a command was selected via Enter.
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
True if a command was selected.
|
|
726
|
+
"""
|
|
727
|
+
return getattr(self, '_command_selected', False)
|
|
728
|
+
|
|
502
729
|
def _get_widget_values(self) -> Dict[str, Any]:
|
|
503
730
|
"""Get all widget values for saving.
|
|
504
731
|
|
|
@@ -545,6 +772,40 @@ class ModalRenderer:
|
|
|
545
772
|
"""
|
|
546
773
|
return re.sub(r'\033\[[0-9;]*m', '', text)
|
|
547
774
|
|
|
775
|
+
def _estimate_total_content_lines(self) -> int:
|
|
776
|
+
"""Estimate total content lines including non-selectable items.
|
|
777
|
+
|
|
778
|
+
Used for scroll calculations when there are non-selectable items
|
|
779
|
+
at the end of the modal content.
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
Estimated total number of content lines.
|
|
783
|
+
"""
|
|
784
|
+
if not hasattr(self, '_last_modal_config') or not self._last_modal_config:
|
|
785
|
+
# Fallback: use command items count * 2 (approximate)
|
|
786
|
+
return len(self.command_items) * 2 if self.command_items else 0
|
|
787
|
+
|
|
788
|
+
total_lines = 0
|
|
789
|
+
sections = self._last_modal_config.get("sections", [])
|
|
790
|
+
|
|
791
|
+
for section in sections:
|
|
792
|
+
# Section header
|
|
793
|
+
if section.get("title"):
|
|
794
|
+
total_lines += 1
|
|
795
|
+
|
|
796
|
+
# Count all commands (selectable and non-selectable)
|
|
797
|
+
commands = section.get("commands", [])
|
|
798
|
+
total_lines += len(commands)
|
|
799
|
+
|
|
800
|
+
# Count sessions
|
|
801
|
+
sessions = section.get("sessions", [])
|
|
802
|
+
total_lines += len(sessions)
|
|
803
|
+
|
|
804
|
+
# Blank line between sections
|
|
805
|
+
total_lines += 1
|
|
806
|
+
|
|
807
|
+
return total_lines
|
|
808
|
+
|
|
548
809
|
def _pad_line_with_ansi(self, line: str, target_width: int) -> str:
|
|
549
810
|
"""Pad line to target width, accounting for ANSI escape codes.
|
|
550
811
|
|