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.
Files changed (82) hide show
  1. klaude_code/app/runtime.py +1 -1
  2. klaude_code/auth/AGENTS.md +325 -0
  3. klaude_code/auth/__init__.py +17 -1
  4. klaude_code/auth/antigravity/__init__.py +20 -0
  5. klaude_code/auth/antigravity/exceptions.py +17 -0
  6. klaude_code/auth/antigravity/oauth.py +320 -0
  7. klaude_code/auth/antigravity/pkce.py +25 -0
  8. klaude_code/auth/antigravity/token_manager.py +45 -0
  9. klaude_code/auth/base.py +4 -0
  10. klaude_code/auth/claude/oauth.py +29 -9
  11. klaude_code/auth/codex/exceptions.py +4 -0
  12. klaude_code/auth/env.py +19 -15
  13. klaude_code/cli/auth_cmd.py +54 -4
  14. klaude_code/cli/cost_cmd.py +83 -160
  15. klaude_code/cli/list_model.py +50 -0
  16. klaude_code/cli/main.py +99 -9
  17. klaude_code/config/assets/builtin_config.yaml +108 -0
  18. klaude_code/config/builtin_config.py +5 -11
  19. klaude_code/config/config.py +24 -10
  20. klaude_code/const.py +11 -1
  21. klaude_code/core/agent.py +5 -1
  22. klaude_code/core/agent_profile.py +28 -32
  23. klaude_code/core/compaction/AGENTS.md +112 -0
  24. klaude_code/core/compaction/__init__.py +11 -0
  25. klaude_code/core/compaction/compaction.py +707 -0
  26. klaude_code/core/compaction/overflow.py +30 -0
  27. klaude_code/core/compaction/prompts.py +97 -0
  28. klaude_code/core/executor.py +103 -2
  29. klaude_code/core/manager/llm_clients.py +5 -0
  30. klaude_code/core/manager/llm_clients_builder.py +14 -2
  31. klaude_code/core/prompts/prompt-antigravity.md +80 -0
  32. klaude_code/core/prompts/prompt-codex-gpt-5-2.md +335 -0
  33. klaude_code/core/reminders.py +11 -7
  34. klaude_code/core/task.py +126 -0
  35. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  36. klaude_code/core/turn.py +3 -1
  37. klaude_code/llm/antigravity/__init__.py +3 -0
  38. klaude_code/llm/antigravity/client.py +558 -0
  39. klaude_code/llm/antigravity/input.py +261 -0
  40. klaude_code/llm/registry.py +1 -0
  41. klaude_code/protocol/commands.py +0 -1
  42. klaude_code/protocol/events.py +18 -0
  43. klaude_code/protocol/llm_param.py +1 -0
  44. klaude_code/protocol/message.py +23 -1
  45. klaude_code/protocol/op.py +15 -1
  46. klaude_code/protocol/op_handler.py +5 -0
  47. klaude_code/session/session.py +36 -0
  48. klaude_code/skill/assets/create-plan/SKILL.md +6 -6
  49. klaude_code/skill/loader.py +12 -13
  50. klaude_code/skill/manager.py +3 -3
  51. klaude_code/tui/command/__init__.py +4 -4
  52. klaude_code/tui/command/compact_cmd.py +32 -0
  53. klaude_code/tui/command/copy_cmd.py +1 -1
  54. klaude_code/tui/command/fork_session_cmd.py +114 -18
  55. klaude_code/tui/command/model_picker.py +5 -1
  56. klaude_code/tui/command/thinking_cmd.py +1 -1
  57. klaude_code/tui/commands.py +6 -0
  58. klaude_code/tui/components/command_output.py +1 -1
  59. klaude_code/tui/components/rich/markdown.py +117 -1
  60. klaude_code/tui/components/rich/theme.py +18 -2
  61. klaude_code/tui/components/tools.py +39 -25
  62. klaude_code/tui/components/user_input.py +39 -28
  63. klaude_code/tui/input/AGENTS.md +44 -0
  64. klaude_code/tui/input/__init__.py +5 -2
  65. klaude_code/tui/input/completers.py +10 -14
  66. klaude_code/tui/input/drag_drop.py +146 -0
  67. klaude_code/tui/input/images.py +227 -0
  68. klaude_code/tui/input/key_bindings.py +183 -19
  69. klaude_code/tui/input/paste.py +71 -0
  70. klaude_code/tui/input/prompt_toolkit.py +32 -9
  71. klaude_code/tui/machine.py +26 -1
  72. klaude_code/tui/renderer.py +67 -4
  73. klaude_code/tui/runner.py +19 -3
  74. klaude_code/tui/terminal/image.py +103 -10
  75. klaude_code/tui/terminal/selector.py +81 -7
  76. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/METADATA +10 -10
  77. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/RECORD +79 -61
  78. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -117
  79. klaude_code/tui/command/terminal_setup_cmd.py +0 -248
  80. klaude_code/tui/input/clipboard.py +0 -152
  81. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/WHEEL +0 -0
  82. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/entry_points.txt +0 -0
