comate-cli 0.1.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.
Files changed (44) hide show
  1. comate_cli/__init__.py +5 -0
  2. comate_cli/__main__.py +5 -0
  3. comate_cli/main.py +128 -0
  4. comate_cli/terminal_agent/__init__.py +2 -0
  5. comate_cli/terminal_agent/animations.py +283 -0
  6. comate_cli/terminal_agent/app.py +261 -0
  7. comate_cli/terminal_agent/assistant_render.py +243 -0
  8. comate_cli/terminal_agent/env_utils.py +37 -0
  9. comate_cli/terminal_agent/error_display.py +46 -0
  10. comate_cli/terminal_agent/event_renderer.py +867 -0
  11. comate_cli/terminal_agent/fragment_utils.py +25 -0
  12. comate_cli/terminal_agent/history_printer.py +150 -0
  13. comate_cli/terminal_agent/input_geometry.py +92 -0
  14. comate_cli/terminal_agent/layout_coordinator.py +188 -0
  15. comate_cli/terminal_agent/logging_adapter.py +147 -0
  16. comate_cli/terminal_agent/logo.py +58 -0
  17. comate_cli/terminal_agent/markdown_render.py +24 -0
  18. comate_cli/terminal_agent/mention_completer.py +293 -0
  19. comate_cli/terminal_agent/message_style.py +33 -0
  20. comate_cli/terminal_agent/models.py +89 -0
  21. comate_cli/terminal_agent/question_view.py +584 -0
  22. comate_cli/terminal_agent/rewind_store.py +712 -0
  23. comate_cli/terminal_agent/rpc_protocol.py +103 -0
  24. comate_cli/terminal_agent/rpc_stdio.py +280 -0
  25. comate_cli/terminal_agent/selection_menu.py +305 -0
  26. comate_cli/terminal_agent/session_view.py +99 -0
  27. comate_cli/terminal_agent/slash_commands.py +142 -0
  28. comate_cli/terminal_agent/startup.py +77 -0
  29. comate_cli/terminal_agent/status_bar.py +258 -0
  30. comate_cli/terminal_agent/text_effects.py +30 -0
  31. comate_cli/terminal_agent/tool_view.py +584 -0
  32. comate_cli/terminal_agent/tui.py +1006 -0
  33. comate_cli/terminal_agent/tui_parts/__init__.py +17 -0
  34. comate_cli/terminal_agent/tui_parts/commands.py +759 -0
  35. comate_cli/terminal_agent/tui_parts/history_sync.py +262 -0
  36. comate_cli/terminal_agent/tui_parts/input_behavior.py +324 -0
  37. comate_cli/terminal_agent/tui_parts/key_bindings.py +307 -0
  38. comate_cli/terminal_agent/tui_parts/render_panels.py +537 -0
  39. comate_cli/terminal_agent/tui_parts/slash_command_registry.py +45 -0
  40. comate_cli/terminal_agent/tui_parts/ui_mode.py +9 -0
  41. comate_cli-0.1.0.dist-info/METADATA +37 -0
  42. comate_cli-0.1.0.dist-info/RECORD +44 -0
  43. comate_cli-0.1.0.dist-info/WHEEL +4 -0
  44. comate_cli-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,262 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import sys
