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.
@@ -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, aggregated by model+provider."""
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
- # Aggregate by (model_name, provider), sorted by total_cost descending
159
- sorted_items = model.TaskMetadata.aggregate_by_model(e.metadata.sub_agent_task_metadata)
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.
@@ -44,7 +44,7 @@ LIGHT_PALETTE = Palette(
44
44
  red="red",
45
45
  yellow="yellow",
46
46
  green="#00875f",
47
- grey_yellow="#a89a85",
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="#c0b095",
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