klaude-code 1.8.0__py3-none-any.whl → 2.0.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 (142) hide show
  1. klaude_code/auth/base.py +97 -0
  2. klaude_code/auth/claude/__init__.py +6 -0
  3. klaude_code/auth/claude/exceptions.py +9 -0
  4. klaude_code/auth/claude/oauth.py +172 -0
  5. klaude_code/auth/claude/token_manager.py +26 -0
  6. klaude_code/auth/codex/token_manager.py +10 -50
  7. klaude_code/cli/auth_cmd.py +127 -46
  8. klaude_code/cli/config_cmd.py +4 -2
  9. klaude_code/cli/cost_cmd.py +14 -9
  10. klaude_code/cli/list_model.py +248 -200
  11. klaude_code/cli/main.py +1 -1
  12. klaude_code/cli/runtime.py +7 -5
  13. klaude_code/cli/self_update.py +1 -1
  14. klaude_code/cli/session_cmd.py +1 -1
  15. klaude_code/command/clear_cmd.py +6 -2
  16. klaude_code/command/command_abc.py +2 -2
  17. klaude_code/command/debug_cmd.py +4 -4
  18. klaude_code/command/export_cmd.py +2 -2
  19. klaude_code/command/export_online_cmd.py +12 -12
  20. klaude_code/command/fork_session_cmd.py +29 -23
  21. klaude_code/command/help_cmd.py +4 -4
  22. klaude_code/command/model_cmd.py +4 -4
  23. klaude_code/command/model_select.py +1 -1
  24. klaude_code/command/prompt-commit.md +82 -0
  25. klaude_code/command/prompt_command.py +3 -3
  26. klaude_code/command/refresh_cmd.py +2 -2
  27. klaude_code/command/registry.py +7 -5
  28. klaude_code/command/release_notes_cmd.py +4 -4
  29. klaude_code/command/resume_cmd.py +15 -11
  30. klaude_code/command/status_cmd.py +4 -4
  31. klaude_code/command/terminal_setup_cmd.py +8 -8
  32. klaude_code/command/thinking_cmd.py +4 -4
  33. klaude_code/config/assets/builtin_config.yaml +52 -3
  34. klaude_code/config/builtin_config.py +16 -5
  35. klaude_code/config/config.py +31 -7
  36. klaude_code/config/thinking.py +4 -4
  37. klaude_code/const.py +146 -91
  38. klaude_code/core/agent.py +3 -12
  39. klaude_code/core/executor.py +21 -13
  40. klaude_code/core/manager/sub_agent_manager.py +71 -7
  41. klaude_code/core/prompt.py +1 -1
  42. klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
  43. klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
  44. klaude_code/core/reminders.py +88 -69
  45. klaude_code/core/task.py +44 -45
  46. klaude_code/core/tool/file/apply_patch_tool.py +9 -9
  47. klaude_code/core/tool/file/diff_builder.py +3 -5
  48. klaude_code/core/tool/file/edit_tool.py +23 -23
  49. klaude_code/core/tool/file/move_tool.py +43 -43
  50. klaude_code/core/tool/file/read_tool.py +44 -39
  51. klaude_code/core/tool/file/write_tool.py +14 -14
  52. klaude_code/core/tool/report_back_tool.py +4 -4
  53. klaude_code/core/tool/shell/bash_tool.py +23 -23
  54. klaude_code/core/tool/skill/skill_tool.py +7 -7
  55. klaude_code/core/tool/sub_agent_tool.py +38 -9
  56. klaude_code/core/tool/todo/todo_write_tool.py +8 -8
  57. klaude_code/core/tool/todo/update_plan_tool.py +6 -6
  58. klaude_code/core/tool/tool_abc.py +2 -2
  59. klaude_code/core/tool/tool_context.py +27 -0
  60. klaude_code/core/tool/tool_runner.py +88 -42
  61. klaude_code/core/tool/truncation.py +38 -20
  62. klaude_code/core/tool/web/mermaid_tool.py +6 -7
  63. klaude_code/core/tool/web/web_fetch_tool.py +68 -30
  64. klaude_code/core/tool/web/web_search_tool.py +15 -17
  65. klaude_code/core/turn.py +120 -73
  66. klaude_code/llm/anthropic/client.py +104 -44
  67. klaude_code/llm/anthropic/input.py +116 -108
  68. klaude_code/llm/bedrock/client.py +8 -5
  69. klaude_code/llm/claude/__init__.py +3 -0
  70. klaude_code/llm/claude/client.py +105 -0
  71. klaude_code/llm/client.py +4 -3
  72. klaude_code/llm/codex/client.py +16 -10
  73. klaude_code/llm/google/client.py +122 -60
  74. klaude_code/llm/google/input.py +94 -108
  75. klaude_code/llm/image.py +123 -0
  76. klaude_code/llm/input_common.py +136 -189
  77. klaude_code/llm/openai_compatible/client.py +17 -7
  78. klaude_code/llm/openai_compatible/input.py +36 -66
  79. klaude_code/llm/openai_compatible/stream.py +119 -67
  80. klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
  81. klaude_code/llm/openrouter/client.py +34 -9
  82. klaude_code/llm/openrouter/input.py +63 -64
  83. klaude_code/llm/openrouter/reasoning.py +22 -24
  84. klaude_code/llm/registry.py +20 -15
  85. klaude_code/llm/responses/client.py +107 -45
  86. klaude_code/llm/responses/input.py +115 -98
  87. klaude_code/llm/usage.py +52 -25
  88. klaude_code/protocol/__init__.py +1 -0
  89. klaude_code/protocol/events.py +16 -12
  90. klaude_code/protocol/llm_param.py +22 -3
  91. klaude_code/protocol/message.py +250 -0
  92. klaude_code/protocol/model.py +94 -281
  93. klaude_code/protocol/op.py +2 -2
  94. klaude_code/protocol/sub_agent/__init__.py +2 -2
  95. klaude_code/protocol/sub_agent/explore.py +10 -0
  96. klaude_code/protocol/sub_agent/image_gen.py +119 -0
  97. klaude_code/protocol/sub_agent/task.py +10 -0
  98. klaude_code/protocol/sub_agent/web.py +10 -0
  99. klaude_code/session/codec.py +6 -6
  100. klaude_code/session/export.py +261 -62
  101. klaude_code/session/selector.py +7 -24
  102. klaude_code/session/session.py +125 -53
  103. klaude_code/session/store.py +5 -32
  104. klaude_code/session/templates/export_session.html +1 -1
  105. klaude_code/session/templates/mermaid_viewer.html +1 -1
  106. klaude_code/trace/log.py +11 -6
  107. klaude_code/ui/core/input.py +1 -1
  108. klaude_code/ui/core/stage_manager.py +1 -8
  109. klaude_code/ui/modes/debug/display.py +2 -2
  110. klaude_code/ui/modes/repl/clipboard.py +2 -2
  111. klaude_code/ui/modes/repl/completers.py +18 -10
  112. klaude_code/ui/modes/repl/event_handler.py +136 -127
  113. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  114. klaude_code/ui/modes/repl/key_bindings.py +1 -1
  115. klaude_code/ui/modes/repl/renderer.py +107 -15
  116. klaude_code/ui/renderers/assistant.py +2 -2
  117. klaude_code/ui/renderers/common.py +65 -7
  118. klaude_code/ui/renderers/developer.py +7 -6
  119. klaude_code/ui/renderers/diffs.py +11 -11
  120. klaude_code/ui/renderers/mermaid_viewer.py +49 -2
  121. klaude_code/ui/renderers/metadata.py +39 -31
  122. klaude_code/ui/renderers/sub_agent.py +57 -16
  123. klaude_code/ui/renderers/thinking.py +37 -2
  124. klaude_code/ui/renderers/tools.py +180 -165
  125. klaude_code/ui/rich/live.py +3 -1
  126. klaude_code/ui/rich/markdown.py +39 -7
  127. klaude_code/ui/rich/quote.py +76 -1
  128. klaude_code/ui/rich/status.py +14 -8
  129. klaude_code/ui/rich/theme.py +13 -6
  130. klaude_code/ui/terminal/image.py +34 -0
  131. klaude_code/ui/terminal/notifier.py +2 -1
  132. klaude_code/ui/terminal/progress_bar.py +4 -4
  133. klaude_code/ui/terminal/selector.py +22 -4
  134. klaude_code/ui/utils/common.py +55 -0
  135. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +28 -6
  136. klaude_code-2.0.0.dist-info/RECORD +229 -0
  137. klaude_code/command/prompt-jj-describe.md +0 -32
  138. klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -22
  139. klaude_code/protocol/sub_agent/oracle.py +0 -91
  140. klaude_code-1.8.0.dist-info/RECORD +0 -219
  141. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
  142. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.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)]
@@ -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"
@@ -197,8 +200,8 @@ class ThemeKey(str, Enum):
197
200
  CONFIG_STATUS_PRIMARY = "config.status.primary"
198
201
  CONFIG_STATUS_ERROR = "config.status.error"
199
202
  CONFIG_ITEM_NAME = "config.item.name"
203
+ CONFIG_MODEL_ID = "config.model.id"
200
204
  CONFIG_PARAM_LABEL = "config.param.label"
201
- CONFIG_PANEL_BORDER = "config.panel.border"
202
205
 
203
206
  def __str__(self) -> str:
204
207
  return self.value
@@ -216,6 +219,7 @@ class Themes:
216
219
 
217
220
  def get_theme(theme: str | None = None) -> Themes:
218
221
  palette = LIGHT_PALETTE if theme == "light" else DARK_PALETTE
222
+
219
223
  return Themes(
220
224
  app_theme=Theme(
221
225
  styles={
@@ -234,6 +238,7 @@ def get_theme(theme: str | None = None) -> Themes:
234
238
  # ERROR
235
239
  ThemeKey.ERROR.value: palette.red,
236
240
  ThemeKey.ERROR_BOLD.value: "bold " + palette.red,
241
+ ThemeKey.ERROR_DIM.value: "dim " + palette.red,
237
242
  ThemeKey.INTERRUPT.value: "reverse bold " + palette.red,
238
243
  # USER_INPUT
239
244
  ThemeKey.USER_INPUT.value: palette.magenta,
@@ -263,13 +268,15 @@ def get_theme(theme: str | None = None) -> Themes:
263
268
  ThemeKey.TOOL_PARAM.value: palette.green,
264
269
  ThemeKey.TOOL_PARAM_BOLD.value: "bold " + palette.green,
265
270
  ThemeKey.TOOL_RESULT.value: palette.grey_green,
271
+ ThemeKey.TOOL_RESULT_TREE_PREFIX.value: palette.grey_green + " dim",
266
272
  ThemeKey.TOOL_RESULT_BOLD.value: "bold " + palette.grey_green,
267
- ThemeKey.TOOL_RESULT_TRUNCATED.value: palette.yellow,
273
+ ThemeKey.TOOL_RESULT_TRUNCATED.value: palette.yellow + " dim",
268
274
  ThemeKey.TOOL_MARK.value: "bold",
269
275
  ThemeKey.TOOL_APPROVED.value: palette.green + " bold reverse",
270
276
  ThemeKey.TOOL_REJECTED.value: palette.red + " bold reverse",
271
277
  ThemeKey.TOOL_TIMEOUT.value: palette.yellow,
272
278
  ThemeKey.TOOL_RESULT_MERMAID: palette.blue + " underline",
279
+ ThemeKey.SUB_AGENT_FOOTER.value: "dim " + palette.grey2,
273
280
  # BASH SYNTAX
274
281
  ThemeKey.BASH_COMMAND.value: "bold " + palette.green,
275
282
  ThemeKey.BASH_ARGUMENT.value: palette.green,
@@ -300,13 +307,13 @@ def get_theme(theme: str | None = None) -> Themes:
300
307
  ThemeKey.RESUME_FLAG.value: "bold reverse " + palette.green,
301
308
  ThemeKey.RESUME_INFO.value: palette.green,
302
309
  # CONFIGURATION DISPLAY
303
- ThemeKey.CONFIG_TABLE_HEADER.value: palette.grey1,
310
+ ThemeKey.CONFIG_TABLE_HEADER.value: "bold " + palette.grey1,
304
311
  ThemeKey.CONFIG_STATUS_OK.value: palette.green,
305
- ThemeKey.CONFIG_STATUS_PRIMARY.value: palette.yellow,
312
+ ThemeKey.CONFIG_STATUS_PRIMARY.value: "bold " + palette.yellow,
306
313
  ThemeKey.CONFIG_STATUS_ERROR.value: palette.red,
307
314
  ThemeKey.CONFIG_ITEM_NAME.value: palette.cyan,
308
- ThemeKey.CONFIG_PARAM_LABEL.value: palette.grey1,
309
- ThemeKey.CONFIG_PANEL_BORDER.value: palette.grey3,
315
+ ThemeKey.CONFIG_MODEL_ID.value: palette.blue,
316
+ ThemeKey.CONFIG_PARAM_LABEL.value: "dim",
310
317
  ThemeKey.CONFIG_PROVIDER.value: palette.cyan + " bold",
311
318
  }
312
319
  ),
@@ -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
 
@@ -1,6 +1,10 @@
1
1
  import re
2
2
  import subprocess
3
3
  from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from klaude_code.protocol.llm_param import LLMConfigModelParameter, OpenRouterProviderRouting
4
8
 
5
9
  LEADING_NEWLINES_REGEX = re.compile(r"^\n{2,}")
6
10
 
@@ -88,3 +92,54 @@ def show_path_with_tilde(path: Path | None = None):
88
92
  return f"~/{relative_path}"
89
93
  except ValueError:
90
94
  return str(path)
95
+
96
+
97
+ def format_model_params(model_params: "LLMConfigModelParameter") -> list[str]:
98
+ """Format model parameters in a concise style.
99
+
100
+ Returns a list of formatted parameter strings like:
101
+ - "reasoning medium"
102
+ - "thinking budget 10000"
103
+ - "verbosity 2"
104
+ - "provider-routing: {…}"
105
+ """
106
+ parts: list[str] = []
107
+
108
+ if model_params.thinking:
109
+ if model_params.thinking.reasoning_effort:
110
+ parts.append(f"reasoning {model_params.thinking.reasoning_effort}")
111
+ if model_params.thinking.reasoning_summary:
112
+ parts.append(f"summary {model_params.thinking.reasoning_summary}")
113
+ if model_params.thinking.budget_tokens:
114
+ parts.append(f"thinking budget {model_params.thinking.budget_tokens}")
115
+
116
+ if model_params.verbosity:
117
+ parts.append(f"verbosity {model_params.verbosity}")
118
+
119
+ if model_params.provider_routing:
120
+ parts.append(f"provider routing {_format_provider_routing(model_params.provider_routing)}")
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
+
131
+ return parts
132
+
133
+
134
+ def _format_provider_routing(pr: "OpenRouterProviderRouting") -> str:
135
+ """Format provider routing settings concisely."""
136
+ items: list[str] = []
137
+ if pr.sort:
138
+ items.append(pr.sort)
139
+ if pr.only:
140
+ items.append(">".join(pr.only))
141
+ if pr.order:
142
+ items.append(">".join(pr.order))
143
+ if pr.ignore:
144
+ items.append(f"ignore {'>'.join(pr.ignore)}")
145
+ return " · ".join(items) if items else ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.8.0
3
+ Version: 2.0.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
@@ -9,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
@@ -120,11 +121,12 @@ On first run, you'll be prompted to select a model. Your choice is saved as `mai
120
121
  | Provider | Env Variable | Models |
121
122
  |-------------|-----------------------|-------------------------------------------------------------------------------|
122
123
  | anthropic | `ANTHROPIC_API_KEY` | sonnet, opus |
124
+ | claude | N/A (OAuth) | sonnet@claude, opus@claude (requires Claude Pro/Max subscription) |
123
125
  | openai | `OPENAI_API_KEY` | gpt-5.2 |
124
126
  | openrouter | `OPENROUTER_API_KEY` | gpt-5.2, gpt-5.2-fast, gpt-5.1-codex-max, sonnet, opus, haiku, kimi, gemini-* |
125
127
  | deepseek | `DEEPSEEK_API_KEY` | deepseek |
126
128
  | moonshot | `MOONSHOT_API_KEY` | kimi@moonshot |
127
- | codex | N/A (OAuth) | gpt-5.2-codex |
129
+ | codex | N/A (OAuth) | gpt-5.2-codex (requires ChatGPT Pro subscription) |
128
130
 
129
131
  List all configured providers and models:
130
132
 
@@ -134,6 +136,26 @@ klaude list
134
136
 
135
137
  Models from providers without a valid API key are shown as dimmed/unavailable.
136
138
 
139
+ #### OAuth Login
140
+
141
+ For subscription-based providers (Claude Pro/Max, ChatGPT Pro), use the login command:
142
+
143
+ ```bash
144
+ # Interactive provider selection
145
+ klaude login
146
+
147
+ # Or specify provider directly
148
+ klaude login claude # Claude Pro/Max subscription
149
+ klaude login codex # ChatGPT Pro subscription
150
+ ```
151
+
152
+ To logout:
153
+
154
+ ```bash
155
+ klaude logout claude
156
+ klaude logout codex
157
+ ```
158
+
137
159
  #### Custom Configuration
138
160
 
139
161
  User config file: `~/.klaude/klaude-config.yaml`
@@ -240,7 +262,6 @@ provider_list:
240
262
  main_model: opus
241
263
 
242
264
  sub_agent_models:
243
- oracle: gpt-4.1
244
265
  explore: sonnet
245
266
  task: opus
246
267
  webagent: sonnet
@@ -269,12 +290,13 @@ provider_list:
269
290
  ##### Supported Protocols
270
291
 
271
292
  - `anthropic` - Anthropic Claude API
293
+ - `claude_oauth` - Claude OAuth (for Claude Pro/Max subscribers)
272
294
  - `openai` - OpenAI-compatible API
273
295
  - `responses` - OpenAI Responses API (for o-series, GPT-5, Codex)
274
296
  - `openrouter` - OpenRouter API
275
297
  - `google` - Google Gemini API
276
298
  - `bedrock` - AWS Bedrock (uses AWS credentials instead of api_key)
277
- - `codex` - OpenAI Codex CLI (OAuth-based)
299
+ - `codex_oauth` - OpenAI Codex CLI (OAuth-based, for ChatGPT Pro subscribers)
278
300
 
279
301
  List configured providers and models:
280
302
 
@@ -374,4 +396,4 @@ The main agent can spawn specialized sub-agents for specific tasks:
374
396
  | **Explore** | Fast codebase exploration - find files, search code, answer questions about the codebase |
375
397
  | **Task** | Handle complex multi-step tasks autonomously |
376
398
  | **WebAgent** | Search the web, fetch pages, and analyze content |
377
- | **Oracle** | Advanced reasoning advisor for code reviews, architecture planning, and bug analysis |
399
+ | **ImageGen** | Generate images from text prompts via OpenRouter Nano Banana Pro |