From 0a079469f436e7349e59d57dfea6a91f009be518 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Fri, 25 Sep 2026 13:44:07 +0200 Subject: [PATCH] feat(sessions): switch preview sides through Exit & see details / Edit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preview tab's side is now driven by the same affordances the full-page editor and detail page already carry, instead of a View|Edit toggle in the session header: `Exit & see details` in the deploy dropdown flips the tab to the deployed item, and the detail page's `Edit` flips it back. Both stay in the same tab, and neither leaves /sessions. `Exit & see details` is gated on `no_deployed !== true` in both builders, so a draft-only item offers no entry at all — that replaces the toggle's disabled state and its tooltip. `deployedExists` in SessionRuntime existed only to feed that tooltip and is gone. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CoC4MCs3BhVV1DyevrVc7N --- .../src/lib/components/FlowBuilder.svelte | 12 ++- .../src/lib/components/ScriptBuilder.svelte | 12 +-- .../details/DetailPageHeader.svelte | 23 ++--- .../details/DetailPageLayout.svelte | 6 +- .../lib/components/details/FlowDetail.svelte | 3 +- .../components/details/ScriptDetail.svelte | 3 +- .../components/sessions/FlowEditorView.svelte | 5 + .../components/sessions/ItemViewerView.svelte | 3 - .../components/sessions/PreviewTabHost.svelte | 16 ++++ .../sessions/ScriptEditorView.svelte | 5 + .../sessions/sessionRuntime.svelte.ts | 29 ------ .../(root)/(logged)/sessions/+page.svelte | 94 ++----------------- 12 files changed, 64 insertions(+), 147 deletions(-) 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}
- {@render header?.()} + {@render header?.({ wide: useDesktopLayout })}
@@ -101,7 +101,7 @@
{:else}
- {@render header?.()} + {@render header?.({ wide: useDesktopLayout })}
-
- switchSide(detail as PreviewItemMode)} - > - {#snippet children({ item })} - - - {/snippet} - -
- {/if} {#if !activeTabHasNoWorkspacePage} tabs && onTabLoad(tabs, tab, frame)} /> {/each}