klaude-code 2.8.0__py3-none-any.whl → 2.9.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 +2 -1
- klaude_code/auth/antigravity/oauth.py +0 -9
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- klaude_code/auth/codex/exceptions.py +0 -4
- klaude_code/auth/codex/oauth.py +32 -28
- klaude_code/auth/codex/token_manager.py +0 -18
- klaude_code/cli/cost_cmd.py +128 -39
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +15 -4
- klaude_code/config/assets/builtin_config.yaml +8 -24
- klaude_code/config/config.py +47 -25
- klaude_code/config/sub_agent_model_helper.py +18 -13
- klaude_code/config/thinking.py +0 -8
- klaude_code/const.py +2 -2
- klaude_code/core/agent_profile.py +11 -53
- klaude_code/core/compaction/compaction.py +4 -6
- klaude_code/core/compaction/overflow.py +0 -4
- klaude_code/core/executor.py +51 -5
- klaude_code/core/manager/llm_clients.py +9 -1
- klaude_code/core/prompts/prompt-claude-code.md +4 -4
- klaude_code/core/reminders.py +21 -23
- klaude_code/core/task.py +0 -4
- klaude_code/core/tool/__init__.py +3 -2
- klaude_code/core/tool/file/apply_patch.py +0 -27
- klaude_code/core/tool/file/edit_tool.py +1 -2
- klaude_code/core/tool/file/read_tool.md +3 -2
- klaude_code/core/tool/file/read_tool.py +15 -2
- klaude_code/core/tool/offload.py +0 -35
- klaude_code/core/tool/sub_agent/__init__.py +6 -0
- klaude_code/core/tool/sub_agent/image_gen.md +16 -0
- klaude_code/core/tool/sub_agent/image_gen.py +146 -0
- klaude_code/core/tool/sub_agent/task.md +20 -0
- klaude_code/core/tool/sub_agent/task.py +205 -0
- klaude_code/core/tool/tool_registry.py +0 -16
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/input.py +6 -5
- klaude_code/llm/antigravity/input.py +14 -7
- klaude_code/llm/codex/client.py +22 -0
- klaude_code/llm/codex/prompt_sync.py +237 -0
- klaude_code/llm/google/client.py +8 -6
- klaude_code/llm/google/input.py +20 -12
- klaude_code/llm/image.py +18 -11
- klaude_code/llm/input_common.py +14 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +16 -1
- klaude_code/llm/registry.py +0 -5
- klaude_code/llm/responses/input.py +15 -5
- klaude_code/llm/usage.py +0 -8
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +2 -1
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/model.py +20 -1
- klaude_code/protocol/op.py +27 -0
- klaude_code/protocol/op_handler.py +10 -0
- klaude_code/protocol/sub_agent/AGENTS.md +5 -5
- klaude_code/protocol/sub_agent/__init__.py +13 -34
- klaude_code/protocol/sub_agent/explore.py +7 -34
- klaude_code/protocol/sub_agent/image_gen.py +3 -74
- klaude_code/protocol/sub_agent/task.py +3 -47
- klaude_code/protocol/sub_agent/web.py +8 -52
- klaude_code/protocol/tools.py +2 -0
- klaude_code/session/export.py +308 -299
- klaude_code/session/session.py +58 -21
- klaude_code/session/store.py +0 -4
- klaude_code/session/templates/export_session.html +430 -134
- klaude_code/skill/assets/deslop/SKILL.md +9 -0
- klaude_code/skill/system_skills.py +0 -20
- klaude_code/tui/command/__init__.py +3 -0
- klaude_code/tui/command/continue_cmd.py +34 -0
- klaude_code/tui/command/fork_session_cmd.py +5 -2
- klaude_code/tui/command/resume_cmd.py +9 -2
- klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
- klaude_code/tui/components/assistant.py +0 -26
- klaude_code/tui/components/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +2 -208
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/rich/markdown.py +60 -63
- klaude_code/tui/components/rich/theme.py +2 -0
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +43 -21
- klaude_code/tui/input/images.py +21 -18
- klaude_code/tui/input/key_bindings.py +2 -2
- klaude_code/tui/input/prompt_toolkit.py +49 -49
- klaude_code/tui/machine.py +15 -11
- klaude_code/tui/renderer.py +12 -20
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +6 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/RECORD +97 -92
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
|
@@ -20,6 +20,7 @@ from prompt_toolkit.key_binding import merge_key_bindings
|
|
|
20
20
|
from prompt_toolkit.layout import Float
|
|
21
21
|
from prompt_toolkit.layout.containers import Container, FloatContainer, Window
|
|
22
22
|
from prompt_toolkit.layout.controls import BufferControl, UIContent
|
|
23
|
+
from prompt_toolkit.layout.dimension import Dimension
|
|
23
24
|
from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu
|
|
24
25
|
from prompt_toolkit.patch_stdout import patch_stdout
|
|
25
26
|
from prompt_toolkit.styles import Style
|
|
@@ -61,9 +62,9 @@ COMPLETION_SELECTED_LIGHT_BG = "ansigreen"
|
|
|
61
62
|
COMPLETION_SELECTED_UNKNOWN_BG = "ansigreen"
|
|
62
63
|
COMPLETION_MENU = "ansibrightblack"
|
|
63
64
|
INPUT_PROMPT_STYLE = "ansimagenta bold"
|
|
64
|
-
PLACEHOLDER_TEXT_STYLE_DARK_BG = "fg:#5a5a5a
|
|
65
|
-
PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a
|
|
66
|
-
PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG = "fg:#8a8a8a
|
|
65
|
+
PLACEHOLDER_TEXT_STYLE_DARK_BG = "fg:#5a5a5a"
|
|
66
|
+
PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a"
|
|
67
|
+
PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG = "fg:#8a8a8a"
|
|
67
68
|
PLACEHOLDER_SYMBOL_STYLE_DARK_BG = "bg:#2a2a2a fg:#5a5a5a"
|
|
68
69
|
PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "bg:#e6e6e6 fg:#7a7a7a"
|
|
69
70
|
PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "bg:#2a2a2a fg:#8a8a8a"
|
|
@@ -81,6 +82,9 @@ def _left_align_completion_menus(container: Container) -> None:
|
|
|
81
82
|
cursor (`xcursor=True`). That makes the popup indent as the caret moves.
|
|
82
83
|
We walk the layout tree and rewrite the Float positioning for completion menus
|
|
83
84
|
to keep them fixed at the left edge.
|
|
85
|
+
|
|
86
|
+
Note: We intentionally keep Y positioning (ycursor) unchanged so that the
|
|
87
|
+
completion menu stays near the cursor/input line.
|
|
84
88
|
"""
|
|
85
89
|
if isinstance(container, FloatContainer):
|
|
86
90
|
for flt in container.floats:
|
|
@@ -300,6 +304,10 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
300
304
|
key_bindings=kb,
|
|
301
305
|
completer=ThreadedCompleter(create_repl_completer(command_info_provider=self._command_info_provider)),
|
|
302
306
|
complete_while_typing=True,
|
|
307
|
+
# Keep the bottom toolbar stable while completion menus open/close.
|
|
308
|
+
# Reserving space dynamically can make the non-fullscreen prompt
|
|
309
|
+
# "jump" by printing extra lines.
|
|
310
|
+
reserve_space_for_menu=0,
|
|
303
311
|
erase_when_done=True,
|
|
304
312
|
mouse_support=False,
|
|
305
313
|
style=Style.from_dict(
|
|
@@ -417,41 +425,40 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
417
425
|
|
|
418
426
|
original_height = input_window.height
|
|
419
427
|
|
|
428
|
+
# Keep a comfortable multiline editing area even when no completion
|
|
429
|
+
# space is reserved. (We set reserve_space_for_menu=0 to avoid the
|
|
430
|
+
# bottom toolbar jumping when completions open/close.)
|
|
431
|
+
base_rows = 10
|
|
432
|
+
|
|
420
433
|
def _height(): # type: ignore[no-untyped-def]
|
|
421
434
|
picker_open = (self._model_picker is not None and self._model_picker.is_open) or (
|
|
422
435
|
self._thinking_picker is not None and self._thinking_picker.is_open
|
|
423
436
|
)
|
|
424
437
|
|
|
425
|
-
try:
|
|
426
|
-
complete_state = self._session.default_buffer.complete_state
|
|
427
|
-
completion_open = complete_state is not None and bool(complete_state.completions)
|
|
428
|
-
except Exception:
|
|
429
|
-
completion_open = False
|
|
430
|
-
|
|
431
438
|
try:
|
|
432
439
|
original_height_value = original_height() if callable(original_height) else original_height
|
|
433
440
|
except Exception:
|
|
434
441
|
original_height_value = None
|
|
435
|
-
|
|
442
|
+
original_min = 0
|
|
443
|
+
if isinstance(original_height_value, Dimension):
|
|
444
|
+
original_min = int(original_height_value.min)
|
|
445
|
+
elif isinstance(original_height_value, int):
|
|
446
|
+
original_min = int(original_height_value)
|
|
436
447
|
|
|
437
|
-
if picker_open
|
|
438
|
-
target_rows = 24 if picker_open else 14
|
|
448
|
+
target_rows = 24 if picker_open else base_rows
|
|
439
449
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
450
|
+
# Cap to the current terminal size.
|
|
451
|
+
# Leave a small buffer to avoid triggering "Window too small".
|
|
452
|
+
try:
|
|
453
|
+
rows = get_app().output.get_size().rows
|
|
454
|
+
except Exception:
|
|
455
|
+
rows = 0
|
|
446
456
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return expanded
|
|
457
|
+
desired = max(original_min, target_rows)
|
|
458
|
+
if rows > 0:
|
|
459
|
+
desired = max(3, min(desired, rows - 2))
|
|
451
460
|
|
|
452
|
-
|
|
453
|
-
return original_height()
|
|
454
|
-
return original_height
|
|
461
|
+
return Dimension(min=desired, preferred=desired)
|
|
455
462
|
|
|
456
463
|
input_window.height = _height
|
|
457
464
|
|
|
@@ -583,7 +590,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
583
590
|
except (AttributeError, RuntimeError):
|
|
584
591
|
pass
|
|
585
592
|
|
|
586
|
-
# Priority: update_message > debug_log_path
|
|
593
|
+
# Priority: update_message > debug_log_path > shortcut hints
|
|
587
594
|
display_text: str | None = None
|
|
588
595
|
text_style: str = ""
|
|
589
596
|
if update_message:
|
|
@@ -593,31 +600,25 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
593
600
|
display_text = f"Debug log: {debug_log_path}"
|
|
594
601
|
text_style = "fg:ansibrightblack"
|
|
595
602
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
# will still reserve the toolbar line.)
|
|
599
|
-
if not display_text:
|
|
603
|
+
if display_text:
|
|
604
|
+
left_text = " " + display_text
|
|
600
605
|
try:
|
|
601
606
|
terminal_width = shutil.get_terminal_size().columns
|
|
607
|
+
padding = " " * max(0, terminal_width - len(left_text))
|
|
602
608
|
except (OSError, ValueError):
|
|
603
|
-
|
|
604
|
-
return FormattedText([("", " " * max(0, terminal_width))])
|
|
609
|
+
padding = ""
|
|
605
610
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
terminal_width = shutil.get_terminal_size().columns
|
|
609
|
-
padding = " " * max(0, terminal_width - len(left_text))
|
|
610
|
-
except (OSError, ValueError):
|
|
611
|
-
padding = ""
|
|
611
|
+
toolbar_text = left_text + padding
|
|
612
|
+
return FormattedText([(text_style, toolbar_text)])
|
|
612
613
|
|
|
613
|
-
|
|
614
|
-
return
|
|
614
|
+
# Show shortcut hints when nothing else to display
|
|
615
|
+
return self._render_shortcut_hints()
|
|
615
616
|
|
|
616
617
|
# -------------------------------------------------------------------------
|
|
617
|
-
#
|
|
618
|
+
# Shortcut hints (bottom toolbar)
|
|
618
619
|
# -------------------------------------------------------------------------
|
|
619
620
|
|
|
620
|
-
def
|
|
621
|
+
def _render_shortcut_hints(self) -> FormattedText:
|
|
621
622
|
if self._is_light_terminal_background is True:
|
|
622
623
|
text_style = PLACEHOLDER_TEXT_STYLE_LIGHT_BG
|
|
623
624
|
symbol_style = PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG
|
|
@@ -630,27 +631,27 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
630
631
|
|
|
631
632
|
return FormattedText(
|
|
632
633
|
[
|
|
633
|
-
(text_style, " "
|
|
634
|
+
(text_style, " "),
|
|
634
635
|
(symbol_style, " @ "),
|
|
635
636
|
(text_style, " "),
|
|
636
637
|
(text_style, "files"),
|
|
637
|
-
(text_style, "
|
|
638
|
+
(text_style, " • "),
|
|
638
639
|
(symbol_style, " $ "),
|
|
639
640
|
(text_style, " "),
|
|
640
641
|
(text_style, "skills"),
|
|
641
|
-
(text_style, "
|
|
642
|
+
(text_style, " • "),
|
|
642
643
|
(symbol_style, " / "),
|
|
643
644
|
(text_style, " "),
|
|
644
645
|
(text_style, "commands"),
|
|
645
|
-
(text_style, "
|
|
646
|
+
(text_style, " • "),
|
|
646
647
|
(symbol_style, " ctrl-l "),
|
|
647
648
|
(text_style, " "),
|
|
648
649
|
(text_style, "models"),
|
|
649
|
-
(text_style, "
|
|
650
|
+
(text_style, " • "),
|
|
650
651
|
(symbol_style, " ctrl-t "),
|
|
651
652
|
(text_style, " "),
|
|
652
653
|
(text_style, "think"),
|
|
653
|
-
(text_style, "
|
|
654
|
+
(text_style, " • "),
|
|
654
655
|
(symbol_style, " ctrl-v "),
|
|
655
656
|
(text_style, " "),
|
|
656
657
|
(text_style, "paste image"),
|
|
@@ -679,7 +680,6 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
679
680
|
# proper styling instead of showing raw escape codes.
|
|
680
681
|
with patch_stdout(raw=True):
|
|
681
682
|
line: str = await self._session.prompt_async(
|
|
682
|
-
placeholder=self._render_input_placeholder(),
|
|
683
683
|
bottom_toolbar=self._get_bottom_toolbar,
|
|
684
684
|
)
|
|
685
685
|
if self._post_prompt is not None:
|
klaude_code/tui/machine.py
CHANGED
|
@@ -49,7 +49,7 @@ from klaude_code.tui.commands import (
|
|
|
49
49
|
from klaude_code.tui.components.rich import status as r_status
|
|
50
50
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
51
51
|
from klaude_code.tui.components.thinking import extract_last_bold_header, normalize_thinking_content
|
|
52
|
-
from klaude_code.tui.components.tools import get_tool_active_form, is_sub_agent_tool
|
|
52
|
+
from klaude_code.tui.components.tools import get_task_active_form, get_tool_active_form, is_sub_agent_tool
|
|
53
53
|
|
|
54
54
|
# Tools that complete quickly and don't benefit from streaming activity display.
|
|
55
55
|
# For models without fine-grained tool JSON streaming (e.g., Gemini), showing these
|
|
@@ -97,10 +97,6 @@ class ActivityState:
|
|
|
97
97
|
self._sub_agent_tool_calls: dict[str, int] = {}
|
|
98
98
|
self._sub_agent_tool_calls_by_id: dict[str, str] = {}
|
|
99
99
|
|
|
100
|
-
@property
|
|
101
|
-
def is_composing(self) -> bool:
|
|
102
|
-
return self._composing and not self._tool_calls and not self._sub_agent_tool_calls
|
|
103
|
-
|
|
104
100
|
def set_composing(self, composing: bool) -> None:
|
|
105
101
|
self._composing = composing
|
|
106
102
|
if not composing:
|
|
@@ -321,7 +317,7 @@ class _SessionState:
|
|
|
321
317
|
|
|
322
318
|
@property
|
|
323
319
|
def should_show_sub_agent_thinking_header(self) -> bool:
|
|
324
|
-
return bool(self.sub_agent_state and self.sub_agent_state.sub_agent_type ==
|
|
320
|
+
return bool(self.sub_agent_state and self.sub_agent_state.sub_agent_type == tools.IMAGE_GEN)
|
|
325
321
|
|
|
326
322
|
@property
|
|
327
323
|
def should_extract_reasoning_header(self) -> bool:
|
|
@@ -607,11 +603,14 @@ class DisplayStateMachine:
|
|
|
607
603
|
# Skip activity state for fast tools on non-streaming models (e.g., Gemini)
|
|
608
604
|
# to avoid flash-and-disappear effect
|
|
609
605
|
if not is_replay and not s.should_skip_tool_activity(e.tool_name):
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
|
|
606
|
+
if e.tool_name == tools.TASK:
|
|
607
|
+
pass
|
|
613
608
|
else:
|
|
614
|
-
|
|
609
|
+
tool_active_form = get_tool_active_form(e.tool_name)
|
|
610
|
+
if is_sub_agent_tool(e.tool_name):
|
|
611
|
+
self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
|
|
612
|
+
else:
|
|
613
|
+
self._spinner.add_tool_call(tool_active_form)
|
|
615
614
|
|
|
616
615
|
if not is_replay:
|
|
617
616
|
cmds.extend(self._spinner_update_commands())
|
|
@@ -629,12 +628,17 @@ class DisplayStateMachine:
|
|
|
629
628
|
primary.thinking_stream_active = False
|
|
630
629
|
cmds.append(EndThinkingStream(session_id=primary.session_id))
|
|
631
630
|
|
|
631
|
+
if not is_replay and e.tool_name == tools.TASK and not s.should_skip_tool_activity(e.tool_name):
|
|
632
|
+
tool_active_form = get_task_active_form(e.arguments)
|
|
633
|
+
self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
|
|
634
|
+
cmds.extend(self._spinner_update_commands())
|
|
635
|
+
|
|
632
636
|
cmds.append(RenderToolCall(e))
|
|
633
637
|
return cmds
|
|
634
638
|
|
|
635
639
|
case events.ToolResultEvent() as e:
|
|
636
640
|
if not is_replay and is_sub_agent_tool(e.tool_name):
|
|
637
|
-
self._spinner.finish_sub_agent_tool_call(e.tool_call_id
|
|
641
|
+
self._spinner.finish_sub_agent_tool_call(e.tool_call_id)
|
|
638
642
|
cmds.extend(self._spinner_update_commands())
|
|
639
643
|
|
|
640
644
|
if s.is_sub_agent and not e.is_error:
|
klaude_code/tui/renderer.py
CHANGED
|
@@ -184,13 +184,6 @@ class TUICommandRenderer:
|
|
|
184
184
|
def is_sub_agent_session(self, session_id: str) -> bool:
|
|
185
185
|
return session_id in self._sessions and self._sessions[session_id].sub_agent_state is not None
|
|
186
186
|
|
|
187
|
-
def _should_display_sub_agent_thinking_header(self, session_id: str) -> bool:
|
|
188
|
-
# Hardcoded: only show sub-agent thinking headers for ImageGen.
|
|
189
|
-
st = self._sessions.get(session_id)
|
|
190
|
-
if st is None or st.sub_agent_state is None:
|
|
191
|
-
return False
|
|
192
|
-
return st.sub_agent_state.sub_agent_type == "ImageGen"
|
|
193
|
-
|
|
194
187
|
def _advance_sub_agent_color_index(self) -> None:
|
|
195
188
|
palette_size = len(self.themes.sub_agent_colors)
|
|
196
189
|
if palette_size == 0:
|
|
@@ -379,6 +372,7 @@ class TUICommandRenderer:
|
|
|
379
372
|
live_sink=self.set_stream_renderable,
|
|
380
373
|
mark=c_assistant.ASSISTANT_MESSAGE_MARK,
|
|
381
374
|
left_margin=MARKDOWN_LEFT_MARGIN,
|
|
375
|
+
image_callback=self.display_image,
|
|
382
376
|
)
|
|
383
377
|
|
|
384
378
|
def _flush_thinking(self) -> None:
|
|
@@ -416,21 +410,12 @@ class TUICommandRenderer:
|
|
|
416
410
|
if image_path is not None:
|
|
417
411
|
self.display_image(str(image_path))
|
|
418
412
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
self.print(renderable)
|
|
413
|
+
if not is_sub_agent and isinstance(e.ui_extra, model.ImageUIExtra):
|
|
414
|
+
self.display_image(e.ui_extra.file_path)
|
|
422
415
|
|
|
423
|
-
|
|
424
|
-
renderable = c_thinking.render_thinking(
|
|
425
|
-
content,
|
|
426
|
-
code_theme=self.themes.code_theme,
|
|
427
|
-
style=ThemeKey.THINKING,
|
|
428
|
-
)
|
|
416
|
+
renderable = c_tools.render_tool_result(e, code_theme=self.themes.code_theme, session_id=e.session_id)
|
|
429
417
|
if renderable is not None:
|
|
430
|
-
self.console.push_theme(theme=self.themes.thinking_markdown_theme)
|
|
431
418
|
self.print(renderable)
|
|
432
|
-
self.console.pop_theme()
|
|
433
|
-
self.print()
|
|
434
419
|
|
|
435
420
|
def display_thinking_header(self, header: str) -> None:
|
|
436
421
|
stripped = header.strip()
|
|
@@ -450,6 +435,13 @@ class TUICommandRenderer:
|
|
|
450
435
|
with self.session_print_context(e.session_id):
|
|
451
436
|
self.print(c_developer.render_developer_message(e))
|
|
452
437
|
|
|
438
|
+
# Display images from @ file references and user attachments
|
|
439
|
+
if e.item.ui_extra:
|
|
440
|
+
for ui_item in e.item.ui_extra.items:
|
|
441
|
+
if isinstance(ui_item, (model.AtFileImagesUIItem, model.UserImagesUIItem)):
|
|
442
|
+
for image_path in ui_item.paths:
|
|
443
|
+
self.display_image(image_path)
|
|
444
|
+
|
|
453
445
|
def display_command_output(self, e: events.CommandOutputEvent) -> None:
|
|
454
446
|
with self.session_print_context(e.session_id):
|
|
455
447
|
self.print(c_command_output.render_command_output(e))
|
|
@@ -689,7 +681,7 @@ class TUICommandRenderer:
|
|
|
689
681
|
case PrintBlankLine():
|
|
690
682
|
self.print()
|
|
691
683
|
case PrintRuleLine():
|
|
692
|
-
self.console.print(Rule(characters="─", style=ThemeKey.
|
|
684
|
+
self.console.print(Rule(characters="─", style=ThemeKey.LINES_DIM))
|
|
693
685
|
case EmitOsc94Error():
|
|
694
686
|
emit_osc94(OSC94States.ERROR)
|
|
695
687
|
case EmitTmuxSignal():
|
klaude_code/tui/runner.py
CHANGED
|
@@ -327,5 +327,6 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
327
327
|
if not exit_hint_printed:
|
|
328
328
|
active_session_id = components.executor.context.current_session_id()
|
|
329
329
|
if active_session_id and Session.exists(active_session_id):
|
|
330
|
+
short_id = Session.shortest_unique_prefix(active_session_id)
|
|
330
331
|
log(f"Session ID: {active_session_id}")
|
|
331
|
-
log(f"Resume with: klaude
|
|
332
|
+
log(f"Resume with: klaude -r {short_id}")
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import shutil
|
|
5
|
-
import struct
|
|
6
5
|
import subprocess
|
|
7
6
|
import sys
|
|
8
7
|
import tempfile
|
|
@@ -12,8 +11,8 @@ from typing import IO
|
|
|
12
11
|
# Kitty graphics protocol chunk size (4096 is the recommended max)
|
|
13
12
|
_CHUNK_SIZE = 4096
|
|
14
13
|
|
|
15
|
-
# Max columns for
|
|
16
|
-
_MAX_COLS =
|
|
14
|
+
# Max columns for image display
|
|
15
|
+
_MAX_COLS = 80
|
|
17
16
|
|
|
18
17
|
# Image formats that need conversion to PNG
|
|
19
18
|
_NEEDS_CONVERSION = {".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff", ".tif"}
|
|
@@ -40,26 +39,10 @@ def _convert_to_png(path: Path) -> bytes | None:
|
|
|
40
39
|
return None
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
def _get_png_dimensions(data: bytes) -> tuple[int, int] | None:
|
|
44
|
-
"""Extract width and height from PNG file header."""
|
|
45
|
-
# PNG: 8-byte signature + IHDR chunk (4 len + 4 type + 4 width + 4 height)
|
|
46
|
-
if len(data) < 24 or data[:8] != b"\x89PNG\r\n\x1a\n":
|
|
47
|
-
return None
|
|
48
|
-
width, height = struct.unpack(">II", data[16:24])
|
|
49
|
-
return width, height
|
|
50
|
-
|
|
51
|
-
|
|
52
42
|
def print_kitty_image(file_path: str | Path, *, file: IO[str] | None = None) -> None:
|
|
53
43
|
"""Print an image to the terminal using Kitty graphics protocol.
|
|
54
44
|
|
|
55
|
-
|
|
56
|
-
with raw escape sequences. Image size adapts based on aspect ratio:
|
|
57
|
-
- Landscape images: fill terminal width
|
|
58
|
-
- Portrait images: limit height to avoid oversized display
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
file_path: Path to the image file (PNG recommended).
|
|
62
|
-
file: Output file stream. Defaults to stdout.
|
|
45
|
+
Only specifies column width; Kitty auto-scales height to preserve aspect ratio.
|
|
63
46
|
"""
|
|
64
47
|
path = Path(file_path) if isinstance(file_path, str) else file_path
|
|
65
48
|
if not path.exists():
|
|
@@ -80,20 +63,9 @@ def print_kitty_image(file_path: str | Path, *, file: IO[str] | None = None) ->
|
|
|
80
63
|
out = file or sys.stdout
|
|
81
64
|
|
|
82
65
|
term_size = shutil.get_terminal_size()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if dimensions is not None:
|
|
87
|
-
img_width, img_height = dimensions
|
|
88
|
-
if img_width > 2 * img_height:
|
|
89
|
-
# Wide landscape (width > 2x height): fill terminal width
|
|
90
|
-
size_param = f"c={term_size.columns}"
|
|
91
|
-
else:
|
|
92
|
-
# Other images: limit width to 80% of terminal
|
|
93
|
-
size_param = f"c={min(_MAX_COLS, term_size.columns * 4 // 5)}"
|
|
94
|
-
else:
|
|
95
|
-
# Fallback: limit width to 80% of terminal
|
|
96
|
-
size_param = f"c={min(_MAX_COLS, term_size.columns * 4 // 5)}"
|
|
66
|
+
# Only specify columns, let Kitty auto-scale height to preserve aspect ratio
|
|
67
|
+
target_cols = min(_MAX_COLS, term_size.columns)
|
|
68
|
+
size_param = f"c={target_cols}"
|
|
97
69
|
print("", file=out)
|
|
98
70
|
_write_kitty_graphics(out, encoded, size_param=size_param)
|
|
99
71
|
print("", file=out)
|
klaude_code/ui/common.py
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import subprocess
|
|
3
|
-
from pathlib import Path
|
|
4
1
|
from typing import TYPE_CHECKING
|
|
5
2
|
|
|
6
3
|
if TYPE_CHECKING:
|
|
7
4
|
from klaude_code.protocol.llm_param import LLMConfigModelParameter, OpenRouterProviderRouting
|
|
8
5
|
|
|
9
|
-
LEADING_NEWLINES_REGEX = re.compile(r"^\n{2,}")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def remove_leading_newlines(text: str) -> str:
|
|
13
|
-
return text.lstrip("\n")
|
|
14
|
-
|
|
15
6
|
|
|
16
7
|
def format_number(tokens: int) -> str:
|
|
17
8
|
if tokens < 1000:
|
|
@@ -33,67 +24,6 @@ def format_number(tokens: int) -> str:
|
|
|
33
24
|
return f"{m}M{remaining}k"
|
|
34
25
|
|
|
35
26
|
|
|
36
|
-
def get_current_git_branch(path: Path | None = None) -> str | None:
|
|
37
|
-
"""Get current git branch name, return None if not in a git repository"""
|
|
38
|
-
if path is None:
|
|
39
|
-
path = Path.cwd()
|
|
40
|
-
|
|
41
|
-
try:
|
|
42
|
-
# Check if in git repository
|
|
43
|
-
git_dir = subprocess.run(
|
|
44
|
-
["git", "rev-parse", "--git-dir"],
|
|
45
|
-
cwd=path,
|
|
46
|
-
capture_output=True,
|
|
47
|
-
text=True,
|
|
48
|
-
timeout=2,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
if git_dir.returncode != 0:
|
|
52
|
-
return None
|
|
53
|
-
|
|
54
|
-
# Get current branch name
|
|
55
|
-
result = subprocess.run(
|
|
56
|
-
["git", "branch", "--show-current"],
|
|
57
|
-
cwd=path,
|
|
58
|
-
capture_output=True,
|
|
59
|
-
text=True,
|
|
60
|
-
timeout=2,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
if result.returncode == 0:
|
|
64
|
-
branch = result.stdout.strip()
|
|
65
|
-
return branch if branch else None
|
|
66
|
-
|
|
67
|
-
# Fallback: get HEAD reference
|
|
68
|
-
head_file = subprocess.run(
|
|
69
|
-
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
70
|
-
cwd=path,
|
|
71
|
-
capture_output=True,
|
|
72
|
-
text=True,
|
|
73
|
-
timeout=2,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
if head_file.returncode == 0:
|
|
77
|
-
branch = head_file.stdout.strip()
|
|
78
|
-
return branch if branch and branch != "HEAD" else None
|
|
79
|
-
|
|
80
|
-
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
|
|
81
|
-
pass
|
|
82
|
-
|
|
83
|
-
return None
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def show_path_with_tilde(path: Path | None = None):
|
|
87
|
-
if path is None:
|
|
88
|
-
path = Path.cwd()
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
relative_path = path.relative_to(Path.home())
|
|
92
|
-
return f"~/{relative_path}"
|
|
93
|
-
except ValueError:
|
|
94
|
-
return str(path)
|
|
95
|
-
|
|
96
|
-
|
|
97
27
|
def format_model_params(model_params: "LLMConfigModelParameter") -> list[str]:
|
|
98
28
|
"""Format model parameters in a concise style.
|
|
99
29
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: klaude-code
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0
|
|
4
4
|
Summary: Minimal code agent CLI
|
|
5
5
|
Requires-Dist: anthropic>=0.66.0
|
|
6
6
|
Requires-Dist: chardet>=5.2.0
|
|
7
7
|
Requires-Dist: ddgs>=9.9.3
|
|
8
8
|
Requires-Dist: diff-match-patch>=20241021
|
|
9
|
+
Requires-Dist: filelock>=3.20.3
|
|
9
10
|
Requires-Dist: google-genai>=1.56.0
|
|
10
11
|
Requires-Dist: markdown-it-py>=4.0.0
|
|
11
12
|
Requires-Dist: openai>=1.102.0
|
|
@@ -23,7 +24,7 @@ Description-Content-Type: text/markdown
|
|
|
23
24
|
Minimal code agent CLI.
|
|
24
25
|
|
|
25
26
|
## Features
|
|
26
|
-
- **Multi-provider**: Anthropic Message API, OpenAI Responses API, OpenRouter,
|
|
27
|
+
- **Multi-provider**: Anthropic Message API, OpenAI Responses API, OpenRouter, ChatGPT Codex OAuth etc.
|
|
27
28
|
- **Keep reasoning item in context**: Interleaved thinking support
|
|
28
29
|
- **Model-aware tools**: Claude Code tool set for Opus, `apply_patch` for GPT-5/Codex
|
|
29
30
|
- **Reminders**: Cooldown-based todo tracking, instruction reinforcement and external file change reminder
|
|
@@ -107,7 +108,6 @@ On first run, you'll be prompted to select a model. Your choice is saved as `mai
|
|
|
107
108
|
| Provider | Env Variable | Models |
|
|
108
109
|
|-------------|-----------------------|-------------------------------------------------------------------------------|
|
|
109
110
|
| anthropic | `ANTHROPIC_API_KEY` | sonnet, opus |
|
|
110
|
-
| claude | N/A (OAuth) | sonnet@claude, opus@claude (requires Claude Pro/Max subscription) |
|
|
111
111
|
| openai | `OPENAI_API_KEY` | gpt-5.2 |
|
|
112
112
|
| openrouter | `OPENROUTER_API_KEY` | gpt-5.2, gpt-5.2-fast, gpt-5.1-codex-max, sonnet, opus, haiku, kimi, gemini-* |
|
|
113
113
|
| deepseek | `DEEPSEEK_API_KEY` | deepseek |
|
|
@@ -139,7 +139,6 @@ klaude auth login deepseek # Set DEEPSEEK_API_KEY
|
|
|
139
139
|
klaude auth login moonshot # Set MOONSHOT_API_KEY
|
|
140
140
|
|
|
141
141
|
# OAuth login for subscription-based providers
|
|
142
|
-
klaude auth login claude # Claude Pro/Max subscription
|
|
143
142
|
klaude auth login codex # ChatGPT Pro subscription
|
|
144
143
|
```
|
|
145
144
|
|
|
@@ -148,7 +147,6 @@ API keys are stored in `~/.klaude/klaude-auth.json` and used as fallback when en
|
|
|
148
147
|
To logout from OAuth providers:
|
|
149
148
|
|
|
150
149
|
```bash
|
|
151
|
-
klaude auth logout claude
|
|
152
150
|
klaude auth logout codex
|
|
153
151
|
```
|
|
154
152
|
|
|
@@ -201,7 +199,6 @@ provider_list:
|
|
|
201
199
|
##### Supported Protocols
|
|
202
200
|
|
|
203
201
|
- `anthropic` - Anthropic Messages API
|
|
204
|
-
- `claude_oauth` - Claude OAuth (for Claude Pro/Max subscribers)
|
|
205
202
|
- `openai` - OpenAI Chat Completion API
|
|
206
203
|
- `responses` - OpenAI Responses API (for o-series, GPT-5, Codex)
|
|
207
204
|
- `codex_oauth` - OpenAI Codex CLI (OAuth-based, for ChatGPT Pro subscribers)
|