klaude-code 1.2.2__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.
Files changed (58) hide show
  1. klaude_code/cli/main.py +7 -0
  2. klaude_code/cli/runtime.py +6 -6
  3. klaude_code/command/__init__.py +7 -5
  4. klaude_code/command/clear_cmd.py +3 -24
  5. klaude_code/command/command_abc.py +36 -1
  6. klaude_code/command/export_cmd.py +14 -20
  7. klaude_code/command/help_cmd.py +1 -0
  8. klaude_code/command/model_cmd.py +3 -30
  9. klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
  10. klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
  11. klaude_code/command/prompt-init.md +2 -5
  12. klaude_code/command/prompt_command.py +3 -3
  13. klaude_code/command/registry.py +6 -7
  14. klaude_code/config/config.py +1 -1
  15. klaude_code/config/list_model.py +1 -1
  16. klaude_code/const/__init__.py +1 -1
  17. klaude_code/core/agent.py +2 -11
  18. klaude_code/core/executor.py +155 -14
  19. klaude_code/core/prompts/prompt-gemini.md +1 -1
  20. klaude_code/core/reminders.py +24 -0
  21. klaude_code/core/task.py +10 -0
  22. klaude_code/core/tool/shell/bash_tool.py +6 -2
  23. klaude_code/core/tool/sub_agent_tool.py +1 -1
  24. klaude_code/core/tool/tool_context.py +1 -1
  25. klaude_code/core/tool/tool_registry.py +1 -1
  26. klaude_code/core/tool/tool_runner.py +1 -1
  27. klaude_code/core/tool/web/mermaid_tool.py +1 -1
  28. klaude_code/llm/__init__.py +3 -4
  29. klaude_code/llm/anthropic/client.py +12 -9
  30. klaude_code/llm/openai_compatible/client.py +2 -18
  31. klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
  32. klaude_code/llm/openrouter/client.py +2 -18
  33. klaude_code/llm/openrouter/input.py +6 -2
  34. klaude_code/llm/registry.py +2 -71
  35. klaude_code/llm/responses/client.py +2 -0
  36. klaude_code/llm/{metadata_tracker.py → usage.py} +49 -2
  37. klaude_code/protocol/llm_param.py +12 -0
  38. klaude_code/protocol/model.py +23 -3
  39. klaude_code/protocol/op.py +14 -14
  40. klaude_code/protocol/op_handler.py +28 -0
  41. klaude_code/protocol/tools.py +0 -2
  42. klaude_code/session/export.py +124 -35
  43. klaude_code/session/session.py +1 -1
  44. klaude_code/session/templates/export_session.html +180 -42
  45. klaude_code/ui/__init__.py +6 -2
  46. klaude_code/ui/modes/exec/display.py +26 -0
  47. klaude_code/ui/modes/repl/event_handler.py +5 -1
  48. klaude_code/ui/renderers/developer.py +6 -10
  49. klaude_code/ui/renderers/metadata.py +33 -24
  50. klaude_code/ui/renderers/sub_agent.py +1 -1
  51. klaude_code/ui/renderers/tools.py +2 -2
  52. klaude_code/ui/renderers/user_input.py +18 -22
  53. klaude_code/ui/rich/status.py +13 -2
  54. {klaude_code-1.2.2.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
  55. {klaude_code-1.2.2.dist-info → klaude_code-1.2.3.dist-info}/RECORD +58 -57
  56. /klaude_code/{core → protocol}/sub_agent.py +0 -0
  57. {klaude_code-1.2.2.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
  58. {klaude_code-1.2.2.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
@@ -4,16 +4,29 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Klaude Code - $first_user_message</title>
7
- <link href="https://cdn.jsdelivr.net/npm/@fontsource/commit-mono/400.css" rel="stylesheet" />
8
- <link href="https://cdn.jsdelivr.net/npm/@fontsource/commit-mono/500.css" rel="stylesheet" />
9
- <link href="https://cdn.jsdelivr.net/npm/@fontsource/commit-mono/700.css" rel="stylesheet" />
7
+ <link
8
+ rel="icon"
9
+ href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%230851b2%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22><polyline points=%2216 18 22 12 16 6%22></polyline><polyline points=%228 6 2 12 8 18%22></polyline></svg>"
10
+ />
11
+ <link
12
+ href="https://cdn.jsdelivr.net/npm/@fontsource/iosevka/400.css"
13
+ rel="stylesheet"
14
+ />
15
+ <link
16
+ href="https://cdn.jsdelivr.net/npm/@fontsource/iosevka/500.css"
17
+ rel="stylesheet"
18
+ />
19
+ <link
20
+ href="https://cdn.jsdelivr.net/npm/@fontsource/iosevka/800.css"
21
+ rel="stylesheet"
22
+ />
10
23
  <style>
11
24
  :root {
12
25
  --bg-body: #ededed;
13
26
  --bg-container: #f0f0f0;
14
27
  --bg-card: #f0f0f0;
15
28
  --border: #c8c8c8;
16
- --text: #333333;
29
+ --text: #111111;
17
30
  --text-dim: #64748b;
18
31
  --accent: #0851b2;
19
32
  --accent-dim: rgba(8, 145, 178, 0.08);
@@ -21,12 +34,14 @@
21
34
  --error: #dc2626;
22
35
  --bg-error: #ffebee;
23
36
  --bg-code: #f3f3f3;
24
- --font-sans: "Commit Mono", "SF Mono", Menlo, monospace;
25
- --font-mono: "Commit Mono", "SF Mono", Menlo, monospace;
26
- --font-size-xs: 11px;
27
- --font-size-sm: 12px;
28
- --font-size-base: 13px;
29
- --font-size-lg: 16px;
37
+ --font-sans: "Iosevka", "SF Mono", Menlo, monospace;
38
+ --font-mono: "Iosevka", "SF Mono", Menlo, monospace;
39
+ --font-markdown: "Iosevka", "SF Mono", Menlo, monospace;
40
+ --font-weight-bold: 800;
41
+ --font-size-xs: 13px;
42
+ --font-size-sm: 14px;
43
+ --font-size-base: 16px;
44
+ --font-size-lg: 18px;
30
45
  --spacing-xs: 4px;
31
46
  --spacing-sm: 8px;
32
47
  --spacing-md: 12px;
@@ -66,7 +81,7 @@
66
81
 
67
82
  .header h1 {
68
83
  font-size: 20px;
69
- font-weight: 700;
84
+ font-weight: var(--font-weight-bold);
70
85
  margin-bottom: 16px;
71
86
  color: var(--text);
72
87
  display: inline-block;
@@ -86,9 +101,8 @@
86
101
 
87
102
  .meta-label {
88
103
  font-size: var(--font-size-sm);
89
- font-weight: 700;
104
+ font-weight: var(--font-weight-bold);
90
105
  text-transform: uppercase;
91
- letter-spacing: 0.05em;
92
106
  color: var(--text-dim);
93
107
  }
94
108
 
@@ -116,8 +130,7 @@
116
130
  font-family: var(--font-mono);
117
131
  font-size: var(--font-size-sm);
118
132
  text-transform: uppercase;
119
- letter-spacing: 0.05em;
120
- font-weight: 700;
133
+ font-weight: var(--font-weight-bold);
121
134
  cursor: pointer;
122
135
  user-select: none;
123
136
  list-style: none;
@@ -184,14 +197,17 @@
184
197
 
185
198
  .role-label {
186
199
  font-size: var(--font-size-sm);
187
- font-weight: 700;
200
+ font-weight: var(--font-weight-bold);
188
201
  text-transform: uppercase;
189
- letter-spacing: 0.05em;
190
202
  margin-bottom: 4px;
191
203
  color: var(--text-dim);
192
204
  display: flex;
193
205
  align-items: center;
194
206
  gap: 8px;
207
+ width: 100%;
208
+ }
209
+ .message-header .role-label {
210
+ width: auto;
195
211
  }
196
212
  .role-label.user {
197
213
  color: var(--accent);
@@ -210,8 +226,7 @@
210
226
  font-family: var(--font-mono);
211
227
  font-size: var(--font-size-xs);
212
228
  text-transform: uppercase;
213
- letter-spacing: 0.05em;
214
- font-weight: 700;
229
+ font-weight: var(--font-weight-bold);
215
230
  cursor: pointer;
216
231
  user-select: none;
217
232
  list-style: none;
@@ -279,14 +294,14 @@
279
294
  border: 1px solid var(--border);
280
295
  background: transparent;
281
296
  color: var(--text-dim);
297
+ font-family: var(--font-mono);
282
298
  font-size: var(--font-size-xs);
283
299
  text-transform: uppercase;
284
- letter-spacing: 0.08em;
285
300
  padding: 2px 10px;
286
301
  border-radius: 999px;
287
302
  cursor: pointer;
288
303
  transition: color 0.2s, border-color 0.2s, background 0.2s;
289
- font-weight: 700;
304
+ font-weight: var(--font-weight-bold);
290
305
  }
291
306
  .raw-toggle:hover {
292
307
  color: var(--text);
@@ -303,14 +318,14 @@
303
318
  border: 1px solid var(--border);
304
319
  background: transparent;
305
320
  color: var(--text-dim);
321
+ font-family: var(--font-mono);
306
322
  font-size: var(--font-size-xs);
307
323
  text-transform: uppercase;
308
- letter-spacing: 0.08em;
309
324
  padding: 2px 10px;
310
325
  border-radius: 999px;
311
326
  cursor: pointer;
312
327
  transition: color 0.2s, border-color 0.2s, background 0.2s;
313
- font-weight: 700;
328
+ font-weight: var(--font-weight-bold);
314
329
  }
315
330
  .copy-raw-btn:hover {
316
331
  color: var(--text);
@@ -321,14 +336,14 @@
321
336
  border: 1px solid var(--border);
322
337
  background: transparent;
323
338
  color: var(--text-dim);
339
+ font-family: var(--font-mono);
324
340
  font-size: var(--font-size-xs);
325
341
  text-transform: uppercase;
326
- letter-spacing: 0.08em;
327
342
  padding: 2px 10px;
328
343
  border-radius: 999px;
329
344
  cursor: pointer;
330
345
  transition: color 0.2s, border-color 0.2s, background 0.2s;
331
- font-weight: 700;
346
+ font-weight: var(--font-weight-bold);
332
347
  }
333
348
  .copy-mermaid-btn:hover {
334
349
  color: var(--text);
@@ -361,7 +376,7 @@
361
376
  border-radius: 0;
362
377
  padding: 0;
363
378
  box-shadow: none;
364
- font-weight: 700;
379
+ font-weight: var(--font-weight-bold);
365
380
  }
366
381
 
367
382
  .thinking-block {
@@ -371,9 +386,15 @@
371
386
  border-left: 2px solid var(--border);
372
387
  color: var(--text-dim);
373
388
  font-style: italic;
374
- font-size: var(--font-size-base);
389
+ font-size: var(--font-size-sm);
375
390
  background: var(--bg-body);
376
391
  }
392
+ .thinking-block.markdown-body p {
393
+ margin-bottom: 8px;
394
+ }
395
+ .thinking-block.markdown-body p:last-child {
396
+ margin-bottom: 0;
397
+ }
377
398
 
378
399
  /* Response Metadata */
379
400
  .response-metadata {
@@ -392,7 +413,7 @@
392
413
  line-height: 1.4;
393
414
  }
394
415
  .metadata-model {
395
- font-weight: 700;
416
+ font-weight: var(--font-weight-bold);
396
417
  color: var(--text);
397
418
  }
398
419
  .metadata-provider {
@@ -427,7 +448,7 @@
427
448
  }
428
449
 
429
450
  .tool-name {
430
- font-weight: 700;
451
+ font-weight: var(--font-weight-bold);
431
452
  color: var(--accent);
432
453
  }
433
454
  .tool-id {
@@ -435,6 +456,24 @@
435
456
  font-size: var(--font-size-xs);
436
457
  }
437
458
 
459
+ .tool-header-right {
460
+ display: flex;
461
+ align-items: center;
462
+ gap: 8px;
463
+ }
464
+
465
+ .timestamp {
466
+ font-family: var(--font-mono);
467
+ font-size: var(--font-size-xs);
468
+ color: var(--text-dim);
469
+ margin-left: auto;
470
+ font-weight: normal;
471
+ }
472
+
473
+ .assistant-toolbar .timestamp {
474
+ margin-right: 8px;
475
+ }
476
+
438
477
  .tool-args {
439
478
  padding: 12px;
440
479
  background: var(--bg-body);
@@ -455,8 +494,7 @@
455
494
  font-family: var(--font-mono);
456
495
  font-size: var(--font-size-xs);
457
496
  text-transform: uppercase;
458
- letter-spacing: 0.05em;
459
- font-weight: 700;
497
+ font-weight: var(--font-weight-bold);
460
498
  cursor: pointer;
461
499
  user-select: none;
462
500
  list-style: none;
@@ -557,7 +595,7 @@
557
595
 
558
596
  /* Markdown Elements */
559
597
  .markdown-body {
560
- font-family: var(--font-sans);
598
+ font-family: var(--font-markdown);
561
599
  }
562
600
  .markdown-body hr {
563
601
  height: 0;
@@ -574,7 +612,7 @@
574
612
  border: 1px solid var(--border);
575
613
  }
576
614
  .markdown-body code {
577
- font-family: var(--font-mono);
615
+ font-family: var(--font-markdown);
578
616
  font-size: 0.9em;
579
617
  background: var(--bg-code);
580
618
  padding: 2px 4px;
@@ -648,8 +686,7 @@
648
686
  font-family: var(--font-mono);
649
687
  font-size: var(--font-size-xs);
650
688
  text-transform: uppercase;
651
- letter-spacing: 0.05em;
652
- font-weight: 700;
689
+ font-weight: var(--font-weight-bold);
653
690
  cursor: pointer;
654
691
  user-select: none;
655
692
  list-style: none;
@@ -689,6 +726,17 @@
689
726
  padding-top: 24px;
690
727
  }
691
728
 
729
+ .footer-link {
730
+ color: inherit;
731
+ text-decoration: none;
732
+ font-weight: 600;
733
+ transition: color 0.2s;
734
+ }
735
+
736
+ .footer-link:hover {
737
+ color: var(--accent);
738
+ }
739
+
692
740
  .expandable {
693
741
  cursor: pointer;
694
742
  }
@@ -757,6 +805,42 @@
757
805
  color: var(--text-dim);
758
806
  }
759
807
 
808
+ /* Scroll to Bottom Button */
809
+ .scroll-btn {
810
+ position: fixed;
811
+ bottom: 24px;
812
+ right: 24px;
813
+ width: 40px;
814
+ height: 40px;
815
+ border-radius: 50%;
816
+ background: var(--bg-card);
817
+ border: 1px solid var(--border);
818
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
819
+ color: var(--text-dim);
820
+ display: flex;
821
+ align-items: center;
822
+ justify-content: center;
823
+ cursor: pointer;
824
+ opacity: 0;
825
+ visibility: hidden;
826
+ transform: translateY(10px);
827
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
828
+ z-index: 100;
829
+ }
830
+
831
+ .scroll-btn:hover {
832
+ color: var(--accent);
833
+ border-color: var(--accent);
834
+ transform: translateY(-2px);
835
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
836
+ }
837
+
838
+ .scroll-btn.visible {
839
+ opacity: 1;
840
+ visibility: visible;
841
+ transform: translateY(0);
842
+ }
843
+
760
844
  /* Tool details in Available Tools section */
761
845
  .tool-details {
762
846
  margin-bottom: 2px;
@@ -798,9 +882,8 @@
798
882
  }
799
883
  .tool-params-title {
800
884
  font-size: var(--font-size-xs);
801
- font-weight: 700;
885
+ font-weight: var(--font-weight-bold);
802
886
  text-transform: uppercase;
803
- letter-spacing: 0.05em;
804
887
  color: var(--text-dim);
805
888
  margin-bottom: 8px;
806
889
  }
@@ -858,9 +941,7 @@
858
941
  <div
859
942
  class="details-content system-prompt-content"
860
943
  style="font-family: var(--font-mono); white-space: pre-wrap"
861
- >
862
- $system_prompt
863
- </div>
944
+ >$system_prompt</div>
864
945
  </details>
865
946
 
866
947
  <details class="collapsible-section">
@@ -871,17 +952,48 @@
871
952
  <div class="message-stream">$messages_html</div>
872
953
 
873
954
  <div class="footer">
874
- Generated by klaude-code &bull; $footer_time &bull; $total_messages
875
- messages
955
+ Generated by
956
+ <a
957
+ href="https://github.com/inspirepan/klaude-code"
958
+ class="footer-link"
959
+ target="_blank"
960
+ >klaude-code</a
961
+ >
962
+ &bull; $footer_time &bull; $total_messages messages
876
963
  </div>
877
964
  </div>
878
965
 
966
+ <div id="scroll-btn" class="scroll-btn" title="Scroll to bottom">
967
+ <svg
968
+ xmlns="http://www.w3.org/2000/svg"
969
+ width="20"
970
+ height="20"
971
+ viewBox="0 0 24 24"
972
+ fill="none"
973
+ stroke="currentColor"
974
+ stroke-width="2"
975
+ stroke-linecap="round"
976
+ stroke-linejoin="round"
977
+ >
978
+ <line x1="12" y1="5" x2="12" y2="19"></line>
979
+ <polyline points="19 12 12 19 5 12"></polyline>
980
+ </svg>
981
+ </div>
982
+
879
983
  <link
880
984
  rel="stylesheet"
881
985
  href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css"
882
986
  />
883
987
  <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
884
988
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
989
+ <script type="module">
990
+ import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
991
+ mermaid.initialize({
992
+ startOnLoad: true,
993
+ theme: "neutral",
994
+ fontFamily: '"Iosevka", "SF Mono", Menlo, monospace',
995
+ });
996
+ </script>
885
997
  <script>
886
998
  // Markdown rendering and Syntax Highlighting
887
999
  document.querySelectorAll(".markdown-content").forEach((el) => {
@@ -1077,6 +1189,32 @@
1077
1189
  }
1078
1190
  });
1079
1191
  });
1192
+
1193
+ // Scroll to bottom button
1194
+ const scrollBtn = document.getElementById("scroll-btn");
1195
+
1196
+ function updateScrollBtn() {
1197
+ // Show button if we are not at the bottom
1198
+ const isAtBottom =
1199
+ window.innerHeight + window.scrollY >=
1200
+ document.body.offsetHeight - 100;
1201
+ if (isAtBottom) {
1202
+ scrollBtn.classList.remove("visible");
1203
+ } else {
1204
+ scrollBtn.classList.add("visible");
1205
+ }
1206
+ }
1207
+
1208
+ window.addEventListener("scroll", updateScrollBtn);
1209
+ window.addEventListener("resize", updateScrollBtn);
1210
+ updateScrollBtn(); // Initial check
1211
+
1212
+ scrollBtn.addEventListener("click", () => {
1213
+ window.scrollTo({
1214
+ top: document.body.scrollHeight,
1215
+ behavior: "smooth",
1216
+ });
1217
+ });
1080
1218
  </script>
1081
1219
  </body>
1082
1220
  </html>
@@ -21,7 +21,7 @@ Factory Functions:
21
21
  from .core.display import DisplayABC
22
22
  from .core.input import InputProviderABC
23
23
  from .modes.debug.display import DebugEventDisplay
24
- from .modes.exec.display import ExecDisplay
24
+ from .modes.exec.display import ExecDisplay, StreamJsonDisplay
25
25
 
26
26
  # --- Display Mode Implementations ---
27
27
  from .modes.repl.display import REPLDisplay
@@ -53,16 +53,19 @@ def create_default_display(
53
53
  return repl_display
54
54
 
55
55
 
56
- def create_exec_display(debug: bool = False) -> DisplayABC:
56
+ def create_exec_display(debug: bool = False, stream_json: bool = False) -> DisplayABC:
57
57
  """
58
58
  Create a display for exec (non-interactive) mode.
59
59
 
60
60
  Args:
61
61
  debug: If True, wrap the display with DebugEventDisplay to log all events.
62
+ stream_json: If True, stream all events as JSON lines instead of normal output.
62
63
 
63
64
  Returns:
64
65
  A DisplayABC implementation that only outputs task results.
65
66
  """
67
+ if stream_json:
68
+ return StreamJsonDisplay()
66
69
  exec_display = ExecDisplay()
67
70
  if debug:
68
71
  return DebugEventDisplay(exec_display)
@@ -76,6 +79,7 @@ __all__ = [
76
79
  # Display mode implementations
77
80
  "REPLDisplay",
78
81
  "ExecDisplay",
82
+ "StreamJsonDisplay",
79
83
  "DebugEventDisplay",
80
84
  # Input implementations
81
85
  "PromptToolkitInput",
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  from typing import override
2
3
 
3
4
  from klaude_code.protocol import events
@@ -35,3 +36,28 @@ class ExecDisplay(DisplayABC):
35
36
  async def stop(self) -> None:
36
37
  """Do nothing on stop."""
37
38
  pass
39
+
40
+
41
+ class StreamJsonDisplay(DisplayABC):
42
+ """A display implementation that streams all events as JSON lines."""
43
+
44
+ @override
45
+ async def consume_event(self, event: events.Event) -> None:
46
+ """Stream each event as a JSON line."""
47
+ if isinstance(event, events.EndEvent):
48
+ return
49
+ event_type = type(event).__name__
50
+ json_data = event.model_dump_json()
51
+ # Output format: {"type": "EventName", "data": {...}}
52
+ print(f'{{"type": "{event_type}", "data": {json_data}}}', flush=True)
53
+ sys.stdout.flush()
54
+
55
+ @override
56
+ async def start(self) -> None:
57
+ """Do nothing on start."""
58
+ pass
59
+
60
+ @override
61
+ async def stop(self) -> None:
62
+ """Do nothing on stop."""
63
+ pass
@@ -107,7 +107,7 @@ class SpinnerStatusState:
107
107
  if self._base_status:
108
108
  result = Text(self._base_status)
109
109
  if activity_text:
110
- result.append(" ")
110
+ result.append(" | ")
111
111
  result.append_text(activity_text)
112
112
  return result
113
113
  if activity_text:
@@ -286,12 +286,14 @@ class DisplayEventHandler:
286
286
  self.renderer.display_task_finish(event)
287
287
  if not self.renderer.is_sub_agent_session(event.session_id):
288
288
  emit_osc94(OSC94States.HIDDEN)
289
+ self.spinner_status.reset()
289
290
  self.renderer.spinner_stop()
290
291
  await self.stage_manager.transition_to(Stage.WAITING)
291
292
  self._maybe_notify_task_finish(event)
292
293
 
293
294
  async def _on_interrupt(self, event: events.InterruptEvent) -> None:
294
295
  self.renderer.spinner_stop()
296
+ self.spinner_status.reset()
295
297
  await self.stage_manager.transition_to(Stage.WAITING)
296
298
  emit_osc94(OSC94States.HIDDEN)
297
299
  self.renderer.display_interrupt()
@@ -302,11 +304,13 @@ class DisplayEventHandler:
302
304
  self.renderer.display_error(event)
303
305
  if not event.can_retry:
304
306
  self.renderer.spinner_stop()
307
+ self.spinner_status.reset()
305
308
 
306
309
  async def _on_end(self, event: events.EndEvent) -> None:
307
310
  emit_osc94(OSC94States.HIDDEN)
308
311
  await self.stage_manager.transition_to(Stage.WAITING)
309
312
  self.renderer.spinner_stop()
313
+ self.spinner_status.reset()
310
314
 
311
315
  # ─────────────────────────────────────────────────────────────────────────────
312
316
  # Private helper methods
@@ -16,7 +16,7 @@ def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
16
16
  or e.item.external_file_changes
17
17
  or e.item.todo_use
18
18
  or e.item.at_files
19
- or e.item.clipboard_images
19
+ or e.item.user_image_count
20
20
  )
21
21
 
22
22
 
@@ -74,16 +74,12 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
74
74
  )
75
75
  parts.append(grid)
76
76
 
77
- if ci := e.item.clipboard_images:
77
+ if uic := e.item.user_image_count:
78
78
  grid = create_grid()
79
- for img_tag in ci:
80
- grid.add_row(
81
- Text(" +", style=ThemeKey.REMINDER),
82
- Text.assemble(
83
- ("Read ", ThemeKey.REMINDER),
84
- Text(f"{img_tag} Image", style=ThemeKey.REMINDER_BOLD),
85
- ),
86
- )
79
+ grid.add_row(
80
+ Text(" +", style=ThemeKey.REMINDER),
81
+ Text(f"Attached {uic} image{'s' if uic > 1 else ''}", style=ThemeKey.REMINDER),
82
+ )
87
83
  parts.append(grid)
88
84
 
89
85
  return Group(*parts) if parts else Text("")
@@ -41,33 +41,32 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
41
41
 
42
42
  if metadata.usage is not None:
43
43
  # Input
44
- parts.append(
45
- Text.assemble(
46
- ("input:", ThemeKey.METADATA_DIM),
47
- (format_number(metadata.usage.input_tokens), ThemeKey.METADATA_DIM),
48
- )
49
- )
44
+ input_parts: list[tuple[str, str]] = [
45
+ ("input:", ThemeKey.METADATA_DIM),
46
+ (format_number(metadata.usage.input_tokens), ThemeKey.METADATA_DIM),
47
+ ]
48
+ if metadata.usage.input_cost is not None:
49
+ input_parts.append((f"(${metadata.usage.input_cost:.4f})", ThemeKey.METADATA_DIM))
50
+ parts.append(Text.assemble(*input_parts))
50
51
 
51
52
  # Cached
52
53
  if metadata.usage.cached_tokens > 0:
53
- parts.append(
54
- Text.assemble(
55
- ("cached", ThemeKey.METADATA_DIM),
56
- (":", ThemeKey.METADATA_DIM),
57
- (
58
- format_number(metadata.usage.cached_tokens),
59
- ThemeKey.METADATA_DIM,
60
- ),
61
- )
62
- )
54
+ cached_parts: list[tuple[str, str]] = [
55
+ ("cached:", ThemeKey.METADATA_DIM),
56
+ (format_number(metadata.usage.cached_tokens), ThemeKey.METADATA_DIM),
57
+ ]
58
+ if metadata.usage.cache_read_cost is not None:
59
+ cached_parts.append((f"(${metadata.usage.cache_read_cost:.4f})", ThemeKey.METADATA_DIM))
60
+ parts.append(Text.assemble(*cached_parts))
63
61
 
64
62
  # Output
65
- parts.append(
66
- Text.assemble(
67
- ("output:", ThemeKey.METADATA_DIM),
68
- (format_number(metadata.usage.output_tokens), ThemeKey.METADATA_DIM),
69
- )
70
- )
63
+ output_parts: list[tuple[str, str]] = [
64
+ ("output:", ThemeKey.METADATA_DIM),
65
+ (format_number(metadata.usage.output_tokens), ThemeKey.METADATA_DIM),
66
+ ]
67
+ if metadata.usage.output_cost is not None:
68
+ output_parts.append((f"(${metadata.usage.output_cost:.4f})", ThemeKey.METADATA_DIM))
69
+ parts.append(Text.assemble(*output_parts))
71
70
 
72
71
  # Reasoning
73
72
  if metadata.usage.reasoning_tokens > 0:
@@ -105,16 +104,26 @@ def render_response_metadata(e: events.ResponseMetadataEvent) -> RenderableType:
105
104
  )
106
105
  )
107
106
 
108
- # Cost
107
+ # Duration
109
108
  if metadata.task_duration_s is not None:
110
109
  parts.append(
111
110
  Text.assemble(
112
- ("cost", ThemeKey.METADATA_DIM),
111
+ ("time", ThemeKey.METADATA_DIM),
113
112
  (":", ThemeKey.METADATA_DIM),
114
113
  (f"{metadata.task_duration_s:.1f}s", ThemeKey.METADATA_DIM),
115
114
  )
116
115
  )
117
116
 
117
+ # Cost (USD)
118
+ if metadata.usage is not None and metadata.usage.total_cost is not None:
119
+ parts.append(
120
+ Text.assemble(
121
+ ("cost", ThemeKey.METADATA_DIM),
122
+ (":", ThemeKey.METADATA_DIM),
123
+ (f"${metadata.usage.total_cost:.4f}", ThemeKey.METADATA_DIM),
124
+ )
125
+ )
126
+
118
127
  if parts:
119
128
  line2 = Text("/", style=ThemeKey.METADATA_DIM).join(parts)
120
129
  renderables.append(Padding(line2, (0, 0, 0, 2)))
@@ -6,8 +6,8 @@ from rich.style import Style
6
6
  from rich.text import Text
7
7
 
8
8
  from klaude_code import const
