@@ -23,6 +23,11 @@ import { areCreditsRestored } from './components/out-of-credits-banner'
2323import { PendingBashMessage } from './components/pending-bash-message'
2424import { SessionEndedBanner } from './components/session-ended-banner'
2525import { StatusBar } from './components/status-bar'
26+ import {
27+ SuggestedPrompts ,
28+ DEFAULT_SUGGESTED_PROMPTS ,
29+ type SuggestedPromptSelection ,
30+ } from './components/suggested-prompts'
2631import { TopBanner } from './components/top-banner'
2732import { getSlashCommandsWithSkills } from './data/slash-commands'
2833import { useAgentValidation } from './hooks/use-agent-validation'
@@ -61,6 +66,10 @@ import { returnToFreebuffLanding } from './hooks/use-freebuff-session'
6166import { END_SESSION_MESSAGE , IS_FREEBUFF } from './utils/constants'
6267import { getSystemMessage } from './utils/message-history'
6368import { getInputModeConfig } from './utils/input-modes'
69+ import {
70+ hasSubmittedFirstPrompt ,
71+ markFirstPromptSubmitted ,
72+ } from './utils/settings'
6473
6574import {
6675 type ChatKeyboardState ,
@@ -129,6 +138,12 @@ export const Chat = ({
129138} ) => {
130139 const [ forceFileOnlyMentions , setForceFileOnlyMentions ] = useState ( false )
131140
141+ // First-time onboarding: show clickable starter prompts until the user
142+ // submits their first prompt ever (persisted in settings). Freebuff only.
143+ const [ showSuggestedPrompts , setShowSuggestedPrompts ] = useState (
144+ ( ) => IS_FREEBUFF && ! hasSubmittedFirstPrompt ( ) ,
145+ )
146+
132147 const { validate : validateAgents } = useAgentValidation ( )
133148
134149 // Subscribe to ask_user bridge to trigger form display
@@ -274,7 +289,12 @@ export const Chat = ({
274289 } )
275290 }
276291 prevSlashActiveRef . current = slashContext . active
277- } , [ slashContext . active , slashContext . query , slashMatches . length , inputValue . length ] )
292+ } , [
293+ slashContext . active ,
294+ slashContext . query ,
295+ slashMatches . length ,
296+ inputValue . length ,
297+ ] )
278298
279299 // Reset suggestion menu indexes when context changes
280300 useEffect ( ( ) => {
@@ -343,11 +363,8 @@ export const Chat = ({
343363 setForceFileOnlyMentions ( true )
344364 } , [ cursorPosition , inputValue , setInputValue ] )
345365
346- const { saveToHistory, navigateUp, navigateDown, resetHistoryNavigation } = useInputHistory (
347- inputValue ,
348- setInputValue ,
349- { inputMode, setInputMode } ,
350- )
366+ const { saveToHistory, navigateUp, navigateDown, resetHistoryNavigation } =
367+ useInputHistory ( inputValue , setInputValue , { inputMode, setInputMode } )
351368
352369 // Use extracted streaming hook for connection, timer, queue, and exit handling
353370 const {
@@ -517,7 +534,10 @@ export const Chat = ({
517534 }
518535
519536 // Restore attachments if they were preserved and none have been added since
520- if ( preservedAttachments && useChatStore . getState ( ) . pendingAttachments . length === 0 ) {
537+ if (
538+ preservedAttachments &&
539+ useChatStore . getState ( ) . pendingAttachments . length === 0
540+ ) {
521541 useChatStore . setState ( ( state ) => {
522542 state . pendingAttachments = preservedAttachments
523543 } )
@@ -526,6 +546,31 @@ export const Chat = ({
526546 } ,
527547 )
528548
549+ // Retire onboarding suggested prompts once the user submits anything
550+ // (typed or clicked), persisting so they don't return on future launches.
551+ useEffect ( ( ) => {
552+ if ( showSuggestedPrompts && messages . length > 0 ) {
553+ markFirstPromptSubmitted ( )
554+ setShowSuggestedPrompts ( false )
555+ }
556+ } , [ showSuggestedPrompts , messages . length ] )
557+
558+ // Submit a suggested onboarding prompt as if the user had typed and sent it
559+ const handleSelectSuggestedPrompt = useEvent (
560+ ( prompt : string , selection : SuggestedPromptSelection ) => {
561+ trackEvent ( AnalyticsEvent . SUGGESTED_PROMPT_CLICKED , {
562+ label : selection . label ,
563+ index : selection . index ,
564+ promptLength : prompt . length ,
565+ agentMode,
566+ } )
567+ onSubmitPrompt ( prompt , agentMode ) . catch ( ( error ) => {
568+ logger . error ( { error } , '[suggested-prompt] Failed to submit prompt' )
569+ showClipboardMessage ( 'Failed to send prompt' , { durationMs : 3000 } )
570+ } )
571+ } ,
572+ )
573+
529574 // Handle followup suggestion clicks
530575 useEffect ( ( ) => {
531576 const handleFollowupClick = ( event : Event ) => {
@@ -617,16 +662,17 @@ export const Chat = ({
617662 ] ,
618663 )
619664
620- const { inputWidth, handleBuildFast, handleBuildMax, handleBuildLite } = useChatInput ( {
621- setInputValue,
622- agentMode,
623- setAgentMode,
624- separatorWidth,
625- initialPrompt,
626- onSubmitPrompt,
627- isCompactHeight,
628- isNarrowWidth,
629- } )
665+ const { inputWidth, handleBuildFast, handleBuildMax, handleBuildLite } =
666+ useChatInput ( {
667+ setInputValue,
668+ agentMode,
669+ setAgentMode,
670+ separatorWidth,
671+ initialPrompt,
672+ onSubmitPrompt,
673+ isCompactHeight,
674+ isNarrowWidth,
675+ } )
630676
631677 const {
632678 feedbackMode,
@@ -814,7 +860,12 @@ export const Chat = ({
814860 } )
815861 setInputFocused ( true )
816862 resetHistoryNavigation ( )
817- } , [ restoreSavedInput , setInputValue , setInputFocused , resetHistoryNavigation ] )
863+ } , [
864+ restoreSavedInput ,
865+ setInputValue ,
866+ setInputFocused ,
867+ resetHistoryNavigation ,
868+ ] )
818869
819870 const handleCloseFeedback = useCallback ( ( ) => {
820871 closeFeedback ( )
@@ -835,10 +886,18 @@ export const Chat = ({
835886 . then ( ( result ) => handleCommandResult ( result ) )
836887 . catch ( ( error ) => {
837888 logger . error ( { error } , '[review] Failed to submit review prompt' )
838- showClipboardMessage ( 'Failed to send review request' , { durationMs : 3000 } )
889+ showClipboardMessage ( 'Failed to send review request' , {
890+ durationMs : 3000 ,
891+ } )
839892 } )
840893 } ,
841- [ closeReviewScreen , setInputFocused , onSubmitPrompt , agentMode , handleCommandResult ] ,
894+ [
895+ closeReviewScreen ,
896+ setInputFocused ,
897+ onSubmitPrompt ,
898+ agentMode ,
899+ handleCommandResult ,
900+ ] ,
842901 )
843902
844903 const handleCloseReviewScreen = useCallback ( ( ) => {
@@ -1060,12 +1119,18 @@ export const Chat = ({
10601119 let replacement : string
10611120 const index = agentSelectedIndex
10621121 if ( index < agentMatches . length ) {
1063- const selected = agentMatches . length > 0 ? ( agentMatches [ index ] || agentMatches [ 0 ] ) : undefined
1122+ const selected =
1123+ agentMatches . length > 0
1124+ ? agentMatches [ index ] || agentMatches [ 0 ]
1125+ : undefined
10641126 if ( ! selected ) return
10651127 replacement = `@${ selected . id } `
10661128 } else {
10671129 const fileIndex = index - agentMatches . length
1068- const selectedFile = fileMatches . length > 0 ? ( fileMatches [ fileIndex ] || fileMatches [ 0 ] ) : undefined
1130+ const selectedFile =
1131+ fileMatches . length > 0
1132+ ? fileMatches [ fileIndex ] || fileMatches [ 0 ]
1133+ : undefined
10691134 if ( ! selectedFile ) return
10701135 replacement = `@${ selectedFile . filePath } `
10711136 }
@@ -1127,7 +1192,7 @@ export const Chat = ({
11271192 ( error ) => {
11281193 logger . error ( { error } , 'Failed to add pending image from file' )
11291194 showClipboardMessage ( 'Failed to add image' , { durationMs : 3000 } )
1130- }
1195+ } ,
11311196 )
11321197 } , 0 )
11331198 } ,
@@ -1270,6 +1335,36 @@ export const Chat = ({
12701335 ( agentSuggestionItems . length > 0 || fileSuggestionItems . length > 0 )
12711336 const hasSuggestionMenu = hasSlashSuggestions || hasMentionSuggestions
12721337
1338+ // Show first-time onboarding starter prompts only on a pristine, idle,
1339+ // empty-input default-mode chat — and never while a menu/overlay is up.
1340+ const showOnboardingPrompts =
1341+ showSuggestedPrompts &&
1342+ messages . length === 0 &&
1343+ inputValue . length === 0 &&
1344+ inputMode === 'default' &&
1345+ ! hasSuggestionMenu &&
1346+ ! isStreaming &&
1347+ ! isWaitingForResponse &&
1348+ ! feedbackMode &&
1349+ ! publishMode &&
1350+ ! reviewMode &&
1351+ askUserState === null
1352+
1353+ // Fire a one-time impression so we can measure onboarding-prompt usage
1354+ // (click-through = SUGGESTED_PROMPT_CLICKED / SUGGESTED_PROMPT_SHOWN).
1355+ const suggestedPromptsShownRef = useRef ( false )
1356+ useEffect ( ( ) => {
1357+ if ( showOnboardingPrompts && ! suggestedPromptsShownRef . current ) {
1358+ suggestedPromptsShownRef . current = true
1359+ trackEvent ( AnalyticsEvent . SUGGESTED_PROMPT_SHOWN , {
1360+ count : isCompactHeight
1361+ ? Math . min ( 2 , DEFAULT_SUGGESTED_PROMPTS . length )
1362+ : DEFAULT_SUGGESTED_PROMPTS . length ,
1363+ isCompactHeight,
1364+ } )
1365+ }
1366+ } , [ showOnboardingPrompts , isCompactHeight ] )
1367+
12731368 const inputLayoutMetrics = useMemo ( ( ) => {
12741369 // In bash mode, layout is based on the actual input (no ! prefix needed)
12751370 const text = inputValue ?? ''
@@ -1306,7 +1401,9 @@ export const Chat = ({
13061401
13071402 // Auto-show subscription limit banner when rate limit becomes active
13081403 const subscriptionLimitShownRef = useRef ( false )
1309- const subscriptionRateLimit = subscriptionData ?. hasSubscription ? subscriptionData . rateLimit : undefined
1404+ const subscriptionRateLimit = subscriptionData ?. hasSubscription
1405+ ? subscriptionData . rateLimit
1406+ : undefined
13101407 const fallbackToALaCarte = subscriptionData ?. fallbackToALaCarte ?? false
13111408 useEffect ( ( ) => {
13121409 const isLimited = subscriptionRateLimit ?. limited === true
@@ -1490,66 +1587,74 @@ export const Chat = ({
14901587 isStreaming = { isStreaming || isWaitingForResponse }
14911588 />
14921589 ) : (
1493- < ChatInputBar
1494- inputValue = { inputValue }
1495- cursorPosition = { cursorPosition }
1496- setInputValue = { setInputValue }
1497- inputFocused = { inputFocused }
1498- inputRef = { inputRef }
1499- inputPlaceholder = { inputPlaceholder }
1500- lastEditDueToNav = { lastEditDueToNav }
1501- agentMode = { agentMode }
1502- toggleAgentMode = { toggleAgentMode }
1503- setAgentMode = { setAgentMode }
1504- hasSlashSuggestions = { hasSlashSuggestions }
1505- hasMentionSuggestions = { hasMentionSuggestions }
1506- hasSuggestionMenu = { hasSuggestionMenu }
1507- slashSuggestionItems = { slashSuggestionItems }
1508- agentSuggestionItems = { agentSuggestionItems }
1509- fileSuggestionItems = { fileSuggestionItems }
1510- slashSelectedIndex = { slashSelectedIndex }
1511- agentSelectedIndex = { agentSelectedIndex }
1512- onSlashItemClick = { handleSlashItemClick }
1513- onMentionItemClick = { handleMentionItemClick }
1514- theme = { theme }
1515- terminalHeight = { terminalHeight }
1516- separatorWidth = { separatorWidth }
1517- shouldCenterInputVertically = { shouldCenterInputVertically }
1518- inputBoxTitle = { inputBoxTitle }
1519- isCompactHeight = { isCompactHeight }
1520- isNarrowWidth = { isNarrowWidth }
1521- feedbackMode = { feedbackMode }
1522- handleExitFeedback = { handleExitFeedback }
1523- publishMode = { publishMode }
1524- handleExitPublish = { handleExitPublish }
1525- handlePublish = { handlePublish }
1526- handleSubmit = { handleSubmit }
1527- onPaste = { createPasteHandler ( {
1528- text : inputValue ,
1529- cursorPosition,
1530- onChange : setInputValue ,
1531- onPasteImage : chatKeyboardHandlers . onPasteImage ,
1532- onPasteImagePath : chatKeyboardHandlers . onPasteImagePath ,
1533- onPasteFilePath : chatKeyboardHandlers . onPasteFilePath ,
1534- onPasteLongText : ( pastedText ) => {
1535- const id = crypto . randomUUID ( )
1536- const preview = pastedText . slice ( 0 , 100 ) . replace ( / \n / g, ' ' )
1537- useChatStore . getState ( ) . addPendingTextAttachment ( {
1538- id,
1539- content : pastedText ,
1540- preview,
1541- charCount : pastedText . length ,
1542- } )
1543- // Show temporary status message
1544- showClipboardMessage (
1545- `📋 Pasted text (${ pastedText . length . toLocaleString ( ) } chars)` ,
1546- { durationMs : 5000 } ,
1547- )
1548- } ,
1549- cwd : getProjectRoot ( ) ?? process . cwd ( ) ,
1550- } ) }
1551- onInterruptStream = { chatKeyboardHandlers . onInterruptStream }
1552- />
1590+ < >
1591+ { showOnboardingPrompts && (
1592+ < SuggestedPrompts
1593+ onSelect = { handleSelectSuggestedPrompt }
1594+ maxItems = { isCompactHeight ? 2 : undefined }
1595+ />
1596+ ) }
1597+ < ChatInputBar
1598+ inputValue = { inputValue }
1599+ cursorPosition = { cursorPosition }
1600+ setInputValue = { setInputValue }
1601+ inputFocused = { inputFocused }
1602+ inputRef = { inputRef }
1603+ inputPlaceholder = { inputPlaceholder }
1604+ lastEditDueToNav = { lastEditDueToNav }
1605+ agentMode = { agentMode }
1606+ toggleAgentMode = { toggleAgentMode }
1607+ setAgentMode = { setAgentMode }
1608+ hasSlashSuggestions = { hasSlashSuggestions }
1609+ hasMentionSuggestions = { hasMentionSuggestions }
1610+ hasSuggestionMenu = { hasSuggestionMenu }
1611+ slashSuggestionItems = { slashSuggestionItems }
1612+ agentSuggestionItems = { agentSuggestionItems }
1613+ fileSuggestionItems = { fileSuggestionItems }
1614+ slashSelectedIndex = { slashSelectedIndex }
1615+ agentSelectedIndex = { agentSelectedIndex }
1616+ onSlashItemClick = { handleSlashItemClick }
1617+ onMentionItemClick = { handleMentionItemClick }
1618+ theme = { theme }
1619+ terminalHeight = { terminalHeight }
1620+ separatorWidth = { separatorWidth }
1621+ shouldCenterInputVertically = { shouldCenterInputVertically }
1622+ inputBoxTitle = { inputBoxTitle }
1623+ isCompactHeight = { isCompactHeight }
1624+ isNarrowWidth = { isNarrowWidth }
1625+ feedbackMode = { feedbackMode }
1626+ handleExitFeedback = { handleExitFeedback }
1627+ publishMode = { publishMode }
1628+ handleExitPublish = { handleExitPublish }
1629+ handlePublish = { handlePublish }
1630+ handleSubmit = { handleSubmit }
1631+ onPaste = { createPasteHandler ( {
1632+ text : inputValue ,
1633+ cursorPosition,
1634+ onChange : setInputValue ,
1635+ onPasteImage : chatKeyboardHandlers . onPasteImage ,
1636+ onPasteImagePath : chatKeyboardHandlers . onPasteImagePath ,
1637+ onPasteFilePath : chatKeyboardHandlers . onPasteFilePath ,
1638+ onPasteLongText : ( pastedText ) => {
1639+ const id = crypto . randomUUID ( )
1640+ const preview = pastedText . slice ( 0 , 100 ) . replace ( / \n / g, ' ' )
1641+ useChatStore . getState ( ) . addPendingTextAttachment ( {
1642+ id,
1643+ content : pastedText ,
1644+ preview,
1645+ charCount : pastedText . length ,
1646+ } )
1647+ // Show temporary status message
1648+ showClipboardMessage (
1649+ `📋 Pasted text (${ pastedText . length . toLocaleString ( ) } chars)` ,
1650+ { durationMs : 5000 } ,
1651+ )
1652+ } ,
1653+ cwd : getProjectRoot ( ) ?? process . cwd ( ) ,
1654+ } ) }
1655+ onInterruptStream = { chatKeyboardHandlers . onInterruptStream }
1656+ />
1657+ </ >
15531658 ) }
15541659 </ box >
15551660 </ box >
0 commit comments