Add current branch latest version display to self-update modal with collapsible last attempt section
- Add get_current_branch_latest_info helper to fetch and format latest version info for current branch - Add _format_branch_head_version helper to format version display (tag only for main, tag+commits for other branches) - Include current_branch_latest in get_update_info response - Replace single current version card with two-column grid showing current and latest versions - Move last attempt status
frdel committed
Mar 26, 2026 at 11:09 UTC
c1d709726e2c2e6e6c03fb6c9882cc62f5778773
5 files changed
+208
-40
helpers/self_update.py
+47
@@ -388,6 +388,49 @@ def _format_latest_selector_label(branch: str, describe: str) -> str:
388
return f"latest ({short_tag}+{commits_since_tag})"
389
390
391
+def _format_branch_head_version(branch: str, describe: str) -> str:
392
+ short_tag, commits_since_tag = _split_describe_version(describe)
393
+ if not short_tag:
394
+ return ""
395
+ if branch.strip().lower() == "main" or commits_since_tag <= 0:
396
+ return short_tag
397
+ return f"{short_tag}+{commits_since_tag}"
398
+
399
+
400
+def get_current_branch_latest_info(
401
+ current_branch: str,
402
+ *,
403
+ repo_dir: str | Path | None = None,
404
+) -> dict[str, Any]:
405
+ repository = get_repo_dir(repo_dir)
406
+ normalized_branch = current_branch.strip().lower()
407
+ if normalized_branch not in SUPPORTED_BRANCHES:
408
+ return {
409
+ "branch": current_branch.strip(),
410
+ "supported": False,
411
+ "describe": "",
412
+ "short_tag": "",
413
+ "display_version": "",
414
+ "commit": "",
415
+ "short_commit": "",
416
+ }
417
+
418
+ branch_head_info = _get_branch_head_info(normalized_branch, repo_dir=repository)
419
+ commit = branch_head_info.get("commit", "")
420
+ return {
421
+ "branch": normalized_branch,
422
+ "supported": True,
423
+ "describe": branch_head_info.get("describe", ""),
424
+ "short_tag": branch_head_info.get("short_tag", ""),
425
+ "display_version": _format_branch_head_version(
426
+ normalized_branch,
427
+ branch_head_info.get("describe", ""),
428
+ ),
429
+ "commit": commit,
430
+ "short_commit": commit[:7] if commit else "",
431
+ }
432
+
433
+
434
def get_available_tags(
435
branch: str | None = None,
436
*,
@@ -477,6 +520,10 @@ def get_update_info(repo_dir: str | Path | None = None) -> dict[str, Any]:
520
return {
521
"repo_dir": str(repository),
522
"current": version_info,
523
+ "current_branch_latest": get_current_branch_latest_info(
524
+ current_branch,
525
+ repo_dir=repository,
526
+ ),
527
"pending": load_pending_update(),
528
"last_status": load_last_status(),
529
"branches": BRANCH_OPTIONS,
tests/test_self_update_tag_filter.py
+56
@@ -99,6 +99,59 @@ def test_self_update_selector_tag_options_filter_to_current_major(monkeypatch):
99
assert higher_major_versions == [2, 3]
100
101
102
+def test_self_update_update_info_uses_current_branch_for_latest_version(monkeypatch):
103
+ monkeypatch.setattr(
104
+ self_update,
105
+ "get_repo_version_info",
106
+ lambda _repo=None: {
107
+ "branch": "main",
108
+ "describe": "v1.2",
109
+ "short_tag": "v1.2",
110
+ "commit": "abc1234def",
111
+ "short_commit": "abc1234",
112
+ },
113
+ )
114
+ monkeypatch.setattr(
115
+ self_update,
116
+ "get_selector_tag_options",
117
+ lambda branch, *, repo_dir=None, current_version=None: (
118
+ [{"value": "latest", "label": "latest (v1.4)"}],
119
+ [],
120
+ "",
121
+ ),
122
+ )
123
+ monkeypatch.setattr(self_update, "load_pending_update", lambda: None)
124
+ monkeypatch.setattr(self_update, "load_last_status", lambda: None)
125
+ monkeypatch.setattr(
126
+ self_update,
127
+ "_get_branch_head_info",
128
+ lambda branch, repo_dir=None: {
129
+ "main": {
130
+ "describe": "v1.4",
131
+ "short_tag": "v1.4",
132
+ "commit": "def5678abcd",
133
+ },
134
+ "development": {
135
+ "describe": "v9.9-3-gfeedbee",
136
+ "short_tag": "v9.9",
137
+ "commit": "feedbee1234",
138
+ },
139
+ }[branch],
140
+ )
141
+
142
+ info = self_update.get_update_info()
143
+
144
+ assert info["current_branch_latest"] == {
145
+ "branch": "main",
146
+ "supported": True,
147
+ "describe": "v1.4",
148
+ "short_tag": "v1.4",
149
+ "display_version": "v1.4",
150
+ "commit": "def5678abcd",
151
+ "short_commit": "def5678",
152
+ }
153
+
154
+
155
def test_self_update_frontend_uses_preloaded_select():
156
store_path = (
157
PROJECT_ROOT
@@ -154,7 +207,10 @@ def test_self_update_modal_uses_standard_select_and_manual_backup():
207
assert "$store.selfUpdateStore.versionSelectPlaceholder" in content
208
assert "$store.selfUpdateStore.higherMajorVersionMessage" in content
209
assert "$store.selfUpdateStore.availableTagOptions" in content
210
+ assert "current_branch_latest?.display_version" in content
211
assert "tagOption.label" in content
212
+ assert 'data-bs-target="#self-update-last-attempt-collapse"' in content
213
+ assert "Latest version" in content
214
assert "Docker update guide" in content
215
assert "https://www.agent-zero.ai/p/docs/get-started/" in content
216
assert "Manual backup" in content
webui/components/settings/external/self-update-modal.html
+97
-37
@@ -14,20 +14,20 @@
14
x-destroy="$store.selfUpdateStore.cleanup()"
15
class="self-update-modal"
16
>
17
- <div class="self-update-howto">
17
+ <div class="self-update-panel">
18
<button
19
type="button"
20
- class="self-update-howto-toggle"
20
+ class="self-update-panel-toggle"
21
data-bs-toggle="collapse"
22
data-bs-target="#self-update-howto-collapse"
23
aria-expanded="false"
24
aria-controls="self-update-howto-collapse"
25
>
26
<span>How it works?</span>
27
- <span class="material-symbols-outlined self-update-howto-toggle-icon">expand_more</span>
27
+ <span class="material-symbols-outlined self-update-panel-toggle-icon">expand_more</span>
28
</button>
29
<div class="collapse" id="self-update-howto-collapse">
30
- <div class="self-update-howto-body self-update-copy">
30
+ <div class="self-update-panel-body self-update-copy">
31
<p>
32
Agent Zero saves this request into
33
<code x-text="$store.selfUpdateStore.info?.paths?.update_file || '/exe/a0-self-update.yaml'"></code>,
@@ -44,15 +44,50 @@
44
</div>
45
</div>
46
47
- <div class="self-update-summary-grid">
47
+ <div class="self-update-version-grid">
48
<div class="self-update-summary-card">
49
<div class="summary-label">Current version</div>
50
<div class="summary-value" x-text="$store.selfUpdateStore.currentVersion"></div>
51
- <div class="summary-meta" x-text="$store.selfUpdateStore.info?.current?.short_commit || ''"></div>
52
- <div class="summary-meta" x-text="$store.selfUpdateStore.currentBranch || ''"></div>
51
+ <div class="summary-meta">
52
+ Commit
53
+ <code x-text="$store.selfUpdateStore.info?.current?.short_commit || 'unknown'"></code>
54
+ </div>
55
+ <div class="summary-meta" x-text="`Branch ${$store.selfUpdateStore.currentBranch || 'unknown'}`"></div>
56
</div>
57
55
- <div class="self-update-summary-card" x-show="$store.selfUpdateStore.info?.pending">
58
+ <div class="self-update-summary-card">
59
+ <div class="summary-label">Latest version</div>
60
+ <div
61
+ class="summary-value"
62
+ x-text="$store.selfUpdateStore.info?.current_branch_latest?.display_version || 'Unavailable'"
63
+ ></div>
64
+ <template x-if="$store.selfUpdateStore.info?.current_branch_latest?.short_commit && $store.selfUpdateStore.info?.current_branch_latest?.branch !== 'main'">
65
+ <div class="summary-meta">
66
+ Commit
67
+ <code x-text="$store.selfUpdateStore.info?.current_branch_latest?.short_commit"></code>
68
+ </div>
69
+ </template>
70
+ <div
71
+ class="summary-meta"
72
+ x-text="`Branch ${$store.selfUpdateStore.info?.current_branch_latest?.branch || $store.selfUpdateStore.currentBranch || 'unknown'}`"
73
+ ></div>
74
+ <template x-if="$store.selfUpdateStore.info?.current_branch_latest?.describe && $store.selfUpdateStore.info?.current_branch_latest?.describe !== $store.selfUpdateStore.info?.current_branch_latest?.short_tag">
75
+ <div
76
+ class="summary-meta"
77
+ x-text="$store.selfUpdateStore.info?.current_branch_latest?.describe"
78
+ ></div>
79
+ </template>
80
+ <template x-if="$store.selfUpdateStore.info?.current_branch_latest?.supported === false">
81
+ <div class="summary-meta">
82
+ Latest official version is only tracked for <code>main</code>, <code>testing</code>,
83
+ and <code>development</code>.
84
+ </div>
85
+ </template>
86
+ </div>
87
+ </div>
88
+
89
+ <div class="self-update-summary-grid" x-show="$store.selfUpdateStore.info?.pending">
90
+ <div class="self-update-summary-card">
91
<div class="summary-label">Pending request</div>
92
<div
93
class="summary-value"
@@ -73,28 +108,42 @@
108
</template>
109
110
<template x-if="$store.selfUpdateStore.info?.last_status">
76
- <div class="self-update-status-card">
77
- <div class="section-title">Last Attempt</div>
78
- <div class="status-pill" x-text="$store.selfUpdateStore.info?.last_status?.status || 'unknown'"></div>
79
- <div class="status-message" x-text="$store.selfUpdateStore.info?.last_status?.message || ''"></div>
80
- <div class="summary-meta">
81
- Trigger:
82
- <code x-text="$store.selfUpdateStore.info?.paths?.update_file || '/exe/a0-self-update.yaml'"></code>
83
- </div>
84
- <div class="summary-meta">
85
- Log:
86
- <code x-text="$store.selfUpdateStore.info?.paths?.log_file || '/exe/a0-self-update.log'"></code>
87
- </div>
88
- <div
89
- class="summary-meta"
90
- x-text="$store.selfUpdateStore.formatTimestamp($store.selfUpdateStore.info?.last_status?.finished_at)"
91
- ></div>
92
- <template x-if="$store.selfUpdateStore.info?.last_status?.backup_zip_path">
93
- <div class="status-path">
94
- Backup:
95
- <code x-text="$store.selfUpdateStore.info?.last_status?.backup_zip_path"></code>
111
+ <div class="self-update-panel">
112
+ <button
113
+ type="button"
114
+ class="self-update-panel-toggle"
115
+ data-bs-toggle="collapse"
116
+ data-bs-target="#self-update-last-attempt-collapse"
117
+ aria-expanded="false"
118
+ aria-controls="self-update-last-attempt-collapse"
119
+ >
120
+ <span>Last Attempt</span>
121
+ <span class="material-symbols-outlined self-update-panel-toggle-icon">expand_more</span>
122
+ </button>
123
+ <div class="collapse" id="self-update-last-attempt-collapse">
124
+ <div class="self-update-panel-body">
125
+ <div class="status-pill" x-text="$store.selfUpdateStore.info?.last_status?.status || 'unknown'"></div>
126
+ <div class="status-message" x-text="$store.selfUpdateStore.info?.last_status?.message || ''"></div>
127
+ <div class="summary-meta">
128
+ Trigger:
129
+ <code x-text="$store.selfUpdateStore.info?.paths?.update_file || '/exe/a0-self-update.yaml'"></code>
130
+ </div>
131
+ <div class="summary-meta">
132
+ Log:
133
+ <code x-text="$store.selfUpdateStore.info?.paths?.log_file || '/exe/a0-self-update.log'"></code>
134
+ </div>
135
+ <div
136
+ class="summary-meta"
137
+ x-text="$store.selfUpdateStore.formatTimestamp($store.selfUpdateStore.info?.last_status?.finished_at)"
138
+ ></div>
139
+ <template x-if="$store.selfUpdateStore.info?.last_status?.backup_zip_path">
140
+ <div class="status-path">
141
+ Backup:
142
+ <code x-text="$store.selfUpdateStore.info?.last_status?.backup_zip_path"></code>
143
+ </div>
144
+ </template>
145
</div>
97
- </template>
146
+ </div>
147
</div>
148
</template>
149
@@ -325,13 +374,13 @@
374
margin: 0 0 0.75rem;
375
}
376
328
- .self-update-howto {
377
+ .self-update-panel {
378
border: 1px solid var(--color-border);
379
border-radius: 0.9rem;
380
background: var(--color-panel);
381
}
382
334
- .self-update-howto-toggle {
383
+ .self-update-panel-toggle {
384
width: 100%;
385
display: flex;
386
align-items: center;
@@ -346,15 +395,15 @@
395
text-align: left;
396
}
397
349
- .self-update-howto-toggle-icon {
398
+ .self-update-panel-toggle-icon {
399
transition: transform 0.2s ease;
400
}
401
353
- .self-update-howto-toggle[aria-expanded="true"] .self-update-howto-toggle-icon {
402
+ .self-update-panel-toggle[aria-expanded="true"] .self-update-panel-toggle-icon {
403
transform: rotate(180deg);
404
}
405
357
- .self-update-howto-body {
406
+ .self-update-panel-body {
407
padding: 0 1rem 1rem;
408
}
409
@@ -408,16 +457,21 @@
457
text-decoration: underline;
458
}
459
460
+ .self-update-version-grid {
461
+ display: grid;
462
+ gap: 0.75rem;
463
+ grid-template-columns: repeat(2, minmax(0, 1fr));
464
+ }
465
+
466
.self-update-summary-grid {
467
display: grid;
468
gap: 0.75rem;
469
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
470
}
471
417
- .self-update-summary-card,
418
- .self-update-status-card {
472
+ .self-update-summary-card {
473
border: 1px solid var(--color-border);
420
- border-radius: 10px;
474
+ border-radius: 0.9rem;
475
padding: 0.9rem 1rem;
476
background: var(--color-panel);
477
}
@@ -487,6 +541,12 @@
541
margin-top: 0.7rem;
542
line-height: 1.5;
543
}
544
+
545
+ @media (max-width: 720px) {
546
+ .self-update-version-grid {
547
+ grid-template-columns: minmax(0, 1fr);
548
+ }
549
+ }
550
</style>
551
</body>
552
</html>
webui/components/welcome/welcome-screen.html
+4
-3
@@ -7,9 +7,10 @@
7
</head>
8
9
<body>
10
- <div x-data >
11
- <template x-if="$store.welcomeStore && $store.welcomeStore.isVisible">
12
- <div x-create="$store.welcomeStore.onCreate()" class="welcome-container">
10
+ <div x-data>
11
+ <template x-if="$store.welcomeStore">
12
+ <div x-init="$store.welcomeStore.init(); if ($store.welcomeStore.isVisible) { $store.welcomeStore.onCreate(); } $watch('$store.welcomeStore.isVisible', (visible) => { if (visible) { $store.welcomeStore.onCreate(); } });"
13
+ class="welcome-container">
14
<x-extension id="welcome-screen-start"></x-extension>
15
<div class="welcome-content">
16
<!-- Action Cards -->
webui/components/welcome/welcome-store.js
+4
@@ -12,12 +12,16 @@ const model = {
12
bannersLoading: false,
13
lastBannerRefresh: 0,
14
hasDismissedBanners: false,
15
+ _initialized: false,
16
17
get isVisible() {
18
return !chatsStore.selected;
19
},
20
21
init() {
22
+ if (this._initialized) return;
23
+ this._initialized = true;
24
+
25
// Reload banners when a modal closes while the welcome screen is visible.
26
document.addEventListener("modal-closed", () => {
27
if (this.isVisible) {