klaude-code 2.5.3__py3-none-any.whl → 2.7.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 (60) hide show
  1. klaude_code/app/runtime.py +1 -1
  2. klaude_code/auth/__init__.py +10 -0
  3. klaude_code/auth/env.py +81 -0
  4. klaude_code/cli/auth_cmd.py +87 -8
  5. klaude_code/cli/config_cmd.py +5 -5
  6. klaude_code/cli/cost_cmd.py +159 -60
  7. klaude_code/cli/main.py +146 -65
  8. klaude_code/cli/self_update.py +7 -7
  9. klaude_code/config/builtin_config.py +23 -9
  10. klaude_code/config/config.py +19 -9
  11. klaude_code/const.py +10 -1
  12. klaude_code/core/reminders.py +4 -5
  13. klaude_code/core/turn.py +8 -9
  14. klaude_code/llm/google/client.py +12 -0
  15. klaude_code/llm/openai_compatible/stream.py +5 -1
  16. klaude_code/llm/openrouter/client.py +1 -0
  17. klaude_code/protocol/commands.py +0 -1
  18. klaude_code/protocol/events.py +214 -0
  19. klaude_code/protocol/sub_agent/image_gen.py +0 -4
  20. klaude_code/session/session.py +51 -18
  21. klaude_code/skill/loader.py +12 -13
  22. klaude_code/skill/manager.py +3 -3
  23. klaude_code/tui/command/__init__.py +1 -4
  24. klaude_code/tui/command/copy_cmd.py +1 -1
  25. klaude_code/tui/command/fork_session_cmd.py +4 -4
  26. klaude_code/tui/commands.py +0 -5
  27. klaude_code/tui/components/command_output.py +1 -1
  28. klaude_code/tui/components/metadata.py +4 -5
  29. klaude_code/tui/components/rich/markdown.py +60 -0
  30. klaude_code/tui/components/rich/theme.py +8 -0
  31. klaude_code/tui/components/sub_agent.py +6 -0
  32. klaude_code/tui/components/user_input.py +38 -27
  33. klaude_code/tui/display.py +11 -1
  34. klaude_code/tui/input/AGENTS.md +44 -0
  35. klaude_code/tui/input/completers.py +21 -21
  36. klaude_code/tui/input/drag_drop.py +197 -0
  37. klaude_code/tui/input/images.py +227 -0
  38. klaude_code/tui/input/key_bindings.py +173 -19
  39. klaude_code/tui/input/paste.py +71 -0
  40. klaude_code/tui/input/prompt_toolkit.py +13 -3
  41. klaude_code/tui/machine.py +90 -56
  42. klaude_code/tui/renderer.py +1 -62
  43. klaude_code/tui/runner.py +1 -1
  44. klaude_code/tui/terminal/image.py +40 -9
  45. klaude_code/tui/terminal/selector.py +52 -2
  46. {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/METADATA +32 -40
  47. {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/RECORD +49 -54
  48. klaude_code/cli/session_cmd.py +0 -87
  49. klaude_code/protocol/events/__init__.py +0 -63
  50. klaude_code/protocol/events/base.py +0 -18
  51. klaude_code/protocol/events/chat.py +0 -30
  52. klaude_code/protocol/events/lifecycle.py +0 -23
  53. klaude_code/protocol/events/metadata.py +0 -16
  54. klaude_code/protocol/events/streaming.py +0 -43
  55. klaude_code/protocol/events/system.py +0 -56
  56. klaude_code/protocol/events/tools.py +0 -27
  57. klaude_code/tui/command/terminal_setup_cmd.py +0 -248
  58. klaude_code/tui/input/clipboard.py +0 -152
  59. {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/WHEEL +0 -0
  60. {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Literal
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from klaude_code.protocol import llm_param, message, model
9
+ from klaude_code.protocol.commands import CommandName
10
+
11
+ __all__ = [
12
+ "AssistantImageDeltaEvent",
13
+ "AssistantTextDeltaEvent",
14
+ "AssistantTextEndEvent",
15
+ "AssistantTextStartEvent",
16
+ "CommandOutputEvent",
17
+ "DeveloperMessageEvent",
18
+ "EndEvent",
19
+ "ErrorEvent",
20
+ "Event",
21
+ "InterruptEvent",
22
+ "ReplayEventUnion",
23
+ "ReplayHistoryEvent",
24
+ "ResponseCompleteEvent",
25
+ "ResponseEvent",
26
+ "TaskFinishEvent",
27
+ "TaskMetadataEvent",
28
+ "TaskStartEvent",
29
+ "ThinkingDeltaEvent",
30
+ "ThinkingEndEvent",
31
+ "ThinkingStartEvent",
32
+ "TodoChangeEvent",
33
+ "ToolCallEvent",
34
+ "ToolCallStartEvent",
35
+ "ToolResultEvent",
36
+ "TurnEndEvent",
37
+ "TurnStartEvent",
38
+ "UsageEvent",
39
+ "UserMessageEvent",
40
+ "WelcomeEvent",
41
+ ]
42
+
43
+
44
+ class Event(BaseModel):
45
+ """Base event."""
46
+
47
+ session_id: str
48
+ timestamp: float = Field(default_factory=time.time)
49
+
50
+
51
+ class ResponseEvent(Event):
52
+ """Event associated with a single model response."""
53
+
54
+ response_id: str | None = None
55
+
56
+
57
+ class UserMessageEvent(Event):
58
+ content: str
59
+ images: list[message.ImageURLPart] | None = None
60
+
61
+
62
+ class DeveloperMessageEvent(Event):
63
+ """DeveloperMessages are reminders in user messages or tool results."""
64
+
65
+ item: message.DeveloperMessage
66
+
67
+
68
+ class TodoChangeEvent(Event):
69
+ todos: list[model.TodoItem]
70
+
71
+
72
+ class CommandOutputEvent(Event):
73
+ """Event for command output display. Not persisted to session history."""
74
+
75
+ command_name: CommandName | str
76
+ content: str = ""
77
+ ui_extra: model.ToolResultUIExtra | None = None
78
+ is_error: bool = False
79
+
80
+
81
+ class TaskStartEvent(Event):
82
+ sub_agent_state: model.SubAgentState | None = None
83
+ model_id: str | None = None
84
+
85
+
86
+ class TaskFinishEvent(Event):
87
+ task_result: str
88
+ has_structured_output: bool = False
89
+
90
+
91
+ class TurnStartEvent(Event):
92
+ pass
93
+
94
+
95
+ class TurnEndEvent(Event):
96
+ pass
97
+
98
+
99
+ class UsageEvent(ResponseEvent):
100
+ usage: model.Usage
101
+
102
+
103
+ class TaskMetadataEvent(Event):
104
+ metadata: model.TaskMetadataItem
105
+ cancelled: bool = False
106
+
107
+
108
+ class ThinkingStartEvent(ResponseEvent):
109
+ pass
110
+
111
+
112
+ class ThinkingDeltaEvent(ResponseEvent):
113
+ content: str
114
+
115
+
116
+ class ThinkingEndEvent(ResponseEvent):
117
+ pass
118
+
119
+
120
+ class AssistantTextStartEvent(ResponseEvent):
121
+ pass
122
+
123
+
124
+ class AssistantTextDeltaEvent(ResponseEvent):
125
+ content: str
126
+
127
+
128
+ class AssistantTextEndEvent(ResponseEvent):
129
+ pass
130
+
131
+
132
+ class AssistantImageDeltaEvent(ResponseEvent):
133
+ file_path: str
134
+
135
+
136
+ class ToolCallStartEvent(ResponseEvent):
137
+ tool_call_id: str
138
+ tool_name: str
139
+
140
+
141
+ class ResponseCompleteEvent(ResponseEvent):
142
+ """Final snapshot of the model response."""
143
+
144
+ content: str
145
+ thinking_text: str | None = None
146
+
147
+
148
+ class WelcomeEvent(Event):
149
+ work_dir: str
150
+ llm_config: llm_param.LLMConfigParameter
151
+ show_klaude_code_info: bool = True
152
+ loaded_skills: dict[str, list[str]] = Field(default_factory=dict)
153
+
154
+
155
+ class ErrorEvent(Event):
156
+ error_message: str
157
+ can_retry: bool = False
158
+
159
+
160
+ class InterruptEvent(Event):
161
+ pass
162
+
163
+
164
+ class EndEvent(Event):
165
+ """Global display shutdown."""
166
+
167
+ session_id: str = "__app__"
168
+
169
+
170
+ type ReplayEventUnion = (
171
+ TaskStartEvent
172
+ | TaskFinishEvent
173
+ | TurnStartEvent
174
+ | ThinkingStartEvent
175
+ | ThinkingDeltaEvent
176
+ | ThinkingEndEvent
177
+ | AssistantTextStartEvent
178
+ | AssistantTextDeltaEvent
179
+ | AssistantTextEndEvent
180
+ | AssistantImageDeltaEvent
181
+ | ToolCallEvent
182
+ | ToolResultEvent
183
+ | UserMessageEvent
184
+ | TaskMetadataEvent
185
+ | InterruptEvent
186
+ | DeveloperMessageEvent
187
+ | ErrorEvent
188
+ )
189
+
190
+
191
+ class ReplayHistoryEvent(Event):
192
+ events: list[ReplayEventUnion]
193
+ updated_at: float
194
+ is_load: bool = True
195
+
196
+
197
+ class ToolCallEvent(ResponseEvent):
198
+ tool_call_id: str
199
+ tool_name: str
200
+ arguments: str
201
+
202
+
203
+ class ToolResultEvent(ResponseEvent):
204
+ tool_call_id: str
205
+ tool_name: str
206
+ result: str
207
+ ui_extra: model.ToolResultUIExtra | None = None
208
+ status: Literal["success", "error", "aborted"]
209
+ task_metadata: model.TaskMetadata | None = None
210
+ is_last_in_turn: bool = True
211
+
212
+ @property
213
+ def is_error(self) -> bool:
214
+ return self.status in ("error", "aborted")
@@ -66,10 +66,6 @@ IMAGE_GEN_PARAMETERS: dict[str, Any] = {
66
66
  "enum": ["1K", "2K", "4K"],
67
67
  "description": "Output size for Nano Banana Pro (must use uppercase K).",
68
68
  },
69
- "extra": {
70
- "type": "object",
71
- "description": "Provider/model-specific extra parameters (future-proofing).",
72
- },
73
69
  },
74
70
  "additionalProperties": False,
75
71
  },
@@ -304,24 +304,57 @@ class Session(BaseModel):
304
304
  yield events.TurnStartEvent(session_id=self.id)
305
305
  match it:
306
306
  case message.AssistantMessage() as am:
307
- content = message.join_text_parts(am.parts)
308
- images = [part for part in am.parts if isinstance(part, message.ImageFilePart)]
309
- last_assistant_content = message.format_saved_images(images, content)
310
- thinking_text = "".join(
311
- part.text for part in am.parts if isinstance(part, message.ThinkingTextPart)
312
- )
313
- for image in images:
314
- yield events.AssistantImageDeltaEvent(
315
- file_path=image.file_path,
316
- response_id=am.response_id,
317
- session_id=self.id,
318
- )
319
- yield events.ResponseCompleteEvent(
320
- thinking_text=thinking_text or None,
321
- content=content,
322
- response_id=am.response_id,
323
- session_id=self.id,
324
- )
307
+ all_images = [part for part in am.parts if isinstance(part, message.ImageFilePart)]
308
+ full_content = message.join_text_parts(am.parts)
309
+ last_assistant_content = message.format_saved_images(all_images, full_content)
310
+
311
+ # Reconstruct streaming boundaries from saved parts.
312
+ # This allows replay to reuse the same TUI state machine as live events.
313
+ thinking_open = False
314
+ assistant_open = False
315
+
316
+ for part in am.parts:
317
+ if isinstance(part, message.ThinkingTextPart):
318
+ if assistant_open:
319
+ assistant_open = False
320
+ yield events.AssistantTextEndEvent(response_id=am.response_id, session_id=self.id)
321
+ if not thinking_open:
322
+ thinking_open = True
323
+ yield events.ThinkingStartEvent(response_id=am.response_id, session_id=self.id)
324
+ if part.text:
325
+ yield events.ThinkingDeltaEvent(
326
+ content=part.text,
327
+ response_id=am.response_id,
328
+ session_id=self.id,
329
+ )
330
+ continue
331
+
332
+ if thinking_open:
333
+ thinking_open = False
334
+ yield events.ThinkingEndEvent(response_id=am.response_id, session_id=self.id)
335
+
336
+ if isinstance(part, message.TextPart):
337
+ if not assistant_open:
338
+ assistant_open = True
339
+ yield events.AssistantTextStartEvent(response_id=am.response_id, session_id=self.id)
340
+ if part.text:
341
+ yield events.AssistantTextDeltaEvent(
342
+ content=part.text,
343
+ response_id=am.response_id,
344
+ session_id=self.id,
345
+ )
346
+ elif isinstance(part, message.ImageFilePart):
347
+ yield events.AssistantImageDeltaEvent(
348
+ file_path=part.file_path,
349
+ response_id=am.response_id,
350
+ session_id=self.id,
351
+ )
352
+
353
+ if thinking_open:
354
+ yield events.ThinkingEndEvent(response_id=am.response_id, session_id=self.id)
355
+ if assistant_open:
356
+ yield events.AssistantTextEndEvent(response_id=am.response_id, session_id=self.id)
357
+
325
358
  for part in am.parts:
326
359
  if not isinstance(part, message.ToolCallPart):
327
360
  continue
@@ -209,22 +209,21 @@ class SkillLoader:
209
209
  """Get list of all loaded skill names"""
210
210
  return list(self.loaded_skills.keys())
211
211
 
212
- def get_skills_xml(self) -> str:
213
- """Generate Level 1 metadata in XML format for tool description
212
+ def get_skills_yaml(self) -> str:
213
+ """Generate skill metadata in YAML format for system prompt.
214
214
 
215
215
  Returns:
216
- XML string with all skill metadata
216
+ YAML string with all skill metadata
217
217
  """
218
- xml_parts: list[str] = []
219
- # Prefer showing higher-priority skills first (project > user > system).
218
+ yaml_parts: list[str] = []
220
219
  location_order = {"project": 0, "user": 1, "system": 2}
221
220
  for skill in sorted(self.loaded_skills.values(), key=lambda s: location_order.get(s.location, 3)):
222
- xml_parts.append(
223
- f"""<skill>
224
- <name>{skill.name}</name>
225
- <description>{skill.description}</description>
226
- <scope>{skill.location}</scope>
227
- <location>{skill.skill_path}</location>
228
- </skill>"""
221
+ # Escape description for YAML (handle multi-line and special chars)
222
+ desc = skill.description.replace("\n", " ").strip()
223
+ yaml_parts.append(
224
+ f"- name: {skill.name}\n"
225
+ f" description: {desc}\n"
226
+ f" scope: {skill.location}\n"
227
+ f" location: {skill.skill_path}"
229
228
  )
230
- return "\n".join(xml_parts)
229
+ return "\n".join(yaml_parts)
@@ -80,8 +80,8 @@ def format_available_skills_for_system_prompt() -> str:
80
80
 
81
81
  try:
82
82
  loader = _ensure_initialized()
83
- skills_xml = loader.get_skills_xml().strip()
84
- if not skills_xml:
83
+ skills_yaml = loader.get_skills_yaml().strip()
84
+ if not skills_yaml:
85
85
  return ""
86
86
 
87
87
  return f"""
@@ -102,7 +102,7 @@ Important:
102
102
  The list below is metadata only (name/description/location). The full instructions live in the referenced file.
103
103
 
104
104
  <available_skills>
105
- {skills_xml}
105
+ {skills_yaml}
106
106
  </available_skills>"""
107
107
  except Exception:
108
108
  # Skills are an optional enhancement; do not fail prompt construction if discovery breaks.
@@ -40,7 +40,6 @@ def ensure_commands_loaded() -> None:
40
40
  from .resume_cmd import ResumeCommand
41
41
  from .status_cmd import StatusCommand
42
42
  from .sub_agent_model_cmd import SubAgentModelCommand
43
- from .terminal_setup_cmd import TerminalSetupCommand
44
43
  from .thinking_cmd import ThinkingCommand
45
44
 
46
45
  # Register in desired display order
@@ -55,7 +54,6 @@ def ensure_commands_loaded() -> None:
55
54
  register(StatusCommand())
56
55
  register(ResumeCommand())
57
56
  register(ExportOnlineCommand())
58
- register(TerminalSetupCommand())
59
57
  register(DebugCommand())
60
58
  register(ClearCommand())
61
59
 
@@ -76,7 +74,6 @@ def __getattr__(name: str) -> object:
76
74
  "ResumeCommand": "resume_cmd",
77
75
  "StatusCommand": "status_cmd",
78
76
  "SubAgentModelCommand": "sub_agent_model_cmd",
79
- "TerminalSetupCommand": "terminal_setup_cmd",
80
77
  "ThinkingCommand": "thinking_cmd",
81
78
  }
82
79
  if name in _commands_map:
@@ -91,7 +88,7 @@ __all__ = [
91
88
  # Command classes are lazily loaded via __getattr__
92
89
  # "ClearCommand", "DiffCommand", "HelpCommand", "ModelCommand",
93
90
  # "ExportCommand", "RefreshTerminalCommand", "ReleaseNotesCommand",
94
- # "StatusCommand", "TerminalSetupCommand",
91
+ # "StatusCommand",
95
92
  "CommandABC",
96
93
  "CommandResult",
97
94
  "dispatch_command",
@@ -1,5 +1,5 @@
1
1
  from klaude_code.protocol import commands, events, message
2
- from klaude_code.tui.input.clipboard import copy_to_clipboard
2
+ from klaude_code.tui.input.key_bindings import copy_to_clipboard
3
3
 
4
4
  from .command_abc import Agent, CommandABC, CommandResult
5
5
 
@@ -6,7 +6,7 @@ from typing import Literal
6
6
  from prompt_toolkit.styles import Style, merge_styles
7
7
 
8
8
  from klaude_code.protocol import commands, events, message, model
9
- from klaude_code.tui.input.clipboard import copy_to_clipboard
9
+ from klaude_code.tui.input.key_bindings import copy_to_clipboard
10
10
  from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, select_one
11
11
 
12
12
  from .command_abc import Agent, CommandABC, CommandResult
@@ -194,7 +194,7 @@ class ForkSessionCommand(CommandABC):
194
194
 
195
195
  @property
196
196
  def summary(self) -> str:
197
- return "Fork the current session and show a resume-by-id command"
197
+ return "Fork the current session and show a resume command"
198
198
 
199
199
  @property
200
200
  def is_interactive(self) -> bool:
@@ -220,7 +220,7 @@ class ForkSessionCommand(CommandABC):
220
220
  new_session = agent.session.fork()
221
221
  await new_session.wait_for_flush()
222
222
 
223
- resume_cmd = f"klaude --resume-by-id {new_session.id}"
223
+ resume_cmd = f"klaude --resume {new_session.id}"
224
224
  copy_to_clipboard(resume_cmd)
225
225
 
226
226
  event = events.CommandOutputEvent(
@@ -249,7 +249,7 @@ class ForkSessionCommand(CommandABC):
249
249
  # Build result message
250
250
  fork_description = "entire conversation" if selected == -1 else f"up to message index {selected}"
251
251
 
252
- resume_cmd = f"klaude --resume-by-id {new_session.id}"
252
+ resume_cmd = f"klaude --resume {new_session.id}"
253
253
  copy_to_clipboard(resume_cmd)
254
254
 
255
255
  event = events.CommandOutputEvent(
@@ -13,11 +13,6 @@ class RenderCommand:
13
13
  pass
14
14
 
15
15
 
16
- @dataclass(frozen=True, slots=True)
17
- class RenderReplayHistory(RenderCommand):
18
- event: events.ReplayHistoryEvent
19
-
20
-
21
16
  @dataclass(frozen=True, slots=True)
22
17
  class RenderWelcome(RenderCommand):
23
18
  event: events.WelcomeEvent
@@ -50,7 +50,7 @@ def _render_fork_session_output(e: events.CommandOutputEvent) -> RenderableType:
50
50
  grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
51
51
 
52
52
  grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
53
- grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.TOOL_RESULT_BOLD))
53
+ grid.add_row(Text(f" klaude --resume {session_id}", style=ThemeKey.TOOL_RESULT_BOLD))
54
54
 
55
55
  return Padding.indent(grid, level=2)
56
56
 
@@ -30,13 +30,12 @@ def _render_task_metadata_block(
30
30
  currency = metadata.usage.currency if metadata.usage else "USD"
31
31
  currency_symbol = "¥" if currency == "CNY" else "$"
32
32
 
33
- # Second column: model@provider description / tokens / cost / …
33
+ # Second column: provider/model description / tokens / cost / …
34
34
  content = Text()
35
- content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
36
35
  if metadata.provider is not None:
37
- content.append_text(Text("@", style=ThemeKey.METADATA)).append_text(
38
- Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA)
39
- )
36
+ content.append_text(Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA))
37
+ content.append_text(Text("/", style=ThemeKey.METADATA_DIM))
38
+ content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
40
39
  if metadata.description:
41
40
  content.append_text(Text(" ", style=ThemeKey.METADATA)).append_text(
42
41
  Text(metadata.description, style=ThemeKey.METADATA_ITALIC)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import contextlib
4
4
  import io
5
+ import re
5
6
  import time
6
7
  from collections.abc import Callable
7
8
  from typing import Any, ClassVar
@@ -26,6 +27,63 @@ from klaude_code.const import (
26
27
  )
27
28
  from klaude_code.tui.components.rich.code_panel import CodePanel
28
29
 
30
+ _THINKING_HTML_BLOCK_RE = re.compile(
31
+ r"\A\s*<thinking>\s*\n?(?P<body>.*?)(?:\n\s*)?</thinking>\s*\Z",
32
+ flags=re.IGNORECASE | re.DOTALL,
33
+ )
34
+
35
+ _HTML_COMMENT_BLOCK_RE = re.compile(r"\A\s*<!--.*?-->\s*\Z", flags=re.DOTALL)
36
+
37
+
38
+ class ThinkingHTMLBlock(MarkdownElement):
39
+ """Render `<thinking>...</thinking>` HTML blocks as Rich Markdown.
40
+
41
+ markdown-it-py treats custom tags like `<thinking>` as HTML blocks, and Rich
42
+ Markdown ignores HTML blocks by default. This element restores visibility by
43
+ re-parsing the inner content as Markdown and applying a dedicated style.
44
+
45
+ Non-thinking HTML blocks (including comment sentinels like `<!-- -->`) render
46
+ no visible output, matching Rich's default behavior.
47
+ """
48
+
49
+ new_line: ClassVar[bool] = True
50
+
51
+ @classmethod
52
+ def create(cls, markdown: Markdown, token: Token) -> ThinkingHTMLBlock:
53
+ return cls(content=token.content or "", code_theme=markdown.code_theme)
54
+
55
+ def __init__(self, *, content: str, code_theme: str) -> None:
56
+ self._content = content
57
+ self._code_theme = code_theme
58
+
59
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
60
+ stripped = self._content.strip()
61
+
62
+ # Keep HTML comments invisible. MarkdownStream relies on a comment sentinel
63
+ # (`<!-- -->`) to preserve inter-block spacing in some streaming frames.
64
+ if _HTML_COMMENT_BLOCK_RE.match(stripped):
65
+ return
66
+
67
+ match = _THINKING_HTML_BLOCK_RE.match(stripped)
68
+ if match is None:
69
+ return
70
+
71
+ body = match.group("body").strip("\n")
72
+ if not body.strip():
73
+ return
74
+
75
+ # Render as a single line to avoid the extra blank lines produced by
76
+ # paragraph/block rendering.
77
+ collapsed = " ".join(body.split())
78
+ if not collapsed:
79
+ return
80
+
81
+ text = Text()
82
+ text.append("<thinking>", style="markdown.thinking.tag")
83
+ text.append(collapsed, style="markdown.thinking")
84
+ text.append("</thinking>", style="markdown.thinking.tag")
85
+ yield text
86
+
29
87
 
30
88
  class NoInsetCodeBlock(CodeBlock):
31
89
  """A code block with syntax highlighting and no padding."""
@@ -105,6 +163,7 @@ class NoInsetMarkdown(Markdown):
105
163
  "heading_open": LeftHeading,
106
164
  "hr": Divider,
107
165
  "table_open": MarkdownTable,
166
+ "html_block": ThinkingHTMLBlock,
108
167
  }
109
168
 
110
169
 
@@ -118,6 +177,7 @@ class ThinkingMarkdown(Markdown):
118
177
  "heading_open": LeftHeading,
119
178
  "hr": Divider,
120
179
  "table_open": MarkdownTable,
180
+ "html_block": ThinkingHTMLBlock,
121
181
  }
122
182
 
123
183
 
@@ -331,7 +331,14 @@ def get_theme(theme: str | None = None) -> Themes:
331
331
  markdown_theme=Theme(
332
332
  styles={
333
333
  "markdown.code": palette.purple,
334
+ # Render degraded `<thinking>...</thinking>` blocks inside assistant markdown.
335
+ # This must live in markdown_theme (not just thinking_markdown_theme) because
336
+ # it is used while rendering assistant output.
337
+ "markdown.thinking": "italic " + palette.grey2,
338
+ "markdown.thinking.tag": palette.grey2,
334
339
  "markdown.code.border": palette.grey3,
340
+ # Used by ThinkingMarkdown when rendering `<thinking>` blocks.
341
+ "markdown.code.block": palette.grey1,
335
342
  "markdown.h1": "bold reverse",
336
343
  "markdown.h1.border": palette.grey3,
337
344
  "markdown.h2": "bold underline",
@@ -353,6 +360,7 @@ def get_theme(theme: str | None = None) -> Themes:
353
360
  "markdown.code": palette.grey1 + " italic on " + palette.code_background,
354
361
  "markdown.code.block": palette.grey1,
355
362
  "markdown.code.border": palette.grey3,
363
+ "markdown.thinking.tag": palette.grey2 + " dim",
356
364
  "markdown.h1": "bold reverse",
357
365
  "markdown.h1.border": palette.grey3,
358
366
  "markdown.h3": "bold " + palette.grey1,
@@ -135,6 +135,7 @@ def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAg
135
135
  description = profile.name
136
136
  prompt = ""
137
137
  output_schema: dict[str, Any] | None = None
138
+ generation: dict[str, Any] | None = None
138
139
  resume: str | None = None
139
140
  if e.arguments:
140
141
  try:
@@ -155,10 +156,15 @@ def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAg
155
156
  schema_value = payload.get(profile.output_schema_arg)
156
157
  if isinstance(schema_value, dict):
157
158
  output_schema = cast(dict[str, Any], schema_value)
159
+ # Extract generation config for ImageGen
160
+ generation_value = payload.get("generation")
161
+ if isinstance(generation_value, dict):
162
+ generation = cast(dict[str, Any], generation_value)
158
163
  return model.SubAgentState(
159
164
  sub_agent_type=profile.name,
160
165
  sub_agent_desc=description,
161
166
  sub_agent_prompt=prompt,
162
167
  resume=resume,
163
168
  output_schema=output_schema,
169
+ generation=generation,
164
170
  )