mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
Keep usage overflow chip width stable as hidden count changes (#26873)
The "+N" overflow chip in the status bar changed width whenever its count changed, so the density probe that decides how many usage chips fit kept re-measuring. Reserve the width of the provider-count label with an invisible sizer span, so the chip keeps the same size across counts. - Pass the total provider count into UsageOverflowChip and render it in an invisible, grid-stacked sizer span. - Overlay the visible "+N" label on the same grid cell so the chip width tracks the total, not the hidden subset. - Add an e2e test that loads ten usage providers, shrinks the viewport to collapse them, and checks the chip width stays stable and no "Maximum update depth" or React #185 errors occur.
This commit is contained in:
@@ -95,9 +95,11 @@ export function getUsageTone(p: ProviderRateLimits): UsageTone {
|
||||
*/
|
||||
export function UsageOverflowChip({
|
||||
hidden,
|
||||
providerCount,
|
||||
display
|
||||
}: {
|
||||
hidden: readonly ProviderRateLimits[]
|
||||
providerCount: number
|
||||
display: UsagePercentageDisplay
|
||||
}): React.JSX.Element {
|
||||
const tones = hidden.map(getUsageTone)
|
||||
@@ -128,9 +130,13 @@ export function UsageOverflowChip({
|
||||
value0: names
|
||||
}
|
||||
)}
|
||||
className="inline-flex h-4 items-center rounded-full border border-border px-1.5 text-[11px] font-medium tabular-nums text-foreground data-[tone=urgent]:border-destructive/40 data-[tone=urgent]:text-destructive data-[tone=warning]:border-status-warning-border data-[tone=warning]:text-status-warning data-[usage-collapsed=true]:invisible data-[usage-collapsed=true]:absolute"
|
||||
className="inline-grid h-4 items-center justify-items-center rounded-full border border-border px-1.5 text-[11px] font-medium tabular-nums text-foreground data-[tone=urgent]:border-destructive/40 data-[tone=urgent]:text-destructive data-[tone=warning]:border-status-warning-border data-[tone=warning]:text-status-warning data-[usage-collapsed=true]:invisible data-[usage-collapsed=true]:absolute"
|
||||
>
|
||||
+{Math.max(1, hidden.length)}
|
||||
{/* Why: count changes must not invalidate density measurements and restart probing. */}
|
||||
<span aria-hidden="true" className="invisible col-start-1 row-start-1">
|
||||
+{providerCount}
|
||||
</span>
|
||||
<span className="col-start-1 row-start-1">+{Math.max(1, hidden.length)}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ export function StatusBarSurface({
|
||||
))}
|
||||
{collapseUsage ? (
|
||||
<UsageOverflowChip
|
||||
providerCount={rosterProviders.length}
|
||||
hidden={rosterProviders.filter((p) =>
|
||||
collapsedUsageProviders.includes(p.provider)
|
||||
)}
|
||||
|
||||
@@ -80,7 +80,9 @@ describe('Cursor compact usage headline', () => {
|
||||
expect(getUsageTone(p)).toBe('urgent')
|
||||
expect(getTightestUsageSection(p)?.label).toBe(bucket)
|
||||
expect(getUsageHeadlineSection(p)?.label).toBe('Cursor Models')
|
||||
const overflow = renderToStaticMarkup(<UsageOverflowChip hidden={[p]} display="used" />)
|
||||
const overflow = renderToStaticMarkup(
|
||||
<UsageOverflowChip hidden={[p]} providerCount={1} display="used" />
|
||||
)
|
||||
expect(overflow).toContain('data-tone="urgent"')
|
||||
expect(
|
||||
pickCollapsedUsageChips(
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { ProviderRateLimits } from '../../src/shared/rate-limit-types'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
|
||||
test.use({ seedTestRepo: false })
|
||||
|
||||
test('ten usage providers survive a narrow viewport and expand again', async ({
|
||||
orcaPage: page
|
||||
}, testInfo) => {
|
||||
const densityErrors: string[] = []
|
||||
const captureDensityError = (message: string): void => {
|
||||
if (/Maximum update depth|Minified React error #185/.test(message)) {
|
||||
densityErrors.push(message)
|
||||
}
|
||||
}
|
||||
page.on('pageerror', (error) => captureDensityError(error.message))
|
||||
page.on('console', (message) => captureDensityError(message.text()))
|
||||
|
||||
await page.setViewportSize({ width: 1800, height: 800 })
|
||||
await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
if (!store) {
|
||||
throw new Error('store unavailable')
|
||||
}
|
||||
const limits = (provider: ProviderRateLimits['provider']): ProviderRateLimits => ({
|
||||
provider,
|
||||
session: { usedPercent: 25, windowMinutes: 300, resetsAt: null, resetDescription: null },
|
||||
weekly: null,
|
||||
status: 'ok',
|
||||
updatedAt: Date.now(),
|
||||
error: null
|
||||
})
|
||||
store.setState({
|
||||
statusBarVisible: true,
|
||||
statusBarUsageMode: 'verbose',
|
||||
detectedAgentIds: null,
|
||||
statusBarItems: [
|
||||
'claude',
|
||||
'codex',
|
||||
'gemini',
|
||||
'antigravity',
|
||||
'opencode-go',
|
||||
'kimi',
|
||||
'minimax',
|
||||
'grok',
|
||||
'cursor',
|
||||
'zcode',
|
||||
'resource-usage',
|
||||
'ports',
|
||||
'ssh'
|
||||
],
|
||||
statusBarCompactChangeNoticeDismissed: true,
|
||||
usagePercentageDisplayChangeNoticeDismissed: true,
|
||||
rateLimits: {
|
||||
...store.getState().rateLimits,
|
||||
claude: limits('claude'),
|
||||
codex: limits('codex'),
|
||||
gemini: limits('gemini'),
|
||||
antigravity: limits('antigravity'),
|
||||
opencodeGo: limits('opencode-go'),
|
||||
kimi: limits('kimi'),
|
||||
minimax: limits('minimax'),
|
||||
grok: limits('grok'),
|
||||
cursor: limits('cursor'),
|
||||
zcode: limits('zcode')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const usage = page.getByRole('button', { name: 'Usage', exact: true })
|
||||
const collapsed = usage.locator('[data-usage-chip][data-usage-collapsed=true]')
|
||||
const overflow = usage.locator('[data-usage-more]')
|
||||
await expect(usage).toBeVisible()
|
||||
await expect(usage.locator('[data-usage-chip]')).toHaveCount(10)
|
||||
await expect(collapsed).toHaveCount(0)
|
||||
|
||||
let badgeWidth = 0
|
||||
for (const width of [180, 600, 180, 1800]) {
|
||||
await page.setViewportSize({ width, height: 800 })
|
||||
await expect(usage).toBeVisible()
|
||||
if (width === 180) {
|
||||
await expect(collapsed).toHaveCount(10)
|
||||
await expect(overflow).toBeVisible()
|
||||
await expect.poll(() => overflow.innerText()).toBe('+10')
|
||||
badgeWidth = await overflow.evaluate((element) => element.getBoundingClientRect().width)
|
||||
} else if (width === 600) {
|
||||
await expect(overflow).toBeVisible()
|
||||
await expect.poll(() => overflow.innerText()).toMatch(/^\+[1-9]$/)
|
||||
await expect
|
||||
.poll(() => overflow.evaluate((element) => element.getBoundingClientRect().width))
|
||||
.toBeCloseTo(badgeWidth)
|
||||
} else if (width === 1800) {
|
||||
await expect(collapsed).toHaveCount(0)
|
||||
await expect(overflow).toHaveCount(0)
|
||||
}
|
||||
expect(densityErrors).toEqual([])
|
||||
await testInfo.attach(`status-bar-${width}`, {
|
||||
body: await page.screenshot(),
|
||||
contentType: 'image/png'
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user