Add recipe download and copy yaml (#6201)

This commit is contained in:
Zane
2026-01-05 09:55:50 -08:00
committed by GitHub
parent 901cac9441
commit edcd011543
12 changed files with 300 additions and 19 deletions
+3
View File
@@ -392,6 +392,7 @@ derive_utoipa!(Icon as IconSchema);
super::routes::recipe::set_recipe_slash_command, super::routes::recipe::set_recipe_slash_command,
super::routes::recipe::save_recipe, super::routes::recipe::save_recipe,
super::routes::recipe::parse_recipe, super::routes::recipe::parse_recipe,
super::routes::recipe::recipe_to_yaml,
super::routes::setup::start_openrouter_setup, super::routes::setup::start_openrouter_setup,
super::routes::setup::start_tetrate_setup, super::routes::setup::start_tetrate_setup,
super::routes::tunnel::start_tunnel, super::routes::tunnel::start_tunnel,
@@ -508,6 +509,8 @@ derive_utoipa!(Icon as IconSchema);
super::routes::errors::ErrorResponse, super::routes::errors::ErrorResponse,
super::routes::recipe::ParseRecipeRequest, super::routes::recipe::ParseRecipeRequest,
super::routes::recipe::ParseRecipeResponse, super::routes::recipe::ParseRecipeResponse,
super::routes::recipe::RecipeToYamlRequest,
super::routes::recipe::RecipeToYamlResponse,
goose::recipe::Recipe, goose::recipe::Recipe,
goose::recipe::Author, goose::recipe::Author,
goose::recipe::Settings, goose::recipe::Settings,
+32
View File
@@ -137,6 +137,16 @@ pub struct SetSlashCommandRequest {
slash_command: Option<String>, slash_command: Option<String>,
} }
#[derive(Debug, Deserialize, ToSchema)]
pub struct RecipeToYamlRequest {
recipe: Recipe,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct RecipeToYamlResponse {
yaml: String,
}
#[utoipa::path( #[utoipa::path(
post, post,
path = "/recipes/create", path = "/recipes/create",
@@ -520,6 +530,27 @@ async fn parse_recipe(
Ok(Json(ParseRecipeResponse { recipe })) Ok(Json(ParseRecipeResponse { recipe }))
} }
#[utoipa::path(
post,
path = "/recipes/to-yaml",
request_body = RecipeToYamlRequest,
responses(
(status = 200, description = "Recipe converted to YAML successfully", body = RecipeToYamlResponse),
(status = 400, description = "Bad request - Failed to convert recipe to YAML", body = ErrorResponse),
),
tag = "Recipe Management"
)]
async fn recipe_to_yaml(
Json(request): Json<RecipeToYamlRequest>,
) -> Result<Json<RecipeToYamlResponse>, ErrorResponse> {
let yaml = request.recipe.to_yaml().map_err(|e| ErrorResponse {
message: format!("Failed to convert recipe to YAML: {}", e),
status: StatusCode::BAD_REQUEST,
})?;
Ok(Json(RecipeToYamlResponse { yaml }))
}
pub fn routes(state: Arc<AppState>) -> Router { pub fn routes(state: Arc<AppState>) -> Router {
Router::new() Router::new()
.route("/recipes/create", post(create_recipe)) .route("/recipes/create", post(create_recipe))
@@ -532,6 +563,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/recipes/slash-command", post(set_recipe_slash_command)) .route("/recipes/slash-command", post(set_recipe_slash_command))
.route("/recipes/save", post(save_recipe)) .route("/recipes/save", post(save_recipe))
.route("/recipes/parse", post(parse_recipe)) .route("/recipes/parse", post(parse_recipe))
.route("/recipes/to-yaml", post(recipe_to_yaml))
.with_state(state) .with_state(state)
} }
+62
View File
@@ -1558,6 +1558,46 @@
} }
} }
}, },
"/recipes/to-yaml": {
"post": {
"tags": [
"Recipe Management"
],
"operationId": "recipe_to_yaml",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RecipeToYamlRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Recipe converted to YAML successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RecipeToYamlResponse"
}
}
}
},
"400": {
"description": "Bad request - Failed to convert recipe to YAML",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/reply": { "/reply": {
"post": { "post": {
"tags": [ "tags": [
@@ -4762,6 +4802,28 @@
"user_prompt" "user_prompt"
] ]
}, },
"RecipeToYamlRequest": {
"type": "object",
"required": [
"recipe"
],
"properties": {
"recipe": {
"$ref": "#/components/schemas/Recipe"
}
}
},
"RecipeToYamlResponse": {
"type": "object",
"required": [
"yaml"
],
"properties": {
"yaml": {
"type": "string"
}
}
},
"RedactedThinkingContent": { "RedactedThinkingContent": {
"type": "object", "type": "object",
"required": [ "required": [
+10 -1
View File
@@ -2,7 +2,7 @@
import type { Client, Options as Options2, TDataShape } from './client'; import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen'; import { client } from './client.gen';
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & { export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/** /**
@@ -315,6 +315,15 @@ export const setRecipeSlashCommand = <ThrowOnError extends boolean = false>(opti
} }
}); });
export const recipeToYaml = <ThrowOnError extends boolean = false>(options: Options<RecipeToYamlData, ThrowOnError>) => (options.client ?? client).post<RecipeToYamlResponses, RecipeToYamlErrors, ThrowOnError>({
url: '/recipes/to-yaml',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
export const reply = <ThrowOnError extends boolean = false>(options: Options<ReplyData, ThrowOnError>) => (options.client ?? client).sse.post<ReplyResponses, ReplyErrors, ThrowOnError>({ export const reply = <ThrowOnError extends boolean = false>(options: Options<ReplyData, ThrowOnError>) => (options.client ?? client).sse.post<ReplyResponses, ReplyErrors, ThrowOnError>({
url: '/reply', url: '/reply',
...options, ...options,
+33
View File
@@ -732,6 +732,14 @@ export type RecipeParameterInputType = 'string' | 'number' | 'boolean' | 'date'
export type RecipeParameterRequirement = 'required' | 'optional' | 'user_prompt'; export type RecipeParameterRequirement = 'required' | 'optional' | 'user_prompt';
export type RecipeToYamlRequest = {
recipe: Recipe;
};
export type RecipeToYamlResponse = {
yaml: string;
};
export type RedactedThinkingContent = { export type RedactedThinkingContent = {
data: string; data: string;
}; };
@@ -2325,6 +2333,31 @@ export type SetRecipeSlashCommandResponses = {
200: unknown; 200: unknown;
}; };
export type RecipeToYamlData = {
body: RecipeToYamlRequest;
path?: never;
query?: never;
url: '/recipes/to-yaml';
};
export type RecipeToYamlErrors = {
/**
* Bad request - Failed to convert recipe to YAML
*/
400: ErrorResponse;
};
export type RecipeToYamlError = RecipeToYamlErrors[keyof RecipeToYamlErrors];
export type RecipeToYamlResponses = {
/**
* Recipe converted to YAML successfully
*/
200: RecipeToYamlResponse;
};
export type RecipeToYamlResponse2 = RecipeToYamlResponses[keyof RecipeToYamlResponses];
export type ReplyData = { export type ReplyData = {
body: ChatRequest; body: ChatRequest;
path?: never; path?: never;
+5 -1
View File
@@ -12,7 +12,11 @@ export type McpMethodParams = {
export type McpMethodResponse = { export type McpMethodResponse = {
'ui/open-link': { status: string; message: string }; 'ui/open-link': { status: string; message: string };
'ui/message': { status: string; message: string }; 'ui/message': { status: string; message: string };
'tools/call': { content: unknown[]; isError: boolean; structuredContent?: Record<string, unknown> }; 'tools/call': {
content: unknown[];
isError: boolean;
structuredContent?: Record<string, unknown>;
};
'resources/read': { contents: unknown[] }; 'resources/read': { contents: unknown[] };
'notifications/message': Record<string, never>; 'notifications/message': Record<string, never>;
ping: Record<string, never>; ping: Record<string, never>;
@@ -275,7 +275,7 @@ const getToolName = (toolCallName: string): string => {
if (lastIndex === -1) return toolCallName; if (lastIndex === -1) return toolCallName;
return toolCallName.substring(lastIndex + 2); return toolCallName.substring(lastIndex + 2);
} };
// Helper function to extract extension name for tooltip // Helper function to extract extension name for tooltip
const getExtensionTooltip = (toolCallName: string): string | null => { const getExtensionTooltip = (toolCallName: string): string | null => {
+115 -14
View File
@@ -11,13 +11,16 @@ import {
Clock, Clock,
Terminal, Terminal,
ExternalLink, ExternalLink,
Share2,
Copy,
Download,
} from 'lucide-react'; } from 'lucide-react';
import { ScrollArea } from '../ui/scroll-area'; import { ScrollArea } from '../ui/scroll-area';
import { Card } from '../ui/card'; import { Card } from '../ui/card';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Skeleton } from '../ui/skeleton'; import { Skeleton } from '../ui/skeleton';
import { MainPanelLayout } from '../Layout/MainPanelLayout'; import { MainPanelLayout } from '../Layout/MainPanelLayout';
import { toastSuccess } from '../../toasts'; import { toastSuccess, toastError } from '../../toasts';
import { useEscapeKey } from '../../hooks/useEscapeKey'; import { useEscapeKey } from '../../hooks/useEscapeKey';
import { import {
deleteRecipe, deleteRecipe,
@@ -25,6 +28,7 @@ import {
startAgent, startAgent,
scheduleRecipe, scheduleRecipe,
setRecipeSlashCommand, setRecipeSlashCommand,
recipeToYaml,
} from '../../api'; } from '../../api';
import ImportRecipeForm, { ImportRecipeButton } from './ImportRecipeForm'; import ImportRecipeForm, { ImportRecipeButton } from './ImportRecipeForm';
import CreateEditRecipeModal from './CreateEditRecipeModal'; import CreateEditRecipeModal from './CreateEditRecipeModal';
@@ -38,10 +42,19 @@ import {
trackRecipeDeleted, trackRecipeDeleted,
trackRecipeStarted, trackRecipeStarted,
trackRecipeDeeplinkCopied, trackRecipeDeeplinkCopied,
trackRecipeYamlCopied,
trackRecipeExportedToFile,
trackRecipeScheduled, trackRecipeScheduled,
trackRecipeSlashCommandSet, trackRecipeSlashCommandSet,
getErrorType, getErrorType,
} from '../../utils/analytics'; } from '../../utils/analytics';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from '../ui/dropdown-menu';
export default function RecipesView() { export default function RecipesView() {
const setView = useNavigation(); const setView = useNavigation();
@@ -217,13 +230,85 @@ export default function RecipesView() {
} catch (error) { } catch (error) {
console.error('Failed to copy deeplink:', error); console.error('Failed to copy deeplink:', error);
trackRecipeDeeplinkCopied(false, getErrorType(error)); trackRecipeDeeplinkCopied(false, getErrorType(error));
toastSuccess({ toastError({
title: 'Copy failed', title: 'Copy failed',
msg: 'Failed to copy deeplink to clipboard', msg: 'Failed to copy deeplink to clipboard',
}); });
} }
}; };
const handleCopyYaml = async (recipeManifest: RecipeManifest) => {
try {
const response = await recipeToYaml({
body: { recipe: recipeManifest.recipe },
throwOnError: true,
});
if (!response.data?.yaml) {
throw new Error('No YAML data returned from API');
}
await navigator.clipboard.writeText(response.data.yaml);
trackRecipeYamlCopied(true);
toastSuccess({
title: 'YAML copied',
msg: 'Recipe YAML has been copied to clipboard',
});
} catch (error) {
console.error('Failed to copy YAML:', error);
trackRecipeYamlCopied(false, getErrorType(error));
toastError({
title: 'Copy failed',
msg: 'Failed to copy recipe YAML to clipboard',
});
}
};
const handleExportFile = async (recipeManifest: RecipeManifest) => {
try {
const response = await recipeToYaml({
body: { recipe: recipeManifest.recipe },
throwOnError: true,
});
if (!response.data?.yaml) {
throw new Error('No YAML data returned from API');
}
const sanitizedTitle = (recipeManifest.recipe.title || 'recipe')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
const filename = `${sanitizedTitle}.yaml`;
const result = await window.electron.showSaveDialog({
title: 'Export Recipe',
defaultPath: filename,
filters: [
{ name: 'YAML Files', extensions: ['yaml', 'yml'] },
{ name: 'All Files', extensions: ['*'] },
],
});
if (!result.canceled && result.filePath) {
await window.electron.writeFile(result.filePath, response.data.yaml);
trackRecipeExportedToFile(true);
toastSuccess({
title: 'Recipe exported',
msg: `Recipe saved to ${result.filePath}`,
});
}
} catch (error) {
console.error('Failed to export recipe:', error);
trackRecipeExportedToFile(false, getErrorType(error));
toastError({
title: 'Export failed',
msg: 'Failed to export recipe to file',
});
}
};
const handleOpenScheduleDialog = (recipeManifest: RecipeManifest) => { const handleOpenScheduleDialog = (recipeManifest: RecipeManifest) => {
setScheduleRecipeManifest(recipeManifest); setScheduleRecipeManifest(recipeManifest);
setScheduleCron(recipeManifest.schedule_cron || '0 0 14 * * *'); setScheduleCron(recipeManifest.schedule_cron || '0 0 14 * * *');
@@ -450,18 +535,34 @@ export default function RecipesView() {
> >
<Edit className="w-4 h-4" /> <Edit className="w-4 h-4" />
</Button> </Button>
<Button <DropdownMenu>
onClick={(e) => { <DropdownMenuTrigger asChild>
e.stopPropagation(); <Button
handleCopyDeeplink(recipeManifestResponse); onClick={(e) => e.stopPropagation()}
}} variant="outline"
variant="outline" size="sm"
size="sm" className="h-8 w-8 p-0"
className="h-8 w-8 p-0" title="Share recipe"
title="Copy deeplink" >
> <Share2 className="w-4 h-4" />
<Link className="w-4 h-4" /> </Button>
</Button> </DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuItem onClick={() => handleCopyDeeplink(recipeManifestResponse)}>
<Link className="w-4 h-4" />
Copy Deeplink
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleCopyYaml(recipeManifestResponse)}>
<Copy className="w-4 h-4" />
Copy YAML
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleExportFile(recipeManifestResponse)}>
<Download className="w-4 h-4" />
Export to File
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button <Button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -38,7 +38,8 @@ export default function ExtensionsSection({
onModalClose, onModalClose,
searchTerm = '', searchTerm = '',
}: ExtensionSectionProps) { }: ExtensionSectionProps) {
const { getExtensions, addExtension, removeExtension, extensionsList, extensionWarnings } = useConfig(); const { getExtensions, addExtension, removeExtension, extensionsList, extensionWarnings } =
useConfig();
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null); const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
+4 -1
View File
@@ -1766,11 +1766,14 @@ ipcMain.handle('list-files', async (_event, dirPath, extension) => {
} }
}); });
// Handle message box dialogs
ipcMain.handle('show-message-box', async (_event, options) => { ipcMain.handle('show-message-box', async (_event, options) => {
return dialog.showMessageBox(options); return dialog.showMessageBox(options);
}); });
ipcMain.handle('show-save-dialog', async (_event, options) => {
return dialog.showSaveDialog(options);
});
ipcMain.handle('get-allowed-extensions', async () => { ipcMain.handle('get-allowed-extensions', async () => {
return await getAllowList(); return await getAllowList();
}); });
+17
View File
@@ -20,6 +20,21 @@ interface MessageBoxResponse {
checkboxChecked?: boolean; checkboxChecked?: boolean;
} }
interface SaveDialogOptions {
title?: string;
defaultPath?: string;
buttonLabel?: string;
filters?: Array<{ name: string; extensions: string[] }>;
message?: string;
nameFieldLabel?: string;
showsTagField?: boolean;
}
interface SaveDialogResponse {
canceled: boolean;
filePath?: string;
}
interface FileResponse { interface FileResponse {
file: string; file: string;
filePath: string; filePath: string;
@@ -58,6 +73,7 @@ type ElectronAPI = {
logInfo: (txt: string) => void; logInfo: (txt: string) => void;
showNotification: (data: NotificationData) => void; showNotification: (data: NotificationData) => void;
showMessageBox: (options: MessageBoxOptions) => Promise<MessageBoxResponse>; showMessageBox: (options: MessageBoxOptions) => Promise<MessageBoxResponse>;
showSaveDialog: (options: SaveDialogOptions) => Promise<SaveDialogResponse>;
openInChrome: (url: string) => void; openInChrome: (url: string) => void;
fetchMetadata: (url: string) => Promise<string>; fetchMetadata: (url: string) => Promise<string>;
reloadApp: () => void; reloadApp: () => void;
@@ -158,6 +174,7 @@ const electronAPI: ElectronAPI = {
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt), logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
showNotification: (data: NotificationData) => ipcRenderer.send('notify', data), showNotification: (data: NotificationData) => ipcRenderer.send('notify', data),
showMessageBox: (options: MessageBoxOptions) => ipcRenderer.invoke('show-message-box', options), showMessageBox: (options: MessageBoxOptions) => ipcRenderer.invoke('show-message-box', options),
showSaveDialog: (options: SaveDialogOptions) => ipcRenderer.invoke('show-save-dialog', options),
openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url), openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url),
fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', url), fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', url),
reloadApp: () => ipcRenderer.send('reload-app'), reloadApp: () => ipcRenderer.send('reload-app'),
+16
View File
@@ -115,6 +115,8 @@ export type AnalyticsEvent =
properties: { success: boolean; error_details?: string; in_new_window?: boolean }; properties: { success: boolean; error_details?: string; in_new_window?: boolean };
} }
| { name: 'recipe_deeplink_copied'; properties: { success: boolean; error_details?: string } } | { name: 'recipe_deeplink_copied'; properties: { success: boolean; error_details?: string } }
| { name: 'recipe_yaml_copied'; properties: { success: boolean; error_details?: string } }
| { name: 'recipe_exported_to_file'; properties: { success: boolean; error_details?: string } }
| { | {
name: 'recipe_scheduled'; name: 'recipe_scheduled';
properties: { success: boolean; error_details?: string; action: 'add' | 'edit' | 'remove' }; properties: { success: boolean; error_details?: string; action: 'add' | 'edit' | 'remove' };
@@ -536,6 +538,20 @@ export function trackRecipeDeeplinkCopied(success: boolean, errorDetails?: strin
}); });
} }
export function trackRecipeYamlCopied(success: boolean, errorDetails?: string): void {
trackEvent({
name: 'recipe_yaml_copied',
properties: { success, error_details: errorDetails },
});
}
export function trackRecipeExportedToFile(success: boolean, errorDetails?: string): void {
trackEvent({
name: 'recipe_exported_to_file',
properties: { success, error_details: errorDetails },
});
}
export function trackRecipeScheduled( export function trackRecipeScheduled(
success: boolean, success: boolean,
action: 'add' | 'edit' | 'remove', action: 'add' | 'edit' | 'remove',