Add dynamic() support to prompt.form() plugin API (#386)

This commit is contained in:
Gregory Schier
2026-02-07 08:09:40 -08:00
committed by GitHub
parent 2984eb40c9
commit f98a70ecb4
22 changed files with 925 additions and 55 deletions

View File

@@ -360,8 +360,9 @@ function EditorArg({
className={classNames(
'border border-border rounded-md overflow-hidden px-2 py-1',
'focus-within:border-border-focus',
'max-h-[10rem]', // So it doesn't take up too much space
!arg.rows && 'max-h-[10rem]', // So it doesn't take up too much space
)}
style={arg.rows ? { height: `${arg.rows * 1.4 + 0.75}rem` } : undefined}
>
<Editor
id={id}

View File

@@ -5,6 +5,7 @@ import { useMemo } from 'react';
import { Overlay } from '../Overlay';
import { Heading } from './Heading';
import { IconButton } from './IconButton';
import { DialogSize } from '@yaakapp-internal/plugins';
export interface DialogProps {
children: ReactNode;
@@ -14,7 +15,7 @@ export interface DialogProps {
title?: ReactNode;
description?: ReactNode;
className?: string;
size?: 'sm' | 'md' | 'lg' | 'full' | 'dynamic';
size?: DialogSize;
hideX?: boolean;
noPadding?: boolean;
noScroll?: boolean;

View File

@@ -5,11 +5,14 @@ import {
completionKeymap,
} from '@codemirror/autocomplete';
import { history, historyKeymap } from '@codemirror/commands';
import { go } from '@codemirror/lang-go';
import { java } from '@codemirror/lang-java';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import { php } from '@codemirror/lang-php';
import { python } from '@codemirror/lang-python';
import { xml } from '@codemirror/lang-xml';
import type { LanguageSupport } from '@codemirror/language';
import {
bracketMatching,
codeFolding,
@@ -17,8 +20,19 @@ import {
foldKeymap,
HighlightStyle,
indentOnInput,
LanguageSupport,
StreamLanguage,
syntaxHighlighting,
} from '@codemirror/language';
import { c, csharp, kotlin, objectiveC } from '@codemirror/legacy-modes/mode/clike';
import { clojure } from '@codemirror/legacy-modes/mode/clojure';
import { http } from '@codemirror/legacy-modes/mode/http';
import { oCaml } from '@codemirror/legacy-modes/mode/mllike';
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
import { r } from '@codemirror/legacy-modes/mode/r';
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { swift } from '@codemirror/legacy-modes/mode/swift';
import { linter, lintGutter, lintKeymap } from '@codemirror/lint';
import { search, searchKeymap } from '@codemirror/search';
@@ -83,6 +97,10 @@ const syntaxTheme = EditorView.theme({}, { dark: true });
const closeBracketsExtensions: Extension = [closeBrackets(), keymap.of([...closeBracketsKeymap])];
const legacyLang = (mode: Parameters<typeof StreamLanguage.define>[0]) => {
return () => new LanguageSupport(StreamLanguage.define(mode));
};
const syntaxExtensions: Record<
NonNullable<EditorProps['language']>,
null | (() => LanguageSupport)
@@ -98,6 +116,22 @@ const syntaxExtensions: Record<
text: text,
timeline: timeline,
markdown: markdown,
c: legacyLang(c),
clojure: legacyLang(clojure),
csharp: legacyLang(csharp),
go: go,
http: legacyLang(http),
java: java,
kotlin: legacyLang(kotlin),
objective_c: legacyLang(objectiveC),
ocaml: legacyLang(oCaml),
php: php,
powershell: legacyLang(powerShell),
python: python,
r: legacyLang(r),
ruby: legacyLang(ruby),
shell: legacyLang(shell),
swift: legacyLang(swift),
};
const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ['json', 'javascript', 'graphql'];

View File

@@ -1,6 +1,6 @@
import type { FormInput, JsonPrimitive } from '@yaakapp-internal/plugins';
import type { FormEvent } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { generateId } from '../../lib/generateId';
import { DynamicForm } from '../DynamicForm';
import { Button } from './Button';
@@ -12,16 +12,21 @@ export interface PromptProps {
onResult: (value: Record<string, JsonPrimitive> | null) => void;
confirmText?: string;
cancelText?: string;
onValuesChange?: (values: Record<string, JsonPrimitive>) => void;
onInputsUpdated?: (cb: (inputs: FormInput[]) => void) => void;
}
export function Prompt({
onCancel,
inputs,
inputs: initialInputs,
onResult,
confirmText = 'Confirm',
cancelText = 'Cancel',
onValuesChange,
onInputsUpdated,
}: PromptProps) {
const [value, setValue] = useState<Record<string, JsonPrimitive>>({});
const [inputs, setInputs] = useState<FormInput[]>(initialInputs);
const handleSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
@@ -30,6 +35,16 @@ export function Prompt({
[onResult, value],
);
// Register callback for external input updates (from plugin dynamic resolution)
useEffect(() => {
onInputsUpdated?.(setInputs);
}, [onInputsUpdated]);
// Notify of value changes for dynamic resolution
useEffect(() => {
onValuesChange?.(value);
}, [value, onValuesChange]);
const id = `prompt.form.${useRef(generateId()).current}`;
return (

View File

@@ -1,6 +1,12 @@
import { emit } from '@tauri-apps/api/event';
import { openUrl } from '@tauri-apps/plugin-opener';
import type { InternalEvent, ShowToastRequest } from '@yaakapp-internal/plugins';
import { debounce } from '@yaakapp-internal/lib';
import type {
FormInput,
InternalEvent,
JsonPrimitive,
ShowToastRequest,
} from '@yaakapp-internal/plugins';
import { updateAllPlugins } from '@yaakapp-internal/plugins';
import type {
PluginUpdateNotification,
@@ -32,6 +38,9 @@ export function initGlobalListeners() {
listenToTauriEvent('settings', () => openSettings.mutate(null));
// Track active dynamic form dialogs so follow-up input updates can reach them
const activeForms = new Map<string, (inputs: FormInput[]) => void>();
// Listen for plugin events
listenToTauriEvent<InternalEvent>('plugin_event', async ({ payload: event }) => {
if (event.payload.type === 'prompt_text_request') {
@@ -49,26 +58,47 @@ export function initGlobalListeners() {
};
await emit(event.id, result);
} else if (event.payload.type === 'prompt_form_request') {
if (event.replyId != null) {
// Follow-up update from plugin runtime — update the active dialog's inputs
const updateInputs = activeForms.get(event.replyId);
if (updateInputs) {
updateInputs(event.payload.inputs);
}
return;
}
// Initial request — show the dialog with bidirectional support
const emitFormResponse = (values: Record<string, JsonPrimitive> | null, done: boolean) => {
const result: InternalEvent = {
id: generateId(),
replyId: event.id,
pluginName: event.pluginName,
pluginRefId: event.pluginRefId,
context: event.context,
payload: {
type: 'prompt_form_response',
values,
done,
},
};
emit(event.id, result);
};
const values = await showPromptForm({
id: event.payload.id,
title: event.payload.title,
description: event.payload.description,
size: event.payload.size,
inputs: event.payload.inputs,
confirmText: event.payload.confirmText,
cancelText: event.payload.cancelText,
onValuesChange: debounce((values) => emitFormResponse(values, false), 150),
onInputsUpdated: (cb) => activeForms.set(event.id, cb),
});
const result: InternalEvent = {
id: generateId(),
replyId: event.id,
pluginName: event.pluginName,
pluginRefId: event.pluginRefId,
context: event.context,
payload: {
type: 'prompt_form_response',
values,
},
};
await emit(event.id, result);
// Clean up and send final response
activeForms.delete(event.id);
emitFormResponse(values, true);
}
});

View File

@@ -1,21 +1,32 @@
import type { FormInput, JsonPrimitive } from '@yaakapp-internal/plugins';
import type { DialogProps } from '../components/core/Dialog';
import type { PromptProps } from '../components/core/Prompt';
import { Prompt } from '../components/core/Prompt';
import { showDialog } from './dialog';
type FormArgs = Pick<DialogProps, 'title' | 'description'> &
type FormArgs = Pick<DialogProps, 'title' | 'description' | 'size'> &
Omit<PromptProps, 'onClose' | 'onCancel' | 'onResult'> & {
id: string;
onValuesChange?: (values: Record<string, JsonPrimitive>) => void;
onInputsUpdated?: (cb: (inputs: FormInput[]) => void) => void;
};
export async function showPromptForm({ id, title, description, ...props }: FormArgs) {
export async function showPromptForm({
id,
title,
description,
size,
onValuesChange,
onInputsUpdated,
...props
}: FormArgs) {
return new Promise((resolve: PromptProps['onResult']) => {
showDialog({
id,
title,
description,
hideX: true,
size: 'sm',
size: size ?? 'sm',
disableBackdropClose: true, // Prevent accidental dismisses
onClose: () => {
// Click backdrop, close, or escape
@@ -32,6 +43,8 @@ export async function showPromptForm({ id, title, description, ...props }: FormA
resolve(v);
hide();
},
onValuesChange,
onInputsUpdated,
...props,
}),
});