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.
Files changed (100) hide show
  1. klaude_code/app/runtime.py +2 -1
  2. klaude_code/auth/antigravity/oauth.py +0 -9
  3. klaude_code/auth/antigravity/token_manager.py +0 -18
  4. klaude_code/auth/base.py +53 -0
  5. klaude_code/auth/codex/exceptions.py +0 -4
  6. klaude_code/auth/codex/oauth.py +32 -28
  7. klaude_code/auth/codex/token_manager.py +0 -18
  8. klaude_code/cli/cost_cmd.py +128 -39
  9. klaude_code/cli/list_model.py +27 -10
  10. klaude_code/cli/main.py +15 -4
  11. klaude_code/config/assets/builtin_config.yaml +8 -24
  12. klaude_code/config/config.py +47 -25
  13. klaude_code/config/sub_agent_model_helper.py +18 -13
  14. klaude_code/config/thinking.py +0 -8
  15. klaude_code/const.py +2 -2
  16. klaude_code/core/agent_profile.py +11 -53
  17. klaude_code/core/compaction/compaction.py +4 -6
  18. klaude_code/core/compaction/overflow.py +0 -4
  19. klaude_code/core/executor.py +51 -5
  20. klaude_code/core/manager/llm_clients.py +9 -1
  21. klaude_code/core/prompts/prompt-claude-code.md +4 -4
  22. klaude_code/core/reminders.py +21 -23
  23. klaude_code/core/task.py +0 -4
  24. klaude_code/core/tool/__init__.py +3 -2
  25. klaude_code/core/tool/file/apply_patch.py +0 -27
  26. klaude_code/core/tool/file/edit_tool.py +1 -2
  27. klaude_code/core/tool/file/read_tool.md +3 -2
  28. klaude_code/core/tool/file/read_tool.py +15 -2
  29. klaude_code/core/tool/offload.py +0 -35
  30. klaude_code/core/tool/sub_agent/__init__.py +6 -0
  31. klaude_code/core/tool/sub_agent/image_gen.md +16 -0
  32. klaude_code/core/tool/sub_agent/image_gen.py +146 -0
  33. klaude_code/core/tool/sub_agent/task.md +20 -0
  34. klaude_code/core/tool/sub_agent/task.py +205 -0
  35. klaude_code/core/tool/tool_registry.py +0 -16
  36. klaude_code/core/turn.py +1 -1
  37. klaude_code/llm/anthropic/input.py +6 -5
  38. klaude_code/llm/antigravity/input.py +14 -7
  39. klaude_code/llm/codex/client.py +22 -0
  40. klaude_code/llm/codex/prompt_sync.py +237 -0
  41. klaude_code/llm/google/client.py +8 -6
  42. klaude_code/llm/google/input.py +20 -12
  43. klaude_code/llm/image.py +18 -11
  44. klaude_code/llm/input_common.py +14 -6
  45. klaude_code/llm/json_stable.py +37 -0
  46. klaude_code/llm/openai_compatible/input.py +0 -10
  47. klaude_code/llm/openai_compatible/stream.py +16 -1
  48. klaude_code/llm/registry.py +0 -5
  49. klaude_code/llm/responses/input.py +15 -5
  50. klaude_code/llm/usage.py +0 -8
  51. klaude_code/protocol/commands.py +1 -0
  52. klaude_code/protocol/events.py +2 -1
  53. klaude_code/protocol/message.py +2 -2
  54. klaude_code/protocol/model.py +20 -1
  55. klaude_code/protocol/op.py +27 -0
  56. klaude_code/protocol/op_handler.py +10 -0
  57. klaude_code/protocol/sub_agent/AGENTS.md +5 -5
  58. klaude_code/protocol/sub_agent/__init__.py +13 -34
  59. klaude_code/protocol/sub_agent/explore.py +7 -34
  60. klaude_code/protocol/sub_agent/image_gen.py +3 -74
  61. klaude_code/protocol/sub_agent/task.py +3 -47
  62. klaude_code/protocol/sub_agent/web.py +8 -52
  63. klaude_code/protocol/tools.py +2 -0
  64. klaude_code/session/export.py +308 -299
  65. klaude_code/session/session.py +58 -21
  66. klaude_code/session/store.py +0 -4
  67. klaude_code/session/templates/export_session.html +430 -134
  68. klaude_code/skill/assets/deslop/SKILL.md +9 -0
  69. klaude_code/skill/system_skills.py +0 -20
  70. klaude_code/tui/command/__init__.py +3 -0
  71. klaude_code/tui/command/continue_cmd.py +34 -0
  72. klaude_code/tui/command/fork_session_cmd.py +5 -2
  73. klaude_code/tui/command/resume_cmd.py +9 -2
  74. klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
  75. klaude_code/tui/components/assistant.py +0 -26
  76. klaude_code/tui/components/command_output.py +3 -1
  77. klaude_code/tui/components/developer.py +3 -0
  78. klaude_code/tui/components/diffs.py +2 -208
  79. klaude_code/tui/components/errors.py +4 -0
  80. klaude_code/tui/components/mermaid_viewer.py +2 -2
  81. klaude_code/tui/components/rich/markdown.py +60 -63
  82. klaude_code/tui/components/rich/theme.py +2 -0
  83. klaude_code/tui/components/sub_agent.py +2 -46
  84. klaude_code/tui/components/thinking.py +0 -33
  85. klaude_code/tui/components/tools.py +43 -21
  86. klaude_code/tui/input/images.py +21 -18
  87. klaude_code/tui/input/key_bindings.py +2 -2
  88. klaude_code/tui/input/prompt_toolkit.py +49 -49
  89. klaude_code/tui/machine.py +15 -11
  90. klaude_code/tui/renderer.py +12 -20
  91. klaude_code/tui/runner.py +2 -1
  92. klaude_code/tui/terminal/image.py +6 -34
  93. klaude_code/ui/common.py +0 -70
  94. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
  95. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/RECORD +97 -92
  96. klaude_code/core/tool/sub_agent_tool.py +0 -126
  97. klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
  98. klaude_code/tui/components/rich/searchable_text.py +0 -68
  99. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
  100. {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 italic"
65
- PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a italic"
66
- PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG = "fg:#8a8a8a italic"
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
- original_height_int = original_height_value if isinstance(original_height_value, int) else None
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 or completion_open:
438
- target_rows = 24 if picker_open else 14
448
+ target_rows = 24 if picker_open else base_rows
439
449
 
440
- # Cap to the current terminal size.
441
- # Leave a small buffer to avoid triggering "Window too small".
442
- try:
443
- rows = get_app().output.get_size().rows
444
- except Exception:
445
- rows = 0
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
- expanded = max(3, min(target_rows, rows - 2))
448
- if original_height_int is not None:
449
- expanded = max(original_height_int, expanded)
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
- if callable(original_height):
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
- # If nothing to show, return a blank line to actively clear any previously
597
- # rendered content. (When `bottom_toolbar` is a callable, prompt_toolkit
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
- terminal_width = 0
604
- return FormattedText([("", " " * max(0, terminal_width))])
609
+ padding = ""
605
610
 
606
- left_text = " " + display_text
607
- try:
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
- toolbar_text = left_text + padding
614
- return FormattedText([(text_style, toolbar_text)])
614
+ # Show shortcut hints when nothing else to display
615
+ return self._render_shortcut_hints()
615
616
 
616
617
  # -------------------------------------------------------------------------
617
- # Placeholder
618
+ # Shortcut hints (bottom toolbar)
618
619
  # -------------------------------------------------------------------------
619
620
 
620
- def _render_input_placeholder(self) -> FormattedText:
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, " " * 10),
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:
@@ -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 == "ImageGen")
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
- tool_active_form = get_tool_active_form(e.tool_name)
611
- if is_sub_agent_tool(e.tool_name):
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
- self._spinner.add_tool_call(tool_active_form)
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, get_tool_active_form(e.tool_name))
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:
@@ -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
- renderable = c_tools.render_tool_result(e, code_theme=self.themes.code_theme, session_id=e.session_id)
420
- if renderable is not None:
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
- def display_thinking(self, content: str) -> None:
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.LINES))
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 --resume {active_session_id}")
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 non-wide images
16
- _MAX_COLS = 120
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
- This intentionally bypasses Rich rendering to avoid interleaving Live refreshes
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
- dimensions = _get_png_dimensions(data)
84
-
85
- # Determine sizing strategy based on aspect ratio
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.8.0
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, Claude Max OAuth and ChatGPT Codex OAuth etc.
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)