Skip to content

fix(cli): render assistant <options> blocks as a numbered list in the remote transcript view#197

Open
romerjon wants to merge 3 commits into
happier-dev:devfrom
romerjon:fix/cli-options-rendering
Open

fix(cli): render assistant <options> blocks as a numbered list in the remote transcript view#197
romerjon wants to merge 3 commits into
happier-dev:devfrom
romerjon:fix/cli-options-rendering

Conversation

@romerjon

@romerjon romerjon commented Jul 10, 2026

Copy link
Copy Markdown

Problem

The Happier base system prompt (HAPPIER_BASE_SYSTEM_PROMPT_OPTIONS_V1, packages/protocol/src/prompts/systemPromptBaseV1.ts) instructs agents to append quick-reply options as an <options><option>…</option></options> XML block. The mobile/web app extracts this at markdown render time (apps/ui/sources/components/markdown/parseMarkdownBlock.ts) and shows tappable buttons, and the Gemini backend extracts it at turn end (optionsParser.ts), but the Claude remote-mode terminal transcript view prints the raw XML to the user: formatClaudeMessageForInk (apps/cli/src/ui/messageFormatterInk.ts) passes assistant text blocks and the result summary straight to the Ink message buffer.

Repro

Run happier claude, switch the session to remote mode, and from the app send a prompt that makes the agent ask a clarifying question. The terminal transcript shows the literal <options>…</options> block; the app shows buttons.

Fix

  • Move optionsParser.ts from backends/gemini/utils to utils (it is no longer Gemini-specific; import in runGemini.ts updated).
  • Add formatTextWithOptionsForTerminal(), which replaces a complete options block with a readable Options: 1./2./… list. Incomplete or absent blocks pass through unchanged.
  • Route assistant text + result summaries through it in messageFormatterInk.ts and the parallel messageFormatter.ts.

Behavior on every other surface is unchanged.

How tested

yarn workspace @happier-dev/cli typecheck, test:import-cycles, and vitest are all green; added unit tests for the parser/formatter (src/utils/optionsParser.test.ts) and for formatClaudeMessageForInk via a real MessageBuffer (src/ui/messageFormatterInk.test.ts).

Related leaks noticed while investigating (out of scope, happy to file separately)

  1. ACP providers stream raw deltas into the terminal buffer (acpCommonHandlers.ts), so the XML also shows there — fixing needs a turn-end rewrite of the last buffer message.
  2. Ready-notification preview text (normalizeTurnAssistantText) does not strip the block, so push previews can contain the collapsed XML.
  3. In local/unified-terminal mode, Claude Code's own TUI shows the XML — only avoidable by disabling codingPromptBehaviorV1.responseOptions, a behavior change since the app mirrors local sessions.

AI disclosure (per CONTRIBUTING)

Investigated and implemented with Claude under my direction; I defined the bug, the surface audit, and the fix shape, and verified typecheck + tests on my machine.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Note

Render assistant <options> blocks as numbered lists in the CLI terminal view

  • Adds formatTextWithOptionsForTerminal to optionsParser.ts, which replaces a trailing <options>...</options> block with a numbered list while leaving all preceding text intact.
  • Updates messageFormatterInk.ts to accumulate contiguous text blocks and format them together, ensuring options split across adjacent SDK blocks are handled correctly.
  • Updates runGemini.ts to use segmentTrailingOptions instead of the old parseOptionsFromText, so only trailing options blocks are parsed and removed from the displayed message.
  • Deletes the now-superseded optionsParser.ts from the Gemini backend in favour of the shared utility.
  • Behavioral Change: raw <options> XML is no longer shown in assistant or result messages; only trailing blocks are converted — mid-text or incomplete (still-streaming) blocks are left as-is.

Macroscope summarized 9782746.

Summary by CodeRabbit

  • New Features

    • Improved handling of end-of-message options, presenting them as clear numbered lists in terminal and app messages.
    • Preserved surrounding message text and formatting while removing options markup.
    • Added support for options streamed across multiple text segments.
  • Bug Fixes

    • Prevented incomplete or embedded options blocks from being prematurely reformatted.
    • Empty trailing options blocks are now omitted cleanly.

… remote transcript view

The Happier base system prompt instructs agents to append quick-reply
options as an <options>...</options> XML block. The mobile/web app parses
this at markdown render time and shows tappable buttons, and the Gemini
backend extracts it at turn end, but the Claude remote-mode terminal
transcript (formatClaudeMessageForInk / formatClaudeMessage) printed the
raw XML to the user.

