agentcrew-ai 0.8.13__py3-none-any.whl → 0.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. AgentCrew/__init__.py +1 -1
  2. AgentCrew/app.py +46 -634
  3. AgentCrew/main_docker.py +1 -30
  4. AgentCrew/modules/a2a/common/client/card_resolver.py +27 -8
  5. AgentCrew/modules/a2a/server.py +5 -0
  6. AgentCrew/modules/a2a/task_manager.py +1 -0
  7. AgentCrew/modules/agents/local_agent.py +2 -2
  8. AgentCrew/modules/chat/message/command_processor.py +33 -8
  9. AgentCrew/modules/chat/message/conversation.py +18 -1
  10. AgentCrew/modules/chat/message/handler.py +5 -1
  11. AgentCrew/modules/code_analysis/service.py +50 -7
  12. AgentCrew/modules/code_analysis/tool.py +9 -8
  13. AgentCrew/modules/console/completers.py +5 -1
  14. AgentCrew/modules/console/console_ui.py +23 -11
  15. AgentCrew/modules/console/conversation_browser/__init__.py +9 -0
  16. AgentCrew/modules/console/conversation_browser/browser.py +84 -0
  17. AgentCrew/modules/console/conversation_browser/browser_input_handler.py +279 -0
  18. AgentCrew/modules/console/{conversation_browser.py → conversation_browser/browser_ui.py} +249 -163
  19. AgentCrew/modules/console/conversation_handler.py +34 -1
  20. AgentCrew/modules/console/display_handlers.py +127 -7
  21. AgentCrew/modules/console/visual_mode/__init__.py +5 -0
  22. AgentCrew/modules/console/visual_mode/viewer.py +41 -0
  23. AgentCrew/modules/console/visual_mode/viewer_input_handler.py +315 -0
  24. AgentCrew/modules/console/visual_mode/viewer_ui.py +608 -0
  25. AgentCrew/modules/gui/components/command_handler.py +137 -29
  26. AgentCrew/modules/gui/components/menu_components.py +8 -7
  27. AgentCrew/modules/gui/themes/README.md +30 -14
  28. AgentCrew/modules/gui/themes/__init__.py +2 -1
  29. AgentCrew/modules/gui/themes/atom_light.yaml +1287 -0
  30. AgentCrew/modules/gui/themes/catppuccin.yaml +1276 -0
  31. AgentCrew/modules/gui/themes/dracula.yaml +1262 -0
  32. AgentCrew/modules/gui/themes/nord.yaml +1267 -0
  33. AgentCrew/modules/gui/themes/saigontech.yaml +1268 -0
  34. AgentCrew/modules/gui/themes/style_provider.py +78 -264
  35. AgentCrew/modules/gui/themes/theme_loader.py +379 -0
  36. AgentCrew/modules/gui/themes/unicorn.yaml +1276 -0
  37. AgentCrew/modules/gui/widgets/configs/global_settings.py +4 -4
  38. AgentCrew/modules/gui/widgets/history_sidebar.py +6 -1
  39. AgentCrew/modules/llm/constants.py +28 -9
  40. AgentCrew/modules/mcpclient/service.py +0 -1
  41. AgentCrew/modules/memory/base_service.py +13 -0
  42. AgentCrew/modules/memory/chroma_service.py +50 -0
  43. AgentCrew/setup.py +470 -0
  44. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/METADATA +1 -1
  45. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/RECORD +49 -40
  46. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/WHEEL +1 -1
  47. AgentCrew/modules/gui/themes/atom_light.py +0 -1365
  48. AgentCrew/modules/gui/themes/catppuccin.py +0 -1404
  49. AgentCrew/modules/gui/themes/dracula.py +0 -1372
  50. AgentCrew/modules/gui/themes/nord.py +0 -1365
  51. AgentCrew/modules/gui/themes/saigontech.py +0 -1359
  52. AgentCrew/modules/gui/themes/unicorn.py +0 -1372
  53. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/entry_points.txt +0 -0
  54. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/licenses/LICENSE +0 -0
  55. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/top_level.txt +0 -0
AgentCrew/main_docker.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import click
2
2
  import os
3
3
  import sys
4
+ from AgentCrew.app import common_options
4
5
 
5
6
 
6
7
  def _custom_unraisable_hook(unraisable):
@@ -48,36 +49,6 @@ def cli():
48
49
  )
49
50
 
50
51
 
51
- def common_options(func):
52
- import functools
53
-
54
- @click.option(
55
- "--provider",
56
- type=click.Choice(PROVIDER_LIST),
57
- default=None,
58
- help="LLM provider to use (claude, groq, openai, google, github_copilot, or deepinfra)",
59
- )
60
- @click.option(
61
- "--agent-config", default=None, help="Path/URL to the agent configuration file."
62
- )
63
- @click.option(
64
- "--mcp-config", default=None, help="Path to the mcp servers configuration file."
65
- )
66
- @click.option(
67
- "--memory-llm",
68
- type=click.Choice(
69
- ["claude", "groq", "openai", "google", "deepinfra", "github_copilot"]
70
- ),
71
- default=None,
72
- help="LLM Model use for analyzing and processing memory",
73
- )
74
- @functools.wraps(func)
75
- def wrapper(*args, **kwargs):
76
- return func(*args, **kwargs)
77
-
78
- return wrapper
79
-
80
-
81
52
  def cli_prod():
82
53
  if sys.argv[1] == "--version":
83
54
  click.echo(f"AgentCrew version: {get_current_version()}")
@@ -4,17 +4,36 @@ import httpx
4
4
 
5
5
  from a2a.types import AgentCard
6
6
 
7
+ DEFAULT_AGENT_CARD_PATHS = [
8
+ "/.well-known/agent-card.json",
9
+ "/.well-known/agent.json",
10
+ ]
11
+
7
12
 
8
13
  class A2ACardResolver:
9
- def __init__(self, base_url, agent_card_path="/.well-known/agent.json"):
14
+ def __init__(self, base_url, agent_card_path: str | None = None):
10
15
  self.base_url = base_url.rstrip("/")
11
- self.agent_card_path = agent_card_path.lstrip("/")
16
+ self.agent_card_path = agent_card_path.lstrip("/") if agent_card_path else None
12
17
 
13
18
  def get_agent_card(self) -> AgentCard:
14
19
  with httpx.Client() as client:
15
- response = client.get(self.base_url + "/" + self.agent_card_path)
16
- response.raise_for_status()
17
- try:
18
- return AgentCard(**response.json())
19
- except json.JSONDecodeError as e:
20
- raise httpx.RequestError(str(e)) from e
20
+ if self.agent_card_path:
21
+ return self._fetch_agent_card(client, self.agent_card_path)
22
+
23
+ for path in DEFAULT_AGENT_CARD_PATHS:
24
+ try:
25
+ return self._fetch_agent_card(client, path.lstrip("/"))
26
+ except httpx.HTTPStatusError:
27
+ continue
28
+
29
+ raise httpx.RequestError(
30
+ f"Agent card not found at any of the default paths: {DEFAULT_AGENT_CARD_PATHS}"
31
+ )
32
+
33
+ def _fetch_agent_card(self, client: httpx.Client, path: str) -> AgentCard:
34
+ response = client.get(self.base_url + "/" + path)
35
+ response.raise_for_status()
36
+ try:
37
+ return AgentCard(**response.json())
38
+ except json.JSONDecodeError as e:
39
+ raise httpx.RequestError(str(e)) from e
@@ -82,6 +82,11 @@ class A2AServer:
82
82
  agent_routes = Mount(
83
83
  f"/{agent_name}",
84
84
  routes=[
85
+ Route(
86
+ "/.well-known/agent-card.json",
87
+ self._get_agent_card_factory(agent_name),
88
+ methods=["GET"],
89
+ ),
85
90
  Route(
86
91
  "/.well-known/agent.json",
87
92
  self._get_agent_card_factory(agent_name),
@@ -608,6 +608,7 @@ class AgentTaskManager(TaskManager):
608
608
 
609
609
  except Exception as e:
610
610
  logger.error(str(e))
611
+ logger.debug(self.task_history[task.context_id])
611
612
  # Handle errors
612
613
  task.status.state = TaskState.failed
613
614
  task.status.timestamp = datetime.now().isoformat()
@@ -672,7 +672,7 @@ Whenever condition on `when` clause in a **Behavior** matches, tailor your respo
672
672
  adaptive_messages["content"].append(
673
673
  {
674
674
  "type": "text",
675
- "text": f"Cwd structure:\n{dir_structure}",
675
+ "text": f"cwd `{os.getcwd()}` structure:\n{dir_structure}",
676
676
  }
677
677
  )
678
678
 
@@ -763,7 +763,7 @@ Whenever condition on `when` clause in a **Behavior** matches, tailor your respo
763
763
  continue
764
764
 
765
765
  if is_shrinkable and i < shrink_threshold:
766
- msg["content"] = "[REDACTED]"
766
+ msg["content"] = "[PRUNED]"
767
767
  continue
768
768
 
769
769
  # Check if content starts with [UNIQUE]
@@ -43,14 +43,8 @@ class CommandProcessor:
43
43
  return CommandResult(handled=True, clear_flag=True)
44
44
  elif user_input.lower().startswith("/copy"):
45
45
  return await self._handle_copy_command(user_input)
46
- elif user_input.lower() == "/debug":
47
- self.message_handler._notify(
48
- "debug_requested", self.message_handler.agent.clean_history
49
- )
50
- self.message_handler._notify(
51
- "debug_requested", self.message_handler.streamline_messages
52
- )
53
- return CommandResult(handled=True, clear_flag=True)
46
+ elif user_input.lower().startswith("/debug"):
47
+ return self._handle_debug_command(user_input)
54
48
  elif user_input.lower().startswith("/think "):
55
49
  try:
56
50
  budget = user_input[7:].strip()
@@ -759,6 +753,37 @@ class CommandProcessor:
759
753
  self.message_handler._notify("voice_recording_completed", None)
760
754
  return CommandResult(handled=True, clear_flag=True)
761
755
 
756
+ def _handle_debug_command(self, user_input: str) -> CommandResult:
757
+ """Handle /debug command with optional filtering.
758
+
759
+ Usage:
760
+ /debug - Show both agent and chat messages
761
+ /debug agent - Show only agent messages
762
+ /debug chat - Show only chat/streamline messages
763
+ """
764
+ parts = user_input.lower().split()
765
+ filter_type = parts[1] if len(parts) > 1 else None
766
+
767
+ if filter_type and filter_type not in ("agent", "chat"):
768
+ self.message_handler._notify(
769
+ "error", f"Invalid filter '{filter_type}'. Use 'agent' or 'chat'."
770
+ )
771
+ return CommandResult(handled=True, clear_flag=True)
772
+
773
+ if filter_type is None or filter_type == "agent":
774
+ self.message_handler._notify(
775
+ "debug_requested",
776
+ {"type": "agent", "messages": self.message_handler.agent.clean_history},
777
+ )
778
+
779
+ if filter_type is None or filter_type == "chat":
780
+ self.message_handler._notify(
781
+ "debug_requested",
782
+ {"type": "chat", "messages": self.message_handler.streamline_messages},
783
+ )
784
+
785
+ return CommandResult(handled=True, clear_flag=True)
786
+
762
787
  def _handle_toggle_transfer_command(self, user_input: str) -> CommandResult:
763
788
  """Handle /toggle_transfer command to toggle the enforce_transfer property of agent_manager."""
764
789
  try:
@@ -198,6 +198,7 @@ class ConversationManager:
198
198
  def delete_conversation_by_id(self, conversation_id: str) -> bool:
199
199
  """
200
200
  Deletes a conversation by its ID, handling file deletion and UI updates.
201
+ Also deletes associated memory data.
201
202
 
202
203
  Args:
203
204
  conversation_id: The ID of the conversation to delete.
@@ -215,6 +216,22 @@ class ConversationManager:
215
216
  logger.info(
216
217
  f"INFO: Successfully deleted conversation file for ID: {conversation_id}"
217
218
  )
219
+
220
+ if self.message_handler.memory_service:
221
+ memory_result = (
222
+ self.message_handler.memory_service.delete_by_conversation_id(
223
+ conversation_id
224
+ )
225
+ )
226
+ if memory_result.get("success"):
227
+ logger.info(
228
+ f"INFO: Deleted {memory_result.get('count', 0)} memories for conversation {conversation_id}"
229
+ )
230
+ else:
231
+ logger.warning(
232
+ f"WARNING: Failed to delete memories for conversation {conversation_id}: {memory_result.get('message')}"
233
+ )
234
+
218
235
  self.message_handler._notify("conversations_changed", None)
219
236
  self.message_handler._notify(
220
237
  "system_message", f"Conversation {conversation_id[:8]}... deleted."
@@ -224,7 +241,7 @@ class ConversationManager:
224
241
  logger.info(
225
242
  f"INFO: Deleted conversation {conversation_id} was the current one. Starting new conversation."
226
243
  )
227
- self.start_new_conversation() # This will notify "clear_requested"
244
+ self.start_new_conversation()
228
245
  return True
229
246
  else:
230
247
  error_msg = f"Failed to delete conversation {conversation_id[:8]}..."
@@ -470,9 +470,13 @@ class MessageHandler(Observable):
470
470
  if isinstance(e, BadRequestError):
471
471
  if e.code == "model_max_prompt_tokens_exceeded":
472
472
  from AgentCrew.modules.agents import LocalAgent
473
+ from AgentCrew.modules.llm.model_registry import ModelRegistry
473
474
 
474
475
  if isinstance(self.agent, LocalAgent):
475
- self.agent.input_tokens_usage = 128_000
476
+ max_token = ModelRegistry.get_model_limit(
477
+ self.agent.get_model()
478
+ )
479
+ self.agent.input_tokens_usage = max_token
476
480
  return await self.get_assistant_response()
477
481
  if self.current_user_input:
478
482
  self.conversation_manager.store_conversation_turn(
@@ -3,13 +3,22 @@ import fnmatch
3
3
  import subprocess
4
4
  import json
5
5
  import asyncio
6
- from typing import Any, Dict, List, Optional, TYPE_CHECKING
6
+ import base64
7
+ from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
7
8
  from loguru import logger
8
9
 
9
10
  from tree_sitter_language_pack import get_parser
10
11
  from tree_sitter import Parser
11
12
 
12
13
  from .parsers import get_parser_for_language, BaseLanguageParser
14
+ import mimetypes
15
+
16
+ IMAGE_MIME_TYPES = [
17
+ "image/jpeg",
18
+ "image/png",
19
+ "image/gif",
20
+ "image/webp",
21
+ ]
13
22
 
14
23
  if TYPE_CHECKING:
15
24
  from AgentCrew.modules.llm.base import BaseLLMService
@@ -59,6 +68,7 @@ class CodeAnalysisService:
59
68
  analyzing large repositories (>500 files).
60
69
  """
61
70
  self.llm_service = llm_service
71
+ self.file_handler = None
62
72
  if self.llm_service:
63
73
  if self.llm_service.provider_name == "google":
64
74
  self.llm_service.model = "gemini-2.5-flash-lite"
@@ -666,18 +676,49 @@ Example response format:
666
676
  file_path,
667
677
  start_line=None,
668
678
  end_line=None,
669
- ) -> Dict[str, str]:
679
+ ) -> Union[Tuple[str, str], Tuple[str, Dict[str, Any]]]:
670
680
  """
671
681
  Return the content of a file, optionally reading only a specific line range.
682
+ For document files (PDF, DOCX, XLSX, PPTX), uses Docling to convert
683
+ to text/markdown and ignores start_line/end_line parameters.
684
+ For image files, returns base64 encoded data in image_url format.
672
685
 
673
686
  Args:
674
687
  file_path: Path to the file to read
675
- start_line: Optional starting line number (1-indexed)
676
- end_line: Optional ending line number (1-indexed, inclusive)
688
+ start_line: Optional starting line number (1-indexed) - ignored for document files
689
+ end_line: Optional ending line number (1-indexed, inclusive) - ignored for document files
677
690
 
678
691
  Returns:
679
- Dictionary with file content (key: "file", value: file content string)
692
+ Tuple of (file_path, content) where content is either:
693
+ - str: text content for text/document files
694
+ - dict: {"type": "image_url", "image_url": {"url": "data:mime;base64,..."}} for images
680
695
  """
696
+
697
+ from AgentCrew.modules.chat.file_handler import (
698
+ FileHandler,
699
+ ALLOWED_MIME_TYPES,
700
+ )
701
+
702
+ mime_type, _ = mimetypes.guess_type(file_path)
703
+
704
+ if mime_type and mime_type in IMAGE_MIME_TYPES:
705
+ with open(file_path, "rb") as file:
706
+ binary_data = file.read()
707
+ base64_data = base64.b64encode(binary_data).decode("utf-8")
708
+ return file_path, {
709
+ "type": "image_url",
710
+ "image_url": {"url": f"data:{mime_type};base64,{base64_data}"},
711
+ }
712
+
713
+ if mime_type and mime_type in ALLOWED_MIME_TYPES:
714
+ if self.file_handler is None:
715
+ self.file_handler = FileHandler()
716
+ result = self.file_handler.process_file(file_path)
717
+ if result and "text" in result:
718
+ return file_path, result["text"]
719
+ elif result is None:
720
+ raise ValueError(f"Failed to process document file: {file_path}")
721
+
681
722
  with open(file_path, "rb") as file:
682
723
  content = file.read()
683
724
 
@@ -700,9 +741,11 @@ Example response format:
700
741
  end_line = total_lines
701
742
 
702
743
  selected_lines = lines[start_line - 1 : end_line]
703
- return {"file": "\n".join(selected_lines)}
744
+ return file_path, "\n".join(selected_lines)
745
+
746
+ return file_path, decoded_content
704
747
 
705
- return {"file": decoded_content}
748
+ return file_path, decoded_content
706
749
 
707
750
  def _build_file_tree(self, file_paths: List[str]) -> Dict[str, Any]:
708
751
  """Build a hierarchical tree structure from flat file paths.
@@ -109,7 +109,7 @@ def get_file_content_tool_definition(provider="claude"):
109
109
  Returns:
110
110
  Dict containing the tool definition
111
111
  """
112
- tool_description = "Gets the content of a file, or a specific lines within that file (function or class body). Use this to examine the logic of specific functions, the structure of classes, or the overall content of a file."
112
+ tool_description = "Gets the content of a file, or a specific lines within that file (function or class body). Use this to examine the logic of specific functions, the structure of classes, or the overall content of a file. Also supports reading document files (PDF, DOCX, XLSX, PPTX, images) which will be converted to text/markdown - for document files, start_line and end_line parameters are ignored."
113
113
 
114
114
  tool_arguments = {
115
115
  "file_path": {
@@ -157,7 +157,7 @@ def get_file_content_tool_handler(
157
157
  ):
158
158
  """Returns a function that handles the get_file_content tool."""
159
159
 
160
- def handler(**params) -> str:
160
+ def handler(**params):
161
161
  file_path = params.get("file_path", "./")
162
162
  start_line = params.get("start_line")
163
163
  end_line = params.get("end_line")
@@ -168,16 +168,17 @@ def get_file_content_tool_handler(
168
168
  if not os.path.isabs(file_path):
169
169
  file_path = os.path.abspath(file_path)
170
170
 
171
- results = code_analysis_service.get_file_content(
171
+ path, file_content = code_analysis_service.get_file_content(
172
172
  file_path, start_line=start_line, end_line=end_line
173
173
  )
174
174
 
175
- content = ""
176
-
177
- for path, code in results.items():
178
- content += f"{path}: {code}\n"
175
+ if isinstance(file_content, dict) and file_content.get("type") == "image_url":
176
+ return [
177
+ {"type": "text", "text": f"Image file: {path}"},
178
+ file_content,
179
+ ]
179
180
 
180
- return content
181
+ return f"`{path}`: {file_content}"
181
182
 
182
183
  return handler
183
184
 
@@ -174,7 +174,7 @@ class ChatCompleter(Completer):
174
174
  ),
175
175
  (
176
176
  "/debug",
177
- "Show debug information (agent history and streamline messages)",
177
+ "Show debug info (usage: /debug [agent|chat])",
178
178
  ),
179
179
  ("/think", "Set thinking budget (usage: /think <budget>)"),
180
180
  (
@@ -233,6 +233,10 @@ class ChatCompleter(Completer):
233
233
  "/delete_behavior",
234
234
  "Delete an adaptive behavior (usage: /delete_behavior <id>)",
235
235
  ),
236
+ (
237
+ "/visual",
238
+ "Open visual mode to view raw message content with vim-like navigation",
239
+ ),
236
240
  ("/exit", "Exit the application"),
237
241
  ("/quit", "Exit the application"),
238
242
  ]
@@ -71,14 +71,6 @@ class ConsoleUI(Observer):
71
71
  self.conversation_handler = ConversationHandler(self)
72
72
  self.command_handlers = CommandHandlers(self)
73
73
 
74
- def _get_conversation_history(self, conversation_id: str):
75
- """Get conversation history for preview in browser."""
76
- if self.message_handler.persistent_service:
77
- return self.message_handler.persistent_service.get_conversation_history(
78
- conversation_id
79
- )
80
- return None
81
-
82
74
  def listen(self, event: str, data: Any = None):
83
75
  """
84
76
  Update method required by the Observer interface. Handles events from the MessageHandler.
@@ -223,8 +215,10 @@ class ConsoleUI(Observer):
223
215
  )
224
216
  elif event == "conversations_listed":
225
217
  self.display_handlers.display_conversations(
226
- data
227
- ) # data is list of conversation metadata
218
+ data,
219
+ get_history_callback=self.conversation_handler.get_conversation_history,
220
+ delete_callback=self.conversation_handler.delete_conversations,
221
+ )
228
222
  self.conversation_handler.update_cached_conversations(data)
229
223
  elif event == "conversation_loaded":
230
224
  loaded_text = Text("Loaded conversation: ", style=RICH_STYLE_YELLOW)
@@ -466,7 +460,8 @@ class ConsoleUI(Observer):
466
460
  try:
467
461
  selected_id = self.display_handlers.display_conversations(
468
462
  conversations,
469
- get_history_callback=self._get_conversation_history,
463
+ get_history_callback=self.conversation_handler.get_conversation_history,
464
+ delete_callback=self.conversation_handler.delete_conversations,
470
465
  )
471
466
  if selected_id:
472
467
  self.conversation_handler.handle_load_conversation(
@@ -499,6 +494,23 @@ class ConsoleUI(Observer):
499
494
  self.print_welcome_message()
500
495
  continue
501
496
 
497
+ elif user_input.strip() == "/visual":
498
+ self.input_handler._stop_input_thread()
499
+ try:
500
+ from .visual_mode import VisualModeViewer
501
+
502
+ viewer = VisualModeViewer(
503
+ console=self.console,
504
+ on_copy=self.copy_to_clipboard,
505
+ )
506
+ viewer.set_messages(
507
+ self.message_handler.streamline_messages
508
+ )
509
+ viewer.show()
510
+ finally:
511
+ self.input_handler._start_input_thread()
512
+ continue
513
+
502
514
  # Handle toggle_session_yolo command directly (console only, session-based)
503
515
  elif user_input.strip() == "/toggle_session_yolo":
504
516
  self.command_handlers.handle_toggle_session_yolo_command()
@@ -0,0 +1,9 @@
1
+ from .browser import ConversationBrowser
2
+ from .browser_ui import ConversationBrowserUI
3
+ from .browser_input_handler import ConversationBrowserInputHandler
4
+
5
+ __all__ = [
6
+ "ConversationBrowser",
7
+ "ConversationBrowserUI",
8
+ "ConversationBrowserInputHandler",
9
+ ]
@@ -0,0 +1,84 @@
1
+ """Conversation browser with split-panel interface.
2
+
3
+ Provides Rich-based UI for listing and loading conversations with preview.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import List, Dict, Any, Optional, Callable
9
+
10
+ from rich.console import Console
11
+ from rich.text import Text
12
+
13
+ from ..constants import RICH_STYLE_YELLOW
14
+ from .browser_ui import ConversationBrowserUI
15
+ from .browser_input_handler import ConversationBrowserInputHandler
16
+
17
+
18
+ class ConversationBrowser:
19
+ """Interactive conversation browser with split-panel layout.
20
+
21
+ This class orchestrates the UI rendering and input handling components
22
+ to provide a complete interactive conversation browsing experience.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ console: Console,
28
+ get_conversation_history: Optional[
29
+ Callable[[str], List[Dict[str, Any]]]
30
+ ] = None,
31
+ on_delete: Optional[Callable[[List[str]], bool]] = None,
32
+ ):
33
+ """Initialize the conversation browser.
34
+
35
+ Args:
36
+ console: Rich console for rendering
37
+ get_conversation_history: Optional callback to fetch full conversation history
38
+ on_delete: Optional callback to delete conversations by IDs. Returns True if successful.
39
+ """
40
+ self._console = console
41
+ self._ui = ConversationBrowserUI(
42
+ console=console,
43
+ get_conversation_history=get_conversation_history,
44
+ )
45
+ self._input_handler = ConversationBrowserInputHandler(
46
+ ui=self._ui,
47
+ on_delete=on_delete,
48
+ )
49
+
50
+ def set_conversations(self, conversations: List[Dict[str, Any]]):
51
+ """Set the conversations list to browse."""
52
+ self._ui.set_conversations(conversations)
53
+
54
+ def get_selected_conversation_id(self) -> Optional[str]:
55
+ """Get the ID of the currently selected conversation."""
56
+ return self._ui.get_selected_conversation_id()
57
+
58
+ def get_selected_conversation_index(self) -> int:
59
+ """Get the 1-based index of the currently selected conversation."""
60
+ return self._ui.get_selected_conversation_index()
61
+
62
+ @property
63
+ def ui(self) -> ConversationBrowserUI:
64
+ """Access the UI component directly."""
65
+ return self._ui
66
+
67
+ @property
68
+ def input_handler(self) -> ConversationBrowserInputHandler:
69
+ """Access the input handler component directly."""
70
+ return self._input_handler
71
+
72
+ def show(self) -> Optional[str]:
73
+ """Show the interactive conversation browser.
74
+
75
+ Returns:
76
+ The ID of the selected conversation, or None if cancelled.
77
+ """
78
+ if not self._ui.conversations:
79
+ self._console.print(
80
+ Text("No conversations available.", style=RICH_STYLE_YELLOW)
81
+ )
82
+ return None
83
+
84
+ return self._input_handler.run()