import { defineAction, fail } from "@agent-native/core/action"; import { buildDeepLink } from "@agent-native/core/server"; import { assertAccess } from "@agent-native/core/sharing"; import { getGenerationCreativeContext, mergeCreativeContextReuseLabels, recordGenerationCreativeContext, replaceCreativeContextElementProvenance, validateGenerationCreativeContext, } from "@agent-native/creative-context/server"; import type { CreativeContextElementProvenance, CreativeContextReuseLabel, } from "@agent-native/creative-context/types"; import { eq } from "drizzle-orm"; import { z } from "zod"; import { normalizeSlidePaddingForWrite } from "../app/lib/normalize-slide-padding.js"; import { getDb, schema } from "../server/db/index.js"; import { notifyClients } from "../server/handlers/decks.js"; import { deckDataColumns } from "../server/lib/deck-list-columns.js"; import { createDeckVersionSnapshot, deckVersionChangeGroupFromAction, deckVersionChatContextFromAction, } from "../server/lib/deck-versions.js"; import { applySlideContentEdits, formatSlideHtml, SlideContentEditSchema, type SlideContentEdit, } from "../server/lib/slide-content-patch.js"; import { HYGIENE_ACTION_DESCRIPTION, slideHygieneResult, } from "../server/lib/slide-hygiene.js"; import { generationAttemptIdOf, trackSlides, } from "../server/lib/slides-tracking.js"; import { assertSourceSlidePreserved, sourceImportForDeck, } from "../server/lib/source-import.js"; import { assertSlideAnimationsResolve, type RepointedReveal, } from "../server/lib/validate-slide-animations.js"; import { generationTimingFields } from "../shared/generation-timing.js"; import { createLayoutFitRevision, hashSlideContent, } from "../shared/slide-fit.js"; import { withUniqueSlideIds } from "../shared/slide-ids.js"; import { slideLabelFor, touchAgentSlidePresence } from "./_agent-presence.js"; import { getDeckUrl } from "./_app-url.js"; import { assertDeckWriteApplied, deckRevisionWhere, nextDeckRevision, retryDeckWrite, } from "./_deck-write.js"; import { assertNoRenderArtifacts } from "./_render-artifacts.js"; import { getCurrentRequestStateTabId, readAppStateForCurrentTab, } from "./_tab-state.js"; import { isAgentPatchCaller, withDeckLock } from "./patch-deck.js"; function deckDeepLink(deckId: string): string { return buildDeepLink({ app: "slides", view: "editor", params: { deckId }, }); } const reuseLabelSchema = z .object({ itemId: z.string().min(1).optional(), itemVersionId: z.string().min(1).optional(), kind: z.string().min(1), label: z.string().min(1), dataRole: z.literal("untrusted-reference").default("untrusted-reference"), elementId: z.string().min(1).optional(), influence: z .enum(["reused", "adapted", "reference-conditioned", "generated"]) .optional(), }) .superRefine((label, context) => { if (Boolean(label.itemId) !== Boolean(label.itemVersionId)) { context.addIssue({ code: "custom", message: "itemId and itemVersionId must be provided together", }); } if ( (label.influence ?? "reference-conditioned") !== "generated" && !label.itemId ) { context.addIssue({ code: "custom", message: "Only generated labels may omit context item ids", }); } }); function storedCreativeContext(value: unknown): { contextMode: "off" | "auto" | "pinned"; contextPackId: string | null; reuseLabels: CreativeContextReuseLabel[]; } | null { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const record = value as Record; if ( record.contextMode !== "off" && record.contextMode !== "auto" && record.contextMode !== "pinned" ) { return null; } return { contextMode: record.contextMode, contextPackId: typeof record.contextPackId === "string" ? record.contextPackId : null, reuseLabels: Array.isArray(record.reuseLabels) ? (record.reuseLabels as CreativeContextReuseLabel[]) : [], }; } const unresolvedPlaceholderPattern = /__[A-Za-z][A-Za-z0-9]*(?:[_ -][A-Za-z0-9]+)*__/g; function assertNoNewUnresolvedPlaceholders( previousContent: string, nextContent: string, ): void { const previous = new Set(previousContent.match(unresolvedPlaceholderPattern)); const introduced = [ ...new Set(nextContent.match(unresolvedPlaceholderPattern)), ].filter((marker) => !previous.has(marker)); if (introduced.length > 0) { fail( `Slide edit introduced unresolved placeholder content: ${introduced.join(", ")}. Re-read the slide and preserve the existing content instead of using markers as stand-ins.`, { errorCode: "slide_unresolved_placeholder" }, ); } } export default defineAction({ title: "Edit one Slides slide", description: 'Edit exactly one Slides slide. For a focused edit or translation of current or selected text, use one literal replace item in edits with the exact text and expectedMatches=1; when view-screen supplies an objectId for a selected element, use that objectId instead of find to replace only that element\'s inner content. The top-level objectId and replace fields are also supported as a compact alternative to edits. When view-screen already supplies the target, do not fetch the full deck, use fullContent, or wait for layout-fit. The exception is a verified layout overflow: call get-deck with slideId to read the complete HTML and contentHash, then make one fullContent repair with that baseContentHash. For a style request, get-deck\'s designSystem and deckStyle (also printed by view-screen) are authoritative; for anything beyond colors (spacing, element order, sizes) first read the representativeSlideId with a targeted get-deck and mirror its structure. Introduce colors or fonts the deck does not already use only when the user asks for them. Use targeted get-deck with slideId only if the selection text is missing, truncated, ambiguous, the literal match fails, or the edit changes markup or layout; pass that slide\'s exact contentHash as baseContentHash for fullContent and objectId writes. Use a supplied current slide hash for focused text edits when available. Use exactly one input mode: edits, legacy find/replace or objectId/replace, or fullContent. Mixed modes are rejected and write nothing. Prefer edits over fullContent so unrelated markup is not regenerated. For a style request, send edits that change only the requested CSS declarations, for example edits: [{"find":"...","replace":"...","occurrence":1}] — occurrence, not expectedMatches, because a CSS declaration often repeats on a slide and expectedMatches rejects that outright. To copy one slide\'s look onto others ("make every slide match slide 1"), read the reference slide and each target with get-deck (slideId, compact=false), change only the .fmd-slide wrapper\'s own background declaration, and leave interior card, image, and gradient fills alone unless the user asked for those too. A deck-wide restyle is one patch-deck call covering every slide, not one update-slide per slide; give each patch-slide the same find/replace edits instead of rewriting fields.content; reserve update-slide for one or a few targeted slides. Never use unresolved placeholder markers as stand-ins for preserved content. Content edits keep existing click-reveal steps, moving each to its target\'s new path; the result\'s repointedReveals lists every move as { slideId, animationIndex, animationId, fromPath, toPath }. A content edit that newly leaves a step unresolved, or leaves it unclear which element a kept step\'s target became, fails with a slide_animation_* error (400) and writes nothing; a step already unresolved before the edit does not block it. Use patch-deck with the complete animations list when a content edit intentionally changes both content and reveals. Source-imported slides preserve their original images and factual copy by default. The action returns immediately after persistence; layoutFit.status=pending means the open editor will measure the new content asynchronously. Finish all slide edits before calling get-layout-overflows once; after a repair, check once more. If it returns unknown, do not call again this turn unless the editor has produced a new measurement.' + HYGIENE_ACTION_DESCRIPTION, schema: z .object({ deckId: z.string().describe("Deck ID"), slideId: z.string().describe("Slide ID"), find: z .string() .optional() .describe("Text to find (for surgical search-replace edit)"), objectId: z .string() .min(1) .optional() .describe( "Stable data-slide-object-id for replacing the selected element's inner content", ), replace: z .string() .optional() .describe( "Replacement text; pass an empty string explicitly to clear it", ), notes: z .string() .optional() .describe( "Presenter-only speaker notes to save on this slide; may be sent alone or with a content edit", ), fullContent: z .string() .optional() .describe("Full HTML to replace entire slide content"), edits: z .array(SlideContentEditSchema) .min(1) .optional() .describe( "Ordered atomic edits against the current HTML. For one exact text replacement, use find and expectedMatches=1; for a selected element without exact selectedText, use its objectId to replace only the element's inner content. Each edit must match unless required=false.", ), baseContentHash: z .string() .min(1) .optional() .describe( "Hash returned by view-screen or get-deck for the exact slide source. Required for fullContent and objectId writes; any supplied hash rejects a stale source.", ), format: z .boolean() .optional() .default(false) .describe( "Format the resulting HTML with Prettier before persisting it. With edits or objectId the stored HTML is formatted first too, so find text should come from a get-deck format=true read; omit it otherwise. Not applied to fullContent.", ), preserveSource: z .boolean() .optional() .default(true) .describe( "Keep source-imported images and factual copy (default true). Set false only when the user explicitly asks to rewrite the source slide.", ), contextPackId: z .string() .optional() .describe( "Exact pack used for this edit; omit to inherit the deck pack.", ), contextModeOverride: z .literal("off") .optional() .describe( "Disable Creative Context for this edit only without changing the saved preference or deck pack.", ), reuseLabels: z .array(reuseLabelSchema) .optional() .default([]) .describe( "Exact context item versions that influenced this slide edit.", ), }) .superRefine((args, context) => { const elementEdit = args.edits?.some( (edit) => typeof (edit as { objectId?: unknown }).objectId === "string", ); if ( (args.fullContent !== undefined || args.objectId !== undefined || elementEdit) && args.baseContentHash === undefined ) { context.addIssue({ code: z.ZodIssueCode.custom, path: ["baseContentHash"], message: "This slide replacement requires the exact source contentHash", }); } }), http: { method: "POST" }, mcpAnnotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false, }, // Only with a source hash: a replay then fails slide_content_stale once the // first write changed the slide. Without one, an occurrence-based find can // re-apply to a different match. retrySafe: (input) => typeof input.baseContentHash === "string", run: async (args, ctx) => { const startedAt = Date.now(); const isAgentCaller = isAgentPatchCaller(ctx?.caller); const { deckId, slideId, find, objectId, replace, fullContent, edits, baseContentHash, format, preserveSource, contextPackId, contextModeOverride, reuseLabels, notes, } = args; const hasLegacyMode = find !== undefined || objectId !== undefined || replace !== undefined; const inputModeCount = Number(Boolean(edits)) + Number(hasLegacyMode) + Number(fullContent !== undefined); if (inputModeCount === 0 && notes === undefined) { fail( "One of --edits, --find/--replace, --objectId/--replace, --fullContent, or notes is required", { errorCode: "slide_edit_mode_required", }, ); } if (inputModeCount > 1) { fail( "Use exactly one input mode: --edits, --find/--replace, --objectId/--replace, or --fullContent; do not combine modes", { errorCode: "slide_edit_modes_conflict" }, ); } if (objectId !== undefined && find !== undefined) { fail("Use either --find or --objectId for a legacy text edit, not both", { errorCode: "slide_find_object_id_conflict", }); } if (objectId !== undefined && replace === undefined) { fail( "Legacy --objectId requires --replace; pass an empty string explicitly to clear the element", { errorCode: "slide_object_id_replace_required" }, ); } if (find !== undefined && replace === undefined) { fail( "Legacy --find requires --replace; pass an empty string explicitly to clear the match", { errorCode: "slide_find_replace_required" }, ); } if (replace !== undefined && find === undefined && objectId === undefined) { fail("Legacy --replace requires --find", { errorCode: "slide_replace_requires_find", }); } if (find !== undefined && find.length === 0) { fail("find must not be empty for legacy search/replace", { errorCode: "slide_find_empty", }); } const elementEdit = edits?.some( (edit) => typeof (edit as { objectId?: unknown }).objectId === "string", ); if ( (fullContent !== undefined || objectId !== undefined || elementEdit) && baseContentHash === undefined ) { fail( "This slide replacement requires the exact source contentHash. Read the slide with get-deck or use view-screen's matching hash, then retry.", { errorCode: "slide_content_hash_required", statusCode: 400 }, ); } await assertAccess("deck", deckId, "editor"); // An MCP call checks against the selection in the tab used last. if (await getCurrentRequestStateTabId()) { const currentSelection = await readAppStateForCurrentTab( "slides-selection", { fallbackToGlobal: false }, ); const currentSlideId = currentSelection?.deckId === deckId && typeof currentSelection.slideId === "string" ? currentSelection.slideId : null; const selectionItems = Array.isArray(currentSelection?.items) ? currentSelection.items.filter( (item): item is Record => typeof item === "object" && item !== null, ) : []; const selectedObjectIds = new Set( selectionItems.flatMap((item) => typeof item.objectId === "string" ? [item.objectId] : [], ), ); const selectedTexts = new Set( selectionItems.flatMap((item) => typeof item.selectedText === "string" ? [item.selectedText] : [], ), ); const usesCurrentSelection = (typeof objectId === "string" && selectedObjectIds.has(objectId)) || (typeof find === "string" && selectedTexts.has(find)) || (Array.isArray(edits) && edits.some((edit) => { if (!edit || typeof edit !== "object") return false; const candidate = edit as { find?: unknown; objectId?: unknown; }; return ( (typeof candidate.objectId === "string" && selectedObjectIds.has(candidate.objectId)) || (typeof candidate.find === "string" && selectedTexts.has(candidate.find)) ); })); if ( currentSlideId && currentSlideId !== slideId && usesCurrentSelection && baseContentHash === undefined ) { fail( `The selected Slides target is on slide ${currentSlideId}, but this edit targets ${slideId}. Re-read view-screen and use the selection slide ID; no write was made.`, { errorCode: "slide_target_not_current", statusCode: 409, details: { deckId, currentSlideId, requestedSlideId: slideId }, }, ); } } let firstAttemptContentHash: string | undefined; const applyEdit = async () => { const db = getDb(); const [row] = await db .select({ id: schema.decks.id, title: schema.decks.title, data: schema.decks.data, ownerEmail: schema.decks.ownerEmail, designSystemId: schema.decks.designSystemId, updatedAt: schema.decks.updatedAt, }) .from(schema.decks) .where(eq(schema.decks.id, deckId)) .limit(1); if (!row) { fail(`Deck ${deckId} not found`, { errorCode: "deck_not_found", statusCode: 404, }); } const deck = withUniqueSlideIds(deckId, JSON.parse(row.data)); const existingContext = storedCreativeContext(deck.creativeContext); if ( existingContext && contextPackId !== undefined && contextPackId !== existingContext.contextPackId ) { fail( "The slide edit must use the deck's existing creative-context pack", { errorCode: "creative_context_pack_mismatch", statusCode: 409 }, ); } // eslint-disable-next-line @typescript-eslint/no-explicit-any const slideIndex = Array.isArray(deck.slides) ? // eslint-disable-next-line @typescript-eslint/no-explicit-any deck.slides.findIndex((s: any) => s.id === slideId) : -1; const slide = slideIndex >= 0 ? deck.slides[slideIndex] : undefined; if (!slide) { fail(`Slide ${slideId} not found in deck ${deckId}`, { errorCode: "slide_not_found", statusCode: 404, }); } const currentContentHash = hashSlideContent(String(slide.content ?? "")); if ( (baseContentHash !== undefined && currentContentHash !== baseContentHash) || (firstAttemptContentHash !== undefined && currentContentHash !== firstAttemptContentHash) ) { fail( "Slide content changed since it was read. Call get-deck with this slideId for the exact current HTML, rebase the patch on it, and retry with details.currentContentHash as baseContentHash. details.currentContent is only a preview: it is omitted when large and may have credential-like text redacted, so never write from it.", { errorCode: "slide_content_stale", statusCode: 409, details: { deckId, slideId, currentContent: String(slide.content ?? ""), currentContentHash, }, }, ); } firstAttemptContentHash ??= currentContentHash; let applied = false; let notFound = false; let editResults: string[] | undefined; const previousContent = String(slide.content ?? ""); const validateNextContent = (nextContent: string) => { assertNoRenderArtifacts(previousContent, nextContent, slideId); assertNoNewUnresolvedPlaceholders(previousContent, nextContent); assertSourceSlidePreserved({ metadata: sourceImportForDeck(deck.sourceImport), slideId, nextContent, preserveSource, }); }; if (fullContent !== undefined) { const nextContent = normalizeSlidePaddingForWrite( previousContent, fullContent, ); validateNextContent(nextContent); slide.content = nextContent; applied = nextContent !== previousContent; } else if (edits) { const sourceContent = format ? await formatSlideHtml(previousContent) : previousContent; const patched = await applySlideContentEdits( sourceContent, edits as SlideContentEdit[], format, ); const nextContent = patched.content; validateNextContent(nextContent); slide.content = nextContent; applied = patched.changed; editResults = patched.applied; if (!applied) slide.content = previousContent; } else if (objectId !== undefined) { const sourceContent = format ? await formatSlideHtml(previousContent) : previousContent; const patched = await applySlideContentEdits( sourceContent, [{ objectId, replace: replace! }], format, ); const nextContent = normalizeSlidePaddingForWrite( previousContent, patched.content, ); validateNextContent(nextContent); slide.content = nextContent; applied = patched.changed; editResults = patched.applied; if (!applied) slide.content = previousContent; } else if (find !== undefined) { const idx = previousContent.indexOf(find); if (idx === -1) { notFound = true; } else { const nextContent = previousContent.slice(0, idx) + (replace ?? "") + previousContent.slice(idx + find.length); validateNextContent(nextContent); slide.content = nextContent; applied = nextContent !== previousContent; } } const contentApplied = applied; if (contentApplied) { slide.layoutFitRevision = createLayoutFitRevision(); if (isAgentCaller) delete slide.layoutWarningDismissed; } if (!notFound && notes !== undefined && notes !== slide.notes) { slide.notes = notes; applied = true; } // A content edit keeps the reveals. One it newly strands fails the edit and // is never dropped silently; a reveal already unresolved does not block it. let repointedReveals: RepointedReveal[] = []; if (applied && Array.isArray(slide.animations)) { const saved = assertSlideAnimationsResolve({ slideId, content: String(slide.content ?? ""), animations: slide.animations, baseline: { content: previousContent, animations: slide.animations }, followContent: true, }); slide.animations = saved.animations; repointedReveals = saved.repointed; } if (applied) { const shouldResolveCreativeContext = Boolean(existingContext) || contextPackId !== undefined || contextModeOverride !== undefined || reuseLabels.length > 0; let creativeContext: | { contextMode: "off" | "auto" | "pinned"; contextPackId: string | null; reuseLabels: CreativeContextReuseLabel[]; mergedReuseLabels: CreativeContextReuseLabel[]; elementProvenance: CreativeContextElementProvenance[]; } | undefined; if (contentApplied && shouldResolveCreativeContext) { const effectivePackId = contextPackId ?? existingContext?.contextPackId; const requestedLabels: CreativeContextReuseLabel[] = reuseLabels.length ? reuseLabels : [ { kind: "slide", label: "Net-new slide edit", dataRole: "untrusted-reference", elementId: slideId, influence: "generated", }, ]; const validated = await validateGenerationCreativeContext({ contextPackId: effectivePackId, contextPackSource: contextPackId === undefined ? "inherited" : "explicit", contextModeOverride, reuseLabels: requestedLabels, reuseLabelsSource: reuseLabels.length ? "explicit" : "inherited", }); const contextMode = validated.contextMode === "off" ? "off" : (existingContext?.contextMode ?? validated.contextMode); const slideReuseLabels = validated.reuseLabels.map((label) => ({ ...label, elementId: slideId, })); const mergedReuseLabels = mergeCreativeContextReuseLabels( existingContext?.reuseLabels ?? [], slideReuseLabels, ); const previous = contextMode === "off" ? null : await getGenerationCreativeContext({ appId: "slides", artifactType: "deck", artifactId: deckId, }); const editedElementProvenance = slideReuseLabels.map((label) => ({ elementId: slideId, influence: label.influence ?? ("reference-conditioned" as const), ...(label.itemId ? { itemId: label.itemId } : {}), ...(label.itemVersionId ? { itemVersionId: label.itemVersionId } : {}), label: label.label, })); const elementProvenance = contextMode === "off" ? editedElementProvenance : replaceCreativeContextElementProvenance( previous?.elementProvenance ?? [], editedElementProvenance, ); creativeContext = { contextMode, contextPackId: validated.contextPackId, reuseLabels: slideReuseLabels, mergedReuseLabels, elementProvenance, }; slide.creativeContextReuseLabels = slideReuseLabels; deck.creativeContext = contextMode === "off" && existingContext ? existingContext : { contextMode, contextPackId: validated.contextPackId, reuseLabels: mergedReuseLabels, }; } const now = nextDeckRevision(row.updatedAt); deck.updatedAt = now; await db.transaction(async (tx: any) => { await createDeckVersionSnapshot( { id: row.id, title: row.title ?? "Untitled", data: row.data ?? "", ownerEmail: row.ownerEmail ?? "", }, { force: isAgentPatchCaller(ctx?.caller), chatContext: deckVersionChatContextFromAction(ctx), label: "Before slide edit", db: tx, }, ); const updateResult = await tx .update(schema.decks) .set({ ...deckDataColumns(deck), updatedAt: now }) .where(deckRevisionWhere(schema.decks, deckId, row.updatedAt)); assertDeckWriteApplied(updateResult, deckId, "slide edit"); if (creativeContext) { await recordGenerationCreativeContext( { appId: "slides", artifactType: "deck", artifactId: deckId, contextMode: creativeContext.contextMode, contextPackId: creativeContext.contextPackId, reuseLabels: creativeContext.contextMode === "off" ? creativeContext.reuseLabels : creativeContext.mergedReuseLabels, elementProvenance: creativeContext.elementProvenance, }, { db: tx }, ); } }); return { applied, notFound, editResults, slide, slideIndex, previousContent, contentHash: hashSlideContent(String(slide.content ?? "")), layoutFitRevision: slide.layoutFitRevision, slideCount: deck.slides.length as number, generationAttemptId: generationAttemptIdOf(deck.generationContext), repointedReveals, ...(creativeContext ? { contextMode: creativeContext.contextMode, contextPackId: creativeContext.contextPackId, reuseLabels: creativeContext.reuseLabels, } : {}), }; } return { applied, notFound, editResults, slide, slideIndex, previousContent, contentHash: hashSlideContent(String(slide.content ?? "")), layoutFitRevision: slide.layoutFitRevision, slideCount: deck.slides.length as number, generationAttemptId: generationAttemptIdOf(deck.generationContext), repointedReveals, }; }; const rmw = await withDeckLock(deckId, () => retryDeckWrite(applyEdit)); if (rmw.notFound) { fail( `Nothing was written: text not found in slide: "${find!.slice(0, 60)}". Current slide contentHash is ${rmw.contentHash}; call get-deck with this slideId and rebase the patch against the current HTML.`, { errorCode: "slide_text_not_found" }, ); } const { applied, editResults } = rmw; const unmatched = (editResults ?? []).filter((entry) => entry.endsWith(":0"), ); if (!applied) { fail( unmatched.length ? `Nothing was written: ${unmatched.join(", ")} matched no text in the slide. Current slide contentHash is ${rmw.contentHash}; call get-deck with this slideId and rebase the patch against the current HTML.` : `Nothing was written: the result is identical to the current slide content (contentHash ${rmw.contentHash}). The slide already says what this edit would have made it say.`, { errorCode: "slide_edit_noop" }, ); } if (applied) { touchAgentSlidePresence({ deckId, slideId, label: slideLabelFor(rmw.slide, rmw.slideIndex), }); } const agentChangeId = deckVersionChangeGroupFromAction(ctx); await notifyClients(deckId, { slideId, actor: "agent", ...(agentChangeId ? { agentChangeId } : {}), }); const endedAt = Date.now(); trackSlides( "deck_edited", { output_id: deckId, output_type: "deck", slide_id: slideId, slide_count: rmw.slideCount, edit_mode: "update_slide", edits_count: applied, ...generationTimingFields(startedAt, endedAt), ...(rmw.generationAttemptId ? { generation_attempt_id: rmw.generationAttemptId } : {}), }, ctx, ); console.log( `update-slide: deck=${deckId} slide=${slideId} ${edits ? `edits=${edits.length}` : objectId !== undefined ? `objectId="${objectId}"` : find !== undefined ? `find="${find.slice(0, 40)}"` : "fullContent"} applied=${applied}`, ); const base = { ok: true, deckId, slideId, applied, editResults, ...(unmatched.length ? { partial: true, message: `Applied, but ${unmatched.join(", ")} matched no text and was skipped — do not report those parts as done.`, } : {}), contentHash: rmw.contentHash, ...(rmw.repointedReveals.length ? { repointedReveals: rmw.repointedReveals } : {}), layoutFit: { status: "pending" as const, slideId, contentHash: rmw.contentHash, layoutFitRevision: rmw.layoutFitRevision, }, ...slideHygieneResult([ { slideId, html: String(rmw.slide.content ?? ""), previousHtml: rmw.previousContent, }, ]), appUrl: getDeckUrl(deckId), deepLink: deckDeepLink(deckId), ...(rmw.contextMode ? { contextMode: rmw.contextMode, contextPackId: rmw.contextPackId, reuseLabels: rmw.reuseLabels, } : {}), }; return base; }, link: ({ result, args }) => { const deckId = result && typeof result === "object" ? ((result as { deckId?: string }).deckId ?? (typeof args.deckId === "string" ? args.deckId : undefined)) : typeof args.deckId === "string" ? args.deckId : undefined; if (!deckId) return null; return { url: deckDeepLink(deckId), label: "Open deck in Slides", view: "editor", }; }, });