Skip to content

Flow text: one run of text flows through a chain of linked boxes, across pages - #8354

Draft
hatton wants to merge 17 commits into
masterfrom
flowText
Draft

hatton wants to merge 17 commits into
masterfrom
flowText

Conversation

@hatton

@hatton hatton commented Sep 12, 2026

Copy link
Copy Markdown
Member

Problem

Bloom has no way for a run of text to continue from one text box into the next. A box that
holds more than it can show marks itself as overflowing, and the author has to cut the tail
and paste it into another box, or onto another page, by hand. Any later change to the font,
the style, the paper size or the page layout breaks the whole arrangement, and the author
does it all again.

Fix

This PR adds "flow text": a chain of linked text boxes that one run of text flows through,
on one page and across pages.

  • Linking and unlinking. An empty box below (or on a later page than) an overflowing box
    offers to continue it; the text context menu can unlink a box again. A chain is recorded on
    the translation group with data-flow-chain.
  • Live flow while typing. The browser measures the page being edited, marks the character
    where a box stops fitting, and hands the tail to the next box. The caret and the keys typed
    during the hand-off follow the text to its new box, across a page boundary if that is where
    it went.
  • Off-screen refit. Boxes on pages the browser cannot see are refitted in C#
    (FlowTextWalk, FlowTextChains), which lays each page out off-screen to measure it with
    the page's own styles.
  • Refit is queued, not automatic. A style, paper-size or layout change queues a refit
    rather than starting one. A panel beside the page offers "Reflow now", and a book setting
    runs the queue on each page change.
  • Pages added and removed. With "Automatically add & remove pages" on, a refit appends
    text-only pages while text remains, and removes a page of the chain that the run leaves
    holding nothing. The chain's first page and the page being edited are never removed.
  • Output is clean. The overflow marker is stripped from PDF, ePUB and BloomPUB, and the
    reader tools ignore it.

Both long jobs report through the Edit tab's progress dialog. Covered by Vitest specs for each
front-end module, NUnit tests for the C# walk and chain code, and Playwright end-to-end specs
that drive the whole feature in a running Bloom.

Devin review


This change is Reviewable

hatton and others added 9 commits September 8, 2026 16:33
Fast-forward the flowText branch to master and add @chenglou/pretext 0.0.8
(0.0.9 is younger than the repo's minimumReleaseAge). Port the March
prototype into bookEdit/flowText/ as small modules with vitest specs:
chain discovery from data-flow-chain on the translation group, linearize
and move DOM fragments between boxes, a LineMeasurer interface with a
Pretext implementation, real-layout verification with one-word nudges,
caret capture and restore, refusal rules, indicators, timing, and a
MutationObserver trigger coalesced to one pass per frame. Hook the trigger
into SetupElements and removeEditingDebris, suppress the overflow warning
on a chained box that has a next box, reflow after style changes, and set
text-wrap: wrap on chained boxes so the measurer and the browser break
lines the same way.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The overflow checker now places an inline span, bloom-overflowStart, at
the character where a normal-style box stops fitting, keeps it current
while the user types, and removes it when the box fits. It finds the
point with the Pretext proposal and confirms it against the real layout
with a doubling step and a binary search. The marker is saved with the
page and stripped from PDF, ePUB and BloomPUB output; the reader tools
skip it. InsertPageAfter strips chain attributes and markers from copies.

An empty normal-style box below an overflowing box on the same page
shows a "Continue text from the box above" button; clicking links the
two boxes and the tail flows in. The text context menu gains "Unlink
text box". Continuation paragraphs are styled from their attribute
instead of bloom-noIndent. Backspace at the start of a linked box with
a previous box on the page deletes into that box.

Adds e2e hooks, helpers and same-page specs (not yet run), and fixes a
split of the first typed character caused by cloning an empty box's
placeholder paragraph.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ng it

A chain of linked text boxes now spans pages. The browser measures only the page
it is editing; the boxes on other pages live in the book DOM in C#, reached
through the new FlowTextApi (pendingOverflow, continueInto, peekNext,
setNextContent, unlinkFrom, previous, pendingCaret). FlowTextChains holds the
chain walking and the paragraph split and merge on the C# side.

flowCrossPage settles the last chained box of a page against the next box of its
chain: a push takes the text after the overflow marker, snapped to a word start;
a pull-back is measured against the real layout. When the caret is in text that
moves, the caret goes with it: the keys typed while C# is still taking the text
are carried, posted as a pending caret, and put in when the next page has
loaded. A refusal from C# gives the typed text back to the box it left.

Labels and offers: an empty box on a later page offers to continue an
overflowing box on an earlier page; a continuation box shows where its text
comes from; a class change on an unchained box brings its offers up to date.
Unlinking clears the continuation and seam-space attributes.

E2E: flow-text-happy-path, flow-text-cross-page, flow-text-same-page and
flow-text-chain-management cover the flows end to end in Bloom.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Off-screen refit across pages (FlowTextWalk) and a "Create pages and
continue flow" button that adds pages until the run fits, then jumps the
Edit tab to the last page made. Both jobs show the Edit tab's reusable
EmbeddedProgressDialog (mounted once in App.tsx as "editView") through a
new BrowserProgressDialog.DoWorkWithDeterminateProgressDialogAsync; the
unlocalized externalProcessing overlay is gone. The dialog's ready wait
is bounded so work runs even when no document hosts the dialog.

Also: continuation paragraphs no longer take an indented style's
text-indent at the top of a page (the rule is !important because the
Format dialog writes its indent !important); "flows from page N" label
wording; the create-pages button sits below its box so it covers no
text; a page loaded only to be measured no longer runs its own flow
pass, which was losing text; e2e specs for whole-chain, create-pages,
delete-page and thumbnails, with assertions that the progress dialog
opens and closes.

New XLF units are translate="no" until the branch merges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…g edited

Reflow never starts by itself. Every request for a whole-chain walk (a style,
paper-size or layout change, typing that crossed a page boundary, a page delete)
only queues it in FlowTextWalk. The queue runs when the user changes pages, if the
book setting "Reflow when you change pages" is on (book.userPrefs,
flowTextReflowOnPageChange, stored only when off), or when the user clicks
"Reflow now". While a walk waits for a chain with a box on the page being edited,
that page shows a panel beside it with "Reflow pending", the checkbox and the
button; there is no panel when nothing is pending. The browser learns the state
from GET flowText/pendingWalks and the flowText websocket events walkQueued and
walkFinished.

A size, layout or style change queues a whole-flow walk for every chain,
including one with a box on the page being edited, so the affordance appears on
the last page too. A walk covers the chain from its start page to its end: text
moves back into earlier pages and the page being edited can end up empty.
"Reflow now" saves the page being edited first (EditingModel.SaveThen), and once
the walk has run C# hands the browser the new content of any box on that page
(GET flowText/refitResult?pageId=), because the state machine refuses to
navigate while editing. The browser writes it in and settles; a boundary pass
that moves no character reports no move, so nothing stays pending.

Also: setFitRangeEnd no longer depends on how a text node is split; the
progress dialog names the kind of walk under its bar; e2e helpers and specs for
the pending state, and a whole-chain test that changes the paper size from the
last page of the chain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
"Create pages and flow text" now turns on a book setting, flowTextAutoPages
(book.userPrefs, default off, written only when on). While it is on, a
pending reflow that reaches the last box of a chain with text left adds
text-only pages after the chain, one at a time, up to the existing cap; and
after the run is placed it removes a page of the chain that the run leaves
holding nothing at all. The first page of a chain and the page being edited
are never removed. The reflow panel shows the setting as a second checkbox,
"Automatically add & remove pages", with GET/POST flowText/autoPages behind
it; pendingWalks reports it too.

FlowTextWalk.Distribute takes the page maker and the cap; FlowTextApi hands
it AddTextOnlyPageAfter (cached Just Text template, cleared on book change).
FlowTextChains.PageHoldsNothingBut decides emptiness. Removal goes through
EditingModel.RemovePageFromBook on the UI thread, then the book is saved.

Two fixes found on the way. The style sheet the walk applies off-screen is
now serialized from the CSSOM, because StyleEditor edits the rules in place
and the style element's text is stale, so a walk after a font change was
measuring at the old size. And the off-screen fit stamps a request id on the
page body and waits for it: the browser answers a poll from the previous
page's document until the new one takes over, so the fit request could be
sent to the wrong page and the walk timed out.

E2E: flow-text-auto-pages.spec.ts builds two chains, grows the first with a
bigger font (pages added between the chains) and shrinks it with a larger
paper size (pages removed), with the second chain's pages and text intact
throughout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…it emptied

A style change alters where the text breaks on every page of the book, not just
the page being edited, so the refit now runs over every chain from its first
page, carrying the page's own style rules so Bloom measures the off-screen pages
the way the author sees this one.

When a refit leaves the page being edited holding nothing but an empty box of a
chain, the browser asks Bloom to take that page away, via the new
flowText/deleteEmptiedPage endpoint. The browser makes the request rather than
the refit deleting the page itself, because deleting saves the page: what the
refit made of the page's boxes has to be in them first, and only the browser
knows when it is. Bloom re-checks that the page may still go, so a request that
arrives after the author has typed does nothing.

Also: OverflowChecker.UpdatePageOverflow is public, so code that changes a box
and has the page saved at once brings the page-level overflow class up to date
before that save instead of on a deferred timer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	PAPERCUTS.md
#	src/BloomE2E/AUTOMATION-DEBT.md
A walk waiting in the queue names its chain and its page by id and nothing
else, and a copy of a book keeps those ids. So a reflow queued in one book
could match, and rewrite, the pages of another book opened before the queue
ran, while the book it was meant for lost its request. FlowTextApi now empties
the queue when the book selection changes, alongside the refit results and the
pending caret it already forgot there.

Also mark the new "Unlink text box" string translate="no", like the rest of the
flow-text strings on this branch: it is not ready for Crowdin while the branch
is unmerged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
{
// The box holds the whole of what is left while the browser measures it:
// the fit is a question about this box's layout with this text in it.
HtmlDom.SetInnerHtmlFromFragment(editable, remaining);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Devin] Bug: Failed refits leave duplicated text

Devin flagged this as severe.

A refit rewrites the real book as it goes. Before it measures a box, it puts the whole of the text still to be placed into that box; only after the measurement comes back does it cut the box down to the part that fits. If the measurement throws or times out, the code that cuts it down is never reached, so this box keeps the whole run while the boxes after it still hold their own copies of the later parts. RunQueue catches the exception and clears the queue, but nothing puts the book back.

What an author would see: a chain holds "one" on page 1 and "two three" on page 2. A refit starts, puts "one two three" into page 1, and the off-screen measurement fails. Page 2 still says "two three". The next save can write both copies to the book, so the text is now duplicated.

Devin's suggested direction is to make a walk transactional: snapshot every affected box and group style before distributing, track the pages added, and on any failure restore the content and styles and remove the added pages, only exposing and saving the result once the whole distribution verifies.

Mirrored from Devin's review by Claude Opus 5 from Hatton's machine during preflight.

while (remaining == null || HoldsAnyText(scratch, remaining))
{
var isLast = result.PagesCreated + 1 >= maxPages;
var group = addPage();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Devin] Bug: Failed page creation duplicates text

Devin flagged this as severe.

"Create pages and flow text" changes the book before it knows the work will succeed. It inserts a real page, moves the tail of the source box onto it, and only then asks the browser to lay the new page out. A timeout, a missing result, or a failed verification leaves the run without saving the created pages, but nothing removes the pages already inserted and nothing puts the source box back. The editor still holds the original source text, because C# only hands back the new source when the whole thing succeeds.

What an author would see: the box holds "A B C" with the overflow mark before "B". Bloom inserts a page and moves "B C" onto it, then the layout times out. The editor still shows "A B C", and a later save writes that back, while the inserted page can keep "B C". The text is now in the book twice, alongside a half-finished page.

Devin's suggested direction is to treat page creation as a transaction: snapshot the source group and the page position first, and on any failure restore the source, remove every inserted page, restore the original chain attribute and refresh the page list, committing and saving only after every fit and the final text-integrity check pass.

Mirrored from Devin's review by Claude Opus 5 from Hatton's machine during preflight.

Comment thread src/BloomExe/web/controllers/FlowTextApi.cs
var lang = book.Language1Tag;
if (string.IsNullOrEmpty(lang))
return;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Devin] Investigate: Whole-book reflow skips secondary languages

