feat(assistant): optimize MCP and custom widget authoring (#6743)

* feat(assistant): optimize MCP and custom widget authoring

* fix(assistant): isolate server-safe JSX analyzer

* refactor(assistant): generate MCP tool staging

* fix(assistant): preserve widget placement follow-up

* fix(ci): allow fast gate to finish

* fix(custom-widgets): address review findings

* fix(custom-widgets): split preview evidence module

* style: format assistant custom widget files

* fix(custom-widgets): sync formatted skill schema
This commit is contained in:
Thomas authored and GitHub committed 2026-08-30 23:43:29 +02:00
1 parent 38f4b0e1f8
commit 5e23659d99
95 files changed
+6799 -1778

No files matched your search

@@ -7,21 +7,11 @@ import { renderSafeJsx } from "../jsx/interpreter";
import { CustomJsxInputsProvider } from "../jsx/runtime-components";
import type { WidgetInputType, WidgetInputValue } from "../jsx/runtime-components";
import { sameInputRecord, sameStringArray, sameTypeRecord } from "./input-state-utils";
import type { CustomJsxRequestCapability } from "./types";
export { CUSTOM_JSX_METHOD_COLORS, parseRequestCapabilities } from "./request-capabilities";
const EMPTY_RECORD: Record<string, never> = {};
const methodColors: Readonly<Record<string, string>> = {
GET: "blue",
POST: "orange",
PUT: "yellow",
DELETE: "red",
PATCH: "grape",
};
const methods = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
const kinds = new Set(["query", "action"]);
const permissions = new Set(["view", "modify", "full"]);
export interface CustomJsxRendererMessages {
noTemplate: string;
templateWarnings(count: number): string;
@@ -38,42 +28,6 @@ export interface CustomJsxRendererProps {
messages: CustomJsxRendererMessages;
}
export const CUSTOM_JSX_METHOD_COLORS = methodColors;
export function parseRequestCapabilities(value: unknown): CustomJsxRequestCapability[] {
if (!Array.isArray(value)) return [];
return value.flatMap((candidate) => {
if (!candidate || typeof candidate !== "object") return [];
const record = candidate as Record<string, unknown>;
if (
typeof record.id !== "string" ||
typeof record.kind !== "string" ||
!kinds.has(record.kind) ||
typeof record.method !== "string" ||
!methods.has(record.method) ||
typeof record.minimumBoardPermission !== "string" ||
!permissions.has(record.minimumBoardPermission)
)
return [];
return [
{
id: record.id,
kind: record.kind,
method: record.method,
trigger: record.trigger === "load" ? "load" : "manual",
minimumBoardPermission: record.minimumBoardPermission,
confirmation:
record.confirmation && typeof record.confirmation === "object"
? (record.confirmation as CustomJsxRequestCapability["confirmation"])
: undefined,
invalidates: Array.isArray(record.invalidates)
? record.invalidates.filter((entry): entry is string => typeof entry === "string")
: [],
} as CustomJsxRequestCapability,
];
});
}
class RendererErrorBoundary extends Component<
{ children: ReactNode; resetKey: string; onError(error: Error): void },
{ error: Error | null; resetKey: string }
@@ -216,6 +170,21 @@ function CustomJsxRendererSession({
};
});
}, []);
const resetInput = useCallback((name: string, type: WidgetInputType) => {
let activeRegistration: InputRegistration | undefined;
for (const registration of registrations.current.values()) {
if (registration.name !== name) continue;
activeRegistration = registration;
break;
}
if (!activeRegistration || activeRegistration.type !== type) return;
setInputState((current) => {
if (current.types[name] !== type || Object.is(current.values[name], activeRegistration.initialValue)) {
return current;
}
return { ...current, values: { ...current.values, [name]: activeRegistration.initialValue } };
});
}, []);
const rendered = useMemo(() => {
try {
const bindings = { ...createBindings(data), status, options, inputs };
@@ -259,6 +228,7 @@ function CustomJsxRendererSession({
inputTypes={inputTypes}
registerInput={registerInput}
setInputValue={setInputValue}
resetInput={resetInput}
>
<RendererErrorBoundary resetKey={rendered.boundaryKey} onError={handleError}>
{rendered.node}
@@ -7,23 +7,32 @@ import { IconRefresh } from "@tabler/icons-react";
import { useCustomWidgetRuntime } from "./context";
export interface RefreshButtonProps {
requestId?: string;
label?: string;
color?: string;
variant?: string;
size?: string;
}
export function RefreshButton({ label, color = "gray", variant = "subtle", size = "sm" }: RefreshButtonProps) {
export function RefreshButton({
requestId,
label,
color = "gray",
variant = "subtle",
size = "sm",
}: RefreshButtonProps) {
const runtime = useCustomWidgetRuntime();
const [loading, setLoading] = useState(false);
const refresh = async () => {
if (!runtime.itemId && !runtime.previewSessionId) return;
setLoading(true);
try {
let targets = ["parent", "*"];
if (requestId) targets = [requestId];
await runtime.port.invalidate({
itemId: runtime.itemId,
previewSessionId: runtime.previewSessionId,
targets: ["parent", "*"],
targets,
});
} finally {
setLoading(false);
@@ -0,0 +1,60 @@
import type { CustomJsxRequestCapability } from "./types";
export const CUSTOM_JSX_METHOD_COLORS: Readonly<Record<string, string>> = {
GET: "blue",
POST: "orange",
PUT: "yellow",
DELETE: "red",
PATCH: "grape",
};
const methods = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
const kinds = new Set(["query", "action"]);
const permissions = new Set(["view", "modify", "full"]);
const parseConfirmation = (value: unknown): CustomJsxRequestCapability["confirmation"] => {
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
const record = value as Record<string, unknown>;
if (typeof record.title !== "string" || typeof record.message !== "string") return undefined;
if (record.confirmLabel !== undefined && typeof record.confirmLabel !== "string") return undefined;
if (record.destructive !== undefined && typeof record.destructive !== "boolean") return undefined;
const confirmation: NonNullable<CustomJsxRequestCapability["confirmation"]> = {
title: record.title,
message: record.message,
};
if (typeof record.confirmLabel === "string") confirmation.confirmLabel = record.confirmLabel;
if (typeof record.destructive === "boolean") confirmation.destructive = record.destructive;
return confirmation;
};
export function parseRequestCapabilities(value: unknown): CustomJsxRequestCapability[] {
if (!Array.isArray(value)) return [];
return value.flatMap((candidate) => {
if (!candidate || typeof candidate !== "object") return [];
const record = candidate as Record<string, unknown>;
if (
typeof record.id !== "string" ||
typeof record.kind !== "string" ||
!kinds.has(record.kind) ||
typeof record.method !== "string" ||
!methods.has(record.method) ||
typeof record.minimumBoardPermission !== "string" ||
!permissions.has(record.minimumBoardPermission)
)
return [];
return [
{
id: record.id,
kind: record.kind,
method: record.method,
trigger: record.trigger === "load" ? "load" : "manual",
minimumBoardPermission: record.minimumBoardPermission,
confirmation: parseConfirmation(record.confirmation),
invalidates: Array.isArray(record.invalidates)
? record.invalidates.filter((entry): entry is string => typeof entry === "string")
: [],
} as CustomJsxRequestCapability,
];
});
}
@@ -42,16 +42,23 @@ export const MAX_REFRESH_INTERVAL_MS = MAX_REFRESH_INTERVAL_SECONDS * 1_000;
export function SubFetch(props: SubFetchProps) {
const { itemId, previewSessionId, queryCacheKey, queriesDisabled, port, messages, setQueryState } =
useCustomWidgetRuntime();
const [manualRun, setManualRun] = useState(false);
const [manualRunKey, setManualRunKey] = useState<string | null>(null);
const params = useMemo(() => normalizeParams(props.params), [props.params]);
const paramsKey = useMemo(() => JSON.stringify(params), [params]);
useEffect(() => setManualRun(false), [paramsKey, props.requestId, props.trigger]);
const manualRequestKey = JSON.stringify([
itemId,
previewSessionId,
queryCacheKey,
props.requestId,
props.trigger,
paramsKey,
]);
const enabled = Boolean(
!queriesDisabled &&
(itemId || previewSessionId) &&
props.requestId &&
params &&
(props.trigger !== "manual" || manualRun),
(props.trigger !== "manual" || manualRunKey === manualRequestKey),
);
const refreshMs = normalizeRefreshInterval(props.refreshInterval);
const scope = itemId ? "item" : "preview";
@@ -101,14 +108,14 @@ export function SubFetch(props: SubFetchProps) {
? props.triggerContent
: (props.fallback ?? <Text c="dimmed">{messages.unsavedPreview}</Text>);
if (!params) return <RequestAlert message={messages.invalidParams} />;
if (props.trigger === "manual" && !manualRun) {
if (props.trigger === "manual" && manualRunKey !== manualRequestKey) {
if (props.triggerContent) {
return (
<Box
component="button"
type="button"
aria-label={props.triggerAriaLabel ?? messages.loadRequest}
onClick={() => setManualRun(true)}
onClick={() => setManualRunKey(manualRequestKey)}
style={{
appearance: "none",
background: "transparent",
@@ -126,7 +133,7 @@ export function SubFetch(props: SubFetchProps) {
);
}
return (
<Button size="compact-sm" variant="light" onClick={() => setManualRun(true)}>
<Button size="compact-sm" variant="light" onClick={() => setManualRunKey(manualRequestKey)}>
{messages.loadRequest}
</Button>
);