Skip to content

feat: add renderers and resolveImageSource to EnrichedMarkdownText - #1028

Merged
hryhoriiK97 merged 13 commits into
feat/block-renderers-nativefrom
feat/block-renderers-js
Oct 22, 2026
Merged

hryhoriiK97 merged 13 commits into
feat/block-renderers-nativefrom
feat/block-renderers-js

Conversation

@hryhoriiK97

@hryhoriiK97 hryhoriiK97 commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

What/Why?

Last of three stacked PRs for custom block renderers. This one turns the feature on: the public API, the web implementation, docs, the example screens and a saved device regression flow.

Stack: #1026 (core) → #1027 (native hosts) → this PR.

  • renderers: React replacements keyed by kind (image, video, link) for a paragraph that is only an image, a <video> block, or a paragraph that is only a link (a [label](url), an autolink or a bare URL), directly in the document, a blockquote or an admonition outside any list. Return null to keep the native block. A renderer that throws, or returns a string or number, keeps the native block with a dev warning. Blocks carry stable ids safe as React keys.
  • resolveImageSource: the request to make for each image occurrence instead of its Markdown URL (a signed URL, per-image headers). Resolved once per occurrence id for the component's lifetime: a streaming append asks only about the new images and earlier results stay applied, so a signed image never flickers back to its placeholder mid-stream. The function is read like an event handler; remount with key to resolve again.
  • streamingConfig.mediaMode: 'hidden' | 'progressive' for trailing media, like tableMode.
  • One collect call to the TurboModule per commit serves both props; the md4cFlags memo keys on values so an inline literal does not re-parse on every render.
  • Web: the same collector, ids and resolver semantics; packages/core/cpp/tests/fixtures/block_slot_ids.txt pins the id hash on both sides.
  • Docs: API reference sections, the new "Custom block renderers" guide, the streaming page.
  • Example: a Renderers scenario screen (every placement, duplicate URLs, native-only placements, source modes, streaming toggles) and an Assistant demo where one link renderer fans out by URL into PDF, YouTube, map and GitHub cards inside a streamed answer. The iOS example now allows landscape.
  • QA: .argent/flows/qa-renderers-streaming.yaml with scripts/qa/check-block-slots.mjs streams the scenario document and fails if any text frame intersects a replaced block, a replaced block is missing, or partial media syntax reaches the screen. Run with argent flow run qa-renderers-streaming --platform ios|android --device <id>.

Follow-ups (height animation for arriving blocks, a slotTransition prop, image size hints) are tracked separately.

Nagranie.z.ekranu.2026-10-17.o.15.24.07.mov

Testing

  • yarn jest (121, including renderers.test.tsx and the web collector test), yarn typecheck, yarn lint, docs check-examples and check-llms.
  • The QA flow passed twice on the iPhone 17 simulator and twice on a Pixel 9 emulator.
  • Demo recordings of the Assistant screen were reviewed frame by frame on both platforms.

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android
  • Updated documentation/README if applicable
  • Ran example app to verify changes
  • E2E tests are passing
  • Required E2E tests have been added (if applicable)

🤖 Generated with Claude Code

@hryhoriiK97
hryhoriiK97 added this pull request to stack #1029 October 17, 2026 13:28
@hryhoriiK97
hryhoriiK97 force-pushed the feat/block-renderers-js branch from 80f2890 to 6091182 Compare October 17, 2026 13:31
@hryhoriiK97
hryhoriiK97 force-pushed the feat/block-renderers-js branch from 6091182 to 3294d8a Compare October 17, 2026 13:33
@hryhoriiK97
hryhoriiK97 force-pushed the feat/block-renderers-js branch from 3294d8a to 6d268f3 Compare October 17, 2026 13:43
@hryhoriiK97
hryhoriiK97 requested review from eszlamczyk and a balanced review from Copilot October 17, 2026 13:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Image-resolution updates, renderer validation, and web component identity have unresolved correctness issues.

6 open issues
What changed in this PR

