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