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
|
@@ -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
|
|
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 [
|
|
237
|
-
|
|
238
|
-
if
|
|
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(
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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)
|
|
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=
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 = " " +
|
|
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([(
|
|
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
|
|
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,
|
|
@@ -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:
|
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
|
|
@@ -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)
|
|
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
|
|
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,
|
|
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():
|