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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. klaude_code/auth/base.py +97 -0
  2. klaude_code/auth/claude/__init__.py +6 -0
  3. klaude_code/auth/claude/exceptions.py +9 -0
  4. klaude_code/auth/claude/oauth.py +172 -0
  5. klaude_code/auth/claude/token_manager.py +26 -0
  6. klaude_code/auth/codex/token_manager.py +10 -50
  7. klaude_code/cli/auth_cmd.py +127 -46
  8. klaude_code/cli/config_cmd.py +4 -2
  9. klaude_code/cli/cost_cmd.py +14 -9
  10. klaude_code/cli/list_model.py +248 -200
  11. klaude_code/cli/main.py +1 -1
  12. klaude_code/cli/runtime.py +7 -5
  13. klaude_code/cli/self_update.py +1 -1
  14. klaude_code/cli/session_cmd.py +1 -1
  15. klaude_code/command/clear_cmd.py +6 -2
  16. klaude_code/command/command_abc.py +2 -2
  17. klaude_code/command/debug_cmd.py +4 -4
  18. klaude_code/command/export_cmd.py +2 -2
  19. klaude_code/command/export_online_cmd.py +12 -12
  20. klaude_code/command/fork_session_cmd.py +29 -23
  21. klaude_code/command/help_cmd.py +4 -4
  22. klaude_code/command/model_cmd.py +4 -4
  23. klaude_code/command/model_select.py +1 -1
  24. klaude_code/command/prompt-commit.md +82 -0
  25. klaude_code/command/prompt_command.py +3 -3
  26. klaude_code/command/refresh_cmd.py +2 -2
  27. klaude_code/command/registry.py +7 -5
  28. klaude_code/command/release_notes_cmd.py +4 -4
  29. klaude_code/command/resume_cmd.py +15 -11
  30. klaude_code/command/status_cmd.py +4 -4
  31. klaude_code/command/terminal_setup_cmd.py +8 -8
  32. klaude_code/command/thinking_cmd.py +4 -4
  33. klaude_code/config/assets/builtin_config.yaml +52 -3
  34. klaude_code/config/builtin_config.py +16 -5
  35. klaude_code/config/config.py +31 -7
  36. klaude_code/config/thinking.py +4 -4
  37. klaude_code/const.py +146 -91
  38. klaude_code/core/agent.py +3 -12
  39. klaude_code/core/executor.py +21 -13
  40. klaude_code/core/manager/sub_agent_manager.py +71 -7
  41. klaude_code/core/prompt.py +1 -1
  42. klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
  43. klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
  44. klaude_code/core/reminders.py +88 -69
  45. klaude_code/core/task.py +44 -45
  46. klaude_code/core/tool/file/apply_patch_tool.py +9 -9
  47. klaude_code/core/tool/file/diff_builder.py +3 -5
  48. klaude_code/core/tool/file/edit_tool.py +23 -23
  49. klaude_code/core/tool/file/move_tool.py +43 -43
  50. klaude_code/core/tool/file/read_tool.py +44 -39
  51. klaude_code/core/tool/file/write_tool.py +14 -14
  52. klaude_code/core/tool/report_back_tool.py +4 -4
  53. klaude_code/core/tool/shell/bash_tool.py +23 -23
  54. klaude_code/core/tool/skill/skill_tool.py +7 -7
  55. klaude_code/core/tool/sub_agent_tool.py +38 -9
  56. klaude_code/core/tool/todo/todo_write_tool.py +8 -8
  57. klaude_code/core/tool/todo/update_plan_tool.py +6 -6
  58. klaude_code/core/tool/tool_abc.py +2 -2
  59. klaude_code/core/tool/tool_context.py +27 -0
  60. klaude_code/core/tool/tool_runner.py +88 -42
  61. klaude_code/core/tool/truncation.py +38 -20
  62. klaude_code/core/tool/web/mermaid_tool.py +6 -7
  63. klaude_code/core/tool/web/web_fetch_tool.py +68 -30
  64. klaude_code/core/tool/web/web_search_tool.py +15 -17
  65. klaude_code/core/turn.py +120 -73
  66. klaude_code/llm/anthropic/client.py +104 -44
  67. klaude_code/llm/anthropic/input.py +116 -108
  68. klaude_code/llm/bedrock/client.py +8 -5
  69. klaude_code/llm/claude/__init__.py +3 -0
  70. klaude_code/llm/claude/client.py +105 -0
  71. klaude_code/llm/client.py +4 -3
  72. klaude_code/llm/codex/client.py +16 -10
  73. klaude_code/llm/google/client.py +122 -60
  74. klaude_code/llm/google/input.py +94 -108
  75. klaude_code/llm/image.py +123 -0
  76. klaude_code/llm/input_common.py +136 -189
  77. klaude_code/llm/openai_compatible/client.py +17 -7
  78. klaude_code/llm/openai_compatible/input.py +36 -66
  79. klaude_code/llm/openai_compatible/stream.py +119 -67
  80. klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
  81. klaude_code/llm/openrouter/client.py +34 -9
  82. klaude_code/llm/openrouter/input.py +63 -64
  83. klaude_code/llm/openrouter/reasoning.py +22 -24
  84. klaude_code/llm/registry.py +20 -15
  85. klaude_code/llm/responses/client.py +107 -45
  86. klaude_code/llm/responses/input.py +115 -98
  87. klaude_code/llm/usage.py +52 -25
  88. klaude_code/protocol/__init__.py +1 -0
  89. klaude_code/protocol/events.py +16 -12
  90. klaude_code/protocol/llm_param.py +22 -3
  91. klaude_code/protocol/message.py +250 -0
  92. klaude_code/protocol/model.py +94 -281
  93. klaude_code/protocol/op.py +2 -2
  94. klaude_code/protocol/sub_agent/__init__.py +2 -2
  95. klaude_code/protocol/sub_agent/explore.py +10 -0
  96. klaude_code/protocol/sub_agent/image_gen.py +119 -0
  97. klaude_code/protocol/sub_agent/task.py +10 -0
  98. klaude_code/protocol/sub_agent/web.py +10 -0
  99. klaude_code/session/codec.py +6 -6
  100. klaude_code/session/export.py +261 -62
  101. klaude_code/session/selector.py +7 -24
  102. klaude_code/session/session.py +125 -53
  103. klaude_code/session/store.py +5 -32
  104. klaude_code/session/templates/export_session.html +1 -1
  105. klaude_code/session/templates/mermaid_viewer.html +1 -1
  106. klaude_code/trace/log.py +11 -6
  107. klaude_code/ui/core/input.py +1 -1
  108. klaude_code/ui/core/stage_manager.py +1 -8
  109. klaude_code/ui/modes/debug/display.py +2 -2
  110. klaude_code/ui/modes/repl/clipboard.py +2 -2
  111. klaude_code/ui/modes/repl/completers.py +18 -10
  112. klaude_code/ui/modes/repl/event_handler.py +136 -127
  113. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  114. klaude_code/ui/modes/repl/key_bindings.py +1 -1
  115. klaude_code/ui/modes/repl/renderer.py +107 -15
  116. klaude_code/ui/renderers/assistant.py +2 -2
  117. klaude_code/ui/renderers/common.py +65 -7
  118. klaude_code/ui/renderers/developer.py +7 -6
  119. klaude_code/ui/renderers/diffs.py +11 -11
  120. klaude_code/ui/renderers/mermaid_viewer.py +49 -2
  121. klaude_code/ui/renderers/metadata.py +39 -31
  122. klaude_code/ui/renderers/sub_agent.py +57 -16
  123. klaude_code/ui/renderers/thinking.py +37 -2
  124. klaude_code/ui/renderers/tools.py +180 -165
  125. klaude_code/ui/rich/live.py +3 -1
  126. klaude_code/ui/rich/markdown.py +39 -7
  127. klaude_code/ui/rich/quote.py +76 -1
  128. klaude_code/ui/rich/status.py +14 -8
  129. klaude_code/ui/rich/theme.py +13 -6
  130. klaude_code/ui/terminal/image.py +34 -0
  131. klaude_code/ui/terminal/notifier.py +2 -1
  132. klaude_code/ui/terminal/progress_bar.py +4 -4
  133. klaude_code/ui/terminal/selector.py +22 -4
  134. klaude_code/ui/utils/common.py +55 -0
  135. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +28 -6
  136. klaude_code-2.0.0.dist-info/RECORD +229 -0
  137. klaude_code/command/prompt-jj-describe.md +0 -32
  138. klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -22
  139. klaude_code/protocol/sub_agent/oracle.py +0 -91
  140. klaude_code-1.8.0.dist-info/RECORD +0 -219
  141. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
  142. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,16 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Callable
