klaude-code 2.7.0__py3-none-any.whl → 2.8.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/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/cli/auth_cmd.py +53 -3
- klaude_code/cli/cost_cmd.py +83 -160
- klaude_code/cli/list_model.py +50 -0
- klaude_code/cli/main.py +2 -2
- 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 +2 -1
- klaude_code/core/agent.py +5 -1
- klaude_code/core/agent_profile.py +29 -33
- klaude_code/core/compaction/AGENTS.md +112 -0
- klaude_code/core/compaction/__init__.py +11 -0
- klaude_code/core/compaction/compaction.py +705 -0
- klaude_code/core/compaction/overflow.py +30 -0
- klaude_code/core/compaction/prompts.py +97 -0
- klaude_code/core/executor.py +121 -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 +7 -2
- klaude_code/core/task.py +126 -0
- klaude_code/core/tool/file/edit_tool.py +1 -2
- 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 +1 -0
- 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 +29 -1
- klaude_code/protocol/op_handler.py +10 -0
- klaude_code/session/export.py +308 -299
- klaude_code/session/session.py +36 -0
- klaude_code/session/templates/export_session.html +430 -134
- klaude_code/skill/assets/create-plan/SKILL.md +6 -6
- klaude_code/tui/command/__init__.py +6 -0
- klaude_code/tui/command/compact_cmd.py +32 -0
- klaude_code/tui/command/continue_cmd.py +34 -0
- klaude_code/tui/command/fork_session_cmd.py +110 -14
- 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/rich/markdown.py +119 -12
- klaude_code/tui/components/rich/theme.py +10 -2
- klaude_code/tui/components/tools.py +39 -25
- klaude_code/tui/components/user_input.py +1 -1
- klaude_code/tui/input/__init__.py +5 -2
- klaude_code/tui/input/drag_drop.py +6 -57
- klaude_code/tui/input/key_bindings.py +10 -0
- klaude_code/tui/input/prompt_toolkit.py +19 -6
- klaude_code/tui/machine.py +25 -0
- klaude_code/tui/renderer.py +68 -4
- klaude_code/tui/runner.py +18 -2
- klaude_code/tui/terminal/image.py +72 -10
- klaude_code/tui/terminal/selector.py +31 -7
- {klaude_code-2.7.0.dist-info → klaude_code-2.8.1.dist-info}/METADATA +1 -1
- {klaude_code-2.7.0.dist-info → klaude_code-2.8.1.dist-info}/RECORD +73 -56
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -117
- {klaude_code-2.7.0.dist-info → klaude_code-2.8.1.dist-info}/WHEEL +0 -0
- {klaude_code-2.7.0.dist-info → klaude_code-2.8.1.dist-info}/entry_points.txt +0 -0
|
@@ -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()
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from klaude_code.tui.input.prompt_toolkit import REPLStatusSnapshot
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
def build_repl_status_snapshot(
|
|
4
|
+
def build_repl_status_snapshot(
|
|
5
|
+
update_message: str | None,
|
|
6
|
+
debug_log_path: str | None = None,
|
|
7
|
+
) -> REPLStatusSnapshot:
|
|
5
8
|
"""Build a status snapshot for the REPL bottom toolbar."""
|
|
6
|
-
return REPLStatusSnapshot(update_message=update_message)
|
|
9
|
+
return REPLStatusSnapshot(update_message=update_message, debug_log_path=debug_log_path)
|
|
@@ -13,7 +13,6 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import contextlib
|
|
15
15
|
import re
|
|
16
|
-
import shlex
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from urllib.parse import unquote, urlparse
|
|
19
18
|
|
|
@@ -132,66 +131,16 @@ def _replace_file_uris(
|
|
|
132
131
|
return out, changed
|
|
133
132
|
|
|
134
133
|
|
|
135
|
-
def _looks_like_path_list(text: str) -> list[str] | None:
|
|
136
|
-
"""Return tokens if text looks like a pure path list, else None."""
|
|
137
|
-
|
|
138
|
-
stripped = text.strip()
|
|
139
|
-
if not stripped:
|
|
140
|
-
return None
|
|
141
|
-
|
|
142
|
-
# Avoid converting when the paste already contains our input syntax.
|
|
143
|
-
if "@" in stripped or "[image " in stripped:
|
|
144
|
-
return None
|
|
145
|
-
|
|
146
|
-
try:
|
|
147
|
-
tokens = shlex.split(stripped, posix=True)
|
|
148
|
-
except ValueError:
|
|
149
|
-
return None
|
|
150
|
-
|
|
151
|
-
if not tokens:
|
|
152
|
-
return None
|
|
153
|
-
|
|
154
|
-
# Heuristic: all tokens must exist on disk.
|
|
155
|
-
for tok in tokens:
|
|
156
|
-
p = Path(tok).expanduser()
|
|
157
|
-
try:
|
|
158
|
-
if not p.exists():
|
|
159
|
-
return None
|
|
160
|
-
except OSError:
|
|
161
|
-
return None
|
|
162
|
-
|
|
163
|
-
return tokens
|
|
164
|
-
|
|
165
|
-
|
|
166
134
|
def convert_dropped_text(
|
|
167
135
|
text: str,
|
|
168
136
|
*,
|
|
169
137
|
cwd: Path,
|
|
170
138
|
) -> str:
|
|
171
|
-
"""Convert drag-and-drop
|
|
172
|
-
|
|
173
|
-
out, changed = _replace_file_uris(text, cwd=cwd)
|
|
174
|
-
if changed:
|
|
175
|
-
return out
|
|
176
|
-
|
|
177
|
-
tokens = _looks_like_path_list(text)
|
|
178
|
-
if not tokens:
|
|
179
|
-
return text
|
|
139
|
+
"""Convert drag-and-drop file:// URIs into @ tokens and/or image markers.
|
|
180
140
|
|
|
181
|
-
converted
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
try:
|
|
185
|
-
is_img = p.exists() and p.is_file() and is_image_file(p)
|
|
186
|
-
except OSError:
|
|
187
|
-
is_img = False
|
|
188
|
-
|
|
189
|
-
if is_img:
|
|
190
|
-
converted.append(format_image_marker(_normalize_path_for_at(p, cwd=cwd)))
|
|
191
|
-
continue
|
|
192
|
-
|
|
193
|
-
converted.append(_format_at_token(_normalize_path_for_at(p, cwd=cwd)))
|
|
141
|
+
Only file:// URIs are converted. Plain paths are not auto-converted to avoid
|
|
142
|
+
unintended transformations when users paste regular path strings.
|
|
143
|
+
"""
|
|
194
144
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return " ".join(converted) + suffix
|
|
145
|
+
out, _ = _replace_file_uris(text, cwd=cwd)
|
|
146
|
+
return out
|
|
@@ -578,4 +578,14 @@ def create_key_bindings(
|
|
|
578
578
|
with contextlib.suppress(Exception):
|
|
579
579
|
open_thinking_picker()
|
|
580
580
|
|
|
581
|
+
@kb.add("escape", "up", filter=enabled & ~has_completions)
|
|
582
|
+
def _(event: KeyPressEvent) -> None:
|
|
583
|
+
"""Option+Up switches to previous history entry."""
|
|
584
|
+
event.current_buffer.history_backward()
|
|
585
|
+
|
|
586
|
+
@kb.add("escape", "down", filter=enabled & ~has_completions)
|
|
587
|
+
def _(event: KeyPressEvent) -> None:
|
|
588
|
+
"""Option+Down switches to next history entry."""
|
|
589
|
+
event.current_buffer.history_forward()
|
|
590
|
+
|
|
581
591
|
return kb
|
|
@@ -53,6 +53,7 @@ class REPLStatusSnapshot(NamedTuple):
|
|
|
53
53
|
"""Snapshot of REPL status for bottom toolbar display."""
|
|
54
54
|
|
|
55
55
|
update_message: str | None = None
|
|
56
|
+
debug_log_path: str | None = None
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
COMPLETION_SELECTED_DARK_BG = "ansigreen"
|
|
@@ -434,7 +435,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
434
435
|
original_height_int = original_height_value if isinstance(original_height_value, int) else None
|
|
435
436
|
|
|
436
437
|
if picker_open or completion_open:
|
|
437
|
-
target_rows =
|
|
438
|
+
target_rows = 24 if picker_open else 14
|
|
438
439
|
|
|
439
440
|
# Cap to the current terminal size.
|
|
440
441
|
# Leave a small buffer to avoid triggering "Window too small".
|
|
@@ -531,7 +532,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
531
532
|
return [], None
|
|
532
533
|
|
|
533
534
|
items: list[SelectItem[str]] = [
|
|
534
|
-
SelectItem(title=[("class:
|
|
535
|
+
SelectItem(title=[("class:msg", opt.label + "\n")], value=opt.value, search_text=opt.label)
|
|
535
536
|
for opt in data.options
|
|
536
537
|
]
|
|
537
538
|
return items, data.current_value
|
|
@@ -573,24 +574,36 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
573
574
|
doing any blocking IO here.
|
|
574
575
|
"""
|
|
575
576
|
update_message: str | None = None
|
|
577
|
+
debug_log_path: str | None = None
|
|
576
578
|
if self._status_provider is not None:
|
|
577
579
|
try:
|
|
578
580
|
status = self._status_provider()
|
|
579
581
|
update_message = status.update_message
|
|
582
|
+
debug_log_path = status.debug_log_path
|
|
580
583
|
except (AttributeError, RuntimeError):
|
|
581
|
-
|
|
584
|
+
pass
|
|
585
|
+
|
|
586
|
+
# Priority: update_message > debug_log_path
|
|
587
|
+
display_text: str | None = None
|
|
588
|
+
text_style: str = ""
|
|
589
|
+
if update_message:
|
|
590
|
+
display_text = update_message
|
|
591
|
+
text_style = "#ansiyellow"
|
|
592
|
+
elif debug_log_path:
|
|
593
|
+
display_text = f"Debug log: {debug_log_path}"
|
|
594
|
+
text_style = "fg:ansibrightblack"
|
|
582
595
|
|
|
583
596
|
# If nothing to show, return a blank line to actively clear any previously
|
|
584
597
|
# rendered content. (When `bottom_toolbar` is a callable, prompt_toolkit
|
|
585
598
|
# will still reserve the toolbar line.)
|
|
586
|
-
if not
|
|
599
|
+
if not display_text:
|
|
587
600
|
try:
|
|
588
601
|
terminal_width = shutil.get_terminal_size().columns
|
|
589
602
|
except (OSError, ValueError):
|
|
590
603
|
terminal_width = 0
|
|
591
604
|
return FormattedText([("", " " * max(0, terminal_width))])
|
|
592
605
|
|
|
593
|
-
left_text = " " +
|
|
606
|
+
left_text = " " + display_text
|
|
594
607
|
try:
|
|
595
608
|
terminal_width = shutil.get_terminal_size().columns
|
|
596
609
|
padding = " " * max(0, terminal_width - len(left_text))
|
|
@@ -598,7 +611,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
598
611
|
padding = ""
|
|
599
612
|
|
|
600
613
|
toolbar_text = left_text + padding
|
|
601
|
-
return FormattedText([(
|
|
614
|
+
return FormattedText([(text_style, toolbar_text)])
|
|
602
615
|
|
|
603
616
|
# -------------------------------------------------------------------------
|
|
604
617
|
# Placeholder
|
klaude_code/tui/machine.py
CHANGED
|
@@ -6,6 +6,7 @@ from rich.text import Text
|
|
|
6
6
|
|
|
7
7
|
from klaude_code.const import (
|
|
8
8
|
SIGINT_DOUBLE_PRESS_EXIT_TEXT,
|
|
9
|
+
STATUS_COMPACTING_TEXT,
|
|
9
10
|
STATUS_COMPOSING_TEXT,
|
|
10
11
|
STATUS_DEFAULT_TEXT,
|
|
11
12
|
STATUS_SHOW_BUFFER_LENGTH,
|
|
@@ -24,6 +25,7 @@ from klaude_code.tui.commands import (
|
|
|
24
25
|
RenderAssistantImage,
|
|
25
26
|
RenderCommand,
|
|
26
27
|
RenderCommandOutput,
|
|
28
|
+
RenderCompactionSummary,
|
|
27
29
|
RenderDeveloperMessage,
|
|
28
30
|
RenderError,
|
|
29
31
|
RenderInterrupt,
|
|
@@ -311,6 +313,7 @@ class _SessionState:
|
|
|
311
313
|
thinking_stream_active: bool = False
|
|
312
314
|
assistant_char_count: int = 0
|
|
313
315
|
thinking_tail: str = ""
|
|
316
|
+
task_active: bool = False
|
|
314
317
|
|
|
315
318
|
@property
|
|
316
319
|
def is_sub_agent(self) -> bool:
|
|
@@ -414,6 +417,7 @@ class DisplayStateMachine:
|
|
|
414
417
|
case events.TaskStartEvent() as e:
|
|
415
418
|
s.sub_agent_state = e.sub_agent_state
|
|
416
419
|
s.model_id = e.model_id
|
|
420
|
+
s.task_active = True
|
|
417
421
|
if not s.is_sub_agent:
|
|
418
422
|
self._set_primary_if_needed(e.session_id)
|
|
419
423
|
if not is_replay:
|
|
@@ -428,6 +432,25 @@ class DisplayStateMachine:
|
|
|
428
432
|
cmds.extend(self._spinner_update_commands())
|
|
429
433
|
return cmds
|
|
430
434
|
|
|
435
|
+
case events.CompactionStartEvent():
|
|
436
|
+
if not is_replay:
|
|
437
|
+
self._spinner.set_reasoning_status(STATUS_COMPACTING_TEXT)
|
|
438
|
+
if not s.task_active:
|
|
439
|
+
cmds.append(SpinnerStart())
|
|
440
|
+
cmds.extend(self._spinner_update_commands())
|
|
441
|
+
return cmds
|
|
442
|
+
|
|
443
|
+
case events.CompactionEndEvent() as e:
|
|
444
|
+
if not is_replay:
|
|
445
|
+
self._spinner.set_reasoning_status(None)
|
|
446
|
+
if not s.task_active:
|
|
447
|
+
cmds.append(SpinnerStop())
|
|
448
|
+
cmds.extend(self._spinner_update_commands())
|
|
449
|
+
if e.summary and not e.aborted:
|
|
450
|
+
kept_brief = tuple((item.item_type, item.count, item.preview) for item in e.kept_items_brief)
|
|
451
|
+
cmds.append(RenderCompactionSummary(summary=e.summary, kept_items_brief=kept_brief))
|
|
452
|
+
return cmds
|
|
453
|
+
|
|
431
454
|
case events.DeveloperMessageEvent() as e:
|
|
432
455
|
cmds.append(RenderDeveloperMessage(e))
|
|
433
456
|
return cmds
|
|
@@ -650,6 +673,7 @@ class DisplayStateMachine:
|
|
|
650
673
|
return []
|
|
651
674
|
|
|
652
675
|
case events.TaskFinishEvent() as e:
|
|
676
|
+
s.task_active = False
|
|
653
677
|
cmds.append(RenderTaskFinish(e))
|
|
654
678
|
if not s.is_sub_agent:
|
|
655
679
|
if not is_replay:
|
|
@@ -666,6 +690,7 @@ class DisplayStateMachine:
|
|
|
666
690
|
if not is_replay:
|
|
667
691
|
self._spinner.reset()
|
|
668
692
|
cmds.append(SpinnerStop())
|
|
693
|
+
s.task_active = False
|
|
669
694
|
cmds.append(EndThinkingStream(session_id=e.session_id))
|
|
670
695
|
cmds.append(EndAssistantStream(session_id=e.session_id))
|
|
671
696
|
if not is_replay:
|
klaude_code/tui/renderer.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
|
+
import shutil
|
|
4
5
|
from collections.abc import Callable, Iterator
|
|
5
6
|
from contextlib import contextmanager
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
10
|
+
from rich import box
|
|
9
11
|
from rich.console import Console, Group, RenderableType
|
|
10
12
|
from rich.padding import Padding
|
|
13
|
+
from rich.panel import Panel
|
|
11
14
|
from rich.rule import Rule
|
|
12
15
|
from rich.spinner import Spinner
|
|
13
16
|
from rich.style import Style, StyleType
|
|
@@ -32,6 +35,7 @@ from klaude_code.tui.commands import (
|
|
|
32
35
|
RenderAssistantImage,
|
|
33
36
|
RenderCommand,
|
|
34
37
|
RenderCommandOutput,
|
|
38
|
+
RenderCompactionSummary,
|
|
35
39
|
RenderDeveloperMessage,
|
|
36
40
|
RenderError,
|
|
37
41
|
RenderInterrupt,
|
|
@@ -66,7 +70,7 @@ from klaude_code.tui.components import welcome as c_welcome
|
|
|
66
70
|
from klaude_code.tui.components.common import truncate_head
|
|
67
71
|
from klaude_code.tui.components.rich import status as r_status
|
|
68
72
|
from klaude_code.tui.components.rich.live import CropAboveLive, SingleLine
|
|
69
|
-
from klaude_code.tui.components.rich.markdown import MarkdownStream, ThinkingMarkdown
|
|
73
|
+
from klaude_code.tui.components.rich.markdown import MarkdownStream, NoInsetMarkdown, ThinkingMarkdown
|
|
70
74
|
from klaude_code.tui.components.rich.quote import Quote
|
|
71
75
|
from klaude_code.tui.components.rich.status import BreathingSpinner, ShimmerStatusText
|
|
72
76
|
from klaude_code.tui.components.rich.theme import ThemeKey, get_theme
|
|
@@ -375,6 +379,7 @@ class TUICommandRenderer:
|
|
|
375
379
|
live_sink=self.set_stream_renderable,
|
|
376
380
|
mark=c_assistant.ASSISTANT_MESSAGE_MARK,
|
|
377
381
|
left_margin=MARKDOWN_LEFT_MARGIN,
|
|
382
|
+
image_callback=self.display_image,
|
|
378
383
|
)
|
|
379
384
|
|
|
380
385
|
def _flush_thinking(self) -> None:
|
|
@@ -410,7 +415,7 @@ class TUICommandRenderer:
|
|
|
410
415
|
session_id=e.session_id,
|
|
411
416
|
)
|
|
412
417
|
if image_path is not None:
|
|
413
|
-
self.display_image(str(image_path)
|
|
418
|
+
self.display_image(str(image_path))
|
|
414
419
|
|
|
415
420
|
renderable = c_tools.render_tool_result(e, code_theme=self.themes.code_theme, session_id=e.session_id)
|
|
416
421
|
if renderable is not None:
|
|
@@ -472,7 +477,7 @@ class TUICommandRenderer:
|
|
|
472
477
|
if not self.is_sub_agent_session(event.session_id):
|
|
473
478
|
self.print()
|
|
474
479
|
|
|
475
|
-
def display_image(self, file_path: str
|
|
480
|
+
def display_image(self, file_path: str) -> None:
|
|
476
481
|
# Suspend the Live status bar while emitting raw terminal output.
|
|
477
482
|
had_live = self._bottom_live is not None
|
|
478
483
|
was_spinner_visible = self._spinner_visible
|
|
@@ -485,7 +490,7 @@ class TUICommandRenderer:
|
|
|
485
490
|
self._bottom_live = None
|
|
486
491
|
|
|
487
492
|
try:
|
|
488
|
-
print_kitty_image(file_path,
|
|
493
|
+
print_kitty_image(file_path, file=self.console.file)
|
|
489
494
|
finally:
|
|
490
495
|
if resume_live:
|
|
491
496
|
if was_spinner_visible:
|
|
@@ -524,6 +529,63 @@ class TUICommandRenderer:
|
|
|
524
529
|
else:
|
|
525
530
|
self.print(c_errors.render_error(Text(event.error_message)))
|
|
526
531
|
|
|
532
|
+
def display_compaction_summary(self, summary: str, kept_items_brief: tuple[tuple[str, int, str], ...] = ()) -> None:
|
|
533
|
+
stripped = summary.strip()
|
|
534
|
+
if not stripped:
|
|
535
|
+
return
|
|
536
|
+
stripped = (
|
|
537
|
+
stripped.replace("<summary>", "")
|
|
538
|
+
.replace("</summary>", "")
|
|
539
|
+
.replace("<read_files>", "")
|
|
540
|
+
.replace("</read_files>", "")
|
|
541
|
+
.replace("<modified-files>", "")
|
|
542
|
+
.replace("</modified-files>", "")
|
|
543
|
+
)
|
|
544
|
+
self.console.print(
|
|
545
|
+
Rule(
|
|
546
|
+
Text("Context Compact", style=ThemeKey.COMPACTION_SUMMARY),
|
|
547
|
+
characters="=",
|
|
548
|
+
style=ThemeKey.COMPACTION_SUMMARY,
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
self.print()
|
|
552
|
+
|
|
553
|
+
# Limit panel width to min(100, terminal_width) minus left indent (2)
|
|
554
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
555
|
+
panel_width = min(100, terminal_width) - 2
|
|
556
|
+
|
|
557
|
+
self.console.push_theme(self.themes.markdown_theme)
|
|
558
|
+
panel = Panel(
|
|
559
|
+
NoInsetMarkdown(stripped, code_theme=self.themes.code_theme, style=ThemeKey.COMPACTION_SUMMARY),
|
|
560
|
+
box=box.SIMPLE,
|
|
561
|
+
border_style=ThemeKey.LINES,
|
|
562
|
+
style=ThemeKey.COMPACTION_SUMMARY_PANEL,
|
|
563
|
+
width=panel_width,
|
|
564
|
+
)
|
|
565
|
+
self.print(Padding(panel, (0, 0, 0, MARKDOWN_LEFT_MARGIN)))
|
|
566
|
+
self.console.pop_theme()
|
|
567
|
+
|
|
568
|
+
if kept_items_brief:
|
|
569
|
+
# Collect tool call counts (skip User/Assistant entries)
|
|
570
|
+
tool_counts: dict[str, int] = {}
|
|
571
|
+
for item_type, count, _ in kept_items_brief:
|
|
572
|
+
if item_type not in ("User", "Assistant"):
|
|
573
|
+
tool_counts[item_type] = tool_counts.get(item_type, 0) + count
|
|
574
|
+
|
|
575
|
+
if tool_counts:
|
|
576
|
+
parts: list[str] = []
|
|
577
|
+
for tool_type, tool_count in tool_counts.items():
|
|
578
|
+
if tool_count > 1:
|
|
579
|
+
parts.append(f"{tool_type} x {tool_count}")
|
|
580
|
+
else:
|
|
581
|
+
parts.append(tool_type)
|
|
582
|
+
line = Text()
|
|
583
|
+
line.append("\n Kept uncompacted: ", style=ThemeKey.COMPACTION_SUMMARY)
|
|
584
|
+
line.append(", ".join(parts), style=ThemeKey.COMPACTION_SUMMARY)
|
|
585
|
+
self.print(line)
|
|
586
|
+
|
|
587
|
+
self.print()
|
|
588
|
+
|
|
527
589
|
# ---------------------------------------------------------------------
|
|
528
590
|
# Notifications
|
|
529
591
|
# ---------------------------------------------------------------------
|
|
@@ -617,6 +679,8 @@ class TUICommandRenderer:
|
|
|
617
679
|
self.display_interrupt()
|
|
618
680
|
case RenderError(event=event):
|
|
619
681
|
self.display_error(event)
|
|
682
|
+
case RenderCompactionSummary(summary=summary, kept_items_brief=kept_items_brief):
|
|
683
|
+
self.display_compaction_summary(summary, kept_items_brief)
|
|
620
684
|
case SpinnerStart():
|
|
621
685
|
self.spinner_start()
|
|
622
686
|
case SpinnerStop():
|
klaude_code/tui/runner.py
CHANGED
|
@@ -16,8 +16,9 @@ from klaude_code.app.runtime import (
|
|
|
16
16
|
)
|
|
17
17
|
from klaude_code.config import load_config
|
|
18
18
|
from klaude_code.const import SIGINT_DOUBLE_PRESS_EXIT_TEXT
|
|
19
|
+
from klaude_code.core.compaction import should_compact_threshold
|
|
19
20
|
from klaude_code.core.executor import Executor
|
|
20
|
-
from klaude_code.log import log
|
|
21
|
+
from klaude_code.log import get_current_log_file, log
|
|
21
22
|
from klaude_code.protocol import events, llm_param, op
|
|
22
23
|
from klaude_code.protocol.message import UserInputPayload
|
|
23
24
|
from klaude_code.session.session import Session
|
|
@@ -80,6 +81,19 @@ async def submit_user_input_payload(
|
|
|
80
81
|
for evt in cmd_result.events:
|
|
81
82
|
await executor.context.emit_event(evt)
|
|
82
83
|
|
|
84
|
+
if run_ops and should_compact_threshold(
|
|
85
|
+
session=agent.session,
|
|
86
|
+
config=None,
|
|
87
|
+
llm_config=agent.profile.llm_client.get_llm_config(),
|
|
88
|
+
):
|
|
89
|
+
await executor.submit_and_wait(
|
|
90
|
+
op.CompactSessionOperation(
|
|
91
|
+
session_id=agent.session.id,
|
|
92
|
+
reason="threshold",
|
|
93
|
+
will_retry=False,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
83
97
|
submitted_ids: list[str] = []
|
|
84
98
|
for operation_item in operations:
|
|
85
99
|
submitted_ids.append(await executor.submit(operation_item))
|
|
@@ -124,7 +138,9 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
124
138
|
|
|
125
139
|
def _status_provider() -> REPLStatusSnapshot:
|
|
126
140
|
update_message = get_update_message()
|
|
127
|
-
|
|
141
|
+
debug_log = get_current_log_file()
|
|
142
|
+
debug_log_path = str(debug_log) if debug_log else None
|
|
143
|
+
return build_repl_status_snapshot(update_message, debug_log_path=debug_log_path)
|
|
128
144
|
|
|
129
145
|
def _stop_rich_bottom_ui() -> None:
|
|
130
146
|
active_display = components.display
|