deepy-cli 0.1.6__tar.gz → 0.1.8__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.8}/PKG-INFO +2 -2
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/README.md +1 -1
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/pyproject.toml +1 -1
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/__init__.py +1 -1
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/data/tools/WebFetch.md +2 -1
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/data/tools/shell.md +4 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/sessions/jsonl.py +39 -7
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/tools/builtin.py +112 -15
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/message_view.py +116 -29
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/prompt_input.py +56 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/styles.py +14 -17
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/terminal.py +21 -2
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/cli.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/config/settings.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/data/tools/edit.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/data/tools/modify.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/data/tools/read.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/data/tools/write.md +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/errors.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/llm/agent.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/llm/runner.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/prompts/system.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/prompts/tool_docs.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/skills.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/status.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/tools/agents.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/tools/file_state.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/usage.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.1.6 → deepy_cli-0.1.8}/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.8
|
|
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.8` 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.8` 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.
|
|
@@ -9,5 +9,9 @@ Use the runtime context's command dialect and path style: PowerShell uses
|
|
|
9
9
|
PowerShell commands and Windows paths, `cmd` uses cmd syntax, and `posix` uses
|
|
10
10
|
POSIX shell syntax.
|
|
11
11
|
|
|
12
|
+
On Windows PowerShell, Python child processes run with UTF-8 I/O defaults for
|
|
13
|
+
the command invocation; do not ask users to run `chcp` or change their
|
|
14
|
+
PowerShell profile for Unicode output.
|
|
15
|
+
|
|
12
16
|
Runs in the session cwd, preserves cwd between calls when supported, and returns
|
|
13
17
|
stdout/stderr JSON with cwd, exit-code, and shell metadata.
|
|
@@ -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
|
|
@@ -207,6 +208,7 @@ class ShellInvocation:
|
|
|
207
208
|
shell_path: str
|
|
208
209
|
args: list[str]
|
|
209
210
|
runtime_environment: RuntimeEnvironment
|
|
211
|
+
env: dict[str, str] | None = None
|
|
210
212
|
|
|
211
213
|
|
|
212
214
|
def _find_occurrences(text: str, needle: str, scope: tuple[int, int]) -> list[MatchOccurrence]:
|
|
@@ -942,12 +944,15 @@ class _ReadableHtmlParser(HTMLParser):
|
|
|
942
944
|
super().__init__(convert_charrefs=True)
|
|
943
945
|
self.title_parts: list[str] = []
|
|
944
946
|
self.text_parts: list[str] = []
|
|
947
|
+
self.description_candidates: dict[str, str] = {}
|
|
945
948
|
self._in_title = False
|
|
946
949
|
self._skip_depth = 0
|
|
947
950
|
|
|
948
951
|
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
949
|
-
del attrs
|
|
950
952
|
normalized = tag.lower()
|
|
953
|
+
if normalized == "meta":
|
|
954
|
+
self._record_meta_description(attrs)
|
|
955
|
+
return
|
|
951
956
|
if normalized in self.SKIP_TAGS:
|
|
952
957
|
self._skip_depth += 1
|
|
953
958
|
return
|
|
@@ -994,6 +999,27 @@ class _ReadableHtmlParser(HTMLParser):
|
|
|
994
999
|
raw = re.sub(r"\n{3,}", "\n\n", raw)
|
|
995
1000
|
return "\n".join(line.strip() for line in raw.splitlines()).strip()
|
|
996
1001
|
|
|
1002
|
+
@property
|
|
1003
|
+
def meta_description(self) -> str:
|
|
1004
|
+
for key in ("description", "og:description", "twitter:description"):
|
|
1005
|
+
text = self.description_candidates.get(key, "").strip()
|
|
1006
|
+
if text:
|
|
1007
|
+
return text
|
|
1008
|
+
return ""
|
|
1009
|
+
|
|
1010
|
+
def _record_meta_description(self, attrs: list[tuple[str, str | None]]) -> None:
|
|
1011
|
+
attr_map = {name.lower(): value for name, value in attrs if value is not None}
|
|
1012
|
+
raw_key = attr_map.get("name") or attr_map.get("property")
|
|
1013
|
+
content = attr_map.get("content", "")
|
|
1014
|
+
if not raw_key or not content:
|
|
1015
|
+
return
|
|
1016
|
+
key = raw_key.strip().lower()
|
|
1017
|
+
if key not in {"description", "og:description", "twitter:description"}:
|
|
1018
|
+
return
|
|
1019
|
+
normalized = " ".join(content.split()).strip()
|
|
1020
|
+
if normalized and key not in self.description_candidates:
|
|
1021
|
+
self.description_candidates[key] = normalized
|
|
1022
|
+
|
|
997
1023
|
|
|
998
1024
|
def _validate_web_fetch_url(url: str) -> tuple[str | None, str | None]:
|
|
999
1025
|
stripped = url.strip()
|
|
@@ -1016,11 +1042,31 @@ def _is_html_response(content_type: str, text: str) -> bool:
|
|
|
1016
1042
|
return "<html" in prefix or "<!doctype html" in prefix
|
|
1017
1043
|
|
|
1018
1044
|
|
|
1019
|
-
def _extract_readable_html(html: str) -> tuple[str, str]:
|
|
1045
|
+
def _extract_readable_html(html: str) -> tuple[str, str, str]:
|
|
1020
1046
|
parser = _ReadableHtmlParser()
|
|
1021
1047
|
parser.feed(html)
|
|
1022
1048
|
parser.close()
|
|
1023
|
-
return parser.title, parser.readable_text
|
|
1049
|
+
return parser.title, parser.readable_text, parser.meta_description
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _select_web_fetch_html_text(readable_text: str, metadata_text: str) -> str:
|
|
1053
|
+
stripped = readable_text.strip()
|
|
1054
|
+
if stripped and _is_useful_web_fetch_body_text(stripped):
|
|
1055
|
+
return stripped
|
|
1056
|
+
return metadata_text.strip() or stripped
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _is_useful_web_fetch_body_text(text: str) -> bool:
|
|
1060
|
+
normalized = " ".join(text.split()).strip().lower()
|
|
1061
|
+
if len(normalized) >= MIN_USEFUL_WEB_FETCH_BODY_CHARS:
|
|
1062
|
+
return True
|
|
1063
|
+
return normalized not in {
|
|
1064
|
+
"",
|
|
1065
|
+
"loading",
|
|
1066
|
+
"loading...",
|
|
1067
|
+
"please enable javascript",
|
|
1068
|
+
"you need to enable javascript to run this app.",
|
|
1069
|
+
}
|
|
1024
1070
|
|
|
1025
1071
|
|
|
1026
1072
|
def _format_web_fetch_output(
|
|
@@ -1354,6 +1400,7 @@ class ToolRuntime:
|
|
|
1354
1400
|
process = subprocess.Popen(
|
|
1355
1401
|
[shell_invocation.shell_path, *shell_invocation.args],
|
|
1356
1402
|
cwd=self.cwd,
|
|
1403
|
+
env=shell_invocation.env,
|
|
1357
1404
|
text=True,
|
|
1358
1405
|
stdout=stdout_file,
|
|
1359
1406
|
stderr=stderr_file,
|
|
@@ -1473,18 +1520,16 @@ class ToolRuntime:
|
|
|
1473
1520
|
request = urllib.request.Request(
|
|
1474
1521
|
target_url,
|
|
1475
1522
|
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",
|
|
1523
|
+
**WEB_SEARCH_BROWSER_HEADERS,
|
|
1524
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.7",
|
|
1481
1525
|
},
|
|
1482
1526
|
method="GET",
|
|
1483
1527
|
)
|
|
1484
1528
|
try:
|
|
1485
1529
|
with urllib.request.urlopen(request, timeout=30) as response:
|
|
1486
1530
|
final_url = response.geturl()
|
|
1487
|
-
content_type = response
|
|
1531
|
+
content_type = _response_header(response, "Content-Type") or ""
|
|
1532
|
+
content_encoding = _response_header(response, "Content-Encoding")
|
|
1488
1533
|
body = response.read(MAX_WEB_FETCH_BYTES + 1)
|
|
1489
1534
|
except Exception as exc:
|
|
1490
1535
|
return ToolResult.error_result(
|
|
@@ -1501,9 +1546,24 @@ class ToolRuntime:
|
|
|
1501
1546
|
bytes_truncated = len(body) > MAX_WEB_FETCH_BYTES
|
|
1502
1547
|
body = body[:MAX_WEB_FETCH_BYTES]
|
|
1503
1548
|
charset = _charset_from_content_type(content_type)
|
|
1504
|
-
|
|
1549
|
+
try:
|
|
1550
|
+
decoded = _decode_http_body(body, encoding=content_encoding, charset=charset)
|
|
1551
|
+
except Exception as exc:
|
|
1552
|
+
return ToolResult.error_result(
|
|
1553
|
+
name,
|
|
1554
|
+
f"WebFetch response decode failed: {exc}",
|
|
1555
|
+
metadata={
|
|
1556
|
+
"url": target_url,
|
|
1557
|
+
"finalUrl": final_url,
|
|
1558
|
+
"contentType": content_type,
|
|
1559
|
+
"contentEncoding": content_encoding,
|
|
1560
|
+
"charset": charset,
|
|
1561
|
+
"activityLabel": activity_label,
|
|
1562
|
+
},
|
|
1563
|
+
).to_json()
|
|
1505
1564
|
if _is_html_response(content_type, decoded):
|
|
1506
|
-
title, readable_text = _extract_readable_html(decoded)
|
|
1565
|
+
title, readable_text, metadata_text = _extract_readable_html(decoded)
|
|
1566
|
+
readable_text = _select_web_fetch_html_text(readable_text, metadata_text)
|
|
1507
1567
|
else:
|
|
1508
1568
|
title = ""
|
|
1509
1569
|
readable_text = decoded.strip()
|
|
@@ -1692,8 +1752,7 @@ def _read_text_preserving_newlines(path: Path) -> str:
|
|
|
1692
1752
|
def _read_text_metadata(path: Path) -> TextFileMetadata:
|
|
1693
1753
|
data = path.read_bytes()
|
|
1694
1754
|
encoding = _detect_text_encoding(data)
|
|
1695
|
-
|
|
1696
|
-
text = data.decode(python_encoding, errors="replace")
|
|
1755
|
+
text = data.decode(_python_text_encoding(encoding), errors="replace")
|
|
1697
1756
|
return TextFileMetadata(
|
|
1698
1757
|
content=text,
|
|
1699
1758
|
encoding=encoding,
|
|
@@ -1704,12 +1763,32 @@ def _read_text_metadata(path: Path) -> TextFileMetadata:
|
|
|
1704
1763
|
def _detect_text_encoding(data: bytes) -> str:
|
|
1705
1764
|
if len(data) >= 2 and data[0] == 0xFF and data[1] == 0xFE:
|
|
1706
1765
|
return "utf16le"
|
|
1766
|
+
if data.startswith(b"\xef\xbb\xbf"):
|
|
1767
|
+
return "utf8-sig"
|
|
1768
|
+
try:
|
|
1769
|
+
data.decode("utf-8", errors="strict")
|
|
1770
|
+
return "utf8"
|
|
1771
|
+
except UnicodeDecodeError:
|
|
1772
|
+
pass
|
|
1773
|
+
try:
|
|
1774
|
+
data.decode("gb18030", errors="strict")
|
|
1775
|
+
return "gb18030"
|
|
1776
|
+
except UnicodeDecodeError:
|
|
1777
|
+
return "utf8"
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
def _python_text_encoding(encoding: str) -> str:
|
|
1781
|
+
if encoding == "utf16le":
|
|
1782
|
+
return "utf-16"
|
|
1783
|
+
if encoding == "utf8-sig":
|
|
1784
|
+
return "utf-8-sig"
|
|
1785
|
+
if encoding == "gb18030":
|
|
1786
|
+
return "gb18030"
|
|
1707
1787
|
return "utf8"
|
|
1708
1788
|
|
|
1709
1789
|
|
|
1710
1790
|
def _write_text_with_encoding(path: Path, content: str, encoding: str) -> None:
|
|
1711
|
-
|
|
1712
|
-
path.write_text(content, encoding=python_encoding)
|
|
1791
|
+
path.write_text(content, encoding=_python_text_encoding(encoding))
|
|
1713
1792
|
|
|
1714
1793
|
|
|
1715
1794
|
def _coerce_write_content(path: Path, content: object) -> tuple[str, dict[str, object], str | None]:
|
|
@@ -1976,25 +2055,41 @@ def _build_shell_command(
|
|
|
1976
2055
|
platform_name=platform_name,
|
|
1977
2056
|
os_name=os_name,
|
|
1978
2057
|
)
|
|
2058
|
+
process_env = _build_shell_process_env(runtime_environment, env)
|
|
1979
2059
|
if runtime_environment.command_dialect == "powershell":
|
|
1980
2060
|
return ShellInvocation(
|
|
1981
2061
|
shell_path=resolved_shell,
|
|
1982
2062
|
args=_build_powershell_args(command, marker),
|
|
1983
2063
|
runtime_environment=runtime_environment,
|
|
2064
|
+
env=process_env,
|
|
1984
2065
|
)
|
|
1985
2066
|
if runtime_environment.command_dialect == "cmd":
|
|
1986
2067
|
return ShellInvocation(
|
|
1987
2068
|
shell_path=resolved_shell,
|
|
1988
2069
|
args=_build_cmd_args(command, marker),
|
|
1989
2070
|
runtime_environment=runtime_environment,
|
|
2071
|
+
env=process_env,
|
|
1990
2072
|
)
|
|
1991
2073
|
return ShellInvocation(
|
|
1992
2074
|
shell_path=resolved_shell,
|
|
1993
2075
|
args=_build_posix_shell_args(command, marker, resolved_shell),
|
|
1994
2076
|
runtime_environment=runtime_environment,
|
|
2077
|
+
env=process_env,
|
|
1995
2078
|
)
|
|
1996
2079
|
|
|
1997
2080
|
|
|
2081
|
+
def _build_shell_process_env(
|
|
2082
|
+
runtime_environment: RuntimeEnvironment,
|
|
2083
|
+
env: dict[str, str] | None = None,
|
|
2084
|
+
) -> dict[str, str] | None:
|
|
2085
|
+
if runtime_environment.os_family != "windows":
|
|
2086
|
+
return dict(env) if env is not None else None
|
|
2087
|
+
process_env = dict(os.environ if env is None else env)
|
|
2088
|
+
process_env.setdefault("PYTHONUTF8", "1")
|
|
2089
|
+
process_env.setdefault("PYTHONIOENCODING", "utf-8")
|
|
2090
|
+
return process_env
|
|
2091
|
+
|
|
2092
|
+
|
|
1998
2093
|
def _build_posix_shell_args(command: str, marker: str, shell_path: str) -> list[str]:
|
|
1999
2094
|
normalized_command = rewrite_windows_null_redirect(command)
|
|
2000
2095
|
parts = [
|
|
@@ -2015,6 +2110,8 @@ def _build_posix_shell_args(command: str, marker: str, shell_path: str) -> list[
|
|
|
2015
2110
|
def _build_powershell_args(command: str, marker: str) -> list[str]:
|
|
2016
2111
|
script = "\n".join(
|
|
2017
2112
|
[
|
|
2113
|
+
"$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
|
|
2114
|
+
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
|
|
2018
2115
|
"$global:LASTEXITCODE = $null",
|
|
2019
2116
|
"try {",
|
|
2020
2117
|
command,
|
|
@@ -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":
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
import sys
|
|
5
6
|
from typing import Callable
|
|
6
7
|
from unicodedata import normalize
|
|
7
8
|
|
|
@@ -32,6 +33,7 @@ SHIFT_ENTER_SEQUENCES = (
|
|
|
32
33
|
"\x1b[27;2;13~", # xterm modified-key format.
|
|
33
34
|
"\x1b[13;2u", # Kitty/fixterms CSI-u format, used by modern terminals.
|
|
34
35
|
)
|
|
36
|
+
_WINDOWS_SHIFT_ENTER_PATCH_ATTR = "_deepy_shift_enter_patched"
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
@dataclass(frozen=True)
|
|
@@ -102,6 +104,60 @@ def install_shift_enter_key_sequence_overrides() -> None:
|
|
|
102
104
|
prefix_cache = getattr(vt100_parser, "_IS_PREFIX_OF_LONGER_MATCH_CACHE", None)
|
|
103
105
|
if hasattr(prefix_cache, "clear"):
|
|
104
106
|
prefix_cache.clear()
|
|
107
|
+
install_windows_shift_enter_key_sequence_override()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def install_windows_shift_enter_key_sequence_override(
|
|
111
|
+
*,
|
|
112
|
+
platform_name: str | None = None,
|
|
113
|
+
console_input_reader_cls: type | None = None,
|
|
114
|
+
) -> bool:
|
|
115
|
+
resolved_platform = platform_name or sys.platform
|
|
116
|
+
if not resolved_platform.startswith("win"):
|
|
117
|
+
return False
|
|
118
|
+
if console_input_reader_cls is None:
|
|
119
|
+
try:
|
|
120
|
+
from prompt_toolkit.input import win32
|
|
121
|
+
except (AssertionError, ImportError):
|
|
122
|
+
return False
|
|
123
|
+
console_input_reader_cls = win32.ConsoleInputReader
|
|
124
|
+
if getattr(console_input_reader_cls, _WINDOWS_SHIFT_ENTER_PATCH_ATTR, False):
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
from prompt_toolkit.key_binding.key_processor import KeyPress
|
|
128
|
+
|
|
129
|
+
original_handler = console_input_reader_cls._event_to_key_presses
|
|
130
|
+
shift_pressed = getattr(console_input_reader_cls, "SHIFT_PRESSED", 0x0010)
|
|
131
|
+
|
|
132
|
+
def patched_event_to_key_presses(self, ev):
|
|
133
|
+
key_presses = original_handler(self, ev)
|
|
134
|
+
if _is_windows_shift_enter_key_press(ev, key_presses, shift_pressed=shift_pressed):
|
|
135
|
+
return [KeyPress(Keys.Escape, ""), key_presses[0]]
|
|
136
|
+
return key_presses
|
|
137
|
+
|
|
138
|
+
setattr(
|
|
139
|
+
console_input_reader_cls,
|
|
140
|
+
"_deepy_original_event_to_key_presses",
|
|
141
|
+
original_handler,
|
|
142
|
+
)
|
|
143
|
+
console_input_reader_cls._event_to_key_presses = patched_event_to_key_presses
|
|
144
|
+
setattr(console_input_reader_cls, _WINDOWS_SHIFT_ENTER_PATCH_ATTR, True)
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _is_windows_shift_enter_key_press(
|
|
149
|
+
ev,
|
|
150
|
+
key_presses: list,
|
|
151
|
+
*,
|
|
152
|
+
shift_pressed: int,
|
|
153
|
+
) -> bool:
|
|
154
|
+
if not key_presses or len(key_presses) != 1:
|
|
155
|
+
return False
|
|
156
|
+
control_key_state = getattr(ev, "ControlKeyState", 0)
|
|
157
|
+
if not control_key_state & shift_pressed:
|
|
158
|
+
return False
|
|
159
|
+
key = getattr(key_presses[0], "key", None)
|
|
160
|
+
return key in {Keys.ControlM, Keys.ControlJ, Keys.Enter}
|
|
105
161
|
|
|
106
162
|
|
|
107
163
|
def prompt_for_input(
|
|
@@ -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
|