fix: wrap update banner on mobile

This commit is contained in:
Jordan SkyLF 2026-05-13 22:17:56 -07:00
parent 62eb703dcf
commit afbcc9a6d5
8 changed files with 95 additions and 11 deletions

View file

@ -8,6 +8,8 @@
- **PR #2222** by @franksong2702 — Settings → Appearance now wraps the "Load older messages while scrolling up" checkbox in its own `<label>` AND moves it into its own `settings-field` div instead of leaving it orphaned after the session-jump description with a stray closing `</label>`. Stage-353 maintainer resolution adopted PR #2227's stronger structural variant (each preference in its own `settings-field`) over PR #2222's smaller in-place wrap. Regression test `test_session_endless_scroll.py` pins the new per-label per-settings-field contract.
- Update summaries no longer repeat the same bullet under both “What you’ll notice” and “Worth knowing”; visible notice items keep priority, and the secondary section is omitted when there is no distinct detail to show.
- Update summaries now cap large commit lists before sending them to the summarizer and disclose when the summary uses only the latest commit subjects, while keeping the full comparison link available.
- Update banners now wrap generated-summary links and long update text on narrow/mobile screens instead of overflowing off-screen.
### Added
@ -31,7 +33,6 @@
### Fixed
- **PR #2210** by @Jordan-SkyLF — MCP Tools list in Settings → System no longer renders an unbounded inventory that makes the settings panel scroll-trapping. Added a toolbar (result-count summary, page-size 5/10/20/50/all, search input), bounded scroll area with consistent height, paginated rendering, and focused regression coverage for the large-inventory case. Existing WebUI-only/runtime-only contract preserved (no MCP server probing, no agent-side changes). Visual before/after evidence shipped under `docs/pr-media/2210/`.
- **PR #2213** by @franksong2702 (fixes #2152) — Literal `<think>`/`</think>` discussions in normal assistant prose are no longer stripped from saved messages and re-renders. The old server cleanup and stored-message render regexes stripped the first closed thinking-looking block anywhere in the content. PR aligns saved/static paths with the existing streaming rule: provider reasoning wrappers (`<think>...</think>`, MiniMax `<|channel>thought...<channel|>`, Gemma 4 `<|turn|>thinking...<turn|>`) are stripped only when they lead the response (i.e. the wrapper is the first non-whitespace content).

View file

@ -317,22 +317,31 @@ def _repo_path_for_update_target(target: str):
def _commit_subjects_for_update(info: dict, *, limit: int = 24) -> list[str]:
"""Return commit subjects for an update range, if the local git refs exist."""
subjects, _truncated = _commit_subjects_for_update_with_limit(info, limit=limit)
return subjects
def _commit_subjects_for_update_with_limit(info: dict, *, limit: int = 24) -> tuple[list[str], bool]:
"""Return recent commit subjects plus whether the local list was capped."""
if not isinstance(info, dict):
return []
return [], False
target = info.get('name')
if target not in ('webui', 'agent'):
target = 'webui' if info.get('repo_url', '').endswith('hermes-webui') else target
path = _repo_path_for_update_target(target)
if path is None or not (Path(path) / '.git').exists():
return []
return [], False
current = str(info.get('current_sha') or '').strip()
latest = str(info.get('latest_sha') or '').strip()
if not (current and latest):
return []
out, ok = _run_git(['log', '--format=%s', f'{current}..{latest}', f'-n{limit}'], path, timeout=5)
return [], False
probe_limit = max(1, int(limit)) + 1
out, ok = _run_git(['log', '--format=%s', f'{current}..{latest}', f'-n{probe_limit}'], path, timeout=5)
if not ok or not out:
return []
return [line.strip() for line in out.splitlines() if line.strip()]
return [], False
subjects = [line.strip() for line in out.splitlines() if line.strip()]
truncated = len(subjects) > limit
return subjects[:limit], truncated
def _summary_cache_key(updates: dict, details: list[dict]) -> str:
@ -392,13 +401,25 @@ def _fallback_update_bullets(details: list[dict]) -> list[str]:
commits = item.get('commits') or []
if commits:
highlights = '; '.join(commits[:3])
bullets.append(f"{label} has {behind} update(s), including: {highlights}.")
qualifier = 'recent updates' if item.get('commits_truncated') else 'updates'
bullets.append(f"{label} has {behind} update(s), including {qualifier}: {highlights}.")
else:
bullets.append(f"{label} has {behind} update(s) available.")
return bullets or ['Updates are available.']
def _worth_knowing_bullets(details: list[dict]) -> list[str]:
items = []
truncated = [item for item in details if item.get('commits_truncated') and item.get('commits_limit')]
for item in truncated[:2]:
label = item.get('label') or item.get('name') or 'Hermes'
behind = item.get('behind') or 0
limit = item.get('commits_limit') or len(item.get('commits') or [])
items.append(
f"{label} has {behind} updates; this summary uses the latest {limit} commit subjects, with the full comparison still available in the diff link."
)
if items:
return items
targets = [
f"{item.get('label') or item.get('name') or 'Hermes'} ({item.get('behind') or 0} update{'s' if (item.get('behind') or 0) != 1 else ''})"
for item in details
@ -468,6 +489,10 @@ def _update_summary_prompt(details: list[dict]) -> tuple[str, str]:
user_lines.append(f"{item['label']}: {item['behind']} commit(s) behind")
commits = item.get('commits') or []
if commits:
if item.get('commits_truncated'):
user_lines.append(
f"- Showing latest {len(commits)} of {item['behind']} commit subjects; summarize trends, not every commit."
)
user_lines.extend(f"- {subject}" for subject in commits)
else:
user_lines.append("- No local commit subjects available; summarize only the update count.")
@ -494,14 +519,19 @@ def summarize_update_payload(updates: dict, llm_callback=None, *, target: str |
info = updates.get(key)
if not isinstance(info, dict) or int(info.get('behind') or 0) <= 0:
continue
commit_limit = 24
commits, commits_truncated = _commit_subjects_for_update_with_limit({'name': key, **info}, limit=commit_limit)
behind = int(info.get('behind') or 0)
item = {
'name': key,
'label': label,
'behind': int(info.get('behind') or 0),
'behind': behind,
'current_sha': info.get('current_sha'),
'latest_sha': info.get('latest_sha'),
'compare_url': info.get('compare_url'),
'commits': _commit_subjects_for_update({'name': key, **info}),
'commits': commits,
'commits_limit': commit_limit,
'commits_truncated': bool(commits_truncated or (commits and behind > len(commits))),
}
details.append(item)
cache_key = _summary_cache_key(updates, details)

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View file

@ -568,8 +568,12 @@
.agent-health-dismiss{flex-shrink:0;padding:6px 12px;border-radius:8px;border:1px solid color-mix(in srgb,var(--error) 45%,var(--surface));background:color-mix(in srgb,var(--error) 10%,var(--surface));color:var(--error);font-size:12px;font-weight:600;cursor:pointer;}
.agent-health-dismiss:hover{background:color-mix(in srgb,var(--error) 18%,var(--surface));}
/* ── Update banner ── */
.update-banner{display:none;background:var(--surface);border:1px solid var(--accent);border-radius:10px;padding:10px 16px;margin:10px auto;max-width:780px;font-size:13px;color:var(--accent-text);align-items:center;justify-content:space-between;gap:12px;}
.update-banner{display:none;background:var(--surface);border:1px solid var(--accent);border-radius:10px;padding:10px 16px;margin:10px auto;max-width:780px;width:calc(100% - 24px);box-sizing:border-box;font-size:13px;color:var(--accent-text);align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;overflow-wrap:anywhere;}
.update-banner.visible{display:flex;}
#updateMsg,#updateWhatsNewLinks,#updateSummaryPanel,#updateSummaryDiffLinks{max-width:100%;overflow-wrap:anywhere;word-break:break-word;}
#updateWhatsNewLinks{white-space:normal!important;margin-left:0!important;}
.update-banner>div:last-child{margin-left:auto;}
@media (max-width:600px){.update-banner{width:calc(100% - 16px);padding:10px 12px;gap:10px;}.update-banner>div:last-child{width:100%;justify-content:flex-end;}}
.update-btn{padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;background:var(--accent-bg);border:1px solid var(--accent-bg-strong);color:var(--accent-text);cursor:pointer;transition:background .15s;}
.update-btn:hover{background:var(--accent-bg-strong);}
.update-primary{background:var(--accent-bg-strong);border-color:var(--accent);}

View file

@ -875,6 +875,55 @@ class TestWhatsNewSummaryToggle:
assert result['summary'].count(duplicate_menu_item) == 1
assert result['summary'].count(duplicate_quality_item) == 1
def test_update_summary_many_updates_caps_commit_input_and_discloses_scope(self, monkeypatch):
import api.updates as upd
subjects = [f'Commit subject {idx}' for idx in range(1, 25)]
monkeypatch.setattr(
upd,
'_commit_subjects_for_update_with_limit',
lambda _info, *, limit=24: (subjects[:limit], True),
)
prompts = []
def fake_llm(_system, prompt):
prompts.append(prompt)
return '\n'.join([
'Several user-facing fixes are ready.',
'Settings and update messaging should be easier to understand.',
'The update flow should feel safer and clearer.',
])
result = upd.summarize_update_payload(
{
'webui': {
'behind': 57,
'current_sha': 'abc',
'latest_sha': 'def',
'compare_url': 'https://example.test/webui',
}
},
target='webui',
llm_callback=fake_llm,
use_cache=False,
)
assert len(subjects) == 24
assert prompts
assert 'Showing latest 24 of 57 commit subjects; summarize trends, not every commit.' in prompts[0]
assert 'Commit subject 24' in prompts[0]
assert 'Commit subject 25' not in prompts[0]
sections = {section['title']: section['items'] for section in result['summary_sections']}
assert sections["What you'll notice"] == [
'Several user-facing fixes are ready.',
'Settings and update messaging should be easier to understand.',
'The update flow should feel safer and clearer.',
]
assert sections['Worth knowing'] == [
'WebUI has 57 updates; this summary uses the latest 24 commit subjects, with the full comparison still available in the diff link.'
]
assert result['targets'][0]['commits_truncated'] is True
def test_update_summary_cache_reuses_same_update_summary(self):
import api.updates as upd