@@ -7,8 +7,13 @@ with dependencies injected to avoid circular imports.
7
7
  from __future__ import annotations
8
8
 
9
9
  import contextlib
10
+ import os
10
11
  import re
12
+ import shutil
13
+ import subprocess
14
+ import sys
11
15
  from collections.abc import Callable
16
+ from pathlib import Path
12
17
  from typing import cast
13
18
 
14
19
  from prompt_toolkit.application.current import get_app
@@ -17,11 +22,39 @@ from prompt_toolkit.filters import Always, Condition, Filter
17
22
  from prompt_toolkit.filters.app import has_completions
18
23
  from prompt_toolkit.key_binding import KeyBindings
19
24
  from prompt_toolkit.key_binding.key_processor import KeyPressEvent
25
+ from prompt_toolkit.keys import Keys
26
+
27
+ from klaude_code.tui.input.drag_drop import convert_dropped_text
28
+ from klaude_code.tui.input.paste import expand_paste_markers, store_paste
29
+
30
+
31
+ def copy_to_clipboard(text: str) -> None:
32
+ """Copy text to system clipboard using platform-specific commands."""
33
+ try:
34
+ if sys.platform == "darwin":
35
+ subprocess.run(["pbcopy"], input=text.encode("utf-8"), check=True)
36
+ elif sys.platform == "win32":
37
+ subprocess.run(["clip"], input=text.encode("utf-16"), check=True)
38
+ else:
39
+ # Linux: try xclip first, then xsel
40
+ if shutil.which("xclip"):
41
+ subprocess.run(
42
+ ["xclip", "-selection", "clipboard"],
43
+ input=text.encode("utf-8"),
44
+ check=True,
45
+ )
46
+ elif shutil.which("xsel"):
47
+ subprocess.run(
48
+ ["xsel", "--clipboard", "--input"],
49
+ input=text.encode("utf-8"),
50
+ check=True,
51
+ )
52
+ except (OSError, subprocess.SubprocessError):
53
+ pass
20
54
 
21
55
 
