Skip to content

Commit b98789e

Browse files
committed
fix(#863): belt-and-suspenders Windows console hide + JS error surfacing
User reported on Windows + Claude Desktop that even after the pythonw fix in b0268d7, the cmd window still pops AND the settings page still says "Loading..." indefinitely. **Windows console:** add ``STARTUPINFO`` with ``wShowWindow=SW_HIDE`` on top of pythonw preference + ``CREATE_NO_WINDOW``. uv-managed Pythons sometimes strip pythonw.exe, falling back to python.exe. ``CREATE_NO_WINDOW`` alone leaves a window in some console-allocation paths under GUI parents; ``STARTF_USESHOWWINDOW`` + ``SW_HIDE`` force-hides whatever console does get allocated. **JS error surfacing:** if any function definition in the settings- page script throws during top-level evaluation (e.g. a runtime error referencing a missing DOM element), the script aborts before ``loadTools()`` is ever called and the status bar stays at the initial ``Loading...`` literal. Add ``window.addEventListener('error', ...)`` + ``unhandledrejection`` so the next time the page hangs at "Loading", the actual error message appears in the status bar instead — no devtools required.
1 parent b0268d7 commit b98789e

2 files changed

Lines changed: 39 additions & 20 deletions

File tree

src/ha_mcp/settings_ui.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,24 @@ def apply_tool_visibility(
529529
<input type="text" class="search" id="search" placeholder="Search tools...">
530530
<div id="groups"></div>
531531
<script>
532+
// Catch top-level / async script errors and surface them in the
533+
// status bar so a perpetually-"Loading" page becomes self-diagnosing
534+
// (no devtools required). Without this, a script-evaluation error
535+
// in any of the function definitions below would abort the script
536+
// before loadTools() is even called, leaving the status stuck at
537+
// the initial "Loading...".
538+
window.addEventListener('error', (e) => {
539+
const el = document.getElementById('status');
540+
if (!el) return;
541+
const where = e.filename ? `${e.filename}:${e.lineno}:${e.colno}` : 'inline';
542+
el.textContent = `JS error: ${e.message} @ ${where}`;
543+
});
544+
window.addEventListener('unhandledrejection', (e) => {
545+
const el = document.getElementById('status');
546+
if (!el) return;
547+
el.textContent = `Async error: ${e.reason && e.reason.message ? e.reason.message : String(e.reason)}`;
548+
});
549+
532550
let toolData = [];
533551
let toolStates = {};
534552
let saveTimer = null;

src/ha_mcp/stdio_settings_sidecar.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -318,30 +318,31 @@ def _do_spawn() -> None:
318318
"close_fds": True,
319319
}
320320
if sys.platform == "win32":
321-
# Prefer ``pythonw.exe`` over ``python.exe``. pythonw is the
322-
# GUI-subsystem Python interpreter — Windows never allocates a
323-
# console window for it. Falls back to python.exe if pythonw
324-
# isn't alongside (rare; most CPython distributions ship both,
325-
# including the uv-installed Python that uvx uses).
321+
# Three-layer defense against the cmd-window-pops-and-closing-
322+
# it-kills-the-server bug reported against Claude Desktop:
326323
#
327-
# Why not DETACHED_PROCESS: the prior attempt used
328-
# ``DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW``
329-
# and the cmd window still popped under Claude Desktop. Python's
330-
# subprocess docs spell out that ``CREATE_NO_WINDOW`` is
331-
# IGNORED when ``DETACHED_PROCESS`` is set, and
332-
# ``DETACHED_PROCESS`` on a CUI binary auto-allocates a fresh
333-
# visible console. pythonw avoids the whole console-allocation
334-
# path.
324+
# 1. Prefer ``pythonw.exe`` over ``python.exe``. pythonw is the
325+
# GUI-subsystem Python — Windows never allocates a console
326+
# for it. Falls back to python.exe if pythonw isn't there
327+
# (uv-installed Pythons sometimes strip it).
328+
# 2. ``STARTUPINFO`` with ``SW_HIDE`` forces any console that
329+
# DOES get allocated to be hidden. Catches the fallback case.
330+
# 3. ``CREATE_NO_WINDOW`` suppresses console allocation entirely
331+
# for the python.exe fallback. ``CREATE_NEW_PROCESS_GROUP``
332+
# prevents a CTRL_CLOSE_EVENT (X-button on any console that
333+
# sneaks through) from killing the sidecar.
335334
#
336-
# CREATE_NEW_PROCESS_GROUP is still useful: closing any console
337-
# that happens to be attached (if pythonw isn't found and we
338-
# fall back to python.exe) sends ``CTRL_CLOSE_EVENT`` to the
339-
# console's process group. Putting the child in its own group
340-
# prevents that event from killing the sidecar.
341-
# CREATE_NO_WINDOW is belt-and-suspenders for the python.exe
342-
# fallback path — harmless when pythonw is in use.
335+
# Why not DETACHED_PROCESS: prior attempts used it, but Python's
336+
# docs spell out that ``CREATE_NO_WINDOW`` is IGNORED when
337+
# DETACHED_PROCESS is set, and DETACHED_PROCESS on a CUI binary
338+
# auto-allocates a fresh visible console — the exact failure
339+
# mode the user saw.
343340
pythonw = Path(sys.executable).with_name("pythonw.exe")
344341
cmd_python = str(pythonw) if pythonw.exists() else sys.executable
342+
startupinfo = subprocess.STARTUPINFO() # type: ignore[attr-defined]
343+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore[attr-defined]
344+
startupinfo.wShowWindow = subprocess.SW_HIDE # type: ignore[attr-defined]
345+
popen_kwargs["startupinfo"] = startupinfo
345346
popen_kwargs["creationflags"] = (
346347
subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
347348
| subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined]

0 commit comments

Comments
 (0)