Skip to content

Commit ce47240

Browse files
feat(cli): resume active freebuff session with -c/--continue
Add the -c short flag for --continue and surface a clear "nothing to continue" notice on the landing screen when a session resume is requested but no active session exists. The underlying resume path (auto-takeover of a dead local process's seat plus conversation restore) already preserves the server session and its remaining time.
1 parent 2e67ad0 commit ce47240

6 files changed

Lines changed: 141 additions & 3 deletions

File tree

‎cli/src/__tests__/cli-args.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,37 @@ describe('Freebuff CLI Argument Parsing', () => {
178178
expect(result.command).toBe('login')
179179
expect(result.initialPrompt).toBeNull()
180180
})
181+
182+
test('accepts -c as continue without a conversation id', () => {
183+
const result = parseArgs({
184+
argv: ['node', 'freebuff', '-c'],
185+
isFreebuff: true,
186+
version: '1.0.0',
187+
})
188+
189+
expect(result.continue).toBe(true)
190+
expect(result.continueId).toBeNull()
191+
})
192+
193+
test('accepts --continue as continue without a conversation id', () => {
194+
const result = parseArgs({
195+
argv: ['node', 'freebuff', '--continue'],
196+
isFreebuff: true,
197+
version: '1.0.0',
198+
})
199+
200+
expect(result.continue).toBe(true)
201+
expect(result.continueId).toBeNull()
202+
})
203+
204+
test('accepts -c with an explicit conversation id', () => {
205+
const result = parseArgs({
206+
argv: ['node', 'freebuff', '-c', 'abc-123'],
207+
isFreebuff: true,
208+
version: '1.0.0',
209+
})
210+
211+
expect(result.continue).toBe(true)
212+
expect(result.continueId).toBe('abc-123')
213+
})
181214
})

‎cli/src/app.tsx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ interface AppProps {
3434
hasInvalidCredentials: boolean
3535
fileTree: FileTreeNode[]
3636
continueChat: boolean
37+
/** Raw `-c` / `--continue` flag from the CLI, before history-pick resume
38+
* folds in. Lets the landing screen distinguish "I asked to continue a
39+
* session" from "I resumed a historical conversation". */
40+
continueRequested?: boolean
3741
continueChatId?: string
3842
initialMode?: AgentMode
3943
showProjectPicker: boolean
@@ -47,6 +51,7 @@ export const App = ({
4751
hasInvalidCredentials,
4852
fileTree,
4953
continueChat,
54+
continueRequested,
5055
continueChatId,
5156
initialMode,
5257
showProjectPicker,
@@ -260,6 +265,7 @@ export const App = ({
260265
logoutMutation={logoutMutation}
261266
continueChat={effectiveContinueChat}
262267
continueChatId={effectiveContinueChatId}
268+
continueRequested={continueRequested === true}
263269
authStatus={authStatus}
264270
initialMode={initialMode}
265271
gitRoot={gitRoot}
@@ -285,6 +291,7 @@ interface AuthedSurfaceProps {
285291
logoutMutation: ReturnType<typeof useAuthState>['logoutMutation']
286292
continueChat: boolean
287293
continueChatId: string | undefined
294+
continueRequested: boolean
288295
authStatus: AuthStatus
289296
initialMode: AgentMode | undefined
290297
gitRoot: string | null | undefined
@@ -338,6 +345,7 @@ const AuthedSurfaceRoutes = ({
338345
setUser,
339346
logoutMutation,
340347
authStatus,
348+
continueRequested,
341349
initialMode,
342350
gitRoot,
343351
onSwitchToGitRoot,
@@ -400,6 +408,7 @@ const AuthedSurfaceRoutes = ({
400408
failure={sessionFailure}
401409
lastRefund={lastRefund}
402410
refundPending={refundPending}
411+
continueRequested={continueRequested}
403412
/>
404413
)
405414
}

‎cli/src/cli-args.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function parseArgs({
5454
.description('Freebuff - Free AI coding assistant')
5555
.version(version, '-v, --version', 'Print the CLI version')
5656
.option(
57-
'--continue [conversation-id]',
57+
'-c, --continue [conversation-id]',
5858
'Continue from a previous conversation (optionally specify a conversation id)',
5959
)
6060
.option(
@@ -80,7 +80,7 @@ export function parseArgs({
8080
'Remove any existing CLI log files before starting',
8181
)
8282
.option(
83-
'--continue [conversation-id]',
83+
'-c, --continue [conversation-id]',
8484
'Continue from a previous conversation (optionally specify a conversation id)',
8585
)
8686
.option(
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { afterEach, beforeAll, describe, expect, test } from 'bun:test'
2+
import { createTestRenderer } from '@opentui/core/testing'
3+
import { createRoot, flushSync } from '@opentui/react'
4+
import React from 'react'
5+
6+
import {
7+
FreebuffNothingToContinueNotice,
8+
NOTHING_TO_CONTINUE_MESSAGE,
9+
} from '../freebuff-landing-screen'
10+
import { initializeThemeStore } from '../../hooks/use-theme'
11+
12+
let cleanupRenderer: (() => void) | undefined
13+
14+
beforeAll(() => {
15+
initializeThemeStore()
16+
})
17+
18+
afterEach(() => {
19+
cleanupRenderer?.()
20+
cleanupRenderer = undefined
21+
})
22+
23+
const renderNotice = async () => {
24+
const setup = await createTestRenderer({ width: 100, height: 3 })
25+
const root = createRoot(setup.renderer)
26+
cleanupRenderer = () => {
27+
flushSync(() => root.unmount())
28+
setup.renderer.destroy()
29+
}
30+
flushSync(() => root.render(<FreebuffNothingToContinueNotice />))
31+
await setup.renderOnce()
32+
return setup
33+
}
34+
35+
describe('FreebuffNothingToContinueNotice', () => {
36+
test('tells the user there is nothing to continue', async () => {
37+
const setup = await renderNotice()
38+
const frame = setup.captureCharFrame().replace(/\s+/g, ' ')
39+
40+
expect(frame).toContain('nothing to continue')
41+
expect(frame).toContain('no active session was found')
42+
})
43+
44+
test('mentions starting a new session as the way forward', async () => {
45+
const setup = await renderNotice()
46+
47+
expect(setup.captureCharFrame()).toContain('Pick a model below to start')
48+
})
49+
50+
test('wraps the message on a narrow terminal rather than clipping it', async () => {
51+
const setup = await createTestRenderer({ width: 40, height: 4 })
52+
const root = createRoot(setup.renderer)
53+
cleanupRenderer = () => {
54+
flushSync(() => root.unmount())
55+
setup.renderer.destroy()
56+
}
57+
flushSync(() => root.render(<FreebuffNothingToContinueNotice />))
58+
await setup.renderOnce()
59+
60+
const frame = setup.captureCharFrame().replace(/\s+/g, ' ')
61+
expect(frame).toContain(NOTHING_TO_CONTINUE_MESSAGE.replace(/\s+/g, ' '))
62+
})
63+
})

‎cli/src/components/freebuff-landing-screen.tsx‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ interface FreebuffLandingScreenProps {
6969
failure: FreebuffSessionFailure | null
7070
lastRefund: number | null
7171
refundPending: boolean
72+
/** True when the CLI was launched with `-c` / `--continue`. Drives the
73+
* "nothing to continue" notice: only a session resume can satisfy the
74+
* user's ask, so the landing screen has to say it found nothing rather
75+
* than behaving like a plain first launch. */
76+
continueRequested?: boolean
7277
}
7378

7479
/** Landing-screen heading. Referenced both as rendered text and by the
@@ -77,6 +82,22 @@ interface FreebuffLandingScreenProps {
7782
const LANDING_HEADING = 'Start coding for free'
7883
const COLLAPSED_LOGO_MIN_HEIGHT = 26
7984

85+
/** Shown on the landing screen when the user launched with `-c` /
86+
* `--continue` but the probe found no active session to resume (no seat,
87+
* expired, or explicitly ended). Exported so the render test can mount it
88+
* without the landing screen's ad/streak/logo machinery. */
89+
export const NOTHING_TO_CONTINUE_MESSAGE =
90+
"There's nothing to continue — no active session was found. Pick a model below to start a new one."
91+
92+
export const FreebuffNothingToContinueNotice: React.FC = () => {
93+
const theme = useTheme()
94+
return (
95+
<text style={{ fg: theme.secondary, wrapMode: 'word', marginTop: 1 }}>
96+
{NOTHING_TO_CONTINUE_MESSAGE}
97+
</text>
98+
)
99+
}
100+
80101
/** "in ~3h 20m" / "in ~45 min" / "in under a minute". Used on the
81102
* rate-limited screen so users know when they can try again. */
82103
const formatRetryAfter = (ms: number): string => {
@@ -358,6 +379,7 @@ export const FreebuffLandingScreen: React.FC<FreebuffLandingScreenProps> = ({
358379
failure,
359380
lastRefund,
360381
refundPending,
382+
continueRequested,
361383
}) => {
362384
const theme = useTheme()
363385
const renderer = useRenderer()
@@ -593,9 +615,18 @@ export const FreebuffLandingScreen: React.FC<FreebuffLandingScreenProps> = ({
593615
// scrollbox is measured by the selector itself and must NOT be reserved
594616
// here as well, or the viewport shrinks while the content grows.
595617
const belowPickerRows = streakRows + noticeRows + streakBonusRows
618+
// The continue notice renders above the picker, so its rows must be carved
619+
// out of the picker's viewport budget the way the heading's are.
620+
const continueNoticeRows =
621+
continueRequested && isLanding
622+
? 1 /* marginTop */ + wrappedRows(NOTHING_TO_CONTINUE_MESSAGE)
623+
: 0
596624
const reservedChrome = 2 + adRows + 1 /* main paddingBottom */ + logoBlockRows
597625
const landingTextRows =
598-
wrappedRows(LANDING_HEADING) + textMarginBottom + belowPickerRows
626+
wrappedRows(LANDING_HEADING) +
627+
textMarginBottom +
628+
continueNoticeRows +
629+
belowPickerRows
599630
// Floor = one whole recommended card: 2 border rows + its 2 text lines (name
600631
// + tagline, then the AI-training warning on its own line). Rows grew from
601632
// one text line to two when the warning stopped inlining, so the old floor of
@@ -726,6 +757,7 @@ export const FreebuffLandingScreen: React.FC<FreebuffLandingScreenProps> = ({
726757
onDismiss={freebucksIntro.dismiss}
727758
/>
728759
)}
760+
{continueRequested && <FreebuffNothingToContinueNotice />}
729761
<LandingHeadingRow
730762
streakLine={streakOnHeadingRow ? streakLine : null}
731763
marginBottom={textMarginBottom}

‎cli/src/index.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ async function main(): Promise<void> {
378378
hasInvalidCredentials={hasInvalidCredentials}
379379
fileTree={fileTree}
380380
continueChat={continueChat}
381+
continueRequested={continueChat}
381382
continueChatId={continueId ?? undefined}
382383
initialMode={initialMode}
383384
showProjectPicker={showProjectPickerScreen}

0 commit comments

Comments
 (0)