5
+ from typing import Any
6
+
7
+ from prompt_toolkit.application import run_in_terminal
8
+ from rich.console import Console
9
+
10
+ from comate_agent_sdk.context.items import ItemType
11
+ from comate_agent_sdk.llm.messages import AssistantMessage, UserMessage
12
+
13
+ from comate_cli.terminal_agent.history_printer import (
14
+ print_history_group_sync,
15
+ render_history_group,
16
+ )
17
+ from comate_cli.terminal_agent.logo import print_logo
18
+ from comate_cli.terminal_agent.markdown_render import render_markdown_to_plain
19
+ from comate_cli.terminal_agent.models import HistoryEntry
20
+
21
+ console = Console()
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class HistorySyncMixin:
26
+ async def _replay_scrollback_after_rewind(self) -> None:
27
+ """清屏并重建 scrollback:Logo + 当前会话历史。"""
28
+ self._renderer.reset_history_view()
29
+ self._printed_history_index = 0
30
+
31
+ def _clear_and_print_logo() -> None:
32
+ out = sys.__stdout__ or sys.stdout
33
+ out.write("\x1b[3J\x1b[2J\x1b[H")
34
+ out.flush()
35
+ scrollback_console = Console(
36
+ file=out,
37
+ force_terminal=True,
38
+ width=self._terminal_width(),
39
+ )
40
+ print_logo(scrollback_console)
41
+
42
+ try:
43
+ await run_in_terminal(_clear_and_print_logo, in_executor=False)
44
+ except Exception as exc:
45
+ logger.warning(
46
+ f"failed to clear terminal and replay logo: {exc}",
47
+ exc_info=True,
48
+ )
49
+
50
+ self.add_resume_history("resume")
51
+
52
+ def add_resume_history(self, mode: str) -> None:
53
+ if mode != "resume":
54
+ return
55
+
56
+ items = self._session._agent._context.get_conversation_items_snapshot()
57
+ history: list[Any] = []
58
+ for item in items:
59
+ if item.item_type not in (
60
+ ItemType.USER_MESSAGE,
61
+ ItemType.ASSISTANT_MESSAGE,
62
+ ItemType.TOOL_RESULT,
63
+ ):
64
+ continue
65
+ if item.item_type == ItemType.USER_MESSAGE:
66
+ message = getattr(item, "message", None)
67
+ if isinstance(message, UserMessage) and bool(getattr(message, "is_meta", False)):
68
+ continue
69
+ history.append(item)
70
+ if not history:
71
+ return
72
+
73
+ self._renderer.append_system_message(f"history loaded: {len(history)} messages")
74
+
75
+ # 维护 tool_call_id 映射,用于匹配工具调用和结果
76
+ tool_call_info: dict[str, tuple[str, str]] = {} # tool_call_id → (tool_name, signature)
77
+
78
+ for item in history:
79
+ if item.item_type == ItemType.USER_MESSAGE:
80
+ message = getattr(item, "message", None)
81
+ content = str(item.content_text or "").strip()
82
+ if not content and isinstance(message, UserMessage):
83
+ content = str(message.text or "").strip()
84
+ if content:
85
+ self._renderer.seed_user_message(content)
86
+ continue
87
+
88
+ # 处理 AssistantMessage - 提取 tool_calls 信息但不渲染为动态组件
89
+ if item.item_type == ItemType.ASSISTANT_MESSAGE:
90
+ message = getattr(item, "message", None)
91
+ if isinstance(message, AssistantMessage) and message.tool_calls:
92
+ for tc in message.tool_calls:
93
+ tool_name = tc.function.name
94
+ args_str = tc.function.arguments
95
+ try:
96
+ import json
97
+
98
+ args_dict = json.loads(args_str) if args_str else {}
99
+ except json.JSONDecodeError:
100
+ args_dict = {"_raw": args_str}
101
+ signature = self._build_resume_tool_signature(tool_name, args_dict)
102
+ # 存储映射,不调用 restore_tool_call(避免加入 _running_tools)
103
+ tool_call_info[tc.id] = (tool_name, signature)
104
+
105
+ # 显示 assistant text
106
+ assistant_text = self._extract_assistant_text(item).strip()
107
+ if assistant_text:
108
+ self._renderer.append_assistant_message(assistant_text)
109
+ continue
110
+
111
+ # 处理 TOOL_RESULT - 直接输出静态文本(无计时器)
112
+ if item.item_type == ItemType.TOOL_RESULT:
113
+ message = getattr(item, "message", None)
114
+ if message is None:
115
+ continue
116
+
117
+ tool_call_id = getattr(message, "tool_call_id", None)
118
+ is_error = getattr(message, "is_error", False)
119
+
120
+ # 从映射中获取工具信息
121
+ if tool_call_id and tool_call_id in tool_call_info:
122
+ tool_name, signature = tool_call_info[tool_call_id]
123
+ else:
124
+ # 回退:直接使用 tool_name
125
+ tool_name = getattr(message, "tool_name", item.tool_name or "UnknownTool")
126
+ signature = f"{tool_name}()"
127
+
128
+ # Extract diff from raw_envelope for Edit/MultiEdit
129
+ diff_lines: list[str] | None = None
130
+ if tool_name in ("Edit", "MultiEdit") and not is_error:
131
+ raw_envelope = getattr(item, "metadata", {}) or {}
132
+ envelope = raw_envelope.get("tool_raw_envelope")
133
+ if isinstance(envelope, dict):
134
+ data = envelope.get("data", {})
135
+ if isinstance(data, dict):
136
+ diff = data.get("diff")
137
+ if isinstance(diff, list) and len(diff) > 0:
138
+ diff_lines = diff
139
+
140
+ # 直接追加静态 HistoryEntry(无计时器)
141
+ self._renderer.append_static_tool_result(
142
+ signature,
143
+ is_error,
144
+ diff_lines=diff_lines,
145
+ )
146
+ continue
147
+
148
+ self._drain_history_sync()
149
+
150
+ def _summarize_tool_args(self, tool_name: str, args: dict[str, Any]) -> str:
151
+ """Summarize tool arguments for display in history."""
152
+ from comate_cli.terminal_agent.tool_view import summarize_tool_args
153
+
154
+ summary = summarize_tool_args(tool_name, args, self._renderer._project_root).strip()
155
+ return summary
156
+
157
+ @staticmethod
158
+ def _lookup_arg(args: dict[str, Any], *keys: str) -> Any:
159
+ for key in keys:
160
+ if key in args:
161
+ return args.get(key)
162
+ params = args.get("params")
163
+ if isinstance(params, dict):
164
+ for key in keys:
165
+ if key in params:
166
+ return params.get(key)
167
+ return None
168
+
169
+ def _build_resume_tool_signature(self, tool_name: str, args: dict[str, Any]) -> str:
170
+ lowered = tool_name.lower()
171
+ if lowered != "task":
172
+ summary = self._summarize_tool_args(tool_name, args)
173
+ if summary:
174
+ return f"{tool_name}({summary})"
175
+ return f"{tool_name}()"
176
+
177
+ raw_subagent = self._lookup_arg(args, "subagent_type")
178
+ subagent_name = raw_subagent.strip() if isinstance(raw_subagent, str) else ""
179
+ if not subagent_name:
180
+ subagent_name = "Task"
181
+
182
+ raw_description = self._lookup_arg(args, "description")
183
+ description = raw_description.strip() if isinstance(raw_description, str) else ""
184
+ if description and description != subagent_name:
185
+ return f"{subagent_name}({description})"
186
+ return subagent_name
187
+
188
+ @staticmethod
189
+ def _extract_assistant_text(item: Any) -> str:
190
+ """从 ContextItem 提取用于显示的文本(不含 tool_calls JSON)"""
191
+ message = getattr(item, "message", None)
192
+ if message is None:
193
+ return ""
194
+
195
+ # 优先使用 message.text(纯文本,不含 tool_calls)
196
+ if hasattr(message, "text"):
197
+ text = message.text
198
+ if isinstance(text, str):
199
+ return text
200
+
201
+ # 回退:处理非标准 content
202
+ msg_content = getattr(message, "content", "")
203
+ if isinstance(msg_content, str):
204
+ return msg_content
205
+ if isinstance(msg_content, list):
206
+ text_parts: list[str] = []
207
+ for part in msg_content:
208
+ if isinstance(part, dict) and part.get("type") == "text":
209
+ text_parts.append(str(part.get("text", "")))
210
+ return "".join(text_parts)
211
+
212
+ return ""
213
+
214
+ async def _drain_history_async(self) -> None:
215
+ pending = self._pending_history_entries()
216
+ if not pending:
217
+ return
218
+ group = render_history_group(
219
+ console,
220
+ pending,
221
+ terminal_width=self._terminal_width(),
222
+ render_markdown_to_plain=render_markdown_to_plain,
223
+ )
224
+ if group is None:
225
+ return
226
+
227
+ def _print() -> None:
228
+ width = self._terminal_width()
229
+ out = sys.__stdout__ or sys.stdout
230
+ scrollback_console = Console(
231
+ file=out,
232
+ force_terminal=True,
233
+ width=width,
234
+ )
235
+ print_history_group_sync(scrollback_console, group)
236
+
237
+ await run_in_terminal(_print, in_executor=False)
238
+
239
+ def _drain_history_sync(self) -> None:
240
+ pending = self._pending_history_entries()
241
+ if not pending:
242
+ return
243
+ group = render_history_group(
244
+ console,
245
+ pending,
246
+ terminal_width=self._terminal_width(),
247
+ render_markdown_to_plain=render_markdown_to_plain,
248
+ )
249
+ if group is None:
250
+ return
251
+ print_history_group_sync(console, group)
252
+
253
+ def _pending_history_entries(self) -> list[HistoryEntry]:
254
+ entries = self._renderer.history_entries()
255
+ if self._printed_history_index >= len(entries):
256
+ return []
257
+ pending = entries[self._printed_history_index :]
258
+ self._printed_history_index = len(entries)
259
+ # 根据 _show_thinking 开关过滤 thinking 条目
260
+ if not getattr(self, "_show_thinking", False):
261
+ pending = [e for e in pending if e.entry_type != "thinking"]
262
+ return pending
@@ -0,0 +1,324 @@
1
+ from __future__ import annotations
2
+
3
+ from bisect import bisect_right
4
+ from typing import Any
5
+
6
+ from prompt_toolkit.document import Document
7
+
8
+ from comate_cli.terminal_agent.input_geometry import (
9
+ compute_visual_line_ranges,
10
+ index_for_visual_col,
11
+ visual_col_for_index,
12
+ )
13
+ from comate_cli.terminal_agent.question_view import QuestionAction
14
+ from comate_cli.terminal_agent.tui_parts.ui_mode import UIMode
15
+
16
+
17
+ class InputBehaviorMixin:
18
+ def _should_handle_history(self) -> bool:
19
+ """检查是否应该处理历史浏览(而不是补全导航)
20
+
21
+ Returns:
22
+ True 表示可以浏览历史,False 表示应该优先处理补全
23
+ """
24
+ buffer = self._input_area.buffer
25
+
26
+ # 如果有活动的补全菜单,不应该浏览历史
27
+ if buffer.complete_state is not None and buffer.complete_state.completions:
28
+ return False
29
+
30
+ # 如果在补全上下文中(输入了 / 或 @),不应该浏览历史
31
+ document = buffer.document
32
+ if self._completion_context_active(
33
+ document.text_before_cursor,
34
+ document.text_after_cursor,
35
+ ):
36
+ return False
37
+
38
+ return True
39
+
40
+ def _completion_context_active(
41
+ self,
42
+ text_before_cursor: str,
43
+ text_after_cursor: str,
44
+ ) -> bool:
45
+ if text_after_cursor.strip():
46
+ return False
47
+ if text_before_cursor.startswith("/") and " " not in text_before_cursor:
48
+ return True
49
+ return self._mention_completer.extract_context(text_before_cursor) is not None
50
+
51
+ def _move_completion_selection(self, buffer: Any, *, backward: bool) -> bool:
52
+ """尝试在补全菜单中移动选择项
53
+
54
+ 行为:
55
+ - 如果补全菜单已显示,在菜单中导航
56
+ - 如果在补全上下文中(输入 / 或 @)但菜单未显示,触发补全并选中第一项
57
+ - 否则返回 False,允许调用者处理其他行为(如历史浏览)
58
+
59
+ Args:
60
+ buffer: 当前的 Buffer 对象
61
+ backward: True 表示向上导航,False 表示向下导航
62
+
63
+ Returns:
64
+ True 表示已处理补全导航,False 表示未处理
65
+ """
66
+ complete_state = buffer.complete_state
67
+
68
+ # 情况1: 已有补全菜单且有补全项
69
+ if complete_state is not None and complete_state.completions:
70
+ if backward:
71
+ buffer.complete_previous()
72
+ else:
73
+ buffer.complete_next()
74
+ return True
75
+
76
+ # 情况2: 在补全上下文中但菜单未显示
77
+ document = buffer.document
78
+ if self._completion_context_active(
79
+ document.text_before_cursor,
80
+ document.text_after_cursor,
81
+ ):
82
+ # 启动补全并选择第一项(关键改动:select_first=True)
83
+ buffer.start_completion(select_first=True)
84
+ return True
85
+
86
+ return False
87
+
88
+ def _enter_question_mode(self, questions: list[dict[str, Any]]) -> None:
89
+ if not self._question_ui.set_questions(questions):
90
+ return
91
+ self._ui_mode = UIMode.QUESTION
92
+ self._sync_focus_for_mode()
93
+ self._refresh_layers()
94
+
95
+ def _exit_question_mode(self) -> None:
96
+ self._ui_mode = UIMode.NORMAL
97
+ self._question_ui.clear()
98
+ self._sync_focus_for_mode()
99
+
100
+ def _sync_focus_for_mode(self) -> None:
101
+ if self._app is None:
102
+ return
103
+ if self._ui_mode == UIMode.QUESTION:
104
+ self._app.layout.focus(self._question_ui.focus_target())
105
+ return
106
+ if self._ui_mode == UIMode.SELECTION:
107
+ self._app.layout.focus(self._selection_ui.focus_target())
108
+ return
109
+ self._app.layout.focus(self._input_area.window)
110
+
111
+ def _handle_question_action(self, action: QuestionAction | None) -> None:
112
+ if action is None:
113
+ return
114
+ if action.kind == "cancel":
115
+ self._submit_question_reply(action.message, cancelled=True)
116
+ return
117
+ if action.kind == "submit":
118
+ self._submit_question_reply(action.message, cancelled=False)
119
+
120
+ def _submit_question_reply(self, message: str, *, cancelled: bool) -> None:
121
+ normalized = message.strip()
122
+ if not normalized:
123
+ return
124
+ self._exit_question_mode()
125
+ self._waiting_for_input = False
126
+ self._pending_questions = None
127
+ if cancelled:
128
+ self._renderer.append_system_message("用户取消问答,已发送拒绝回答消息。")
129
+ self._refresh_layers()
130
+ self._schedule_background(self._submit_user_message(normalized))
131
+
132
+ def _clear_input_area(self) -> None:
133
+ self._clear_paste_state()
134
+ self._last_input_len = 0
135
+ self._last_input_text = ""
136
+
137
+ buffer = self._input_area.buffer
138
+ buffer.cancel_completion()
139
+ self._suppress_input_change_hook = True
140
+ try:
141
+ buffer.set_document(Document("", cursor_position=0), bypass_readonly=True)
142
+ finally:
143
+ self._suppress_input_change_hook = False
144
+
145
+ def _next_paste_token(self) -> str:
146
+ self._paste_token_seq += 1
147
+ return f"paste_{self._paste_token_seq}"
148
+
149
+ def _clear_paste_state(self) -> None:
150
+ self._active_paste_token = None
151
+ self._paste_placeholder_text = None
152
+ self._paste_payload_by_token.clear()
153
+
154
+ @staticmethod
155
+ def _find_inserted_segment(
156
+ previous_text: str,
157
+ current_text: str,
158
+ ) -> tuple[int, int, str] | None:
159
+ if len(current_text) <= len(previous_text):
160
+ return None
161
+
162
+ prefix = 0
163
+ max_prefix = min(len(previous_text), len(current_text))
164
+ while prefix < max_prefix and previous_text[prefix] == current_text[prefix]:
165
+ prefix += 1
166
+
167
+ previous_remaining = len(previous_text) - prefix
168
+ current_remaining = len(current_text) - prefix
169
+ suffix = 0
170
+ max_suffix = min(previous_remaining, current_remaining)
171
+ while (
172
+ suffix < max_suffix
173
+ and previous_text[len(previous_text) - 1 - suffix]
174
+ == current_text[len(current_text) - 1 - suffix]
175
+ ):
176
+ suffix += 1
177
+
178
+ start = prefix
179
+ end = len(current_text) - suffix
180
+ if end <= start:
181
+ return None
182
+ return start, end, current_text[start:end]
183
+
184
+ def _resolve_submit_texts(self, raw_text: str) -> tuple[str, str]:
185
+ stripped = raw_text.strip()
186
+ token = self._active_paste_token
187
+ placeholder = self._paste_placeholder_text
188
+ if token is not None and placeholder is not None and placeholder in raw_text:
189
+ payload = self._paste_payload_by_token.get(token)
190
+ if payload is not None:
191
+ submit_text = raw_text.replace(placeholder, payload, 1).strip()
192
+ # 在 history 中展示真实发送内容,避免显示占位符文本。
193
+ return submit_text, submit_text
194
+ return stripped, stripped
195
+
196
+ def _handle_large_paste(self, buffer: Any) -> bool:
197
+ if self._suppress_input_change_hook:
198
+ return False
199
+ if self._busy:
200
+ return False
201
+
202
+ text = str(buffer.text)
203
+ previous_text = self._last_input_text
204
+ self._last_input_len = len(text)
205
+ self._last_input_text = text
206
+
207
+ placeholder = self._paste_placeholder_text
208
+ if self._active_paste_token is not None and placeholder and placeholder in text:
209
+ return False
210
+
211
+ if (
212
+ self._active_paste_token is not None
213
+ and self._paste_placeholder_text is not None
214
+ ):
215
+ self._clear_paste_state()
216
+
217
+ threshold = max(1, int(self._paste_threshold_chars))
218
+ segment = self._find_inserted_segment(previous_text, text)
219
+ if segment is None:
220
+ return False
221
+ start, end, inserted_text = segment
222
+ if len(inserted_text) < threshold:
223
+ return False
224
+
225
+ token = self._next_paste_token()
226
+ self._active_paste_token = token
227
+ self._paste_payload_by_token[token] = inserted_text
228
+ placeholder = f"[Pasted Content {len(inserted_text)} chars]"
229
+ self._paste_placeholder_text = placeholder
230
+ replaced_text = f"{text[:start]}{placeholder}{text[end:]}"
231
+
232
+ self._suppress_input_change_hook = True
233
+ try:
234
+ buffer.cancel_completion()
235
+ buffer.set_document(
236
+ Document(replaced_text, cursor_position=start + len(placeholder)),
237
+ bypass_readonly=True,
238
+ )
239
+ self._last_input_len = len(replaced_text)
240
+ self._last_input_text = replaced_text
241
+ finally:
242
+ self._suppress_input_change_hook = False
243
+
244
+ self._invalidate()
245
+ return True
246
+
247
+ def _move_cursor_visual(self, buffer: Any, *, backward: bool) -> bool:
248
+ text = str(buffer.text)
249
+ if not text:
250
+ return False
251
+
252
+ width = self._terminal_width()
253
+ max_cols = max(1, width - self._input_prompt_width)
254
+ ranges = compute_visual_line_ranges(text, max_cols=max_cols)
255
+ if len(ranges) <= 1:
256
+ return False
257
+
258
+ cursor_index = int(buffer.cursor_position)
259
+ starts = [start for start, _end in ranges]
260
+ row = max(0, bisect_right(starts, cursor_index) - 1)
261
+ row_start, row_end = ranges[row]
262
+
263
+ current_col = visual_col_for_index(
264
+ text,
265
+ row_start,
266
+ row_end,
267
+ max_cols,
268
+ cursor_index,
269
+ )
270
+
271
+ target_row = row - 1 if backward else row + 1
272
+ if target_row < 0 or target_row >= len(ranges):
273
+ return False
274
+
275
+ target_start, target_end = ranges[target_row]
276
+ target_index = index_for_visual_col(
277
+ text, target_start, target_end, max_cols, current_col
278
+ )
279
+ buffer.cursor_position = target_index
280
+ return True
281
+
282
+ def _submit_from_input(self) -> None:
283
+ if self._ui_mode != UIMode.NORMAL:
284
+ return
285
+
286
+ raw_text = self._input_area.text
287
+ display_text, submit_text = self._resolve_submit_texts(raw_text)
288
+ if not submit_text.strip():
289
+ return
290
+
291
+ if self._input_area.buffer.complete_state is not None:
292
+ self._input_area.buffer.cancel_completion()
293
+ self._clear_input_area()
294
+
295
+ # busy 或 initializing 时:非斜杠命令 → 入队,斜杠命令 → 交由命令分发决定
296
+ is_busy = self._busy or self._initializing
297
+ if is_busy:
298
+ if display_text.lstrip().startswith("/"):
299
+ self._schedule_background(self._execute_command(display_text.strip()))
300
+ return
301
+
302
+ if not display_text.lstrip().startswith("/"):
303
+ queue_size = len(self._queued_messages)
304
+ if queue_size >= int(self._message_queue_max_size):
305
+ self._renderer.append_system_message(
306
+ f"消息队列已满({queue_size}/{self._message_queue_max_size}),请等待当前任务完成。",
307
+ severity="error",
308
+ )
309
+ self._refresh_layers()
310
+ return
311
+ self._queued_messages.append(submit_text)
312
+ self._invalidate()
313
+ return
314
+
315
+ if display_text.lstrip().startswith("/"):
316
+ self._schedule_background(self._execute_command(display_text.strip()))
317
+ return
318
+
319
+ self._schedule_background(
320
+ self._submit_user_message(
321
+ submit_text,
322
+ display_text=display_text,
323
+ )
324
+ )