diff --git a/frontend/src/lib/aiStore.ts b/frontend/src/lib/aiStore.ts index 60c39903f4..bd03f1baa4 100644 --- a/frontend/src/lib/aiStore.ts +++ b/frontend/src/lib/aiStore.ts @@ -77,13 +77,28 @@ function dedupeModels(models: AIProviderModel[]): AIProviderModel[] { }) } +// copilotInfo/copilotSessionModel are global, so concurrent loads (e.g. a fast +// session switch between workspaces) race: an earlier call resolving last would +// clobber the active workspace's config. Apply only the most recent call's +// result via a monotonic token — last invocation wins regardless of resolution +// order. init() is synchronous so its ordering already matches. +let loadCopilotToken = 0 +// The workspace copilotInfo currently reflects. A session send awaits this +// matching its committed workspace so getCurrentModel() can't read the previous +// workspace's provider/model while the scoped load is still in flight. +export const copilotWorkspace = writable(undefined) export async function loadCopilot(workspace: string) { + const token = ++loadCopilotToken workspaceAIClients.init(workspace) try { const info = await WorkspaceService.getCopilotInfo({ workspace }) + if (token !== loadCopilotToken) return setCopilotInfo(info) + copilotWorkspace.set(workspace) } catch (err) { + if (token !== loadCopilotToken) return setCopilotInfo({}) + copilotWorkspace.set(workspace) console.error('Could not get copilot info', err) } } diff --git a/frontend/src/lib/components/BreadcrumbSegment.svelte b/frontend/src/lib/components/BreadcrumbSegment.svelte index 8b18503f75..8a266e4fc4 100644 --- a/frontend/src/lib/components/BreadcrumbSegment.svelte +++ b/frontend/src/lib/components/BreadcrumbSegment.svelte @@ -26,6 +26,9 @@ close siblings. isCurrent?: boolean currentItem?: WorkspaceItem & { savedPath?: string } onPick: (item: WorkspaceItem) => void + /** Load the picker's items from this workspace (session editors pass their + * acting workspace); falls back to $workspaceStore inside the picker. */ + workspaceId?: string } let { @@ -37,7 +40,8 @@ close siblings. initialHighlight, isCurrent = false, currentItem, - onPick + onPick, + workspaceId }: Props = $props() let isOpen = $state(false) @@ -71,6 +75,12 @@ close siblings. >{:else}{label}{/if} {/snippet} {#snippet content()} - + {/snippet} diff --git a/frontend/src/lib/components/DiffEditor.svelte b/frontend/src/lib/components/DiffEditor.svelte index 59bc8a33a5..8ebfe1c23f 100644 --- a/frontend/src/lib/components/DiffEditor.svelte +++ b/frontend/src/lib/components/DiffEditor.svelte @@ -13,9 +13,7 @@ import EditorTheme from './EditorTheme.svelte' import Button from '$lib/components/common/button/Button.svelte' import { twMerge } from 'tailwind-merge' - import type { ButtonProp } from './diffEditorTypes' - - const SIDE_BY_SIDE_MIN_WIDTH = 700 + import { SIDE_BY_SIDE_MIN_WIDTH, type ButtonProp } from './diffEditorTypes' interface Props { open?: boolean @@ -30,6 +28,10 @@ buttons?: ButtonProp[] modifiedModel?: meditor.ITextModel | meditor.IEditorModel inlineDiff?: boolean + // Opt out of Monaco's auto-inline fallback (see useInlineViewWhenSpaceIsLimited + // below). Only set this when the consumer fully owns the inline/side-by-side + // decision; otherwise the default keeps Monaco's built-in narrow fallback. + disableAutoInline?: boolean } let { @@ -44,7 +46,8 @@ readOnly = false, buttons = [], modifiedModel, - inlineDiff = false + inlineDiff = false, + disableAutoInline = false }: Props = $props() let diffEditor: meditor.IStandaloneDiffEditor | undefined = $state(undefined) @@ -62,6 +65,12 @@ diffEditor = meditor.createDiffEditor(diffDivEl!, { automaticLayout, renderSideBySide: inlineDiff ? false : editorWidth >= SIDE_BY_SIDE_MIN_WIDTH, + // Monaco forces the inline view below renderSideBySideInlineBreakpoint (900px), + // overriding our SIDE_BY_SIDE_MIN_WIDTH gate. Consumers that fully own the + // inline/side-by-side decision (e.g. the diff drawer's toggle) opt out via + // disableAutoInline; everyone else keeps Monaco's auto-inline fallback so + // narrow panels (inline scripts, flow modules) stay readable in unified view. + useInlineViewWhenSpaceIsLimited: !disableAutoInline, originalEditable: false, readOnly, minimap: { diff --git a/frontend/src/lib/components/DropdownV2.svelte b/frontend/src/lib/components/DropdownV2.svelte index bf83d59886..3a12198188 100644 --- a/frontend/src/lib/components/DropdownV2.svelte +++ b/frontend/src/lib/components/DropdownV2.svelte @@ -19,7 +19,7 @@ import { twMerge } from 'tailwind-merge' import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte' import { untrack } from 'svelte' - import { fly } from 'svelte/transition' + import { placementFly } from '$lib/utils/placementFly' import { ButtonType } from './common/button/model' interface Props { @@ -187,7 +187,7 @@ use:melt={$menuEl} data-menu class="z-[6000] transition-all duration-100" - transition:fly={{ duration: enableFlyTransition ? 100 : 0, y: -16 }} + transition:placementFly={{ duration: enableFlyTransition ? 100 : 0, placement }} > {#if customMenu} {@render menu?.({ item, close, builders })} diff --git a/frontend/src/lib/components/EditorHeader.svelte b/frontend/src/lib/components/EditorHeader.svelte index 87a0c6e1da..7812e8a6a5 100644 --- a/frontend/src/lib/components/EditorHeader.svelte +++ b/frontend/src/lib/components/EditorHeader.svelte @@ -43,6 +43,10 @@ * inline. Breadcrumb navigation still works — only the rename UI is * gated. */ pathEditable?: boolean + /** Workspace whose items the breadcrumb picker lists. Session live + * editors pass their acting workspace so the picker isn't scoped to the + * navigation workspace; falls back to $workspaceStore in the picker. */ + workspaceId?: string } let { @@ -55,7 +59,8 @@ onBehalfOfEmail, penVisibility = 'hover', summaryEditable = true, - pathEditable = true + pathEditable = true, + workspaceId }: Props = $props() let pathPopoverOpen = $state(false) @@ -138,6 +143,7 @@ initialHighlight={kindKey(kind)} isCurrent={!segments} {currentItem} + {workspaceId} onPick={handlePickerSelect} /> {#if segments} @@ -152,6 +158,7 @@ : { kind: 'all', dir: segments.dirs[i - 1].fullPath }} initialHighlight={dKey} {currentItem} + {workspaceId} onPick={handlePickerSelect} /> {/each} @@ -165,6 +172,7 @@ initialHighlight={leafKey} isCurrent {currentItem} + {workspaceId} onPick={handlePickerSelect} /> {/if} diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index d4be7a3b1b..418b33b77f 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -139,10 +139,13 @@ onTestJob }: FlowBuilderProps = $props() - // Key the AutosaveIndicator watches. Falls back to this component's own - // draft key, so the full-page editor is unchanged; the sessions preview - // overrides both to the (forked) workspace + path its autosave saves under. - const indicatorWorkspace = $derived(autosaveWorkspace ?? $workspaceStore) + // The workspace this editor operates on: deploy, save-draft, trigger loading + // and the AutosaveIndicator all target it. Falls back to the global store, so + // the full-page editor is unchanged; the sessions preview overrides it to the + // session's (forked) workspace, so an embedded editor acts on the session's + // fork rather than the navigation workspace ($workspaceStore, which stays put). + // indicatorPath is the matching draft path. + const opWorkspace = $derived(autosaveWorkspace ?? $workspaceStore) const indicatorPath = $derived(autosavePath ?? liveEditorDraftStoragePath) let initialPathStore = writable(initialPath) @@ -237,7 +240,7 @@ try { if (initialPath && initialPath != '') { const flowVersion = await FlowService.getFlowLatestVersion({ - workspace: $workspaceStore!, + workspace: opWorkspace!, path: initialPath }) @@ -289,9 +292,9 @@ // failure: `flush` never rejects (postSave catches and routes errors // to the failures map), so the success branch fired regardless. export async function saveDraft(): Promise { - if (!$workspaceStore || !liveEditorDraftStoragePath) return + if (!opWorkspace || !liveEditorDraftStoragePath) return await UserDraftDbSyncer.flush({ - workspace: $workspaceStore, + workspace: opWorkspace, itemKind: 'flow', path: liveEditorDraftStoragePath }) @@ -339,7 +342,7 @@ } async function syncWithDeployed() { const flow = await FlowService.getFlowByPath({ - workspace: $workspaceStore!, + workspace: opWorkspace!, path: initialPath, withStarredInfo: true }) @@ -396,7 +399,7 @@ if (newFlow) { await FlowService.createFlow({ - workspace: $workspaceStore!, + workspace: opWorkspace!, requestBody: { path: $pathStore, summary: flow.summary ?? '', @@ -414,7 +417,7 @@ } }) await CaptureService.moveCapturesAndConfigs({ - workspace: $workspaceStore!, + workspace: opWorkspace!, path: fakeInitialPath, requestBody: { new_path: $pathStore @@ -424,7 +427,7 @@ if (triggersToDeploy) { await deployTriggers( triggersToDeploy, - $workspaceStore, + opWorkspace, !!$userStore?.is_admin || !!$userStore?.is_super_admin, usedTriggerKinds, $pathStore, @@ -435,7 +438,7 @@ if (triggersToDeploy) { await deployTriggers( triggersToDeploy, - $workspaceStore, + opWorkspace, !!$userStore?.is_admin || !!$userStore?.is_super_admin, usedTriggerKinds, initialPath @@ -443,7 +446,7 @@ } await FlowService.updateFlow({ - workspace: $workspaceStore!, + workspace: opWorkspace!, path: initialPath, requestBody: { path: $pathStore, @@ -465,7 +468,7 @@ // New/updated path now exists server-side — drop the autocomplete // cache so it shows up immediately instead of after the 60s TTL. - invalidateWorkspacePaths($workspaceStore!) + invalidateWorkspacePaths(opWorkspace!) const { draft_triggers: _, ...newSavedFlow } = flowStore.val as OpenFlow & { draft_triggers: Trigger[] @@ -505,8 +508,8 @@ const pathStore = writable(untrack(() => pathStoreInit) ?? initialPath) $effect(() => { - if (liveEditorDraftStoragePath === undefined || !$workspaceStore) return - const workspace = $workspaceStore + if (liveEditorDraftStoragePath === undefined || !opWorkspace) return + const workspace = opWorkspace UserDraft.setLiveEditorDraft({ workspace, itemKind: 'flow', @@ -561,7 +564,8 @@ modulesTestStates, outputPickerOpenFns, preserveOnBehalfOf, - savedOnBehalfOfEmail + savedOnBehalfOfEmail, + opWorkspace: () => opWorkspace }) // Set up NoteEditor context for note editing capabilities @@ -606,14 +610,14 @@ export async function loadTriggers() { if (initialPath == '') return $triggersCount = await FlowService.getTriggersCountOfFlow({ - workspace: $workspaceStore!, + workspace: opWorkspace!, path: initialPath }) // Initialize triggers using utility function await triggersState.fetchTriggers( triggersCount, - $workspaceStore, + opWorkspace, initialPath, true, $primaryScheduleStore, @@ -740,10 +744,10 @@ if ( !untrack(() => newFlow) && !isCloudHosted() && - editInForkAllowed($workspaceStore, $userWorkspaces) + editInForkAllowed(opWorkspace, $userWorkspaces) ) { dropdownItems.push({ - label: editInForkLabel($workspaceStore, $userWorkspaces), + label: editInForkLabel(opWorkspace, $userWorkspaces), onClick: () => window.open(buildForkEditUrl('flow', initialPath)) }) } @@ -980,7 +984,7 @@ selectedId && untrack(() => select(selectedId)) }) $effect.pre(() => { - initialPath && initialPath != '' && $workspaceStore && untrack(() => loadTriggers()) + initialPath && initialPath != '' && opWorkspace && untrack(() => loadTriggers()) }) $effect.pre(() => { const hasAiDiff = aiChatManager.flowAiChatHelpers?.hasPendingChanges() ?? false @@ -991,7 +995,7 @@ await stepHistoryLoader.loadIndividualStepsStates( flowStore.val as Flow, flowStateStore, - $workspaceStore!, + opWorkspace!, $initialPathStore, $pathStore ) @@ -1099,17 +1103,20 @@ bind:clientWidth={topbarWidth} class="justify-between flex flex-row items-center pl-2 pr-4 space-x-4 scrollbar-hidden overflow-x-auto max-h-12 h-full relative" > -
- onNavigate?.(item)} - /> - {#if indicatorWorkspace && indicatorPath !== undefined} +
+
+ onNavigate?.(item)} + /> +
+ {#if opWorkspace && indicatorPath !== undefined} {/if}
-
+
{#if $enterpriseLicense && !newFlow} {/if} @@ -1240,6 +1247,16 @@ aiChatOpen={aiChatManager.open} showFlowAiButton={!disableAi && customUi?.topBar?.aiBuilder != false} toggleAiChat={() => aiChatManager.toggleOpen()} + sessionOpen={$pathStore + ? { + target: { kind: 'flow', path: $pathStore }, + workspaceId: opWorkspace ?? undefined, + // Persist unsaved edits so the session preview + // (/flows/edit/) opens the flow exactly as it is in the + // editor right now. + beforeOpen: saveDraft + } + : undefined} onOpenPreview={flowPreviewButtons?.openPreview} localModuleStates={showJobStatus ? localModuleStates : {}} {showJobStatus} diff --git a/frontend/src/lib/components/FlowDiffViewer.svelte b/frontend/src/lib/components/FlowDiffViewer.svelte index 3539cbc5fe..56a15687a8 100644 --- a/frontend/src/lib/components/FlowDiffViewer.svelte +++ b/frontend/src/lib/components/FlowDiffViewer.svelte @@ -10,6 +10,8 @@ * FlowGraphDiffViewer show its own user-facing toggle (matches the * pre-fork-diff-drawer behavior). */ inlineDiff?: boolean + /** Forward Monaco's auto-inline opt-out to the YAML-mode DiffEditor. */ + disableAutoInline?: boolean /** Forwarded to FlowGraphDiffViewer — render an empty surface * placeholder for the "before" / "after" pane when the item is * added / removed. */ @@ -21,6 +23,7 @@ beforeYaml, afterYaml, inlineDiff = undefined, + disableAutoInline = false, beforeMissing = false, afterMissing = false }: Props = $props() @@ -46,6 +49,7 @@ defaultOriginal={beforeYaml} defaultModified={afterYaml} {inlineDiff} + {disableAutoInline} readOnly /> {/await} diff --git a/frontend/src/lib/components/PageHeader.svelte b/frontend/src/lib/components/PageHeader.svelte index 556ac1b0cc..9a927c52fa 100644 --- a/frontend/src/lib/components/PageHeader.svelte +++ b/frontend/src/lib/components/PageHeader.svelte @@ -2,12 +2,15 @@ import Tooltip from './Tooltip.svelte' interface Props { - title: string; - tooltip?: string; - documentationLink?: string | undefined; - primary?: boolean; - childrenWrapperDivClasses?: string; - children?: import('svelte').Snippet; + title: string + tooltip?: string + documentationLink?: string | undefined + primary?: boolean + childrenWrapperDivClasses?: string + // Inline actions rendered right after the title (e.g. a copy-id button), + // as opposed to `children` which lands on the far right of the header row. + titleActions?: import('svelte').Snippet + children?: import('svelte').Snippet } let { @@ -16,8 +19,9 @@ documentationLink = undefined, primary = true, childrenWrapperDivClasses = '', + titleActions, children - }: Props = $props(); + }: Props = $props()
@@ -31,6 +35,7 @@ {tooltip} {/if} + {@render titleActions?.()} {:else} @@ -40,6 +45,7 @@ {tooltip} {/if} + {@render titleActions?.()} {/if} diff --git a/frontend/src/lib/components/PrefixedInput.svelte b/frontend/src/lib/components/PrefixedInput.svelte index 2a1dccf386..adc578b545 100644 --- a/frontend/src/lib/components/PrefixedInput.svelte +++ b/frontend/src/lib/components/PrefixedInput.svelte @@ -1,139 +1,68 @@ - - - + +
+ + +
diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 7d639b2c75..5b86f7dbad 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -173,15 +173,24 @@ let topbarWidth = $state(0) const compactTopbar = $derived(topbarWidth > 0 && topbarWidth < 900) - // AutosaveIndicator watch key. Falls back to the full-page editor's - // global store + URL draft path; the sessions preview overrides both so the - // icon tracks the session's (forked) workspace + target path where autosave - // actually happens. - const indicatorWorkspace = $derived(autosaveWorkspace ?? $workspaceStore) + // The workspace this editor operates on: deploy, save-draft, trigger loading + // and the AutosaveIndicator all target it. Falls back to the full-page + // editor's global store; the sessions preview overrides it to the session's + // (forked) workspace, so an embedded editor acts on the session's fork rather + // than the navigation workspace ($workspaceStore, which stays put). indicatorPath + // is the matching draft path (URL path full-page, session target in preview). + const opWorkspace = $derived(autosaveWorkspace ?? $workspaceStore) const indicatorPath = $derived(autosavePath ?? userDraftPath) + // The shared `workerTags` store caches tags for the navigation workspace. A + // session editor deploys to `opWorkspace` (a fork), so it keeps a local list to + // gate/populate the tag picker without reading or clobbering the shared cache. + const usesLocalTags = $derived(opWorkspace != undefined && opWorkspace !== $workspaceStore) + let localWorkerTags = $state(undefined) + const scriptWorkerTags = $derived(usesLocalTags ? localWorkerTags : $workerTags) + function getCompactMenuItems(): Item[] { - const hasTags = ($workerTags?.length ?? 0) > 0 + const hasTags = (scriptWorkerTags?.length ?? 0) > 0 return [ ...(customUi?.topBar?.tagEdit != false && hasTags ? [ @@ -285,13 +294,13 @@ return } $triggersCount = await ScriptService.getTriggersCountOfScript({ - workspace: $workspaceStore!, + workspace: opWorkspace!, path: initialPath }) await triggersState.fetchTriggers( triggersCount, - $workspaceStore, + opWorkspace, initialPath, false, $primaryScheduleStore, @@ -443,7 +452,7 @@ } try { const templateScript = await PostgresTriggerService.getTemplateScript({ - workspace: $workspaceStore!, + workspace: opWorkspace!, id: templateId }) return templateScript @@ -497,7 +506,7 @@ if (initialPath && initialPath != '') { actual_parent_hash = ( await ScriptService.getScriptLatestVersion({ - workspace: $workspaceStore!, + workspace: opWorkspace!, path: initialPath }) )?.script_hash @@ -544,7 +553,7 @@ async function syncWithDeployed() { const latestScript = await ScriptService.getScriptByPath({ - workspace: $workspaceStore!, + workspace: opWorkspace!, path: initialPath, withStarredInfo: true }) @@ -608,7 +617,7 @@ } const newHash = await ScriptService.createScript({ - workspace: $workspaceStore!, + workspace: opWorkspace!, requestBody: { path: script.path, summary: script.summary, @@ -653,16 +662,16 @@ // New/updated path now exists server-side — drop the autocomplete // cache so it shows up immediately instead of after the 60s TTL. - invalidateWorkspacePaths($workspaceStore!) + invalidateWorkspacePaths(opWorkspace!) // Authoritative save-time schema-contract check (pipelines gap #2b): // warn-only, post-commit so a self-produced target resolves to the // content just deployed. Fire-and-forget — must never gate the deploy. - notifyContractWarnings($workspaceStore!, script.language, script.content) + notifyContractWarnings(opWorkspace!, script.language, script.content) if (!initialPath) { await CaptureService.moveCapturesAndConfigs({ - workspace: $workspaceStore!, + workspace: opWorkspace!, path: fakeInitialPath, requestBody: { new_path: script.path @@ -674,7 +683,7 @@ if (triggersToDeploy) { await deployTriggers( triggersToDeploy, - $workspaceStore, + opWorkspace, !!$userStore?.is_admin || !!$userStore?.is_super_admin, usedTriggerKinds, script.path, @@ -718,11 +727,11 @@ // syncer flushes. No toast — the AutosaveIndicator narrates the result, and // `flush` never rejects (postSave routes errors to the failures map). async function saveDraft(): Promise { - if (!$workspaceStore || !userDraftPath) return + if (!opWorkspace || !userDraftPath) return editor?.flushPendingChanges() await tick() await UserDraftDbSyncer.flush({ - workspace: $workspaceStore, + workspace: opWorkspace, itemKind: 'script', path: userDraftPath }) @@ -786,10 +795,10 @@ window.open(`/scripts/add?template=${initialPath}`) } }, - ...(!isCloudHosted() && editInForkAllowed($workspaceStore, $userWorkspaces) + ...(!isCloudHosted() && editInForkAllowed(opWorkspace, $userWorkspaces) ? [ { - label: editInForkLabel($workspaceStore, $userWorkspaces), + label: editInForkLabel(opWorkspace, $userWorkspaces), onClick: () => { window.open(buildForkEditUrl('script', initialPath)) } @@ -990,8 +999,12 @@ loadWorkerTags() async function loadWorkerTags() { - if (!$workerTags) { - $workerTags = await WorkerService.getCustomTagsForWorkspace({ workspace: $workspaceStore! }) + if (usesLocalTags) { + if (!localWorkerTags) { + localWorkerTags = await WorkerService.getCustomTagsForWorkspace({ workspace: opWorkspace! }) + } + } else if (!$workerTags) { + $workerTags = await WorkerService.getCustomTagsForWorkspace({ workspace: opWorkspace! }) } } @@ -1748,7 +1761,7 @@ /> {#if script.on_behalf_of_email && canPreserve} → { @@ -1906,13 +1919,14 @@ kind="script" summaryEditable={customUi?.topBar?.editableSummary != false} pathEditable={customUi?.topBar?.editablePath != false} + workspaceId={autosaveWorkspace} onNavigate={(item) => onNavigate?.(item)} />
{/if} - {#if indicatorWorkspace} + {#if opWorkspace} 0} + {#if scriptWorkerTags} + {#if scriptWorkerTags?.length ?? 0 > 0}
{/if} @@ -2056,6 +2071,15 @@
diff --git a/frontend/src/lib/components/WorkerTagPicker.svelte b/frontend/src/lib/components/WorkerTagPicker.svelte index 1b316a61f5..ccc3266ced 100644 --- a/frontend/src/lib/components/WorkerTagPicker.svelte +++ b/frontend/src/lib/components/WorkerTagPicker.svelte @@ -11,28 +11,53 @@ popupPlacement?: 'bottom-end' | 'top-end' disabled?: boolean placeholder?: string + // Workspace to read tags from; defaults to $workspaceStore. A fork-scoped + // session passes its effective workspace so the picker matches the deploy target. + workspaceId?: string } let { tag = $bindable(), popupPlacement = 'bottom-end', disabled = false, - placeholder + placeholder, + workspaceId = undefined }: Props = $props() + // See WorkerTagSelect: the shared `workerTags` cache is navigation-scoped, so a + // different target workspace reads/writes a local list to avoid clobbering it. + let effectiveWorkspace = $derived(workspaceId ?? $workspaceStore) + let usesLocal = $derived(workspaceId != undefined && workspaceId !== $workspaceStore) + let localWorkerTags = $state(undefined) + let currentTags = $derived(usesLocal ? localWorkerTags : $workerTags) + loadWorkerTags() async function loadWorkerTags(force = false) { - if (!$workerTags || force) { - $workerTags = await WorkerService.getCustomTagsForWorkspace({ workspace: $workspaceStore! }) + if (usesLocal) { + if (!localWorkerTags || force) { + localWorkerTags = await WorkerService.getCustomTagsForWorkspace({ + workspace: effectiveWorkspace! + }) + } + } else if (!$workerTags || force) { + $workerTags = await WorkerService.getCustomTagsForWorkspace({ + workspace: effectiveWorkspace! + }) } }
- {#if $workerTags} - {#if $workerTags?.length ?? 0 > 0} - + {#if currentTags} + {#if currentTags?.length ?? 0 > 0} + {:else}
No custom worker group tag defined on this instance in "Workers {'->'} Custom tags" @@ -56,7 +81,6 @@
{:else if kind === 'raw_app_file' && rawFile} @@ -119,6 +130,7 @@ doesn't reflow the parent. fullYamlOriginal={rawFile.fullYamlOriginal} fullYamlCurrent={rawFile.fullYamlCurrent} {inlineDiff} + {disableAutoInline} /> {:else if hasContent}
@@ -139,6 +151,7 @@ doesn't reflow the parent. defaultOriginal={original.content ?? ''} defaultModified={current.content ?? ''} {inlineDiff} + {disableAutoInline} readOnly /> {/await} @@ -154,6 +167,7 @@ doesn't reflow the parent. defaultOriginal={original.metadata} defaultModified={current.metadata} {inlineDiff} + {disableAutoInline} readOnly /> {/await} @@ -173,6 +187,7 @@ doesn't reflow the parent. defaultOriginal={original.metadata} defaultModified={current.metadata} {inlineDiff} + {disableAutoInline} readOnly />
diff --git a/frontend/src/lib/components/WorkspaceItemDrillPicker.svelte b/frontend/src/lib/components/WorkspaceItemDrillPicker.svelte index f9c538b50a..8ff680ef70 100644 --- a/frontend/src/lib/components/WorkspaceItemDrillPicker.svelte +++ b/frontend/src/lib/components/WorkspaceItemDrillPicker.svelte @@ -44,6 +44,10 @@ would be surprising. externalFilter?: string autoFocus?: boolean flush?: boolean + // Load items and drafts from this workspace instead of the navigation + // workspace. Set by session live editors, whose acting workspace can + // differ from $workspaceStore; falls back to $workspaceStore otherwise. + workspaceId?: string } let { @@ -54,9 +58,12 @@ would be surprising. currentItem, externalFilter, autoFocus = true, - flush = false + flush = false, + workspaceId }: Props = $props() + const effectiveWorkspace = $derived(workspaceId ?? $workspaceStore) + let inner = $state(undefined) export function focus() { @@ -70,7 +77,7 @@ would be surprising. } const loader = useWorkspaceItemsLoader( - () => $workspaceStore, + () => effectiveWorkspace, () => kinds ) @@ -86,7 +93,7 @@ would be surprising. // `listGlobalDrafts` is backend-backed (async); fetch once and derive the // per-kind lists synchronously from the resolved snapshot. const globalDraftsResource = resource( - () => ({ ws: $workspaceStore, enabled: isGlobalAiEnabled() }), + () => ({ ws: effectiveWorkspace, enabled: isGlobalAiEnabled() }), async ({ ws, enabled }) => (enabled && ws ? await listGlobalDrafts(ws) : []) ) function aiDraftsForKind(k: Kind): WorkspaceItem[] { diff --git a/frontend/src/lib/components/WorkspaceScopeTrigger.svelte b/frontend/src/lib/components/WorkspaceScopeTrigger.svelte new file mode 100644 index 0000000000..ac132bdca2 --- /dev/null +++ b/frontend/src/lib/components/WorkspaceScopeTrigger.svelte @@ -0,0 +1,205 @@ + + +{#snippet chipButton(grouped: boolean)} + +{/snippet} + +{#if menuItems?.length && !isCollapsed} +
+ {@render chipButton(true)} + + {#snippet buttonReplacement()} + + + + {/snippet} + +
+{:else} + {@render chipButton(false)} +{/if} diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index ed471e8262..84261bcf80 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -908,15 +908,22 @@ bind:clientWidth={topbarWidth} class="flex flex-row justify-between gap-2 gap-y-2 px-2 items-center overflow-y-visible overflow-x-auto max-h-12 h-12 shrink-0" > -
- (onNavigate ? onNavigate(item) : goto(editPathFor(item)))} - /> -
+ +
+
+ (onNavigate ? onNavigate(item) : goto(editPathFor(item)))} + /> +
+
{#if $app} {#if $mode !== 'preview'} -
+