Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `requests>=2.31.0` dependency for HTTP communication
- Comprehensive test suite for LLM service and chat panel

## [0.7.1] - 2026-07-30

### Added

- The desktop GUI can update its exact virtual environment in place, restart,
and restore the complete plate-manager document, UI configuration, and typed
ObjectState history. The detached updater runs outside the target
environment so Windows can replace active environment files safely.
- The update check is available through the existing typed main-window MCP
action surface.

### Changed

- UI configuration forms now retain annotated validation metadata while using
dedicated key-sequence and finite color controls instead of free-text
editing.
- Placeholder and enabled-state styling is applied as fields materialize
instead of appearing only after a large form finishes constructing.

### Fixed

- ObjectState 1.1 history persistence now preserves paths, enums, dataclasses,
callable objects, shared identity, the active timeline position, and unsaved
typed state across application restarts.
- Function-pattern add and reset operations no longer fail on invalid transient
editor text, and selectors support registered plate-scoped functions that do
not consume image arrays.
- Compact UI and MCP parameter help render the owning type of annotated
parameters without exposing raw `typing.Annotated` representations.
- ZMQ submission timeouts now cover progress-stream registration as well as
execution dispatch, preventing installer smoke runs and slow-starting
runtimes from falling back to an unrelated five-second handshake limit.

## [0.7.0] - 2026-07-30

### Changed
Expand Down
2 changes: 1 addition & 1 deletion openhcs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from openhcs._source_dependencies import ensure_source_checkout_external_paths

__version__ = "0.7.0"
__version__ = "0.7.1"

# Configure polystore defaults for OpenHCS integration
os.environ.setdefault("POLYSTORE_METADATA_FILENAME", "openhcs_metadata.json")
Expand Down Expand Up @@ -49,7 +49,7 @@
configured_level_name = os.environ.get("OPENHCS_LOG_LEVEL", "INFO").upper()
configured_level = getattr(logging, configured_level_name, None)
if not isinstance(configured_level, int):
raise ValueError(f"Unknown OPENHCS_LOG_LEVEL: {configured_level_name!r}")

Check failure on line 52 in openhcs/__init__.py

View workflow job for this annotation

GitHub Actions / code-quality

ruff (TRY004)

openhcs/__init__.py:52:9: TRY004 Prefer `TypeError` exception for invalid type

# Only configure if no handlers exist and level is too high
if not root_logger.handlers and root_logger.level > configured_level:
Expand Down Expand Up @@ -77,17 +77,17 @@
# PhysicalPath,
#)
#
__all__ = [
# Core functions
"initialize",
"create_config",
"run_pipeline",
"stitch_images",

# Key types
"PipelineConfig",
"BackendConfig",
"MISTConfig",
"VirtualPath",
"PhysicalPath",
]

Check failure on line 93 in openhcs/__init__.py

View workflow job for this annotation

GitHub Actions / code-quality

ruff (RUF022)

openhcs/__init__.py:80:11: RUF022 `__all__` is not sorted help: Apply an isort-style sorting to `__all__`
29 changes: 29 additions & 0 deletions openhcs/agent/ui_bridge_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,35 @@ class PlateOperation(str, Enum):
RUN = "run"


class MainWindowAction(str, Enum):
"""Closed set of agent-facing main-window actions."""

title: str
side_effects: tuple[str, ...]
confirmation_required: bool

def __new__(
cls,
value: str,
title: str,
side_effects: tuple[str, ...],
confirmation_required: bool,
) -> "MainWindowAction":
member = str.__new__(cls, value)
member._value_ = value
member.title = title
member.side_effects = side_effects
member.confirmation_required = confirmation_required
return member

CHECK_FOR_UPDATES = (
"check_for_updates",
"Check for Updates",
("checks_trusted_release_service", "may_open_update_confirmation"),
False,
)


class PlateManagerAction(str, Enum):
"""Closed set of PlateManager button actions and agent-facing semantics."""

Expand Down
8 changes: 5 additions & 3 deletions openhcs/processing/backends/lib_registry/unified_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,15 +1193,17 @@ def display_name(self) -> str:
return self.original_name
return self.name

def get_memory_type(self) -> str:
def get_memory_type(self) -> str | None:
"""
Get the actual memory type (backend) of this function.
Get the actual memory type (backend), if the function consumes arrays.

Returns the memory type recorded at metadata creation time, otherwise
the registry-level memory type for older cache entries.

Returns:
Memory type string (e.g., "cupy", "numpy", "torch", "pyclesperanto")
Memory type string (e.g., "cupy", "numpy", "torch",
"pyclesperanto"), or ``None`` for plate-scoped functions that do
not consume image arrays.
"""
if self.memory_type is not None:
return self.memory_type
Expand Down
51 changes: 51 additions & 0 deletions openhcs/pyqt_gui/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,14 @@ def parse_arguments():
action='version',
version='OpenHCS PyQt6 GUI 1.0.0'
)

from openhcs.pyqt_gui.services.desktop_update import UPDATE_SESSION_ARGUMENT

parser.add_argument(
UPDATE_SESSION_ARGUMENT,
type=Path,
help=argparse.SUPPRESS,
)

return parser.parse_args()

Expand Down Expand Up @@ -438,6 +446,49 @@ def main(
install_global_window_bounds_filter(app) # install once, early

def _main_window_ready() -> None:
from openhcs.pyqt_gui.services.desktop_update import DesktopUpdateSession
from PyQt6.QtWidgets import QMessageBox

session = (
DesktopUpdateSession(args.restore_update_session)
if args.restore_update_session is not None
else DesktopUpdateSession.pending()
)
if session.directory.exists():
if not session.is_complete:
QMessageBox.warning(
app.main_window,
"OpenHCS Update Recovery",
"OpenHCS found an incomplete saved update session. The "
f"recovery files were preserved at:\n\n{session.directory}",
)
else:
try:
update_error = session.restore(app.main_window)
except Exception as error:
logging.exception("Failed to restore the saved update session")
QMessageBox.warning(
app.main_window,
"OpenHCS Update Recovery",
"OpenHCS reopened, but could not restore the saved "
"session. The recovery files were preserved at:\n\n"
f"{session.directory}\n\n{type(error).__name__}: {error}",
)
else:
if update_error:
QMessageBox.warning(
app.main_window,
"OpenHCS Update Failed",
"The update failed, so OpenHCS reopened the prior "
f"installation and restored the session.\n\n{update_error}",
)
else:
QMessageBox.information(
app.main_window,
"OpenHCS Updated",
"OpenHCS updated successfully and restored the "
"working session and ObjectState history.",
)
if startup_progress is not None:
startup_progress.ready()

Expand Down
64 changes: 43 additions & 21 deletions openhcs/pyqt_gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
from openhcs.pyqt_gui.services.desktop_update import (
DesktopUpdate,
DesktopUpdateError,
DesktopRuntimeEnvironment,
DesktopUpdateSession,
DesktopUpdateService,
)
from objectstate.object_state import ObjectState
Expand Down Expand Up @@ -1464,40 +1466,60 @@ def _on_update_check_completed(self, update: DesktopUpdate) -> None:
return

self.status_message.emit(f"OpenHCS {update.latest_version} is available")
destination = (
"the official installer download"
if update.has_native_installer
else "the official release page"
)
handoff_detail = (
"After downloading, open the installer. It creates and validates a "
"new environment before switching the OpenHCS launcher."
if update.has_native_installer
else "The release page contains the supported update downloads and "
"instructions for this platform."
)
response = QMessageBox.question(
self,
"OpenHCS Update Available",
f"OpenHCS {update.latest_version} is available "
f"(installed: {update.installed_version}).\n\n"
f"Open {destination} now?\n\n{handoff_detail}",
QMessageBox.StandardButton.Open | QMessageBox.StandardButton.Cancel,
QMessageBox.StandardButton.Open,
"Install the update now? OpenHCS will save the complete working "
"session and ObjectState history, close, update its current "
"environment, then reopen and restore the session.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
QMessageBox.StandardButton.Yes,
)
if response != QMessageBox.StandardButton.Open:
if response != QMessageBox.StandardButton.Yes:
return
session = None
try:
opened = self.desktop_update_service.open_update(update)
runtime = DesktopRuntimeEnvironment.current()
session = DesktopUpdateSession.capture(self)
started = self.desktop_update_service.start_update(
update,
runtime=runtime,
session=session,
)
except DesktopUpdateError as exc:
opened = False
logger.warning("Refused OpenHCS update handoff: %s", exc)
if not opened:
if session is not None:
session.discard()
logger.warning("Automatic OpenHCS update is unavailable: %s", exc)
QMessageBox.warning(
self,
"OpenHCS Updates",
f"OpenHCS could not start the automatic update.\n\n{exc}",
)
return
except Exception as exc:
if session is not None:
session.discard()
logger.exception("Failed to prepare the OpenHCS update")
QMessageBox.warning(
self,
"OpenHCS Updates",
f"OpenHCS could not save and start the update.\n\n{exc}",
)
return
if not started:
session.discard()
QMessageBox.warning(
self,
"OpenHCS Updates",
"The system browser could not open the official update destination.",
"OpenHCS could not start the background updater. The current "
"application and session are unchanged.",
)
return

self.status_message.emit("OpenHCS update prepared; restarting…")
self.close()

def _on_update_check_failed(self, error_message: str) -> None:
self.check_for_updates_action.setEnabled(True)
Expand Down
Loading
Loading