arrayview 0.26.2__py3-none-any.whl → 0.27.0__py3-none-any.whl

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/_analysis.py CHANGED
@@ -67,7 +67,7 @@ def _safe_float(v) -> float | None:
67
67
  return f if np.isfinite(f) else None
68
68
 
69
69
 
70
- def _histogram_payload(finite: np.ndarray, bins: int) -> dict:
70
+ def _histogram_payload(finite: np.ndarray, bins: int, log_bins: bool = False) -> dict:
71
71
  """Build a histogram response from finite values."""
72
72
  if finite.size == 0:
73
73
  return {"counts": [], "edges": [], "vmin": 0.0, "vmax": 1.0}
@@ -81,7 +81,13 @@ def _histogram_payload(finite: np.ndarray, bins: int) -> dict:
81
81
  "vmax": vmax,
82
82
  }
83
83
  bins = max(8, min(int(bins), 512))
84
- counts, edges = np.histogram(finite, bins=bins)
84
+ if log_bins and vmin > 0:
85
+ log_data = np.log10(finite)
86
+ _, log_edges = np.histogram(log_data, bins=bins)
87
+ edges = np.power(10.0, log_edges)
88
+ counts, _ = np.histogram(finite, bins=edges)
89
+ else:
90
+ counts, edges = np.histogram(finite, bins=bins)
85
91
  return {
86
92
  "counts": counts.tolist(),
87
93
  "edges": [float(e) for e in edges],
@@ -113,7 +119,8 @@ def _slice_histogram(
113
119
  data = apply_complex_mode(raw, complex_mode)
114
120
  finite = data.ravel()
115
121
  finite = finite[np.isfinite(finite)]
116
- return _histogram_payload(finite, bins)
122
+ log_bins = qmri_role in ("t1", "t2")
123
+ return _histogram_payload(finite, bins, log_bins=log_bins)
117
124
 
118
125
 
119
126
  def _parse_fixed_indices(fixed_indices: str) -> dict[int, int]:
@@ -172,6 +179,7 @@ def _volume_histogram(
172
179
  session, dim_x, dim_y, scroll_dim, scroll_dims
173
180
  )
174
181
 
182
+ log_bins = qmri_role in ("t1", "t2")
175
183
  cache_key = (
176
184
  dim_x,
177
185
  dim_y,
@@ -179,6 +187,8 @@ def _volume_histogram(
179
187
  tuple(sorted(fixed.items())),
180
188
  complex_mode,
181
189
  qmri_role,
190
+ bins,
191
+ log_bins,
182
192
  )
183
193
  if not hasattr(session, "_volume_hist_cache"):
184
194
  session._volume_hist_cache = {}
@@ -224,7 +234,7 @@ def _volume_histogram(
224
234
  pixels.append(finite)
225
235
 
226
236
  if pixels:
227
- result = _histogram_payload(np.concatenate(pixels), bins)
237
+ result = _histogram_payload(np.concatenate(pixels), bins, log_bins=log_bins)
228
238
  else:
229
239
  result = {"counts": [], "edges": [], "vmin": 0.0, "vmax": 1.0}
230
240
  session._volume_hist_cache[cache_key] = {
@@ -241,14 +251,19 @@ def _lebesgue_slice(
241
251
  indices,
242
252
  complex_mode: int = 0,
243
253
  log_scale: bool = False,
254
+ qmri_role: str = "",
244
255
  ) -> np.ndarray:
245
256
  """Return float32 slice data for Lebesgue mode."""
257
+ from arrayview._synthetic_mri import normalize_relaxation_ms
258
+
246
259
  idx_tuple = (
247
260
  tuple(int(v) for v in indices.split(","))
248
261
  if isinstance(indices, str)
249
262
  else tuple(indices)
250
263
  )
251
264
  raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
265
+ if qmri_role:
266
+ raw = normalize_relaxation_ms(raw, qmri_role)
252
267
  data = apply_complex_mode(raw, complex_mode).astype(np.float32)
253
268
  if log_scale:
254
269
  data = np.log10(np.abs(data) + 1).astype(np.float32)
@@ -0,0 +1,285 @@
1
+ """Data-only launch routing primitives and environment snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from enum import Enum
7
+ import json
8
+ import os
9
+ import socket
10
+ import sys
11
+ import urllib.request
12
+
13
+ _LOOPBACK_HOST = "localhost"
14
+ _PING_TIMEOUT_SECONDS = 0.2
15
+
16
+ _SNAPSHOT_ENV_KEYS = (
17
+ "ARRAYVIEW_WINDOW",
18
+ "ARRAYVIEW_WINDOW_ID",
19
+ "DISPLAY",
20
+ "TERM_PROGRAM",
21
+ "VSCODE_AGENT_FOLDER",
22
+ "VSCODE_INJECTION",
23
+ "VSCODE_IPC_HOOK_CLI",
24
+ "WAYLAND_DISPLAY",
25
+ "SSH_CLIENT",
26
+ "SSH_CONNECTION",
27
+ )
28
+
29
+
30
+ class _StrEnum(str, Enum):
31
+ def __str__(self) -> str:
32
+ return self.value
33
+
34
+
35
+ class Invocation(_StrEnum):
36
+ CLI = "cli"
37
+ PYTHON = "python"
38
+ JUPYTER = "jupyter"
39
+ JULIA = "julia"
40
+ STDIO = "stdio"
41
+ CODEX = "codex"
42
+
43
+
44
+ class Environment(_StrEnum):
45
+ TERMINAL = "terminal"
46
+ VSCODE_LOCAL = "vscode_local"
47
+ VSCODE_REMOTE = "vscode_remote"
48
+ SSH = "ssh"
49
+ JUPYTER = "jupyter"
50
+ JULIA = "julia"
51
+
52
+
53
+ class Transport(_StrEnum):
54
+ HTTP = "http"
55
+ STDIO_FILE = "stdio_file"
56
+ STDIO_SHM = "stdio_shm"
57
+ NONE = "none"
58
+
59
+
60
+ class ServerOwner(_StrEnum):
61
+ EXISTING = "existing"
62
+ SPAWNED_DAEMON = "spawned_daemon"
63
+ IN_PROCESS = "in_process"
64
+ PERSISTENT = "persistent"
65
+ EXTERNAL = "external"
66
+
67
+
68
+ class Display(_StrEnum):
69
+ NATIVE = "native"
70
+ BROWSER = "browser"
71
+ VSCODE = "vscode"
72
+ INLINE = "inline"
73
+ NONE = "none"
74
+
75
+
76
+ class Registration(_StrEnum):
77
+ HTTP_LOAD = "http_load"
78
+ DAEMON_STARTUP = "daemon_startup"
79
+ IN_PROCESS_SESSION = "in_process_session"
80
+ STDIO_REGISTER = "stdio_register"
81
+ RELAY = "relay"
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class ServerSnapshot:
86
+ port: int
87
+ port_busy: bool
88
+ arrayview_server_alive: bool
89
+ server_pid: int | None = None
90
+ server_hostname: str | None = None
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class LaunchEnvironmentSnapshot:
95
+ invocation: Invocation
96
+ requested_window: str | None
97
+ environment: Environment
98
+ platform: str
99
+ env_vars: dict[str, str]
100
+ config_default: str | None
101
+ native_backend: str | None
102
+ server: ServerSnapshot
103
+ in_jupyter: bool
104
+ in_julia: bool
105
+ in_vscode_terminal: bool
106
+ is_vscode_remote: bool
107
+ in_vscode_tunnel: bool
108
+ ssh_connection: bool
109
+ ssh_client: bool
110
+ hostname: str
111
+
112
+ def to_dict(self) -> dict:
113
+ return asdict(self)
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class LaunchRequest:
118
+ port: int
119
+ requested_window: str | None = None
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class LaunchPlan:
124
+ invocation: Invocation
125
+ environment: Environment
126
+ transport: Transport
127
+ server_owner: ServerOwner
128
+ display: Display
129
+ registration: Registration
130
+ fallback_display: Display | None = None
131
+ fallback_allowed: bool = False
132
+
133
+
134
+ def snapshot_launch_environment(
135
+ port: int,
136
+ invocation: Invocation | str,
137
+ requested_window: str | None = None,
138
+ ) -> LaunchEnvironmentSnapshot:
139
+ """Capture launch-relevant facts without importing the server stack."""
140
+ inv = _coerce_invocation(invocation)
141
+ env_vars = _snapshot_env_vars()
142
+ in_jupyter = _platform_bool("_in_jupyter")
143
+ in_julia = _platform_bool("_is_julia_env")
144
+ in_vscode_terminal = _platform_bool("_in_vscode_terminal")
145
+ is_vscode_remote = _platform_bool("_is_vscode_remote")
146
+ in_vscode_tunnel = _platform_bool("_in_vscode_tunnel")
147
+ ssh_connection = bool(os.environ.get("SSH_CONNECTION"))
148
+ ssh_client = bool(os.environ.get("SSH_CLIENT"))
149
+ environment = _classify_environment(
150
+ in_jupyter=in_jupyter,
151
+ in_julia=in_julia,
152
+ in_vscode_terminal=in_vscode_terminal,
153
+ is_vscode_remote=is_vscode_remote,
154
+ ssh_connection=ssh_connection,
155
+ ssh_client=ssh_client,
156
+ )
157
+
158
+ return LaunchEnvironmentSnapshot(
159
+ invocation=inv,
160
+ requested_window=requested_window,
161
+ environment=environment,
162
+ platform=sys.platform,
163
+ env_vars=env_vars,
164
+ config_default=_config_window_default(environment.value),
165
+ native_backend=_native_window_gui(),
166
+ server=_server_snapshot(port),
167
+ in_jupyter=in_jupyter,
168
+ in_julia=in_julia,
169
+ in_vscode_terminal=in_vscode_terminal,
170
+ is_vscode_remote=is_vscode_remote,
171
+ in_vscode_tunnel=in_vscode_tunnel,
172
+ ssh_connection=ssh_connection,
173
+ ssh_client=ssh_client,
174
+ hostname=socket.gethostname(),
175
+ )
176
+
177
+
178
+ def _coerce_invocation(value: Invocation | str) -> Invocation:
179
+ if isinstance(value, Invocation):
180
+ return value
181
+ return Invocation(value)
182
+
183
+
184
+ def _snapshot_env_vars() -> dict[str, str]:
185
+ return {key: os.environ[key] for key in _SNAPSHOT_ENV_KEYS if key in os.environ}
186
+
187
+
188
+ def _platform_bool(name: str) -> bool:
189
+ try:
190
+ from arrayview import _platform
191
+
192
+ return bool(getattr(_platform, name)())
193
+ except Exception:
194
+ return False
195
+
196
+
197
+ def _native_window_gui() -> str | None:
198
+ try:
199
+ from arrayview._platform import _native_window_gui as native_window_gui
200
+
201
+ return native_window_gui()
202
+ except Exception:
203
+ return None
204
+
205
+
206
+ def _config_window_default(environment: str) -> str | None:
207
+ try:
208
+ from arrayview._config import load_config
209
+
210
+ cfg = load_config()
211
+ except Exception:
212
+ return None
213
+ window_cfg = cfg.get("window", {})
214
+ if not isinstance(window_cfg, dict):
215
+ return None
216
+ env_key = "vscode" if environment in {"vscode_local", "vscode_remote"} else environment
217
+ value = window_cfg.get(env_key) or window_cfg.get("default")
218
+ if isinstance(value, str):
219
+ return value.strip().lower() or None
220
+ return None
221
+
222
+
223
+ def _classify_environment(
224
+ *,
225
+ in_jupyter: bool,
226
+ in_julia: bool,
227
+ in_vscode_terminal: bool,
228
+ is_vscode_remote: bool,
229
+ ssh_connection: bool,
230
+ ssh_client: bool,
231
+ ) -> Environment:
232
+ if in_jupyter:
233
+ return Environment.JUPYTER
234
+ if in_julia:
235
+ return Environment.JULIA
236
+ if is_vscode_remote:
237
+ return Environment.VSCODE_REMOTE
238
+ if in_vscode_terminal:
239
+ return Environment.VSCODE_LOCAL
240
+ if ssh_connection or ssh_client:
241
+ return Environment.SSH
242
+ return Environment.TERMINAL
243
+
244
+
245
+ def _server_snapshot(port: int) -> ServerSnapshot:
246
+ payload = _ping_arrayview_server(port)
247
+ return ServerSnapshot(
248
+ port=port,
249
+ port_busy=_port_busy(port),
250
+ arrayview_server_alive=payload is not None,
251
+ server_pid=_int_or_none(payload.get("pid")) if payload else None,
252
+ server_hostname=_str_or_none(payload.get("hostname")) if payload else None,
253
+ )
254
+
255
+
256
+ def _port_busy(port: int) -> bool:
257
+ try:
258
+ with socket.create_connection(
259
+ (_LOOPBACK_HOST, port), timeout=_PING_TIMEOUT_SECONDS
260
+ ):
261
+ return True
262
+ except OSError:
263
+ return False
264
+
265
+
266
+ def _ping_arrayview_server(port: int) -> dict | None:
267
+ url = f"http://{_LOOPBACK_HOST}:{port}/ping"
268
+ try:
269
+ with urllib.request.urlopen(url, timeout=_PING_TIMEOUT_SECONDS) as resp:
270
+ if resp.status != 200:
271
+ return None
272
+ payload = json.loads(resp.read().decode("utf-8"))
273
+ except Exception:
274
+ return None
275
+ if payload.get("ok") is True and payload.get("service") == "arrayview":
276
+ return payload
277
+ return None
278
+
279
+
280
+ def _int_or_none(value: object) -> int | None:
281
+ return value if isinstance(value, int) else None
282
+
283
+
284
+ def _str_or_none(value: object) -> str | None:
285
+ return value if isinstance(value, str) else None
arrayview/_launcher.py CHANGED
@@ -33,6 +33,7 @@ from arrayview._platform import (
33
33
  _is_vscode_remote,
34
34
  _in_vscode_tunnel,
35
35
  _can_native_window,
36
+ _native_window_gui,
36
37
  _find_vscode_ipc_hook,
37
38
  _is_julia_env,
38
39
  _in_julia_jupyter,
@@ -249,6 +250,7 @@ def _open_webview(
249
250
 
250
251
  icon_path = _get_icon_png_path() or ""
251
252
  inline_html_b64 = None
253
+ gui_name = _native_window_gui() or ""
252
254
 
253
255
  if shell_port is not None:
254
256
  shell_html = _build_inline_shell_html(url, shell_port)
@@ -258,14 +260,14 @@ def _open_webview(
258
260
  if inline_html_b64:
259
261
  script_lines = [
260
262
  "import sys, base64, webview",
261
- "u, w, h, icon, html_b64, ready_file = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5], sys.argv[6]",
263
+ "u, w, h, icon, html_b64, ready_file, gui = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5], sys.argv[6], sys.argv[7]",
262
264
  "html = base64.b64decode(html_b64.encode()).decode()",
263
265
  "class Api:",
264
266
  " def set_title(self, title):",
265
267
  " try: webview.windows[0].set_title(str(title)[:240])",
266
268
  " except Exception: pass",
267
269
  "win = webview.create_window('ArrayView', html=html, width=w, height=h, background_color='#0c0c0c', js_api=Api())",
268
- "kw = {'gui': 'qt'} if sys.platform.startswith('linux') else {}",
270
+ "kw = {'gui': gui} if gui else {}",
269
271
  "def _start_func():",
270
272
  " if ready_file:",
271
273
  " try:",
@@ -298,6 +300,7 @@ def _open_webview(
298
300
  icon_path,
299
301
  inline_html_b64,
300
302
  ready_file or "",
303
+ gui_name,
301
304
  ],
302
305
  stdout=subprocess.DEVNULL,
303
306
  stderr=subprocess.PIPE if capture_stderr else subprocess.DEVNULL,
@@ -306,13 +309,13 @@ def _open_webview(
306
309
  # URL mode — direct load (used when shell_port not provided)
307
310
  script_lines = [
308
311
  "import sys, webview",
309
- "u, w, h, icon, ready_file = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5]",
312
+ "u, w, h, icon, ready_file, gui = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5], sys.argv[6]",
310
313
  "class Api:",
311
314
  " def set_title(self, title):",
312
315
  " try: webview.windows[0].set_title(str(title)[:240])",
313
316
  " except Exception: pass",
314
317
  "win = webview.create_window('ArrayView', u, width=w, height=h, background_color='#0c0c0c', js_api=Api())",
315
- "kw = {'gui': 'qt'} if sys.platform.startswith('linux') else {}",
318
+ "kw = {'gui': gui} if gui else {}",
316
319
  "def _start_func():",
317
320
  " if ready_file:",
318
321
  " try:",
@@ -344,6 +347,7 @@ def _open_webview(
344
347
  str(win_h),
345
348
  icon_path,
346
349
  ready_file or "",
350
+ gui_name,
347
351
  ],
348
352
  stdout=subprocess.DEVNULL,
349
353
  stderr=subprocess.PIPE if capture_stderr else subprocess.DEVNULL,
@@ -368,6 +372,7 @@ def _open_webview_with_fallback(
368
372
  sockets_before = (
369
373
  _session_mod.VIEWER_SOCKETS
370
374
  ) # capture count so we detect a NEW connection
375
+ shell_sockets_before = len(_session_mod.SHELL_SOCKETS)
371
376
 
372
377
  def _read_stderr():
373
378
  try:
@@ -390,14 +395,18 @@ def _open_webview_with_fallback(
390
395
  _open_browser(url, floating=floating)
391
396
  return
392
397
 
393
- # Phase 2: process is alive — wait up to 8 s for a NEW viewer WebSocket to connect.
394
- # We compare against sockets_before so an already-open browser tab doesn't
395
- # falsely confirm that the native window launched successfully.
398
+ # Phase 2: process is alive — wait up to 8 s for a NEW viewer or shell
399
+ # WebSocket. The native shell connection proves pywebview is usable;
400
+ # the embedded viewer iframe can legitimately connect later.
396
401
  import arrayview._session as _sm
397
402
 
398
403
  for _ in range(80):
399
404
  time.sleep(0.1)
400
- if _sm.VIEWER_SOCKETS > sockets_before:
405
+ shell_connected = (
406
+ shell_port is not None
407
+ and len(_sm.SHELL_SOCKETS) > shell_sockets_before
408
+ )
409
+ if _sm.VIEWER_SOCKETS > sockets_before or shell_connected:
401
410
  _vprint("[ArrayView] Native window connected successfully", flush=True)
402
411
  if sys.platform == "darwin":
403
412
  subprocess.Popen(
@@ -530,6 +539,21 @@ def _server_viewer_connections_seen(port: int, timeout: float = 0.5) -> int:
530
539
  return 0
531
540
 
532
541
 
542
+ def _server_shell_sockets_open(port: int, timeout: float = 0.5) -> int:
543
+ """Return the daemon's current native shell WebSocket count."""
544
+ url = f"http://{_LOOPBACK_HOST}:{port}/ping"
545
+ try:
546
+ with urllib.request.urlopen(url, timeout=timeout) as resp:
547
+ if resp.status != 200:
548
+ return 0
549
+ payload = json.loads(resp.read().decode("utf-8"))
550
+ if payload.get("ok") is True and payload.get("service") == "arrayview":
551
+ return int(payload.get("shell_sockets") or 0)
552
+ except Exception:
553
+ pass
554
+ return 0
555
+
556
+
533
557
  def _wait_for_viewer_connection(
534
558
  port: int,
535
559
  *,
@@ -545,6 +569,23 @@ def _wait_for_viewer_connection(
545
569
  return False
546
570
 
547
571
 
572
+ def _wait_for_native_shell_or_viewer_connection(
573
+ port: int,
574
+ *,
575
+ viewer_before: int,
576
+ timeout: float = 8.0,
577
+ ) -> bool:
578
+ """Wait until pywebview proves it is alive via shell or viewer WebSocket."""
579
+ deadline = time.monotonic() + timeout
580
+ while time.monotonic() < deadline:
581
+ if _server_viewer_connections_seen(port, timeout=0.3) > viewer_before:
582
+ return True
583
+ if _server_shell_sockets_open(port, timeout=0.3) > 0:
584
+ return True
585
+ time.sleep(0.1)
586
+ return False
587
+
588
+
548
589
  def _terminate_native_process(proc: subprocess.Popen | None) -> None:
549
590
  if proc is None or proc.poll() is not None:
550
591
  return
@@ -554,6 +595,64 @@ def _terminate_native_process(proc: subprocess.Popen | None) -> None:
554
595
  return
555
596
 
556
597
 
598
+ def _open_cli_native_shell_after_server(
599
+ *,
600
+ port: int,
601
+ sid: str,
602
+ name: str,
603
+ compare_sids: "_CompareSids | None",
604
+ win_w: int,
605
+ win_h: int,
606
+ ) -> bool:
607
+ """Open a CLI native shell and return whether it is usable."""
608
+ url_shell = _shell_url(port, sid, name, compare_sids=compare_sids)
609
+ viewer_before = _server_viewer_connections_seen(port)
610
+ opened, proc = _open_webview_cli_tracked(url_shell, win_w, win_h)
611
+ if opened and _wait_for_native_shell_or_viewer_connection(
612
+ port, viewer_before=viewer_before
613
+ ):
614
+ return True
615
+ if opened:
616
+ _vprint(
617
+ "[ArrayView] Native window did not connect to the backend; falling back to browser",
618
+ flush=True,
619
+ )
620
+ _terminate_native_process(proc)
621
+ return False
622
+
623
+
624
+ def _activate_early_cli_native_shell(
625
+ *,
626
+ port: int,
627
+ sid: str,
628
+ name: str,
629
+ proc: subprocess.Popen | None,
630
+ ) -> bool:
631
+ """Attach an already-started preload shell to a spawned daemon session."""
632
+ viewer_before = _server_viewer_connections_seen(port)
633
+ try:
634
+ notify_result = _notify_existing_session(
635
+ port,
636
+ sid,
637
+ name,
638
+ url=_viewer_path(sid),
639
+ wait=True,
640
+ )
641
+ notified = bool(notify_result.get("notified"))
642
+ except Exception:
643
+ notified = False
644
+ if notified and _wait_for_native_shell_or_viewer_connection(
645
+ port, viewer_before=viewer_before
646
+ ):
647
+ return True
648
+ _vprint(
649
+ "[ArrayView] Native window did not connect to the backend; falling back to browser",
650
+ flush=True,
651
+ )
652
+ _terminate_native_process(proc)
653
+ return False
654
+
655
+
557
656
  # ── Server Port Utilities ─────────────────────────────────────────
558
657
 
559
658
 
@@ -862,23 +961,21 @@ def _open_cli_existing_server_view(
862
961
  _vprint(f"Injected into existing window (port {port})")
863
962
  return
864
963
  if notify_webview and not notified:
865
- url_shell = _shell_url(port, sid, name, compare_sids=compare_sids)
866
- seen_before = _server_viewer_connections_seen(port)
867
- opened, proc = _open_webview_cli_tracked(url_shell, 1200, 800)
868
- if not opened or not _wait_for_viewer_connection(port, before=seen_before):
869
- if opened:
870
- _vprint(
871
- "[ArrayView] Native window did not connect to the backend; falling back to browser",
872
- flush=True,
873
- )
874
- _terminate_native_process(proc)
964
+ native_ready = _open_cli_native_shell_after_server(
965
+ port=port,
966
+ sid=sid,
967
+ name=name,
968
+ compare_sids=compare_sids,
969
+ win_w=1200,
970
+ win_h=800,
971
+ )
972
+ if not native_ready:
875
973
  _vprint("[ArrayView] Falling back to browser", flush=True)
876
974
  _print_viewer_location(url)
877
975
  _open_browser(
878
976
  url,
879
977
  blocking=True,
880
978
  title=f"ArrayView: {name}",
881
- filepath=base_file,
882
979
  floating=floating,
883
980
  )
884
981
  return
@@ -889,7 +986,6 @@ def _open_cli_existing_server_view(
889
986
  blocking=True,
890
987
  force_vscode=(window_mode == "vscode"),
891
988
  title=f"ArrayView: {name}",
892
- filepath=base_file,
893
989
  floating=floating,
894
990
  )
895
991
 
@@ -1052,11 +1148,11 @@ def _handle_cli_spawned_daemon(
1052
1148
  stdout=subprocess.DEVNULL,
1053
1149
  stderr=subprocess.DEVNULL,
1054
1150
  close_fds=True,
1151
+ start_new_session=(sys.platform != "win32"),
1055
1152
  )
1056
1153
 
1057
1154
  early_webview_opened = False
1058
1155
  early_webview_proc = None
1059
- early_webview_notified = False
1060
1156
  early_webview_connected = False
1061
1157
  if (
1062
1158
  use_webview
@@ -1083,29 +1179,12 @@ def _handle_cli_spawned_daemon(
1083
1179
  sys.exit(1)
1084
1180
 
1085
1181
  if early_webview_opened:
1086
- seen_before = _server_viewer_connections_seen(port)
1087
- try:
1088
- notify_result = _notify_existing_session(
1089
- port,
1090
- sid,
1091
- name,
1092
- url=_viewer_path(sid),
1093
- wait=True,
1094
- )
1095
- early_webview_notified = bool(notify_result.get("notified"))
1096
- except Exception:
1097
- early_webview_notified = False
1098
- if early_webview_notified:
1099
- early_webview_connected = _wait_for_viewer_connection(
1100
- port,
1101
- before=seen_before,
1102
- )
1103
- if not early_webview_connected:
1104
- _vprint(
1105
- "[ArrayView] Native window did not connect to the backend; falling back to browser",
1106
- flush=True,
1107
- )
1108
- _terminate_native_process(early_webview_proc)
1182
+ early_webview_connected = _activate_early_cli_native_shell(
1183
+ port=port,
1184
+ sid=sid,
1185
+ name=name,
1186
+ proc=early_webview_proc,
1187
+ )
1109
1188
 
1110
1189
  should_retry_webview = use_webview and not (
1111
1190
  early_webview_opened and not early_webview_connected
@@ -1154,21 +1233,20 @@ def _open_cli_spawned_view(
1154
1233
  if _should_notify_webview(use_webview, overlay_sid):
1155
1234
  if webview_already_opened:
1156
1235
  return
1157
- url_shell = _shell_url(port, sid, name, compare_sids=compare_sids)
1158
- seen_before = _server_viewer_connections_seen(port)
1159
- opened, proc = _open_webview_cli_tracked(url_shell, 1400, 900)
1160
- if not opened or not _wait_for_viewer_connection(port, before=seen_before):
1161
- if opened:
1162
- _vprint(
1163
- "[ArrayView] Native window did not connect to the backend; falling back to browser",
1164
- flush=True,
1165
- )
1166
- _terminate_native_process(proc)
1236
+ native_ready = _open_cli_native_shell_after_server(
1237
+ port=port,
1238
+ sid=sid,
1239
+ name=name,
1240
+ compare_sids=compare_sids,
1241
+ win_w=1400,
1242
+ win_h=900,
1243
+ )
1244
+ if not native_ready:
1167
1245
  _vprint("[ArrayView] Falling back to browser", flush=True)
1168
1246
  _print_viewer_location(url)
1169
1247
  _open_browser(
1170
1248
  url,
1171
- blocking=False,
1249
+ blocking=(window_mode == "vscode"),
1172
1250
  force_vscode=(window_mode == "vscode"),
1173
1251
  title=f"ArrayView: {name}",
1174
1252
  floating=floating,