klaude-code 1.2.16__py3-none-any.whl → 1.2.18__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 (70) hide show
  1. klaude_code/cli/config_cmd.py +1 -1
  2. klaude_code/cli/debug.py +1 -1
  3. klaude_code/cli/main.py +3 -9
  4. klaude_code/cli/runtime.py +20 -13
  5. klaude_code/command/__init__.py +7 -1
  6. klaude_code/command/clear_cmd.py +2 -7
  7. klaude_code/command/command_abc.py +33 -5
  8. klaude_code/command/debug_cmd.py +79 -0
  9. klaude_code/command/diff_cmd.py +2 -6
  10. klaude_code/command/export_cmd.py +7 -7
  11. klaude_code/command/export_online_cmd.py +145 -0
  12. klaude_code/command/help_cmd.py +4 -9
  13. klaude_code/command/model_cmd.py +10 -6
  14. klaude_code/command/prompt_command.py +2 -6
  15. klaude_code/command/refresh_cmd.py +2 -7
  16. klaude_code/command/registry.py +2 -4
  17. klaude_code/command/release_notes_cmd.py +2 -6
  18. klaude_code/command/status_cmd.py +2 -7
  19. klaude_code/command/terminal_setup_cmd.py +2 -6
  20. klaude_code/command/thinking_cmd.py +13 -8
  21. klaude_code/config/config.py +16 -17
  22. klaude_code/config/select_model.py +81 -5
  23. klaude_code/const/__init__.py +1 -1
  24. klaude_code/core/executor.py +236 -109
  25. klaude_code/core/manager/__init__.py +2 -4
  26. klaude_code/core/manager/sub_agent_manager.py +1 -1
  27. klaude_code/core/prompts/prompt-claude-code.md +1 -1
  28. klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -1
  29. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  30. klaude_code/core/reminders.py +9 -35
  31. klaude_code/core/task.py +8 -0
  32. klaude_code/core/tool/__init__.py +2 -0
  33. klaude_code/core/tool/file/read_tool.py +38 -10
  34. klaude_code/core/tool/report_back_tool.py +28 -2
  35. klaude_code/core/tool/shell/bash_tool.py +22 -2
  36. klaude_code/core/tool/tool_runner.py +26 -23
  37. klaude_code/core/tool/truncation.py +23 -9
  38. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  39. klaude_code/core/tool/web/web_fetch_tool.py +36 -1
  40. klaude_code/core/tool/web/web_search_tool.md +23 -0
  41. klaude_code/core/tool/web/web_search_tool.py +126 -0
  42. klaude_code/core/turn.py +28 -0
  43. klaude_code/protocol/commands.py +2 -0
  44. klaude_code/protocol/events.py +8 -0
  45. klaude_code/protocol/sub_agent/__init__.py +1 -1
  46. klaude_code/protocol/sub_agent/explore.py +1 -1
  47. klaude_code/protocol/sub_agent/web.py +79 -0
  48. klaude_code/protocol/tools.py +1 -0
  49. klaude_code/session/session.py +2 -2
  50. klaude_code/session/templates/export_session.html +123 -37
  51. klaude_code/trace/__init__.py +20 -2
  52. klaude_code/ui/modes/repl/completers.py +19 -2
  53. klaude_code/ui/modes/repl/event_handler.py +44 -15
  54. klaude_code/ui/modes/repl/renderer.py +3 -3
  55. klaude_code/ui/renderers/metadata.py +2 -4
  56. klaude_code/ui/renderers/sub_agent.py +14 -10
  57. klaude_code/ui/renderers/thinking.py +24 -8
  58. klaude_code/ui/renderers/tools.py +83 -20
  59. klaude_code/ui/rich/code_panel.py +112 -0
  60. klaude_code/ui/rich/markdown.py +3 -4
  61. klaude_code/ui/rich/status.py +30 -6
  62. klaude_code/ui/rich/theme.py +10 -1
  63. {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/METADATA +126 -25
  64. {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/RECORD +67 -63
  65. klaude_code/core/manager/agent_manager.py +0 -132
  66. klaude_code/core/prompts/prompt-sub-agent-webfetch.md +0 -46
  67. klaude_code/protocol/sub_agent/web_fetch.py +0 -74
  68. /klaude_code/{config → cli}/list_model.py +0 -0
  69. {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/WHEEL +0 -0
  70. {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/entry_points.txt +0 -0
@@ -133,6 +133,13 @@ class TodoChangeEvent(BaseModel):
133
133
  todos: list[model.TodoItem]
134
134
 
135
135
 
136
+ class ContextUsageEvent(BaseModel):
137
+ """Real-time context usage update during task execution."""
138
+
139
+ session_id: str
140
+ context_percent: float # Context usage percentage (0-100)
141
+
142
+
136
143
  HistoryItemEvent = (
137
144
  ThinkingEvent
138
145
  | TaskStartEvent
@@ -178,4 +185,5 @@ Event = (
178
185
  | TurnStartEvent
179
186
  | TurnEndEvent
180
187
  | TurnToolCallStartEvent
188
+ | ContextUsageEvent
181
189
  )
@@ -114,4 +114,4 @@ def sub_agent_tool_names(enabled_only: bool = False, model_name: str | None = No
114
114
  from klaude_code.protocol.sub_agent import explore as explore # noqa: E402
115
115
  from klaude_code.protocol.sub_agent import oracle as oracle # noqa: E402
116
116
  from klaude_code.protocol.sub_agent import task as task # noqa: E402
117
- from klaude_code.protocol.sub_agent import web_fetch as web_fetch # noqa: E402
117
+ from klaude_code.protocol.sub_agent import web as web # noqa: E402
@@ -37,7 +37,7 @@ EXPLORE_PARAMETERS = {
37
37
  "description": "Optional JSON Schema for sub-agent structured output",
38
38
  },
39
39
  },
40
- "required": ["description", "prompt", "output_format"],
40
+ "required": ["description", "prompt"],
41
41
  "additionalProperties": False,
42
42
  }
43
43
 
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from klaude_code.protocol import tools
6
+ from klaude_code.protocol.sub_agent import SubAgentProfile, register_sub_agent
7
+
8
+ WEB_AGENT_DESCRIPTION = """\
9
+ Launch a sub-agent to search the web, fetch pages, and analyze content. Use this for:
10
+ - Accessing up-to-date information beyond your knowledge cutoff (current events, recent releases, latest docs)
11
+ - Researching topics, news, APIs, or technical references
12
+ - Fetching and analyzing specific URLs
13
+ - Gathering comprehensive information from multiple web sources
14
+
15
+ Capabilities:
16
+ - Search the web to find relevant pages (no URL required)
17
+ - Fetch and parse web pages (HTML-to-Markdown conversion)
18
+ - Follow links across multiple pages autonomously
19
+ - Aggregate findings from multiple sources
20
+
21
+ How to use:
22
+ - Write a clear prompt describing what information you need - the agent will search and fetch as needed
23
+ - Account for "Today's date" in <env>. For example, if <env> says "Today's date: 2025-07-01", and the user wants the latest docs, do not use 2024 in the search query. Use 2025.
24
+ - Optionally provide a `url` if you already know the target page
25
+ - Use `output_format` (JSON Schema) to get structured data back from the agent
26
+
27
+ What you receive:
28
+ - The agent returns a text response summarizing its findings
29
+ - With `output_format`, you receive structured JSON matching your schema
30
+ - The response is the agent's analysis, not raw web content
31
+ - Web content is saved to local files (paths included in Sources) - read them directly if you need full content\
32
+ """
33
+
34
+ WEB_AGENT_PARAMETERS = {
35
+ "type": "object",
36
+ "properties": {
37
+ "description": {
38
+ "type": "string",
39
+ "description": "A short (3-5 word) description of the task",
40
+ },
41
+ "url": {
42
+ "type": "string",
43
+ "description": "The URL to fetch and analyze. If not provided, the agent will search the web first",
44
+ },
45
+ "prompt": {
46
+ "type": "string",
47
+ "description": "Instructions for searching, analyzing, or extracting content from the web page",
48
+ },
49
+ "output_format": {
50
+ "type": "object",
51
+ "description": "Optional JSON Schema for sub-agent structured output",
52
+ },
53
+ },
54
+ "required": ["description", "prompt"],
55
+ "additionalProperties": False,
56
+ }
57
+
58
+
59
+ def _web_agent_prompt_builder(args: dict[str, Any]) -> str:
60
+ """Build the WebAgent prompt from tool arguments."""
61
+ url = args.get("url", "")
62
+ prompt = args.get("prompt", "")
63
+ if url:
64
+ return f"URL to fetch: {url}\nTask: {prompt}"
65
+ return prompt
66
+
67
+
68
+ register_sub_agent(
69
+ SubAgentProfile(
70
+ name="WebAgent",
71
+ description=WEB_AGENT_DESCRIPTION,
72
+ parameters=WEB_AGENT_PARAMETERS,
73
+ prompt_file="prompts/prompt-sub-agent-web.md",
74
+ tool_set=(tools.BASH, tools.READ, tools.WEB_FETCH, tools.WEB_SEARCH),
75
+ prompt_builder=_web_agent_prompt_builder,
76
+ active_form="Surfing",
77
+ output_schema_arg="output_format",
78
+ )
79
+ )
@@ -10,6 +10,7 @@ SKILL = "Skill"
10
10
  MERMAID = "Mermaid"
11
11
  MEMORY = "Memory"
12
12
  WEB_FETCH = "WebFetch"
13
+ WEB_SEARCH = "WebSearch"
13
14
  REPORT_BACK = "report_back"
14
15
 
15
16
  # SubAgentType is just a string alias now; agent types are defined via SubAgentProfile
@@ -36,7 +36,7 @@ class Session(BaseModel):
36
36
 
37
37
  @property
38
38
  def messages_count(self) -> int:
39
- """Count of user and assistant messages in conversation history.
39
+ """Count of user, assistant messages, and tool calls in conversation history.
40
40
 
41
41
  This is a cached property that is invalidated when append_history is called.
42
42
  """
@@ -44,7 +44,7 @@ class Session(BaseModel):
44
44
  self._messages_count_cache = sum(
45
45
  1
46
46
  for it in self.conversation_history
47
- if isinstance(it, (model.UserMessageItem, model.AssistantMessageItem))
47
+ if isinstance(it, (model.UserMessageItem, model.AssistantMessageItem, model.ToolCallItem))
48
48
  )
49
49
  return self._messages_count_cache
50
50
 
@@ -9,22 +9,36 @@
9
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
10
  />
11
11
  <link
12
- href="https://cdn.jsdelivr.net/npm/@fontsource/geist-sans/latin-400.css"
12
+ href="https://cdn.jsdelivr.net/npm/@fontsource/source-sans-3/400.css"
13
13
  rel="stylesheet"
14
14
  />
15
15
  <link
16
- href="https://cdn.jsdelivr.net/npm/@fontsource/geist-sans/latin-500.css"
16
+ href="https://cdn.jsdelivr.net/npm/@fontsource/source-sans-3/400-italic.css"
17
17
  rel="stylesheet"
18
18
  />
19
19
  <link
20
- href="https://cdn.jsdelivr.net/npm/@fontsource/geist-sans/latin-700.css"
20
+ href="https://cdn.jsdelivr.net/npm/@fontsource/source-sans-3/700.css"
21
+ rel="stylesheet"
22
+ />
23
+ <link
24
+ href="https://cdn.jsdelivr.net/npm/@fontsource/source-sans-3/700-italic.css"
25
+ rel="stylesheet"
26
+ />
27
+ <link
28
+ href="https://cdn.jsdelivr.net/npm/@fontsource/fira-code/400.css"
29
+ rel="stylesheet"
30
+ />
31
+ <link
32
+ href="https://cdn.jsdelivr.net/npm/@fontsource/fira-code/700.css"
21
33
  rel="stylesheet"
22
34
  />
23
35
  <style>
24
36
  :root {
25
- --bg-body: #ededed;
26
- --bg-container: #f0f0f0;
27
- --bg-card: #f0f0f0;
37
+ --bg-body: #eae9e5;
38
+ --bg-container: #edece9;
39
+ --bg-card: #efeeeb;
40
+ --bg-error: #ffebee;
41
+ --bg-code: #f2f1ed;
28
42
  --border: #c8c8c8;
29
43
  --text: #111111;
30
44
  --text-dim: #64748b;
@@ -32,13 +46,11 @@
32
46
  --accent-dim: rgba(8, 145, 178, 0.08);
33
47
  --success: #15803d;
34
48
  --error: #dc2626;
35
- --bg-error: #ffebee;
36
- --bg-code: #f3f3f3;
37
49
  --fg-inline-code: #4f4fc7;
38
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
39
- --font-markdown-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
40
- --font-markdown: "Geist Sans", system-ui, sans-serif;
41
- --font-weight-bold: 800;
50
+ --font-mono: "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
51
+ --font-markdown-mono: "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
52
+ --font-markdown: "Source Sans 3", system-ui, sans-serif;
53
+ --font-weight-bold: 700;
42
54
  --font-size-xs: 13px;
43
55
  --font-size-sm: 14px;
44
56
  --font-size-base: 15px;
@@ -62,7 +74,6 @@
62
74
  background-color: var(--bg-body);
63
75
  color: var(--text);
64
76
  font-family: var(--font-mono);
65
- font-feature-settings: "ss18";
66
77
  line-height: 1.6;
67
78
  font-size: var(--font-size-lg);
68
79
  -webkit-font-smoothing: antialiased;
@@ -672,8 +683,102 @@
672
683
  /* Markdown Elements */
673
684
  .markdown-body {
674
685
  font-family: var(--font-markdown);
675
- line-height: 1.5;
686
+ line-height: 1.6;
687
+ font-size: var(--font-size-base);
688
+ }
689
+ .markdown-body > *:first-child {
690
+ margin-top: 0 !important;
691
+ }
692
+ .markdown-body > *:last-child {
693
+ margin-bottom: 0 !important;
694
+ }
695
+
696
+ .markdown-body h1,
697
+ .markdown-body h2,
698
+ .markdown-body h3,
699
+ .markdown-body h4,
700
+ .markdown-body h5,
701
+ .markdown-body h6 {
702
+ margin-top: 24px;
703
+ margin-bottom: 16px;
704
+ font-weight: var(--font-weight-bold);
705
+ line-height: 1.25;
706
+ }
707
+
708
+ .markdown-body a {
709
+ color: var(--accent);
710
+ text-decoration: none;
711
+ border-bottom: 1px solid rgba(8, 81, 178, 0.2);
712
+ transition: border-color 0.2s, background-color 0.2s;
713
+ }
714
+ .markdown-body a:hover {
715
+ border-bottom-color: var(--accent);
716
+ background-color: rgba(8, 81, 178, 0.05);
717
+ border-radius: 2px;
718
+ }
719
+
720
+ .markdown-body p {
721
+ margin-bottom: 8px; /* Tighter spacing */
722
+ }
723
+
724
+ .markdown-body ul,
725
+ .markdown-body ol {
726
+ margin-bottom: 8px; /* Tighter spacing */
727
+ padding-left: 1.5rem;
728
+ list-style-position: outside;
729
+ }
730
+
731
+ .markdown-body ul ul,
732
+ .markdown-body ol ul,
733
+ .markdown-body ul ol,
734
+ .markdown-body ol ol {
735
+ margin-top: 0;
736
+ margin-bottom: 0;
737
+ }
738
+
739
+ .markdown-body li > p {
740
+ margin-bottom: 0;
741
+ }
742
+ .markdown-body li + li {
743
+ margin-top: 0.25em;
744
+ }
745
+
746
+ .markdown-body blockquote {
747
+ margin: 0 0 16px;
748
+ padding: 0 1em;
749
+ color: var(--text-dim);
750
+ border-left: 0.25em solid var(--border);
676
751
  }
752
+
753
+ .markdown-body table {
754
+ border-collapse: collapse;
755
+ width: 100%;
756
+ margin-top: 8px;
757
+ margin-bottom: 16px;
758
+ display: block;
759
+ overflow-x: auto;
760
+ }
761
+
762
+ .markdown-body table tr {
763
+ background-color: var(--bg-card);
764
+ border-top: 1px solid var(--border);
765
+ }
766
+
767
+ .markdown-body table tr:nth-child(2n) {
768
+ background-color: rgba(0, 0, 0, 0.02);
769
+ }
770
+
771
+ .markdown-body table th,
772
+ .markdown-body table td {
773
+ padding: 6px 13px;
774
+ border: 1px solid var(--border);
775
+ }
776
+
777
+ .markdown-body table th {
778
+ font-weight: var(--font-weight-bold);
779
+ background-color: rgba(0, 0, 0, 0.04);
780
+ }
781
+
677
782
  .markdown-body hr {
678
783
  height: 0;
679
784
  margin: 24px 0;
@@ -682,10 +787,10 @@
682
787
  }
683
788
  .markdown-body pre {
684
789
  background: var(--bg-code);
685
- padding: 16px;
790
+ padding: 12px;
686
791
  border-radius: var(--radius-md);
687
792
  overflow-x: auto;
688
- margin: 12px 0;
793
+ margin: 8px 0 16px 0;
689
794
  border: 1px solid var(--border);
690
795
  }
691
796
  .markdown-body code {
@@ -694,36 +799,17 @@
694
799
  font-size: var(--font-size-sm);
695
800
  padding: 2px 4px;
696
801
  border-radius: var(--radius-sm);
802
+ background-color: rgba(0, 0, 0, 0.05); /* Slight bg for inline code */
697
803
  }
698
804
  .markdown-body pre code {
699
805
  background: transparent;
700
806
  padding: 0;
701
807
  border-radius: 0;
808
+ color: inherit;
702
809
  }
703
810
  .markdown-body pre code.hljs {
704
811
  background: transparent;
705
812
  }
706
- .markdown-body p {
707
- margin-bottom: 12px;
708
- }
709
- .markdown-body > *:first-child {
710
- margin-top: 0;
711
- }
712
- .markdown-body > *:last-child {
713
- margin-bottom: 0;
714
- }
715
- .markdown-body ul,
716
- .markdown-body ol {
717
- margin-bottom: 12px;
718
- padding-left: 1.5rem;
719
- list-style-position: outside;
720
- }
721
- .markdown-body ul ul,
722
- .markdown-body ol ul,
723
- .markdown-body ul ol,
724
- .markdown-body ol ol {
725
- margin-left: 1rem;
726
- }
727
813
 
728
814
  /* Diff View */
729
815
  .diff-view {
@@ -1,3 +1,21 @@
1
- from .log import DebugType, is_debug_enabled, log, log_debug, logger, prepare_debug_log_file, set_debug_logging
1
+ from .log import (
2
+ DebugType,
3
+ get_current_log_file,
4
+ is_debug_enabled,
5
+ log,
6
+ log_debug,
7
+ logger,
8
+ prepare_debug_log_file,
9
+ set_debug_logging,
10
+ )
2
11
 
3
- __all__ = ["DebugType", "is_debug_enabled", "log", "log_debug", "logger", "prepare_debug_log_file", "set_debug_logging"]
12
+ __all__ = [
13
+ "DebugType",
14
+ "get_current_log_file",
15
+ "is_debug_enabled",
16
+ "log",
17
+ "log_debug",
18
+ "logger",
19
+ "prepare_debug_log_file",
20
+ "set_debug_logging",
21
+ ]
@@ -26,6 +26,7 @@ from prompt_toolkit.document import Document
26
26
  from prompt_toolkit.formatted_text import HTML
27
27
 
28
28
  from klaude_code.command import get_commands
29
+ from klaude_code.trace.log import DebugType, log_debug
29
30
 
30
31
  # Pattern to match @token for completion refresh (used by key bindings).
31
32
  # Supports both plain tokens like `@src/file.py` and quoted tokens like
@@ -85,7 +86,7 @@ class _SlashCommandCompleter(Completer):
85
86
  matched: list[tuple[str, object, str]] = []
86
87
  for cmd_name, cmd_obj in commands.items():
87
88
  if cmd_name.startswith(frag):
88
- hint = " [instructions]" if cmd_obj.support_addition_params else ""
89
+ hint = f" [{cmd_obj.placeholder}]" if cmd_obj.support_addition_params else ""
89
90
  matched.append((cmd_name, cmd_obj, hint))
90
91
 
91
92
  if not matched:
@@ -444,6 +445,8 @@ class _AtFilesCompleter(Completer):
444
445
  return items[: min(self._max_results, 100)]
445
446
 
446
447
  def _run_cmd(self, cmd: list[str], cwd: Path | None = None) -> _CmdResult:
448
+ cmd_str = " ".join(cmd)
449
+ start = time.monotonic()
447
450
  try:
448
451
  p = subprocess.run(
449
452
  cmd,
@@ -453,9 +456,23 @@ class _AtFilesCompleter(Completer):
453
456
  text=True,
454
457
  timeout=1.5,
455
458
  )
459
+ elapsed_ms = (time.monotonic() - start) * 1000
456
460
  if p.returncode == 0:
457
461
  lines = [ln.strip() for ln in p.stdout.splitlines() if ln.strip()]
462
+ log_debug(
463
+ f"[completer] cmd={cmd_str} elapsed={elapsed_ms:.1f}ms results={len(lines)}",
464
+ debug_type=DebugType.EXECUTION,
465
+ )
458
466
  return _CmdResult(True, lines)
467
+ log_debug(
468
+ f"[completer] cmd={cmd_str} elapsed={elapsed_ms:.1f}ms returncode={p.returncode}",
469
+ debug_type=DebugType.EXECUTION,
470
+ )
459
471
  return _CmdResult(False, [])
460
- except Exception:
472
+ except Exception as e:
473
+ elapsed_ms = (time.monotonic() - start) * 1000
474
+ log_debug(
475
+ f"[completer] cmd={cmd_str} elapsed={elapsed_ms:.1f}ms error={e!r}",
476
+ debug_type=DebugType.EXECUTION,
477
+ )
461
478
  return _CmdResult(False, [])
@@ -9,6 +9,7 @@ from klaude_code import const
9
9
  from klaude_code.protocol import events
10
10
  from klaude_code.ui.core.stage_manager import Stage, StageManager
11
11
  from klaude_code.ui.modes.repl.renderer import REPLRenderer
12
+ from klaude_code.ui.renderers.thinking import normalize_thinking_content
12
13
  from klaude_code.ui.rich.markdown import MarkdownStream, ThinkingMarkdown
13
14
  from klaude_code.ui.rich.theme import ThemeKey
14
15
  from klaude_code.ui.terminal.notifier import Notification, NotificationType, TerminalNotifier
@@ -121,7 +122,7 @@ class ActivityState:
121
122
  for name, count in self._tool_calls.items():
122
123
  if not first:
123
124
  activity_text.append(", ")
124
- activity_text.append(name)
125
+ activity_text.append(Text(name, style=ThemeKey.SPINNER_STATUS_TEXT_BOLD))
125
126
  if count > 1:
126
127
  activity_text.append(f" x {count}")
127
128
  first = False
@@ -137,11 +138,13 @@ class SpinnerStatusState:
137
138
  Composed of two independent layers:
138
139
  - base_status: Set by TodoChange, persistent within a turn
139
140
  - activity: Current activity (composing or tool_calls), mutually exclusive
141
+ - context_percent: Context usage percentage, updated during task execution
140
142
 
141
143
  Display logic:
142
144
  - If activity: show base + activity (if base exists) or activity + "..."
143
145
  - Elif base_status: show base_status
144
146
  - Else: show "Thinking …"
147
+ - Context percent is appended at the end if available
145
148
  """
146
149
 
147
150
  DEFAULT_STATUS = "Thinking …"
@@ -149,11 +152,13 @@ class SpinnerStatusState:
149
152
  def __init__(self) -> None:
150
153
  self._base_status: str | None = None
151
154
  self._activity = ActivityState()
155
+ self._context_percent: float | None = None
152
156
 
153
157
  def reset(self) -> None:
154
158
  """Reset all layers."""
155
159
  self._base_status = None
156
160
  self._activity.reset()
161
+ self._context_percent = None
157
162
 
158
163
  def set_base_status(self, status: str | None) -> None:
159
164
  """Set base status from TodoChange."""
@@ -175,12 +180,16 @@ class SpinnerStatusState:
175
180
  """Clear activity state for a new turn."""
176
181
  self._activity.reset()
177
182
 
183
+ def set_context_percent(self, percent: float) -> None:
184
+ """Set context usage percentage."""
185
+ self._context_percent = percent
186
+
178
187
  def get_activity_text(self) -> Text | None:
179
188
  """Get current activity text. Returns None if idle."""
180
189
  return self._activity.get_activity_text()
181
190
 
182
191
  def get_status(self) -> Text:
183
- """Get current spinner status as rich Text."""
192
+ """Get current spinner status as rich Text (without context)."""
184
193
  activity_text = self._activity.get_activity_text()
185
194
 
186
195
  if self._base_status:
@@ -188,11 +197,19 @@ class SpinnerStatusState:
188
197
  if activity_text:
189
198
  result.append(" | ")
190
199
  result.append_text(activity_text)
191
- return result
192
- if activity_text:
200
+ elif activity_text:
193
201
  activity_text.append(" …")
194
- return activity_text
195
- return Text(self.DEFAULT_STATUS)
202
+ result = activity_text
203
+ else:
204
+ result = Text(self.DEFAULT_STATUS)
205
+
206
+ return result
207
+
208
+ def get_context_text(self) -> Text | None:
209
+ """Get context usage text for right-aligned display."""
210
+ if self._context_percent is None:
211
+ return None
212
+ return Text(f"{self._context_percent:.1f}%", style=ThemeKey.METADATA_DIM)
196
213
 
197
214
 
198
215
  class DisplayEventHandler:
@@ -247,6 +264,8 @@ class DisplayEventHandler:
247
264
  self._on_task_metadata(e)
248
265
  case events.TodoChangeEvent() as e:
249
266
  self._on_todo_change(e)
267
+ case events.ContextUsageEvent() as e:
268
+ self._on_context_usage(e)
250
269
  case events.TurnEndEvent():
251
270
  pass
252
271
  case events.ResponseMetadataEvent():
@@ -330,7 +349,7 @@ class DisplayEventHandler:
330
349
  self.thinking_stream.append(event.content)
331
350
 
332
351
  if first_delta and self.thinking_stream.mdstream is not None:
333
- self.thinking_stream.mdstream.update(self.thinking_stream.buffer)
352
+ self.thinking_stream.mdstream.update(normalize_thinking_content(self.thinking_stream.buffer))
334
353
 
335
354
  await self.stage_manager.enter_thinking_stage()
336
355
  self.thinking_stream.debouncer.schedule()
@@ -397,10 +416,11 @@ class DisplayEventHandler:
397
416
  self.renderer.display_tool_call(event)
398
417
 
399
418
  async def _on_tool_result(self, event: events.ToolResultEvent) -> None:
400
- if self.renderer.is_sub_agent_session(event.session_id):
419
+ if self.renderer.is_sub_agent_session(event.session_id) and event.status == "success":
401
420
  return
402
421
  await self.stage_manager.transition_to(Stage.TOOL_RESULT)
403
- self.renderer.display_tool_call_result(event)
422
+ with self.renderer.session_print_context(event.session_id):
423
+ self.renderer.display_tool_call_result(event)
404
424
 
405
425
  def _on_task_metadata(self, event: events.TaskMetadataEvent) -> None:
406
426
  self.renderer.display_task_metadata(event)
@@ -412,6 +432,12 @@ class DisplayEventHandler:
412
432
  self.spinner_status.clear_for_new_turn()
413
433
  self._update_spinner()
414
434
 
435
+ def _on_context_usage(self, event: events.ContextUsageEvent) -> None:
436
+ if self.renderer.is_sub_agent_session(event.session_id):
437
+ return
438
+ self.spinner_status.set_context_percent(event.context_percent)
439
+ self._update_spinner()
440
+
415
441
  async def _on_task_finish(self, event: events.TaskFinishEvent) -> None:
416
442
  self.renderer.display_task_finish(event)
417
443
  if not self.renderer.is_sub_agent_session(event.session_id):
@@ -459,7 +485,10 @@ class DisplayEventHandler:
459
485
 
460
486
  def _update_spinner(self) -> None:
461
487
  """Update spinner text from current status state."""
462
- self.renderer.spinner_update(self.spinner_status.get_status())
488
+ self.renderer.spinner_update(
489
+ self.spinner_status.get_status(),
490
+ self.spinner_status.get_context_text(),
491
+ )
463
492
 
464
493
  async def _flush_assistant_buffer(self, state: StreamState) -> None:
465
494
  if state.is_active:
@@ -471,14 +500,14 @@ class DisplayEventHandler:
471
500
  if state.is_active:
472
501
  mdstream = state.mdstream
473
502
  assert mdstream is not None
474
- mdstream.update(state.buffer)
503
+ mdstream.update(normalize_thinking_content(state.buffer))
475
504
 
476
505
  async def _finish_thinking_stream(self) -> None:
477
506
  if self.thinking_stream.is_active:
478
507
  self.thinking_stream.debouncer.cancel()
479
508
  mdstream = self.thinking_stream.mdstream
480
509
  assert mdstream is not None
481
- mdstream.update(self.thinking_stream.buffer, final=True)
510
+ mdstream.update(normalize_thinking_content(self.thinking_stream.buffer), final=True)
482
511
  self.thinking_stream.finish()
483
512
  self.renderer.console.pop_theme()
484
513
  self.renderer.print()
@@ -525,15 +554,15 @@ class DisplayEventHandler:
525
554
  """Calculate max length for base_status based on terminal width.
526
555
 
527
556
  Reserve space for:
528
- - Spinner glyph + space: 2 chars
557
+ - Spinner glyph + space + context text: 2 chars + context text length 10 chars
529
558
  - " | " separator: 3 chars (only if activity text present)
530
559
  - Activity text: actual length (only if present)
531
560
  - Status hint text (esc to interrupt)
532
561
  """
533
562
  terminal_width = self.renderer.console.size.width
534
563
 
535
- # Base reserved space: spinner + status hint
536
- reserved_space = 2 + len(const.STATUS_HINT_TEXT)
564
+ # Base reserved space: spinner + context + status hint
565
+ reserved_space = 12 + len(const.STATUS_HINT_TEXT)
537
566
 
538
567
  # Add space for activity text if present
539
568
  activity_text = self.spinner_status.get_activity_text()
@@ -264,9 +264,9 @@ class REPLRenderer:
264
264
  """Stop the spinner animation."""
265
265
  self._spinner.stop()
266
266
 
267
- def spinner_update(self, status_text: str | Text) -> None:
268
- """Update the spinner status text."""
269
- self._spinner.update(ShimmerStatusText(status_text, ThemeKey.SPINNER_STATUS_TEXT))
267
+ def spinner_update(self, status_text: str | Text, right_text: Text | None = None) -> None:
268
+ """Update the spinner status text with optional right-aligned text."""
269
+ self._spinner.update(ShimmerStatusText(status_text, ThemeKey.SPINNER_STATUS_TEXT, right_text))
270
270
 
271
271
  def spinner_renderable(self) -> Spinner:
272
272
  """Return the spinner's renderable for embedding in other components."""
@@ -61,9 +61,7 @@ def _render_task_metadata_block(
61
61
  if metadata.usage is not None:
62
62
  # Tokens: ↑ 37k cache 5k ↓ 907 think 45k
63
63
  token_parts: list[Text] = [
64
- Text.assemble(
65
- ("↑ ", ThemeKey.METADATA_DIM), (format_number(metadata.usage.input_tokens), ThemeKey.METADATA)
66
- )
64
+ Text.assemble(("↑", ThemeKey.METADATA_DIM), (format_number(metadata.usage.input_tokens), ThemeKey.METADATA))
67
65
  ]
68
66
  if metadata.usage.cached_tokens > 0:
69
67
  token_parts.append(
@@ -74,7 +72,7 @@ def _render_task_metadata_block(
74
72
  )
75
73
  token_parts.append(
76
74
  Text.assemble(
77
- ("↓ ", ThemeKey.METADATA_DIM), (format_number(metadata.usage.output_tokens), ThemeKey.METADATA)
75
+ ("↓", ThemeKey.METADATA_DIM), (format_number(metadata.usage.output_tokens), ThemeKey.METADATA)
78
76
  )
79
77
  )
80
78
  if metadata.usage.reasoning_tokens > 0: