fix: wrap update banner on mobile
This commit is contained in:
parent
62eb703dcf
commit
afbcc9a6d5
8 changed files with 95 additions and 11 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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);}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue