Skip to content

Commit 8eff67b

Browse files
committed
polish Hermes and Pi provider setup UX
1 parent d439ab1 commit 8eff67b

6 files changed

Lines changed: 383 additions & 17 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ T3 Code is a minimal web GUI for coding agents (currently Codex, Claude, OpenCod
1616
1717
Hermes setup notes: [docs/providers/hermes.md](./docs/providers/hermes.md)
1818
Pi setup notes: [docs/providers/pi.md](./docs/providers/pi.md)
19+
Release readiness checklist: [docs/providers/release-readiness.md](./docs/providers/release-readiness.md)
1920

2021
## Hermes Agent support
2122

apps/web/src/components/chat/ModelPickerContent.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
238238
const sidebarInstanceEntries = showLockedInstanceSidebar
239239
? lockedInstanceEntries
240240
: instanceEntries.filter((entry) => visibleRailInstanceSet.has(entry.instanceId));
241+
const selectedEntry =
242+
selectedInstanceId === "favorites" ? null : (entryByInstanceId.get(selectedInstanceId) ?? null);
241243
const instanceOrder = useMemo(
242244
() => instanceEntries.map((entry) => entry.instanceId),
243245
[instanceEntries],
@@ -423,6 +425,27 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
423425
(): string[] => filteredModels.map((model) => `${model.instanceId}:${model.slug}`),
424426
[filteredModels],
425427
);
428+
const emptyMessage = useMemo(() => {
429+
if (searchQuery.trim().length > 0) {
430+
return "No models match that search";
431+
}
432+
if (selectedInstanceId === "favorites") {
433+
return "No favorite models yet";
434+
}
435+
if (selectedEntry && !selectedEntry.enabled) {
436+
return `${selectedEntry.displayName} is disabled in Settings`;
437+
}
438+
if (selectedEntry && selectedEntry.status !== "ready") {
439+
const detail = selectedEntry.snapshot.message?.trim();
440+
return detail
441+
? `${selectedEntry.displayName} needs setup: ${detail}`
442+
: `${selectedEntry.displayName} needs setup in Settings`;
443+
}
444+
if (selectedEntry) {
445+
return `${selectedEntry.displayName} has no models yet. Finish setup, refresh provider status, then try again.`;
446+
}
447+
return "No models found";
448+
}, [searchQuery, selectedEntry, selectedInstanceId]);
426449
const filteredModelByKey = useMemo(
427450
(): ReadonlyMap<string, ModelPickerItem> =>
428451
new Map(filteredModels.map((model) => [`${model.instanceId}:${model.slug}`, model] as const)),
@@ -654,7 +677,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
654677
</ComboboxList>
655678
</div>
656679
<ComboboxEmpty className="not-empty:py-6 empty:h-0 text-xs font-normal leading-snug">
657-
No models found
680+
{emptyMessage}
658681
</ComboboxEmpty>
659682
</div>
660683
</Combobox>

apps/web/src/components/chat/ProviderModelPicker.browser.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,32 @@ describe("ProviderModelPicker", () => {
13301330
}
13311331
});
13321332

1333+
it("explains provider setup when the active Hermes provider has no models", async () => {
1334+
const mounted = await mountPicker({
1335+
activeInstanceId: ProviderInstanceId.make("hermes"),
1336+
model: "hermes-default",
1337+
lockedProvider: null,
1338+
providers: [
1339+
...TEST_PROVIDERS,
1340+
buildHermesProvider({
1341+
models: [],
1342+
}),
1343+
],
1344+
});
1345+
1346+
try {
1347+
await page.getByRole("button").click();
1348+
1349+
await vi.waitFor(() => {
1350+
expect(document.body.textContent ?? "").toContain(
1351+
"Hermes has no models yet. Finish setup, refresh provider status, then try again.",
1352+
);
1353+
});
1354+
} finally {
1355+
await mounted.cleanup();
1356+
}
1357+
});
1358+
13331359
it("accepts outline trigger styling", async () => {
13341360
const mounted = await mountPicker({
13351361
model: "gpt-5-codex",

apps/web/src/components/settings/ProviderInstanceCard.test.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { describe, expect, it } from "vitest";
2-
import type { ServerProviderModel } from "@t3tools/contracts";
2+
import {
3+
ProviderDriverKind,
4+
ProviderInstanceId,
5+
type ServerProviderModel,
6+
} from "@t3tools/contracts";
37

4-
import { deriveProviderModelsForDisplay } from "./ProviderInstanceCard";
8+
import {
9+
buildProviderSetupChecklist,
10+
deriveProviderModelsForDisplay,
11+
} from "./ProviderInstanceCard";
512

613
describe("deriveProviderModelsForDisplay", () => {
714
it("uses current config custom models instead of stale live custom rows", () => {
@@ -34,3 +41,72 @@ describe("deriveProviderModelsForDisplay", () => {
3441
).toEqual(["server-model", "kept-custom"]);
3542
});
3643
});
44+
45+
describe("buildProviderSetupChecklist", () => {
46+
it("treats installed Hermes with CLI-managed auth as ready for a test message", () => {
47+
const checklist = buildProviderSetupChecklist({
48+
displayName: "Hermes",
49+
isHermesDriver: true,
50+
isPiDriver: false,
51+
enabled: true,
52+
configuredBinaryPath: "hermes",
53+
configuredPiBinaryPath: null,
54+
modelCount: 1,
55+
liveProvider: {
56+
driver: ProviderDriverKind.make("hermes"),
57+
instanceId: ProviderInstanceId.make("hermes"),
58+
displayName: "Hermes",
59+
enabled: true,
60+
installed: true,
61+
version: "0.11.0",
62+
status: "ready",
63+
auth: { status: "unknown" },
64+
checkedAt: new Date().toISOString(),
65+
models: [],
66+
slashCommands: [],
67+
skills: [],
68+
},
69+
});
70+
71+
expect(checklist.map((item) => [item.label, item.state])).toEqual([
72+
["Enabled", "complete"],
73+
["CLI detected", "complete"],
74+
["Authentication", "complete"],
75+
["Model visible", "complete"],
76+
]);
77+
});
78+
79+
it("points Pi users at login and model setup when auth and models are missing", () => {
80+
const checklist = buildProviderSetupChecklist({
81+
displayName: "Pi",
82+
isHermesDriver: false,
83+
isPiDriver: true,
84+
enabled: true,
85+
configuredBinaryPath: "pi-acp",
86+
configuredPiBinaryPath: null,
87+
modelCount: 0,
88+
liveProvider: {
89+
driver: ProviderDriverKind.make("pi"),
90+
instanceId: ProviderInstanceId.make("pi"),
91+
displayName: "Pi",
92+
enabled: true,
93+
installed: true,
94+
version: "0.73.1",
95+
status: "error",
96+
auth: { status: "unauthenticated" },
97+
checkedAt: new Date().toISOString(),
98+
models: [],
99+
slashCommands: [],
100+
skills: [],
101+
},
102+
});
103+
104+
expect(checklist.map((item) => [item.label, item.state])).toEqual([
105+
["Enabled", "complete"],
106+
["Adapter detected", "complete"],
107+
["Pi binary", "pending"],
108+
["Authentication", "action"],
109+
["Model visible", "action"],
110+
]);
111+
});
112+
});

0 commit comments

Comments
 (0)