Skip to content

Add Bionify reading mode to supported reading surfaces#854

Merged
jeffscottward merged 25 commits intoRunMaestro:mainfrom
jeffscottward:bionify-button
Apr 18, 2026
Merged

Add Bionify reading mode to supported reading surfaces#854
jeffscottward merged 25 commits intoRunMaestro:mainfrom
jeffscottward:bionify-button

Conversation

@jeffscottward
Copy link
Copy Markdown
Contributor

@jeffscottward jeffscottward commented Apr 17, 2026

Summary

This PR ships the Bionify reading mode rollout for Maestro's opted-in long-form reading surfaces.

  • Adds a global Bionify display setting and shared reading-mode rendering helpers.
  • Applies reading emphasis to supported desktop reading surfaces including File Preview, Auto Run, history/detail style markdown readers, and related document-preview flows.
  • Syncs the same global setting into the web/mobile live session surface so supported AI reading views stay aligned with desktop behavior.
  • Preserves the v1 exclusions: terminal output, the normal bottom prompt composer, and code/inline-code content remain un-bionified.

Verification

  • npm run validate:push
  • MAESTRO_WRITE_DURABLE_SCREENSHOTS=true npx playwright test e2e/bionify-reading-mode.spec.ts

Screenshot Evidence

File Preview

Bionify File Preview

Auto Run

Bionify Auto Run

Supported Surfaces

  • Desktop File Preview
  • Desktop Auto Run
  • Desktop markdown-based read views that reuse the shared renderer path
  • Web/mobile live-session reading surfaces fed by the desktop global setting

Intentional Exclusions

  • Terminal output
  • Normal bottom chat input / prompt composer
  • Code fences and inline code spans

Notes

  • Browser/live-session support is driven by the desktop setting broadcast path that now syncs over the web socket connection.
  • The broader npm run test:integration suite 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

    • Reading Mode (Bionify): new global Display setting, per-preview toggle, and surface controls—applies word-emphasis reading transforms to long-form prose across previews, markdown views, editors, mobile and desktop readers while excluding code, terminals and chat input; setting syncs to connected web clients.
  • Tests

    • Extensive unit and end-to-end coverage for rendering, exclusions, sync messages, parsing, and UI toggles.
  • Documentation

    • Release notes Markdown reformatted for consistent styling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a persisted bionifyReadingMode setting and UI toggle, implements Bionify text rendering utilities and styles, threads the flag through desktop and mobile markdown/rendering components, introduces readable-text previews and per-preview overrides, adds WebSocket broadcast/initial-state plumbing for sync, many unit/e2e tests, and minor docs formatting fixes.

Changes

Cohort / File(s) Summary
Docs
docs/releases.md
Normalized Markdown formatting and spacing; no semantic content changes.
Settings metadata & store
src/shared/settingsMetadata.ts, src/renderer/stores/settingsStore.ts, src/renderer/hooks/settings/useSettings.ts
Added bionifyReadingMode metadata, persisted store field, setter, and hook surface.
Settings UI
src/renderer/components/Settings/tabs/DisplayTab.tsx
Added Reading Mode toggle wired to settings API.
Bionify core utils
src/renderer/utils/bionifyReadingMode.tsx
New tokenization/transform functions, CSS generator, BionifyText and BionifyTextBlock exports.
Markdown integration
src/renderer/utils/markdownConfig.ts, src/renderer/components/MarkdownRenderer.tsx
Added enableBionifyReadingMode option, applyReadableTextTransforms, applied transforms to prose nodes, updated table cell renderers and prose style generation.
Preview & editor consumers
src/renderer/components/FilePreview.tsx, src/renderer/components/AutoRun.tsx, src/renderer/components/AutoRunDocumentSelector.tsx, src/renderer/components/Wizard/shared/DocumentEditor.tsx, src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx
Readable-text detection, per-preview Bionify toggle, pass flag into markdown components, updated memo deps and rendering branches.
Various renderer consumers
src/renderer/components/DirectorNotes/AIOverviewTab.tsx, src/renderer/components/HistoryDetailModal.tsx, src/renderer/components/DocumentGraph/DocumentGraphView.tsx
Propagated bionifyReadingMode into MarkdownRenderer usage.
Web / Mobile reading surfaces
src/web/mobile/WebReadingContent.tsx, src/web/mobile/readingContent.ts, src/web/mobile/MobileMarkdownRenderer.tsx, src/web/mobile/ResponseViewer.tsx, src/web/mobile/MessageHistory.tsx, src/web/mobile/App.tsx
Added WebReadingContent adapter and normalization, mobile bionify props, code-block handling moved into MobileCodeBlock, integrated Bionify into mobile markdown rendering.
Desktop app & hooks / WebSocket
src/web/App.tsx, src/web/hooks/useWebSocket.ts, src/web/hooks/useMobileSessionManagement.ts
Desktop theme context extended with bionify state; new bionify_reading_mode server message type and client handler; session-management callback wiring.
Main process & webserver
src/main/ipc/handlers/persistence.ts, src/main/web-server/...
On settings:set for bionifyReadingMode, broadcast change when web clients exist; added WebServer callback getter/setter and broadcast method; wsRoute sends initial state on connect.
Tests — E2E & Unit
e2e/bionify-reading-mode.spec.ts, src/__tests__/**/*
Added Playwright e2e and numerous unit tests covering rendering, exclusions, normalization, broadcast wiring, and updated mocks.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

ready to merge

Suggested reviewers

  • pedramamini
  • reachrazamair

Poem

🐇 I nibbled prose into playful parts,
I wrapped each word with little arts,
Left code and links untouched and bright,
So readers hop through text just right,
A rabbit cheers: happy-reading starts.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Bionify reading mode to supported reading surfaces' directly and clearly summarizes the main objective of the changeset: introducing Bionify reading mode to designated UI surfaces across desktop and web/mobile clients.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 17, 2026

Greptile Summary

This 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 bionifyReadingMode.tsx utility, wires the transform into all targeted desktop surfaces (FilePreview, AutoRun, HistoryDetailModal, DocumentGraph, StreamingDocumentPreview, DocumentEditor), and syncs the setting to the web/mobile client over WebSocket.

  • Bionify word-splitting breaks FilePreview search: when the feature is on, each word is split across two sibling <span> text nodes (bionify-word-emphasis + bionify-word-rest). The CSS Custom Highlight API loop — and the DOM-mutation fallback — search each text node in isolation, so "reading" stored as "Rea"+"ding" returns 0 results. This affects both Markdown and the new readable-text path whenever a search query is active.
  • The BionifyTextBlock wrapper injects an inline <style> tag without forwarding the active theme, so bionify-word-rest opacity is stuck at 0.96 in light mode instead of the intended 0.90; and multiple text segments in WebReadingContent each inject the same CSS block independently.

Confidence Score: 4/5

Safe 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

Filename Overview
src/renderer/components/FilePreview.tsx Adds isReadableText classification and BionifyTextBlock for plain-text files. Has two issues: (1) bionify word-splitting breaks the CSS Custom Highlight API search; (2) .mdx files land in the plain-text path instead of the markdown renderer.
src/renderer/utils/bionifyReadingMode.tsx New utility implementing word-splitting Bionify transforms. Core logic is sound, but BionifyTextBlock injects per-instance style tags without passing the active theme, causing incorrect light-mode opacity and DOM clutter from duplicate injections.
src/renderer/utils/markdownConfig.ts Replaces withHighlight with applyReadableTextTransforms that chains search highlighting and bionify. Theme is correctly forwarded to getBionifyReadingModeStyles.
src/renderer/components/MarkdownRenderer.tsx Adds enableBionifyReadingMode prop and wires withReadableTransforms into all prose component overrides. Implementation is consistent.
src/main/web-server/WebServer.ts Adds getBionifyReadingMode callback and broadcast method. Simplifies resolveWebAssetsPath by removing the isServableWebAssetsPath guard.
src/web/mobile/WebReadingContent.tsx New shared adapter routing content through MobileMarkdownRenderer or BionifyTextBlock segments. Each text segment gets its own BionifyTextBlock with a duplicate style injection.
src/web/mobile/readingContent.ts Clean extraction of LANGUAGE_MAP, parseTextWithCodeBlocks, and normalizeWebReaderContent from ResponseViewer.
src/main/ipc/handlers/persistence.ts Adds bionifyReadingMode broadcast on settings change, correctly guarding on webClientCount.
src/web/hooks/useWebSocket.ts Adds bionify_reading_mode message type, handler, and interface following existing patterns exactly.
src/shared/settingsMetadata.ts Adds bionifyReadingMode setting with correct type, default, and category.

