diff --git a/specification/v2_storage.md b/specification/v2_storage.md index 33e8687a..3ff99e71 100644 --- a/specification/v2_storage.md +++ b/specification/v2_storage.md @@ -304,7 +304,9 @@ interface LogsDisplayActions { ```typescript interface ExecutionFormState { // Tools - selectedToolName: string | null; + // The selected row's `toolRowKey` (`index:name`), not the tool's name — a + // `tools/list` may repeat a name (#2001). + selectedToolKey: string | null; toolFormValues: Record; lastToolResult: ToolResult | null; diff --git a/specification/v2_ux_interfaces.md b/specification/v2_ux_interfaces.md index f1e3a158..c97c03e8 100644 --- a/specification/v2_ux_interfaces.md +++ b/specification/v2_ux_interfaces.md @@ -646,7 +646,8 @@ will close out as part of that work. - **Purpose**: Searchable tool list sidebar with list-changed indicator. - **Current props**: `tools: ToolListItemProps[]`, `listChanged`, `onRefreshList`, `onSelectTool`. - **MCP schema touch points**: `Tool`, `ListToolsResult`, `ToolListChangedNotification`. -- **Target props**: `tools: Tool[]`, `selectedName?`, `listChanged`, `onRefreshList`, `onSelectTool`. +- **Target props**: `tools: Tool[]`, `selectedKey?`, `listChanged`, `onRefreshList`, `onSelectTool`. +- **Row identity**: a `tools/list` may repeat a name, so rows are keyed — for React reconciliation *and* for selection — by `toolRowKey(name, sourceIndex)` (`utils/toolUtils.ts`), not by `tool.name`. `selectedKey` and `onSelectTool` both carry that key (#1957/#2001). - **Callbacks → core hook**: `useManagedTools`. - **Internal refactors**: Consume schema `Tool[]` directly. @@ -716,14 +717,14 @@ themselves — every callback passed down must originate in a core hook. - **MCP schema touch points**: `Tool`, `ListToolsResult`, `CallToolRequest.params`, `CallToolResult`, `ToolListChangedNotification`. - **Target props**: - `tools: Tool[]` — raw MCP `Tool[]` from `ListToolsResult.tools`. - - `selectedToolName?: string` — selection is screen-local; detail derived from `tools`. + - `selectedToolKey?: string` — selection is screen-local; detail derived from `tools`. This is the row's `toolRowKey`, not its name: duplicate names would otherwise select every copy at once and resolve only the first (#2001). - `callState?: { status: 'idle' | 'pending' | 'ok' | 'error'; request?: CallToolRequest['params']; result?: CallToolResult; error?: string }`. - `listChanged: boolean` — driven by `notifications/tools/list_changed`. - - `onRefreshList: () => void`, `onSelectTool: (name: string) => void`, `onCallTool: (name: string, args: Record) => void`. + - `onRefreshList: () => void`, `onSelectTool: (key: string) => void`, `onCallTool: (name: string, args: Record) => void` — note `onSelectTool` carries the row key (UI identity) while `onCallTool` carries the protocol name (wire identity). - **Callbacks → core hook**: `useManagedTools` provides `tools`, `listChanged`, `refreshTools`, `callTool`, `lastCallResult`; backed by `useInspectorClient` for the underlying `client.request`. - **Internal refactors**: - Stop flattening `Tool` into `ToolListItemProps` at the screen boundary — pass `Tool` down and let `ToolListItem` project fields. - - Derive the selected tool from `tools.find(t => t.name === selectedToolName)` instead of receiving a pre-built `ToolDetailPanelProps`. + - Derive the selected tool from `findToolByRowKey(tools, selectedToolKey)` instead of receiving a pre-built `ToolDetailPanelProps`. - Replace the separate `result` prop with a unified `callState` so pending/error UI can be rendered by `ToolResultPanel`. ### PromptsScreen