klaude-code 2.5.3__py3-none-any.whl → 2.7.0__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/app/runtime.py +1 -1
- klaude_code/auth/__init__.py +10 -0
- klaude_code/auth/env.py +81 -0
- klaude_code/cli/auth_cmd.py +87 -8
- klaude_code/cli/config_cmd.py +5 -5
- klaude_code/cli/cost_cmd.py +159 -60
- klaude_code/cli/main.py +146 -65
- klaude_code/cli/self_update.py +7 -7
- klaude_code/config/builtin_config.py +23 -9
- klaude_code/config/config.py +19 -9
- klaude_code/const.py +10 -1
- klaude_code/core/reminders.py +4 -5
- klaude_code/core/turn.py +8 -9
- klaude_code/llm/google/client.py +12 -0
- klaude_code/llm/openai_compatible/stream.py +5 -1
- klaude_code/llm/openrouter/client.py +1 -0
- klaude_code/protocol/commands.py +0 -1
- klaude_code/protocol/events.py +214 -0
- klaude_code/protocol/sub_agent/image_gen.py +0 -4
- klaude_code/session/session.py +51 -18
- klaude_code/skill/loader.py +12 -13
- klaude_code/skill/manager.py +3 -3
- klaude_code/tui/command/__init__.py +1 -4
- klaude_code/tui/command/copy_cmd.py +1 -1
- klaude_code/tui/command/fork_session_cmd.py +4 -4
- klaude_code/tui/commands.py +0 -5
- klaude_code/tui/components/command_output.py +1 -1
- klaude_code/tui/components/metadata.py +4 -5
- klaude_code/tui/components/rich/markdown.py +60 -0
- klaude_code/tui/components/rich/theme.py +8 -0
- klaude_code/tui/components/sub_agent.py +6 -0
- klaude_code/tui/components/user_input.py +38 -27
- klaude_code/tui/display.py +11 -1
- klaude_code/tui/input/AGENTS.md +44 -0
- klaude_code/tui/input/completers.py +21 -21
- klaude_code/tui/input/drag_drop.py +197 -0
- klaude_code/tui/input/images.py +227 -0
- klaude_code/tui/input/key_bindings.py +173 -19
- klaude_code/tui/input/paste.py +71 -0
- klaude_code/tui/input/prompt_toolkit.py +13 -3
- klaude_code/tui/machine.py +90 -56
- klaude_code/tui/renderer.py +1 -62
- klaude_code/tui/runner.py +1 -1
- klaude_code/tui/terminal/image.py +40 -9
- klaude_code/tui/terminal/selector.py +52 -2
- {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/METADATA +32 -40
- {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/RECORD +49 -54
- klaude_code/cli/session_cmd.py +0 -87
- klaude_code/protocol/events/__init__.py +0 -63
- klaude_code/protocol/events/base.py +0 -18
- klaude_code/protocol/events/chat.py +0 -30
- klaude_code/protocol/events/lifecycle.py +0 -23
- klaude_code/protocol/events/metadata.py +0 -16
- klaude_code/protocol/events/streaming.py +0 -43
- klaude_code/protocol/events/system.py +0 -56
- klaude_code/protocol/events/tools.py +0 -27
- klaude_code/tui/command/terminal_setup_cmd.py +0 -248
- klaude_code/tui/input/clipboard.py +0 -152
- {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from klaude_code.protocol import llm_param, message, model
|
|
9
|
+
from klaude_code.protocol.commands import CommandName
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AssistantImageDeltaEvent",
|
|
13
|
+
"AssistantTextDeltaEvent",
|
|
14
|
+
"AssistantTextEndEvent",
|
|
15
|
+
"AssistantTextStartEvent",
|
|
16
|
+
"CommandOutputEvent",
|
|
17
|
+
"DeveloperMessageEvent",
|
|
18
|
+
"EndEvent",
|
|
19
|
+
"ErrorEvent",
|
|
20
|
+
"Event",
|
|
21
|
+
"InterruptEvent",
|
|
22
|
+
"ReplayEventUnion",
|
|
23
|
+
"ReplayHistoryEvent",
|
|
24
|
+
"ResponseCompleteEvent",
|
|
25
|
+
"ResponseEvent",
|
|
26
|
+
"TaskFinishEvent",
|
|
27
|
+
"TaskMetadataEvent",
|
|
28
|
+
"TaskStartEvent",
|
|
29
|
+
"ThinkingDeltaEvent",
|
|
30
|
+
"ThinkingEndEvent",
|
|
31
|
+
"ThinkingStartEvent",
|
|
32
|
+
"TodoChangeEvent",
|
|
33
|
+
"ToolCallEvent",
|
|
34
|
+
"ToolCallStartEvent",
|
|
35
|
+
"ToolResultEvent",
|
|
36
|
+
"TurnEndEvent",
|
|
37
|
+
"TurnStartEvent",
|
|
38
|
+
"UsageEvent",
|
|
39
|
+
"UserMessageEvent",
|
|
40
|
+
"WelcomeEvent",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Event(BaseModel):
|
|
45
|
+
"""Base event."""
|
|
46
|
+
|
|
47
|
+
session_id: str
|
|
48
|
+
timestamp: float = Field(default_factory=time.time)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ResponseEvent(Event):
|
|
52
|
+
"""Event associated with a single model response."""
|
|
53
|
+
|
|
54
|
+
response_id: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class UserMessageEvent(Event):
|
|
58
|
+
content: str
|
|
59
|
+
images: list[message.ImageURLPart] | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DeveloperMessageEvent(Event):
|
|
63
|
+
"""DeveloperMessages are reminders in user messages or tool results."""
|
|
64
|
+
|
|
65
|
+
item: message.DeveloperMessage
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TodoChangeEvent(Event):
|
|
69
|
+
todos: list[model.TodoItem]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class CommandOutputEvent(Event):
|
|
73
|
+
"""Event for command output display. Not persisted to session history."""
|
|
74
|
+
|
|
75
|
+
command_name: CommandName | str
|
|
76
|
+
content: str = ""
|
|
77
|
+
ui_extra: model.ToolResultUIExtra | None = None
|
|
78
|
+
is_error: bool = False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TaskStartEvent(Event):
|
|
82
|
+
sub_agent_state: model.SubAgentState | None = None
|
|
83
|
+
model_id: str | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TaskFinishEvent(Event):
|
|
87
|
+
task_result: str
|
|
88
|
+
has_structured_output: bool = False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TurnStartEvent(Event):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TurnEndEvent(Event):
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class UsageEvent(ResponseEvent):
|
|
100
|
+
usage: model.Usage
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TaskMetadataEvent(Event):
|
|
104
|
+
metadata: model.TaskMetadataItem
|
|
105
|
+
cancelled: bool = False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ThinkingStartEvent(ResponseEvent):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ThinkingDeltaEvent(ResponseEvent):
|
|
113
|
+
content: str
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ThinkingEndEvent(ResponseEvent):
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class AssistantTextStartEvent(ResponseEvent):
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AssistantTextDeltaEvent(ResponseEvent):
|
|
125
|
+
content: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class AssistantTextEndEvent(ResponseEvent):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class AssistantImageDeltaEvent(ResponseEvent):
|
|
133
|
+
file_path: str
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ToolCallStartEvent(ResponseEvent):
|
|
137
|
+
tool_call_id: str
|
|
138
|
+
tool_name: str
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ResponseCompleteEvent(ResponseEvent):
|
|
142
|
+
"""Final snapshot of the model response."""
|
|
143
|
+
|
|
144
|
+
content: str
|
|
145
|
+
thinking_text: str | None = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class WelcomeEvent(Event):
|
|
149
|
+
work_dir: str
|
|
150
|
+
llm_config: llm_param.LLMConfigParameter
|
|
151
|
+
show_klaude_code_info: bool = True
|
|
152
|
+
loaded_skills: dict[str, list[str]] = Field(default_factory=dict)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ErrorEvent(Event):
|
|
156
|
+
error_message: str
|
|
157
|
+
can_retry: bool = False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class InterruptEvent(Event):
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class EndEvent(Event):
|
|
165
|
+
"""Global display shutdown."""
|
|
166
|
+
|
|
167
|
+
session_id: str = "__app__"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
type ReplayEventUnion = (
|
|
171
|
+
TaskStartEvent
|
|
172
|
+
| TaskFinishEvent
|
|
173
|
+
| TurnStartEvent
|
|
174
|
+
| ThinkingStartEvent
|
|
175
|
+
| ThinkingDeltaEvent
|
|
176
|
+
| ThinkingEndEvent
|
|
177
|
+
| AssistantTextStartEvent
|
|
178
|
+
| AssistantTextDeltaEvent
|
|
179
|
+
| AssistantTextEndEvent
|
|
180
|
+
| AssistantImageDeltaEvent
|
|
181
|
+
| ToolCallEvent
|
|
182
|
+
| ToolResultEvent
|
|
183
|
+
| UserMessageEvent
|
|
184
|
+
| TaskMetadataEvent
|
|
185
|
+
| InterruptEvent
|
|
186
|
+
| DeveloperMessageEvent
|
|
187
|
+
| ErrorEvent
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ReplayHistoryEvent(Event):
|
|
192
|
+
events: list[ReplayEventUnion]
|
|
193
|
+
updated_at: float
|
|
194
|
+
is_load: bool = True
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ToolCallEvent(ResponseEvent):
|
|
198
|
+
tool_call_id: str
|
|
199
|
+
tool_name: str
|
|
200
|
+
arguments: str
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ToolResultEvent(ResponseEvent):
|
|
204
|
+
tool_call_id: str
|
|
205
|
+
tool_name: str
|
|
206
|
+
result: str
|
|
207
|
+
ui_extra: model.ToolResultUIExtra | None = None
|
|
208
|
+
status: Literal["success", "error", "aborted"]
|
|
209
|
+
task_metadata: model.TaskMetadata | None = None
|
|
210
|
+
is_last_in_turn: bool = True
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def is_error(self) -> bool:
|
|
214
|
+
return self.status in ("error", "aborted")
|
|
@@ -66,10 +66,6 @@ IMAGE_GEN_PARAMETERS: dict[str, Any] = {
|
|
|
66
66
|
"enum": ["1K", "2K", "4K"],
|
|
67
67
|
"description": "Output size for Nano Banana Pro (must use uppercase K).",
|
|
68
68
|
},
|
|
69
|
-
"extra": {
|
|
70
|
-
"type": "object",
|
|
71
|
-
"description": "Provider/model-specific extra parameters (future-proofing).",
|
|
72
|
-
},
|
|
73
69
|
},
|
|
74
70
|
"additionalProperties": False,
|
|
75
71
|
},
|
klaude_code/session/session.py
CHANGED
|
@@ -304,24 +304,57 @@ class Session(BaseModel):
|
|
|
304
304
|
yield events.TurnStartEvent(session_id=self.id)
|
|
305
305
|
match it:
|
|
306
306
|
case message.AssistantMessage() as am:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
last_assistant_content = message.format_saved_images(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
307
|
+
all_images = [part for part in am.parts if isinstance(part, message.ImageFilePart)]
|
|
308
|
+
full_content = message.join_text_parts(am.parts)
|
|
309
|
+
last_assistant_content = message.format_saved_images(all_images, full_content)
|
|
310
|
+
|
|
311
|
+
# Reconstruct streaming boundaries from saved parts.
|
|
312
|
+
# This allows replay to reuse the same TUI state machine as live events.
|
|
313
|
+
thinking_open = False
|
|
314
|
+
assistant_open = False
|
|
315
|
+
|
|
316
|
+
for part in am.parts:
|
|
317
|
+
if isinstance(part, message.ThinkingTextPart):
|
|
318
|
+
if assistant_open:
|
|
319
|
+
assistant_open = False
|
|
320
|
+
yield events.AssistantTextEndEvent(response_id=am.response_id, session_id=self.id)
|
|
321
|
+
if not thinking_open:
|
|
322
|
+
thinking_open = True
|
|
323
|
+
yield events.ThinkingStartEvent(response_id=am.response_id, session_id=self.id)
|
|
324
|
+
if part.text:
|
|
325
|
+
yield events.ThinkingDeltaEvent(
|
|
326
|
+
content=part.text,
|
|
327
|
+
response_id=am.response_id,
|
|
328
|
+
session_id=self.id,
|
|
329
|
+
)
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
if thinking_open:
|
|
333
|
+
thinking_open = False
|
|
334
|
+
yield events.ThinkingEndEvent(response_id=am.response_id, session_id=self.id)
|
|
335
|
+
|
|
336
|
+
if isinstance(part, message.TextPart):
|
|
337
|
+
if not assistant_open:
|
|
338
|
+
assistant_open = True
|
|
339
|
+
yield events.AssistantTextStartEvent(response_id=am.response_id, session_id=self.id)
|
|
340
|
+
if part.text:
|
|
341
|
+
yield events.AssistantTextDeltaEvent(
|
|
342
|
+
content=part.text,
|
|
343
|
+
response_id=am.response_id,
|
|
344
|
+
session_id=self.id,
|
|
345
|
+
)
|
|
346
|
+
elif isinstance(part, message.ImageFilePart):
|
|
347
|
+
yield events.AssistantImageDeltaEvent(
|
|
348
|
+
file_path=part.file_path,
|
|
349
|
+
response_id=am.response_id,
|
|
350
|
+
session_id=self.id,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
if thinking_open:
|
|
354
|
+
yield events.ThinkingEndEvent(response_id=am.response_id, session_id=self.id)
|
|
355
|
+
if assistant_open:
|
|
356
|
+
yield events.AssistantTextEndEvent(response_id=am.response_id, session_id=self.id)
|
|
357
|
+
|
|
325
358
|
for part in am.parts:
|
|
326
359
|
if not isinstance(part, message.ToolCallPart):
|
|
327
360
|
continue
|
klaude_code/skill/loader.py
CHANGED
|
@@ -209,22 +209,21 @@ class SkillLoader:
|
|
|
209
209
|
"""Get list of all loaded skill names"""
|
|
210
210
|
return list(self.loaded_skills.keys())
|
|
211
211
|
|
|
212
|
-
def
|
|
213
|
-
"""Generate
|
|
212
|
+
def get_skills_yaml(self) -> str:
|
|
213
|
+
"""Generate skill metadata in YAML format for system prompt.
|
|
214
214
|
|
|
215
215
|
Returns:
|
|
216
|
-
|
|
216
|
+
YAML string with all skill metadata
|
|
217
217
|
"""
|
|
218
|
-
|
|
219
|
-
# Prefer showing higher-priority skills first (project > user > system).
|
|
218
|
+
yaml_parts: list[str] = []
|
|
220
219
|
location_order = {"project": 0, "user": 1, "system": 2}
|
|
221
220
|
for skill in sorted(self.loaded_skills.values(), key=lambda s: location_order.get(s.location, 3)):
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
# Escape description for YAML (handle multi-line and special chars)
|
|
222
|
+
desc = skill.description.replace("\n", " ").strip()
|
|
223
|
+
yaml_parts.append(
|
|
224
|
+
f"- name: {skill.name}\n"
|
|
225
|
+
f" description: {desc}\n"
|
|
226
|
+
f" scope: {skill.location}\n"
|
|
227
|
+
f" location: {skill.skill_path}"
|
|
229
228
|
)
|
|
230
|
-
return "\n".join(
|
|
229
|
+
return "\n".join(yaml_parts)
|
klaude_code/skill/manager.py
CHANGED
|
@@ -80,8 +80,8 @@ def format_available_skills_for_system_prompt() -> str:
|
|
|
80
80
|
|
|
81
81
|
try:
|
|
82
82
|
loader = _ensure_initialized()
|
|
83
|
-
|
|
84
|
-
if not
|
|
83
|
+
skills_yaml = loader.get_skills_yaml().strip()
|
|
84
|
+
if not skills_yaml:
|
|
85
85
|
return ""
|
|
86
86
|
|
|
87
87
|
return f"""
|
|
@@ -102,7 +102,7 @@ Important:
|
|
|
102
102
|
The list below is metadata only (name/description/location). The full instructions live in the referenced file.
|
|
103
103
|
|
|
104
104
|
<available_skills>
|
|
105
|
-
{
|
|
105
|
+
{skills_yaml}
|
|
106
106
|
</available_skills>"""
|
|
107
107
|
except Exception:
|
|
108
108
|
# Skills are an optional enhancement; do not fail prompt construction if discovery breaks.
|
|
@@ -40,7 +40,6 @@ def ensure_commands_loaded() -> None:
|
|
|
40
40
|
from .resume_cmd import ResumeCommand
|
|
41
41
|
from .status_cmd import StatusCommand
|
|
42
42
|
from .sub_agent_model_cmd import SubAgentModelCommand
|
|
43
|
-
from .terminal_setup_cmd import TerminalSetupCommand
|
|
44
43
|
from .thinking_cmd import ThinkingCommand
|
|
45
44
|
|
|
46
45
|
# Register in desired display order
|
|
@@ -55,7 +54,6 @@ def ensure_commands_loaded() -> None:
|
|
|
55
54
|
register(StatusCommand())
|
|
56
55
|
register(ResumeCommand())
|
|
57
56
|
register(ExportOnlineCommand())
|
|
58
|
-
register(TerminalSetupCommand())
|
|
59
57
|
register(DebugCommand())
|
|
60
58
|
register(ClearCommand())
|
|
61
59
|
|
|
@@ -76,7 +74,6 @@ def __getattr__(name: str) -> object:
|
|
|
76
74
|
"ResumeCommand": "resume_cmd",
|
|
77
75
|
"StatusCommand": "status_cmd",
|
|
78
76
|
"SubAgentModelCommand": "sub_agent_model_cmd",
|
|
79
|
-
"TerminalSetupCommand": "terminal_setup_cmd",
|
|
80
77
|
"ThinkingCommand": "thinking_cmd",
|
|
81
78
|
}
|
|
82
79
|
if name in _commands_map:
|
|
@@ -91,7 +88,7 @@ __all__ = [
|
|
|
91
88
|
# Command classes are lazily loaded via __getattr__
|
|
92
89
|
# "ClearCommand", "DiffCommand", "HelpCommand", "ModelCommand",
|
|
93
90
|
# "ExportCommand", "RefreshTerminalCommand", "ReleaseNotesCommand",
|
|
94
|
-
# "StatusCommand",
|
|
91
|
+
# "StatusCommand",
|
|
95
92
|
"CommandABC",
|
|
96
93
|
"CommandResult",
|
|
97
94
|
"dispatch_command",
|
|
@@ -6,7 +6,7 @@ from typing import Literal
|
|
|
6
6
|
from prompt_toolkit.styles import Style, merge_styles
|
|
7
7
|
|
|
8
8
|
from klaude_code.protocol import commands, events, message, model
|
|
9
|
-
from klaude_code.tui.input.
|
|
9
|
+
from klaude_code.tui.input.key_bindings import copy_to_clipboard
|
|
10
10
|
from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, select_one
|
|
11
11
|
|
|
12
12
|
from .command_abc import Agent, CommandABC, CommandResult
|
|
@@ -194,7 +194,7 @@ class ForkSessionCommand(CommandABC):
|
|
|
194
194
|
|
|
195
195
|
@property
|
|
196
196
|
def summary(self) -> str:
|
|
197
|
-
return "Fork the current session and show a resume
|
|
197
|
+
return "Fork the current session and show a resume command"
|
|
198
198
|
|
|
199
199
|
@property
|
|
200
200
|
def is_interactive(self) -> bool:
|
|
@@ -220,7 +220,7 @@ class ForkSessionCommand(CommandABC):
|
|
|
220
220
|
new_session = agent.session.fork()
|
|
221
221
|
await new_session.wait_for_flush()
|
|
222
222
|
|
|
223
|
-
resume_cmd = f"klaude --resume
|
|
223
|
+
resume_cmd = f"klaude --resume {new_session.id}"
|
|
224
224
|
copy_to_clipboard(resume_cmd)
|
|
225
225
|
|
|
226
226
|
event = events.CommandOutputEvent(
|
|
@@ -249,7 +249,7 @@ class ForkSessionCommand(CommandABC):
|
|
|
249
249
|
# Build result message
|
|
250
250
|
fork_description = "entire conversation" if selected == -1 else f"up to message index {selected}"
|
|
251
251
|
|
|
252
|
-
resume_cmd = f"klaude --resume
|
|
252
|
+
resume_cmd = f"klaude --resume {new_session.id}"
|
|
253
253
|
copy_to_clipboard(resume_cmd)
|
|
254
254
|
|
|
255
255
|
event = events.CommandOutputEvent(
|
klaude_code/tui/commands.py
CHANGED
|
@@ -13,11 +13,6 @@ class RenderCommand:
|
|
|
13
13
|
pass
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
@dataclass(frozen=True, slots=True)
|
|
17
|
-
class RenderReplayHistory(RenderCommand):
|
|
18
|
-
event: events.ReplayHistoryEvent
|
|
19
|
-
|
|
20
|
-
|
|
21
16
|
@dataclass(frozen=True, slots=True)
|
|
22
17
|
class RenderWelcome(RenderCommand):
|
|
23
18
|
event: events.WelcomeEvent
|
|
@@ -50,7 +50,7 @@ def _render_fork_session_output(e: events.CommandOutputEvent) -> RenderableType:
|
|
|
50
50
|
grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
|
|
51
51
|
|
|
52
52
|
grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
|
|
53
|
-
grid.add_row(Text(f" klaude --resume
|
|
53
|
+
grid.add_row(Text(f" klaude --resume {session_id}", style=ThemeKey.TOOL_RESULT_BOLD))
|
|
54
54
|
|
|
55
55
|
return Padding.indent(grid, level=2)
|
|
56
56
|
|
|
@@ -30,13 +30,12 @@ def _render_task_metadata_block(
|
|
|
30
30
|
currency = metadata.usage.currency if metadata.usage else "USD"
|
|
31
31
|
currency_symbol = "¥" if currency == "CNY" else "$"
|
|
32
32
|
|
|
33
|
-
# Second column: model
|
|
33
|
+
# Second column: provider/model description / tokens / cost / …
|
|
34
34
|
content = Text()
|
|
35
|
-
content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
|
|
36
35
|
if metadata.provider is not None:
|
|
37
|
-
content.append_text(Text("
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
content.append_text(Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA))
|
|
37
|
+
content.append_text(Text("/", style=ThemeKey.METADATA_DIM))
|
|
38
|
+
content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA_BOLD))
|
|
40
39
|
if metadata.description:
|
|
41
40
|
content.append_text(Text(" ", style=ThemeKey.METADATA)).append_text(
|
|
42
41
|
Text(metadata.description, style=ThemeKey.METADATA_ITALIC)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import io
|
|
5
|
+
import re
|
|
5
6
|
import time
|
|
6
7
|
from collections.abc import Callable
|
|
7
8
|
from typing import Any, ClassVar
|
|
@@ -26,6 +27,63 @@ from klaude_code.const import (
|
|
|
26
27
|
)
|
|
27
28
|
from klaude_code.tui.components.rich.code_panel import CodePanel
|
|
28
29
|
|
|
30
|
+
_THINKING_HTML_BLOCK_RE = re.compile(
|
|
31
|
+
r"\A\s*<thinking>\s*\n?(?P<body>.*?)(?:\n\s*)?</thinking>\s*\Z",
|
|
32
|
+
flags=re.IGNORECASE | re.DOTALL,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_HTML_COMMENT_BLOCK_RE = re.compile(r"\A\s*<!--.*?-->\s*\Z", flags=re.DOTALL)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ThinkingHTMLBlock(MarkdownElement):
|
|
39
|
+
"""Render `<thinking>...</thinking>` HTML blocks as Rich Markdown.
|
|
40
|
+
|
|
41
|
+
markdown-it-py treats custom tags like `<thinking>` as HTML blocks, and Rich
|
|
42
|
+
Markdown ignores HTML blocks by default. This element restores visibility by
|
|
43
|
+
re-parsing the inner content as Markdown and applying a dedicated style.
|
|
44
|
+
|
|
45
|
+
Non-thinking HTML blocks (including comment sentinels like `<!-- -->`) render
|
|
46
|
+
no visible output, matching Rich's default behavior.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
new_line: ClassVar[bool] = True
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def create(cls, markdown: Markdown, token: Token) -> ThinkingHTMLBlock:
|
|
53
|
+
return cls(content=token.content or "", code_theme=markdown.code_theme)
|
|
54
|
+
|
|
55
|
+
def __init__(self, *, content: str, code_theme: str) -> None:
|
|
56
|
+
self._content = content
|
|
57
|
+
self._code_theme = code_theme
|
|
58
|
+
|
|
59
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
60
|
+
stripped = self._content.strip()
|
|
61
|
+
|
|
62
|
+
# Keep HTML comments invisible. MarkdownStream relies on a comment sentinel
|
|
63
|
+
# (`<!-- -->`) to preserve inter-block spacing in some streaming frames.
|
|
64
|
+
if _HTML_COMMENT_BLOCK_RE.match(stripped):
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
match = _THINKING_HTML_BLOCK_RE.match(stripped)
|
|
68
|
+
if match is None:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
body = match.group("body").strip("\n")
|
|
72
|
+
if not body.strip():
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Render as a single line to avoid the extra blank lines produced by
|
|
76
|
+
# paragraph/block rendering.
|
|
77
|
+
collapsed = " ".join(body.split())
|
|
78
|
+
if not collapsed:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
text = Text()
|
|
82
|
+
text.append("<thinking>", style="markdown.thinking.tag")
|
|
83
|
+
text.append(collapsed, style="markdown.thinking")
|
|
84
|
+
text.append("</thinking>", style="markdown.thinking.tag")
|
|
85
|
+
yield text
|
|
86
|
+
|
|
29
87
|
|
|
30
88
|
class NoInsetCodeBlock(CodeBlock):
|
|
31
89
|
"""A code block with syntax highlighting and no padding."""
|
|
@@ -105,6 +163,7 @@ class NoInsetMarkdown(Markdown):
|
|
|
105
163
|
"heading_open": LeftHeading,
|
|
106
164
|
"hr": Divider,
|
|
107
165
|
"table_open": MarkdownTable,
|
|
166
|
+
"html_block": ThinkingHTMLBlock,
|
|
108
167
|
}
|
|
109
168
|
|
|
110
169
|
|
|
@@ -118,6 +177,7 @@ class ThinkingMarkdown(Markdown):
|
|
|
118
177
|
"heading_open": LeftHeading,
|
|
119
178
|
"hr": Divider,
|
|
120
179
|
"table_open": MarkdownTable,
|
|
180
|
+
"html_block": ThinkingHTMLBlock,
|
|
121
181
|
}
|
|
122
182
|
|
|
123
183
|
|
|
@@ -331,7 +331,14 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
331
331
|
markdown_theme=Theme(
|
|
332
332
|
styles={
|
|
333
333
|
"markdown.code": palette.purple,
|
|
334
|
+
# Render degraded `<thinking>...</thinking>` blocks inside assistant markdown.
|
|
335
|
+
# This must live in markdown_theme (not just thinking_markdown_theme) because
|
|
336
|
+
# it is used while rendering assistant output.
|
|
337
|
+
"markdown.thinking": "italic " + palette.grey2,
|
|
338
|
+
"markdown.thinking.tag": palette.grey2,
|
|
334
339
|
"markdown.code.border": palette.grey3,
|
|
340
|
+
# Used by ThinkingMarkdown when rendering `<thinking>` blocks.
|
|
341
|
+
"markdown.code.block": palette.grey1,
|
|
335
342
|
"markdown.h1": "bold reverse",
|
|
336
343
|
"markdown.h1.border": palette.grey3,
|
|
337
344
|
"markdown.h2": "bold underline",
|
|
@@ -353,6 +360,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
353
360
|
"markdown.code": palette.grey1 + " italic on " + palette.code_background,
|
|
354
361
|
"markdown.code.block": palette.grey1,
|
|
355
362
|
"markdown.code.border": palette.grey3,
|
|
363
|
+
"markdown.thinking.tag": palette.grey2 + " dim",
|
|
356
364
|
"markdown.h1": "bold reverse",
|
|
357
365
|
"markdown.h1.border": palette.grey3,
|
|
358
366
|
"markdown.h3": "bold " + palette.grey1,
|
|
@@ -135,6 +135,7 @@ def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAg
|
|
|
135
135
|
description = profile.name
|
|
136
136
|
prompt = ""
|
|
137
137
|
output_schema: dict[str, Any] | None = None
|
|
138
|
+
generation: dict[str, Any] | None = None
|
|
138
139
|
resume: str | None = None
|
|
139
140
|
if e.arguments:
|
|
140
141
|
try:
|
|
@@ -155,10 +156,15 @@ def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAg
|
|
|
155
156
|
schema_value = payload.get(profile.output_schema_arg)
|
|
156
157
|
if isinstance(schema_value, dict):
|
|
157
158
|
output_schema = cast(dict[str, Any], schema_value)
|
|
159
|
+
# Extract generation config for ImageGen
|
|
160
|
+
generation_value = payload.get("generation")
|
|
161
|
+
if isinstance(generation_value, dict):
|
|
162
|
+
generation = cast(dict[str, Any], generation_value)
|
|
158
163
|
return model.SubAgentState(
|
|
159
164
|
sub_agent_type=profile.name,
|
|
160
165
|
sub_agent_desc=description,
|
|
161
166
|
sub_agent_prompt=prompt,
|
|
162
167
|
resume=resume,
|
|
163
168
|
output_schema=output_schema,
|
|
169
|
+
generation=generation,
|
|
164
170
|
)
|