arrayview 0.23.0__tar.gz → 0.24.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/architecture.md +13 -1
  2. {arrayview-0.23.0 → arrayview-0.24.0}/PKG-INFO +1 -1
  3. {arrayview-0.23.0 → arrayview-0.24.0}/pyproject.toml +1 -1
  4. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_launcher.py +74 -52
  5. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_platform.py +40 -42
  6. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_rendering.py +33 -6
  7. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_websocket.py +23 -0
  8. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_shell.html +20 -20
  9. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_stdio_server.py +81 -28
  10. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_viewer.html +1062 -525
  11. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vscode_extension.py +1 -1
  12. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vscode_signal.py +3 -45
  13. arrayview-0.24.0/src/arrayview/arrayview-opener.vsix +0 -0
  14. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_api.py +125 -0
  15. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_browser.py +94 -12
  16. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_loading_server.py +1 -1
  17. {arrayview-0.23.0 → arrayview-0.24.0}/uv.lock +1 -1
  18. {arrayview-0.23.0 → arrayview-0.24.0}/vscode-extension/extension.js +73 -5
  19. {arrayview-0.23.0 → arrayview-0.24.0}/vscode-extension/package.json +1 -1
  20. arrayview-0.23.0/src/arrayview/arrayview-opener.vsix +0 -0
  21. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/frontend-designer/SKILL.md +0 -0
  22. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/invocation-consistency/SKILL.md +0 -0
  23. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/modes-consistency/SKILL.md +0 -0
  24. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/SKILL.md +0 -0
  25. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/element-attributes.md +0 -0
  26. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/playwright-tests.md +0 -0
  27. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/request-mocking.md +0 -0
  28. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/running-code.md +0 -0
  29. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/session-management.md +0 -0
  30. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/storage-state.md +0 -0
  31. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/test-generation.md +0 -0
  32. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/tracing.md +0 -0
  33. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/playwright-cli/references/video-recording.md +0 -0
  34. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/todo-workflow/SKILL.md +0 -0
  35. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/ui-consistency-audit/SKILL.md +0 -0
  36. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/viewer-ui-checklist/SKILL.md +0 -0
  37. {arrayview-0.23.0 → arrayview-0.24.0}/.agents/skills/visual-bug-fixing/SKILL.md +0 -0
  38. {arrayview-0.23.0 → arrayview-0.24.0}/.github/copilot-instructions.md +0 -0
  39. {arrayview-0.23.0 → arrayview-0.24.0}/.github/workflows/docs.yml +0 -0
  40. {arrayview-0.23.0 → arrayview-0.24.0}/.github/workflows/python-publish.yml +0 -0
  41. {arrayview-0.23.0 → arrayview-0.24.0}/.gitignore +0 -0
  42. {arrayview-0.23.0 → arrayview-0.24.0}/.ignore +0 -0
  43. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/AGENTS.md +0 -0
  44. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/ROUTER.md +0 -0
  45. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/SETUP.md +0 -0
  46. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/SYNC.md +0 -0
  47. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/conventions.md +0 -0
  48. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/decisions.md +0 -0
  49. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/frontend.md +0 -0
  50. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/project-state.md +0 -0
  51. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/render-pipeline.md +0 -0
  52. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/setup.md +0 -0
  53. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/context/stack.md +0 -0
  54. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/INDEX.md +0 -0
  55. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/README.md +0 -0
  56. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/add-file-format.md +0 -0
  57. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/add-server-endpoint.md +0 -0
  58. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/animation-verify.md +0 -0
  59. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/debug-render.md +0 -0
  60. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/debug-vscode-extension-python.md +0 -0
  61. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/extend-compare-mode.md +0 -0
  62. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/extract-server-route-module.md +0 -0
  63. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/patterns/frontend-change.md +0 -0
  64. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/setup.sh +0 -0
  65. {arrayview-0.23.0 → arrayview-0.24.0}/.mex/sync.sh +0 -0
  66. {arrayview-0.23.0 → arrayview-0.24.0}/.opencode/opencode.json +0 -0
  67. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T18-46-49-737Z.yml +0 -0
  68. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T18-48-21-979Z.yml +0 -0
  69. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T18-51-35-665Z.yml +0 -0
  70. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-07-01-393Z.yml +0 -0
  71. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-14-37-969Z.yml +0 -0
  72. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-21-30-940Z.yml +0 -0
  73. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-23-08-126Z.yml +0 -0
  74. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-29-33-155Z.yml +0 -0
  75. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-31-25-336Z.yml +0 -0
  76. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-31-53-789Z.yml +0 -0
  77. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-39-12-257Z.yml +0 -0
  78. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T19-39-16-449Z.yml +0 -0
  79. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T20-15-25-513Z.yml +0 -0
  80. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T20-25-13-179Z.yml +0 -0
  81. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T20-39-01-435Z.yml +0 -0
  82. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-01-27-659Z.yml +0 -0
  83. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-01-41-283Z.yml +0 -0
  84. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-03-00-625Z.yml +0 -0
  85. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-04-12-887Z.yml +0 -0
  86. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-33-39-044Z.yml +0 -0
  87. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-38-01-530Z.yml +0 -0
  88. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-45-20-383Z.yml +0 -0
  89. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-55-11-545Z.yml +0 -0
  90. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-56-03-307Z.yml +0 -0
  91. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-56-35-733Z.yml +0 -0
  92. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-57-12-181Z.yml +0 -0
  93. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-57-37-748Z.yml +0 -0
  94. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T21-58-13-679Z.yml +0 -0
  95. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-06T22-37-23-895Z.yml +0 -0
  96. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T00-39-18-637Z.yml +0 -0
  97. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T01-41-46-243Z.yml +0 -0
  98. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T04-31-48-472Z.yml +0 -0
  99. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-14-15-632Z.yml +0 -0
  100. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-14-47-582Z.yml +0 -0
  101. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-16-23-471Z.yml +0 -0
  102. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-17-10-247Z.yml +0 -0
  103. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-18-24-707Z.yml +0 -0
  104. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-20-06-164Z.yml +0 -0
  105. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-20-28-342Z.yml +0 -0
  106. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-21-54-962Z.yml +0 -0
  107. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-22-34-666Z.yml +0 -0
  108. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-23-11-336Z.yml +0 -0
  109. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-23-36-260Z.yml +0 -0
  110. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-24-09-267Z.yml +0 -0
  111. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-24-35-434Z.yml +0 -0
  112. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-25-57-010Z.yml +0 -0
  113. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-34-48-823Z.yml +0 -0
  114. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-46-46-468Z.yml +0 -0
  115. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-48-17-930Z.yml +0 -0
  116. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-49-26-400Z.yml +0 -0
  117. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-50-31-563Z.yml +0 -0
  118. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/page-2026-05-07T12-56-45-568Z.yml +0 -0
  119. {arrayview-0.23.0 → arrayview-0.24.0}/.playwright-cli/theme-dark.yml +0 -0
  120. {arrayview-0.23.0 → arrayview-0.24.0}/.python-version +0 -0
  121. {arrayview-0.23.0 → arrayview-0.24.0}/.vscode/settings.json +0 -0
  122. {arrayview-0.23.0 → arrayview-0.24.0}/AGENTS.md +0 -0
  123. {arrayview-0.23.0 → arrayview-0.24.0}/CONTRIBUTING.md +0 -0
  124. {arrayview-0.23.0 → arrayview-0.24.0}/DESIGN.md +0 -0
  125. {arrayview-0.23.0 → arrayview-0.24.0}/LICENSE +0 -0
  126. {arrayview-0.23.0 → arrayview-0.24.0}/README.md +0 -0
  127. {arrayview-0.23.0 → arrayview-0.24.0}/docs/comparing.md +0 -0
  128. {arrayview-0.23.0 → arrayview-0.24.0}/docs/configuration.md +0 -0
  129. {arrayview-0.23.0 → arrayview-0.24.0}/docs/display.md +0 -0
  130. {arrayview-0.23.0 → arrayview-0.24.0}/docs/index.md +0 -0
  131. {arrayview-0.23.0 → arrayview-0.24.0}/docs/loading.md +0 -0
  132. {arrayview-0.23.0 → arrayview-0.24.0}/docs/logo.png +0 -0
  133. {arrayview-0.23.0 → arrayview-0.24.0}/docs/measurement.md +0 -0
  134. {arrayview-0.23.0 → arrayview-0.24.0}/docs/remote.md +0 -0
  135. {arrayview-0.23.0 → arrayview-0.24.0}/docs/stylesheets/extra.css +0 -0
  136. {arrayview-0.23.0 → arrayview-0.24.0}/docs/viewing.md +0 -0
  137. {arrayview-0.23.0 → arrayview-0.24.0}/matlab/arrayview.m +0 -0
  138. {arrayview-0.23.0 → arrayview-0.24.0}/mkdocs.yml +0 -0
  139. {arrayview-0.23.0 → arrayview-0.24.0}/plans/2026-04-14-immersive-animation.md +0 -0
  140. {arrayview-0.23.0 → arrayview-0.24.0}/plans/2026-05-07-unified-colormap-picker.md +0 -0
  141. {arrayview-0.23.0 → arrayview-0.24.0}/plans/arrayview_tool_menu_fix_prompt.md +0 -0
  142. {arrayview-0.23.0 → arrayview-0.24.0}/plans/arrayview_tool_menu_implementation_plan.md +0 -0
  143. {arrayview-0.23.0 → arrayview-0.24.0}/plans/egg-placement-mockup.html +0 -0
  144. {arrayview-0.23.0 → arrayview-0.24.0}/plans/refactoring_plan.md +0 -0
  145. {arrayview-0.23.0 → arrayview-0.24.0}/plans/webview/LOG.md +0 -0
  146. {arrayview-0.23.0 → arrayview-0.24.0}/scripts/demo.py +0 -0
  147. {arrayview-0.23.0 → arrayview-0.24.0}/scripts/release.sh +0 -0
  148. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/ARCHITECTURE.md +0 -0
  149. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/__init__.py +0 -0
  150. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/__main__.py +0 -0
  151. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_analysis.py +0 -0
  152. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_app.py +0 -0
  153. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_codex_open.py +0 -0
  154. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_config.py +0 -0
  155. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_diff.py +0 -0
  156. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_icon.png +0 -0
  157. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_imaging.py +0 -0
  158. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_io.py +0 -0
  159. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_overlays.py +0 -0
  160. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_render.py +0 -0
  161. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_analysis.py +0 -0
  162. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_export.py +0 -0
  163. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_loading.py +0 -0
  164. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_persistence.py +0 -0
  165. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_preload.py +0 -0
  166. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_query.py +0 -0
  167. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_segmentation.py +0 -0
  168. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_state.py +0 -0
  169. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_routes_vectorfield.py +0 -0
  170. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_segmentation.py +0 -0
  171. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_server.py +0 -0
  172. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_session.py +0 -0
  173. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_synthetic_mri.py +0 -0
  174. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_torch.py +0 -0
  175. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vectorfield.py +0 -0
  176. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vscode.py +0 -0
  177. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vscode_browser.py +0 -0
  178. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/_vscode_shm.py +0 -0
  179. {arrayview-0.23.0 → arrayview-0.24.0}/src/arrayview/gsap.min.js +0 -0
  180. {arrayview-0.23.0 → arrayview-0.24.0}/tests/capture_v_animation.py +0 -0
  181. {arrayview-0.23.0 → arrayview-0.24.0}/tests/conftest.py +0 -0
  182. {arrayview-0.23.0 → arrayview-0.24.0}/tests/make_vectorfield_test_arrays.py +0 -0
  183. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_backend_shared.py +0 -0
  184. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_cli.py +0 -0
  185. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_colorbar_hover_highlight.py +0 -0
  186. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_command_reachability.py +0 -0
  187. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_config.py +0 -0
  188. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_cross_mode_parametrized.py +0 -0
  189. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_interactions.py +0 -0
  190. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_large_arrays.py +0 -0
  191. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_mode_consistency.py +0 -0
  192. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_mode_entry_batching.py +0 -0
  193. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_mode_matrix.py +0 -0
  194. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_mode_roundtrip.py +0 -0
  195. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_nifti_meta.py +0 -0
  196. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_rgb_pixel_art.py +0 -0
  197. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_stdio_server.py +0 -0
  198. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_torch.py +0 -0
  199. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_view_component_integration.py +0 -0
  200. {arrayview-0.23.0 → arrayview-0.24.0}/tests/test_view_component_unit.py +0 -0
  201. {arrayview-0.23.0 → arrayview-0.24.0}/tests/ui_audit.py +0 -0
  202. {arrayview-0.23.0 → arrayview-0.24.0}/tests/v_anim_frames/.gitkeep +0 -0
  203. {arrayview-0.23.0 → arrayview-0.24.0}/tests/visual_smoke.py +0 -0
  204. {arrayview-0.23.0 → arrayview-0.24.0}/vscode-extension/AGENTS.md +0 -0
  205. {arrayview-0.23.0 → arrayview-0.24.0}/vscode-extension/LICENSE +0 -0
@@ -18,7 +18,7 @@ edges:
18
18
  condition: when the task involves _viewer.html, modes, reconcilers, or the View Component System
19
19
  - target: context/render-pipeline.md
20
20
  condition: when the task involves slice extraction, colormaps, caching, or the render thread
21
- last_updated: 2026-05-05
21
+ last_updated: 2026-05-26
22
22
  ---
23
23
 
24
24
  # Architecture
@@ -63,6 +63,18 @@ pywebview, or system browser).
63
63
  - **`_stdio_server.py`** — Alternative to FastAPI for VS Code tunnel (direct webview): JSON on stdin, length-prefixed binary on stdout.
64
64
  - **`_viewer.html`** — The entire frontend (~24 100 lines). CSS + JS in one file, no build step. Canvas-based rendering, WebSocket binary protocol, all viewing modes, reconcilers, command registry. See `context/frontend.md`.
65
65
 
66
+ ## Frontend Tool Lifecycle
67
+
68
+ Tool launch, tool activation, and drawer visibility are separate states.
69
+
70
+ - Selecting an actionable tool activates it immediately and opens the right drawer.
71
+ - Closing the right drawer hides controls only; it does not deactivate the active tool.
72
+ - Re-selecting an active tool reopens its drawer when hidden, or hides the drawer when already visible.
73
+ - `Esc` targets visible UI first: drawer, launcher, picker, or help before exiting active tools.
74
+ - Passive tools such as Save open a drawer without entering a mode.
75
+ - Overlay and vector tools show their rendered layer immediately when selected and available.
76
+ - Existing tool output persists after mode exit unless the tool has an explicit destructive action.
77
+
66
78
  ## Display Routing
