klaude-code 2.6.0__py3-none-any.whl → 2.7.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/env.py +19 -15
- klaude_code/cli/auth_cmd.py +1 -1
- klaude_code/cli/main.py +98 -8
- klaude_code/const.py +10 -1
- klaude_code/core/reminders.py +4 -5
- klaude_code/core/turn.py +1 -1
- klaude_code/protocol/commands.py +0 -1
- klaude_code/skill/loader.py +12 -13
- klaude_code/skill/manager.py +3 -3
- klaude_code/tui/command/__init__.py +1 -4
- klaude_code/tui/command/copy_cmd.py +1 -1
- klaude_code/tui/command/fork_session_cmd.py +4 -4
- klaude_code/tui/components/command_output.py +1 -1
- klaude_code/tui/components/rich/markdown.py +60 -0
- klaude_code/tui/components/rich/theme.py +8 -0
- klaude_code/tui/components/user_input.py +38 -27
- klaude_code/tui/input/AGENTS.md +44 -0
- klaude_code/tui/input/completers.py +10 -14
- klaude_code/tui/input/drag_drop.py +197 -0
- klaude_code/tui/input/images.py +227 -0
- klaude_code/tui/input/key_bindings.py +173 -19
- klaude_code/tui/input/paste.py +71 -0
- klaude_code/tui/input/prompt_toolkit.py +13 -3
- klaude_code/tui/machine.py +1 -1
- klaude_code/tui/runner.py +1 -1
- klaude_code/tui/terminal/image.py +40 -9
- klaude_code/tui/terminal/selector.py +52 -2
- {klaude_code-2.6.0.dist-info → klaude_code-2.7.0.dist-info}/METADATA +10 -10
- {klaude_code-2.6.0.dist-info → klaude_code-2.7.0.dist-info}/RECORD +32 -30
- 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.7.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.6.0.dist-info → klaude_code-2.7.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:
|
|
316
|
+
with contextlib.suppress(Exception):
|
|
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 += " "
|
|
239
340
|
with contextlib.suppress(Exception):
|
|
240
|
-
event.current_buffer.insert_text(
|
|
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]
|
|
@@ -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
|
|
@@ -271,7 +276,6 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
271
276
|
|
|
272
277
|
kb = create_key_bindings(
|
|
273
278
|
capture_clipboard_tag=capture_clipboard_tag,
|
|
274
|
-
copy_to_clipboard=copy_to_clipboard,
|
|
275
279
|
at_token_pattern=AT_TOKEN_PATTERN,
|
|
276
280
|
input_enabled=input_enabled,
|
|
277
281
|
open_model_picker=self._open_model_picker,
|
|
@@ -333,7 +337,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
333
337
|
pointer="→",
|
|
334
338
|
use_search_filter=True,
|
|
335
339
|
search_placeholder="type to search",
|
|
336
|
-
list_height=
|
|
340
|
+
list_height=20,
|
|
337
341
|
on_select=self._handle_model_selected,
|
|
338
342
|
)
|
|
339
343
|
self._model_picker = model_picker
|
|
@@ -669,6 +673,12 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
669
673
|
with contextlib.suppress(Exception):
|
|
670
674
|
self._post_prompt()
|
|
671
675
|
|
|
676
|
+
# Expand folded paste markers back into the original content.
|
|
677
|
+
line = expand_paste_markers(line)
|
|
678
|
+
|
|
679
|
+
# Convert drag-and-drop file:// URIs that may have bypassed bracketed paste.
|
|
680
|
+
line = convert_dropped_text(line, cwd=Path.cwd())
|
|
681
|
+
|
|
672
682
|
# Extract images referenced in the input text
|
|
673
683
|
images = extract_images_from_text(line)
|
|
674
684
|
|
klaude_code/tui/machine.py
CHANGED
|
@@ -160,7 +160,7 @@ class ActivityState:
|
|
|
160
160
|
if self._sub_agent_tool_calls:
|
|
161
161
|
_append_counts(self._sub_agent_tool_calls)
|
|
162
162
|
if self._tool_calls:
|
|
163
|
-
activity_text.append("
|
|
163
|
+
activity_text.append(", ")
|
|
164
164
|
|
|
165
165
|
if self._tool_calls:
|
|
166
166
|
_append_counts(self._tool_calls)
|
klaude_code/tui/runner.py
CHANGED
|
@@ -312,4 +312,4 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
312
312
|
active_session_id = components.executor.context.current_session_id()
|
|
313
313
|
if active_session_id and Session.exists(active_session_id):
|
|
314
314
|
log(f"Session ID: {active_session_id}")
|
|
315
|
-
log(f"Resume with: klaude --resume
|
|
315
|
+
log(f"Resume with: klaude --resume {active_session_id}")
|
|
@@ -1,34 +1,65 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import IO
|
|
6
7
|
|
|
8
|
+
# Kitty graphics protocol chunk size (4096 is the recommended max)
|
|
9
|
+
_CHUNK_SIZE = 4096
|
|
10
|
+
|
|
7
11
|
|
|
8
12
|
def print_kitty_image(file_path: str | Path, *, height: int | None = None, file: IO[str] | None = None) -> None:
|
|
9
13
|
"""Print an image to the terminal using Kitty graphics protocol.
|
|
10
14
|
|
|
11
15
|
This intentionally bypasses Rich rendering to avoid interleaving Live refreshes
|
|
12
16
|
with raw escape sequences.
|
|
13
|
-
"""
|
|
14
17
|
|
|
18
|
+
Args:
|
|
19
|
+
file_path: Path to the image file (PNG recommended).
|
|
20
|
+
height: Display height in terminal rows. If None, uses terminal default.
|
|
21
|
+
file: Output file stream. Defaults to stdout.
|
|
22
|
+
"""
|
|
15
23
|
path = Path(file_path) if isinstance(file_path, str) else file_path
|
|
16
24
|
if not path.exists():
|
|
17
25
|
print(f"Image not found: {path}", file=file or sys.stdout, flush=True)
|
|
18
26
|
return
|
|
19
27
|
|
|
20
28
|
try:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
KittyImage.forced_support = True # type: ignore[reportUnknownMemberType]
|
|
24
|
-
img = KittyImage.from_file(path) # type: ignore[reportUnknownMemberType]
|
|
25
|
-
if height is not None:
|
|
26
|
-
img.height = height # type: ignore[reportUnknownMemberType]
|
|
27
|
-
|
|
29
|
+
data = path.read_bytes()
|
|
30
|
+
encoded = base64.standard_b64encode(data).decode("ascii")
|
|
28
31
|
out = file or sys.stdout
|
|
32
|
+
|
|
29
33
|
print("", file=out)
|
|
30
|
-
|
|
34
|
+
_write_kitty_graphics(out, encoded, height=height)
|
|
31
35
|
print("", file=out)
|
|
32
36
|
out.flush()
|
|
33
37
|
except Exception:
|
|
34
38
|
print(f"Saved image: {path}", file=file or sys.stdout, flush=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _write_kitty_graphics(out: IO[str], encoded_data: str, *, height: int | None = None) -> None:
|
|
42
|
+
"""Write Kitty graphics protocol escape sequences.
|
|
43
|
+
|
|
44
|
+
Protocol format: ESC _ G <control>;<payload> ESC \\
|
|
45
|
+
- a=T: direct transmission (data in payload)
|
|
46
|
+
- f=100: PNG format (auto-detected by Kitty)
|
|
47
|
+
- r=N: display height in rows
|
|
48
|
+
- m=1: more data follows, m=0: last chunk
|
|
49
|
+
"""
|
|
50
|
+
total_len = len(encoded_data)
|
|
51
|
+
|
|
52
|
+
for i in range(0, total_len, _CHUNK_SIZE):
|
|
53
|
+
chunk = encoded_data[i : i + _CHUNK_SIZE]
|
|
54
|
+
is_last = i + _CHUNK_SIZE >= total_len
|
|
55
|
+
|
|
56
|
+
if i == 0:
|
|
57
|
+
# First chunk: include control parameters
|
|
58
|
+
ctrl = "a=T,f=100"
|
|
59
|
+
if height is not None:
|
|
60
|
+
ctrl += f",r={height}"
|
|
61
|
+
ctrl += f",m={0 if is_last else 1}"
|
|
62
|
+
out.write(f"\033_G{ctrl};{chunk}\033\\")
|
|
63
|
+
else:
|
|
64
|
+
# Subsequent chunks: only m parameter needed
|
|
65
|
+
out.write(f"\033_Gm={0 if is_last else 1};{chunk}\033\\")
|
|
@@ -389,7 +389,7 @@ def _build_search_container(
|
|
|
389
389
|
frame: bool = True,
|
|
390
390
|
) -> tuple[Window, Container]:
|
|
391
391
|
"""Build the search input container with placeholder."""
|
|
392
|
-
placeholder_text = f"{search_placeholder} · ↑↓ to select · esc to quit"
|
|
392
|
+
placeholder_text = f"{search_placeholder} · ↑↓ to select · enter/tab to confirm · esc to quit"
|
|
393
393
|
|
|
394
394
|
search_prefix_window = Window(
|
|
395
395
|
FormattedTextControl([("class:search_prefix", "/ ")]),
|
|
@@ -522,6 +522,23 @@ def select_one[T](
|
|
|
522
522
|
return
|
|
523
523
|
event.app.exit(result=value)
|
|
524
524
|
|
|
525
|
+
@kb.add(Keys.Tab, eager=True)
|
|
526
|
+
def _(event: KeyPressEvent) -> None:
|
|
527
|
+
"""Accept the currently pointed item."""
|
|
528
|
+
indices, _ = _filter_items(items, get_filter_text())
|
|
529
|
+
if not indices:
|
|
530
|
+
event.app.exit(result=None)
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
nonlocal pointed_at
|
|
534
|
+
pointed_at = _coerce_pointed_at_to_selectable(items, indices, pointed_at)
|
|
535
|
+
idx = indices[pointed_at % len(indices)]
|
|
536
|
+
value = items[idx].value
|
|
537
|
+
if value is None:
|
|
538
|
+
event.app.exit(result=None)
|
|
539
|
+
return
|
|
540
|
+
event.app.exit(result=value)
|
|
541
|
+
|
|
525
542
|
@kb.add(Keys.Escape, eager=True)
|
|
526
543
|
def _(event: KeyPressEvent) -> None:
|
|
527
544
|
nonlocal pointed_at
|
|
@@ -702,6 +719,28 @@ class SelectOverlay[T]:
|
|
|
702
719
|
if hasattr(result, "__await__"):
|
|
703
720
|
event.app.create_background_task(cast(Coroutine[Any, Any, None], result))
|
|
704
721
|
|
|
722
|
+
@kb.add(Keys.Tab, filter=is_open_filter, eager=True)
|
|
723
|
+
def _(event: KeyPressEvent) -> None:
|
|
724
|
+
indices, _ = self._get_visible_indices()
|
|
725
|
+
if not indices:
|
|
726
|
+
self.close()
|
|
727
|
+
return
|
|
728
|
+
|
|
729
|
+
self._pointed_at = _coerce_pointed_at_to_selectable(self._items, indices, self._pointed_at)
|
|
730
|
+
idx = indices[self._pointed_at % len(indices)]
|
|
731
|
+
value = self._items[idx].value
|
|
732
|
+
if value is None:
|
|
733
|
+
self.close()
|
|
734
|
+
return
|
|
735
|
+
self.close()
|
|
736
|
+
|
|
737
|
+
if self._on_select is None:
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
result = self._on_select(value)
|
|
741
|
+
if hasattr(result, "__await__"):
|
|
742
|
+
event.app.create_background_task(cast(Coroutine[Any, Any, None], result))
|
|
743
|
+
|
|
705
744
|
@kb.add(Keys.Escape, filter=is_open_filter, eager=True)
|
|
706
745
|
def _(event: KeyPressEvent) -> None:
|
|
707
746
|
if self._use_search_filter and self._search_buffer is not None and self._search_buffer.text:
|
|
@@ -757,9 +796,20 @@ class SelectOverlay[T]:
|
|
|
757
796
|
dont_extend_height=Always(),
|
|
758
797
|
always_hide_cursor=Always(),
|
|
759
798
|
)
|
|
799
|
+
def get_list_height() -> int:
|
|
800
|
+
# Dynamic height: min of configured height and available terminal space
|
|
801
|
+
# Overhead: header(1) + spacer(1) + search(1) + frame borders(2) + prompt area(3)
|
|
802
|
+
overhead = 8
|
|
803
|
+
try:
|
|
804
|
+
terminal_height = get_app().output.get_size().rows
|
|
805
|
+
available = max(3, terminal_height - overhead)
|
|
806
|
+
return min(self._list_height, available)
|
|
807
|
+
except Exception:
|
|
808
|
+
return self._list_height
|
|
809
|
+
|
|
760
810
|
list_window = Window(
|
|
761
811
|
FormattedTextControl(get_choices_tokens),
|
|
762
|
-
height=
|
|
812
|
+
height=get_list_height,
|
|
763
813
|
scroll_offsets=ScrollOffsets(top=0, bottom=2),
|
|
764
814
|
allow_scroll_beyond_bottom=True,
|
|
765
815
|
dont_extend_height=Always(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: klaude-code
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Minimal code agent CLI
|
|
5
5
|
Requires-Dist: anthropic>=0.66.0
|
|
6
6
|
Requires-Dist: chardet>=5.2.0
|
|
@@ -9,15 +9,13 @@ Requires-Dist: diff-match-patch>=20241021
|
|
|
9
9
|
Requires-Dist: google-genai>=1.56.0
|
|
10
10
|
Requires-Dist: markdown-it-py>=4.0.0
|
|
11
11
|
Requires-Dist: openai>=1.102.0
|
|
12
|
-
Requires-Dist: pillow>=9.1,<11.0
|
|
13
12
|
Requires-Dist: prompt-toolkit>=3.0.52
|
|
14
13
|
Requires-Dist: pydantic>=2.11.7
|
|
15
14
|
Requires-Dist: pyyaml>=6.0.2
|
|
16
15
|
Requires-Dist: rich>=14.1.0
|
|
17
|
-
Requires-Dist: term-image>=0.7.2
|
|
18
16
|
Requires-Dist: trafilatura>=2.0.0
|
|
19
17
|
Requires-Dist: typer>=0.17.3
|
|
20
|
-
Requires-Python: >=3.13
|
|
18
|
+
Requires-Python: >=3.13
|
|
21
19
|
Description-Content-Type: text/markdown
|
|
22
20
|
|
|
23
21
|
# Klaude Code
|
|
@@ -61,20 +59,22 @@ klaude upgrade
|
|
|
61
59
|
## Usage
|
|
62
60
|
|
|
63
61
|
```bash
|
|
64
|
-
klaude [--model <name>] [--
|
|
62
|
+
klaude [--model [<name>]] [--continue] [--resume [<id>]]
|
|
65
63
|
```
|
|
66
64
|
|
|
67
65
|
**Options:**
|
|
68
|
-
- `--model`/`-m`:
|
|
69
|
-
- `--
|
|
66
|
+
- `--model`/`-m`: Choose a model.
|
|
67
|
+
- `--model` (no value): opens the interactive selector.
|
|
68
|
+
- `--model <value>`: resolves `<value>` to a single model; if it can't, it opens the interactive selector filtered by `<value>`.
|
|
70
69
|
- `--continue`/`-c`: Resume the most recent session.
|
|
71
|
-
- `--resume`/`-r`:
|
|
72
|
-
- `--resume
|
|
70
|
+
- `--resume`/`-r`: Resume a session.
|
|
71
|
+
- `--resume` (no value): select a session to resume for this project.
|
|
72
|
+
- `--resume <id>`: resume a session by its ID directly.
|
|
73
73
|
- `--vanilla`: Minimal mode with only basic tools (Bash, Read, Edit, Write) and no system prompts.
|
|
74
74
|
|
|
75
75
|
**Model selection behavior:**
|
|
76
76
|
- Default: uses `main_model` from config.
|
|
77
|
-
- `--
|
|
77
|
+
- `--model` (no value): always prompts you to pick.
|
|
78
78
|
- `--model <value>`: tries to resolve `<value>` to a single model; if it can't, it prompts with a filtered list (and falls back to showing all models if there are no matches).
|
|
79
79
|
|
|
80
80
|
**Debug Options:**
|