minion-code 0.1.0__py3-none-any.whl → 0.1.2__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.
- examples/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.2.dist-info/METADATA +476 -0
- minion_code-0.1.2.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
- minion_code-0.1.2.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -14,19 +14,19 @@ class MessageResponse(Container):
|
|
|
14
14
|
"""
|
|
15
15
|
MessageResponse component equivalent to React MessageResponse
|
|
16
16
|
Provides visual indentation with "⎿" indicator for children widgets
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
Usage:
|
|
19
19
|
# With direct content
|
|
20
20
|
response = MessageResponse(content="Operation completed")
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
# With child widgets (like React children)
|
|
23
23
|
response = MessageResponse()
|
|
24
24
|
response.mount(Message(...)) # Mount child widgets
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
# Or pass children during initialization
|
|
27
27
|
response = MessageResponse(children=[message_widget, status_widget])
|
|
28
28
|
"""
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
DEFAULT_CSS = """
|
|
31
31
|
MessageResponse {
|
|
32
32
|
height: auto;
|
|
@@ -71,35 +71,36 @@ class MessageResponse(Container):
|
|
|
71
71
|
padding: 0;
|
|
72
72
|
}
|
|
73
73
|
"""
|
|
74
|
-
|
|
75
|
-
def __init__(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
children: Optional[Union[Widget, List[Widget]]] = None,
|
|
78
|
+
content: Optional[str] = None,
|
|
79
|
+
**kwargs,
|
|
80
|
+
):
|
|
79
81
|
super().__init__(**kwargs)
|
|
80
82
|
self.children_widgets = []
|
|
81
83
|
self.content = content
|
|
82
|
-
|
|
84
|
+
|
|
83
85
|
# Handle children parameter
|
|
84
86
|
if children:
|
|
85
87
|
if isinstance(children, list):
|
|
86
88
|
self.children_widgets = children
|
|
87
89
|
else:
|
|
88
90
|
self.children_widgets = [children]
|
|
89
|
-
|
|
91
|
+
|
|
90
92
|
def compose(self):
|
|
91
93
|
"""Compose the MessageResponse interface"""
|
|
92
94
|
from textual.containers import Vertical
|
|
93
|
-
|
|
95
|
+
|
|
94
96
|
# 使用Vertical多层布局
|
|
95
97
|
with Horizontal(classes="message-response-container"):
|
|
96
|
-
#yield (Static(" ⎿", classes="response-indicator"))
|
|
98
|
+
# yield (Static(" ⎿", classes="response-indicator"))
|
|
97
99
|
if self.children_widgets:
|
|
98
100
|
|
|
99
101
|
for child in self.children_widgets:
|
|
100
102
|
yield child
|
|
101
103
|
|
|
102
|
-
|
|
103
104
|
# def on_mount(self):
|
|
104
105
|
# print("mounting")
|
|
105
106
|
# pass
|
|
@@ -110,12 +111,12 @@ class MessageResponse(Container):
|
|
|
110
111
|
"""
|
|
111
112
|
content_area = self.query_one("#content-area", Container)
|
|
112
113
|
content_area.mount(widget)
|
|
113
|
-
|
|
114
|
+
|
|
114
115
|
def mount_children(self, widgets: List[Widget]):
|
|
115
116
|
"""Mount multiple child widgets"""
|
|
116
117
|
for widget in widgets:
|
|
117
118
|
self.mount_child(widget)
|
|
118
|
-
|
|
119
|
+
|
|
119
120
|
def clear_children(self):
|
|
120
121
|
"""Clear all child widgets from content area"""
|
|
121
122
|
content_area = self.query_one("#content-area", Container)
|
|
@@ -125,51 +126,51 @@ class MessageResponse(Container):
|
|
|
125
126
|
|
|
126
127
|
class MessageResponseText(MessageResponse):
|
|
127
128
|
"""Specialized MessageResponse for text content"""
|
|
128
|
-
|
|
129
|
+
|
|
129
130
|
def __init__(self, text: str, **kwargs):
|
|
130
131
|
super().__init__(content=text, **kwargs)
|
|
131
132
|
|
|
132
133
|
|
|
133
134
|
class MessageResponseStatus(MessageResponse):
|
|
134
135
|
"""Specialized MessageResponse for status messages"""
|
|
135
|
-
|
|
136
|
+
|
|
136
137
|
def __init__(self, status: str, message: str = "", **kwargs):
|
|
137
138
|
status_icons = {
|
|
138
139
|
"loading": "⏳",
|
|
139
|
-
"success": "✅",
|
|
140
|
+
"success": "✅",
|
|
140
141
|
"error": "❌",
|
|
141
142
|
"warning": "⚠️",
|
|
142
143
|
"info": "ℹ️",
|
|
143
|
-
"thinking": "🤔"
|
|
144
|
+
"thinking": "🤔",
|
|
144
145
|
}
|
|
145
|
-
|
|
146
|
+
|
|
146
147
|
icon = status_icons.get(status, "•")
|
|
147
148
|
content = f"{icon} {message}" if message else icon
|
|
148
|
-
|
|
149
|
+
|
|
149
150
|
super().__init__(content=content, **kwargs)
|
|
150
151
|
|
|
151
152
|
|
|
152
153
|
class MessageResponseProgress(MessageResponse):
|
|
153
154
|
"""Specialized MessageResponse for progress indicators"""
|
|
154
|
-
|
|
155
|
+
|
|
155
156
|
def __init__(self, current: int, total: int, message: str = "", **kwargs):
|
|
156
157
|
progress_text = f"[{current}/{total}]"
|
|
157
158
|
if message:
|
|
158
159
|
progress_text += f" {message}"
|
|
159
|
-
|
|
160
|
+
|
|
160
161
|
super().__init__(content=progress_text, **kwargs)
|
|
161
162
|
|
|
162
163
|
|
|
163
164
|
class MessageResponseTyping(MessageResponse):
|
|
164
165
|
"""Specialized MessageResponse for typing indicators"""
|
|
165
|
-
|
|
166
|
+
|
|
166
167
|
def __init__(self, **kwargs):
|
|
167
168
|
super().__init__(content="typing...", **kwargs)
|
|
168
|
-
|
|
169
|
+
|
|
169
170
|
def on_mount(self):
|
|
170
171
|
"""Start typing animation when mounted"""
|
|
171
172
|
self._animate_typing()
|
|
172
|
-
|
|
173
|
+
|
|
173
174
|
def _animate_typing(self):
|
|
174
175
|
"""Simple typing animation"""
|
|
175
176
|
# This could be enhanced with actual animation
|
|
@@ -182,7 +183,7 @@ class MessageResponseWithChildren(MessageResponse):
|
|
|
182
183
|
Specialized MessageResponse that demonstrates children usage
|
|
183
184
|
Equivalent to React's <MessageResponse><Message /></MessageResponse>
|
|
184
185
|
"""
|
|
185
|
-
|
|
186
|
+
|
|
186
187
|
def __init__(self, message_widget: Widget, **kwargs):
|
|
187
188
|
# Pass the message widget as a child
|
|
188
|
-
super().__init__(children=[message_widget], **kwargs)
|
|
189
|
+
super().__init__(children=[message_widget], **kwargs)
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Messages Component - Python equivalent of React Messages component
|
|
3
|
+
Renders a list of messages in the REPL interface
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from textual.containers import Container, ScrollableContainer, Vertical
|
|
7
|
+
from textual.widgets import Static
|
|
8
|
+
from textual.reactive import reactive, var
|
|
9
|
+
from typing import List, Dict, Any, Optional, Set
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
# Import shared types and components
|
|
13
|
+
from ..type_defs import Message as MessageType, MessageContent, InputMode
|
|
14
|
+
from .Message import Message, UserMessage, AssistantMessage, ToolUseMessage
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Messages(ScrollableContainer):
|
|
18
|
+
"""
|
|
19
|
+
Messages container component equivalent to React Messages component
|
|
20
|
+
Renders a list of messages with proper scrolling and layout
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
DEFAULT_CSS = """
|
|
24
|
+
Messages {
|
|
25
|
+
height: 1fr;
|
|
26
|
+
width: 100%;
|
|
27
|
+
margin: 1;
|
|
28
|
+
padding: 1;
|
|
29
|
+
scrollbar-background: $surface-lighten-1;
|
|
30
|
+
scrollbar-color: $primary;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.messages-container {
|
|
34
|
+
width: 100%;
|
|
35
|
+
height: auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.empty-state {
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 100%;
|
|
41
|
+
content-align: center middle;
|
|
42
|
+
color: $text-muted;
|
|
43
|
+
text-style: dim;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.message-item {
|
|
47
|
+
width: 100%;
|
|
48
|
+
margin-bottom: 1;
|
|
49
|
+
}
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Reactive properties
|
|
53
|
+
messages = reactive(list, recompose=True) # List[MessageType]
|
|
54
|
+
# messages = vars(list) # List[MessageType]
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
messages: List[MessageType] = None,
|
|
59
|
+
tools: List[Any] = None,
|
|
60
|
+
verbose: bool = False,
|
|
61
|
+
debug: bool = False,
|
|
62
|
+
errored_tool_use_ids: Set[str] = None,
|
|
63
|
+
in_progress_tool_use_ids: Set[str] = None,
|
|
64
|
+
unresolved_tool_use_ids: Set[str] = None,
|
|
65
|
+
should_animate: bool = False,
|
|
66
|
+
auto_scroll: bool = True,
|
|
67
|
+
**kwargs,
|
|
68
|
+
):
|
|
69
|
+
super().__init__(**kwargs)
|
|
70
|
+
|
|
71
|
+
# Props equivalent to TypeScript Props interface
|
|
72
|
+
self._initial_messages = messages or []
|
|
73
|
+
print(
|
|
74
|
+
f"DEBUG: Messages component initialized with {len(self._initial_messages)} initial messages"
|
|
75
|
+
)
|
|
76
|
+
self.tools = tools or []
|
|
77
|
+
self.verbose = verbose
|
|
78
|
+
self.debug = debug
|
|
79
|
+
self.errored_tool_use_ids = errored_tool_use_ids or set()
|
|
80
|
+
self.in_progress_tool_use_ids = in_progress_tool_use_ids or set()
|
|
81
|
+
self.unresolved_tool_use_ids = unresolved_tool_use_ids or set()
|
|
82
|
+
self.should_animate = should_animate
|
|
83
|
+
self.auto_scroll = auto_scroll
|
|
84
|
+
|
|
85
|
+
# Internal state
|
|
86
|
+
self._last_message_count = 0
|
|
87
|
+
self._is_mounted = False
|
|
88
|
+
|
|
89
|
+
# Set messages after initialization to avoid watch_messages being called too early
|
|
90
|
+
if self._initial_messages:
|
|
91
|
+
self.messages = self._initial_messages.copy()
|
|
92
|
+
|
|
93
|
+
def compose(self):
|
|
94
|
+
"""Compose the messages interface - equivalent to React render method"""
|
|
95
|
+
print(f"DEBUG: Messages.compose() called with {len(self.messages)} messages")
|
|
96
|
+
if not self.messages:
|
|
97
|
+
# Empty state - equivalent to showing placeholder when no messages
|
|
98
|
+
print("DEBUG: Showing empty state")
|
|
99
|
+
yield Static(
|
|
100
|
+
"💬 Start a conversation by typing a message below...",
|
|
101
|
+
classes="empty-state",
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
# Messages container
|
|
105
|
+
print(f"DEBUG: Rendering {len(self.messages)} messages")
|
|
106
|
+
with Vertical(classes="messages-container"):
|
|
107
|
+
for i, message in enumerate(self.messages):
|
|
108
|
+
print(f"DEBUG: Creating message widget {i}: {message.type}")
|
|
109
|
+
yield self._create_message_widget(message, i)
|
|
110
|
+
|
|
111
|
+
def on_mount(self):
|
|
112
|
+
"""Called when the widget is mounted"""
|
|
113
|
+
self._is_mounted = True
|
|
114
|
+
# Now it's safe to update the display if needed
|
|
115
|
+
if self.messages != self._initial_messages:
|
|
116
|
+
self._update_display()
|
|
117
|
+
|
|
118
|
+
def _create_message_widget(self, message: MessageType, index: int) -> Message:
|
|
119
|
+
"""Create a message widget based on message type"""
|
|
120
|
+
|
|
121
|
+
# Common props for all message types
|
|
122
|
+
message_props = {
|
|
123
|
+
"message": message,
|
|
124
|
+
"messages": self.messages,
|
|
125
|
+
"tools": self.tools,
|
|
126
|
+
"verbose": self.verbose,
|
|
127
|
+
"debug": self.debug,
|
|
128
|
+
"errored_tool_use_ids": self.errored_tool_use_ids,
|
|
129
|
+
"in_progress_tool_use_ids": self.in_progress_tool_use_ids,
|
|
130
|
+
"unresolved_tool_use_ids": self.unresolved_tool_use_ids,
|
|
131
|
+
"should_animate": self.should_animate,
|
|
132
|
+
"classes": "message-item",
|
|
133
|
+
"id": f"message_{index}",
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Create appropriate message component based on type
|
|
137
|
+
if message.type.value == "user":
|
|
138
|
+
return UserMessage(**message_props)
|
|
139
|
+
elif message.type.value == "assistant":
|
|
140
|
+
# Check if this is a tool use message
|
|
141
|
+
if self._is_tool_use_message(message):
|
|
142
|
+
return ToolUseMessage(**message_props)
|
|
143
|
+
else:
|
|
144
|
+
return AssistantMessage(**message_props)
|
|
145
|
+
else:
|
|
146
|
+
# Default to generic Message component
|
|
147
|
+
return Message(**message_props)
|
|
148
|
+
|
|
149
|
+
def _is_tool_use_message(self, message: MessageType) -> bool:
|
|
150
|
+
"""Check if message contains tool use content"""
|
|
151
|
+
content = message.message.content
|
|
152
|
+
if isinstance(content, list):
|
|
153
|
+
return any(
|
|
154
|
+
isinstance(item, dict) and item.get("type") == "tool_use"
|
|
155
|
+
for item in content
|
|
156
|
+
)
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def add_message(self, message: MessageType):
|
|
160
|
+
"""Add a new message to the list"""
|
|
161
|
+
self.messages.append(message)
|
|
162
|
+
self.mutate_reactive(Messages.messages)
|
|
163
|
+
|
|
164
|
+
if self.auto_scroll:
|
|
165
|
+
self.call_later(self._scroll_to_bottom)
|
|
166
|
+
|
|
167
|
+
def update_messages(self, messages: List[MessageType]):
|
|
168
|
+
"""Update the entire messages list"""
|
|
169
|
+
# Clear and replace all messages
|
|
170
|
+
self.messages = messages
|
|
171
|
+
self.mutate_reactive(Messages.messages)
|
|
172
|
+
|
|
173
|
+
# Auto-scroll if new messages were added
|
|
174
|
+
if len(messages) > self._last_message_count and self.auto_scroll:
|
|
175
|
+
self.call_later(self._scroll_to_bottom)
|
|
176
|
+
|
|
177
|
+
self._last_message_count = len(messages)
|
|
178
|
+
|
|
179
|
+
def update_streaming_message(self, message_index: int, new_content: str):
|
|
180
|
+
"""Update a streaming message at specific index"""
|
|
181
|
+
if 0 <= message_index < len(self.messages):
|
|
182
|
+
# Update the message content
|
|
183
|
+
self.messages[message_index].message.content = new_content
|
|
184
|
+
|
|
185
|
+
# Find and update the corresponding widget
|
|
186
|
+
try:
|
|
187
|
+
message_widget = self.query_one(f"#message_{message_index}")
|
|
188
|
+
if hasattr(message_widget, "update_streaming_content"):
|
|
189
|
+
message_widget.update_streaming_content(new_content)
|
|
190
|
+
except Exception:
|
|
191
|
+
pass # Widget might not exist yet
|
|
192
|
+
|
|
193
|
+
def finalize_streaming_message(self, message_index: int, final_content: str):
|
|
194
|
+
"""Finalize a streaming message with final content"""
|
|
195
|
+
if 0 <= message_index < len(self.messages):
|
|
196
|
+
# Update the message content and remove streaming flag
|
|
197
|
+
message = self.messages[message_index]
|
|
198
|
+
message.message.content = final_content
|
|
199
|
+
if message.options:
|
|
200
|
+
message.options.pop("streaming", None)
|
|
201
|
+
|
|
202
|
+
# Find and update the corresponding widget
|
|
203
|
+
try:
|
|
204
|
+
message_widget = self.query_one(f"#message_{message_index}")
|
|
205
|
+
if hasattr(message_widget, "finalize_streaming"):
|
|
206
|
+
message_widget.finalize_streaming(final_content)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass # Widget might not exist yet
|
|
209
|
+
|
|
210
|
+
def clear_messages(self):
|
|
211
|
+
"""Clear all messages"""
|
|
212
|
+
self.messages = []
|
|
213
|
+
self.mutate_reactive(Messages.messages)
|
|
214
|
+
|
|
215
|
+
def _update_display(self):
|
|
216
|
+
"""Update the display when messages change"""
|
|
217
|
+
# Use recompose to rebuild the entire widget tree
|
|
218
|
+
self.recompose()
|
|
219
|
+
|
|
220
|
+
def _scroll_to_bottom(self):
|
|
221
|
+
"""Scroll to the bottom of the messages container"""
|
|
222
|
+
try:
|
|
223
|
+
self.scroll_end(animate=True)
|
|
224
|
+
except Exception:
|
|
225
|
+
pass # Silently handle scroll errors
|
|
226
|
+
|
|
227
|
+
def get_message_count(self) -> int:
|
|
228
|
+
"""Get the current number of messages"""
|
|
229
|
+
return len(self.messages)
|
|
230
|
+
|
|
231
|
+
def get_last_message(self) -> Optional[MessageType]:
|
|
232
|
+
"""Get the last message in the list"""
|
|
233
|
+
return self.messages[-1] if self.messages else None
|
|
234
|
+
|
|
235
|
+
def get_messages_by_type(self, message_type: str) -> List[MessageType]:
|
|
236
|
+
"""Get all messages of a specific type"""
|
|
237
|
+
return [msg for msg in self.messages if msg.type.value == message_type]
|
|
238
|
+
|
|
239
|
+
def find_message_by_id(self, message_id: str) -> Optional[MessageType]:
|
|
240
|
+
"""Find a message by its ID"""
|
|
241
|
+
for message in self.messages:
|
|
242
|
+
if hasattr(message, "id") and message.id == message_id:
|
|
243
|
+
return message
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
# Reactive property watchers
|
|
247
|
+
def watch_messages(self, messages: List[MessageType]):
|
|
248
|
+
"""Watch for changes to the messages list"""
|
|
249
|
+
# Only update display if the widget is mounted
|
|
250
|
+
if self._is_mounted:
|
|
251
|
+
self._update_display()
|
|
252
|
+
|
|
253
|
+
# Auto-scroll if new messages were added
|
|
254
|
+
if len(messages) > self._last_message_count and self.auto_scroll:
|
|
255
|
+
self.call_later(self._scroll_to_bottom)
|
|
256
|
+
|
|
257
|
+
self._last_message_count = len(messages)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class MessagesWithStatus(Container):
|
|
261
|
+
"""
|
|
262
|
+
Messages container with status indicators
|
|
263
|
+
Equivalent to a more advanced Messages component with loading states
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
DEFAULT_CSS = """
|
|
267
|
+
MessagesWithStatus {
|
|
268
|
+
height: 1fr;
|
|
269
|
+
width: 100%;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.status-bar {
|
|
273
|
+
dock: bottom;
|
|
274
|
+
height: 1;
|
|
275
|
+
background: $surface-lighten-1;
|
|
276
|
+
content-align: center middle;
|
|
277
|
+
color: $text-muted;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.typing-indicator {
|
|
281
|
+
color: $primary;
|
|
282
|
+
text-style: italic;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.error-indicator {
|
|
286
|
+
color: $error;
|
|
287
|
+
text-style: bold;
|
|
288
|
+
}
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
def __init__(
|
|
292
|
+
self,
|
|
293
|
+
messages: List[MessageType] = None,
|
|
294
|
+
is_loading: bool = False,
|
|
295
|
+
error_message: Optional[str] = None,
|
|
296
|
+
typing_indicator: Optional[str] = None,
|
|
297
|
+
**kwargs,
|
|
298
|
+
):
|
|
299
|
+
super().__init__(**kwargs)
|
|
300
|
+
|
|
301
|
+
self.messages = messages or []
|
|
302
|
+
self.is_loading = is_loading
|
|
303
|
+
self.error_message = error_message
|
|
304
|
+
self.typing_indicator = typing_indicator
|
|
305
|
+
|
|
306
|
+
def compose(self):
|
|
307
|
+
"""Compose messages with status bar"""
|
|
308
|
+
# Main messages component
|
|
309
|
+
yield Messages(messages=self.messages, id="main_messages")
|
|
310
|
+
|
|
311
|
+
# Status bar
|
|
312
|
+
yield self._render_status_bar()
|
|
313
|
+
|
|
314
|
+
def _render_status_bar(self) -> Static:
|
|
315
|
+
"""Render the status bar based on current state"""
|
|
316
|
+
if self.error_message:
|
|
317
|
+
return Static(
|
|
318
|
+
f"❌ {self.error_message}", classes="status-bar error-indicator"
|
|
319
|
+
)
|
|
320
|
+
elif self.is_loading:
|
|
321
|
+
return Static(
|
|
322
|
+
"⠋ Assistant is thinking...", classes="status-bar typing-indicator"
|
|
323
|
+
)
|
|
324
|
+
elif self.typing_indicator:
|
|
325
|
+
return Static(
|
|
326
|
+
f"⌨️ {self.typing_indicator}", classes="status-bar typing-indicator"
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
return Static("", classes="status-bar")
|
|
330
|
+
|
|
331
|
+
def update_status(
|
|
332
|
+
self,
|
|
333
|
+
is_loading: bool = None,
|
|
334
|
+
error_message: str = None,
|
|
335
|
+
typing_indicator: str = None,
|
|
336
|
+
):
|
|
337
|
+
"""Update the status indicators"""
|
|
338
|
+
if is_loading is not None:
|
|
339
|
+
self.is_loading = is_loading
|
|
340
|
+
if error_message is not None:
|
|
341
|
+
self.error_message = error_message
|
|
342
|
+
if typing_indicator is not None:
|
|
343
|
+
self.typing_indicator = typing_indicator
|
|
344
|
+
|
|
345
|
+
# Update status bar
|
|
346
|
+
try:
|
|
347
|
+
status_bar = self.query_one(".status-bar")
|
|
348
|
+
new_status_bar = self._render_status_bar()
|
|
349
|
+
status_bar.update(new_status_bar.renderable)
|
|
350
|
+
except Exception:
|
|
351
|
+
pass # Status bar might not be mounted yet
|