agentcrew-ai 0.8.13__py3-none-any.whl → 0.9.0__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 +34 -633
- AgentCrew/main_docker.py +1 -30
- AgentCrew/modules/agents/local_agent.py +2 -2
- AgentCrew/modules/chat/message/command_processor.py +33 -8
- AgentCrew/modules/chat/message/handler.py +5 -1
- AgentCrew/modules/console/completers.py +1 -1
- AgentCrew/modules/console/console_ui.py +6 -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 +123 -7
- AgentCrew/modules/gui/components/command_handler.py +137 -29
- 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 +76 -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 +3 -4
- AgentCrew/modules/llm/constants.py +13 -4
- AgentCrew/setup.py +470 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.dist-info}/METADATA +1 -1
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.dist-info}/RECORD +34 -29
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.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.0.dist-info}/entry_points.txt +0 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.0.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()}")
|
|
@@ -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:
|
|
@@ -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(
|
|
@@ -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
|
(
|
|
@@ -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(
|
|
@@ -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()
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Conversation browser input handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Optional, Callable, List
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit import PromptSession
|
|
8
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
9
|
+
from prompt_toolkit.keys import Keys
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .browser_ui import ConversationBrowserUI
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConversationBrowserInputHandler:
|
|
18
|
+
"""Handles keyboard input for the conversation browser."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
ui: ConversationBrowserUI,
|
|
23
|
+
on_select: Optional[Callable[[str], None]] = None,
|
|
24
|
+
on_cancel: Optional[Callable[[], None]] = None,
|
|
25
|
+
on_delete: Optional[Callable[[List[str]], bool]] = None,
|
|
26
|
+
):
|
|
27
|
+
self._ui = ui
|
|
28
|
+
self._running = False
|
|
29
|
+
self._g_pressed = False
|
|
30
|
+
self._d_pressed = False
|
|
31
|
+
self._selected_id: Optional[str] = None
|
|
32
|
+
self._on_select = on_select
|
|
33
|
+
self._on_cancel = on_cancel
|
|
34
|
+
self._on_delete = on_delete
|
|
35
|
+
|
|
36
|
+
def _create_key_bindings(self) -> KeyBindings:
|
|
37
|
+
"""Create and configure key bindings for the browser."""
|
|
38
|
+
kb = KeyBindings()
|
|
39
|
+
|
|
40
|
+
@kb.add(Keys.Up)
|
|
41
|
+
@kb.add("k")
|
|
42
|
+
def _(event):
|
|
43
|
+
if self._ui.search_mode:
|
|
44
|
+
return
|
|
45
|
+
self._g_pressed = False
|
|
46
|
+
self._d_pressed = False
|
|
47
|
+
if self._ui.handle_navigation("up"):
|
|
48
|
+
self._ui.render()
|
|
49
|
+
|
|
50
|
+
@kb.add(Keys.Down)
|
|
51
|
+
@kb.add("j")
|
|
52
|
+
def _(event):
|
|
53
|
+
if self._ui.search_mode:
|
|
54
|
+
return
|
|
55
|
+
self._g_pressed = False
|
|
56
|
+
self._d_pressed = False
|
|
57
|
+
if self._ui.handle_navigation("down"):
|
|
58
|
+
self._ui.render()
|
|
59
|
+
|
|
60
|
+
@kb.add(Keys.ControlP)
|
|
61
|
+
def _(event):
|
|
62
|
+
if self._ui.search_mode:
|
|
63
|
+
return
|
|
64
|
+
self._g_pressed = False
|
|
65
|
+
self._d_pressed = False
|
|
66
|
+
if self._ui.handle_navigation("up"):
|
|
67
|
+
self._ui.render()
|
|
68
|
+
|
|
69
|
+
@kb.add(Keys.ControlN)
|
|
70
|
+
def _(event):
|
|
71
|
+
if self._ui.search_mode:
|
|
72
|
+
return
|
|
73
|
+
self._g_pressed = False
|
|
74
|
+
self._d_pressed = False
|
|
75
|
+
if self._ui.handle_navigation("down"):
|
|
76
|
+
self._ui.render()
|
|
77
|
+
|
|
78
|
+
@kb.add("g")
|
|
79
|
+
def _(event):
|
|
80
|
+
if self._ui.search_mode:
|
|
81
|
+
self._ui.append_search_char("g")
|
|
82
|
+
self._ui.render()
|
|
83
|
+
return
|
|
84
|
+
self._d_pressed = False
|
|
85
|
+
if self._g_pressed:
|
|
86
|
+
self._g_pressed = False
|
|
87
|
+
if self._ui.handle_navigation("top"):
|
|
88
|
+
self._ui.render()
|
|
89
|
+
else:
|
|
90
|
+
self._g_pressed = True
|
|
91
|
+
|
|
92
|
+
@kb.add("G")
|
|
93
|
+
def _(event):
|
|
94
|
+
if self._ui.search_mode:
|
|
95
|
+
self._ui.append_search_char("G")
|
|
96
|
+
self._ui.render()
|
|
97
|
+
return
|
|
98
|
+
self._g_pressed = False
|
|
99
|
+
self._d_pressed = False
|
|
100
|
+
if self._ui.handle_navigation("bottom"):
|
|
101
|
+
self._ui.render()
|
|
102
|
+
|
|
103
|
+
@kb.add(Keys.ControlU)
|
|
104
|
+
@kb.add(Keys.PageUp)
|
|
105
|
+
def _(event):
|
|
106
|
+
if self._ui.search_mode:
|
|
107
|
+
return
|
|
108
|
+
self._g_pressed = False
|
|
109
|
+
self._d_pressed = False
|
|
110
|
+
if self._ui.handle_navigation("page_up"):
|
|
111
|
+
self._ui.render()
|
|
112
|
+
|
|
113
|
+
@kb.add(Keys.ControlD)
|
|
114
|
+
@kb.add(Keys.PageDown)
|
|
115
|
+
def _(event):
|
|
116
|
+
if self._ui.search_mode:
|
|
117
|
+
return
|
|
118
|
+
self._g_pressed = False
|
|
119
|
+
self._d_pressed = False
|
|
120
|
+
if self._ui.handle_navigation("page_down"):
|
|
121
|
+
self._ui.render()
|
|
122
|
+
|
|
123
|
+
@kb.add("v")
|
|
124
|
+
def _(event):
|
|
125
|
+
if self._ui.search_mode:
|
|
126
|
+
self._ui.append_search_char("v")
|
|
127
|
+
self._ui.render()
|
|
128
|
+
return
|
|
129
|
+
self._g_pressed = False
|
|
130
|
+
self._d_pressed = False
|
|
131
|
+
if self._ui.toggle_selection():
|
|
132
|
+
self._ui.render()
|
|
133
|
+
|
|
134
|
+
@kb.add("d")
|
|
135
|
+
def _(event):
|
|
136
|
+
if self._ui.search_mode:
|
|
137
|
+
self._ui.append_search_char("d")
|
|
138
|
+
self._ui.render()
|
|
139
|
+
return
|
|
140
|
+
self._g_pressed = False
|
|
141
|
+
if self._d_pressed:
|
|
142
|
+
self._d_pressed = False
|
|
143
|
+
self._handle_delete()
|
|
144
|
+
else:
|
|
145
|
+
self._d_pressed = True
|
|
146
|
+
|
|
147
|
+
@kb.add("/")
|
|
148
|
+
def _(event):
|
|
149
|
+
if self._ui.search_mode:
|
|
150
|
+
self._ui.append_search_char("/")
|
|
151
|
+
self._ui.render()
|
|
152
|
+
return
|
|
153
|
+
self._g_pressed = False
|
|
154
|
+
self._d_pressed = False
|
|
155
|
+
self._ui.start_search_mode()
|
|
156
|
+
self._ui.render()
|
|
157
|
+
|
|
158
|
+
@kb.add(Keys.Backspace)
|
|
159
|
+
def _(event):
|
|
160
|
+
if self._ui.search_mode:
|
|
161
|
+
self._ui.backspace_search()
|
|
162
|
+
self._ui.render()
|
|
163
|
+
|
|
164
|
+
@kb.add(Keys.Enter)
|
|
165
|
+
@kb.add("l")
|
|
166
|
+
def _(event):
|
|
167
|
+
self._g_pressed = False
|
|
168
|
+
self._d_pressed = False
|
|
169
|
+
if self._ui.search_mode:
|
|
170
|
+
self._ui.exit_search_mode(clear_filter=False)
|
|
171
|
+
self._ui.render()
|
|
172
|
+
return
|
|
173
|
+
self._selected_id = self._ui.get_selected_conversation_id()
|
|
174
|
+
event.app.exit()
|
|
175
|
+
|
|
176
|
+
@kb.add(Keys.Escape)
|
|
177
|
+
def _(event):
|
|
178
|
+
self._g_pressed = False
|
|
179
|
+
self._d_pressed = False
|
|
180
|
+
if self._ui.search_mode:
|
|
181
|
+
self._ui.exit_search_mode(clear_filter=True)
|
|
182
|
+
self._ui.render()
|
|
183
|
+
return
|
|
184
|
+
event.app.exit()
|
|
185
|
+
|
|
186
|
+
@kb.add("q")
|
|
187
|
+
def _(event):
|
|
188
|
+
if self._ui.search_mode:
|
|
189
|
+
self._ui.append_search_char("q")
|
|
190
|
+
self._ui.render()
|
|
191
|
+
return
|
|
192
|
+
self._g_pressed = False
|
|
193
|
+
self._d_pressed = False
|
|
194
|
+
event.app.exit()
|
|
195
|
+
|
|
196
|
+
@kb.add(Keys.ControlC)
|
|
197
|
+
def _(event):
|
|
198
|
+
self._g_pressed = False
|
|
199
|
+
self._d_pressed = False
|
|
200
|
+
if self._ui.search_mode:
|
|
201
|
+
self._ui.exit_search_mode(clear_filter=True)
|
|
202
|
+
self._ui.render()
|
|
203
|
+
return
|
|
204
|
+
event.app.exit()
|
|
205
|
+
|
|
206
|
+
@kb.add(Keys.Any)
|
|
207
|
+
def _(event):
|
|
208
|
+
if self._ui.search_mode:
|
|
209
|
+
char = event.data
|
|
210
|
+
if char and char.isprintable():
|
|
211
|
+
self._ui.append_search_char(char)
|
|
212
|
+
self._ui.render()
|
|
213
|
+
return
|
|
214
|
+
self._g_pressed = False
|
|
215
|
+
self._d_pressed = False
|
|
216
|
+
|
|
217
|
+
return kb
|
|
218
|
+
|
|
219
|
+
def _handle_delete(self):
|
|
220
|
+
"""Handle delete action for selected or current conversation."""
|
|
221
|
+
if not self._ui.conversations:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
if self._ui.selected_items:
|
|
225
|
+
indices_to_delete = list(self._ui.selected_items)
|
|
226
|
+
ids_to_delete = self._ui.get_selected_conversation_ids()
|
|
227
|
+
else:
|
|
228
|
+
indices_to_delete = [self._ui.selected_index]
|
|
229
|
+
current_id = self._ui.get_selected_conversation_id()
|
|
230
|
+
ids_to_delete = [current_id] if current_id else []
|
|
231
|
+
|
|
232
|
+
if not ids_to_delete:
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
if self._on_delete:
|
|
236
|
+
success = self._on_delete(ids_to_delete)
|
|
237
|
+
if success:
|
|
238
|
+
self._ui.remove_conversations(indices_to_delete)
|
|
239
|
+
self._ui.render()
|
|
240
|
+
else:
|
|
241
|
+
self._ui.remove_conversations(indices_to_delete)
|
|
242
|
+
self._ui.render()
|
|
243
|
+
|
|
244
|
+
def run(self) -> Optional[str]:
|
|
245
|
+
"""Run the input handler loop.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The ID of the selected conversation, or None if cancelled.
|
|
249
|
+
"""
|
|
250
|
+
self._running = True
|
|
251
|
+
self._g_pressed = False
|
|
252
|
+
self._d_pressed = False
|
|
253
|
+
self._selected_id = None
|
|
254
|
+
|
|
255
|
+
self._ui.start_live()
|
|
256
|
+
|
|
257
|
+
kb = self._create_key_bindings()
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
session = PromptSession(key_bindings=kb)
|
|
261
|
+
session.prompt("")
|
|
262
|
+
except (KeyboardInterrupt, EOFError):
|
|
263
|
+
pass
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.error(f"Error in conversation browser input handler: {e}")
|
|
266
|
+
finally:
|
|
267
|
+
self._ui.stop_live()
|
|
268
|
+
|
|
269
|
+
self._running = False
|
|
270
|
+
return self._selected_id
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def is_running(self) -> bool:
|
|
274
|
+
"""Check if the input handler is currently running."""
|
|
275
|
+
return self._running
|
|
276
|
+
|
|
277
|
+
def stop(self):
|
|
278
|
+
"""Stop the input handler."""
|
|
279
|
+
self._running = False
|