clay-server 2.39.0-beta.2 → 2.39.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -157,6 +157,21 @@ function attachConnection(ctx) {
157
157
  var _comVal = _comUid ? usersModule.getClaudeOpenMode(_comUid) : "tui";
158
158
  sendTo(ws, { type: "claude_open_mode_changed", claudeOpenMode: _comVal || "tui" });
159
159
  }
160
+
161
+ // What's New: push the full entries list (for the home-page feed)
162
+ // plus the subset of unseen ids (for the auto-pop carousel). Content
163
+ // lives in lib/whats-new-content.js so adding an entry doesn't touch
164
+ // this file.
165
+ try {
166
+ var _wn = require("./whats-new");
167
+ var _wnUid = (wsUser && wsUser.id) || null;
168
+ var _wnState = _wnUid ? _wn.getStateForUser(_wnUid) : { entries: _wn.listEntries(), unseenIds: [] };
169
+ if (_wnState.entries.length > 0) {
170
+ sendTo(ws, { type: "whats_new_state", entries: _wnState.entries, unseenIds: _wnState.unseenIds });
171
+ }
172
+ } catch (e) {
173
+ if (debug) console.error("[project] whats_new send failed:", e && e.message);
174
+ }
160
175
  _loop.sendConnectionState(ws);
161
176
  if (_mcp) _mcp.sendConnectionState(ws);
162
177
  if (_notifications) _notifications.sendConnectionState(ws, sendTo);
@@ -1754,6 +1754,24 @@ function attachSessions(ctx) {
1754
1754
  return true;
1755
1755
  }
1756
1756
 
1757
+ if (msg.type === "whats_new_seen") {
1758
+ // Persist that the current user dismissed a What's New entry so it
1759
+ // is not shown again on future connects.
1760
+ var wnUserId = ws._clayUser ? ws._clayUser.id : null;
1761
+ if (!wnUserId) {
1762
+ sendTo(ws, { type: "whats_new_seen_result", ok: false, error: "no_user" });
1763
+ return true;
1764
+ }
1765
+ var wnSvc = require("./whats-new");
1766
+ var wnResult = wnSvc.markSeen(wnUserId, msg.id);
1767
+ if (wnResult && wnResult.ok) {
1768
+ sendTo(ws, { type: "whats_new_seen_result", ok: true, id: msg.id });
1769
+ } else {
1770
+ sendTo(ws, { type: "whats_new_seen_result", ok: false, error: (wnResult && wnResult.error) || "unknown" });
1771
+ }
1772
+ return true;
1773
+ }
1774
+
1757
1775
  if (msg.type === "set_claude_open_mode") {
1758
1776
  // Per-user preference: when Clay opens a Claude session, render it as
1759
1777
  // the SDK-driven custom chat ("gui") or as an embedded `claude` TUI
package/lib/public/app.js CHANGED
@@ -63,6 +63,8 @@ import { initDebateUi, showDebateConcludeConfirm as _debShowDebateConcludeConfir
63
63
  import { initLoopUi, updateLoopInputVisibility as _loopUpdateLoopInputVisibility, updateLoopButton as _loopUpdateLoopButton, showLoopBanner as _loopShowLoopBanner, updateLoopBanner as _loopUpdateLoopBanner, updateRalphBars as _loopUpdateRalphBars, showRalphCraftingBar as _loopShowRalphCraftingBar, showRalphApprovalBar as _loopShowRalphApprovalBar, updateRalphApprovalStatus as _loopUpdateRalphApprovalStatus, openRalphPreviewModal as _loopOpenRalphPreviewModal, showExecModal as _loopShowExecModal, closeExecModal as _loopCloseExecModal, updateExecModalStatus as _loopUpdateExecModalStatus } from './modules/app-loop-ui.js';
64
64
  import { initLoopWizard, openRalphWizard as _loopOpenRalphWizard, closeRalphWizard as _loopCloseRalphWizard, getWizardSource as _loopGetWizardSource } from './modules/app-loop-wizard.js';
65
65
  import { initAppNotifications, handleNotificationsState as _notifHandleState, handleNotificationCreated as _notifHandleCreated, handleNotificationDismissed as _notifHandleDismissed, handleNotificationDismissedAll as _notifHandleDismissedAll } from './modules/app-notifications.js';
66
+ import { initWhatsNew, handleWhatsNewState as _wnHandleState, handleWhatsNewSeenResult as _wnHandleSeenResult } from './modules/whats-new.js';
67
+ import { initWhatsNewArticle, openArticle as openWhatsNewArticle } from './modules/whats-new-article.js';
66
68
  import { createStore, store } from './modules/store.js';
67
69
  import { initPanels, updateConfigChip as _panUpdateConfigChip, getModelEffortLevels as _panGetModelEffortLevels, accumulateUsage as _panAccumulateUsage, updateUsagePanel as _panUpdateUsagePanel, resetUsage as _panResetUsage, toggleUsagePanel as _panToggleUsagePanel, formatTokens as _panFormatTokens, updateStatusPanel as _panUpdateStatusPanel, requestProcessStats as _panRequestProcessStats, toggleStatusPanel as _panToggleStatusPanel, accumulateContext as _panAccumulateContext, updateContextPanel as _panUpdateContextPanel, resetContext as _panResetContext, resetContextData as _panResetContextData, minimizeContext as _panMinimizeContext, expandContext as _panExpandContext, toggleContextPanel as _panToggleContextPanel, getContextView as _panGetContextView, renderCtxPopover as _panRenderCtxPopover, hideCtxPopover as _panHideCtxPopover, formatBytes as _panFormatBytes, formatUptime as _panFormatUptime, getModelSupportsEffort as _panGetModelSupportsEffort, getSessionUsage, setSessionUsage, getContextData, setContextData, setContextView as _panSetContextView, applyContextView as _panApplyContextView } from './modules/app-panels.js';
68
70
  import { initProjects, updateProjectList as _projUpdateProjectList, renderProjectList as _projRenderProjectList, renderTopbarPresence as _projRenderTopbarPresence, switchProject as _projSwitchProject, resetClientState as _projResetClientState, confirmRemoveProject as _projConfirmRemoveProject, handleRemoveProjectCheckResult as _projHandleRemoveProjectCheckResult, handleRemoveProjectResult as _projHandleRemoveProjectResult, openAddProjectModal as _projOpenAddProjectModal, closeAddProjectModal as _projCloseAddProjectModal, handleBrowseDirResult as _projHandleBrowseDirResult, handleAddProjectResult as _projHandleAddProjectResult, handleCloneProgress as _projHandleCloneProgress, showUpdateAvailable as _projShowUpdateAvailable, getCachedProjects, setCachedProjects, getCachedProjectCount, getCachedRemovedProjects, setCachedRemovedProjects } from './modules/app-projects.js';
@@ -590,6 +592,17 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
590
592
  // --- Notifications module ---
591
593
  initAppNotifications();
592
594
 
595
+ // --- What's New viewer ---
596
+ initWhatsNewArticle();
597
+ initWhatsNew({
598
+ // "Read more" in the carousel opens the dedicated article viewer
599
+ // for the chosen entry, jumping straight to the body content
600
+ // (skipping the home list).
601
+ onReadMore: function (entryId) {
602
+ if (entryId) openWhatsNewArticle(entryId);
603
+ },
604
+ });
605
+
593
606
  // --- Panels module ---
594
607
  initPanels();
595
608
 
@@ -369,3 +369,320 @@
369
369
  padding: 4px;
370
370
  background: #000;
371
371
  }
372
+
373
+ /* ============================================================
374
+ What's New carousel
375
+ ============================================================ */
376
+ .whats-new-backdrop {
377
+ position: fixed;
378
+ inset: 0;
379
+ z-index: 9600;
380
+ background: rgba(0, 0, 0, 0.55);
381
+ display: flex;
382
+ align-items: center;
383
+ justify-content: center;
384
+ padding: 24px;
385
+ opacity: 0;
386
+ transition: opacity 160ms ease;
387
+ }
388
+ .whats-new-backdrop.show { opacity: 1; }
389
+ .whats-new-card {
390
+ position: relative;
391
+ width: min(440px, 100%);
392
+ max-height: min(80vh, 640px);
393
+ background: var(--sidebar-bg);
394
+ border: 1px solid var(--border);
395
+ border-radius: 14px;
396
+ overflow: hidden;
397
+ display: flex;
398
+ flex-direction: column;
399
+ box-shadow: 0 12px 48px rgba(var(--shadow-rgb), 0.4);
400
+ color: var(--text);
401
+ }
402
+ .whats-new-close {
403
+ position: absolute;
404
+ top: 10px;
405
+ right: 10px;
406
+ width: 28px;
407
+ height: 28px;
408
+ padding: 0;
409
+ border: none;
410
+ border-radius: 6px;
411
+ background: rgba(var(--overlay-rgb), 0.08);
412
+ color: var(--text-dimmer);
413
+ cursor: pointer;
414
+ display: flex;
415
+ align-items: center;
416
+ justify-content: center;
417
+ z-index: 2;
418
+ }
419
+ .whats-new-close:hover {
420
+ background: rgba(var(--overlay-rgb), 0.15);
421
+ color: var(--text);
422
+ }
423
+ .whats-new-image-slot {
424
+ width: 100%;
425
+ aspect-ratio: 16 / 9;
426
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent2, var(--accent)) 100%);
427
+ flex-shrink: 0;
428
+ overflow: hidden;
429
+ display: block;
430
+ }
431
+ .whats-new-image-slot img {
432
+ width: 100%;
433
+ height: 100%;
434
+ object-fit: cover;
435
+ display: block;
436
+ }
437
+ .whats-new-image-slot.empty {
438
+ aspect-ratio: 16 / 6;
439
+ }
440
+ .whats-new-body {
441
+ padding: 18px 20px 14px;
442
+ display: flex;
443
+ flex-direction: column;
444
+ gap: 8px;
445
+ overflow-y: auto;
446
+ }
447
+ .whats-new-eyebrow {
448
+ font-size: 11px;
449
+ font-weight: 600;
450
+ letter-spacing: 0.06em;
451
+ text-transform: uppercase;
452
+ color: var(--text-dimmer);
453
+ }
454
+ .whats-new-title {
455
+ margin: 0;
456
+ font-size: 18px;
457
+ font-weight: 600;
458
+ line-height: 1.3;
459
+ color: var(--text);
460
+ }
461
+ .whats-new-summary {
462
+ margin: 0;
463
+ font-size: 13px;
464
+ line-height: 1.55;
465
+ color: var(--text-secondary);
466
+ }
467
+ .whats-new-read-more {
468
+ align-self: flex-start;
469
+ margin-top: 6px;
470
+ padding: 7px 14px;
471
+ font-size: 13px;
472
+ font-weight: 500;
473
+ border: none;
474
+ border-radius: 7px;
475
+ background: var(--accent);
476
+ color: var(--accent-contrast, #fff);
477
+ cursor: pointer;
478
+ display: inline-flex;
479
+ align-items: center;
480
+ }
481
+ .whats-new-read-more:hover {
482
+ filter: brightness(1.08);
483
+ }
484
+ .whats-new-nav {
485
+ display: flex;
486
+ align-items: center;
487
+ justify-content: space-between;
488
+ padding: 8px 16px 14px;
489
+ border-top: 1px solid var(--border);
490
+ flex-shrink: 0;
491
+ }
492
+ .whats-new-nav.hidden { display: none; }
493
+ .whats-new-prev,
494
+ .whats-new-next {
495
+ width: 30px;
496
+ height: 30px;
497
+ padding: 0;
498
+ border: none;
499
+ border-radius: 6px;
500
+ background: transparent;
501
+ color: var(--text-dimmer);
502
+ cursor: pointer;
503
+ display: flex;
504
+ align-items: center;
505
+ justify-content: center;
506
+ }
507
+ .whats-new-prev:hover,
508
+ .whats-new-next:hover {
509
+ background: rgba(var(--overlay-rgb), 0.08);
510
+ color: var(--text);
511
+ }
512
+ .whats-new-prev:disabled,
513
+ .whats-new-next:disabled {
514
+ opacity: 0.35;
515
+ cursor: default;
516
+ }
517
+ .whats-new-dots {
518
+ display: flex;
519
+ gap: 6px;
520
+ align-items: center;
521
+ }
522
+ .whats-new-dot {
523
+ width: 7px;
524
+ height: 7px;
525
+ padding: 0;
526
+ border: none;
527
+ border-radius: 50%;
528
+ background: var(--text-dimmer);
529
+ opacity: 0.35;
530
+ cursor: pointer;
531
+ transition: opacity 120ms ease, transform 120ms ease;
532
+ }
533
+ .whats-new-dot.active {
534
+ opacity: 1;
535
+ background: var(--accent);
536
+ transform: scale(1.2);
537
+ }
538
+
539
+ /* ============================================================
540
+ Home page "What's New" index (title-only list, blog-style)
541
+ ============================================================ */
542
+ .hub-whats-new {
543
+ display: flex;
544
+ flex-direction: column;
545
+ }
546
+ .hub-whats-new-item {
547
+ display: flex;
548
+ align-items: baseline;
549
+ gap: 12px;
550
+ padding: 10px 4px;
551
+ border: none;
552
+ background: transparent;
553
+ text-align: left;
554
+ cursor: pointer;
555
+ color: var(--text);
556
+ border-bottom: 1px solid var(--border);
557
+ transition: background 120ms ease;
558
+ width: 100%;
559
+ }
560
+ .hub-whats-new-item:last-child {
561
+ border-bottom: none;
562
+ }
563
+ .hub-whats-new-item:hover {
564
+ background: rgba(var(--overlay-rgb), 0.04);
565
+ }
566
+ .hub-whats-new-item-date {
567
+ font-size: 11px;
568
+ color: var(--text-dimmer);
569
+ flex-shrink: 0;
570
+ width: 84px;
571
+ font-variant-numeric: tabular-nums;
572
+ }
573
+ .hub-whats-new-item-title {
574
+ margin: 0;
575
+ font-size: 14px;
576
+ font-weight: 500;
577
+ line-height: 1.4;
578
+ color: var(--text);
579
+ flex: 1 1 auto;
580
+ min-width: 0;
581
+ }
582
+
583
+ /* ============================================================
584
+ Whats New article (blog-style reading view)
585
+ ============================================================
586
+ Mirrors #home-hub positioning: absolute inset:0 inside
587
+ #main-area so the icon strip, top bar, and any other chrome
588
+ outside #main-area stay visible. Sidebar-column (inside
589
+ main-area) is covered, same as home-hub. */
590
+ #whats-new-article {
591
+ position: absolute;
592
+ inset: 0;
593
+ z-index: 210;
594
+ background: var(--bg);
595
+ overflow-y: auto;
596
+ display: flex;
597
+ flex-direction: column;
598
+ border-top-left-radius: 8px;
599
+ }
600
+ #whats-new-article.hidden { display: none; }
601
+ .wna-toolbar {
602
+ position: sticky;
603
+ top: 0;
604
+ display: flex;
605
+ align-items: center;
606
+ padding: 12px 20px;
607
+ background: var(--bg);
608
+ border-bottom: 1px solid var(--border);
609
+ z-index: 1;
610
+ }
611
+ .wna-back {
612
+ display: inline-flex;
613
+ align-items: center;
614
+ gap: 8px;
615
+ padding: 6px 12px;
616
+ border: none;
617
+ border-radius: 6px;
618
+ background: transparent;
619
+ color: var(--text-secondary);
620
+ font-size: 13px;
621
+ cursor: pointer;
622
+ }
623
+ .wna-back:hover {
624
+ background: rgba(var(--overlay-rgb), 0.06);
625
+ color: var(--text);
626
+ }
627
+ .wna-content {
628
+ width: 100%;
629
+ max-width: 680px;
630
+ margin: 0 auto;
631
+ padding: 56px 32px 96px;
632
+ box-sizing: border-box;
633
+ }
634
+ .wna-date {
635
+ font-size: 12px;
636
+ color: var(--text-dimmer);
637
+ letter-spacing: 0.04em;
638
+ margin-bottom: 12px;
639
+ font-variant-numeric: tabular-nums;
640
+ }
641
+ .wna-title {
642
+ margin: 0 0 32px;
643
+ font-size: 32px;
644
+ font-weight: 700;
645
+ line-height: 1.2;
646
+ color: var(--text);
647
+ letter-spacing: -0.01em;
648
+ }
649
+ .wna-body {
650
+ font-size: 15px;
651
+ line-height: 1.7;
652
+ color: var(--text);
653
+ }
654
+ .wna-body p {
655
+ margin: 0 0 18px;
656
+ }
657
+ .wna-body p:last-child { margin-bottom: 0; }
658
+ .wna-body h3 {
659
+ margin: 36px 0 12px;
660
+ font-size: 18px;
661
+ font-weight: 600;
662
+ color: var(--text);
663
+ letter-spacing: -0.005em;
664
+ }
665
+ .wna-body ul, .wna-body ol {
666
+ margin: 0 0 18px;
667
+ padding-left: 22px;
668
+ }
669
+ .wna-body li {
670
+ margin-bottom: 8px;
671
+ }
672
+ .wna-body code {
673
+ padding: 2px 6px;
674
+ border-radius: 4px;
675
+ background: var(--code-bg);
676
+ color: var(--accent);
677
+ font-size: 13px;
678
+ font-family: "Roboto Mono", monospace;
679
+ }
680
+ .wna-body strong { color: var(--text); font-weight: 600; }
681
+ .wna-body em { color: var(--text-secondary); }
682
+ .wna-body a { color: var(--accent2); }
683
+ .wna-body a:hover { text-decoration: underline; }
684
+ @media (max-width: 640px) {
685
+ .wna-content { padding: 32px 20px 80px; }
686
+ .wna-title { font-size: 26px; }
687
+ }
688
+
@@ -150,6 +150,13 @@
150
150
  <div class="hub-week-strip" id="hub-week-strip"></div>
151
151
  </div>
152
152
 
153
+ <div class="hub-card hub-whats-new-card" id="hub-whats-new-card">
154
+ <div class="hub-card-header">
155
+ <span class="hub-card-title">What's New</span>
156
+ </div>
157
+ <div class="hub-whats-new" id="hub-whats-new"></div>
158
+ </div>
159
+
153
160
  <div class="hub-card hub-playbooks" id="hub-playbooks">
154
161
  <div class="hub-card-header">
155
162
  <span class="hub-card-title">Quick Start</span>
@@ -175,6 +182,20 @@
175
182
  </div>
176
183
  </div>
177
184
  </div>
185
+
186
+ <div id="whats-new-article" class="hidden">
187
+ <header class="wna-toolbar">
188
+ <button type="button" class="wna-back" id="wna-back">
189
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
190
+ <span>Back</span>
191
+ </button>
192
+ </header>
193
+ <article class="wna-content">
194
+ <div class="wna-date" id="wna-date"></div>
195
+ <h1 class="wna-title" id="wna-title"></h1>
196
+ <div class="wna-body" id="wna-body"></div>
197
+ </article>
198
+ </div>
178
199
  <div id="sidebar-column">
179
200
  <div class="title-bar-sidebar">
180
201
  <button class="title-bar-project-dropdown" id="title-bar-project-dropdown">
@@ -9,6 +9,8 @@ import { openSchedulerToTab } from './scheduler.js';
9
9
  import { getPlaybooks, openPlaybook, getPlaybookForTip } from './playbook.js';
10
10
  import { mateAvatarUrl } from './avatar.js';
11
11
  import { openDm, exitDmMode } from './app-dm.js';
12
+ import { getKnownEntries as getWhatsNewEntries } from './whats-new.js';
13
+ import { openArticle as openWhatsNewArticle } from './whats-new-article.js';
12
14
 
13
15
  function $hub(id) { return document.getElementById(id); }
14
16
 
@@ -489,6 +491,45 @@ export function renderHomeHub(projects) {
489
491
 
490
492
  // Render twemoji for all emoji in the hub
491
493
 
494
+ // --- What's New feed ---
495
+ renderHomeWhatsNew();
496
+ }
497
+
498
+ function renderHomeWhatsNew() {
499
+ var list = $hub("hub-whats-new");
500
+ var card = $hub("hub-whats-new-card");
501
+ if (!list || !card) return;
502
+ var entries = getWhatsNewEntries();
503
+ // Sort newest first by publishedAt (YYYY-MM-DD string compare).
504
+ entries = entries.slice().sort(function (a, b) {
505
+ return (b.publishedAt || "").localeCompare(a.publishedAt || "");
506
+ });
507
+ if (entries.length === 0) {
508
+ card.classList.add("hidden");
509
+ list.innerHTML = "";
510
+ return;
511
+ }
512
+ card.classList.remove("hidden");
513
+ list.innerHTML = "";
514
+ for (var i = 0; i < entries.length; i++) {
515
+ var e = entries[i];
516
+ var btn = document.createElement("button");
517
+ btn.type = "button";
518
+ btn.className = "hub-whats-new-item";
519
+ btn.setAttribute("data-whats-new-id", e.id);
520
+ var dateEl = document.createElement("div");
521
+ dateEl.className = "hub-whats-new-item-date";
522
+ dateEl.textContent = e.publishedAt || "";
523
+ var titleEl = document.createElement("h3");
524
+ titleEl.className = "hub-whats-new-item-title";
525
+ titleEl.textContent = e.title || "";
526
+ btn.appendChild(dateEl);
527
+ btn.appendChild(titleEl);
528
+ (function (id) {
529
+ btn.addEventListener("click", function () { openWhatsNewArticle(id); });
530
+ })(e.id);
531
+ list.appendChild(btn);
532
+ }
492
533
  }
493
534
 
494
535
  export function handleHubSchedules(msg) {
@@ -44,6 +44,8 @@ import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunF
44
44
  import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom } from './app-rendering.js';
45
45
  import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, updateCrossProjectBlink } from './app-favicon.js';
46
46
  import { setStatus } from './app-connection.js';
47
+ import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
48
+ import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
47
49
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
48
50
  import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
49
51
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
@@ -589,6 +591,7 @@ export function processMessage(msg) {
589
591
 
590
592
  case "session_switched":
591
593
  hideHomeHub();
594
+ closeWhatsNewArticle();
592
595
  // Save draft from outgoing session
593
596
  var _prevSid = store.get('activeSessionId');
594
597
  if (_prevSid && inputEl.value) {
@@ -1694,6 +1697,18 @@ export function processMessage(msg) {
1694
1697
  handleAutoContinueChanged(msg);
1695
1698
  break;
1696
1699
 
1700
+ case "whats_new_state":
1701
+ // Keep a known-entries cache for the home page feed (which shows
1702
+ // both seen and unseen entries), then queue unseen ones for the
1703
+ // carousel.
1704
+ if (msg && Array.isArray(msg.entries)) setWhatsNewKnownEntries(msg.entries);
1705
+ handleWhatsNewState(msg);
1706
+ break;
1707
+
1708
+ case "whats_new_seen_result":
1709
+ handleWhatsNewSeenResult(msg);
1710
+ break;
1711
+
1697
1712
  case "set_claude_open_mode_result":
1698
1713
  case "claude_open_mode_changed":
1699
1714
  if (msg.claudeOpenMode === "tui" || msg.claudeOpenMode === "gui") {
@@ -640,9 +640,17 @@ export function showUpdateBanner(msg) {
640
640
  pendingUpdateMsg = msg;
641
641
  if (!bannerContainer) return;
642
642
 
643
- // Remove any existing update banner
643
+ // If an update banner is already showing for the same version, skip the
644
+ // re-render. The server pushes update_available every hour but we don't
645
+ // want to flash/refresh the banner if the user already sees it.
644
646
  var existing = bannerContainer.querySelector('[data-notif-id="_update"]');
645
- if (existing) removeBanner(existing);
647
+ if (existing) {
648
+ var existingVersion = existing.getAttribute("data-update-version");
649
+ if (existingVersion === msg.version) return;
650
+ // Version changed (e.g. a newer release came in while old banner was
651
+ // still up): tear down and re-render with the new version.
652
+ removeBanner(existing);
653
+ }
646
654
 
647
655
  var isHeadless = store.get('isHeadlessMode');
648
656
  var updTag = msg.version.indexOf("-beta") !== -1 ? "beta" : "latest";
@@ -650,6 +658,7 @@ export function showUpdateBanner(msg) {
650
658
  var banner = document.createElement("div");
651
659
  banner.className = "notif-banner notif-banner-update";
652
660
  banner.setAttribute("data-notif-id", "_update");
661
+ banner.setAttribute("data-update-version", msg.version);
653
662
 
654
663
  var actionsHtml = '';
655
664
  if (!isHeadless) {
@@ -16,6 +16,7 @@ import { spawnDustParticles } from './sidebar.js';
16
16
  import { isSearchOpen, closeSearch } from './session-search.js';
17
17
  import { exitDmMode } from './app-dm.js';
18
18
  import { isHomeHubVisible, hideHomeHub, showHomeHub } from './app-home-hub.js';
19
+ import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
19
20
  import { resetFileBrowser } from './filebrowser.js';
20
21
  import { closeArchive } from './sticky-notes.js';
21
22
  import { hideMemory } from './mate-memory.js';
@@ -411,6 +412,7 @@ export function switchProject(slug) {
411
412
  var wasDm = st.dmMode;
412
413
  var wasMate = st.dmMode && st.dmTargetUser && st.dmTargetUser.isMate;
413
414
  if (st.dmMode) exitDmMode(wasMate);
415
+ closeWhatsNewArticle();
414
416
  if (isHomeHubVisible()) {
415
417
  hideHomeHub();
416
418
  if (slug === store.get('currentSlug')) return;