Skip to content

fix(windows): repair coder mode input, path validation, prompt rendering and version info#1177

Merged
diillson merged 3 commits into
mainfrom
fix/windows-coder-experience
Jul 12, 2026
Merged

fix(windows): repair coder mode input, path validation, prompt rendering and version info#1177
diillson merged 3 commits into
mainfrom
fix/windows-coder-experience

Conversation

@diillson

Copy link
Copy Markdown
Owner

Summary

Windows users hit four independent defects that together made coder mode nearly unusable. This PR fixes all of them at root cause, plus the security gaps a full Windows path-handling sweep uncovered.

1. Coder mode froze after the first message

Typing showed nothing until Enter was pressed. Root cause: the centralized stdin reader polls with WaitForSingleObject, which signals for any console event — resize, focus, mouse — not just keystrokes. The goroutine then entered a blocking console read that stopStdinReader could not cancel, and the orphaned read stole keystrokes from the next go-prompt instance (which is what echoes them).

  • The poll now classifies pending records with PeekConsoleInput: only a key-down carrying a character counts as ready, and non-text records are drained so they stop re-signaling.
  • A still-blocked read is aborted with CancelSynchronousIo on the reader's locked OS thread — the documented API for cancelling synchronous console I/O. No synthetic input, no console-buffer mutation.
  • RestoreCookedMode and resetTTYToSane now have a real SetConsoleMode implementation instead of being no-ops on Windows, so a raw mode left behind by a dirty overlay teardown no longer persists forever.

2. Coder write rejected every valid Windows path

Boundary checks concatenated a slash by hand, which never matches backslashed absolute paths — so anything short of exact equality was outside the workspace boundary. The new stdlib-only pkg/fspath package normalizes separators and compares case-insensitively on Windows, and now backs the engine and both agent validators.

The sweep across all tools also fixed real security gaps on Windows:

  • The sensitive-file read blocklist was anchored to HOME, which is unset on Windows — SSH, AWS, GPG keys and kubeconfig were readable in agent mode. Now uses os.UserHomeDir.
  • CHATCLI_AGENT_EXTRA_READ_PATHS split on colon, breaking drive letters. Now filepath.SplitList.
  • Path traversal detection only matched forward-slash traversal; backslash traversal passed.
  • Sensitive-path matching was case-sensitive on a case-insensitive filesystem.
  • Command allowlists did not strip backslashed directory prefixes nor Windows executable extensions.
  • SystemRoot joined the sensitive-path blocklist as the Windows counterpart of the unix system dirs.

3. Tool args with Windows paths aborted the call

Models emit single-backslash Windows paths — invalid JSON escapes — and sometimes double-encode the args envelope as a JSON string. Both aborted the tool call with parsing errors. RepairJSONInvalidEscapes repairs dirty string literals with an all-or-nothing per-literal rule: a string containing any invalid escape is treated as unescaped, so coincidentally valid escapes inside it (backslash-b in builder, backslash-t in temp) become path separators, while clean sibling strings keep their legitimate escapes. The flattener and the policy normalizer both recover string envelopes, so the security prompt shows real flags instead of a JSON blob.

4. Prompt climbed and left ghost lines; version info empty on go install

  • go-prompt skips the wrap-materializing newline on Windows, assuming legacy immediate wrap — but VT-enabled consoles (Windows Terminal, conhost with VT) use deferred wrap, desyncing its cursor model one row per exact column boundary. With the animated live prefix re-rendering four times a second, the prompt climbed continuously and the completion dropdown left ghost copies of the input line. A wrap-aware ConsoleWriter (OptionWriter, gated on VT being active) tracks the cursor column and injects the newline at the boundary, aligning the console with go-prompt's model. The palette overlay also erases the committed trigger line before opening.
  • Version displays snapshotted ldflags globals before the update check enriched them, so go install builds always showed unknown commit and date — and one display path had already drifted out of the required call order. The version package is now side-effect free: a single injectable fetch seam, pure enrichment on VersionInfo, and GetReport composing everything order-independently for all three display paths.

Testing

  • Unit tests for every fix, including regression fixtures reproducing the exact field failures; red-green verified by reverting the flattener and watching the new tests fail.
  • Full suite green; builds for darwin, linux and windows; golangci-lint clean natively and with GOOS=windows (which the native run does not cover for the _windows.go files).
  • Known limitation stated in code: resizing the terminal mid-session still confuses go-prompt's frozen width on Windows; that requires the planned TUI migration.

Impact

No API or dependency changes. go.mod untouched. All changes are Windows-gated or behavior-preserving on unix.

diillson added 2 commits July 12, 2026 19:13
…ork on Windows

Coder mode input hang: the stdin reader goroutine could get stuck in a
blocking console read after WaitForSingleObject signaled for non-key
events, stealing keystrokes from the next prompt until Enter was
pressed. The poll now classifies pending console records via
PeekConsoleInput and only reports readiness for real text input, and
stopStdinReader aborts an in-flight read with CancelSynchronousIo on the
reader thread. RestoreCookedMode and resetTTYToSane gained a real
Windows implementation via SetConsoleMode instead of being no-ops.

Workspace boundary: path checks concatenated a slash separator by hand,
which never matches backslashed absolute paths, so every coder write
outside exact equality was rejected on Windows. The new stdlib-only
pkg/fspath package provides WithinBoundary and Equal with normalized
separators and case-insensitive comparison on Windows, now used by the
engine, the agent path validators and the read validator. The sweep
also fixed real security gaps: the sensitive-file read blocklist was
anchored to HOME which is unset on Windows, extra read paths split on
colon breaking drive letters, path traversal detection ignored
backslash traversal, sensitive-path matching was case-sensitive on a
case-insensitive filesystem, and command allowlists did not strip
backslashed directories or Windows executable extensions.

Tool args: models emit Windows paths with single backslashes, which are
invalid JSON escapes, and sometimes double-encode the args envelope as
a string. Both used to abort the tool call. RepairJSONInvalidEscapes
now repairs dirty string literals with an all-or-nothing per-literal
rule so coincidentally valid escapes inside dirty strings are treated
as path separators while clean sibling strings keep their escapes, and
the flattener and policy normalizer recover string envelopes.

Prompt rendering: go-prompt skips the wrap-materializing newline on
Windows, assuming legacy immediate wrap, but VT-enabled consoles use
deferred wrap, desyncing its cursor model one row per exact column
boundary. That made the prompt climb and leave ghost lines with large
input and with the completion dropdown. A wrap-aware ConsoleWriter now
tracks the cursor column and injects the newline at the boundary,
gated on VT being active. The palette overlay erases the committed
trigger line before opening, and the coder inter-turn prompt gained the
AltGr sanitizer and the same writer.
…ation

Version displays snapshotted the raw ldflags globals before the update
check ran, and the check enriched those globals as a side effect, so
builds installed with go install always showed unknown commit and date.
One display path had already drifted out of the required call order,
proving the ordering contract fragile.

The version package is now side-effect free: FetchLatestReleaseImpl is
the single injectable network seam, enrichment is a pure method on
VersionInfo that fills commit and date from release metadata only when
the build itself could not stamp them, and GetReport composes build
info resolution, update check and enrichment in one call used by all
display paths. The approximate binary-date fallback is marked with a
shared suffix constant so the release publish date can take precedence
over it, while ldflags-stamped data always wins. Tests now exercise the
real default fetch through a test server instead of mocking a copy of
the implementation.
@github-actions

github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Quality Gate

Result: ✅ all floors passed

Floor Status Result Δ vs main Budget
1 · Build & Static go build / vet / fmt / lint
2 · Coverage 50.6% (bootstrap) 0 ≥ baseline
3 · Patch coverage 70.6% (req ≥ 60%) ≥ 60%
4 · AI smells diff scanned
5 · Scope budget ⚠️ 36 files / 1832 LOC (code 1832 + tooling 0) warn 800·25
6 · E2E go test -race ./e2e/... ≤ 15min
7 · Commit lint conventional commits
8 · Cyclo (new code) 26 file(s) under threshold ≤ 30
9 · Secrets scan gitleaks
10 · i18n parity missing 0, unknown 0
11 · CRD drift drifted: 0
12 · License headers 0 missing
13 · API breaking 0 incompatible
14 · Binary size chatcli 90.9MB · operator 52.3MB 100MB each
15 · Provider parity 15 providers · 0 violations

Config: .github/quality-gate.yml. Workflow: .github/workflows/quality-gate.yml.

@diillson diillson added the breaking-change This PR removes or changes exported APIs intentionally label Jul 12, 2026
@diillson

Copy link
Copy Markdown
Owner Author

Migration note for the intentional API break flagged by Floor 13:

  • Removed: CheckLatestVersionImpl (injectable var). It coupled the network fetch, the comparison logic and a hidden side effect that mutated package globals — the root cause of the stale /version display this PR fixes.
  • Replacement for callers: GetReport composes build-info resolution, update check and release enrichment order-independently; CheckLatestVersionWithContext remains for plain checks.
  • Replacement for tests: inject FetchLatestReleaseImpl (pure network seam) or point CHATCLI_LATEST_VERSION_URL at a test server to exercise the real fetch path. All in-repo consumers and tests were migrated in this PR; a repo-wide grep shows no other usage.

…njection

Go error strings are not capitalized, and the quality gate reads a
capitalized literal under the cli tree as an untranslated user-facing
message. The rewritten message also names the operation instead of just
the API call.
@diillson diillson force-pushed the fix/windows-coder-experience branch from d7c091e to 1c23fa2 Compare July 12, 2026 22:32
@diillson diillson merged commit 74c8b6e into main Jul 12, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change This PR removes or changes exported APIs intentionally

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant