diff --git a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte index be77af7e0a..8360ed962f 100644 --- a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte @@ -97,7 +97,7 @@ {/snippet} - {#if showPreviewChip && message.previewCard} diff --git a/frontend/src/lib/components/copilot/chat/ToolPreviewCard.svelte b/frontend/src/lib/components/copilot/chat/ToolPreviewCard.svelte index 79e37e90b7..81d00fd8e4 100644 --- a/frontend/src/lib/components/copilot/chat/ToolPreviewCard.svelte +++ b/frontend/src/lib/components/copilot/chat/ToolPreviewCard.svelte @@ -1,6 +1,8 @@ - + + Preview + diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index 447b98a525..0ddb11afe1 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -3266,18 +3266,7 @@ export const globalTools: Tool<{}>[] = [ ), fn: async (ctx) => { const parsed = openPreviewSchema.parse(ctx.args) - const sessionId = sessionIdFromCtx(ctx) - const { opened, message } = openSessionPreview(parsed, sessionId) - // Surface the same discrete card the write tools show, so the user can - // re-open/focus the preview later from the tool call. Only when the tool - // actually opened the preview (not for a refused open, e.g. a gated - // pipeline), and only in a session. - if (sessionId && opened) { - ctx.toolCallbacks.setToolStatus(ctx.toolId, { - previewCard: { kind: parsed.kind, path: parsed.path } - }) - } - return message + return openSessionPreview(parsed, sessionIdFromCtx(ctx)) } }, { @@ -3491,6 +3480,9 @@ type WriteDraftCtx = { // reloads the preview of the session that issued the deploy — not the // UI-active one. Undefined for the global side-panel chat. sessionId?: string + // Present when the ctx is the raw tool `fn` context — session chats carry + // their id here (see SessionToolHelpers / sessionIdFromCtx). + helpers?: unknown } // Sessions are the only context where `open_preview` makes sense — the global @@ -3566,25 +3558,18 @@ export function setOpenPreviewHandler(handler: OpenPreviewHandler | undefined): openPreviewHandler = handler } -// `opened` distinguishes a real open (the caller may then offer a preview card) -// from a refusal that is returned as a message rather than thrown — so the card is -// never shown for an item the tool declined to preview (e.g. a gated pipeline). function openSessionPreview( args: { kind: 'script' | 'flow' | 'raw_app' | 'pipeline'; path: string }, sessionId: string | undefined -): { opened: boolean; message: string } { +): string { if (!openPreviewHandler) { - return { - opened: false, - message: - 'Error: open_preview is only available inside an AI session. Tell the user to switch to a session to view the preview, or describe the item textually.' - } + return 'Error: open_preview is only available inside an AI session. Tell the user to switch to a session to view the preview, or describe the item textually.' } // open_preview only exists in sessions, so no sessionId check is needed here. if (args.kind === 'pipeline' && !isSessionPipelinesEnabled()) { - return { opened: false, message: SESSION_PIPELINES_GATED_MESSAGE } + return SESSION_PIPELINES_GATED_MESSAGE } - return { opened: true, message: openPreviewHandler({ ...args, sessionId }) } + return openPreviewHandler({ ...args, sessionId }) } // Opens a workspace *page* (Runs, Schedules, …) as a page tab in the session's @@ -3949,15 +3934,17 @@ const PREVIEW_CARD_KIND_BY_ITEM_KIND: Partial< } // Offer a preview card for a write that landed a previewable item. Session chats -// only (`ctx.sessionId`): the card opens the item in the side panel, which the -// global side-panel chat has no equivalent of. `path` is the item's display path -// (what `open_preview` takes), not its synthetic draft storage key. +// only: the card opens the item in the side panel, which the global side-panel +// chat has no equivalent of. `path` is the item's display path (what +// `open_preview` takes), not its synthetic draft storage key. function maybeAttachPreviewCard( ctx: WriteDraftCtx, itemKind: DraftPersistResult['itemKind'], path: string ): void { - if (!ctx.sessionId) return + // Write tools pass the raw tool ctx, whose session id lives in `helpers` — + // `ctx.sessionId` is only set by callers that thread it explicitly. + if (!ctx.sessionId && !sessionIdFromCtx(ctx)) return const kind = PREVIEW_CARD_KIND_BY_ITEM_KIND[itemKind] if (!kind) return ctx.toolCallbacks.setToolStatus(ctx.toolId, { previewCard: { kind, path } }) diff --git a/frontend/src/lib/components/copilot/chat/shared.ts b/frontend/src/lib/components/copilot/chat/shared.ts index ce2bfe0d02..53e72ab1c1 100644 --- a/frontend/src/lib/components/copilot/chat/shared.ts +++ b/frontend/src/lib/components/copilot/chat/shared.ts @@ -515,14 +515,14 @@ export type NavigateAction = { page: string } -/** Kinds of item a session preview can host — the subset the `open_preview` tool - * accepts. `pipeline` targets a folder graph, the rest a workspace item path. */ -export type PreviewCardKind = 'script' | 'flow' | 'raw_app' | 'pipeline' +/** Kinds of previewable item a write tool can land — the subset of draft item + * kinds a session preview can host. */ +export type PreviewCardKind = 'script' | 'flow' | 'raw_app' -// A discrete card shown under a tool call that created/updated/opened the preview of -// a workspace item. Clicking it opens the item's live preview in the session side -// panel — or focuses the tab if it is already open. The handler is registered by the -// sessions page (the only surface with a preview panel). +// A discrete card shown on a tool call that created or updated a workspace item. +// Clicking it opens the item's live preview in the session side panel — or focuses +// the tab if it is already open. The handler is registered by the sessions page +// (the only surface with a preview panel). export type OpenItemPreviewAction = { id: string type: 'open_item_preview' @@ -586,8 +586,8 @@ export type ToolDisplayMessage = { webSearchSources?: WebSearchSource[] /** Data URL of an image the tool produced (e.g. take_screenshot), shown on the card. */ imageUrl?: string - /** Workspace item this tool created/updated or opened a preview of. Rendered as a - * discrete, always-visible card that opens (or focuses) the item's preview in the + /** Workspace item this tool created or updated. Rendered as a discrete, + * always-visible card that opens (or focuses) the item's preview in the * session side panel. Set only for session chats — the side panel is their surface. */ previewCard?: { kind: PreviewCardKind; path: string } } diff --git a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte index b0be11a98f..225bbec8ed 100644 --- a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte @@ -463,7 +463,7 @@ } }) - // Preview cards under create/update/open-preview tool calls dispatch here. Open + // Preview cards on create/update tool calls dispatch here. Open // (or focus, if already shown) the item's preview in the active session's panel — // the visible chat is always the active session, so `owner` is its panel. Read // `owner` lazily inside the handler (not in the effect body) so this registers