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.
|
(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.
|
# the complete result, typed once the rewrite finishes.
|
||||||
on_token = None
|
on_token = None
|
||||||
if self.text_cb:
|
if self.text_cb:
|
||||||
acc: list[str] = []
|
# Use a deque to stream the last ~400 chars to the overlay so
|
||||||
# Pulse "thinking" dots until the first token arrives.
|
# 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_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]
|
_first_token: list[bool] = [False]
|
||||||
|
|
||||||
def _pulse_thinking(_s=_thinking_state, _f=_first_token) -> bool:
|
def _pulse_thinking(_s=_thinking_state, _f=_first_token) -> bool:
|
||||||
if _f[0]:
|
if _f[0]:
|
||||||
return False # first token arrived, stop pulsing
|
return False
|
||||||
self.text_cb(_thinking_frames[_s[0] % len(_thinking_frames)])
|
self.text_cb(_thinking_frames[_s[0] % len(_thinking_frames)])
|
||||||
_s[0] += 1
|
_s[0] += 1
|
||||||
return True # keep timer running
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from gi.repository import GLib as _GLib
|
from gi.repository import GLib as _GLib
|
||||||
# MUST be scheduled via idle_add so timeout_add is called
|
# idle_add ensures timeout_add runs on the GTK main thread —
|
||||||
# from the GTK main thread, not from _process's background
|
# calling timeout_add from a background thread is not safe.
|
||||||
# thread. GLib.timeout_add from non-main threads is not
|
|
||||||
# thread-safe in PyGObject/GTK3 and can wedge the main loop.
|
|
||||||
_GLib.idle_add(lambda: _GLib.timeout_add(400, _pulse_thinking) and False)
|
_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
|
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
|
_f[0] = True
|
||||||
_acc.append(delta)
|
_w.append(delta)
|
||||||
self.text_cb("".join(_acc))
|
_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.
|
# Use the preset's pinned engine when set, else the active one.
|
||||||
engine_name = getattr(target, "llm_engine", "") or ""
|
engine_name = getattr(target, "llm_engine", "") or ""
|
||||||
|
|||||||
@ -41,6 +41,13 @@ def _focus(window_id: str | None) -> None:
|
|||||||
time.sleep(0.05)
|
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:
|
def deliver(text: str, *, mode: str = "type", window_id: str | None = None, type_delay_ms: int = 4) -> None:
|
||||||
if not text:
|
if not text:
|
||||||
return
|
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.
|
# Give the user time to release the hotkey modifiers before we synthesize input.
|
||||||
time.sleep(0.12)
|
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 mode == "paste" and _set_clipboard(text):
|
||||||
if wayland:
|
if wayland:
|
||||||
if shutil.which("wtype"):
|
if shutil.which("wtype"):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user