Skip to content

AIDA-2346: Keep spoilers hidden in reminder previews - #2261

Merged
ravarora2 merged 1 commit into
mainfrom
rarora/aida-2346-spoiler-previews
Nov 3, 2026
Merged

ravarora2 merged 1 commit into
mainfrom
rarora/aida-2346-spoiler-previews

Conversation

@ravarora2

Copy link
Copy Markdown
Contributor

Why

AIDA-2346: a message hides Rosebud in The answer is ||Rosebud||, but its reminder exposes and saves the secret in plain text.

What

Replace spoiler content with [Spoiler] before previews reach reminder storage. Protect existing reminder previews and due notifications too. Open the original message to reveal the spoiler.

How

Reuse the message renderer's spoiler rules before text flattening and length limits. Preserve literal code and escaped pipes when reading original message source. Cover delimiter collisions, emphasis boundaries and closing delimiters beyond the source limit.

Old saved previews lost formatting and may have lost closing delimiters. Hide their remainder from the first || onward, since literal pipes inside a spoiler cannot be distinguished safely.

Risk

The shared preview generator also affects message and plugin notifications. Conservative handling can hide extra text in saved previews with literal pipes. Previously stored bytes are not rewritten.

Testing

On rebased commit 8a2030bda17b722d06f74163e7fbc5a988078562, exercised production MessageMarkdown, RemindDialog and RemindersPage in a local browser fixture:

  1. Show The answer is ||Rosebud||; confirm the source spoiler is hidden.
  2. Select Set a reminder, then Set reminder.
  3. Confirm both the saved preview argument and reminder row contain The answer is [Spoiler].
  4. Reveal the original message; confirm the reminder stays redacted.

Also checked old reminder rows in Chromium and WebKit, light/dark themes, 1000px/360px widths, including 200% text. No page errors or document horizontal overflow in those checks. Reused existing fixture components and in-memory storage; no browser journeys were added or removed. Parser and storage-boundary regression cases live in colocated unit/component tests.

Required pre-push TypeScript, related unit tests and design checks passed on the rebased commit. No hooks were bypassed.

Earlier local validation before rebasing: full Vitest run 15,361 passed, 21 skipped; final four affected test files 102 passed; TypeScript and production build passed. Independent review found no remaining blockers after fixes. These counts describe the earlier snapshot, not the rebased commit.

Human acceptance, installed desktop behavior, native banners and live relay persistence/relaunch remain untested. This PR stays draft pending human testing.

View the local screenshot in Buzz (Buzz sign-in required). The screenshot is from the earlier validation; the same flow passed again on the rebased commit above. Native GitHub attachment upload is unavailable in the installed CLI, so the screenshot is linked rather than embedded.

Generated with Codex

Co-authored-by: BuzzPRReviewAssistant1 <a581a7e8115222acdc32ffb14bfce55d48c24478d98e155f8eba126b69b83c7b@buzz.block.builderlab.xyz>
Signed-off-by: Ravneet Arora <[email protected]>
@ravarora2
ravarora2 marked this pull request as ready for review November 3, 2026 16:31
@ravarora2
ravarora2 requested a review from a team as a code owner November 3, 2026 16:32

@loganj loganj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Larry and Auto: an independent adversarial review of 8a2030b. Source review only; we did not test the installed desktop app. Larry and Auto each reviewed the PR without seeing the other's findings. We reproduced every finding below in Vitest on this head.

Approving, on condition that the two must-fix items are fixed before merge. Both are small and well scoped:

  1. error, must-fix: OS message notifications still show a spoiler that crosses the 4096-character cut (inline on content.ts:79).
  2. error, must-fix: new reminders hide literal code pipes (inline on RemindersPage.tsx:515).

Other items:

  • question: pluginNotificationText now gives every plugin body spoiler semantics. For example, a GitHub plugin body fix a || b || c becomes fix a [Spoiler] c. Is that intended for plugin text that is not Buzz message Markdown? If not, apply the redaction only to message-derived text.
  • advice: content.ts:67-77 duplicates the renderer's delimiter-marker step (protectInlineContent in MessageMarkdown.tsx), with its own collision check. One owner in markdown-preparation.ts, which already owns spoilerDelimiters, would mean one place to fix bugs like the next item.
  • advice, follow-up (also in the renderer, not a regression): an inline HTML declaration can swallow the marker. hi <!x ||Rosebud|| previews as hi <!x <>Rosebud||, and MessageMarkdown shows the same leak. Suggest one follow-up that fixes both paths.
  • advice, perf: a preview that contains || now parses Markdown about four times. That is bounded to 4096 characters, but InboxPage.tsx:1163 runs it unmemoized for each row on every render. Reuse the prepareMarkdown scan or memoize there.
  • advice: the Inbox row previews (InboxPage.tsx:1163) now redact too. That is good, but the PR body and tests do not mention it.
  • advice, follow-up (pre-existing, outside AIDA-2346): these surfaces still show raw spoiler text:
    • search message results (useSearchMessages.ts:94)
    • search channel detail and the Solo preview (channel.preview is raw row.content from membership.ts:53, used at SearchResults.tsx:976 and SoloDestination.tsx:192)
    • the Buzz link hover card (BuzzLinkPreview.tsx:159)
    • the audience row aria-label (MessageRow.tsx:1392)
  • preference: when the 4096 cut applies, hideUnclosed hides every unclosed || in the message, not only the block at the cut. This hides extra text but never exposes any.
  • preference: the 200-character shortText cut can split the label itself ([Sp…).

Tests: the new tests prove the main claim. With the four source files reset to the merge base, 22 of them fail. Two gaps match the must-fix items: no test goes through the real notification input (a pre-cut previewContent), and no test saves a new reminder for a message with literal code pipes and then reads it back.

Fine as is: spoilers inside code spans, fences and links stay literal, the same as the renderer. Escaped pipes, entities, emphasis around delimiters, multi-line spoilers, headings, quotes, lists, |||x||| and HTML inside a spoiler also match the renderer, and none of them exposed spoiler text.

Checks: on this head, Auto ran the full Vitest suite (875 files, 15,477 passed, 21 skipped) and tsc --noEmit (clean). Larry ran the affected directories (105 files, 2,621 passed).

for (const offset of [...prepared.spoilerDelimiters].reverse())
source = source.slice(0, offset) + delimiter + source.slice(offset + 2);
const { tree, tooDeep } = scanMarkdown(source);
if (!tooDeep) remarkSpoilers(delimiter, content.length > end)(tree);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error, must-fix: this check never fires for OS message notifications. messageNotificationText passes previewContent, and session.ts:3392 already cut that with content.slice(0, 4096), so content.length > end is always false on that path.

Repro: "The answer is ||Rosebud. " + "filler ".repeat(700) + "||".

  • Through messageNotificationText, the body is The answer is ||Rosebud. filler filler ….
  • messagePreview on the full text returns The answer is [Spoiler].

The "secret ".repeat(700) test passes uncut content straight to messagePreview, so it misses the real path.

Suggested fix: keep bounding in one place. Either stop the pre-cut in session.ts and let plainText do the cut, or have the producer send a truncation flag (slicing to 4097 would also work). Add a test that goes through messageNotificationText with a pre-cut body. This also makes the new buzz-help.md line about notifications true.

@ravarora2 ravarora2 Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed in db17c75ff in follow-up PR #2285. #2261 merged before this follow-up could be published. The live session keeps its 4,096-character preview bound and carries previewTruncated when the source continues. messageNotificationText passes that provenance to the spoiler formatter, which hides an unfinished spoiler after a cut. Coverage now includes your pre-cut notification example and a signed live event through the actual session delivery path into messageNotificationText. An ordinary short unfinished delimiter remains literal.

pending && reminder.notBefore !== undefined && reminder.notBefore <= now;
const preview = reminder.target?.preview
? messagePreview(reminder.target.preview)
? messagePreview(reminder.target.preview, { storedPreview: true })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error, must-fix: this conservative read also hits every new reminder whose message has literal pipes. RemindDialog.tsx:49 saves messagePreview(content), and that output keeps literal || from code spans, escapes and entities. This PR's own test expects `||literal||` to become ||literal||.

Repro: the message run `a || b` now is saved as run a || b now. The reminder row and the due banner then both show run a [Spoiler].

The comment at content.ts:57-59 ("newly saved redacted previews contain no such delimiter") is therefore not true. The Risk section describes this only for old saved previews.

Suggested fix, verified by Auto: have RemindDialog save preview.replaceAll("||", "\\|\\|"). The conservative reader then finds no raw ||, so new rows read back as run a || b now, and old rows still take the conservative path. A version marker on the saved preview would also work.

@ravarora2 ravarora2 Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed in db17c75ff in follow-up PR #2285. #2261 merged before this follow-up could be published. RemindDialog redacts real spoilers, then escapes all CommonMark ASCII punctuation in the stored preview, including pipes and backslashes. One Markdown conversion restores the literal text in rows and banners; old ambiguous previews retain conservative redaction. Added save → serialized storage → RemindersPage cases for code pipes, paired pipes, escapes, entities, backslashes and real spoilers, plus banner tests through NotificationsService. Both Chromium and WebKit preserve run a || b || c now after saving. The author explicitly scoped this change to the current app; older clients that display the stored preview directly may show escapes.

(text) => text && plainText(text),
) ?? "A reminder is waiting",
[
plainText(reminder.target?.preview ?? "", {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

advice: the due banner is now flattened twice. These calls run plainText, and then notify.submit runs pluginNotificationText, which flattens again. On main the note was flattened only once.

Effects:

  • A note \*urgent\* shows as urgent; on main it shows *urgent*.
  • A preview of `a || b || c` becomes a [Spoiler] c.

Suggestion: redact the saved preview once, pass the note raw, and let the plugin-notification step do the single flatten. This interacts with the fix on RemindersPage.tsx:515.

@ravarora2 ravarora2 Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed in db17c75ff in follow-up PR #2285. #2261 merged before this follow-up could be published. The reminder producer now removes only an ambiguous legacy spoiler tail before submission, and passes notes without flattening them. The selected source is converted once by pluginNotificationText in NotificationsService. plainText no longer applies Buzz spoiler semantics to generic plugin text. The real notification-service test covers an escaped urgent note, literal/paired pipes, and an old unfinished spoiler preview.

let bounded = content.slice(0, end);
// Old flattened text cannot distinguish a real closing delimiter from
// literal pipes inside the hidden text. Hide everything after its first
// possible opener; newly saved redacted previews contain no such delimiter.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convention: this comment is not accurate. Newly saved previews can contain ||, because literal pipes from code, escapes and entities survive messagePreview (see the comment on RemindersPage.tsx:515). Update the comment along with the fix.

@ravarora2 ravarora2 Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed in db17c75ff in follow-up PR #2285. #2261 merged before this follow-up could be published. Removed the inaccurate claim about new previews. The legacy-redaction helper now documents lost code/escape provenance, and the save boundary documents its punctuation encoding for one later Markdown conversion. Regression tests cover literal pipes surviving both row and notification reads.

@ravarora2

ravarora2 commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Follow-up in db17c75ff addresses both must-fix items, the double-flattening regression, the inaccurate comment, and the plugin scope question.

Published as follow-up PR #2285, because #2261 merged and its branch was deleted before this update could be pushed. The follow-up is based on that merge. Required TypeScript/related-unit and design checks passed on its head; the Chromium/WebKit workflows passed again after rebasing.

  • Live notifications retain truncation provenance; real session-delivery coverage exercises the 4,096-character boundary.
  • New reminder previews encode literal punctuation after spoiler redaction. Row round trips and the actual notification service preserve pipes, backslashes and escaped notes.
  • Generic plugin notifications retain ordinary CommonMark semantics. Buzz spoiler parsing applies to message-derived previews.
  • Inbox preview behavior now has component coverage and is described in the help guide and PR body.
  • The author explicitly chose current-app-only scope. Older raw-preview readers may show encoding escapes; no shared format field or protocol tag was introduced.

Disposition of the remaining nonblocking advice and preferences:

  • Shared delimiter protection and the repeated-parse/Inbox memoization advice remain a follow-up, as scoped before this update. The message preview still bounds source parsing to 4,096 characters; this update does not refactor the renderer or claim a performance improvement.
  • The inline HTML declaration leak is pre-existing in both the renderer and preview path. It remains a follow-up requiring a common fix for both paths.
  • The pre-existing search results, channel/Solo previews, Buzz-link hover preview and audience aria-label exposures remain separate follow-ups. They are not fixed by this PR.
  • Conservative handling of an unfinished spoiler in truncated source remains intentional to avoid exposure. The [Sp… placeholder at the output boundary retains the existing 200-codepoint limit; that preference is unchanged.

Before rebasing onto the merge of #2261: 109 reminder/formatter tests, 165 live-delivery tests and 129 Inbox tests passed; TypeScript passed. Chromium/WebKit component checks passed for both spoilers and new literal-pipe reminders. The full Vitest run reported 15,486 passed, 1 failed, 21 skipped. The failure was entity-navigation.test.tsx / “opens Inbox for a malformed launch link when the catalog settles after the client” (missing the expected dialog). That entire file then passed 25/25 when run separately on both the unchanged PR head and this patch. No tests, timeouts or assertions were weakened; the full run is not claimed clean. The initial concurrent Inbox run also had three failures, followed by a separate 129/129 pass; this remains recorded in the validation notes.

Human acceptance, the installed desktop app, OS banners and live relay persistence remain unverified. Browser evidence uses the actual components with local fixture storage. No new browser journeys were added or removed.

@ravarora2
ravarora2 merged commit f33d469 into main Nov 3, 2026
41 checks passed
@ravarora2
ravarora2 deleted the rarora/aida-2346-spoiler-previews branch November 3, 2026 17:51
dr0uleau added a commit that referenced this pull request Nov 3, 2026
…xecution-support-for-native-buzz-33ea2bea

* origin/main: (74 commits)
  feat(host): harden byte lifecycle and add protected image upload (#2199)
  Updated onboarding (#2183)
  perf(relay): read each signed roster once in large channels (#2244)
  AIDA-2346: Keep spoilers hidden in reminder previews (#2261)
  fix: AIDA-2371 Keep sidebar selection pill aligned during resize (#2182)
  fix(relay): skip background rereads of profiles a read found absent (#2147)
  fix: prevent startup logo selection (#1823)
  fix(agents): send sign-in evidence with agent Git credentials (#2240)
  perf(mentions): stop scanning every member on each mention keystroke (#2246)
  fix(attachments): send image dimensions so previews don't shift (#2262)
  feat(shortcuts): add Slack-style hotkeys (#2170)
  perf(plugins): share host request clients across calls (#2243)
  fix(identity): keep federated sessions and agent grants in one keychain item (#2225)
  fix(profiles): evict the least recently requested profile (#2255)
  fix(unread): repair unread from the recovery tick only every 15 minutes (#2252)
  AIDA-2374: Use readable labels in chat titles (#2185)
  perf(profiles): keep verified names checked through a planned renewal (#2234)
  feat(plugins): let a plugin ask Buzz to review an install (#2059)
  fix(identity): sign FI builds in at first run through the native session (FOUNDATION: App.tsx, services.ts) (#2249)
  Place profile verification beside the verified name (#2224)
  ...

Signed-off-by: Daniel Rouleau <[email protected]>
dr0uleau added a commit that referenced this pull request Nov 3, 2026
* commit 'c8e882165': (50 commits)
  Scope execution targets by harness session
  Read durable execution state before admission
  feat(host): harden byte lifecycle and add protected image upload (#2199)
  Updated onboarding (#2183)
  perf(relay): read each signed roster once in large channels (#2244)
  AIDA-2346: Keep spoilers hidden in reminder previews (#2261)
  fix: AIDA-2371 Keep sidebar selection pill aligned during resize (#2182)
  fix(relay): skip background rereads of profiles a read found absent (#2147)
  fix: prevent startup logo selection (#1823)
  fix(agents): send sign-in evidence with agent Git credentials (#2240)
  perf(mentions): stop scanning every member on each mention keystroke (#2246)
  fix(attachments): send image dimensions so previews don't shift (#2262)
  feat(shortcuts): add Slack-style hotkeys (#2170)
  perf(plugins): share host request clients across calls (#2243)
  fix(identity): keep federated sessions and agent grants in one keychain item (#2225)
  fix(profiles): evict the least recently requested profile (#2255)
  fix(unread): repair unread from the recovery tick only every 15 minutes (#2252)
  AIDA-2374: Use readable labels in chat titles (#2185)
  perf(profiles): keep verified names checked through a planned renewal (#2234)
  feat(plugins): let a plugin ask Buzz to review an install (#2059)
  ...

Signed-off-by: Daniel Rouleau <[email protected]>
Co-authored-by: Codex <[email protected]>
ljknight added a commit to ljknight/buzz-app that referenced this pull request Nov 3, 2026
…ection

* origin/main: (39 commits)
  Authorize local apps to use Buzz MCP tools (block#2211)
  feat(bestie): choose the private channels Bestie can read (block#2036)
  Polish message actions, spacing, and membership notices (block#2258)
  test(unread): prevent startup catch-up in receipt reserve journey (block#2274)
  test(search): wait for rejected join clearance to settle (block#2278)
  test(reactions): provide stable emoji media for icon rendering (block#2279)
  feat(profiles): reserve profile room for an open channel's roster (block#2269)
  feat(host): harden byte lifecycle and add protected image upload (block#2199)
  Updated onboarding (block#2183)
  perf(relay): read each signed roster once in large channels (block#2244)
  AIDA-2346: Keep spoilers hidden in reminder previews (block#2261)
  fix: AIDA-2371 Keep sidebar selection pill aligned during resize (block#2182)
  fix(relay): skip background rereads of profiles a read found absent (block#2147)
  fix: prevent startup logo selection (block#1823)
  fix(agents): send sign-in evidence with agent Git credentials (block#2240)
  perf(mentions): stop scanning every member on each mention keystroke (block#2246)
  fix(attachments): send image dimensions so previews don't shift (block#2262)
  feat(shortcuts): add Slack-style hotkeys (block#2170)
  perf(plugins): share host request clients across calls (block#2243)
  fix(identity): keep federated sessions and agent grants in one keychain item (block#2225)
  ...

# Conflicts:
#	src/features/messages/ThreadPanel.tsx
arividar pushed a commit to minniai/minni-buzz-app that referenced this pull request Nov 4, 2026
## Why

[AIDA-2346](https://linear.app/squareup/issue/AIDA-2346): a message
hides `Rosebud` in `The answer is ||Rosebud||`, but its reminder exposes
and saves the secret in plain text.

## What

Replace spoiler content with `[Spoiler]` before previews reach reminder
storage. Protect existing reminder previews and due notifications too.
Open the original message to reveal the spoiler.

## How

Reuse the message renderer's spoiler rules before text flattening and
length limits. Preserve literal code and escaped pipes when reading
original message source. Cover delimiter collisions, emphasis boundaries
and closing delimiters beyond the source limit.

Old saved previews lost formatting and may have lost closing delimiters.
Hide their remainder from the first `||` onward, since literal pipes
inside a spoiler cannot be distinguished safely.

## Risk

The shared preview generator also affects message and plugin
notifications. Conservative handling can hide extra text in saved
previews with literal pipes. Previously stored bytes are not rewritten.

## Testing

On rebased commit `8a2030bda17b722d06f74163e7fbc5a988078562`, exercised
production `MessageMarkdown`, `RemindDialog` and `RemindersPage` in a
local browser fixture:

1. Show `The answer is ||Rosebud||`; confirm the source spoiler is
hidden.
2. Select **Set a reminder**, then **Set reminder**.
3. Confirm both the saved preview argument and reminder row contain `The
answer is [Spoiler]`.
4. Reveal the original message; confirm the reminder stays redacted.

Also checked old reminder rows in Chromium and WebKit, light/dark
themes, 1000px/360px widths, including 200% text. No page errors or
document horizontal overflow in those checks. Reused existing fixture
components and in-memory storage; no browser journeys were added or
removed. Parser and storage-boundary regression cases live in colocated
unit/component tests.

Required pre-push TypeScript, related unit tests and design checks
passed on the rebased commit. No hooks were bypassed.

Earlier local validation before rebasing: full Vitest run **15,361
passed, 21 skipped**; final four affected test files **102 passed**;
TypeScript and production build passed. Independent review found no
remaining blockers after fixes. These counts describe the earlier
snapshot, not the rebased commit.

Human acceptance, installed desktop behavior, native banners and live
relay persistence/relaunch remain untested. This PR stays draft pending
human testing.

[View the local screenshot in
Buzz](https://buzz.block.builderlab.xyz/media/4ac3dee43dc990ec5f86043ad5e70464ed6dc77bad6dc26e07554ca44b77ff86.png)
(Buzz sign-in required). The screenshot is from the earlier validation;
the same flow passed again on the rebased commit above. Native GitHub
attachment upload is unavailable in the installed CLI, so the screenshot
is linked rather than embedded.

Generated with Codex

Signed-off-by: Ravneet Arora <[email protected]>
Co-authored-by: BuzzPRReviewAssistant1 <a581a7e8115222acdc32ffb14bfce55d48c24478d98e155f8eba126b69b83c7b@buzz.block.builderlab.xyz>
Sign up for free to 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.

2 participants