Desktop workbench overhaul: toolbar options, input/constants/result redesign, design-token pass#84
Desktop workbench overhaul: toolbar options, input/constants/result redesign, design-token pass#84yilibinbin wants to merge 139 commits into
Conversation
…s (SWARM P0 F01/F02) The deployment docs told operators to run `gunicorn/waitress app_web.server:app`, but app_web/server.py exposes no module-level `app` — only the create_app() factory — so every documented production command crashed at startup with "Failed to find attribute 'app'". Fixed across all 5 deployment surfaces (docs/web/deploy.en|zh.md, docs/DATALAB_WEB_GUIDE.en|zh.md, gunicorn.conf.py) by switching to the app-factory form: gunicorn `'app_web.server:create_app()'`, waitress `--call app_web.server:create_app`. The docs also recommended multi-worker (`-w 4`/`-w 9`) without noting that the SSE rate-limiter (_RATE_HISTORY) and collab session registry are per-process in-memory state. Rather than force single-worker (which would serialize ALL mpmath compute — mpmath is process-global and serialized by _MP_SERIAL_LOCK, so gunicorn.conf.py deliberately floors workers at 2 and sse.py notes "scale by processes, not threads"), the multi-worker recommendation is KEPT and the trade-offs are now documented honestly: the DoS rate budget is per-worker (≈ RATE_MAX_REQUESTS × workers, or enforce a strict global cap at nginx), and multi-worker collaboration needs sticky sessions plus a shared store (Redis). Adds tests/test_deploy_docs_wsgi_targets.py: parses all deployment surfaces, asserts every documented WSGI target resolves to a Flask app via werkzeug.import_string, and forbids the bare `app_web.server:app` from reappearing. Reviewed: Codex + Gemini 3.1 Pro adversarial (both PASS); full suite 3834 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multi-dimensional swarm review of the whole package (11 Claude dimension reviewers + Codex external pass), every finding adversarially verified by 2 independent skeptics (86 candidates → 56 survived), then re-verified line-by-line against the code (0 overturned) and passed a Codex + Gemini 3.1 Pro external adversarial review. F01/F02 are marked fixed (landed in the preceding commit). Analysis document only — the fixes it recommends land as separate changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…specs Section 1 (architecture) and Section 2 (5-batch implementation plan) for the desktop Adaptive Workbench redesign. Both passed external dual-model adversarial review (Codex + Gemini 3.1 Pro): Section 1 over 4 rounds, Section 2 over 5 rounds, each finding fixed and re-verified against code to zero findings. Section 1: keep the 3-pane QSplitter; icon rail in a new inner content HBox wrapping [icon_rail | splitter]; pane 0 hosts a CurrentPageStack (page 0 = existing config); result rail stretch 0->1; fold via setVisible(False). Section 2 batches: (1) shell scaffold, zero test breakage; (2) control migration + frequency tiering; (3) F19/F04 layout-coupled run/stop fixes; (4) fold-to-widen + focus mode + layout memory; (5) polish (F03/F14/empty-state). Each batch: own worktree, TDD, ruff/mypy, dual-external review, full suite, user-confirmed merge. Plan/analysis documents only — no production code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… strip Replaces the abandoned page-switch sidebar with a menu-bar approach that keeps every config control in place (never reparented/hidden), per a 3-round dual-model design review. - menu_options.py: adds icon-ified 计算 (精度 + 并行/资源) and LaTeX menus after 文件 in build_menu; icons on all existing menus. Menu items are NAVIGATION (reveal gate → ensureWidgetVisible → setFocus on the SAME rail widget) plus two-way signal-synced checkable QActions for checkboxes (blockSignals-guarded). latex_engine_combo deliberately omitted — it is a result-output control that lives in the LaTeX result tab and is only reachable after a result exists. - result_overview_popover.py: a top-level QWidget(Qt.WindowType.Popup) opened on clicking the overview card, showing method/value/uncertainty/elapsed/#points read from the same state; auto-closes on click-outside. Existing overview widgets are NOT reparented. - result_status_strip.py: a minimal always-visible footer strip (status badge + method + elapsed) driven by the shared result-rail refresh; theme.py extended so the strip's status badge gets the same colored pill as the overview badge. Adds tests/test_desktop_option_reachability.py — the hard acceptance criterion: every config control is reachable via its visible gate, asserted with isVisibleTo(window) AND unchanged parent() (proven to fail if a control is hidden). This guards the exact hiding-bug class that sank the prior approach. Invariants: single parent per control (no reparent/duplicate), no mixin/MRO change, no shared/ui_specs change, file-size ratchet respected (panels.py 2194 < 2207; new modules <800). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, complete reachability coverage External dual-model review (Codex + Gemini) found 4 confirmed issues, all fixed: - Gated checkbox menu actions (dcolumn/caption, gate=latex) toggled a HIDDEN control without revealing its gate. _bind_check_action now reveals the gate (checks generate_latex_checkbox) on trigger so the control becomes visible — no more operating an unseen control. Two-way blockSignals sync intact. - Reachability test was masking: it forgot caption_edit, use_file_checkbox, data_file_edit, manual_table, and the constants control. Added coverage (each asserts isVisibleTo + unchanged parent after its real gate). The named use_constants_file_checkbox/constants_file_edit don't exist on the live window (dead hasattr refs) — covered the real input_constants_editor instead. - latex_input_precision_spin (输入列位数) was missing from the LaTeX menu; added. - Removed the unimplemented result_numeric gate docstring; display_digits/ scientific stay in the result numeric tab (like latex_engine_combo), covered by the result-tab reachability test. Verified non-masking: hiding caption_edit's reveal makes the reachability test fail (independently reproduced). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> EOF
…-bound controls Codex review found the hand-written reachability test still masked per-mode schema-bound controls (power_p_edit, uncertainty_combo, all statistics.* etc.) — ~89 input controls carry datalab_schema_key but only ~20 were listed. Rewritten to enumerate every schema-bound input widget (QLineEdit/QSpinBox/ QComboBox/QCheckBox/QPlainTextEdit) from the live tree and sweep each for reachability in its mode/gate: switch to the owning mode, sweep single + pairwise gate selectors, assert isVisibleTo(window) AND unchanged parent(). Result-only controls (results.*/latex.engine, hidden until a result exists) asserted as result-gated. Empty unreachable-allowlist — all 89 are reachable, no production change. A baked-in test_non_masking_guard force-hides a control and asserts the sweep fails, so the test provably catches the hiding-bug class on every run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pinBox) Codex review found the type filter excluded QDoubleSpinBox, masking pdf.zoom_percent (a live QDoubleSpinBox bound to that key). Fixed: filter is now QAbstractSpinBox-based (covers QSpinBox + QDoubleSpinBox) plus combo/checkbox/ lineedit/plaintextedit. Enumeration 89 -> 90 (adds pdf.zoom_percent). pdf.* is a result-only prefix (PDF lives in the result tabs, hidden pre-result) so it is asserted result-gated. Verified non-masking for the new type: force-hiding pdf_zoom_spin makes the sweep fail. QPushButton (command controls) and QTextBrowser (read-only display) are excluded by a documented principled rule (not a silent type omission); they carry schema keys for dispatch/display, not editable options, and are covered by their own tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stiveness guard Gemini + my probe found the sweep still masked custom tabular editable inputs (ParameterTable fitting.*.parameters, DetectedRowsTable root.unknowns, ConstantsEditor *.units.*). Fixed: _INPUT_TYPES is now capability-based — Qt value inputs (base classes) PLUS the app's custom editors (ConstantsEditor, ParameterTable, DetectedRowsTable), imported by class. Enumeration 90 -> 101, all reachable in their mode (units editors gated by units.enabled/units.mode, revealed by the pairwise sweep). Empty unreachable-allowlist. Adds test_no_editable_schema_type_is_excluded: asserts every excluded schema-keyed type is in a CLOSED _NON_INPUT_SCHEMA_TYPES set (QLabel/QPushButton/QScrollArea/QTabWidget/QTextBrowser — all non-input, all documented). A future editable widget type now FAILS this guard instead of slipping past — ending the type-omission masking class structurally. Verified non-masking: force-hiding a ParameterTable fails the sweep; injecting a new editable type (QDateEdit) is flagged unclassified by the guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex found the shared input_constants_editor is relabelled at runtime (fitting.custom.constants -> fitting.implicit.constants when fit_model_combo == self_consistent), and the sweep captured (widget,key) once before sweeping so it never observed the dynamic key state. The widget itself is reachable (visible in that mode) — this was a test-coverage gap, not a hidden control. Fix: _record() now re-reads each widget's LIVE datalab_schema_key at record time, so a key that only exists mid-sweep is covered. _DYNAMIC_KEYS_BY_MODE lists the known runtime-relabelled keys and the per-mode test asserts each becomes reached during the sweep. Verified meaningful: disabling the live-key re-read makes the fitting test fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…two-way sync) User feedback: the menu items were navigation shortcuts (click → focus the config rail), not editable in the menu. Now each value item is a QWidgetAction hosting a NEW mirror widget (QSpinBox/QComboBox/QLineEdit matching the real control's range/items) that two-way-syncs to the real in-rail control via signals with recursion guards. The real control STAYS in the config rail (no reparent → single-parent invariant + reachability test intact) and the menu shows an editable copy. - New menu_option_editors.py: builds the QWidgetAction mirrors + recursion-safe sync (mirror→real keeps real's signals live so downstream slots run; real→mirror blockSignals-guarded). Gated LaTeX editors reveal_gate before applying. Combo mirrors register with the real combo's i18n spec so bilingual relabel works. - menu_options.py: value items become editors at wire time; checkboxes stay as the existing two-way checkable QActions. Verified: setting a menu mirror changes the real control and vice versa (spin + combo), no recursion, real controls not reparented (reachability test still 18 passed). Full suite 67 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (not a silent set) Gemini review found test_precision_mirror_drives_real_downstream_slot was trivially true — it asserted the spin's own value (which a silent set also satisfies) instead of the downstream effect it claims to verify. Fixed: spy on the real control's valueChanged and assert it FIRED when the mirror was edited, proving downstream schema/UI slots run. Verified non-trivial: making the mirror->real set silent (blockSignals) makes the test fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The card click filter fired on ANY MouseButtonRelease, so a right- or middle-click on the overview card spuriously opened the popover. Guard on Qt.MouseButton.LeftButton. Adds a regression test that drives left/right/ middle releases through the filter and asserts only left opens it (proven to fail on the unguarded form). CodeRabbit finding 1 (QAction.NoRole -> scoped MenuRole.NoRole) intentionally skipped: the whole codebase (13 sites in panels.py) uses the unscoped form; a lone scoped call here would break consistency (Rule 8). A scoped-enum sweep is a separate change if desired.
…m off-window QMenu Replaces the icon-menubar approach (options landed in macOS system menu bar, off-window; left 选项 panel stayed, no space freed). Dual-model VERDICT: FRAME (QMenu breaks nested QComboBox; QFrame(Qt.Popup) doesn't). Real controls move from options_box into two in-window toolbar dropdown popups (计算/LaTeX), shrinking the rail so the result area maximizes.
…ERDICT: INLINE) Second adversarial round (Codex+Gemini, live offscreen probes) reversed the QFrame(Qt.Popup) choice: a QComboBox inside any Qt.Popup can be dismissed by the macOS Cocoa grab when its dropdown opens — invisible offscreen (always passes CI, only fails in production on Mac) = unacceptable. INLINE = a normal QWidget child toggled by the toolbar button: no grab, isVisibleTo(window) meaningful, stable parent, trivial reachability gate. Row inserts at root_layout index 1 (between toolbar and splitter).
…选项 rail panel Low-frequency options (precision, parallel, LaTeX, plots/verbose) move OUT of the left-rail 选项 QGroupBox INTO two inline toggle panels dropped under the toolbar, toggled by checkable 计算/LaTeX toolbar buttons next to 运行/停止. The left rail shrinks so the result area maximizes — the user's original request. Mechanism = INLINE (dual-model VERDICT: INLINE), NOT a floating Qt.Popup: a QComboBox inside a Qt.Popup can be dismissed by the macOS Cocoa grab when its dropdown opens — a bug invisible offscreen (always passes CI, only fails on Mac). A normal QWidget child toggled setVisible has no grab, keeps isVisibleTo(window) meaningful and the parent stable, so the reachability sweep just gains a trivial open-panel gate. - NEW app_desktop/workbench_options_panel.py: build_options_panel + bind_options_toggle. - workbench_toolbar.py: two checkable 计算/LaTeX buttons. - panels.py: reparent the REAL controls (schema keys/signals preserved) into the panels; options_panels_row inserted at root_layout index 1; options_box detached from the rail (attr kept for the legacy shell-layout test). - DELETE the off-window QMenu approach (menu_options.py, menu_option_editors.py + 2 tests). - Tests: new panel behaviour suite; reachability sweep gains an open-panel gate; global-options schema audit repointed at the panels; shell-layout contract adds the two buttons and drops the stale rail-geometry assertion for the moved spin. Full desktop suite: 769 passed. ruff clean.
… review — Codex) Serial adversarial review (Claude→Gemini→Codex) found a real masked gate: tools/scan_desktop_gui_schema.py still audited window.options_box, which is now empty (controls moved into the 计算/LaTeX toolbar panels). A required-but-unbound widget in a panel passed the release schema-scan silently. Repoint the scan at compute_options_panel + latex_options_panel; add a regression test that strips a required widget's schema key and asserts the scan flags it (proven to fail against the old options_box-pointed scanner). Adjudication: Gemini's 'options_box leaks OS window handles' finding is REFUTED — a never-shown unparented QGroupBox has WA_WState_Created False and no native handle (Claude + Codex probes agree). The orphan is a benign dead attr (kept: it is also registered in _translations, so removal needs more than deleting the attr; not worth the churn for a harmless empty widget).
…t (CodeRabbit) The points label used the column count as a fallback whenever the row count was falsy — so an empty 0-row/N-col tabular result displayed N points instead of 0. Show the actual row count for tabular states (including 0); keep the column-count fallback only for non-tabular states. Adds a regression test (proven RED first).
…s (CodeRabbit) The selector sweep toggled every combo/checkbox, including ones hidden inside a collapsed options panel — which could mark a downstream control reachable via a gate the user cannot see (a masking risk). Guard each selector toggle on isVisibleTo(window) AND isEnabled(). Today harmless (panel combos are leaf controls gating nothing, and are covered as reachable via the panel-open gate in the mode-independent test), but this makes the sweep model true user-operability. Skipped CodeRabbit's paired-order (combinations→permutations) suggestion: an empirical run showed permutations reaches the identical 32 keys for this UI — zero coverage gain for double the pairwise runtime. Full reachability suite: 18 passed (incl. non-masking guard).
…dius Serial adversarial (Codex FAIL → Gemini FAIL, both on incomplete test audit) + two independent lead sweeps enumerate the full 3-pane assumption set: - app code: visual_contract (3-pane), _refresh_main_splitter_left_min_width + left_layout aliases (config-rail-anchored), workbench_layout splitter. - tests: workbench_layout, mode_stack, shell_layout (mode_section order), workbench_data_area (mode_section parent), splitter_persistence (invert the stale-blob test), workbench_visual_contract (rewrite to 2 regions), screenshots. Design confirmed sound: merged pane = workbench_workspace_* as left source of truth, config_rail compatibility-only, mode_combo → toolbar. Ready for TDD.
…2-pane) The 计算模式 selector (mode_combo) moves from the left-rail mode_section card onto the in-window toolbar (left, after the DataLab identity label, before 新建). The toolbar reserves a _toolbar_mode_slot; panels.py inserts the SAME mode_combo into it once created. mode_section/mode_box kept as detached compatibility attrs, no longer added to the config rail. _on_mode_change wiring unchanged — verified all 5 modes still switch mode_stack (live probe + test). Tests: new test_desktop_mode_selector_on_toolbar.py (mode on toolbar, left of 新建, 5-mode stack switch, mode_section not a rail card); updated shell_layout + workbench_data_area to the new rail order [input, output_setup, run].
The left config rail merges into the workspace canvas so the splitter holds two panes: [输入 + 配置, vertically stacked | 结果]. The merged pane is the new left-pane source of truth; workbench_config_rail/_content survive only as detached compatibility attributes (never a splitter pane). App changes: - workbench_layout.py: build_workbench_main_splitter → 2 panes (workspace | result). - panels.py: left_layout/left_container/_left_scroll re-anchored to the merged workspace pane; sections stacked 输入(top) → 配置(formula/mode_stack) → output_setup → run(bottom); _refresh_main_splitter_left_min_width rewritten for 2 panes and floored at WORKSPACE_CANVAS_MIN_WIDTH (the merged pane holds both input and config, so it needs the wider workspace minimum — this fixes a real visual-contract violation the GUI scan caught). - workbench_visual_contract.py: rewritten to a 2-region contract (merged + result; drops the config region and the 3-way order assert). - tools/scan_desktop_gui_schema.py: horizontal-scrollbar gate scans the merged pane. Tests: new test_desktop_two_pane_layout.py (2 panes, input-above-config, config_rail-not-a-pane, 2-pane visual contract, merged-pane min-width); updated the cataloged 3-pane assertions across workbench_layout, mode_stack, splitter_persistence (stale 3-pane blob now rejected by the 2-pane window), visual_contract (rewrite), screenshots, redesign_scan, shell_layout, workbench_data_area, root_solving_ui. Full desktop suite green (832 passed).
…eRabbit) The compute/latex options-panel audit passed vacuously when a panel attribute was absent (only present-but-empty panels were checked). A refactor that dropped a panel entirely would slip through. Now a missing panel attribute is reported as a schema_binding issue, like an unbound required widget.
4-module change: (1) LaTeX preview dialog with TeX/PDF tabs + copy/save-to-file; (2) tectonic-only compile (drop local-tex fallback + engine combo); (3) compute + LaTeX options as QDialogs (real widgets reparented into the dialog, output-path field removed); (4) result-panel cleanup — 生成TeX/预览PDF buttons replace the TeX/PDF result tabs, delete redundant bottom 开始执行 + empty output_setup_section, collapse history by default. Options dialogs hold the REAL schema-keyed widgets (reachability test forbids hidden state-holders); the LaTeX-preview window uses new display widgets reusing render/gen logic. Verified against live probe + code.
… findings) Codex FAIL → all 5 grounded findings addressed: 1. PDF reuse: _render_pdf_preview is coupled to main-window state (zoom/dpi/ container/last_pdf_path/tab-select) → spec now mandates a PURE pdf_to_images helper + dialog-owned render state. 2. results.latex.source is a schema key, not a payload; modes are file-first and skip tex when output_path empty → temp-path resolution promoted to MANDATORY. 3. Resolved the spec's fresh-widget vs reparent contradiction: options dialogs reparent REAL widgets; the LaTeX-preview window uses fresh widgets + reused logic. 4. Expanded the deletion blast radius (run_button shortcuts/state/lang-restore, latex_engine_combo workspace capture/restore + schema, TeX/PDF tabs in _RESULT_VIEW_ORDER + scanner, output_setup_section in _config_card_sections). 5. Tectonic-only: auto-install via ensure_tectonic_installed (not the prompt-based path); remove worker fallback; rewrite test_desktop_latex_compile_ui.py:114 which asserts the old fallback.
…independence Lead independent recon (while Gemini reviews): (a) shared/pdf_preview_raster.py:234 convert_pdf_to_images already exists — the pure PDF-render helper is wiring, not net-new; tectonic-only applies to tex→PDF compile, the PDF→images preview rasterize still uses pdftoppm/gs. (b) app_web has its OWN compile path (latex_security.py, security.validate_latex_engine) and does NOT import shared/latex_engine.py's desktop fallback — removing the desktop fallback is verified web-safe.
… schema bump) Gemini hung 3x (infra failure, not a review signal) — substituted its role with lead independent verification of its intended items: (2b) statistics/error modes thread generate_latex/output_path identically → temp-path mandate covers all 5 modes; (3) dropping latex_engine_combo's optional 'engine' field from workspace capture/restore is backward-compatible on datalab.workspace.v2 (restore is already null-safe) — NO schema_version bump; old .datalab with engine=pdflatex loads + compiles via tectonic. Design gate: Codex FAIL (5 findings all folded) + independent verification = satisfied.
…odule 2)
compile_latex_to_pdf now always uses tectonic (via _ensure_latex_engine('tectonic'),
which resolves bundled/PATH/auto-installed tectonic) — no latex_engine_combo read, no
pdflatex/xelatex fallback, no local-TeX dependency. Missing tectonic surfaces a clear
'download/install failed, check network' error (no local escape hatch). Removed the
now-dead _latex_compile_fallback_candidates + _resolve_latex_engine_no_prompt helpers;
the worker's fallback params are passed None (inert). Rewrote the 3 fallback/explicit-
engine tests to assert tectonic-only: uses tectonic even with local tex present, and
reports an error (starts no worker) when tectonic is unavailable. 19 latex tests pass.
Note: the latex_engine_combo WIDGET removal + workspace-schema cleanup is Module 3/4
(deletion blast radius); this commit makes the compile LOGIC tectonic-only.
…ule 3) 计算/LaTeX toolbar buttons now open resizable, non-modal QDialog windows instead of inline toggle panels. Each dialog holds the SAME real option controls (reparented once into the dialog's content), so the run pipeline keeps reading self.<widget> unchanged and there are no hidden state-holders/mirrors. The reachability sweep opens the dialogs (a QDialog child is isVisibleTo(window) only while shown) — verified live. NEW app_desktop/options_dialogs.py (OptionsDialog + build/bind helpers + add_separator). Deleted app_desktop/workbench_options_panel.py + tests/test_desktop_toolbar_options_panel.py (superseded by tests/test_desktop_options_dialogs.py). LaTeX output-PATH field removed from the options UI (path chosen at save-time in the TeX window, Module 1): output_file_edit/output_browse_button kept as detached widgets for the save code paths but stripped of their output.latex.path schema binding, so they are not enumerated as reachable config inputs. Dropped the output_path_field/ output_browse_field specs + lbl_output param. Scanner + reachability gate + global-options tests updated to the dialogs. 157 shell/data/mode/workspace tests pass; options-dialog + reachability + schema-scan green; ruff clean.
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
app_desktop/history_panel.py (1)
118-166: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReapply the collapse state after language changes
_apply_language()rewritestitle_labelfrom the registered bare title, so the ▸/▾ prefix is lost on translation switches. Call_apply_history_collapsed()from the language-change path, or keep the arrow out of the registered title text.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/history_panel.py` around lines 118 - 166, The collapse indicator is being overwritten when `_apply_language()` refreshes `title_label`, so the ▸/▾ state is lost after a language switch. Update the language-change flow in `HistoryPanel` to re-run `_apply_history_collapsed()` after `_register_texts()`/title rewrites, or change `title_label` registration so it does not include the arrow prefix; use `toggle_history_collapsed()`, `set_history_collapsed()`, and `_apply_history_collapsed()` as the hooks to keep the collapsed state and label in sync.statistics_utils.py (2)
172-185: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftForward
native_group_widthin the statistics worker path.
generate_statistics_latex()defaults this toTrue, soapp_desktop/workers_core.pywill emitdigit-group-sizewhenever grouped, non-dcolumnLaTeX is generated. The UI path already passesself._engine_supports_group_width(), but the worker path does not, so bundled Tectonic can still hit the unsupported key in background runs.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@statistics_utils.py` around lines 172 - 185, The statistics worker path is not forwarding native_group_width into generate_statistics_latex, so grouped non-dcolumn LaTeX can still emit digit-group-size for bundled Tectonic. Update the worker-side call in workers_core.py to pass the engine capability check result, using the same _engine_supports_group_width logic already used by the UI path, so generate_statistics_latex only enables native grouping when supported.
496-519: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftPropagate the grouping fallback to the remaining statistics writers
generate_statistics_bootstrap_latex,generate_statistics_time_series_latex, andgenerate_statistics_hypothesis_latexstill rely on siunitx for digit grouping; whennative_group_width=Falseandlatex_group_size != 3, they fall back to siunitx’s default 3-digit grouping instead of the requested width. Mirror the app-side fallback used bygenerate_statistics_latex/generate_statistics_latex_batches, and threadnative_group_widththrough the worker callsite too if it still defaults toTrue.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@statistics_utils.py` around lines 496 - 519, The remaining statistics LaTeX writers still ignore the requested grouping width when native_group_width is disabled. Update generate_statistics_bootstrap_latex, generate_statistics_time_series_latex, and generate_statistics_hypothesis_latex to use the same app-side grouping fallback as generate_statistics_latex and generate_statistics_latex_batches instead of relying on siunitx’s default 3-digit grouping, and make sure the worker callsite passes native_group_width through where these generators are invoked. Use the existing symbols _statistics_latex_preamble, latex_group_size, and native_group_width to keep the behavior consistent.
🧹 Nitpick comments (15)
app_desktop/result_overview_popover.py (1)
90-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider using
CARD_PADDINGfor layout consistency.
workbench_results.pywas just refactored to useCARD_PADDINGfromtheme.py, but this new popover uses hardcoded12, 10, 12, 10. If the design-token pass is meant to be consistent across result-overview surfaces, consider importing and usingCARD_PADDINGhere too (or a popover-specific token if the different padding is intentional).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/result_overview_popover.py` at line 90, The popover layout is still using hardcoded contents margins instead of the shared design token, which breaks consistency with the recent padding refactor. Update the result-overview popover setup in the relevant layout initialization to use CARD_PADDING from theme.py, or a dedicated popover padding token if the spacing is intentionally different, and make sure the import and margin assignment stay aligned with the existing card-style surfaces.app_desktop/window_statistics_mixin.py (2)
288-349: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate single/multi-batch LaTeX dispatch logic between run-time and on-demand paths.
generate_statistics_latex_on_demand(Lines 319-346) re-implements the same single-vs-multi-batchgenerate_statistics_latex/generate_statistics_latex_batchesargument-building already in_run_statistics_mode(Lines 506-533). Both blocks were updated together fornative_group_widththis time, but future option additions risk drifting between the two copies.Consider extracting a shared helper, e.g.
_write_statistics_latex(display_batches, rows, sigma_rows, digits, output_path, use_dcolumn, group_size, native, value_col_joined, caption), used by both call sites.Also applies to: 504-535
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/window_statistics_mixin.py` around lines 288 - 349, The single/multi-batch LaTeX dispatch is duplicated between generate_statistics_latex_on_demand and the runtime path in _run_statistics_mode, so future argument changes can drift. Extract the shared single-vs-batch decision and generate_statistics_latex / generate_statistics_latex_batches argument assembly into one helper, then call it from both generate_statistics_latex_on_demand and _run_statistics_mode, keeping native_group_width, caption, group_size, and value_col_joined handling in one place.
318-318: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRepeated engine-capability lookup expression across 8+ sites.
self._engine_supports_group_width() if hasattr(self, "_engine_supports_group_width") else Trueis copy-pasted at every LaTeX generation call site in this file. Extracting a small helper (e.g.self._native_group_width_flag()) would centralize this so future changes to the fallback/probing logic don't need to be replicated everywhere.♻️ Sketch
+ def _native_group_width_flag(self) -> bool: + return self._engine_supports_group_width() if hasattr(self, "_engine_supports_group_width") else True + def generate_statistics_latex_on_demand(self) -> str | None: ... - native = self._engine_supports_group_width() if hasattr(self, "_engine_supports_group_width") else True + native = self._native_group_width_flag()Also applies to: 519-519, 532-532, 641-641, 743-743, 1058-1058, 1289-1289, 1424-1424
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/window_statistics_mixin.py` at line 318, Repeated engine-capability probing for the group-width flag is duplicated across many LaTeX generation call sites in window_statistics_mixin.py. Add a small helper on the same class, such as a private method like _native_group_width_flag(), that encapsulates the hasattr(self, "_engine_supports_group_width") fallback logic and returns the computed boolean. Then update the call sites that currently inline self._engine_supports_group_width() if hasattr(self, "_engine_supports_group_width") else True to use the new helper instead, keeping the behavior identical while centralizing the logic.app_desktop/window_extrapolation_mixin.py (1)
827-831: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCross-module import of private helpers.
_input_units_for_headers/_result_unit_from_unitsare underscore-prefixed (private) helpers from.workers_core, imported directly into this UI mixin. Consider exposing them as public helpers (or moving to a shared units-utils module) to avoid coupling UI code toworkers_core's private surface.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/window_extrapolation_mixin.py` around lines 827 - 831, The UI mixin is importing underscore-prefixed private helpers from workers_core, creating tight cross-module coupling; update window_extrapolation_mixin to stop relying on _input_units_for_headers and _result_unit_from_units directly. Move the shared unit conversion/header logic into a public helper location or a dedicated units-utils module, then import those public symbols here and keep workers_core's private surface internal.app_desktop/result_status_strip.py (1)
17-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider exposing
_overview_state/_status_badgeas public API.Importing underscore-prefixed private functions from
workbench_resultscreates a fragile cross-module coupling. If these are stable internal contracts shared within the package, consider dropping the underscore prefix or providing public wrapper functions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/result_status_strip.py` at line 17, The import in result_status_strip is reaching into underscore-prefixed private helpers from workbench_results, which creates a fragile internal dependency. Update workbench_results to expose _overview_state and _status_badge through a stable public API, either by renaming them to public symbols or adding public wrapper functions, and then switch result_status_strip to import and use those public names instead.datalab_latex/latex_formatting.py (1)
636-679: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting the shared digit-grouping loop.
The int-part (reversed) and frac-part (forward) loops both re-implement "insert
sepeverygroup_sizedigits" — same pattern already present inadd_spacing_to_numberabove. A small shared helper (_group_digits(digits, group_size, sep, reverse)) would cut duplication across both call sites.♻️ Suggested consolidation
+def _group_digit_string(digits: str, group_size: int, sep: str, *, from_left: bool) -> str: + seq = digits if from_left else digits[::-1] + chars: list[str] = [] + for i, ch in enumerate(seq): + if i > 0 and i % group_size == 0: + chars.append(sep) + chars.append(ch) + grouped = "".join(chars) + return grouped if from_left else grouped[::-1] + def group_digits_both_sides(number_str: str, group_size: int, sep: str = "\\,") -> str: ... - grouped_int_chars: list[str] = [] - for i, ch in enumerate(reversed(int_part)): - if i > 0 and i % group_size == 0: - grouped_int_chars.append(sep) - grouped_int_chars.append(ch) - grouped_int = "".join(reversed(grouped_int_chars)) + grouped_int = _group_digit_string(int_part, group_size, sep, from_left=False) result = (sign or "") + grouped_int if frac_part is not None: - grouped_frac_chars: list[str] = [] - for i, ch in enumerate(frac_part): - if i > 0 and i % group_size == 0: - grouped_frac_chars.append(sep) - grouped_frac_chars.append(ch) - result += "." + "".join(grouped_frac_chars) + result += "." + _group_digit_string(frac_part, group_size, sep, from_left=True) return result + (tail or "")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@datalab_latex/latex_formatting.py` around lines 636 - 679, The digit-grouping logic in group_digits_both_sides duplicates the same “insert sep every group_size digits” pattern used elsewhere, so extract that shared behavior into a small helper (for example a private digit-grouping function) and reuse it for both the reversed integer-part loop and the forward fractional-part loop. Keep group_digits_both_sides focused on parsing sign/int/frac/tail and delegating to the helper, and align the new helper with the existing add_spacing_to_number implementation so the grouping behavior stays consistent.app_desktop/theme.py (2)
258-287: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFormula preview dark background still hardcodes
#1f2328in two places.
formula_preview_surface_styleandformula_preview_source_edit_styleboth special-case"#1f2328"for the dark background rather than resolving it through_tok(), duplicating the exact kind of per-function hex literal this token pass was introduced to eliminate. Consider adding a token role (e.g.formula_bg) so the two functions can't drift from each other.♻️ Suggested fix
_TOKENS: dict[str, tuple[str, str]] = { ... "surface_hover": ("`#eef2f7`", "`#303746`"), # button/tab hover + "formula_bg": ("`#ffffff`", "`#1f2328`"), # formula preview / source edit background }def formula_preview_surface_style(*, dark: bool | None = None) -> str: dark = is_dark_theme() if dark is None else bool(dark) - background = "`#1f2328`" if dark else _tok("card_bg", dark) + background = _tok("formula_bg", dark)def formula_preview_source_edit_style(*, dark: bool | None = None) -> str: dark = is_dark_theme() if dark is None else bool(dark) - background = "`#1f2328`" if dark else _tok("card_bg", dark) + background = _tok("formula_bg", dark)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/theme.py` around lines 258 - 287, The formula preview styles still duplicate a hardcoded dark background value, so update `formula_preview_surface_style` and `formula_preview_source_edit_style` to resolve the background through `_tok()` instead of embedding `#1f2328`. Add or reuse a dedicated token role such as `formula_bg` in the theme token lookup so both helpers share the same source of truth and cannot drift apart.
441-452: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTab-corner radius hardcodes
6pxinstead ofRADIUS_CONTROL.Same function uses
{RADIUS_CARD}pxfor the pane border a few lines up (line 437); the tab corner radii at lines 449-450 are literal6pxeven thoughRADIUS_CONTROL = 6exists for exactly this purpose.♻️ Suggested fix
- border-top-left-radius: 6px; - border-top-right-radius: 6px; + border-top-left-radius: {RADIUS_CONTROL}px; + border-top-right-radius: {RADIUS_CONTROL}px;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/theme.py` around lines 441 - 452, The tab corner radius in the input data tabs style is hardcoded instead of using the shared control radius constant. Update the `build_theme`/theme stylesheet section that defines `QTabWidget#input_data_tabs QTabBar::tab` to replace the literal corner radius values with `RADIUS_CONTROL` (consistent with the nearby `RADIUS_CARD` usage), so the tab corners follow the same theme token as other control elements.docs/SWARM_REVIEW_2026.md (1)
262-269: 📐 Maintainability & Code Quality | 🔵 TrivialSelf-flagged truncation should be resolved before merge.
The doc explicitly notes the
__main__/SocketIO finding'srecommendationtext was truncated in the source JSON and only "reasonably completed" by the report author, asking to "核对原始 finding" before relying on it.Want me to help track this down as a follow-up so the recommendation text is verified against the original finding before this report is treated as final?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/SWARM_REVIEW_2026.md` around lines 262 - 269, The issue is that the recommendation text for the `__main__` SocketIO finding was truncated and only reconstructed from context, so it should not be treated as final. Verify the original finding source and replace the placeholder wording in this SWARM review entry with the exact recommendation text, keeping the `__main__`/SocketIO note consistent with the rest of the report.statistics_utils.py (1)
184-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated
app_group/_maybe_grouphelper.The same 6-line closure (
app_group = ...;_maybe_group) is copy-pasted verbatim betweengenerate_statistics_latex(196-201) andgenerate_statistics_latex_batches(341-346). Given this PR's own stated rationale elsewhere for centralizing near-duplicate siunitx logic ("5 near-duplicate inline copies... removing them all keeps the format drift-free"), the same argument applies here.♻️ Extract a shared module-level helper
+def _maybe_group_cell(cell: str, app_group: bool, group_size: int) -> str: + from datalab_latex.latex_formatting import group_digits_both_sides + if app_group and "\\multicolumn" not in cell and "\\text" not in cell: + return "\\text{" + group_digits_both_sides(cell, group_size) + "}" + return cellThen in both
generate_statistics_latexandgenerate_statistics_latex_batches, replace the local closure withfunctools.partial(_maybe_group_cell, app_group=app_group, group_size=group_size)(or just call_maybe_group_cell(cell, app_group, group_size)directly at each call site).Also applies to: 334-399
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@statistics_utils.py` around lines 184 - 257, The duplicated app_group/_maybe_group closure should be removed and centralized so the siunitx grouping behavior stays consistent across both statistics table generators. Extract the shared grouping logic into a module-level helper, then update generate_statistics_latex and generate_statistics_latex_batches to use that helper instead of defining the same local closure twice; keep the existing behavior for app_group, group_size, and _maybe_group call sites unchanged.datalab_latex/sisetup_block.py (1)
26-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDocstring not updated for the new
emit_digit_group_sizeparameter.The
Parameterssection still only documentsgroup_sizeandinclude_dcolumn; the new tri-stateemit_digit_group_size(probe result vs. legacy heuristic) isn't described, even though it's a public function used across five call sites.📝 Add a Parameters entry
include_dcolumn: When ``True`` the document uses dcolumn for alignment; siunitx must NOT emit grouping characters or the alignment breaks. The block emits the minimal "no grouping" body in that case. + emit_digit_group_size: + ``True`` -> engine was probed and honours the key; emit unguarded. + ``False`` -> probed as unsupported; never emit. + ``None`` (default) -> no probe result; fall back to the legacy + ``\`@ifpackagelater``` date heuristic.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@datalab_latex/sisetup_block.py` around lines 26 - 53, Update the docstring for build_sisetup_block to document the new emit_digit_group_size parameter alongside group_size and include_dcolumn; add a Parameters entry explaining the tri-state behavior (True emits digit-group-size, False omits it, None falls back to the legacy heuristic) so the public API matches the implementation used by the call sites.app_desktop/table_copy.py (1)
35-42: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueNo escaping for embedded tabs/newlines in cell text.
If any cell's
text()contains a literal tab or newline, the TSV grid produced here will misalign columns/rows on paste. Low risk for numeric data but worth a defensive check if free-text columns are ever copyable through this path.🧹 Optional: sanitize cell text before joining
for col in range(left, right + 1): item = table.item(row, col) - cells.append(item.text() if item is not None else "") + text = item.text() if item is not None else "" + cells.append(text.replace("\t", " ").replace("\n", " "))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/table_copy.py` around lines 35 - 42, The TSV export in the table copy path does not escape embedded tabs or newlines in cell text, so pasted data can misalign rows and columns. Update the copy routine that builds lines from table.item(...).text() so each cell value is sanitized before joining, replacing or escaping literal tab/newline characters consistently. Keep the fix localized to the table copy helper that appends cells and writes to QApplication.clipboard().setText.app_desktop/workbench_variable_panel.py (2)
259-270: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueClean up dead "constants" role from
_variable_summary_text.Since
_mounts_in_panel_orderno longer includes constants, no section will ever have role"constants". The"constants"key incountsand its entry in the iteration tuple are dead code.♻️ Proposed cleanup
- counts: dict[str, int] = {"parameters": 0, "constants": 0, "unknowns": 0} + counts: dict[str, int] = {"parameters": 0, "unknowns": 0}- for role in ("parameters", "unknowns", "constants"): + for role in ("parameters", "unknowns"):🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/workbench_variable_panel.py` around lines 259 - 270, The `_variable_summary_text` helper still carries dead handling for the `"constants"` role even though `_mounts_in_panel_order` no longer produces it. Remove the unused `"constants"` entry from the `counts` map and drop `"constants"` from the role iteration in `_variable_summary_text`, keeping only the active roles so the summary logic matches the current section roles.
218-218: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove stale
spec.constantsfromhas_variablescheck.Constants are no longer mounted in the variable panel (per
_mounts_in_panel_order), so checkingspec.constantshere is semantically misleading. If a mode has only constants,has_variablesisTruebut no sections are visible — the panel is hidden anyway after unnecessary iteration.♻️ Proposed cleanup
- has_variables = bool(spec and (spec.parameters or spec.tables or spec.constants)) + has_variables = bool(spec and (spec.parameters or spec.tables))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/workbench_variable_panel.py` at line 218, Update the `has_variables` check in `workbench_variable_panel.py` to stop considering `spec.constants`, since constants are no longer mounted by `_mounts_in_panel_order`. Keep the visibility logic aligned with the actual mounted sections by checking only the active variable sources used by the panel (for example, parameters and tables), so `has_variables` reflects what can really be shown and avoids the extra empty iteration path.app_desktop/constants_editor.py (1)
146-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDeduplicate
refresh_theme_styleandset_embedded_in_workbench.Both methods end with the identical
setStyleSheet(constants_editor_style(embedded=...))+unpolish/polishsequence. Haveset_embedded_in_workbenchdelegate torefresh_theme_styleinstead of repeating it.♻️ Proposed dedup
def set_embedded_in_workbench(self, embedded: bool) -> None: embedded = bool(embedded) self.setProperty("datalab_constants_embedded", embedded) layout = self.layout() if layout is not None: # Embedded card now has its own border (like the data card) → pad content off it with # the shared CARD_PADDING; standalone keeps its tighter 8px inset. if embedded: layout.setContentsMargins(*CARD_PADDING) else: layout.setContentsMargins(8, 8, 8, 8) - self.setStyleSheet(constants_editor_style(embedded=embedded)) - self.style().unpolish(self) - self.style().polish(self) + self.refresh_theme_style() def refresh_theme_style(self) -> None: """Re-apply the (theme-dependent) editor style for the current embedded state. Its style is otherwise set once at construction/embedding, so a live light↔dark toggle would leave the button colors stale — the theme refresh calls this.""" embedded = bool(self.property("datalab_constants_embedded")) self.setStyleSheet(constants_editor_style(embedded=embedded)) self.style().unpolish(self) self.style().polish(self)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app_desktop/constants_editor.py` around lines 146 - 168, The style refresh logic is duplicated between set_embedded_in_workbench and refresh_theme_style in ConstantsEditor. Refactor so set_embedded_in_workbench only updates the embedded property and layout margins, then delegates to refresh_theme_style for the shared setStyleSheet/unpolish/polish sequence. Keep refresh_theme_style as the single place that reads datalab_constants_embedded and reapplies constants_editor_style.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app_desktop/fitting_latex_writer.py`:
- Around line 115-131: The fallback grouping in fitting_latex_writer.py is
wrapping grouped values in \text{...}, which breaks rendering when app_group
emits them into a plain r column. Update the _maybe_group helper inside the
formatting function so it returns the result of group_digits_both_sides(cell,
_group) directly, keeping the existing multicolumn/text guards but removing the
extra \text wrapper.
In `@app_desktop/window_extrapolation_mixin.py`:
- Around line 728-867: The on-demand LaTeX generator methods can raise directly
and bypass the existing warning flow. Add try/except handling around
generate_root_latex_on_demand, generate_extrapolation_latex_on_demand, and
generate_error_latex_on_demand (or in
generate_latex_for_current_result/open_latex_preview) so malformed stashes or
bad paths are caught and reported via the same QMessageBox.warning path used by
_write_root_latex_if_requested, using the existing method names to keep the fix
localized.
In `@app_desktop/window_fitting_residuals_mixin.py`:
- Around line 518-576: The on-demand LaTeX regeneration helpers are creating a
fresh temporary file on every call via latex_output_path_for_run(True), which
leaks temp files across repeated refreshes. Update
generate_fitting_latex_on_demand and the other generate_*_latex_on_demand
methods it references to reuse or delete the previous per-kind temp path before
creating a new one, storing the last on-demand path on the instance and
unlinking it safely before requesting a replacement. Keep the fix localized
around latex_output_path_for_run, _load_latex_into_editor, and the
generate_fitting_latex_on_demand-style methods so repeated live option changes
do not accumulate files until window close.
In `@app_desktop/window.py`:
- Around line 3092-3125: The dispatch in generate_latex_for_current_result does
not treat the active statistics result as the preferred stash because
_last_result_kind uses statistics_single/statistics_batches while
generate_statistics_latex_on_demand reads the statistics stash key. Update the
routing so statistics result kinds resolve to the statistics stash key (or track
the active stash key separately) before checking builders, and keep the fallback
order unchanged so the current result always wins over older retained stashes.
In `@app_desktop/workspace_controller.py`:
- Around line 259-280: The capture logic in workspace_controller should decide
file-backed sections from path_text, not the use_file checkbox, so save/restore
stays consistent after a restore. Update the source_kind selection and the
file-read branch in the workspace capture path to treat any non-empty path_text
as the file source, matching the restore behavior that preserves the path even
when use_file_checkbox is unset. Keep the existing fallback to
manual_text/manual_table only when path_text is empty, and ensure the file
attachment is still captured whenever a valid path exists.
In `@tests/test_latex_inputs_serialization.py`:
- Around line 120-158: Add import guards for the Qt-based roundtrip test so it
skips cleanly when dependencies are missing: in
test_workspace_roundtrip_lets_generate_tex_work_after_reopen, add
pytest.importorskip checks for pytestqt and PySide6 before using qtbot or
importing ExtrapolationWindow. Keep the guard placement consistent with the
other Qt-dependent tests in this file so the test is only collected when the
required packages are available.
---
Outside diff comments:
In `@app_desktop/history_panel.py`:
- Around line 118-166: The collapse indicator is being overwritten when
`_apply_language()` refreshes `title_label`, so the ▸/▾ state is lost after a
language switch. Update the language-change flow in `HistoryPanel` to re-run
`_apply_history_collapsed()` after `_register_texts()`/title rewrites, or change
`title_label` registration so it does not include the arrow prefix; use
`toggle_history_collapsed()`, `set_history_collapsed()`, and
`_apply_history_collapsed()` as the hooks to keep the collapsed state and label
in sync.
In `@statistics_utils.py`:
- Around line 172-185: The statistics worker path is not forwarding
native_group_width into generate_statistics_latex, so grouped non-dcolumn LaTeX
can still emit digit-group-size for bundled Tectonic. Update the worker-side
call in workers_core.py to pass the engine capability check result, using the
same _engine_supports_group_width logic already used by the UI path, so
generate_statistics_latex only enables native grouping when supported.
- Around line 496-519: The remaining statistics LaTeX writers still ignore the
requested grouping width when native_group_width is disabled. Update
generate_statistics_bootstrap_latex, generate_statistics_time_series_latex, and
generate_statistics_hypothesis_latex to use the same app-side grouping fallback
as generate_statistics_latex and generate_statistics_latex_batches instead of
relying on siunitx’s default 3-digit grouping, and make sure the worker callsite
passes native_group_width through where these generators are invoked. Use the
existing symbols _statistics_latex_preamble, latex_group_size, and
native_group_width to keep the behavior consistent.
---
Nitpick comments:
In `@app_desktop/constants_editor.py`:
- Around line 146-168: The style refresh logic is duplicated between
set_embedded_in_workbench and refresh_theme_style in ConstantsEditor. Refactor
so set_embedded_in_workbench only updates the embedded property and layout
margins, then delegates to refresh_theme_style for the shared
setStyleSheet/unpolish/polish sequence. Keep refresh_theme_style as the single
place that reads datalab_constants_embedded and reapplies
constants_editor_style.
In `@app_desktop/result_overview_popover.py`:
- Line 90: The popover layout is still using hardcoded contents margins instead
of the shared design token, which breaks consistency with the recent padding
refactor. Update the result-overview popover setup in the relevant layout
initialization to use CARD_PADDING from theme.py, or a dedicated popover padding
token if the spacing is intentionally different, and make sure the import and
margin assignment stay aligned with the existing card-style surfaces.
In `@app_desktop/result_status_strip.py`:
- Line 17: The import in result_status_strip is reaching into
underscore-prefixed private helpers from workbench_results, which creates a
fragile internal dependency. Update workbench_results to expose _overview_state
and _status_badge through a stable public API, either by renaming them to public
symbols or adding public wrapper functions, and then switch result_status_strip
to import and use those public names instead.
In `@app_desktop/table_copy.py`:
- Around line 35-42: The TSV export in the table copy path does not escape
embedded tabs or newlines in cell text, so pasted data can misalign rows and
columns. Update the copy routine that builds lines from table.item(...).text()
so each cell value is sanitized before joining, replacing or escaping literal
tab/newline characters consistently. Keep the fix localized to the table copy
helper that appends cells and writes to QApplication.clipboard().setText.
In `@app_desktop/theme.py`:
- Around line 258-287: The formula preview styles still duplicate a hardcoded
dark background value, so update `formula_preview_surface_style` and
`formula_preview_source_edit_style` to resolve the background through `_tok()`
instead of embedding `#1f2328`. Add or reuse a dedicated token role such as
`formula_bg` in the theme token lookup so both helpers share the same source of
truth and cannot drift apart.
- Around line 441-452: The tab corner radius in the input data tabs style is
hardcoded instead of using the shared control radius constant. Update the
`build_theme`/theme stylesheet section that defines `QTabWidget#input_data_tabs
QTabBar::tab` to replace the literal corner radius values with `RADIUS_CONTROL`
(consistent with the nearby `RADIUS_CARD` usage), so the tab corners follow the
same theme token as other control elements.
In `@app_desktop/window_extrapolation_mixin.py`:
- Around line 827-831: The UI mixin is importing underscore-prefixed private
helpers from workers_core, creating tight cross-module coupling; update
window_extrapolation_mixin to stop relying on _input_units_for_headers and
_result_unit_from_units directly. Move the shared unit conversion/header logic
into a public helper location or a dedicated units-utils module, then import
those public symbols here and keep workers_core's private surface internal.
In `@app_desktop/window_statistics_mixin.py`:
- Around line 288-349: The single/multi-batch LaTeX dispatch is duplicated
between generate_statistics_latex_on_demand and the runtime path in
_run_statistics_mode, so future argument changes can drift. Extract the shared
single-vs-batch decision and generate_statistics_latex /
generate_statistics_latex_batches argument assembly into one helper, then call
it from both generate_statistics_latex_on_demand and _run_statistics_mode,
keeping native_group_width, caption, group_size, and value_col_joined handling
in one place.
- Line 318: Repeated engine-capability probing for the group-width flag is
duplicated across many LaTeX generation call sites in
window_statistics_mixin.py. Add a small helper on the same class, such as a
private method like _native_group_width_flag(), that encapsulates the
hasattr(self, "_engine_supports_group_width") fallback logic and returns the
computed boolean. Then update the call sites that currently inline
self._engine_supports_group_width() if hasattr(self,
"_engine_supports_group_width") else True to use the new helper instead, keeping
the behavior identical while centralizing the logic.
In `@app_desktop/workbench_variable_panel.py`:
- Around line 259-270: The `_variable_summary_text` helper still carries dead
handling for the `"constants"` role even though `_mounts_in_panel_order` no
longer produces it. Remove the unused `"constants"` entry from the `counts` map
and drop `"constants"` from the role iteration in `_variable_summary_text`,
keeping only the active roles so the summary logic matches the current section
roles.
- Line 218: Update the `has_variables` check in `workbench_variable_panel.py` to
stop considering `spec.constants`, since constants are no longer mounted by
`_mounts_in_panel_order`. Keep the visibility logic aligned with the actual
mounted sections by checking only the active variable sources used by the panel
(for example, parameters and tables), so `has_variables` reflects what can
really be shown and avoids the extra empty iteration path.
In `@datalab_latex/latex_formatting.py`:
- Around line 636-679: The digit-grouping logic in group_digits_both_sides
duplicates the same “insert sep every group_size digits” pattern used elsewhere,
so extract that shared behavior into a small helper (for example a private
digit-grouping function) and reuse it for both the reversed integer-part loop
and the forward fractional-part loop. Keep group_digits_both_sides focused on
parsing sign/int/frac/tail and delegating to the helper, and align the new
helper with the existing add_spacing_to_number implementation so the grouping
behavior stays consistent.
In `@datalab_latex/sisetup_block.py`:
- Around line 26-53: Update the docstring for build_sisetup_block to document
the new emit_digit_group_size parameter alongside group_size and
include_dcolumn; add a Parameters entry explaining the tri-state behavior (True
emits digit-group-size, False omits it, None falls back to the legacy heuristic)
so the public API matches the implementation used by the call sites.
In `@docs/SWARM_REVIEW_2026.md`:
- Around line 262-269: The issue is that the recommendation text for the
`__main__` SocketIO finding was truncated and only reconstructed from context,
so it should not be treated as final. Verify the original finding source and
replace the placeholder wording in this SWARM review entry with the exact
recommendation text, keeping the `__main__`/SocketIO note consistent with the
rest of the report.
In `@statistics_utils.py`:
- Around line 184-257: The duplicated app_group/_maybe_group closure should be
removed and centralized so the siunitx grouping behavior stays consistent across
both statistics table generators. Extract the shared grouping logic into a
module-level helper, then update generate_statistics_latex and
generate_statistics_latex_batches to use that helper instead of defining the
same local closure twice; keep the existing behavior for app_group, group_size,
and _maybe_group call sites unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8a1aab45-9fc4-4976-90e5-477f92c5b751
📒 Files selected for processing (127)
app_desktop/constants_editor.pyapp_desktop/current_page_stack.pyapp_desktop/fitting_latex_writer.pyapp_desktop/formula_preview.pyapp_desktop/history_panel.pyapp_desktop/history_popup.pyapp_desktop/latex_inputs_serialization.pyapp_desktop/latex_preview_dialog.pyapp_desktop/message_dialogs.pyapp_desktop/options_dialogs.pyapp_desktop/panels.pyapp_desktop/result_overview_popover.pyapp_desktop/result_status_strip.pyapp_desktop/root_latex_writer.pyapp_desktop/shell_layout.pyapp_desktop/table_copy.pyapp_desktop/theme.pyapp_desktop/views/error.pyapp_desktop/views/fitting.pyapp_desktop/views/helpers.pyapp_desktop/views/root_solving.pyapp_desktop/views/statistics.pyapp_desktop/window.pyapp_desktop/window_data_mixin.pyapp_desktop/window_extrapolation_mixin.pyapp_desktop/window_fitting_formatters_mixin.pyapp_desktop/window_fitting_models_mixin.pyapp_desktop/window_fitting_residuals_mixin.pyapp_desktop/window_i18n_mixin.pyapp_desktop/window_latex_compile_mixin.pyapp_desktop/window_statistics_mixin.pyapp_desktop/workbench_formula_panel.pyapp_desktop/workbench_layout.pyapp_desktop/workbench_results.pyapp_desktop/workbench_toolbar.pyapp_desktop/workbench_variable_panel.pyapp_desktop/workbench_visual_contract.pyapp_desktop/workers_core.pyapp_desktop/workspace_controller.pydatalab_latex/latex_formatting.pydatalab_latex/latex_tables_common.pydatalab_latex/latex_tables_error_propagation.pydatalab_latex/latex_tables_extrapolation.pydatalab_latex/latex_tables_fitting.pydatalab_latex/latex_tables_root.pydatalab_latex/latex_tables_statistics_grouped.pydatalab_latex/latex_tables_statistics_matrix.pydatalab_latex/sisetup_block.pydocs/DATALAB_WEB_GUIDE.en.mddocs/DATALAB_WEB_GUIDE.mddocs/SWARM_REVIEW_2026.mddocs/superpowers/specs/2026-07-03-adaptive-workbench-section1-design.mddocs/superpowers/specs/2026-07-03-adaptive-workbench-section2-plan.mddocs/superpowers/specs/2026-07-04-iconified-menubar-design.mddocs/superpowers/specs/2026-07-04-toolbar-options-popup-design.mddocs/superpowers/specs/2026-07-05-latex-ondemand-4.2-integration-notes.mddocs/superpowers/specs/2026-07-05-latex-ondemand-4.4b-result-tabs-removal.mddocs/superpowers/specs/2026-07-05-latex-ondemand-4.4d-remove-generate-checkbox.mddocs/superpowers/specs/2026-07-05-latex-ondemand-refactor-design.mddocs/superpowers/specs/2026-07-05-latex-pdf-window-cleanup-design.mddocs/superpowers/specs/2026-07-05-two-pane-layout-design.mddocs/superpowers/specs/2026-07-06-engine-capability-grouping.mddocs/superpowers/specs/2026-07-08-left-config-card-unify-design.mddocs/web/deploy.en.mddocs/web/deploy.zh.mdgunicorn.conf.pyshared/latex_engine.pyshared/ui_specs.pystatistics_utils.pytest_probe_widget_tree.pytests/test_constants_editor.pytests/test_deploy_docs_wsgi_targets.pytests/test_desktop_about_dialog.pytests/test_desktop_bilingual_inventory.pytests/test_desktop_custom_fit_ui.pytests/test_desktop_engine_group_width.pytests/test_desktop_error_propagation_ui.pytests/test_desktop_example_workspace_menu.pytests/test_desktop_global_options_ui.pytests/test_desktop_gui_redesign_scan.pytests/test_desktop_gui_schema_scan.pytests/test_desktop_gui_workflows.pytests/test_desktop_history_collapse.pytests/test_desktop_history_toolbar_popup.pytests/test_desktop_input_constants_tabs.pytests/test_desktop_latex_compile_ui.pytests/test_desktop_latex_ondemand_dispatch.pytests/test_desktop_latex_ondemand_error.pytests/test_desktop_latex_ondemand_extrapolation.pytests/test_desktop_latex_ondemand_fitting.pytests/test_desktop_latex_ondemand_root.pytests/test_desktop_latex_ondemand_statistics.pytests/test_desktop_latex_preview_dialog.pytests/test_desktop_message_dialogs.pytests/test_desktop_mode_selector_on_toolbar.pytests/test_desktop_mode_stack.pytests/test_desktop_option_reachability.pytests/test_desktop_options_dialogs.pytests/test_desktop_result_overview_popover.pytests/test_desktop_result_schema_ui.pytests/test_desktop_result_status_strip.pytests/test_desktop_root_solving_ui.pytests/test_desktop_shell_layout.pytests/test_desktop_statistics_ui.pytests/test_desktop_table_cell_copy.pytests/test_desktop_theme_tokens.pytests/test_desktop_toolbar_status_overview.pytests/test_desktop_two_pane_layout.pytests/test_desktop_workbench_data_area.pytests/test_desktop_workbench_editor_canvas.pytests/test_desktop_workbench_layout.pytests/test_desktop_workbench_results.pytests/test_desktop_workbench_state_ownership.pytests/test_desktop_workbench_variable_panel.pytests/test_desktop_workbench_visual_contract.pytests/test_desktop_workbench_visual_screenshots.pytests/test_file_size_ratchet.pytests/test_fitting_latex_writer.pytests/test_formula_preview_rendering.pytests/test_latex_engine_adaptive_review_fixes.pytests/test_latex_engine_capability.pytests/test_latex_group_size_zero.pytests/test_latex_inputs_serialization.pytests/test_sisetup_block.pytests/test_splitter_persistence.pytests/test_workspace_controller.pytools/scan_desktop_gui_schema.py
💤 Files with no reviewable changes (8)
- app_desktop/views/statistics.py
- tests/test_desktop_workbench_state_ownership.py
- app_desktop/views/fitting.py
- app_desktop/views/root_solving.py
- tests/test_desktop_custom_fit_ui.py
- tests/test_desktop_statistics_ui.py
- app_desktop/views/helpers.py
- app_desktop/views/error.py
| # Only claim source_kind="file" when there is an actual path (review P-C): use_file with an | ||
| # empty path used to tag the section "file" while capturing the manual table → inconsistent | ||
| # state with no attachment. Fall back to the manual kind so save/restore stay coherent. | ||
| _text_view = stack is not None and stack.currentIndex() == 1 | ||
| source_kind = ( | ||
| "file" if (use_file and path_text) else ("manual_text" if _text_view else "manual_table") | ||
| ) | ||
| if constants and editor is not None and not use_file: | ||
| source_kind = "manual_text" if editor.using_text_view() else "manual_table" | ||
| canonical: dict[str, Any] | ||
| if use_file and path_text: | ||
| raw = Path(path_text).read_bytes() | ||
| # Guard the file read (review P-A): if the file was moved/deleted/unreadable since the | ||
| # user picked it, saving the workspace must NOT crash. Keep the source as "file" and the | ||
| # path (so the user can re-point it), but attach empty content rather than raising. | ||
| try: | ||
| raw = Path(path_text).read_bytes() | ||
| except OSError: | ||
| raw = b"" | ||
| decoded_text, encoding = _decode_bytes(raw) | ||
| raw_path = f"attachments/sources/{section_name}.bin" | ||
| attachments[raw_path] = raw | ||
| canonical = {"rows": []} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Capture code must derive file source from path_text, not use_file_checkbox, to match file-precedence restore behavior.
The restore path (line 1926) unconditionally sets use_file_checkbox.setChecked(False) and keeps the file path only if the file still exists. But the capture path still gates on use_file (the checkbox): source_kind = "file" if (use_file and path_text) else ... and if use_file and path_text: for the file read.
After a restore where the file exists, the checkbox is False and the path is set. On the next save, the capture sees use_file=False, so it:
- Tags
source_kindas"manual_table"/"manual_text"instead of"file". - Does not read the file — no attachment is saved.
On a subsequent restore where the file has since been moved/deleted, the path is cleared and the workspace falls back to the manual editor's data (possibly empty or stale), with no file attachment. The restore comment claims "its CONTENTS were captured as an attachment on save → the workspace stays self-contained," but this only holds for the initial save — after a restore→save cycle, the workspace is no longer self-contained.
The fix is to gate the file source on path_text instead of use_file:
🔒 Proposed fix
_text_view = stack is not None and stack.currentIndex() == 1
source_kind = (
- "file" if (use_file and path_text) else ("manual_text" if _text_view else "manual_table")
+ "file" if path_text else ("manual_text" if _text_view else "manual_table")
)
if constants and editor is not None and not use_file:
source_kind = "manual_text" if editor.using_text_view() else "manual_table"
canonical: dict[str, Any]
- if use_file and path_text:
+ if path_text:
# Guard the file read (review P-A): if the file was moved/deleted/unreadable since the
# user picked it, saving the workspace must NOT crash. Keep the source as "file" and the
# path (so the user can re-point it), but attach empty content rather than raising.This aligns the capture with the restore's file-precedence model: the file path is the sole determinant of whether the section is file-backed, not the legacy checkbox.
A regression test for the second save-restore cycle (restore → save → delete file → restore) should be added to confirm the workspace remains self-contained.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app_desktop/workspace_controller.py` around lines 259 - 280, The capture
logic in workspace_controller should decide file-backed sections from path_text,
not the use_file checkbox, so save/restore stays consistent after a restore.
Update the source_kind selection and the file-read branch in the workspace
capture path to treat any non-empty path_text as the file source, matching the
restore behavior that preserves the path even when use_file_checkbox is unset.
Keep the existing fallback to manual_text/manual_table only when path_text is
empty, and ensure the file attachment is still captured whenever a valid path
exists.
- [Critical] fitting_latex_writer: the app-side group fallback wrapped grouped values in
\text{...} while emitting them into a plain r column → the \, thin-spaces broke TeX
compilation. Return the grouped string directly (CodeRabbit).
- [Major] generate_latex_for_current_result: _last_result_kind is granular for statistics
(statistics_single/_batches/_grouped/…) but they share the single "statistics" builder+stash;
collapse any statistics* kind to "statistics" so the current result wins dispatch instead of
falling through to a stale stash.
- [Major] open_latex_preview: wrap generate_latex_for_current_result() in try/except (Value/OS/
Runtime) → surface a warning dialog instead of letting a builder exception escape the button slot.
- [Minor] latex_output_path_for_run(reuse=True): on-demand regeneration reused one temp .tex per
session instead of leaking a fresh NamedTemporaryFile per call; run path still allocates fresh.
- [Minor] test_latex_inputs_serialization: importorskip pytestqt/PySide6 inside the one qtbot test
so it skips cleanly (not fixture-not-found) without gating the non-Qt tests.
Refuted (verified against code): capture "not self-contained after restore→save" — the
_FilePathChecked shim makes use_file == (path present), so a restore-where-file-exists→re-save
still records source_kind=file + saves the attachment (reproduced: attachment present). 49 latex/
serialization/on-demand tests pass.
|
Addressed the CodeRabbit review in
49 latex / serialization / on-demand-dispatch tests pass locally. |
…laceholder (CI gate) CI's runtime bilingual/accessibility gate (test_desktop_bilingual_inventory) failed: the new data_file_edit / constants_file_edit QLineEdits had only a one-time placeholder and no tooltip/accessibility affordance, so the gate flagged 48 "lacks tooltip/description/help" items and 24 "placeholder unchanged across zh/en" items. Registered a bilingual tooltip (via _register_text setToolTip) and switched the placeholder to a registered bilingual text (setPlaceholderText) so it re-translates on language switch. Gate passes.
…g modal Root cause of the CI hang (test_desktop_workbench_results.py, serial): commit 5dc9f60 added job.target_column / job.variable_map access to the fit-batches SUCCESS path (for on-demand-TeX fidelity), but two test mock jobs didn't carry those attributes. That raised AttributeError in the success path → the except branch showed a modal QMessageBox.critical, which BLOCKS FOREVER in offscreen/headless CI (no one clicks OK) → the whole serial run hung with no timeout. (I'd been masking this locally by --ignore-ing the file.) Fixes: - Added an autouse fixture that makes QMessageBox critical/warning/information non-blocking for every test in the file — a modal must never block a headless run. - Gave the two affected mock jobs the target_column + variable_map the success-path stash reads, so they exercise their intended (post-processing-error / success) paths instead of failing early. Full file: 66 passed, no hang.
Full serial CI run (now that the modal hang is fixed) surfaced 4 failures — 3 real quality-gate violations from this branch's work + 1 order-dependent flake: - precision guardrail: latex_inputs_serialization used mp.workdps() (forbidden — mp.dps is process-global). Switched both mpf-reconstruction sites to precision_guard(_MPF_RECONSTRUCT_DPS, clamp_max=MAX_MPMATH_DPS), the sanctioned guard; the S1 lossless round-trip still passes. - file-size ratchet: theme.py crossed the 800-line soft limit during the design-review token pass (baselined it, +1 entry); window.py/panels.py/workspace_controller.py grew past their frozen god-file baselines from this feature's approved work (raised to current: 3467/2478/2130). The formula-preview contrast test passed both in isolation and after these fixes — it was an order-dependent flake in the long serial run, not a real failure.
…stale error
_update_theme_from_palette is a Qt slot wired to the app-global paletteChanged signal. In the full
test suite, stale ExtrapolationWindow instances from earlier tests stay connected to that signal, so
a later test's setPalette() fires their handlers too — and any window carrying a cached
formula-population RuntimeError re-raised it INTO the Qt event loop ("CALL ERROR: Exceptions caught
in Qt event loop"), failing an unrelated test. A palette change is purely cosmetic; the slot now
catches + logs instead of letting the exception escape. The real population error is still surfaced
at its own call site. Deterministic repro (formula_panel then preview_dialog): was 1 failed → 74
passed.
…m-independent) CI failed on test_constants_tab_only_in_constant_using_modes: it asserts Chinese tab titles (["输入数据","常数"]) but a fresh ExtrapolationWindow follows QLocale.system() — zh on a dev Mac, en on Linux CI — so it got ["Data input","Constants"] on CI. The test never pinned the language. The _window helper now applies "zh" so every title assertion in the file is deterministic regardless of the host locale; the retranslate test re-applies "en" after and still passes. Reproduced the CI failure locally via LANG=en_US.UTF-8 and confirmed the fix under that locale.
…, high) mp.dps is process-global and each web compute route holds a serial lock while it runs, so an unbounded precision field (e.g. mp_precision=100000000) set the process dps to 100M and stalled the worker — a trivial unauthenticated DoS (CSRF token is freely obtainable; there is no auth). The desktop worker and the SSE path already clamp; the direct compute POST routes did not. Added _parse_precision() in app_web/logic/common.py that clamps at parse time to [MIN_MPMATH_DPS, MAX_MPMATH_DPS], and routed the *_mp_precision field of all five compute logic modules (extrapolation/error_propagation/statistics/root_solving/fitting) through it instead of the unclamped _parse_int. Regression tests assert the clamp + that every route uses it.
…high) The fixed-place LaTeX formatters ran at the AMBIENT mp.dps. The on-demand TeX rebuild (生成 TeX) formats a stashed high-precision result AFTER the run's own precision_guard has closed, so the ambient dps is the process default (~15) while `places` is 20-200 — the intermediate mp.power(10,places) / value*factor products silently lost every digit past ~16, corrupting the toolkit's headline high-precision guarantee (e.g. sqrt(2)@50 places off from digit ~17). Rather than guard each of the ~dozen on-demand/web callers, fixed it at the ROOT: _round_to_places and _format_fixed_places now floor their working precision to places + 12 guard digits via precision_guard, so they are self-protecting regardless of the caller's ambient dps. Verified the exact audit scenario (60-digit sqrt(2) formatted at ambient dps=15 now matches dps=60 and mpmath's own reference). 207 latex/format tests pass.
…e save (audit A4, high) capture_workspace wrote the tex-rebuild stash (_last_latex_inputs) into manifest[latex_inputs], but the budget trimmer only ever shrank history — never latex_inputs. A high-dps fit with many points encodes to several MiB, over the 2 MiB manifest budget, so write_workspace raised "manifest exceeds size limit" and the ENTIRE workspace became unsaveable (risking loss of the user's work). Since the stash is a best-effort convenience (optional on decode; the user re-runs to regenerate tex on reopen), capture now measures the manifest and drops latex_inputs when it would exceed the budget, so no valid save ever hard-fails. Small stashes are retained. Regression test added.
…, medium) _compute_covariance already guards `noise = chi2/dof if dof > 0 else mp.nan`, but the caller passed `dof if dof > 0 else 1`, pre-substituting 1 so the guard never fired: for a k-parameter model fit to exactly k points (dof=0, exact interpolation, chi2~0) it computed noise=chi2/1~0 → sqrt(~0)~0, a spuriously precise near-zero uncertainty. The linear auto_models path returns NaN in the same case. Now passes the true dof so the covariance guard yields NaN uncertainties for an exactly-determined fit, matching the linear path. Regression: 2-param fit to 2 points → dof=0 → NaN errors.
…A7, medium) The heading-id injection used r"<(h[123])>(.+?)</\\1>" — in a raw string \\1 is a literal backslash+1, so the closing-tag pattern was the literal text </\1>, which never matches real </h1>. No id attributes were emitted, so every docs-page TOC in-page link (href="#slug") was dead. Changed \\1 to a real backreference \1. Regression asserts heading ids are emitted and every TOC anchor resolves to one.
…t-detail tabs (audit B5, medium)
input_data_tabs_style hardcoded four dark greys (#1c2129/#161a21/#222833/#2a313c) while its sibling
result_detail_card_style derives every surface from _tok(...). The P1 token pass missed these, so in
dark mode the 输入数据/常数 tab strip visibly mismatched the result-detail tab strip across the
splitter — the exact per-role divergence the token pass was meant to remove, and which this
function's own docstring claims it avoids ("mirrors the result-detail tab chrome"). Now resolves
panel_bg→card_bg, tab_bg→surface_raised, tab_hover→surface_hover (identical mapping to the detail
strip). Updated the theme test that pinned the old #1c2129 literal.
The 5 compute routes (/ /error /fit /stats /roots) had only @csrf_protect — no rate limit — while each runs an mpmath computation holding a process-global serial lock, so an attacker hammering them starves legitimate users (the SSE limiter covered only the two GET streaming routes). Added a pages-blueprint before_request that throttles POST (the heavy path) per-IP, reusing the SSE blueprint's battle-tested sliding-window limiter (and its TESTING / DATALAB_SSE_DISABLE_RATE_LIMIT bypasses); GET form render is untouched. Over budget → 429. (A1 already bounded per-request cost by clamping precision.) Also dropped the now-stale theme.py file-size baseline (B5 shrank it back to 800). Regression: POST throttled, GET not, TESTING-bypass preserved.
…he junk auxiliary (audit A6) In mpmath's Wynn-epsilon table the ODD columns are non-convergent auxiliary entries that diverge to huge junk magnitudes (~82 for a Leibniz pi/4 sequence). The cancellation_indicator used |last_row[-2]| (the junk aux) and the 2-element-row error_estimate used |last[-1]-last[-2]| — so the reported cancellation/error diagnostic was garbage, and (via model_selector) a 3-input auto-fit could get a wildly inflated statistical uncertainty. Both diagnostics now derive from the proper convergent gap |last[-1]-last[-3]|; a 2-element last row (3-input sequence) has no previous convergent so neither is emitted (model_selector falls back to sqrt(noise)). Regression added.
…tative rows (audit A12) A workspace saved in TABLE view stores both the rows and a (possibly stale) text-view draft. _restore_constants_editor_state restored that draft via set_raw_text, which stamps it in-sync with the table — defeating the editor's anti-stale guard, so a later table→text toggle surfaced the stale draft instead of regenerating from the restored rows. Now the stored text is only restored when the workspace was saved in TEXT view (where it is authoritative); in TABLE view the rows are authoritative and the text is left out-of-sync, so a toggle regenerates from them. Regression: table-view restore regenerates from rows (no stale draft); text-view restore keeps its custom text.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_app_web_docs_baseline.py (1)
88-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider making the heading-ID and TOC-anchor regexes more robust.
The heading-ID regex
<h[123]\s+id="([^"]+)"requiresidto be the first attribute after the tag name. If the template ever emits<h2 class="..." id="slug">, the regex won't capture it, potentially causing false failures in thetoc_targets <= heading_idsassertion. Similarly,href="#([^"]+)"captures all in-page anchors, not just TOC links — any non-TOC anchor (footnote, nav) pointing to a non-heading element would fail the subset check.If the current HTML output always puts
idfirst and the page has no non-TOC anchors, the test is fine today. Consider tightening the patterns for long-term robustness.♻️ Suggested more robust regex patterns
- heading_ids = set(re.findall(r'<h[123]\s+id="([^"]+)"', html)) + heading_ids = set(re.findall(r'<h[123]\s[^>]*\bid="([^"]+)"', html)) assert heading_ids, "no heading id= attributes were emitted (TOC anchors would be dead)" # Every TOC anchor (href="`#slug`") must point at a heading id that exists on the page. - toc_targets = set(re.findall(r'href="#([^"]+)"', html)) + toc_targets = set(re.findall(r'<a\s[^>]*\bhref="#([^"]+)"', html))The
\bid=pattern matchesidregardless of attribute position, and restricting to<atags narrows to actual anchor elements.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_app_web_docs_baseline.py` around lines 88 - 104, Make the regex assertions in test_docs_headings_get_ids_so_toc_anchors_resolve robust to HTML attribute ordering and anchor context: match heading id attributes using a word boundary so id need not be first, and restrict href extraction to anchor tags while preserving fragment matching. Update the patterns so valid headings with other attributes and relevant TOC links are handled without false failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/test_app_web_docs_baseline.py`:
- Around line 88-104: Make the regex assertions in
test_docs_headings_get_ids_so_toc_anchors_resolve robust to HTML attribute
ordering and anchor context: match heading id attributes using a word boundary
so id need not be first, and restrict href extraction to anchor tags while
preserving fragment matching. Update the patterns so valid headings with other
attributes and relevant TOC links are handled without false failures.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9dfe179a-016a-492c-8ad4-f1e2ed109c22
📒 Files selected for processing (24)
app_desktop/theme.pyapp_desktop/workspace_controller.pyapp_web/blueprints/docs.pyapp_web/blueprints/pages.pyapp_web/logic/common.pyapp_web/logic/error_propagation.pyapp_web/logic/extrapolation.pyapp_web/logic/fitting.pyapp_web/logic/root_solving.pyapp_web/logic/statistics.pydatalab_latex/latex_formatting.pyextrapolation_methods/accelerators.pyfitting/hp_fitter.pytests/test_app_web_compute_rate_limit.pytests/test_app_web_docs_baseline.pytests/test_app_web_precision_clamp.pytests/test_constants_text_view.pytests/test_desktop_input_constants_tabs.pytests/test_desktop_theme_tokens.pytests/test_extrapolation_accelerators.pytests/test_file_size_ratchet.pytests/test_fitting_linear_model_sanity.pytests/test_latex_formatting_precision_floor.pytests/test_workspace_controller.py
💤 Files with no reviewable changes (1)
- tests/test_file_size_ratchet.py
🚧 Files skipped from review as they are similar to previous changes (4)
- tests/test_workspace_controller.py
- tests/test_desktop_theme_tokens.py
- app_desktop/workspace_controller.py
- tests/test_desktop_input_constants_tabs.py
mp.dps is typed Any, so max(mp.dps, ...) returned Any while the function declares -> int, failing the core-layer mypy-strict CI gate. Cast int(mp.dps) so the return is typed int. No behavior change; A3 precision-floor tests still pass.
…ombo The quadratic (默认三点公式) accelerator was already a recognized THREE_POINT_METHOD in the compute engine and had a full description in get_method_description, but the desktop method combo never offered it. Add the QUADRATIC_PARAMS form_section (empty — the method derives its three values A/B/C from the data columns, no user params), the 'quadratic' MethodSpec, and place it after 'power_law' in METHOD_DISPLAY_ORDER (both are three-point methods). Run wiring is generic, so no branch changes in the mixins, window, or datalab_core. Update the exact-order combo assertion in test_desktop_extrapolation_ui.
…ckend Wires the desktop's self_consistent/implicit fitting mode into the web /fit form by reusing the pure-math fitting.FitRunner() directly (the core session service deliberately rejects self_consistent, so this mode bypasses it, same pattern as the existing comparison-mode branch). - app_web/logic/fitting.py: new _build_self_consistent_problem() helper parses/validates the implicit-model form fields (equation, implicit variable, output expression, solve options, optional params JSON) and builds ModelProblem + ImplicitModelDefinition; a new elif branch in _run_fit() calls it then FitRunner().fit(), guarding the core-service dispatch with `if fit_res is None:` so the shared LaTeX/CSV/plot tail works unchanged for both paths. - app_web/templates/fit.html + static/js/i18n.js: new self_consistent option and field block (equation/variable/output/params/solve-options), bilingual i18n keys for both languages. - shared/ui_specs.py: FITTING_MODEL_FIELD choices corrected from the stale 3-mode list to the real 7-mode set the frontends offer. - tests/test_app_web_fitting_self_consistent.py: positive recovery test (known implicit model, recovers a=2 exactly) + 3 bilingual validation tests. - tests/test_auto_fit_removed.py: updated the now-obsolete assertion that self_consistent was NOT exposed on web. - tests/test_file_size_ratchet.py: consciously raised app_web/logic/fitting.py's baseline (1143 -> 1238) for the new implicit-model helper + branch.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app_web/logic/fitting.py`:
- Around line 254-265: In _build_self_consistent_problem, keep JSON loading
inside the try block but move the params_cfg dictionary validation and
normalized_cfg construction after it, so the intentional ValueError for
non-object JSON is not caught and wrapped again. Preserve exception chaining and
bilingual parsing-error handling for JSON load failures.
In `@shared/ui_specs.py`:
- Around line 360-370: The help metadata is missing the newly supported
self_consistent fit model. Add a self_consistent entry to shared/help_specs.json
matching the model’s identifier and description defined near the self_consistent
UI specification, keeping the desktop and web help content consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d33f42b5-4ca1-492d-ad5f-19e9f73ca94f
📒 Files selected for processing (9)
app_web/logic/fitting.pyapp_web/static/js/i18n.jsapp_web/templates/fit.htmldatalab_latex/latex_formatting.pyshared/ui_specs.pytests/test_app_web_fitting_self_consistent.pytests/test_auto_fit_removed.pytests/test_desktop_extrapolation_ui.pytests/test_file_size_ratchet.py
✅ Files skipped from review due to trivial changes (1)
- tests/test_file_size_ratchet.py
🚧 Files skipped from review as they are similar to previous changes (1)
- datalab_latex/latex_formatting.py
| # quadratic ("默认三点公式") derives its three values from the data columns themselves, so it takes | ||
| # no tunable parameters (empty group, like shanks/wynn_epsilon). Added so the desktop offers the | ||
| # backend's default three-point method that the web already exposes (audit B3). | ||
| QUADRATIC_PARAMS = form_section( | ||
| key="quadratic_params", | ||
| title_zh="默认三点公式", | ||
| title_en="Default three-point formula", | ||
| fields=[], | ||
| visible_when=VisibilityRule.equals("method", "quadratic"), | ||
| ) | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify help_specs.json was updated for new user-visible parameters
if [ -f shared/help_specs.json ]; then
echo "=== Checking for 'quadratic' ==="
rg -n 'quadratic' shared/help_specs.json || echo "No 'quadratic' entries found"
echo "=== Checking for 'self_consistent' ==="
rg -n 'self_consistent' shared/help_specs.json || echo "No 'self_consistent' entries found"
else
echo "shared/help_specs.json not found"
fiRepository: yilibinbin/DataLab
Length of output: 286
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file list =="
git ls-files 'shared/ui_specs.py' 'shared/help_specs.json'
echo
echo "== ui_specs.py around cited lines =="
sed -n '340,430p' shared/ui_specs.py
echo
echo "== help_specs.json around quadratic =="
sed -n '100,150p' shared/help_specs.json
echo
echo "== search for self_consistent across repo =="
rg -n 'self_consistent|self-consistent|self consistent' shared . --glob '!**/.git/**'Repository: yilibinbin/DataLab
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== help_specs fitting-related keys =="
rg -n '"(fitting|fit|model|self_consistent|quadratic|custom)"\s*:' shared/help_specs.json
echo
echo "== help_specs around any fitting section =="
python3 - <<'PY'
import json
from pathlib import Path
data = json.loads(Path("shared/help_specs.json").read_text(encoding="utf-8"))
for k in data:
if any(s in k.lower() for s in ("fit", "model", "quadratic", "custom", "self")):
print(k)
PYRepository: yilibinbin/DataLab
Length of output: 281
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== ui_specs.py around 440-560 =="
sed -n '440,560p' shared/ui_specs.py
echo
echo "== full help_specs top-level keys =="
python3 - <<'PY'
import json
from pathlib import Path
data = json.loads(Path("shared/help_specs.json").read_text(encoding="utf-8"))
print("\n".join(data.keys()))
PYRepository: yilibinbin/DataLab
Length of output: 4168
Add the self_consistent help entry
shared/help_specs.json already includes quadratic, but it still has no entry for the new self_consistent fit model added in shared/ui_specs.py:540-544. Keep the desktop/web help data in sync by adding that model description here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/ui_specs.py` around lines 360 - 370, The help metadata is missing the
newly supported self_consistent fit model. Add a self_consistent entry to
shared/help_specs.json matching the model’s identifier and description defined
near the self_consistent UI specification, keeping the desktop and web help
content consistent.
Source: Coding guidelines
…istent uncertainty Two review-chain findings on the B4 self_consistent web branch (Codex + CodeRabbit, both reproduced): - CR-1: a non-dict fit_implicit_params JSON raised its bilingual 'must be a JSON object' ValueError INSIDE the json.loads try, so the outer except re-wrapped it into a doubled '汉语 / English / 汉语 / English' message that breaks the locale layer's single ' / ' split. Hoist the isinstance check out of the try so only genuine JSON syntax errors get wrapped. - CX-1: a self_consistent fit with an unused parameter converges on the real parameter but leaves a rank-deficient covariance, so uncertainties come back NaN. _collect_params then called format_result_with_uncertainty_latex(val, nan), which raised a raw, non-bilingual 'cannot convert inf or nan to int' and 500-crashed the whole fit response. Guard non-finite sigma at that shared sink and report 'N/A' instead (protects every fit mode, not just implicit). Both covered by new regression tests in test_app_web_fitting_self_consistent.
…lingual error dedup (round-2 review) Round-2 adversarial review (4 fresh dimensions + per-finding refute-verify, user-requested re-review) confirmed 5 defects; all reproduced before fixing: - R2-1 [medium] ROOT FIX: format_value_for_latex_file raised 'cannot convert inf or nan to int' on non-finite input, discarding an otherwise-successful fit at every caller (web _generate_fitting_latex params/metrics/diagnostics branches; trigger: dof=0 → NaN reduced χ²). The public wrapper now degrades: non-finite sigma → bare value, non-finite value → parse-safe \multicolumn literal (works in S and dcolumn columns via siunitx_safe_cell). - R2-4 [medium]: desktop fit LaTeX writer crashed on NaN sigmas from a degenerate self_consistent covariance (both engine modes) — healed by the same root fix; regression test parametrizes siunitx + dcolumn. - R2-2 [low]: one response showed the same undefined sigma as 'N/A' (table) but 'nan' (CSV, summary). CSV parameter rows and summarize_fit_result now render non-finite sigmas as 'N/A'; undefined VALUES (metrics/covariance) stay literal 'nan' by convention. - CR-2 [low]: custom-mode non-dict params JSON double-wrapped its bilingual error (twin of CR-1, same file) — isinstance check hoisted out of the json.loads try. - R2-3 [low]: statistics two-column parser double-wrapped its 'uncertainty is not finite' bilingual error — raise hoisted out of the parse try. The re-run b3-runtime review dimension returned zero findings (desktop quadratic runtime clean). Verified: 21 targeted + 1255 broad latex/fitting/statistics tests pass (CI-parity LANG=en), mypy-strict clean, ruff clean.
…press undefined stat/sys split (Gemini round-2) Gemini's serial-chain review of 17153d0 confirmed two real gaps (both independently reproduced before fixing): - G-1 [high]: the extrapolation and error-propagation table builders call the PRIVATE _format_value_for_latex_file directly, bypassing the public-wrapper guard — a formula producing nan/inf still crashed those tables. The guard now lives at the private function's head (right after its own _mp() normalization, using direct mp.isfinite — no extra mpf copies, which also resolves Gemini's G-3 style note), and the public wrapper's duplicate guard is removed. - G-2 [low]: with data sigmas, a degenerate fit yields NaN stat AND sys errors; summarize_fit_result rendered '± N/A' but then printed '(stat nan, sys nan)' — NaN passes truthiness and fails almosteq. The stat/sys split is now suppressed for non-finite sys and stat renders as N/A. Verified: 20 targeted + 463 latex-surface tests pass (CI-parity LANG=en), mypy-strict clean, ruff clean.
…rouping (Codex CX-2)
Codex's final serial-chain review found the remaining hole in the
non-finite-value handling: with native_group_width=False (bundled-engine
app-side grouping, siunitx mode) the extrapolation, error-propagation, and
root table builders blindly wrap every value cell in \text{...} — wrapping
the new \multicolumn{1}{c}{nan} literal cell produces invalid TeX
('Misplaced \omit', reproduced by Codex with an actual lualatex compile).
All three app-group wrap sites now pass \multicolumn cells through
unchanged, mirroring the precedent in the desktop fitting writer's
_maybe_group. Regression test drives generate_latex_table end-to-end with a
NaN result in app-group mode.
Codex also adjudicated the Gemini-round fixes as correct (G-1 guard
placement complete for all paths through the private formatter; G-2
condition correct for 0/nan/±inf).
…kspace User-reported: after opening an example from the 示例 menu (e.g. 外推示例), the +列/-列/+行/-行/清除/文本视图 table-editing buttons were dead — clicks did nothing. Root cause: example workspaces ship in "file" data-source mode whose source_path is a RELATIVE path into the bundled examples/ dir (e.g. examples/extrapolation_richardson.txt). _restore_data_section keeps the file path when the file still exists (self-contained file-precedence). When the app runs from the repo root that relative path resolves, so _update_data_source_visibility grey-disables the whole manual_box (table + toolbar) — data is treated as coming from the file, not the manual table. The example rows are already inlined into the table by the restore, so the disabled toolbar left the user unable to edit visibly-present data. (It only reproduced when CWD made the relative path resolve, which is why it looked intermittent.) Fix: on a template open (as_template=True — all example workspaces), drop the bundled relative file path and fall back to the editable manual table. The inlined rows are preserved; _update_data_source_visibility re-enables manual_box once the path edit is empty. Non-template (real user) workspace opens are unaffected — they keep file mode. Regression test drives every bundled example through a template open and asserts the manual box + all six editing buttons are enabled, the rows survive, and a +列 click actually adds a column. Verified end-to-end under real (cocoa) Qt too. 339 workspace/example tests + 26 example-menu tests pass; ruff + mypy-strict + ratchet clean.
User-reported: the result-detail panel had three LaTeX buttons — 生成 TeX, 预览 PDF, LaTeX 选项 — but 生成 TeX opens the LaTeX preview dialog which itself carries both a TeX tab AND a PDF tab. So the standalone 预览 PDF button just duplicated the dialog's PDF tab. Remove result_preview_pdf_button; the result panel now shows 生成 TeX + LaTeX 选项. The PDF preview is unchanged — reachable via the PDF tab inside the 生成 TeX dialog (open_latex_preview builds both tabs; switching to the PDF tab compiles + shows the PDF). Updated the button→dialog test to assert the standalone button is gone while the dialog still exposes a PDF tab; refreshed the stale subtab comment. Verified end-to-end under real (cocoa) Qt: the row shows only 生成 TeX + LaTeX 选项. 99 result-panel/latex tests pass; ruff + ratchet clean.
…nstants-editor parity) Follow-up to 288a305: that fix cleared the bundled data file path so the manual DATA table re-enabled, but left the CONSTANTS editor with the same file-precedence disable bug. error-propagation.datalab ships its constants in "file" mode (source_path examples/constants.txt), so opening it as a template left the constants editor + its +行/-行/清除 buttons dead while the inlined constants rows sat visible-but-uneditable — the same class of bug the data-table fix cured. Extend the template-open cleanup to also clear constants_file_edit and refresh the constants panel (_update_constants_visibility). Verified end-to-end under real (cocoa) Qt on error-propagation: constants editor + buttons enabled, 8 constants rows survive; data side unchanged. Found by the adversarial review's constants-parity finder; the workflow verifier first mis-refuted it as an unshown-window isVisible() artifact — independent re-repro with a shown window + isEnabled() confirmed it was real. Regression test test_example_with_file_mode_constants_keeps_constants_editor_editable locks it in. 27 example-menu tests pass; ruff + ratchet clean.
…w count Codex review: the constants-survival check in test_example_with_file_mode_constants_keeps_constants_editor_editable only asserted table.rowCount() > 0. The constants editor has min_rows=1, so an empty restore would still leave one blank row and pass — a future content-loss regression could slip through. Assert the actual example constant (ALPHA) is present via editor.rows() instead, so the test fails if the inlined constants are dropped. Drop the now-unused QTableWidget import.
Summary
Desktop GUI overhaul for DataLab's workbench: toolbar-driven options, a redesigned
input/constants/result layout, on-demand LaTeX, a full design-review token pass, plus a
batch of UX polish and bug fixes accumulated on this branch. One Python core, two frontends
— desktop changes only; the web app and compute layers are untouched.
Highlights
Layout / workbench
输入数据 / 常数sheet tabs; each tab self-contained.[输入数据] + [one config card]per mode; mode selector on the toolbar.Input / constants UX
使用数据文件checkbox — a non-empty file path now takes precedence over themanual editor (which greys/hides live when a path is entered). Same for constants.
输入常数title, anN 行row-count, and a?help button (matching thedata card); both input cards now carry a consistent border + padding.
LaTeX / result
生成 TeX/预览 PDF(no generate-checkbox); tex-rebuild inputs persist in theworkspace so it works after reopen without recomputing.
不确定度位数moved to the result panel with live re-render.push OK off-screen; result font-size control live-updates.
Feature removal
单位标注(units) feature end-to-end on the UI side (not general enough); thedisplay-only backend is left as harmless dead code and old workspaces still open.
Design-review token pass (
theme.py)(
text_primary/text_muted/border/card_bg/region_bg/surface_raised/surface_hover).RADIUS_CARD=8/RADIUS_CONTROL=6); one canonicalCARD_PADDING; formula-preview border fixed; fixed-white tutorial card text pinned dark.Precision / workspace correctness
Review gates already passed on this branch
all confirmed findings fixed (mpf precision, theme-toggle stale styles, animation-fight,
tutorial contrast, file-precedence live feedback, …). Gemini's
agychannel timed out on thelast two rounds — its lens was covered by direct code inspection and noted as such.
convergence finished, per-keystroke constants refresh gated, dead widget removed.
Test plan
QT_QPA_PLATFORM=offscreen pytest -q— full suite green (a couple of files run separately dueto an offscreen Qt-teardown hang unrelated to these changes).
constants editor, formula preview, workspace controller, option reachability) all pass.
pixmap inset, constants row-count + control-row placement, theme-toggle restyle, mpf round-trip.
Requesting
CodeRabbit review before merge to
main.Summary by CodeRabbit