klaude-code 1.2.9__py3-none-any.whl → 1.2.11__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/main.py +11 -5
- klaude_code/cli/runtime.py +21 -21
- klaude_code/command/__init__.py +68 -23
- 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_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 +5 -2
- klaude_code/command/status_cmd.py +8 -4
- klaude_code/command/terminal_setup_cmd.py +7 -4
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/agent.py +62 -9
- klaude_code/core/executor.py +1 -4
- klaude_code/core/manager/agent_manager.py +19 -14
- klaude_code/core/manager/llm_clients.py +47 -22
- klaude_code/core/manager/llm_clients_builder.py +22 -13
- klaude_code/core/manager/sub_agent_manager.py +1 -1
- klaude_code/core/prompt.py +4 -4
- klaude_code/core/prompts/prompt-claude-code.md +1 -12
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/reminders.py +0 -3
- klaude_code/core/task.py +6 -2
- klaude_code/core/tool/file/_utils.py +30 -0
- klaude_code/core/tool/file/edit_tool.py +5 -30
- klaude_code/core/tool/file/multi_edit_tool.py +6 -31
- klaude_code/core/tool/file/read_tool.py +6 -18
- klaude_code/core/tool/file/write_tool.py +5 -30
- klaude_code/core/tool/memory/__init__.py +5 -0
- klaude_code/core/tool/memory/memory_tool.md +4 -0
- klaude_code/core/tool/memory/skill_loader.py +3 -2
- klaude_code/core/tool/memory/skill_tool.py +13 -0
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/tool_registry.py +3 -4
- klaude_code/llm/__init__.py +2 -12
- klaude_code/llm/anthropic/client.py +2 -1
- klaude_code/llm/client.py +2 -2
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +3 -2
- klaude_code/llm/openrouter/client.py +3 -3
- klaude_code/llm/registry.py +33 -7
- klaude_code/llm/responses/client.py +2 -1
- klaude_code/llm/responses/input.py +1 -1
- klaude_code/llm/usage.py +17 -8
- klaude_code/protocol/model.py +15 -7
- klaude_code/protocol/op.py +5 -1
- klaude_code/protocol/sub_agent.py +1 -0
- klaude_code/session/export.py +16 -6
- klaude_code/session/session.py +10 -4
- klaude_code/session/templates/export_session.html +155 -0
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/event_handler.py +1 -5
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +3 -34
- klaude_code/ui/renderers/metadata.py +22 -1
- klaude_code/ui/renderers/tools.py +13 -2
- klaude_code/ui/rich/markdown.py +4 -1
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/control.py +2 -2
- klaude_code/version.py +3 -3
- {klaude_code-1.2.9.dist-info → klaude_code-1.2.11.dist-info}/METADATA +1 -4
- {klaude_code-1.2.9.dist-info → klaude_code-1.2.11.dist-info}/RECORD +69 -66
- {klaude_code-1.2.9.dist-info → klaude_code-1.2.11.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.9.dist-info → klaude_code-1.2.11.dist-info}/entry_points.txt +0 -0
|
@@ -46,7 +46,7 @@ class AnthropicClient(LLMClientABC):
|
|
|
46
46
|
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
|
|
47
47
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
48
48
|
|
|
49
|
-
metadata_tracker = MetadataTracker(cost_config=self.
|
|
49
|
+
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
50
50
|
|
|
51
51
|
messages = convert_history_to_input(param.input, param.model)
|
|
52
52
|
tools = convert_tool_schema(param.tools)
|
|
@@ -179,6 +179,7 @@ class AnthropicClient(LLMClientABC):
|
|
|
179
179
|
output_tokens=output_tokens,
|
|
180
180
|
cached_tokens=cached_tokens,
|
|
181
181
|
context_limit=param.context_limit,
|
|
182
|
+
max_tokens=param.max_tokens,
|
|
182
183
|
)
|
|
183
184
|
metadata_tracker.set_usage(usage)
|
|
184
185
|
metadata_tracker.set_model_name(str(param.model))
|
klaude_code/llm/client.py
CHANGED
|
@@ -19,7 +19,7 @@ class LLMClientABC(ABC):
|
|
|
19
19
|
@abstractmethod
|
|
20
20
|
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
|
|
21
21
|
raise NotImplementedError
|
|
22
|
-
yield cast(model.ConversationItem, None)
|
|
22
|
+
yield cast(model.ConversationItem, None)
|
|
23
23
|
|
|
24
24
|
def get_llm_config(self) -> llm_param.LLMConfigParameter:
|
|
25
25
|
return self._config
|
|
@@ -42,7 +42,7 @@ def call_with_logged_payload(func: Callable[P, R], *args: P.args, **kwargs: P.kw
|
|
|
42
42
|
|
|
43
43
|
payload = {k: v for k, v in kwargs.items() if v is not None}
|
|
44
44
|
log_debug(
|
|
45
|
-
json.dumps(payload, ensure_ascii=False, default=str
|
|
45
|
+
json.dumps(payload, ensure_ascii=False, default=str),
|
|
46
46
|
style="yellow",
|
|
47
47
|
debug_type=DebugType.LLM_PAYLOAD,
|
|
48
48
|
)
|
klaude_code/llm/codex/client.py
CHANGED
|
@@ -84,7 +84,7 @@ class CodexClient(LLMClientABC):
|
|
|
84
84
|
# Codex API requires store=False
|
|
85
85
|
param.store = False
|
|
86
86
|
|
|
87
|
-
metadata_tracker = MetadataTracker(cost_config=self.
|
|
87
|
+
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
88
88
|
|
|
89
89
|
inputs = convert_history_to_input(param.input, param.model)
|
|
90
90
|
tools = convert_tool_schema(param.tools)
|
|
@@ -47,7 +47,7 @@ class OpenAICompatibleClient(LLMClientABC):
|
|
|
47
47
|
messages = convert_history_to_input(param.input, param.system, param.model)
|
|
48
48
|
tools = convert_tool_schema(param.tools)
|
|
49
49
|
|
|
50
|
-
metadata_tracker = MetadataTracker(cost_config=self.
|
|
50
|
+
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
51
51
|
|
|
52
52
|
extra_body = {}
|
|
53
53
|
extra_headers = {"extra": json.dumps({"session_id": param.session_id}, sort_keys=True)}
|
|
@@ -88,7 +88,7 @@ class OpenAICompatibleClient(LLMClientABC):
|
|
|
88
88
|
if (
|
|
89
89
|
event.usage is not None and event.usage.completion_tokens is not None # pyright: ignore[reportUnnecessaryComparison] gcp gemini will return None usage field
|
|
90
90
|
):
|
|
91
|
-
metadata_tracker.set_usage(convert_usage(event.usage, param.context_limit))
|
|
91
|
+
metadata_tracker.set_usage(convert_usage(event.usage, param.context_limit, param.max_tokens))
|
|
92
92
|
if event.model:
|
|
93
93
|
metadata_tracker.set_model_name(event.model)
|
|
94
94
|
if provider := getattr(event, "provider", None):
|
|
@@ -104,6 +104,7 @@ class OpenAICompatibleClient(LLMClientABC):
|
|
|
104
104
|
convert_usage(
|
|
105
105
|
openai.types.CompletionUsage.model_validate(getattr(event.choices[0], "usage")),
|
|
106
106
|
param.context_limit,
|
|
107
|
+
param.max_tokens,
|
|
107
108
|
)
|
|
108
109
|
)
|
|
109
110
|
|
|
@@ -38,7 +38,7 @@ class OpenRouterClient(LLMClientABC):
|
|
|
38
38
|
messages = convert_history_to_input(param.input, param.system, param.model)
|
|
39
39
|
tools = convert_tool_schema(param.tools)
|
|
40
40
|
|
|
41
|
-
metadata_tracker = MetadataTracker(cost_config=self.
|
|
41
|
+
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
42
42
|
|
|
43
43
|
extra_body: dict[str, object] = {
|
|
44
44
|
"usage": {"include": True} # To get the cache tokens at the end of the response
|
|
@@ -73,7 +73,7 @@ class OpenRouterClient(LLMClientABC):
|
|
|
73
73
|
max_tokens=param.max_tokens,
|
|
74
74
|
tools=tools,
|
|
75
75
|
verbosity=param.verbosity,
|
|
76
|
-
extra_body=extra_body,
|
|
76
|
+
extra_body=extra_body,
|
|
77
77
|
extra_headers=extra_headers, # pyright: ignore[reportUnknownArgumentType]
|
|
78
78
|
)
|
|
79
79
|
|
|
@@ -100,7 +100,7 @@ class OpenRouterClient(LLMClientABC):
|
|
|
100
100
|
if (
|
|
101
101
|
event.usage is not None and event.usage.completion_tokens is not None # pyright: ignore[reportUnnecessaryComparison]
|
|
102
102
|
): # gcp gemini will return None usage field
|
|
103
|
-
metadata_tracker.set_usage(convert_usage(event.usage, param.context_limit))
|
|
103
|
+
metadata_tracker.set_usage(convert_usage(event.usage, param.context_limit, param.max_tokens))
|
|
104
104
|
if event.model:
|
|
105
105
|
metadata_tracker.set_model_name(event.model)
|
|
106
106
|
if provider := getattr(event, "provider", None):
|
klaude_code/llm/registry.py
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
|
-
from typing import Callable, TypeVar
|
|
1
|
+
from typing import TYPE_CHECKING, Callable, TypeVar
|
|
2
2
|
|
|
3
|
-
from klaude_code.llm.client import LLMClientABC
|
|
4
3
|
from klaude_code.protocol import llm_param
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from klaude_code.llm.client import LLMClientABC
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
_T = TypeVar("_T", bound=type["LLMClientABC"])
|
|
9
9
|
|
|
10
|
+
# Track which protocols have been loaded
|
|
11
|
+
_loaded_protocols: set[llm_param.LLMClientProtocol] = set()
|
|
12
|
+
_REGISTRY: dict[llm_param.LLMClientProtocol, type["LLMClientABC"]] = {}
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
|
|
15
|
+
def _load_protocol(protocol: llm_param.LLMClientProtocol) -> None:
|
|
16
|
+
"""Load the module for a specific protocol on demand."""
|
|
17
|
+
if protocol in _loaded_protocols:
|
|
18
|
+
return
|
|
19
|
+
_loaded_protocols.add(protocol)
|
|
20
|
+
|
|
21
|
+
# Import only the needed module to trigger @register decorator
|
|
22
|
+
if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
|
|
23
|
+
from . import anthropic as _ # noqa: F401
|
|
24
|
+
elif protocol == llm_param.LLMClientProtocol.CODEX:
|
|
25
|
+
from . import codex as _ # noqa: F401
|
|
26
|
+
elif protocol == llm_param.LLMClientProtocol.OPENAI:
|
|
27
|
+
from . import openai_compatible as _ # noqa: F401
|
|
28
|
+
elif protocol == llm_param.LLMClientProtocol.OPENROUTER:
|
|
29
|
+
from . import openrouter as _ # noqa: F401
|
|
30
|
+
elif protocol == llm_param.LLMClientProtocol.RESPONSES:
|
|
31
|
+
from . import responses as _ # noqa: F401
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def register(name: llm_param.LLMClientProtocol) -> Callable[[_T], _T]:
|
|
35
|
+
"""Decorator to register an LLM client class for a protocol."""
|
|
36
|
+
|
|
37
|
+
def _decorator(cls: _T) -> _T:
|
|
13
38
|
_REGISTRY[name] = cls
|
|
14
39
|
return cls
|
|
15
40
|
|
|
16
41
|
return _decorator
|
|
17
42
|
|
|
18
43
|
|
|
19
|
-
def create_llm_client(config: llm_param.LLMConfigParameter) -> LLMClientABC:
|
|
44
|
+
def create_llm_client(config: llm_param.LLMConfigParameter) -> "LLMClientABC":
|
|
45
|
+
_load_protocol(config.protocol)
|
|
20
46
|
if config.protocol not in _REGISTRY:
|
|
21
47
|
raise ValueError(f"Unknown LLMClient protocol: {config.protocol}")
|
|
22
48
|
return _REGISTRY[config.protocol].create(config)
|
|
@@ -102,6 +102,7 @@ async def parse_responses_stream(
|
|
|
102
102
|
reasoning_tokens=event.response.usage.output_tokens_details.reasoning_tokens,
|
|
103
103
|
total_tokens=event.response.usage.total_tokens,
|
|
104
104
|
context_limit=param.context_limit,
|
|
105
|
+
max_tokens=param.max_tokens,
|
|
105
106
|
)
|
|
106
107
|
metadata_tracker.set_usage(usage)
|
|
107
108
|
metadata_tracker.set_model_name(str(param.model))
|
|
@@ -159,7 +160,7 @@ class ResponsesClient(LLMClientABC):
|
|
|
159
160
|
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem, None]:
|
|
160
161
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
161
162
|
|
|
162
|
-
metadata_tracker = MetadataTracker(cost_config=self.
|
|
163
|
+
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
163
164
|
|
|
164
165
|
inputs = convert_history_to_input(param.input, param.model)
|
|
165
166
|
tools = convert_tool_schema(param.tools)
|
klaude_code/llm/usage.py
CHANGED
|
@@ -92,10 +92,14 @@ class MetadataTracker:
|
|
|
92
92
|
return self._metadata_item
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
def convert_usage(
|
|
95
|
+
def convert_usage(
|
|
96
|
+
usage: openai.types.CompletionUsage,
|
|
97
|
+
context_limit: int | None = None,
|
|
98
|
+
max_tokens: int | None = None,
|
|
99
|
+
) -> model.Usage:
|
|
96
100
|
"""Convert OpenAI CompletionUsage to internal Usage model.
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
context_token is set to total_tokens from the API response,
|
|
99
103
|
representing the actual context window usage for this turn.
|
|
100
104
|
"""
|
|
101
105
|
return model.Usage(
|
|
@@ -104,8 +108,9 @@ def convert_usage(usage: openai.types.CompletionUsage, context_limit: int | None
|
|
|
104
108
|
reasoning_tokens=(usage.completion_tokens_details.reasoning_tokens if usage.completion_tokens_details else 0)
|
|
105
109
|
or 0,
|
|
106
110
|
output_tokens=usage.completion_tokens,
|
|
107
|
-
|
|
111
|
+
context_token=usage.total_tokens,
|
|
108
112
|
context_limit=context_limit,
|
|
113
|
+
max_tokens=max_tokens,
|
|
109
114
|
)
|
|
110
115
|
|
|
111
116
|
|
|
@@ -114,19 +119,21 @@ def convert_anthropic_usage(
|
|
|
114
119
|
output_tokens: int,
|
|
115
120
|
cached_tokens: int,
|
|
116
121
|
context_limit: int | None = None,
|
|
122
|
+
max_tokens: int | None = None,
|
|
117
123
|
) -> model.Usage:
|
|
118
124
|
"""Convert Anthropic usage data to internal Usage model.
|
|
119
125
|
|
|
120
|
-
|
|
126
|
+
context_token is computed from input + cached + output tokens,
|
|
121
127
|
representing the actual context window usage for this turn.
|
|
122
128
|
"""
|
|
123
|
-
|
|
129
|
+
context_token = input_tokens + cached_tokens + output_tokens
|
|
124
130
|
return model.Usage(
|
|
125
131
|
input_tokens=input_tokens,
|
|
126
132
|
output_tokens=output_tokens,
|
|
127
133
|
cached_tokens=cached_tokens,
|
|
128
|
-
|
|
134
|
+
context_token=context_token,
|
|
129
135
|
context_limit=context_limit,
|
|
136
|
+
max_tokens=max_tokens,
|
|
130
137
|
)
|
|
131
138
|
|
|
132
139
|
|
|
@@ -137,10 +144,11 @@ def convert_responses_usage(
|
|
|
137
144
|
reasoning_tokens: int,
|
|
138
145
|
total_tokens: int,
|
|
139
146
|
context_limit: int | None = None,
|
|
147
|
+
max_tokens: int | None = None,
|
|
140
148
|
) -> model.Usage:
|
|
141
149
|
"""Convert OpenAI Responses API usage data to internal Usage model.
|
|
142
150
|
|
|
143
|
-
|
|
151
|
+
context_token is set to total_tokens from the API response,
|
|
144
152
|
representing the actual context window usage for this turn.
|
|
145
153
|
"""
|
|
146
154
|
return model.Usage(
|
|
@@ -148,6 +156,7 @@ def convert_responses_usage(
|
|
|
148
156
|
output_tokens=output_tokens,
|
|
149
157
|
cached_tokens=cached_tokens,
|
|
150
158
|
reasoning_tokens=reasoning_tokens,
|
|
151
|
-
|
|
159
|
+
context_token=total_tokens,
|
|
152
160
|
context_limit=context_limit,
|
|
161
|
+
max_tokens=max_tokens,
|
|
153
162
|
)
|
klaude_code/protocol/model.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import Annotated, Literal
|
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
6
6
|
|
|
7
|
+
from klaude_code import const
|
|
7
8
|
from klaude_code.protocol.commands import CommandName
|
|
8
9
|
from klaude_code.protocol.tools import SubAgentType
|
|
9
10
|
|
|
@@ -19,8 +20,11 @@ class Usage(BaseModel):
|
|
|
19
20
|
output_tokens: int = 0
|
|
20
21
|
|
|
21
22
|
# Context window tracking
|
|
22
|
-
|
|
23
|
+
context_token: int | None = None # Peak total_tokens seen (for context usage display)
|
|
24
|
+
context_delta: int | None = None # Context growth since last task (for cache ratio calculation)
|
|
25
|
+
last_turn_output_token: int | None = None # Context growth since last task (for cache ratio calculation)
|
|
23
26
|
context_limit: int | None = None # Model's context limit
|
|
27
|
+
max_tokens: int | None = None # Max output tokens for this request
|
|
24
28
|
|
|
25
29
|
throughput_tps: float | None = None
|
|
26
30
|
first_token_latency_ms: float | None = None
|
|
@@ -31,13 +35,13 @@ class Usage(BaseModel):
|
|
|
31
35
|
cache_read_cost: float | None = None # Cost for cached tokens
|
|
32
36
|
currency: str = "USD" # Currency for cost display (USD or CNY)
|
|
33
37
|
|
|
34
|
-
@computed_field
|
|
38
|
+
@computed_field
|
|
35
39
|
@property
|
|
36
40
|
def total_tokens(self) -> int:
|
|
37
41
|
"""Total tokens computed from input + output tokens."""
|
|
38
42
|
return self.input_tokens + self.output_tokens
|
|
39
43
|
|
|
40
|
-
@computed_field
|
|
44
|
+
@computed_field
|
|
41
45
|
@property
|
|
42
46
|
def total_cost(self) -> float | None:
|
|
43
47
|
"""Total cost computed from input + output + cache_read costs."""
|
|
@@ -45,15 +49,18 @@ class Usage(BaseModel):
|
|
|
45
49
|
non_none = [c for c in costs if c is not None]
|
|
46
50
|
return sum(non_none) if non_none else None
|
|
47
51
|
|
|
48
|
-
@computed_field
|
|
52
|
+
@computed_field
|
|
49
53
|
@property
|
|
50
54
|
def context_usage_percent(self) -> float | None:
|
|
51
|
-
"""Context usage percentage computed from
|
|
55
|
+
"""Context usage percentage computed from context_token / (context_limit - max_tokens)."""
|
|
52
56
|
if self.context_limit is None or self.context_limit <= 0:
|
|
53
57
|
return None
|
|
54
|
-
if self.
|
|
58
|
+
if self.context_token is None:
|
|
55
59
|
return None
|
|
56
|
-
|
|
60
|
+
effective_limit = self.context_limit - (self.max_tokens or const.DEFAULT_MAX_TOKENS)
|
|
61
|
+
if effective_limit <= 0:
|
|
62
|
+
return None
|
|
63
|
+
return (self.context_token / effective_limit) * 100
|
|
57
64
|
|
|
58
65
|
|
|
59
66
|
class TodoItem(BaseModel):
|
|
@@ -314,6 +321,7 @@ class TaskMetadata(BaseModel):
|
|
|
314
321
|
model_name: str = ""
|
|
315
322
|
provider: str | None = None
|
|
316
323
|
task_duration_s: float | None = None
|
|
324
|
+
turn_count: int = 0
|
|
317
325
|
|
|
318
326
|
@staticmethod
|
|
319
327
|
def aggregate_by_model(metadata_list: list["TaskMetadata"]) -> list["TaskMetadata"]:
|
klaude_code/protocol/op.py
CHANGED
|
@@ -63,7 +63,11 @@ class InterruptOperation(Operation):
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
class InitAgentOperation(Operation):
|
|
66
|
-
"""Operation for initializing an agent and replaying history if any.
|
|
66
|
+
"""Operation for initializing an agent and replaying history if any.
|
|
67
|
+
|
|
68
|
+
If session_id is None, a new session is created with an auto-generated ID.
|
|
69
|
+
If session_id is provided, attempts to load existing session or creates new one.
|
|
70
|
+
"""
|
|
67
71
|
|
|
68
72
|
type: OperationType = OperationType.INIT_AGENT
|
|
69
73
|
session_id: str | None = None
|
|
@@ -290,6 +290,7 @@ register_sub_agent(
|
|
|
290
290
|
tool_set=(tools.BASH, tools.READ),
|
|
291
291
|
prompt_builder=_explore_prompt_builder,
|
|
292
292
|
active_form="Exploring",
|
|
293
|
+
target_model_filter=lambda model: ("haiku" not in model) and ("kimi" not in model) and ("grok" not in model),
|
|
293
294
|
)
|
|
294
295
|
)
|
|
295
296
|
|
klaude_code/session/export.py
CHANGED
|
@@ -194,11 +194,18 @@ def _render_single_metadata(
|
|
|
194
194
|
input_stat += f"({_format_cost(u.input_cost, u.currency)})"
|
|
195
195
|
parts.append(f'<span class="metadata-stat">{input_stat}</span>')
|
|
196
196
|
|
|
197
|
-
# Cached with cost
|
|
197
|
+
# Cached with cost and cache ratio
|
|
198
198
|
if u.cached_tokens > 0:
|
|
199
199
|
cached_stat = f"cached: {_format_token_count(u.cached_tokens)}"
|
|
200
200
|
if u.cache_read_cost is not None:
|
|
201
201
|
cached_stat += f"({_format_cost(u.cache_read_cost, u.currency)})"
|
|
202
|
+
# Cache ratio: (cached + context_delta - last_turn_output) / input tokens
|
|
203
|
+
# Shows how much of the input was cached (not new context growth)
|
|
204
|
+
if u.input_tokens > 0:
|
|
205
|
+
context_delta = u.context_delta or 0
|
|
206
|
+
last_turn_output_token = u.last_turn_output_token or 0
|
|
207
|
+
cache_ratio = (u.cached_tokens + context_delta - last_turn_output_token) / u.input_tokens * 100
|
|
208
|
+
cached_stat += f"[{cache_ratio:.0f}%]"
|
|
202
209
|
parts.append(f'<span class="metadata-stat">{cached_stat}</span>')
|
|
203
210
|
|
|
204
211
|
# Output with cost
|
|
@@ -294,7 +301,7 @@ def _try_render_todo_args(arguments: str) -> str | None:
|
|
|
294
301
|
return None
|
|
295
302
|
|
|
296
303
|
return f'<div class="todo-list">{"".join(items_html)}</div>'
|
|
297
|
-
except
|
|
304
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
298
305
|
return None
|
|
299
306
|
|
|
300
307
|
|
|
@@ -380,7 +387,7 @@ def _get_mermaid_link_html(
|
|
|
380
387
|
try:
|
|
381
388
|
args = json.loads(tool_call.arguments)
|
|
382
389
|
code = args.get("code", "")
|
|
383
|
-
except
|
|
390
|
+
except (json.JSONDecodeError, TypeError):
|
|
384
391
|
code = ""
|
|
385
392
|
else:
|
|
386
393
|
code = ""
|
|
@@ -403,6 +410,9 @@ def _get_mermaid_link_html(
|
|
|
403
410
|
buttons_html.append(
|
|
404
411
|
f'<button type="button" class="copy-mermaid-btn" data-code="{escaped_code}" title="Copy Mermaid Code">Copy Code</button>'
|
|
405
412
|
)
|
|
413
|
+
buttons_html.append(
|
|
414
|
+
'<button type="button" class="fullscreen-mermaid-btn" title="View Fullscreen">Fullscreen</button>'
|
|
415
|
+
)
|
|
406
416
|
|
|
407
417
|
link = ui_extra.link if isinstance(ui_extra, model.MermaidLinkUIExtra) else None
|
|
408
418
|
|
|
@@ -447,7 +457,7 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
447
457
|
try:
|
|
448
458
|
parsed = json.loads(tool_call.arguments)
|
|
449
459
|
args_text = json.dumps(parsed, ensure_ascii=False, indent=2)
|
|
450
|
-
except
|
|
460
|
+
except (json.JSONDecodeError, TypeError):
|
|
451
461
|
args_text = tool_call.arguments
|
|
452
462
|
|
|
453
463
|
args_html = _escape_html(args_text or "")
|
|
@@ -469,7 +479,7 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
469
479
|
parsed_args = json.loads(tool_call.arguments)
|
|
470
480
|
if parsed_args.get("command") in {"create", "str_replace", "insert"}:
|
|
471
481
|
force_collapse = True
|
|
472
|
-
except
|
|
482
|
+
except (json.JSONDecodeError, TypeError):
|
|
473
483
|
pass
|
|
474
484
|
|
|
475
485
|
should_collapse = force_collapse or _should_collapse(args_html)
|
|
@@ -506,7 +516,7 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
|
|
|
506
516
|
new_string = args_data.get("new_string", "")
|
|
507
517
|
if old_string == "" and new_string:
|
|
508
518
|
diff_text = "\n".join(f"+{line}" for line in new_string.splitlines())
|
|
509
|
-
except
|
|
519
|
+
except (json.JSONDecodeError, TypeError):
|
|
510
520
|
pass
|
|
511
521
|
|
|
512
522
|
items_to_render: list[str] = []
|
klaude_code/session/session.py
CHANGED
|
@@ -102,8 +102,14 @@ class Session(BaseModel):
|
|
|
102
102
|
prefix = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(self.created_at))
|
|
103
103
|
return self._messages_dir() / f"{prefix}-{self.id}.jsonl"
|
|
104
104
|
|
|
105
|
+
@classmethod
|
|
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
|
+
|
|
105
110
|
@classmethod
|
|
106
111
|
def load(cls, id: str) -> "Session":
|
|
112
|
+
"""Load an existing session or create a new one if not found."""
|
|
107
113
|
# Load session metadata
|
|
108
114
|
sessions_dir = cls._sessions_dir()
|
|
109
115
|
session_candidates = sorted(
|
|
@@ -167,7 +173,7 @@ class Session(BaseModel):
|
|
|
167
173
|
item = cls_type(**data)
|
|
168
174
|
# pyright: ignore[reportAssignmentType]
|
|
169
175
|
history.append(item) # type: ignore[arg-type]
|
|
170
|
-
except
|
|
176
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
171
177
|
# Best-effort load; skip malformed lines
|
|
172
178
|
continue
|
|
173
179
|
sess.conversation_history = history
|
|
@@ -242,7 +248,7 @@ class Session(BaseModel):
|
|
|
242
248
|
if ts > latest_ts:
|
|
243
249
|
latest_ts = ts
|
|
244
250
|
latest_id = sid
|
|
245
|
-
except
|
|
251
|
+
except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
246
252
|
continue
|
|
247
253
|
return latest_id
|
|
248
254
|
|
|
@@ -395,7 +401,7 @@ class Session(BaseModel):
|
|
|
395
401
|
text_parts.append(text)
|
|
396
402
|
return " ".join(text_parts) if text_parts else None
|
|
397
403
|
return None
|
|
398
|
-
except
|
|
404
|
+
except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
399
405
|
return None
|
|
400
406
|
return None
|
|
401
407
|
|
|
@@ -403,7 +409,7 @@ class Session(BaseModel):
|
|
|
403
409
|
for p in sessions_dir.glob("*.json"):
|
|
404
410
|
try:
|
|
405
411
|
data = json.loads(p.read_text())
|
|
406
|
-
except
|
|
412
|
+
except (json.JSONDecodeError, OSError):
|
|
407
413
|
# Skip unreadable files
|
|
408
414
|
continue
|
|
409
415
|
# Filter out sub-agent sessions
|
|
@@ -338,6 +338,57 @@
|
|
|
338
338
|
border-color: var(--accent);
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
.mermaid-modal {
|
|
342
|
+
position: fixed;
|
|
343
|
+
top: 0;
|
|
344
|
+
left: 0;
|
|
345
|
+
width: 100vw;
|
|
346
|
+
height: 100vh;
|
|
347
|
+
background: rgba(255, 255, 255, 0.98);
|
|
348
|
+
z-index: 1000;
|
|
349
|
+
display: flex;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: center;
|
|
353
|
+
opacity: 0;
|
|
354
|
+
pointer-events: none;
|
|
355
|
+
transition: opacity 0.2s;
|
|
356
|
+
}
|
|
357
|
+
.mermaid-modal.active {
|
|
358
|
+
opacity: 1;
|
|
359
|
+
pointer-events: auto;
|
|
360
|
+
}
|
|
361
|
+
.mermaid-modal-content {
|
|
362
|
+
width: 95%;
|
|
363
|
+
height: 90%;
|
|
364
|
+
display: flex;
|
|
365
|
+
align-items: center;
|
|
366
|
+
justify-content: center;
|
|
367
|
+
overflow: auto;
|
|
368
|
+
}
|
|
369
|
+
.mermaid-modal-content svg {
|
|
370
|
+
width: auto !important;
|
|
371
|
+
height: auto !important;
|
|
372
|
+
max-width: 100%;
|
|
373
|
+
max-height: 100%;
|
|
374
|
+
}
|
|
375
|
+
.mermaid-modal-close {
|
|
376
|
+
position: absolute;
|
|
377
|
+
top: 20px;
|
|
378
|
+
right: 20px;
|
|
379
|
+
background: transparent;
|
|
380
|
+
border: none;
|
|
381
|
+
font-size: 32px;
|
|
382
|
+
cursor: pointer;
|
|
383
|
+
color: var(--text-dim);
|
|
384
|
+
z-index: 1001;
|
|
385
|
+
line-height: 1;
|
|
386
|
+
padding: 8px;
|
|
387
|
+
}
|
|
388
|
+
.mermaid-modal-close:hover {
|
|
389
|
+
color: var(--text);
|
|
390
|
+
}
|
|
391
|
+
|
|
341
392
|
.copy-mermaid-btn {
|
|
342
393
|
border: 1px solid var(--border);
|
|
343
394
|
background: transparent;
|
|
@@ -356,6 +407,25 @@
|
|
|
356
407
|
border-color: var(--accent);
|
|
357
408
|
}
|
|
358
409
|
|
|
410
|
+
.fullscreen-mermaid-btn {
|
|
411
|
+
margin-left: 8px;
|
|
412
|
+
border: 1px solid var(--border);
|
|
413
|
+
background: transparent;
|
|
414
|
+
color: var(--text-dim);
|
|
415
|
+
font-family: var(--font-mono);
|
|
416
|
+
font-size: var(--font-size-xs);
|
|
417
|
+
text-transform: uppercase;
|
|
418
|
+
padding: 2px 10px;
|
|
419
|
+
border-radius: 999px;
|
|
420
|
+
cursor: pointer;
|
|
421
|
+
transition: color 0.2s, border-color 0.2s, background 0.2s;
|
|
422
|
+
font-weight: var(--font-weight-bold);
|
|
423
|
+
}
|
|
424
|
+
.fullscreen-mermaid-btn:hover {
|
|
425
|
+
color: var(--text);
|
|
426
|
+
border-color: var(--accent);
|
|
427
|
+
}
|
|
428
|
+
|
|
359
429
|
.assistant-rendered {
|
|
360
430
|
width: 100%;
|
|
361
431
|
}
|
|
@@ -1065,6 +1135,13 @@
|
|
|
1065
1135
|
</svg>
|
|
1066
1136
|
</div>
|
|
1067
1137
|
|
|
1138
|
+
<div id="mermaid-modal" class="mermaid-modal">
|
|
1139
|
+
<button class="mermaid-modal-close" id="mermaid-modal-close">
|
|
1140
|
+
×
|
|
1141
|
+
</button>
|
|
1142
|
+
<div class="mermaid-modal-content" id="mermaid-modal-content"></div>
|
|
1143
|
+
</div>
|
|
1144
|
+
|
|
1068
1145
|
<link
|
|
1069
1146
|
rel="stylesheet"
|
|
1070
1147
|
href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css"
|
|
@@ -1280,6 +1357,84 @@
|
|
|
1280
1357
|
});
|
|
1281
1358
|
});
|
|
1282
1359
|
|
|
1360
|
+
// Mermaid Fullscreen Logic
|
|
1361
|
+
const modal = document.getElementById("mermaid-modal");
|
|
1362
|
+
const modalContent = document.getElementById("mermaid-modal-content");
|
|
1363
|
+
const modalClose = document.getElementById("mermaid-modal-close");
|
|
1364
|
+
|
|
1365
|
+
if (modal && modalContent && modalClose) {
|
|
1366
|
+
const closeModal = () => {
|
|
1367
|
+
modal.classList.remove("active");
|
|
1368
|
+
modalContent.innerHTML = "";
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
modalClose.addEventListener("click", closeModal);
|
|
1372
|
+
|
|
1373
|
+
modal.addEventListener("click", (e) => {
|
|
1374
|
+
if (e.target === modal) {
|
|
1375
|
+
closeModal();
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// Handle Escape key
|
|
1380
|
+
document.addEventListener("keydown", (e) => {
|
|
1381
|
+
if (e.key === "Escape" && modal.classList.contains("active")) {
|
|
1382
|
+
closeModal();
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
document.querySelectorAll(".fullscreen-mermaid-btn").forEach((btn) => {
|
|
1387
|
+
btn.addEventListener("click", (e) => {
|
|
1388
|
+
// The structure is:
|
|
1389
|
+
// wrapper > mermaid > svg
|
|
1390
|
+
// wrapper > toolbar > buttons > btn
|
|
1391
|
+
|
|
1392
|
+
// We need to find the mermaid div that is a sibling of the toolbar
|
|
1393
|
+
|
|
1394
|
+
// Traverse up to the wrapper
|
|
1395
|
+
let wrapper = btn.closest("div[style*='background: white']");
|
|
1396
|
+
|
|
1397
|
+
if (!wrapper) {
|
|
1398
|
+
// Fallback: try to find by traversing up and looking for .mermaid
|
|
1399
|
+
let p = btn.parentElement;
|
|
1400
|
+
while (p) {
|
|
1401
|
+
if (p.querySelector(".mermaid")) {
|
|
1402
|
+
wrapper = p;
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
p = p.parentElement;
|
|
1406
|
+
if (p === document.body) break;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
if (wrapper) {
|
|
1411
|
+
const mermaidDiv = wrapper.querySelector(".mermaid");
|
|
1412
|
+
if (mermaidDiv) {
|
|
1413
|
+
const svg = mermaidDiv.querySelector("svg");
|
|
1414
|
+
|
|
1415
|
+
if (svg) {
|
|
1416
|
+
// Clone the SVG to put in modal
|
|
1417
|
+
// We treat the SVG as the source
|
|
1418
|
+
const clone = svg.cloneNode(true);
|
|
1419
|
+
// Remove fixed sizes to let it scale in flex container
|
|
1420
|
+
clone.removeAttribute("height");
|
|
1421
|
+
clone.removeAttribute("width");
|
|
1422
|
+
clone.style.maxWidth = "100%";
|
|
1423
|
+
clone.style.maxHeight = "100%";
|
|
1424
|
+
|
|
1425
|
+
modalContent.appendChild(clone);
|
|
1426
|
+
modal.classList.add("active");
|
|
1427
|
+
} else if (mermaidDiv.textContent.trim()) {
|
|
1428
|
+
// Fallback if not rendered yet (should not happen on export usually)
|
|
1429
|
+
modalContent.textContent = "Diagram not rendered yet.";
|
|
1430
|
+
modal.classList.add("active");
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1283
1438
|
// Scroll to bottom button
|
|
1284
1439
|
const scrollBtn = document.getElementById("scroll-btn");
|
|
1285
1440
|
|
klaude_code/ui/core/input.py
CHANGED