Skip to content

fix(ui): cleanup estimated string length hacks in composer#23694

Merged
keithguerin merged 2 commits intomainfrom
fix-est-str-len-23573
Mar 25, 2026
Merged

fix(ui): cleanup estimated string length hacks in composer#23694
keithguerin merged 2 commits intomainfrom
fix-est-str-len-23573

Conversation

@keithguerin
Copy link
Copy Markdown
Contributor

Fixes #23573

Summary of Changes

This PR addresses the technical debt outlined in #23573 by significantly refactoring the Composer.tsx status display logic.

  • Refactored Status Row: The complex rendering logic for the status row has been extracted from Composer.tsx into a new dedicated StatusRow component. This improves modularity and maintainability.
  • Accurate String Length Estimation: Arbitrary string length offsets (e.g., +25, +10) previously used for UI layout calculations have been replaced with precise measurements using getCachedStringWidth. This ensures more accurate UI rendering and prevents collisions between status messages and tips.
  • Named Constants: Introduced named constants (STATUS_TIP_MIN_GAP, TIP_COLLISION_BUFFER, and STATUS_INDICATOR_OVERHEAD) to eliminate magic numbers and clarify layout logic.
  • Improved Cleanup: The usePhraseCycler hook now explicitly returns the clearTimers function in its cleanup, ensuring that timers are consistently cleared when the component unmounts.
  • Naming Convention Adherence: Variables related to 'mini mode' (e.g., miniMode_ShowToast) have been renamed to follow camelCase conventions (e.g., showMinimalToast) for consistency.

@keithguerin keithguerin requested a review from a team as a code owner March 24, 2026 19:07
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses technical debt by centralizing and improving the UI's status display logic. It extracts a significant portion of the Composer component's rendering responsibilities into a new, dedicated component, leading to a cleaner and more maintainable codebase. The changes also enhance the accuracy of UI element positioning by replacing heuristic-based string length estimations with precise calculations, thereby preventing visual overlaps and improving the overall user experience.

Highlights

  • Refactored Status Display: The complex status display logic from Composer.tsx has been extracted into a new, dedicated StatusRow component, significantly improving modularity and maintainability.
  • Accurate String Length Estimation: Arbitrary string length offsets previously used for UI layout calculations have been replaced with precise measurements using getCachedStringWidth, ensuring more accurate rendering and preventing UI collisions.
  • Introduced Named Constants: Magic numbers in layout logic have been replaced with named constants (STATUS_TIP_MIN_GAP, TIP_COLLISION_BUFFER, STATUS_INDICATOR_OVERHEAD) for better readability and maintainability.
  • Improved Hook Cleanup: The usePhraseCycler hook now explicitly returns the clearTimers function in its cleanup, guaranteeing that timers are consistently cleared when the component unmounts.
  • Naming Convention Adherence: Variables related to 'mini mode' (e.g., miniMode_ShowToast) have been renamed to follow camelCase conventions (e.g., showMinimalToast) for consistency.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Comment thread packages/cli/src/ui/components/StatusRow.tsx Fixed
@gemini-cli gemini-cli Bot added the area/core Issues related to User Interface, OS Support, Core Functionality label Mar 24, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 24, 2026

Size Change: -3.93 kB (-0.01%)

Total Size: 26.3 MB