22
56
  def create_key_bindings(
23
57
  capture_clipboard_tag: Callable[[], str | None],
24
- copy_to_clipboard: Callable[[str], None],
25
58
  at_token_pattern: re.Pattern[str],
26
59
  *,
27
60
  input_enabled: Filter | None = None,
@@ -31,8 +64,7 @@ def create_key_bindings(
31
64
  """Create REPL key bindings with injected dependencies.
32
65
 
33
66
  Args:
34
- capture_clipboard_tag: Callable to capture clipboard image and return tag
35
- copy_to_clipboard: Callable to copy text to system clipboard
67
+ capture_clipboard_tag: Callable to capture clipboard image and return [image ...] marker
36
68
  at_token_pattern: Pattern to match @token for completion refresh
37
69
 
38
70
  Returns:
@@ -41,6 +73,51 @@ def create_key_bindings(
41
73
  kb = KeyBindings()
42
74
  enabled = input_enabled if input_enabled is not None else Always()
43
75
 
76
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
77
+ swallow_next_control_j = False
78
+
79
+ def _data_requests_newline(data: str) -> bool:
80
+ """Return True when incoming key data should insert a newline.
81
+
82
+ Different terminals and editor-integrated terminals can emit different
83
+ sequences for Shift+Enter/Alt+Enter. We treat these as "insert newline"
84
+ instead of "submit".
85
+ """
86
+
87
+ if not data:
88
+ return False
89
+
90
+ # Pure LF or LF-prefixed sequences (e.g. when modifiers are encoded).
91
+ if data == "\n" or (ord(data[0]) == 10 and len(data) > 1):
92
+ return True
93
+
94
+ # Known escape sequences observed in some terminals.
95
+ if data in {
96
+ "\x1b\r", # Alt+Enter (ESC + CR)
97
+ "\x1b[13;2~", # Shift+Enter (some terminals)
98
+ "\x1b[27;2;13~", # Shift+Enter (xterm "CSI 27" modified keys)
99
+ "\\\r", # Backslash+Enter sentinel (some editor terminals)
100
+ }:
101
+ return True
102
+
103
+ # Any payload that contains both ESC and CR.
104
+ return len(data) > 1 and "\x1b" in data and "\r" in data
105
+
106
+ def _insert_newline(event: KeyPressEvent, *, strip_trailing_backslash: bool = False) -> None:
107
+ buf = event.current_buffer
108
+ if strip_trailing_backslash:
109
+ try:
110
+ doc = buf.document # type: ignore[reportUnknownMemberType]
111
+ if doc.text_before_cursor.endswith("\\"): # type: ignore[reportUnknownMemberType]
112
+ buf.delete_before_cursor() # type: ignore[reportUnknownMemberType]
113
+ except Exception:
114
+ pass
115
+
116
+ with contextlib.suppress(Exception):
117
+ buf.insert_text("\n") # type: ignore[reportUnknownMemberType]
118
+ with contextlib.suppress(Exception):
119
+ event.app.invalidate() # type: ignore[reportUnknownMemberType]
120
+
44
121
  def _can_move_cursor_visually_within_wrapped_line(delta_visible_y: int) -> bool:
45
122
  """Return True when Up/Down should move within a wrapped visual line.
46
123
 
@@ -233,28 +310,85 @@ def create_key_bindings(
233
310
 
234
311
  @kb.add("c-v", filter=enabled)
235
312
  def _(event: KeyPressEvent) -> None:
236
- """Paste image from clipboard as [Image #N]."""
237
- tag = capture_clipboard_tag()
238
- if tag:
313
+ """Paste image from clipboard as an `[image ...]` marker."""
314
+ marker = capture_clipboard_tag()
315
+ if marker:
239
316
  with contextlib.suppress(Exception):
240
- event.current_buffer.insert_text(tag) # pyright: ignore[reportUnknownMemberType]
317
+ event.current_buffer.insert_text(marker) # pyright: ignore[reportUnknownMemberType]
318
+
319
+ @kb.add(Keys.BracketedPaste, filter=enabled)
320
+ def _(event: KeyPressEvent) -> None:
321
+ """Handle bracketed paste.
322
+
323
+ - Large multi-line pastes are folded into a marker: `[paste #N ...]`.
324
+ - Otherwise, try to convert dropped file URLs/paths into @ tokens or `[image ...]` markers.
325
+ """
326
+
327
+ data = getattr(event, "data", "")
328
+ if not isinstance(data, str) or not data:
329
+ return
330
+
331
+ pasted_lines = data.splitlines()
332
+ line_count = max(1, len(pasted_lines))
333
+ total_chars = len(data)
334
+
335
+ should_fold = line_count > 10 or total_chars > 1000
336
+ if should_fold:
337
+ marker = store_paste(data)
338
+ if marker and not marker.endswith((" ", "\t", "\n")):
339
+ marker += " "
340
+ with contextlib.suppress(Exception):
341
+ event.current_buffer.insert_text(marker) # pyright: ignore[reportUnknownMemberType]
342
+ return
343
+
344
+ converted = convert_dropped_text(data, cwd=Path.cwd())
345
+ if converted != data and converted and not converted.endswith((" ", "\t", "\n")):
346
+ converted += " "
347
+
348
+ buf = event.current_buffer
349
+ try:
350
+ if buf.selection_state: # type: ignore[reportUnknownMemberType]
351
+ buf.cut_selection() # type: ignore[reportUnknownMemberType]
352
+ except Exception:
353
+ pass
354
+
355
+ with contextlib.suppress(Exception):
356
+ buf.insert_text(converted) # type: ignore[reportUnknownMemberType]
357
+
358
+ @kb.add("escape", "enter", filter=enabled)
359
+ def _(event: KeyPressEvent) -> None:
360
+ """Alt+Enter inserts a newline."""
361
+
362
+ _insert_newline(event)
363
+
364
+ @kb.add("escape", "[", "1", "3", ";", "2", "~", filter=enabled)
365
+ def _(event: KeyPressEvent) -> None:
366
+ """Shift+Enter sequence used by some terminals inserts a newline."""
367
+
368
+ _insert_newline(event)
241
369
 
242
370
  @kb.add("enter", filter=enabled)
243
371
  def _(event: KeyPressEvent) -> None:
372
+ nonlocal swallow_next_control_j
373
+
244
374
  buf = event.current_buffer
245
375
  doc = buf.document # type: ignore
246
376
 
247
- # If VS Code/Windsurf/Cursor sent a "\\" sentinel before Enter (Shift+Enter mapping),
248
- # treat it as a request for a newline instead of submit.
249
- # This allows Shift+Enter to insert a newline in our multiline prompt.
250
- try:
251
- if doc.text_before_cursor.endswith("\\"): # type: ignore[reportUnknownMemberType]
252
- buf.delete_before_cursor() # remove the sentinel backslash # type: ignore[reportUnknownMemberType]
253
- buf.insert_text("\n") # type: ignore[reportUnknownMemberType]
254
- return
255
- except (AttributeError, TypeError):
256
- # Fall through to default behavior if anything goes wrong
257
- pass
377
+ data = getattr(event, "data", "")
378
+ if isinstance(data, str) and _data_requests_newline(data):
379
+ _insert_newline(event)
380
+ return
381
+
382
+ # VS Code-family terminals often implement Shift+Enter via a "\\" sentinel
383
+ # before Enter. Only enable this heuristic under TERM_PROGRAM=vscode.
384
+ if term_program == "vscode":
385
+ try:
386
+ if doc.text_before_cursor.endswith("\\"): # type: ignore[reportUnknownMemberType]
387
+ swallow_next_control_j = True
388
+ _insert_newline(event, strip_trailing_backslash=True)
389
+ return
390
+ except (AttributeError, TypeError):
391
+ pass
258
392
 
259
393
  # When completions are visible, Enter accepts the current selection.
260
394
  # This aligns with common TUI completion UX: navigation doesn't modify
@@ -262,6 +396,21 @@ def create_key_bindings(
262
396
  if not _should_submit_instead_of_accepting_completion(buf) and _accept_current_completion(buf):
263
397
  return
264
398
 
399
+ # Before submitting, expand any folded paste markers so that:
400
+ # - the actual request contains the full pasted content
401
+ # - prompt_toolkit history stores the expanded content
402
+ # Also convert any remaining file:// drops that bypassed bracketed paste.
403
+ try:
404
+ current_text = buf.text # type: ignore[reportUnknownMemberType]
405
+ except Exception:
406
+ current_text = ""
407
+ prepared = expand_paste_markers(current_text)
408
+ prepared = convert_dropped_text(prepared, cwd=Path.cwd())
409
+ if prepared != current_text:
410
+ with contextlib.suppress(Exception):
411
+ buf.text = prepared # type: ignore[reportUnknownMemberType]
412
+ buf.cursor_position = len(prepared) # type: ignore[reportUnknownMemberType]
413
+
265
414
  # If the entire buffer is whitespace-only, insert a newline rather than submitting.
266
415
  if len(buf.text.strip()) == 0: # type: ignore
267
416
  buf.insert_text("\n") # type: ignore
@@ -310,6 +459,11 @@ def create_key_bindings(
310
459
 
311
460
  @kb.add("c-j", filter=enabled)
312
461
  def _(event: KeyPressEvent) -> None:
462
+ nonlocal swallow_next_control_j
463
+ if swallow_next_control_j:
464
+ swallow_next_control_j = False
465
+ return
466
+
313
467
  event.current_buffer.insert_text("\n") # type: ignore
314
468
 
315
469
  @kb.add("c", filter=enabled)
@@ -322,7 +476,7 @@ def create_key_bindings(
322
476
  selected_text: str = doc.text[start:end] # type: ignore[reportUnknownMemberType]
323
477
 
324
478
  if selected_text:
325
- copy_to_clipboard(selected_text) # type: ignore[reportUnknownArgumentType]
479
+ copy_to_clipboard(selected_text)
326
480
  buf.exit_selection() # type: ignore[reportUnknownMemberType]
327
481
  else:
328
482
  buf.insert_text("c") # type: ignore[reportUnknownMemberType]
@@ -424,4 +578,14 @@ def create_key_bindings(
424
578
  with contextlib.suppress(Exception):
425
579
  open_thinking_picker()
426
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
+
427
591
  return kb
@@ -0,0 +1,71 @@
1
+ """Fold large multi-line pastes into a short marker.
2
+
3
+ prompt_toolkit already parses terminal bracketed paste mode and exposes the
4
+ pasted payload via a `<bracketed-paste>` key event.
5
+
6
+ We keep the editor buffer small by inserting a marker like:
7
+ - `[paste #3 +42 lines]` (when many lines)
8
+ - `[paste #3 1205 chars]` (when very long)
9
+
10
+ On submit, markers are expanded back to the original pasted content.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+
17
+ _PASTE_MARKER_RE = re.compile(r"\[paste #(?P<id>\d+)(?: (?P<meta>\+\d+ lines|\d+ chars))?\]")
18
+
19
+
20
+ class PasteBufferState:
21
+ def __init__(self) -> None:
22
+ self._next_id = 1
23
+ self._pastes: dict[int, str] = {}
24
+
25
+ def store(self, text: str) -> str:
26
+ paste_id = self._next_id
27
+ self._next_id += 1
28
+
29
+ lines = text.splitlines()
30
+ line_count = max(1, len(lines))
31
+ total_chars = len(text)
32
+
33
+ if line_count > 10:
34
+ marker = f"[paste #{paste_id} +{line_count} lines]"
35
+ else:
36
+ marker = f"[paste #{paste_id} {total_chars} chars]"
37
+
38
+ self._pastes[paste_id] = text
39
+ return marker
40
+
41
+ def expand_markers(self, text: str) -> str:
42
+ used: set[int] = set()
43
+
44
+ def _replace(m: re.Match[str]) -> str:
45
+ try:
46
+ paste_id = int(m.group("id"))
47
+ except (TypeError, ValueError):
48
+ return m.group(0)
49
+
50
+ content = self._pastes.get(paste_id)
51
+ if content is None:
52
+ return m.group(0)
53
+
54
+ used.add(paste_id)
55
+ return content
56
+
57
+ out = _PASTE_MARKER_RE.sub(_replace, text)
58
+ for pid in used:
59
+ self._pastes.pop(pid, None)
60
+ return out
61
+
62
+
63
+ paste_state = PasteBufferState()
64
+
65
+
66
+ def store_paste(text: str) -> str:
67
+ return paste_state.store(text)
68
+
69
+
70
+ def expand_paste_markers(text: str) -> str:
71
+ return paste_state.expand_markers(text)
@@ -36,9 +36,14 @@ from klaude_code.protocol import llm_param
36
36
  from klaude_code.protocol.commands import CommandInfo
37
37
  from klaude_code.protocol.message import UserInputPayload
38
38
  from klaude_code.tui.components.user_input import USER_MESSAGE_MARK
39
- from klaude_code.tui.input.clipboard import capture_clipboard_tag, copy_to_clipboard, extract_images_from_text
40
39
  from klaude_code.tui.input.completers import AT_TOKEN_PATTERN, create_repl_completer
40
+ from klaude_code.tui.input.drag_drop import convert_dropped_text
41
+ from klaude_code.tui.input.images import (
42
+ capture_clipboard_tag,
43
+ extract_images_from_text,
44
+ )
41
45
  from klaude_code.tui.input.key_bindings import create_key_bindings
46
+ from klaude_code.tui.input.paste import expand_paste_markers
42
47
  from klaude_code.tui.terminal.color import is_light_terminal_background
43
48
  from klaude_code.tui.terminal.selector import SelectItem, SelectOverlay, build_model_select_items
44
49
  from klaude_code.ui.core.input import InputProviderABC
@@ -48,6 +53,7 @@ class REPLStatusSnapshot(NamedTuple):
48
53
  """Snapshot of REPL status for bottom toolbar display."""
49
54
 
50
55
  update_message: str | None = None
56
+ debug_log_path: str | None = None
51
57
 
52
58
 
53
59
  COMPLETION_SELECTED_DARK_BG = "ansigreen"
@@ -271,7 +277,6 @@ class PromptToolkitInput(InputProviderABC):
271
277
 
272
278
  kb = create_key_bindings(
273
279
  capture_clipboard_tag=capture_clipboard_tag,
274
- copy_to_clipboard=copy_to_clipboard,
275
280
  at_token_pattern=AT_TOKEN_PATTERN,
276
281
  input_enabled=input_enabled,
277
282
  open_model_picker=self._open_model_picker,
@@ -333,7 +338,7 @@ class PromptToolkitInput(InputProviderABC):
333
338
  pointer="→",
334
339
  use_search_filter=True,
335
340
  search_placeholder="type to search",
336
- list_height=10,
341
+ list_height=20,
337
342
  on_select=self._handle_model_selected,
338
343
  )
339
344
  self._model_picker = model_picker
@@ -430,7 +435,7 @@ class PromptToolkitInput(InputProviderABC):
430
435
  original_height_int = original_height_value if isinstance(original_height_value, int) else None
431
436
 
432
437
  if picker_open or completion_open:
433
- target_rows = 20 if picker_open else 14
438
+ target_rows = 24 if picker_open else 14
434
439
 
435
440
  # Cap to the current terminal size.
436
441
  # Leave a small buffer to avoid triggering "Window too small".
@@ -527,7 +532,7 @@ class PromptToolkitInput(InputProviderABC):
527
532
  return [], None
528
533
 
529
534
  items: list[SelectItem[str]] = [
530
- 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)
531
536
  for opt in data.options
532
537
  ]
533
538
  return items, data.current_value
@@ -569,24 +574,36 @@ class PromptToolkitInput(InputProviderABC):
569
574
  doing any blocking IO here.
570
575
  """
571
576
  update_message: str | None = None
577
+ debug_log_path: str | None = None
572
578
  if self._status_provider is not None:
573
579
  try:
574
580
  status = self._status_provider()
575
581
  update_message = status.update_message
582
+ debug_log_path = status.debug_log_path
576
583
  except (AttributeError, RuntimeError):
577
- 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"
578
595
 
579
596
  # If nothing to show, return a blank line to actively clear any previously
580
597
  # rendered content. (When `bottom_toolbar` is a callable, prompt_toolkit
581
598
  # will still reserve the toolbar line.)
582
- if not update_message:
599
+ if not display_text:
583
600
  try:
584
601
  terminal_width = shutil.get_terminal_size().columns
585
602
  except (OSError, ValueError):
586
603
  terminal_width = 0
587
604
  return FormattedText([("", " " * max(0, terminal_width))])
588
605
 
589
- left_text = " " + update_message
606
+ left_text = " " + display_text
590
607
  try:
591
608
  terminal_width = shutil.get_terminal_size().columns
592
609
  padding = " " * max(0, terminal_width - len(left_text))
@@ -594,7 +611,7 @@ class PromptToolkitInput(InputProviderABC):
594
611
  padding = ""
595
612
 
596
613
  toolbar_text = left_text + padding
597
- return FormattedText([("#ansiyellow", toolbar_text)])
614
+ return FormattedText([(text_style, toolbar_text)])
598
615
 
599
616
  # -------------------------------------------------------------------------
600
617
  # Placeholder
@@ -669,6 +686,12 @@ class PromptToolkitInput(InputProviderABC):
669
686
  with contextlib.suppress(Exception):
670
687
  self._post_prompt()
671
688
 
689
+ # Expand folded paste markers back into the original content.
690
+ line = expand_paste_markers(line)
691
+
692
+ # Convert drag-and-drop file:// URIs that may have bypassed bracketed paste.
693
+ line = convert_dropped_text(line, cwd=Path.cwd())
694
+
672
695
  # Extract images referenced in the input text
673
696
  images = extract_images_from_text(line)
674
697
 
@@ -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,
@@ -160,7 +162,7 @@ class ActivityState:
160
162
  if self._sub_agent_tool_calls:
161
163
  _append_counts(self._sub_agent_tool_calls)
162
164
  if self._tool_calls:
163
- activity_text.append(" , ")
165
+ activity_text.append(", ")
164
166
 
165
167
  if self._tool_calls:
166
168
  _append_counts(self._tool_calls)
@@ -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
@@ -410,7 +414,7 @@ class TUICommandRenderer:
410
414
  session_id=e.session_id,
411
415
  )
412
416
  if image_path is not None:
413
- self.display_image(str(image_path), height=None)
417
+ self.display_image(str(image_path))
414
418
 
415
419
  renderable = c_tools.render_tool_result(e, code_theme=self.themes.code_theme, session_id=e.session_id)
416
420
  if renderable is not None:
@@ -472,7 +476,7 @@ class TUICommandRenderer:
472
476
  if not self.is_sub_agent_session(event.session_id):
473
477
  self.print()
474
478
 
475
- def display_image(self, file_path: str, height: int | None = 40) -> None:
479
+ def display_image(self, file_path: str) -> None:
476
480
  # Suspend the Live status bar while emitting raw terminal output.
477
481
  had_live = self._bottom_live is not None
478
482
  was_spinner_visible = self._spinner_visible
@@ -485,7 +489,7 @@ class TUICommandRenderer:
485
489
  self._bottom_live = None
486
490
 
487
491
  try:
488
- print_kitty_image(file_path, height=height, file=self.console.file)
492
+ print_kitty_image(file_path, file=self.console.file)
489
493
  finally:
490
494
  if resume_live:
491
495
  if was_spinner_visible:
@@ -524,6 +528,63 @@ class TUICommandRenderer:
524
528
  else:
525
529
  self.print(c_errors.render_error(Text(event.error_message)))
526
530
 
531
+ def display_compaction_summary(self, summary: str, kept_items_brief: tuple[tuple[str, int, str], ...] = ()) -> None:
532
+ stripped = summary.strip()
533
+ if not stripped:
534
+ return
535
+ stripped = (
536
+ stripped.replace("<summary>", "")
537
+ .replace("</summary>", "")
538
+ .replace("<read_files>", "")
539
+ .replace("</read_files>", "")
540
+ .replace("<modified-files>", "")
541
+ .replace("</modified-files>", "")
542
+ )
543
+ self.console.print(
544
+ Rule(
545
+ Text("Context Compact", style=ThemeKey.COMPACTION_SUMMARY),
546
+ characters="=",
547
+ style=ThemeKey.COMPACTION_SUMMARY,
548
+ )
549
+ )
550
+ self.print()
551
+
552
+ # Limit panel width to min(100, terminal_width) minus left indent (2)
553
+ terminal_width = shutil.get_terminal_size().columns
554
+ panel_width = min(100, terminal_width) - 2
555
+
556
+ self.console.push_theme(self.themes.markdown_theme)
557
+ panel = Panel(
558
+ NoInsetMarkdown(stripped, code_theme=self.themes.code_theme, style=ThemeKey.COMPACTION_SUMMARY),
559
+ box=box.SIMPLE,
560
+ border_style=ThemeKey.LINES,
561
+ style=ThemeKey.COMPACTION_SUMMARY_PANEL,
562
+ width=panel_width,
563
+ )
564
+ self.print(Padding(panel, (0, 0, 0, MARKDOWN_LEFT_MARGIN)))
565
+ self.console.pop_theme()
566
+
567
+ if kept_items_brief:
568
+ # Collect tool call counts (skip User/Assistant entries)
569
+ tool_counts: dict[str, int] = {}
570
+ for item_type, count, _ in kept_items_brief:
571
+ if item_type not in ("User", "Assistant"):
572
+ tool_counts[item_type] = tool_counts.get(item_type, 0) + count
573
+
574
+ if tool_counts:
575
+ parts: list[str] = []
576
+ for tool_type, tool_count in tool_counts.items():
577
+ if tool_count > 1:
578
+ parts.append(f"{tool_type} x {tool_count}")
579
+ else:
580
+ parts.append(tool_type)
581
+ line = Text()
582
+ line.append("\n Kept uncompacted: ", style=ThemeKey.COMPACTION_SUMMARY)
583
+ line.append(", ".join(parts), style=ThemeKey.COMPACTION_SUMMARY)
584
+ self.print(line)
585
+
586
+ self.print()
587
+
527
588
  # ---------------------------------------------------------------------
528
589
  # Notifications
529
590
  # ---------------------------------------------------------------------
@@ -617,6 +678,8 @@ class TUICommandRenderer:
617
678
  self.display_interrupt()
618
679
  case RenderError(event=event):
619
680
  self.display_error(event)
681
+ case RenderCompactionSummary(summary=summary, kept_items_brief=kept_items_brief):
682
+ self.display_compaction_summary(summary, kept_items_brief)
620
683
  case SpinnerStart():
621
684
  self.spinner_start()
622
685
  case SpinnerStop():