mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-10 11:23:34 +02:00
Add dynamic() support to prompt.form() plugin API
- prompt.form() inputs can now have dynamic() callbacks that update reactively when form values change (same pattern as auth/template plugins) - Changed PromptFormRequest routing from one-shot to bidirectional events - Added PromptFormResponse.done field to distinguish intermediate updates - Added optional size (enum) to PromptFormRequest for dialog sizing - Added optional rows to FormInputEditor for fixed height editors - New httpsnippet plugin: generates code snippets with dynamic language and library selectors that update the code preview in real-time
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import console from 'node:console';
|
||||
import { type Stats, statSync, watch } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { Context, PluginDefinition } from '@yaakapp/api';
|
||||
import type {
|
||||
CallPromptFormDynamicArgs,
|
||||
Context,
|
||||
DynamicPromptFormArg,
|
||||
PluginDefinition,
|
||||
} from '@yaakapp/api';
|
||||
import {
|
||||
applyFormInputDefaults,
|
||||
validateTemplateFunctionArgs,
|
||||
@@ -12,6 +17,7 @@ import type {
|
||||
DeleteModelResponse,
|
||||
FindHttpResponsesResponse,
|
||||
Folder,
|
||||
FormInput,
|
||||
GetCookieValueRequest,
|
||||
GetCookieValueResponse,
|
||||
GetHttpRequestByIdResponse,
|
||||
@@ -55,6 +61,7 @@ export class PluginInstance {
|
||||
#mod: PluginDefinition;
|
||||
#pluginToAppEvents: EventChannel;
|
||||
#appToPluginEvents: EventChannel;
|
||||
#pendingDynamicForms = new Map<string, DynamicPromptFormArg[]>();
|
||||
|
||||
constructor(workerData: PluginWorkerData, pluginEvents: EventChannel) {
|
||||
this.#workerData = workerData;
|
||||
@@ -106,6 +113,7 @@ export class PluginInstance {
|
||||
|
||||
async terminate() {
|
||||
await this.#mod?.dispose?.();
|
||||
this.#pendingDynamicForms.clear();
|
||||
this.#unimportModule();
|
||||
}
|
||||
|
||||
@@ -664,10 +672,58 @@ export class PluginInstance {
|
||||
return reply.value;
|
||||
},
|
||||
form: async (args) => {
|
||||
const reply: PromptFormResponse = await this.#sendForReply(context, {
|
||||
type: 'prompt_form_request',
|
||||
...args,
|
||||
// Strip dynamic callbacks before serializing (they can't cross the wire)
|
||||
const strippedInputs = stripDynamicCallbacks(args.inputs);
|
||||
|
||||
// Build the event manually so we can get the event ID for keying
|
||||
const eventToSend = this.#buildEventToSend(
|
||||
context,
|
||||
{ type: 'prompt_form_request', ...args, inputs: strippedInputs },
|
||||
null,
|
||||
);
|
||||
|
||||
// Store original inputs (with dynamic callbacks) for later resolution
|
||||
this.#pendingDynamicForms.set(eventToSend.id, args.inputs);
|
||||
|
||||
const reply = await new Promise<PromptFormResponse>((resolve) => {
|
||||
const cb = (event: InternalEvent) => {
|
||||
if (event.replyId !== eventToSend.id) return;
|
||||
|
||||
if (event.payload.type === 'prompt_form_response') {
|
||||
const { done, values } = event.payload as PromptFormResponse;
|
||||
if (done) {
|
||||
// Final response — resolve the promise and clean up
|
||||
this.#appToPluginEvents.unlisten(cb);
|
||||
this.#pendingDynamicForms.delete(eventToSend.id);
|
||||
resolve({ values } as PromptFormResponse);
|
||||
} else {
|
||||
// Intermediate value change — resolve dynamic inputs and send back
|
||||
const storedInputs = this.#pendingDynamicForms.get(eventToSend.id);
|
||||
if (storedInputs && values) {
|
||||
const ctx = this.#newCtx(context);
|
||||
const callArgs: CallPromptFormDynamicArgs = { values };
|
||||
applyDynamicFormInput(ctx, storedInputs, callArgs)
|
||||
.then((resolvedInputs) => {
|
||||
const stripped = stripDynamicCallbacks(resolvedInputs);
|
||||
this.#sendPayload(
|
||||
context,
|
||||
{ type: 'prompt_form_request', ...args, inputs: stripped },
|
||||
eventToSend.id,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to resolve dynamic form inputs', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
this.#appToPluginEvents.listen(cb);
|
||||
|
||||
// Send the initial event after we start listening (to prevent race)
|
||||
this.#sendEvent(eventToSend);
|
||||
});
|
||||
|
||||
return reply.values;
|
||||
},
|
||||
},
|
||||
@@ -906,6 +962,17 @@ export class PluginInstance {
|
||||
}
|
||||
}
|
||||
|
||||
function stripDynamicCallbacks(inputs: DynamicPromptFormArg[]): FormInput[] {
|
||||
return inputs.map((input) => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: stripping dynamic from union type
|
||||
const { dynamic, ...rest } = input as any;
|
||||
if ('inputs' in rest && Array.isArray(rest.inputs)) {
|
||||
rest.inputs = stripDynamicCallbacks(rest.inputs);
|
||||
}
|
||||
return rest as FormInput;
|
||||
});
|
||||
}
|
||||
|
||||
function genId(len = 5): string {
|
||||
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let id = '';
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import type { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api';
|
||||
import type {
|
||||
CallPromptFormDynamicArgs,
|
||||
Context,
|
||||
DynamicAuthenticationArg,
|
||||
DynamicPromptFormArg,
|
||||
DynamicTemplateFunctionArg,
|
||||
} from '@yaakapp/api';
|
||||
import type {
|
||||
CallHttpAuthenticationActionArgs,
|
||||
CallTemplateFunctionArgs,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
|
||||
type AnyDynamicArg = DynamicTemplateFunctionArg | DynamicAuthenticationArg | DynamicPromptFormArg;
|
||||
type AnyCallArgs =
|
||||
| CallTemplateFunctionArgs
|
||||
| CallHttpAuthenticationActionArgs
|
||||
| CallPromptFormDynamicArgs;
|
||||
|
||||
export async function applyDynamicFormInput(
|
||||
ctx: Context,
|
||||
args: DynamicTemplateFunctionArg[],
|
||||
@@ -18,30 +30,40 @@ export async function applyDynamicFormInput(
|
||||
|
||||
export async function applyDynamicFormInput(
|
||||
ctx: Context,
|
||||
args: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[],
|
||||
callArgs: CallTemplateFunctionArgs | CallHttpAuthenticationActionArgs,
|
||||
): Promise<(DynamicTemplateFunctionArg | DynamicAuthenticationArg)[]> {
|
||||
const resolvedArgs: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[] = [];
|
||||
args: DynamicPromptFormArg[],
|
||||
callArgs: CallPromptFormDynamicArgs,
|
||||
): Promise<DynamicPromptFormArg[]>;
|
||||
|
||||
export async function applyDynamicFormInput(
|
||||
ctx: Context,
|
||||
args: AnyDynamicArg[],
|
||||
callArgs: AnyCallArgs,
|
||||
): Promise<AnyDynamicArg[]> {
|
||||
const resolvedArgs: AnyDynamicArg[] = [];
|
||||
for (const { dynamic, ...arg } of args) {
|
||||
const dynamicResult =
|
||||
typeof dynamic === 'function'
|
||||
? await dynamic(
|
||||
ctx,
|
||||
callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs,
|
||||
callArgs as CallTemplateFunctionArgs &
|
||||
CallHttpAuthenticationActionArgs &
|
||||
CallPromptFormDynamicArgs,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const newArg = {
|
||||
...arg,
|
||||
...dynamicResult,
|
||||
} as DynamicTemplateFunctionArg | DynamicAuthenticationArg;
|
||||
} as AnyDynamicArg;
|
||||
|
||||
if ('inputs' in newArg && Array.isArray(newArg.inputs)) {
|
||||
try {
|
||||
newArg.inputs = await applyDynamicFormInput(
|
||||
ctx,
|
||||
newArg.inputs as DynamicTemplateFunctionArg[],
|
||||
callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs,
|
||||
callArgs as CallTemplateFunctionArgs &
|
||||
CallHttpAuthenticationActionArgs &
|
||||
CallPromptFormDynamicArgs,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to apply dynamic form input', e);
|
||||
|
||||
Reference in New Issue
Block a user