fix: auto-paste long/multiline output, O(n²) overlay accumulation (v2.0.1)
Root cause of session freeze confirmed: a 15 000-char code block was typed character-by-character via xdotool at 12ms/char = ~3 min, flooding the X11 per-client event buffer until the entire session froze. - paste.py: any text >300 chars or containing newlines auto-upgrades to clipboard paste (instant Ctrl+V) regardless of configured output mode. xdotool type is kept only for short single-line text where it matters. - daemon.py: replace on_token "".join(acc) accumulation (O(n²) for long code blocks) with a sliding deque that shows only the last 400 chars in the overlay — Pango no longer re-lays out a growing 15 KB string on each incoming token. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
73264af2be
commit
aea24400af
@ -6,4 +6,4 @@ counterpart to the macOS Blitztext menu bar app: it runs natively on the host
|
||||
(not in a container) so it can type into any application via xdotool.
|
||||
"""
|
||||
|
||||
__version__ = "2.0.0"
|
||||
__version__ = "2.0.1"
|
||||
|
||||
@ -555,33 +555,43 @@ class Daemon:
|
||||
# the complete result, typed once the rewrite finishes.
|
||||
on_token = None
|
||||
if self.text_cb:
|
||||
acc: list[str] = []
|
||||
# Pulse "thinking" dots until the first token arrives.
|
||||
# Use a deque to stream the last ~400 chars to the overlay so
|
||||
# Pango never has to lay out a 15 000-char code block on each
|
||||
# token, and "".join() stays O(window) not O(total).
|
||||
from collections import deque
|
||||
_window: deque[str] = deque()
|
||||
_window_len: list[int] = [0]
|
||||
_OVERLAY_CHARS = 400
|
||||
|
||||
_thinking_frames = ["⏳ Thinking.", "⏳ Thinking..", "⏳ Thinking...", "⏳ Thinking"]
|
||||
_thinking_state: list[int] = [0] # [frame_idx] 0 = still thinking
|
||||
_thinking_state: list[int] = [0]
|
||||
_first_token: list[bool] = [False]
|
||||
|
||||
def _pulse_thinking(_s=_thinking_state, _f=_first_token) -> bool:
|
||||
if _f[0]:
|
||||
return False # first token arrived, stop pulsing
|
||||
return False
|
||||
self.text_cb(_thinking_frames[_s[0] % len(_thinking_frames)])
|
||||
_s[0] += 1
|
||||
return True # keep timer running
|
||||
return True
|
||||
|
||||
try:
|
||||
from gi.repository import GLib as _GLib
|
||||
# MUST be scheduled via idle_add so timeout_add is called
|
||||
# from the GTK main thread, not from _process's background
|
||||
# thread. GLib.timeout_add from non-main threads is not
|
||||
# thread-safe in PyGObject/GTK3 and can wedge the main loop.
|
||||
# idle_add ensures timeout_add runs on the GTK main thread —
|
||||
# calling timeout_add from a background thread is not safe.
|
||||
_GLib.idle_add(lambda: _GLib.timeout_add(400, _pulse_thinking) and False)
|
||||
except Exception: # noqa: BLE001 - headless mode, no GLib
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
def on_token(delta: str, _acc=acc, _f=_first_token) -> None:
|
||||
def on_token(delta: str,
|
||||
_w=_window, _wl=_window_len, _f=_first_token) -> None:
|
||||
_f[0] = True
|
||||
_acc.append(delta)
|
||||
self.text_cb("".join(_acc))
|
||||
_w.append(delta)
|
||||
_wl[0] += len(delta)
|
||||
# Trim old tokens from the front to stay within the window.
|
||||
while _wl[0] > _OVERLAY_CHARS and len(_w) > 1:
|
||||
_wl[0] -= len(_w[0])
|
||||
_w.popleft()
|
||||
self.text_cb("".join(_w))
|
||||
|
||||
# Use the preset's pinned engine when set, else the active one.
|
||||
engine_name = getattr(target, "llm_engine", "") or ""
|
||||
|
||||
@ -41,6 +41,13 @@ def _focus(window_id: str | None) -> None:
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
# Above this character count, or when the text contains newlines, xdotool type
|
||||
# sends thousands of synchronous X11 round-trips and can flood the X11 server's
|
||||
# per-client event buffer until the whole session freezes. Auto-upgrade to a
|
||||
# single clipboard paste instead, which is instantaneous.
|
||||
_TYPE_THRESHOLD = 300
|
||||
|
||||
|
||||
def deliver(text: str, *, mode: str = "type", window_id: str | None = None, type_delay_ms: int = 4) -> None:
|
||||
if not text:
|
||||
return
|
||||
@ -55,6 +62,15 @@ def deliver(text: str, *, mode: str = "type", window_id: str | None = None, type
|
||||
# Give the user time to release the hotkey modifiers before we synthesize input.
|
||||
time.sleep(0.12)
|
||||
|
||||
# Long or multi-line text: force clipboard paste regardless of configured mode.
|
||||
# xdotool type at 12ms/char for a 15 000-char code block takes ~3 minutes and
|
||||
# sends so many synchronous X11 events that the server's per-client buffer
|
||||
# overflows, freezing the entire X11 session.
|
||||
if mode == "type" and (len(text) > _TYPE_THRESHOLD or "\n" in text):
|
||||
if _set_clipboard(text):
|
||||
mode = "paste"
|
||||
# If clipboard isn't available we fall through to xdotool type as before.
|
||||
|
||||
if mode == "paste" and _set_clipboard(text):
|
||||
if wayland:
|
||||
if shutil.which("wtype"):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user