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
@@ -0,0 +1,592 @@
1
+ """Setup wizard plugin for first-time user onboarding."""
2
+
3
+ import asyncio
4
+ import os
5
+ import re
6
+ from core.fullscreen import FullScreenPlugin
7
+ from core.fullscreen.plugin import PluginMetadata
8
+ from core.fullscreen.components.drawing import DrawingPrimitives
9
+ from core.fullscreen.components.animation import AnimationFramework
10
+ from core.io.visual_effects import ColorPalette, GradientRenderer
11
+ from core.io.key_parser import KeyPress
12
+
13
+
14
+ class SetupWizardPlugin(FullScreenPlugin):
15
+ """Interactive setup wizard for new users.
16
+
17
+ Single-screen wizard that shows:
18
+ - LLM connection configuration
19
+ - Keyboard shortcuts reference
20
+ - Slash commands reference
21
+ """
22
+
23
+ # Kollabor ASCII banner (same as main app)
24
+ KOLLABOR_LOGO = [
25
+ "╭──────────────────────────────────────────────────╮",
26
+ "│ ▄█─●─●─█▄ █ ▄▀ █▀▀█ █ █ █▀▀█ █▀▀▄ █▀▀█ █▀▀█ │",
27
+ "│ ●──███──● █▀▄ █ █ █ █ █▄▄█ █▀▀▄ █ █ █▄▄▀ │",
28
+ "│ ▀█─●─●─█▀ █ █ █▄▄█ █▄▄ █▄▄ █ █ █▄▄▀ █▄▄█ █ █▄ │",
29
+ "╰──────────────────────────────────────────────────╯",
30
+ ]
31
+
32
+ def __init__(self):
33
+ """Initialize the setup wizard plugin."""
34
+ metadata = PluginMetadata(
35
+ name="setup",
36
+ description="Interactive setup wizard for first-time configuration",
37
+ version="1.0.0",
38
+ author="Kollabor",
39
+ category="config",
40
+ icon="*",
41
+ aliases=["wizard", "onboarding"]
42
+ )
43
+ super().__init__(metadata)
44
+
45
+ # Lower FPS for static form (reduces CPU and eliminates unnecessary redraws)
46
+ self.target_fps = 3.0
47
+
48
+ # Form state - all fields on one screen
49
+ self.fields = ["profile_name", "api_url", "model", "token", "temperature", "tool_format"]
50
+ self.current_field_index = 0
51
+ self.field_values = {
52
+ "profile_name": "local",
53
+ "api_url": "http://localhost:1234",
54
+ "model": "qwen3-4b",
55
+ "token": "", # Can be entered here or via env var
56
+ "temperature": "0.7",
57
+ "tool_format": "openai",
58
+ }
59
+ self.cursor_positions = {field: len(self.field_values.get(field, "")) for field in self.fields}
60
+
61
+ # Tool format options
62
+ self.tool_formats = ["openai", "anthropic"]
63
+ self.tool_format_index = 0
64
+
65
+ # Page state: 0 = form, 1 = tips (shown when height too small)
66
+ self.current_page = 0
67
+
68
+ # Animation
69
+ self.animation_framework = AnimationFramework()
70
+ self.frame_count = 0
71
+
72
+ # Wizard completion flag
73
+ self.completed = False
74
+ self.skipped = False
75
+
76
+ # Config and profile manager references (set during initialize)
77
+ self._config = None
78
+ self._profile_manager = None
79
+
80
+ # State tracking to skip unnecessary redraws
81
+ self._last_render_state = None
82
+
83
+ async def initialize(self, renderer) -> bool:
84
+ """Initialize the setup wizard."""
85
+ if not await super().initialize(renderer):
86
+ return False
87
+
88
+ # Reset render state to force initial render
89
+ self._last_render_state = None
90
+
91
+ # Setup animations
92
+ current_time = asyncio.get_event_loop().time()
93
+ self.demo_animations = {
94
+ 'title_fade': self.animation_framework.fade_in(1.5, current_time),
95
+ 'bounce': self.animation_framework.bounce_in(1.0, current_time + 0.3)
96
+ }
97
+
98
+ return True
99
+
100
+ def set_managers(self, config, profile_manager):
101
+ """Set config and profile managers for saving configuration.
102
+
103
+ Args:
104
+ config: ConfigService instance for setup completion flag
105
+ profile_manager: ProfileManager instance for creating profiles
106
+ """
107
+ self._config = config
108
+ self._profile_manager = profile_manager
109
+
110
+ # Load values from active profile if available
111
+ if profile_manager:
112
+ try:
113
+ profile = profile_manager.get_active_profile()
114
+ if profile:
115
+ self.field_values["profile_name"] = profile.name or "local"
116
+ self.field_values["api_url"] = profile.api_url or "http://localhost:1234"
117
+ self.field_values["model"] = profile.model or "qwen3-4b"
118
+ self.field_values["token"] = profile.api_token or "" # From config
119
+ self.field_values["temperature"] = str(profile.temperature) if profile.temperature else "0.7"
120
+ self.field_values["tool_format"] = profile.tool_format or "openai"
121
+
122
+ # Update cursor positions to end of values
123
+ for field in self.fields:
124
+ self.cursor_positions[field] = len(self.field_values.get(field, ""))
125
+
126
+ # Update tool format index
127
+ if profile.tool_format in self.tool_formats:
128
+ self.tool_format_index = self.tool_formats.index(profile.tool_format)
129
+ except Exception:
130
+ pass # Use defaults if profile loading fails
131
+
132
+ async def render_frame(self, delta_time: float) -> bool:
133
+ """Render the wizard screen."""
134
+ if not self.renderer:
135
+ return False
136
+
137
+ width, height = self.renderer.get_terminal_size()
138
+
139
+ # Build state hash to detect changes
140
+ current_state = (
141
+ self.current_field_index,
142
+ tuple(sorted(self.field_values.items())),
143
+ tuple(sorted(self.cursor_positions.items())),
144
+ self.current_page,
145
+ self.tool_format_index,
146
+ width,
147
+ height
148
+ )
149
+
150
+ # Skip render if nothing changed (static form optimization)
151
+ if current_state == self._last_render_state:
152
+ return True
153
+
154
+ self._last_render_state = current_state
155
+ self.frame_count += 1
156
+
157
+ # Now do the actual render (buffered by session's begin_frame/end_frame)
158
+ self.renderer.clear_screen()
159
+
160
+ # Minimum height check
161
+ if height < 18:
162
+ DrawingPrimitives.draw_text_centered(
163
+ self.renderer, height // 2,
164
+ f"Terminal too small (need 18 rows, have {height})",
165
+ ColorPalette.YELLOW
166
+ )
167
+ return True
168
+
169
+ # Track if we need a tips page
170
+ self._tips_on_separate_page = height < 30
171
+
172
+ if self.current_page == 0:
173
+ self._render_main_screen(width, height)
174
+ else:
175
+ self._render_tips_screen(width, height)
176
+
177
+ return True
178
+
179
+ def _render_main_screen(self, width: int, height: int):
180
+ """Render the single-screen setup wizard."""
181
+ y = 1
182
+
183
+ # Calculate available space for optional sections
184
+ # Required: logo(5) + header(2) + fields(6) + status(2) + footer(1) = 16 lines minimum
185
+ # Optional: separator(2) + shortcuts(6) + commands(7) = 15 lines
186
+ show_shortcuts = height >= 30
187
+ show_commands = height >= 37
188
+
189
+ # --- Logo ---
190
+ for i, line in enumerate(self.KOLLABOR_LOGO):
191
+ gradient_line = GradientRenderer.apply_dim_scheme_gradient(line)
192
+ x = 4
193
+ self.renderer.write_at(x, y + i, gradient_line)
194
+ y += len(self.KOLLABOR_LOGO) + 1
195
+
196
+ # --- Welcome header ---
197
+ self.renderer.write_at(4, y, ">> Welcome to Kollabor!", ColorPalette.WHITE)
198
+ y += 1
199
+ self.renderer.write_at(4, y, "// SETUP LLM CONNECTION", ColorPalette.DIM_GREY)
200
+ y += 1
201
+
202
+ # Generate env var prefix based on profile name
203
+ profile_name = self.field_values.get("profile_name", "local")
204
+ normalized_name = re.sub(r'[^a-zA-Z0-9]', '_', profile_name.strip()).upper()
205
+ env_prefix = f"KOLLABOR_{normalized_name}"
206
+ env_token = f"{env_prefix}_TOKEN"
207
+
208
+ # Get token from env
209
+ token_value = os.environ.get(env_token, "")
210
+
211
+ # --- Form fields (inline style) ---
212
+ label_x = 4
213
+ value_x = 14
214
+
215
+ # Profile
216
+ self._render_inline_field(y, "profile_name", "profile:", label_x, value_x, width)
217
+ y += 1
218
+
219
+ # Endpoint
220
+ self._render_inline_field(y, "api_url", "endpoint:", label_x, value_x, width)
221
+ y += 1
222
+
223
+ # Model
224
+ self._render_inline_field(y, "model", "model:", label_x, value_x, width)
225
+ y += 1
226
+
227
+ # Token (editable, masked display)
228
+ self._render_token_field(y, "token", "token:", label_x, value_x, width, env_token, token_value)
229
+ y += 1
230
+
231
+ # Temperature
232
+ self._render_inline_field(y, "temperature", "temp:", label_x, value_x, width)
233
+ y += 1
234
+
235
+ # Format (checkbox style)
236
+ self._render_format_field(y, label_x, value_x)
237
+ y += 2
238
+
239
+ # --- Status line ---
240
+ form_token = self.field_values.get("token", "")
241
+ has_token = bool(form_token or token_value)
242
+
243
+ issues = []
244
+ if not self.field_values.get("api_url"):
245
+ issues.append("endpoint")
246
+ if not self.field_values.get("model"):
247
+ issues.append("model")
248
+ if not has_token:
249
+ issues.append("token")
250
+
251
+ if issues:
252
+ self.renderer.write_at(4, y, f"STATUS: [!] Missing: {', '.join(issues)}", ColorPalette.YELLOW)
253
+ else:
254
+ self.renderer.write_at(4, y, "STATUS: [ok] Ready to connect", ColorPalette.BRIGHT_GREEN)
255
+ y += 1
256
+
257
+ # --- Optional sections based on height ---
258
+ if show_shortcuts or show_commands:
259
+ # Separator
260
+ separator = "─" * (width - 8)
261
+ self.renderer.write_at(4, y, separator, ColorPalette.DIM_GREY)
262
+ y += 2
263
+
264
+ if show_shortcuts:
265
+ # --- Keyboard Shortcuts ---
266
+ self.renderer.write_at(4, y, "// Keyboard Shortcuts", ColorPalette.DIM_GREY)
267
+ y += 1
268
+
269
+ shortcuts = [
270
+ ("Esc", "Cancel / close modals"),
271
+ ("Enter", "Submit / confirm"),
272
+ ("Up / Down", "Navigate prompt history"),
273
+ ("Ctrl+C", "Exit application"),
274
+ ]
275
+ for key, desc in shortcuts:
276
+ self.renderer.write_at(4, y, key.ljust(12), ColorPalette.WHITE)
277
+ self.renderer.write_at(16, y, desc, ColorPalette.DIM_GREY)
278
+ y += 1
279
+ y += 1
280
+
281
+ if show_commands:
282
+ # --- Slash Commands ---
283
+ self.renderer.write_at(4, y, "// Slash commands", ColorPalette.DIM_GREY)
284
+ y += 1
285
+
286
+ commands = [
287
+ ("/help", "Show all available commands"),
288
+ ("/profile", "Manage LLM API profiles"),
289
+ ("/terminal", "Tmux session management"),
290
+ ("/save", "Save conversation to file"),
291
+ ("/resume", "Resume conversations"),
292
+ ]
293
+ for cmd, desc in commands:
294
+ self.renderer.write_at(4, y, cmd.ljust(12), ColorPalette.BRIGHT_GREEN)
295
+ self.renderer.write_at(16, y, f"- {desc}", ColorPalette.DIM_GREY)
296
+ y += 1
297
+
298
+ # --- Footer navigation ---
299
+ footer_y = height - 1
300
+ if self._tips_on_separate_page:
301
+ self.renderer.write_at(4, footer_y, "Tab: next field | Enter: continue | Esc: cancel", ColorPalette.DIM_GREY)
302
+ else:
303
+ self.renderer.write_at(4, footer_y, "Tab: next field | Enter: save & start | Esc: cancel", ColorPalette.DIM_GREY)
304
+
305
+ def _render_tips_screen(self, width: int, height: int):
306
+ """Render the tips/shortcuts screen."""
307
+ y = 2
308
+
309
+ # Header
310
+ self.renderer.write_at(4, y, "// Keyboard Shortcuts", ColorPalette.CYAN)
311
+ y += 2
312
+
313
+ shortcuts = [
314
+ ("Esc", "Cancel / close modals"),
315
+ ("Enter", "Submit / confirm"),
316
+ ("Up / Down", "Navigate prompt history"),
317
+ ("Ctrl+C", "Exit application"),
318
+ ]
319
+ for key, desc in shortcuts:
320
+ self.renderer.write_at(4, y, key.ljust(14), ColorPalette.WHITE)
321
+ self.renderer.write_at(18, y, desc, ColorPalette.DIM_GREY)
322
+ y += 1
323
+ y += 2
324
+
325
+ # --- Slash Commands ---
326
+ self.renderer.write_at(4, y, "// Slash commands", ColorPalette.CYAN)
327
+ y += 2
328
+
329
+ commands = [
330
+ ("/help", "Show all available commands"),
331
+ ("/profile", "Manage LLM API profiles"),
332
+ ("/terminal", "Tmux session management"),
333
+ ("/save", "Save conversation to file"),
334
+ ("/resume", "Resume conversations"),
335
+ ]
336
+ for cmd, desc in commands:
337
+ self.renderer.write_at(4, y, cmd.ljust(14), ColorPalette.BRIGHT_GREEN)
338
+ self.renderer.write_at(18, y, f"- {desc}", ColorPalette.DIM_GREY)
339
+ y += 1
340
+
341
+ # Footer
342
+ footer_y = height - 1
343
+ self.renderer.write_at(4, footer_y, "Press any key to save & start", ColorPalette.DIM_GREY)
344
+
345
+ def _render_inline_field(self, y: int, field: str, label: str, label_x: int, value_x: int, width: int):
346
+ """Render an inline form field (label: value on same line)."""
347
+ field_index = self.fields.index(field) if field in self.fields else -1
348
+ is_active = field_index == self.current_field_index
349
+
350
+ # Label
351
+ label_color = ColorPalette.BRIGHT_GREEN if is_active else ColorPalette.DIM_GREY
352
+ self.renderer.write_at(label_x, y, label, label_color)
353
+
354
+ # Value with cursor if active
355
+ value = self.field_values.get(field, "")
356
+ cursor_pos = self.cursor_positions.get(field, len(value))
357
+ max_width = width - value_x - 4
358
+
359
+ if is_active:
360
+ before = value[:cursor_pos]
361
+ after = value[cursor_pos:]
362
+ display = before + "_" + after
363
+ value_color = ColorPalette.BRIGHT_YELLOW
364
+ else:
365
+ display = value
366
+ value_color = ColorPalette.WHITE
367
+
368
+ # Truncate if needed
369
+ if len(display) > max_width:
370
+ display = display[:max_width - 3] + "..."
371
+
372
+ self.renderer.write_at(value_x, y, display, value_color)
373
+
374
+ def _render_token_field(self, y: int, field: str, label: str, label_x: int, value_x: int, width: int, env_var: str, env_value: str):
375
+ """Render the token field with masking."""
376
+ field_index = self.fields.index(field) if field in self.fields else -1
377
+ is_active = field_index == self.current_field_index
378
+
379
+ # Label
380
+ label_color = ColorPalette.BRIGHT_GREEN if is_active else ColorPalette.DIM_GREY
381
+ self.renderer.write_at(label_x, y, label, label_color)
382
+
383
+ # Get token value (form value takes precedence, then env var)
384
+ form_value = self.field_values.get(field, "")
385
+
386
+ if is_active:
387
+ # When editing, show masked with cursor
388
+ cursor_pos = self.cursor_positions.get(field, len(form_value))
389
+ if form_value:
390
+ # Show asterisks with cursor position
391
+ masked = "*" * cursor_pos + "_" + "*" * (len(form_value) - cursor_pos)
392
+ else:
393
+ masked = "_"
394
+ self.renderer.write_at(value_x, y, masked, ColorPalette.BRIGHT_YELLOW)
395
+ else:
396
+ # When not editing, show status
397
+ if form_value:
398
+ # Has form value - show masked
399
+ masked = form_value[:3] + "*" * (len(form_value) - 5) + form_value[-2:] if len(form_value) > 8 else "****"
400
+ self.renderer.write_at(value_x, y, masked, ColorPalette.WHITE)
401
+ elif env_value:
402
+ # Has env value - show env var name
403
+ self.renderer.write_at(value_x, y, f"({env_var})", ColorPalette.BRIGHT_GREEN)
404
+ else:
405
+ # Neither - show env var hint
406
+ self.renderer.write_at(value_x, y, env_var, ColorPalette.YELLOW)
407
+
408
+ def _render_format_field(self, y: int, label_x: int, value_x: int):
409
+ """Render the format field as checkboxes."""
410
+ field_index = self.fields.index("tool_format")
411
+ is_active = field_index == self.current_field_index
412
+
413
+ # Label
414
+ label_color = ColorPalette.BRIGHT_GREEN if is_active else ColorPalette.DIM_GREY
415
+ self.renderer.write_at(label_x, y, "format:", label_color)
416
+
417
+ # Checkbox options
418
+ current_format = self.field_values["tool_format"]
419
+ x = value_x
420
+
421
+ for fmt in self.tool_formats:
422
+ if fmt == current_format:
423
+ checkbox = f"[x] {fmt}"
424
+ color = ColorPalette.BRIGHT_YELLOW if is_active else ColorPalette.WHITE
425
+ else:
426
+ checkbox = f"[ ] {fmt}"
427
+ color = ColorPalette.DIM_GREY
428
+
429
+ self.renderer.write_at(x, y, checkbox, color)
430
+ x += len(checkbox) + 2
431
+
432
+ async def handle_input(self, key_press: KeyPress) -> bool:
433
+ """Handle user input."""
434
+ # Tips page - any key saves and exits
435
+ if self.current_page == 1:
436
+ await self._save_configuration()
437
+ self.completed = True
438
+ return True
439
+
440
+ # Escape to skip wizard
441
+ if key_press.name == "Escape":
442
+ self.skipped = True
443
+ return True
444
+
445
+ # Enter - if tips on separate page, show tips first; otherwise save and exit
446
+ if key_press.name == "Enter" or key_press.char == '\n' or key_press.char == '\r':
447
+ if getattr(self, '_tips_on_separate_page', False):
448
+ self.current_page = 1
449
+ return False
450
+ else:
451
+ await self._save_configuration()
452
+ self.completed = True
453
+ return True
454
+
455
+ # Tab navigation between fields
456
+ if key_press.name == "Tab" or key_press.char == '\t':
457
+ self.current_field_index = (self.current_field_index + 1) % len(self.fields)
458
+ return False
459
+
460
+ # Shift+Tab
461
+ if key_press.name == "Shift+Tab":
462
+ self.current_field_index = (self.current_field_index - 1) % len(self.fields)
463
+ return False
464
+
465
+ # Arrow up/down for field navigation
466
+ if key_press.name == "ArrowUp":
467
+ self.current_field_index = (self.current_field_index - 1) % len(self.fields)
468
+ return False
469
+ if key_press.name == "ArrowDown":
470
+ self.current_field_index = (self.current_field_index + 1) % len(self.fields)
471
+ return False
472
+
473
+ # Get current field
474
+ current_field = self.fields[self.current_field_index]
475
+
476
+ # Tool format - use arrow left/right or space to toggle
477
+ if current_field == "tool_format":
478
+ if key_press.name in ("ArrowRight", "ArrowLeft") or key_press.char == ' ':
479
+ self.tool_format_index = (self.tool_format_index + 1) % len(self.tool_formats)
480
+ self.field_values["tool_format"] = self.tool_formats[self.tool_format_index]
481
+ return False
482
+
483
+ # Text input handling for other fields
484
+ return self._handle_text_input(key_press, current_field)
485
+
486
+ def _handle_text_input(self, key_press: KeyPress, field: str) -> bool:
487
+ """Handle text input for a field."""
488
+ value = self.field_values.get(field, "")
489
+ cursor_pos = self.cursor_positions.get(field, len(value))
490
+
491
+ # Backspace
492
+ if key_press.name == "Backspace" or key_press.char == '\x7f' or key_press.char == '\x08':
493
+ if cursor_pos > 0:
494
+ value = value[:cursor_pos - 1] + value[cursor_pos:]
495
+ cursor_pos -= 1
496
+ self.field_values[field] = value
497
+ self.cursor_positions[field] = cursor_pos
498
+ return False
499
+
500
+ # Delete
501
+ if key_press.name == "Delete":
502
+ if cursor_pos < len(value):
503
+ value = value[:cursor_pos] + value[cursor_pos + 1:]
504
+ self.field_values[field] = value
505
+ return False
506
+
507
+ # Cursor movement
508
+ if key_press.name == "ArrowLeft":
509
+ if cursor_pos > 0:
510
+ self.cursor_positions[field] = cursor_pos - 1
511
+ return False
512
+
513
+ if key_press.name == "ArrowRight":
514
+ if cursor_pos < len(value):
515
+ self.cursor_positions[field] = cursor_pos + 1
516
+ return False
517
+
518
+ # Home/End
519
+ if key_press.name == "Home":
520
+ self.cursor_positions[field] = 0
521
+ return False
522
+
523
+ if key_press.name == "End":
524
+ self.cursor_positions[field] = len(value)
525
+ return False
526
+
527
+ # Printable character
528
+ if key_press.char and key_press.char.isprintable() and len(key_press.char) == 1:
529
+ value = value[:cursor_pos] + key_press.char + value[cursor_pos:]
530
+ cursor_pos += 1
531
+ self.field_values[field] = value
532
+ self.cursor_positions[field] = cursor_pos
533
+ return False
534
+
535
+ return False
536
+
537
+ async def _save_configuration(self):
538
+ """Save the configuration to profile manager."""
539
+ if self._profile_manager:
540
+ try:
541
+ # Parse temperature
542
+ temp = float(self.field_values.get("temperature", "0.7"))
543
+ except ValueError:
544
+ temp = 0.7
545
+
546
+ try:
547
+ # Get token and profile name
548
+ token = self.field_values.get("token", "")
549
+ profile_name = self.field_values["profile_name"]
550
+
551
+ # Check if profile exists
552
+ existing = self._profile_manager.get_profile(profile_name)
553
+ if existing:
554
+ # Update existing profile
555
+ self._profile_manager.update_profile(
556
+ original_name=profile_name,
557
+ api_url=self.field_values["api_url"],
558
+ model=self.field_values["model"],
559
+ temperature=temp,
560
+ tool_format=self.field_values["tool_format"],
561
+ api_token=token if token else None,
562
+ description="Updated via setup wizard",
563
+ save_to_config=True
564
+ )
565
+ else:
566
+ # Create new profile
567
+ self._profile_manager.create_profile(
568
+ name=profile_name,
569
+ api_url=self.field_values["api_url"],
570
+ model=self.field_values["model"],
571
+ temperature=temp,
572
+ tool_format=self.field_values["tool_format"],
573
+ api_token=token if token else None,
574
+ description="Created via setup wizard",
575
+ save_to_config=True
576
+ )
577
+
578
+ # Set as active profile
579
+ self._profile_manager.set_active_profile(profile_name)
580
+ except Exception as e:
581
+ # Log error but don't fail
582
+ import logging
583
+ logging.getLogger(__name__).error(f"Failed to save profile: {e}")
584
+
585
+ # Mark setup as completed
586
+ if self._config:
587
+ self._config.set("application.setup_completed", True)
588
+
589
+ async def cleanup(self):
590
+ """Clean up wizard resources."""
591
+ self.animation_framework.clear_all()
592
+ await super().cleanup()