From d0e14b05e1dbc4b99241308efab6eeda6d0b89be Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Tue, 6 Oct 2026 09:56:13 -0700 Subject: [PATCH] fix: a page keeps its path segment when it cannot offer a rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four things the local reviews caught in the rename work: A detail page handed the band a `pathTrigger` whether or not it could save one. Without `onSaved` — every script, flow or agent page the reader cannot write — `SummaryPathDisplay` fell through to its read-only branch, which ignores the `label` it was given and draws its own block: the trail lost its path and the summary appeared twice, two lines deep, in a 44px bar. The page now offers the trigger only where a rename can be saved, and the component renders what it was handed rather than its own row. The band gives up its own segment the moment a page offers a `pathTrigger`, so a trigger whose own gate is closed left the trail with no path at all — for the whole of an agent draft's load, not just a flush. Each gated snippet now draws the path itself in that case. `openFocus` named the path field as `#path`, which melt resolves with a document-wide query; `Path` hardcodes that id at every call site, and the popover is portalled to the body, so it lost document order to any other `Path` on the page. It is scoped to the popover's own field now. And `open` was an exported `$bindable(false)` on an optional prop, which the frontend's own rules ban. No caller bound it, so it is local state. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/components/FlowBuilder.svelte | 7 ++- .../src/lib/components/PageHeaderBar.svelte | 17 +++--- .../src/lib/components/PathEditPopover.svelte | 47 +++++++++------ .../src/lib/components/ScriptBuilder.svelte | 8 ++- .../lib/components/SummaryPathDisplay.svelte | 60 ++++++++++++------- .../details/DetailPageHeader.svelte | 9 ++- .../flows/content/AgentEditorModal.svelte | 7 ++- 7 files changed, 96 insertions(+), 59 deletions(-) diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 3929490d89..7f3e96de0d 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -1667,8 +1667,9 @@ path, with the cursor in the path field. `bind:` cannot be spread, so the slots are written out twice; both instances bind the same ones and only one is ever open. --> {#snippet flowPathTrigger(pathLabel: Snippet, triggerClass: string)} - + {#if flowStore?.val} + {:else} + {@render pathLabel()} {/if} {/snippet} diff --git a/frontend/src/lib/components/PageHeaderBar.svelte b/frontend/src/lib/components/PageHeaderBar.svelte index a0c73220e6..bcb1bc4a12 100644 --- a/frontend/src/lib/components/PageHeaderBar.svelte +++ b/frontend/src/lib/components/PageHeaderBar.svelte @@ -92,8 +92,8 @@ The row's height matches the sidebar's own header row, so the two read as one ba // wants: the trail truncating from its start. phone ? 'h-11 pl-1 pr-2' : 'h-11 pl-2 pr-4', // The filling surface is what meets the right edge, so the bar keeps no gutter of its own - // there — and it cannot wrap, since that surface is as tall as the bar. - content?.actionsFill && 'pr-0 flex-nowrap', + // there. + content?.actionsFill && 'pr-0', // The small left padding is there to sit a button against: the sidebar handle, or the // workspace disc, both of which carry their own visual margin. Embedded there is neither, // and the page's name would start as bare text 8px from the edge. @@ -132,14 +132,14 @@ The row's height matches the sidebar's own header row, so the two read as one ba workspace and the folders before it ever reaches the name of the thing the page is about. The 5rem floor is what is left when every name in it has gone — the workspace disc, the fork mark and the environment badge, which is as small as the part can be and still say - where the user is. Only below that does the bar wrap and give the trail a line of its own. + where the user is. An embed (`navHidden`) keeps the page's own name but drops the workspace part around it: the trail would offer to navigate the host's workspace, while the name is what says which page the controls beside it belong to. -->
- +
- + {#if item.summaryContent} console.error('page header: summary failed to render', e)}> diff --git a/frontend/src/lib/components/PathEditPopover.svelte b/frontend/src/lib/components/PathEditPopover.svelte index 95cc6551fc..bcf6106be0 100644 --- a/frontend/src/lib/components/PathEditPopover.svelte +++ b/frontend/src/lib/components/PathEditPopover.svelte @@ -59,8 +59,6 @@ follow. * click opens an editor here rather than going anywhere, and the fill is what says the * name is a control at all now that no pen sits beside it. */ triggerClass?: string - /** Bindable, for a host that drives the popover from elsewhere. */ - open?: boolean /** Which field takes the cursor on open. A popover anchored under the path opens on the * path, one under the summary opens on the summary: the click says which half of the name * the user came to change. `Path` autofocuses itself, so 'path' is the absence of a @@ -84,7 +82,6 @@ follow. penVisibility = 'hover', label, triggerClass = 'inline-flex items-center gap-1.5 min-w-0 max-w-full px-1 py-0.5 rounded cursor-pointer text-left hover:bg-surface-hover transition-colors', - open = $bindable(false), focusField = 'summary', snapshotPath = $bindable(), error = $bindable() @@ -98,10 +95,20 @@ follow. : 'Edit path' ) + // The popover's own state: a host opens this editor by clicking the name it handed over, so + // there is nothing for one to drive from outside. + let open = $state(false) + // Treat an empty path as ownable so the popover lets a user pick the path for a brand-new // item. `Path.reset()` then synthesizes a default under their own user/folder scope. const own = $derived(pathEditable && (!path || isOwner(path, actingUser, $operatingWorkspace))) + /** The path's name field inside this popover. `Path` hardcodes `id="path"`, and melt resolves a + * string `openFocus` with a document-wide `querySelector`, so a popover portalled to the body + * would hand the keystrokes to whichever other `Path` the page happens to have mounted. */ + const pathField = () => + document.querySelector('[data-path-edit-path] #path') ?? null + function setOpen(v: boolean) { open = v // Only snapshot when there is a path to freeze. A new flow or script opens with `path === ''` @@ -111,8 +118,8 @@ follow. } - + - +
+ +
{#if savedPath && path && path !== savedPath} {#snippet scriptPathTrigger(pathLabel: Snippet, triggerClass: string)} + the same flush that drops this editor, and this renders from the band's tree. The + `{:else}` is not decoration: the band hands the segment over the moment a page offers + a `pathTrigger`, so a gate that drew nothing would take the path off the trail. --> {#if script} + {:else} + {@render pathLabel()} {/if} {/snippet} diff --git a/frontend/src/lib/components/SummaryPathDisplay.svelte b/frontend/src/lib/components/SummaryPathDisplay.svelte index 7ad88ebee9..a634691d10 100644 --- a/frontend/src/lib/components/SummaryPathDisplay.svelte +++ b/frontend/src/lib/components/SummaryPathDisplay.svelte @@ -87,6 +87,12 @@ } }) + /** The path's name field inside this popover. `Path` hardcodes `id="path"`, and melt resolves a + * string `openFocus` with a document-wide `querySelector`, so a popover portalled to the body + * would hand the keystrokes to whichever other `Path` the page happens to have mounted. */ + const pathField = () => + document.querySelector('[data-path-edit-path] #path') ?? null + async function save(close: () => void) { const initialPath = path ?? '' const newPath = own ? editPath : initialPath @@ -111,8 +117,8 @@ {#snippet editor()} - + { summaryInput?.focus() return null @@ -182,16 +188,18 @@ {/if}
Path
- +
+ +
{/if}
@@ -279,6 +289,10 @@
+{:else if label} + + {@render label()} {:else}
{#if !emptyString(summary) && !compact} diff --git a/frontend/src/lib/components/details/DetailPageHeader.svelte b/frontend/src/lib/components/details/DetailPageHeader.svelte index a301deb768..34a7fab355 100644 --- a/frontend/src/lib/components/details/DetailPageHeader.svelte +++ b/frontend/src/lib/components/details/DetailPageHeader.svelte @@ -269,7 +269,14 @@ + throws there rather than where it was written. The `{:else}` carries the whole load of + that wait: the band gave up its own segment to this snippet, so without it the trail + would show no path until the draft arrives. --> {#if draft?.state} + {:else} + {@render pathLabel()} {/if} {/snippet}