Skip to content

fix(mobile): keep #-prefixed markdown link labels visible - #6135

Draft
olympusbuildz wants to merge 1 commit into
block:mainfrom
olympusbuildz:fix/mobile-hash-link-label-6124
Draft

fix(mobile): keep #-prefixed markdown link labels visible#6135
olympusbuildz wants to merge 1 commit into
block:mainfrom
olympusbuildz:fix/mobile-hash-link-label-6124

Conversation

@olympusbuildz

@olympusbuildz olympusbuildz commented Aug 17, 2026

Copy link
Copy Markdown

Problem

On iOS, Markdown links whose label starts with # render as nothing. A body like:

See [#2959](https://github.com/block/buzz/issues/2959) for details.

shows See for details. — no label, no underline, no tap target. Desktop renders the same message correctly.

Root cause

  1. MessageContent registers _ChannelLinkMd (and mentions/emoji) ahead of gpt_markdown defaults.
  2. Default ATagMd builds the link label by re-running the full inlineComponents list on the label text.
  3. _ChannelLinkMd matches digits-only tokens (#2959) and replaces them with a _TokenPill WidgetSpan.
  4. _buildLink only collects plain TextSpan.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 stock ATagMd from the list to avoid double handling.
  • Channel generic token: require at least one letter/underscore so bare digits-only #2959 is 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

cd mobile
flutter test test/features/channels/message_content_test.dart
# 76 passed @ fa17ab2ed

Named regressions:

  • digits-only bare #token is not a channel pill
  • keeps #-prefixed link labels visible (not channel pills)
  • known channel name inside a link label stays plain text
  • existing #channel links group still green

Manual verify

  1. Post See [#2959](https://github.com/block/buzz/issues/2959) for details. from Desktop.
  2. Open the message on iOS — label #2959 visible, tappable, opens the URL.
  3. Bare #general for a real channel still pills and navigates.

Risk / blast radius

  • Link labels no longer turn into channel/mention pills (authored [label](url) keeps plain label text). Bare #channel / @mention outside links unchanged.
  • Digits-only bare #N no 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)

  • Rebased onto current main
  • Head: b8d66159908f9cb5b5420c20a740cee85275a981
  • Mini: cd mobile && flutter test test/features/channels/message_content_test.dart (see nest)

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>
@masfour7

Copy link
Copy Markdown

We reviewed this from the gpt_markdown side. The diagnosis and regression coverage are correct, but 1.2.1 provides a smaller fix that avoids maintaining a fork of ATagMd.

After upgrading to:

gpt_markdown: ^1.2.1

add this to _MentionMd, _ChannelLinkMd, and CustomEmojiMd:

@override
Set<MarkdownScope> get scopes =>
    MarkdownComponent.allScopesExceptLinkLabel;

This prevents the custom widget components from running inside authored link labels, which removes the nested WidgetSpan responsible for the invisible iOS output.

Because Buzz is upgrading from 1.1.6, imageBuilder also needs the current signature:

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 InlinePattern.prefixed, which excludes link labels by default.

With that approach, _AuthoredMarkdownLinkMd and the filtering of the stock ATagMd can be removed while keeping the PR’s existing regression tests.

wesbillman pushed a commit that referenced this pull request Sep 8, 2026
## 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 |
![before-inline-code-light](https://raw.githubusercontent.com/TolgaCinisli/buzz/2d2d846291416d9b32d3fb9cfead950bcc4fe123/pr-6631--before-inline-code-light.png)
|
![after-inline-code-light](https://raw.githubusercontent.com/TolgaCinisli/buzz/f230b95c7260a32bd5d76b1ac42130720a168521/pr-6631--after-inline-code-light.png)
|
| Dark |
![before-inline-code-dark](https://raw.githubusercontent.com/TolgaCinisli/buzz/2d2d846291416d9b32d3fb9cfead950bcc4fe123/pr-6631--before-inline-code-dark.png)
|
![after-inline-code-dark](https://raw.githubusercontent.com/TolgaCinisli/buzz/f230b95c7260a32bd5d76b1ac42130720a168521/pr-6631--after-inline-code-dark.png)
|

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>
yjc801 added a commit to yjc801/buzz that referenced this pull request Sep 8, 2026
* 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 |
![before-inline-code-light](https://raw.githubusercontent.com/TolgaCinisli/buzz/2d2d846291416d9b32d3fb9cfead950bcc4fe123/pr-6631--before-inline-code-light.png)
|
![after-inline-code-light](https://raw.githubusercontent.com/TolgaCinisli/buzz/f230b95c7260a32bd5d76b1ac42130720a168521/pr-6631--after-inline-code-light.png)
|
| Dark |
![before-inline-code-dark](https://raw.githubusercontent.com/TolgaCinisli/buzz/2d2d846291416d9b32d3fb9cfead950bcc4fe123/pr-6631--before-inline-code-dark.png)
|
![after-inline-code-dark](https://raw.githubusercontent.com/TolgaCinisli/buzz/f230b95c7260a32bd5d76b1ac42130720a168521/pr-6631--after-inline-code-dark.png)
|

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>
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.

mobile: Markdown links with a #-prefixed label render as nothing (desktop renders them fine)

2 participants