inspector/clients/web/src/components/screens/screenUiState.ts
cliffhall ab40fb621a 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>
2026-08-17 00:02:13 -04:00

81 lines
2.7 KiB
TypeScript

// Empty-state defaults for each screen's lifted `ui` object — the value App
// seeds its per-screen UI state with and resets to on disconnect (#1417). The
// `*UiState` interfaces live alongside their screens; the defaults are gathered
// here (a non-component module) so the screen files can keep a single component
// export under the react-refresh rule, mirroring logLevels.ts / fetchCategories.ts.
import type { ToolsUiState } from "./ToolsScreen/ToolsScreen";
import type { PromptsUiState } from "./PromptsScreen/PromptsScreen";
import type { ResourcesUiState } from "./ResourcesScreen/ResourcesScreen";
import type { AppsUiState } from "./AppsScreen/AppsScreen";
import type { TasksUiState } from "./TasksScreen/TasksScreen";
import type { LogsUiState } from "./LoggingScreen/LoggingScreen";
import type { ProtocolUiState } from "./ProtocolScreen/ProtocolScreen";
import type { NetworkUiState } from "./NetworkScreen/NetworkScreen";
import type { ConsoleUiState } from "./ConsoleScreen/ConsoleScreen";
import { ALL_LEVELS_VISIBLE } from "./LoggingScreen/logLevels";
import { ALL_CATEGORIES_VISIBLE } from "./NetworkScreen/fetchCategories";
export const EMPTY_TOOLS_UI: ToolsUiState = {
selectedToolKey: undefined,
formValues: {},
search: "",
runAsTask: false,
};
export const EMPTY_PROMPTS_UI: PromptsUiState = {
selectedPromptName: undefined,
argumentValues: {},
submittedFor: undefined,
search: "",
};
export const EMPTY_RESOURCES_UI: ResourcesUiState = {
selectedResourceUri: undefined,
selectedTemplateUri: undefined,
originatingTemplateUri: undefined,
search: "",
openSections: undefined,
};
export const EMPTY_APPS_UI: AppsUiState = {
selectedAppName: undefined,
formValues: {},
search: "",
};
export const EMPTY_TASKS_UI: TasksUiState = {
search: "",
statusFilter: undefined,
};
export const EMPTY_LOGS_UI: LogsUiState = {
filterText: "",
visibleLevels: ALL_LEVELS_VISIBLE,
};
export const EMPTY_PROTOCOL_UI: ProtocolUiState = {
search: "",
methodFilter: undefined,
visibleDirections: { client: true, server: true },
};
export const EMPTY_NETWORK_UI: NetworkUiState = {
filterText: "",
visibleCategories: ALL_CATEGORIES_VISIBLE,
};
export const EMPTY_CONSOLE_UI: ConsoleUiState = {
filterText: "",
};
/** Registry for OAuth resume snapshot restore (data-only; setters live in App). */
export const TAB_UI_REGISTRY = {
Apps: { empty: EMPTY_APPS_UI },
Tools: { empty: EMPTY_TOOLS_UI },
Prompts: { empty: EMPTY_PROMPTS_UI },
Resources: { empty: EMPTY_RESOURCES_UI },
Tasks: { empty: EMPTY_TASKS_UI },
Logs: { empty: EMPTY_LOGS_UI },
Protocol: { empty: EMPTY_PROTOCOL_UI },
Network: { empty: EMPTY_NETWORK_UI },
} as const;