Skip to content

[Bug]: Desktop app's t3 on Windows still has no terminal for prompts: the #20162 stdin check never fires #23815

Open

Description

@CaseyBlackburn

Before submitting

  • I searched existing issues and did not find a duplicate.
  • 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):

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:

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 @ 22840ab.

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions