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.
- AgentCrew/__init__.py +1 -1
- AgentCrew/app.py +46 -634
- AgentCrew/main_docker.py +1 -30
- AgentCrew/modules/a2a/common/client/card_resolver.py +27 -8
- AgentCrew/modules/a2a/server.py +5 -0
- AgentCrew/modules/a2a/task_manager.py +1 -0
- AgentCrew/modules/agents/local_agent.py +2 -2
- AgentCrew/modules/chat/message/command_processor.py +33 -8
- AgentCrew/modules/chat/message/conversation.py +18 -1
- AgentCrew/modules/chat/message/handler.py +5 -1
- AgentCrew/modules/code_analysis/service.py +50 -7
- AgentCrew/modules/code_analysis/tool.py +9 -8
- AgentCrew/modules/console/completers.py +5 -1
- AgentCrew/modules/console/console_ui.py +23 -11
- AgentCrew/modules/console/conversation_browser/__init__.py +9 -0
- AgentCrew/modules/console/conversation_browser/browser.py +84 -0
- AgentCrew/modules/console/conversation_browser/browser_input_handler.py +279 -0
- AgentCrew/modules/console/{conversation_browser.py → conversation_browser/browser_ui.py} +249 -163
- AgentCrew/modules/console/conversation_handler.py +34 -1
- AgentCrew/modules/console/display_handlers.py +127 -7
- AgentCrew/modules/console/visual_mode/__init__.py +5 -0
- AgentCrew/modules/console/visual_mode/viewer.py +41 -0
- AgentCrew/modules/console/visual_mode/viewer_input_handler.py +315 -0
- AgentCrew/modules/console/visual_mode/viewer_ui.py +608 -0
- AgentCrew/modules/gui/components/command_handler.py +137 -29
- AgentCrew/modules/gui/components/menu_components.py +8 -7
- AgentCrew/modules/gui/themes/README.md +30 -14
- AgentCrew/modules/gui/themes/__init__.py +2 -1
- AgentCrew/modules/gui/themes/atom_light.yaml +1287 -0
- AgentCrew/modules/gui/themes/catppuccin.yaml +1276 -0
- AgentCrew/modules/gui/themes/dracula.yaml +1262 -0
- AgentCrew/modules/gui/themes/nord.yaml +1267 -0
- AgentCrew/modules/gui/themes/saigontech.yaml +1268 -0
- AgentCrew/modules/gui/themes/style_provider.py +78 -264
- AgentCrew/modules/gui/themes/theme_loader.py +379 -0
- AgentCrew/modules/gui/themes/unicorn.yaml +1276 -0
- AgentCrew/modules/gui/widgets/configs/global_settings.py +4 -4
- AgentCrew/modules/gui/widgets/history_sidebar.py +6 -1
- AgentCrew/modules/llm/constants.py +28 -9
- AgentCrew/modules/mcpclient/service.py +0 -1
- AgentCrew/modules/memory/base_service.py +13 -0
- AgentCrew/modules/memory/chroma_service.py +50 -0
- AgentCrew/setup.py +470 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/METADATA +1 -1
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/RECORD +49 -40
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/WHEEL +1 -1
- AgentCrew/modules/gui/themes/atom_light.py +0 -1365
- AgentCrew/modules/gui/themes/catppuccin.py +0 -1404
- AgentCrew/modules/gui/themes/dracula.py +0 -1372
- AgentCrew/modules/gui/themes/nord.py +0 -1365
- AgentCrew/modules/gui/themes/saigontech.py +0 -1359
- AgentCrew/modules/gui/themes/unicorn.py +0 -1372
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/entry_points.txt +0 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {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=
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
AgentCrew/modules/a2a/server.py
CHANGED
|
@@ -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"
|
|
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"] = "[
|
|
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()
|
|
47
|
-
self.
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
|
744
|
+
return file_path, "\n".join(selected_lines)
|
|
745
|
+
|
|
746
|
+
return file_path, decoded_content
|
|
704
747
|
|
|
705
|
-
return
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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()
|