diff --git a/frontend/src/lib/components/AutosaveIndicator.svelte b/frontend/src/lib/components/AutosaveIndicator.svelte index 1e318ed9b2..c6aa20ab48 100644 --- a/frontend/src/lib/components/AutosaveIndicator.svelte +++ b/frontend/src/lib/components/AutosaveIndicator.svelte @@ -25,7 +25,8 @@ // Count of OTHER users with a draft here. >0 makes the on-mount hint // take priority over "Loaded from draft" and offers "See others' drafts". othersDraftsCount = 0, - onOpenOthersDrafts + onOpenOthersDrafts, + compact = false }: { workspace: string itemKind: UserDraftItemKind @@ -35,6 +36,8 @@ loadedFromDraft?: boolean othersDraftsCount?: number onOpenOthersDrafts?: () => void + /** The icon alone, for a bar with no room for the label, which stays for screen readers. */ + compact?: boolean } = $props() // Reactive to the in-memory draft appearing/disappearing. @@ -365,7 +368,7 @@ {#if label} {label} diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index e951dca8f8..a919fc82f5 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -84,6 +84,7 @@ import { type TriggerContext, type ScheduleTrigger } from './triggers' import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes' import DeployButton from './DeployButton.svelte' + import OpenInSessionButton from './sessions/OpenInSessionButton.svelte' import { invalidateWorkspacePaths } from './PathNameAutocomplete.svelte' import type { Trigger } from './triggers/utils' import { deployTriggers, handleSelectTriggerFromKind } from './triggers/utils' @@ -908,6 +909,9 @@ // falling back to `$pathStore` in drawer mounts that carry no storage path. const sessionTargetPath = $derived(liveEditorDraftStoragePath || $pathStore) + const aiBuilderShown = $derived(!disableAi && customUi?.topBar?.aiBuilder != false) + const showFlowAiButton = $derived(aiBuilderShown && $roleCaps.flows !== 'compose') + const sessionOpen = $derived( sessionTargetPath ? { @@ -917,6 +921,7 @@ } : undefined ) + const headerSessionOpen = $derived(aiBuilderShown ? sessionOpen : undefined) // Reaches the AI entry point in a step's inline-editor toolbar, which the // recursive module wrapper sits too deep under to be handed a prop. `selected` @@ -1611,6 +1616,7 @@ : undefined }} actions={flowHeaderActions} + sessionSource={headerSessionOpen} actionFolds={2} contexts={headerContexts} separator="none" @@ -1793,6 +1799,7 @@ {@render previewButtons()} {/if} + await handleSaveFlow(detail)} {loading} @@ -1875,9 +1882,7 @@ {forceTestTab} {highlightArg} aiChatOpen={aiChatManager.open} - showFlowAiButton={!disableAi && - customUi?.topBar?.aiBuilder != false && - $roleCaps.flows !== 'compose'} + {showFlowAiButton} toggleAiChat={() => aiChatManager.toggleOpen()} {sessionOpen} onOpenPreview={flowPreviewButtons?.openPreview} diff --git a/frontend/src/lib/components/PageHeaderBar.svelte b/frontend/src/lib/components/PageHeaderBar.svelte index 1ad2602d8e..e7e32df604 100644 --- a/frontend/src/lib/components/PageHeaderBar.svelte +++ b/frontend/src/lib/components/PageHeaderBar.svelte @@ -33,6 +33,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba const content = $derived(pageHeader.content) const actions = $derived(pageHeader.actions) + const hasActions = $derived(actions.length > 0) const item = $derived(content?.item) const section = $derived(content?.section) @@ -82,8 +83,8 @@ The row's height matches the sidebar's own header row, so the two read as one ba trail: trailMeasures.trail, summary: BAR_STAGES.map(() => (hasSummary ? GAP + summaryFloorWidth : 0)), actions: - actions.length === 0 ? [0] : actionsRigid ? actionWidths : [GAP + FLEXIBLE_ACTIONS_RESERVE], - folds: actions.length > 0 && actionsRigid ? (content?.actionFolds ?? 0) : 0, + !hasActions ? [0] : actionsRigid ? actionWidths : [GAP + FLEXIBLE_ACTIONS_RESERVE], + folds: hasActions && actionsRigid ? (content?.actionFolds ?? 0) : 0, earlyFolds: content?.earlyActionFolds, summaryNamesItem, natural: { @@ -314,7 +315,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba {/if} - {#if actions.length > 0} + {#if hasActions} -
= 2 || hosted ? 'hidden' : 'contents'}> - RUNS_PATH + window.location.search, - workspaceId: $operatingWorkspace ?? undefined - }} - btnProps={{ unifiedSize: 'sm' }} - /> -
+ {#if !hosted} + + + {/if} {/snippet} {/if} diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 247382d6d4..3ee365be89 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -22,6 +22,10 @@ } from '$lib/script_helpers' import { isWorkflowAsCode } from './graph/wacToFlow' import AIFormSettings from './copilot/AIFormSettings.svelte' + import { SUPPORTED_CHAT_SCRIPT_LANGUAGES } from './copilot/chat/script/core' + import OpenInSessionButton, { + type OpenInSessionSource + } from './sessions/OpenInSessionButton.svelte' import { defaultScripts, enterpriseLicense, @@ -212,9 +216,13 @@ const compactTopbar = $derived( ownsPageHeader ? pageHeader.actionFold >= 1 : topbarWidth > 0 && topbarWidth < 900 ) - // The second fold holds the trail and one button. Diff and Settings join the menu that compact + // In the page header no fold drops a button's label: what does not fit goes into the menu. The + // second fold holds the trail and one button. Diff and Settings join the menu that compact // already shows, and Deploy stays out: it is what this page is for. const phoneTopbar = $derived(ownsPageHeader && pageHeader.actionFold >= 2) + // The third fold leaves the autosave indicator as its icon, so the AI session button still fits + // right before Deploy on a phone. + const narrowTopbar = $derived(ownsPageHeader && pageHeader.actionFold >= 3) // The workspace this editor operates on: deploy, save-draft, trigger loading // and the AutosaveIndicator all target it. Falls back to the full-page @@ -915,6 +923,24 @@ } } + const sessionOpen: OpenInSessionSource | undefined = $derived( + userDraftPath + ? { + // URL draft path the editor loads/saves by, not the friendly + // `script.path` (a new script's has no row → "not found"). + target: { kind: 'script', path: userDraftPath }, + workspaceId: opWorkspace ?? undefined, + beforeOpen: persistDraftForSession + } + : undefined + ) + // The header's button, gated like the code editor's AI entry point it stands in for. + function headerSessionOpen(): OpenInSessionSource | undefined { + if (disableAi || customUi?.editorBar?.aiGen == false) return undefined + if (!SUPPORTED_CHAT_SCRIPT_LANGUAGES.includes(script?.language ?? '')) return undefined + return sessionOpen + } + // Inside an AI session pane (which injects an aiChatManager via context) the // extra deploy-dropdown options — Deploy & Stay here, Fork, Edit in workspace // fork, Export — don't make sense: the session always stays put and is already @@ -2323,7 +2349,8 @@ }} afterName={scriptMarks} actions={scriptHeaderActions} - actionFolds={2} + sessionSource={headerSessionOpen()} + actionFolds={3} separator="none" /> {:else} @@ -2481,6 +2508,7 @@ {loadedFromDraft} {othersDraftsCount} {onOpenOthersDrafts} + compact={narrowTopbar} /> {/if} {/snippet} @@ -2510,7 +2538,7 @@ unifiedSize={headerBtnSize} on:click={() => (metadataOpen = true)} startIcon={{ icon: Settings }} - iconOnly={compactTopbar} + iconOnly={compactTopbar && !ownsPageHeader} title="Settings" > Settings @@ -2534,7 +2562,7 @@ on:click={() => openDiffDrawer()} disabled={diffDisabled} btnClasses={diffDisabled ? 'pointer-events-none' : undefined} - iconOnly={compactTopbar} + iconOnly={compactTopbar && !ownsPageHeader} title={diffTitle} startIcon={{ icon: DiffIcon }} > @@ -2580,6 +2608,9 @@ {@render settingsButton()} {/if} + {#if ownsPageHeader} + + {/if} + {#snippet fallback()}