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.
- klaude_code/auth/codex/oauth.py +3 -3
- klaude_code/cli/main.py +5 -5
- klaude_code/cli/runtime.py +19 -27
- klaude_code/cli/session_cmd.py +6 -8
- klaude_code/command/__init__.py +6 -6
- klaude_code/command/export_cmd.py +3 -3
- klaude_code/command/registry.py +1 -1
- klaude_code/command/terminal_setup_cmd.py +2 -2
- klaude_code/command/thinking_cmd.py +8 -6
- klaude_code/config/__init__.py +1 -1
- klaude_code/config/list_model.py +1 -1
- klaude_code/core/agent.py +13 -61
- klaude_code/core/executor.py +11 -10
- klaude_code/core/manager/agent_manager.py +4 -4
- klaude_code/core/manager/llm_clients.py +10 -49
- klaude_code/core/manager/llm_clients_builder.py +8 -21
- klaude_code/core/manager/sub_agent_manager.py +3 -3
- klaude_code/core/prompt.py +2 -2
- klaude_code/core/reminders.py +1 -1
- klaude_code/core/task.py +2 -2
- klaude_code/core/tool/__init__.py +16 -25
- klaude_code/core/tool/file/_utils.py +1 -1
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +4 -7
- klaude_code/core/tool/file/edit_tool.py +4 -11
- klaude_code/core/tool/file/multi_edit_tool.py +2 -3
- klaude_code/core/tool/file/read_tool.py +3 -4
- klaude_code/core/tool/file/write_tool.py +2 -3
- klaude_code/core/tool/memory/memory_tool.py +2 -8
- klaude_code/core/tool/memory/skill_loader.py +3 -2
- klaude_code/core/tool/shell/command_safety.py +0 -1
- klaude_code/core/tool/tool_context.py +1 -3
- klaude_code/core/tool/tool_registry.py +2 -1
- klaude_code/core/tool/tool_runner.py +1 -1
- klaude_code/core/tool/truncation.py +2 -5
- klaude_code/core/turn.py +9 -3
- klaude_code/llm/anthropic/client.py +6 -2
- klaude_code/llm/client.py +1 -1
- klaude_code/llm/codex/client.py +2 -2
- klaude_code/llm/input_common.py +2 -2
- klaude_code/llm/openai_compatible/client.py +11 -8
- klaude_code/llm/openai_compatible/stream_processor.py +2 -1
- klaude_code/llm/openrouter/client.py +20 -8
- klaude_code/llm/openrouter/reasoning_handler.py +19 -132
- klaude_code/llm/registry.py +6 -5
- klaude_code/llm/responses/client.py +10 -5
- klaude_code/protocol/events.py +7 -0
- klaude_code/protocol/model.py +7 -1
- klaude_code/protocol/sub_agent.py +2 -1
- klaude_code/session/selector.py +2 -2
- klaude_code/session/session.py +2 -4
- klaude_code/trace/__init__.py +1 -1
- klaude_code/trace/log.py +1 -1
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/stage_manager.py +7 -4
- klaude_code/ui/modes/repl/__init__.py +1 -1
- klaude_code/ui/modes/repl/completers.py +3 -4
- klaude_code/ui/modes/repl/display.py +3 -4
- klaude_code/ui/modes/repl/event_handler.py +63 -5
- klaude_code/ui/modes/repl/key_bindings.py +2 -3
- klaude_code/ui/modes/repl/renderer.py +2 -1
- klaude_code/ui/renderers/diffs.py +1 -4
- klaude_code/ui/rich/markdown.py +3 -3
- klaude_code/ui/rich/searchable_text.py +6 -6
- klaude_code/ui/rich/status.py +3 -4
- klaude_code/ui/rich/theme.py +1 -4
- klaude_code/ui/terminal/control.py +7 -16
- klaude_code/ui/terminal/notifier.py +2 -4
- klaude_code/ui/utils/common.py +1 -1
- klaude_code/ui/utils/debouncer.py +2 -2
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/METADATA +1 -1
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/RECORD +74 -74
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.13.dist-info}/WHEEL +0 -0
- {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[[
|
|
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.
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
klaude_code/ui/rich/markdown.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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[
|
|
20
|
-
self._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[
|
|
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[
|
|
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[
|
|
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)
|
klaude_code/ui/rich/status.py
CHANGED
|
@@ -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
|
-
|
|
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
|
klaude_code/ui/rich/theme.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
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:
|
klaude_code/ui/utils/common.py
CHANGED
|
@@ -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]
|
|
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
|
|
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:
|
|
18
|
+
self._task: asyncio.Task[None] | None = None
|
|
19
19
|
|
|
20
20
|
def cancel(self) -> None:
|
|
21
21
|
"""Cancel current debounce task"""
|