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,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