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,1494 @@
|
|
|
1
|
+
"""Resume conversation plugin for session management."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Dict, Any, List, Optional
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from core.events.models import CommandDefinition, CommandMode, CommandCategory, CommandResult, SlashCommand, UIConfig, Event
|
|
9
|
+
from core.models.resume import SessionMetadata, SessionSummary, ConversationMetadata
|
|
10
|
+
from core.io.visual_effects import AgnosterSegment
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResumeConversationPlugin:
|
|
16
|
+
"""Plugin for resuming previous conversations."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, **kwargs) -> None:
|
|
19
|
+
"""Initialize the resume conversation plugin."""
|
|
20
|
+
self.name = "resume_conversation"
|
|
21
|
+
self.version = "1.0.0"
|
|
22
|
+
self.description = "Resume previous conversation sessions"
|
|
23
|
+
self.enabled = True
|
|
24
|
+
self.logger = logger
|
|
25
|
+
|
|
26
|
+
# Dependencies (will be injected during initialization)
|
|
27
|
+
self.conversation_manager = None
|
|
28
|
+
self.conversation_logger = None
|
|
29
|
+
self.event_bus = None
|
|
30
|
+
self.config = None
|
|
31
|
+
self.llm_service = None
|
|
32
|
+
self.renderer = None
|
|
33
|
+
|
|
34
|
+
async def initialize(self, event_bus, config, **kwargs) -> None:
|
|
35
|
+
"""Initialize the plugin and register commands.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
event_bus: Application event bus.
|
|
39
|
+
config: Configuration manager.
|
|
40
|
+
**kwargs: Additional initialization parameters.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
self.event_bus = event_bus
|
|
44
|
+
self.config = config
|
|
45
|
+
|
|
46
|
+
# Get dependencies from kwargs
|
|
47
|
+
self.conversation_manager = kwargs.get('conversation_manager')
|
|
48
|
+
self.conversation_logger = kwargs.get('conversation_logger')
|
|
49
|
+
self.llm_service = kwargs.get('llm_service')
|
|
50
|
+
self.renderer = kwargs.get('renderer')
|
|
51
|
+
|
|
52
|
+
# Get command registry
|
|
53
|
+
command_registry = kwargs.get('command_registry')
|
|
54
|
+
if not command_registry:
|
|
55
|
+
self.logger.warning("No command registry provided, resume commands not registered")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Register resume command
|
|
59
|
+
self._register_resume_commands(command_registry)
|
|
60
|
+
|
|
61
|
+
# Register status view
|
|
62
|
+
await self._register_status_view()
|
|
63
|
+
|
|
64
|
+
self.logger.info("Resume conversation plugin initialized successfully")
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
self.logger.error(f"Error initializing resume conversation plugin: {e}")
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
async def _register_status_view(self) -> None:
|
|
71
|
+
"""Register resume conversation status view."""
|
|
72
|
+
try:
|
|
73
|
+
if (self.renderer and
|
|
74
|
+
hasattr(self.renderer, 'status_renderer') and
|
|
75
|
+
self.renderer.status_renderer and
|
|
76
|
+
hasattr(self.renderer.status_renderer, 'status_registry') and
|
|
77
|
+
self.renderer.status_renderer.status_registry):
|
|
78
|
+
|
|
79
|
+
from core.io.status_renderer import StatusViewConfig, BlockConfig
|
|
80
|
+
|
|
81
|
+
view = StatusViewConfig(
|
|
82
|
+
name="Resume",
|
|
83
|
+
plugin_source="resume_conversation",
|
|
84
|
+
priority=290,
|
|
85
|
+
blocks=[BlockConfig(
|
|
86
|
+
width_fraction=1.0,
|
|
87
|
+
content_provider=self._get_status_content,
|
|
88
|
+
title="Resume",
|
|
89
|
+
priority=100
|
|
90
|
+
)],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
registry = self.renderer.status_renderer.status_registry
|
|
94
|
+
registry.register_status_view("resume_conversation", view)
|
|
95
|
+
self.logger.info("Registered 'Resume' status view")
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
self.logger.error(f"Failed to register status view: {e}")
|
|
99
|
+
|
|
100
|
+
def _get_status_content(self) -> List[str]:
|
|
101
|
+
"""Get resume conversation status (agnoster style)."""
|
|
102
|
+
try:
|
|
103
|
+
seg = AgnosterSegment()
|
|
104
|
+
seg.add_lime("Resume", "dark")
|
|
105
|
+
seg.add_cyan("/resume", "dark")
|
|
106
|
+
seg.add_neutral("restore previous sessions", "mid")
|
|
107
|
+
return [seg.render()]
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self.logger.error(f"Error getting status content: {e}")
|
|
111
|
+
seg = AgnosterSegment()
|
|
112
|
+
seg.add_neutral("Resume: Error", "dark")
|
|
113
|
+
return [seg.render()]
|
|
114
|
+
|
|
115
|
+
def _register_resume_commands(self, command_registry) -> None:
|
|
116
|
+
"""Register resume-related commands.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
command_registry: Command registry for registration.
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
# Main resume command
|
|
123
|
+
resume_command = CommandDefinition(
|
|
124
|
+
name="resume",
|
|
125
|
+
description="Resume a previous conversation session",
|
|
126
|
+
handler=self.handle_resume,
|
|
127
|
+
plugin_name=self.name,
|
|
128
|
+
category=CommandCategory.CONVERSATION,
|
|
129
|
+
mode=CommandMode.STATUS_TAKEOVER,
|
|
130
|
+
aliases=["restore", "continue"],
|
|
131
|
+
icon="[⏯]",
|
|
132
|
+
ui_config=UIConfig(
|
|
133
|
+
type="modal",
|
|
134
|
+
title="Resume Conversation",
|
|
135
|
+
height=20,
|
|
136
|
+
width=80
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
command_registry.register_command(resume_command)
|
|
140
|
+
|
|
141
|
+
# Session search command
|
|
142
|
+
search_command = CommandDefinition(
|
|
143
|
+
name="sessions",
|
|
144
|
+
description="Search and browse conversation sessions",
|
|
145
|
+
handler=self.handle_sessions,
|
|
146
|
+
plugin_name=self.name,
|
|
147
|
+
category=CommandCategory.CONVERSATION,
|
|
148
|
+
mode=CommandMode.STATUS_TAKEOVER,
|
|
149
|
+
aliases=["history", "conversations"],
|
|
150
|
+
icon="[s]",
|
|
151
|
+
ui_config=UIConfig(
|
|
152
|
+
type="modal",
|
|
153
|
+
title="Conversation History",
|
|
154
|
+
height=20,
|
|
155
|
+
width=80
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
command_registry.register_command(search_command)
|
|
159
|
+
|
|
160
|
+
# Branch command
|
|
161
|
+
branch_command = CommandDefinition(
|
|
162
|
+
name="branch",
|
|
163
|
+
description="Branch conversation from a specific message",
|
|
164
|
+
handler=self.handle_branch,
|
|
165
|
+
plugin_name=self.name,
|
|
166
|
+
category=CommandCategory.CONVERSATION,
|
|
167
|
+
mode=CommandMode.STATUS_TAKEOVER,
|
|
168
|
+
aliases=["fork"],
|
|
169
|
+
icon="[⑂]",
|
|
170
|
+
ui_config=UIConfig(
|
|
171
|
+
type="modal",
|
|
172
|
+
title="Branch Conversation",
|
|
173
|
+
height=20,
|
|
174
|
+
width=80
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
command_registry.register_command(branch_command)
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
self.logger.error(f"Error registering resume commands: {e}")
|
|
181
|
+
|
|
182
|
+
async def handle_resume(self, command: SlashCommand) -> CommandResult:
|
|
183
|
+
"""Handle /resume command.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
command: Parsed slash command.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Command execution result.
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
args = command.args or []
|
|
193
|
+
force = False
|
|
194
|
+
if args:
|
|
195
|
+
force = "--force" in args
|
|
196
|
+
args = [arg for arg in args if arg != "--force"]
|
|
197
|
+
|
|
198
|
+
if len(args) == 0:
|
|
199
|
+
# Show session selection modal
|
|
200
|
+
return await self._show_conversation_menu()
|
|
201
|
+
elif len(args) == 1:
|
|
202
|
+
# Resume specific session by ID
|
|
203
|
+
session_id = args[0]
|
|
204
|
+
return await self._load_conversation(session_id, force=force)
|
|
205
|
+
elif len(args) >= 2 and args[0].lower() == "search":
|
|
206
|
+
# Search sessions
|
|
207
|
+
query = " ".join(args[1:])
|
|
208
|
+
return await self._search_conversations(query)
|
|
209
|
+
else:
|
|
210
|
+
# Handle filters and other options
|
|
211
|
+
return await self._handle_resume_options(args)
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
self.logger.error(f"Error in resume command: {e}")
|
|
215
|
+
return CommandResult(
|
|
216
|
+
success=False,
|
|
217
|
+
message=f"Error resuming conversation: {str(e)}",
|
|
218
|
+
display_type="error"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
async def handle_sessions(self, command: SlashCommand) -> CommandResult:
|
|
222
|
+
"""Handle /sessions command.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
command: Parsed slash command.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Command execution result.
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
args = command.args or []
|
|
232
|
+
|
|
233
|
+
if len(args) == 0:
|
|
234
|
+
# Show all sessions
|
|
235
|
+
return await self._show_conversation_menu()
|
|
236
|
+
elif args[0].lower() == "search":
|
|
237
|
+
if len(args) > 1:
|
|
238
|
+
query = " ".join(args[1:])
|
|
239
|
+
return await self._search_conversations(query)
|
|
240
|
+
else:
|
|
241
|
+
return CommandResult(
|
|
242
|
+
success=False,
|
|
243
|
+
message="Usage: /sessions search <query>",
|
|
244
|
+
display_type="error"
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
return CommandResult(
|
|
248
|
+
success=False,
|
|
249
|
+
message="Usage: /sessions [search <query>]",
|
|
250
|
+
display_type="error"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
self.logger.error(f"Error in sessions command: {e}")
|
|
255
|
+
return CommandResult(
|
|
256
|
+
success=False,
|
|
257
|
+
message=f"Error browsing sessions: {str(e)}",
|
|
258
|
+
display_type="error"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
async def handle_branch(self, command: SlashCommand) -> CommandResult:
|
|
262
|
+
"""Handle /branch command for branching conversations.
|
|
263
|
+
|
|
264
|
+
Creates a new branch from any conversation at any message point.
|
|
265
|
+
Original conversation remains intact, new branch is created.
|
|
266
|
+
|
|
267
|
+
Usage:
|
|
268
|
+
/branch - Show sessions to branch from
|
|
269
|
+
/branch <session_id> - Show messages to select branch point
|
|
270
|
+
/branch <session_id> <idx> - Create branch from message at index
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
command: Parsed slash command.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Command execution result.
|
|
277
|
+
"""
|
|
278
|
+
try:
|
|
279
|
+
args = command.args or []
|
|
280
|
+
|
|
281
|
+
if not self._ensure_conversation_manager():
|
|
282
|
+
return CommandResult(success=False, message="Conversation manager not available", display_type="error")
|
|
283
|
+
|
|
284
|
+
if len(args) == 0:
|
|
285
|
+
# Step 1: Show sessions to branch from (including current)
|
|
286
|
+
return await self._show_branch_session_selector()
|
|
287
|
+
|
|
288
|
+
elif len(args) == 1:
|
|
289
|
+
# Step 2: Show messages from session to select branch point
|
|
290
|
+
session_id = args[0]
|
|
291
|
+
return await self._show_branch_point_selector(session_id)
|
|
292
|
+
|
|
293
|
+
elif len(args) >= 2:
|
|
294
|
+
# Step 3: Execute the branch - create new session from branch point
|
|
295
|
+
session_id = args[0]
|
|
296
|
+
self.logger.info(f"[BRANCH] Step 3: Execute branch from session={session_id}")
|
|
297
|
+
try:
|
|
298
|
+
branch_index = int(args[1])
|
|
299
|
+
except ValueError:
|
|
300
|
+
self.logger.error(f"[BRANCH] Invalid branch index: {args[1]}")
|
|
301
|
+
return CommandResult(
|
|
302
|
+
success=False,
|
|
303
|
+
message=f"Invalid branch index: {args[1]}. Must be a number.",
|
|
304
|
+
display_type="error"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
self.logger.info(f"[BRANCH] Executing branch at index {branch_index}")
|
|
308
|
+
# Create branch and load it
|
|
309
|
+
result = await self._execute_branch_from_session(session_id, branch_index)
|
|
310
|
+
self.logger.info(f"[BRANCH] Result: success={result.success}, message={result.message[:50]}...")
|
|
311
|
+
return result
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.logger.error(f"Error in branch command: {e}")
|
|
315
|
+
return CommandResult(
|
|
316
|
+
success=False,
|
|
317
|
+
message=f"Error branching conversation: {str(e)}",
|
|
318
|
+
display_type="error"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
async def _execute_branch_from_session(self, session_id: str, branch_index: int) -> CommandResult:
|
|
322
|
+
"""Execute branch operation - create new session from branch point and load it.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
session_id: Source session to branch from (or "current" for active conversation).
|
|
326
|
+
branch_index: Index to branch from (inclusive).
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Command result.
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
# Handle current conversation
|
|
333
|
+
if session_id == "current":
|
|
334
|
+
if not self.llm_service or not hasattr(self.llm_service, 'conversation_history'):
|
|
335
|
+
return CommandResult(
|
|
336
|
+
success=False,
|
|
337
|
+
message="No current conversation available",
|
|
338
|
+
display_type="error"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
current_messages = self.llm_service.conversation_history
|
|
342
|
+
if branch_index < 0 or branch_index >= len(current_messages):
|
|
343
|
+
return CommandResult(
|
|
344
|
+
success=False,
|
|
345
|
+
message=f"Invalid index. Must be 0-{len(current_messages)-1}.",
|
|
346
|
+
display_type="error"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Create branch from current - keep messages up to branch_index
|
|
350
|
+
branched_messages = current_messages[:branch_index + 1]
|
|
351
|
+
self.llm_service.conversation_history = branched_messages
|
|
352
|
+
|
|
353
|
+
# Update session stats to reflect truncated messages
|
|
354
|
+
if hasattr(self.llm_service, 'session_stats'):
|
|
355
|
+
self.llm_service.session_stats["messages"] = len(branched_messages)
|
|
356
|
+
|
|
357
|
+
msg_count = len(branched_messages)
|
|
358
|
+
removed_count = len(current_messages) - msg_count
|
|
359
|
+
|
|
360
|
+
return CommandResult(
|
|
361
|
+
success=True,
|
|
362
|
+
message=f"[ok] Branched at message {branch_index}\n"
|
|
363
|
+
f" Kept {msg_count} messages, removed {removed_count}\n"
|
|
364
|
+
f" Continue the conversation from this point.",
|
|
365
|
+
display_type="success"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Branch from saved session
|
|
369
|
+
result = self.conversation_manager.branch_session(session_id, branch_index)
|
|
370
|
+
|
|
371
|
+
if not result.get("success"):
|
|
372
|
+
return CommandResult(success=False, message=f"Branch failed: {result.get('error', 'Unknown error')}", display_type="error")
|
|
373
|
+
|
|
374
|
+
new_session_id = result.get("session_id")
|
|
375
|
+
return await self._load_and_display_session(
|
|
376
|
+
header=f"--- Branched from {session_id} at message {branch_index} ---",
|
|
377
|
+
success_msg=(
|
|
378
|
+
f"[ok] Created branch: {new_session_id}\n"
|
|
379
|
+
f" From: {session_id} at message {result['branch_point']}\n"
|
|
380
|
+
f" Loaded {result['message_count']} messages. Continue below."
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
except Exception as e:
|
|
385
|
+
self.logger.error(f"Error executing branch: {e}")
|
|
386
|
+
return CommandResult(
|
|
387
|
+
success=False,
|
|
388
|
+
message=f"Error executing branch: {str(e)}",
|
|
389
|
+
display_type="error"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
async def _show_branch_session_selector(self) -> CommandResult:
|
|
393
|
+
"""Show modal to select a session to branch from."""
|
|
394
|
+
session_items = []
|
|
395
|
+
|
|
396
|
+
# Add current conversation as first option if it has messages
|
|
397
|
+
current_msg_count = 0
|
|
398
|
+
if self.llm_service and hasattr(self.llm_service, 'conversation_history'):
|
|
399
|
+
current_messages = self.llm_service.conversation_history
|
|
400
|
+
current_msg_count = len(current_messages) if current_messages else 0
|
|
401
|
+
|
|
402
|
+
if current_msg_count >= 2:
|
|
403
|
+
# Get first user message for preview
|
|
404
|
+
first_user_msg = ""
|
|
405
|
+
for msg in current_messages:
|
|
406
|
+
role = msg.role if hasattr(msg, 'role') else msg.get('role', '')
|
|
407
|
+
content = msg.content if hasattr(msg, 'content') else msg.get('content', '')
|
|
408
|
+
if role == "user" and content:
|
|
409
|
+
first_user_msg = content[:45].split('\n')[0]
|
|
410
|
+
if len(content) > 45:
|
|
411
|
+
first_user_msg += "..."
|
|
412
|
+
break
|
|
413
|
+
|
|
414
|
+
session_items.append({
|
|
415
|
+
"id": "current",
|
|
416
|
+
"title": f"[*CURRENT*] {first_user_msg or 'Active conversation'}",
|
|
417
|
+
"subtitle": f"{current_msg_count} msgs | this session",
|
|
418
|
+
"metadata": {"session_id": "current"},
|
|
419
|
+
"action": "branch_select_session"
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
# Add saved conversations
|
|
423
|
+
conversations = await self.discover_conversations(limit=20)
|
|
424
|
+
|
|
425
|
+
for conv in conversations:
|
|
426
|
+
time_str = conv.created_time.strftime("%m/%d %H:%M") if conv.created_time else "Unknown"
|
|
427
|
+
project_name = conv.working_directory.split("/")[-1] if conv.working_directory else "unknown"
|
|
428
|
+
|
|
429
|
+
# Get user's actual request - skip system prompts
|
|
430
|
+
user_request = ""
|
|
431
|
+
if conv.preview_messages:
|
|
432
|
+
for msg in conv.preview_messages:
|
|
433
|
+
role = msg.get("role", "")
|
|
434
|
+
content = msg.get("content", "")
|
|
435
|
+
# Skip system messages and empty content
|
|
436
|
+
if role == "system" or not content:
|
|
437
|
+
continue
|
|
438
|
+
# Skip if content looks like a system prompt (starts with common prompt markers)
|
|
439
|
+
content_lower = content.lower()[:50]
|
|
440
|
+
if any(marker in content_lower for marker in ["you are", "system prompt", "assistant", "kollabor"]):
|
|
441
|
+
continue
|
|
442
|
+
if role == "user" and content:
|
|
443
|
+
user_request = content
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
# Truncate and clean up first line
|
|
447
|
+
if user_request:
|
|
448
|
+
first_line = user_request.split('\n')[0].strip()[:50]
|
|
449
|
+
if len(user_request.split('\n')[0]) > 50:
|
|
450
|
+
first_line += "..."
|
|
451
|
+
else:
|
|
452
|
+
first_line = f"{conv.message_count} messages"
|
|
453
|
+
|
|
454
|
+
session_items.append({
|
|
455
|
+
"id": conv.session_id,
|
|
456
|
+
"title": f"[{time_str}] {first_line}",
|
|
457
|
+
"subtitle": f"{conv.message_count} msgs | {project_name} | {conv.git_branch or '-'}",
|
|
458
|
+
"metadata": {"session_id": conv.session_id},
|
|
459
|
+
"action": "branch_select_session"
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
if not session_items:
|
|
463
|
+
return CommandResult(
|
|
464
|
+
success=False,
|
|
465
|
+
message="No conversations found to branch from. Start a conversation first.",
|
|
466
|
+
display_type="info"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
modal_definition = {
|
|
470
|
+
"title": "Branch From Session",
|
|
471
|
+
"footer": "Up/Down navigate | Enter select | Esc cancel",
|
|
472
|
+
"width": 80,
|
|
473
|
+
"height": 20,
|
|
474
|
+
"sections": [
|
|
475
|
+
{
|
|
476
|
+
"title": f"Select session to branch ({len(session_items)} available)",
|
|
477
|
+
"type": "session_list",
|
|
478
|
+
"sessions": session_items
|
|
479
|
+
}
|
|
480
|
+
]
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return CommandResult(
|
|
484
|
+
success=True,
|
|
485
|
+
message="Select a session to branch from",
|
|
486
|
+
display_type="modal",
|
|
487
|
+
ui_config=UIConfig(
|
|
488
|
+
type="modal",
|
|
489
|
+
title=modal_definition["title"],
|
|
490
|
+
modal_config=modal_definition
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
async def _show_branch_point_selector(self, session_id: str) -> CommandResult:
|
|
495
|
+
"""Show modal to select branch point message."""
|
|
496
|
+
messages = []
|
|
497
|
+
|
|
498
|
+
# Handle current conversation
|
|
499
|
+
if session_id == "current":
|
|
500
|
+
if self.llm_service and hasattr(self.llm_service, 'conversation_history'):
|
|
501
|
+
current_messages = self.llm_service.conversation_history
|
|
502
|
+
for i, msg in enumerate(current_messages or []):
|
|
503
|
+
role = msg.role if hasattr(msg, 'role') else msg.get('role', 'unknown')
|
|
504
|
+
content = msg.content if hasattr(msg, 'content') else msg.get('content', '')
|
|
505
|
+
preview = content[:50].replace('\n', ' ')
|
|
506
|
+
if len(content) > 50:
|
|
507
|
+
preview += "..."
|
|
508
|
+
messages.append({
|
|
509
|
+
"index": i,
|
|
510
|
+
"role": role,
|
|
511
|
+
"preview": preview,
|
|
512
|
+
"timestamp": None
|
|
513
|
+
})
|
|
514
|
+
title_text = "Current Session"
|
|
515
|
+
else:
|
|
516
|
+
# Get messages from saved session
|
|
517
|
+
messages = self.conversation_manager.get_session_messages(session_id)
|
|
518
|
+
# Format title - extract readable part from session_id (e.g., "2512301235-shadow-rise" -> "shadow-rise")
|
|
519
|
+
parts = session_id.split("-", 1)
|
|
520
|
+
title_text = parts[1] if len(parts) > 1 else session_id[:12]
|
|
521
|
+
|
|
522
|
+
if not messages:
|
|
523
|
+
return CommandResult(
|
|
524
|
+
success=False,
|
|
525
|
+
message=f"No messages found in session: {session_id}",
|
|
526
|
+
display_type="error"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Build message selector - skip system messages from display
|
|
530
|
+
message_items = []
|
|
531
|
+
for msg in messages:
|
|
532
|
+
role = msg["role"]
|
|
533
|
+
preview = msg["preview"]
|
|
534
|
+
|
|
535
|
+
# Skip system prompt messages from selection (can't meaningfully branch from them)
|
|
536
|
+
if role == "system":
|
|
537
|
+
continue
|
|
538
|
+
|
|
539
|
+
# Role indicators and labels
|
|
540
|
+
if role == "user":
|
|
541
|
+
role_indicator = "YOU:"
|
|
542
|
+
role_label = "user"
|
|
543
|
+
else:
|
|
544
|
+
role_indicator = "AI:"
|
|
545
|
+
role_label = "assistant"
|
|
546
|
+
|
|
547
|
+
# Clean up preview
|
|
548
|
+
clean_preview = preview.strip()[:55]
|
|
549
|
+
if len(preview) > 55:
|
|
550
|
+
clean_preview += "..."
|
|
551
|
+
|
|
552
|
+
# Format timestamp if available
|
|
553
|
+
ts = msg.get('timestamp')
|
|
554
|
+
time_str = ts[11:16] if ts and len(ts) > 16 else "" # Just HH:MM
|
|
555
|
+
|
|
556
|
+
message_items.append({
|
|
557
|
+
"id": str(msg["index"]),
|
|
558
|
+
"title": f"[{msg['index']}] {role_indicator} {clean_preview}",
|
|
559
|
+
"subtitle": f"{role_label}{' | ' + time_str if time_str else ''}",
|
|
560
|
+
"metadata": {
|
|
561
|
+
"session_id": session_id,
|
|
562
|
+
"message_index": msg["index"]
|
|
563
|
+
},
|
|
564
|
+
"action": "branch_execute",
|
|
565
|
+
"exit_mode": "minimal" # Plugin will display content after modal exit
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
modal_definition = {
|
|
569
|
+
"title": f"Branch Point: {title_text}",
|
|
570
|
+
"footer": "Up/Down navigate | Enter branch here | Esc cancel",
|
|
571
|
+
"width": 80,
|
|
572
|
+
"height": 20,
|
|
573
|
+
"sections": [
|
|
574
|
+
{
|
|
575
|
+
"title": f"Select message to branch from ({len(messages)} messages)",
|
|
576
|
+
"type": "session_list",
|
|
577
|
+
"sessions": message_items
|
|
578
|
+
}
|
|
579
|
+
]
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return CommandResult(
|
|
583
|
+
success=True,
|
|
584
|
+
message=f"Select branch point",
|
|
585
|
+
display_type="modal",
|
|
586
|
+
ui_config=UIConfig(
|
|
587
|
+
type="modal",
|
|
588
|
+
title=modal_definition["title"],
|
|
589
|
+
modal_config=modal_definition
|
|
590
|
+
)
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
async def discover_conversations(self, limit: int = 50) -> List[ConversationMetadata]:
|
|
594
|
+
"""Discover available conversations.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
limit: Maximum number of conversations to return
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
List of conversation metadata
|
|
601
|
+
"""
|
|
602
|
+
conversations = []
|
|
603
|
+
|
|
604
|
+
try:
|
|
605
|
+
if not self.conversation_logger:
|
|
606
|
+
self.logger.warning("Conversation logger not available")
|
|
607
|
+
return conversations
|
|
608
|
+
|
|
609
|
+
# Get sessions from conversation logger
|
|
610
|
+
sessions = self.conversation_logger.list_sessions()
|
|
611
|
+
|
|
612
|
+
for session_data in sessions:
|
|
613
|
+
# Stop if we have enough conversations
|
|
614
|
+
if len(conversations) >= limit:
|
|
615
|
+
break
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
# Filter out sessions with 2 or fewer messages
|
|
619
|
+
message_count = session_data.get("message_count", 0)
|
|
620
|
+
if message_count <= 2:
|
|
621
|
+
continue
|
|
622
|
+
|
|
623
|
+
# Strip 'session_' prefix if present for compatibility with conversation_manager
|
|
624
|
+
raw_session_id = session_data.get("session_id", "")
|
|
625
|
+
session_id = raw_session_id.replace("session_", "") if raw_session_id.startswith("session_") else raw_session_id
|
|
626
|
+
|
|
627
|
+
metadata = ConversationMetadata(
|
|
628
|
+
file_path=session_data.get("file_path", ""),
|
|
629
|
+
session_id=session_id,
|
|
630
|
+
title=self._generate_session_title(session_data),
|
|
631
|
+
message_count=message_count,
|
|
632
|
+
created_time=self._parse_datetime(session_data.get("start_time")),
|
|
633
|
+
modified_time=self._parse_datetime(session_data.get("end_time")),
|
|
634
|
+
last_message_preview=session_data.get("preview_messages", [{}])[0].get("content", ""),
|
|
635
|
+
topics=session_data.get("topics", []),
|
|
636
|
+
file_id=self._generate_file_id(session_data.get("session_id", "")),
|
|
637
|
+
working_directory=session_data.get("working_directory", "unknown"),
|
|
638
|
+
git_branch=session_data.get("git_branch", "unknown"),
|
|
639
|
+
duration=session_data.get("duration"),
|
|
640
|
+
size_bytes=session_data.get("size_bytes", 0),
|
|
641
|
+
preview_messages=session_data.get("preview_messages", [])
|
|
642
|
+
)
|
|
643
|
+
conversations.append(metadata)
|
|
644
|
+
|
|
645
|
+
except Exception as e:
|
|
646
|
+
self.logger.warning(f"Failed to process session: {e}")
|
|
647
|
+
continue
|
|
648
|
+
|
|
649
|
+
except Exception as e:
|
|
650
|
+
self.logger.error(f"Failed to discover conversations: {e}")
|
|
651
|
+
|
|
652
|
+
return conversations
|
|
653
|
+
|
|
654
|
+
async def _show_conversation_menu(self) -> CommandResult:
|
|
655
|
+
"""Show interactive conversation selection menu.
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
Command result with modal UI.
|
|
659
|
+
"""
|
|
660
|
+
try:
|
|
661
|
+
conversations = await self.discover_conversations()
|
|
662
|
+
|
|
663
|
+
if not conversations:
|
|
664
|
+
return CommandResult(
|
|
665
|
+
success=False,
|
|
666
|
+
message="No saved conversations found.\n\nTip: Use /save to save current conversations for future resumption.",
|
|
667
|
+
display_type="info"
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# Build modal definition
|
|
671
|
+
modal_definition = self._build_conversation_modal(conversations)
|
|
672
|
+
|
|
673
|
+
return CommandResult(
|
|
674
|
+
success=True,
|
|
675
|
+
message="Select a conversation to resume",
|
|
676
|
+
ui_config=UIConfig(
|
|
677
|
+
type="modal",
|
|
678
|
+
title=modal_definition["title"],
|
|
679
|
+
width=modal_definition["width"],
|
|
680
|
+
height=modal_definition["height"],
|
|
681
|
+
modal_config=modal_definition
|
|
682
|
+
),
|
|
683
|
+
display_type="modal"
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
except Exception as e:
|
|
687
|
+
self.logger.error(f"Error showing conversation menu: {e}")
|
|
688
|
+
return CommandResult(
|
|
689
|
+
success=False,
|
|
690
|
+
message=f"Error loading conversations: {str(e)}",
|
|
691
|
+
display_type="error"
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
async def _search_conversations(self, query: str) -> CommandResult:
|
|
695
|
+
"""Search conversations by content.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
query: Search query
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
Command result with search modal.
|
|
702
|
+
"""
|
|
703
|
+
try:
|
|
704
|
+
if not self.conversation_logger:
|
|
705
|
+
return CommandResult(
|
|
706
|
+
success=False,
|
|
707
|
+
message="Conversation search not available",
|
|
708
|
+
display_type="error"
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
sessions = self.conversation_logger.search_sessions(query)
|
|
712
|
+
|
|
713
|
+
if not sessions:
|
|
714
|
+
return CommandResult(
|
|
715
|
+
success=False,
|
|
716
|
+
message=f"No conversations found matching: {query}",
|
|
717
|
+
display_type="info"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
# Convert to conversation metadata
|
|
721
|
+
conversations = []
|
|
722
|
+
for session_data in sessions[:20]: # Limit search results
|
|
723
|
+
# Strip 'session_' prefix if present for compatibility with conversation_manager
|
|
724
|
+
raw_session_id = session_data.get("session_id", "")
|
|
725
|
+
session_id = raw_session_id.replace("session_", "") if raw_session_id.startswith("session_") else raw_session_id
|
|
726
|
+
|
|
727
|
+
metadata = ConversationMetadata(
|
|
728
|
+
file_path=session_data.get("file_path", ""),
|
|
729
|
+
session_id=session_id,
|
|
730
|
+
title=self._generate_session_title(session_data),
|
|
731
|
+
message_count=session_data.get("message_count", 0),
|
|
732
|
+
created_time=self._parse_datetime(session_data.get("start_time")),
|
|
733
|
+
modified_time=self._parse_datetime(session_data.get("end_time")),
|
|
734
|
+
last_message_preview=session_data.get("preview_messages", [{}])[0].get("content", ""),
|
|
735
|
+
topics=session_data.get("topics", []),
|
|
736
|
+
file_id=self._generate_file_id(session_data.get("session_id", "")),
|
|
737
|
+
working_directory=session_data.get("working_directory", "unknown"),
|
|
738
|
+
git_branch=session_data.get("git_branch", "unknown"),
|
|
739
|
+
duration=session_data.get("duration"),
|
|
740
|
+
size_bytes=session_data.get("size_bytes", 0),
|
|
741
|
+
preview_messages=session_data.get("preview_messages", []),
|
|
742
|
+
search_relevance=session_data.get("search_relevance")
|
|
743
|
+
)
|
|
744
|
+
conversations.append(metadata)
|
|
745
|
+
|
|
746
|
+
# Build search modal
|
|
747
|
+
modal_definition = self._build_search_modal(conversations, query)
|
|
748
|
+
|
|
749
|
+
return CommandResult(
|
|
750
|
+
success=True,
|
|
751
|
+
message=f"Found {len(conversations)} conversations matching: {query}",
|
|
752
|
+
ui_config=UIConfig(
|
|
753
|
+
type="modal",
|
|
754
|
+
title=modal_definition["title"],
|
|
755
|
+
width=modal_definition["width"],
|
|
756
|
+
height=modal_definition["height"],
|
|
757
|
+
modal_config=modal_definition
|
|
758
|
+
),
|
|
759
|
+
display_type="modal"
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
except Exception as e:
|
|
763
|
+
self.logger.error(f"Error searching conversations: {e}")
|
|
764
|
+
return CommandResult(
|
|
765
|
+
success=False,
|
|
766
|
+
message=f"Error searching conversations: {str(e)}",
|
|
767
|
+
display_type="error"
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
def _get_conversation_manager(self):
|
|
771
|
+
"""Get or create conversation manager instance.
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
Conversation manager instance or None.
|
|
775
|
+
"""
|
|
776
|
+
# Return existing if available
|
|
777
|
+
if self.conversation_manager:
|
|
778
|
+
return self.conversation_manager
|
|
779
|
+
|
|
780
|
+
# Try to create one
|
|
781
|
+
try:
|
|
782
|
+
from core.llm.conversation_manager import ConversationManager
|
|
783
|
+
from core.utils.config_utils import get_conversations_dir
|
|
784
|
+
|
|
785
|
+
# Use a basic config if no manager available
|
|
786
|
+
class BasicConfig:
|
|
787
|
+
def get(self, key, default=None):
|
|
788
|
+
return default
|
|
789
|
+
|
|
790
|
+
config = BasicConfig()
|
|
791
|
+
conversations_dir = get_conversations_dir()
|
|
792
|
+
conversations_dir.mkdir(parents=True, exist_ok=True)
|
|
793
|
+
|
|
794
|
+
self.conversation_manager = ConversationManager(config)
|
|
795
|
+
return self.conversation_manager
|
|
796
|
+
except Exception as e:
|
|
797
|
+
self.logger.warning(f"Could not create conversation manager: {e}")
|
|
798
|
+
return None
|
|
799
|
+
|
|
800
|
+
def _ensure_conversation_manager(self) -> bool:
|
|
801
|
+
"""Ensure conversation manager is available. Returns True if available."""
|
|
802
|
+
if not self.conversation_manager:
|
|
803
|
+
self.conversation_manager = self._get_conversation_manager()
|
|
804
|
+
return self.conversation_manager is not None
|
|
805
|
+
|
|
806
|
+
def _prepare_session_display(self, header: str, success_msg: str) -> list:
|
|
807
|
+
"""Prepare session messages for display. Loads into llm_service but returns messages instead of displaying.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
header: Header message
|
|
811
|
+
success_msg: Success message
|
|
812
|
+
|
|
813
|
+
Returns:
|
|
814
|
+
List of display message tuples, or empty list on failure
|
|
815
|
+
"""
|
|
816
|
+
if not self.llm_service:
|
|
817
|
+
self.logger.warning("llm_service not available")
|
|
818
|
+
return []
|
|
819
|
+
|
|
820
|
+
raw_messages = self.conversation_manager.messages
|
|
821
|
+
self.logger.info(f"[SESSION] raw_messages count: {len(raw_messages)}")
|
|
822
|
+
|
|
823
|
+
from core.models import ConversationMessage
|
|
824
|
+
|
|
825
|
+
loaded_messages = []
|
|
826
|
+
display_messages = [("system", header, {})]
|
|
827
|
+
|
|
828
|
+
for msg in raw_messages:
|
|
829
|
+
role = msg.get("role", "user")
|
|
830
|
+
content = msg.get("content", "")
|
|
831
|
+
loaded_messages.append(ConversationMessage(role=role, content=content))
|
|
832
|
+
|
|
833
|
+
if role in ("user", "assistant"):
|
|
834
|
+
display_messages.append((role, content, {}))
|
|
835
|
+
|
|
836
|
+
self.llm_service.conversation_history = loaded_messages
|
|
837
|
+
|
|
838
|
+
if hasattr(self.llm_service, 'session_stats'):
|
|
839
|
+
self.llm_service.session_stats["messages"] = len(loaded_messages)
|
|
840
|
+
|
|
841
|
+
display_messages.append(("system", success_msg, {}))
|
|
842
|
+
return display_messages
|
|
843
|
+
|
|
844
|
+
async def _load_and_display_session(self, header: str, success_msg: str) -> CommandResult:
|
|
845
|
+
"""Load session into llm_service and display in UI.
|
|
846
|
+
|
|
847
|
+
Reads messages from self.conversation_manager.messages (must be populated first).
|
|
848
|
+
Used by both resume and branch after they load/create the session.
|
|
849
|
+
|
|
850
|
+
Args:
|
|
851
|
+
header: Header message for the display
|
|
852
|
+
success_msg: Success message to show at end
|
|
853
|
+
|
|
854
|
+
Returns:
|
|
855
|
+
CommandResult (always success with empty message, or error)
|
|
856
|
+
"""
|
|
857
|
+
if not self.llm_service:
|
|
858
|
+
return CommandResult(success=False, message="LLM service not available", display_type="error")
|
|
859
|
+
|
|
860
|
+
raw_messages = self.conversation_manager.messages
|
|
861
|
+
self.logger.info(f"[SESSION] raw_messages count: {len(raw_messages)}")
|
|
862
|
+
|
|
863
|
+
from core.models import ConversationMessage
|
|
864
|
+
|
|
865
|
+
loaded_messages = []
|
|
866
|
+
messages_for_display = [{"role": "system", "content": header}]
|
|
867
|
+
|
|
868
|
+
for msg in raw_messages:
|
|
869
|
+
role = msg.get("role", "user")
|
|
870
|
+
content = msg.get("content", "")
|
|
871
|
+
loaded_messages.append(ConversationMessage(role=role, content=content))
|
|
872
|
+
|
|
873
|
+
if role in ("user", "assistant"):
|
|
874
|
+
messages_for_display.append({"role": role, "content": content})
|
|
875
|
+
|
|
876
|
+
self.llm_service.conversation_history = loaded_messages
|
|
877
|
+
|
|
878
|
+
if hasattr(self.llm_service, 'session_stats'):
|
|
879
|
+
self.llm_service.session_stats["messages"] = len(loaded_messages)
|
|
880
|
+
|
|
881
|
+
# Add success message
|
|
882
|
+
messages_for_display.append({"role": "system", "content": success_msg})
|
|
883
|
+
|
|
884
|
+
# Use ADD_MESSAGE event for unified display with loading
|
|
885
|
+
from core.events.models import EventType
|
|
886
|
+
await self.event_bus.emit_with_hooks(
|
|
887
|
+
EventType.ADD_MESSAGE,
|
|
888
|
+
{
|
|
889
|
+
"messages": messages_for_display,
|
|
890
|
+
"options": {
|
|
891
|
+
"show_loading": True,
|
|
892
|
+
"loading_message": "Loading conversation...",
|
|
893
|
+
"log_messages": False, # Already logged
|
|
894
|
+
"add_to_history": False, # Already loaded above
|
|
895
|
+
"display_messages": True
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
"resume_plugin"
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
return CommandResult(success=True, message="", display_type="success")
|
|
902
|
+
|
|
903
|
+
async def _load_conversation(self, session_id: str, force: bool = False) -> CommandResult:
|
|
904
|
+
"""Load specific conversation by session ID into a new session."""
|
|
905
|
+
try:
|
|
906
|
+
if not self._ensure_conversation_manager():
|
|
907
|
+
return CommandResult(success=False, message="Conversation manager not available", display_type="error")
|
|
908
|
+
|
|
909
|
+
# Auto-save current conversation if it has messages
|
|
910
|
+
if self.conversation_manager.messages:
|
|
911
|
+
old_session = self.conversation_manager.current_session_id
|
|
912
|
+
self.conversation_manager.save_conversation()
|
|
913
|
+
self.logger.info(f"Auto-saved current session: {old_session}")
|
|
914
|
+
|
|
915
|
+
# Load the selected conversation's data
|
|
916
|
+
if not self.conversation_manager.load_session(session_id):
|
|
917
|
+
return CommandResult(success=False, message=f"Failed to load session: {session_id}", display_type="error")
|
|
918
|
+
|
|
919
|
+
# Create fresh session name for the resumed conversation
|
|
920
|
+
from core.utils.session_naming import generate_session_name
|
|
921
|
+
new_session_id = generate_session_name()
|
|
922
|
+
self.conversation_manager.current_session_id = new_session_id
|
|
923
|
+
|
|
924
|
+
return await self._load_and_display_session(
|
|
925
|
+
header=f"--- Resumed: {session_id[:20]}... as {new_session_id} ---",
|
|
926
|
+
success_msg=f"[ok] Resumed: {new_session_id}. Continue below."
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
except Exception as e:
|
|
930
|
+
self.logger.error(f"Error loading conversation: {e}")
|
|
931
|
+
return CommandResult(success=False, message=f"Error: {str(e)}", display_type="error")
|
|
932
|
+
|
|
933
|
+
async def _handle_resume_options(self, args: List[str]) -> CommandResult:
|
|
934
|
+
"""Handle additional resume options and filters.
|
|
935
|
+
|
|
936
|
+
Args:
|
|
937
|
+
args: Command arguments
|
|
938
|
+
|
|
939
|
+
Returns:
|
|
940
|
+
Command result.
|
|
941
|
+
"""
|
|
942
|
+
# Parse filters like --today, --week, --limit N
|
|
943
|
+
filters = {}
|
|
944
|
+
limit = 20
|
|
945
|
+
|
|
946
|
+
i = 0
|
|
947
|
+
while i < len(args):
|
|
948
|
+
arg = args[i]
|
|
949
|
+
|
|
950
|
+
if arg == "--today":
|
|
951
|
+
from datetime import date
|
|
952
|
+
filters["date"] = date.today().isoformat()
|
|
953
|
+
elif arg == "--week":
|
|
954
|
+
from datetime import date, timedelta
|
|
955
|
+
filters["date_range"] = (
|
|
956
|
+
(date.today() - timedelta(days=7)).isoformat(),
|
|
957
|
+
date.today().isoformat()
|
|
958
|
+
)
|
|
959
|
+
elif arg == "--limit" and i + 1 < len(args):
|
|
960
|
+
try:
|
|
961
|
+
limit = int(args[i + 1])
|
|
962
|
+
i += 1
|
|
963
|
+
except ValueError:
|
|
964
|
+
pass
|
|
965
|
+
elif arg.startswith("--"):
|
|
966
|
+
return CommandResult(
|
|
967
|
+
success=False,
|
|
968
|
+
message=f"Unknown option: {arg}",
|
|
969
|
+
display_type="error"
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
i += 1
|
|
973
|
+
|
|
974
|
+
# Apply filters
|
|
975
|
+
conversations = await self.discover_conversations(limit)
|
|
976
|
+
|
|
977
|
+
# Filter conversations based on criteria
|
|
978
|
+
filtered_conversations = []
|
|
979
|
+
for conv in conversations:
|
|
980
|
+
include = True
|
|
981
|
+
|
|
982
|
+
if "date" in filters:
|
|
983
|
+
if conv.created_time and conv.created_time.date().isoformat() != filters["date"]:
|
|
984
|
+
include = False
|
|
985
|
+
|
|
986
|
+
if "date_range" in filters:
|
|
987
|
+
start_date, end_date = filters["date_range"]
|
|
988
|
+
if conv.created_time:
|
|
989
|
+
conv_date = conv.created_time.date().isoformat()
|
|
990
|
+
if not (start_date <= conv_date <= end_date):
|
|
991
|
+
include = False
|
|
992
|
+
|
|
993
|
+
if include:
|
|
994
|
+
filtered_conversations.append(conv)
|
|
995
|
+
|
|
996
|
+
if not filtered_conversations:
|
|
997
|
+
return CommandResult(
|
|
998
|
+
success=False,
|
|
999
|
+
message="No conversations found matching the specified criteria",
|
|
1000
|
+
display_type="info"
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
# Build filtered modal
|
|
1004
|
+
modal_definition = self._build_filtered_modal(filtered_conversations, filters)
|
|
1005
|
+
|
|
1006
|
+
return CommandResult(
|
|
1007
|
+
success=True,
|
|
1008
|
+
message=f"Showing {len(filtered_conversations)} filtered conversations",
|
|
1009
|
+
ui_config=UIConfig(
|
|
1010
|
+
type="modal",
|
|
1011
|
+
title=modal_definition["title"],
|
|
1012
|
+
width=modal_definition["width"],
|
|
1013
|
+
height=modal_definition["height"],
|
|
1014
|
+
modal_config=modal_definition
|
|
1015
|
+
),
|
|
1016
|
+
display_type="modal"
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
def _build_conversation_modal(self, conversations: List[ConversationMetadata]) -> Dict[str, Any]:
|
|
1020
|
+
"""Build conversation selection modal definition.
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
conversations: List of conversations
|
|
1024
|
+
|
|
1025
|
+
Returns:
|
|
1026
|
+
Modal definition dictionary.
|
|
1027
|
+
"""
|
|
1028
|
+
session_items = []
|
|
1029
|
+
for conv in conversations:
|
|
1030
|
+
# Format time for display
|
|
1031
|
+
time_str = "Unknown"
|
|
1032
|
+
if conv.created_time:
|
|
1033
|
+
time_str = conv.created_time.strftime("%m/%d %H:%M")
|
|
1034
|
+
|
|
1035
|
+
# Extract short project name from working directory
|
|
1036
|
+
project_name = conv.working_directory.split("/")[-1] if conv.working_directory else "unknown"
|
|
1037
|
+
|
|
1038
|
+
# Create preview
|
|
1039
|
+
preview = conv.last_message_preview[:80]
|
|
1040
|
+
if len(conv.last_message_preview) > 80:
|
|
1041
|
+
preview += "..."
|
|
1042
|
+
|
|
1043
|
+
# Get user's actual request (skip system prompt if it's first)
|
|
1044
|
+
user_request = ""
|
|
1045
|
+
if conv.preview_messages:
|
|
1046
|
+
for msg in conv.preview_messages:
|
|
1047
|
+
role = msg.get("role", "")
|
|
1048
|
+
content = msg.get("content", "")
|
|
1049
|
+
# Skip system messages - look for first user message
|
|
1050
|
+
if role == "system":
|
|
1051
|
+
continue
|
|
1052
|
+
if role == "user" and content:
|
|
1053
|
+
user_request = content
|
|
1054
|
+
break
|
|
1055
|
+
|
|
1056
|
+
first_line = user_request.split('\n')[0][:50] if user_request else "Empty"
|
|
1057
|
+
if user_request and len(user_request.split('\n')[0]) > 50:
|
|
1058
|
+
first_line += "..."
|
|
1059
|
+
|
|
1060
|
+
# Format: [12/11 14:30] "first line of request"
|
|
1061
|
+
session_items.append({
|
|
1062
|
+
"id": conv.session_id,
|
|
1063
|
+
"title": f"[{time_str}] {first_line}",
|
|
1064
|
+
"subtitle": f"{conv.message_count} msgs | {conv.duration or '?'} | {project_name} | {conv.git_branch or '-'}",
|
|
1065
|
+
"preview": preview,
|
|
1066
|
+
"action": "resume_session",
|
|
1067
|
+
"exit_mode": "minimal", # Plugin will display content after modal exit
|
|
1068
|
+
"metadata": {
|
|
1069
|
+
"session_id": conv.session_id,
|
|
1070
|
+
"file_id": conv.file_id,
|
|
1071
|
+
"working_directory": conv.working_directory,
|
|
1072
|
+
"git_branch": conv.git_branch,
|
|
1073
|
+
"topics": conv.topics
|
|
1074
|
+
}
|
|
1075
|
+
})
|
|
1076
|
+
|
|
1077
|
+
return {
|
|
1078
|
+
"title": "Resume Conversation",
|
|
1079
|
+
"footer": "↑↓ navigate • Enter select • Tab search • F filter • Esc exit",
|
|
1080
|
+
"width": 80,
|
|
1081
|
+
"height": 20,
|
|
1082
|
+
"sections": [
|
|
1083
|
+
{
|
|
1084
|
+
"title": f"Recent Conversations ({len(conversations)} available)",
|
|
1085
|
+
"type": "session_list",
|
|
1086
|
+
"sessions": session_items
|
|
1087
|
+
}
|
|
1088
|
+
],
|
|
1089
|
+
"actions": [
|
|
1090
|
+
{"key": "Enter", "label": "Resume", "action": "select"},
|
|
1091
|
+
{"key": "Tab", "label": "Search", "action": "search"},
|
|
1092
|
+
{"key": "F", "label": "Filter", "action": "filter"},
|
|
1093
|
+
{"key": "Escape", "label": "Cancel", "action": "cancel"}
|
|
1094
|
+
]
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
def _build_search_modal(self, conversations: List[ConversationMetadata], query: str) -> Dict[str, Any]:
|
|
1098
|
+
"""Build search results modal definition.
|
|
1099
|
+
|
|
1100
|
+
Args:
|
|
1101
|
+
conversations: Search results
|
|
1102
|
+
query: Search query
|
|
1103
|
+
|
|
1104
|
+
Returns:
|
|
1105
|
+
Modal definition dictionary.
|
|
1106
|
+
"""
|
|
1107
|
+
session_items = []
|
|
1108
|
+
for conv in conversations:
|
|
1109
|
+
# Format time for display
|
|
1110
|
+
time_str = "Unknown"
|
|
1111
|
+
if conv.created_time:
|
|
1112
|
+
time_str = conv.created_time.strftime("%m/%d %H:%M")
|
|
1113
|
+
|
|
1114
|
+
# Extract short project name
|
|
1115
|
+
project_name = conv.working_directory.split("/")[-1] if conv.working_directory else "unknown"
|
|
1116
|
+
|
|
1117
|
+
# Get user's actual request (skip system prompt if it's first)
|
|
1118
|
+
user_request = ""
|
|
1119
|
+
if conv.preview_messages:
|
|
1120
|
+
for msg in conv.preview_messages:
|
|
1121
|
+
role = msg.get("role", "")
|
|
1122
|
+
content = msg.get("content", "")
|
|
1123
|
+
# Skip system messages - look for first user message
|
|
1124
|
+
if role == "system":
|
|
1125
|
+
continue
|
|
1126
|
+
if role == "user" and content:
|
|
1127
|
+
user_request = content
|
|
1128
|
+
break
|
|
1129
|
+
|
|
1130
|
+
first_line = user_request.split('\n')[0][:40] if user_request else "Empty"
|
|
1131
|
+
if user_request and len(user_request.split('\n')[0]) > 40:
|
|
1132
|
+
first_line += "..."
|
|
1133
|
+
|
|
1134
|
+
# Relevance score if available
|
|
1135
|
+
relevance_text = f" [{conv.search_relevance:.0%}]" if conv.search_relevance else ""
|
|
1136
|
+
|
|
1137
|
+
session_items.append({
|
|
1138
|
+
"id": conv.session_id,
|
|
1139
|
+
"title": f"[{time_str}] {first_line}{relevance_text}",
|
|
1140
|
+
"subtitle": f"{conv.message_count} msgs | {conv.duration or '?'} | {project_name} | {conv.git_branch or '-'}",
|
|
1141
|
+
"preview": conv.last_message_preview[:80],
|
|
1142
|
+
"metadata": {
|
|
1143
|
+
"session_id": conv.session_id,
|
|
1144
|
+
"file_id": conv.file_id,
|
|
1145
|
+
"search_relevance": conv.search_relevance
|
|
1146
|
+
}
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
return {
|
|
1150
|
+
"title": f"Search Results: {query}",
|
|
1151
|
+
"footer": "↑↓ navigate • Enter select • Esc back",
|
|
1152
|
+
"width": 80,
|
|
1153
|
+
"height": 20,
|
|
1154
|
+
"sections": [
|
|
1155
|
+
{
|
|
1156
|
+
"title": f"Found {len(conversations)} conversations",
|
|
1157
|
+
"type": "session_list",
|
|
1158
|
+
"sessions": session_items
|
|
1159
|
+
}
|
|
1160
|
+
],
|
|
1161
|
+
"actions": [
|
|
1162
|
+
{"key": "Enter", "label": "Resume", "action": "select"},
|
|
1163
|
+
{"key": "Escape", "label": "Back", "action": "cancel"}
|
|
1164
|
+
]
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
def _build_filtered_modal(self, conversations: List[ConversationMetadata], filters: Dict) -> Dict[str, Any]:
|
|
1168
|
+
"""Build filtered results modal definition.
|
|
1169
|
+
|
|
1170
|
+
Args:
|
|
1171
|
+
conversations: Filtered conversations
|
|
1172
|
+
filters: Applied filters
|
|
1173
|
+
|
|
1174
|
+
Returns:
|
|
1175
|
+
Modal definition dictionary.
|
|
1176
|
+
"""
|
|
1177
|
+
filter_desc = []
|
|
1178
|
+
if "date" in filters:
|
|
1179
|
+
filter_desc.append(f"Date: {filters['date']}")
|
|
1180
|
+
if "date_range" in filters:
|
|
1181
|
+
start, end = filters["date_range"]
|
|
1182
|
+
filter_desc.append(f"Date: {start} to {end}")
|
|
1183
|
+
|
|
1184
|
+
filter_text = " • ".join(filter_desc) if filter_desc else "Filtered"
|
|
1185
|
+
|
|
1186
|
+
session_items = []
|
|
1187
|
+
for conv in conversations:
|
|
1188
|
+
# Format time for display
|
|
1189
|
+
time_str = "Unknown"
|
|
1190
|
+
if conv.created_time:
|
|
1191
|
+
time_str = conv.created_time.strftime("%m/%d %H:%M")
|
|
1192
|
+
|
|
1193
|
+
# Extract short project name
|
|
1194
|
+
project_name = conv.working_directory.split("/")[-1] if conv.working_directory else "unknown"
|
|
1195
|
+
|
|
1196
|
+
# Get user's actual request (skip system prompt if it's first)
|
|
1197
|
+
user_request = ""
|
|
1198
|
+
if conv.preview_messages:
|
|
1199
|
+
for msg in conv.preview_messages:
|
|
1200
|
+
role = msg.get("role", "")
|
|
1201
|
+
content = msg.get("content", "")
|
|
1202
|
+
# Skip system messages - look for first user message
|
|
1203
|
+
if role == "system":
|
|
1204
|
+
continue
|
|
1205
|
+
if role == "user" and content:
|
|
1206
|
+
user_request = content
|
|
1207
|
+
break
|
|
1208
|
+
|
|
1209
|
+
first_line = user_request.split('\n')[0][:50] if user_request else "Empty"
|
|
1210
|
+
if user_request and len(user_request.split('\n')[0]) > 50:
|
|
1211
|
+
first_line += "..."
|
|
1212
|
+
|
|
1213
|
+
session_items.append({
|
|
1214
|
+
"id": conv.session_id,
|
|
1215
|
+
"title": f"[{time_str}] {first_line}",
|
|
1216
|
+
"subtitle": f"{conv.message_count} msgs | {conv.duration or '?'} | {project_name} | {conv.git_branch or '-'}",
|
|
1217
|
+
"preview": conv.last_message_preview[:80],
|
|
1218
|
+
"metadata": {
|
|
1219
|
+
"session_id": conv.session_id,
|
|
1220
|
+
"file_id": conv.file_id
|
|
1221
|
+
}
|
|
1222
|
+
})
|
|
1223
|
+
|
|
1224
|
+
return {
|
|
1225
|
+
"title": f"Filtered Conversations ({filter_text})",
|
|
1226
|
+
"footer": "↑↓ navigate • Enter select • Esc back",
|
|
1227
|
+
"width": 80,
|
|
1228
|
+
"height": 20,
|
|
1229
|
+
"sections": [
|
|
1230
|
+
{
|
|
1231
|
+
"title": f"Showing {len(conversations)} conversations",
|
|
1232
|
+
"type": "session_list",
|
|
1233
|
+
"sessions": session_items
|
|
1234
|
+
}
|
|
1235
|
+
],
|
|
1236
|
+
"actions": [
|
|
1237
|
+
{"key": "Enter", "label": "Resume", "action": "select"},
|
|
1238
|
+
{"key": "Escape", "label": "Back", "action": "cancel"}
|
|
1239
|
+
]
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
def _generate_session_title(self, session_data: Dict) -> str:
|
|
1243
|
+
"""Generate a descriptive title for a session.
|
|
1244
|
+
|
|
1245
|
+
Args:
|
|
1246
|
+
session_data: Session data
|
|
1247
|
+
|
|
1248
|
+
Returns:
|
|
1249
|
+
Generated title
|
|
1250
|
+
"""
|
|
1251
|
+
topics = session_data.get("topics", [])
|
|
1252
|
+
working_dir = session_data.get("working_directory", "unknown")
|
|
1253
|
+
|
|
1254
|
+
# Extract project name from working directory
|
|
1255
|
+
project_name = working_dir.split("/")[-1] if working_dir != "unknown" else "Unknown Project"
|
|
1256
|
+
|
|
1257
|
+
# Use topic if available, otherwise use generic title
|
|
1258
|
+
if topics:
|
|
1259
|
+
main_topic = topics[0].replace("_", " ").title()
|
|
1260
|
+
return f"{main_topic} - {project_name}"
|
|
1261
|
+
else:
|
|
1262
|
+
return f"Conversation - {project_name}"
|
|
1263
|
+
|
|
1264
|
+
def _generate_file_id(self, session_id: str) -> str:
|
|
1265
|
+
"""Generate short file ID for display.
|
|
1266
|
+
|
|
1267
|
+
Args:
|
|
1268
|
+
session_id: Full session ID
|
|
1269
|
+
|
|
1270
|
+
Returns:
|
|
1271
|
+
Short file ID
|
|
1272
|
+
"""
|
|
1273
|
+
# Simple hash to generate a consistent short ID
|
|
1274
|
+
hash_val = hash(session_id) % 100000
|
|
1275
|
+
return f"#{hash_val:05d}"
|
|
1276
|
+
|
|
1277
|
+
def _parse_datetime(self, dt_str: Optional[str]) -> Optional[datetime]:
|
|
1278
|
+
"""Parse datetime string.
|
|
1279
|
+
|
|
1280
|
+
Args:
|
|
1281
|
+
dt_str: Datetime string
|
|
1282
|
+
|
|
1283
|
+
Returns:
|
|
1284
|
+
Parsed datetime or None
|
|
1285
|
+
"""
|
|
1286
|
+
if not dt_str:
|
|
1287
|
+
return None
|
|
1288
|
+
|
|
1289
|
+
try:
|
|
1290
|
+
from datetime import datetime
|
|
1291
|
+
# Handle ISO format with Z
|
|
1292
|
+
if dt_str.endswith('Z'):
|
|
1293
|
+
dt_str = dt_str.replace('Z', '+00:00')
|
|
1294
|
+
return datetime.fromisoformat(dt_str)
|
|
1295
|
+
except:
|
|
1296
|
+
return None
|
|
1297
|
+
|
|
1298
|
+
async def shutdown(self) -> None:
|
|
1299
|
+
"""Shutdown the plugin and cleanup resources."""
|
|
1300
|
+
try:
|
|
1301
|
+
self.logger.info("Resume conversation plugin shutdown completed")
|
|
1302
|
+
except Exception as e:
|
|
1303
|
+
self.logger.error(f"Error shutting down resume conversation plugin: {e}")
|
|
1304
|
+
|
|
1305
|
+
@staticmethod
|
|
1306
|
+
def get_default_config() -> Dict[str, Any]:
|
|
1307
|
+
"""Get default configuration for resume conversation plugin.
|
|
1308
|
+
|
|
1309
|
+
Returns:
|
|
1310
|
+
Default configuration dictionary.
|
|
1311
|
+
"""
|
|
1312
|
+
return {
|
|
1313
|
+
"plugins": {
|
|
1314
|
+
"resume_conversation": {
|
|
1315
|
+
"enabled": True,
|
|
1316
|
+
"max_conversations": 50,
|
|
1317
|
+
"preview_length": 80,
|
|
1318
|
+
"auto_save_current": True,
|
|
1319
|
+
"confirm_load": True,
|
|
1320
|
+
"session_retention_days": 30
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async def register_hooks(self) -> None:
|
|
1326
|
+
"""Register event hooks for the plugin."""
|
|
1327
|
+
from core.events.models import EventType, Hook
|
|
1328
|
+
hook = Hook(
|
|
1329
|
+
name="resume_modal_command",
|
|
1330
|
+
plugin_name="resume_conversation",
|
|
1331
|
+
event_type=EventType.MODAL_COMMAND_SELECTED,
|
|
1332
|
+
priority=10,
|
|
1333
|
+
callback=self._handle_modal_command
|
|
1334
|
+
)
|
|
1335
|
+
await self.event_bus.register_hook(hook)
|
|
1336
|
+
|
|
1337
|
+
async def _handle_modal_command(self, data: Dict[str, Any], event: Event) -> Dict[str, Any]:
|
|
1338
|
+
"""Handle modal command selection events.
|
|
1339
|
+
|
|
1340
|
+
Args:
|
|
1341
|
+
data: Event data containing command info.
|
|
1342
|
+
event: Event object.
|
|
1343
|
+
|
|
1344
|
+
Returns:
|
|
1345
|
+
Modified data dict with display_messages or show_modal keys.
|
|
1346
|
+
"""
|
|
1347
|
+
command = data.get("command", {})
|
|
1348
|
+
action = command.get("action")
|
|
1349
|
+
|
|
1350
|
+
if action == "resume_session":
|
|
1351
|
+
session_id = command.get("session_id") or command.get("metadata", {}).get("session_id")
|
|
1352
|
+
if session_id:
|
|
1353
|
+
self.logger.info(f"[RESUME] Modal selected session: {session_id}")
|
|
1354
|
+
|
|
1355
|
+
if self._ensure_conversation_manager() and self.conversation_manager.load_session(session_id):
|
|
1356
|
+
# Load messages into llm_service history
|
|
1357
|
+
display_messages = self._prepare_session_display(
|
|
1358
|
+
header=f"--- Resumed session: {session_id} ---",
|
|
1359
|
+
success_msg=f"[ok] Resumed: {session_id}. Continue below."
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
if display_messages:
|
|
1363
|
+
# Convert display tuples to ADD_MESSAGE format
|
|
1364
|
+
messages = [
|
|
1365
|
+
{"role": role, "content": content}
|
|
1366
|
+
for role, content, _ in display_messages
|
|
1367
|
+
]
|
|
1368
|
+
|
|
1369
|
+
# Use ADD_MESSAGE event for unified display with loading
|
|
1370
|
+
from core.events.models import EventType
|
|
1371
|
+
await self.event_bus.emit_with_hooks(
|
|
1372
|
+
EventType.ADD_MESSAGE,
|
|
1373
|
+
{
|
|
1374
|
+
"messages": messages,
|
|
1375
|
+
"options": {
|
|
1376
|
+
"show_loading": True,
|
|
1377
|
+
"loading_message": "Loading conversation...",
|
|
1378
|
+
"log_messages": False, # Already logged
|
|
1379
|
+
"add_to_history": False, # Already loaded by _prepare_session_display
|
|
1380
|
+
"display_messages": True
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
"resume_plugin"
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
elif action == "branch_select_session":
|
|
1387
|
+
session_id = command.get("session_id") or command.get("metadata", {}).get("session_id")
|
|
1388
|
+
if session_id:
|
|
1389
|
+
self.logger.info(f"[BRANCH] Modal selected session for branch: {session_id}")
|
|
1390
|
+
result = await self._show_branch_point_selector(session_id)
|
|
1391
|
+
if result and result.ui_config and result.display_type == "modal":
|
|
1392
|
+
data["show_modal"] = result.ui_config.modal_config
|
|
1393
|
+
|
|
1394
|
+
elif action == "search":
|
|
1395
|
+
# Show search options modal
|
|
1396
|
+
self.logger.info("[RESUME] Search action triggered")
|
|
1397
|
+
data["show_modal"] = {
|
|
1398
|
+
"title": "Search Conversations",
|
|
1399
|
+
"footer": "Enter select • Esc back",
|
|
1400
|
+
"width": 80,
|
|
1401
|
+
"height": 12,
|
|
1402
|
+
"sections": [
|
|
1403
|
+
{
|
|
1404
|
+
"title": "Search by command",
|
|
1405
|
+
"commands": [
|
|
1406
|
+
{"name": "/resume search <query>", "description": "Search conversation content"},
|
|
1407
|
+
{"name": "/resume search git", "description": "Example: find git-related chats"},
|
|
1408
|
+
{"name": "/resume search modal", "description": "Example: find modal discussions"},
|
|
1409
|
+
]
|
|
1410
|
+
}
|
|
1411
|
+
]
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
elif action == "filter":
|
|
1415
|
+
# Show filter options modal
|
|
1416
|
+
self.logger.info("[RESUME] Filter action triggered")
|
|
1417
|
+
data["show_modal"] = {
|
|
1418
|
+
"title": "Filter Conversations",
|
|
1419
|
+
"footer": "Enter select • Esc back",
|
|
1420
|
+
"width": 80,
|
|
1421
|
+
"height": 14,
|
|
1422
|
+
"sections": [
|
|
1423
|
+
{
|
|
1424
|
+
"title": "Filter options",
|
|
1425
|
+
"commands": [
|
|
1426
|
+
{"name": "Today's conversations", "description": "Show only today", "action": "filter_today"},
|
|
1427
|
+
{"name": "This week", "description": "Show this week's conversations", "action": "filter_week"},
|
|
1428
|
+
{"name": "Show more", "description": "Show up to 50 conversations", "action": "filter_limit"},
|
|
1429
|
+
]
|
|
1430
|
+
}
|
|
1431
|
+
]
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
elif action == "filter_today":
|
|
1435
|
+
self.logger.info("[RESUME] Filter today triggered")
|
|
1436
|
+
result = await self._handle_resume_options(["--today"])
|
|
1437
|
+
if result and result.ui_config and result.display_type == "modal":
|
|
1438
|
+
data["show_modal"] = result.ui_config.modal_config
|
|
1439
|
+
|
|
1440
|
+
elif action == "filter_week":
|
|
1441
|
+
self.logger.info("[RESUME] Filter week triggered")
|
|
1442
|
+
result = await self._handle_resume_options(["--week"])
|
|
1443
|
+
if result and result.ui_config and result.display_type == "modal":
|
|
1444
|
+
data["show_modal"] = result.ui_config.modal_config
|
|
1445
|
+
|
|
1446
|
+
elif action == "filter_limit":
|
|
1447
|
+
self.logger.info("[RESUME] Filter limit triggered")
|
|
1448
|
+
result = await self._handle_resume_options(["--limit", "50"])
|
|
1449
|
+
if result and result.ui_config and result.display_type == "modal":
|
|
1450
|
+
data["show_modal"] = result.ui_config.modal_config
|
|
1451
|
+
|
|
1452
|
+
elif action == "branch_execute":
|
|
1453
|
+
metadata = command.get("metadata", {})
|
|
1454
|
+
session_id = metadata.get("session_id")
|
|
1455
|
+
message_index = metadata.get("message_index")
|
|
1456
|
+
if session_id is not None and message_index is not None:
|
|
1457
|
+
self.logger.info(f"[BRANCH] Executing branch: {session_id} at {message_index}")
|
|
1458
|
+
if self._ensure_conversation_manager():
|
|
1459
|
+
result = self.conversation_manager.branch_session(session_id, message_index)
|
|
1460
|
+
if result.get("success"):
|
|
1461
|
+
new_session_id = result.get("session_id")
|
|
1462
|
+
display_messages = self._prepare_session_display(
|
|
1463
|
+
header=f"--- Branched from {session_id} at message {message_index} ---",
|
|
1464
|
+
success_msg=(
|
|
1465
|
+
f"[ok] Created branch: {new_session_id}\n"
|
|
1466
|
+
f" From: {session_id} at message {result['branch_point']}\n"
|
|
1467
|
+
f" Loaded {result['message_count']} messages. Continue below."
|
|
1468
|
+
)
|
|
1469
|
+
)
|
|
1470
|
+
if display_messages:
|
|
1471
|
+
# Convert display tuples to ADD_MESSAGE format
|
|
1472
|
+
messages = [
|
|
1473
|
+
{"role": role, "content": content}
|
|
1474
|
+
for role, content, _ in display_messages
|
|
1475
|
+
]
|
|
1476
|
+
|
|
1477
|
+
# Use ADD_MESSAGE event for unified display with loading
|
|
1478
|
+
from core.events.models import EventType
|
|
1479
|
+
await self.event_bus.emit_with_hooks(
|
|
1480
|
+
EventType.ADD_MESSAGE,
|
|
1481
|
+
{
|
|
1482
|
+
"messages": messages,
|
|
1483
|
+
"options": {
|
|
1484
|
+
"show_loading": True,
|
|
1485
|
+
"loading_message": "Creating branch...",
|
|
1486
|
+
"log_messages": False, # Already logged
|
|
1487
|
+
"add_to_history": False, # Already loaded by _prepare_session_display
|
|
1488
|
+
"display_messages": True
|
|
1489
|
+
}
|
|
1490
|
+
},
|
|
1491
|
+
"resume_plugin"
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1494
|
+
return data
|