# \[Bug\]: Desktop app's \`t3\` on Windows still has no terminal for prompts: the \#20162 stdin check never fires · Issue \#23815 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/issues/23815)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: backlog

Priority: 0

## Description

[Discussion and activity](https://git.cafe/pingdotgg/t3code/issues/23815)

---

### Before submitting

- [x] I searched existing issues and did not find a duplicate.
- [x] I included enough detail to reproduce or investigate the problem.

### Area

apps/server

### Steps to reproduce

1. On Windows, install T3 Code (Nightly) and use its `t3` command (`~/.t3/bin/t3.cmd`).
2. Have a `state.sqlite` from before 0.1.0 next to `statev2.sqlite`.
3. Open Windows Terminal or a plain console and run `t3 doctor`.

### Expected behavior

`doctor` shows its **Choose repairs to run** picker. #20162 was meant to fix this: it reopens `CONIN$` as `process.stdin` when the desktop launcher runs in a console.

### Actual behavior

`doctor` acts as if it was run from a script:

```
> t3 doctor
Database: 2.94 GiB, 0 KiB reclaimable.
Database from before 0.1.0: 1.93 GiB, kept for rolling back.

Delete the database from before 0.1.0
  Frees 1.93 GiB. Your threads and settings are already in the current database.
  Warning: after this, a T3 Code version before 0.1.0 starts with no threads or settings, so you can no longer roll back.

Run in a terminal to choose repairs, or pass --fix for the recommended ones.
```

This affects every prompt that #20162 listed (`t3 triage`, `t3 update`, `t3 uninstall`, `t3 doctor`), because none of them reaches `CONIN$`.

### Cause

`needsConsoleStdin` in `apps/server/src/cli/consoleStdin.ts` reopens the console only when stdin is closed (`!input.stdinOpen`, where `isOpen(0)` is `fstatSync(0)` succeeding). On Windows that never happens. If stdio is missing at startup, Node replaces it with `NUL` in `PlatformInit` (`src/node.cc`):

```cpp
for (int fd = 0; fd <= 2; ++fd) {
  auto handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
  if (handle == INVALID_HANDLE_VALUE ||
      GetFileType(handle) == FILE_TYPE_UNKNOWN) {
    _close(fd);
    if (fd != _open("nul", _O_RDWR)) ABORT();
  }
}
```

By the time `attachConsoleStdin()` runs, fd 0 is an open `NUL` device. `fstatSync(0)` succeeds, `needsConsoleStdin` returns false, and `process.stdin` stays a non-TTY `fs.ReadStream` over `NUL`.

The unit tests pass `stdinOpen: false`, but the real process never gets into that state on Windows. #20162 said it had not yet been run on Windows.

### Evidence

I used the shipped app's executable, `ELECTRON_RUN_AS_NODE=1`, `T3CODE_DESKTOP_CLI_LAUNCHER=1`, and a new console window started with `cmd /c`. Like `t3.cmd`, no stdio was redirected unless a row says so.

The probe ran the same inputs `attachConsoleStdin` uses:

| stdin | `fstatSync(0)` | char device | mode | `isatty(0)` | `isatty(1)` | `needsConsoleStdin` |
|---|---|---|---|---|---|---|
| console, no redirect | ok | yes | `20666` | false | true | **false** |
| `< NUL` | ok | yes | `20666` | false | true | false |
| `echo hi \|` | ok | no | `10000` | false | true | false |

The console case gives the same result as an explicit `< NUL`.

I also imported the shipped `consoleStdin-*.mjs` from `server.asar` and called its `attachConsoleStdin()` from the same console. It returned `undefined`, with `process.stdin.isTTY === false` and `process.stdout.isTTY === true`.

In the same process, `\\.\CONIN$` opens and gives a working terminal: `tty.isatty(fd)` is true and `new tty.ReadStream(fd).setRawMode(true)` succeeds. The reopen part of #20162 works. Only the check before it fails.

### Possible fix

When the launcher marker is set, treat stdin as missing if it is a character device that is not a console. That is what Node puts there in place of the missing handle:

```ts
const stdinMissing = (fd: number) => {
  try {
    return NodeFS.fstatSync(fd).isCharacterDevice() && !NodeTty.isatty(fd);
  } catch {
    return true;
  }
};
```

Trade-off: under the launcher, with output to a console, an explicit `t3 … < NUL` would also get console input. Node makes the two cases look identical from JavaScript, and redirecting output too (as scripts usually do) still skips the reopen. Piped input (a FIFO) and files are unaffected.

It would also help for the test to cover the descriptor Node really leaves behind (an open `NUL`), not a closed fd 0.

### Impact

Major degradation or frequent failure. On Windows, prompts in the desktop app's `t3` don't work. For `doctor`, the only way to delete the database from before 0.1.0 is the interactive picker: by design, `--fix` never selects that repair (#20948). So no CLI command can do that cleanup on Windows.

### Version or commit

T3 Code (Nightly) 0.1.2-nightly.20261022.3541. The same check is on `main` @ 22840ab943.

### Environment

Windows 11 Pro for Workstations 10.0.26200, PowerShell 7.6.7. Desktop app executable: Electron 44.9.3 / Node 24.22.0, run with `ELECTRON_RUN_AS_NODE`.

### Workaround

For `doctor`: quit T3 Code, stop the background service if it is installed, then delete `state.sqlite`, `state.sqlite-wal` and `state.sqlite-shm` next to `statev2.sqlite` by hand. This is what the repair does, and you can no longer roll back to a version before 0.1.0.

