* feat: 第一版大重构 * fix: 修复类型问题 * chore: 更新版本到 1.3.2 * Add brave as alternative WebSearchTool * fix: 修正顺序 * fix: 修复对穷鬼模式的 auto dream 和 session memory 越过 * feat: 穷鬼模式去除 session-summary * feat: 创建 builtin-tools 包,搬运所有工具实现 将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/, 内部导入路径已更新为 src/ alias 模式。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/ - src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/ - 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock - tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射 - 新增 packages/builtin-tools/src 至 include Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀 所有包名及 import 路径统一添加 @claude-code-best/ 前缀: - builtin-tools → @claude-code-best/builtin-tools - mcp-client → @claude-code-best/mcp-client - agent-tools → @claude-code-best/agent-tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复 node 环境没有 bun 的问题 --------- Co-authored-by: Eric-Guo <eric.guocz@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
4.8 KiB
Chapter 6: Scrolling
ScrollBox
A scrollable container with imperative scroll API, viewport culling, and sticky scroll support.
import { ScrollBox } from '@anthropic/ink'
import type { ScrollBoxHandle } from '@anthropic/ink'
function MessageList({ messages }) {
const scrollRef = useRef<ScrollBoxHandle>(null)
// Auto-scroll to bottom on new messages
useEffect(() => {
scrollRef.current?.scrollToBottom()
}, [messages.length])
return (
<ScrollBox ref={scrollRef} stickyScroll flexDirection="column" height={20}>
{messages.map(msg => (
<Text key={msg.id}>{msg.text}</Text>
))}
</ScrollBox>
)
}
Props
ScrollBox accepts all Box layout props except textWrap, overflow, overflowX, overflowY (these are managed internally):
| Prop | Type | Default | Description |
|---|---|---|---|
ref |
Ref<ScrollBoxHandle> |
- | Imperative handle |
stickyScroll |
boolean |
false |
Auto-follow new content |
| (layout props) | Styles |
- | Width, height, padding, etc. |
ScrollBoxHandle (Imperative API)
interface ScrollBoxHandle {
// Absolute positioning
scrollTo(y: number): void
scrollToElement(el: DOMElement, offset?: number): void
scrollToBottom(): void
// Relative positioning
scrollBy(dy: number): void
// Query state
getScrollTop(): number
getPendingDelta(): number
getScrollHeight(): number
getFreshScrollHeight(): number
getViewportHeight(): number
getViewportTop(): number
isSticky(): boolean
// Events
subscribe(listener: () => void): () => void
// Virtual scroll support
setClampBounds(min?: number, max?: number): void
}
Method Details
scrollTo(y)
Jump to an absolute position. Breaks sticky scroll.
scrollRef.current?.scrollTo(0) // Scroll to top
scrollBy(dy)
Scroll by a relative amount. Accumulates deltas for smooth scrolling.
scrollRef.current?.scrollBy(3) // Scroll down 3 rows
scrollRef.current?.scrollBy(-5) // Scroll up 5 rows
scrollToElement(el, offset?)
Scroll so a specific DOM element is at the viewport top. More reliable than scrollTo because it reads the element's position at render time (avoids stale layout values).
const elementRef = useRef<DOMElement>(null)
scrollRef.current?.scrollToElement(elementRef.current!, 2)
scrollToBottom()
Pin scroll to bottom. Enables sticky mode.
scrollRef.current?.scrollToBottom()
isSticky()
Returns true when scroll is pinned to the bottom.
if (scrollRef.current?.isSticky()) {
// User hasn't scrolled up
}
subscribe(listener)
Subscribe to imperative scroll changes. Returns unsubscribe function.
useEffect(() => {
return scrollRef.current?.subscribe(() => {
console.log('Scroll position changed')
})
}, [])
Sticky Scroll
When stickyScroll is enabled:
- Scroll automatically follows new content at the bottom
- User scroll (via
scrollBy/scrollTo) breaks stickiness scrollToBottom()re-enables stickiness- Content growth at the bottom is detected and followed automatically
<ScrollBox stickyScroll height={20}>
{/* New items auto-scroll to bottom */}
{items.map(renderItem)}
</ScrollBox>
Viewport Culling
ScrollBox only renders children that intersect the visible viewport. Children outside the viewport are still mounted in React but skipped during terminal rendering. This makes large lists performant.
Virtual Scrolling
For very large lists, use setClampBounds in combination with a virtual scrolling hook:
const scrollRef = useRef<ScrollBoxHandle>(null)
// After computing visible range
scrollRef.current?.setClampBounds(firstVisibleRow, lastVisibleRow)
This prevents burst scrollTo calls from showing blank space beyond mounted content.
Scroll Events
ScrollBox bypasses React state for scroll operations. Instead:
scrollTo/scrollBymutatescrollTopdirectly on the DOM node- The node is marked dirty
- A microtask-deferred render fires to coalesce multiple scroll events
- The Ink renderer reads
scrollTopduring layout
This avoids React reconciler overhead per wheel event.
Integration with Mouse Wheel
In alt-screen mode, mouse wheel events are captured by the App component and forwarded to the focused ScrollBox:
Wheel event → App.handleMouseEvent → ScrollBox.scrollBy(delta)
Layout Structure
ScrollBox creates a two-level DOM structure:
ink-box (overflow: scroll, constrained height)
└── Box (flexGrow: 1, flexShrink: 0, width: 100%)
├── Child 1
├── Child 2
└── ...
The outer ink-box is the viewport with constrained size. The inner Box grows to fit all content. The renderer computes scrollHeight from the inner box and translates content by -scrollTop.