klaude-code 1.2.12__py3-none-any.whl → 1.2.13__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.
Files changed (74) hide show
  1. klaude_code/auth/codex/oauth.py +3 -3
  2. klaude_code/cli/main.py +5 -5
  3. klaude_code/cli/runtime.py +19 -27
  4. klaude_code/cli/session_cmd.py +6 -8
  5. klaude_code/command/__init__.py +6 -6
  6. klaude_code/command/export_cmd.py +3 -3
  7. klaude_code/command/registry.py +1 -1
  8. klaude_code/command/terminal_setup_cmd.py +2 -2
  9. klaude_code/command/thinking_cmd.py +8 -6
  10. klaude_code/config/__init__.py +1 -1
  11. klaude_code/config/list_model.py +1 -1
  12. klaude_code/core/agent.py +13 -61
  13. klaude_code/core/executor.py +11 -10
  14. klaude_code/core/manager/agent_manager.py +4 -4
  15. klaude_code/core/manager/llm_clients.py +10 -49
  16. klaude_code/core/manager/llm_clients_builder.py +8 -21
  17. klaude_code/core/manager/sub_agent_manager.py +3 -3
  18. klaude_code/core/prompt.py +2 -2
  19. klaude_code/core/reminders.py +1 -1
  20. klaude_code/core/task.py +2 -2
  21. klaude_code/core/tool/__init__.py +16 -25
  22. klaude_code/core/tool/file/_utils.py +1 -1
  23. klaude_code/core/tool/file/apply_patch.py +17 -25
  24. klaude_code/core/tool/file/apply_patch_tool.py +4 -7
  25. klaude_code/core/tool/file/edit_tool.py +4 -11
  26. klaude_code/core/tool/file/multi_edit_tool.py +2 -3
  27. klaude_code/core/tool/file/read_tool.py +3 -4
  28. klaude_code/core/tool/file/write_tool.py +2 -3
  29. klaude_code/core/tool/memory/memory_tool.py +2 -8
  30. klaude_code/core/tool/memory/skill_loader.py +3 -2
  31. klaude_code/core/tool/shell/command_safety.py +0 -1
  32. klaude_code/core/tool/tool_context.py +1 -3
  33. klaude_code/core/tool/tool_registry.py +2 -1
  34. klaude_code/core/tool/tool_runner.py +1 -1
  35. klaude_code/core/tool/truncation.py +2 -5
  36. klaude_code/core/turn.py +9 -3
  37. klaude_code/llm/anthropic/client.py +6 -2
  38. klaude_code/llm/client.py +1 -1
  39. klaude_code/llm/codex/client.py +2 -2
  40. klaude_code/llm/input_common.py +2 -2
  41. klaude_code/llm/openai_compatible/client.py +11 -8
  42. klaude_code/llm/openai_compatible/stream_processor.py +2 -1
  43. klaude_code/llm/openrouter/client.py +20 -8
  44. klaude_code/llm/openrouter/reasoning_handler.py +19 -132
  45. klaude_code/llm/registry.py +6 -5
  46. klaude_code/llm/responses/client.py +10 -5
  47. klaude_code/protocol/events.py +7 -0
  48. klaude_code/protocol/model.py +7 -1
  49. klaude_code/protocol/sub_agent.py +2 -1
  50. klaude_code/session/selector.py +2 -2
  51. klaude_code/session/session.py +2 -4
  52. klaude_code/trace/__init__.py +1 -1
  53. klaude_code/trace/log.py +1 -1
  54. klaude_code/ui/__init__.py +4 -9
  55. klaude_code/ui/core/stage_manager.py +7 -4
  56. klaude_code/ui/modes/repl/__init__.py +1 -1
  57. klaude_code/ui/modes/repl/completers.py +3 -4
  58. klaude_code/ui/modes/repl/display.py +3 -4
  59. klaude_code/ui/modes/repl/event_handler.py +63 -5
  60. klaude_code/ui/modes/repl/key_bindings.py +2 -3
  61. klaude_code/ui/modes/repl/renderer.py +2 -1
  62. klaude_code/ui/renderers/diffs.py +1 -4
  63. klaude_code/ui/rich/markdown.py +3 -3
  64. klaude_code/ui/rich/searchable_text.py +6 -6
  65. klaude_code/ui/rich/status.py +3 -4
  66. klaude_code/ui/rich/theme.py +1 -4
  67. klaude_code/ui/terminal/control.py +7 -16
  68. klaude_code/ui/terminal/notifier.py +2 -4
  69. klaude_code/ui/utils/common.py +1 -1
  70. klaude_code/ui/utils/debouncer.py +2 -2
  71. {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/METADATA +1 -1
  72. {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/RECORD +74 -74
  73. {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/WHEEL +0 -0
  74. {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Awaitable, Callable
3
4
  from dataclasses import dataclass
4
- from typing import Awaitable, Callable
5
5
 
6
6
  from rich.text import Text
7
7
 
@@ -10,6 +10,7 @@ from klaude_code.protocol import events
10
10
  from klaude_code.ui.core.stage_manager import Stage, StageManager
11
11
  from klaude_code.ui.modes.repl.renderer import REPLRenderer
12
12
  from klaude_code.ui.rich.markdown import MarkdownStream
13
+ from klaude_code.ui.rich.theme import ThemeKey
13
14
  from klaude_code.ui.terminal.notifier import Notification, NotificationType, TerminalNotifier
14
15
  from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
15
16
  from klaude_code.ui.utils.debouncer import Debouncer
@@ -41,7 +42,7 @@ class StreamState:
41
42
  This design ensures buffer and mdstream are always in sync.
42
43
  """
43
44
 
44
- def __init__(self, interval: float, flush_handler: Callable[["StreamState"], Awaitable[None]]):
45
+ def __init__(self, interval: float, flush_handler: Callable[[StreamState], Awaitable[None]]):
45
46
  self._active: ActiveStream | None = None
46
47
  self._flush_handler = flush_handler
47
48
  self.debouncer = Debouncer(interval=interval, callback=self._debounced_flush)
@@ -199,10 +200,14 @@ class DisplayEventHandler:
199
200
  self.assistant_stream = StreamState(
200
201
  interval=1 / const.UI_REFRESH_RATE_FPS, flush_handler=self._flush_assistant_buffer
201
202
  )
203
+ self.thinking_stream = StreamState(
204
+ interval=1 / const.UI_REFRESH_RATE_FPS, flush_handler=self._flush_thinking_buffer
205
+ )
202
206
  self.spinner_status = SpinnerStatusState()
203
207
 
204
208
  self.stage_manager = StageManager(
205
209
  finish_assistant=self._finish_assistant_stream,
210
+ finish_thinking=self._finish_thinking_stream,
206
211
  on_enter_thinking=self._print_thinking_prefix,
207
212
  )
208
213
 
@@ -222,6 +227,8 @@ class DisplayEventHandler:
222
227
  self._on_turn_start(e)
223
228
  case events.ThinkingEvent() as e:
224
229
  await self._on_thinking(e)
230
+ case events.ThinkingDeltaEvent() as e:
231
+ await self._on_thinking_delta(e)
225
232
  case events.AssistantMessageDeltaEvent() as e:
226
233
  await self._on_assistant_delta(e)
227
234
  case events.AssistantMessageEvent() as e:
@@ -252,6 +259,8 @@ class DisplayEventHandler:
252
259
  async def stop(self) -> None:
253
260
  await self.assistant_stream.debouncer.flush()
254
261
  self.assistant_stream.debouncer.cancel()
262
+ await self.thinking_stream.debouncer.flush()
263
+ self.thinking_stream.debouncer.cancel()
255
264
 
256
265
  # ─────────────────────────────────────────────────────────────────────────────
257
266
  # Private event handlers
@@ -285,8 +294,41 @@ class DisplayEventHandler:
285
294
  async def _on_thinking(self, event: events.ThinkingEvent) -> None:
286
295
  if self.renderer.is_sub_agent_session(event.session_id):
287
296
  return
297
+ # If streaming was active, finalize it
298
+ if self.thinking_stream.is_active:
299
+ await self._finish_thinking_stream()
300
+ else:
301
+ # Non-streaming path (history replay or models without delta support)
302
+ await self.stage_manager.enter_thinking_stage()
303
+ self.renderer.display_thinking(event.content)
304
+
305
+ async def _on_thinking_delta(self, event: events.ThinkingDeltaEvent) -> None:
306
+ if self.renderer.is_sub_agent_session(event.session_id):
307
+ return
308
+
309
+ first_delta = not self.thinking_stream.is_active
310
+ if first_delta:
311
+ self.renderer.console.push_theme(self.renderer.themes.thinking_markdown_theme)
312
+ mdstream = MarkdownStream(
313
+ mdargs={
314
+ "code_theme": self.renderer.themes.code_theme,
315
+ "style": self.renderer.console.get_style(ThemeKey.THINKING),
316
+ },
317
+ theme=self.renderer.themes.thinking_markdown_theme,
318
+ console=self.renderer.console,
319
+ spinner=self.renderer.spinner_renderable(),
320
+ indent=2,
321
+ )
322
+ self.thinking_stream.start(mdstream)
323
+ self.renderer.spinner_stop()
324
+
325
+ self.thinking_stream.append(event.content)
326
+
327
+ if first_delta and self.thinking_stream.mdstream is not None:
328
+ self.thinking_stream.mdstream.update(self.thinking_stream.buffer)
329
+
288
330
  await self.stage_manager.enter_thinking_stage()
289
- self.renderer.display_thinking(event.content)
331
+ self.thinking_stream.debouncer.schedule()
290
332
 
291
333
  async def _on_assistant_delta(self, event: events.AssistantMessageDeltaEvent) -> None:
292
334
  if self.renderer.is_sub_agent_session(event.session_id):
@@ -419,6 +461,22 @@ class DisplayEventHandler:
419
461
  assert mdstream is not None
420
462
  mdstream.update(state.buffer)
421
463
 
464
+ async def _flush_thinking_buffer(self, state: StreamState) -> None:
465
+ if state.is_active:
466
+ mdstream = state.mdstream
467
+ assert mdstream is not None
468
+ mdstream.update(state.buffer)
469
+
470
+ async def _finish_thinking_stream(self) -> None:
471
+ if self.thinking_stream.is_active:
472
+ self.thinking_stream.debouncer.cancel()
473
+ mdstream = self.thinking_stream.mdstream
474
+ assert mdstream is not None
475
+ mdstream.update(self.thinking_stream.buffer, final=True)
476
+ self.thinking_stream.finish()
477
+ self.renderer.console.pop_theme()
478
+ self.renderer.spinner_start()
479
+
422
480
  def _maybe_notify_task_finish(self, event: events.TaskFinishEvent) -> None:
423
481
  if self.notifier is None:
424
482
  return
@@ -453,10 +511,10 @@ class DisplayEventHandler:
453
511
  if len(todo.content) > 0:
454
512
  status_text = todo.content
455
513
  status_text = status_text.replace("\n", "")
456
- return self._truncate_status_text(status_text, max_length=100)
514
+ return self._truncate_status_text(status_text, max_length=50)
457
515
 
458
516
  def _truncate_status_text(self, text: str, max_length: int) -> str:
459
517
  if len(text) <= max_length:
460
518
  return text
461
519
  truncated = text[:max_length]
462
- return truncated + "..."
520
+ return truncated + ""
@@ -6,6 +6,7 @@ with dependencies injected to avoid circular imports.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import contextlib
9
10
  import re
10
11
  from collections.abc import Callable
11
12
  from typing import cast
@@ -35,10 +36,8 @@ def create_key_bindings(
35
36
  """Paste image from clipboard as [Image #N]."""
36
37
  tag = capture_clipboard_tag()
37
38
  if tag:
38
- try:
39
+ with contextlib.suppress(Exception):
39
40
  event.current_buffer.insert_text(tag) # pyright: ignore[reportUnknownMemberType]
40
- except Exception:
41
- pass
42
41
 
43
42
  @kb.add("enter")
44
43
  def _(event): # type: ignore
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Iterator
3
4
  from contextlib import contextmanager
4
5
  from dataclasses import dataclass
5
- from typing import Any, Iterator
6
+ from typing import Any
6
7
 
7
8
  from rich import box
8
9
  from rich.box import Box
@@ -73,10 +73,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
73
73
  if line.startswith("--- "):
74
74
  raw = line[4:].strip()
75
75
  if raw != "/dev/null":
76
- if raw.startswith(("a/", "b/")):
77
- from_file_name = raw[2:]
78
- else:
79
- from_file_name = raw
76
+ from_file_name = raw[2:] if raw.startswith(("a/", "b/")) else raw
80
77
  continue
81
78
 
82
79
  # Parse file name from diff headers
@@ -1,6 +1,7 @@
1
1
  # copy from https://github.com/Aider-AI/aider/blob/main/aider/mdstream.py
2
2
  from __future__ import annotations
3
3
 
4
+ import contextlib
4
5
  import io
5
6
  import time
6
7
  from typing import Any, ClassVar
@@ -183,10 +184,9 @@ class MarkdownStream:
183
184
  def __del__(self) -> None:
184
185
  """Destructor to ensure Live display is properly cleaned up."""
185
186
  if self.live:
186
- try:
187
+ # Ignore any errors during cleanup
188
+ with contextlib.suppress(Exception):
187
189
  self.live.stop()
188
- except Exception:
189
- pass # Ignore any errors during cleanup
190
190
 
191
191
  def update(self, text: str, final: bool = False) -> None:
192
192
  """Update the displayed markdown content.
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Iterable, List, Sequence, Tuple
3
+ from collections.abc import Iterable, Sequence
4
4
 
5
5
 
6
6
  class SearchableFormattedText:
@@ -16,8 +16,8 @@ class SearchableFormattedText:
16
16
  concatenating the text parts of the fragments.
17
17
  """
18
18
 
19
- def __init__(self, fragments: Sequence[Tuple[str, str]], plain: str | None = None):
20
- self._fragments: List[Tuple[str, str]] = list(fragments)
19
+ def __init__(self, fragments: Sequence[tuple[str, str]], plain: str | None = None):
20
+ self._fragments: list[tuple[str, str]] = list(fragments)
21
21
  if plain is None:
22
22
  plain = "".join(text for _, text in self._fragments)
23
23
  self._plain = plain
@@ -25,7 +25,7 @@ class SearchableFormattedText:
25
25
  # Recognized by prompt_toolkit's to_formatted_text(value)
26
26
  def __pt_formatted_text__(
27
27
  self,
28
- ) -> Iterable[Tuple[str, str]]: # pragma: no cover - passthrough
28
+ ) -> Iterable[tuple[str, str]]: # pragma: no cover - passthrough
29
29
  return self._fragments
30
30
 
31
31
  # Provide a human-readable representation.
@@ -45,7 +45,7 @@ class SearchableFormattedText:
45
45
  return self._plain
46
46
 
47
47
 
48
- class SearchableFormattedList(list[Tuple[str, str]]):
48
+ class SearchableFormattedList(list[tuple[str, str]]):
49
49
  """
50
50
  List variant compatible with questionary's expected ``Choice.title`` type.
51
51
 
@@ -54,7 +54,7 @@ class SearchableFormattedList(list[Tuple[str, str]]):
54
54
  - Provides ``.lower()``/``.upper()`` returning the plain text for search filtering.
55
55
  """
56
56
 
57
- def __init__(self, fragments: Sequence[Tuple[str, str]], plain: str | None = None):
57
+ def __init__(self, fragments: Sequence[tuple[str, str]], plain: str | None = None):
58
58
  super().__init__(fragments)
59
59
  if plain is None:
60
60
  plain = "".join(text for _, text in fragments)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import math
4
5
  import time
5
6
 
@@ -233,8 +234,6 @@ class BreathingSpinner(RichSpinner):
233
234
 
234
235
  # Monkey-patch Rich's Status module to use the breathing spinner implementation
235
236
  # for the configured spinner name, while preserving default behavior elsewhere.
236
- try:
237
+ # Best-effort patch; if it fails we silently fall back to default spinner.
238
+ with contextlib.suppress(Exception):
237
239
  rich_status.Spinner = BreathingSpinner # type: ignore[assignment]
238
- except Exception:
239
- # Best-effort patch; if it fails we silently fall back to default spinner.
240
- pass
@@ -153,10 +153,7 @@ class Themes:
153
153
 
154
154
 
155
155
  def get_theme(theme: str | None = None) -> Themes:
156
- if theme == "light":
157
- palette = LIGHT_PALETTE
158
- else:
159
- palette = DARK_PALETTE
156
+ palette = LIGHT_PALETTE if theme == "light" else DARK_PALETTE
160
157
  return Themes(
161
158
  app_theme=Theme(
162
159
  styles={
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import contextlib
2
3
  import os
3
4
  import select
4
5
  import signal
@@ -75,19 +76,15 @@ def start_esc_interrupt_monitor(
75
76
  r2, _, _ = select.select([sys.stdin], [], [], 0.0)
76
77
 
77
78
  if seq == "":
78
- try:
79
+ # Best-effort only; failures here should not crash the UI.
80
+ with contextlib.suppress(Exception):
79
81
  asyncio.run_coroutine_threadsafe(on_interrupt(), loop)
80
- except Exception:
81
- # Best-effort only; failures here should not crash the UI.
82
- pass
83
82
  stop.set()
84
83
  except Exception as exc: # pragma: no cover - environment dependent
85
84
  log((f"esc monitor error: {exc}", "r red"))
86
85
  finally:
87
- try:
86
+ with contextlib.suppress(Exception):
88
87
  termios.tcsetattr(fd, termios.TCSADRAIN, old)
89
- except Exception:
90
- pass
91
88
 
92
89
  esc_task: asyncio.Task[None] = asyncio.create_task(asyncio.to_thread(_esc_monitor, stop_event))
93
90
  return stop_event, esc_task
@@ -119,18 +116,14 @@ def install_sigint_double_press_exit(
119
116
  now = time.monotonic()
120
117
  if now - last_sigint_time <= window_seconds:
121
118
  # Second press within window: hide progress UI and exit.
122
- try:
119
+ with contextlib.suppress(Exception):
123
120
  hide_progress()
124
- except Exception:
125
- pass
126
121
  raise KeyboardInterrupt
127
122
 
128
123
  # First press: remember timestamp and show toast.
129
124
  last_sigint_time = now
130
- try:
125
+ with contextlib.suppress(Exception):
131
126
  show_toast()
132
- except Exception:
133
- pass
134
127
 
135
128
  try:
136
129
  signal.signal(signal.SIGINT, _handler)
@@ -139,9 +132,7 @@ def install_sigint_double_press_exit(
139
132
  return lambda: None
140
133
 
141
134
  def restore() -> None:
142
- try:
135
+ with contextlib.suppress(Exception):
143
136
  signal.signal(signal.SIGINT, original_handler)
144
- except Exception:
145
- pass
146
137
 
147
138
  return restore
@@ -39,7 +39,7 @@ class TerminalNotifierConfig:
39
39
  stream: TextIO | None = None
40
40
 
41
41
  @classmethod
42
- def from_env(cls) -> "TerminalNotifierConfig":
42
+ def from_env(cls) -> TerminalNotifierConfig:
43
43
  env = os.getenv("KLAUDE_NOTIFY", "").strip().lower()
44
44
  if env in {"0", "off", "false", "disable", "disabled"}:
45
45
  return cls(enabled=False)
@@ -95,9 +95,7 @@ class TerminalNotifier:
95
95
  if not getattr(stream, "isatty", lambda: False)():
96
96
  return False
97
97
  term = os.getenv("TERM", "")
98
- if term.lower() in {"", "dumb"}:
99
- return False
100
- return True
98
+ return term.lower() not in {"", "dumb"}
101
99
 
102
100
 
103
101
  def _compact(text: str, limit: int = 160) -> str:
@@ -99,7 +99,7 @@ def truncate_display(
99
99
  ) -> str:
100
100
  lines = text.split("\n")
101
101
  if len(lines) > max_lines:
102
- lines = lines[:max_lines] + ["… (more " + str(len(lines) - max_lines) + " lines)"]
102
+ lines = [*lines[:max_lines], "… (more " + str(len(lines) - max_lines) + " lines)"]
103
103
  for i, line in enumerate(lines):
104
104
  if len(line) > max_line_length:
105
105
  lines[i] = (
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from typing import Awaitable, Callable, Optional
2
+ from collections.abc import Awaitable, Callable
3
3
 
4
4
 
5
5
  class Debouncer:
@@ -15,7 +15,7 @@ class Debouncer:
15
15
  """
16
16
  self.interval = interval
17
17
  self.callback = callback
18
- self._task: Optional[asyncio.Task[None]] = None
18
+ self._task: asyncio.Task[None] | None = None
19
19
 
20
20
  def cancel(self) -> None:
21
21
  """Cancel current debounce task"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.2.12
3
+ Version: 1.2.13
4
4
  Summary: Add your description here
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: openai>=1.102.0