docs: 添加 ToolSearch 设计指南 + 禁用 turn-zero 工具推荐弹窗

- 新增 docs/design/tool-search-design-guide.md,涵盖架构、搜索算法、执行管道、演进历史
- 禁用 getTurnZeroSearchExtraToolsPrefetch,消除用户输入时的频繁弹窗
- inter-turn 发现机制保持不变

Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-05-09 16:45:56 +08:00
parent bd2253846f
commit 2cf18c4c49
61 changed files with 753 additions and 423 deletions

View File

@@ -6,7 +6,7 @@ import type { Tools } from '../Tool.js';
import type { RenderableMessage } from '../types/message.js';
import {
getDisplayMessageFromCollapsed,
getToolSearchOrReadInfo,
getSearchExtraToolsOrReadInfo,
getToolUseIdsFromCollapsedGroup,
hasAnyToolInProgress,
} from '../utils/collapseReadSearch.js';
@@ -89,7 +89,7 @@ export function hasContentAfterIndex(
continue;
}
if (content?.type === 'tool_use') {
if (getToolSearchOrReadInfo(content.name!, content.input, tools).isCollapsible) {
if (getSearchExtraToolsOrReadInfo(content.name!, content.input, tools).isCollapsible) {
continue;
}
// Non-collapsible tool uses appear in syntheticStreamingToolUseMessages
@@ -115,7 +115,7 @@ export function hasContentAfterIndex(
// merged into the current collapsed group on the next render cycle
if (msg?.type === 'grouped_tool_use') {
const firstInput = firstBlock(msg.messages[0]?.message.content)?.input;
if (getToolSearchOrReadInfo(msg.toolName, firstInput, tools).isCollapsible) {
if (getSearchExtraToolsOrReadInfo(msg.toolName, firstInput, tools).isCollapsible) {
continue;
}
}

View File

@@ -852,7 +852,7 @@ const MessagesImpl = ({
// renderToolResultMessage shows. Falls back to renderableSearchText
// (duck-types toolUseResult) for tools that haven't implemented it,
// and for all non-tool-result message types. The drift-catcher test
// (toolSearchText.test.tsx) renders + compares to keep these in sync.
// (searchExtraToolsText.test.tsx) renders + compares to keep these in sync.
//
// A second-React-root reconcile approach was tried and ruled out
// (measured 3.1ms/msg, growing — flushSyncWork processes all roots;

View File

@@ -3,21 +3,21 @@ import { Box, Text } from '@anthropic/ink';
import { Select } from './CustomSelect/select.js';
import { PermissionDialog } from './permissions/PermissionDialog.js';
type ToolSearchHintItem = {
type SearchExtraToolsHintItem = {
name: string;
description: string;
score: number;
};
type Props = {
tools: ToolSearchHintItem[];
tools: SearchExtraToolsHintItem[];
onSelect: (toolName: string) => void;
onDismiss: () => void;
};
const AUTO_DISMISS_MS = 30_000;
export function ToolSearchHint({ tools, onSelect, onDismiss }: Props): React.ReactNode {
export function SearchExtraToolsHint({ tools, onSelect, onDismiss }: Props): React.ReactNode {
const onSelectRef = React.useRef(onSelect);
const onDismissRef = React.useRef(onDismiss);
onSelectRef.current = onSelect;

View File

@@ -30,35 +30,37 @@ mock.module('src/services/analytics/growthbook.js', () => ({
}))
const {
subscribeToToolSearchPrefetch,
getToolSearchPrefetchSnapshot,
clearToolSearchPrefetchResults,
} = await import('src/services/toolSearch/prefetch.js')
subscribeToSearchExtraToolsPrefetch,
getSearchExtraToolsPrefetchSnapshot,
clearSearchExtraToolsPrefetchResults,
} = await import('src/services/searchExtraTools/prefetch.js')
const { useToolSearchHint } = await import('src/hooks/useToolSearchHint.js')
const { useSearchExtraToolsHint } = await import(
'src/hooks/useSearchExtraToolsHint.js'
)
describe('useToolSearchHint', () => {
describe('useSearchExtraToolsHint', () => {
// We test the subscription/snapshot API directly since
// React hooks require a renderer.
test('returns empty tools when no prefetch result', () => {
clearToolSearchPrefetchResults()
const snapshot = getToolSearchPrefetchSnapshot()
clearSearchExtraToolsPrefetchResults()
const snapshot = getSearchExtraToolsPrefetchSnapshot()
expect(snapshot).toEqual([])
})
test('snapshot updates when listeners are notified', () => {
clearToolSearchPrefetchResults()
clearSearchExtraToolsPrefetchResults()
// Simulate what prefetch does: set results and notify
const mockSetResults = (results: unknown[]) => {
// We can't directly set latestPrefetchResult, but we can test
// the clear function and subscription mechanism
clearToolSearchPrefetchResults()
clearSearchExtraToolsPrefetchResults()
}
// Test subscription
let callCount = 0
const unsubscribe = subscribeToToolSearchPrefetch(() => {
const unsubscribe = subscribeToSearchExtraToolsPrefetch(() => {
callCount++
})
expect(callCount).toBe(0)
@@ -69,12 +71,12 @@ describe('useToolSearchHint', () => {
// Unsubscribe and verify no more calls
unsubscribe()
clearToolSearchPrefetchResults()
clearSearchExtraToolsPrefetchResults()
expect(callCount).toBe(1)
})
test('clearToolSearchPrefetchResults resets snapshot', () => {
clearToolSearchPrefetchResults()
expect(getToolSearchPrefetchSnapshot()).toEqual([])
test('clearSearchExtraToolsPrefetchResults resets snapshot', () => {
clearSearchExtraToolsPrefetchResults()
expect(getSearchExtraToolsPrefetchSnapshot()).toEqual([])
})
})

View File

@@ -140,7 +140,7 @@ export function AttachmentMessage({ attachment, addMargin, verbose, isTranscript
// tool_discovery rendered here (not in the switch) so the 'tool_discovery'
// string literal stays inside a feature()-guarded block.
if (feature('EXPERIMENTAL_TOOL_SEARCH')) {
if (feature('EXPERIMENTAL_SEARCH_EXTRA_TOOLS')) {
if (attachment.type === 'tool_discovery') {
if (attachment.tools.length === 0) return null;
const names = attachment.tools.map(t => t.name).join(', ');

View File

@@ -57,7 +57,7 @@ function VerboseToolUse({
theme: ThemeName;
}): React.ReactNode {
const bg = useSelectedMessageBg();
// Same REPL-primitive fallback as getToolSearchOrReadInfo — REPL mode strips
// Same REPL-primitive fallback as getSearchExtraToolsOrReadInfo — REPL mode strips
// these from the execution tools list, but virtual messages still need them
// to render in verbose mode.
const tool = findToolByName(tools, content.name) ?? findToolByName(getReplPrimitiveTools(), content.name);