Comments Outside Diff (1)

  1. src/renderer/components/FilePreview.tsx, line 1351-1366 (link)

    P1 Bionify breaks text search — words split across DOM nodes

    When bionify is active, each word is split into two sibling text nodes (bionify-word-emphasis + bionify-word-rest). The CSS Custom Highlight API loop searches each text node independently:

    while ((textNode = walker.nextNode())) {
        const text = textNode.textContent || '';   // "Rea" then "ding" — never "Reading"
        while ((match = localRegex.exec(text)) !== null) { ... }
    }

    A user who enables Bionify then searches for "reading" in a Markdown or readable-text file will get 0 results even though the word appears throughout the content. The same problem applies to the fallback path (line ~1418) and to every other markdown surface that gets enableBionifyReadingMode.

    The simplest fix is to disable bionify when a search query is active:

    // When search is active, skip bionify transforms to keep text nodes intact
    const withReadableTransforms = (children: React.ReactNode) =>
        applyReadableTextTransforms(children, {
            theme,
            enableBionifyReadingMode: enableBionifyReadingMode && !activeSearch,
        });

    Alternatively, the walker could walk the bionify-word parent spans directly and create ranges on their concatenated text content.

Reviews (1): Last reviewed commit: "MAESTRO: prepare bionify reading mode PR" | Re-trigger Greptile

Comment on lines +140 to +154
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>
);
}
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread src/renderer/components/FilePreview.tsx Outdated
Comment on lines +190 to +213
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@pedramamini
Copy link
Copy Markdown
Collaborator

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 (bionify-word-emphasis + bionify-word-rest). The CSS Custom Highlight API loop in src/renderer/components/FilePreview.tsx (~line 1351-1366) and the DOM-mutation fallback (~line 1418) walk text nodes individually, so searching for "reading" against Rea+ding returns 0 hits. Same shape applies to every other markdown surface that gets enableBionifyReadingMode while a search query is active.

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 bionify-word parent span and build ranges from the concatenated text content — that preserves emphasis during search but is more code.

Two smaller items worth a look while you're in there:

  • BionifyTextBlock in src/renderer/utils/bionifyReadingMode.tsx injects an inline <style> per instance without forwarding the active theme, so bionify-word-rest opacity stays at the dark-mode value (0.96) in light mode rather than 0.90. WebReadingContent also re-injects the same block per text segment.
  • .mdx files appear to fall into the plain-text path rather than the markdown renderer in FilePreview — worth confirming that's intentional.

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 | 🟠 Major

Make markdown/readable search match across Bionify word spans.

With Bionify enabled, words are rendered inside .bionify-word spans, but this search path runs the regex against each individual Text node. Multi-word queries like hello world can 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 | 🟠 Major

Fix grouped selector handling for inline wizard Bionify styles.

getBionifyReadingModeStyles() appends .bionify-word to the provided selector. With s = "${scopeSelector}.prose, ${scopeSelector} .prose", the generated CSS becomes a grouped selector where ${scopeSelector}.prose styles the container itself instead of its Bionify descendants. That can leave same-element .prose previews 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: redundant as boolean cast.