3
4
  from dataclasses import dataclass
4
5
 
5
6
  from rich.rule import Rule
6
7
  from rich.text import Text
7
8
 
8
- from klaude_code import const
9
+ from klaude_code.const import MARKDOWN_LEFT_MARGIN, MARKDOWN_STREAM_LIVE_REPAINT_ENABLED, STATUS_DEFAULT_TEXT
9
10
  from klaude_code.protocol import events
10
11
  from klaude_code.ui.core.stage_manager import Stage, StageManager
11
12
  from klaude_code.ui.modes.repl.renderer import REPLRenderer
12
13
  from klaude_code.ui.renderers.assistant import ASSISTANT_MESSAGE_MARK
13
- from klaude_code.ui.renderers.thinking import THINKING_MESSAGE_MARK, normalize_thinking_content
14
+ from klaude_code.ui.renderers.thinking import (
15
+ THINKING_MESSAGE_MARK,
16
+ extract_last_bold_header,
17
+ normalize_thinking_content,
18
+ )
19
+ from klaude_code.ui.renderers.tools import get_tool_active_form
14
20
  from klaude_code.ui.rich import status as r_status
15
21
  from klaude_code.ui.rich.markdown import MarkdownStream, ThinkingMarkdown
16
22
  from klaude_code.ui.rich.theme import ThemeKey
@@ -18,39 +24,24 @@ from klaude_code.ui.terminal.notifier import Notification, NotificationType, Ter
18
24
  from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
19
25
 
20
26
 
21
- def extract_last_bold_header(text: str) -> str | None:
22
- """Extract the latest complete bold header ("**...**") from text.
23
-
24
- We treat a bold segment as a "header" only if it appears at the beginning
25
- of a line (ignoring leading whitespace). This avoids picking up incidental
26
- emphasis inside paragraphs.
27
-
28
- Returns None if no complete bold segment is available yet.
29
- """
30
-
31
- last: str | None = None
32
- i = 0
33
- while True:
34
- start = text.find("**", i)
35
- if start < 0:
36
- break
37
-
38
- line_start = text.rfind("\n", 0, start) + 1
39
- if text[line_start:start].strip():
40
- i = start + 2
41
- continue
42
-
43
- end = text.find("**", start + 2)
44
- if end < 0:
45
- break
27
+ @dataclass
28
+ class SubAgentThinkingHeaderState:
29
+ buffer: str = ""
30
+ last_header: str | None = None
46
31
 
47
- inner = " ".join(text[start + 2 : end].split())
48
- if inner and "\n" not in inner:
49
- last = inner
32
+ def append_and_extract_new_header(self, content: str) -> str | None:
33
+ self.buffer += content
50
34
 
51
- i = end + 2
35
+ # Sub-agent thinking does not need full streaming; keep a bounded tail.
36
+ max_chars = 8192
37
+ if len(self.buffer) > max_chars:
38
+ self.buffer = self.buffer[-max_chars:]
52
39
 
