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.
Files changed (192) hide show
  1. agents/__init__.py +2 -0
  2. agents/coder/__init__.py +0 -0
  3. agents/coder/agent.json +4 -0
  4. agents/coder/api-integration.md +2150 -0
  5. agents/coder/cli-pretty.md +765 -0
  6. agents/coder/code-review.md +1092 -0
  7. agents/coder/database-design.md +1525 -0
  8. agents/coder/debugging.md +1102 -0
  9. agents/coder/dependency-management.md +1397 -0
  10. agents/coder/git-workflow.md +1099 -0
  11. agents/coder/refactoring.md +1454 -0
  12. agents/coder/security-hardening.md +1732 -0
  13. agents/coder/system_prompt.md +1448 -0
  14. agents/coder/tdd.md +1367 -0
  15. agents/creative-writer/__init__.py +0 -0
  16. agents/creative-writer/agent.json +4 -0
  17. agents/creative-writer/character-development.md +1852 -0
  18. agents/creative-writer/dialogue-craft.md +1122 -0
  19. agents/creative-writer/plot-structure.md +1073 -0
  20. agents/creative-writer/revision-editing.md +1484 -0
  21. agents/creative-writer/system_prompt.md +690 -0
  22. agents/creative-writer/worldbuilding.md +2049 -0
  23. agents/data-analyst/__init__.py +30 -0
  24. agents/data-analyst/agent.json +4 -0
  25. agents/data-analyst/data-visualization.md +992 -0
  26. agents/data-analyst/exploratory-data-analysis.md +1110 -0
  27. agents/data-analyst/pandas-data-manipulation.md +1081 -0
  28. agents/data-analyst/sql-query-optimization.md +881 -0
  29. agents/data-analyst/statistical-analysis.md +1118 -0
  30. agents/data-analyst/system_prompt.md +928 -0
  31. agents/default/__init__.py +0 -0
  32. agents/default/agent.json +4 -0
  33. agents/default/dead-code.md +794 -0
  34. agents/default/explore-agent-system.md +585 -0
  35. agents/default/system_prompt.md +1448 -0
  36. agents/kollabor/__init__.py +0 -0
  37. agents/kollabor/analyze-plugin-lifecycle.md +175 -0
  38. agents/kollabor/analyze-terminal-rendering.md +388 -0
  39. agents/kollabor/code-review.md +1092 -0
  40. agents/kollabor/debug-mcp-integration.md +521 -0
  41. agents/kollabor/debug-plugin-hooks.md +547 -0
  42. agents/kollabor/debugging.md +1102 -0
  43. agents/kollabor/dependency-management.md +1397 -0
  44. agents/kollabor/git-workflow.md +1099 -0
  45. agents/kollabor/inspect-llm-conversation.md +148 -0
  46. agents/kollabor/monitor-event-bus.md +558 -0
  47. agents/kollabor/profile-performance.md +576 -0
  48. agents/kollabor/refactoring.md +1454 -0
  49. agents/kollabor/system_prompt copy.md +1448 -0
  50. agents/kollabor/system_prompt.md +757 -0
  51. agents/kollabor/trace-command-execution.md +178 -0
  52. agents/kollabor/validate-config.md +879 -0
  53. agents/research/__init__.py +0 -0
  54. agents/research/agent.json +4 -0
  55. agents/research/architecture-mapping.md +1099 -0
  56. agents/research/codebase-analysis.md +1077 -0
  57. agents/research/dependency-audit.md +1027 -0
  58. agents/research/performance-profiling.md +1047 -0
  59. agents/research/security-review.md +1359 -0
  60. agents/research/system_prompt.md +492 -0
  61. agents/technical-writer/__init__.py +0 -0
  62. agents/technical-writer/agent.json +4 -0
  63. agents/technical-writer/api-documentation.md +2328 -0
  64. agents/technical-writer/changelog-management.md +1181 -0
  65. agents/technical-writer/readme-writing.md +1360 -0
  66. agents/technical-writer/style-guide.md +1410 -0
  67. agents/technical-writer/system_prompt.md +653 -0
  68. agents/technical-writer/tutorial-creation.md +1448 -0
  69. core/__init__.py +0 -2
  70. core/application.py +343 -88
  71. core/cli.py +229 -10
  72. core/commands/menu_renderer.py +463 -59
  73. core/commands/registry.py +14 -9
  74. core/commands/system_commands.py +2461 -14
  75. core/config/loader.py +151 -37
  76. core/config/service.py +18 -6
  77. core/events/bus.py +29 -9
  78. core/events/executor.py +205 -75
  79. core/events/models.py +27 -8
  80. core/fullscreen/command_integration.py +20 -24
  81. core/fullscreen/components/__init__.py +10 -1
  82. core/fullscreen/components/matrix_components.py +1 -2
  83. core/fullscreen/components/space_shooter_components.py +654 -0
  84. core/fullscreen/plugin.py +5 -0
  85. core/fullscreen/renderer.py +52 -13
  86. core/fullscreen/session.py +52 -15
  87. core/io/__init__.py +29 -5
  88. core/io/buffer_manager.py +6 -1
  89. core/io/config_status_view.py +7 -29
  90. core/io/core_status_views.py +267 -347
  91. core/io/input/__init__.py +25 -0
  92. core/io/input/command_mode_handler.py +711 -0
  93. core/io/input/display_controller.py +128 -0
  94. core/io/input/hook_registrar.py +286 -0
  95. core/io/input/input_loop_manager.py +421 -0
  96. core/io/input/key_press_handler.py +502 -0
  97. core/io/input/modal_controller.py +1011 -0
  98. core/io/input/paste_processor.py +339 -0
  99. core/io/input/status_modal_renderer.py +184 -0
  100. core/io/input_errors.py +5 -1
  101. core/io/input_handler.py +211 -2452
  102. core/io/key_parser.py +7 -0
  103. core/io/layout.py +15 -3
  104. core/io/message_coordinator.py +111 -2
  105. core/io/message_renderer.py +129 -4
  106. core/io/status_renderer.py +147 -607
  107. core/io/terminal_renderer.py +97 -51
  108. core/io/terminal_state.py +21 -4
  109. core/io/visual_effects.py +816 -165
  110. core/llm/agent_manager.py +1063 -0
  111. core/llm/api_adapters/__init__.py +44 -0
  112. core/llm/api_adapters/anthropic_adapter.py +432 -0
  113. core/llm/api_adapters/base.py +241 -0
  114. core/llm/api_adapters/openai_adapter.py +326 -0
  115. core/llm/api_communication_service.py +167 -113
  116. core/llm/conversation_logger.py +322 -16
  117. core/llm/conversation_manager.py +556 -30
  118. core/llm/file_operations_executor.py +84 -32
  119. core/llm/llm_service.py +934 -103
  120. core/llm/mcp_integration.py +541 -57
  121. core/llm/message_display_service.py +135 -18
  122. core/llm/plugin_sdk.py +1 -2
  123. core/llm/profile_manager.py +1183 -0
  124. core/llm/response_parser.py +274 -56
  125. core/llm/response_processor.py +16 -3
  126. core/llm/tool_executor.py +6 -1
  127. core/logging/__init__.py +2 -0
  128. core/logging/setup.py +34 -6
  129. core/models/resume.py +54 -0
  130. core/plugins/__init__.py +4 -2
  131. core/plugins/base.py +127 -0
  132. core/plugins/collector.py +23 -161
  133. core/plugins/discovery.py +37 -3
  134. core/plugins/factory.py +6 -12
  135. core/plugins/registry.py +5 -17
  136. core/ui/config_widgets.py +128 -28
  137. core/ui/live_modal_renderer.py +2 -1
  138. core/ui/modal_actions.py +5 -0
  139. core/ui/modal_overlay_renderer.py +0 -60
  140. core/ui/modal_renderer.py +268 -7
  141. core/ui/modal_state_manager.py +29 -4
  142. core/ui/widgets/base_widget.py +7 -0
  143. core/updates/__init__.py +10 -0
  144. core/updates/version_check_service.py +348 -0
  145. core/updates/version_comparator.py +103 -0
  146. core/utils/config_utils.py +685 -526
  147. core/utils/plugin_utils.py +1 -1
  148. core/utils/session_naming.py +111 -0
  149. fonts/LICENSE +21 -0
  150. fonts/README.md +46 -0
  151. fonts/SymbolsNerdFont-Regular.ttf +0 -0
  152. fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
  153. fonts/__init__.py +44 -0
  154. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
  155. kollabor-0.4.15.dist-info/RECORD +228 -0
  156. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
  157. plugins/agent_orchestrator/__init__.py +39 -0
  158. plugins/agent_orchestrator/activity_monitor.py +181 -0
  159. plugins/agent_orchestrator/file_attacher.py +77 -0
  160. plugins/agent_orchestrator/message_injector.py +135 -0
  161. plugins/agent_orchestrator/models.py +48 -0
  162. plugins/agent_orchestrator/orchestrator.py +403 -0
  163. plugins/agent_orchestrator/plugin.py +976 -0
  164. plugins/agent_orchestrator/xml_parser.py +191 -0
  165. plugins/agent_orchestrator_plugin.py +9 -0
  166. plugins/enhanced_input/box_styles.py +1 -0
  167. plugins/enhanced_input/color_engine.py +19 -4
  168. plugins/enhanced_input/config.py +2 -2
  169. plugins/enhanced_input_plugin.py +61 -11
  170. plugins/fullscreen/__init__.py +6 -2
  171. plugins/fullscreen/example_plugin.py +1035 -222
  172. plugins/fullscreen/setup_wizard_plugin.py +592 -0
  173. plugins/fullscreen/space_shooter_plugin.py +131 -0
  174. plugins/hook_monitoring_plugin.py +436 -78
  175. plugins/query_enhancer_plugin.py +66 -30
  176. plugins/resume_conversation_plugin.py +1494 -0
  177. plugins/save_conversation_plugin.py +98 -32
  178. plugins/system_commands_plugin.py +70 -56
  179. plugins/tmux_plugin.py +154 -78
  180. plugins/workflow_enforcement_plugin.py +94 -92
  181. system_prompt/default.md +952 -886
  182. core/io/input_mode_manager.py +0 -402
  183. core/io/modal_interaction_handler.py +0 -315
  184. core/io/raw_input_processor.py +0 -946
  185. core/storage/__init__.py +0 -5
  186. core/storage/state_manager.py +0 -84
  187. core/ui/widget_integration.py +0 -222
  188. core/utils/key_reader.py +0 -171
  189. kollabor-0.4.9.dist-info/RECORD +0 -128
  190. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
  191. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
  192. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  """Core system commands for Kollabor CLI."""
2
2
 
3
3
  import logging
4
- from typing import Dict, Any, List
4
+ from typing import Dict, Any, List, Optional
5
5
  from datetime import datetime
6
6
 
7
7
  from ..events.models import (
@@ -10,7 +10,11 @@ from ..events.models import (
10
10
  CommandCategory,
11
11
  CommandResult,
12
12
  SlashCommand,
13
- UIConfig
13
+ UIConfig,
14
+ EventType,
15
+ Hook,
16
+ Event,
17
+ SubcommandInfo,
14
18
  )
15
19
 
16
20
  logger = logging.getLogger(__name__)
@@ -23,18 +27,32 @@ class SystemCommandsPlugin:
23
27
  These commands are automatically registered at application startup.
24
28
  """
25
29
 
26
- def __init__(self, command_registry, event_bus, config_manager) -> None:
30
+ def __init__(
31
+ self,
32
+ command_registry,
33
+ event_bus,
34
+ config_manager,
35
+ llm_service=None,
36
+ profile_manager=None,
37
+ agent_manager=None,
38
+ ) -> None:
27
39
  """Initialize system commands plugin.
28
40
 
29
41
  Args:
30
42
  command_registry: Command registry for registration.
31
43
  event_bus: Event bus for system events.
32
44
  config_manager: Configuration manager for system settings.
45
+ llm_service: LLM service for conversation management.
46
+ profile_manager: LLM profile manager.
47
+ agent_manager: Agent/skill manager.
33
48
  """
34
49
  self.name = "system"
35
50
  self.command_registry = command_registry
36
51
  self.event_bus = event_bus
37
52
  self.config_manager = config_manager
53
+ self.llm_service = llm_service
54
+ self.profile_manager = profile_manager
55
+ self.agent_manager = agent_manager
38
56
  self.logger = logger
39
57
 
40
58
  def register_commands(self) -> None:
@@ -106,12 +124,1692 @@ class SystemCommandsPlugin:
106
124
  )
107
125
  self.command_registry.register_command(version_command)
108
126
 
127
+ # Note: /resume command is handled by resume_conversation_plugin.py
109
128
 
129
+ # Register /profile command
130
+ profile_command = CommandDefinition(
131
+ name="profile",
132
+ description="Manage LLM API profiles",
133
+ handler=self.handle_profile,
134
+ plugin_name=self.name,
135
+ category=CommandCategory.SYSTEM,
136
+ mode=CommandMode.STATUS_TAKEOVER,
137
+ aliases=["prof", "llm"],
138
+ icon="[PROF]",
139
+ ui_config=UIConfig(
140
+ type="modal",
141
+ navigation=["↑↓", "Enter", "Esc"],
142
+ height=15,
143
+ title="LLM Profiles",
144
+ footer="↑↓ navigate • Enter select • Esc exit"
145
+ ),
146
+ subcommands=[
147
+ SubcommandInfo("list", "", "Show profile selection modal"),
148
+ SubcommandInfo("set", "<name>", "Switch to specified profile"),
149
+ SubcommandInfo("create", "", "Open create profile form"),
150
+ ]
151
+ )
152
+ self.command_registry.register_command(profile_command)
153
+
154
+ # Register /agent command
155
+ agent_command = CommandDefinition(
156
+ name="agent",
157
+ description="Manage agents and their configurations",
158
+ handler=self.handle_agent,
159
+ plugin_name=self.name,
160
+ category=CommandCategory.AGENT,
161
+ mode=CommandMode.STATUS_TAKEOVER,
162
+ aliases=["ag"],
163
+ icon="[AGENT]",
164
+ ui_config=UIConfig(
165
+ type="modal",
166
+ navigation=["↑↓", "Enter", "Esc"],
167
+ height=15,
168
+ title="Agents",
169
+ footer="↑↓ navigate • Enter select • Esc exit"
170
+ )
171
+ )
172
+ self.command_registry.register_command(agent_command)
173
+
174
+ # Register /skill command
175
+ skill_command = CommandDefinition(
176
+ name="skill",
177
+ description="Load or unload agent skills",
178
+ handler=self.handle_skill,
179
+ plugin_name=self.name,
180
+ category=CommandCategory.AGENT,
181
+ mode=CommandMode.STATUS_TAKEOVER,
182
+ aliases=["sk"],
183
+ icon="[SKILL]",
184
+ ui_config=UIConfig(
185
+ type="modal",
186
+ navigation=["↑↓", "Enter", "Esc"],
187
+ height=15,
188
+ title="Agent Skills",
189
+ footer="↑↓ navigate • Enter select • Esc exit"
190
+ )
191
+ )
192
+ self.command_registry.register_command(skill_command)
193
+
194
+ self.logger.info("System commands registered successfully")
195
+
196
+ except Exception as e:
197
+ self.logger.error(f"Error registering system commands: {e}")
198
+
199
+ async def register_hooks(self) -> None:
200
+ """Register event hooks for modal command handling."""
201
+ try:
202
+ hook = Hook(
203
+ name="system_modal_command",
204
+ plugin_name="system",
205
+ event_type=EventType.MODAL_COMMAND_SELECTED,
206
+ priority=10,
207
+ callback=self._handle_modal_command
208
+ )
209
+ await self.event_bus.register_hook(hook)
210
+ self.logger.info("System modal command hook registered")
211
+ except Exception as e:
212
+ self.logger.error(f"Error registering system hooks: {e}")
213
+
214
+ async def _handle_modal_command(
215
+ self, data: Dict[str, Any], event: Event
216
+ ) -> Dict[str, Any]:
217
+ """Handle modal command selection events for profile/agent/skill.
218
+
219
+ Args:
220
+ data: Event data containing command info.
221
+ event: Event object.
222
+
223
+ Returns:
224
+ Modified data dict with display_messages key.
225
+ """
226
+ command = data.get("command", {})
227
+ action = command.get("action")
228
+
229
+ self.logger.info(f"System modal command received: action={action}")
230
+
231
+ # Handle profile selection
232
+ if action == "select_profile":
233
+ profile_name = command.get("profile_name")
234
+ if profile_name and self.profile_manager:
235
+ if self.profile_manager.set_active_profile(profile_name):
236
+ profile = self.profile_manager.get_active_profile()
237
+ # Update the API service with new profile settings
238
+ if self.llm_service and hasattr(self.llm_service, 'api_service'):
239
+ self.llm_service.api_service.update_from_profile(profile)
240
+ # Reload native tools (profile may have different native_tool_calling setting)
241
+ import asyncio
242
+ asyncio.create_task(self.llm_service._load_native_tools())
243
+ tool_mode = "native" if profile.get_native_tool_calling() else "xml"
244
+ data["display_messages"] = [
245
+ ("system", f"[ok] Switched to profile: {profile_name}\n Model: {profile.model}\n API: {profile.api_url}\n Tool format: {profile.tool_format}\n Tool calling: {tool_mode}", {}),
246
+ ]
247
+ else:
248
+ data["display_messages"] = [
249
+ ("error", f"[err] Profile not found: {profile_name}", {}),
250
+ ]
251
+
252
+ # Handle agent selection
253
+ elif action == "select_agent":
254
+ agent_name = command.get("agent_name")
255
+ if agent_name and self.agent_manager:
256
+ if self.agent_manager.set_active_agent(agent_name):
257
+ # Rebuild system prompt for the new agent
258
+ if self.llm_service:
259
+ self.llm_service.rebuild_system_prompt()
260
+ agent = self.agent_manager.get_active_agent()
261
+ skills = agent.list_skills() if agent else []
262
+ skill_info = f" ({len(skills)} skills)" if skills else ""
263
+ msg = f"[ok] Switched to agent: {agent_name}{skill_info}"
264
+ if agent and agent.profile:
265
+ msg += f"\n Preferred profile: {agent.profile}"
266
+ data["display_messages"] = [("system", msg, {})]
267
+ else:
268
+ data["display_messages"] = [
269
+ ("error", f"[err] Agent not found: {agent_name}", {}),
270
+ ]
271
+
272
+ # Handle agent clear
273
+ elif action == "clear_agent":
274
+ if self.agent_manager:
275
+ self.agent_manager.clear_active_agent()
276
+ # Rebuild system prompt without agent
277
+ if self.llm_service:
278
+ self.llm_service.rebuild_system_prompt()
279
+ data["display_messages"] = [
280
+ ("system", "[ok] Cleared active agent", {}),
281
+ ]
282
+
283
+ # Handle skill load
284
+ elif action == "load_skill":
285
+ skill_name = command.get("skill_name")
286
+ if skill_name and self.agent_manager:
287
+ agent = self.agent_manager.get_active_agent()
288
+ skill = agent.get_skill(skill_name) if agent else None
289
+ if skill and self.agent_manager.load_skill(skill_name):
290
+ # Inject skill content as user message instead of system prompt
291
+ if self.llm_service:
292
+ skill_message = f"## Skill: {skill_name}\n\n{skill.content}"
293
+ self.llm_service._add_conversation_message("user", skill_message)
294
+ data["display_messages"] = [
295
+ ("system", f"[ok] Loaded skill: {skill_name}", {}),
296
+ ]
297
+ # Reopen the skills modal (skip reload since memory is fresh)
298
+ modal_def = self._get_skills_modal_definition(skip_reload=True)
299
+ if modal_def:
300
+ data["show_modal"] = modal_def
301
+ else:
302
+ data["display_messages"] = [
303
+ ("error", f"[err] Skill not found: {skill_name}", {}),
304
+ ]
305
+
306
+ # Handle skill unload
307
+ elif action == "unload_skill":
308
+ skill_name = command.get("skill_name")
309
+ if skill_name and self.agent_manager:
310
+ if self.agent_manager.unload_skill(skill_name):
311
+ # Add message indicating skill was unloaded
312
+ if self.llm_service:
313
+ self.llm_service._add_conversation_message(
314
+ "user",
315
+ f"[Skill '{skill_name}' has been unloaded - please disregard its instructions]"
316
+ )
317
+ data["display_messages"] = [
318
+ ("system", f"[ok] Unloaded skill: {skill_name}", {}),
319
+ ]
320
+ # Reopen the skills modal (skip reload since memory is fresh)
321
+ modal_def = self._get_skills_modal_definition(skip_reload=True)
322
+ if modal_def:
323
+ data["show_modal"] = modal_def
324
+ else:
325
+ data["display_messages"] = [
326
+ ("error", f"[err] Skill not loaded: {skill_name}", {}),
327
+ ]
328
+
329
+ # Handle toggle default skill (project scope)
330
+ elif action == "toggle_default_skill":
331
+ skill_name = command.get("skill_name")
332
+ if skill_name and self.agent_manager:
333
+ success, is_default = self.agent_manager.toggle_default_skill(
334
+ skill_name, scope="project"
335
+ )
336
+ if success:
337
+ status = "added to" if is_default else "removed from"
338
+ data["display_messages"] = [
339
+ ("system", f"[ok] Skill '{skill_name}' {status} project defaults", {}),
340
+ ]
341
+ # Reopen the skills modal
342
+ modal_def = self._get_skills_modal_definition(skip_reload=True)
343
+ if modal_def:
344
+ data["show_modal"] = modal_def
345
+ else:
346
+ data["display_messages"] = [
347
+ ("error", f"[err] Failed to toggle project default for: {skill_name}", {}),
348
+ ]
349
+
350
+ # Handle toggle global default skill
351
+ elif action == "toggle_global_default_skill":
352
+ skill_name = command.get("skill_name")
353
+ if skill_name and self.agent_manager:
354
+ success, is_default = self.agent_manager.toggle_default_skill(
355
+ skill_name, scope="global"
356
+ )
357
+ if success:
358
+ status = "added to" if is_default else "removed from"
359
+ data["display_messages"] = [
360
+ ("system", f"[ok] Skill '{skill_name}' {status} global defaults", {}),
361
+ ]
362
+ # Reopen the skills modal
363
+ modal_def = self._get_skills_modal_definition(skip_reload=True)
364
+ if modal_def:
365
+ data["show_modal"] = modal_def
366
+ else:
367
+ data["display_messages"] = [
368
+ ("error", f"[err] Failed to toggle global default for: {skill_name}", {}),
369
+ ]
370
+
371
+ # Handle create skill - show form modal
372
+ elif action == "create_skill_prompt":
373
+ if self.agent_manager:
374
+ active_agent = self.agent_manager.get_active_agent()
375
+ if active_agent:
376
+ data["show_modal"] = self._get_create_skill_modal_definition(active_agent.name)
377
+ else:
378
+ data["display_messages"] = [
379
+ ("error", "[err] No active agent", {}),
380
+ ]
381
+
382
+ # Handle create skill form submission
383
+ elif action == "create_skill_submit":
384
+ form_data = command.get("form_data", {})
385
+ name = form_data.get("name", "").strip()
386
+ description = form_data.get("description", "").strip()
387
+
388
+ if not name:
389
+ data["display_messages"] = [
390
+ ("error", "[err] Skill name is required", {}),
391
+ ]
392
+ elif not description:
393
+ data["display_messages"] = [
394
+ ("error", "[err] Description is required for AI generation", {}),
395
+ ]
396
+ elif self.agent_manager:
397
+ active_agent = self.agent_manager.get_active_agent()
398
+ if active_agent:
399
+ if self.llm_service:
400
+ # Build the generation prompt and send to LLM
401
+ generation_prompt = self._build_skill_generation_prompt(
402
+ agent_name=active_agent.name,
403
+ skill_name=name,
404
+ description=description,
405
+ )
406
+ # Send to LLM - it will use @@@FILE blocks to generate the file
407
+ await self.llm_service.process_user_input(generation_prompt)
408
+ # Close modal - LLM handles the rest with existing tool infrastructure
409
+ data["close_modal"] = True
410
+ else:
411
+ data["display_messages"] = [
412
+ ("error", "[err] LLM service not available", {}),
413
+ ]
414
+
415
+ # Handle edit skill - show form modal
416
+ elif action == "edit_skill_prompt":
417
+ skill_name = command.get("skill_name")
418
+ if skill_name and self.agent_manager:
419
+ active_agent = self.agent_manager.get_active_agent()
420
+ if active_agent:
421
+ modal_def = self._get_edit_skill_modal_definition(active_agent.name, skill_name)
422
+ if modal_def:
423
+ data["show_modal"] = modal_def
424
+ else:
425
+ data["display_messages"] = [
426
+ ("error", f"[err] Skill not found: {skill_name}", {}),
427
+ ]
428
+ else:
429
+ data["display_messages"] = [
430
+ ("error", "[err] Select a skill to edit", {}),
431
+ ]
432
+
433
+ # Handle edit skill form submission (rename only)
434
+ elif action == "edit_skill_submit":
435
+ form_data = command.get("form_data", {})
436
+ original_name = command.get("edit_skill_name", "")
437
+ new_name = form_data.get("name", "").strip()
438
+
439
+ if not new_name:
440
+ data["display_messages"] = [
441
+ ("error", "[err] Skill name is required", {}),
442
+ ]
443
+ elif self.agent_manager:
444
+ active_agent = self.agent_manager.get_active_agent()
445
+ if active_agent:
446
+ success = self._rename_skill_file(active_agent, original_name, new_name)
447
+ if success:
448
+ self.agent_manager.refresh()
449
+ msg = f"[ok] Updated skill: {new_name}"
450
+ if new_name != original_name:
451
+ msg += f"\n Renamed from: {original_name}"
452
+ data["display_messages"] = [("system", msg, {})]
453
+ modal_def = self._get_skills_modal_definition(skip_reload=True)
454
+ if modal_def:
455
+ data["show_modal"] = modal_def
456
+ else:
457
+ data["display_messages"] = [
458
+ ("error", f"[err] Failed to rename skill", {}),
459
+ ]
460
+
461
+ # Handle delete skill - show confirmation modal
462
+ elif action == "delete_skill_prompt":
463
+ skill_name = command.get("skill_name")
464
+ if skill_name and self.agent_manager:
465
+ active_agent = self.agent_manager.get_active_agent()
466
+ if active_agent:
467
+ modal_def = self._get_delete_skill_confirm_modal(active_agent.name, skill_name)
468
+ if modal_def:
469
+ data["show_modal"] = modal_def
470
+ else:
471
+ data["display_messages"] = [
472
+ ("error", f"[err] Cannot delete skill: {skill_name}", {}),
473
+ ]
474
+ else:
475
+ data["display_messages"] = [
476
+ ("error", "[err] Select a skill to delete", {}),
477
+ ]
478
+
479
+ # Handle delete skill confirmation
480
+ elif action == "delete_skill_confirm":
481
+ skill_name = command.get("skill_name")
482
+ if skill_name and self.agent_manager:
483
+ active_agent = self.agent_manager.get_active_agent()
484
+ if active_agent:
485
+ success = self._delete_skill_file(active_agent, skill_name)
486
+ if success:
487
+ self.agent_manager.refresh()
488
+ data["display_messages"] = [
489
+ ("system", f"[ok] Deleted skill: {skill_name}", {}),
490
+ ]
491
+ modal_def = self._get_skills_modal_definition(skip_reload=True)
492
+ if modal_def:
493
+ data["show_modal"] = modal_def
494
+ else:
495
+ data["display_messages"] = [
496
+ ("error", f"[err] Failed to delete skill: {skill_name}", {}),
497
+ ]
498
+
499
+ # Handle save profile to config (profiles are global-only)
500
+ elif action == "save_profile_to_config":
501
+ if self.profile_manager:
502
+ profile = self.profile_manager.get_active_profile()
503
+ if profile:
504
+ result = self.profile_manager.save_profile_values_to_config(profile)
505
+
506
+ if result.get("global"):
507
+ # Reload profiles from config to pick up saved values
508
+ self.profile_manager.reload()
509
+ data["display_messages"] = [
510
+ ("system", f"[ok] Saved '{profile.name}' profile to global config (~/.kollabor-cli/)", {}),
511
+ ]
512
+ else:
513
+ data["display_messages"] = [
514
+ ("error", f"[err] Failed to save profile '{profile.name}'.", {}),
515
+ ]
516
+ # Reopen the profile modal (skip_reload since we just reloaded above)
517
+ data["show_modal"] = self._get_profiles_modal_definition(skip_reload=True)
518
+ else:
519
+ data["display_messages"] = [
520
+ ("error", "[err] No active profile to save.", {}),
521
+ ]
522
+
523
+ # Handle create profile - show form modal
524
+ elif action == "create_profile_prompt":
525
+ data["show_modal"] = self._get_create_profile_modal_definition()
526
+
527
+ # Handle create profile form submission
528
+ elif action == "create_profile_submit":
529
+ form_data = command.get("form_data", {})
530
+ name = form_data.get("name", "").strip()
531
+ api_url = form_data.get("api_url", "").strip()
532
+ model = form_data.get("model", "").strip()
533
+ api_token = form_data.get("api_token", "").strip() or None
534
+ temperature = float(form_data.get("temperature", 0.7))
535
+ tool_format = form_data.get("tool_format", "openai")
536
+ # Convert dropdown value to bool (native=True, xml=False)
537
+ native_tool_calling = form_data.get("native_tool_calling", "native") == "native"
538
+ description = form_data.get("description", "").strip()
539
+
540
+ if not name or not api_url or not model:
541
+ data["display_messages"] = [
542
+ ("error", "[err] Name, API URL, and Model are required", {}),
543
+ ]
544
+ elif self.profile_manager:
545
+ profile = self.profile_manager.create_profile(
546
+ name=name,
547
+ api_url=api_url,
548
+ model=model,
549
+ api_token=api_token,
550
+ temperature=temperature,
551
+ tool_format=tool_format,
552
+ native_tool_calling=native_tool_calling,
553
+ description=description or f"Created via /profile",
554
+ save_to_config=True
555
+ )
556
+ if profile:
557
+ data["display_messages"] = [
558
+ ("system", f"[ok] Created profile: {name}\n API: {api_url}\n Model: {model}\n Saved to config.json", {}),
559
+ ]
560
+ # Reopen the profile modal so user can see the new profile
561
+ data["show_modal"] = self._get_profiles_modal_definition(skip_reload=True)
562
+ else:
563
+ data["display_messages"] = [
564
+ ("error", f"[err] Failed. Profile '{name}' may already exist.", {}),
565
+ ]
566
+
567
+ # Handle create agent - show form modal
568
+ elif action == "create_agent_prompt":
569
+ data["show_modal"] = self._get_create_agent_modal_definition()
570
+
571
+ # Handle create agent form submission - AI generation
572
+ elif action == "create_agent_submit":
573
+ form_data = command.get("form_data", {})
574
+ name = form_data.get("name", "").strip()
575
+ description = form_data.get("description", "").strip()
576
+ profile = form_data.get("profile", "").strip()
577
+ source = form_data.get("source", "global").strip()
578
+
579
+ if not name:
580
+ data["display_messages"] = [
581
+ ("error", "[err] Agent name is required", {}),
582
+ ]
583
+ elif not description:
584
+ data["display_messages"] = [
585
+ ("error", "[err] Description is required for AI generation", {}),
586
+ ]
587
+ elif self.llm_service:
588
+ # Build the generation prompt and send to LLM
589
+ generation_prompt = self._build_agent_generation_prompt(
590
+ name=name,
591
+ description=description,
592
+ profile=profile if profile and profile != "(none)" else None,
593
+ source=source,
594
+ )
595
+ # Send to LLM - it will use @@@FILE blocks to generate files
596
+ await self.llm_service.process_user_input(generation_prompt)
597
+ # Close modal - LLM handles the rest with existing tool infrastructure
598
+ data["close_modal"] = True
599
+ else:
600
+ data["display_messages"] = [
601
+ ("error", "[err] LLM service not available", {}),
602
+ ]
603
+
604
+ # Handle edit profile - show form modal with profile data
605
+ elif action == "edit_profile_prompt":
606
+ profile_name = command.get("profile_name")
607
+ if profile_name and self.profile_manager:
608
+ modal_def = self._get_edit_profile_modal_definition(profile_name)
609
+ if modal_def:
610
+ data["show_modal"] = modal_def
611
+ else:
612
+ data["display_messages"] = [
613
+ ("error", f"[err] Profile not found: {profile_name}", {}),
614
+ ]
615
+ else:
616
+ data["display_messages"] = [
617
+ ("error", "[err] Select a profile to edit", {}),
618
+ ]
619
+
620
+ # Handle edit profile form submission
621
+ elif action == "edit_profile_submit":
622
+ form_data = command.get("form_data", {})
623
+ original_name = command.get("edit_profile_name", "")
624
+ new_name = form_data.get("name", "").strip()
625
+ api_url = form_data.get("api_url", "").strip()
626
+ model = form_data.get("model", "").strip()
627
+ api_token = form_data.get("api_token", "").strip() or None
628
+ temperature = float(form_data.get("temperature", 0.7))
629
+ tool_format = form_data.get("tool_format", "openai")
630
+ # Convert dropdown value to bool (native=True, xml=False)
631
+ native_tool_calling = form_data.get("native_tool_calling", "native") == "native"
632
+ description = form_data.get("description", "").strip()
633
+
634
+ if not new_name or not api_url or not model:
635
+ data["display_messages"] = [
636
+ ("error", "[err] Name, API URL, and Model are required", {}),
637
+ ]
638
+ elif self.profile_manager:
639
+ success = self.profile_manager.update_profile(
640
+ original_name=original_name,
641
+ new_name=new_name,
642
+ api_url=api_url,
643
+ model=model,
644
+ api_token=api_token,
645
+ temperature=temperature,
646
+ tool_format=tool_format,
647
+ native_tool_calling=native_tool_calling,
648
+ description=description,
649
+ save_to_config=True
650
+ )
651
+ if success:
652
+ # If this profile is active (check both original and new name), update the API service
653
+ is_active = (self.profile_manager.is_active(new_name) or
654
+ self.profile_manager.is_active(original_name))
655
+ if is_active and self.llm_service and hasattr(self.llm_service, 'api_service'):
656
+ profile = self.profile_manager.get_profile(new_name) or self.profile_manager.get_profile(original_name)
657
+ if profile:
658
+ self.llm_service.api_service.update_from_profile(profile)
659
+ # Reload native tools (tool calling mode may have changed)
660
+ import asyncio
661
+ asyncio.create_task(self.llm_service._load_native_tools())
662
+ tool_mode = "native" if native_tool_calling else "xml"
663
+ msg = f"[ok] Updated profile: {new_name}\n API: {api_url}\n Model: {model}\n Tool format: {tool_format}\n Tool calling: {tool_mode}"
664
+ if is_active:
665
+ msg += "\n [reloaded - changes applied]"
666
+ data["display_messages"] = [("system", msg, {})]
667
+ # Reopen the profile modal
668
+ data["show_modal"] = self._get_profiles_modal_definition(skip_reload=True)
669
+ else:
670
+ data["display_messages"] = [
671
+ ("error", "[err] Failed to update profile", {}),
672
+ ]
673
+
674
+ # Handle delete profile prompt - show confirmation modal
675
+ elif action == "delete_profile_prompt":
676
+ profile_name = command.get("profile_name")
677
+ if profile_name and self.profile_manager:
678
+ modal_def = self._get_delete_profile_confirm_modal(profile_name)
679
+ if modal_def:
680
+ data["show_modal"] = modal_def
681
+ else:
682
+ data["display_messages"] = [
683
+ ("error", f"[err] Cannot delete profile: {profile_name}", {}),
684
+ ]
685
+ else:
686
+ data["display_messages"] = [
687
+ ("error", "[err] Select a profile to delete", {}),
688
+ ]
689
+
690
+ # Handle delete profile confirmation
691
+ elif action == "delete_profile_confirm":
692
+ profile_name = command.get("profile_name")
693
+ if profile_name and self.profile_manager:
694
+ success = self.profile_manager.delete_profile(profile_name)
695
+ if success:
696
+ data["display_messages"] = [
697
+ ("system", f"[ok] Deleted profile: {profile_name}", {}),
698
+ ]
699
+ # Reopen the profile modal so user can continue managing
700
+ # Skip reload since memory state is already updated
701
+ data["show_modal"] = self._get_profiles_modal_definition(skip_reload=True)
702
+ else:
703
+ data["display_messages"] = [
704
+ ("error", f"[err] Failed to delete profile: {profile_name}", {}),
705
+ ]
706
+
707
+ # Handle delete agent prompt - show confirmation modal
708
+ elif action == "delete_agent_prompt":
709
+ agent_name = command.get("agent_name")
710
+ if agent_name and self.agent_manager:
711
+ modal_def = self._get_delete_agent_confirm_modal(agent_name)
712
+ if modal_def:
713
+ data["show_modal"] = modal_def
714
+ else:
715
+ data["display_messages"] = [
716
+ ("error", f"[err] Cannot delete agent: {agent_name}", {}),
717
+ ]
718
+ else:
719
+ data["display_messages"] = [
720
+ ("error", "[err] Select an agent to delete", {}),
721
+ ]
722
+
723
+ # Handle delete agent confirmation
724
+ elif action == "delete_agent_confirm":
725
+ agent_name = command.get("agent_name")
726
+ if agent_name and self.agent_manager:
727
+ success = self.agent_manager.delete_agent(agent_name)
728
+ if success:
729
+ data["display_messages"] = [
730
+ ("system", f"[ok] Deleted agent: {agent_name}", {}),
731
+ ]
732
+ # Reopen the agents modal so user can continue managing
733
+ # Skip reload since memory state is already updated
734
+ data["show_modal"] = self._get_agents_modal_definition(skip_reload=True)
735
+ else:
736
+ data["display_messages"] = [
737
+ ("error", f"[err] Failed to delete agent: {agent_name}", {}),
738
+ ]
739
+
740
+ # Handle edit agent - show form modal with agent data
741
+ elif action == "edit_agent_prompt":
742
+ agent_name = command.get("agent_name")
743
+ if agent_name and self.agent_manager:
744
+ modal_def = self._get_edit_agent_modal_definition(agent_name)
745
+ if modal_def:
746
+ data["show_modal"] = modal_def
747
+ else:
748
+ data["display_messages"] = [
749
+ ("error", f"[err] Agent not found: {agent_name}", {}),
750
+ ]
751
+ else:
752
+ data["display_messages"] = [
753
+ ("error", "[err] Select an agent to edit", {}),
754
+ ]
755
+
756
+ # Handle toggle project default
757
+ elif action == "toggle_project_default":
758
+ agent_name = command.get("agent_name")
759
+ if agent_name and self.agent_manager:
760
+ from ..utils.config_utils import get_all_default_agents, set_default_agent, clear_default_agent
761
+
762
+ # Check if this agent is already project default
763
+ defaults = get_all_default_agents()
764
+ current_project_default = defaults.get("project")
765
+
766
+ if current_project_default == agent_name:
767
+ # Clear it
768
+ if clear_default_agent("project"):
769
+ data["display_messages"] = [
770
+ ("system", f"[ok] Cleared project default agent", {}),
771
+ ]
772
+ else:
773
+ # Set it
774
+ if set_default_agent(agent_name, "project"):
775
+ data["display_messages"] = [
776
+ ("system", f"[ok] Set '{agent_name}' as project default agent", {}),
777
+ ]
778
+ else:
779
+ data["display_messages"] = [
780
+ ("error", f"[err] Failed to set project default", {}),
781
+ ]
782
+
783
+ # Reopen modal to show updated indicators
784
+ modal_def = self._get_agents_modal_definition(skip_reload=True)
785
+ if modal_def:
786
+ data["show_modal"] = modal_def
787
+
788
+ # Handle toggle global default
789
+ elif action == "toggle_global_default":
790
+ agent_name = command.get("agent_name")
791
+ if agent_name and self.agent_manager:
792
+ from ..utils.config_utils import get_all_default_agents, set_default_agent, clear_default_agent
793
+
794
+ # Check if this agent is already global default
795
+ defaults = get_all_default_agents()
796
+ current_global_default = defaults.get("global")
797
+
798
+ if current_global_default == agent_name:
799
+ # Clear it
800
+ if clear_default_agent("global"):
801
+ data["display_messages"] = [
802
+ ("system", f"[ok] Cleared global default agent", {}),
803
+ ]
804
+ else:
805
+ # Set it
806
+ if set_default_agent(agent_name, "global"):
807
+ data["display_messages"] = [
808
+ ("system", f"[ok] Set '{agent_name}' as global default agent", {}),
809
+ ]
810
+ else:
811
+ data["display_messages"] = [
812
+ ("error", f"[err] Failed to set global default", {}),
813
+ ]
814
+
815
+ # Reopen modal to show updated indicators
816
+ modal_def = self._get_agents_modal_definition(skip_reload=True)
817
+ if modal_def:
818
+ data["show_modal"] = modal_def
819
+
820
+ # Handle edit agent form submission
821
+ elif action == "edit_agent_submit":
822
+ form_data = command.get("form_data", {})
823
+ original_name = command.get("edit_agent_name", "")
824
+ new_name = form_data.get("name", "").strip()
825
+ description = form_data.get("description", "").strip()
826
+ profile = form_data.get("profile", "").strip()
827
+
828
+ if not new_name:
829
+ data["display_messages"] = [
830
+ ("error", "[err] Agent name is required", {}),
831
+ ]
832
+ elif self.agent_manager:
833
+ success = self.agent_manager.update_agent(
834
+ original_name=original_name,
835
+ new_name=new_name,
836
+ description=description,
837
+ profile=profile if profile and profile != "(none)" else None,
838
+ system_prompt=None, # Don't update system_prompt via modal
839
+ )
840
+ if success:
841
+ msg = f"[ok] Updated agent: {new_name}"
842
+ if new_name != original_name:
843
+ msg += f"\n Renamed from: {original_name}"
844
+ if description:
845
+ msg += f"\n Description: {description[:50]}..."
846
+ data["display_messages"] = [("system", msg, {})]
847
+ # Reopen the agents modal
848
+ data["show_modal"] = self._get_agents_modal_definition(skip_reload=True)
849
+ else:
850
+ data["display_messages"] = [
851
+ ("error", f"[err] Failed to update agent", {}),
852
+ ]
853
+
854
+ return data
855
+
856
+ def _get_create_profile_modal_definition(self) -> Dict[str, Any]:
857
+ """Get modal definition for creating a new profile."""
858
+ return {
859
+ "title": "Create New Profile",
860
+ "footer": "Tab: next • Ctrl+S: create • Esc: cancel",
861
+ "width": 82,
862
+ "height": 26,
863
+ "form_action": "create_profile_submit",
864
+ "sections": [
865
+ {
866
+ "title": "Profile Name (required)",
867
+ "widgets": [
868
+ {
869
+ "type": "text_input",
870
+ "label": "Name *",
871
+ "field": "name",
872
+ "placeholder": "my-llm, claude-prod, openai-dev, etc.",
873
+ "help": "Used for env vars: KOLLABOR_{NAME}_TOKEN"
874
+ },
875
+ ]
876
+ },
877
+ {
878
+ "title": "Connection (required)",
879
+ "widgets": [
880
+ {
881
+ "type": "text_input",
882
+ "label": "Endpoint *",
883
+ "field": "api_url",
884
+ "placeholder": "https://api.openai.com/v1/chat/completions",
885
+ "help": "API endpoint URL"
886
+ },
887
+ {
888
+ "type": "dropdown",
889
+ "label": "Provider",
890
+ "field": "tool_format",
891
+ "options": ["openai", "anthropic"],
892
+ "current_value": "openai",
893
+ "help": "API format (most use openai)"
894
+ },
895
+ {
896
+ "type": "dropdown",
897
+ "label": "Tool Calling",
898
+ "field": "native_tool_calling",
899
+ "options": ["native", "xml"],
900
+ "current_value": "native",
901
+ "help": "native=API tools, xml=XML tags only"
902
+ },
903
+ ]
904
+ },
905
+ {
906
+ "title": "Authentication (required)",
907
+ "widgets": [
908
+ {
909
+ "type": "text_input",
910
+ "label": "Token *",
911
+ "field": "api_token",
912
+ "placeholder": "sk-... or set env var KOLLABOR_{NAME}_TOKEN",
913
+ "password": True,
914
+ "help": "API key (or leave empty and set env var)"
915
+ },
916
+ ]
917
+ },
918
+ {
919
+ "title": "Model (required)",
920
+ "widgets": [
921
+ {
922
+ "type": "text_input",
923
+ "label": "Model *",
924
+ "field": "model",
925
+ "placeholder": "gpt-4-turbo, claude-sonnet-4-20250514, qwen/qwen3-4b",
926
+ "help": "Model identifier"
927
+ },
928
+ ]
929
+ },
930
+ {
931
+ "title": "Advanced (optional)",
932
+ "widgets": [
933
+ {
934
+ "type": "slider",
935
+ "label": "Temperature",
936
+ "field": "temperature",
937
+ "min_value": 0.0,
938
+ "max_value": 2.0,
939
+ "step": 0.1,
940
+ "current_value": 0.7,
941
+ "help": "0.0 = precise, 2.0 = creative"
942
+ },
943
+ {
944
+ "type": "text_input",
945
+ "label": "Description",
946
+ "field": "description",
947
+ "placeholder": "Optional description"
948
+ },
949
+ ]
950
+ }
951
+ ],
952
+ "actions": [
953
+ {"key": "Ctrl+S", "label": "[ Create ]", "action": "submit", "style": "primary"},
954
+ {"key": "Escape", "label": "[ Cancel ]", "action": "cancel", "style": "secondary"}
955
+ ]
956
+ }
957
+
958
+ def _get_edit_profile_modal_definition(self, profile_name: str) -> Dict[str, Any]:
959
+ """Get modal definition for editing an existing profile.
960
+
961
+ Args:
962
+ profile_name: Name of the profile to edit.
963
+
964
+ Returns:
965
+ Modal definition dict with pre-populated values.
966
+ """
967
+ if not self.profile_manager:
968
+ return {}
969
+
970
+ profile = self.profile_manager.get_profile(profile_name)
971
+ if not profile:
972
+ return {}
973
+
974
+ # Get env var hints for this profile
975
+ env_hints = profile.get_env_var_hints()
976
+
977
+ # Determine token status
978
+ token_from_env = env_hints['token'].is_set
979
+ token_in_config = bool(profile.api_token)
980
+ if token_from_env:
981
+ token_status = f"(using env: {env_hints['token'].name})"
982
+ token_placeholder = "Leave empty to use env var"
983
+ elif token_in_config:
984
+ token_status = "(set in config)"
985
+ token_placeholder = ""
986
+ else:
987
+ token_status = "[REQUIRED - not set]"
988
+ token_placeholder = "Enter API key or set env var"
989
+
990
+ # Determine overall status
991
+ issues = []
992
+ if not profile.api_url:
993
+ issues.append("endpoint missing")
994
+ if not profile.model:
995
+ issues.append("model missing")
996
+ if not token_from_env and not token_in_config:
997
+ issues.append("token missing")
998
+
999
+ if issues:
1000
+ status_line = f"[!] Fix {len(issues)} issue(s): {', '.join(issues)}"
1001
+ else:
1002
+ status_line = "[ok] Ready to use"
1003
+
1004
+ return {
1005
+ "title": f"Edit Profile: {profile_name}",
1006
+ "footer": "Tab: next • Ctrl+S: save • Ctrl+T: test • Esc: cancel",
1007
+ "width": 82,
1008
+ "height": 26,
1009
+ "form_action": "edit_profile_submit",
1010
+ "edit_profile_name": profile_name,
1011
+ "sections": [
1012
+ {
1013
+ "title": "Connection (required)",
1014
+ "widgets": [
1015
+ {
1016
+ "type": "text_input",
1017
+ "label": "Endpoint *",
1018
+ "field": "api_url",
1019
+ "value": profile.api_url,
1020
+ "placeholder": "https://api.openai.com/v1/chat/completions",
1021
+ "help": "API endpoint URL"
1022
+ },
1023
+ {
1024
+ "type": "dropdown",
1025
+ "label": "Provider",
1026
+ "field": "tool_format",
1027
+ "options": ["openai", "anthropic"],
1028
+ "current_value": profile.tool_format,
1029
+ "help": "API format (most use openai)"
1030
+ },
1031
+ {
1032
+ "type": "dropdown",
1033
+ "label": "Tool Calling",
1034
+ "field": "native_tool_calling",
1035
+ "options": ["native", "xml"],
1036
+ "current_value": "native" if profile.native_tool_calling else "xml",
1037
+ "help": "native=API tools, xml=XML tags only"
1038
+ },
1039
+ ]
1040
+ },
1041
+ {
1042
+ "title": "Authentication (required)",
1043
+ "widgets": [
1044
+ {
1045
+ "type": "text_input",
1046
+ "label": f"Token * {token_status}",
1047
+ "field": "api_token",
1048
+ "value": profile.api_token or "",
1049
+ "placeholder": token_placeholder,
1050
+ "password": True
1051
+ },
1052
+ ]
1053
+ },
1054
+ {
1055
+ "title": "Model (required)",
1056
+ "widgets": [
1057
+ {
1058
+ "type": "text_input",
1059
+ "label": "Model *",
1060
+ "field": "model",
1061
+ "value": profile.model,
1062
+ "placeholder": "gpt-4-turbo, claude-sonnet-4-20250514, etc.",
1063
+ "help": "Model identifier"
1064
+ },
1065
+ ]
1066
+ },
1067
+ {
1068
+ "title": "Advanced (optional)",
1069
+ "widgets": [
1070
+ {
1071
+ "type": "text_input",
1072
+ "label": "Profile Name",
1073
+ "field": "name",
1074
+ "value": profile.name,
1075
+ "placeholder": "my-profile",
1076
+ "help": "Determines env var prefix: KOLLABOR_{NAME}_*"
1077
+ },
1078
+ {
1079
+ "type": "slider",
1080
+ "label": "Temperature",
1081
+ "field": "temperature",
1082
+ "min_value": 0.0,
1083
+ "max_value": 2.0,
1084
+ "step": 0.1,
1085
+ "current_value": profile.temperature,
1086
+ "help": "0.0 = precise, 2.0 = creative"
1087
+ },
1088
+ {
1089
+ "type": "text_input",
1090
+ "label": "Description",
1091
+ "field": "description",
1092
+ "value": profile.description or "",
1093
+ "placeholder": "Optional description"
1094
+ },
1095
+ ]
1096
+ },
1097
+ {
1098
+ "title": f"Status: {status_line}",
1099
+ "widgets": [
1100
+ {
1101
+ "type": "label",
1102
+ "label": "Env vars",
1103
+ "value": f"{env_hints['token'].name}={'[set]' if token_from_env else '[not set]'}"
1104
+ },
1105
+ ]
1106
+ }
1107
+ ],
1108
+ "actions": [
1109
+ {"key": "Ctrl+S", "label": "[ Save ]", "action": "submit", "style": "primary"},
1110
+ {"key": "Ctrl+T", "label": "[ Test ]", "action": "test_connection", "style": "secondary"},
1111
+ {"key": "Escape", "label": "[ Cancel ]", "action": "cancel", "style": "secondary"}
1112
+ ]
1113
+ }
1114
+
1115
+ def _get_delete_profile_confirm_modal(self, profile_name: str) -> Dict[str, Any]:
1116
+ """Get modal definition for delete profile confirmation.
1117
+
1118
+ Args:
1119
+ profile_name: Name of the profile to delete.
1120
+
1121
+ Returns:
1122
+ Modal definition dict for confirmation, or empty dict if cannot delete.
1123
+ """
1124
+ if not self.profile_manager:
1125
+ return {}
1126
+
1127
+ profile = self.profile_manager.get_profile(profile_name)
1128
+ if not profile:
1129
+ return {}
1130
+
1131
+ # Check if profile can be deleted
1132
+ if profile_name in self.profile_manager.DEFAULT_PROFILES:
1133
+ # Cannot delete built-in profiles
1134
+ return {}
1135
+
1136
+ if self.profile_manager.is_active(profile_name):
1137
+ # Cannot delete active profile - but we can show a warning
1138
+ pass
1139
+
1140
+ is_active = self.profile_manager.is_active(profile_name)
1141
+ warning_msg = ""
1142
+ if is_active:
1143
+ warning_msg = "\n\n[!] This is the currently active profile.\n You must switch to another profile first."
1144
+ can_delete = False
1145
+ else:
1146
+ can_delete = True
1147
+
1148
+ return {
1149
+ "title": f"Delete Profile: {profile_name}?",
1150
+ "footer": "Enter confirm • Esc cancel",
1151
+ "width": 60,
1152
+ "height": 12,
1153
+ "sections": [
1154
+ {
1155
+ "title": "Confirm Deletion",
1156
+ "commands": [
1157
+ {
1158
+ "name": f"Delete '{profile_name}'",
1159
+ "description": f"Model: {profile.model} @ {profile.api_url}{warning_msg}",
1160
+ "profile_name": profile_name,
1161
+ "action": "delete_profile_confirm" if can_delete else "cancel"
1162
+ },
1163
+ {
1164
+ "name": "Cancel",
1165
+ "description": "Keep the profile",
1166
+ "action": "cancel"
1167
+ }
1168
+ ]
1169
+ }
1170
+ ],
1171
+ "actions": [
1172
+ {"key": "Enter", "label": "Confirm", "action": "select"},
1173
+ {"key": "Escape", "label": "Cancel", "action": "cancel"}
1174
+ ]
1175
+ }
1176
+
1177
+ def _get_create_agent_modal_definition(self) -> Dict[str, Any]:
1178
+ """Get modal definition for creating a new agent."""
1179
+ # Get available profiles for dropdown
1180
+ profile_options = ["(none)"]
1181
+ if self.profile_manager:
1182
+ profile_options.extend(self.profile_manager.get_profile_names())
1183
+
1184
+ return {
1185
+ "title": "Create Agent",
1186
+ "footer": "Tab navigate • Enter confirm • Ctrl+S save • Esc cancel",
1187
+ "width": 70,
1188
+ "height": 20,
1189
+ "form_action": "create_agent_submit",
1190
+ "sections": [
1191
+ {
1192
+ "title": "Agent Settings",
1193
+ "widgets": [
1194
+ {
1195
+ "type": "text_input",
1196
+ "label": "Agent Name",
1197
+ "field": "name",
1198
+ "placeholder": "my-agent",
1199
+ "help": "Unique identifier (creates agents/<name>/ directory)"
1200
+ },
1201
+ {
1202
+ "type": "text_input",
1203
+ "label": "Description",
1204
+ "field": "description",
1205
+ "placeholder": "A Python web development specialist...",
1206
+ "help": "Describe what this agent specializes in (AI generates from this)"
1207
+ },
1208
+ {
1209
+ "type": "dropdown",
1210
+ "label": "Source",
1211
+ "field": "source",
1212
+ "options": ["global", "local"],
1213
+ "current_value": "global",
1214
+ "help": "global=~/shared, local=project-specific"
1215
+ },
1216
+ {
1217
+ "type": "dropdown",
1218
+ "label": "Preferred Profile",
1219
+ "field": "profile",
1220
+ "options": profile_options,
1221
+ "current_value": "(none)",
1222
+ "help": "LLM profile to use with this agent"
1223
+ },
1224
+ {
1225
+ "type": "label",
1226
+ "label": "Generation",
1227
+ "value": "AI will generate system prompt and 5-6 skills based on description"
1228
+ },
1229
+ ]
1230
+ }
1231
+ ],
1232
+ "actions": [
1233
+ {"key": "Ctrl+S", "label": "Generate", "action": "submit", "style": "primary"},
1234
+ {"key": "Escape", "label": "Cancel", "action": "cancel", "style": "secondary"}
1235
+ ]
1236
+ }
1237
+
1238
+ def _build_agent_creation_system_prompt(self) -> str:
1239
+ """Build custom system prompt for agent creation background task.
1240
+
1241
+ This prompt focuses the LLM on agent file generation without
1242
+ the usual conversational context.
1243
+
1244
+ Returns:
1245
+ System prompt string for agent creation
1246
+ """
1247
+ return """You are an expert AI agent creator for the Kollabor CLI system.
1248
+
1249
+ Your ONLY job is to generate comprehensive, high-quality agent files when requested.
1250
+
1251
+ # Core Mission
1252
+
1253
+ When asked to create an agent, you will:
1254
+ 1. Read the default agent templates to understand the format
1255
+ 2. Generate a complete agent with system_prompt.md, agent.json, and 5-6 skill files
1256
+ 3. Match or exceed the quality and depth of the default templates
1257
+
1258
+ # Critical Rules
1259
+
1260
+ 1. **Use @@@FILE blocks**: ALL file generation MUST use @@@FILE/@@@END blocks (NOT XML tags)
1261
+ 2. **Be comprehensive**: System prompts should be 500+ lines, skills 300+ lines minimum
1262
+ 3. **Include examples**: Skills should have concrete examples, not abstract guidance
1263
+ 4. **Follow structure**: Match the structure of default agent templates exactly
1264
+ 5. **No shortcuts**: Do not generate placeholder content or "TODO" sections
1265
+
1266
+ # File Generation Format
1267
+
1268
+ ALWAYS use this exact format (one file per block):
1269
+
1270
+ @@@FILE ~/.kollabor-cli/agents/name/system_prompt.md
1271
+ <!-- Agent Name -->
1272
+ # System Prompt Content Here
1273
+ ... (500+ lines of comprehensive content) ...
1274
+ ... can include <create>, <edit>, <read> XML examples safely ...
1275
+ @@@END
1276
+
1277
+ @@@FILE ~/.kollabor-cli/agents/name/agent.json
1278
+ {"description": "...", "profile": null}
1279
+ @@@END
1280
+
1281
+ @@@FILE ~/.kollabor-cli/agents/name/skill1.md
1282
+ <!-- Skill Description -->
1283
+ # Skill Name
1284
+ ... (300+ lines of actionable content) ...
1285
+ @@@END
1286
+
1287
+ [repeat @@@FILE blocks for 5-6 skills total]
1288
+
1289
+ # Quality Standards
1290
+
1291
+ - System prompts: 500+ lines with clear sections, examples, and guidelines
1292
+ - Skills: 300+ lines with phases, examples, and mandatory rules
1293
+ - Content: Real, actionable guidance (not "you should" but "do this:")
1294
+ - Structure: Match default templates exactly
1295
+ - Examples: Include concrete code/command examples where relevant
1296
+
1297
+ You will receive requests to create agents. Respond ONLY with @@@FILE blocks containing all generated files.
1298
+ Do not engage in conversation. Do not ask questions. Just generate the files.
1299
+ """
1300
+
1301
+ def _build_agent_generation_prompt(
1302
+ self, name: str, description: str, profile: Optional[str] = None, source: str = "global"
1303
+ ) -> str:
1304
+ """Build prompt for LLM-powered agent generation.
1305
+
1306
+ Args:
1307
+ name: Agent name (directory name).
1308
+ description: What the agent specializes in.
1309
+ profile: Optional preferred LLM profile.
1310
+ source: Agent source - "global" or "local".
1311
+
1312
+ Returns:
1313
+ Prompt string for the LLM to generate agent files.
1314
+ """
1315
+ profile_value = f'"{profile}"' if profile else "null"
1316
+
1317
+ # Determine the base path for the agent
1318
+ if source == "local":
1319
+ agents_path = ".kollabor-cli/agents"
1320
+ else:
1321
+ agents_path = "~/.kollabor-cli/agents"
1322
+
1323
+ return f'''Create a new agent called "{name}" that specializes in: {description}
1324
+
1325
+ IMPORTANT: First, review the structure of the default agent to understand the format:
1326
+ - Read ~/.kollabor-cli/agents/default/system_prompt.md (the main system prompt template)
1327
+ - Read ~/.kollabor-cli/agents/default/agent.json (the configuration format)
1328
+ - Read ~/.kollabor-cli/agents/default/debugging.md (an example skill file format)
1329
+
1330
+ After reviewing the templates, create the new agent with the SAME level of detail and quality.
1331
+
1332
+ Create these files using <create> tags:
1333
+
1334
+ 1. system_prompt.md - Comprehensive system prompt (500+ lines) following the default template structure:
1335
+ - Header with agent name
1336
+ - Core philosophy and mission
1337
+ - Session context with <trender> tags for dynamic content
1338
+ - Tool execution guidelines
1339
+ - Response patterns and examples
1340
+ - Quality assurance checklist
1341
+ - Error handling guidance
1342
+
1343
+ 2. agent.json - Configuration file:
1344
+ {{"description": "{description}", "profile": {profile_value}}}
1345
+
1346
+ 3. Create 5-6 skill files (.md) relevant to this agent's specialty. Each skill should:
1347
+ - Start with HTML comment description: <!-- Skill name - brief purpose -->
1348
+ - Include PHASE 0: Environment verification
1349
+ - Include multiple phases with detailed guidance
1350
+ - End with Mandatory rules section
1351
+ - Be 500+ lines with comprehensive, actionable content
1352
+
1353
+ CRITICAL: Use @@@FILE/@@@END blocks to generate all files. This protects your content
1354
+ from being parsed as actual tool calls. The format is:
1355
+
1356
+ @@@FILE {agents_path}/{name}/system_prompt.md
1357
+ ... full system prompt content here (500+ lines) ...
1358
+ ... can include XML command examples like <create>, <read>, etc ...
1359
+ @@@END
1360
+
1361
+ @@@FILE {agents_path}/{name}/agent.json
1362
+ {{"description": "{description}", "profile": {profile_value}}}
1363
+ @@@END
1364
+
1365
+ @@@FILE {agents_path}/{name}/skill_name.md
1366
+ ... full skill content here (500+ lines) ...
1367
+ ... can include command examples without breaking ...
1368
+ @@@END
1369
+
1370
+ [repeat @@@FILE blocks for each of the 5-6 skill files]
1371
+
1372
+ Generate ONE file at a time using @@@FILE blocks. Match the quality and depth of the default agent templates.'''
1373
+
1374
+ def _build_skill_generation_prompt(
1375
+ self, agent_name: str, skill_name: str, description: str
1376
+ ) -> str:
1377
+ """Build prompt for LLM-powered skill generation.
1378
+
1379
+ Args:
1380
+ agent_name: Name of the agent this skill belongs to.
1381
+ skill_name: Skill name (filename without .md).
1382
+ description: What the skill helps with.
1383
+
1384
+ Returns:
1385
+ Prompt string for the LLM to generate the skill file.
1386
+ """
1387
+ # Get the agent's actual directory (could be local or global)
1388
+ agent_dir = f".kollabor-cli/agents/{agent_name}" # default fallback
1389
+ if self.agent_manager:
1390
+ agent = self.agent_manager.get_agent(agent_name)
1391
+ if agent:
1392
+ agent_dir = str(agent.directory)
1393
+
1394
+ return f'''Create a new skill called "{skill_name}" for the "{agent_name}" agent.
1395
+
1396
+ The skill should help with: {description}
1397
+
1398
+ IMPORTANT: First, review existing skills to understand the format and style:
1399
+ - Read {agent_dir}/system_prompt.md (to understand the agent's purpose)
1400
+ - Read ~/.kollabor-cli/agents/default/debugging.md (an example skill file format)
1401
+ - Read any existing .md skill files in {agent_dir}/ for format reference
1402
+
1403
+ After reviewing, create a comprehensive skill file that:
1404
+ 1. Starts with HTML comment description: <!-- {skill_name} - {description} -->
1405
+ 2. Has a clear header with skill name
1406
+ 3. Includes PHASE 0: Environment/context verification
1407
+ 4. Has multiple phases with detailed, actionable guidance
1408
+ 5. Includes examples and code snippets where relevant
1409
+ 6. Ends with a "Mandatory Rules" or "Quality Checklist" section
1410
+ 7. Is comprehensive (500+ lines) with real, actionable content
1411
+
1412
+ CRITICAL: Use @@@FILE/@@@END blocks to generate the file. This protects your content
1413
+ from being parsed as actual tool calls. The format is:
1414
+
1415
+ @@@FILE {agent_dir}/{skill_name}.md
1416
+ <!-- {skill_name} - {description} -->
1417
+
1418
+ # {skill_name.replace('-', ' ').title()}
1419
+
1420
+ ... comprehensive skill content here (500+ lines) ...
1421
+ ... include PHASE 0, PHASE 1, etc with detailed guidance ...
1422
+ ... can include XML command examples like <create>, <read>, <terminal>, etc ...
1423
+ ... these examples will be preserved as literal text, not executed ...
1424
+
1425
+ ## Mandatory Rules
1426
+ - Rule 1
1427
+ - Rule 2
1428
+ @@@END
1429
+
1430
+ Generate ONE comprehensive skill file using the @@@FILE block. Match the quality and depth of existing default agent skills.'''
1431
+
1432
+ def _get_delete_agent_confirm_modal(self, agent_name: str) -> Optional[Dict[str, Any]]:
1433
+ """Get modal definition for delete agent confirmation.
1434
+
1435
+ Args:
1436
+ agent_name: Name of the agent to delete.
1437
+
1438
+ Returns:
1439
+ Modal definition dict for confirmation, or empty dict if cannot delete.
1440
+ """
1441
+ if not self.agent_manager:
1442
+ return {}
1443
+
1444
+ agents = self.agent_manager.list_agents()
1445
+ agent = next((a for a in agents if a.name == agent_name), None)
1446
+ if not agent:
1447
+ return {}
1448
+
1449
+ active_agent = self.agent_manager.get_active_agent()
1450
+ active_name = active_agent.name if active_agent else None
1451
+ is_active = agent_name == active_name
1452
+
1453
+ warning_msg = ""
1454
+ if is_active:
1455
+ warning_msg = "\n\n[!] This is the currently active agent.\n You must clear or switch to another agent first."
1456
+ can_delete = False
1457
+ else:
1458
+ can_delete = True
1459
+
1460
+ skills = agent.list_skills()
1461
+ skill_info = f", {len(skills)} skills" if skills else ""
1462
+
1463
+ return {
1464
+ "title": f"Delete Agent: {agent_name}?",
1465
+ "footer": "Enter confirm • Esc cancel",
1466
+ "width": 60,
1467
+ "height": 12,
1468
+ "sections": [
1469
+ {
1470
+ "title": "Confirm Deletion",
1471
+ "commands": [
1472
+ {
1473
+ "name": f"Delete '{agent_name}'",
1474
+ "description": f"{agent.description or 'No description'}{skill_info}{warning_msg}",
1475
+ "agent_name": agent_name,
1476
+ "action": "delete_agent_confirm" if can_delete else "cancel"
1477
+ },
1478
+ {
1479
+ "name": "Cancel",
1480
+ "description": "Keep the agent",
1481
+ "action": "cancel"
1482
+ }
1483
+ ]
1484
+ }
1485
+ ],
1486
+ "actions": [
1487
+ {"key": "Enter", "label": "Confirm", "action": "select"},
1488
+ {"key": "Escape", "label": "Cancel", "action": "cancel"}
1489
+ ]
1490
+ }
1491
+
1492
+ def _get_edit_agent_modal_definition(self, agent_name: str) -> Optional[Dict[str, Any]]:
1493
+ """Get modal definition for editing an existing agent.
1494
+
1495
+ Args:
1496
+ agent_name: Name of the agent to edit.
1497
+
1498
+ Returns:
1499
+ Modal definition dict with pre-populated values, or None if not found.
1500
+ """
1501
+ if not self.agent_manager:
1502
+ return None
1503
+
1504
+ agent = self.agent_manager.get_agent(agent_name)
1505
+ if not agent:
1506
+ return None
1507
+
1508
+ # Get available profiles for dropdown
1509
+ profile_options = ["(none)"]
1510
+ if self.profile_manager:
1511
+ profile_options.extend(self.profile_manager.get_profile_names())
1512
+
1513
+ # Determine current profile value
1514
+ current_profile = agent.profile if agent.profile else "(none)"
1515
+
1516
+ # Read system prompt from file
1517
+ system_prompt = agent.system_prompt
1518
+
1519
+ # Get skill info for display
1520
+ skills = agent.list_skills()
1521
+ skill_info = f", {len(skills)} skills" if skills else ""
1522
+
1523
+ # Determine if agent is protected (cannot be renamed to default name)
1524
+ is_protected = agent_name in self.agent_manager.list_agents() and agent_name == "default"
1525
+
1526
+ # Show short path for system_prompt file
1527
+ short_path = f"agents/{agent_name}/system_prompt.md"
1528
+
1529
+ return {
1530
+ "title": f"Edit Agent: {agent_name}",
1531
+ "footer": "Tab navigate • Ctrl+S save • Esc cancel",
1532
+ "width": 70,
1533
+ "height": 16,
1534
+ "form_action": "edit_agent_submit",
1535
+ "edit_agent_name": agent_name, # Track original name for rename
1536
+ "sections": [
1537
+ {
1538
+ "title": "Agent Settings",
1539
+ "widgets": [
1540
+ {
1541
+ "type": "text_input",
1542
+ "label": "Name",
1543
+ "field": "name",
1544
+ "value": agent.name,
1545
+ "placeholder": "my-agent",
1546
+ "help": "Renames agent directory"
1547
+ },
1548
+ {
1549
+ "type": "text_input",
1550
+ "label": "Desc",
1551
+ "field": "description",
1552
+ "value": agent.description or "",
1553
+ "placeholder": "What this agent does",
1554
+ "help": "Agent description"
1555
+ },
1556
+ {
1557
+ "type": "dropdown",
1558
+ "label": "Profile",
1559
+ "field": "profile",
1560
+ "options": profile_options,
1561
+ "current_value": current_profile,
1562
+ "help": "Preferred LLM profile"
1563
+ },
1564
+ ]
1565
+ },
1566
+ {
1567
+ "title": f"Files{skill_info}",
1568
+ "widgets": [
1569
+ {
1570
+ "type": "label",
1571
+ "label": "Prompt",
1572
+ "value": short_path,
1573
+ "help": "nano or vim to edit"
1574
+ },
1575
+ ]
1576
+ }
1577
+ ],
1578
+ "actions": [
1579
+ {"key": "Ctrl+S", "label": "Save", "action": "submit", "style": "primary"},
1580
+ {"key": "Escape", "label": "Cancel", "action": "cancel", "style": "secondary"}
1581
+ ]
1582
+ }
1583
+
1584
+ def _get_create_skill_modal_definition(self, agent_name: str) -> Dict[str, Any]:
1585
+ """Get modal definition for creating a new skill."""
1586
+ short_path = f"agents/{agent_name}/<name>.md"
1587
+
1588
+ return {
1589
+ "title": f"Create Skill - {agent_name}",
1590
+ "footer": "Ctrl+S: create • Esc: cancel",
1591
+ "width": 70,
1592
+ "height": 18,
1593
+ "form_action": "create_skill_submit",
1594
+ "sections": [
1595
+ {
1596
+ "title": "New Skill",
1597
+ "widgets": [
1598
+ {
1599
+ "type": "text_input",
1600
+ "label": "Name",
1601
+ "field": "name",
1602
+ "placeholder": "my-skill",
1603
+ "help": "Creates <name>.md in agent directory"
1604
+ },
1605
+ {
1606
+ "type": "text_input",
1607
+ "label": "Description",
1608
+ "field": "description",
1609
+ "placeholder": "What this skill helps with...",
1610
+ "help": "AI generates comprehensive skill from this"
1611
+ },
1612
+ ]
1613
+ },
1614
+ {
1615
+ "title": "Info",
1616
+ "widgets": [
1617
+ {
1618
+ "type": "label",
1619
+ "label": "Location",
1620
+ "value": short_path,
1621
+ "help": "AI generates detailed skill content"
1622
+ },
1623
+ ]
1624
+ }
1625
+ ],
1626
+ "actions": [
1627
+ {"key": "Ctrl+S", "label": "Create", "action": "submit", "style": "primary"},
1628
+ {"key": "Escape", "label": "Cancel", "action": "cancel", "style": "secondary"}
1629
+ ]
1630
+ }
1631
+
1632
+ def _get_edit_skill_modal_definition(self, agent_name: str, skill_name: str) -> Optional[Dict[str, Any]]:
1633
+ """Get modal definition for editing an existing skill."""
1634
+ if not self.agent_manager:
1635
+ return None
1636
+
1637
+ active_agent = self.agent_manager.get_active_agent()
1638
+ if not active_agent or active_agent.name != agent_name:
1639
+ return None
1640
+
1641
+ # Find the skill
1642
+ skill = None
1643
+ for s in active_agent.list_skills():
1644
+ if s.name == skill_name:
1645
+ skill = s
1646
+ break
1647
+
1648
+ if not skill:
1649
+ return None
1650
+
1651
+ # Short path for display
1652
+ short_path = f"agents/{agent_name}/{skill_name}.md"
1653
+
1654
+ return {
1655
+ "title": f"Edit Skill: {skill_name}",
1656
+ "footer": "Tab navigate • Ctrl+S save • Esc cancel",
1657
+ "width": 70,
1658
+ "height": 14,
1659
+ "form_action": "edit_skill_submit",
1660
+ "edit_skill_name": skill_name,
1661
+ "sections": [
1662
+ {
1663
+ "title": "Skill Settings",
1664
+ "widgets": [
1665
+ {
1666
+ "type": "text_input",
1667
+ "label": "Name",
1668
+ "field": "name",
1669
+ "value": skill_name,
1670
+ "placeholder": "my-skill",
1671
+ "help": "Rename the skill file"
1672
+ },
1673
+ ]
1674
+ },
1675
+ {
1676
+ "title": "File",
1677
+ "widgets": [
1678
+ {
1679
+ "type": "label",
1680
+ "label": "Path",
1681
+ "value": short_path,
1682
+ "help": "nano or vim to edit"
1683
+ },
1684
+ ]
1685
+ }
1686
+ ],
1687
+ "actions": [
1688
+ {"key": "Ctrl+S", "label": "Save", "action": "submit", "style": "primary"},
1689
+ {"key": "Escape", "label": "Cancel", "action": "cancel", "style": "secondary"}
1690
+ ]
1691
+ }
1692
+
1693
+ def _get_delete_skill_confirm_modal(self, agent_name: str, skill_name: str) -> Optional[Dict[str, Any]]:
1694
+ """Get modal definition for delete skill confirmation."""
1695
+ if not self.agent_manager:
1696
+ return None
1697
+
1698
+ active_agent = self.agent_manager.get_active_agent()
1699
+ if not active_agent or active_agent.name != agent_name:
1700
+ return None
1701
+
1702
+ # Find the skill
1703
+ skill = None
1704
+ for s in active_agent.list_skills():
1705
+ if s.name == skill_name:
1706
+ skill = s
1707
+ break
1708
+
1709
+ if not skill:
1710
+ return None
1711
+
1712
+ is_loaded = skill_name in active_agent.active_skills
1713
+ warning_msg = ""
1714
+ if is_loaded:
1715
+ warning_msg = "\n\n[!] This skill is currently loaded."
1716
+
1717
+ return {
1718
+ "title": f"Delete Skill: {skill_name}?",
1719
+ "footer": "Enter confirm • Esc cancel",
1720
+ "width": 60,
1721
+ "height": 12,
1722
+ "sections": [
1723
+ {
1724
+ "title": "Confirm Deletion",
1725
+ "commands": [
1726
+ {
1727
+ "name": f"Delete '{skill_name}'",
1728
+ "description": f"{skill.description or skill.file_path.name}{warning_msg}",
1729
+ "skill_name": skill_name,
1730
+ "action": "delete_skill_confirm"
1731
+ },
1732
+ {
1733
+ "name": "Cancel",
1734
+ "description": "Keep the skill",
1735
+ "action": "cancel"
1736
+ }
1737
+ ]
1738
+ }
1739
+ ],
1740
+ "actions": [
1741
+ {"key": "Enter", "label": "Confirm", "action": "select"},
1742
+ {"key": "Escape", "label": "Cancel", "action": "cancel"}
1743
+ ]
1744
+ }
1745
+
1746
+ def _create_skill_file(self, agent, name: str, content: str) -> bool:
1747
+ """Create a new skill file in the agent directory."""
1748
+ try:
1749
+ # Sanitize name - remove .md extension if present
1750
+ if name.endswith(".md"):
1751
+ name = name[:-3]
1752
+
1753
+ skill_path = agent.directory / f"{name}.md"
110
1754
 
111
- self.logger.info("System commands registered successfully")
1755
+ # Don't overwrite existing files
1756
+ if skill_path.exists():
1757
+ return False
112
1758
 
113
- except Exception as e:
114
- self.logger.error(f"Error registering system commands: {e}")
1759
+ skill_path.write_text(content, encoding="utf-8")
1760
+ return True
1761
+ except Exception:
1762
+ return False
1763
+
1764
+ def _rename_skill_file(self, agent, original_name: str, new_name: str) -> bool:
1765
+ """Rename a skill file."""
1766
+ try:
1767
+ # Sanitize names
1768
+ if original_name.endswith(".md"):
1769
+ original_name = original_name[:-3]
1770
+ if new_name.endswith(".md"):
1771
+ new_name = new_name[:-3]
1772
+
1773
+ # Same name = no-op success
1774
+ if original_name == new_name:
1775
+ return True
1776
+
1777
+ original_path = agent.directory / f"{original_name}.md"
1778
+ new_path = agent.directory / f"{new_name}.md"
1779
+
1780
+ if not original_path.exists():
1781
+ return False
1782
+
1783
+ # Check new name doesn't exist
1784
+ if new_path.exists():
1785
+ return False
1786
+
1787
+ # Rename file
1788
+ original_path.rename(new_path)
1789
+ return True
1790
+ except Exception:
1791
+ return False
1792
+
1793
+ def _delete_skill_file(self, agent, skill_name: str) -> bool:
1794
+ """Delete a skill file from the agent directory."""
1795
+ try:
1796
+ # Sanitize name
1797
+ if skill_name.endswith(".md"):
1798
+ skill_name = skill_name[:-3]
1799
+
1800
+ # Don't delete system_prompt.md
1801
+ if skill_name == "system_prompt":
1802
+ return False
1803
+
1804
+ skill_path = agent.directory / f"{skill_name}.md"
1805
+
1806
+ if not skill_path.exists():
1807
+ return False
1808
+
1809
+ skill_path.unlink()
1810
+ return True
1811
+ except Exception:
1812
+ return False
115
1813
 
116
1814
  async def handle_help(self, command: SlashCommand) -> CommandResult:
117
1815
  """Handle /help command.
@@ -251,9 +1949,9 @@ class SystemCommandsPlugin:
251
1949
  {
252
1950
  "title": "Services",
253
1951
  "widgets": [
254
- {"type": "label", "label": "Event Bus", "value": "[OK] Active"},
255
- {"type": "label", "label": "Input Handler", "value": "[OK] Running"},
256
- {"type": "label", "label": "Terminal Renderer", "value": "[OK] Active"},
1952
+ {"type": "label", "label": "Event Bus", "value": "[ok] Active"},
1953
+ {"type": "label", "label": "Input Handler", "value": "[ok] Running"},
1954
+ {"type": "label", "label": "Terminal Renderer", "value": "[ok] Active"},
257
1955
  ]
258
1956
  }
259
1957
  ],
@@ -295,7 +1993,756 @@ Platform: {version_info['platform']}"""
295
1993
  display_type="error"
296
1994
  )
297
1995
 
1996
+ async def handle_profile(self, command: SlashCommand) -> CommandResult:
1997
+ """Handle /profile command.
1998
+
1999
+ Args:
2000
+ command: Parsed slash command.
2001
+
2002
+ Returns:
2003
+ Command execution result.
2004
+ """
2005
+ try:
2006
+ if not self.profile_manager:
2007
+ return CommandResult(
2008
+ success=False,
2009
+ message="Profile manager not available",
2010
+ display_type="error"
2011
+ )
2012
+
2013
+ args = command.args or []
2014
+
2015
+ if not args or args[0] in ("list", "ls"):
2016
+ # Show profile selection modal
2017
+ return await self._show_profiles_modal()
2018
+ elif args[0] == "set" and len(args) >= 2:
2019
+ # Switch to profile: /profile set <name>
2020
+ profile_name = args[1]
2021
+ return await self._switch_profile(profile_name)
2022
+ elif args[0] == "create":
2023
+ # Show create profile form: /profile create
2024
+ return await self._show_create_profile_modal()
2025
+ else:
2026
+ # Switch to specified profile (direct command)
2027
+ profile_name = args[0]
2028
+ return await self._switch_profile(profile_name)
2029
+
2030
+ except Exception as e:
2031
+ self.logger.error(f"Error in profile command: {e}")
2032
+ return CommandResult(
2033
+ success=False,
2034
+ message=f"Error managing profiles: {str(e)}",
2035
+ display_type="error"
2036
+ )
2037
+
2038
+ def _get_profiles_modal_definition(self, skip_reload: bool = False) -> Dict[str, Any]:
2039
+ """Get modal definition for profile selection.
2040
+
2041
+ Args:
2042
+ skip_reload: If True, don't reload from config (use current state).
2043
+
2044
+ Returns:
2045
+ Modal definition dictionary.
2046
+ """
2047
+ # Reload profiles from config to pick up any changes
2048
+ # Skip reload when called immediately after delete (memory state is fresher)
2049
+ if not skip_reload:
2050
+ self.profile_manager.reload()
2051
+
2052
+ profiles = self.profile_manager.list_profiles()
2053
+ active_name = self.profile_manager.active_profile_name
2054
+
2055
+ # Build profile list for modal
2056
+ profile_items = []
2057
+ for profile in profiles:
2058
+ is_active = profile.name == active_name
2059
+ # Use getter methods to show resolved values (respects env vars)
2060
+ model = profile.get_model() or "unknown"
2061
+ api_url = profile.get_endpoint() or "unknown"
2062
+ profile_items.append({
2063
+ "name": f"{'[*] ' if is_active else ' '}{profile.name}",
2064
+ "description": f"{model} @ {api_url}",
2065
+ "profile_name": profile.name,
2066
+ "action": "select_profile"
2067
+ })
2068
+
2069
+ # Add management options
2070
+ management_items = [
2071
+ {
2072
+ "name": " [+] Save to Config",
2073
+ "description": "Save current profile settings (from env vars) to config.json",
2074
+ "action": "save_profile_to_config"
2075
+ },
2076
+ {
2077
+ "name": " [+] Create New Profile",
2078
+ "description": "Create a new profile from scratch",
2079
+ "action": "create_profile_prompt"
2080
+ },
2081
+ ]
2082
+
2083
+ # Env var help section (non-selectable info items)
2084
+ # Short label on left (name), env var on right (description)
2085
+ env_help_items = [
2086
+ {
2087
+ "name": "auto-create from env vars",
2088
+ "description": "python main.py --profile NAME --save",
2089
+ "action": "noop",
2090
+ "selectable": False
2091
+ },
2092
+ {
2093
+ "name": "API URL (required)",
2094
+ "description": "KOLLABOR_{NAME}_ENDPOINT",
2095
+ "action": "noop",
2096
+ "selectable": False
2097
+ },
2098
+ {
2099
+ "name": "API key",
2100
+ "description": "KOLLABOR_{NAME}_TOKEN",
2101
+ "action": "noop",
2102
+ "selectable": False
2103
+ },
2104
+ {
2105
+ "name": "model name",
2106
+ "description": "KOLLABOR_{NAME}_MODEL",
2107
+ "action": "noop",
2108
+ "selectable": False
2109
+ },
2110
+ {
2111
+ "name": "tool format",
2112
+ "description": "KOLLABOR_{NAME}_TOOL_FORMAT",
2113
+ "action": "noop",
2114
+ "selectable": False
2115
+ },
2116
+ {
2117
+ "name": "max tokens",
2118
+ "description": "KOLLABOR_{NAME}_MAX_TOKENS",
2119
+ "action": "noop",
2120
+ "selectable": False
2121
+ },
2122
+ {
2123
+ "name": "temperature",
2124
+ "description": "KOLLABOR_{NAME}_TEMPERATURE",
2125
+ "action": "noop",
2126
+ "selectable": False
2127
+ },
2128
+ {
2129
+ "name": "timeout (ms)",
2130
+ "description": "KOLLABOR_{NAME}_TIMEOUT",
2131
+ "action": "noop",
2132
+ "selectable": False
2133
+ },
2134
+ ]
2135
+
2136
+ return {
2137
+ "title": "LLM Profiles",
2138
+ "footer": "↑↓ navigate • Enter select • e edit • d delete • Esc exit",
2139
+ "width": 75,
2140
+ "height": 28,
2141
+ "sections": [
2142
+ {
2143
+ "title": f"Available Profiles (active: {active_name})",
2144
+ "commands": profile_items
2145
+ },
2146
+ {
2147
+ "title": "Management",
2148
+ "commands": management_items
2149
+ },
2150
+ {
2151
+ "title": "Create via Environment Variables",
2152
+ "commands": env_help_items
2153
+ }
2154
+ ],
2155
+ "actions": [
2156
+ {"key": "Enter", "label": "Select", "action": "select"},
2157
+ {"key": "e", "label": "Edit", "action": "edit_profile_prompt"},
2158
+ {"key": "d", "label": "Delete", "action": "delete_profile_prompt"},
2159
+ {"key": "Escape", "label": "Close", "action": "cancel"}
2160
+ ]
2161
+ }
2162
+
2163
+ async def _show_profiles_modal(self) -> CommandResult:
2164
+ """Show profile selection modal.
2165
+
2166
+ Returns:
2167
+ Command result with modal UI.
2168
+ """
2169
+ modal_definition = self._get_profiles_modal_definition()
2170
+
2171
+ return CommandResult(
2172
+ success=True,
2173
+ message="Select a profile",
2174
+ ui_config=UIConfig(
2175
+ type="modal",
2176
+ title=modal_definition["title"],
2177
+ width=modal_definition["width"],
2178
+ height=modal_definition["height"],
2179
+ modal_config=modal_definition
2180
+ ),
2181
+ display_type="modal"
2182
+ )
2183
+
2184
+ async def _show_create_profile_modal(self) -> CommandResult:
2185
+ """Show create profile form modal.
2186
+
2187
+ Returns:
2188
+ Command result with create profile form modal.
2189
+ """
2190
+ modal_definition = self._get_create_profile_modal_definition()
2191
+
2192
+ return CommandResult(
2193
+ success=True,
2194
+ message="Create new profile",
2195
+ ui_config=UIConfig(
2196
+ type="modal",
2197
+ title=modal_definition["title"],
2198
+ width=modal_definition["width"],
2199
+ height=modal_definition["height"],
2200
+ modal_config=modal_definition
2201
+ ),
2202
+ display_type="modal"
2203
+ )
2204
+
2205
+ async def _switch_profile(self, profile_name: str) -> CommandResult:
2206
+ """Switch to a different profile.
2207
+
2208
+ Args:
2209
+ profile_name: Name of profile to switch to.
2210
+
2211
+ Returns:
2212
+ Command result.
2213
+ """
2214
+ if self.profile_manager.set_active_profile(profile_name):
2215
+ profile = self.profile_manager.get_active_profile()
2216
+ # Update the API service with new profile settings
2217
+ if self.llm_service and hasattr(self.llm_service, 'api_service'):
2218
+ self.llm_service.api_service.update_from_profile(profile)
2219
+ return CommandResult(
2220
+ success=True,
2221
+ message=f"Switched to profile: {profile_name}\n API: {profile.api_url}\n Model: {profile.model}",
2222
+ display_type="success"
2223
+ )
2224
+ else:
2225
+ available = ", ".join(self.profile_manager.get_profile_names())
2226
+ return CommandResult(
2227
+ success=False,
2228
+ message=f"Profile not found: {profile_name}\nAvailable: {available}",
2229
+ display_type="error"
2230
+ )
2231
+
2232
+ async def _create_profile(
2233
+ self, name: str, api_url: str, model: str, temperature: float = 0.7
2234
+ ) -> CommandResult:
2235
+ """Create a new profile.
2236
+
2237
+ Args:
2238
+ name: Profile name.
2239
+ api_url: API endpoint URL.
2240
+ model: Model identifier.
2241
+ temperature: Sampling temperature.
2242
+
2243
+ Returns:
2244
+ Command result.
2245
+ """
2246
+ profile = self.profile_manager.create_profile(
2247
+ name=name,
2248
+ api_url=api_url,
2249
+ model=model,
2250
+ temperature=temperature,
2251
+ description=f"Created via /profile create",
2252
+ save_to_config=True
2253
+ )
2254
+ if profile:
2255
+ return CommandResult(
2256
+ success=True,
2257
+ message=f"[ok] Created profile: {name}\n API: {api_url}\n Model: {model}\n Saved to config.json",
2258
+ display_type="success"
2259
+ )
2260
+ else:
2261
+ return CommandResult(
2262
+ success=False,
2263
+ message=f"[err] Failed to create profile. '{name}' may already exist.",
2264
+ display_type="error"
2265
+ )
2266
+
2267
+ async def handle_agent(self, command: SlashCommand) -> CommandResult:
2268
+ """Handle /agent command.
2269
+
2270
+ Args:
2271
+ command: Parsed slash command.
2272
+
2273
+ Returns:
2274
+ Command execution result.
2275
+ """
2276
+ try:
2277
+ if not self.agent_manager:
2278
+ return CommandResult(
2279
+ success=False,
2280
+ message="Agent manager not available",
2281
+ display_type="error"
2282
+ )
2283
+
2284
+ args = command.args or []
2285
+
2286
+ if not args or args[0] in ("list", "ls"):
2287
+ # Show agent selection modal
2288
+ return await self._show_agents_modal()
2289
+ elif args[0] == "clear":
2290
+ # Clear active agent
2291
+ self.agent_manager.clear_active_agent()
2292
+ return CommandResult(
2293
+ success=True,
2294
+ message="Cleared active agent, using default behavior",
2295
+ display_type="success"
2296
+ )
2297
+ else:
2298
+ # Switch to specified agent (direct command)
2299
+ agent_name = args[0]
2300
+ return await self._switch_agent(agent_name)
2301
+
2302
+ except Exception as e:
2303
+ self.logger.error(f"Error in agent command: {e}")
2304
+ return CommandResult(
2305
+ success=False,
2306
+ message=f"Error managing agents: {str(e)}",
2307
+ display_type="error"
2308
+ )
2309
+
2310
+ def _get_agents_modal_definition(self, skip_reload: bool = False) -> Optional[Dict[str, Any]]:
2311
+ """Get modal definition for agent selection with default indicators.
2312
+
2313
+ Args:
2314
+ skip_reload: If True, don't reload from disk (use current state).
2315
+
2316
+ Returns:
2317
+ Modal definition dictionary, or None if no agents found.
2318
+ """
2319
+ from ..utils.config_utils import get_all_default_agents
2320
+
2321
+ # Get all default agents
2322
+ default_agents = get_all_default_agents() # {"project": "coder", "global": "research"}
2323
+ project_default = default_agents.get("project")
2324
+ global_default = default_agents.get("global")
2325
+
2326
+ # Refresh agents from directories to pick up any changes
2327
+ if not skip_reload:
2328
+ self.agent_manager.refresh()
2329
+
2330
+ agents = self.agent_manager.list_agents()
2331
+ active_agent = self.agent_manager.get_active_agent()
2332
+ active_name = active_agent.name if active_agent else None
2333
+
2334
+ if not agents:
2335
+ return None
2336
+
2337
+ # Build agent list with indicators
2338
+ agent_items = []
2339
+ for agent in agents:
2340
+ is_active = agent.name == active_name
2341
+ is_project_default = agent.name == project_default
2342
+ is_global_default = agent.name == global_default
2343
+
2344
+ # Build source indicator (L=local only, G=global only, *=both)
2345
+ if agent.source == "local" and agent.overrides_global:
2346
+ source_char = "*"
2347
+ elif agent.source == "local":
2348
+ source_char = "L"
2349
+ else: # global
2350
+ source_char = "G"
2351
+
2352
+ # Build default indicator
2353
+ default_parts = []
2354
+ if is_project_default:
2355
+ default_parts.append("D")
2356
+ if is_global_default:
2357
+ default_parts.append("g")
2358
+ default_str = "".join(default_parts) if default_parts else " "
2359
+
2360
+ # Format: [active] source default - examples: [*G ] [ L] [ Gd]
2361
+ active_char = "*" if is_active else " "
2362
+ indicator = f"{active_char}{source_char}{default_str}"
2363
+
2364
+ skills = agent.list_skills()
2365
+ skill_count = f" ({len(skills)} skills)" if skills else ""
2366
+ description = agent.description or "No description"
2367
+
2368
+ agent_items.append({
2369
+ "name": f"[{indicator}] {agent.name}{skill_count}",
2370
+ "description": description,
2371
+ "agent_name": agent.name,
2372
+ "action": "select_agent",
2373
+ "is_active": is_active,
2374
+ "is_project_default": is_project_default,
2375
+ "is_global_default": is_global_default
2376
+ })
2377
+
2378
+ # Add clear option
2379
+ agent_items.append({
2380
+ "name": " [Clear Agent]",
2381
+ "description": "Use default system prompt behavior",
2382
+ "agent_name": None,
2383
+ "action": "clear_agent"
2384
+ })
2385
+
2386
+ # Management options
2387
+ management_items = [
2388
+ {
2389
+ "name": " [+] Create New Agent",
2390
+ "description": "Create a new agent with system prompt",
2391
+ "action": "create_agent_prompt"
2392
+ }
2393
+ ]
2394
+
2395
+ return {
2396
+ "title": "Agents",
2397
+ "footer": "L=local G=global *=both | D=proj g=global | ↑↓ Enter",
2398
+ "width": 70,
2399
+ "height": 18,
2400
+ "sections": [
2401
+ {
2402
+ "title": f"Available Agents (active: {active_name or 'none'})",
2403
+ "commands": agent_items
2404
+ },
2405
+ {
2406
+ "title": "Management",
2407
+ "commands": management_items
2408
+ }
2409
+ ],
2410
+ "actions": [
2411
+ {"key": "Enter", "label": "Select", "action": "select"},
2412
+ {"key": "d", "label": "Project Default", "action": "toggle_project_default"},
2413
+ {"key": "g", "label": "Global Default", "action": "toggle_global_default"},
2414
+ {"key": "e", "label": "Edit", "action": "edit_agent_prompt"},
2415
+ {"key": "r", "label": "Delete", "action": "delete_agent_prompt"},
2416
+ {"key": "Escape", "label": "Close", "action": "cancel"}
2417
+ ]
2418
+ }
2419
+
2420
+ async def _show_agents_modal(self) -> CommandResult:
2421
+ """Show agent selection modal.
2422
+
2423
+ Returns:
2424
+ Command result with modal UI.
2425
+ """
2426
+ modal_definition = self._get_agents_modal_definition()
2427
+
2428
+ if not modal_definition:
2429
+ return CommandResult(
2430
+ success=True,
2431
+ message="No agents found.\nCreate agents in .kollabor-cli/agents/<name>/system_prompt.md",
2432
+ display_type="info"
2433
+ )
2434
+
2435
+ return CommandResult(
2436
+ success=True,
2437
+ message="Select an agent",
2438
+ ui_config=UIConfig(
2439
+ type="modal",
2440
+ title=modal_definition["title"],
2441
+ width=modal_definition["width"],
2442
+ height=modal_definition["height"],
2443
+ modal_config=modal_definition
2444
+ ),
2445
+ display_type="modal"
2446
+ )
2447
+
2448
+ async def _switch_agent(self, agent_name: str) -> CommandResult:
2449
+ """Switch to a different agent.
2450
+
2451
+ Args:
2452
+ agent_name: Name of agent to switch to.
2453
+
2454
+ Returns:
2455
+ Command result.
2456
+ """
2457
+ if self.agent_manager.set_active_agent(agent_name):
2458
+ # Rebuild system prompt for the new agent
2459
+ if self.llm_service:
2460
+ self.llm_service.rebuild_system_prompt()
2461
+
2462
+ agent = self.agent_manager.get_active_agent()
2463
+ skills = agent.list_skills()
2464
+ skill_info = f", {len(skills)} skills available" if skills else ""
2465
+
2466
+ # If agent has a preferred profile, mention it
2467
+ profile_info = ""
2468
+ if agent.profile:
2469
+ profile_info = f"\n Preferred profile: {agent.profile}"
2470
+
2471
+ return CommandResult(
2472
+ success=True,
2473
+ message=f"Switched to agent: {agent_name}{skill_info}{profile_info}",
2474
+ display_type="success"
2475
+ )
2476
+ else:
2477
+ available = ", ".join(self.agent_manager.get_agent_names())
2478
+ return CommandResult(
2479
+ success=False,
2480
+ message=f"Agent not found: {agent_name}\nAvailable: {available}",
2481
+ display_type="error"
2482
+ )
2483
+
2484
+ async def handle_skill(self, command: SlashCommand) -> CommandResult:
2485
+ """Handle /skill command.
2486
+
2487
+ Args:
2488
+ command: Parsed slash command.
2489
+
2490
+ Returns:
2491
+ Command execution result.
2492
+ """
2493
+ try:
2494
+ if not self.agent_manager:
2495
+ return CommandResult(
2496
+ success=False,
2497
+ message="Agent manager not available",
2498
+ display_type="error"
2499
+ )
2500
+
2501
+ active_agent = self.agent_manager.get_active_agent()
2502
+ if not active_agent:
2503
+ return CommandResult(
2504
+ success=False,
2505
+ message="No active agent. Use /agent <name> first.",
2506
+ display_type="error"
2507
+ )
2508
+
2509
+ args = command.args or []
2510
+
2511
+ if not args:
2512
+ # Show skill selection modal
2513
+ return await self._show_skills_modal()
2514
+ elif args[0] in ("list", "ls"):
2515
+ # Show skill selection modal
2516
+ return await self._show_skills_modal()
2517
+ elif args[0] == "load" and len(args) > 1:
2518
+ # Load skill
2519
+ skill_name = args[1]
2520
+ return await self._load_skill(skill_name)
2521
+ elif args[0] == "unload" and len(args) > 1:
2522
+ # Unload skill
2523
+ skill_name = args[1]
2524
+ return await self._unload_skill(skill_name)
2525
+ else:
2526
+ # Try to load skill by name directly
2527
+ skill_name = args[0]
2528
+ return await self._load_skill(skill_name)
2529
+
2530
+ except Exception as e:
2531
+ self.logger.error(f"Error in skill command: {e}")
2532
+ return CommandResult(
2533
+ success=False,
2534
+ message=f"Error managing skills: {str(e)}",
2535
+ display_type="error"
2536
+ )
2537
+
2538
+ def _get_skills_modal_definition(self, skip_reload: bool = False) -> Optional[Dict[str, Any]]:
2539
+ """Get modal definition for skill selection.
2540
+
2541
+ Args:
2542
+ skip_reload: If True, don't reload from disk (use current state).
2543
+
2544
+ Returns:
2545
+ Modal definition dictionary, or None if no skills available.
2546
+ """
2547
+ active_agent = self.agent_manager.get_active_agent()
2548
+ if not active_agent:
2549
+ return None
2550
+
2551
+ # Refresh agent from disk to pick up any changes (unless skipped)
2552
+ if not skip_reload:
2553
+ self.agent_manager.refresh()
2554
+ # Re-get active agent in case it was refreshed
2555
+ active_agent = self.agent_manager.get_active_agent()
2556
+ if not active_agent:
2557
+ return None
2558
+
2559
+ skills = active_agent.list_skills()
2560
+ active_skills = active_agent.active_skills
2561
+
2562
+ # Check project and global defaults
2563
+ from pathlib import Path
2564
+ import json
2565
+
2566
+ local_config = (self.agent_manager.local_agents_dir / active_agent.name / "agent.json"
2567
+ if self.agent_manager.local_agents_dir else None)
2568
+ global_config = self.agent_manager.global_agents_dir / active_agent.name / "agent.json"
2569
+
2570
+ project_defaults = set()
2571
+ global_defaults = set()
2572
+
2573
+ if local_config and local_config.exists():
2574
+ try:
2575
+ config_data = json.loads(local_config.read_text(encoding="utf-8"))
2576
+ project_defaults = set(config_data.get("default_skills", []))
2577
+ except Exception:
2578
+ pass
2579
+
2580
+ if global_config.exists():
2581
+ try:
2582
+ config_data = json.loads(global_config.read_text(encoding="utf-8"))
2583
+ global_defaults = set(config_data.get("default_skills", []))
2584
+ except Exception:
2585
+ pass
2586
+
2587
+ if not skills:
2588
+ return None
2589
+
2590
+ # Build skill list for modal
2591
+ skill_items = []
2592
+ for skill in skills:
2593
+ is_loaded = skill.name in active_skills
2594
+ is_proj_default = skill.name in project_defaults
2595
+ is_global_default = skill.name in global_defaults
2596
+
2597
+ # Show markers: [*] loaded, [d] proj default, [g] global default
2598
+ # Examples: [*dg] [*d ] [ g] [ ]
2599
+ loaded_char = "*" if is_loaded else " "
2600
+ proj_char = "d" if is_proj_default else " "
2601
+ global_char = "g" if is_global_default else " "
2602
+ marker = f"[{loaded_char}{proj_char}{global_char}]"
2603
+
2604
+ action = "unload_skill" if is_loaded else "load_skill"
2605
+ description = skill.description or f"Skill file: {skill.file_path.name}"
2606
+
2607
+ skill_items.append({
2608
+ "name": f"{marker} {skill.name}",
2609
+ "description": description,
2610
+ "skill_name": skill.name,
2611
+ "action": action,
2612
+ "loaded": is_loaded,
2613
+ "is_default": is_proj_default or is_global_default
2614
+ })
2615
+
2616
+ loaded_count = len(active_skills)
2617
+ total_count = len(skills)
2618
+ default_count = len(project_defaults | global_defaults)
2619
+
2620
+ # Management options
2621
+ management_items = [
2622
+ {
2623
+ "name": " [+] Create New Skill",
2624
+ "description": "Create a new skill file for this agent",
2625
+ "action": "create_skill_prompt"
2626
+ }
2627
+ ]
2628
+
2629
+ return {
2630
+ "title": f"Skills - {active_agent.name}",
2631
+ "footer": "*=loaded d=proj g=global | ↑↓ Enter | d/g dflt | e r",
2632
+ "width": 70,
2633
+ "height": 18,
2634
+ "sections": [
2635
+ {
2636
+ "title": f"Available Skills ({loaded_count}/{total_count} loaded, {default_count} default)",
2637
+ "commands": skill_items
2638
+ },
2639
+ {
2640
+ "title": "Management",
2641
+ "commands": management_items
2642
+ }
2643
+ ],
2644
+ "actions": [
2645
+ {"key": "Enter", "label": "Toggle", "action": "toggle"},
2646
+ {"key": "d", "label": "Project Default", "action": "toggle_default_skill"},
2647
+ {"key": "g", "label": "Global Default", "action": "toggle_global_default_skill"},
2648
+ {"key": "e", "label": "Edit", "action": "edit_skill_prompt"},
2649
+ {"key": "r", "label": "Delete", "action": "delete_skill_prompt"},
2650
+ {"key": "Escape", "label": "Close", "action": "cancel"}
2651
+ ]
2652
+ }
2653
+
2654
+ async def _show_skills_modal(self) -> CommandResult:
2655
+ """Show skill selection modal for active agent.
2656
+
2657
+ Returns:
2658
+ Command result with modal UI.
2659
+ """
2660
+ active_agent = self.agent_manager.get_active_agent()
2661
+ if not active_agent:
2662
+ return CommandResult(
2663
+ success=False,
2664
+ message="No active agent",
2665
+ display_type="error"
2666
+ )
2667
+
2668
+ modal_definition = self._get_skills_modal_definition()
2669
+ if not modal_definition:
2670
+ return CommandResult(
2671
+ success=True,
2672
+ message=f"Agent '{active_agent.name}' has no skills defined.\nAdd .md files to the agent directory to create skills.",
2673
+ display_type="info"
2674
+ )
2675
+
2676
+ return CommandResult(
2677
+ success=True,
2678
+ message="Select a skill to load/unload",
2679
+ ui_config=UIConfig(
2680
+ type="modal",
2681
+ title=modal_definition["title"],
2682
+ width=modal_definition["width"],
2683
+ height=modal_definition["height"],
2684
+ modal_config=modal_definition
2685
+ ),
2686
+ display_type="modal"
2687
+ )
2688
+
2689
+ async def _load_skill(self, skill_name: str) -> CommandResult:
2690
+ """Load a skill into active agent.
2691
+
2692
+ Args:
2693
+ skill_name: Name of skill to load.
2694
+
2695
+ Returns:
2696
+ Command result.
2697
+ """
2698
+ agent = self.agent_manager.get_active_agent()
2699
+ skill = agent.get_skill(skill_name) if agent else None
2700
+ if skill and self.agent_manager.load_skill(skill_name):
2701
+ # Inject skill content as user message
2702
+ if self.llm_service:
2703
+ skill_message = f"## Skill: {skill_name}\n\n{skill.content}"
2704
+ self.llm_service._add_conversation_message("user", skill_message)
2705
+ return CommandResult(
2706
+ success=True,
2707
+ message=f"Loaded skill: {skill_name}",
2708
+ display_type="success"
2709
+ )
2710
+ else:
2711
+ active_agent = self.agent_manager.get_active_agent()
2712
+ available = ", ".join(s.name for s in active_agent.list_skills()) if active_agent else ""
2713
+ return CommandResult(
2714
+ success=False,
2715
+ message=f"Skill not found: {skill_name}\nAvailable: {available}",
2716
+ display_type="error"
2717
+ )
2718
+
2719
+ async def _unload_skill(self, skill_name: str) -> CommandResult:
2720
+ """Unload a skill from active agent.
2721
+
2722
+ Args:
2723
+ skill_name: Name of skill to unload.
298
2724
 
2725
+ Returns:
2726
+ Command result.
2727
+ """
2728
+ if self.agent_manager.unload_skill(skill_name):
2729
+ # Add message indicating skill was unloaded
2730
+ if self.llm_service:
2731
+ self.llm_service._add_conversation_message(
2732
+ "user",
2733
+ f"[Skill '{skill_name}' has been unloaded - please disregard its instructions]"
2734
+ )
2735
+ return CommandResult(
2736
+ success=True,
2737
+ message=f"Unloaded skill: {skill_name}",
2738
+ display_type="success"
2739
+ )
2740
+ else:
2741
+ return CommandResult(
2742
+ success=False,
2743
+ message=f"Skill not loaded: {skill_name}",
2744
+ display_type="error"
2745
+ )
299
2746
 
300
2747
  async def _show_command_help(self, command_name: str) -> CommandResult:
301
2748
  """Show help for a specific command.
@@ -377,7 +2824,7 @@ Mode: {command_def.mode.value}"""
377
2824
  width=80,
378
2825
  modal_config={
379
2826
  "sections": command_sections,
380
- "footer": "Press Esc to close • Use /help <command> for detailed help",
2827
+ "footer": "Esc/Enter close • /help <command> for details",
381
2828
  "scrollable": True
382
2829
  }
383
2830
  ),
@@ -467,13 +2914,13 @@ class SystemStatusUI:
467
2914
  f"│ Plugins: {stats['plugins']} active │",
468
2915
  f"│ Categories: {stats['categories']} in use │",
469
2916
  "│ │",
470
- "│ Event Bus: [OK] Active │",
471
- "│ Input Handler: [OK] Running │",
472
- "│ Terminal Renderer: [OK] Active │",
2917
+ "│ Event Bus: [ok] Active │",
2918
+ "│ Input Handler: [ok] Running │",
2919
+ "│ Terminal Renderer: [ok] Active │",
473
2920
  "│ │",
474
2921
  "│ Memory Usage: ~ 45MB │",
475
2922
  "│ Uptime: 00:15:32 │",
476
2923
  "│ │",
477
2924
  "╰─────────────────────────────────────────────────────────────╯",
478
2925
  " ↑↓ navigate • Esc exit"
479
- ]
2926
+ ]