klaude-code 1.4.3__py3-none-any.whl → 1.5.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/cli/main.py +75 -11
- klaude_code/cli/runtime.py +171 -34
- klaude_code/command/__init__.py +4 -0
- klaude_code/command/help_cmd.py +2 -1
- klaude_code/command/model_cmd.py +3 -5
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/registry.py +23 -0
- klaude_code/command/resume_cmd.py +52 -2
- klaude_code/command/thinking_cmd.py +30 -199
- klaude_code/config/select_model.py +47 -97
- klaude_code/config/thinking.py +255 -0
- klaude_code/core/executor.py +53 -63
- klaude_code/protocol/commands.py +11 -0
- klaude_code/protocol/op.py +15 -0
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/selector.py +33 -61
- klaude_code/ui/modes/repl/completers.py +27 -15
- klaude_code/ui/modes/repl/event_handler.py +2 -1
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +393 -57
- klaude_code/ui/modes/repl/key_bindings.py +30 -10
- klaude_code/ui/renderers/metadata.py +3 -6
- klaude_code/ui/renderers/user_input.py +18 -1
- klaude_code/ui/rich/theme.py +2 -2
- klaude_code/ui/terminal/notifier.py +42 -0
- klaude_code/ui/terminal/selector.py +419 -136
- {klaude_code-1.4.3.dist-info → klaude_code-1.5.0.dist-info}/METADATA +1 -1
- {klaude_code-1.4.3.dist-info → klaude_code-1.5.0.dist-info}/RECORD +29 -27
- {klaude_code-1.4.3.dist-info → klaude_code-1.5.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.4.3.dist-info → klaude_code-1.5.0.dist-info}/entry_points.txt +0 -0
|
@@ -12,6 +12,7 @@ from collections.abc import Callable
|
|
|
12
12
|
from typing import cast
|
|
13
13
|
|
|
14
14
|
from prompt_toolkit.buffer import Buffer
|
|
15
|
+
from prompt_toolkit.filters import Always, Filter
|
|
15
16
|
from prompt_toolkit.filters.app import has_completions
|
|
16
17
|
from prompt_toolkit.key_binding import KeyBindings
|
|
17
18
|
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
|
@@ -21,6 +22,10 @@ def create_key_bindings(
|
|
|
21
22
|
capture_clipboard_tag: Callable[[], str | None],
|
|
22
23
|
copy_to_clipboard: Callable[[str], None],
|
|
23
24
|
at_token_pattern: re.Pattern[str],
|
|
25
|
+
*,
|
|
26
|
+
input_enabled: Filter | None = None,
|
|
27
|
+
open_model_picker: Callable[[], None] | None = None,
|
|
28
|
+
open_thinking_picker: Callable[[], None] | None = None,
|
|
24
29
|
) -> KeyBindings:
|
|
25
30
|
"""Create REPL key bindings with injected dependencies.
|
|
26
31
|
|
|
@@ -33,6 +38,7 @@ def create_key_bindings(
|
|
|
33
38
|
KeyBindings instance with all REPL handlers configured
|
|
34
39
|
"""
|
|
35
40
|
kb = KeyBindings()
|
|
41
|
+
enabled = input_enabled if input_enabled is not None else Always()
|
|
36
42
|
|
|
37
43
|
def _should_submit_instead_of_accepting_completion(buf: Buffer) -> bool:
|
|
38
44
|
"""Return True when Enter should submit even if completions are visible.
|
|
@@ -111,7 +117,7 @@ def create_key_bindings(
|
|
|
111
117
|
buf.apply_completion(completion)
|
|
112
118
|
return True
|
|
113
119
|
|
|
114
|
-
@kb.add("c-v")
|
|
120
|
+
@kb.add("c-v", filter=enabled)
|
|
115
121
|
def _(event: KeyPressEvent) -> None:
|
|
116
122
|
"""Paste image from clipboard as [Image #N]."""
|
|
117
123
|
tag = capture_clipboard_tag()
|
|
@@ -119,7 +125,7 @@ def create_key_bindings(
|
|
|
119
125
|
with contextlib.suppress(Exception):
|
|
120
126
|
event.current_buffer.insert_text(tag) # pyright: ignore[reportUnknownMemberType]
|
|
121
127
|
|
|
122
|
-
@kb.add("enter")
|
|
128
|
+
@kb.add("enter", filter=enabled)
|
|
123
129
|
def _(event: KeyPressEvent) -> None:
|
|
124
130
|
buf = event.current_buffer
|
|
125
131
|
doc = buf.document # type: ignore
|
|
@@ -150,29 +156,29 @@ def create_key_bindings(
|
|
|
150
156
|
# No need to persist manifest anymore - iter_inputs will handle image extraction
|
|
151
157
|
buf.validate_and_handle() # type: ignore
|
|
152
158
|
|
|
153
|
-
@kb.add("tab", filter=has_completions)
|
|
159
|
+
@kb.add("tab", filter=enabled & has_completions)
|
|
154
160
|
def _(event: KeyPressEvent) -> None:
|
|
155
161
|
buf = event.current_buffer
|
|
156
162
|
if _accept_current_completion(buf):
|
|
157
163
|
event.app.invalidate() # type: ignore[reportUnknownMemberType]
|
|
158
164
|
|
|
159
|
-
@kb.add("down", filter=has_completions)
|
|
165
|
+
@kb.add("down", filter=enabled & has_completions)
|
|
160
166
|
def _(event: KeyPressEvent) -> None:
|
|
161
167
|
buf = event.current_buffer
|
|
162
168
|
_cycle_completion(buf, delta=1)
|
|
163
169
|
event.app.invalidate() # type: ignore[reportUnknownMemberType]
|
|
164
170
|
|
|
165
|
-
@kb.add("up", filter=has_completions)
|
|
171
|
+
@kb.add("up", filter=enabled & has_completions)
|
|
166
172
|
def _(event: KeyPressEvent) -> None:
|
|
167
173
|
buf = event.current_buffer
|
|
168
174
|
_cycle_completion(buf, delta=-1)
|
|
169
175
|
event.app.invalidate() # type: ignore[reportUnknownMemberType]
|
|
170
176
|
|
|
171
|
-
@kb.add("c-j")
|
|
177
|
+
@kb.add("c-j", filter=enabled)
|
|
172
178
|
def _(event: KeyPressEvent) -> None:
|
|
173
179
|
event.current_buffer.insert_text("\n") # type: ignore
|
|
174
180
|
|
|
175
|
-
@kb.add("c")
|
|
181
|
+
@kb.add("c", filter=enabled)
|
|
176
182
|
def _(event: KeyPressEvent) -> None:
|
|
177
183
|
"""Copy selected text to system clipboard, or insert 'c' if no selection."""
|
|
178
184
|
buf = event.current_buffer # type: ignore
|
|
@@ -187,7 +193,7 @@ def create_key_bindings(
|
|
|
187
193
|
else:
|
|
188
194
|
buf.insert_text("c") # type: ignore[reportUnknownMemberType]
|
|
189
195
|
|
|
190
|
-
@kb.add("backspace")
|
|
196
|
+
@kb.add("backspace", filter=enabled)
|
|
191
197
|
def _(event: KeyPressEvent) -> None:
|
|
192
198
|
"""Ensure completions refresh on backspace when editing an @token.
|
|
193
199
|
|
|
@@ -218,7 +224,7 @@ def create_key_bindings(
|
|
|
218
224
|
except (AttributeError, TypeError):
|
|
219
225
|
pass
|
|
220
226
|
|
|
221
|
-
@kb.add("left")
|
|
227
|
+
@kb.add("left", filter=enabled)
|
|
222
228
|
def _(event: KeyPressEvent) -> None:
|
|
223
229
|
"""Support wrapping to previous line when pressing left at column 0."""
|
|
224
230
|
buf = event.current_buffer # type: ignore
|
|
@@ -243,7 +249,7 @@ def create_key_bindings(
|
|
|
243
249
|
except (AttributeError, IndexError, TypeError):
|
|
244
250
|
pass
|
|
245
251
|
|
|
246
|
-
@kb.add("right")
|
|
252
|
+
@kb.add("right", filter=enabled)
|
|
247
253
|
def _(event: KeyPressEvent) -> None:
|
|
248
254
|
"""Support wrapping to next line when pressing right at line end."""
|
|
249
255
|
buf = event.current_buffer # type: ignore
|
|
@@ -270,4 +276,18 @@ def create_key_bindings(
|
|
|
270
276
|
except (AttributeError, IndexError, TypeError):
|
|
271
277
|
pass
|
|
272
278
|
|
|
279
|
+
@kb.add("c-l", filter=enabled, eager=True)
|
|
280
|
+
def _(event: KeyPressEvent) -> None:
|
|
281
|
+
del event
|
|
282
|
+
if open_model_picker is not None:
|
|
283
|
+
with contextlib.suppress(Exception):
|
|
284
|
+
open_model_picker()
|
|
285
|
+
|
|
286
|
+
@kb.add("c-t", filter=enabled, eager=True)
|
|
287
|
+
def _(event: KeyPressEvent) -> None:
|
|
288
|
+
del event
|
|
289
|
+
if open_thinking_picker is not None:
|
|
290
|
+
with contextlib.suppress(Exception):
|
|
291
|
+
open_thinking_picker()
|
|
292
|
+
|
|
273
293
|
return kb
|
|
@@ -148,18 +148,15 @@ def _render_task_metadata_block(
|
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
151
|
-
"""Render task metadata including main agent and sub-agents
|
|
151
|
+
"""Render task metadata including main agent and sub-agents."""
|
|
152
152
|
renderables: list[RenderableType] = []
|
|
153
153
|
|
|
154
154
|
renderables.append(
|
|
155
155
|
_render_task_metadata_block(e.metadata.main_agent, is_sub_agent=False, show_context_and_time=True)
|
|
156
156
|
)
|
|
157
157
|
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
# Render each aggregated model block
|
|
162
|
-
for meta in sorted_items:
|
|
158
|
+
# Render each sub-agent metadata block
|
|
159
|
+
for meta in e.metadata.sub_agent_task_metadata:
|
|
163
160
|
renderables.append(_render_task_metadata_block(meta, is_sub_agent=True, show_context_and_time=False))
|
|
164
161
|
|
|
165
162
|
return Group(*renderables)
|
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from collections.abc import Callable
|
|
2
3
|
|
|
3
4
|
from rich.console import Group, RenderableType
|
|
4
5
|
from rich.text import Text
|
|
5
6
|
|
|
6
|
-
from klaude_code.command import is_slash_command_name
|
|
7
7
|
from klaude_code.skill import get_available_skills
|
|
8
8
|
from klaude_code.ui.renderers.common import create_grid
|
|
9
9
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
10
10
|
|
|
11
|
+
# Module-level command name checker. Set by cli/runtime.py on startup.
|
|
12
|
+
_command_name_checker: Callable[[str], bool] | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def set_command_name_checker(checker: Callable[[str], bool]) -> None:
|
|
16
|
+
"""Set the command name validation function (called from runtime layer)."""
|
|
17
|
+
global _command_name_checker
|
|
18
|
+
_command_name_checker = checker
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_slash_command_name(name: str) -> bool:
|
|
22
|
+
"""Check if name is a valid slash command using the injected checker."""
|
|
23
|
+
if _command_name_checker is None:
|
|
24
|
+
return False
|
|
25
|
+
return _command_name_checker(name)
|
|
26
|
+
|
|
27
|
+
|
|
11
28
|
# Match @-file patterns only when they appear at the beginning of the line
|
|
12
29
|
# or immediately after whitespace, to avoid treating mid-word email-like
|
|
13
30
|
# patterns such as foo@bar.com as file references.
|
klaude_code/ui/rich/theme.py
CHANGED
|
@@ -44,7 +44,7 @@ LIGHT_PALETTE = Palette(
|
|
|
44
44
|
red="red",
|
|
45
45
|
yellow="yellow",
|
|
46
46
|
green="#00875f",
|
|
47
|
-
grey_yellow="#
|
|
47
|
+
grey_yellow="#5f9f7a",
|
|
48
48
|
cyan="cyan",
|
|
49
49
|
blue="#3078C5",
|
|
50
50
|
orange="#d77757",
|
|
@@ -77,7 +77,7 @@ DARK_PALETTE = Palette(
|
|
|
77
77
|
red="#d75f5f",
|
|
78
78
|
yellow="yellow",
|
|
79
79
|
green="#5fd787",
|
|
80
|
-
grey_yellow="#
|
|
80
|
+
grey_yellow="#8ac89a",
|
|
81
81
|
cyan="cyan",
|
|
82
82
|
blue="#00afff",
|
|
83
83
|
orange="#e6704e",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import subprocess
|
|
4
5
|
import sys
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from enum import Enum
|
|
@@ -8,6 +9,9 @@ from typing import TextIO, cast
|
|
|
8
9
|
|
|
9
10
|
from klaude_code.trace import DebugType, log_debug
|
|
10
11
|
|
|
12
|
+
# Environment variable for tmux test signal channel
|
|
13
|
+
TMUX_SIGNAL_ENV = "KLAUDE_TEST_SIGNAL"
|
|
14
|
+
|
|
11
15
|
ST = "\033\\"
|
|
12
16
|
BEL = "\a"
|
|
13
17
|
|
|
@@ -103,3 +107,41 @@ def _compact(text: str, limit: int = 160) -> str:
|
|
|
103
107
|
if len(squashed) > limit:
|
|
104
108
|
return squashed[: limit - 3] + "…"
|
|
105
109
|
return squashed
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def emit_tmux_signal(channel: str | None = None) -> bool:
|
|
113
|
+
"""Send a tmux wait-for signal when a task completes.
|
|
114
|
+
|
|
115
|
+
This enables synchronous testing by allowing external scripts to block
|
|
116
|
+
until a task finishes, eliminating the need for polling or sleep.
|
|
117
|
+
|
|
118
|
+
Usage:
|
|
119
|
+
KLAUDE_TEST_SIGNAL=done klaude # In tmux session
|
|
120
|
+
tmux wait-for done # Blocks until task completes
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
channel: Signal channel name. If None, reads from KLAUDE_TEST_SIGNAL env var.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if signal was sent successfully, False otherwise.
|
|
127
|
+
"""
|
|
128
|
+
channel = channel or os.getenv(TMUX_SIGNAL_ENV)
|
|
129
|
+
if not channel:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
# Check if we're in a tmux session
|
|
133
|
+
if not os.getenv("TMUX"):
|
|
134
|
+
log_debug("tmux signal skipped: not in tmux session", debug_type=DebugType.TERMINAL)
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
subprocess.run(
|
|
139
|
+
["tmux", "wait-for", "-S", channel],
|
|
140
|
+
check=True,
|
|
141
|
+
capture_output=True,
|
|
142
|
+
)
|
|
143
|
+
log_debug(f"tmux signal sent: {channel}", debug_type=DebugType.TERMINAL)
|
|
144
|
+
return True
|
|
145
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
146
|
+
log_debug(f"tmux signal failed: {e}", debug_type=DebugType.TERMINAL)
|
|
147
|
+
return False
|