klaude-code 2.8.1__py3-none-any.whl → 2.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/app/runtime.py +2 -1
- klaude_code/auth/antigravity/oauth.py +33 -38
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- klaude_code/auth/claude/oauth.py +34 -49
- klaude_code/auth/codex/exceptions.py +0 -4
- klaude_code/auth/codex/oauth.py +32 -28
- klaude_code/auth/codex/token_manager.py +0 -18
- klaude_code/cli/cost_cmd.py +128 -39
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +14 -3
- klaude_code/config/assets/builtin_config.yaml +25 -24
- klaude_code/config/config.py +47 -25
- klaude_code/config/sub_agent_model_helper.py +18 -13
- klaude_code/config/thinking.py +0 -8
- klaude_code/const.py +1 -1
- klaude_code/core/agent_profile.py +11 -56
- klaude_code/core/compaction/overflow.py +0 -4
- klaude_code/core/executor.py +33 -5
- klaude_code/core/manager/llm_clients.py +9 -1
- klaude_code/core/prompts/prompt-claude-code.md +4 -4
- klaude_code/core/reminders.py +21 -23
- klaude_code/core/task.py +1 -5
- klaude_code/core/tool/__init__.py +3 -2
- klaude_code/core/tool/file/apply_patch.py +0 -27
- klaude_code/core/tool/file/read_tool.md +3 -2
- klaude_code/core/tool/file/read_tool.py +27 -3
- klaude_code/core/tool/offload.py +0 -35
- klaude_code/core/tool/shell/bash_tool.py +1 -1
- klaude_code/core/tool/sub_agent/__init__.py +6 -0
- klaude_code/core/tool/sub_agent/image_gen.md +16 -0
- klaude_code/core/tool/sub_agent/image_gen.py +146 -0
- klaude_code/core/tool/sub_agent/task.md +20 -0
- klaude_code/core/tool/sub_agent/task.py +205 -0
- klaude_code/core/tool/tool_registry.py +0 -16
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/input.py +6 -5
- klaude_code/llm/antigravity/input.py +14 -7
- klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
- klaude_code/llm/google/client.py +8 -6
- klaude_code/llm/google/input.py +20 -12
- klaude_code/llm/image.py +18 -11
- klaude_code/llm/input_common.py +32 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
- klaude_code/llm/{codex → openai_codex}/client.py +24 -2
- klaude_code/llm/openai_codex/prompt_sync.py +237 -0
- klaude_code/llm/openai_compatible/client.py +3 -1
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +35 -10
- klaude_code/llm/{responses → openai_responses}/client.py +1 -1
- klaude_code/llm/{responses → openai_responses}/input.py +15 -5
- klaude_code/llm/registry.py +3 -8
- klaude_code/llm/stream_parts.py +3 -1
- klaude_code/llm/usage.py +1 -9
- klaude_code/protocol/events.py +2 -2
- klaude_code/protocol/message.py +3 -2
- klaude_code/protocol/model.py +34 -2
- klaude_code/protocol/op.py +13 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/protocol/sub_agent/AGENTS.md +5 -5
- klaude_code/protocol/sub_agent/__init__.py +13 -34
- klaude_code/protocol/sub_agent/explore.py +7 -34
- klaude_code/protocol/sub_agent/image_gen.py +3 -74
- klaude_code/protocol/sub_agent/task.py +3 -47
- klaude_code/protocol/sub_agent/web.py +8 -52
- klaude_code/protocol/tools.py +2 -0
- klaude_code/session/session.py +80 -22
- klaude_code/session/store.py +0 -4
- klaude_code/skill/assets/deslop/SKILL.md +9 -0
- klaude_code/skill/system_skills.py +0 -20
- klaude_code/tui/command/fork_session_cmd.py +5 -2
- klaude_code/tui/command/resume_cmd.py +9 -2
- klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
- klaude_code/tui/components/assistant.py +0 -26
- klaude_code/tui/components/bash_syntax.py +4 -0
- klaude_code/tui/components/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +4 -209
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/metadata.py +0 -3
- klaude_code/tui/components/rich/markdown.py +120 -87
- klaude_code/tui/components/rich/status.py +2 -2
- klaude_code/tui/components/rich/theme.py +11 -6
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +65 -21
- klaude_code/tui/components/user_input.py +2 -0
- klaude_code/tui/input/images.py +21 -18
- klaude_code/tui/input/key_bindings.py +2 -2
- klaude_code/tui/input/prompt_toolkit.py +49 -49
- klaude_code/tui/machine.py +29 -47
- klaude_code/tui/renderer.py +48 -33
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +27 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/METADATA +3 -6
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/RECORD +103 -99
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/bedrock/__init__.py +0 -3
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/WHEEL +0 -0
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,185 +1,12 @@
|
|
|
1
|
-
from rich import
|
|
2
|
-
from rich.console import Group, RenderableType
|
|
3
|
-
from rich.padding import Padding
|
|
4
|
-
from rich.panel import Panel
|
|
1
|
+
from rich.console import RenderableType
|
|
5
2
|
from rich.text import Text
|
|
6
3
|
|
|
7
|
-
from klaude_code.const import DIFF_PREFIX_WIDTH,
|
|
4
|
+
from klaude_code.const import DIFF_PREFIX_WIDTH, TAB_EXPAND_WIDTH
|
|
8
5
|
from klaude_code.protocol import model
|
|
9
6
|
from klaude_code.tui.components.common import create_grid
|
|
10
7
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
11
8
|
|
|
12
9
|
|
|
13
|
-
def _make_diff_prefix(line: str, new_ln: int | None, width: int) -> tuple[str, int | None]:
|
|
14
|
-
kind = line[0]
|
|
15
|
-
|
|
16
|
-
number = " " * width
|
|
17
|
-
if kind in {"+", " "} and new_ln is not None:
|
|
18
|
-
number = f"{new_ln:>{width}}"
|
|
19
|
-
new_ln += 1
|
|
20
|
-
|
|
21
|
-
if kind == "-":
|
|
22
|
-
marker = "-"
|
|
23
|
-
elif kind == "+":
|
|
24
|
-
marker = "+"
|
|
25
|
-
else:
|
|
26
|
-
marker = " "
|
|
27
|
-
|
|
28
|
-
prefix = f"{number} {marker}"
|
|
29
|
-
return prefix, new_ln
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
|
|
33
|
-
if diff_text == "":
|
|
34
|
-
return Text("")
|
|
35
|
-
|
|
36
|
-
lines = diff_text.split("\n")
|
|
37
|
-
grid = create_grid()
|
|
38
|
-
grid.padding = (0, 0)
|
|
39
|
-
|
|
40
|
-
# Track line numbers based on hunk headers
|
|
41
|
-
new_ln: int | None = None
|
|
42
|
-
# Track if we're in untracked files section
|
|
43
|
-
in_untracked_section = False
|
|
44
|
-
# Track whether we've already rendered a file header
|
|
45
|
-
has_rendered_file_header = False
|
|
46
|
-
# Track whether we have rendered actual diff content for the current file
|
|
47
|
-
has_rendered_diff_content = False
|
|
48
|
-
# Track the "from" file name from --- line (used for deleted files)
|
|
49
|
-
from_file_name: str | None = None
|
|
50
|
-
|
|
51
|
-
for i, line in enumerate(lines):
|
|
52
|
-
# Check for untracked files section header
|
|
53
|
-
if line == "git ls-files --others --exclude-standard":
|
|
54
|
-
in_untracked_section = True
|
|
55
|
-
grid.add_row("", "")
|
|
56
|
-
grid.add_row("", Text("Untracked files:", style=ThemeKey.TOOL_MARK))
|
|
57
|
-
grid.add_row("", "")
|
|
58
|
-
continue
|
|
59
|
-
|
|
60
|
-
# Handle untracked files
|
|
61
|
-
if in_untracked_section:
|
|
62
|
-
# If we hit a new section or empty line, we're done with untracked files
|
|
63
|
-
if line.startswith("diff --git") or line.strip() == "":
|
|
64
|
-
in_untracked_section = False
|
|
65
|
-
elif line.strip(): # Non-empty line in untracked section
|
|
66
|
-
file_text = Text(line.strip(), style=ThemeKey.TOOL_PARAM_BOLD)
|
|
67
|
-
grid.add_row(
|
|
68
|
-
Text(f"{'+':>{DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_PARAM_BOLD),
|
|
69
|
-
file_text,
|
|
70
|
-
)
|
|
71
|
-
continue
|
|
72
|
-
|
|
73
|
-
# Capture "from" file name from --- line (needed for deleted files)
|
|
74
|
-
if line.startswith("--- "):
|
|
75
|
-
raw = line[4:].strip()
|
|
76
|
-
if raw != "/dev/null":
|
|
77
|
-
from_file_name = raw[2:] if raw.startswith(("a/", "b/")) else raw
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
# Parse file name from diff headers
|
|
81
|
-
if show_file_name and line.startswith("+++ "):
|
|
82
|
-
# Extract file name from +++ header with proper handling of /dev/null
|
|
83
|
-
raw = line[4:].strip()
|
|
84
|
-
if raw == "/dev/null":
|
|
85
|
-
# File was deleted, use the "from" file name
|
|
86
|
-
file_name = from_file_name or raw
|
|
87
|
-
elif raw.startswith(("a/", "b/")):
|
|
88
|
-
file_name = raw[2:]
|
|
89
|
-
else:
|
|
90
|
-
file_name = raw
|
|
91
|
-
|
|
92
|
-
file_text = Text(file_name, style=ThemeKey.DIFF_FILE_NAME)
|
|
93
|
-
|
|
94
|
-
# Count actual +/- lines for this file from i+1 onwards
|
|
95
|
-
file_additions = 0
|
|
96
|
-
file_deletions = 0
|
|
97
|
-
for remaining_line in lines[i + 1 :]:
|
|
98
|
-
if remaining_line.startswith("diff --git"):
|
|
99
|
-
break
|
|
100
|
-
elif remaining_line.startswith("+") and not remaining_line.startswith("+++"):
|
|
101
|
-
file_additions += 1
|
|
102
|
-
elif remaining_line.startswith("-") and not remaining_line.startswith("---"):
|
|
103
|
-
file_deletions += 1
|
|
104
|
-
|
|
105
|
-
# Create stats text
|
|
106
|
-
stats_text = Text()
|
|
107
|
-
if file_additions > 0:
|
|
108
|
-
stats_text.append(f"+{file_additions}", style=ThemeKey.DIFF_STATS_ADD)
|
|
109
|
-
if file_deletions > 0:
|
|
110
|
-
if file_additions > 0:
|
|
111
|
-
stats_text.append(" ")
|
|
112
|
-
stats_text.append(f"-{file_deletions}", style=ThemeKey.DIFF_STATS_REMOVE)
|
|
113
|
-
|
|
114
|
-
# Combine file name and stats
|
|
115
|
-
file_line = Text(style=ThemeKey.DIFF_FILE_NAME)
|
|
116
|
-
file_line.append_text(file_text)
|
|
117
|
-
if stats_text.plain:
|
|
118
|
-
file_line.append(" (")
|
|
119
|
-
file_line.append_text(stats_text)
|
|
120
|
-
file_line.append(")")
|
|
121
|
-
|
|
122
|
-
if has_rendered_file_header:
|
|
123
|
-
grid.add_row("", "")
|
|
124
|
-
|
|
125
|
-
if file_additions > 0 and file_deletions == 0:
|
|
126
|
-
file_mark = "+"
|
|
127
|
-
elif file_deletions > 0 and file_additions == 0:
|
|
128
|
-
file_mark = "-"
|
|
129
|
-
else:
|
|
130
|
-
file_mark = "±"
|
|
131
|
-
|
|
132
|
-
grid.add_row(
|
|
133
|
-
Text(f"{file_mark:>{DIFF_PREFIX_WIDTH}} ", style=ThemeKey.DIFF_FILE_NAME),
|
|
134
|
-
file_line,
|
|
135
|
-
)
|
|
136
|
-
has_rendered_file_header = True
|
|
137
|
-
has_rendered_diff_content = False
|
|
138
|
-
continue
|
|
139
|
-
|
|
140
|
-
if line.startswith("diff --git"):
|
|
141
|
-
has_rendered_diff_content = False
|
|
142
|
-
continue
|
|
143
|
-
|
|
144
|
-
# Parse hunk headers to reset counters: @@ -l,s +l,s @@
|
|
145
|
-
if line.startswith("@@"):
|
|
146
|
-
try:
|
|
147
|
-
parts = line.split()
|
|
148
|
-
plus = parts[2] # like '+12,4'
|
|
149
|
-
new_start = int(plus[1:].split(",")[0])
|
|
150
|
-
new_ln = new_start
|
|
151
|
-
except (IndexError, ValueError):
|
|
152
|
-
new_ln = None
|
|
153
|
-
if has_rendered_diff_content:
|
|
154
|
-
grid.add_row(Text(f"{'⋮':>{DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
|
|
155
|
-
continue
|
|
156
|
-
|
|
157
|
-
# Skip +++ lines (already handled above)
|
|
158
|
-
if line.startswith("+++ "):
|
|
159
|
-
continue
|
|
160
|
-
|
|
161
|
-
# Only handle unified diff hunk lines; ignore other metadata like
|
|
162
|
-
# "diff --git" or "index …" which would otherwise skew counters.
|
|
163
|
-
if not line or line[:1] not in {" ", "+", "-"}:
|
|
164
|
-
continue
|
|
165
|
-
|
|
166
|
-
# Compute line number prefix and style diff content
|
|
167
|
-
prefix, new_ln = _make_diff_prefix(line, new_ln, DIFF_PREFIX_WIDTH)
|
|
168
|
-
|
|
169
|
-
if line.startswith("-"):
|
|
170
|
-
text = Text(line[1:])
|
|
171
|
-
text.stylize(ThemeKey.DIFF_REMOVE)
|
|
172
|
-
elif line.startswith("+"):
|
|
173
|
-
text = Text(line[1:])
|
|
174
|
-
text.stylize(ThemeKey.DIFF_ADD)
|
|
175
|
-
else:
|
|
176
|
-
text = Text(line, style=ThemeKey.TOOL_RESULT)
|
|
177
|
-
grid.add_row(Text(prefix, ThemeKey.TOOL_RESULT), text)
|
|
178
|
-
has_rendered_diff_content = True
|
|
179
|
-
|
|
180
|
-
return grid
|
|
181
|
-
|
|
182
|
-
|
|
183
10
|
def render_structured_diff(ui_extra: model.DiffUIExtra, show_file_name: bool = False) -> RenderableType:
|
|
184
11
|
files = ui_extra.files
|
|
185
12
|
if not files:
|
|
@@ -204,39 +31,6 @@ def render_structured_diff(ui_extra: model.DiffUIExtra, show_file_name: bool = F
|
|
|
204
31
|
return grid
|
|
205
32
|
|
|
206
33
|
|
|
207
|
-
def render_diff_panel(
|
|
208
|
-
diff_text: str,
|
|
209
|
-
*,
|
|
210
|
-
show_file_name: bool = True,
|
|
211
|
-
heading: str = "DIFF",
|
|
212
|
-
indent: int = 2,
|
|
213
|
-
) -> RenderableType:
|
|
214
|
-
lines = diff_text.splitlines()
|
|
215
|
-
truncated_notice: Text | None = None
|
|
216
|
-
if len(lines) > MAX_DIFF_LINES:
|
|
217
|
-
truncated_lines = len(lines) - MAX_DIFF_LINES
|
|
218
|
-
diff_text = "\n".join(lines[:MAX_DIFF_LINES])
|
|
219
|
-
truncated_notice = Text(f"… truncated {truncated_lines} lines", style=ThemeKey.TOOL_MARK)
|
|
220
|
-
|
|
221
|
-
diff_body = render_diff(diff_text, show_file_name=show_file_name)
|
|
222
|
-
renderables: list[RenderableType] = [
|
|
223
|
-
Text(f" {heading} ", style="bold reverse"),
|
|
224
|
-
diff_body,
|
|
225
|
-
]
|
|
226
|
-
if truncated_notice is not None:
|
|
227
|
-
renderables.extend([Text(""), truncated_notice])
|
|
228
|
-
|
|
229
|
-
panel = Panel.fit(
|
|
230
|
-
Group(*renderables),
|
|
231
|
-
border_style=ThemeKey.LINES,
|
|
232
|
-
title_align="center",
|
|
233
|
-
box=box.ROUNDED,
|
|
234
|
-
)
|
|
235
|
-
if indent <= 0:
|
|
236
|
-
return panel
|
|
237
|
-
return Padding.indent(panel, level=indent)
|
|
238
|
-
|
|
239
|
-
|
|
240
34
|
def _render_file_header(file_diff: model.DiffFileDiff) -> tuple[Text, Text]:
|
|
241
35
|
file_text = Text(file_diff.file_path, style=ThemeKey.DIFF_FILE_NAME)
|
|
242
36
|
stats_text = Text()
|
|
@@ -280,7 +74,8 @@ def _render_structured_line(line: model.DiffLine) -> Text:
|
|
|
280
74
|
return Text("")
|
|
281
75
|
text = Text()
|
|
282
76
|
for span in line.spans:
|
|
283
|
-
|
|
77
|
+
content = span.text.expandtabs(TAB_EXPAND_WIDTH)
|
|
78
|
+
text.append(content, style=_span_style(line.kind, span.op))
|
|
284
79
|
return text
|
|
285
80
|
|
|
286
81
|
|
|
@@ -9,6 +9,8 @@ def render_error(error_msg: Text) -> RenderableType:
|
|
|
9
9
|
"""Render error with X mark for error events."""
|
|
10
10
|
grid = create_grid()
|
|
11
11
|
error_msg.style = ThemeKey.ERROR
|
|
12
|
+
error_msg.overflow = "ellipsis"
|
|
13
|
+
error_msg.no_wrap = True
|
|
12
14
|
grid.add_row(Text("✘", style=ThemeKey.ERROR_BOLD), error_msg)
|
|
13
15
|
return grid
|
|
14
16
|
|
|
@@ -17,5 +19,7 @@ def render_tool_error(error_msg: Text) -> RenderableType:
|
|
|
17
19
|
"""Render error with indent for tool results."""
|
|
18
20
|
grid = create_grid()
|
|
19
21
|
error_msg.style = ThemeKey.ERROR
|
|
22
|
+
error_msg.overflow = "ellipsis"
|
|
23
|
+
error_msg.no_wrap = True
|
|
20
24
|
grid.add_row(Text(" "), error_msg)
|
|
21
25
|
return grid
|
|
@@ -16,7 +16,7 @@ _MERMAID_DEFAULT_PNG_SCALE = 2
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def artifacts_dir() -> Path:
|
|
19
|
-
return Path(TOOL_OUTPUT_TRUNCATION_DIR)
|
|
19
|
+
return Path(TOOL_OUTPUT_TRUNCATION_DIR)
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def _extract_pako_from_link(link: str) -> str | None:
|
|
@@ -72,7 +72,7 @@ def ensure_viewer_file(*, code: str, link: str, tool_call_id: str) -> Path | Non
|
|
|
72
72
|
return None
|
|
73
73
|
|
|
74
74
|
safe_id = tool_call_id.replace("/", "_")
|
|
75
|
-
path = artifacts_dir() / f"mermaid-
|
|
75
|
+
path = artifacts_dir() / f"klaude-mermaid-{safe_id}.html"
|
|
76
76
|
if path.exists():
|
|
77
77
|
return path
|
|
78
78
|
|
|
@@ -136,9 +136,6 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
|
136
136
|
"""Render task metadata including main agent and sub-agents."""
|
|
137
137
|
renderables: list[RenderableType] = []
|
|
138
138
|
|
|
139
|
-
if e.cancelled:
|
|
140
|
-
renderables.append(Text())
|
|
141
|
-
|
|
142
139
|
has_sub_agents = len(e.metadata.sub_agent_task_metadata) > 0
|
|
143
140
|
# Use an extra space for the main agent mark to align with two-character marks (├─, └─)
|
|
144
141
|
main_mark_text = "✓"
|
|
@@ -14,7 +14,6 @@ from rich import box
|
|
|
14
14
|
from rich._loop import loop_first
|
|
15
15
|
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
16
16
|
from rich.markdown import CodeBlock, Heading, ImageItem, ListItem, Markdown, MarkdownElement, TableElement
|
|
17
|
-
from rich.rule import Rule
|
|
18
17
|
from rich.segment import Segment
|
|
19
18
|
from rich.style import Style, StyleType
|
|
20
19
|
from rich.syntax import Syntax
|
|
@@ -120,7 +119,8 @@ class Divider(MarkdownElement):
|
|
|
120
119
|
|
|
121
120
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
122
121
|
style = console.get_style("markdown.hr", default="none")
|
|
123
|
-
|
|
122
|
+
width = min(options.max_width, 100)
|
|
123
|
+
yield Text("-" * width, style=style)
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
class MarkdownTable(TableElement):
|
|
@@ -153,7 +153,8 @@ class LeftHeading(Heading):
|
|
|
153
153
|
h1_text = text.assemble((" ", "markdown.h1"), text, (" ", "markdown.h1"))
|
|
154
154
|
yield h1_text
|
|
155
155
|
elif self.tag == "h2":
|
|
156
|
-
|
|
156
|
+
h2_style = console.get_style("markdown.h2", default="bold")
|
|
157
|
+
text.stylize(h2_style + Style(underline=False))
|
|
157
158
|
yield text
|
|
158
159
|
else:
|
|
159
160
|
yield text
|
|
@@ -332,11 +333,6 @@ class MarkdownStream:
|
|
|
332
333
|
self.right_margin: int = max(right_margin, 0)
|
|
333
334
|
self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
|
|
334
335
|
|
|
335
|
-
@property
|
|
336
|
-
def _live_started(self) -> bool:
|
|
337
|
-
"""Check if Live display has been started (derived from self.live)."""
|
|
338
|
-
return self._live_sink is not None
|
|
339
|
-
|
|
340
336
|
def _get_base_width(self) -> int:
|
|
341
337
|
return self.console.options.max_width
|
|
342
338
|
|
|
@@ -399,12 +395,25 @@ class MarkdownStream:
|
|
|
399
395
|
return 0
|
|
400
396
|
|
|
401
397
|
top_level: list[Token] = [token for token in tokens if token.level == 0 and token.map is not None]
|
|
402
|
-
if
|
|
398
|
+
if not top_level:
|
|
403
399
|
return 0
|
|
404
400
|
|
|
405
401
|
last = top_level[-1]
|
|
406
402
|
assert last.map is not None
|
|
407
403
|
|
|
404
|
+
# Lists are a special case: markdown-it-py treats the whole list as one
|
|
405
|
+
# top-level block, which would keep the entire list in the live area
|
|
406
|
+
# until the list ends.
|
|
407
|
+
#
|
|
408
|
+
# For streaming UX, we want all but the final list item to be eligible
|
|
409
|
+
# for stabilization, leaving only the *last* item in the live area.
|
|
410
|
+
if last.type in {"bullet_list_open", "ordered_list_open"}:
|
|
411
|
+
stable_line = self._compute_list_item_boundary(tokens, last)
|
|
412
|
+
return max(stable_line, 0)
|
|
413
|
+
|
|
414
|
+
if len(top_level) < 2:
|
|
415
|
+
return 0
|
|
416
|
+
|
|
408
417
|
# When the buffer ends mid-line, markdown-it-py can temporarily classify
|
|
409
418
|
# some lines as a thematic break (hr). For example, a trailing "- --"
|
|
410
419
|
# parses as an hr, but appending a non-hr character ("- --0") turns it
|
|
@@ -421,6 +430,59 @@ class MarkdownStream:
|
|
|
421
430
|
start_line = last.map[0]
|
|
422
431
|
return max(start_line, 0)
|
|
423
432
|
|
|
433
|
+
def _compute_list_item_boundary(self, tokens: list[Token], list_open: Token) -> int:
|
|
434
|
+
"""Return the start line of the last list item in a list.
|
|
435
|
+
|
|
436
|
+
This allows stabilizing all list items except the final one.
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
if list_open.map is None:
|
|
440
|
+
return 0
|
|
441
|
+
|
|
442
|
+
list_start, list_end = list_open.map
|
|
443
|
+
item_level = list_open.level + 1
|
|
444
|
+
|
|
445
|
+
last_item_start: int | None = None
|
|
446
|
+
for token in tokens:
|
|
447
|
+
if token.type != "list_item_open":
|
|
448
|
+
continue
|
|
449
|
+
if token.level != item_level:
|
|
450
|
+
continue
|
|
451
|
+
if token.map is None:
|
|
452
|
+
continue
|
|
453
|
+
start_line = token.map[0]
|
|
454
|
+
if start_line < list_start or start_line >= list_end:
|
|
455
|
+
continue
|
|
456
|
+
last_item_start = start_line
|
|
457
|
+
|
|
458
|
+
return last_item_start if last_item_start is not None else list_start
|
|
459
|
+
|
|
460
|
+
def _stable_boundary_continues_list(self, text: str, stable_line: int, *, final: bool) -> bool:
|
|
461
|
+
"""Whether the stable/live split point is inside the final top-level list."""
|
|
462
|
+
|
|
463
|
+
if final:
|
|
464
|
+
return False
|
|
465
|
+
if stable_line <= 0:
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
tokens = self._parser.parse(text)
|
|
470
|
+
except Exception:
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
top_level: list[Token] = [token for token in tokens if token.level == 0 and token.map is not None]
|
|
474
|
+
if not top_level:
|
|
475
|
+
return False
|
|
476
|
+
|
|
477
|
+
last = top_level[-1]
|
|
478
|
+
if last.type not in {"bullet_list_open", "ordered_list_open"}:
|
|
479
|
+
return False
|
|
480
|
+
if last.map is None:
|
|
481
|
+
return False
|
|
482
|
+
|
|
483
|
+
list_start, list_end = last.map
|
|
484
|
+
return list_start < stable_line < list_end
|
|
485
|
+
|
|
424
486
|
def split_blocks(self, text: str, *, min_stable_line: int = 0, final: bool = False) -> tuple[str, str, int]:
|
|
425
487
|
"""Split full markdown into stable and live sources.
|
|
426
488
|
|
|
@@ -450,15 +512,14 @@ class MarkdownStream:
|
|
|
450
512
|
return "", text, 0
|
|
451
513
|
return stable_source, live_source, stable_line
|
|
452
514
|
|
|
453
|
-
def
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
def render_stable_ansi(self, stable_source: str, *, has_live_suffix: bool, final: bool) -> tuple[str, list[str]]:
|
|
515
|
+
def render_stable_ansi(
|
|
516
|
+
self,
|
|
517
|
+
stable_source: str,
|
|
518
|
+
*,
|
|
519
|
+
has_live_suffix: bool,
|
|
520
|
+
final: bool,
|
|
521
|
+
continues_list: bool = False,
|
|
522
|
+
) -> tuple[str, list[str]]:
|
|
462
523
|
"""Render stable prefix to ANSI, preserving inter-block spacing.
|
|
463
524
|
|
|
464
525
|
Returns:
|
|
@@ -468,52 +529,15 @@ class MarkdownStream:
|
|
|
468
529
|
return "", []
|
|
469
530
|
|
|
470
531
|
render_source = stable_source
|
|
471
|
-
if not final and has_live_suffix:
|
|
532
|
+
if not final and has_live_suffix and not continues_list:
|
|
472
533
|
render_source = self._append_nonfinal_sentinel(stable_source)
|
|
473
534
|
|
|
474
535
|
lines, images = self._render_markdown_to_lines(render_source, apply_mark=True)
|
|
475
|
-
return "".join(lines), images
|
|
476
536
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
Some Rich Markdown blocks (e.g. lists) render with a leading blank line.
|
|
482
|
-
If the stable prefix already renders a trailing blank line, rendering the
|
|
483
|
-
live suffix separately may introduce an extra blank line that wouldn't
|
|
484
|
-
appear when rendering the full document.
|
|
485
|
-
|
|
486
|
-
This function removes *overlapping* blank lines from the live ANSI when
|
|
487
|
-
the stable ANSI already ends with one or more blank lines.
|
|
488
|
-
|
|
489
|
-
Important: don't remove *all* leading blank lines from the live suffix.
|
|
490
|
-
In some incomplete-block cases, the live render may begin with multiple
|
|
491
|
-
blank lines while the full-document render would keep one of them.
|
|
492
|
-
"""
|
|
493
|
-
|
|
494
|
-
stable_lines = stable_ansi.splitlines(keepends=True)
|
|
495
|
-
if not stable_lines:
|
|
496
|
-
return live_ansi
|
|
497
|
-
|
|
498
|
-
stable_trailing_blank = 0
|
|
499
|
-
for line in reversed(stable_lines):
|
|
500
|
-
if line.strip():
|
|
501
|
-
break
|
|
502
|
-
stable_trailing_blank += 1
|
|
503
|
-
if stable_trailing_blank <= 0:
|
|
504
|
-
return live_ansi
|
|
505
|
-
|
|
506
|
-
live_lines = live_ansi.splitlines(keepends=True)
|
|
507
|
-
live_leading_blank = 0
|
|
508
|
-
for line in live_lines:
|
|
509
|
-
if line.strip():
|
|
510
|
-
break
|
|
511
|
-
live_leading_blank += 1
|
|
512
|
-
|
|
513
|
-
drop = min(stable_trailing_blank, live_leading_blank)
|
|
514
|
-
if drop > 0:
|
|
515
|
-
live_lines = live_lines[drop:]
|
|
516
|
-
return "".join(live_lines)
|
|
537
|
+
if continues_list:
|
|
538
|
+
while lines and not lines[-1].strip():
|
|
539
|
+
lines.pop()
|
|
540
|
+
return "".join(lines), images
|
|
517
541
|
|
|
518
542
|
def _append_nonfinal_sentinel(self, stable_source: str) -> str:
|
|
519
543
|
"""Make Rich render stable content as if it isn't the last block.
|
|
@@ -624,6 +648,8 @@ class MarkdownStream:
|
|
|
624
648
|
final=final,
|
|
625
649
|
)
|
|
626
650
|
|
|
651
|
+
continues_list = self._stable_boundary_continues_list(text, stable_line, final=final) and bool(live_source)
|
|
652
|
+
|
|
627
653
|
start = time.time()
|
|
628
654
|
|
|
629
655
|
stable_chunk_to_print: str | None = None
|
|
@@ -631,7 +657,10 @@ class MarkdownStream:
|
|
|
631
657
|
stable_changed = final or stable_line > self._stable_source_line_count
|
|
632
658
|
if stable_changed and stable_source:
|
|
633
659
|
stable_ansi, collected_images = self.render_stable_ansi(
|
|
634
|
-
stable_source,
|
|
660
|
+
stable_source,
|
|
661
|
+
has_live_suffix=bool(live_source),
|
|
662
|
+
final=final,
|
|
663
|
+
continues_list=continues_list,
|
|
635
664
|
)
|
|
636
665
|
stable_lines = stable_ansi.splitlines(keepends=True)
|
|
637
666
|
new_lines = stable_lines[len(self._stable_rendered_lines) :]
|
|
@@ -649,30 +678,42 @@ class MarkdownStream:
|
|
|
649
678
|
|
|
650
679
|
live_text_to_set: Text | None = None
|
|
651
680
|
if not final and MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._live_sink is not None:
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
live_leading_blank = 0
|
|
664
|
-
for line in live_lines:
|
|
681
|
+
if continues_list and self._stable_rendered_lines:
|
|
682
|
+
full_lines, _ = self._render_markdown_to_lines(text, apply_mark=True)
|
|
683
|
+
skip = min(len(self._stable_rendered_lines), len(full_lines))
|
|
684
|
+
live_text_to_set = Text.from_ansi("".join(full_lines[skip:]))
|
|
685
|
+
else:
|
|
686
|
+
apply_mark = not self._stable_rendered_lines
|
|
687
|
+
live_lines, _ = self._render_markdown_to_lines(live_source, apply_mark=apply_mark)
|
|
688
|
+
|
|
689
|
+
if self._stable_rendered_lines:
|
|
690
|
+
stable_trailing_blank = 0
|
|
691
|
+
for line in reversed(self._stable_rendered_lines):
|
|
665
692
|
if line.strip():
|
|
666
693
|
break
|
|
667
|
-
|
|
694
|
+
stable_trailing_blank += 1
|
|
668
695
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
696
|
+
if stable_trailing_blank > 0:
|
|
697
|
+
live_leading_blank = 0
|
|
698
|
+
for line in live_lines:
|
|
699
|
+
if line.strip():
|
|
700
|
+
break
|
|
701
|
+
live_leading_blank += 1
|
|
672
702
|
|
|
673
|
-
|
|
703
|
+
drop = min(stable_trailing_blank, live_leading_blank)
|
|
704
|
+
if drop > 0:
|
|
705
|
+
live_lines = live_lines[drop:]
|
|
706
|
+
|
|
707
|
+
live_text_to_set = Text.from_ansi("".join(live_lines))
|
|
674
708
|
|
|
675
709
|
with self._synchronized_output():
|
|
710
|
+
# Update/clear live area first to avoid blank padding when stable block appears
|
|
711
|
+
if final:
|
|
712
|
+
if self._live_sink is not None:
|
|
713
|
+
self._live_sink(None)
|
|
714
|
+
elif live_text_to_set is not None and self._live_sink is not None:
|
|
715
|
+
self._live_sink(live_text_to_set)
|
|
716
|
+
|
|
676
717
|
if stable_chunk_to_print:
|
|
677
718
|
self.console.print(Text.from_ansi(stable_chunk_to_print), end="\n")
|
|
678
719
|
|
|
@@ -680,13 +721,5 @@ class MarkdownStream:
|
|
|
680
721
|
for img_path in new_images:
|
|
681
722
|
self._image_callback(img_path)
|
|
682
723
|
|
|
683
|
-
if final:
|
|
684
|
-
if self._live_sink is not None:
|
|
685
|
-
self._live_sink(None)
|
|
686
|
-
return
|
|
687
|
-
|
|
688
|
-
if live_text_to_set is not None and self._live_sink is not None:
|
|
689
|
-
self._live_sink(live_text_to_set)
|
|
690
|
-
|
|
691
724
|
elapsed = time.time() - start
|
|
692
725
|
self.min_delay = min(max(elapsed * 6, 1.0 / 30), 0.5)
|
|
@@ -277,7 +277,7 @@ def truncate_left(text: Text, max_cells: int, *, console: Console, ellipsis: str
|
|
|
277
277
|
if cell_len(text.plain) <= max_cells:
|
|
278
278
|
return text
|
|
279
279
|
|
|
280
|
-
ellipsis_cells = cell_len(ellipsis)
|
|
280
|
+
ellipsis_cells = cell_len(ellipsis) + 1 # +1 for trailing space
|
|
281
281
|
if max_cells <= ellipsis_cells:
|
|
282
282
|
# Not enough space to show any meaningful suffix.
|
|
283
283
|
clipped = Text(ellipsis, style=text.style)
|
|
@@ -307,7 +307,7 @@ def truncate_left(text: Text, max_cells: int, *, console: Console, ellipsis: str
|
|
|
307
307
|
except Exception:
|
|
308
308
|
ellipsis_style = suffix.style or text.style
|
|
309
309
|
|
|
310
|
-
return Text.assemble(Text(ellipsis, style=ellipsis_style), suffix)
|
|
310
|
+
return Text.assemble(Text(ellipsis + " ", style=ellipsis_style), suffix)
|
|
311
311
|
|
|
312
312
|
|
|
313
313
|
class ShimmerStatusText:
|
|
@@ -21,6 +21,7 @@ class Palette:
|
|
|
21
21
|
grey_green: str
|
|
22
22
|
purple: str
|
|
23
23
|
lavender: str
|
|
24
|
+
black: str
|
|
24
25
|
diff_add: str
|
|
25
26
|
diff_add_char: str
|
|
26
27
|
diff_remove: str
|
|
@@ -55,6 +56,7 @@ LIGHT_PALETTE = Palette(
|
|
|
55
56
|
grey_green="#96a096",
|
|
56
57
|
purple="#5f5fb7",
|
|
57
58
|
lavender="#7878b0",
|
|
59
|
+
black="#101827",
|
|
58
60
|
diff_add="#2e5a32 on #dafbe1",
|
|
59
61
|
diff_add_char="#2e5a32 on #aceebb",
|
|
60
62
|
diff_remove="#82071e on #ffecec",
|
|
@@ -88,6 +90,7 @@ DARK_PALETTE = Palette(
|
|
|
88
90
|
grey_green="#6d8672",
|
|
89
91
|
purple="#afbafe",
|
|
90
92
|
lavender="#9898b8",
|
|
93
|
+
black="white",
|
|
91
94
|
diff_add="#c8e6c9 on #1b3928",
|
|
92
95
|
diff_add_char="#c8e6c9 on #2d6b42",
|
|
93
96
|
diff_remove="#ffcdd2 on #3d1f23",
|
|
@@ -109,6 +112,7 @@ DARK_PALETTE = Palette(
|
|
|
109
112
|
|
|
110
113
|
class ThemeKey(str, Enum):
|
|
111
114
|
LINES = "lines"
|
|
115
|
+
LINES_DIM = "lines.dim"
|
|
112
116
|
|
|
113
117
|
# CODE
|
|
114
118
|
CODE_BACKGROUND = "code_background"
|
|
@@ -233,6 +237,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
233
237
|
app_theme=Theme(
|
|
234
238
|
styles={
|
|
235
239
|
ThemeKey.LINES.value: palette.grey3,
|
|
240
|
+
ThemeKey.LINES_DIM.value: "dim " + palette.grey3,
|
|
236
241
|
# CODE
|
|
237
242
|
ThemeKey.CODE_BACKGROUND.value: f"on {palette.code_background}",
|
|
238
243
|
# PANEL
|
|
@@ -299,9 +304,9 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
299
304
|
ThemeKey.BASH_HEREDOC_DELIMITER.value: "bold " + palette.grey1,
|
|
300
305
|
# THINKING
|
|
301
306
|
ThemeKey.THINKING.value: "italic " + palette.grey2,
|
|
302
|
-
ThemeKey.THINKING_BOLD.value: "
|
|
307
|
+
ThemeKey.THINKING_BOLD.value: "italic " + palette.grey1,
|
|
303
308
|
# COMPACTION
|
|
304
|
-
ThemeKey.COMPACTION_SUMMARY.value:
|
|
309
|
+
ThemeKey.COMPACTION_SUMMARY.value: palette.grey1,
|
|
305
310
|
# TODO_ITEM
|
|
306
311
|
ThemeKey.TODO_EXPLANATION.value: palette.grey1 + " italic",
|
|
307
312
|
ThemeKey.TODO_PENDING_MARK.value: "bold " + palette.grey1,
|
|
@@ -345,9 +350,9 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
345
350
|
"markdown.code.border": palette.grey3,
|
|
346
351
|
# Used by ThinkingMarkdown when rendering `<thinking>` blocks.
|
|
347
352
|
"markdown.code.block": palette.grey1,
|
|
348
|
-
"markdown.h1": "bold reverse",
|
|
353
|
+
"markdown.h1": "bold reverse " + palette.black,
|
|
349
354
|
"markdown.h1.border": palette.grey3,
|
|
350
|
-
"markdown.h2": "bold underline",
|
|
355
|
+
"markdown.h2": "bold underline " + palette.black,
|
|
351
356
|
"markdown.h3": "bold " + palette.grey1,
|
|
352
357
|
"markdown.h4": "bold " + palette.grey2,
|
|
353
358
|
"markdown.hr": palette.grey3,
|
|
@@ -363,7 +368,8 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
363
368
|
styles={
|
|
364
369
|
# THINKING (used for left-side mark in thinking output)
|
|
365
370
|
ThemeKey.THINKING.value: "italic " + palette.grey2,
|
|
366
|
-
ThemeKey.THINKING_BOLD.value: "
|
|
371
|
+
ThemeKey.THINKING_BOLD.value: "italic " + palette.grey1,
|
|
372
|
+
"markdown.strong": "italic " + palette.grey1,
|
|
367
373
|
"markdown.code": palette.grey1 + " italic on " + palette.code_background,
|
|
368
374
|
"markdown.code.block": palette.grey1,
|
|
369
375
|
"markdown.code.border": palette.grey3,
|
|
@@ -377,7 +383,6 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
377
383
|
"markdown.item.number": palette.grey2,
|
|
378
384
|
"markdown.link": "underline " + palette.blue,
|
|
379
385
|
"markdown.link_url": "underline " + palette.blue,
|
|
380
|
-
"markdown.strong": "bold italic " + palette.grey1,
|
|
381
386
|
"markdown.table.border": palette.grey2,
|
|
382
387
|
"markdown.checkbox.checked": palette.green,
|
|
383
388
|
}
|