53
- return last
40
+ header = extract_last_bold_header(normalize_thinking_content(self.buffer))
41
+ if header and header != self.last_header:
42
+ self.last_header = header
43
+ return header
44
+ return None
54
45
 
55
46
 
56
47
  @dataclass
@@ -107,6 +98,31 @@ class StreamState:
107
98
  """End the current streaming session."""
108
99
  self._active = None
109
100
 
101
+ def render(self, *, transform: Callable[[str], str] | None = None, final: bool = False) -> bool:
102
+ """Render the current buffer to the markdown stream.
103
+
104
+ Returns:
105
+ bool: True if an active stream was rendered.
106
+ """
107
+
108
+ if self._active is None:
109
+ return False
110
+
111
+ text = self._active.buffer
112
+ if transform is not None:
113
+ text = transform(text)
114
+ self._active.mdstream.update(text, final=final)
115
+
116
+ if final:
117
+ self.finish()
118
+
119
+ return True
120
+
121
+ def finalize(self, *, transform: Callable[[str], str] | None = None) -> bool:
122
+ """Finalize rendering and end the current streaming session."""
123
+
124
+ return self.render(transform=transform, final=True)
125
+
110
126
 
111
127
  class ActivityState:
112
128
  """Represents the current activity state for spinner display.
@@ -185,7 +201,7 @@ class SpinnerStatusState:
185
201
  - context_percent: Context usage percentage, updated during task execution
186
202
 
187
203
  Display logic:
188
- - If activity: show base + activity (if base exists) or activity + "..."
204
+ - If activity: show base + activity (if base exists) or activity + ""
189
205
  - Elif base_status: show base_status
190
206
  - Else: show "Thinking …"
191
207
  - Context percent is appended at the end if available
@@ -260,7 +276,7 @@ class SpinnerStatusState:
260
276
  activity_text.append(" …")
261
277
  result = activity_text
262
278
  else:
263
- result = Text(const.STATUS_DEFAULT_TEXT, style=ThemeKey.STATUS_TEXT)
279
+ result = Text(STATUS_DEFAULT_TEXT, style=ThemeKey.STATUS_TEXT)
264
280
 
265
281
  return result
266
282
 
@@ -295,6 +311,7 @@ class DisplayEventHandler:
295
311
  self.notifier = notifier
296
312
  self.assistant_stream = StreamState()
297
313
  self.thinking_stream = StreamState()
314
+ self._sub_agent_thinking_headers: dict[str, SubAgentThinkingHeaderState] = {}
298
315
  self.spinner_status = SpinnerStatusState()
299
316
 
300
317
  self.stage_manager = StageManager(
@@ -302,6 +319,31 @@ class DisplayEventHandler:
302
319
  finish_thinking=self._finish_thinking_stream,
303
320
  )
304
321
 
322
+ def _new_thinking_mdstream(self) -> MarkdownStream:
323
+ return MarkdownStream(
324
+ mdargs={
325
+ "code_theme": self.renderer.themes.code_theme,
326
+ "style": ThemeKey.THINKING,
327
+ },
328
+ theme=self.renderer.themes.thinking_markdown_theme,
329
+ console=self.renderer.console,
330
+ live_sink=self.renderer.set_stream_renderable if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
331
+ mark=THINKING_MESSAGE_MARK,
332
+ mark_style=ThemeKey.THINKING,
333
+ left_margin=MARKDOWN_LEFT_MARGIN,
334
+ markdown_class=ThinkingMarkdown,
335
+ )
336
+
337
+ def _new_assistant_mdstream(self) -> MarkdownStream:
338
+ return MarkdownStream(
339
+ mdargs={"code_theme": self.renderer.themes.code_theme},
340
+ theme=self.renderer.themes.markdown_theme,
341
+ console=self.renderer.console,
342
+ live_sink=self.renderer.set_stream_renderable if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
343
+ mark=ASSISTANT_MESSAGE_MARK,
344
+ left_margin=MARKDOWN_LEFT_MARGIN,
345
+ )
346
+
305
347
  async def consume_event(self, event: events.Event) -> None:
306
348
  match event:
307
349
  case events.ReplayHistoryEvent() as e:
@@ -316,16 +358,16 @@ class DisplayEventHandler:
316
358
  self._on_developer_message(e)
317
359
  case events.TurnStartEvent() as e:
318
360
  self._on_turn_start(e)
319
- case events.ThinkingEvent() as e:
320
- await self._on_thinking(e)
321
361
  case events.ThinkingDeltaEvent() as e:
322
362
  await self._on_thinking_delta(e)
323
- case events.AssistantMessageDeltaEvent() as e:
363
+ case events.AssistantTextDeltaEvent() as e:
324
364
  await self._on_assistant_delta(e)
365
+ case events.AssistantImageDeltaEvent() as e:
366
+ await self._on_assistant_image_delta(e)
325
367
  case events.AssistantMessageEvent() as e:
326
368
  await self._on_assistant_message(e)
327
369
  case events.TurnToolCallStartEvent() as e:
328
- self._on_tool_call_start(e)
370
+ await self._on_tool_call_start(e)
329
371
  case events.ToolCallEvent() as e:
330
372
  await self._on_tool_call(e)
331
373
  case events.ToolResultEvent() as e:
@@ -350,8 +392,8 @@ class DisplayEventHandler:
350
392
  await self._on_end(e)
351
393
 
352
394
  async def stop(self) -> None:
353
- await self._flush_assistant_buffer(self.assistant_stream)
354
- await self._flush_thinking_buffer(self.thinking_stream)
395
+ self._flush_assistant_buffer()
396
+ self._flush_thinking_buffer()
355
397
 
356
398
  # ─────────────────────────────────────────────────────────────────────────────
357
399
  # Private event handlers
@@ -370,6 +412,8 @@ class DisplayEventHandler:
370
412
  def _on_task_start(self, event: events.TaskStartEvent) -> None:
371
413
  if event.sub_agent_state is None:
372
414
  r_status.set_task_start()
415
+ else:
416
+ self._sub_agent_thinking_headers[event.session_id] = SubAgentThinkingHeaderState()
373
417
  self.renderer.spinner_start()
374
418
  self.renderer.display_task_start(event)
375
419
  emit_osc94(OSC94States.INDETERMINATE)
@@ -385,41 +429,20 @@ class DisplayEventHandler:
385
429
  self.spinner_status.set_reasoning_status(None)
386
430
  self._update_spinner()
387
431
 
388
- async def _on_thinking(self, event: events.ThinkingEvent) -> None:
389
- if self.renderer.is_sub_agent_session(event.session_id):
390
- return
391
- # If streaming was active, finalize it
392
- if self.thinking_stream.is_active:
393
- await self._finish_thinking_stream()
394
- else:
395
- # Non-streaming path (history replay or models without delta support)
396
- reasoning_status = extract_last_bold_header(normalize_thinking_content(event.content))
397
- if reasoning_status:
398
- self.spinner_status.set_reasoning_status(reasoning_status)
399
- self._update_spinner()
400
- await self.stage_manager.enter_thinking_stage()
401
- self.renderer.display_thinking(event.content)
402
-
403
432
  async def _on_thinking_delta(self, event: events.ThinkingDeltaEvent) -> None:
404
433
  if self.renderer.is_sub_agent_session(event.session_id):
434
+ if not self.renderer.should_display_sub_agent_thinking_header(event.session_id):
435
+ return
436
+ state = self._sub_agent_thinking_headers.setdefault(event.session_id, SubAgentThinkingHeaderState())
437
+ header = state.append_and_extract_new_header(event.content)
438
+ if header:
439
+ with self.renderer.session_print_context(event.session_id):
440
+ self.renderer.display_thinking_header(header)
405
441
  return
406
442
 
407
443
  first_delta = not self.thinking_stream.is_active
408
444
  if first_delta:
409
- mdstream = MarkdownStream(
410
- mdargs={
411
- "code_theme": self.renderer.themes.code_theme,
412
- "style": ThemeKey.THINKING,
413
- },
414
- theme=self.renderer.themes.thinking_markdown_theme,
415
- console=self.renderer.console,
416
- live_sink=self.renderer.set_stream_renderable if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
417
- mark=THINKING_MESSAGE_MARK,
418
- mark_style=ThemeKey.THINKING,
419
- left_margin=const.MARKDOWN_LEFT_MARGIN,
420
- markdown_class=ThinkingMarkdown,
421
- )
422
- self.thinking_stream.start(mdstream)
445
+ self.thinking_stream.start(self._new_thinking_mdstream())
423
446
 
424
447
  self.thinking_stream.append(event.content)
425
448
 
@@ -428,62 +451,52 @@ class DisplayEventHandler:
428
451
  self.spinner_status.set_reasoning_status(reasoning_status)
429
452
  self._update_spinner()
430
453
 
431
- if first_delta and self.thinking_stream.mdstream is not None:
432
- self.thinking_stream.mdstream.update(normalize_thinking_content(self.thinking_stream.buffer))
454
+ if first_delta:
455
+ self.thinking_stream.render(transform=normalize_thinking_content)
433
456
 
434
457
  await self.stage_manager.enter_thinking_stage()
435
- await self._flush_thinking_buffer(self.thinking_stream)
458
+ self._flush_thinking_buffer()
436
459
 
437
- async def _on_assistant_delta(self, event: events.AssistantMessageDeltaEvent) -> None:
460
+ async def _on_assistant_delta(self, event: events.AssistantTextDeltaEvent) -> None:
438
461
  if self.renderer.is_sub_agent_session(event.session_id):
439
462
  self.spinner_status.set_composing(True)
440
463
  self._update_spinner()
441
464
  return
465
+
442
466
  if len(event.content.strip()) == 0 and self.stage_manager.current_stage != Stage.ASSISTANT:
467
+ await self.stage_manager.transition_to(Stage.WAITING)
443
468
  return
469
+
470
+ await self.stage_manager.transition_to(Stage.ASSISTANT)
444
471
  first_delta = not self.assistant_stream.is_active
445
472
  if first_delta:
446
473
  self.spinner_status.set_composing(True)
447
474
  self.spinner_status.clear_tool_calls()
448
475
  self._update_spinner()
449
- mdstream = MarkdownStream(
450
- mdargs={"code_theme": self.renderer.themes.code_theme},
451
- theme=self.renderer.themes.markdown_theme,
452
- console=self.renderer.console,
453
- live_sink=self.renderer.set_stream_renderable if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
454
- mark=ASSISTANT_MESSAGE_MARK,
455
- left_margin=const.MARKDOWN_LEFT_MARGIN,
456
- )
457
- self.assistant_stream.start(mdstream)
476
+ self.assistant_stream.start(self._new_assistant_mdstream())
458
477
  self.assistant_stream.append(event.content)
459
478
  self.spinner_status.set_buffer_length(len(self.assistant_stream.buffer))
460
479
  if not first_delta:
461
480
  self._update_spinner()
462
- if first_delta and self.assistant_stream.mdstream is not None:
463
- self.assistant_stream.mdstream.update(self.assistant_stream.buffer)
464
- await self.stage_manager.transition_to(Stage.ASSISTANT)
465
- await self._flush_assistant_buffer(self.assistant_stream)
481
+ if first_delta:
482
+ self.assistant_stream.render()
483
+ self._flush_assistant_buffer()
466
484
 
467
485
  async def _on_assistant_message(self, event: events.AssistantMessageEvent) -> None:
468
486
  if self.renderer.is_sub_agent_session(event.session_id):
469
487
  return
470
- await self.stage_manager.transition_to(Stage.ASSISTANT)
471
- if self.assistant_stream.is_active:
472
- mdstream = self.assistant_stream.mdstream
473
- assert mdstream is not None
474
- mdstream.update(event.content.strip(), final=True)
475
- else:
476
- self.renderer.display_assistant_message(event.content)
477
- self.assistant_stream.finish()
488
+
489
+ await self.stage_manager.transition_to(Stage.WAITING)
478
490
  self.spinner_status.set_composing(False)
479
491
  self._update_spinner()
480
- await self.stage_manager.transition_to(Stage.WAITING)
481
- self.renderer.print()
482
492
  self.renderer.spinner_start()
483
493
 
484
- def _on_tool_call_start(self, event: events.TurnToolCallStartEvent) -> None:
485
- from klaude_code.ui.renderers.tools import get_tool_active_form
494
+ async def _on_assistant_image_delta(self, event: events.AssistantImageDeltaEvent) -> None:
495
+ await self.stage_manager.transition_to(Stage.ASSISTANT)
496
+ self.renderer.display_image(event.file_path)
486
497
 
498
+ async def _on_tool_call_start(self, event: events.TurnToolCallStartEvent) -> None:
499
+ self._flush_assistant_buffer()
487
500
  self.spinner_status.set_composing(False)
488
501
  self.spinner_status.add_tool_call(get_tool_active_form(event.tool_name))
489
502
  self._update_spinner()
@@ -494,18 +507,18 @@ class DisplayEventHandler:
494
507
  self.renderer.display_tool_call(event)
495
508
 
496
509
  async def _on_tool_result(self, event: events.ToolResultEvent) -> None:
497
- if self.renderer.is_sub_agent_session(event.session_id) and event.status == "success":
510
+ is_sub_agent = self.renderer.is_sub_agent_session(event.session_id)
511
+ if is_sub_agent and event.status == "success":
498
512
  return
499
513
  await self.stage_manager.transition_to(Stage.TOOL_RESULT)
500
514
  with self.renderer.session_print_context(event.session_id):
501
- self.renderer.display_tool_call_result(event)
515
+ self.renderer.display_tool_call_result(event, is_sub_agent=is_sub_agent)
502
516
 
503
517
  def _on_task_metadata(self, event: events.TaskMetadataEvent) -> None:
504
518
  self.renderer.display_task_metadata(event)
505
519
 
506
520
  def _on_todo_change(self, event: events.TodoChangeEvent) -> None:
507
- active_form_status_text = self._extract_active_form_text(event)
508
- self.spinner_status.set_todo_status(active_form_status_text if active_form_status_text else None)
521
+ self.spinner_status.set_todo_status(self._extract_active_form_text(event))
509
522
  # Clear tool calls when todo changes, as the tool execution has advanced
510
523
  self.spinner_status.clear_for_new_turn()
511
524
  self._update_spinner()
@@ -525,6 +538,8 @@ class DisplayEventHandler:
525
538
  self.renderer.spinner_stop()
526
539
  self.renderer.console.print(Rule(characters="─", style=ThemeKey.LINES))
527
540
  emit_tmux_signal() # Signal test harness if KLAUDE_TEST_SIGNAL is set
541
+ else:
542
+ self._sub_agent_thinking_headers.pop(event.session_id, None)
528
543
  await self.stage_manager.transition_to(Stage.WAITING)
529
544
  self._maybe_notify_task_finish(event)
530
545
 
@@ -555,13 +570,6 @@ class DisplayEventHandler:
555
570
  # Private helper methods
556
571
  # ─────────────────────────────────────────────────────────────────────────────
557
572
 
558
- async def _finish_assistant_stream(self) -> None:
559
- if self.assistant_stream.is_active:
560
- mdstream = self.assistant_stream.mdstream
561
- assert mdstream is not None
562
- mdstream.update(self.assistant_stream.buffer, final=True)
563
- self.assistant_stream.finish()
564
-
565
573
  def _update_spinner(self) -> None:
566
574
  """Update spinner text from current status state."""
567
575
  status_text = self.spinner_status.get_status()
@@ -571,27 +579,23 @@ class DisplayEventHandler:
571
579
  right_text,
572
580
  )
573
581
 
574
- async def _flush_assistant_buffer(self, state: StreamState) -> None:
575
- if state.is_active:
576
- mdstream = state.mdstream
577
- assert mdstream is not None
578
- mdstream.update(state.buffer)
582
+ def _flush_thinking_buffer(self) -> None:
583
+ self.thinking_stream.render(transform=normalize_thinking_content)
579
584
 
580
- async def _flush_thinking_buffer(self, state: StreamState) -> None:
581
- if state.is_active:
582
- mdstream = state.mdstream
583
- assert mdstream is not None
584
- mdstream.update(normalize_thinking_content(state.buffer))
585
+ def _flush_assistant_buffer(self) -> None:
586
+ self.assistant_stream.render()
585
587
 
586
588
  async def _finish_thinking_stream(self) -> None:
587
- if self.thinking_stream.is_active:
588
- mdstream = self.thinking_stream.mdstream
589
- assert mdstream is not None
590
- mdstream.update(normalize_thinking_content(self.thinking_stream.buffer), final=True)
591
- self.thinking_stream.finish()
589
+ finalized = self.thinking_stream.finalize(transform=normalize_thinking_content)
590
+ if finalized:
592
591
  self.renderer.print()
593
592
  self.renderer.spinner_start()
594
593
 
594
+ async def _finish_assistant_stream(self) -> None:
595
+ finalized = self.assistant_stream.finalize()
596
+ if finalized:
597
+ self.renderer.print()
598
+
595
599
  def _maybe_notify_task_finish(self, event: events.TaskFinishEvent) -> None:
596
600
  if self.notifier is None:
597
601
  return
@@ -617,12 +621,17 @@ class DisplayEventHandler:
617
621
  return squashed[:197] + "…"
618
622
  return squashed
619
623
 
620
- def _extract_active_form_text(self, todo_event: events.TodoChangeEvent) -> str:
621
- status_text = ""
624
+ def _extract_active_form_text(self, todo_event: events.TodoChangeEvent) -> str | None:
625
+ status_text: str | None = None
622
626
  for todo in todo_event.todos:
623
627
  if todo.status == "in_progress":
624
628
  if len(todo.active_form) > 0:
625
629
  status_text = todo.active_form
626
630
  if len(todo.content) > 0:
627
631
  status_text = todo.content
628
- return status_text.replace("\n", " ").strip()
632
+
633
+ if status_text is None:
634
+ return None
635
+
636
+ normalized = status_text.replace("\n", " ").strip()
637
+ return normalized if normalized else None
@@ -34,7 +34,7 @@ from klaude_code.config.thinking import (
34
34
  )
35
35
  from klaude_code.protocol import llm_param
36
36
  from klaude_code.protocol.commands import CommandInfo
37
- from klaude_code.protocol.model import UserInputPayload
37
+ from klaude_code.protocol.message import UserInputPayload
38
38
  from klaude_code.ui.core.input import InputProviderABC
39
39
  from klaude_code.ui.modes.repl.clipboard import capture_clipboard_tag, copy_to_clipboard, extract_images_from_text
40
40
  from klaude_code.ui.modes.repl.completers import AT_TOKEN_PATTERN, create_repl_completer
@@ -198,7 +198,7 @@ def create_key_bindings(
198
198
  """Ensure completions refresh on backspace when editing an @token.
199
199
 
200
200
  We delete the character before cursor (default behavior), then explicitly
201
- trigger completion refresh if the caret is still within an @... token.
201
+ trigger completion refresh if the caret is still within an @… token.
202
202
  """
203
203
  buf = event.current_buffer # type: ignore
204
204
  # Handle selection: cut selection if present, otherwise delete one character