Filename Size Change
./bundle/chunk-EOSW57B5.js 0 B -14.7 MB (removed) 🏆
./bundle/chunk-FFC3BYG4.js 0 B -3.4 kB (removed) 🏆
./bundle/chunk-PILHHCW4.js 0 B -3.64 MB (removed) 🏆
./bundle/chunk-QWZ2ZTVN.js 0 B -1.96 MB (removed) 🏆
./bundle/core-674ECGWC.js 0 B -43.6 kB (removed) 🏆
./bundle/devtoolsService-4Z7OM23Q.js 0 B -27.7 kB (removed) 🏆
./bundle/gemini-NWB4M6PU.js 0 B -528 kB (removed) 🏆
./bundle/interactiveCli-2QFCS5HI.js 0 B -1.62 MB (removed) 🏆
./bundle/oauth2-provider-GVO62VOZ.js 0 B -9.16 kB (removed) 🏆
./bundle/chunk-2GAI333X.js 14.6 MB +14.6 MB (new file) 🆕
./bundle/chunk-GWQRIYMB.js 1.96 MB +1.96 MB (new file) 🆕
./bundle/chunk-KWE7ZXE6.js 3.4 kB +3.4 kB (new file) 🆕
./bundle/chunk-USZSAHPP.js 3.64 MB +3.64 MB (new file) 🆕
./bundle/core-GD6JHKUS.js 43.5 kB +43.5 kB (new file) 🆕
./bundle/devtoolsService-GCPK6B2T.js 27.7 kB +27.7 kB (new file) 🆕
./bundle/gemini-DWZ7OTSK.js 528 kB +528 kB (new file) 🆕
./bundle/interactiveCli-AU6UZGJQ.js 1.62 MB +1.62 MB (new file) 🆕
./bundle/oauth2-provider-WAKFXIV5.js 9.16 kB +9.16 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size Change
./bundle/chunk-34MYV7JD.js 2.45 kB 0 B
./bundle/chunk-5AUYMPVF.js 858 B 0 B
./bundle/chunk-664ZODQF.js 124 kB 0 B
./bundle/chunk-DAHVX5MI.js 206 kB 0 B
./bundle/chunk-IUUIT4SU.js 56.5 kB 0 B
./bundle/chunk-RJTRUG2J.js 39.8 kB 0 B
./bundle/cleanup-E65KVJE3.js 0 B -856 B (removed) 🏆
./bundle/devtools-36NN55EP.js 696 kB 0 B
./bundle/dist-T73EYRDX.js 356 B 0 B
./bundle/gemini.js 2.06 kB 0 B
./bundle/getMachineId-bsd-TXG52NKR.js 1.55 kB 0 B
./bundle/getMachineId-darwin-7OE4DDZ6.js 1.55 kB 0 B
./bundle/getMachineId-linux-SHIFKOOX.js 1.34 kB 0 B
./bundle/getMachineId-unsupported-5U5DOEYY.js 1.06 kB 0 B
./bundle/getMachineId-win-6KLLGOI4.js 1.72 kB 0 B
./bundle/memoryDiscovery-HC34TZH7.js 0 B -922 B (removed) 🏆
./bundle/multipart-parser-KPBZEGQU.js 11.7 kB 0 B
./bundle/node_modules/@google/gemini-cli-devtools/dist/client/main.js 221 kB 0 B
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/_client-assets.js 227 kB 0 B
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/index.js 11.5 kB 0 B
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/types.js 132 B 0 B
./bundle/sandbox-macos-permissive-open.sb 890 B 0 B
./bundle/sandbox-macos-permissive-proxied.sb 1.31 kB 0 B
./bundle/sandbox-macos-restrictive-open.sb 3.36 kB 0 B
./bundle/sandbox-macos-restrictive-proxied.sb 3.56 kB 0 B
./bundle/sandbox-macos-strict-open.sb 4.82 kB 0 B
./bundle/sandbox-macos-strict-proxied.sb 5.02 kB 0 B
./bundle/src-QVCVGIUX.js 47 kB 0 B
./bundle/tree-sitter-7U6MW5PS.js 274 kB 0 B
./bundle/tree-sitter-bash-34ZGLXVX.js 1.84 MB 0 B
./bundle/cleanup-65ZZVUIE.js 856 B +856 B (new file) 🆕
./bundle/memoryDiscovery-GX6HE5X5.js 922 B +922 B (new file) 🆕

compressed-size-action

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the Composer component by extracting the status and tip display logic into a new StatusRow component, improving modularity and maintainability. However, there are security concerns regarding terminal injection in StatusRow.tsx, as LLM-generated content (thought.subject and activeHooks[].name) is rendered without proper sanitization for ANSI escape sequences. Additionally, the logic for calculating isInteractiveShellWaiting and showLoadingIndicator is duplicated in StatusRow.tsx and should be passed down as props from the Composer component.

Comment on lines +137 to +144
const displayNames = userVisibleHooks.map((h) => {
let name = h.name;
if (h.index && h.total && h.total > 1) {
name += ` (${h.index}/${h.total})`;
}
return name;
});
currentLoadingPhrase = `${label}: ${displayNames.join(', ')}`;
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.

security-high high

The StatusRow component renders thought.subject (from the LLM) and activeHooks[].name (from tools) directly in the terminal using ink's Text component. These strings are not sanitized for ANSI escape sequences. An attacker (e.g., via a malicious LLM response or a malicious tool) could inject ANSI escape sequences to spoof the UI, hide information, or potentially exfiltrate data from the terminal. This is a form of "Improper Output Handling" where untrusted LLM-generated content is passed to a sensitive sink (the terminal) without proper sanitization. Use stripUnsafeCharacters or sanitizeForDisplay from textUtils.ts to sanitize these strings before rendering them.

References
  1. User-provided data, such as LLM output or tool names, must always be sanitized before rendering in the UI to prevent injection vulnerabilities, even if believed to be safe upstream. This adheres to the principle of defense-in-depth.

Comment on lines +153 to +163
<LoadingIndicator
inline
showTips={showTips}
showWit={showWit}
errorVerbosity={errorVerbosity}
thought={currentThought}
currentLoadingPhrase={currentLoadingPhrase}
elapsedTime={elapsedTime}
forceRealStatusOnly={false}
wittyPhrase={currentWittyPhrase}
/>
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.

security-high high

The thought object, which contains content generated by the LLM, is passed to the LoadingIndicator without sanitization. If the LLM output contains malicious ANSI escape sequences, it could lead to terminal injection vulnerabilities. It is recommended to sanitize the thought.subject before passing it to the UI components.

References
  1. User-provided data, such as LLM output, must always be sanitized before rendering in the UI to prevent injection vulnerabilities, even if believed to be safe upstream. This adheres to the principle of defense-in-depth.

Comment on lines +182 to +202
const isInteractiveShellWaiting = uiState.currentLoadingPhrase?.includes(
INTERACTIVE_SHELL_WAITING_PHRASE,
);

const showLoadingIndicator =
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
uiState.streamingState === 'responding' &&
!(
uiState.pendingHistoryItems?.some(
(item) =>
item.type === 'tool_group' &&
item.tools.some((t) => t.status === 'awaiting_approval'),
) ||
uiState.commandConfirmationRequest ||
uiState.authConsentRequest ||
(uiState.confirmUpdateExtensionRequests?.length ?? 0) > 0 ||
uiState.loopDetectionConfirmationRequest ||
uiState.quota.proQuotaRequest ||
uiState.quota.validationRequest ||
uiState.customDialog
);
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.

high

The logic for calculating isInteractiveShellWaiting and showLoadingIndicator is duplicated from the Composer component. These values are already available in the parent component and should be passed down as props to avoid redundancy and improve maintainability.

You can refactor this by:

  1. Adding isInteractiveShellWaiting: boolean and showLoadingIndicator: boolean to the StatusRowProps interface.
  2. Removing the duplicated logic from this component.
  3. In Composer.tsx, passing these values as props to the <StatusRow /> component.

This will centralize the logic in one place, making the code easier to maintain.

@keithguerin keithguerin force-pushed the fix-est-str-len-23573 branch from 6f3e28c to 0f1eb6b Compare March 24, 2026 19:56
@keithguerin
Copy link
Copy Markdown
Contributor Author

I've addressed the findings:

  1. Sanitization: Added stripAnsi to sanitize LLM-generated content (thought.subject and h.name) to prevent potential terminal injection.
  2. Duplication: showLoadingIndicator and isInteractiveShellWaiting are now calculated once in Composer.tsx and passed down as props to StatusRow.
  3. Bug Fix: Added missing isContextUsageHigh import in StatusRow.tsx and correctly implemented the context usage bleed-through logic.
  4. Magic Numbers: Replaced remaining magic numbers with named constants in StatusRow.tsx.

Comment thread packages/cli/src/ui/components/StatusRow.tsx Fixed
@keithguerin keithguerin force-pushed the fix-est-str-len-23573 branch from 0f1eb6b to 2f0f0c9 Compare March 24, 2026 20:07
Comment thread packages/cli/src/ui/hooks/usePhraseCycler.ts Fixed
@keithguerin keithguerin force-pushed the fix-est-str-len-23573 branch 2 times, most recently from 8b7e445 to 9e28415 Compare March 24, 2026 22:50
@keithguerin
Copy link
Copy Markdown
Contributor Author

The tests were failing because CodeQL flagged the use of the modulo operator % on a cryptographically secure value as creating a biased random output. To fix this properly:

  1. I introduced a getSecureRandomInt helper in src/ui/utils/randomUtils.ts which wraps node:crypto's randomInt directly. This guarantees a mathematically unbiased distribution natively.
  2. Switched usePhraseCycler to consume this mockable helper.
  3. Updated the tests in usePhraseCycler.test.tsx and useLoadingIndicator.test.tsx to spy on getSecureRandomInt and safely return the middle element for testing purposes, exactly as the previous Math.random mocks behaved.

This satisfies both CodeQL and the existing unit test assertions!

@keithguerin keithguerin force-pushed the fix-est-str-len-23573 branch 2 times, most recently from f009e50 to 8d2d25f Compare March 25, 2026 07:21
@@ -0,0 +1,15 @@
/**
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.

why did this get added. these random numbers do not need to be secure. revert this file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted this file in the latest commit.

@@ -102,7 +103,7 @@ export const usePhraseCycler = (

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.

revert changes in this file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted the randomInt changes here. Standard Math.random() is back, with CodeQL suppression to keep the CI green.

*/

import { useState, useEffect, useRef } from 'react';
import { getSecureRandomInt } from '../utils/randomUtils.js';
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.

revert these changes. they are slop from not understanding that this is not a security issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged. Reverted to Math.random() as requested.

Copy link
Copy Markdown
Contributor

@jacob314 jacob314 left a comment

Choose a reason for hiding this comment

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

Approved once the unrelated randomUtils changes are reverted.

@keithguerin keithguerin force-pushed the fix-est-str-len-23573 branch 2 times, most recently from ddd6322 to 2eaf011 Compare March 25, 2026 18:03
- Replace arbitrary string length offsets (+25, +10) with accurate `getCachedStringWidth` measurements.
- Extract status row rendering logic into a dedicated `StatusRow` component.
- Rename `miniMode_` variables to camelCase to adhere to naming conventions.
- Ensure `clearTimers` is correctly returned from `usePhraseCycler` cleanup.
@keithguerin keithguerin force-pushed the fix-est-str-len-23573 branch from 2eaf011 to 1a1abeb Compare March 25, 2026 18:06
@keithguerin
Copy link
Copy Markdown
Contributor Author

@jacob314 I've addressed your feedback. I have reverted the randomInt and randomUtils.ts changes in a separate commit as requested. I kept the CodeQL suppression comments in usePhraseCycler.ts to prevent the security scanner from blocking the PR again, but it's back to using standard Math.random() for the UI flavor text.

Copy link
Copy Markdown
Contributor

@jacob314 jacob314 left a comment

Choose a reason for hiding this comment

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

lgtm

@keithguerin keithguerin added this pull request to the merge queue Mar 25, 2026
Merged via the queue into main with commit fe92a43 Mar 25, 2026
27 checks passed
@keithguerin keithguerin deleted the fix-est-str-len-23573 branch March 25, 2026 19:30
ProthamD pushed a commit to ProthamD/gemini-cli that referenced this pull request Mar 29, 2026
afanty2021 pushed a commit to afanty2021/gemini-cli that referenced this pull request Apr 4, 2026
warrenzhu25 pushed a commit to warrenzhu25/gemini-cli that referenced this pull request Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cleanup estimated string length hacks in composer.tsx

3 participants