mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 08:05:44 +00:00
fix(frontend): open new script/flow/app in AI session (not-found + friendly tab) (#10028)
* fix(frontend): open new script/flow/app in AI session without "not found" "Open in AI session" on a never-deployed item opened the session preview against the friendly live-edited path (script.path / $pathStore) instead of the URL draft path the editor loads and saves by, so get-by-path 404'd. It also flushed only queued autosaves, so an untouched new item — which never triggered autosave — had no draft row to load at all. Target the URL draft path (userDraftPath / liveEditorDraftStoragePath; raw-app already used appPath), and add UserDraft.forcePersist to materialize a brand-new draft in beforeOpen, gated to never-deployed items where there is no deployed baseline to discard against. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): label a new session preview tab by its friendly name A never-deployed item's preview tab read draft_<uuid> instead of the typed/ auto name. The sessions page can't reactively read a runtime cell's state across reactive roots, so the live editor (SessionEditorTarget, handed the runtime as a prop) now stamps a transient friendlyLabel onto the tab model — which the page does observe — via a pure draftFriendlyLeaf helper. Unifies scripts, flows and raw apps through one path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address review nits on AI-session open - Flow drawer (FlowEditorDrawer) mounts FlowBuilder with no liveEditorDraftStoragePath, so gating the AI button solely on it hid the session entry point there; fall back to $pathStore (the pre-PR behavior for those deployed-flow drawers) while the main editor still prefers the URL draft path. - Clear a tab's stamped friendlyLabel when it is retargeted, so a draft tab's friendly name no longer lingers after navigating to a plain page. - Trim the repeated persist-hook comments to satisfy the AGENTS.md comment rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
co-authored by
Claude Opus 4.8
Ruben Fiszel
parent
6c521e9d87
commit
03535691d6
@@ -305,6 +305,17 @@
|
||||
})
|
||||
}
|
||||
|
||||
// Materialize a brand-new flow's draft before the session preview loads it by
|
||||
// path — an untouched new flow never autosaved, so forcePersist is the only
|
||||
// thing that creates the row. Gated to never-deployed: forcePersist skips the
|
||||
// discardIf baseline, safe only when there is none.
|
||||
async function persistDraftForSession(): Promise<void> {
|
||||
await saveDraft()
|
||||
if (opWorkspace && liveEditorDraftStoragePath && newFlow) {
|
||||
await UserDraft.forcePersist('flow', liveEditorDraftStoragePath, { workspace: opWorkspace })
|
||||
}
|
||||
}
|
||||
|
||||
export function computeUnlockedSteps(flow: Flow) {
|
||||
return Object.fromEntries(
|
||||
getAllModules(flow.value.modules, flow.value.failure_module)
|
||||
@@ -512,6 +523,11 @@
|
||||
const history = initHistory(untrack(() => flowStore).val)
|
||||
const pathStore = writable<string>(untrack(() => pathStoreInit) ?? initialPath)
|
||||
|
||||
// "Open in AI session" target: the URL draft path the editor loads/saves by
|
||||
// (which for a new flow differs from the live-edited friendly `$pathStore`),
|
||||
// falling back to `$pathStore` in drawer mounts that carry no storage path.
|
||||
const sessionTargetPath = $derived(liveEditorDraftStoragePath || $pathStore)
|
||||
|
||||
$effect(() => {
|
||||
if (liveEditorDraftStoragePath === undefined || !opWorkspace) return
|
||||
const workspace = opWorkspace
|
||||
@@ -1257,14 +1273,11 @@
|
||||
aiChatOpen={aiChatManager.open}
|
||||
showFlowAiButton={!disableAi && customUi?.topBar?.aiBuilder != false}
|
||||
toggleAiChat={() => aiChatManager.toggleOpen()}
|
||||
sessionOpen={$pathStore
|
||||
sessionOpen={sessionTargetPath
|
||||
? {
|
||||
target: { kind: 'flow', path: $pathStore },
|
||||
target: { kind: 'flow', path: sessionTargetPath },
|
||||
workspaceId: opWorkspace ?? undefined,
|
||||
// Persist unsaved edits so the session preview
|
||||
// (/flows/edit/<path>) opens the flow exactly as it is in the
|
||||
// editor right now.
|
||||
beforeOpen: saveDraft
|
||||
beforeOpen: persistDraftForSession
|
||||
}
|
||||
: undefined}
|
||||
onOpenPreview={flowPreviewButtons?.openPreview}
|
||||
|
||||
@@ -756,6 +756,17 @@
|
||||
})
|
||||
}
|
||||
|
||||
// Materialize a brand-new script's draft before the session preview loads it by
|
||||
// path — an untouched new script never autosaved, so forcePersist is the only
|
||||
// thing that creates the row. Gated to never-deployed: forcePersist skips the
|
||||
// discardIf baseline, safe only when there is none.
|
||||
async function persistDraftForSession(): Promise<void> {
|
||||
await saveDraft()
|
||||
if (opWorkspace && userDraftPath && savedScript?.no_deployed === true) {
|
||||
await UserDraft.forcePersist('script', userDraftPath, { workspace: opWorkspace })
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -2098,13 +2109,13 @@
|
||||
<ScriptEditor
|
||||
{disableAi}
|
||||
workspaceOverride={opWorkspace}
|
||||
sessionOpen={script.path
|
||||
sessionOpen={userDraftPath
|
||||
? {
|
||||
target: { kind: 'script', path: script.path },
|
||||
// URL draft path the editor loads/saves by, not the friendly
|
||||
// `script.path` (a new script's has no row → "not found").
|
||||
target: { kind: 'script', path: userDraftPath },
|
||||
workspaceId: opWorkspace ?? undefined,
|
||||
// Flush the per-user draft so the session preview opens the script
|
||||
// exactly as it is in the editor right now.
|
||||
beforeOpen: saveDraft
|
||||
beforeOpen: persistDraftForSession
|
||||
}
|
||||
: undefined}
|
||||
bind:selectedTab={selectedInputTab}
|
||||
|
||||
@@ -223,6 +223,22 @@
|
||||
const opWorkspace = $derived(autosaveWorkspace ?? $workspaceStore)
|
||||
const indicatorPath = $derived(autosavePath ?? liveEditorDraftStoragePath)
|
||||
|
||||
// Materialize a brand-new app's draft before the session preview loads it by
|
||||
// path — an untouched new app never autosaved, so forcePersist is the only
|
||||
// thing that creates the row (`appPath === indicatorPath` in the full-page
|
||||
// editor). Gated to never-deployed: forcePersist skips the discardIf baseline.
|
||||
async function persistDraftForSession(): Promise<void> {
|
||||
if (!opWorkspace || indicatorPath === undefined) return
|
||||
await UserDraftDbSyncer.flush({
|
||||
workspace: opWorkspace,
|
||||
itemKind: 'raw_app',
|
||||
path: indicatorPath
|
||||
})
|
||||
if (newApp) {
|
||||
await UserDraft.forcePersist('raw_app', indicatorPath, { workspace: opWorkspace })
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const typed = newEditedPath
|
||||
const baseline = savedApp?.path ?? ''
|
||||
@@ -898,16 +914,9 @@
|
||||
? {
|
||||
target: { kind: 'raw_app', path: appPath },
|
||||
workspaceId: opWorkspace ?? undefined,
|
||||
// Flush the autosaved draft so the session preview opens the app
|
||||
// exactly as it is in the editor right now.
|
||||
beforeOpen: () =>
|
||||
opWorkspace && indicatorPath !== undefined
|
||||
? UserDraftDbSyncer.flush({
|
||||
workspace: opWorkspace,
|
||||
itemKind: 'raw_app',
|
||||
path: indicatorPath
|
||||
})
|
||||
: undefined
|
||||
// Persist the draft (and materialize a brand-new one) so the session
|
||||
// preview opens the app exactly as it is in the editor right now.
|
||||
beforeOpen: persistDraftForSession
|
||||
}
|
||||
: undefined}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import type { SessionRuntime, SessionTargetKind } from './sessionRuntime.svelte'
|
||||
import { useUserDraftSync, type DraftSyncCodec } from './useUserDraftSync.svelte'
|
||||
import { makeFlowCodec, makeScriptCodec, makeRawAppCodec } from './sessionDraftCodecs'
|
||||
import { draftFriendlyLeaf } from './previewRouter'
|
||||
import SessionItemNotFound from './SessionItemNotFound.svelte'
|
||||
|
||||
let {
|
||||
@@ -106,6 +107,18 @@
|
||||
codec: () => codec
|
||||
})
|
||||
|
||||
// Stamp the tab's friendly label once this editor's cell knows the item's
|
||||
// typed/auto name. The page can't read the runtime cell reactively (it lives
|
||||
// outside the page's reactive root), but this editor — handed `runtime` as a
|
||||
// prop — can, so it mirrors the name onto the tab model the page does observe.
|
||||
// Only for a never-deployed item still parked at a `…/draft_<uuid>` storage
|
||||
// path; a deployed/real path keeps the plain location label.
|
||||
$effect(() => {
|
||||
const v = cell.store.val as { path?: string; draft_path?: string } | undefined
|
||||
const label = draftFriendlyLeaf(path, v?.draft_path ?? v?.path)
|
||||
runtime.previewTabs.setEditorFriendlyLabel({ kind, path }, label)
|
||||
})
|
||||
|
||||
// Debounced loading affordance for a breadcrumb swap: while the loaded path
|
||||
// lags the requested `path` (data not landed), keep the old editor visible
|
||||
// for ~150ms, then dim it under a spinner. Cleared the moment the load
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parsePreviewItemRoute, previewTabLabel, resolvePreviewTab } from './previewRouter'
|
||||
import { draftFriendlyLeaf, parsePreviewItemRoute, resolvePreviewTab } from './previewRouter'
|
||||
|
||||
describe('parsePreviewItemRoute', () => {
|
||||
it('maps edit/get routes to item kinds', () => {
|
||||
@@ -32,35 +32,24 @@ describe('parsePreviewItemRoute', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('previewTabLabel', () => {
|
||||
it('labels a new raw app by its pending friendly path, not the draft uuid', () => {
|
||||
const rawApp = { path: 'u/admin/draft_abc123', draft_path: 'u/admin/my_pretty_app' }
|
||||
expect(previewTabLabel('/apps_raw/edit/u/admin/draft_abc123', rawApp)).toBe('my_pretty_app')
|
||||
describe('draftFriendlyLeaf', () => {
|
||||
it('returns the friendly leaf for a new item parked at a draft uuid', () => {
|
||||
expect(draftFriendlyLeaf('u/admin/draft_abc123', 'u/admin/valuable_script')).toBe(
|
||||
'valuable_script'
|
||||
)
|
||||
expect(draftFriendlyLeaf('u/admin/draft_abc123', 'u/admin/my_flow')).toBe('my_flow')
|
||||
})
|
||||
|
||||
it('falls back to the uuid leaf when no friendly draft_path is pending', () => {
|
||||
const rawApp = { path: 'u/admin/draft_abc123' }
|
||||
expect(previewTabLabel('/apps_raw/edit/u/admin/draft_abc123', rawApp)).toBe('draft_abc123')
|
||||
it('returns undefined when no friendly path is available', () => {
|
||||
expect(draftFriendlyLeaf('u/admin/draft_abc123', undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps the real leaf for a raw app already at a named (non-draft) path', () => {
|
||||
const rawApp = { path: 'u/admin/my_app', draft_path: 'u/admin/renamed' }
|
||||
expect(previewTabLabel('/apps_raw/edit/u/admin/my_app', rawApp)).toBe('my_app')
|
||||
it('returns undefined when the friendly path is itself a draft placeholder', () => {
|
||||
expect(draftFriendlyLeaf('u/admin/draft_abc123', 'u/admin/draft_xyz')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('ignores a draft_path that belongs to a different raw app than the tab shows', () => {
|
||||
const rawApp = { path: 'u/admin/draft_other', draft_path: 'u/admin/friendly' }
|
||||
expect(previewTabLabel('/apps_raw/edit/u/admin/draft_abc123', rawApp)).toBe('draft_abc123')
|
||||
})
|
||||
|
||||
it('does not touch non-raw-app tabs', () => {
|
||||
const rawApp = { path: 'u/admin/draft_abc123', draft_path: 'u/admin/friendly' }
|
||||
expect(previewTabLabel('/scripts/edit/f/foo/bar', rawApp)).toBe('bar')
|
||||
expect(previewTabLabel('/runs', rawApp)).toBe('Runs')
|
||||
})
|
||||
|
||||
it('falls back to the plain location label when no raw app is loaded', () => {
|
||||
expect(previewTabLabel('/apps_raw/edit/u/admin/draft_abc123', undefined)).toBe('draft_abc123')
|
||||
it('returns undefined for an item already at a named (non-draft) storage path', () => {
|
||||
expect(draftFriendlyLeaf('u/admin/my_app', 'u/admin/renamed')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -110,27 +110,20 @@ export function previewLocationLabel(url: string): string {
|
||||
return stripBase(url)
|
||||
}
|
||||
|
||||
/** Tab label for a preview location, preferring the friendly path a raw-app
|
||||
* editor was renamed to while still parked at its throwaway `…/draft_<uuid>`
|
||||
* storage path. `rawAppDraft` is the session's live raw app (its storage `path`
|
||||
* plus the pending `draft_path` the user typed in the editor). When the tab
|
||||
* shows that app at a draft placeholder path, it's labelled by the friendly
|
||||
* leaf rather than the uuid. Display-only — the tab's URL keeps the storage
|
||||
* path. Falls back to `previewLocationLabel` for every other tab. */
|
||||
export function previewTabLabel(
|
||||
url: string,
|
||||
rawAppDraft?: { path: string; draft_path?: string }
|
||||
): string {
|
||||
const route = parsePreviewItemRoute(url)
|
||||
if (
|
||||
route?.raw_app &&
|
||||
rawAppDraft?.draft_path &&
|
||||
rawAppDraft.path === route.itemPath &&
|
||||
route.itemPath.split('/').pop()?.startsWith('draft_')
|
||||
) {
|
||||
return rawAppDraft.draft_path.split('/').pop() ?? rawAppDraft.draft_path
|
||||
}
|
||||
return previewLocationLabel(url)
|
||||
/** The friendly display leaf for a preview tab, or `undefined` to fall back to
|
||||
* `previewLocationLabel`. A never-deployed script / flow / raw app is parked at a
|
||||
* throwaway `…/draft_<uuid>` storage path while its editor shows a friendly name
|
||||
* (auto-generated or typed); pass that `friendlyPath` — the live cell's
|
||||
* `draft_path`/`path` — to label the tab by its leaf instead of the uuid. Returns
|
||||
* `undefined` for a deployed item (real storage path) or when the friendly path
|
||||
* is itself a placeholder. Display-only: the tab's URL keeps the storage path. */
|
||||
export function draftFriendlyLeaf(
|
||||
storagePath: string,
|
||||
friendlyPath: string | undefined
|
||||
): string | undefined {
|
||||
if (!storagePath.split('/').pop()?.startsWith('draft_')) return undefined
|
||||
const leaf = friendlyPath?.split('/').pop()
|
||||
return leaf && !leaf.startsWith('draft_') ? leaf : undefined
|
||||
}
|
||||
|
||||
export type PreviewItemRoute = { kind: WorkspaceItemKind; raw_app: boolean; itemPath: string }
|
||||
|
||||
@@ -48,6 +48,15 @@ function targetUrl(target: PreviewTarget): string {
|
||||
return target.type === 'page' ? target.href : `${base}${editPathFor(target.item)}`
|
||||
}
|
||||
|
||||
// Point a tab at a new destination. Clears `friendlyLabel` (bound to the previous
|
||||
// editor's item): a new editor re-stamps it, and navigating to a plain page must
|
||||
// drop the stale name so the tab falls back to the location label.
|
||||
function retargetTab(tab: SessionPreviewTab, url: string): void {
|
||||
tab.url = url
|
||||
tab.loc = url
|
||||
tab.friendlyLabel = undefined
|
||||
}
|
||||
|
||||
// Strip the query params the sessions preview injects into iframe URLs
|
||||
// (`nomenubar` to hide the nav, `workspace` to scope the page): they aren't part
|
||||
// of the canonical page URL. The observed `loc` must drop them to stay symmetric
|
||||
@@ -211,8 +220,7 @@ export class SessionPreviewTabs {
|
||||
const existing = this.#tabs.find((t) => parsePipelineRoute(t.url) !== null)
|
||||
if (existing) {
|
||||
const same = existing.url === url
|
||||
existing.url = url
|
||||
existing.loc = url
|
||||
retargetTab(existing, url)
|
||||
this.#activeId = existing.id
|
||||
this.#flush()
|
||||
return { status: same ? 'focused' : 'opened' }
|
||||
@@ -260,15 +268,13 @@ export class SessionPreviewTabs {
|
||||
if (pipelineFolder) {
|
||||
const existing = this.#tabs.find((x) => parsePipelineRoute(x.url) !== null)
|
||||
if (existing && existing.id !== t.id) {
|
||||
existing.url = url
|
||||
existing.loc = url
|
||||
retargetTab(existing, url)
|
||||
this.#activeId = existing.id
|
||||
this.#flush()
|
||||
return
|
||||
}
|
||||
}
|
||||
t.url = url
|
||||
t.loc = url
|
||||
retargetTab(t, url)
|
||||
this.#flush()
|
||||
}
|
||||
|
||||
@@ -335,6 +341,17 @@ export class SessionPreviewTabs {
|
||||
this.#flush()
|
||||
}
|
||||
|
||||
// Stamp the friendly display label for the editor tab hosting `target` (the
|
||||
// live editor knows the item's typed/auto name once its cell loads, which the
|
||||
// page can't read reactively from the runtime cell). Matched on the tab's
|
||||
// commanded `url` — the stable per-(kind,path) editor identity. Transient, so
|
||||
// no persist/flush: it's recomputed when the tab remounts.
|
||||
setEditorFriendlyLabel(target: SessionTarget, label: string | undefined): void {
|
||||
const t = this.#tabs.find((x) => isEditorTabFor(x.url, target))
|
||||
if (!t || t.friendlyLabel === label) return
|
||||
t.friendlyLabel = label
|
||||
}
|
||||
|
||||
// Persist a pending write immediately, cancelling the debounce. Called on
|
||||
// page hide — a mutation inside the debounce window would otherwise be lost
|
||||
// to a reload/navigation. No-op when nothing is pending.
|
||||
|
||||
@@ -290,6 +290,16 @@ describe('SessionPreviewTabs.navigate', () => {
|
||||
expect(o.activeId).toBe(tabId)
|
||||
expect(o.tabs[0].url).toBe(`${base}/pipeline/sales`)
|
||||
})
|
||||
|
||||
it('drops a stale friendly label when the tab is retargeted', () => {
|
||||
const o = owner()
|
||||
o.open(flowTarget)
|
||||
o.setEditorFriendlyLabel({ kind: 'flow', path: 'u/me/bar' }, 'luminous_flow')
|
||||
expect(o.tabs[0].friendlyLabel).toBe('luminous_flow')
|
||||
// Navigating the same tab to a plain page must clear the flow's name.
|
||||
o.navigate(pageTarget)
|
||||
expect(o.tabs[0].friendlyLabel).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SessionPreviewTabs.select / close / setCollapsed', () => {
|
||||
|
||||
@@ -124,7 +124,10 @@ export type Session = {
|
||||
|
||||
// One preview tab: `url` is the URL we command the iframe to load, `loc` the
|
||||
// last observed location (see the sessions page for the url/loc split).
|
||||
export type SessionPreviewTab = { id: string; url: string; loc: string }
|
||||
// `friendlyLabel` is a transient display override the live editor stamps for a
|
||||
// never-deployed item parked at `…/draft_<uuid>` (its typed/auto name); not
|
||||
// persisted (hydrate rebuilds tabs field-by-field), recomputed on next mount.
|
||||
export type SessionPreviewTab = { id: string; url: string; loc: string; friendlyLabel?: string }
|
||||
|
||||
// Sessions live in one per-user IndexedDB, one record per session in the
|
||||
// `sessions` store keyed by `id`. IndexedDB is the sole store — no localStorage
|
||||
|
||||
@@ -259,6 +259,38 @@ export const UserDraft = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Persist this key's CURRENT in-memory value to the server immediately,
|
||||
* bypassing autosave's change-detection. For "Open in AI session" on a
|
||||
* never-deployed item: it may have no draft row yet (an untouched new
|
||||
* script's template seed never triggers autosave), and the session preview
|
||||
* loads it via get-by-path, which 404s without a persisted draft. No-op
|
||||
* when no value is held for this key. Resolves once the POST lands.
|
||||
*
|
||||
* MUST NOT be a general save path: it skips the handle's `discardIf`
|
||||
* baseline check, so a value back at the deployed baseline would persist a
|
||||
* no-op draft instead of the delete autosave sends. Only safe for
|
||||
* never-deployed items, which have no baseline.
|
||||
*/
|
||||
async forcePersist(
|
||||
itemKind: UserDraftItemKind,
|
||||
path: string,
|
||||
opts?: UserDraftOptions
|
||||
): Promise<void> {
|
||||
const ws = resolveWorkspace(opts)
|
||||
const mk = mapKey(ws, itemKind, path)
|
||||
const entry = entries.get(mk)
|
||||
const val = entry ? entry.state.val : writtenCache.get(mk)?.val
|
||||
if (val === undefined) return
|
||||
await UserDraftDbSyncer.save({
|
||||
workspace: ws,
|
||||
itemKind,
|
||||
path,
|
||||
value: snapshotDraftValue(val),
|
||||
immediate: true
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Current draft value from the in-memory cell. `undefined` when no
|
||||
* editor has mounted a handle for this key in this tab.
|
||||
|
||||
@@ -39,8 +39,7 @@
|
||||
import {
|
||||
getOrCreateRuntime,
|
||||
getRuntime,
|
||||
listRuntimes,
|
||||
type SessionRuntime
|
||||
listRuntimes
|
||||
} from '$lib/components/sessions/sessionRuntime.svelte'
|
||||
import { markSessionSeen } from '$lib/components/sessions/sessionUnread.svelte'
|
||||
import { isGlobalAiEnabled } from '$lib/components/copilot/chat/global/gate'
|
||||
@@ -50,7 +49,7 @@
|
||||
matchPreviewPage,
|
||||
pageKey,
|
||||
parsePreviewItemRoute,
|
||||
previewTabLabel,
|
||||
previewLocationLabel,
|
||||
type PreviewTarget
|
||||
} from '$lib/components/sessions/previewRouter'
|
||||
import { toolReloadEffect, tabsToReload } from '$lib/components/sessions/previewReload'
|
||||
@@ -249,7 +248,7 @@
|
||||
// Adapt the session tab model to DraggableTabs items (labels derived from the
|
||||
// observed location; every tab closable, none pinned).
|
||||
const previewTabItems = $derived<TabItem[]>(
|
||||
(owner?.tabs ?? []).map((t) => ({ id: t.id, label: tabLabelFor(activeRuntime, t.loc) }))
|
||||
(owner?.tabs ?? []).map((t) => ({ id: t.id, label: tabLabelFor(t) }))
|
||||
)
|
||||
let newTabOpen = $state(false)
|
||||
// Separate open flag for the empty-state launcher: it can be mounted at the
|
||||
@@ -467,13 +466,12 @@
|
||||
owner?.navigate(target)
|
||||
}
|
||||
|
||||
// Short tab label. For a raw-app tab, feed its own per-path cell so the tab is
|
||||
// labelled by that app's pending draft path (a rename parked at `draft_<uuid>`),
|
||||
// scoped to the tab's own runtime rather than another session's.
|
||||
function tabLabelFor(rt: SessionRuntime | undefined, url: string): string {
|
||||
const route = parsePreviewItemRoute(url)
|
||||
const rawAppDraft = rt && route?.raw_app ? rt.rawAppCell(route.itemPath).store.val : undefined
|
||||
return previewTabLabel(url, rawAppDraft)
|
||||
// Short tab label. A never-deployed item parked at `…/draft_<uuid>` carries a
|
||||
// `friendlyLabel` its live editor stamped (the page can't read the runtime cell
|
||||
// reactively; the editor mirrors the typed/auto name onto the tab model). Falls
|
||||
// back to the plain location label for deployed items and non-item pages.
|
||||
function tabLabelFor(tab: SessionPreviewTab): string {
|
||||
return tab.friendlyLabel ?? previewLocationLabel(tab.loc)
|
||||
}
|
||||
|
||||
// A link click inside a live editor (e.g. a subflow reference) re-points the
|
||||
@@ -761,7 +759,7 @@
|
||||
runtime={rt}
|
||||
active={s.id === activeSession?.id && tab.id === tabs?.activeId}
|
||||
mounted={mountedTabKeys.has(tabKey(s.id, tab.id))}
|
||||
label={tabLabelFor(rt, tab.loc)}
|
||||
label={tabLabelFor(tab)}
|
||||
darkMode={isDarkMode.val}
|
||||
onNavigate={navigateEditorTo}
|
||||
onLoad={(frame) => tabs && onTabLoad(tabs, tab, frame)}
|
||||
|
||||
Reference in New Issue
Block a user