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
klaude_code/session/export.py
CHANGED
|
@@ -364,6 +364,228 @@ def _render_thinking_block(text: str) -> str:
|
|
|
364
364
|
return f'<div class="thinking-block markdown-body markdown-content" data-raw="{encoded}"></div>'
|
|
365
365
|
|
|
366
366
|
|
|
367
|
+
class _TurnGroup:
|
|
368
|
+
def __init__(self) -> None:
|
|
369
|
+
self.user_message: message.UserMessage | None = None
|
|
370
|
+
self.body_items: list[message.HistoryEvent] = []
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _group_messages_by_turn(history: list[message.HistoryEvent]) -> list[_TurnGroup]:
|
|
374
|
+
groups: list[_TurnGroup] = []
|
|
375
|
+
current_group = _TurnGroup()
|
|
376
|
+
|
|
377
|
+
# Filter for renderable items only
|
|
378
|
+
renderable_items = [item for item in history if not isinstance(item, message.ToolResultMessage)]
|
|
379
|
+
|
|
380
|
+
for item in renderable_items:
|
|
381
|
+
if isinstance(item, message.UserMessage):
|
|
382
|
+
# If current group has content, save it and start new
|
|
383
|
+
if current_group.user_message or current_group.body_items:
|
|
384
|
+
groups.append(current_group)
|
|
385
|
+
current_group = _TurnGroup()
|
|
386
|
+
current_group.user_message = item
|
|
387
|
+
else:
|
|
388
|
+
current_group.body_items.append(item)
|
|
389
|
+
|
|
390
|
+
# Append the last group if it has content
|
|
391
|
+
if current_group.user_message or current_group.body_items:
|
|
392
|
+
groups.append(current_group)
|
|
393
|
+
|
|
394
|
+
return groups
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _render_event_item(
|
|
398
|
+
item: message.HistoryEvent,
|
|
399
|
+
tool_results: dict[str, message.ToolResultMessage],
|
|
400
|
+
seen_session_ids: set[str],
|
|
401
|
+
nesting_level: int,
|
|
402
|
+
assistant_counter: list[int],
|
|
403
|
+
) -> str:
|
|
404
|
+
blocks: list[str] = []
|
|
405
|
+
|
|
406
|
+
if isinstance(item, message.UserMessage):
|
|
407
|
+
text = message.join_text_parts(item.parts)
|
|
408
|
+
images = _extract_image_parts(item.parts)
|
|
409
|
+
images_html = _render_image_parts(images)
|
|
410
|
+
ts_str = _format_msg_timestamp(item.created_at)
|
|
411
|
+
body_parts: list[str] = []
|
|
412
|
+
if images_html:
|
|
413
|
+
body_parts.append(images_html)
|
|
414
|
+
if text:
|
|
415
|
+
body_parts.append(f'<div style="white-space: pre-wrap;">{_escape_html(text)}</div>')
|
|
416
|
+
if not body_parts:
|
|
417
|
+
body_parts.append('<div style="color: var(--text-dim); font-style: italic;">(empty)</div>')
|
|
418
|
+
blocks.append(
|
|
419
|
+
f'<div class="message-group">'
|
|
420
|
+
f'<div class="role-label user">'
|
|
421
|
+
f"User"
|
|
422
|
+
f'<span class="timestamp">{ts_str}</span>'
|
|
423
|
+
f"</div>"
|
|
424
|
+
f'<div class="message-content user">{"".join(body_parts)}</div>'
|
|
425
|
+
f"</div>"
|
|
426
|
+
)
|
|
427
|
+
elif isinstance(item, message.AssistantMessage):
|
|
428
|
+
assistant_counter[0] += 1
|
|
429
|
+
thinking_text = "".join(part.text for part in item.parts if isinstance(part, message.ThinkingTextPart))
|
|
430
|
+
if thinking_text:
|
|
431
|
+
blocks.append(_render_thinking_block(thinking_text))
|
|
432
|
+
|
|
433
|
+
assistant_text = message.join_text_parts(item.parts)
|
|
434
|
+
assistant_images = _extract_image_parts(item.parts)
|
|
435
|
+
if assistant_text or assistant_images:
|
|
436
|
+
blocks.append(
|
|
437
|
+
_render_assistant_message(
|
|
438
|
+
assistant_counter[0],
|
|
439
|
+
assistant_text,
|
|
440
|
+
item.created_at,
|
|
441
|
+
assistant_images,
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
for part in item.parts:
|
|
446
|
+
if isinstance(part, message.ToolCallPart):
|
|
447
|
+
result = tool_results.get(part.call_id)
|
|
448
|
+
blocks.append(_format_tool_call(part, result, item.created_at))
|
|
449
|
+
if result is not None:
|
|
450
|
+
sub_agent_html = _render_sub_agent_session(result, seen_session_ids, nesting_level)
|
|
451
|
+
if sub_agent_html:
|
|
452
|
+
blocks.append(sub_agent_html)
|
|
453
|
+
elif isinstance(item, model.TaskMetadataItem):
|
|
454
|
+
blocks.append(_render_metadata_item(item))
|
|
455
|
+
elif isinstance(item, message.DeveloperMessage):
|
|
456
|
+
content = message.join_text_parts(item.parts)
|
|
457
|
+
images = _extract_image_parts(item.parts)
|
|
458
|
+
images_html = _render_image_parts(images)
|
|
459
|
+
ts_str = _format_msg_timestamp(item.created_at)
|
|
460
|
+
|
|
461
|
+
detail_body = ""
|
|
462
|
+
if images_html:
|
|
463
|
+
detail_body += images_html
|
|
464
|
+
if content:
|
|
465
|
+
detail_body += f'<div style="white-space: pre-wrap;">{_escape_html(content)}</div>'
|
|
466
|
+
if not detail_body:
|
|
467
|
+
detail_body = '<div style="color: var(--text-dim); font-style: italic;">(empty)</div>'
|
|
468
|
+
|
|
469
|
+
blocks.append(
|
|
470
|
+
f'<details class="developer-message gap-below">'
|
|
471
|
+
f"<summary>"
|
|
472
|
+
f"Developer"
|
|
473
|
+
f'<span class="timestamp">{ts_str}</span>'
|
|
474
|
+
f"</summary>"
|
|
475
|
+
f'<div class="details-content">{detail_body}</div>'
|
|
476
|
+
f"</details>"
|
|
477
|
+
)
|
|
478
|
+
elif isinstance(item, message.SystemMessage):
|
|
479
|
+
content = message.join_text_parts(item.parts)
|
|
480
|
+
if content:
|
|
481
|
+
ts_str = _format_msg_timestamp(item.created_at)
|
|
482
|
+
blocks.append(
|
|
483
|
+
f'<details class="developer-message">'
|
|
484
|
+
f"<summary>"
|
|
485
|
+
f"System"
|
|
486
|
+
f'<span class="timestamp">{ts_str}</span>'
|
|
487
|
+
f"</summary>"
|
|
488
|
+
f'<div class="details-content" style="white-space: pre-wrap;">{_escape_html(content)}</div>'
|
|
489
|
+
f"</details>"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
return "".join(blocks)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _has_non_mermaid_tool(msg: message.AssistantMessage) -> bool:
|
|
496
|
+
has_non_mermaid = False
|
|
497
|
+
for part in msg.parts:
|
|
498
|
+
if isinstance(part, message.ToolCallPart):
|
|
499
|
+
if part.tool_name != "Mermaid":
|
|
500
|
+
has_non_mermaid = True
|
|
501
|
+
else:
|
|
502
|
+
# If it has Mermaid, we treat it as visible, overriding the non-mermaid flag
|
|
503
|
+
# for the purpose of finding the BARRIER.
|
|
504
|
+
# Logic: We want to find the LAST item that is STRICTLY non-mermaid/intermediate.
|
|
505
|
+
# If an item has Mermaid, it belongs to the visible chain.
|
|
506
|
+
return False
|
|
507
|
+
return has_non_mermaid
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _build_messages_html(
|
|
511
|
+
history: list[message.HistoryEvent],
|
|
512
|
+
tool_results: dict[str, message.ToolResultMessage],
|
|
513
|
+
*,
|
|
514
|
+
seen_session_ids: set[str] | None = None,
|
|
515
|
+
nesting_level: int = 0,
|
|
516
|
+
) -> str:
|
|
517
|
+
if seen_session_ids is None:
|
|
518
|
+
seen_session_ids = set()
|
|
519
|
+
|
|
520
|
+
blocks: list[str] = []
|
|
521
|
+
assistant_counter = [0] # Use list for mutable reference
|
|
522
|
+
|
|
523
|
+
turns = _group_messages_by_turn(history)
|
|
524
|
+
|
|
525
|
+
for turn in turns:
|
|
526
|
+
# 1. Render User Message
|
|
527
|
+
if turn.user_message:
|
|
528
|
+
blocks.append(
|
|
529
|
+
_render_event_item(turn.user_message, tool_results, seen_session_ids, nesting_level, assistant_counter)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
if not turn.body_items:
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
# 2. Identify split point (barrier)
|
|
536
|
+
# Find the LAST AssistantMessage that has a non-Mermaid tool call (and NO Mermaid tool call).
|
|
537
|
+
barrier_index = -1
|
|
538
|
+
for i, item in enumerate(turn.body_items):
|
|
539
|
+
if isinstance(item, message.AssistantMessage) and _has_non_mermaid_tool(item):
|
|
540
|
+
barrier_index = i
|
|
541
|
+
|
|
542
|
+
# If barrier found, everything up to it is collapsible.
|
|
543
|
+
# Everything after is visible.
|
|
544
|
+
# If no barrier (-1), checks depend on if we have any AssistantMessages
|
|
545
|
+
collapsible_items = []
|
|
546
|
+
visible_items = []
|
|
547
|
+
|
|
548
|
+
if barrier_index != -1:
|
|
549
|
+
collapsible_items = turn.body_items[: barrier_index + 1]
|
|
550
|
+
visible_items = turn.body_items[barrier_index + 1 :]
|
|
551
|
+
else:
|
|
552
|
+
# No barrier found (no non-Mermaid tools).
|
|
553
|
+
# If purely conversational, all visible?
|
|
554
|
+
# Or should we fold intermediate chat steps?
|
|
555
|
+
# Current logic: If no tools used, assume chat mode -> All visible.
|
|
556
|
+
collapsible_items = []
|
|
557
|
+
visible_items = turn.body_items
|
|
558
|
+
|
|
559
|
+
# 3. Render Collapsible Items
|
|
560
|
+
if collapsible_items:
|
|
561
|
+
# Count steps (assistant messages + tool calls approx)
|
|
562
|
+
step_count = sum(1 for item in collapsible_items if isinstance(item, message.AssistantMessage))
|
|
563
|
+
step_label = f"{step_count} steps" if step_count != 1 else "1 step"
|
|
564
|
+
|
|
565
|
+
collapsed_html = "".join(
|
|
566
|
+
_render_event_item(item, tool_results, seen_session_ids, nesting_level, assistant_counter)
|
|
567
|
+
for item in collapsible_items
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
blocks.append(
|
|
571
|
+
f'<div class="turn-collapsible">'
|
|
572
|
+
f'<button class="turn-collapse-btn" title="Show/hide intermediate steps">'
|
|
573
|
+
f'<span class="collapse-icon">[+]</span>'
|
|
574
|
+
f'<span class="collapse-count">{step_label}</span>'
|
|
575
|
+
f"</button>"
|
|
576
|
+
f'<div class="turn-steps" style="display: none;">'
|
|
577
|
+
f"{collapsed_html}"
|
|
578
|
+
f"</div>"
|
|
579
|
+
f"</div>"
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# 4. Render Visible Items
|
|
583
|
+
for item in visible_items:
|
|
584
|
+
blocks.append(_render_event_item(item, tool_results, seen_session_ids, nesting_level, assistant_counter))
|
|
585
|
+
|
|
586
|
+
return "\n".join(blocks)
|
|
587
|
+
|
|
588
|
+
|
|
367
589
|
def _try_render_todo_args(arguments: str, tool_name: str) -> str | None:
|
|
368
590
|
try:
|
|
369
591
|
parsed = json.loads(arguments)
|
|
@@ -460,6 +682,31 @@ def _render_sub_agent_result(content: str, description: str | None = None) -> st
|
|
|
460
682
|
if images_html and not formatted.strip():
|
|
461
683
|
return f'<div class="sub-agent-result-container">{images_html}</div>'
|
|
462
684
|
|
|
685
|
+
# Check if content needs collapsing (approx > 20 lines or > 2000 chars)
|
|
686
|
+
# Using a simpler metric since we don't know rendered height
|
|
687
|
+
needs_collapse = formatted.count("\n") > 20 or len(formatted) > 2000
|
|
688
|
+
|
|
689
|
+
rendered_html = (
|
|
690
|
+
f'<div class="sub-agent-rendered markdown-content markdown-body" data-raw="{encoded}">'
|
|
691
|
+
f'<noscript><pre style="white-space: pre-wrap;">{encoded}</pre></noscript>'
|
|
692
|
+
f"</div>"
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
if needs_collapse:
|
|
696
|
+
content_html = (
|
|
697
|
+
f'<div class="expandable-markdown">'
|
|
698
|
+
f'<div class="markdown-preview">'
|
|
699
|
+
f"{rendered_html}"
|
|
700
|
+
f"</div>"
|
|
701
|
+
f'<div class="expand-control">'
|
|
702
|
+
f'<button class="expand-btn">Show full output</button>'
|
|
703
|
+
f"</div>"
|
|
704
|
+
f"</div>"
|
|
705
|
+
)
|
|
706
|
+
else:
|
|
707
|
+
# No collapse wrapper needed, but we keep the structure compatible
|
|
708
|
+
content_html = rendered_html
|
|
709
|
+
|
|
463
710
|
return (
|
|
464
711
|
f'<div class="sub-agent-result-container">'
|
|
465
712
|
f"{images_html}"
|
|
@@ -468,9 +715,7 @@ def _render_sub_agent_result(content: str, description: str | None = None) -> st
|
|
|
468
715
|
f'<button type="button" class="copy-raw-btn" title="Copy raw content">Copy</button>'
|
|
469
716
|
f"</div>"
|
|
470
717
|
f'<div class="sub-agent-content">'
|
|
471
|
-
f
|
|
472
|
-
f'<noscript><pre style="white-space: pre-wrap;">{encoded}</pre></noscript>'
|
|
473
|
-
f"</div>"
|
|
718
|
+
f"{content_html}"
|
|
474
719
|
f'<pre class="sub-agent-raw">{encoded}</pre>'
|
|
475
720
|
f"</div>"
|
|
476
721
|
f"</div>"
|
|
@@ -479,21 +724,20 @@ def _render_sub_agent_result(content: str, description: str | None = None) -> st
|
|
|
479
724
|
|
|
480
725
|
def _render_text_block(text: str) -> str:
|
|
481
726
|
lines = text.splitlines()
|
|
482
|
-
|
|
727
|
+
encoded = _escape_html(text)
|
|
728
|
+
content_html = f'<div style="white-space: pre-wrap; font-family: var(--font-mono);">{encoded}</div>'
|
|
483
729
|
|
|
484
730
|
if len(lines) <= _TOOL_OUTPUT_PREVIEW_LINES:
|
|
485
|
-
|
|
486
|
-
return f'<div style="white-space: pre-wrap; font-family: var(--font-mono);">{content}</div>'
|
|
487
|
-
|
|
488
|
-
preview = "\n".join(escaped_lines[:_TOOL_OUTPUT_PREVIEW_LINES])
|
|
489
|
-
full = "\n".join(escaped_lines)
|
|
731
|
+
return content_html
|
|
490
732
|
|
|
491
733
|
return (
|
|
492
|
-
f'<div class="expandable-output
|
|
493
|
-
f'<div class="
|
|
494
|
-
f
|
|
495
|
-
f
|
|
496
|
-
f'<div class="
|
|
734
|
+
f'<div class="expandable-tool-output">'
|
|
735
|
+
f'<div class="tool-output-preview">'
|
|
736
|
+
f"{content_html}"
|
|
737
|
+
f"</div>"
|
|
738
|
+
f'<div class="expand-control">'
|
|
739
|
+
f'<button class="expand-btn">Show full output</button>'
|
|
740
|
+
f"</div>"
|
|
497
741
|
f"</div>"
|
|
498
742
|
)
|
|
499
743
|
|
|
@@ -674,17 +918,51 @@ def _get_mermaid_link_html(
|
|
|
674
918
|
|
|
675
919
|
# If we have code, render the diagram
|
|
676
920
|
if code:
|
|
677
|
-
return
|
|
678
|
-
f'<div style="background: white; padding: 16px; border-radius: 4px; margin-top: 8px; border: 1px solid var(--border);">'
|
|
679
|
-
f'<div class="mermaid">{escaped_code}</div>'
|
|
680
|
-
f"{toolbar_html}"
|
|
681
|
-
f"</div>"
|
|
682
|
-
)
|
|
921
|
+
return f'<div class="mermaid-container"><div class="mermaid">{escaped_code}</div>{toolbar_html}</div>'
|
|
683
922
|
|
|
684
923
|
# Fallback to just link/toolbar if no code available (legacy support behavior)
|
|
685
924
|
return toolbar_html
|
|
686
925
|
|
|
687
926
|
|
|
927
|
+
def _format_mermaid_tool_call(
|
|
928
|
+
tool_call: message.ToolCallPart,
|
|
929
|
+
result: message.ToolResultMessage | None,
|
|
930
|
+
timestamp: datetime,
|
|
931
|
+
mermaid_html: str,
|
|
932
|
+
args_html: str,
|
|
933
|
+
ts_str: str,
|
|
934
|
+
) -> str:
|
|
935
|
+
# Build standard tool details but hidden
|
|
936
|
+
should_collapse = _should_collapse(args_html)
|
|
937
|
+
open_attr = "" if should_collapse else " open"
|
|
938
|
+
|
|
939
|
+
details_html = (
|
|
940
|
+
f'<div class="mermaid-meta" style="display: none;">'
|
|
941
|
+
f'<div class="tool-header">'
|
|
942
|
+
f'<span class="tool-name">{_escape_html(tool_call.tool_name)}</span>'
|
|
943
|
+
f'<div class="tool-header-right">'
|
|
944
|
+
f'<span class="tool-id">{_escape_html(tool_call.call_id)}</span>'
|
|
945
|
+
f'<span class="timestamp">{ts_str}</span>'
|
|
946
|
+
f"</div>"
|
|
947
|
+
f"</div>"
|
|
948
|
+
f'<details class="tool-args-collapsible"{open_attr}>'
|
|
949
|
+
f"<summary>Arguments</summary>"
|
|
950
|
+
f'<div class="tool-args-content">{args_html}</div>'
|
|
951
|
+
f"</details>"
|
|
952
|
+
f"</div>"
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
return (
|
|
956
|
+
f'<div class="tool-call mermaid-tool-call">'
|
|
957
|
+
f'<div class="mermaid-view">'
|
|
958
|
+
f'<button class="mermaid-info-btn" title="Show/Hide Details">i</button>'
|
|
959
|
+
f"{mermaid_html}"
|
|
960
|
+
f"</div>"
|
|
961
|
+
f"{details_html}"
|
|
962
|
+
f"</div>"
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
|
|
688
966
|
def _format_tool_call(
|
|
689
967
|
tool_call: message.ToolCallPart,
|
|
690
968
|
result: message.ToolResultMessage | None,
|
|
@@ -711,6 +989,16 @@ def _format_tool_call(
|
|
|
711
989
|
if not args_html:
|
|
712
990
|
args_html = '<span style="color: var(--text-dim); font-style: italic;">(no arguments)</span>'
|
|
713
991
|
|
|
992
|
+
# Special handling for Mermaid
|
|
993
|
+
if tool_call.tool_name == "Mermaid" and result:
|
|
994
|
+
extras = _collect_ui_extras(result.ui_extra)
|
|
995
|
+
mermaid_extra = next((x for x in extras if isinstance(x, model.MermaidLinkUIExtra)), None)
|
|
996
|
+
mermaid_source = mermaid_extra if mermaid_extra else result.ui_extra
|
|
997
|
+
mermaid_html = _get_mermaid_link_html(mermaid_source, tool_call)
|
|
998
|
+
|
|
999
|
+
if mermaid_html:
|
|
1000
|
+
return _format_mermaid_tool_call(tool_call, result, timestamp, mermaid_html, args_html, ts_str)
|
|
1001
|
+
|
|
714
1002
|
# Wrap tool-args with collapsible details element (except for TodoWrite which renders as a list)
|
|
715
1003
|
if is_todo_list:
|
|
716
1004
|
args_section = f'<div class="tool-args">{args_html}</div>'
|
|
@@ -820,285 +1108,6 @@ def _format_tool_call(
|
|
|
820
1108
|
return "".join(html_parts)
|
|
821
1109
|
|
|
822
1110
|
|
|
823
|
-
def _build_messages_html(
|
|
824
|
-
history: list[message.HistoryEvent],
|
|
825
|
-
tool_results: dict[str, message.ToolResultMessage],
|
|
826
|
-
*,
|
|
827
|
-
seen_session_ids: set[str] | None = None,
|
|
828
|
-
nesting_level: int = 0,
|
|
829
|
-
) -> str:
|
|
830
|
-
"""Render session history into HTML.
|
|
831
|
-
|
|
832
|
-
This export groups the conversation into Tasks:
|
|
833
|
-
- A Task starts with a UserMessage.
|
|
834
|
-
- The Task includes all subsequent model thinking/replies/tool calls/results until the next UserMessage.
|
|
835
|
-
- By default, only the final assistant reply is shown; intermediate steps are folded.
|
|
836
|
-
- Mermaid is a special case: show the last consecutive assistant messages + Mermaid diagram result.
|
|
837
|
-
"""
|
|
838
|
-
|
|
839
|
-
if seen_session_ids is None:
|
|
840
|
-
seen_session_ids = set()
|
|
841
|
-
|
|
842
|
-
renderable_items = [item for item in history if not isinstance(item, message.ToolResultMessage)]
|
|
843
|
-
blocks: list[str] = []
|
|
844
|
-
|
|
845
|
-
def _render_user_message(item: message.UserMessage) -> str:
|
|
846
|
-
text = message.join_text_parts(item.parts)
|
|
847
|
-
images = _extract_image_parts(item.parts)
|
|
848
|
-
images_html = _render_image_parts(images)
|
|
849
|
-
ts_str = _format_msg_timestamp(item.created_at)
|
|
850
|
-
body_parts: list[str] = []
|
|
851
|
-
if images_html:
|
|
852
|
-
body_parts.append(images_html)
|
|
853
|
-
if text:
|
|
854
|
-
body_parts.append(f'<div style="white-space: pre-wrap;">{_escape_html(text)}</div>')
|
|
855
|
-
if not body_parts:
|
|
856
|
-
body_parts.append('<div style="color: var(--text-dim); font-style: italic;">(empty)</div>')
|
|
857
|
-
return (
|
|
858
|
-
f'<div class="message-group">'
|
|
859
|
-
f'<div class="role-label user">'
|
|
860
|
-
f"User"
|
|
861
|
-
f'<span class="timestamp">{ts_str}</span>'
|
|
862
|
-
f"</div>"
|
|
863
|
-
f'<div class="message-content user">{"".join(body_parts)}</div>'
|
|
864
|
-
f"</div>"
|
|
865
|
-
)
|
|
866
|
-
|
|
867
|
-
def _render_developer_message(item: message.DeveloperMessage, next_item: message.HistoryEvent | None) -> str:
|
|
868
|
-
content = message.join_text_parts(item.parts)
|
|
869
|
-
images = _extract_image_parts(item.parts)
|
|
870
|
-
images_html = _render_image_parts(images)
|
|
871
|
-
ts_str = _format_msg_timestamp(item.created_at)
|
|
872
|
-
|
|
873
|
-
extra_class = ""
|
|
874
|
-
if isinstance(next_item, (message.UserMessage, message.AssistantMessage)):
|
|
875
|
-
extra_class = " gap-below"
|
|
876
|
-
|
|
877
|
-
detail_body = ""
|
|
878
|
-
if images_html:
|
|
879
|
-
detail_body += images_html
|
|
880
|
-
if content:
|
|
881
|
-
detail_body += f'<div style="white-space: pre-wrap;">{_escape_html(content)}</div>'
|
|
882
|
-
if not detail_body:
|
|
883
|
-
detail_body = '<div style="color: var(--text-dim); font-style: italic;">(empty)</div>'
|
|
884
|
-
|
|
885
|
-
return (
|
|
886
|
-
f'<details class="developer-message{extra_class}">'
|
|
887
|
-
f"<summary>"
|
|
888
|
-
f"Developer"
|
|
889
|
-
f'<span class="timestamp">{ts_str}</span>'
|
|
890
|
-
f"</summary>"
|
|
891
|
-
f'<div class="details-content">{detail_body}</div>'
|
|
892
|
-
f"</details>"
|
|
893
|
-
)
|
|
894
|
-
|
|
895
|
-
def _render_system_message(item: message.SystemMessage) -> str | None:
|
|
896
|
-
content = message.join_text_parts(item.parts)
|
|
897
|
-
if not content:
|
|
898
|
-
return None
|
|
899
|
-
ts_str = _format_msg_timestamp(item.created_at)
|
|
900
|
-
return (
|
|
901
|
-
f'<details class="developer-message">'
|
|
902
|
-
f"<summary>"
|
|
903
|
-
f"System"
|
|
904
|
-
f'<span class="timestamp">{ts_str}</span>'
|
|
905
|
-
f"</summary>"
|
|
906
|
-
f'<div class="details-content" style="white-space: pre-wrap;">{_escape_html(content)}</div>'
|
|
907
|
-
f"</details>"
|
|
908
|
-
)
|
|
909
|
-
|
|
910
|
-
def _render_task_full(task_events: list[message.HistoryEvent]) -> str:
|
|
911
|
-
full_parts: list[str] = []
|
|
912
|
-
assistant_counter = 0
|
|
913
|
-
|
|
914
|
-
for idx, event in enumerate(task_events):
|
|
915
|
-
next_event = task_events[idx + 1] if idx + 1 < len(task_events) else None
|
|
916
|
-
|
|
917
|
-
if isinstance(event, message.AssistantMessage):
|
|
918
|
-
assistant_counter += 1
|
|
919
|
-
|
|
920
|
-
thinking_text = "".join(
|
|
921
|
-
part.text for part in event.parts if isinstance(part, message.ThinkingTextPart)
|
|
922
|
-
).strip()
|
|
923
|
-
if thinking_text:
|
|
924
|
-
full_parts.append(_render_thinking_block(thinking_text))
|
|
925
|
-
|
|
926
|
-
assistant_text = message.join_text_parts(event.parts)
|
|
927
|
-
assistant_images = _extract_image_parts(event.parts)
|
|
928
|
-
if assistant_text or assistant_images:
|
|
929
|
-
full_parts.append(
|
|
930
|
-
_render_assistant_message(
|
|
931
|
-
assistant_counter,
|
|
932
|
-
assistant_text,
|
|
933
|
-
event.created_at,
|
|
934
|
-
assistant_images,
|
|
935
|
-
)
|
|
936
|
-
)
|
|
937
|
-
|
|
938
|
-
for part in event.parts:
|
|
939
|
-
if isinstance(part, message.ToolCallPart):
|
|
940
|
-
result = tool_results.get(part.call_id)
|
|
941
|
-
full_parts.append(_format_tool_call(part, result, event.created_at))
|
|
942
|
-
if result is not None:
|
|
943
|
-
sub_agent_html = _render_sub_agent_session(result, seen_session_ids, nesting_level)
|
|
944
|
-
if sub_agent_html:
|
|
945
|
-
full_parts.append(sub_agent_html)
|
|
946
|
-
elif isinstance(event, model.TaskMetadataItem):
|
|
947
|
-
full_parts.append(_render_metadata_item(event))
|
|
948
|
-
elif isinstance(event, message.DeveloperMessage):
|
|
949
|
-
full_parts.append(_render_developer_message(event, next_event))
|
|
950
|
-
elif isinstance(event, message.SystemMessage):
|
|
951
|
-
rendered = _render_system_message(event)
|
|
952
|
-
if rendered:
|
|
953
|
-
full_parts.append(rendered)
|
|
954
|
-
|
|
955
|
-
return "\n".join(full_parts)
|
|
956
|
-
|
|
957
|
-
def _choose_compact_assistants(
|
|
958
|
-
task_events: list[message.HistoryEvent],
|
|
959
|
-
) -> tuple[list[message.AssistantMessage], bool]:
|
|
960
|
-
# Mermaid exception: show last consecutive assistant messages + Mermaid diagram result.
|
|
961
|
-
tail: list[message.AssistantMessage] = []
|
|
962
|
-
|
|
963
|
-
idx = len(task_events) - 1
|
|
964
|
-
while idx >= 0 and isinstance(task_events[idx], model.TaskMetadataItem):
|
|
965
|
-
idx -= 1
|
|
966
|
-
|
|
967
|
-
while idx >= 0 and isinstance(task_events[idx], message.AssistantMessage):
|
|
968
|
-
tail.append(cast(message.AssistantMessage, task_events[idx]))
|
|
969
|
-
idx -= 1
|
|
970
|
-
|
|
971
|
-
tail.reverse()
|
|
972
|
-
if tail:
|
|
973
|
-
has_mermaid = any(
|
|
974
|
-
isinstance(p, message.ToolCallPart) and p.tool_name == "Mermaid" for msg in tail for p in msg.parts
|
|
975
|
-
)
|
|
976
|
-
if has_mermaid:
|
|
977
|
-
return tail, True
|
|
978
|
-
|
|
979
|
-
for idx in range(len(task_events) - 1, -1, -1):
|
|
980
|
-
if isinstance(task_events[idx], message.AssistantMessage):
|
|
981
|
-
return [cast(message.AssistantMessage, task_events[idx])], False
|
|
982
|
-
|
|
983
|
-
return [], False
|
|
984
|
-
|
|
985
|
-
def _render_assistant_compact(assistant_messages: list[message.AssistantMessage], *, show_mermaid: bool) -> str:
|
|
986
|
-
parts: list[str] = []
|
|
987
|
-
|
|
988
|
-
for counter, msg in enumerate(assistant_messages, start=1):
|
|
989
|
-
assistant_text = message.join_text_parts(msg.parts)
|
|
990
|
-
assistant_images = _extract_image_parts(msg.parts)
|
|
991
|
-
if assistant_text or assistant_images:
|
|
992
|
-
parts.append(_render_assistant_message(counter, assistant_text, msg.created_at, assistant_images))
|
|
993
|
-
|
|
994
|
-
if show_mermaid:
|
|
995
|
-
# Keep Mermaid diagram interleaved where the tool call occurred.
|
|
996
|
-
for p in msg.parts:
|
|
997
|
-
if not isinstance(p, message.ToolCallPart) or p.tool_name != "Mermaid":
|
|
998
|
-
continue
|
|
999
|
-
|
|
1000
|
-
result = tool_results.get(p.call_id)
|
|
1001
|
-
ui_extra = result.ui_extra if result else None
|
|
1002
|
-
extras = _collect_ui_extras(ui_extra)
|
|
1003
|
-
mermaid_extra = next((x for x in extras if isinstance(x, model.MermaidLinkUIExtra)), None)
|
|
1004
|
-
mermaid_source = mermaid_extra if mermaid_extra else ui_extra
|
|
1005
|
-
mermaid_html = _get_mermaid_link_html(mermaid_source, p)
|
|
1006
|
-
if mermaid_html:
|
|
1007
|
-
parts.append(f'<div class="task-mermaid-result">{mermaid_html}</div>')
|
|
1008
|
-
|
|
1009
|
-
return "\n".join(parts)
|
|
1010
|
-
|
|
1011
|
-
def _count_hidden_steps(
|
|
1012
|
-
task_events: list[message.HistoryEvent],
|
|
1013
|
-
compact_assistants: list[message.AssistantMessage],
|
|
1014
|
-
) -> int:
|
|
1015
|
-
compact_ids = {id(x) for x in compact_assistants}
|
|
1016
|
-
hidden_assistant_messages = sum(
|
|
1017
|
-
1 for x in task_events if isinstance(x, message.AssistantMessage) and id(x) not in compact_ids
|
|
1018
|
-
)
|
|
1019
|
-
|
|
1020
|
-
thinking_blocks = 0
|
|
1021
|
-
tool_calls = 0
|
|
1022
|
-
other_events = 0
|
|
1023
|
-
for e in task_events:
|
|
1024
|
-
if isinstance(e, message.AssistantMessage):
|
|
1025
|
-
for part in e.parts:
|
|
1026
|
-
if isinstance(part, message.ThinkingTextPart) and part.text.strip():
|
|
1027
|
-
thinking_blocks += 1
|
|
1028
|
-
elif isinstance(part, message.ToolCallPart):
|
|
1029
|
-
tool_calls += 1
|
|
1030
|
-
elif isinstance(e, (model.TaskMetadataItem, message.DeveloperMessage, message.SystemMessage)):
|
|
1031
|
-
other_events += 1
|
|
1032
|
-
|
|
1033
|
-
return hidden_assistant_messages + thinking_blocks + tool_calls + other_events
|
|
1034
|
-
|
|
1035
|
-
def _render_task(user: message.UserMessage, task_events: list[message.HistoryEvent]) -> str:
|
|
1036
|
-
compact_assistants, show_mermaid = _choose_compact_assistants(task_events)
|
|
1037
|
-
hidden_steps = _count_hidden_steps(task_events, compact_assistants)
|
|
1038
|
-
|
|
1039
|
-
full_html = _render_task_full(task_events)
|
|
1040
|
-
compact_html = _render_assistant_compact(compact_assistants, show_mermaid=show_mermaid)
|
|
1041
|
-
|
|
1042
|
-
if not compact_html.strip() and full_html.strip():
|
|
1043
|
-
compact_html = '<div style="color: var(--text-dim); font-style: italic;">(no assistant message)</div>'
|
|
1044
|
-
|
|
1045
|
-
steps_details = ""
|
|
1046
|
-
if hidden_steps > 0 and full_html.strip():
|
|
1047
|
-
steps_details = (
|
|
1048
|
-
f'<details class="task-steps" data-step-count="{hidden_steps}">'
|
|
1049
|
-
f'<summary data-step-count="{hidden_steps}">Show {hidden_steps} steps</summary>'
|
|
1050
|
-
f'<div class="task-steps-content">{full_html}</div>'
|
|
1051
|
-
f"</details>"
|
|
1052
|
-
)
|
|
1053
|
-
|
|
1054
|
-
return (
|
|
1055
|
-
'<div class="task">'
|
|
1056
|
-
f"{_render_user_message(user)}"
|
|
1057
|
-
f"{steps_details}"
|
|
1058
|
-
f'<div class="task-final">{compact_html}</div>'
|
|
1059
|
-
"</div>"
|
|
1060
|
-
)
|
|
1061
|
-
|
|
1062
|
-
current_user: message.UserMessage | None = None
|
|
1063
|
-
current_task_events: list[message.HistoryEvent] = []
|
|
1064
|
-
|
|
1065
|
-
def _flush_task() -> None:
|
|
1066
|
-
nonlocal current_user, current_task_events
|
|
1067
|
-
if current_user is None:
|
|
1068
|
-
return
|
|
1069
|
-
blocks.append(_render_task(current_user, current_task_events))
|
|
1070
|
-
current_user = None
|
|
1071
|
-
current_task_events = []
|
|
1072
|
-
|
|
1073
|
-
for idx, item in enumerate(renderable_items):
|
|
1074
|
-
if isinstance(item, message.UserMessage):
|
|
1075
|
-
_flush_task()
|
|
1076
|
-
current_user = item
|
|
1077
|
-
current_task_events = []
|
|
1078
|
-
continue
|
|
1079
|
-
|
|
1080
|
-
if current_user is not None:
|
|
1081
|
-
current_task_events.append(item)
|
|
1082
|
-
continue
|
|
1083
|
-
|
|
1084
|
-
# Events before the first user message.
|
|
1085
|
-
next_item = renderable_items[idx + 1] if idx + 1 < len(renderable_items) else None
|
|
1086
|
-
if isinstance(item, message.DeveloperMessage):
|
|
1087
|
-
blocks.append(_render_developer_message(item, next_item))
|
|
1088
|
-
elif isinstance(item, message.SystemMessage):
|
|
1089
|
-
rendered = _render_system_message(item)
|
|
1090
|
-
if rendered:
|
|
1091
|
-
blocks.append(rendered)
|
|
1092
|
-
elif isinstance(item, message.AssistantMessage):
|
|
1093
|
-
blocks.append(_render_task_full([item]))
|
|
1094
|
-
elif isinstance(item, model.TaskMetadataItem):
|
|
1095
|
-
blocks.append(_render_metadata_item(item))
|
|
1096
|
-
|
|
1097
|
-
_flush_task()
|
|
1098
|
-
|
|
1099
|
-
return "\n".join(blocks)
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
1111
|
def _render_sub_agent_session(
|
|
1103
1112
|
tool_result: message.ToolResultMessage,
|
|
1104
1113
|
seen_session_ids: set[str],
|