9
- from klaude_code.core.sub_agent import get_sub_agent_profile_by_tool
10
9
  from klaude_code.protocol import events, model
10
+ from klaude_code.protocol.sub_agent import get_sub_agent_profile_by_tool
11
11
  from klaude_code.ui.rich.markdown import NoInsetMarkdown
12
12
  from klaude_code.ui.rich.theme import ThemeKey
13
13
 
@@ -6,8 +6,8 @@ from rich.padding import Padding
6
6
  from rich.text import Text
7
7
 
8
8
  from klaude_code import const
9
- from klaude_code.core.sub_agent import is_sub_agent_tool as _is_sub_agent_tool
10
9
  from klaude_code.protocol import events, model
10
+ from klaude_code.protocol.sub_agent import is_sub_agent_tool as _is_sub_agent_tool
11
11
  from klaude_code.ui.renderers import diffs as r_diffs
12
12
  from klaude_code.ui.renderers.common import create_grid
13
13
  from klaude_code.ui.rich.theme import ThemeKey
@@ -449,7 +449,7 @@ def get_tool_active_form(tool_name: str) -> str:
449
449
  return _TOOL_ACTIVE_FORM[tool_name]
450
450
 
451
451
  # Check sub agent profiles
452
- from klaude_code.core.sub_agent import get_sub_agent_profile_by_tool
452
+ from klaude_code.protocol.sub_agent import get_sub_agent_profile_by_tool
453
453
 
454
454
  profile = get_sub_agent_profile_by_tool(tool_name)
455
455
  if profile and profile.active_form: