nimbie-shell 0.0.1.dev0__tar.gz → 0.0.2.dev0__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 (32) hide show
  1. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/PKG-INFO +3 -2
  2. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/pyproject.toml +3 -4
  3. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/browser_gateway.py +1 -1
  4. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/shared/remote_shell_support.py +74 -11
  5. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/v2/repl_app.py +4 -85
  6. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/v3/repl_app.py +1 -3
  7. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/README.md +0 -0
  8. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/__init__.py +0 -0
  9. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/cli.py +0 -0
  10. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/command_policy.py +0 -0
  11. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/config.py +0 -0
  12. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/model.py +0 -0
  13. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/summary_entry.py +0 -0
  14. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/system.md +0 -0
  15. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/system_prompt.py +0 -0
  16. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/tools/__init__.py +0 -0
  17. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/tools/internal/__init__.py +0 -0
  18. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/tools/internal/file_patcher.py +0 -0
  19. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/tools/internal/selection_prompt.py +0 -0
  20. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/tools/internal/set_result.py +0 -0
  21. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/tools/internal/shell_runner.py +0 -0
  22. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/tools/tool_runtime.py +0 -0
  23. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/gateway_textual.py +0 -0
  24. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/incremental_markdown.py +0 -0
  25. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/process_overview_textual.py +0 -0
  26. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/selection_panel_textual.py +0 -0
  27. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/session_history_inspector_textual.py +0 -0
  28. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/streaming_tail.py +0 -0
  29. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/terminal_markdown.py +0 -0
  30. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/v2/output_hub.py +0 -0
  31. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/v2/renderer.py +0 -0
  32. {nimbie_shell-0.0.1.dev0 → nimbie_shell-0.0.2.dev0}/src/nimbie/ui/v2/slash_commands.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nimbie-shell
3
- Version: 0.0.1.dev0
3
+ Version: 0.0.2.dev0
4
4
  Summary: Lightweight agent loop and context manager (standalone)
5
5
  Requires-Dist: click>=8.1.7
6
6
  Requires-Dist: httpx[http2]>=0.28.1
7
7
  Requires-Dist: litellm>=1.81.11
8
8
  Requires-Dist: chronml>=0.1.0
9
- Requires-Dist: nimbie-flash>=0.0.1.dev0
9
+ Requires-Dist: oturn>=0.1.0
10
+ Requires-Dist: nimbie-flash>=0.0.2.dev0
10
11
  Requires-Dist: prompt-toolkit>=3.0.39
11
12
  Requires-Dist: rich>=13.7.0
12
13
  Requires-Dist: textual>=0.80.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nimbie-shell"
3
- version = "0.0.1.dev0"
3
+ version = "0.0.2.dev0"
4
4
  description = "Lightweight agent loop and context manager (standalone)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -9,7 +9,8 @@ dependencies = [
9
9
  "httpx[http2]>=0.28.1",
10
10
  "litellm>=1.81.11",
11
11
  "chronml>=0.1.0",
12
- "nimbie_flash>=0.0.1.dev0",
12
+ "oturn>=0.1.0",
13
+ "nimbie_flash>=0.0.2.dev0",
13
14
  "prompt_toolkit>=3.0.39",
14
15
  "rich>=13.7.0",
15
16
  "textual>=0.80.0",
@@ -25,9 +26,7 @@ build-backend = "uv_build"
25
26
  [tool.uv.build-backend]
26
27
  module-name = "nimbie"
27
28
  source-exclude = [
28
- "**/.cyf/**",
29
29
  "**/.nimbie/**",
30
- "src/nimbie/.cyf/**",
31
30
  "src/nimbie/.nimbie/**",
32
31
  "src/nimbie/**/__pycache__/**",
33
32
  "src/nimbie/**/*.pyc",
@@ -215,7 +215,7 @@ class BrowserGatewayServer:
215
215
  self.work_dir = work_dir
216
216
  self._repo_root = Path(__file__).resolve().parents[2]
217
217
  self._frontend_root = self._repo_root / "frontend-app"
218
- self.token = token or os.environ.get("CYF_WEB_TOKEN") or f"t_{random.randint(100000, 999999)}"
218
+ self.token = token or os.environ.get("NIMBIE_WEB_TOKEN") or f"t_{random.randint(100000, 999999)}"
219
219
  self.port = _pick_free_port(8413, 8419)
220
220
  self.vite_port = _pick_free_port(5173, 5183)
221
221
  self._app = FastAPI()
@@ -46,16 +46,17 @@ class RemoteShellSupport:
46
46
  self._prompt_build_executor = ThreadPoolExecutor(max_workers=1)
47
47
  self._prompt_env_cache: Dict[str, str] = {}
48
48
  self._prompt_env_cache_backend: str = ''
49
+ self._flash_executor_kind: Optional[str] = None
49
50
 
50
51
  def set_output_sink(self, sink) -> None:
51
52
  self._output_sink = sink
52
53
 
53
54
  def _executor_debug_enabled(self) -> bool:
54
- v = str(os.getenv('CYF_EXECUTOR_DEBUG', '')).strip().lower()
55
+ v = str(os.getenv('NIMBIE_EXECUTOR_DEBUG', '')).strip().lower()
55
56
  return v in {'1', 'true', 'yes', 'on'}
56
57
 
57
58
  def _executor_debug_verbose(self) -> bool:
58
- v = str(os.getenv('CYF_EXECUTOR_DEBUG_VERBOSE', '')).strip().lower()
59
+ v = str(os.getenv('NIMBIE_EXECUTOR_DEBUG_VERBOSE', '')).strip().lower()
59
60
  return v in {'1', 'true', 'yes', 'on'}
60
61
 
61
62
  def _emit_executor_debug(self, msg: str) -> None:
@@ -88,9 +89,7 @@ class RemoteShellSupport:
88
89
  except Exception as e:
89
90
  raise RuntimeError(f'failed to import nimbie_flash for prompt rendering: {e}') from e
90
91
  self._sync_flash_backend_env()
91
- if self._flash_executor is None:
92
- self._flash_executor = flash.LocalExecutor()
93
- executor = self._flash_executor
92
+ executor = self._ensure_flash_executor(flash, remote=(self._build_remote_ssh_command() is not None))
94
93
  if not hasattr(flash, 'build_prompt_from_env'):
95
94
  raise RuntimeError('nimbie_flash.build_prompt_from_env is not available')
96
95
  backend_key = str(self.remote_status_text() or 'local')
@@ -127,7 +126,7 @@ class RemoteShellSupport:
127
126
  def build_prompt(self) -> str:
128
127
  timeout_s = 3.0
129
128
  try:
130
- timeout_s = max(0.2, float(os.getenv('CYF_PROMPT_TIMEOUT_S', '3')))
129
+ timeout_s = max(0.2, float(os.getenv('NIMBIE_PROMPT_TIMEOUT_S', '3')))
131
130
  except Exception:
132
131
  timeout_s = 3.0
133
132
  fut = self._prompt_build_executor.submit(self._build_prompt_impl)
@@ -187,9 +186,12 @@ class RemoteShellSupport:
187
186
  seen.add(rp)
188
187
  meta = self._executor_meta_from_path(pp)
189
188
  out.append({'path': pp, 'source': source, 'meta': meta})
190
- override = os.getenv('CYF_FLASH_STDIO_EXECUTOR_LOCAL_BIN', '').strip()
191
- if override:
192
- _add_candidate(Path(override), 'env:CYF_FLASH_STDIO_EXECUTOR_LOCAL_BIN')
189
+ local_override = os.getenv('NIMBIE_FLASH_STDIO_EXECUTOR_LOCAL_BIN', '').strip()
190
+ if local_override:
191
+ _add_candidate(Path(local_override), 'env:NIMBIE_FLASH_STDIO_EXECUTOR_LOCAL_BIN')
192
+ flash_override = os.getenv('FLASH_STDIO_EXECUTOR_LOCAL_BIN', '').strip()
193
+ if flash_override:
194
+ _add_candidate(Path(flash_override), 'env:FLASH_STDIO_EXECUTOR_LOCAL_BIN')
193
195
  try:
194
196
  import nimbie_flash
195
197
  except Exception:
@@ -201,12 +203,12 @@ class RemoteShellSupport:
201
203
  bundled = None
202
204
  if bundled is not None:
203
205
  _add_candidate(Path(bundled), 'python-package:nimbie_flash')
204
- multi = os.getenv('CYF_FLASH_STDIO_EXECUTOR_CANDIDATES', '').strip()
206
+ multi = os.getenv('FLASH_STDIO_EXECUTOR_CANDIDATES', '').strip()
205
207
  if multi:
206
208
  for item in multi.split(os.pathsep):
207
209
  it = item.strip()
208
210
  if it:
209
- _add_candidate(Path(it), 'env:CYF_FLASH_STDIO_EXECUTOR_CANDIDATES')
211
+ _add_candidate(Path(it), 'env:FLASH_STDIO_EXECUTOR_CANDIDATES')
210
212
  repo_root = Path(__file__).resolve().parents[4]
211
213
  candidates = [repo_root / 'flash' / 'target' / 'x86_64-unknown-linux-musl' / 'release' / 'flash-stdio-executor', repo_root / 'flash' / 'target' / 'x86_64-unknown-linux-gnu' / 'release' / 'flash-stdio-executor', repo_root / 'flash' / 'target' / 'aarch64-unknown-linux-musl' / 'release' / 'flash-stdio-executor', repo_root / 'flash' / 'target' / 'aarch64-unknown-linux-gnu' / 'release' / 'flash-stdio-executor', repo_root / 'flash' / 'target' / 'x86_64-apple-darwin' / 'release' / 'flash-stdio-executor', repo_root / 'flash' / 'target' / 'aarch64-apple-darwin' / 'release' / 'flash-stdio-executor', repo_root / 'flash' / 'target' / 'maturin' / 'flash-stdio-executor', repo_root / 'flash' / 'target' / 'release' / 'flash-stdio-executor', Path.cwd() / 'flash' / 'target' / 'x86_64-unknown-linux-musl' / 'release' / 'flash-stdio-executor', Path.cwd() / 'flash' / 'target' / 'x86_64-unknown-linux-gnu' / 'release' / 'flash-stdio-executor', Path.cwd() / 'flash' / 'target' / 'aarch64-unknown-linux-musl' / 'release' / 'flash-stdio-executor', Path.cwd() / 'flash' / 'target' / 'aarch64-unknown-linux-gnu' / 'release' / 'flash-stdio-executor', Path.cwd() / 'flash' / 'target' / 'x86_64-apple-darwin' / 'release' / 'flash-stdio-executor', Path.cwd() / 'flash' / 'target' / 'aarch64-apple-darwin' / 'release' / 'flash-stdio-executor', Path.cwd() / 'flash' / 'target' / 'maturin' / 'flash-stdio-executor', Path.cwd() / 'flash' / 'target' / 'release' / 'flash-stdio-executor', Path.cwd() / 'target' / 'x86_64-unknown-linux-musl' / 'release' / 'flash-stdio-executor', Path.cwd() / 'target' / 'x86_64-unknown-linux-gnu' / 'release' / 'flash-stdio-executor', Path.cwd() / 'target' / 'aarch64-unknown-linux-musl' / 'release' / 'flash-stdio-executor', Path.cwd() / 'target' / 'aarch64-unknown-linux-gnu' / 'release' / 'flash-stdio-executor', Path.cwd() / 'target' / 'x86_64-apple-darwin' / 'release' / 'flash-stdio-executor', Path.cwd() / 'target' / 'aarch64-apple-darwin' / 'release' / 'flash-stdio-executor', Path.cwd() / 'target' / 'maturin' / 'flash-stdio-executor', Path.cwd() / 'target' / 'release' / 'flash-stdio-executor']
212
214
  for c in candidates:
@@ -216,6 +218,52 @@ class RemoteShellSupport:
216
218
  _add_candidate(Path(which), 'PATH')
217
219
  return out
218
220
 
221
+ def _resolve_local_stdio_executor_binary(self) -> Tuple[Optional[str], Dict[str, Any]]:
222
+ info: Dict[str, Any] = {
223
+ 'explicit_bin': os.getenv('FLASH_STDIO_EXECUTOR_BIN', '').strip(),
224
+ 'nimbie_local_override': os.getenv('NIMBIE_FLASH_STDIO_EXECUTOR_LOCAL_BIN', '').strip(),
225
+ 'flash_local_override': os.getenv('FLASH_STDIO_EXECUTOR_LOCAL_BIN', '').strip(),
226
+ 'bundled_path': None,
227
+ 'bundled_exists': False,
228
+ 'bundled_executable': False,
229
+ 'bundled_error': '',
230
+ 'selected_source': '',
231
+ 'candidates': [],
232
+ }
233
+ selected = info['explicit_bin'] or info['nimbie_local_override'] or info['flash_local_override']
234
+ if info['explicit_bin']:
235
+ info['selected_source'] = 'env:FLASH_STDIO_EXECUTOR_BIN'
236
+ elif info['nimbie_local_override']:
237
+ info['selected_source'] = 'env:NIMBIE_FLASH_STDIO_EXECUTOR_LOCAL_BIN'
238
+ elif info['flash_local_override']:
239
+ info['selected_source'] = 'env:FLASH_STDIO_EXECUTOR_LOCAL_BIN'
240
+ try:
241
+ import nimbie_flash
242
+ except Exception as e:
243
+ info['bundled_error'] = str(e)
244
+ nimbie_flash = None
245
+ if nimbie_flash is not None:
246
+ try:
247
+ bundled = nimbie_flash.bundled_stdio_executor_path()
248
+ except Exception as e:
249
+ bundled = None
250
+ info['bundled_error'] = str(e)
251
+ if bundled is not None:
252
+ bundled_path = Path(bundled)
253
+ info['bundled_path'] = str(bundled_path)
254
+ try:
255
+ info['bundled_exists'] = bundled_path.exists()
256
+ info['bundled_executable'] = os.access(bundled_path, os.X_OK)
257
+ except Exception:
258
+ info['bundled_exists'] = False
259
+ info['bundled_executable'] = False
260
+ if (not selected) and info['bundled_exists'] and info['bundled_executable']:
261
+ selected = str(bundled_path)
262
+ info['selected_source'] = 'python-package:nimbie_flash'
263
+ candidates = self._collect_local_stdio_executor_binaries()
264
+ info['candidates'] = [{'path': str(item.get('path')), 'source': str(item.get('source') or '')} for item in candidates]
265
+ return (str(selected) if selected else None, info)
266
+
219
267
  def _rank_executor_candidates(self, candidates: List[Dict[str, Any]], *, remote_arch: str, remote_libc: str, remote_os: str, remote_glibc_version: str) -> List[Dict[str, Any]]:
220
268
 
221
269
  def _parse_ver(v: str) -> Tuple[int, ...]:
@@ -348,6 +396,19 @@ class RemoteShellSupport:
348
396
  os.environ.pop('FLASH_REMOTE_ENV_JSON', None)
349
397
  os.environ['FLASH_DISABLE_BACKEND_IDENTITY_PROBE'] = '1'
350
398
 
399
+ def _ensure_flash_executor(self, flash: Any, *, remote: bool) -> Any:
400
+ want = 'stdio' if remote else 'local'
401
+ if self._flash_executor is not None and self._flash_executor_kind == want:
402
+ return self._flash_executor
403
+ if remote:
404
+ if not hasattr(flash, 'StdioExecutor'):
405
+ raise RuntimeError('nimbie_flash.StdioExecutor is not available')
406
+ self._flash_executor = flash.StdioExecutor()
407
+ else:
408
+ self._flash_executor = flash.LocalExecutor()
409
+ self._flash_executor_kind = want
410
+ return self._flash_executor
411
+
351
412
  def disconnect_ssh(self) -> None:
352
413
  self._emit_executor_debug('disconnect ssh backend')
353
414
  self._remote_ssh_host = None
@@ -366,6 +427,7 @@ class RemoteShellSupport:
366
427
  self._sync_flash_backend_env()
367
428
  self._flash_interp = None
368
429
  self._flash_executor = None
430
+ self._flash_executor_kind = None
369
431
  self._prompt_env_cache = {}
370
432
  self._prompt_env_cache_backend = ''
371
433
 
@@ -573,6 +635,7 @@ class RemoteShellSupport:
573
635
  self._sync_flash_backend_env()
574
636
  self._flash_interp = None
575
637
  self._flash_executor = None
638
+ self._flash_executor_kind = None
576
639
  self._prompt_env_cache = {}
577
640
  self._prompt_env_cache_backend = ''
578
641
  user_prefix = f'{user}@' if user else ''
@@ -631,14 +631,12 @@ class ShellAdapter(RemoteShellSupport):
631
631
  import nimbie_flash as flash
632
632
  if self._flash_interp is None:
633
633
  self._flash_interp = flash.FlashInterpreter()
634
- if self._flash_executor is None:
635
- self._flash_executor = flash.LocalExecutor()
634
+ custom_env_builder = getattr(self, '_build_remote_ssh_command', None)
635
+ custom_env = custom_env_builder() if callable(custom_env_builder) else None
636
636
  interp = self._flash_interp
637
- executor = self._flash_executor
637
+ executor = self._ensure_flash_executor(flash, remote=(custom_env is not None))
638
638
  old_bin = os.environ.get('FLASH_STDIO_EXECUTOR_BIN')
639
639
  old_args = os.environ.get('FLASH_STDIO_EXECUTOR_ARGS_JSON')
640
- custom_env_builder = getattr(self, '_build_remote_ssh_command', None)
641
- custom_env = custom_env_builder() if callable(custom_env_builder) else None
642
640
  if custom_env is not None:
643
641
  bin_name, args = custom_env
644
642
  os.environ['FLASH_STDIO_EXECUTOR_BIN'] = bin_name
@@ -743,89 +741,10 @@ class ShellAdapter(RemoteShellSupport):
743
741
  self._emit_executor_debug(f"stream event type={ev_type} bytes={len(data.encode('utf-8', errors='ignore'))}")
744
742
  if data:
745
743
  _emit_text(fix_surrogate(data))
746
- direct_run_started = False
747
744
  try:
748
- result = None
749
- if proxy_stdin and hasattr(executor, 'start_run') and hasattr(executor, 'poll_event'):
750
- try:
751
- cwd = os.getcwd()
752
- try:
753
- cwd = str(getattr(interp, 'cwd', None) or cwd)
754
- except Exception:
755
- pass
756
- active_run_id = str(executor.start_run(cmd, cwd))
757
- direct_run_started = True
758
- stdout_acc: list[str] = []
759
- stderr_acc: list[str] = []
760
- exit_code = 1
761
- while True:
762
- ev_line = executor.poll_event(0)
763
- if ev_line is None:
764
- if not bool(executor.is_running()):
765
- for _ in range(5):
766
- ev_line = executor.poll_event(20)
767
- if ev_line is None:
768
- continue
769
- break
770
- if ev_line is None:
771
- break
772
- else:
773
- time.sleep(0.02)
774
- continue
775
- try:
776
- ev_json = json.loads(str(ev_line))
777
- except Exception:
778
- ev_json = {'type': 'raw', 'data': str(ev_line)}
779
- _on_event(ev_json)
780
- ev_type = str(ev_json.get('type') or '')
781
- if ev_type == 'stdout':
782
- stdout_acc.append(str(ev_json.get('data') or ev_json.get('stdout') or ''))
783
- elif ev_type == 'stderr':
784
- stderr_acc.append(str(ev_json.get('data') or ev_json.get('stderr') or ''))
785
- elif ev_type == 'exit':
786
- try:
787
- raw_exit = ev_json.get('exit_code')
788
- exit_code = int(raw_exit) if raw_exit is not None else 1
789
- except Exception:
790
- exit_code = 1
791
- break
792
- else:
793
- if ev_json.get('stdout'):
794
- stdout_acc.append(str(ev_json.get('stdout') or ''))
795
- if ev_json.get('stderr'):
796
- stderr_acc.append(str(ev_json.get('stderr') or ''))
797
- if ev_json.get('exit_code') is not None:
798
- try:
799
- raw_exit = ev_json.get('exit_code')
800
- exit_code = int(raw_exit) if raw_exit is not None else 1
801
- except Exception:
802
- exit_code = 1
803
- break
804
- try:
805
- executor.flush(active_run_id, 50)
806
- except Exception:
807
- pass
808
-
809
- class _DirectResult:
810
-
811
- def __init__(self, stdout: str, stderr: str, exit_code: int) -> None:
812
- self.stdout = stdout
813
- self.stderr = stderr
814
- self.exit_code = exit_code
815
- result = _DirectResult(''.join(stdout_acc), ''.join(stderr_acc), exit_code)
816
- except Exception as direct_err:
817
- self._emit_executor_debug(f'direct run fallback err={direct_err}')
818
- direct_run_started = False
819
- active_run_id = None
820
- if result is None:
821
- result = interp.run_shell_stream(cmd, executor, _on_event)
745
+ result = interp.run_shell_stream(cmd, executor, _on_event)
822
746
  finally:
823
747
  _cleanup_input_thread()
824
- if direct_run_started and active_run_id is not None:
825
- try:
826
- executor.close_run()
827
- except Exception:
828
- pass
829
748
  if old_bin is not None:
830
749
  os.environ['FLASH_STDIO_EXECUTOR_BIN'] = old_bin
831
750
  else:
@@ -697,11 +697,9 @@ class ShellAdapter(RemoteShellSupport):
697
697
  import nimbie_flash as flash
698
698
  if self._flash_interp is None:
699
699
  self._flash_interp = flash.FlashInterpreter()
700
- if self._flash_executor is None:
701
- self._flash_executor = flash.LocalExecutor()
702
700
  interp = self._flash_interp
703
- executor = self._flash_executor
704
701
  custom_env = self._build_remote_ssh_command()
702
+ executor = self._ensure_flash_executor(flash, remote=(custom_env is not None))
705
703
  self._emit_executor_debug(f"backend_route={('remote' if custom_env is not None else 'local')}")
706
704
  old_bin = os.environ.get('FLASH_STDIO_EXECUTOR_BIN')
707
705
  old_args = os.environ.get('FLASH_STDIO_EXECUTOR_ARGS_JSON')