New capability requested for eco desktop's (and, via eco.logs.widget(),
any) log viewer: filter entries by kind, select a range, and copy that
selection as a runnable Python script -- optionally replaying the
original pacing via inserted time.sleep() calls between lines.
- KernelSession.log_widget_control(code): a new event kind for GUI
actions that never went through the console at all -- e.g. clicking
"Open" in the Namespace launcher. Same "code IS the text" contract as
log_input (not a description -- actual runnable Python), so it's usable
in generated scripts the same way console input already is.
- EcoDesktopApp._open_widget now logs one of these on every successful
open, as a bare name (`<name>.widget()`) -- not `namespace.<name>`,
which per build_namespace's own docstring doesn't actually resolve
(Namespace.append_obj writes devices onto the scope module, not the
Namespace instance); the bare form is exactly what build_namespace_vars
already makes available in that console.
- eco.logs._kernel_record_to_entry maps "widget_control" events to their
own TimelineEntry kind, alongside the existing input/result/stream/
error/session ones.
- LogTimelineQt: a "Show:" row of one checkbox per distinct kind present
(hide/show via setHidden, no rebuild); ExtendedSelection on the list
(Qt's own click/Shift+click/Ctrl+click/drag range selection, nothing
else to wire up); a "Copy Selected as Script" button + "Replicate
timing" checkbox -- input/widget_control entries in the selection
become script lines verbatim, everything else becomes a `# [kind] ...`
comment for context; timing mode inserts time.sleep(dt) between lines
using the real gap between those two entries' timestamps.
Verified via scripted offscreen construction (filter checkboxes present
and working, selection mode set, script content correct both with and
without timing, hidden/filtered rows correctly excluded from a selection
that still nominally contains them) and end-to-end through a real
EcoDesktopApp (_open_widget really writes a widget_control JSONL line,
eco.logs reads it back with the right kind/text) -- plus the full test
suite (361/363, the 2 failures pre-existing in test_config_lazy_init.py,
unrelated).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New "Tools" menu -> "Log Viewer" calls the already-existing
eco.logs.widget(prefer="qt") (kernel console history merged across
sessions, on a timeline -- see eco/logs.py and
eco.widgets.log_timeline_qt), which had no menu entry point before this.
Kept referenced on self._log_viewer so it isn't garbage-collected out from
under the user; reopening replaces (stops) the previous one rather than
piling up, and it's torn down alongside everything else in
_on_window_closing.
Verified via a scripted offscreen run (menu present, opens, reopening
replaces, stopped on window close) and the full test suite (342/344 --
the 2 failures are pre-existing in test_config_lazy_init.py, unrelated).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real bug reported after live testing: `eco jupyterlab` rendered the
Namespace launcher as a large inline block (full notebook width, not a
narrow sidebar like eco desktop's dock), and opening a device apparently
popped a Qt window instead of an ipywidgets view. Root cause: eco jupyterlab
was pointed at the same voila_app.ipynb used by `eco webapp`, which renders
everything via eco.widgets.widget_tray.make_namespace_dashboard -- built
for Voila, which (per eco.widgets.jupyter_sidecar's own existing docstring)
has no Lumino shell to dock into at all, so there was never a "narrow
sidebar" option available there in the first place. JupyterLab DOES have a
Lumino shell, and eco already had eco.widgets.jupyter_sidecar.open_in_sidecar
for exactly this -- it just wasn't wired up to the CLI.
- NamespaceLauncherWidget now takes a generic on_open(obj, label) callback
instead of a hardcoded WidgetTray, so it can drive either a WidgetTray
panel (Voila/webapp, unchanged) or a Sidecar panel (JupyterLab, new).
- New eco.widgets.jupyter_sidecar.open_namespace_dashboard(): docks the
Namespace launcher in its own Sidecar panel, and routes each opened
device into its own Sidecar panel too -- the closest browser-side
analogue to eco desktop's dockable Namespace panel + tiled device docks.
- New eco/jupyterlab_app.ipynb (packaged; added to package-data, learned
that lesson the hard way earlier this session) uses it, with its code
cell collapsed by default (standard nbformat source_hidden metadata) --
JupyterLab has no server-side route to auto-open a real Console tab (it's
a frontend-only command, confirmed by grepping jupyterlab's own source
for any matching handler), so a notebook is still the minimal anchor tab
needed for Sidecar/ipywidgets to have something to render into; this
keeps its visible footprint to just the dashboard.
- New _write_jupyterlab_notebook(): opening an *existing* .ipynb always
uses the kernel named in its own file, not JupyterLab's server-wide
default -- so eco_cli.py now regenerates a copy with its kernelspec
pointed at the just-registered eco-<scope> kernel, same as
_register_eco_kernel's other generated files.
Voila (`eco webapp`) is unchanged -- make_namespace_dashboard/WidgetTray
remains correct there, confirmed working earlier this session.
Verified: the notebook's cell code executes without error against a real
eco-bernina kernel (same mechanism used to verify the kernel registration
earlier), a scripted Sidecar dashboard construction + simulated click
against a fake namespace, and the full test suite (338/340 -- the 2
failures are pre-existing in test_config_lazy_init.py, unrelated).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real, severe regression from the --workspace feature: _main() had been
restructured into app._build_window(); app.load_workspace(...); app.run()
so --workspace could be loaded between building the window and entering
the blocking event loop. But _build_window() creates the QApplication as
a side effect if none exists yet -- so by the time run() got to its own
`created_app = QtWidgets.QApplication.instance() is None` check, an app
already existed, created_app was False, and the whole
`if created_app: ... app.exec_()` block was skipped. The window opened
(shown() runs during _build_window()) and the process then fell straight
through run() and exited -- "opens and closes immediately", confirmed via
the user's own `eco desktop -s bernina` output.
run() now takes workspace= itself and loads it internally, between
building the window and its own created_app check -- keeping window
construction and the exec_() decision in the one call that's actually
allowed to make it. _main() goes back to a single `app.run(workspace=
args.workspace)` call.
Verified both with a scripted regression test (mocking QApplication.exec_
to confirm it's actually entered) and end-to-end via eco-dev desktop -s
bernina (stayed alive for the full observation window instead of
exiting immediately).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real bug found via manual testing ("even not closing, desktop is super
unresponsive and laggy... the qt console, when entering something takes
ages to update, 30s ish"): eco.utilities.config.Proxy (the lazy device
proxy) only special-cases __class__ to avoid resolving while unresolved --
__dir__ isn't one of lazy_object_proxy's protected operations, so it
forwards straight to __wrapped__, fully constructing the device (real
EPICS calls) the moment anything calls dir() on it. Confirmed for real:
dir() on a still-lazy complex device took 62.7s. Since eco desktop's
console now has ~180 bare device names in its namespace (many still lazy),
and any completion mechanism scanning that namespace calls dir() on
candidates, typing in the console could trigger this on nearly every
keystroke. Added Proxy.__dir__, mirroring __class__'s existing "stay shy
to introspection" pattern exactly. Also measured Jedi-based completion as
independently slower (5.8s vs 0.001s) and less correct (0 matches vs 63)
than the classic completer for this dynamic a namespace -- now disabled
for every eco console (build_console_widget, so it applies uniformly to
both in-process and subprocess kernels).
Second, separate perf issue in the Namespace launcher panel: its
live-refresh timer (every 2s) fully rebuilds the table, and the Required
column (added last session) used a real QCheckBox+QWidget+QHBoxLayout per
row -- constructing/destroying ~120 native Qt widgets twice a second at
bernina's scale is real, visible GUI-thread cost. Switched to checkable
QTableWidgetItems (blockSignals()'d during the rebuild so programmatic
setCheckState() calls don't themselves trigger spurious required_names()
writes) -- same functionality, far cheaper to rebuild.
New: "Save Startup Script..." in the desktop window's Workspace menu --
writes a standalone, executable .sh (paired with a .json workspace file,
same format as Save Workspace) that relaunches `eco desktop` with just
this session's open widgets reopened. Lazy loading means nothing else in
the namespace gets touched, so this is a fast, minimal per-task dashboard
instead of the full namespace. New --workspace PATH flag on both eco_cli's
desktop subcommand and eco.widgets.desktop_app's own CLI loads a workspace
on startup; desktop_app._main() now builds the window and loads it before
entering run()'s blocking loop, instead of auto-starting straight into
that block.
Also: the Namespace launcher's lazy (not actively loading) entries no
longer show a spinner-adjacent hourglass -- gray text alone marks
"not built yet"; the animated spinner is now reserved for "actually
initializing right now". And the desktop window's Material dark theme
(qt-material's dark_teal.xml) is now the default -- it already existed as
an opt-in --theme dark, but wasn's what a first `eco desktop` showed; added
'none' as an explicit choice for anyone who wants native OS style back,
and qt-material to eco[gui]'s extras so pip installs get the real theme
too, not just the built-in approximation.
All confirmed via scripted offscreen reproductions of each bug before
writing its fix (not just after), plus the full test suite (336/338 --
the 2 failures are pre-existing in test_config_lazy_init.py, confirmed
via git stash, unrelated to any of this).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
run() (the `eco desktop` CLI path) builds its own QApplication and blocks in
exec_() until something calls quit(). _build_window() sets WA_QuitOnClose to
False on the window -- needed so closing it from *inside* an existing
IPython session (start()'s non-blocking path, used by eco.start_desktop())
doesn't kill that whole session -- but that also disables Qt's normal "last
window closed -> auto quit" behaviour. Nothing ever called quit() in that
case, so exec_() -- and the whole process/terminal -- hung forever after a
native close. Confirmed via manual testing (reported after closing a real
`eco desktop` window) and reproduced/verified with a scripted offscreen
test before writing the actual fix.
run() now sets self._owns_event_loop = True right before exec_(), and
_on_window_closing() calls QApplication.quit() itself when that's set --
only for the case where this instance actually owns the blocking loop, so
the embedded-in-an-IPython-session case (self._owns_event_loop stays False)
is unaffected, and no existing test's shared QApplication gets quit from
under it (none of them go through run()).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CLI: eco --ui {shell,lab,voila,desktop} -> subcommands eco {console,desktop,
webapp,jupyterlab}, console being the default (bare `eco` == `eco console`).
desktop's -s/--scope has no forced default any more: without it you get a
plain embedded Qt console with no Namespace launcher panel, instead of
silently defaulting to bernina. Both desktop and jupyterlab get a
--console/--no-console flag (console on by default). jupyterlab, when given
-s, now registers a real eco-<scope> Jupyter kernel (IPython profile startup
file + kernelspec, regenerated each launch) and makes it JupyterLab's
default kernel, then opens a real `jupyter console` on it in the launching
terminal -- so a fresh Console/Notebook opened from JupyterLab's own
launcher gets eco.<scope> preloaded too, not just one pre-built notebook.
Also added --set-rcfile [PATH] to persist the current invocation into an
.ecorc instead of launching.
Real bug found and fixed: Namespace.append_obj writes constructed/lazy
device objects onto sys.modules[root_module] (the scope module, e.g.
eco.bernina), not onto the Namespace instance itself -- the instance only
tracks name -> state bookkeeping (lazy_items/failed_items/initialized_items).
eco.widgets.desktop_app's launcher panel and console-namespace-population
both assumed `getattr(namespace, name)` worked, which silently raised
AttributeError for every single namespace entry -- opening or initializing
anything from the desktop launcher failed outright. Added
Namespace.resolve_item() as the canonical fix (the lazy-or-failed-or-
initialized dict chain reinitialize() itself already used internally) and
build_namespace_vars() (mirrors `from eco.<scope> import *` -- bare names
like mono/att, not just a `namespace` variable) for both the in-process and
subprocess console-kernel paths.
Also ported this same fix to eco.widgets.widget_tray (the ipywidgets
dashboard backing webapp/jupyterlab), replacing the old status_collection-
based member dropdown (initialized-only, no lazy/failed visibility) with a
NamespaceLauncherWidget matching desktop's launcher: browse every
registered name including lazy/failed ones, click to initialize+open.
New: a Required column/checkbox (Namespace.required_names()) in both
launchers -- desktop's Qt _NamespaceLauncher (QListWidget -> QTableWidget,
Name + Required columns) and the new ipywidgets NamespaceLauncherWidget.
Verified via eco-dev across all four subcommands against the real bernina
scope (shell/console, desktop with and without -s, webapp, jupyterlab
including the registered-kernel mechanism via a live jupyter_client
KernelManager), plus the full test suite (44/44 in test_desktop_app.py,
323/325 overall -- the 2 failures are pre-existing in
test_config_lazy_init.py, unrelated to any of this).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>