orcheo-studio 0.24.0 → 0.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.bumpversion.cfg +1 -1
  2. package/dist/assets/{arc-DAv23sxp.js → arc-B5ofENSG.js} +1 -1
  3. package/dist/assets/{architectureDiagram-VXUJARFQ-D_gUzQPQ.js → architectureDiagram-VXUJARFQ-DxYHdW7c.js} +1 -1
  4. package/dist/assets/{blockDiagram-VD42YOAC-2_hx47qT.js → blockDiagram-VD42YOAC-DJFNLztP.js} +1 -1
  5. package/dist/assets/{c4Diagram-YG6GDRKO-dehdDKxT.js → c4Diagram-YG6GDRKO-i42OpjrQ.js} +1 -1
  6. package/dist/assets/channel-D4idvlBv.js +1 -0
  7. package/dist/assets/{chunk-4BX2VUAB-CtLOuP9F.js → chunk-4BX2VUAB-BP38Jeu1.js} +1 -1
  8. package/dist/assets/{chunk-55IACEB6-CwinXV4-.js → chunk-55IACEB6-DgOteLeo.js} +1 -1
  9. package/dist/assets/{chunk-B4BG7PRW-BI7iPFRv.js → chunk-B4BG7PRW-DSFMHhnv.js} +1 -1
  10. package/dist/assets/{chunk-DI55MBZ5-CNYNrYjW.js → chunk-DI55MBZ5-houO_rDU.js} +1 -1
  11. package/dist/assets/{chunk-FMBD7UC4-BFVTHaU-.js → chunk-FMBD7UC4-2uWxVMuK.js} +1 -1
  12. package/dist/assets/{chunk-QN33PNHL-xWy7sSv1.js → chunk-QN33PNHL-CNE7iDzV.js} +1 -1
  13. package/dist/assets/{chunk-QZHKN3VN-Ds_R1QaP.js → chunk-QZHKN3VN-CuJasZjZ.js} +1 -1
  14. package/dist/assets/{chunk-TZMSLE5B-D8g2OOzb.js → chunk-TZMSLE5B-DdOfh1YS.js} +1 -1
  15. package/dist/assets/classDiagram-2ON5EDUG-BbsIqyB3.js +1 -0
  16. package/dist/assets/classDiagram-v2-WZHVMYZB-BbsIqyB3.js +1 -0
  17. package/dist/assets/clone-BtsJ7Mgy.js +1 -0
  18. package/dist/assets/{cose-bilkent-S5V4N54A-DnkMvL-f.js → cose-bilkent-S5V4N54A-n5L5u7aX.js} +1 -1
  19. package/dist/assets/{dagre-6UL2VRFP-dpKYJ1gN.js → dagre-6UL2VRFP-DA0iPKXN.js} +1 -1
  20. package/dist/assets/{diagram-PSM6KHXK-BnFoR8YQ.js → diagram-PSM6KHXK-3FRZnvw9.js} +1 -1
  21. package/dist/assets/{diagram-QEK2KX5R-Brg1AGVu.js → diagram-QEK2KX5R-ePGkAGnY.js} +1 -1
  22. package/dist/assets/{diagram-S2PKOQOG-Dhmom7Q4.js → diagram-S2PKOQOG-FYWv9iIE.js} +1 -1
  23. package/dist/assets/{erDiagram-Q2GNP2WA-BTez8fDL.js → erDiagram-Q2GNP2WA-2la-V9DA.js} +1 -1
  24. package/dist/assets/{flowDiagram-NV44I4VS-CVXmtc0D.js → flowDiagram-NV44I4VS-Cv-HCQV2.js} +1 -1
  25. package/dist/assets/{ganttDiagram-JELNMOA3-CHbw3AtF.js → ganttDiagram-JELNMOA3-DZhWBmY1.js} +1 -1
  26. package/dist/assets/{gitGraphDiagram-V2S2FVAM-CaPZ1SUE.js → gitGraphDiagram-V2S2FVAM-CBfpN1uw.js} +1 -1
  27. package/dist/assets/{graph-DjLsaDIL.js → graph-D6aI-bPq.js} +1 -1
  28. package/dist/assets/index-D8PZ9Puu.js +795 -0
  29. package/dist/assets/index-Df4jFRhd.css +1 -0
  30. package/dist/assets/{infoDiagram-HS3SLOUP-D_9DW9VU.js → infoDiagram-HS3SLOUP-Lvd-vDDU.js} +1 -1
  31. package/dist/assets/{isUndefined-BkijP54r.js → isUndefined-DLcwy6eJ.js} +1 -1
  32. package/dist/assets/{journeyDiagram-XKPGCS4Q-xxRWc5wn.js → journeyDiagram-XKPGCS4Q-rVvUlw1X.js} +1 -1
  33. package/dist/assets/{kanban-definition-3W4ZIXB7-BCI2NbpB.js → kanban-definition-3W4ZIXB7-Dg-tIUSI.js} +1 -1
  34. package/dist/assets/{layout-M840MAqW.js → layout-CYgJ9n1K.js} +1 -1
  35. package/dist/assets/{linear--_gMRK53.js → linear-CfAw1dgz.js} +1 -1
  36. package/dist/assets/{min-D06s3WKk.js → min-BBMETs6R.js} +1 -1
  37. package/dist/assets/{mindmap-definition-VGOIOE7T-B1-ALKFj.js → mindmap-definition-VGOIOE7T-DY__eUNT.js} +1 -1
  38. package/dist/assets/{pieDiagram-ADFJNKIX-76kjoNcf.js → pieDiagram-ADFJNKIX-Nr7ukKrm.js} +1 -1
  39. package/dist/assets/{quadrantDiagram-AYHSOK5B-O_d3GTQl.js → quadrantDiagram-AYHSOK5B-Bb54Oaa_.js} +1 -1
  40. package/dist/assets/{requirementDiagram-UZGBJVZJ-CasSIIPr.js → requirementDiagram-UZGBJVZJ-BtLViRZC.js} +1 -1
  41. package/dist/assets/{sankeyDiagram-TZEHDZUN-CL_kG7jD.js → sankeyDiagram-TZEHDZUN-COqbhL3I.js} +1 -1
  42. package/dist/assets/{sequenceDiagram-WL72ISMW-RYnztN0k.js → sequenceDiagram-WL72ISMW-DHKy94Yw.js} +1 -1
  43. package/dist/assets/{stateDiagram-FKZM4ZOC-NHFLaLiz.js → stateDiagram-FKZM4ZOC-8dJVXo1X.js} +1 -1
  44. package/dist/assets/stateDiagram-v2-4FDKWEC3-_96AsFOw.js +1 -0
  45. package/dist/assets/{timeline-definition-IT6M3QCI-BH1cZoua.js → timeline-definition-IT6M3QCI-CyS6rBy_.js} +1 -1
  46. package/dist/assets/{treemap-GDKQZRPO-QAQDPzuO.js → treemap-GDKQZRPO-CUM9baT2.js} +1 -1
  47. package/dist/assets/{xychartDiagram-PRI3JC2R-C3M4ZzCP.js → xychartDiagram-PRI3JC2R-BztiRmM1.js} +1 -1
  48. package/dist/index.html +2 -2
  49. package/package.json +1 -1
  50. package/src/features/account/pages/service-tokens.tsx +13 -60
  51. package/src/features/auth/pages/login.tsx +8 -1
  52. package/src/features/workflow/components/dialogs/update-workflow-dialog.tsx +1 -1
  53. package/src/features/workflow/components/dialogs/upload-workflow-dialog.tsx +14 -2
  54. package/src/features/workflow/components/panels/rjsf-cron-widget.tsx +421 -0
  55. package/src/features/workflow/components/panels/rjsf-theme.tsx +2 -0
  56. package/src/features/workflow/data/workflow-types.ts +5 -0
  57. package/src/features/workflow/lib/workflow-storage-api.ts +103 -2
  58. package/src/features/workflow/lib/workflow-storage-helpers.ts +40 -0
  59. package/src/features/workflow/lib/workflow-storage.ts +58 -31
  60. package/src/features/workflow/lib/workflow-storage.types.ts +11 -0
  61. package/src/features/workflow/lib/workflow-upload-errors.ts +81 -0
  62. package/src/features/workflow/pages/workflow/components/workflow-config-sheet.tsx +26 -5
  63. package/src/features/workflow/pages/workflow/components/workflow-layout.tsx +4 -1
  64. package/src/features/workflow/pages/workflow/components/workflow-tab-content.tsx +169 -5
  65. package/src/features/workflow/pages/workflow/hooks/controller/build-layout-props.ts +8 -0
  66. package/src/features/workflow/pages/workflow/hooks/controller/use-workflow-lifecycle.ts +6 -0
  67. package/src/features/workflow/pages/workflow/hooks/controller/use-workflow-resources.ts +2 -0
  68. package/src/features/workflow/pages/workflow/hooks/execution-runtime-updates.ts +1 -1
  69. package/src/features/workflow/pages/workflow/hooks/use-workflow-loader.ts +22 -0
  70. package/src/features/workflow/pages/workflow/hooks/use-workflow-metadata-state.ts +11 -0
  71. package/src/features/workflow/pages/workflow/hooks/use-workflow-saver.ts +8 -0
  72. package/src/features/workflow/pages/workflow/hooks/use-workflow-storage-listener.ts +14 -0
  73. package/src/features/workflow/pages/workflow-gallery/workflow-card.tsx +26 -4
  74. package/src/lib/api.ts +2 -17
  75. package/dist/assets/channel-C3cH7p5A.js +0 -1
  76. package/dist/assets/classDiagram-2ON5EDUG-o6x5AEsV.js +0 -1
  77. package/dist/assets/classDiagram-v2-WZHVMYZB-o6x5AEsV.js +0 -1
  78. package/dist/assets/clone-CnJR7M_G.js +0 -1
  79. package/dist/assets/index-DSthAh3a.css +0 -1
  80. package/dist/assets/index-DlptXU2u.js +0 -795
  81. package/dist/assets/stateDiagram-v2-4FDKWEC3-3y5AUrwb.js +0 -1
@@ -25,6 +25,10 @@ import {
25
25
  persistRunnableConfig,
26
26
  primeWorkflowCache,
27
27
  } from "./workflow-storage-versioning";
28
+ import {
29
+ clearWorkflowUploadError,
30
+ saveWorkflowUploadError,
31
+ } from "./workflow-upload-errors";
28
32
  import type {
29
33
  ApiWorkflow,
30
34
  SaveWorkflowInput,
@@ -53,6 +57,16 @@ let workflowListCache: WorkflowListCacheEntry | undefined;
53
57
  let workflowListInflight: Promise<StoredWorkflow[]> | undefined;
54
58
  let workflowListRequestId = 0;
55
59
 
60
+ export class WorkflowUploadFailedError extends Error {
61
+ workflow: StoredWorkflow;
62
+
63
+ constructor(message: string, workflow: StoredWorkflow) {
64
+ super(message);
65
+ this.name = "WorkflowUploadFailedError";
66
+ this.workflow = workflow;
67
+ }
68
+ }
69
+
56
70
  const resolveActor = (actor?: string): string => {
57
71
  const explicitActor = actor?.trim();
58
72
  if (explicitActor) {
@@ -135,24 +149,6 @@ const buildTemplateWorkflowMetadata = ({
135
149
  summary: { added: 0, removed: 0, modified: 0 },
136
150
  });
137
151
 
138
- const archiveWorkflowAfterFailedIngest = async (
139
- workflowId: string,
140
- actor: string,
141
- ): Promise<void> => {
142
- try {
143
- await request(
144
- `${API_BASE}/${workflowId}?actor=${encodeURIComponent(actor)}`,
145
- {
146
- method: "DELETE",
147
- },
148
- );
149
- invalidateWorkflowListCache();
150
- emitUpdate();
151
- } catch {
152
- // Preserve the original ingest error so callers see why the upload failed.
153
- }
154
- };
155
-
156
152
  const assertTemplatePluginRequirements = async (
157
153
  requiredPlugins: string[] | undefined,
158
154
  ): Promise<void> => {
@@ -249,6 +245,8 @@ export const saveWorkflow = async (
249
245
  if (options?.runnableConfig !== undefined) {
250
246
  await persistRunnableConfig(workflowId, actor, options.runnableConfig);
251
247
  }
248
+ clearWorkflowUploadError(workflowId);
249
+ invalidateWorkflowCache(workflowId);
252
250
 
253
251
  const stored = await ensureWorkflow(workflowId);
254
252
  if (!stored) {
@@ -397,9 +395,27 @@ export const uploadWorkflowFromFiles = async (
397
395
  }),
398
396
  });
399
397
  } catch (error) {
400
- await archiveWorkflowAfterFailedIngest(created.id, actor);
401
- throw error;
398
+ const message =
399
+ error instanceof Error ? error.message : "Upload failed. Please try again.";
400
+ const uploadError = saveWorkflowUploadError(created.id, message);
401
+ const stored = await ensureWorkflow(created.id);
402
+ invalidateWorkflowListCache();
403
+ if (stored) {
404
+ const failedWorkflow = {
405
+ ...stored,
406
+ uploadError,
407
+ };
408
+ primeWorkflowCache(failedWorkflow);
409
+ emitUpdate();
410
+ throw new WorkflowUploadFailedError(message, failedWorkflow);
411
+ }
412
+ emitUpdate();
413
+ throw new WorkflowUploadFailedError(message, {
414
+ ...toStoredWorkflow(created, []),
415
+ uploadError,
416
+ });
402
417
  }
418
+ clearWorkflowUploadError(created.id);
403
419
 
404
420
  const stored = await ensureWorkflow(created.id);
405
421
  if (!stored) {
@@ -420,17 +436,28 @@ export const updateWorkflowFromFiles = async (
420
436
  ): Promise<StoredWorkflow> => {
421
437
  const actor = resolveActor(options?.actor);
422
438
 
423
- await request(`${API_BASE}/${workflowId}/versions/ingest`, {
424
- method: "POST",
425
- body: JSON.stringify({
426
- script,
427
- entrypoint: null,
428
- runnable_config: config ?? null,
429
- metadata: { source: "studio-update" },
430
- notes: null,
431
- created_by: actor,
432
- }),
433
- });
439
+ try {
440
+ await request(`${API_BASE}/${workflowId}/versions/ingest`, {
441
+ method: "POST",
442
+ body: JSON.stringify({
443
+ script,
444
+ entrypoint: null,
445
+ runnable_config: config ?? null,
446
+ metadata: { source: "studio-update" },
447
+ notes: null,
448
+ created_by: actor,
449
+ }),
450
+ });
451
+ } catch (error) {
452
+ const message =
453
+ error instanceof Error ? error.message : "Update failed. Please try again.";
454
+ saveWorkflowUploadError(workflowId, message);
455
+ invalidateWorkflowListCache();
456
+ invalidateWorkflowCache(workflowId);
457
+ emitUpdate();
458
+ throw error;
459
+ }
460
+ clearWorkflowUploadError(workflowId);
434
461
 
435
462
  const stored = await ensureWorkflow(workflowId);
436
463
  if (!stored) {
@@ -17,6 +17,7 @@ export interface ApiWorkflow {
17
17
  id: string;
18
18
  handle?: string | null;
19
19
  team_id?: string | null;
20
+ team_slug?: string | null;
20
21
  name: string;
21
22
  slug: string;
22
23
  description: string | null;
@@ -31,10 +32,16 @@ export interface ApiWorkflow {
31
32
  updated_at: string;
32
33
  share_url?: string | null;
33
34
  chatkit?: WorkflowChatKitConfig | null;
35
+ upload_error?: ApiWorkflowUploadError | null;
34
36
  latest_version?: ApiWorkflowVersion | null;
35
37
  is_scheduled?: boolean;
36
38
  }
37
39
 
40
+ export interface ApiWorkflowUploadError {
41
+ message: string;
42
+ occurred_at: string;
43
+ }
44
+
38
45
  export interface ChatKitStartScreenPrompt {
39
46
  label: string;
40
47
  prompt: string;
@@ -262,6 +269,10 @@ export interface WorkflowVersionRecord {
262
269
  export interface StoredWorkflow extends Workflow {
263
270
  versions: WorkflowVersionRecord[];
264
271
  teamId?: string | null;
272
+ uploadError?: {
273
+ message: string;
274
+ occurredAt: string;
275
+ };
265
276
  draftAccess?: "personal" | "authenticated" | "workspace";
266
277
  isArchived?: boolean;
267
278
  isPublic?: boolean;
@@ -0,0 +1,81 @@
1
+ import type { StoredWorkflow } from "./workflow-storage.types";
2
+
3
+ export interface WorkflowUploadError {
4
+ message: string;
5
+ occurredAt: string;
6
+ }
7
+
8
+ const STORAGE_PREFIX = "orcheo.workflowUploadError.";
9
+
10
+ const storageKey = (workflowId: string): string => `${STORAGE_PREFIX}${workflowId}`;
11
+
12
+ const canUseSessionStorage = (): boolean =>
13
+ typeof window !== "undefined" && Boolean(window.sessionStorage);
14
+
15
+ export const saveWorkflowUploadError = (
16
+ workflowId: string,
17
+ message: string,
18
+ ): WorkflowUploadError => {
19
+ const error = {
20
+ message,
21
+ occurredAt: new Date().toISOString(),
22
+ };
23
+
24
+ if (!canUseSessionStorage()) {
25
+ return error;
26
+ }
27
+
28
+ try {
29
+ window.sessionStorage.setItem(storageKey(workflowId), JSON.stringify(error));
30
+ } catch {
31
+ // Upload failure rendering should not depend on storage availability.
32
+ }
33
+
34
+ return error;
35
+ };
36
+
37
+ export const readWorkflowUploadError = (
38
+ workflowId: string,
39
+ ): WorkflowUploadError | undefined => {
40
+ if (!canUseSessionStorage()) {
41
+ return undefined;
42
+ }
43
+
44
+ try {
45
+ const raw = window.sessionStorage.getItem(storageKey(workflowId));
46
+ if (!raw) {
47
+ return undefined;
48
+ }
49
+ const parsed = JSON.parse(raw) as Partial<WorkflowUploadError>;
50
+ if (
51
+ typeof parsed.message === "string" &&
52
+ parsed.message.trim().length > 0 &&
53
+ typeof parsed.occurredAt === "string"
54
+ ) {
55
+ return {
56
+ message: parsed.message,
57
+ occurredAt: parsed.occurredAt,
58
+ };
59
+ }
60
+ } catch {
61
+ return undefined;
62
+ }
63
+
64
+ return undefined;
65
+ };
66
+
67
+ export const clearWorkflowUploadError = (workflowId: string): void => {
68
+ if (!canUseSessionStorage()) {
69
+ return;
70
+ }
71
+
72
+ try {
73
+ window.sessionStorage.removeItem(storageKey(workflowId));
74
+ } catch {
75
+ // Ignore storage errors; the next successful version is authoritative.
76
+ }
77
+ };
78
+
79
+ export const getWorkflowUploadError = (
80
+ workflow: Pick<StoredWorkflow, "versions" | "uploadError">,
81
+ ): WorkflowUploadError | undefined => workflow.uploadError;
@@ -44,12 +44,28 @@ const baseWorkflowConfigSchema: RJSFSchema = {
44
44
  },
45
45
  };
46
46
 
47
- const workflowConfigUiSchema: UiSchema = {
48
- configurable: {
49
- fields: {
50
- "ui:field": "jsonObject",
47
+ const buildWorkflowConfigUiSchema = (
48
+ configurableSchemas?: Record<string, RJSFSchema>,
49
+ ): UiSchema => {
50
+ // Opt any key declared with `"format": "cron"` into the visual cron widget.
51
+ const cronEntries: UiSchema = {};
52
+ for (const [key, schema] of Object.entries(configurableSchemas ?? {})) {
53
+ if (schema?.format === "cron") {
54
+ cronEntries[key] = { "ui:widget": "cron" };
55
+ }
56
+ }
57
+
58
+ return {
59
+ configurable: {
60
+ // Render configurable fields in the order declared by config.json,
61
+ // with any data-only keys appended afterwards via the "*" wildcard.
62
+ "ui:order": [...Object.keys(configurableSchemas ?? {}), "*"],
63
+ fields: {
64
+ "ui:field": "jsonObject",
65
+ },
66
+ ...cronEntries,
51
67
  },
52
- },
68
+ };
53
69
  };
54
70
 
55
71
  interface WorkflowConfigSheetProps {
@@ -91,6 +107,11 @@ export function WorkflowConfigSheet({
91
107
  };
92
108
  }, [formData.configurable, configurableSchemas]);
93
109
 
110
+ const workflowConfigUiSchema = useMemo<UiSchema>(
111
+ () => buildWorkflowConfigUiSchema(configurableSchemas),
112
+ [configurableSchemas],
113
+ );
114
+
94
115
  useEffect(() => {
95
116
  if (open) {
96
117
  setFormData(toFormData(initialConfig));
@@ -88,7 +88,10 @@ export function WorkflowLayout({
88
88
  forceMount
89
89
  className="m-0 flex min-h-0 flex-1 flex-col overflow-hidden p-0 data-[state=inactive]:hidden"
90
90
  >
91
- <WorkflowTabContent {...workflowProps} />
91
+ <WorkflowTabContent
92
+ {...workflowProps}
93
+ isActive={tabsProps.activeTab === "workflow"}
94
+ />
92
95
  </TabsContent>
93
96
 
94
97
  <TabsContent
@@ -1,13 +1,15 @@
1
- import { useEffect, useMemo, useState } from "react";
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { useUploadsAllowed } from "@/hooks/use-uploads-allowed";
3
3
  import {
4
4
  AlertTriangle,
5
+ CheckCircle2,
5
6
  Copy,
6
7
  ExternalLink,
7
8
  LoaderCircle,
8
9
  Play,
9
10
  RefreshCw,
10
11
  UserMinus,
12
+ XCircle,
11
13
  } from "lucide-react";
12
14
  import { useNavigate } from "react-router-dom";
13
15
  import { Controls, ReactFlow, type Node, type NodeProps } from "@xyflow/react";
@@ -56,16 +58,27 @@ import {
56
58
  } from "@features/workflow/lib/mermaid-renderer";
57
59
  import { resolveWorkflowVersionMermaidSource } from "@features/workflow/lib/workflow-storage-helpers";
58
60
  import { WorkflowConfigSheet } from "@features/workflow/pages/workflow/components/workflow-config-sheet";
61
+ import type { WorkflowExecutionStatus } from "@features/workflow/pages/workflow/helpers/types";
62
+
63
+ const RUN_RESULT_BANNER_TIMEOUT_MS = 6000;
59
64
 
60
65
  export interface WorkflowTabContentProps {
61
66
  workflowId: string | null;
62
67
  workflowRouteRef?: string | null;
63
68
  workflowName: string;
69
+ workflowHandle?: string | null;
70
+ workflowTeamSlug?: string | null;
64
71
  versions: WorkflowVersionRecord[];
72
+ uploadError?: {
73
+ message: string;
74
+ occurredAt: string;
75
+ } | null;
65
76
  isLoading: boolean;
66
77
  loadError: string | null;
67
78
  isRunPending: boolean;
68
79
  isRunning: boolean;
80
+ isActive?: boolean;
81
+ lastRunStatus?: WorkflowExecutionStatus | null;
69
82
  onRunWorkflow: () => Promise<void>;
70
83
  onSaveConfig: (nextConfig: WorkflowRunnableConfig | null) => Promise<void>;
71
84
  hasCronTriggerNode: boolean;
@@ -189,14 +202,85 @@ const getErrorMessage = (error: unknown, fallback: string): string => {
189
202
  return rawMessage;
190
203
  };
191
204
 
205
+ const formatWorkflowHandle = (
206
+ handle?: string | null,
207
+ teamSlug?: string | null,
208
+ ): string | null => {
209
+ const trimmedHandle = handle?.trim().replace(/^@+/, "");
210
+ if (!trimmedHandle) {
211
+ return null;
212
+ }
213
+
214
+ if (trimmedHandle.includes("/")) {
215
+ return `@${trimmedHandle}`;
216
+ }
217
+
218
+ const trimmedTeamSlug = teamSlug
219
+ ?.trim()
220
+ .replace(/^@+/, "")
221
+ .replace(/\/+$/, "");
222
+
223
+ return trimmedTeamSlug
224
+ ? `@${trimmedTeamSlug}/${trimmedHandle}`
225
+ : `@${trimmedHandle}`;
226
+ };
227
+
228
+ interface RunResultBanner {
229
+ Icon: typeof CheckCircle2;
230
+ className: string;
231
+ message: string;
232
+ }
233
+
234
+ const resolveRunResultBanner = (
235
+ status: WorkflowExecutionStatus,
236
+ ): RunResultBanner => {
237
+ switch (status) {
238
+ case "success":
239
+ return {
240
+ Icon: CheckCircle2,
241
+ className:
242
+ "border-emerald-500/40 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300",
243
+ message: "Workflow run succeeded. See the full trace on the Trace tab.",
244
+ };
245
+ case "failed":
246
+ return {
247
+ Icon: XCircle,
248
+ className: "border-destructive/40 bg-destructive/5 text-destructive",
249
+ message: "Workflow run failed. Check the Trace tab for details.",
250
+ };
251
+ case "partial":
252
+ return {
253
+ Icon: AlertTriangle,
254
+ className:
255
+ "border-amber-500/40 bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300",
256
+ message:
257
+ "Workflow run finished with partial results. Check the Trace tab for details.",
258
+ };
259
+ default:
260
+ // "running" reaching here means the stream ended without a final status.
261
+ return {
262
+ Icon: AlertTriangle,
263
+ className:
264
+ "border-amber-500/40 bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300",
265
+ message:
266
+ "Workflow run ended without a final status. Check the Trace tab for the latest status.",
267
+ };
268
+ }
269
+ };
270
+
192
271
  export function WorkflowTabContent({
193
272
  workflowId,
194
273
  workflowName,
274
+ workflowHandle,
275
+ workflowTeamSlug,
195
276
  versions,
277
+ uploadError,
196
278
  isLoading,
197
279
  loadError,
198
280
  isRunPending,
199
281
  isRunning,
282
+ isActive = true,
283
+ lastRunStatus = null,
200
284
  onRunWorkflow,
201
285
  onSaveConfig,
202
286
  hasCronTriggerNode,
@@ -208,6 +292,8 @@ export function WorkflowTabContent({
208
292
  const navigate = useNavigate();
209
293
  const uploadsAllowed = useUploadsAllowed();
210
294
  const latestVersion = versions.at(-1);
295
+ const workflowSubtitleLabel =
296
+ formatWorkflowHandle(workflowHandle, workflowTeamSlug) ?? workflowName;
211
297
  const [isConfigOpen, setIsConfigOpen] = useState(false);
212
298
  const [isPublished, setIsPublished] = useState(initialIsPublished);
213
299
  const [requireLogin, setRequireLogin] = useState(initialRequireLogin);
@@ -223,6 +309,10 @@ export function WorkflowTabContent({
223
309
  const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false);
224
310
  const [isMissingCredentialsDialogOpen, setIsMissingCredentialsDialogOpen] =
225
311
  useState(false);
312
+ const [visibleRunResultStatus, setVisibleRunResultStatus] =
313
+ useState<WorkflowExecutionStatus | null>(null);
314
+ const isRunActive = isRunPending || isRunning;
315
+ const wasRunActiveRef = useRef(isRunActive);
226
316
  const hasMissingCredentials = missingCredentials.length > 0;
227
317
  const canDeleteWorkflow = Boolean(workflowId);
228
318
 
@@ -348,6 +438,26 @@ export function WorkflowTabContent({
348
438
  };
349
439
  }, [mermaidCacheKey, mermaidRenderId, mermaidSource]);
350
440
 
441
+ useEffect(() => {
442
+ const didFinishRun =
443
+ wasRunActiveRef.current && !isRunActive && Boolean(lastRunStatus);
444
+ wasRunActiveRef.current = isRunActive;
445
+
446
+ if (!isActive || isRunActive || !didFinishRun || !lastRunStatus) {
447
+ setVisibleRunResultStatus(null);
448
+ return;
449
+ }
450
+
451
+ setVisibleRunResultStatus(lastRunStatus);
452
+ const timeoutId = window.setTimeout(() => {
453
+ setVisibleRunResultStatus(null);
454
+ }, RUN_RESULT_BANNER_TIMEOUT_MS);
455
+
456
+ return () => {
457
+ window.clearTimeout(timeoutId);
458
+ };
459
+ }, [isActive, isRunActive, lastRunStatus]);
460
+
351
461
  const diagramNodes = useMemo(() => {
352
462
  if (!diagramSvg) {
353
463
  return [] as Node[];
@@ -390,6 +500,12 @@ export function WorkflowTabContent({
390
500
  }
391
501
 
392
502
  const canConfigure = Boolean(workflowId);
503
+ const hasUploadError = Boolean(uploadError);
504
+ const uploadBlocksWorkflow = hasUploadError && !latestVersion;
505
+ const runResultBanner =
506
+ !isRunActive && visibleRunResultStatus
507
+ ? resolveRunResultBanner(visibleRunResultStatus)
508
+ : null;
393
509
  const canRun = Boolean(workflowId && latestVersion);
394
510
  const runButtonDisabled = !canRun || isRunPending || isRunning;
395
511
  const runButtonLabel = isRunPending || isRunning ? "Running..." : "Run";
@@ -427,6 +543,16 @@ export function WorkflowTabContent({
427
543
  return;
428
544
  }
429
545
 
546
+ if (uploadBlocksWorkflow) {
547
+ setIsPublished(false);
548
+ toast({
549
+ title: "Fix upload error first",
550
+ description: "Fix the error and upload again before publishing.",
551
+ variant: "destructive",
552
+ });
553
+ return;
554
+ }
555
+
430
556
  if (nextValue) {
431
557
  setIsPublishDialogOpen(true);
432
558
  return;
@@ -500,6 +626,16 @@ export function WorkflowTabContent({
500
626
  return;
501
627
  }
502
628
 
629
+ if (uploadBlocksWorkflow) {
630
+ setIsScheduled(false);
631
+ toast({
632
+ title: "Fix upload error first",
633
+ description: "Fix the error and upload again before scheduling.",
634
+ variant: "destructive",
635
+ });
636
+ return;
637
+ }
638
+
503
639
  setIsSchedulePending(true);
504
640
  try {
505
641
  if (nextValue) {
@@ -577,7 +713,7 @@ export function WorkflowTabContent({
577
713
  {workflowName || "Workflow"}
578
714
  </h2>
579
715
  <p className="text-sm text-muted-foreground">
580
- {workflowName}
716
+ {workflowSubtitleLabel}
581
717
  {latestVersion ? ` · ${latestVersion.version}` : ""}
582
718
  </p>
583
719
  </div>
@@ -595,7 +731,7 @@ export function WorkflowTabContent({
595
731
  onCheckedChange={(checked) =>
596
732
  void handlePublishToggle(checked)
597
733
  }
598
- disabled={isPublishPending}
734
+ disabled={isPublishPending || uploadBlocksWorkflow}
599
735
  />
600
736
  </div>
601
737
  </TooltipTrigger>
@@ -615,7 +751,9 @@ export function WorkflowTabContent({
615
751
  onCheckedChange={(checked) =>
616
752
  void handleScheduleToggle(checked)
617
753
  }
618
- disabled={isSchedulePending || !canToggleSchedule}
754
+ disabled={
755
+ isSchedulePending || !canToggleSchedule || uploadBlocksWorkflow
756
+ }
619
757
  />
620
758
  </div>
621
759
  {workflowId && uploadsAllowed === true ? (
@@ -667,7 +805,7 @@ export function WorkflowTabContent({
667
805
  </div>
668
806
  </div>
669
807
 
670
- {(isRunPending || isRunning) && (
808
+ {isRunActive && (
671
809
  <div
672
810
  role="status"
673
811
  className="rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-sm text-primary"
@@ -677,6 +815,32 @@ export function WorkflowTabContent({
677
815
  </div>
678
816
  )}
679
817
 
818
+ {runResultBanner && (
819
+ <div
820
+ role="status"
821
+ className={`flex items-center gap-2 rounded-md border px-3 py-2 text-sm ${runResultBanner.className}`}
822
+ >
823
+ <runResultBanner.Icon className="h-4 w-4 shrink-0" />
824
+ <span>{runResultBanner.message}</span>
825
+ </div>
826
+ )}
827
+
828
+ {uploadError && (
829
+ <Alert variant="destructive">
830
+ <XCircle className="h-4 w-4" />
831
+ <AlertTitle>Workflow upload failed</AlertTitle>
832
+ <AlertDescription>
833
+ <div className="space-y-2">
834
+ <p>{uploadError.message}</p>
835
+ <p>
836
+ Fix the error in the workflow script or config, then upload
837
+ again.
838
+ </p>
839
+ </div>
840
+ </AlertDescription>
841
+ </Alert>
842
+ )}
843
+
680
844
  {isPublished && shareUrl && (
681
845
  <div className="flex items-center justify-between rounded-md border border-border/60 bg-muted/20 px-3 py-2">
682
846
  <div className="min-w-0">
@@ -75,15 +75,23 @@ export function buildWorkflowLayoutProps(
75
75
  resources: WorkflowResources,
76
76
  execution: WorkflowExecutionController,
77
77
  ): WorkflowLayoutProps {
78
+ const activeExecution = core.execution.executions.find(
79
+ (execution) => execution.id === core.execution.activeExecutionId,
80
+ );
81
+
78
82
  const workflowProps: WorkflowTabContentProps = {
79
83
  workflowId: core.metadata.currentWorkflowId,
80
84
  workflowRouteRef: core.routeWorkflowRef,
81
85
  workflowName: core.metadata.workflowName,
86
+ workflowHandle: core.metadata.workflowHandle,
87
+ workflowTeamSlug: core.metadata.workflowTeamSlug,
82
88
  versions: core.metadata.workflowVersions ?? [],
89
+ uploadError: core.metadata.workflowUploadError,
83
90
  isLoading: core.metadata.isWorkflowLoading,
84
91
  loadError: core.metadata.workflowLoadError,
85
92
  isRunPending: execution.isRunPending,
86
93
  isRunning: core.execution.isRunning,
94
+ lastRunStatus: activeExecution?.status ?? null,
87
95
  onRunWorkflow: execution.handleRunPersistedWorkflow,
88
96
  onSaveConfig: resources.saver.handleSaveWorkflowConfig,
89
97
  hasCronTriggerNode: hasSchedulableCronTrigger(
@@ -13,8 +13,11 @@ export function useWorkflowLifecycle(
13
13
  setCurrentWorkflowId: core.metadata.setCurrentWorkflowId,
14
14
  setWorkflowName: core.metadata.setWorkflowName,
15
15
  setWorkflowDescription: core.metadata.setWorkflowDescription,
16
+ setWorkflowHandle: core.metadata.setWorkflowHandle,
17
+ setWorkflowTeamSlug: core.metadata.setWorkflowTeamSlug,
16
18
  setWorkflowTags: core.metadata.setWorkflowTags,
17
19
  setWorkflowVersions: core.metadata.setWorkflowVersions,
20
+ setWorkflowUploadError: core.metadata.setWorkflowUploadError,
18
21
  setChatkitStartScreenPrompts: core.metadata.setChatkitStartScreenPrompts,
19
22
  setChatkitSupportedModels: core.metadata.setChatkitSupportedModels,
20
23
  setIsWorkflowPublic: core.metadata.setIsWorkflowPublic,
@@ -30,7 +33,10 @@ export function useWorkflowLifecycle(
30
33
  currentWorkflowId: core.metadata.currentWorkflowId,
31
34
  setWorkflowName: core.metadata.setWorkflowName,
32
35
  setWorkflowDescription: core.metadata.setWorkflowDescription,
36
+ setWorkflowHandle: core.metadata.setWorkflowHandle,
37
+ setWorkflowTeamSlug: core.metadata.setWorkflowTeamSlug,
33
38
  setWorkflowVersions: core.metadata.setWorkflowVersions,
39
+ setWorkflowUploadError: core.metadata.setWorkflowUploadError,
34
40
  setWorkflowTags: core.metadata.setWorkflowTags,
35
41
  setChatkitStartScreenPrompts: core.metadata.setChatkitStartScreenPrompts,
36
42
  setChatkitSupportedModels: core.metadata.setChatkitSupportedModels,
@@ -40,6 +40,8 @@ export function useWorkflowResources(
40
40
  const saver = useWorkflowSaver({
41
41
  setWorkflowName: core.metadata.setWorkflowName,
42
42
  setWorkflowDescription: core.metadata.setWorkflowDescription,
43
+ setWorkflowHandle: core.metadata.setWorkflowHandle,
44
+ setWorkflowTeamSlug: core.metadata.setWorkflowTeamSlug,
43
45
  setWorkflowVersions: core.metadata.setWorkflowVersions,
44
46
  setWorkflowTags: core.metadata.setWorkflowTags,
45
47
  workflowName: core.metadata.workflowName,
@@ -43,7 +43,7 @@ export function collectRuntimeUpdates(
43
43
  return;
44
44
  }
45
45
 
46
- const resultsCandidate = value["results"];
46
+ const resultsCandidate = value["node_results"];
47
47
  let candidatePayload: unknown;
48
48
 
49
49
  if (isRecord(resultsCandidate)) {