Merge pull request #5526 from nesquena/release/stage-5522

Release — Windows git.exe resolution for version/cache-busting (#5522)
This commit is contained in:
nesquena-hermes 2026-07-04 01:19:58 -07:00 committed by GitHub
commit edfccb9cd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 204 additions and 0 deletions

View file

@ -14,6 +14,7 @@
### Fixed
- **Windows: after an upgrade, browsers no longer keep serving stale JS/CSS when git isn't on the server's PATH.** When the WebUI server was launched on Windows from an environment without git on `PATH` (a venv, a service wrapper, a non-interactive launcher), version detection silently degraded to `unknown`, which froze the `?v=<stamp>` static-asset cache key — so browsers kept serving cached `ui.js` / `messages.js` / CSS even after the server restarted with fixed code, and the only workaround was a manual hard-refresh on every device. Version detection now resolves `git.exe` from the Git-for-Windows registry key (and common install paths) when it isn't on `PATH`, so the cache stamp updates and clients pick up frontend fixes automatically. Non-Windows behavior is unchanged. Thanks @allenliang2022. (#5522)
- **Transparent Stream is smoother during streaming — less row churn and no thinking-block scrollbar flicker.** Building on the identity-preserving live-row reconcile, the renderer now skips redundant work while a response streams: a preserved row's markup isn't rewritten when the incoming HTML is unchanged, a row isn't reinserted when it's already in the correct position, and long thinking blocks no longer flicker an internal scrollbar as their scroll container appears. Purely a reduction of no-op DOM work — what renders is unchanged, and matching rows still preserve their DOM node, copy button, tool-call data, and expanded/collapsed state. Thanks @Stacey2911. (#5456, #5367)
- **Models from a user-defined provider in `config.yaml` now route correctly.** If you defined a custom provider under the `providers:` block with an explicit `models:` allowlist, a bare model id from that list wasn't matched by request-time routing — it silently fell back to the default provider and often 404'd. Resolution now scans your `providers:` allowlists (exact match, same as `custom_providers:`), so those models reach the right endpoint. Copilot is deliberately excluded from this scan because `providers.copilot.models` is a per-model settings map (reasoning effort, limits), not a routing allowlist. Thanks @akay64. (#5511)
- **The transcript no longer scrolls up when the composer grows.** When you were pinned to the bottom of a conversation and the composer got taller — multi-line typing, `Shift+Enter`, or a multi-line / dictation paste — the transcript viewport shrank by the same amount and left you stranded a row or two above the bottom (it read as the chat "randomly scrolling up" during normal use). The transcript now re-pins to the bottom whenever the composer grows, but only when you were genuinely still pinned there — if you'd scrolled up to read history, nothing yanks you back down. (#5516, #5514, #5515)

View file

@ -214,12 +214,60 @@ def _run_git(args, cwd, timeout=10):
return f'git failed to start: {exc}', False
def _windows_git_from_registry():
"""Best-effort resolve git.exe from the Git-for-Windows registry key.
Git for Windows records its install root at
``HKLM\\SOFTWARE\\GitForWindows\\InstallPath`` (and the WOW6432Node mirror
for a 32-bit install on 64-bit Windows). ``git.exe`` lives under
``<InstallPath>\\cmd\\git.exe``. This is the reliable way to find git when
it is installed but NOT on the launching process's PATH — e.g. the WebUI
server started from a venv python whose environment does not inherit the
interactive shell PATH, which otherwise degrades WEBUI_VERSION to
``'unknown'`` and freezes the ``?v=`` static-asset cache-busting stamp.
"""
try:
import winreg
except ImportError:
return None
for hive, flag in (
(winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_64KEY),
(winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_32KEY),
(winreg.HKEY_CURRENT_USER, 0),
):
try:
with winreg.OpenKey(
hive, r'SOFTWARE\GitForWindows', 0,
winreg.KEY_READ | flag,
) as key:
install_path, _ = winreg.QueryValueEx(key, 'InstallPath')
except OSError:
continue
if not install_path:
continue
candidate = os.path.join(install_path, 'cmd', 'git.exe')
if os.path.exists(candidate):
return candidate
return None
def _resolve_git_executable():
git_executable = shutil.which('git')
if git_executable:
return git_executable
if sys.platform == 'darwin' and os.path.exists('/usr/bin/git'):
return '/usr/bin/git'
if sys.platform == 'win32':
from_registry = _windows_git_from_registry()
if from_registry:
return from_registry
for candidate in (
os.path.expandvars(r'%ProgramFiles%\Git\cmd\git.exe'),
os.path.expandvars(r'%ProgramFiles(x86)%\Git\cmd\git.exe'),
os.path.expandvars(r'%LocalAppData%\Programs\Git\cmd\git.exe'),
):
if candidate and os.path.exists(candidate):
return candidate
return None

View file

@ -0,0 +1,155 @@
"""Windows parallel to issue #5175 (macOS launchd git-on-PATH miss).
When the source WebUI server is launched from a Python environment whose PATH
does not include git (e.g. a venv on Windows, mirroring the macOS launchd case),
``shutil.which('git')`` returns None. Before this fix ``_resolve_git_executable``
had only a darwin fallback, so on Windows it returned None -> ``git describe``
never ran -> ``WEBUI_VERSION`` degraded to ``'unknown'`` -> the ``?v=<stamp>``
static-asset cache key froze, so browsers kept serving stale cached JS/CSS even
after the server restarted with fixed code.
The fix adds a Windows fallback that resolves git.exe from the Git-for-Windows
registry ``InstallPath`` (and common install dirs) without relying on PATH.
These tests mock the OS-specific surfaces (winreg, os.path.exists,
os.path.expandvars) so they exercise the Windows code path deterministically on
ANY host — the CI runners are Linux, where %VAR% expansion and native path
separators differ, so nothing here may depend on the running platform's real
environment or filesystem.
"""
import sys
from unittest.mock import MagicMock, patch
import api.updates as updates
# Deterministic install root used by the winreg stub. Kept as a forward-slash
# string so the expected git path is identical regardless of the host's
# os.path.join separator (CI is Linux; dev is Windows).
FAKE_GIT_ROOT = 'C:/Program Files/Git'
FAKE_GIT_EXE = FAKE_GIT_ROOT + '/cmd/git.exe'
def _fake_winreg(install_path=FAKE_GIT_ROOT):
"""Minimal winreg stub whose OpenKey/QueryValueEx yield a Git InstallPath."""
mod = MagicMock()
mod.HKEY_LOCAL_MACHINE = 0
mod.HKEY_CURRENT_USER = 1
mod.KEY_READ = 0x20019
mod.KEY_WOW64_64KEY = 0x0100
mod.KEY_WOW64_32KEY = 0x0200
class _Key:
def __enter__(self):
return self
def __exit__(self, *a):
return False
mod.OpenKey.return_value = _Key()
mod.QueryValueEx.return_value = (install_path, 1)
return mod
def _patch_join_forward_slash():
"""Make os.path.join deterministic (forward slash) regardless of host OS.
The production code builds candidates with os.path.join; on Linux CI that
uses '/', on Windows '\\'. Pin it so expected paths match on both.
"""
def fake_join(*parts):
return '/'.join(p.rstrip('/\\') for p in parts)
return patch.object(updates.os.path, 'join', side_effect=fake_join)
def test_resolve_git_executable_uses_registry_on_windows():
winreg = _fake_winreg()
with patch.object(updates.shutil, 'which', return_value=None), \
patch.object(sys, 'platform', 'win32'), \
patch.dict('sys.modules', {'winreg': winreg}), \
_patch_join_forward_slash(), \
patch.object(updates.os.path, 'exists', side_effect=lambda p: p == FAKE_GIT_EXE):
resolved = updates._resolve_git_executable()
assert resolved == FAKE_GIT_EXE
def test_resolve_git_executable_probes_known_dirs_when_registry_absent():
# Registry has no value -> QueryValueEx raises -> fall through to dir probe.
winreg = _fake_winreg()
winreg.QueryValueEx.side_effect = OSError('no InstallPath')
# The code probes os.path.expandvars('%ProgramFiles%\\Git\\cmd\\git.exe');
# mock expandvars deterministically so the test does not depend on the
# host's %VAR% expansion (Linux CI does not expand %ProgramFiles%).
probed = 'C:/Program Files/Git/cmd/git.exe'
def fake_expandvars(s):
if 'ProgramFiles%' in s and '(x86)' not in s:
return probed
# Other candidates resolve to a sentinel that never "exists".
return '/nonexistent/' + s
with patch.object(updates.shutil, 'which', return_value=None), \
patch.object(sys, 'platform', 'win32'), \
patch.dict('sys.modules', {'winreg': winreg}), \
patch.object(updates.os.path, 'expandvars', side_effect=fake_expandvars), \
patch.object(updates.os.path, 'exists', side_effect=lambda p: p == probed):
resolved = updates._resolve_git_executable()
assert resolved == probed
def test_resolve_git_executable_returns_none_on_windows_when_git_truly_absent():
winreg = _fake_winreg()
winreg.QueryValueEx.side_effect = OSError('no InstallPath')
with patch.object(updates.shutil, 'which', return_value=None), \
patch.object(sys, 'platform', 'win32'), \
patch.dict('sys.modules', {'winreg': winreg}), \
patch.object(updates.os.path, 'expandvars', side_effect=lambda s: s), \
patch.object(updates.os.path, 'exists', return_value=False):
resolved = updates._resolve_git_executable()
assert resolved is None
def test_non_windows_never_touches_registry_fallback():
"""Falsifiable guard: the registry path must be Windows-only."""
winreg = _fake_winreg()
with patch.object(updates.shutil, 'which', return_value=None), \
patch.object(sys, 'platform', 'linux'), \
patch.dict('sys.modules', {'winreg': winreg}), \
patch.object(updates.os.path, 'exists', return_value=True):
resolved = updates._resolve_git_executable()
assert resolved is None
winreg.OpenKey.assert_not_called()
def test_detect_webui_version_recovers_via_windows_registry_fallback(tmp_path):
winreg = _fake_winreg()
def fake_exists(p):
# git.exe resolves; no api/_version.py fallback file present.
return p == FAKE_GIT_EXE
def fake_run(cmd, **kwargs):
assert cmd[0] == FAKE_GIT_EXE
if cmd[1:] == ['describe', '--tags', '--always']:
return MagicMock(returncode=0, stdout='v0.51.999\n', stderr='')
if cmd[1:] == ['diff-index', '--quiet', 'HEAD', '--']:
return MagicMock(returncode=0, stdout='', stderr='')
raise AssertionError(f'unexpected git args: {cmd[1:]!r}')
with patch.object(updates.shutil, 'which', return_value=None), \
patch.object(sys, 'platform', 'win32'), \
patch.dict('sys.modules', {'winreg': winreg}), \
_patch_join_forward_slash(), \
patch.object(updates.os.path, 'exists', side_effect=fake_exists), \
patch.object(updates, 'REPO_ROOT', tmp_path), \
patch.object(updates.subprocess, 'run', side_effect=fake_run):
version = updates._detect_webui_version()
assert version == 'v0.51.999'