klaude-code 2.8.0__py3-none-any.whl → 2.9.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 (100) hide show
  1. klaude_code/app/runtime.py +2 -1
  2. klaude_code/auth/antigravity/oauth.py +0 -9
  3. klaude_code/auth/antigravity/token_manager.py +0 -18
  4. klaude_code/auth/base.py +53 -0
  5. klaude_code/auth/codex/exceptions.py +0 -4
  6. klaude_code/auth/codex/oauth.py +32 -28
  7. klaude_code/auth/codex/token_manager.py +0 -18
  8. klaude_code/cli/cost_cmd.py +128 -39
  9. klaude_code/cli/list_model.py +27 -10
  10. klaude_code/cli/main.py +15 -4
  11. klaude_code/config/assets/builtin_config.yaml +8 -24
  12. klaude_code/config/config.py +47 -25
  13. klaude_code/config/sub_agent_model_helper.py +18 -13
  14. klaude_code/config/thinking.py +0 -8
  15. klaude_code/const.py +2 -2
  16. klaude_code/core/agent_profile.py +11 -53
  17. klaude_code/core/compaction/compaction.py +4 -6
  18. klaude_code/core/compaction/overflow.py +0 -4
  19. klaude_code/core/executor.py +51 -5
  20. klaude_code/core/manager/llm_clients.py +9 -1
  21. klaude_code/core/prompts/prompt-claude-code.md +4 -4
  22. klaude_code/core/reminders.py +21 -23
  23. klaude_code/core/task.py +0 -4
  24. klaude_code/core/tool/__init__.py +3 -2
  25. klaude_code/core/tool/file/apply_patch.py +0 -27
  26. klaude_code/core/tool/file/edit_tool.py +1 -2
  27. klaude_code/core/tool/file/read_tool.md +3 -2
  28. klaude_code/core/tool/file/read_tool.py +15 -2
  29. klaude_code/core/tool/offload.py +0 -35
  30. klaude_code/core/tool/sub_agent/__init__.py +6 -0
  31. klaude_code/core/tool/sub_agent/image_gen.md +16 -0
  32. klaude_code/core/tool/sub_agent/image_gen.py +146 -0
  33. klaude_code/core/tool/sub_agent/task.md +20 -0
  34. klaude_code/core/tool/sub_agent/task.py +205 -0
  35. klaude_code/core/tool/tool_registry.py +0 -16
  36. klaude_code/core/turn.py +1 -1
  37. klaude_code/llm/anthropic/input.py +6 -5
  38. klaude_code/llm/antigravity/input.py +14 -7
  39. klaude_code/llm/codex/client.py +22 -0
  40. klaude_code/llm/codex/prompt_sync.py +237 -0
  41. klaude_code/llm/google/client.py +8 -6
  42. klaude_code/llm/google/input.py +20 -12
  43. klaude_code/llm/image.py +18 -11
  44. klaude_code/llm/input_common.py +14 -6
  45. klaude_code/llm/json_stable.py +37 -0
  46. klaude_code/llm/openai_compatible/input.py +0 -10
  47. klaude_code/llm/openai_compatible/stream.py +16 -1
  48. klaude_code/llm/registry.py +0 -5
  49. klaude_code/llm/responses/input.py +15 -5
  50. klaude_code/llm/usage.py +0 -8
  51. klaude_code/protocol/commands.py +1 -0
  52. klaude_code/protocol/events.py +2 -1
  53. klaude_code/protocol/message.py +2 -2
  54. klaude_code/protocol/model.py +20 -1
  55. klaude_code/protocol/op.py +27 -0
  56. klaude_code/protocol/op_handler.py +10 -0
  57. klaude_code/protocol/sub_agent/AGENTS.md +5 -5
  58. klaude_code/protocol/sub_agent/__init__.py +13 -34
  59. klaude_code/protocol/sub_agent/explore.py +7 -34
  60. klaude_code/protocol/sub_agent/image_gen.py +3 -74
  61. klaude_code/protocol/sub_agent/task.py +3 -47
  62. klaude_code/protocol/sub_agent/web.py +8 -52
  63. klaude_code/protocol/tools.py +2 -0
  64. klaude_code/session/export.py +308 -299
  65. klaude_code/session/session.py +58 -21
  66. klaude_code/session/store.py +0 -4
  67. klaude_code/session/templates/export_session.html +430 -134
  68. klaude_code/skill/assets/deslop/SKILL.md +9 -0
  69. klaude_code/skill/system_skills.py +0 -20
  70. klaude_code/tui/command/__init__.py +3 -0
  71. klaude_code/tui/command/continue_cmd.py +34 -0
  72. klaude_code/tui/command/fork_session_cmd.py +5 -2
  73. klaude_code/tui/command/resume_cmd.py +9 -2
  74. klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
  75. klaude_code/tui/components/assistant.py +0 -26
  76. klaude_code/tui/components/command_output.py +3 -1
  77. klaude_code/tui/components/developer.py +3 -0
  78. klaude_code/tui/components/diffs.py +2 -208
  79. klaude_code/tui/components/errors.py +4 -0
  80. klaude_code/tui/components/mermaid_viewer.py +2 -2
  81. klaude_code/tui/components/rich/markdown.py +60 -63
  82. klaude_code/tui/components/rich/theme.py +2 -0
  83. klaude_code/tui/components/sub_agent.py +2 -46
  84. klaude_code/tui/components/thinking.py +0 -33
  85. klaude_code/tui/components/tools.py +43 -21
  86. klaude_code/tui/input/images.py +21 -18
  87. klaude_code/tui/input/key_bindings.py +2 -2
  88. klaude_code/tui/input/prompt_toolkit.py +49 -49
  89. klaude_code/tui/machine.py +15 -11
  90. klaude_code/tui/renderer.py +12 -20
  91. klaude_code/tui/runner.py +2 -1
  92. klaude_code/tui/terminal/image.py +6 -34
  93. klaude_code/ui/common.py +0 -70
  94. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
  95. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/RECORD +97 -92
  96. klaude_code/core/tool/sub_agent_tool.py +0 -126
  97. klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
  98. klaude_code/tui/components/rich/searchable_text.py +0 -68
  99. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
  100. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
@@ -5,6 +5,7 @@ import io
5
5
  import re
6
6
  import time
7
7
  from collections.abc import Callable
8
+ from pathlib import Path
8
9
  from typing import Any, ClassVar
9
10
 
10
11
  from markdown_it import MarkdownIt
@@ -12,7 +13,7 @@ from markdown_it.token import Token
12
13
  from rich import box
13
14
  from rich._loop import loop_first
14
15
  from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
15
- from rich.markdown import CodeBlock, Heading, ListItem, Markdown, MarkdownElement, TableElement
16
+ from rich.markdown import CodeBlock, Heading, ImageItem, ListItem, Markdown, MarkdownElement, TableElement
16
17
  from rich.rule import Rule
17
18
  from rich.segment import Segment
18
19
  from rich.style import Style, StyleType
@@ -207,6 +208,25 @@ class CheckboxListItem(ListItem):
207
208
  yield new_line
208
209
 
209
210
 
211
+ class LocalImageItem(ImageItem):
212
+ """Image element that collects local file paths for external rendering."""
213
+
214
+ @classmethod
215
+ def create(cls, markdown: Markdown, token: Token) -> MarkdownElement:
216
+ src = str(token.attrs.get("src", ""))
217
+ instance = cls(src, markdown.hyperlinks)
218
+ if src.startswith("/") and Path(src).exists():
219
+ collected = getattr(markdown, "collected_images", None)
220
+ if collected is not None:
221
+ collected.append(src)
222
+ return instance
223
+
224
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
225
+ if self.destination.startswith("/") and Path(self.destination).exists():
226
+ return
227
+ yield from super().__rich_console__(console, options)
228
+
229
+
210
230
  class NoInsetMarkdown(Markdown):
211
231
  """Markdown with code blocks that have no padding and left-justified headings."""
212
232
 
@@ -219,8 +239,13 @@ class NoInsetMarkdown(Markdown):
219
239
  "table_open": MarkdownTable,
220
240
  "html_block": ThinkingHTMLBlock,
221
241
  "list_item_open": CheckboxListItem,
242
+ "image": LocalImageItem,
222
243
  }
223
244
 
245
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
246
+ super().__init__(*args, **kwargs)
247
+ self.collected_images: list[str] = []
248
+
224
249
 
225
250
  class ThinkingMarkdown(Markdown):
226
251
  """Markdown for thinking content with grey-styled code blocks and left-justified headings."""
@@ -234,8 +259,13 @@ class ThinkingMarkdown(Markdown):
234
259
  "table_open": MarkdownTable,
235
260
  "html_block": ThinkingHTMLBlock,
236
261
  "list_item_open": CheckboxListItem,
262
+ "image": LocalImageItem,
237
263
  }
238
264
 
265
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
266
+ super().__init__(*args, **kwargs)
267
+ self.collected_images: list[str] = []
268
+
239
269
 
240
270
  class MarkdownStream:
241
271
  """Block-based streaming Markdown renderer.
@@ -260,6 +290,7 @@ class MarkdownStream:
260
290
  left_margin: int = 0,
261
291
  right_margin: int = MARKDOWN_RIGHT_MARGIN,
262
292
  markdown_class: Callable[..., Markdown] | None = None,
293
+ image_callback: Callable[[str], None] | None = None,
263
294
  ) -> None:
264
295
  """Initialize the markdown stream.
265
296
 
@@ -272,6 +303,7 @@ class MarkdownStream:
272
303
  left_margin (int, optional): Number of columns to reserve on the left side
273
304
  right_margin (int, optional): Number of columns to reserve on the right side
274
305
  markdown_class: Markdown class to use for rendering (defaults to NoInsetMarkdown)
306
+ image_callback: Callback to display local images (called with file path)
275
307
  """
276
308
  self._stable_rendered_lines: list[str] = []
277
309
  self._stable_source_line_count: int = 0
@@ -282,6 +314,8 @@ class MarkdownStream:
282
314
  self.mdargs = {}
283
315
 
284
316
  self._live_sink = live_sink
317
+ self._image_callback = image_callback
318
+ self._displayed_images: set[str] = set()
285
319
 
286
320
  # Streaming control
287
321
  self.when: float = 0.0 # Timestamp of last update
@@ -298,11 +332,6 @@ class MarkdownStream:
298
332
  self.right_margin: int = max(right_margin, 0)
299
333
  self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
300
334
 
301
- @property
302
- def _live_started(self) -> bool:
303
- """Check if Live display has been started (derived from self.live)."""
304
- return self._live_sink is not None
305
-
306
335
  def _get_base_width(self) -> int:
307
336
  return self.console.options.max_width
308
337
 
@@ -416,66 +445,21 @@ class MarkdownStream:
416
445
  return "", text, 0
417
446
  return stable_source, live_source, stable_line
418
447
 
419
- def render_ansi(self, text: str, *, apply_mark: bool) -> str:
420
- """Render markdown source to an ANSI string.
448
+ def render_stable_ansi(self, stable_source: str, *, has_live_suffix: bool, final: bool) -> tuple[str, list[str]]:
449
+ """Render stable prefix to ANSI, preserving inter-block spacing.
421
450
 
422
- This is primarily intended for internal debugging and tests.
451
+ Returns:
452
+ tuple: (ANSI string, collected local image paths)
423
453
  """
424
-
425
- return "".join(self._render_markdown_to_lines(text, apply_mark=apply_mark))
426
-
427
- def render_stable_ansi(self, stable_source: str, *, has_live_suffix: bool, final: bool) -> str:
428
- """Render stable prefix to ANSI, preserving inter-block spacing."""
429
-
430
454
  if not stable_source:
431
- return ""
455
+ return "", []
432
456
 
433
457
  render_source = stable_source
434
458
  if not final and has_live_suffix:
435
459
  render_source = self._append_nonfinal_sentinel(stable_source)
436
460
 