When a style, paper-size or layout change asks for every chain in the book to be refitted, the request carries only the book's first language. On a bilingual page each content language has its own boxes inside the translation group, and its text flows through its own chain of them. So after such a change the first language's boxes are re-divided and the other languages' boxes are left divided the way they were, at the old size.

Worth confirming against a bilingual book before this ships. The branch already marks the bilingual same-page case as not yet covered (test.fixme in flow-text-same-page.spec.ts), and src/BloomE2E/AUTOMATION-DEBT.md records that no helper yet reports which languages a page is showing, so an end-to-end test of this is blocked on that helper.

Mirrored from Devin's review by Claude Opus 5 from Hatton's machine during preflight.

Comment thread DistFiles/localization/en/BloomMediumPriority.xlf
Emptying the walk queue whenever the book selection changes was too broad.
SelectionChanged is also how Bloom announces that the selected book has been
reloaded in place, which a spreadsheet import and a Team Collection update both
do, and a reload does not invalidate a queued refit: the queue is the only
record that the pages outside the editor still need refitting. The queue is now
emptied only when the selected book is a different book from the one it was
emptied for last.

The caret and the refit results are still forgotten on every such event,
including a reload, because both name a page of a document that no longer
exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/BloomExe/web/controllers/FlowTextApi.cs
@hatton

hatton commented Sep 12, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 5 from Hatton's machine during preflight] Consulted Devin on 2026-09-12 up to commit ae85089e0d.

Three rounds, one per commit. Five distinct findings, each mirrored above as its own review thread:

  • Fixed and resolved: a queued reflow could be run against a different book (a5bdd0ecec); the follow-on that a reload of the same book should keep its queue (ae85089e0d); the new "Unlink text box" string missing translate="no" (a5bdd0ecec).
  • Open, waiting on a decision: a failed refit, and a failed page creation, leave the book holding the text twice, because neither operation rolls back what it changed before the off-screen measurement failed.
  • Open, waiting on a decision: a whole-book reflow carries only the book's first language, so a bilingual page's other languages keep their old division.

The third round raised nothing new; it re-listed the earlier findings because it re-reads the whole diff, and each of those repeats was checked against the code at this commit and is stale.

CI here is only pr-automation, which passed. CodeRabbit does not review this repo (auto_review.enabled: false in .coderabbit.yml). Both unit suites are green at this commit: 1081 front-end tests, 3542 C# tests.

hatton and others added 7 commits September 12, 2026 15:16
…ry language

Two findings from the review of this branch, both in the C# side of the refit.

A refit that fails part way through left the author's text in the book twice.
The refit writes the book as it goes: each box takes the whole of the text
still to be placed while the browser measures it, and is cut down to its own
share only when the answer comes back. If the answer never came, that box was
left holding text the boxes after it still held as well, and the next save
would have written both copies. Making pages for the rest of a box's text had
the same hole: the page was inserted and the tail moved onto it before the page
had been laid out, and a failed layout left the page behind while the editor
still held the whole text.

FlowTextWalk.ChainSnapshot now remembers what a box held, and the style and
chain its group carried, before the first write to it, and notes each page the
run makes. Distribute and FlowTextCreatePages.Run restore all of that and take
the pages away again if anything between the first write and the final check
that no word was lost or repeated throws. Taking a page away is the caller's to
do, as adding one already was, so both take a removePage alongside their
addPage: the walk builds one from EditingModel.RemovePageFromBook on the UI
thread, and FlowTextApi does the same for the create-pages route. Nothing is
saved on that path, because the save that would have written the work is the
one that never happened.

Second, a refit of the whole book carried only the book's first language. On a
bilingual page each content language has its own box in the translation group
and its own run of text flowing through its own boxes, so a change of style or
paper size alters where the text breaks for each of them; refitting only the
first left the others divided as they were, at a size the page no longer shows.
RequestEveryChain now queues a walk per content language (Book.ActiveLanguages),
skipping any language a chain has no box of. The queue was already keyed by
chain and language, so nothing else had to change.

That also settles a third finding, that the inline font size a refit copies back
onto the translation group -- which is all a page-list thumbnail has to draw the
text at -- is not copied for a box the run no longer reaches, because only the
path that measures a box writes it. The case where that shows is a group whose
other language still holds text, and that language now gets its own walk, whose
own fit writes the size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each spec wrote its own collectionSpec object literal, and Playwright decides
whether the next file can keep a worker -- and so the Bloom that worker's
fixture launched -- from a hash built over the identity of every worker-scoped
test.use value, not its content. Two files passing two deep-equal literals
therefore got two workers, and each launch costs the better part of a minute.
(A string is compared by value, which is why two files naming the same
collectionName already shared a Bloom.)

The ten specs now import one object, kFlowTextCollection, whose comment carries
the reason and the reason sharing is safe: every one of these files makes its
own book first and asserts only about the selected book; the two settings they
change are the book's own; and the queue of waiting refits, which is Bloom's
rather than the book's, is emptied whenever a different book is selected, so a
file that deliberately ends with a refit still waiting cannot leave it to run
against the next file's book. Books are left in the collection rather than
deleted, because deleting one opens a WinForms confirmation dialog that would
hang the run.

No spec was merged and no case removed: 51 flow-text tests in ten files before
and after, one Bloom launch instead of ten. Also corrects a comment in
bloomTest.ts that called the page fixture worker-scoped when it is test-scoped,
which is what makes the deliberate restart in flow-text-happy-path safe for the
files that follow it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flow text is now a FeatureRegistry entry: FeatureName.FlowText at
SubscriptionTier.Pro, which is the lowest paid tier and so means "any
subscription", with ExperimentalFeatureToken set to the new "flow-text" token.
One entry covers both halves, because FeatureStatus already turns a registry
entry into an "enabled" flag for the subscription and a "visible" flag for the
experimental token.

Every flowText/ endpoint goes through a gate that refuses the request unless the
feature is both enabled and visible, so the browser is not trusted to enforce
this. The api takes CollectionSettings and passes the selected book, so a
Playground book unlocks flow text as it unlocks other features.

In the browser, flowTextAvailable asks features/status once per page and caches
the answer, and setupFlowText waits for it before any pass runs, so no text
moves before Bloom has ruled. The continue button, the create-pages button, the
reflow panel and the Unlink menu item each do nothing when the feature is off,
and so does the suppression of Bloom's ordinary overflow warning -- without
that, a book that already held chains would go on hiding the red warning while
nothing was left to move its text.

A book that already has chains keeps every word when the feature is off. The
chain attributes stay, the boxes hold what they held, and the text simply stops
re-flowing; turning the feature back on picks the book up where it was.

The checkbox is in the Experimental Features group of the Advanced tab, built
the way Team Collections is: bound to a pending value applied on OK, disabled
when the subscription does not allow it, with the subscription indicator beside
it. No restart is needed, because the edit view asks for the feature status as
each page loads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The questions are how many people use flow text, how many distinct books they
use it on, and over what range of pages a run of text is spread. One event
answers all three: "Flow Text", carrying the book's id, which makes the books
countable, and the number of pages spanned by the longest run in that book,
which is the figure the range is read from. Nothing else, because nothing else
answers one of the three: the person is already Segment's own identity, and the
country, branding and collection languages are already application properties on
every event.

The event is sent only when a book's longest run reaches a length not yet
reported for that book in this Bloom run. Typing sends nothing, and a refit that
merely changes where the lines break sends nothing; a run growing from two pages
to nine sends a handful of events over a session rather than one per keystroke.

Reporting growth rather than waiting for the author to be finished with a book
is deliberate: no moment reliably means finished. The author can close Bloom,
change collections, or crash, and the edit view has already lost the outgoing
book by the time its selection handler runs. Reporting each new maximum means
the longest run a book ever reached is in the data whatever happens next.

Two call sites. Every Edit tab page save passes through EditingModel's saveBook,
which costs nothing on a page holding no box of a chain. And the end of a refit
in FlowTextWalk.RunQueue, after the browser has been told the chain is free, and
never on the failure path, because a refit that threw has put the book back as
it found it.

A chain of two boxes on one page counts as one page, which is real use of the
feature and worth telling apart from no use. A lone box carrying a chain
attribute counts for nothing: it has nowhere to send its text, the same rule
QueueEveryChain applies when it decides what is worth refitting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… bilingual

Ten Bloom launches become one. Each spec wrote its own collectionSpec object
literal, and Playwright decides whether the next file keeps a worker -- and so
the Bloom that worker's fixture launched -- from a hash built over the identity
of every worker-scoped test.use value, not its content. Two files passing two
deep-equal literals got two workers. The ten specs now import one object,
kFlowTextCollection, whose comment carries the reason and why sharing is safe.

Sharing exposed a bug in makeBookFromTemplate: it selected the factory template
while Bloom was still in the Edit tab, and WorkspaceView.ChangeTab returns at
once when that tab is already active, so nothing rebuilt the view and it sat
empty until the wait expired. Only a file inheriting a Bloom left in the Edit
tab hit it. The sibling helper makeBookFromBookInCollection already carried the
guard and the comment explaining it.

Tests flow three pages, not twenty. One Just Text page holds about 1200
characters, now the unit every text size is written in. Two tests keep more and
say why where their text is defined: one draws at A4, where a three-page run
leaves the last page empty, and the thumbnails test needs the run to overflow or
there is no warning triangle to find.

A run of text goes in as paragraphs now, so splitting and rejoining a paragraph
at a box boundary is exercised everywhere rather than in one spec.
flow-text-paragraphs covers a paragraph cut at a box boundary and marked, the
halves rejoining when the text is drawn smaller, a cut falling exactly between
paragraphs, and a cut at a page boundary.

flow-text-bilingual flows the first language until it makes its pages, turns the
second language on, puts in a run half as long again, and lets it flow through
those pages and add more. Each language's run reads back whole and neither
language's words appear in the other's boxes. getPageLanguages is written, which
is what the automation-debt entry said was blocking this, so that entry is gone,
and the same-page bilingual test is no longer a fixme.

flow-text-torture flows a book-sized run and is excluded unless BLOOM_E2E_TORTURE
is set. It asserts that doubling the pages does not much more than double the
time, against a bound a quadratic regression would breach, and that Bloom holds
no more memory per page afterwards. It says in its own comments that the reading
is the Bloom process's working set, so it cannot see the WebView2 processes or a
leak below the garbage collector's noise.

Every spec now asks for the flow-text experimental feature and a paid
subscription, because the feature needs both. Sample-Pro branding rather than
the Enterprise test code: testing a Pro-tier feature at Enterprise would pass
whether or not the gate names the right tier.

59 tests in twelve files, one Bloom launch plus the restart flow-text-happy-path
performs on purpose, 13.6 minutes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ropped

Asking Bloom for the pending caret is what takes it: Bloom holds one and hands
it over once. placePendingCaret then gave up without putting it back on three
paths -- the page gone, no box of the caret's language on the page, and the box
gone after the wait for CKEditor. On each of those the caret was taken and
dropped, so the author's caret silently failed to follow the text it was in,
with nothing to say so and nothing left to try again with.

Every way out now either places the caret or hands it back, so a later attempt,
such as the page finishing its load or a refit settling, can place it.

The box is also looked for again after the wait rather than reused. That wait
runs for as long as two seconds, which is time enough for a refit landing on
this page to replace the box the caret was going to be written into.

Found by reading the code while chasing an e2e observation that a run typed
paragraph by paragraph into a chained box does not reliably reach the box being
typed in. That observation is not reproducible and is not explained by this;
what is fixed here is wrong on its own terms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A caret Bloom hands over and the browser cannot place is now named in the
console, with the reason: the page gone, no box of that language in that chain,
or the box replaced while we waited for its editor. The author sees nothing when
this happens -- their caret simply does not arrive, and they carry on typing
wherever the new page put the focus -- so a silent give-back is indistinguishable
from a caret that was never sent, which is what made this hard to chase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.

1 participant