Преглед на файлове

fix(windows): set windowsHide on remaining child spawns to stop console flash (#1092) (#1104)

On Windows, a black console (conhost) window flashed briefly when CodeGraph
ran as a background MCP server. Several child spawns were missing
`windowsHide: true`, so Windows created a visible console for the child:

- scripts/npm-shim.js — launching the bundled runtime (every server start /
  daemon-idle reconnect) and the self-heal `tar` extraction of a missing
  platform bundle.
- src/reasoning/login.ts — the detached `cmd /c start` browser open.
- src/upgrade/index.ts — package-manager spawn (console-attached, so no flash
  in practice, but set for uniformity: every child spawn now hides).

The daemon spawn (#411) and all git execFileSync sites already set it; this
closes the remaining gaps. Adds an all-platforms source guard to
__tests__/npm-shim.test.ts asserting every spawn in the shim sets windowsHide.

Closes #1092

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry преди 12 часа
родител
ревизия
6dd5512d8d
променени са 5 файла, в които са добавени 22 реда и са изтрити 4 реда
  1. 1 0
      CHANGELOG.md
  2. 17 0
      __tests__/npm-shim.test.ts
  3. 2 2
      scripts/npm-shim.js
  4. 1 1
      src/reasoning/login.ts
  5. 1 1
      src/upgrade/index.ts

+ 1 - 0
CHANGELOG.md

@@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ### Fixes
 
+- On Windows, a console window no longer briefly flashes when CodeGraph runs as a background MCP server. When the npm launcher started the bundled runtime — which happens every time an editor starts the server or reconnects after the daemon idles out — and during its self-heal step that extracts a missing platform bundle, Windows would pop up a black console (conhost) window for a moment. Both now launch hidden, matching how the daemon already behaved; the browser-open step of `codegraph login` was hardened the same way. Thanks @luoyerr for the report and root-cause analysis. (#1092)
 - C++ forward declarations no longer crowd out the real class definition. A `class Foo;` forward declaration — common in large C++ and Unreal Engine codebases, where a heavily used class is forward-declared across dozens of headers — was indexed as its own class node every time it appeared. So exploring that class returned mostly forward-declaration sites, and could even pick one of them as the representative for blast-radius, burying the actual definition and its members and callers. Bodiless forward declarations are now skipped for C and C++, exactly as forward-declared structs and enums already were, so only the real definition is indexed. Languages where a class with no body is a complete definition — such as Kotlin's `class Empty` and Scala — are unaffected. Thanks @luoyxy for the report and root-cause analysis. (#1093)
 - C++ methods that return a reference, and user-defined conversion operators, are now indexed under their correct names. An inline getter like `const FGameplayTagContainer& GetActiveTags() const` — everywhere in Unreal Engine headers — was indexed as `& GetActiveTags() const` instead of `GetActiveTags`, and a conversion operator like `operator EALSMovementState() const` kept its trailing `() const` instead of reading `operator EALSMovementState`. In both cases the garbled name meant you couldn't find the symbol by name and its callers weren't linked. Both now read cleanly, matching how pointer-returning and value-returning methods already worked. (#1096)
 - C++ functions written with an inline-specifier macro before the return type are now indexed correctly. In Unreal Engine, inline helpers are commonly written `FORCEINLINE FString GetEnumerationToString(...)`; the `FORCEINLINE` macro made the parser read the return type as part of the function's name (`FString GetEnumerationToString` instead of `GetEnumerationToString`) and lose the real return type, so the function couldn't be found by name and its callers weren't linked. CodeGraph now recognizes the standard Unreal inline macros (`FORCEINLINE`, `FORCENOINLINE`, `FORCEINLINE_DEBUGGABLE`), so both the name and the return type are captured. (#1100)

+ 17 - 0
__tests__/npm-shim.test.ts

@@ -71,6 +71,23 @@ function runShim(pkgDir: string, args: string[], env: Record<string, string>) {
   });
 }
 
+// Static source guard (all platforms): every child spawn in the shim must set
+// windowsHide, or a console (conhost) window flashes when the shim runs as a
+// background MCP server on Windows (issue #1092). windowsHide is a Windows-only
+// spawn behavior that can't be observed from these POSIX-only subprocess tests,
+// so we assert it at the source level instead — and this also catches any new
+// spawn site added to the shim later.
+describe('npm-shim windowsHide (#1092)', () => {
+  it('sets windowsHide: true on every spawn in the shim', () => {
+    const src = fs.readFileSync(SHIM_SRC, 'utf8');
+    const spawnLines = src.split('\n').filter((l) => /\.spawn(Sync)?\(/.test(l));
+    expect(spawnLines.length).toBeGreaterThan(0); // guard against a false pass if the calls move
+    for (const line of spawnLines) {
+      expect(line, `spawn without windowsHide: ${line.trim()}`).toContain('windowsHide: true');
+    }
+  });
+});
+
 describe.skipIf(isWindows)('npm-shim launcher', () => {
   it('runs the installed optional-dependency bundle without any download', async () => {
     const pkg = makePkg();

+ 2 - 2
scripts/npm-shim.js

@@ -45,7 +45,7 @@ async function main() {
   // Happy path: the npm-installed optional dependency. Fall back to a download
   // when the registry didn't deliver it.
   var resolved = resolveInstalledBundle() || (await selfHealBundle());
-  var res = childProcess.spawnSync(resolved.command, resolved.args, { stdio: 'inherit' });
+  var res = childProcess.spawnSync(resolved.command, resolved.args, { stdio: 'inherit', windowsHide: true });
   if (res.error) {
     process.stderr.write('codegraph: ' + res.error.message + '\n');
     process.exit(1);
@@ -222,7 +222,7 @@ function extract(archive, destDir) {
   var args = isWindows
     ? ['-xf', archive, '-C', destDir, '--strip-components=1']
     : ['-xzf', archive, '-C', destDir, '--strip-components=1'];
-  var res = childProcess.spawnSync('tar', args, { stdio: 'ignore' });
+  var res = childProcess.spawnSync('tar', args, { stdio: 'ignore', windowsHide: true });
   if (res.error) throw new Error('tar unavailable: ' + res.error.message);
   if (res.status !== 0) throw new Error('tar exited ' + res.status);
 }

+ 1 - 1
src/reasoning/login.ts

@@ -80,7 +80,7 @@ export async function openBrowser(url: string): Promise<void> {
     : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
     : ['xdg-open', [url]];
   try {
-    const child = spawn(cmd as string, args as string[], { stdio: 'ignore', detached: true });
+    const child = spawn(cmd as string, args as string[], { stdio: 'ignore', detached: true, windowsHide: true });
     child.on('error', () => {});
     child.unref();
   } catch {

+ 1 - 1
src/upgrade/index.ts

@@ -546,7 +546,7 @@ export function hasCommand(cmd: string): boolean {
 }
 
 export function defaultRun(cmd: string, args: string[], env?: NodeJS.ProcessEnv): number {
-  const r = spawnSync(cmd, args, { stdio: 'inherit', env: env ?? process.env });
+  const r = spawnSync(cmd, args, { stdio: 'inherit', env: env ?? process.env, windowsHide: true });
   if (r.error) return -1;
   return r.status ?? -1;
 }