feat(web): list-changed handling — indicator, pull-on-demand default, and per-server auto-refresh opt-in (#1402) (#1443)
* feat(web): wire the listChanged indicator across all screens (#1402) The per-screen list-changed indicator was hardcoded false in InspectorView for Tools, Apps, Prompts, and Resources, so it could never light up. Source it from the managed-state layer instead. Each ManagedXState now tracks a listChanged flag: set true when its */list_changed notification arrives (the existing auto-refresh leaves it set), exposed via getListChanged()/a listChangedChange event, and cleared by clearListChanged() or on disconnect. The useManagedX hooks surface the flag and clear it on a user-initiated refresh (the indicator's Refresh button = acknowledge). App threads the three flags to InspectorView, which maps them to the screens — Apps shares the tools flag since apps are a filtered view of tools. Tests cover set/clear/disconnect at the state-manager layer, the hook subscription + refresh-clears behavior, and the InspectorView routing (including Apps sharing the tools flag and no cross-wiring between screens). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): close clear-after-await race in managed-list refresh (#1402 review) The useManagedX refresh() callbacks cleared the listChanged flag AFTER awaiting the fetch. If a */list_changed arrived mid-fetch, the state re-set the flag (and auto-refreshed), then the trailing clear wiped that genuinely-new signal. Clear BEFORE the await instead: the user's refresh acknowledges only the change in hand, and a mid-fetch change re-sets the flag and survives. Adds a race regression test on the tools hook. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): drop duplicate Refresh on the Apps screen (#1402) AppControls uniquely had a standalone toolbar Refresh button AND the ListChangedIndicator on a separate row, so once the indicator could actually appear (this PR), the Apps screen showed two Refresh buttons. Match the other three controls (Tools/Prompts/Resources): the indicator is the single header-right refresh affordance. Updates AppControls and AppsScreen tests to assert exactly one Refresh (and none when the list hasn't changed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(core): stop auto-invoking */list on list_changed; user pulls via Refresh (#1402) With auto-refresh on, the list was already current by the time the list-changed indicator appeared, making its Refresh button moot. Now the managed Tools/Prompts/Resources state managers only flag the change on */list_changed (they no longer auto-refresh); the list stays as-is until the user clicks Refresh, which fetches the new list and clears the indicator. The connect-time initial load is unchanged. Resource templates (no own indicator, part of the Resources screen) also stop auto-refreshing; onRefreshResources now refreshes both resources and templates, since a single resources/list_changed covers both. Tasks are left auto-refreshing — they have no list-changed indicator and need live status updates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(core): correct ManagedResourceTemplatesState header comments (#1402 review) The class no longer subscribes to resourceTemplatesListChanged; update the stale file/class docstrings that still claimed it syncs on that notification to reflect connect + statusChange only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): add per-server 'Auto Refresh on List Changed Notifications' option (#1402) New Options section in ServerSettingsForm with a single checkbox. When checked, the managed Tools/Prompts/Resources (and resource templates) state auto-refreshes on */list_changed notifications; when unchecked (default) the notification only lights the list-changed indicator and the user pulls via Refresh. Plumbing: new optional autoRefreshOnListChanged on InspectorServerSettings and StoredMCPServer, round-tripped through serverList (lifted with a false default, persisted only when true, added to the exhaustive field-key map); surfaced from InspectorClient via a new getServerSettings() on the protocol; the state managers read it live at notification time to decide auto-refresh vs flag-only. Resource templates re-subscribe to their list_changed but only auto-refresh when the option is on (they have no indicator of their own). Tests cover the round-trip (true persists, false omitted), the form section (render/checked/toggle), and each state manager's on/off branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(server): persist autoRefreshOnListChanged through validateSettings (#1402) The PUT /api/servers write path runs settings through validateSettings, which rebuilds the value from explicitly-named fields and silently drops unknown keys — so autoRefreshOnListChanged never reached disk (the serverList round-trip test passed because it calls inspectorSettingsToStoredFields directly, bypassing this path). Validate it as an optional boolean and include it in the built value; false is omitted on disk by inspectorSettingsToStoredFields as before. Adds integration tests over the real PUT path: true persists, false is omitted, and a non-boolean is rejected (400). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): default the Server Settings modal to the Options section (#1402) Open 'Options' instead of 'Custom Headers' when the Server Settings modal first opens, so the new Auto Refresh toggle is the first thing surfaced. Updates the modal default-expanded section, the modal test's initial-state assertions, the three header tests (now expand 'Custom Headers' first since it's no longer open by default), and the story defaults. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: drop accidentally-committed Inspector history dump; gitignore the class 83076441 swept an exported Inspector message-history JSON (manual-testing artifact at the repo root) into the tree via git add -A. Untrack it and extend the existing repo-root debug-artifact .gitignore block (alongside /*.png) to cover /inspector-history-*.json so these can't be re-added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9b558afee1
commit
b0a024269e
2 changed files with 1109 additions and 93 deletions
|
|
@ -56,6 +56,9 @@ function makeProps(
|
|||
prompts: [],
|
||||
resources: [],
|
||||
resourceTemplates: [],
|
||||
toolsListChanged: false,
|
||||
promptsListChanged: false,
|
||||
resourcesListChanged: false,
|
||||
subscriptions: [],
|
||||
logs: [],
|
||||
tasks: [],
|
||||
|
|
@ -585,4 +588,104 @@ describe("InspectorView", () => {
|
|||
await screen.findByRole("button", { name: "Expand all" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("listChanged indicator wiring (#1402)", () => {
|
||||
// The indicator only mounts on the active screen, so each case connects,
|
||||
// navigates to the target tab, and asserts the "List updated" affordance.
|
||||
async function gotoTab(tab: string) {
|
||||
const user = userEvent.setup();
|
||||
const tabSelect = await screen.findByDisplayValue("Servers");
|
||||
await user.click(tabSelect);
|
||||
await user.click(await screen.findByText(tab));
|
||||
return user;
|
||||
}
|
||||
|
||||
it("routes toolsListChanged to the Tools screen indicator", async () => {
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
activeServer: "alpha",
|
||||
connectionStatus: "connected",
|
||||
initializeResult: connectedInit,
|
||||
toolsListChanged: true,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
await gotoTab("Tools");
|
||||
expect(await screen.findByText("List updated")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shares the tools flag with the Apps screen (apps are filtered tools)", async () => {
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
activeServer: "alpha",
|
||||
connectionStatus: "connected",
|
||||
initializeResult: connectedInit,
|
||||
toolsListChanged: true,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
await gotoTab("Apps");
|
||||
expect(await screen.findByText("List updated")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("routes promptsListChanged to the Prompts screen indicator", async () => {
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
activeServer: "alpha",
|
||||
connectionStatus: "connected",
|
||||
initializeResult: connectedInit,
|
||||
promptsListChanged: true,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
await gotoTab("Prompts");
|
||||
expect(await screen.findByText("List updated")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("routes resourcesListChanged to the Resources screen indicator", async () => {
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
activeServer: "alpha",
|
||||
connectionStatus: "connected",
|
||||
initializeResult: connectedInit,
|
||||
resourcesListChanged: true,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
await gotoTab("Resources");
|
||||
expect(await screen.findByText("List updated")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show the indicator on a screen whose flag is false (no cross-wiring)", async () => {
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
activeServer: "alpha",
|
||||
connectionStatus: "connected",
|
||||
initializeResult: connectedInit,
|
||||
// Tools changed, but Prompts did not — the Prompts screen must
|
||||
// stay quiet.
|
||||
toolsListChanged: true,
|
||||
promptsListChanged: false,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
await gotoTab("Prompts");
|
||||
// The Prompts screen has mounted (its heading is present)...
|
||||
expect(
|
||||
await screen.findByRole("heading", { name: "Prompts" }),
|
||||
).toBeInTheDocument();
|
||||
// ...but the indicator is not, since promptsListChanged is false.
|
||||
expect(screen.queryByText("List updated")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -201,6 +201,13 @@ export interface InspectorViewProps {
|
|||
resources: Resource[];
|
||||
resourceTemplates: ResourceTemplate[];
|
||||
subscriptions: InspectorResourceSubscription[];
|
||||
|
||||
// "List changed since last refresh" flags, sourced from the managed-state
|
||||
// layer (#1402). They light the per-screen list-changed indicator. Apps is a
|
||||
// filtered view of tools, so it shares the tools flag.
|
||||
toolsListChanged: boolean;
|
||||
promptsListChanged: boolean;
|
||||
resourcesListChanged: boolean;
|
||||
logs: LogEntryData[];
|
||||
tasks: Task[];
|
||||
progressByTaskId?: Record<string, TaskProgress>;
|
||||
|
|
@ -336,6 +343,9 @@ export function InspectorView({
|
|||
prompts,
|
||||
resources,
|
||||
resourceTemplates,
|
||||
toolsListChanged,
|
||||
promptsListChanged,
|
||||
resourcesListChanged,
|
||||
subscriptions,
|
||||
logs,
|
||||
tasks,
|
||||
|
|
@ -548,7 +558,7 @@ export function InspectorView({
|
|||
tools={tools}
|
||||
callState={toolCallState}
|
||||
ui={toolsUi}
|
||||
listChanged={false}
|
||||
listChanged={toolsListChanged}
|
||||
serverSupportsTaskToolCalls={serverSupportsTaskToolCalls}
|
||||
onUiChange={onToolsUiChange}
|
||||
onRefreshList={onRefreshTools}
|
||||
|
|
@ -560,7 +570,7 @@ export function InspectorView({
|
|||
<ScreenStage active={activeTab === "Apps"}>
|
||||
<AppsScreen
|
||||
tools={appTools}
|
||||
listChanged={false}
|
||||
listChanged={toolsListChanged}
|
||||
sandboxPath={sandboxPath}
|
||||
bridgeFactory={bridgeFactory}
|
||||
rendererRef={appRendererRef}
|
||||
|
|
@ -578,7 +588,7 @@ export function InspectorView({
|
|||
prompts={prompts}
|
||||
getPromptState={getPromptState}
|
||||
ui={promptsUi}
|
||||
listChanged={false}
|
||||
listChanged={promptsListChanged}
|
||||
completionsSupported={completionsSupported}
|
||||
onUiChange={onPromptsUiChange}
|
||||
onRefreshList={onRefreshPrompts}
|
||||
|
|
@ -594,7 +604,7 @@ export function InspectorView({
|
|||
subscriptions={subscriptions}
|
||||
readState={readResourceState}
|
||||
ui={resourcesUi}
|
||||
listChanged={false}
|
||||
listChanged={resourcesListChanged}
|
||||
completionsSupported={completionsSupported}
|
||||
onUiChange={onResourcesUiChange}
|
||||
onRefreshList={onRefreshResources}
|
||||
|
|
|
|||
Loading…
Reference in a new issue