klaude-code 1.2.8__py3-none-any.whl → 1.2.10__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/codex/__init__.py +1 -1
- klaude_code/cli/main.py +12 -1
- klaude_code/cli/runtime.py +7 -11
- klaude_code/command/__init__.py +68 -21
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +5 -2
- klaude_code/command/diff_cmd.py +5 -2
- klaude_code/command/export_cmd.py +7 -4
- klaude_code/command/help_cmd.py +6 -2
- klaude_code/command/model_cmd.py +5 -2
- klaude_code/command/prompt-deslop.md +14 -0
- klaude_code/command/prompt_command.py +8 -3
- klaude_code/command/refresh_cmd.py +6 -2
- klaude_code/command/registry.py +17 -5
- klaude_code/command/release_notes_cmd.py +89 -0
- klaude_code/command/status_cmd.py +98 -56
- klaude_code/command/terminal_setup_cmd.py +7 -4
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/agent.py +66 -26
- klaude_code/core/executor.py +2 -2
- klaude_code/core/manager/agent_manager.py +6 -7
- klaude_code/core/manager/llm_clients.py +47 -22
- klaude_code/core/manager/llm_clients_builder.py +19 -7
- klaude_code/core/manager/sub_agent_manager.py +6 -2
- klaude_code/core/prompt.py +38 -28
- klaude_code/core/reminders.py +4 -7
- klaude_code/core/task.py +59 -40
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/file/_utils.py +30 -0
- klaude_code/core/tool/file/apply_patch_tool.py +1 -1
- klaude_code/core/tool/file/edit_tool.py +6 -31
- klaude_code/core/tool/file/multi_edit_tool.py +7 -32
- klaude_code/core/tool/file/read_tool.py +6 -18
- klaude_code/core/tool/file/write_tool.py +6 -31
- klaude_code/core/tool/memory/__init__.py +5 -0
- klaude_code/core/tool/memory/memory_tool.py +2 -2
- klaude_code/core/tool/memory/skill_loader.py +2 -1
- klaude_code/core/tool/memory/skill_tool.py +13 -0
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_context.py +21 -4
- klaude_code/core/tool/tool_runner.py +5 -8
- klaude_code/core/tool/web/mermaid_tool.py +1 -4
- klaude_code/core/turn.py +40 -37
- klaude_code/llm/__init__.py +2 -12
- klaude_code/llm/anthropic/client.py +14 -44
- klaude_code/llm/client.py +2 -2
- klaude_code/llm/codex/client.py +4 -3
- klaude_code/llm/input_common.py +0 -6
- klaude_code/llm/openai_compatible/client.py +31 -74
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream_processor.py +82 -0
- klaude_code/llm/openrouter/client.py +32 -62
- klaude_code/llm/openrouter/input.py +4 -27
- klaude_code/llm/registry.py +33 -7
- klaude_code/llm/responses/client.py +16 -48
- klaude_code/llm/responses/input.py +1 -1
- klaude_code/llm/usage.py +61 -11
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +11 -2
- klaude_code/protocol/model.py +147 -24
- klaude_code/protocol/op.py +1 -0
- klaude_code/protocol/sub_agent.py +5 -1
- klaude_code/session/export.py +56 -32
- klaude_code/session/session.py +43 -21
- klaude_code/session/templates/export_session.html +4 -1
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/modes/repl/__init__.py +1 -5
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/event_handler.py +153 -54
- klaude_code/ui/modes/repl/renderer.py +4 -4
- klaude_code/ui/renderers/developer.py +35 -25
- klaude_code/ui/renderers/metadata.py +68 -30
- klaude_code/ui/renderers/tools.py +53 -87
- klaude_code/ui/rich/markdown.py +5 -5
- klaude_code/ui/terminal/control.py +2 -2
- klaude_code/version.py +3 -3
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/METADATA +1 -1
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/RECORD +82 -78
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/entry_points.txt +0 -0
klaude_code/session/export.py
CHANGED
|
@@ -159,20 +159,35 @@ def _format_cost(cost: float, currency: str = "USD") -> str:
|
|
|
159
159
|
return f"{symbol}{cost:.4f}"
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
def
|
|
163
|
-
|
|
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
|
+
"""
|
|
164
178
|
parts: list[str] = []
|
|
165
179
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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(" ", "-"))
|
|
169
184
|
model_parts.append(f'<span class="metadata-provider">@{provider}</span>')
|
|
170
185
|
|
|
171
186
|
parts.append("".join(model_parts))
|
|
172
187
|
|
|
173
188
|
# Stats
|
|
174
|
-
if
|
|
175
|
-
u =
|
|
189
|
+
if metadata.usage:
|
|
190
|
+
u = metadata.usage
|
|
176
191
|
# Input with cost
|
|
177
192
|
input_stat = f"input: {_format_token_count(u.input_tokens)}"
|
|
178
193
|
if u.input_cost is not None:
|
|
@@ -194,22 +209,39 @@ def _render_metadata_item(item: model.ResponseMetadataItem) -> str:
|
|
|
194
209
|
|
|
195
210
|
if u.reasoning_tokens > 0:
|
|
196
211
|
parts.append(f'<span class="metadata-stat">thinking: {_format_token_count(u.reasoning_tokens)}</span>')
|
|
197
|
-
if u.context_usage_percent is not None:
|
|
212
|
+
if show_context and u.context_usage_percent is not None:
|
|
198
213
|
parts.append(f'<span class="metadata-stat">context: {u.context_usage_percent:.1f}%</span>')
|
|
199
214
|
if u.throughput_tps is not None:
|
|
200
215
|
parts.append(f'<span class="metadata-stat">tps: {u.throughput_tps:.1f}</span>')
|
|
201
216
|
|
|
202
|
-
if
|
|
203
|
-
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>')
|
|
204
219
|
|
|
205
220
|
# Total cost
|
|
206
|
-
if
|
|
207
|
-
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
|
+
)
|
|
208
225
|
|
|
209
226
|
divider = '<span class="metadata-divider">/</span>'
|
|
210
227
|
joined_html = divider.join(parts)
|
|
211
228
|
|
|
212
|
-
|
|
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, 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>'
|
|
213
245
|
|
|
214
246
|
|
|
215
247
|
def _render_assistant_message(index: int, content: str, timestamp: datetime) -> str:
|
|
@@ -262,7 +294,7 @@ def _try_render_todo_args(arguments: str) -> str | None:
|
|
|
262
294
|
return None
|
|
263
295
|
|
|
264
296
|
return f'<div class="todo-list">{"".join(items_html)}</div>'
|
|
265
|
-
except
|
|
297
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
266
298
|
return None
|
|
267
299
|
|
|
268
300
|
|
|
@@ -336,11 +368,9 @@ def _render_diff_block(diff: str) -> str:
|
|
|
336
368
|
|
|
337
369
|
|
|
338
370
|
def _get_diff_text(ui_extra: model.ToolResultUIExtra | None) -> str | None:
|
|
339
|
-
if ui_extra
|
|
340
|
-
return
|
|
341
|
-
|
|
342
|
-
return None
|
|
343
|
-
return ui_extra.diff_text
|
|
371
|
+
if isinstance(ui_extra, model.DiffTextUIExtra):
|
|
372
|
+
return ui_extra.diff_text
|
|
373
|
+
return None
|
|
344
374
|
|
|
345
375
|
|
|
346
376
|
def _get_mermaid_link_html(
|
|
@@ -350,14 +380,12 @@ def _get_mermaid_link_html(
|
|
|
350
380
|
try:
|
|
351
381
|
args = json.loads(tool_call.arguments)
|
|
352
382
|
code = args.get("code", "")
|
|
353
|
-
except
|
|
383
|
+
except (json.JSONDecodeError, TypeError):
|
|
354
384
|
code = ""
|
|
355
385
|
else:
|
|
356
386
|
code = ""
|
|
357
387
|
|
|
358
|
-
if not code and (
|
|
359
|
-
ui_extra is None or ui_extra.type != model.ToolResultUIExtraType.MERMAID_LINK or not ui_extra.mermaid_link
|
|
360
|
-
):
|
|
388
|
+
if not code and not isinstance(ui_extra, model.MermaidLinkUIExtra):
|
|
361
389
|
return None
|
|
362
390
|
|
|
363
391
|
# Prepare code for rendering and copy
|
|
@@ -376,11 +404,7 @@ def _get_mermaid_link_html(
|
|
|
376
404
|
f'<button type="button" class="copy-mermaid-btn" data-code="{escaped_code}" title="Copy Mermaid Code">Copy Code</button>'
|
|
377
405
|
)
|
|
378
406
|
|
|
379
|
-
link = (
|
|
380
|
-
ui_extra.mermaid_link.link
|
|
381
|
-
if (ui_extra and ui_extra.type == model.ToolResultUIExtraType.MERMAID_LINK and ui_extra.mermaid_link)
|
|
382
|
-
else None
|
|
383
|
-
)
|
|
407
|
+
link = ui_extra.link if isinstance(ui_extra, model.MermaidLinkUIExtra) else None
|
|
384
408
|
|
|
385
409
|
if link:
|
|
386
410
|
link_url = _escape_html(link)
|
|
@@ -423,7 +447,7 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
423
447
|
try:
|
|
424
448
|
parsed = json.loads(tool_call.arguments)
|
|
425
449
|
args_text = json.dumps(parsed, ensure_ascii=False, indent=2)
|
|
426
|
-
except
|
|
450
|
+
except (json.JSONDecodeError, TypeError):
|
|
427
451
|
args_text = tool_call.arguments
|
|
428
452
|
|
|
429
453
|
args_html = _escape_html(args_text or "")
|
|
@@ -445,7 +469,7 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
445
469
|
parsed_args = json.loads(tool_call.arguments)
|
|
446
470
|
if parsed_args.get("command") in {"create", "str_replace", "insert"}:
|
|
447
471
|
force_collapse = True
|
|
448
|
-
except
|
|
472
|
+
except (json.JSONDecodeError, TypeError):
|
|
449
473
|
pass
|
|
450
474
|
|
|
451
475
|
should_collapse = force_collapse or _should_collapse(args_html)
|
|
@@ -482,7 +506,7 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
482
506
|
new_string = args_data.get("new_string", "")
|
|
483
507
|
if old_string == "" and new_string:
|
|
484
508
|
diff_text = "\n".join(f"+{line}" for line in new_string.splitlines())
|
|
485
|
-
except
|
|
509
|
+
except (json.JSONDecodeError, TypeError):
|
|
486
510
|
pass
|
|
487
511
|
|
|
488
512
|
items_to_render: list[str] = []
|
|
@@ -544,7 +568,7 @@ def _build_messages_html(
|
|
|
544
568
|
elif isinstance(item, model.AssistantMessageItem):
|
|
545
569
|
assistant_counter += 1
|
|
546
570
|
blocks.append(_render_assistant_message(assistant_counter, item.content or "", item.created_at))
|
|
547
|
-
elif isinstance(item, model.
|
|
571
|
+
elif isinstance(item, model.TaskMetadataItem):
|
|
548
572
|
blocks.append(_render_metadata_item(item))
|
|
549
573
|
elif isinstance(item, model.DeveloperMessageItem):
|
|
550
574
|
content = _escape_html(item.content or "")
|
klaude_code/session/session.py
CHANGED
|
@@ -5,7 +5,7 @@ from collections.abc import Iterable, Sequence
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import ClassVar
|
|
7
7
|
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
8
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
9
9
|
|
|
10
10
|
from klaude_code.protocol import events, model
|
|
11
11
|
|
|
@@ -19,8 +19,6 @@ class Session(BaseModel):
|
|
|
19
19
|
file_tracker: dict[str, float] = Field(default_factory=dict)
|
|
20
20
|
# Todo list for the session
|
|
21
21
|
todos: list[model.TodoItem] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
|
22
|
-
# Messages count, redundant state for performance optimization to avoid reading entire jsonl file
|
|
23
|
-
messages_count: int = Field(default=0)
|
|
24
22
|
# Model name used for this session
|
|
25
23
|
# Used in list method SessionMetaBrief
|
|
26
24
|
model_name: str | None = None
|
|
@@ -33,6 +31,27 @@ class Session(BaseModel):
|
|
|
33
31
|
need_todo_empty_cooldown_counter: int = Field(exclude=True, default=0)
|
|
34
32
|
need_todo_not_used_cooldown_counter: int = Field(exclude=True, default=0)
|
|
35
33
|
|
|
34
|
+
# Cached messages count (computed property)
|
|
35
|
+
_messages_count_cache: int | None = PrivateAttr(default=None)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def messages_count(self) -> int:
|
|
39
|
+
"""Count of user and assistant messages in conversation history.
|
|
40
|
+
|
|
41
|
+
This is a cached property that is invalidated when append_history is called.
|
|
42
|
+
"""
|
|
43
|
+
if self._messages_count_cache is None:
|
|
44
|
+
self._messages_count_cache = sum(
|
|
45
|
+
1
|
|
46
|
+
for it in self.conversation_history
|
|
47
|
+
if isinstance(it, (model.UserMessageItem, model.AssistantMessageItem))
|
|
48
|
+
)
|
|
49
|
+
return self._messages_count_cache
|
|
50
|
+
|
|
51
|
+
def _invalidate_messages_count_cache(self) -> None:
|
|
52
|
+
"""Invalidate the cached messages count."""
|
|
53
|
+
self._messages_count_cache = None
|
|
54
|
+
|
|
36
55
|
# Internal: mapping for (de)serialization of conversation items
|
|
37
56
|
_TypeMap: ClassVar[dict[str, type[BaseModel]]] = {
|
|
38
57
|
# Messages
|
|
@@ -50,7 +69,7 @@ class Session(BaseModel):
|
|
|
50
69
|
"AssistantMessageDelta": model.AssistantMessageDelta,
|
|
51
70
|
"StartItem": model.StartItem,
|
|
52
71
|
"StreamErrorItem": model.StreamErrorItem,
|
|
53
|
-
"
|
|
72
|
+
"TaskMetadataItem": model.TaskMetadataItem,
|
|
54
73
|
"InterruptItem": model.InterruptItem,
|
|
55
74
|
}
|
|
56
75
|
|
|
@@ -84,7 +103,17 @@ class Session(BaseModel):
|
|
|
84
103
|
return self._messages_dir() / f"{prefix}-{self.id}.jsonl"
|
|
85
104
|
|
|
86
105
|
@classmethod
|
|
87
|
-
def
|
|
106
|
+
def create(cls, id: str | None = None) -> "Session":
|
|
107
|
+
"""Create a new session without checking for existing files."""
|
|
108
|
+
return Session(id=id or uuid.uuid4().hex, work_dir=Path.cwd())
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def load(cls, id: str, *, skip_if_missing: bool = False) -> "Session":
|
|
112
|
+
"""Load an existing session or create a new one if not found."""
|
|
113
|
+
|
|
114
|
+
if skip_if_missing:
|
|
115
|
+
return Session(id=id, work_dir=Path.cwd())
|
|
116
|
+
|
|
88
117
|
# Load session metadata
|
|
89
118
|
sessions_dir = cls._sessions_dir()
|
|
90
119
|
session_candidates = sorted(
|
|
@@ -109,7 +138,6 @@ class Session(BaseModel):
|
|
|
109
138
|
loaded_memory = list(raw.get("loaded_memory", []))
|
|
110
139
|
created_at = float(raw.get("created_at", time.time()))
|
|
111
140
|
updated_at = float(raw.get("updated_at", created_at))
|
|
112
|
-
messages_count = int(raw.get("messages_count", 0))
|
|
113
141
|
model_name = raw.get("model_name")
|
|
114
142
|
|
|
115
143
|
sess = Session(
|
|
@@ -121,7 +149,6 @@ class Session(BaseModel):
|
|
|
121
149
|
loaded_memory=loaded_memory,
|
|
122
150
|
created_at=created_at,
|
|
123
151
|
updated_at=updated_at,
|
|
124
|
-
messages_count=messages_count,
|
|
125
152
|
model_name=model_name,
|
|
126
153
|
)
|
|
127
154
|
|
|
@@ -150,14 +177,11 @@ class Session(BaseModel):
|
|
|
150
177
|
item = cls_type(**data)
|
|
151
178
|
# pyright: ignore[reportAssignmentType]
|
|
152
179
|
history.append(item) # type: ignore[arg-type]
|
|
153
|
-
except
|
|
180
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
154
181
|
# Best-effort load; skip malformed lines
|
|
155
182
|
continue
|
|
156
183
|
sess.conversation_history = history
|
|
157
|
-
#
|
|
158
|
-
sess.messages_count = sum(
|
|
159
|
-
1 for it in history if isinstance(it, (model.UserMessageItem, model.AssistantMessageItem))
|
|
160
|
-
)
|
|
184
|
+
# messages_count is now a computed property, no need to set it
|
|
161
185
|
|
|
162
186
|
return sess
|
|
163
187
|
|
|
@@ -190,10 +214,8 @@ class Session(BaseModel):
|
|
|
190
214
|
def append_history(self, items: Sequence[model.ConversationItem]):
|
|
191
215
|
# Append to in-memory history
|
|
192
216
|
self.conversation_history.extend(items)
|
|
193
|
-
#
|
|
194
|
-
self.
|
|
195
|
-
1 for it in items if isinstance(it, (model.UserMessageItem, model.AssistantMessageItem))
|
|
196
|
-
)
|
|
217
|
+
# Invalidate messages count cache
|
|
218
|
+
self._invalidate_messages_count_cache()
|
|
197
219
|
|
|
198
220
|
# Incrementally persist to JSONL under messages directory
|
|
199
221
|
messages_dir = self._messages_dir()
|
|
@@ -230,7 +252,7 @@ class Session(BaseModel):
|
|
|
230
252
|
if ts > latest_ts:
|
|
231
253
|
latest_ts = ts
|
|
232
254
|
latest_id = sid
|
|
233
|
-
except
|
|
255
|
+
except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
234
256
|
continue
|
|
235
257
|
return latest_id
|
|
236
258
|
|
|
@@ -295,8 +317,8 @@ class Session(BaseModel):
|
|
|
295
317
|
content=ri.content,
|
|
296
318
|
session_id=self.id,
|
|
297
319
|
)
|
|
298
|
-
case model.
|
|
299
|
-
yield events.
|
|
320
|
+
case model.TaskMetadataItem() as mt:
|
|
321
|
+
yield events.TaskMetadataEvent(
|
|
300
322
|
session_id=self.id,
|
|
301
323
|
metadata=mt,
|
|
302
324
|
)
|
|
@@ -383,7 +405,7 @@ class Session(BaseModel):
|
|
|
383
405
|
text_parts.append(text)
|
|
384
406
|
return " ".join(text_parts) if text_parts else None
|
|
385
407
|
return None
|
|
386
|
-
except
|
|
408
|
+
except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
387
409
|
return None
|
|
388
410
|
return None
|
|
389
411
|
|
|
@@ -391,7 +413,7 @@ class Session(BaseModel):
|
|
|
391
413
|
for p in sessions_dir.glob("*.json"):
|
|
392
414
|
try:
|
|
393
415
|
data = json.loads(p.read_text())
|
|
394
|
-
except
|
|
416
|
+
except (json.JSONDecodeError, OSError):
|
|
395
417
|
# Skip unreadable files
|
|
396
418
|
continue
|
|
397
419
|
# Filter out sub-agent sessions
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
rel="stylesheet"
|
|
22
22
|
/>
|
|
23
23
|
<link
|
|
24
|
-
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&family=IBM+Plex+Sans:wght@400;500;700&display=swap"
|
|
24
|
+
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
|
|
25
25
|
rel="stylesheet"
|
|
26
26
|
/>
|
|
27
27
|
<style>
|
|
@@ -411,6 +411,9 @@
|
|
|
411
411
|
font-size: var(--font-size-xs);
|
|
412
412
|
color: var(--text-dim);
|
|
413
413
|
border-left: 2px solid transparent;
|
|
414
|
+
display: flex;
|
|
415
|
+
flex-direction: column;
|
|
416
|
+
gap: 8px;
|
|
414
417
|
}
|
|
415
418
|
.metadata-line {
|
|
416
419
|
display: flex;
|
klaude_code/ui/core/input.py
CHANGED
|
@@ -22,11 +22,7 @@ def build_repl_status_snapshot(agent: "Agent | None", update_message: str | None
|
|
|
22
22
|
tool_calls = 0
|
|
23
23
|
|
|
24
24
|
if agent is not None:
|
|
25
|
-
|
|
26
|
-
if profile is not None:
|
|
27
|
-
model_name = profile.llm_client.model_name or ""
|
|
28
|
-
else:
|
|
29
|
-
model_name = "N/A"
|
|
25
|
+
model_name = agent.profile.llm_client.model_name or ""
|
|
30
26
|
|
|
31
27
|
history = agent.session.conversation_history
|
|
32
28
|
for item in history:
|
|
@@ -40,19 +40,19 @@ class ClipboardCaptureState:
|
|
|
40
40
|
"""Capture image from clipboard, save to disk, and return a tag like [Image #N]."""
|
|
41
41
|
try:
|
|
42
42
|
clipboard_data = ImageGrab.grabclipboard()
|
|
43
|
-
except
|
|
43
|
+
except OSError:
|
|
44
44
|
return None
|
|
45
45
|
if not isinstance(clipboard_data, Image.Image):
|
|
46
46
|
return None
|
|
47
47
|
try:
|
|
48
48
|
self._images_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
-
except
|
|
49
|
+
except OSError:
|
|
50
50
|
return None
|
|
51
51
|
filename = f"clipboard_{uuid.uuid4().hex[:8]}.png"
|
|
52
52
|
path = self._images_dir / filename
|
|
53
53
|
try:
|
|
54
54
|
clipboard_data.save(path, "PNG")
|
|
55
|
-
except
|
|
55
|
+
except OSError:
|
|
56
56
|
return None
|
|
57
57
|
tag = f"[Image #{self._counter}]"
|
|
58
58
|
self._counter += 1
|
|
@@ -123,7 +123,7 @@ def _encode_image_file(file_path: str) -> ImageURLPart | None:
|
|
|
123
123
|
# Clipboard images are always saved as PNG
|
|
124
124
|
data_url = f"data:image/png;base64,{encoded}"
|
|
125
125
|
return ImageURLPart(image_url=ImageURLPart.ImageURL(url=data_url, id=None))
|
|
126
|
-
except
|
|
126
|
+
except OSError:
|
|
127
127
|
return None
|
|
128
128
|
|
|
129
129
|
|
|
@@ -148,5 +148,5 @@ def copy_to_clipboard(text: str) -> None:
|
|
|
148
148
|
input=text.encode("utf-8"),
|
|
149
149
|
check=True,
|
|
150
150
|
)
|
|
151
|
-
except
|
|
151
|
+
except (OSError, subprocess.SubprocessError):
|
|
152
152
|
pass
|