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
@@ -6,17 +6,26 @@ with dependencies injected to avoid circular imports.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import contextlib
9
10
  import re
10
11
  from collections.abc import Callable
11
12
  from typing import cast
12
13
 
14
+ from prompt_toolkit.buffer import Buffer
15
+ from prompt_toolkit.filters import Always, Filter
16
+ from prompt_toolkit.filters.app import has_completions
13
17
  from prompt_toolkit.key_binding import KeyBindings
18
+ from prompt_toolkit.key_binding.key_processor import KeyPressEvent
14
19
 
15
20
 
16
21
  def create_key_bindings(
17
22
  capture_clipboard_tag: Callable[[], str | None],
18
23
  copy_to_clipboard: Callable[[str], None],
19
24
  at_token_pattern: re.Pattern[str],
25
+ *,
26
+ input_enabled: Filter | None = None,
27
+ open_model_picker: Callable[[], None] | None = None,
28
+ open_thinking_picker: Callable[[], None] | None = None,
20
29
  ) -> KeyBindings:
21
30
  """Create REPL key bindings with injected dependencies.
22
31
 
@@ -29,20 +38,96 @@ def create_key_bindings(
29
38
  KeyBindings instance with all REPL handlers configured
30
39
  """
31
40
  kb = KeyBindings()
41
+ enabled = input_enabled if input_enabled is not None else Always()
32
42
 
33
- @kb.add("c-v")
34
- def _(event): # type: ignore
43
+ def _should_submit_instead_of_accepting_completion(buf: Buffer) -> bool:
44
+ """Return True when Enter should submit even if completions are visible.
45
+
46
+ We show completions proactively for contexts like `/`.
47
+ If the user already typed an exact candidate (e.g. `/clear`), accepting
48
+ a completion often only adds a trailing space and makes Enter require
49
+ two presses. In that case, prefer submitting.
50
+ """
51
+ state = buf.complete_state
52
+ if state is None or not state.completions:
53
+ return False
54
+
55
+ try:
56
+ doc = buf.document # type: ignore[reportUnknownMemberType]
57
+ text = cast(str, doc.text) # type: ignore[reportUnknownMemberType]
58
+ cursor_pos = cast(int, doc.cursor_position) # type: ignore[reportUnknownMemberType]
59
+ except Exception:
60
+ return False
61
+
62
+ # Only apply this heuristic when the caret is at the end of the buffer.
63
+ if cursor_pos != len(text):
64
+ return False
65
+
66
+ for completion in state.completions:
67
+ try:
68
+ start = cursor_pos + completion.start_position
69
+ if start < 0 or start > cursor_pos:
70
+ continue
71
+
72
+ replaced = text[start:cursor_pos]
73
+ inserted = completion.text
74
+
75
+ # If the user already typed an exact candidate, don't force
76
+ # accepting a completion (which often just adds a space).
77
+ if replaced == inserted or replaced == inserted.rstrip():
78
+ return True
79
+ except Exception:
80
+ continue
81
+
82
+ return False
83
+
84
+ def _select_first_completion_if_needed(buf: Buffer) -> None:
85
+ """Ensure the completion menu has an active selection.
86
+
87
+ prompt_toolkit's default behavior keeps `complete_index=None` until the
88
+ user explicitly selects an item. We want the first item to be selected
89
+ by default, without modifying the buffer text.
90
+ """
91
+ state = buf.complete_state
92
+ if state is None or not state.completions:
93
+ return
94
+ if state.complete_index is None:
95
+ state.complete_index = 0
96
+
97
+ def _cycle_completion(buf: Buffer, *, delta: int) -> None:
98
+ state = buf.complete_state
99
+ if state is None or not state.completions:
100
+ return
101
+
102
+ _select_first_completion_if_needed(buf)
103
+ idx = state.complete_index or 0
104
+ state.complete_index = (idx + delta) % len(state.completions)
105
+
106
+ def _accept_current_completion(buf: Buffer) -> bool:
107
+ """Apply the currently selected completion, if any.
108
+
109
+ Returns True when a completion was applied.
110
+ """
111
+ state = buf.complete_state
112
+ if state is None or not state.completions:
113
+ return False
114
+
115
+ _select_first_completion_if_needed(buf)
116
+ completion = state.current_completion or state.completions[0]
117
+ buf.apply_completion(completion)
118
+ return True
119
+
120
+ @kb.add("c-v", filter=enabled)
121
+ def _(event: KeyPressEvent) -> None:
35
122
  """Paste image from clipboard as [Image #N]."""
