Skip to content

Commit 8878e11

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@2dd75f87b1c2932bff0c1584e3912c84c45c8ce7
1 parent e4425f0 commit 8878e11

7 files changed

Lines changed: 456 additions & 157 deletions

File tree

bun.lock

Lines changed: 38 additions & 70 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/release/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codebuff",
3-
"version": "1.0.681",
3+
"version": "1.0.682",
44
"description": "AI coding agent",
55
"license": "MIT",
66
"bin": {

cli/src/chat.tsx

Lines changed: 189 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ import { areCreditsRestored } from './components/out-of-credits-banner'
2323
import { PendingBashMessage } from './components/pending-bash-message'
2424
import { SessionEndedBanner } from './components/session-ended-banner'
2525
import { StatusBar } from './components/status-bar'
26+
import {
27+
SuggestedPrompts,
28+
DEFAULT_SUGGESTED_PROMPTS,
29+
type SuggestedPromptSelection,
30+
} from './components/suggested-prompts'
2631
import { TopBanner } from './components/top-banner'
2732
import { getSlashCommandsWithSkills } from './data/slash-commands'
2833
import { useAgentValidation } from './hooks/use-agent-validation'
@@ -61,6 +66,10 @@ import { returnToFreebuffLanding } from './hooks/use-freebuff-session'
6166
import { END_SESSION_MESSAGE, IS_FREEBUFF } from './utils/constants'
6267
import { getSystemMessage } from './utils/message-history'
6368
import { getInputModeConfig } from './utils/input-modes'
69+
import {
70+
hasSubmittedFirstPrompt,
71+
markFirstPromptSubmitted,
72+
} from './utils/settings'
6473

6574
import {
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

Comments
 (0)