fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205) - #2211

Merged
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator
Aug 5, 2026
Merged

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205)#2211
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator

Conversation

@cat0825

Copy link
Copy Markdown
Contributor

Fixes#2205

Summary

The desktop chat surface's "New messages" scroll-to-bottom indicator now clears when the user scrolls back to the bottom, and its state resets fully when switching conversations. Repro screenshots are attached in the issue.

Root cause

Two independent problems in @astryxdesign/core@0.2.0:

  1. Not dismissed at the bottom.useChatNewMessages only clears hasNewMessages via dismiss() (the indicator button). useChatStreamScroll re-locks auto-follow on scrollend but never reports "at bottom" back to the indicator, so the label stays visible after the user scrolls back down.
  2. Leaks across conversations.ChatLayout is not remounted per conversation — lastMessageRef (pointing at the previous conversation's last message node) and the unlocked isLocked=false survive the switch, so the first message of a new conversation re-flags hasNewMessages.

Changes

  • apps/desktop/src/renderer/app-shell.tsx, quote-companion-panel.tsx: key ChatSurfaceLayout per conversation so the scroll/new-message state is remounted and reset on conversation switch.
  • patches/@astryxdesign+core+0.2.0.patch: adds the two @astryxdesign/core hunks for this fix (ChatLayout.js, useChatNewMessages.js). The file already carried the fix(ui): localize the field required/optional marker app-wide #2184FieldLabel localization hunks, so the patch now contains both sets; verified with git apply --check that the merged patch applies cleanly to a pristine @astryxdesign/core@0.2.0.
  • patches/README.md: rationale and removal conditions for the new hunks, following the repo convention.
  • apps/desktop/e2e/new-messages-indicator.spec.ts: Playwright regression coverage for both reported symptoms.

Verification

  • New e2e spec passes; with the fix reverted the two new cases fail exactly as described in the issue (negative control).
  • Regression: quote-companion, send-message, scroll-geometry, sidebar-navigation e2e suites (17 tests), packages/ui unit tests (316/316), desktop typecheck and biome all clean.

@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch 2 times, most recently from 0732a67 to 81759dbCompareAugust 5, 2026 07:20
…ersation (apache#2205)
Astryx ChatLayout only cleared hasNewMessages through the button's
dismiss(), so scrolling back to the bottom re-locked auto-follow but left
the "New messages" label visible; and a conversation switch reused the
same ChatLayout instance, leaking hasNewMessages, lastMessageRef and the
unlocked scroll state into the new conversation, whose first message then
re-triggered the indicator.
- patches/@astryxdesign+core+0.2.0.patch: ChatLayout clears the flag on
every re-lock (scrollend within the lock threshold) and gains an
optional conversationKey prop that resets the scroll lock and the
new-message baseline when it changes; useChatNewMessages exposes reset().
- ChatSurfaceLayout forwards conversationKey from the active session id
(app-shell + quote companion). No remount: a remount would drop an
in-progress composer draft, a regression composer-skill-invocation e2e
caught on the earlier keyed-remount attempt.
- e2e regression: new-messages-indicator.spec.ts passes with the fix and
fails without it.
@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch from 81759db to 0b40f6aCompareAugust 5, 2026 07:29
Resolve patches/README.md conflict by keeping both sections:
- our apache#2205 "New messages" indicator section
- upstream apache#2225 Astryx List accessible-name section
Verified the merged @astryxdesign/core patch still applies cleanly
via patch-package --error-on-fail.
@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for this — the fix is well-scoped and I verified it end to end in a browser harness against the patched dist: the badge appears on unlocked growth, clears on real scrollend at bottom, resets on conversation switch, and the composer element survives the switch (no remount — conversationKey is a plain prop, the patch resets in place, which also keeps it compatible with #2239's arrival-pin approach; that PR is still open, so nothing merged is at risk). The patch applies cleanly to a pristine @astryxdesign/core@0.2.0, and the patches/README.md conflict with the merged #2225 is purely mechanical (both appended sections; keep both).

P1 — the e2e selectors no longer match main: #2202 (merged after your branch point) localized the two Astryx keys. The spec's header comment claims "Maka's Astryx overrides do not cover these keys", but packages/ui/src/astryx-i18n.tsx:97-98 covers both — '@astryx.chatLayout.newMessages''跳到最新消息' and '@astryx.chatLayoutScrollButton.scrollToBottom''滚动到底部' — and the e2e window fixture runs zh. After a squash-merge both tests would fail at the first English-name assertion (scroll-geometry-adjacent, on the e2e shard). Please switch the selectors to '跳到最新消息' / '滚动到底部' (the rest of the spec already uses zh names) and fix the comment — otherwise the regression guard ships red and its negative control is void.

P3 (optional):

  • growTranscriptOverflow only guarantees scrollHeight > clientHeight, but the 'Scroll to bottom' visibility depends on Astryx's 100px buttonThreshold — a compact 3-message transcript could fail at that assertion with a misleading error. Assert sh - ch > 100 with diagnostics instead.
  • The "no remount → draft survives" constraint has no e2e guard: composer-skill-invocation / skill-draft-lifecycle only cover revision drafts, not cross-conversation persistence. Typing a draft in test 2 before switching (and asserting it survives) would pin the property that makes the prop-not-key design load-bearing. Cheap.
  • The quote-companion side (conversationKey for the fork session) has no e2e coverage — acceptable per the representative-journey guidance, just noting the leak would go unnoticed there.

The fix itself is sound — happy to approve once the selectors are updated.

Root causes of the CI e2e failures (this spec had never run on Linux
before the upstream-main merge):
1. Button names are locale-dependent — Maka's Astryx copy overrides
(astryx-copy.ts) map 'New messages'/'Scroll to bottom' to zh
('跳到最新消息'/'滚动到底部') under the default zh fixture locale.
The spec hardcoded the en names, so both scroll-button assertions
could not find the button. Match both locales — the assertion is
about the affordance, not Astryx's copy.
2. The prompt anchor rail (upstream apache#2237) renders a preview of the
sent text, so loose getByText(/Fake backend received: .../) regexes
matched two nodes (preview + transcript echo) → strict-mode
violation. Assert with { exact: true } on the echo, which the longer
preview never matches.
3. growTranscriptOverflow only required sh > ch (merely scrollable);
Astryx flags scrolled-up only past buttonThreshold (100px), so a
compact font could leave the button unrendered. Grow until
sh - ch > 150px (threshold + headroom), up to 6 messages.
Verified locally: 2/2 pass.
@cat0825

Copy link
Copy Markdown
ContributorAuthor

Fixed the e2e failures — root cause was not the product fix, it was the spec itself (it had never run on Linux before this merge):

  1. Locale: Maka's Astryx copy overrides (astryx-copy.ts) map the scroll buttons to zh ('跳到最新消息'/'滚动到底部') under the default zh fixture locale; the spec hardcoded the en names. Now matches both locales — the assertion is about the affordance appearing/clearing, not Astryx's copy.
  2. Strict-mode violation: the prompt anchor rail (upstream perf(ui): seed transcript geometry before progressive fill #2237) renders a preview of sent text, so loose getByText(/Fake backend received: .../) matched two nodes. Now uses { exact: true } on the echo.
  3. Threshold: growTranscriptOverflow required only sh > ch; Astryx flags scrolled-up only past buttonThreshold (100px), so a compact font could keep the button unrendered. Grows until sh - ch > 150px.

Verified locally: 2/2 pass. Pushed as 243b921.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks — the new commit resolves everything from the prior pass: the locale-agnostic matchers cover both the zh override (astryx-copy.ts:114-115) and Astryx's en defaults, the { exact: true } echoes correctly dodge the prompt-rail preview (which stays mounted via HoverCard), and the 150px growth bound (buttonThreshold 100 + headroom) is bounded and cannot hang. I also verified the conversationKey passthrough at both call sites (app-shell and quote-companion — the companion's companionSession?.id ?? sourceSession?.id is the right key, since each quote source forks a new session), the patch is exactly the 3 #2205-related hunks and applies cleanly to pristine 0.2.0, and both tests fail on the pre-fix product code.

One new finding, tracked as a follow-up rather than a blocker:

P2 (deferred, not blocking) — transient strict-mode double match during streaming. Between the fake backend's chunk 6 and 7 (~45ms window), the rail's reply preview normalizes+trims to exactly the query string while the transcript's first <p> also matches — two elements at a poll instant → Playwright's strict-mode violation is non-retriable, so the whole spec hard-fails for that run (probability is phase-dependent; author's local runs and CI are green). One-line fix: scope the echo locators to the message list (.maka-chat-message-list — the rail is a sibling), or assert after settle. If we see strict mode violation in future CI runs of this spec, that's the cause.

P3s (open, optional): the quote-companion conversationKey path still has no e2e coverage (the spec covers only the sessions surface), and the fixed 250ms wait could be a poll.

Merging now — thanks for the fast turnaround!

@Astro-Han
Astro-Han merged commit 2b5852f into apache:mainAug 5, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(desktop): "New messages" scroll-to-bottom indicator not dismissed after scrolling to bottom or switching conversations

2 participants

@cat0825@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205) - #2211

Merged
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator
Aug 5, 2026
Merged

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205)#2211
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator

Conversation

@cat0825

Copy link
Copy Markdown
Contributor

Fixes#2205

Summary

The desktop chat surface's "New messages" scroll-to-bottom indicator now clears when the user scrolls back to the bottom, and its state resets fully when switching conversations. Repro screenshots are attached in the issue.

Root cause

Two independent problems in @astryxdesign/core@0.2.0:

  1. Not dismissed at the bottom.useChatNewMessages only clears hasNewMessages via dismiss() (the indicator button). useChatStreamScroll re-locks auto-follow on scrollend but never reports "at bottom" back to the indicator, so the label stays visible after the user scrolls back down.
  2. Leaks across conversations.ChatLayout is not remounted per conversation — lastMessageRef (pointing at the previous conversation's last message node) and the unlocked isLocked=false survive the switch, so the first message of a new conversation re-flags hasNewMessages.

Changes

  • apps/desktop/src/renderer/app-shell.tsx, quote-companion-panel.tsx: key ChatSurfaceLayout per conversation so the scroll/new-message state is remounted and reset on conversation switch.
  • patches/@astryxdesign+core+0.2.0.patch: adds the two @astryxdesign/core hunks for this fix (ChatLayout.js, useChatNewMessages.js). The file already carried the fix(ui): localize the field required/optional marker app-wide #2184FieldLabel localization hunks, so the patch now contains both sets; verified with git apply --check that the merged patch applies cleanly to a pristine @astryxdesign/core@0.2.0.
  • patches/README.md: rationale and removal conditions for the new hunks, following the repo convention.
  • apps/desktop/e2e/new-messages-indicator.spec.ts: Playwright regression coverage for both reported symptoms.

Verification

  • New e2e spec passes; with the fix reverted the two new cases fail exactly as described in the issue (negative control).
  • Regression: quote-companion, send-message, scroll-geometry, sidebar-navigation e2e suites (17 tests), packages/ui unit tests (316/316), desktop typecheck and biome all clean.

@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch 2 times, most recently from 0732a67 to 81759dbCompareAugust 5, 2026 07:20
…ersation (apache#2205)
Astryx ChatLayout only cleared hasNewMessages through the button's
dismiss(), so scrolling back to the bottom re-locked auto-follow but left
the "New messages" label visible; and a conversation switch reused the
same ChatLayout instance, leaking hasNewMessages, lastMessageRef and the
unlocked scroll state into the new conversation, whose first message then
re-triggered the indicator.
- patches/@astryxdesign+core+0.2.0.patch: ChatLayout clears the flag on
every re-lock (scrollend within the lock threshold) and gains an
optional conversationKey prop that resets the scroll lock and the
new-message baseline when it changes; useChatNewMessages exposes reset().
- ChatSurfaceLayout forwards conversationKey from the active session id
(app-shell + quote companion). No remount: a remount would drop an
in-progress composer draft, a regression composer-skill-invocation e2e
caught on the earlier keyed-remount attempt.
- e2e regression: new-messages-indicator.spec.ts passes with the fix and
fails without it.
@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch from 81759db to 0b40f6aCompareAugust 5, 2026 07:29
Resolve patches/README.md conflict by keeping both sections:
- our apache#2205 "New messages" indicator section
- upstream apache#2225 Astryx List accessible-name section
Verified the merged @astryxdesign/core patch still applies cleanly
via patch-package --error-on-fail.
@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for this — the fix is well-scoped and I verified it end to end in a browser harness against the patched dist: the badge appears on unlocked growth, clears on real scrollend at bottom, resets on conversation switch, and the composer element survives the switch (no remount — conversationKey is a plain prop, the patch resets in place, which also keeps it compatible with #2239's arrival-pin approach; that PR is still open, so nothing merged is at risk). The patch applies cleanly to a pristine @astryxdesign/core@0.2.0, and the patches/README.md conflict with the merged #2225 is purely mechanical (both appended sections; keep both).

P1 — the e2e selectors no longer match main: #2202 (merged after your branch point) localized the two Astryx keys. The spec's header comment claims "Maka's Astryx overrides do not cover these keys", but packages/ui/src/astryx-i18n.tsx:97-98 covers both — '@astryx.chatLayout.newMessages''跳到最新消息' and '@astryx.chatLayoutScrollButton.scrollToBottom''滚动到底部' — and the e2e window fixture runs zh. After a squash-merge both tests would fail at the first English-name assertion (scroll-geometry-adjacent, on the e2e shard). Please switch the selectors to '跳到最新消息' / '滚动到底部' (the rest of the spec already uses zh names) and fix the comment — otherwise the regression guard ships red and its negative control is void.

P3 (optional):

  • growTranscriptOverflow only guarantees scrollHeight > clientHeight, but the 'Scroll to bottom' visibility depends on Astryx's 100px buttonThreshold — a compact 3-message transcript could fail at that assertion with a misleading error. Assert sh - ch > 100 with diagnostics instead.
  • The "no remount → draft survives" constraint has no e2e guard: composer-skill-invocation / skill-draft-lifecycle only cover revision drafts, not cross-conversation persistence. Typing a draft in test 2 before switching (and asserting it survives) would pin the property that makes the prop-not-key design load-bearing. Cheap.
  • The quote-companion side (conversationKey for the fork session) has no e2e coverage — acceptable per the representative-journey guidance, just noting the leak would go unnoticed there.

The fix itself is sound — happy to approve once the selectors are updated.

Root causes of the CI e2e failures (this spec had never run on Linux
before the upstream-main merge):
1. Button names are locale-dependent — Maka's Astryx copy overrides
(astryx-copy.ts) map 'New messages'/'Scroll to bottom' to zh
('跳到最新消息'/'滚动到底部') under the default zh fixture locale.
The spec hardcoded the en names, so both scroll-button assertions
could not find the button. Match both locales — the assertion is
about the affordance, not Astryx's copy.
2. The prompt anchor rail (upstream apache#2237) renders a preview of the
sent text, so loose getByText(/Fake backend received: .../) regexes
matched two nodes (preview + transcript echo) → strict-mode
violation. Assert with { exact: true } on the echo, which the longer
preview never matches.
3. growTranscriptOverflow only required sh > ch (merely scrollable);
Astryx flags scrolled-up only past buttonThreshold (100px), so a
compact font could leave the button unrendered. Grow until
sh - ch > 150px (threshold + headroom), up to 6 messages.
Verified locally: 2/2 pass.
@cat0825

Copy link
Copy Markdown
ContributorAuthor

Fixed the e2e failures — root cause was not the product fix, it was the spec itself (it had never run on Linux before this merge):

  1. Locale: Maka's Astryx copy overrides (astryx-copy.ts) map the scroll buttons to zh ('跳到最新消息'/'滚动到底部') under the default zh fixture locale; the spec hardcoded the en names. Now matches both locales — the assertion is about the affordance appearing/clearing, not Astryx's copy.
  2. Strict-mode violation: the prompt anchor rail (upstream perf(ui): seed transcript geometry before progressive fill #2237) renders a preview of sent text, so loose getByText(/Fake backend received: .../) matched two nodes. Now uses { exact: true } on the echo.
  3. Threshold: growTranscriptOverflow required only sh > ch; Astryx flags scrolled-up only past buttonThreshold (100px), so a compact font could keep the button unrendered. Grows until sh - ch > 150px.

Verified locally: 2/2 pass. Pushed as 243b921.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks — the new commit resolves everything from the prior pass: the locale-agnostic matchers cover both the zh override (astryx-copy.ts:114-115) and Astryx's en defaults, the { exact: true } echoes correctly dodge the prompt-rail preview (which stays mounted via HoverCard), and the 150px growth bound (buttonThreshold 100 + headroom) is bounded and cannot hang. I also verified the conversationKey passthrough at both call sites (app-shell and quote-companion — the companion's companionSession?.id ?? sourceSession?.id is the right key, since each quote source forks a new session), the patch is exactly the 3 #2205-related hunks and applies cleanly to pristine 0.2.0, and both tests fail on the pre-fix product code.

One new finding, tracked as a follow-up rather than a blocker:

P2 (deferred, not blocking) — transient strict-mode double match during streaming. Between the fake backend's chunk 6 and 7 (~45ms window), the rail's reply preview normalizes+trims to exactly the query string while the transcript's first <p> also matches — two elements at a poll instant → Playwright's strict-mode violation is non-retriable, so the whole spec hard-fails for that run (probability is phase-dependent; author's local runs and CI are green). One-line fix: scope the echo locators to the message list (.maka-chat-message-list — the rail is a sibling), or assert after settle. If we see strict mode violation in future CI runs of this spec, that's the cause.

P3s (open, optional): the quote-companion conversationKey path still has no e2e coverage (the spec covers only the sessions surface), and the fixed 250ms wait could be a poll.

Merging now — thanks for the fast turnaround!

@Astro-Han
Astro-Han merged commit 2b5852f into apache:mainAug 5, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(desktop): "New messages" scroll-to-bottom indicator not dismissed after scrolling to bottom or switching conversations

2 participants

@cat0825@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205) - #2211

Merged
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator
Aug 5, 2026
Merged

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205)#2211
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator

Conversation

@cat0825

Copy link
Copy Markdown
Contributor

Fixes#2205

Summary

The desktop chat surface's "New messages" scroll-to-bottom indicator now clears when the user scrolls back to the bottom, and its state resets fully when switching conversations. Repro screenshots are attached in the issue.

Root cause

Two independent problems in @astryxdesign/core@0.2.0:

  1. Not dismissed at the bottom.useChatNewMessages only clears hasNewMessages via dismiss() (the indicator button). useChatStreamScroll re-locks auto-follow on scrollend but never reports "at bottom" back to the indicator, so the label stays visible after the user scrolls back down.
  2. Leaks across conversations.ChatLayout is not remounted per conversation — lastMessageRef (pointing at the previous conversation's last message node) and the unlocked isLocked=false survive the switch, so the first message of a new conversation re-flags hasNewMessages.

Changes

  • apps/desktop/src/renderer/app-shell.tsx, quote-companion-panel.tsx: key ChatSurfaceLayout per conversation so the scroll/new-message state is remounted and reset on conversation switch.
  • patches/@astryxdesign+core+0.2.0.patch: adds the two @astryxdesign/core hunks for this fix (ChatLayout.js, useChatNewMessages.js). The file already carried the fix(ui): localize the field required/optional marker app-wide #2184FieldLabel localization hunks, so the patch now contains both sets; verified with git apply --check that the merged patch applies cleanly to a pristine @astryxdesign/core@0.2.0.
  • patches/README.md: rationale and removal conditions for the new hunks, following the repo convention.
  • apps/desktop/e2e/new-messages-indicator.spec.ts: Playwright regression coverage for both reported symptoms.

Verification

  • New e2e spec passes; with the fix reverted the two new cases fail exactly as described in the issue (negative control).
  • Regression: quote-companion, send-message, scroll-geometry, sidebar-navigation e2e suites (17 tests), packages/ui unit tests (316/316), desktop typecheck and biome all clean.

@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch 2 times, most recently from 0732a67 to 81759dbCompareAugust 5, 2026 07:20
…ersation (apache#2205)
Astryx ChatLayout only cleared hasNewMessages through the button's
dismiss(), so scrolling back to the bottom re-locked auto-follow but left
the "New messages" label visible; and a conversation switch reused the
same ChatLayout instance, leaking hasNewMessages, lastMessageRef and the
unlocked scroll state into the new conversation, whose first message then
re-triggered the indicator.
- patches/@astryxdesign+core+0.2.0.patch: ChatLayout clears the flag on
every re-lock (scrollend within the lock threshold) and gains an
optional conversationKey prop that resets the scroll lock and the
new-message baseline when it changes; useChatNewMessages exposes reset().
- ChatSurfaceLayout forwards conversationKey from the active session id
(app-shell + quote companion). No remount: a remount would drop an
in-progress composer draft, a regression composer-skill-invocation e2e
caught on the earlier keyed-remount attempt.
- e2e regression: new-messages-indicator.spec.ts passes with the fix and
fails without it.
@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch from 81759db to 0b40f6aCompareAugust 5, 2026 07:29
Resolve patches/README.md conflict by keeping both sections:
- our apache#2205 "New messages" indicator section
- upstream apache#2225 Astryx List accessible-name section
Verified the merged @astryxdesign/core patch still applies cleanly
via patch-package --error-on-fail.
@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for this — the fix is well-scoped and I verified it end to end in a browser harness against the patched dist: the badge appears on unlocked growth, clears on real scrollend at bottom, resets on conversation switch, and the composer element survives the switch (no remount — conversationKey is a plain prop, the patch resets in place, which also keeps it compatible with #2239's arrival-pin approach; that PR is still open, so nothing merged is at risk). The patch applies cleanly to a pristine @astryxdesign/core@0.2.0, and the patches/README.md conflict with the merged #2225 is purely mechanical (both appended sections; keep both).

P1 — the e2e selectors no longer match main: #2202 (merged after your branch point) localized the two Astryx keys. The spec's header comment claims "Maka's Astryx overrides do not cover these keys", but packages/ui/src/astryx-i18n.tsx:97-98 covers both — '@astryx.chatLayout.newMessages''跳到最新消息' and '@astryx.chatLayoutScrollButton.scrollToBottom''滚动到底部' — and the e2e window fixture runs zh. After a squash-merge both tests would fail at the first English-name assertion (scroll-geometry-adjacent, on the e2e shard). Please switch the selectors to '跳到最新消息' / '滚动到底部' (the rest of the spec already uses zh names) and fix the comment — otherwise the regression guard ships red and its negative control is void.

P3 (optional):

  • growTranscriptOverflow only guarantees scrollHeight > clientHeight, but the 'Scroll to bottom' visibility depends on Astryx's 100px buttonThreshold — a compact 3-message transcript could fail at that assertion with a misleading error. Assert sh - ch > 100 with diagnostics instead.
  • The "no remount → draft survives" constraint has no e2e guard: composer-skill-invocation / skill-draft-lifecycle only cover revision drafts, not cross-conversation persistence. Typing a draft in test 2 before switching (and asserting it survives) would pin the property that makes the prop-not-key design load-bearing. Cheap.
  • The quote-companion side (conversationKey for the fork session) has no e2e coverage — acceptable per the representative-journey guidance, just noting the leak would go unnoticed there.

The fix itself is sound — happy to approve once the selectors are updated.

Root causes of the CI e2e failures (this spec had never run on Linux
before the upstream-main merge):
1. Button names are locale-dependent — Maka's Astryx copy overrides
(astryx-copy.ts) map 'New messages'/'Scroll to bottom' to zh
('跳到最新消息'/'滚动到底部') under the default zh fixture locale.
The spec hardcoded the en names, so both scroll-button assertions
could not find the button. Match both locales — the assertion is
about the affordance, not Astryx's copy.
2. The prompt anchor rail (upstream apache#2237) renders a preview of the
sent text, so loose getByText(/Fake backend received: .../) regexes
matched two nodes (preview + transcript echo) → strict-mode
violation. Assert with { exact: true } on the echo, which the longer
preview never matches.
3. growTranscriptOverflow only required sh > ch (merely scrollable);
Astryx flags scrolled-up only past buttonThreshold (100px), so a
compact font could leave the button unrendered. Grow until
sh - ch > 150px (threshold + headroom), up to 6 messages.
Verified locally: 2/2 pass.
@cat0825

Copy link
Copy Markdown
ContributorAuthor

Fixed the e2e failures — root cause was not the product fix, it was the spec itself (it had never run on Linux before this merge):

  1. Locale: Maka's Astryx copy overrides (astryx-copy.ts) map the scroll buttons to zh ('跳到最新消息'/'滚动到底部') under the default zh fixture locale; the spec hardcoded the en names. Now matches both locales — the assertion is about the affordance appearing/clearing, not Astryx's copy.
  2. Strict-mode violation: the prompt anchor rail (upstream perf(ui): seed transcript geometry before progressive fill #2237) renders a preview of sent text, so loose getByText(/Fake backend received: .../) matched two nodes. Now uses { exact: true } on the echo.
  3. Threshold: growTranscriptOverflow required only sh > ch; Astryx flags scrolled-up only past buttonThreshold (100px), so a compact font could keep the button unrendered. Grows until sh - ch > 150px.

Verified locally: 2/2 pass. Pushed as 243b921.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks — the new commit resolves everything from the prior pass: the locale-agnostic matchers cover both the zh override (astryx-copy.ts:114-115) and Astryx's en defaults, the { exact: true } echoes correctly dodge the prompt-rail preview (which stays mounted via HoverCard), and the 150px growth bound (buttonThreshold 100 + headroom) is bounded and cannot hang. I also verified the conversationKey passthrough at both call sites (app-shell and quote-companion — the companion's companionSession?.id ?? sourceSession?.id is the right key, since each quote source forks a new session), the patch is exactly the 3 #2205-related hunks and applies cleanly to pristine 0.2.0, and both tests fail on the pre-fix product code.

One new finding, tracked as a follow-up rather than a blocker:

P2 (deferred, not blocking) — transient strict-mode double match during streaming. Between the fake backend's chunk 6 and 7 (~45ms window), the rail's reply preview normalizes+trims to exactly the query string while the transcript's first <p> also matches — two elements at a poll instant → Playwright's strict-mode violation is non-retriable, so the whole spec hard-fails for that run (probability is phase-dependent; author's local runs and CI are green). One-line fix: scope the echo locators to the message list (.maka-chat-message-list — the rail is a sibling), or assert after settle. If we see strict mode violation in future CI runs of this spec, that's the cause.

P3s (open, optional): the quote-companion conversationKey path still has no e2e coverage (the spec covers only the sessions surface), and the fixed 250ms wait could be a poll.

Merging now — thanks for the fast turnaround!

@Astro-Han
Astro-Han merged commit 2b5852f into apache:mainAug 5, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(desktop): "New messages" scroll-to-bottom indicator not dismissed after scrolling to bottom or switching conversations

2 participants

@cat0825@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205) - #2211

Merged
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator
Aug 5, 2026
Merged

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205)#2211
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator

Conversation

@cat0825

Copy link
Copy Markdown
Contributor

Fixes#2205

Summary

The desktop chat surface's "New messages" scroll-to-bottom indicator now clears when the user scrolls back to the bottom, and its state resets fully when switching conversations. Repro screenshots are attached in the issue.

Root cause

Two independent problems in @astryxdesign/core@0.2.0:

  1. Not dismissed at the bottom.useChatNewMessages only clears hasNewMessages via dismiss() (the indicator button). useChatStreamScroll re-locks auto-follow on scrollend but never reports "at bottom" back to the indicator, so the label stays visible after the user scrolls back down.
  2. Leaks across conversations.ChatLayout is not remounted per conversation — lastMessageRef (pointing at the previous conversation's last message node) and the unlocked isLocked=false survive the switch, so the first message of a new conversation re-flags hasNewMessages.

Changes

  • apps/desktop/src/renderer/app-shell.tsx, quote-companion-panel.tsx: key ChatSurfaceLayout per conversation so the scroll/new-message state is remounted and reset on conversation switch.
  • patches/@astryxdesign+core+0.2.0.patch: adds the two @astryxdesign/core hunks for this fix (ChatLayout.js, useChatNewMessages.js). The file already carried the fix(ui): localize the field required/optional marker app-wide #2184FieldLabel localization hunks, so the patch now contains both sets; verified with git apply --check that the merged patch applies cleanly to a pristine @astryxdesign/core@0.2.0.
  • patches/README.md: rationale and removal conditions for the new hunks, following the repo convention.
  • apps/desktop/e2e/new-messages-indicator.spec.ts: Playwright regression coverage for both reported symptoms.

Verification

  • New e2e spec passes; with the fix reverted the two new cases fail exactly as described in the issue (negative control).
  • Regression: quote-companion, send-message, scroll-geometry, sidebar-navigation e2e suites (17 tests), packages/ui unit tests (316/316), desktop typecheck and biome all clean.

@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch 2 times, most recently from 0732a67 to 81759dbCompareAugust 5, 2026 07:20
…ersation (apache#2205)
Astryx ChatLayout only cleared hasNewMessages through the button's
dismiss(), so scrolling back to the bottom re-locked auto-follow but left
the "New messages" label visible; and a conversation switch reused the
same ChatLayout instance, leaking hasNewMessages, lastMessageRef and the
unlocked scroll state into the new conversation, whose first message then
re-triggered the indicator.
- patches/@astryxdesign+core+0.2.0.patch: ChatLayout clears the flag on
every re-lock (scrollend within the lock threshold) and gains an
optional conversationKey prop that resets the scroll lock and the
new-message baseline when it changes; useChatNewMessages exposes reset().
- ChatSurfaceLayout forwards conversationKey from the active session id
(app-shell + quote companion). No remount: a remount would drop an
in-progress composer draft, a regression composer-skill-invocation e2e
caught on the earlier keyed-remount attempt.
- e2e regression: new-messages-indicator.spec.ts passes with the fix and
fails without it.
@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch from 81759db to 0b40f6aCompareAugust 5, 2026 07:29
Resolve patches/README.md conflict by keeping both sections:
- our apache#2205 "New messages" indicator section
- upstream apache#2225 Astryx List accessible-name section
Verified the merged @astryxdesign/core patch still applies cleanly
via patch-package --error-on-fail.
@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for this — the fix is well-scoped and I verified it end to end in a browser harness against the patched dist: the badge appears on unlocked growth, clears on real scrollend at bottom, resets on conversation switch, and the composer element survives the switch (no remount — conversationKey is a plain prop, the patch resets in place, which also keeps it compatible with #2239's arrival-pin approach; that PR is still open, so nothing merged is at risk). The patch applies cleanly to a pristine @astryxdesign/core@0.2.0, and the patches/README.md conflict with the merged #2225 is purely mechanical (both appended sections; keep both).

P1 — the e2e selectors no longer match main: #2202 (merged after your branch point) localized the two Astryx keys. The spec's header comment claims "Maka's Astryx overrides do not cover these keys", but packages/ui/src/astryx-i18n.tsx:97-98 covers both — '@astryx.chatLayout.newMessages''跳到最新消息' and '@astryx.chatLayoutScrollButton.scrollToBottom''滚动到底部' — and the e2e window fixture runs zh. After a squash-merge both tests would fail at the first English-name assertion (scroll-geometry-adjacent, on the e2e shard). Please switch the selectors to '跳到最新消息' / '滚动到底部' (the rest of the spec already uses zh names) and fix the comment — otherwise the regression guard ships red and its negative control is void.

P3 (optional):

  • growTranscriptOverflow only guarantees scrollHeight > clientHeight, but the 'Scroll to bottom' visibility depends on Astryx's 100px buttonThreshold — a compact 3-message transcript could fail at that assertion with a misleading error. Assert sh - ch > 100 with diagnostics instead.
  • The "no remount → draft survives" constraint has no e2e guard: composer-skill-invocation / skill-draft-lifecycle only cover revision drafts, not cross-conversation persistence. Typing a draft in test 2 before switching (and asserting it survives) would pin the property that makes the prop-not-key design load-bearing. Cheap.
  • The quote-companion side (conversationKey for the fork session) has no e2e coverage — acceptable per the representative-journey guidance, just noting the leak would go unnoticed there.

The fix itself is sound — happy to approve once the selectors are updated.

Root causes of the CI e2e failures (this spec had never run on Linux
before the upstream-main merge):
1. Button names are locale-dependent — Maka's Astryx copy overrides
(astryx-copy.ts) map 'New messages'/'Scroll to bottom' to zh
('跳到最新消息'/'滚动到底部') under the default zh fixture locale.
The spec hardcoded the en names, so both scroll-button assertions
could not find the button. Match both locales — the assertion is
about the affordance, not Astryx's copy.
2. The prompt anchor rail (upstream apache#2237) renders a preview of the
sent text, so loose getByText(/Fake backend received: .../) regexes
matched two nodes (preview + transcript echo) → strict-mode
violation. Assert with { exact: true } on the echo, which the longer
preview never matches.
3. growTranscriptOverflow only required sh > ch (merely scrollable);
Astryx flags scrolled-up only past buttonThreshold (100px), so a
compact font could leave the button unrendered. Grow until
sh - ch > 150px (threshold + headroom), up to 6 messages.
Verified locally: 2/2 pass.
@cat0825

Copy link
Copy Markdown
ContributorAuthor

Fixed the e2e failures — root cause was not the product fix, it was the spec itself (it had never run on Linux before this merge):

  1. Locale: Maka's Astryx copy overrides (astryx-copy.ts) map the scroll buttons to zh ('跳到最新消息'/'滚动到底部') under the default zh fixture locale; the spec hardcoded the en names. Now matches both locales — the assertion is about the affordance appearing/clearing, not Astryx's copy.
  2. Strict-mode violation: the prompt anchor rail (upstream perf(ui): seed transcript geometry before progressive fill #2237) renders a preview of sent text, so loose getByText(/Fake backend received: .../) matched two nodes. Now uses { exact: true } on the echo.
  3. Threshold: growTranscriptOverflow required only sh > ch; Astryx flags scrolled-up only past buttonThreshold (100px), so a compact font could keep the button unrendered. Grows until sh - ch > 150px.

Verified locally: 2/2 pass. Pushed as 243b921.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks — the new commit resolves everything from the prior pass: the locale-agnostic matchers cover both the zh override (astryx-copy.ts:114-115) and Astryx's en defaults, the { exact: true } echoes correctly dodge the prompt-rail preview (which stays mounted via HoverCard), and the 150px growth bound (buttonThreshold 100 + headroom) is bounded and cannot hang. I also verified the conversationKey passthrough at both call sites (app-shell and quote-companion — the companion's companionSession?.id ?? sourceSession?.id is the right key, since each quote source forks a new session), the patch is exactly the 3 #2205-related hunks and applies cleanly to pristine 0.2.0, and both tests fail on the pre-fix product code.

One new finding, tracked as a follow-up rather than a blocker:

P2 (deferred, not blocking) — transient strict-mode double match during streaming. Between the fake backend's chunk 6 and 7 (~45ms window), the rail's reply preview normalizes+trims to exactly the query string while the transcript's first <p> also matches — two elements at a poll instant → Playwright's strict-mode violation is non-retriable, so the whole spec hard-fails for that run (probability is phase-dependent; author's local runs and CI are green). One-line fix: scope the echo locators to the message list (.maka-chat-message-list — the rail is a sibling), or assert after settle. If we see strict mode violation in future CI runs of this spec, that's the cause.

P3s (open, optional): the quote-companion conversationKey path still has no e2e coverage (the spec covers only the sessions surface), and the fixed 250ms wait could be a poll.

Merging now — thanks for the fast turnaround!

@Astro-Han
Astro-Han merged commit 2b5852f into apache:mainAug 5, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(desktop): "New messages" scroll-to-bottom indicator not dismissed after scrolling to bottom or switching conversations

2 participants

@cat0825@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205) - #2211

Merged
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator
Aug 5, 2026
Merged

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205)#2211
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator

Conversation

@cat0825

Copy link
Copy Markdown
Contributor

Fixes#2205

Summary

The desktop chat surface's "New messages" scroll-to-bottom indicator now clears when the user scrolls back to the bottom, and its state resets fully when switching conversations. Repro screenshots are attached in the issue.

Root cause

Two independent problems in @astryxdesign/core@0.2.0:

  1. Not dismissed at the bottom.useChatNewMessages only clears hasNewMessages via dismiss() (the indicator button). useChatStreamScroll re-locks auto-follow on scrollend but never reports "at bottom" back to the indicator, so the label stays visible after the user scrolls back down.
  2. Leaks across conversations.ChatLayout is not remounted per conversation — lastMessageRef (pointing at the previous conversation's last message node) and the unlocked isLocked=false survive the switch, so the first message of a new conversation re-flags hasNewMessages.

Changes

  • apps/desktop/src/renderer/app-shell.tsx, quote-companion-panel.tsx: key ChatSurfaceLayout per conversation so the scroll/new-message state is remounted and reset on conversation switch.
  • patches/@astryxdesign+core+0.2.0.patch: adds the two @astryxdesign/core hunks for this fix (ChatLayout.js, useChatNewMessages.js). The file already carried the fix(ui): localize the field required/optional marker app-wide #2184FieldLabel localization hunks, so the patch now contains both sets; verified with git apply --check that the merged patch applies cleanly to a pristine @astryxdesign/core@0.2.0.
  • patches/README.md: rationale and removal conditions for the new hunks, following the repo convention.
  • apps/desktop/e2e/new-messages-indicator.spec.ts: Playwright regression coverage for both reported symptoms.

Verification

  • New e2e spec passes; with the fix reverted the two new cases fail exactly as described in the issue (negative control).
  • Regression: quote-companion, send-message, scroll-geometry, sidebar-navigation e2e suites (17 tests), packages/ui unit tests (316/316), desktop typecheck and biome all clean.

@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch 2 times, most recently from 0732a67 to 81759dbCompareAugust 5, 2026 07:20
…ersation (apache#2205)
Astryx ChatLayout only cleared hasNewMessages through the button's
dismiss(), so scrolling back to the bottom re-locked auto-follow but left
the "New messages" label visible; and a conversation switch reused the
same ChatLayout instance, leaking hasNewMessages, lastMessageRef and the
unlocked scroll state into the new conversation, whose first message then
re-triggered the indicator.
- patches/@astryxdesign+core+0.2.0.patch: ChatLayout clears the flag on
every re-lock (scrollend within the lock threshold) and gains an
optional conversationKey prop that resets the scroll lock and the
new-message baseline when it changes; useChatNewMessages exposes reset().
- ChatSurfaceLayout forwards conversationKey from the active session id
(app-shell + quote companion). No remount: a remount would drop an
in-progress composer draft, a regression composer-skill-invocation e2e
caught on the earlier keyed-remount attempt.
- e2e regression: new-messages-indicator.spec.ts passes with the fix and
fails without it.
@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch from 81759db to 0b40f6aCompareAugust 5, 2026 07:29
Resolve patches/README.md conflict by keeping both sections:
- our apache#2205 "New messages" indicator section
- upstream apache#2225 Astryx List accessible-name section
Verified the merged @astryxdesign/core patch still applies cleanly
via patch-package --error-on-fail.
@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for this — the fix is well-scoped and I verified it end to end in a browser harness against the patched dist: the badge appears on unlocked growth, clears on real scrollend at bottom, resets on conversation switch, and the composer element survives the switch (no remount — conversationKey is a plain prop, the patch resets in place, which also keeps it compatible with #2239's arrival-pin approach; that PR is still open, so nothing merged is at risk). The patch applies cleanly to a pristine @astryxdesign/core@0.2.0, and the patches/README.md conflict with the merged #2225 is purely mechanical (both appended sections; keep both).

P1 — the e2e selectors no longer match main: #2202 (merged after your branch point) localized the two Astryx keys. The spec's header comment claims "Maka's Astryx overrides do not cover these keys", but packages/ui/src/astryx-i18n.tsx:97-98 covers both — '@astryx.chatLayout.newMessages''跳到最新消息' and '@astryx.chatLayoutScrollButton.scrollToBottom''滚动到底部' — and the e2e window fixture runs zh. After a squash-merge both tests would fail at the first English-name assertion (scroll-geometry-adjacent, on the e2e shard). Please switch the selectors to '跳到最新消息' / '滚动到底部' (the rest of the spec already uses zh names) and fix the comment — otherwise the regression guard ships red and its negative control is void.

P3 (optional):

  • growTranscriptOverflow only guarantees scrollHeight > clientHeight, but the 'Scroll to bottom' visibility depends on Astryx's 100px buttonThreshold — a compact 3-message transcript could fail at that assertion with a misleading error. Assert sh - ch > 100 with diagnostics instead.
  • The "no remount → draft survives" constraint has no e2e guard: composer-skill-invocation / skill-draft-lifecycle only cover revision drafts, not cross-conversation persistence. Typing a draft in test 2 before switching (and asserting it survives) would pin the property that makes the prop-not-key design load-bearing. Cheap.
  • The quote-companion side (conversationKey for the fork session) has no e2e coverage — acceptable per the representative-journey guidance, just noting the leak would go unnoticed there.

The fix itself is sound — happy to approve once the selectors are updated.

Root causes of the CI e2e failures (this spec had never run on Linux
before the upstream-main merge):
1. Button names are locale-dependent — Maka's Astryx copy overrides
(astryx-copy.ts) map 'New messages'/'Scroll to bottom' to zh
('跳到最新消息'/'滚动到底部') under the default zh fixture locale.
The spec hardcoded the en names, so both scroll-button assertions
could not find the button. Match both locales — the assertion is
about the affordance, not Astryx's copy.
2. The prompt anchor rail (upstream apache#2237) renders a preview of the
sent text, so loose getByText(/Fake backend received: .../) regexes
matched two nodes (preview + transcript echo) → strict-mode
violation. Assert with { exact: true } on the echo, which the longer
preview never matches.
3. growTranscriptOverflow only required sh > ch (merely scrollable);
Astryx flags scrolled-up only past buttonThreshold (100px), so a
compact font could leave the button unrendered. Grow until
sh - ch > 150px (threshold + headroom), up to 6 messages.
Verified locally: 2/2 pass.
@cat0825

Copy link
Copy Markdown
ContributorAuthor

Fixed the e2e failures — root cause was not the product fix, it was the spec itself (it had never run on Linux before this merge):

  1. Locale: Maka's Astryx copy overrides (astryx-copy.ts) map the scroll buttons to zh ('跳到最新消息'/'滚动到底部') under the default zh fixture locale; the spec hardcoded the en names. Now matches both locales — the assertion is about the affordance appearing/clearing, not Astryx's copy.
  2. Strict-mode violation: the prompt anchor rail (upstream perf(ui): seed transcript geometry before progressive fill #2237) renders a preview of sent text, so loose getByText(/Fake backend received: .../) matched two nodes. Now uses { exact: true } on the echo.
  3. Threshold: growTranscriptOverflow required only sh > ch; Astryx flags scrolled-up only past buttonThreshold (100px), so a compact font could keep the button unrendered. Grows until sh - ch > 150px.

Verified locally: 2/2 pass. Pushed as 243b921.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks — the new commit resolves everything from the prior pass: the locale-agnostic matchers cover both the zh override (astryx-copy.ts:114-115) and Astryx's en defaults, the { exact: true } echoes correctly dodge the prompt-rail preview (which stays mounted via HoverCard), and the 150px growth bound (buttonThreshold 100 + headroom) is bounded and cannot hang. I also verified the conversationKey passthrough at both call sites (app-shell and quote-companion — the companion's companionSession?.id ?? sourceSession?.id is the right key, since each quote source forks a new session), the patch is exactly the 3 #2205-related hunks and applies cleanly to pristine 0.2.0, and both tests fail on the pre-fix product code.

One new finding, tracked as a follow-up rather than a blocker:

P2 (deferred, not blocking) — transient strict-mode double match during streaming. Between the fake backend's chunk 6 and 7 (~45ms window), the rail's reply preview normalizes+trims to exactly the query string while the transcript's first <p> also matches — two elements at a poll instant → Playwright's strict-mode violation is non-retriable, so the whole spec hard-fails for that run (probability is phase-dependent; author's local runs and CI are green). One-line fix: scope the echo locators to the message list (.maka-chat-message-list — the rail is a sibling), or assert after settle. If we see strict mode violation in future CI runs of this spec, that's the cause.

P3s (open, optional): the quote-companion conversationKey path still has no e2e coverage (the spec covers only the sessions surface), and the fixed 250ms wait could be a poll.

Merging now — thanks for the fast turnaround!

@Astro-Han
Astro-Han merged commit 2b5852f into apache:mainAug 5, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(desktop): "New messages" scroll-to-bottom indicator not dismissed after scrolling to bottom or switching conversations

2 participants

@cat0825@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205) - #2211

Merged
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator
Aug 5, 2026
Merged

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205)#2211
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator

Conversation

@cat0825

Copy link
Copy Markdown
Contributor

Fixes#2205

Summary

The desktop chat surface's "New messages" scroll-to-bottom indicator now clears when the user scrolls back to the bottom, and its state resets fully when switching conversations. Repro screenshots are attached in the issue.

Root cause

Two independent problems in @astryxdesign/core@0.2.0:

  1. Not dismissed at the bottom.useChatNewMessages only clears hasNewMessages via dismiss() (the indicator button). useChatStreamScroll re-locks auto-follow on scrollend but never reports "at bottom" back to the indicator, so the label stays visible after the user scrolls back down.
  2. Leaks across conversations.ChatLayout is not remounted per conversation — lastMessageRef (pointing at the previous conversation's last message node) and the unlocked isLocked=false survive the switch, so the first message of a new conversation re-flags hasNewMessages.

Changes

  • apps/desktop/src/renderer/app-shell.tsx, quote-companion-panel.tsx: key ChatSurfaceLayout per conversation so the scroll/new-message state is remounted and reset on conversation switch.
  • patches/@astryxdesign+core+0.2.0.patch: adds the two @astryxdesign/core hunks for this fix (ChatLayout.js, useChatNewMessages.js). The file already carried the fix(ui): localize the field required/optional marker app-wide #2184FieldLabel localization hunks, so the patch now contains both sets; verified with git apply --check that the merged patch applies cleanly to a pristine @astryxdesign/core@0.2.0.
  • patches/README.md: rationale and removal conditions for the new hunks, following the repo convention.
  • apps/desktop/e2e/new-messages-indicator.spec.ts: Playwright regression coverage for both reported symptoms.

Verification

  • New e2e spec passes; with the fix reverted the two new cases fail exactly as described in the issue (negative control).
  • Regression: quote-companion, send-message, scroll-geometry, sidebar-navigation e2e suites (17 tests), packages/ui unit tests (316/316), desktop typecheck and biome all clean.

@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch 2 times, most recently from 0732a67 to 81759dbCompareAugust 5, 2026 07:20
…ersation (apache#2205)
Astryx ChatLayout only cleared hasNewMessages through the button's
dismiss(), so scrolling back to the bottom re-locked auto-follow but left
the "New messages" label visible; and a conversation switch reused the
same ChatLayout instance, leaking hasNewMessages, lastMessageRef and the
unlocked scroll state into the new conversation, whose first message then
re-triggered the indicator.
- patches/@astryxdesign+core+0.2.0.patch: ChatLayout clears the flag on
every re-lock (scrollend within the lock threshold) and gains an
optional conversationKey prop that resets the scroll lock and the
new-message baseline when it changes; useChatNewMessages exposes reset().
- ChatSurfaceLayout forwards conversationKey from the active session id
(app-shell + quote companion). No remount: a remount would drop an
in-progress composer draft, a regression composer-skill-invocation e2e
caught on the earlier keyed-remount attempt.
- e2e regression: new-messages-indicator.spec.ts passes with the fix and
fails without it.
@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch from 81759db to 0b40f6aCompareAugust 5, 2026 07:29
Resolve patches/README.md conflict by keeping both sections:
- our apache#2205 "New messages" indicator section
- upstream apache#2225 Astryx List accessible-name section
Verified the merged @astryxdesign/core patch still applies cleanly
via patch-package --error-on-fail.
@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for this — the fix is well-scoped and I verified it end to end in a browser harness against the patched dist: the badge appears on unlocked growth, clears on real scrollend at bottom, resets on conversation switch, and the composer element survives the switch (no remount — conversationKey is a plain prop, the patch resets in place, which also keeps it compatible with #2239's arrival-pin approach; that PR is still open, so nothing merged is at risk). The patch applies cleanly to a pristine @astryxdesign/core@0.2.0, and the patches/README.md conflict with the merged #2225 is purely mechanical (both appended sections; keep both).

P1 — the e2e selectors no longer match main: #2202 (merged after your branch point) localized the two Astryx keys. The spec's header comment claims "Maka's Astryx overrides do not cover these keys", but packages/ui/src/astryx-i18n.tsx:97-98 covers both — '@astryx.chatLayout.newMessages''跳到最新消息' and '@astryx.chatLayoutScrollButton.scrollToBottom''滚动到底部' — and the e2e window fixture runs zh. After a squash-merge both tests would fail at the first English-name assertion (scroll-geometry-adjacent, on the e2e shard). Please switch the selectors to '跳到最新消息' / '滚动到底部' (the rest of the spec already uses zh names) and fix the comment — otherwise the regression guard ships red and its negative control is void.

P3 (optional):

  • growTranscriptOverflow only guarantees scrollHeight > clientHeight, but the 'Scroll to bottom' visibility depends on Astryx's 100px buttonThreshold — a compact 3-message transcript could fail at that assertion with a misleading error. Assert sh - ch > 100 with diagnostics instead.
  • The "no remount → draft survives" constraint has no e2e guard: composer-skill-invocation / skill-draft-lifecycle only cover revision drafts, not cross-conversation persistence. Typing a draft in test 2 before switching (and asserting it survives) would pin the property that makes the prop-not-key design load-bearing. Cheap.
  • The quote-companion side (conversationKey for the fork session) has no e2e coverage — acceptable per the representative-journey guidance, just noting the leak would go unnoticed there.

The fix itself is sound — happy to approve once the selectors are updated.

Root causes of the CI e2e failures (this spec had never run on Linux
before the upstream-main merge):
1. Button names are locale-dependent — Maka's Astryx copy overrides
(astryx-copy.ts) map 'New messages'/'Scroll to bottom' to zh
('跳到最新消息'/'滚动到底部') under the default zh fixture locale.
The spec hardcoded the en names, so both scroll-button assertions
could not find the button. Match both locales — the assertion is
about the affordance, not Astryx's copy.
2. The prompt anchor rail (upstream apache#2237) renders a preview of the
sent text, so loose getByText(/Fake backend received: .../) regexes
matched two nodes (preview + transcript echo) → strict-mode
violation. Assert with { exact: true } on the echo, which the longer
preview never matches.
3. growTranscriptOverflow only required sh > ch (merely scrollable);
Astryx flags scrolled-up only past buttonThreshold (100px), so a
compact font could leave the button unrendered. Grow until
sh - ch > 150px (threshold + headroom), up to 6 messages.
Verified locally: 2/2 pass.
@cat0825

Copy link
Copy Markdown
ContributorAuthor

Fixed the e2e failures — root cause was not the product fix, it was the spec itself (it had never run on Linux before this merge):

  1. Locale: Maka's Astryx copy overrides (astryx-copy.ts) map the scroll buttons to zh ('跳到最新消息'/'滚动到底部') under the default zh fixture locale; the spec hardcoded the en names. Now matches both locales — the assertion is about the affordance appearing/clearing, not Astryx's copy.
  2. Strict-mode violation: the prompt anchor rail (upstream perf(ui): seed transcript geometry before progressive fill #2237) renders a preview of sent text, so loose getByText(/Fake backend received: .../) matched two nodes. Now uses { exact: true } on the echo.
  3. Threshold: growTranscriptOverflow required only sh > ch; Astryx flags scrolled-up only past buttonThreshold (100px), so a compact font could keep the button unrendered. Grows until sh - ch > 150px.

Verified locally: 2/2 pass. Pushed as 243b921.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks — the new commit resolves everything from the prior pass: the locale-agnostic matchers cover both the zh override (astryx-copy.ts:114-115) and Astryx's en defaults, the { exact: true } echoes correctly dodge the prompt-rail preview (which stays mounted via HoverCard), and the 150px growth bound (buttonThreshold 100 + headroom) is bounded and cannot hang. I also verified the conversationKey passthrough at both call sites (app-shell and quote-companion — the companion's companionSession?.id ?? sourceSession?.id is the right key, since each quote source forks a new session), the patch is exactly the 3 #2205-related hunks and applies cleanly to pristine 0.2.0, and both tests fail on the pre-fix product code.

One new finding, tracked as a follow-up rather than a blocker:

P2 (deferred, not blocking) — transient strict-mode double match during streaming. Between the fake backend's chunk 6 and 7 (~45ms window), the rail's reply preview normalizes+trims to exactly the query string while the transcript's first <p> also matches — two elements at a poll instant → Playwright's strict-mode violation is non-retriable, so the whole spec hard-fails for that run (probability is phase-dependent; author's local runs and CI are green). One-line fix: scope the echo locators to the message list (.maka-chat-message-list — the rail is a sibling), or assert after settle. If we see strict mode violation in future CI runs of this spec, that's the cause.

P3s (open, optional): the quote-companion conversationKey path still has no e2e coverage (the spec covers only the sessions surface), and the fixed 250ms wait could be a poll.

Merging now — thanks for the fast turnaround!

@Astro-Han
Astro-Han merged commit 2b5852f into apache:mainAug 5, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(desktop): "New messages" scroll-to-bottom indicator not dismissed after scrolling to bottom or switching conversations

2 participants

@cat0825@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205) - #2211

Merged
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator
Aug 5, 2026
Merged

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205)#2211
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator

Conversation

@cat0825

Copy link
Copy Markdown
Contributor

Fixes#2205

Summary

The desktop chat surface's "New messages" scroll-to-bottom indicator now clears when the user scrolls back to the bottom, and its state resets fully when switching conversations. Repro screenshots are attached in the issue.

Root cause

Two independent problems in @astryxdesign/core@0.2.0:

  1. Not dismissed at the bottom.useChatNewMessages only clears hasNewMessages via dismiss() (the indicator button). useChatStreamScroll re-locks auto-follow on scrollend but never reports "at bottom" back to the indicator, so the label stays visible after the user scrolls back down.
  2. Leaks across conversations.ChatLayout is not remounted per conversation — lastMessageRef (pointing at the previous conversation's last message node) and the unlocked isLocked=false survive the switch, so the first message of a new conversation re-flags hasNewMessages.

Changes

  • apps/desktop/src/renderer/app-shell.tsx, quote-companion-panel.tsx: key ChatSurfaceLayout per conversation so the scroll/new-message state is remounted and reset on conversation switch.
  • patches/@astryxdesign+core+0.2.0.patch: adds the two @astryxdesign/core hunks for this fix (ChatLayout.js, useChatNewMessages.js). The file already carried the fix(ui): localize the field required/optional marker app-wide #2184FieldLabel localization hunks, so the patch now contains both sets; verified with git apply --check that the merged patch applies cleanly to a pristine @astryxdesign/core@0.2.0.
  • patches/README.md: rationale and removal conditions for the new hunks, following the repo convention.
  • apps/desktop/e2e/new-messages-indicator.spec.ts: Playwright regression coverage for both reported symptoms.

Verification

  • New e2e spec passes; with the fix reverted the two new cases fail exactly as described in the issue (negative control).
  • Regression: quote-companion, send-message, scroll-geometry, sidebar-navigation e2e suites (17 tests), packages/ui unit tests (316/316), desktop typecheck and biome all clean.

@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch 2 times, most recently from 0732a67 to 81759dbCompareAugust 5, 2026 07:20
…ersation (apache#2205)
Astryx ChatLayout only cleared hasNewMessages through the button's
dismiss(), so scrolling back to the bottom re-locked auto-follow but left
the "New messages" label visible; and a conversation switch reused the
same ChatLayout instance, leaking hasNewMessages, lastMessageRef and the
unlocked scroll state into the new conversation, whose first message then
re-triggered the indicator.
- patches/@astryxdesign+core+0.2.0.patch: ChatLayout clears the flag on
every re-lock (scrollend within the lock threshold) and gains an
optional conversationKey prop that resets the scroll lock and the
new-message baseline when it changes; useChatNewMessages exposes reset().
- ChatSurfaceLayout forwards conversationKey from the active session id
(app-shell + quote companion). No remount: a remount would drop an
in-progress composer draft, a regression composer-skill-invocation e2e
caught on the earlier keyed-remount attempt.
- e2e regression: new-messages-indicator.spec.ts passes with the fix and
fails without it.
@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch from 81759db to 0b40f6aCompareAugust 5, 2026 07:29
Resolve patches/README.md conflict by keeping both sections:
- our apache#2205 "New messages" indicator section
- upstream apache#2225 Astryx List accessible-name section
Verified the merged @astryxdesign/core patch still applies cleanly
via patch-package --error-on-fail.
@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for this — the fix is well-scoped and I verified it end to end in a browser harness against the patched dist: the badge appears on unlocked growth, clears on real scrollend at bottom, resets on conversation switch, and the composer element survives the switch (no remount — conversationKey is a plain prop, the patch resets in place, which also keeps it compatible with #2239's arrival-pin approach; that PR is still open, so nothing merged is at risk). The patch applies cleanly to a pristine @astryxdesign/core@0.2.0, and the patches/README.md conflict with the merged #2225 is purely mechanical (both appended sections; keep both).

P1 — the e2e selectors no longer match main: #2202 (merged after your branch point) localized the two Astryx keys. The spec's header comment claims "Maka's Astryx overrides do not cover these keys", but packages/ui/src/astryx-i18n.tsx:97-98 covers both — '@astryx.chatLayout.newMessages''跳到最新消息' and '@astryx.chatLayoutScrollButton.scrollToBottom''滚动到底部' — and the e2e window fixture runs zh. After a squash-merge both tests would fail at the first English-name assertion (scroll-geometry-adjacent, on the e2e shard). Please switch the selectors to '跳到最新消息' / '滚动到底部' (the rest of the spec already uses zh names) and fix the comment — otherwise the regression guard ships red and its negative control is void.

P3 (optional):

  • growTranscriptOverflow only guarantees scrollHeight > clientHeight, but the 'Scroll to bottom' visibility depends on Astryx's 100px buttonThreshold — a compact 3-message transcript could fail at that assertion with a misleading error. Assert sh - ch > 100 with diagnostics instead.
  • The "no remount → draft survives" constraint has no e2e guard: composer-skill-invocation / skill-draft-lifecycle only cover revision drafts, not cross-conversation persistence. Typing a draft in test 2 before switching (and asserting it survives) would pin the property that makes the prop-not-key design load-bearing. Cheap.
  • The quote-companion side (conversationKey for the fork session) has no e2e coverage — acceptable per the representative-journey guidance, just noting the leak would go unnoticed there.

The fix itself is sound — happy to approve once the selectors are updated.

Root causes of the CI e2e failures (this spec had never run on Linux
before the upstream-main merge):
1. Button names are locale-dependent — Maka's Astryx copy overrides
(astryx-copy.ts) map 'New messages'/'Scroll to bottom' to zh
('跳到最新消息'/'滚动到底部') under the default zh fixture locale.
The spec hardcoded the en names, so both scroll-button assertions
could not find the button. Match both locales — the assertion is
about the affordance, not Astryx's copy.
2. The prompt anchor rail (upstream apache#2237) renders a preview of the
sent text, so loose getByText(/Fake backend received: .../) regexes
matched two nodes (preview + transcript echo) → strict-mode
violation. Assert with { exact: true } on the echo, which the longer
preview never matches.
3. growTranscriptOverflow only required sh > ch (merely scrollable);
Astryx flags scrolled-up only past buttonThreshold (100px), so a
compact font could leave the button unrendered. Grow until
sh - ch > 150px (threshold + headroom), up to 6 messages.
Verified locally: 2/2 pass.
@cat0825

Copy link
Copy Markdown
ContributorAuthor

Fixed the e2e failures — root cause was not the product fix, it was the spec itself (it had never run on Linux before this merge):

  1. Locale: Maka's Astryx copy overrides (astryx-copy.ts) map the scroll buttons to zh ('跳到最新消息'/'滚动到底部') under the default zh fixture locale; the spec hardcoded the en names. Now matches both locales — the assertion is about the affordance appearing/clearing, not Astryx's copy.
  2. Strict-mode violation: the prompt anchor rail (upstream perf(ui): seed transcript geometry before progressive fill #2237) renders a preview of sent text, so loose getByText(/Fake backend received: .../) matched two nodes. Now uses { exact: true } on the echo.
  3. Threshold: growTranscriptOverflow required only sh > ch; Astryx flags scrolled-up only past buttonThreshold (100px), so a compact font could keep the button unrendered. Grows until sh - ch > 150px.

Verified locally: 2/2 pass. Pushed as 243b921.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks — the new commit resolves everything from the prior pass: the locale-agnostic matchers cover both the zh override (astryx-copy.ts:114-115) and Astryx's en defaults, the { exact: true } echoes correctly dodge the prompt-rail preview (which stays mounted via HoverCard), and the 150px growth bound (buttonThreshold 100 + headroom) is bounded and cannot hang. I also verified the conversationKey passthrough at both call sites (app-shell and quote-companion — the companion's companionSession?.id ?? sourceSession?.id is the right key, since each quote source forks a new session), the patch is exactly the 3 #2205-related hunks and applies cleanly to pristine 0.2.0, and both tests fail on the pre-fix product code.

One new finding, tracked as a follow-up rather than a blocker:

P2 (deferred, not blocking) — transient strict-mode double match during streaming. Between the fake backend's chunk 6 and 7 (~45ms window), the rail's reply preview normalizes+trims to exactly the query string while the transcript's first <p> also matches — two elements at a poll instant → Playwright's strict-mode violation is non-retriable, so the whole spec hard-fails for that run (probability is phase-dependent; author's local runs and CI are green). One-line fix: scope the echo locators to the message list (.maka-chat-message-list — the rail is a sibling), or assert after settle. If we see strict mode violation in future CI runs of this spec, that's the cause.

P3s (open, optional): the quote-companion conversationKey path still has no e2e coverage (the spec covers only the sessions surface), and the fixed 250ms wait could be a poll.

Merging now — thanks for the fast turnaround!

@Astro-Han
Astro-Han merged commit 2b5852f into apache:mainAug 5, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(desktop): "New messages" scroll-to-bottom indicator not dismissed after scrolling to bottom or switching conversations

2 participants

@cat0825@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205) - #2211

Merged
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator
Aug 5, 2026
Merged

fix(desktop): clear "New messages" indicator at bottom and per conversation (#2205)#2211
Astro-Han merged 3 commits into
apache:mainfrom
cat0825:fix/2205-new-messages-indicator

Conversation

@cat0825

Copy link
Copy Markdown
Contributor

Fixes#2205

Summary

The desktop chat surface's "New messages" scroll-to-bottom indicator now clears when the user scrolls back to the bottom, and its state resets fully when switching conversations. Repro screenshots are attached in the issue.

Root cause

Two independent problems in @astryxdesign/core@0.2.0:

  1. Not dismissed at the bottom.useChatNewMessages only clears hasNewMessages via dismiss() (the indicator button). useChatStreamScroll re-locks auto-follow on scrollend but never reports "at bottom" back to the indicator, so the label stays visible after the user scrolls back down.
  2. Leaks across conversations.ChatLayout is not remounted per conversation — lastMessageRef (pointing at the previous conversation's last message node) and the unlocked isLocked=false survive the switch, so the first message of a new conversation re-flags hasNewMessages.

Changes

  • apps/desktop/src/renderer/app-shell.tsx, quote-companion-panel.tsx: key ChatSurfaceLayout per conversation so the scroll/new-message state is remounted and reset on conversation switch.
  • patches/@astryxdesign+core+0.2.0.patch: adds the two @astryxdesign/core hunks for this fix (ChatLayout.js, useChatNewMessages.js). The file already carried the fix(ui): localize the field required/optional marker app-wide #2184FieldLabel localization hunks, so the patch now contains both sets; verified with git apply --check that the merged patch applies cleanly to a pristine @astryxdesign/core@0.2.0.
  • patches/README.md: rationale and removal conditions for the new hunks, following the repo convention.
  • apps/desktop/e2e/new-messages-indicator.spec.ts: Playwright regression coverage for both reported symptoms.

Verification

  • New e2e spec passes; with the fix reverted the two new cases fail exactly as described in the issue (negative control).
  • Regression: quote-companion, send-message, scroll-geometry, sidebar-navigation e2e suites (17 tests), packages/ui unit tests (316/316), desktop typecheck and biome all clean.

@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch 2 times, most recently from 0732a67 to 81759dbCompareAugust 5, 2026 07:20
…ersation (apache#2205)
Astryx ChatLayout only cleared hasNewMessages through the button's
dismiss(), so scrolling back to the bottom re-locked auto-follow but left
the "New messages" label visible; and a conversation switch reused the
same ChatLayout instance, leaking hasNewMessages, lastMessageRef and the
unlocked scroll state into the new conversation, whose first message then
re-triggered the indicator.
- patches/@astryxdesign+core+0.2.0.patch: ChatLayout clears the flag on
every re-lock (scrollend within the lock threshold) and gains an
optional conversationKey prop that resets the scroll lock and the
new-message baseline when it changes; useChatNewMessages exposes reset().
- ChatSurfaceLayout forwards conversationKey from the active session id
(app-shell + quote companion). No remount: a remount would drop an
in-progress composer draft, a regression composer-skill-invocation e2e
caught on the earlier keyed-remount attempt.
- e2e regression: new-messages-indicator.spec.ts passes with the fix and
fails without it.
@cat0825
cat0825force-pushed the fix/2205-new-messages-indicator branch from 81759db to 0b40f6aCompareAugust 5, 2026 07:29
Resolve patches/README.md conflict by keeping both sections:
- our apache#2205 "New messages" indicator section
- upstream apache#2225 Astryx List accessible-name section
Verified the merged @astryxdesign/core patch still applies cleanly
via patch-package --error-on-fail.
@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for this — the fix is well-scoped and I verified it end to end in a browser harness against the patched dist: the badge appears on unlocked growth, clears on real scrollend at bottom, resets on conversation switch, and the composer element survives the switch (no remount — conversationKey is a plain prop, the patch resets in place, which also keeps it compatible with #2239's arrival-pin approach; that PR is still open, so nothing merged is at risk). The patch applies cleanly to a pristine @astryxdesign/core@0.2.0, and the patches/README.md conflict with the merged #2225 is purely mechanical (both appended sections; keep both).

P1 — the e2e selectors no longer match main: #2202 (merged after your branch point) localized the two Astryx keys. The spec's header comment claims "Maka's Astryx overrides do not cover these keys", but packages/ui/src/astryx-i18n.tsx:97-98 covers both — '@astryx.chatLayout.newMessages''跳到最新消息' and '@astryx.chatLayoutScrollButton.scrollToBottom''滚动到底部' — and the e2e window fixture runs zh. After a squash-merge both tests would fail at the first English-name assertion (scroll-geometry-adjacent, on the e2e shard). Please switch the selectors to '跳到最新消息' / '滚动到底部' (the rest of the spec already uses zh names) and fix the comment — otherwise the regression guard ships red and its negative control is void.

P3 (optional):

  • growTranscriptOverflow only guarantees scrollHeight > clientHeight, but the 'Scroll to bottom' visibility depends on Astryx's 100px buttonThreshold — a compact 3-message transcript could fail at that assertion with a misleading error. Assert sh - ch > 100 with diagnostics instead.
  • The "no remount → draft survives" constraint has no e2e guard: composer-skill-invocation / skill-draft-lifecycle only cover revision drafts, not cross-conversation persistence. Typing a draft in test 2 before switching (and asserting it survives) would pin the property that makes the prop-not-key design load-bearing. Cheap.
  • The quote-companion side (conversationKey for the fork session) has no e2e coverage — acceptable per the representative-journey guidance, just noting the leak would go unnoticed there.

The fix itself is sound — happy to approve once the selectors are updated.

Root causes of the CI e2e failures (this spec had never run on Linux
before the upstream-main merge):
1. Button names are locale-dependent — Maka's Astryx copy overrides
(astryx-copy.ts) map 'New messages'/'Scroll to bottom' to zh
('跳到最新消息'/'滚动到底部') under the default zh fixture locale.
The spec hardcoded the en names, so both scroll-button assertions
could not find the button. Match both locales — the assertion is
about the affordance, not Astryx's copy.
2. The prompt anchor rail (upstream apache#2237) renders a preview of the
sent text, so loose getByText(/Fake backend received: .../) regexes
matched two nodes (preview + transcript echo) → strict-mode
violation. Assert with { exact: true } on the echo, which the longer
preview never matches.
3. growTranscriptOverflow only required sh > ch (merely scrollable);
Astryx flags scrolled-up only past buttonThreshold (100px), so a
compact font could leave the button unrendered. Grow until
sh - ch > 150px (threshold + headroom), up to 6 messages.
Verified locally: 2/2 pass.
@cat0825

Copy link
Copy Markdown
ContributorAuthor

Fixed the e2e failures — root cause was not the product fix, it was the spec itself (it had never run on Linux before this merge):

  1. Locale: Maka's Astryx copy overrides (astryx-copy.ts) map the scroll buttons to zh ('跳到最新消息'/'滚动到底部') under the default zh fixture locale; the spec hardcoded the en names. Now matches both locales — the assertion is about the affordance appearing/clearing, not Astryx's copy.
  2. Strict-mode violation: the prompt anchor rail (upstream perf(ui): seed transcript geometry before progressive fill #2237) renders a preview of sent text, so loose getByText(/Fake backend received: .../) matched two nodes. Now uses { exact: true } on the echo.
  3. Threshold: growTranscriptOverflow required only sh > ch; Astryx flags scrolled-up only past buttonThreshold (100px), so a compact font could keep the button unrendered. Grows until sh - ch > 150px.

Verified locally: 2/2 pass. Pushed as 243b921.

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks — the new commit resolves everything from the prior pass: the locale-agnostic matchers cover both the zh override (astryx-copy.ts:114-115) and Astryx's en defaults, the { exact: true } echoes correctly dodge the prompt-rail preview (which stays mounted via HoverCard), and the 150px growth bound (buttonThreshold 100 + headroom) is bounded and cannot hang. I also verified the conversationKey passthrough at both call sites (app-shell and quote-companion — the companion's companionSession?.id ?? sourceSession?.id is the right key, since each quote source forks a new session), the patch is exactly the 3 #2205-related hunks and applies cleanly to pristine 0.2.0, and both tests fail on the pre-fix product code.

One new finding, tracked as a follow-up rather than a blocker:

P2 (deferred, not blocking) — transient strict-mode double match during streaming. Between the fake backend's chunk 6 and 7 (~45ms window), the rail's reply preview normalizes+trims to exactly the query string while the transcript's first <p> also matches — two elements at a poll instant → Playwright's strict-mode violation is non-retriable, so the whole spec hard-fails for that run (probability is phase-dependent; author's local runs and CI are green). One-line fix: scope the echo locators to the message list (.maka-chat-message-list — the rail is a sibling), or assert after settle. If we see strict mode violation in future CI runs of this spec, that's the cause.

P3s (open, optional): the quote-companion conversationKey path still has no e2e coverage (the spec covers only the sessions surface), and the fixed 250ms wait could be a poll.

Merging now — thanks for the fast turnaround!

@Astro-Han
Astro-Han merged commit 2b5852f into apache:mainAug 5, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(desktop): "New messages" scroll-to-bottom indicator not dismissed after scrolling to bottom or switching conversations

2 participants

@cat0825@Astro-Han