From 6939454edb55a8df39b4fc5460f77333554f48a5 Mon Sep 17 00:00:00 2001 From: Guilhem Date: Mon, 26 Oct 2026 16:27:44 +0100 Subject: [PATCH] feat: name the previewed item in an empty AI session's composer (#12021) * feat: name the previewed item in an empty session's composer instead of greeting * feat: name the item's kind in the empty session's composer placeholder * fix: name the previewed item as its tab does, so view tabs get the summary --- .../components/sessions/SessionWrapper.svelte | 61 +++++++++++++++---- .../(root)/(logged)/sessions/+page.svelte | 11 +++- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/components/sessions/SessionWrapper.svelte b/frontend/src/lib/components/sessions/SessionWrapper.svelte index e51445deeb..fc128584ba 100644 --- a/frontend/src/lib/components/sessions/SessionWrapper.svelte +++ b/frontend/src/lib/components/sessions/SessionWrapper.svelte @@ -79,17 +79,23 @@ import { goto } from '$lib/navigation' import { splitterPointerCapture } from '$lib/utils/splitterPointerCapture' import { StarterPrompts } from '$lib/components/copilot/chat/starter/starterPrompts.svelte' + import { resolvePreviewTab } from './previewRouter' + import { whereIs } from './sessionPreviewTabs.svelte' + import type { SessionPreviewTab } from './sessionState.svelte' let { sessionId, headerInPage = false, - covered = false + covered = false, + previewTabName }: { sessionId: string /** True for the session the page is showing: its header rides the page header bar. */ headerInPage?: boolean /** True while the page holds this wrapper inert under an overlay, where nothing takes focus. */ covered?: boolean + /** The name the preview's tab strip shows for a tab, without its side marker. */ + previewTabName: (tab: SessionPreviewTab) => string } = $props() // Parent keys by sessionId; this wrapper only mounts when the session exists. @@ -117,8 +123,25 @@ // Whether the first message goes out in Use. Not `permissions.readOnly`: before that send a // staged fork's session reads the workspace it forks, while the message builds in the fork. const heldInUse = $derived(!!workModeLock.current || runtime?.manager.workMode === AIWorkMode.USE) + // The item an unsent session opened on ("Open in AI session", or a tab opened before the + // first send): the chat reads the displayed tab with each message, so the empty chat asks + // about that item instead of greeting. What is on screen only: a collapsed panel hides it. + // Named as its tab is, so a deployed item in View mode gets its summary from the listing. + const previewedItem = $derived.by(() => { + if (!opensSession) return undefined + const tab = runtime?.previewTabs.displayedTab + if (!tab) return undefined + const slot = resolvePreviewTab(whereIs(tab)) + if (slot.kind !== 'editor' && slot.kind !== 'viewer') return undefined + const kind = slot.kind === 'editor' ? slot.editorKind : slot.viewerKind + return { + kind: kind === 'raw_app' ? 'app' : kind, + name: previewTabName(tab) + } + }) + const greets = $derived(opensSession && !previewedItem) const starterPrompts = new StarterPrompts( - () => (opensSession ? (actingWorkspaceId ?? $workspaceStore ?? undefined) : undefined), + () => (greets ? (actingWorkspaceId ?? $workspaceStore ?? undefined) : undefined), () => !heldInUse ) @@ -402,6 +425,13 @@ // utilities on one element resolve by stylesheet order, not attribute order. const bannerClass = 'flex flex-row items-center justify-between gap-2 py-2 px-3 text-xs border rounded-md' + + const previewedItemPlaceholder = $derived( + previewedItem && + (heldInUse + ? `Ask about the ${previewedItem.kind} ${previewedItem.name}, or run it…` + : `Ask about the ${previewedItem.kind} ${previewedItem.name}, or describe a change…`) + ) {#if !session || !runtime} @@ -409,7 +439,12 @@ {:else} - {#snippet sessionEmptyHint()}{/snippet} + {#snippet sessionEmptyHint()} + {#if previewedItem} + +
+ {/if} + {/snippet} {#snippet inputPreface()} {#if showRecoveryNotice} @@ -583,14 +618,14 @@ onDraftChange={(text) => setSessionDraftPrompt(sessionId, text)} initialAttachments={armedAtInit ? undefined : getSessionDraftAttachments(sessionId)} onAttachmentsChange={(a) => setSessionDraftAttachments(sessionId, a)} - placeholder={composerPlaceholder} + placeholder={previewedItemPlaceholder ?? composerPlaceholder} forceDisabled={isUnavailable || !!session.archived} forceDisabledMessage={isUnavailable ? 'This session is linked to a workspace that no longer exists. Move it or discard it from the banner above to keep working.' : session.archived ? 'This session is archived. Unarchive it from the banner above to keep working.' : ''} - starter={opensSession + starter={greets ? { where: 'session', suggestions: starterPrompts.suggestions } : undefined} viewTransitionNames={headerInPage && (opensSession || arrivingFromHomeSend())} @@ -604,37 +639,37 @@ its transcript area, its column, and its footer row's height; for a session that has never sent, the greeting above the centred composer. -->
() => runtime.manager.setComposerHasDraft(PLACEHOLDER_DRAFT_KEY, false)} > - {#if opensSession} + {#if greets}
- {:else if runtime.manager.displayMessages.length > 0} + {:else if previewedItem || runtime.manager.displayMessages.length > 0}
{/if}
{@render inputPreface()} draftText, setPlaceholderText} - toolbar={opensSession ? placeholderToolbar : undefined} - placeholder={opensSession + toolbar={greets ? placeholderToolbar : undefined} + placeholder={greets ? heldInUse ? USE_STARTER_PLACEHOLDER : STARTER_PLACEHOLDER : runtime.manager.displayMessages.length > 0 ? 'Ask followup' - : composerPlaceholder} + : (previewedItemPlaceholder ?? composerPlaceholder)} disabled={isUnavailable || !!session.archived} autofocus={sessionState.currentSessionId === sessionId && !covered} onSelectionChange={(selection) => (handoff = selection ?? null)} onCompositionChange={(composing) => (placeholderComposing = composing)} /> - {#if !opensSession} + {#if !greets}
{:else if !isUnavailable && !session.archived && starterPrompts.suggestions.length > 0}
@@ -646,7 +681,7 @@
{/if}
- {#if opensSession} + {#if greets}
{/if}
diff --git a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte index ee1b95c765..19e7b60462 100644 --- a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte @@ -852,13 +852,16 @@ // cell reactively, so the editor mirrors the name onto the tab model). Before // that, the workspace listing names it. Falls back to the plain location label // for summary-less items and non-item pages. - function tabLabelFor(tab: SessionPreviewTab, workspace: string): string { + function tabNameFor(tab: SessionPreviewTab, workspace: string): string { const listed = listedItemFor(tab, workspace) - const name = + return ( tab.friendlyLabel ?? (listed && itemDisplayName(listed.path, listed.draftPath, listed.summary)) ?? previewLocationLabel(tab.loc) - return name + sideSuffix(tab.url) + ) + } + function tabLabelFor(tab: SessionPreviewTab, workspace: string): string { + return tabNameFor(tab, workspace) + sideSuffix(tab.url) } // Hover title for a tab. A summary label is free text the strip truncates, and @@ -1118,6 +1121,8 @@ sessionId={s.id} headerInPage={s.id === activeSession?.id} covered={aiHiddenVerdict !== false} + previewTabName={(tab) => + tabNameFor(tab, getEffectiveWorkspaceId(s) ?? $workspaceStore ?? '')} /> {/each}