klaude-code 1.9.0__py3-none-any.whl → 2.0.1__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 (132) hide show
  1. klaude_code/auth/base.py +2 -6
  2. klaude_code/cli/auth_cmd.py +4 -4
  3. klaude_code/cli/cost_cmd.py +1 -1
  4. klaude_code/cli/list_model.py +1 -1
  5. klaude_code/cli/main.py +1 -1
  6. klaude_code/cli/runtime.py +7 -5
  7. klaude_code/cli/self_update.py +1 -1
  8. klaude_code/cli/session_cmd.py +1 -1
  9. klaude_code/command/clear_cmd.py +6 -2
  10. klaude_code/command/command_abc.py +2 -2
  11. klaude_code/command/debug_cmd.py +4 -4
  12. klaude_code/command/export_cmd.py +2 -2
  13. klaude_code/command/export_online_cmd.py +12 -12
  14. klaude_code/command/fork_session_cmd.py +29 -23
  15. klaude_code/command/help_cmd.py +4 -4
  16. klaude_code/command/model_cmd.py +4 -4
  17. klaude_code/command/model_select.py +1 -1
  18. klaude_code/command/prompt-commit.md +11 -2
  19. klaude_code/command/prompt_command.py +3 -3
  20. klaude_code/command/refresh_cmd.py +2 -2
  21. klaude_code/command/registry.py +7 -5
  22. klaude_code/command/release_notes_cmd.py +4 -4
  23. klaude_code/command/resume_cmd.py +15 -11
  24. klaude_code/command/status_cmd.py +4 -4
  25. klaude_code/command/terminal_setup_cmd.py +8 -8
  26. klaude_code/command/thinking_cmd.py +4 -4
  27. klaude_code/config/assets/builtin_config.yaml +20 -0
  28. klaude_code/config/builtin_config.py +16 -5
  29. klaude_code/config/config.py +7 -2
  30. klaude_code/const.py +147 -91
  31. klaude_code/core/agent.py +3 -12
  32. klaude_code/core/executor.py +18 -39
  33. klaude_code/core/manager/sub_agent_manager.py +71 -7
  34. klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
  35. klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
  36. klaude_code/core/reminders.py +88 -69
  37. klaude_code/core/task.py +44 -45
  38. klaude_code/core/tool/file/apply_patch_tool.py +9 -9
  39. klaude_code/core/tool/file/diff_builder.py +3 -5
  40. klaude_code/core/tool/file/edit_tool.py +23 -23
  41. klaude_code/core/tool/file/move_tool.py +43 -43
  42. klaude_code/core/tool/file/read_tool.py +44 -39
  43. klaude_code/core/tool/file/write_tool.py +14 -14
  44. klaude_code/core/tool/report_back_tool.py +4 -4
  45. klaude_code/core/tool/shell/bash_tool.py +23 -23
  46. klaude_code/core/tool/skill/skill_tool.py +7 -7
  47. klaude_code/core/tool/sub_agent_tool.py +38 -9
  48. klaude_code/core/tool/todo/todo_write_tool.py +9 -10
  49. klaude_code/core/tool/todo/update_plan_tool.py +6 -6
  50. klaude_code/core/tool/tool_abc.py +2 -2
  51. klaude_code/core/tool/tool_context.py +27 -0
  52. klaude_code/core/tool/tool_runner.py +88 -42
  53. klaude_code/core/tool/truncation.py +38 -20
  54. klaude_code/core/tool/web/mermaid_tool.py +6 -7
  55. klaude_code/core/tool/web/web_fetch_tool.py +68 -30
  56. klaude_code/core/tool/web/web_search_tool.py +15 -17
  57. klaude_code/core/turn.py +120 -73
  58. klaude_code/llm/anthropic/client.py +79 -44
  59. klaude_code/llm/anthropic/input.py +116 -108
  60. klaude_code/llm/bedrock/client.py +8 -5
  61. klaude_code/llm/claude/client.py +18 -8
  62. klaude_code/llm/client.py +4 -3
  63. klaude_code/llm/codex/client.py +15 -9
  64. klaude_code/llm/google/client.py +122 -60
  65. klaude_code/llm/google/input.py +94 -108
  66. klaude_code/llm/image.py +123 -0
  67. klaude_code/llm/input_common.py +136 -189
  68. klaude_code/llm/openai_compatible/client.py +17 -7
  69. klaude_code/llm/openai_compatible/input.py +36 -66
  70. klaude_code/llm/openai_compatible/stream.py +119 -67
  71. klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
  72. klaude_code/llm/openrouter/client.py +34 -9
  73. klaude_code/llm/openrouter/input.py +63 -64
  74. klaude_code/llm/openrouter/reasoning.py +22 -24
  75. klaude_code/llm/registry.py +20 -17
  76. klaude_code/llm/responses/client.py +107 -45
  77. klaude_code/llm/responses/input.py +115 -98
  78. klaude_code/llm/usage.py +52 -25
  79. klaude_code/protocol/__init__.py +1 -0
  80. klaude_code/protocol/events.py +16 -12
  81. klaude_code/protocol/llm_param.py +20 -2
  82. klaude_code/protocol/message.py +250 -0
  83. klaude_code/protocol/model.py +95 -285
  84. klaude_code/protocol/op.py +2 -15
  85. klaude_code/protocol/op_handler.py +0 -5
  86. klaude_code/protocol/sub_agent/__init__.py +1 -0
  87. klaude_code/protocol/sub_agent/explore.py +10 -0
  88. klaude_code/protocol/sub_agent/image_gen.py +119 -0
  89. klaude_code/protocol/sub_agent/task.py +10 -0
  90. klaude_code/protocol/sub_agent/web.py +10 -0
  91. klaude_code/session/codec.py +6 -6
  92. klaude_code/session/export.py +261 -62
  93. klaude_code/session/selector.py +7 -24
  94. klaude_code/session/session.py +126 -54
  95. klaude_code/session/store.py +5 -32
  96. klaude_code/session/templates/export_session.html +1 -1
  97. klaude_code/session/templates/mermaid_viewer.html +1 -1
  98. klaude_code/trace/log.py +11 -6
  99. klaude_code/ui/core/input.py +1 -1
  100. klaude_code/ui/core/stage_manager.py +1 -8
  101. klaude_code/ui/modes/debug/display.py +2 -2
  102. klaude_code/ui/modes/repl/clipboard.py +2 -2
  103. klaude_code/ui/modes/repl/completers.py +18 -10
  104. klaude_code/ui/modes/repl/event_handler.py +138 -132
  105. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  106. klaude_code/ui/modes/repl/key_bindings.py +136 -2
  107. klaude_code/ui/modes/repl/renderer.py +107 -15
  108. klaude_code/ui/renderers/assistant.py +2 -2
  109. klaude_code/ui/renderers/bash_syntax.py +36 -4
  110. klaude_code/ui/renderers/common.py +70 -10
  111. klaude_code/ui/renderers/developer.py +7 -6
  112. klaude_code/ui/renderers/diffs.py +11 -11
  113. klaude_code/ui/renderers/mermaid_viewer.py +49 -2
  114. klaude_code/ui/renderers/metadata.py +33 -5
  115. klaude_code/ui/renderers/sub_agent.py +57 -16
  116. klaude_code/ui/renderers/thinking.py +37 -2
  117. klaude_code/ui/renderers/tools.py +188 -178
  118. klaude_code/ui/rich/live.py +3 -1
  119. klaude_code/ui/rich/markdown.py +39 -7
  120. klaude_code/ui/rich/quote.py +76 -1
  121. klaude_code/ui/rich/status.py +14 -8
  122. klaude_code/ui/rich/theme.py +20 -14
  123. klaude_code/ui/terminal/image.py +34 -0
  124. klaude_code/ui/terminal/notifier.py +2 -1
  125. klaude_code/ui/terminal/progress_bar.py +4 -4
  126. klaude_code/ui/terminal/selector.py +22 -4
  127. klaude_code/ui/utils/common.py +11 -2
  128. {klaude_code-1.9.0.dist-info → klaude_code-2.0.1.dist-info}/METADATA +4 -2
  129. klaude_code-2.0.1.dist-info/RECORD +229 -0
  130. klaude_code-1.9.0.dist-info/RECORD +0 -224
  131. {klaude_code-1.9.0.dist-info → klaude_code-2.0.1.dist-info}/WHEEL +0 -0
  132. {klaude_code-1.9.0.dist-info → klaude_code-2.0.1.dist-info}/entry_points.txt +0 -0
@@ -7,6 +7,8 @@ from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
7
7
  from rich.live import Live
8
8
  from rich.segment import Segment
9
9
 
10
+ from klaude_code.const import CROP_ABOVE_LIVE_REFRESH_PER_SECOND
11
+
10
12
 
11
13
  class CropAbove:
12
14
  def __init__(self, renderable: RenderableType, style: str = "") -> None:
@@ -33,7 +35,7 @@ class CropAboveLive(Live):
33
35
  renderable: RenderableType | None = None,
34
36
  *,
35
37
  console: Console | None = None,
36
- refresh_per_second: float = 4,
38
+ refresh_per_second: float = CROP_ABOVE_LIVE_REFRESH_PER_SECOND,
37
39
  transient: bool = False,
38
40
  get_renderable: Any | None = None,
39
41
  style: str = "",
@@ -18,7 +18,7 @@ from rich.table import Table
18
18
  from rich.text import Text
19
19
  from rich.theme import Theme
20
20
 
21
- from klaude_code import const
21
+ from klaude_code.const import MARKDOWN_RIGHT_MARGIN, MARKDOWN_STREAM_LIVE_REPAINT_ENABLED, UI_REFRESH_RATE_FPS
22
22
  from klaude_code.ui.rich.code_panel import CodePanel
