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.
- klaude_code/cli/config_cmd.py +1 -1
- klaude_code/cli/debug.py +1 -1
- klaude_code/cli/main.py +3 -9
- klaude_code/cli/runtime.py +20 -13
- klaude_code/command/__init__.py +7 -1
- klaude_code/command/clear_cmd.py +2 -7
- klaude_code/command/command_abc.py +33 -5
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/diff_cmd.py +2 -6
- klaude_code/command/export_cmd.py +7 -7
- klaude_code/command/export_online_cmd.py +145 -0
- klaude_code/command/help_cmd.py +4 -9
- klaude_code/command/model_cmd.py +10 -6
- klaude_code/command/prompt_command.py +2 -6
- klaude_code/command/refresh_cmd.py +2 -7
- klaude_code/command/registry.py +2 -4
- klaude_code/command/release_notes_cmd.py +2 -6
- klaude_code/command/status_cmd.py +2 -7
- klaude_code/command/terminal_setup_cmd.py +2 -6
- klaude_code/command/thinking_cmd.py +13 -8
- klaude_code/config/config.py +16 -17
- klaude_code/config/select_model.py +81 -5
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/executor.py +236 -109
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/sub_agent_manager.py +1 -1
- klaude_code/core/prompts/prompt-claude-code.md +1 -1
- klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -1
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +9 -35
- klaude_code/core/task.py +8 -0
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/file/read_tool.py +38 -10
- klaude_code/core/tool/report_back_tool.py +28 -2
- klaude_code/core/tool/shell/bash_tool.py +22 -2
- klaude_code/core/tool/tool_runner.py +26 -23
- klaude_code/core/tool/truncation.py +23 -9
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +36 -1
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +126 -0
- klaude_code/core/turn.py +28 -0
- klaude_code/protocol/commands.py +2 -0
- klaude_code/protocol/events.py +8 -0
- klaude_code/protocol/sub_agent/__init__.py +1 -1
- klaude_code/protocol/sub_agent/explore.py +1 -1
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +1 -0
- klaude_code/session/session.py +2 -2
- klaude_code/session/templates/export_session.html +123 -37
- klaude_code/trace/__init__.py +20 -2
- klaude_code/ui/modes/repl/completers.py +19 -2
- klaude_code/ui/modes/repl/event_handler.py +44 -15
- klaude_code/ui/modes/repl/renderer.py +3 -3
- klaude_code/ui/renderers/metadata.py +2 -4
- klaude_code/ui/renderers/sub_agent.py +14 -10
- klaude_code/ui/renderers/thinking.py +24 -8
- klaude_code/ui/renderers/tools.py +83 -20
- klaude_code/ui/rich/code_panel.py +112 -0
- klaude_code/ui/rich/markdown.py +3 -4
- klaude_code/ui/rich/status.py +30 -6
- klaude_code/ui/rich/theme.py +10 -1
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/METADATA +126 -25
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/RECORD +67 -63
- klaude_code/core/manager/agent_manager.py +0 -132
- klaude_code/core/prompts/prompt-sub-agent-webfetch.md +0 -46
- klaude_code/protocol/sub_agent/web_fetch.py +0 -74
- /klaude_code/{config → cli}/list_model.py +0 -0
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.16.dist-info → klaude_code-1.2.18.dist-info}/entry_points.txt +0 -0
klaude_code/protocol/events.py
CHANGED
|
@@ -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
|
|
117
|
+
from klaude_code.protocol.sub_agent import web as web # noqa: E402
|
|
@@ -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
|
+
)
|
klaude_code/protocol/tools.py
CHANGED
klaude_code/session/session.py
CHANGED
|
@@ -36,7 +36,7 @@ class Session(BaseModel):
|
|
|
36
36
|
|
|
37
37
|
@property
|
|
38
38
|
def messages_count(self) -> int:
|
|
39
|
-
"""Count of user
|
|
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/
|
|
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/
|
|
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/
|
|
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: #
|
|
26
|
-
--bg-container: #
|
|
27
|
-
--bg-card: #
|
|
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: "
|
|
41
|
-
--font-weight-bold:
|
|
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.
|
|
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:
|
|
790
|
+
padding: 12px;
|
|
686
791
|
border-radius: var(--radius-md);
|
|
687
792
|
overflow-x: auto;
|
|
688
|
-
margin:
|
|
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 {
|
klaude_code/trace/__init__.py
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
from .log import
|
|
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__ = [
|
|
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 = " [
|
|
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
|
-
|
|
192
|
-
if activity_text:
|
|
200
|
+
elif activity_text:
|
|
193
201
|
activity_text.append(" …")
|
|
194
|
-
|
|
195
|
-
|
|
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.
|
|
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(
|
|
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 =
|
|
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
|
-
("↓
|
|
75
|
+
("↓", ThemeKey.METADATA_DIM), (format_number(metadata.usage.output_tokens), ThemeKey.METADATA)
|
|
78
76
|
)
|
|
79
77
|
)
|
|
80
78
|
if metadata.usage.reasoning_tokens > 0:
|