Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Tool.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from 'bun:test'

import { findToolByNameOrUniquePrefix, type Tools } from './Tool.js'
import { getAllBaseTools } from './tools.js'

const tools = [
{ name: 'Read' },
Expand Down Expand Up @@ -37,4 +38,16 @@ describe('findToolByNameOrUniquePrefix', () => {
),
).toBeUndefined()
})

test('recovers a missing final character for every registered built-in tool', () => {
const baseTools = getAllBaseTools()

for (const tool of baseTools) {
if (tool.name.length < 4 || tool.name.startsWith('mcp__')) continue
const truncatedName = tool.name.slice(0, -1)
expect(
findToolByNameOrUniquePrefix(baseTools, truncatedName)?.name,
).toBe(tool.name)
}
})
})
43 changes: 33 additions & 10 deletions src/Tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,33 @@ export function findToolByName(tools: Tools, name: string): Tool | undefined {
return tools.find(t => toolMatchesName(t, name))
}

/**
* Resolves a canonical tool name from an exact match or an unambiguous
* provider-truncated prefix. This string-only form is shared by API adapters,
* which have the advertised tool schemas but not the runtime Tool objects.
*/
export function resolveToolNameByUniquePrefix(
toolNames: readonly string[],
name: string,
): string | undefined {
const uniqueToolNames = [...new Set(toolNames)]
if (uniqueToolNames.includes(name)) return name

if (name.length < 3 || name.startsWith('mcp__')) return undefined

const prefixMatches = uniqueToolNames.filter(toolName =>
toolName.startsWith(name),
)
const oneCharacterCompletions = prefixMatches.filter(
toolName => toolName.length === name.length + 1,
)
if (oneCharacterCompletions.length === 1) {
return oneCharacterCompletions[0]
}

return prefixMatches.length === 1 ? prefixMatches[0] : undefined
}

/**
* Resolves a provider-truncated built-in tool name only when the prefix is
* unambiguous. Prefer a unique one-character completion before considering
Expand All @@ -388,17 +415,13 @@ export function findToolByNameOrUniquePrefix(
const exactMatch = findToolByName(tools, name)
if (exactMatch) return exactMatch

if (name.length < 3 || name.startsWith('mcp__')) return undefined

const prefixMatches = tools.filter(tool => tool.name.startsWith(name))
const oneCharacterCompletions = prefixMatches.filter(
tool => tool.name.length === name.length + 1,
const resolvedName = resolveToolNameByUniquePrefix(
tools.map(tool => tool.name),
name,
)
if (oneCharacterCompletions.length === 1) {
return oneCharacterCompletions[0]
}

return prefixMatches.length === 1 ? prefixMatches[0] : undefined
return resolvedName
? tools.find(tool => tool.name === resolvedName)
: undefined
}

export type Tool<
Expand Down
6 changes: 3 additions & 3 deletions src/components/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Box, Text } from '../ink.js';
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js';
import type { Screen } from '../screens/REPL.js';
import type { Tools } from '../Tool.js';
import { findToolByName } from '../Tool.js';
import { findToolByNameOrUniquePrefix } from '../Tool.js';
import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js';
import type { Message as MessageType, NormalizedMessage, ProgressMessage as ProgressMessageType, RenderableMessage } from '../types/message.js';
import { type AdvisorBlock, isAdvisorBlock } from '../utils/advisor.js';
Expand Down Expand Up @@ -589,7 +589,7 @@ const MessagesImpl = ({
const b_0 = msg_6.message.content[0];
if (b_0?.type !== 'tool_result' || b_0.is_error || !msg_6.toolUseResult) return false;
const name = lookupsRef.current.toolUseByToolUseID.get(b_0.tool_use_id)?.name;
const tool = name ? findToolByName(tools, name) : undefined;
const tool = name ? findToolByNameOrUniquePrefix(tools, name) : undefined;
return tool?.isResultTruncated?.(msg_6.toolUseResult as never) ?? false;
}, [tools]);
const canAnimate = (!toolJSX || !!toolJSX.shouldContinueAnimation) && !toolUseConfirmQueue.length && !isMessageSelectorVisible;
Expand Down Expand Up @@ -658,7 +658,7 @@ const MessagesImpl = ({
const tr = msg_9.message.content.find(b_1 => b_1.type === 'tool_result');
if (tr && 'tool_use_id' in tr) {
const tu = lookups_0.toolUseByToolUseID.get(tr.tool_use_id);
const tool_0 = tu && findToolByName(tools, tu.name);
const tool_0 = tu && findToolByNameOrUniquePrefix(tools, tu.name);
const extracted = tool_0?.extractSearchText?.(msg_9.toolUseResult as never);
// undefined = tool didn't implement → keep heuristic. Empty
// string = tool says "nothing to index" → respect that.
Expand Down
4 changes: 2 additions & 2 deletions src/components/messages/AssistantToolUseMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { BLACK_CIRCLE } from '../../constants/figures.js';
import { stringWidth } from '../../ink/stringWidth.js';
import { Box, Text, useTheme } from '../../ink.js';
import { useAppStateMaybeOutsideOfProvider } from '../../state/AppState.js';
import { findToolByName, type Tool, type ToolProgressData, type Tools } from '../../Tool.js';
import { findToolByNameOrUniquePrefix, type Tool, type ToolProgressData, type Tools } from '../../Tool.js';
import type { ProgressMessage } from '../../types/message.js';
import { useIsClassifierChecking } from '../../utils/classifierApprovalsHook.js';
import { logError } from '../../utils/log.js';
Expand Down Expand Up @@ -64,7 +64,7 @@ export function AssistantToolUseMessage(t0) {
t1 = null;
break bb0;
}
const tool = findToolByName(tools, param.name);
const tool = findToolByNameOrUniquePrefix(tools, param.name);
if (!tool) {
t1 = null;
break bb0;
Expand Down
4 changes: 2 additions & 2 deletions src/components/messages/CollapsedReadSearchContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { basename } from 'path';
import React, { useRef } from 'react';
import { useMinDisplayTime } from '../../hooks/useMinDisplayTime.js';
import { Ansi, Box, Text, useTheme } from '../../ink.js';
import { findToolByName, type Tools } from '../../Tool.js';
import { findToolByNameOrUniquePrefix, type Tools } from '../../Tool.js';
import { getReplPrimitiveTools } from '../../tools/REPLTool/primitiveTools.js';
import type { CollapsedReadSearchGroup, NormalizedAssistantMessage } from '../../types/message.js';
import { uniq } from '../../utils/array.js';
Expand Down Expand Up @@ -55,7 +55,7 @@ function VerboseToolUse(t0) {
if ($[0] !== bg || $[1] !== content.id || $[2] !== content.input || $[3] !== content.name || $[4] !== inProgressToolUseIDs || $[5] !== lookups || $[6] !== shouldAnimate || $[7] !== theme || $[8] !== tools) {
t2 = Symbol.for("react.early_return_sentinel");
bb0: {
const tool = findToolByName(tools, content.name) ?? findToolByName(getReplPrimitiveTools(), content.name);
const tool = findToolByNameOrUniquePrefix(tools, content.name) ?? findToolByNameOrUniquePrefix(getReplPrimitiveTools(), content.name);
if (!tool) {
t2 = null;
break bb0;
Expand Down
4 changes: 2 additions & 2 deletions src/components/messages/GroupedToolUseContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ToolResultBlockParam, ToolUseBlockParam } from '@anthropic-ai/sdk/resources/messages/messages.mjs';
import * as React from 'react';
import { filterToolProgressMessages, findToolByName, type Tools } from '../../Tool.js';
import { filterToolProgressMessages, findToolByNameOrUniquePrefix, type Tools } from '../../Tool.js';
import type { GroupedToolUseMessage } from '../../types/message.js';
import type { buildMessageLookups } from '../../utils/messages.js';
type Props = {
Expand All @@ -17,7 +17,7 @@ export function GroupedToolUseContent({
inProgressToolUseIDs,
shouldAnimate
}: Props): React.ReactNode {
const tool = findToolByName(tools, message.toolName);
const tool = findToolByNameOrUniquePrefix(tools, message.toolName);
if (!tool?.renderGroupedToolUse) {
return null;
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/messages/UserToolResultMessage/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { c as _c } from "react-compiler-runtime";
import type { ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
import { useMemo } from 'react';
import { findToolByName, type Tool, type Tools } from '../../../Tool.js';
import { findToolByNameOrUniquePrefix, type Tool, type Tools } from '../../../Tool.js';
import type { buildMessageLookups } from '../../../utils/messages.js';
export function useGetToolFromMessages(toolUseID, tools, lookups) {
const $ = _c(7);
Expand All @@ -13,7 +13,7 @@ export function useGetToolFromMessages(toolUseID, tools, lookups) {
t0 = null;
break bb0;
}
const tool = findToolByName(tools, toolUse.name);
const tool = findToolByNameOrUniquePrefix(tools, toolUse.name);
if (!tool) {
t0 = null;
break bb0;
Expand Down
4 changes: 2 additions & 2 deletions src/components/tasks/renderToolActivity.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import { Text } from '../../ink.js';
import type { Tools } from '../../Tool.js';
import { findToolByName } from '../../Tool.js';
import { findToolByNameOrUniquePrefix } from '../../Tool.js';
import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
import type { ThemeName } from '../../utils/theme.js';
export function renderToolActivity(activity: ToolActivity, tools: Tools, theme: ThemeName): React.ReactNode {
const tool = findToolByName(tools, activity.toolName);
const tool = findToolByNameOrUniquePrefix(tools, activity.toolName);
if (!tool) {
return activity.toolName;
}
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useDirectConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
DirectConnectSessionManager,
} from '../server/directConnectManager.js'
import type { Tool } from '../Tool.js'
import { findToolByName } from '../Tool.js'
import { findToolByNameOrUniquePrefix } from '../Tool.js'
import type { Message as MessageType } from '../types/message.js'
import type { PermissionAskDecision } from '../types/permissions.js'
import { logForDebugging } from '../utils/debug.js'
Expand Down Expand Up @@ -90,7 +90,7 @@ export function useDirectConnect({
)

const tool =
findToolByName(toolsRef.current, request.tool_name) ??
findToolByNameOrUniquePrefix(toolsRef.current, request.tool_name) ??
createToolStub(request.tool_name)

const syntheticMessage = createSyntheticAssistantMessage(
Expand Down
7 changes: 5 additions & 2 deletions src/hooks/useInboxPoller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
useAppStateStore,
useSetAppState,
} from '../state/AppState.js'
import { findToolByName } from '../Tool.js'
import { findToolByNameOrUniquePrefix } from '../Tool.js'
import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js'
import { getAllBaseTools } from '../tools.js'
import type { PermissionUpdate } from '../types/permissions.js'
Expand Down Expand Up @@ -267,7 +267,10 @@ export function useInboxPoller({
// Route through the standard ToolUseConfirmQueue so tmux workers
// get the same tool-specific UI (BashPermissionRequest, FileEditToolDiff, etc.)
// as in-process teammates.
const tool = findToolByName(getAllBaseTools(), parsed.tool_name)
const tool = findToolByNameOrUniquePrefix(
getAllBaseTools(),
parsed.tool_name,
)
if (!tool) {
logForDebugging(
`[InboxPoller] Unknown tool ${parsed.tool_name}, skipping permission request`,
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useRemoteSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { useSetAppState } from '../state/AppState.js'
import type { AppState } from '../state/AppStateStore.js'
import type { Tool } from '../Tool.js'
import { findToolByName } from '../Tool.js'
import { findToolByNameOrUniquePrefix } from '../Tool.js'
import type { Message as MessageType } from '../types/message.js'
import type { PermissionAskDecision } from '../types/permissions.js'
import { logForDebugging } from '../utils/debug.js'
Expand Down Expand Up @@ -338,7 +338,7 @@ export function useRemoteSession({

// Look up the Tool object by name, or create a stub for unknown tools
const tool =
findToolByName(toolsRef.current, request.tool_name) ??
findToolByNameOrUniquePrefix(toolsRef.current, request.tool_name) ??
createToolStub(request.tool_name)

const syntheticMessage = createSyntheticAssistantMessage(
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useSSHSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
import type { SSHSession } from '../ssh/createSSHSession.js'
import type { SSHSessionManager } from '../ssh/SSHSessionManager.js'
import type { Tool } from '../Tool.js'
import { findToolByName } from '../Tool.js'
import { findToolByNameOrUniquePrefix } from '../Tool.js'
import type { Message as MessageType } from '../types/message.js'
import type { PermissionAskDecision } from '../types/permissions.js'
import { logForDebugging } from '../utils/debug.js'
Expand Down Expand Up @@ -94,7 +94,7 @@ export function useSSHSession({
)

const tool =
findToolByName(toolsRef.current, request.tool_name) ??
findToolByNameOrUniquePrefix(toolsRef.current, request.tool_name) ??
createToolStub(request.tool_name)

const syntheticMessage = createSyntheticAssistantMessage(
Expand Down
4 changes: 2 additions & 2 deletions src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from 'src/services/analytics/index.js'
import { ImageSizeError } from './utils/imageValidation.js'
import { ImageResizeError } from './utils/imageResizer.js'
import { findToolByName, type ToolUseContext } from './Tool.js'
import { findToolByNameOrUniquePrefix, type ToolUseContext } from './Tool.js'
import { asSystemPrompt, type SystemPrompt } from './utils/systemPromptType.js'
import type {
AssistantMessage,
Expand Down Expand Up @@ -947,7 +947,7 @@ async function* queryLoop(
typeof block.input === 'object' &&
block.input !== null
) {
const tool = findToolByName(
const tool = findToolByNameOrUniquePrefix(
toolUseContext.options.tools,
block.name,
)
Expand Down
Loading
Loading