diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 6c55398bb5..3929490d89 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -37,7 +37,7 @@ import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte' import AIChangesWarningModal from '$lib/components/copilot/chat/flow/AIChangesWarningModal.svelte' - import { getContext, onDestroy, setContext, untrack } from 'svelte' + import { getContext, onDestroy, setContext, untrack, type Snippet } from 'svelte' import PageHeaderContent from '$lib/components/PageHeaderContent.svelte' import PathEditPopover from '$lib/components/PathEditPopover.svelte' import { pageHeader, PHONE_BAR } from '$lib/components/pageHeaderRegistry.svelte' @@ -224,7 +224,7 @@ // Top-bar responsive collapse. Measured via bind:clientWidth — we can't // rely on viewport `md:` because the editor lives inside other panes // (session pane, drawer, etc.) where the viewport stays wide. - /** Held by the pen's popover while it is open; the band's trail reads it so the path does not + /** Held by the rename popover while it is open; the band's trail reads it so the path does not * reflow under the pointer as the user types. */ let pathSnapshot = $state(undefined) @@ -360,7 +360,7 @@ savedValue: savedFlow, modifiedValue: { ...flowStore.val, - // `$pathStore` is the live-edited path (the pen popover binds it). + // `$pathStore` is the live-edited path (the rename popover binds it). // `flowStore.val.path` doesn't track those edits, so without this the // rename wouldn't show up in the diff and the unsaved-changes warning // wouldn't fire when leaving with a pending rename. @@ -1578,10 +1578,11 @@ bar's edge would be a second one on top of it. --> {/if} + + {#snippet flowPathTrigger(pathLabel: Snippet, triggerClass: string)} + + {#if flowStore?.val} + + {/if} + {/snippet} + {#snippet flowSummary()} - -
- {emptyString(flowStore.val.summary) ? 'Add a summary...' : flowStore.val.summary} + +
+ {#snippet summaryText()} + {emptyString(flowStore.val.summary) + ? 'Add a summary...' + : flowStore.val.summary} + {/snippet} {#if customUi?.topBar?.editablePath != false || customUi?.topBar?.editableSummary != false} + {:else} + {@render summaryText()} {/if}
{/snippet} diff --git a/frontend/src/lib/components/NavBreadcrumb.svelte b/frontend/src/lib/components/NavBreadcrumb.svelte index 496a5c2ca4..d88b6101d4 100644 --- a/frontend/src/lib/components/NavBreadcrumb.svelte +++ b/frontend/src/lib/components/NavBreadcrumb.svelte @@ -3,8 +3,13 @@ The page header's breadcrumb: workspace / fork / kind / path, in one flat line. Every segment is a trigger for the picker that changes it — the workspace menu, the fork family -picker, and the item drill picker for the kind and each path level — and every segment wears the -same weight, size and icon size, so the line reads as one control rather than four. +picker, and the item drill picker for the kind and each path level — and all of them share one +size and one icon size, so the line reads as one control rather than four. + +Two weights, not one: the segment that ends the trail is what the page is about and carries the +weight, and everything leading to it is a step quieter. Which segment that is depends on the +route — the workspace on home, a list page's own name, an item's summary (which the band draws, +not this component) — so the lead can run the whole line. --> + open, setOpen} > {#snippet trigger()} - + {#if label} + + {@render label()} + {:else} + + {/if} {/snippet} {#snippet content()}
+ name lives. --> {#if summaryEditable}
{/if} + +{#snippet appPathTrigger(pathLabel: Snippet, triggerClass: string)} + +{/snippet} + {#snippet appSummary()} - -
- {emptyString($summary) ? 'Add a summary...' : $summary} + +
+ {#snippet summaryText()} + {emptyString($summary) ? 'Add a summary...' : $summary} + {/snippet} {/snippet} + +{#snippet pathTrigger(pathLabel: Snippet, triggerClass: string)} + +{/snippet} + {#snippet badges(size: 'sm' | 'md')} @@ -252,7 +269,7 @@ | undefined>(undefined) let versionDrawer: Drawer | undefined = $state(undefined) let settingsDrawer: Drawer | undefined = $state(undefined) - /** Held by the pen's popover while it is open; the band's trail reads it so the path does not + /** Held by the rename popover while it is open; the band's trail reads it so the path does not * reflow under the pointer as the user types. */ let pathSnapshot = $state(undefined) let evalsModal: AgentEvalsModal | undefined = $state(undefined) @@ -316,7 +316,11 @@ breadcrumb, the level it is on reads after that name, and its controls are the band's actions. --> + {#snippet agentPathTrigger(pathLabel: Snippet, triggerClass: string)} + + {#if draft?.state} + draft.state?.description ?? '', + (v) => { + if (draft.state) draft.state.description = v + } + } + bind:path={ + () => draft.state?.path ?? '', + (v) => { + if (draft.state) draft.state.path = v + } + } + bind:snapshotPath={pathSnapshot} + bind:error={() => host?.pathError(), (error) => host?.setPathError(error)} + savedPath={draft?.noDeployed ? undefined : target?.path} + kind="resource" + workspaceId={ws} + pathEditable={!readOnly} + summaryEditable={!readOnly} + /> + {/if} + {/snippet} + {#snippet agentSummary()} - -
+
{#if draft?.state} - {emptyString(draft.state.description) - ? 'Add a summary...' - : draft.state.description} + {#snippet summaryText()} + {emptyString(draft.state?.description) + ? 'Add a summary...' + : draft.state?.description} + {/snippet} draft.state?.description ?? '', (v) => { diff --git a/frontend/src/lib/components/pageHeaderRegistry.svelte.ts b/frontend/src/lib/components/pageHeaderRegistry.svelte.ts index 03a004769e..e499c77025 100644 --- a/frontend/src/lib/components/pageHeaderRegistry.svelte.ts +++ b/frontend/src/lib/components/pageHeaderRegistry.svelte.ts @@ -6,8 +6,14 @@ export type PageHeaderItem = { /** Absent for an item the picker does not file under a kind — an agent's detail page. The * breadcrumb then names the item without claiming it is one of the three. */ kind?: WorkspaceItemKind - /** Shown whole, and copied on click. */ + /** Shown whole. Clicking it opens `onEditName` where a page offers one, and copies otherwise. */ path: string | undefined + /** Wraps the band's own path segment in whatever the page wants behind it — its rename + * popover, anchored there rather than beside the summary, so the editor opens under the half + * of the name that was clicked. The band passes the path as it renders it and the classes its + * own segment wears, so the page's control keeps the breadcrumb's look rather than inventing + * one. Without this the segment copies the path on click. */ + pathTrigger?: Snippet<[Snippet, string]> summary?: string /** Rendered in place of the plain summary, for a page whose summary is itself a control — * a detail page's rename-and-labels popover, an editor's editable title. */ @@ -93,6 +99,24 @@ export const PHONE_BAR = 800 // which the sidebar and a page's side panel both take from. let barWidth = $state(0) +/** + * One registration's content, or undefined while it cannot answer. + * + * An entry is a getter closing over another component's props, evaluated here, in a tree of its + * own. Nothing orders a parent clearing those props against the child's teardown: a route that + * drops the `{#if}` around an editor and the value it binds into it does both in one flush, and + * this derived re-runs on the same flush, before the editor's own cleanup releases its entry. The + * entry has nothing to say in that window, which is not the same as the page having no header — + * so it is skipped, and whatever registered before it still stands. + */ +function read(e: { get: () => PageHeaderContent }): PageHeaderContent | undefined { + try { + return e.get() + } catch { + return undefined + } +} + export const pageHeader = { /** * The registrations merged, later ones winning field by field: a route sets the frame (where @@ -103,7 +127,8 @@ export const pageHeader = { if (entries.length === 0) return undefined const merged: PageHeaderContent = {} for (const e of entries) { - const c = e.get() + const c = read(e) + if (!c) continue if (c.item !== undefined) merged.item = c.item if (c.section !== undefined) merged.section = c.section if (c.afterName !== undefined) merged.afterName = c.afterName @@ -123,8 +148,8 @@ export const pageHeader = { get actions(): { render: Snippet; contexts?: Map }[] { return entries .flatMap((e, i) => { - const c = e.get() - return c.actions + const c = read(e) + return c?.actions ? [{ render: c.actions, contexts: c.contexts, order: c.actionsOrder ?? 0, i }] : [] }) diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index b3ceb4c565..c8e9106943 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -28,7 +28,7 @@ Undo, WandSparkles } from 'lucide-svelte' - import { onDestroy, untrack } from 'svelte' + import { onDestroy, untrack, type Snippet } from 'svelte' import { orderedJsonStringify, type Value, replaceFalseWithUndefined } from '../../utils' import { random_adj } from '$lib/components/random_positive_adjetive' @@ -340,7 +340,7 @@ // header uses. const headerBtnSize = $derived(condensedHeader || ownsPageHeader ? 'sm' : 'md') - /** Held by the pen's popover while it is open; the band's trail reads it so the path does not + /** Held by the rename popover while it is open; the band's trail reads it so the path does not * reflow under the pointer as the user types. */ let pathSnapshot = $state(undefined) @@ -947,10 +947,11 @@ // The path being edited, not the stored one: a brand-new app is parked at a // `draft_` placeholder, and the trail would name that instead of the path Deploy // will create — which is why `newEditedPath` refuses the placeholder. Frozen while the - // pen's popover is open so the trail holds still as the user types. + // rename popover is open so the trail holds still as the user types. kind: 'app', path: pathSnapshot ?? (newEditedPath || appPath || newPath || undefined), - summaryContent: appSummary + summaryContent: appSummary, + pathTrigger: rawAppPathTrigger }} actions={rawAppHeaderActions} contexts={headerContexts} @@ -1027,18 +1028,37 @@ {/if} {/snippet} + +{#snippet rawAppPathTrigger(pathLabel: Snippet, triggerClass: string)} + +{/snippet} + {#snippet appSummary()} - -
- {emptyString(summary) ? 'Add a summary...' : summary} + +
+ {#snippet summaryText()} + {emptyString(summary) ? 'Add a summary...' : summary} + {/snippet}