Address Homarr provider review feedback

This commit is contained in:
ajnart committed 2026-08-11 21:50:40 +02:00
1 parent 4ebaea29fd
commit ea9075bde5
9 files changed
+199 -88

No files matched your search

@@ -1,6 +1,10 @@
import { describe, expect, test } from "vitest";
import { getAssistantProviderQuotaLevel, isAssistantProviderUnavailable } from "./assistant-provider-quota";
import {
getAssistantProviderQuotaLevel,
getAssistantProviderQuotaRefreshDelay,
isAssistantProviderUnavailable,
} from "./assistant-provider-quota";
describe("getAssistantProviderQuotaLevel", () => {
test.each([
@@ -19,4 +23,10 @@ describe("getAssistantProviderQuotaLevel", () => {
expect(isAssistantProviderUnavailable({ provider: "homarr", signedIn: true, remaining: undefined })).toBe(false);
expect(isAssistantProviderUnavailable({ provider: "openrouter", signedIn: false, remaining: 0 })).toBe(false);
});
test("refreshes just after the UTC reset, or immediately when it has passed", () => {
const reset = "2026-08-12T00:00:00.000Z";
expect(getAssistantProviderQuotaRefreshDelay(reset, Date.parse("2026-08-11T23:59:00.000Z"))).toBe(61_000);
expect(getAssistantProviderQuotaRefreshDelay(reset, Date.parse("2026-08-12T00:00:01.000Z"))).toBe(1_000);
});
});
@@ -3,6 +3,9 @@ import type { AssistantProvider } from "@homarr/definitions";
export type AssistantProviderQuotaLevel = "ok" | "warning" | "bad" | "dead";
export const getAssistantProviderQuotaRefreshDelay = (resetsAt: string, now = Date.now()) =>
Math.max(0, Date.parse(resetsAt) - now) + 1_000;
export const getAssistantProviderQuotaLevel = (
usage: Pick<WorkshopAssistantUsage, "limit" | "remaining">,
): AssistantProviderQuotaLevel => {
@@ -29,7 +29,7 @@ import { createWorkshopClient } from "~/components/workshop/workshop-client";
import { AssistantContext, AssistantPreferencesContext, useAssistantPreferences } from "./assistant-context";
import { shouldAutomaticallyContinueAssistant } from "./assistant-auto-submit";
import { isAssistantProviderUnavailable } from "./assistant-provider-quota";
import { getAssistantProviderQuotaRefreshDelay, isAssistantProviderUnavailable } from "./assistant-provider-quota";
import { AssistantAutoApprovalProvider } from "./assistant-auto-approval";
import { getRunningAssistantPartType } from "./assistant-activity-state";
import { prepareAssistantRequestBody } from "./assistant-attachment-payload";
@@ -237,6 +237,12 @@ const AssistantPreferencesProvider = ({ children }: PropsWithChildren) => {
void refreshQuota();
}, [data?.provider, providerUser, refreshQuota]);
useEffect(() => {
if (data?.provider !== "homarr" || !providerUser || !quota) return;
const timeout = window.setTimeout(() => void refreshQuota(), getAssistantProviderQuotaRefreshDelay(quota.resetsAt));
return () => window.clearTimeout(timeout);
}, [data?.provider, providerUser, quota, refreshQuota]);
const value = useMemo(
() => ({
provider: data?.provider ?? null,
@@ -3,13 +3,23 @@ import { WORKSHOP_API_URL, WORKSHOP_WEB_URL } from "@homarr/workshop/schema";
let browserClient: WorkshopBackend | undefined;
export function getWorkshopApiUrl() {
const runtimeUrl =
typeof document === "undefined"
? undefined
: document.querySelector<HTMLMetaElement>('meta[name="homarr-workshop-api-url"]')?.content;
return (runtimeUrl || WORKSHOP_API_URL).replace(/\/+$/u, "");
}
export function getWorkshopAssistantProviderUrl() {
return `${getWorkshopApiUrl()}/api/ai/v1`;
}
export function createWorkshopClient() {
if (typeof document === "undefined") return new WorkshopBackend(WORKSHOP_API_URL);
if (typeof document === "undefined") return new WorkshopBackend(getWorkshopApiUrl());
if (browserClient) return browserClient;
const runtimeUrl = document.querySelector<HTMLMetaElement>('meta[name="homarr-workshop-api-url"]')?.content;
browserClient = new WorkshopBackend(runtimeUrl || WORKSHOP_API_URL);
browserClient = new WorkshopBackend(getWorkshopApiUrl());
return browserClient;
}
@@ -3,7 +3,7 @@
import { readFileSync } from "node:fs";
import { afterEach, describe, expect, it } from "vitest";
import { getWorkshopWebUrl } from "./workshop-client";
import { getWorkshopAssistantProviderUrl, getWorkshopWebUrl } from "./workshop-client";
describe("Workshop widget install flow", () => {
afterEach(() => {
@@ -17,6 +17,12 @@ describe("Workshop widget install flow", () => {
expect(getWorkshopWebUrl("widget/id")).toBe("https://preview.example/workshop/widget%2Fid");
});
it("derives the Homarr provider endpoint from the runtime Workshop API", () => {
document.head.innerHTML = '<meta name="homarr-workshop-api-url" content="https://api.preview.example/">';
expect(getWorkshopAssistantProviderUrl()).toBe("https://api.preview.example/api/ai/v1");
});
it("uses one registered modal stack for browsing, details, reports, and confirmation", () => {
const installButton = readFileSync(
`${process.cwd()}/apps/nextjs/src/components/workshop/workshop-install-button.tsx`,