Skip to content

Commit 1dec305

Browse files
fix(ui): show empty state when no model configs exist (kagent-dev#1930) (kagent-dev#1944)
--- Title: fix(ui): show empty state when no model configs exist (kagent-dev#1930) Description: ## What When no `ModelConfig` resources are deployed, the UI showed a "Failed to fetch models" error instead of rendering the normal empty state ("no agents yet" / empty models page). Fixes kagent-dev#1930 ## Why The backend's `StandardResponse.Data` field uses `json:"data,omitempty"`, so an empty list serializes with the `data` field omitted entirely (e.g. `{"error":false,"message":"Successfully listed ModelConfigs"}`), returned with HTTP 200. The UI treated a missing `data` as a fetch failure: ```ts if (!response.data || response.error) { throw new Error(response.error || "Failed to fetch models"); } ``` So a valid "zero model configs" response was misread as an error and surfaced via the shared error state in AgentsProvider (and on the /models page). Change Treat a missing/empty list as a valid empty result, and only fail on an explicit response.error — matching the existing defensive pattern in AgentList (result.data || []): - ui/src/components/AgentsProvider.tsx — fetchModels - ui/src/app/models/page.tsx — fetchModels This is a frontend-only fix; the backend omitempty behavior is unchanged. Screenshots ## Before: <img width="1895" height="970" alt="Screenshot 2026-05-29 225451" src="https://github.com/user-attachments/assets/f15e192f-11c2-4547-abb4-d6bc22e2c78d" /> ## After: <img width="980" height="530" alt="image" src="https://github.com/user-attachments/assets/c812607d-0da9-42ed-81fb-590c9b5978a1" /> --------- Signed-off-by: gauravshinde1729 <shindegauravpict@gmail.com>
1 parent 1b6f30a commit 1dec305

3 files changed

Lines changed: 77 additions & 8 deletions

File tree

ui/src/app/models/page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ export default function ModelsPage() {
3030
try {
3131
setLoading(true);
3232
const response = await getModelConfigs();
33-
if (response.error || !response.data) {
34-
throw new Error(response.error || "Failed to fetch models");
33+
if (response.error) {
34+
throw new Error(response.error);
3535
}
36-
setModels(response.data);
36+
// An empty list is valid (no ModelConfigs deployed). The backend omits
37+
// `data` for empty collections, so treat missing data as an empty list.
38+
setModels(response.data ?? []);
3739
} catch (err) {
3840
const errorMessage = err instanceof Error ? err.message : "Failed to fetch models";
3941
setError(errorMessage);

ui/src/components/AgentsProvider.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,14 @@ export function AgentsProvider({ children }: AgentsProviderProps) {
125125
const fetchModels = useCallback(async () => {
126126
try {
127127
const response = await getModelConfigs();
128-
if (!response.data || response.error) {
129-
throw new Error(response.error || "Failed to fetch models");
128+
if (response.error) {
129+
throw new Error(response.error);
130130
}
131131

132-
setModels(response.data);
132+
// An empty list is a valid result (e.g. no ModelConfigs deployed). The
133+
// backend omits `data` for empty collections (json omitempty), so treat
134+
// missing data as an empty list rather than a fetch failure.
135+
setModels(response.data ?? []);
133136
setError("");
134137
} catch (err) {
135138
console.error("Error fetching models:", err);

ui/src/lib/__tests__/AgentsProvider.namespace.test.tsx

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { render, screen, waitFor } from "@testing-library/react";
2-
import { AgentsProvider } from "@/components/AgentsProvider";
1+
import { act, render, screen, waitFor } from "@testing-library/react";
2+
import { AgentsProvider, useAgents } from "@/components/AgentsProvider";
33
import { getAgents } from "@/app/actions/agents";
44
import { getTools } from "@/app/actions/tools";
55
import { getModelConfigs } from "@/app/actions/modelConfigs";
@@ -22,6 +22,22 @@ const mockGetAgents = getAgents as jest.MockedFunction<typeof getAgents>;
2222
const mockGetTools = getTools as jest.MockedFunction<typeof getTools>;
2323
const mockGetModelConfigs = getModelConfigs as jest.MockedFunction<typeof getModelConfigs>;
2424

25+
// A tiny consumer that surfaces the two pieces of provider state we assert on:
26+
// the shared `error` string and the list of model configs.
27+
function ModelsConsumer() {
28+
const { error, models } = useAgents();
29+
return (
30+
<div>
31+
<p data-testid="model-error">{error}</p>
32+
<ul data-testid="model-list">
33+
{models.map((m) => (
34+
<li key={m.ref}>{m.ref}</li>
35+
))}
36+
</ul>
37+
</div>
38+
);
39+
}
40+
2541
describe("AgentsProvider list fetching", () => {
2642
beforeEach(() => {
2743
jest.clearAllMocks();
@@ -43,4 +59,52 @@ describe("AgentsProvider list fetching", () => {
4359
await waitFor(() => expect(mockGetTools).toHaveBeenCalled());
4460
expect(mockGetAgents).not.toHaveBeenCalled();
4561
});
62+
63+
// Regression for #1930.
64+
//
65+
// When no ModelConfigs are deployed, the backend responds 200 OK but with the
66+
// `data` field omitted entirely (Go json omitempty), e.g.:
67+
// { "error": false, "message": "Successfully listed ModelConfigs" }
68+
// The provider must read that as "zero models", NOT as a fetch failure.
69+
// Before the fix, the missing `data` was treated as an error and the UI showed
70+
// "Failed to fetch models".
71+
it("shows no error when there are no model configs", async () => {
72+
// The response below has no `data` key on purpose — that is exactly what an
73+
// empty list looks like on the wire.
74+
mockGetModelConfigs.mockResolvedValue({ message: "Successfully listed ModelConfigs" });
75+
76+
render(
77+
<AgentsProvider>
78+
<ModelsConsumer />
79+
</AgentsProvider>,
80+
);
81+
82+
// Wait for the fetch to be made, then let its promise + setState calls flush
83+
// so we assert on the state *after* fetchModels has run (not the initial state,
84+
// which also happens to be "no error / no models").
85+
await waitFor(() => expect(mockGetModelConfigs).toHaveBeenCalled());
86+
await act(async () => {});
87+
88+
expect(screen.getByTestId("model-error")).toBeEmptyDOMElement();
89+
expect(screen.getByTestId("model-list").children).toHaveLength(0);
90+
});
91+
92+
// The fix must not hide genuine failures: a real backend error should still be
93+
// surfaced via the provider's `error` state.
94+
it("surfaces a real error returned by the backend", async () => {
95+
mockGetModelConfigs.mockResolvedValue({
96+
message: "Failed",
97+
error: "model configs unavailable",
98+
});
99+
100+
render(
101+
<AgentsProvider>
102+
<ModelsConsumer />
103+
</AgentsProvider>,
104+
);
105+
106+
await waitFor(() =>
107+
expect(screen.getByTestId("model-error")).toHaveTextContent("model configs unavailable"),
108+
);
109+
});
46110
});

0 commit comments

Comments
 (0)