arrayview 0.7.0__tar.gz → 0.9.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 (85) hide show
  1. {arrayview-0.7.0 → arrayview-0.9.0}/.claude/skills/ui-consistency-audit/SKILL.md +32 -0
  2. arrayview-0.9.0/.claude/skills/visual-bug-fixing/SKILL.md +153 -0
  3. {arrayview-0.7.0 → arrayview-0.9.0}/.gitignore +5 -1
  4. arrayview-0.9.0/AGENTS.md +40 -0
  5. {arrayview-0.7.0 → arrayview-0.9.0}/PKG-INFO +18 -2
  6. {arrayview-0.7.0 → arrayview-0.9.0}/README.md +17 -1
  7. {arrayview-0.7.0 → arrayview-0.9.0}/docs/remote.md +26 -5
  8. arrayview-0.9.0/plans/webview/LOG.md +130 -0
  9. {arrayview-0.7.0 → arrayview-0.9.0}/pyproject.toml +1 -1
  10. arrayview-0.9.0/scripts/release.sh +78 -0
  11. arrayview-0.9.0/src/arrayview/ARCHITECTURE.md +197 -0
  12. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/__init__.py +1 -1
  13. arrayview-0.9.0/src/arrayview/__main__.py +4 -0
  14. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_app.py +1 -2
  15. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_config.py +17 -0
  16. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_io.py +87 -4
  17. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_launcher.py +332 -28
  18. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_render.py +9 -25
  19. arrayview-0.9.0/src/arrayview/_segmentation.py +227 -0
  20. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_server.py +954 -48
  21. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_session.py +13 -84
  22. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_shell.html +8 -1
  23. arrayview-0.9.0/src/arrayview/_stdio_server.py +791 -0
  24. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_viewer.html +2682 -1620
  25. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_vscode.py +247 -75
  26. arrayview-0.9.0/src/arrayview/arrayview-opener.vsix +0 -0
  27. {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_api.py +59 -0
  28. {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_browser.py +7 -2
  29. {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_cli.py +3 -3
  30. {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_interactions.py +23 -23
  31. {arrayview-0.7.0 → arrayview-0.9.0}/tests/ui_audit.py +118 -0
  32. {arrayview-0.7.0 → arrayview-0.9.0}/tests/visual_smoke.py +29 -2
  33. {arrayview-0.7.0 → arrayview-0.9.0}/uv.lock +1 -1
  34. arrayview-0.9.0/vscode-extension/extension.js +895 -0
  35. {arrayview-0.7.0 → arrayview-0.9.0}/vscode-extension/package.json +9 -2
  36. arrayview-0.7.0/.claude/skills/task-workflow/SKILL.md +0 -82
  37. arrayview-0.7.0/AGENTS.md +0 -142
  38. arrayview-0.7.0/docs/superpowers/plans/2026-03-27-colorbar-class-refactor.md +0 -1642
  39. arrayview-0.7.0/docs/superpowers/plans/2026-03-27-mode-dispatch-refactor.md +0 -717
  40. arrayview-0.7.0/docs/superpowers/plans/2026-03-28-exploded-volume-view.md +0 -854
  41. arrayview-0.7.0/docs/superpowers/plans/2026-03-28-instance-previewer-ram-guard.md +0 -601
  42. arrayview-0.7.0/docs/superpowers/plans/2026-03-29-multi-array-immersive.md +0 -726
  43. arrayview-0.7.0/docs/superpowers/plans/2026-03-30-loading-screen.md +0 -192
  44. arrayview-0.7.0/docs/superpowers/plans/2026-03-30-pytorch-dl-integration.md +0 -807
  45. arrayview-0.7.0/docs/superpowers/specs/2026-03-27-islands-immersive-only-design.md +0 -60
  46. arrayview-0.7.0/docs/superpowers/specs/2026-03-28-exploded-volume-view-design.md +0 -94
  47. arrayview-0.7.0/docs/superpowers/specs/2026-03-28-instance-previewer-ram-guard-design.md +0 -158
  48. arrayview-0.7.0/docs/superpowers/specs/2026-03-29-multi-array-immersive-design.md +0 -84
  49. arrayview-0.7.0/docs/superpowers/specs/2026-03-30-loading-screen-design.md +0 -33
  50. arrayview-0.7.0/docs/superpowers/specs/2026-03-30-pytorch-dl-integration-design.md +0 -205
  51. arrayview-0.7.0/src/arrayview/arrayview-opener.vsix +0 -0
  52. arrayview-0.7.0/vscode-extension/extension.js +0 -415
  53. {arrayview-0.7.0 → arrayview-0.9.0}/.claude/skills/invocation-consistency/SKILL.md +0 -0
  54. {arrayview-0.7.0 → arrayview-0.9.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
  55. {arrayview-0.7.0 → arrayview-0.9.0}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
  56. {arrayview-0.7.0 → arrayview-0.9.0}/.github/workflows/docs.yml +0 -0
  57. {arrayview-0.7.0 → arrayview-0.9.0}/.github/workflows/python-publish.yml +0 -0
  58. {arrayview-0.7.0 → arrayview-0.9.0}/.python-version +0 -0
  59. {arrayview-0.7.0 → arrayview-0.9.0}/.tmp-vsix/extension/extension.js +0 -0
  60. {arrayview-0.7.0 → arrayview-0.9.0}/.tmp-vsix/extension/package.json +0 -0
  61. {arrayview-0.7.0 → arrayview-0.9.0}/LICENSE +0 -0
  62. {arrayview-0.7.0 → arrayview-0.9.0}/docs/comparing.md +0 -0
  63. {arrayview-0.7.0 → arrayview-0.9.0}/docs/configuration.md +0 -0
  64. {arrayview-0.7.0 → arrayview-0.9.0}/docs/display.md +0 -0
  65. {arrayview-0.7.0 → arrayview-0.9.0}/docs/index.md +0 -0
  66. {arrayview-0.7.0 → arrayview-0.9.0}/docs/loading.md +0 -0
  67. {arrayview-0.7.0 → arrayview-0.9.0}/docs/logo.png +0 -0
  68. {arrayview-0.7.0 → arrayview-0.9.0}/docs/measurement.md +0 -0
  69. {arrayview-0.7.0 → arrayview-0.9.0}/docs/stylesheets/extra.css +0 -0
  70. {arrayview-0.7.0 → arrayview-0.9.0}/docs/viewing.md +0 -0
  71. {arrayview-0.7.0 → arrayview-0.9.0}/matlab/arrayview.m +0 -0
  72. {arrayview-0.7.0 → arrayview-0.9.0}/mkdocs.yml +0 -0
  73. {arrayview-0.7.0 → arrayview-0.9.0}/scripts/demo.py +0 -0
  74. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_icon.png +0 -0
  75. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_platform.py +0 -0
  76. {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_torch.py +0 -0
  77. {arrayview-0.7.0 → arrayview-0.9.0}/tests/conftest.py +0 -0
  78. {arrayview-0.7.0 → arrayview-0.9.0}/tests/make_vectorfield_test_arrays.py +0 -0
  79. {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_config.py +0 -0
  80. {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_large_arrays.py +0 -0
  81. {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_mode_consistency.py +0 -0
  82. {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_mode_matrix.py +0 -0
  83. {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_rgb_pixel_art.py +0 -0
  84. {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_torch.py +0 -0
  85. {arrayview-0.7.0 → arrayview-0.9.0}/vscode-extension/LICENSE +0 -0
@@ -222,6 +222,13 @@ These rules are codified as DOM assertions in `tests/ui_audit.py`. When a new in
222
222
  | R26 | Shared colorbar width capped in compare mode | `#slim-cb-wrap` width ≤ 500px in compare mode |
223
223
  | R27 | Mode eggs hidden during loading | `#mode-eggs` empty/hidden until at least one frame has been received |
224
224
  | R28 | Compare+multiview crosshair fade works | Crosshair lines in compare+multiview fade in/out on hover, scoped to the hovered array's panes |
225
+ | R29 | Dimbar/colorbar height sync | `#info` and `#slim-cb-wrap` offsetHeight within 4px when both visible. Enforced by `_validateUIState()` (runtime) and `run_invariant_assertions()` (Playwright) |
226
+ | R30 | Drag positions cleared on immersive exit | `_infoDragPos`, `_cbDragPos`, `_islandDragPos` all null when `!_fullscreenActive`. Enforced by `_validateUIState()` and `run_immersive_exit_assertions()` |
227
+ | R31 | No .fs-overlay outside immersive | Zero `.fs-overlay` elements when `!_fullscreenActive && !_immersiveAnimating`. Enforced by `_validateUIState()` and `run_immersive_exit_assertions()` |
228
+ | R32 | Per-pane cbs only in immersive+center | `.compare-pane-cb-island.fs-overlay` count > 0 only when `_fullscreenActive && centerMode`. Enforced by `_validateUIState()` |
229
+ | R33 | Colorbar flex-direction always row | All visible `.cb-island` computed `flex-direction === 'row'`. Enforced by CSS (`!important`) and `_validateUIState()` and `run_invariant_assertions()` |
230
+ | R34 | Shared colorbar visible in compare non-center | `#slim-cb-wrap` not `display:none` when `compareActive && !centerMode`. Enforced by `_validateUIState()` |
231
+ | R35 | Islands fully inside or outside pane | Dynamic islands must be fully inside pane bounds (immersive) or fully outside in normal flow (non-immersive) — never partially overlapping |
225
232
 
226
233
  ---
227
234
 
@@ -240,6 +247,31 @@ All colorbars across ALL modes follow the same structure: `[vmin span] [gradient
240
247
 
241
248
  ---
242
249
 
250
+ ## Section 5d: Enforcement Layers
251
+
252
+ Rules are enforced through multiple layers. When adding new rules, add enforcement at the appropriate layer(s):
253
+
254
+ | Layer | Mechanism | Where | When it runs |
255
+ |-------|-----------|-------|--------------|
256
+ | **CSS** | `!important` structural constraints | `_viewer.html` CSS (line ~156) | Always — structurally impossible to violate |
257
+ | **Runtime JS** | `_validateUIState()` | `_viewer.html` JS (after `_positionFullscreenChrome`) | After every layout change, gated behind `?debug_ui=1` |
258
+ | **Playwright** | `run_invariant_assertions()` + `run_immersive_exit_assertions()` | `tests/ui_audit.py` | During `ui_audit.py` runs (all tiers) |
259
+ | **Skill** | This document (Section 5b) | `.claude/skills/ui-consistency-audit/SKILL.md` | When AI agent invokes the skill |
260
+
261
+ **Cross-reference:**
262
+
263
+ | Rule | CSS | Runtime JS | Playwright | Notes |
264
+ |------|-----|-----------|------------|-------|
265
+ | R29 (height sync) | — | ✓ | ✓ | |
266
+ | R30 (drag cleanup) | — | ✓ | ✓ | Only after immersive exit |
267
+ | R31 (fs-overlay cleanup) | — | ✓ | ✓ | Only after immersive exit |
268
+ | R32 (per-pane cb visibility) | — | ✓ | — | |
269
+ | R33 (flex-direction row) | ✓ | ✓ | ✓ | CSS makes violation impossible |
270
+ | R34 (shared cb in compare) | — | ✓ | — | |
271
+ | R35 (island containment) | — | — | — | Design principle, enforced by code review |
272
+
273
+ ---
274
+
243
275
  ## Section 6: Common Feature Categories
244
276
 
245
277
  ### Zoom / Canvas Sizing
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: visual-bug-fixing
3
+ description: Use when fixing any visual bug, layout glitch, rendering artifact, or UI regression in arrayview. Triggered by symptoms like overlapping elements, wrong positioning, missing components, flickering, or incorrect sizing in any view mode.
4
+ ---
5
+
6
+ # Visual Bug Fixing
7
+
8
+ ## Overview
9
+
10
+ **Fix visual bugs with screenshot evidence, not guesswork.** Capture before-state, diagnose root cause, fix, capture after-state, verify no regressions across all view modes. Never commit a visual fix without photographic proof it works and doesn't break anything else.
11
+
12
+ ## When to Use
13
+
14
+ - User reports a visual bug (overlap, misalignment, wrong size, missing element, rendering artifact)
15
+ - You notice a visual inconsistency during other work
16
+ - A UI change causes unexpected layout shifts
17
+ - Screenshot diff failures in `ui_audit.py`
18
+
19
+ **When NOT to use:**
20
+ - Pure logic bugs with no visual impact — use superpowers:systematic-debugging
21
+ - Adding new UI features — use ui-consistency-audit + frontend-designer
22
+ - Updating test coverage — use viewer-ui-checklist
23
+
24
+ ## Workflow
25
+
26
+ ```dot
27
+ digraph visual_bug_fix {
28
+ rankdir=TB;
29
+ "Bug reported" -> "Capture baseline screenshots";
30
+ "Capture baseline screenshots" -> "Diagnose root cause";
31
+ "Diagnose root cause" -> "Implement fix";
32
+ "Implement fix" -> "Capture post-fix screenshots";
33
+ "Capture post-fix screenshots" -> "Compare before/after";
34
+ "Compare before/after" -> "Run full regression check";
35
+ "Run full regression check" -> "Regressions found?" [shape=diamond];
36
+ "Regressions found?" -> "Implement fix" [label="yes — iterate"];
37
+ "Regressions found?" -> "Show evidence & commit" [label="no"];
38
+ }
39
+ ```
40
+
41
+ ### Step 1: Capture Baseline Screenshots
42
+
43
+ Before touching any code, capture the current broken state. Use the Playwright MCP tools:
44
+
45
+ 1. **Navigate** to the affected view using `browser_navigate` to the arrayview URL
46
+ 2. **Set up the bug conditions** — press keys to enter the right mode, load the right array
47
+ 3. **Screenshot the broken state** with `browser_take_screenshot` — save as `baseline_bug.png`
48
+ 4. **Screenshot ALL other modes** that could be affected:
49
+ - Normal view (default)
50
+ - Immersive mode (`Shift+K`)
51
+ - Compare mode (navigate to compare URL)
52
+ - Diff modes (`Shift+X` to cycle)
53
+ - Multiview (`v`)
54
+ - Zen mode (`Shift+F`)
55
+ - Zoomed state (`+` keys)
56
+
57
+ **Key selectors to inspect** (via `browser_snapshot` or `browser_evaluate`):
58
+ - `#canvas-wrap` — main canvas container
59
+ - `#slim-cb-wrap` — colorbar wrapper
60
+ - `#info` — info bar
61
+ - `.mode-egg` — mode indicator badges
62
+ - `#miniview` — minimap overlay
63
+ - `#dimbar` — dimension navigation bar
64
+
65
+ ### Step 2: Diagnose Root Cause
66
+
67
+ Read the relevant source files. The UI lives in:
68
+
69
+ | Layer | File |
70
+ |-------|------|
71
+ | HTML structure | `arrayview/_viewer.html` |
72
+ | Inline CSS | `<style>` blocks in `_viewer.html` |
73
+ | Canvas rendering | JavaScript in `_viewer.html` (search for `scaleCanvas`, `mvScaleAllCanvases`, `compareScaleCanvases`) |
74
+ | Python server | `arrayview/_app.py` |
75
+ | Colorbar logic | `ColorBar` class in `_viewer.html` |
76
+
77
+ **Diagnosis checklist:**
78
+ - Is this a CSS issue (positioning, overflow, z-index, flexbox)?
79
+ - Is this a JS layout calculation issue (wrong width/height, missing mode branch)?
80
+ - Does the bug only appear in certain modes? Which scale function is involved?
81
+ - Does zoom level matter? Check the zoom-related code paths.
82
+ - Is this a race condition (element rendered before data arrives)?
83
+
84
+ **Check hard rules:** Before implementing a fix, check if the bug is a violation of a hard UI rule (R1-R35 in `ui-consistency-audit` Section 5b). If so, the fix should restore the invariant rather than work around it. Enable `?debug_ui=1` to see runtime invariant warnings in the browser console — they may pinpoint the exact rule being violated.
85
+
86
+ **Use `browser_evaluate` to inspect live DOM state:**
87
+ ```javascript
88
+ // Get bounding boxes of key elements
89
+ JSON.stringify({
90
+ canvas: document.querySelector('#canvas-wrap')?.getBoundingClientRect(),
91
+ colorbar: document.querySelector('#slim-cb-wrap')?.getBoundingClientRect(),
92
+ info: document.querySelector('#info')?.getBoundingClientRect(),
93
+ viewport: { width: window.innerWidth, height: window.innerHeight }
94
+ })
95
+ ```
96
+
97
+ ### Step 3: Implement the Fix
98
+
99
+ Apply the minimal fix. Reference the ui-consistency-audit design rules (R1-R35) to ensure the fix doesn't violate any existing constraints.
100
+
101
+ **Common fix patterns:**
102
+ - **Overlap bugs**: Check z-index hierarchy, adjust `position`/`top`/`left`/`right`
103
+ - **Sizing bugs**: Trace the calculation chain in the relevant scale function
104
+ - **Mode-specific bugs**: Ensure all mode branches handle the element (check `isCompare`, `isMultiview`, `isImmersive`, `isZen` flags)
105
+ - **Zoom bugs**: Check `zoomLevel` interactions with element positioning
106
+
107
+ ### Step 4: Capture Post-Fix Screenshots
108
+
109
+ Repeat the exact same navigation and key presses from Step 1. Screenshot every view that was captured in the baseline.
110
+
111
+ ### Step 5: Compare Before/After
112
+
113
+ Present the before and after screenshots to the user. Explain:
114
+ - What was broken (with baseline screenshot)
115
+ - What changed in the code
116
+ - What it looks like now (with post-fix screenshot)
117
+
118
+ ### Step 6: Full Regression Check
119
+
120
+ Run the automated visual audit:
121
+
122
+ ```bash
123
+ uv run python tests/ui_audit.py --tier 1
124
+ ```
125
+
126
+ This checks 14 core scenarios with DOM assertions and pixel diffs. If any fail:
127
+ - Read the failure output to identify which scenario regressed
128
+ - Screenshot that specific scenario manually to understand the regression
129
+ - Go back to Step 3 and iterate
130
+
131
+ For thorough validation (before committing):
132
+ ```bash
133
+ uv run python tests/ui_audit.py --tier 2
134
+ ```
135
+
136
+ **Only commit when Tier 1 passes clean.** Tier 2 failures should be investigated but may be pre-existing.
137
+
138
+ ## Red Flags — STOP and Investigate
139
+
140
+ | Symptom | Likely Cause |
141
+ |---------|-------------|
142
+ | Fix works in normal mode but breaks compare | Missing branch in `compareScaleCanvases` |
143
+ | Element disappears on zoom | Absolute positioning without zoom-aware offsets |
144
+ | Fix works at 1440x900 but breaks at other sizes | Hardcoded pixel values instead of relative/calc |
145
+ | Colorbar overlaps canvas after fix | Forgot to account for colorbar width in canvas sizing |
146
+ | Mode eggs shift position | Changed a parent container's layout without updating egg anchoring |
147
+
148
+ ## Common Mistakes
149
+
150
+ - **Fixing the symptom, not the cause** — A `display:none` hack hides the bug. Find why the element is in the wrong place.
151
+ - **Testing only the affected mode** — Visual bugs often have cross-mode impact. Always check at minimum: normal, immersive, compare, multiview.
152
+ - **Skipping the baseline screenshot** — Without before/after evidence, you can't prove the fix worked or detect subtle regressions.
153
+ - **Committing without running ui_audit.py** — DOM assertions catch things screenshots miss (off-by-1px overlaps, viewport overflow).
@@ -29,4 +29,8 @@ dev/
29
29
 
30
30
  benchmarks/data
31
31
  .superpowers/brainstorm
32
- site/
32
+ site/
33
+ van_gogh/
34
+ cig_presentation/
35
+ docs/superpowers
36
+ .playwright-mcp/
@@ -0,0 +1,40 @@
1
+ Read `src/arrayview/ARCHITECTURE.md` for codebase orientation.
2
+
3
+ # ArrayView
4
+
5
+ ## Skills
6
+
7
+ Load the relevant skill before touching the corresponding area.
8
+
9
+ | Skill | When |
10
+ |-------|------|
11
+ | `ui-consistency-audit` | Any visual/UI change |
12
+ | `frontend-designer` | Styling/layout changes to `_viewer.html` |
13
+ | `vscode-simplebrowser` | Extension, signal-file IPC, `_VSCODE_EXT_VERSION` |
14
+ | `invocation-consistency` | Server startup, display-opening, env detection |
15
+ | `docs-style` | README, help overlay, docstrings |
16
+
17
+ ## Non-Negotiables
18
+
19
+ - Always use `localhost` (not `127.0.0.1`) -- required for VS Code port forwarding
20
+ - Never `--force` reinstall the extension if correct version is on disk
21
+ - Do not add logic to `_app.py` -- compat shim only
22
+ - Avoid orphan processes; shutdown must be automatic
23
+ - Do not regress working display paths when fixing another
24
+ - For visual/animation features, propose 2-3 options BEFORE implementing
25
+ - UI visibility changes go through reconcilers (`_reconcileUI`/`_reconcileLayout`/`_reconcileCompareState`/`_reconcileCbVisibility`), not inline `style.display` or `classList` toggles in mode functions
26
+ - All colorbar state (animation, window/level, hover, drag) flows through `primaryCb` ColorBar instance — never read/write legacy globals. Multiview colorbars sync via `primaryCb`.
27
+
28
+ ## Execution
29
+
30
+ Always use **subagent-driven development** for implementation. Commit completed work automatically.
31
+
32
+ ## Testing
33
+
34
+ ```bash
35
+ uv run pytest tests/test_api.py -v # HTTP API
36
+ uv run pytest tests/test_browser.py -v # Playwright
37
+ uv run python tests/visual_smoke.py # screenshots
38
+ ```
39
+
40
+ After any UI change, use `/ui-consistency-audit` to verify across all modes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.7.0
3
+ Version: 0.9.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
@@ -103,7 +103,23 @@ for epoch in range(100):
103
103
 
104
104
  ## Once open
105
105
 
106
- `c` colormaps · `d` dynamic range · `v` 3-plane · `z` mosaic · `Shift+O` overlay toggle · `?` help · colorbar dblclick histogram
106
+ **Navigation:** scroll slices · `h`/`l` cycle dims · `j`/`k` slices · `=`/`-` zoom · drag pan
107
+ **Views:** `v` 3-plane · `z` mosaic · `q` qMRI · `n` compare · `=` immersive
108
+ **Display:** `c`/`C` colormaps · `d`/`D` dynamic range · `f` FFT · `m` complex · `p` projections · `L` log
109
+ **Tools:** `S` segmentation · `u` ruler · `s` screenshot · `?` help
110
+
111
+
112
+ ## nnInteractive Segmentation
113
+
114
+ `S` starts AI-assisted 3D segmentation (requires CUDA). Click/draw to segment, `Enter` to accept.
115
+
116
+ ```toml
117
+ [nninteractive]
118
+ url = "http://gpu-server:1527" # skip auto-launch, use running server
119
+ ```
120
+
121
+ Or: `ARRAYVIEW_NNINTERACTIVE_URL=http://gpu-server:1527`
122
+
107
123
 
108
124
  ## Config
109
125
 
@@ -68,7 +68,23 @@ for epoch in range(100):
68
68
 
69
69
  ## Once open
70
70
 
71
- `c` colormaps · `d` dynamic range · `v` 3-plane · `z` mosaic · `Shift+O` overlay toggle · `?` help · colorbar dblclick histogram
71
+ **Navigation:** scroll slices · `h`/`l` cycle dims · `j`/`k` slices · `=`/`-` zoom · drag pan
72
+ **Views:** `v` 3-plane · `z` mosaic · `q` qMRI · `n` compare · `=` immersive
73
+ **Display:** `c`/`C` colormaps · `d`/`D` dynamic range · `f` FFT · `m` complex · `p` projections · `L` log
74
+ **Tools:** `S` segmentation · `u` ruler · `s` screenshot · `?` help
75
+
76
+
77
+ ## nnInteractive Segmentation
78
+
79
+ `S` starts AI-assisted 3D segmentation (requires CUDA). Click/draw to segment, `Enter` to accept.
80
+
81
+ ```toml
82
+ [nninteractive]
83
+ url = "http://gpu-server:1527" # skip auto-launch, use running server
84
+ ```
85
+
86
+ Or: `ARRAYVIEW_NNINTERACTIVE_URL=http://gpu-server:1527`
87
+
72
88
 
73
89
  ## Config
74
90
 
@@ -16,19 +16,40 @@ Auto-detects VS Code terminals and opens in Simple Browser. Works automatically.
16
16
 
17
17
  ## VS Code tunnel
18
18
 
19
- For remote development through a VS Code tunnel, start the server on the remote machine:
19
+ The VS Code extension uses a **direct webview** transport: the viewer runs
20
+ inside a VS Code webview panel and communicates with a Python subprocess via
21
+ the extension host. No port forwarding or public ports are needed — everything
22
+ stays inside the tunnel.
20
23
 
21
24
  ```bash
22
- # on the remote machine
23
- arrayview --serve
25
+ arrayview volume.nii.gz # opens in a webview tab automatically
26
+ ```
27
+
28
+ ### How it works
29
+
30
+ ```
31
+ Viewer (webview) ←postMessage→ Extension Host ←stdin/stdout→ Python
24
32
  ```
25
33
 
26
- Set port 8000 to Public in the VS Code Ports tab. Load arrays normally — each opens in Simple Browser:
34
+ Instead of a WebSocket connection to a running server, the extension spawns a
35
+ dedicated Python process per array. Slice requests travel through VS Code's
36
+ `postMessage` IPC and the extension relays them to Python's stdin as JSON.
37
+ Binary RGBA responses flow back through stdout with a length prefix.
38
+
39
+ This avoids the main limitation of the WebSocket approach: VS Code tunnels
40
+ don't expose arbitrary ports, so the old method required manually setting
41
+ port 8000 to "Public" in the Ports tab.
42
+
43
+ ### Fallback: WebSocket mode
44
+
45
+ If you prefer the traditional WebSocket server (e.g. for multi-hop setups or
46
+ when sharing the viewer URL), you can still use it:
27
47
 
28
48
  ```bash
29
- arrayview volume.nii.gz
49
+ arrayview --serve
30
50
  ```
31
51
 
52
+ Set port 8000 to Public in the VS Code Ports tab, then load arrays normally.
32
53
  The server persists across invocations. Kill it with `arrayview --kill`.
33
54
 
34
55
  ## Multi-hop
@@ -0,0 +1,130 @@
1
+ # Webview Message Passing Debug Log
2
+
3
+ ## Problem Statement
4
+ `uv run arrayview medium_array.npy` in VS Code tunnel: opens webview tab the first time, but subsequent runs (after closing tab + Ctrl+C) do nothing.
5
+
6
+ ## Root Cause Found (2026-03-31)
7
+
8
+ ### Extension log for first run (works):
9
+ ```
10
+ DISPATCH: file=open-request-v0900.json mode=direct hasFilepath=true ...
11
+ SIGNAL-DATA: mode=direct filepath=...medium_array.npy
12
+ PYTHON: spawning .../python3 -m arrayview --mode stdio .../medium_array.npy
13
+ SIGNAL: missing url <-- v0800 compat duplicate being processed
14
+ SESSION READY: sid=...
15
+ DIRECT: opened "ArrayView: medium_array.npy"
16
+ ```
17
+
18
+ ### Extension log for second run (broken):
19
+ ```
20
+ PYTHON: exited with code null <-- user closed tab + Ctrl+C
21
+ SIGNAL: missing url <-- NO DISPATCH line!
22
+ SIGNAL: missing url
23
+ ```
24
+
25
+ ### Analysis
26
+ 1. First run: v0900 signal (mode=direct) is processed correctly. v0800 compat duplicate also fires, falls through to "missing url" (harmless).
27
+ 2. User closes tab, presses Ctrl+C -> Python subprocess exits (`code null` = SIGTERM)
28
+ 3. Second run: signal files are written by Python, but NO `DISPATCH:` log appears, meaning the signals are being consumed before reaching `processSignalData`.
29
+
30
+ ### Key insight: the v0800 compat signal from the FIRST run
31
+ Python writes to BOTH `open-request-v0900.json` AND `open-request-v0800.json` (same data). On the first run:
32
+ - Tick 1: v0900 is claimed and processed (direct mode -> success)
33
+ - Tick 2: v0800 is claimed and processed (direct mode, but duplicate -> "missing url" because it passes the direct check but the Python process already exited, OR it's a different issue)
34
+
35
+ Wait -- looking again at the log: `SIGNAL: missing url` at 10:12:12.781 happens DURING the first run's processing (between PYTHON spawn and SESSION READY). This is the v0800 being processed on the SAME tick? No -- `tryOpenSignalFile` processes one signal per tick and returns.
36
+
37
+ Actually the v0800 `SIGNAL: missing url` at 10:12:12.781 has NO `DISPATCH:` line either! So this is coming from a DIFFERENT code path or an OLD extension host.
38
+
39
+ ### Root cause hypothesis
40
+ There may be TWO signal file consumers:
41
+ 1. The fs.watch handler
42
+ 2. The setInterval polling handler
43
+
44
+ Both call `tryOpenSignalFile()`. The `isProcessingSignal` flag prevents re-entry WITHIN `processSignalData`, but the claim (rename) happens BEFORE `processSignalData` is called. Two concurrent `tryOpenSignalFile` calls could each claim different files.
45
+
46
+ Actually no -- looking at the code flow: `tryOpenSignalFile` iterates through candidates, tries to rename (claim) each one. If rename succeeds, it processes that file. If another call already claimed it, `rename` throws and `continue` moves to the next candidate.
47
+
48
+ The REAL issue: the v0800 compat signal is being claimed and processed by a SECOND `tryOpenSignalFile` invocation that runs with the OLD in-memory code (no DISPATCH logging). This happens because `fs.watch` can fire multiple times for a single file write, and each fires `tryOpenSignalFile`.
49
+
50
+ ### Actual fix needed
51
+ The v0800 compat signal duplicates are harmful. The second signal gets processed while the first is still in-flight (the `isProcessingSignal` flag isn't set yet when the second call starts because the first hasn't reached `processSignalData` yet -- it's still in the claiming loop).
52
+
53
+ **The real fix**: Stop writing to the v0800 compat signal file. Only write to v0900. The v0800 compat path was for older extension versions which are no longer relevant since we control the extension install.
54
+
55
+ OR: the second run fails because the v0800 from the SECOND run gets claimed by a concurrent `tryOpenSignalFile` that started before `isProcessingSignal` was set by the first one.
56
+
57
+ ## Previous issues resolved
58
+ 1. **System python not found** (initial): Extension spawned `/usr/bin/python3` which didn't have arrayview. Fixed by passing `pythonPath` (sys.executable) in the signal file.
59
+ 2. **Extension not reloading**: `code --install-extension --force` updates files on disk but the extension host in a tunnel doesn't auto-restart. Need "Developer: Restart Extension Host" specifically.
60
+
61
+ ## Fix: v0800 compat duplicate causing isProcessingSignal deadlock
62
+
63
+ **Root cause confirmed**: Python writes to both `v0900` and `v0800` signal files (same data). The v0800 duplicate gets processed after the first direct webview completes, triggering a SECOND `openDirectWebview` that spawns another Python subprocess. This keeps `isProcessingSignal = true` for up to 30 seconds (timeout). During that window, any new signals from a second `arrayview` invocation are silently dropped.
64
+
65
+ **Fix applied**:
66
+ 1. **Python side**: `_open_direct_via_signal_file()` and `_open_direct_via_shm()` now pass `skip_compat=True` to `_write_vscode_signal()`, so direct-mode signals only write to `v0900` (no v0800 duplicate).
67
+ 2. **Extension side**: After detecting a direct-mode signal, immediately delete any compat signal files (`v0800`, `v0400`) before spawning the subprocess. This prevents duplicates even if old Python code writes compat files.
68
+
69
+ ## Fix: extension not loading due to version mismatch (2026-03-31, session 2)
70
+
71
+ **Symptom**: `uv run arrayview medium_array.npy` writes signal file, but nothing happens. Extension log shows no `ACTIVATE` since 10:50. Extension hosts running (PIDs 28784, 4052896) but not loading the extension.
72
+
73
+ **Root cause**: `_VSCODE_EXT_VERSION` in `_vscode.py` was `"0.10.0"` but the bundled VSIX and `vscode-extension/package.json` are at `"0.10.2"`. This caused a cascade:
74
+ 1. `_ensure_vscode_extension()` calls `_remove_old_extension_versions("0.10.0")`, which deletes the `arrayview.arrayview-opener-0.10.2/` directory.
75
+ 2. `_extension_on_disk("0.10.0")` finds the v0.10.0 directory and skips reinstall.
76
+ 3. But `extensions.json` (VS Code's extension registry) still points to the v0.10.2 path.
77
+ 4. VS Code extension host looks for `arrayview.arrayview-opener-0.10.2/`, can't find it, silently skips loading → no ACTIVATE, no signal handling.
78
+
79
+ **Evidence**:
80
+ - `extensions.json` referenced `arrayview.arrayview-opener-0.10.2` but only `0.10.0` directory existed
81
+ - `code --list-extensions --show-versions` showed `@0.10.2` (from metadata) but directory was named `0.10.0`
82
+ - No ACTIVATE log entries after 10:50 despite two running extension hosts
83
+ - `SIGNAL: missing url` entries in log came from a PREVIOUS (now-dead) extension host whose fs.watch/setInterval leaked past deactivation
84
+
85
+ **Fix**:
86
+ 1. Updated `_VSCODE_EXT_VERSION` to `"0.10.2"` in `_vscode.py`
87
+ 2. Uninstalled + reinstalled extension to sync directory name with metadata
88
+ 3. Restart extension host needed to load the extension
89
+
90
+ ## Multi-window targeting issue (2026-03-31, session 3)
91
+
92
+ **Symptom**: When multiple VS Code tunnel windows are open to the same remote (different directories), `arrayview` opens the webview tab in the wrong window.
93
+
94
+ **Analysis**:
95
+ In tunnel/remote mode, the original code skips hookTag matching (`if ipc_hook and not _is_vscode_remote()` — line 642) and falls back to PID ancestry matching. Two potential problems:
96
+
97
+ 1. **hookTag matching was disabled for remotes**: The comment says "terminal and extension host have different IPC hooks in tunnel mode" — but this may not be true. In SSH remotes and possibly tunnels, both the terminal and extension host may share the same `VSCODE_IPC_HOOK_CLI` from the same VS Code server instance. If so, hookTag is the most reliable targeting method.
98
+
99
+ 2. **PID ancestry may be ambiguous**: If the tunnel server's process tree doesn't clearly separate per-window subtrees, PID ancestry matching picks the wrong window (or falls to broadcast, where any window can claim the signal).
100
+
101
+ **Root cause confirmed via diagnostics**:
102
+ 1. Both extension hosts have IDENTICAL ppids `[3342979, 3342975, 22342, ...]` — both are direct children of the same `node` process. PID ancestry matching gives identical scores.
103
+ 2. In a tunnel, all terminals share the same `VSCODE_IPC_HOOK_CLI` socket (the tunnel server's CLI socket), so hookTag is the same for both terminals → always targets the same window.
104
+
105
+ **Fix: EnvironmentVariableCollection**:
106
+ - **Extension side**: Uses `context.environmentVariableCollection.replace('ARRAYVIEW_WINDOW_ID', windowId)` to inject a window-specific env var into all terminals opened in that window. Each window has a unique `windowId` (hookTag or PID).
107
+ - **Python side**: New `_find_arrayview_window_id()` reads `ARRAYVIEW_WINDOW_ID` from direct env or ancestor `/proc/<pid>/environ` (handles `uv run` env stripping). Used as primary targeting method for remote/tunnel, with PID ancestry as fallback.
108
+ - Rebuilt VSIX, force-reinstalled extension, cleaned up stale window registrations.
109
+
110
+ **Result**: Fix confirmed working — each window now correctly targets its own webview tab.
111
+
112
+ ## Fix: av.view() in tunnel using ports instead of direct webview (2026-03-31, session 3)
113
+
114
+ **Symptom**: `av.view(x)` from a Python script in a VS Code tunnel terminal shows "Remote tunnel session on port 8123" message and uses port-based approach instead of direct webview.
115
+
116
+ **Root cause**: A stale server running on port 8123 (from a previous CLI session) caused `_server_alive(port)` at line 836 to return True, taking the URL-based path before reaching the direct webview path at line 928.
117
+
118
+ **Fix**:
119
+ 1. Moved the non-Jupyter tunnel direct webview check ABOVE the `_server_alive` check. Tunnel sessions now always use direct webview regardless of stale servers.
120
+ 2. Changed the `_server_alive` path to only activate for non-remote sessions (`not _is_vscode_remote()`).
121
+ 3. Jupyter in tunnel: changed from "webview tab + message" to inline IFrame mode. VS Code tunnel auto-forwards ports, so IFrames work for the tunnel owner. Added `_configure_vscode_port_preview(port)` to set the port as silent.
122
+
123
+ ## Changes made so far
124
+ - `_vscode.py`: `_open_direct_via_signal_file()` now includes `pythonPath: sys.executable`
125
+ - `_vscode.py`: New `_open_direct_via_shm()` for passing arrays via shared memory
126
+ - `_launcher.py`: `view()` tunnel paths use `_open_direct_via_shm()` instead of temp files
127
+ - `_launcher.py`: Added `--shm-name/--shm-shape/--shm-dtype/--name` CLI args for stdio mode
128
+ - `extension.js`: `PythonBridge` accepts `pythonPath` and `shmParams`
129
+ - `extension.js`: `processSignalData` handles `data.shm` for shared memory mode
130
+ - `extension.js`: Debug logging (`DISPATCH:`, `SIGNAL-DATA:`)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.7.0"
7
+ version = "0.9.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"
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # ---------------------------------------------------------------------------
5
+ # release.sh — bump version, commit, tag, push, create GitHub release
6
+ # ---------------------------------------------------------------------------
7
+
8
+ BUMP="minor"
9
+ DRY_RUN=true
10
+
11
+ usage() {
12
+ cat <<EOF
13
+ Usage: $(basename "$0") [OPTIONS]
14
+
15
+ Options:
16
+ --bump {major,minor,patch} Version bump type (default: minor)
17
+ --execute Actually run (default is dry-run)
18
+ -h, --help Show this help
19
+ EOF
20
+ }
21
+
22
+ while [[ $# -gt 0 ]]; do
23
+ case "$1" in
24
+ --bump) BUMP="$2"; shift 2 ;;
25
+ --execute) DRY_RUN=false; shift ;;
26
+ -h|--help) usage; exit 0 ;;
27
+ *) echo "Unknown option: $1"; usage; exit 1 ;;
28
+ esac
29
+ done
30
+
31
+ if [[ ! "$BUMP" =~ ^(major|minor|patch)$ ]]; then
32
+ echo "Error: --bump must be major, minor, or patch (got '$BUMP')"
33
+ exit 1
34
+ fi
35
+
36
+ # --- Guard: clean working tree on main ---
37
+ branch=$(git rev-parse --abbrev-ref HEAD)
38
+ if [[ "$branch" != "main" ]]; then
39
+ echo "Error: must be on main (currently on '$branch')"
40
+ exit 1
41
+ fi
42
+
43
+ if ! git diff --quiet || ! git diff --cached --quiet; then
44
+ echo "Error: working tree is dirty — commit or stash first"
45
+ exit 1
46
+ fi
47
+
48
+ # --- Bump version ---
49
+ uv version --bump "$BUMP"
50
+ VERSION=$(uv version --short)
51
+ TAG="v${VERSION}"
52
+
53
+ echo "Version bumped to $VERSION (tag: $TAG)"
54
+
55
+ run() {
56
+ echo "+ $*"
57
+ if [[ "$DRY_RUN" == true ]]; then
58
+ return
59
+ fi
60
+ "$@"
61
+ }
62
+
63
+ # --- Commit, tag, push, release ---
64
+ run git add pyproject.toml
65
+ run git commit -m "release: $TAG"
66
+ run git push origin main
67
+ run git tag "$TAG"
68
+ run git push origin "$TAG"
69
+ run gh release create "$TAG" \
70
+ --title "[Pre-release] $TAG" \
71
+ --generate-notes \
72
+ --prerelease
73
+
74
+ if [[ "$DRY_RUN" == true ]]; then
75
+ echo ""
76
+ echo "(dry-run — re-run with --execute to apply)"
77
+ git checkout pyproject.toml
78
+ fi