67
79
 
68
80
  | Environment | Default display | Server mode |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.23.0
3
+ Version: 0.24.0
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.23.0"
7
+ version = "0.24.0"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -156,6 +156,7 @@ def _vprint(*args, **kwargs) -> None:
156
156
  # ── Subprocess GUI Launcher ───────────────────────────────────────
157
157
 
158
158
  _ICON_PNG_PATH: str | None = None
159
+ _LOOPBACK_HOST = "localhost"
159
160
 
160
161
 
161
162
  def _get_icon_png_path() -> str | None:
@@ -177,6 +178,57 @@ def _get_icon_png_path() -> str | None:
177
178
  return _ICON_PNG_PATH or None
178
179
 
179
180
 
181
+ def _build_inline_shell_html(url: str, shell_port: int) -> str | None:
182
+ """Return embedded shell HTML for a cold-start native window."""
183
+ try:
184
+ shell_html = _pkg_files("arrayview").joinpath("_shell.html").read_text(
185
+ encoding="utf-8"
186
+ )
187
+ parsed = urllib.parse.urlparse(url)
188
+ inline_query = parsed.query
189
+ shell_html = shell_html.replace(
190
+ "</head>",
191
+ f"<script>"
192
+ f"window.__av_inline=true;"
193
+ f"window.__av_inlineQuery={inline_query!r};"
194
+ f"</script>\n"
195
+ f'<base href="http://{_LOOPBACK_HOST}:{shell_port}/">\n'
196
+ f"</head>",
197
+ 1,
198
+ )
199
+ # Fix WebSocket URL — location.host is "" in inline html= mode
200
+ shell_html = shell_html.replace(
201
+ "`${proto}//${location.host}/ws/shell`",
202
+ f"`ws://{_LOOPBACK_HOST}:{shell_port}/ws/shell`",
203
+ )
204
+ return shell_html
205
+ except Exception:
206
+ return None
207
+
208
+
209
+ def _make_loopback_socket(port: int) -> "socket.socket":
210
+ """Bind a TCP listener on the same loopback host the viewer URLs use."""
211
+ for family, socktype, proto, _, sockaddr in socket.getaddrinfo(
212
+ _LOOPBACK_HOST,
213
+ port,
214
+ type=socket.SOCK_STREAM,
215
+ ):
216
+ sock = None
217
+ try:
218
+ sock = socket.socket(family, socktype, proto)
219
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
220
+ sock.bind(sockaddr)
221
+ sock.listen(128)
222
+ sock.set_inheritable(True)
223
+ return sock
224
+ except OSError:
225
+ try:
226
+ sock.close()
227
+ except Exception:
228
+ pass
229
+ raise OSError(f"Could not bind {_LOOPBACK_HOST}:{port}")
230
+
231
+
180
232
  def _open_webview(
181
233
  url: str,
182
234
  win_w: int,
@@ -199,25 +251,9 @@ def _open_webview(
199
251
  inline_html_b64 = None
200
252
 
201
253
  if shell_port is not None:
202
- try:
203
- from importlib.resources import files as _pkg_files
204
- shell_html = _pkg_files("arrayview").joinpath("_shell.html").read_text(encoding="utf-8")
205
- # Inject inline-mode flag and hardcoded host (location.host is empty in html= mode)
206
- shell_html = shell_html.replace(
207
- "</head>",
208
- f'<script>window.__av_inline=true;</script>\n'
209
- f'<base href="http://localhost:{shell_port}/">\n'
210
- f"</head>",
211
- 1,
212
- )
213
- # Fix WebSocket URL — location.host is "" in inline html= mode
214
- shell_html = shell_html.replace(
215
- "`${proto}//${location.host}/ws/shell`",
216
- f"`ws://localhost:{shell_port}/ws/shell`",
217
- )
254
+ shell_html = _build_inline_shell_html(url, shell_port)
255
+ if shell_html is not None:
218
256
  inline_html_b64 = _b64.b64encode(shell_html.encode()).decode()
219
- except Exception:
220
- pass # fall back to URL mode
221
257
 
222
258
  if inline_html_b64:
223
259
  script_lines = [
@@ -469,7 +505,7 @@ def _open_webview_cli(
469
505
 
470
506
  def _server_alive(port: int, timeout: float = 0.5) -> bool:
471
507
  """Return True only if an ArrayView server is responding on the port."""
472
- url = f"http://127.0.0.1:{port}/ping"
508
+ url = f"http://{_LOOPBACK_HOST}:{port}/ping"
473
509
  try:
474
510
  with urllib.request.urlopen(url, timeout=timeout) as resp:
475
511
  if resp.status != 200:
@@ -482,7 +518,7 @@ def _server_alive(port: int, timeout: float = 0.5) -> bool:
482
518
 
483
519
  def _server_pid(port: int) -> int | None:
484
520
  """Return the pid of the responding ArrayView server, or None if unreachable."""
485
- url = f"http://127.0.0.1:{port}/ping"
521
+ url = f"http://{_LOOPBACK_HOST}:{port}/ping"
486
522
  try:
487
523
  with urllib.request.urlopen(url, timeout=0.5) as resp:
488
524
  if resp.status != 200:
@@ -497,7 +533,7 @@ def _server_pid(port: int) -> int | None:
497
533
 
498
534
  def _server_hostname(port: int, timeout: float = 0.5) -> str | None:
499
535
  """Return the hostname reported by the ArrayView server on ``port``, or None."""
500
- url = f"http://127.0.0.1:{port}/ping"
536
+ url = f"http://{_LOOPBACK_HOST}:{port}/ping"
501
537
  try:
502
538
  with urllib.request.urlopen(url, timeout=timeout) as resp:
503
539
  if resp.status != 200:
@@ -515,7 +551,7 @@ def _relay_array_to_server(
515
551
  port: int,
516
552
  name: str,
517
553
  rgb: bool = False,
518
- relay_host: str = "127.0.0.1",
554
+ relay_host: str = _LOOPBACK_HOST,
519
555
  ) -> None:
520
556
  """Load *filepath* locally and POST the bytes to an ArrayView relay server.
521
557
 
@@ -523,7 +559,7 @@ def _relay_array_to_server(
523
559
  ArrayView server (e.g. tunnel-remote). The relay server registers the
524
560
  session and writes its own VS Code signal file so a viewer tab opens there.
525
561
 
526
- ``relay_host`` defaults to 127.0.0.1; only change it when the relay server
562
+ ``relay_host`` defaults to localhost; only change it when the relay server
527
563
  is genuinely on a different network interface (rare).
528
564
  """
529
565
  import base64
@@ -567,7 +603,7 @@ type _CompareSids = list[str] | tuple[str, ...]
567
603
 
568
604
  def _server_json_request(port: int, path: str, payload: dict) -> dict:
569
605
  req = urllib.request.Request(
570
- f"http://127.0.0.1:{port}{path}",
606
+ f"http://{_LOOPBACK_HOST}:{port}{path}",
571
607
  data=json.dumps(payload).encode(),
572
608
  headers={"Content-Type": "application/json"},
573
609
  method="POST",
@@ -1002,7 +1038,7 @@ def _handle_cli_spawned_daemon(
1002
1038
  window_mode=window_mode,
1003
1039
  floating=floating,
1004
1040
  is_remote=is_remote,
1005
- webview_already_opened=early_webview_notified,
1041
+ webview_already_opened=early_webview_opened,
1006
1042
  )
1007
1043
 
1008
1044
 
@@ -1170,7 +1206,7 @@ def _promote_view_to_vscode_terminal(
1170
1206
 
1171
1207
  def _port_in_use(port: int) -> bool:
1172
1208
  try:
1173
- with socket.create_connection(("127.0.0.1", port), timeout=0.3):
1209
+ with socket.create_connection((_LOOPBACK_HOST, port), timeout=0.3):
1174
1210
  return True
1175
1211
  except OSError:
1176
1212
  return False
@@ -1311,25 +1347,17 @@ async def _serve_background(port: int, stop_when_closed: bool = False):
1311
1347
  _loading_port = None # reset for this server lifetime
1312
1348
  _session_mod.SERVER_LOOP = asyncio.get_running_loop()
1313
1349
  _session_mod.SERVER_PORT = port
1314
- import socket as _socket
1315
-
1316
- # Pre-create the socket with SO_REUSEADDR so we can rebind immediately after
1317
- # a previous server on this port was killed (avoids TIME_WAIT Errno 48).
1318
- sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
1319
- sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
1320
- sock.bind(("127.0.0.1", port))
1321
- sock.listen(128)
1322
- sock.set_inheritable(True)
1350
+ # Bind on the same loopback hostname the viewer URLs use. On macOS,
1351
+ # localhost often resolves to ::1 first, so binding only 127.0.0.1 can
1352
+ # leave the native shell stuck on its loading overlay forever.
1353
+ sock = _make_loopback_socket(port)
1323
1354
 
1324
1355
  # Bind the loading-page server on an OS-chosen ephemeral port.
1325
1356
  # This uses only stdlib so it starts in microseconds — well before
1326
1357
  # uvicorn's heavy imports finish. _loading_port is read by the main
1327
1358
  # thread after _server_ready_event fires.
1328
1359
  try:
1329
- _lsock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
1330
- _lsock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
1331
- _lsock.bind(("127.0.0.1", 0))
1332
- _lsock.listen(16)
1360
+ _lsock = _make_loopback_socket(0)
1333
1361
  _loading_port = _lsock.getsockname()[1]
1334
1362
  threading.Thread(
1335
1363
  target=_run_loading_server, args=(_lsock,), daemon=True
@@ -1361,7 +1389,7 @@ def _with_loading(url: str) -> str:
1361
1389
  """
1362
1390
  if _loading_port is not None:
1363
1391
  encoded = urllib.parse.quote(url, safe="")
1364
- return f"http://127.0.0.1:{_loading_port}/?target={encoded}"
1392
+ return f"http://{_LOOPBACK_HOST}:{_loading_port}/?target={encoded}"
1365
1393
  return url
1366
1394
 
1367
1395
 
@@ -1615,7 +1643,7 @@ class ViewHandle(str):
1615
1643
  _np.save(buf, arr)
1616
1644
  body = buf.getvalue()
1617
1645
  request = _req.Request(
1618
- f"http://127.0.0.1:{self._port}/update/{self._sid}",
1646
+ f"http://{_LOOPBACK_HOST}:{self._port}/update/{self._sid}",
1619
1647
  data=body,
1620
1648
  method="POST",
1621
1649
  )
@@ -1625,7 +1653,7 @@ class ViewHandle(str):
1625
1653
  except Exception as e:
1626
1654
  raise RuntimeError(
1627
1655
  f"[ArrayView] Failed to update viewer: {e}\n"
1628
- f" URL: http://127.0.0.1:{self._port}/update/{self._sid}\n"
1656
+ f" URL: http://{_LOOPBACK_HOST}:{self._port}/update/{self._sid}\n"
1629
1657
  f" Is the ArrayView server still running?"
1630
1658
  ) from e
1631
1659
 
@@ -2507,7 +2535,7 @@ def _serve_empty(port: int) -> None:
2507
2535
  threading.Thread(
2508
2536
  target=lambda: _uvicorn().run(
2509
2537
  _server_mod().app,
2510
- host="127.0.0.1",
2538
+ host=_LOOPBACK_HOST,
2511
2539
  port=port,
2512
2540
  log_level="error",
2513
2541
  timeout_keep_alive=30,
@@ -2548,13 +2576,7 @@ def _serve_daemon(
2548
2576
  _session_mod.PENDING_SESSION_EVENTS[sid] = _pending_event
2549
2577
  _session_mod.SERVER_PORT = port
2550
2578
 
2551
- import socket as _socket
2552
-
2553
- sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
2554
- sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
2555
- sock.bind(("127.0.0.1", port))
2556
- sock.listen(128)
2557
- sock.set_inheritable(True)
2579
+ sock = _make_loopback_socket(port)
2558
2580
 
2559
2581
  def _run_uvicorn_on_socket():
2560
2582
  config = _uvicorn().Config(
@@ -2741,7 +2763,7 @@ def _start_watch_thread(filepath: str, sid: str, port: int) -> None:
2741
2763
  last_mtime = mtime
2742
2764
  try:
2743
2765
  req = _urlreq.Request(
2744
- f"http://127.0.0.1:{port}/reload/{sid}",
2766
+ f"http://{_LOOPBACK_HOST}:{port}/reload/{sid}",
2745
2767
  data=b"",
2746
2768
  method="POST",
2747
2769
  )
@@ -3209,7 +3231,7 @@ def arrayview():
3209
3231
  if ":" in relay_str:
3210
3232
  relay_host, relay_port_str = relay_str.rsplit(":", 1)
3211
3233
  else:
3212
- relay_host, relay_port_str = "127.0.0.1", relay_str
3234
+ relay_host, relay_port_str = _LOOPBACK_HOST, relay_str
3213
3235
  try:
3214
3236
  relay_port = int(relay_port_str)
3215
3237
  except ValueError:
@@ -6,6 +6,44 @@ import os
6
6
  import subprocess
7
7
  import sys
8
8
 
9
+
10
+ def get_ppid(pid: int) -> int:
11
+ """Return parent PID of *pid*, or -1 if unavailable.
12
+
13
+ Cross-platform: /proc on Linux, ps on macOS, wmic on Windows.
14
+ """
15
+ if sys.platform == "win32":
16
+ try:
17
+ r = subprocess.run(
18
+ ["wmic", "process", "where", f"processid={pid}",
19
+ "get", "parentprocessid"],
20
+ capture_output=True, text=True, timeout=5,
21
+ )
22
+ for line in r.stdout.strip().splitlines():
23
+ val = line.strip()
24
+ if val.isdigit():
25
+ return int(val)
26
+ except Exception:
27
+ pass
28
+ return -1
29
+ try:
30
+ with open(f"/proc/{pid}/status") as fh:
31
+ for line in fh:
32
+ if line.startswith("PPid:"):
33
+ return int(line.split()[1])
34
+ except Exception:
35
+ pass
36
+ try:
37
+ r = subprocess.run(
38
+ ["ps", "-p", str(pid), "-o", "ppid="],
39
+ capture_output=True, text=True, timeout=2,
40
+ )
41
+ return int(r.stdout.strip())
42
+ except Exception:
43
+ pass
44
+ return -1
45
+
46
+
9
47
  # ---------------------------------------------------------------------------
10
48
  # Jupyter
11
49
  # ---------------------------------------------------------------------------
@@ -58,26 +96,6 @@ def _in_matlab() -> bool:
58
96
  _MATLAB_CACHE = True
59
97
  return True
60
98
 
61
- def _ppid(pid: int) -> int:
62
- try:
63
- with open(f"/proc/{pid}/status") as fh:
64
- for line in fh:
65
- if line.startswith("PPid:"):
66
- return int(line.split()[1])
67
- except Exception:
68
- pass
69
- try:
70
- r = subprocess.run(
71
- ["ps", "-p", str(pid), "-o", "ppid="],
72
- capture_output=True,
73
- text=True,
74
- timeout=2,
75
- )
76
- return int(r.stdout.strip())
77
- except Exception:
78
- pass
79
- return -1
80
-
81
99
  def _command_from_pid(pid: int) -> str:
82
100
  try:
83
101
  with open(f"/proc/{pid}/cmdline", "rb") as fh:
@@ -103,7 +121,7 @@ def _in_matlab() -> bool:
103
121
  if "matlab" in cmd:
104
122
  _MATLAB_CACHE = True
105
123
  return True
106
- pid = _ppid(pid)
124
+ pid = get_ppid(pid)
107
125
  if pid <= 1:
108
126
  break
109
127
 
@@ -130,26 +148,6 @@ def _find_vscode_ipc_hook() -> str | None:
130
148
  if _VSCODE_IPC_HOOK_CACHE != "__unset__":
131
149
  return _VSCODE_IPC_HOOK_CACHE
132
150
 
133
- def _ppid(pid: int) -> int:
134
- try:
135
- with open(f"/proc/{pid}/status") as fh:
136
- for line in fh:
137
- if line.startswith("PPid:"):
138
- return int(line.split()[1])
139
- except Exception:
140
- pass
141
- try:
142
- r = subprocess.run(
143
- ["ps", "-p", str(pid), "-o", "ppid="],
144
- capture_output=True,
145
- text=True,
146
- timeout=2,
147
- )
148
- return int(r.stdout.strip())
149
- except Exception:
150
- pass
151
- return -1
152
-
153
151
  def _ipc_from_pid(pid: int) -> str:
154
152
  # Linux: /proc/<pid>/environ (null-separated KEY=VALUE pairs)
155
153
  try:
@@ -184,7 +182,7 @@ def _find_vscode_ipc_hook() -> str | None:
184
182
  # Walk up to 12 ancestor processes
185
183
  pid = os.getpid()
186
184
  for _ in range(12):
187
- pid = _ppid(pid)
185
+ pid = get_ppid(pid)
188
186
  if pid <= 1:
189
187
  break
190
188
  val = _ipc_from_pid(pid)
@@ -1,4 +1,5 @@
1
1
  import io
2
+ import time
2
3
 
3
4
  import numpy as np
4
5
  from fastapi import Depends, Response
@@ -93,8 +94,11 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
93
94
  te: float | None = None,
94
95
  tr: float | None = None,
95
96
  ti: float | None = None,
97
+ perf: bool = False,
96
98
  session=Depends(get_session_or_404),
97
99
  ):
100
+ total_t0 = time.perf_counter()
101
+ render_t0 = total_t0
98
102
  idx_tuple = tuple(int(x) for x in indices.split(","))
99
103
  if synthetic_mri:
100
104
  try:
@@ -252,17 +256,40 @@ def register_rendering_routes(app, *, get_session_or_404) -> None:
252
256
  vmin_override=vmin_override,
253
257
  vmax_override=vmax_override,
254
258
  )
259
+ render_ms = (time.perf_counter() - render_t0) * 1000.0
260
+ encode_t0 = time.perf_counter()
255
261
  img = _pil_image().fromarray(rgba[:, :, :3], mode="RGB")
256
262
  buf = io.BytesIO()
257
263
  img.save(buf, format="JPEG", quality=90)
264
+ payload = buf.getvalue()
265
+ encode_ms = (time.perf_counter() - encode_t0) * 1000.0
266
+ total_ms = (time.perf_counter() - total_t0) * 1000.0
267
+ headers = {
268
+ "Cache-Control": "max-age=300",
269
+ "X-ArrayView-Vmin": str(vmin),
270
+ "X-ArrayView-Vmax": str(vmax),
271
+ }
272
+ if perf:
273
+ payload_bytes = len(payload)
274
+ headers.update(
275
+ {
276
+ "Server-Timing": (
277
+ f"render;dur={render_ms:.3f}, "
278
+ f"encode;dur={encode_ms:.3f}, total;dur={total_ms:.3f}"
279
+ ),
280
+ "X-ArrayView-Render-Ms": f"{render_ms:.3f}",
281
+ "X-ArrayView-Encode-Ms": f"{encode_ms:.3f}",
282
+ "X-ArrayView-Total-Ms": f"{total_ms:.3f}",
283
+ "X-ArrayView-Payload-Bytes": str(payload_bytes),
284
+ "X-ArrayView-Raw-Cache-Entries": str(len(session.raw_cache)),
285
+ "X-ArrayView-RGBA-Cache-Entries": str(len(session.rgba_cache)),
286
+ "X-ArrayView-Mosaic-Cache-Entries": str(len(session.mosaic_cache)),
287
+ }
288
+ )
258
289
  return Response(
259
- content=buf.getvalue(),
290
+ content=payload,
260
291
  media_type="image/jpeg",
261
- headers={
262
- "Cache-Control": "max-age=300",
263
- "X-ArrayView-Vmin": str(vmin),
264
- "X-ArrayView-Vmax": str(vmax),
265
- },
292
+ headers=headers,
266
293
  )
267
294
 
268
295
  @app.get("/diff/{sid_a}/{sid_b}")
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import time
2
3
 
3
4
  import numpy as np
4
5
  from fastapi import WebSocket
@@ -159,6 +160,9 @@ def register_websocket_routes(app) -> None:
159
160
  qmri_dim = int(msg.get("qmri_dim", -1))
160
161
  qmri_role = str(msg.get("qmri_role", ""))
161
162
  synthetic_mri = str(msg.get("synthetic_mri", ""))
163
+ perf = bool(msg.get("perf", False))
164
+ total_t0 = time.perf_counter()
165
+ render_t0 = total_t0
162
166
 
163
167
  if synthetic_mri:
164
168
  te = msg.get("te")
@@ -331,6 +335,8 @@ def register_websocket_routes(app) -> None:
331
335
  (h, w),
332
336
  )
333
337
 
338
+ render_ms = (time.perf_counter() - render_t0) * 1000.0
339
+ post_t0 = time.perf_counter()
334
340
  header = np.array([seq, w, h], dtype=np.uint32).tobytes()
335
341
  vminmax = np.array([vmin, vmax], dtype=np.float32).tobytes()
336
342
  if canvas_w and canvas_h and (w > canvas_w or h > canvas_h):
@@ -362,6 +368,23 @@ def register_websocket_routes(app) -> None:
362
368
  vf_scale = np.array([vf_result["scale"]], dtype=np.float32).tobytes()
363
369
  payload += vf_hdr + vf_scale + arrows.tobytes()
364
370
  await ws.send_bytes(payload)
371
+ if perf:
372
+ total_ms = (time.perf_counter() - total_t0) * 1000.0
373
+ await ws.send_json(
374
+ {
375
+ "type": "render_timing",
376
+ "seq": seq,
377
+ "total_ms": total_ms,
378
+ "render_ms": render_ms,
379
+ "post_ms": (time.perf_counter() - post_t0) * 1000.0,
380
+ "payload_bytes": len(payload),
381
+ "width": int(w),
382
+ "height": int(h),
383
+ "raw_cache_entries": len(session.raw_cache),
384
+ "rgba_cache_entries": len(session.rgba_cache),
385
+ "mosaic_cache_entries": len(session.mosaic_cache),
386
+ }
387
+ )
365
388
 
366
389
  if slice_dim >= 0 and not (dim_z >= 0):
367
390
  _schedule_prefetch(
@@ -243,26 +243,26 @@
243
243
  }
244
244
  connectShellWS();
245
245
 
246
- if (!window.__av_inline) {
247
- const params = new URLSearchParams(window.location.search);
248
- const initSid = params.get('init_sid');
249
- const initName = params.get('init_name');
250
- const initCompareSid = params.get('init_compare_sid');
251
- const initCompareSids = params.get('init_compare_sids');
252
- if (initSid) {
253
- let url = `/?sid=${initSid}`;
254
- if (initCompareSid) url += `&compare_sid=${initCompareSid}&compare_sids=${initCompareSids}`;
255
- addTab(initSid, initName || 'Array', url);
256
- } else {
257
- fetch('/sessions').then(r => r.json()).then(sessions => {
258
- if (sessions.length > 0) {
259
- sessions.forEach(s => addTab(s.sid, s.name));
260
- } else {
261
- // No arrays loaded yet — show the welcome/plasma screen
262
- addTab('__welcome__', '', '/');
263
- }
264
- }).catch(() => { addTab('__welcome__', '', '/'); });
265
- }
246
+ const initParams = new URLSearchParams(
247
+ window.__av_inline ? (window.__av_inlineQuery || '') : window.location.search
248
+ );
249
+ const initSid = initParams.get('init_sid');
250
+ const initName = initParams.get('init_name');
251
+ const initCompareSid = initParams.get('init_compare_sid');
252
+ const initCompareSids = initParams.get('init_compare_sids');
253
+ if (initSid) {
254
+ let url = `/?sid=${initSid}`;
255
+ if (initCompareSid) url += `&compare_sid=${initCompareSid}&compare_sids=${initCompareSids}`;
256
+ addTab(initSid, initName || 'Array', url);
257
+ } else if (!window.__av_inline) {
258
+ fetch('/sessions').then(r => r.json()).then(sessions => {
259
+ if (sessions.length > 0) {
260
+ sessions.forEach(s => addTab(s.sid, s.name));
261
+ } else {
262
+ // No arrays loaded yet — show the welcome/plasma screen
263
+ addTab('__welcome__', '', '/');
264
+ }
265
+ }).catch(() => { addTab('__welcome__', '', '/'); });
266
266
  }
267
267
  </script>
268
268
  </body>