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:
mARTin-B78 2026-06-09 15:56:44 +02:00
parent 73264af2be
commit aea24400af
3 changed files with 40 additions and 14 deletions

View File

@ -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"

View File

@ -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 ""

View File

@ -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"):