deepy-cli 0.1.6__tar.gz → 0.1.7__tar.gz
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.
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/PKG-INFO +2 -2
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/README.md +1 -1
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/pyproject.toml +1 -1
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/__init__.py +1 -1
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/data/tools/WebFetch.md +2 -1
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/sessions/jsonl.py +39 -7
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/tools/builtin.py +69 -11
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/message_view.py +116 -29
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/styles.py +14 -17
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/terminal.py +21 -2
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/cli.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/config/settings.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/data/tools/edit.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/data/tools/modify.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/data/tools/read.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/data/tools/shell.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/data/tools/write.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/errors.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/llm/agent.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/llm/runner.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/prompts/system.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/prompts/tool_docs.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/skills.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/status.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/tools/agents.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/tools/file_state.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/prompt_input.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/usage.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.7}/src/deepy/utils/notify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: deepy-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Deepy - Vibe coding for DeepSeek models in your terminal
|
|
5
5
|
Keywords: deepseek,coding-agent,terminal,cli,agents
|
|
6
6
|
Author: kirineko
|
|
@@ -238,5 +238,5 @@ assets live outside the package directory and are not included in the wheel.
|
|
|
238
238
|
|
|
239
239
|
## Release Status
|
|
240
240
|
|
|
241
|
-
Deepy `0.1.
|
|
241
|
+
Deepy `0.1.7` is released through GitHub and PyPI. Standalone binaries and npm
|
|
242
242
|
wrappers can be added later, but the primary distribution is the Python CLI.
|
|
@@ -210,5 +210,5 @@ assets live outside the package directory and are not included in the wheel.
|
|
|
210
210
|
|
|
211
211
|
## Release Status
|
|
212
212
|
|
|
213
|
-
Deepy `0.1.
|
|
213
|
+
Deepy `0.1.7` is released through GitHub and PyPI. Standalone binaries and npm
|
|
214
214
|
wrappers can be added later, but the primary distribution is the Python CLI.
|
|
@@ -5,5 +5,6 @@ Fetch a specific web page when the user provides a complete URL.
|
|
|
5
5
|
Args: `url`.
|
|
6
6
|
|
|
7
7
|
Accepts only complete `http://` or `https://` URLs. Returns the final URL, title,
|
|
8
|
-
content type, and extracted readable text for HTML pages
|
|
8
|
+
content type, and extracted readable text for HTML pages, including standard
|
|
9
|
+
description metadata when ordinary body text is unavailable. Use `WebSearch` to
|
|
9
10
|
discover URLs; use `WebFetch` when the URL is already known.
|
|
@@ -13,6 +13,8 @@ from deepy.utils import json as json_utils
|
|
|
13
13
|
|
|
14
14
|
SESSION_INDEX_VERSION = 2
|
|
15
15
|
MAX_SESSION_INDEX_ENTRIES = 50
|
|
16
|
+
CONTEXT_UNDERCOUNT_REPAIR_RATIO = 2
|
|
17
|
+
CONTEXT_UNDERCOUNT_REPAIR_MIN_DELTA = 128
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
@dataclass(frozen=True)
|
|
@@ -147,11 +149,13 @@ class DeepyJsonlSession:
|
|
|
147
149
|
return
|
|
148
150
|
previous = _entry_for_session(self.path.parent / "sessions-index.json", self.session_id)
|
|
149
151
|
accumulated = merge_usage(previous.get("usage") if previous else None, normalized)
|
|
152
|
+
current_state = self.context_token_state()
|
|
153
|
+
checkpoint_tokens = max(normalized.prompt_tokens, current_state.active_tokens)
|
|
150
154
|
record_count = len(self._load_records())
|
|
151
155
|
self._touch_index(
|
|
152
|
-
active_tokens=
|
|
156
|
+
active_tokens=checkpoint_tokens,
|
|
153
157
|
usage=accumulated.to_dict(),
|
|
154
|
-
last_usage_tokens=
|
|
158
|
+
last_usage_tokens=checkpoint_tokens,
|
|
155
159
|
pending_tokens=0,
|
|
156
160
|
last_usage_record_count=record_count,
|
|
157
161
|
)
|
|
@@ -166,13 +170,27 @@ class DeepyJsonlSession:
|
|
|
166
170
|
last_usage_tokens = _optional_int(previous.get("lastUsageTokens"))
|
|
167
171
|
last_usage_record_count = _optional_int(previous.get("lastUsageRecordCount"))
|
|
168
172
|
if last_usage_tokens is not None and last_usage_record_count is not None:
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
if last_usage_record_count > len(source):
|
|
174
|
+
active_tokens = self._estimate_active_tokens(source)
|
|
175
|
+
return ContextTokenState(
|
|
176
|
+
active_tokens=active_tokens,
|
|
177
|
+
last_usage_tokens=None,
|
|
178
|
+
pending_tokens=0,
|
|
179
|
+
last_usage_record_count=None,
|
|
180
|
+
estimated=True,
|
|
181
|
+
)
|
|
182
|
+
pending_tokens = sum(_estimate_record_tokens(record) for record in source[last_usage_record_count:])
|
|
183
|
+
checkpoint_tokens = last_usage_tokens + pending_tokens
|
|
184
|
+
estimated_tokens = self._estimate_active_tokens(source)
|
|
185
|
+
active_tokens = _repair_undercounted_context_tokens(
|
|
186
|
+
checkpoint_tokens,
|
|
187
|
+
estimated_tokens,
|
|
188
|
+
)
|
|
171
189
|
return ContextTokenState(
|
|
172
|
-
active_tokens=
|
|
190
|
+
active_tokens=active_tokens,
|
|
173
191
|
last_usage_tokens=last_usage_tokens,
|
|
174
|
-
pending_tokens=pending_tokens,
|
|
175
|
-
last_usage_record_count=
|
|
192
|
+
pending_tokens=max(active_tokens - last_usage_tokens, pending_tokens),
|
|
193
|
+
last_usage_record_count=last_usage_record_count,
|
|
176
194
|
estimated=True,
|
|
177
195
|
)
|
|
178
196
|
active_tokens = self._estimate_active_tokens(source)
|
|
@@ -348,6 +366,9 @@ class DeepyJsonlSession:
|
|
|
348
366
|
|
|
349
367
|
@dataclass(frozen=True)
|
|
350
368
|
class ContextTokenState:
|
|
369
|
+
# active_tokens is the effective context pressure used by UI and auto compaction.
|
|
370
|
+
# last_usage_tokens is the latest checkpoint covered by usage or session rewrite.
|
|
371
|
+
# pending_tokens estimates records appended after that checkpoint.
|
|
351
372
|
active_tokens: int
|
|
352
373
|
last_usage_tokens: int | None = None
|
|
353
374
|
pending_tokens: int = 0
|
|
@@ -452,3 +473,14 @@ def _estimate_record_tokens(record: dict[str, Any]) -> int:
|
|
|
452
473
|
if item is not None:
|
|
453
474
|
return estimate_tokens_for_item(item)
|
|
454
475
|
return estimate_tokens_for_item(record.get("content", ""))
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _repair_undercounted_context_tokens(checkpoint_tokens: int, estimated_tokens: int) -> int:
|
|
479
|
+
if estimated_tokens <= checkpoint_tokens:
|
|
480
|
+
return checkpoint_tokens
|
|
481
|
+
if (
|
|
482
|
+
estimated_tokens - checkpoint_tokens >= CONTEXT_UNDERCOUNT_REPAIR_MIN_DELTA
|
|
483
|
+
and estimated_tokens >= checkpoint_tokens * CONTEXT_UNDERCOUNT_REPAIR_RATIO
|
|
484
|
+
):
|
|
485
|
+
return estimated_tokens
|
|
486
|
+
return checkpoint_tokens
|
|
@@ -38,6 +38,7 @@ MAX_BASH_OUTPUT_CHARS = 30_000
|
|
|
38
38
|
MAX_BASH_CAPTURE_CHARS = 10 * 1024 * 1024
|
|
39
39
|
MAX_WEB_FETCH_BYTES = 2 * 1024 * 1024
|
|
40
40
|
MAX_WEB_FETCH_OUTPUT_CHARS = 30_000
|
|
41
|
+
MIN_USEFUL_WEB_FETCH_BODY_CHARS = 40
|
|
41
42
|
DEFAULT_WEB_SEARCH_URL = "https://html.duckduckgo.com/html/"
|
|
42
43
|
DEFAULT_WEB_SEARCH_RESULTS = 8
|
|
43
44
|
MAX_WEB_SEARCH_CALLS_PER_TURN = 8
|
|
@@ -942,12 +943,15 @@ class _ReadableHtmlParser(HTMLParser):
|
|
|
942
943
|
super().__init__(convert_charrefs=True)
|
|
943
944
|
self.title_parts: list[str] = []
|
|
944
945
|
self.text_parts: list[str] = []
|
|
946
|
+
self.description_candidates: dict[str, str] = {}
|
|
945
947
|
self._in_title = False
|
|
946
948
|
self._skip_depth = 0
|
|
947
949
|
|
|
948
950
|
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
949
|
-
del attrs
|
|
950
951
|
normalized = tag.lower()
|
|
952
|
+
if normalized == "meta":
|
|
953
|
+
self._record_meta_description(attrs)
|
|
954
|
+
return
|
|
951
955
|
if normalized in self.SKIP_TAGS:
|
|
952
956
|
self._skip_depth += 1
|
|
953
957
|
return
|
|
@@ -994,6 +998,27 @@ class _ReadableHtmlParser(HTMLParser):
|
|
|
994
998
|
raw = re.sub(r"\n{3,}", "\n\n", raw)
|
|
995
999
|
return "\n".join(line.strip() for line in raw.splitlines()).strip()
|
|
996
1000
|
|
|
1001
|
+
@property
|
|
1002
|
+
def meta_description(self) -> str:
|
|
1003
|
+
for key in ("description", "og:description", "twitter:description"):
|
|
1004
|
+
text = self.description_candidates.get(key, "").strip()
|
|
1005
|
+
if text:
|
|
1006
|
+
return text
|
|
1007
|
+
return ""
|
|
1008
|
+
|
|
1009
|
+
def _record_meta_description(self, attrs: list[tuple[str, str | None]]) -> None:
|
|
1010
|
+
attr_map = {name.lower(): value for name, value in attrs if value is not None}
|
|
1011
|
+
raw_key = attr_map.get("name") or attr_map.get("property")
|
|
1012
|
+
content = attr_map.get("content", "")
|
|
1013
|
+
if not raw_key or not content:
|
|
1014
|
+
return
|
|
1015
|
+
key = raw_key.strip().lower()
|
|
1016
|
+
if key not in {"description", "og:description", "twitter:description"}:
|
|
1017
|
+
return
|
|
1018
|
+
normalized = " ".join(content.split()).strip()
|
|
1019
|
+
if normalized and key not in self.description_candidates:
|
|
1020
|
+
self.description_candidates[key] = normalized
|
|
1021
|
+
|
|
997
1022
|
|
|
998
1023
|
def _validate_web_fetch_url(url: str) -> tuple[str | None, str | None]:
|
|
999
1024
|
stripped = url.strip()
|
|
@@ -1016,11 +1041,31 @@ def _is_html_response(content_type: str, text: str) -> bool:
|
|
|
1016
1041
|
return "<html" in prefix or "<!doctype html" in prefix
|
|
1017
1042
|
|
|
1018
1043
|
|
|
1019
|
-
def _extract_readable_html(html: str) -> tuple[str, str]:
|
|
1044
|
+
def _extract_readable_html(html: str) -> tuple[str, str, str]:
|
|
1020
1045
|
parser = _ReadableHtmlParser()
|
|
1021
1046
|
parser.feed(html)
|
|
1022
1047
|
parser.close()
|
|
1023
|
-
return parser.title, parser.readable_text
|
|
1048
|
+
return parser.title, parser.readable_text, parser.meta_description
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _select_web_fetch_html_text(readable_text: str, metadata_text: str) -> str:
|
|
1052
|
+
stripped = readable_text.strip()
|
|
1053
|
+
if stripped and _is_useful_web_fetch_body_text(stripped):
|
|
1054
|
+
return stripped
|
|
1055
|
+
return metadata_text.strip() or stripped
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _is_useful_web_fetch_body_text(text: str) -> bool:
|
|
1059
|
+
normalized = " ".join(text.split()).strip().lower()
|
|
1060
|
+
if len(normalized) >= MIN_USEFUL_WEB_FETCH_BODY_CHARS:
|
|
1061
|
+
return True
|
|
1062
|
+
return normalized not in {
|
|
1063
|
+
"",
|
|
1064
|
+
"loading",
|
|
1065
|
+
"loading...",
|
|
1066
|
+
"please enable javascript",
|
|
1067
|
+
"you need to enable javascript to run this app.",
|
|
1068
|
+
}
|
|
1024
1069
|
|
|
1025
1070
|
|
|
1026
1071
|
def _format_web_fetch_output(
|
|
@@ -1473,18 +1518,16 @@ class ToolRuntime:
|
|
|
1473
1518
|
request = urllib.request.Request(
|
|
1474
1519
|
target_url,
|
|
1475
1520
|
headers={
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
"AppleWebKit/537.36 (KHTML, like Gecko) Deepy/0.1"
|
|
1479
|
-
),
|
|
1480
|
-
"Accept": "text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8",
|
|
1521
|
+
**WEB_SEARCH_BROWSER_HEADERS,
|
|
1522
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.7",
|
|
1481
1523
|
},
|
|
1482
1524
|
method="GET",
|
|
1483
1525
|
)
|
|
1484
1526
|
try:
|
|
1485
1527
|
with urllib.request.urlopen(request, timeout=30) as response:
|
|
1486
1528
|
final_url = response.geturl()
|
|
1487
|
-
content_type = response
|
|
1529
|
+
content_type = _response_header(response, "Content-Type") or ""
|
|
1530
|
+
content_encoding = _response_header(response, "Content-Encoding")
|
|
1488
1531
|
body = response.read(MAX_WEB_FETCH_BYTES + 1)
|
|
1489
1532
|
except Exception as exc:
|
|
1490
1533
|
return ToolResult.error_result(
|
|
@@ -1501,9 +1544,24 @@ class ToolRuntime:
|
|
|
1501
1544
|
bytes_truncated = len(body) > MAX_WEB_FETCH_BYTES
|
|
1502
1545
|
body = body[:MAX_WEB_FETCH_BYTES]
|
|
1503
1546
|
charset = _charset_from_content_type(content_type)
|
|
1504
|
-
|
|
1547
|
+
try:
|
|
1548
|
+
decoded = _decode_http_body(body, encoding=content_encoding, charset=charset)
|
|
1549
|
+
except Exception as exc:
|
|
1550
|
+
return ToolResult.error_result(
|
|
1551
|
+
name,
|
|
1552
|
+
f"WebFetch response decode failed: {exc}",
|
|
1553
|
+
metadata={
|
|
1554
|
+
"url": target_url,
|
|
1555
|
+
"finalUrl": final_url,
|
|
1556
|
+
"contentType": content_type,
|
|
1557
|
+
"contentEncoding": content_encoding,
|
|
1558
|
+
"charset": charset,
|
|
1559
|
+
"activityLabel": activity_label,
|
|
1560
|
+
},
|
|
1561
|
+
).to_json()
|
|
1505
1562
|
if _is_html_response(content_type, decoded):
|
|
1506
|
-
title, readable_text = _extract_readable_html(decoded)
|
|
1563
|
+
title, readable_text, metadata_text = _extract_readable_html(decoded)
|
|
1564
|
+
readable_text = _select_web_fetch_html_text(readable_text, metadata_text)
|
|
1507
1565
|
else:
|
|
1508
1566
|
title = ""
|
|
1509
1567
|
readable_text = decoded.strip()
|
|
@@ -4,8 +4,11 @@ import re
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from rich.cells import cell_len
|
|
7
8
|
from rich.console import Group
|
|
8
9
|
from rich.panel import Panel
|
|
10
|
+
from rich.style import Style
|
|
11
|
+
from rich.syntax import Syntax
|
|
9
12
|
from rich.text import Text
|
|
10
13
|
|
|
11
14
|
from deepy.utils import json as json_utils
|
|
@@ -19,6 +22,8 @@ from deepy.ui.styles import (
|
|
|
19
22
|
MAX_SUMMARY_CHARS = 160
|
|
20
23
|
MAX_THINKING_SUMMARY_CHARS = 360
|
|
21
24
|
MAX_DIFF_LINES = 80
|
|
25
|
+
MAX_SYNTAX_SAMPLE_CHARS = 4_000
|
|
26
|
+
MAX_SYNTAX_SAMPLE_LINES = 80
|
|
22
27
|
DIFF_PREVIEW_TOOLS = {"edit", "write"}
|
|
23
28
|
ROLE_TITLES = {
|
|
24
29
|
"user": "You",
|
|
@@ -146,6 +151,7 @@ def render_tool_diff_preview(
|
|
|
146
151
|
*,
|
|
147
152
|
max_lines: int = MAX_DIFF_LINES,
|
|
148
153
|
palette: UiPalette | None = None,
|
|
154
|
+
width: int | None = None,
|
|
149
155
|
) -> Group | None:
|
|
150
156
|
palette = palette or DARK_PALETTE
|
|
151
157
|
view = parse_tool_output(output)
|
|
@@ -158,14 +164,11 @@ def render_tool_diff_preview(
|
|
|
158
164
|
preview = parse_diff_preview_view(diff, path=view.path)
|
|
159
165
|
if not preview.lines:
|
|
160
166
|
return None
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
render_diff_preview_header(preview, label="Wrote", palette=palette),
|
|
164
|
-
*(render_write_preview_line(line, palette=palette) for line in preview.lines),
|
|
165
|
-
)
|
|
167
|
+
syntax = _diff_preview_syntax(preview, palette)
|
|
168
|
+
label = "Wrote" if view.name.lower() == "write" else "Edited"
|
|
166
169
|
return Group(
|
|
167
|
-
render_diff_preview_header(preview, label=
|
|
168
|
-
*(render_diff_preview_line(line, palette=palette) for line in preview.lines),
|
|
170
|
+
render_diff_preview_header(preview, label=label, palette=palette),
|
|
171
|
+
*(render_diff_preview_line(line, palette=palette, width=width, syntax=syntax) for line in preview.lines),
|
|
169
172
|
)
|
|
170
173
|
|
|
171
174
|
|
|
@@ -192,20 +195,36 @@ def render_diff_preview_header(
|
|
|
192
195
|
return Text(f"• {label}", style=f"bold {palette.info}")
|
|
193
196
|
|
|
194
197
|
|
|
195
|
-
def render_diff_preview_line(
|
|
198
|
+
def render_diff_preview_line(
|
|
199
|
+
line: DiffPreviewLine,
|
|
200
|
+
*,
|
|
201
|
+
palette: UiPalette | None = None,
|
|
202
|
+
width: int | None = None,
|
|
203
|
+
syntax: Syntax | None = None,
|
|
204
|
+
) -> Text:
|
|
196
205
|
palette = palette or DARK_PALETTE
|
|
197
206
|
content = line.content if line.content else " "
|
|
198
207
|
old_lineno = _line_number_text(line.old_lineno)
|
|
199
208
|
new_lineno = _line_number_text(line.new_lineno)
|
|
200
209
|
if line.kind == "added":
|
|
201
|
-
return
|
|
202
|
-
(
|
|
203
|
-
|
|
210
|
+
return _pad_changed_diff_line(
|
|
211
|
+
Text.assemble(
|
|
212
|
+
(f"{old_lineno} {new_lineno} ", palette.diff_added_gutter),
|
|
213
|
+
("+ ", palette.diff_added_marker),
|
|
214
|
+
_highlight_diff_content(content, syntax=syntax, style=palette.diff_added),
|
|
215
|
+
),
|
|
216
|
+
width=width,
|
|
217
|
+
style=palette.diff_added,
|
|
204
218
|
)
|
|
205
219
|
if line.kind == "removed":
|
|
206
|
-
return
|
|
207
|
-
(
|
|
208
|
-
|
|
220
|
+
return _pad_changed_diff_line(
|
|
221
|
+
Text.assemble(
|
|
222
|
+
(f"{old_lineno} {new_lineno} ", palette.diff_removed_gutter),
|
|
223
|
+
("- ", palette.diff_removed_marker),
|
|
224
|
+
_highlight_diff_content(content, syntax=syntax, style=palette.diff_removed),
|
|
225
|
+
),
|
|
226
|
+
width=width,
|
|
227
|
+
style=palette.diff_removed,
|
|
209
228
|
)
|
|
210
229
|
return Text.assemble(
|
|
211
230
|
(f"{old_lineno} {new_lineno} ", palette.diff_context),
|
|
@@ -213,18 +232,80 @@ def render_diff_preview_line(line: DiffPreviewLine, *, palette: UiPalette | None
|
|
|
213
232
|
)
|
|
214
233
|
|
|
215
234
|
|
|
216
|
-
def
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
235
|
+
def _pad_changed_diff_line(text: Text, *, width: int | None, style: str) -> Text:
|
|
236
|
+
if width is None or width <= 0:
|
|
237
|
+
return text
|
|
238
|
+
padding = width - cell_len(text.plain)
|
|
239
|
+
if padding > 0:
|
|
240
|
+
text.append(" " * padding, style=style)
|
|
241
|
+
return text
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _diff_preview_syntax(preview: DiffPreview, palette: UiPalette) -> Syntax | None:
|
|
245
|
+
lexer = _guess_diff_preview_lexer(preview)
|
|
246
|
+
if lexer is None:
|
|
247
|
+
return None
|
|
248
|
+
try:
|
|
249
|
+
return Syntax("", lexer, theme=_diff_syntax_theme(palette), line_numbers=False)
|
|
250
|
+
except Exception:
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _guess_diff_preview_lexer(preview: DiffPreview) -> str | None:
|
|
255
|
+
if not preview.path:
|
|
256
|
+
return None
|
|
257
|
+
sample_lines: list[str] = []
|
|
258
|
+
sample_size = 0
|
|
259
|
+
for line in preview.lines:
|
|
260
|
+
if line.kind not in {"added", "removed", "context"} or not line.content.strip():
|
|
261
|
+
continue
|
|
262
|
+
sample_lines.append(line.content)
|
|
263
|
+
sample_size += len(line.content) + 1
|
|
264
|
+
if len(sample_lines) >= MAX_SYNTAX_SAMPLE_LINES or sample_size >= MAX_SYNTAX_SAMPLE_CHARS:
|
|
265
|
+
break
|
|
266
|
+
sample = "\n".join(sample_lines)
|
|
267
|
+
if not sample.strip():
|
|
268
|
+
return None
|
|
269
|
+
try:
|
|
270
|
+
lexer = Syntax.guess_lexer(preview.path, sample)
|
|
271
|
+
except Exception:
|
|
272
|
+
return None
|
|
273
|
+
return lexer if lexer and lexer != "default" else None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _highlight_diff_content(
|
|
277
|
+
content: str,
|
|
278
|
+
*,
|
|
279
|
+
syntax: Syntax | None,
|
|
280
|
+
style: str,
|
|
281
|
+
) -> Text:
|
|
282
|
+
if syntax is None or not content.strip():
|
|
283
|
+
return Text(content, style=style)
|
|
284
|
+
try:
|
|
285
|
+
highlighted = syntax.highlight(content)
|
|
286
|
+
except Exception:
|
|
287
|
+
return Text(content, style=style)
|
|
288
|
+
|
|
289
|
+
base = Style.parse(style)
|
|
290
|
+
text = Text(content, style=base)
|
|
291
|
+
for span in highlighted.spans:
|
|
292
|
+
text.stylize(_syntax_style_on_diff_background(span.style, base), span.start, span.end)
|
|
293
|
+
return text
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _diff_syntax_theme(palette: UiPalette) -> str:
|
|
297
|
+
return "default" if palette.name == "light" else "monokai"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _syntax_style_on_diff_background(style: Style, base: Style) -> Style:
|
|
301
|
+
return Style(
|
|
302
|
+
color=style.color or base.color,
|
|
303
|
+
bgcolor=base.bgcolor,
|
|
304
|
+
bold=style.bold,
|
|
305
|
+
italic=style.italic,
|
|
306
|
+
underline=style.underline,
|
|
307
|
+
dim=style.dim,
|
|
308
|
+
strike=style.strike,
|
|
228
309
|
)
|
|
229
310
|
|
|
230
311
|
|
|
@@ -342,11 +423,16 @@ def is_invisible_execution(content: str) -> bool:
|
|
|
342
423
|
return isinstance(parsed, dict) and parsed.get("name") == "shell" and parsed.get("ok") is not True
|
|
343
424
|
|
|
344
425
|
|
|
345
|
-
def render_tool_output(
|
|
426
|
+
def render_tool_output(
|
|
427
|
+
output: str,
|
|
428
|
+
*,
|
|
429
|
+
palette: UiPalette | None = None,
|
|
430
|
+
width: int | None = None,
|
|
431
|
+
) -> Group:
|
|
346
432
|
palette = palette or DARK_PALETTE
|
|
347
433
|
view = parse_tool_output(output)
|
|
348
434
|
parts: list[Any] = [Text(view.summary, style=status_style(view.ok, palette))]
|
|
349
|
-
diff = render_tool_diff_preview(output, palette=palette)
|
|
435
|
+
diff = render_tool_diff_preview(output, palette=palette, width=width)
|
|
350
436
|
if diff:
|
|
351
437
|
parts.append(diff)
|
|
352
438
|
return Group(*parts)
|
|
@@ -357,12 +443,13 @@ def render_message(
|
|
|
357
443
|
*,
|
|
358
444
|
project_root: str | None = None,
|
|
359
445
|
palette: UiPalette | None = None,
|
|
446
|
+
width: int | None = None,
|
|
360
447
|
) -> Any:
|
|
361
448
|
palette = palette or DARK_PALETTE
|
|
362
449
|
role = _string_or_default(message.get("role"), "message")
|
|
363
450
|
content = _message_content_text(message.get("content"))
|
|
364
451
|
if role == "tool":
|
|
365
|
-
return render_tool_output(content, palette=palette)
|
|
452
|
+
return render_tool_output(content, palette=palette, width=width)
|
|
366
453
|
|
|
367
454
|
title = ROLE_TITLES.get(role, role.title())
|
|
368
455
|
if role == "assistant":
|
|
@@ -32,12 +32,11 @@ class UiPalette:
|
|
|
32
32
|
panel_border: str
|
|
33
33
|
diff_added: str
|
|
34
34
|
diff_added_gutter: str
|
|
35
|
+
diff_added_marker: str
|
|
35
36
|
diff_removed: str
|
|
36
37
|
diff_removed_gutter: str
|
|
38
|
+
diff_removed_marker: str
|
|
37
39
|
diff_context: str
|
|
38
|
-
write_preview_gutter: str
|
|
39
|
-
write_preview_content: str
|
|
40
|
-
write_preview_removed: str
|
|
41
40
|
prompt: str
|
|
42
41
|
placeholder: str
|
|
43
42
|
toolbar_background: str
|
|
@@ -69,14 +68,13 @@ DARK_PALETTE = UiPalette(
|
|
|
69
68
|
system=STYLE_SYSTEM,
|
|
70
69
|
tool=STYLE_TOOL,
|
|
71
70
|
panel_border=STYLE_INFO,
|
|
72
|
-
diff_added="#e5e7eb on #
|
|
73
|
-
diff_added_gutter="#cbd5e1 on #
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
diff_added="#e5e7eb on #1f3d2b",
|
|
72
|
+
diff_added_gutter="#cbd5e1 on #1f3d2b",
|
|
73
|
+
diff_added_marker="bold #86efac on #1f3d2b",
|
|
74
|
+
diff_removed="#e5e7eb on #4a2528",
|
|
75
|
+
diff_removed_gutter="#cbd5e1 on #4a2528",
|
|
76
|
+
diff_removed_marker="bold #fca5a5 on #4a2528",
|
|
76
77
|
diff_context=STYLE_MUTED,
|
|
77
|
-
write_preview_gutter="#94a3b8 on #1f2937",
|
|
78
|
-
write_preview_content="#d7def8 on #1f2937",
|
|
79
|
-
write_preview_removed="#fecaca on #7f1d1d",
|
|
80
78
|
prompt="ansicyan bold",
|
|
81
79
|
placeholder="#8a90aa",
|
|
82
80
|
toolbar_background="#161821",
|
|
@@ -108,14 +106,13 @@ LIGHT_PALETTE = UiPalette(
|
|
|
108
106
|
system="#7e22ce",
|
|
109
107
|
tool="#92400e",
|
|
110
108
|
panel_border="#2563eb",
|
|
111
|
-
diff_added="#064e3b on #
|
|
112
|
-
diff_added_gutter="#065f46 on #
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
diff_added="#064e3b on #ecfdf5",
|
|
110
|
+
diff_added_gutter="#065f46 on #d1fae5",
|
|
111
|
+
diff_added_marker="bold #047857 on #d1fae5",
|
|
112
|
+
diff_removed="#7f1d1d on #fef2f2",
|
|
113
|
+
diff_removed_gutter="#991b1b on #fee2e2",
|
|
114
|
+
diff_removed_marker="bold #b91c1c on #fee2e2",
|
|
115
115
|
diff_context="#374151",
|
|
116
|
-
write_preview_gutter="#475569 on #e2e8f0",
|
|
117
|
-
write_preview_content="#111827 on #f8fafc",
|
|
118
|
-
write_preview_removed="#7f1d1d on #fee2e2",
|
|
119
116
|
prompt="#0369a1 bold",
|
|
120
117
|
placeholder="#64748b",
|
|
121
118
|
toolbar_background="#e2e8f0",
|
|
@@ -1328,7 +1328,7 @@ def _format_context_footer(
|
|
|
1328
1328
|
return " · ".join(parts)
|
|
1329
1329
|
|
|
1330
1330
|
session_entry = _session_entry(project_root, session_id)
|
|
1331
|
-
used_tokens =
|
|
1331
|
+
used_tokens = _session_context_tokens(project_root, session_id, session_entry)
|
|
1332
1332
|
used_text = _format_token_count_short(used_tokens) if used_tokens is not None else "unknown"
|
|
1333
1333
|
used_ratio = (
|
|
1334
1334
|
f" ({used_tokens / window_tokens * 100:.1f}%)"
|
|
@@ -1372,6 +1372,25 @@ def _session_entry(project_root: Path | None, session_id: str | None) -> Session
|
|
|
1372
1372
|
return entry
|
|
1373
1373
|
|
|
1374
1374
|
|
|
1375
|
+
def _session_context_tokens(
|
|
1376
|
+
project_root: Path | None,
|
|
1377
|
+
session_id: str | None,
|
|
1378
|
+
session_entry: SessionEntry | None,
|
|
1379
|
+
) -> int | None:
|
|
1380
|
+
if not session_id:
|
|
1381
|
+
return 0
|
|
1382
|
+
if project_root is not None:
|
|
1383
|
+
try:
|
|
1384
|
+
session = DeepyJsonlSession.open(project_root, session_id)
|
|
1385
|
+
if session.path.exists():
|
|
1386
|
+
return session.context_token_state().active_tokens
|
|
1387
|
+
except Exception:
|
|
1388
|
+
pass
|
|
1389
|
+
if session_entry is not None:
|
|
1390
|
+
return session_entry.active_tokens
|
|
1391
|
+
return None
|
|
1392
|
+
|
|
1393
|
+
|
|
1375
1394
|
def _format_token_count_short(value: int) -> str:
|
|
1376
1395
|
if value < 1_000:
|
|
1377
1396
|
return str(value)
|
|
@@ -1500,7 +1519,7 @@ def _print_stream_event(
|
|
|
1500
1519
|
call_summary = call.summary if call is not None else view.name
|
|
1501
1520
|
summary = format_tool_progress_summary(call_summary, event.text)
|
|
1502
1521
|
console.print(_status_line(summary, status_style(view.ok, palette)))
|
|
1503
|
-
diff = render_tool_diff_preview(event.text, palette=palette)
|
|
1522
|
+
diff = render_tool_diff_preview(event.text, palette=palette, width=console.width)
|
|
1504
1523
|
if diff:
|
|
1505
1524
|
console.print(diff)
|
|
1506
1525
|
return
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|