Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 51 additions & 26 deletions cli/src/components/limited-landing-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ interface LimitedLandingPanelProps {
sessionCounterText: string
/** True when the shared per-day quota is fully spent. Disables the CTA. */
isQuotaExhausted: boolean
/** Plain-text explanation shown instead of the CTA when quota is exhausted. */
exhaustedMessageText: string
/** Max vertical rows the panel may occupy. When its content is taller the
* panel scrolls (scrollbar shown) instead of letting flexbox compress the
* bordered button onto its own border. */
Expand All @@ -42,6 +44,7 @@ export const LimitedLandingPanel: React.FC<LimitedLandingPanelProps> = ({
sessionCounter,
sessionCounterText,
isQuotaExhausted,
exhaustedMessageText,
maxHeight,
}) => {
const theme = useTheme()
Expand All @@ -52,16 +55,22 @@ export const LimitedLandingPanel: React.FC<LimitedLandingPanelProps> = ({

// Rendered height of the panel, matching the JSX below row-for-row so the
// scroll budget is exact: name + warning (each wrap-aware) + the counter
// line with its 1-row top/bottom margins + the 3-row bordered button.
// line with its 1-row top/bottom margins + either the 3-row bordered button
// or the exhausted-quota message.
const exhaustedTitleText = 'Daily session limit reached'
const wrappedRows = (text: string) =>
Math.max(1, Math.ceil(text.length / contentMaxWidth))
const BUTTON_ROWS = 3 // 2 border rows + label
const actionRows = isQuotaExhausted
? wrappedRows(exhaustedTitleText) + wrappedRows(exhaustedMessageText)
: BUTTON_ROWS
const contentHeight =
wrappedRows(model.displayName) +
(model.warning ? wrappedRows(model.warning) : 0) +
1 /* counter marginTop */ +
wrappedRows(sessionCounterText) +
1 /* counter marginBottom */ +
3 /* button: 2 border rows + label */
actionRows
const needsScroll = contentHeight > maxHeight
const viewportHeight = Math.max(1, Math.min(contentHeight, maxHeight))

Expand All @@ -72,14 +81,17 @@ export const LimitedLandingPanel: React.FC<LimitedLandingPanelProps> = ({
// 'center'` on the parent can center the whole block again.
const BUTTON_LABEL = 'Start session Enter'
const BUTTON_CHROME = 6 // 2 border + 4 padding (paddingLeft/Right 2)
const actionWidth = isQuotaExhausted
? Math.max(exhaustedTitleText.length, exhaustedMessageText.length)
: BUTTON_LABEL.length + BUTTON_CHROME
const panelWidth =
Math.min(
contentMaxWidth,
Math.max(
model.displayName.length,
model.warning?.length ?? 0,
sessionCounterText.length,
BUTTON_LABEL.length + BUTTON_CHROME,
actionWidth,
),
) + (needsScroll ? 1 : 0) /* scrollbar gutter */

Expand Down Expand Up @@ -159,30 +171,43 @@ export const LimitedLandingPanel: React.FC<LimitedLandingPanelProps> = ({
>
{sessionCounter}
</text>
<Button
onClick={start}
style={{
borderStyle: 'single',
borderColor: interactable ? theme.primary : theme.border,
paddingLeft: 2,
paddingRight: 2,
flexShrink: 0,
}}
border={['top', 'bottom', 'left', 'right']}
>
<text
style={{ fg: interactable ? theme.foreground : theme.muted }}
attributes={TextAttributes.BOLD}
{isQuotaExhausted ? (
<>
<text style={{ wrapMode: 'word', flexShrink: 0 }}>
<span fg={theme.secondary} attributes={TextAttributes.BOLD}>
{exhaustedTitleText}
</span>
</text>
<text style={{ fg: theme.muted, wrapMode: 'word', flexShrink: 0 }}>
{exhaustedMessageText}
</text>
</>
) : (
<Button
onClick={start}
style={{
borderStyle: 'single',
borderColor: interactable ? theme.primary : theme.border,
paddingLeft: 2,
paddingRight: 2,
flexShrink: 0,
}}
border={['top', 'bottom', 'left', 'right']}
>
{pending ? (
'Starting…'
) : (
<>
Start session<span fg={theme.muted}>{' Enter'}</span>
</>
)}
</text>
</Button>
<text
style={{ fg: interactable ? theme.foreground : theme.muted }}
attributes={TextAttributes.BOLD}
>
{pending ? (
'Starting…'
) : (
<>
Start session<span fg={theme.muted}>{' Enter'}</span>
</>
)}
</text>
</Button>
)}
</scrollbox>
)
}
1 change: 1 addition & 0 deletions cli/src/components/waiting-room-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
{isLanding && accessTier === 'limited' && (
<LimitedLandingPanel
isQuotaExhausted={isPremiumExhausted}
exhaustedMessageText={`You've used your ${sessionLimit} ${sessionLabel} for today. Resets in ${premiumResetCountdown}.`}
maxHeight={limitedPanelMaxHeight}
sessionCounterText={`${formatSessionUnits(
sharedPremiumUsed,
Expand Down
26 changes: 22 additions & 4 deletions freebuff/e2e/tests/help-command.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ describe('Freebuff: --help flag', () => {
describe('Freebuff: /help slash command', () => {
let session: FreebuffSession | null = null

const openHelp = async (session: FreebuffSession): Promise<string | null> => {
const initialOutput = await session.capture()
if (!initialOutput.includes('Enter a coding task')) {
console.log(
'Skipping /help slash command assertion: Freebuff is not on the chat input screen.',
)
return null
}

await session.sendKey('C-u')
for (const key of ['/', 'h', 'e', 'l', 'p']) {
await session.sendKey(key)
}
await session.waitForText('/help', 10_000)
await session.sendKey('Enter')
return session.waitForText('Shortcuts', 10_000)
}

afterEach(async () => {
if (session) {
await session.stop()
Expand All @@ -50,8 +68,8 @@ describe('Freebuff: /help slash command', () => {
session = await FreebuffSession.start(binary)
await session.waitForReady()

await session.send('/help')
const output = await session.capture(2)
const output = await openHelp(session)
if (!output) return

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

await session.send('/help')
const output = await session.capture(2)
const output = await openHelp(session)
if (!output) return

// Freebuff should NOT show these paid/subscription commands
expect(output).not.toContain('/subscribe')
Expand Down
Loading