437
- return self.render_ansi(render_source, apply_mark=True)
438
-
439
- @staticmethod
440
- def normalize_live_ansi_for_boundary(*, stable_ansi: str, live_ansi: str) -> str:
441
- """Normalize whitespace at the stable/live boundary.
442
-
443
- Some Rich Markdown blocks (e.g. lists) render with a leading blank line.
444
- If the stable prefix already renders a trailing blank line, rendering the
445
- live suffix separately may introduce an extra blank line that wouldn't
446
- appear when rendering the full document.
447
-
448
- This function removes *overlapping* blank lines from the live ANSI when
449
- the stable ANSI already ends with one or more blank lines.
450
-
451
- Important: don't remove *all* leading blank lines from the live suffix.
452
- In some incomplete-block cases, the live render may begin with multiple
453
- blank lines while the full-document render would keep one of them.
454
- """
455
-
456
- stable_lines = stable_ansi.splitlines(keepends=True)
457
- if not stable_lines:
458
- return live_ansi
459
-
460
- stable_trailing_blank = 0
461
- for line in reversed(stable_lines):
462
- if line.strip():
463
- break
464
- stable_trailing_blank += 1
465
- if stable_trailing_blank <= 0:
466
- return live_ansi
467
-
468
- live_lines = live_ansi.splitlines(keepends=True)
469
- live_leading_blank = 0
470
- for line in live_lines:
471
- if line.strip():
472
- break
473
- live_leading_blank += 1
474
-
475
- drop = min(stable_trailing_blank, live_leading_blank)
476
- if drop > 0:
477
- live_lines = live_lines[drop:]
478
- return "".join(live_lines)
461
+ lines, images = self._render_markdown_to_lines(render_source, apply_mark=True)
462
+ return "".join(lines), images
479
463
 
480
464
  def _append_nonfinal_sentinel(self, stable_source: str) -> str:
481
465
  """Make Rich render stable content as if it isn't the last block.
@@ -497,14 +481,14 @@ class MarkdownStream:
497
481
  return stable_source + "\n<!-- -->"
498
482
  return stable_source + "\n\n<!-- -->"
499
483
 
500
- def _render_markdown_to_lines(self, text: str, *, apply_mark: bool) -> list[str]:
484
+ def _render_markdown_to_lines(self, text: str, *, apply_mark: bool) -> tuple[list[str], list[str]]:
501
485
  """Render markdown text to a list of lines.
502
486
 
503
487
  Args:
504
488
  text (str): Markdown text to render
505
489
 
506
490
  Returns:
507
- list: List of rendered lines with line endings preserved
491
+ tuple: (lines with line endings preserved, collected local image paths)
508
492
  """
509
493
  # Render the markdown to a string buffer
510
494
  string_io = io.StringIO()
@@ -526,6 +510,8 @@ class MarkdownStream:
526
510
  temp_console.print(markdown)
527
511
  output = string_io.getvalue()
528
512
 
513
+ collected_images = getattr(markdown, "collected_images", [])
514
+
529
515
  # Split rendered output into lines, strip trailing spaces, and apply left margin.
530
516
  lines = output.splitlines(keepends=True)
531
517
  indent_prefix = " " * self.left_margin if self.left_margin > 0 else ""
@@ -559,7 +545,7 @@ class MarkdownStream:
559
545
  stripped += "\n"
560
546
  processed_lines.append(stripped)
561
547
 
562
- return processed_lines
548
+ return processed_lines, list(collected_images)
563
549
 
564
550
  def __del__(self) -> None:
565
551
  """Destructor to ensure Live display is properly cleaned up."""
@@ -587,15 +573,22 @@ class MarkdownStream:
587
573
  start = time.time()
588
574
 
589
575
  stable_chunk_to_print: str | None = None
576
+ new_images: list[str] = []
590
577
  stable_changed = final or stable_line > self._stable_source_line_count
591
578
  if stable_changed and stable_source:
592
- stable_ansi = self.render_stable_ansi(stable_source, has_live_suffix=bool(live_source), final=final)
579
+ stable_ansi, collected_images = self.render_stable_ansi(
580
+ stable_source, has_live_suffix=bool(live_source), final=final
581
+ )
593
582
  stable_lines = stable_ansi.splitlines(keepends=True)
594
583
  new_lines = stable_lines[len(self._stable_rendered_lines) :]
595
584
  if new_lines:
596
585
  stable_chunk_to_print = "".join(new_lines)
597
586
  self._stable_rendered_lines = stable_lines
598
587
  self._stable_source_line_count = stable_line
588
+ for img in collected_images:
589
+ if img not in self._displayed_images:
590
+ new_images.append(img)
591
+ self._displayed_images.add(img)
599
592
  elif final and not stable_source:
600
593
  self._stable_rendered_lines = []
601
594
  self._stable_source_line_count = stable_line
@@ -603,7 +596,7 @@ class MarkdownStream:
603
596
  live_text_to_set: Text | None = None
604
597
  if not final and MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._live_sink is not None:
605
598
  apply_mark_live = self._stable_source_line_count == 0
606
- live_lines = self._render_markdown_to_lines(live_source, apply_mark=apply_mark_live)
599
+ live_lines, _ = self._render_markdown_to_lines(live_source, apply_mark=apply_mark_live)
607
600
 
608
601
  if self._stable_rendered_lines:
609
602
  stable_trailing_blank = 0
@@ -629,6 +622,10 @@ class MarkdownStream:
629
622
  if stable_chunk_to_print:
630
623
  self.console.print(Text.from_ansi(stable_chunk_to_print), end="\n")
631
624
 
625
+ if new_images and self._image_callback:
626
+ for img_path in new_images:
627
+ self._image_callback(img_path)
628
+
632
629
  if final:
633
630
  if self._live_sink is not None:
634
631
  self._live_sink(None)
@@ -109,6 +109,7 @@ DARK_PALETTE = Palette(
109
109
 
110
110
  class ThemeKey(str, Enum):
111
111
  LINES = "lines"
112
+ LINES_DIM = "lines.dim"
112
113
 
113
114
  # CODE
114
115
  CODE_BACKGROUND = "code_background"
@@ -233,6 +234,7 @@ def get_theme(theme: str | None = None) -> Themes:
233
234
  app_theme=Theme(
234
235
  styles={
235
236
  ThemeKey.LINES.value: palette.grey3,
237
+ ThemeKey.LINES_DIM.value: "dim " + palette.grey3,
236
238
  # CODE
237
239
  ThemeKey.CODE_BACKGROUND.value: f"on {palette.code_background}",
238
240
  # PANEL
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Any, cast
2
+ from typing import Any
3
3
 
4
4
  from rich.console import Group, RenderableType
5
5
  from rich.json import JSON
@@ -7,8 +7,7 @@ from rich.style import Style
7
7
  from rich.text import Text
8
8
 
9
9
  from klaude_code.const import SUB_AGENT_RESULT_MAX_LINES
10
- from klaude_code.protocol import events, model
11
- from klaude_code.protocol.sub_agent import get_sub_agent_profile_by_tool
10
+ from klaude_code.protocol import model
12
11
  from klaude_code.tui.components.common import truncate_head
13
12
  from klaude_code.tui.components.rich.theme import ThemeKey
14
13
 
@@ -125,46 +124,3 @@ def render_sub_agent_result(
125
124
  elements.append(Text(agent_id_footer, style=ThemeKey.SUB_AGENT_FOOTER))
126
125
 
127
126
  return Group(*elements)
128
-
129
-
130
- def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAgentState | None:
131
- """Build SubAgentState from a tool call event for replay rendering."""
132
- profile = get_sub_agent_profile_by_tool(e.tool_name)
133
- if profile is None:
134
- return None
135
- description = profile.name
136
- prompt = ""
137
- output_schema: dict[str, Any] | None = None
138
- generation: dict[str, Any] | None = None
139
- resume: str | None = None
140
- if e.arguments:
141
- try:
142
- payload: dict[str, object] = json.loads(e.arguments)
143
- except json.JSONDecodeError:
144
- payload = {}
145
- desc_value = payload.get("description")
146
- if isinstance(desc_value, str) and desc_value.strip():
147
- description = desc_value.strip()
148
- prompt_value = payload.get("prompt") or payload.get("task")
149
- if isinstance(prompt_value, str):
150
- prompt = prompt_value.strip()
151
- resume_value = payload.get("resume")
152
- if isinstance(resume_value, str) and resume_value.strip():
153
- resume = resume_value.strip()
154
- # Extract output_schema if profile supports it
155
- if profile.output_schema_arg:
156
- schema_value = payload.get(profile.output_schema_arg)
157
- if isinstance(schema_value, dict):
158
- output_schema = cast(dict[str, Any], schema_value)
159
- # Extract generation config for ImageGen
160
- generation_value = payload.get("generation")
161
- if isinstance(generation_value, dict):
162
- generation = cast(dict[str, Any], generation_value)
163
- return model.SubAgentState(
164
- sub_agent_type=profile.name,
165
- sub_agent_desc=description,
166
- sub_agent_prompt=prompt,
167
- resume=resume,
168
- output_schema=output_schema,
169
- generation=generation,
170
- )
@@ -1,14 +1,5 @@
1
1
  import re
2
2
 
3
- from rich.console import RenderableType
4
- from rich.padding import Padding
5
- from rich.text import Text
6
-
7
- from klaude_code.const import MARKDOWN_RIGHT_MARGIN
8
- from klaude_code.tui.components.common import create_grid
9
- from klaude_code.tui.components.rich.markdown import ThinkingMarkdown
10
- from klaude_code.tui.components.rich.theme import ThemeKey
11
-
12
3
  # UI markers
13
4
  THINKING_MESSAGE_MARK = "∴"
14
5
 
@@ -70,27 +61,3 @@ def extract_last_bold_header(text: str) -> str | None:
70
61
  i = end + 2
71
62
 
72
63
  return last
73
-
74
-
75
- def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableType | None:
76
- """Render thinking content as markdown with left mark.
77
-
78
- Returns None if content is empty.
79
- Note: Caller should push thinking_markdown_theme before printing.
80
- """
81
- if len(content.strip()) == 0:
82
- return None
83
-
84
- grid = create_grid()
85
- grid.add_row(
86
- Text(THINKING_MESSAGE_MARK, style=ThemeKey.THINKING),
87
- Padding(
88
- ThinkingMarkdown(
89
- normalize_thinking_content(content),
90
- code_theme=code_theme,
91
- style=style,
92
- ),
93
- (0, MARKDOWN_RIGHT_MARGIN, 0, 0),
94
- ),
95
- )
96
- return grid
@@ -48,6 +48,33 @@ def is_sub_agent_tool(tool_name: str) -> bool:
48
48
  return _is_sub_agent_tool(tool_name)
49
49
 
50
50
 
51
+ def get_task_active_form(arguments: str) -> str:
52
+ """Return active form text for Task tool based on its arguments."""
53
+ import json
54
+
55
+ try:
56
+ parsed = json.loads(arguments)
57
+ except json.JSONDecodeError:
58
+ return "Tasking"
59
+
60
+ if not isinstance(parsed, dict):
61
+ return "Tasking"
62
+
63
+ args = cast(dict[str, Any], parsed)
64
+
65
+ type_raw = args.get("type")
66
+ if not isinstance(type_raw, str):
67
+ return "Tasking"
68
+
69
+ match type_raw.strip():
70
+ case "explore":
71
+ return "Exploring"
72
+ case "web":
73
+ return "Surfing"
74
+ case _:
75
+ return "Tasking"
76
+
77
+
51
78
  def render_path(path: str, style: str, is_directory: bool = False) -> Text:
52
79
  if path.startswith(str(Path().cwd())):
53
80
  path = path.replace(str(Path().cwd()), "").lstrip("/")
@@ -173,7 +200,7 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
173
200
 
174
201
 
175
202
  def render_update_plan_tool_call(arguments: str) -> RenderableType:
176
- tool_name = "Update Plan"
203
+ tool_name = "Plan"
177
204
  details: RenderableType | None = None
178
205
 
179
206
  if arguments:
@@ -273,7 +300,7 @@ def render_write_tool_call(arguments: str) -> RenderableType:
273
300
 
274
301
 
275
302
  def render_apply_patch_tool_call(arguments: str) -> RenderableType:
276
- tool_name = "Apply Patch"
303
+ tool_name = "Patch"
277
304
 
278
305
  try:
279
306
  payload = json.loads(arguments)
@@ -299,21 +326,22 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
299
326
  elif line.startswith("*** Delete File:"):
300
327
  delete_files.append(line[len("*** Delete File:") :].strip())
301
328
 
302
- parts: list[str] = []
329
+ details = Text("", ThemeKey.TOOL_PARAM)
303
330
  if update_files:
304
- parts.append(f"Update File × {len(update_files)}" if len(update_files) > 1 else "Update File")
331
+ details.append(f"Edit × {len(update_files)}")
305
332
  if add_files:
333
+ if details.plain:
334
+ details.append(", ")
306
335
  # For single .md file addition, show filename in parentheses
307
336
  if len(add_files) == 1 and add_files[0].endswith(".md"):
308
- file_name = Path(add_files[0]).name
309
- parts.append(f"Add File ({file_name})")
337
+ details.append("Create ")
338
+ details.append_text(render_path(add_files[0], ThemeKey.TOOL_PARAM_FILE_PATH))
310
339
  else:
311
- parts.append(f"Add File × {len(add_files)}" if len(add_files) > 1 else "Add File")
340
+ details.append(f"Create × {len(add_files)}")
312
341
  if delete_files:
313
- parts.append(f"Delete File × {len(delete_files)}" if len(delete_files) > 1 else "Delete File")
314
-
315
- if parts:
316
- details = Text(", ".join(parts), ThemeKey.TOOL_PARAM)
342
+ if details.plain:
343
+ details.append(", ")
344
+ details.append(f"Delete × {len(delete_files)}")
317
345
  else:
318
346
  details = Text(
319
347
  str(patch_content)[:INVALID_TOOL_CALL_MAX_LENGTH],
@@ -434,7 +462,7 @@ def _render_mermaid_viewer_link(
434
462
 
435
463
 
436
464
  def render_web_fetch_tool_call(arguments: str) -> RenderableType:
437
- tool_name = "Fetch"
465
+ tool_name = "Fetch Web"
438
466
 
439
467
  try:
440
468
  payload: dict[str, str] = json.loads(arguments)
@@ -452,7 +480,7 @@ def render_web_fetch_tool_call(arguments: str) -> RenderableType:
452
480
 
453
481
 
454
482
  def render_web_search_tool_call(arguments: str) -> RenderableType:
455
- tool_name = "Web Search"
483
+ tool_name = "Search Web"
456
484
 
457
485
  try:
458
486
  payload: dict[str, Any] = json.loads(arguments)
@@ -516,6 +544,7 @@ _TOOL_ACTIVE_FORM: dict[str, str] = {
516
544
  tools.WEB_FETCH: "Fetching Web",
517
545
  tools.WEB_SEARCH: "Searching Web",
518
546
  tools.REPORT_BACK: "Reporting",
547
+ tools.IMAGE_GEN: "Generating Image",
519
548
  }
520
549
 
521
550
 
@@ -527,13 +556,6 @@ def get_tool_active_form(tool_name: str) -> str:
527
556
  if tool_name in _TOOL_ACTIVE_FORM:
528
557
  return _TOOL_ACTIVE_FORM[tool_name]
529
558
 
530
- # Check sub agent profiles
531
- from klaude_code.protocol.sub_agent import get_sub_agent_profile_by_tool
532
-
533
- profile = get_sub_agent_profile_by_tool(tool_name)
534
- if profile and profile.active_form:
535
- return profile.active_form
536
-
537
559
  return f"Calling {tool_name}"
538
560
 
539
561
 
@@ -558,7 +580,7 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
558
580
  case tools.APPLY_PATCH:
559
581
  return render_apply_patch_tool_call(e.arguments)
560
582
  case tools.TODO_WRITE:
561
- return render_generic_tool_call("Update Todos", "", MARK_PLAN)
583
+ return render_generic_tool_call("Update To-Dos", "", MARK_PLAN)
562
584
  case tools.UPDATE_PLAN:
563
585
  return render_update_plan_tool_call(e.arguments)
564
586
  case tools.MERMAID:
@@ -17,11 +17,10 @@ import shutil
17
17
  import subprocess
18
18
  import sys
19
19
  import uuid
20
- from base64 import b64encode
21
20
  from pathlib import Path
22
21
 
23
22
  from klaude_code.const import get_system_temp
24
- from klaude_code.protocol.message import ImageURLPart
23
+ from klaude_code.protocol.message import ImageFilePart
25
24
 
26
25
  # ---------------------------------------------------------------------------
27
26
  # Constants and marker syntax
@@ -183,36 +182,40 @@ def capture_clipboard_tag() -> str | None:
183
182
  # ---------------------------------------------------------------------------
184
183
 
185
184
 
186
- def _encode_image_file(file_path: str) -> ImageURLPart | None:
187
- """Encode an image file as base64 data URL and create ImageURLPart."""
185
+ _MIME_TYPES: dict[str, str] = {
186
+ ".png": "image/png",
187
+ ".jpg": "image/jpeg",
188
+ ".jpeg": "image/jpeg",
189
+ ".gif": "image/gif",
190
+ ".webp": "image/webp",
191
+ }
192
+
193
+
194
+ def _create_image_file_part(file_path: str) -> ImageFilePart | None:
195
+ """Create an ImageFilePart from a file path."""
188
196
  try:
189
197
  path = Path(file_path)
190
198
  if not path.exists():
191
199
  return None
192
- with open(path, "rb") as f:
193
- encoded = b64encode(f.read()).decode("ascii")
194
200
 
195
201
  suffix = path.suffix.lower()
196
- mime = {
197
- ".png": "image/png",
198
- ".jpg": "image/jpeg",
199
- ".jpeg": "image/jpeg",
200
- ".gif": "image/gif",
201
- ".webp": "image/webp",
202
- }.get(suffix)
202
+ mime = _MIME_TYPES.get(suffix)
203
203
  if mime is None:
204
204
  return None
205
205
 
206
- data_url = f"data:{mime};base64,{encoded}"
207
- return ImageURLPart(url=data_url, id=None)
206
+ return ImageFilePart(
207
+ file_path=str(path),
208
+ mime_type=mime,
209
+ byte_size=path.stat().st_size,
210
+ )
208
211
  except OSError:
209
212
  return None
210
213
 
211
214
 
212
- def extract_images_from_text(text: str) -> list[ImageURLPart]:
215
+ def extract_images_from_text(text: str) -> list[ImageFilePart]:
213
216
  """Extract images referenced by [image ...] markers in text."""
214
217
 
215
- images: list[ImageURLPart] = []
218
+ images: list[ImageFilePart] = []
216
219
  for m in IMAGE_MARKER_RE.finditer(text):
217
220
  raw = m.group("path")
218
221
  path_str = parse_image_marker_path(raw)
@@ -221,7 +224,7 @@ def extract_images_from_text(text: str) -> list[ImageURLPart]:
221
224
  p = Path(path_str).expanduser()
222
225
  if not p.is_absolute():
223
226
  p = (Path.cwd() / p).resolve()
224
- image_part = _encode_image_file(str(p))
227
+ image_part = _create_image_file_part(str(p))
225
228
  if image_part:
226
229
  images.append(image_part)
227
230
  return images
@@ -19,7 +19,7 @@ from typing import cast
19
19
  from prompt_toolkit.application.current import get_app
20
20
  from prompt_toolkit.buffer import Buffer
21
21
  from prompt_toolkit.filters import Always, Condition, Filter
22
- from prompt_toolkit.filters.app import has_completions
22
+ from prompt_toolkit.filters.app import has_completions, is_searching
23
23
  from prompt_toolkit.key_binding import KeyBindings
24
24
  from prompt_toolkit.key_binding.key_processor import KeyPressEvent
25
25
  from prompt_toolkit.keys import Keys
@@ -367,7 +367,7 @@ def create_key_bindings(
367
367
 
368
368
  _insert_newline(event)
369
369
 
370
- @kb.add("enter", filter=enabled)
370
+ @kb.add("enter", filter=enabled & ~is_searching)
371
371
  def _(event: KeyPressEvent) -> None:
372
372
  nonlocal swallow_next_control_j
373
373