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.
- {arrayview-0.7.0 → arrayview-0.9.0}/.claude/skills/ui-consistency-audit/SKILL.md +32 -0
- arrayview-0.9.0/.claude/skills/visual-bug-fixing/SKILL.md +153 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/.gitignore +5 -1
- arrayview-0.9.0/AGENTS.md +40 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/PKG-INFO +18 -2
- {arrayview-0.7.0 → arrayview-0.9.0}/README.md +17 -1
- {arrayview-0.7.0 → arrayview-0.9.0}/docs/remote.md +26 -5
- arrayview-0.9.0/plans/webview/LOG.md +130 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/pyproject.toml +1 -1
- arrayview-0.9.0/scripts/release.sh +78 -0
- arrayview-0.9.0/src/arrayview/ARCHITECTURE.md +197 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/__init__.py +1 -1
- arrayview-0.9.0/src/arrayview/__main__.py +4 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_app.py +1 -2
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_config.py +17 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_io.py +87 -4
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_launcher.py +332 -28
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_render.py +9 -25
- arrayview-0.9.0/src/arrayview/_segmentation.py +227 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_server.py +954 -48
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_session.py +13 -84
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_shell.html +8 -1
- arrayview-0.9.0/src/arrayview/_stdio_server.py +791 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_viewer.html +2682 -1620
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_vscode.py +247 -75
- arrayview-0.9.0/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_api.py +59 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_browser.py +7 -2
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_cli.py +3 -3
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_interactions.py +23 -23
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/ui_audit.py +118 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/visual_smoke.py +29 -2
- {arrayview-0.7.0 → arrayview-0.9.0}/uv.lock +1 -1
- arrayview-0.9.0/vscode-extension/extension.js +895 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/vscode-extension/package.json +9 -2
- arrayview-0.7.0/.claude/skills/task-workflow/SKILL.md +0 -82
- arrayview-0.7.0/AGENTS.md +0 -142
- arrayview-0.7.0/docs/superpowers/plans/2026-03-27-colorbar-class-refactor.md +0 -1642
- arrayview-0.7.0/docs/superpowers/plans/2026-03-27-mode-dispatch-refactor.md +0 -717
- arrayview-0.7.0/docs/superpowers/plans/2026-03-28-exploded-volume-view.md +0 -854
- arrayview-0.7.0/docs/superpowers/plans/2026-03-28-instance-previewer-ram-guard.md +0 -601
- arrayview-0.7.0/docs/superpowers/plans/2026-03-29-multi-array-immersive.md +0 -726
- arrayview-0.7.0/docs/superpowers/plans/2026-03-30-loading-screen.md +0 -192
- arrayview-0.7.0/docs/superpowers/plans/2026-03-30-pytorch-dl-integration.md +0 -807
- arrayview-0.7.0/docs/superpowers/specs/2026-03-27-islands-immersive-only-design.md +0 -60
- arrayview-0.7.0/docs/superpowers/specs/2026-03-28-exploded-volume-view-design.md +0 -94
- arrayview-0.7.0/docs/superpowers/specs/2026-03-28-instance-previewer-ram-guard-design.md +0 -158
- arrayview-0.7.0/docs/superpowers/specs/2026-03-29-multi-array-immersive-design.md +0 -84
- arrayview-0.7.0/docs/superpowers/specs/2026-03-30-loading-screen-design.md +0 -33
- arrayview-0.7.0/docs/superpowers/specs/2026-03-30-pytorch-dl-integration-design.md +0 -205
- arrayview-0.7.0/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.7.0/vscode-extension/extension.js +0 -415
- {arrayview-0.7.0 → arrayview-0.9.0}/.claude/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/.python-version +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/.tmp-vsix/extension/extension.js +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/.tmp-vsix/extension/package.json +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/LICENSE +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/docs/comparing.md +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/docs/configuration.md +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/docs/display.md +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/docs/index.md +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/docs/loading.md +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/docs/logo.png +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/docs/measurement.md +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/docs/viewing.md +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/matlab/arrayview.m +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/mkdocs.yml +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/scripts/demo.py +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/conftest.py +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_config.py +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.7.0 → arrayview-0.9.0}/tests/test_torch.py +0 -0
- {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).
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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:`)
|
|
@@ -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
|