arrayview 0.2.4__tar.gz → 0.4.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 (51) hide show
  1. arrayview-0.4.0/.claude/skills/invocation-consistency/SKILL.md +152 -0
  2. arrayview-0.4.0/.claude/skills/modes-consistency/SKILL.md +126 -0
  3. arrayview-0.4.0/.claude/skills/task-workflow/SKILL.md +78 -0
  4. arrayview-0.4.0/.claude/skills/viewer-ui-checklist/SKILL.md +62 -0
  5. arrayview-0.4.0/.gitignore +27 -0
  6. arrayview-0.4.0/.tmp-vsix/extension/extension.js +343 -0
  7. arrayview-0.4.0/.tmp-vsix/extension/package.json +13 -0
  8. arrayview-0.4.0/AGENTS.md +186 -0
  9. arrayview-0.4.0/PKG-INFO +401 -0
  10. arrayview-0.4.0/README.md +366 -0
  11. arrayview-0.4.0/TODO.md +2 -0
  12. arrayview-0.4.0/docs/large-arrays.md +197 -0
  13. arrayview-0.4.0/docs/superpowers/specs/2026-03-14-todo-batch-design.md +77 -0
  14. arrayview-0.4.0/plans/tunnel-fix/LOG.md +284 -0
  15. arrayview-0.4.0/plans/tunnel-fix/PLAN.md +148 -0
  16. {arrayview-0.2.4 → arrayview-0.4.0}/pyproject.toml +26 -1
  17. arrayview-0.4.0/scripts/demo.py +61 -0
  18. arrayview-0.4.0/src/arrayview/__init__.py +4 -0
  19. arrayview-0.4.0/src/arrayview/_app.py +178 -0
  20. arrayview-0.4.0/src/arrayview/_icon.png +0 -0
  21. arrayview-0.4.0/src/arrayview/_io.py +142 -0
  22. arrayview-0.4.0/src/arrayview/_launcher.py +1747 -0
  23. arrayview-0.4.0/src/arrayview/_platform.py +303 -0
  24. arrayview-0.4.0/src/arrayview/_render.py +757 -0
  25. arrayview-0.4.0/src/arrayview/_server.py +1580 -0
  26. arrayview-0.4.0/src/arrayview/_session.py +371 -0
  27. arrayview-0.4.0/src/arrayview/_shell.html +167 -0
  28. arrayview-0.4.0/src/arrayview/_viewer.html +5094 -0
  29. arrayview-0.4.0/src/arrayview/_vscode.py +543 -0
  30. arrayview-0.4.0/src/arrayview/arrayview-opener.vsix +0 -0
  31. arrayview-0.4.0/tests/conftest.py +125 -0
  32. arrayview-0.4.0/tests/make_vectorfield_test_arrays.py +45 -0
  33. arrayview-0.4.0/tests/test_api.py +608 -0
  34. arrayview-0.4.0/tests/test_browser.py +678 -0
  35. arrayview-0.4.0/tests/test_cli.py +103 -0
  36. arrayview-0.4.0/tests/test_large_arrays.py +285 -0
  37. arrayview-0.4.0/tests/visual_smoke.py +854 -0
  38. {arrayview-0.2.4 → arrayview-0.4.0}/uv.lock +386 -1
  39. arrayview-0.4.0/vscode-extension/LICENSE +21 -0
  40. arrayview-0.4.0/vscode-extension/extension.js +248 -0
  41. arrayview-0.4.0/vscode-extension/package.json +23 -0
  42. arrayview-0.2.4/.gitignore +0 -15
  43. arrayview-0.2.4/PKG-INFO +0 -54
  44. arrayview-0.2.4/README.md +0 -26
  45. arrayview-0.2.4/src/arrayview/__init__.py +0 -3
  46. arrayview-0.2.4/src/arrayview/_app.py +0 -948
  47. arrayview-0.2.4/src/arrayview/_shell.html +0 -132
  48. arrayview-0.2.4/src/arrayview/_viewer.html +0 -818
  49. {arrayview-0.2.4 → arrayview-0.4.0}/.github/workflows/python-publish.yml +0 -0
  50. {arrayview-0.2.4 → arrayview-0.4.0}/.python-version +0 -0
  51. {arrayview-0.2.4 → arrayview-0.4.0}/LICENSE +0 -0
@@ -0,0 +1,152 @@
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 | Key detection | Server model |
15
+ |------|------------|--------------|-------------|
16
+ | **CLI** | `arrayview()` in `_app.py` | — (always CLI path) | `_run_server_subprocess()` or in-process thread |
17
+ | **Python script** | `view(arr)` | `_in_jupyter()` → False | In-process daemon thread |
18
+ | **Jupyter / VS Code interactive** | `view(arr)` | `_in_jupyter()` → True | In-process daemon thread |
19
+ | **Julia (PythonCall)** | `view(arr)` | `_is_julia_env()` → True | Always subprocess (`_view_julia()`) |
20
+ | **VS Code tunnel / SSH remote** | `arrayview()` or `view()` | `_is_vscode_remote()` → True | Subprocess server |
21
+ | **Plain SSH (no VS Code)** | `arrayview()` or `view()` | `SSH_CONNECTION` set, no VS Code remote | In-process or subprocess; prints port-forward hint |
22
+
23
+ ## Key Detection Functions (all in `_app.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 → subprocess + extension routing
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
+ ## Feature Categories & Where Each Path Diverges
35
+
36
+ ### Server Lifecycle
37
+
38
+ | Path | Server model | Implication |
39
+ |------|-------------|-------------|
40
+ | CLI | Background thread in `main()` → blocks until Ctrl-C | `_shutdown_event` drives cleanup |
41
+ | Python script | `_start_server_thread()` → daemon thread | Dies with caller process |
42
+ | Jupyter | `_start_server_thread()` → daemon thread | Persists across cells; port reuse matters |
43
+ | Julia | `_view_julia()` spawns detached subprocess | Caller (Julia) may not share GIL; subprocess manages its own lifecycle |
44
+ | VS Code remote | Subprocess via `_run_server_subprocess()` | Caller exits; server stays alive independently |
45
+ | Plain SSH | In-process or subprocess | No GUI; user must port-forward manually |
46
+
47
+ When changing server startup or shutdown logic, check whether daemonized threads, subprocess reaping, and port reuse are all handled correctly for each path.
48
+
49
+ ### Browser / Display Opening
50
+
51
+ | Path | Opens how | Key function |
52
+ |------|-----------|-------------|
53
+ | CLI local | Native window (`pywebview`) OR system browser | `_open_webview_with_fallback()` → `_open_browser()` |
54
+ | Python script local | Same as CLI local | Same |
55
+ | Jupyter | Inline IFrame via `IPython.display.IFrame` | `view()` returns IFrame object |
56
+ | Julia | System browser via subprocess signal | `_view_julia()` writes signal file |
57
+ | VS Code terminal (local) | VS Code Simple Browser via extension | `_open_via_signal_file()` + `_ensure_vscode_extension()` |
58
+ | VS Code tunnel/remote | VS Code Simple Browser on *client* side | signal file + extension + port auto-forward |
59
+ | Plain SSH | Prints `ssh -L <port>:localhost:<port>` hint | `_open_browser()` fallback |
60
+
61
+ When changing URL construction, port logic, or display routing, trace through `_open_browser()` and its callers for each path — particularly the VS Code tunnel path where the *client's* VS Code picks up the signal file.
62
+
63
+ ### VS Code Extension (`arrayview-opener.vsix`)
64
+
65
+ - Extension is auto-installed by `_ensure_vscode_extension()` when `_in_vscode_terminal()` is True
66
+ - Version must match `_VSCODE_EXT_VERSION` in `_app.py` AND `vscode-extension/package.json`
67
+ - After rebuilding the VSIX: update `_VSCODE_EXT_VERSION` in `_app.py`
68
+ - IPC hook may be stripped by `uv run` → `_find_vscode_ipc_hook()` walks parent process env
69
+ - Tunnel path: extension must configure `remote.portsAttributes` to make port public/silent
70
+
71
+ ### Port & URL Construction
72
+
73
+ - Always use `localhost` (not `127.0.0.1`) so VS Code port-forwarding works
74
+ - Port default: `8123`; CLI: `--port` flag; `view()`: `port=` kwarg
75
+ - Compare mode URLs include `?compare_sid=...&compare_sids=...`
76
+ - Overlay URLs include `?overlay_sid=...`
77
+ - Mosaic/qMRI state is in the JS (not URL params); no server changes needed for those
78
+
79
+ ### Julia-Specific Constraints
80
+
81
+ - GIL conflicts: never run server in-process when `_is_julia_env()` is True
82
+ - `_view_julia()` starts a detached subprocess running `arrayview_server` CLI tool
83
+ - Array data is serialized to a temp `.npy` file and loaded by the subprocess
84
+ - Julia arrays use PythonCall → numpy conversion before any arrayview API call
85
+ - No interactive stdin in subprocesses; all params must be encoded in CLI flags or files
86
+
87
+ ### Jupyter-Specific Constraints
88
+
89
+ - `_in_jupyter()` returns True for ipykernel (VS Code notebook, JupyterLab, classic Notebook)
90
+ - Julia's IJulia kernel is NOT ipykernel → `_in_jupyter()` returns False in Julia notebooks (use Julia path instead)
91
+ - `inline=True` → returns `IPython.display.IFrame`; caller must return it from the cell for display
92
+ - Port reuse: calling `view()` multiple times reuses same port and server if already running
93
+ - `window='native'` override still works in Jupyter; viewer opens as separate window
94
+
95
+ ## Step-by-Step Checklist
96
+
97
+ When implementing a new feature, ask:
98
+
99
+ 1. **Does it touch server startup, teardown, or port selection?**
100
+ - Test CLI (`uv run arrayview file.npy`) — server must start and terminate cleanly
101
+ - Test Python script (`python script.py`) — daemon thread must not orphan
102
+ - Test Jupyter cell — repeated calls must not fail on port-already-in-use
103
+ - Check Julia path — `_view_julia()` must pass any new params via CLI flag or file
104
+
105
+ 2. **Does it touch browser/display opening?**
106
+ - Test local native window (macOS) — `pywebview` opens correctly
107
+ - Test local system browser — `open http://localhost:8123` path works
108
+ - Test VS Code terminal — Simple Browser opens via extension signal file
109
+ - Test VS Code tunnel — route reaches client-side Simple Browser, not remote host browser
110
+ - Verify `_ensure_vscode_extension()` installs/updates if VSIX changed
111
+
112
+ 3. **Does it touch URL construction or query params?**
113
+ - Verify `localhost` (not `127.0.0.1`)
114
+ - Verify all modes still get the right params (compare, overlay, vectorfield)
115
+
116
+ 4. **Does it touch environment detection?**
117
+ - Check the detection order in `view()`: Julia → Jupyter → VS Code remote → VS Code terminal → local
118
+ - Any new detection must not break the fallback chain for paths it shouldn't match
119
+ - `_find_vscode_ipc_hook()` walks up to 12 parent processes — new subprocess wrappers may need the same treatment
120
+
121
+ 5. **Does it change the VS Code extension?**
122
+ - Rebuild VSIX: `cd vscode-extension && vsce package -o ../src/arrayview/arrayview-opener.vsix`
123
+ - Bump `_VSCODE_EXT_VERSION` in `_app.py` and `vscode-extension/package.json` together
124
+ - Test install in a fresh VS Code profile
125
+
126
+ ## Validation Matrix
127
+
128
+ After implementing, run through this matrix manually or in CI:
129
+
130
+ | Check | CLI | Python script | Jupyter | Julia | VS Code local | VS Code tunnel |
131
+ |-------|-----|--------------|---------|-------|--------------|----------------|
132
+ | Server starts | ✓? | ✓? | ✓? | ✓? | ✓? | ✓? |
133
+ | Array loads & renders | ✓? | ✓? | ✓? | ✓? | ✓? | ✓? |
134
+ | Display opens (window/browser/inline) | ✓? | ✓? | inline IFrame | browser | Simple Browser | Simple Browser on client |
135
+ | Ctrl-C / kernel stop cleans up | ✓? | ✓? | ✓? | ✓? | ✓? | ✓? |
136
+ | Compare mode works (if file-based) | ✓? | N/A | N/A | N/A | ✓? | ✓? |
137
+
138
+ Quick automated checks:
139
+ ```bash
140
+ uv run pytest tests/test_api.py -x # API contract
141
+ uv run pytest tests/test_cli.py -x # CLI entry point
142
+ uv run python -c "from arrayview import view; import numpy as np; view(np.zeros((10,10)))"
143
+ ```
144
+
145
+ ## Red Flags — STOP
146
+
147
+ - "I changed how the server starts but only tested CLI" → test all paths
148
+ - "The feature works locally but not in tunnel" → check `_is_vscode_remote()` path and port exposure
149
+ - "I used `127.0.0.1` in the URL" → use `localhost` so VS Code port-forwarding intercepts it
150
+ - "Julia works but I only tested the Python path" → Julia always uses subprocess — test `_view_julia()` explicitly
151
+ - "I bumped the extension version in package.json but not `_VSCODE_EXT_VERSION`" → they must stay in sync
152
+ - "The server starts but leaves an orphan process after Ctrl-C" → check `_shutdown_event`, signal handlers, and subprocess reaping
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: modes-consistency
3
+ description: Use when implementing any visual feature in arrayview that touches canvas rendering, zoom, eggs, colorbars, keyboard shortcuts, or layout. Ensures the feature is applied consistently across ALL viewing modes, not just the one being worked on.
4
+ ---
5
+
6
+ # ArrayView Modes Consistency Checklist
7
+
8
+ ## Rule
9
+
10
+ Every visual feature in `_viewer.html` MUST be implemented for all applicable modes. Implementing it only in one mode and shipping is a bug, not a partial feature.
11
+
12
+ ## The Six Modes
13
+
14
+ | Mode | State flag | Scale function | Entry key |
15
+ |------|-----------|---------------|-----------|
16
+ | **Normal** | (default) | `scaleCanvas()` | — |
17
+ | **Multi-view** | `multiViewActive` | `mvScaleAllCanvases()` | V / v |
18
+ | **Compare** | `compareActive` | `compareScaleCanvases()` | B / P |
19
+ | **Diff** | `diffMode > 0` (inside compare) | `compareScaleCanvases()` | X (in compare) |
20
+ | **Registration** | `registrationMode` (inside compare) | `compareScaleCanvases()` | R (in compare) |
21
+ | **qMRI** | `qmriActive` | `qvScaleAllCanvases()` | q |
22
+
23
+ Note: Overlay mode (`overlay_sid` URL param) is composited server-side into the normal frame — check backend rendering too.
24
+
25
+ ## Common Feature Categories & Where to Implement
26
+
27
+ ### Zoom / Canvas Sizing
28
+
29
+ Every mode has a dedicated scale function. When changing zoom behavior, check **all four**:
30
+
31
+ - `scaleCanvas(w, h)` [Normal] — applies `baseScale * userZoom`, snaps `userZoom` to cap at top
32
+ - `mvScaleAllCanvases()` [Multi-view] — iterates `mvViews`, caps `userZoom` via per-view calculation
33
+ - `compareScaleCanvases()` [Compare / Diff / Registration] — grid layout, caps `userZoom` before applying
34
+ - `qvScaleAllCanvases()` [qMRI] — grid layout for parameter maps
35
+
36
+ **Cap pattern** (already applied in compare): compute `capZoom` from max allowable scale across all panes, then `if (userZoom > capZoom) userZoom = capZoom;` before the sizing loop.
37
+
38
+ ### Eggs (Mode Indicator Dots — LOG, complex, mask, overlay badges)
39
+
40
+ All eggs are positioned by `positionEggs()`. The function branches by mode. When changing egg placement rules, check:
41
+
42
+ - Normal branch: uses `#slim-cb-wrap` bounding box if visible, else canvas bottom
43
+ - Multi-view branch: uses `#mv-cb-wrap` bounding box if present, else estimates 36px below panes
44
+ - Compare branch: walks `.compare-canvas-wrap` rects to find the tallest pane bottom
45
+ - qMRI branch: uses `.qv-canvas-wrap` rects + 36px estimate
46
+
47
+ When adding new egg types or changing vertical anchor, update all four branches.
48
+
49
+ ### Colorbar / Window-Level Interaction
50
+
51
+ Colorbars are drawn by separate per-mode functions — there is no shared abstraction:
52
+
53
+ | Mode | Colorbar function(s) |
54
+ |------|---------------------|
55
+ | Normal | `drawSlimColorbar(markerFrac)` |
56
+ | Compare | `drawComparePaneCb(idx)` + `drawAllComparePaneCbs()` |
57
+ | Diff (in compare) | `drawDiffPaneCb(vmin, vmax)` |
58
+ | Registration (in compare) | `drawRegBlendCb()` |
59
+ | Multi-view | `drawMvCbs()` (called inside `mvScaleAllCanvases`) |
60
+ | qMRI | Drawn inline per view in `qvRender()` |
61
+
62
+ When adding colorbar interactivity (e.g., drag, scroll), check which colorbars the feature should apply to, and implement it per-element.
63
+
64
+ ### Keyboard Shortcuts
65
+
66
+ The `keydown` handler on `#keyboard-sink` dispatches by active mode. New shortcuts must:
67
+
68
+ 1. Not conflict with mode-specific keys — check the table in _viewer.html's keyboard section
69
+ 2. Have an explicit guard when the shortcut only makes sense in specific modes (e.g., `if (!compareActive) return;`)
70
+ 3. Fall through correctly when multiple modes are active (e.g., `registrationMode` requires `compareActive`)
71
+
72
+ ### Canvas Elements Present Per Mode
73
+
74
+ | Mode | Canvas elements |
75
+ |------|----------------|
76
+ | Normal | `#canvas` |
77
+ | Multi-view | `.mv-canvas` × 3 (inside `.mv-view`) |
78
+ | Compare | `.compare-canvas` × 2–6 (inside `.compare-canvas-inner`) |
79
+ | Diff | `#compare-diff-canvas` (shown only when `diffMode > 0` and `compareSids.length === 2`) |
80
+ | Registration | 3rd `.compare-canvas` = blended overlay; `compareRegistrationFrame` drives it |
81
+ | qMRI | `.qv-canvas` × 3–6 |
82
+
83
+ Features that attach event listeners to canvas elements (mouse, wheel, etc.) must attach to the correct per-mode set of canvases, not just `document.getElementById('canvas')`.
84
+
85
+ ## Step-by-Step Implementation Checklist
86
+
87
+ When implementing a new feature, run through this list:
88
+
89
+ 1. **Identify applicable modes**: Does this feature affect canvas sizing? colorbars? eggs? cursor? Yes → all modes.
90
+
91
+ 2. **Normal mode** — implement first, verify it works.
92
+
93
+ 3. **Compare mode** (includes Diff and Registration) — `compareScaleCanvases()` or the relevant compare-specific functions.
94
+
95
+ 4. **Multi-view mode** — `mvScaleAllCanvases()`, or per-view listener attachment if it's an event feature.
96
+
97
+ 5. **qMRI mode** — `qvScaleAllCanvases()`, or inline in `qvRender()` if it's a per-pane colorbar feature.
98
+
99
+ 6. **Overlay mode (backend)** — if the feature affects how frames are composited, check the `/slice`, `/frame`, `/diff`, and `/mosaic` endpoints in `_app.py`. Overlay compositing happens in `_composite_overlay_mask()` before PNG encoding.
100
+
101
+ 7. **Mosaic / z-grid** — when feature involves the diff endpoint (`/diff`), check that `dim_z` is correctly passed and handled server-side (backend `get_diff` must produce a mosaic grid when `dim_z >= 0`).
102
+
103
+ 8. **State snapshot** — if the feature introduces a new state variable, add it to `collectStateSnapshot()` and `applyStateSnapshot()` so it is preserved across page reloads and compare mode transitions.
104
+
105
+ ## Red Flags — STOP
106
+
107
+ - "I only changed it for normal mode, the others are TODO" → implement all applicable modes now
108
+ - "`compareScaleCanvases` and `scaleCanvas` now behave differently for zoom capping" → pick one pattern and apply to both
109
+ - "I added a canvas listener to `#canvas` and now it doesn't work in compare" → multi-canvas modes have different DOM structures
110
+ - "I only pass `dim_z` to some endpoints but not others" → check every endpoint that renders an image and needs the mosaic path
111
+
112
+ ## Quick Sanity Test
113
+
114
+ After implementing, run:
115
+ ```
116
+ uv run pytest tests/test_api.py -x
117
+ uv run python tests/visual_smoke.py # then review smoke_output/
118
+ ```
119
+
120
+ And manually verify:
121
+ - [ ] Feature works in normal view
122
+ - [ ] Feature works in compare (press B → pick second array)
123
+ - [ ] Feature works in diff (press X while in compare)
124
+ - [ ] Feature works in multi-view (press v)
125
+ - [ ] Feature works in qMRI if applicable (press q — needs array with 3–6 dim of size 3–6)
126
+ - [ ] Eggs remain correctly anchored below canvases in each mode after feature is applied
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: task-workflow
3
+ description: Enforce one-commit-per-TODO-item workflow and required collateral updates (README/help/tests/CHANGELOG) for arrayview feature/fix tasks.
4
+ ---
5
+
6
+ # ArrayView Task Workflow Skill
7
+
8
+ ## Purpose
9
+
10
+ Use this skill whenever implementing or fixing a TODO item. It enforces a disciplined workflow so each TODO becomes a single, self-contained commit (and ideally a single PR) that also updates documentation, help text, and tests.
11
+
12
+ ## Rule
13
+
14
+ Every completed TODO item must:
15
+
16
+ - Be implemented in a single commit with a clear commit message (see Commit Message format below).
17
+ - Include or update automated tests that validate the feature/fix where possible.
18
+ - Update `README.md` or the in-app help overlay if usage or shortcuts changed.
19
+ - Update `tests/visual_smoke.py` for UI/layout changes (add a numbered scenario and screenshot capture).
20
+ - Add a short entry to `CHANGELOG.md` or `AGENTS.md` (if CHANGELOG.md doesn't exist, add to `AGENTS.md` under a "Changelog" or the Skills section).
21
+
22
+ If any of these steps cannot be completed (e.g., untestable UI dialog that requires human review), the implementer must document the reason in the PR description and add a smoke-test TODO row in `tests/visual_smoke.py` with `✗ (reason)`.
23
+
24
+ ## Commit Message Format
25
+
26
+ - Use a concise, searchable prefix describing the TODO ID or short label, then a clear subject, then an optional body.
27
+ - Recommended: `todo: <short-label>: <one-line-summary>`
28
+ - Example: `todo: picker-two-column: add two-column compare picker and shape-filtering`
29
+
30
+ Include a bullet list in the commit body showing collateral updates, e.g.:
31
+
32
+ - Tests: `tests/test_picker.py` and `tests/visual_smoke.py#NN`
33
+ - Docs: `README.md` updated section "Compare Picker"
34
+ - Changelog: `AGENTS.md` / `CHANGELOG.md`
35
+
36
+ ## Checklist (enforced by this skill)
37
+
38
+ For each TODO item, ensure the following before marking the task done:
39
+
40
+ - [ ] Code implements the feature/fix and passes `python -m mccabe`/lint checks (if configured).
41
+ - [ ] Unit tests or API tests added/updated in `tests/`.
42
+ - [ ] If UI changes, `tests/visual_smoke.py` updated with a numbered scenario and `_shot()` call.
43
+ - [ ] README or in-app help overlay (`#help-overlay` content in `src/arrayview/_viewer.html`) updated if usage changed.
44
+ - [ ] `CHANGELOG.md` or `AGENTS.md` updated with a one-line summary.
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/`)
49
+
50
+ ## How to use
51
+
52
+ 1. Create a branch for the TODO item: `git checkout -b todo/<short-label>`.
53
+ 2. Implement the change and add tests and docs as required.
54
+ 3. Run tests and smoke script locally until passing (or document failures).
55
+ 4. Stage and commit only the files relevant to this TODO with a single commit message as defined above.
56
+ 5. Push and open a PR.
57
+
58
+ ## Special cases
59
+
60
+ - If a TODO necessarily spans multiple commits (e.g., large refactor), use a temporary feature branch with descriptive commits, then squash into a single commit before merging. The PR must include a description explaining why squashing is required and ensure tests/docs are included in the final squashed commit.
61
+
62
+ - If a fix must touch unrelated files (rare), keep those touches minimal and include an explanation in the commit body.
63
+
64
+ ## Automation hints for maintainers
65
+
66
+ - Prefer adding a simple CI check that asserts commits touching `src/arrayview/` are accompanied by changes in `tests/` or `README.md`. This can be implemented as a lightweight script in `.github/workflows/`.
67
+
68
+ - Encourage using `git commit --no-verify -m "..."` only when CI is temporarily failing for reasons unrelated to the task; prefer fixing CI first.
69
+
70
+
71
+ ## Red Flags — STOP
72
+
73
+ - "I'll do tests later" — must add tests or justify why not and add smoke test TODO entry.
74
+ - "I'll bundle multiple unrelated TODOs into one commit" — split them or squash locally, but final PR should present one item per commit semantics.
75
+ - "Docs unchanged although usage changed" — update README/help or explain in PR why docs remain unchanged.
76
+
77
+
78
+ # End of skill
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: viewer-ui-checklist
3
+ description: Use when adding keyboard shortcuts, changing layout, or making any UI change to arrayview. Ensures visual_smoke.py stays in sync.
4
+ ---
5
+
6
+ # ArrayView UI Checklist
7
+
8
+ ## Rule
9
+
10
+ Every UI change to arrayview MUST be reflected in `tests/visual_smoke.py` before the task is complete.
11
+
12
+ ## What counts as a UI change
13
+
14
+ - New keyboard shortcut
15
+ - Changed keyboard shortcut behavior
16
+ - New view mode or display mode
17
+ - Layout changes (canvas sizing, colorbar position, overlays)
18
+ - New overlay, dialog, or panel
19
+
20
+ ## Steps (mandatory, in order)
21
+
22
+ 1. **Update coverage table** at the top of `visual_smoke.py`
23
+ - If shortcut is now testable: change `✗` to `✓ NN` with scenario number
24
+ - If new shortcut: add a row
25
+ - Mark untestable shortcuts with `✗ (reason)`
26
+
27
+ 2. **Add or update scenario** in `run_smoke()` with a numbered section comment
28
+ - Follow the existing pattern: `# ── NN: description ──────`
29
+ - Capture at least one screenshot with `_shot(page, "NN_descriptive_name")`
30
+
31
+ 3. **Run the smoke test** to verify the new scenario works:
32
+ ```
33
+ uv run python tests/visual_smoke.py
34
+ ```
35
+
36
+ 4. **Open and review** the new screenshots in `tests/smoke_output/`
37
+
38
+ ## Red flags — STOP
39
+
40
+ - "The shortcut is too simple to need a smoke test" → ALL shortcuts need entries
41
+ - "I'll add the test later" → add it in the same task
42
+ - "The coverage table says ✗, that's fine" → only fine if you document WHY (requires dialog, etc.)
43
+
44
+ ## Stability check pattern
45
+
46
+ When verifying a key causes no visual jumps:
47
+
48
+ ```python
49
+ def _check_no_jump(page, key, selectors, shot_name):
50
+ before = {s: page.locator(s).bounding_box() for s in selectors}
51
+ _press(page, key)
52
+ after = {s: page.locator(s).bounding_box() for s in selectors}
53
+ _shot(page, shot_name)
54
+ for s in selectors:
55
+ b, a = before[s], after[s]
56
+ if b and a:
57
+ dx = abs(b["x"] - a["x"]); dy = abs(b["y"] - a["y"])
58
+ if dx > 2 or dy > 2:
59
+ print(f" JUMP: {s} moved {dx:.0f}px/{dy:.0f}px after {key}")
60
+ ```
61
+
62
+ Key selectors to check: `#canvas-wrap`, `#slim-cb-wrap`, `#info`
@@ -0,0 +1,27 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ *.npy
13
+ .claude/
14
+ !.claude/skills/
15
+ !.claude/skills/**
16
+ CLAUDE.md
17
+ debug/
18
+ tests/snapshots/
19
+
20
+ *.jl
21
+ *.ipynb
22
+ logs/
23
+ *.png
24
+ !src/arrayview/_icon.png
25
+ .worktrees/
26
+
27
+ benchmarks/data