23
23
 
24
24
 
@@ -55,8 +55,6 @@ class Divider(MarkdownElement):
55
55
 
56
56
 
57
57
  class MarkdownTable(TableElement):
58
- """A table element with MINIMAL_HEAVY_HEAD box style."""
59
-
60
58
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
61
59
  table = Table(box=box.MARKDOWN, border_style=console.get_style("markdown.table.border"))
62
60
 
@@ -69,7 +67,27 @@ class MarkdownTable(TableElement):
69
67
  row_content = [element.content for element in row.cells]
70
68
  table.add_row(*row_content)
71
69
 
72
- yield table
70
+ # Render table and strip top/bottom blank lines that MARKDOWN box adds
71
+ segments = list(console.render(table, options))
72
+
73
+ # Skip leading blank line (before first newline)
74
+ first_newline_idx = next((i for i, s in enumerate(segments) if s.text == "\n"), None)
75
+ if first_newline_idx is not None:
76
+ first_line = "".join(s.text for s in segments[:first_newline_idx])
77
+ if not first_line.strip():
78
+ segments = segments[first_newline_idx + 1 :]
79
+
80
+ # Skip trailing blank line (after last newline)
81
+ while len(segments) >= 2 and segments[-1].text == "\n":
82
+ prev_newline = next((i for i in range(len(segments) - 2, -1, -1) if segments[i].text == "\n"), None)
83
+ if prev_newline is not None:
84
+ between = "".join(s.text for s in segments[prev_newline + 1 : -1])
85
+ if not between.strip():
86
+ segments = segments[: prev_newline + 1]
87
+ continue
88
+ break
89
+
90
+ yield from segments
73
91
 
74
92
 
75
93
  class LeftHeading(Heading):
@@ -135,7 +153,7 @@ class MarkdownStream:
135
153
  mark: str | None = None,
136
154
  mark_style: StyleType | None = None,
137
155
  left_margin: int = 0,
138
- right_margin: int = const.MARKDOWN_RIGHT_MARGIN,
156
+ right_margin: int = MARKDOWN_RIGHT_MARGIN,
139
157
  markdown_class: Callable[..., Markdown] | None = None,
140
158
  ) -> None:
141
159
  """Initialize the markdown stream.
@@ -162,7 +180,7 @@ class MarkdownStream:
162
180
 
163
181
  # Streaming control
164
182
  self.when: float = 0.0 # Timestamp of last update
165
- self.min_delay: float = 1.0 / 20 # Minimum time between updates (20fps)
183
+ self.min_delay: float = 1.0 / UI_REFRESH_RATE_FPS
166
184
  self._parser: MarkdownIt = MarkdownIt("commonmark")
167
185
 
168
186
  self.theme = theme
@@ -201,6 +219,20 @@ class MarkdownStream:
201
219
 
202
220
  last = top_level[-1]
203
221
  assert last.map is not None
222
+
223
+ # When the buffer ends mid-line, markdown-it-py can temporarily classify
224
+ # some lines as a thematic break (hr). For example, a trailing "- --"
225
+ # parses as an hr, but appending a non-hr character ("- --0") turns it
226
+ # into a list item, which should belong to the previous list block.
227
+ #
228
+ # Because stable_line is clamped to be monotonic, advancing to the hr's
229
+ # start line would be irreversible and can split a list across
230
+ # stable/live, producing a render mismatch.
231
+ if last.type == "hr" and not text.endswith("\n"):
232
+ prev = top_level[-2]
233
+ assert prev.map is not None
234
+ return max(prev.map[0], 0)
235
+
204
236
  start_line = last.map[0]
205
237
  return max(start_line, 0)
206
238
 
@@ -414,7 +446,7 @@ class MarkdownStream:
414
446
  self._live_sink(None)
415
447
  return
416
448
 
417
- if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._live_sink is not None:
449
+ if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._live_sink is not None:
418
450
  apply_mark_live = self._stable_source_line_count == 0
419
451
  live_lines = self._render_markdown_to_lines(live_source, apply_mark=apply_mark_live)
420
452
 
@@ -1,9 +1,12 @@
1
- from typing import Any
1
+ from typing import TYPE_CHECKING, Any, Self
2
2
 
3
3
  from rich.console import Console, ConsoleOptions, RenderResult
4
4
  from rich.segment import Segment
5
5
  from rich.style import Style
6
6
 
7
+ if TYPE_CHECKING:
8
+ from rich.console import RenderableType
9
+
7
10
 
8
11
  class Quote:
9
12
  """Wrapper to add quote prefix to any content"""
@@ -32,3 +35,75 @@ class Quote:
32
35
  yield prefix_segment
33
36
  yield from line
34
37
  yield new_line
38
+
39
+
40
+ class TreeQuote:
41
+ """Wrapper to add a tree-style prefix to any content."""
42
+
43
+ def __init__(
44
+ self,
45
+ content: Any,
46
+ *,
47
+ prefix_first: str | None = None,
48
+ prefix_middle: str = "│ ",
49
+ prefix_last: str = "└ ",
50
+ style: str | Style = "magenta",
51
+ style_first: str | Style | None = None,
52
+ ):
53
+ self.content = content
54
+ self.prefix_first = prefix_first
55
+ self.prefix_middle = prefix_middle
56
+ self.prefix_last = prefix_last
57
+ self.style = style
58
+ self.style_first = style_first
59
+
60
+ @classmethod
61
+ def for_tool_call(cls, content: "RenderableType", *, mark: str, style: str, style_first: str) -> Self:
62
+ """Create a tree quote for tool call display.
63
+
64
+ The mark appears on the first line, with continuation lines using "│ ".
65
+ """
66
+ return cls(
67
+ content,
68
+ prefix_first=f"{mark} ",
69
+ prefix_middle="│ ",
70
+ prefix_last="│ ",
71
+ style=style,
72
+ style_first=style_first,
73
+ )
74
+
75
+ @classmethod
76
+ def for_tool_result(
77
+ cls, content: "RenderableType", *, is_last: bool, style: str = "tool.result.tree_prefix"
78
+ ) -> Self:
79
+ """Create a tree quote for tool result display.
80
+
81
+ Uses "└ " for the last result in a turn, "│ " otherwise.
82
+ """
83
+ return cls(content, prefix_last="└ " if is_last else "│ ", style=style)
84
+
85
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
86
+ # Reduce width to leave space for prefix
87
+ prefix_width = max(
88
+ len(self.prefix_middle),
89
+ len(self.prefix_last),
90
+ len(self.prefix_first) if self.prefix_first is not None else 0,
91
+ )
92
+ render_options = options.update(width=options.max_width - prefix_width)
93
+
94
+ quote_style = console.get_style(self.style) if isinstance(self.style, str) else self.style
95
+ first_style = console.get_style(self.style_first) if isinstance(self.style_first, str) else self.style_first
96
+
97
+ new_line = Segment("\n")
98
+ lines = console.render_lines(self.content, render_options)
99
+ line_count = len(lines)
100
+
101
+ for idx, line in enumerate(lines):
102
+ if idx == 0 and self.prefix_first is not None:
103
+ yield Segment(self.prefix_first, first_style or quote_style)
104
+ else:
105
+ is_last = idx == line_count - 1
106
+ prefix = self.prefix_last if is_last else self.prefix_middle
107
+ yield Segment(prefix, quote_style)
108
+ yield from line
109
+ yield new_line
@@ -16,7 +16,13 @@ from rich.style import Style
16
16
  from rich.table import Table
17
17
  from rich.text import Text
18
18
 
19
- from klaude_code import const
19
+ from klaude_code.const import (
20
+ SPINNER_BREATH_PERIOD_SECONDS,
21
+ STATUS_HINT_TEXT,
22
+ STATUS_SHIMMER_ALPHA_SCALE,
23
+ STATUS_SHIMMER_BAND_HALF_WIDTH,
24
+ STATUS_SHIMMER_PADDING,
25
+ )
20
26
  from klaude_code.ui.rich.theme import ThemeKey
21
27
  from klaude_code.ui.terminal.color import get_last_terminal_background_rgb
22
28
 
@@ -91,7 +97,7 @@ def current_hint_text(*, min_time_width: int = 0) -> str:
91
97
 
92
98
  # Keep the signature stable; min_time_width is intentionally ignored.
93
99
  _ = min_time_width
94
- return const.STATUS_HINT_TEXT
100
+ return STATUS_HINT_TEXT
95
101
 
96
102
 
97
103
  def current_elapsed_text(*, min_time_width: int = 0) -> str | None:
@@ -152,18 +158,18 @@ def _shimmer_profile(main_text: str) -> list[tuple[str, float]]:
152
158
  if not chars:
153
159
  return []
154
160
 
155
- padding = const.STATUS_SHIMMER_PADDING
161
+ padding = STATUS_SHIMMER_PADDING
156
162
  char_count = len(chars)
157
163
  period = char_count + padding * 2
158
164
 
159
165
  # Use same period as breathing spinner for visual consistency
160
- sweep_seconds = max(const.SPINNER_BREATH_PERIOD_SECONDS, 0.1)
166
+ sweep_seconds = max(SPINNER_BREATH_PERIOD_SECONDS, 0.1)
161
167
 
162
168
  elapsed = _elapsed_since_start()
163
169
  # Complete one full sweep in sweep_seconds, regardless of text length
164
170
  pos_f = (elapsed / sweep_seconds % 1.0) * period
165
171
  pos = int(pos_f)
166
- band_half_width = const.STATUS_SHIMMER_BAND_HALF_WIDTH
172
+ band_half_width = STATUS_SHIMMER_BAND_HALF_WIDTH
167
173
 
168
174
  profile: list[tuple[str, float]] = []
169
175
  for index, ch in enumerate(chars):
@@ -189,7 +195,7 @@ def _shimmer_style(console: Console, base_style: Style, intensity: float) -> Sty
189
195
  if intensity <= 0.0:
190
196
  return base_style
191
197
 
192
- alpha = max(0.0, min(1.0, intensity * const.STATUS_SHIMMER_ALPHA_SCALE))
198
+ alpha = max(0.0, min(1.0, intensity * STATUS_SHIMMER_ALPHA_SCALE))
193
199
 
194
200
  base_color = base_style.color or Color.default()
195
201
  base_triplet = base_color.get_truecolor()
@@ -213,7 +219,7 @@ def _breathing_intensity() -> float:
213
219
  then returning to 0, giving a subtle "breathing" effect.
214
220
  """
215
221
 
216
- period = max(const.SPINNER_BREATH_PERIOD_SECONDS, 0.1)
222
+ period = max(SPINNER_BREATH_PERIOD_SECONDS, 0.1)
217
223
  elapsed = _elapsed_since_start()
218
224
  phase = (elapsed % period) / period
219
225
  return 0.5 * (1.0 - math.cos(2.0 * math.pi * phase))
@@ -224,7 +230,7 @@ def _breathing_glyph() -> str:
224
230
 
225
231
  Alternates between glyphs at each breath cycle (when intensity reaches 0).
226
232
  """
227
- period = max(const.SPINNER_BREATH_PERIOD_SECONDS, 0.1)
233
+ period = max(SPINNER_BREATH_PERIOD_SECONDS, 0.1)
228
234
  elapsed = _elapsed_since_start()
229
235
  cycle = int(elapsed / period)
230
236
  return BREATHING_SPINNER_GLYPHS[cycle % len(BREATHING_SPINNER_GLYPHS)]
@@ -60,17 +60,17 @@ LIGHT_PALETTE = Palette(
60
60
  diff_remove="#82071e on #ffecec",
61
61
  diff_remove_char="#82071e on #ffcfcf",
62
62
  code_theme="ansi_light",
63
- code_background="#e0e0e0",
64
- green_background="#e8f1e9",
65
- blue_grey_background="#e8e9f1",
66
- cyan_background="#e0f0f0",
67
- green_sub_background="#e0f0e0",
68
- blue_sub_background="#e0e8f5",
69
- purple_background="#ede0f5",
70
- orange_background="#f5ebe0",
71
- red_background="#f5e0e0",
72
- grey_background="#e8e8e8",
73
- yellow_background="#f5f5e0",
63
+ code_background="#ebebeb",
64
+ green_background="#f0f7f1",
65
+ blue_grey_background="#f0f1f7",
66
+ cyan_background="#ecf7f7",
67
+ green_sub_background="#ecf7ec",
68
+ blue_sub_background="#ecf1f9",
69
+ purple_background="#f5ecf9",
70
+ orange_background="#f9f3ec",
71
+ red_background="#f9ecec",
72
+ grey_background="#f0f0f0",
73
+ yellow_background="#f9f9ec",
74
74
  )
75
75
 
76
76
  DARK_PALETTE = Palette(
@@ -124,6 +124,7 @@ class ThemeKey(str, Enum):
124
124
  # ERROR
125
125
  ERROR = "error"
126
126
  ERROR_BOLD = "error.bold"
127
+ ERROR_DIM = "error.dim"
127
128
  INTERRUPT = "interrupt"
128
129
  # METADATA
129
130
  METADATA = "metadata"
@@ -154,6 +155,7 @@ class ThemeKey(str, Enum):
154
155
  TOOL_PARAM = "tool.param"
155
156
  TOOL_PARAM_BOLD = "tool.param.bold"
156
157
  TOOL_RESULT = "tool.result"
158
+ TOOL_RESULT_TREE_PREFIX = "tool.result.tree_prefix"
157
159
  TOOL_RESULT_TRUNCATED = "tool.result.truncated"
158
160
  TOOL_RESULT_BOLD = "tool.result.bold"
159
161
  TOOL_MARK = "tool.mark"
@@ -161,6 +163,7 @@ class ThemeKey(str, Enum):
161
163
  TOOL_REJECTED = "tool.rejected"
162
164
  TOOL_TIMEOUT = "tool.timeout"
163
165
  TOOL_RESULT_MERMAID = "tool.result.mermaid"
166
+ SUB_AGENT_FOOTER = "sub_agent.footer"
164
167
  # BASH SYNTAX
165
168
  BASH_COMMAND = "bash.command"
166
169
  BASH_ARGUMENT = "bash.argument"
@@ -200,7 +203,6 @@ class ThemeKey(str, Enum):
200
203
  CONFIG_MODEL_ID = "config.model.id"
201
204
  CONFIG_PARAM_LABEL = "config.param.label"
202
205
 
203
-
204
206
  def __str__(self) -> str:
205
207
  return self.value
206
208
 
@@ -217,6 +219,7 @@ class Themes:
217
219
 
218
220
  def get_theme(theme: str | None = None) -> Themes:
219
221
  palette = LIGHT_PALETTE if theme == "light" else DARK_PALETTE
222
+
220
223
  return Themes(
221
224
  app_theme=Theme(
222
225
  styles={
@@ -235,6 +238,7 @@ def get_theme(theme: str | None = None) -> Themes:
235
238
  # ERROR
236
239
  ThemeKey.ERROR.value: palette.red,
237
240
  ThemeKey.ERROR_BOLD.value: "bold " + palette.red,
241
+ ThemeKey.ERROR_DIM.value: "dim " + palette.red,
238
242
  ThemeKey.INTERRUPT.value: "reverse bold " + palette.red,
239
243
  # USER_INPUT
240
244
  ThemeKey.USER_INPUT.value: palette.magenta,
@@ -264,13 +268,15 @@ def get_theme(theme: str | None = None) -> Themes:
264
268
  ThemeKey.TOOL_PARAM.value: palette.green,
265
269
  ThemeKey.TOOL_PARAM_BOLD.value: "bold " + palette.green,
266
270
  ThemeKey.TOOL_RESULT.value: palette.grey_green,
271
+ ThemeKey.TOOL_RESULT_TREE_PREFIX.value: palette.grey_green + " dim",
267
272
  ThemeKey.TOOL_RESULT_BOLD.value: "bold " + palette.grey_green,
268
- ThemeKey.TOOL_RESULT_TRUNCATED.value: palette.yellow,
273
+ ThemeKey.TOOL_RESULT_TRUNCATED.value: palette.grey1,
269
274
  ThemeKey.TOOL_MARK.value: "bold",
270
275
  ThemeKey.TOOL_APPROVED.value: palette.green + " bold reverse",
271
276
  ThemeKey.TOOL_REJECTED.value: palette.red + " bold reverse",
272
- ThemeKey.TOOL_TIMEOUT.value: palette.yellow,
277
+ ThemeKey.TOOL_TIMEOUT.value: palette.grey2,
273
278
  ThemeKey.TOOL_RESULT_MERMAID: palette.blue + " underline",
279
+ ThemeKey.SUB_AGENT_FOOTER.value: "dim " + palette.grey2,
274
280
  # BASH SYNTAX
275
281
  ThemeKey.BASH_COMMAND.value: "bold " + palette.green,
276
282
  ThemeKey.BASH_ARGUMENT.value: palette.green,
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import IO
6
+
7
+
8
+ def print_kitty_image(file_path: str | Path, *, height: int | None = None, file: IO[str] | None = None) -> None:
9
+ """Print an image to the terminal using Kitty graphics protocol.
10
+
11
+ This intentionally bypasses Rich rendering to avoid interleaving Live refreshes
12
+ with raw escape sequences.
13
+ """
14
+
15
+ path = Path(file_path) if isinstance(file_path, str) else file_path
16
+ if not path.exists():
17
+ print(f"Image not found: {path}", file=file or sys.stdout, flush=True)
18
+ return
19
+
20
+ try:
21
+ from term_image.image import KittyImage # type: ignore[import-untyped]
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
+
28
+ out = file or sys.stdout
29
+ print("", file=out)
30
+ print(str(img), file=out)
31
+ print("", file=out)
32
+ out.flush()
33
+ except Exception:
34
+ print(f"Saved image: {path}", file=file or sys.stdout, flush=True)
@@ -7,6 +7,7 @@ from dataclasses import dataclass
7
7
  from enum import Enum
8
8
  from typing import TextIO, cast
9
9
 
10
+ from klaude_code.const import NOTIFY_COMPACT_LIMIT
10
11
  from klaude_code.trace import DebugType, log_debug
11
12
 
12
13
  # Environment variable for tmux test signal channel
@@ -102,7 +103,7 @@ class TerminalNotifier:
102
103
  return term.lower() not in {"", "dumb"}
103
104
 
104
105
 
105
- def _compact(text: str, limit: int = 160) -> str:
106
+ def _compact(text: str, limit: int = NOTIFY_COMPACT_LIMIT) -> str:
106
107
  squashed = " ".join(text.split())
107
108
  if len(squashed) > limit:
108
109
  return squashed[: limit - 3] + "…"
@@ -1,5 +1,5 @@
1
1
  """
2
- Use OSC 9;4;... to control progress bar in terminal like Ghostty
2
+ Use OSC 9;4;… to control progress bar in terminal like Ghostty
3
3
  States:
4
4
  0/hidden
5
5
  1/normal
@@ -71,17 +71,17 @@ if __name__ == "__main__":
71
71
  # Clear progress bar
72
72
  emit_osc94(OSC94States.HIDDEN)
73
73
 
74
- print("Waiting...")
74
+ print("Waiting")
75
75
  # Indeterminate
76
76
  emit_osc94(OSC94States.INDETERMINATE)
77
77
 
78
78
  time.sleep(2)
79
- print("Error...")
79
+ print("Error")
80
80
  # Error
81
81
  emit_osc94(OSC94States.ERROR)
82
82
 
83
83
  time.sleep(2)
84
- print("Warning...")
84
+ print("Warning")
85
85
  # Warning
86
86
  emit_osc94(OSC94States.WARNING)
87
87
  time.sleep(2)
@@ -88,12 +88,30 @@ def build_model_select_items(models: list[Any]) -> list[SelectItem[str]]:
88
88
 
89
89
 
90
90
  def _restyle_title(title: list[tuple[str, str]], cls: str) -> list[tuple[str, str]]:
91
- """Re-apply a style class while keeping text attributes like bold/italic."""
92
- keep_attrs = {"bold", "italic", "underline", "reverse", "blink", "strike"}
91
+ """Re-apply a style class while keeping existing style tokens.
92
+
93
+ This is used to highlight the currently-pointed item. We want to:
94
+ - preserve explicit colors (e.g. `fg:ansibrightblack`) defined by callers
95
+ - preserve existing classes (e.g. `class:msg`, `class:meta`) so their
96
+ non-color attributes remain in effect
97
+ - preserve text attributes like bold/italic/dim
98
+ """
99
+
100
+ keep_attrs = {"bold", "italic", "underline", "reverse", "blink", "strike", "dim"}
93
101
  restyled: list[tuple[str, str]] = []
94
102
  for old_style, text in title:
95
- attrs = [tok for tok in old_style.split() if tok in keep_attrs]
96
- style = f"{cls} {' '.join(attrs)}".strip()
103
+ tokens = old_style.split()
104
+ attrs = [tok for tok in tokens if tok in keep_attrs]
105
+ style_tokens = [tok for tok in tokens if tok not in keep_attrs]
106
+
107
+ if cls in style_tokens:
108
+ style_tokens = [tok for tok in style_tokens if tok != cls]
109
+
110
+ # Place the highlight class first, so existing per-token styles (classes
111
+ # or explicit fg/bg) keep their precedence. This prevents highlight from
112
+ # accidentally overriding caller-defined colors.
113
+ combined = [cls, *style_tokens, *attrs]
114
+ style = " ".join(tok for tok in combined if tok)
97
115
  restyled.append((style, text))
98
116
  return restyled
99
117
 
@@ -101,7 +101,7 @@ def format_model_params(model_params: "LLMConfigModelParameter") -> list[str]:
101
101
  - "reasoning medium"
102
102
  - "thinking budget 10000"
103
103
  - "verbosity 2"
104
- - "provider-routing: {...}"
104
+ - "provider-routing: {}"
105
105
  """
106
106
  parts: list[str] = []
107
107
 
@@ -109,7 +109,7 @@ def format_model_params(model_params: "LLMConfigModelParameter") -> list[str]:
109
109
  if model_params.thinking.reasoning_effort:
110
110
  parts.append(f"reasoning {model_params.thinking.reasoning_effort}")
111
111
  if model_params.thinking.reasoning_summary:
112
- parts.append(f"reason-summary {model_params.thinking.reasoning_summary}")
112
+ parts.append(f"summary {model_params.thinking.reasoning_summary}")
113
113
  if model_params.thinking.budget_tokens:
114
114
  parts.append(f"thinking budget {model_params.thinking.budget_tokens}")
115
115
 
@@ -119,6 +119,15 @@ def format_model_params(model_params: "LLMConfigModelParameter") -> list[str]:
119
119
  if model_params.provider_routing:
120
120
  parts.append(f"provider routing {_format_provider_routing(model_params.provider_routing)}")
121
121
 
122
+ if model_params.modalities:
123
+ parts.append(f"modalities {','.join(model_params.modalities)}")
124
+
125
+ if model_params.image_config:
126
+ if model_params.image_config.aspect_ratio:
127
+ parts.append(f"image aspect {model_params.image_config.aspect_ratio}")
128
+ if model_params.image_config.image_size:
129
+ parts.append(f"image size {model_params.image_config.image_size}")
130
+
122
131
  return parts
123
132
 
124
133
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.9.0
3
+ Version: 2.0.1
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
@@ -9,11 +9,12 @@ 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>=12.0.0
12
+ Requires-Dist: pillow>=9.1,<11.0
13
13
  Requires-Dist: prompt-toolkit>=3.0.52
14
14
  Requires-Dist: pydantic>=2.11.7
15
15
  Requires-Dist: pyyaml>=6.0.2
16
16
  Requires-Dist: rich>=14.1.0
17
+ Requires-Dist: term-image>=0.7.2
17
18
  Requires-Dist: trafilatura>=2.0.0
18
19
  Requires-Dist: typer>=0.17.3
19
20
  Requires-Python: >=3.13
@@ -395,3 +396,4 @@ The main agent can spawn specialized sub-agents for specific tasks:
395
396
  | **Explore** | Fast codebase exploration - find files, search code, answer questions about the codebase |
396
397
  | **Task** | Handle complex multi-step tasks autonomously |
397
398
  | **WebAgent** | Search the web, fetch pages, and analyze content |
399
+ | **ImageGen** | Generate images from text prompts via OpenRouter Nano Banana Pro |