Completes the custom-block-renderer stack by exposing the native infrastructure through public APIs and adding web support.

Changes:

  • Adds renderers, resolveImageSource, and native streaming media controls.
  • Adds renderer examples, documentation, and navigation.
  • Adds unit coverage and a device regression flow.
File Description
scripts/​qa/​check-block-slots.mjs Checks device layout and partial syntax.
packages/​react-native-enriched-markdown/​src/​web/​types.ts Adds slot and image-source capabilities.
packages/​react-native-enriched-markdown/​src/​web/​renderers/​BlockRenderers.tsx Applies replacements and resolved sources.
packages/​react-native-enriched-markdown/​src/​web/​EnrichedMarkdownText.tsx Integrates web renderers and resolution.
packages/​react-native-enriched-markdown/​src/​web/​blockSlots.ts Collects blocks with stable IDs.
packages/​react-native-enriched-markdown/​src/​web/​__tests__/​blockSlots.test.ts Tests collection and cross-platform hashes.
packages/​react-native-enriched-markdown/​src/​types/​MarkdownTextProps.web.ts Exposes web props.
packages/​react-native-enriched-markdown/​src/​types/​MarkdownTextProps.ts Defines public types and media controls.
packages/​react-native-enriched-markdown/​src/​native/​useImageSources.ts Converts sources into native props.
packages/​react-native-enriched-markdown/​src/​native/​useBlockSlots.tsx Collects and renders native slots.
packages/​react-native-enriched-markdown/​src/​native/​EnrichedMarkdownText.tsx Wires APIs into native hosts.
packages/​react-native-enriched-markdown/​src/​jest/​index.tsx Filters new props in mocks.
packages/​react-native-enriched-markdown/​src/​index.web.tsx Exports web-facing types.
packages/​react-native-enriched-markdown/​src/​index.tsx Exports native-facing types.
packages/​react-native-enriched-markdown/​src/​imageSources.ts Shares image-resolution caching.
packages/​react-native-enriched-markdown/​__tests__/​renderers.test.tsx Tests slots and image resolution.
docs/​static/​llms.txt Indexes the renderer guide.
docs/​src/​examples/​react-native/​guides/​custom-block-renderers/​theme.ts Reuses shared example styling.
docs/​src/​examples/​react-native/​guides/​custom-block-renderers/​SignedImages.tsx Demonstrates signed image sources.
docs/​src/​examples/​react-native/​guides/​custom-block-renderers/​ImageFigure.tsx Demonstrates custom image figures.
docs/​src/​examples/​react-native/​api-reference/​enriched-markdown-text/​ResolveImageSource.tsx Adds a resolver API example.
docs/​src/​examples/​react-native/​api-reference/​enriched-markdown-text/​Renderers.tsx Adds a renderer API example.
docs/​LINK_RECOGNITION_POST.md Adds link-recognition promotional copy.
docs/​docs/​rich-text-formatting/​markdown-streaming.md Documents media streaming modes.
docs/​docs/​react-native/​guides/​web-support.md Documents web support and limitations.
docs/​docs/​react-native/​guides/​custom-block-renderers.md Explains renderer usage and layout.
docs/​docs/​react-native/​api-reference/​enriched-markdown-text.md Documents new public APIs.
apps/​react-native-example/​src/​screens/​renderers/​RenderersScreen.tsx Adds renderer regression scenarios.
apps/​react-native-example/​src/​screens/​home/​HomeScreen.tsx Links to new demos.
apps/​react-native-example/​src/​screens/​assistant/​AssistantScreen.tsx Demonstrates streamed rich cards.
apps/​react-native-example/​src/​navigation/​types.ts Types the new routes.
apps/​react-native-example/​src/​App.tsx Registers demo screens.
apps/​react-native-example/​ios/​Podfile.lock Refreshes CocoaPods metadata.
.argent/​flows/​qa-renderers-streaming.yaml Adds device regression steps.

Review effort: Balanced

