fix(web): key tool selection by row identity, not name
#1961 keyed the sidebar's React children by source position, but everything downstream still identified a tool by `tool.name`. With a `tools/list` that repeats a name, both copies highlighted at once, the click guard made the other copy a no-op, and `tools.find((t) => t.name === …)` always resolved to the first — so a later copy rendered but could never be opened. Thread the row key through selection instead: - `utils/toolUtils.ts` now owns `toolRowKey` (moved out of ToolControls) and adds `findToolByRowKey`, which compares computed keys rather than parsing one so the two can't drift. - `ToolControls`: `selectedName` → `selectedKey`; the highlight and the click guard compare keys, and `onSelectTool` emits the row key. - `ToolsScreen`: `ToolsUiState.selectedToolName` → `selectedToolKey`, with both lookups going through `findToolByRowKey`. Renamed through `screenUiState.ts` and `App.tsx`'s `onToolsUiChange`. `onCallTool` still sends `selectedTool.name` — the duplicated name is the genuine wire identity; only the UI identity needed to be unique. Also prefixes the excluded (SEP-2243) rows' keys: they render into the same `Stack` as the list rows but index within their own list, so an excluded tool sharing a name and index with a listed one collided in the key space. Closes #2001 Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
e9a581d912
commit
ab40fb621a
1 changed files with 210 additions and 48 deletions
|
|
@ -363,7 +363,7 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
InspectorView: (props: {
|
||||
toolCallState?: { status?: string };
|
||||
toolsUi?: {
|
||||
selectedToolName?: string;
|
||||
selectedToolKey?: string;
|
||||
formValues: Record<string, unknown>;
|
||||
search: string;
|
||||
};
|
||||
|
|
@ -384,7 +384,7 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
onConnectionInfo: () => void;
|
||||
onToggleConnection: (id: string) => void;
|
||||
onToolsUiChange: (next: {
|
||||
selectedToolName?: string;
|
||||
selectedToolKey?: string;
|
||||
formValues: Record<string, unknown>;
|
||||
search: string;
|
||||
}) => void;
|
||||
|
|
@ -433,7 +433,7 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
{Object.keys(props.progressByTaskId ?? {}).join(",") || "none"}
|
||||
</span>
|
||||
<span data-testid="selected-tool">
|
||||
{props.toolsUi?.selectedToolName ?? "none"}
|
||||
{props.toolsUi?.selectedToolKey ?? "none"}
|
||||
</span>
|
||||
<span data-testid="tool-search">{props.toolsUi?.search || "none"}</span>
|
||||
<span data-testid="selected-prompt">
|
||||
|
|
@ -469,7 +469,7 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
formValues: {},
|
||||
search: "",
|
||||
...props.toolsUi,
|
||||
selectedToolName: "get_acts",
|
||||
selectedToolKey: "0:get_acts",
|
||||
})
|
||||
}
|
||||
>
|
||||
|
|
@ -481,7 +481,7 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
formValues: {},
|
||||
search: "",
|
||||
...props.toolsUi,
|
||||
selectedToolName: "other_tool",
|
||||
selectedToolKey: "1:other_tool",
|
||||
})
|
||||
}
|
||||
>
|
||||
|
|
@ -869,7 +869,7 @@ describe("App session-scoped state reset on disconnect", () => {
|
|||
expect(screen.getByTestId("tool-status")).toHaveTextContent("ok"),
|
||||
);
|
||||
|
||||
// A search keystroke leaves `selectedToolName` unchanged, so the result
|
||||
// A search keystroke leaves `selectedToolKey` unchanged, so the result
|
||||
// stays put.
|
||||
await user.click(screen.getByText("set-tool-search"));
|
||||
await waitFor(() =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue