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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- 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"))
|