Add Bionify reading mode to supported reading surfaces#854
Add Bionify reading mode to supported reading surfaces#854jeffscottward merged 25 commits intoRunMaestro:mainfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a persisted Changes
Sequence Diagram(s)sequenceDiagram
participant UI as User (UI)
participant Settings as Settings Store
participant IPC as Main IPC
participant WebServer as WebServer
participant Broadcast as Broadcast Service
participant WebClient as Web App (WebSocket)
participant Renderer as Renderer Component
UI->>Settings: setBionifyReadingMode(true)
Settings->>IPC: settings:set('bionifyReadingMode', true)
IPC->>WebServer: getWebClientCount()
alt clients > 0
IPC->>WebServer: broadcastBionifyReadingModeChange(true)
WebServer->>Broadcast: broadcastBionifyReadingModeChange(true)
Broadcast->>WebClient: {type:'bionify_reading_mode', enabled:true, timestamp:Date.now()}
end
WebClient->>Renderer: onBionifyReadingModeUpdate(true)
Renderer->>Renderer: re-render prose with Bionify transforms
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR ships the Bionify reading-mode feature — a global toggle that boldfacews the first few characters of each prose word to guide eye movement. It adds the setting, a new
Confidence Score: 4/5Safe to merge after addressing the search-breaking interaction between bionify word-splitting and the CSS Custom Highlight API in FilePreview. One P1 defect: enabling Bionify then searching in FilePreview produces wrong/zero results because the CSS Custom Highlight API matches against individual text nodes, which bionify splits across two siblings per word. The remaining P2 findings do not block functionality. src/renderer/components/FilePreview.tsx (search x bionify interaction) and src/renderer/utils/bionifyReadingMode.tsx (BionifyTextBlock style injection) Important Files Changed
|
| export const BionifyTextBlock = forwardRef<HTMLDivElement, BionifyTextBlockProps>( | ||
| function BionifyTextBlock( | ||
| { children, enabled, className = '', style, ...props }, | ||
| ref: ForwardedRef<HTMLDivElement> | ||
| ) { | ||
| const blockClassName = ['bionify-text-block', className].filter(Boolean).join(' '); | ||
|
|
||
| return ( | ||
| <div ref={ref} className={blockClassName} style={style} {...props}> | ||
| <style>{getBionifyReadingModeStyles()}</style> | ||
| <BionifyText enabled={enabled}>{children}</BionifyText> | ||
| </div> | ||
| ); | ||
| } | ||
| ); |
There was a problem hiding this comment.
Theme-unaware opacity and duplicate
<style> injections
getBionifyReadingModeStyles() is called here with no theme argument, so restOpacity is hardcoded to 0.96 (the dark-mode value). In light mode the intended value is 0.90. Every other call site (generateProseStyles, generateInlineWizardPreviewProseStyles) correctly forwards the theme.
Additionally, every BionifyTextBlock instance adds its own identical <style> element to the DOM — WebReadingContent can render several text segments, each producing a duplicate. Styles are also injected when enabled={false}.
Consider accepting an optional theme prop and hoisting the style injection to a single shared location (a CSS file, a single <style> in the app root, or a global style sheet).
| const READABLE_TEXT_EXTENSIONS = new Set(['txt', 'text', 'rst', 'adoc', 'asc', 'mdx']); | ||
| const READABLE_TEXT_BASENAMES = new Set([ | ||
| 'readme', | ||
| 'changelog', | ||
| 'contributing', | ||
| 'license', | ||
| 'copying', | ||
| 'authors', | ||
| 'notice', | ||
| 'todo', | ||
| ]); | ||
|
|
||
| function isReadableTextPreview(filename: string): boolean { | ||
| const lowerFilename = filename.toLowerCase(); | ||
| const ext = lowerFilename.includes('.') ? lowerFilename.split('.').pop() : ''; | ||
| if (ext && READABLE_TEXT_EXTENSIONS.has(ext)) { | ||
| return true; | ||
| } | ||
|
|
||
| const basename = lowerFilename.includes('.') | ||
| ? lowerFilename.slice(0, lowerFilename.indexOf('.')) | ||
| : lowerFilename; | ||
| return READABLE_TEXT_BASENAMES.has(basename); | ||
| } |
There was a problem hiding this comment.
.mdx treated as plain pre-wrapped text rather than rendered markdown
READABLE_TEXT_EXTENSIONS includes 'mdx', but getLanguageFromFilename has no entry for mdx, so it returns 'text'. isMarkdown is therefore false and isReadableTextPreview('file.mdx') returns true, routing .mdx files through BionifyTextBlock with whitespace-pre-wrap rather than the markdown renderer.
MDX is a markdown superset and users typically expect its prose and headings to be rendered. Adding mdx: 'markdown' to getLanguageFromFilename would keep it on the established markdown rendering path.
|
Thanks for putting this together, @jeffscottward — the surface coverage and the screenshot evidence are super helpful, and the v1 exclusions feel right. One blocker before approval, flagged by Greptile and worth confirming manually: Bionify breaks text search in FilePreview (P1). When Bionify is enabled, each word becomes two sibling text nodes ( Simplest fix is to short-circuit the bionify transform when search is active: const withReadableTransforms = (children: React.ReactNode) =>
applyReadableTextTransforms(children, {
theme,
enableBionifyReadingMode: enableBionifyReadingMode && !activeSearch,
});Alternatively, walk the Two smaller items worth a look while you're in there:
Could you also add a quick repro/test for the search-while-bionified case so we don't regress it? Happy to re-review once the search interaction is resolved. |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/renderer/components/FilePreview.tsx (1)
1327-1366:⚠️ Potential issue | 🟠 MajorMake markdown/readable search match across Bionify word spans.
With Bionify enabled, words are rendered inside
.bionify-wordspans, but this search path runs the regex against each individualTextnode. Multi-word queries likehello worldcan be split across span/space nodes and report no match in markdown/readable previews. Consider building matches from concatenated container text with node-offset mapping, or searching before the Bionify span split.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/FilePreview.tsx` around lines 1327 - 1366, The search logic in the markdown preview uses a TreeWalker over text nodes (walker) and runs localRegex against each Text node individually, so queries that span .bionify-word spans (rendered by Bionify) are missed; update the search in the function that uses markdownContainerRef and searchQuery to operate on the container's full concatenated text and map back to node offsets: build a single string of container text (preserving whitespace), run searchRegex/localRegex on that string to find match start/end indices, then convert those indices into per-node Range objects (filling allRanges) before applying CSS.highlights (and preserve existing behavior of clearing highlights and setting matchElementsRef/currentMatchIndex/totalMatches).src/renderer/utils/markdownConfig.ts (1)
545-620:⚠️ Potential issue | 🟠 MajorFix grouped selector handling for inline wizard Bionify styles.
getBionifyReadingModeStyles()appends.bionify-wordto the provided selector. Withs = "${scopeSelector}.prose, ${scopeSelector} .prose", the generated CSS becomes a grouped selector where${scopeSelector}.prosestyles the container itself instead of its Bionify descendants. That can leave same-element.prosepreviews without the intended emphasis/rest styling.Proposed fix
- const s = scopeSelector ? `${scopeSelector}.prose, ${scopeSelector} .prose` : '.prose'; + const sameElementScope = scopeSelector ? `${scopeSelector}.prose` : '.prose'; + const descendantScope = scopeSelector ? `${scopeSelector} .prose` : '.prose'; + const s = scopeSelector ? `${sameElementScope}, ${descendantScope}` : '.prose'; + const bionifyStyles = scopeSelector + ? `${getBionifyReadingModeStyles(sameElementScope, theme)} + ${getBionifyReadingModeStyles(descendantScope, theme)}` + : getBionifyReadingModeStyles(s, theme); @@ - ${getBionifyReadingModeStyles(s, theme)} + ${bionifyStyles} `;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/utils/markdownConfig.ts` around lines 545 - 620, The grouped selector s ("${scopeSelector}.prose, ${scopeSelector} .prose") causes getBionifyReadingModeStyles(...) to append .bionify-word to the container selector too, so container previews miss the intended inline emphasis; fix by calling getBionifyReadingModeStyles with the descendant-only selector (e.g. "${scopeSelector} .prose") instead of the grouped s, or split the call and pass each part separately so the function only targets descendant .prose elements (update the call site of getBionifyReadingModeStyles in markdownConfig.ts accordingly).
🧹 Nitpick comments (4)
src/main/web-server/web-server-factory.ts (1)
210-213: Minor: redundantas booleancast.
settingsStore.get('bionifyReadingMode', false)already infersbooleanfrom the default value parameter (get<T>(key, defaultValue?: T): T). The explicitas booleancast is unnecessary. ConsidersettingsStore.get<boolean>('bionifyReadingMode', false)or dropping the cast entirely for consistency with the nearbyactiveThemeId/webInterfaceUseCustomPortreads.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/web-server/web-server-factory.ts` around lines 210 - 213, The callback passed to server.setGetBionifyReadingModeCallback uses an unnecessary "as boolean" cast on settingsStore.get('bionifyReadingMode', false); remove the redundant cast and either call settingsStore.get<boolean>('bionifyReadingMode', false) or simply return settingsStore.get('bionifyReadingMode', false) to match surrounding reads (e.g., activeThemeId/webInterfaceUseCustomPort) and keep type inference consistent.src/__tests__/main/web-server/services/broadcastService.test.ts (1)
184-221: Consider a dedicated describe block for Bionify sync.This new test is nested inside the
Theme Sync (Desktop → Web)describe block but asserts behavior for a different broadcast type. Moving it to its owndescribe('Bionify Reading Mode Sync (Desktop → Web)')would better reflect the grouping used for the other broadcasts in this file and make test output easier to scan.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/main/web-server/services/broadcastService.test.ts` around lines 184 - 221, The bionify_reading_mode test is incorrectly nested under describe('Theme Sync (Desktop → Web)'); move the it(...) that calls service.broadcastBionifyReadingModeChange and its setup/expectations into a new top-level describe('Bionify Reading Mode Sync (Desktop → Web)') block to mirror the file’s grouping; locate the test referencing broadcastBionifyReadingModeChange (and mocks createMockClient/clients usage) and cut/paste it into the new describe block so the test suite outputs and organization accurately reflect the broadcast type.src/web/mobile/readingContent.ts (1)
136-142:isMarkdownPreviewablecounts all```substrings, including nested occurrences.
content.match(/```/g)doesn't distinguish fence openers from closers, so content like```js\n...\n```\nplus stray ``` in prosewould be deemed not previewable and fall through to structured parsing. GivenparseTextWithCodeBlockshas the same fence model this is mostly self-consistent, but the heuristic is worth a brief comment noting it's a parity check rather than a true fence validator — it helps future readers avoid “fixing” it with a stricter parser that breaks the current fallback contract.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/web/mobile/readingContent.ts` around lines 136 - 142, The function isMarkdownPreviewable currently simply counts occurrences of triple backticks and treats an odd count as unpreviewable, but this is a heuristic parity check (not a full fence validator) and should be documented to prevent future refactors that tighten validation and break parity with parseTextWithCodeBlocks; add a short comment above isMarkdownPreviewable stating it intentionally counts fence markers only, doesn't attempt to validate nesting or distinguish openers/closers, and that its behavior is expected to match the simpler fence model used by parseTextWithCodeBlocks so the fallback parsing remains consistent.src/renderer/utils/bionifyReadingMode.tsx (1)
81-102: Hoist the<style>injection to avoid DOM churn.The
<style>{getBionifyReadingModeStyles()}</style>at line 149 runs on everyBionifyTextBlockmount/render. With multiple readable surfaces mounted, you get N identical style nodes in the DOM and N re-inserts on each render. Since the selector targets the global.bionify-text-blockclass, move the injection to module level (or use a deduplication helper) to achieve the same visual result with significantly less DOM churn.♻️ Sketch: inject once at module level
+const BIONIFY_STYLE_ELEMENT_ID = 'bionify-text-block-styles'; + +function ensureBionifyStylesInjected() { + if (typeof document === 'undefined') return; + if (document.getElementById(BIONIFY_STYLE_ELEMENT_ID)) return; + const styleEl = document.createElement('style'); + styleEl.id = BIONIFY_STYLE_ELEMENT_ID; + styleEl.textContent = getBionifyReadingModeStyles(); + document.head.appendChild(styleEl); +} + export const BionifyTextBlock = forwardRef<HTMLDivElement, BionifyTextBlockProps>( function BionifyTextBlock( { children, enabled, className = '', style, ...props }, ref: ForwardedRef<HTMLDivElement> ) { + ensureBionifyStylesInjected(); const blockClassName = ['bionify-text-block', className].filter(Boolean).join(' '); return ( <div ref={ref} className={blockClassName} style={style} {...props}> - <style>{getBionifyReadingModeStyles()}</style> <BionifyText enabled={enabled}>{children}</BionifyText> </div> ); } );Note
markdownConfig.tsalready handles prose styles separately, so this only changes how the plain-text block variant emits its CSS.The
node.tagNamecontract is secure. The testpreserves markdown task lists and fenced code blocks when enabledverifies with real markdown-rendered fenced code blocks ('- [x] Ship reader tests\n\n```ts\nconst value = 1;\n```') that.bionify-wordelements do not appear inside<pre>tags. Inline code elements like<code>havenode.type === 'code'which is correctly skipped, and the fallback tonodeProps.node?.tagNameprovides redundancy if react-markdown version behavior changes. No action needed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/utils/bionifyReadingMode.tsx` around lines 81 - 102, The style injection for getBionifyReadingModeStyles() is being created per BionifyTextBlock render causing duplicate style nodes; hoist this so the CSS is injected once at module-evaluation time (or use a simple dedupe helper) instead of inside the component render. Add a top-level initialization (e.g., an immediately-run function or an injectBionifyStyles helper called once) that checks for an existing marker (class/id) or a module-scoped boolean and appends a single <style> with getBionifyReadingModeStyles(); leave BIONIFY_SKIPPED_TAGS and transformBionifyNode unchanged, just remove any per-render style creation from BionifyTextBlock and rely on the module-level injection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/releases.md`:
- Line 74: Fix the typos in the user-facing release notes text: change "FIle" to
"File" in the sentence starting "🗄️ Document Graphs. Launch from file
preview..." (the Document Graphs line) and change "built in updater" to
"built-in updater" wherever it appears (also noted around the second occurrence
on the release notes line referenced). Ensure both corrections are applied in
the release notes content.
- Around line 439-448: Replace the nonstandard bullet character "•" with the
markdown list marker "-" for the release note entries (the lines that currently
start with "• Enhanced mobile web interface..." through "Added splash screen..."
and the other block mentioned around the 462-471 range); update each list item
so it begins with "- " and ensure spacing/indentation matches the surrounding
markdown list style used elsewhere in the document to keep rendering consistent.
- Line 20: Several release subsection headings (e.g., the "Major 0.15.x
Additions" heading and other headings inside existing "## v..." sections) use
top-level '#' / '##' and break the Markdown hierarchy; update those headings
inside each "## v..." release block to be demoted consistently (use "###" or
"####" as appropriate) so they nest under their containing "## v..."
headings—apply this fix for the indicated sections including the heading "Major
0.15.x Additions" and the blocks corresponding to the ranges referenced in the
review (80-80, 143-151, 176-176, 268-297).
- Line 80: Update the release heading string "# Smaller Changes in 014.x" to use
the correct version format "0.14.x" so it reads "# Smaller Changes in 0.14.x";
edit the heading text in docs/releases.md (search for the exact heading "Smaller
Changes in 014.x") and replace the version token only, leaving surrounding
formatting unchanged.
In `@e2e/bionify-reading-mode.spec.ts`:
- Around line 254-314: The poll currently returns counts but only asserts after
the poll, allowing the poll to succeed with zeros; change the poll callback used
in await expect.poll(...) (the window.evaluate block that builds total,
fileSurfaceWords, autoRunSurfaceWords, codeWords, composerWords) to assert
positivity inside the poll itself—e.g. have it return a boolean or the counts
and check total > 0, fileSurfaceWords > 0 and autoRunSurfaceWords > 0 (and
codeWords === 0, composerWords === 0) so the poll only resolves when those
counts are > 0; update the poll invocation that references previewPhrase /
autoRunPhrase / 'Chat input plain text remains editable.' and .bionify-word,
fileSurfaceWords, autoRunSurfaceWords, codeWords, composerWords accordingly.
In `@src/__tests__/renderer/components/AgentPromptComposerModal.test.tsx`:
- Around line 182-199: The test currently relies on the default bionify setting
and can pass spuriously; update the test to explicitly enable the bionify
reading mode in the settings store (set bionifyReadingMode = true) before
calling renderWithLayerStack(…) for AgentPromptComposerModal, then assert the
textbox value via screen.getByRole('textbox') and that
document.querySelector('.bionify-word') is not present to prove the composer is
excluded; alternatively, if you prefer not to change settings, add a short
comment above the test clarifying it is only a smoke test and does not exercise
bionify=true.
In `@src/renderer/components/FilePreview.tsx`:
- Around line 2395-2403: Readable-text previews are being silently truncated
because the readable-text branch renders displayContent in BionifyTextBlock
without the truncation banner / "Load full file" control; update the
isReadableText && !markdownEditMode branch (where BionifyTextBlock is used) to
detect the same truncation condition used by the syntax-highlighter branch
(e.g., compare displayContent.length or use the existing
isTruncated/originalContentLength vs LARGE_FILE_PREVIEW_LIMIT flag) and render
the same truncation affordance and "Load full file" control beneath the
BionifyTextBlock so large .txt/.rst files show the banner and load button just
like the other branch.
In `@src/web/mobile/MessageHistory.tsx`:
- Around line 353-362: The Bionify flag is being applied to all messages passed
to WebReadingContent; change the prop so enableBionifyReadingMode is only true
when the message source is the AI stdout (i.e., check message.source ===
'stdout' or equivalent) while still passing displayText and other props
unchanged; update the WebReadingContent invocation (the component usage around
displayText and enableBionifyReadingMode) to compute a boolean like
isStdoutBionify = enableBionifyReadingMode && message.source === 'stdout' and
pass that instead so stderr/system messages keep normal markdown/rendering.
In `@src/web/mobile/readingContent.ts`:
- Around line 75-134: parseTextWithCodeBlocks currently uses the fixed regex
codeBlockRegex = /```([^\n\r`]*)\n?([\s\S]*?)```/g which only recognizes
exactly-3-backtick fences and can mis-parse CommonMark fences with 4+ backticks;
update the parsing to capture the opening fence length and require the same
number of backticks for the closing fence (i.e., change the regex/logic to
capture a sequence of backticks like /(`{3,})([^\n\r`]*)\n?([\s\S]*?)\1/ or
implement equivalent manual matching) so that parseTextWithCodeBlocks correctly
pairs fences of the same length and avoids producing corrupted code segments
(keep use of normalizeReaderLanguage and the existing segment push logic).
---
Outside diff comments:
In `@src/renderer/components/FilePreview.tsx`:
- Around line 1327-1366: The search logic in the markdown preview uses a
TreeWalker over text nodes (walker) and runs localRegex against each Text node
individually, so queries that span .bionify-word spans (rendered by Bionify) are
missed; update the search in the function that uses markdownContainerRef and
searchQuery to operate on the container's full concatenated text and map back to
node offsets: build a single string of container text (preserving whitespace),
run searchRegex/localRegex on that string to find match start/end indices, then
convert those indices into per-node Range objects (filling allRanges) before
applying CSS.highlights (and preserve existing behavior of clearing highlights
and setting matchElementsRef/currentMatchIndex/totalMatches).
In `@src/renderer/utils/markdownConfig.ts`:
- Around line 545-620: The grouped selector s ("${scopeSelector}.prose,
${scopeSelector} .prose") causes getBionifyReadingModeStyles(...) to append
.bionify-word to the container selector too, so container previews miss the
intended inline emphasis; fix by calling getBionifyReadingModeStyles with the
descendant-only selector (e.g. "${scopeSelector} .prose") instead of the grouped
s, or split the call and pass each part separately so the function only targets
descendant .prose elements (update the call site of getBionifyReadingModeStyles
in markdownConfig.ts accordingly).
---
Nitpick comments:
In `@src/__tests__/main/web-server/services/broadcastService.test.ts`:
- Around line 184-221: The bionify_reading_mode test is incorrectly nested under
describe('Theme Sync (Desktop → Web)'); move the it(...) that calls
service.broadcastBionifyReadingModeChange and its setup/expectations into a new
top-level describe('Bionify Reading Mode Sync (Desktop → Web)') block to mirror
the file’s grouping; locate the test referencing
broadcastBionifyReadingModeChange (and mocks createMockClient/clients usage) and
cut/paste it into the new describe block so the test suite outputs and
organization accurately reflect the broadcast type.
In `@src/main/web-server/web-server-factory.ts`:
- Around line 210-213: The callback passed to
server.setGetBionifyReadingModeCallback uses an unnecessary "as boolean" cast on
settingsStore.get('bionifyReadingMode', false); remove the redundant cast and
either call settingsStore.get<boolean>('bionifyReadingMode', false) or simply
return settingsStore.get('bionifyReadingMode', false) to match surrounding reads
(e.g., activeThemeId/webInterfaceUseCustomPort) and keep type inference
consistent.
In `@src/renderer/utils/bionifyReadingMode.tsx`:
- Around line 81-102: The style injection for getBionifyReadingModeStyles() is
being created per BionifyTextBlock render causing duplicate style nodes; hoist
this so the CSS is injected once at module-evaluation time (or use a simple
dedupe helper) instead of inside the component render. Add a top-level
initialization (e.g., an immediately-run function or an injectBionifyStyles
helper called once) that checks for an existing marker (class/id) or a
module-scoped boolean and appends a single <style> with
getBionifyReadingModeStyles(); leave BIONIFY_SKIPPED_TAGS and
transformBionifyNode unchanged, just remove any per-render style creation from
BionifyTextBlock and rely on the module-level injection.
In `@src/web/mobile/readingContent.ts`:
- Around line 136-142: The function isMarkdownPreviewable currently simply
counts occurrences of triple backticks and treats an odd count as unpreviewable,
but this is a heuristic parity check (not a full fence validator) and should be
documented to prevent future refactors that tighten validation and break parity
with parseTextWithCodeBlocks; add a short comment above isMarkdownPreviewable
stating it intentionally counts fence markers only, doesn't attempt to validate
nesting or distinguish openers/closers, and that its behavior is expected to
match the simpler fence model used by parseTextWithCodeBlocks so the fallback
parsing remains consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d67e80dd-e143-4538-9810-29779c0ef267
⛔ Files ignored due to path filters (2)
docs/screenshots/bionify-autorun.pngis excluded by!**/*.pngdocs/screenshots/bionify-file-preview.pngis excluded by!**/*.png
📒 Files selected for processing (51)
docs/releases.mde2e/bionify-reading-mode.spec.tssrc/__tests__/main/ipc/handlers/persistence.test.tssrc/__tests__/main/web-server/services/broadcastService.test.tssrc/__tests__/main/web-server/web-server-factory.test.tssrc/__tests__/renderer/components/AgentPromptComposerModal.test.tsxsrc/__tests__/renderer/components/DirectorNotes/AIOverviewTab.test.tsxsrc/__tests__/renderer/components/FilePreview.test.tsxsrc/__tests__/renderer/components/HistoryDetailModal.test.tsxsrc/__tests__/renderer/components/InlineWizard/StreamingDocumentPreview.test.tsxsrc/__tests__/renderer/components/MarkdownRenderer.bionify.test.tsxsrc/__tests__/renderer/components/PromptComposerModal.test.tsxsrc/__tests__/renderer/components/Wizard/shared/DocumentEditor.test.tsxsrc/__tests__/renderer/utils/bionifyReadingMode.test.tsxsrc/__tests__/renderer/utils/markdownConfig.test.tssrc/__tests__/web/hooks/useWebSocket.test.tssrc/__tests__/web/mobile/App.test.tsxsrc/__tests__/web/mobile/MessageHistory.test.tsxsrc/__tests__/web/mobile/MobileMarkdownRenderer.test.tsxsrc/__tests__/web/mobile/ResponseViewer.test.tsxsrc/__tests__/web/mobile/readingContent.test.tssrc/main/ipc/handlers/persistence.tssrc/main/web-server/WebServer.tssrc/main/web-server/managers/CallbackRegistry.tssrc/main/web-server/routes/wsRoute.tssrc/main/web-server/services/broadcastService.tssrc/main/web-server/types.tssrc/main/web-server/web-server-factory.tssrc/renderer/components/AutoRun.tsxsrc/renderer/components/DirectorNotes/AIOverviewTab.tsxsrc/renderer/components/DocumentGraph/DocumentGraphView.tsxsrc/renderer/components/FilePreview.tsxsrc/renderer/components/HistoryDetailModal.tsxsrc/renderer/components/InlineWizard/StreamingDocumentPreview.tsxsrc/renderer/components/MarkdownRenderer.tsxsrc/renderer/components/Settings/tabs/DisplayTab.tsxsrc/renderer/components/Wizard/shared/DocumentEditor.tsxsrc/renderer/hooks/settings/useSettings.tssrc/renderer/stores/settingsStore.tssrc/renderer/utils/bionifyReadingMode.tsxsrc/renderer/utils/markdownConfig.tssrc/shared/settingsMetadata.tssrc/web/App.tsxsrc/web/hooks/useMobileSessionManagement.tssrc/web/hooks/useWebSocket.tssrc/web/mobile/App.tsxsrc/web/mobile/MessageHistory.tsxsrc/web/mobile/MobileMarkdownRenderer.tsxsrc/web/mobile/ResponseViewer.tsxsrc/web/mobile/WebReadingContent.tsxsrc/web/mobile/readingContent.ts
| - **Group chat participant management:** Remove button on participant cards lets you remove stale or unwanted participants from a group chat | ||
| - **Batch resume/abort:** New controls in the right panel for resuming or aborting batch operations | ||
| - **Default worktree directory:** Worktree configuration now defaults to the parent of the agent's working directory instead of blank | ||
| # Major 0.15.x Additions |
There was a problem hiding this comment.
Fix heading-level hierarchy inside release sections.
Several subheadings are promoted to #/## inside existing ## v... sections, which breaks Markdown structure and TOC nesting. These should be demoted consistently (typically to ### or ####).
Suggested heading-level adjustment pattern
-# Major 0.15.x Additions
+### Major 0.15.x Additions
-# Smaller Changes in 014.x
+### Smaller Changes in 0.14.x
-## Show Thinking
+### Show Thinking
-## GitHub Spec-Kit Integration
+### GitHub Spec-Kit Integration
-## Context Management Tools
+### Context Management Tools
-# Other Changes
+### Other ChangesAlso applies to: 80-80, 143-151, 176-176, 268-297
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/releases.md` at line 20, Several release subsection headings (e.g., the
"Major 0.15.x Additions" heading and other headings inside existing "## v..."
sections) use top-level '#' / '##' and break the Markdown hierarchy; update
those headings inside each "## v..." release block to be demoted consistently
(use "###" or "####" as appropriate) so they nest under their containing "##
v..." headings—apply this fix for the indicated sections including the heading
"Major 0.15.x Additions" and the blocks corresponding to the ranges referenced
in the review (80-80, 143-151, 176-176, 268-297).
|
|
||
| The major contributions to 0.14.x remain: | ||
|
|
||
| 🗄️ Document Graphs. Launch from file preview or from the FIle tree panel. Explore relationships between Markdown documents that contain links between documents and to URLs. |
There was a problem hiding this comment.
Correct typos in user-facing release notes text.
There are a couple of visible copy issues: FIle → File, and built in updater → built-in updater.
Quick text fix
-🗄️ Document Graphs. Launch from file preview or from the FIle tree panel.
+🗄️ Document Graphs. Launch from file preview or from the File tree panel.
-🎯 Added [GitHub Spec-Kit](https://github.com/github/spec-kit) commands into Maestro with a built in updater...
+🎯 Added [GitHub Spec-Kit](https://github.com/github/spec-kit) commands into Maestro with a built-in updater...Also applies to: 149-149
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/releases.md` at line 74, Fix the typos in the user-facing release notes
text: change "FIle" to "File" in the sentence starting "🗄️ Document Graphs.
Launch from file preview..." (the Document Graphs line) and change "built in
updater" to "built-in updater" wherever it appears (also noted around the second
occurrence on the release notes line referenced). Ensure both corrections are
applied in the release notes content.
|
|
||
| 🧙♂️ Added an in-tab wizard for generating Auto Run Playbooks via `/wizard` or a new button in the Auto Run panel. | ||
|
|
||
| # Smaller Changes in 014.x |
There was a problem hiding this comment.
Fix version formatting typo in heading.
014.x should be 0.14.x for consistency with the section/version naming.
Direct fix
-# Smaller Changes in 014.x
+### Smaller Changes in 0.14.x📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Smaller Changes in 014.x | |
| ### Smaller Changes in 0.14.x |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/releases.md` at line 80, Update the release heading string "# Smaller
Changes in 014.x" to use the correct version format "0.14.x" so it reads "#
Smaller Changes in 0.14.x"; edit the heading text in docs/releases.md (search
for the exact heading "Smaller Changes in 014.x") and replace the version token
only, leaving surrounding formatting unchanged.
| • Enhanced mobile web interface with session sync and history panel 📱 | ||
| • Added ThinkingStatusPill showing real-time token counts and elapsed time ⏱️ | ||
| • Implemented task count badges and session deduplication for batch runner 📊 | ||
| • Added TTS stop control and improved voice synthesis compatibility 🔊 | ||
| • Created image lightbox with navigation, clipboard, and delete features 🖼️ | ||
| • Fixed UI bugs in search, auto-scroll, and sidebar interactions 🐛 | ||
| • Added global Claude stats with streaming updates across projects 📈 | ||
| • Improved markdown checkbox styling and collapsed palette hover UX ✨ | ||
| • Enhanced scratchpad with search, image paste, and attachment support 🔍 | ||
| • Added splash screen with logo and progress bar during startup 🎨 |
There was a problem hiding this comment.
Use Markdown list markers consistently (- instead of •).
These sections switched to • bullets, which can render inconsistently and diverge from the rest of this file’s list style.
Example normalization
-• Enhanced mobile web interface with session sync and history panel 📱
+- Enhanced mobile web interface with session sync and history panel 📱Also applies to: 462-471
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/releases.md` around lines 439 - 448, Replace the nonstandard bullet
character "•" with the markdown list marker "-" for the release note entries
(the lines that currently start with "• Enhanced mobile web interface..."
through "Added splash screen..." and the other block mentioned around the
462-471 range); update each list item so it begins with "- " and ensure
spacing/indentation matches the surrounding markdown list style used elsewhere
in the document to keep rendering consistent.
| await expect | ||
| .poll(async () => { | ||
| return await window.evaluate( | ||
| ([fileSnippet, autoRunSnippet, chatValue]) => { | ||
| const blocks = Array.from( | ||
| document.querySelectorAll('div, section, article, main, aside') | ||
| ); | ||
| const fileSurface = blocks.find((node) => node.textContent?.includes(fileSnippet)); | ||
| const autoRunSurface = blocks.find((node) => | ||
| node.textContent?.includes(autoRunSnippet) | ||
| ); | ||
| const composer = Array.from(document.querySelectorAll('textarea')).find((node) => | ||
| node.value.includes(chatValue) | ||
| ); | ||
|
|
||
| return { | ||
| total: document.querySelectorAll('.bionify-word').length, | ||
| fileSurfaceWords: fileSurface?.querySelectorAll('.bionify-word').length ?? 0, | ||
| autoRunSurfaceWords: autoRunSurface?.querySelectorAll('.bionify-word').length ?? 0, | ||
| codeWords: document.querySelectorAll('code .bionify-word').length, | ||
| composerWords: composer?.querySelectorAll('.bionify-word').length ?? 0, | ||
| }; | ||
| }, | ||
| [previewPhrase, autoRunPhrase, 'Chat input plain text remains editable.'] | ||
| ); | ||
| }) | ||
| .toEqual({ | ||
| total: expect.any(Number), | ||
| fileSurfaceWords: expect.any(Number), | ||
| autoRunSurfaceWords: expect.any(Number), | ||
| codeWords: 0, | ||
| composerWords: 0, | ||
| }); | ||
|
|
||
| const counts = await window.evaluate( | ||
| ([fileSnippet, autoRunSnippet, chatValue]) => { | ||
| const blocks = Array.from( | ||
| document.querySelectorAll('div, section, article, main, aside') | ||
| ); | ||
| const fileSurface = blocks.find((node) => node.textContent?.includes(fileSnippet)); | ||
| const autoRunSurface = blocks.find((node) => node.textContent?.includes(autoRunSnippet)); | ||
| const composer = Array.from(document.querySelectorAll('textarea')).find((node) => | ||
| node.value.includes(chatValue) | ||
| ); | ||
|
|
||
| return { | ||
| total: document.querySelectorAll('.bionify-word').length, | ||
| fileSurfaceWords: fileSurface?.querySelectorAll('.bionify-word').length ?? 0, | ||
| autoRunSurfaceWords: autoRunSurface?.querySelectorAll('.bionify-word').length ?? 0, | ||
| codeWords: document.querySelectorAll('code .bionify-word').length, | ||
| composerWords: composer?.querySelectorAll('.bionify-word').length ?? 0, | ||
| }; | ||
| }, | ||
| [previewPhrase, autoRunPhrase, 'Chat input plain text remains editable.'] | ||
| ); | ||
|
|
||
| expect(counts.total).toBeGreaterThan(0); | ||
| expect(counts.fileSurfaceWords).toBeGreaterThan(0); | ||
| expect(counts.autoRunSurfaceWords).toBeGreaterThan(0); | ||
| expect(counts.codeWords).toBe(0); | ||
| expect(counts.composerWords).toBe(0); |
There was a problem hiding this comment.
Wait for positive Bionify counts inside the poll.
The current poll can pass with 0 for total, fileSurfaceWords, and autoRunSurfaceWords; the immediate toBeGreaterThan(0) checks after it are therefore timing-dependent.
Proposed fix
await expect
.poll(async () => {
- return await window.evaluate(
+ const counts = await window.evaluate(
([fileSnippet, autoRunSnippet, chatValue]) => {
const blocks = Array.from(
document.querySelectorAll('div, section, article, main, aside')
);
@@
},
[previewPhrase, autoRunPhrase, 'Chat input plain text remains editable.']
);
+
+ return {
+ hasTotalWords: counts.total > 0,
+ hasFileSurfaceWords: counts.fileSurfaceWords > 0,
+ hasAutoRunSurfaceWords: counts.autoRunSurfaceWords > 0,
+ codeWords: counts.codeWords,
+ composerWords: counts.composerWords,
+ };
})
.toEqual({
- total: expect.any(Number),
- fileSurfaceWords: expect.any(Number),
- autoRunSurfaceWords: expect.any(Number),
+ hasTotalWords: true,
+ hasFileSurfaceWords: true,
+ hasAutoRunSurfaceWords: true,
codeWords: 0,
composerWords: 0,
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@e2e/bionify-reading-mode.spec.ts` around lines 254 - 314, The poll currently
returns counts but only asserts after the poll, allowing the poll to succeed
with zeros; change the poll callback used in await expect.poll(...) (the
window.evaluate block that builds total, fileSurfaceWords, autoRunSurfaceWords,
codeWords, composerWords) to assert positivity inside the poll itself—e.g. have
it return a boolean or the counts and check total > 0, fileSurfaceWords > 0 and
autoRunSurfaceWords > 0 (and codeWords === 0, composerWords === 0) so the poll
only resolves when those counts are > 0; update the poll invocation that
references previewPhrase / autoRunPhrase / 'Chat input plain text remains
editable.' and .bionify-word, fileSurfaceWords, autoRunSurfaceWords, codeWords,
composerWords accordingly.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/renderer/components/FilePreview.tsx`:
- Around line 190-213: isReadableTextPreview currently lets files with
extensions (e.g., README.ts) match basenames and also treats .mdx as plain text;
update READABLE_TEXT_EXTENSIONS to remove 'mdx' and change isReadableTextPreview
so basename checks only run when there is no extension: inside
isReadableTextPreview (and referencing READABLE_TEXT_EXTENSIONS and
READABLE_TEXT_BASENAMES) first compute ext and if ext is non-empty return true
only when ext is in READABLE_TEXT_EXTENSIONS, otherwise return false; only if
ext is empty then check the basename against READABLE_TEXT_BASENAMES.
In `@src/renderer/utils/bionifyReadingMode.tsx`:
- Around line 167-177: BionifyTextBlock currently always injects the CSS
variable --bionify-rest-opacity defaulting to DEFAULT_BIONIFY_REST_OPACITY which
forces light-theme callers to get dark-mode styling; change the component
contract so restOpacity is strictly an optional override and only set the CSS
variable when the caller provides restOpacity (do not fall back to
DEFAULT_BIONIFY_REST_OPACITY inside blockStyle). Update the
BionifyTextBlockProps type to keep restOpacity optional, then change the
blockStyle construction in function BionifyTextBlock to conditionally add
['--bionify-rest-opacity' as const]: restOpacity only when restOpacity !==
undefined (leave the variable unset otherwise), or alternatively add a theme
prop if you prefer explicit theme control—refer to BionifyTextBlock,
restOpacity, DEFAULT_BIONIFY_REST_OPACITY while making this change.
- Around line 61-70: The current renderBionifyWord function uses word.slice(...)
which slices by UTF-16 code units and can split grapheme clusters (e.g., base +
combining marks); change it to compute and slice by grapheme clusters instead:
use Intl.Segmenter (or a grapheme-splitter library) to split the input word into
an array of grapheme clusters, compute emphasisLength in clusters (adjust or
replace getEmphasisLength to count clusters or map its output to cluster count),
then join the first N clusters for emphasis and the remainder for rest before
rendering the two spans in renderBionifyWord so combining marks stay attached to
their base characters.
In `@src/web/mobile/MessageHistory.tsx`:
- Around line 360-363: The stderr text color from the parent wrapper isn't being
forwarded to WebReadingContent, letting stderr lose the red error styling;
update the WebReadingContent invocation (the component currently receiving props
enableBionifyReadingMode, fontSize, gap) to also pass the textColor prop (the
same value the parent uses for stderr, e.g. colors.error or the variable used
for stderr text) so WebReadingContent can apply it instead of falling back to
its default textColor || colors.textMain.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 56cabb41-3b3e-423f-a1fd-bcbe64829969
📒 Files selected for processing (14)
src/__tests__/main/web-server/services/broadcastService.test.tssrc/__tests__/renderer/components/AgentPromptComposerModal.test.tsxsrc/__tests__/renderer/components/FilePreview.test.tsxsrc/__tests__/renderer/utils/bionifyReadingMode.test.tsxsrc/__tests__/renderer/utils/markdownConfig.test.tssrc/__tests__/web/mobile/MessageHistory.test.tsxsrc/__tests__/web/mobile/readingContent.test.tssrc/main/web-server/web-server-factory.tssrc/renderer/components/FilePreview.tsxsrc/renderer/utils/bionifyReadingMode.tsxsrc/renderer/utils/markdownConfig.tssrc/web/mobile/MessageHistory.tsxsrc/web/mobile/WebReadingContent.tsxsrc/web/mobile/readingContent.ts
🚧 Files skipped from review as they are similar to previous changes (9)
- src/tests/main/web-server/services/broadcastService.test.ts
- src/main/web-server/web-server-factory.ts
- src/tests/web/mobile/readingContent.test.ts
- src/tests/renderer/utils/markdownConfig.test.ts
- src/tests/renderer/components/FilePreview.test.tsx
- src/renderer/utils/markdownConfig.ts
- src/tests/renderer/utils/bionifyReadingMode.test.tsx
- src/web/mobile/WebReadingContent.tsx
- src/web/mobile/readingContent.ts
| function renderBionifyWord(word: string, key: string): ReactNode { | ||
| const emphasisLength = Math.min(getEmphasisLength(word), word.length); | ||
| const emphasis = word.slice(0, emphasisLength); | ||
| const rest = word.slice(emphasisLength); | ||
|
|
||
| return ( | ||
| <span key={key} className="bionify-word"> | ||
| <span className="bionify-word-emphasis">{emphasis}</span> | ||
| {rest ? <span className="bionify-word-rest">{rest}</span> : null} | ||
| </span> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/renderer/utils/bionifyReadingMode.tsxRepository: RunMaestro/Maestro
Length of output: 6215
🏁 Script executed:
rg -A 3 -B 3 "renderBionifyText|BionifyText|renderBionifyWord" --type tsx --type ts | head -100Repository: RunMaestro/Maestro
Length of output: 89
🏁 Script executed:
# Search for any documentation or comments about supported languages
rg -i "language|unicode|grapheme|accent" src/renderer/utils/bionifyReadingMode.tsx
# Check if there are any tests for this module
fd -e "test" -e "spec" . | rg -i bionifyRepository: RunMaestro/Maestro
Length of output: 44
🏁 Script executed:
python3 << 'EOF'
import re
# The pattern from the file
pattern = r"(\p{L}[\p{L}\p{M}''-]*)"
# Test case: decomposed accent (e + combining acute accent)
decomposed = "café" # This will be a single grapheme "é" in normal form
# UTF-16 code units test
test_strings = [
"café", # é as single character
"e\u0301", # e + combining acute (decomposed)
"naïve", # ï
"Zürich", # ü
]
for s in test_strings:
print(f"String: {repr(s)}")
print(f" Length (UTF-16): {len(s)}")
print(f" Characters: {list(s)}")
print(f" Slice [0:1]: {repr(s[0:1])}")
print()
EOFRepository: RunMaestro/Maestro
Length of output: 431
🏁 Script executed:
python3 << 'EOF'
# Simulate the bionify logic
def getEmphasisLength(word):
if len(word) <= 3: return 1
if len(word) <= 6: return 2
if len(word) <= 9: return 3
return 4
# Test with a word containing combining marks
test_cases = [
"café", # Common case - é as single codepoint (U+00E9)
"e\u0301", # Decomposed - e + combining acute (U+0301)
"naïve",
]
for word in test_cases:
emphasis_len = getEmphasisLength(word)
emphasis = word[:emphasis_len]
rest = word[emphasis_len:]
print(f"Word: {repr(word)} (len={len(word)})")
print(f" Emphasis length: {emphasis_len}")
print(f" Emphasis part: {repr(emphasis)}")
print(f" Rest part: {repr(rest)}")
# Check if combining mark was split
for i, char in enumerate(emphasis):
if ord(char) in range(0x0300, 0x036F): # Combining marks range
print(f" ⚠️ Combining mark in emphasis: {repr(char)}")
for i, char in enumerate(rest):
if ord(char) in range(0x0300, 0x036F):
print(f" ⚠️ Combining mark in rest: {repr(char)}")
print()
EOFRepository: RunMaestro/Maestro
Length of output: 342
🏁 Script executed:
# Search for where BionifyText and BionifyTextBlock are actually used
rg "BionifyText|bionifyReadingMode" --type tsx --type ts -lRepository: RunMaestro/Maestro
Length of output: 89
🏁 Script executed:
rg "BionifyText|bionifyReadingMode" -lRepository: RunMaestro/Maestro
Length of output: 1509
🏁 Script executed:
# Let me check the actual usage to understand the context better
rg "BionifyTextBlock\|BionifyText" -A 2 -B 2 src/Repository: RunMaestro/Maestro
Length of output: 44
🏁 Script executed:
web_search
What is the browser support for Intl.Segmenter API and when was it introduced?Repository: RunMaestro/Maestro
Length of output: 155
🏁 Script executed:
cat -n src/__tests__/renderer/utils/bionifyReadingMode.test.tsxRepository: RunMaestro/Maestro
Length of output: 3503
🌐 Web query:
Intl.Segmenter browser support compatibility
💡 Result:
Intl.Segmenter has good support in modern browsers as of April 2026. It is fully supported in: - Chrome/Edge 87+ - Firefox 125+ - Safari 14.1+ - Opera 73+ No support in IE or older versions (pre-87 Chrome/Edge, pre-125 Firefox, pre-14.1 Safari). Mobile support mirrors desktop: Chrome for Android 87+, Firefox for Android 125+, Safari on iOS 14.5+. MDN marks it as "Baseline 2024", available in latest versions since April 2024. Polyfills available via FormatJS if needed for older browsers.
Citations:
- 1: https://caniuse.com/mdn-javascript_builtins_intl_segmenter
- 2: https://caniuse.com/wf-intl-segmenter
- 3: https://caniuse.com/mdn-javascript_builtins_intl_segmenter_segmenter
- 4: https://caniuse.com/mdn-javascript_builtins_intl_segmenter_segment
- 5: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter
- 6: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter
- 7: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter
🏁 Script executed:
# Check if there are any e2e tests related to Unicode or non-ASCII text
cat -n e2e/bionify-reading-mode.spec.ts | head -50Repository: RunMaestro/Maestro
Length of output: 2180
🏁 Script executed:
# Search for any comments about supported languages or input formats
rg -i "language|unicode|locali|i18n|accent" src/renderer/ --type tsRepository: RunMaestro/Maestro
Length of output: 50377
🏁 Script executed:
# Check the project's target browser support
cat -n tsconfig.json | head -30Repository: RunMaestro/Maestro
Length of output: 668
Avoid splitting Unicode grapheme clusters across spans.
word.slice(...) splits on UTF-16 code unit boundaries rather than grapheme boundaries. With decomposed Unicode (e.g., "e" + combining acute accent), the emphasis slice can separate a combining mark from its base character, resulting in corrupted rendering. For example, slicing "é" (decomposed as "e\u0301") at position 1 yields emphasis="e" and rest="́", rendering the combining mark in isolation.
Use grapheme-aware segmentation:
Suggested direction
function renderBionifyWord(word: string, key: string): ReactNode {
- const emphasisLength = Math.min(getEmphasisLength(word), word.length);
- const emphasis = word.slice(0, emphasisLength);
- const rest = word.slice(emphasisLength);
+ const graphemes =
+ typeof Intl !== 'undefined' && 'Segmenter' in Intl
+ ? Array.from(
+ new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(word),
+ (segment) => segment.segment
+ )
+ : Array.from(word);
+ const emphasisLength = Math.min(getEmphasisLength(word), graphemes.length);
+ const emphasis = graphemes.slice(0, emphasisLength).join('');
+ const rest = graphemes.slice(emphasisLength).join('');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function renderBionifyWord(word: string, key: string): ReactNode { | |
| const emphasisLength = Math.min(getEmphasisLength(word), word.length); | |
| const emphasis = word.slice(0, emphasisLength); | |
| const rest = word.slice(emphasisLength); | |
| return ( | |
| <span key={key} className="bionify-word"> | |
| <span className="bionify-word-emphasis">{emphasis}</span> | |
| {rest ? <span className="bionify-word-rest">{rest}</span> : null} | |
| </span> | |
| function renderBionifyWord(word: string, key: string): ReactNode { | |
| const graphemes = | |
| typeof Intl !== 'undefined' && 'Segmenter' in Intl | |
| ? Array.from( | |
| new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(word), | |
| (segment) => segment.segment | |
| ) | |
| : Array.from(word); | |
| const emphasisLength = Math.min(getEmphasisLength(word), graphemes.length); | |
| const emphasis = graphemes.slice(0, emphasisLength).join(''); | |
| const rest = graphemes.slice(emphasisLength).join(''); | |
| return ( | |
| <span key={key} className="bionify-word"> | |
| <span className="bionify-word-emphasis">{emphasis}</span> | |
| {rest ? <span className="bionify-word-rest">{rest}</span> : null} | |
| </span> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/utils/bionifyReadingMode.tsx` around lines 61 - 70, The current
renderBionifyWord function uses word.slice(...) which slices by UTF-16 code
units and can split grapheme clusters (e.g., base + combining marks); change it
to compute and slice by grapheme clusters instead: use Intl.Segmenter (or a
grapheme-splitter library) to split the input word into an array of grapheme
clusters, compute emphasisLength in clusters (adjust or replace
getEmphasisLength to count clusters or map its output to cluster count), then
join the first N clusters for emphasis and the remainder for rest before
rendering the two spans in renderBionifyWord so combining marks stay attached to
their base characters.
| export const BionifyTextBlock = forwardRef<HTMLDivElement, BionifyTextBlockProps>( | ||
| function BionifyTextBlock( | ||
| { children, enabled, className = '', restOpacity, style, ...props }, | ||
| ref: ForwardedRef<HTMLDivElement> | ||
| ) { | ||
| ensureBionifyStylesInjected(); | ||
| const blockClassName = ['bionify-text-block', className].filter(Boolean).join(' '); | ||
| const blockStyle = { | ||
| ...style, | ||
| ['--bionify-rest-opacity' as const]: restOpacity ?? DEFAULT_BIONIFY_REST_OPACITY, | ||
| } as CSSProperties; |
There was a problem hiding this comment.
Make light-mode opacity part of the component contract.
BionifyTextBlock defaults --bionify-rest-opacity to 0.96, so any light-theme caller that forgets restOpacity gets dark-mode styling. Consider accepting theme here, or only setting the CSS variable when a caller provides an override.
Suggested fix
interface BionifyTextBlockProps extends HTMLAttributes<HTMLDivElement> {
enabled: boolean;
children: ReactNode;
+ theme?: Theme;
restOpacity?: number;
style?: CSSProperties;
}
@@
- { children, enabled, className = '', restOpacity, style, ...props },
+ { children, enabled, className = '', theme, restOpacity, style, ...props },
ref: ForwardedRef<HTMLDivElement>
) {
ensureBionifyStylesInjected();
const blockClassName = ['bionify-text-block', className].filter(Boolean).join(' ');
+ const resolvedRestOpacity = restOpacity ?? resolveBionifyRestOpacity(theme);
const blockStyle = {
...style,
- ['--bionify-rest-opacity' as const]: restOpacity ?? DEFAULT_BIONIFY_REST_OPACITY,
+ ['--bionify-rest-opacity' as const]: resolvedRestOpacity,
} as CSSProperties;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/utils/bionifyReadingMode.tsx` around lines 167 - 177,
BionifyTextBlock currently always injects the CSS variable
--bionify-rest-opacity defaulting to DEFAULT_BIONIFY_REST_OPACITY which forces
light-theme callers to get dark-mode styling; change the component contract so
restOpacity is strictly an optional override and only set the CSS variable when
the caller provides restOpacity (do not fall back to
DEFAULT_BIONIFY_REST_OPACITY inside blockStyle). Update the
BionifyTextBlockProps type to keep restOpacity optional, then change the
blockStyle construction in function BionifyTextBlock to conditionally add
['--bionify-rest-opacity' as const]: restOpacity only when restOpacity !==
undefined (leave the variable unset otherwise), or alternatively add a theme
prop if you prefer explicit theme control—refer to BionifyTextBlock,
restOpacity, DEFAULT_BIONIFY_REST_OPACITY while making this change.
| enableBionifyReadingMode={isStdoutBionify} | ||
| fontSize={13} | ||
| gap="8px" | ||
| /> |
There was a problem hiding this comment.
Forward the error text color into WebReadingContent.
The parent wrapper sets stderr text to colors.error, but WebReadingContent applies its own textColor || colors.textMain, so stderr body text can lose the red error styling. Pass textColor through this invocation.
🎨 Proposed fix
<WebReadingContent
content={displayText}
enableBionifyReadingMode={isStdoutBionify}
fontSize={13}
+ textColor={isError ? colors.error : colors.textMain}
gap="8px"
/>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/web/mobile/MessageHistory.tsx` around lines 360 - 363, The stderr text
color from the parent wrapper isn't being forwarded to WebReadingContent,
letting stderr lose the red error styling; update the WebReadingContent
invocation (the component currently receiving props enableBionifyReadingMode,
fontSize, gap) to also pass the textColor prop (the same value the parent uses
for stderr, e.g. colors.error or the variable used for stderr text) so
WebReadingContent can apply it instead of falling back to its default textColor
|| colors.textMain.
|
Reference screenshot for the red-highlighted Bionify button placement discussed in review: Follow-up implemented on this branch:
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/renderer/components/AutoRun.tsx (1)
1457-1517:⚠️ Potential issue | 🟠 MajorDisable Bionify while AutoRun search is active.
FilePreview now guards Bionify with
!hasActiveSearch, but AutoRun still passespreviewBionifyReadingModeinto both normal and search-highlighted markdown components. That can reintroduce the word-splitting/search mismatch for AutoRun previews.Suggested fix
+ const hasActivePreviewSearch = searchOpen && searchQuery.trim().length > 0; + const effectivePreviewBionifyReadingMode = + previewBionifyReadingMode && !hasActivePreviewSearch; + // Base markdown components - stable unless theme, folderPath, or callbacks change // Separated from search highlighting to prevent rebuilds on every search state change const baseMarkdownComponents = useMemo(() => { const components = createMarkdownComponents({ theme, customLanguageRenderers: { mermaid: ({ code, theme: t }) => <MermaidRenderer chart={code} theme={t} />, }, - enableBionifyReadingMode: previewBionifyReadingMode, + enableBionifyReadingMode: effectivePreviewBionifyReadingMode, @@ }, [ - previewBionifyReadingMode, + effectivePreviewBionifyReadingMode, theme, folderPath, sshRemoteId, openLightboxByFilename, handleFileClick, @@ const components = createMarkdownComponents({ theme, customLanguageRenderers: { mermaid: ({ code, theme: t }) => <MermaidRenderer chart={code} theme={t} />, }, - enableBionifyReadingMode: previewBionifyReadingMode, + enableBionifyReadingMode: effectivePreviewBionifyReadingMode, @@ }, [ theme, - previewBionifyReadingMode, + effectivePreviewBionifyReadingMode, folderPath, sshRemoteId,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/AutoRun.tsx` around lines 1457 - 1517, Define a local hasActiveSearch boolean (e.g. const hasActiveSearch = searchOpen && searchQuery.trim() && totalMatches > 0) and use it to disable Bionify when search is active by changing both createMarkdownComponents calls (the ones inside baseMarkdownComponents and searchHighlightedComponents) to set enableBionifyReadingMode: previewBionifyReadingMode && !hasActiveSearch; this keeps Bionify off during active AutoRun search while preserving the existing behavior otherwise.src/__tests__/renderer/components/AutoRun.test.tsx (1)
96-120:⚠️ Potential issue | 🟠 MajorAdd regression test for Bionify behavior in preview mode with search.
The current test only verifies that the toggle button changes state, but does not confirm that
enableBionifyReadingModeis actually forwarded to the preview renderer or that search highlighting works correctly when Bionify is active. Sincereact-markdownis mocked in the test suite, the actualBionifyTexttransformations applied viaapplyReadableTextTransforms()are not tested. Add a test that enables Bionify in preview mode, performs a search (e.g., for "Reading"), and verifies the match count remains accurate with the real Bionify rendering path—similar to the existing test inFilePreview.test.tsx(line 352) that validates this interaction.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/renderer/components/AutoRun.test.tsx` around lines 96 - 120, Add a regression test in AutoRun.test.tsx that enables bionify (simulate clicking the toggle or setting bionifyEnabled) and runs the preview search flow using the real Bionify rendering path (avoid the mocked react-markdown) to ensure enableBionifyReadingMode is forwarded to the preview renderer; perform a search for a term like "Reading" and assert the highlighted/match count is correct using the same approach as the FilePreview.test.tsx example around line 352, verifying that applyReadableTextTransforms / BionifyText transformations are actually applied and do not break search highlighting.
♻️ Duplicate comments (1)
src/renderer/components/FilePreview.tsx (1)
191-214:⚠️ Potential issue | 🟠 MajorTighten readable-text routing so code and MDX don’t bypass their renderers.
This still treats
.mdxas plain readable text and still lets basenames match before extensions, so files likeREADME.tsorTODO.jscan render throughBionifyTextBlockinstead of syntax/markdown handling. This is the same unresolved routing issue from the previous review.Suggested fix
const languageMap: Record<string, string> = { // ... md: 'markdown', + mdx: 'markdown', // ... }; -const READABLE_TEXT_EXTENSIONS = new Set(['txt', 'text', 'rst', 'adoc', 'asc', 'mdx']); +const READABLE_TEXT_EXTENSIONS = new Set(['txt', 'text', 'rst', 'adoc', 'asc']); @@ function isReadableTextPreview(filename: string): boolean { const lowerFilename = filename.toLowerCase(); - const ext = lowerFilename.includes('.') ? lowerFilename.split('.').pop() : ''; - if (ext && READABLE_TEXT_EXTENSIONS.has(ext)) { - return true; - } + const dotIndex = lowerFilename.lastIndexOf('.'); - const basename = lowerFilename.includes('.') - ? lowerFilename.slice(0, lowerFilename.indexOf('.')) - : lowerFilename; - return READABLE_TEXT_BASENAMES.has(basename); + if (dotIndex !== -1) { + const ext = lowerFilename.slice(dotIndex + 1); + return READABLE_TEXT_EXTENSIONS.has(ext); + } + + return READABLE_TEXT_BASENAMES.has(lowerFilename); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/FilePreview.tsx` around lines 191 - 214, Remove 'mdx' from READABLE_TEXT_EXTENSIONS and update isReadableTextPreview so extension presence takes precedence: if a filename has an extension, only check READABLE_TEXT_EXTENSIONS (don't fall back to READABLE_TEXT_BASENAMES); only when there is no extension should you check READABLE_TEXT_BASENAMES. This ensures files like README.ts or TODO.js are routed to syntax/MDX renderers (e.g., BionifyTextBlock should not handle .ts/.js files) while still allowing basename-only files (like "README") to be treated as readable text.
🧹 Nitpick comments (1)
src/__tests__/renderer/components/AutoRun.test.tsx (1)
249-253: Reset the Bionify setting for every suite, not onlydescribe('AutoRun').Line 252 only covers tests nested under this first top-level
describe; the rest of this file also rendersAutoRunand can inherit mutated Zustand state when run in isolation or after future tests. Move this to a file-levelbeforeEachor shared setup.♻️ Proposed test isolation cleanup
+beforeEach(() => { + useSettingsStore.setState({ bionifyReadingMode: false }); +}); + describe('AutoRun', () => { let mockMaestro: ReturnType<typeof setupMaestroMock>; beforeEach(() => { mockMaestro = setupMaestroMock(); vi.useFakeTimers({ shouldAdvanceTime: true }); - useSettingsStore.setState({ bionifyReadingMode: false }); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/renderer/components/AutoRun.test.tsx` around lines 249 - 253, The test mutates Zustand state only inside a nested describe; move the call resetting settings—useSettingsStore.setState({ bionifyReadingMode: false })—out of the nested beforeEach and into a file-level (top-level) beforeEach or shared test setup so every test in this file always gets a fresh bionifyReadingMode=false state; keep other setup (mockMaestro = setupMaestroMock() and vi.useFakeTimers(...)) where they belong or replicate if different suites need them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/renderer/components/AutoRun.tsx`:
- Around line 1380-1382: The effect that calls
setPreviewBionifyReadingMode(bionifyReadingMode) only depends on
bionifyReadingMode and selectedFile, so a local override can persist when
switching AutoRun folders/sessions with the same selectedFile; update the
useEffect that contains setPreviewBionifyReadingMode to also listen for the
AutoRun folder/session state (add the relevant state variables, e.g.,
selectedAutoRunFolder and/or selectedAutoRunSession) and reset the local toggle
by including those variables in the dependency array so the preview override is
reinitialized whenever the AutoRun folder or session changes.
---
Outside diff comments:
In `@src/__tests__/renderer/components/AutoRun.test.tsx`:
- Around line 96-120: Add a regression test in AutoRun.test.tsx that enables
bionify (simulate clicking the toggle or setting bionifyEnabled) and runs the
preview search flow using the real Bionify rendering path (avoid the mocked
react-markdown) to ensure enableBionifyReadingMode is forwarded to the preview
renderer; perform a search for a term like "Reading" and assert the
highlighted/match count is correct using the same approach as the
FilePreview.test.tsx example around line 352, verifying that
applyReadableTextTransforms / BionifyText transformations are actually applied
and do not break search highlighting.
In `@src/renderer/components/AutoRun.tsx`:
- Around line 1457-1517: Define a local hasActiveSearch boolean (e.g. const
hasActiveSearch = searchOpen && searchQuery.trim() && totalMatches > 0) and use
it to disable Bionify when search is active by changing both
createMarkdownComponents calls (the ones inside baseMarkdownComponents and
searchHighlightedComponents) to set enableBionifyReadingMode:
previewBionifyReadingMode && !hasActiveSearch; this keeps Bionify off during
active AutoRun search while preserving the existing behavior otherwise.
---
Duplicate comments:
In `@src/renderer/components/FilePreview.tsx`:
- Around line 191-214: Remove 'mdx' from READABLE_TEXT_EXTENSIONS and update
isReadableTextPreview so extension presence takes precedence: if a filename has
an extension, only check READABLE_TEXT_EXTENSIONS (don't fall back to
READABLE_TEXT_BASENAMES); only when there is no extension should you check
READABLE_TEXT_BASENAMES. This ensures files like README.ts or TODO.js are routed
to syntax/MDX renderers (e.g., BionifyTextBlock should not handle .ts/.js files)
while still allowing basename-only files (like "README") to be treated as
readable text.
---
Nitpick comments:
In `@src/__tests__/renderer/components/AutoRun.test.tsx`:
- Around line 249-253: The test mutates Zustand state only inside a nested
describe; move the call resetting settings—useSettingsStore.setState({
bionifyReadingMode: false })—out of the nested beforeEach and into a file-level
(top-level) beforeEach or shared test setup so every test in this file always
gets a fresh bionifyReadingMode=false state; keep other setup (mockMaestro =
setupMaestroMock() and vi.useFakeTimers(...)) where they belong or replicate if
different suites need them.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0c71890d-0cb1-436d-92fe-bb48f45759dc
⛔ Files ignored due to path filters (1)
docs/screenshots/bionify-button-reference-annotated.pngis excluded by!**/*.png
📒 Files selected for processing (5)
src/__tests__/renderer/components/AutoRun.test.tsxsrc/__tests__/renderer/components/FilePreview.test.tsxsrc/renderer/components/AutoRun.tsxsrc/renderer/components/AutoRunDocumentSelector.tsxsrc/renderer/components/FilePreview.tsx
|
Fresh screenshots from the latest branch head are now up and show the new surface-level Bionify buttons in the implemented UI. Updated assets:
This push also included the AutoRun/FilePreview search guard follow-up so Bionify is disabled while preview search is active. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/__tests__/renderer/components/AutoRun.test.tsx (1)
56-59:⚠️ Potential issue | 🟡 MinorAdd test coverage for Bionify + search interaction in preview mode.
The test at lines 279–288 only verifies that the Bionify toggle changes button state. The Preview Mode with Search test suite (starting line 2256) lacks a test that enables Bionify, opens preview search, and asserts that search matches remain findable. FilePreview.test.tsx includes a similar test ("disables Bionify spans while search is active…" at line 352), confirming the pattern: when search is active,
enableBionifyReadingModeshould be passed asfalseto prevent readable-text transforms from breaking match discovery. Add a test to AutoRun's search suite that enables Bionify, opens preview search with a query that would be split by Bionify (e.g., "information"), and verifies either that matches are still found or that the markdown renderer receivesenableBionifyReadingMode={false}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/renderer/components/AutoRun.test.tsx` around lines 56 - 59, Add a new test inside the "Preview Mode with Search" suite in AutoRun.test.tsx that flips the Bionify toggle on, opens the preview search UI, types a query like "information" (which would be split by Bionify), and then asserts that search still finds matches or that the markdown renderer received enableBionifyReadingMode={false}; locate the Bionify toggle control (the same control used in the existing Bionify toggle test), the preview search open action, and the markdown renderer mock (react-markdown mock returning <div data-testid="react-markdown">) to inspect props or query results, and mirror the pattern used in FilePreview.test.tsx's "disables Bionify spans while search is active…" test to ensure the test toggles state, triggers search, and verifies either found matches or that enableBionifyReadingMode was passed false to the renderer.
🧹 Nitpick comments (2)
src/renderer/components/FilePreview.tsx (1)
2448-2460:codeContainerRefattachment here is a no-op for readable-text.The DOM-based search highlighter short-circuits when
isReadableTextis true (see lines 1260–1272), socodeContainerRefis never read in this branch. The ref attachment here is harmless but misleading — readers will assume the code-path search uses this container. The active highlighting container ismarkdownContainerRef(attached toBionifyTextBlock), which the CSS Custom Highlight API effect walks.♻️ Minor cleanup
- ) : isReadableText && !markdownEditMode ? ( - <div ref={codeContainerRef}> - {truncationBanner} - <BionifyTextBlock + ) : isReadableText && !markdownEditMode ? ( + <div> + {truncationBanner} + <BionifyTextBlock ref={markdownContainerRef} className="prose prose-sm max-w-none whitespace-pre-wrap break-words" style={{ color: theme.colors.textMain }} enabled={effectiveBionifyReadingMode} restOpacity={theme.mode === 'light' ? 0.9 : 0.96} > {displayContent} </BionifyTextBlock> </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/FilePreview.tsx` around lines 2448 - 2460, The code attaches codeContainerRef to the outer div in the readable-text branch even though DOM-based highlighting short-circuits for isReadableText and the highlighting actually uses markdownContainerRef on BionifyTextBlock; remove the misleading/no-op ref attachment or replace it with the correct markdownContainerRef so readers and future maintainers aren't confused—update the JSX in the isReadableText && !markdownEditMode branch to not bind codeContainerRef (or bind markdownContainerRef) and ensure BionifyTextBlock keeps markdownContainerRef for the CSS Custom Highlight API effect.src/__tests__/renderer/components/FilePreview.test.tsx (1)
316-439: Good coverage for the new readable-text + Bionify paths.The new suite correctly exercises the P1 regression from the PR thread (Bionify + active search producing zero matches), plus
.mdxmarkdown routing and theREADME.tsbasename edge case that a previous review called out. A couple of optional hardening ideas if you want to tighten it further:
- The search-while-bionified test asserts
document.querySelector('.bionify-word')is null, which confirms Bionify is off, but doesn't directly assert that search found both occurrences end-to-end. ThewaitFor(() => expect(screen.getByText('1/2')))check is a good proxy; consider also assertingtotalMatchesvia the visible1/2indicator both for an unsplit word ("reading") and a word that would have been split by Bionify across text nodes, to lock in the root-cause fix.- The truncation test uses
'y'.repeat(200 * 1024)which is a single word with no spaces; Bionify's per-word pattern likely won't emit.bionify-wordspans for this input regardless of toggle state, so it validates the banner flow but not the interaction between truncation and Bionify. A short-ish but truncation-triggering content with real words would cover both.- For
does not treat files with code extensions as readable-text basenames, also considerLICENSE.py/TODO.jssince those are the exact cases discussed in the earlier review thread.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/renderer/components/FilePreview.test.tsx` around lines 316 - 439, The tests exercise readable-text/Bionify behavior but should also assert search match counts and use realistic truncation content: update the "disables Bionify spans while search is active" test (the FilePreview render with initialSearchQuery="reading") to additionally assert the visible match indicator shows "1/2" for both a plain token ("reading") and a token that would be split by Bionify, by checking the match counter element used by the component; change the truncation test that builds largeContent in FilePreview (currently 'y'.repeat(...)) to use a long paragraph of real words that still exceeds the truncation threshold so Bionify/span behavior can be observed when loading full file; and add small extra cases similar to the README.ts test to cover basenames like "LICENSE.py" and "TODO.js" to ensure they route to the syntax highlighter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/renderer/components/FilePreview.tsx`:
- Around line 892-894: The useEffect that calls setSurfaceBionifyReadingMode
currently depends on bionifyReadingMode and file?.path, causing the local toggle
to be reset whenever the global bionifyReadingMode changes; update the
dependency array to remove bionifyReadingMode and keep only file?.path so the
local state initializes from the global value on file load but is not
overwritten by later global changes (modify the useEffect containing
setSurfaceBionifyReadingMode to depend only on file?.path).
---
Outside diff comments:
In `@src/__tests__/renderer/components/AutoRun.test.tsx`:
- Around line 56-59: Add a new test inside the "Preview Mode with Search" suite
in AutoRun.test.tsx that flips the Bionify toggle on, opens the preview search
UI, types a query like "information" (which would be split by Bionify), and then
asserts that search still finds matches or that the markdown renderer received
enableBionifyReadingMode={false}; locate the Bionify toggle control (the same
control used in the existing Bionify toggle test), the preview search open
action, and the markdown renderer mock (react-markdown mock returning <div
data-testid="react-markdown">) to inspect props or query results, and mirror the
pattern used in FilePreview.test.tsx's "disables Bionify spans while search is
active…" test to ensure the test toggles state, triggers search, and verifies
either found matches or that enableBionifyReadingMode was passed false to the
renderer.
---
Nitpick comments:
In `@src/__tests__/renderer/components/FilePreview.test.tsx`:
- Around line 316-439: The tests exercise readable-text/Bionify behavior but
should also assert search match counts and use realistic truncation content:
update the "disables Bionify spans while search is active" test (the FilePreview
render with initialSearchQuery="reading") to additionally assert the visible
match indicator shows "1/2" for both a plain token ("reading") and a token that
would be split by Bionify, by checking the match counter element used by the
component; change the truncation test that builds largeContent in FilePreview
(currently 'y'.repeat(...)) to use a long paragraph of real words that still
exceeds the truncation threshold so Bionify/span behavior can be observed when
loading full file; and add small extra cases similar to the README.ts test to
cover basenames like "LICENSE.py" and "TODO.js" to ensure they route to the
syntax highlighter.
In `@src/renderer/components/FilePreview.tsx`:
- Around line 2448-2460: The code attaches codeContainerRef to the outer div in
the readable-text branch even though DOM-based highlighting short-circuits for
isReadableText and the highlighting actually uses markdownContainerRef on
BionifyTextBlock; remove the misleading/no-op ref attachment or replace it with
the correct markdownContainerRef so readers and future maintainers aren't
confused—update the JSX in the isReadableText && !markdownEditMode branch to not
bind codeContainerRef (or bind markdownContainerRef) and ensure BionifyTextBlock
keeps markdownContainerRef for the CSS Custom Highlight API effect.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 502e7421-ec26-4b41-b6af-3605c4062fcf
⛔ Files ignored due to path filters (2)
docs/screenshots/bionify-autorun.pngis excluded by!**/*.pngdocs/screenshots/bionify-file-preview.pngis excluded by!**/*.png
📒 Files selected for processing (4)
src/__tests__/renderer/components/AutoRun.test.tsxsrc/__tests__/renderer/components/FilePreview.test.tsxsrc/renderer/components/AutoRun.tsxsrc/renderer/components/FilePreview.tsx
|
Fresh highlighted screenshots from the latest branch head, with temporary red outlines/labels on the actual implemented Bionify buttons:
These are newly generated from the updated app, not reused from previous screenshots. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/components/FilePreview.tsx (1)
1448-1477:⚠️ Potential issue | 🟡 MinorCount fallback search matches from rendered text, not
file.content.When CSS Custom Highlight is unavailable, this fallback counts matches in the full
file.content, but readable-text previews may render onlydisplayContentwhen truncated. That can show matches the user cannot navigate to.🐛 Proposed fix
} else { // Fallback: count matches and scroll to location (no highlighting) - const matches = file?.content?.match(searchRegex); - const count = matches ? matches.length : 0; + const textNodes: Text[] = []; + const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT); + let textNode; + while ((textNode = walker.nextNode())) { + textNodes.push(textNode as Text); + } + + const count = textNodes.reduce((total, node) => { + const text = node.textContent || ''; + const nodeRegex = new RegExp(escapedQuery, 'gi'); + return total + Array.from(text.matchAll(nodeRegex)).length; + }, 0); setTotalMatches(count); if (count > 0) { - const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT); let matchCount = 0; const targetIndex = Math.max(0, Math.min(currentMatchIndex, count - 1)); - let textNode; - while ((textNode = walker.nextNode())) { + for (const textNode of textNodes) { const text = textNode.textContent || ''; - const nodeMatches = text.match(searchRegex); - if (nodeMatches) { - for (const _ of nodeMatches) { - if (matchCount === targetIndex) { - const parentElement = (textNode as Text).parentElement; - if (parentElement) { - parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - return; + const nodeRegex = new RegExp(escapedQuery, 'gi'); + let match; + while ((match = nodeRegex.exec(text)) !== null) { + if (matchCount === targetIndex) { + const parentElement = textNode.parentElement; + if (parentElement) { + parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } - matchCount++; + return; } + matchCount++; } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/FilePreview.tsx` around lines 1448 - 1477, The fallback match-count and navigation logic currently uses file.content which can differ from what is rendered (displayContent), so update the logic in the fallback branch (the block using searchRegex, setTotalMatches, currentMatchIndex and the document.createTreeWalker(container)) to compute matches from the rendered DOM/text instead of file.content: derive the text to count from the container's visible text (e.g., concatenating text nodes or using container.textContent/innerText) to setTotalMatches and use that same rendered-text matching when calculating targetIndex and walking text nodes for scrollIntoView so counts and navigation align with what the user actually sees.
🧹 Nitpick comments (1)
src/renderer/components/AutoRun.tsx (1)
1843-1846: Optional: consider which Bionify state to surface in the selector label during an active search.
bionifyEnabled={previewBionifyReadingMode}reflects the user's intent, but during an active preview searcheffectivePreviewBionifyReadingModeis forced tofalse, so the button can read "Bionify On" while the preview is actually rendering un-bionified text. If the toggle is meant to indicate the current render state, passeffectivePreviewBionifyReadingModeinstead; if it's meant to indicate the persisted preference that will re-apply once search closes, the current wiring is correct — in which case a brief tooltip noting that Bionify is temporarily suppressed during search would help users avoid confusion. Not a blocker either way.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/AutoRun.tsx` around lines 1843 - 1846, The selector currently passes bionifyEnabled={previewBionifyReadingMode} which shows the persisted preference, but during an active preview search the actual rendering uses effectivePreviewBionifyReadingMode (forced false) causing mismatch; either change the prop to bionifyEnabled={effectivePreviewBionifyReadingMode} so the button reflects the real render state, or keep the current prop but add a short tooltip (e.g., via the component's title/tooltip prop) explaining “Bionify is temporarily suppressed during active search” so users know the preference is applied only after search closes; update the JSX where bionifyEnabled, previewBionifyReadingMode, effectivePreviewBionifyReadingMode and setPreviewBionifyOverride are used accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/renderer/components/FilePreview.tsx`:
- Around line 1448-1477: The fallback match-count and navigation logic currently
uses file.content which can differ from what is rendered (displayContent), so
update the logic in the fallback branch (the block using searchRegex,
setTotalMatches, currentMatchIndex and the document.createTreeWalker(container))
to compute matches from the rendered DOM/text instead of file.content: derive
the text to count from the container's visible text (e.g., concatenating text
nodes or using container.textContent/innerText) to setTotalMatches and use that
same rendered-text matching when calculating targetIndex and walking text nodes
for scrollIntoView so counts and navigation align with what the user actually
sees.
---
Nitpick comments:
In `@src/renderer/components/AutoRun.tsx`:
- Around line 1843-1846: The selector currently passes
bionifyEnabled={previewBionifyReadingMode} which shows the persisted preference,
but during an active preview search the actual rendering uses
effectivePreviewBionifyReadingMode (forced false) causing mismatch; either
change the prop to bionifyEnabled={effectivePreviewBionifyReadingMode} so the
button reflects the real render state, or keep the current prop but add a short
tooltip (e.g., via the component's title/tooltip prop) explaining “Bionify is
temporarily suppressed during active search” so users know the preference is
applied only after search closes; update the JSX where bionifyEnabled,
previewBionifyReadingMode, effectivePreviewBionifyReadingMode and
setPreviewBionifyOverride are used accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 89c96b3e-a638-4857-bbc9-1d78fbb08b84
⛔ Files ignored due to path filters (2)
docs/screenshots/bionify-autorun-highlighted.pngis excluded by!**/*.pngdocs/screenshots/bionify-file-preview-highlighted.pngis excluded by!**/*.png
📒 Files selected for processing (4)
src/__tests__/renderer/components/AutoRun.test.tsxsrc/__tests__/renderer/components/FilePreview.test.tsxsrc/renderer/components/AutoRun.tsxsrc/renderer/components/FilePreview.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/tests/renderer/components/FilePreview.test.tsx
|
Fresh highlighted screenshots from the latest branch head, showing the actual implemented
These are newly generated from the updated branch head and use temporary screenshot-only styling to make both the |

Summary
This PR ships the Bionify reading mode rollout for Maestro's opted-in long-form reading surfaces.
Bionifydisplay setting and shared reading-mode rendering helpers.Verification
npm run validate:pushMAESTRO_WRITE_DURABLE_SCREENSHOTS=true npx playwright test e2e/bionify-reading-mode.spec.tsScreenshot Evidence
File Preview
Auto Run
Supported Surfaces
Intentional Exclusions
Notes
npm run test:integrationsuite still has unrelated pre-existing failures outside the Bionify surfaces, so the gating pass for this feature remains the successful repo validation stack above plus the dedicated Bionify Electron flow.Summary by CodeRabbit
New Features
Tests
Documentation