Move the options parser from backends/gemini/utils to utils (it is no
longer Gemini-specific), add formatTextWithOptionsForTerminal, and route
assistant text and result summaries through it so terminal surfaces show
a readable numbered list instead of raw XML.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

CLI behavior updates

Layer / File(s) Summary
Option parsing and terminal formatting
apps/cli/src/utils/optionsParser.ts, apps/cli/src/utils/optionsParser.test.ts
Adds end-anchored trailing option parsing, XML serialization, terminal list formatting, and tests for complete, incomplete, empty, split, and non-trailing blocks.
Message renderer integration
apps/cli/src/ui/messageFormatterInk.ts, apps/cli/src/ui/messageFormatterInk.test.ts
Ink combines contiguous assistant text before formatting options and formats successful result summaries, with corresponding coverage.
Gemini response integration
apps/cli/src/backends/gemini/runGemini.ts
Gemini uses shared option segmentation to build app messages and detect incomplete trailing options.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AssistantOutput
  participant segmentTrailingOptions
  participant formatTextWithOptionsForTerminal
  participant InkTerminal
  AssistantOutput->>segmentTrailingOptions: provide accumulated text
  segmentTrailingOptions->>formatTextWithOptionsForTerminal: return prose and trailing options
  formatTextWithOptionsForTerminal->>InkTerminal: render numbered Options list
Loading

Suggested reviewers: leeroybrun

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly states the main change: rendering assistant blocks as a numbered list in the remote transcript view.
Description check ✅ Passed It includes problem, fix, repro, testing, and notes; missing exact template headings, screenshots, and checklist details but is mostly complete.
✨ 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.

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes Claude terminal transcripts render assistant options as readable text. The main changes are:

  • Moved the CLI options parser into shared utilities.
  • Added terminal formatting for <options> blocks.
  • Applied the formatter to Claude assistant text and result summaries.
  • Added parser and Ink formatter tests.

Confidence Score: 5/5

The changed flow looks mergeable after small cleanup to option-block edge cases.

  • The import move uses a dependency-free utility and does not introduce an obvious build or load problem.
  • Single complete option blocks are handled on the changed terminal paths.
  • Multiple option blocks and split assistant text blocks can still show raw XML in the transcript.

apps/cli/src/utils/optionsParser.ts and apps/cli/src/ui/messageFormatterInk.ts

Important Files Changed

Filename Overview
apps/cli/src/utils/optionsParser.ts Moved the parser into shared utilities and added terminal list rendering, with a remaining edge case for multiple option blocks.
apps/cli/src/ui/messageFormatterInk.ts Applies terminal options formatting to Ink transcript messages, with a remaining edge case for split text blocks.
apps/cli/src/ui/messageFormatter.ts Applies terminal options formatting to the non-Ink Claude message formatter.
apps/cli/src/backends/gemini/runGemini.ts Updates the Gemini backend import to the moved parser location.
apps/cli/src/utils/optionsParser.test.ts Adds focused tests for parser extraction and terminal formatting.
apps/cli/src/ui/messageFormatterInk.test.ts Adds Ink formatter tests for assistant text and result summaries.

Reviews (1): Last reviewed commit: "fix(cli): render assistant <options> blo..." | Re-trigger Greptile

Comment thread apps/cli/src/utils/optionsParser.ts Outdated
* @returns The text with the options block rendered as a numbered list
*/
export function formatTextWithOptionsForTerminal(text: string): string {
const { text: textWithoutOptions, options } = parseOptionsFromText(text);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Later Option Blocks Leak

When assistant or result text contains two complete <options> blocks, parseOptionsFromText() only matches and removes the first one. The terminal formatter then renders the first block as a list but leaves the later block as raw XML, so the transcript still shows the markup this change is meant to hide.

Comment thread apps/cli/src/ui/messageFormatterInk.ts Outdated
for (const block of assistantMsg.message.content) {
if (block.type === 'text') {
messageBuffer.addMessage(block.text || '', 'assistant')
messageBuffer.addMessage(formatTextWithOptionsForTerminal(block.text || ''), 'assistant')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Split Text Blocks Leak

This formats each assistant text block on its own, so a complete options block is only recognized when <options> and </options> are in the same block.text. If the SDK message carries adjacent text blocks where the options XML is split across them, both fragments pass through unchanged and the remote transcript still displays raw XML.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
apps/cli/src/ui/messageFormatterInk.test.ts (1)

6-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding message.role to the assistant fixture to avoid the unknown cast.

buildAssistantMessage casts through unknown because the fixture is missing message.role: 'assistant' required by SDKAssistantMessage. Adding the field would let the cast be simplified to as SDKAssistantMessage or removed entirely with a typed builder.

♻️ Optional typed fixture improvement
 function buildAssistantMessage(text: string): SDKMessage {
     return {
         type: 'assistant',
         message: {
+            role: 'assistant',
             content: [{ type: 'text', text }],
         },
-    } as unknown as SDKAssistantMessage
+    } as SDKAssistantMessage
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/ui/messageFormatterInk.test.ts` around lines 6 - 13, Update
buildAssistantMessage to include message.role: 'assistant' in the fixture, then
remove the unnecessary unknown cast and use a direct SDKAssistantMessage type
assertion or a properly typed return value.
apps/cli/src/ui/messageFormatterInk.ts (1)

76-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align null-handling style with messageFormatter.ts for consistency.

Line 76 uses block.text || '' while the equivalent in messageFormatter.ts line 80 uses block.text ?? ''. For string | undefined both are equivalent, but ?? is more precise (preserves empty strings). Line 99's resultMsg.result || '' is redundant since the surrounding if ('result' in resultMsg && resultMsg.result) already narrows to truthy stringmessageFormatter.ts line 103 correctly omits the fallback.

♻️ Optional consistency fix
-                    messageBuffer.addMessage(formatTextWithOptionsForTerminal(block.text || ''), 'assistant')
+                    messageBuffer.addMessage(formatTextWithOptionsForTerminal(block.text ?? ''), 'assistant')
-                    messageBuffer.addMessage(formatTextWithOptionsForTerminal(resultMsg.result || ''), 'result')
+                    messageBuffer.addMessage(formatTextWithOptionsForTerminal(resultMsg.result), 'result')

Also applies to: 99-99

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/ui/messageFormatterInk.ts` at line 76, Update message formatting
in the relevant handlers to match messageFormatter.ts: replace block.text || ''
with block.text ?? '', and remove the redundant || '' fallback from
resultMsg.result after the existing truthiness check. Use the surrounding
message-buffer calls to locate both changes.
apps/cli/src/utils/optionsParser.test.ts (1)

59-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a test for text after the options block.

The tests cover text before options, options-only, no options, and incomplete blocks. A message with text both before and after the <options> block (e.g., "Before\n<options>...</options>\nAfter") isn't tested for formatTextWithOptionsForTerminal. parseOptionsFromText should preserve trailing text via text.replace(optionsRegex, '').trim(), but an explicit test would guard against regressions in the text-assembly logic.

🧪 Suggested additional test
 describe('formatTextWithOptionsForTerminal', () => {
+    it('preserves text after the options block', () => {
+        const input = 'Before text\n<options>\n<option>A</option>\n<option>B</option>\n</options>\nAfter text';
+        const result = formatTextWithOptionsForTerminal(input);
+        expect(result).toBe('Before text\n\nAfter text\n\nOptions:\n  1. A\n  2. B');
+        expect(result).not.toContain('<options>');
+    });
+
     it('returns text unchanged for an incomplete options block', () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/utils/optionsParser.test.ts` around lines 59 - 81, Add a test in
the formatTextWithOptionsForTerminal suite covering text both before and after a
complete <options> block, such as “Before\n<options>...</options>\nAfter”.
Assert the formatted output preserves both surrounding text, inserts the
numbered Options list between them, and removes the original options markup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/cli/src/ui/messageFormatterInk.test.ts`:
- Around line 6-13: Update buildAssistantMessage to include message.role:
'assistant' in the fixture, then remove the unnecessary unknown cast and use a
direct SDKAssistantMessage type assertion or a properly typed return value.

In `@apps/cli/src/ui/messageFormatterInk.ts`:
- Line 76: Update message formatting in the relevant handlers to match
messageFormatter.ts: replace block.text || '' with block.text ?? '', and remove
the redundant || '' fallback from resultMsg.result after the existing truthiness
check. Use the surrounding message-buffer calls to locate both changes.

In `@apps/cli/src/utils/optionsParser.test.ts`:
- Around line 59-81: Add a test in the formatTextWithOptionsForTerminal suite
covering text both before and after a complete <options> block, such as
“Before\n<options>...</options>\nAfter”. Assert the formatted output preserves
both surrounding text, inserts the numbered Options list between them, and
removes the original options markup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 73c4fa6f-e09e-49f3-9bbe-878305bfe7bb

📥 Commits

Reviewing files that changed from the base of the PR and between 212776e and 8f0e245.

📒 Files selected for processing (6)
  • apps/cli/src/backends/gemini/runGemini.ts
  • apps/cli/src/ui/messageFormatter.ts
  • apps/cli/src/ui/messageFormatterInk.test.ts
  • apps/cli/src/ui/messageFormatterInk.ts
  • apps/cli/src/utils/optionsParser.test.ts
  • apps/cli/src/utils/optionsParser.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/cli/src/backends/claude/runClaude.ts (1)

846-857: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use the resolved bridge state before deciding to rebuild

  • nextLocalPermissionBridgeEnabled is still raw meta here, so HAPPIER_LOCAL_PERMISSION_BRIDGE=0/1 can retrigger rebuildLocalPermissionBridge() on unrelated meta churn even when the effective state is unchanged.
  • That disposes/recreates the bridge unnecessarily and can interrupt in-flight permission requests.
  • The same fix is needed in the fast-start path at apps/cli/src/backends/claude/runClaude.ts:1589-1604.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/backends/claude/runClaude.ts` around lines 846 - 857, Resolve
nextLocalPermissionBridgeEnabled through
resolveLocalPermissionBridgeEnabledOverride before comparing it with
localPermissionBridgeEnabled in the shown meta-update path, and use that
effective value for subsequent assignment and rebuild decisions. Apply the same
change in the fast-start path near the existing bridge state initialization so
raw metadata changes masked by HAPPIER_LOCAL_PERMISSION_BRIDGE do not trigger
unnecessary rebuildLocalPermissionBridge calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/cli/src/backends/claude/runClaude.ts`:
- Around line 846-857: Resolve nextLocalPermissionBridgeEnabled through
resolveLocalPermissionBridgeEnabledOverride before comparing it with
localPermissionBridgeEnabled in the shown meta-update path, and use that
effective value for subsequent assignment and rebuild decisions. Apply the same
change in the fast-start path near the existing bridge state initialization so
raw metadata changes masked by HAPPIER_LOCAL_PERMISSION_BRIDGE do not trigger
unnecessary rebuildLocalPermissionBridge calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 889aace1-6645-4dcb-903d-dfd0cbc29c56

📥 Commits

Reviewing files that changed from the base of the PR and between 8f0e245 and 5183076.

📒 Files selected for processing (3)
  • apps/cli/src/backends/claude/remote/claudeRemoteMetaState.ts
  • apps/cli/src/backends/claude/runClaude.ts
  • packages/protocol/src/prompts/systemPromptBaseV1.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 11, 2026
… first (in-place replacement; empty blocks dropped)
@romerjon

Copy link
Copy Markdown
Author

Addressed the multi-block edge case from Greptile's review: formatTextWithOptionsForTerminal now replaces every complete options block in place (empty blocks dropped), with added tests — 17/17 green plus typecheck.

Also force-pushed to remove an unrelated commit that briefly leaked into this branch (prompt/tooling changes preferring the native question tool — those now live on a separate branch and may become a follow-up PR so this one stays a pure rendering fix).

The XML-split-across-streamed-text-blocks case remains out of scope here since it needs turn-end buffering — same category as related leak #1 in the PR body.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 11, 2026
@leeroybrun

leeroybrun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

@romerjon Thank you very much for this contribution — this is a genuinely useful fix, and we really appreciate the care you put into identifying the different surfaces, documenting the repro, adding tests, and following up on the initial review feedback. The core user-facing direction is exactly right: Happier-owned terminal views should show readable numbered choices rather than leaking the raw <options> markup, while the app keeps the raw representation so it can render tappable buttons.

We did a deeper review against the current local worktree and found a few cases we would like to address before merging:

  • formatTextWithOptionsForTerminal() currently matches <options> anywhere in the response, so it can rewrite literal examples or fenced XML/code that the user actually asked Claude to display.
  • Once a block is found, the final .trim() and newline collapsing can change unrelated leading/trailing whitespace and paragraph spacing. Ideally, only the option block itself should change and all surrounding text should be preserved exactly.
  • The remaining Greptile finding about an options block split across adjacent SDK text blocks is valid: because each text block is formatted independently, the raw fragments can still appear in the terminal. We would prefer assembling/processing the complete contiguous assistant text (or doing a turn-final presentation rewrite) before formatting.
  • The new formatter duplicates the block/item regexes already used by parseOptionsFromText, while parseOptionsFromText, hasIncompleteOptions, and the UI parser currently have different behavior for multiple/incomplete blocks. We would like to avoid adding another similar-but-different grammar and instead have one canonical response-options parser/segmenter that the terminal and Gemini adapters can reuse.
  • apps/cli/src/ui/messageFormatter.ts appears to have no active callers; the live path is messageFormatterInk.ts via claudeRemoteLauncher.ts. It would be better not to expand that legacy parallel formatter unless there is a runtime owner we missed.
  • The tests would be stronger with valid typed SDK fixtures (message.role: 'assistant') and coverage for fenced/literal markup, exact whitespace preservation, and options split across text/streaming boundaries.

The happy path and your multi-block follow-up both work, and we did not identify a security, auth, database, or public API concern. This is mainly about making the presentation boundary robust and preventing the options grammar from becoming split across several owners.

Would you be happy either to apply these changes on this PR, or to enable Allow edits from maintainers so we can help apply them directly before merging? maintainerCanModify is currently disabled on the fork PR. We would be very glad to collaborate on the revision, and again, thank you for finding and tackling this — the underlying improvement is absolutely worth landing.

sent by Codex

@romerjon

Copy link
Copy Markdown
Author

Thanks for the thorough review — the direction you're describing is better than what I shipped, and I'm happy to go with it. I've just enabled Allow edits from maintainers on this branch, so please feel free to push directly; I'm also glad to do the revisions myself if you'd rather I take a pass first — whichever is less work on your side.

Agreeing point by point:

  • One canonical parser/segmenter. Fully agree, and this is the right call — the duplicated block/item regexes plus the pre-existing behavior drift between parseOptionsFromText, hasIncompleteOptions, and the UI parser is exactly the kind of thing that shouldn't get a third dialect. A single response-options parser that the terminal and Gemini adapters reuse is the fix; my formatTextWithOptionsForTerminal should consume that rather than re-implement it.
  • Scope the match. Good catch — rewriting fenced/literal <options> the user asked Claude to display is a real bug. Restricting to the turn-final trailing block (the contract the prompt actually specifies) rather than "anywhere in the text" addresses both this and the whitespace concern.
  • Whitespace preservation. Agreed — drop the blanket .trim()/newline-collapse and mutate only the block itself.
  • Split-across-SDK-text-blocks. Agreed this is the correct place to solve it; a turn-final presentation rewrite over the assembled assistant text is cleaner than per-block formatting and also subsumes the multi-block case.
  • messageFormatter.ts. I only touched it for parity and couldn't find a live caller either — happy to leave it untouched (or you can point me at an owner if one exists).
  • Typed fixtures. Will switch to valid role: 'assistant' SDK fixtures and add coverage for fenced markup, exact whitespace, and the streaming-boundary split.

One heads-up for context, not scope-creep: I have a separate local branch that reworks the injected options prompt to prefer a native question tool (AskUserQuestion) with XML only as fallback. I'm deliberately not opening that as a competing PR — it intersects your canonical-parser plan and the prompt contract, so I'd rather raise it as a discussion once this lands and defer to whatever shape you settle on here. Let me know if you'd like me to apply the six revisions on this PR or leave it to you.

…-block segmenter

Addresses maintainer review on happier-dev#197:
- single segmentTrailingOptions() owns the block/item grammar; terminal
  formatter and Gemini adapter both consume it (drop parseOptionsFromText /
  hasIncompleteOptions divergent duplicates)
- match only the turn-final trailing block (end-anchored, last-opener) so
  literal/fenced <options> mid-text is never rewritten
- preserve surrounding whitespace verbatim; only the trailing block span changes
- assemble contiguous SDK text blocks before formatting so an options block
  split across adjacent text fragments is recognised
- revert the dead-code parallel messageFormatter.ts to original
- typed SDKAssistantMessage fixtures; cover fenced markup, exact whitespace,
  empty/unclosed blocks, last-block-only, and split-across-blocks

Gemini app message stays trimmed (byte-identical); terminal path preserves
whitespace. 209 tests green, typecheck + build + import-cycles clean.
@romerjon

Copy link
Copy Markdown
Author

Applied all six revisions in 978274665. Summary:

  • One canonical segmenter. New segmentTrailingOptions() in optionsParser.ts is now the sole owner of the block/item grammar (two module-level regexes). Both the terminal formatter and the Gemini adapter consume it; the divergent parseOptionsFromText/hasIncompleteOptions are removed (grepped apps+packages — only callers were Gemini and the tests, so no wrapper was needed).
  • Turn-final trailing scope. The block regex is end-anchored with a (?!<options>) lookahead so it begins at the last opener and only matches a block at the very end of the text (trailing whitespace tolerated). Literal/fenced <options> earlier in the message is left untouched — covered by explicit tests.
  • Exact whitespace. before is text.slice(0, match.index), returned verbatim; the blanket .trim()/newline-collapse is gone. Only the trailing block span changes.
  • Split across SDK text blocks. messageFormatterInk.ts now accumulates contiguous text blocks into one run and formats once (flushing on any non-text block or end), so an options block spanning the fragment boundary is recognised. New test with two adjacent text blocks that jointly form the block.
  • Dead path. Reverted messageFormatter.ts to its original (no formatter call); it has no live caller.
  • Tests. Typed SDKAssistantMessage fixtures (message.role: 'assistant', via satisfies); coverage for fenced/literal markup, exact leading/trailing whitespace, empty and unclosed trailing blocks, last-block-only, and the split-across-blocks case.

One deliberate call worth flagging: Gemini's app-facing message keeps its previous .trim() (I trim before there) to stay byte-identical for the app; only the terminal path preserves whitespace verbatim. Verified: typecheck clean, 209 tests pass (optionsParser + messageFormatterInk + backends/gemini), build and test:import-cycles green. Maintainer edits are enabled if you'd like to adjust anything directly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/cli/src/utils/optionsParser.ts (1)

79-95: 📐 Maintainability & Code Quality | 🔵 Trivial

Extract a named interface for the segmentation result.

segmentTrailingOptions returns an anonymous inline object type that gets destructured identically in this file (formatTextWithOptionsForTerminal) and in runGemini.ts. A named interface would satisfy the "prefer interface for object shapes"/"prefer explicit exported types" guidelines and give both consumers a shared, documented contract instead of re-deriving the shape at each call site.

As per coding guidelines, apps/cli/**/*.{ts,tsx}: "Prefer interface over type for defining object shapes in TypeScript" and "Prefer explicit exported types and named exports in TypeScript modules."

♻️ Proposed refactor
+export interface TrailingOptionsSegment {
+  before: string;
+  options: string[];
+  hasIncompleteTrailingOptions: boolean;
+}
+
-export function segmentTrailingOptions(text: string): {
-  before: string;
-  options: string[];
-  hasIncompleteTrailingOptions: boolean;
-} {
+export function segmentTrailingOptions(text: string): TrailingOptionsSegment {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/utils/optionsParser.ts` around lines 79 - 95, Define and export
a named interface for the result returned by segmentTrailingOptions, including
before, options, and hasIncompleteTrailingOptions, then use it as the function’s
return type. Preserve the existing return values and update consumers such as
formatTextWithOptionsForTerminal and runGemini.ts to rely on this shared
exported contract.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/cli/src/utils/optionsParser.ts`:
- Around line 79-95: Define and export a named interface for the result returned
by segmentTrailingOptions, including before, options, and
hasIncompleteTrailingOptions, then use it as the function’s return type.
Preserve the existing return values and update consumers such as
formatTextWithOptionsForTerminal and runGemini.ts to rely on this shared
exported contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 775b2d4d-71c4-4d1c-b12a-4fce32afa3d5

📥 Commits

Reviewing files that changed from the base of the PR and between a8e6df2 and 9782746.

📒 Files selected for processing (5)
  • apps/cli/src/backends/gemini/runGemini.ts
  • apps/cli/src/ui/messageFormatterInk.test.ts
  • apps/cli/src/ui/messageFormatterInk.ts
  • apps/cli/src/utils/optionsParser.test.ts
  • apps/cli/src/utils/optionsParser.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cli/src/ui/messageFormatterInk.test.ts

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