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.
- klaude_code/cli/debug.py +8 -10
- klaude_code/cli/main.py +23 -0
- klaude_code/cli/runtime.py +13 -1
- klaude_code/command/__init__.py +0 -3
- klaude_code/command/prompt-deslop.md +1 -1
- klaude_code/command/thinking_cmd.py +10 -0
- klaude_code/const/__init__.py +2 -5
- klaude_code/core/prompt.py +5 -2
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/{prompt-codex-gpt-5-1.md → prompt-codex.md} +9 -42
- klaude_code/core/reminders.py +36 -2
- klaude_code/core/tool/__init__.py +0 -5
- klaude_code/core/tool/file/_utils.py +6 -0
- klaude_code/core/tool/file/apply_patch_tool.py +30 -72
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +35 -18
- klaude_code/core/tool/file/read_tool.py +45 -86
- klaude_code/core/tool/file/write_tool.py +40 -30
- klaude_code/core/tool/shell/bash_tool.py +151 -3
- klaude_code/core/tool/web/mermaid_tool.md +26 -0
- klaude_code/protocol/commands.py +0 -1
- klaude_code/protocol/model.py +29 -10
- klaude_code/protocol/tools.py +1 -2
- klaude_code/session/export.py +75 -20
- klaude_code/session/session.py +7 -0
- klaude_code/session/templates/export_session.html +28 -0
- klaude_code/ui/renderers/common.py +26 -11
- klaude_code/ui/renderers/developer.py +0 -5
- klaude_code/ui/renderers/diffs.py +84 -0
- klaude_code/ui/renderers/tools.py +19 -98
- klaude_code/ui/rich/markdown.py +11 -1
- klaude_code/ui/rich/status.py +8 -11
- klaude_code/ui/rich/theme.py +14 -4
- {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/METADATA +2 -1
- {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/RECORD +37 -40
- klaude_code/command/diff_cmd.py +0 -136
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -175
- klaude_code/core/tool/memory/memory_tool.md +0 -20
- klaude_code/core/tool/memory/memory_tool.py +0 -456
- {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/entry_points.txt +0 -0
klaude_code/protocol/model.py
CHANGED
|
@@ -70,9 +70,15 @@ class TodoItem(BaseModel):
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
class FileStatus(BaseModel):
|
|
73
|
-
"""Tracks file state including modification time and
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
klaude_code/protocol/tools.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
BASH = "Bash"
|
|
2
2
|
APPLY_PATCH = "apply_patch"
|
|
3
3
|
EDIT = "Edit"
|
|
4
|
-
|
|
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"
|
klaude_code/session/export.py
CHANGED
|
@@ -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:
|
|
366
|
-
lines = diff.splitlines()
|
|
365
|
+
def _render_diff_block(diff: model.DiffUIExtra) -> str:
|
|
367
366
|
rendered: list[str] = []
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
rendered.append(
|
|
374
|
-
|
|
375
|
-
rendered.append(
|
|
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"> </span>')
|
|
379
|
+
|
|
376
380
|
diff_content = f'<div class="diff-view">{"".join(rendered)}</div>'
|
|
377
|
-
open_attr = "" if _should_collapse(
|
|
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 ({
|
|
384
|
+
f"<summary>Diff ({line_count} lines)</summary>"
|
|
381
385
|
f"{diff_content}"
|
|
382
386
|
"</details>"
|
|
383
387
|
)
|
|
384
388
|
|
|
385
389
|
|
|
386
|
-
def
|
|
387
|
-
|
|
388
|
-
|
|
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 = " "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
540
|
-
items_to_render.append(_render_diff_block(
|
|
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)
|
klaude_code/session/session.py
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
lines = lines[:max_lines]
|
|
34
|
+
truncated_lines = 0
|
|
35
|
+
head_lines: list[str] = []
|
|
36
|
+
tail_lines: list[str] = []
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
60
|
-
out.append_text(Text(f"
|
|
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
|
-
|
|
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
|
|
582
|
-
if isinstance(ui_extra, model.
|
|
583
|
-
return ui_extra
|
|
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
|
-
|
|
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.
|
|
613
|
-
return Padding.indent(r_diffs.
|
|
614
|
-
case tools.
|
|
615
|
-
if
|
|
616
|
-
return Padding.indent(r_diffs.
|
|
617
|
-
|
|
618
|
-
return render_generic_tool_result(
|
|
619
|
-
return
|
|
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.
|
|
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.
|
|
628
|
-
return
|
|
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)
|
klaude_code/ui/rich/markdown.py
CHANGED
|
@@ -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
|
|