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.
- klaude_code/app/runtime.py +2 -1
- klaude_code/auth/antigravity/oauth.py +0 -9
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- klaude_code/auth/codex/exceptions.py +0 -4
- klaude_code/auth/codex/oauth.py +32 -28
- klaude_code/auth/codex/token_manager.py +0 -18
- klaude_code/cli/cost_cmd.py +128 -39
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +15 -4
- klaude_code/config/assets/builtin_config.yaml +8 -24
- klaude_code/config/config.py +47 -25
- klaude_code/config/sub_agent_model_helper.py +18 -13
- klaude_code/config/thinking.py +0 -8
- klaude_code/const.py +2 -2
- klaude_code/core/agent_profile.py +11 -53
- klaude_code/core/compaction/compaction.py +4 -6
- klaude_code/core/compaction/overflow.py +0 -4
- klaude_code/core/executor.py +51 -5
- klaude_code/core/manager/llm_clients.py +9 -1
- klaude_code/core/prompts/prompt-claude-code.md +4 -4
- klaude_code/core/reminders.py +21 -23
- klaude_code/core/task.py +0 -4
- klaude_code/core/tool/__init__.py +3 -2
- klaude_code/core/tool/file/apply_patch.py +0 -27
- klaude_code/core/tool/file/edit_tool.py +1 -2
- klaude_code/core/tool/file/read_tool.md +3 -2
- klaude_code/core/tool/file/read_tool.py +15 -2
- klaude_code/core/tool/offload.py +0 -35
- klaude_code/core/tool/sub_agent/__init__.py +6 -0
- klaude_code/core/tool/sub_agent/image_gen.md +16 -0
- klaude_code/core/tool/sub_agent/image_gen.py +146 -0
- klaude_code/core/tool/sub_agent/task.md +20 -0
- klaude_code/core/tool/sub_agent/task.py +205 -0
- klaude_code/core/tool/tool_registry.py +0 -16
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/input.py +6 -5
- klaude_code/llm/antigravity/input.py +14 -7
- klaude_code/llm/codex/client.py +22 -0
- klaude_code/llm/codex/prompt_sync.py +237 -0
- klaude_code/llm/google/client.py +8 -6
- klaude_code/llm/google/input.py +20 -12
- klaude_code/llm/image.py +18 -11
- klaude_code/llm/input_common.py +14 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +16 -1
- klaude_code/llm/registry.py +0 -5
- klaude_code/llm/responses/input.py +15 -5
- klaude_code/llm/usage.py +0 -8
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +2 -1
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/model.py +20 -1
- klaude_code/protocol/op.py +27 -0
- klaude_code/protocol/op_handler.py +10 -0
- klaude_code/protocol/sub_agent/AGENTS.md +5 -5
- klaude_code/protocol/sub_agent/__init__.py +13 -34
- klaude_code/protocol/sub_agent/explore.py +7 -34
- klaude_code/protocol/sub_agent/image_gen.py +3 -74
- klaude_code/protocol/sub_agent/task.py +3 -47
- klaude_code/protocol/sub_agent/web.py +8 -52
- klaude_code/protocol/tools.py +2 -0
- klaude_code/session/export.py +308 -299
- klaude_code/session/session.py +58 -21
- klaude_code/session/store.py +0 -4
- klaude_code/session/templates/export_session.html +430 -134
- klaude_code/skill/assets/deslop/SKILL.md +9 -0
- klaude_code/skill/system_skills.py +0 -20
- klaude_code/tui/command/__init__.py +3 -0
- klaude_code/tui/command/continue_cmd.py +34 -0
- klaude_code/tui/command/fork_session_cmd.py +5 -2
- klaude_code/tui/command/resume_cmd.py +9 -2
- klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
- klaude_code/tui/components/assistant.py +0 -26
- klaude_code/tui/components/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +2 -208
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/rich/markdown.py +60 -63
- klaude_code/tui/components/rich/theme.py +2 -0
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +43 -21
- klaude_code/tui/input/images.py +21 -18
- klaude_code/tui/input/key_bindings.py +2 -2
- klaude_code/tui/input/prompt_toolkit.py +49 -49
- klaude_code/tui/machine.py +15 -11
- klaude_code/tui/renderer.py +12 -20
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +6 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/RECORD +97 -92
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
- {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
|
|
420
|
-
"""Render
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 = "
|
|
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 = "
|
|
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
|
-
|
|
329
|
+
details = Text("", ThemeKey.TOOL_PARAM)
|
|
303
330
|
if update_files:
|
|
304
|
-
|
|
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
|
-
|
|
309
|
-
|
|
337
|
+
details.append("Create ")
|
|
338
|
+
details.append_text(render_path(add_files[0], ThemeKey.TOOL_PARAM_FILE_PATH))
|
|
310
339
|
else:
|
|
311
|
-
|
|
340
|
+
details.append(f"Create × {len(add_files)}")
|
|
312
341
|
if delete_files:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
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
|
|
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:
|
klaude_code/tui/input/images.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
187
|
-
"""
|
|
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
|
-
|
|
207
|
-
|
|
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[
|
|
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[
|
|
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 =
|
|
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
|
|