diff --git a/bin/nexus_window.py b/bin/nexus_window.py
index ff6dcb0..aab5396 100644
--- a/bin/nexus_window.py
+++ b/bin/nexus_window.py
@@ -6,6 +6,11 @@ present on Win10/11) -- a real app window with no browser chrome and none of the
Edge --app profile cold-start. Blocks until the window is closed; the launcher
waits on this process and stops the services when it exits.
+The window opens IMMEDIATELY on a loading page and navigates to the app once the
+backend answers, rather than waiting up to 40s with nothing on screen and then
+appearing. One window the whole time: nothing pops up and vanishes, and there is
+no moment where the user is left wondering whether the command did anything.
+
Falls back to the default browser if pywebview/WebView2 is unavailable, staying
alive so the launcher doesn't tear the services down underneath it.
"""
@@ -15,6 +20,38 @@ import urllib.request
URL = "http://localhost:8000"
+# Inline so it renders with no server and no asset files - the whole point is
+# that it shows before anything is listening. Colours match the app's dark UI so
+# the swap to the real page is not a flash of a different-looking window.
+LOADING_HTML = """
+
NexusOS
+
+
+
Loading Nexus core services
+
Starting the memory service and backend...
+
+"""
+
+FAILED_HTML = LOADING_HTML.replace(
+ "", ""
+).replace(
+ "Loading Nexus core services", "Backend did not start"
+).replace(
+ "Starting the memory service and backend...",
+ "Nothing answered on :8000 after 40s. Check: ncp logs -b"
+)
+
def _wait_for_backend(timeout: float = 40.0) -> bool:
"""Poll /status until the backend answers, so the window never loads before
@@ -37,18 +74,27 @@ def main() -> int:
except Exception as e: # pywebview not installed
return _browser_fallback(f"pywebview unavailable ({e})")
- if not _wait_for_backend():
- print("[nexus] backend not reachable on :8000 after 40s.", file=sys.stderr)
-
try:
- webview.create_window(
+ window = webview.create_window(
"NexusOS",
- URL,
+ html=LOADING_HTML,
width=1200,
height=800,
min_size=(900, 600),
)
- webview.start() # blocks until the window is closed
+
+ def _swap_in_app(win):
+ """Runs once the GUI loop is up, so the spinner is already on screen
+ while we wait. load_url replaces the loading page in place - there is
+ never a second window to close."""
+ if _wait_for_backend():
+ win.load_url(URL)
+ else:
+ print("[nexus] backend not reachable on :8000 after 40s.",
+ file=sys.stderr)
+ win.load_html(FAILED_HTML)
+
+ webview.start(_swap_in_app, window) # blocks until the window is closed
return 0
except Exception as e: # no WebView2 runtime / backend failure
return _browser_fallback(f"native window failed ({e})")
diff --git a/management/ncp.py b/management/ncp.py
index b71f45f..4b5f276 100644
--- a/management/ncp.py
+++ b/management/ncp.py
@@ -100,8 +100,15 @@ class Service:
def _uvicorn(app: str, port: int):
+ # No --reload. It is a dev-loop flag: uvicorn's reloader runs a supervisor
+ # that spawns the real server as a CHILD, so every service became two
+ # processes - and on Windows that child, spawned from a parent with no
+ # console, got handed a brand new console WINDOW. `ncp web` popped two black
+ # terminals for what should have been a silent start. launch_nexus.sh still
+ # passes --reload for the Linux dev loop, where a visible console is the
+ # point; this launcher is the one users run.
return [str(PYTHON), "-m", "uvicorn", app, "--host", "0.0.0.0",
- "--port", str(port), "--reload"]
+ "--port", str(port)]
SERVICES = {
@@ -153,8 +160,12 @@ def launch(svc: Service) -> None:
svc.log_file.write_text("")
with open(svc.log_file, "ab") as log:
# Detach so the service outlives this process, on both platforms.
+ # CREATE_NO_WINDOW, not DETACHED_PROCESS: detached means the process has
+ # NO console, and Windows then gives a console window to any console
+ # program it starts in turn. CREATE_NO_WINDOW gives it a console that is
+ # never shown, which its children inherit - so nothing flashes up.
kwargs = ({"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP
- | getattr(subprocess, "DETACHED_PROCESS", 0)}
+ | getattr(subprocess, "CREATE_NO_WINDOW", 0)}
if WINDOWS else {"start_new_session": True})
proc = subprocess.Popen(argv, cwd=str(svc.cwd), stdout=log,
stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL,