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) {