crabcode 0.1.6__py3-none-win_amd64.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 (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,168 @@
1
+ """Full-screen selection of configured model profiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from prompt_toolkit.application import Application
8
+ from prompt_toolkit.buffer import Buffer
9
+ from prompt_toolkit.data_structures import Point
10
+ from prompt_toolkit.filters import Condition
11
+ from prompt_toolkit.key_binding import KeyBindings
12
+ from prompt_toolkit.layout import HSplit, Layout, VSplit, Window
13
+ from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
14
+
15
+ from crabcode_cli.session_picker import PICKER_STYLE, _plain
16
+ from crabcode_core.events import CoreSession
17
+
18
+
19
+ class ModelPicker:
20
+ def __init__(self, session: CoreSession) -> None:
21
+ # The REPL initializes its session lazily. Before the first request,
22
+ # the configured default is already the effective model profile.
23
+ self.current_name = getattr(session, "_current_model_name", None) or session.settings.default_model
24
+ active = session.settings.get_api_config(self.current_name)
25
+ self.current_label = (
26
+ f"{self.current_name or 'default'} · {active.provider or 'anthropic'}/"
27
+ f"{active.model or 'default'} · {active.group or 'default'}"
28
+ )
29
+ grouped: dict[str, list[dict[str, str]]] = {}
30
+ for name, description in session.list_models().items():
31
+ config = session.settings.get_api_config(name)
32
+ group = config.group or "default"
33
+ grouped.setdefault(group, []).append({
34
+ "name": name, "description": description, "group": group,
35
+ "provider": config.provider or "anthropic", "model": config.model or "default",
36
+ })
37
+ self.rows = [row for rows in grouped.values() for row in rows]
38
+ self.groups = list(grouped)
39
+ self.group_index = 0 # Zero means all groups; actual names may also be "All".
40
+ self.matches = self.rows.copy()
41
+ self.selected = next((i for i, row in enumerate(self.matches)
42
+ if row["name"] == self.current_name), 0)
43
+ self.query = Buffer(on_text_changed=lambda _: self.refresh())
44
+ self.search = BufferControl(buffer=self.query)
45
+ self.group_control = FormattedTextControl(self._group, focusable=True)
46
+ self.list_window = Window(
47
+ FormattedTextControl(self._list, get_cursor_position=lambda: Point(0, self._selected_line())),
48
+ wrap_lines=False, always_hide_cursor=True,
49
+ )
50
+ bindings = KeyBindings()
51
+
52
+ @bindings.add("escape", eager=True)
53
+ @bindings.add("c-c", eager=True)
54
+ @bindings.add("c-d", eager=True)
55
+ def cancel(event: Any) -> None:
56
+ event.app.exit(result=None)
57
+
58
+ @bindings.add("enter", eager=True)
59
+ def confirm(event: Any) -> None:
60
+ if self.matches:
61
+ event.app.exit(result=self.matches[self.selected]["name"])
62
+
63
+ @bindings.add("up")
64
+ @bindings.add("down")
65
+ @bindings.add("pageup")
66
+ @bindings.add("pagedown")
67
+ def navigate(event: Any) -> None:
68
+ page = max(1, self.list_window.render_info.window_height - 1) if self.list_window.render_info else 10
69
+ delta = {"up": -1, "down": 1, "pageup": -page, "pagedown": page}[event.key_sequence[0].key]
70
+ self.selected = max(0, min(len(self.matches) - 1, self.selected + delta))
71
+
72
+ @bindings.add("tab")
73
+ @bindings.add("s-tab")
74
+ def focus(event: Any) -> None:
75
+ event.app.layout.focus(self.search if event.app.layout.has_focus(self.group_control) else self.group_control)
76
+
77
+ @bindings.add("left", filter=Condition(lambda: self.app.layout.has_focus(self.group_control)))
78
+ @bindings.add("right", filter=Condition(lambda: self.app.layout.has_focus(self.group_control)))
79
+ @bindings.add("space", filter=Condition(lambda: self.app.layout.has_focus(self.group_control)))
80
+ def change_group(event: Any) -> None:
81
+ delta = -1 if event.key_sequence[0].key == "left" else 1
82
+ self.group_index = (self.group_index + delta) % (len(self.groups) + 1)
83
+ self.refresh()
84
+
85
+ self.app: Application[str | None] = Application(
86
+ layout=Layout(HSplit([
87
+ Window(FormattedTextControl([("class:accent", " ╭─ CrabCode · Select model")]), height=1),
88
+ Window(FormattedTextControl([("class:hint", f" Current: {_plain(self.current_label)}")]), height=1, wrap_lines=False),
89
+ Window(height=1),
90
+ VSplit([
91
+ Window(FormattedTextControl([("class:accent", " Search ❯ ")]), width=11),
92
+ Window(self.search, height=1, wrap_lines=False),
93
+ ], height=1),
94
+ Window(self.group_control, height=1),
95
+ Window(height=1),
96
+ self.list_window,
97
+ Window(FormattedTextControl(self._details), height=2, wrap_lines=False),
98
+ Window(char="─", height=1, style="class:hint"),
99
+ Window(FormattedTextControl(self._footer), height=2, wrap_lines=True),
100
+ ]), focused_element=self.search),
101
+ key_bindings=bindings, full_screen=True, erase_when_done=True, style=PICKER_STYLE,
102
+ )
103
+
104
+ def refresh(self) -> None:
105
+ terms = self.query.text.casefold().split()
106
+ group = self.groups[self.group_index - 1] if self.group_index else None
107
+ self.matches = [row for row in self.rows
108
+ if (group is None or row["group"] == group)
109
+ and all(term in " ".join(row.values()).casefold() for term in terms)]
110
+ self.selected = next((i for i, row in enumerate(self.matches)
111
+ if row["name"] == self.current_name), 0)
112
+ self.app.invalidate()
113
+
114
+ def _group(self) -> list[tuple[str, str]]:
115
+ focused = self.app.layout.has_focus(self.group_control)
116
+ group = self.groups[self.group_index - 1] if self.group_index else "All groups"
117
+ return [("class:focused" if focused else "class:hint", f" Group: {_plain(group)}")]
118
+
119
+ def _list(self) -> list[tuple[str, str]]:
120
+ if not self.rows:
121
+ return [("class:hint", " No named models configured. Add models to settings.json.")]
122
+ if not self.matches:
123
+ return [("class:hint", " No matching models. Clear search or select All groups.")]
124
+ fragments = []
125
+ previous_group = None
126
+ for i, row in enumerate(self.matches):
127
+ if row["group"] != previous_group:
128
+ fragments.append(("class:accent", f" ─ {_plain(row['group'])}\n"))
129
+ previous_group = row["group"]
130
+ style = "class:selected" if i == self.selected else ""
131
+ marker = " ← active" if row["name"] == self.current_name else ""
132
+ fragments.extend([
133
+ (style, " ❯ " if i == self.selected else " "),
134
+ (style, _plain(row["name"]) + marker),
135
+ (style if i == self.selected else "class:hint",
136
+ f" · {_plain(row['provider'])}/{_plain(row['model'])}"),
137
+ ("", "\n" if i < len(self.matches) - 1 else ""),
138
+ ])
139
+ return fragments
140
+
141
+ def _selected_line(self) -> int:
142
+ # Group headings occupy display lines but are never selectable.
143
+ headings = 0
144
+ previous_group = None
145
+ for row in self.matches[:self.selected + 1]:
146
+ if row["group"] != previous_group:
147
+ headings += 1
148
+ previous_group = row["group"]
149
+ return self.selected + headings
150
+
151
+ def _details(self) -> list[tuple[str, str]]:
152
+ if not self.matches:
153
+ return []
154
+ row = self.matches[self.selected]
155
+ return [("class:hint", f" {_plain(row['name'])} · {_plain(row['group'])}\n"
156
+ f" {_plain(row['description'])}")]
157
+
158
+ def _footer(self) -> list[tuple[str, str]]:
159
+ count = len(self.matches)
160
+ return [("class:hint", f" ↑↓ browse · PgUp/PgDn page · Enter switch · Esc/Ctrl+C cancel {self.selected + 1 if count else 0}/{count}\n"
161
+ " Type to search · Tab focus group · ←→ change group")]
162
+
163
+ async def run(self) -> str | None:
164
+ return await self.app.run_async()
165
+
166
+
167
+ async def select_model(session: CoreSession) -> str | None:
168
+ return await ModelPicker(session).run()
crabcode_cli/pipe.py ADDED
@@ -0,0 +1,148 @@
1
+ """Pipe mode — reads from stdin, sends to Core, writes to stdout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+
9
+ from crabcode_core.events import CoreSession
10
+ from crabcode_core.utf8_sanitize import safe_utf8_str
11
+ from crabcode_core.types.config import CrabCodeSettings
12
+ from crabcode_core.types.event import (
13
+ ChoiceRequestEvent,
14
+ ChoiceResponseEvent,
15
+ ErrorEvent,
16
+ PermissionRequestEvent,
17
+ PermissionResponseEvent,
18
+ StreamRetryEvent,
19
+ StreamTextEvent,
20
+ ThinkingEvent,
21
+ ToolResultEvent,
22
+ ToolUseEvent,
23
+ TurnCompleteEvent,
24
+ )
25
+
26
+ _DEBUG_TOOL_PAYLOAD = os.getenv("CRABCODE_DEBUG_TOOL_PAYLOAD", "").strip().lower() in {
27
+ "1",
28
+ "true",
29
+ "yes",
30
+ "on",
31
+ }
32
+
33
+
34
+ def _format_token_count(tokens: int) -> str:
35
+ if tokens >= 1_000_000:
36
+ return f"{tokens / 1_000_000:.1f}M"
37
+ if tokens >= 1_000:
38
+ return f"{tokens // 1_000}k"
39
+ return str(tokens)
40
+
41
+
42
+ def _format_percent(percent: float) -> str:
43
+ rounded = round(percent)
44
+ if abs(percent - rounded) < 0.05:
45
+ return f"{rounded}%"
46
+ return f"{percent:.1f}%"
47
+
48
+
49
+ def _format_context_usage(event: TurnCompleteEvent) -> str | None:
50
+ used = max(0, int(getattr(event, "context_used_tokens", 0) or 0))
51
+ window = max(0, int(getattr(event, "context_window_tokens", 0) or 0))
52
+ parts: list[str] = []
53
+ if window:
54
+ used_percent = max(0.0, float(getattr(event, "context_used_percent", 0.0) or 0.0))
55
+ remaining_percent = max(0.0, 100.0 - used_percent)
56
+ parts.append(
57
+ f"Context: {_format_percent(used_percent)} used "
58
+ f"({_format_percent(remaining_percent)} remaining) · "
59
+ f"{_format_token_count(used)} tokens used of {_format_token_count(window)}"
60
+ )
61
+ elif used:
62
+ parts.append(f"Context: {_format_token_count(used)} tokens used (window unknown)")
63
+
64
+ usage = event.usage
65
+ if "cache_read_tokens" in usage or "cache_write_tokens" in usage:
66
+ cache_read = max(0, int(usage.get("cache_read_tokens", 0) or 0))
67
+ cache_write = max(0, int(usage.get("cache_write_tokens", 0) or 0))
68
+ total_input = max(
69
+ 0,
70
+ int(usage.get("total_input_tokens", usage.get("input_tokens", 0)) or 0),
71
+ )
72
+ hit_rate = cache_read / total_input * 100 if total_input else 0.0
73
+ cache_parts = [
74
+ f"Cache: {_format_percent(hit_rate)} hit",
75
+ f"read {_format_token_count(cache_read)}",
76
+ ]
77
+ if "cache_write_tokens" in usage:
78
+ cache_parts.append(f"write {_format_token_count(cache_write)}")
79
+ parts.append(" · ".join(cache_parts))
80
+ return " · ".join(parts) or None
81
+
82
+
83
+ async def run_pipe(
84
+ prompt: str,
85
+ settings: CrabCodeSettings | None = None,
86
+ cwd: str = ".",
87
+ images: list[dict[str, str]] | None = None,
88
+ ) -> None:
89
+ """Run a single prompt through the core and print the response."""
90
+ from crabcode_cli.computer_use import connect_session
91
+
92
+ session = CoreSession(cwd=cwd, settings=settings)
93
+ computer_use = None
94
+ try:
95
+ computer_use = await connect_session(session)
96
+ async for event in session.send_message(prompt, images=images):
97
+ if isinstance(event, StreamTextEvent):
98
+ sys.stdout.write(safe_utf8_str(event.text))
99
+ sys.stdout.flush()
100
+ elif isinstance(event, ThinkingEvent):
101
+ pass
102
+ elif isinstance(event, ToolUseEvent):
103
+ sys.stderr.write(f"\n[Tool: {event.tool_name}]\n")
104
+ if _DEBUG_TOOL_PAYLOAD:
105
+ pretty = json.dumps(event.tool_input, ensure_ascii=False, indent=2)
106
+ sys.stderr.write(f"{pretty}\n")
107
+ sys.stderr.flush()
108
+ elif isinstance(event, PermissionRequestEvent):
109
+ await session.respond_permission(
110
+ PermissionResponseEvent(
111
+ tool_use_id=event.tool_use_id, allowed=True
112
+ )
113
+ )
114
+ elif isinstance(event, ChoiceRequestEvent):
115
+ selected = [event.options[0]] if event.options else []
116
+ await session.respond_choice(
117
+ ChoiceResponseEvent(
118
+ tool_use_id=event.tool_use_id,
119
+ selected=selected,
120
+ cancelled=not bool(selected),
121
+ )
122
+ )
123
+ elif isinstance(event, ToolResultEvent):
124
+ if event.is_error:
125
+ sys.stderr.write(f"\n[Error: {event.result}]\n")
126
+ sys.stderr.flush()
127
+ elif isinstance(event, StreamRetryEvent):
128
+ sys.stderr.write(f"\n[↻ {safe_utf8_str(event.message)}]\n")
129
+ sys.stderr.flush()
130
+ elif isinstance(event, ErrorEvent):
131
+ sys.stderr.write(f"\nError: {safe_utf8_str(event.message)}\n")
132
+ sys.stderr.flush()
133
+ if not event.recoverable:
134
+ sys.exit(1)
135
+ elif isinstance(event, TurnCompleteEvent):
136
+ usage_text = _format_context_usage(event)
137
+ if usage_text:
138
+ sys.stderr.write(f"\n{usage_text}\n")
139
+ sys.stderr.flush()
140
+ finally:
141
+ try:
142
+ await session.close()
143
+ finally:
144
+ if computer_use is not None:
145
+ await computer_use.close()
146
+
147
+ sys.stdout.write("\n")
148
+ sys.stdout.flush()
File without changes
@@ -0,0 +1,19 @@
1
+ """Message rendering utilities for the CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.markdown import Markdown
7
+ from rich.panel import Panel
8
+
9
+
10
+ def render_assistant_markdown(text: str, console: Console | None = None) -> None:
11
+ """Render assistant text as Markdown."""
12
+ c = console or Console()
13
+ c.print(Markdown(text))
14
+
15
+
16
+ def render_error(message: str, console: Console | None = None) -> None:
17
+ """Render an error message."""
18
+ c = console or Console()
19
+ c.print(Panel(message, title="Error", border_style="red"))