Add recipe download and copy yaml (#6201)
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user