arrayview 0.18.0__tar.gz → 0.19.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.19.0/.agents/skills/frontend-designer/SKILL.md +127 -0
- arrayview-0.19.0/.agents/skills/todo-workflow/SKILL.md +37 -0
- {arrayview-0.18.0/.claude → arrayview-0.19.0/.agents}/skills/ui-consistency-audit/SKILL.md +1 -1
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/ROUTER.md +1 -1
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/context/architecture.md +2 -2
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/context/project-state.md +3 -3
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/context/setup.md +3 -3
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/context/stack.md +2 -2
- {arrayview-0.18.0 → arrayview-0.19.0}/.vscode/settings.json +18 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/PKG-INFO +1 -1
- {arrayview-0.18.0 → arrayview-0.19.0}/docs/display.md +1 -1
- {arrayview-0.18.0 → arrayview-0.19.0}/pyproject.toml +1 -1
- {arrayview-0.18.0 → arrayview-0.19.0}/scripts/release.sh +31 -4
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_config.py +18 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_launcher.py +31 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_server.py +11 -22
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_session.py +1 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_stdio_server.py +4 -1
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_viewer.html +1670 -361
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_api.py +0 -33
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_browser.py +163 -9
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_command_reachability.py +1 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_interactions.py +14 -17
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_mode_consistency.py +4 -3
- arrayview-0.19.0/tests/test_mode_entry_batching.py +63 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_mode_roundtrip.py +15 -3
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/visual_smoke.py +31 -35
- {arrayview-0.18.0 → arrayview-0.19.0}/uv.lock +1 -1
- arrayview-0.18.0/IMMERSIVE_ANIMATION.md +0 -43
- {arrayview-0.18.0/.claude → arrayview-0.19.0/.agents}/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.18.0/.claude → arrayview-0.19.0/.agents}/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.18.0/.claude → arrayview-0.19.0/.agents}/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.18.0/.claude → arrayview-0.19.0/.agents}/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.github/copilot-instructions.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.gitignore +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.ignore +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/AGENTS.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/SETUP.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/SYNC.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/context/conventions.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/context/decisions.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/context/frontend.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/context/render-pipeline.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/patterns/INDEX.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/patterns/README.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/patterns/add-file-format.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/patterns/add-server-endpoint.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/patterns/debug-render.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.mex/patterns/frontend-change.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.opencode/opencode.json +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/.python-version +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/AGENTS.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/CONTRIBUTING.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/LICENSE +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/README.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/docs/comparing.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/docs/configuration.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/docs/index.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/docs/loading.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/docs/logo.png +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/docs/measurement.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/docs/remote.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/docs/viewing.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/matlab/arrayview.m +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/mkdocs.yml +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/plans/2026-04-14-immersive-animation.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/scripts/demo.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_io.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/conftest.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_cli.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_config.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_cross_mode_parametrized.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_loading_server.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_torch.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_view_component_integration.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/test_view_component_unit.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/tests/ui_audit.py +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/vscode-extension/AGENTS.md +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/vscode-extension/LICENSE +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/vscode-extension/extension.js +0 -0
- {arrayview-0.18.0 → arrayview-0.19.0}/vscode-extension/package.json +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-designer
|
|
3
|
+
description: Use when making any styling or layout change to _viewer.html. Ensures new UI is visually consistent with the established design language — dark theme, monospace typography, yellow accents, and minimal chrome.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ArrayView Frontend Design Skill
|
|
7
|
+
|
|
8
|
+
## Design Philosophy
|
|
9
|
+
|
|
10
|
+
Minimal chrome. Let arrays fill the screen. UI elements are dim until needed; the array is always the primary focus.
|
|
11
|
+
|
|
12
|
+
- Controls fade in on hover or keypress, not permanently visible
|
|
13
|
+
- Text is small and monospaced
|
|
14
|
+
- No decorative elements — every pixel either shows data or provides affordance
|
|
15
|
+
- All four themes must look correct; never hardcode colors
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Design Tokens (CSS Custom Properties)
|
|
20
|
+
|
|
21
|
+
All colors come from CSS variables defined on `:root`. Never use raw hex values in new code.
|
|
22
|
+
|
|
23
|
+
| Variable | Dark default | Purpose |
|
|
24
|
+
|----------|-------------|---------|
|
|
25
|
+
| `--bg` | `#0c0c0c` | Page/canvas background |
|
|
26
|
+
| `--surface` | `#141414` | Panel/overlay backgrounds |
|
|
27
|
+
| `--surface-2` | `#1c1c1c` | Input fields, nested surfaces |
|
|
28
|
+
| `--border` | `#2c2c2c` | Borders, dividers |
|
|
29
|
+
| `--text` | `#d8d8d8` | Primary text |
|
|
30
|
+
| `--muted` | `#5a5a5a` | Secondary/dim text, inactive labels |
|
|
31
|
+
| `--highlight` | `#fff` | Maximum contrast text |
|
|
32
|
+
| `--active-dim` | `#f5c842` | **Primary accent** — active state, keyboard keys, clim handles |
|
|
33
|
+
| `--spatial-dim` | `#b48ead` | Spatial (x/y) dimension labels |
|
|
34
|
+
| `--help-key` | `#f5c842` | Keyboard shortcut key labels in help overlay |
|
|
35
|
+
| `--overlay-bg` | `rgba(0,0,0,0.72)` | Modal backdrop |
|
|
36
|
+
| `--blur` | `blur(14px)` | Backdrop blur for overlays |
|
|
37
|
+
| `--radius` | `10px` | Standard border radius |
|
|
38
|
+
| `--radius-lg` | `14px` | Large panel border radius |
|
|
39
|
+
|
|
40
|
+
Four themes exist: `dark` (default), `.light`, `.solarized`, `.nord`. Each redefines all tokens. Test with `T` key.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Typography
|
|
45
|
+
|
|
46
|
+
Single font stack throughout:
|
|
47
|
+
```css
|
|
48
|
+
font-family: 'SF Mono', ui-monospace, 'Cascadia Code', 'JetBrains Mono', monospace;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Sizes used in practice:
|
|
52
|
+
- `10px` — tooltips, micro labels
|
|
53
|
+
- `11px` — colorbar value labels, secondary info
|
|
54
|
+
- `12px` — dim labels, position info
|
|
55
|
+
- `13px` — array name, status bar, picker items
|
|
56
|
+
- `15px` — dim scrubber labels
|
|
57
|
+
- `16px` — mode/view headers
|
|
58
|
+
|
|
59
|
+
Never use a sans-serif font. Never use `font-weight: bold` except via the `.highlight` or `.active-dim` classes.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Layout Principles
|
|
64
|
+
|
|
65
|
+
- `#wrapper`: flexbox column, centers content vertically and horizontally
|
|
66
|
+
- Canvas fills available space; UI chrome is positioned absolutely around it
|
|
67
|
+
- Bottom bar (`#info`): fixed height, monospace, dim text
|
|
68
|
+
- Overlays (`#help-overlay`, `#uni-picker`, `#inline-prompt`): centered modal with `--overlay-bg` backdrop
|
|
69
|
+
- Colorbar (`#slim-cb-wrap`): thin strip below canvas, expands only in Lebesgue mode
|
|
70
|
+
|
|
71
|
+
**Canvas sizing:** canvas width/height are set by JavaScript (`scaleCanvas` and friends), not CSS. Do not set canvas dimensions in CSS.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Component Patterns
|
|
76
|
+
|
|
77
|
+
### Status/info text
|
|
78
|
+
```css
|
|
79
|
+
color: var(--muted); font-size: 12px; /* passive info */
|
|
80
|
+
color: var(--text); font-size: 12px; /* active info */
|
|
81
|
+
color: var(--active-dim); font-weight: bold; /* highlighted state */
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Panel/overlay
|
|
85
|
+
```css
|
|
86
|
+
background: var(--surface);
|
|
87
|
+
border: 1px solid var(--border);
|
|
88
|
+
border-radius: var(--radius-lg);
|
|
89
|
+
padding: 16px 20px;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Modal backdrop
|
|
93
|
+
```css
|
|
94
|
+
background: var(--overlay-bg);
|
|
95
|
+
backdrop-filter: var(--blur);
|
|
96
|
+
-webkit-backdrop-filter: var(--blur);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Keyboard key labels (in help overlay)
|
|
100
|
+
```html
|
|
101
|
+
<span class="highlight">X</span>
|
|
102
|
+
```
|
|
103
|
+
CSS: `color: var(--help-key); font-weight: bold;`
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Do / Don't
|
|
108
|
+
|
|
109
|
+
| Do | Don't |
|
|
110
|
+
|----|-------|
|
|
111
|
+
| Use `var(--active-dim)` for active/selected state | Hardcode `#f5c842` |
|
|
112
|
+
| Keep new controls hidden by default, shown on interaction | Add persistent toolbar buttons |
|
|
113
|
+
| Use `opacity` or `color: var(--muted)` for inactive state | Use `visibility: hidden` (breaks layout) |
|
|
114
|
+
| Use `transition: opacity 0.15s` for hover reveals | Animate position/size (janky on canvas) |
|
|
115
|
+
| Test all four themes with `T` key | Assume dark theme only |
|
|
116
|
+
| Match existing font sizes | Introduce new size values |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Checklist Before Shipping a UI Change
|
|
121
|
+
|
|
122
|
+
- [ ] Tested in all four themes (dark, light, solarized, nord) with `T` key
|
|
123
|
+
- [ ] No hardcoded color values — all via `var(--...)`
|
|
124
|
+
- [ ] Font is monospace, size matches existing scale
|
|
125
|
+
- [ ] New panel/overlay uses `--surface` + `--border` + `--radius-lg`
|
|
126
|
+
- [ ] `viewer-ui-checklist` skill followed (smoke test updated)
|
|
127
|
+
- [ ] `modes-consistency` skill followed if canvas/colorbar is involved
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: todo-workflow
|
|
3
|
+
description: Use when working through TODO items, implementing batches of features or fixes, or when the user gives multiple tasks at once. Enforces commit-per-item, collateral updates, and cross-mode verification.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TODO Workflow
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
When working through multiple tasks (TODO items, feature requests, bug fixes given together), follow these rules for every item. The TODO list lives in `dev/TODO.md`.
|
|
11
|
+
|
|
12
|
+
## Per-Item Rules
|
|
13
|
+
|
|
14
|
+
Each finished item gets:
|
|
15
|
+
1. **Its own commit** — one item, one commit, no batching
|
|
16
|
+
2. **Updated tests** — add/update test coverage for new functionality
|
|
17
|
+
3. **Updated README** — if user-facing behavior changed
|
|
18
|
+
4. **UI audit** — run ui-consistency-audit skill to verify across all modes
|
|
19
|
+
5. **Invocation check** — use invocation-consistency skill if touching server/startup/display-opening
|
|
20
|
+
6. **VS Code check** — use vscode-simplebrowser skill if touching extension install, signal-file IPC, or auto-open logic. This breaks often.
|
|
21
|
+
7. **Lessons learned** — update `dev/lessons_learned.md` with anything important for future sessions
|
|
22
|
+
|
|
23
|
+
## Execution Rules
|
|
24
|
+
|
|
25
|
+
- **Plan first** — write a plan before starting implementation
|
|
26
|
+
- **Subagents** — spawn them for independent items to parallelize work
|
|
27
|
+
- **Branching** — work on `main` unless parallelizing; if branches needed, rebase (no merge commits)
|
|
28
|
+
- **Compact context** — clear/compact between items to stay sharp
|
|
29
|
+
- **Skills** — use and update relevant skills, especially ui-consistency-audit
|
|
30
|
+
|
|
31
|
+
## Design Philosophy
|
|
32
|
+
|
|
33
|
+
The app is feature-rich but minimal — no clutter. Users should discover features and think: *"wait... it already does that?!"* Every feature should feel like a hidden gift, not visual noise.
|
|
34
|
+
|
|
35
|
+
## When User Says They're Going to Sleep
|
|
36
|
+
|
|
37
|
+
Don't ask for confirmation. Make your own decisions on remaining items. They expect to be impressed when they wake up.
|
|
@@ -256,7 +256,7 @@ Rules are enforced through multiple layers. When adding new rules, add enforceme
|
|
|
256
256
|
| **CSS** | `!important` structural constraints | `_viewer.html` CSS (line ~156) | Always — structurally impossible to violate |
|
|
257
257
|
| **Runtime JS** | `_validateUIState()` | `_viewer.html` JS (after `_positionFullscreenChrome`) | After every layout change, gated behind `?debug_ui=1` |
|
|
258
258
|
| **Playwright** | `run_invariant_assertions()` + `run_immersive_exit_assertions()` | `tests/ui_audit.py` | During `ui_audit.py` runs (all tiers) |
|
|
259
|
-
| **Skill** | This document (Section 5b) | `.
|
|
259
|
+
| **Skill** | This document (Section 5b) | `.Codex/skills/ui-consistency-audit/SKILL.md` | When AI agent invokes the skill |
|
|
260
260
|
|
|
261
261
|
**Cross-reference:**
|
|
262
262
|
|
|
@@ -37,7 +37,7 @@ Python package for interactively viewing multi-dimensional arrays (numpy, NIfTI,
|
|
|
37
37
|
|
|
38
38
|
## Commands
|
|
39
39
|
- Test: `uv run pytest tests/`
|
|
40
|
-
- Visual smoke: `uv run
|
|
40
|
+
- Visual smoke: `uv run python tests/visual_smoke.py`
|
|
41
41
|
- CLI: `uvx arrayview <file>`
|
|
42
42
|
- Build: `uv build`
|
|
43
43
|
|
|
@@ -52,7 +52,7 @@ pywebview, or system browser).
|
|
|
52
52
|
- **`_server.py`** — FastAPI app with all REST and WebSocket routes (`/meta/{sid}`, `/load`, `/slice`, `/ws/{sid}`, `/seg/*`, `/reload`, etc.). Dispatches render work to the render thread via `_render()` from `_session.py`.
|
|
53
53
|
- **`_session.py`** — Single source of global mutable state: `SESSIONS`, `SERVER_LOOP`, `VIEWER_SOCKETS`, `VIEWER_SIDS`, `SHELL_SOCKETS`. Owns the render thread (`_RENDER_QUEUE`, `_RENDER_THREAD`), prefetch pool, and the `Session` class with its three LRU caches.
|
|
54
54
|
- **`_render.py`** — Stateless rendering functions: `extract_slice()`, `apply_complex_mode()`, `render_rgba()`, `render_rgb_rgba()`, `render_mosaic()`, `extract_projection()`. Owns colormap LUTs (`LUTS` dict, lazy-initialized by `_init_luts()`).
|
|
55
|
-
- **`_io.py`** — All file-format loading behind `load_data(filepath)`. Lazy nibabel import for NIfTI. Handles `.npy`, `.npz`, `.nii
|
|
55
|
+
- **`_io.py`** — All file-format loading behind `load_data(filepath)`. Lazy nibabel import for NIfTI. Handles `.npy`, `.npz`, `.nii` and `.nii.gz`, `.zarr`, `.zarr.zip`, `.pt` and `.pth`, `.h5` and `.hdf5`, `.tif` and `.tiff`, `.mat`. Extensions registered in `_SUPPORTED_EXTS`.
|
|
56
56
|
- **`_platform.py`** — Environment detection: checks jupyter → vscode → julia → ssh → terminal in priority order. Results cached. Never short-circuit this order.
|
|
57
57
|
- **`_vscode.py`** — VS Code extension install/management, signal-file IPC, shared-memory IPC, webview panel and direct webview opening.
|
|
58
58
|
- **`_stdio_server.py`** — Alternative to FastAPI for VS Code tunnel (direct webview): JSON on stdin, length-prefixed binary on stdout.
|
|
@@ -77,7 +77,7 @@ Detection logic: `_platform.py`. Display opening: `_launcher.py` + `_vscode.py`.
|
|
|
77
77
|
- **nibabel** — NIfTI file loading. Lazy-imported in `_io.py` via `_nib()`. Only loaded for `.nii` / `.nii.gz`.
|
|
78
78
|
- **numpy** — Core array type throughout. The only non-lazy import in the render path.
|
|
79
79
|
- **matplotlib** — Colormap LUT generation only. Lazy, initialized once by `_init_luts()` in `_render.py`.
|
|
80
|
-
- **qmricolors** — Registers the `lipari` and `navia` colormaps. Git dependency (`oscarvanderheide/qmricolors`).
|
|
80
|
+
- **qmricolors** — Registers the `lipari` and `navia` colormaps. Git dependency (`https://github.com/oscarvanderheide/qmricolors.git`).
|
|
81
81
|
- **zarr** — Lazy chunk access for `.zarr` / `.zarr.zip`. Chunk presets via `zarr_chunk_preset()` in `_session.py`.
|
|
82
82
|
- **pywebview** — Native OS window. Lazy, only started when `_can_native_window()` is true.
|
|
83
83
|
|
|
@@ -7,7 +7,7 @@ triggers:
|
|
|
7
7
|
- "recent work"
|
|
8
8
|
- "active feature"
|
|
9
9
|
- "shipped recently"
|
|
10
|
-
last_updated: 2026-04-
|
|
10
|
+
last_updated: 2026-04-24
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Project State
|
|
@@ -21,7 +21,7 @@ last_updated: 2026-04-17
|
|
|
21
21
|
- NIfTI spatial metadata, RAS resampling
|
|
22
22
|
- VS Code extension v0.14.5 — stable window ID via `EnvironmentVariableCollection`; `arrayview.openInFloatingWindow` setting moves new tabs to a floating window; `view(arr, floating=True)` and `arrayview file.npy --floating` open in a floating window per-call regardless of global setting; `!vscode.env.remoteName` guard removed (remote VS Code supports floating windows); floating mode now uses a single persistent shell hub panel (`_shell.html`) so all arrays share one floating window as tabs instead of opening separate windows; fixed: second CLI call now injects tab via `new_tab` postMessage relay (extension -> hub wrapper -> shell iframe) instead of relying on WebSocket notify which wasn't sent in VS Code mode
|
|
23
23
|
- Colorbar refactor: `ColorBar` JS class partially migrated (in progress)
|
|
24
|
-
-
|
|
24
|
+
- Colormap picker: `c` opens an expanded colorbar-island grid without changing the colormap; subsequent `c` taps cycle, hover/hjkl/arrows live-preview, Enter/click commits, Esc cancels, and auto-dismiss pauses while hovered
|
|
25
25
|
- Cold-start loading spinner in VS Code and native shell
|
|
26
26
|
- Plugin shelf (`/` menu) supports multi-select: spacebar toggles plugins, Enter applies selection. Mutual exclusion enforced (ROI ↔ Segmentation, and overlay/vectorfield ↔ everything else). Cursor indicator shows focused tile via yellow background + left accent bar
|
|
27
27
|
- Dynamic island renders sections for all active plugins simultaneously (qMRI pills + ROI shapes/stats separated by divider), replacing the old single-plugin priority chain
|
|
@@ -42,4 +42,4 @@ last_updated: 2026-04-17
|
|
|
42
42
|
## Not Yet Built
|
|
43
43
|
|
|
44
44
|
- Independent split view for mismatched-shape arrays (designed, shelved)
|
|
45
|
-
- Admin/config UI (file-based
|
|
45
|
+
- Admin/config UI (design intent: file-based user config only, no in-app admin panel)
|
|
@@ -13,7 +13,7 @@ edges:
|
|
|
13
13
|
condition: when specific technology versions or library details are needed
|
|
14
14
|
- target: context/architecture.md
|
|
15
15
|
condition: when understanding how components connect during setup
|
|
16
|
-
last_updated: 2026-04-
|
|
16
|
+
last_updated: 2026-04-22
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
# Setup
|
|
@@ -28,7 +28,7 @@ last_updated: 2026-04-15
|
|
|
28
28
|
|
|
29
29
|
1. Clone the repo
|
|
30
30
|
2. `uv sync --all-groups` — installs all dependencies including dev and test groups
|
|
31
|
-
3. `uv run arrayview
|
|
31
|
+
3. `uv run arrayview debug/cig_meeting_examples/BraTS2021_00009_t1.nii.gz` — smoke test: should open the viewer with a sample array
|
|
32
32
|
4. `uv run pytest tests/test_mode_consistency.py` — verify core render consistency passes
|
|
33
33
|
|
|
34
34
|
For browser-based tests (playwright):
|
|
@@ -49,7 +49,7 @@ No `.env` file is needed. All env vars are optional overrides; the server runs w
|
|
|
49
49
|
- `uvx arrayview <file>` — launch from anywhere without activating the venv
|
|
50
50
|
- `uv run pytest tests/<target>` — run a specific test file
|
|
51
51
|
- `uv run pytest tests/test_mode_consistency.py` — mode consistency suite (run after render changes)
|
|
52
|
-
- `uv run
|
|
52
|
+
- `uv run python tests/visual_smoke.py` — browser smoke tests (requires playwright)
|
|
53
53
|
- `uv run pytest -m "not browser"` — all non-browser tests
|
|
54
54
|
- `uv build` — build wheel + sdist in `dist/`
|
|
55
55
|
|
|
@@ -14,7 +14,7 @@ edges:
|
|
|
14
14
|
condition: when understanding how to use a technology in this codebase
|
|
15
15
|
- target: context/architecture.md
|
|
16
16
|
condition: when understanding how a library fits into the overall system
|
|
17
|
-
last_updated: 2026-04-
|
|
17
|
+
last_updated: 2026-04-22
|
|
18
18
|
---
|
|
19
19
|
|
|
20
20
|
# Stack
|
|
@@ -34,7 +34,7 @@ last_updated: 2026-04-15
|
|
|
34
34
|
- **zarr 2.17+** — lazy chunk access for `.zarr` / `.zarr.zip` files; chunk preset utility in `_session.py`.
|
|
35
35
|
- **pillow 12+** — PNG encoding for slice frames sent over WebSocket.
|
|
36
36
|
- **pywebview 6.1+** — native OS window for CLI / script invocations; lazy, only started when `_can_native_window()` is true.
|
|
37
|
-
- **qmricolors** — registers `lipari` and `navia` colormaps into matplotlib; Git dependency (`oscarvanderheide/qmricolors`). Imported inside `_init_luts()`.
|
|
37
|
+
- **qmricolors** — registers `lipari` and `navia` colormaps into matplotlib; Git dependency (`https://github.com/oscarvanderheide/qmricolors.git`). Imported inside `_init_luts()`.
|
|
38
38
|
- **scipy** — `.mat` file loading via `scipy.io.loadmat`; lazy in `_io.py`.
|
|
39
39
|
- **h5py** — `.h5` / `.hdf5` file loading; lazy in `_io.py`.
|
|
40
40
|
- **tifffile** — `.tif` / `.tiff` file loading; lazy in `_io.py`.
|
|
@@ -196,6 +196,24 @@
|
|
|
196
196
|
"label": "ArrayView",
|
|
197
197
|
"onAutoForward": "silent",
|
|
198
198
|
"privacy": "public"
|
|
199
|
+
},
|
|
200
|
+
"8777": {
|
|
201
|
+
"protocol": "http",
|
|
202
|
+
"label": "ArrayView",
|
|
203
|
+
"onAutoForward": "silent",
|
|
204
|
+
"privacy": "public"
|
|
205
|
+
},
|
|
206
|
+
"8778": {
|
|
207
|
+
"protocol": "http",
|
|
208
|
+
"label": "ArrayView",
|
|
209
|
+
"onAutoForward": "silent",
|
|
210
|
+
"privacy": "public"
|
|
211
|
+
},
|
|
212
|
+
"8779": {
|
|
213
|
+
"protocol": "http",
|
|
214
|
+
"label": "ArrayView",
|
|
215
|
+
"onAutoForward": "silent",
|
|
216
|
+
"privacy": "public"
|
|
199
217
|
}
|
|
200
218
|
},
|
|
201
219
|
"search.exclude": {
|
|
@@ -8,6 +8,7 @@ set -euo pipefail
|
|
|
8
8
|
BUMP="minor"
|
|
9
9
|
DRY_RUN=true
|
|
10
10
|
NO_AI=false
|
|
11
|
+
AI_TOOL="claude"
|
|
11
12
|
|
|
12
13
|
usage() {
|
|
13
14
|
cat <<EOF
|
|
@@ -15,6 +16,7 @@ Usage: $(basename "$0") [OPTIONS]
|
|
|
15
16
|
|
|
16
17
|
Options:
|
|
17
18
|
--bump {major,minor,patch} Version bump type (default: minor)
|
|
19
|
+
--ai {claude,codex} AI tool for release notes (default: claude)
|
|
18
20
|
--execute Actually run (default is dry-run)
|
|
19
21
|
--no-ai Skip AI release notes, use GitHub's --generate-notes
|
|
20
22
|
-h, --help Show this help
|
|
@@ -24,6 +26,7 @@ EOF
|
|
|
24
26
|
while [[ $# -gt 0 ]]; do
|
|
25
27
|
case "$1" in
|
|
26
28
|
--bump) BUMP="$2"; shift 2 ;;
|
|
29
|
+
--ai) AI_TOOL="$2"; shift 2 ;;
|
|
27
30
|
--execute) DRY_RUN=false; shift ;;
|
|
28
31
|
--no-ai) NO_AI=true; shift ;;
|
|
29
32
|
-h|--help) usage; exit 0 ;;
|
|
@@ -36,6 +39,11 @@ if [[ ! "$BUMP" =~ ^(major|minor|patch)$ ]]; then
|
|
|
36
39
|
exit 1
|
|
37
40
|
fi
|
|
38
41
|
|
|
42
|
+
if [[ ! "$AI_TOOL" =~ ^(claude|codex)$ ]]; then
|
|
43
|
+
echo "Error: --ai must be claude or codex (got '$AI_TOOL')"
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
39
47
|
# --- Guard: clean working tree on main ---
|
|
40
48
|
branch=$(git rev-parse --abbrev-ref HEAD)
|
|
41
49
|
if [[ "$branch" != "main" ]]; then
|
|
@@ -67,6 +75,7 @@ run() {
|
|
|
67
75
|
|
|
68
76
|
# --- Generate release notes ---
|
|
69
77
|
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
|
|
78
|
+
CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
|
|
70
79
|
PREV_TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "")
|
|
71
80
|
NOTES=""
|
|
72
81
|
|
|
@@ -76,9 +85,8 @@ else
|
|
|
76
85
|
COMMITS=$(git log --oneline -20)
|
|
77
86
|
fi
|
|
78
87
|
|
|
79
|
-
if [[ "$NO_AI" == false ]]
|
|
80
|
-
|
|
81
|
-
NOTES=$("$CLAUDE_BIN" -p "You are writing release notes for arrayview $TAG (a Python array/image viewer).
|
|
88
|
+
if [[ "$NO_AI" == false ]]; then
|
|
89
|
+
PROMPT="You are writing release notes for arrayview $TAG (a Python array/image viewer).
|
|
82
90
|
|
|
83
91
|
Here are the commits since the last release ($PREV_TAG):
|
|
84
92
|
|
|
@@ -95,7 +103,26 @@ Rules:
|
|
|
95
103
|
- Write for end-users, not developers (no commit hashes, no file names)
|
|
96
104
|
- Use past tense (\"added\", \"fixed\", \"improved\")
|
|
97
105
|
- Skip pure refactors/docs unless they affect user experience
|
|
98
|
-
- Bold the feature name, keep the description to one sentence"
|
|
106
|
+
- Bold the feature name, keep the description to one sentence"
|
|
107
|
+
|
|
108
|
+
case "$AI_TOOL" in
|
|
109
|
+
claude)
|
|
110
|
+
if command -v "$CLAUDE_BIN" &>/dev/null; then
|
|
111
|
+
echo "Generating release notes with Claude..."
|
|
112
|
+
NOTES=$("$CLAUDE_BIN" -p "$PROMPT" 2>/dev/null) || true
|
|
113
|
+
fi
|
|
114
|
+
;;
|
|
115
|
+
codex)
|
|
116
|
+
if command -v "$CODEX_BIN" &>/dev/null; then
|
|
117
|
+
echo "Generating release notes with Codex..."
|
|
118
|
+
tmpfile=$(mktemp)
|
|
119
|
+
trap 'rm -f "$tmpfile"' EXIT
|
|
120
|
+
if printf '%s\n' "$PROMPT" | "$CODEX_BIN" exec --output-last-message "$tmpfile" - >/dev/null 2>/dev/null; then
|
|
121
|
+
NOTES=$(<"$tmpfile")
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
;;
|
|
125
|
+
esac
|
|
99
126
|
fi
|
|
100
127
|
|
|
101
128
|
if [[ -z "$NOTES" ]]; then
|
|
@@ -56,6 +56,24 @@ def get_viewer_theme() -> str | None:
|
|
|
56
56
|
return None
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def get_viewer_rounded_panes() -> bool | None:
|
|
60
|
+
"""Return user-configured default for rounded panes, or None if not configured."""
|
|
61
|
+
cfg = load_config()
|
|
62
|
+
viewer_cfg = cfg.get("viewer", {})
|
|
63
|
+
if not isinstance(viewer_cfg, dict):
|
|
64
|
+
return None
|
|
65
|
+
val = viewer_cfg.get("rounded_panes")
|
|
66
|
+
if isinstance(val, bool):
|
|
67
|
+
return val
|
|
68
|
+
if isinstance(val, str):
|
|
69
|
+
s = val.strip().lower()
|
|
70
|
+
if s in ("true", "1", "yes", "on"):
|
|
71
|
+
return True
|
|
72
|
+
if s in ("false", "0", "no", "off"):
|
|
73
|
+
return False
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
59
77
|
def get_nninteractive_url() -> str | None:
|
|
60
78
|
"""Return configured nnInteractive server URL, or None.
|
|
61
79
|
|
|
@@ -2504,6 +2504,37 @@ def arrayview():
|
|
|
2504
2504
|
print(f"[ArrayView] Killed process {pid} on port {args.port}")
|
|
2505
2505
|
except ProcessLookupError:
|
|
2506
2506
|
pass
|
|
2507
|
+
deadline = time.time() + 1.0
|
|
2508
|
+
while time.time() < deadline:
|
|
2509
|
+
alive = []
|
|
2510
|
+
for pid in pids:
|
|
2511
|
+
try:
|
|
2512
|
+
os.kill(pid, 0)
|
|
2513
|
+
alive.append(pid)
|
|
2514
|
+
except ProcessLookupError:
|
|
2515
|
+
pass
|
|
2516
|
+
if not alive:
|
|
2517
|
+
break
|
|
2518
|
+
time.sleep(0.05)
|
|
2519
|
+
else:
|
|
2520
|
+
for pid in alive:
|
|
2521
|
+
try:
|
|
2522
|
+
os.kill(pid, _signal.SIGKILL)
|
|
2523
|
+
print(f"[ArrayView] Force-killed process {pid} on port {args.port}")
|
|
2524
|
+
except ProcessLookupError:
|
|
2525
|
+
pass
|
|
2526
|
+
deadline = time.time() + 2.0
|
|
2527
|
+
while time.time() < deadline:
|
|
2528
|
+
alive = []
|
|
2529
|
+
for pid in pids:
|
|
2530
|
+
try:
|
|
2531
|
+
os.kill(pid, 0)
|
|
2532
|
+
alive.append(pid)
|
|
2533
|
+
except ProcessLookupError:
|
|
2534
|
+
pass
|
|
2535
|
+
if not alive:
|
|
2536
|
+
break
|
|
2537
|
+
time.sleep(0.05)
|
|
2507
2538
|
return
|
|
2508
2539
|
|
|
2509
2540
|
# -- --serve: start a persistent empty server and exit --
|
|
@@ -74,7 +74,7 @@ from arrayview._render import (
|
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
from arrayview._io import load_data, _SUPPORTED_EXTS, _peek_file_shape
|
|
77
|
-
from arrayview._config import get_viewer_colormaps, get_viewer_theme
|
|
77
|
+
from arrayview._config import get_viewer_colormaps, get_viewer_rounded_panes, get_viewer_theme
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
# ── Vector Field Helpers ──────────────────────────────────────────
|
|
@@ -1025,6 +1025,7 @@ def _build_metadata(session) -> dict:
|
|
|
1025
1025
|
"has_vectorfield": session.vfield is not None,
|
|
1026
1026
|
"vfield_n_times": _vfield_n_times(session),
|
|
1027
1027
|
"is_rgb": session.rgb_axis is not None,
|
|
1028
|
+
"has_source_file": bool(getattr(session, "filepath", None)),
|
|
1028
1029
|
}
|
|
1029
1030
|
target_shape = (
|
|
1030
1031
|
session.spatial_shape if session.rgb_axis is not None else session.shape
|
|
@@ -2368,34 +2369,19 @@ def get_lebesgue_slice(
|
|
|
2368
2369
|
)
|
|
2369
2370
|
|
|
2370
2371
|
|
|
2371
|
-
@app.get("/
|
|
2372
|
-
def
|
|
2372
|
+
@app.get("/export_array/{sid}")
|
|
2373
|
+
def export_array(
|
|
2373
2374
|
sid: str,
|
|
2374
|
-
dim_x: int,
|
|
2375
|
-
dim_y: int,
|
|
2376
|
-
indices: str,
|
|
2377
|
-
complex_mode: int = 0,
|
|
2378
2375
|
save_to_downloads: int = 0,
|
|
2379
2376
|
session: "Session" = Depends(get_session_or_404),
|
|
2380
2377
|
):
|
|
2381
|
-
"""Return the
|
|
2382
|
-
|
|
2383
|
-
The slice is the raw floating-point data (before colormap/LUT), with the
|
|
2384
|
-
complex mode applied (mag/phase/real/imag). Used by the N-key shortcut.
|
|
2385
|
-
|
|
2386
|
-
When save_to_downloads=1 (PyWebView), saves the file directly to ~/Downloads
|
|
2387
|
-
instead of returning it as a download response.
|
|
2388
|
-
"""
|
|
2389
|
-
idx_tuple = tuple(int(v) for v in indices.split(","))
|
|
2390
|
-
raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
|
|
2391
|
-
data = apply_complex_mode(raw, complex_mode)
|
|
2378
|
+
"""Return the full N-D array as a downloadable .npy file (raw data, no transforms)."""
|
|
2379
|
+
data = np.asarray(session.data)
|
|
2392
2380
|
buf = io.BytesIO()
|
|
2393
2381
|
np.save(buf, data)
|
|
2394
2382
|
buf.seek(0)
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
idx_str = "_".join(str(v) for v in idx_tuple)
|
|
2398
|
-
filename = f"{name_stem}_x{dim_x}_y{dim_y}_{idx_str}.npy"
|
|
2383
|
+
name_stem = (session.name or "array").replace(" ", "_").replace("/", "_")
|
|
2384
|
+
filename = f"{name_stem}.npy"
|
|
2399
2385
|
if save_to_downloads:
|
|
2400
2386
|
import pathlib
|
|
2401
2387
|
downloads = pathlib.Path.home() / "Downloads"
|
|
@@ -4271,6 +4257,8 @@ def get_ui(sid: str = None):
|
|
|
4271
4257
|
_theme_names = ["dark", "light", "solarized", "nord"]
|
|
4272
4258
|
_cfg_theme = get_viewer_theme()
|
|
4273
4259
|
_default_theme_idx = _theme_names.index(_cfg_theme) if _cfg_theme in _theme_names else 0
|
|
4260
|
+
_cfg_rounded = get_viewer_rounded_panes()
|
|
4261
|
+
_default_rounded_panes = "true" if _cfg_rounded else "false"
|
|
4274
4262
|
html = (
|
|
4275
4263
|
_VIEWER_HTML_TEMPLATE.replace("__COLORMAPS__", str(_active_colormaps))
|
|
4276
4264
|
.replace("__COLORMAP_GRADIENT_STOPS__", json.dumps(COLORMAP_GRADIENT_STOPS))
|
|
@@ -4278,6 +4266,7 @@ def get_ui(sid: str = None):
|
|
|
4278
4266
|
.replace("__REAL_MODES__", str(REAL_MODES))
|
|
4279
4267
|
.replace("__ARRAYVIEW_QUERY__", query_val)
|
|
4280
4268
|
.replace("__DEFAULT_THEME_IDX__", str(_default_theme_idx))
|
|
4269
|
+
.replace("__DEFAULT_ROUNDED_PANES__", _default_rounded_panes)
|
|
4281
4270
|
.replace("__BODY_CLASS__", "av-loading" if sid else "")
|
|
4282
4271
|
)
|
|
4283
4272
|
headers = {"Cache-Control": "no-store"}
|
|
@@ -943,7 +943,7 @@ def _handle_get_viewer_html(msg: dict) -> None:
|
|
|
943
943
|
"""Return the rendered viewer HTML with template substitutions."""
|
|
944
944
|
from importlib.resources import files as _pkg_files
|
|
945
945
|
|
|
946
|
-
from arrayview._config import get_viewer_colormaps, get_viewer_theme
|
|
946
|
+
from arrayview._config import get_viewer_colormaps, get_viewer_rounded_panes, get_viewer_theme
|
|
947
947
|
from arrayview._render import COLORMAP_GRADIENT_STOPS, COMPLEX_MODES, REAL_MODES
|
|
948
948
|
from arrayview._session import COLORMAPS
|
|
949
949
|
|
|
@@ -980,6 +980,8 @@ def _handle_get_viewer_html(msg: dict) -> None:
|
|
|
980
980
|
_theme_names = ["dark", "light", "solarized", "nord"]
|
|
981
981
|
_cfg_theme = get_viewer_theme()
|
|
982
982
|
_default_theme_idx = _theme_names.index(_cfg_theme) if _cfg_theme in _theme_names else 0
|
|
983
|
+
_cfg_rounded = get_viewer_rounded_panes()
|
|
984
|
+
_default_rounded_panes = "true" if _cfg_rounded else "false"
|
|
983
985
|
|
|
984
986
|
html = (
|
|
985
987
|
template.replace("__COLORMAPS__", str(_active_colormaps))
|
|
@@ -988,6 +990,7 @@ def _handle_get_viewer_html(msg: dict) -> None:
|
|
|
988
990
|
.replace("__REAL_MODES__", str(REAL_MODES))
|
|
989
991
|
.replace("__ARRAYVIEW_QUERY__", query_val)
|
|
990
992
|
.replace("__DEFAULT_THEME_IDX__", str(_default_theme_idx))
|
|
993
|
+
.replace("__DEFAULT_ROUNDED_PANES__", _default_rounded_panes)
|
|
991
994
|
.replace("__BODY_CLASS__", "av-loading" if sid else "")
|
|
992
995
|
)
|
|
993
996
|
|