fix(mobile): keep #-prefixed markdown link labels visible - #6135
fix(mobile): keep #-prefixed markdown link labels visible#6135olympusbuildz wants to merge 1 commit into
Conversation
gpt_markdown re-runs inline components on link labels, so _ChannelLinkMd turned [block#2959](url) into a nested token pill and the link drew blank on iOS. Own the ATag path so labels skip mention/channel/emoji components, and reject digits-only bare #tokens as channels. Regression coverage in message_content_test for GitHub-style issue labels and known channel names inside link text. Signed-off-by: Olympusbuildz <Olympus.roots@outlook.com> Co-authored-by: Olympusbuildz <Olympus.roots@outlook.com> Signed-off-by: Olympusbuildz <Olympus.roots@outlook.com>
fa17ab2 to
b8d6615
Compare
|
We reviewed this from the After upgrading to: gpt_markdown: ^1.2.1add this to @override
Set<MarkdownScope> get scopes =>
MarkdownComponent.allScopesExceptLinkLabel;This prevents the custom widget components from running inside authored link labels, which removes the nested Because Buzz is upgrading from 1.1.6, imageBuilder: (context, imageUrl, width, height) =>
_buildMedia(context, imageUrl, imetaByUrl[imageUrl]),The digits-only channel regression should remain covered. The cleaner implementation is to match only known channel names—either by making the existing generic fallback optional or by moving the channel token to With that approach, |
## Summary Inline code on mobile renders as **bold body text on a faint background wash** — no monospace face, no chip, and it cannot wrap. #5257 diagnosed this as a missing `highlightBuilder`. That is no longer the right fix. `gpt_markdown` 1.2.0 deprecates `highlightBuilder` (removal in 2.0.0), renders inline code as a real chip, and adds `InlineCodeStyle` for restyling it. The package author confirmed this on the issue. So this PR is an upgrade — 1.1.6 → 1.2.1 — plus one theme declaration, rather than the builder the issue originally asked for. **Where the style is declared.** `GptMarkdownThemeData` goes in `AppTheme._buildTheme`, which both `light()` and `dark()` call. That reaches all four `GptMarkdown` call sites — `message_content`, `transcript_item_widget`, `token_pill`, `custom_emoji_render` — so the style is stated once instead of per widget. A widget-level `inlineCodeStyle` would have covered channel messages only, leaving the other three on the package's defaults. **What is declared.** Face, size, ink, chip fill and outline — not the face alone. A face name on its own leaves the rest on the package's defaults, which put inline code at 14.1sp beside a fenced block's 13, on a neutral `onSurface` tint rather than the app's code surface. In dark that tint is *lighter* than the surface, while every other code surface in the app is recessed, so the chip read as a different kind of object. All of it now comes from one `CodeStyle` declaration that the fenced block reads from too, so the two cannot be edited apart. **Three adaptations the upgrade requires.** Each was found by running the gate, not by reading the changelog: 1. **`imageBuilder` widened** to `(context, url, width, height)`. This is a hard compile error, and it is **not listed in the package's migration guide**, which states "nothing here stops code compiling". Worth reporting upstream. 2. **`autolink` now defaults to `true`.** `normalizeBareLinks()` already rewrites bare URLs into Markdown links before rendering, so both would run. `message_content` opts out with `autolink: false` to keep current behaviour exactly. The migration guide argues for dropping the pre-processor instead — a better fix, but a behavioural change that belongs in its own PR. 3. **`gpt_markdown.dart` now re-exports `markdown_config.dart`**, making two direct imports redundant. `flutter analyze` reports `No issues found!` on 1.1.6 and flags both on 1.2.1, so these warnings are new, not pre-existing. **Deliberately out of scope.** The three non-message call sites now autolink bare URLs, since only `message_content` has a pre-processor to collide with. Custom inline components (`_MentionMd`, `CustomEmojiMd`, `_ChannelLinkMd`) could additionally declare `allScopesExceptLinkLabel` — 1.2.0 offers it as the fix for a `WidgetSpan` chip going blank inside a link label on iOS — but current behaviour is unchanged without it, so that stays a separate change. ### Related issue Fixes #5257 Duplicate scan: searched `gpt_markdown`, `inline code mobile`, `highlightBuilder` and `InlineCodeStyle` across both PRs and issues. No open PR touches inline code styling. #6135 (link labels) and #6166 (text selection) also touch mobile Markdown but address different defects. ### Testing Full gate, `just ci` — exit 0: | Stage | Result | |---|---| | Rust (33 suites) | 4768 passed, 0 failed | | Desktop | 5799 passed, 0 failed | | Mobile | **2011 passed**, 0 failed | | `flutter analyze` | `No issues found!` | | Desktop + web build | ok | Run on the branch with `main` merged in, so these numbers match what CI builds. **New regression test** — `renders inline code in the app code style`. It resolves the `CodeTextSpan` the package tags inline code with, which carries both the resolved `TextStyle` and the colours the chip behind it is painted with, so face, size, ink, fill and outline are all asserted rather than a widget's presence. It is negative-controlled: reverting only the theme declaration fails it with ```text Expected: a numeric value within <0.001> of <13.0> Actual: <14.1> ``` and dropping the declaration entirely falls back to `packages/gpt_markdown/JetBrainsMono` — so the test measures the real thing, and it would catch a future regression that silently drops the theme extension. The test passes `baseStyle: messageBodyTextStyle`, the style the message surfaces actually use; the widget's own fallback is the smaller `bodyMedium`, which would move the expected size. The test finds paragraphs with `find.byWidgetPredicate((widget) => widget is RichText)`, not `find.byType(RichText)`: inline code renders through `BidiRichText`, a `RichText` subclass, and `byType` matches exact runtime types. That is a hazard for any test that reads text back out of a paragraph, and one landed after this branch was cut: `message_content_custom_emoji_test.dart` arrived with #6996 and its `code keeps literal emoji while adjacent known tokens render` case reads a code span through `find.byType(RichText)`. It passes on `main` and fails on the merge result, which is what CI builds, so it went red only once CI was authorized. It now uses the same predicate. The two other `byType(RichText)` call sites — the rest of that file and `message_author_meta_test.dart` — were re-run and pass: their content carries no code span, so the exact type still matches. They were left alone. ### Screenshots Rendered through the real `MessageContent` widget with the app's own fonts loaded, at 390pt wide, 3x DPR. Sample text: ``Set `BUZZ_RELAY_URL` before launch, then run `just mobile-test` to verify.`` | | Before (1.1.6) | After (1.2.1) | |---|---|---| | Light |  |  | | Dark |  |  | Before: bold Inter on a flat wash, no chip edge, and `just mobile-test` breaks across the line with the wash simply ending. After: Geist Mono in a bordered, rounded chip, and the wrapped fragment gets its own chip on each line. --------- Signed-off-by: Tolga Cinisli <tolgacinisli@gmail.com> Co-authored-by: Tolga Cinisli <tolgacinisli@gmail.com>
* fix(mobile): style inline code with the app mono face (block#6631) ## Summary Inline code on mobile renders as **bold body text on a faint background wash** — no monospace face, no chip, and it cannot wrap. block#5257 diagnosed this as a missing `highlightBuilder`. That is no longer the right fix. `gpt_markdown` 1.2.0 deprecates `highlightBuilder` (removal in 2.0.0), renders inline code as a real chip, and adds `InlineCodeStyle` for restyling it. The package author confirmed this on the issue. So this PR is an upgrade — 1.1.6 → 1.2.1 — plus one theme declaration, rather than the builder the issue originally asked for. **Where the style is declared.** `GptMarkdownThemeData` goes in `AppTheme._buildTheme`, which both `light()` and `dark()` call. That reaches all four `GptMarkdown` call sites — `message_content`, `transcript_item_widget`, `token_pill`, `custom_emoji_render` — so the style is stated once instead of per widget. A widget-level `inlineCodeStyle` would have covered channel messages only, leaving the other three on the package's defaults. **What is declared.** Face, size, ink, chip fill and outline — not the face alone. A face name on its own leaves the rest on the package's defaults, which put inline code at 14.1sp beside a fenced block's 13, on a neutral `onSurface` tint rather than the app's code surface. In dark that tint is *lighter* than the surface, while every other code surface in the app is recessed, so the chip read as a different kind of object. All of it now comes from one `CodeStyle` declaration that the fenced block reads from too, so the two cannot be edited apart. **Three adaptations the upgrade requires.** Each was found by running the gate, not by reading the changelog: 1. **`imageBuilder` widened** to `(context, url, width, height)`. This is a hard compile error, and it is **not listed in the package's migration guide**, which states "nothing here stops code compiling". Worth reporting upstream. 2. **`autolink` now defaults to `true`.** `normalizeBareLinks()` already rewrites bare URLs into Markdown links before rendering, so both would run. `message_content` opts out with `autolink: false` to keep current behaviour exactly. The migration guide argues for dropping the pre-processor instead — a better fix, but a behavioural change that belongs in its own PR. 3. **`gpt_markdown.dart` now re-exports `markdown_config.dart`**, making two direct imports redundant. `flutter analyze` reports `No issues found!` on 1.1.6 and flags both on 1.2.1, so these warnings are new, not pre-existing. **Deliberately out of scope.** The three non-message call sites now autolink bare URLs, since only `message_content` has a pre-processor to collide with. Custom inline components (`_MentionMd`, `CustomEmojiMd`, `_ChannelLinkMd`) could additionally declare `allScopesExceptLinkLabel` — 1.2.0 offers it as the fix for a `WidgetSpan` chip going blank inside a link label on iOS — but current behaviour is unchanged without it, so that stays a separate change. ### Related issue Fixes block#5257 Duplicate scan: searched `gpt_markdown`, `inline code mobile`, `highlightBuilder` and `InlineCodeStyle` across both PRs and issues. No open PR touches inline code styling. block#6135 (link labels) and block#6166 (text selection) also touch mobile Markdown but address different defects. ### Testing Full gate, `just ci` — exit 0: | Stage | Result | |---|---| | Rust (33 suites) | 4768 passed, 0 failed | | Desktop | 5799 passed, 0 failed | | Mobile | **2011 passed**, 0 failed | | `flutter analyze` | `No issues found!` | | Desktop + web build | ok | Run on the branch with `main` merged in, so these numbers match what CI builds. **New regression test** — `renders inline code in the app code style`. It resolves the `CodeTextSpan` the package tags inline code with, which carries both the resolved `TextStyle` and the colours the chip behind it is painted with, so face, size, ink, fill and outline are all asserted rather than a widget's presence. It is negative-controlled: reverting only the theme declaration fails it with ```text Expected: a numeric value within <0.001> of <13.0> Actual: <14.1> ``` and dropping the declaration entirely falls back to `packages/gpt_markdown/JetBrainsMono` — so the test measures the real thing, and it would catch a future regression that silently drops the theme extension. The test passes `baseStyle: messageBodyTextStyle`, the style the message surfaces actually use; the widget's own fallback is the smaller `bodyMedium`, which would move the expected size. The test finds paragraphs with `find.byWidgetPredicate((widget) => widget is RichText)`, not `find.byType(RichText)`: inline code renders through `BidiRichText`, a `RichText` subclass, and `byType` matches exact runtime types. That is a hazard for any test that reads text back out of a paragraph, and one landed after this branch was cut: `message_content_custom_emoji_test.dart` arrived with block#6996 and its `code keeps literal emoji while adjacent known tokens render` case reads a code span through `find.byType(RichText)`. It passes on `main` and fails on the merge result, which is what CI builds, so it went red only once CI was authorized. It now uses the same predicate. The two other `byType(RichText)` call sites — the rest of that file and `message_author_meta_test.dart` — were re-run and pass: their content carries no code span, so the exact type still matches. They were left alone. ### Screenshots Rendered through the real `MessageContent` widget with the app's own fonts loaded, at 390pt wide, 3x DPR. Sample text: ``Set `BUZZ_RELAY_URL` before launch, then run `just mobile-test` to verify.`` | | Before (1.1.6) | After (1.2.1) | |---|---|---| | Light |  |  | | Dark |  |  | Before: bold Inter on a flat wash, no chip edge, and `just mobile-test` breaks across the line with the wash simply ending. After: Geist Mono in a bordered, rounded chip, and the wrapped fragment gets its own chip on each line. --------- Signed-off-by: Tolga Cinisli <tolgacinisli@gmail.com> Co-authored-by: Tolga Cinisli <tolgacinisli@gmail.com> * fix(buzz-acp): wake held ACP threads and fence forked sessions (block#7340) ## Summary Adds an independent deadline wakeup so held thread work dispatches after its 10-second bound even when the relay loop is otherwise quiet. Fences session ownership by generation so a worker returning after a fork cannot make an older provider session claimable again. This follows up on the two post-merge findings from [block#7337](block#7337 (review)). ### Related issue Follow-up to block#7337. ### Testing - `cargo test -p buzz-acp` - `cargo clippy -p buzz-acp --all-targets -- -D warnings` - Pre-push file-size, differential Rust test, and desktop Tauri gates No UI changes. --- **Update Sep 4, 15:35:** Addressed both Codex review findings. - Queue-cap eviction now prunes orphaned hold deadlines. - An expired hold stays expired until a worker is successfully claimed. - Hold timers remain disabled while every worker is busy; worker return wakes dispatch directly. - Added regressions for queue eviction and pool exhaustion. Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> --------- Signed-off-by: Tolga Cinisli <tolgacinisli@gmail.com> Signed-off-by: Salman Mohammed <smohammed@squareup.com> Co-authored-by: TolgaCinisli <tolga.cinisli@photier.com> Co-authored-by: Tolga Cinisli <tolgacinisli@gmail.com> Co-authored-by: Salman Mohammed <smohammed@squareup.com>
Problem
On iOS, Markdown links whose label starts with
#render as nothing. A body like:shows
See for details.— no label, no underline, no tap target. Desktop renders the same message correctly.Root cause
MessageContentregisters_ChannelLinkMd(and mentions/emoji) ahead of gpt_markdown defaults.ATagMdbuilds the link label by re-running the fullinlineComponentslist on the label text._ChannelLinkMdmatches digits-only tokens (#2959) and replaces them with a_TokenPillWidgetSpan._buildLinkonly collects plainTextSpan.text, so the label becomes empty nested placeholders and draws blank on device.Paths:
mobile/lib/features/channels/message_content.dart,message_content/token_pill.dart.Fix
_AuthoredMarkdownLinkMd: own the[label](url)path; generate labels with stock formatting components only (no mention/channel/emoji). Drop stockATagMdfrom the list to avoid double handling.#2959is never a channel pill (defense in depth for non-link text too).Why it matters
GitHub-style issue/PR links in chat are common. Invisible links on mobile lose the destination and break reading flow; desktop already works.
Test plan
Named regressions:
digits-only bare #token is not a channel pillkeeps #-prefixed link labels visible (not channel pills)known channel name inside a link label stays plain text#channel linksgroup still greenManual verify
See [#2959](https://github.com/block/buzz/issues/2959) for details.from Desktop.#2959visible, tappable, opens the URL.#generalfor a real channel still pills and navigates.Risk / blast radius
[label](url)keeps plain label text). Bare#channel/@mentionoutside links unchanged.#Nno longer becomes a non-interactive channel pill (never a valid channel name).Closest work
none found (issue #6124 unlinked; related area #4572 is tappable-links, different symptom)
Fixes #6124
Acceptance retest (2026-08-18b)
mainb8d66159908f9cb5b5420c20a740cee85275a981cd mobile && flutter test test/features/channels/message_content_test.dart(see nest)