mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-25 02:08:28 +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:
@@ -12,7 +12,7 @@ use chrono::Utc;
|
|||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Emitter, Manager, Runtime};
|
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime};
|
||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
@@ -59,7 +59,55 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
}
|
}
|
||||||
InternalEventPayload::PromptFormRequest(_) => {
|
InternalEventPayload::PromptFormRequest(_) => {
|
||||||
let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
|
let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
|
||||||
Ok(call_frontend(&window, event).await)
|
if event.reply_id.is_some() {
|
||||||
|
// Follow-up update from plugin runtime with resolved inputs — forward to frontend
|
||||||
|
window.emit_to(window.label(), "plugin_event", event.clone())?;
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
// Initial request — set up bidirectional communication
|
||||||
|
window.emit_to(window.label(), "plugin_event", event.clone()).unwrap();
|
||||||
|
|
||||||
|
let event_id = event.id.clone();
|
||||||
|
let plugin_handle = plugin_handle.clone();
|
||||||
|
let plugin_context = plugin_context.clone();
|
||||||
|
let window = window.clone();
|
||||||
|
|
||||||
|
// Spawn async task to handle bidirectional form communication
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::channel::<InternalEvent>(128);
|
||||||
|
|
||||||
|
// Listen for replies from the frontend
|
||||||
|
let listener_id = window.listen(event_id, move |ev: tauri::Event| {
|
||||||
|
let resp: InternalEvent = serde_json::from_str(ev.payload()).unwrap();
|
||||||
|
let _ = tx.try_send(resp);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Forward each reply to the plugin runtime
|
||||||
|
while let Some(resp) = rx.recv().await {
|
||||||
|
let is_done = matches!(
|
||||||
|
&resp.payload,
|
||||||
|
InternalEventPayload::PromptFormResponse(r) if r.done.unwrap_or(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
let event_to_send = plugin_handle.build_event_to_send(
|
||||||
|
&plugin_context,
|
||||||
|
&resp.payload,
|
||||||
|
Some(resp.reply_id.unwrap_or_default()),
|
||||||
|
);
|
||||||
|
if let Err(e) = plugin_handle.send(&event_to_send).await {
|
||||||
|
log::warn!("Failed to forward form response to plugin: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_done {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.unlisten(listener_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
||||||
let http_responses = app_handle
|
let http_responses = app_handle
|
||||||
|
|||||||
12
crates/yaak-plugins/bindings/gen_events.ts
generated
12
crates/yaak-plugins/bindings/gen_events.ts
generated
@@ -172,7 +172,11 @@ hideGutter?: boolean,
|
|||||||
/**
|
/**
|
||||||
* Language for syntax highlighting
|
* Language for syntax highlighting
|
||||||
*/
|
*/
|
||||||
language?: EditorLanguage, readOnly?: boolean, completionOptions?: Array<GenericCompletionOption>,
|
language?: EditorLanguage, readOnly?: boolean,
|
||||||
|
/**
|
||||||
|
* Fixed number of visible rows
|
||||||
|
*/
|
||||||
|
rows?: number, completionOptions?: Array<GenericCompletionOption>,
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
*/
|
*/
|
||||||
@@ -476,9 +480,11 @@ label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
|
|||||||
|
|
||||||
export type PluginContext = { id: string, label: string | null, workspaceId: string | null, };
|
export type PluginContext = { id: string, label: string | null, workspaceId: string | null, };
|
||||||
|
|
||||||
export type PromptFormRequest = { id: string, title: string, description?: string, inputs: Array<FormInput>, confirmText?: string, cancelText?: string, };
|
export type PromptFormRequest = { id: string, title: string, description?: string, inputs: Array<FormInput>, confirmText?: string, cancelText?: string, size?: PromptFormSize, };
|
||||||
|
|
||||||
export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, };
|
export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, done?: boolean, };
|
||||||
|
|
||||||
|
export type PromptFormSize = "sm" | "md" | "lg" | "full" | "dynamic";
|
||||||
|
|
||||||
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
|
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
|
||||||
/**
|
/**
|
||||||
|
|||||||
2
crates/yaak-plugins/bindings/gen_models.ts
generated
2
crates/yaak-plugins/bindings/gen_models.ts
generated
@@ -49,7 +49,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -587,6 +587,19 @@ pub struct PromptFormRequest {
|
|||||||
pub confirm_text: Option<String>,
|
pub confirm_text: Option<String>,
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub cancel_text: Option<String>,
|
pub cancel_text: Option<String>,
|
||||||
|
#[ts(optional)]
|
||||||
|
pub size: Option<PromptFormSize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
|
pub enum PromptFormSize {
|
||||||
|
Sm,
|
||||||
|
Md,
|
||||||
|
Lg,
|
||||||
|
Full,
|
||||||
|
Dynamic,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
@@ -594,6 +607,8 @@ pub struct PromptFormRequest {
|
|||||||
#[ts(export, export_to = "gen_events.ts")]
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
pub struct PromptFormResponse {
|
pub struct PromptFormResponse {
|
||||||
pub values: Option<HashMap<String, JsonPrimitive>>,
|
pub values: Option<HashMap<String, JsonPrimitive>>,
|
||||||
|
#[ts(optional)]
|
||||||
|
pub done: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
@@ -966,6 +981,10 @@ pub struct FormInputEditor {
|
|||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub read_only: Option<bool>,
|
pub read_only: Option<bool>,
|
||||||
|
|
||||||
|
/// Fixed number of visible rows
|
||||||
|
#[ts(optional)]
|
||||||
|
pub rows: Option<i32>,
|
||||||
|
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub completion_options: Option<Vec<GenericCompletionOption>>,
|
pub completion_options: Option<Vec<GenericCompletionOption>>,
|
||||||
}
|
}
|
||||||
|
|||||||
84
package-lock.json
generated
84
package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"packages/plugin-runtime-types",
|
"packages/plugin-runtime-types",
|
||||||
"plugins-external/mcp-server",
|
"plugins-external/mcp-server",
|
||||||
"plugins-external/template-function-faker",
|
"plugins-external/template-function-faker",
|
||||||
|
"plugins-external/httpsnippet",
|
||||||
"plugins/action-copy-curl",
|
"plugins/action-copy-curl",
|
||||||
"plugins/action-copy-grpcurl",
|
"plugins/action-copy-grpcurl",
|
||||||
"plugins/action-send-folder",
|
"plugins/action-send-folder",
|
||||||
@@ -2022,6 +2023,19 @@
|
|||||||
"node": ">=16.9"
|
"node": ">=16.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@readme/httpsnippet": {
|
||||||
|
"version": "11.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@readme/httpsnippet/-/httpsnippet-11.0.0.tgz",
|
||||||
|
"integrity": "sha512-XSyaAsJkZfmMO9R4WDlVJARZgd4wlImftSkMkKclidniXA1h6DTya9iTqJenQo9mHQLh3u6kAC3CDRaIV+LbLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"qs": "^6.11.2",
|
||||||
|
"stringify-object": "^3.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@replit/codemirror-emacs": {
|
"node_modules/@replit/codemirror-emacs": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz",
|
||||||
@@ -4045,6 +4059,10 @@
|
|||||||
"resolved": "plugins/filter-xpath",
|
"resolved": "plugins/filter-xpath",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@yaak/httpsnippet": {
|
||||||
|
"resolved": "plugins-external/httpsnippet",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@yaak/importer-curl": {
|
"node_modules/@yaak/importer-curl": {
|
||||||
"resolved": "plugins/importer-curl",
|
"resolved": "plugins/importer-curl",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -7411,6 +7429,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-own-enumerable-property-symbols": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/get-proto": {
|
"node_modules/get-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
@@ -8529,6 +8553,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-obj": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-plain-obj": {
|
"node_modules/is-plain-obj": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||||
@@ -13789,6 +13822,29 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stringify-object": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"get-own-enumerable-property-symbols": "^3.0.0",
|
||||||
|
"is-obj": "^1.0.1",
|
||||||
|
"is-regexp": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stringify-object/node_modules/is-regexp": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
@@ -15788,6 +15844,34 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"plugins-external/httpsnippet": {
|
||||||
|
"name": "@yaak/httpsnippet",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@readme/httpsnippet": "^11.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins-external/httpsnippet/node_modules/@types/node": {
|
||||||
|
"version": "22.19.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz",
|
||||||
|
"integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins-external/httpsnippet/node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"plugins-external/mcp-server": {
|
"plugins-external/mcp-server": {
|
||||||
"name": "@yaak/mcp-server",
|
"name": "@yaak/mcp-server",
|
||||||
"version": "0.1.7",
|
"version": "0.1.7",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"packages/plugin-runtime-types",
|
"packages/plugin-runtime-types",
|
||||||
"plugins-external/mcp-server",
|
"plugins-external/mcp-server",
|
||||||
"plugins-external/template-function-faker",
|
"plugins-external/template-function-faker",
|
||||||
|
"plugins-external/httpsnippet",
|
||||||
"plugins/action-copy-curl",
|
"plugins/action-copy-curl",
|
||||||
"plugins/action-copy-grpcurl",
|
"plugins/action-copy-grpcurl",
|
||||||
"plugins/action-send-folder",
|
"plugins/action-send-folder",
|
||||||
|
|||||||
@@ -172,7 +172,11 @@ hideGutter?: boolean,
|
|||||||
/**
|
/**
|
||||||
* Language for syntax highlighting
|
* Language for syntax highlighting
|
||||||
*/
|
*/
|
||||||
language?: EditorLanguage, readOnly?: boolean, completionOptions?: Array<GenericCompletionOption>,
|
language?: EditorLanguage, readOnly?: boolean,
|
||||||
|
/**
|
||||||
|
* Fixed number of visible rows
|
||||||
|
*/
|
||||||
|
rows?: number, completionOptions?: Array<GenericCompletionOption>,
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
*/
|
*/
|
||||||
@@ -476,9 +480,11 @@ label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
|
|||||||
|
|
||||||
export type PluginContext = { id: string, label: string | null, workspaceId: string | null, };
|
export type PluginContext = { id: string, label: string | null, workspaceId: string | null, };
|
||||||
|
|
||||||
export type PromptFormRequest = { id: string, title: string, description?: string, inputs: Array<FormInput>, confirmText?: string, cancelText?: string, };
|
export type PromptFormRequest = { id: string, title: string, description?: string, inputs: Array<FormInput>, confirmText?: string, cancelText?: string, size?: PromptFormSize, };
|
||||||
|
|
||||||
export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, };
|
export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, done?: boolean, };
|
||||||
|
|
||||||
|
export type PromptFormSize = "sm" | "md" | "lg" | "full" | "dynamic";
|
||||||
|
|
||||||
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
|
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ import type {
|
|||||||
} from '../bindings/gen_events.ts';
|
} from '../bindings/gen_events.ts';
|
||||||
import type { Folder, HttpRequest } from '../bindings/gen_models.ts';
|
import type { Folder, HttpRequest } from '../bindings/gen_models.ts';
|
||||||
import type { JsonValue } from '../bindings/serde_json/JsonValue';
|
import type { JsonValue } from '../bindings/serde_json/JsonValue';
|
||||||
|
import type { DynamicPromptFormArg } from './PromptFormPlugin';
|
||||||
|
|
||||||
|
type DynamicPromptFormRequest = Omit<PromptFormRequest, 'inputs'> & {
|
||||||
|
inputs: DynamicPromptFormArg[];
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkspaceHandle = Pick<WorkspaceInfo, 'id' | 'name'>;
|
export type WorkspaceHandle = Pick<WorkspaceInfo, 'id' | 'name'>;
|
||||||
|
|
||||||
@@ -39,7 +44,7 @@ export interface Context {
|
|||||||
};
|
};
|
||||||
prompt: {
|
prompt: {
|
||||||
text(args: PromptTextRequest): Promise<PromptTextResponse['value']>;
|
text(args: PromptTextRequest): Promise<PromptTextResponse['value']>;
|
||||||
form(args: PromptFormRequest): Promise<PromptFormResponse['values']>;
|
form(args: DynamicPromptFormRequest): Promise<PromptFormResponse['values']>;
|
||||||
};
|
};
|
||||||
store: {
|
store: {
|
||||||
set<T>(key: string, value: T): Promise<void>;
|
set<T>(key: string, value: T): Promise<void>;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { FormInput, JsonPrimitive } from '../bindings/gen_events';
|
||||||
|
import type { MaybePromise } from '../helpers';
|
||||||
|
import type { Context } from './Context';
|
||||||
|
|
||||||
|
export type CallPromptFormDynamicArgs = {
|
||||||
|
values: { [key in string]?: JsonPrimitive };
|
||||||
|
};
|
||||||
|
|
||||||
|
type AddDynamicMethod<T> = {
|
||||||
|
dynamic?: (
|
||||||
|
ctx: Context,
|
||||||
|
args: CallPromptFormDynamicArgs,
|
||||||
|
) => MaybePromise<Partial<T> | null | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern
|
||||||
|
type AddDynamic<T> = T extends any
|
||||||
|
? T extends { inputs?: FormInput[] }
|
||||||
|
? Omit<T, 'inputs'> & {
|
||||||
|
inputs: Array<AddDynamic<FormInput>>;
|
||||||
|
dynamic?: (
|
||||||
|
ctx: Context,
|
||||||
|
args: CallPromptFormDynamicArgs,
|
||||||
|
) => MaybePromise<
|
||||||
|
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
: T & AddDynamicMethod<T>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type DynamicPromptFormArg = AddDynamic<FormInput>;
|
||||||
@@ -2,21 +2,22 @@ import type { AuthenticationPlugin } from './AuthenticationPlugin';
|
|||||||
|
|
||||||
import type { Context } from './Context';
|
import type { Context } from './Context';
|
||||||
import type { FilterPlugin } from './FilterPlugin';
|
import type { FilterPlugin } from './FilterPlugin';
|
||||||
|
import type { FolderActionPlugin } from './FolderActionPlugin';
|
||||||
import type { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
|
import type { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
|
||||||
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
|
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
|
||||||
import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin';
|
|
||||||
import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
|
|
||||||
import type { FolderActionPlugin } from './FolderActionPlugin';
|
|
||||||
import type { ImporterPlugin } from './ImporterPlugin';
|
import type { ImporterPlugin } from './ImporterPlugin';
|
||||||
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
|
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
|
||||||
import type { ThemePlugin } from './ThemePlugin';
|
import type { ThemePlugin } from './ThemePlugin';
|
||||||
|
import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin';
|
||||||
|
import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
|
||||||
|
|
||||||
export type { Context };
|
export type { Context };
|
||||||
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin';
|
|
||||||
export type { DynamicAuthenticationArg } from './AuthenticationPlugin';
|
export type { DynamicAuthenticationArg } from './AuthenticationPlugin';
|
||||||
|
export type { CallPromptFormDynamicArgs, DynamicPromptFormArg } from './PromptFormPlugin';
|
||||||
|
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin';
|
||||||
export type { TemplateFunctionPlugin };
|
export type { TemplateFunctionPlugin };
|
||||||
export type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
|
|
||||||
export type { FolderActionPlugin } from './FolderActionPlugin';
|
export type { FolderActionPlugin } from './FolderActionPlugin';
|
||||||
|
export type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The global structure of a Yaak plugin
|
* The global structure of a Yaak plugin
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import console from 'node:console';
|
import console from 'node:console';
|
||||||
import { type Stats, statSync, watch } from 'node:fs';
|
import { type Stats, statSync, watch } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import type { Context, PluginDefinition } from '@yaakapp/api';
|
import type {
|
||||||
|
CallPromptFormDynamicArgs,
|
||||||
|
Context,
|
||||||
|
DynamicPromptFormArg,
|
||||||
|
PluginDefinition,
|
||||||
|
} from '@yaakapp/api';
|
||||||
import {
|
import {
|
||||||
applyFormInputDefaults,
|
applyFormInputDefaults,
|
||||||
validateTemplateFunctionArgs,
|
validateTemplateFunctionArgs,
|
||||||
@@ -12,6 +17,7 @@ import type {
|
|||||||
DeleteModelResponse,
|
DeleteModelResponse,
|
||||||
FindHttpResponsesResponse,
|
FindHttpResponsesResponse,
|
||||||
Folder,
|
Folder,
|
||||||
|
FormInput,
|
||||||
GetCookieValueRequest,
|
GetCookieValueRequest,
|
||||||
GetCookieValueResponse,
|
GetCookieValueResponse,
|
||||||
GetHttpRequestByIdResponse,
|
GetHttpRequestByIdResponse,
|
||||||
@@ -55,6 +61,7 @@ export class PluginInstance {
|
|||||||
#mod: PluginDefinition;
|
#mod: PluginDefinition;
|
||||||
#pluginToAppEvents: EventChannel;
|
#pluginToAppEvents: EventChannel;
|
||||||
#appToPluginEvents: EventChannel;
|
#appToPluginEvents: EventChannel;
|
||||||
|
#pendingDynamicForms = new Map<string, DynamicPromptFormArg[]>();
|
||||||
|
|
||||||
constructor(workerData: PluginWorkerData, pluginEvents: EventChannel) {
|
constructor(workerData: PluginWorkerData, pluginEvents: EventChannel) {
|
||||||
this.#workerData = workerData;
|
this.#workerData = workerData;
|
||||||
@@ -106,6 +113,7 @@ export class PluginInstance {
|
|||||||
|
|
||||||
async terminate() {
|
async terminate() {
|
||||||
await this.#mod?.dispose?.();
|
await this.#mod?.dispose?.();
|
||||||
|
this.#pendingDynamicForms.clear();
|
||||||
this.#unimportModule();
|
this.#unimportModule();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,10 +672,58 @@ export class PluginInstance {
|
|||||||
return reply.value;
|
return reply.value;
|
||||||
},
|
},
|
||||||
form: async (args) => {
|
form: async (args) => {
|
||||||
const reply: PromptFormResponse = await this.#sendForReply(context, {
|
// Strip dynamic callbacks before serializing (they can't cross the wire)
|
||||||
type: 'prompt_form_request',
|
const strippedInputs = stripDynamicCallbacks(args.inputs);
|
||||||
...args,
|
|
||||||
|
// 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;
|
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 {
|
function genId(len = 5): string {
|
||||||
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
let id = '';
|
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 {
|
import type {
|
||||||
CallHttpAuthenticationActionArgs,
|
CallHttpAuthenticationActionArgs,
|
||||||
CallTemplateFunctionArgs,
|
CallTemplateFunctionArgs,
|
||||||
} from '@yaakapp-internal/plugins';
|
} from '@yaakapp-internal/plugins';
|
||||||
|
|
||||||
|
type AnyDynamicArg = DynamicTemplateFunctionArg | DynamicAuthenticationArg | DynamicPromptFormArg;
|
||||||
|
type AnyCallArgs =
|
||||||
|
| CallTemplateFunctionArgs
|
||||||
|
| CallHttpAuthenticationActionArgs
|
||||||
|
| CallPromptFormDynamicArgs;
|
||||||
|
|
||||||
export async function applyDynamicFormInput(
|
export async function applyDynamicFormInput(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
args: DynamicTemplateFunctionArg[],
|
args: DynamicTemplateFunctionArg[],
|
||||||
@@ -18,30 +30,40 @@ export async function applyDynamicFormInput(
|
|||||||
|
|
||||||
export async function applyDynamicFormInput(
|
export async function applyDynamicFormInput(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
args: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[],
|
args: DynamicPromptFormArg[],
|
||||||
callArgs: CallTemplateFunctionArgs | CallHttpAuthenticationActionArgs,
|
callArgs: CallPromptFormDynamicArgs,
|
||||||
): Promise<(DynamicTemplateFunctionArg | DynamicAuthenticationArg)[]> {
|
): Promise<DynamicPromptFormArg[]>;
|
||||||
const resolvedArgs: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[] = [];
|
|
||||||
|
export async function applyDynamicFormInput(
|
||||||
|
ctx: Context,
|
||||||
|
args: AnyDynamicArg[],
|
||||||
|
callArgs: AnyCallArgs,
|
||||||
|
): Promise<AnyDynamicArg[]> {
|
||||||
|
const resolvedArgs: AnyDynamicArg[] = [];
|
||||||
for (const { dynamic, ...arg } of args) {
|
for (const { dynamic, ...arg } of args) {
|
||||||
const dynamicResult =
|
const dynamicResult =
|
||||||
typeof dynamic === 'function'
|
typeof dynamic === 'function'
|
||||||
? await dynamic(
|
? await dynamic(
|
||||||
ctx,
|
ctx,
|
||||||
callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs,
|
callArgs as CallTemplateFunctionArgs &
|
||||||
|
CallHttpAuthenticationActionArgs &
|
||||||
|
CallPromptFormDynamicArgs,
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const newArg = {
|
const newArg = {
|
||||||
...arg,
|
...arg,
|
||||||
...dynamicResult,
|
...dynamicResult,
|
||||||
} as DynamicTemplateFunctionArg | DynamicAuthenticationArg;
|
} as AnyDynamicArg;
|
||||||
|
|
||||||
if ('inputs' in newArg && Array.isArray(newArg.inputs)) {
|
if ('inputs' in newArg && Array.isArray(newArg.inputs)) {
|
||||||
try {
|
try {
|
||||||
newArg.inputs = await applyDynamicFormInput(
|
newArg.inputs = await applyDynamicFormInput(
|
||||||
ctx,
|
ctx,
|
||||||
newArg.inputs as DynamicTemplateFunctionArg[],
|
newArg.inputs as DynamicTemplateFunctionArg[],
|
||||||
callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs,
|
callArgs as CallTemplateFunctionArgs &
|
||||||
|
CallHttpAuthenticationActionArgs &
|
||||||
|
CallPromptFormDynamicArgs,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to apply dynamic form input', e);
|
console.error('Failed to apply dynamic form input', e);
|
||||||
|
|||||||
24
plugins-external/httpsnippet/package.json
Normal file
24
plugins-external/httpsnippet/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@yaak/httpsnippet",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"displayName": "HTTP Snippet",
|
||||||
|
"description": "Generate code snippets for HTTP requests in various languages and frameworks",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mountain-loop/yaak.git",
|
||||||
|
"directory": "plugins-external/httpsnippet"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "yaakcli build",
|
||||||
|
"dev": "yaakcli dev",
|
||||||
|
"test": "vitest --run tests"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@readme/httpsnippet": "^11.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
277
plugins-external/httpsnippet/src/index.ts
Normal file
277
plugins-external/httpsnippet/src/index.ts
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import { availableTargets, HTTPSnippet } from '@readme/httpsnippet';
|
||||||
|
import type { HttpRequest, PluginDefinition } from '@yaakapp/api';
|
||||||
|
|
||||||
|
// Get all available targets and build select options
|
||||||
|
const targets = availableTargets();
|
||||||
|
|
||||||
|
// Build language (target) options
|
||||||
|
const languageOptions = targets.map((target) => ({
|
||||||
|
label: target.title,
|
||||||
|
value: target.key,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get client options for a given target key
|
||||||
|
function getClientOptions(targetKey: string) {
|
||||||
|
const target = targets.find((t) => t.key === targetKey);
|
||||||
|
if (!target) return [];
|
||||||
|
return target.clients.map((client) => ({
|
||||||
|
label: client.title,
|
||||||
|
value: client.key,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get default client for a target
|
||||||
|
function getDefaultClient(targetKey: string): string {
|
||||||
|
const target = targets.find((t) => t.key === targetKey);
|
||||||
|
return target?.clients[0]?.key ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
const defaultTarget = 'javascript';
|
||||||
|
const defaultClient = 'fetch';
|
||||||
|
|
||||||
|
// Map target key to editor language for syntax highlighting
|
||||||
|
function getEditorLanguage(targetKey: string): 'javascript' | 'json' | 'text' {
|
||||||
|
if (['javascript', 'node'].includes(targetKey)) return 'javascript';
|
||||||
|
if (targetKey === 'json') return 'json';
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Yaak HttpRequest to HAR format
|
||||||
|
function toHarRequest(request: Partial<HttpRequest>) {
|
||||||
|
// Build URL with query parameters
|
||||||
|
let finalUrl = request.url || '';
|
||||||
|
const urlParams = (request.urlParameters ?? []).filter((p) => p.enabled !== false && !!p.name);
|
||||||
|
if (urlParams.length > 0) {
|
||||||
|
const [base, hash] = finalUrl.split('#');
|
||||||
|
const separator = base?.includes('?') ? '&' : '?';
|
||||||
|
const queryString = urlParams
|
||||||
|
.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
|
||||||
|
.join('&');
|
||||||
|
finalUrl = base + separator + queryString + (hash ? `#${hash}` : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build headers array
|
||||||
|
const headers: Array<{ name: string; value: string }> = (request.headers ?? [])
|
||||||
|
.filter((h) => h.enabled !== false && !!h.name)
|
||||||
|
.map((h) => ({ name: h.name, value: h.value }));
|
||||||
|
|
||||||
|
// Handle authentication
|
||||||
|
if (request.authentication?.disabled !== true) {
|
||||||
|
if (request.authenticationType === 'basic') {
|
||||||
|
const credentials = btoa(
|
||||||
|
`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`,
|
||||||
|
);
|
||||||
|
headers.push({ name: 'Authorization', value: `Basic ${credentials}` });
|
||||||
|
} else if (request.authenticationType === 'bearer') {
|
||||||
|
const prefix = request.authentication?.prefix ?? 'Bearer';
|
||||||
|
const token = request.authentication?.token ?? '';
|
||||||
|
headers.push({ name: 'Authorization', value: `${prefix} ${token}`.trim() });
|
||||||
|
} else if (request.authenticationType === 'apikey') {
|
||||||
|
if (request.authentication?.location === 'header') {
|
||||||
|
headers.push({
|
||||||
|
name: request.authentication?.key ?? 'X-Api-Key',
|
||||||
|
value: request.authentication?.value ?? '',
|
||||||
|
});
|
||||||
|
} else if (request.authentication?.location === 'query') {
|
||||||
|
const sep = finalUrl.includes('?') ? '&' : '?';
|
||||||
|
finalUrl = [
|
||||||
|
finalUrl,
|
||||||
|
sep,
|
||||||
|
encodeURIComponent(request.authentication?.key ?? 'token'),
|
||||||
|
'=',
|
||||||
|
encodeURIComponent(request.authentication?.value ?? ''),
|
||||||
|
].join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build HAR request object
|
||||||
|
const har: Record<string, unknown> = {
|
||||||
|
method: request.method || 'GET',
|
||||||
|
url: finalUrl,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle request body
|
||||||
|
const bodyType = request.bodyType ?? 'none';
|
||||||
|
if (bodyType !== 'none' && request.body) {
|
||||||
|
if (bodyType === 'application/x-www-form-urlencoded' && Array.isArray(request.body.form)) {
|
||||||
|
const params = request.body.form
|
||||||
|
.filter((p: { enabled?: boolean; name?: string }) => p.enabled !== false && !!p.name)
|
||||||
|
.map((p: { name: string; value: string }) => ({ name: p.name, value: p.value }));
|
||||||
|
har.postData = {
|
||||||
|
mimeType: 'application/x-www-form-urlencoded',
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
} else if (bodyType === 'multipart/form-data' && Array.isArray(request.body.form)) {
|
||||||
|
const params = request.body.form
|
||||||
|
.filter((p: { enabled?: boolean; name?: string }) => p.enabled !== false && !!p.name)
|
||||||
|
.map((p: { name: string; value: string; file?: string; contentType?: string }) => {
|
||||||
|
const param: Record<string, string> = { name: p.name, value: p.value || '' };
|
||||||
|
if (p.file) param.fileName = p.file;
|
||||||
|
if (p.contentType) param.contentType = p.contentType;
|
||||||
|
return param;
|
||||||
|
});
|
||||||
|
har.postData = {
|
||||||
|
mimeType: 'multipart/form-data',
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
} else if (bodyType === 'graphql' && typeof request.body.query === 'string') {
|
||||||
|
const body = {
|
||||||
|
query: request.body.query || '',
|
||||||
|
variables: maybeParseJSON(request.body.variables, undefined),
|
||||||
|
};
|
||||||
|
har.postData = {
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(body),
|
||||||
|
};
|
||||||
|
} else if (typeof request.body.text === 'string') {
|
||||||
|
har.postData = {
|
||||||
|
mimeType: bodyType,
|
||||||
|
text: request.body.text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return har;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeParseJSON<T>(v: unknown, fallback: T): T | unknown {
|
||||||
|
if (typeof v !== 'string') return fallback;
|
||||||
|
try {
|
||||||
|
return JSON.parse(v);
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const plugin: PluginDefinition = {
|
||||||
|
httpRequestActions: [
|
||||||
|
{
|
||||||
|
label: 'Generate Code Snippet',
|
||||||
|
icon: 'copy',
|
||||||
|
async onSelect(ctx, args) {
|
||||||
|
// Render the request with variables resolved
|
||||||
|
const renderedRequest = await ctx.httpRequest.render({
|
||||||
|
httpRequest: args.httpRequest,
|
||||||
|
purpose: 'send',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to HAR format
|
||||||
|
const harRequest = toHarRequest(renderedRequest);
|
||||||
|
|
||||||
|
// Get previously selected language or use defaults
|
||||||
|
const storedTarget = await ctx.store.get<string>('selectedTarget');
|
||||||
|
const storedClient = await ctx.store.get<string>('selectedClient');
|
||||||
|
const initialTarget = storedTarget || defaultTarget;
|
||||||
|
const initialClient = storedClient || defaultClient;
|
||||||
|
|
||||||
|
// Create snippet generator
|
||||||
|
const snippet = new HTTPSnippet(harRequest);
|
||||||
|
|
||||||
|
// Generate initial code preview
|
||||||
|
let initialCode = '';
|
||||||
|
try {
|
||||||
|
const result = snippet.convert(initialTarget as any, initialClient);
|
||||||
|
initialCode = Array.isArray(result) ? result.join('\n') : result || '';
|
||||||
|
} catch {
|
||||||
|
initialCode = '// Error generating snippet';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show dialog with language/library selectors and code preview
|
||||||
|
const result = await ctx.prompt.form({
|
||||||
|
id: 'httpsnippet',
|
||||||
|
title: 'Generate Code Snippet',
|
||||||
|
confirmText: 'Copy to Clipboard',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
size: 'md',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: 'h_stack',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'target',
|
||||||
|
label: 'Language',
|
||||||
|
defaultValue: initialTarget,
|
||||||
|
options: languageOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: `client-${initialTarget}`,
|
||||||
|
label: 'Library',
|
||||||
|
defaultValue: initialClient,
|
||||||
|
options: getClientOptions(initialTarget),
|
||||||
|
dynamic(_ctx, { values }) {
|
||||||
|
const targetKey = String(values.target || defaultTarget);
|
||||||
|
const options = getClientOptions(targetKey);
|
||||||
|
return {
|
||||||
|
name: `client-${targetKey}`,
|
||||||
|
options,
|
||||||
|
defaultValue: options[0]?.value ?? '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'editor',
|
||||||
|
name: 'code',
|
||||||
|
label: 'Preview',
|
||||||
|
language: getEditorLanguage(initialTarget),
|
||||||
|
defaultValue: initialCode,
|
||||||
|
readOnly: true,
|
||||||
|
rows: 15,
|
||||||
|
dynamic(_ctx, { values }) {
|
||||||
|
const targetKey = String(values.target || defaultTarget);
|
||||||
|
const clientKey = String(
|
||||||
|
values[`client-${targetKey}`] || getDefaultClient(targetKey),
|
||||||
|
);
|
||||||
|
let code: string;
|
||||||
|
try {
|
||||||
|
const result = snippet.convert(targetKey as any, clientKey);
|
||||||
|
code = Array.isArray(result) ? result.join('\n') : result || '';
|
||||||
|
} catch {
|
||||||
|
code = '// Error generating snippet';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
defaultValue: code,
|
||||||
|
language: getEditorLanguage(targetKey),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// Store the selected language and library for next time
|
||||||
|
const selectedTarget = String(result.target || initialTarget);
|
||||||
|
const selectedClient = String(
|
||||||
|
result[`client-${selectedTarget}`] || getDefaultClient(selectedTarget),
|
||||||
|
);
|
||||||
|
await ctx.store.set('selectedTarget', selectedTarget);
|
||||||
|
await ctx.store.set('selectedClient', selectedClient);
|
||||||
|
|
||||||
|
// Generate snippet for the selected language
|
||||||
|
try {
|
||||||
|
const code = snippet.convert(selectedTarget as any, selectedClient);
|
||||||
|
const codeText = Array.isArray(code) ? code.join('\n') : code || '';
|
||||||
|
await ctx.clipboard.copyText(codeText);
|
||||||
|
await ctx.toast.show({
|
||||||
|
message: 'Code snippet copied to clipboard',
|
||||||
|
icon: 'copy',
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
await ctx.toast.show({
|
||||||
|
message: `Failed to generate snippet: ${err}`,
|
||||||
|
icon: 'alert_triangle',
|
||||||
|
color: 'danger',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -360,8 +360,9 @@ function EditorArg({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
'border border-border rounded-md overflow-hidden px-2 py-1',
|
'border border-border rounded-md overflow-hidden px-2 py-1',
|
||||||
'focus-within:border-border-focus',
|
'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
|
<Editor
|
||||||
id={id}
|
id={id}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { FormInput, JsonPrimitive } from '@yaakapp-internal/plugins';
|
import type { FormInput, JsonPrimitive } from '@yaakapp-internal/plugins';
|
||||||
import type { FormEvent } from 'react';
|
import type { FormEvent } from 'react';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { generateId } from '../../lib/generateId';
|
import { generateId } from '../../lib/generateId';
|
||||||
import { DynamicForm } from '../DynamicForm';
|
import { DynamicForm } from '../DynamicForm';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
@@ -12,16 +12,21 @@ export interface PromptProps {
|
|||||||
onResult: (value: Record<string, JsonPrimitive> | null) => void;
|
onResult: (value: Record<string, JsonPrimitive> | null) => void;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
|
onValuesChange?: (values: Record<string, JsonPrimitive>) => void;
|
||||||
|
onInputsUpdated?: (cb: (inputs: FormInput[]) => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Prompt({
|
export function Prompt({
|
||||||
onCancel,
|
onCancel,
|
||||||
inputs,
|
inputs: initialInputs,
|
||||||
onResult,
|
onResult,
|
||||||
confirmText = 'Confirm',
|
confirmText = 'Confirm',
|
||||||
cancelText = 'Cancel',
|
cancelText = 'Cancel',
|
||||||
|
onValuesChange,
|
||||||
|
onInputsUpdated,
|
||||||
}: PromptProps) {
|
}: PromptProps) {
|
||||||
const [value, setValue] = useState<Record<string, JsonPrimitive>>({});
|
const [value, setValue] = useState<Record<string, JsonPrimitive>>({});
|
||||||
|
const [inputs, setInputs] = useState<FormInput[]>(initialInputs);
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(e: FormEvent<HTMLFormElement>) => {
|
(e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -30,6 +35,16 @@ export function Prompt({
|
|||||||
[onResult, value],
|
[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}`;
|
const id = `prompt.form.${useRef(generateId()).current}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { emit } from '@tauri-apps/api/event';
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
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 { updateAllPlugins } from '@yaakapp-internal/plugins';
|
||||||
import type {
|
import type {
|
||||||
PluginUpdateNotification,
|
PluginUpdateNotification,
|
||||||
@@ -32,6 +38,9 @@ export function initGlobalListeners() {
|
|||||||
|
|
||||||
listenToTauriEvent('settings', () => openSettings.mutate(null));
|
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
|
// Listen for plugin events
|
||||||
listenToTauriEvent<InternalEvent>('plugin_event', async ({ payload: event }) => {
|
listenToTauriEvent<InternalEvent>('plugin_event', async ({ payload: event }) => {
|
||||||
if (event.payload.type === 'prompt_text_request') {
|
if (event.payload.type === 'prompt_text_request') {
|
||||||
@@ -49,26 +58,47 @@ export function initGlobalListeners() {
|
|||||||
};
|
};
|
||||||
await emit(event.id, result);
|
await emit(event.id, result);
|
||||||
} else if (event.payload.type === 'prompt_form_request') {
|
} 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({
|
const values = await showPromptForm({
|
||||||
id: event.payload.id,
|
id: event.payload.id,
|
||||||
title: event.payload.title,
|
title: event.payload.title,
|
||||||
description: event.payload.description,
|
description: event.payload.description,
|
||||||
|
size: event.payload.size,
|
||||||
inputs: event.payload.inputs,
|
inputs: event.payload.inputs,
|
||||||
confirmText: event.payload.confirmText,
|
confirmText: event.payload.confirmText,
|
||||||
cancelText: event.payload.cancelText,
|
cancelText: event.payload.cancelText,
|
||||||
|
onValuesChange: debounce((values) => emitFormResponse(values, false), 150),
|
||||||
|
onInputsUpdated: (cb) => activeForms.set(event.id, cb),
|
||||||
});
|
});
|
||||||
const result: InternalEvent = {
|
|
||||||
id: generateId(),
|
// Clean up and send final response
|
||||||
replyId: event.id,
|
activeForms.delete(event.id);
|
||||||
pluginName: event.pluginName,
|
emitFormResponse(values, true);
|
||||||
pluginRefId: event.pluginRefId,
|
|
||||||
context: event.context,
|
|
||||||
payload: {
|
|
||||||
type: 'prompt_form_response',
|
|
||||||
values,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await emit(event.id, result);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
|
import type { FormInput, JsonPrimitive } from '@yaakapp-internal/plugins';
|
||||||
import type { DialogProps } from '../components/core/Dialog';
|
import type { DialogProps } from '../components/core/Dialog';
|
||||||
import type { PromptProps } from '../components/core/Prompt';
|
import type { PromptProps } from '../components/core/Prompt';
|
||||||
import { Prompt } from '../components/core/Prompt';
|
import { Prompt } from '../components/core/Prompt';
|
||||||
import { showDialog } from './dialog';
|
import { showDialog } from './dialog';
|
||||||
|
|
||||||
type FormArgs = Pick<DialogProps, 'title' | 'description'> &
|
type FormArgs = Pick<DialogProps, 'title' | 'description' | 'size'> &
|
||||||
Omit<PromptProps, 'onClose' | 'onCancel' | 'onResult'> & {
|
Omit<PromptProps, 'onClose' | 'onCancel' | 'onResult'> & {
|
||||||
id: string;
|
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']) => {
|
return new Promise((resolve: PromptProps['onResult']) => {
|
||||||
showDialog({
|
showDialog({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
hideX: true,
|
hideX: true,
|
||||||
size: 'sm',
|
size: size ?? 'sm',
|
||||||
disableBackdropClose: true, // Prevent accidental dismisses
|
disableBackdropClose: true, // Prevent accidental dismisses
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
// Click backdrop, close, or escape
|
// Click backdrop, close, or escape
|
||||||
@@ -32,6 +43,8 @@ export async function showPromptForm({ id, title, description, ...props }: FormA
|
|||||||
resolve(v);
|
resolve(v);
|
||||||
hide();
|
hide();
|
||||||
},
|
},
|
||||||
|
onValuesChange,
|
||||||
|
onInputsUpdated,
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user