klaude-code 1.2.6__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
klaude_code/session/export.py
CHANGED
|
@@ -63,7 +63,7 @@ def get_default_export_path(session: Session) -> Path:
|
|
|
63
63
|
"""Get default export path for a session."""
|
|
64
64
|
from klaude_code.session.session import Session as SessionClass
|
|
65
65
|
|
|
66
|
-
exports_dir = SessionClass.
|
|
66
|
+
exports_dir = SessionClass.exports_dir()
|
|
67
67
|
timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
|
|
68
68
|
first_msg = get_first_user_message(session.conversation_history)
|
|
69
69
|
sanitized_msg = _sanitize_filename(first_msg)
|
|
@@ -154,67 +154,94 @@ def _format_token_count(count: int) -> str:
|
|
|
154
154
|
return f"{m}M" if rem == 0 else f"{m}M{rem}k"
|
|
155
155
|
|
|
156
156
|
|
|
157
|
-
def _format_cost(cost: float) -> str:
|
|
158
|
-
|
|
157
|
+
def _format_cost(cost: float, currency: str = "USD") -> str:
|
|
158
|
+
symbol = "¥" if currency == "CNY" else "$"
|
|
159
|
+
return f"{symbol}{cost:.4f}"
|
|
159
160
|
|
|
160
161
|
|
|
161
|
-
def
|
|
162
|
-
|
|
162
|
+
def _render_single_metadata(
|
|
163
|
+
metadata: model.TaskMetadata,
|
|
164
|
+
*,
|
|
165
|
+
indent: int = 0,
|
|
166
|
+
show_context: bool = True,
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Render a single TaskMetadata block as HTML.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
metadata: The TaskMetadata to render.
|
|
172
|
+
indent: Number of spaces to indent (0 for main, 2 for sub-agents).
|
|
173
|
+
show_context: Whether to show context usage percent.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
HTML string for this metadata block.
|
|
177
|
+
"""
|
|
163
178
|
parts: list[str] = []
|
|
164
179
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
# Model Name [@ Provider]
|
|
181
|
+
model_parts = [f'<span class="metadata-model">{_escape_html(metadata.model_name)}</span>']
|
|
182
|
+
if metadata.provider:
|
|
183
|
+
provider = _escape_html(metadata.provider.lower().replace(" ", "-"))
|
|
168
184
|
model_parts.append(f'<span class="metadata-provider">@{provider}</span>')
|
|
169
185
|
|
|
170
186
|
parts.append("".join(model_parts))
|
|
171
187
|
|
|
172
188
|
# Stats
|
|
173
|
-
if
|
|
174
|
-
u =
|
|
189
|
+
if metadata.usage:
|
|
190
|
+
u = metadata.usage
|
|
175
191
|
# Input with cost
|
|
176
192
|
input_stat = f"input: {_format_token_count(u.input_tokens)}"
|
|
177
193
|
if u.input_cost is not None:
|
|
178
|
-
input_stat += f"({_format_cost(u.input_cost)})"
|
|
194
|
+
input_stat += f"({_format_cost(u.input_cost, u.currency)})"
|
|
179
195
|
parts.append(f'<span class="metadata-stat">{input_stat}</span>')
|
|
180
196
|
|
|
181
197
|
# Cached with cost
|
|
182
198
|
if u.cached_tokens > 0:
|
|
183
199
|
cached_stat = f"cached: {_format_token_count(u.cached_tokens)}"
|
|
184
200
|
if u.cache_read_cost is not None:
|
|
185
|
-
cached_stat += f"({_format_cost(u.cache_read_cost)})"
|
|
201
|
+
cached_stat += f"({_format_cost(u.cache_read_cost, u.currency)})"
|
|
186
202
|
parts.append(f'<span class="metadata-stat">{cached_stat}</span>')
|
|
187
203
|
|
|
188
204
|
# Output with cost
|
|
189
205
|
output_stat = f"output: {_format_token_count(u.output_tokens)}"
|
|
190
206
|
if u.output_cost is not None:
|
|
191
|
-
output_stat += f"({_format_cost(u.output_cost)})"
|
|
207
|
+
output_stat += f"({_format_cost(u.output_cost, u.currency)})"
|
|
192
208
|
parts.append(f'<span class="metadata-stat">{output_stat}</span>')
|
|
193
209
|
|
|
194
210
|
if u.reasoning_tokens > 0:
|
|
195
|
-
parts.append(
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
if u.context_usage_percent is not None:
|
|
211
|
+
parts.append(f'<span class="metadata-stat">thinking: {_format_token_count(u.reasoning_tokens)}</span>')
|
|
212
|
+
if show_context and u.context_usage_percent is not None:
|
|
199
213
|
parts.append(f'<span class="metadata-stat">context: {u.context_usage_percent:.1f}%</span>')
|
|
200
214
|
if u.throughput_tps is not None:
|
|
201
215
|
parts.append(f'<span class="metadata-stat">tps: {u.throughput_tps:.1f}</span>')
|
|
202
216
|
|
|
203
|
-
if
|
|
204
|
-
parts.append(f'<span class="metadata-stat">time: {
|
|
217
|
+
if metadata.task_duration_s is not None:
|
|
218
|
+
parts.append(f'<span class="metadata-stat">time: {metadata.task_duration_s:.1f}s</span>')
|
|
205
219
|
|
|
206
220
|
# Total cost
|
|
207
|
-
if
|
|
208
|
-
parts.append(
|
|
221
|
+
if metadata.usage is not None and metadata.usage.total_cost is not None:
|
|
222
|
+
parts.append(
|
|
223
|
+
f'<span class="metadata-stat">cost: {_format_cost(metadata.usage.total_cost, metadata.usage.currency)}</span>'
|
|
224
|
+
)
|
|
209
225
|
|
|
210
226
|
divider = '<span class="metadata-divider">/</span>'
|
|
211
227
|
joined_html = divider.join(parts)
|
|
212
228
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
229
|
+
indent_style = f' style="padding-left: {indent}em;"' if indent > 0 else ""
|
|
230
|
+
return f'<div class="metadata-line"{indent_style}>{joined_html}</div>'
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _render_metadata_item(item: model.TaskMetadataItem) -> str:
|
|
234
|
+
"""Render TaskMetadataItem including main agent and sub-agents."""
|
|
235
|
+
lines: list[str] = []
|
|
236
|
+
|
|
237
|
+
# Main agent metadata
|
|
238
|
+
lines.append(_render_single_metadata(item.main_agent, indent=0, show_context=True))
|
|
239
|
+
|
|
240
|
+
# Sub-agent metadata with indent
|
|
241
|
+
for sub in item.sub_agent_task_metadata:
|
|
242
|
+
lines.append(_render_single_metadata(sub, indent=1, show_context=False))
|
|
243
|
+
|
|
244
|
+
return f'<div class="response-metadata">{"".join(lines)}</div>'
|
|
218
245
|
|
|
219
246
|
|
|
220
247
|
def _render_assistant_message(index: int, content: str, timestamp: datetime) -> str:
|
|
@@ -240,20 +267,30 @@ def _render_assistant_message(index: int, content: str, timestamp: datetime) ->
|
|
|
240
267
|
)
|
|
241
268
|
|
|
242
269
|
|
|
243
|
-
def _try_render_todo_args(arguments: str) -> str | None:
|
|
270
|
+
def _try_render_todo_args(arguments: str, tool_name: str) -> str | None:
|
|
244
271
|
try:
|
|
245
272
|
parsed = json.loads(arguments)
|
|
246
|
-
if not isinstance(parsed, dict)
|
|
273
|
+
if not isinstance(parsed, dict):
|
|
247
274
|
return None
|
|
248
275
|
|
|
249
|
-
|
|
250
|
-
|
|
276
|
+
# Support both TodoWrite (todos/content) and update_plan (plan/step)
|
|
277
|
+
parsed_dict = cast(dict[str, Any], parsed)
|
|
278
|
+
if tool_name == "TodoWrite":
|
|
279
|
+
items = parsed_dict.get("todos")
|
|
280
|
+
content_key = "content"
|
|
281
|
+
elif tool_name == "update_plan":
|
|
282
|
+
items = parsed_dict.get("plan")
|
|
283
|
+
content_key = "step"
|
|
284
|
+
else:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
if not isinstance(items, list) or not items:
|
|
251
288
|
return None
|
|
252
289
|
|
|
253
290
|
items_html: list[str] = []
|
|
254
|
-
for
|
|
255
|
-
content = _escape_html(
|
|
256
|
-
status =
|
|
291
|
+
for item in cast(list[dict[str, str]], items):
|
|
292
|
+
content = _escape_html(item.get(content_key, ""))
|
|
293
|
+
status = item.get("status", "pending")
|
|
257
294
|
status_class = f"status-{status}"
|
|
258
295
|
|
|
259
296
|
items_html.append(
|
|
@@ -267,23 +304,33 @@ def _try_render_todo_args(arguments: str) -> str | None:
|
|
|
267
304
|
return None
|
|
268
305
|
|
|
269
306
|
return f'<div class="todo-list">{"".join(items_html)}</div>'
|
|
270
|
-
except
|
|
307
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
271
308
|
return None
|
|
272
309
|
|
|
273
310
|
|
|
274
|
-
def _render_sub_agent_result(content: str) -> str:
|
|
275
|
-
|
|
311
|
+
def _render_sub_agent_result(content: str, description: str | None = None) -> str:
|
|
312
|
+
# Try to format as JSON for better readability
|
|
313
|
+
try:
|
|
314
|
+
parsed = json.loads(content)
|
|
315
|
+
formatted = "```json\n" + json.dumps(parsed, ensure_ascii=False, indent=2) + "\n```"
|
|
316
|
+
except (json.JSONDecodeError, TypeError):
|
|
317
|
+
formatted = content
|
|
318
|
+
|
|
319
|
+
if description:
|
|
320
|
+
formatted = f"# {description}\n\n{formatted}"
|
|
321
|
+
|
|
322
|
+
encoded = _escape_html(formatted)
|
|
276
323
|
return (
|
|
277
|
-
f'<div class="
|
|
278
|
-
f'<div class="
|
|
324
|
+
f'<div class="sub-agent-result-container">'
|
|
325
|
+
f'<div class="sub-agent-toolbar">'
|
|
279
326
|
f'<button type="button" class="raw-toggle" aria-pressed="false" title="Toggle raw text view">Raw</button>'
|
|
280
327
|
f'<button type="button" class="copy-raw-btn" title="Copy raw content">Copy</button>'
|
|
281
328
|
f"</div>"
|
|
282
|
-
f'<div class="
|
|
283
|
-
f'<div class="
|
|
329
|
+
f'<div class="sub-agent-content">'
|
|
330
|
+
f'<div class="sub-agent-rendered markdown-content markdown-body" data-raw="{encoded}">'
|
|
284
331
|
f'<noscript><pre style="white-space: pre-wrap;">{encoded}</pre></noscript>'
|
|
285
332
|
f"</div>"
|
|
286
|
-
f'<pre class="
|
|
333
|
+
f'<pre class="sub-agent-raw">{encoded}</pre>'
|
|
287
334
|
f"</div>"
|
|
288
335
|
f"</div>"
|
|
289
336
|
)
|
|
@@ -319,55 +366,141 @@ def _should_collapse(text: str) -> bool:
|
|
|
319
366
|
return text.count("\n") + 1 > _COLLAPSIBLE_LINE_THRESHOLD or len(text) > _COLLAPSIBLE_CHAR_THRESHOLD
|
|
320
367
|
|
|
321
368
|
|
|
322
|
-
def _render_diff_block(diff:
|
|
323
|
-
lines = diff.splitlines()
|
|
369
|
+
def _render_diff_block(diff: model.DiffUIExtra) -> str:
|
|
324
370
|
rendered: list[str] = []
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
rendered.append(
|
|
331
|
-
|
|
332
|
-
rendered.append(
|
|
371
|
+
line_count = 0
|
|
372
|
+
|
|
373
|
+
for file_diff in diff.files:
|
|
374
|
+
header = _render_diff_file_header(file_diff)
|
|
375
|
+
if header:
|
|
376
|
+
rendered.append(header)
|
|
377
|
+
for line in file_diff.lines:
|
|
378
|
+
rendered.append(_render_diff_line(line))
|
|
379
|
+
line_count += 1
|
|
380
|
+
|
|
381
|
+
if line_count == 0:
|
|
382
|
+
rendered.append('<span class="diff-line diff-ctx"> </span>')
|
|
383
|
+
|
|
333
384
|
diff_content = f'<div class="diff-view">{"".join(rendered)}</div>'
|
|
334
|
-
open_attr = "" if _should_collapse(
|
|
385
|
+
open_attr = "" if _should_collapse("\n" * max(1, line_count)) else " open"
|
|
335
386
|
return (
|
|
336
387
|
f'<details class="diff-collapsible"{open_attr}>'
|
|
337
|
-
f"<summary>Diff ({
|
|
388
|
+
f"<summary>Diff ({line_count} lines)</summary>"
|
|
338
389
|
f"{diff_content}"
|
|
339
390
|
"</details>"
|
|
340
391
|
)
|
|
341
392
|
|
|
342
393
|
|
|
343
|
-
def
|
|
394
|
+
def _render_diff_file_header(file_diff: model.DiffFileDiff) -> str:
|
|
395
|
+
stats_parts: list[str] = []
|
|
396
|
+
if file_diff.stats_add > 0:
|
|
397
|
+
stats_parts.append(f'<span class="diff-stats-add">+{file_diff.stats_add}</span>')
|
|
398
|
+
if file_diff.stats_remove > 0:
|
|
399
|
+
stats_parts.append(f'<span class="diff-stats-remove">-{file_diff.stats_remove}</span>')
|
|
400
|
+
stats_html = f' <span class="diff-stats">{" ".join(stats_parts)}</span>' if stats_parts else ""
|
|
401
|
+
file_name = _escape_html(file_diff.file_path)
|
|
402
|
+
return f'<div class="diff-file">{file_name}{stats_html}</div>'
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _render_diff_line(line: model.DiffLine) -> str:
|
|
406
|
+
if line.kind == "gap":
|
|
407
|
+
line_class = "diff-ctx"
|
|
408
|
+
prefix = "⋮"
|
|
409
|
+
else:
|
|
410
|
+
line_class = "diff-plus" if line.kind == "add" else "diff-minus" if line.kind == "remove" else "diff-ctx"
|
|
411
|
+
prefix = "+" if line.kind == "add" else "-" if line.kind == "remove" else " "
|
|
412
|
+
spans = [_render_diff_span(span, line.kind) for span in line.spans]
|
|
413
|
+
content = "".join(spans)
|
|
414
|
+
if not content:
|
|
415
|
+
content = " "
|
|
416
|
+
return f'<span class="diff-line {line_class}">{prefix} {content}</span>'
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _render_diff_span(span: model.DiffSpan, line_kind: str) -> str:
|
|
420
|
+
text = _escape_html(span.text)
|
|
421
|
+
if line_kind == "add" and span.op == "insert":
|
|
422
|
+
return f'<span class="diff-span diff-char-add">{text}</span>'
|
|
423
|
+
if line_kind == "remove" and span.op == "delete":
|
|
424
|
+
return f'<span class="diff-span diff-char-remove">{text}</span>'
|
|
425
|
+
return f'<span class="diff-span">{text}</span>'
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _render_markdown_doc(doc: model.MarkdownDocUIExtra) -> str:
|
|
429
|
+
encoded = _escape_html(doc.content)
|
|
430
|
+
file_path = _escape_html(doc.file_path)
|
|
431
|
+
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>'
|
|
432
|
+
|
|
433
|
+
# Using a container that mimics diff-view but for markdown
|
|
434
|
+
content = (
|
|
435
|
+
f'<div class="markdown-content markdown-body" data-raw="{encoded}" '
|
|
436
|
+
f'style="padding: 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-body); margin-top: 4px;">'
|
|
437
|
+
f'<noscript><pre style="white-space: pre-wrap;">{encoded}</pre></noscript>'
|
|
438
|
+
f"</div>"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
line_count = doc.content.count("\n") + 1
|
|
442
|
+
open_attr = " open"
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
f'<details class="diff-collapsible"{open_attr}>'
|
|
446
|
+
f"<summary>File Content ({line_count} lines)</summary>"
|
|
447
|
+
f'<div style="margin-top: 8px;">'
|
|
448
|
+
f"{header}"
|
|
449
|
+
f"{content}"
|
|
450
|
+
f"</div>"
|
|
451
|
+
f"</details>"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _collect_ui_extras(ui_extra: model.ToolResultUIExtra | None) -> list[model.ToolResultUIExtra]:
|
|
344
456
|
if ui_extra is None:
|
|
345
|
-
return
|
|
346
|
-
if ui_extra
|
|
347
|
-
return
|
|
348
|
-
return ui_extra
|
|
457
|
+
return []
|
|
458
|
+
if isinstance(ui_extra, model.MultiUIExtra):
|
|
459
|
+
return list(ui_extra.items)
|
|
460
|
+
return [ui_extra]
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _build_add_only_diff(text: str, file_path: str) -> model.DiffUIExtra:
|
|
464
|
+
lines: list[model.DiffLine] = []
|
|
465
|
+
new_line_no = 1
|
|
466
|
+
for line in text.splitlines():
|
|
467
|
+
lines.append(
|
|
468
|
+
model.DiffLine(
|
|
469
|
+
kind="add",
|
|
470
|
+
new_line_no=new_line_no,
|
|
471
|
+
spans=[model.DiffSpan(op="equal", text=line)],
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
new_line_no += 1
|
|
475
|
+
file_diff = model.DiffFileDiff(file_path=file_path, lines=lines, stats_add=len(lines), stats_remove=0)
|
|
476
|
+
return model.DiffUIExtra(files=[file_diff])
|
|
349
477
|
|
|
350
478
|
|
|
351
479
|
def _get_mermaid_link_html(
|
|
352
480
|
ui_extra: model.ToolResultUIExtra | None, tool_call: model.ToolCallItem | None = None
|
|
353
481
|
) -> str | None:
|
|
354
|
-
|
|
482
|
+
code = ""
|
|
483
|
+
link: str | None = None
|
|
484
|
+
line_count = 0
|
|
485
|
+
|
|
486
|
+
if isinstance(ui_extra, model.MermaidLinkUIExtra):
|
|
487
|
+
code = ui_extra.code
|
|
488
|
+
link = ui_extra.link
|
|
489
|
+
line_count = ui_extra.line_count
|
|
490
|
+
|
|
491
|
+
if not code and tool_call and tool_call.name == "Mermaid":
|
|
355
492
|
try:
|
|
356
493
|
args = json.loads(tool_call.arguments)
|
|
357
494
|
code = args.get("code", "")
|
|
358
|
-
except
|
|
495
|
+
except (json.JSONDecodeError, TypeError):
|
|
359
496
|
code = ""
|
|
360
|
-
|
|
361
|
-
code = ""
|
|
497
|
+
line_count = code.count("\n") + 1 if code else 0
|
|
362
498
|
|
|
363
|
-
if not code and
|
|
364
|
-
ui_extra is None or ui_extra.type != model.ToolResultUIExtraType.MERMAID_LINK or not ui_extra.mermaid_link
|
|
365
|
-
):
|
|
499
|
+
if not code and not link:
|
|
366
500
|
return None
|
|
367
501
|
|
|
368
502
|
# Prepare code for rendering and copy
|
|
369
503
|
escaped_code = _escape_html(code) if code else ""
|
|
370
|
-
line_count = code.count("\n") + 1 if code else 0
|
|
371
504
|
|
|
372
505
|
# Build Toolbar
|
|
373
506
|
toolbar_items: list[str] = []
|
|
@@ -380,12 +513,9 @@ def _get_mermaid_link_html(
|
|
|
380
513
|
buttons_html.append(
|
|
381
514
|
f'<button type="button" class="copy-mermaid-btn" data-code="{escaped_code}" title="Copy Mermaid Code">Copy Code</button>'
|
|
382
515
|
)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (ui_extra and ui_extra.type == model.ToolResultUIExtraType.MERMAID_LINK and ui_extra.mermaid_link)
|
|
387
|
-
else None
|
|
388
|
-
)
|
|
516
|
+
buttons_html.append(
|
|
517
|
+
'<button type="button" class="fullscreen-mermaid-btn" title="View Fullscreen">Fullscreen</button>'
|
|
518
|
+
)
|
|
389
519
|
|
|
390
520
|
if link:
|
|
391
521
|
link_url = _escape_html(link)
|
|
@@ -419,8 +549,8 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
419
549
|
is_todo_list = False
|
|
420
550
|
ts_str = _format_msg_timestamp(tool_call.created_at)
|
|
421
551
|
|
|
422
|
-
if tool_call.name
|
|
423
|
-
args_html = _try_render_todo_args(tool_call.arguments)
|
|
552
|
+
if tool_call.name in ("TodoWrite", "update_plan"):
|
|
553
|
+
args_html = _try_render_todo_args(tool_call.arguments, tool_call.name)
|
|
424
554
|
if args_html:
|
|
425
555
|
is_todo_list = True
|
|
426
556
|
|
|
@@ -428,7 +558,7 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
428
558
|
try:
|
|
429
559
|
parsed = json.loads(tool_call.arguments)
|
|
430
560
|
args_text = json.dumps(parsed, ensure_ascii=False, indent=2)
|
|
431
|
-
except
|
|
561
|
+
except (json.JSONDecodeError, TypeError):
|
|
432
562
|
args_text = tool_call.arguments
|
|
433
563
|
|
|
434
564
|
args_html = _escape_html(args_text or "")
|
|
@@ -450,7 +580,7 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
450
580
|
parsed_args = json.loads(tool_call.arguments)
|
|
451
581
|
if parsed_args.get("command") in {"create", "str_replace", "insert"}:
|
|
452
582
|
force_collapse = True
|
|
453
|
-
except
|
|
583
|
+
except (json.JSONDecodeError, TypeError):
|
|
454
584
|
pass
|
|
455
585
|
|
|
456
586
|
should_collapse = force_collapse or _should_collapse(args_html)
|
|
@@ -475,31 +605,50 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
475
605
|
]
|
|
476
606
|
|
|
477
607
|
if result:
|
|
478
|
-
|
|
479
|
-
mermaid_html = _get_mermaid_link_html(result.ui_extra, tool_call)
|
|
608
|
+
extras = _collect_ui_extras(result.ui_extra)
|
|
480
609
|
|
|
481
|
-
|
|
610
|
+
mermaid_extra = next((x for x in extras if isinstance(x, model.MermaidLinkUIExtra)), None)
|
|
611
|
+
mermaid_source = mermaid_extra if mermaid_extra else result.ui_extra
|
|
612
|
+
mermaid_html = _get_mermaid_link_html(mermaid_source, tool_call)
|
|
482
613
|
|
|
483
|
-
|
|
614
|
+
should_hide_text = tool_call.name in ("TodoWrite", "update_plan") and result.status != "error"
|
|
615
|
+
|
|
616
|
+
if (
|
|
617
|
+
tool_call.name == "Edit"
|
|
618
|
+
and not any(isinstance(x, model.DiffUIExtra) for x in extras)
|
|
619
|
+
and result.status != "error"
|
|
620
|
+
):
|
|
484
621
|
try:
|
|
485
622
|
args_data = json.loads(tool_call.arguments)
|
|
623
|
+
file_path = args_data.get("file_path", "Unknown file")
|
|
486
624
|
old_string = args_data.get("old_string", "")
|
|
487
625
|
new_string = args_data.get("new_string", "")
|
|
488
626
|
if old_string == "" and new_string:
|
|
489
|
-
|
|
490
|
-
except
|
|
627
|
+
extras.append(_build_add_only_diff(new_string, file_path))
|
|
628
|
+
except (json.JSONDecodeError, TypeError):
|
|
491
629
|
pass
|
|
492
630
|
|
|
493
631
|
items_to_render: list[str] = []
|
|
494
632
|
|
|
495
633
|
if result.output and not should_hide_text:
|
|
496
634
|
if is_sub_agent_tool(tool_call.name):
|
|
497
|
-
|
|
635
|
+
description = None
|
|
636
|
+
try:
|
|
637
|
+
args = json.loads(tool_call.arguments)
|
|
638
|
+
if isinstance(args, dict):
|
|
639
|
+
typed_args = cast(dict[str, Any], args)
|
|
640
|
+
description = cast(str | None, typed_args.get("description"))
|
|
641
|
+
except (json.JSONDecodeError, TypeError):
|
|
642
|
+
pass
|
|
643
|
+
items_to_render.append(_render_sub_agent_result(result.output, description))
|
|
498
644
|
else:
|
|
499
645
|
items_to_render.append(_render_text_block(result.output))
|
|
500
646
|
|
|
501
|
-
|
|
502
|
-
|
|
647
|
+
for extra in extras:
|
|
648
|
+
if isinstance(extra, model.DiffUIExtra):
|
|
649
|
+
items_to_render.append(_render_diff_block(extra))
|
|
650
|
+
elif isinstance(extra, model.MarkdownDocUIExtra):
|
|
651
|
+
items_to_render.append(_render_markdown_doc(extra))
|
|
503
652
|
|
|
504
653
|
if mermaid_html:
|
|
505
654
|
items_to_render.append(mermaid_html)
|
|
@@ -522,7 +671,13 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
522
671
|
def _build_messages_html(
|
|
523
672
|
history: list[model.ConversationItem],
|
|
524
673
|
tool_results: dict[str, model.ToolResultItem],
|
|
674
|
+
*,
|
|
675
|
+
seen_session_ids: set[str] | None = None,
|
|
676
|
+
nesting_level: int = 0,
|
|
525
677
|
) -> str:
|
|
678
|
+
if seen_session_ids is None:
|
|
679
|
+
seen_session_ids = set()
|
|
680
|
+
|
|
526
681
|
blocks: list[str] = []
|
|
527
682
|
assistant_counter = 0
|
|
528
683
|
|
|
@@ -549,7 +704,7 @@ def _build_messages_html(
|
|
|
549
704
|
elif isinstance(item, model.AssistantMessageItem):
|
|
550
705
|
assistant_counter += 1
|
|
551
706
|
blocks.append(_render_assistant_message(assistant_counter, item.content or "", item.created_at))
|
|
552
|
-
elif isinstance(item, model.
|
|
707
|
+
elif isinstance(item, model.TaskMetadataItem):
|
|
553
708
|
blocks.append(_render_metadata_item(item))
|
|
554
709
|
elif isinstance(item, model.DeveloperMessageItem):
|
|
555
710
|
content = _escape_html(item.content or "")
|
|
@@ -574,9 +729,61 @@ def _build_messages_html(
|
|
|
574
729
|
result = tool_results.get(item.call_id)
|
|
575
730
|
blocks.append(_format_tool_call(item, result))
|
|
576
731
|
|
|
732
|
+
# Recursively render sub-agent session history
|
|
733
|
+
if result is not None:
|
|
734
|
+
sub_agent_html = _render_sub_agent_session(result, seen_session_ids, nesting_level)
|
|
735
|
+
if sub_agent_html:
|
|
736
|
+
blocks.append(sub_agent_html)
|
|
737
|
+
|
|
577
738
|
return "\n".join(blocks)
|
|
578
739
|
|
|
579
740
|
|
|
741
|
+
def _render_sub_agent_session(
|
|
742
|
+
tool_result: model.ToolResultItem,
|
|
743
|
+
seen_session_ids: set[str],
|
|
744
|
+
nesting_level: int,
|
|
745
|
+
) -> str | None:
|
|
746
|
+
"""Render sub-agent session history when a tool result references it."""
|
|
747
|
+
from klaude_code.session.session import Session
|
|
748
|
+
|
|
749
|
+
ui_extra = tool_result.ui_extra
|
|
750
|
+
if not isinstance(ui_extra, model.SessionIdUIExtra):
|
|
751
|
+
return None
|
|
752
|
+
|
|
753
|
+
session_id = ui_extra.session_id
|
|
754
|
+
if not session_id or session_id in seen_session_ids:
|
|
755
|
+
return None
|
|
756
|
+
|
|
757
|
+
seen_session_ids.add(session_id)
|
|
758
|
+
|
|
759
|
+
try:
|
|
760
|
+
sub_session = Session.load(session_id)
|
|
761
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
762
|
+
return None
|
|
763
|
+
|
|
764
|
+
sub_history = sub_session.conversation_history
|
|
765
|
+
sub_tool_results = {item.call_id: item for item in sub_history if isinstance(item, model.ToolResultItem)}
|
|
766
|
+
|
|
767
|
+
sub_html = _build_messages_html(
|
|
768
|
+
sub_history,
|
|
769
|
+
sub_tool_results,
|
|
770
|
+
seen_session_ids=seen_session_ids,
|
|
771
|
+
nesting_level=nesting_level + 1,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
if not sub_html:
|
|
775
|
+
return None
|
|
776
|
+
|
|
777
|
+
# Wrap in a collapsible sub-agent container using same style as other collapsible sections
|
|
778
|
+
indent_style = f' style="margin-left: {nesting_level * 16}px;"' if nesting_level > 0 else ""
|
|
779
|
+
return (
|
|
780
|
+
f'<details class="sub-agent-session"{indent_style}>'
|
|
781
|
+
f"<summary>Sub-agent: {_escape_html(session_id)}</summary>"
|
|
782
|
+
f'<div class="sub-agent-content">{sub_html}</div>'
|
|
783
|
+
f"</details>"
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
|
|
580
787
|
def build_export_html(
|
|
581
788
|
session: Session,
|
|
582
789
|
system_prompt: str,
|