import { defineAction } from "@agent-native/core/action"; import { getRequestRunContext, getRequestUserEmail, } from "@agent-native/core/server/request-context"; import { formatAgentDesignSystemContext, loadAgentDesignSystemContext, } from "@agent-native/core/shared"; import { accessFilter } from "@agent-native/core/sharing"; import { and, asc, desc, eq } from "drizzle-orm"; import { z } from "zod"; import { getDb, schema } from "../server/db/index.js"; import { commentActorKey } from "../shared/comment-author.js"; import { resolveDeckDesignSystemId } from "../shared/deck-content.js"; import { normalizeOwnerEmail } from "../shared/ownership.js"; import { summarizeDeckStyle } from "../shared/representative-slide.js"; import { parseSlideCommentAnchor } from "../shared/slide-comment-anchor.js"; import { summarizeSlideCommentReactions } from "../shared/slide-comment-reactions.js"; import { hashSlideContent, slideFitMeasurementMatchesSlide, type DeckFitState, } from "../shared/slide-fit.js"; import { readAppStateForCurrentTab } from "./_tab-state.js"; import getDeckTemplate from "./get-deck-template.js"; import getDesignSystem from "./get-design-system.js"; import listDeckTemplates from "./list-deck-templates.js"; type CurrentSlideFitMeasurement = DeckFitState["slides"][string] & { slideId: string; }; const CURRENT_SLIDE_COMMENT_LIMIT = 100; const HOME_DECK_LIST_LIMIT = 20; function getCurrentSlideFitMeasurement( value: unknown, slide: { id: string; content?: string; layoutFitRevision?: string } | null, deckId: string, ): CurrentSlideFitMeasurement | null { if (!slide || !value || typeof value !== "object") return null; const measurement = value as Record; const slideId = measurement.slideId; const measurementDeckId = measurement.deckId; const contentHash = measurement.contentHash; const contentHeight = measurement.contentHeight; const contentWidth = measurement.contentWidth; const viewportHeight = measurement.viewportHeight; const viewportWidth = measurement.viewportWidth; const verticalOverflow = measurement.verticalOverflow; const horizontalOverflow = measurement.horizontalOverflow; const measuredAt = measurement.measuredAt; const layoutFitRevision = measurement.layoutFitRevision; if ( typeof slideId !== "string" || slideId !== slide.id || (measurementDeckId !== undefined && measurementDeckId !== deckId) || typeof contentHash !== "string" || (layoutFitRevision !== undefined && typeof layoutFitRevision !== "string") || !slideFitMeasurementMatchesSlide( { contentHash, ...(typeof layoutFitRevision === "string" ? { layoutFitRevision } : {}), }, slide, ) || typeof contentHeight !== "number" || !Number.isFinite(contentHeight) || typeof contentWidth !== "number" || !Number.isFinite(contentWidth) || typeof viewportHeight !== "number" || !Number.isFinite(viewportHeight) || typeof viewportWidth !== "number" || !Number.isFinite(viewportWidth) || typeof verticalOverflow !== "number" || !Number.isFinite(verticalOverflow) || typeof horizontalOverflow !== "number" || !Number.isFinite(horizontalOverflow) || typeof measuredAt !== "number" || !Number.isFinite(measuredAt) ) { return null; } return { slideId, contentHash, ...(typeof layoutFitRevision === "string" ? { layoutFitRevision } : {}), contentHeight, contentWidth, viewportHeight, viewportWidth, verticalOverflow, horizontalOverflow, measuredAt, }; } export default defineAction({ title: "Inspect current Slides screen", description: "Inspect the current Slides editor context when the active deck, slide, or selection is unknown. Returns the current deck and slide IDs, slide previews, current slide HTML, and matching visual selection metadata (or the deck list on the home page). For a short exact selectedText browser-range edit, use this result directly with one update-slide literal replacement and expectedMatches=1. When a selected element has an objectId but no exact selectedText, use that objectId with one update-slide replace edit to change only its inner content; do not load the full deck for either focused path.", schema: z.object({}), http: false, run: async (_args, ctx) => { const navigation = (await readAppStateForCurrentTab("navigation")) as { view?: string; deckId?: string; templateId?: string; search?: string; deckFilter?: "all" | "created-by-me"; slideNumber?: number; slideIndex?: number; } | null; const chatScope = getRequestRunContext()?.chatScope; const scopedDeckId = chatScope?.type === "deck" && typeof chatScope.id === "string" ? chatScope.id : null; const effectiveNavigation = scopedDeckId ? { ...(navigation ?? {}), view: "editor", deckId: scopedDeckId, slideNumber: navigation?.deckId === scopedDeckId ? navigation.slideNumber : undefined, slideIndex: navigation?.deckId === scopedDeckId ? navigation.slideIndex : 0, } : navigation; if ( !scopedDeckId && (navigation?.view === "templates" || navigation?.templateId) ) { const result = await listDeckTemplates.run({ search: navigation.search, page: 1, pageSize: 6, includePreview: "false", }); const selected = navigation.templateId ? await getDeckTemplate.run({ id: navigation.templateId }) : null; return [ "## Current Screen", `view: ${navigation.view ?? "list"}`, `templateSearch: ${navigation.search ?? ""}`, ...(selected ? [ `templateId: ${selected.id}`, `templateTitle: ${selected.title}`, `slideCount: ${selected.slideCount}`, ] : []), "### Templates", ...result.templates.map( (template) => `- id=${template.id} title=${JSON.stringify(template.title)} slides=${template.slideCount}`, ), "Use get-deck-template to inspect a template, then create-deck-from-template to save an editable copy without AI generation.", ].join("\n"); } const db = getDb(); if (effectiveNavigation?.deckId) { const rows = await db .select() .from(schema.decks) .where( and( eq(schema.decks.id, effectiveNavigation.deckId), accessFilter(schema.decks, schema.deckShares), ), ) .limit(1); if (rows.length === 0) { return [ `view: ${effectiveNavigation.view ?? "editor"}`, `deckId: ${effectiveNavigation.deckId} (NOT FOUND in database — the deck may have just been created and not yet persisted)`, "", "Wait a moment and call view-screen again, or list-decks to see what's available.", ].join("\n"); } const deck = JSON.parse(rows[0].data); const slides: Array<{ id: string; layout?: string; content?: string; background?: string; layoutFitRevision?: string; }> = Array.isArray(deck?.slides) ? deck.slides : []; const slideIndex = typeof effectiveNavigation.slideIndex === "number" ? effectiveNavigation.slideIndex : typeof effectiveNavigation.slideNumber === "number" && Number.isFinite(effectiveNavigation.slideNumber) && effectiveNavigation.slideNumber >= 1 ? effectiveNavigation.slideNumber - 1 : 0; const slideNumber = slideIndex + 1; const currentSlide = slides[slideIndex] ?? null; const lines: string[] = []; lines.push(`## Current Screen`); lines.push(``); lines.push(`view: ${effectiveNavigation.view ?? "editor"}`); lines.push( `deckId: ${rows[0].id} ← use this as deckId for add-slide / update-slide / create-deck, and as id for get-deck`, ); lines.push(`deckTitle: ${rows[0].title ?? deck?.title ?? "(untitled)"}`); lines.push(`slideCount: ${slides.length}`); lines.push( `slideNumbering: User-visible slide numbers are 1-based and match the UI. "Slide 1" means the first slide, not internal index 1. Use slideId for edits.`, ); lines.push( `currentSlideNumber: ${slideNumber} of ${slides.length} (1-based; matches the UI)`, ); lines.push( `currentSlideIndex: ${slideIndex} (0-based internal value only; do not use this to interpret "slide N" from the user)`, ); if (currentSlide) { lines.push( `currentSlideId: ${currentSlide.id} ← use this for update-slide --slideId`, ); lines.push(`currentSlideLayout: ${currentSlide.layout ?? "(none)"}`); lines.push( `currentSlideContentHash: ${hashSlideContent(String(currentSlide.content ?? ""))} ← baseContentHash for update-slide`, ); } else { lines.push( `currentSlideId: (no slide for slide number ${slideNumber} / internal index ${slideIndex} — deck may be empty)`, ); } lines.push(``); lines.push(`### All slides in this deck (${slides.length})`); if (slides.length === 0) { lines.push(`(empty — use add-slide to add slides)`); } else { for (let i = 0; i < slides.length; i++) { const s = slides[i]; const marker = i === slideIndex ? " ◀ current" : ""; const contentPreview = typeof s.content === "string" ? s.content .replace(/<[^>]+>/g, " ") .replace(/\s+/g, " ") .trim() .slice(0, 60) : ""; lines.push( `Slide ${i + 1}. id=${s.id} internalIndex=${i} layout=${s.layout ?? "-"} "${contentPreview}"${marker}`, ); } } const { deckStyle, representativeSlideIndex } = summarizeDeckStyle( slides, slideIndex, ); const designSystem = await loadAgentDesignSystemContext( resolveDeckDesignSystemId(rows[0], deck), getDesignSystem, ); if (deckStyle.length > 0 || representativeSlideIndex !== null) { lines.push(``); lines.push(`### Deck style (shared across slides)`); lines.push(...deckStyle); if (representativeSlideIndex !== null) { const sibling = slides[representativeSlideIndex]!; lines.push( `representativeSlide: id=${sibling.id} (slide ${representativeSlideIndex + 1}, layout=${sibling.layout ?? "-"}) ← before a style or layout change, read it with get-deck { id: deckId, slideId: "${sibling.id}", compact: "false" } and mirror its structure and values`, ); } } if (designSystem) { lines.push("", ...formatAgentDesignSystemContext(designSystem)); } if (currentSlide?.content) { lines.push(``); lines.push( `### Current slide HTML (slide ${slideNumber}, internal index ${slideIndex}, id ${currentSlide.id})`, ); lines.push("```html"); lines.push(currentSlide.content); lines.push("```"); } const fetchedCommentRows = currentSlide ? await db .select({ id: schema.slideComments.id, slideId: schema.slideComments.slideId, threadId: schema.slideComments.threadId, parentId: schema.slideComments.parentId, content: schema.slideComments.content, quotedText: schema.slideComments.quotedText, anchor: schema.slideComments.anchor, emojiReactionsJson: schema.slideComments.emojiReactionsJson, authorEmail: schema.slideComments.authorEmail, resolved: schema.slideComments.resolved, createdAt: schema.slideComments.createdAt, }) .from(schema.slideComments) .where( and( eq(schema.slideComments.deckId, rows[0].id), eq(schema.slideComments.slideId, currentSlide.id), ), ) .orderBy(asc(schema.slideComments.createdAt)) .limit(CURRENT_SLIDE_COMMENT_LIMIT + 1) : []; const commentsTruncated = fetchedCommentRows.length > CURRENT_SLIDE_COMMENT_LIMIT; const commentRows = commentsTruncated ? fetchedCommentRows.slice(0, CURRENT_SLIDE_COMMENT_LIMIT) : fetchedCommentRows; lines.push(``); lines.push( `### Comments on current slide (${commentRows.length}${commentsTruncated ? "; more available" : ""})`, ); if (commentsTruncated) { lines.push( `commentsStatus: truncated; showing the first ${CURRENT_SLIDE_COMMENT_LIMIT}. Use list-slide-comments with { deckId: "${rows[0].id}", slideId: "${currentSlide?.id}", limit: ${CURRENT_SLIDE_COMMENT_LIMIT}, offset: ${CURRENT_SLIDE_COMMENT_LIMIT} } to continue.`, ); } if (commentRows.length === 0) { lines.push(`(no comments)`); } else { for (const comment of commentRows) { const anchor = parseSlideCommentAnchor(comment.anchor); const viewerEmail = getRequestUserEmail(); const reactions = summarizeSlideCommentReactions( comment.emojiReactionsJson, viewerEmail && commentActorKey(ctx?.caller, viewerEmail.trim().toLowerCase()), ); lines.push( `commentId: ${comment.id} threadId: ${comment.threadId} parentId: ${comment.parentId ?? "(root)"}`, ); lines.push( `author: ${comment.authorEmail} resolved: ${comment.resolved ? "true" : "false"} createdAt: ${comment.createdAt}`, ); lines.push(`content: ${comment.content}`); if (comment.quotedText) lines.push(`quotedText: ${comment.quotedText}`); if (anchor) lines.push(`anchor: ${JSON.stringify(anchor)}`); if (reactions.length > 0) { lines.push(`reactions: ${JSON.stringify(reactions)}`); } } } const panels = (await readAppStateForCurrentTab("slides-panels", { fallbackToGlobal: false, })) as { layersOpen?: boolean; animationsOpen?: boolean } | null; const openPanels = [ panels?.layersOpen ? "layers" : null, panels?.animationsOpen ? "animations" : null, ].filter(Boolean); lines.push(``); lines.push(`### Editor panels`); lines.push( panels ? `openPanels: ${openPanels.join(", ") || "none"}` : `openPanels: unknown`, ); const selection = (await readAppStateForCurrentTab("slides-selection", { fallbackToGlobal: false, })) as { deckId?: string; slideId?: string; mode?: string; activeTool?: string; items?: Array<{ selector?: string; runtimeSelector?: string; objectId?: string; text?: string; selectedText?: string; textTruncated?: boolean; kind?: string; tagName?: string; imageSrc?: string; videoSrc?: string; style?: Record; }>; } | null; const openPanel = (await readAppStateForCurrentTab("slides-open-panel", { fallbackToGlobal: false, })) as { slideId?: string; panel?: string | null; notesOpen?: boolean; } | null; if (openPanel?.panel && openPanel.slideId === currentSlide?.id) { lines.push(``); lines.push(`### Open side panel`); lines.push(`openPanel: ${openPanel.panel}`); } if (openPanel?.notesOpen && openPanel.slideId === currentSlide?.id) { lines.push(``); lines.push(`### Speaker notes panel`); lines.push(`notesOpen: true`); } const selectionSlide = selection?.slideId && (selection.deckId ? selection.deckId === rows[0].id : true) ? (slides.find((s) => s.id === selection.slideId) ?? null) : null; if (selection && selectionSlide) { lines.push(``); lines.push(`### Current visual selection`); lines.push( `editorCurrentSlideId: ${selectionSlide.id} ← authoritative slide recorded by the editor; use this for the next focused edit`, ); lines.push( `selectionSlideId: ${selection.slideId}` + (selectionSlide.id === currentSlide?.id ? ` (matches currentSlideId)` : ` (differs from currentSlideId ${currentSlide?.id ?? "(none)"} — use selectionSlideId, the slide this selection was made on)`), ); if (selectionSlide.id !== currentSlide?.id) { lines.push( `selectionSlideContentHash: ${hashSlideContent(String(selectionSlide.content ?? ""))} ← use as baseContentHash with selectionSlideId`, ); } lines.push(`mode: ${selection.mode ?? "unknown"}`); lines.push(`activeTool: ${selection.activeTool ?? "select"}`); if (Array.isArray(selection.items) && selection.items.length > 0) { for (const [index, item] of selection.items.entries()) { const isImageSelection = item.kind === "image" || item.tagName?.toLowerCase() === "img"; const isVideoSelection = item.kind === "video" || item.tagName?.toLowerCase() === "video"; lines.push( `selected ${index + 1}: ${item.kind ?? "element"} ${item.tagName ?? ""} selector=${item.selector ?? "(none)"}`, ); if (item.objectId && !isImageSelection) { lines.push(`objectId: ${item.objectId}`); lines.push( "objectIdStatus: stable selected-element target; use it with one update-slide replace edit when selectedText is unavailable", ); } if (item.runtimeSelector) { lines.push(`runtimeSelector: ${item.runtimeSelector}`); } if (item.selectedText) { lines.push(`selectedText: ${item.selectedText}`); lines.push( "selectedTextStatus: exact browser range; use verbatim as edits.find with expectedMatches: 1", ); } if (isImageSelection) { lines.push( "imageStatus: image selection has no editable text content; use the targeted image/markup workflow", ); } else if (isVideoSelection) { lines.push( "videoStatus: video selection has no editable text content; use update-slide to change its source or playback attributes", ); } else if (item.text) { lines.push(`text: ${item.text}`); if (!item.selectedText) { lines.push( item.objectId ? "textStatus: element preview is not an exact browser-range selection; use objectId with update-slide for an element-only replacement" : item.textTruncated === true ? `textStatus: element preview may be truncated; use get-deck with slideId=${selectionSlide.id} before editing` : item.textTruncated === false ? `textStatus: element text is complete but is not an exact browser-range selection; use get-deck with slideId=${selectionSlide.id} before editing` : `textStatus: element preview status unknown; use get-deck with slideId=${selectionSlide.id} before editing`, ); } else { lines.push( "textStatus: element preview; use selectedText for a literal replacement", ); } } if (item.imageSrc) lines.push(`imageSrc: ${item.imageSrc}`); if (item.videoSrc) lines.push(`videoSrc: ${item.videoSrc}`); if (item.style) { lines.push(`style: ${JSON.stringify(item.style)}`); } } } else { lines.push(`(no selected elements)`); } } const currentSlideMeasurement = getCurrentSlideFitMeasurement( await readAppStateForCurrentTab("slide-fit-check"), currentSlide, rows[0].id, ); const verticalOverflow = currentSlideMeasurement?.verticalOverflow ?? 0; const horizontalOverflow = currentSlideMeasurement?.horizontalOverflow ?? 0; if ( currentSlideMeasurement && (verticalOverflow > 0 || horizontalOverflow > 0) ) { lines.push(``); lines.push(`### ⚠ Layout overflows the canvas`); lines.push( `This slide's natural rendered content is ${currentSlideMeasurement.contentWidth}x${currentSlideMeasurement.contentHeight}px, ` + `but the canvas content area is ${currentSlideMeasurement.viewportWidth}x${currentSlideMeasurement.viewportHeight}px ` + `(overflow: ${verticalOverflow}px vertical, ${horizontalOverflow}px horizontal). The renderer no longer ` + `auto-shrinks overflowing slides — you must rewrite the slide HTML so ` + `the rendered content fits the measured content area. Options, ` + `in order of preference: (1) tighten copy — shorter headings/bullets, ` + `drop low-value lines; (2) reduce vertical density — fewer stacked ` + `cards, smaller gaps, slightly smaller body font (not below 16px); ` + `(3) reduce slide padding (e.g. 40px top/bottom); (4) split the ` + `content across two slides if it genuinely cannot be compressed. ` + `Do not solve this with transform: scale, overflow: scroll, or ` + `absolute positioning — only the HTML shape can fix it now.`, ); } const deckFit = (await readAppStateForCurrentTab( "deck-fit-checks", )) as DeckFitState | null; if ( deckFit?.deckId === rows[0].id && deckFit.aspectRatio === (deck.aspectRatio ?? "16:9") && deckFit.slides ) { type DeckFitSummary = | { kind: "unknown"; index: number } | { kind: "overflow"; index: number; measurement: (typeof deckFit.slides)[string]; }; const measured: DeckFitSummary[] = slides.flatMap( (slide, index): DeckFitSummary[] => { const measurement = slide.id === currentSlideMeasurement?.slideId ? currentSlideMeasurement : deckFit.slides[slide.id]; if ( !measurement || !slideFitMeasurementMatchesSlide(measurement, slide) || !Number.isFinite(measurement.verticalOverflow) || !Number.isFinite(measurement.horizontalOverflow) || !Number.isFinite(measurement.contentHeight) || !Number.isFinite(measurement.contentWidth) || !Number.isFinite(measurement.viewportHeight) || !Number.isFinite(measurement.viewportWidth) || !Number.isFinite(measurement.measuredAt) ) { return [{ kind: "unknown" as const, index }]; } return measurement.verticalOverflow > 0 || measurement.horizontalOverflow > 0 ? [{ kind: "overflow" as const, index, measurement }] : []; }, ); const unknown = measured.filter((item) => item.kind === "unknown"); const overflows = measured.filter((item) => item.kind === "overflow"); lines.push(``); lines.push(`### Deck-wide layout fit`); if (unknown.length > 0) { lines.push( `Measured ${slides.length - unknown.length} of ${slides.length} slides; ` + `the remaining slides need a fresh browser measurement before claiming the deck fits.`, ); } else if (overflows.length > 0) { lines.push( `Overflow detected on ${overflows.length} slide(s): ${overflows .map((item) => { if (item.kind !== "overflow") return ""; return `slide ${item.index + 1} (${item.measurement.verticalOverflow}px vertical, ${item.measurement.horizontalOverflow}px horizontal)`; }) .join(", ")}.`, ); } else { lines.push( `All ${slides.length} slides fit their measured content area.`, ); } } return lines.join("\n"); } const rows = await db .select({ id: schema.decks.id, title: schema.decks.title, ownerEmail: schema.decks.ownerEmail, }) .from(schema.decks) .where(accessFilter(schema.decks, schema.deckShares)) .orderBy(desc(schema.decks.updatedAt)); const normalizedUserEmail = normalizeOwnerEmail(getRequestUserEmail()); const filteredRows = navigation?.deckFilter === "created-by-me" ? normalizedUserEmail !== null ? rows.filter( (row) => normalizeOwnerEmail(row.ownerEmail) === normalizedUserEmail, ) : [] : rows; const lines: string[] = []; lines.push(`## Current Screen`); lines.push(``); lines.push(`view: ${effectiveNavigation?.view ?? "list"}`); lines.push(`No deck currently open. User is on the deck list.`); lines.push( `deckFilter: ${ navigation?.deckFilter === "created-by-me" ? "created by me" : "all accessible decks" }`, ); lines.push(``); lines.push( navigation?.deckFilter === "created-by-me" ? `### Decks created by current user (${filteredRows.length} of ${rows.length})` : `### All decks (${rows.length})`, ); if (filteredRows.length === 0) { lines.push(`(no decks — use create-deck to make one)`); } else { const shownRows = filteredRows.slice(0, HOME_DECK_LIST_LIMIT); for (const row of shownRows) { lines.push(`- id=${row.id} title="${row.title ?? "(untitled)"}"`); } if (shownRows.length < filteredRows.length) { lines.push( `showing ${shownRows.length} of ${filteredRows.length} decks, most recent first; use list-decks for more.`, ); } lines.push(``); lines.push( `(slide counts omitted here for performance — call list-decks or open a deck to see slide content)`, ); } return lines.join("\n"); }, });