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.
Files changed (232) hide show
  1. {arrayview-0.31.2 → arrayview-0.31.3}/.github/workflows/launch-contracts.yml +4 -4
  2. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/lifecycle.md +7 -0
  3. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/project-state.md +6 -5
  4. {arrayview-0.31.2 → arrayview-0.31.3}/PKG-INFO +1 -1
  5. {arrayview-0.31.2 → arrayview-0.31.3}/docs/index.md +2 -0
  6. {arrayview-0.31.2 → arrayview-0.31.3}/docs/loading.md +4 -45
  7. arrayview-0.31.3/docs/stack-overlays.md +129 -0
  8. {arrayview-0.31.2 → arrayview-0.31.3}/mkdocs.yml +1 -0
  9. {arrayview-0.31.2 → arrayview-0.31.3}/pyproject.toml +1 -1
  10. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_loading.py +2 -1
  11. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_session.py +1 -0
  12. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_vscode_browser.py +15 -6
  13. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_vscode_extension.py +174 -23
  14. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_vscode_signal.py +28 -0
  15. arrayview-0.31.3/src/arrayview/arrayview-opener.vsix +0 -0
  16. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_api.py +20 -0
  17. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_cli.py +2 -0
  18. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_display_opener_results.py +20 -0
  19. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_lifecycle_contract.py +119 -4
  20. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_vscode_ack_protocol.py +43 -0
  21. {arrayview-0.31.2 → arrayview-0.31.3}/uv.lock +1 -1
  22. {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/extension.js +98 -26
  23. {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/lifecycle_helpers.js +27 -1
  24. {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/package.json +1 -1
  25. {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/test_lifecycle_helpers.js +18 -1
  26. arrayview-0.31.2/src/arrayview/arrayview-opener.vsix +0 -0
  27. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/frontend-designer/SKILL.md +0 -0
  28. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/invocation-consistency/SKILL.md +0 -0
  29. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/modes-consistency/SKILL.md +0 -0
  30. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/SKILL.md +0 -0
  31. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/element-attributes.md +0 -0
  32. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/playwright-tests.md +0 -0
  33. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/request-mocking.md +0 -0
  34. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/running-code.md +0 -0
  35. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/session-management.md +0 -0
  36. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/storage-state.md +0 -0
  37. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/test-generation.md +0 -0
  38. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/tracing.md +0 -0
  39. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/playwright-cli/references/video-recording.md +0 -0
  40. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/todo-workflow/SKILL.md +0 -0
  41. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/ui-consistency-audit/SKILL.md +0 -0
  42. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/viewer-ui-checklist/SKILL.md +0 -0
  43. {arrayview-0.31.2 → arrayview-0.31.3}/.agents/skills/visual-bug-fixing/SKILL.md +0 -0
  44. {arrayview-0.31.2 → arrayview-0.31.3}/.github/copilot-instructions.md +0 -0
  45. {arrayview-0.31.2 → arrayview-0.31.3}/.github/workflows/docs.yml +0 -0
  46. {arrayview-0.31.2 → arrayview-0.31.3}/.github/workflows/python-publish.yml +0 -0
  47. {arrayview-0.31.2 → arrayview-0.31.3}/.gitignore +0 -0
  48. {arrayview-0.31.2 → arrayview-0.31.3}/.ignore +0 -0
  49. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/AGENTS.md +0 -0
  50. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/ROUTER.md +0 -0
  51. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/SETUP.md +0 -0
  52. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/SYNC.md +0 -0
  53. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/architecture.md +0 -0
  54. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/conventions.md +0 -0
  55. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/decisions.md +0 -0
  56. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/frontend.md +0 -0
  57. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/render-pipeline.md +0 -0
  58. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/setup.md +0 -0
  59. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/context/stack.md +0 -0
  60. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/INDEX.md +0 -0
  61. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/README.md +0 -0
  62. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/add-file-format.md +0 -0
  63. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/add-server-endpoint.md +0 -0
  64. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/animation-verify.md +0 -0
  65. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/debug-render.md +0 -0
  66. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/debug-vscode-extension-python.md +0 -0
  67. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/extend-compare-mode.md +0 -0
  68. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/extract-server-route-module.md +0 -0
  69. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/patterns/frontend-change.md +0 -0
  70. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/setup.sh +0 -0
  71. {arrayview-0.31.2 → arrayview-0.31.3}/.mex/sync.sh +0 -0
  72. {arrayview-0.31.2 → arrayview-0.31.3}/.opencode/opencode.json +0 -0
  73. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T18-46-49-737Z.yml +0 -0
  74. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T18-48-21-979Z.yml +0 -0
  75. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T18-51-35-665Z.yml +0 -0
  76. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-07-01-393Z.yml +0 -0
  77. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-14-37-969Z.yml +0 -0
  78. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-21-30-940Z.yml +0 -0
  79. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-23-08-126Z.yml +0 -0
  80. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-29-33-155Z.yml +0 -0
  81. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-31-25-336Z.yml +0 -0
  82. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-31-53-789Z.yml +0 -0
  83. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-39-12-257Z.yml +0 -0
  84. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T19-39-16-449Z.yml +0 -0
  85. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T20-15-25-513Z.yml +0 -0
  86. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T20-25-13-179Z.yml +0 -0
  87. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T20-39-01-435Z.yml +0 -0
  88. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-01-27-659Z.yml +0 -0
  89. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-01-41-283Z.yml +0 -0
  90. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-03-00-625Z.yml +0 -0
  91. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-04-12-887Z.yml +0 -0
  92. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-33-39-044Z.yml +0 -0
  93. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-38-01-530Z.yml +0 -0
  94. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-45-20-383Z.yml +0 -0
  95. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-55-11-545Z.yml +0 -0
  96. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-56-03-307Z.yml +0 -0
  97. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-56-35-733Z.yml +0 -0
  98. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-57-12-181Z.yml +0 -0
  99. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-57-37-748Z.yml +0 -0
  100. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T21-58-13-679Z.yml +0 -0
  101. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-06T22-37-23-895Z.yml +0 -0
  102. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T00-39-18-637Z.yml +0 -0
  103. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T01-41-46-243Z.yml +0 -0
  104. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T04-31-48-472Z.yml +0 -0
  105. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-14-15-632Z.yml +0 -0
  106. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-14-47-582Z.yml +0 -0
  107. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-16-23-471Z.yml +0 -0
  108. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-17-10-247Z.yml +0 -0
  109. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-18-24-707Z.yml +0 -0
  110. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-20-06-164Z.yml +0 -0
  111. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-20-28-342Z.yml +0 -0
  112. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-21-54-962Z.yml +0 -0
  113. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-22-34-666Z.yml +0 -0
  114. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-23-11-336Z.yml +0 -0
  115. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-23-36-260Z.yml +0 -0
  116. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-24-09-267Z.yml +0 -0
  117. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-24-35-434Z.yml +0 -0
  118. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-25-57-010Z.yml +0 -0
  119. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-34-48-823Z.yml +0 -0
  120. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-46-46-468Z.yml +0 -0
  121. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-48-17-930Z.yml +0 -0
  122. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-49-26-400Z.yml +0 -0
  123. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-50-31-563Z.yml +0 -0
  124. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/page-2026-05-07T12-56-45-568Z.yml +0 -0
  125. {arrayview-0.31.2 → arrayview-0.31.3}/.playwright-cli/theme-dark.yml +0 -0
  126. {arrayview-0.31.2 → arrayview-0.31.3}/.python-version +0 -0
  127. {arrayview-0.31.2 → arrayview-0.31.3}/.vscode/settings.json +0 -0
  128. {arrayview-0.31.2 → arrayview-0.31.3}/AGENTS.md +0 -0
  129. {arrayview-0.31.2 → arrayview-0.31.3}/CONTRIBUTING.md +0 -0
  130. {arrayview-0.31.2 → arrayview-0.31.3}/DESIGN.md +0 -0
  131. {arrayview-0.31.2 → arrayview-0.31.3}/LICENSE +0 -0
  132. {arrayview-0.31.2 → arrayview-0.31.3}/README.md +0 -0
  133. {arrayview-0.31.2 → arrayview-0.31.3}/docs/comparing.md +0 -0
  134. {arrayview-0.31.2 → arrayview-0.31.3}/docs/configuration.md +0 -0
  135. {arrayview-0.31.2 → arrayview-0.31.3}/docs/display.md +0 -0
  136. {arrayview-0.31.2 → arrayview-0.31.3}/docs/foreign-hosts.md +0 -0
  137. {arrayview-0.31.2 → arrayview-0.31.3}/docs/launch-routing-refactor.md +0 -0
  138. {arrayview-0.31.2 → arrayview-0.31.3}/docs/lifecycle.md +0 -0
  139. {arrayview-0.31.2 → arrayview-0.31.3}/docs/logo.png +0 -0
  140. {arrayview-0.31.2 → arrayview-0.31.3}/docs/measurement.md +0 -0
  141. {arrayview-0.31.2 → arrayview-0.31.3}/docs/remote.md +0 -0
  142. {arrayview-0.31.2 → arrayview-0.31.3}/docs/stylesheets/extra.css +0 -0
  143. {arrayview-0.31.2 → arrayview-0.31.3}/docs/viewing.md +0 -0
  144. {arrayview-0.31.2 → arrayview-0.31.3}/docs/vscode-window-routing-plan.md +0 -0
  145. {arrayview-0.31.2 → arrayview-0.31.3}/experiments/ws_tunnel_diag/ws_tunnel_diag.py +0 -0
  146. {arrayview-0.31.2 → arrayview-0.31.3}/matlab/arrayview.m +0 -0
  147. {arrayview-0.31.2 → arrayview-0.31.3}/plans/2026-04-14-immersive-animation.md +0 -0
  148. {arrayview-0.31.2 → arrayview-0.31.3}/plans/2026-05-07-unified-colormap-picker.md +0 -0
  149. {arrayview-0.31.2 → arrayview-0.31.3}/plans/arrayview_tool_menu_fix_prompt.md +0 -0
  150. {arrayview-0.31.2 → arrayview-0.31.3}/plans/arrayview_tool_menu_implementation_plan.md +0 -0
  151. {arrayview-0.31.2 → arrayview-0.31.3}/plans/egg-placement-mockup.html +0 -0
  152. {arrayview-0.31.2 → arrayview-0.31.3}/plans/refactoring_plan.md +0 -0
  153. {arrayview-0.31.2 → arrayview-0.31.3}/plans/roi-analysis-requirements.md +0 -0
  154. {arrayview-0.31.2 → arrayview-0.31.3}/plans/webview/LOG.md +0 -0
  155. {arrayview-0.31.2 → arrayview-0.31.3}/scripts/demo.py +0 -0
  156. {arrayview-0.31.2 → arrayview-0.31.3}/scripts/release.sh +0 -0
  157. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/ARCHITECTURE.md +0 -0
  158. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/__init__.py +0 -0
  159. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/__main__.py +0 -0
  160. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_analysis.py +0 -0
  161. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_app.py +0 -0
  162. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_codex_open.py +0 -0
  163. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_config.py +0 -0
  164. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_diff.py +0 -0
  165. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_icon.png +0 -0
  166. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_imaging.py +0 -0
  167. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_instance_registry.py +0 -0
  168. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_io.py +0 -0
  169. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_launch_plan.py +0 -0
  170. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_launcher.py +0 -0
  171. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_lifecycle.py +0 -0
  172. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_overlays.py +0 -0
  173. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_platform.py +0 -0
  174. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_render.py +0 -0
  175. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_analysis.py +0 -0
  176. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_export.py +0 -0
  177. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_persistence.py +0 -0
  178. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_preload.py +0 -0
  179. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_query.py +0 -0
  180. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_rendering.py +0 -0
  181. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_segmentation.py +0 -0
  182. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_state.py +0 -0
  183. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_vectorfield.py +0 -0
  184. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_routes_websocket.py +0 -0
  185. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_segmentation.py +0 -0
  186. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_server.py +0 -0
  187. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_session_spec.py +0 -0
  188. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_shell.html +0 -0
  189. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_synthetic_mri.py +0 -0
  190. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_torch.py +0 -0
  191. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_vectorfield.py +0 -0
  192. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_viewer.html +0 -0
  193. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/_vscode.py +0 -0
  194. {arrayview-0.31.2 → arrayview-0.31.3}/src/arrayview/gsap.min.js +0 -0
  195. {arrayview-0.31.2 → arrayview-0.31.3}/tests/capture_v_animation.py +0 -0
  196. {arrayview-0.31.2 → arrayview-0.31.3}/tests/conftest.py +0 -0
  197. {arrayview-0.31.2 → arrayview-0.31.3}/tests/lifecycle_matrix.py +0 -0
  198. {arrayview-0.31.2 → arrayview-0.31.3}/tests/make_vectorfield_test_arrays.py +0 -0
  199. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_backend_shared.py +0 -0
  200. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_browser.py +0 -0
  201. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_colorbar_hover_highlight.py +0 -0
  202. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_command_reachability.py +0 -0
  203. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_config.py +0 -0
  204. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_cross_mode_parametrized.py +0 -0
  205. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_doctor_commands.py +0 -0
  206. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_instance_registry.py +0 -0
  207. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_interactions.py +0 -0
  208. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_invocation_contract_matrix.py +0 -0
  209. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_io.py +0 -0
  210. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_large_arrays.py +0 -0
  211. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_launch_diagnostics.py +0 -0
  212. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_launch_plan.py +0 -0
  213. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_loading_server.py +0 -0
  214. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_mode_consistency.py +0 -0
  215. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_mode_matrix.py +0 -0
  216. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_mode_roundtrip.py +0 -0
  217. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_nifti_meta.py +0 -0
  218. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_nifti_series.py +0 -0
  219. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_rgb_pixel_art.py +0 -0
  220. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_safe_server_stop.py +0 -0
  221. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_server_identity_status.py +0 -0
  222. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_session_spec.py +0 -0
  223. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_torch.py +0 -0
  224. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_view_component_integration.py +0 -0
  225. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_view_component_unit.py +0 -0
  226. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_view_handle_lifecycle.py +0 -0
  227. {arrayview-0.31.2 → arrayview-0.31.3}/tests/test_view_launch_execution.py +0 -0
  228. {arrayview-0.31.2 → arrayview-0.31.3}/tests/ui_audit.py +0 -0
  229. {arrayview-0.31.2 → arrayview-0.31.3}/tests/v_anim_frames/.gitkeep +0 -0
  230. {arrayview-0.31.2 → arrayview-0.31.3}/tests/visual_smoke.py +0 -0
  231. {arrayview-0.31.2 → arrayview-0.31.3}/vscode-extension/AGENTS.md +0 -0
  232. {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@v4
34
- - uses: actions/setup-python@v5
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@v6
38
- - uses: actions/setup-node@v4
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-06-30
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.27 — stable window ID via `EnvironmentVariableCollection`; `arrayview.openInFloatingWindow` setting moves new tabs to a floating window; `view(arr, floating=True)` and `arrayview file.npy --floating` open in a floating window per-call regardless of global setting; remote/tunnel display uses the same forwarded HTTP/WebSocket path as local VS Code; floating mode uses a single persistent shell hub panel (`_shell.html`) so all arrays share one floating window as tabs instead of opening separate windows; second CLI calls inject tabs via `new_tab` postMessage relay (extension -> hub wrapper -> shell iframe)
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 their
52
- overlay sessions, and VS Code opener readiness now verifies the requested
53
- session metadata before acknowledging `backend_ready`.
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.31.2
3
+ Version: 0.31.3
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -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=mask.nii.gz"
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. Passing a directory scans supported
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
- Positional patterns become a channel/modality axis:
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
+ ```
@@ -24,6 +24,7 @@ extra_css:
24
24
  nav:
25
25
  - Home: index.md
26
26
  - Loading Arrays: loading.md
27
+ - Stack and Overlays: stack-overlays.md
27
28
  - Viewing: viewing.md
28
29
  - Comparing: comparing.md
29
30
  - Display: display.md
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.31.2"
7
+ version = "0.31.3"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -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
- return {"sid": existing.sid, "name": existing.name, "notified": False}
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, _VSCODE_EXT_FRESH_INSTALL
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
- if ext_ok and _VSCODE_EXT_FRESH_INSTALL:
137
- time.sleep(1.5)
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
- if ext_ok and _VSCODE_EXT_FRESH_INSTALL:
176
- time.sleep(1.5)
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
- _VSCODE_EXT_VERSION = "0.14.40" # current bundled extension version
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 _remove_old_extension_versions(current_version: str) -> None:
96
- """Delete extension directories for versions older than current_version.
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
- for ext_base in (
105
- os.path.expanduser("~/.vscode-server/extensions"),
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(version: str, vsix_path: str | None = None) -> bool:
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
- if _VSCODE_EXT_INSTALLED:
185
- return True
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
- _remove_old_extension_versions(ext_version)
199
- if _extension_on_disk(ext_version, vsix_path):
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 = subprocess.run(
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