spacr.qt.ai.worker¶
QThread worker that streams chat completions from a ChatProvider so the UI stays responsive during long generations.
- Emits:
stage_changed(str) — coarse progress: “connecting”, “streaming” chunk_ready(str) — a partial completion chunk finished(bool, str) — (ok, full_text_or_error)
Classes¶
QObject that drives one provider stream on a worker QThread. |
Functions¶
|
Return (QThread, StreamWorker) — connect signals, then start(). |
Module Contents¶
- class spacr.qt.ai.worker.StreamWorker(provider: spacr.qt.ai.providers.ChatProvider, messages: List[Dict], system: str = '', model: str | None = None)[source]¶
Bases:
PySide6.QtCore.QObjectQObject that drives one provider stream on a worker QThread.
- Variables:
stage_changed – coarse progress signal (“connecting”, “streaming”).
chunk_ready – emitted with each partial completion chunk.
finished – emitted with
(ok, full_text_or_error)on completion.
Prepare the worker; call
run()from a QThread’sstartedsignal.- Parameters:
provider – the ChatProvider to stream from.
messages – conversation history to send.
system – optional system prompt.
model – optional model override.
- cancel() None[source]¶
Cancel: kill the subprocess so the reader unblocks.
Setting a Python flag alone isn’t enough — the worker is blocked in a
for line in proc.stdoutiteration until the subprocess writes or closes. We terminate the subprocess directly viaprovider.cancel_stream(); the reader then exits with an empty read and run() completes cleanly.
- run() None[source]¶
Consume the provider stream, emitting stage/chunk/finished signals.
finishedcarries(True, the whole reply)only when the stream ended on its own and the provider did not report a failure. Anything else gives(False, <what to tell the user>), and whatever the provider printed before failing is not an answer. A stream that was cancelled gives(False, "Cancelled.")even when ending the child made the provider raise, because the user asked for the stop.THE EXCEPTION’S CLASS NAME IS PREFIXED ONLY WHEN IT SAYS SOMETHING. The console writes this text after “[AI error] “, so for a
ProviderFailed– whose whole message is written to be read there, down to the sign-in command to run – the prefix turned a sentence the user could act on into “[AI error] ProviderFailed: claude stopped with exit status 1: …”. Every other exception keeps its class, which is often the only thing naming what went wrong: a bare[Errno 2] No such file or directorydoes not say it is a FileNotFoundError.
- spacr.qt.ai.worker.make_stream_thread(provider: spacr.qt.ai.providers.ChatProvider, messages: List[Dict], system: str = '', model: str | None = None, parent: PySide6.QtCore.QObject | None = None) tuple[PySide6.QtCore.QThread, StreamWorker][source]¶
Return (QThread, StreamWorker) — connect signals, then start().
IMPORTANT: pass a
parent(typically the panel that owns this stream). Without a Qt parent the QThread’s C++ object gets tied exclusively to Python’s refcount — and dropping the ref while QThread.isRunning() is still True (which happens in the tiny window between worker.run returning and thread.finished firing) triggers Qt’sQThread: Destroyed while thread is still running / Abortedcrash. A parent keeps the C++ object alive until deleteLater runs.Callers must ALSO keep a Python reference to the worker until the stream truly finishes (see ConsolePanel._retire).
Two wiring details are load-bearing; both are the same contract
spacr.qt.bridge.make_thread()documents, and this function used to get them wrong:worker.finished -> thread.quitis a DirectConnection. The QThread object is created here, on the GUI thread, so it is GUI-affine — a queuedquit()is posted to the GUI thread’s event queue, not to the worker’s. Measured: with a queued connection, a GUI thread that goes straight intothread.wait()(which is exactly whatConsolePanel.shutdownand every “drain before closing” path does) waits out its whole timeout on a worker that has already finished, because the event that would stop the thread is sitting behind the wait.QThread::quitis explicitly thread-safe, so calling it inline from the worker thread is correct.There is deliberately no
worker.deleteLater. The worker’s affinity is the worker thread, so a deferred delete is posted into a loop that is stopping, while the panel drops the object’s last Python reference from the GUI thread — two owners, one object.bridge.make_thread’s ownership essay records the gdb trace (QThread -> sendPostedEvents -> ~QObject -> Sbk_GetPyOverride) and the measurement: 3 crashes in 8 runs. A PySide6 object built in Python is already owned by Python;ConsolePanel/AIChatPanelhold it in_retireduntil the thread has exited and free it there, on the thread that holds it.
- Parameters:
provider – the
ChatProviderwhosestream_chatthe worker consumes.messages – conversation history as
{role, content}dicts, handed to the provider unchanged.