diff --git a/lib/public/css/home-hub.css b/lib/public/css/home-hub.css
index 9fb32a24..1d6f4318 100644
--- a/lib/public/css/home-hub.css
+++ b/lib/public/css/home-hub.css
@@ -497,6 +497,16 @@
text-align: center;
flex-shrink: 0;
}
+/* Per-project alert indicator — same red used by .icon-strip-project-badge
+ (unread badge). A count wouldn't fit this row, so a plain dot is enough
+ to show WHERE the alert is coming from. */
+.hub-recent-alert-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ background: #e74c3c;
+}
.hub-recent-project-icon--blank {
width: 20px;
height: 20px;
diff --git a/lib/public/modules/app-home-hub.js b/lib/public/modules/app-home-hub.js
index bb93b0ee..ac8330d5 100644
--- a/lib/public/modules/app-home-hub.js
+++ b/lib/public/modules/app-home-hub.js
@@ -232,6 +232,22 @@ function formatScheduleTime(ts) {
return DAY_NAMES[d.getDay()] + " " + timeStr;
}
+// Looks up whether a project has unresolved alerts, using the SAME source
+// that drives the Projects-list unread badge (proj.unread, populated in
+// renderProjectList() / app-projects.js and rendered as
+// .icon-strip-project-badge.has-unread / .hub-project-sessions elsewhere).
+// No separate alert data path — this is a read of the existing cache.
+function projectHasAlert(projectSlug) {
+ if (!projectSlug) return false;
+ var projects = getCachedProjects();
+ for (var i = 0; i < projects.length; i++) {
+ if (projects[i].slug === projectSlug) {
+ return (projects[i].unread || 0) > 0;
+ }
+ }
+ return false;
+}
+
function formatRelativeTime(ts) {
if (!ts) return "";
var diff = Date.now() - ts;
@@ -460,10 +476,18 @@ export function handleHubRecentSessions(msg) {
? '' + escapeHtml(sess.agentName) + ''
: '';
var dotClass = "hub-recent-dot" + (sess.isProcessing ? " processing" : "");
+ // Alert indicator: reuse the same per-project unread count that drives
+ // the Projects-list badge (icon-strip-project-badge / hub-project-*),
+ // rather than a new data path. A count won't fit this row, so render a
+ // plain dot when the row's project has 1+ unresolved (unread) items.
+ var alertHtml = projectHasAlert(sess.projectSlug)
+ ? ''
+ : '';
item.innerHTML =
'' +
iconHtml +
'' + escapeHtml(sess.title) + '' +
+ alertHtml +
agentBadge +
'' + timeStr + '';
item.addEventListener("click", function () {
diff --git a/test/hub-recent-sessions-alert-dot-lr-2b1f03.test.js b/test/hub-recent-sessions-alert-dot-lr-2b1f03.test.js
new file mode 100644
index 00000000..d05d82dc
--- /dev/null
+++ b/test/hub-recent-sessions-alert-dot-lr-2b1f03.test.js
@@ -0,0 +1,92 @@
+// hub-recent-sessions-alert-dot-lr-2b1f03.test.js — lr-2b1f03 regression coverage.
+//
+// Feature: the Home Hub Projects list shows a per-project alert count badge
+// (proj.unread, rendered as .icon-strip-project-badge.has-unread etc), but
+// the Recent Sessions list showed no alert indicator at all. This adds a
+// small red dot on each Recent Sessions row whose project has 1+ unread
+// items — reusing the SAME per-project unread source that drives the
+// Projects-list badge, rather than inventing a new data path.
+//
+// app-home-hub.js imports getCachedProjects from app-projects.js, and
+// pulls in a long chain of DOM-touching modules (theme, scheduler,
+// filebrowser, ws-ref, etc.) that assume a browser environment, so
+// importing it directly under node:test is impractical. Matching the
+// project's existing convention for DOM-heavy frontend modules (see
+// mobile-home-toggle-lr-551048.test.js), this is a source-text regression
+// check against the built file.
+
+"use strict";
+
+var test = require("node:test");
+var assert = require("node:assert/strict");
+var fs = require("fs");
+var path = require("path");
+
+var HOME_HUB_JS = fs.readFileSync(
+ path.join(__dirname, "../lib/public/modules/app-home-hub.js"),
+ "utf8"
+);
+
+var HOME_HUB_CSS = fs.readFileSync(
+ path.join(__dirname, "../lib/public/css/home-hub.css"),
+ "utf8"
+);
+
+test("app-home-hub.js: defines a projectHasAlert helper that reads getCachedProjects() unread state", () => {
+ var idx = HOME_HUB_JS.indexOf("function projectHasAlert");
+ assert.ok(idx !== -1, "expected a projectHasAlert(projectSlug) helper to exist");
+ var block = HOME_HUB_JS.slice(idx, idx + 600);
+
+ assert.match(
+ block,
+ /getCachedProjects\s*\(\s*\)/,
+ "projectHasAlert must reuse getCachedProjects() — the same per-project cache that drives the Projects-list unread badge — rather than inventing a new data path"
+ );
+ assert.match(
+ block,
+ /\.unread/,
+ "projectHasAlert must key off proj.unread, the same field rendered as the Projects-list alert count badge (app-projects.js renderProjectList / sidebar-projects.js .icon-strip-project-badge)"
+ );
+});
+
+test("app-home-hub.js: handleHubRecentSessions renders an alert dot for rows whose project has unread activity", () => {
+ var idx = HOME_HUB_JS.indexOf("export function handleHubRecentSessions");
+ assert.ok(idx !== -1, "expected handleHubRecentSessions to exist");
+ var block = HOME_HUB_JS.slice(idx, idx + 2000);
+
+ assert.match(
+ block,
+ /projectHasAlert\s*\(\s*sess\.projectSlug\s*\)/,
+ "handleHubRecentSessions must call projectHasAlert(sess.projectSlug) per row to decide whether to show the dot"
+ );
+ assert.match(
+ block,
+ /hub-recent-alert-dot/,
+ "the alert indicator must use a dedicated hub-recent-alert-dot class, distinct from the existing hub-recent-dot (processing) indicator"
+ );
+});
+
+test("app-home-hub.js: rows for projects with no alerts render unchanged (alertHtml is empty string, not a hidden element)", () => {
+ var idx = HOME_HUB_JS.indexOf("var alertHtml");
+ assert.ok(idx !== -1, "expected an alertHtml local computed per session row");
+ var block = HOME_HUB_JS.slice(idx, idx + 300);
+
+ assert.match(
+ block,
+ /\?\s*' {
+ var idx = HOME_HUB_CSS.indexOf(".hub-recent-alert-dot");
+ assert.ok(idx !== -1, "expected a .hub-recent-alert-dot rule in home-hub.css");
+ var block = HOME_HUB_CSS.slice(idx, idx + 300);
+
+ assert.match(block, /border-radius:\s*50%/, "alert indicator must be a circular dot");
+ assert.match(
+ block,
+ /#e74c3c/,
+ "alert dot must use the same red (#e74c3c) as the existing .icon-strip-project-badge unread badge, matching established alert-color convention"
+ );
+});