klaude-code 2.6.0__py3-none-any.whl → 2.8.0__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 +1 -1
- klaude_code/auth/AGENTS.md +325 -0
- klaude_code/auth/__init__.py +17 -1
- klaude_code/auth/antigravity/__init__.py +20 -0
- klaude_code/auth/antigravity/exceptions.py +17 -0
- klaude_code/auth/antigravity/oauth.py +320 -0
- klaude_code/auth/antigravity/pkce.py +25 -0
- klaude_code/auth/antigravity/token_manager.py +45 -0
- klaude_code/auth/base.py +4 -0
- klaude_code/auth/claude/oauth.py +29 -9
- klaude_code/auth/codex/exceptions.py +4 -0
- klaude_code/auth/env.py +19 -15
- klaude_code/cli/auth_cmd.py +54 -4
- klaude_code/cli/cost_cmd.py +83 -160
- klaude_code/cli/list_model.py +50 -0
- klaude_code/cli/main.py +99 -9
- klaude_code/config/assets/builtin_config.yaml +108 -0
- klaude_code/config/builtin_config.py +5 -11
- klaude_code/config/config.py +24 -10
- klaude_code/const.py +11 -1
- klaude_code/core/agent.py +5 -1
- klaude_code/core/agent_profile.py +28 -32
- klaude_code/core/compaction/AGENTS.md +112 -0
- klaude_code/core/compaction/__init__.py +11 -0
- klaude_code/core/compaction/compaction.py +707 -0
- klaude_code/core/compaction/overflow.py +30 -0
- klaude_code/core/compaction/prompts.py +97 -0
- klaude_code/core/executor.py +103 -2
- klaude_code/core/manager/llm_clients.py +5 -0
- klaude_code/core/manager/llm_clients_builder.py +14 -2
- klaude_code/core/prompts/prompt-antigravity.md +80 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2.md +335 -0
- klaude_code/core/reminders.py +11 -7
- klaude_code/core/task.py +126 -0
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/turn.py +3 -1
- klaude_code/llm/antigravity/__init__.py +3 -0
- klaude_code/llm/antigravity/client.py +558 -0
- klaude_code/llm/antigravity/input.py +261 -0
- klaude_code/llm/registry.py +1 -0
- klaude_code/protocol/commands.py +0 -1
- klaude_code/protocol/events.py +18 -0
- klaude_code/protocol/llm_param.py +1 -0
- klaude_code/protocol/message.py +23 -1
- klaude_code/protocol/op.py +15 -1
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/session/session.py +36 -0
- klaude_code/skill/assets/create-plan/SKILL.md +6 -6
- klaude_code/skill/loader.py +12 -13
- klaude_code/skill/manager.py +3 -3
- klaude_code/tui/command/__init__.py +4 -4
- klaude_code/tui/command/compact_cmd.py +32 -0
- klaude_code/tui/command/copy_cmd.py +1 -1
- klaude_code/tui/command/fork_session_cmd.py +114 -18
- klaude_code/tui/command/model_picker.py +5 -1
- klaude_code/tui/command/thinking_cmd.py +1 -1
- klaude_code/tui/commands.py +6 -0
- klaude_code/tui/components/command_output.py +1 -1
- klaude_code/tui/components/rich/markdown.py +117 -1
- klaude_code/tui/components/rich/theme.py +18 -2
- klaude_code/tui/components/tools.py +39 -25
- klaude_code/tui/components/user_input.py +39 -28
- klaude_code/tui/input/AGENTS.md +44 -0
- klaude_code/tui/input/__init__.py +5 -2
- klaude_code/tui/input/completers.py +10 -14
- klaude_code/tui/input/drag_drop.py +146 -0
- klaude_code/tui/input/images.py +227 -0
- klaude_code/tui/input/key_bindings.py +183 -19
- klaude_code/tui/input/paste.py +71 -0
- klaude_code/tui/input/prompt_toolkit.py +32 -9
- klaude_code/tui/machine.py +26 -1
- klaude_code/tui/renderer.py +67 -4
- klaude_code/tui/runner.py +19 -3
- klaude_code/tui/terminal/image.py +103 -10
- klaude_code/tui/terminal/selector.py +81 -7
- {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/METADATA +10 -10
- {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/RECORD +79 -61
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -117
- klaude_code/tui/command/terminal_setup_cmd.py +0 -248
- klaude_code/tui/input/clipboard.py +0 -152
- {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/entry_points.txt +0 -0
|
@@ -6,7 +6,7 @@ from typing import Literal
|
|
|
6
6
|
from prompt_toolkit.styles import Style, merge_styles
|
|
7
7
|
|
|
8
8
|
from klaude_code.protocol import commands, events, message, model
|
|
9
|
-
from klaude_code.tui.input.
|
|
9
|
+
from klaude_code.tui.input.key_bindings import copy_to_clipboard
|
|
10
10
|
from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, select_one
|
|
11
11
|
|
|
12
12
|
from .command_abc import Agent, CommandABC, CommandResult
|
|
@@ -28,10 +28,14 @@ FORK_SELECT_STYLE = merge_styles(
|
|
|
28
28
|
class ForkPoint:
|
|
29
29
|
"""A fork point in conversation history."""
|
|
30
30
|
|
|
31
|
+
kind: Literal["user", "compaction", "end"]
|
|
31
32
|
history_index: int # -1 means fork entire conversation
|
|
32
|
-
user_message: str
|
|
33
33
|
tool_call_stats: dict[str, int] # tool_name -> count
|
|
34
|
-
|
|
34
|
+
user_message: str = ""
|
|
35
|
+
last_assistant_summary: str = ""
|
|
36
|
+
compaction_summary_preview: str = ""
|
|
37
|
+
compaction_first_kept_index: int | None = None
|
|
38
|
+
compaction_tokens_before: int | None = None
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
def _truncate(text: str, max_len: int = 60) -> str:
|
|
@@ -42,11 +46,61 @@ def _truncate(text: str, max_len: int = 60) -> str:
|
|
|
42
46
|
return text[: max_len - 1] + "…"
|
|
43
47
|
|
|
44
48
|
|
|
49
|
+
def _first_non_empty_line(text: str) -> str:
|
|
50
|
+
for line in text.splitlines():
|
|
51
|
+
stripped = line.strip()
|
|
52
|
+
if stripped:
|
|
53
|
+
return stripped
|
|
54
|
+
return ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _preview_compaction_summary(summary: str) -> str:
|
|
58
|
+
"""Return a human-friendly preview line for a CompactionEntry summary.
|
|
59
|
+
|
|
60
|
+
Compaction summaries may start with a fixed prefix line and may contain <summary> tags.
|
|
61
|
+
For UI previews we want something more informative than the prefix.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
cleaned = summary.replace("<summary>", "\n").replace("</summary>", "\n")
|
|
65
|
+
lines = [line.strip() for line in cleaned.splitlines()]
|
|
66
|
+
prefix = "the conversation history before this point was compacted"
|
|
67
|
+
|
|
68
|
+
def _is_noise(line: str) -> bool:
|
|
69
|
+
if not line:
|
|
70
|
+
return True
|
|
71
|
+
if line.casefold().startswith(prefix):
|
|
72
|
+
return True
|
|
73
|
+
return line in {"---", "----", "-----"}
|
|
74
|
+
|
|
75
|
+
# Prefer the first non-empty line under the "## Goal" section.
|
|
76
|
+
for i, line in enumerate(lines):
|
|
77
|
+
if line == "## Goal":
|
|
78
|
+
for j in range(i + 1, len(lines)):
|
|
79
|
+
candidate = lines[j]
|
|
80
|
+
if _is_noise(candidate):
|
|
81
|
+
continue
|
|
82
|
+
if candidate.startswith("## "):
|
|
83
|
+
break
|
|
84
|
+
return candidate
|
|
85
|
+
|
|
86
|
+
# Otherwise, pick the first non-heading meaningful line.
|
|
87
|
+
for line in lines:
|
|
88
|
+
if _is_noise(line):
|
|
89
|
+
continue
|
|
90
|
+
if line.startswith("## "):
|
|
91
|
+
continue
|
|
92
|
+
return line
|
|
93
|
+
|
|
94
|
+
# Fallback: first non-empty line.
|
|
95
|
+
return _first_non_empty_line(cleaned)
|
|
96
|
+
|
|
97
|
+
|
|
45
98
|
def _build_fork_points(conversation_history: list[message.HistoryEvent]) -> list[ForkPoint]:
|
|
46
99
|
"""Build list of fork points from conversation history.
|
|
47
100
|
|
|
48
101
|
Fork points are:
|
|
49
102
|
- Each UserMessage position (for UI display, including first which would be empty session)
|
|
103
|
+
- The latest CompactionEntry boundary (just after it)
|
|
50
104
|
- The end of the conversation (fork entire conversation)
|
|
51
105
|
"""
|
|
52
106
|
fork_points: list[ForkPoint] = []
|
|
@@ -80,24 +134,44 @@ def _build_fork_points(conversation_history: list[message.HistoryEvent]) -> list
|
|
|
80
134
|
user_text = message.join_text_parts(user_item.parts)
|
|
81
135
|
fork_points.append(
|
|
82
136
|
ForkPoint(
|
|
137
|
+
kind="user",
|
|
83
138
|
history_index=user_idx,
|
|
84
|
-
user_message=user_text or "(empty)",
|
|
85
139
|
tool_call_stats=tool_stats,
|
|
140
|
+
user_message=user_text or "(empty)",
|
|
86
141
|
last_assistant_summary=_truncate(last_assistant_content) if last_assistant_content else "",
|
|
87
142
|
)
|
|
88
143
|
)
|
|
89
144
|
|
|
90
|
-
# Add
|
|
91
|
-
|
|
145
|
+
# Add a fork point just after the latest compaction entry (if any).
|
|
146
|
+
last_compaction_idx = -1
|
|
147
|
+
last_compaction: message.CompactionEntry | None = None
|
|
148
|
+
for idx in range(len(conversation_history) - 1, -1, -1):
|
|
149
|
+
item = conversation_history[idx]
|
|
150
|
+
if isinstance(item, message.CompactionEntry):
|
|
151
|
+
last_compaction_idx = idx
|
|
152
|
+
last_compaction = item
|
|
153
|
+
break
|
|
154
|
+
if last_compaction is not None:
|
|
155
|
+
# `until_index` is exclusive; `idx + 1` means include the CompactionEntry itself.
|
|
156
|
+
boundary_index = min(len(conversation_history), last_compaction_idx + 1)
|
|
157
|
+
preview = _truncate(_preview_compaction_summary(last_compaction.summary), 70)
|
|
92
158
|
fork_points.append(
|
|
93
159
|
ForkPoint(
|
|
94
|
-
|
|
95
|
-
|
|
160
|
+
kind="compaction",
|
|
161
|
+
history_index=boundary_index,
|
|
96
162
|
tool_call_stats={},
|
|
97
|
-
|
|
163
|
+
compaction_summary_preview=preview,
|
|
164
|
+
compaction_first_kept_index=last_compaction.first_kept_index,
|
|
165
|
+
compaction_tokens_before=last_compaction.tokens_before,
|
|
98
166
|
)
|
|
99
167
|
)
|
|
100
168
|
|
|
169
|
+
fork_points.sort(key=lambda fp: fp.history_index)
|
|
170
|
+
|
|
171
|
+
# Add the "fork entire conversation" option at the end
|
|
172
|
+
if fork_points:
|
|
173
|
+
fork_points.append(ForkPoint(kind="end", history_index=-1, tool_call_stats={}))
|
|
174
|
+
|
|
101
175
|
return fork_points
|
|
102
176
|
|
|
103
177
|
|
|
@@ -107,7 +181,6 @@ def _build_select_items(fork_points: list[ForkPoint]) -> list[SelectItem[int]]:
|
|
|
107
181
|
|
|
108
182
|
for i, fp in enumerate(fork_points):
|
|
109
183
|
is_first = i == 0
|
|
110
|
-
is_last = i == len(fork_points) - 1
|
|
111
184
|
|
|
112
185
|
# Build the title
|
|
113
186
|
title_parts: list[tuple[str, str]] = []
|
|
@@ -115,12 +188,14 @@ def _build_select_items(fork_points: list[ForkPoint]) -> list[SelectItem[int]]:
|
|
|
115
188
|
# First line: separator (with special markers for first/last fork points)
|
|
116
189
|
if is_first:
|
|
117
190
|
pass
|
|
118
|
-
elif
|
|
191
|
+
elif fp.kind == "end":
|
|
119
192
|
title_parts.append(("class:separator", "----- fork from here (entire session) -----\n\n"))
|
|
193
|
+
elif fp.kind == "compaction":
|
|
194
|
+
title_parts.append(("class:separator", "----- fork after compaction -----\n\n"))
|
|
120
195
|
else:
|
|
121
196
|
title_parts.append(("class:separator", "----- fork from here -----\n\n"))
|
|
122
197
|
|
|
123
|
-
if
|
|
198
|
+
if fp.kind == "user":
|
|
124
199
|
# Second line: user message
|
|
125
200
|
title_parts.append(("class:msg", f"user: {_truncate(fp.user_message, 70)}\n"))
|
|
126
201
|
|
|
@@ -133,6 +208,15 @@ def _build_select_items(fork_points: list[ForkPoint]) -> list[SelectItem[int]]:
|
|
|
133
208
|
if fp.last_assistant_summary:
|
|
134
209
|
title_parts.append(("class:assistant", f"ai: {fp.last_assistant_summary}\n"))
|
|
135
210
|
|
|
211
|
+
elif fp.kind == "compaction":
|
|
212
|
+
kept_from = fp.compaction_first_kept_index
|
|
213
|
+
if kept_from is not None:
|
|
214
|
+
title_parts.append(("class:meta", f"kept: from history index {kept_from}\n"))
|
|
215
|
+
if fp.compaction_tokens_before is not None:
|
|
216
|
+
title_parts.append(("class:meta", f"tokens: {fp.compaction_tokens_before}\n"))
|
|
217
|
+
if fp.compaction_summary_preview:
|
|
218
|
+
title_parts.append(("class:assistant", f"sum: {fp.compaction_summary_preview}\n"))
|
|
219
|
+
|
|
136
220
|
# Empty line at the end
|
|
137
221
|
title_parts.append(("class:text", "\n"))
|
|
138
222
|
|
|
@@ -140,8 +224,16 @@ def _build_select_items(fork_points: list[ForkPoint]) -> list[SelectItem[int]]:
|
|
|
140
224
|
SelectItem(
|
|
141
225
|
title=title_parts,
|
|
142
226
|
value=fp.history_index,
|
|
143
|
-
search_text=
|
|
144
|
-
|
|
227
|
+
search_text=(
|
|
228
|
+
fp.user_message
|
|
229
|
+
if fp.kind == "user"
|
|
230
|
+
else (
|
|
231
|
+
f"compaction {fp.compaction_summary_preview}"
|
|
232
|
+
if fp.kind == "compaction"
|
|
233
|
+
else "fork entire conversation"
|
|
234
|
+
)
|
|
235
|
+
),
|
|
236
|
+
selectable=not (fp.kind == "user" and is_first),
|
|
145
237
|
)
|
|
146
238
|
)
|
|
147
239
|
|
|
@@ -194,7 +286,7 @@ class ForkSessionCommand(CommandABC):
|
|
|
194
286
|
|
|
195
287
|
@property
|
|
196
288
|
def summary(self) -> str:
|
|
197
|
-
return "Fork the current session and show a resume
|
|
289
|
+
return "Fork the current session and show a resume command"
|
|
198
290
|
|
|
199
291
|
@property
|
|
200
292
|
def is_interactive(self) -> bool:
|
|
@@ -220,7 +312,7 @@ class ForkSessionCommand(CommandABC):
|
|
|
220
312
|
new_session = agent.session.fork()
|
|
221
313
|
await new_session.wait_for_flush()
|
|
222
314
|
|
|
223
|
-
resume_cmd = f"klaude --resume
|
|
315
|
+
resume_cmd = f"klaude --resume {new_session.id}"
|
|
224
316
|
copy_to_clipboard(resume_cmd)
|
|
225
317
|
|
|
226
318
|
event = events.CommandOutputEvent(
|
|
@@ -247,9 +339,13 @@ class ForkSessionCommand(CommandABC):
|
|
|
247
339
|
await new_session.wait_for_flush()
|
|
248
340
|
|
|
249
341
|
# Build result message
|
|
250
|
-
|
|
342
|
+
selected_point = next((fp for fp in fork_points if fp.history_index == selected), None)
|
|
343
|
+
if selected_point is not None and selected_point.kind == "compaction":
|
|
344
|
+
fork_description = "after compaction"
|
|
345
|
+
else:
|
|
346
|
+
fork_description = "entire conversation" if selected == -1 else f"up to message index {selected}"
|
|
251
347
|
|
|
252
|
-
resume_cmd = f"klaude --resume
|
|
348
|
+
resume_cmd = f"klaude --resume {new_session.id}"
|
|
253
349
|
copy_to_clipboard(resume_cmd)
|
|
254
350
|
|
|
255
351
|
event = events.CommandOutputEvent(
|
|
@@ -79,7 +79,11 @@ def select_model_interactive(
|
|
|
79
79
|
try:
|
|
80
80
|
items = build_model_select_items(result.filtered_models)
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
total_count = len(result.filtered_models)
|
|
83
|
+
if result.filter_hint:
|
|
84
|
+
message = f"Select a model ({total_count}, filtered by '{result.filter_hint}'):"
|
|
85
|
+
else:
|
|
86
|
+
message = f"Select a model ({total_count}):"
|
|
83
87
|
|
|
84
88
|
initial_value = config.main_model
|
|
85
89
|
if isinstance(initial_value, str) and initial_value and "@" not in initial_value:
|
|
@@ -14,7 +14,7 @@ def _select_thinking_sync(config: llm_param.LLMConfigParameter) -> llm_param.Thi
|
|
|
14
14
|
return None
|
|
15
15
|
|
|
16
16
|
items: list[SelectItem[str]] = [
|
|
17
|
-
SelectItem(title=[("class:
|
|
17
|
+
SelectItem(title=[("class:msg", opt.label + "\n")], value=opt.value, search_text=opt.label)
|
|
18
18
|
for opt in data.options
|
|
19
19
|
]
|
|
20
20
|
|
klaude_code/tui/commands.py
CHANGED
|
@@ -162,3 +162,9 @@ class TaskClockStart(RenderCommand):
|
|
|
162
162
|
@dataclass(frozen=True, slots=True)
|
|
163
163
|
class TaskClockClear(RenderCommand):
|
|
164
164
|
pass
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True, slots=True)
|
|
168
|
+
class RenderCompactionSummary(RenderCommand):
|
|
169
|
+
summary: str
|
|
170
|
+
kept_items_brief: tuple[tuple[str, int, str], ...] = () # (item_type, count, preview)
|
|
@@ -50,7 +50,7 @@ def _render_fork_session_output(e: events.CommandOutputEvent) -> RenderableType:
|
|
|
50
50
|
grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
|
|
51
51
|
|
|
52
52
|
grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
|
|
53
|
-
grid.add_row(Text(f" klaude --resume
|
|
53
|
+
grid.add_row(Text(f" klaude --resume {session_id}", style=ThemeKey.TOOL_RESULT_BOLD))
|
|
54
54
|
|
|
55
55
|
return Padding.indent(grid, level=2)
|
|
56
56
|
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import io
|
|
5
|
+
import re
|
|
5
6
|
import time
|
|
6
7
|
from collections.abc import Callable
|
|
7
8
|
from typing import Any, ClassVar
|
|
@@ -9,9 +10,11 @@ from typing import Any, ClassVar
|
|
|
9
10
|
from markdown_it import MarkdownIt
|
|
10
11
|
from markdown_it.token import Token
|
|
11
12
|
from rich import box
|
|
13
|
+
from rich._loop import loop_first
|
|
12
14
|
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
13
|
-
from rich.markdown import CodeBlock, Heading, Markdown, MarkdownElement, TableElement
|
|
15
|
+
from rich.markdown import CodeBlock, Heading, ListItem, Markdown, MarkdownElement, TableElement
|
|
14
16
|
from rich.rule import Rule
|
|
17
|
+
from rich.segment import Segment
|
|
15
18
|
from rich.style import Style, StyleType
|
|
16
19
|
from rich.syntax import Syntax
|
|
17
20
|
from rich.table import Table
|
|
@@ -26,6 +29,66 @@ from klaude_code.const import (
|
|
|
26
29
|
)
|
|
27
30
|
from klaude_code.tui.components.rich.code_panel import CodePanel
|
|
28
31
|
|
|
32
|
+
_THINKING_HTML_BLOCK_RE = re.compile(
|
|
33
|
+
r"\A\s*<thinking>\s*\n?(?P<body>.*?)(?:\n\s*)?</thinking>\s*\Z",
|
|
34
|
+
flags=re.IGNORECASE | re.DOTALL,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_HTML_COMMENT_BLOCK_RE = re.compile(r"\A\s*<!--.*?-->\s*\Z", flags=re.DOTALL)
|
|
38
|
+
|
|
39
|
+
_CHECKBOX_UNCHECKED_RE = re.compile(r"^\[ \]\s*")
|
|
40
|
+
_CHECKBOX_CHECKED_RE = re.compile(r"^\[x\]\s*", re.IGNORECASE)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ThinkingHTMLBlock(MarkdownElement):
|
|
44
|
+
"""Render `<thinking>...</thinking>` HTML blocks as Rich Markdown.
|
|
45
|
+
|
|
46
|
+
markdown-it-py treats custom tags like `<thinking>` as HTML blocks, and Rich
|
|
47
|
+
Markdown ignores HTML blocks by default. This element restores visibility by
|
|
48
|
+
re-parsing the inner content as Markdown and applying a dedicated style.
|
|
49
|
+
|
|
50
|
+
Non-thinking HTML blocks (including comment sentinels like `<!-- -->`) render
|
|
51
|
+
no visible output, matching Rich's default behavior.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
new_line: ClassVar[bool] = True
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def create(cls, markdown: Markdown, token: Token) -> ThinkingHTMLBlock:
|
|
58
|
+
return cls(content=token.content or "", code_theme=markdown.code_theme)
|
|
59
|
+
|
|
60
|
+
def __init__(self, *, content: str, code_theme: str) -> None:
|
|
61
|
+
self._content = content
|
|
62
|
+
self._code_theme = code_theme
|
|
63
|
+
|
|
64
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
65
|
+
stripped = self._content.strip()
|
|
66
|
+
|
|
67
|
+
# Keep HTML comments invisible. MarkdownStream relies on a comment sentinel
|
|
68
|
+
# (`<!-- -->`) to preserve inter-block spacing in some streaming frames.
|
|
69
|
+
if _HTML_COMMENT_BLOCK_RE.match(stripped):
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
match = _THINKING_HTML_BLOCK_RE.match(stripped)
|
|
73
|
+
if match is None:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
body = match.group("body").strip("\n")
|
|
77
|
+
if not body.strip():
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Render as a single line to avoid the extra blank lines produced by
|
|
81
|
+
# paragraph/block rendering.
|
|
82
|
+
collapsed = " ".join(body.split())
|
|
83
|
+
if not collapsed:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
text = Text()
|
|
87
|
+
text.append("<thinking>", style="markdown.thinking.tag")
|
|
88
|
+
text.append(collapsed, style="markdown.thinking")
|
|
89
|
+
text.append("</thinking>", style="markdown.thinking.tag")
|
|
90
|
+
yield text
|
|
91
|
+
|
|
29
92
|
|
|
30
93
|
class NoInsetCodeBlock(CodeBlock):
|
|
31
94
|
"""A code block with syntax highlighting and no padding."""
|
|
@@ -95,6 +158,55 @@ class LeftHeading(Heading):
|
|
|
95
158
|
yield text
|
|
96
159
|
|
|
97
160
|
|
|
161
|
+
class CheckboxListItem(ListItem):
|
|
162
|
+
"""A list item that renders checkbox syntax as Unicode symbols."""
|
|
163
|
+
|
|
164
|
+
def render_bullet(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
165
|
+
render_options = options.update(width=options.max_width - 3)
|
|
166
|
+
lines = console.render_lines(self.elements, render_options, style=self.style)
|
|
167
|
+
bullet_style = console.get_style("markdown.item.bullet", default="none")
|
|
168
|
+
|
|
169
|
+
first_line_text = ""
|
|
170
|
+
if lines:
|
|
171
|
+
first_line_text = "".join(seg.text for seg in lines[0] if seg.text)
|
|
172
|
+
|
|
173
|
+
unchecked_match = _CHECKBOX_UNCHECKED_RE.match(first_line_text)
|
|
174
|
+
checked_match = _CHECKBOX_CHECKED_RE.match(first_line_text)
|
|
175
|
+
|
|
176
|
+
if unchecked_match:
|
|
177
|
+
bullet = Segment(" \u2610 ", bullet_style)
|
|
178
|
+
skip_chars = len(unchecked_match.group(0))
|
|
179
|
+
elif checked_match:
|
|
180
|
+
checked_style = console.get_style("markdown.checkbox.checked", default="none")
|
|
181
|
+
bullet = Segment(" \u2713 ", checked_style)
|
|
182
|
+
skip_chars = len(checked_match.group(0))
|
|
183
|
+
else:
|
|
184
|
+
bullet = Segment(" \u2022 ", bullet_style)
|
|
185
|
+
skip_chars = 0
|
|
186
|
+
|
|
187
|
+
padding = Segment(" " * 3, bullet_style)
|
|
188
|
+
new_line = Segment("\n")
|
|
189
|
+
|
|
190
|
+
for first, line in loop_first(lines):
|
|
191
|
+
yield bullet if first else padding
|
|
192
|
+
if first and skip_chars > 0:
|
|
193
|
+
chars_skipped = 0
|
|
194
|
+
for seg in line:
|
|
195
|
+
if seg.text and chars_skipped < skip_chars:
|
|
196
|
+
remaining = skip_chars - chars_skipped
|
|
197
|
+
if len(seg.text) <= remaining:
|
|
198
|
+
chars_skipped += len(seg.text)
|
|
199
|
+
continue
|
|
200
|
+
else:
|
|
201
|
+
yield Segment(seg.text[remaining:], seg.style)
|
|
202
|
+
chars_skipped = skip_chars
|
|
203
|
+
else:
|
|
204
|
+
yield seg
|
|
205
|
+
else:
|
|
206
|
+
yield from line
|
|
207
|
+
yield new_line
|
|
208
|
+
|
|
209
|
+
|
|
98
210
|
class NoInsetMarkdown(Markdown):
|
|
99
211
|
"""Markdown with code blocks that have no padding and left-justified headings."""
|
|
100
212
|
|
|
@@ -105,6 +217,8 @@ class NoInsetMarkdown(Markdown):
|
|
|
105
217
|
"heading_open": LeftHeading,
|
|
106
218
|
"hr": Divider,
|
|
107
219
|
"table_open": MarkdownTable,
|
|
220
|
+
"html_block": ThinkingHTMLBlock,
|
|
221
|
+
"list_item_open": CheckboxListItem,
|
|
108
222
|
}
|
|
109
223
|
|
|
110
224
|
|
|
@@ -118,6 +232,8 @@ class ThinkingMarkdown(Markdown):
|
|
|
118
232
|
"heading_open": LeftHeading,
|
|
119
233
|
"hr": Divider,
|
|
120
234
|
"table_open": MarkdownTable,
|
|
235
|
+
"html_block": ThinkingHTMLBlock,
|
|
236
|
+
"list_item_open": CheckboxListItem,
|
|
121
237
|
}
|
|
122
238
|
|
|
123
239
|
|
|
@@ -95,7 +95,7 @@ DARK_PALETTE = Palette(
|
|
|
95
95
|
code_theme="ansi_dark",
|
|
96
96
|
code_background="#1a1f2a",
|
|
97
97
|
green_background="#23342c",
|
|
98
|
-
blue_grey_background="#
|
|
98
|
+
blue_grey_background="#262d3a",
|
|
99
99
|
cyan_background="#1a3333",
|
|
100
100
|
green_sub_background="#1b3928",
|
|
101
101
|
blue_sub_background="#1a2a3d",
|
|
@@ -116,6 +116,7 @@ class ThemeKey(str, Enum):
|
|
|
116
116
|
# PANEL
|
|
117
117
|
SUB_AGENT_RESULT_PANEL = "panel.sub_agent_result"
|
|
118
118
|
WRITE_MARKDOWN_PANEL = "panel.write_markdown"
|
|
119
|
+
COMPACTION_SUMMARY_PANEL = "panel.compaction_summary"
|
|
119
120
|
# DIFF
|
|
120
121
|
DIFF_FILE_NAME = "diff.file_name"
|
|
121
122
|
DIFF_REMOVE = "diff.remove"
|
|
@@ -178,6 +179,8 @@ class ThemeKey(str, Enum):
|
|
|
178
179
|
# THINKING
|
|
179
180
|
THINKING = "thinking"
|
|
180
181
|
THINKING_BOLD = "thinking.bold"
|
|
182
|
+
# COMPACTION
|
|
183
|
+
COMPACTION_SUMMARY = "compaction.summary"
|
|
181
184
|
# TODO_ITEM
|
|
182
185
|
TODO_EXPLANATION = "todo.explanation"
|
|
183
186
|
TODO_PENDING_MARK = "todo.pending.mark"
|
|
@@ -235,6 +238,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
235
238
|
# PANEL
|
|
236
239
|
ThemeKey.SUB_AGENT_RESULT_PANEL.value: f"on {palette.blue_grey_background}",
|
|
237
240
|
ThemeKey.WRITE_MARKDOWN_PANEL.value: f"on {palette.green_background}",
|
|
241
|
+
ThemeKey.COMPACTION_SUMMARY_PANEL.value: f"on {palette.blue_grey_background}",
|
|
238
242
|
# DIFF
|
|
239
243
|
ThemeKey.DIFF_FILE_NAME.value: palette.blue,
|
|
240
244
|
ThemeKey.DIFF_REMOVE.value: palette.diff_remove,
|
|
@@ -247,7 +251,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
247
251
|
ThemeKey.ERROR.value: palette.red,
|
|
248
252
|
ThemeKey.ERROR_BOLD.value: "bold " + palette.red,
|
|
249
253
|
ThemeKey.ERROR_DIM.value: "dim " + palette.red,
|
|
250
|
-
ThemeKey.INTERRUPT.value:
|
|
254
|
+
ThemeKey.INTERRUPT.value: palette.red,
|
|
251
255
|
# USER_INPUT
|
|
252
256
|
ThemeKey.USER_INPUT.value: palette.magenta,
|
|
253
257
|
ThemeKey.USER_INPUT_PROMPT.value: "bold " + palette.magenta,
|
|
@@ -296,6 +300,8 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
296
300
|
# THINKING
|
|
297
301
|
ThemeKey.THINKING.value: "italic " + palette.grey2,
|
|
298
302
|
ThemeKey.THINKING_BOLD.value: "bold italic " + palette.grey1,
|
|
303
|
+
# COMPACTION
|
|
304
|
+
ThemeKey.COMPACTION_SUMMARY.value: "italic " + palette.grey1,
|
|
299
305
|
# TODO_ITEM
|
|
300
306
|
ThemeKey.TODO_EXPLANATION.value: palette.grey1 + " italic",
|
|
301
307
|
ThemeKey.TODO_PENDING_MARK.value: "bold " + palette.grey1,
|
|
@@ -331,7 +337,14 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
331
337
|
markdown_theme=Theme(
|
|
332
338
|
styles={
|
|
333
339
|
"markdown.code": palette.purple,
|
|
340
|
+
# Render degraded `<thinking>...</thinking>` blocks inside assistant markdown.
|
|
341
|
+
# This must live in markdown_theme (not just thinking_markdown_theme) because
|
|
342
|
+
# it is used while rendering assistant output.
|
|
343
|
+
"markdown.thinking": "italic " + palette.grey2,
|
|
344
|
+
"markdown.thinking.tag": palette.grey2,
|
|
334
345
|
"markdown.code.border": palette.grey3,
|
|
346
|
+
# Used by ThinkingMarkdown when rendering `<thinking>` blocks.
|
|
347
|
+
"markdown.code.block": palette.grey1,
|
|
335
348
|
"markdown.h1": "bold reverse",
|
|
336
349
|
"markdown.h1.border": palette.grey3,
|
|
337
350
|
"markdown.h2": "bold underline",
|
|
@@ -343,6 +356,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
343
356
|
"markdown.link": "underline " + palette.blue,
|
|
344
357
|
"markdown.link_url": "underline " + palette.blue,
|
|
345
358
|
"markdown.table.border": palette.grey2,
|
|
359
|
+
"markdown.checkbox.checked": palette.green,
|
|
346
360
|
}
|
|
347
361
|
),
|
|
348
362
|
thinking_markdown_theme=Theme(
|
|
@@ -353,6 +367,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
353
367
|
"markdown.code": palette.grey1 + " italic on " + palette.code_background,
|
|
354
368
|
"markdown.code.block": palette.grey1,
|
|
355
369
|
"markdown.code.border": palette.grey3,
|
|
370
|
+
"markdown.thinking.tag": palette.grey2 + " dim",
|
|
356
371
|
"markdown.h1": "bold reverse",
|
|
357
372
|
"markdown.h1.border": palette.grey3,
|
|
358
373
|
"markdown.h3": "bold " + palette.grey1,
|
|
@@ -364,6 +379,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
364
379
|
"markdown.link_url": "underline " + palette.blue,
|
|
365
380
|
"markdown.strong": "bold italic " + palette.grey1,
|
|
366
381
|
"markdown.table.border": palette.grey2,
|
|
382
|
+
"markdown.checkbox.checked": palette.green,
|
|
367
383
|
}
|
|
368
384
|
),
|
|
369
385
|
code_theme=palette.code_theme,
|
|
@@ -263,11 +263,7 @@ def render_write_tool_call(arguments: str) -> RenderableType:
|
|
|
263
263
|
try:
|
|
264
264
|
json_dict = json.loads(arguments)
|
|
265
265
|
file_path = json_dict.get("file_path", "")
|
|
266
|
-
|
|
267
|
-
if file_path.endswith(".md"):
|
|
268
|
-
details: RenderableType | None = None
|
|
269
|
-
else:
|
|
270
|
-
details = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
|
|
266
|
+
details: RenderableType | None = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
|
|
271
267
|
except json.JSONDecodeError:
|
|
272
268
|
details = Text(
|
|
273
269
|
arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
|
|
@@ -292,24 +288,29 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
292
288
|
details = Text("", ThemeKey.TOOL_PARAM)
|
|
293
289
|
|
|
294
290
|
if isinstance(patch_content, str):
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
291
|
+
update_files: list[str] = []
|
|
292
|
+
add_files: list[str] = []
|
|
293
|
+
delete_files: list[str] = []
|
|
298
294
|
for line in patch_content.splitlines():
|
|
299
295
|
if line.startswith("*** Update File:"):
|
|
300
|
-
|
|
296
|
+
update_files.append(line[len("*** Update File:") :].strip())
|
|
301
297
|
elif line.startswith("*** Add File:"):
|
|
302
|
-
|
|
298
|
+
add_files.append(line[len("*** Add File:") :].strip())
|
|
303
299
|
elif line.startswith("*** Delete File:"):
|
|
304
|
-
|
|
300
|
+
delete_files.append(line[len("*** Delete File:") :].strip())
|
|
305
301
|
|
|
306
302
|
parts: list[str] = []
|
|
307
|
-
if
|
|
308
|
-
parts.append(f"Update File × {
|
|
309
|
-
if
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
303
|
+
if update_files:
|
|
304
|
+
parts.append(f"Update File × {len(update_files)}" if len(update_files) > 1 else "Update File")
|
|
305
|
+
if add_files:
|
|
306
|
+
# For single .md file addition, show filename in parentheses
|
|
307
|
+
if len(add_files) == 1 and add_files[0].endswith(".md"):
|
|
308
|
+
file_name = Path(add_files[0]).name
|
|
309
|
+
parts.append(f"Add File ({file_name})")
|
|
310
|
+
else:
|
|
311
|
+
parts.append(f"Add File × {len(add_files)}" if len(add_files) > 1 else "Add File")
|
|
312
|
+
if delete_files:
|
|
313
|
+
parts.append(f"Delete File × {len(delete_files)}" if len(delete_files) > 1 else "Delete File")
|
|
313
314
|
|
|
314
315
|
if parts:
|
|
315
316
|
details = Text(", ".join(parts), ThemeKey.TOOL_PARAM)
|
|
@@ -593,14 +594,24 @@ def _extract_markdown_doc(ui_extra: model.ToolResultUIExtra | None) -> model.Mar
|
|
|
593
594
|
|
|
594
595
|
|
|
595
596
|
def render_markdown_doc(md_ui: model.MarkdownDocUIExtra, *, code_theme: str) -> RenderableType:
|
|
596
|
-
"""Render markdown document content in a panel."""
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
597
|
+
"""Render markdown document content in a panel with 2-char left indent and top margin."""
|
|
598
|
+
import shutil
|
|
599
|
+
|
|
600
|
+
from rich.padding import Padding
|
|
601
|
+
|
|
602
|
+
# Limit panel width to min(100, terminal_width) minus left indent (2)
|
|
603
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
604
|
+
panel_width = min(100, terminal_width) - 2
|
|
605
|
+
|
|
606
|
+
panel = Panel(
|
|
607
|
+
NoInsetMarkdown(md_ui.content, code_theme=code_theme),
|
|
600
608
|
box=box.SIMPLE,
|
|
601
609
|
border_style=ThemeKey.LINES,
|
|
602
610
|
style=ThemeKey.WRITE_MARKDOWN_PANEL,
|
|
611
|
+
width=panel_width,
|
|
603
612
|
)
|
|
613
|
+
# (top, right, bottom, left) - 1 line top margin, 2-char left indent
|
|
614
|
+
return Padding(panel, (1, 0, 0, 2))
|
|
604
615
|
|
|
605
616
|
|
|
606
617
|
def render_tool_result(
|
|
@@ -628,11 +639,12 @@ def render_tool_result(
|
|
|
628
639
|
rendered: list[RenderableType] = []
|
|
629
640
|
for item in e.ui_extra.items:
|
|
630
641
|
if isinstance(item, model.MarkdownDocUIExtra):
|
|
642
|
+
# Markdown docs render without TreeQuote wrap (already has 2-char indent)
|
|
631
643
|
rendered.append(render_markdown_doc(item, code_theme=code_theme))
|
|
632
644
|
elif isinstance(item, model.DiffUIExtra):
|
|
633
645
|
show_file_name = e.tool_name == tools.APPLY_PATCH
|
|
634
|
-
rendered.append(r_diffs.render_structured_diff(item, show_file_name=show_file_name))
|
|
635
|
-
return
|
|
646
|
+
rendered.append(wrap(r_diffs.render_structured_diff(item, show_file_name=show_file_name)))
|
|
647
|
+
return Group(*rendered) if rendered else None
|
|
636
648
|
|
|
637
649
|
diff_ui = _extract_diff(e.ui_extra)
|
|
638
650
|
md_ui = _extract_markdown_doc(e.ui_extra)
|
|
@@ -649,11 +661,13 @@ def render_tool_result(
|
|
|
649
661
|
return wrap(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""))
|
|
650
662
|
case tools.WRITE:
|
|
651
663
|
if md_ui:
|
|
652
|
-
|
|
664
|
+
# Markdown docs render without TreeQuote wrap (already has 2-char indent)
|
|
665
|
+
return render_markdown_doc(md_ui, code_theme=code_theme)
|
|
653
666
|
return wrap(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""))
|
|
654
667
|
case tools.APPLY_PATCH:
|
|
655
668
|
if md_ui:
|
|
656
|
-
|
|
669
|
+
# Markdown docs render without TreeQuote wrap (already has 2-char indent)
|
|
670
|
+
return render_markdown_doc(md_ui, code_theme=code_theme)
|
|
657
671
|
if diff_ui:
|
|
658
672
|
return wrap(r_diffs.render_structured_diff(diff_ui, show_file_name=True))
|
|
659
673
|
return _render_fallback()
|