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.
Files changed (74) hide show
  1. klaude_code/auth/AGENTS.md +325 -0
  2. klaude_code/auth/__init__.py +17 -1
  3. klaude_code/auth/antigravity/__init__.py +20 -0
  4. klaude_code/auth/antigravity/exceptions.py +17 -0
  5. klaude_code/auth/antigravity/oauth.py +320 -0
  6. klaude_code/auth/antigravity/pkce.py +25 -0
  7. klaude_code/auth/antigravity/token_manager.py +45 -0
  8. klaude_code/auth/base.py +4 -0
  9. klaude_code/auth/claude/oauth.py +29 -9
  10. klaude_code/auth/codex/exceptions.py +4 -0
  11. klaude_code/cli/auth_cmd.py +53 -3
  12. klaude_code/cli/cost_cmd.py +83 -160
  13. klaude_code/cli/list_model.py +50 -0
  14. klaude_code/cli/main.py +2 -2
  15. klaude_code/config/assets/builtin_config.yaml +108 -0
  16. klaude_code/config/builtin_config.py +5 -11
  17. klaude_code/config/config.py +24 -10
  18. klaude_code/const.py +2 -1
  19. klaude_code/core/agent.py +5 -1
  20. klaude_code/core/agent_profile.py +29 -33
  21. klaude_code/core/compaction/AGENTS.md +112 -0
  22. klaude_code/core/compaction/__init__.py +11 -0
  23. klaude_code/core/compaction/compaction.py +705 -0
  24. klaude_code/core/compaction/overflow.py +30 -0
  25. klaude_code/core/compaction/prompts.py +97 -0
  26. klaude_code/core/executor.py +121 -2
  27. klaude_code/core/manager/llm_clients.py +5 -0
  28. klaude_code/core/manager/llm_clients_builder.py +14 -2
  29. klaude_code/core/prompts/prompt-antigravity.md +80 -0
  30. klaude_code/core/prompts/prompt-codex-gpt-5-2.md +335 -0
  31. klaude_code/core/reminders.py +7 -2
  32. klaude_code/core/task.py +126 -0
  33. klaude_code/core/tool/file/edit_tool.py +1 -2
  34. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  35. klaude_code/core/turn.py +3 -1
  36. klaude_code/llm/antigravity/__init__.py +3 -0
  37. klaude_code/llm/antigravity/client.py +558 -0
  38. klaude_code/llm/antigravity/input.py +261 -0
  39. klaude_code/llm/registry.py +1 -0
  40. klaude_code/protocol/commands.py +1 -0
  41. klaude_code/protocol/events.py +18 -0
  42. klaude_code/protocol/llm_param.py +1 -0
  43. klaude_code/protocol/message.py +23 -1
  44. klaude_code/protocol/op.py +29 -1
  45. klaude_code/protocol/op_handler.py +10 -0
  46. klaude_code/session/export.py +308 -299
  47. klaude_code/session/session.py +36 -0
  48. klaude_code/session/templates/export_session.html +430 -134
  49. klaude_code/skill/assets/create-plan/SKILL.md +6 -6
  50. klaude_code/tui/command/__init__.py +6 -0
  51. klaude_code/tui/command/compact_cmd.py +32 -0
  52. klaude_code/tui/command/continue_cmd.py +34 -0
  53. klaude_code/tui/command/fork_session_cmd.py +110 -14
  54. klaude_code/tui/command/model_picker.py +5 -1
  55. klaude_code/tui/command/thinking_cmd.py +1 -1
  56. klaude_code/tui/commands.py +6 -0
  57. klaude_code/tui/components/rich/markdown.py +119 -12
  58. klaude_code/tui/components/rich/theme.py +10 -2
  59. klaude_code/tui/components/tools.py +39 -25
  60. klaude_code/tui/components/user_input.py +1 -1
  61. klaude_code/tui/input/__init__.py +5 -2
  62. klaude_code/tui/input/drag_drop.py +6 -57
  63. klaude_code/tui/input/key_bindings.py +10 -0
  64. klaude_code/tui/input/prompt_toolkit.py +19 -6
  65. klaude_code/tui/machine.py +25 -0
  66. klaude_code/tui/renderer.py +68 -4
  67. klaude_code/tui/runner.py +18 -2
  68. klaude_code/tui/terminal/image.py +72 -10
  69. klaude_code/tui/terminal/selector.py +31 -7
  70. {klaude_code-2.7.0.dist-info → klaude_code-2.8.1.dist-info}/METADATA +1 -1
  71. {klaude_code-2.7.0.dist-info → klaude_code-2.8.1.dist-info}/RECORD +73 -56
  72. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -117
  73. {klaude_code-2.7.0.dist-info → klaude_code-2.8.1.dist-info}/WHEEL +0 -0
  74. {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
- # Markdown files show path in result panel, skip here to avoid duplication
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
- update_count = 0
296
- add_count = 0
297
- delete_count = 0
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
- update_count += 1
296
+ update_files.append(line[len("*** Update File:") :].strip())
301
297
  elif line.startswith("*** Add File:"):
302
- add_count += 1
298
+ add_files.append(line[len("*** Add File:") :].strip())
303
299
  elif line.startswith("*** Delete File:"):
304
- delete_count += 1
300
+ delete_files.append(line[len("*** Delete File:") :].strip())
305
301
 
306
302
  parts: list[str] = []
307
- if update_count > 0:
308
- parts.append(f"Update File × {update_count}" if update_count > 1 else "Update File")
309
- if add_count > 0:
310
- parts.append(f"Add File × {add_count}" if add_count > 1 else "Add File")
311
- if delete_count > 0:
312
- parts.append(f"Delete File × {delete_count}" if delete_count > 1 else "Delete File")
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
- header = render_path(md_ui.file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
598
- return Panel.fit(
599
- Group(header, Text(""), NoInsetMarkdown(md_ui.content, code_theme=code_theme)),
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 wrap(Group(*rendered)) if rendered else None
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
- return wrap(render_markdown_doc(md_ui, code_theme=code_theme))
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
- return wrap(render_markdown_doc(md_ui, code_theme=code_theme))
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()
@@ -104,4 +104,4 @@ def render_user_input(content: str) -> RenderableType:
104
104
 
105
105
 
106
106
  def render_interrupt() -> RenderableType:
107
- return Text(" INTERRUPTED \n", style=ThemeKey.INTERRUPT)
107
+ return Text(" Interrupted by user\n", style=ThemeKey.INTERRUPT)
@@ -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(update_message: str | None) -> REPLStatusSnapshot:
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 text into @ tokens and/or image markers."""
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: list[str] = []
182
- for tok in tokens:
183
- p = Path(tok).expanduser()
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
- # Preserve trailing newline if present (common for bracketed paste payloads).
196
- suffix = "\n" if text.endswith("\n") else ""
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 = 20 if picker_open else 14
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:text", opt.label + "\n")], value=opt.value, search_text=opt.label)
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
- update_message = None
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 update_message:
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 = " " + update_message
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([("#ansiyellow", toolbar_text)])
614
+ return FormattedText([(text_style, toolbar_text)])
602
615
 
603
616
  # -------------------------------------------------------------------------
604
617
  # Placeholder
@@ -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:
@@ -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), height=None)
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, height: int | None = 40) -> None:
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, height=height, file=self.console.file)
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
- return build_repl_status_snapshot(update_message)
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