arrayview 0.23.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.23.0 → arrayview-0.25.0}/.mex/ROUTER.md +4 -1
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/context/architecture.md +15 -1
- arrayview-0.25.0/.mex/context/lifecycle.md +92 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/context/project-state.md +1 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/PKG-INFO +1 -1
- arrayview-0.25.0/docs/lifecycle.md +6 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/pyproject.toml +1 -1
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_analysis.py +12 -7
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_io.py +49 -3
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_launcher.py +99 -62
- arrayview-0.25.0/src/arrayview/_lifecycle.py +39 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_platform.py +40 -42
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_loading.py +44 -26
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_rendering.py +33 -6
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_websocket.py +36 -8
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_session.py +3 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_shell.html +20 -20
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_stdio_server.py +81 -28
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_viewer.html +1097 -540
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_vscode_extension.py +1 -1
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_vscode_signal.py +70 -119
- arrayview-0.25.0/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.25.0/tests/lifecycle_matrix.py +391 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_api.py +156 -9
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_browser.py +125 -12
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_cli.py +67 -0
- arrayview-0.25.0/tests/test_lifecycle_contract.py +477 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_loading_server.py +1 -1
- {arrayview-0.23.0 → arrayview-0.25.0}/uv.lock +1 -1
- {arrayview-0.23.0 → arrayview-0.25.0}/vscode-extension/extension.js +226 -16
- arrayview-0.25.0/vscode-extension/lifecycle_helpers.js +49 -0
- {arrayview-0.23.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.23.0/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/frontend-designer/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/element-attributes.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/playwright-tests.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/request-mocking.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/running-code.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/session-management.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/storage-state.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/test-generation.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/tracing.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/playwright-cli/references/video-recording.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/todo-workflow/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.agents/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.github/copilot-instructions.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.gitignore +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.ignore +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/AGENTS.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/SETUP.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/SYNC.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/context/conventions.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/context/decisions.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/context/frontend.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/context/render-pipeline.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/context/setup.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/context/stack.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/patterns/INDEX.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/patterns/README.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/patterns/add-file-format.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/patterns/add-server-endpoint.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/patterns/animation-verify.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/patterns/debug-render.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/patterns/debug-vscode-extension-python.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/patterns/extend-compare-mode.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/patterns/extract-server-route-module.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/patterns/frontend-change.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/setup.sh +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.mex/sync.sh +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.opencode/opencode.json +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T18-46-49-737Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T18-48-21-979Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T18-51-35-665Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-07-01-393Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-14-37-969Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-21-30-940Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-23-08-126Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-29-33-155Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-31-25-336Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-31-53-789Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-39-12-257Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T19-39-16-449Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T20-15-25-513Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T20-25-13-179Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T20-39-01-435Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-01-27-659Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-01-41-283Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-03-00-625Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-04-12-887Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-33-39-044Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-38-01-530Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-45-20-383Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-55-11-545Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-56-03-307Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-56-35-733Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-57-12-181Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-57-37-748Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T21-58-13-679Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-06T22-37-23-895Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T00-39-18-637Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T01-41-46-243Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T04-31-48-472Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-14-15-632Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-14-47-582Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-16-23-471Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-17-10-247Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-18-24-707Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-20-06-164Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-20-28-342Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-21-54-962Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-22-34-666Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-23-11-336Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-23-36-260Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-24-09-267Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-24-35-434Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-25-57-010Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-34-48-823Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-46-46-468Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-48-17-930Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-49-26-400Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-50-31-563Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/page-2026-05-07T12-56-45-568Z.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.playwright-cli/theme-dark.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.python-version +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/.vscode/settings.json +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/AGENTS.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/CONTRIBUTING.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/DESIGN.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/LICENSE +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/README.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/docs/comparing.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/docs/configuration.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/docs/display.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/docs/index.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/docs/loading.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/docs/logo.png +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/docs/measurement.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/docs/remote.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/docs/viewing.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/matlab/arrayview.m +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/mkdocs.yml +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/plans/2026-04-14-immersive-animation.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/plans/2026-05-07-unified-colormap-picker.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/plans/arrayview_tool_menu_fix_prompt.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/plans/arrayview_tool_menu_implementation_plan.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/plans/egg-placement-mockup.html +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/plans/refactoring_plan.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/scripts/demo.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/scripts/release.sh +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_codex_open.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_diff.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_server.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/_vscode_shm.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/capture_v_animation.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/conftest.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_backend_shared.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_command_reachability.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_config.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_interactions.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_mode_entry_batching.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_stdio_server.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_torch.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/ui_audit.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/tests/visual_smoke.py +0 -0
- {arrayview-0.23.0 → arrayview-0.25.0}/vscode-extension/AGENTS.md +0 -0
- {arrayview-0.23.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-
|
|
23
|
+
last_updated: 2026-06-05
|
|
22
24
|
---
|
|
23
25
|
|
|
24
26
|
# Architecture
|
|
@@ -63,6 +65,18 @@ pywebview, or system browser).
|
|
|
63
65
|
- **`_stdio_server.py`** — Alternative to FastAPI for VS Code tunnel (direct webview): JSON on stdin, length-prefixed binary on stdout.
|
|
64
66
|
- **`_viewer.html`** — The entire frontend (~24 100 lines). CSS + JS in one file, no build step. Canvas-based rendering, WebSocket binary protocol, all viewing modes, reconcilers, command registry. See `context/frontend.md`.
|
|
65
67
|
|
|
68
|
+
## Frontend Tool Lifecycle
|
|
69
|
+
|
|
70
|
+
Tool launch, tool activation, and drawer visibility are separate states.
|
|
71
|
+
|
|
72
|
+
- Selecting an actionable tool activates it immediately and opens the right drawer.
|
|
73
|
+
- Closing the right drawer hides controls only; it does not deactivate the active tool.
|
|
74
|
+
- Re-selecting an active tool reopens its drawer when hidden, or hides the drawer when already visible.
|
|
75
|
+
- `Esc` targets visible UI first: drawer, launcher, picker, or help before exiting active tools.
|
|
76
|
+
- Passive tools such as Save open a drawer without entering a mode.
|
|
77
|
+
- Overlay and vector tools show their rendered layer immediately when selected and available.
|
|
78
|
+
- Existing tool output persists after mode exit unless the tool has an explicit destructive action.
|
|
79
|
+
|
|
66
80
|
## Display Routing
|
|
67
81
|
|
|
68
82
|
| Environment | Default display | Server mode |
|
|
@@ -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(
|
|
@@ -156,6 +156,7 @@ def _vprint(*args, **kwargs) -> None:
|
|
|
156
156
|
# ── Subprocess GUI Launcher ───────────────────────────────────────
|
|
157
157
|
|
|
158
158
|
_ICON_PNG_PATH: str | None = None
|
|
159
|
+
_LOOPBACK_HOST = "localhost"
|
|
159
160
|
|
|
160
161
|
|
|
161
162
|
def _get_icon_png_path() -> str | None:
|
|
@@ -177,6 +178,57 @@ def _get_icon_png_path() -> str | None:
|
|
|
177
178
|
return _ICON_PNG_PATH or None
|
|
178
179
|
|
|
179
180
|
|
|
181
|
+
def _build_inline_shell_html(url: str, shell_port: int) -> str | None:
|
|
182
|
+
"""Return embedded shell HTML for a cold-start native window."""
|
|
183
|
+
try:
|
|
184
|
+
shell_html = _pkg_files("arrayview").joinpath("_shell.html").read_text(
|
|
185
|
+
encoding="utf-8"
|
|
186
|
+
)
|
|
187
|
+
parsed = urllib.parse.urlparse(url)
|
|
188
|
+
inline_query = parsed.query
|
|
189
|
+
shell_html = shell_html.replace(
|
|
190
|
+
"</head>",
|
|
191
|
+
f"<script>"
|
|
192
|
+
f"window.__av_inline=true;"
|
|
193
|
+
f"window.__av_inlineQuery={inline_query!r};"
|
|
194
|
+
f"</script>\n"
|
|
195
|
+
f'<base href="http://{_LOOPBACK_HOST}:{shell_port}/">\n'
|
|
196
|
+
f"</head>",
|
|
197
|
+
1,
|
|
198
|
+
)
|
|
199
|
+
# Fix WebSocket URL — location.host is "" in inline html= mode
|
|
200
|
+
shell_html = shell_html.replace(
|
|
201
|
+
"`${proto}//${location.host}/ws/shell`",
|
|
202
|
+
f"`ws://{_LOOPBACK_HOST}:{shell_port}/ws/shell`",
|
|
203
|
+
)
|
|
204
|
+
return shell_html
|
|
205
|
+
except Exception:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _make_loopback_socket(port: int) -> "socket.socket":
|
|
210
|
+
"""Bind a TCP listener on the same loopback host the viewer URLs use."""
|
|
211
|
+
for family, socktype, proto, _, sockaddr in socket.getaddrinfo(
|
|
212
|
+
_LOOPBACK_HOST,
|
|
213
|
+
port,
|
|
214
|
+
type=socket.SOCK_STREAM,
|
|
215
|
+
):
|
|
216
|
+
sock = None
|
|
217
|
+
try:
|
|
218
|
+
sock = socket.socket(family, socktype, proto)
|
|
219
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
220
|
+
sock.bind(sockaddr)
|
|
221
|
+
sock.listen(128)
|
|
222
|
+
sock.set_inheritable(True)
|
|
223
|
+
return sock
|
|
224
|
+
except OSError:
|
|
225
|
+
try:
|
|
226
|
+
sock.close()
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
raise OSError(f"Could not bind {_LOOPBACK_HOST}:{port}")
|
|
230
|
+
|
|
231
|
+
|
|
180
232
|
def _open_webview(
|
|
181
233
|
url: str,
|
|
182
234
|
win_w: int,
|
|
@@ -199,25 +251,9 @@ def _open_webview(
|
|
|
199
251
|
inline_html_b64 = None
|
|
200
252
|
|
|
201
253
|
if shell_port is not None:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
shell_html = _pkg_files("arrayview").joinpath("_shell.html").read_text(encoding="utf-8")
|
|
205
|
-
# Inject inline-mode flag and hardcoded host (location.host is empty in html= mode)
|
|
206
|
-
shell_html = shell_html.replace(
|
|
207
|
-
"</head>",
|
|
208
|
-
f'<script>window.__av_inline=true;</script>\n'
|
|
209
|
-
f'<base href="http://localhost:{shell_port}/">\n'
|
|
210
|
-
f"</head>",
|
|
211
|
-
1,
|
|
212
|
-
)
|
|
213
|
-
# Fix WebSocket URL — location.host is "" in inline html= mode
|
|
214
|
-
shell_html = shell_html.replace(
|
|
215
|
-
"`${proto}//${location.host}/ws/shell`",
|
|
216
|
-
f"`ws://localhost:{shell_port}/ws/shell`",
|
|
217
|
-
)
|
|
254
|
+
shell_html = _build_inline_shell_html(url, shell_port)
|
|
255
|
+
if shell_html is not None:
|
|
218
256
|
inline_html_b64 = _b64.b64encode(shell_html.encode()).decode()
|
|
219
|
-
except Exception:
|
|
220
|
-
pass # fall back to URL mode
|
|
221
257
|
|
|
222
258
|
if inline_html_b64:
|
|
223
259
|
script_lines = [
|
|
@@ -469,7 +505,7 @@ def _open_webview_cli(
|
|
|
469
505
|
|
|
470
506
|
def _server_alive(port: int, timeout: float = 0.5) -> bool:
|
|
471
507
|
"""Return True only if an ArrayView server is responding on the port."""
|
|
472
|
-
url = f"http://
|
|
508
|
+
url = f"http://{_LOOPBACK_HOST}:{port}/ping"
|
|
473
509
|
try:
|
|
474
510
|
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
475
511
|
if resp.status != 200:
|
|
@@ -482,7 +518,7 @@ def _server_alive(port: int, timeout: float = 0.5) -> bool:
|
|
|
482
518
|
|
|
483
519
|
def _server_pid(port: int) -> int | None:
|
|
484
520
|
"""Return the pid of the responding ArrayView server, or None if unreachable."""
|
|
485
|
-
url = f"http://
|
|
521
|
+
url = f"http://{_LOOPBACK_HOST}:{port}/ping"
|
|
486
522
|
try:
|
|
487
523
|
with urllib.request.urlopen(url, timeout=0.5) as resp:
|
|
488
524
|
if resp.status != 200:
|
|
@@ -497,7 +533,7 @@ def _server_pid(port: int) -> int | None:
|
|
|
497
533
|
|
|
498
534
|
def _server_hostname(port: int, timeout: float = 0.5) -> str | None:
|
|
499
535
|
"""Return the hostname reported by the ArrayView server on ``port``, or None."""
|
|
500
|
-
url = f"http://
|
|
536
|
+
url = f"http://{_LOOPBACK_HOST}:{port}/ping"
|
|
501
537
|
try:
|
|
502
538
|
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
503
539
|
if resp.status != 200:
|
|
@@ -515,7 +551,7 @@ def _relay_array_to_server(
|
|
|
515
551
|
port: int,
|
|
516
552
|
name: str,
|
|
517
553
|
rgb: bool = False,
|
|
518
|
-
relay_host: str =
|
|
554
|
+
relay_host: str = _LOOPBACK_HOST,
|
|
519
555
|
) -> None:
|
|
520
556
|
"""Load *filepath* locally and POST the bytes to an ArrayView relay server.
|
|
521
557
|
|
|
@@ -523,7 +559,7 @@ def _relay_array_to_server(
|
|
|
523
559
|
ArrayView server (e.g. tunnel-remote). The relay server registers the
|
|
524
560
|
session and writes its own VS Code signal file so a viewer tab opens there.
|
|
525
561
|
|
|
526
|
-
``relay_host`` defaults to
|
|
562
|
+
``relay_host`` defaults to localhost; only change it when the relay server
|
|
527
563
|
is genuinely on a different network interface (rare).
|
|
528
564
|
"""
|
|
529
565
|
import base64
|
|
@@ -567,7 +603,7 @@ type _CompareSids = list[str] | tuple[str, ...]
|
|
|
567
603
|
|
|
568
604
|
def _server_json_request(port: int, path: str, payload: dict) -> dict:
|
|
569
605
|
req = urllib.request.Request(
|
|
570
|
-
f"http://
|
|
606
|
+
f"http://{_LOOPBACK_HOST}:{port}{path}",
|
|
571
607
|
data=json.dumps(payload).encode(),
|
|
572
608
|
headers={"Content-Type": "application/json"},
|
|
573
609
|
method="POST",
|
|
@@ -948,7 +984,13 @@ def _handle_cli_spawned_daemon(
|
|
|
948
984
|
f" rgb={rgb},"
|
|
949
985
|
f")"
|
|
950
986
|
)
|
|
951
|
-
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
|
+
)
|
|
952
994
|
|
|
953
995
|
early_webview_opened = False
|
|
954
996
|
early_webview_notified = False
|
|
@@ -1002,7 +1044,7 @@ def _handle_cli_spawned_daemon(
|
|
|
1002
1044
|
window_mode=window_mode,
|
|
1003
1045
|
floating=floating,
|
|
1004
1046
|
is_remote=is_remote,
|
|
1005
|
-
webview_already_opened=
|
|
1047
|
+
webview_already_opened=early_webview_opened,
|
|
1006
1048
|
)
|
|
1007
1049
|
|
|
1008
1050
|
|
|
@@ -1170,7 +1212,7 @@ def _promote_view_to_vscode_terminal(
|
|
|
1170
1212
|
|
|
1171
1213
|
def _port_in_use(port: int) -> bool:
|
|
1172
1214
|
try:
|
|
1173
|
-
with socket.create_connection((
|
|
1215
|
+
with socket.create_connection((_LOOPBACK_HOST, port), timeout=0.3):
|
|
1174
1216
|
return True
|
|
1175
1217
|
except OSError:
|
|
1176
1218
|
return False
|
|
@@ -1311,25 +1353,17 @@ async def _serve_background(port: int, stop_when_closed: bool = False):
|
|
|
1311
1353
|
_loading_port = None # reset for this server lifetime
|
|
1312
1354
|
_session_mod.SERVER_LOOP = asyncio.get_running_loop()
|
|
1313
1355
|
_session_mod.SERVER_PORT = port
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
#
|
|
1317
|
-
|
|
1318
|
-
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
|
1319
|
-
sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
|
|
1320
|
-
sock.bind(("127.0.0.1", port))
|
|
1321
|
-
sock.listen(128)
|
|
1322
|
-
sock.set_inheritable(True)
|
|
1356
|
+
# Bind on the same loopback hostname the viewer URLs use. On macOS,
|
|
1357
|
+
# localhost often resolves to ::1 first, so binding only 127.0.0.1 can
|
|
1358
|
+
# leave the native shell stuck on its loading overlay forever.
|
|
1359
|
+
sock = _make_loopback_socket(port)
|
|
1323
1360
|
|
|
1324
1361
|
# Bind the loading-page server on an OS-chosen ephemeral port.
|
|
1325
1362
|
# This uses only stdlib so it starts in microseconds — well before
|
|
1326
1363
|
# uvicorn's heavy imports finish. _loading_port is read by the main
|
|
1327
1364
|
# thread after _server_ready_event fires.
|
|
1328
1365
|
try:
|
|
1329
|
-
_lsock =
|
|
1330
|
-
_lsock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
|
|
1331
|
-
_lsock.bind(("127.0.0.1", 0))
|
|
1332
|
-
_lsock.listen(16)
|
|
1366
|
+
_lsock = _make_loopback_socket(0)
|
|
1333
1367
|
_loading_port = _lsock.getsockname()[1]
|
|
1334
1368
|
threading.Thread(
|
|
1335
1369
|
target=_run_loading_server, args=(_lsock,), daemon=True
|
|
@@ -1361,7 +1395,7 @@ def _with_loading(url: str) -> str:
|
|
|
1361
1395
|
"""
|
|
1362
1396
|
if _loading_port is not None:
|
|
1363
1397
|
encoded = urllib.parse.quote(url, safe="")
|
|
1364
|
-
return f"http://
|
|
1398
|
+
return f"http://{_LOOPBACK_HOST}:{_loading_port}/?target={encoded}"
|
|
1365
1399
|
return url
|
|
1366
1400
|
|
|
1367
1401
|
|
|
@@ -1615,7 +1649,7 @@ class ViewHandle(str):
|
|
|
1615
1649
|
_np.save(buf, arr)
|
|
1616
1650
|
body = buf.getvalue()
|
|
1617
1651
|
request = _req.Request(
|
|
1618
|
-
f"http://
|
|
1652
|
+
f"http://{_LOOPBACK_HOST}:{self._port}/update/{self._sid}",
|
|
1619
1653
|
data=body,
|
|
1620
1654
|
method="POST",
|
|
1621
1655
|
)
|
|
@@ -1625,7 +1659,7 @@ class ViewHandle(str):
|
|
|
1625
1659
|
except Exception as e:
|
|
1626
1660
|
raise RuntimeError(
|
|
1627
1661
|
f"[ArrayView] Failed to update viewer: {e}\n"
|
|
1628
|
-
f" URL: http://
|
|
1662
|
+
f" URL: http://{_LOOPBACK_HOST}:{self._port}/update/{self._sid}\n"
|
|
1629
1663
|
f" Is the ArrayView server still running?"
|
|
1630
1664
|
) from e
|
|
1631
1665
|
|
|
@@ -2254,13 +2288,18 @@ async def _stop_server_when_viewer_closes(
|
|
|
2254
2288
|
Used in script mode so the non-daemon server thread exits cleanly when done."""
|
|
2255
2289
|
import arrayview._session as _sm
|
|
2256
2290
|
|
|
2291
|
+
connections_before = _sm.VIEWER_CONNECTIONS_SEEN
|
|
2257
2292
|
deadline = time.monotonic() + connect_timeout
|
|
2258
|
-
while
|
|
2293
|
+
while (
|
|
2294
|
+
_sm.VIEWER_SOCKETS == 0
|
|
2295
|
+
and _sm.VIEWER_CONNECTIONS_SEEN == connections_before
|
|
2296
|
+
):
|
|
2259
2297
|
if time.monotonic() > deadline:
|
|
2260
2298
|
server.should_exit = True # no viewer connected; give up
|
|
2261
2299
|
return
|
|
2262
2300
|
await asyncio.sleep(0.2)
|
|
2263
|
-
# 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.
|
|
2264
2303
|
while True:
|
|
2265
2304
|
while _sm.VIEWER_SOCKETS > 0:
|
|
2266
2305
|
await asyncio.sleep(0.2)
|
|
@@ -2295,10 +2334,14 @@ def _wait_for_viewer_close(
|
|
|
2295
2334
|
"""
|
|
2296
2335
|
import arrayview._session as _sm
|
|
2297
2336
|
|
|
2337
|
+
connections_before = _sm.VIEWER_CONNECTIONS_SEEN
|
|
2298
2338
|
connect_deadline = (
|
|
2299
2339
|
None if connect_timeout is None else time.monotonic() + connect_timeout
|
|
2300
2340
|
)
|
|
2301
|
-
while
|
|
2341
|
+
while (
|
|
2342
|
+
_sm.VIEWER_SOCKETS == 0
|
|
2343
|
+
and _sm.VIEWER_CONNECTIONS_SEEN == connections_before
|
|
2344
|
+
):
|
|
2302
2345
|
if connect_deadline is not None and time.monotonic() >= connect_deadline:
|
|
2303
2346
|
return
|
|
2304
2347
|
time.sleep(0.2)
|
|
@@ -2507,7 +2550,7 @@ def _serve_empty(port: int) -> None:
|
|
|
2507
2550
|
threading.Thread(
|
|
2508
2551
|
target=lambda: _uvicorn().run(
|
|
2509
2552
|
_server_mod().app,
|
|
2510
|
-
host=
|
|
2553
|
+
host=_LOOPBACK_HOST,
|
|
2511
2554
|
port=port,
|
|
2512
2555
|
log_level="error",
|
|
2513
2556
|
timeout_keep_alive=30,
|
|
@@ -2548,13 +2591,7 @@ def _serve_daemon(
|
|
|
2548
2591
|
_session_mod.PENDING_SESSION_EVENTS[sid] = _pending_event
|
|
2549
2592
|
_session_mod.SERVER_PORT = port
|
|
2550
2593
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
|
2554
|
-
sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
|
|
2555
|
-
sock.bind(("127.0.0.1", port))
|
|
2556
|
-
sock.listen(128)
|
|
2557
|
-
sock.set_inheritable(True)
|
|
2594
|
+
sock = _make_loopback_socket(port)
|
|
2558
2595
|
|
|
2559
2596
|
def _run_uvicorn_on_socket():
|
|
2560
2597
|
config = _uvicorn().Config(
|
|
@@ -2581,7 +2618,7 @@ def _serve_daemon(
|
|
|
2581
2618
|
threading.Thread(target=_warm_luts, daemon=True).start()
|
|
2582
2619
|
|
|
2583
2620
|
def _load():
|
|
2584
|
-
from arrayview._io import load_data, load_data_with_meta,
|
|
2621
|
+
from arrayview._io import load_data, load_data_with_meta, list_array_keys
|
|
2585
2622
|
|
|
2586
2623
|
try:
|
|
2587
2624
|
data, spatial_meta = load_data_with_meta(filepath)
|
|
@@ -2595,13 +2632,13 @@ def _serve_daemon(
|
|
|
2595
2632
|
)
|
|
2596
2633
|
session.sid = sid
|
|
2597
2634
|
session.spatial_meta = spatial_meta
|
|
2598
|
-
# Multi-array .npz: store keys so the viewer can show a picker
|
|
2599
|
-
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:
|
|
2600
2637
|
try:
|
|
2601
|
-
keys =
|
|
2638
|
+
keys = list_array_keys(filepath)
|
|
2602
2639
|
if len(keys) > 1:
|
|
2603
|
-
session.
|
|
2604
|
-
session.
|
|
2640
|
+
session.array_keys = keys
|
|
2641
|
+
session.array_filepath = filepath
|
|
2605
2642
|
except Exception:
|
|
2606
2643
|
pass
|
|
2607
2644
|
if spatial_meta is not None:
|
|
@@ -2741,7 +2778,7 @@ def _start_watch_thread(filepath: str, sid: str, port: int) -> None:
|
|
|
2741
2778
|
last_mtime = mtime
|
|
2742
2779
|
try:
|
|
2743
2780
|
req = _urlreq.Request(
|
|
2744
|
-
f"http://
|
|
2781
|
+
f"http://{_LOOPBACK_HOST}:{port}/reload/{sid}",
|
|
2745
2782
|
data=b"",
|
|
2746
2783
|
method="POST",
|
|
2747
2784
|
)
|
|
@@ -3209,7 +3246,7 @@ def arrayview():
|
|
|
3209
3246
|
if ":" in relay_str:
|
|
3210
3247
|
relay_host, relay_port_str = relay_str.rsplit(":", 1)
|
|
3211
3248
|
else:
|
|
3212
|
-
relay_host, relay_port_str =
|
|
3249
|
+
relay_host, relay_port_str = _LOOPBACK_HOST, relay_str
|
|
3213
3250
|
try:
|
|
3214
3251
|
relay_port = int(relay_port_str)
|
|
3215
3252
|
except ValueError:
|