feat: expose assistant global shortcuts

This commit is contained in:
ajnart committed 2026-08-14 11:32:10 +02:00
1 parent 4b647ec2c9
commit 73e4b80a26
9 files changed
+90 -28

No files matched your search

@@ -30,12 +30,7 @@ import { createId } from "@homarr/common";
import { hotkeys } from "@homarr/definitions";
import { showErrorNotification, showWarningNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import {
openMediaRequestSearch,
openSpotlight,
useRegisterSpotlightContextActions,
useRegisterSpotlightContextResults,
} from "@homarr/spotlight";
import { openMediaRequestSearch, openSpotlight, useRegisterSpotlightContextResults } from "@homarr/spotlight";
import { shouldAutomaticallyContinueAssistant } from "./assistant-auto-submit";
import { AssistantAskUserTool, AssistantConfigureAppTool } from "./assistant-human-tools";
@@ -638,12 +633,12 @@ const EnabledAssistantProvider = ({ children }: PropsWithChildren) => {
name: t("spotlight"),
icon: "/logo/logo.png",
description: t("spotlightDescription"),
interaction: () => createAssistantPromptInteraction({ sendPrompt }),
alwaysVisible: true,
interaction: (query: string) => createAssistantPromptInteraction({ sendPrompt, prompt: query }),
}),
[sendPrompt, t],
);
useRegisterSpotlightContextResults("homarr-assistant", [spotlightItem], [spotlightItem]);
useRegisterSpotlightContextActions("homarr-assistant", [spotlightItem], [spotlightItem]);
const value = useMemo(
() => ({ enabled: true, opened, isRunning: assistantIsRunning, unreadCount, open, close, toggle, sendPrompt }),
@@ -691,12 +686,12 @@ const DisabledAssistantProvider = ({ children, description }: DisabledAssistantP
icon: "/logo/logo.png",
description,
unavailable: true,
interaction: () => ({ type: "none" as const }),
alwaysVisible: true,
interaction: (_query: string) => ({ type: "none" as const }),
}),
[description, t],
);
useRegisterSpotlightContextResults("homarr-assistant", [spotlightItem], [spotlightItem]);
useRegisterSpotlightContextActions("homarr-assistant", [spotlightItem], [spotlightItem]);
const value = useMemo(
() => ({
@@ -3,9 +3,27 @@ import { describe, expect, it, vi } from "vitest";
import { createAssistantPromptInteraction } from "./assistant-spotlight";
describe("createAssistantPromptInteraction", () => {
it("sends an existing default-search query immediately", () => {
const sendPrompt = vi.fn();
const interaction = createAssistantPromptInteraction({
sendPrompt,
prompt: " Check my services ",
});
expect(interaction.type).toBe("javaScript");
if (interaction.type !== "javaScript") return;
interaction.onSelect();
expect(interaction.closeSpotlightOnTrigger).toBe(true);
expect(sendPrompt).toHaveBeenCalledWith("Check my services");
});
it("keeps Spotlight open when the prompt is empty", () => {
const sendPrompt = vi.fn();
const interaction = createAssistantPromptInteraction({ sendPrompt });
expect(interaction.type).toBe("children");
if (interaction.type !== "children") return;
const [action] = interaction.useActions({}, " ");
expect(action).toBeDefined();
if (!action) return;
@@ -22,6 +40,9 @@ describe("createAssistantPromptInteraction", () => {
it("trims and sends a prompt in the background", () => {
const sendPrompt = vi.fn();
const interaction = createAssistantPromptInteraction({ sendPrompt });
expect(interaction.type).toBe("children");
if (interaction.type !== "children") return;
const [action] = interaction.useActions({}, " Check my services ");
expect(action).toBeDefined();
if (!action) return;
@@ -7,6 +7,7 @@ import { useScopedI18n } from "@homarr/translation/client";
interface AssistantPromptInteractionOptions {
sendPrompt: (prompt: string) => void;
prompt?: string;
}
const AssistantPromptDetail = () => {
@@ -39,25 +40,35 @@ const AssistantPromptAction = ({ hasPrompt }: { hasPrompt: boolean }) => {
);
};
export const createAssistantPromptInteraction = ({ sendPrompt }: AssistantPromptInteractionOptions) =>
({
export const createAssistantPromptInteraction = ({ sendPrompt, prompt = "" }: AssistantPromptInteractionOptions) => {
const trimmedPrompt = prompt.trim();
if (trimmedPrompt.length > 0) {
return {
type: "javaScript" as const,
onSelect: () => sendPrompt(trimmedPrompt),
closeSpotlightOnTrigger: true,
};
}
return {
type: "children",
option: {},
DetailComponent: AssistantPromptDetail,
useActions: (_options: Record<string, unknown>, query: string) => {
const prompt = query.trim();
const nextPrompt = query.trim();
return [
{
key: "send-assistant-prompt",
Component: () => <AssistantPromptAction hasPrompt={prompt.length > 0} />,
Component: () => <AssistantPromptAction hasPrompt={nextPrompt.length > 0} />,
useInteraction: () => ({
type: "javaScript" as const,
onSelect: () => {
if (prompt.length > 0) sendPrompt(prompt);
if (nextPrompt.length > 0) sendPrompt(nextPrompt);
},
closeSpotlightOnTrigger: prompt.length > 0,
closeSpotlightOnTrigger: nextPrompt.length > 0,
}),
},
];
},
}) as const;
} as const;
};