klaude-code 1.2.1__py3-none-any.whl → 1.2.3__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/main.py +9 -4
- klaude_code/cli/runtime.py +42 -43
- klaude_code/command/__init__.py +7 -5
- klaude_code/command/clear_cmd.py +6 -29
- klaude_code/command/command_abc.py +44 -8
- klaude_code/command/diff_cmd.py +33 -27
- klaude_code/command/export_cmd.py +18 -26
- klaude_code/command/help_cmd.py +10 -8
- klaude_code/command/model_cmd.py +11 -40
- klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
- klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
- klaude_code/command/prompt-init.md +2 -5
- klaude_code/command/prompt_command.py +6 -6
- klaude_code/command/refresh_cmd.py +4 -5
- klaude_code/command/registry.py +16 -19
- klaude_code/command/terminal_setup_cmd.py +12 -11
- klaude_code/config/__init__.py +4 -0
- klaude_code/config/config.py +25 -26
- klaude_code/config/list_model.py +8 -3
- klaude_code/config/select_model.py +1 -1
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/__init__.py +0 -3
- klaude_code/core/agent.py +25 -50
- klaude_code/core/executor.py +268 -101
- klaude_code/core/prompt.py +12 -12
- klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
- klaude_code/core/reminders.py +76 -95
- klaude_code/core/task.py +21 -14
- klaude_code/core/tool/__init__.py +45 -11
- klaude_code/core/tool/file/apply_patch.py +5 -1
- klaude_code/core/tool/file/apply_patch_tool.py +11 -13
- klaude_code/core/tool/file/edit_tool.py +27 -23
- klaude_code/core/tool/file/multi_edit_tool.py +15 -17
- klaude_code/core/tool/file/read_tool.py +41 -36
- klaude_code/core/tool/file/write_tool.py +13 -15
- klaude_code/core/tool/memory/memory_tool.py +85 -68
- klaude_code/core/tool/memory/skill_tool.py +10 -12
- klaude_code/core/tool/shell/bash_tool.py +24 -22
- klaude_code/core/tool/shell/command_safety.py +12 -1
- klaude_code/core/tool/sub_agent_tool.py +11 -12
- klaude_code/core/tool/todo/todo_write_tool.py +21 -28
- klaude_code/core/tool/todo/update_plan_tool.py +14 -24
- klaude_code/core/tool/tool_abc.py +3 -4
- klaude_code/core/tool/tool_context.py +7 -7
- klaude_code/core/tool/tool_registry.py +30 -47
- klaude_code/core/tool/tool_runner.py +35 -43
- klaude_code/core/tool/truncation.py +14 -20
- klaude_code/core/tool/web/mermaid_tool.py +12 -14
- klaude_code/core/tool/web/web_fetch_tool.py +15 -17
- klaude_code/core/turn.py +19 -7
- klaude_code/llm/__init__.py +3 -4
- klaude_code/llm/anthropic/client.py +30 -46
- klaude_code/llm/anthropic/input.py +4 -11
- klaude_code/llm/client.py +29 -8
- klaude_code/llm/input_common.py +66 -36
- klaude_code/llm/openai_compatible/client.py +42 -84
- klaude_code/llm/openai_compatible/input.py +11 -16
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
- klaude_code/llm/openrouter/client.py +40 -289
- klaude_code/llm/openrouter/input.py +13 -35
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +5 -75
- klaude_code/llm/responses/client.py +34 -55
- klaude_code/llm/responses/input.py +24 -26
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/events.py +3 -2
- klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
- klaude_code/protocol/model.py +49 -4
- klaude_code/protocol/op.py +18 -16
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/{core → protocol}/sub_agent.py +7 -0
- klaude_code/session/export.py +150 -70
- klaude_code/session/session.py +28 -14
- klaude_code/session/templates/export_session.html +180 -42
- klaude_code/trace/__init__.py +2 -2
- klaude_code/trace/log.py +11 -5
- klaude_code/ui/__init__.py +91 -8
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
- klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +0 -16
- klaude_code/ui/renderers/developer.py +18 -18
- klaude_code/ui/renderers/diffs.py +36 -14
- klaude_code/ui/renderers/errors.py +1 -1
- klaude_code/ui/renderers/metadata.py +50 -27
- klaude_code/ui/renderers/sub_agent.py +43 -9
- klaude_code/ui/renderers/thinking.py +33 -1
- klaude_code/ui/renderers/tools.py +212 -20
- klaude_code/ui/renderers/user_input.py +19 -23
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
- klaude_code/ui/{renderers → rich}/status.py +29 -18
- klaude_code/ui/{base → rich}/theme.py +8 -2
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
- klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
- klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
- klaude_code-1.2.3.dist-info/RECORD +161 -0
- klaude_code/core/clipboard_manifest.py +0 -124
- klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
- klaude_code/ui/base/__init__.py +0 -1
- klaude_code/ui/base/display_abc.py +0 -36
- klaude_code/ui/base/input_abc.py +0 -20
- klaude_code/ui/repl/display.py +0 -36
- klaude_code/ui/repl/event_handler.py +0 -247
- klaude_code/ui/repl/input.py +0 -773
- klaude_code/ui/rich_ext/__init__.py +0 -1
- klaude_code-1.2.1.dist-info/RECORD +0 -151
- /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
- /klaude_code/ui/{base → core}/stage_manager.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
- /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
- /klaude_code/ui/{base → utils}/debouncer.py +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
klaude_code/session/export.py
CHANGED
|
@@ -11,21 +11,8 @@ from pathlib import Path
|
|
|
11
11
|
from string import Template
|
|
12
12
|
from typing import TYPE_CHECKING, Any, Final, cast
|
|
13
13
|
|
|
14
|
-
from klaude_code.
|
|
15
|
-
from klaude_code.protocol.
|
|
16
|
-
from klaude_code.protocol.model import (
|
|
17
|
-
AssistantMessageItem,
|
|
18
|
-
ConversationItem,
|
|
19
|
-
DeveloperMessageItem,
|
|
20
|
-
ReasoningEncryptedItem,
|
|
21
|
-
ReasoningTextItem,
|
|
22
|
-
ResponseMetadataItem,
|
|
23
|
-
ToolCallItem,
|
|
24
|
-
ToolResultItem,
|
|
25
|
-
ToolResultUIExtra,
|
|
26
|
-
ToolResultUIExtraType,
|
|
27
|
-
UserMessageItem,
|
|
28
|
-
)
|
|
14
|
+
from klaude_code.protocol import llm_param, model
|
|
15
|
+
from klaude_code.protocol.sub_agent import is_sub_agent_tool
|
|
29
16
|
|
|
30
17
|
if TYPE_CHECKING:
|
|
31
18
|
from klaude_code.session.session import Session
|
|
@@ -58,10 +45,14 @@ def _format_timestamp(value: float | None) -> str:
|
|
|
58
45
|
return datetime.fromtimestamp(value).strftime("%Y-%m-%d %H:%M:%S")
|
|
59
46
|
|
|
60
47
|
|
|
61
|
-
def
|
|
48
|
+
def _format_msg_timestamp(dt: datetime) -> str:
|
|
49
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_first_user_message(history: list[model.ConversationItem]) -> str:
|
|
62
53
|
"""Extract the first user message content from conversation history."""
|
|
63
54
|
for item in history:
|
|
64
|
-
if isinstance(item, UserMessageItem) and item.content:
|
|
55
|
+
if isinstance(item, model.UserMessageItem) and item.content:
|
|
65
56
|
content = item.content.strip()
|
|
66
57
|
first_line = content.split("\n")[0]
|
|
67
58
|
return first_line[:100] if len(first_line) > 100 else first_line
|
|
@@ -86,7 +77,7 @@ def _load_template() -> str:
|
|
|
86
77
|
return template_file.read_text(encoding="utf-8")
|
|
87
78
|
|
|
88
79
|
|
|
89
|
-
def _build_tools_html(tools: list[ToolSchema]) -> str:
|
|
80
|
+
def _build_tools_html(tools: list[llm_param.ToolSchema]) -> str:
|
|
90
81
|
if not tools:
|
|
91
82
|
return '<div style="padding: 12px; font-style: italic;">No tools registered for this session.</div>'
|
|
92
83
|
chunks: list[str] = []
|
|
@@ -163,7 +154,11 @@ def _format_token_count(count: int) -> str:
|
|
|
163
154
|
return f"{m}M" if rem == 0 else f"{m}M{rem}k"
|
|
164
155
|
|
|
165
156
|
|
|
166
|
-
def
|
|
157
|
+
def _format_cost(cost: float) -> str:
|
|
158
|
+
return f"${cost:.4f}"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _render_metadata_item(item: model.ResponseMetadataItem) -> str:
|
|
167
162
|
# Line 1: Model Name [@ Provider]
|
|
168
163
|
model_parts = [f'<span class="metadata-model">{_escape_html(item.model_name)}</span>']
|
|
169
164
|
if item.provider:
|
|
@@ -176,10 +171,25 @@ def _render_metadata_item(item: ResponseMetadataItem) -> str:
|
|
|
176
171
|
stats_parts: list[str] = []
|
|
177
172
|
if item.usage:
|
|
178
173
|
u = item.usage
|
|
179
|
-
|
|
174
|
+
# Input with cost
|
|
175
|
+
input_stat = f"input: {_format_token_count(u.input_tokens)}"
|
|
176
|
+
if u.input_cost is not None:
|
|
177
|
+
input_stat += f"({_format_cost(u.input_cost)})"
|
|
178
|
+
stats_parts.append(f'<span class="metadata-stat">{input_stat}</span>')
|
|
179
|
+
|
|
180
|
+
# Cached with cost
|
|
180
181
|
if u.cached_tokens > 0:
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
cached_stat = f"cached: {_format_token_count(u.cached_tokens)}"
|
|
183
|
+
if u.cache_read_cost is not None:
|
|
184
|
+
cached_stat += f"({_format_cost(u.cache_read_cost)})"
|
|
185
|
+
stats_parts.append(f'<span class="metadata-stat">{cached_stat}</span>')
|
|
186
|
+
|
|
187
|
+
# Output with cost
|
|
188
|
+
output_stat = f"output: {_format_token_count(u.output_tokens)}"
|
|
189
|
+
if u.output_cost is not None:
|
|
190
|
+
output_stat += f"({_format_cost(u.output_cost)})"
|
|
191
|
+
stats_parts.append(f'<span class="metadata-stat">{output_stat}</span>')
|
|
192
|
+
|
|
183
193
|
if u.reasoning_tokens > 0:
|
|
184
194
|
stats_parts.append(
|
|
185
195
|
f'<span class="metadata-stat">thinking: {_format_token_count(u.reasoning_tokens)}</span>'
|
|
@@ -190,7 +200,11 @@ def _render_metadata_item(item: ResponseMetadataItem) -> str:
|
|
|
190
200
|
stats_parts.append(f'<span class="metadata-stat">tps: {u.throughput_tps:.1f}</span>')
|
|
191
201
|
|
|
192
202
|
if item.task_duration_s is not None:
|
|
193
|
-
stats_parts.append(f'<span class="metadata-stat">
|
|
203
|
+
stats_parts.append(f'<span class="metadata-stat">time: {item.task_duration_s:.1f}s</span>')
|
|
204
|
+
|
|
205
|
+
# Total cost
|
|
206
|
+
if item.usage is not None and item.usage.total_cost is not None:
|
|
207
|
+
stats_parts.append(f'<span class="metadata-stat">cost: {_format_cost(item.usage.total_cost)}</span>')
|
|
194
208
|
|
|
195
209
|
stats_html = ""
|
|
196
210
|
if stats_parts:
|
|
@@ -205,13 +219,15 @@ def _render_metadata_item(item: ResponseMetadataItem) -> str:
|
|
|
205
219
|
)
|
|
206
220
|
|
|
207
221
|
|
|
208
|
-
def _render_assistant_message(index: int, content: str) -> str:
|
|
222
|
+
def _render_assistant_message(index: int, content: str, timestamp: datetime) -> str:
|
|
209
223
|
encoded = _escape_html(content)
|
|
224
|
+
ts_str = _format_msg_timestamp(timestamp)
|
|
210
225
|
return (
|
|
211
226
|
f'<div class="message-group assistant-message-group">'
|
|
212
227
|
f'<div class="message-header">'
|
|
213
228
|
f'<div class="role-label assistant">Assistant</div>'
|
|
214
229
|
f'<div class="assistant-toolbar">'
|
|
230
|
+
f'<span class="timestamp">{ts_str}</span>'
|
|
215
231
|
f'<button type="button" class="raw-toggle" aria-pressed="false" title="Toggle raw text view">Raw</button>'
|
|
216
232
|
f'<button type="button" class="copy-raw-btn" title="Copy raw content">Copy</button>'
|
|
217
233
|
f"</div>"
|
|
@@ -289,9 +305,9 @@ def _render_text_block(text: str) -> str:
|
|
|
289
305
|
return (
|
|
290
306
|
f'<div class="expandable-output expandable">'
|
|
291
307
|
f'<div class="preview-text" style="white-space: pre-wrap; font-family: var(--font-mono);">{preview}</div>'
|
|
292
|
-
f'<div class="expand-hint expand-text">
|
|
308
|
+
f'<div class="expand-hint expand-text">click to expand full output ({len(lines)} lines)</div>'
|
|
293
309
|
f'<div class="full-text" style="white-space: pre-wrap; font-family: var(--font-mono);">{full}</div>'
|
|
294
|
-
f'<div class="collapse-hint">
|
|
310
|
+
f'<div class="collapse-hint">click to collapse</div>'
|
|
295
311
|
f"</div>"
|
|
296
312
|
)
|
|
297
313
|
|
|
@@ -326,49 +342,85 @@ def _render_diff_block(diff: str) -> str:
|
|
|
326
342
|
)
|
|
327
343
|
|
|
328
344
|
|
|
329
|
-
def _get_diff_text(ui_extra: ToolResultUIExtra | None) -> str | None:
|
|
345
|
+
def _get_diff_text(ui_extra: model.ToolResultUIExtra | None) -> str | None:
|
|
330
346
|
if ui_extra is None:
|
|
331
347
|
return None
|
|
332
|
-
if ui_extra.type != ToolResultUIExtraType.DIFF_TEXT:
|
|
348
|
+
if ui_extra.type != model.ToolResultUIExtraType.DIFF_TEXT:
|
|
333
349
|
return None
|
|
334
350
|
return ui_extra.diff_text
|
|
335
351
|
|
|
336
352
|
|
|
337
|
-
def _get_mermaid_link_html(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if ui_extra.type != ToolResultUIExtraType.MERMAID_LINK:
|
|
341
|
-
return None
|
|
342
|
-
if ui_extra.mermaid_link is None or not ui_extra.mermaid_link.link:
|
|
343
|
-
return None
|
|
344
|
-
link = _escape_html(ui_extra.mermaid_link.link)
|
|
345
|
-
lines = ui_extra.mermaid_link.line_count
|
|
346
|
-
|
|
347
|
-
copy_btn = ""
|
|
353
|
+
def _get_mermaid_link_html(
|
|
354
|
+
ui_extra: model.ToolResultUIExtra | None, tool_call: model.ToolCallItem | None = None
|
|
355
|
+
) -> str | None:
|
|
348
356
|
if tool_call and tool_call.name == "Mermaid":
|
|
349
357
|
try:
|
|
350
358
|
args = json.loads(tool_call.arguments)
|
|
351
|
-
code = args.get("code")
|
|
352
|
-
if code:
|
|
353
|
-
escaped_code = _escape_html(code)
|
|
354
|
-
copy_btn = f'<button type="button" class="copy-mermaid-btn" data-code="{escaped_code}" title="Copy Mermaid Code">Copy Code</button>'
|
|
359
|
+
code = args.get("code", "")
|
|
355
360
|
except Exception:
|
|
356
|
-
|
|
361
|
+
code = ""
|
|
362
|
+
else:
|
|
363
|
+
code = ""
|
|
357
364
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
+
if not code and (
|
|
366
|
+
ui_extra is None or ui_extra.type != model.ToolResultUIExtraType.MERMAID_LINK or not ui_extra.mermaid_link
|
|
367
|
+
):
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
# Prepare code for rendering and copy
|
|
371
|
+
escaped_code = _escape_html(code) if code else ""
|
|
372
|
+
line_count = code.count("\n") + 1 if code else 0
|
|
373
|
+
|
|
374
|
+
# Build Toolbar
|
|
375
|
+
toolbar_items: list[str] = []
|
|
376
|
+
|
|
377
|
+
if line_count > 0:
|
|
378
|
+
toolbar_items.append(f"<span>Lines: {line_count}</span>")
|
|
379
|
+
|
|
380
|
+
buttons_html: list[str] = []
|
|
381
|
+
if code:
|
|
382
|
+
buttons_html.append(
|
|
383
|
+
f'<button type="button" class="copy-mermaid-btn" data-code="{escaped_code}" title="Copy Mermaid Code">Copy Code</button>'
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
link = (
|
|
387
|
+
ui_extra.mermaid_link.link
|
|
388
|
+
if (ui_extra and ui_extra.type == model.ToolResultUIExtraType.MERMAID_LINK and ui_extra.mermaid_link)
|
|
389
|
+
else None
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if link:
|
|
393
|
+
link_url = _escape_html(link)
|
|
394
|
+
buttons_html.append(
|
|
395
|
+
f'<a href="{link_url}" target="_blank" rel="noopener noreferrer" style="color: var(--accent); text-decoration: underline; margin-left: 8px;">View Online</a>'
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
toolbar_items.append(f"<div>{''.join(buttons_html)}</div>")
|
|
399
|
+
|
|
400
|
+
toolbar_html = (
|
|
401
|
+
'<div style="display: flex; justify-content: space-between; align-items: center; font-family: var(--font-mono); margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border);">'
|
|
402
|
+
f"{''.join(toolbar_items)}"
|
|
365
403
|
"</div>"
|
|
366
404
|
)
|
|
367
405
|
|
|
406
|
+
# If we have code, render the diagram
|
|
407
|
+
if code:
|
|
408
|
+
return (
|
|
409
|
+
f'<div style="background: white; padding: 16px; border-radius: 4px; margin-top: 8px; border: 1px solid var(--border);">'
|
|
410
|
+
f'<div class="mermaid">{escaped_code}</div>'
|
|
411
|
+
f"{toolbar_html}"
|
|
412
|
+
f"</div>"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Fallback to just link/toolbar if no code available (legacy support behavior)
|
|
416
|
+
return toolbar_html
|
|
417
|
+
|
|
368
418
|
|
|
369
|
-
def _format_tool_call(tool_call: ToolCallItem, result: ToolResultItem | None) -> str:
|
|
419
|
+
def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultItem | None) -> str:
|
|
370
420
|
args_html = None
|
|
371
421
|
is_todo_list = False
|
|
422
|
+
ts_str = _format_msg_timestamp(tool_call.created_at)
|
|
423
|
+
|
|
372
424
|
if tool_call.name == "TodoWrite":
|
|
373
425
|
args_html = _try_render_todo_args(tool_call.arguments)
|
|
374
426
|
if args_html:
|
|
@@ -390,7 +442,21 @@ def _format_tool_call(tool_call: ToolCallItem, result: ToolResultItem | None) ->
|
|
|
390
442
|
if is_todo_list:
|
|
391
443
|
args_section = f'<div class="tool-args">{args_html}</div>'
|
|
392
444
|
else:
|
|
393
|
-
|
|
445
|
+
# Always collapse Mermaid, Edit, Write tools by default
|
|
446
|
+
always_collapse_tools = {"Mermaid", "Edit", "Write"}
|
|
447
|
+
force_collapse = tool_call.name in always_collapse_tools
|
|
448
|
+
|
|
449
|
+
# Collapse Memory tool for write operations
|
|
450
|
+
if tool_call.name == "Memory":
|
|
451
|
+
try:
|
|
452
|
+
parsed_args = json.loads(tool_call.arguments)
|
|
453
|
+
if parsed_args.get("command") in {"create", "str_replace", "insert"}:
|
|
454
|
+
force_collapse = True
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
should_collapse = force_collapse or _should_collapse(args_html)
|
|
459
|
+
open_attr = "" if should_collapse else " open"
|
|
394
460
|
args_section = (
|
|
395
461
|
f'<details class="tool-args-collapsible"{open_attr}>'
|
|
396
462
|
"<summary>Arguments</summary>"
|
|
@@ -402,7 +468,10 @@ def _format_tool_call(tool_call: ToolCallItem, result: ToolResultItem | None) ->
|
|
|
402
468
|
'<div class="tool-call">',
|
|
403
469
|
'<div class="tool-header">',
|
|
404
470
|
f'<span class="tool-name">{_escape_html(tool_call.name)}</span>',
|
|
471
|
+
'<div class="tool-header-right">',
|
|
405
472
|
f'<span class="tool-id">{_escape_html(tool_call.call_id)}</span>',
|
|
473
|
+
f'<span class="timestamp">{ts_str}</span>',
|
|
474
|
+
"</div>",
|
|
406
475
|
"</div>",
|
|
407
476
|
args_section,
|
|
408
477
|
]
|
|
@@ -453,46 +522,57 @@ def _format_tool_call(tool_call: ToolCallItem, result: ToolResultItem | None) ->
|
|
|
453
522
|
|
|
454
523
|
|
|
455
524
|
def _build_messages_html(
|
|
456
|
-
history: list[ConversationItem],
|
|
457
|
-
tool_results: dict[str, ToolResultItem],
|
|
525
|
+
history: list[model.ConversationItem],
|
|
526
|
+
tool_results: dict[str, model.ToolResultItem],
|
|
458
527
|
) -> str:
|
|
459
528
|
blocks: list[str] = []
|
|
460
529
|
assistant_counter = 0
|
|
461
530
|
|
|
462
|
-
renderable_items = [
|
|
531
|
+
renderable_items = [
|
|
532
|
+
item for item in history if not isinstance(item, (model.ToolResultItem, model.ReasoningEncryptedItem))
|
|
533
|
+
]
|
|
463
534
|
|
|
464
535
|
for i, item in enumerate(renderable_items):
|
|
465
|
-
if isinstance(item, UserMessageItem):
|
|
536
|
+
if isinstance(item, model.UserMessageItem):
|
|
466
537
|
text = _escape_html(item.content or "")
|
|
538
|
+
ts_str = _format_msg_timestamp(item.created_at)
|
|
467
539
|
blocks.append(
|
|
468
540
|
f'<div class="message-group">'
|
|
469
|
-
f'<div class="role-label user">
|
|
541
|
+
f'<div class="role-label user">'
|
|
542
|
+
f"User"
|
|
543
|
+
f'<span class="timestamp">{ts_str}</span>'
|
|
544
|
+
f"</div>"
|
|
470
545
|
f'<div class="message-content user" style="white-space: pre-wrap;">{text}</div>'
|
|
471
546
|
f"</div>"
|
|
472
547
|
)
|
|
473
|
-
elif isinstance(item, ReasoningTextItem):
|
|
548
|
+
elif isinstance(item, model.ReasoningTextItem):
|
|
474
549
|
text = _escape_html(item.content.strip())
|
|
475
|
-
blocks.append(f'<div class="thinking-block"
|
|
476
|
-
elif isinstance(item, AssistantMessageItem):
|
|
550
|
+
blocks.append(f'<div class="thinking-block markdown-body markdown-content" data-raw="{text}"></div>')
|
|
551
|
+
elif isinstance(item, model.AssistantMessageItem):
|
|
477
552
|
assistant_counter += 1
|
|
478
|
-
blocks.append(_render_assistant_message(assistant_counter, item.content or ""))
|
|
479
|
-
elif isinstance(item, ResponseMetadataItem):
|
|
553
|
+
blocks.append(_render_assistant_message(assistant_counter, item.content or "", item.created_at))
|
|
554
|
+
elif isinstance(item, model.ResponseMetadataItem):
|
|
480
555
|
blocks.append(_render_metadata_item(item))
|
|
481
|
-
elif isinstance(item, DeveloperMessageItem):
|
|
556
|
+
elif isinstance(item, model.DeveloperMessageItem):
|
|
482
557
|
content = _escape_html(item.content or "")
|
|
558
|
+
ts_str = _format_msg_timestamp(item.created_at)
|
|
483
559
|
|
|
484
560
|
next_item = renderable_items[i + 1] if i + 1 < len(renderable_items) else None
|
|
485
561
|
extra_class = ""
|
|
486
|
-
if isinstance(next_item, (UserMessageItem, AssistantMessageItem)):
|
|
562
|
+
if isinstance(next_item, (model.UserMessageItem, model.AssistantMessageItem)):
|
|
487
563
|
extra_class = " gap-below"
|
|
488
564
|
|
|
489
565
|
blocks.append(
|
|
490
566
|
f'<details class="developer-message{extra_class}">'
|
|
491
|
-
f"<summary>
|
|
567
|
+
f"<summary>"
|
|
568
|
+
f"Developer"
|
|
569
|
+
f'<span class="timestamp">{ts_str}</span>'
|
|
570
|
+
f"</summary>"
|
|
492
571
|
f'<div class="details-content" style="white-space: pre-wrap;">{content}</div>'
|
|
493
572
|
f"</details>"
|
|
494
573
|
)
|
|
495
|
-
|
|
574
|
+
|
|
575
|
+
elif isinstance(item, model.ToolCallItem):
|
|
496
576
|
result = tool_results.get(item.call_id)
|
|
497
577
|
blocks.append(_format_tool_call(item, result))
|
|
498
578
|
|
|
@@ -502,7 +582,7 @@ def _build_messages_html(
|
|
|
502
582
|
def build_export_html(
|
|
503
583
|
session: Session,
|
|
504
584
|
system_prompt: str,
|
|
505
|
-
tools: list[ToolSchema],
|
|
585
|
+
tools: list[llm_param.ToolSchema],
|
|
506
586
|
model_name: str,
|
|
507
587
|
) -> str:
|
|
508
588
|
"""Build HTML export for a session.
|
|
@@ -517,7 +597,7 @@ def build_export_html(
|
|
|
517
597
|
Complete HTML document as a string.
|
|
518
598
|
"""
|
|
519
599
|
history = session.conversation_history
|
|
520
|
-
tool_results = {item.call_id: item for item in history if isinstance(item, ToolResultItem)}
|
|
600
|
+
tool_results = {item.call_id: item for item in history if isinstance(item, model.ToolResultItem)}
|
|
521
601
|
messages_html = _build_messages_html(history, tool_results)
|
|
522
602
|
if not messages_html:
|
|
523
603
|
messages_html = '<div class="text-dim p-4 italic">No messages recorded for this session yet.</div>'
|
|
@@ -526,7 +606,7 @@ def build_export_html(
|
|
|
526
606
|
session_id = session.id
|
|
527
607
|
session_updated = _format_timestamp(session.updated_at)
|
|
528
608
|
work_dir = _shorten_path(str(session.work_dir))
|
|
529
|
-
total_messages = len([item for item in history if not isinstance(item, ToolResultItem)])
|
|
609
|
+
total_messages = len([item for item in history if not isinstance(item, model.ToolResultItem)])
|
|
530
610
|
footer_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
531
611
|
first_user_message = get_first_user_message(history)
|
|
532
612
|
|
klaude_code/session/session.py
CHANGED
|
@@ -8,18 +8,17 @@ from typing import ClassVar
|
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
10
|
from klaude_code.protocol import events, model
|
|
11
|
-
from klaude_code.protocol.model import ConversationItem, SubAgentState, TodoItem
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
class Session(BaseModel):
|
|
15
14
|
id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
|
16
15
|
work_dir: Path
|
|
17
|
-
conversation_history: list[ConversationItem] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
18
|
-
sub_agent_state: SubAgentState | None = None
|
|
16
|
+
conversation_history: list[model.ConversationItem] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
17
|
+
sub_agent_state: model.SubAgentState | None = None
|
|
19
18
|
# FileTracker: track file path -> last modification time when last read/edited
|
|
20
19
|
file_tracker: dict[str, float] = Field(default_factory=dict)
|
|
21
20
|
# Todo list for the session
|
|
22
|
-
todos: list[TodoItem] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
21
|
+
todos: list[model.TodoItem] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
23
22
|
# Messages count, redundant state for performance optimization to avoid reading entire jsonl file
|
|
24
23
|
messages_count: int = Field(default=0)
|
|
25
24
|
# Model name used for this session
|
|
@@ -88,7 +87,11 @@ class Session(BaseModel):
|
|
|
88
87
|
def load(cls, id: str) -> "Session":
|
|
89
88
|
# Load session metadata
|
|
90
89
|
sessions_dir = cls._sessions_dir()
|
|
91
|
-
session_candidates = sorted(
|
|
90
|
+
session_candidates = sorted(
|
|
91
|
+
sessions_dir.glob(f"*-{id}.json"),
|
|
92
|
+
key=lambda p: p.stat().st_mtime,
|
|
93
|
+
reverse=True,
|
|
94
|
+
)
|
|
92
95
|
if not session_candidates:
|
|
93
96
|
# No existing session; create a new one
|
|
94
97
|
return Session(id=id, work_dir=Path.cwd())
|
|
@@ -100,9 +103,9 @@ class Session(BaseModel):
|
|
|
100
103
|
work_dir_str = raw.get("work_dir", str(Path.cwd()))
|
|
101
104
|
|
|
102
105
|
sub_agent_state_raw = raw.get("sub_agent_state")
|
|
103
|
-
sub_agent_state = SubAgentState(**sub_agent_state_raw) if sub_agent_state_raw else None
|
|
106
|
+
sub_agent_state = model.SubAgentState(**sub_agent_state_raw) if sub_agent_state_raw else None
|
|
104
107
|
file_tracker = dict(raw.get("file_tracker", {}))
|
|
105
|
-
todos: list[TodoItem] = [TodoItem(**item) for item in raw.get("todos", [])]
|
|
108
|
+
todos: list[model.TodoItem] = [model.TodoItem(**item) for item in raw.get("todos", [])]
|
|
106
109
|
loaded_memory = list(raw.get("loaded_memory", []))
|
|
107
110
|
created_at = float(raw.get("created_at", time.time()))
|
|
108
111
|
updated_at = float(raw.get("updated_at", created_at))
|
|
@@ -125,10 +128,14 @@ class Session(BaseModel):
|
|
|
125
128
|
# Load conversation history from messages JSONL
|
|
126
129
|
messages_dir = cls._messages_dir()
|
|
127
130
|
# Expect a single messages file per session (prefixed filenames only)
|
|
128
|
-
msg_candidates = sorted(
|
|
131
|
+
msg_candidates = sorted(
|
|
132
|
+
messages_dir.glob(f"*-{id}.jsonl"),
|
|
133
|
+
key=lambda p: p.stat().st_mtime,
|
|
134
|
+
reverse=True,
|
|
135
|
+
)
|
|
129
136
|
if msg_candidates:
|
|
130
137
|
messages_path = msg_candidates[0]
|
|
131
|
-
history: list[ConversationItem] = []
|
|
138
|
+
history: list[model.ConversationItem] = []
|
|
132
139
|
for line in messages_path.read_text().splitlines():
|
|
133
140
|
line = line.strip()
|
|
134
141
|
if not line:
|
|
@@ -180,7 +187,7 @@ class Session(BaseModel):
|
|
|
180
187
|
}
|
|
181
188
|
self._session_file().write_text(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
182
189
|
|
|
183
|
-
def append_history(self, items: Sequence[ConversationItem]):
|
|
190
|
+
def append_history(self, items: Sequence[model.ConversationItem]):
|
|
184
191
|
# Append to in-memory history
|
|
185
192
|
self.conversation_history.extend(items)
|
|
186
193
|
# Update messages count (only UserMessageItem and AssistantMessageItem)
|
|
@@ -197,7 +204,7 @@ class Session(BaseModel):
|
|
|
197
204
|
for it in items:
|
|
198
205
|
# Serialize with explicit type tag for reliable load
|
|
199
206
|
t = it.__class__.__name__
|
|
200
|
-
data = it.model_dump()
|
|
207
|
+
data = it.model_dump(mode="json")
|
|
201
208
|
f.write(json.dumps({"type": t, "data": data}, ensure_ascii=False))
|
|
202
209
|
f.write("\n")
|
|
203
210
|
# Refresh metadata timestamp after history change
|
|
@@ -236,7 +243,10 @@ class Session(BaseModel):
|
|
|
236
243
|
return False
|
|
237
244
|
if prev_item is None:
|
|
238
245
|
return True
|
|
239
|
-
if isinstance(
|
|
246
|
+
if isinstance(
|
|
247
|
+
prev_item,
|
|
248
|
+
model.UserMessageItem | model.ToolResultItem | model.DeveloperMessageItem,
|
|
249
|
+
):
|
|
240
250
|
return True
|
|
241
251
|
return False
|
|
242
252
|
|
|
@@ -337,7 +347,9 @@ class Session(BaseModel):
|
|
|
337
347
|
if not msg_file.exists():
|
|
338
348
|
# Try to find by pattern if exact file doesn't exist
|
|
339
349
|
msg_candidates = sorted(
|
|
340
|
-
messages_dir.glob(f"*-{session_id}.jsonl"),
|
|
350
|
+
messages_dir.glob(f"*-{session_id}.jsonl"),
|
|
351
|
+
key=lambda p: p.stat().st_mtime,
|
|
352
|
+
reverse=True,
|
|
341
353
|
)
|
|
342
354
|
if not msg_candidates:
|
|
343
355
|
return None
|
|
@@ -358,7 +370,9 @@ class Session(BaseModel):
|
|
|
358
370
|
# Handle structured content - extract text
|
|
359
371
|
text_parts: list[str] = []
|
|
360
372
|
for part in content: # pyright: ignore[reportUnknownVariableType]
|
|
361
|
-
if
|
|
373
|
+
if (
|
|
374
|
+
isinstance(part, dict) and part.get("type") == "text" # pyright: ignore[reportUnknownMemberType]
|
|
375
|
+
):
|
|
362
376
|
text = part.get("text", "") # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
|
|
363
377
|
if isinstance(text, str):
|
|
364
378
|
text_parts.append(text)
|