klaude-code 1.2.6__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import html
4
+ import importlib.resources
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+
8
+ from klaude_code import const
9
+
10
+
11
+ def artifacts_dir() -> Path:
12
+ return Path(const.TOOL_OUTPUT_TRUNCATION_DIR) / "mermaid"
13
+
14
+
15
+ @lru_cache(maxsize=1)
16
+ def load_template() -> str:
17
+ template_file = importlib.resources.files("klaude_code.session.templates").joinpath("mermaid_viewer.html")
18
+ return template_file.read_text(encoding="utf-8")
19
+
20
+
21
+ def ensure_viewer_file(*, code: str, link: str, tool_call_id: str) -> Path | None:
22
+ """Create a local HTML viewer with large preview + editor."""
23
+
24
+ if not tool_call_id:
25
+ return None
26
+
27
+ safe_id = tool_call_id.replace("/", "_")
28
+ path = artifacts_dir() / f"mermaid-viewer-{safe_id}.html"
29
+ if path.exists():
30
+ return path
31
+
32
+ try:
33
+ path.parent.mkdir(parents=True, exist_ok=True)
34
+
35
+ escaped_code = html.escape(code)
36
+ escaped_view_link = html.escape(link, quote=True)
37
+ escaped_edit_link = html.escape(link.replace("/view#pako:", "/edit#pako:"), quote=True)
38
+
39
+ template = load_template()
40
+ content = (
41
+ template.replace("__KLAUDE_VIEW_LINK__", escaped_view_link)
42
+ .replace("__KLAUDE_EDIT_LINK__", escaped_edit_link)
43
+ .replace("__KLAUDE_CODE__", escaped_code)
44
+ )
45
+ path.write_text(content, encoding="utf-8")
46
+ except OSError:
47
+ return None
48
+
49
+ return path
50
+
51
+
52
+ def build_viewer(*, code: str, link: str, tool_call_id: str) -> Path | None:
53
+ """Create a local Mermaid viewer HTML file."""
54
+
55
+ if not code:
56
+ return None
57
+ return ensure_viewer_file(code=code, link=link, tool_call_id=tool_call_id)
@@ -1,14 +1,15 @@
1
- from importlib.metadata import version
1
+ from importlib.metadata import PackageNotFoundError, version
2
2
 
3
3
  from rich import box
4
- from rich.box import Box
5
4
  from rich.console import Group, RenderableType
6
5
  from rich.padding import Padding
7
6
  from rich.panel import Panel
8
7
  from rich.text import Text
9
8
 
10
- from klaude_code.protocol import events
9
+ from klaude_code import const
10
+ from klaude_code.protocol import events, model
11
11
  from klaude_code.trace import is_debug_enabled
12
+ from klaude_code.ui.renderers.common import create_grid
12
13
  from klaude_code.ui.rich.theme import ThemeKey
13
14
  from klaude_code.ui.utils.common import format_number
14
15
 
@@ -17,80 +18,96 @@ def _get_version() -> str:
17
18
  """Get the current version of klaude-code."""
18
19
  try:
19
20
  return version("klaude-code")
20
- except Exception:
21
+ except PackageNotFoundError:
21
22
  return "unknown"
22
23
 
23
24
 
24
- def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
25
- metadata = e.metadata
25
+ def _render_task_metadata_block(
26
+ metadata: model.TaskMetadata,
27
+ *,
28
+ is_sub_agent: bool = False,
29
+ show_context_and_time: bool = True,
30
+ ) -> RenderableType:
31
+ """Render a single TaskMetadata block.
26
32
 
27
- # Line 1: Model and Provider
28
- model_text = Text()
29
- model_text.append_text(Text("- ", style=ThemeKey.METADATA_BOLD)).append_text(
30
- Text(metadata.model_name, style=ThemeKey.METADATA_BOLD)
31
- )
33
+ Args:
34
+ metadata: The TaskMetadata to render.
35
+ is_sub_agent: Whether this is a sub-agent block.
36
+ show_context_and_time: Whether to show context usage percent and time.
37
+
38
+ Returns:
39
+ A renderable for this metadata block.
40
+ """
41
+ grid = create_grid()
42
+
43
+ # Get currency symbol
44
+ currency = metadata.usage.currency if metadata.usage else "USD"
45
+ currency_symbol = "¥" if currency == "CNY" else "$"
46
+
47
+ # First column: mark only
48
+ mark = Text("└", style=ThemeKey.METADATA_DIM) if is_sub_agent else Text("⇅", style=ThemeKey.METADATA)
49
+
50
+ # Second column: model@provider / tokens / cost / ...
51
+ content = Text()
52
+ content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
32
53
  if metadata.provider is not None:
33
- model_text.append_text(Text("@", style=ThemeKey.METADATA)).append_text(
54
+ content.append_text(Text("@", style=ThemeKey.METADATA)).append_text(
34
55
  Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA)
35
56
  )
36
57
 
37
- renderables: list[RenderableType] = [model_text]
38
-
39
- # Line 2: Token consumption, Context, TPS, Cost
58
+ # All info parts (tokens, cost, context, etc.)
40
59
  parts: list[Text] = []
41
60
 
42
61
  if metadata.usage is not None:
43
- # Input
44
- input_parts: list[tuple[str, str]] = [
45
- ("input:", ThemeKey.METADATA_DIM),
46
- (format_number(metadata.usage.input_tokens), ThemeKey.METADATA_DIM),
62
+ # Tokens: ↑ 37k cache 5k ↓ 907 think 45k
63
+ token_parts: list[Text] = [
64
+ Text.assemble(("", ThemeKey.METADATA_DIM), (format_number(metadata.usage.input_tokens), ThemeKey.METADATA))
47
65
  ]
48
- if metadata.usage.input_cost is not None:
49
- input_parts.append((f"(${metadata.usage.input_cost:.4f})", ThemeKey.METADATA_DIM))
50
- parts.append(Text.assemble(*input_parts))
51
-
52
- # Cached
53
66
  if metadata.usage.cached_tokens > 0:
54
- cached_parts: list[tuple[str, str]] = [
55
- ("cached:", ThemeKey.METADATA_DIM),
56
- (format_number(metadata.usage.cached_tokens), ThemeKey.METADATA_DIM),
57
- ]
58
- if metadata.usage.cache_read_cost is not None:
59
- cached_parts.append((f"(${metadata.usage.cache_read_cost:.4f})", ThemeKey.METADATA_DIM))
60
- parts.append(Text.assemble(*cached_parts))
61
-
62
- # Output
63
- output_parts: list[tuple[str, str]] = [
64
- ("output:", ThemeKey.METADATA_DIM),
65
- (format_number(metadata.usage.output_tokens), ThemeKey.METADATA_DIM),
66
- ]
67
- if metadata.usage.output_cost is not None:
68
- output_parts.append((f"(${metadata.usage.output_cost:.4f})", ThemeKey.METADATA_DIM))
69
- parts.append(Text.assemble(*output_parts))
70
-
71
- # Reasoning
67
+ token_parts.append(
68
+ Text.assemble(
69
+ Text("cache ", style=ThemeKey.METADATA_DIM),
70
+ Text(format_number(metadata.usage.cached_tokens), style=ThemeKey.METADATA),
71
+ )
72
+ )
73
+ token_parts.append(
74
+ Text.assemble(
75
+ ("↓", ThemeKey.METADATA_DIM), (format_number(metadata.usage.output_tokens), ThemeKey.METADATA)
76
+ )
77
+ )
72
78
  if metadata.usage.reasoning_tokens > 0:
73
- parts.append(
79
+ token_parts.append(
74
80
  Text.assemble(
75
- ("thinking", ThemeKey.METADATA_DIM),
76
- (":", ThemeKey.METADATA_DIM),
77
- (
78
- format_number(metadata.usage.reasoning_tokens),
79
- ThemeKey.METADATA_DIM,
80
- ),
81
+ ("think ", ThemeKey.METADATA_DIM),
82
+ (format_number(metadata.usage.reasoning_tokens), ThemeKey.METADATA),
81
83
  )
82
84
  )
85
+ parts.append(Text(" · ").join(token_parts))
83
86
 
84
- # Context
85
- if metadata.usage.context_usage_percent is not None:
87
+ # Cost
88
+ if metadata.usage is not None and metadata.usage.total_cost is not None:
89
+ parts.append(
90
+ Text.assemble(
91
+ (currency_symbol, ThemeKey.METADATA_DIM),
92
+ (f"{metadata.usage.total_cost:.4f}", ThemeKey.METADATA),
93
+ )
94
+ )
95
+ if metadata.usage is not None:
96
+ # Context (only for main agent)
97
+ if show_context_and_time and metadata.usage.context_usage_percent is not None:
98
+ context_size = format_number(metadata.usage.context_size or 0)
99
+ # Calculate effective limit (same as Usage.context_usage_percent)
100
+ effective_limit = (metadata.usage.context_limit or 0) - (
101
+ metadata.usage.max_tokens or const.DEFAULT_MAX_TOKENS
102
+ )
103
+ effective_limit_str = format_number(effective_limit) if effective_limit > 0 else "?"
86
104
  parts.append(
87
105
  Text.assemble(
88
- ("context", ThemeKey.METADATA_DIM),
89
- (":", ThemeKey.METADATA_DIM),
90
- (
91
- f"{metadata.usage.context_usage_percent:.1f}%",
92
- ThemeKey.METADATA_DIM,
93
- ),
106
+ ("context ", ThemeKey.METADATA_DIM),
107
+ (context_size, ThemeKey.METADATA),
108
+ ("/", ThemeKey.METADATA_DIM),
109
+ (effective_limit_str, ThemeKey.METADATA),
110
+ (f" ({metadata.usage.context_usage_percent:.1f}%)", ThemeKey.METADATA_DIM),
94
111
  )
95
112
  )
96
113
 
@@ -98,44 +115,63 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
98
115
  if metadata.usage.throughput_tps is not None:
99
116
  parts.append(
100
117
  Text.assemble(
101
- ("tps", ThemeKey.METADATA_DIM),
102
- (":", ThemeKey.METADATA_DIM),
103
- (f"{metadata.usage.throughput_tps:.1f}", ThemeKey.METADATA_DIM),
118
+ (f"{metadata.usage.throughput_tps:.1f} ", ThemeKey.METADATA),
119
+ ("avg-tps", ThemeKey.METADATA_DIM),
120
+ )
121
+ )
122
+
123
+ # First token latency
124
+ if metadata.usage.first_token_latency_ms is not None:
125
+ parts.append(
126
+ Text.assemble(
127
+ (f"{metadata.usage.first_token_latency_ms:.0f}", ThemeKey.METADATA),
128
+ ("ms avg-ftl", ThemeKey.METADATA_DIM),
104
129
  )
105
130
  )
106
131
 
107
132
  # Duration
108
- if metadata.task_duration_s is not None:
133
+ if show_context_and_time and metadata.task_duration_s is not None:
109
134
  parts.append(
110
135
  Text.assemble(
111
- ("time", ThemeKey.METADATA_DIM),
112
- (":", ThemeKey.METADATA_DIM),
113
- (f"{metadata.task_duration_s:.1f}s", ThemeKey.METADATA_DIM),
136
+ (f"{metadata.task_duration_s:.1f}", ThemeKey.METADATA),
137
+ ("s", ThemeKey.METADATA_DIM),
114
138
  )
115
139
  )
116
140
 
117
- # Cost (USD)
118
- if metadata.usage is not None and metadata.usage.total_cost is not None:
141
+ # Turn count
142
+ if show_context_and_time and metadata.turn_count > 0:
119
143
  parts.append(
120
144
  Text.assemble(
121
- ("cost", ThemeKey.METADATA_DIM),
122
- (":", ThemeKey.METADATA_DIM),
123
- (f"${metadata.usage.total_cost:.4f}", ThemeKey.METADATA_DIM),
145
+ (str(metadata.turn_count), ThemeKey.METADATA),
146
+ (" turns", ThemeKey.METADATA_DIM),
124
147
  )
125
148
  )
126
149
 
127
150
  if parts:
128
- line2 = Text("/", style=ThemeKey.METADATA_DIM).join(parts)
129
- renderables.append(Padding(line2, (0, 0, 0, 2)))
151
+ content.append_text(Text(" · ", style=ThemeKey.METADATA_DIM))
152
+ content.append_text(Text(" · ", style=ThemeKey.METADATA_DIM).join(parts))
153
+
154
+ grid.add_row(mark, content)
155
+ return grid if not is_sub_agent else Padding(grid, (0, 0, 0, 2))
156
+
157
+
158
+ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
159
+ """Render task metadata including main agent and sub-agents."""
160
+ renderables: list[RenderableType] = []
161
+
162
+ renderables.append(
163
+ _render_task_metadata_block(e.metadata.main_agent, is_sub_agent=False, show_context_and_time=True)
164
+ )
165
+
166
+ # Render each sub-agent metadata block
167
+ for meta in e.metadata.sub_agent_task_metadata:
168
+ renderables.append(_render_task_metadata_block(meta, is_sub_agent=True, show_context_and_time=False))
130
169
 
131
170
  return Group(*renderables)
132
171
 
133
172
 
134
- def render_welcome(e: events.WelcomeEvent, *, box_style: Box | None = None) -> RenderableType:
173
+ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
135
174
  """Render the welcome panel with model info and settings."""
136
- if box_style is None:
137
- box_style = box.ROUNDED
138
-
139
175
  debug_mode = is_debug_enabled()
140
176
 
141
177
  # First line: Klaude Code version
@@ -185,6 +221,6 @@ def render_welcome(e: events.WelcomeEvent, *, box_style: Box | None = None) -> R
185
221
 
186
222
  border_style = ThemeKey.WELCOME_DEBUG_BORDER if debug_mode else ThemeKey.LINES
187
223
  return Group(
188
- Panel.fit(panel_content, border_style=border_style, box=box_style),
224
+ Panel.fit(panel_content, border_style=border_style, box=box.ROUNDED),
189
225
  "", # empty line
190
226
  )
@@ -1,6 +1,9 @@
1
1
  import json
2
+ from typing import Any, cast
2
3
 
4
+ from rich import box
3
5
  from rich.console import Group, RenderableType
6
+ from rich.json import JSON
4
7
  from rich.panel import Panel
5
8
  from rich.style import Style
6
9
  from rich.text import Text
@@ -12,37 +15,112 @@ from klaude_code.ui.rich.markdown import NoInsetMarkdown
12
15
  from klaude_code.ui.rich.theme import ThemeKey
13
16
 
14
17
 
18
+ def _compact_schema_value(value: dict[str, Any]) -> str | list[Any] | dict[str, Any]:
19
+ """Convert a JSON Schema value to compact representation."""
20
+ value_type = value.get("type", "any").lower()
21
+ desc = value.get("description", "")
22
+
23
+ if value_type == "object":
24
+ props = value.get("properties", {})
25
+ return {k: _compact_schema_value(v) for k, v in props.items()}
26
+ elif value_type == "array":
27
+ items = value.get("items", {})
28
+ # If items have no description, use the array's description
29
+ if desc and not items.get("description"):
30
+ items = {**items, "description": desc}
31
+ return [_compact_schema_value(items)]
32
+ else:
33
+ if desc:
34
+ return f"{value_type} // {desc}"
35
+ return value_type
36
+
37
+
38
+ def _compact_schema(schema: dict[str, Any]) -> dict[str, Any] | list[Any] | str:
39
+ """Convert JSON Schema to compact representation for display."""
40
+ return _compact_schema_value(schema)
41
+
42
+
15
43
  def render_sub_agent_call(e: model.SubAgentState, style: Style | None = None) -> RenderableType:
16
44
  """Render sub-agent tool call header and prompt body."""
17
45
  desc = Text(
18
46
  f" {e.sub_agent_desc} ",
19
47
  style=Style(color=style.color if style else None, bold=True, reverse=True),
20
48
  )
21
- return Group(
49
+ elements: list[RenderableType] = [
22
50
  Text.assemble((e.sub_agent_type, ThemeKey.TOOL_NAME), " ", desc),
23
51
  Text(e.sub_agent_prompt, style=style or ""),
24
- )
52
+ ]
53
+ if e.output_schema:
54
+ elements.append(Text("\nExpected Output Format JSON:", style=style or ""))
55
+ compact = _compact_schema(e.output_schema)
56
+ schema_text = json.dumps(compact, ensure_ascii=False, indent=2)
57
+ elements.append(JSON(schema_text))
58
+ return Group(*elements)
25
59
 
26
60
 
27
- def render_sub_agent_result(result: str, *, code_theme: str, style: Style | None = None) -> RenderableType:
61
+ def render_sub_agent_result(
62
+ result: str,
63
+ *,
64
+ code_theme: str,
65
+ style: Style | None = None,
66
+ has_structured_output: bool = False,
67
+ description: str | None = None,
68
+ panel_style: Style | None = None,
69
+ ) -> RenderableType:
28
70
  stripped_result = result.strip()
71
+ result_panel_style = panel_style or ThemeKey.SUB_AGENT_RESULT_PANEL
72
+
73
+ # Use rich JSON for structured output
74
+ if has_structured_output:
75
+ try:
76
+ group_elements: list[RenderableType] = [
77
+ Text(
78
+ "use /export to view full output",
79
+ style=ThemeKey.TOOL_RESULT,
80
+ ),
81
+ JSON(stripped_result),
82
+ ]
83
+ if description:
84
+ group_elements.insert(0, NoInsetMarkdown(f"# {description}", code_theme=code_theme, style=style or ""))
85
+ return Panel.fit(
86
+ Group(*group_elements),
87
+ box=box.SIMPLE,
88
+ border_style=ThemeKey.LINES,
89
+ style=result_panel_style,
90
+ )
91
+ except json.JSONDecodeError:
92
+ # Fall back to markdown if not valid JSON
93
+ pass
94
+
95
+ # Add markdown heading if description is provided for non-structured output
96
+ if description:
97
+ stripped_result = f"# {description}\n\n{stripped_result}"
98
+
29
99
  lines = stripped_result.splitlines()
30
100
  if len(lines) > const.SUB_AGENT_RESULT_MAX_LINES:
31
101
  hidden_count = len(lines) - const.SUB_AGENT_RESULT_MAX_LINES
32
- truncated_text = "\n".join(lines[-const.SUB_AGENT_RESULT_MAX_LINES :])
102
+ head_count = const.SUB_AGENT_RESULT_MAX_LINES // 2
103
+ tail_count = const.SUB_AGENT_RESULT_MAX_LINES - head_count
104
+ head_text = "\n".join(lines[:head_count])
105
+ tail_text = "\n".join(lines[-tail_count:])
33
106
  return Panel.fit(
34
107
  Group(
108
+ NoInsetMarkdown(head_text, code_theme=code_theme, style=style or ""),
35
109
  Text(
36
- f"… more {hidden_count} lines — use /export to view full output",
37
- style=ThemeKey.TOOL_RESULT,
110
+ f"\n( … more {hidden_count} lines — use /export to view full output )\n",
111
+ style=ThemeKey.TOOL_RESULT_TRUNCATED,
38
112
  ),
39
- NoInsetMarkdown(truncated_text, code_theme=code_theme, style=style or ""),
113
+ NoInsetMarkdown(tail_text, code_theme=code_theme, style=style or ""),
40
114
  ),
115
+ box=box.SIMPLE,
41
116
  border_style=ThemeKey.LINES,
117
+ style=result_panel_style,
42
118
  )
43
119
  return Panel.fit(
44
120
  NoInsetMarkdown(stripped_result, code_theme=code_theme),
121
+ box=box.SIMPLE,
45
122
  border_style=ThemeKey.LINES,
123
+ style=result_panel_style,
46
124
  )
47
125
 
48
126
 
@@ -53,6 +131,7 @@ def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAg
53
131
  return None
54
132
  description = profile.name
55
133
  prompt = ""
134
+ output_schema: dict[str, Any] | None = None
56
135
  if e.arguments:
57
136
  try:
58
137
  payload: dict[str, object] = json.loads(e.arguments)
@@ -64,8 +143,14 @@ def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAg
64
143
  prompt_value = payload.get("prompt") or payload.get("task")
65
144
  if isinstance(prompt_value, str):
66
145
  prompt = prompt_value.strip()
146
+ # Extract output_schema if profile supports it
147
+ if profile.output_schema_arg:
148
+ schema_value = payload.get(profile.output_schema_arg)
149
+ if isinstance(schema_value, dict):
150
+ output_schema = cast(dict[str, Any], schema_value)
67
151
  return model.SubAgentState(
68
152
  sub_agent_type=profile.name,
69
153
  sub_agent_desc=description,
70
154
  sub_agent_prompt=prompt,
155
+ output_schema=output_schema,
71
156
  )
@@ -1,27 +1,44 @@
1
+ import re
2
+
1
3
  from rich.console import RenderableType
2
4
  from rich.padding import Padding
3
5
  from rich.text import Text
4
6
 
5
- from klaude_code.ui.rich.markdown import NoInsetMarkdown
7
+ from klaude_code import const
8
+ from klaude_code.ui.renderers.common import create_grid
9
+ from klaude_code.ui.rich.markdown import ThinkingMarkdown
6
10
  from klaude_code.ui.rich.theme import ThemeKey
7
11
 
8
-
9
- def thinking_prefix() -> Text:
10
- return Text.from_markup("[not italic]⸫[/not italic] Thinking …", style=ThemeKey.THINKING)
12
+ # UI markers
13
+ THINKING_MESSAGE_MARK = "∴"
11
14
 
12
15
 
13
- def _normalize_thinking_content(content: str) -> str:
16
+ def normalize_thinking_content(content: str) -> str:
14
17
  """Normalize thinking content for display."""
15
- return (
16
- content.rstrip()
17
- .replace("**\n\n", "** \n")
18
- .replace("\\n\\n\n\n", "") # Weird case of Gemini 3
19
- .replace("****", "**\n\n**") # remove extra newlines after bold titles
20
- )
18
+ text = content.rstrip()
19
+
20
+ # Weird case of Gemini 3
21
+ text = text.replace("\\n\\n\n\n", "")
22
+
23
+ # Fix OpenRouter OpenAI reasoning formatting where segments like
24
+ # "text**Title**\n\n" lose the blank line between segments.
25
+ # We want: "text\n**Title**\n" so that each bold title starts on
26
+ # its own line and uses a single trailing newline.
27
+ text = re.sub(r"([^\n])(\*\*[^*]+?\*\*)\n\n", r"\1 \n\n\2 \n", text)
28
+
29
+ # Remove extra newlines between back-to-back bold titles, eg
30
+ # "**Title1****Title2**" -> "**Title1**\n\n**Title2**".
31
+ text = text.replace("****", "**\n\n**")
32
+
33
+ # Compact double-newline after bold so the body text follows
34
+ # directly after the title line, using a markdown line break.
35
+ text = text.replace("**\n\n", "** \n")
36
+
37
+ return text
21
38
 
22
39
 
23
40
  def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableType | None:
24
- """Render thinking content as indented markdown.
41
+ """Render thinking content as markdown with left mark.
25
42
 
26
43
  Returns None if content is empty.
27
44
  Note: Caller should push thinking_markdown_theme before printing.
@@ -29,11 +46,16 @@ def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableT
29
46
  if len(content.strip()) == 0:
30
47
  return None
31
48
 
32
- return Padding.indent(
33
- NoInsetMarkdown(
34
- _normalize_thinking_content(content),
35
- code_theme=code_theme,
36
- style=style,
49
+ grid = create_grid()
50
+ grid.add_row(
51
+ Text(THINKING_MESSAGE_MARK, style=ThemeKey.THINKING),
52
+ Padding(
53
+ ThinkingMarkdown(
54
+ normalize_thinking_content(content),
55
+ code_theme=code_theme,
56
+ style=style,
57
+ ),
58
+ (0, const.MARKDOWN_RIGHT_MARGIN, 0, 0),
37
59
  ),
38
- level=2,
39
60
  )
61
+ return grid