diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte
index 97b871921c..10c73d882e 100644
--- a/frontend/src/lib/components/FlowBuilder.svelte
+++ b/frontend/src/lib/components/FlowBuilder.svelte
@@ -1083,10 +1083,11 @@
onClick: () => void
}> = []
- // In a session pane every one of these leaves the session (details page, new
- // tab), so the deploy button carries no dropdown there — as in ScriptBuilder.
- if (untrack(() => customUi).topBar?.extraDeployOptions != false && !inSessionPane) {
- if (!newFlow) {
+ if (untrack(() => customUi).topBar?.extraDeployOptions != false) {
+ // The one option that stays in a session pane: the session hosts the details page
+ // itself, so this flips the tab to it rather than leaving the session. Everything
+ // below opens a details page or a new tab outside the session, so it stays hidden.
+ if (!newFlow && savedFlow?.no_deployed !== true) {
dropdownItems.push({
label: 'Exit & see details',
// Use the deployed path, not the live `$pathStore` — the latter
@@ -1096,7 +1097,7 @@
})
}
- if (!untrack(() => newFlow)) {
+ if (!untrack(() => newFlow) && !inSessionPane) {
dropdownItems.push({
label: 'Fork',
onClick: () => window.open(`/flows/add?template=${initialPath}`)
@@ -1105,6 +1106,7 @@
if (
!untrack(() => newFlow) &&
+ !inSessionPane &&
!isCloudHosted() &&
editInForkAllowed(opWorkspace, $userWorkspaces)
) {
diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte
index 393744dbca..ea0d8c724a 100644
--- a/frontend/src/lib/components/ScriptBuilder.svelte
+++ b/frontend/src/lib/components/ScriptBuilder.svelte
@@ -806,9 +806,10 @@
// Inside an AI session pane (which injects an aiChatManager via context) the
// extra deploy-dropdown options — Deploy & Stay here, Fork, Edit in workspace
- // fork, Exit & See details, Export — don't make sense: the session always
- // stays put and is already scoped to a fork. Diff is exposed as a standalone
- // top-bar button (rendered independently of the session pane), not here.
+ // fork, Export — don't make sense: the session always stays put and is already
+ // scoped to a fork. `Exit & See details` is the exception: the session hosts the
+ // details page itself, so it switches the tab instead of leaving. Diff is exposed
+ // as a standalone top-bar button (rendered independently of the pane), not here.
const inSessionPane = !!getContext('aiChatManager')
async function openDiffDrawer() {
@@ -874,10 +875,7 @@
: [])
]
: []),
- ...(!inSessionPane &&
- savedScript?.no_deployed !== true &&
- script.kind === 'script' &&
- !script.auto_kind
+ ...(savedScript?.no_deployed !== true && script.kind === 'script' && !script.auto_kind
? [
{
label: 'Exit & See details',
diff --git a/frontend/src/lib/components/details/DetailPageHeader.svelte b/frontend/src/lib/components/details/DetailPageHeader.svelte
index 4f91f1800f..096a82c127 100644
--- a/frontend/src/lib/components/details/DetailPageHeader.svelte
+++ b/frontend/src/lib/components/details/DetailPageHeader.svelte
@@ -6,7 +6,6 @@
import { twMerge } from 'tailwind-merge'
import { userStore } from '$lib/stores'
import { createEventDispatcher, getContext, tick } from 'svelte'
- import { MediaQuery } from 'svelte/reactivity'
import SummaryPathDisplay from '$lib/components/SummaryPathDisplay.svelte'
import type { TriggerContext } from '../triggers'
import type { Item } from '$lib/utils'
@@ -49,6 +48,10 @@
onSaved?: (newPath: string) => void
children?: import('svelte').Snippet
trigger_badges?: import('svelte').Snippet
+ /** Whether the bar is wide enough to hold every button beside the summary, decided by
+ * the layout from its own width. Not a viewport media query: the same page also renders
+ * inside an AI session's preview panel, where the window is wide and the pane is not. */
+ wide?: boolean
}
let {
@@ -64,15 +67,13 @@
inheritedLabels = undefined,
onSaved,
children,
- trigger_badges
+ trigger_badges,
+ wide = true
}: Props = $props()
const dispatch = createEventDispatcher()
- // Tailwind's `lg`, matched in JS so the one ellipsis menu can carry the collapsed buttons.
- const wide = new MediaQuery('(min-width: 1024px)')
-
- const barButtons = $derived(wide.current ? mainButtons : mainButtons.filter((b) => !b.narrow))
+ const barButtons = $derived(wide ? mainButtons : mainButtons.filter((b) => !b.narrow))
function dropdownHost(btn: MainButton): MainButton | undefined {
if (typeof btn.narrow !== 'object') return undefined
@@ -90,7 +91,7 @@
}
const allMenuItems: Item[] = $derived([
- ...(wide.current ? [] : mainButtons.filter((b) => b.narrow && !dropdownHost(b))).map((b) => ({
+ ...(wide ? [] : mainButtons.filter((b) => b.narrow && !dropdownHost(b))).map((b) => ({
displayName: b.label,
description: b.description,
icon: b.buttonProps.startIcon,
@@ -99,7 +100,7 @@
disabled: b.buttonProps.disabled,
type: 'action' as const
})),
- ...(wide.current
+ ...(wide
? []
: [
{
@@ -114,12 +115,12 @@
icon: item.Icon,
action: item.onclick,
type: item.color === 'red' ? ('delete' as const) : ('action' as const),
- separatorTop: i === 0 && !wide.current
+ separatorTop: i === 0 && !wide
}))
])
function dropdownItemsOf(host: MainButton) {
- if (wide.current) return undefined
+ if (wide) return undefined
const items = mainButtons
.filter((b) => dropdownHost(b) === host)
.map((b) => ({
@@ -181,7 +182,7 @@
{/key}
{/if}
- {#if wide.current}
+ {#if wide}
/** `graphInline`: whether the form should carry the flow graph under it. It does in the
* split layout; the tabbed layout gives the graph a tab of its own. */
form?: import('svelte').Snippet<[{ graphInline: boolean }]>
@@ -70,7 +70,7 @@
{#if useDesktopLayout}