arrayview 0.4.0__tar.gz → 0.6.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 (73) hide show
  1. arrayview-0.6.0/.claude/skills/invocation-consistency/SKILL.md +118 -0
  2. {arrayview-0.4.0 → arrayview-0.6.0}/.claude/skills/task-workflow/SKILL.md +7 -3
  3. arrayview-0.6.0/.claude/skills/ui-consistency-audit/SKILL.md +336 -0
  4. {arrayview-0.4.0 → arrayview-0.6.0}/.github/workflows/python-publish.yml +4 -4
  5. {arrayview-0.4.0 → arrayview-0.6.0}/.gitignore +6 -3
  6. arrayview-0.6.0/AGENTS.md +137 -0
  7. arrayview-0.6.0/PKG-INFO +68 -0
  8. arrayview-0.6.0/README.md +33 -0
  9. arrayview-0.6.0/docs/comparing.md +44 -0
  10. arrayview-0.6.0/docs/configuration.md +40 -0
  11. arrayview-0.6.0/docs/display.md +31 -0
  12. arrayview-0.6.0/docs/index.md +37 -0
  13. arrayview-0.6.0/docs/loading.md +108 -0
  14. arrayview-0.6.0/docs/measurement.md +34 -0
  15. arrayview-0.6.0/docs/remote.md +71 -0
  16. arrayview-0.6.0/docs/stylesheets/extra.css +25 -0
  17. arrayview-0.6.0/docs/superpowers/plans/2026-03-27-colorbar-class-refactor.md +1642 -0
  18. arrayview-0.6.0/docs/viewing.md +36 -0
  19. arrayview-0.6.0/mkdocs.yml +37 -0
  20. {arrayview-0.4.0 → arrayview-0.6.0}/pyproject.toml +6 -2
  21. arrayview-0.6.0/src/arrayview/__init__.py +4 -0
  22. {arrayview-0.4.0 → arrayview-0.6.0}/src/arrayview/_app.py +4 -0
  23. arrayview-0.6.0/src/arrayview/_config.py +92 -0
  24. {arrayview-0.4.0 → arrayview-0.6.0}/src/arrayview/_io.py +38 -14
  25. {arrayview-0.4.0 → arrayview-0.6.0}/src/arrayview/_launcher.py +822 -181
  26. {arrayview-0.4.0 → arrayview-0.6.0}/src/arrayview/_platform.py +99 -6
  27. {arrayview-0.4.0 → arrayview-0.6.0}/src/arrayview/_render.py +122 -2
  28. {arrayview-0.4.0 → arrayview-0.6.0}/src/arrayview/_server.py +711 -58
  29. {arrayview-0.4.0 → arrayview-0.6.0}/src/arrayview/_session.py +47 -4
  30. arrayview-0.6.0/src/arrayview/_viewer.html +11991 -0
  31. {arrayview-0.4.0 → arrayview-0.6.0}/src/arrayview/_vscode.py +291 -47
  32. arrayview-0.6.0/src/arrayview/arrayview-opener.vsix +0 -0
  33. arrayview-0.6.0/tests/test_api.py +1399 -0
  34. {arrayview-0.4.0 → arrayview-0.6.0}/tests/test_browser.py +272 -41
  35. arrayview-0.6.0/tests/test_cli.py +172 -0
  36. arrayview-0.6.0/tests/test_config.py +146 -0
  37. arrayview-0.6.0/tests/test_interactions.py +1850 -0
  38. arrayview-0.6.0/tests/test_mode_consistency.py +560 -0
  39. arrayview-0.6.0/tests/test_mode_matrix.py +585 -0
  40. arrayview-0.6.0/tests/test_rgb_pixel_art.py +66 -0
  41. arrayview-0.6.0/tests/ui_audit.py +1179 -0
  42. arrayview-0.6.0/tests/visual_smoke.py +1784 -0
  43. {arrayview-0.4.0 → arrayview-0.6.0}/uv.lock +273 -1
  44. {arrayview-0.4.0 → arrayview-0.6.0}/vscode-extension/extension.js +176 -29
  45. {arrayview-0.4.0 → arrayview-0.6.0}/vscode-extension/package.json +1 -1
  46. arrayview-0.4.0/.claude/skills/invocation-consistency/SKILL.md +0 -152
  47. arrayview-0.4.0/AGENTS.md +0 -186
  48. arrayview-0.4.0/PKG-INFO +0 -401
  49. arrayview-0.4.0/README.md +0 -366
  50. arrayview-0.4.0/TODO.md +0 -2
  51. arrayview-0.4.0/docs/large-arrays.md +0 -197
  52. arrayview-0.4.0/docs/superpowers/specs/2026-03-14-todo-batch-design.md +0 -77
  53. arrayview-0.4.0/plans/tunnel-fix/LOG.md +0 -284
  54. arrayview-0.4.0/plans/tunnel-fix/PLAN.md +0 -148
  55. arrayview-0.4.0/src/arrayview/__init__.py +0 -4
  56. arrayview-0.4.0/src/arrayview/_viewer.html +0 -5094
  57. arrayview-0.4.0/src/arrayview/arrayview-opener.vsix +0 -0
  58. arrayview-0.4.0/tests/test_api.py +0 -608
  59. arrayview-0.4.0/tests/test_cli.py +0 -103
  60. arrayview-0.4.0/tests/visual_smoke.py +0 -854
  61. {arrayview-0.4.0 → arrayview-0.6.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
  62. {arrayview-0.4.0 → arrayview-0.6.0}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
  63. {arrayview-0.4.0 → arrayview-0.6.0}/.python-version +0 -0
  64. {arrayview-0.4.0 → arrayview-0.6.0}/.tmp-vsix/extension/extension.js +0 -0
  65. {arrayview-0.4.0 → arrayview-0.6.0}/.tmp-vsix/extension/package.json +0 -0
  66. {arrayview-0.4.0 → arrayview-0.6.0}/LICENSE +0 -0
  67. {arrayview-0.4.0 → arrayview-0.6.0}/scripts/demo.py +0 -0
  68. {arrayview-0.4.0 → arrayview-0.6.0}/src/arrayview/_icon.png +0 -0
  69. {arrayview-0.4.0 → arrayview-0.6.0}/src/arrayview/_shell.html +0 -0
  70. {arrayview-0.4.0 → arrayview-0.6.0}/tests/conftest.py +0 -0
  71. {arrayview-0.4.0 → arrayview-0.6.0}/tests/make_vectorfield_test_arrays.py +0 -0
  72. {arrayview-0.4.0 → arrayview-0.6.0}/tests/test_large_arrays.py +0 -0
  73. {arrayview-0.4.0 → arrayview-0.6.0}/vscode-extension/LICENSE +0 -0
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: invocation-consistency
3
+ description: Use when implementing any server-side, startup, display-opening, or environment-detection feature in arrayview. Ensures the feature works correctly across all six ways arrayview can be launched — CLI, Python script, Jupyter, Julia, VS Code tunnel, and SSH.
4
+ ---
5
+
6
+ # ArrayView Invocation Consistency Checklist
7
+
8
+ ## Rule
9
+
10
+ Every behavior that depends on *how* arrayview is started (server lifecycle, browser opening, display routing, port forwarding) must be verified across all six invocation paths before it is considered done.
11
+
12
+ ## The Six Invocation Paths
13
+
14
+ | Path | Entry point | Display | Server model |
15
+ |------|------------|---------|-------------|
16
+ | **CLI** | `arrayview()` | native window → browser fallback | background thread, blocks until Ctrl-C |
17
+ | **Python script** | `view(arr)` | native window → browser fallback | daemon thread (dies with caller) |
18
+ | **Jupyter** | `view(arr)` | inline IFrame | daemon thread (persists across cells) |
19
+ | **Julia (PythonCall)** | `view(arr)` | system browser | always subprocess (`_view_julia()`) — GIL |
20
+ | **VS Code terminal** | `arrayview()` or `view()` | VS Code Simple Browser | in-process or subprocess |
21
+ | **Plain SSH (no VS Code)** | `arrayview()` or `view()` | prints port-forward hint | in-process or subprocess |
22
+
23
+ ## Key Detection Functions (all in `_platform.py`)
24
+
25
+ ```python
26
+ _in_jupyter() # ipykernel present → display inline IFrame
27
+ _in_vscode_terminal() # TERM_PROGRAM=vscode OR VSCODE_IPC_HOOK_CLI → use Simple Browser
28
+ _is_vscode_remote() # tunnel/SSH remote with VS Code server → shared signal fallback
29
+ _is_julia_env() # juliacall in sys.modules or julia in sys.executable → subprocess
30
+ _can_native_window() # pywebview available + not remote + display present → native window
31
+ _find_vscode_ipc_hook() # walk process tree for VSCODE_IPC_HOOK_CLI (stripped by uv run)
32
+ ```
33
+
34
+ ## Detection Order in `view()`
35
+
36
+ Julia → Jupyter → VS Code remote → VS Code terminal → local (native/browser)
37
+
38
+ Any new detection must not break the fallback chain for paths it shouldn't match.
39
+
40
+ ## Julia-Specific Constraints
41
+
42
+ - Never run server in-process when `_is_julia_env()` is True (GIL conflicts)
43
+ - `_view_julia()` starts a detached subprocess; array is serialized to a temp `.npy` file
44
+ - No interactive stdin in subprocesses; all params encoded in CLI flags or files
45
+
46
+ ## Jupyter-Specific Constraints
47
+
48
+ - `_in_jupyter()` returns True for ipykernel (VS Code notebook, JupyterLab, classic Notebook)
49
+ - Julia's IJulia kernel is NOT ipykernel → `_in_jupyter()` returns False in Julia notebooks
50
+ - `inline=True` → returns `IPython.display.IFrame`; caller must return it from the cell
51
+ - Port reuse: repeated `view()` calls reuse same port/server if already running
52
+
53
+ ## Port & URL Rules
54
+
55
+ - Always use `localhost` (not `127.0.0.1`) so VS Code port-forwarding works
56
+ - Default port: `8123`; CLI: `--port` flag; `view()`: `port=` kwarg
57
+
58
+ ## tmux and VS Code Terminal Detection
59
+
60
+ tmux breaks the ancestor-walk for `VSCODE_IPC_HOOK_CLI` because the process's parent chain goes through `tmux-server` (an independent daemon with no VS Code env), not through the VS Code terminal shell.
61
+
62
+ **Why `tmux show-environment` fails:** `VSCODE_IPC_HOOK_CLI` is not in tmux's default `update-environment` list.
63
+
64
+ **Why `#{client_pid}` alone fails:** returns only one client; breaks with multiple attached clients.
65
+
66
+ **Correct approach — enumerate all clients for the current session:**
67
+
68
+ ```python
69
+ session_id = subprocess.run(["tmux", "display-message", "-p", "#{session_id}"], ...).stdout.strip()
70
+ client_pids = subprocess.run(
71
+ ["tmux", "list-clients", "-t", session_id, "-F", "#{client_pid}"], ...
72
+ ).stdout.strip().splitlines()
73
+ for pid_str in client_pids:
74
+ val = _ipc_from_pid(int(pid_str))
75
+ if val and os.path.exists(val):
76
+ return val
77
+ ```
78
+
79
+ `list-clients -t <session_id>` scopes to current session only — prevents false-positive from a different VS Code window in another tmux session.
80
+
81
+ ## Step-by-Step Checklist
82
+
83
+ 1. **Server startup/teardown?**
84
+ - CLI: starts and terminates cleanly
85
+ - Python script: daemon thread doesn't orphan
86
+ - Jupyter: repeated calls don't fail on port-already-in-use
87
+ - Julia: `_view_julia()` passes new params via CLI flag or file
88
+
89
+ 2. **Browser/display opening?**
90
+ - Local native window (macOS): `pywebview` opens correctly
91
+ - VS Code terminal: Simple Browser opens via extension signal file
92
+ - VS Code tunnel: reaches client-side Simple Browser, not remote host
93
+ - Verify `_ensure_vscode_extension()` installs/updates if VSIX changed
94
+
95
+ 3. **Environment detection?**
96
+ - Check detection order: Julia → Jupyter → VS Code remote → VS Code terminal → local
97
+ - `_find_vscode_ipc_hook()` walks up to 12 parent processes; new subprocess wrappers may need the same
98
+
99
+ 4. **VS Code extension changed?**
100
+ - Rebuild VSIX: `cd vscode-extension && vsce package -o ../src/arrayview/arrayview-opener.vsix`
101
+ - Bump `_VSCODE_EXT_VERSION` in `_vscode.py` and `vscode-extension/package.json` together
102
+
103
+ ## Quick Automated Checks
104
+
105
+ ```bash
106
+ uv run pytest tests/test_api.py -x
107
+ uv run pytest tests/test_cli.py -x
108
+ uv run python -c "from arrayview import view; import numpy as np; view(np.zeros((10,10)))"
109
+ ```
110
+
111
+ ## Red Flags — STOP
112
+
113
+ - "I changed server startup but only tested CLI" → test all paths
114
+ - "Works locally but not in tunnel" → check `_is_vscode_remote()` path and port exposure
115
+ - "I used `127.0.0.1` in the URL" → use `localhost`
116
+ - "I fixed tmux with `#{client_pid}`" → only works with one client; use `list-clients`
117
+ - "I bumped `package.json` but not `_VSCODE_EXT_VERSION`" → must stay in sync
118
+ - "Server starts but leaves an orphan after Ctrl-C" → check `_shutdown_event` and subprocess reaping
@@ -43,9 +43,13 @@ For each TODO item, ensure the following before marking the task done:
43
43
  - [ ] README or in-app help overlay (`#help-overlay` content in `src/arrayview/_viewer.html`) updated if usage changed.
44
44
  - [ ] `CHANGELOG.md` or `AGENTS.md` updated with a one-line summary.
45
45
  - [ ] Commit message follows the format above and lists affected files.
46
- - [ ] Run the core checks locally:
47
- - `uv run pytest tests/test_api.py -q`
48
- - `uv run python tests/visual_smoke.py` (and review `tests/smoke_output/`)
46
+ - [ ] **Tests MUST be run by the agent NEVER ask the user to run them.**
47
+ - After every code change, run the relevant test suite immediately:
48
+ - Backend/API changes → `uv run pytest tests/test_api.py -q`
49
+ - Viewer UI changes → `uv run pytest tests/test_interactions.py tests/test_mode_consistency.py -q`
50
+ - CLI changes → `uv run pytest tests/test_cli.py -q`
51
+ - If any test fails, fix it before considering the task done.
52
+ - Do NOT end a task with "run this to verify" — run it yourself and report results.
49
53
 
50
54
  ## How to use
51
55
 
@@ -0,0 +1,336 @@
1
+ ---
2
+ name: ui-consistency-audit
3
+ description: Use when implementing or modifying any visual feature, layout, keyboard shortcut, or mode-specific behavior in arrayview. Proactively identifies all affected mode combinations before coding, prescribes per-mode behavior, checks for UI clashes, and runs a Playwright-based visual audit after implementation. Absorbs and replaces the modes-consistency skill.
4
+ ---
5
+
6
+ # ArrayView UI Consistency Audit
7
+
8
+ ## Rule
9
+
10
+ Every visual change must be verified across **all applicable mode × array-count combinations** before it ships. This skill has two phases: a **proactive phase** (before coding) that plans the cross-mode behavior, and a **reactive phase** (after coding) that runs an automated visual audit.
11
+
12
+ ---
13
+
14
+ ## Phase 1: Proactive (Before Coding)
15
+
16
+ ### Step 1 — Classify the Change
17
+
18
+ What area of the UI is being touched?
19
+
20
+ | Category | Examples |
21
+ |----------|---------|
22
+ | **Canvas rendering / sizing / zoom** | scaleCanvas, zoom cap, compact mode, miniview |
23
+ | **Chrome element** | colorbar, dimbar, eggs, array name, logo, miniview, colormap previewer |
24
+ | **Keyboard shortcut** | new or modified shortcut |
25
+ | **Mode entry/exit** | toggling multiview, qMRI, compare, diff, ROI, zen, compact |
26
+ | **Overlay feature** | ROI, vector field, histogram/Lebesgue, hover tooltip |
27
+ | **Multi-array layout** | compare pane alignment, diff center pane, grid layout |
28
+
29
+ ### Step 2 — Enumerate Affected Modes
30
+
31
+ Use the code-area-to-mode mapping (Section 7) to list every mode combination that could be affected. Do not skip modes — if in doubt, include them.
32
+
33
+ ### Step 3 — Prescribe Behavior Per Mode
34
+
35
+ For **each** affected mode, explicitly state:
36
+
37
+ 1. How the feature behaves in this mode
38
+ 2. Whether it's the same as normal mode or different (and why)
39
+ 3. Which UI elements it could clash with (use the design rules in Section 5)
40
+ 4. What happens at fit-to-window zoom AND zoomed-in (compact/miniview) zoom
41
+
42
+ ### Step 4 — Check Against Design Rules
43
+
44
+ Walk through the principles (Section 5a) and specific rules (Section 5b). Flag any potential violations.
45
+
46
+ ### Step 5 — Get Approval
47
+
48
+ Present the mode-by-mode behavior plan to the user before implementing.
49
+
50
+ ---
51
+
52
+ ## Phase 2: Reactive (After Coding)
53
+
54
+ After implementation, run the visual audit:
55
+
56
+ ```bash
57
+ # Tier 1 only (quick, ~15 screenshots × 2 zoom levels)
58
+ uv run python tests/ui_audit.py --tier 1
59
+
60
+ # Full audit (tier 1 + tier 2, ~40 screenshots × 2 zoom levels)
61
+ uv run python tests/ui_audit.py
62
+
63
+ # Specific subset (when you know which modes are affected)
64
+ uv run python tests/ui_audit.py --tier 2 --subset zoom,compact,compare
65
+
66
+ # Update baselines after intentional changes
67
+ uv run python tests/ui_audit.py --update-baselines
68
+ ```
69
+
70
+ **Output:** Terminal summary (pass/fail per combination) + screenshots in `tests/ui_audit/screenshots/`. Visual diffs in `tests/ui_audit/diffs/` when baselines exist.
71
+
72
+ ### Step 3 — Visual Design Inspection (MANDATORY)
73
+
74
+ DOM assertions catch mechanical violations. They **cannot** catch:
75
+ - Bad layout decisions (elements in wrong position, wasted screen space)
76
+ - Overlapping elements not covered by a specific rule
77
+ - Aesthetically wrong choices (miniview floating in a weird spot, panes misaligned)
78
+ - Features that technically work but look broken to a human
79
+
80
+ After the audit script runs, you MUST:
81
+
82
+ 1. **Read every screenshot** from the affected tier using the Read tool (it renders PNGs visually)
83
+ 2. **For each screenshot, evaluate as a UI designer:**
84
+ - Are elements logically positioned? Would a user know where to look?
85
+ - Is screen real estate maximized? Are canvases as large as they can be?
86
+ - Do overlapping or adjacent elements clash? (colorbars, minimaps, eggs, labels)
87
+ - Is the layout consistent with the same feature in other modes?
88
+ - Would this look good on a 13" laptop screen?
89
+ 3. **Report findings** — list every issue found, with the screenshot name and a concrete description
90
+ 4. **Do NOT suppress assertions to make the audit pass.** If an assertion fails, the UI has a bug. Fix the UI, not the assertion. If the assertion is genuinely wrong (testing the wrong thing), fix the assertion AND explain why.
91
+
92
+ **Red flags for this step:**
93
+ - "The assertion failed so I marked it as zoomed=True" → NO. That's suppressing a real bug.
94
+ - "All assertions passed so the UI is fine" → NO. Assertions are a minimum bar, not a complete check.
95
+ - "I didn't look at the screenshots because the report said PASS" → NO. Always look.
96
+
97
+ **This is the verification step.** The `superpowers:verification-before-completion` skill requires running this before claiming work is done.
98
+
99
+ ---
100
+
101
+ ## Section 3: Mode Matrix & Tiers
102
+
103
+ ### Tier 1 — Always Check
104
+
105
+ | Array config | Mode | Screenshot name |
106
+ |---|---|---|
107
+ | 1× 3D | normal (fit) | `t1_single_normal_fit` |
108
+ | 1× 3D | normal (zoomed) | `t1_single_normal_zoom` |
109
+ | 1× 3D | compact | `t1_single_compact` |
110
+ | 1× 3D | multiview | `t1_single_multiview_fit` |
111
+ | 1× 3D | multiview (zoomed) | `t1_single_multiview_zoom` |
112
+ | 1× 4D (dim0=5) | qMRI full | `t1_single_qmri_full` |
113
+ | 1× 4D (dim0=5) | qMRI compact | `t1_single_qmri_compact` |
114
+ | 1× 3D | zen | `t1_single_zen` |
115
+ | 2× 3D | compare | `t1_compare_fit` |
116
+ | 2× 3D | compare (zoomed) | `t1_compare_zoom` |
117
+ | 2× 3D | diff (A-B) | `t1_compare_diff` |
118
+ | 2× 3D | compare + multiview | `t1_compare_multiview` |
119
+ | 1× 3D | normal + ROI | `t1_single_roi` |
120
+ | 1× 3D | compact + ROI | `t1_compact_roi` |
121
+
122
+ ### Tier 2 — Check When Relevant
123
+
124
+ | Array config | Mode | Trigger keywords |
125
+ |---|---|---|
126
+ | 1× 2D | normal | `2d, basic` |
127
+ | 1× 4D | normal | `4d, ndim` |
128
+ | 1× 4D | mosaic (z) | `mosaic, z-grid` |
129
+ | 1× complex | normal | `complex, dtype` |
130
+ | 2× 3D | diff |A-B| | `diff` |
131
+ | 2× 3D | diff |A-B|/|A| | `diff` |
132
+ | 2× 3D | overlay | `overlay, wipe, diff` |
133
+ | 2× 3D | wipe | `wipe, diff` |
134
+ | 2× 3D | registration | `registration` |
135
+ | 2× 4D | compare + qMRI | `qmri, compare` |
136
+ | 2× 3D | diff + ROI | `roi, diff` |
137
+ | 3× 3D | compare grid | `compare, grid, multi-array` |
138
+ | 3× 4D | compare + qMRI | `qmri, compare, multi-array` |
139
+ | 3× 3D | compare + multiview | `multiview, compare, multi-array` |
140
+ | 4× 3D | compare grid | `compare, grid, multi-array` |
141
+ | 3-4× 4D | compare + compact qMRI | `qmri, compare, compact, multi-array` |
142
+ | 1× 3D + vfield | vector field normal | `vector, vfield` |
143
+ | 1× 3D + vfield | vector field + ROI | `vector, roi` |
144
+ | 2× 3D (1+vfield) | compare with vector field | `vector, compare` |
145
+
146
+ ---
147
+
148
+ ## Section 4: The Six+ Modes (Reference)
149
+
150
+ | Mode | State flag | Scale function | Entry | Canvas elements |
151
+ |------|-----------|---------------|-------|-----------------|
152
+ | **Normal** | (default) | `scaleCanvas()` | — | `#canvas` |
153
+ | **Compact** | compact mode active | `scaleCanvas()` | K / auto on zoom | `#canvas` + miniview |
154
+ | **Multi-view** | `multiViewActive` | `mvScaleAllCanvases()` | v | `.mv-canvas` × 3 |
155
+ | **Compare** | `compareActive` | `compareScaleCanvases()` | B / P | `.compare-canvas` × 2–6 |
156
+ | **Diff** | `diffMode > 0` | `compareScaleCanvases()` | X (in compare) | `#compare-diff-canvas` |
157
+ | **Registration** | `registrationMode` | `compareScaleCanvases()` | R (in compare) | 3rd `.compare-canvas` |
158
+ | **qMRI** | `qmriActive` | `qvScaleAllCanvases()` | q | `.qv-canvas` × 3–6 |
159
+ | **Compact qMRI** | qMRI compact toggle | `qvScaleAllCanvases()` | q (second press) | `.qv-canvas` × 3 (T1,T2,|PD|) |
160
+ | **Zen** | zen mode | — | F | fullscreen, no chrome |
161
+ | **Vector field** | vectorfield attached | — | U toggle arrows | arrows on canvas |
162
+
163
+ **Orthogonal overlays** (can be active in any mode):
164
+ - **ROI** (A key): rect → circle → freehand → off
165
+ - **Histogram/Lebesgue** (w key): expanded colorbar with distribution
166
+ - **Hover tooltip** (H key): pixel value follows cursor
167
+ - **Overlay mask** (`overlay_sid` URL param): composited server-side
168
+
169
+ ---
170
+
171
+ ## Section 5a: Design Principles
172
+
173
+ 1. **No overlapping chrome** — UI elements must never visually overlap each other or the canvas. If two elements compete for the same space, one must hide (e.g. colorbar hides when histogram is open).
174
+
175
+ 2. **Consistent positioning** — an element appearing in mode A should appear in the same logical position in mode B. The miniview overlay and the mini 3D pane previewer must share a position (top-right).
176
+
177
+ 3. **Zoom never clips** — canvases must stay within viewport bounds at all zoom levels. In multi-canvas modes, canvases grow until constrained, then zoom together with a single shared miniview (top-right, showing one representative slice).
178
+
179
+ 4. **Maximize canvas real estate** — chrome is minimal and collapses when not needed. On a laptop screen, the array data is always the primary focus. No persistent toolbars or buttons.
180
+
181
+ 5. **Mode transitions are reversible** — toggling a mode on then off returns to the exact prior state. No leftover artifacts, no shifted elements, no orphaned DOM.
182
+
183
+ 6. **Orthogonal features compose cleanly** — ROI, vector field, histogram, colorbar, hover tooltip work independently. Enabling one must not break or interfere with another. ROI mode should be possible in any mode combination.
184
+
185
+ 7. **Multi-array consistency** — when multiple arrays are loaded, all canvases zoom/scroll/navigate together. Colormaps on side panes match; diff center pane uses an independent colormap and dynamic range.
186
+
187
+ 8. **Compare panes stay close** — pane slots shrink-wrap their canvas viewport when zoomed out so that panes remain adjacent for easy visual comparison. Never use fixed pane widths that create large gaps between canvases.
188
+
189
+ ---
190
+
191
+ ## Section 5b: Specific Rules (Growing Catalog)
192
+
193
+ These rules are codified as DOM assertions in `tests/ui_audit.py`. When a new inconsistency is found and fixed, add a rule here AND a corresponding assertion in the audit script.
194
+
195
+ | # | Rule | DOM assertion |
196
+ |---|------|---------------|
197
+ | R1 | Colorbar hides when histogram/Lebesgue is open | `#slim-cb-wrap` has `display:none` or `opacity:0` when Lebesgue mode active |
198
+ | R2 | Miniview position matches mini 3D pane previewer | Miniview overlay bounding box is anchored top-right, same position as multiview pane previewer |
199
+ | R3 | All canvases within viewport | Every `.compare-canvas`, `.mv-canvas`, `.qv-canvas`, `#canvas` bounding box fits within `window.innerWidth × innerHeight` |
200
+ | R4 | Diff colormaps: sides same, center different | Colormap name on diff center pane differs from side panes |
201
+ | R5 | Diff dynamic range is independent | vmin/vmax on diff center canvas differs from side panes |
202
+ | R6 | Eggs don't overlap colorbar | Bounding boxes of `#mode-eggs` children don't intersect `#slim-cb-wrap` |
203
+ | R7 | Fullscreen mode overlays chrome on canvas | `#info`, `#slim-cb-wrap` have `position:fixed` and glassmorphic styling when `body.fullscreen-mode` is active |
204
+ | R8 | ROI hover info within viewport | `.cv-pixel-info` bounding box is within viewport |
205
+ | R9 | No element jump on mode toggle | Pressing a mode toggle key then pressing it again returns bounding boxes to ±2px of original |
206
+ | R10 | Compare panes aligned | All `.compare-canvas` elements share the same `y` coordinate (horizontal layout) or same `x` (vertical layout) |
207
+ | R11 | Vector field arrows hidden when U toggled off | No arrow SVG/canvas elements visible after U press |
208
+ | R12 | Histogram not visible when colorbar is in slim mode | Lebesgue expanded state is mutually exclusive with slim colorbar |
209
+ | R13 | Colorbars don't overlap each other | No pair of colorbar elements (slim, compare-pane, mv, qv) have intersecting bounding boxes |
210
+ | R14 | Minimap within viewport and not overlapping colorbars | `#minimap-canvas` bounding box is within viewport and doesn't intersect any colorbar |
211
+ | R15 | Colorbar labels flank horizontally in all modes | Every `.cb-island`, `.compare-pane-cb-island`, `.qv-cb-island` uses `flex-direction: row` with vmin/vmax spans flanking the gradient canvas — never column layout with labels below |
212
+ | R16 | Per-pane colorbar width matches canvas viewport width (diff mode only) | Each `.compare-pane-cb` and `.qv-cb` canvas CSS width equals its data canvas viewport width. Per-pane colorbars only visible in diff mode. |
213
+ | R17 | Fullscreen mode eggs are visible over image | `#mode-eggs .mode-badge` elements have sufficient background opacity (>= 0.5) when `body.fullscreen-mode` is active, since they overlay the canvas |
214
+ | R18 | Histogram has visible background distinct from page | When `_cbExpanded`, the histogram area has an explicit background fill matching the dynamic island aesthetic, not transparent over the page background |
215
+ | R19 | Shared colorbar visible in all non-diff compare modes | `#slim-cb-wrap` is visible when `compareActive && !diffMode`. Per-pane colorbars hidden. |
216
+ | R20 | Compare panes shrink-wrap when zoomed out | Pane slot width ≤ viewport canvas width + padding, not fixed to max available. Panes stay close for visual comparison |
217
+ | R21 | Per-pane colorbars visible only in diff mode | `.compare-pane-cb-island` elements visible only when `diffMode > 0`. Three colorbars (left, center, right) at same vertical position. |
218
+ | R22 | Array names include logo in compare mode | Each `.compare-title` contains SVG logo + name text, styled like `#array-name` |
219
+ | R23 | Fullscreen mode overlays are glassmorphic islands | When `body.fullscreen-mode`, `#info` and `#slim-cb-wrap` have glassmorphic styling with `backdrop-filter: blur(12px)` |
220
+ | R24 | Diff c/d keys are mouse-aware | When `diffMode > 0`, `c` and `d` keys affect center pane when `_mouseOverCenterPane`, side panes otherwise |
221
+ | R25 | No duplicate colorbars in multiview | `#slim-cb-wrap` hidden when `multiViewActive` or `compareMvActive` (multiview has its own per-pane colorbars) |
222
+ | R26 | Shared colorbar width capped in compare mode | `#slim-cb-wrap` width ≤ 500px in compare mode |
223
+ | R27 | Mode eggs hidden during loading | `#mode-eggs` empty/hidden until at least one frame has been received |
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
+
226
+ ---
227
+
228
+ ## Section 5c: Colorbar Layout Invariant
229
+
230
+ All colorbars across ALL modes follow the same structure: `[vmin span] [gradient canvas] [vmax span]` in a horizontal flex row inside a `.cb-island` container. This applies to:
231
+ - Normal mode: `#slim-cb-wrap` with `#slim-cb-vmin`, `#slim-cb`, `#slim-cb-vmax`
232
+ - Compare/diff: `.compare-pane-cb-island` with `.compare-pane-cb-val` spans + `.compare-pane-cb` canvas
233
+ - qMRI: `.qv-cb-island` with `.qv-cb-val` spans + `.qv-cb` canvas
234
+
235
+ **Never** use column layout with labels below. **Never** use `flex: 1` on colorbar canvases inside inline-flex islands (causes sizing loops). Always set explicit island width = viewport canvas width, with `box-sizing: border-box` so `.cb-island` padding doesn't inflate the width beyond the canvas. Always test with non-square arrays — square arrays hide width mismatches.
236
+
237
+ **Colorbar-to-canvas gap**: The gap between canvas and colorbar must be visually consistent across modes. Both single and compare mode use the shared `#slim-cb-wrap` colorbar positioned via `chromeGap` (measured from `#info` bottom to canvas top). Per-pane colorbars (`.compare-pane-cb-island`) are only visible in diff mode.
238
+
239
+ **Compare mode colorbar**: A single shared `#slim-cb-wrap` replaces per-pane colorbars. All arrays share colormap and dynamic range (union of all vmins/vmaxes). Histogram aggregates bins from all sessions. Per-pane colorbars only appear in diff mode (3 colorbars: left, center, right).
240
+
241
+ ---
242
+
243
+ ## Section 6: Common Feature Categories
244
+
245
+ ### Zoom / Canvas Sizing
246
+
247
+ Four dedicated scale functions. When changing zoom behavior, check **all four**:
248
+
249
+ - `scaleCanvas(w, h)` — Normal/Compact
250
+ - `mvScaleAllCanvases()` — Multi-view
251
+ - `compareScaleCanvases()` — Compare / Diff / Registration
252
+ - `qvScaleAllCanvases()` — qMRI
253
+
254
+ ### Eggs (Mode Badges)
255
+
256
+ Positioned by `positionEggs()` which branches by mode:
257
+ - Normal: uses `#slim-cb-wrap` bounding box if visible, else canvas bottom
258
+ - Multi-view: uses `#mv-cb-wrap` or estimates 36px below panes
259
+ - Compare: walks `.compare-canvas-wrap` rects for tallest pane bottom
260
+ - qMRI: uses `.qv-canvas-wrap` rects + 36px
261
+
262
+ ### Colorbar
263
+
264
+ Separate per-mode drawing functions — no shared abstraction:
265
+
266
+ | Mode | Function |
267
+ |------|----------|
268
+ | Normal | `drawSlimColorbar(markerFrac)` |
269
+ | Compare | `drawComparePaneCb(idx)` + `drawAllComparePaneCbs()` |
270
+ | Diff | `drawDiffPaneCb(vmin, vmax)` |
271
+ | Registration | `drawRegBlendCb()` |
272
+ | Multi-view | `drawMvCbs()` |
273
+ | qMRI | Inline per view in `qvRender()` |
274
+
275
+ ### Keyboard Shortcuts
276
+
277
+ The `keydown` handler on `#keyboard-sink` dispatches by active mode. New shortcuts must:
278
+ 1. Not conflict with mode-specific keys
279
+ 2. Have explicit guards when mode-specific (e.g., `if (!compareActive) return;`)
280
+ 3. Fall through correctly when multiple modes are active
281
+
282
+ ---
283
+
284
+ ## Section 7: Code-Area-to-Mode Mapping
285
+
286
+ Use this to determine which tier 2 subsets to audit based on what code you're touching.
287
+
288
+ | Code area | Tier 2 subsets to include |
289
+ |-----------|--------------------------|
290
+ | `scaleCanvas` / zoom / `userZoom` | `zoom, compact` — all zoom-related modes |
291
+ | `compareScaleCanvases` / `.compare-` | `compare, diff, registration, grid, multi-array` |
292
+ | `mvScaleAllCanvases` / `.mv-` | `multiview` — single + compare multiview |
293
+ | `qvScaleAllCanvases` / `.qv-` / `qmri` | `qmri, compare` — single + compare qMRI, compact qMRI |
294
+ | `positionEggs` / `renderEggs` / `#mode-eggs` | all modes (eggs appear everywhere) |
295
+ | `drawSlimColorbar` / `#slim-cb` | `zoom, compact, diff` — colorbar visibility rules |
296
+ | Lebesgue / histogram / `w` key | `zoom, compact, diff` — mutual exclusivity with colorbar |
297
+ | ROI / `A` key / `.roi-` | `roi` — ROI across all modes |
298
+ | `diffMode` / `X` key / `#compare-diff` | `diff` — all diff variants |
299
+ | Vector field / `U` key / arrows | `vector` — normal, ROI, compare |
300
+ | `#minimap` / miniview | `zoom, compact, multiview` — position consistency |
301
+ | Overlay / `overlay_sid` | `overlay` — server-side compositing |
302
+ | Layout / `G` key / grid | `grid, multi-array` — 3-4 array layouts |
303
+
304
+ ---
305
+
306
+ ## Section 8: Test Arrays
307
+
308
+ The audit script creates these arrays to exercise different code paths:
309
+
310
+ | Array | Shape | Purpose |
311
+ |-------|-------|---------|
312
+ | `arr_3d` | (20, 64, 64) float32 | Primary test array, 3D navigation |
313
+ | `arr_3d_b` | (20, 64, 64) float32 | Second 3D for compare |
314
+ | `arr_2d` | (100, 80) float32 | 2D baseline |
315
+ | `arr_4d` | (5, 20, 32, 32) float32 | qMRI eligible (dim0=5) |
316
+ | `arr_4d_b` | (5, 20, 32, 32) float32 | Second 4D for compare qMRI |
317
+ | `arr_complex` | (20, 32, 32) complex64 | Complex dtype, triggers mag/phase/real/imag |
318
+ | `arr_3d_qmri3` | (3, 20, 32, 32) float32 | 3-panel qMRI (dim0=3) |
319
+ | `vf_3d` | (20, 64, 64, 3) float32 | Vector field (N+1 dims, last dim=3) |
320
+ | `arr_3d_c` | (20, 64, 64) float32 | Third array for 3-pane compare |
321
+ | `arr_3d_d` | (20, 64, 64) float32 | Fourth array for 4-pane grid |
322
+
323
+ ---
324
+
325
+ ## Red Flags — STOP
326
+
327
+ - "I only implemented it for normal mode" → enumerate all affected modes now
328
+ - "The colorbar is visible but the histogram is also showing" → R1 violation
329
+ - "It works but I didn't check zoomed-in" → run both zoom levels
330
+ - "ROI mode isn't relevant here" → ROI is always relevant, it's orthogonal
331
+ - "Compare mode doesn't need this" → if it affects the canvas or chrome, compare needs it
332
+ - "I'll update the audit script later" → update it in the same task as the rule
333
+ - "The screenshots look fine to me" → also check the DOM assertions passed
334
+ - "The assertion failed so I set zoomed=True to skip it" → that's suppressing a real bug, fix the UI
335
+ - "All assertions passed so no need to look at screenshots" → assertions are a minimum bar, always visually inspect
336
+ - "Vector field is separate" → still check it doesn't clash with the new feature
@@ -20,9 +20,9 @@ jobs:
20
20
  runs-on: ubuntu-latest
21
21
 
22
22
  steps:
23
- - uses: actions/checkout@v4
23
+ - uses: actions/checkout@v6
24
24
 
25
- - uses: actions/setup-python@v5
25
+ - uses: actions/setup-python@v6
26
26
  with:
27
27
  python-version: "3.x"
28
28
 
@@ -33,7 +33,7 @@ jobs:
33
33
  python -m build
34
34
 
35
35
  - name: Upload distributions
36
- uses: actions/upload-artifact@v4
36
+ uses: actions/upload-artifact@v7
37
37
  with:
38
38
  name: release-dists
39
39
  path: dist/
@@ -59,7 +59,7 @@ jobs:
59
59
 
60
60
  steps:
61
61
  - name: Retrieve release distributions
62
- uses: actions/download-artifact@v4
62
+ uses: actions/download-artifact@v8
63
63
  with:
64
64
  name: release-dists
65
65
  path: dist/
@@ -16,12 +16,15 @@ wheels/
16
16
  CLAUDE.md
17
17
  debug/
18
18
  tests/snapshots/
19
+ tests/ui_audit/
20
+ tests/smoke_output/
19
21
 
20
22
  *.jl
21
23
  *.ipynb
22
- logs/
24
+ dev/
23
25
  *.png
24
26
  !src/arrayview/_icon.png
25
- .worktrees/
26
27
 
27
- benchmarks/data
28
+ benchmarks/data
29
+ .superpowers/brainstorm
30
+ site/
@@ -0,0 +1,137 @@
1
+ # ArrayView Agent Guide
2
+
3
+ ## Mission
4
+
5
+ Interactive viewer for multi-dimensional arrays and medical/scientific volumes. Runs a FastAPI server + HTML/JS frontend displayed in a native window, browser, VS Code Simple Browser, or Jupyter inline iframe.
6
+
7
+ **Display routing defaults (sane defaults per environment):**
8
+
9
+ | Where you are | Default display |
10
+ |---|---|
11
+ | Jupyter kernel | inline iframe |
12
+ | VS Code terminal (local or tunnel) | VS Code Simple Browser tab |
13
+ | Julia | system browser |
14
+ | otherwise (CLI, Python script) | native pywebview window |
15
+
16
+ VS Code Simple Browser requires the bundled `arrayview-opener.vsix` extension. VS Code provides no public API for auto-opening Simple Browser tabs — the extension uses signal-file IPC. This is fragile; load the `vscode-simplebrowser` skill before touching it.
17
+
18
+ ## Architecture
19
+
20
+ ```
21
+ CLI / Python API
22
+ |
23
+ +- view() Python entry (_launcher.py)
24
+ +- arrayview() CLI entry (_launcher.py)
25
+ |
26
+ +- FastAPI server (_server.py)
27
+ +- / viewer HTML
28
+ +- /shell pywebview shell HTML
29
+ +- /ws/{sid} WebSocket render updates
30
+ +- /load register arrays
31
+ +- /ping health check
32
+ ```
33
+
34
+ ## Core Files
35
+
36
+ ### Backend
37
+
38
+ | File | Responsibility |
39
+ |------|---------------|
40
+ | `_launcher.py` | Entry points, process management, window/browser opening |
41
+ | `_server.py` | FastAPI app, REST routes, WebSocket handlers, HTML templates |
42
+ | `_session.py` | Sessions, global state, caches, render thread, constants |
43
+ | `_render.py` | Rendering: colormaps, LUTs, slice extraction, RGBA/mosaic/RGB |
44
+ | `_vscode.py` | Extension management, signal-file IPC, browser opening |
45
+ | `_platform.py` | Platform/environment detection |
46
+ | `_io.py` | Array I/O, format detection |
47
+ | `_app.py` | **Compat shim only** — re-exports; add no logic here |
48
+
49
+ ### Frontend
50
+
51
+ | File | Responsibility |
52
+ |------|---------------|
53
+ | `_viewer.html` | Viewer UI (single file, all JS/CSS embedded) |
54
+ | `_shell.html` | Shell page for native tab/window management |
55
+
56
+ ### VS Code Extension
57
+
58
+ | File | Responsibility |
59
+ |------|---------------|
60
+ | `vscode-extension/extension.js` | Polls signal file, opens `createWebviewPanel` |
61
+ | `vscode-extension/package.json` | Extension metadata and version |
62
+ | `arrayview-opener.vsix` | Packaged extension (auto-installed by Python) |
63
+
64
+ ## Skills
65
+
66
+ Load the relevant skill before touching the corresponding area.
67
+
68
+ | Skill | Load when... |
69
+ |-------|-------------|
70
+ | `vscode-simplebrowser` | Touching extension install, signal-file IPC, `_ensure_vscode_extension()`, `_VSCODE_EXT_VERSION`, or `vscode-extension/` |
71
+ | `invocation-consistency` | Any server startup, display-opening, or environment-detection change |
72
+ | `ui-consistency-audit` | Any visual feature: zoom, colorbars, canvas events, rendering modes. Or UI changes: keyboard shortcuts, layout, new panels |
73
+ | `frontend-designer` | Any styling/layout change to `_viewer.html` |
74
+ | `docs-style` | Updating README, in-app help overlay, or docstrings |
75
+ | `task-workflow` | Feature or fix tasks |
76
+
77
+ ## Non-Negotiables
78
+
79
+ - Always use `localhost` (not `127.0.0.1`) — required for VS Code port forwarding
80
+ - Never `--force` reinstall the extension if the correct version is already on disk
81
+ - Do not add logic to `_app.py` — compat shim only
82
+ - Avoid orphan processes; shutdown must be automatic
83
+ - Do not regress working display paths when fixing another
84
+
85
+ ## Testing
86
+
87
+ ```bash
88
+ uv sync --group test && uv run playwright install chromium
89
+
90
+ uv run pytest tests/test_api.py -v # HTTP API (~2s)
91
+ uv run pytest tests/test_cli.py -v # CLI entry points
92
+ uv run pytest tests/test_browser.py -v # Playwright (~100s)
93
+ uv run pytest tests/ # all
94
+
95
+ uv run python tests/visual_smoke.py # screenshots → tests/smoke_output/
96
+ ```
97
+
98
+ Visual baselines: `tests/snapshots/` — delete a file to reset.
99
+
100
+ | What changed | Run |
101
+ |---|---|
102
+ | Server / API | `test_api.py` |
103
+ | CLI | `test_cli.py` |
104
+ | Viewer UI | `test_browser.py` + `visual_smoke.py` |
105
+ | VS Code / platform | manual: VS Code local terminal, VS Code tunnel |
106
+
107
+ ## VS Code Integration
108
+
109
+ All key functions live in `_vscode.py`:
110
+ - `_ensure_vscode_extension()` — installs VSIX only if version not already on disk
111
+ - `_open_via_signal_file()` — writes signal; extension polls and opens panel
112
+ - `_schedule_remote_open_retries()` — retry writes for tunnel/first-install latency
113
+ - `_configure_vscode_port_preview()` — port forwarding settings
114
+
115
+ `_VSCODE_EXT_VERSION` in `_vscode.py` must match `vscode-extension/package.json`.
116
+ Rebuild: `cd vscode-extension && vsce package -o ../src/arrayview/arrayview-opener.vsix`
117
+
118
+ ## High-Risk Areas
119
+
120
+ - Extension install: skip `--force` if correct version on disk; clean stale dirs first
121
+ - IPC hook recovery when env vars are stripped by `uv run` or subprocess wrappers
122
+ - tmux: `VSCODE_IPC_HOOK_CLI` not inherited through tmux-server; must walk client PIDs
123
+ - Signal routing: local → per-window targeted file; remote/tunnel → shared fallback
124
+ - Julia: always subprocess (GIL); never run server in-process
125
+ - Port forwarding in tunnel environments; shutdown lifecycle and orphan process prevention
126
+
127
+ ## Documentation
128
+
129
+ Docs live in `docs/` (MkDocs Material site) and `README.md` (minimal quickstart).
130
+
131
+ **Style:** quiet confidence — short sentences, lead with code, imply depth rather than explain everything. No verbose prose, no "Note:", no emojis. Load the `docs-style` skill before editing.
132
+
133
+ **Keep docs in sync:** when a commit adds, changes, or removes user-facing behavior (new shortcut, changed flag, removed feature), update the relevant docs page and README before committing. Skip this for purely internal changes.
134
+
135
+ ## Source of Truth
136
+
137
+ End-user usage: `README.md` and `docs/`