arrayview 0.24.0__tar.gz → 0.25.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/ROUTER.md +4 -1
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/context/architecture.md +3 -1
- arrayview-0.25.0/.mex/context/lifecycle.md +92 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/context/project-state.md +1 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/PKG-INFO +1 -1
- arrayview-0.25.0/docs/lifecycle.md +6 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/pyproject.toml +1 -1
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_analysis.py +12 -7
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_io.py +49 -3
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_launcher.py +25 -10
- arrayview-0.25.0/src/arrayview/_lifecycle.py +39 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_loading.py +44 -26
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_websocket.py +13 -8
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_session.py +3 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_viewer.html +35 -15
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_vscode_extension.py +1 -1
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_vscode_signal.py +67 -74
- arrayview-0.25.0/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.25.0/tests/lifecycle_matrix.py +391 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_api.py +32 -10
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_browser.py +31 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_cli.py +67 -0
- arrayview-0.25.0/tests/test_lifecycle_contract.py +477 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/uv.lock +1 -1
- {arrayview-0.24.0 → arrayview-0.25.0}/vscode-extension/extension.js +153 -11
- arrayview-0.25.0/vscode-extension/lifecycle_helpers.js +49 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/vscode-extension/package.json +2 -1
- arrayview-0.25.0/vscode-extension/test_lifecycle_helpers.js +41 -0
- arrayview-0.24.0/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/frontend-designer/SKILL.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/SKILL.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/element-attributes.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/playwright-tests.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/request-mocking.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/running-code.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/session-management.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/storage-state.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/test-generation.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/tracing.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/video-recording.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/todo-workflow/SKILL.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.agents/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.github/copilot-instructions.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.gitignore +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.ignore +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/AGENTS.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/SETUP.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/SYNC.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/context/conventions.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/context/decisions.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/context/frontend.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/context/render-pipeline.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/context/setup.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/context/stack.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/patterns/INDEX.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/patterns/README.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/patterns/add-file-format.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/patterns/add-server-endpoint.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/patterns/animation-verify.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/patterns/debug-render.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/patterns/debug-vscode-extension-python.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/patterns/extend-compare-mode.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/patterns/extract-server-route-module.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/patterns/frontend-change.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/setup.sh +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.mex/sync.sh +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.opencode/opencode.json +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T18-46-49-737Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T18-48-21-979Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T18-51-35-665Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-07-01-393Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-14-37-969Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-21-30-940Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-23-08-126Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-29-33-155Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-31-25-336Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-31-53-789Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-39-12-257Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-39-16-449Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T20-15-25-513Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T20-25-13-179Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T20-39-01-435Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-01-27-659Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-01-41-283Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-03-00-625Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-04-12-887Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-33-39-044Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-38-01-530Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-45-20-383Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-55-11-545Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-56-03-307Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-56-35-733Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-57-12-181Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-57-37-748Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-58-13-679Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T22-37-23-895Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T00-39-18-637Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T01-41-46-243Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T04-31-48-472Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-14-15-632Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-14-47-582Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-16-23-471Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-17-10-247Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-18-24-707Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-20-06-164Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-20-28-342Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-21-54-962Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-22-34-666Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-23-11-336Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-23-36-260Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-24-09-267Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-24-35-434Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-25-57-010Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-34-48-823Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-46-46-468Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-48-17-930Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-49-26-400Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-50-31-563Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-56-45-568Z.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.playwright-cli/theme-dark.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.python-version +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/.vscode/settings.json +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/AGENTS.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/CONTRIBUTING.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/DESIGN.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/LICENSE +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/README.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/docs/comparing.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/docs/configuration.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/docs/display.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/docs/index.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/docs/loading.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/docs/logo.png +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/docs/measurement.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/docs/remote.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/docs/viewing.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/matlab/arrayview.m +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/mkdocs.yml +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/plans/2026-04-14-immersive-animation.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/plans/2026-05-07-unified-colormap-picker.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/plans/arrayview_tool_menu_fix_prompt.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/plans/arrayview_tool_menu_implementation_plan.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/plans/egg-placement-mockup.html +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/plans/refactoring_plan.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/scripts/demo.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/scripts/release.sh +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_codex_open.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_diff.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_server.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_stdio_server.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/_vscode_shm.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/capture_v_animation.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/conftest.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_backend_shared.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_command_reachability.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_config.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_interactions.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_loading_server.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_mode_entry_batching.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_stdio_server.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_torch.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/ui_audit.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/tests/visual_smoke.py +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/vscode-extension/AGENTS.md +0 -0
- {arrayview-0.24.0 → arrayview-0.25.0}/vscode-extension/LICENSE +0 -0
|
@@ -6,6 +6,8 @@ edges:
|
|
|
6
6
|
condition: when the task depends on what is currently shipped, in progress, or recently changed
|
|
7
7
|
- target: context/architecture.md
|
|
8
8
|
condition: when working on system design, integrations, or understanding how components connect
|
|
9
|
+
- target: context/lifecycle.md
|
|
10
|
+
condition: when working on server startup, shutdown, display ownership, VS Code tabs, orphan processes, or session release
|
|
9
11
|
- target: context/stack.md
|
|
10
12
|
condition: when working with specific technologies, libraries, or making tech decisions
|
|
11
13
|
- target: context/conventions.md
|
|
@@ -22,7 +24,7 @@ edges:
|
|
|
22
24
|
condition: when starting a task — check the pattern index for a matching pattern file
|
|
23
25
|
- target: ../DESIGN.md
|
|
24
26
|
condition: when the task touches design philosophy — new features, UI changes, mode additions, or interaction model decisions
|
|
25
|
-
last_updated: 2026-05
|
|
27
|
+
last_updated: 2026-06-05
|
|
26
28
|
---
|
|
27
29
|
|
|
28
30
|
# arrayview — Router
|
|
@@ -81,6 +83,7 @@ Load `context/project-state.md` only when you need active-workstream or recent-s
|
|
|
81
83
|
|-----------|------|
|
|
82
84
|
| Checking current shipped / in-progress status | `context/project-state.md` |
|
|
83
85
|
| Understanding system architecture | `context/architecture.md` |
|
|
86
|
+
| Startup/shutdown/display ownership lifecycle | `context/lifecycle.md` |
|
|
84
87
|
| Working with a specific technology | `context/stack.md` |
|
|
85
88
|
| Writing or reviewing code | `context/conventions.md` |
|
|
86
89
|
| Making a design decision | `context/decisions.md` |
|
|
@@ -14,11 +14,13 @@ edges:
|
|
|
14
14
|
condition: when specific technology details are needed
|
|
15
15
|
- target: context/decisions.md
|
|
16
16
|
condition: when understanding why the architecture is structured this way
|
|
17
|
+
- target: context/lifecycle.md
|
|
18
|
+
condition: when ownership, startup, teardown, or display routing lifecycle matters
|
|
17
19
|
- target: context/frontend.md
|
|
18
20
|
condition: when the task involves _viewer.html, modes, reconcilers, or the View Component System
|
|
19
21
|
- target: context/render-pipeline.md
|
|
20
22
|
condition: when the task involves slice extraction, colormaps, caching, or the render thread
|
|
21
|
-
last_updated: 2026-05
|
|
23
|
+
last_updated: 2026-06-05
|
|
22
24
|
---
|
|
23
25
|
|
|
24
26
|
# Architecture
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lifecycle
|
|
3
|
+
description: Ownership contract for ArrayView backends, viewer sessions, VS Code tabs, and shutdown/release behavior.
|
|
4
|
+
triggers:
|
|
5
|
+
- "lifecycle"
|
|
6
|
+
- "server ownership"
|
|
7
|
+
- "startup"
|
|
8
|
+
- "shutdown"
|
|
9
|
+
- "orphan process"
|
|
10
|
+
- "VS Code tab"
|
|
11
|
+
- "backend unavailable"
|
|
12
|
+
- "Jupyter iframe"
|
|
13
|
+
- "SSH"
|
|
14
|
+
edges:
|
|
15
|
+
- target: context/architecture.md
|
|
16
|
+
condition: when component boundaries or display routing need broader context
|
|
17
|
+
- target: context/stack.md
|
|
18
|
+
condition: when VS Code, FastAPI, WebSocket, or packaging details are needed
|
|
19
|
+
last_updated: 2026-06-05
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Lifecycle
|
|
23
|
+
|
|
24
|
+
This contract describes who owns the backend, when it starts, and what closes it.
|
|
25
|
+
|
|
26
|
+
## Ownership Matrix
|
|
27
|
+
|
|
28
|
+
| Invocation | Display owner | Backend model | Shutdown/release |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| Local VS Code CLI `arrayview file.npy` | VS Code URL webview panel | Shared transient daemon | Panel close releases URL sessions; last viewer WebSocket close stops daemon |
|
|
31
|
+
| VS Code file click/custom editor | VS Code extension | Extension-owned stdio subprocess | Panel close kills subprocess |
|
|
32
|
+
| Plain Python script `view(arr)` | Browser/native/VS Code display | Non-daemon background server thread | Survives caller until viewer connects then closes |
|
|
33
|
+
| Jupyter `view(arr)` | Notebook kernel inline iframe | Kernel-owned daemon server thread | Iframe disappearance must not hard-kill backend |
|
|
34
|
+
| Julia/PythonCall | Browser/VS Code route from subprocess | Detached subprocess | Never in-process; avoid GIL deadlock |
|
|
35
|
+
| Remote/tunnel | VS Code direct webview when possible | stdio direct webview or intentional persistent server | Persistence allowed only when transport requires it |
|
|
36
|
+
| Plain SSH | User-forwarded localhost URL | Transient server unless `--serve` requested | Viewer close ends transient session |
|
|
37
|
+
|
|
38
|
+
## Local VS Code CLI
|
|
39
|
+
|
|
40
|
+
- `arrayview file.npy` from a local VS Code terminal should return to the prompt.
|
|
41
|
+
- Multiple local CLI launches may share one backend and open separate tabs.
|
|
42
|
+
- Closing one tab releases only that tab's arrays/sessions.
|
|
43
|
+
- Closing the last viewer tab should stop the transient daemon.
|
|
44
|
+
- The VS Code wrapper must not show "backend unavailable" based on a webview-side `fetch()`; backend health checks belong in the extension host.
|
|
45
|
+
|
|
46
|
+
## VS Code File Click And Custom Editor
|
|
47
|
+
|
|
48
|
+
- Prefer direct extension-owned stdio subprocesses.
|
|
49
|
+
- Do not require localhost or a shared port when direct mode is available.
|
|
50
|
+
- Closing the tab should terminate the subprocess.
|
|
51
|
+
- This path is transient and owned by the extension session, not by a long-lived server.
|
|
52
|
+
|
|
53
|
+
## Python Script
|
|
54
|
+
|
|
55
|
+
- `view(arr)` from a script should survive the script exiting.
|
|
56
|
+
- The backend must outlive the caller until viewer instances close.
|
|
57
|
+
- When the last viewer instance closes, free arrays and shut the backend down.
|
|
58
|
+
- Quick viewer connect/disconnect races must count as "a viewer connected" so transient waiters do not linger until connect timeout.
|
|
59
|
+
|
|
60
|
+
## Jupyter
|
|
61
|
+
|
|
62
|
+
- Jupyter keeps the backend kernel-owned.
|
|
63
|
+
- An iframe disappearing should not hard-kill the backend.
|
|
64
|
+
- Explicit close or cleanup should free the session.
|
|
65
|
+
- Repeated `view()` calls should reuse the kernel-owned server when appropriate.
|
|
66
|
+
|
|
67
|
+
## Remote, Tunnel, And SSH
|
|
68
|
+
|
|
69
|
+
- Remote or tunnel launches may persist when `--serve` or direct-server constraints require it.
|
|
70
|
+
- Direct VS Code tunnel display should prefer stdio direct webview to avoid forwarded-port auth and public-port setup.
|
|
71
|
+
- With multiple registered tunnel windows, a missing or stale `ARRAYVIEW_WINDOW_ID` must fail closed with a diagnostic rather than broadcasting to whichever window is focused.
|
|
72
|
+
- An exact registered `ARRAYVIEW_WINDOW_ID` wins; do not redirect it to a newer same-parent registration because live tunnel windows can share ancestry.
|
|
73
|
+
- Plain SSH should use `localhost` forwarding guidance and stay transient unless a shared server was explicitly requested.
|
|
74
|
+
|
|
75
|
+
## Shared Rules
|
|
76
|
+
|
|
77
|
+
- Global lifecycle state lives in `_session.py`.
|
|
78
|
+
- `release_session()` is the session-release primitive.
|
|
79
|
+
- Viewer WebSocket connect/disconnect owns active viewer counts.
|
|
80
|
+
- URL panel disposal must release every SID encoded in the URL: `sid`, `compare_sid`, `compare_sids`, and `overlay_sid`.
|
|
81
|
+
- Tunnel registration cleanup must not remove live same-tunnel sibling windows.
|
|
82
|
+
- Explicit cleanup wins over implicit disappearance.
|
|
83
|
+
- Any VS Code extension source change must rebuild `src/arrayview/arrayview-opener.vsix` and keep the packaged version in sync.
|
|
84
|
+
|
|
85
|
+
## Verification Anchors
|
|
86
|
+
|
|
87
|
+
- `tests/lifecycle_matrix.py` is the top-level lifecycle gate; it reports automated, real-process, local-state, and manual-only checks separately.
|
|
88
|
+
- `tests/test_lifecycle_contract.py` covers invocation ownership, release routes, transient daemon shutdown, and bundled VSIX lifecycle content.
|
|
89
|
+
- `tests/test_cli.py` covers CLI launch behavior.
|
|
90
|
+
- `tests/test_api.py` contains the affected WebSocket close and CLI helper coverage.
|
|
91
|
+
- `vscode-extension/test_lifecycle_helpers.js` covers URL SID collection and backend ping URL parsing.
|
|
92
|
+
- `vscode-extension/extension.js` must pass Node syntax checks after any extension change.
|
|
@@ -40,6 +40,7 @@ last_updated: 2026-05-13
|
|
|
40
40
|
|
|
41
41
|
## Recently Completed
|
|
42
42
|
|
|
43
|
+
- VS Code tab lifecycle hardening: local VS Code CLI launches now use transient daemon shutdown instead of `persist=True`, remote/tunnel launches remain persistent only where transport requires it, URL webview backend checks run in the extension host against `/ping`, URL panel disposal releases all sessions encoded in the viewer URL via tested lifecycle helpers, quick viewer connect/disconnect races are detected by a monotonic connection counter, ambiguous multi-window tunnel routing now fails closed instead of opening in a guessed window, stale viewer SID retry state is cleared on WebSocket disconnect, and the bundled opener extension was rebuilt as v0.14.12.
|
|
43
44
|
- Shift+C colormap picker redesign: the old centered shortlist is now a narrow translucent right-edge drawer with a close button, a yellow `Colormaps` title plus a `Favorites` subtitle, and a plain 12-swatch two-column quick set that stays visible above the search field. Search matches render in a separate results area below the input, Enter first exits the search field before a second Enter commits, arrow-key movement follows the visible two-column grid, and repeated `c` presses cycle through the currently visible swatches while the picker is open.
|
|
44
45
|
- Detached compare-on-X: single-array non-spatial dimensions now support the same compare-center family as two-array compare. The frontend reuses compare mode with per-pane detached indices, the dimbar shows a purple `X`, the compare titles show index-over-total labels, `[` / `]` control pane A, `{` / `}` control pane B, and repeated `X` exits detached compare after cycling the center modes. Focused coverage now includes a browser regression for detached entry/scrubbing/exit plus API coverage for split `/diff` indices on the same session.
|
|
45
46
|
- Normal-mode dimbar readability: inactive non-spatial dims no longer get a blanket reduced parent opacity, so the current index reads bright while `/total` stays subdued via the existing child dim-size styling.
|
|
@@ -43,8 +43,8 @@ def _build_metadata(session) -> dict:
|
|
|
43
43
|
meta["ras_resample_active"] = bool(
|
|
44
44
|
getattr(session, "ras_resample_active", False)
|
|
45
45
|
)
|
|
46
|
-
if getattr(session, "
|
|
47
|
-
meta["
|
|
46
|
+
if getattr(session, "array_keys", None):
|
|
47
|
+
meta["array_keys"] = session.array_keys
|
|
48
48
|
return meta
|
|
49
49
|
|
|
50
50
|
|
|
@@ -268,11 +268,16 @@ def _pixel_value(
|
|
|
268
268
|
"""Return a finite pixel value or None."""
|
|
269
269
|
if session.rgb_axis is not None:
|
|
270
270
|
return None
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if
|
|
274
|
-
|
|
275
|
-
|
|
271
|
+
if isinstance(indices, str):
|
|
272
|
+
parts = indices.split(",")
|
|
273
|
+
if not parts or any(v == "" for v in parts):
|
|
274
|
+
return None
|
|
275
|
+
try:
|
|
276
|
+
idx_tuple = tuple(int(v) for v in parts)
|
|
277
|
+
except ValueError:
|
|
278
|
+
return None
|
|
279
|
+
else:
|
|
280
|
+
idx_tuple = tuple(indices)
|
|
276
281
|
raw = (
|
|
277
282
|
qmri_display_slice(session, dim_x, dim_y, list(idx_tuple), qmri_role)
|
|
278
283
|
if qmri_role
|
|
@@ -53,10 +53,52 @@ def list_npz_keys(filepath):
|
|
|
53
53
|
return keys
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def list_mat_keys(filepath):
|
|
57
|
+
"""Return [{key, shape, dtype}] for each array in a .mat file.
|
|
58
|
+
|
|
59
|
+
Handles both scipy-loadable .mat files and v7.3 HDF5-based files.
|
|
60
|
+
Filters out metadata keys (starting with '_') and non-array entries.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
import scipy.io
|
|
64
|
+
|
|
65
|
+
mat = scipy.io.loadmat(filepath)
|
|
66
|
+
keys = []
|
|
67
|
+
for k, v in mat.items():
|
|
68
|
+
if k.startswith("_"):
|
|
69
|
+
continue
|
|
70
|
+
if isinstance(v, np.ndarray) and v.ndim >= 1:
|
|
71
|
+
keys.append({"key": k, "shape": list(v.shape), "dtype": str(v.dtype)})
|
|
72
|
+
return keys
|
|
73
|
+
except NotImplementedError:
|
|
74
|
+
import h5py
|
|
75
|
+
|
|
76
|
+
f = h5py.File(filepath, "r")
|
|
77
|
+
keys = []
|
|
78
|
+
for k in f.keys():
|
|
79
|
+
ds = f[k]
|
|
80
|
+
if isinstance(ds, h5py.Dataset) and len(ds.shape) >= 1:
|
|
81
|
+
keys.append({"key": k, "shape": list(ds.shape), "dtype": str(ds.dtype)})
|
|
82
|
+
f.close()
|
|
83
|
+
return keys
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def list_array_keys(filepath):
|
|
87
|
+
"""Return [{key, shape, dtype}] for each array in a multi-array file.
|
|
88
|
+
|
|
89
|
+
Dispatches by extension: .npz or .mat.
|
|
90
|
+
"""
|
|
91
|
+
if filepath.endswith(".npz"):
|
|
92
|
+
return list_npz_keys(filepath)
|
|
93
|
+
if filepath.endswith(".mat"):
|
|
94
|
+
return list_mat_keys(filepath)
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
|
|
56
98
|
def _select_npz_array(npz, filepath):
|
|
57
99
|
"""Load the first array from a multi-array .npz file.
|
|
58
100
|
|
|
59
|
-
The in-viewer
|
|
101
|
+
The in-viewer array picker handles array selection — no terminal prompt.
|
|
60
102
|
"""
|
|
61
103
|
keys = list(npz.keys())
|
|
62
104
|
return npz[keys[0]]
|
|
@@ -193,11 +235,13 @@ def load_data(filepath, key=None):
|
|
|
193
235
|
for k, v in mat.items()
|
|
194
236
|
if not k.startswith("_") and isinstance(v, np.ndarray)
|
|
195
237
|
}
|
|
238
|
+
if key is not None:
|
|
239
|
+
return _fix_mat_complex(arrays[key])
|
|
196
240
|
if len(arrays) == 1:
|
|
197
241
|
return _fix_mat_complex(next(iter(arrays.values())))
|
|
198
242
|
raise ValueError(
|
|
199
243
|
f".mat file contains multiple arrays: {list(arrays.keys())}. "
|
|
200
|
-
"
|
|
244
|
+
"Select one in the viewer or pass a key."
|
|
201
245
|
)
|
|
202
246
|
except NotImplementedError:
|
|
203
247
|
# MATLAB v7.3 files use HDF5 — scipy cannot load them; fall back to h5py.
|
|
@@ -205,11 +249,13 @@ def load_data(filepath, key=None):
|
|
|
205
249
|
|
|
206
250
|
f = h5py.File(filepath, "r")
|
|
207
251
|
arrays = {k: f[k] for k in f.keys() if isinstance(f[k], h5py.Dataset)}
|
|
252
|
+
if key is not None:
|
|
253
|
+
return np.array(arrays[key])
|
|
208
254
|
if len(arrays) == 1:
|
|
209
255
|
return np.array(next(iter(arrays.values())))
|
|
210
256
|
raise ValueError(
|
|
211
257
|
f".mat (v7.3) file contains multiple datasets: {list(arrays.keys())}. "
|
|
212
|
-
"
|
|
258
|
+
"Select one in the viewer or pass a key."
|
|
213
259
|
)
|
|
214
260
|
else:
|
|
215
261
|
raise ValueError(
|
|
@@ -984,7 +984,13 @@ def _handle_cli_spawned_daemon(
|
|
|
984
984
|
f" rgb={rgb},"
|
|
985
985
|
f")"
|
|
986
986
|
)
|
|
987
|
-
subprocess.Popen(
|
|
987
|
+
subprocess.Popen(
|
|
988
|
+
[sys.executable, "-c", script],
|
|
989
|
+
stdin=subprocess.DEVNULL,
|
|
990
|
+
stdout=subprocess.DEVNULL,
|
|
991
|
+
stderr=subprocess.DEVNULL,
|
|
992
|
+
close_fds=True,
|
|
993
|
+
)
|
|
988
994
|
|
|
989
995
|
early_webview_opened = False
|
|
990
996
|
early_webview_notified = False
|
|
@@ -2282,13 +2288,18 @@ async def _stop_server_when_viewer_closes(
|
|
|
2282
2288
|
Used in script mode so the non-daemon server thread exits cleanly when done."""
|
|
2283
2289
|
import arrayview._session as _sm
|
|
2284
2290
|
|
|
2291
|
+
connections_before = _sm.VIEWER_CONNECTIONS_SEEN
|
|
2285
2292
|
deadline = time.monotonic() + connect_timeout
|
|
2286
|
-
while
|
|
2293
|
+
while (
|
|
2294
|
+
_sm.VIEWER_SOCKETS == 0
|
|
2295
|
+
and _sm.VIEWER_CONNECTIONS_SEEN == connections_before
|
|
2296
|
+
):
|
|
2287
2297
|
if time.monotonic() > deadline:
|
|
2288
2298
|
server.should_exit = True # no viewer connected; give up
|
|
2289
2299
|
return
|
|
2290
2300
|
await asyncio.sleep(0.2)
|
|
2291
|
-
# At least one viewer connected
|
|
2301
|
+
# At least one viewer connected, even if it already disconnected between
|
|
2302
|
+
# polling ticks. Now wait for all viewer sockets to be closed.
|
|
2292
2303
|
while True:
|
|
2293
2304
|
while _sm.VIEWER_SOCKETS > 0:
|
|
2294
2305
|
await asyncio.sleep(0.2)
|
|
@@ -2323,10 +2334,14 @@ def _wait_for_viewer_close(
|
|
|
2323
2334
|
"""
|
|
2324
2335
|
import arrayview._session as _sm
|
|
2325
2336
|
|
|
2337
|
+
connections_before = _sm.VIEWER_CONNECTIONS_SEEN
|
|
2326
2338
|
connect_deadline = (
|
|
2327
2339
|
None if connect_timeout is None else time.monotonic() + connect_timeout
|
|
2328
2340
|
)
|
|
2329
|
-
while
|
|
2341
|
+
while (
|
|
2342
|
+
_sm.VIEWER_SOCKETS == 0
|
|
2343
|
+
and _sm.VIEWER_CONNECTIONS_SEEN == connections_before
|
|
2344
|
+
):
|
|
2330
2345
|
if connect_deadline is not None and time.monotonic() >= connect_deadline:
|
|
2331
2346
|
return
|
|
2332
2347
|
time.sleep(0.2)
|
|
@@ -2603,7 +2618,7 @@ def _serve_daemon(
|
|
|
2603
2618
|
threading.Thread(target=_warm_luts, daemon=True).start()
|
|
2604
2619
|
|
|
2605
2620
|
def _load():
|
|
2606
|
-
from arrayview._io import load_data, load_data_with_meta,
|
|
2621
|
+
from arrayview._io import load_data, load_data_with_meta, list_array_keys
|
|
2607
2622
|
|
|
2608
2623
|
try:
|
|
2609
2624
|
data, spatial_meta = load_data_with_meta(filepath)
|
|
@@ -2617,13 +2632,13 @@ def _serve_daemon(
|
|
|
2617
2632
|
)
|
|
2618
2633
|
session.sid = sid
|
|
2619
2634
|
session.spatial_meta = spatial_meta
|
|
2620
|
-
# Multi-array .npz: store keys so the viewer can show a picker
|
|
2621
|
-
if filepath.endswith(".npz") and not cleanup:
|
|
2635
|
+
# Multi-array .npz/.mat: store keys so the viewer can show a picker
|
|
2636
|
+
if (filepath.endswith(".npz") or filepath.endswith(".mat")) and not cleanup:
|
|
2622
2637
|
try:
|
|
2623
|
-
keys =
|
|
2638
|
+
keys = list_array_keys(filepath)
|
|
2624
2639
|
if len(keys) > 1:
|
|
2625
|
-
session.
|
|
2626
|
-
session.
|
|
2640
|
+
session.array_keys = keys
|
|
2641
|
+
session.array_filepath = filepath
|
|
2627
2642
|
except Exception:
|
|
2628
2643
|
pass
|
|
2629
2644
|
if spatial_meta is not None:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Session lifecycle helpers shared by REST and WebSocket close paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from arrayview._session import PENDING_SESSION_EVENTS, PENDING_SESSIONS, SESSIONS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def release_session(sid: str) -> bool:
|
|
9
|
+
"""Drop a loaded or pending session and release its cached array memory."""
|
|
10
|
+
PENDING_SESSIONS.discard(sid)
|
|
11
|
+
event = PENDING_SESSION_EVENTS.pop(sid, None)
|
|
12
|
+
if event is not None:
|
|
13
|
+
try:
|
|
14
|
+
event.set()
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
session = SESSIONS.pop(sid, None)
|
|
19
|
+
if session is None:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
session.reset_caches()
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
try:
|
|
27
|
+
session.data = None
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from arrayview._routes_persistence import _CROP_LOCK, _CROP_STATE
|
|
33
|
+
|
|
34
|
+
with _CROP_LOCK:
|
|
35
|
+
_CROP_STATE.pop(sid, None)
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
return True
|
|
@@ -3,10 +3,11 @@ import io
|
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
|
-
from fastapi import File, HTTPException, Request, UploadFile
|
|
6
|
+
from fastapi import File, Form, HTTPException, Request, UploadFile
|
|
7
7
|
from fastapi.responses import JSONResponse
|
|
8
8
|
|
|
9
9
|
from arrayview._io import _SUPPORTED_EXTS, _peek_file_shape, load_data
|
|
10
|
+
from arrayview._lifecycle import release_session
|
|
10
11
|
from arrayview._session import SESSIONS, Session
|
|
11
12
|
|
|
12
13
|
|
|
@@ -112,16 +113,16 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
112
113
|
except ImportError:
|
|
113
114
|
pass
|
|
114
115
|
try:
|
|
115
|
-
from ._io import load_data_with_meta,
|
|
116
|
+
from ._io import load_data_with_meta, list_array_keys
|
|
116
117
|
|
|
117
|
-
# Multi-array .npz: if no key provided, return the key list so
|
|
118
|
-
# client can show a picker instead of blocking on terminal input.
|
|
118
|
+
# Multi-array .npz/.mat: if no key provided, return the key list so
|
|
119
|
+
# the client can show a picker instead of blocking on terminal input.
|
|
119
120
|
_key = body.get("key")
|
|
120
|
-
|
|
121
|
-
if filepath.endswith(".npz"):
|
|
122
|
-
|
|
123
|
-
if len(
|
|
124
|
-
return {"
|
|
121
|
+
_array_keys = None
|
|
122
|
+
if filepath.endswith(".npz") or filepath.endswith(".mat"):
|
|
123
|
+
_array_keys = await asyncio.to_thread(list_array_keys, filepath)
|
|
124
|
+
if len(_array_keys) > 1 and not _key:
|
|
125
|
+
return {"array_keys": _array_keys, "filepath": filepath}
|
|
125
126
|
|
|
126
127
|
data, spatial_meta = await asyncio.to_thread(load_data_with_meta, filepath, key=_key)
|
|
127
128
|
except Exception as e:
|
|
@@ -130,9 +131,9 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
130
131
|
if spatial_meta is not None:
|
|
131
132
|
session.spatial_meta = spatial_meta
|
|
132
133
|
session.original_volume = data
|
|
133
|
-
if
|
|
134
|
-
session.
|
|
135
|
-
session.
|
|
134
|
+
if _array_keys and len(_array_keys) > 1:
|
|
135
|
+
session.array_keys = _array_keys
|
|
136
|
+
session.array_filepath = filepath
|
|
136
137
|
if body.get("rgb"):
|
|
137
138
|
try:
|
|
138
139
|
await asyncio.to_thread(setup_rgb, session)
|
|
@@ -169,22 +170,27 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
169
170
|
)
|
|
170
171
|
return {"sid": session.sid, "name": name, "notified": notified}
|
|
171
172
|
|
|
172
|
-
@app.post("/
|
|
173
|
-
|
|
174
|
-
"""
|
|
173
|
+
@app.post("/release/{sid}")
|
|
174
|
+
def release_viewer_session(sid: str):
|
|
175
|
+
"""Release a session when its owning viewer tab/window closes."""
|
|
176
|
+
return {"sid": sid, "released": release_session(sid)}
|
|
177
|
+
|
|
178
|
+
@app.post("/session/{sid}/reload-key")
|
|
179
|
+
async def reload_array_key(sid: str, request: Request):
|
|
180
|
+
"""Reload a session's data from a multi-array file with a different key."""
|
|
175
181
|
session = SESSIONS.get(sid)
|
|
176
182
|
if session is None:
|
|
177
183
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
178
|
-
|
|
179
|
-
if not
|
|
180
|
-
raise HTTPException(status_code=400, detail="Session
|
|
184
|
+
array_filepath = getattr(session, "array_filepath", None)
|
|
185
|
+
if not array_filepath:
|
|
186
|
+
raise HTTPException(status_code=400, detail="Session has no array keys")
|
|
181
187
|
body = await request.json()
|
|
182
188
|
key = body.get("key")
|
|
183
189
|
if not key:
|
|
184
190
|
raise HTTPException(status_code=400, detail="key is required")
|
|
185
191
|
try:
|
|
186
192
|
from ._io import load_data
|
|
187
|
-
data = await asyncio.to_thread(load_data,
|
|
193
|
+
data = await asyncio.to_thread(load_data, array_filepath, key=key)
|
|
188
194
|
except Exception as e:
|
|
189
195
|
return {"error": str(e)}
|
|
190
196
|
session.data = data
|
|
@@ -197,7 +203,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
197
203
|
session._rgba_bytes = 0
|
|
198
204
|
session._mosaic_bytes = 0
|
|
199
205
|
session._estimated_mem = session._estimate_memory()
|
|
200
|
-
# Keep
|
|
206
|
+
# Keep array_keys so the user can switch again, but don't auto-prompt
|
|
201
207
|
return {"ok": True}
|
|
202
208
|
|
|
203
209
|
@app.get("/fs/list")
|
|
@@ -278,7 +284,10 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
278
284
|
return {"cwd": target, "parent": parent, "home": home, "entries": entries}
|
|
279
285
|
|
|
280
286
|
@app.post("/load-upload")
|
|
281
|
-
async def load_upload(
|
|
287
|
+
async def load_upload(
|
|
288
|
+
file: UploadFile = File(...),
|
|
289
|
+
key: str | None = Form(None),
|
|
290
|
+
):
|
|
282
291
|
"""Accept a drag-and-dropped .npy or .mat file and create a new session."""
|
|
283
292
|
import tempfile
|
|
284
293
|
|
|
@@ -294,16 +303,25 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
294
303
|
tmp.write(contents)
|
|
295
304
|
tmp_path = tmp.name
|
|
296
305
|
|
|
306
|
+
_returned_keys = False
|
|
297
307
|
try:
|
|
298
|
-
|
|
308
|
+
from ._io import list_array_keys
|
|
309
|
+
|
|
310
|
+
if ext == ".mat" and not key:
|
|
311
|
+
array_keys = await asyncio.to_thread(list_array_keys, tmp_path)
|
|
312
|
+
if len(array_keys) > 1:
|
|
313
|
+
_returned_keys = True
|
|
314
|
+
return {"array_keys": array_keys}
|
|
315
|
+
data = await asyncio.to_thread(load_data, tmp_path, key=key)
|
|
299
316
|
except Exception as e:
|
|
300
317
|
os.unlink(tmp_path)
|
|
301
318
|
raise HTTPException(status_code=400, detail=str(e))
|
|
302
319
|
finally:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
320
|
+
if not _returned_keys:
|
|
321
|
+
try:
|
|
322
|
+
os.unlink(tmp_path)
|
|
323
|
+
except OSError:
|
|
324
|
+
pass
|
|
307
325
|
|
|
308
326
|
session = await asyncio.to_thread(Session, data, name=filename)
|
|
309
327
|
SESSIONS[session.sid] = session
|
|
@@ -7,6 +7,7 @@ from starlette.websockets import WebSocketDisconnect
|
|
|
7
7
|
|
|
8
8
|
import arrayview._session as _session_mod
|
|
9
9
|
from arrayview._analysis import _build_metadata
|
|
10
|
+
from arrayview._lifecycle import release_session
|
|
10
11
|
from arrayview._overlays import _composite_overlays
|
|
11
12
|
from arrayview._render import (
|
|
12
13
|
LUTS,
|
|
@@ -80,14 +81,8 @@ def register_websocket_routes(app) -> None:
|
|
|
80
81
|
msg = await ws.receive_json()
|
|
81
82
|
if msg.get("action") == "close":
|
|
82
83
|
sid = msg.get("sid")
|
|
83
|
-
if sid
|
|
84
|
-
|
|
85
|
-
SESSIONS[sid].data = None
|
|
86
|
-
del SESSIONS[sid]
|
|
87
|
-
from arrayview._routes_persistence import _CROP_LOCK, _CROP_STATE
|
|
88
|
-
|
|
89
|
-
with _CROP_LOCK:
|
|
90
|
-
_CROP_STATE.pop(sid, None)
|
|
84
|
+
if sid:
|
|
85
|
+
release_session(sid)
|
|
91
86
|
except Exception:
|
|
92
87
|
pass
|
|
93
88
|
finally:
|
|
@@ -110,6 +105,10 @@ def register_websocket_routes(app) -> None:
|
|
|
110
105
|
pass
|
|
111
106
|
|
|
112
107
|
_session_mod.VIEWER_SOCKETS += 1
|
|
108
|
+
_session_mod.VIEWER_CONNECTIONS_SEEN += 1
|
|
109
|
+
_session_mod.VIEWER_SID_COUNTS[sid] = (
|
|
110
|
+
_session_mod.VIEWER_SID_COUNTS.get(sid, 0) + 1
|
|
111
|
+
)
|
|
113
112
|
_session_mod.VIEWER_SIDS.add(sid)
|
|
114
113
|
loop = asyncio.get_running_loop()
|
|
115
114
|
_pending: asyncio.Queue[dict | None] = asyncio.Queue()
|
|
@@ -402,3 +401,9 @@ def register_websocket_routes(app) -> None:
|
|
|
402
401
|
except asyncio.CancelledError:
|
|
403
402
|
pass
|
|
404
403
|
_session_mod.VIEWER_SOCKETS = max(0, _session_mod.VIEWER_SOCKETS - 1)
|
|
404
|
+
sid_count = max(0, _session_mod.VIEWER_SID_COUNTS.get(sid, 0) - 1)
|
|
405
|
+
if sid_count:
|
|
406
|
+
_session_mod.VIEWER_SID_COUNTS[sid] = sid_count
|
|
407
|
+
else:
|
|
408
|
+
_session_mod.VIEWER_SID_COUNTS.pop(sid, None)
|
|
409
|
+
_session_mod.VIEWER_SIDS.discard(sid)
|
|
@@ -28,6 +28,8 @@ SERVER_LOOP = None
|
|
|
28
28
|
SERVER_PORT: int | None = None # actual port the uvicorn server is bound to
|
|
29
29
|
VIEWER_SOCKETS = 0 # count of active viewer WebSocket connections
|
|
30
30
|
VIEWER_SIDS: set = set() # session IDs with at least one active viewer WS
|
|
31
|
+
VIEWER_SID_COUNTS: dict[str, int] = {} # active viewer WS count per session ID
|
|
32
|
+
VIEWER_CONNECTIONS_SEEN = 0 # monotonic count of accepted viewer WS connections
|
|
31
33
|
SHELL_SOCKETS = [] # webview shell WS connections (for tab injection)
|
|
32
34
|
_window_process = None
|
|
33
35
|
PENDING_SESSIONS: set = set() # sids whose data is still loading in a background thread
|
|
@@ -446,6 +448,7 @@ __all__ = [
|
|
|
446
448
|
"SERVER_LOOP",
|
|
447
449
|
"VIEWER_SOCKETS",
|
|
448
450
|
"VIEWER_SIDS",
|
|
451
|
+
"VIEWER_CONNECTIONS_SEEN",
|
|
449
452
|
"SHELL_SOCKETS",
|
|
450
453
|
"_window_process",
|
|
451
454
|
"PENDING_SESSIONS",
|