klaude-code 2.8.0__py3-none-any.whl → 2.9.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 +2 -1
- klaude_code/auth/antigravity/oauth.py +0 -9
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- klaude_code/auth/codex/exceptions.py +0 -4
- klaude_code/auth/codex/oauth.py +32 -28
- klaude_code/auth/codex/token_manager.py +0 -18
- klaude_code/cli/cost_cmd.py +128 -39
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +15 -4
- klaude_code/config/assets/builtin_config.yaml +8 -24
- klaude_code/config/config.py +47 -25
- klaude_code/config/sub_agent_model_helper.py +18 -13
- klaude_code/config/thinking.py +0 -8
- klaude_code/const.py +2 -2
- klaude_code/core/agent_profile.py +11 -53
- klaude_code/core/compaction/compaction.py +4 -6
- klaude_code/core/compaction/overflow.py +0 -4
- klaude_code/core/executor.py +51 -5
- klaude_code/core/manager/llm_clients.py +9 -1
- klaude_code/core/prompts/prompt-claude-code.md +4 -4
- klaude_code/core/reminders.py +21 -23
- klaude_code/core/task.py +0 -4
- klaude_code/core/tool/__init__.py +3 -2
- klaude_code/core/tool/file/apply_patch.py +0 -27
- klaude_code/core/tool/file/edit_tool.py +1 -2
- klaude_code/core/tool/file/read_tool.md +3 -2
- klaude_code/core/tool/file/read_tool.py +15 -2
- klaude_code/core/tool/offload.py +0 -35
- klaude_code/core/tool/sub_agent/__init__.py +6 -0
- klaude_code/core/tool/sub_agent/image_gen.md +16 -0
- klaude_code/core/tool/sub_agent/image_gen.py +146 -0
- klaude_code/core/tool/sub_agent/task.md +20 -0
- klaude_code/core/tool/sub_agent/task.py +205 -0
- klaude_code/core/tool/tool_registry.py +0 -16
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/input.py +6 -5
- klaude_code/llm/antigravity/input.py +14 -7
- klaude_code/llm/codex/client.py +22 -0
- klaude_code/llm/codex/prompt_sync.py +237 -0
- klaude_code/llm/google/client.py +8 -6
- klaude_code/llm/google/input.py +20 -12
- klaude_code/llm/image.py +18 -11
- klaude_code/llm/input_common.py +14 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +16 -1
- klaude_code/llm/registry.py +0 -5
- klaude_code/llm/responses/input.py +15 -5
- klaude_code/llm/usage.py +0 -8
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +2 -1
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/model.py +20 -1
- klaude_code/protocol/op.py +27 -0
- klaude_code/protocol/op_handler.py +10 -0
- klaude_code/protocol/sub_agent/AGENTS.md +5 -5
- klaude_code/protocol/sub_agent/__init__.py +13 -34
- klaude_code/protocol/sub_agent/explore.py +7 -34
- klaude_code/protocol/sub_agent/image_gen.py +3 -74
- klaude_code/protocol/sub_agent/task.py +3 -47
- klaude_code/protocol/sub_agent/web.py +8 -52
- klaude_code/protocol/tools.py +2 -0
- klaude_code/session/export.py +308 -299
- klaude_code/session/session.py +58 -21
- klaude_code/session/store.py +0 -4
- klaude_code/session/templates/export_session.html +430 -134
- klaude_code/skill/assets/deslop/SKILL.md +9 -0
- klaude_code/skill/system_skills.py +0 -20
- klaude_code/tui/command/__init__.py +3 -0
- klaude_code/tui/command/continue_cmd.py +34 -0
- klaude_code/tui/command/fork_session_cmd.py +5 -2
- klaude_code/tui/command/resume_cmd.py +9 -2
- klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
- klaude_code/tui/components/assistant.py +0 -26
- klaude_code/tui/components/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +2 -208
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/rich/markdown.py +60 -63
- klaude_code/tui/components/rich/theme.py +2 -0
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +43 -21
- klaude_code/tui/input/images.py +21 -18
- klaude_code/tui/input/key_bindings.py +2 -2
- klaude_code/tui/input/prompt_toolkit.py +49 -49
- klaude_code/tui/machine.py +15 -11
- klaude_code/tui/renderer.py +12 -20
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +6 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/RECORD +97 -92
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
|
@@ -170,23 +170,3 @@ def install_system_skills() -> bool:
|
|
|
170
170
|
|
|
171
171
|
log_debug("System skills installation complete")
|
|
172
172
|
return True
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def get_installed_system_skills() -> list[Path]:
|
|
176
|
-
"""Get list of installed system skill directories.
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
List of paths to installed skill directories
|
|
180
|
-
"""
|
|
181
|
-
dest_dir = get_system_skills_dir()
|
|
182
|
-
if not dest_dir.exists():
|
|
183
|
-
return []
|
|
184
|
-
|
|
185
|
-
skills: list[Path] = []
|
|
186
|
-
for item in dest_dir.iterdir():
|
|
187
|
-
if item.is_dir() and not item.name.startswith("."):
|
|
188
|
-
skill_file = item / "SKILL.md"
|
|
189
|
-
if skill_file.exists():
|
|
190
|
-
skills.append(item)
|
|
191
|
-
|
|
192
|
-
return skills
|
|
@@ -31,6 +31,7 @@ def ensure_commands_loaded() -> None:
|
|
|
31
31
|
# Import and register commands in display order
|
|
32
32
|
from .clear_cmd import ClearCommand
|
|
33
33
|
from .compact_cmd import CompactCommand
|
|
34
|
+
from .continue_cmd import ContinueCommand
|
|
34
35
|
from .copy_cmd import CopyCommand
|
|
35
36
|
from .debug_cmd import DebugCommand
|
|
36
37
|
from .export_cmd import ExportCommand
|
|
@@ -47,6 +48,7 @@ def ensure_commands_loaded() -> None:
|
|
|
47
48
|
register(CopyCommand())
|
|
48
49
|
register(ExportCommand())
|
|
49
50
|
register(CompactCommand())
|
|
51
|
+
register(ContinueCommand())
|
|
50
52
|
register(RefreshTerminalCommand())
|
|
51
53
|
register(ModelCommand())
|
|
52
54
|
register(SubAgentModelCommand())
|
|
@@ -67,6 +69,7 @@ def __getattr__(name: str) -> object:
|
|
|
67
69
|
_commands_map = {
|
|
68
70
|
"ClearCommand": "clear_cmd",
|
|
69
71
|
"CompactCommand": "compact_cmd",
|
|
72
|
+
"ContinueCommand": "continue_cmd",
|
|
70
73
|
"CopyCommand": "copy_cmd",
|
|
71
74
|
"DebugCommand": "debug_cmd",
|
|
72
75
|
"ExportCommand": "export_cmd",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from klaude_code.protocol import commands, events, message, op
|
|
2
|
+
|
|
3
|
+
from .command_abc import Agent, CommandABC, CommandResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ContinueCommand(CommandABC):
|
|
7
|
+
"""Continue agent execution without adding a new user message."""
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def name(self) -> commands.CommandName:
|
|
11
|
+
return commands.CommandName.CONTINUE
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def summary(self) -> str:
|
|
15
|
+
return "Continue agent execution (for recovery after interruptions)"
|
|
16
|
+
|
|
17
|
+
async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
|
|
18
|
+
del user_input # unused
|
|
19
|
+
|
|
20
|
+
if agent.session.messages_count == 0:
|
|
21
|
+
return CommandResult(
|
|
22
|
+
events=[
|
|
23
|
+
events.CommandOutputEvent(
|
|
24
|
+
session_id=agent.session.id,
|
|
25
|
+
command_name=self.name,
|
|
26
|
+
content="Cannot continue: no conversation history. Start a conversation first.",
|
|
27
|
+
is_error=True,
|
|
28
|
+
)
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return CommandResult(
|
|
33
|
+
operations=[op.ContinueAgentOperation(session_id=agent.session.id)],
|
|
34
|
+
)
|
|
@@ -6,6 +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.session import Session
|
|
9
10
|
from klaude_code.tui.input.key_bindings import copy_to_clipboard
|
|
10
11
|
from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, select_one
|
|
11
12
|
|
|
@@ -312,7 +313,8 @@ class ForkSessionCommand(CommandABC):
|
|
|
312
313
|
new_session = agent.session.fork()
|
|
313
314
|
await new_session.wait_for_flush()
|
|
314
315
|
|
|
315
|
-
|
|
316
|
+
short_id = Session.shortest_unique_prefix(new_session.id)
|
|
317
|
+
resume_cmd = f"klaude -r {short_id}"
|
|
316
318
|
copy_to_clipboard(resume_cmd)
|
|
317
319
|
|
|
318
320
|
event = events.CommandOutputEvent(
|
|
@@ -345,7 +347,8 @@ class ForkSessionCommand(CommandABC):
|
|
|
345
347
|
else:
|
|
346
348
|
fork_description = "entire conversation" if selected == -1 else f"up to message index {selected}"
|
|
347
349
|
|
|
348
|
-
|
|
350
|
+
short_id = Session.shortest_unique_prefix(new_session.id)
|
|
351
|
+
resume_cmd = f"klaude -r {short_id}"
|
|
349
352
|
copy_to_clipboard(resume_cmd)
|
|
350
353
|
|
|
351
354
|
event = events.CommandOutputEvent(
|
|
@@ -8,9 +8,16 @@ from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem,
|
|
|
8
8
|
from .command_abc import Agent, CommandABC, CommandResult
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def select_session_sync() -> str | None:
|
|
12
|
-
"""Interactive session selection (sync version for asyncio.to_thread).
|
|
11
|
+
def select_session_sync(session_ids: list[str] | None = None) -> str | None:
|
|
12
|
+
"""Interactive session selection (sync version for asyncio.to_thread).
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
session_ids: Optional list of session IDs to filter. If provided, only show these sessions.
|
|
16
|
+
"""
|
|
13
17
|
options = build_session_select_options()
|
|
18
|
+
if session_ids is not None:
|
|
19
|
+
session_id_set = set(session_ids)
|
|
20
|
+
options = [opt for opt in options if opt.session_id in session_id_set]
|
|
14
21
|
if not options:
|
|
15
22
|
log("No sessions found for this project.")
|
|
16
23
|
return None
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
"""Command for changing sub-agent models."""
|
|
1
|
+
"""Command for changing sub-agent models and compact model."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
|
|
7
|
-
from klaude_code.config.config import load_config
|
|
7
|
+
from klaude_code.config.config import Config, load_config
|
|
8
8
|
from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper, SubAgentModelInfo
|
|
9
9
|
from klaude_code.protocol import commands, events, message, op
|
|
10
10
|
from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, build_model_select_items, select_one
|
|
@@ -12,16 +12,35 @@ from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem,
|
|
|
12
12
|
from .command_abc import Agent, CommandABC, CommandResult
|
|
13
13
|
|
|
14
14
|
USE_DEFAULT_BEHAVIOR = "__default__"
|
|
15
|
+
COMPACT_MODEL_OPTION = "__compact__"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_compact_model_item(config: Config, max_name_len: int, main_model_name: str) -> SelectItem[str]:
|
|
19
|
+
"""Build SelectItem for compact model configuration."""
|
|
20
|
+
name = "Compact"
|
|
21
|
+
model_display = config.compact_model or f"(inherit from main agent: {main_model_name})"
|
|
22
|
+
|
|
23
|
+
title = [
|
|
24
|
+
("class:msg", f"{name:<{max_name_len}}"),
|
|
25
|
+
("class:meta", f" current: {model_display}\n"),
|
|
26
|
+
]
|
|
27
|
+
return SelectItem(title=title, value=COMPACT_MODEL_OPTION, search_text="compact")
|
|
15
28
|
|
|
16
29
|
|
|
17
30
|
def _build_sub_agent_select_items(
|
|
18
31
|
sub_agents: list[SubAgentModelInfo],
|
|
19
32
|
helper: SubAgentModelHelper,
|
|
20
33
|
main_model_name: str,
|
|
34
|
+
config: Config,
|
|
21
35
|
) -> list[SelectItem[str]]:
|
|
22
|
-
"""Build SelectItem list for sub-agent selection."""
|
|
36
|
+
"""Build SelectItem list for sub-agent selection (including compact model)."""
|
|
23
37
|
items: list[SelectItem[str]] = []
|
|
38
|
+
# Include "Compact" in max_name_len calculation
|
|
24
39
|
max_name_len = max(len(sa.profile.name) for sa in sub_agents) if sub_agents else 0
|
|
40
|
+
max_name_len = max(max_name_len, len("Compact"))
|
|
41
|
+
|
|
42
|
+
# Add compact model option first
|
|
43
|
+
items.append(_build_compact_model_item(config, max_name_len, main_model_name))
|
|
25
44
|
|
|
26
45
|
for sa in sub_agents:
|
|
27
46
|
name = sa.profile.name
|
|
@@ -45,9 +64,10 @@ def _select_sub_agent_sync(
|
|
|
45
64
|
sub_agents: list[SubAgentModelInfo],
|
|
46
65
|
helper: SubAgentModelHelper,
|
|
47
66
|
main_model_name: str,
|
|
67
|
+
config: Config,
|
|
48
68
|
) -> str | None:
|
|
49
|
-
"""Synchronous sub-agent type selection."""
|
|
50
|
-
items = _build_sub_agent_select_items(sub_agents, helper, main_model_name)
|
|
69
|
+
"""Synchronous sub-agent type selection (including compact model)."""
|
|
70
|
+
items = _build_sub_agent_select_items(sub_agents, helper, main_model_name, config)
|
|
51
71
|
if not items:
|
|
52
72
|
return None
|
|
53
73
|
|
|
@@ -98,8 +118,39 @@ def _select_model_for_sub_agent_sync(
|
|
|
98
118
|
return None
|
|
99
119
|
|
|
100
120
|
|
|
121
|
+
def _select_model_for_compact_sync(
|
|
122
|
+
config: Config,
|
|
123
|
+
main_model_name: str,
|
|
124
|
+
) -> str | None:
|
|
125
|
+
"""Synchronous model selection for compact model."""
|
|
126
|
+
models = config.iter_model_entries(only_available=True, include_disabled=False)
|
|
127
|
+
|
|
128
|
+
inherit_item = SelectItem[str](
|
|
129
|
+
title=[
|
|
130
|
+
("class:msg", "(Use default behavior)"),
|
|
131
|
+
("class:meta", f" -> inherit from main agent: {main_model_name}\n"),
|
|
132
|
+
],
|
|
133
|
+
value=USE_DEFAULT_BEHAVIOR,
|
|
134
|
+
search_text="default unset",
|
|
135
|
+
)
|
|
136
|
+
model_items = build_model_select_items(models)
|
|
137
|
+
all_items = [inherit_item, *model_items]
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
result = select_one(
|
|
141
|
+
message="Select model for Compact:",
|
|
142
|
+
items=all_items,
|
|
143
|
+
pointer="→",
|
|
144
|
+
style=DEFAULT_PICKER_STYLE,
|
|
145
|
+
use_search_filter=True,
|
|
146
|
+
)
|
|
147
|
+
return result if isinstance(result, str) else None
|
|
148
|
+
except KeyboardInterrupt:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
101
152
|
class SubAgentModelCommand(CommandABC):
|
|
102
|
-
"""Configure models for sub-agents (Task, Explore,
|
|
153
|
+
"""Configure models for sub-agents (Task, Explore, Web, ImageGen) and compact model."""
|
|
103
154
|
|
|
104
155
|
@property
|
|
105
156
|
def name(self) -> commands.CommandName:
|
|
@@ -119,32 +170,48 @@ class SubAgentModelCommand(CommandABC):
|
|
|
119
170
|
main_model_name = agent.get_llm_client().model_name
|
|
120
171
|
|
|
121
172
|
sub_agents = helper.get_available_sub_agents()
|
|
122
|
-
|
|
173
|
+
|
|
174
|
+
selected_option = await asyncio.to_thread(_select_sub_agent_sync, sub_agents, helper, main_model_name, config)
|
|
175
|
+
if selected_option is None:
|
|
123
176
|
return CommandResult(
|
|
124
177
|
events=[
|
|
125
178
|
events.CommandOutputEvent(
|
|
126
179
|
session_id=agent.session.id,
|
|
127
180
|
command_name=self.name,
|
|
128
|
-
content="
|
|
129
|
-
is_error=True,
|
|
181
|
+
content="(cancelled)",
|
|
130
182
|
)
|
|
131
183
|
]
|
|
132
184
|
)
|
|
133
185
|
|
|
134
|
-
|
|
135
|
-
if
|
|
186
|
+
# Handle compact model selection
|
|
187
|
+
if selected_option == COMPACT_MODEL_OPTION:
|
|
188
|
+
selected_model = await asyncio.to_thread(_select_model_for_compact_sync, config, main_model_name)
|
|
189
|
+
if selected_model is None:
|
|
190
|
+
return CommandResult(
|
|
191
|
+
events=[
|
|
192
|
+
events.CommandOutputEvent(
|
|
193
|
+
session_id=agent.session.id,
|
|
194
|
+
command_name=self.name,
|
|
195
|
+
content="(cancelled)",
|
|
196
|
+
)
|
|
197
|
+
]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
model_name: str | None = None if selected_model == USE_DEFAULT_BEHAVIOR else selected_model
|
|
201
|
+
|
|
136
202
|
return CommandResult(
|
|
137
|
-
|
|
138
|
-
|
|
203
|
+
operations=[
|
|
204
|
+
op.ChangeCompactModelOperation(
|
|
139
205
|
session_id=agent.session.id,
|
|
140
|
-
|
|
141
|
-
|
|
206
|
+
model_name=model_name,
|
|
207
|
+
save_as_default=True,
|
|
142
208
|
)
|
|
143
209
|
]
|
|
144
210
|
)
|
|
145
211
|
|
|
212
|
+
# Handle sub-agent model selection
|
|
146
213
|
selected_model = await asyncio.to_thread(
|
|
147
|
-
_select_model_for_sub_agent_sync, helper,
|
|
214
|
+
_select_model_for_sub_agent_sync, helper, selected_option, main_model_name
|
|
148
215
|
)
|
|
149
216
|
if selected_model is None:
|
|
150
217
|
return CommandResult(
|
|
@@ -157,13 +224,13 @@ class SubAgentModelCommand(CommandABC):
|
|
|
157
224
|
]
|
|
158
225
|
)
|
|
159
226
|
|
|
160
|
-
model_name
|
|
227
|
+
model_name = None if selected_model == USE_DEFAULT_BEHAVIOR else selected_model
|
|
161
228
|
|
|
162
229
|
return CommandResult(
|
|
163
230
|
operations=[
|
|
164
231
|
op.ChangeSubAgentModelOperation(
|
|
165
232
|
session_id=agent.session.id,
|
|
166
|
-
sub_agent_type=
|
|
233
|
+
sub_agent_type=selected_option,
|
|
167
234
|
model_name=model_name,
|
|
168
235
|
save_as_default=True,
|
|
169
236
|
)
|
|
@@ -1,28 +1,2 @@
|
|
|
1
|
-
from rich.console import RenderableType
|
|
2
|
-
from rich.padding import Padding
|
|
3
|
-
from rich.text import Text
|
|
4
|
-
|
|
5
|
-
from klaude_code.const import MARKDOWN_RIGHT_MARGIN
|
|
6
|
-
from klaude_code.tui.components.common import create_grid
|
|
7
|
-
from klaude_code.tui.components.rich.markdown import NoInsetMarkdown
|
|
8
|
-
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
9
|
-
|
|
10
1
|
# UI markers
|
|
11
2
|
ASSISTANT_MESSAGE_MARK = "•"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def render_assistant_message(content: str, *, code_theme: str) -> RenderableType | None:
|
|
15
|
-
"""Render assistant message for replay history display.
|
|
16
|
-
|
|
17
|
-
Returns None if content is empty.
|
|
18
|
-
"""
|
|
19
|
-
stripped = content.strip()
|
|
20
|
-
if len(stripped) == 0:
|
|
21
|
-
return None
|
|
22
|
-
|
|
23
|
-
grid = create_grid()
|
|
24
|
-
grid.add_row(
|
|
25
|
-
Text(ASSISTANT_MESSAGE_MARK, style=ThemeKey.ASSISTANT_MESSAGE_MARK),
|
|
26
|
-
Padding(NoInsetMarkdown(stripped, code_theme=code_theme), (0, MARKDOWN_RIGHT_MARGIN, 0, 0)),
|
|
27
|
-
)
|
|
28
|
-
return grid
|
|
@@ -4,6 +4,7 @@ from rich.table import Table
|
|
|
4
4
|
from rich.text import Text
|
|
5
5
|
|
|
6
6
|
from klaude_code.protocol import events, model
|
|
7
|
+
from klaude_code.session import Session
|
|
7
8
|
from klaude_code.tui.components.common import truncate_middle
|
|
8
9
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
9
10
|
|
|
@@ -47,10 +48,11 @@ def _render_fork_session_output(e: events.CommandOutputEvent) -> RenderableType:
|
|
|
47
48
|
|
|
48
49
|
grid = Table.grid(padding=(0, 1))
|
|
49
50
|
session_id = e.ui_extra.session_id
|
|
51
|
+
short_id = Session.shortest_unique_prefix(session_id)
|
|
50
52
|
grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
|
|
51
53
|
|
|
52
54
|
grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
|
|
53
|
-
grid.add_row(Text(f" klaude
|
|
55
|
+
grid.add_row(Text(f" klaude -r {short_id}", style=ThemeKey.TOOL_RESULT_BOLD))
|
|
54
56
|
|
|
55
57
|
return Padding.indent(grid, level=2)
|
|
56
58
|
|
|
@@ -115,5 +115,8 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
115
115
|
),
|
|
116
116
|
)
|
|
117
117
|
parts.append(grid)
|
|
118
|
+
case model.AtFileImagesUIItem():
|
|
119
|
+
# Image display is handled by renderer.display_developer_message
|
|
120
|
+
pass
|
|
118
121
|
|
|
119
122
|
return Group(*parts) if parts else Text("")
|
|
@@ -1,185 +1,12 @@
|
|
|
1
|
-
from rich import
|
|
2
|
-
from rich.console import Group, RenderableType
|
|
3
|
-
from rich.padding import Padding
|
|
4
|
-
from rich.panel import Panel
|
|
1
|
+
from rich.console import RenderableType
|
|
5
2
|
from rich.text import Text
|
|
6
3
|
|
|
7
|
-
from klaude_code.const import DIFF_PREFIX_WIDTH
|
|
4
|
+
from klaude_code.const import DIFF_PREFIX_WIDTH
|
|
8
5
|
from klaude_code.protocol import model
|
|
9
6
|
from klaude_code.tui.components.common import create_grid
|
|
10
7
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
11
8
|
|
|
12
9
|
|
|
13
|
-
def _make_diff_prefix(line: str, new_ln: int | None, width: int) -> tuple[str, int | None]:
|
|
14
|
-
kind = line[0]
|
|
15
|
-
|
|
16
|
-
number = " " * width
|
|
17
|
-
if kind in {"+", " "} and new_ln is not None:
|
|
18
|
-
number = f"{new_ln:>{width}}"
|
|
19
|
-
new_ln += 1
|
|
20
|
-
|
|
21
|
-
if kind == "-":
|
|
22
|
-
marker = "-"
|
|
23
|
-
elif kind == "+":
|
|
24
|
-
marker = "+"
|
|
25
|
-
else:
|
|
26
|
-
marker = " "
|
|
27
|
-
|
|
28
|
-
prefix = f"{number} {marker}"
|
|
29
|
-
return prefix, new_ln
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
|
|
33
|
-
if diff_text == "":
|
|
34
|
-
return Text("")
|
|
35
|
-
|
|
36
|
-
lines = diff_text.split("\n")
|
|
37
|
-
grid = create_grid()
|
|
38
|
-
grid.padding = (0, 0)
|
|
39
|
-
|
|
40
|
-
# Track line numbers based on hunk headers
|
|
41
|
-
new_ln: int | None = None
|
|
42
|
-
# Track if we're in untracked files section
|
|
43
|
-
in_untracked_section = False
|
|
44
|
-
# Track whether we've already rendered a file header
|
|
45
|
-
has_rendered_file_header = False
|
|
46
|
-
# Track whether we have rendered actual diff content for the current file
|
|
47
|
-
has_rendered_diff_content = False
|
|
48
|
-
# Track the "from" file name from --- line (used for deleted files)
|
|
49
|
-
from_file_name: str | None = None
|
|
50
|
-
|
|
51
|
-
for i, line in enumerate(lines):
|
|
52
|
-
# Check for untracked files section header
|
|
53
|
-
if line == "git ls-files --others --exclude-standard":
|
|
54
|
-
in_untracked_section = True
|
|
55
|
-
grid.add_row("", "")
|
|
56
|
-
grid.add_row("", Text("Untracked files:", style=ThemeKey.TOOL_MARK))
|
|
57
|
-
grid.add_row("", "")
|
|
58
|
-
continue
|
|
59
|
-
|
|
60
|
-
# Handle untracked files
|
|
61
|
-
if in_untracked_section:
|
|
62
|
-
# If we hit a new section or empty line, we're done with untracked files
|
|
63
|
-
if line.startswith("diff --git") or line.strip() == "":
|
|
64
|
-
in_untracked_section = False
|
|
65
|
-
elif line.strip(): # Non-empty line in untracked section
|
|
66
|
-
file_text = Text(line.strip(), style=ThemeKey.TOOL_PARAM_BOLD)
|
|
67
|
-
grid.add_row(
|
|
68
|
-
Text(f"{'+':>{DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_PARAM_BOLD),
|
|
69
|
-
file_text,
|
|
70
|
-
)
|
|
71
|
-
continue
|
|
72
|
-
|
|
73
|
-
# Capture "from" file name from --- line (needed for deleted files)
|
|
74
|
-
if line.startswith("--- "):
|
|
75
|
-
raw = line[4:].strip()
|
|
76
|
-
if raw != "/dev/null":
|
|
77
|
-
from_file_name = raw[2:] if raw.startswith(("a/", "b/")) else raw
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
# Parse file name from diff headers
|
|
81
|
-
if show_file_name and line.startswith("+++ "):
|
|
82
|
-
# Extract file name from +++ header with proper handling of /dev/null
|
|
83
|
-
raw = line[4:].strip()
|
|
84
|
-
if raw == "/dev/null":
|
|
85
|
-
# File was deleted, use the "from" file name
|
|
86
|
-
file_name = from_file_name or raw
|
|
87
|
-
elif raw.startswith(("a/", "b/")):
|
|
88
|
-
file_name = raw[2:]
|
|
89
|
-
else:
|
|
90
|
-
file_name = raw
|
|
91
|
-
|
|
92
|
-
file_text = Text(file_name, style=ThemeKey.DIFF_FILE_NAME)
|
|
93
|
-
|
|
94
|
-
# Count actual +/- lines for this file from i+1 onwards
|
|
95
|
-
file_additions = 0
|
|
96
|
-
file_deletions = 0
|
|
97
|
-
for remaining_line in lines[i + 1 :]:
|
|
98
|
-
if remaining_line.startswith("diff --git"):
|
|
99
|
-
break
|
|
100
|
-
elif remaining_line.startswith("+") and not remaining_line.startswith("+++"):
|
|
101
|
-
file_additions += 1
|
|
102
|
-
elif remaining_line.startswith("-") and not remaining_line.startswith("---"):
|
|
103
|
-
file_deletions += 1
|
|
104
|
-
|
|
105
|
-
# Create stats text
|
|
106
|
-
stats_text = Text()
|
|
107
|
-
if file_additions > 0:
|
|
108
|
-
stats_text.append(f"+{file_additions}", style=ThemeKey.DIFF_STATS_ADD)
|
|
109
|
-
if file_deletions > 0:
|
|
110
|
-
if file_additions > 0:
|
|
111
|
-
stats_text.append(" ")
|
|
112
|
-
stats_text.append(f"-{file_deletions}", style=ThemeKey.DIFF_STATS_REMOVE)
|
|
113
|
-
|
|
114
|
-
# Combine file name and stats
|
|
115
|
-
file_line = Text(style=ThemeKey.DIFF_FILE_NAME)
|
|
116
|
-
file_line.append_text(file_text)
|
|
117
|
-
if stats_text.plain:
|
|
118
|
-
file_line.append(" (")
|
|
119
|
-
file_line.append_text(stats_text)
|
|
120
|
-
file_line.append(")")
|
|
121
|
-
|
|
122
|
-
if has_rendered_file_header:
|
|
123
|
-
grid.add_row("", "")
|
|
124
|
-
|
|
125
|
-
if file_additions > 0 and file_deletions == 0:
|
|
126
|
-
file_mark = "+"
|
|
127
|
-
elif file_deletions > 0 and file_additions == 0:
|
|
128
|
-
file_mark = "-"
|
|
129
|
-
else:
|
|
130
|
-
file_mark = "±"
|
|
131
|
-
|
|
132
|
-
grid.add_row(
|
|
133
|
-
Text(f"{file_mark:>{DIFF_PREFIX_WIDTH}} ", style=ThemeKey.DIFF_FILE_NAME),
|
|
134
|
-
file_line,
|
|
135
|
-
)
|
|
136
|
-
has_rendered_file_header = True
|
|
137
|
-
has_rendered_diff_content = False
|
|
138
|
-
continue
|
|
139
|
-
|
|
140
|
-
if line.startswith("diff --git"):
|
|
141
|
-
has_rendered_diff_content = False
|
|
142
|
-
continue
|
|
143
|
-
|
|
144
|
-
# Parse hunk headers to reset counters: @@ -l,s +l,s @@
|
|
145
|
-
if line.startswith("@@"):
|
|
146
|
-
try:
|
|
147
|
-
parts = line.split()
|
|
148
|
-
plus = parts[2] # like '+12,4'
|
|
149
|
-
new_start = int(plus[1:].split(",")[0])
|
|
150
|
-
new_ln = new_start
|
|
151
|
-
except (IndexError, ValueError):
|
|
152
|
-
new_ln = None
|
|
153
|
-
if has_rendered_diff_content:
|
|
154
|
-
grid.add_row(Text(f"{'⋮':>{DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
|
|
155
|
-
continue
|
|
156
|
-
|
|
157
|
-
# Skip +++ lines (already handled above)
|
|
158
|
-
if line.startswith("+++ "):
|
|
159
|
-
continue
|
|
160
|
-
|
|
161
|
-
# Only handle unified diff hunk lines; ignore other metadata like
|
|
162
|
-
# "diff --git" or "index …" which would otherwise skew counters.
|
|
163
|
-
if not line or line[:1] not in {" ", "+", "-"}:
|
|
164
|
-
continue
|
|
165
|
-
|
|
166
|
-
# Compute line number prefix and style diff content
|
|
167
|
-
prefix, new_ln = _make_diff_prefix(line, new_ln, DIFF_PREFIX_WIDTH)
|
|
168
|
-
|
|
169
|
-
if line.startswith("-"):
|
|
170
|
-
text = Text(line[1:])
|
|
171
|
-
text.stylize(ThemeKey.DIFF_REMOVE)
|
|
172
|
-
elif line.startswith("+"):
|
|
173
|
-
text = Text(line[1:])
|
|
174
|
-
text.stylize(ThemeKey.DIFF_ADD)
|
|
175
|
-
else:
|
|
176
|
-
text = Text(line, style=ThemeKey.TOOL_RESULT)
|
|
177
|
-
grid.add_row(Text(prefix, ThemeKey.TOOL_RESULT), text)
|
|
178
|
-
has_rendered_diff_content = True
|
|
179
|
-
|
|
180
|
-
return grid
|
|
181
|
-
|
|
182
|
-
|
|
183
10
|
def render_structured_diff(ui_extra: model.DiffUIExtra, show_file_name: bool = False) -> RenderableType:
|
|
184
11
|
files = ui_extra.files
|
|
185
12
|
if not files:
|
|
@@ -204,39 +31,6 @@ def render_structured_diff(ui_extra: model.DiffUIExtra, show_file_name: bool = F
|
|
|
204
31
|
return grid
|
|
205
32
|
|
|
206
33
|
|
|
207
|
-
def render_diff_panel(
|
|
208
|
-
diff_text: str,
|
|
209
|
-
*,
|
|
210
|
-
show_file_name: bool = True,
|
|
211
|
-
heading: str = "DIFF",
|
|
212
|
-
indent: int = 2,
|
|
213
|
-
) -> RenderableType:
|
|
214
|
-
lines = diff_text.splitlines()
|
|
215
|
-
truncated_notice: Text | None = None
|
|
216
|
-
if len(lines) > MAX_DIFF_LINES:
|
|
217
|
-
truncated_lines = len(lines) - MAX_DIFF_LINES
|
|
218
|
-
diff_text = "\n".join(lines[:MAX_DIFF_LINES])
|
|
219
|
-
truncated_notice = Text(f"… truncated {truncated_lines} lines", style=ThemeKey.TOOL_MARK)
|
|
220
|
-
|
|
221
|
-
diff_body = render_diff(diff_text, show_file_name=show_file_name)
|
|
222
|
-
renderables: list[RenderableType] = [
|
|
223
|
-
Text(f" {heading} ", style="bold reverse"),
|
|
224
|
-
diff_body,
|
|
225
|
-
]
|
|
226
|
-
if truncated_notice is not None:
|
|
227
|
-
renderables.extend([Text(""), truncated_notice])
|
|
228
|
-
|
|
229
|
-
panel = Panel.fit(
|
|
230
|
-
Group(*renderables),
|
|
231
|
-
border_style=ThemeKey.LINES,
|
|
232
|
-
title_align="center",
|
|
233
|
-
box=box.ROUNDED,
|
|
234
|
-
)
|
|
235
|
-
if indent <= 0:
|
|
236
|
-
return panel
|
|
237
|
-
return Padding.indent(panel, level=indent)
|
|
238
|
-
|
|
239
|
-
|
|
240
34
|
def _render_file_header(file_diff: model.DiffFileDiff) -> tuple[Text, Text]:
|
|
241
35
|
file_text = Text(file_diff.file_path, style=ThemeKey.DIFF_FILE_NAME)
|
|
242
36
|
stats_text = Text()
|
|
@@ -9,6 +9,8 @@ def render_error(error_msg: Text) -> RenderableType:
|
|
|
9
9
|
"""Render error with X mark for error events."""
|
|
10
10
|
grid = create_grid()
|
|
11
11
|
error_msg.style = ThemeKey.ERROR
|
|
12
|
+
error_msg.overflow = "ellipsis"
|
|
13
|
+
error_msg.no_wrap = True
|
|
12
14
|
grid.add_row(Text("✘", style=ThemeKey.ERROR_BOLD), error_msg)
|
|
13
15
|
return grid
|
|
14
16
|
|
|
@@ -17,5 +19,7 @@ def render_tool_error(error_msg: Text) -> RenderableType:
|
|
|
17
19
|
"""Render error with indent for tool results."""
|
|
18
20
|
grid = create_grid()
|
|
19
21
|
error_msg.style = ThemeKey.ERROR
|
|
22
|
+
error_msg.overflow = "ellipsis"
|
|
23
|
+
error_msg.no_wrap = True
|
|
20
24
|
grid.add_row(Text(" "), error_msg)
|
|
21
25
|
return grid
|
|
@@ -16,7 +16,7 @@ _MERMAID_DEFAULT_PNG_SCALE = 2
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def artifacts_dir() -> Path:
|
|
19
|
-
return Path(TOOL_OUTPUT_TRUNCATION_DIR)
|
|
19
|
+
return Path(TOOL_OUTPUT_TRUNCATION_DIR)
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def _extract_pako_from_link(link: str) -> str | None:
|
|
@@ -72,7 +72,7 @@ def ensure_viewer_file(*, code: str, link: str, tool_call_id: str) -> Path | Non
|
|
|
72
72
|
return None
|
|
73
73
|
|
|
74
74
|
safe_id = tool_call_id.replace("/", "_")
|
|
75
|
-
path = artifacts_dir() / f"mermaid-
|
|
75
|
+
path = artifacts_dir() / f"klaude-mermaid-{safe_id}.html"
|
|
76
76
|
if path.exists():
|
|
77
77
|
return path
|
|
78
78
|
|