36
123
  tag = capture_clipboard_tag()
37
124
  if tag:
38
- try:
125
+ with contextlib.suppress(Exception):
39
126
  event.current_buffer.insert_text(tag) # pyright: ignore[reportUnknownMemberType]
40
- except Exception:
41
- pass
42
127
 
43
- @kb.add("enter")
44
- def _(event): # type: ignore
45
- buf = event.current_buffer # type: ignore
128
+ @kb.add("enter", filter=enabled)
129
+ def _(event: KeyPressEvent) -> None:
130
+ buf = event.current_buffer
46
131
  doc = buf.document # type: ignore
47
132
 
48
133
  # If VS Code/Windsurf/Cursor sent a "\\" sentinel before Enter (Shift+Enter mapping),
@@ -53,10 +138,16 @@ def create_key_bindings(
53
138
  buf.delete_before_cursor() # remove the sentinel backslash # type: ignore[reportUnknownMemberType]
54
139
  buf.insert_text("\n") # type: ignore[reportUnknownMemberType]
55
140
  return
56
- except Exception:
141
+ except (AttributeError, TypeError):
57
142
  # Fall through to default behavior if anything goes wrong
58
143
  pass
59
144
 
145
+ # When completions are visible, Enter accepts the current selection.
146
+ # This aligns with common TUI completion UX: navigation doesn't modify
147
+ # the buffer, and Enter/Tab inserts the selected option.
148
+ if not _should_submit_instead_of_accepting_completion(buf) and _accept_current_completion(buf):
149
+ return
150
+
60
151
  # If the entire buffer is whitespace-only, insert a newline rather than submitting.
61
152
  if len(buf.text.strip()) == 0: # type: ignore
62
153
  buf.insert_text("\n") # type: ignore
@@ -65,12 +156,30 @@ def create_key_bindings(
65
156
  # No need to persist manifest anymore - iter_inputs will handle image extraction
66
157
  buf.validate_and_handle() # type: ignore
67
158
 
68
- @kb.add("c-j")
69
- def _(event): # type: ignore
159
+ @kb.add("tab", filter=enabled & has_completions)
160
+ def _(event: KeyPressEvent) -> None:
161
+ buf = event.current_buffer
162
+ if _accept_current_completion(buf):
163
+ event.app.invalidate() # type: ignore[reportUnknownMemberType]
164
+
165
+ @kb.add("down", filter=enabled & has_completions)
166
+ def _(event: KeyPressEvent) -> None:
167
+ buf = event.current_buffer
168
+ _cycle_completion(buf, delta=1)
169
+ event.app.invalidate() # type: ignore[reportUnknownMemberType]
170
+
171
+ @kb.add("up", filter=enabled & has_completions)
172
+ def _(event: KeyPressEvent) -> None:
173
+ buf = event.current_buffer
174
+ _cycle_completion(buf, delta=-1)
175
+ event.app.invalidate() # type: ignore[reportUnknownMemberType]
176
+
177
+ @kb.add("c-j", filter=enabled)
178
+ def _(event: KeyPressEvent) -> None:
70
179
  event.current_buffer.insert_text("\n") # type: ignore
71
180
 
72
- @kb.add("c")
73
- def _(event): # type: ignore
181
+ @kb.add("c", filter=enabled)
182
+ def _(event: KeyPressEvent) -> None:
74
183
  """Copy selected text to system clipboard, or insert 'c' if no selection."""
75
184
  buf = event.current_buffer # type: ignore
76
185
  if buf.selection_state: # type: ignore[reportUnknownMemberType]
@@ -84,8 +193,8 @@ def create_key_bindings(
84
193
  else:
85
194
  buf.insert_text("c") # type: ignore[reportUnknownMemberType]
86
195
 
87
- @kb.add("backspace")
88
- def _(event): # type: ignore
196
+ @kb.add("backspace", filter=enabled)
197
+ def _(event: KeyPressEvent) -> None:
89
198
  """Ensure completions refresh on backspace when editing an @token.
90
199
 
91
200
  We delete the character before cursor (default behavior), then explicitly
@@ -106,17 +215,17 @@ def create_key_bindings(
106
215
  should_refresh = True
107
216
  elif buf.document.cursor_position_row == 0: # type: ignore[reportUnknownMemberType]
108
217
  # Check for slash command pattern without accessing protected attribute
109
- text_before_str = cast(str, text_before or "")
218
+ text_before_str = text_before or ""
110
219
  if text_before_str.strip().startswith("/") and " " not in text_before_str:
111
220
  should_refresh = True
112
221
 
113
222
  if should_refresh:
114
223
  buf.start_completion(select_first=False) # type: ignore[reportUnknownMemberType]
115
- except Exception:
224
+ except (AttributeError, TypeError):
116
225
  pass
117
226
 
118
- @kb.add("left")
119
- def _(event): # type: ignore
227
+ @kb.add("left", filter=enabled)
228
+ def _(event: KeyPressEvent) -> None:
120
229
  """Support wrapping to previous line when pressing left at column 0."""
121
230
  buf = event.current_buffer # type: ignore
122
231
  try:
@@ -137,11 +246,11 @@ def create_key_bindings(
137
246
  # Default behavior: move one character left when possible.
138
247
  if doc.cursor_position > 0: # type: ignore[reportUnknownMemberType]
139
248
  buf.cursor_left() # type: ignore[reportUnknownMemberType]
140
- except Exception:
249
+ except (AttributeError, IndexError, TypeError):
141
250
  pass
142
251
 
143
- @kb.add("right")
144
- def _(event): # type: ignore
252
+ @kb.add("right", filter=enabled)
253
+ def _(event: KeyPressEvent) -> None:
145
254
  """Support wrapping to next line when pressing right at line end."""
146
255
  buf = event.current_buffer # type: ignore
147
256
  try:
@@ -164,7 +273,21 @@ def create_key_bindings(
164
273
  # Default behavior: move one character right when possible.
165
274
  if doc.cursor_position < len(doc.text): # type: ignore[reportUnknownMemberType]
166
275
  buf.cursor_right() # type: ignore[reportUnknownMemberType]
167
- except Exception:
276
+ except (AttributeError, IndexError, TypeError):
168
277
  pass
169
278
 
279
+ @kb.add("c-l", filter=enabled, eager=True)
280
+ def _(event: KeyPressEvent) -> None:
281
+ del event
282
+ if open_model_picker is not None:
283
+ with contextlib.suppress(Exception):
284
+ open_model_picker()
285
+
286
+ @kb.add("c-t", filter=enabled, eager=True)
287
+ def _(event: KeyPressEvent) -> None:
288
+ del event
289
+ if open_thinking_picker is not None:
290
+ with contextlib.suppress(Exception):
291
+ open_thinking_picker()
292
+
170
293
  return kb
@@ -1,17 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
4
+ from collections.abc import Iterator
3
5
  from contextlib import contextmanager
4
6
  from dataclasses import dataclass
5
- from typing import Any, Iterator
7
+ from typing import Any
6
8
 
7
- from rich import box
8
- from rich.box import Box
9
- from rich.console import Console
9
+ from rich.console import Console, Group, RenderableType
10
+ from rich.padding import Padding
10
11
  from rich.spinner import Spinner
11
- from rich.status import Status
12
12
  from rich.style import Style, StyleType
13
13
  from rich.text import Text
14
14
 
15
+ from klaude_code import const
15
16
  from klaude_code.protocol import events, model
16
17
  from klaude_code.ui.renderers import assistant as r_assistant
17
18
  from klaude_code.ui.renderers import developer as r_developer
@@ -21,16 +22,18 @@ from klaude_code.ui.renderers import sub_agent as r_sub_agent
21
22
  from klaude_code.ui.renderers import thinking as r_thinking
22
23
  from klaude_code.ui.renderers import tools as r_tools
23
24
  from klaude_code.ui.renderers import user_input as r_user_input
25
+ from klaude_code.ui.renderers.common import truncate_display
24
26
  from klaude_code.ui.rich import status as r_status
27
+ from klaude_code.ui.rich.live import CropAboveLive, SingleLine
25
28
  from klaude_code.ui.rich.quote import Quote
26
- from klaude_code.ui.rich.status import ShimmerStatusText
29
+ from klaude_code.ui.rich.status import BreathingSpinner, ShimmerStatusText
27
30
  from klaude_code.ui.rich.theme import ThemeKey, get_theme
28
- from klaude_code.ui.utils.common import truncate_display
29
31
 
30
32
 
31
33
  @dataclass
32
34
  class SessionStatus:
33
35
  color: Style | None = None
36
+ color_index: int | None = None
34
37
  sub_agent_state: model.SubAgentState | None = None
35
38
 
36
39
 
@@ -41,22 +44,32 @@ class REPLRenderer:
41
44
  self.themes = get_theme(theme)
42
45
  self.console: Console = Console(theme=self.themes.app_theme)
43
46
  self.console.push_theme(self.themes.markdown_theme)
44
- self._spinner: Status = self.console.status(
45
- ShimmerStatusText("Thinking …", ThemeKey.SPINNER_STATUS_TEXT),
46
- spinner=r_status.spinner_name(),
47
- spinner_style=ThemeKey.SPINNER_STATUS,
47
+ self._bottom_live: CropAboveLive | None = None
48
+ self._stream_renderable: RenderableType | None = None
49
+ self._stream_max_height: int = 0
50
+ self._stream_last_height: int = 0
51
+ self._stream_last_width: int = 0
52
+ self._spinner_visible: bool = False
53
+
54
+ self._status_text: ShimmerStatusText = ShimmerStatusText(const.STATUS_DEFAULT_TEXT)
55
+ self._status_spinner: Spinner = BreathingSpinner(
56
+ r_status.spinner_name(),
57
+ text=SingleLine(self._status_text),
58
+ style=ThemeKey.STATUS_SPINNER,
48
59
  )
49
60
 
50
61
  self.session_map: dict[str, SessionStatus] = {}
51
62
  self.current_sub_agent_color: Style | None = None
52
- self.subagent_color_index = 0
63
+ self.sub_agent_color_index = 0
53
64
 
54
65
  def register_session(self, session_id: str, sub_agent_state: model.SubAgentState | None = None) -> None:
55
66
  session_status = SessionStatus(
56
67
  sub_agent_state=sub_agent_state,
57
68
  )
58
69
  if sub_agent_state is not None:
59
- session_status.color = self.pick_sub_agent_color()
70
+ color, color_index = self.pick_sub_agent_color()
71
+ session_status.color = color
72
+ session_status.color_index = color_index
60
73
  self.session_map[session_id] = session_status
61
74
 
62
75
  def is_sub_agent_session(self, session_id: str) -> bool:
@@ -65,16 +78,16 @@ class REPLRenderer:
65
78
  def _advance_sub_agent_color_index(self) -> None:
66
79
  palette_size = len(self.themes.sub_agent_colors)
67
80
  if palette_size == 0:
68
- self.subagent_color_index = 0
81
+ self.sub_agent_color_index = 0
69
82
  return
70
- self.subagent_color_index = (self.subagent_color_index + 1) % palette_size
83
+ self.sub_agent_color_index = (self.sub_agent_color_index + 1) % palette_size
71
84
 
72
- def pick_sub_agent_color(self) -> Style:
85
+ def pick_sub_agent_color(self) -> tuple[Style, int]:
73
86
  self._advance_sub_agent_color_index()
74
87
  palette = self.themes.sub_agent_colors
75
88
  if not palette:
76
- return Style()
77
- return palette[self.subagent_color_index]
89
+ return Style(), 0
90
+ return palette[self.sub_agent_color_index], self.sub_agent_color_index
78
91
 
79
92
  def get_session_sub_agent_color(self, session_id: str) -> Style:
80
93
  status = self.session_map.get(session_id)
@@ -82,8 +95,12 @@ class REPLRenderer:
82
95
  return status.color
83
96
  return Style()
84
97
 
85
- def box_style(self) -> Box:
86
- return box.ROUNDED
98
+ def get_session_sub_agent_background(self, session_id: str) -> Style:
99
+ status = self.session_map.get(session_id)
100
+ backgrounds = self.themes.sub_agent_backgrounds
101
+ if status and status.color_index is not None and backgrounds:
102
+ return backgrounds[status.color_index]
103
+ return Style()
87
104
 
88
105
  @contextmanager
89
106
  def session_print_context(self, session_id: str) -> Iterator[None]:
@@ -98,49 +115,22 @@ class REPLRenderer:
98
115
  def print(self, *objects: Any, style: StyleType | None = None, end: str = "\n") -> None:
99
116
  if self.current_sub_agent_color:
100
117
  if objects:
101
- self.console.print(Quote(*objects, style=self.current_sub_agent_color))
118
+ content = objects[0] if len(objects) == 1 else objects
119
+ self.console.print(Quote(content, style=self.current_sub_agent_color), overflow="ellipsis")
102
120
  return
103
- self.console.print(*objects, style=style, end=end)
121
+ self.console.print(*objects, style=style, end=end, overflow="ellipsis")
104
122
 
105
123
  def display_tool_call(self, e: events.ToolCallEvent) -> None:
106
- # Handle sub-agent tool calls in replay mode
107
124
  if r_tools.is_sub_agent_tool(e.tool_name):
108
- if e.is_replay:
109
- state = r_sub_agent.build_sub_agent_state_from_tool_call(e)
110
- if state is not None:
111
- sub_agent_default_style = (
112
- self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
113
- )
114
- self.print(
115
- Quote(
116
- r_sub_agent.render_sub_agent_call(state, sub_agent_default_style),
117
- style=sub_agent_default_style,
118
- )
119
- )
120
125
  return
121
-
122
126
  renderable = r_tools.render_tool_call(e)
123
127
  if renderable is not None:
124
128
  self.print(renderable)
125
129
 
126
130
  def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
127
- # Handle sub-agent tool results in replay mode
128
131
  if r_tools.is_sub_agent_tool(e.tool_name):
129
- if e.is_replay:
130
- sub_agent_default_style = self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
131
- self.print(
132
- Quote(
133
- r_sub_agent.render_sub_agent_result(
134
- e.result,
135
- code_theme=self.themes.code_theme,
136
- style=sub_agent_default_style,
137
- ),
138
- style=sub_agent_default_style,
139
- )
140
- )
141
132
  return
142
-
143
- renderable = r_tools.render_tool_result(e)
133
+ renderable = r_tools.render_tool_result(e, code_theme=self.themes.code_theme)
144
134
  if renderable is not None:
145
135
  self.print(renderable)
146
136
 
@@ -159,37 +149,53 @@ class REPLRenderer:
159
149
  async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
160
150
  tool_call_dict: dict[str, events.ToolCallEvent] = {}
161
151
  for event in history_events.events:
162
- match event:
163
- case events.TurnStartEvent():
164
- self.print()
165
- case events.AssistantMessageEvent() as assistant_event:
166
- renderable = r_assistant.render_assistant_message(
167
- assistant_event.content, code_theme=self.themes.code_theme
168
- )
169
- if renderable is not None:
170
- self.print(renderable)
152
+ event_session_id = getattr(event, "session_id", history_events.session_id)
153
+ is_sub_agent = self.is_sub_agent_session(event_session_id)
154
+
155
+ with self.session_print_context(event_session_id):
156
+ match event:
157
+ case events.TaskStartEvent() as e:
158
+ self.display_task_start(e)
159
+ case events.TurnStartEvent():
160
+ self.print()
161
+ case events.AssistantMessageEvent() as e:
162
+ if is_sub_agent:
163
+ continue
164
+ renderable = r_assistant.render_assistant_message(e.content, code_theme=self.themes.code_theme)
165
+ if renderable is not None:
166
+ self.print(renderable)
167
+ self.print()
168
+ case events.ThinkingEvent() as e:
169
+ if is_sub_agent:
170
+ continue
171
+ self.display_thinking(e.content)
172
+ case events.DeveloperMessageEvent() as e:
173
+ self.display_developer_message(e)
174
+ self.display_command_output(e)
175
+ case events.UserMessageEvent() as e:
176
+ if is_sub_agent:
177
+ continue
178
+ self.print(r_user_input.render_user_input(e.content))
179
+ case events.ToolCallEvent() as e:
180
+ tool_call_dict[e.tool_call_id] = e
181
+ case events.ToolResultEvent() as e:
182
+ tool_call_event = tool_call_dict.get(e.tool_call_id)
183
+ if tool_call_event is not None:
184
+ self.display_tool_call(tool_call_event)
185
+ tool_call_dict.pop(e.tool_call_id, None)
186
+ if is_sub_agent:
187
+ continue
188
+ self.display_tool_call_result(e)
189
+ case events.TaskMetadataEvent() as e:
190
+ self.print(r_metadata.render_task_metadata(e))
171
191
  self.print()
172
- case events.ThinkingEvent() as thinking_event:
173
- self.display_thinking(thinking_event.content)
174
- case events.DeveloperMessageEvent() as developer_event:
175
- self.display_developer_message(developer_event)
176
- self.display_command_output(developer_event)
177
- case events.UserMessageEvent() as user_event:
178
- self.print(r_user_input.render_user_input(user_event.content))
179
- case events.ToolCallEvent() as tool_call_event:
180
- tool_call_dict[tool_call_event.tool_call_id] = tool_call_event
181
- case events.ToolResultEvent() as tool_result_event:
182
- tool_call_event = tool_call_dict.get(tool_result_event.tool_call_id)
183
- if tool_call_event is not None:
184
- self.display_tool_call(tool_call_event)
185
- tool_call_dict.pop(tool_result_event.tool_call_id, None)
186
- self.display_tool_call_result(tool_result_event)
187
- case events.ResponseMetadataEvent() as metadata_event:
188
- self.print(r_metadata.render_response_metadata(metadata_event))
189
- self.print()
190
- case events.InterruptEvent():
191
- self.print()
192
- self.print(r_user_input.render_interrupt())
192
+ case events.InterruptEvent():
193
+ self.print()
194
+ self.print(r_user_input.render_interrupt())
195
+ case events.ErrorEvent() as e:
196
+ self.display_error(e)
197
+ case events.TaskFinishEvent() as e:
198
+ self.display_task_finish(e)
193
199
 
194
200
  def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
195
201
  if not r_developer.need_render_developer_message(e):
@@ -205,7 +211,7 @@ class REPLRenderer:
205
211
  self.print()
206
212
 
207
213
  def display_welcome(self, event: events.WelcomeEvent) -> None:
208
- self.print(r_metadata.render_welcome(event, box_style=self.box_style()))
214
+ self.print(r_metadata.render_welcome(event))
209
215
 
210
216
  def display_user_message(self, event: events.UserMessageEvent) -> None:
211
217
  self.print(r_user_input.render_user_input(event.content))
@@ -231,18 +237,28 @@ class REPLRenderer:
231
237
  self.print(renderable)
232
238
  self.print()
233
239
 
234
- def display_response_metadata(self, event: events.ResponseMetadataEvent) -> None:
240
+ def display_task_metadata(self, event: events.TaskMetadataEvent) -> None:
235
241
  with self.session_print_context(event.session_id):
236
- self.print(r_metadata.render_response_metadata(event))
242
+ self.print(r_metadata.render_task_metadata(event))
237
243
  self.print()
238
244
 
239
245
  def display_task_finish(self, event: events.TaskFinishEvent) -> None:
240
246
  if self.is_sub_agent_session(event.session_id):
247
+ session_status = self.session_map.get(event.session_id)
248
+ description = (
249
+ session_status.sub_agent_state.sub_agent_desc
250
+ if session_status and session_status.sub_agent_state
251
+ else None
252
+ )
253
+ panel_style = self.get_session_sub_agent_background(event.session_id)
241
254
  with self.session_print_context(event.session_id):
242
255
  self.print(
243
256
  r_sub_agent.render_sub_agent_result(
244
257
  event.task_result,
245
258
  code_theme=self.themes.code_theme,
259
+ has_structured_output=event.has_structured_output,
260
+ description=description,
261
+ panel_style=panel_style,
246
262
  )
247
263
  )
248
264
 
@@ -250,15 +266,11 @@ class REPLRenderer:
250
266
  self.print(r_user_input.render_interrupt())
251
267
 
252
268
  def display_error(self, event: events.ErrorEvent) -> None:
253
- self.print(
254
- r_errors.render_error(
255
- self.console.render_str(truncate_display(event.error_message)),
256
- indent=0,
257
- )
258
- )
259
-
260
- def display_thinking_prefix(self) -> None:
261
- self.print(r_thinking.thinking_prefix())
269
+ if event.session_id:
270
+ with self.session_print_context(event.session_id):
271
+ self.print(r_errors.render_error(truncate_display(event.error_message)))
272
+ else:
273
+ self.print(r_errors.render_error(truncate_display(event.error_message)))
262
274
 
263
275
  # -------------------------------------------------------------------------
264
276
  # Spinner control methods
@@ -266,16 +278,94 @@ class REPLRenderer:
266
278
 
267
279
  def spinner_start(self) -> None:
268
280
  """Start the spinner animation."""
269
- self._spinner.start()
281
+ self._spinner_visible = True
282
+ self._ensure_bottom_live_started()
283
+ self._refresh_bottom_live()
270
284
 
271
285
  def spinner_stop(self) -> None:
272
286
  """Stop the spinner animation."""
273
- self._spinner.stop()
287
+ self._spinner_visible = False
288
+ self._refresh_bottom_live()
274
289
 
275
- def spinner_update(self, status_text: str | Text) -> None:
276
- """Update the spinner status text."""
277
- self._spinner.update(ShimmerStatusText(status_text, ThemeKey.SPINNER_STATUS_TEXT))
290
+ def spinner_update(self, status_text: str | Text, right_text: RenderableType | None = None) -> None:
291
+ """Update the spinner status text with optional right-aligned text."""
292
+ self._status_text = ShimmerStatusText(status_text, right_text)
293
+ self._status_spinner.update(text=SingleLine(self._status_text), style=ThemeKey.STATUS_SPINNER)
294
+ self._refresh_bottom_live()
278
295
 
279
296
  def spinner_renderable(self) -> Spinner:
280
297
  """Return the spinner's renderable for embedding in other components."""
281
- return self._spinner.renderable
298
+ return self._status_spinner
299
+
300
+ def set_stream_renderable(self, renderable: RenderableType | None) -> None:
301
+ """Set the current streaming renderable displayed above the status line."""
302
+
303
+ if renderable is None:
304
+ self._stream_renderable = None
305
+ self._stream_max_height = 0
306
+ self._stream_last_height = 0
307
+ self._stream_last_width = 0
308
+ self._refresh_bottom_live()
309
+ return
310
+
311
+ self._ensure_bottom_live_started()
312
+ self._stream_renderable = renderable
313
+
314
+ height = len(self.console.render_lines(renderable, self.console.options, pad=False))
315
+ self._stream_last_height = height
316
+ self._stream_last_width = self.console.size.width
317
+ self._stream_max_height = max(self._stream_max_height, height)
318
+ self._refresh_bottom_live()
319
+
320
+ def _ensure_bottom_live_started(self) -> None:
321
+ if self._bottom_live is not None:
322
+ return
323
+ self._bottom_live = CropAboveLive(
324
+ Text(""),
325
+ console=self.console,
326
+ refresh_per_second=30,
327
+ transient=True,
328
+ redirect_stdout=False,
329
+ redirect_stderr=False,
330
+ )
331
+ self._bottom_live.start()
332
+
333
+ def _bottom_renderable(self) -> RenderableType:
334
+ stream_part: RenderableType = Group()
335
+ gap_part: RenderableType = Group()
336
+
337
+ if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED:
338
+ stream = self._stream_renderable
339
+ if stream is not None:
340
+ current_width = self.console.size.width
341
+ if self._stream_last_width != current_width:
342
+ height = len(self.console.render_lines(stream, self.console.options, pad=False))
343
+ self._stream_last_height = height
344
+ self._stream_last_width = current_width
345
+ self._stream_max_height = max(self._stream_max_height, height)
346
+ else:
347
+ height = self._stream_last_height
348
+
349
+ pad_lines = max(self._stream_max_height - height, 0)
350
+ if pad_lines:
351
+ stream = Padding(stream, (0, 0, pad_lines, 0))
352
+ stream_part = stream
353
+
354
+ gap_part = Text("") if self._spinner_visible else Group()
355
+
356
+ status_part: RenderableType = SingleLine(self._status_spinner) if self._spinner_visible else Group()
357
+ return Group(stream_part, gap_part, status_part)
358
+
359
+ def _refresh_bottom_live(self) -> None:
360
+ if self._bottom_live is None:
361
+ return
362
+ self._bottom_live.update(self._bottom_renderable(), refresh=True)
363
+
364
+ def stop_bottom_live(self) -> None:
365
+ if self._bottom_live is None:
366
+ return
367
+ with contextlib.suppress(Exception):
368
+ # Avoid cursor restore when stopping right before prompt_toolkit.
369
+ self._bottom_live.transient = False
370
+ self._bottom_live.stop()
371
+ self._bottom_live = None