orcheo-studio 0.22.2 → 0.23.1

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 (88) hide show
  1. package/.bumpversion.cfg +1 -1
  2. package/dist/assets/{arc-De9dLqyl.js → arc-CqnxfCHp.js} +1 -1
  3. package/dist/assets/{architectureDiagram-VXUJARFQ-hEslfmYm.js → architectureDiagram-VXUJARFQ-UGPIhA1j.js} +1 -1
  4. package/dist/assets/{blockDiagram-VD42YOAC-CyWSMWJa.js → blockDiagram-VD42YOAC-Cn6wka40.js} +1 -1
  5. package/dist/assets/{c4Diagram-YG6GDRKO-CxoTVJ47.js → c4Diagram-YG6GDRKO-BvPRNdur.js} +1 -1
  6. package/dist/assets/channel-FjxqoZWk.js +1 -0
  7. package/dist/assets/{chunk-4BX2VUAB-twftUw9o.js → chunk-4BX2VUAB-F_o1AmTw.js} +1 -1
  8. package/dist/assets/{chunk-55IACEB6-DQZDNQUo.js → chunk-55IACEB6-Cl075es2.js} +1 -1
  9. package/dist/assets/{chunk-B4BG7PRW-BYGOoNB5.js → chunk-B4BG7PRW-DcErQhy9.js} +1 -1
  10. package/dist/assets/{chunk-DI55MBZ5-qyPhcZF3.js → chunk-DI55MBZ5-CFxCndOp.js} +1 -1
  11. package/dist/assets/{chunk-FMBD7UC4-BgUDHGEG.js → chunk-FMBD7UC4-XOcF0fgy.js} +1 -1
  12. package/dist/assets/{chunk-QN33PNHL-DHlMh8NC.js → chunk-QN33PNHL-CjJH_Bvj.js} +1 -1
  13. package/dist/assets/{chunk-QZHKN3VN-Bfc8IV0O.js → chunk-QZHKN3VN-nLb-Go8u.js} +1 -1
  14. package/dist/assets/{chunk-TZMSLE5B-BqwdftTm.js → chunk-TZMSLE5B-L07ovmT0.js} +1 -1
  15. package/dist/assets/classDiagram-2ON5EDUG-8JYNY9Ic.js +1 -0
  16. package/dist/assets/classDiagram-v2-WZHVMYZB-8JYNY9Ic.js +1 -0
  17. package/dist/assets/clone-NPVQC0Uc.js +1 -0
  18. package/dist/assets/{cose-bilkent-S5V4N54A-g5agH7fS.js → cose-bilkent-S5V4N54A-B6IcYcMk.js} +1 -1
  19. package/dist/assets/{dagre-6UL2VRFP-ajLMaXPP.js → dagre-6UL2VRFP-CN7ADhef.js} +1 -1
  20. package/dist/assets/{diagram-PSM6KHXK-BndVcBhB.js → diagram-PSM6KHXK-3chqLyzk.js} +1 -1
  21. package/dist/assets/{diagram-QEK2KX5R-BFiIYeu7.js → diagram-QEK2KX5R-MrbKZWaG.js} +1 -1
  22. package/dist/assets/{diagram-S2PKOQOG-DXy-A9pn.js → diagram-S2PKOQOG-Dr7BijIx.js} +1 -1
  23. package/dist/assets/{erDiagram-Q2GNP2WA-D_Bac_58.js → erDiagram-Q2GNP2WA-Ck-v9a6N.js} +1 -1
  24. package/dist/assets/{flowDiagram-NV44I4VS-DhEi8qtz.js → flowDiagram-NV44I4VS-D662eMD1.js} +1 -1
  25. package/dist/assets/{ganttDiagram-JELNMOA3-BtpUvOuc.js → ganttDiagram-JELNMOA3-c2mXK47M.js} +1 -1
  26. package/dist/assets/{gitGraphDiagram-V2S2FVAM-MBES60cx.js → gitGraphDiagram-V2S2FVAM-DUMhfhsS.js} +1 -1
  27. package/dist/assets/{graph-BcwvIN-m.js → graph-DkWlmVun.js} +1 -1
  28. package/dist/assets/index-BVY_PYJU.css +1 -0
  29. package/dist/assets/index-kH6EkBsZ.js +790 -0
  30. package/dist/assets/{infoDiagram-HS3SLOUP-vaIFYyC9.js → infoDiagram-HS3SLOUP-OTL7qvkP.js} +1 -1
  31. package/dist/assets/{isUndefined-DVO6xakl.js → isUndefined-BmZvH1PX.js} +1 -1
  32. package/dist/assets/{journeyDiagram-XKPGCS4Q-D-_q99If.js → journeyDiagram-XKPGCS4Q-BYzqE5D8.js} +1 -1
  33. package/dist/assets/{kanban-definition-3W4ZIXB7-i_AZcs_J.js → kanban-definition-3W4ZIXB7-NtzQ4y9p.js} +1 -1
  34. package/dist/assets/{layout-CPJ9nR_z.js → layout-CwhyLMkP.js} +1 -1
  35. package/dist/assets/{linear-KBGpPBiH.js → linear-Cirf2V1x.js} +1 -1
  36. package/dist/assets/{min-By5f7YdD.js → min-C81vLJYh.js} +1 -1
  37. package/dist/assets/{mindmap-definition-VGOIOE7T-DRjtRxFS.js → mindmap-definition-VGOIOE7T-TDnLQIKZ.js} +1 -1
  38. package/dist/assets/{pieDiagram-ADFJNKIX-BkkSglWM.js → pieDiagram-ADFJNKIX-d0-JhjNZ.js} +1 -1
  39. package/dist/assets/{quadrantDiagram-AYHSOK5B-DY-Ng2qt.js → quadrantDiagram-AYHSOK5B-wCoihp8a.js} +1 -1
  40. package/dist/assets/{requirementDiagram-UZGBJVZJ-B5w0etXG.js → requirementDiagram-UZGBJVZJ-BqiCIjcI.js} +1 -1
  41. package/dist/assets/{sankeyDiagram-TZEHDZUN-BWntUp8b.js → sankeyDiagram-TZEHDZUN-BPH0Pd1D.js} +1 -1
  42. package/dist/assets/{sequenceDiagram-WL72ISMW-Cl77fd7y.js → sequenceDiagram-WL72ISMW-B8FfJraH.js} +1 -1
  43. package/dist/assets/{stateDiagram-FKZM4ZOC-B8_DoPW6.js → stateDiagram-FKZM4ZOC-ZQR3Q9_8.js} +1 -1
  44. package/dist/assets/stateDiagram-v2-4FDKWEC3-uDYn1wPF.js +1 -0
  45. package/dist/assets/{timeline-definition-IT6M3QCI-udvNLTe_.js → timeline-definition-IT6M3QCI-D-arYxX5.js} +1 -1
  46. package/dist/assets/{treemap-GDKQZRPO-BSvoMl74.js → treemap-GDKQZRPO-Dw5BHp_B.js} +1 -1
  47. package/dist/assets/{xychartDiagram-PRI3JC2R-CTdy-FCX.js → xychartDiagram-PRI3JC2R-Dt1zH6V2.js} +1 -1
  48. package/dist/index.html +2 -2
  49. package/package.json +1 -1
  50. package/src/App.tsx +7 -2
  51. package/src/features/account/pages/invitation-accept.tsx +92 -0
  52. package/src/features/account/pages/workspace-members.tsx +153 -42
  53. package/src/features/auth/components/require-auth.tsx +13 -18
  54. package/src/features/auth/lib/auth-api.ts +196 -0
  55. package/src/features/auth/lib/auth-session.ts +0 -1
  56. package/src/features/auth/pages/login.tsx +169 -16
  57. package/src/features/auth/pages/verify.tsx +89 -0
  58. package/src/features/chatkit/components/public-chat-widget.tsx +89 -8
  59. package/src/features/chatkit/lib/chatkit-client.ts +66 -0
  60. package/src/features/chatkit/pages/public-chat.tsx +17 -30
  61. package/src/features/shared/components/top-navigation/account-menu.tsx +20 -7
  62. package/src/features/shared/components/top-navigation/studio-brand.tsx +14 -3
  63. package/src/features/workflow/components/dialogs/publish-workflow-dialog.tsx +121 -0
  64. package/src/features/workflow/data/templates/candidate-badges.ts +28 -0
  65. package/src/features/workflow/data/workflow-types.ts +6 -0
  66. package/src/features/workflow/lib/workflow-storage-helpers.ts +5 -3
  67. package/src/features/workflow/lib/workflow-storage-versioning.ts +4 -0
  68. package/src/features/workflow/lib/workflow-storage.types.ts +1 -0
  69. package/src/features/workflow/pages/workflow/components/workflow-tab-content.tsx +122 -37
  70. package/src/features/workflow/pages/workflow/hooks/controller/build-layout-props.ts +2 -0
  71. package/src/features/workflow/pages/workflow/hooks/controller/use-workflow-lifecycle.ts +1 -0
  72. package/src/features/workflow/pages/workflow/hooks/use-workflow-loader.ts +5 -0
  73. package/src/features/workflow/pages/workflow/hooks/use-workflow-metadata-state.ts +3 -0
  74. package/src/features/workflow/pages/workflow/hooks/use-workflow-saver.ts +11 -2
  75. package/src/features/workflow/pages/workflow-gallery/workflow-gallery-tabs.tsx +47 -4
  76. package/src/lib/api.ts +78 -1
  77. package/src/vite-env.d.ts +1 -0
  78. package/dist/assets/channel-CzCzGn0f.js +0 -1
  79. package/dist/assets/classDiagram-2ON5EDUG-ekNI5d_4.js +0 -1
  80. package/dist/assets/classDiagram-v2-WZHVMYZB-ekNI5d_4.js +0 -1
  81. package/dist/assets/clone-DowBu1Kh.js +0 -1
  82. package/dist/assets/index-DLYzf0K-.js +0 -775
  83. package/dist/assets/index-DebsXgfj.css +0 -1
  84. package/dist/assets/stateDiagram-v2-4FDKWEC3-DHL7p-md.js +0 -1
  85. package/src/features/MODULES.md +0 -199
  86. package/src/features/auth/components/auto-login.tsx +0 -51
  87. package/src/features/auth/lib/oidc-client.ts +0 -565
  88. package/src/features/auth/pages/oauth-callback.tsx +0 -63
@@ -0,0 +1,121 @@
1
+ import { useState } from "react";
2
+ import { Check, Globe, Lock } from "lucide-react";
3
+
4
+ import { Button } from "@/design-system/ui/button";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "@/design-system/ui/dialog";
13
+ import { cn } from "@/lib/utils";
14
+
15
+ export type PublishVisibility = "public" | "workspace";
16
+
17
+ interface PublishOption {
18
+ value: PublishVisibility;
19
+ title: string;
20
+ description: string;
21
+ icon: typeof Globe;
22
+ }
23
+
24
+ const PUBLISH_OPTIONS: PublishOption[] = [
25
+ {
26
+ value: "public",
27
+ title: "Public",
28
+ description:
29
+ "Anyone with the link can use the workflow. No sign-in required.",
30
+ icon: Globe,
31
+ },
32
+ {
33
+ value: "workspace",
34
+ title: "Workspace only",
35
+ description:
36
+ "Only signed-in members of this workspace can use the workflow.",
37
+ icon: Lock,
38
+ },
39
+ ];
40
+
41
+ interface PublishWorkflowDialogProps {
42
+ open: boolean;
43
+ isPending?: boolean;
44
+ onOpenChange: (open: boolean) => void;
45
+ onConfirm: (requireLogin: boolean) => Promise<void> | void;
46
+ }
47
+
48
+ export function PublishWorkflowDialog({
49
+ open,
50
+ isPending = false,
51
+ onOpenChange,
52
+ onConfirm,
53
+ }: PublishWorkflowDialogProps) {
54
+ const [selected, setSelected] = useState<PublishVisibility>("public");
55
+ const handleOpenChange = (nextOpen: boolean) => {
56
+ if (!nextOpen) {
57
+ setSelected("public");
58
+ }
59
+ onOpenChange(nextOpen);
60
+ };
61
+
62
+ return (
63
+ <Dialog open={open} onOpenChange={handleOpenChange}>
64
+ <DialogContent>
65
+ <DialogHeader>
66
+ <DialogTitle>Publish workflow</DialogTitle>
67
+ <DialogDescription>
68
+ Choose who can access the published chat.
69
+ </DialogDescription>
70
+ </DialogHeader>
71
+ <div className="grid gap-3 py-2">
72
+ {PUBLISH_OPTIONS.map((option) => {
73
+ const Icon = option.icon;
74
+ const isSelected = selected === option.value;
75
+ return (
76
+ <button
77
+ key={option.value}
78
+ type="button"
79
+ aria-pressed={isSelected}
80
+ onClick={() => setSelected(option.value)}
81
+ disabled={isPending}
82
+ className={cn(
83
+ "flex items-start gap-3 rounded-lg border p-3 text-left transition-colors",
84
+ isSelected
85
+ ? "border-primary bg-primary/5"
86
+ : "border-border hover:bg-muted/40",
87
+ )}
88
+ >
89
+ <Icon className="mt-0.5 h-5 w-5 shrink-0 text-muted-foreground" />
90
+ <div className="min-w-0 flex-1">
91
+ <p className="text-sm font-medium">{option.title}</p>
92
+ <p className="text-xs text-muted-foreground">
93
+ {option.description}
94
+ </p>
95
+ </div>
96
+ {isSelected ? (
97
+ <Check className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
98
+ ) : null}
99
+ </button>
100
+ );
101
+ })}
102
+ </div>
103
+ <DialogFooter>
104
+ <Button
105
+ variant="outline"
106
+ onClick={() => handleOpenChange(false)}
107
+ disabled={isPending}
108
+ >
109
+ Cancel
110
+ </Button>
111
+ <Button
112
+ onClick={() => void onConfirm(selected === "workspace")}
113
+ disabled={isPending}
114
+ >
115
+ {isPending ? "Publishing..." : "Publish"}
116
+ </Button>
117
+ </DialogFooter>
118
+ </DialogContent>
119
+ </Dialog>
120
+ );
121
+ }
@@ -27,6 +27,33 @@ export interface CandidateBadgeDefinition extends CandidateBadgeSpec {
27
27
  templateDefinition: WorkflowTemplateDefinition;
28
28
  }
29
29
 
30
+ /**
31
+ * Derive a candidate's group slug from its colleague-candidates directory path.
32
+ *
33
+ * Candidates are grouped one level under `colleagues/`: a directory that holds a
34
+ * `workflow.py` directly (no slash in the id) is independent and returns null,
35
+ * while a nested path such as `news_desk/feed_curator` belongs to the
36
+ * `news_desk` group.
37
+ */
38
+ export const candidateGroupSlug = (
39
+ candidateId: string | null | undefined,
40
+ ): string | null => {
41
+ if (!candidateId) {
42
+ return null;
43
+ }
44
+ const separator = candidateId.indexOf("/");
45
+ return separator === -1 ? null : candidateId.slice(0, separator);
46
+ };
47
+
48
+ /**
49
+ * Turn an underscored group slug into a display label, e.g.
50
+ * `news_desk` → `News desk`.
51
+ */
52
+ export const formatCandidateGroupName = (slug: string): string => {
53
+ const spaced = slug.replace(/_/g, " ");
54
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
55
+ };
56
+
30
57
  const toStringArray = (value: unknown): string[] | undefined => {
31
58
  if (!Array.isArray(value)) {
32
59
  return undefined;
@@ -70,6 +97,7 @@ const buildCandidateWorkflow = (
70
97
  handle: preserveHandle,
71
98
  name: spec.name,
72
99
  description: spec.description,
100
+ candidateGroup: candidateGroupSlug(spec.candidateId),
73
101
  avatarEmoji: avatarId,
74
102
  createdAt: "2026-01-01T00:00:00Z",
75
103
  updatedAt: "2026-01-01T00:00:00Z",
@@ -34,6 +34,12 @@ export interface Workflow {
34
34
  id: string;
35
35
  handle?: string;
36
36
  teamId?: string | null;
37
+ /**
38
+ * Group slug for candidate colleagues, derived from the first path segment
39
+ * of the colleague-candidates directory (e.g. "news_desk"). Null when the
40
+ * candidate lives directly under `colleagues/` and is therefore independent.
41
+ */
42
+ candidateGroup?: string | null;
37
43
  name: string;
38
44
  description?: string;
39
45
  avatarEmoji?: string | null;
@@ -218,8 +218,7 @@ const parseWorkflowMetadata = (
218
218
  }
219
219
 
220
220
  const metadataRecord = metadata as Record<string, unknown>;
221
- const workflowMetadata =
222
- metadataRecord.workflow ?? metadataRecord.canvas;
221
+ const workflowMetadata = metadataRecord.workflow ?? metadataRecord.canvas;
223
222
  if (!workflowMetadata || typeof workflowMetadata !== "object") {
224
223
  return (
225
224
  resolveTemplateFallback() ?? {
@@ -232,7 +231,9 @@ const parseWorkflowMetadata = (
232
231
  }
233
232
 
234
233
  const workflowRecord = workflowMetadata as Record<string, unknown>;
235
- const snapshotPayload = workflowRecord.snapshot as WorkflowSnapshot | undefined;
234
+ const snapshotPayload = workflowRecord.snapshot as
235
+ | WorkflowSnapshot
236
+ | undefined;
236
237
  const summaryPayload = workflowRecord.summary as
237
238
  | WorkflowDiffResult["summary"]
238
239
  | undefined;
@@ -373,6 +374,7 @@ export const toStoredWorkflow = (
373
374
  lastRun: undefined,
374
375
  isArchived: workflow.is_archived,
375
376
  isPublic: workflow.is_public,
377
+ requireLogin: workflow.require_login,
376
378
  shareUrl: workflow.share_url ?? null,
377
379
  chatkitStartScreenPrompts: workflow.chatkit?.start_screen_prompts ?? null,
378
380
  chatkitSupportedModels: workflow.chatkit?.supported_models ?? null,
@@ -124,4 +124,8 @@ export const persistRunnableConfig = async (
124
124
  }),
125
125
  },
126
126
  );
127
+ // The cached workflow still holds the previous runnable config; drop it so the
128
+ // next fetch reflects the value we just persisted.
129
+ invalidateWorkflowCache(workflowId);
130
+ return targetVersion;
127
131
  };
@@ -249,6 +249,7 @@ export interface StoredWorkflow extends Workflow {
249
249
  draftAccess?: "personal" | "authenticated" | "workspace";
250
250
  isArchived?: boolean;
251
251
  isPublic?: boolean;
252
+ requireLogin?: boolean;
252
253
  shareUrl?: string | null;
253
254
  chatkitStartScreenPrompts?: ChatKitStartScreenPrompt[] | null;
254
255
  chatkitSupportedModels?: ChatKitSupportedModel[] | null;
@@ -4,6 +4,7 @@ import {
4
4
  AlertTriangle,
5
5
  Copy,
6
6
  ExternalLink,
7
+ LoaderCircle,
7
8
  Play,
8
9
  RefreshCw,
9
10
  UserMinus,
@@ -25,8 +26,15 @@ import {
25
26
  } from "@/design-system/ui/alert-dialog";
26
27
  import { Button } from "@/design-system/ui/button";
27
28
  import { Switch } from "@/design-system/ui/switch";
29
+ import {
30
+ Tooltip,
31
+ TooltipContent,
32
+ TooltipProvider,
33
+ TooltipTrigger,
34
+ } from "@/design-system/ui/tooltip";
28
35
  import { toast } from "@/hooks/use-toast";
29
36
  import { ConfirmDeleteWorkflowDialog } from "@features/workflow/components/dialogs/confirm-delete-workflow-dialog";
37
+ import { PublishWorkflowDialog } from "@features/workflow/components/dialogs/publish-workflow-dialog";
30
38
  import { UpdateWorkflowDialog } from "@features/workflow/components/dialogs/update-workflow-dialog";
31
39
  import { deleteWorkflow } from "@features/workflow/lib/workflow-storage";
32
40
  import {
@@ -57,10 +65,12 @@ export interface WorkflowTabContentProps {
57
65
  isLoading: boolean;
58
66
  loadError: string | null;
59
67
  isRunPending: boolean;
68
+ isRunning: boolean;
60
69
  onRunWorkflow: () => Promise<void>;
61
70
  onSaveConfig: (nextConfig: WorkflowRunnableConfig | null) => Promise<void>;
62
71
  hasCronTriggerNode: boolean;
63
72
  initialIsPublished: boolean;
73
+ initialRequireLogin: boolean;
64
74
  initialShareUrl: string | null;
65
75
  missingCredentials?: string[];
66
76
  }
@@ -186,10 +196,12 @@ export function WorkflowTabContent({
186
196
  isLoading,
187
197
  loadError,
188
198
  isRunPending,
199
+ isRunning,
189
200
  onRunWorkflow,
190
201
  onSaveConfig,
191
202
  hasCronTriggerNode,
192
203
  initialIsPublished,
204
+ initialRequireLogin,
193
205
  initialShareUrl,
194
206
  missingCredentials = [],
195
207
  }: WorkflowTabContentProps) {
@@ -198,9 +210,11 @@ export function WorkflowTabContent({
198
210
  const latestVersion = versions.at(-1);
199
211
  const [isConfigOpen, setIsConfigOpen] = useState(false);
200
212
  const [isPublished, setIsPublished] = useState(initialIsPublished);
213
+ const [requireLogin, setRequireLogin] = useState(initialRequireLogin);
201
214
  const [isScheduled, setIsScheduled] = useState(false);
202
215
  const [shareUrl, setShareUrl] = useState<string | null>(initialShareUrl);
203
216
  const [isPublishPending, setIsPublishPending] = useState(false);
217
+ const [isPublishDialogOpen, setIsPublishDialogOpen] = useState(false);
204
218
  const [isSchedulePending, setIsSchedulePending] = useState(false);
205
219
  const [diagramSvg, setDiagramSvg] = useState<string | null>(null);
206
220
  const [diagramError, setDiagramError] = useState<string | null>(null);
@@ -216,12 +230,14 @@ export function WorkflowTabContent({
216
230
  if (!workflowId) {
217
231
  setIsPublished(false);
218
232
  setIsScheduled(false);
233
+ setRequireLogin(false);
219
234
  setShareUrl(null);
220
235
  return;
221
236
  }
222
237
  setIsPublished(initialIsPublished);
238
+ setRequireLogin(initialRequireLogin);
223
239
  setShareUrl(initialShareUrl);
224
- }, [initialIsPublished, initialShareUrl, workflowId]);
240
+ }, [initialIsPublished, initialRequireLogin, initialShareUrl, workflowId]);
225
241
 
226
242
  useEffect(() => {
227
243
  if (!workflowId) {
@@ -375,6 +391,8 @@ export function WorkflowTabContent({
375
391
 
376
392
  const canConfigure = Boolean(workflowId);
377
393
  const canRun = Boolean(workflowId && latestVersion);
394
+ const runButtonDisabled = !canRun || isRunPending || isRunning;
395
+ const runButtonLabel = isRunPending || isRunning ? "Running..." : "Run";
378
396
  const latestConfig = latestVersion?.runnableConfig ?? null;
379
397
  const canToggleSchedule = hasCronTriggerNode || isScheduled;
380
398
 
@@ -409,33 +427,60 @@ export function WorkflowTabContent({
409
427
  return;
410
428
  }
411
429
 
430
+ if (nextValue) {
431
+ setIsPublishDialogOpen(true);
432
+ return;
433
+ }
434
+
412
435
  setIsPublishPending(true);
413
436
  try {
414
- if (nextValue) {
415
- const result = await publishWorkflow(workflowId, { actor: "studio" });
416
- setIsPublished(true);
417
- setShareUrl(result.shareUrl);
418
- toast({
419
- title: "Workflow published",
420
- description:
421
- result.message ??
422
- "Workflow is now public and available via its chat URL.",
423
- });
424
- } else {
425
- await unpublishWorkflow(workflowId, "studio");
426
- setIsPublished(false);
427
- setShareUrl(null);
428
- toast({
429
- title: "Workflow unpublished",
430
- description: "Workflow is now private.",
431
- });
432
- }
437
+ await unpublishWorkflow(workflowId, "studio");
438
+ setIsPublished(false);
439
+ setRequireLogin(false);
440
+ setShareUrl(null);
441
+ toast({
442
+ title: "Workflow unpublished",
443
+ description: "Workflow is now private.",
444
+ });
433
445
  } catch (error) {
434
- setIsPublished(!nextValue);
446
+ setIsPublished(true);
435
447
  toast({
436
- title: nextValue
437
- ? "Failed to publish workflow"
438
- : "Failed to unpublish workflow",
448
+ title: "Failed to unpublish workflow",
449
+ description: getErrorMessage(error, "Unable to update publish status."),
450
+ variant: "destructive",
451
+ });
452
+ } finally {
453
+ setIsPublishPending(false);
454
+ }
455
+ };
456
+
457
+ const handleConfirmPublish = async (requireLogin: boolean) => {
458
+ if (!workflowId) {
459
+ return;
460
+ }
461
+
462
+ setIsPublishPending(true);
463
+ try {
464
+ const result = await publishWorkflow(workflowId, {
465
+ actor: "studio",
466
+ requireLogin,
467
+ });
468
+ setIsPublished(true);
469
+ setRequireLogin(result.workflow.require_login);
470
+ setShareUrl(result.shareUrl);
471
+ setIsPublishDialogOpen(false);
472
+ toast({
473
+ title: "Workflow published",
474
+ description:
475
+ result.message ??
476
+ (requireLogin
477
+ ? "Workflow is now available to signed-in workspace members."
478
+ : "Workflow is now public and available via its chat URL."),
479
+ });
480
+ } catch (error) {
481
+ setIsPublished(false);
482
+ toast({
483
+ title: "Failed to publish workflow",
439
484
  description: getErrorMessage(error, "Unable to update publish status."),
440
485
  variant: "destructive",
441
486
  });
@@ -537,15 +582,31 @@ export function WorkflowTabContent({
537
582
  </p>
538
583
  </div>
539
584
  <div className="flex items-center gap-4">
540
- <div className="flex items-center gap-2">
541
- <span className="text-sm text-muted-foreground">Publish</span>
542
- <Switch
543
- aria-label="Publish workflow"
544
- checked={isPublished}
545
- onCheckedChange={(checked) => void handlePublishToggle(checked)}
546
- disabled={isPublishPending}
547
- />
548
- </div>
585
+ <TooltipProvider>
586
+ <Tooltip>
587
+ <TooltipTrigger asChild>
588
+ <div className="flex items-center gap-2">
589
+ <span className="text-sm text-muted-foreground">
590
+ Publish
591
+ </span>
592
+ <Switch
593
+ aria-label="Publish workflow"
594
+ checked={isPublished}
595
+ onCheckedChange={(checked) =>
596
+ void handlePublishToggle(checked)
597
+ }
598
+ disabled={isPublishPending}
599
+ />
600
+ </div>
601
+ </TooltipTrigger>
602
+ <TooltipContent className="max-w-xs">
603
+ Expose this workflow through a shareable chat URL. When
604
+ publishing, choose <strong>Public</strong> (anyone with the
605
+ link) or <strong>Workspace only</strong> (sign-in required for
606
+ members of this workspace).
607
+ </TooltipContent>
608
+ </Tooltip>
609
+ </TooltipProvider>
549
610
  <div className="flex items-center gap-2">
550
611
  <span className="text-sm text-muted-foreground">Schedule</span>
551
612
  <Switch
@@ -578,16 +639,23 @@ export function WorkflowTabContent({
578
639
  ) : null}
579
640
  <Button
580
641
  onClick={() => {
642
+ if (isRunPending || isRunning) {
643
+ return;
644
+ }
581
645
  if (hasMissingCredentials) {
582
646
  setIsMissingCredentialsDialogOpen(true);
583
647
  return;
584
648
  }
585
649
  void onRunWorkflow();
586
650
  }}
587
- disabled={!canRun || isRunPending}
651
+ disabled={runButtonDisabled}
588
652
  >
589
- <Play className="mr-1.5 h-4 w-4" />
590
- {isRunPending ? "Running..." : "Run"}
653
+ {isRunPending || isRunning ? (
654
+ <LoaderCircle className="mr-1.5 h-4 w-4 animate-spin" />
655
+ ) : (
656
+ <Play className="mr-1.5 h-4 w-4" />
657
+ )}
658
+ {runButtonLabel}
591
659
  </Button>
592
660
  <Button
593
661
  variant="outline"
@@ -599,11 +667,21 @@ export function WorkflowTabContent({
599
667
  </div>
600
668
  </div>
601
669
 
670
+ {(isRunPending || isRunning) && (
671
+ <div
672
+ role="status"
673
+ className="rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-sm text-primary"
674
+ >
675
+ Workflow run in progress. Check the latest record on the Trace tab
676
+ for live status.
677
+ </div>
678
+ )}
679
+
602
680
  {isPublished && shareUrl && (
603
681
  <div className="flex items-center justify-between rounded-md border border-border/60 bg-muted/20 px-3 py-2">
604
682
  <div className="min-w-0">
605
683
  <p className="text-xs uppercase tracking-wide text-muted-foreground">
606
- Public URL
684
+ {requireLogin ? "Members-only URL" : "Public URL"}
607
685
  </p>
608
686
  <a
609
687
  href={shareUrl}
@@ -728,6 +806,13 @@ export function WorkflowTabContent({
728
806
  />
729
807
  ) : null}
730
808
 
809
+ <PublishWorkflowDialog
810
+ open={isPublishDialogOpen}
811
+ isPending={isPublishPending}
812
+ onOpenChange={setIsPublishDialogOpen}
813
+ onConfirm={handleConfirmPublish}
814
+ />
815
+
731
816
  <AlertDialog
732
817
  open={isMissingCredentialsDialogOpen}
733
818
  onOpenChange={setIsMissingCredentialsDialogOpen}
@@ -83,12 +83,14 @@ export function buildWorkflowLayoutProps(
83
83
  isLoading: core.metadata.isWorkflowLoading,
84
84
  loadError: core.metadata.workflowLoadError,
85
85
  isRunPending: execution.isRunPending,
86
+ isRunning: core.execution.isRunning,
86
87
  onRunWorkflow: execution.handleRunPersistedWorkflow,
87
88
  onSaveConfig: resources.saver.handleSaveWorkflowConfig,
88
89
  hasCronTriggerNode: hasSchedulableCronTrigger(
89
90
  core.metadata.workflowVersions ?? [],
90
91
  ),
91
92
  initialIsPublished: core.metadata.isWorkflowPublic,
93
+ initialRequireLogin: core.metadata.workflowRequireLogin,
92
94
  initialShareUrl: core.metadata.workflowShareUrl,
93
95
  missingCredentials: resources.credentialReadiness.missingCredentials,
94
96
  };
@@ -18,6 +18,7 @@ export function useWorkflowLifecycle(
18
18
  setChatkitStartScreenPrompts: core.metadata.setChatkitStartScreenPrompts,
19
19
  setChatkitSupportedModels: core.metadata.setChatkitSupportedModels,
20
20
  setIsWorkflowPublic: core.metadata.setIsWorkflowPublic,
21
+ setWorkflowRequireLogin: core.metadata.setWorkflowRequireLogin,
21
22
  setWorkflowShareUrl: core.metadata.setWorkflowShareUrl,
22
23
  setIsWorkflowLoading: core.metadata.setIsWorkflowLoading,
23
24
  setWorkflowLoadError: core.metadata.setWorkflowLoadError,
@@ -28,6 +28,7 @@ interface UseWorkflowLoaderParams {
28
28
  SetStateAction<ChatKitSupportedModel[] | null>
29
29
  >;
30
30
  setIsWorkflowPublic: Dispatch<SetStateAction<boolean>>;
31
+ setWorkflowRequireLogin: Dispatch<SetStateAction<boolean>>;
31
32
  setWorkflowShareUrl: Dispatch<SetStateAction<string | null>>;
32
33
  setIsWorkflowLoading: Dispatch<SetStateAction<boolean>>;
33
34
  setWorkflowLoadError: Dispatch<SetStateAction<string | null>>;
@@ -46,6 +47,7 @@ export function useWorkflowLoader({
46
47
  setChatkitStartScreenPrompts,
47
48
  setChatkitSupportedModels,
48
49
  setIsWorkflowPublic,
50
+ setWorkflowRequireLogin,
49
51
  setWorkflowShareUrl,
50
52
  setIsWorkflowLoading,
51
53
  setWorkflowLoadError,
@@ -67,6 +69,7 @@ export function useWorkflowLoader({
67
69
  setChatkitStartScreenPrompts(null);
68
70
  setChatkitSupportedModels(null);
69
71
  setIsWorkflowPublic(false);
72
+ setWorkflowRequireLogin(false);
70
73
  setWorkflowShareUrl(null);
71
74
  setExecutions([]);
72
75
  setActiveExecutionId(null);
@@ -102,6 +105,7 @@ export function useWorkflowLoader({
102
105
  );
103
106
  setChatkitSupportedModels(persisted.chatkitSupportedModels ?? null);
104
107
  setIsWorkflowPublic(persisted.isPublic ?? false);
108
+ setWorkflowRequireLogin(persisted.requireLogin ?? false);
105
109
  setWorkflowShareUrl(persisted.shareUrl ?? null);
106
110
  setExecutions([]);
107
111
  setActiveExecutionId(null);
@@ -124,6 +128,7 @@ export function useWorkflowLoader({
124
128
  setChatkitStartScreenPrompts(null);
125
129
  setChatkitSupportedModels(null);
126
130
  setIsWorkflowPublic(false);
131
+ setWorkflowRequireLogin(false);
127
132
  setWorkflowShareUrl(null);
128
133
  setExecutions([]);
129
134
  setActiveExecutionId(null);
@@ -21,6 +21,7 @@ export function useWorkflowMetadataState() {
21
21
  null,
22
22
  );
23
23
  const [isWorkflowPublic, setIsWorkflowPublic] = useState(false);
24
+ const [workflowRequireLogin, setWorkflowRequireLogin] = useState(false);
24
25
  const [workflowShareUrl, setWorkflowShareUrl] = useState<string | null>(null);
25
26
  const [chatkitStartScreenPrompts, setChatkitStartScreenPrompts] = useState<
26
27
  ChatKitStartScreenPrompt[] | null
@@ -46,6 +47,8 @@ export function useWorkflowMetadataState() {
46
47
  setWorkflowLoadError,
47
48
  isWorkflowPublic,
48
49
  setIsWorkflowPublic,
50
+ workflowRequireLogin,
51
+ setWorkflowRequireLogin,
49
52
  workflowShareUrl,
50
53
  setWorkflowShareUrl,
51
54
  chatkitStartScreenPrompts,
@@ -58,11 +58,20 @@ export function useWorkflowSaver(
58
58
  }
59
59
 
60
60
  try {
61
- await persistRunnableConfig(
61
+ const targetVersion = await persistRunnableConfig(
62
62
  currentWorkflowId,
63
63
  "studio",
64
64
  runnableConfig,
65
65
  );
66
+ // Keep the in-memory versions in sync so reopening the config sheet
67
+ // shows the saved values without a page refresh.
68
+ setWorkflowVersions((versions) =>
69
+ versions.map((version) =>
70
+ version.versionNumber === targetVersion
71
+ ? { ...version, runnableConfig }
72
+ : version,
73
+ ),
74
+ );
66
75
  toast({
67
76
  title: "Workflow config saved",
68
77
  description: `Saved config for "${workflowName}".`,
@@ -76,7 +85,7 @@ export function useWorkflowSaver(
76
85
  });
77
86
  }
78
87
  },
79
- [currentWorkflowId, workflowName],
88
+ [currentWorkflowId, setWorkflowVersions, workflowName],
80
89
  );
81
90
 
82
91
  const handleSaveWorkflowDetails = useCallback(async () => {