arrayview 0.17.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.
Files changed (103) hide show
  1. arrayview-0.19.0/.agents/skills/frontend-designer/SKILL.md +127 -0
  2. arrayview-0.19.0/.agents/skills/todo-workflow/SKILL.md +37 -0
  3. {arrayview-0.17.0/.claude → arrayview-0.19.0/.agents}/skills/ui-consistency-audit/SKILL.md +1 -1
  4. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/ROUTER.md +1 -1
  5. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/architecture.md +2 -2
  6. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/project-state.md +3 -3
  7. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/setup.md +3 -3
  8. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/stack.md +2 -2
  9. {arrayview-0.17.0 → arrayview-0.19.0}/.vscode/settings.json +18 -0
  10. {arrayview-0.17.0 → arrayview-0.19.0}/PKG-INFO +1 -1
  11. {arrayview-0.17.0 → arrayview-0.19.0}/docs/display.md +1 -1
  12. {arrayview-0.17.0 → arrayview-0.19.0}/pyproject.toml +1 -1
  13. {arrayview-0.17.0 → arrayview-0.19.0}/scripts/release.sh +31 -4
  14. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_config.py +18 -0
  15. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_launcher.py +31 -0
  16. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_server.py +62 -38
  17. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_session.py +1 -0
  18. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_stdio_server.py +60 -13
  19. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_viewer.html +2235 -743
  20. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_api.py +0 -33
  21. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_browser.py +170 -19
  22. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_command_reachability.py +1 -0
  23. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_interactions.py +181 -80
  24. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_mode_consistency.py +4 -3
  25. arrayview-0.19.0/tests/test_mode_entry_batching.py +63 -0
  26. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_mode_roundtrip.py +20 -6
  27. {arrayview-0.17.0 → arrayview-0.19.0}/tests/visual_smoke.py +60 -57
  28. {arrayview-0.17.0 → arrayview-0.19.0}/uv.lock +1 -1
  29. arrayview-0.17.0/IMMERSIVE_ANIMATION.md +0 -43
  30. {arrayview-0.17.0/.claude → arrayview-0.19.0/.agents}/skills/invocation-consistency/SKILL.md +0 -0
  31. {arrayview-0.17.0/.claude → arrayview-0.19.0/.agents}/skills/modes-consistency/SKILL.md +0 -0
  32. {arrayview-0.17.0/.claude → arrayview-0.19.0/.agents}/skills/viewer-ui-checklist/SKILL.md +0 -0
  33. {arrayview-0.17.0/.claude → arrayview-0.19.0/.agents}/skills/visual-bug-fixing/SKILL.md +0 -0
  34. {arrayview-0.17.0 → arrayview-0.19.0}/.github/copilot-instructions.md +0 -0
  35. {arrayview-0.17.0 → arrayview-0.19.0}/.github/workflows/docs.yml +0 -0
  36. {arrayview-0.17.0 → arrayview-0.19.0}/.github/workflows/python-publish.yml +0 -0
  37. {arrayview-0.17.0 → arrayview-0.19.0}/.gitignore +0 -0
  38. {arrayview-0.17.0 → arrayview-0.19.0}/.ignore +0 -0
  39. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/AGENTS.md +0 -0
  40. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/SETUP.md +0 -0
  41. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/SYNC.md +0 -0
  42. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/conventions.md +0 -0
  43. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/decisions.md +0 -0
  44. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/frontend.md +0 -0
  45. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/context/render-pipeline.md +0 -0
  46. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/INDEX.md +0 -0
  47. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/README.md +0 -0
  48. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/add-file-format.md +0 -0
  49. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/add-server-endpoint.md +0 -0
  50. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/debug-render.md +0 -0
  51. {arrayview-0.17.0 → arrayview-0.19.0}/.mex/patterns/frontend-change.md +0 -0
  52. {arrayview-0.17.0 → arrayview-0.19.0}/.opencode/opencode.json +0 -0
  53. {arrayview-0.17.0 → arrayview-0.19.0}/.python-version +0 -0
  54. {arrayview-0.17.0 → arrayview-0.19.0}/AGENTS.md +0 -0
  55. {arrayview-0.17.0 → arrayview-0.19.0}/CONTRIBUTING.md +0 -0
  56. {arrayview-0.17.0 → arrayview-0.19.0}/LICENSE +0 -0
  57. {arrayview-0.17.0 → arrayview-0.19.0}/README.md +0 -0
  58. {arrayview-0.17.0 → arrayview-0.19.0}/docs/comparing.md +0 -0
  59. {arrayview-0.17.0 → arrayview-0.19.0}/docs/configuration.md +0 -0
  60. {arrayview-0.17.0 → arrayview-0.19.0}/docs/index.md +0 -0
  61. {arrayview-0.17.0 → arrayview-0.19.0}/docs/loading.md +0 -0
  62. {arrayview-0.17.0 → arrayview-0.19.0}/docs/logo.png +0 -0
  63. {arrayview-0.17.0 → arrayview-0.19.0}/docs/measurement.md +0 -0
  64. {arrayview-0.17.0 → arrayview-0.19.0}/docs/remote.md +0 -0
  65. {arrayview-0.17.0 → arrayview-0.19.0}/docs/stylesheets/extra.css +0 -0
  66. {arrayview-0.17.0 → arrayview-0.19.0}/docs/viewing.md +0 -0
  67. {arrayview-0.17.0 → arrayview-0.19.0}/matlab/arrayview.m +0 -0
  68. {arrayview-0.17.0 → arrayview-0.19.0}/mkdocs.yml +0 -0
  69. {arrayview-0.17.0 → arrayview-0.19.0}/plans/2026-04-14-immersive-animation.md +0 -0
  70. {arrayview-0.17.0 → arrayview-0.19.0}/plans/webview/LOG.md +0 -0
  71. {arrayview-0.17.0 → arrayview-0.19.0}/scripts/demo.py +0 -0
  72. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/ARCHITECTURE.md +0 -0
  73. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/__init__.py +0 -0
  74. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/__main__.py +0 -0
  75. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_app.py +0 -0
  76. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_icon.png +0 -0
  77. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_io.py +0 -0
  78. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_platform.py +0 -0
  79. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_render.py +0 -0
  80. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_segmentation.py +0 -0
  81. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_shell.html +0 -0
  82. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_torch.py +0 -0
  83. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/_vscode.py +0 -0
  84. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/arrayview-opener.vsix +0 -0
  85. {arrayview-0.17.0 → arrayview-0.19.0}/src/arrayview/gsap.min.js +0 -0
  86. {arrayview-0.17.0 → arrayview-0.19.0}/tests/conftest.py +0 -0
  87. {arrayview-0.17.0 → arrayview-0.19.0}/tests/make_vectorfield_test_arrays.py +0 -0
  88. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_cli.py +0 -0
  89. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_config.py +0 -0
  90. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_cross_mode_parametrized.py +0 -0
  91. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_large_arrays.py +0 -0
  92. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_loading_server.py +0 -0
  93. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_mode_matrix.py +0 -0
  94. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_nifti_meta.py +0 -0
  95. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_rgb_pixel_art.py +0 -0
  96. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_torch.py +0 -0
  97. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_view_component_integration.py +0 -0
  98. {arrayview-0.17.0 → arrayview-0.19.0}/tests/test_view_component_unit.py +0 -0
  99. {arrayview-0.17.0 → arrayview-0.19.0}/tests/ui_audit.py +0 -0
  100. {arrayview-0.17.0 → arrayview-0.19.0}/vscode-extension/AGENTS.md +0 -0
  101. {arrayview-0.17.0 → arrayview-0.19.0}/vscode-extension/LICENSE +0 -0
  102. {arrayview-0.17.0 → arrayview-0.19.0}/vscode-extension/extension.js +0 -0
  103. {arrayview-0.17.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) | `.claude/skills/ui-consistency-audit/SKILL.md` | When AI agent invokes the skill |
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 pytest tests/visual_smoke.py`
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/.nii.gz`, `.zarr`, `.zarr.zip`, `.pt/.pth`, `.h5/.hdf5`, `.tif/.tiff`, `.mat`. Extensions registered in `_SUPPORTED_EXTS`.
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-17
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
- - Colorbar island flip: `c` and `d` keys trigger 3D `rotateX` card flip (front=colorbar, back=cmap thumbnails/histogram)
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 `~/.arrayview/config.toml` only)
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-15
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 dev/sample.npy` — smoke test: should open the viewer with a sample array
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 pytest tests/visual_smoke.py` — browser smoke tests (requires playwright)
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-15
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": {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.17.0
3
+ Version: 0.19.0
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Colormaps
4
4
 
5
- `c` cycles through defaults: gray, lipari, navia, viridis, plasma, and more.
5
+ `c` opens the colormap grid. Press `c` again to cycle; `Enter` accepts, `Esc` cancels.
6
6
  `C` enters any matplotlib colormap by name.
7
7
 
8
8
  ## Dynamic range
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.17.0"
7
+ version = "0.19.0"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -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 ]] && command -v "$CLAUDE_BIN" &>/dev/null; then
80
- echo "Generating release notes with Claude..."
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" 2>/dev/null) || true
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
@@ -2130,21 +2131,24 @@ def get_volume_histogram(
2130
2131
  sid: str,
2131
2132
  dim_x: int,
2132
2133
  dim_y: int,
2133
- scroll_dim: int,
2134
+ scroll_dim: int = -1,
2135
+ scroll_dims: str = "",
2134
2136
  fixed_indices: str = "",
2135
2137
  complex_mode: int = 0,
2136
2138
  bins: int = 64,
2137
2139
  session: "Session" = Depends(get_session_or_404),
2138
2140
  ):
2139
- """Return a histogram sampled across the scroll dimension.
2141
+ """Return a histogram sampled across one or more aggregation dims.
2140
2142
 
2141
- Subsamples up to 16 evenly-spaced slices along *scroll_dim*, merges
2142
- their pixel data, and returns a single histogram. The result is
2143
- cached on the session so repeated requests are instant.
2143
+ *scroll_dims* is a comma-separated list of dim indices to aggregate
2144
+ over (in addition to *dim_x* / *dim_y*). When unset, falls back to
2145
+ the legacy single *scroll_dim* param. The sampler enumerates index
2146
+ combinations across all aggregation dims but caps total samples at
2147
+ ~16 via stride-subsampling.
2144
2148
 
2145
2149
  *fixed_indices* is a comma-separated list of ``dim:idx`` pairs that
2146
- pin non-display, non-scroll dimensions (e.g. ``"3:0"`` to select the
2147
- first parameter map in qMRI mode).
2150
+ pin non-display, non-aggregation dimensions (e.g. ``"3:0"`` to
2151
+ select the first parameter map in qMRI mode).
2148
2152
  """
2149
2153
 
2150
2154
  # Parse fixed indices
@@ -2155,27 +2159,59 @@ def get_volume_histogram(
2155
2159
  d, v = pair.split(":", 1)
2156
2160
  fixed[int(d)] = int(v)
2157
2161
 
2162
+ # Resolve aggregation dims: prefer scroll_dims, fall back to scroll_dim.
2163
+ agg_dims: list[int] = []
2164
+ if scroll_dims:
2165
+ for tok in scroll_dims.split(","):
2166
+ tok = tok.strip()
2167
+ if not tok:
2168
+ continue
2169
+ try:
2170
+ d = int(tok)
2171
+ except ValueError:
2172
+ continue
2173
+ if 0 <= d < len(session.shape) and d != dim_x and d != dim_y and d not in agg_dims:
2174
+ agg_dims.append(d)
2175
+ if not agg_dims and scroll_dim >= 0 and scroll_dim != dim_x and scroll_dim != dim_y:
2176
+ agg_dims = [scroll_dim]
2177
+
2158
2178
  # Check cache
2159
- cache_key = (dim_x, dim_y, scroll_dim, tuple(sorted(fixed.items())), complex_mode)
2179
+ cache_key = (dim_x, dim_y, tuple(agg_dims), tuple(sorted(fixed.items())), complex_mode)
2160
2180
  if not hasattr(session, "_volume_hist_cache"):
2161
2181
  session._volume_hist_cache = {}
2162
2182
  cached = session._volume_hist_cache.get(cache_key)
2163
2183
  if cached is not None and cached.get("_data_version") == session.data_version:
2164
2184
  return cached["result"]
2165
2185
 
2166
- # Sample slices along scroll_dim
2167
- n = session.shape[scroll_dim]
2186
+ # Build sample index lists per aggregation dim, stride-subsampled so
2187
+ # their Cartesian product stays around ~16 samples total.
2168
2188
  max_samples = 16
2169
- if n <= max_samples:
2170
- sample_indices = list(range(n))
2189
+ if not agg_dims:
2190
+ # No aggregation dims — use the current middle slice only.
2191
+ sample_combos = [tuple()]
2171
2192
  else:
2172
- step = n / max_samples
2173
- sample_indices = [int(i * step) for i in range(max_samples)]
2193
+ per_dim_counts = [session.shape[d] for d in agg_dims]
2194
+ # Target roughly equal sample count per dim so the product ≈ max_samples.
2195
+ # Ceil to avoid zero; clamp to dim size.
2196
+ k = len(agg_dims)
2197
+ per_dim_target = max(1, int(round(max_samples ** (1.0 / k))))
2198
+ sample_per_dim: list[list[int]] = []
2199
+ for n in per_dim_counts:
2200
+ m = min(n, per_dim_target)
2201
+ if n <= m:
2202
+ sample_per_dim.append(list(range(n)))
2203
+ else:
2204
+ step = n / m
2205
+ sample_per_dim.append([int(i * step) for i in range(m)])
2206
+ # Cartesian product with a hard cap.
2207
+ import itertools as _it
2208
+ sample_combos = list(_it.islice(_it.product(*sample_per_dim), max_samples))
2174
2209
 
2175
2210
  pixels = []
2176
- for si in sample_indices:
2211
+ for combo in sample_combos:
2177
2212
  idx_list = [s // 2 for s in session.shape]
2178
- idx_list[scroll_dim] = si
2213
+ for d, si in zip(agg_dims, combo):
2214
+ idx_list[d] = si
2179
2215
  for d, v in fixed.items():
2180
2216
  idx_list[d] = v
2181
2217
  raw = extract_slice(session, dim_x, dim_y, idx_list)
@@ -2333,34 +2369,19 @@ def get_lebesgue_slice(
2333
2369
  )
2334
2370
 
2335
2371
 
2336
- @app.get("/export_slice/{sid}")
2337
- def export_slice(
2372
+ @app.get("/export_array/{sid}")
2373
+ def export_array(
2338
2374
  sid: str,
2339
- dim_x: int,
2340
- dim_y: int,
2341
- indices: str,
2342
- complex_mode: int = 0,
2343
2375
  save_to_downloads: int = 0,
2344
2376
  session: "Session" = Depends(get_session_or_404),
2345
2377
  ):
2346
- """Return the current 2-D slice as a downloadable .npy file.
2347
-
2348
- The slice is the raw floating-point data (before colormap/LUT), with the
2349
- complex mode applied (mag/phase/real/imag). Used by the N-key shortcut.
2350
-
2351
- When save_to_downloads=1 (PyWebView), saves the file directly to ~/Downloads
2352
- instead of returning it as a download response.
2353
- """
2354
- idx_tuple = tuple(int(v) for v in indices.split(","))
2355
- raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
2356
- 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)
2357
2380
  buf = io.BytesIO()
2358
2381
  np.save(buf, data)
2359
2382
  buf.seek(0)
2360
- # Build a suggested filename: sessionname_dim_x_dim_y_idx.npy
2361
- name_stem = (session.name or "slice").replace(" ", "_").replace("/", "_")
2362
- idx_str = "_".join(str(v) for v in idx_tuple)
2363
- 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"
2364
2385
  if save_to_downloads:
2365
2386
  import pathlib
2366
2387
  downloads = pathlib.Path.home() / "Downloads"
@@ -4236,6 +4257,8 @@ def get_ui(sid: str = None):
4236
4257
  _theme_names = ["dark", "light", "solarized", "nord"]
4237
4258
  _cfg_theme = get_viewer_theme()
4238
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"
4239
4262
  html = (
4240
4263
  _VIEWER_HTML_TEMPLATE.replace("__COLORMAPS__", str(_active_colormaps))
4241
4264
  .replace("__COLORMAP_GRADIENT_STOPS__", json.dumps(COLORMAP_GRADIENT_STOPS))
@@ -4243,6 +4266,7 @@ def get_ui(sid: str = None):
4243
4266
  .replace("__REAL_MODES__", str(REAL_MODES))
4244
4267
  .replace("__ARRAYVIEW_QUERY__", query_val)
4245
4268
  .replace("__DEFAULT_THEME_IDX__", str(_default_theme_idx))
4269
+ .replace("__DEFAULT_ROUNDED_PANES__", _default_rounded_panes)
4246
4270
  .replace("__BODY_CLASS__", "av-loading" if sid else "")
4247
4271
  )
4248
4272
  headers = {"Cache-Control": "no-store"}