Fix stuck "thinking…" — background task GC dropped the done signal
The QRunnable was auto-deleted the instant its work finished, destroying its signals object before Qt delivered the queued result to the UI thread, so the "done" callback never fired and the command bar stayed disabled. Now tasks keep a strong reference (autoDelete off) until the result is delivered, then drop it. Also: the WoodShop reply summary is now logged to the transcript on success (previously computed but never shown), and the error path re-enables input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e9422aa133
commit
7d01144143
|
|
@ -88,11 +88,17 @@ class CommandBar(QWidget):
|
||||||
|
|
||||||
def done(summary):
|
def done(summary):
|
||||||
self._busy(False)
|
self._busy(False)
|
||||||
if self.speak.isChecked() and summary:
|
if summary:
|
||||||
|
self._log("ws", summary)
|
||||||
|
if self.speak.isChecked():
|
||||||
run_async(self.pool, lambda: subprocess.run(
|
run_async(self.pool, lambda: subprocess.run(
|
||||||
["read-aloud", "--strip-md", "true"], input=summary, text=True))
|
["read-aloud", "--strip-md", "true"], input=summary, text=True))
|
||||||
|
|
||||||
run_async(self.pool, work, on_done=done, on_error=lambda e: (self._busy(False), self._log("sys", e)))
|
def failed(err):
|
||||||
|
self._busy(False)
|
||||||
|
self._log("sys", err)
|
||||||
|
|
||||||
|
run_async(self.pool, work, on_done=done, on_error=failed)
|
||||||
|
|
||||||
# ----- voice -------------------------------------------------------
|
# ----- voice -------------------------------------------------------
|
||||||
def _listen(self) -> None:
|
def _listen(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
"""Run slow work (dictate, the LLM call, read-aloud) off the Qt event loop so
|
"""Run slow work (dictate, the LLM call, read-aloud) off the Qt event loop so
|
||||||
the UI never freezes."""
|
the UI never freezes.
|
||||||
|
|
||||||
|
Lifetime note: a QRunnable is auto-deleted by the pool the moment run() returns,
|
||||||
|
which can destroy its signals object before Qt delivers the queued result to the
|
||||||
|
UI thread (the "done" callback then never fires). We disable auto-delete and
|
||||||
|
keep a strong reference until the result is delivered, then drop it.
|
||||||
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal
|
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal
|
||||||
|
|
@ -15,6 +21,7 @@ class _Task(QRunnable):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.fn = fn
|
self.fn = fn
|
||||||
self.signals = _Signals()
|
self.signals = _Signals()
|
||||||
|
self.setAutoDelete(False) # we manage lifetime (see module docstring)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -23,10 +30,23 @@ class _Task(QRunnable):
|
||||||
self.signals.error.emit(str(exc))
|
self.signals.error.emit(str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
_active: set[_Task] = set()
|
||||||
|
|
||||||
|
|
||||||
def run_async(pool: QThreadPool, fn, on_done=None, on_error=None) -> None:
|
def run_async(pool: QThreadPool, fn, on_done=None, on_error=None) -> None:
|
||||||
task = _Task(fn)
|
task = _Task(fn)
|
||||||
|
_active.add(task) # keep alive until a result is delivered on the UI thread
|
||||||
|
|
||||||
|
def finish_done(result):
|
||||||
|
_active.discard(task)
|
||||||
if on_done:
|
if on_done:
|
||||||
task.signals.done.connect(on_done)
|
on_done(result)
|
||||||
|
|
||||||
|
def finish_error(message):
|
||||||
|
_active.discard(task)
|
||||||
if on_error:
|
if on_error:
|
||||||
task.signals.error.connect(on_error)
|
on_error(message)
|
||||||
|
|
||||||
|
task.signals.done.connect(finish_done)
|
||||||
|
task.signals.error.connect(finish_error)
|
||||||
pool.start(task)
|
pool.start(task)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue