kollabor 0.4.9__py3-none-any.whl → 0.4.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -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()
|