show preview chip on write tools, not open_preview (#10261)

* fix(ai-sessions): show preview chip on write tools, not open_preview

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai-sessions): render preview chip label in UI font, not mono

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai-sessions): cast RowIcon to IconType for Button startIcon

RowIcon has a required `kind` prop, so it is not assignable to Button's
`IconType` (Component<{ size?: number }>). The `props` field carries the
runtime prop, so cast the icon to satisfy the type check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Guilhem
2026-07-22 14:35:22 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 Ruben Fiszel
parent 1da664fc89
commit c23f1880d2
5 changed files with 39 additions and 55 deletions
@@ -97,7 +97,7 @@
</button>
{/snippet}
<!-- Discrete preview chip for an item a tool created/updated/opened, pinned to
<!-- Discrete preview chip for an item a tool created/updated, pinned to
the right of the header row. Rendered inline (not gated on expand) so it
stays visible after the tool collapses. -->
{#if showPreviewChip && message.previewCard}
@@ -1,6 +1,8 @@
<script lang="ts">
import { PanelRight } from 'lucide-svelte'
import { Button } from '$lib/components/common'
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
import type { IconType } from '$lib/utils'
import { runToolDisplayAction } from './createdResourceActions.svelte'
import { openItemPreviewAction, type PreviewCardKind } from './shared'
@@ -10,12 +12,7 @@
let { card }: Props = $props()
// RowIcon has no 'pipeline' kind — a pipeline is a folder graph, shown with the
// data-pipeline icon.
const iconKind = $derived(card.kind === 'pipeline' ? 'data_pipeline' : card.kind)
const kindLabel = $derived(
card.kind === 'raw_app' ? 'app' : card.kind === 'pipeline' ? 'pipeline' : card.kind
)
const kindLabel = $derived(card.kind === 'raw_app' ? 'app' : card.kind)
let opening = $state(false)
async function open() {
@@ -29,17 +26,17 @@
}
</script>
<button
type="button"
onclick={open}
<Button
variant="default"
unifiedSize="2xs"
disabled={opening}
title="Open {kindLabel} preview: {card.path}"
class="group shrink-0 inline-flex items-center gap-1.5 rounded-md border border-light bg-surface pl-1.5 pr-2 py-1 transition-colors hover:bg-surface-hover disabled:opacity-60"
onClick={open}
startIcon={{ icon: RowIcon as unknown as IconType, props: { kind: card.kind, size: 12 } }}
endIcon={{ icon: PanelRight }}
wrapperClasses="shrink-0"
>
<span class="inline-flex shrink-0">
<RowIcon kind={iconKind} size={12} />
</span>
<span class="inline-flex items-center gap-1 text-2xs text-tertiary group-hover:text-secondary">
Preview <PanelRight size={11} />
</span>
</button>
<!-- The chip renders inside the tool row's font-mono scope; the label is UI text.
(font-main, not font-sans — this Tailwind config only defines main/mono.) -->
<span class="font-main">Preview</span>
</Button>
@@ -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 } })
@@ -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 }
}
@@ -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