spacr.qt.widgets.console_panel¶
Combine pipeline output, errors, and AI chat in one console panel.
Topic bars separate pipeline stages and AI conversations in a shared scrolling
view. The input area and transcript form a vertical QSplitter; when a
persist_key is supplied, its state is stored under console/split/<key>
and restored per screen.
ConsolePanel can start a topic, append ordinary or error output,
launch the AI error-explanation flow, and clear the transcript. It owns its AI
worker so streamed state remains coherent while the active pipeline screen
changes.
Widget creation always occurs on the GUI thread. Calls to
ConsolePanel.append_stdout() and ConsolePanel.append_error() from
logging or pipeline workers are relayed through queued Qt signals before they
modify the transcript.
Classes¶
Merged pipeline stdout + AI chat panel. |
Functions¶
|
Serve |
|
Return the spaCR-AI text colour for a provider name. |
|
Error colour for the theme on screen right now. |
|
Pipeline stdout colour for the theme on screen right now. |
|
User-input colour for the theme on screen right now. |
|
Warning colour for the theme on screen right now. |
|
Return the saved |
|
Persist a |
Module Contents¶
- class spacr.qt.widgets.console_panel.ConsolePanel(active_app_label: str = '', parent=None, persist_key: str = '', *, follow_log: bool = True, chat: bool = True)[source]¶
Bases:
PySide6.QtWidgets.QWidgetMerged pipeline stdout + AI chat panel.
Owns the AI stream thread so provider switches and app changes do not orphan a running subprocess. See the module docstring for the full public surface.
The console box and the AI chat box are the two halves of a vertical
QSplitter, so the user can drag the handle between them to trade height — a taller chat box is a shorter console. Passpersist_keyand the position is remembered per screen.- Variables:
ai_stream_finished – emitted when an AI stream ends (ok or error) so the parent screen can flip its Cancel button back.
- Parameters:
active_app_label – the app name shown in the output banner.
parent – parent widget.
persist_key – screen key the console/chat split is remembered against (usually the screen’s
app_key). Empty means the split is not persisted, which is what a bare panel in a test wants.follow_log – False keeps the application-wide log out of this panel, for a screen whose console carries only its own messages (Make Masks); the log still reaches the file and the shell’s console.
chat – False hides the chat row, for a console that only reports.
- ai_explanation_of(traceback_text: str) str[source]¶
spaCR AI’s answer about
traceback_text, or"".Used by the bug reporter: when the AI is switched on it has usually already diagnosed the crash by the time the user files, and that analysis is the most useful thing in the report – it is what whoever picks the report up would otherwise spend the first hour reproducing.
Empty unless there IS an answer AND it is an answer to THIS error. The console holds one conversation across a whole session, so without the second condition a report about one crash would carry an explanation of an earlier one, stated with equal confidence.
- Parameters:
traceback_text – the traceback the report is about; compared, whitespace-stripped, with the traceback the AI last explained.
- append_error(tb: str) None[source]¶
Append red error text under a ‘spaCR ERROR — <module> — <function>’ banner.
- Parameters:
tb – traceback text; empty strings are ignored.
Thread-safe in the same way as
append_stdout().
- append_notice(source: str, **values: object) None[source]¶
Append one localized spaCR-authored UI notice.
This is intentionally separate from
append_stdout(): arbitrary worker stdout, logs, tracebacks, paths and AI responses must remain byte-for-byte English/canonical. Off-thread notices carry their stable English template to the GUI thread and are translated only there.- Parameters:
source – the notice’s untranslated English template; empty does nothing. It is translated on the GUI thread and filled with the keyword values.
- append_stdout(text: str) None[source]¶
Append pipeline output as blue text under a ‘spaCR output’ banner.
Safe to call from any thread: an off-thread call is re-posted to the GUI thread through
_relay_stdoutand returns without touching a widget. See_on_gui_thread().Re-entrant calls on the same thread are refused. Drawing a line runs Python inside a QWidget, and with verbose logging on the function-trace profile hook logs on entry to every spaCR function it passes through — including this one. Both console log sinks feed that record straight back here, and
_StdoutBlock.appendanswers it with a nestedsetPlainTextwhose first act is to destroy the QTextDocument’s frames — the ones the outer call is still inside. gdb:QTextFrame::~QTextFrame -> QTextDocumentPrivate::clear,#0in freed memory. Reproduced aspytest tests/qt/test_all_module_smoke.py tests/qt/test_batch_f_diagnostics.py(exit 139).- Parameters:
text – the pipeline output to append; empty does nothing.
- append_warning(text: str) None[source]¶
Append warning text in the theme’s amber, under the output banner.
DELIBERATELY NOT ITS OWN BANNER, and the reason is a coordination one rather than a design one: a “spaCR warning” heading would need a new row in
spacr.qt.i18n._ROWSwith nine translations, which moves the COMPACT caption ratchet. Amber under the existing translated “spaCR output” heading already achieves the point – warnings out of the ERROR pane, errors still in it – without reaching into that machinery. A dedicated banner would be the nicer end state.- Parameters:
text – the formatted record; empty strings are ignored.
- apply_column_text_scale(scale: float) None[source]¶
Size the console’s text with the column it sits in.
The entries carry their own point size (a per-widget sheet and an explicit font), which a sheet on the column cannot reach, so the column’s Ctrl + wheel hands its size here and it multiplies Zoom.
- Parameters:
scale – 1.0 for the size Zoom alone gives.
- as_text(start: int = 0, stop: int | None = None) str[source]¶
The console as plain text, section headers included.
- Parameters:
start – first entry index to include.
stop – one past the last, or
Nonefor the rest.
- Returns:
the text a person would have selected by hand.
- at_the_end() bool[source]¶
Whether the view is showing the newest line.
A few pixels of tolerance, because a scrollbar dragged to the end does not always land exactly on maximum() – the same tolerance
_on_console_scrolleduses, and for the same reason.
- begin_topic(label: str, accent: str | None = None, trailing: PySide6.QtWidgets.QWidget | None = None) _TopicBar | None[source]¶
Insert a divider bar labeled
label(e.g. ‘spaCR output — …’).A BAR IS NOT REDRAWN WHEN IT WOULD SAY THE SAME THING. Three bands now write under the “spaCR output” heading – stdout, warnings, and the notice path – and each opens its topic. A run that alternates between them therefore drew the identical banner before EVERY line:
=== spaCR output — Mask Generation === Source directory (src): … === spaCR output — Mask Generation === 12:09:37 [WARNING] cellpose.vit: Could not import CPDINO… === spaCR output — Mask Generation === 12:09:47 [INFO] spacr.qt.resource_cleanup: memory budget…
which is what the console looked like when this was reported. The divider exists to say the subject CHANGED; repeating it says nothing and costs three lines of a panel people read during a run.
The accent is deliberately not part of the comparison. It rides on the TEXT below the bar – amber for a warning, blue for output – so a warning still reads differently without a second identical heading above it.
- Parameters:
label – the heading text.
accent – colour for the heading, or
Nonefor the theme’s.trailing – a widget pinned to the right of the heading, such as a working indicator.
- Returns:
the bar that was drawn, or
Nonewhen the one already showing says the same thing and was kept. The caller needs the widget to be able to take an empty heading down again – see_take_down_the_waiting_heading().
- closeEvent(event) None[source]¶
Ensure the AI thread is drained before Qt destroys the panel.
- Parameters:
event – the close event, passed to the base class after
shutdown()drains the AI thread.
- collapse_section(bar: _TopicBar) None[source]¶
Hide
bar’s body, leaving its heading in place.- Parameters:
bar – the topic bar (section heading) whose section is meant.
- jump_to_the_end() None[source]¶
Show the newest line, and follow the tail again.
BOTH HALVES, because they are one decision. A console that jumped without resuming the follow would slide back off the end on the very next line written, and the user would press it again.
- open_error_flow(traceback_text: str, active_app: str = '', show_raw: bool = True) None[source]¶
Send a traceback to the AI explainer and stream the reply inline.
- Parameters:
traceback_text – raw traceback captured from the pipeline.
active_app – optional app label used in the framing prompt.
show_raw – when False, the raw traceback is NOT printed to the console (only a short note); the AI still receives it in its prompt, so the user can ask the AI to show the error.
- raise_section(bar: _TopicBar) None[source]¶
Bring
bar’s section to the top of the view and expand it.The console is a transcript, so the order of its sections is the one property a log has: this SCROLLS, it does not reorder.
Raising a section also stops the view following new output. A user who clicked a heading is reading THERE, and appending output that yanks the viewport away is what makes a live log unreadable. Following resumes when they scroll back to the bottom, which is the convention every log viewer uses.
- Parameters:
bar – the topic bar (section heading) whose section is meant.
- section_body(bar: _TopicBar)[source]¶
The widgets under
bar, up to the next topic bar.The same span
section_text()copies, as widgets rather than as text, so raising, collapsing and copying a section cannot disagree about where it ends. A nested heading inside the span is part of the body: folding a module banner folds the “spaCR output” banner under it too, because that banner is the section’s own content.- Parameters:
bar – the topic bar (section heading) whose section is meant. A bar not in the console gives an empty list.
- section_text(bar: _TopicBar) str[source]¶
Return a topic bar and its content up to the next topic bar.
- Parameters:
bar – the topic bar (section heading) whose section is meant. A bar not in the console gives
"".
- set_active_app(label: str) None[source]¶
Set the label used in the next auto-inserted topic divider.
- Parameters:
label – the text shown in the next automatic topic divider.
- set_ai_active(on: bool) None[source]¶
Enable/disable AI routing for Enter-submits from the input.
- Parameters:
on – whether Enter in the input goes to the AI; converted to bool.
- set_ai_provider(provider_name: str | None) None[source]¶
Select the provider used for AI submissions, or None to unset.
- Parameters:
provider_name – the provider’s name, or None to unset it.
- set_console_font_pt(pt: int) None[source]¶
Set the console font size and apply it to every existing entry.
- Parameters:
pt – the font size in points, converted to int.
- set_run_context(module: str = '', function: str = '') None[source]¶
Record the module/function the pipeline output comes from.
Shown in the “spaCR output — <module> — <function>” banner so users can see the source of the output at a glance.
- set_split_sizes(console_px: int, chat_px: int) None[source]¶
Move the handle programmatically and persist the result.
Same end state as a user drag, so a caller restoring a layout and a user dragging leave the panel in the same place.
- Parameters:
console_px – height for the console box.
chat_px – height for the AI chat box.
- shutdown() None[source]¶
Cancel any active stream and block until its QThread has exited. Must be called before the panel (or its parent window) is destroyed — otherwise Python drops the last reference to the running QThread and Qt aborts with:
QThread: Destroyed while thread '' is still running.The cancel path kills the CLI subprocess directly so the stream reader unblocks immediately; we then wait for the worker’s run() to return and the QThread to quit normally.
There is deliberately no
QThread.terminate()fallback. It used to be here, described as a last resort, and it was reached far more often than “last resort” suggests:spacr.qt.ai.workerqueuedworker.finished -> thread.quitto the GUI-affine QThread object, so the event that stops the thread sat behind this method’s ownwait()and the wait timed out on streams that had already finished. Terminating a thread that is running Python ispthread_cancel: if it dies holding the GIL the process stops making progress with every thread still alive, and if it dies inside Qt or PySide the heap is corrupt and the crash lands somewhere unrelated later.bridge.drain_threadparks a thread that will not stop instead, which keeps the “never destroy a running QThread” rule without buying it with undefined behaviour.
- split_sizes() List[int][source]¶
Current
[console_height, chat_height]in pixels.Public because it is the honest thing for a test — or a caller arranging the screen — to read, rather than reaching into
_split.
- toggle_section(bar: _TopicBar) None[source]¶
Reach the section first; fold it away second.
Sections are created EXPANDED, so a plain expanded/collapsed toggle spent the user’s first click hiding the very section they were reaching for, and the viewport never moved – the opposite of “click a console section heading to bring it to the top of the console”.
A heading that is not already at the top of the viewport is therefore a request to GO THERE, whatever its state. Only a heading already sitting at the top has nowhere left to navigate to, and there collapsing is the one thing the gesture can still mean – reachable on a second click, exactly where the user’s hand already is.
- Parameters:
bar – the topic bar (section heading) whose section is meant.
- spacr.qt.widgets.console_panel.__getattr__(name: str) str[source]¶
Serve
COLOR_OUTPUT/COLOR_USER/COLOR_ERRORlive.PEP 562. Reading one of the three resolves it against the current theme, so
from ...console_panel import COLOR_USERcan no longer freeze a dark-theme hex into a caller at import time.
- spacr.qt.widgets.console_panel.ai_color_for_provider(provider_name: str | None) str[source]¶
Return the spaCR-AI text colour for a provider name.
- Parameters:
provider_name – the provider’s name, matched case-insensitively by substring (
claude,gpt,geminiand the like); None or unknown gives the default colour.
- spacr.qt.widgets.console_panel.color_error() str[source]¶
Error colour for the theme on screen right now.
- spacr.qt.widgets.console_panel.color_output() str[source]¶
Pipeline stdout colour for the theme on screen right now.
- spacr.qt.widgets.console_panel.color_user() str[source]¶
User-input colour for the theme on screen right now.
- spacr.qt.widgets.console_panel.color_warning() str[source]¶
Warning colour for the theme on screen right now.
Distinct from
color_error()on purpose – seeConsolePanel.append_warning(). Every theme already defines thewarningrole; nothing here needed inventing.
- spacr.qt.widgets.console_panel.get_split_state(screen_key: str)[source]¶
Return the saved
QSplitter.saveState()blob forscreen_key.- Parameters:
screen_key – the screen’s app key, e.g.
"mask".- Returns:
the stored
QByteArray, orNonewhen the user has never dragged this screen’s handle (or the stored value is unusable).
- spacr.qt.widgets.console_panel.set_split_state(screen_key: str, state) None[source]¶
Persist a
QSplitter.saveState()blob againstscreen_key.Stored as the splitter’s own state rather than as a pixel pair on purpose: a saved
[572, 120]means something different on a laptop panel than on the 4K display the same user docks into, whereasrestoreStateis the mechanism Qt itself defines for this.- Parameters:
screen_key – the screen the state belongs to; stripped, and a blank key stores nothing.
state – the
QSplitter.saveState()bytes, stored as aQByteArray.