klaude-code 1.2.20__py3-none-any.whl → 1.2.22__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 (42) hide show
  1. klaude_code/cli/debug.py +8 -10
  2. klaude_code/cli/main.py +23 -0
  3. klaude_code/cli/runtime.py +13 -1
  4. klaude_code/command/__init__.py +0 -3
  5. klaude_code/command/prompt-deslop.md +1 -1
  6. klaude_code/command/thinking_cmd.py +10 -0
  7. klaude_code/const/__init__.py +2 -5
  8. klaude_code/core/prompt.py +5 -2
  9. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  10. klaude_code/core/prompts/{prompt-codex-gpt-5-1.md → prompt-codex.md} +9 -42
  11. klaude_code/core/reminders.py +36 -2
  12. klaude_code/core/tool/__init__.py +0 -5
  13. klaude_code/core/tool/file/_utils.py +6 -0
  14. klaude_code/core/tool/file/apply_patch_tool.py +30 -72
  15. klaude_code/core/tool/file/diff_builder.py +151 -0
  16. klaude_code/core/tool/file/edit_tool.py +35 -18
  17. klaude_code/core/tool/file/read_tool.py +45 -86
  18. klaude_code/core/tool/file/write_tool.py +40 -30
  19. klaude_code/core/tool/shell/bash_tool.py +151 -3
  20. klaude_code/core/tool/web/mermaid_tool.md +26 -0
  21. klaude_code/protocol/commands.py +0 -1
  22. klaude_code/protocol/model.py +29 -10
  23. klaude_code/protocol/tools.py +1 -2
  24. klaude_code/session/export.py +75 -20
  25. klaude_code/session/session.py +7 -0
  26. klaude_code/session/templates/export_session.html +28 -0
  27. klaude_code/ui/renderers/common.py +26 -11
  28. klaude_code/ui/renderers/developer.py +0 -5
  29. klaude_code/ui/renderers/diffs.py +84 -0
  30. klaude_code/ui/renderers/tools.py +19 -98
  31. klaude_code/ui/rich/markdown.py +11 -1
  32. klaude_code/ui/rich/status.py +8 -11
  33. klaude_code/ui/rich/theme.py +14 -4
  34. {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/METADATA +2 -1
  35. {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/RECORD +37 -40
  36. klaude_code/command/diff_cmd.py +0 -136
  37. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  38. klaude_code/core/tool/file/multi_edit_tool.py +0 -175
  39. klaude_code/core/tool/memory/memory_tool.md +0 -20
  40. klaude_code/core/tool/memory/memory_tool.py +0 -456
  41. {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/WHEEL +0 -0
  42. {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/entry_points.txt +0 -0
@@ -70,9 +70,15 @@ class TodoItem(BaseModel):
70
70
 
71
71
 
72
72
  class FileStatus(BaseModel):
73
- """Tracks file state including modification time and memory file flag."""
73
+ """Tracks file state including modification time and content hash.
74
+
75
+ Notes:
76
+ - `mtime` is a cheap heuristic and may miss changes on some filesystems.
77
+ - `content_sha256` provides an explicit content-based change detector.
78
+ """
74
79
 
75
80
  mtime: float
81
+ content_sha256: str | None = None
76
82
  is_memory: bool = False
77
83
 
78
84
 
@@ -86,9 +92,27 @@ class ToolSideEffect(str, Enum):
86
92
 
87
93
 
88
94
  # Discriminated union types for ToolResultUIExtra
89
- class DiffTextUIExtra(BaseModel):
90
- type: Literal["diff_text"] = "diff_text"
91
- diff_text: str
95
+ class DiffSpan(BaseModel):
96
+ op: Literal["equal", "insert", "delete"]
97
+ text: str
98
+
99
+
100
+ class DiffLine(BaseModel):
101
+ kind: Literal["ctx", "add", "remove", "gap"]
102
+ new_line_no: int | None = None
103
+ spans: list[DiffSpan]
104
+
105
+
106
+ class DiffFileDiff(BaseModel):
107
+ file_path: str
108
+ lines: list[DiffLine]
109
+ stats_add: int = 0
110
+ stats_remove: int = 0
111
+
112
+
113
+ class DiffUIExtra(BaseModel):
114
+ type: Literal["diff"] = "diff"
115
+ files: list[DiffFileDiff]
92
116
 
93
117
 
94
118
  class TodoListUIExtra(BaseModel):
@@ -122,12 +146,7 @@ class SessionStatusUIExtra(BaseModel):
122
146
 
123
147
 
124
148
  ToolResultUIExtra = Annotated[
125
- DiffTextUIExtra
126
- | TodoListUIExtra
127
- | SessionIdUIExtra
128
- | MermaidLinkUIExtra
129
- | TruncationUIExtra
130
- | SessionStatusUIExtra,
149
+ DiffUIExtra | TodoListUIExtra | SessionIdUIExtra | MermaidLinkUIExtra | TruncationUIExtra | SessionStatusUIExtra,
131
150
  Field(discriminator="type"),
132
151
  ]
133
152
 
@@ -1,14 +1,13 @@
1
1
  BASH = "Bash"
2
2
  APPLY_PATCH = "apply_patch"
3
3
  EDIT = "Edit"
4
- MULTI_EDIT = "MultiEdit"
4
+
5
5
  READ = "Read"
6
6
  WRITE = "Write"
7
7
  TODO_WRITE = "TodoWrite"
8
8
  UPDATE_PLAN = "update_plan"
9
9
  SKILL = "Skill"
10
10
  MERMAID = "Mermaid"
11
- MEMORY = "Memory"
12
11
  WEB_FETCH = "WebFetch"
13
12
  WEB_SEARCH = "WebSearch"
14
13
  REPORT_BACK = "report_back"
@@ -362,33 +362,87 @@ def _should_collapse(text: str) -> bool:
362
362
  return text.count("\n") + 1 > _COLLAPSIBLE_LINE_THRESHOLD or len(text) > _COLLAPSIBLE_CHAR_THRESHOLD
363
363
 
364
364
 
365
- def _render_diff_block(diff: str) -> str:
366
- lines = diff.splitlines()
365
+ def _render_diff_block(diff: model.DiffUIExtra) -> str:
367
366
  rendered: list[str] = []
368
- for line in lines:
369
- escaped = _escape_html(line)
370
- if line.startswith("+"):
371
- rendered.append(f'<span class="diff-line diff-plus">{escaped}</span>')
372
- elif line.startswith("-"):
373
- rendered.append(f'<span class="diff-line diff-minus">{escaped}</span>')
374
- else:
375
- rendered.append(f'<span class="diff-line diff-ctx">{escaped}</span>')
367
+ line_count = 0
368
+
369
+ for file_diff in diff.files:
370
+ header = _render_diff_file_header(file_diff)
371
+ if header:
372
+ rendered.append(header)
373
+ for line in file_diff.lines:
374
+ rendered.append(_render_diff_line(line))
375
+ line_count += 1
376
+
377
+ if line_count == 0:
378
+ rendered.append('<span class="diff-line diff-ctx">&nbsp;</span>')
379
+
376
380
  diff_content = f'<div class="diff-view">{"".join(rendered)}</div>'
377
- open_attr = "" if _should_collapse(diff) else " open"
381
+ open_attr = "" if _should_collapse("\n" * max(1, line_count)) else " open"
378
382
  return (
379
383
  f'<details class="diff-collapsible"{open_attr}>'
380
- f"<summary>Diff ({len(lines)} lines)</summary>"
384
+ f"<summary>Diff ({line_count} lines)</summary>"
381
385
  f"{diff_content}"
382
386
  "</details>"
383
387
  )
384
388
 
385
389
 
386
- def _get_diff_text(ui_extra: model.ToolResultUIExtra | None) -> str | None:
387
- if isinstance(ui_extra, model.DiffTextUIExtra):
388
- return ui_extra.diff_text
390
+ def _render_diff_file_header(file_diff: model.DiffFileDiff) -> str:
391
+ stats_parts: list[str] = []
392
+ if file_diff.stats_add > 0:
393
+ stats_parts.append(f'<span class="diff-stats-add">+{file_diff.stats_add}</span>')
394
+ if file_diff.stats_remove > 0:
395
+ stats_parts.append(f'<span class="diff-stats-remove">-{file_diff.stats_remove}</span>')
396
+ stats_html = f' <span class="diff-stats">{" ".join(stats_parts)}</span>' if stats_parts else ""
397
+ file_name = _escape_html(file_diff.file_path)
398
+ return f'<div class="diff-file">{file_name}{stats_html}</div>'
399
+
400
+
401
+ def _render_diff_line(line: model.DiffLine) -> str:
402
+ if line.kind == "gap":
403
+ line_class = "diff-ctx"
404
+ prefix = "⋮"
405
+ else:
406
+ line_class = "diff-plus" if line.kind == "add" else "diff-minus" if line.kind == "remove" else "diff-ctx"
407
+ prefix = "+" if line.kind == "add" else "-" if line.kind == "remove" else " "
408
+ spans = [_render_diff_span(span, line.kind) for span in line.spans]
409
+ content = "".join(spans)
410
+ if not content:
411
+ content = "&nbsp;"
412
+ return f'<span class="diff-line {line_class}">{prefix} {content}</span>'
413
+
414
+
415
+ def _render_diff_span(span: model.DiffSpan, line_kind: str) -> str:
416
+ text = _escape_html(span.text)
417
+ if line_kind == "add" and span.op == "insert":
418
+ return f'<span class="diff-span diff-char-add">{text}</span>'
419
+ if line_kind == "remove" and span.op == "delete":
420
+ return f'<span class="diff-span diff-char-remove">{text}</span>'
421
+ return f'<span class="diff-span">{text}</span>'
422
+
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
389
427
  return None
390
428
 
391
429
 
430
+ def _build_add_only_diff(text: str, file_path: str) -> model.DiffUIExtra:
431
+ lines: list[model.DiffLine] = []
432
+ new_line_no = 1
433
+ for line in text.splitlines():
434
+ lines.append(
435
+ model.DiffLine(
436
+ kind="add",
437
+ new_line_no=new_line_no,
438
+ spans=[model.DiffSpan(op="equal", text=line)],
439
+ )
440
+ )
441
+ new_line_no += 1
442
+ file_diff = model.DiffFileDiff(file_path=file_path, lines=lines, stats_add=len(lines), stats_remove=0)
443
+ return model.DiffUIExtra(files=[file_diff])
444
+
445
+
392
446
  def _get_mermaid_link_html(
393
447
  ui_extra: model.ToolResultUIExtra | None, tool_call: model.ToolCallItem | None = None
394
448
  ) -> str | None:
@@ -513,18 +567,19 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
513
567
  ]
514
568
 
515
569
  if result:
516
- diff_text = _get_diff_text(result.ui_extra)
570
+ diff_ui = _get_diff_ui_extra(result.ui_extra)
517
571
  mermaid_html = _get_mermaid_link_html(result.ui_extra, tool_call)
518
572
 
519
573
  should_hide_text = tool_call.name in ("TodoWrite", "update_plan") and result.status != "error"
520
574
 
521
- if tool_call.name == "Edit" and not diff_text and result.status != "error":
575
+ if tool_call.name == "Edit" and not diff_ui and result.status != "error":
522
576
  try:
523
577
  args_data = json.loads(tool_call.arguments)
578
+ file_path = args_data.get("file_path", "Unknown file")
524
579
  old_string = args_data.get("old_string", "")
525
580
  new_string = args_data.get("new_string", "")
526
581
  if old_string == "" and new_string:
527
- diff_text = "\n".join(f"+{line}" for line in new_string.splitlines())
582
+ diff_ui = _build_add_only_diff(new_string, file_path)
528
583
  except (json.JSONDecodeError, TypeError):
529
584
  pass
530
585
 
@@ -536,8 +591,8 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
536
591
  else:
537
592
  items_to_render.append(_render_text_block(result.output))
538
593
 
539
- if diff_text:
540
- items_to_render.append(_render_diff_block(diff_text))
594
+ if diff_ui:
595
+ items_to_render.append(_render_diff_block(diff_ui))
541
596
 
542
597
  if mermaid_html:
543
598
  items_to_render.append(mermaid_html)
@@ -86,6 +86,13 @@ class Session(BaseModel):
86
86
  def paths(cls) -> ProjectPaths:
87
87
  return get_default_store().paths
88
88
 
89
+ @classmethod
90
+ def exists(cls, id: str) -> bool:
91
+ """Return True if a persisted session exists for the current project."""
92
+
93
+ paths = cls.paths()
94
+ return paths.meta_file(id).exists() or paths.events_file(id).exists()
95
+
89
96
  @classmethod
90
97
  def create(cls, id: str | None = None, *, work_dir: Path | None = None) -> Session:
91
98
  session = Session(id=id or uuid.uuid4().hex, work_dir=work_dir or Path.cwd())
@@ -31,6 +31,8 @@
31
31
  --bg-overlay: rgba(248, 246, 240, 0.98);
32
32
  --bg-error: #fdecec;
33
33
  --bg-success: #eaf6ed;
34
+ --bg-error-strong: #f9d6d6;
35
+ --bg-success-strong: #d5efdd;
34
36
  --bg-code: #f7f7f4;
35
37
  --border: #ded8cf;
36
38
  --text: #151515;
@@ -815,6 +817,21 @@
815
817
  overflow-x: auto;
816
818
  border: 1px solid var(--border);
817
819
  }
820
+ .diff-file {
821
+ color: var(--accent);
822
+ font-weight: 700;
823
+ margin: 6px 0 4px 0;
824
+ font-size: var(--font-size-sm);
825
+ }
826
+ .diff-stats {
827
+ font-weight: 600;
828
+ }
829
+ .diff-stats-add {
830
+ color: var(--success);
831
+ }
832
+ .diff-stats-remove {
833
+ color: var(--error);
834
+ }
818
835
  .diff-line {
819
836
  white-space: pre;
820
837
  }
@@ -833,6 +850,17 @@
833
850
  opacity: 0.7;
834
851
  display: block;
835
852
  }
853
+ .diff-span {
854
+ white-space: pre;
855
+ }
856
+ .diff-char-add {
857
+ background: var(--bg-success-strong);
858
+ font-weight: 600;
859
+ }
860
+ .diff-char-remove {
861
+ background: var(--bg-error-strong);
862
+ font-weight: 600;
863
+ }
836
864
 
837
865
  /* Collapsible Diff View */
838
866
  details.diff-collapsible {
@@ -31,16 +31,20 @@ def truncate_display(
31
31
  return Text(f"… (more {remaining} lines)", style=ThemeKey.TOOL_RESULT_TRUNCATED)
32
32
 
33
33
  lines = text.split("\n")
34
- extra_lines = 0
35
- if len(lines) > max_lines:
36
- extra_lines = len(lines) - max_lines
37
- lines = lines[:max_lines]
34
+ truncated_lines = 0
35
+ head_lines: list[str] = []
36
+ tail_lines: list[str] = []
38
37
 
39
- out = Text()
40
- if base_style is not None:
41
- out.style = base_style
38
+ if len(lines) > max_lines:
39
+ truncated_lines = len(lines) - max_lines
40
+ head_count = max_lines // 2
41
+ tail_count = max_lines - head_count
42
+ head_lines = lines[:head_count]
43
+ tail_lines = lines[-tail_count:]
44
+ else:
45
+ head_lines = lines
42
46
 
43
- for idx, line in enumerate(lines):
47
+ def append_line(out: Text, line: str) -> None:
44
48
  if len(line) > max_line_length:
45
49
  extra_chars = len(line) - max_line_length
46
50
  out.append(line[:max_line_length])
@@ -53,10 +57,21 @@ def truncate_display(
53
57
  else:
54
58
  out.append(line)
55
59
 
56
- if idx != len(lines) - 1 or extra_lines > 0:
60
+ out = Text()
61
+ if base_style is not None:
62
+ out.style = base_style
63
+
64
+ for idx, line in enumerate(head_lines):
65
+ append_line(out, line)
66
+ if idx < len(head_lines) - 1 or truncated_lines > 0 or tail_lines:
57
67
  out.append("\n")
58
68
 
59
- if extra_lines > 0:
60
- out.append_text(Text(f" (more {extra_lines} lines)", style=ThemeKey.TOOL_RESULT_TRUNCATED))
69
+ if truncated_lines > 0:
70
+ out.append_text(Text(f" (more {truncated_lines} lines)\n", style=ThemeKey.TOOL_RESULT_TRUNCATED))
71
+
72
+ for idx, line in enumerate(tail_lines):
73
+ append_line(out, line)
74
+ if idx < len(tail_lines) - 1:
75
+ out.append("\n")
61
76
 
62
77
  return out
@@ -4,7 +4,6 @@ from rich.table import Table
4
4
  from rich.text import Text
5
5
 
6
6
  from klaude_code.protocol import commands, events, model
7
- from klaude_code.ui.renderers import diffs as r_diffs
8
7
  from klaude_code.ui.renderers.common import create_grid, truncate_display
9
8
  from klaude_code.ui.renderers.tools import render_path
10
9
  from klaude_code.ui.rich.markdown import NoInsetMarkdown
@@ -103,10 +102,6 @@ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
103
102
  return Text("")
104
103
 
105
104
  match e.item.command_output.command_name:
106
- case commands.CommandName.DIFF:
107
- if e.item.content is None or len(e.item.content) == 0:
108
- return Padding.indent(Text("(no changes)", style=ThemeKey.TOOL_RESULT), level=2)
109
- return r_diffs.render_diff_panel(e.item.content, show_file_name=True)
110
105
  case commands.CommandName.HELP:
111
106
  return Padding.indent(Text.from_markup(e.item.content or ""), level=2)
112
107
  case commands.CommandName.STATUS:
@@ -5,6 +5,7 @@ from rich.panel import Panel
5
5
  from rich.text import Text
6
6
 
7
7
  from klaude_code import const
8
+ from klaude_code.protocol import model
8
9
  from klaude_code.ui.renderers.common import create_grid
9
10
  from klaude_code.ui.rich.theme import ThemeKey
10
11
 
@@ -179,6 +180,30 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
179
180
  return grid
180
181
 
181
182
 
183
+ def render_structured_diff(ui_extra: model.DiffUIExtra, show_file_name: bool = False) -> RenderableType:
184
+ files = ui_extra.files
185
+ if not files:
186
+ return Text("")
187
+
188
+ grid = create_grid()
189
+ grid.padding = (0, 0)
190
+ show_headers = show_file_name or len(files) > 1
191
+
192
+ for idx, file_diff in enumerate(files):
193
+ if idx > 0:
194
+ grid.add_row("", "")
195
+
196
+ if show_headers:
197
+ grid.add_row(*_render_file_header(file_diff))
198
+
199
+ for line in file_diff.lines:
200
+ prefix = _make_structured_prefix(line, const.DIFF_PREFIX_WIDTH)
201
+ text = _render_structured_line(line)
202
+ grid.add_row(Text(prefix, ThemeKey.TOOL_RESULT), text)
203
+
204
+ return grid
205
+
206
+
182
207
  def render_diff_panel(
183
208
  diff_text: str,
184
209
  *,
@@ -210,3 +235,62 @@ def render_diff_panel(
210
235
  if indent <= 0:
211
236
  return panel
212
237
  return Padding.indent(panel, level=indent)
238
+
239
+
240
+ def _render_file_header(file_diff: model.DiffFileDiff) -> tuple[Text, Text]:
241
+ file_text = Text(file_diff.file_path, style=ThemeKey.DIFF_FILE_NAME)
242
+ stats_text = Text()
243
+ if file_diff.stats_add > 0:
244
+ stats_text.append(f"+{file_diff.stats_add}", style=ThemeKey.DIFF_STATS_ADD)
245
+ if file_diff.stats_remove > 0:
246
+ if stats_text.plain:
247
+ stats_text.append(" ")
248
+ stats_text.append(f"-{file_diff.stats_remove}", style=ThemeKey.DIFF_STATS_REMOVE)
249
+
250
+ file_line = Text(style=ThemeKey.DIFF_FILE_NAME)
251
+ file_line.append_text(file_text)
252
+ if stats_text.plain:
253
+ file_line.append(" (")
254
+ file_line.append_text(stats_text)
255
+ file_line.append(")")
256
+
257
+ if file_diff.stats_add > 0 and file_diff.stats_remove == 0:
258
+ file_mark = "+"
259
+ elif file_diff.stats_remove > 0 and file_diff.stats_add == 0:
260
+ file_mark = "-"
261
+ else:
262
+ file_mark = "±"
263
+
264
+ prefix = Text(f"{file_mark:>{const.DIFF_PREFIX_WIDTH}} ", style=ThemeKey.DIFF_FILE_NAME)
265
+ return prefix, file_line
266
+
267
+
268
+ def _make_structured_prefix(line: model.DiffLine, width: int) -> str:
269
+ if line.kind == "gap":
270
+ return f"{'⋮':>{width}} "
271
+ number = " " * width
272
+ if line.kind in {"add", "ctx"} and line.new_line_no is not None:
273
+ number = f"{line.new_line_no:>{width}}"
274
+ marker = "+" if line.kind == "add" else "-" if line.kind == "remove" else " "
275
+ return f"{number} {marker}"
276
+
277
+
278
+ def _render_structured_line(line: model.DiffLine) -> Text:
279
+ if line.kind == "gap":
280
+ return Text("")
281
+ text = Text()
282
+ for span in line.spans:
283
+ text.append(span.text, style=_span_style(line.kind, span.op))
284
+ return text
285
+
286
+
287
+ def _span_style(line_kind: str, span_op: str) -> ThemeKey:
288
+ if line_kind == "add":
289
+ if span_op == "insert":
290
+ return ThemeKey.DIFF_ADD_CHAR
291
+ return ThemeKey.DIFF_ADD
292
+ if line_kind == "remove":
293
+ if span_op == "delete":
294
+ return ThemeKey.DIFF_REMOVE_CHAR
295
+ return ThemeKey.DIFF_REMOVE
296
+ return ThemeKey.TOOL_RESULT
@@ -197,28 +197,6 @@ def render_write_tool_call(arguments: str) -> RenderableType:
197
197
  return grid
198
198
 
199
199
 
200
- def render_multi_edit_tool_call(arguments: str) -> RenderableType:
201
- grid = create_grid()
202
- tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("MultiEdit", ThemeKey.TOOL_NAME))
203
- try:
204
- json_dict = json.loads(arguments)
205
- file_path = json_dict.get("file_path")
206
- edits = json_dict.get("edits", [])
207
- arguments_column = Text.assemble(
208
- render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH),
209
- Text(" - "),
210
- Text(f"{len(edits)}", ThemeKey.TOOL_PARAM_BOLD),
211
- Text(" updates", ThemeKey.TOOL_PARAM_FILE_PATH),
212
- )
213
- except json.JSONDecodeError:
214
- arguments_column = Text(
215
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
216
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
217
- )
218
- grid.add_row(tool_name_column, arguments_column)
219
- return grid
220
-
221
-
222
200
  def render_apply_patch_tool_call(arguments: str) -> RenderableType:
223
201
  grid = create_grid()
224
202
  tool_name_column = Text.assemble(("→", ThemeKey.TOOL_MARK), " ", ("Apply Patch", ThemeKey.TOOL_NAME))
@@ -300,60 +278,6 @@ def _extract_mermaid_link(
300
278
  return None
301
279
 
302
280
 
303
- def render_memory_tool_call(arguments: str) -> RenderableType:
304
- grid = create_grid()
305
- command_display_names: dict[str, str] = {
306
- "view": "View",
307
- "create": "Create",
308
- "str_replace": "Replace",
309
- "insert": "Insert",
310
- "delete": "Delete",
311
- "rename": "Rename",
312
- }
313
-
314
- try:
315
- payload: dict[str, str] = json.loads(arguments)
316
- except json.JSONDecodeError:
317
- tool_name_column = Text.assemble(("★", ThemeKey.TOOL_MARK), " ", ("Memory", ThemeKey.TOOL_NAME))
318
- summary = Text(
319
- arguments.strip()[: const.INVALID_TOOL_CALL_MAX_LENGTH],
320
- style=ThemeKey.INVALID_TOOL_CALL_ARGS,
321
- )
322
- grid.add_row(tool_name_column, summary)
323
- return grid
324
-
325
- command = payload.get("command", "")
326
- display_name = command_display_names.get(command, command.title())
327
- tool_name_column = Text.assemble(("★", ThemeKey.TOOL_MARK), " ", (f"{display_name} Memory", ThemeKey.TOOL_NAME))
328
-
329
- summary = Text("", ThemeKey.TOOL_PARAM)
330
- path = payload.get("path")
331
- old_path = payload.get("old_path")
332
- new_path = payload.get("new_path")
333
-
334
- if command == "rename" and old_path and new_path:
335
- summary = Text.assemble(
336
- Text(old_path, ThemeKey.TOOL_PARAM_FILE_PATH),
337
- Text(" -> ", ThemeKey.TOOL_PARAM),
338
- Text(new_path, ThemeKey.TOOL_PARAM_FILE_PATH),
339
- )
340
- elif command == "insert" and path:
341
- insert_line = payload.get("insert_line")
342
- summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
343
- if insert_line is not None:
344
- summary.append(f" line {insert_line}", ThemeKey.TOOL_PARAM)
345
- elif command == "view" and path:
346
- view_range = payload.get("view_range")
347
- summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
348
- if view_range and isinstance(view_range, list) and len(view_range) >= 2:
349
- summary.append(f" {view_range[0]}:{view_range[1]}", ThemeKey.TOOL_PARAM)
350
- elif path:
351
- summary = Text(path, ThemeKey.TOOL_PARAM_FILE_PATH)
352
-
353
- grid.add_row(tool_name_column, summary)
354
- return grid
355
-
356
-
357
281
  def render_mermaid_tool_call(arguments: str) -> RenderableType:
358
282
  grid = create_grid()
359
283
  tool_name_column = Text.assemble(("⧉", ThemeKey.TOOL_MARK), " ", ("Mermaid", ThemeKey.TOOL_NAME))
@@ -504,14 +428,12 @@ _TOOL_ACTIVE_FORM: dict[str, str] = {
504
428
  tools.BASH: "Bashing",
505
429
  tools.APPLY_PATCH: "Patching",
506
430
  tools.EDIT: "Editing",
507
- tools.MULTI_EDIT: "Editing",
508
431
  tools.READ: "Reading",
509
432
  tools.WRITE: "Writing",
510
433
  tools.TODO_WRITE: "Planning",
511
434
  tools.UPDATE_PLAN: "Planning",
512
435
  tools.SKILL: "Skilling",
513
436
  tools.MERMAID: "Diagramming",
514
- tools.MEMORY: "Memorizing",
515
437
  tools.WEB_FETCH: "Fetching Web",
516
438
  tools.WEB_SEARCH: "Searching Web",
517
439
  tools.REPORT_BACK: "Reporting",
@@ -552,8 +474,7 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
552
474
  return render_edit_tool_call(e.arguments)
553
475
  case tools.WRITE:
554
476
  return render_write_tool_call(e.arguments)
555
- case tools.MULTI_EDIT:
556
- return render_multi_edit_tool_call(e.arguments)
477
+
557
478
  case tools.BASH:
558
479
  return render_bash_tool_call(e.arguments)
559
480
  case tools.APPLY_PATCH:
@@ -564,8 +485,6 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
564
485
  return render_update_plan_tool_call(e.arguments)
565
486
  case tools.MERMAID:
566
487
  return render_mermaid_tool_call(e.arguments)
567
- case tools.MEMORY:
568
- return render_memory_tool_call(e.arguments)
569
488
  case tools.SKILL:
570
489
  return render_generic_tool_call(e.tool_name, e.arguments, "◈")
571
490
  case tools.REPORT_BACK:
@@ -578,9 +497,9 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
578
497
  return render_generic_tool_call(e.tool_name, e.arguments)
579
498
 
580
499
 
581
- def _extract_diff_text(ui_extra: model.ToolResultUIExtra | None) -> str | None:
582
- if isinstance(ui_extra, model.DiffTextUIExtra):
583
- return ui_extra.diff_text
500
+ def _extract_diff(ui_extra: model.ToolResultUIExtra | None) -> model.DiffUIExtra | None:
501
+ if isinstance(ui_extra, model.DiffUIExtra):
502
+ return ui_extra
584
503
  return None
585
504
 
586
505
 
@@ -604,28 +523,30 @@ def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
604
523
  if truncation_info:
605
524
  return Group(render_truncation_info(truncation_info), render_generic_tool_result(e.result))
606
525
 
607
- diff_text = _extract_diff_text(e.ui_extra)
526
+ diff_ui = _extract_diff(e.ui_extra)
608
527
 
609
528
  match e.tool_name:
610
529
  case tools.READ:
611
530
  return None
612
- case tools.EDIT | tools.MULTI_EDIT | tools.WRITE:
613
- return Padding.indent(r_diffs.render_diff(diff_text or ""), level=2)
614
- case tools.MEMORY:
615
- if diff_text:
616
- return Padding.indent(r_diffs.render_diff(diff_text), level=2)
617
- elif len(e.result.strip()) > 0:
618
- return render_generic_tool_result(e.result)
619
- return None
531
+ case tools.EDIT | tools.WRITE:
532
+ return Padding.indent(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""), level=2)
533
+ case tools.APPLY_PATCH:
534
+ if diff_ui:
535
+ return Padding.indent(r_diffs.render_structured_diff(diff_ui, show_file_name=True), level=2)
536
+ if len(e.result.strip()) == 0:
537
+ return render_generic_tool_result("(no content)")
538
+ return render_generic_tool_result(e.result)
620
539
  case tools.TODO_WRITE | tools.UPDATE_PLAN:
621
540
  return render_todo(e)
622
541
  case tools.MERMAID:
623
542
  return render_mermaid_tool_result(e)
624
- case _:
625
- if e.tool_name in (tools.BASH, tools.APPLY_PATCH) and e.result.startswith("diff --git"):
543
+ case tools.BASH:
544
+ if e.result.startswith("diff --git"):
626
545
  return r_diffs.render_diff_panel(e.result, show_file_name=True)
627
- if e.tool_name == tools.APPLY_PATCH and diff_text:
628
- return Padding.indent(r_diffs.render_diff(diff_text, show_file_name=True), level=2)
546
+ if len(e.result.strip()) == 0:
547
+ return render_generic_tool_result("(no content)")
548
+ return render_generic_tool_result(e.result)
549
+ case _:
629
550
  if len(e.result.strip()) == 0:
630
551
  return render_generic_tool_result("(no content)")
631
552
  return render_generic_tool_result(e.result)
@@ -9,7 +9,7 @@ from typing import Any, ClassVar
9
9
 
10
10
  from rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
11
11
  from rich.live import Live
12
- from rich.markdown import CodeBlock, Heading, Markdown
12
+ from rich.markdown import CodeBlock, Heading, HorizontalRule, Markdown
13
13
  from rich.rule import Rule
14
14
  from rich.spinner import Spinner
15
15
  from rich.style import Style
@@ -45,6 +45,14 @@ class ThinkingCodeBlock(CodeBlock):
45
45
  yield CodePanel(text, border_style="markdown.code.border")
46
46
 
47
47
 
48
+ class SpacedHorizontalRule(HorizontalRule):
49
+ """A horizontal rule with an extra blank line below."""
50
+
51
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
52
+ yield from super().__rich_console__(console, options)
53
+ yield Text()
54
+
55
+
48
56
  class LeftHeading(Heading):
49
57
  """A heading class that renders left-justified."""
50
58
 
@@ -69,6 +77,7 @@ class NoInsetMarkdown(Markdown):
69
77
  "fence": NoInsetCodeBlock,
70
78
  "code_block": NoInsetCodeBlock,
71
79
  "heading_open": LeftHeading,
80
+ "hr": SpacedHorizontalRule,
72
81
  }
73
82
 
74
83
 
@@ -80,6 +89,7 @@ class ThinkingMarkdown(Markdown):
80
89
  "fence": ThinkingCodeBlock,
81
90
  "code_block": ThinkingCodeBlock,
82
91
  "heading_open": LeftHeading,
92
+ "hr": SpacedHorizontalRule,
83
93
  }
84
94
 
85
95