fix(ncp web): no stray consoles, and show a loading window while services start

The two black terminals had two causes: uvicorn --reload (a dev flag whose
reloader spawns the server as a child, and Windows gives a console window to a
child of a console-less parent), and DETACHED_PROCESS in launch(), which is
exactly the console-less condition that triggers it. Drops --reload here
(launch_nexus.sh keeps it for the Linux dev loop) and uses CREATE_NO_WINDOW.

The window now opens immediately on an inline 'Loading Nexus core services' page
and navigates to the app once /status answers, instead of waiting 40s offscreen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-22 15:22:44 -05:00
co-authored by Claude Opus 4.8
parent a62b68a909
commit 4cc0481ffd
2 changed files with 65 additions and 8 deletions
+52 -6
View File
@@ -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 = """<!doctype html>
<html><head><meta charset="utf-8"><title>NexusOS</title><style>
html,body{height:100%;margin:0}
body{background:#14161a;color:#e6e8ec;display:flex;align-items:center;
justify-content:center;font-family:Segoe UI,system-ui,sans-serif}
.box{text-align:center}
.ring{width:44px;height:44px;margin:0 auto 22px;border-radius:50%;
border:3px solid #2a2f38;border-top-color:#4c8dff;
animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
h1{font-size:17px;font-weight:600;margin:0 0 6px}
p{font-size:13px;color:#8b93a1;margin:0}
</style></head><body>
<div class="box">
<div class="ring"></div>
<h1>Loading Nexus core services</h1>
<p>Starting the memory service and backend...</p>
</div>
</body></html>"""
FAILED_HTML = LOADING_HTML.replace(
"<div class=\"ring\"></div>", ""
).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})")