klaude-code 2.9.0__py3-none-any.whl → 2.9.1__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/auth/antigravity/oauth.py +33 -29
- klaude_code/auth/claude/oauth.py +34 -49
- klaude_code/config/assets/builtin_config.yaml +17 -0
- klaude_code/core/agent_profile.py +2 -5
- klaude_code/core/task.py +1 -1
- klaude_code/core/tool/file/read_tool.py +13 -2
- klaude_code/core/tool/shell/bash_tool.py +1 -1
- klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
- klaude_code/llm/input_common.py +18 -0
- klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
- klaude_code/llm/{codex → openai_codex}/client.py +3 -3
- klaude_code/llm/openai_compatible/client.py +3 -1
- klaude_code/llm/openai_compatible/stream.py +19 -9
- klaude_code/llm/{responses → openai_responses}/client.py +1 -1
- klaude_code/llm/registry.py +3 -3
- klaude_code/llm/stream_parts.py +3 -1
- klaude_code/llm/usage.py +1 -1
- klaude_code/protocol/events.py +0 -1
- klaude_code/protocol/message.py +1 -0
- klaude_code/protocol/model.py +14 -1
- klaude_code/session/session.py +22 -1
- klaude_code/tui/components/bash_syntax.py +4 -0
- klaude_code/tui/components/diffs.py +3 -2
- klaude_code/tui/components/metadata.py +0 -3
- klaude_code/tui/components/rich/markdown.py +120 -33
- klaude_code/tui/components/rich/status.py +2 -2
- klaude_code/tui/components/rich/theme.py +9 -6
- klaude_code/tui/components/tools.py +22 -0
- klaude_code/tui/components/user_input.py +2 -0
- klaude_code/tui/machine.py +25 -47
- klaude_code/tui/renderer.py +37 -13
- klaude_code/tui/terminal/image.py +24 -3
- {klaude_code-2.9.0.dist-info → klaude_code-2.9.1.dist-info}/METADATA +1 -1
- {klaude_code-2.9.0.dist-info → klaude_code-2.9.1.dist-info}/RECORD +40 -40
- klaude_code/llm/bedrock/__init__.py +0 -3
- /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
- /klaude_code/llm/{codex → openai_codex}/prompt_sync.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/input.py +0 -0
- {klaude_code-2.9.0.dist-info → klaude_code-2.9.1.dist-info}/WHEEL +0 -0
- {klaude_code-2.9.0.dist-info → klaude_code-2.9.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from rich.console import RenderableType
|
|
2
2
|
from rich.text import Text
|
|
3
3
|
|
|
4
|
-
from klaude_code.const import DIFF_PREFIX_WIDTH
|
|
4
|
+
from klaude_code.const import DIFF_PREFIX_WIDTH, TAB_EXPAND_WIDTH
|
|
5
5
|
from klaude_code.protocol import model
|
|
6
6
|
from klaude_code.tui.components.common import create_grid
|
|
7
7
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
@@ -74,7 +74,8 @@ def _render_structured_line(line: model.DiffLine) -> Text:
|
|
|
74
74
|
return Text("")
|
|
75
75
|
text = Text()
|
|
76
76
|
for span in line.spans:
|
|
77
|
-
|
|
77
|
+
content = span.text.expandtabs(TAB_EXPAND_WIDTH)
|
|
78
|
+
text.append(content, style=_span_style(line.kind, span.op))
|
|
78
79
|
return text
|
|
79
80
|
|
|
80
81
|
|
|
@@ -136,9 +136,6 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
|
136
136
|
"""Render task metadata including main agent and sub-agents."""
|
|
137
137
|
renderables: list[RenderableType] = []
|
|
138
138
|
|
|
139
|
-
if e.cancelled:
|
|
140
|
-
renderables.append(Text())
|
|
141
|
-
|
|
142
139
|
has_sub_agents = len(e.metadata.sub_agent_task_metadata) > 0
|
|
143
140
|
# Use an extra space for the main agent mark to align with two-character marks (├─, └─)
|
|
144
141
|
main_mark_text = "✓"
|
|
@@ -14,7 +14,6 @@ from rich import box
|
|
|
14
14
|
from rich._loop import loop_first
|
|
15
15
|
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
16
16
|
from rich.markdown import CodeBlock, Heading, ImageItem, ListItem, Markdown, MarkdownElement, TableElement
|
|
17
|
-
from rich.rule import Rule
|
|
18
17
|
from rich.segment import Segment
|
|
19
18
|
from rich.style import Style, StyleType
|
|
20
19
|
from rich.syntax import Syntax
|
|
@@ -120,7 +119,8 @@ class Divider(MarkdownElement):
|
|
|
120
119
|
|
|
121
120
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
122
121
|
style = console.get_style("markdown.hr", default="none")
|
|
123
|
-
|
|
122
|
+
width = min(options.max_width, 100)
|
|
123
|
+
yield Text("-" * width, style=style)
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
class MarkdownTable(TableElement):
|
|
@@ -153,7 +153,8 @@ class LeftHeading(Heading):
|
|
|
153
153
|
h1_text = text.assemble((" ", "markdown.h1"), text, (" ", "markdown.h1"))
|
|
154
154
|
yield h1_text
|
|
155
155
|
elif self.tag == "h2":
|
|
156
|
-
|
|
156
|
+
h2_style = console.get_style("markdown.h2", default="bold")
|
|
157
|
+
text.stylize(h2_style + Style(underline=False))
|
|
157
158
|
yield text
|
|
158
159
|
else:
|
|
159
160
|
yield text
|
|
@@ -394,12 +395,25 @@ class MarkdownStream:
|
|
|
394
395
|
return 0
|
|
395
396
|
|
|
396
397
|
top_level: list[Token] = [token for token in tokens if token.level == 0 and token.map is not None]
|
|
397
|
-
if
|
|
398
|
+
if not top_level:
|
|
398
399
|
return 0
|
|
399
400
|
|
|
400
401
|
last = top_level[-1]
|
|
401
402
|
assert last.map is not None
|
|
402
403
|
|
|
404
|
+
# Lists are a special case: markdown-it-py treats the whole list as one
|
|
405
|
+
# top-level block, which would keep the entire list in the live area
|
|
406
|
+
# until the list ends.
|
|
407
|
+
#
|
|
408
|
+
# For streaming UX, we want all but the final list item to be eligible
|
|
409
|
+
# for stabilization, leaving only the *last* item in the live area.
|
|
410
|
+
if last.type in {"bullet_list_open", "ordered_list_open"}:
|
|
411
|
+
stable_line = self._compute_list_item_boundary(tokens, last)
|
|
412
|
+
return max(stable_line, 0)
|
|
413
|
+
|
|
414
|
+
if len(top_level) < 2:
|
|
415
|
+
return 0
|
|
416
|
+
|
|
403
417
|
# When the buffer ends mid-line, markdown-it-py can temporarily classify
|
|
404
418
|
# some lines as a thematic break (hr). For example, a trailing "- --"
|
|
405
419
|
# parses as an hr, but appending a non-hr character ("- --0") turns it
|
|
@@ -416,6 +430,59 @@ class MarkdownStream:
|
|
|
416
430
|
start_line = last.map[0]
|
|
417
431
|
return max(start_line, 0)
|
|
418
432
|
|
|
433
|
+
def _compute_list_item_boundary(self, tokens: list[Token], list_open: Token) -> int:
|
|
434
|
+
"""Return the start line of the last list item in a list.
|
|
435
|
+
|
|
436
|
+
This allows stabilizing all list items except the final one.
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
if list_open.map is None:
|
|
440
|
+
return 0
|
|
441
|
+
|
|
442
|
+
list_start, list_end = list_open.map
|
|
443
|
+
item_level = list_open.level + 1
|
|
444
|
+
|
|
445
|
+
last_item_start: int | None = None
|
|
446
|
+
for token in tokens:
|
|
447
|
+
if token.type != "list_item_open":
|
|
448
|
+
continue
|
|
449
|
+
if token.level != item_level:
|
|
450
|
+
continue
|
|
451
|
+
if token.map is None:
|
|
452
|
+
continue
|
|
453
|
+
start_line = token.map[0]
|
|
454
|
+
if start_line < list_start or start_line >= list_end:
|
|
455
|
+
continue
|
|
456
|
+
last_item_start = start_line
|
|
457
|
+
|
|
458
|
+
return last_item_start if last_item_start is not None else list_start
|
|
459
|
+
|
|
460
|
+
def _stable_boundary_continues_list(self, text: str, stable_line: int, *, final: bool) -> bool:
|
|
461
|
+
"""Whether the stable/live split point is inside the final top-level list."""
|
|
462
|
+
|
|
463
|
+
if final:
|
|
464
|
+
return False
|
|
465
|
+
if stable_line <= 0:
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
tokens = self._parser.parse(text)
|
|
470
|
+
except Exception:
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
top_level: list[Token] = [token for token in tokens if token.level == 0 and token.map is not None]
|
|
474
|
+
if not top_level:
|
|
475
|
+
return False
|
|
476
|
+
|
|
477
|
+
last = top_level[-1]
|
|
478
|
+
if last.type not in {"bullet_list_open", "ordered_list_open"}:
|
|
479
|
+
return False
|
|
480
|
+
if last.map is None:
|
|
481
|
+
return False
|
|
482
|
+
|
|
483
|
+
list_start, list_end = last.map
|
|
484
|
+
return list_start < stable_line < list_end
|
|
485
|
+
|
|
419
486
|
def split_blocks(self, text: str, *, min_stable_line: int = 0, final: bool = False) -> tuple[str, str, int]:
|
|
420
487
|
"""Split full markdown into stable and live sources.
|
|
421
488
|
|
|
@@ -445,7 +512,14 @@ class MarkdownStream:
|
|
|
445
512
|
return "", text, 0
|
|
446
513
|
return stable_source, live_source, stable_line
|
|
447
514
|
|
|
448
|
-
def render_stable_ansi(
|
|
515
|
+
def render_stable_ansi(
|
|
516
|
+
self,
|
|
517
|
+
stable_source: str,
|
|
518
|
+
*,
|
|
519
|
+
has_live_suffix: bool,
|
|
520
|
+
final: bool,
|
|
521
|
+
continues_list: bool = False,
|
|
522
|
+
) -> tuple[str, list[str]]:
|
|
449
523
|
"""Render stable prefix to ANSI, preserving inter-block spacing.
|
|
450
524
|
|
|
451
525
|
Returns:
|
|
@@ -455,10 +529,14 @@ class MarkdownStream:
|
|
|
455
529
|
return "", []
|
|
456
530
|
|
|
457
531
|
render_source = stable_source
|
|
458
|
-
if not final and has_live_suffix:
|
|
532
|
+
if not final and has_live_suffix and not continues_list:
|
|
459
533
|
render_source = self._append_nonfinal_sentinel(stable_source)
|
|
460
534
|
|
|
461
535
|
lines, images = self._render_markdown_to_lines(render_source, apply_mark=True)
|
|
536
|
+
|
|
537
|
+
if continues_list:
|
|
538
|
+
while lines and not lines[-1].strip():
|
|
539
|
+
lines.pop()
|
|
462
540
|
return "".join(lines), images
|
|
463
541
|
|
|
464
542
|
def _append_nonfinal_sentinel(self, stable_source: str) -> str:
|
|
@@ -570,6 +648,8 @@ class MarkdownStream:
|
|
|
570
648
|
final=final,
|
|
571
649
|
)
|
|
572
650
|
|
|
651
|
+
continues_list = self._stable_boundary_continues_list(text, stable_line, final=final) and bool(live_source)
|
|
652
|
+
|
|
573
653
|
start = time.time()
|
|
574
654
|
|
|
575
655
|
stable_chunk_to_print: str | None = None
|
|
@@ -577,7 +657,10 @@ class MarkdownStream:
|
|
|
577
657
|
stable_changed = final or stable_line > self._stable_source_line_count
|
|
578
658
|
if stable_changed and stable_source:
|
|
579
659
|
stable_ansi, collected_images = self.render_stable_ansi(
|
|
580
|
-
stable_source,
|
|
660
|
+
stable_source,
|
|
661
|
+
has_live_suffix=bool(live_source),
|
|
662
|
+
final=final,
|
|
663
|
+
continues_list=continues_list,
|
|
581
664
|
)
|
|
582
665
|
stable_lines = stable_ansi.splitlines(keepends=True)
|
|
583
666
|
new_lines = stable_lines[len(self._stable_rendered_lines) :]
|
|
@@ -595,30 +678,42 @@ class MarkdownStream:
|
|
|
595
678
|
|
|
596
679
|
live_text_to_set: Text | None = None
|
|
597
680
|
if not final and MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._live_sink is not None:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
live_leading_blank = 0
|
|
610
|
-
for line in live_lines:
|
|
681
|
+
if continues_list and self._stable_rendered_lines:
|
|
682
|
+
full_lines, _ = self._render_markdown_to_lines(text, apply_mark=True)
|
|
683
|
+
skip = min(len(self._stable_rendered_lines), len(full_lines))
|
|
684
|
+
live_text_to_set = Text.from_ansi("".join(full_lines[skip:]))
|
|
685
|
+
else:
|
|
686
|
+
apply_mark = not self._stable_rendered_lines
|
|
687
|
+
live_lines, _ = self._render_markdown_to_lines(live_source, apply_mark=apply_mark)
|
|
688
|
+
|
|
689
|
+
if self._stable_rendered_lines:
|
|
690
|
+
stable_trailing_blank = 0
|
|
691
|
+
for line in reversed(self._stable_rendered_lines):
|
|
611
692
|
if line.strip():
|
|
612
693
|
break
|
|
613
|
-
|
|
694
|
+
stable_trailing_blank += 1
|
|
614
695
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
696
|
+
if stable_trailing_blank > 0:
|
|
697
|
+
live_leading_blank = 0
|
|
698
|
+
for line in live_lines:
|
|
699
|
+
if line.strip():
|
|
700
|
+
break
|
|
701
|
+
live_leading_blank += 1
|
|
618
702
|
|
|
619
|
-
|
|
703
|
+
drop = min(stable_trailing_blank, live_leading_blank)
|
|
704
|
+
if drop > 0:
|
|
705
|
+
live_lines = live_lines[drop:]
|
|
706
|
+
|
|
707
|
+
live_text_to_set = Text.from_ansi("".join(live_lines))
|
|
620
708
|
|
|
621
709
|
with self._synchronized_output():
|
|
710
|
+
# Update/clear live area first to avoid blank padding when stable block appears
|
|
711
|
+
if final:
|
|
712
|
+
if self._live_sink is not None:
|
|
713
|
+
self._live_sink(None)
|
|
714
|
+
elif live_text_to_set is not None and self._live_sink is not None:
|
|
715
|
+
self._live_sink(live_text_to_set)
|
|
716
|
+
|
|
622
717
|
if stable_chunk_to_print:
|
|
623
718
|
self.console.print(Text.from_ansi(stable_chunk_to_print), end="\n")
|
|
624
719
|
|
|
@@ -626,13 +721,5 @@ class MarkdownStream:
|
|
|
626
721
|
for img_path in new_images:
|
|
627
722
|
self._image_callback(img_path)
|
|
628
723
|
|
|
629
|
-
if final:
|
|
630
|
-
if self._live_sink is not None:
|
|
631
|
-
self._live_sink(None)
|
|
632
|
-
return
|
|
633
|
-
|
|
634
|
-
if live_text_to_set is not None and self._live_sink is not None:
|
|
635
|
-
self._live_sink(live_text_to_set)
|
|
636
|
-
|
|
637
724
|
elapsed = time.time() - start
|
|
638
725
|
self.min_delay = min(max(elapsed * 6, 1.0 / 30), 0.5)
|
|
@@ -277,7 +277,7 @@ def truncate_left(text: Text, max_cells: int, *, console: Console, ellipsis: str
|
|
|
277
277
|
if cell_len(text.plain) <= max_cells:
|
|
278
278
|
return text
|
|
279
279
|
|
|
280
|
-
ellipsis_cells = cell_len(ellipsis)
|
|
280
|
+
ellipsis_cells = cell_len(ellipsis) + 1 # +1 for trailing space
|
|
281
281
|
if max_cells <= ellipsis_cells:
|
|
282
282
|
# Not enough space to show any meaningful suffix.
|
|
283
283
|
clipped = Text(ellipsis, style=text.style)
|
|
@@ -307,7 +307,7 @@ def truncate_left(text: Text, max_cells: int, *, console: Console, ellipsis: str
|
|
|
307
307
|
except Exception:
|
|
308
308
|
ellipsis_style = suffix.style or text.style
|
|
309
309
|
|
|
310
|
-
return Text.assemble(Text(ellipsis, style=ellipsis_style), suffix)
|
|
310
|
+
return Text.assemble(Text(ellipsis + " ", style=ellipsis_style), suffix)
|
|
311
311
|
|
|
312
312
|
|
|
313
313
|
class ShimmerStatusText:
|
|
@@ -21,6 +21,7 @@ class Palette:
|
|
|
21
21
|
grey_green: str
|
|
22
22
|
purple: str
|
|
23
23
|
lavender: str
|
|
24
|
+
black: str
|
|
24
25
|
diff_add: str
|
|
25
26
|
diff_add_char: str
|
|
26
27
|
diff_remove: str
|
|
@@ -55,6 +56,7 @@ LIGHT_PALETTE = Palette(
|
|
|
55
56
|
grey_green="#96a096",
|
|
56
57
|
purple="#5f5fb7",
|
|
57
58
|
lavender="#7878b0",
|
|
59
|
+
black="#101827",
|
|
58
60
|
diff_add="#2e5a32 on #dafbe1",
|
|
59
61
|
diff_add_char="#2e5a32 on #aceebb",
|
|
60
62
|
diff_remove="#82071e on #ffecec",
|
|
@@ -88,6 +90,7 @@ DARK_PALETTE = Palette(
|
|
|
88
90
|
grey_green="#6d8672",
|
|
89
91
|
purple="#afbafe",
|
|
90
92
|
lavender="#9898b8",
|
|
93
|
+
black="white",
|
|
91
94
|
diff_add="#c8e6c9 on #1b3928",
|
|
92
95
|
diff_add_char="#c8e6c9 on #2d6b42",
|
|
93
96
|
diff_remove="#ffcdd2 on #3d1f23",
|
|
@@ -301,9 +304,9 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
301
304
|
ThemeKey.BASH_HEREDOC_DELIMITER.value: "bold " + palette.grey1,
|
|
302
305
|
# THINKING
|
|
303
306
|
ThemeKey.THINKING.value: "italic " + palette.grey2,
|
|
304
|
-
ThemeKey.THINKING_BOLD.value: "
|
|
307
|
+
ThemeKey.THINKING_BOLD.value: "italic " + palette.grey1,
|
|
305
308
|
# COMPACTION
|
|
306
|
-
ThemeKey.COMPACTION_SUMMARY.value:
|
|
309
|
+
ThemeKey.COMPACTION_SUMMARY.value: palette.grey1,
|
|
307
310
|
# TODO_ITEM
|
|
308
311
|
ThemeKey.TODO_EXPLANATION.value: palette.grey1 + " italic",
|
|
309
312
|
ThemeKey.TODO_PENDING_MARK.value: "bold " + palette.grey1,
|
|
@@ -347,9 +350,9 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
347
350
|
"markdown.code.border": palette.grey3,
|
|
348
351
|
# Used by ThinkingMarkdown when rendering `<thinking>` blocks.
|
|
349
352
|
"markdown.code.block": palette.grey1,
|
|
350
|
-
"markdown.h1": "bold reverse",
|
|
353
|
+
"markdown.h1": "bold reverse " + palette.black,
|
|
351
354
|
"markdown.h1.border": palette.grey3,
|
|
352
|
-
"markdown.h2": "bold underline",
|
|
355
|
+
"markdown.h2": "bold underline " + palette.black,
|
|
353
356
|
"markdown.h3": "bold " + palette.grey1,
|
|
354
357
|
"markdown.h4": "bold " + palette.grey2,
|
|
355
358
|
"markdown.hr": palette.grey3,
|
|
@@ -365,7 +368,8 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
365
368
|
styles={
|
|
366
369
|
# THINKING (used for left-side mark in thinking output)
|
|
367
370
|
ThemeKey.THINKING.value: "italic " + palette.grey2,
|
|
368
|
-
ThemeKey.THINKING_BOLD.value: "
|
|
371
|
+
ThemeKey.THINKING_BOLD.value: "italic " + palette.grey1,
|
|
372
|
+
"markdown.strong": "italic " + palette.grey1,
|
|
369
373
|
"markdown.code": palette.grey1 + " italic on " + palette.code_background,
|
|
370
374
|
"markdown.code.block": palette.grey1,
|
|
371
375
|
"markdown.code.border": palette.grey3,
|
|
@@ -379,7 +383,6 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
379
383
|
"markdown.item.number": palette.grey2,
|
|
380
384
|
"markdown.link": "underline " + palette.blue,
|
|
381
385
|
"markdown.link_url": "underline " + palette.blue,
|
|
382
|
-
"markdown.strong": "bold italic " + palette.grey1,
|
|
383
386
|
"markdown.table.border": palette.grey2,
|
|
384
387
|
"markdown.checkbox.checked": palette.green,
|
|
385
388
|
}
|
|
@@ -10,8 +10,10 @@ from rich.text import Text
|
|
|
10
10
|
|
|
11
11
|
from klaude_code.const import (
|
|
12
12
|
BASH_OUTPUT_PANEL_THRESHOLD,
|
|
13
|
+
DIFF_PREFIX_WIDTH,
|
|
13
14
|
INVALID_TOOL_CALL_MAX_LENGTH,
|
|
14
15
|
QUERY_DISPLAY_TRUNCATE_LENGTH,
|
|
16
|
+
TAB_EXPAND_WIDTH,
|
|
15
17
|
URL_TRUNCATE_MAX_LENGTH,
|
|
16
18
|
WEB_SEARCH_DEFAULT_MAX_RESULTS,
|
|
17
19
|
)
|
|
@@ -387,6 +389,24 @@ def render_generic_tool_result(result: str, *, is_error: bool = False) -> Render
|
|
|
387
389
|
return text
|
|
388
390
|
|
|
389
391
|
|
|
392
|
+
def render_read_preview(ui_extra: model.ReadPreviewUIExtra) -> RenderableType:
|
|
393
|
+
"""Render read preview with line numbers aligned to diff style."""
|
|
394
|
+
grid = create_grid()
|
|
395
|
+
grid.padding = (0, 0)
|
|
396
|
+
|
|
397
|
+
for line in ui_extra.lines:
|
|
398
|
+
prefix = f"{line.line_no:>{DIFF_PREFIX_WIDTH}} "
|
|
399
|
+
content = line.content.expandtabs(TAB_EXPAND_WIDTH)
|
|
400
|
+
grid.add_row(Text(prefix, ThemeKey.TOOL_RESULT), Text(content, ThemeKey.TOOL_RESULT))
|
|
401
|
+
|
|
402
|
+
if ui_extra.remaining_lines > 0:
|
|
403
|
+
remaining_prefix = f"{'⋮':>{DIFF_PREFIX_WIDTH}} "
|
|
404
|
+
remaining_text = Text(f"(more {ui_extra.remaining_lines} lines)", ThemeKey.TOOL_RESULT_TRUNCATED)
|
|
405
|
+
grid.add_row(Text(remaining_prefix, ThemeKey.TOOL_RESULT_TRUNCATED), remaining_text)
|
|
406
|
+
|
|
407
|
+
return grid
|
|
408
|
+
|
|
409
|
+
|
|
390
410
|
def _extract_mermaid_link(
|
|
391
411
|
ui_extra: model.ToolResultUIExtra | None,
|
|
392
412
|
) -> model.MermaidLinkUIExtra | None:
|
|
@@ -678,6 +698,8 @@ def render_tool_result(
|
|
|
678
698
|
|
|
679
699
|
match e.tool_name:
|
|
680
700
|
case tools.READ:
|
|
701
|
+
if isinstance(e.ui_extra, model.ReadPreviewUIExtra):
|
|
702
|
+
return wrap(render_read_preview(e.ui_extra))
|
|
681
703
|
return None
|
|
682
704
|
case tools.EDIT:
|
|
683
705
|
return wrap(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""))
|
|
@@ -3,6 +3,7 @@ import re
|
|
|
3
3
|
from rich.console import Group, RenderableType
|
|
4
4
|
from rich.text import Text
|
|
5
5
|
|
|
6
|
+
from klaude_code.const import TAB_EXPAND_WIDTH
|
|
6
7
|
from klaude_code.skill import get_available_skills
|
|
7
8
|
from klaude_code.tui.components.common import create_grid
|
|
8
9
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
@@ -82,6 +83,7 @@ def render_user_input(content: str) -> RenderableType:
|
|
|
82
83
|
lines = content.strip().split("\n")
|
|
83
84
|
renderables: list[RenderableType] = []
|
|
84
85
|
for i, line in enumerate(lines):
|
|
86
|
+
line = line.expandtabs(TAB_EXPAND_WIDTH)
|
|
85
87
|
# Handle slash command on first line
|
|
86
88
|
if i == 0 and line.startswith("/"):
|
|
87
89
|
splits = line.split(" ", maxsplit=1)
|
klaude_code/tui/machine.py
CHANGED
|
@@ -32,7 +32,6 @@ from klaude_code.tui.commands import (
|
|
|
32
32
|
RenderTaskFinish,
|
|
33
33
|
RenderTaskMetadata,
|
|
34
34
|
RenderTaskStart,
|
|
35
|
-
RenderThinkingHeader,
|
|
36
35
|
RenderToolCall,
|
|
37
36
|
RenderToolResult,
|
|
38
37
|
RenderTurnStart,
|
|
@@ -68,25 +67,6 @@ FAST_TOOLS: frozenset[str] = frozenset(
|
|
|
68
67
|
)
|
|
69
68
|
|
|
70
69
|
|
|
71
|
-
@dataclass
|
|
72
|
-
class SubAgentThinkingHeaderState:
|
|
73
|
-
buffer: str = ""
|
|
74
|
-
last_header: str | None = None
|
|
75
|
-
|
|
76
|
-
def append_and_extract_new_header(self, content: str) -> str | None:
|
|
77
|
-
self.buffer += content
|
|
78
|
-
|
|
79
|
-
max_chars = 8192
|
|
80
|
-
if len(self.buffer) > max_chars:
|
|
81
|
-
self.buffer = self.buffer[-max_chars:]
|
|
82
|
-
|
|
83
|
-
header = extract_last_bold_header(normalize_thinking_content(self.buffer))
|
|
84
|
-
if header and header != self.last_header:
|
|
85
|
-
self.last_header = header
|
|
86
|
-
return header
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
|
|
90
70
|
class ActivityState:
|
|
91
71
|
"""Tracks composing/tool activity for spinner display."""
|
|
92
72
|
|
|
@@ -110,7 +90,10 @@ class ActivityState:
|
|
|
110
90
|
|
|
111
91
|
def add_sub_agent_tool_call(self, tool_call_id: str, tool_name: str) -> None:
|
|
112
92
|
if tool_call_id in self._sub_agent_tool_calls_by_id:
|
|
113
|
-
|
|
93
|
+
old_tool_name = self._sub_agent_tool_calls_by_id[tool_call_id]
|
|
94
|
+
self._sub_agent_tool_calls[old_tool_name] = self._sub_agent_tool_calls.get(old_tool_name, 0) - 1
|
|
95
|
+
if self._sub_agent_tool_calls[old_tool_name] <= 0:
|
|
96
|
+
self._sub_agent_tool_calls.pop(old_tool_name, None)
|
|
114
97
|
self._sub_agent_tool_calls_by_id[tool_call_id] = tool_name
|
|
115
98
|
self._sub_agent_tool_calls[tool_name] = self._sub_agent_tool_calls.get(tool_name, 0) + 1
|
|
116
99
|
|
|
@@ -303,7 +286,6 @@ class SpinnerStatusState:
|
|
|
303
286
|
class _SessionState:
|
|
304
287
|
session_id: str
|
|
305
288
|
sub_agent_state: model.SubAgentState | None = None
|
|
306
|
-
sub_agent_thinking_header: SubAgentThinkingHeaderState | None = None
|
|
307
289
|
model_id: str | None = None
|
|
308
290
|
assistant_stream_active: bool = False
|
|
309
291
|
thinking_stream_active: bool = False
|
|
@@ -418,8 +400,6 @@ class DisplayStateMachine:
|
|
|
418
400
|
self._set_primary_if_needed(e.session_id)
|
|
419
401
|
if not is_replay:
|
|
420
402
|
cmds.append(TaskClockStart())
|
|
421
|
-
else:
|
|
422
|
-
s.sub_agent_thinking_header = SubAgentThinkingHeaderState()
|
|
423
403
|
|
|
424
404
|
if not is_replay:
|
|
425
405
|
cmds.append(SpinnerStart())
|
|
@@ -465,7 +445,11 @@ class DisplayStateMachine:
|
|
|
465
445
|
|
|
466
446
|
case events.ThinkingStartEvent() as e:
|
|
467
447
|
if s.is_sub_agent:
|
|
468
|
-
|
|
448
|
+
if not s.should_show_sub_agent_thinking_header:
|
|
449
|
+
return []
|
|
450
|
+
s.thinking_stream_active = True
|
|
451
|
+
cmds.append(StartThinkingStream(session_id=e.session_id))
|
|
452
|
+
return cmds
|
|
469
453
|
if not self._is_primary(e.session_id):
|
|
470
454
|
return []
|
|
471
455
|
s.thinking_stream_active = True
|
|
@@ -483,11 +467,7 @@ class DisplayStateMachine:
|
|
|
483
467
|
if s.is_sub_agent:
|
|
484
468
|
if not s.should_show_sub_agent_thinking_header:
|
|
485
469
|
return []
|
|
486
|
-
|
|
487
|
-
s.sub_agent_thinking_header = SubAgentThinkingHeaderState()
|
|
488
|
-
header = s.sub_agent_thinking_header.append_and_extract_new_header(e.content)
|
|
489
|
-
if header:
|
|
490
|
-
cmds.append(RenderThinkingHeader(session_id=e.session_id, header=header))
|
|
470
|
+
cmds.append(AppendThinking(session_id=e.session_id, content=e.content))
|
|
491
471
|
return cmds
|
|
492
472
|
|
|
493
473
|
if not self._is_primary(e.session_id):
|
|
@@ -507,7 +487,11 @@ class DisplayStateMachine:
|
|
|
507
487
|
|
|
508
488
|
case events.ThinkingEndEvent() as e:
|
|
509
489
|
if s.is_sub_agent:
|
|
510
|
-
|
|
490
|
+
if not s.should_show_sub_agent_thinking_header:
|
|
491
|
+
return []
|
|
492
|
+
s.thinking_stream_active = False
|
|
493
|
+
cmds.append(EndThinkingStream(session_id=e.session_id))
|
|
494
|
+
return cmds
|
|
511
495
|
if not self._is_primary(e.session_id):
|
|
512
496
|
return []
|
|
513
497
|
s.thinking_stream_active = False
|
|
@@ -603,14 +587,11 @@ class DisplayStateMachine:
|
|
|
603
587
|
# Skip activity state for fast tools on non-streaming models (e.g., Gemini)
|
|
604
588
|
# to avoid flash-and-disappear effect
|
|
605
589
|
if not is_replay and not s.should_skip_tool_activity(e.tool_name):
|
|
606
|
-
|
|
607
|
-
|
|
590
|
+
tool_active_form = get_tool_active_form(e.tool_name)
|
|
591
|
+
if is_sub_agent_tool(e.tool_name):
|
|
592
|
+
self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
|
|
608
593
|
else:
|
|
609
|
-
tool_active_form
|
|
610
|
-
if is_sub_agent_tool(e.tool_name):
|
|
611
|
-
self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
|
|
612
|
-
else:
|
|
613
|
-
self._spinner.add_tool_call(tool_active_form)
|
|
594
|
+
self._spinner.add_tool_call(tool_active_form)
|
|
614
595
|
|
|
615
596
|
if not is_replay:
|
|
616
597
|
cmds.extend(self._spinner_update_commands())
|
|
@@ -679,15 +660,12 @@ class DisplayStateMachine:
|
|
|
679
660
|
case events.TaskFinishEvent() as e:
|
|
680
661
|
s.task_active = False
|
|
681
662
|
cmds.append(RenderTaskFinish(e))
|
|
682
|
-
if not s.is_sub_agent:
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
cmds.append(EmitTmuxSignal())
|
|
689
|
-
else:
|
|
690
|
-
s.sub_agent_thinking_header = None
|
|
663
|
+
if not s.is_sub_agent and not is_replay:
|
|
664
|
+
cmds.append(TaskClockClear())
|
|
665
|
+
self._spinner.reset()
|
|
666
|
+
cmds.append(SpinnerStop())
|
|
667
|
+
cmds.append(PrintRuleLine())
|
|
668
|
+
cmds.append(EmitTmuxSignal())
|
|
691
669
|
return cmds
|
|
692
670
|
|
|
693
671
|
case events.InterruptEvent() as e:
|
klaude_code/tui/renderer.py
CHANGED
|
@@ -67,7 +67,7 @@ from klaude_code.tui.components import thinking as c_thinking
|
|
|
67
67
|
from klaude_code.tui.components import tools as c_tools
|
|
68
68
|
from klaude_code.tui.components import user_input as c_user_input
|
|
69
69
|
from klaude_code.tui.components import welcome as c_welcome
|
|
70
|
-
from klaude_code.tui.components.common import truncate_head
|
|
70
|
+
from klaude_code.tui.components.common import create_grid, truncate_head
|
|
71
71
|
from klaude_code.tui.components.rich import status as r_status
|
|
72
72
|
from klaude_code.tui.components.rich.live import CropAboveLive, SingleLine
|
|
73
73
|
from klaude_code.tui.components.rich.markdown import MarkdownStream, NoInsetMarkdown, ThinkingMarkdown
|
|
@@ -168,6 +168,7 @@ class TUICommandRenderer:
|
|
|
168
168
|
self._sessions: dict[str, _SessionStatus] = {}
|
|
169
169
|
self._current_sub_agent_color: Style | None = None
|
|
170
170
|
self._sub_agent_color_index = 0
|
|
171
|
+
self._sub_agent_thinking_buffers: dict[str, str] = {}
|
|
171
172
|
|
|
172
173
|
# ---------------------------------------------------------------------
|
|
173
174
|
# Session helpers
|
|
@@ -325,8 +326,7 @@ class TUICommandRenderer:
|
|
|
325
326
|
if pad_lines:
|
|
326
327
|
stream = Padding(stream, (0, 0, pad_lines, 0))
|
|
327
328
|
stream_part = stream
|
|
328
|
-
|
|
329
|
-
gap_part = Text("") if self._spinner_visible else Group()
|
|
329
|
+
gap_part = Text("")
|
|
330
330
|
|
|
331
331
|
status_part: RenderableType = SingleLine(self._status_spinner) if self._spinner_visible else Group()
|
|
332
332
|
return Group(stream_part, gap_part, status_part)
|
|
@@ -381,6 +381,19 @@ class TUICommandRenderer:
|
|
|
381
381
|
def _flush_assistant(self) -> None:
|
|
382
382
|
self._assistant_stream.render()
|
|
383
383
|
|
|
384
|
+
def _render_sub_agent_thinking(self, content: str) -> None:
|
|
385
|
+
"""Render sub-agent thinking content as a single block."""
|
|
386
|
+
normalized = c_thinking.normalize_thinking_content(content)
|
|
387
|
+
if not normalized.strip():
|
|
388
|
+
return
|
|
389
|
+
md = ThinkingMarkdown(normalized, code_theme=self.themes.code_theme, style=ThemeKey.THINKING)
|
|
390
|
+
self.console.push_theme(self.themes.thinking_markdown_theme)
|
|
391
|
+
grid = create_grid()
|
|
392
|
+
grid.add_row(Text(c_thinking.THINKING_MESSAGE_MARK, style=ThemeKey.THINKING), md)
|
|
393
|
+
self.print(grid)
|
|
394
|
+
self.console.pop_theme()
|
|
395
|
+
self.print()
|
|
396
|
+
|
|
384
397
|
# ---------------------------------------------------------------------
|
|
385
398
|
# Event-specific rendering helpers
|
|
386
399
|
# ---------------------------------------------------------------------
|
|
@@ -534,9 +547,9 @@ class TUICommandRenderer:
|
|
|
534
547
|
)
|
|
535
548
|
self.console.print(
|
|
536
549
|
Rule(
|
|
537
|
-
Text("Context
|
|
550
|
+
Text("Context Compacted", style=ThemeKey.COMPACTION_SUMMARY),
|
|
538
551
|
characters="=",
|
|
539
|
-
style=ThemeKey.
|
|
552
|
+
style=ThemeKey.LINES,
|
|
540
553
|
)
|
|
541
554
|
)
|
|
542
555
|
self.print()
|
|
@@ -622,20 +635,31 @@ class TUICommandRenderer:
|
|
|
622
635
|
self.display_command_output(event)
|
|
623
636
|
case RenderTurnStart(event=event):
|
|
624
637
|
self.display_turn_start(event)
|
|
625
|
-
case StartThinkingStream():
|
|
626
|
-
if
|
|
638
|
+
case StartThinkingStream(session_id=session_id):
|
|
639
|
+
if self.is_sub_agent_session(session_id):
|
|
640
|
+
self._sub_agent_thinking_buffers[session_id] = ""
|
|
641
|
+
elif not self._thinking_stream.is_active:
|
|
627
642
|
self._thinking_stream.start(self._new_thinking_mdstream())
|
|
628
|
-
case AppendThinking(content=content):
|
|
629
|
-
if self.
|
|
643
|
+
case AppendThinking(session_id=session_id, content=content):
|
|
644
|
+
if self.is_sub_agent_session(session_id):
|
|
645
|
+
if session_id in self._sub_agent_thinking_buffers:
|
|
646
|
+
self._sub_agent_thinking_buffers[session_id] += content
|
|
647
|
+
elif self._thinking_stream.is_active:
|
|
630
648
|
first_delta = self._thinking_stream.buffer == ""
|
|
631
649
|
self._thinking_stream.append(content)
|
|
632
650
|
if first_delta:
|
|
633
651
|
self._thinking_stream.render(transform=c_thinking.normalize_thinking_content)
|
|
634
652
|
self._flush_thinking()
|
|
635
|
-
case EndThinkingStream():
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
653
|
+
case EndThinkingStream(session_id=session_id):
|
|
654
|
+
if self.is_sub_agent_session(session_id):
|
|
655
|
+
buf = self._sub_agent_thinking_buffers.pop(session_id, "")
|
|
656
|
+
if buf.strip():
|
|
657
|
+
with self.session_print_context(session_id):
|
|
658
|
+
self._render_sub_agent_thinking(buf)
|
|
659
|
+
else:
|
|
660
|
+
finalized = self._thinking_stream.finalize(transform=c_thinking.normalize_thinking_content)
|
|
661
|
+
if finalized:
|
|
662
|
+
self.print()
|
|
639
663
|
case StartAssistantStream():
|
|
640
664
|
if not self._assistant_stream.is_active:
|
|
641
665
|
self._assistant_stream.start(self._new_assistant_mdstream())
|