klaude-code 2.6.0__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/env.py +19 -15
- klaude_code/cli/auth_cmd.py +1 -1
- klaude_code/cli/main.py +98 -8
- klaude_code/const.py +10 -1
- klaude_code/core/reminders.py +4 -5
- klaude_code/core/turn.py +1 -1
- klaude_code/protocol/commands.py +0 -1
- 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/components/command_output.py +1 -1
- klaude_code/tui/components/rich/markdown.py +60 -0
- klaude_code/tui/components/rich/theme.py +8 -0
- klaude_code/tui/components/user_input.py +38 -27
- klaude_code/tui/input/AGENTS.md +44 -0
- klaude_code/tui/input/completers.py +10 -14
- 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 +1 -1
- 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.6.0.dist-info → klaude_code-2.7.0.dist-info}/METADATA +10 -10
- {klaude_code-2.6.0.dist-info → klaude_code-2.7.0.dist-info}/RECORD +32 -30
- klaude_code/tui/command/terminal_setup_cmd.py +0 -248
- klaude_code/tui/input/clipboard.py +0 -152
- {klaude_code-2.6.0.dist-info → klaude_code-2.7.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.6.0.dist-info → klaude_code-2.7.0.dist-info}/entry_points.txt +0 -0
klaude_code/app/runtime.py
CHANGED
|
@@ -178,6 +178,6 @@ async def handle_keyboard_interrupt(executor: Executor) -> None:
|
|
|
178
178
|
log("Bye!")
|
|
179
179
|
session_id = executor.context.current_session_id()
|
|
180
180
|
if session_id and Session.exists(session_id):
|
|
181
|
-
log(("Resume with:", "dim"), (f"klaude --resume
|
|
181
|
+
log(("Resume with:", "dim"), (f"klaude --resume {session_id}", "green"))
|
|
182
182
|
with contextlib.suppress(Exception):
|
|
183
183
|
await executor.submit(op.InterruptOperation(target_session_id=None))
|
klaude_code/auth/env.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Environment variable configuration stored in klaude-auth.json."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, cast
|
|
5
5
|
|
|
6
6
|
from klaude_code.auth.base import KLAUDE_AUTH_FILE
|
|
7
7
|
|
|
@@ -11,9 +11,9 @@ def _load_store() -> dict[str, Any]:
|
|
|
11
11
|
if not KLAUDE_AUTH_FILE.exists():
|
|
12
12
|
return {}
|
|
13
13
|
try:
|
|
14
|
-
data
|
|
14
|
+
data = json.loads(KLAUDE_AUTH_FILE.read_text())
|
|
15
15
|
if isinstance(data, dict):
|
|
16
|
-
return dict
|
|
16
|
+
return cast(dict[str, Any], data)
|
|
17
17
|
return {}
|
|
18
18
|
except (json.JSONDecodeError, ValueError):
|
|
19
19
|
return {}
|
|
@@ -25,26 +25,31 @@ def _save_store(data: dict[str, Any]) -> None:
|
|
|
25
25
|
KLAUDE_AUTH_FILE.write_text(json.dumps(data, indent=2))
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _get_env_section(store: dict[str, Any]) -> dict[str, Any] | None:
|
|
29
|
+
"""Extract and validate the 'env' section from store."""
|
|
30
|
+
env_section = store.get("env")
|
|
31
|
+
if isinstance(env_section, dict):
|
|
32
|
+
return cast(dict[str, Any], env_section)
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
def get_auth_env(env_var: str) -> str | None:
|
|
29
37
|
"""Get environment variable value from klaude-auth.json 'env' section.
|
|
30
38
|
|
|
31
39
|
This provides a fallback for API keys when real environment variables are not set.
|
|
32
40
|
Priority: os.environ > klaude-auth.json env
|
|
33
41
|
"""
|
|
34
|
-
|
|
35
|
-
env_section
|
|
36
|
-
if not isinstance(env_section, dict):
|
|
42
|
+
env_section = _get_env_section(_load_store())
|
|
43
|
+
if env_section is None:
|
|
37
44
|
return None
|
|
38
|
-
value
|
|
45
|
+
value = env_section.get(env_var)
|
|
39
46
|
return str(value) if value is not None else None
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
def set_auth_env(env_var: str, value: str) -> None:
|
|
43
50
|
"""Set environment variable value in klaude-auth.json 'env' section."""
|
|
44
51
|
store = _load_store()
|
|
45
|
-
env_section
|
|
46
|
-
if not isinstance(env_section, dict):
|
|
47
|
-
env_section = {}
|
|
52
|
+
env_section = _get_env_section(store) or {}
|
|
48
53
|
env_section[env_var] = value
|
|
49
54
|
store["env"] = env_section
|
|
50
55
|
_save_store(store)
|
|
@@ -53,8 +58,8 @@ def set_auth_env(env_var: str, value: str) -> None:
|
|
|
53
58
|
def delete_auth_env(env_var: str) -> None:
|
|
54
59
|
"""Delete environment variable from klaude-auth.json 'env' section."""
|
|
55
60
|
store = _load_store()
|
|
56
|
-
env_section
|
|
57
|
-
if
|
|
61
|
+
env_section = _get_env_section(store)
|
|
62
|
+
if env_section is None:
|
|
58
63
|
return
|
|
59
64
|
env_section.pop(env_var, None)
|
|
60
65
|
if len(env_section) == 0:
|
|
@@ -70,8 +75,7 @@ def delete_auth_env(env_var: str) -> None:
|
|
|
70
75
|
|
|
71
76
|
def list_auth_env() -> dict[str, str]:
|
|
72
77
|
"""List all environment variables in klaude-auth.json 'env' section."""
|
|
73
|
-
|
|
74
|
-
env_section
|
|
75
|
-
if not isinstance(env_section, dict):
|
|
78
|
+
env_section = _get_env_section(_load_store())
|
|
79
|
+
if env_section is None:
|
|
76
80
|
return {}
|
|
77
81
|
return {k: str(v) for k, v in env_section.items() if v is not None}
|
klaude_code/cli/auth_cmd.py
CHANGED
|
@@ -212,7 +212,7 @@ def register_auth_commands(app: typer.Typer) -> None:
|
|
|
212
212
|
auth_app = typer.Typer(help="Login/logout", invoke_without_command=True)
|
|
213
213
|
|
|
214
214
|
@auth_app.callback()
|
|
215
|
-
def auth_callback(ctx: typer.Context) -> None:
|
|
215
|
+
def auth_callback(ctx: typer.Context) -> None: # pyright: ignore[reportUnusedFunction]
|
|
216
216
|
"""Authentication commands for managing provider logins."""
|
|
217
217
|
if ctx.invoked_subcommand is None:
|
|
218
218
|
typer.echo(ctx.get_help())
|
klaude_code/cli/main.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import sys
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any
|
|
3
5
|
|
|
4
6
|
import typer
|
|
7
|
+
from typer.core import TyperGroup
|
|
5
8
|
|
|
6
9
|
from klaude_code.cli.auth_cmd import register_auth_commands
|
|
7
10
|
from klaude_code.cli.config_cmd import register_config_commands
|
|
@@ -40,7 +43,88 @@ def _build_env_help() -> str:
|
|
|
40
43
|
|
|
41
44
|
ENV_HELP = _build_env_help()
|
|
42
45
|
|
|
46
|
+
|
|
47
|
+
def _looks_like_flag(token: str) -> bool:
|
|
48
|
+
return token.startswith("-") and token != "-"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _preprocess_cli_args(args: list[str]) -> list[str]:
|
|
52
|
+
"""Rewrite CLI args to support optional values for selected options.
|
|
53
|
+
|
|
54
|
+
Supported rewrites:
|
|
55
|
+
- --model / -m with no value -> --model-select
|
|
56
|
+
- --resume / -r with value -> --resume-by-id <value>
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
rewritten: list[str] = []
|
|
60
|
+
i = 0
|
|
61
|
+
while i < len(args):
|
|
62
|
+
token = args[i]
|
|
63
|
+
|
|
64
|
+
if token in {"--model", "-m"}:
|
|
65
|
+
next_token = args[i + 1] if i + 1 < len(args) else None
|
|
66
|
+
if next_token is None or next_token == "--" or _looks_like_flag(next_token):
|
|
67
|
+
rewritten.append("--model-select")
|
|
68
|
+
i += 1
|
|
69
|
+
continue
|
|
70
|
+
rewritten.append(token)
|
|
71
|
+
i += 1
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if token.startswith("--model="):
|
|
75
|
+
value = token.split("=", 1)[1]
|
|
76
|
+
if value == "":
|
|
77
|
+
rewritten.append("--model-select")
|
|
78
|
+
else:
|
|
79
|
+
rewritten.append(token)
|
|
80
|
+
i += 1
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if token in {"--resume", "-r"}:
|
|
84
|
+
next_token = args[i + 1] if i + 1 < len(args) else None
|
|
85
|
+
if next_token is not None and next_token != "--" and not _looks_like_flag(next_token):
|
|
86
|
+
rewritten.extend(["--resume-by-id", next_token])
|
|
87
|
+
i += 2
|
|
88
|
+
continue
|
|
89
|
+
rewritten.append(token)
|
|
90
|
+
i += 1
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
if token.startswith("--resume="):
|
|
94
|
+
value = token.split("=", 1)[1]
|
|
95
|
+
rewritten.extend(["--resume-by-id", value])
|
|
96
|
+
i += 1
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
rewritten.append(token)
|
|
100
|
+
i += 1
|
|
101
|
+
|
|
102
|
+
return rewritten
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class _PreprocessingTyperGroup(TyperGroup):
|
|
106
|
+
def main(
|
|
107
|
+
self,
|
|
108
|
+
args: Sequence[str] | None = None,
|
|
109
|
+
prog_name: str | None = None,
|
|
110
|
+
complete_var: str | None = None,
|
|
111
|
+
standalone_mode: bool = True,
|
|
112
|
+
windows_expand_args: bool = True,
|
|
113
|
+
**extra: Any,
|
|
114
|
+
) -> Any:
|
|
115
|
+
click_args = _preprocess_cli_args(list(args) if args is not None else sys.argv[1:])
|
|
116
|
+
return super().main(
|
|
117
|
+
args=click_args,
|
|
118
|
+
prog_name=prog_name,
|
|
119
|
+
complete_var=complete_var,
|
|
120
|
+
standalone_mode=standalone_mode,
|
|
121
|
+
windows_expand_args=windows_expand_args,
|
|
122
|
+
**extra,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
43
126
|
app = typer.Typer(
|
|
127
|
+
cls=_PreprocessingTyperGroup,
|
|
44
128
|
add_completion=False,
|
|
45
129
|
pretty_exceptions_enable=False,
|
|
46
130
|
no_args_is_help=False,
|
|
@@ -69,21 +153,27 @@ def main_callback(
|
|
|
69
153
|
None,
|
|
70
154
|
"--model",
|
|
71
155
|
"-m",
|
|
72
|
-
help="Select model by name",
|
|
156
|
+
help="Select model by name; use --model with no value to choose interactively",
|
|
73
157
|
rich_help_panel="LLM",
|
|
74
158
|
),
|
|
75
159
|
continue_: bool = typer.Option(False, "--continue", "-c", help="Resume latest session"),
|
|
76
|
-
resume: bool = typer.Option(
|
|
160
|
+
resume: bool = typer.Option(
|
|
161
|
+
False,
|
|
162
|
+
"--resume",
|
|
163
|
+
"-r",
|
|
164
|
+
help="Resume a session; use --resume <id> to resume directly, or --resume to pick interactively",
|
|
165
|
+
),
|
|
77
166
|
resume_by_id: str | None = typer.Option(
|
|
78
167
|
None,
|
|
79
168
|
"--resume-by-id",
|
|
80
169
|
help="Resume session by ID",
|
|
170
|
+
hidden=True,
|
|
81
171
|
),
|
|
82
172
|
select_model: bool = typer.Option(
|
|
83
173
|
False,
|
|
84
|
-
"--select
|
|
85
|
-
"
|
|
86
|
-
|
|
174
|
+
"--model-select",
|
|
175
|
+
help="Choose model interactively (same as --model with no value)",
|
|
176
|
+
hidden=True,
|
|
87
177
|
rich_help_panel="LLM",
|
|
88
178
|
),
|
|
89
179
|
debug: bool = typer.Option(
|
|
@@ -107,7 +197,7 @@ def main_callback(
|
|
|
107
197
|
banana: bool = typer.Option(
|
|
108
198
|
False,
|
|
109
199
|
"--banana",
|
|
110
|
-
help="Image generation mode",
|
|
200
|
+
help="Image generation mode (alias for --model banana)",
|
|
111
201
|
rich_help_panel="LLM",
|
|
112
202
|
),
|
|
113
203
|
version: bool = typer.Option(
|
|
@@ -130,11 +220,11 @@ def main_callback(
|
|
|
130
220
|
|
|
131
221
|
resume_by_id_value = resume_by_id.strip() if resume_by_id is not None else None
|
|
132
222
|
if resume_by_id_value == "":
|
|
133
|
-
log(("Error: --resume
|
|
223
|
+
log(("Error: --resume <id> cannot be empty", "red"))
|
|
134
224
|
raise typer.Exit(2)
|
|
135
225
|
|
|
136
226
|
if resume_by_id_value is not None and (resume or continue_):
|
|
137
|
-
log(("Error: --resume
|
|
227
|
+
log(("Error: --resume <id> cannot be combined with --continue or interactive --resume", "red"))
|
|
138
228
|
raise typer.Exit(2)
|
|
139
229
|
|
|
140
230
|
if resume_by_id_value is not None and not Session.exists(resume_by_id_value):
|
klaude_code/const.py
CHANGED
|
@@ -7,6 +7,8 @@ that were previously scattered across the codebase.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
10
12
|
from dataclasses import dataclass
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
|
|
@@ -22,6 +24,13 @@ def _get_int_env(name: str, default: int) -> int:
|
|
|
22
24
|
return default
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
def get_system_temp() -> str:
|
|
28
|
+
"""Return system-level temp directory: /tmp on Unix, system temp on Windows."""
|
|
29
|
+
if sys.platform == "win32":
|
|
30
|
+
return tempfile.gettempdir()
|
|
31
|
+
return "/tmp"
|
|
32
|
+
|
|
33
|
+
|
|
25
34
|
# =============================================================================
|
|
26
35
|
# Agent / LLM Configuration
|
|
27
36
|
# =============================================================================
|
|
@@ -115,7 +124,7 @@ TOOL_OUTPUT_DISPLAY_TAIL = 10000 # Characters to show from the end of truncated
|
|
|
115
124
|
TOOL_OUTPUT_MAX_LINES = 2000 # Maximum lines for tool output before truncation
|
|
116
125
|
TOOL_OUTPUT_DISPLAY_HEAD_LINES = 1000 # Lines to show from the beginning of truncated output
|
|
117
126
|
TOOL_OUTPUT_DISPLAY_TAIL_LINES = 1000 # Lines to show from the end of truncated output
|
|
118
|
-
TOOL_OUTPUT_TRUNCATION_DIR =
|
|
127
|
+
TOOL_OUTPUT_TRUNCATION_DIR = get_system_temp() # Directory for saving full truncated output
|
|
119
128
|
|
|
120
129
|
|
|
121
130
|
# =============================================================================
|
klaude_code/core/reminders.py
CHANGED
|
@@ -17,8 +17,8 @@ from klaude_code.skill import get_skill
|
|
|
17
17
|
# Match @ preceded by whitespace, start of line, or → (ReadTool line number arrow)
|
|
18
18
|
AT_FILE_PATTERN = re.compile(r'(?:(?<!\S)|(?<=\u2192))@("(?P<quoted>[^\"]+)"|(?P<plain>\S+))')
|
|
19
19
|
|
|
20
|
-
# Match $skill or ¥skill at
|
|
21
|
-
SKILL_PATTERN = re.compile(r"
|
|
20
|
+
# Match $skill or ¥skill inline (at start of line or after whitespace)
|
|
21
|
+
SKILL_PATTERN = re.compile(r"(?:^|\s)[$¥](?P<skill>\S+)")
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def get_last_new_user_input(session: Session) -> str | None:
|
|
@@ -79,14 +79,13 @@ def get_at_patterns_with_source(session: Session) -> list[AtPatternSource]:
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def get_skill_from_user_input(session: Session) -> str | None:
|
|
82
|
-
"""Get $skill reference from
|
|
82
|
+
"""Get $skill reference from last user input (first match wins)."""
|
|
83
83
|
for item in reversed(session.conversation_history):
|
|
84
84
|
if isinstance(item, message.ToolResultMessage):
|
|
85
85
|
return None
|
|
86
86
|
if isinstance(item, message.UserMessage):
|
|
87
87
|
content = message.join_text_parts(item.parts)
|
|
88
|
-
|
|
89
|
-
m = SKILL_PATTERN.match(first_line)
|
|
88
|
+
m = SKILL_PATTERN.search(content)
|
|
90
89
|
if m:
|
|
91
90
|
return m.group("skill")
|
|
92
91
|
return None
|
klaude_code/core/turn.py
CHANGED
|
@@ -196,7 +196,7 @@ class TurnExecutor:
|
|
|
196
196
|
):
|
|
197
197
|
session_ctx.append_history([self._turn_result.assistant_message])
|
|
198
198
|
# Add continuation prompt to avoid Anthropic thinking block requirement
|
|
199
|
-
session_ctx.append_history([message.UserMessage(parts=[message.TextPart(text="continue")])])
|
|
199
|
+
session_ctx.append_history([message.UserMessage(parts=[message.TextPart(text="<system>continue</system>")])])
|
|
200
200
|
yield events.TurnEndEvent(session_id=session_ctx.session_id)
|
|
201
201
|
raise TurnError(self._turn_result.stream_error.error)
|
|
202
202
|
|
klaude_code/protocol/commands.py
CHANGED
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(
|
|
@@ -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
|
|
|
@@ -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,
|