arrayview 0.3.0__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.
- arrayview-0.4.0/.claude/skills/invocation-consistency/SKILL.md +152 -0
- arrayview-0.4.0/.claude/skills/modes-consistency/SKILL.md +126 -0
- arrayview-0.4.0/.claude/skills/task-workflow/SKILL.md +78 -0
- arrayview-0.4.0/.claude/skills/viewer-ui-checklist/SKILL.md +62 -0
- {arrayview-0.3.0 → arrayview-0.4.0}/.gitignore +11 -0
- arrayview-0.4.0/.tmp-vsix/extension/extension.js +343 -0
- arrayview-0.4.0/.tmp-vsix/extension/package.json +13 -0
- arrayview-0.4.0/AGENTS.md +186 -0
- arrayview-0.4.0/PKG-INFO +401 -0
- arrayview-0.4.0/README.md +366 -0
- arrayview-0.4.0/TODO.md +2 -0
- arrayview-0.4.0/docs/large-arrays.md +197 -0
- arrayview-0.4.0/docs/superpowers/specs/2026-03-14-todo-batch-design.md +77 -0
- arrayview-0.4.0/plans/tunnel-fix/LOG.md +284 -0
- arrayview-0.4.0/plans/tunnel-fix/PLAN.md +148 -0
- {arrayview-0.3.0 → arrayview-0.4.0}/pyproject.toml +2 -1
- arrayview-0.4.0/scripts/demo.py +61 -0
- arrayview-0.4.0/src/arrayview/__init__.py +4 -0
- arrayview-0.4.0/src/arrayview/_app.py +178 -0
- arrayview-0.4.0/src/arrayview/_icon.png +0 -0
- arrayview-0.4.0/src/arrayview/_io.py +142 -0
- arrayview-0.4.0/src/arrayview/_launcher.py +1747 -0
- arrayview-0.4.0/src/arrayview/_platform.py +303 -0
- arrayview-0.4.0/src/arrayview/_render.py +757 -0
- arrayview-0.4.0/src/arrayview/_server.py +1580 -0
- arrayview-0.4.0/src/arrayview/_session.py +371 -0
- arrayview-0.4.0/src/arrayview/_shell.html +167 -0
- arrayview-0.4.0/src/arrayview/_viewer.html +5094 -0
- arrayview-0.4.0/src/arrayview/_vscode.py +543 -0
- arrayview-0.4.0/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.4.0/tests/make_vectorfield_test_arrays.py +45 -0
- {arrayview-0.3.0 → arrayview-0.4.0}/tests/test_api.py +234 -4
- arrayview-0.4.0/tests/test_browser.py +678 -0
- arrayview-0.4.0/tests/test_cli.py +103 -0
- arrayview-0.4.0/tests/test_large_arrays.py +285 -0
- arrayview-0.4.0/tests/visual_smoke.py +854 -0
- {arrayview-0.3.0 → arrayview-0.4.0}/uv.lock +11 -0
- arrayview-0.4.0/vscode-extension/LICENSE +21 -0
- arrayview-0.4.0/vscode-extension/extension.js +248 -0
- arrayview-0.4.0/vscode-extension/package.json +23 -0
- arrayview-0.3.0/PKG-INFO +0 -79
- arrayview-0.3.0/README.md +0 -45
- arrayview-0.3.0/src/arrayview/__init__.py +0 -3
- arrayview-0.3.0/src/arrayview/_app.py +0 -1738
- arrayview-0.3.0/src/arrayview/_shell.html +0 -136
- arrayview-0.3.0/src/arrayview/_viewer.html +0 -1179
- arrayview-0.3.0/tests/test_browser.py +0 -350
- {arrayview-0.3.0 → arrayview-0.4.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.3.0 → arrayview-0.4.0}/.python-version +0 -0
- {arrayview-0.3.0 → arrayview-0.4.0}/LICENSE +0 -0
- {arrayview-0.3.0 → arrayview-0.4.0}/tests/conftest.py +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`
|