klaude-code 1.2.27__py3-none-any.whl → 1.2.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. klaude_code/cli/config_cmd.py +13 -6
  2. klaude_code/cli/debug.py +9 -1
  3. klaude_code/cli/list_model.py +1 -1
  4. klaude_code/cli/main.py +39 -14
  5. klaude_code/cli/runtime.py +11 -5
  6. klaude_code/command/__init__.py +3 -0
  7. klaude_code/command/export_online_cmd.py +15 -12
  8. klaude_code/command/fork_session_cmd.py +42 -0
  9. klaude_code/config/__init__.py +11 -1
  10. klaude_code/config/config.py +21 -17
  11. klaude_code/config/select_model.py +1 -0
  12. klaude_code/core/executor.py +2 -1
  13. klaude_code/core/reminders.py +52 -16
  14. klaude_code/core/tool/web/mermaid_tool.md +17 -0
  15. klaude_code/core/tool/web/mermaid_tool.py +2 -2
  16. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  17. klaude_code/protocol/commands.py +1 -0
  18. klaude_code/protocol/model.py +2 -0
  19. klaude_code/session/export.py +61 -17
  20. klaude_code/session/session.py +23 -1
  21. klaude_code/session/templates/mermaid_viewer.html +926 -0
  22. klaude_code/trace/log.py +7 -1
  23. klaude_code/ui/modes/repl/__init__.py +3 -44
  24. klaude_code/ui/modes/repl/completers.py +35 -3
  25. klaude_code/ui/modes/repl/event_handler.py +9 -5
  26. klaude_code/ui/modes/repl/input_prompt_toolkit.py +32 -65
  27. klaude_code/ui/modes/repl/renderer.py +1 -6
  28. klaude_code/ui/renderers/assistant.py +4 -2
  29. klaude_code/ui/renderers/common.py +11 -4
  30. klaude_code/ui/renderers/developer.py +26 -7
  31. klaude_code/ui/renderers/errors.py +10 -5
  32. klaude_code/ui/renderers/mermaid_viewer.py +58 -0
  33. klaude_code/ui/renderers/tools.py +46 -18
  34. klaude_code/ui/rich/markdown.py +4 -4
  35. klaude_code/ui/rich/theme.py +12 -2
  36. {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/METADATA +1 -1
  37. {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/RECORD +39 -36
  38. {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/entry_points.txt +1 -0
  39. {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/WHEEL +0 -0
@@ -421,10 +421,39 @@ def _render_diff_span(span: model.DiffSpan, line_kind: str) -> str:
421
421
  return f'<span class="diff-span">{text}</span>'
422
422
 
423
423
 
424
- def _get_diff_ui_extra(ui_extra: model.ToolResultUIExtra | None) -> model.DiffUIExtra | None:
425
- if isinstance(ui_extra, model.DiffUIExtra):
426
- return ui_extra
427
- return None
424
+ def _render_markdown_doc(doc: model.MarkdownDocUIExtra) -> str:
425
+ encoded = _escape_html(doc.content)
426
+ file_path = _escape_html(doc.file_path)
427
+ header = f'<div class="diff-file">{file_path} <span style="font-weight: normal; color: var(--text-dim); font-size: 12px; margin-left: 8px;">(markdown content)</span></div>'
428
+
429
+ # Using a container that mimics diff-view but for markdown
430
+ content = (
431
+ f'<div class="markdown-content markdown-body" data-raw="{encoded}" '
432
+ f'style="padding: 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-body); margin-top: 4px;">'
433
+ f'<noscript><pre style="white-space: pre-wrap;">{encoded}</pre></noscript>'
434
+ f"</div>"
435
+ )
436
+
437
+ line_count = doc.content.count("\n") + 1
438
+ open_attr = " open"
439
+
440
+ return (
441
+ f'<details class="diff-collapsible"{open_attr}>'
442
+ f"<summary>File Content ({line_count} lines)</summary>"
443
+ f'<div style="margin-top: 8px;">'
444
+ f"{header}"
445
+ f"{content}"
446
+ f"</div>"
447
+ f"</details>"
448
+ )
449
+
450
+
451
+ def _collect_ui_extras(ui_extra: model.ToolResultUIExtra | None) -> list[model.ToolResultUIExtra]:
452
+ if ui_extra is None:
453
+ return []
454
+ if isinstance(ui_extra, model.MultiUIExtra):
455
+ return list(ui_extra.items)
456
+ return [ui_extra]
428
457
 
429
458
 
430
459
  def _build_add_only_diff(text: str, file_path: str) -> model.DiffUIExtra:
@@ -446,21 +475,28 @@ def _build_add_only_diff(text: str, file_path: str) -> model.DiffUIExtra:
446
475
  def _get_mermaid_link_html(
447
476
  ui_extra: model.ToolResultUIExtra | None, tool_call: model.ToolCallItem | None = None
448
477
  ) -> str | None:
449
- if tool_call and tool_call.name == "Mermaid":
478
+ code = ""
479
+ link: str | None = None
480
+ line_count = 0
481
+
482
+ if isinstance(ui_extra, model.MermaidLinkUIExtra):
483
+ code = ui_extra.code
484
+ link = ui_extra.link
485
+ line_count = ui_extra.line_count
486
+
487
+ if not code and tool_call and tool_call.name == "Mermaid":
450
488
  try:
451
489
  args = json.loads(tool_call.arguments)
452
490
  code = args.get("code", "")
453
491
  except (json.JSONDecodeError, TypeError):
454
492
  code = ""
455
- else:
456
- code = ""
493
+ line_count = code.count("\n") + 1 if code else 0
457
494
 
458
- if not code and not isinstance(ui_extra, model.MermaidLinkUIExtra):
495
+ if not code and not link:
459
496
  return None
460
497
 
461
498
  # Prepare code for rendering and copy
462
499
  escaped_code = _escape_html(code) if code else ""
463
- line_count = code.count("\n") + 1 if code else 0
464
500
 
465
501
  # Build Toolbar
466
502
  toolbar_items: list[str] = []
@@ -477,8 +513,6 @@ def _get_mermaid_link_html(
477
513
  '<button type="button" class="fullscreen-mermaid-btn" title="View Fullscreen">Fullscreen</button>'
478
514
  )
479
515
 
480
- link = ui_extra.link if isinstance(ui_extra, model.MermaidLinkUIExtra) else None
481
-
482
516
  if link:
483
517
  link_url = _escape_html(link)
484
518
  buttons_html.append(
@@ -567,19 +601,26 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
567
601
  ]
568
602
 
569
603
  if result:
570
- diff_ui = _get_diff_ui_extra(result.ui_extra)
571
- mermaid_html = _get_mermaid_link_html(result.ui_extra, tool_call)
604
+ extras = _collect_ui_extras(result.ui_extra)
605
+
606
+ mermaid_extra = next((x for x in extras if isinstance(x, model.MermaidLinkUIExtra)), None)
607
+ mermaid_source = mermaid_extra if mermaid_extra else result.ui_extra
608
+ mermaid_html = _get_mermaid_link_html(mermaid_source, tool_call)
572
609
 
573
610
  should_hide_text = tool_call.name in ("TodoWrite", "update_plan") and result.status != "error"
574
611
 
575
- if tool_call.name == "Edit" and not diff_ui and result.status != "error":
612
+ if (
613
+ tool_call.name == "Edit"
614
+ and not any(isinstance(x, model.DiffUIExtra) for x in extras)
615
+ and result.status != "error"
616
+ ):
576
617
  try:
577
618
  args_data = json.loads(tool_call.arguments)
578
619
  file_path = args_data.get("file_path", "Unknown file")
579
620
  old_string = args_data.get("old_string", "")
580
621
  new_string = args_data.get("new_string", "")
581
622
  if old_string == "" and new_string:
582
- diff_ui = _build_add_only_diff(new_string, file_path)
623
+ extras.append(_build_add_only_diff(new_string, file_path))
583
624
  except (json.JSONDecodeError, TypeError):
584
625
  pass
585
626
 
@@ -591,8 +632,11 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
591
632
  else:
592
633
  items_to_render.append(_render_text_block(result.output))
593
634
 
594
- if diff_ui:
595
- items_to_render.append(_render_diff_block(diff_ui))
635
+ for extra in extras:
636
+ if isinstance(extra, model.DiffUIExtra):
637
+ items_to_render.append(_render_diff_block(extra))
638
+ elif isinstance(extra, model.MarkdownDocUIExtra):
639
+ items_to_render.append(_render_markdown_doc(extra))
596
640
 
597
641
  if mermaid_html:
598
642
  items_to_render.append(mermaid_html)
@@ -197,6 +197,28 @@ class Session(BaseModel):
197
197
  )
198
198
  self._store.append_and_flush(session_id=self.id, items=items, meta=meta)
199
199
 
200
+ def fork(self, *, new_id: str | None = None) -> Session:
201
+ """Create a new session as a fork of the current session.
202
+
203
+ The forked session copies metadata and conversation history, but does not
204
+ modify the current session.
205
+ """
206
+
207
+ forked = Session.create(id=new_id, work_dir=self.work_dir)
208
+
209
+ forked.sub_agent_state = None
210
+ forked.model_name = self.model_name
211
+ forked.model_config_name = self.model_config_name
212
+ forked.model_thinking = self.model_thinking.model_copy(deep=True) if self.model_thinking is not None else None
213
+ forked.file_tracker = {k: v.model_copy(deep=True) for k, v in self.file_tracker.items()}
214
+ forked.todos = [todo.model_copy(deep=True) for todo in self.todos]
215
+
216
+ items = [it.model_copy(deep=True) for it in self.conversation_history]
217
+ if items:
218
+ forked.append_history(items)
219
+
220
+ return forked
221
+
200
222
  async def wait_for_flush(self) -> None:
201
223
  await self._store.wait_for_flush(self.id)
202
224
 
@@ -224,7 +246,7 @@ class Session(BaseModel):
224
246
  def need_turn_start(self, prev_item: model.ConversationItem | None, item: model.ConversationItem) -> bool:
225
247
  if not isinstance(
226
248
  item,
227
- model.ReasoningEncryptedItem | model.ReasoningTextItem | model.AssistantMessageItem | model.ToolCallItem,
249
+ model.ReasoningTextItem | model.AssistantMessageItem | model.ToolCallItem,
228
250
  ):
229
251
  return False
230
252
  if prev_item is None: