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.
- comate_cli/__init__.py +5 -0
- comate_cli/__main__.py +5 -0
- comate_cli/main.py +128 -0
- comate_cli/terminal_agent/__init__.py +2 -0
- comate_cli/terminal_agent/animations.py +283 -0
- comate_cli/terminal_agent/app.py +261 -0
- comate_cli/terminal_agent/assistant_render.py +243 -0
- comate_cli/terminal_agent/env_utils.py +37 -0
- comate_cli/terminal_agent/error_display.py +46 -0
- comate_cli/terminal_agent/event_renderer.py +867 -0
- comate_cli/terminal_agent/fragment_utils.py +25 -0
- comate_cli/terminal_agent/history_printer.py +150 -0
- comate_cli/terminal_agent/input_geometry.py +92 -0
- comate_cli/terminal_agent/layout_coordinator.py +188 -0
- comate_cli/terminal_agent/logging_adapter.py +147 -0
- comate_cli/terminal_agent/logo.py +58 -0
- comate_cli/terminal_agent/markdown_render.py +24 -0
- comate_cli/terminal_agent/mention_completer.py +293 -0
- comate_cli/terminal_agent/message_style.py +33 -0
- comate_cli/terminal_agent/models.py +89 -0
- comate_cli/terminal_agent/question_view.py +584 -0
- comate_cli/terminal_agent/rewind_store.py +712 -0
- comate_cli/terminal_agent/rpc_protocol.py +103 -0
- comate_cli/terminal_agent/rpc_stdio.py +280 -0
- comate_cli/terminal_agent/selection_menu.py +305 -0
- comate_cli/terminal_agent/session_view.py +99 -0
- comate_cli/terminal_agent/slash_commands.py +142 -0
- comate_cli/terminal_agent/startup.py +77 -0
- comate_cli/terminal_agent/status_bar.py +258 -0
- comate_cli/terminal_agent/text_effects.py +30 -0
- comate_cli/terminal_agent/tool_view.py +584 -0
- comate_cli/terminal_agent/tui.py +1006 -0
- comate_cli/terminal_agent/tui_parts/__init__.py +17 -0
- comate_cli/terminal_agent/tui_parts/commands.py +759 -0
- comate_cli/terminal_agent/tui_parts/history_sync.py +262 -0
- comate_cli/terminal_agent/tui_parts/input_behavior.py +324 -0
- comate_cli/terminal_agent/tui_parts/key_bindings.py +307 -0
- comate_cli/terminal_agent/tui_parts/render_panels.py +537 -0
- comate_cli/terminal_agent/tui_parts/slash_command_registry.py +45 -0
- comate_cli/terminal_agent/tui_parts/ui_mode.py +9 -0
- comate_cli-0.1.0.dist-info/METADATA +37 -0
- comate_cli-0.1.0.dist-info/RECORD +44 -0
- comate_cli-0.1.0.dist-info/WHEEL +4 -0
- comate_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from prompt_toolkit.filters import Condition, has_completions, has_focus
|
|
6
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
7
|
+
|
|
8
|
+
from comate_cli.terminal_agent.slash_commands import parse_slash_command_call
|
|
9
|
+
from comate_cli.terminal_agent.tui_parts.ui_mode import UIMode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KeyBindingsMixin:
|
|
13
|
+
def _build_key_bindings(self) -> KeyBindings:
|
|
14
|
+
bindings = KeyBindings()
|
|
15
|
+
normal_mode = Condition(lambda: self._ui_mode == UIMode.NORMAL)
|
|
16
|
+
question_mode = Condition(lambda: self._ui_mode == UIMode.QUESTION)
|
|
17
|
+
selection_mode = Condition(lambda: self._ui_mode == UIMode.SELECTION)
|
|
18
|
+
|
|
19
|
+
# Selection menu key bindings
|
|
20
|
+
@bindings.add("enter", filter=selection_mode)
|
|
21
|
+
def _selection_enter(event) -> None:
|
|
22
|
+
del event
|
|
23
|
+
result = self._selection_ui.confirm()
|
|
24
|
+
self._handle_selection_result(result)
|
|
25
|
+
self._invalidate()
|
|
26
|
+
|
|
27
|
+
@bindings.add("up", filter=selection_mode)
|
|
28
|
+
def _selection_up(event) -> None:
|
|
29
|
+
del event
|
|
30
|
+
self._selection_ui.move_selection(-1)
|
|
31
|
+
self._invalidate()
|
|
32
|
+
|
|
33
|
+
@bindings.add("down", filter=selection_mode)
|
|
34
|
+
def _selection_down(event) -> None:
|
|
35
|
+
del event
|
|
36
|
+
self._selection_ui.move_selection(1)
|
|
37
|
+
self._invalidate()
|
|
38
|
+
|
|
39
|
+
@bindings.add("k", filter=selection_mode)
|
|
40
|
+
def _selection_k(event) -> None:
|
|
41
|
+
del event
|
|
42
|
+
self._selection_ui.move_selection(-1)
|
|
43
|
+
self._invalidate()
|
|
44
|
+
|
|
45
|
+
@bindings.add("j", filter=selection_mode)
|
|
46
|
+
def _selection_j(event) -> None:
|
|
47
|
+
del event
|
|
48
|
+
self._selection_ui.move_selection(1)
|
|
49
|
+
self._invalidate()
|
|
50
|
+
|
|
51
|
+
@bindings.add("escape", filter=selection_mode)
|
|
52
|
+
def _selection_cancel(event) -> None:
|
|
53
|
+
del event
|
|
54
|
+
result = self._selection_ui.cancel()
|
|
55
|
+
self._handle_selection_result(result)
|
|
56
|
+
self._invalidate()
|
|
57
|
+
|
|
58
|
+
@bindings.add("enter", filter=question_mode)
|
|
59
|
+
def _question_enter(event) -> None:
|
|
60
|
+
del event
|
|
61
|
+
action = self._question_ui.handle_enter()
|
|
62
|
+
self._handle_question_action(action)
|
|
63
|
+
self._sync_focus_for_mode()
|
|
64
|
+
self._invalidate()
|
|
65
|
+
|
|
66
|
+
@bindings.add("up", filter=question_mode)
|
|
67
|
+
def _question_up(event) -> None:
|
|
68
|
+
del event
|
|
69
|
+
self._question_ui.move_option(-1)
|
|
70
|
+
self._sync_focus_for_mode()
|
|
71
|
+
self._invalidate()
|
|
72
|
+
|
|
73
|
+
@bindings.add("down", filter=question_mode)
|
|
74
|
+
def _question_down(event) -> None:
|
|
75
|
+
del event
|
|
76
|
+
self._question_ui.move_option(1)
|
|
77
|
+
self._sync_focus_for_mode()
|
|
78
|
+
self._invalidate()
|
|
79
|
+
|
|
80
|
+
@bindings.add("left", filter=question_mode)
|
|
81
|
+
def _question_prev(event) -> None:
|
|
82
|
+
del event
|
|
83
|
+
self._question_ui.prev_question()
|
|
84
|
+
self._sync_focus_for_mode()
|
|
85
|
+
self._invalidate()
|
|
86
|
+
|
|
87
|
+
@bindings.add("right", filter=question_mode)
|
|
88
|
+
def _question_next(event) -> None:
|
|
89
|
+
del event
|
|
90
|
+
self._question_ui.next_question()
|
|
91
|
+
self._sync_focus_for_mode()
|
|
92
|
+
self._invalidate()
|
|
93
|
+
|
|
94
|
+
@bindings.add(
|
|
95
|
+
"space",
|
|
96
|
+
filter=question_mode & ~has_focus(self._question_ui.custom_input_window),
|
|
97
|
+
)
|
|
98
|
+
def _question_toggle(event) -> None:
|
|
99
|
+
del event
|
|
100
|
+
self._question_ui.toggle_current_selection()
|
|
101
|
+
self._invalidate()
|
|
102
|
+
|
|
103
|
+
@bindings.add("tab", filter=question_mode)
|
|
104
|
+
def _question_submit_tab(event) -> None:
|
|
105
|
+
del event
|
|
106
|
+
self._question_ui.focus_submit()
|
|
107
|
+
self._sync_focus_for_mode()
|
|
108
|
+
self._invalidate()
|
|
109
|
+
|
|
110
|
+
@bindings.add("escape", filter=question_mode)
|
|
111
|
+
def _question_cancel(event) -> None:
|
|
112
|
+
del event
|
|
113
|
+
self._handle_question_action(self._question_ui.handle_escape())
|
|
114
|
+
self._sync_focus_for_mode()
|
|
115
|
+
self._invalidate()
|
|
116
|
+
|
|
117
|
+
@bindings.add("enter", filter=normal_mode, eager=True)
|
|
118
|
+
def _enter(event) -> None:
|
|
119
|
+
buffer = event.current_buffer
|
|
120
|
+
cs = buffer.complete_state
|
|
121
|
+
|
|
122
|
+
# 菜单打开时:先接受补全
|
|
123
|
+
if cs is not None and cs.completions:
|
|
124
|
+
completion = cs.current_completion or cs.completions[0]
|
|
125
|
+
buffer.apply_completion(completion)
|
|
126
|
+
buffer.cancel_completion()
|
|
127
|
+
|
|
128
|
+
# ✅ 决策:slash 补全 -> 立即提交;mention 补全 -> 只填入不提交
|
|
129
|
+
text_now = buffer.text
|
|
130
|
+
doc_now = buffer.document
|
|
131
|
+
|
|
132
|
+
is_mention = (
|
|
133
|
+
self._mention_completer.extract_context(doc_now.text_before_cursor)
|
|
134
|
+
is not None
|
|
135
|
+
)
|
|
136
|
+
parsed_slash = parse_slash_command_call(text_now)
|
|
137
|
+
|
|
138
|
+
if (parsed_slash is not None) and (not is_mention):
|
|
139
|
+
self._submit_from_input()
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# 没有菜单:正常提交
|
|
143
|
+
self._submit_from_input()
|
|
144
|
+
|
|
145
|
+
@bindings.add("tab", filter=normal_mode)
|
|
146
|
+
def _tab(event) -> None:
|
|
147
|
+
buffer = event.current_buffer
|
|
148
|
+
cs = buffer.complete_state
|
|
149
|
+
# Tab:如果有补全项 -> 接受当前项并关闭菜单
|
|
150
|
+
if cs is not None and cs.completions:
|
|
151
|
+
completion = cs.current_completion or cs.completions[0]
|
|
152
|
+
buffer.apply_completion(completion)
|
|
153
|
+
buffer.cancel_completion()
|
|
154
|
+
return
|
|
155
|
+
# 否则触发补全菜单
|
|
156
|
+
buffer.start_completion(select_first=False)
|
|
157
|
+
|
|
158
|
+
@bindings.add("s-tab", filter=normal_mode & has_completions)
|
|
159
|
+
def _active_prev(event) -> None:
|
|
160
|
+
event.current_buffer.complete_previous()
|
|
161
|
+
|
|
162
|
+
@bindings.add("s-tab", filter=normal_mode & ~has_completions)
|
|
163
|
+
def _cycle_mode(event) -> None:
|
|
164
|
+
del event
|
|
165
|
+
self._cycle_agent_mode()
|
|
166
|
+
|
|
167
|
+
@bindings.add("up", filter=normal_mode)
|
|
168
|
+
def _active_prev_up(event) -> None:
|
|
169
|
+
buffer = event.current_buffer
|
|
170
|
+
# busy + 有 queue + input 为空 → 取回最近一条 queue,加载回 input
|
|
171
|
+
is_busy = self._busy or self._initializing
|
|
172
|
+
if is_busy and self._queued_messages and not buffer.text.strip():
|
|
173
|
+
buffer.text = self._queued_messages.pop()
|
|
174
|
+
buffer.cursor_position = len(buffer.text)
|
|
175
|
+
self._invalidate()
|
|
176
|
+
return
|
|
177
|
+
if self._move_completion_selection(buffer, backward=True):
|
|
178
|
+
return
|
|
179
|
+
if self._move_cursor_visual(buffer, backward=True):
|
|
180
|
+
self._invalidate()
|
|
181
|
+
return
|
|
182
|
+
if self._should_handle_history():
|
|
183
|
+
buffer.auto_up(count=1)
|
|
184
|
+
|
|
185
|
+
@bindings.add("down", filter=normal_mode)
|
|
186
|
+
def _active_next_down(event) -> None:
|
|
187
|
+
buffer = event.current_buffer
|
|
188
|
+
if self._move_completion_selection(buffer, backward=False):
|
|
189
|
+
return
|
|
190
|
+
if self._move_cursor_visual(buffer, backward=False):
|
|
191
|
+
self._invalidate()
|
|
192
|
+
return
|
|
193
|
+
if self._should_handle_history():
|
|
194
|
+
buffer.auto_down(count=1)
|
|
195
|
+
|
|
196
|
+
@bindings.add("escape", filter=normal_mode)
|
|
197
|
+
def _esc(event) -> None:
|
|
198
|
+
buffer = event.current_buffer
|
|
199
|
+
now = time.monotonic()
|
|
200
|
+
if buffer.complete_state is not None:
|
|
201
|
+
buffer.cancel_completion()
|
|
202
|
+
self._esc_press_count = 0
|
|
203
|
+
self._esc_last_pressed_at = now
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if now - self._esc_last_pressed_at > self._esc_clear_window_seconds:
|
|
207
|
+
self._esc_press_count = 0
|
|
208
|
+
self._esc_press_count += 1
|
|
209
|
+
self._esc_last_pressed_at = now
|
|
210
|
+
|
|
211
|
+
if self._esc_press_count >= 2:
|
|
212
|
+
self._esc_press_count = 0
|
|
213
|
+
self._clear_input_area()
|
|
214
|
+
self._invalidate()
|
|
215
|
+
return
|
|
216
|
+
if self._app is not None:
|
|
217
|
+
self._app.layout.focus(self._input_area.window)
|
|
218
|
+
|
|
219
|
+
@bindings.add("c-c")
|
|
220
|
+
def _interrupt_or_exit(event) -> None:
|
|
221
|
+
del event
|
|
222
|
+
if self._busy and self._stream_task is not None:
|
|
223
|
+
now = time.monotonic()
|
|
224
|
+
interrupted_once = (
|
|
225
|
+
self._interrupt_requested_at is not None
|
|
226
|
+
and (now - self._interrupt_requested_at)
|
|
227
|
+
<= self._interrupt_force_window_seconds
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if interrupted_once:
|
|
231
|
+
self._stream_task.cancel()
|
|
232
|
+
self._session.run_controller.clear()
|
|
233
|
+
self._interrupt_requested_at = None
|
|
234
|
+
self._renderer.interrupt_turn()
|
|
235
|
+
self._renderer.append_system_message(
|
|
236
|
+
"已强制中断当前任务,可继续输入。"
|
|
237
|
+
)
|
|
238
|
+
self._set_busy(False)
|
|
239
|
+
self._waiting_for_input = False
|
|
240
|
+
self._pending_questions = None
|
|
241
|
+
self._exit_question_mode()
|
|
242
|
+
self._refresh_layers()
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
self._session.run_controller.interrupt(reason="user")
|
|
246
|
+
self._interrupt_requested_at = now
|
|
247
|
+
self._renderer.append_system_message(
|
|
248
|
+
"已发送中断信号。再次按 Ctrl+C 将强制中断。"
|
|
249
|
+
)
|
|
250
|
+
self._refresh_layers()
|
|
251
|
+
return
|
|
252
|
+
now = time.monotonic()
|
|
253
|
+
if now - self._ctrl_c_last_pressed_at > self._ctrl_c_exit_window_seconds:
|
|
254
|
+
self._ctrl_c_press_count = 0
|
|
255
|
+
self._ctrl_c_press_count += 1
|
|
256
|
+
self._ctrl_c_last_pressed_at = now
|
|
257
|
+
|
|
258
|
+
if self._ctrl_c_press_count >= 2:
|
|
259
|
+
self._ctrl_c_press_count = 0
|
|
260
|
+
self._request_exit()
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
self._clear_input_area()
|
|
264
|
+
self._invalidate()
|
|
265
|
+
|
|
266
|
+
@bindings.add("c-d")
|
|
267
|
+
def _exit(event) -> None:
|
|
268
|
+
del event
|
|
269
|
+
self._request_exit()
|
|
270
|
+
|
|
271
|
+
@bindings.add("c-o", filter=normal_mode)
|
|
272
|
+
def _toggle_diff_panel(event) -> None:
|
|
273
|
+
del event
|
|
274
|
+
self._diff_panel_visible = not self._diff_panel_visible
|
|
275
|
+
self._diff_panel_scroll = 0
|
|
276
|
+
self._invalidate()
|
|
277
|
+
|
|
278
|
+
@bindings.add("c-t", filter=normal_mode)
|
|
279
|
+
def _toggle_thinking(event) -> None:
|
|
280
|
+
del event
|
|
281
|
+
self._show_thinking = not self._show_thinking
|
|
282
|
+
status = "开启" if self._show_thinking else "关闭"
|
|
283
|
+
self._renderer.append_system_message(f"Thinking 显示已{status}")
|
|
284
|
+
self._invalidate()
|
|
285
|
+
|
|
286
|
+
diff_panel_open = Condition(lambda: self._diff_panel_visible)
|
|
287
|
+
|
|
288
|
+
@bindings.add("up", filter=normal_mode & diff_panel_open)
|
|
289
|
+
def _diff_scroll_up(event) -> None:
|
|
290
|
+
del event
|
|
291
|
+
if self._diff_panel_scroll > 0:
|
|
292
|
+
self._diff_panel_scroll -= 1
|
|
293
|
+
self._invalidate()
|
|
294
|
+
|
|
295
|
+
@bindings.add("down", filter=normal_mode & diff_panel_open)
|
|
296
|
+
def _diff_scroll_down(event) -> None:
|
|
297
|
+
del event
|
|
298
|
+
diff_lines = self._renderer.latest_diff_lines
|
|
299
|
+
if diff_lines:
|
|
300
|
+
max_scroll = max(
|
|
301
|
+
0, len(diff_lines) - (self._DIFF_PANEL_MAX_VISIBLE - 1)
|
|
302
|
+
)
|
|
303
|
+
if self._diff_panel_scroll < max_scroll:
|
|
304
|
+
self._diff_panel_scroll += 1
|
|
305
|
+
self._invalidate()
|
|
306
|
+
|
|
307
|
+
return bindings
|