Repository navigation
[Bug]: Desktop app's t3 on Windows still has no terminal for prompts: the #20162 stdin check never fires #23815
Description
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
- On Windows, install T3 Code (Nightly) and use its
t3command (~/.t3/bin/t3.cmd). - Have a
state.sqlitefrom before 0.1.0 next tostatev2.sqlite. - 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