Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 86 additions & 5 deletions apps/puffer-desktop/src/lib/api/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type {
AutomationCatalogResult,
AutomationDeleteResult,
AutomationListResult,
AutomationPendingActionDetailResult,
AutomationPendingActionListResult,
AutomationPendingActionRejectResult,
AutomationPreviewResult,
AutomationRecordDto,
AutomationRuntimeSyncResult,
Expand Down Expand Up @@ -55,6 +58,7 @@ import type {
TimelineItem,
WorkflowBindingCreateRequest,
WorkflowBackendConnectionTest,
WorkflowBackendLocalRepairResult,
WorkflowBackendSettings,
WorkflowCreateRequest,
WorkflowExecutionListResult,
Expand Down Expand Up @@ -349,8 +353,11 @@ type BackendChromeSecretsImportResult = ChromeSecretsImportResult;

type BackendRemoteOperation = RemoteOperation;

const WORKFLOW_DAEMON_OPTIONS = { requireWebSocket: true, timeoutMs: 15000 } as const;
const AUTOMATION_DAEMON_OPTIONS = { requireWebSocket: true, timeoutMs: 15000 } as const;
// Local workflow runtime startup can pull/recreate containers, run migrations,
// seed credentials, and then wait up to 30s for health checks.
const WORKFLOW_RUNTIME_TIMEOUT_MS = 120_000;
const WORKFLOW_DAEMON_OPTIONS = { requireWebSocket: true, timeoutMs: WORKFLOW_RUNTIME_TIMEOUT_MS } as const;
const AUTOMATION_DAEMON_OPTIONS = { requireWebSocket: true, timeoutMs: WORKFLOW_RUNTIME_TIMEOUT_MS } as const;

type StageChatAttachmentHook = (
sessionId: string,
Expand Down Expand Up @@ -1089,6 +1096,13 @@ export async function testWorkflowBackendConnection(): Promise<WorkflowBackendCo
return workflowRequest<WorkflowBackendConnectionTest>("workflow_backend_test_connection");
}

/** Rebuild the Puffer-managed local workflow runtime after user confirmation. */
export async function repairWorkflowBackendLocalRuntime(): Promise<WorkflowBackendLocalRepairResult> {
return workflowRequest<WorkflowBackendLocalRepairResult>("workflow_backend_repair_local_runtime", {
confirm: true
});
}

export async function saveRemoteSettings(
input: SaveRemoteSettingsInput
): Promise<SettingsSnapshot> {
Expand Down Expand Up @@ -1762,9 +1776,11 @@ export async function saveAutomationRecord(
): Promise<AutomationRecordDto> {
const params: Record<string, unknown> = {
id: input.id,
status: input.status,
spec: input.spec
};
if (input.status !== undefined) {
params.status = input.status;
}
if (input.expectedRevision !== undefined) {
params.expectedRevision = input.expectedRevision;
}
Expand Down Expand Up @@ -1793,6 +1809,18 @@ export async function syncAutomationPreview(
return automationRequest<AutomationRuntimeSyncResult>("automation_sync_preview", params);
}

/** Activate an Automation by compiling/deploying its live runtime artifacts. */
export async function activateAutomationRecord(
id: string,
expectedRevision?: number
): Promise<AutomationRuntimeSyncResult> {
const params: Record<string, unknown> = { id };
if (expectedRevision !== undefined) {
params.expectedRevision = expectedRevision;
}
return automationRequest<AutomationRuntimeSyncResult>("automation_compile_deploy", params);
}

/** Execute a daemon-backed Automation preview run. */
export async function runAutomationPreview(
id: string,
Expand All @@ -1806,6 +1834,35 @@ export async function loadAutomationRunHistory(id: string): Promise<AutomationRu
return automationRequest<AutomationRunHistoryResult>("automation_run_history", { id });
}

/** Load Automation-originated connector drafts waiting for review. */
export async function listAutomationPendingActions(): Promise<AutomationPendingActionListResult> {
return automationRequest<AutomationPendingActionListResult>("automation_pending_action_list");
}

/** Load one Automation-originated connector draft body for review/edit. */
export async function getAutomationPendingAction(
draftId: string,
version: number
): Promise<AutomationPendingActionDetailResult> {
return automationRequest<AutomationPendingActionDetailResult>("automation_pending_action_get", {
draft_id: draftId,
version
});
}

/** Reject one Automation-originated connector draft without sending it. */
export async function rejectAutomationPendingAction(params: {
draftId: string;
version: number;
reason: string;
}): Promise<AutomationPendingActionRejectResult> {
return automationRequest<AutomationPendingActionRejectResult>("automation_pending_action_reject", {
draft_id: params.draftId,
version: params.version,
reason: params.reason
});
}

/** Load registered workflows from the daemon. */
export async function loadWorkflowSnapshot(options?: { includeWorkflows?: boolean }): Promise<WorkflowSnapshot> {
return workflowRequest<WorkflowSnapshot>("workflow_list", {
Expand Down Expand Up @@ -1968,23 +2025,47 @@ export async function saveMonitorMemory(connectionSlug: string, content: string)
export async function executeOutboundAction(params: {
actionId: string;
version: number;
approvedMessage: string;
approvedMessage?: string;
approvedInput?: Record<string, unknown>;
clientRequestId: string;
duplicateRiskAck?: boolean;
}): Promise<{ status: string; actionId: string; receipt?: unknown }> {
const client = await ensureLocalDaemonClient();
const payload: Record<string, unknown> = {
action_id: params.actionId,
version: params.version,
approved_message: params.approvedMessage,
client_request_id: params.clientRequestId
};
if (params.approvedMessage !== undefined) {
payload.approved_message = params.approvedMessage;
}
if (params.approvedInput !== undefined) {
payload.approved_input = params.approvedInput;
}
if (params.duplicateRiskAck === true) {
payload.duplicate_risk_ack = true;
}
return client.request<{ status: string; actionId: string; receipt?: unknown }>("outbound_action_execute", payload);
}

/** Compatibility wrapper for Automation review drafts, now backed by outbound actions. */
export async function executeConnectorActionDraft(params: {
draftId: string;
version: number;
approvedMessage?: string;
approvedInput?: Record<string, unknown>;
clientRequestId: string;
}): Promise<{ status: string; draftId: string; receipt?: unknown }> {
const result = await executeOutboundAction({
actionId: params.draftId,
version: params.version,
approvedMessage: params.approvedMessage,
approvedInput: params.approvedInput,
clientRequestId: params.clientRequestId
});
return { status: result.status, draftId: params.draftId, receipt: result.receipt };
}

/** Read the persisted status for an outbound action. */
export async function outboundActionStatus(params: {
actionId: string;
Expand Down
29 changes: 25 additions & 4 deletions apps/puffer-desktop/src/lib/api/desktop.workflow-daemon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,24 @@ test("marks automation and workflow runtime API calls as daemon-only", async ()
name: "Automation 1",
source: { type: "blank" as const },
instructions: "Run the automation.",
triggers: [{ type: "manual" as const, id: "manual" }],
triggers: [
{
type: "agent_env_node" as const,
id: "trigger-1",
node: {
node_type: "webhook",
name: "Webhook",
trusted: false,
config: { path: "automation-1", methods: ["POST"], authentication: "none" }
}
}
],
flow: {
steps: [
{
type: "agent_env_node" as const,
id: "agent",
node: { node_type: "puffer_agent" }
node: { node_type: "transform_js", config: { code: "return input;" } }
}
]
},
Expand All @@ -115,10 +126,14 @@ test("marks automation and workflow runtime API calls as daemon-only", async ()
await api.saveAutomationRecord({
id: "automation-1",
expectedRevision: 1,
status: "enabled",
status: "paused",
spec: automationSpec
});
await api.activateAutomationRecord("automation-1", 1);
await api.deleteAutomationRecord("automation-1");
await api.listAutomationPendingActions();
await api.getAutomationPendingAction("draft-1", 1);
await api.rejectAutomationPendingAction({ draftId: "draft-1", version: 1, reason: "No longer needed" });
await api.loadWorkflowBackendConfig();
await api.saveWorkflowBackendConfig({
mode: "agent_env_cloud",
Expand All @@ -128,6 +143,7 @@ test("marks automation and workflow runtime API calls as daemon-only", async ()
keepToken: true
});
await api.testWorkflowBackendConnection();
await api.repairWorkflowBackendLocalRuntime();
await api.loadWorkflowSnapshot();
await api.openWorkflowConsole();
await api.listWorkflowNodeDefinitions();
Expand Down Expand Up @@ -167,15 +183,20 @@ test("marks automation and workflow runtime API calls as daemon-only", async ()
await api.deleteWorkflowConnection("telegram-user");
await api.toggleWorkflow("monitor-telegram-user", false);

const workflowOptions = { requireWebSocket: true, timeoutMs: 15000 };
const workflowOptions = { requireWebSocket: true, timeoutMs: 120000 };
expect(request.mock.calls.map(([method, _params, options]) => [method, options])).toEqual([
["automation_list", workflowOptions],
["automation_get", workflowOptions],
["automation_save", workflowOptions],
["automation_compile_deploy", workflowOptions],
["automation_delete", workflowOptions],
["automation_pending_action_list", workflowOptions],
["automation_pending_action_get", workflowOptions],
["automation_pending_action_reject", workflowOptions],
["workflow_backend_get_config", workflowOptions],
["workflow_backend_save_config", workflowOptions],
["workflow_backend_test_connection", workflowOptions],
["workflow_backend_repair_local_runtime", workflowOptions],
["workflow_list", workflowOptions],
["workflow_open_ui", workflowOptions],
["workflow_node_definitions", workflowOptions],
Expand Down
12 changes: 12 additions & 0 deletions apps/puffer-desktop/src/lib/design/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
}
.pf-workflow-backend-mode,
.pf-workflow-backend-token,
.pf-workflow-backend-local-summary,
.pf-workflow-backend-actions {
justify-self: end;
width: min(420px, 100%);
Expand Down Expand Up @@ -255,6 +256,16 @@
.pf-workflow-backend-token .pf-workflow-backend-input {
width: 100%;
}
.pf-workflow-backend-local-summary {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
color: var(--muted-foreground);
font-size: 12px;
line-height: 17px;
}
.pf-workflow-backend-actions {
display: flex;
justify-content: flex-end;
Expand Down Expand Up @@ -889,6 +900,7 @@
.pf-connector-form,
.pf-workflow-backend-mode,
.pf-workflow-backend-token,
.pf-workflow-backend-local-summary,
.pf-workflow-backend-actions,
.pf-workflow-backend-input {
justify-self: stretch;
Expand Down
Loading