settingsStore.get('bionifyReadingMode', false) already infers boolean from the default value parameter (get<T>(key, defaultValue?: T): T). The explicit as boolean cast is unnecessary. Consider settingsStore.get<boolean>('bionifyReadingMode', false) or dropping the cast entirely for consistency with the nearby activeThemeId/webInterfaceUseCustomPort reads.

🤖 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 own describe('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: isMarkdownPreviewable counts all ``` substrings, including nested occurrences.

content.match(/```/g) doesn't distinguish fence openers from closers, so content like ```js\n...\n```\nplus stray ``` in prose would be deemed not previewable and fall through to structured parsing. Given parseTextWithCodeBlocks has 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 every BionifyTextBlock mount/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-block class, 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.ts already handles prose styles separately, so this only changes how the plain-text block variant emits its CSS.

The node.tagName contract is secure. The test preserves markdown task lists and fenced code blocks when enabled verifies with real markdown-rendered fenced code blocks ('- [x] Ship reader tests\n\n```ts\nconst value = 1;\n```') that .bionify-word elements do not appear inside <pre> tags. Inline code elements like <code> have node.type === 'code' which is correctly skipped, and the fallback to nodeProps.node?.tagName provides 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1c50c9b and 9161477.

⛔ Files ignored due to path filters (2)
  • docs/screenshots/bionify-autorun.png is excluded by !**/*.png
  • docs/screenshots/bionify-file-preview.png is excluded by !**/*.png
📒 Files selected for processing (51)
  • docs/releases.md
  • e2e/bionify-reading-mode.spec.ts
  • src/__tests__/main/ipc/handlers/persistence.test.ts
  • src/__tests__/main/web-server/services/broadcastService.test.ts
  • src/__tests__/main/web-server/web-server-factory.test.ts
  • src/__tests__/renderer/components/AgentPromptComposerModal.test.tsx
  • src/__tests__/renderer/components/DirectorNotes/AIOverviewTab.test.tsx
  • src/__tests__/renderer/components/FilePreview.test.tsx
  • src/__tests__/renderer/components/HistoryDetailModal.test.tsx
  • src/__tests__/renderer/components/InlineWizard/StreamingDocumentPreview.test.tsx
  • src/__tests__/renderer/components/MarkdownRenderer.bionify.test.tsx
  • src/__tests__/renderer/components/PromptComposerModal.test.tsx
  • src/__tests__/renderer/components/Wizard/shared/DocumentEditor.test.tsx
  • src/__tests__/renderer/utils/bionifyReadingMode.test.tsx
  • src/__tests__/renderer/utils/markdownConfig.test.ts
  • src/__tests__/web/hooks/useWebSocket.test.ts
  • src/__tests__/web/mobile/App.test.tsx
  • src/__tests__/web/mobile/MessageHistory.test.tsx
  • src/__tests__/web/mobile/MobileMarkdownRenderer.test.tsx
  • src/__tests__/web/mobile/ResponseViewer.test.tsx
  • src/__tests__/web/mobile/readingContent.test.ts
  • src/main/ipc/handlers/persistence.ts
  • src/main/web-server/WebServer.ts
  • src/main/web-server/managers/CallbackRegistry.ts
  • src/main/web-server/routes/wsRoute.ts
  • src/main/web-server/services/broadcastService.ts
  • src/main/web-server/types.ts
  • src/main/web-server/web-server-factory.ts
  • src/renderer/components/AutoRun.tsx
  • src/renderer/components/DirectorNotes/AIOverviewTab.tsx
  • src/renderer/components/DocumentGraph/DocumentGraphView.tsx
  • src/renderer/components/FilePreview.tsx
  • src/renderer/components/HistoryDetailModal.tsx
  • src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx
  • src/renderer/components/MarkdownRenderer.tsx
  • src/renderer/components/Settings/tabs/DisplayTab.tsx
  • src/renderer/components/Wizard/shared/DocumentEditor.tsx
  • src/renderer/hooks/settings/useSettings.ts
  • src/renderer/stores/settingsStore.ts
  • src/renderer/utils/bionifyReadingMode.tsx
  • src/renderer/utils/markdownConfig.ts
  • src/shared/settingsMetadata.ts
  • src/web/App.tsx
  • src/web/hooks/useMobileSessionManagement.ts
  • src/web/hooks/useWebSocket.ts
  • src/web/mobile/App.tsx
  • src/web/mobile/MessageHistory.tsx
  • src/web/mobile/MobileMarkdownRenderer.tsx
  • src/web/mobile/ResponseViewer.tsx
  • src/web/mobile/WebReadingContent.tsx
  • src/web/mobile/readingContent.ts

Comment thread docs/releases.md
- **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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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 Changes

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

Comment thread docs/releases.md

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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Correct typos in user-facing release notes text.

There are a couple of visible copy issues: FIleFile, and built in updaterbuilt-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.

Comment thread docs/releases.md

🧙‍♂️ 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
# 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.

Comment thread docs/releases.md
Comment on lines +439 to 448
• 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 🎨
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +254 to +314
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment thread src/__tests__/renderer/components/AgentPromptComposerModal.test.tsx
Comment thread src/renderer/components/FilePreview.tsx Outdated
Comment thread src/web/mobile/MessageHistory.tsx
Comment thread src/web/mobile/readingContent.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9161477 and 60fcc2b.

📒 Files selected for processing (14)
  • src/__tests__/main/web-server/services/broadcastService.test.ts
  • src/__tests__/renderer/components/AgentPromptComposerModal.test.tsx
  • src/__tests__/renderer/components/FilePreview.test.tsx
  • src/__tests__/renderer/utils/bionifyReadingMode.test.tsx
  • src/__tests__/renderer/utils/markdownConfig.test.ts
  • src/__tests__/web/mobile/MessageHistory.test.tsx
  • src/__tests__/web/mobile/readingContent.test.ts
  • src/main/web-server/web-server-factory.ts
  • src/renderer/components/FilePreview.tsx
  • src/renderer/utils/bionifyReadingMode.tsx
  • src/renderer/utils/markdownConfig.ts
  • src/web/mobile/MessageHistory.tsx
  • src/web/mobile/WebReadingContent.tsx
  • src/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

Comment thread src/renderer/components/FilePreview.tsx Outdated
Comment on lines +61 to +70
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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/renderer/utils/bionifyReadingMode.tsx

Repository: RunMaestro/Maestro

Length of output: 6215


🏁 Script executed:

rg -A 3 -B 3 "renderBionifyText|BionifyText|renderBionifyWord" --type tsx --type ts | head -100

Repository: 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 bionify

Repository: 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()
EOF

Repository: 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()
EOF

Repository: RunMaestro/Maestro

Length of output: 342


🏁 Script executed:

# Search for where BionifyText and BionifyTextBlock are actually used
rg "BionifyText|bionifyReadingMode" --type tsx --type ts -l

Repository: RunMaestro/Maestro

Length of output: 89


🏁 Script executed:

rg "BionifyText|bionifyReadingMode" -l

Repository: 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.tsx

Repository: 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:


🏁 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 -50

Repository: 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 ts

Repository: RunMaestro/Maestro

Length of output: 50377


🏁 Script executed:

# Check the project's target browser support
cat -n tsconfig.json | head -30

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

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

Comment on lines +167 to +177
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +360 to +363
enableBionifyReadingMode={isStdoutBionify}
fontSize={13}
gap="8px"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

@jeffscottward
Copy link
Copy Markdown
Contributor Author

Reference screenshot for the red-highlighted Bionify button placement discussed in review:

Annotated Bionify button reference

Follow-up implemented on this branch:

  • Added a local Bionify toggle in File Preview header actions.
  • Added a local Bionify toggle in the Auto Run document-selector area.
  • Both surfaces initialize from the global Display setting, but can now be toggled in-place from the reader UI.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 | 🟠 Major

Disable Bionify while AutoRun search is active.

FilePreview now guards Bionify with !hasActiveSearch, but AutoRun still passes previewBionifyReadingMode into 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 | 🟠 Major

Add 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 enableBionifyReadingMode is actually forwarded to the preview renderer or that search highlighting works correctly when Bionify is active. Since react-markdown is mocked in the test suite, the actual BionifyText transformations applied via applyReadableTextTransforms() 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 in FilePreview.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 | 🟠 Major

Tighten readable-text routing so code and MDX don’t bypass their renderers.

This still treats .mdx as plain readable text and still lets basenames match before extensions, so files like README.ts or TODO.js can render through BionifyTextBlock instead 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 only describe('AutoRun').

Line 252 only covers tests nested under this first top-level describe; the rest of this file also renders AutoRun and can inherit mutated Zustand state when run in isolation or after future tests. Move this to a file-level beforeEach or 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

📥 Commits

Reviewing files that changed from the base of the PR and between 60fcc2b and 0d30429.

⛔ Files ignored due to path filters (1)
  • docs/screenshots/bionify-button-reference-annotated.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • src/__tests__/renderer/components/AutoRun.test.tsx
  • src/__tests__/renderer/components/FilePreview.test.tsx
  • src/renderer/components/AutoRun.tsx
  • src/renderer/components/AutoRunDocumentSelector.tsx
  • src/renderer/components/FilePreview.tsx

Comment thread src/renderer/components/AutoRun.tsx Outdated
@jeffscottward
Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Add 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, enableBionifyReadingMode should be passed as false to 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 receives enableBionifyReadingMode={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: codeContainerRef attachment here is a no-op for readable-text.

The DOM-based search highlighter short-circuits when isReadableText is true (see lines 1260–1272), so codeContainerRef is 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 is markdownContainerRef (attached to BionifyTextBlock), 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 .mdx markdown routing and the README.ts basename 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. The waitFor(() => expect(screen.getByText('1/2'))) check is a good proxy; consider also asserting totalMatches via the visible 1/2 indicator 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-word spans 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 consider LICENSE.py / TODO.js since 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0d30429 and 9053f30.

⛔ Files ignored due to path filters (2)
  • docs/screenshots/bionify-autorun.png is excluded by !**/*.png
  • docs/screenshots/bionify-file-preview.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • src/__tests__/renderer/components/AutoRun.test.tsx
  • src/__tests__/renderer/components/FilePreview.test.tsx
  • src/renderer/components/AutoRun.tsx
  • src/renderer/components/FilePreview.tsx

Comment thread src/renderer/components/FilePreview.tsx Outdated
@jeffscottward
Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Count 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 only displayContent when 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 search effectivePreviewBionifyReadingMode is forced to false, 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, pass effectivePreviewBionifyReadingMode instead; 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9053f30 and 30ceee3.

⛔ Files ignored due to path filters (2)
  • docs/screenshots/bionify-autorun-highlighted.png is excluded by !**/*.png
  • docs/screenshots/bionify-file-preview-highlighted.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • src/__tests__/renderer/components/AutoRun.test.tsx
  • src/__tests__/renderer/components/FilePreview.test.tsx
  • src/renderer/components/AutoRun.tsx
  • src/renderer/components/FilePreview.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tests/renderer/components/FilePreview.test.tsx

@jeffscottward
Copy link
Copy Markdown
Contributor Author

Fresh highlighted screenshots from the latest branch head, showing the actual implemented B buttons and visibly emphasized Bionify text in every desktop surface that now has a local control:

These are newly generated from the updated branch head and use temporary screenshot-only styling to make both the B button location and the Bionify text effect obvious.

@jeffscottward jeffscottward merged commit dd64e57 into RunMaestro:main Apr 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants