klaude-code 1.2.14__py3-none-any.whl → 1.2.16__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 (54) hide show
  1. klaude_code/cli/main.py +66 -42
  2. klaude_code/cli/runtime.py +24 -13
  3. klaude_code/command/export_cmd.py +2 -2
  4. klaude_code/command/prompt-handoff.md +33 -0
  5. klaude_code/command/thinking_cmd.py +6 -2
  6. klaude_code/config/config.py +5 -5
  7. klaude_code/config/list_model.py +1 -1
  8. klaude_code/const/__init__.py +3 -0
  9. klaude_code/core/executor.py +2 -2
  10. klaude_code/core/manager/llm_clients_builder.py +1 -1
  11. klaude_code/core/manager/sub_agent_manager.py +30 -6
  12. klaude_code/core/prompt.py +15 -13
  13. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +0 -1
  14. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -1
  15. klaude_code/core/reminders.py +75 -32
  16. klaude_code/core/task.py +10 -22
  17. klaude_code/core/tool/__init__.py +2 -0
  18. klaude_code/core/tool/report_back_tool.py +58 -0
  19. klaude_code/core/tool/sub_agent_tool.py +6 -0
  20. klaude_code/core/tool/tool_runner.py +9 -1
  21. klaude_code/core/turn.py +45 -4
  22. klaude_code/llm/anthropic/input.py +14 -5
  23. klaude_code/llm/input_common.py +1 -1
  24. klaude_code/llm/openrouter/input.py +14 -3
  25. klaude_code/llm/responses/input.py +19 -0
  26. klaude_code/protocol/events.py +1 -0
  27. klaude_code/protocol/model.py +24 -14
  28. klaude_code/protocol/sub_agent/__init__.py +117 -0
  29. klaude_code/protocol/sub_agent/explore.py +63 -0
  30. klaude_code/protocol/sub_agent/oracle.py +91 -0
  31. klaude_code/protocol/sub_agent/task.py +61 -0
  32. klaude_code/protocol/sub_agent/web_fetch.py +74 -0
  33. klaude_code/protocol/tools.py +1 -0
  34. klaude_code/session/export.py +12 -6
  35. klaude_code/session/session.py +12 -2
  36. klaude_code/session/templates/export_session.html +20 -24
  37. klaude_code/ui/modes/repl/completers.py +1 -1
  38. klaude_code/ui/modes/repl/event_handler.py +34 -3
  39. klaude_code/ui/modes/repl/renderer.py +9 -9
  40. klaude_code/ui/renderers/developer.py +18 -7
  41. klaude_code/ui/renderers/metadata.py +57 -84
  42. klaude_code/ui/renderers/sub_agent.py +59 -3
  43. klaude_code/ui/renderers/thinking.py +3 -3
  44. klaude_code/ui/renderers/tools.py +67 -30
  45. klaude_code/ui/rich/markdown.py +45 -57
  46. klaude_code/ui/rich/status.py +32 -14
  47. klaude_code/ui/rich/theme.py +18 -17
  48. {klaude_code-1.2.14.dist-info → klaude_code-1.2.16.dist-info}/METADATA +3 -2
  49. {klaude_code-1.2.14.dist-info → klaude_code-1.2.16.dist-info}/RECORD +53 -47
  50. klaude_code/protocol/sub_agent.py +0 -354
  51. /klaude_code/core/prompts/{prompt-subagent-webfetch.md → prompt-sub-agent-webfetch.md} +0 -0
  52. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  53. {klaude_code-1.2.14.dist-info → klaude_code-1.2.16.dist-info}/WHEEL +0 -0
  54. {klaude_code-1.2.14.dist-info → klaude_code-1.2.16.dist-info}/entry_points.txt +0 -0
@@ -4,8 +4,10 @@ from __future__ import annotations
4
4
  import contextlib
5
5
  import io
6
6
  import time
7
+ from collections.abc import Callable
7
8
  from typing import Any, ClassVar
8
9
 
10
+ from rich import box
9
11
  from rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
10
12
  from rich.live import Live
11
13
  from rich.markdown import CodeBlock, Heading, Markdown
@@ -30,9 +32,18 @@ class NoInsetCodeBlock(CodeBlock):
30
32
  self.lexer_name,
31
33
  theme=self.theme,
32
34
  word_wrap=True,
33
- padding=(0, 1),
35
+ padding=(0, 0),
34
36
  )
35
- yield Panel.fit(syntax, padding=0, border_style="markdown.code.panel")
37
+ yield Panel.fit(syntax, padding=(0, 0), box=box.HORIZONTALS, border_style="markdown.code.border")
38
+
39
+
40
+ class ThinkingCodeBlock(CodeBlock):
41
+ """A code block for thinking content that uses grey styling instead of syntax highlighting."""
42
+
43
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
44
+ code = str(self.text).rstrip()
45
+ text = Text(code, "markdown.code.block")
46
+ yield text
36
47
 
37
48
 
38
49
  class LeftHeading(Heading):
@@ -41,16 +52,10 @@ class LeftHeading(Heading):
41
52
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
42
53
  text = self.text
43
54
  text.justify = "left" # Override justification
44
- # if self.tag == "h1":
45
- # from rich.panel import Panel
46
- # from rich import box
47
- # # Draw a border around h1s, but keep text left-aligned
48
- # yield Panel(
49
- # text,
50
- # box=box.SQUARE,
51
- # style="markdown.h1.border",
52
- # )
53
- if self.tag == "h2":
55
+ if self.tag == "h1":
56
+ h1_text = text.assemble((" ", "markdown.h1"), text, (" ", "markdown.h1"))
57
+ yield h1_text
58
+ elif self.tag == "h2":
54
59
  text.stylize(Style(bold=True, underline=False))
55
60
  yield Rule(title=text, characters="-", style="markdown.h2.border", align="left")
56
61
  else:
@@ -68,6 +73,17 @@ class NoInsetMarkdown(Markdown):
68
73
  }
69
74
 
70
75
 
76
+ class ThinkingMarkdown(Markdown):
77
+ """Markdown for thinking content with grey-styled code blocks and left-justified headings."""
78
+
79
+ elements: ClassVar[dict[str, type[Any]]] = {
80
+ **Markdown.elements,
81
+ "fence": ThinkingCodeBlock,
82
+ "code_block": ThinkingCodeBlock,
83
+ "heading_open": LeftHeading,
84
+ }
85
+
86
+
71
87
  class MarkdownStream:
72
88
  """Streaming markdown renderer that progressively displays content with a live updating window.
73
89
 
@@ -84,6 +100,7 @@ class MarkdownStream:
84
100
  spinner: Spinner | None = None,
85
101
  mark: str | None = None,
86
102
  indent: int = 0,
103
+ markdown_class: Callable[..., Markdown] | None = None,
87
104
  ) -> None:
88
105
  """Initialize the markdown stream.
89
106
 
@@ -93,6 +110,7 @@ class MarkdownStream:
93
110
  console (Console, optional): External console to use for rendering
94
111
  mark (str | None, optional): Marker shown before the first non-empty line when indent >= 2
95
112
  indent (int, optional): Number of spaces to indent all rendered lines on the left
113
+ markdown_class: Markdown class to use for rendering (defaults to NoInsetMarkdown)
96
114
  """
97
115
  self.printed: list[str] = [] # Stores lines that have already been printed
98
116
 
@@ -108,16 +126,13 @@ class MarkdownStream:
108
126
  self.when: float = 0.0 # Timestamp of last update
109
127
  self.min_delay: float = 1.0 / 20 # Minimum time between updates (20fps)
110
128
  self.live_window: int = const.MARKDOWN_STREAM_LIVE_WINDOW
111
- # Track the maximum height the live window has ever reached
112
- # so we only pad when it shrinks from a previous height,
113
- # instead of always padding to live_window from the start.
114
- self._live_window_seen_height: int = 0
115
129
 
116
130
  self.theme = theme
117
131
  self.console = console
118
132
  self.spinner: Spinner | None = spinner
119
133
  self.mark: str | None = mark
120
134
  self.indent: int = max(indent, 0)
135
+ self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
121
136
 
122
137
  @property
123
138
  def _live_started(self) -> bool:
@@ -154,7 +169,7 @@ class MarkdownStream:
154
169
  width=effective_width,
155
170
  )
156
171
 
157
- markdown = NoInsetMarkdown(text, **self.mdargs)
172
+ markdown = self.markdown_class(text, **self.mdargs)
158
173
  temp_console.print(markdown)
159
174
  output = string_io.getvalue()
160
175
 
@@ -205,18 +220,16 @@ class MarkdownStream:
205
220
  Markdown going to the console works better in terminal scrollback buffers.
206
221
  The live window doesn't play nice with terminal scrollback.
207
222
  """
208
- # On the first call, start the Live renderer
209
223
  if not self._live_started:
210
224
  initial_content = self._live_renderable(Text(""), final=False)
225
+ # transient=False keeps final frame on screen after stop()
211
226
  self.live = Live(
212
227
  initial_content,
213
228
  refresh_per_second=1.0 / self.min_delay,
214
229
  console=self.console,
215
230
  )
216
231
  self.live.start()
217
- # Note: self._live_started is now a property derived from self.live
218
232
 
219
- # If live rendering isn't available (e.g., after a final update), stop.
220
233
  if self.live is None:
221
234
  return
222
235
 
@@ -236,61 +249,36 @@ class MarkdownStream:
236
249
 
237
250
  num_lines = len(lines)
238
251
 
239
- # How many lines have "left" the live window and are now considered stable?
240
- # Or if final, consider all lines to be stable.
241
- if not final:
242
- num_lines = max(num_lines - self.live_window, 0)
252
+ # Reserve last live_window lines for Live area to keep height stable
253
+ num_lines = max(num_lines - self.live_window, 0)
243
254
 
244
- # If there is new stable content, append only the new part
245
- # Update Live window to prevent visual duplication
246
- if final or num_lines > 0:
247
- # Lines to append to stable area
255
+ # Print new stable lines above Live window
256
+ if num_lines > 0:
248
257
  num_printed = len(self.printed)
249
258
  to_append_count = num_lines - num_printed
250
259
 
251
260
  if to_append_count > 0:
252
- # Print new stable lines above Live window
253
261
  append_chunk = lines[num_printed:num_lines]
254
262
  append_chunk_text = Text.from_ansi("".join(append_chunk))
255
263
  live = self.live
256
264
  assert live is not None
257
- live.console.print(append_chunk_text) # Print above Live area
258
-
259
- # Track printed stable lines
265
+ live.console.print(append_chunk_text)
260
266
  self.printed = lines[:num_lines]
261
267
 
262
- # Handle final update cleanup
268
+ rest_lines = lines[num_lines:]
269
+
270
+ # Final: render remaining lines without spinner, then stop Live
263
271
  if final:
264
272
  live = self.live
265
273
  assert live is not None
266
- live.update(Text(""))
274
+ rest = "".join(rest_lines)
275
+ rest_text = Text.from_ansi(rest)
276
+ final_renderable = self._live_renderable(rest_text, final=True)
277
+ live.update(final_renderable)
267
278
  live.stop()
268
279
  self.live = None
269
280
  return
270
281
 
271
- # Update Live window to prevent timing issues
272
- # with console.print above. We pad the live region
273
- # so that its height stays stable when it shrinks
274
- # from a previously reached height, avoiding spinner jitter.
275
- rest_lines = lines[num_lines:]
276
-
277
- if not final:
278
- current_height = len(rest_lines)
279
-
280
- # Update the maximum height we've seen so far for this live window.
281
- if current_height > self._live_window_seen_height:
282
- # Never exceed configured live_window, even if logic changes later.
283
- self._live_window_seen_height = min(current_height, self.live_window)
284
-
285
- target_height = min(self._live_window_seen_height, self.live_window)
286
- if target_height > 0 and current_height < target_height:
287
- # Pad only up to the maximum height we've seen so far.
288
- # This keeps the Live region height stable without overshooting,
289
- # which can cause the spinner to jump by a line.
290
- pad_count = target_height - current_height
291
- # Pad after the existing lines so spinner visually stays at the bottom.
292
- rest_lines = rest_lines + ["\n"] * pad_count
293
-
294
282
  rest = "".join(rest_lines)
295
283
  rest = Text.from_ansi(rest)
296
284
  live = self.live
@@ -2,10 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import contextlib
4
4
  import math
5
+ import random
5
6
  import time
6
7
 
7
8
  import rich.status as rich_status
8
- from rich._spinners import SPINNERS
9
9
  from rich.color import Color
10
10
  from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
11
11
  from rich.spinner import Spinner as RichSpinner
@@ -17,18 +17,25 @@ from klaude_code import const
17
17
  from klaude_code.ui.rich.theme import ThemeKey
18
18
  from klaude_code.ui.terminal.color import get_last_terminal_background_rgb
19
19
 
20
- BREATHING_SPINNER_NAME = "dot"
20
+ # Use an existing Rich spinner name; BreathingSpinner overrides its rendering
21
+ BREATHING_SPINNER_NAME = "dots"
22
+
23
+ # Alternating glyphs for the breathing spinner - switches at each "transparent" point
24
+ # All glyphs are center-symmetric (point-symmetric)
25
+ _BREATHING_SPINNER_GLYPHS_BASE = [
26
+ # Stars
27
+ "✦",
28
+ "✶",
29
+ "✲",
30
+ "⏺",
31
+ "◆",
32
+ "❖",
33
+ ]
34
+
35
+ # Shuffle glyphs on module load for variety across sessions
36
+ BREATHING_SPINNER_GLYPHS = _BREATHING_SPINNER_GLYPHS_BASE.copy()
37
+ random.shuffle(BREATHING_SPINNER_GLYPHS)
21
38
 
22
- SPINNERS.update(
23
- {
24
- BREATHING_SPINNER_NAME: {
25
- "interval": 100,
26
- # Frames content is ignored by the custom breathing spinner implementation,
27
- # but we keep a single-frame list for correct width measurement.
28
- "frames": ["⏺"],
29
- }
30
- }
31
- )
32
39
 
33
40
  _process_start: float | None = None
34
41
 
@@ -126,6 +133,17 @@ def _breathing_intensity() -> float:
126
133
  return 0.5 * (1.0 - math.cos(2.0 * math.pi * phase))
127
134
 
128
135
 
136
+ def _breathing_glyph() -> str:
137
+ """Get the current glyph for the breathing spinner.
138
+
139
+ Alternates between glyphs at each breath cycle (when intensity reaches 0).
140
+ """
141
+ period = max(const.SPINNER_BREATH_PERIOD_SECONDS, 0.1)
142
+ elapsed = _elapsed_since_start()
143
+ cycle = int(elapsed / period)
144
+ return BREATHING_SPINNER_GLYPHS[cycle % len(BREATHING_SPINNER_GLYPHS)]
145
+
146
+
129
147
  def _breathing_style(console: Console, base_style: Style, intensity: float) -> Style:
130
148
  """Blend a base style's foreground color toward terminal background.
131
149
 
@@ -159,7 +177,7 @@ class ShimmerStatusText:
159
177
  def __init__(self, main_text: str | Text, main_style: ThemeKey) -> None:
160
178
  self._main_text = main_text if isinstance(main_text, Text) else Text(main_text)
161
179
  self._main_style = main_style
162
- self._hint_text = Text(" (esc to interrupt)")
180
+ self._hint_text = Text(const.STATUS_HINT_TEXT)
163
181
  self._hint_style = ThemeKey.STATUS_HINT
164
182
 
165
183
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
@@ -219,7 +237,7 @@ class BreathingSpinner(RichSpinner):
219
237
  intensity = _breathing_intensity()
220
238
  style = _breathing_style(console, base_style, intensity)
221
239
 
222
- glyph = self.frames[0] if self.frames else "⏺"
240
+ glyph = _breathing_glyph()
223
241
  frame = Text(glyph, style=style)
224
242
 
225
243
  if not self.text:
@@ -14,12 +14,12 @@ class Palette:
14
14
  blue: str
15
15
  orange: str
16
16
  magenta: str
17
- grey_blue: str
18
17
  grey1: str
19
18
  grey2: str
20
19
  grey3: str
21
20
  grey_green: str
22
21
  purple: str
22
+ lavender: str
23
23
  diff_add: str
24
24
  diff_remove: str
25
25
  code_theme: str
@@ -29,17 +29,17 @@ class Palette:
29
29
  LIGHT_PALETTE = Palette(
30
30
  red="red",
31
31
  yellow="yellow",
32
- green="spring_green4",
32
+ green="#00875f",
33
33
  cyan="cyan",
34
- blue="#3678b7",
34
+ blue="#3078C5",
35
35
  orange="#d77757",
36
36
  magenta="magenta",
37
- grey_blue="steel_blue",
38
37
  grey1="#667e90",
39
38
  grey2="#93a4b1",
40
39
  grey3="#c4ced4",
41
- grey_green="#96a696",
42
- purple="slate_blue3",
40
+ grey_green="#96a096",
41
+ purple="#5f5fd7",
42
+ lavender="#5f87af",
43
43
  diff_add="#2e5a32 on #e8f5e9",
44
44
  diff_remove="#5a2e32 on #ffebee",
45
45
  code_theme="ansi_light",
@@ -47,19 +47,19 @@ LIGHT_PALETTE = Palette(
47
47
  )
48
48
 
49
49
  DARK_PALETTE = Palette(
50
- red="indian_red",
50
+ red="#d75f5f",
51
51
  yellow="yellow",
52
- green="sea_green3",
52
+ green="#5fd787",
53
53
  cyan="cyan",
54
- blue="deep_sky_blue1",
54
+ blue="#00afff",
55
55
  orange="#e6704e",
56
56
  magenta="magenta",
57
- grey_blue="steel_blue",
58
57
  grey1="#99aabb",
59
58
  grey2="#778899",
60
59
  grey3="#646464",
61
60
  grey_green="#6d8672",
62
61
  purple="#afbafe",
62
+ lavender="#9898b8",
63
63
  diff_add="#c8e6c9 on #2e4a32",
64
64
  diff_remove="#ffcdd2 on #4a2e32",
65
65
  code_theme="ansi_dark",
@@ -107,6 +107,7 @@ class ThemeKey(str, Enum):
107
107
  TOOL_MARK = "tool.mark"
108
108
  TOOL_APPROVED = "tool.approved"
109
109
  TOOL_REJECTED = "tool.rejected"
110
+ TOOL_TIMEOUT = "tool.timeout"
110
111
  # THINKING
111
112
  THINKING = "thinking"
112
113
  THINKING_BOLD = "thinking.bold"
@@ -174,9 +175,9 @@ def get_theme(theme: str | None = None) -> Themes:
174
175
  ThemeKey.USER_INPUT_AT_PATTERN.value: palette.purple,
175
176
  ThemeKey.USER_INPUT_SLASH_COMMAND.value: "bold reverse " + palette.blue,
176
177
  # METADATA
177
- ThemeKey.METADATA.value: palette.grey_blue,
178
- ThemeKey.METADATA_DIM.value: "dim " + palette.grey_blue,
179
- ThemeKey.METADATA_BOLD.value: "bold " + palette.grey_blue,
178
+ ThemeKey.METADATA.value: palette.lavender,
179
+ ThemeKey.METADATA_DIM.value: "dim " + palette.lavender,
180
+ ThemeKey.METADATA_BOLD.value: "bold " + palette.lavender,
180
181
  # SPINNER_STATUS
181
182
  ThemeKey.SPINNER_STATUS.value: palette.blue,
182
183
  ThemeKey.SPINNER_STATUS_TEXT.value: palette.blue,
@@ -196,6 +197,7 @@ def get_theme(theme: str | None = None) -> Themes:
196
197
  ThemeKey.TOOL_MARK.value: "bold",
197
198
  ThemeKey.TOOL_APPROVED.value: palette.green + " bold reverse",
198
199
  ThemeKey.TOOL_REJECTED.value: palette.red + " bold reverse",
200
+ ThemeKey.TOOL_TIMEOUT.value: palette.yellow,
199
201
  # THINKING
200
202
  ThemeKey.THINKING.value: "italic " + palette.grey2,
201
203
  ThemeKey.THINKING_BOLD.value: "bold italic " + palette.grey1,
@@ -232,7 +234,7 @@ def get_theme(theme: str | None = None) -> Themes:
232
234
  markdown_theme=Theme(
233
235
  styles={
234
236
  "markdown.code": palette.purple,
235
- "markdown.code.panel": palette.grey3,
237
+ "markdown.code.border": palette.grey3,
236
238
  "markdown.h1": "bold reverse",
237
239
  "markdown.h1.border": palette.grey3,
238
240
  "markdown.h2.border": palette.grey3,
@@ -245,8 +247,7 @@ def get_theme(theme: str | None = None) -> Themes:
245
247
  ),
246
248
  thinking_markdown_theme=Theme(
247
249
  styles={
248
- "markdown.code": palette.grey1 + " on " + palette.text_background,
249
- "markdown.code.panel": palette.grey3,
250
+ "markdown.code": palette.grey1 + " italic on " + palette.text_background,
250
251
  "markdown.h1": "bold reverse",
251
252
  "markdown.h1.border": palette.grey3,
252
253
  "markdown.h2.border": palette.grey3,
@@ -255,6 +256,7 @@ def get_theme(theme: str | None = None) -> Themes:
255
256
  "markdown.hr": palette.grey3,
256
257
  "markdown.item.bullet": palette.grey2,
257
258
  "markdown.item.number": palette.grey2,
259
+ "markdown.code.block": palette.grey1,
258
260
  "markdown.strong": "bold italic " + palette.grey1,
259
261
  }
260
262
  ),
@@ -265,7 +267,6 @@ def get_theme(theme: str | None = None) -> Themes:
265
267
  Style(color=palette.blue),
266
268
  Style(color=palette.purple),
267
269
  Style(color=palette.orange),
268
- Style(color=palette.grey_blue),
269
270
  Style(color=palette.red),
270
271
  Style(color=palette.grey1),
271
272
  Style(color=palette.yellow),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.2.14
3
+ Version: 1.2.16
4
4
  Summary: Add your description here
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: openai>=1.102.0
@@ -22,6 +22,7 @@ An minimal and opinionated code agent with multi-model support.
22
22
  ## Key Features
23
23
  - **Adaptive Tooling**: Model-aware toolsets (Claude Code tools for Sonnet, Codex `apply_patch` for GPT-5.1/Codex).
24
24
  - **Multi-Provider Support**: Compatible with `anthropic-messages-api`,`openai-responses-api`, and `openai-compatible-api`(`openrouter-api`), featuring interleaved thinking, OpenRouter's provider sorting etc.
25
+ - **Structured Sub-Agent Output**: Main agent defines output JSON schema for sub-agents; sub-agents use `report_back` tool with constrained decoding to return schema-compliant structured data.
25
26
  - **Skill System**: Extensible support for loading Claude Skills.
26
27
  - **Session Management**: Robust context preservation with resumable sessions (`--continue`).
27
28
  - **Simple TUI**: Clean interface offering full visibility into model responses, reasoning and actions.
@@ -107,7 +108,7 @@ model_list:
107
108
  provider_routing:
108
109
  sort: throughput
109
110
  main_model: gpt-5.1-codex
110
- subagent_models:
111
+ sub_agent_models:
111
112
  oracle: gpt-5.1-high
112
113
  explore: haiku
113
114
  task: sonnet