Skip to content

Commit 6758b72

Browse files
authored
Improve Freebuff session limit messaging (#721)
1 parent dc3aad6 commit 6758b72

3 files changed

Lines changed: 74 additions & 30 deletions

File tree

cli/src/components/limited-landing-panel.tsx

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ interface LimitedLandingPanelProps {
2424
sessionCounterText: string
2525
/** True when the shared per-day quota is fully spent. Disables the CTA. */
2626
isQuotaExhausted: boolean
27+
/** Plain-text explanation shown instead of the CTA when quota is exhausted. */
28+
exhaustedMessageText: string
2729
/** Max vertical rows the panel may occupy. When its content is taller the
2830
* panel scrolls (scrollbar shown) instead of letting flexbox compress the
2931
* bordered button onto its own border. */
@@ -42,6 +44,7 @@ export const LimitedLandingPanel: React.FC<LimitedLandingPanelProps> = ({
4244
sessionCounter,
4345
sessionCounterText,
4446
isQuotaExhausted,
47+
exhaustedMessageText,
4548
maxHeight,
4649
}) => {
4750
const theme = useTheme()
@@ -52,16 +55,22 @@ export const LimitedLandingPanel: React.FC<LimitedLandingPanelProps> = ({
5255

5356
// Rendered height of the panel, matching the JSX below row-for-row so the
5457
// scroll budget is exact: name + warning (each wrap-aware) + the counter
55-
// line with its 1-row top/bottom margins + the 3-row bordered button.
58+
// line with its 1-row top/bottom margins + either the 3-row bordered button
59+
// or the exhausted-quota message.
60+
const exhaustedTitleText = 'Daily session limit reached'
5661
const wrappedRows = (text: string) =>
5762
Math.max(1, Math.ceil(text.length / contentMaxWidth))
63+
const BUTTON_ROWS = 3 // 2 border rows + label
64+
const actionRows = isQuotaExhausted
65+
? wrappedRows(exhaustedTitleText) + wrappedRows(exhaustedMessageText)
66+
: BUTTON_ROWS
5867
const contentHeight =
5968
wrappedRows(model.displayName) +
6069
(model.warning ? wrappedRows(model.warning) : 0) +
6170
1 /* counter marginTop */ +
6271
wrappedRows(sessionCounterText) +
6372
1 /* counter marginBottom */ +
64-
3 /* button: 2 border rows + label */
73+
actionRows
6574
const needsScroll = contentHeight > maxHeight
6675
const viewportHeight = Math.max(1, Math.min(contentHeight, maxHeight))
6776

@@ -72,14 +81,17 @@ export const LimitedLandingPanel: React.FC<LimitedLandingPanelProps> = ({
7281
// 'center'` on the parent can center the whole block again.
7382
const BUTTON_LABEL = 'Start session Enter'
7483
const BUTTON_CHROME = 6 // 2 border + 4 padding (paddingLeft/Right 2)
84+
const actionWidth = isQuotaExhausted
85+
? Math.max(exhaustedTitleText.length, exhaustedMessageText.length)
86+
: BUTTON_LABEL.length + BUTTON_CHROME
7587
const panelWidth =
7688
Math.min(
7789
contentMaxWidth,
7890
Math.max(
7991
model.displayName.length,
8092
model.warning?.length ?? 0,
8193
sessionCounterText.length,
82-
BUTTON_LABEL.length + BUTTON_CHROME,
94+
actionWidth,
8395
),
8496
) + (needsScroll ? 1 : 0) /* scrollbar gutter */
8597

@@ -159,30 +171,43 @@ export const LimitedLandingPanel: React.FC<LimitedLandingPanelProps> = ({
159171
>
160172
{sessionCounter}
161173
</text>
162-
<Button
163-
onClick={start}
164-
style={{
165-
borderStyle: 'single',
166-
borderColor: interactable ? theme.primary : theme.border,
167-
paddingLeft: 2,
168-
paddingRight: 2,
169-
flexShrink: 0,
170-
}}
171-
border={['top', 'bottom', 'left', 'right']}
172-
>
173-
<text
174-
style={{ fg: interactable ? theme.foreground : theme.muted }}
175-
attributes={TextAttributes.BOLD}
174+
{isQuotaExhausted ? (
175+
<>
176+
<text style={{ wrapMode: 'word', flexShrink: 0 }}>
177+
<span fg={theme.secondary} attributes={TextAttributes.BOLD}>
178+
{exhaustedTitleText}
179+
</span>
180+
</text>
181+
<text style={{ fg: theme.muted, wrapMode: 'word', flexShrink: 0 }}>
182+
{exhaustedMessageText}
183+
</text>
184+
</>
185+
) : (
186+
<Button
187+
onClick={start}
188+
style={{
189+
borderStyle: 'single',
190+
borderColor: interactable ? theme.primary : theme.border,
191+
paddingLeft: 2,
192+
paddingRight: 2,
193+
flexShrink: 0,
194+
}}
195+
border={['top', 'bottom', 'left', 'right']}
176196
>
177-
{pending ? (
178-
'Starting…'
179-
) : (
180-
<>
181-
Start session<span fg={theme.muted}>{' Enter'}</span>
182-
</>
183-
)}
184-
</text>
185-
</Button>
197+
<text
198+
style={{ fg: interactable ? theme.foreground : theme.muted }}
199+
attributes={TextAttributes.BOLD}
200+
>
201+
{pending ? (
202+
'Starting…'
203+
) : (
204+
<>
205+
Start session<span fg={theme.muted}>{' Enter'}</span>
206+
</>
207+
)}
208+
</text>
209+
</Button>
210+
)}
186211
</scrollbox>
187212
)
188213
}

cli/src/components/waiting-room-screen.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
520520
{isLanding && accessTier === 'limited' && (
521521
<LimitedLandingPanel
522522
isQuotaExhausted={isPremiumExhausted}
523+
exhaustedMessageText={`You've used your ${sessionLimit} ${sessionLabel} for today. Resets in ${premiumResetCountdown}.`}
523524
maxHeight={limitedPanelMaxHeight}
524525
sessionCounterText={`${formatSessionUnits(
525526
sharedPremiumUsed,

freebuff/e2e/tests/help-command.e2e.test.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ describe('Freebuff: --help flag', () => {
3636
describe('Freebuff: /help slash command', () => {
3737
let session: FreebuffSession | null = null
3838

39+
const openHelp = async (session: FreebuffSession): Promise<string | null> => {
40+
const initialOutput = await session.capture()
41+
if (!initialOutput.includes('Enter a coding task')) {
42+
console.log(
43+
'Skipping /help slash command assertion: Freebuff is not on the chat input screen.',
44+
)
45+
return null
46+
}
47+
48+
await session.sendKey('C-u')
49+
for (const key of ['/', 'h', 'e', 'l', 'p']) {
50+
await session.sendKey(key)
51+
}
52+
await session.waitForText('/help', 10_000)
53+
await session.sendKey('Enter')
54+
return session.waitForText('Shortcuts', 10_000)
55+
}
56+
3957
afterEach(async () => {
4058
if (session) {
4159
await session.stop()
@@ -50,8 +68,8 @@ describe('Freebuff: /help slash command', () => {
5068
session = await FreebuffSession.start(binary)
5169
await session.waitForReady()
5270

53-
await session.send('/help')
54-
const output = await session.capture(2)
71+
const output = await openHelp(session)
72+
if (!output) return
5573

5674
// Should show shortcuts section
5775
expect(output).toMatch(/shortcut|ctrl|esc/i)
@@ -66,8 +84,8 @@ describe('Freebuff: /help slash command', () => {
6684
session = await FreebuffSession.start(binary)
6785
await session.waitForReady()
6886

69-
await session.send('/help')
70-
const output = await session.capture(2)
87+
const output = await openHelp(session)
88+
if (!output) return
7189

7290
// Freebuff should NOT show these paid/subscription commands
7391
expect(output).not.toContain('/subscribe')

0 commit comments

Comments
 (0)