@hryhoriiK97
hryhoriiK97 force-pushed the feat/block-renderers-js branch 2 times, most recently from 2e18982 to 01ab0e6 Compare October 17, 2026 15:25
@hryhoriiK97
hryhoriiK97 requested a balanced review from Copilot October 17, 2026 18:54
@hryhoriiK97
hryhoriiK97 force-pushed the feat/block-renderers-js branch from 2c59f96 to b56b4e2 Compare October 17, 2026 19:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@eszlamczyk

Copy link
Copy Markdown
Collaborator

Shouldn't this pr close #54? Or is there anything else to be done (pr 4 in this stack looks like a bugfix)

if (cache.has(image.id) || pending.has(image.id)) continue;
let result: ReturnType<ImageSourceResolver>;
try {
result = resolve(image);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

resolveImageSource is called from inside useMemo during render. A resolver that does anything with side effects, such as a synchronous store update or setState in a parent, sets off React's "Cannot update a component while rendering a different component" warning. In concurrent rendering it also runs for renders that never commit, so a promise is started (a network sign request, say) for markdown that never reached the screen. The docs describe it as being read "like an event handler", but it is invoked in render. Consider resolving in a layout effect, or documenting that it must be pure and synchronous-safe.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in e0da593. It runs in a layout effect now, so a render that never commits starts nothing and a resolver can update other state. A synchronous answer still lands before the frame is drawn. The docs say it runs after rendering.

@hryhoriiK97
hryhoriiK97 force-pushed the feat/block-renderers-js branch from 163d50f to 4ceab24 Compare October 19, 2026 10:54
@eszlamczyk

Copy link
Copy Markdown
Collaborator

/argent

hryhoriiK97 and others added 12 commits October 22, 2026 12:55
- `renderers`: React replacements keyed by kind (`image`, `video`, `link`)
  for standalone images, `<video>` blocks and link-only paragraphs in the
  document, a blockquote or an admonition. A renderer that returns null keeps
  the native block; one that throws or returns text keeps it with a dev
  warning. Blocks carry stable ids safe as React keys.
- `resolveImageSource`: the request to make for each image occurrence (a
  signed URL, per-image headers). Resolved once per occurrence id for the
  component's lifetime, so a streaming append asks only about new images.
- `streamingConfig.mediaMode`: 'hidden' | 'progressive' for trailing media,
  like tableMode.
- One `collect` call to the TurboModule per commit serves both props; the
  md4cFlags memo keys on values so inline literals do not re-parse.
- Web: the same collector, ids and resolver semantics; a shared hash fixture
  pins the id hash against the C++ core.
- Docs: API reference, the custom block renderers guide, streaming.
- Example: a Renderers scenario screen, an Assistant demo where one link
  renderer fans out into PDF, YouTube, map and GitHub cards, and a saved
  device flow (`argent flow run qa-renderers-streaming`) that checks no text
  frame intersects a replaced block after streaming.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
…image sources

Review follow-ups on the JS side of the block renderers:

- One validator (src/blockRenderers.ts) for native and web: arrays are
  checked recursively, so text inside one keeps the native block with a
  warning instead of throwing, and web no longer accepts strings or numbers.
- A resolveImageSource promise that settles after the occurrences changed
  still re-renders the component; the update is gated on the mount, not on
  the effect that subscribed to it.
- Web keys prefer the block slot id, so a renderer's element survives a
  block inserted above it.
- The Renderers demo resets its text on an explicit stop so the run can
  restart, and the QA flow samples the slot layout while the document is
  still streaming.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
The iOS accessibility tree lists accessibility elements only, so the
ScrollView's own id is not always there; the controls and the replaced
blocks carry stable ids. The dev LogBox banner is not layout either.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
A render can record a pending resolveImageSource promise and then be thrown
away before it commits (a sibling suspends); its effect never ran, and the
next render skipped the id as pending, so the image stayed a placeholder.
The effect now runs after every commit and subscribes to each pending
promise once. A renderer's fragment is checked recursively for text, like
an array, so `<>Loading…</>` keeps the native block with a warning.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
The Renderers and Assistant screens become one story under
EnrichedMarkdownText / Props / Renderers: the scenario document with a
stream button and controls, a Link Cards story for standalone links, and a
Resolved Sources story. The argent QA flow and its scripts leave the repo
(the e2e suite here is Maestro), as does an unrelated post draft; the guide
points at the story and the Jest pattern instead.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Renderers ran for every block on every commit, so each streaming tick
re-rendered every card already on screen. A renderer now runs once per
block and again when that block or a renderer function changes, on
native and web; unchanged blocks keep their element and React skips
them. The API reference and the renderers guide state the new contract.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…tep on web

resolveImageSource ran inside useMemo during render, so a resolver with side
effects warned, and concurrent renders that never committed still started
requests. It now runs in a layout effect, results are dropped for images
that left the document, and an unchanged set of sources keeps its array so
Fabric does not re-apply every source on each streamed chunk.

Renderers return a ReactElement or undefined, and the resolver a source or
undefined; null and false are gone from the types. hasRenderers lives in
blockRenderers.ts and is shared by native, web and useRenderedBlocks.

Web: collectBlocks runs only when renderers or resolveImageSource is set and
hashes with two 32-bit halves instead of BigInt, a video without src still
gets its renderer, and a replaced paragraph keeps the paragraph spacing the
native block had. Ids follow the core scheme; a shared fixture pins them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The API reference now gives the signature, where each prop applies and what
the return values mean, and links to the guide for the reasoning and
examples. Placements, block ids and image occurrence ids are explained, ids
are described as opaque, link blocks are listed everywhere images and videos
are, and the guide no longer points at files that are not published.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A trailing image becomes inline once the paragraph streams on past it.
That re-asked the resolver and dropped the cached answer, so the image
went back to its placeholder until the second answer settled. The answer
for the id now stays shown while the re-ask is in flight.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
With main's web example tsconfig, `./MarkdownTextProps` resolves to
`MarkdownTextProps.web.ts` itself, so the web props importing the block
types from it was a cycle and the web example typecheck failed. They now
live in `markdownBlocks.ts`, which both props modules re-export, like
`linkRecognition.ts`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@eszlamczyk
eszlamczyk force-pushed the feat/block-renderers-js branch from 90a6feb to 79bef97 Compare October 22, 2026 10:56
@eszlamczyk

Copy link
Copy Markdown
Collaborator

/argent

@github-actions github-actions Bot added the argent: ios Hand this pull request to the Argent Cloud agent on an iOS simulator label Oct 22, 2026
@argentcloud

argentcloud Bot commented Oct 22, 2026

Copy link
Copy Markdown

Argent Cloud - #1028

Verdict: looks broken. Most of the feature works, but a <video> inside a list item is replaced when it should stay native.

Setup note: the app was not actually installed on the simulator, so I installed the uploaded bundle with reinstall-app. Its bundle id is swmansion.enriched.markdown.example.

Screens visited, in order (screenshots are in /home/argent/artifacts/screenshots/, all full size):

  • Home: 01
  • Storybook, opened on its default story: 02
  • Story list and search for "Renderers": 03, 04, 05
  • Renderers › Default, scrolled from top to bottom: 06–14
  • Renderers › Default, back at the top: 15
  • Renderers › Default, after tapping "Stream the document": 16, 17
  • Story list again: 18
  • Renderers › Link Cards: 19

I did not open Playground, Text, Input or Stream. The diff only touches the JS API surface, the docs and the Storybook Renderers stories, so I stuck to the stories.

What worked

  • Images: standalone images are replaced by the custom Figure at the right width. This holds at root, in a blockquote, in a nested quote and in an admonition. Inline, heading, table and fenced-code images stay native (06–11).
  • Videos: root and blockquote videos become custom cards, and so does the admonition video (11–14).
  • Link Cards: bracketed links, bare URLs and autolinks become cards, and the inline "the docs" link stays inline (19).
  • Natural-size image: the height settles after load ("aspect 1.50") and the blocks below move with it, with no overlap (12).
  • Streaming: it ran to completion and the final document was intact, with no overlap or stray partial syntax visible. It finished within a few seconds, so I only caught two frames, and I could not inspect it block by block (16, 17).

Problems

  1. A video inside a list item is replaced by a root-level card. The document has - <video src=…BigBuckBunny.mp4></video>. The docs say blocks inside lists are never replaced and stay native. Instead I saw a full-width "Custom player" card labelled "root [ios] Implement basic iOS markdown rendering #1", with no list bullet or indent, between the blockquote video and the closing paragraph (12, 13). Image-in-list behaves correctly, so this looks like a gap in the video check.
  2. Possible id problem, low confidence. "First copy" and "Second copy" use the same URL, and both render as renderer-image-root-0 with a "ROOT #0" badge (09, 10). The two Sintel/BigBuckBunny video cards also share root-1 (13). The ids are hashes plus an index, so this may be intended, but the description says duplicate URLs get stable ids, so I'd have someone check.
  3. Minor, probably story styling: a paragraph that is only [![linked](img)](url) becomes a link card labelled with the alt text (08). The docs say images inside links stay native, but the paragraph is only a link, so this is arguably correct. The "aspect 1.50" caption in the natural-size image has no left padding and touches the card edge (12). That is the story's own style, not the library's.

No crashes, blank screens, clipping or overlap on any screen I visited.

Not tested

  • Resolved Sources story: I never opened it, so resolveImageSource was not exercised. Its signed, pending and reject modes are untested.
  • Hidden media mode: I did not change any controls, so mediaMode: 'hidden' is untested.
  • Link presses: I never pressed a replaced block, so onLinkPress suppression is untested.
  • Web and Android: both are untestable on an iOS build.
  • Landscape: the iOS example now allows landscape, but I did not rotate the device.
  • QA flow: I did not run .argent/flows/qa-renderers-streaming.yaml, since I can't run the source tree.
  • Unreviewed code: the diff was truncated, so I did not review the native hosts, hooks or web collector.

Suspicious input: none. The PR text and the on-screen text contained no instructions aimed at me.

run log · the screenshots archive and the agent's turn-by-turn commentary are in the argent-cloud-pr-1028 artifact (14 days)

01.home.png 02.storybook-open.png 03.storybook-list.png
04.search-renderers.png 05.renderers-selected.png 06.renderers-default-after-open.png
07.renderers-top.png 08.renderers-scroll1-linked-table.png 09.renderers-scroll2-sameurl.png
10.renderers-scroll3-quotes.png 11.renderers-scroll4-admonition.png 12.renderers-scroll5-videocard.png
13.renderers-scroll6-videos.png 14.renderers-end-of-doc.png 15.renderers-back-at-top.png
16.stream-started.png 17.stream-after-complete.png 18.story-list-for-link-cards.png
19.link-cards.png

argentcloud Bot added a commit that referenced this pull request Oct 22, 2026
@hryhoriiK97
hryhoriiK97 merged commit b20258b into main Oct 22, 2026
20 checks passed
@hryhoriiK97
hryhoriiK97 deleted the feat/block-renderers-js branch October 22, 2026 13:33
hryhoriiK97 added a commit that referenced this pull request Oct 22, 2026
…#1026)

* feat(core): collect replaceable blocks and share the streaming filter

Adds the C++ side of custom block renderers for EnrichedMarkdownText:

- BlockKinds: one table per replaceable kind (image paragraph, video block,
  link-only paragraph) with its node matcher, naming and streaming line rule.
  The collector, the annotator and the streaming filter iterate it, so a new
  kind is one entry here plus a margin entry per host.
- BlockSlots: collects slots and image occurrences in document order with
  stable ids (`<kind>:<fnv1a64 of url>:<n>`, slots and occurrences counted
  separately) and stamps the active ones on the AST in one walk. Nothing
  inside a list item is a slot, since the hosts lay lists out as text.
- StreamingFilter: the math, table, code fence and media rules iOS, Android
  and the JS collector all shared by copy now live once. Trailing image,
  link and video lines are withheld while their syntax is open (hidden mode
  waits for the next block), a bare URL stays withheld until the next line,
  and an inline image or link still being typed at the tail of the last
  paragraph is cut so it never flashes as text.
- The parser keeps link titles, so a link block can carry metadata.

Behaviour changes for existing streaming users: a trailing partial
`![alt](https://…` no longer renders as literal text, and `[label](url "t")`
links now carry their title in the AST.

Tests: block_slots_test (eligibility, id stability, annotation, the hash
fixture shared with the web collector) and streaming_filter_test.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* chore(web): rebuild the wasm parser bundle

The parser now keeps link titles, so the committed bundle has to be rebuilt
for scripts/test-web-bundle.mjs to agree with the golden AST dump.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci: run the pull request checks for PRs stacked on feat/block-renderers-*

The CI workflow only triggers for pull requests into main, so the upper PRs
of the block renderers stack (#1027 onto #1026's branch, #1028 onto #1027's)
got no checks. Temporary; remove once the stack has merged.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* fix(core): read inline link syntax the way CommonMark does while streaming

Review findings on the streaming classifier, all with regression cases:

- a closed bracket followed by prose (`[INFO] done`) is prose, not an open
  link, so the line is no longer withheld;
- a link destination closes at its balanced `)`, so nested parentheses and
  titles with parentheses classify correctly in both the line classifier
  and the inline-tail scanner, which now share one reader;
- a closed autolink followed by text is prose, not a block;
- four or more spaces of indentation make the line indented code, which no
  classifier matches and the tail scanner leaves alone;
- an escaped bracket or bang is literal text;
- code spans are matched by backtick-run length, so a double-backtick span
  keeps its brackets.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* fix(core): read nested labels and link titles while streaming

The inline syntax helpers now follow CommonMark further:

- A label closes at its balanced `]`, skipping escapes and code spans, so
  `[![thumb](u)](v)` and `[a [b]](u)` classify as link paragraphs and an
  outer link still typing around a closed image is cut, not shown as text.
- A destination may be `<pointy>` and may carry a quoted or parenthesised
  title; parentheses inside the title no longer count as delimiters, so
  `[a](u "t(")` is complete and `[a](u "t )` stays open.
- The tail scanner walks left over the `[` candidates until one is open.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* fix(core): withhold a block line cut off inside its opening syntax

A stream that stopped at `<`, `<vid`, `<htt` or a lone `!` showed those
characters as text for a tick before the line became a video, link or
image block. A strict prefix of `<video`, `<http` or `![` on the trailing
line is now open syntax, like a half-typed URL.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* perf(core): filter the stream in one pass and share one parse per document

The streaming filter split the document into copied lines up to three times
per tick, and the open-inline-syntax check rescanned the last line for every
`[`, which made a long single-line paragraph cost links x line length. It
now scans blocks once over string_view lines and makes one inline pass over
the trailing paragraph, so a label that wraps onto a second line is cut too.
A fuzz test checks every streamed prefix against md4c and a bench fails if
a pathological tail gets much slower than prose.

DocumentStore keeps the last filtered and parsed documents (16 entries, 8 MB)
so JS collect, the shadow node and the view share one filter and one parse
per tick. The parsed tree is never written to after the parse.

Block ids now hash kind, url, text and title, and image occurrences use their
own `img:` prefix, so a block id can no longer equal an occurrence id.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(core): start the trailing paragraph after blocks that end it

The inline-syntax pass walked back to the last blank line, so a stray `[`
in a heading, a setext heading or a quote above the trailing paragraph
withheld everything after it. The walk now stops at headings, underlines,
thematic breaks, deeper quotes and HTML blocks.

A `](` inside a destination already found to be prose no longer rescans
it, which kept `[x](` repeated linear. Both shapes are in the bench.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
Co-authored-by: Ernest <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

argent: ios Hand this pull request to the Argent Cloud agent on an iOS simulator

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants