Conversation
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); |
There was a problem hiding this comment.
[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(); |
There was a problem hiding this comment.
[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.
| var lang = book.Language1Tag; | ||
| if (string.IsNullOrEmpty(lang)) | ||
| return; | ||
|
|
There was a problem hiding this comment.
[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.
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>
|
[Claude Opus 5 from Hatton's machine during preflight] Consulted Devin on 2026-09-12 up to commit Three rounds, one per commit. Five distinct findings, each mirrored above as its own review thread:
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 |
…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>
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.
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.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.
(
FlowTextWalk,FlowTextChains), which lays each page out off-screen to measure it withthe page's own styles.
rather than starting one. A panel beside the page offers "Reflow now", and a book setting
runs the queue on each page change.
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.
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