orcheo-studio 0.24.3 → 0.24.5

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 (83) hide show
  1. package/.bumpversion.cfg +1 -1
  2. package/dist/assets/{arc-BnLNnEym.js → arc-C-7b0zH4.js} +1 -1
  3. package/dist/assets/{architectureDiagram-VXUJARFQ-B8dIgVkK.js → architectureDiagram-VXUJARFQ-Ciy8lV6J.js} +1 -1
  4. package/dist/assets/{blockDiagram-VD42YOAC-BhDb_8Vg.js → blockDiagram-VD42YOAC-dC88adQ_.js} +1 -1
  5. package/dist/assets/{c4Diagram-YG6GDRKO-DxHEo9Fv.js → c4Diagram-YG6GDRKO-BiHVvXEu.js} +1 -1
  6. package/dist/assets/channel-pJdT5i-M.js +1 -0
  7. package/dist/assets/{chunk-4BX2VUAB-DMBtc1td.js → chunk-4BX2VUAB-C6V7Huup.js} +1 -1
  8. package/dist/assets/{chunk-55IACEB6-DMMV3uf8.js → chunk-55IACEB6-DRmZnlbz.js} +1 -1
  9. package/dist/assets/{chunk-B4BG7PRW-BVmjJQGD.js → chunk-B4BG7PRW-DwV5xRJr.js} +1 -1
  10. package/dist/assets/{chunk-DI55MBZ5-3C02UFww.js → chunk-DI55MBZ5-BHsUZ0IJ.js} +1 -1
  11. package/dist/assets/{chunk-FMBD7UC4-D-PvzEp6.js → chunk-FMBD7UC4-6Q-Lz0AM.js} +1 -1
  12. package/dist/assets/{chunk-QN33PNHL-BNp075o8.js → chunk-QN33PNHL-BFy_ml8O.js} +1 -1
  13. package/dist/assets/{chunk-QZHKN3VN-ChdFCxgk.js → chunk-QZHKN3VN-C1fN91FN.js} +1 -1
  14. package/dist/assets/{chunk-TZMSLE5B-DKr2kcEO.js → chunk-TZMSLE5B-FLRSQ_Fi.js} +1 -1
  15. package/dist/assets/classDiagram-2ON5EDUG-CNT-EGNU.js +1 -0
  16. package/dist/assets/classDiagram-v2-WZHVMYZB-CNT-EGNU.js +1 -0
  17. package/dist/assets/clone-BCi0j082.js +1 -0
  18. package/dist/assets/{cose-bilkent-S5V4N54A-T8_HzNYG.js → cose-bilkent-S5V4N54A-BSWP1X1D.js} +1 -1
  19. package/dist/assets/{dagre-6UL2VRFP-Chzqb9xS.js → dagre-6UL2VRFP-CYoZFlcz.js} +1 -1
  20. package/dist/assets/{diagram-PSM6KHXK-CLiXfMfi.js → diagram-PSM6KHXK-B8RGX0YL.js} +1 -1
  21. package/dist/assets/{diagram-QEK2KX5R-DYE_yT-r.js → diagram-QEK2KX5R-ChWXJ8p_.js} +1 -1
  22. package/dist/assets/{diagram-S2PKOQOG-B4LSPAmZ.js → diagram-S2PKOQOG-Dwic0Fjn.js} +1 -1
  23. package/dist/assets/{erDiagram-Q2GNP2WA-D9fO0sd2.js → erDiagram-Q2GNP2WA-0NSDB0DX.js} +1 -1
  24. package/dist/assets/{flowDiagram-NV44I4VS-BU7f4j4T.js → flowDiagram-NV44I4VS-DEoGWs9L.js} +1 -1
  25. package/dist/assets/{ganttDiagram-JELNMOA3-BSvXqVC4.js → ganttDiagram-JELNMOA3-DqzV4S6l.js} +1 -1
  26. package/dist/assets/{gitGraphDiagram-V2S2FVAM-B6qztLiE.js → gitGraphDiagram-V2S2FVAM-B-EIQM_H.js} +1 -1
  27. package/dist/assets/{graph-CxstBGnL.js → graph-Ha1DTJNG.js} +1 -1
  28. package/dist/assets/{index-Cnz1aoh0.js → index-DTxvVcXp.js} +108 -108
  29. package/dist/assets/{infoDiagram-HS3SLOUP-BN6_fsOH.js → infoDiagram-HS3SLOUP-C4_vpEkn.js} +1 -1
  30. package/dist/assets/{isUndefined-DxBJKB1b.js → isUndefined-DkuoqItu.js} +1 -1
  31. package/dist/assets/{journeyDiagram-XKPGCS4Q-CnjRjeOv.js → journeyDiagram-XKPGCS4Q-BxY4e6OZ.js} +1 -1
  32. package/dist/assets/{kanban-definition-3W4ZIXB7-qaU6OHR6.js → kanban-definition-3W4ZIXB7-BTX3_Q7O.js} +1 -1
  33. package/dist/assets/{layout-BULznkFT.js → layout-Dt1W0fl0.js} +1 -1
  34. package/dist/assets/{linear-ogckhhec.js → linear-CjywSMUX.js} +1 -1
  35. package/dist/assets/{min-DljR10o-.js → min-Dblq6bty.js} +1 -1
  36. package/dist/assets/{mindmap-definition-VGOIOE7T-DaEYqWVU.js → mindmap-definition-VGOIOE7T-C5P7bAFm.js} +1 -1
  37. package/dist/assets/{pieDiagram-ADFJNKIX-C_xhGVfM.js → pieDiagram-ADFJNKIX-BSMylwok.js} +1 -1
  38. package/dist/assets/{quadrantDiagram-AYHSOK5B-D_TV5asG.js → quadrantDiagram-AYHSOK5B-C74B7bqa.js} +1 -1
  39. package/dist/assets/{requirementDiagram-UZGBJVZJ-BuLlPC62.js → requirementDiagram-UZGBJVZJ-CvPzL8Le.js} +1 -1
  40. package/dist/assets/{sankeyDiagram-TZEHDZUN-1P0__QD_.js → sankeyDiagram-TZEHDZUN-BFHm6g6t.js} +1 -1
  41. package/dist/assets/{sequenceDiagram-WL72ISMW-TfKpluC6.js → sequenceDiagram-WL72ISMW-e74mJ0xk.js} +1 -1
  42. package/dist/assets/{stateDiagram-FKZM4ZOC-nQ-BoGjY.js → stateDiagram-FKZM4ZOC-VLQxNVA7.js} +1 -1
  43. package/dist/assets/stateDiagram-v2-4FDKWEC3-Bbpq2t6B.js +1 -0
  44. package/dist/assets/{timeline-definition-IT6M3QCI-Bz79EHNk.js → timeline-definition-IT6M3QCI-oveNP_QD.js} +1 -1
  45. package/dist/assets/{treemap-GDKQZRPO-BiJYOG6x.js → treemap-GDKQZRPO-Cowimd29.js} +1 -1
  46. package/dist/assets/{xychartDiagram-PRI3JC2R-_EJmDCRo.js → xychartDiagram-PRI3JC2R-CP9Fm_aO.js} +1 -1
  47. package/dist/index.html +1 -1
  48. package/dist/orcheo.png +0 -0
  49. package/package.json +1 -1
  50. package/public/orcheo.png +0 -0
  51. package/src/App.tsx +48 -44
  52. package/src/features/account/pages/workspace-members.tsx +58 -62
  53. package/src/features/auth/pages/login.tsx +3 -1
  54. package/src/features/chatkit/lib/public-chat-access.ts +13 -0
  55. package/src/features/chatkit/pages/public-chat.tsx +1 -12
  56. package/src/features/shared/components/chat-interface.types.ts +1 -5
  57. package/src/features/shared/components/top-navigation/version-status.tsx +27 -31
  58. package/src/features/workflow/components/dialogs/add-credential-dialog.tsx +6 -2
  59. package/src/features/workflow/components/dialogs/credentials-vault.tsx +9 -3
  60. package/src/features/workflow/components/dialogs/upload-workflow-dialog.tsx +5 -0
  61. package/src/features/workflow/components/panels/rjsf-cron-widget.tsx +35 -10
  62. package/src/features/workflow/components/trace/agent-prism/SpanCard/SpanCardConnector.tsx +1 -5
  63. package/src/features/workflow/components/trace/agent-prism/shared.ts +1 -10
  64. package/src/features/workflow/lib/workflow-execution-builders.ts +4 -4
  65. package/src/features/workflow/lib/workflow-execution.types.ts +2 -9
  66. package/src/features/workflow/lib/workflow-frontmatter.ts +112 -0
  67. package/src/features/workflow/lib/workflow-storage-api.ts +5 -9
  68. package/src/features/workflow/lib/workflow-storage-helpers.ts +2 -4
  69. package/src/features/workflow/lib/workflow-storage.ts +11 -4
  70. package/src/features/workflow/lib/workflow-storage.types.ts +1 -6
  71. package/src/features/workflow/lib/workflow-upload-errors.ts +6 -2
  72. package/src/features/workflow/pages/workflow/components/workflow-tab-content.tsx +3 -1
  73. package/src/features/workflow/pages/workflow/hooks/controller/use-workflow-execution-controller.ts +4 -1
  74. package/src/features/workflow/pages/workflow/hooks/use-execution-updates.ts +4 -2
  75. package/src/features/workflow/pages/workflow-gallery/create-team-dialog.tsx +2 -6
  76. package/src/lib/api.ts +7 -2
  77. package/src/lib/config.ts +25 -3
  78. package/src/lib/workspace-routing.ts +1 -3
  79. package/dist/assets/channel-0htV4G8n.js +0 -1
  80. package/dist/assets/classDiagram-2ON5EDUG-Blgk-ihj.js +0 -1
  81. package/dist/assets/classDiagram-v2-WZHVMYZB-Blgk-ihj.js +0 -1
  82. package/dist/assets/clone-BgMTf_8w.js +0 -1
  83. package/dist/assets/stateDiagram-v2-4FDKWEC3-hx1MeqAD.js +0 -1
@@ -282,69 +282,65 @@ export default function WorkspaceMembers() {
282
282
  </div>
283
283
  )}
284
284
 
285
- {canManage &&
286
- invitations.some((i) => i.status === "pending") && (
287
- <div className="rounded-lg border">
288
- <div className="border-b px-4 py-3">
289
- <h3 className="text-sm font-medium">Pending Invitations</h3>
290
- </div>
291
- <Table>
292
- <TableHeader>
293
- <TableRow>
294
- <TableHead>Email</TableHead>
295
- <TableHead>Role</TableHead>
296
- <TableHead>Expires</TableHead>
297
- <TableHead className="text-right">Actions</TableHead>
298
- </TableRow>
299
- </TableHeader>
300
- <TableBody>
301
- {invitations
302
- .filter((i) => i.status === "pending")
303
- .map((invitation) => {
304
- const isRevoking =
305
- revokingInvitationId === invitation.id;
306
- const isExpired =
307
- new Date(invitation.expires_at) < new Date();
308
- return (
309
- <TableRow
310
- key={invitation.id}
311
- className={isExpired ? "opacity-60" : undefined}
312
- >
313
- <TableCell className="text-sm">
314
- {invitation.email}
315
- </TableCell>
316
- <TableCell className="capitalize">
317
- {invitation.role}
318
- </TableCell>
319
- <TableCell className="text-sm text-muted-foreground">
320
- {isExpired ? (
321
- <span className="text-destructive">Expired</span>
322
- ) : (
323
- new Date(
324
- invitation.expires_at,
325
- ).toLocaleDateString()
326
- )}
327
- </TableCell>
328
- <TableCell className="text-right">
329
- <Button
330
- variant="ghost"
331
- size="sm"
332
- className="text-destructive hover:text-destructive"
333
- onClick={() =>
334
- void handleRevokeInvitation(invitation.id)
335
- }
336
- disabled={isRevoking}
337
- >
338
- {isRevoking ? "Revoking..." : "Revoke"}
339
- </Button>
340
- </TableCell>
341
- </TableRow>
342
- );
343
- })}
344
- </TableBody>
345
- </Table>
285
+ {canManage && invitations.some((i) => i.status === "pending") && (
286
+ <div className="rounded-lg border">
287
+ <div className="border-b px-4 py-3">
288
+ <h3 className="text-sm font-medium">Pending Invitations</h3>
346
289
  </div>
347
- )}
290
+ <Table>
291
+ <TableHeader>
292
+ <TableRow>
293
+ <TableHead>Email</TableHead>
294
+ <TableHead>Role</TableHead>
295
+ <TableHead>Expires</TableHead>
296
+ <TableHead className="text-right">Actions</TableHead>
297
+ </TableRow>
298
+ </TableHeader>
299
+ <TableBody>
300
+ {invitations
301
+ .filter((i) => i.status === "pending")
302
+ .map((invitation) => {
303
+ const isRevoking = revokingInvitationId === invitation.id;
304
+ const isExpired =
305
+ new Date(invitation.expires_at) < new Date();
306
+ return (
307
+ <TableRow
308
+ key={invitation.id}
309
+ className={isExpired ? "opacity-60" : undefined}
310
+ >
311
+ <TableCell className="text-sm">
312
+ {invitation.email}
313
+ </TableCell>
314
+ <TableCell className="capitalize">
315
+ {invitation.role}
316
+ </TableCell>
317
+ <TableCell className="text-sm text-muted-foreground">
318
+ {isExpired ? (
319
+ <span className="text-destructive">Expired</span>
320
+ ) : (
321
+ new Date(invitation.expires_at).toLocaleDateString()
322
+ )}
323
+ </TableCell>
324
+ <TableCell className="text-right">
325
+ <Button
326
+ variant="ghost"
327
+ size="sm"
328
+ className="text-destructive hover:text-destructive"
329
+ onClick={() =>
330
+ void handleRevokeInvitation(invitation.id)
331
+ }
332
+ disabled={isRevoking}
333
+ >
334
+ {isRevoking ? "Revoking..." : "Revoke"}
335
+ </Button>
336
+ </TableCell>
337
+ </TableRow>
338
+ );
339
+ })}
340
+ </TableBody>
341
+ </Table>
342
+ </div>
343
+ )}
348
344
 
349
345
  {isForbidden ? null : isLoading ? (
350
346
  <p className="text-sm text-muted-foreground">Loading members...</p>
@@ -73,7 +73,9 @@ export default function Login() {
73
73
  setStage("sent");
74
74
  } catch (err) {
75
75
  setError(
76
- err instanceof Error ? err.message : "Unable to send the sign-in email.",
76
+ err instanceof Error
77
+ ? err.message
78
+ : "Unable to send the sign-in email.",
77
79
  );
78
80
  } finally {
79
81
  setBusy(false);
@@ -0,0 +1,13 @@
1
+ import type { PublicChatHttpError } from "@features/chatkit/lib/chatkit-client";
2
+
3
+ export const getPublicChatAccessErrorMessage = (
4
+ error: Pick<PublicChatHttpError, "status" | "code">,
5
+ ): string => {
6
+ if (error.code === "chatkit.auth.workspace_mismatch") {
7
+ return "Your account is signed in, but it is not a member of this workflow workspace. Switch workspaces or ask the owner to add you.";
8
+ }
9
+ if (error.status === 403) {
10
+ return "You do not have permission to use this workflow. Ask the owner to confirm your workspace access.";
11
+ }
12
+ return "You do not have access to this workflow yet. Ask the owner to confirm it is still published.";
13
+ };
@@ -19,6 +19,7 @@ import type { PublicWorkflowMetadata } from "@features/workflow/lib/workflow-sto
19
19
  import { PublicChatWidget } from "@features/chatkit/components/public-chat-widget";
20
20
  import { PublicChatErrorBoundary } from "@features/chatkit/components/public-chat-error-boundary";
21
21
  import type { PublicChatHttpError } from "@features/chatkit/lib/chatkit-client";
22
+ import { getPublicChatAccessErrorMessage } from "@features/chatkit/lib/public-chat-access";
22
23
 
23
24
  type ColorScheme = "light" | "dark";
24
25
 
@@ -27,18 +28,6 @@ type WorkflowState =
27
28
  | { status: "error"; message: string }
28
29
  | { status: "ready"; workflow: PublicWorkflowMetadata };
29
30
 
30
- export const getPublicChatAccessErrorMessage = (
31
- error: Pick<PublicChatHttpError, "status" | "code">,
32
- ): string => {
33
- if (error.code === "chatkit.auth.workspace_mismatch") {
34
- return "Your account is signed in, but it is not a member of this workflow workspace. Switch workspaces or ask the owner to add you.";
35
- }
36
- if (error.status === 403) {
37
- return "You do not have permission to use this workflow. Ask the owner to confirm your workspace access.";
38
- }
39
- return "You do not have access to this workflow yet. Ask the owner to confirm it is still published.";
40
- };
41
-
42
31
  const sanitizeWorkflowNameForEmail = (value: string): string =>
43
32
  value
44
33
  .replace(/[\r\n]+/g, " ")
@@ -27,11 +27,7 @@ export interface ChatInterfaceProps {
27
27
  isMinimizable?: boolean;
28
28
  isClosable?: boolean;
29
29
  position?:
30
- | "bottom-right"
31
- | "bottom-left"
32
- | "top-right"
33
- | "top-left"
34
- | "center";
30
+ "bottom-right" | "bottom-left" | "top-right" | "top-left" | "center";
35
31
  triggerButton?: React.ReactNode;
36
32
  user: ChatParticipant;
37
33
  ai: ChatParticipant;
@@ -44,17 +44,6 @@ const parseCache = (raw: string | null): SystemInfoCachePayload | null => {
44
44
  }
45
45
  };
46
46
 
47
- const shouldRefresh = (cache: SystemInfoCachePayload | null): boolean => {
48
- if (!cache) {
49
- return true;
50
- }
51
- const lastCheckedAt = Date.parse(cache.checkedAt);
52
- if (Number.isNaN(lastCheckedAt)) {
53
- return true;
54
- }
55
- return Date.now() - lastCheckedAt >= UPDATE_CHECK_TTL_MS;
56
- };
57
-
58
47
  const parseSemver = (value: string | null | undefined): ParsedSemver | null => {
59
48
  if (!value) {
60
49
  return null;
@@ -152,7 +141,10 @@ const isDismissed = (): boolean => {
152
141
  };
153
142
 
154
143
  export default function VersionStatus() {
155
- const [systemInfo, setSystemInfo] = useState<SystemInfoResponse | null>(null);
144
+ const [cachedInfo, setCachedInfo] = useState<SystemInfoResponse | null>(null);
145
+ const [liveBackendVersion, setLiveBackendVersion] = useState<string | null>(
146
+ null,
147
+ );
156
148
  const [dismissedReminder, setDismissedReminder] = useState<boolean>(() =>
157
149
  isDismissed(),
158
150
  );
@@ -163,11 +155,12 @@ export default function VersionStatus() {
163
155
  window.localStorage.getItem(UPDATE_CHECK_CACHE_KEY),
164
156
  );
165
157
  if (cache) {
166
- setSystemInfo(cache.payload);
167
- }
168
-
169
- if (!shouldRefresh(cache)) {
170
- return;
158
+ setCachedInfo(cache.payload);
159
+ setLiveBackendVersion(cache.payload.backend.current_version);
160
+ const cacheAge = Date.now() - Date.parse(cache.checkedAt);
161
+ if (cacheAge < UPDATE_CHECK_TTL_MS) {
162
+ return;
163
+ }
171
164
  }
172
165
 
173
166
  let active = true;
@@ -176,7 +169,8 @@ export default function VersionStatus() {
176
169
  if (!active) {
177
170
  return;
178
171
  }
179
- setSystemInfo(payload);
172
+ setLiveBackendVersion(payload.backend.current_version);
173
+ setCachedInfo(payload);
180
174
  const cachePayload: SystemInfoCachePayload = {
181
175
  checkedAt: new Date().toISOString(),
182
176
  payload,
@@ -196,39 +190,41 @@ export default function VersionStatus() {
196
190
  }, []);
197
191
 
198
192
  const studioUpdateAvailable = useMemo(() => {
199
- if (!systemInfo) {
193
+ if (!cachedInfo) {
200
194
  return false;
201
195
  }
202
- if (systemInfo.studio.update_available) {
196
+ if (cachedInfo.studio.update_available) {
203
197
  return true;
204
198
  }
205
- return compareSemver(studioVersion, systemInfo.studio.latest_version) < 0;
206
- }, [studioVersion, systemInfo]);
199
+ return compareSemver(studioVersion, cachedInfo.studio.latest_version) < 0;
200
+ }, [studioVersion, cachedInfo]);
207
201
 
208
202
  const showReminder =
209
- ((systemInfo?.backend.update_available ?? false) ||
203
+ ((cachedInfo?.backend.update_available ?? false) ||
210
204
  studioUpdateAvailable) &&
211
205
  !dismissedReminder;
212
206
 
213
207
  const versionSummary = useMemo(() => {
214
- const backend = systemInfo?.backend.current_version ?? "unknown";
208
+ const backend = liveBackendVersion ?? "unknown";
215
209
  return `Orcheo ${studioVersion} · Backend ${backend}`;
216
- }, [studioVersion, systemInfo]);
210
+ }, [studioVersion, liveBackendVersion]);
217
211
 
218
212
  const updateLines = useMemo(() => {
219
- if (!systemInfo) return [];
213
+ if (!cachedInfo) return [];
220
214
  const lines: string[] = [];
221
- if (systemInfo.backend.update_available) {
215
+ if (cachedInfo.backend.update_available) {
222
216
  lines.push(
223
- `Backend: ${systemInfo.backend.current_version} → ${systemInfo.backend.latest_version}`,
217
+ `Backend: ${cachedInfo.backend.current_version} → ${cachedInfo.backend.latest_version}`,
224
218
  );
225
219
  }
226
220
  if (studioUpdateAvailable) {
227
- const studioCurrent = systemInfo.studio.current_version ?? studioVersion;
228
- lines.push(`Orcheo: ${studioCurrent} → ${systemInfo.studio.latest_version}`);
221
+ const studioCurrent = cachedInfo.studio.current_version ?? studioVersion;
222
+ lines.push(
223
+ `Orcheo: ${studioCurrent} → ${cachedInfo.studio.latest_version}`,
224
+ );
229
225
  }
230
226
  return lines;
231
- }, [studioUpdateAvailable, studioVersion, systemInfo]);
227
+ }, [studioUpdateAvailable, studioVersion, cachedInfo]);
232
228
 
233
229
  const dismissUpdateReminder = () => {
234
230
  if (typeof window === "undefined") {
@@ -27,6 +27,8 @@ type CredentialAccess = CredentialVaultAccessLevel;
27
27
 
28
28
  interface AddCredentialDialogProps {
29
29
  onAddCredential?: (credential: CredentialInput) => Promise<void> | void;
30
+ triggerLabel?: string;
31
+ showTriggerIcon?: boolean;
30
32
  }
31
33
 
32
34
  const DEFAULT_CREDENTIAL: CredentialInput = {
@@ -38,6 +40,8 @@ const DEFAULT_CREDENTIAL: CredentialInput = {
38
40
 
39
41
  export function AddCredentialDialog({
40
42
  onAddCredential,
43
+ triggerLabel = "Add Credential",
44
+ showTriggerIcon = true,
41
45
  }: AddCredentialDialogProps) {
42
46
  const [isOpen, setIsOpen] = useState(false);
43
47
  const [pendingCredential, setPendingCredential] =
@@ -117,8 +121,8 @@ export function AddCredentialDialog({
117
121
  <Dialog open={isOpen} onOpenChange={handleOpenChange}>
118
122
  <DialogTrigger asChild>
119
123
  <Button>
120
- <Plus className="h-4 w-4 mr-2" />
121
- Add Credential
124
+ {showTriggerIcon ? <Plus className="h-4 w-4 mr-2" /> : null}
125
+ {triggerLabel}
122
126
  </Button>
123
127
  </DialogTrigger>
124
128
  <DialogContent className="sm:max-w-[500px]">
@@ -43,16 +43,22 @@ export default function CredentialsVault({
43
43
 
44
44
  return (
45
45
  <div className={containerClassName}>
46
- <div className="flex flex-wrap items-start justify-between gap-3 pr-10">
47
- <div className="space-y-1">
46
+ <div className="space-y-2 pr-10">
47
+ <div className="flex items-start justify-between gap-3">
48
48
  <h2 className="text-xl font-bold">Credential Vault</h2>
49
+ <AddCredentialDialog
50
+ onAddCredential={onAddCredential}
51
+ showTriggerIcon={false}
52
+ triggerLabel="+ Add"
53
+ />
54
+ </div>
55
+ <div className="space-y-1">
49
56
  <p className="text-sm text-muted-foreground">
50
57
  Credentials stored here are securely injected into your workflows at
51
58
  runtime, so nodes can authenticate to external services without
52
59
  hardcoding secrets.
53
60
  </p>
54
61
  </div>
55
- <AddCredentialDialog onAddCredential={onAddCredential} />
56
62
  </div>
57
63
 
58
64
  <div className="flex min-w-0 items-center">
@@ -17,6 +17,7 @@ import {
17
17
  uploadWorkflowFromFiles,
18
18
  WorkflowUploadFailedError,
19
19
  } from "@features/workflow/lib/workflow-storage";
20
+ import { parseWorkflowFrontmatter } from "@features/workflow/lib/workflow-frontmatter";
20
21
  import { getWorkflowRouteRef } from "@features/workflow/lib/workflow-storage-helpers";
21
22
  import { getSelectedWorkspaceSlug } from "@/lib/workspace-session";
22
23
  import { getWorkspaceWorkflowPath } from "@/lib/workspace-routing";
@@ -126,6 +127,10 @@ export function UploadWorkflowDialog({
126
127
  const text = e.target?.result;
127
128
  if (typeof text === "string") {
128
129
  setScriptContent(text);
130
+ const frontmatter = parseWorkflowFrontmatter(text);
131
+ if (!workflowName && frontmatter.name) {
132
+ setWorkflowName(frontmatter.name);
133
+ }
129
134
  }
130
135
  };
131
136
  reader.readAsText(file);
@@ -72,11 +72,20 @@ const clampInt = (value: string, max: number, fallback: number) => {
72
72
  };
73
73
 
74
74
  /** Parse a single cron field's first numeric token, falling back to `fallback`. */
75
- const parseField = (field: string | undefined, max: number, fallback: number) =>
76
- field && field !== "*" ? clampInt(field.split(/[,/-]/)[0], max, fallback) : fallback;
75
+ const parseField = (
76
+ field: string | undefined,
77
+ max: number,
78
+ fallback: number,
79
+ ) =>
80
+ field && field !== "*"
81
+ ? clampInt(field.split(/[,/-]/)[0], max, fallback)
82
+ : fallback;
77
83
 
78
84
  /** Extract the step N from a `*\/N` field (clamped to `max`), or undefined if it isn't a step. */
79
- const parseStep = (field: string | undefined, max: number): number | undefined => {
85
+ const parseStep = (
86
+ field: string | undefined,
87
+ max: number,
88
+ ): number | undefined => {
80
89
  const match = /^\*\/(\d+)$/.exec(field ?? "");
81
90
  return match ? clampInt(match[1], max, 1) || 1 : undefined;
82
91
  };
@@ -98,7 +107,9 @@ const parseDaysOfWeek = (field: string | undefined): number[] => {
98
107
  return Array.from(new Set(days));
99
108
  };
100
109
 
101
- const parseCron = (value: string | undefined): { freq: Frequency; parts: CronParts } => {
110
+ const parseCron = (
111
+ value: string | undefined,
112
+ ): { freq: Frequency; parts: CronParts } => {
102
113
  const [minute, hour, dom, , dow] = (value ?? "").trim().split(/\s+/);
103
114
  const minuteStep = parseStep(minute, 59);
104
115
  const hourStep = parseStep(hour, 23);
@@ -117,13 +128,15 @@ const parseCron = (value: string | undefined): { freq: Frequency; parts: CronPar
117
128
  // `*/30 * * * *` (step) or `* * * * *` (every minute) → minute interval.
118
129
  else if (minuteStep !== undefined || minute === "*") freq = "minute";
119
130
  // `M */2 * * *` (step) or `M * * * *` (every hour) → hour interval.
120
- else if (hourStep !== undefined || hour === "*" || hour === undefined) freq = "hour";
131
+ else if (hourStep !== undefined || hour === "*" || hour === undefined)
132
+ freq = "hour";
121
133
 
122
134
  return { freq, parts };
123
135
  };
124
136
 
125
137
  const buildCron = (freq: Frequency, parts: CronParts): string => {
126
- const { interval, hourInterval, minute, hour, dayOfMonth, daysOfWeek } = parts;
138
+ const { interval, hourInterval, minute, hour, dayOfMonth, daysOfWeek } =
139
+ parts;
127
140
  switch (freq) {
128
141
  case "minute":
129
142
  // `*/1` is just "every minute"; emit the canonical wildcard form.
@@ -136,7 +149,9 @@ const buildCron = (freq: Frequency, parts: CronParts): string => {
136
149
  case "day":
137
150
  return `${minute} ${hour} * * *`;
138
151
  case "week": {
139
- const dow = daysOfWeek.length ? [...daysOfWeek].sort((a, b) => a - b).join(",") : "*";
152
+ const dow = daysOfWeek.length
153
+ ? [...daysOfWeek].sort((a, b) => a - b).join(",")
154
+ : "*";
140
155
  return `${minute} ${hour} * * ${dow}`;
141
156
  }
142
157
  case "month":
@@ -165,7 +180,11 @@ const canonicalCron = (value: string | undefined): string | null => {
165
180
  if (/[a-z]/i.test(fields.join(""))) return null;
166
181
  const days = parseDaysOfWeek(dow);
167
182
  const dowField =
168
- dow === "*" ? "*" : days.length ? [...days].sort((a, b) => a - b).join(",") : dow;
183
+ dow === "*"
184
+ ? "*"
185
+ : days.length
186
+ ? [...days].sort((a, b) => a - b).join(",")
187
+ : dow;
169
188
  return [minute, hour, dom, month, dowField].join(" ");
170
189
  };
171
190
 
@@ -292,7 +311,9 @@ function CronWidget(props: WidgetProps) {
292
311
 
293
312
  {(freq === "minute" || freq === "hour") && (
294
313
  <Select
295
- value={String(freq === "minute" ? parts.interval : parts.hourInterval)}
314
+ value={String(
315
+ freq === "minute" ? parts.interval : parts.hourInterval,
316
+ )}
296
317
  onValueChange={(v) =>
297
318
  freq === "minute"
298
319
  ? update("minute", { ...parts, interval: Number(v) })
@@ -343,7 +364,11 @@ function CronWidget(props: WidgetProps) {
343
364
  "flex h-9 min-w-[6rem] items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
344
365
  )}
345
366
  >
346
- <span className={cn(parts.daysOfWeek.length === 0 && "text-muted-foreground")}>
367
+ <span
368
+ className={cn(
369
+ parts.daysOfWeek.length === 0 && "text-muted-foreground",
370
+ )}
371
+ >
347
372
  {selectedDaysLabel}
348
373
  </span>
349
374
  <ChevronDown className="h-4 w-4 opacity-50" />
@@ -1,9 +1,5 @@
1
1
  export type SpanCardConnectorType =
2
- | "horizontal"
3
- | "vertical"
4
- | "t-right"
5
- | "corner-top-right"
6
- | "empty";
2
+ "horizontal" | "vertical" | "t-right" | "corner-top-right" | "empty";
7
3
 
8
4
  interface SpanCardConnectorProps {
9
5
  type: SpanCardConnectorType;
@@ -31,16 +31,7 @@ export type ColorVariant =
31
31
  | "gray";
32
32
 
33
33
  export type ComponentSize =
34
- | "4"
35
- | "5"
36
- | "6"
37
- | "7"
38
- | "8"
39
- | "9"
40
- | "10"
41
- | "11"
42
- | "12"
43
- | "16";
34
+ "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12" | "16";
44
35
 
45
36
  // CONSTANTS
46
37
 
@@ -50,10 +50,10 @@ const extractVersionRecord = (
50
50
  lookup: WorkflowLookup,
51
51
  ) => {
52
52
  const inputs = history.inputs ?? {};
53
- const metadata = (inputs.metadata ?? inputs.workflow ?? inputs.canvas ?? {}) as Record<
54
- string,
55
- unknown
56
- >;
53
+ const metadata = (inputs.metadata ??
54
+ inputs.workflow ??
55
+ inputs.canvas ??
56
+ {}) as Record<string, unknown>;
57
57
  const versionIdRaw = metadata?.workflow_version_id;
58
58
  if (typeof versionIdRaw === "string" && versionIdRaw) {
59
59
  return lookup.versions.get(versionIdRaw);
@@ -31,17 +31,10 @@ export type WorkflowLookup = {
31
31
  };
32
32
 
33
33
  export type WorkflowExecutionStatus =
34
- | "running"
35
- | "success"
36
- | "failed"
37
- | "partial";
34
+ "running" | "success" | "failed" | "partial";
38
35
 
39
36
  export type WorkflowExecutionNodeStatus =
40
- | "idle"
41
- | "running"
42
- | "success"
43
- | "error"
44
- | "warning";
37
+ "idle" | "running" | "success" | "error" | "warning";
45
38
 
46
39
  export interface WorkflowExecutionNode {
47
40
  id: string;
@@ -0,0 +1,112 @@
1
+ const FRONTMATTER_START_RE = /^# \/\/\/ (?<type>[a-zA-Z0-9_-]+)[ \t]*$/;
2
+ const FRONTMATTER_END_RE = /^# \/\/\/[ \t]*$/;
3
+
4
+ const STRING_FIELDS = new Set([
5
+ "name",
6
+ "id",
7
+ "handle",
8
+ "description",
9
+ "entrypoint",
10
+ ]);
11
+
12
+ export interface WorkflowFrontmatter {
13
+ name?: string;
14
+ id?: string;
15
+ handle?: string;
16
+ description?: string;
17
+ entrypoint?: string;
18
+ }
19
+
20
+ const parseTomlString = (value: string): string | undefined => {
21
+ const trimmed = value.trim();
22
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
23
+ try {
24
+ const parsed = JSON.parse(trimmed) as unknown;
25
+ return typeof parsed === "string" && parsed.trim()
26
+ ? parsed.trim()
27
+ : undefined;
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
33
+ const parsed = trimmed.slice(1, -1);
34
+ return parsed.trim() ? parsed.trim() : undefined;
35
+ }
36
+ return undefined;
37
+ };
38
+
39
+ const collectOrcheoFrontmatter = (source: string): string[] | undefined => {
40
+ const blocks: string[][] = [];
41
+ let currentLines: string[] = [];
42
+ let currentType: string | undefined;
43
+ let inBlock = false;
44
+
45
+ for (const line of source.split(/\r?\n/)) {
46
+ if (inBlock) {
47
+ if (FRONTMATTER_END_RE.test(line)) {
48
+ if (currentType === "orcheo") {
49
+ blocks.push(currentLines);
50
+ }
51
+ currentLines = [];
52
+ currentType = undefined;
53
+ inBlock = false;
54
+ continue;
55
+ }
56
+ currentLines.push(line);
57
+ continue;
58
+ }
59
+
60
+ const match = line.match(FRONTMATTER_START_RE);
61
+ if (match?.groups?.type) {
62
+ currentType = match.groups.type;
63
+ currentLines = [];
64
+ inBlock = true;
65
+ }
66
+ }
67
+
68
+ if (blocks.length !== 1) {
69
+ return undefined;
70
+ }
71
+ return blocks[0];
72
+ };
73
+
74
+ const toTomlLines = (contentLines: string[]): string[] =>
75
+ contentLines.flatMap((line) => {
76
+ if (!line.startsWith("#")) {
77
+ return [];
78
+ }
79
+ let stripped = line.slice(1);
80
+ if (stripped.startsWith(" ") || stripped.startsWith("\t")) {
81
+ stripped = stripped.slice(1);
82
+ }
83
+ return [stripped];
84
+ });
85
+
86
+ export const parseWorkflowFrontmatter = (
87
+ source: string,
88
+ ): WorkflowFrontmatter => {
89
+ const contentLines = collectOrcheoFrontmatter(source);
90
+ if (!contentLines) {
91
+ return {};
92
+ }
93
+
94
+ const frontmatter: WorkflowFrontmatter = {};
95
+ for (const line of toTomlLines(contentLines)) {
96
+ const trimmed = line.trim();
97
+ if (!trimmed || trimmed.startsWith("#")) {
98
+ continue;
99
+ }
100
+ const match = trimmed.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
101
+ const key = match?.[1];
102
+ if (!key || !STRING_FIELDS.has(key)) {
103
+ continue;
104
+ }
105
+ const value = parseTomlString(match[2] ?? "");
106
+ if (!value) {
107
+ continue;
108
+ }
109
+ frontmatter[key as keyof WorkflowFrontmatter] = value;
110
+ }
111
+ return frontmatter;
112
+ };