Skip to content

Commit f59b7f1

Browse files
Add inertial scroll acceleration with IDE-specific tuning
Implements physics-based scrolling with momentum, decay, and directional awareness. Detects Zed and Cursor terminals for custom tuning. Auto-scrolls to bottom after sending messages. Controlled via CODEBUFF_SCROLL_MODE env variable. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent c782e5f commit f59b7f1

File tree

2 files changed

+312
-29
lines changed

2 files changed

+312
-29
lines changed

cli/src/chat.tsx

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ import {
2525
type LocalAgentInfo,
2626
} from './utils/local-agent-registry'
2727

28-
import {
29-
chatThemes,
30-
createMarkdownPalette,
31-
} from './utils/theme-system'
28+
import { chatThemes, createMarkdownPalette } from './utils/theme-system'
29+
import { createChatScrollAcceleration } from './utils/chat-scroll-accel'
3230
import { TextAttributes } from '@opentui/core'
3331

3432
import type { ToolName } from '@codebuff/sdk'
@@ -150,7 +148,9 @@ const filterSlashCommands = (
150148

151149
for (const command of commands) {
152150
const id = command.id.toLowerCase()
153-
const aliasList = (command.aliases ?? []).map((alias) => alias.toLowerCase())
151+
const aliasList = (command.aliases ?? []).map((alias) =>
152+
alias.toLowerCase(),
153+
)
154154

155155
if (
156156
id.startsWith(normalized) ||
@@ -162,7 +162,9 @@ const filterSlashCommands = (
162162

163163
for (const command of commands) {
164164
const id = command.id.toLowerCase()
165-
const aliasList = (command.aliases ?? []).map((alias) => alias.toLowerCase())
165+
const aliasList = (command.aliases ?? []).map((alias) =>
166+
alias.toLowerCase(),
167+
)
166168
const description = command.description.toLowerCase()
167169

168170
if (
@@ -271,12 +273,21 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
271273
}
272274
}, [])
273275

274-
const { scrollToAgent, scrollboxProps } = useChatScrollbox(
276+
const { scrollToLatest, scrollToAgent, scrollboxProps } = useChatScrollbox(
275277
scrollRef,
276278
messages,
277279
agentRefsMap,
278280
)
279281

282+
const inertialScrollAcceleration = useMemo(
283+
() => createChatScrollAcceleration(),
284+
[],
285+
)
286+
287+
const appliedScrollboxProps = inertialScrollAcceleration
288+
? { ...scrollboxProps, scrollAcceleration: inertialScrollAcceleration }
289+
: scrollboxProps
290+
280291
const localAgents = useMemo(() => loadLocalAgents(), [])
281292

282293
const slashContext = useMemo(
@@ -334,10 +345,7 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
334345
}, [slashContext.active, slashContext.query])
335346

336347
useEffect(() => {
337-
if (
338-
slashMatches.length > 0 &&
339-
slashSelectedIndex >= slashMatches.length
340-
) {
348+
if (slashMatches.length > 0 && slashSelectedIndex >= slashMatches.length) {
341349
setSlashSelectedIndex(slashMatches.length - 1)
342350
}
343351
if (slashMatches.length === 0 && slashSelectedIndex !== 0) {
@@ -354,10 +362,7 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
354362
}, [mentionContext.active, mentionContext.query])
355363

356364
useEffect(() => {
357-
if (
358-
agentMatches.length > 0 &&
359-
agentSelectedIndex >= agentMatches.length
360-
) {
365+
if (agentMatches.length > 0 && agentSelectedIndex >= agentMatches.length) {
361366
setAgentSelectedIndex(agentMatches.length - 1)
362367
}
363368
if (agentMatches.length === 0 && agentSelectedIndex !== 0) {
@@ -379,9 +384,7 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
379384
return false
380385
}
381386

382-
const hasModifier = Boolean(
383-
key.ctrl || key.meta || key.alt || key.option,
384-
)
387+
const hasModifier = Boolean(key.ctrl || key.meta || key.alt || key.option)
385388

386389
if (key.name === 'down' && !hasModifier) {
387390
setSlashSelectedIndex((prev) =>
@@ -408,8 +411,7 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
408411
}
409412

410413
if (key.name === 'return' && !key.shift && !hasModifier) {
411-
const selected =
412-
slashMatches[slashSelectedIndex] ?? slashMatches[0]
414+
const selected = slashMatches[slashSelectedIndex] ?? slashMatches[0]
413415
if (!selected) {
414416
return true
415417
}
@@ -432,7 +434,13 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
432434

433435
return false
434436
},
435-
[slashContext.active, slashContext.startIndex, slashContext.query, slashMatches, slashSelectedIndex],
437+
[
438+
slashContext.active,
439+
slashContext.startIndex,
440+
slashContext.query,
441+
slashMatches,
442+
slashSelectedIndex,
443+
],
436444
)
437445

438446
const handleAgentMenuKey = useCallback(
@@ -449,9 +457,7 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
449457
return false
450458
}
451459

452-
const hasModifier = Boolean(
453-
key.ctrl || key.meta || key.alt || key.option,
454-
)
460+
const hasModifier = Boolean(key.ctrl || key.meta || key.alt || key.option)
455461

456462
if (key.name === 'down' && !hasModifier) {
457463
setAgentSelectedIndex((prev) =>
@@ -478,8 +484,7 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
478484
}
479485

480486
if (key.name === 'return' && !key.shift && !hasModifier) {
481-
const selected =
482-
agentMatches[agentSelectedIndex] ?? agentMatches[0]
487+
const selected = agentMatches[agentSelectedIndex] ?? agentMatches[0]
483488
if (!selected) {
484489
return true
485490
}
@@ -503,7 +508,13 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
503508

504509
return false
505510
},
506-
[mentionContext.active, mentionContext.startIndex, mentionContext.query, agentMatches, agentSelectedIndex],
511+
[
512+
mentionContext.active,
513+
mentionContext.startIndex,
514+
mentionContext.query,
515+
agentMatches,
516+
agentSelectedIndex,
517+
],
507518
)
508519

509520
const handleSuggestionMenuKey = useCallback(
@@ -610,6 +621,10 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
610621
}
611622

612623
sendMessage(trimmed)
624+
625+
setTimeout(() => {
626+
scrollToLatest()
627+
}, 0)
613628
}, [
614629
inputValue,
615630
isStreaming,
@@ -618,6 +633,7 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
618633
addToQueue,
619634
streamMessageIdRef,
620635
isChainInProgressRef,
636+
scrollToLatest,
621637
])
622638

623639
useKeyboardHandlers({
@@ -678,7 +694,7 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
678694
stickyStart="bottom"
679695
scrollX={false}
680696
scrollbarOptions={{ visible: false }}
681-
{...scrollboxProps}
697+
{...appliedScrollboxProps}
682698
style={{
683699
flexGrow: 1,
684700
rootOptions: {
@@ -747,7 +763,8 @@ export const App = ({ initialPrompt }: { initialPrompt?: string } = {}) => {
747763
prefix="/"
748764
/>
749765
) : null}
750-
{!slashContext.active && mentionContext.active &&
766+
{!slashContext.active &&
767+
mentionContext.active &&
751768
agentSuggestionItems.length > 0 ? (
752769
<SuggestionMenu
753770
items={agentSuggestionItems}

0 commit comments

Comments
 (0)