feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
"""
|
|
|
|
|
Hermes Web UI -- Self-update checker.
|
|
|
|
|
|
|
|
|
|
Checks if the webui and hermes-agent git repos are behind their upstream
|
|
|
|
|
branches. Results are cached server-side (30-min TTL) so git fetch runs
|
|
|
|
|
at most twice per hour regardless of client count.
|
|
|
|
|
|
|
|
|
|
Skips repos that are not git checkouts (e.g. Docker baked images where
|
|
|
|
|
.git does not exist).
|
|
|
|
|
"""
|
|
|
|
|
import subprocess
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from api.config import REPO_ROOT
|
|
|
|
|
|
|
|
|
|
# Lazy -- may be None if agent not found
|
|
|
|
|
try:
|
|
|
|
|
from api.config import _AGENT_DIR
|
|
|
|
|
except ImportError:
|
|
|
|
|
_AGENT_DIR = None
|
|
|
|
|
|
|
|
|
|
_update_cache = {'webui': None, 'agent': None, 'checked_at': 0}
|
|
|
|
|
_cache_lock = threading.Lock()
|
|
|
|
|
_check_in_progress = False
|
2026-04-05 16:20:12 +00:00
|
|
|
_apply_lock = threading.Lock() # prevents concurrent stash/pull/pop on same repo
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
CACHE_TTL = 1800 # 30 minutes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_git(args, cwd, timeout=10):
|
2026-04-12 07:19:33 +00:00
|
|
|
"""Run a git command and return (useful output, ok).
|
|
|
|
|
|
|
|
|
|
On failure, returns stderr (or stdout as fallback) so callers can
|
|
|
|
|
surface actionable git error messages instead of empty strings.
|
|
|
|
|
"""
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
try:
|
|
|
|
|
r = subprocess.run(
|
|
|
|
|
['git'] + args, cwd=str(cwd), capture_output=True,
|
|
|
|
|
text=True, timeout=timeout,
|
|
|
|
|
)
|
2026-04-12 07:19:33 +00:00
|
|
|
stdout = r.stdout.strip()
|
|
|
|
|
stderr = r.stderr.strip()
|
|
|
|
|
if r.returncode == 0:
|
|
|
|
|
return stdout, True
|
|
|
|
|
return stderr or stdout or f"git exited with status {r.returncode}", False
|
|
|
|
|
except subprocess.TimeoutExpired as exc:
|
|
|
|
|
detail = (getattr(exc, 'stderr', None) or getattr(exc, 'stdout', None) or '').strip()
|
|
|
|
|
return detail or f"git {' '.join(args)} timed out after {timeout}s", False
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
return 'git executable not found', False
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
return f'git failed to start: {exc}', False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _split_remote_ref(ref):
|
|
|
|
|
"""Split 'origin/branch-name' into ('origin', 'branch-name').
|
|
|
|
|
|
|
|
|
|
Returns (None, ref) if ref contains no slash.
|
|
|
|
|
"""
|
|
|
|
|
if '/' not in ref:
|
|
|
|
|
return None, ref
|
|
|
|
|
remote, branch = ref.split('/', 1)
|
|
|
|
|
return remote, branch
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _detect_default_branch(path):
|
|
|
|
|
"""Detect the remote default branch (master or main)."""
|
|
|
|
|
out, ok = _run_git(['symbolic-ref', 'refs/remotes/origin/HEAD'], path)
|
|
|
|
|
if ok and out:
|
|
|
|
|
# refs/remotes/origin/master -> master
|
|
|
|
|
return out.split('/')[-1]
|
|
|
|
|
# Fallback: try master, then main
|
|
|
|
|
for branch in ('master', 'main'):
|
|
|
|
|
_, ok = _run_git(['rev-parse', '--verify', f'origin/{branch}'], path)
|
|
|
|
|
if ok:
|
|
|
|
|
return branch
|
|
|
|
|
return 'master'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _check_repo(path, name):
|
|
|
|
|
"""Check if a git repo is behind its upstream. Returns dict or None."""
|
|
|
|
|
if path is None or not (path / '.git').exists():
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# Fetch latest from origin (network call, cached by TTL)
|
|
|
|
|
_, fetch_ok = _run_git(['fetch', 'origin', '--quiet'], path, timeout=15)
|
|
|
|
|
if not fetch_ok:
|
|
|
|
|
return {'name': name, 'behind': 0, 'error': 'fetch failed'}
|
|
|
|
|
|
2026-04-10 01:10:11 +00:00
|
|
|
# Use the current branch's upstream tracking branch, not the repo default.
|
|
|
|
|
# This avoids false "N updates behind" alerts when the user is on a feature
|
|
|
|
|
# branch and master/main has moved forward with unrelated commits.
|
|
|
|
|
# If no upstream is set (brand-new local branch), fall back to the default branch.
|
|
|
|
|
upstream, ok = _run_git(['rev-parse', '--abbrev-ref', '@{upstream}'], path)
|
|
|
|
|
if ok and upstream:
|
|
|
|
|
# upstream is like "origin/feat/foo" — use it directly in rev-list
|
|
|
|
|
compare_ref = upstream
|
|
|
|
|
else:
|
|
|
|
|
branch = _detect_default_branch(path)
|
|
|
|
|
compare_ref = f'origin/{branch}'
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
|
|
|
|
# Count commits behind
|
2026-04-10 01:10:11 +00:00
|
|
|
out, ok = _run_git(['rev-list', '--count', f'HEAD..{compare_ref}'], path)
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
behind = int(out) if ok and out.isdigit() else 0
|
|
|
|
|
|
|
|
|
|
# Get short SHAs for display
|
|
|
|
|
current, _ = _run_git(['rev-parse', '--short', 'HEAD'], path)
|
2026-04-10 01:10:11 +00:00
|
|
|
latest, _ = _run_git(['rev-parse', '--short', compare_ref], path)
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'name': name,
|
|
|
|
|
'behind': behind,
|
|
|
|
|
'current_sha': current,
|
|
|
|
|
'latest_sha': latest,
|
2026-04-10 01:10:11 +00:00
|
|
|
'branch': compare_ref,
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_for_updates(force=False):
|
|
|
|
|
"""Return cached update status for webui and agent repos."""
|
|
|
|
|
global _check_in_progress
|
|
|
|
|
with _cache_lock:
|
|
|
|
|
if not force and time.time() - _update_cache['checked_at'] < CACHE_TTL:
|
|
|
|
|
return dict(_update_cache)
|
|
|
|
|
if _check_in_progress:
|
|
|
|
|
return dict(_update_cache) # another thread is already checking
|
|
|
|
|
_check_in_progress = True
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Run checks outside the lock (network I/O)
|
|
|
|
|
webui_info = _check_repo(REPO_ROOT, 'webui')
|
|
|
|
|
agent_info = _check_repo(_AGENT_DIR, 'agent')
|
|
|
|
|
|
|
|
|
|
with _cache_lock:
|
|
|
|
|
_update_cache['webui'] = webui_info
|
|
|
|
|
_update_cache['agent'] = agent_info
|
|
|
|
|
_update_cache['checked_at'] = time.time()
|
|
|
|
|
return dict(_update_cache)
|
|
|
|
|
finally:
|
|
|
|
|
_check_in_progress = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_update(target):
|
|
|
|
|
"""Stash, pull --ff-only, pop for the given target repo."""
|
2026-04-05 16:20:12 +00:00
|
|
|
if not _apply_lock.acquire(blocking=False):
|
|
|
|
|
return {'ok': False, 'message': 'Update already in progress'}
|
|
|
|
|
try:
|
|
|
|
|
return _apply_update_inner(target)
|
|
|
|
|
finally:
|
|
|
|
|
_apply_lock.release()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_update_inner(target):
|
|
|
|
|
"""Inner implementation of apply_update, called under _apply_lock."""
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
if target == 'webui':
|
|
|
|
|
path = REPO_ROOT
|
|
|
|
|
elif target == 'agent':
|
|
|
|
|
path = _AGENT_DIR
|
|
|
|
|
else:
|
|
|
|
|
return {'ok': False, 'message': f'Unknown target: {target}'}
|
|
|
|
|
|
|
|
|
|
if path is None or not (path / '.git').exists():
|
|
|
|
|
return {'ok': False, 'message': 'Not a git repository'}
|
|
|
|
|
|
2026-04-10 01:10:11 +00:00
|
|
|
# Use the current branch's upstream for pull, matching the behaviour
|
|
|
|
|
# of _check_repo. Falls back to default branch if no upstream is set.
|
|
|
|
|
upstream, ok = _run_git(['rev-parse', '--abbrev-ref', '@{upstream}'], path)
|
|
|
|
|
if ok and upstream:
|
|
|
|
|
compare_ref = upstream
|
|
|
|
|
else:
|
|
|
|
|
branch = _detect_default_branch(path)
|
|
|
|
|
compare_ref = f'origin/{branch}'
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons
* fix: approval pending check broken by stale has_pending import (#228)
api/routes.py imported has_pending/pop_pending from tools.approval, but the
agent module renamed has_pending to has_blocking_approval (checks gateway
queue, not _pending dict) and removed pop_pending. The import fell through
to fallback lambdas that always returned False, making GET /api/approval/pending
always return {pending:null} even after a successful inject_test.
Fix: check _pending directly under _lock — same dict submit_pending writes to.
Stale imports removed.
Before: 554 pass, 1 fail | After: 555 pass, 0 fail
* fix: move login JS into external file, remove inline handlers (#226)
Login page used inline onsubmit/onkeydown handlers and an inline <script>
block — all blocked by strict script-src CSP, causing silent login failure.
Fix: extract doLogin() and Enter key listener into static/login.js (served
from /static/, already a public path). Form uses id='login-form' and
data-* attributes for i18n strings instead of injected JS literals.
Also guards res.json() parse with try/catch so non-JSON error bodies
(e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'.
Fixes #222.
* fix: improve update error messages when pull fails (#227)
_apply_update_inner() ran git pull --ff-only and returned only raw stderr
on failure, making all failure modes indistinguishable.
Fix: explicit git fetch before pull; if fetch fails, returns human-readable
network error. Diverged history and missing upstream tracking branch each
get distinct messages with exact recovery commands. Generic fallback
truncates to 300 chars and shows sentinel when git produces no output.
Also adds tests/test_update_checker.py with 13 tests covering all 4 new
diagnostic code paths (0 tests existed before).
Fixes #223.
* fix: stabilize 30s terminal approval prompt visibility (#225)
Adds minimum 30-second visibility guard for the approval card using
_approvalVisibleSince, _approvalHideTimer, and a signature fingerprint
to deduplicate repeated poll ticks.
Fix: respondApproval() and all stream-end paths (done/cancel/apperror/
error/start-error) now call hideApprovalCard(true) so the card hides
immediately when the user responds or the session ends. The 30s guard
only applies to mid-session poll ticks where the approval is still live
but briefly absent.
Adds 11 structural tests covering the new timer variables, force
parameter, force-on-respond, force-on-stream-end, and poll-loop
no-force behavior.
* feat: replace emoji icons with self-hosted Lucide SVG icons (#221)
Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled
in static/icons.js (no CDN dependency). Adds li(name) function returning
inline SVG geometry from a hardcoded whitelist — unknown keys return '' so
dynamic server-supplied names never inject arbitrary SVG.
Changes:
- static/icons.js: new file with 21 icon paths + li() renderer
- static/index.html: all nav/action buttons now use li() icons
- static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons
- static/messages.js: cancelStream button uses SVG square stop icon
- .gitignore: adds node_modules/ entry
Verified: all 35 onclick= functions exist in JS, all 21 li() calls
reference defined icons, applyBotName() selectors intact, version
label present, no removed IDs referenced by JS.
* docs: v0.44.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 17:02:28 +00:00
|
|
|
# Fetch before attempting pull, so the remote ref is current.
|
|
|
|
|
_, fetch_ok = _run_git(['fetch', 'origin', '--quiet'], path, timeout=15)
|
|
|
|
|
if not fetch_ok:
|
|
|
|
|
return {
|
|
|
|
|
'ok': False,
|
|
|
|
|
'message': (
|
|
|
|
|
'Could not reach the remote repository. '
|
|
|
|
|
'Check your internet connection and try again.'
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 07:19:33 +00:00
|
|
|
# Check for dirty working tree (ignore untracked files — git stash
|
|
|
|
|
# doesn't include them, so stashing on '??' alone leaves nothing to pop)
|
|
|
|
|
status_out, status_ok = _run_git(
|
|
|
|
|
['status', '--porcelain', '--untracked-files=no'], path
|
|
|
|
|
)
|
|
|
|
|
if not status_ok:
|
|
|
|
|
return {'ok': False, 'message': f'Failed to inspect repo status: {status_out[:200]}'}
|
|
|
|
|
# Fail early on unresolved merge conflicts
|
|
|
|
|
if any(line[:2] in {'DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'}
|
|
|
|
|
for line in status_out.splitlines()):
|
|
|
|
|
return {'ok': False, 'message': 'Repository has unresolved merge conflicts'}
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
stashed = False
|
|
|
|
|
if status_out:
|
|
|
|
|
_, ok = _run_git(['stash'], path)
|
|
|
|
|
if not ok:
|
|
|
|
|
return {'ok': False, 'message': 'Failed to stash local changes'}
|
|
|
|
|
stashed = True
|
|
|
|
|
|
2026-04-12 07:19:33 +00:00
|
|
|
# Pull with ff-only (no merge commits).
|
|
|
|
|
# Split tracking refs like 'origin/main' into separate remote + branch
|
|
|
|
|
# arguments — git treats 'origin/main' as a repository name otherwise.
|
|
|
|
|
remote, branch = _split_remote_ref(compare_ref)
|
|
|
|
|
pull_args = ['pull', '--ff-only']
|
|
|
|
|
if remote:
|
|
|
|
|
pull_args.extend([remote, branch])
|
|
|
|
|
else:
|
|
|
|
|
pull_args.append(compare_ref)
|
|
|
|
|
pull_out, pull_ok = _run_git(pull_args, path, timeout=30)
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
if not pull_ok:
|
|
|
|
|
if stashed:
|
|
|
|
|
_run_git(['stash', 'pop'], path)
|
v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons
* fix: approval pending check broken by stale has_pending import (#228)
api/routes.py imported has_pending/pop_pending from tools.approval, but the
agent module renamed has_pending to has_blocking_approval (checks gateway
queue, not _pending dict) and removed pop_pending. The import fell through
to fallback lambdas that always returned False, making GET /api/approval/pending
always return {pending:null} even after a successful inject_test.
Fix: check _pending directly under _lock — same dict submit_pending writes to.
Stale imports removed.
Before: 554 pass, 1 fail | After: 555 pass, 0 fail
* fix: move login JS into external file, remove inline handlers (#226)
Login page used inline onsubmit/onkeydown handlers and an inline <script>
block — all blocked by strict script-src CSP, causing silent login failure.
Fix: extract doLogin() and Enter key listener into static/login.js (served
from /static/, already a public path). Form uses id='login-form' and
data-* attributes for i18n strings instead of injected JS literals.
Also guards res.json() parse with try/catch so non-JSON error bodies
(e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'.
Fixes #222.
* fix: improve update error messages when pull fails (#227)
_apply_update_inner() ran git pull --ff-only and returned only raw stderr
on failure, making all failure modes indistinguishable.
Fix: explicit git fetch before pull; if fetch fails, returns human-readable
network error. Diverged history and missing upstream tracking branch each
get distinct messages with exact recovery commands. Generic fallback
truncates to 300 chars and shows sentinel when git produces no output.
Also adds tests/test_update_checker.py with 13 tests covering all 4 new
diagnostic code paths (0 tests existed before).
Fixes #223.
* fix: stabilize 30s terminal approval prompt visibility (#225)
Adds minimum 30-second visibility guard for the approval card using
_approvalVisibleSince, _approvalHideTimer, and a signature fingerprint
to deduplicate repeated poll ticks.
Fix: respondApproval() and all stream-end paths (done/cancel/apperror/
error/start-error) now call hideApprovalCard(true) so the card hides
immediately when the user responds or the session ends. The 30s guard
only applies to mid-session poll ticks where the approval is still live
but briefly absent.
Adds 11 structural tests covering the new timer variables, force
parameter, force-on-respond, force-on-stream-end, and poll-loop
no-force behavior.
* feat: replace emoji icons with self-hosted Lucide SVG icons (#221)
Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled
in static/icons.js (no CDN dependency). Adds li(name) function returning
inline SVG geometry from a hardcoded whitelist — unknown keys return '' so
dynamic server-supplied names never inject arbitrary SVG.
Changes:
- static/icons.js: new file with 21 icon paths + li() renderer
- static/index.html: all nav/action buttons now use li() icons
- static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons
- static/messages.js: cancelStream button uses SVG square stop icon
- .gitignore: adds node_modules/ entry
Verified: all 35 onclick= functions exist in JS, all 21 li() calls
reference defined icons, applyBotName() selectors intact, version
label present, no removed IDs referenced by JS.
* docs: v0.44.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 17:02:28 +00:00
|
|
|
|
|
|
|
|
# Diagnose the most common failure modes and surface actionable messages.
|
|
|
|
|
pull_lower = pull_out.lower()
|
|
|
|
|
if 'not possible to fast-forward' in pull_lower or 'diverged' in pull_lower:
|
|
|
|
|
return {
|
|
|
|
|
'ok': False,
|
|
|
|
|
'message': (
|
|
|
|
|
f'The local {target} repo has commits that are not on the remote '
|
|
|
|
|
'branch, so a fast-forward update is not possible. '
|
|
|
|
|
'Run: git -C ' + str(path) + ' fetch origin && '
|
|
|
|
|
'git -C ' + str(path) + ' reset --hard ' + compare_ref
|
|
|
|
|
),
|
|
|
|
|
'diverged': True,
|
|
|
|
|
}
|
|
|
|
|
if 'does not track' in pull_lower or 'no tracking information' in pull_lower:
|
|
|
|
|
return {
|
|
|
|
|
'ok': False,
|
|
|
|
|
'message': (
|
|
|
|
|
f'The local {target} branch has no upstream tracking branch configured. '
|
|
|
|
|
'Run: git -C ' + str(path) + ' branch --set-upstream-to=' + compare_ref
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
# Generic fallback — include the raw git output for debugging.
|
|
|
|
|
detail = pull_out.strip()[:300] if pull_out.strip() else '(no output from git)'
|
|
|
|
|
return {'ok': False, 'message': f'Pull failed: {detail}'}
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
|
|
|
|
|
# Pop stash if we stashed
|
|
|
|
|
if stashed:
|
|
|
|
|
_, pop_ok = _run_git(['stash', 'pop'], path)
|
|
|
|
|
if not pop_ok:
|
|
|
|
|
return {
|
|
|
|
|
'ok': False,
|
|
|
|
|
'message': 'Updated but stash pop failed -- manual merge needed',
|
|
|
|
|
'stash_conflict': True,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Invalidate cache
|
|
|
|
|
with _cache_lock:
|
|
|
|
|
_update_cache['checked_at'] = 0
|
|
|
|
|
|
|
|
|
|
return {'ok': True, 'message': f'{target} updated successfully', 'target': target}
|