klaude-code 2.7.0__py3-none-any.whl → 2.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. klaude_code/auth/AGENTS.md +325 -0
  2. klaude_code/auth/__init__.py +17 -1
  3. klaude_code/auth/antigravity/__init__.py +20 -0
  4. klaude_code/auth/antigravity/exceptions.py +17 -0
  5. klaude_code/auth/antigravity/oauth.py +320 -0
  6. klaude_code/auth/antigravity/pkce.py +25 -0
  7. klaude_code/auth/antigravity/token_manager.py +45 -0
  8. klaude_code/auth/base.py +4 -0
  9. klaude_code/auth/claude/oauth.py +29 -9
  10. klaude_code/auth/codex/exceptions.py +4 -0
  11. klaude_code/cli/auth_cmd.py +53 -3
  12. klaude_code/cli/cost_cmd.py +83 -160
  13. klaude_code/cli/list_model.py +50 -0
  14. klaude_code/cli/main.py +1 -1
  15. klaude_code/config/assets/builtin_config.yaml +108 -0
  16. klaude_code/config/builtin_config.py +5 -11
  17. klaude_code/config/config.py +24 -10
  18. klaude_code/const.py +1 -0
  19. klaude_code/core/agent.py +5 -1
  20. klaude_code/core/agent_profile.py +28 -32
  21. klaude_code/core/compaction/AGENTS.md +112 -0
  22. klaude_code/core/compaction/__init__.py +11 -0
  23. klaude_code/core/compaction/compaction.py +707 -0
  24. klaude_code/core/compaction/overflow.py +30 -0
  25. klaude_code/core/compaction/prompts.py +97 -0
  26. klaude_code/core/executor.py +103 -2
  27. klaude_code/core/manager/llm_clients.py +5 -0
  28. klaude_code/core/manager/llm_clients_builder.py +14 -2
  29. klaude_code/core/prompts/prompt-antigravity.md +80 -0
  30. klaude_code/core/prompts/prompt-codex-gpt-5-2.md +335 -0
  31. klaude_code/core/reminders.py +7 -2
  32. klaude_code/core/task.py +126 -0
  33. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  34. klaude_code/core/turn.py +3 -1
  35. klaude_code/llm/antigravity/__init__.py +3 -0
  36. klaude_code/llm/antigravity/client.py +558 -0
  37. klaude_code/llm/antigravity/input.py +261 -0
  38. klaude_code/llm/registry.py +1 -0
  39. klaude_code/protocol/events.py +18 -0
  40. klaude_code/protocol/llm_param.py +1 -0
  41. klaude_code/protocol/message.py +23 -1
  42. klaude_code/protocol/op.py +15 -1
  43. klaude_code/protocol/op_handler.py +5 -0
  44. klaude_code/session/session.py +36 -0
  45. klaude_code/skill/assets/create-plan/SKILL.md +6 -6
  46. klaude_code/tui/command/__init__.py +3 -0
  47. klaude_code/tui/command/compact_cmd.py +32 -0
  48. klaude_code/tui/command/fork_session_cmd.py +110 -14
  49. klaude_code/tui/command/model_picker.py +5 -1
  50. klaude_code/tui/command/thinking_cmd.py +1 -1
  51. klaude_code/tui/commands.py +6 -0
  52. klaude_code/tui/components/rich/markdown.py +57 -1
  53. klaude_code/tui/components/rich/theme.py +10 -2
  54. klaude_code/tui/components/tools.py +39 -25
  55. klaude_code/tui/components/user_input.py +1 -1
  56. klaude_code/tui/input/__init__.py +5 -2
  57. klaude_code/tui/input/drag_drop.py +6 -57
  58. klaude_code/tui/input/key_bindings.py +10 -0
  59. klaude_code/tui/input/prompt_toolkit.py +19 -6
  60. klaude_code/tui/machine.py +25 -0
  61. klaude_code/tui/renderer.py +67 -4
  62. klaude_code/tui/runner.py +18 -2
  63. klaude_code/tui/terminal/image.py +72 -10
  64. klaude_code/tui/terminal/selector.py +31 -7
  65. {klaude_code-2.7.0.dist-info → klaude_code-2.8.0.dist-info}/METADATA +1 -1
  66. {klaude_code-2.7.0.dist-info → klaude_code-2.8.0.dist-info}/RECORD +68 -52
  67. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -117
  68. {klaude_code-2.7.0.dist-info → klaude_code-2.8.0.dist-info}/WHEEL +0 -0
  69. {klaude_code-2.7.0.dist-info → klaude_code-2.8.0.dist-info}/entry_points.txt +0 -0
@@ -53,6 +53,7 @@ class REPLStatusSnapshot(NamedTuple):
53
53
  """Snapshot of REPL status for bottom toolbar display."""
54
54
 
55
55
  update_message: str | None = None
56
+ debug_log_path: str | None = None
56
57
 
57
58
 
58
59
  COMPLETION_SELECTED_DARK_BG = "ansigreen"
@@ -434,7 +435,7 @@ class PromptToolkitInput(InputProviderABC):
434
435
  original_height_int = original_height_value if isinstance(original_height_value, int) else None
435
436
 
436
437
  if picker_open or completion_open:
437
- target_rows = 20 if picker_open else 14
438
+ target_rows = 24 if picker_open else 14
438
439
 
439
440
  # Cap to the current terminal size.
440
441
  # Leave a small buffer to avoid triggering "Window too small".
@@ -531,7 +532,7 @@ class PromptToolkitInput(InputProviderABC):
531
532
  return [], None
532
533
 
533
534
  items: list[SelectItem[str]] = [
534
- SelectItem(title=[("class:text", opt.label + "\n")], value=opt.value, search_text=opt.label)
535
+ SelectItem(title=[("class:msg", opt.label + "\n")], value=opt.value, search_text=opt.label)
535
536
  for opt in data.options
536
537
  ]
537
538
  return items, data.current_value
@@ -573,24 +574,36 @@ class PromptToolkitInput(InputProviderABC):
573
574
  doing any blocking IO here.
574
575
  """
575
576
  update_message: str | None = None
577
+ debug_log_path: str | None = None
576
578
  if self._status_provider is not None:
577
579
  try:
578
580
  status = self._status_provider()
579
581
  update_message = status.update_message
582
+ debug_log_path = status.debug_log_path
580
583
  except (AttributeError, RuntimeError):
581
- update_message = None
584
+ pass
585
+
586
+ # Priority: update_message > debug_log_path
587
+ display_text: str | None = None
588
+ text_style: str = ""
589
+ if update_message:
590
+ display_text = update_message
591
+ text_style = "#ansiyellow"
592
+ elif debug_log_path:
593
+ display_text = f"Debug log: {debug_log_path}"
594
+ text_style = "fg:ansibrightblack"
582
595
 
583
596
  # If nothing to show, return a blank line to actively clear any previously
584
597
  # rendered content. (When `bottom_toolbar` is a callable, prompt_toolkit
585
598
  # will still reserve the toolbar line.)
586
- if not update_message:
599
+ if not display_text:
587
600
  try:
588
601
  terminal_width = shutil.get_terminal_size().columns
589
602
  except (OSError, ValueError):
590
603
  terminal_width = 0
591
604
  return FormattedText([("", " " * max(0, terminal_width))])
592
605
 
593
- left_text = " " + update_message
606
+ left_text = " " + display_text
594
607
  try:
595
608
  terminal_width = shutil.get_terminal_size().columns
596
609
  padding = " " * max(0, terminal_width - len(left_text))
@@ -598,7 +611,7 @@ class PromptToolkitInput(InputProviderABC):
598
611
  padding = ""
599
612
 
600
613
  toolbar_text = left_text + padding
601
- return FormattedText([("#ansiyellow", toolbar_text)])
614
+ return FormattedText([(text_style, toolbar_text)])
602
615
 
603
616
  # -------------------------------------------------------------------------
604
617
  # Placeholder
@@ -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,
@@ -311,6 +313,7 @@ class _SessionState:
311
313
  thinking_stream_active: bool = False
312
314
  assistant_char_count: int = 0
313
315
  thinking_tail: str = ""
316
+ task_active: bool = False
314
317
 
315
318
  @property
316
319
  def is_sub_agent(self) -> bool:
@@ -414,6 +417,7 @@ class DisplayStateMachine:
414
417
  case events.TaskStartEvent() as e:
415
418
  s.sub_agent_state = e.sub_agent_state
416
419
  s.model_id = e.model_id
420
+ s.task_active = True
417
421
  if not s.is_sub_agent:
418
422
  self._set_primary_if_needed(e.session_id)
419
423
  if not is_replay:
@@ -428,6 +432,25 @@ class DisplayStateMachine:
428
432
  cmds.extend(self._spinner_update_commands())
429
433
  return cmds
430
434
 
435
+ case events.CompactionStartEvent():
436
+ if not is_replay:
437
+ self._spinner.set_reasoning_status(STATUS_COMPACTING_TEXT)
438
+ if not s.task_active:
439
+ cmds.append(SpinnerStart())
440
+ cmds.extend(self._spinner_update_commands())
441
+ return cmds
442
+
443
+ case events.CompactionEndEvent() as e:
444
+ if not is_replay:
445
+ self._spinner.set_reasoning_status(None)
446
+ if not s.task_active:
447
+ cmds.append(SpinnerStop())
448
+ cmds.extend(self._spinner_update_commands())
449
+ if e.summary and not e.aborted:
450
+ kept_brief = tuple((item.item_type, item.count, item.preview) for item in e.kept_items_brief)
451
+ cmds.append(RenderCompactionSummary(summary=e.summary, kept_items_brief=kept_brief))
452
+ return cmds
453
+
431
454
  case events.DeveloperMessageEvent() as e:
432
455
  cmds.append(RenderDeveloperMessage(e))
433
456
  return cmds
@@ -650,6 +673,7 @@ class DisplayStateMachine:
650
673
  return []
651
674
 
652
675
  case events.TaskFinishEvent() as e:
676
+ s.task_active = False
653
677
  cmds.append(RenderTaskFinish(e))
654
678
  if not s.is_sub_agent:
655
679
  if not is_replay:
@@ -666,6 +690,7 @@ class DisplayStateMachine:
666
690
  if not is_replay:
667
691
  self._spinner.reset()
668
692
  cmds.append(SpinnerStop())
693
+ s.task_active = False
669
694
  cmds.append(EndThinkingStream(session_id=e.session_id))
670
695
  cmds.append(EndAssistantStream(session_id=e.session_id))
671
696
  if not is_replay:
@@ -1,13 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import contextlib
4
+ import shutil
4
5
  from collections.abc import Callable, Iterator
5
6
  from contextlib import contextmanager
6
7
  from dataclasses import dataclass
7
8
  from typing import Any
8
9
 
10
+ from rich import box
9
11
  from rich.console import Console, Group, RenderableType
10
12
  from rich.padding import Padding
13
+ from rich.panel import Panel
11
14
  from rich.rule import Rule
12
15
  from rich.spinner import Spinner
13
16
  from rich.style import Style, StyleType
@@ -32,6 +35,7 @@ from klaude_code.tui.commands import (
32
35
  RenderAssistantImage,
33
36
  RenderCommand,
34
37
  RenderCommandOutput,
38
+ RenderCompactionSummary,
35
39
  RenderDeveloperMessage,
36
40
  RenderError,
37
41
  RenderInterrupt,
@@ -66,7 +70,7 @@ from klaude_code.tui.components import welcome as c_welcome
66
70
  from klaude_code.tui.components.common import truncate_head
67
71
  from klaude_code.tui.components.rich import status as r_status
68
72
  from klaude_code.tui.components.rich.live import CropAboveLive, SingleLine
69
- from klaude_code.tui.components.rich.markdown import MarkdownStream, ThinkingMarkdown
73
+ from klaude_code.tui.components.rich.markdown import MarkdownStream, NoInsetMarkdown, ThinkingMarkdown
70
74
  from klaude_code.tui.components.rich.quote import Quote
71
75
  from klaude_code.tui.components.rich.status import BreathingSpinner, ShimmerStatusText
72
76
  from klaude_code.tui.components.rich.theme import ThemeKey, get_theme
@@ -410,7 +414,7 @@ class TUICommandRenderer:
410
414
  session_id=e.session_id,
411
415
  )
412
416
  if image_path is not None:
413
- self.display_image(str(image_path), height=None)
417
+ self.display_image(str(image_path))
414
418
 
415
419
  renderable = c_tools.render_tool_result(e, code_theme=self.themes.code_theme, session_id=e.session_id)
416
420
  if renderable is not None:
@@ -472,7 +476,7 @@ class TUICommandRenderer:
472
476
  if not self.is_sub_agent_session(event.session_id):
473
477
  self.print()
474
478
 
475
- def display_image(self, file_path: str, height: int | None = 40) -> None:
479
+ def display_image(self, file_path: str) -> None:
476
480
  # Suspend the Live status bar while emitting raw terminal output.
477
481
  had_live = self._bottom_live is not None
478
482
  was_spinner_visible = self._spinner_visible
@@ -485,7 +489,7 @@ class TUICommandRenderer:
485
489
  self._bottom_live = None
486
490
 
487
491
  try:
488
- print_kitty_image(file_path, height=height, file=self.console.file)
492
+ print_kitty_image(file_path, file=self.console.file)
489
493
  finally:
490
494
  if resume_live:
491
495
  if was_spinner_visible:
@@ -524,6 +528,63 @@ class TUICommandRenderer:
524
528
  else:
525
529
  self.print(c_errors.render_error(Text(event.error_message)))
526
530
 
531
+ def display_compaction_summary(self, summary: str, kept_items_brief: tuple[tuple[str, int, str], ...] = ()) -> None:
532
+ stripped = summary.strip()
533
+ if not stripped:
534
+ return
535
+ stripped = (
536
+ stripped.replace("<summary>", "")
537
+ .replace("</summary>", "")
538
+ .replace("<read_files>", "")
539
+ .replace("</read_files>", "")
540
+ .replace("<modified-files>", "")
541
+ .replace("</modified-files>", "")
542
+ )
543
+ self.console.print(
544
+ Rule(
545
+ Text("Context Compact", style=ThemeKey.COMPACTION_SUMMARY),
546
+ characters="=",
547
+ style=ThemeKey.COMPACTION_SUMMARY,
548
+ )
549
+ )
550
+ self.print()
551
+
552
+ # Limit panel width to min(100, terminal_width) minus left indent (2)
553
+ terminal_width = shutil.get_terminal_size().columns
554
+ panel_width = min(100, terminal_width) - 2
555
+
556
+ self.console.push_theme(self.themes.markdown_theme)
557
+ panel = Panel(
558
+ NoInsetMarkdown(stripped, code_theme=self.themes.code_theme, style=ThemeKey.COMPACTION_SUMMARY),
559
+ box=box.SIMPLE,
560
+ border_style=ThemeKey.LINES,
561
+ style=ThemeKey.COMPACTION_SUMMARY_PANEL,
562
+ width=panel_width,
563
+ )
564
+ self.print(Padding(panel, (0, 0, 0, MARKDOWN_LEFT_MARGIN)))
565
+ self.console.pop_theme()
566
+
567
+ if kept_items_brief:
568
+ # Collect tool call counts (skip User/Assistant entries)
569
+ tool_counts: dict[str, int] = {}
570
+ for item_type, count, _ in kept_items_brief:
571
+ if item_type not in ("User", "Assistant"):
572
+ tool_counts[item_type] = tool_counts.get(item_type, 0) + count
573
+
574
+ if tool_counts:
575
+ parts: list[str] = []
576
+ for tool_type, tool_count in tool_counts.items():
577
+ if tool_count > 1:
578
+ parts.append(f"{tool_type} x {tool_count}")
579
+ else:
580
+ parts.append(tool_type)
581
+ line = Text()
582
+ line.append("\n Kept uncompacted: ", style=ThemeKey.COMPACTION_SUMMARY)
583
+ line.append(", ".join(parts), style=ThemeKey.COMPACTION_SUMMARY)
584
+ self.print(line)
585
+
586
+ self.print()
587
+
527
588
  # ---------------------------------------------------------------------
528
589
  # Notifications
529
590
  # ---------------------------------------------------------------------
@@ -617,6 +678,8 @@ class TUICommandRenderer:
617
678
  self.display_interrupt()
618
679
  case RenderError(event=event):
619
680
  self.display_error(event)
681
+ case RenderCompactionSummary(summary=summary, kept_items_brief=kept_items_brief):
682
+ self.display_compaction_summary(summary, kept_items_brief)
620
683
  case SpinnerStart():
621
684
  self.spinner_start()
622
685
  case SpinnerStop():
klaude_code/tui/runner.py CHANGED
@@ -16,8 +16,9 @@ from klaude_code.app.runtime import (
16
16
  )
17
17
  from klaude_code.config import load_config
18
18
  from klaude_code.const import SIGINT_DOUBLE_PRESS_EXIT_TEXT
19
+ from klaude_code.core.compaction import should_compact_threshold
19
20
  from klaude_code.core.executor import Executor
20
- from klaude_code.log import log
21
+ from klaude_code.log import get_current_log_file, log
21
22
  from klaude_code.protocol import events, llm_param, op
22
23
  from klaude_code.protocol.message import UserInputPayload
23
24
  from klaude_code.session.session import Session
@@ -80,6 +81,19 @@ async def submit_user_input_payload(
80
81
  for evt in cmd_result.events:
81
82
  await executor.context.emit_event(evt)
82
83
 
84
+ if run_ops and should_compact_threshold(
85
+ session=agent.session,
86
+ config=None,
87
+ llm_config=agent.profile.llm_client.get_llm_config(),
88
+ ):
89
+ await executor.submit_and_wait(
90
+ op.CompactSessionOperation(
91
+ session_id=agent.session.id,
92
+ reason="threshold",
93
+ will_retry=False,
94
+ )
95
+ )
96
+
83
97
  submitted_ids: list[str] = []
84
98
  for operation_item in operations:
85
99
  submitted_ids.append(await executor.submit(operation_item))
@@ -124,7 +138,9 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
124
138
 
125
139
  def _status_provider() -> REPLStatusSnapshot:
126
140
  update_message = get_update_message()
127
- return build_repl_status_snapshot(update_message)
141
+ debug_log = get_current_log_file()
142
+ debug_log_path = str(debug_log) if debug_log else None
143
+ return build_repl_status_snapshot(update_message, debug_log_path=debug_log_path)
128
144
 
129
145
  def _stop_rich_bottom_ui() -> None:
130
146
  active_display = components.display
@@ -1,23 +1,64 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import base64
4
+ import shutil
5
+ import struct
6
+ import subprocess
4
7
  import sys
8
+ import tempfile
5
9
  from pathlib import Path
6
10
  from typing import IO
7
11
 
8
12
  # Kitty graphics protocol chunk size (4096 is the recommended max)
9
13
  _CHUNK_SIZE = 4096
10
14
 
15
+ # Max columns for non-wide images
16
+ _MAX_COLS = 120
11
17
 
12
- def print_kitty_image(file_path: str | Path, *, height: int | None = None, file: IO[str] | None = None) -> None:
18
+ # Image formats that need conversion to PNG
19
+ _NEEDS_CONVERSION = {".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff", ".tif"}
20
+
21
+
22
+ def _convert_to_png(path: Path) -> bytes | None:
23
+ """Convert image to PNG using sips (macOS) or convert (ImageMagick)."""
24
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as tmp:
25
+ tmp_path = tmp.name
26
+ # Try sips first (macOS built-in)
27
+ result = subprocess.run(
28
+ ["sips", "-s", "format", "png", str(path), "--out", tmp_path],
29
+ capture_output=True,
30
+ )
31
+ if result.returncode == 0:
32
+ return Path(tmp_path).read_bytes()
33
+ # Fallback to ImageMagick convert
34
+ result = subprocess.run(
35
+ ["convert", str(path), tmp_path],
36
+ capture_output=True,
37
+ )
38
+ if result.returncode == 0:
39
+ return Path(tmp_path).read_bytes()
40
+ return None
41
+
42
+
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
+ def print_kitty_image(file_path: str | Path, *, file: IO[str] | None = None) -> None:
13
53
  """Print an image to the terminal using Kitty graphics protocol.
14
54
 
15
55
  This intentionally bypasses Rich rendering to avoid interleaving Live refreshes
16
- with raw escape sequences.
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
17
59
 
18
60
  Args:
19
61
  file_path: Path to the image file (PNG recommended).
20
- height: Display height in terminal rows. If None, uses terminal default.
21
62
  file: Output file stream. Defaults to stdout.
22
63
  """
23
64
  path = Path(file_path) if isinstance(file_path, str) else file_path
@@ -26,24 +67,48 @@ def print_kitty_image(file_path: str | Path, *, height: int | None = None, file:
26
67
  return
27
68
 
28
69
  try:
29
- data = path.read_bytes()
70
+ # Convert non-PNG formats to PNG for Kitty graphics protocol compatibility
71
+ if path.suffix.lower() in _NEEDS_CONVERSION:
72
+ data = _convert_to_png(path)
73
+ if data is None:
74
+ print(f"Saved image: {path}", file=file or sys.stdout, flush=True)
75
+ return
76
+ else:
77
+ data = path.read_bytes()
78
+
30
79
  encoded = base64.standard_b64encode(data).decode("ascii")
31
80
  out = file or sys.stdout
32
81
 
82
+ 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)}"
33
97
  print("", file=out)
34
- _write_kitty_graphics(out, encoded, height=height)
98
+ _write_kitty_graphics(out, encoded, size_param=size_param)
35
99
  print("", file=out)
36
100
  out.flush()
37
101
  except Exception:
38
102
  print(f"Saved image: {path}", file=file or sys.stdout, flush=True)
39
103
 
40
104
 
41
- def _write_kitty_graphics(out: IO[str], encoded_data: str, *, height: int | None = None) -> None:
105
+ def _write_kitty_graphics(out: IO[str], encoded_data: str, *, size_param: str) -> None:
42
106
  """Write Kitty graphics protocol escape sequences.
43
107
 
44
108
  Protocol format: ESC _ G <control>;<payload> ESC \\
45
109
  - a=T: direct transmission (data in payload)
46
110
  - f=100: PNG format (auto-detected by Kitty)
111
+ - c=N: display width in columns
47
112
  - r=N: display height in rows
48
113
  - m=1: more data follows, m=0: last chunk
49
114
  """
@@ -55,10 +120,7 @@ def _write_kitty_graphics(out: IO[str], encoded_data: str, *, height: int | None
55
120
 
56
121
  if i == 0:
57
122
  # 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}"
123
+ ctrl = f"a=T,f=100,{size_param},m={0 if is_last else 1}"
62
124
  out.write(f"\033_G{ctrl};{chunk}\033\\")
63
125
  else:
64
126
  # Subsequent chunks: only m parameter needed
@@ -81,13 +81,13 @@ def build_model_select_items(models: list[Any]) -> list[SelectItem[str]]:
81
81
 
82
82
  items: list[SelectItem[str]] = []
83
83
  model_idx = 0
84
- separator_base_len = 40
84
+ separator_base_len = 80
85
85
  for provider, provider_models in grouped.items():
86
86
  provider_text = provider.lower()
87
87
  count_text = f"({len(provider_models)})"
88
88
  header_len = len(provider_text) + 1 + len(count_text)
89
89
  separator_len = separator_base_len + max_header_len - header_len
90
- separator = "" * separator_len
90
+ separator = "-" * separator_len
91
91
  items.append(
92
92
  SelectItem(
93
93
  title=[
@@ -574,7 +574,10 @@ def select_one[T](
574
574
  )
575
575
  list_window = Window(
576
576
  FormattedTextControl(get_choices_tokens),
577
- scroll_offsets=ScrollOffsets(top=0, bottom=2),
577
+ # Keep 1 line of context above the cursor so non-selectable header rows
578
+ # (e.g. provider group labels) remain visible when wrapping back to the
579
+ # first selectable item in a scrolled list.
580
+ scroll_offsets=ScrollOffsets(top=1, bottom=2),
578
581
  allow_scroll_beyond_bottom=True,
579
582
  dont_extend_height=Always(),
580
583
  always_hide_cursor=Always(),
@@ -796,6 +799,7 @@ class SelectOverlay[T]:
796
799
  dont_extend_height=Always(),
797
800
  always_hide_cursor=Always(),
798
801
  )
802
+
799
803
  def get_list_height() -> int:
800
804
  # Dynamic height: min of configured height and available terminal space
801
805
  # Overhead: header(1) + spacer(1) + search(1) + frame borders(2) + prompt area(3)
@@ -803,15 +807,35 @@ class SelectOverlay[T]:
803
807
  try:
804
808
  terminal_height = get_app().output.get_size().rows
805
809
  available = max(3, terminal_height - overhead)
806
- return min(self._list_height, available)
810
+ cap = min(self._list_height, available)
807
811
  except Exception:
808
- return self._list_height
812
+ cap = self._list_height
813
+
814
+ # Shrink list height when content is shorter than the configured cap.
815
+ # This is especially helpful for small pickers (e.g. thinking level)
816
+ # where a fixed list_height would otherwise render extra blank rows.
817
+ indices, _ = self._get_visible_indices()
818
+ if not indices:
819
+ return max(1, cap)
820
+
821
+ visible_lines = 0
822
+ for idx in indices:
823
+ item = self._items[idx]
824
+ newlines = sum(text.count("\n") for _style, text in item.title)
825
+ visible_lines += max(1, newlines)
826
+ if visible_lines >= cap:
827
+ break
828
+
829
+ return max(1, min(cap, visible_lines))
809
830
 
810
831
  list_window = Window(
811
832
  FormattedTextControl(get_choices_tokens),
812
833
  height=get_list_height,
813
- scroll_offsets=ScrollOffsets(top=0, bottom=2),
814
- allow_scroll_beyond_bottom=True,
834
+ # See select_one(): keep header rows visible when wrapping.
835
+ # For embedded overlays, avoid reserving extra blank lines near the
836
+ # bottom when the list height is tight (e.g. short pickers).
837
+ scroll_offsets=ScrollOffsets(top=1, bottom=0),
838
+ allow_scroll_beyond_bottom=False,
815
839
  dont_extend_height=Always(),
816
840
  always_hide_cursor=Always(),
817
841
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 2.7.0
3
+ Version: 2.8.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0