fix(custom-widgets): restore bundled authoring
This commit is contained in:
16 files changed
+180
-58
No files matched your search
@@ -48,10 +48,14 @@ export function buildCustomWidgetAiPrompt(
|
||||
currentConfig?: Partial<HomarrCustomWidgetV2> | Record<string, unknown> | null,
|
||||
request?: string | null,
|
||||
documentationUrl?: string | null,
|
||||
embeddedSkill?: string,
|
||||
) {
|
||||
const sections = [AUTHORING_PROMPT];
|
||||
if (documentationUrl) sections.push(`API documentation: ${redactUrl(documentationUrl)}`);
|
||||
sections.push(`Requested widget: ${redactText(request?.trim() || "Describe the widget you want to create.")}`);
|
||||
if (embeddedSkill) {
|
||||
sections.push(`Use this embedded Homarr Custom Widget skill while authoring:\n\n${embeddedSkill}`);
|
||||
}
|
||||
if (rawResponse) sections.push(`Sample API response:\n\n\`\`\`json\n${redactResponse(rawResponse)}\n\`\`\``);
|
||||
if (currentConfig) {
|
||||
sections.push(`Current widget:\n\n\`\`\`json\n${JSON.stringify(redactValue(currentConfig), null, 2)}\n\`\`\``);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { customJsxTablerIconNames } from "./tabler-icons";
|
||||
export const CUSTOM_WIDGET_SKILL_VERSION = "2.0.0";
|
||||
export const CUSTOM_WIDGET_SKILLS_SH_URL = "https://www.skills.sh/homarr-labs/homarr/homarr-custom-widget";
|
||||
export const CUSTOM_WIDGET_SKILL_SOURCE_URL =
|
||||
"https://github.com/homarr-labs/homarr/tree/v2/.agents/skills/homarr-custom-widget";
|
||||
"https://github.com/homarr-labs/homarr/tree/dev/.agents/skills/homarr-custom-widget";
|
||||
export const CUSTOM_WIDGET_SKILL_INSTALL_COMMAND =
|
||||
"npx skills add https://github.com/homarr-labs/homarr --skill homarr-custom-widget";
|
||||
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { buildCustomWidgetAiPrompt, buildCustomWidgetFixPrompt, buildCustomWidgetMcpPrompt } from "../core/ai-prompt";
|
||||
import { CUSTOM_WIDGET_SKILL_MD } from "../core/authoring-resources";
|
||||
import { CUSTOM_WIDGET_STARTER } from "../core/examples";
|
||||
|
||||
describe("buildCustomWidgetAiPrompt", () => {
|
||||
test("can embed the portable skill for standalone chat models", () => {
|
||||
const prompt = buildCustomWidgetAiPrompt(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
"Create a status widget",
|
||||
undefined,
|
||||
CUSTOM_WIDGET_SKILL_MD,
|
||||
);
|
||||
|
||||
expect(prompt).toContain("Use this embedded Homarr Custom Widget skill while authoring:");
|
||||
expect(prompt).toContain("# Homarr Custom Widget");
|
||||
expect(prompt).toContain("installed Mantine components can differ by Homarr release.");
|
||||
expect(prompt.length).toBeLessThanOrEqual(8_000);
|
||||
});
|
||||
|
||||
test("is concise and describes the one-widget v2 contract", () => {
|
||||
const prompt = buildCustomWidgetAiPrompt(
|
||||
undefined,
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("custom widget Agent Skill", () => {
|
||||
test("publishes repository-based skills.sh installation metadata", () => {
|
||||
const skill = getCustomWidgetSkill();
|
||||
expect(skill.skillsShUrl).toBe("https://www.skills.sh/homarr-labs/homarr/homarr-custom-widget");
|
||||
expect(skill.sourceUrl).toContain("github.com/homarr-labs/homarr/tree/v2/.agents/skills/homarr-custom-widget");
|
||||
expect(skill.sourceUrl).toBe("https://github.com/homarr-labs/homarr/tree/dev/.agents/skills/homarr-custom-widget");
|
||||
expect(skill.installCommand).toBe(
|
||||
"npx skills add https://github.com/homarr-labs/homarr --skill homarr-custom-widget",
|
||||
);
|
||||
|
||||
@@ -7,7 +7,9 @@ import { json } from "@codemirror/lang-json";
|
||||
import { linter } from "@codemirror/lint";
|
||||
import type { EditorView as EditorViewType } from "@codemirror/view";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
|
||||
import type { ComponentType } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||
import { Badge, Collapse, Group, Input, Loader, Tabs, Text, useComputedColorScheme } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
@@ -27,19 +29,26 @@ export type {
|
||||
CustomWidgetSchemaReferenceData,
|
||||
} from "./code-editor-types";
|
||||
|
||||
const LazyCodeMirror = lazy(() => import("@uiw/react-codemirror").then((module) => ({ default: module.default })));
|
||||
const EMPTY_DIAGNOSTICS: NonNullable<CustomWidgetCodeEditorProps["diagnostics"]> = [];
|
||||
|
||||
export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
const diagnostics = props.diagnostics ?? EMPTY_DIAGNOSTICS;
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [CodeMirror, setCodeMirror] = useState<ComponentType<ReactCodeMirrorProps> | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [referenceOpened, setReferenceOpened] = useState(false);
|
||||
const [cursor, setCursor] = useState({ line: 1, column: 1 });
|
||||
const [editorView, setEditorView] = useState<EditorViewType | null>(null);
|
||||
const [historyDepth, setHistoryDepth] = useState({ undo: 0, redo: 0 });
|
||||
const colorScheme = useComputedColorScheme("light");
|
||||
useEffect(() => setMounted(true), []);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void import("@uiw/react-codemirror").then((module) => {
|
||||
if (!cancelled) setCodeMirror(() => module.default);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
const extensions = useMemo(() => {
|
||||
const diagnosticsExtension = linter((view) =>
|
||||
diagnostics.map((diagnostic) => {
|
||||
@@ -77,6 +86,11 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
});
|
||||
editorView.focus();
|
||||
}, [editorView, props.revealKey, props.revealText]);
|
||||
const handleCreateEditor = useCallback((view: EditorViewType) => {
|
||||
// @uiw invokes this while constructing CodeMirror. Deferring the parent update avoids
|
||||
// re-entering React's renderer during initialisation in Next.js and Docusaurus.
|
||||
window.setTimeout(() => setEditorView(view), 0);
|
||||
}, []);
|
||||
const formattedValue = useMemo(() => formatCode(props.value, props.language), [props.language, props.value]);
|
||||
const errorCount = diagnostics.filter(({ severity }) => severity === "error").length;
|
||||
const warningCount = diagnostics.length - errorCount;
|
||||
@@ -115,37 +129,36 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
|
||||
</Collapse>
|
||||
)}
|
||||
<div className={classes.viewport}>
|
||||
{mounted ? (
|
||||
<Suspense fallback={<EditorLoader />}>
|
||||
<LazyCodeMirror
|
||||
id={`${props.id}-root`}
|
||||
value={props.value}
|
||||
onChange={props.readOnly ? () => undefined : props.onChange}
|
||||
placeholder={props.placeholder}
|
||||
extensions={extensions}
|
||||
theme={colorScheme}
|
||||
height={props.height ?? (props.language === "jsx" ? "320px" : "220px")}
|
||||
basicSetup={{
|
||||
autocompletion: props.language !== "jsx" && !props.readOnly,
|
||||
bracketMatching: true,
|
||||
closeBrackets: !props.readOnly,
|
||||
foldGutter: true,
|
||||
history: !props.readOnly,
|
||||
highlightActiveLine: !props.readOnly,
|
||||
highlightActiveLineGutter: !props.readOnly,
|
||||
indentOnInput: !props.readOnly,
|
||||
lineNumbers: true,
|
||||
}}
|
||||
onCreateEditor={setEditorView}
|
||||
onUpdate={(update) => {
|
||||
if (!update.selectionSet && !update.docChanged) return;
|
||||
const head = update.state.selection.main.head;
|
||||
const line = update.state.doc.lineAt(head);
|
||||
setCursor({ line: line.number, column: head - line.from + 1 });
|
||||
setHistoryDepth({ undo: undoDepth(update.state), redo: redoDepth(update.state) });
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
{CodeMirror ? (
|
||||
<CodeMirror
|
||||
id={`${props.id}-root`}
|
||||
value={props.value}
|
||||
onChange={props.readOnly ? () => undefined : props.onChange}
|
||||
placeholder={props.placeholder}
|
||||
extensions={extensions}
|
||||
theme={colorScheme}
|
||||
height={props.height ?? (props.language === "jsx" ? "320px" : "220px")}
|
||||
basicSetup={{
|
||||
autocompletion: props.language !== "jsx" && !props.readOnly,
|
||||
bracketMatching: true,
|
||||
closeBrackets: !props.readOnly,
|
||||
foldGutter: true,
|
||||
history: !props.readOnly,
|
||||
highlightActiveLine: !props.readOnly,
|
||||
highlightActiveLineGutter: !props.readOnly,
|
||||
indentOnInput: !props.readOnly,
|
||||
lineNumbers: true,
|
||||
}}
|
||||
onCreateEditor={handleCreateEditor}
|
||||
onUpdate={(update) => {
|
||||
if (!editorView) return;
|
||||
if (!update.selectionSet && !update.docChanged) return;
|
||||
const head = update.state.selection.main.head;
|
||||
const line = update.state.doc.lineAt(head);
|
||||
setCursor({ line: line.number, column: head - line.from + 1 });
|
||||
setHistoryDepth({ undo: undoDepth(update.state), redo: redoDepth(update.state) });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EditorLoader />
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user