arrayview 0.31.2__tar.gz → 0.31.3__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.31.2 → arrayview-0.31.3}/.github/workflows/launch-contracts.yml +4 -4
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/lifecycle.md +7 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/project-state.md +6 -5
- {arrayview-0.31.2 → arrayview-0.31.3}/PKG-INFO +1 -1
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/index.md +2 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/loading.md +4 -45
- arrayview-0.31.3/docs/stack-overlays.md +129 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/mkdocs.yml +1 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/pyproject.toml +1 -1
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_loading.py +2 -1
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_session.py +1 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_vscode_browser.py +15 -6
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_vscode_extension.py +174 -23
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_vscode_signal.py +28 -0
- arrayview-0.31.3/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_api.py +20 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_cli.py +2 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_display_opener_results.py +20 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_lifecycle_contract.py +119 -4
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_vscode_ack_protocol.py +43 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/uv.lock +1 -1
- {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/extension.js +98 -26
- {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/lifecycle_helpers.js +27 -1
- {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/package.json +1 -1
- {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/test_lifecycle_helpers.js +18 -1
- arrayview-0.31.2/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/frontend-designer/SKILL.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/SKILL.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/element-attributes.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/playwright-tests.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/request-mocking.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/running-code.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/session-management.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/storage-state.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/test-generation.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/tracing.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/video-recording.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/todo-workflow/SKILL.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.github/copilot-instructions.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.github/workflows/docs.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.gitignore +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.ignore +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/AGENTS.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/ROUTER.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/SETUP.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/SYNC.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/architecture.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/conventions.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/decisions.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/frontend.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/render-pipeline.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/setup.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/stack.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/INDEX.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/README.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/add-file-format.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/add-server-endpoint.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/animation-verify.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/debug-render.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/debug-vscode-extension-python.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/extend-compare-mode.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/extract-server-route-module.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/frontend-change.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/setup.sh +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.mex/sync.sh +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.opencode/opencode.json +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T18-46-49-737Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T18-48-21-979Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T18-51-35-665Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-07-01-393Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-14-37-969Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-21-30-940Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-23-08-126Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-29-33-155Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-31-25-336Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-31-53-789Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-39-12-257Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-39-16-449Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T20-15-25-513Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T20-25-13-179Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T20-39-01-435Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-01-27-659Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-01-41-283Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-03-00-625Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-04-12-887Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-33-39-044Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-38-01-530Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-45-20-383Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-55-11-545Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-56-03-307Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-56-35-733Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-57-12-181Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-57-37-748Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-58-13-679Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T22-37-23-895Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T00-39-18-637Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T01-41-46-243Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T04-31-48-472Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-14-15-632Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-14-47-582Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-16-23-471Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-17-10-247Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-18-24-707Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-20-06-164Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-20-28-342Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-21-54-962Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-22-34-666Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-23-11-336Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-23-36-260Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-24-09-267Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-24-35-434Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-25-57-010Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-34-48-823Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-46-46-468Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-48-17-930Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-49-26-400Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-50-31-563Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-56-45-568Z.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/theme-dark.yml +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.python-version +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/.vscode/settings.json +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/AGENTS.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/CONTRIBUTING.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/DESIGN.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/LICENSE +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/README.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/comparing.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/configuration.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/display.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/foreign-hosts.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/launch-routing-refactor.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/lifecycle.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/logo.png +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/measurement.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/remote.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/viewing.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/docs/vscode-window-routing-plan.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/experiments/ws_tunnel_diag/ws_tunnel_diag.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/matlab/arrayview.m +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/plans/2026-04-14-immersive-animation.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/plans/2026-05-07-unified-colormap-picker.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/plans/arrayview_tool_menu_fix_prompt.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/plans/arrayview_tool_menu_implementation_plan.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/plans/egg-placement-mockup.html +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/plans/refactoring_plan.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/plans/roi-analysis-requirements.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/plans/webview/LOG.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/scripts/demo.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/scripts/release.sh +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/__init__.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/__main__.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_app.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_codex_open.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_config.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_diff.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_icon.png +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_io.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_platform.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_render.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_websocket.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_server.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_shell.html +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_torch.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_viewer.html +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/capture_v_animation.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/conftest.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/lifecycle_matrix.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_backend_shared.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_browser.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_colorbar_hover_highlight.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_command_reachability.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_config.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_doctor_commands.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_instance_registry.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_interactions.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_invocation_contract_matrix.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_io.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_large_arrays.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_launch_diagnostics.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_launch_plan.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_loading_server.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_nifti_series.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_server_identity_status.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_session_spec.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_torch.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_view_handle_lifecycle.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_view_launch_execution.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/ui_audit.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/tests/visual_smoke.py +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/AGENTS.md +0 -0
- {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/LICENSE +0 -0
|
@@ -30,12 +30,12 @@ jobs:
|
|
|
30
30
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
31
31
|
runs-on: ${{ matrix.os }}
|
|
32
32
|
steps:
|
|
33
|
-
- uses: actions/checkout@
|
|
34
|
-
- uses: actions/setup-python@
|
|
33
|
+
- uses: actions/checkout@v7
|
|
34
|
+
- uses: actions/setup-python@v6
|
|
35
35
|
with:
|
|
36
36
|
python-version: "3.12"
|
|
37
|
-
- uses: astral-sh/setup-uv@
|
|
38
|
-
- uses: actions/setup-node@
|
|
37
|
+
- uses: astral-sh/setup-uv@v8
|
|
38
|
+
- uses: actions/setup-node@v6
|
|
39
39
|
with:
|
|
40
40
|
node-version: "22"
|
|
41
41
|
- run: uv sync --frozen --group dev --group test
|
|
@@ -70,6 +70,13 @@ This contract describes who owns the backend, when it starts, and what closes it
|
|
|
70
70
|
- `release_session()` is the session-release primitive.
|
|
71
71
|
- Viewer WebSocket connect/disconnect owns active viewer counts.
|
|
72
72
|
- URL panel disposal must release every SID encoded in the URL: `sid`, `compare_sid`, `compare_sids`, and `overlay_sid`.
|
|
73
|
+
- Tunnel panel disposal posts release requests to the local backend immediately;
|
|
74
|
+
the forwarded public URL is display-only and is not the cleanup authority.
|
|
75
|
+
- Reused file and collection sessions acquire one lease per tab, so closing one
|
|
76
|
+
tab cannot invalidate another tab that shares the same SID.
|
|
77
|
+
- A VS Code readiness ACK includes the live opener version and is terminal only
|
|
78
|
+
after the requested SID exists and the viewer reports its first rendered frame.
|
|
79
|
+
- Older ArrayView packages must not delete or downgrade a newer installed opener.
|
|
73
80
|
- Tunnel registration cleanup must not remove live same-tunnel sibling windows.
|
|
74
81
|
- Explicit cleanup wins over implicit disappearance.
|
|
75
82
|
- Any VS Code extension source change must rebuild `src/arrayview/arrayview-opener.vsix` and keep the packaged version in sync.
|
|
@@ -7,7 +7,7 @@ triggers:
|
|
|
7
7
|
- "recent work"
|
|
8
8
|
- "active feature"
|
|
9
9
|
- "shipped recently"
|
|
10
|
-
last_updated: 2026-
|
|
10
|
+
last_updated: 2026-07-13
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Project State
|
|
@@ -21,7 +21,7 @@ last_updated: 2026-06-30
|
|
|
21
21
|
- Backend transport: FastAPI HTTP/WebSocket is the single viewer transport; shared helpers keep route modules small for metadata/analysis, compare/diff, overlay compositing, and vector field layout/arrow sampling.
|
|
22
22
|
- NIfTI spatial metadata, RAS resampling
|
|
23
23
|
- Directory collections are header-scanned and lazy by default: compatible files form a dense virtual stack, mixed shapes use a ragged collection, and `--load lazy|eager` plus `--stack-policy auto|dense|ragged` make both choices explicit. Compressed NIfTI volumes use a byte-bounded LRU cache; `view_dir()` exposes the same controls.
|
|
24
|
-
- VS Code extension v0.14.
|
|
24
|
+
- VS Code extension v0.14.41 — stable per-window routing, queued concurrent requests, remote-only tunnel claims, live opener-version ACKs, first-frame readiness, and immediate local-backend session release. An installed-but-stale extension host now fails with a clear one-time reload instruction instead of opening an unverified blank tab; older cached ArrayView packages never delete a newer opener.
|
|
25
25
|
- Colorbar refactor: `ColorBar` JS class partially migrated (in progress)
|
|
26
26
|
- Colormap picker: `c` opens an expanded colorbar-island grid without changing the colormap; subsequent `c` taps cycle, hover/hjkl/arrows live-preview, Enter/click commits, Esc cancels, and auto-dismiss pauses while hovered
|
|
27
27
|
- Cold-start loading spinner in VS Code and native shell
|
|
@@ -48,9 +48,10 @@ last_updated: 2026-06-30
|
|
|
48
48
|
patient/modality directory layouts pair automatically. `--case-regex` remains
|
|
49
49
|
an override for unusual layouts. CLI header scans show an in-place file counter
|
|
50
50
|
and elapsed time on interactive terminals.
|
|
51
|
-
- Reused collection sessions are protected by per-tab leases, including
|
|
52
|
-
overlay sessions
|
|
53
|
-
|
|
51
|
+
- Reused file and collection sessions are protected by per-tab leases, including
|
|
52
|
+
collection overlay sessions. The VS Code opener verifies the local SID before
|
|
53
|
+
creating a panel and waits for the viewer's first rendered frame before
|
|
54
|
+
acknowledging `backend_ready`.
|
|
54
55
|
- Filesystem picker endpoint (`GET /fs/list`) clamped to `$HOME`. Accepts `base_sid` + `mode` (`overlay` | `vectorfield`) to filter entries by a cheap header-shape peek (`.npy`, `.nii`/`.nii.gz`, `.h5`, `.zarr`); overlay mode requires identical shape, vectorfield mode requires base shape plus one axis of size 3
|
|
55
56
|
- Island collapse affordance: inline `~` at the island's top-right animates the panel into the bottom-left `~` hint circle; external `~` hint only visible while the island is actually collapsed. New `/` hint circle at bottom-right opens the tool menu
|
|
56
57
|
- Compare center tool menu: `/` now re-opens the last-used compare center mode from the tool menu, while compare pane header buttons select diff / overlay / wipe directly. Eligible two-array compare layouts can switch into a big-left arrangement with a wider center pane, and the diff colorbar now matches that center-pane width.
|
|
@@ -30,6 +30,8 @@ MATLAB and Julia setup: [MATLAB and Julia](foreign-hosts.md).
|
|
|
30
30
|
|
|
31
31
|
Remote and tunnel setup: [Remote](remote.md).
|
|
32
32
|
|
|
33
|
+
Stacked patient collections and masks: [Stack and Overlay Collections](stack-overlays.md).
|
|
34
|
+
|
|
33
35
|
## Formats
|
|
34
36
|
|
|
35
37
|
`.npy` `.npz` `.nii` `.nii.gz` `.zarr` `.pt` `.h5` `.tif` `.mat`
|
|
@@ -85,7 +85,7 @@ Native windows also show a static preview while the live viewer warms up.
|
|
|
85
85
|
```bash
|
|
86
86
|
uvx arrayview base.npy moving.npy # compare mode
|
|
87
87
|
uvx arrayview volume.nii.gz --overlay mask.nii.gz
|
|
88
|
-
uvx arrayview volume.nii.gz --overlay "ground truth=
|
|
88
|
+
uvx arrayview volume.nii.gz --overlay "ground truth=mask_gt.nii.gz" --overlay "prediction=mask_pred.nii.gz"
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
An unnamed overlay uses the filename stem as its display name (`mask` above).
|
|
@@ -99,8 +99,7 @@ shows all overlays at once.
|
|
|
99
99
|
|
|
100
100
|
## Directory Pattern Collections
|
|
101
101
|
|
|
102
|
-
Use `--stack` when reviewing many arrays.
|
|
103
|
-
files recursively; passing patterns keeps the aligned channel/overlay workflow.
|
|
102
|
+
Use `--stack` when reviewing many arrays.
|
|
104
103
|
|
|
105
104
|
```bash
|
|
106
105
|
uvx arrayview --stack scans/
|
|
@@ -115,48 +114,8 @@ dense` requires matching shapes, while `--stack-policy ragged` forces collection
|
|
|
115
114
|
semantics. `--load eager` is intended for small datasets that should be loaded
|
|
116
115
|
up front.
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
uvx arrayview --stack "data/**/*_0000.nii.gz" "data/**/*_0001.nii.gz"
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Overlays become named recursive patterns:
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
uvx arrayview --stack \
|
|
128
|
-
"data/**/*_0000.nii.gz" \
|
|
129
|
-
"data/**/*_0001.nii.gz" \
|
|
130
|
-
--overlay "gt=data/**/labels/**/*.nii.gz" \
|
|
131
|
-
--overlay "pred=data/**/pred/**/*.nii.gz"
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
When every case has a directory containing separately named masks,
|
|
135
|
-
`--overlay-dir` discovers one overlay role per filename. Masks that are absent
|
|
136
|
-
for a case are shown as empty. ArrayView infers the shared case directory from
|
|
137
|
-
layouts such as `sub-0001/T1_W/` and `sub-0001/masks/`:
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
|
-
uvx arrayview --stack "data/*/T1_W/*.nii.gz" \
|
|
141
|
-
--overlay-dir "data/*/masks"
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Repeat `--overlay-dir` to combine masks from multiple per-case directories.
|
|
145
|
-
|
|
146
|
-
ArrayView pairs files by sorted match order. The first match from each pattern
|
|
147
|
-
belongs to the first case, the second match belongs to the second case, and so
|
|
148
|
-
on. Use `--dry-run` to inspect matches without opening a viewer.
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
uvx arrayview --stack "data/**/*_0000.nii.gz" --overlay "gt=data/**/labels/*.nii.gz" --dry-run
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
For unusual or ambiguous layouts, pass a regex with a `case` group to override
|
|
155
|
-
automatic directory inference:
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
uvx arrayview --stack "data/**/*.nii.gz" --case-regex "(?P<case>patient-[0-9]+)"
|
|
159
|
-
```
|
|
117
|
+
Pattern collections, named overlays, multiple overlays, and `--overlay-dir`:
|
|
118
|
+
[Stack and Overlay Collections](stack-overlays.md).
|
|
160
119
|
|
|
161
120
|
## NIfTI Series (4D/5D from a directory)
|
|
162
121
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Stack and Overlay Collections
|
|
2
|
+
|
|
3
|
+
Review many arrays with aligned image and mask patterns.
|
|
4
|
+
|
|
5
|
+
## Patient Stack
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uvx arrayview --stack "data/*/T1w.nii.gz"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`*` matches one path segment. `**` matches recursively:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uvx arrayview --stack "data/**/*.nii.gz"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
ArrayView sorts matches and adds a collection index dimension. Use `--dry-run`
|
|
18
|
+
to inspect pairings without opening the viewer:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uvx arrayview --stack "data/*/T1w.nii.gz" --dry-run
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## MR Contrasts
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uvx arrayview --stack \
|
|
28
|
+
"data/*/T1w.nii.gz" \
|
|
29
|
+
"data/*/T2w.nii.gz" \
|
|
30
|
+
"data/*/FLAIR.nii.gz"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Each positional pattern becomes a contrast dimension. The first `T1w`, first
|
|
34
|
+
`T2w`, and first `FLAIR` belong to patient 1; the second match from each pattern
|
|
35
|
+
belongs to patient 2.
|
|
36
|
+
|
|
37
|
+
For ambiguous layouts, define the patient key:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvx arrayview --stack "data/**/*.nii.gz" \
|
|
41
|
+
--case-regex "(?P<case>sub-[0-9]+)"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Ragged Shapes
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uvx arrayview --stack "data/*/T1w.nii.gz" --stack-policy ragged
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`--stack-policy auto` uses a dense stack when shapes match and ragged mode when
|
|
51
|
+
they do not. `--stack-policy dense` requires matching shapes.
|
|
52
|
+
|
|
53
|
+
## One Overlay
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uvx arrayview --stack "data/*/T1w.nii.gz" \
|
|
57
|
+
--overlay "lesion=data/*/masks/lesion.nii.gz"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use `name=pattern` to control the overlay label. Without `name=`, the filename
|
|
61
|
+
stem is used.
|
|
62
|
+
|
|
63
|
+
## Multiple Overlays
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uvx arrayview --stack "data/*/T1w.nii.gz" \
|
|
67
|
+
--overlay "gt=data/*/masks/gt.nii.gz" \
|
|
68
|
+
--overlay "pred=data/*/masks/pred.nii.gz"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Overlays are paired to cases by sorted match order unless `--case-regex` is
|
|
72
|
+
provided.
|
|
73
|
+
|
|
74
|
+
## Overlay Directories
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uvx arrayview --stack "data/*/T1w.nii.gz" \
|
|
78
|
+
--overlay-dir "data/*/masks"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`--overlay-dir` creates one overlay role per mask filename inside each matched
|
|
82
|
+
directory. Missing masks are shown as empty overlays.
|
|
83
|
+
|
|
84
|
+
Repeat it to combine mask folders:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uvx arrayview --stack "data/*/T1w.nii.gz" \
|
|
88
|
+
--overlay-dir "data/*/manual_masks" \
|
|
89
|
+
--overlay-dir "data/*/model_masks"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Complete Example
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
data/
|
|
96
|
+
sub-001/
|
|
97
|
+
anat/T1w.nii.gz
|
|
98
|
+
anat/T2w.nii.gz
|
|
99
|
+
masks/lesion.nii.gz
|
|
100
|
+
masks/edema.nii.gz
|
|
101
|
+
sub-002/
|
|
102
|
+
anat/T1w.nii.gz
|
|
103
|
+
anat/T2w.nii.gz
|
|
104
|
+
masks/lesion.nii.gz
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
uvx arrayview --stack \
|
|
109
|
+
"data/*/anat/T1w.nii.gz" \
|
|
110
|
+
"data/*/anat/T2w.nii.gz" \
|
|
111
|
+
--overlay-dir "data/*/masks" \
|
|
112
|
+
--stack-policy auto \
|
|
113
|
+
--dry-run
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Remove `--dry-run` to open the viewer.
|
|
117
|
+
|
|
118
|
+
## Compressed NIfTI Performance
|
|
119
|
+
|
|
120
|
+
`.nii.gz` files are lazy through nibabel, but gzip compression limits random
|
|
121
|
+
slice access. Large compressed collections can feel slower when scrolling across
|
|
122
|
+
many patients or contrasts.
|
|
123
|
+
|
|
124
|
+
For repeated review, prefer uncompressed `.nii`, chunked `.zarr`, or a local
|
|
125
|
+
fast disk cache. Use `--load eager` only for small collections that fit in RAM:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
uvx arrayview --stack "data/*/T1w.nii.gz" --load eager
|
|
129
|
+
```
|
|
@@ -141,7 +141,8 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
|
|
|
141
141
|
else:
|
|
142
142
|
for existing in SESSIONS.values():
|
|
143
143
|
if existing.filepath and os.path.abspath(existing.filepath) == abs_path:
|
|
144
|
-
|
|
144
|
+
if acquire_session_leases([existing.sid]):
|
|
145
|
+
return {"sid": existing.sid, "name": existing.name, "notified": False}
|
|
145
146
|
if not dir_patterns and not os.environ.get("ARRAYVIEW_SKIP_RAM_GUARD"):
|
|
146
147
|
from ._io import FULL_LOAD_EXTS
|
|
147
148
|
|
|
@@ -283,6 +283,7 @@ class Session:
|
|
|
283
283
|
self.fft_original_data = None
|
|
284
284
|
self.fft_axes = None
|
|
285
285
|
self.data_version = 0 # incremented by /reload when file is reloaded
|
|
286
|
+
self.viewer_leases = 1 # one release is required for each tab using this SID
|
|
286
287
|
|
|
287
288
|
self.rgb_axis = None # set by _setup_rgb(); axis index in actual shape
|
|
288
289
|
self.spatial_shape = data.shape # shape without rgb_axis (set by _setup_rgb)
|
|
@@ -16,7 +16,7 @@ from enum import Enum
|
|
|
16
16
|
|
|
17
17
|
from arrayview._session import _vprint
|
|
18
18
|
from arrayview._platform import _in_vscode_terminal, _is_vscode_remote
|
|
19
|
-
from arrayview._vscode_extension import _configure_vscode_port_preview, _ensure_vscode_extension
|
|
19
|
+
from arrayview._vscode_extension import _configure_vscode_port_preview, _ensure_vscode_extension
|
|
20
20
|
from arrayview._vscode_signal import (
|
|
21
21
|
AckState,
|
|
22
22
|
_open_via_signal_file,
|
|
@@ -133,9 +133,13 @@ def _open_browser(
|
|
|
133
133
|
if is_remote:
|
|
134
134
|
# Remote/tunnel: install extension + write signal file.
|
|
135
135
|
ext_ok = _ensure_vscode_extension()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
from arrayview import _vscode_extension as _extension_state
|
|
137
|
+
if not ext_ok and _extension_state._VSCODE_EXT_RELOAD_REQUIRED:
|
|
138
|
+
return OpenResult(
|
|
139
|
+
OpenState.FAILED,
|
|
140
|
+
"vscode-extension",
|
|
141
|
+
"ArrayView updated its VS Code opener; reload this VS Code window once, then retry",
|
|
142
|
+
)
|
|
139
143
|
# URL-based mode: port is forwarded by VS Code and the viewer
|
|
140
144
|
# connects via WebSocket through the devtunnel.
|
|
141
145
|
_configure_vscode_port_preview(parsed_port)
|
|
@@ -172,8 +176,13 @@ def _open_browser(
|
|
|
172
176
|
# Local VS Code terminal (or --window vscode forced): install extension + signal file.
|
|
173
177
|
_configure_vscode_port_preview(parsed_port)
|
|
174
178
|
ext_ok = _ensure_vscode_extension()
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
from arrayview import _vscode_extension as _extension_state
|
|
180
|
+
if not ext_ok and _extension_state._VSCODE_EXT_RELOAD_REQUIRED:
|
|
181
|
+
return OpenResult(
|
|
182
|
+
OpenState.FAILED,
|
|
183
|
+
"vscode-extension",
|
|
184
|
+
"ArrayView updated its VS Code opener; reload this VS Code window once, then retry",
|
|
185
|
+
)
|
|
177
186
|
# Always write the signal file: the extension may already be
|
|
178
187
|
# installed even if _ensure failed (e.g. `code` CLI not found).
|
|
179
188
|
request = _open_via_signal_file(
|
|
@@ -5,7 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
+
import signal
|
|
8
9
|
import subprocess
|
|
10
|
+
import time
|
|
9
11
|
import zipfile
|
|
10
12
|
from importlib.resources import files as _pkg_files
|
|
11
13
|
|
|
@@ -44,7 +46,8 @@ def _vscode_app_bundle() -> str | None:
|
|
|
44
46
|
|
|
45
47
|
_VSCODE_EXT_INSTALLED = False # cached so we only check once per process
|
|
46
48
|
_VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
|
|
47
|
-
|
|
49
|
+
_VSCODE_EXT_RELOAD_REQUIRED = False # installed files are newer than the live host
|
|
50
|
+
_VSCODE_EXT_VERSION = "0.14.41" # current bundled extension version
|
|
48
51
|
|
|
49
52
|
def _bundled_vscode_vsix_version(vsix_path: str) -> str | None:
|
|
50
53
|
"""Return the bundled opener extension version recorded inside the VSIX."""
|
|
@@ -92,19 +95,43 @@ def _patch_vscode_extension_metadata(version: str) -> None:
|
|
|
92
95
|
)
|
|
93
96
|
|
|
94
97
|
|
|
95
|
-
def
|
|
96
|
-
"""
|
|
98
|
+
def _version_tuple(version: str) -> tuple[int, ...] | None:
|
|
99
|
+
"""Return a numeric version tuple, or ``None`` for unknown formats."""
|
|
100
|
+
try:
|
|
101
|
+
return tuple(int(part) for part in version.split("."))
|
|
102
|
+
except (AttributeError, TypeError, ValueError):
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _extension_bases(*, remote: bool | None = None) -> tuple[str, ...]:
|
|
107
|
+
"""Return extension roots for the active VS Code host."""
|
|
108
|
+
local = os.path.expanduser("~/.vscode/extensions")
|
|
109
|
+
remote_base = os.path.expanduser("~/.vscode-server/extensions")
|
|
110
|
+
if remote is True:
|
|
111
|
+
if os.path.isdir(os.path.expanduser("~/.vscode-server")):
|
|
112
|
+
return (remote_base,)
|
|
113
|
+
# `code tunnel` uses ~/.vscode/extensions on the remote host, while
|
|
114
|
+
# Remote-SSH uses ~/.vscode-server/extensions.
|
|
115
|
+
return (local,)
|
|
116
|
+
if remote is False:
|
|
117
|
+
return (local,)
|
|
118
|
+
return (local, remote_base)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _remove_old_extension_versions(
|
|
122
|
+
current_version: str, *, remote: bool | None = None
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Delete older extension directories without ever deleting a newer build.
|
|
97
125
|
|
|
98
126
|
When multiple versions of arrayview-opener are installed side-by-side,
|
|
99
127
|
VS Code may load an older version instead of the latest. Removing stale
|
|
100
128
|
directories ensures the correct version is picked up on the next reload.
|
|
129
|
+
Cleanup is scoped to the active local or remote extension host.
|
|
101
130
|
"""
|
|
102
131
|
import shutil
|
|
103
132
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
os.path.expanduser("~/.vscode/extensions"),
|
|
107
|
-
):
|
|
133
|
+
current_key = _version_tuple(current_version)
|
|
134
|
+
for ext_base in _extension_bases(remote=remote):
|
|
108
135
|
if not os.path.isdir(ext_base):
|
|
109
136
|
continue
|
|
110
137
|
try:
|
|
@@ -118,6 +145,9 @@ def _remove_old_extension_versions(current_version: str) -> None:
|
|
|
118
145
|
version_str = entry[len(prefix) :]
|
|
119
146
|
if version_str == current_version:
|
|
120
147
|
continue # keep
|
|
148
|
+
version_key = _version_tuple(version_str)
|
|
149
|
+
if current_key is None or version_key is None or version_key > current_key:
|
|
150
|
+
continue
|
|
121
151
|
old_dir = os.path.join(ext_base, entry)
|
|
122
152
|
try:
|
|
123
153
|
shutil.rmtree(old_dir)
|
|
@@ -126,7 +156,12 @@ def _remove_old_extension_versions(current_version: str) -> None:
|
|
|
126
156
|
_vprint(f"[ArrayView] could not remove {entry}: {exc}", flush=True)
|
|
127
157
|
|
|
128
158
|
|
|
129
|
-
def _extension_on_disk(
|
|
159
|
+
def _extension_on_disk(
|
|
160
|
+
version: str,
|
|
161
|
+
vsix_path: str | None = None,
|
|
162
|
+
*,
|
|
163
|
+
remote: bool | None = None,
|
|
164
|
+
) -> bool:
|
|
130
165
|
"""Return True if the extension directory for *version* exists on disk.
|
|
131
166
|
|
|
132
167
|
When *vsix_path* is given, also verifies that the installed extension
|
|
@@ -136,10 +171,7 @@ def _extension_on_disk(version: str, vsix_path: str | None = None) -> bool:
|
|
|
136
171
|
"""
|
|
137
172
|
import hashlib
|
|
138
173
|
|
|
139
|
-
for base in (
|
|
140
|
-
os.path.expanduser("~/.vscode/extensions"),
|
|
141
|
-
os.path.expanduser("~/.vscode-server/extensions"),
|
|
142
|
-
):
|
|
174
|
+
for base in _extension_bases(remote=remote):
|
|
143
175
|
ext_dir = os.path.join(base, f"arrayview.arrayview-opener-{version}")
|
|
144
176
|
if not os.path.isdir(ext_dir):
|
|
145
177
|
continue
|
|
@@ -165,6 +197,88 @@ def _extension_on_disk(version: str, vsix_path: str | None = None) -> bool:
|
|
|
165
197
|
return False
|
|
166
198
|
|
|
167
199
|
|
|
200
|
+
def _newer_extension_on_disk(version: str, *, remote: bool) -> str | None:
|
|
201
|
+
"""Return the newest installed opener newer than *version*, if any."""
|
|
202
|
+
wanted = _version_tuple(version)
|
|
203
|
+
if wanted is None:
|
|
204
|
+
return None
|
|
205
|
+
prefix = "arrayview.arrayview-opener-"
|
|
206
|
+
newer: list[tuple[tuple[int, ...], str]] = []
|
|
207
|
+
for base in _extension_bases(remote=remote):
|
|
208
|
+
try:
|
|
209
|
+
entries = os.listdir(base)
|
|
210
|
+
except OSError:
|
|
211
|
+
continue
|
|
212
|
+
for entry in entries:
|
|
213
|
+
if not entry.startswith(prefix):
|
|
214
|
+
continue
|
|
215
|
+
candidate = entry[len(prefix) :]
|
|
216
|
+
key = _version_tuple(candidate)
|
|
217
|
+
if key is not None and key > wanted:
|
|
218
|
+
newer.append((key, candidate))
|
|
219
|
+
return max(newer)[1] if newer else None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _active_extension_version() -> str | None:
|
|
223
|
+
"""Return the opener version advertised by this terminal's live host."""
|
|
224
|
+
window_id = os.environ.get("ARRAYVIEW_WINDOW_ID")
|
|
225
|
+
if not window_id:
|
|
226
|
+
ipc = _find_vscode_ipc_hook()
|
|
227
|
+
if ipc:
|
|
228
|
+
import hashlib
|
|
229
|
+
|
|
230
|
+
window_id = hashlib.sha256(ipc.encode()).hexdigest()[:16]
|
|
231
|
+
if not window_id:
|
|
232
|
+
return None
|
|
233
|
+
registration = os.path.expanduser(f"~/.arrayview/window-{window_id}.json")
|
|
234
|
+
try:
|
|
235
|
+
with open(registration) as file:
|
|
236
|
+
value = json.load(file).get("extensionVersion")
|
|
237
|
+
# An existing registration without a version belongs to a legacy host.
|
|
238
|
+
return value if isinstance(value, str) else ""
|
|
239
|
+
except (OSError, TypeError, ValueError):
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _wait_for_active_extension_version(version: str, timeout: float = 15.0) -> bool:
|
|
244
|
+
"""Wait for an updated extension host registration after installation."""
|
|
245
|
+
deadline = time.monotonic() + timeout
|
|
246
|
+
while time.monotonic() < deadline:
|
|
247
|
+
if _active_extension_version() == version:
|
|
248
|
+
return True
|
|
249
|
+
time.sleep(0.1)
|
|
250
|
+
return _active_extension_version() == version
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _run_extension_installer(command: list[str], env: dict[str, str]) -> subprocess.CompletedProcess:
|
|
254
|
+
"""Run VS Code's installer without leaving its server-cli child behind."""
|
|
255
|
+
process = subprocess.Popen(
|
|
256
|
+
command,
|
|
257
|
+
stdout=subprocess.PIPE,
|
|
258
|
+
stderr=subprocess.PIPE,
|
|
259
|
+
text=True,
|
|
260
|
+
env=env,
|
|
261
|
+
start_new_session=os.name != "nt",
|
|
262
|
+
)
|
|
263
|
+
try:
|
|
264
|
+
stdout, stderr = process.communicate(timeout=30)
|
|
265
|
+
except subprocess.TimeoutExpired:
|
|
266
|
+
if os.name == "nt":
|
|
267
|
+
process.kill()
|
|
268
|
+
else:
|
|
269
|
+
os.killpg(process.pid, signal.SIGTERM)
|
|
270
|
+
try:
|
|
271
|
+
process.communicate(timeout=2)
|
|
272
|
+
except subprocess.TimeoutExpired:
|
|
273
|
+
if os.name == "nt":
|
|
274
|
+
process.kill()
|
|
275
|
+
else:
|
|
276
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
277
|
+
process.communicate()
|
|
278
|
+
raise
|
|
279
|
+
return subprocess.CompletedProcess(command, process.returncode, stdout, stderr)
|
|
280
|
+
|
|
281
|
+
|
|
168
282
|
def _ensure_vscode_extension() -> bool:
|
|
169
283
|
"""Install the bundled arrayview-opener VS Code extension for local VS Code use.
|
|
170
284
|
|
|
@@ -180,9 +294,9 @@ def _ensure_vscode_extension() -> bool:
|
|
|
180
294
|
The authoritative version is read from the bundled VSIX — no hardcoded
|
|
181
295
|
version constant needed.
|
|
182
296
|
"""
|
|
183
|
-
global _VSCODE_EXT_INSTALLED, _VSCODE_EXT_FRESH_INSTALL
|
|
184
|
-
|
|
185
|
-
|
|
297
|
+
global _VSCODE_EXT_INSTALLED, _VSCODE_EXT_FRESH_INSTALL, _VSCODE_EXT_RELOAD_REQUIRED
|
|
298
|
+
_VSCODE_EXT_FRESH_INSTALL = False
|
|
299
|
+
_VSCODE_EXT_RELOAD_REQUIRED = False
|
|
186
300
|
|
|
187
301
|
vsix_path = str(_pkg_files("arrayview").joinpath("arrayview-opener.vsix"))
|
|
188
302
|
if not os.path.isfile(vsix_path):
|
|
@@ -195,8 +309,18 @@ def _ensure_vscode_extension() -> bool:
|
|
|
195
309
|
# Fast path: correct version and content already installed — no reinstall
|
|
196
310
|
# needed. Reinstalling with --force triggers an extension-host reload,
|
|
197
311
|
# which creates a ~10-15s gap during which the signal file can be missed.
|
|
198
|
-
|
|
199
|
-
|
|
312
|
+
is_remote = _is_vscode_remote()
|
|
313
|
+
active_version = _active_extension_version()
|
|
314
|
+
if _extension_on_disk(ext_version, vsix_path, remote=is_remote):
|
|
315
|
+
if active_version not in (None, ext_version):
|
|
316
|
+
_VSCODE_EXT_RELOAD_REQUIRED = True
|
|
317
|
+
_vprint(
|
|
318
|
+
f"[ArrayView] opener v{ext_version} is installed, but this VS Code "
|
|
319
|
+
"window is still running an older extension host; reload this window",
|
|
320
|
+
flush=True,
|
|
321
|
+
)
|
|
322
|
+
return False
|
|
323
|
+
_remove_old_extension_versions(ext_version, remote=is_remote)
|
|
200
324
|
_VSCODE_EXT_INSTALLED = True
|
|
201
325
|
_vprint(
|
|
202
326
|
f"[ArrayView] extension v{ext_version} already installed — skipping reinstall",
|
|
@@ -204,6 +328,23 @@ def _ensure_vscode_extension() -> bool:
|
|
|
204
328
|
)
|
|
205
329
|
return True
|
|
206
330
|
|
|
331
|
+
newer_version = _newer_extension_on_disk(ext_version, remote=is_remote)
|
|
332
|
+
if newer_version is not None:
|
|
333
|
+
if active_version not in (None, newer_version):
|
|
334
|
+
_VSCODE_EXT_RELOAD_REQUIRED = True
|
|
335
|
+
_vprint(
|
|
336
|
+
f"[ArrayView] newer opener v{newer_version} is installed, but this "
|
|
337
|
+
"VS Code window is still running an older extension host; reload this window",
|
|
338
|
+
flush=True,
|
|
339
|
+
)
|
|
340
|
+
return False
|
|
341
|
+
_VSCODE_EXT_INSTALLED = True
|
|
342
|
+
_vprint(
|
|
343
|
+
f"[ArrayView] newer extension v{newer_version} is installed — keeping it",
|
|
344
|
+
flush=True,
|
|
345
|
+
)
|
|
346
|
+
return True
|
|
347
|
+
|
|
207
348
|
code = _find_code_cli()
|
|
208
349
|
if not code:
|
|
209
350
|
return False
|
|
@@ -214,12 +355,8 @@ def _ensure_vscode_extension() -> bool:
|
|
|
214
355
|
env["VSCODE_IPC_HOOK_CLI"] = ipc
|
|
215
356
|
|
|
216
357
|
try:
|
|
217
|
-
r =
|
|
218
|
-
[code, "--install-extension", vsix_path, "--force"],
|
|
219
|
-
capture_output=True,
|
|
220
|
-
text=True,
|
|
221
|
-
timeout=30,
|
|
222
|
-
env=env,
|
|
358
|
+
r = _run_extension_installer(
|
|
359
|
+
[code, "--install-extension", vsix_path, "--force"], env
|
|
223
360
|
)
|
|
224
361
|
combined = (r.stdout or "") + (r.stderr or "")
|
|
225
362
|
install_failed = (
|
|
@@ -247,9 +384,23 @@ def _ensure_vscode_extension() -> bool:
|
|
|
247
384
|
pass # non-critical
|
|
248
385
|
_VSCODE_EXT_INSTALLED = True
|
|
249
386
|
_VSCODE_EXT_FRESH_INSTALL = True
|
|
387
|
+
_remove_old_extension_versions(ext_version, remote=is_remote)
|
|
388
|
+
if active_version not in (None, ext_version):
|
|
389
|
+
if not _wait_for_active_extension_version(ext_version):
|
|
390
|
+
_VSCODE_EXT_RELOAD_REQUIRED = True
|
|
391
|
+
_vprint(
|
|
392
|
+
"[ArrayView] VS Code installed the new opener, but this window "
|
|
393
|
+
"is still running the old extension host; reload this VS Code window",
|
|
394
|
+
flush=True,
|
|
395
|
+
)
|
|
396
|
+
return False
|
|
250
397
|
return True
|
|
398
|
+
if active_version not in (None, ext_version):
|
|
399
|
+
_VSCODE_EXT_RELOAD_REQUIRED = True
|
|
251
400
|
print(f"[ArrayView] extension install failed: {combined.strip()!r}", flush=True)
|
|
252
401
|
except Exception as exc:
|
|
402
|
+
if active_version not in (None, ext_version):
|
|
403
|
+
_VSCODE_EXT_RELOAD_REQUIRED = True
|
|
253
404
|
print(f"[ArrayView] extension install error: {exc}", flush=True)
|
|
254
405
|
return False
|
|
255
406
|
|