{"version":3,"file":"vue.cjs","names":[],"sources":["../src/vue/support.ts","../src/vue/helpers.ts","../src/vue/subscription.ts","../src/vue/transport.ts","../src/vue/hostAdapter.ts","../src/vue/composables/useMediaExport.ts","../src/vue/composables/useBeatMovie.ts","../src/vue/composables/useCharacterImages.ts","../src/vue/composables/useDeckEditor.ts","../src/lang/de.ts","../src/lang/en.ts","../src/lang/es.ts","../src/lang/fr.ts","../src/lang/ja.ts","../src/lang/ko.ts","../src/lang/ptBR.ts","../src/lang/zh.ts","../src/lang/index.ts","../src/vue/components/BeatLightbox.vue","../src/vue/components/BeatLightbox.vue","../src/vue/components/CharacterStrip.vue","../src/vue/components/CharacterStrip.vue","../src/vue/components/MulmoScriptToolbar.vue","../src/vue/components/MulmoScriptToolbar.vue","../src/vue/View.vue","../src/vue/View.vue","../src/vue/Preview.vue","../src/vue/Preview.vue","../src/vue/index.ts"],"sourcesContent":["// Small host-independent utilities the View needs, ported from\n// MulmoClaude's `src/composables/useClipboardCopy.ts` so the package has no\n// host imports. (`errorMessage` moved to `@mulmoclaude/common`.)\n\nimport { ref, type Ref } from \"vue\";\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Read a dropped image File as a base64 data URL, the form the upload\n *  dispatches expect. Shared by the beat and character drop handlers.\n *  `readAsDataURL` always yields a string on load; the non-string reject\n *  is an unreachable guard kept so the resolve type stays `string` without\n *  a cast. */\nexport function readFileAsDataUrl(file: File): Promise<string> {\n  return new Promise<string>((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      const { result } = reader;\n      if (typeof result === \"string\") resolve(result);\n      else reject(new Error(\"FileReader did not return a data URL string\"));\n    };\n    reader.onerror = reject;\n    reader.readAsDataURL(file);\n  });\n}\n\nexport interface UseClipboardCopyHandle {\n  copied: Ref<boolean>;\n  copy: (text: string) => Promise<void>;\n}\n\n/** Clipboard failures (permissions, insecure context) are swallowed on\n *  purpose: the UI just leaves the \"Copied!\" hint off, which is what\n *  `copied=false` already signals. */\nexport function useClipboardCopy(resetMs = 2000): UseClipboardCopyHandle {\n  const copied = ref(false);\n\n  async function copy(text: string): Promise<void> {\n    try {\n      await navigator.clipboard.writeText(text);\n      copied.value = true;\n      setTimeout(() => {\n        copied.value = false;\n      }, resetMs);\n    } catch {\n      // Clipboard API blocked (iframe without permissions, non-HTTPS origin) — leave `copied` false.\n    }\n  }\n\n  return { copied, copy };\n}\n","// Pure helpers for the presentMulmoScript View. Kept separate so their\n// logic is unit-testable without mounting the Vue component. Ported from\n// the host's `src/plugins/presentMulmoScript/helpers.ts`; the SSE-stream\n// helpers did not move — per-beat generation progress now arrives on the\n// plugin pubsub channel (see `core/contract.ts`).\n\nimport { sameRoot } from \"../core/contract\";\nimport { isRecord } from \"./support\";\n\n/**\n * Decide whether a beat should be rendered automatically at\n * script load time. Text-based beats (slides, charts, etc.) are\n * auto-rendered only when the script has no characters —\n * characters must be rendered first so they can be referenced by\n * any character-using beat.\n */\nexport function shouldAutoRenderBeat(\n  beat: { image?: { type?: string | undefined } | undefined },\n  hasCharacters: boolean,\n  autoRenderTypes: readonly string[],\n): boolean {\n  if (hasCharacters) return false;\n  const type = beat.image?.type;\n  if (typeof type !== \"string\") return false;\n  return autoRenderTypes.includes(type);\n}\n\n/**\n * Of the given character keys, return those whose image is not\n * yet loaded and is not currently rendering. Used to fetch only\n * what's missing after a movie-generation event arrives.\n */\nexport function getMissingCharacterKeys(keys: readonly string[], images: Record<string, unknown>, renderState: Record<string, string | undefined>): string[] {\n  return keys.filter((charKey) => !images[charKey] && renderState[charKey] !== \"rendering\");\n}\n\n/**\n * A schema shape that exposes `safeParse` — matches Zod's API\n * without pulling the dep into this module.\n */\nexport interface SafeParseSchema {\n  safeParse: (value: unknown) => { success: boolean };\n}\n\n/**\n * Validate a candidate Beat JSON string against a schema.\n * Returns false on any JSON parse error or schema mismatch.\n */\nexport function validateBeatJSON(json: string, schema: SafeParseSchema): boolean {\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(json);\n  } catch {\n    return false;\n  }\n  return schema.safeParse(parsed).success;\n}\n\n/**\n * Stable structural equality for two MulmoScripts via JSON\n * canonicalisation. We compare the full re-serialised string\n * rather than walking keys because (a) MulmoScript is\n * deeply-nested and Object.keys-recursion would be ~50 lines, and\n * (b) `JSON.stringify` already preserves insertion order, which\n * `mulmoScriptSchema.safeParse` keeps stable across runs of the\n * same input. False positives (= \"differ\" when they don't) only\n * cost an extra `emit(\"updateResult\", ...)` which is a no-op when\n * data hasn't actually changed.\n */\nexport function isSameScript(left: unknown, right: unknown): boolean {\n  return JSON.stringify(left) === JSON.stringify(right);\n}\n\n/**\n * True when a beat can have a generated video clip on disk — used to\n * decide whether to probe the beat-movie endpoint. `moviePrompt`\n * beats produce a per-beat movie file; `html_tailwind` beats with\n * `animation` set (either `true` or an options object) produce an\n * `_animated.mp4` render.\n */\nexport function beatMayHaveMovie(beat: { moviePrompt?: string; image?: { type?: string; animation?: unknown } }): boolean {\n  if (beat.moviePrompt) return true;\n  return beat.image?.type === \"html_tailwind\" && Boolean(beat.image.animation);\n}\n\n/**\n * True for a beat whose image merely REFERENCES another beat's image\n * (`image: { type: \"beat\", id }` — mulmoBeatReferenceMediaSchema). Such a\n * beat owns no asset of its own, so there is nothing to generate for it:\n * the View hides the Generate button (offering it produced a render that\n * could never succeed on its own terms).\n */\nexport function isBeatImageReference(beat: { image?: { type?: string; [key: string]: unknown } }): boolean {\n  return beat.image?.type === \"beat\";\n}\n\n/** Whether the beat editor has anything to edit.\n *\n *  Any beat type, not just `slide`: `@mulmocast/beat-editor` renders and edits all eight\n *  (`textSlide` / `markdown` / `chart` / `mermaid` / `image` / `movie` / `slide` /\n *  `html_tailwind`). The all-slide test this replaces was a limit of the OLD iframe deck\n *  editor, which only understood decks — it stayed in place through the migration and kept a\n *  markdown script read-only for no reason.\n *\n *  Empty / missing `beats[]` is false: there is nothing to edit, and the per-beat list already\n *  renders an empty state. */\nexport function hasEditableBeats(script: unknown): boolean {\n  if (!isRecord(script)) return false;\n  const { beats } = script;\n  return Array.isArray(beats) && beats.length > 0;\n}\n\n/** A single MulmoScript beat as the View consumes it — every field\n *  optional so the empty-beat fallback (`effectiveBeat` on an\n *  out-of-range index) is a valid instance without a cast. */\nexport interface Beat {\n  speaker?: string;\n  text?: string;\n  id?: string;\n  imagePrompt?: string;\n  moviePrompt?: string;\n  image?: { type: string; [key: string]: unknown };\n  /** Beat duration in seconds. The mulmocast schema notes this is\n   *  \"Used only when the text is empty\" — the silent-beat Play loop\n   *  uses it as the auto-advance timer (#1073). */\n  duration?: number;\n}\n\n/** Resolve the beat the View should render at `index`: the user's\n *  in-place edit (`overrides`) wins over the on-disk beat, and an\n *  out-of-range index yields an empty beat so callers can read\n *  `.text` / `.image` without a guard. */\nexport function effectiveBeat(overrides: Record<number, Beat>, beats: readonly Beat[], index: number): Beat {\n  return overrides[index] ?? beats[index] ?? {};\n}\n\nconst BEAT_TOOLTIP_MAX_CHARS = 80;\n\n/** Beat-strip hover tooltip: the beat text, truncated with an ellipsis\n *  past the cap. Missing text yields an empty string. Text of exactly\n *  the cap length is returned whole (only a longer string is cut). */\nexport function beatTooltip(text: string | undefined): string {\n  const value = text ?? \"\";\n  return value.length > BEAT_TOOLTIP_MAX_CHARS ? `${value.slice(0, BEAT_TOOLTIP_MAX_CHARS)}…` : value;\n}\n\n/** The prompt for a character image, or \"\" when the key or its prompt\n *  is absent — the character strip renders the empty string as no\n *  caption rather than `undefined`. */\nexport function characterPrompt(images: Record<string, { prompt?: string }> | undefined, key: string): string {\n  return images?.[key]?.prompt ?? \"\";\n}\n\n/** Is the in-editor JSON for a beat currently valid? A missing entry\n *  (source editor never opened) validates the empty string, which is\n *  not parseable JSON, so it reports invalid rather than throwing. */\nexport function isValidBeat(source: string | undefined, schema: SafeParseSchema): boolean {\n  return validateBeatJSON(source ?? \"\", schema);\n}\n\n/** A story as the wire addresses it: the path plus the root it is relative to (absent = the\n *  host's default root). The PAIR is the identity — see `staleSince`. */\nexport interface StoryRef {\n  filePath: string;\n  root?: string | undefined;\n}\n\n/**\n * Stale-response guard: a per-beat / per-character response is stale once the View has\n * navigated to a different result.\n *\n * The identity is the PAIR `(root, filePath)`, not the path — `stories/deck.json` exists in\n * every registered root (#3014), so comparing paths alone lets one repository's deck accept\n * another's late response while both are open under the same name. `sameRoot` reads an absent\n * root as the host's default rather than as \"different\", which is what keeps every pre-root\n * caller's behaviour byte-identical.\n *\n * Keeping the direction pinned matters — an inverted check would let script A's late responses\n * write into script B's state.\n */\nexport function staleSince(current: StoryRef, requested: StoryRef): boolean {\n  return current.filePath !== requested.filePath || !sameRoot(current.root, requested.root);\n}\n\nconst JSON_INDENT = 2;\n\n/** Pretty-print a script (or any value) as the source-editor / clipboard\n *  text — two-space indent, matching what the beat and disk views emit. */\nexport function scriptSourceText(value: unknown): string {\n  return JSON.stringify(value, null, JSON_INDENT);\n}\n\n/** Basename for a download `<a download>` attribute, falling back when\n *  the path has no basename. Mirrors the exact existing behaviour, and\n *  it has a sharp edge: `.pop()` returns \"\" (not undefined) for a\n *  trailing slash or empty path, and `??` does NOT replace \"\", so those\n *  yield an empty filename rather than the fallback. Server paths always\n *  carry a basename, so this never bites in practice — pinned so a later\n *  reader doesn't \"simplify\" `??` to `||` and change behaviour. */\nexport function downloadFilename(path: string, fallback: string): string {\n  return path.split(\"/\").pop() ?? fallback;\n}\n\n/** Narrow a script-supplied silent-beat duration to a safe positive number.\n *  Zero / negative / NaN / Infinity / non-number collapse the auto-advance\n *  timer to an immediate fire, which races the Play loop through every silent\n *  beat in a single tick (#1365) — fall back to the default so a run of silent\n *  beats stays watchable. The script's own valid `duration` always wins. */\nexport function resolveSilentAdvanceSeconds(raw: unknown, defaultSec: number): number {\n  return typeof raw === \"number\" && Number.isFinite(raw) && raw > 0 ? raw : defaultSec;\n}\n\n/** Delete every own enumerable key of each record, in place. Used to reset the\n *  View's per-beat / per-character reactive maps between scripts — passing the\n *  reactive proxies mutates them so the template re-renders empty. Replaces a\n *  wall of hand-rolled `Object.keys(map).forEach(delete)` loops. */\nexport function clearReactiveRecords(...records: object[]): void {\n  records.forEach((record) => {\n    Object.keys(record).forEach((key) => Reflect.deleteProperty(record, key));\n  });\n}\n\n/**\n * Whether a `focusout` means focus actually LEFT `container`, rather than moving between two\n * fields inside it.\n *\n * The distinction decides whether pending edits are written: treating every focusout as a\n * departure would write on each hop between inputs, and treating none as one would let the user\n * walk away with the last keystroke unsaved.\n *\n * `null` is focus going nowhere the document can name — clicking the page chrome, or the window\n * losing focus. That counts as leaving: the editor is no longer where the typing goes.\n */\nexport function focusLeftContainer(container: Node | null, movedTo: EventTarget | null): boolean {\n  if (!container) return false;\n  if (!(movedTo instanceof Node)) return true;\n  return !container.contains(movedTo);\n}\n","// One subscription shape from two call forms (#3015).\n//\n// `MulmoScriptTransport` is exported from `/vue` on a package already\n// published to npm, so the two-argument calls that predate roots must keep\n// working: a third positional parameter binds an existing caller's `handler`\n// to `root`, and the event path then calls a handler as `root()` (Codex +\n// CodeRabbit on #3015). But an OPTIONAL root is the shape that shipped broken\n// twice on the server side of this same PR — the pair filter compares\n// `undefined` against every named root and silently drops the events it exists\n// to route.\n//\n// An options object satisfies both, and the discrimination is total: the\n// legacy form's first argument is a function, which an options object never\n// is. It lives here, as a pure function, because it is the part with a wrong\n// answer available — the pubsub wiring around it has none.\nimport type { MulmoScriptGenerationEvent } from \"../core/contract\";\n\nexport interface GenerationSubscription {\n  filePath: () => string;\n  root: () => string | undefined;\n  handler: (event: MulmoScriptGenerationEvent) => void;\n}\n\nexport interface ScriptChangedSubscription {\n  filePath: () => string;\n  root: () => string | undefined;\n  /** This View's id — its own writes echo back and must not be acted on. */\n  ownOrigin: string;\n  handler: () => void;\n}\n\n/** A caller that predates roots is asking for the host's default root. */\nconst DEFAULT_ROOT_GETTER = (): string | undefined => undefined;\n\n/**\n * The object form is checked at runtime, not only by the type.\n *\n * This package is published, so a JavaScript consumer reaches these functions\n * with no call-site checking at all. An object missing `root` type-checks\n * nowhere and passed through here unchanged, and the failure surfaced much\n * later as `sub.root is not a function` — thrown inside a pubsub delivery,\n * where there is no caller left to catch it and the only symptom is a View\n * that silently stops updating (CodeRabbit on #3015).\n *\n * A missing `root` is the one field with a safe answer: absent means the\n * default root, which is exactly what a caller who did not think about roots\n * intended. A missing `filePath` or `handler` has no such answer, so it\n * throws HERE, at the subscribe call, where the stack points at the bug.\n */\nfunction checkedSubscription<T extends { filePath: unknown; handler: unknown; root?: unknown }>(subscription: T, label: string): T {\n  if (typeof subscription.filePath !== \"function\" || typeof subscription.handler !== \"function\") {\n    throw new TypeError(`${label}: filePath and handler must both be functions`);\n  }\n  if (subscription.root === undefined) return { ...subscription, root: DEFAULT_ROOT_GETTER };\n  if (typeof subscription.root !== \"function\") throw new TypeError(`${label}: root must be a function returning the root id, or be omitted`);\n  return subscription;\n}\n\nexport function normalizeGenerationSubscription(\n  first: GenerationSubscription | (() => string),\n  legacyHandler?: (event: MulmoScriptGenerationEvent) => void,\n): GenerationSubscription {\n  if (typeof first !== \"function\") return checkedSubscription(first, \"onGenerationEvent(subscription)\");\n  if (!legacyHandler) throw new TypeError(\"onGenerationEvent(filePath, handler): handler is required\");\n  return { filePath: first, root: DEFAULT_ROOT_GETTER, handler: legacyHandler };\n}\n\nexport function normalizeScriptChangedSubscription(\n  first: ScriptChangedSubscription | (() => string),\n  legacyOwnOrigin?: string,\n  legacyHandler?: () => void,\n): ScriptChangedSubscription {\n  if (typeof first !== \"function\") {\n    const checked = checkedSubscription(first, \"onScriptChanged(subscription)\");\n    if (typeof checked.ownOrigin !== \"string\") throw new TypeError(\"onScriptChanged(subscription): ownOrigin must be a string\");\n    return checked;\n  }\n  if (typeof legacyOwnOrigin !== \"string\" || !legacyHandler) {\n    throw new TypeError(\"onScriptChanged(filePath, ownOrigin, handler): ownOrigin and handler are required\");\n  }\n  return { filePath: first, root: DEFAULT_ROOT_GETTER, ownOrigin: legacyOwnOrigin, handler: legacyHandler };\n}\n","// Host-agnostic transport for the presentMulmoScript View. Every operation\n// goes through `useRuntime().dispatch({ kind, … })` and returns the same\n// `{ ok, data | error }` shape the pre-extraction `apiGet`/`apiPost`\n// helpers produced, so the View's call sites stay structurally identical.\n//\n// Dispatch responses are `{ ok: … }` envelopes (see `core/contract.ts`):\n// business failures arrive as `{ ok: false, error }` data rather than HTTP\n// errors, keeping user-facing messages free of transport prefixes. A thrown\n// dispatch (network drop, host bug) is caught and folded into the same\n// failure shape.\n\nimport { useRuntime } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptChangedEvent, MulmoScriptDispatchArgs, MulmoScriptDispatchResult, MulmoScriptGenerationEvent } from \"../core/contract\";\nimport { GENERATION_EVENT, SCRIPT_CHANGED_EVENT, sameRoot, shouldReloadForScriptChange } from \"../core/contract\";\nimport { normalizeGenerationSubscription, normalizeScriptChangedSubscription } from \"./subscription\";\nimport type { GenerationSubscription, ScriptChangedSubscription } from \"./subscription\";\nexport type { GenerationSubscription, ScriptChangedSubscription } from \"./subscription\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { isRecord } from \"./support\";\n\nexport type TransportResult<T> = { ok: true; data: T } | { ok: false; error: string };\n\ntype ArgsFor<K extends MulmoScriptDispatchArgs[\"kind\"]> = Omit<Extract<MulmoScriptDispatchArgs, { kind: K }>, \"kind\">;\n\nconst GENERATION_EVENT_KINDS: ReadonlySet<string> = new Set([\"beatImage\", \"beatAudio\", \"characterImage\", \"movie\", \"pdf\"]);\n\n// These parsers are the boundary the pair identity has to survive. Rebuilding\n// the event field by field means a field nobody listed is silently dropped —\n// which is how `(root, filePath)` checks were added downstream and compared\n// `undefined` against every named root, filtering out the very events they\n// were written to route (Codex P1 on #3015).\n/**\n * A field that is absent, or a string.\n *\n * `undefined` is a legitimate answer for both `root` and `origin`, so a\n * present-but-wrong-typed value CANNOT be flattened into it: `root: 42` read\n * as \"no root\" makes a named root's event look like a default-root one, and\n * the subscriber then routes another repository's script to this View\n * (CodeRabbit on #3015 — the same shape the dispatch boundary was just fixed\n * for). Malformed means malformed; the caller rejects the payload.\n */\nfunction optionalString(value: unknown): { ok: true; value: string | undefined } | { ok: false } {\n  if (value === undefined) return { ok: true, value: undefined };\n  return typeof value === \"string\" ? { ok: true, value } : { ok: false };\n}\n\nexport function parseScriptChangedEvent(payload: unknown): MulmoScriptChangedEvent | null {\n  if (!isRecord(payload)) return null;\n  const { filePath, origin, root } = payload;\n  if (typeof filePath !== \"string\") return null;\n  // `origin` decides whether this View acts on the echo of its OWN write.\n  // Read as absent, a malformed one makes every keystroke rebuild the element\n  // the caret is in — so it is identity too, and rejected the same way.\n  const parsedOrigin = optionalString(origin);\n  const parsedRoot = optionalString(root);\n  if (!parsedOrigin.ok || !parsedRoot.ok) return null;\n  return {\n    filePath,\n    ...(parsedOrigin.value !== undefined ? { origin: parsedOrigin.value } : {}),\n    ...(parsedRoot.value !== undefined ? { root: parsedRoot.value } : {}),\n  };\n}\n\nexport function parseGenerationEvent(payload: unknown): MulmoScriptGenerationEvent | null {\n  if (!isRecord(payload)) return null;\n  const { kind, filePath, key, done, error, root } = payload;\n  if (typeof kind !== \"string\" || !GENERATION_EVENT_KINDS.has(kind)) return null;\n  if (typeof filePath !== \"string\" || typeof key !== \"string\" || typeof done !== \"boolean\") return null;\n  const parsedRoot = optionalString(root);\n  if (!parsedRoot.ok) return null;\n  return {\n    kind: kind as MulmoScriptGenerationEvent[\"kind\"],\n    filePath,\n    key,\n    done,\n    // `error` is the one field a malformed value may be dropped from rather\n    // than rejected with: it is a message shown beside a finished generation,\n    // and rejecting the whole event would drop the FINISH — leaving the\n    // spinner running forever, which is worse than losing the text.\n    ...(typeof error === \"string\" ? { error } : {}),\n    ...(parsedRoot.value !== undefined ? { root: parsedRoot.value } : {}),\n  };\n}\n\nexport interface MulmoScriptTransport {\n  call<K extends MulmoScriptDispatchArgs[\"kind\"]>(kind: K, args: ArgsFor<K>): Promise<TransportResult<MulmoScriptDispatchResult[K]>>;\n  /** Subscribe to the host's generation channel, pre-filtered to one script —\n   *  identified by the PAIR `(root, filePath)`, since the same wire path\n   *  exists in every registered root (#3014). Returns the unsubscribe\n   *  function. */\n  onGenerationEvent(subscription: GenerationSubscription): () => void;\n  /** The pre-#3014 form: no root named, so the host's default root. Kept\n   *  because this interface is exported from `/vue` on a published package. */\n  onGenerationEvent(filePath: () => string, handler: (event: MulmoScriptGenerationEvent) => void): () => void;\n  /** Subscribe to writes of one script, skipping the echo of this View's own\n   *  (`ownOrigin`). Same pair identity as above. Returns the unsubscribe\n   *  function. */\n  onScriptChanged(subscription: ScriptChangedSubscription): () => void;\n  /** The pre-#3014 form — see `onGenerationEvent` above. */\n  onScriptChanged(filePath: () => string, ownOrigin: string, handler: () => void): () => void;\n}\n\nexport function useMulmoScriptTransport(): MulmoScriptTransport {\n  const runtime = useRuntime();\n\n  async function call<K extends MulmoScriptDispatchArgs[\"kind\"]>(kind: K, args: ArgsFor<K>): Promise<TransportResult<MulmoScriptDispatchResult[K]>> {\n    let result: unknown;\n    try {\n      result = await runtime.dispatch({ kind, ...args });\n    } catch (err) {\n      return { ok: false, error: errorMessage(err) };\n    }\n    if (!isRecord(result) || result.ok !== true) {\n      const error = isRecord(result) && typeof result.error === \"string\" ? result.error : `dispatch ${kind} returned an unexpected response`;\n      return { ok: false, error };\n    }\n    return { ok: true, data: result as MulmoScriptDispatchResult[K] };\n  }\n\n  function onGenerationEvent(first: GenerationSubscription | (() => string), legacyHandler?: (event: MulmoScriptGenerationEvent) => void): () => void {\n    const { filePath, root, handler } = normalizeGenerationSubscription(first, legacyHandler);\n    return runtime.pubsub.subscribe(GENERATION_EVENT, (payload: unknown) => {\n      const event = parseGenerationEvent(payload);\n      if (!event) return;\n      const current = filePath();\n      // The PAIR, not the path: `stories/deck.json` exists in every root, so\n      // filtering on the path alone puts another repository's spinners on this\n      // View (Codex P1 on #3015). `root` is a required FIELD rather than an\n      // optional parameter because a forgotten optional is exactly how the\n      // server side of this shipped broken twice — see `GenerationSubscription`.\n      if (!current || event.filePath !== current || !sameRoot(event.root, root())) return;\n      handler(event);\n    });\n  }\n\n  /**\n   * A write to this script landed — reload from disk.\n   *\n   * `ownOrigin` is this View's id. Its own writes echo back on the same channel, and acting\n   * on them would rebuild the element the caret is in on every keystroke, so they are dropped\n   * here. A write from the agent carries no origin and always reaches the handler.\n   */\n  function onScriptChanged(first: ScriptChangedSubscription | (() => string), legacyOwnOrigin?: string, legacyHandler?: () => void): () => void {\n    const { filePath, root, ownOrigin, handler } = normalizeScriptChangedSubscription(first, legacyOwnOrigin, legacyHandler);\n    return runtime.pubsub.subscribe(SCRIPT_CHANGED_EVENT, (payload: unknown) => {\n      const event = parseScriptChangedEvent(payload);\n      if (!event) return;\n      if (!shouldReloadForScriptChange(event, filePath(), ownOrigin, root())) return;\n      handler();\n    });\n  }\n\n  return { call, onGenerationEvent, onScriptChanged };\n}\n","// Optional host-supplied capabilities that are genuinely host TRANSPORT,\n// not plugin logic — the browser-side sibling of html-plugin's host-injected\n// `previewUrl`. The generic runtime covers JSON dispatch + pubsub; what it\n// can't cover is (a) which chat session a generation should be tagged to\n// (MulmoClaude's sidebar indicator) and (b) how to fetch movie/PDF bytes,\n// which every host serves behind its own auth (MulmoClaude keeps them on\n// bearer-guarded /api routes by explicit review decision — see the\n// downloadMovie comment trail in the pre-extraction View).\n//\n// Hosts provide the adapter with Vue's provide() around the View; absent\n// capabilities degrade gracefully (no session tagging; download / clip-play\n// UI hidden).\n\nimport { inject, type InjectionKey, type Ref } from \"vue\";\n\nexport interface MulmoScriptHostAdapter {\n  /** Active chat session id, forwarded on generation dispatches so the\n   *  host can light its per-session progress indicators. */\n  chatSessionId?: Ref<string | undefined>;\n  /** Authenticated media download. Exactly one of `moviePath` / `pdfPath`\n   *  is set — both are the wire `stories/…` paths the status/probe\n   *  dispatches return. Rejects on transport/HTTP failure.\n   *\n   *  `root` is which registered stories root that path is relative to (#3014); absent = the\n   *  host's default. It is REQUIRED for correctness, not decoration: `toStoryRef` relativizes\n   *  an artifact against its own root's directory, so the returned path does not carry the\n   *  root, and the same `stories/…/__movies__/x.mov` exists in every one of them. A host that\n   *  ignores it serves the DEFAULT root's file of that name, or 404s. Optional so an older\n   *  host keeps compiling; a single-root host can ignore it because for it the two agree. */\n  fetchMediaBlob?: (query: { moviePath?: string; pdfPath?: string; root?: string | undefined }) => Promise<Blob>;\n}\n\nexport const MULMOSCRIPT_HOST_ADAPTER_KEY: InjectionKey<MulmoScriptHostAdapter> = Symbol(\"mulmoscript-host-adapter\");\n\nconst EMPTY_ADAPTER: MulmoScriptHostAdapter = {};\n\nexport function useHostAdapter(): MulmoScriptHostAdapter {\n  return inject(MULMOSCRIPT_HOST_ADAPTER_KEY, EMPTY_ADAPTER);\n}\n","// Movie + PDF export (#1614): each output has the same status-poll →\n// long-held generate dispatch → authenticated download triple, kept as\n// independent state so a movie and a PDF can be requested for the same\n// script without collision. Media bytes are served behind host auth; the\n// host-injected `fetchMediaBlob` keeps the auth boundary intact (a plain\n// `<a href download>` can't attach the host's headers).\n\nimport { ref, type ComputedRef, type Ref } from \"vue\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { downloadFilename, staleSince, type StoryRef } from \"../helpers\";\nimport type { MulmoScriptTransport } from \"../transport\";\nimport type { MulmoScriptHostAdapter } from \"../hostAdapter\";\n\nexport interface UseMediaExportOptions {\n  api: MulmoScriptTransport;\n  adapter: MulmoScriptHostAdapter;\n  filePath: ComputedRef<string>;\n  /** Which registered root `filePath` is relative to; `undefined` = the host's default (#3014). */\n  root: ComputedRef<string | undefined>;\n  chatSessionId: ComputedRef<string | undefined>;\n}\n\ntype MediaKind = \"movie\" | \"pdf\";\n\nexport function useMediaExport({ api, adapter, filePath, root, chatSessionId }: UseMediaExportOptions) {\n  // The PAIR, not the path: `stories/deck.json` exists in every root (#3014), so a late\n  // resolution must be matched against the root it was asked for as well.\n  const storyRef = (): StoryRef => ({ filePath: filePath.value, root: root.value });\n  const movieGenerating = ref(false);\n  const movieDownloading = ref(false);\n  const moviePath = ref<string | null>(null);\n  // Persists the most-recent movie-generation failure so the toolbar can\n  // surface it inline with a retry button (#1197). Cleared at the start of\n  // every generate / regenerate attempt.\n  const movieError = ref<string | null>(null);\n  const pdfGenerating = ref(false);\n  const pdfDownloading = ref(false);\n  const pdfPath = ref<string | null>(null);\n\n  // Long-held dispatch — resolves when the whole pipeline finishes (minutes).\n  // If the user navigates to a different result meanwhile the resolution\n  // describes the OLD script, so drop it; the new script's own\n  // initializeScript / pubsub subscription owns the visible state.\n  async function generateMovie(): Promise<void> {\n    const requested = storyRef();\n    movieGenerating.value = true;\n    movieError.value = null;\n    const response = await api.call(\"generateMovie\", { ...requested, chatSessionId: chatSessionId.value });\n    if (staleSince(storyRef(), requested)) return;\n    movieGenerating.value = false;\n    if (!response.ok) {\n      // Surface inline (instead of `alert()` which blocks + has no retry\n      // affordance). The error chip with a retry button lives in the toolbar.\n      movieError.value = response.error;\n      return;\n    }\n    moviePath.value = response.data.moviePath;\n  }\n\n  async function generatePdf(): Promise<void> {\n    const requested = storyRef();\n    pdfGenerating.value = true;\n    const response = await api.call(\"generatePdf\", { ...requested, chatSessionId: chatSessionId.value });\n    if (staleSince(storyRef(), requested)) return;\n    pdfGenerating.value = false;\n    if (!response.ok) {\n      alert(response.error);\n      return;\n    }\n    pdfPath.value = response.data.pdfPath;\n  }\n\n  async function refreshMoviePath(): Promise<void> {\n    const requested = storyRef();\n    if (!requested.filePath) return;\n    const response = await api.call(\"movieStatus\", requested);\n    if (staleSince(storyRef(), requested)) return;\n    if (response.ok && response.data.moviePath) moviePath.value = response.data.moviePath;\n  }\n\n  async function refreshPdfPath(): Promise<void> {\n    const requested = storyRef();\n    if (!requested.filePath) return;\n    const response = await api.call(\"pdfStatus\", requested);\n    if (staleSince(storyRef(), requested)) return;\n    if (response.ok && response.data.pdfPath) pdfPath.value = response.data.pdfPath;\n  }\n\n  // Authenticated blob → synthetic <a download> click. The download attribute\n  // carries the filename so the browser still surfaces a native save dialog.\n  async function downloadMedia(kind: MediaKind, sourcePath: string | null, fallbackName: string, downloading: Ref<boolean>): Promise<void> {\n    const fetchMediaBlob = adapter.fetchMediaBlob;\n    if (!fetchMediaBlob || !sourcePath || downloading.value) return;\n    downloading.value = true;\n    let objectUrl: string | null = null;\n    try {\n      // The root travels with the path: an artifact ref is relative to ITS root, and the same\n      // spelling exists in every other one (#3014).\n      const blob = await fetchMediaBlob(kind === \"movie\" ? { moviePath: sourcePath, root: root.value } : { pdfPath: sourcePath, root: root.value });\n      objectUrl = URL.createObjectURL(blob);\n      clickDownloadAnchor(objectUrl, downloadFilename(sourcePath, fallbackName));\n    } catch (err) {\n      alert(errorMessage(err));\n    } finally {\n      if (objectUrl) URL.revokeObjectURL(objectUrl);\n      downloading.value = false;\n    }\n  }\n\n  function downloadMovie(): Promise<void> {\n    return downloadMedia(\"movie\", moviePath.value, \"movie.mp4\", movieDownloading);\n  }\n\n  function downloadPdf(): Promise<void> {\n    return downloadMedia(\"pdf\", pdfPath.value, \"deck.pdf\", pdfDownloading);\n  }\n\n  // Movie/PDF spinners + paths are per-script: without a reset, switching away\n  // from a generating script would leave the new script's toolbar spinning.\n  function resetMedia(): void {\n    moviePath.value = null;\n    pdfPath.value = null;\n    movieGenerating.value = false;\n    pdfGenerating.value = false;\n    movieError.value = null;\n  }\n\n  return {\n    moviePath,\n    movieGenerating,\n    movieDownloading,\n    movieError,\n    pdfPath,\n    pdfGenerating,\n    pdfDownloading,\n    generateMovie,\n    downloadMovie,\n    refreshMoviePath,\n    generatePdf,\n    downloadPdf,\n    refreshPdfPath,\n    resetMedia,\n  };\n}\n\nfunction clickDownloadAnchor(href: string, filename: string): void {\n  const anchor = document.createElement(\"a\");\n  anchor.href = href;\n  anchor.download = filename;\n  document.body.appendChild(anchor);\n  anchor.click();\n  anchor.remove();\n}\n","// Per-beat generated video clip state (moviePrompt / animated beats). The wire\n// `stories/…` path comes from the beat-movie probe; the blob object URL is\n// fetched lazily on first play through the host adapter's authenticated\n// `fetchMediaBlob` — a plain <video src> can't attach the host's auth headers.\n\nimport { reactive, type ComputedRef } from \"vue\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { clearReactiveRecords, staleSince as staleSinceOf, type StoryRef } from \"../helpers\";\nimport type { MulmoScriptTransport } from \"../transport\";\nimport type { MulmoScriptHostAdapter } from \"../hostAdapter\";\n\nexport interface UseBeatMovieOptions {\n  api: MulmoScriptTransport;\n  adapter: MulmoScriptHostAdapter;\n  filePath: ComputedRef<string>;\n  /** Which registered root `filePath` is relative to; `undefined` = the host's default (#3014). */\n  root: ComputedRef<string | undefined>;\n}\n\nexport function useBeatMovie({ api, adapter, filePath, root }: UseBeatMovieOptions) {\n  const beatMovies = reactive<Record<number, string>>({});\n  const beatMovieUrls = reactive<Record<number, string>>({});\n  const beatMovieOpen = reactive<Record<number, boolean>>({});\n  const beatMovieLoading = reactive<Record<number, boolean>>({});\n\n  // The PAIR, not the path: `stories/deck.json` exists in every root (#3014).\n  const storyRef = (): StoryRef => ({ filePath: filePath.value, root: root.value });\n  const staleSince = (requested: StoryRef): boolean => staleSinceOf(storyRef(), requested);\n\n  async function loadExistingBeatMovie(index: number): Promise<void> {\n    const requested = storyRef();\n    const response = await api.call(\"beatMovie\", { ...requested, beatIndex: index });\n    if (staleSince(requested)) return;\n    // silently ignore errors — the clip simply hasn't been generated yet\n    if (response.ok && response.data.moviePath) {\n      beatMovies[index] = response.data.moviePath;\n    }\n  }\n\n  async function playBeatMovie(index: number): Promise<void> {\n    const fetchMediaBlob = adapter.fetchMediaBlob;\n    if (!fetchMediaBlob || !beatMovies[index] || beatMovieLoading[index]) return;\n    if (beatMovieUrls[index]) {\n      beatMovieOpen[index] = true;\n      return;\n    }\n    beatMovieLoading[index] = true;\n    try {\n      // Re-type the .mov blob as video/mp4 — same ISO-BMFF family, and\n      // <video> support for \"video/mp4\" is broader than \"video/quicktime\".\n      const blob = new Blob([await fetchMediaBlob({ moviePath: beatMovies[index], root: root.value })], { type: \"video/mp4\" });\n      beatMovieUrls[index] = URL.createObjectURL(blob);\n      beatMovieOpen[index] = true;\n    } catch (err) {\n      alert(errorMessage(err));\n    } finally {\n      Reflect.deleteProperty(beatMovieLoading, index);\n    }\n  }\n\n  function closeBeatMovie(index: number): void {\n    Reflect.deleteProperty(beatMovieOpen, index);\n  }\n\n  // Drop one beat's cached clip (regenerate is about to replace it on\n  // disk). Revoking the object URL frees the blob immediately.\n  function invalidateBeatMovie(index: number): void {\n    if (beatMovieUrls[index]) URL.revokeObjectURL(beatMovieUrls[index]);\n    [beatMovies, beatMovieUrls, beatMovieOpen].forEach((map) => Reflect.deleteProperty(map, index));\n  }\n\n  function resetBeatMovies(): void {\n    Object.values(beatMovieUrls).forEach((url) => URL.revokeObjectURL(url));\n    clearReactiveRecords(beatMovies, beatMovieUrls, beatMovieOpen, beatMovieLoading);\n  }\n\n  return {\n    beatMovies,\n    beatMovieUrls,\n    beatMovieOpen,\n    beatMovieLoading,\n    loadExistingBeatMovie,\n    playBeatMovie,\n    closeBeatMovie,\n    invalidateBeatMovie,\n    resetBeatMovies,\n  };\n}\n","// Character (imageParams.images) strip: thumbnails, drag-and-drop upload, and\n// render / generate-all for the `imagePrompt` characters a script references.\n// Characters must be rendered before the beats that use them, so the View\n// probes these on mount and after every beat render.\n\nimport { computed, reactive, type ComputedRef } from \"vue\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { characterPrompt as characterPromptOf, clearReactiveRecords, getMissingCharacterKeys, staleSince as staleSinceOf, type StoryRef } from \"../helpers\";\nimport { readFileAsDataUrl } from \"../support\";\nimport type { MulmoScriptTransport } from \"../transport\";\n\ntype CharRenderState = \"idle\" | \"rendering\" | \"done\" | \"error\";\n\ntype ScriptImages = Record<string, { type?: string; prompt?: string }> | undefined;\n\nexport interface UseCharacterImagesOptions {\n  api: MulmoScriptTransport;\n  filePath: ComputedRef<string>;\n  /** Which registered root `filePath` is relative to; `undefined` = the host's default (#3014). */\n  root: ComputedRef<string | undefined>;\n  chatSessionId: ComputedRef<string | undefined>;\n  getImages: () => ScriptImages;\n}\n\nexport function useCharacterImages({ api, filePath, root, chatSessionId, getImages }: UseCharacterImagesOptions) {\n  const charRenderState = reactive<Record<string, CharRenderState>>({});\n  const charImages = reactive<Record<string, string>>({});\n  const charErrors = reactive<Record<string, string>>({});\n  const charDragOver = reactive<Record<string, boolean>>({});\n\n  // The PAIR, not the path: `stories/deck.json` exists in every root (#3014).\n  const storyRef = (): StoryRef => ({ filePath: filePath.value, root: root.value });\n  const staleSince = (requested: StoryRef): boolean => staleSinceOf(storyRef(), requested);\n\n  const characterKeys = computed(() => {\n    const imgs = getImages() ?? {};\n    return Object.keys(imgs).filter((key) => imgs[key]?.type === \"imagePrompt\");\n  });\n\n  function characterPrompt(key: string): string {\n    return characterPromptOf(getImages(), key);\n  }\n\n  function onCharDragOver(event: DragEvent, key: string): void {\n    if (!event.dataTransfer?.types.includes(\"Files\")) return;\n    event.preventDefault();\n    charDragOver[key] = true;\n  }\n\n  function onCharDragLeave(key: string): void {\n    charDragOver[key] = false;\n  }\n\n  async function onCharDrop(event: DragEvent, key: string): Promise<void> {\n    event.preventDefault();\n    charDragOver[key] = false;\n    const file = event.dataTransfer?.files[0];\n    if (!file || !file.type.startsWith(\"image/\")) return;\n\n    charRenderState[key] = \"rendering\";\n    Reflect.deleteProperty(charErrors, key);\n    let imageData: string;\n    try {\n      imageData = await readFileAsDataUrl(file);\n    } catch (err) {\n      charErrors[key] = errorMessage(err);\n      charRenderState[key] = \"error\";\n      return;\n    }\n    const requested = storyRef();\n    const response = await api.call(\"uploadCharacterImage\", { ...requested, key, imageData });\n    if (staleSince(requested)) return;\n    if (!response.ok) {\n      charErrors[key] = response.error || \"Upload failed\";\n      charRenderState[key] = \"error\";\n      return;\n    }\n    charImages[key] = response.data.image ?? \"\";\n    charRenderState[key] = \"done\";\n  }\n\n  async function loadExistingCharacterImage(key: string): Promise<void> {\n    const requested = storyRef();\n    const response = await api.call(\"characterImage\", { ...requested, key });\n    if (staleSince(requested)) return;\n    // silently ignore errors\n    if (response.ok && response.data.image) {\n      charImages[key] = response.data.image;\n      charRenderState[key] = \"done\";\n    }\n  }\n\n  function refreshMissingCharacterImages(): void {\n    getMissingCharacterKeys(characterKeys.value, charImages, charRenderState).forEach((key) => loadExistingCharacterImage(key));\n  }\n\n  async function renderCharacter(key: string, force: boolean): Promise<void> {\n    const requested = storyRef();\n    charRenderState[key] = \"rendering\";\n    Reflect.deleteProperty(charErrors, key);\n    const response = await api.call(\"renderCharacter\", { ...requested, key, force, chatSessionId: chatSessionId.value });\n    if (staleSince(requested)) return;\n    if (!response.ok) {\n      charErrors[key] = response.error || \"Render failed\";\n      charRenderState[key] = \"error\";\n      return;\n    }\n    charImages[key] = response.data.image ?? \"\";\n    charRenderState[key] = \"done\";\n  }\n\n  async function generateAllCharacters(): Promise<void> {\n    await Promise.all(characterKeys.value.filter((key) => charRenderState[key] !== \"rendering\").map((key) => renderCharacter(key, false)));\n  }\n\n  function resetCharacters(): void {\n    clearReactiveRecords(charRenderState, charImages, charErrors, charDragOver);\n  }\n\n  return {\n    charRenderState,\n    charImages,\n    charErrors,\n    charDragOver,\n    characterKeys,\n    characterPrompt,\n    onCharDragOver,\n    onCharDragLeave,\n    onCharDrop,\n    loadExistingCharacterImage,\n    refreshMissingCharacterImages,\n    renderCharacter,\n    generateAllCharacters,\n    resetCharacters,\n  };\n}\n","// #1575 — the View offers the interactive beat editor (@mulmocast/beat-editor) beside the\n// per-beat list. Each editor emit fires\n// `update:script`; this debounces them into one updateScript round-trip per\n// quiet stretch (300ms — short enough to feel live, long enough that typing in\n// the Inspector doesn't carpet-bomb the server).\n\nimport { computed, ref, type ComputedRef, type Ref } from \"vue\";\nimport { hasEditableBeats } from \"../helpers\";\nimport type { MulmoScriptDispatchResult } from \"../../core/contract\";\nimport type { MulmoScriptTransport, TransportResult } from \"../transport\";\nimport type { DeckScriptShape, MulmoScript } from \"../viewTypes\";\n\nconst DECK_SAVE_DEBOUNCE_MS = 300;\n\n/**\n * Who this editor is, on the wire.\n *\n * Every write carries it so the server's \"this script changed\" broadcast can be told apart\n * from someone else's. Without it a save would echo back and reload the editor mid-keystroke,\n * rebuilding the element the caret sits in.\n *\n * Per module instance rather than per component: one View is mounted at a time, and a value\n * that survives a remount keeps a save in flight from being mistaken for a foreign write.\n */\nconst EDITOR_ORIGIN = `deck-editor-${Math.random().toString(36).slice(2)}`;\n\n/**\n * The slice of the transport this composable uses.\n *\n * `MulmoScriptTransport[\"call\"]` is generic in the dispatch kind, so a fake standing in for it\n * has to answer every kind — impossible to write without a cast. Naming the ONE call made here\n * is what lets the save path (and its failure) be tested; the real transport satisfies this\n * structurally, so the View passes the same object it always did.\n */\nexport type DeckEditorTransport = Pick<MulmoScriptTransport, \"onScriptChanged\"> & {\n  call(\n    kind: \"updateScript\",\n    args: { filePath: string; root?: string | undefined; script: MulmoScript; origin: string },\n  ): Promise<TransportResult<MulmoScriptDispatchResult[\"updateScript\"]>>;\n};\n\nexport interface UseDeckEditorOptions {\n  api: DeckEditorTransport;\n  filePath: ComputedRef<string>;\n  /** Which registered root `filePath` is relative to; `undefined` = the host's default (#3014). */\n  root: ComputedRef<string | undefined>;\n  effectiveScript: ComputedRef<MulmoScript>;\n  /** Persist the saved script back into the parent's toolResult so the\n   *  in-memory script and reactive beats[] stay in sync without a remount. */\n  commitScript: (next: MulmoScript) => void;\n}\n\nexport function useDeckEditor({ api, filePath, root, effectiveScript, commitScript }: UseDeckEditorOptions) {\n  const canEditBeats = computed(() => hasEditableBeats(effectiveScript.value));\n  const deckScriptInput = computed<DeckScriptShape>(() => effectiveScript.value as unknown as DeckScriptShape);\n\n  let deckSaveTimer: ReturnType<typeof setTimeout> | null = null;\n  let pendingDeckScript: MulmoScript | null = null;\n\n  /**\n   * The last save that failed, in the server's own words — or null.\n   *\n   * A failed save leaves the editor showing the user's edit (see `flushDeckSave`), which is\n   * right for a transient failure and indistinguishable from success without this: #3070 was\n   * filed after edits that only reverted on the next reload. Within one script the only thing\n   * that clears it is the next SUCCESSFUL save — never the next keystroke, which would blank\n   * the message for the debounce window and then bring it back, and an edit that is still\n   * unsaved has not stopped being unsaved. Leaving the script clears it too, for a different\n   * reason: see `resetForScriptChange`.\n   */\n  const deckSaveError: Ref<string | null> = ref(null);\n\n  /**\n   * Which edit the in-flight save is answering for.\n   *\n   * Advanced when an edit is QUEUED, not when its save is dispatched — those are up to 300ms\n   * apart, and a write can outlive the gap (the failing kind is the slow kind: a timeout costs\n   * the whole budget). An answer about superseded content must not be acted on either way\n   * round: committing it puts the older script back over what the user is typing, and its\n   * verdict is about text that is no longer on screen — a green light for an edit that never\n   * reached the server, or a red banner for one already replaced.\n   */\n  let editRevision = 0;\n\n  function scheduleDeckSave(next: MulmoScript): void {\n    pendingDeckScript = next;\n    editRevision += 1;\n    if (deckSaveTimer) clearTimeout(deckSaveTimer);\n    deckSaveTimer = setTimeout(() => {\n      void flushDeckSave();\n    }, DECK_SAVE_DEBOUNCE_MS);\n  }\n\n  async function flushDeckSave(): Promise<void> {\n    deckSaveTimer = null;\n    const next = pendingDeckScript;\n    pendingDeckScript = null;\n    if (!next || !filePath.value) return;\n    const revision = editRevision;\n    const response = await api.call(\"updateScript\", { filePath: filePath.value, root: root.value, script: next, origin: EDITOR_ORIGIN });\n    if (revision !== editRevision) return;\n    if (!response.ok) {\n      // The deck editor still holds the latest edit in its props until the next refresh, so\n      // the view doesn't snap back on a transient failure — which is why the failure has to be\n      // said out loud (#3070). Console too: it is where the existing bug reports start.\n      deckSaveError.value = response.error;\n      console.error(\"[presentMulmoScript] deck save failed:\", response.error);\n      return;\n    }\n    deckSaveError.value = null;\n    commitScript(next);\n  }\n\n  function onDeckUpdate(next: DeckScriptShape): void {\n    scheduleDeckSave(next as unknown as MulmoScript);\n  }\n\n  // Flush synchronously-scheduled work on unmount so a quick switch away\n  // doesn't lose the last keystroke. Fire-and-forget — the component is gone,\n  // we just want the bytes to land.\n  function flushPendingDeckSave(): void {\n    if (deckSaveTimer) {\n      clearTimeout(deckSaveTimer);\n      void flushDeckSave();\n    }\n  }\n\n  /**\n   * Reload when someone else writes this script — the agent, or another window.\n   *\n   * A pending local edit is flushed first rather than dropped: the user's keystrokes are the\n   * thing they would notice losing, and the write that triggered this has already landed, so\n   * flushing cannot clobber it out of order.\n   */\n  function watchForeignWrites(reload: () => void): () => void {\n    return api.onScriptChanged({\n      filePath: () => filePath.value,\n      // The PAIR is the identity: `stories/deck.json` exists in every root, so filtering on the\n      // path alone reloads this editor when ANOTHER repository's same-named deck is written\n      // (#3014).\n      root: () => root.value,\n      ownOrigin: EDITOR_ORIGIN,\n      handler: () => {\n        flushPendingDeckSave();\n        reload();\n      },\n    });\n  }\n\n  /**\n   * The script this composable was editing is gone — the View moved to a different result, or\n   * this one was rewritten whole by another route.\n   *\n   * Clearing the banner is not enough, because this View re-initializes in place rather than\n   * remounting (`watch(() => props.selectedResult, initializeScript)`), so everything the old\n   * script left behind survives the switch and lands on the new one:\n   *\n   * - an answer still IN FLIGHT would repopulate the banner, or commit the old script into the\n   *   new result — its revision is still current, since no new edit has been queued;\n   * - an edit still QUEUED would be written out by its own timer against `filePath.value`,\n   *   which by then names the NEW file. That one writes one deck's beats into another deck.\n   *\n   * So the revision advances (every in-flight answer becomes stale) and the queue is dropped.\n   * The per-beat errors beside this are reset the same way, in the same function.\n   */\n  function resetForScriptChange(): void {\n    if (deckSaveTimer) clearTimeout(deckSaveTimer);\n    deckSaveTimer = null;\n    pendingDeckScript = null;\n    editRevision += 1;\n    deckSaveError.value = null;\n  }\n\n  return { canEditBeats, deckScriptInput, deckSaveError, resetForScriptChange, onDeckUpdate, flushPendingDeckSave, watchForeignWrites };\n}\n","import type { Messages } from \"./messages\";\n\nconst de: Messages = {\n  beatCount: (count) => (count === 1 ? `${count} Beat` : `${count} Beats`),\n  editTab: \"Bearbeiten\",\n  mediaTab: \"Medien\",\n  movie: \"Video\",\n  generating: \"Wird generiert…\",\n  rendering: \"Wird gerendert…\",\n  saving: \"Wird gespeichert…\",\n  update: \"Aktualisieren\",\n  characters: \"Charaktere\",\n  drop: \"Ablegen\",\n  gen: \"Generieren\",\n  play: \"▶ Abspielen\",\n  stop: \"■ Stoppen\",\n  playPresentation: \"Präsentation abspielen\",\n  regenerateMovie: \"Video neu generieren\",\n  movieGenerationFailed: \"Videoerstellung fehlgeschlagen\",\n  pdf: \"PDF\",\n  regeneratePdf: \"PDF neu generieren\",\n  generatingPdf: \"PDF wird erstellt…\",\n  retry: \"Erneut versuchen\",\n  errPrefix: \"⚠ Fehler\",\n  noBeats: \"Keine Beats im Skript gefunden\",\n  editSource: \"Skript-Quelle bearbeiten\",\n  applyChanges: \"Änderungen übernehmen\",\n  generateAll: \"Alle generieren\",\n  orDropImage: \"oder Bild ablegen\",\n  generate: \"Generieren\",\n  generateAudio: \"♪ Generieren\",\n  saveErrorInvalidJson: (error) => `⚠ Ungültiges JSON: ${error}`,\n  saveErrorSaveFailed: (error) => `⚠ Speichern fehlgeschlagen: ${error}`,\n  close: \"Schließen\",\n  cancel: \"Abbrechen\",\n};\n\nexport default de;\n","import type { Messages } from \"./messages\";\n\nconst en: Messages = {\n  beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n  editTab: \"Edit\",\n  mediaTab: \"Media\",\n  movie: \"Movie\",\n  generating: \"Generating…\",\n  rendering: \"Rendering…\",\n  saving: \"Saving…\",\n  update: \"Update\",\n  characters: \"Characters\",\n  drop: \"Drop\",\n  gen: \"Gen\",\n  play: \"▶ Play\",\n  stop: \"■ Stop\",\n  playPresentation: \"Play presentation\",\n  regenerateMovie: \"Regenerate movie\",\n  movieGenerationFailed: \"Movie generation failed\",\n  pdf: \"PDF\",\n  regeneratePdf: \"Regenerate PDF\",\n  generatingPdf: \"Generating PDF…\",\n  retry: \"Retry\",\n  errPrefix: \"⚠ Error\",\n  noBeats: \"No beats found in script\",\n  editSource: \"Edit Script Source\",\n  applyChanges: \"Apply Changes\",\n  generateAll: \"Generate All\",\n  orDropImage: \"or drop image\",\n  generate: \"Generate\",\n  generateAudio: \"♪ Generate\",\n  saveErrorInvalidJson: (error) => `⚠ Invalid JSON: ${error}`,\n  saveErrorSaveFailed: (error) => `⚠ Save failed: ${error}`,\n  close: \"Close\",\n  cancel: \"Cancel\",\n};\n\nexport default en;\n","import type { Messages } from \"./messages\";\n\nconst es: Messages = {\n  beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n  editTab: \"Editar\",\n  mediaTab: \"Medios\",\n  movie: \"Vídeo\",\n  generating: \"Generando…\",\n  rendering: \"Renderizando…\",\n  saving: \"Guardando…\",\n  update: \"Actualizar\",\n  characters: \"Personajes\",\n  drop: \"Soltar\",\n  gen: \"Generar\",\n  play: \"▶ Reproducir\",\n  stop: \"■ Detener\",\n  playPresentation: \"Reproducir presentación\",\n  regenerateMovie: \"Regenerar vídeo\",\n  movieGenerationFailed: \"Error al generar el vídeo\",\n  pdf: \"PDF\",\n  regeneratePdf: \"Regenerar PDF\",\n  generatingPdf: \"Generando PDF…\",\n  retry: \"Reintentar\",\n  errPrefix: \"⚠ Error\",\n  noBeats: \"No se encontraron beats en el script\",\n  editSource: \"Editar fuente del script\",\n  applyChanges: \"Aplicar cambios\",\n  generateAll: \"Generar todo\",\n  orDropImage: \"o arrastra una imagen\",\n  generate: \"Generar\",\n  generateAudio: \"♪ Generar\",\n  saveErrorInvalidJson: (error) => `⚠ JSON no válido: ${error}`,\n  saveErrorSaveFailed: (error) => `⚠ Error al guardar: ${error}`,\n  close: \"Cerrar\",\n  cancel: \"Cancelar\",\n};\n\nexport default es;\n","import type { Messages } from \"./messages\";\n\nconst fr: Messages = {\n  beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n  editTab: \"Éditer\",\n  mediaTab: \"Médias\",\n  movie: \"Film\",\n  generating: \"Génération…\",\n  rendering: \"Rendu…\",\n  saving: \"Enregistrement…\",\n  update: \"Mettre à jour\",\n  characters: \"Personnages\",\n  drop: \"Déposer\",\n  gen: \"Générer\",\n  play: \"▶ Lire\",\n  stop: \"■ Arrêter\",\n  playPresentation: \"Lire la présentation\",\n  regenerateMovie: \"Régénérer la vidéo\",\n  movieGenerationFailed: \"Échec de la génération de la vidéo\",\n  pdf: \"PDF\",\n  regeneratePdf: \"Régénérer le PDF\",\n  generatingPdf: \"Génération du PDF…\",\n  retry: \"Réessayer\",\n  errPrefix: \"⚠ Erreur\",\n  noBeats: \"Aucun beat trouvé dans le script\",\n  editSource: \"Modifier la source du script\",\n  applyChanges: \"Appliquer les modifications\",\n  generateAll: \"Tout générer\",\n  orDropImage: \"ou déposez une image\",\n  generate: \"Générer\",\n  generateAudio: \"♪ Générer\",\n  saveErrorInvalidJson: (error) => `⚠ JSON invalide : ${error}`,\n  saveErrorSaveFailed: (error) => `⚠ Échec de la sauvegarde : ${error}`,\n  close: \"Fermer\",\n  cancel: \"Annuler\",\n};\n\nexport default fr;\n","import type { Messages } from \"./messages\";\n\nconst ja: Messages = {\n  beatCount: (count) => `${count} ビート`,\n  editTab: \"編集\",\n  mediaTab: \"メディア\",\n  movie: \"動画\",\n  generating: \"生成中…\",\n  rendering: \"レンダリング中…\",\n  saving: \"保存中…\",\n  update: \"更新\",\n  characters: \"キャラクター\",\n  drop: \"ドロップ\",\n  gen: \"生成\",\n  play: \"▶ 再生\",\n  stop: \"■ 停止\",\n  playPresentation: \"プレゼンテーション再生\",\n  regenerateMovie: \"動画を再生成\",\n  movieGenerationFailed: \"動画の生成に失敗しました\",\n  pdf: \"PDF\",\n  regeneratePdf: \"PDF を再生成\",\n  generatingPdf: \"PDF を生成中…\",\n  retry: \"再試行\",\n  errPrefix: \"⚠ エラー\",\n  noBeats: \"スクリプトにビートが見つかりません\",\n  editSource: \"スクリプトソースを編集\",\n  applyChanges: \"変更を適用\",\n  generateAll: \"すべて生成\",\n  orDropImage: \"画像をドロップ\",\n  generate: \"生成\",\n  generateAudio: \"♪ 生成\",\n  saveErrorInvalidJson: (error) => `⚠ 不正な JSON: ${error}`,\n  saveErrorSaveFailed: (error) => `⚠ 保存失敗: ${error}`,\n  close: \"閉じる\",\n  cancel: \"キャンセル\",\n};\n\nexport default ja;\n","import type { Messages } from \"./messages\";\n\nconst ko: Messages = {\n  beatCount: (count) => `${count}개 비트`,\n  editTab: \"편집\",\n  mediaTab: \"미디어\",\n  movie: \"영상\",\n  generating: \"생성 중…\",\n  rendering: \"렌더링 중…\",\n  saving: \"저장 중…\",\n  update: \"업데이트\",\n  characters: \"캐릭터\",\n  drop: \"드롭\",\n  gen: \"생성\",\n  play: \"▶ 재생\",\n  stop: \"■ 정지\",\n  playPresentation: \"프레젠테이션 재생\",\n  regenerateMovie: \"동영상 재생성\",\n  movieGenerationFailed: \"동영상 생성에 실패했습니다\",\n  pdf: \"PDF\",\n  regeneratePdf: \"PDF 재생성\",\n  generatingPdf: \"PDF 생성 중…\",\n  retry: \"다시 시도\",\n  errPrefix: \"⚠ 오류\",\n  noBeats: \"스크립트에서 비트를 찾을 수 없습니다\",\n  editSource: \"스크립트 원본 편집\",\n  applyChanges: \"변경 사항 적용\",\n  generateAll: \"전체 생성\",\n  orDropImage: \"또는 이미지 드롭\",\n  generate: \"생성\",\n  generateAudio: \"♪ 생성\",\n  saveErrorInvalidJson: (error) => `⚠ 잘못된 JSON: ${error}`,\n  saveErrorSaveFailed: (error) => `⚠ 저장 실패: ${error}`,\n  close: \"닫기\",\n  cancel: \"취소\",\n};\n\nexport default ko;\n","import type { Messages } from \"./messages\";\n\nconst ptBR: Messages = {\n  beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n  editTab: \"Editar\",\n  mediaTab: \"Mídia\",\n  movie: \"Vídeo\",\n  generating: \"Gerando…\",\n  rendering: \"Renderizando…\",\n  saving: \"Salvando…\",\n  update: \"Atualizar\",\n  characters: \"Personagens\",\n  drop: \"Soltar\",\n  gen: \"Gerar\",\n  play: \"▶ Reproduzir\",\n  stop: \"■ Parar\",\n  playPresentation: \"Reproduzir apresentação\",\n  regenerateMovie: \"Regenerar vídeo\",\n  movieGenerationFailed: \"Falha ao gerar o vídeo\",\n  pdf: \"PDF\",\n  regeneratePdf: \"Regenerar PDF\",\n  generatingPdf: \"Gerando PDF…\",\n  retry: \"Tentar novamente\",\n  errPrefix: \"⚠ Erro\",\n  noBeats: \"Nenhum beat encontrado no script\",\n  editSource: \"Editar fonte do script\",\n  applyChanges: \"Aplicar alterações\",\n  generateAll: \"Gerar tudo\",\n  orDropImage: \"ou solte uma imagem\",\n  generate: \"Gerar\",\n  generateAudio: \"♪ Gerar\",\n  saveErrorInvalidJson: (error) => `⚠ JSON inválido: ${error}`,\n  saveErrorSaveFailed: (error) => `⚠ Falha ao salvar: ${error}`,\n  close: \"Fechar\",\n  cancel: \"Cancelar\",\n};\n\nexport default ptBR;\n","import type { Messages } from \"./messages\";\n\nconst zh: Messages = {\n  beatCount: (count) => `${count} 个 beat`,\n  editTab: \"编辑\",\n  mediaTab: \"媒体\",\n  movie: \"视频\",\n  generating: \"生成中…\",\n  rendering: \"渲染中…\",\n  saving: \"保存中…\",\n  update: \"更新\",\n  characters: \"角色\",\n  drop: \"拖放\",\n  gen: \"生成\",\n  play: \"▶ 播放\",\n  stop: \"■ 停止\",\n  playPresentation: \"播放演示\",\n  regenerateMovie: \"重新生成视频\",\n  movieGenerationFailed: \"视频生成失败\",\n  pdf: \"PDF\",\n  regeneratePdf: \"重新生成 PDF\",\n  generatingPdf: \"生成 PDF…\",\n  retry: \"重试\",\n  errPrefix: \"⚠ 错误\",\n  noBeats: \"脚本中没有找到 beat\",\n  editSource: \"编辑脚本源\",\n  applyChanges: \"应用更改\",\n  generateAll: \"全部生成\",\n  orDropImage: \"或拖入图片\",\n  generate: \"生成\",\n  generateAudio: \"♪ 生成\",\n  saveErrorInvalidJson: (error) => `⚠ JSON 无效: ${error}`,\n  saveErrorSaveFailed: (error) => `⚠ 保存失败: ${error}`,\n  close: \"关闭\",\n  cancel: \"取消\",\n};\n\nexport default zh;\n","import { createUseT } from \"gui-chat-protocol/vue\";\nimport type { Messages } from \"./messages\";\nimport de from \"./de\";\nimport en from \"./en\";\nimport es from \"./es\";\nimport fr from \"./fr\";\nimport ja from \"./ja\";\nimport ko from \"./ko\";\nimport ptBR from \"./ptBR\";\nimport zh from \"./zh\";\n\nconst MESSAGES = { de, en, es, fr, ja, ko, \"pt-BR\": ptBR, zh } as const;\n\n/** Reactive message bundle for the active host locale. The plugin carries its\n *  own translations (no host i18n dependency); it reads the locale off the\n *  injected `BrowserPluginRuntime.locale` ref and falls back to English.\n *  Same pattern as @mulmoclaude/html-plugin. */\nexport const useT = createUseT(MESSAGES);\n\nexport type { Messages };\n","<template>\n  <div class=\"fixed inset-0 z-50 bg-black/80 overflow-y-auto\" @click=\"emit('close')\">\n    <button class=\"fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none\" :title=\"m.close\" @click.stop=\"emit('close')\">✕</button>\n    <div class=\"flex flex-col items-center gap-4 pt-4 pb-8\" @click.stop>\n      <div class=\"flex items-center gap-4\">\n        <button\n          v-if=\"!lightbox.isCharacter\"\n          class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n          :disabled=\"!hasPrev\"\n          @click=\"emit('move', -1)\"\n        >\n          ‹\n        </button>\n        <div class=\"flex flex-col items-center\">\n          <img :src=\"lightbox.src\" class=\"max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl\" />\n          <div v-if=\"!lightbox.isCharacter && beatCount > 1\" class=\"relative w-full h-1\">\n            <div class=\"flex gap-1 h-full\">\n              <div\n                v-for=\"i in beatCount\"\n                :key=\"i - 1\"\n                class=\"group flex-1 cursor-pointer relative transition-colors\"\n                :class=\"\n                  i - 1 === lightbox.index\n                    ? 'bg-white/80 hover:bg-white'\n                    : i - 1 < lightbox.index\n                      ? 'bg-white/40 hover:bg-white/60'\n                      : 'bg-white/20 hover:bg-white/40'\n                \"\n                @click=\"emit('jump', i - 1)\"\n              >\n                <span class=\"absolute -inset-y-3 inset-x-0\" />\n                <div\n                  v-if=\"beatTooltip(beatTexts[i - 1])\"\n                  class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 px-2 py-1 rounded bg-black/90 text-white text-xs leading-tight w-48 max-h-[53px] overflow-hidden opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity\"\n                >\n                  {{ beatTooltip(beatTexts[i - 1]) }}\n                </div>\n              </div>\n            </div>\n            <div\n              v-if=\"playingAudioIndex !== null && playingAudioIndex === lightbox.index\"\n              class=\"absolute top-1/2 w-3.5 h-3.5 rounded-full bg-white shadow ring-2 ring-black/30 -translate-y-1/2 -translate-x-1/2 pointer-events-none\"\n              :style=\"{ left: `${((lightbox.index + audioProgress) / beatCount) * 100}%` }\"\n            />\n          </div>\n        </div>\n        <button\n          v-if=\"!lightbox.isCharacter\"\n          class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n          :disabled=\"!hasNext\"\n          @click=\"emit('move', 1)\"\n        >\n          ›\n        </button>\n      </div>\n      <div v-if=\"lightbox.text || hasCurrentAudio\" class=\"relative w-screen flex justify-center px-16\">\n        <p v-if=\"lightbox.text\" class=\"max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]\">\n          {{ lightbox.text }}\n        </p>\n        <button\n          v-if=\"hasCurrentAudio\"\n          class=\"absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20\"\n          @click=\"emit('playAudio', lightbox.index)\"\n        >\n          {{ playingAudioIndex === lightbox.index ? m.stop : m.play }}\n        </button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\n// Full-screen beat / character image viewer with the beat strip, prev-next\n// arrows and the narration Play control. Pure Tailwind — the parent's\n// `<style scoped>` block only targets the bottom-bar region, so nothing here\n// relies on styles that stop at the component boundary.\n//\n// The parent owns `v-if=\"lightbox\"`, so `lightbox` is never null in here and\n// the template can read `.index` / `.src` without a guard.\nimport { beatTooltip } from \"../helpers\";\nimport type { LightboxState } from \"../viewTypes\";\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\ndefineProps<{\n  lightbox: LightboxState;\n  beatCount: number;\n  beatTexts: (string | undefined)[];\n  hasPrev: boolean;\n  hasNext: boolean;\n  playingAudioIndex: number | null;\n  audioProgress: number;\n  hasCurrentAudio: boolean;\n}>();\n\nconst emit = defineEmits<{\n  close: [];\n  move: [delta: number];\n  jump: [index: number];\n  playAudio: [index: number];\n}>();\n</script>\n","<template>\n  <div class=\"fixed inset-0 z-50 bg-black/80 overflow-y-auto\" @click=\"emit('close')\">\n    <button class=\"fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none\" :title=\"m.close\" @click.stop=\"emit('close')\">✕</button>\n    <div class=\"flex flex-col items-center gap-4 pt-4 pb-8\" @click.stop>\n      <div class=\"flex items-center gap-4\">\n        <button\n          v-if=\"!lightbox.isCharacter\"\n          class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n          :disabled=\"!hasPrev\"\n          @click=\"emit('move', -1)\"\n        >\n          ‹\n        </button>\n        <div class=\"flex flex-col items-center\">\n          <img :src=\"lightbox.src\" class=\"max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl\" />\n          <div v-if=\"!lightbox.isCharacter && beatCount > 1\" class=\"relative w-full h-1\">\n            <div class=\"flex gap-1 h-full\">\n              <div\n                v-for=\"i in beatCount\"\n                :key=\"i - 1\"\n                class=\"group flex-1 cursor-pointer relative transition-colors\"\n                :class=\"\n                  i - 1 === lightbox.index\n                    ? 'bg-white/80 hover:bg-white'\n                    : i - 1 < lightbox.index\n                      ? 'bg-white/40 hover:bg-white/60'\n                      : 'bg-white/20 hover:bg-white/40'\n                \"\n                @click=\"emit('jump', i - 1)\"\n              >\n                <span class=\"absolute -inset-y-3 inset-x-0\" />\n                <div\n                  v-if=\"beatTooltip(beatTexts[i - 1])\"\n                  class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 px-2 py-1 rounded bg-black/90 text-white text-xs leading-tight w-48 max-h-[53px] overflow-hidden opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity\"\n                >\n                  {{ beatTooltip(beatTexts[i - 1]) }}\n                </div>\n              </div>\n            </div>\n            <div\n              v-if=\"playingAudioIndex !== null && playingAudioIndex === lightbox.index\"\n              class=\"absolute top-1/2 w-3.5 h-3.5 rounded-full bg-white shadow ring-2 ring-black/30 -translate-y-1/2 -translate-x-1/2 pointer-events-none\"\n              :style=\"{ left: `${((lightbox.index + audioProgress) / beatCount) * 100}%` }\"\n            />\n          </div>\n        </div>\n        <button\n          v-if=\"!lightbox.isCharacter\"\n          class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n          :disabled=\"!hasNext\"\n          @click=\"emit('move', 1)\"\n        >\n          ›\n        </button>\n      </div>\n      <div v-if=\"lightbox.text || hasCurrentAudio\" class=\"relative w-screen flex justify-center px-16\">\n        <p v-if=\"lightbox.text\" class=\"max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]\">\n          {{ lightbox.text }}\n        </p>\n        <button\n          v-if=\"hasCurrentAudio\"\n          class=\"absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20\"\n          @click=\"emit('playAudio', lightbox.index)\"\n        >\n          {{ playingAudioIndex === lightbox.index ? m.stop : m.play }}\n        </button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\n// Full-screen beat / character image viewer with the beat strip, prev-next\n// arrows and the narration Play control. Pure Tailwind — the parent's\n// `<style scoped>` block only targets the bottom-bar region, so nothing here\n// relies on styles that stop at the component boundary.\n//\n// The parent owns `v-if=\"lightbox\"`, so `lightbox` is never null in here and\n// the template can read `.index` / `.src` without a guard.\nimport { beatTooltip } from \"../helpers\";\nimport type { LightboxState } from \"../viewTypes\";\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\ndefineProps<{\n  lightbox: LightboxState;\n  beatCount: number;\n  beatTexts: (string | undefined)[];\n  hasPrev: boolean;\n  hasNext: boolean;\n  playingAudioIndex: number | null;\n  audioProgress: number;\n  hasCurrentAudio: boolean;\n}>();\n\nconst emit = defineEmits<{\n  close: [];\n  move: [delta: number];\n  jump: [index: number];\n  playAudio: [index: number];\n}>();\n</script>\n","<template>\n  <div class=\"border-b border-gray-100 shrink-0 px-4 py-3\">\n    <div class=\"flex items-center justify-between mb-2\">\n      <span class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ m.characters }}</span>\n      <button\n        class=\"px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n        :disabled=\"busy || characterKeys.every((key) => renderState[key] === 'rendering')\"\n        @click=\"emit('generateAll')\"\n      >\n        {{ m.generateAll }}\n      </button>\n    </div>\n    <div class=\"flex gap-3 flex-wrap\">\n      <div v-for=\"key in characterKeys\" :key=\"key\" class=\"flex flex-col items-center gap-1 w-36\">\n        <!-- Character thumbnail -->\n        <div\n          class=\"relative w-36 h-36 rounded-lg border overflow-hidden bg-gray-50 flex items-center justify-center transition-colors\"\n          :class=\"dragOver[key] ? 'border-blue-400 bg-blue-50' : 'border-gray-200'\"\n          @dragover=\"emit('charDragOver', $event, key)\"\n          @dragleave=\"emit('charDragLeave', key)\"\n          @drop=\"emit('charDrop', $event, key)\"\n        >\n          <img v-if=\"thumbnails[key]\" :src=\"thumbnails[key]\" class=\"w-full h-full object-cover cursor-zoom-in\" :alt=\"key\" @click=\"emit('openLightbox', key)\" />\n          <template v-else-if=\"renderState[key] === 'rendering'\">\n            <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n              <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n              <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n            </svg>\n          </template>\n          <template v-else-if=\"renderState[key] === 'error'\">\n            <span class=\"text-xs text-red-400 text-center px-1\">{{ errors[key] }}</span>\n          </template>\n          <template v-else>\n            <span class=\"text-xs text-gray-300 text-center px-1 leading-tight\">{{ characterPrompt(images, key) }}</span>\n          </template>\n          <!-- Permanent drop hint -->\n          <div v-if=\"!dragOver[key]\" class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\">\n            {{ m.orDropImage }}\n          </div>\n          <!-- Drop overlay -->\n          <div v-if=\"dragOver[key]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n            <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n          </div>\n          <!-- Regenerate button -->\n          <button\n            v-if=\"thumbnails[key] && renderState[key] !== 'rendering'\"\n            class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n            :class=\"busy ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-gray-400 text-gray-600 hover:bg-gray-50'\"\n            :disabled=\"busy\"\n            @click.stop=\"emit('renderCharacter', key, true)\"\n          >\n            <span v-if=\"busy\" class=\"inline-block animate-spin\">↺</span>\n            <span v-else>↺</span>\n          </button>\n          <!-- Generate button -->\n          <button\n            v-else-if=\"!thumbnails[key] && renderState[key] !== 'rendering'\"\n            class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n            :class=\"busy ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-blue-400 text-blue-600 hover:bg-blue-50'\"\n            :disabled=\"busy\"\n            @click.stop=\"emit('renderCharacter', key, false)\"\n          >\n            <svg v-if=\"busy\" class=\"animate-spin w-3 h-3\" viewBox=\"0 0 24 24\" fill=\"none\">\n              <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n              <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n            </svg>\n            <span v-else>{{ m.gen }}</span>\n          </button>\n        </div>\n        <span class=\"text-xs text-gray-600 text-center truncate w-full\">{{ key }}</span>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\n// `imageParams.images` character thumbnails: drag-and-drop upload, per-character\n// render, and generate-all. Pure Tailwind — the parent's `<style scoped>` block\n// only targets the bottom-bar region, so nothing here relies on styles that stop\n// at the component boundary. The `char` prefix the parent uses to disambiguate\n// character state from beat state is redundant inside this component.\nimport { computed } from \"vue\";\nimport { characterPrompt } from \"../helpers\";\nimport type { ImageEntry } from \"../viewTypes\";\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\nconst props = defineProps<{\n  characterKeys: string[];\n  images: Record<string, ImageEntry> | undefined;\n  thumbnails: Record<string, string>;\n  renderState: Record<string, string>;\n  errors: Record<string, string>;\n  dragOver: Record<string, boolean>;\n  movieGenerating: boolean;\n  anyBeatRendering: boolean;\n}>();\n\nconst emit = defineEmits<{\n  generateAll: [];\n  charDragOver: [event: DragEvent, key: string];\n  charDragLeave: [key: string];\n  charDrop: [event: DragEvent, key: string];\n  openLightbox: [key: string];\n  renderCharacter: [key: string, force: boolean];\n}>();\n\n// A movie render or any in-flight beat render locks every per-character\n// action — the generated frames must not change underneath them.\nconst busy = computed(() => props.movieGenerating || props.anyBeatRendering);\n</script>\n","<template>\n  <div class=\"border-b border-gray-100 shrink-0 px-4 py-3\">\n    <div class=\"flex items-center justify-between mb-2\">\n      <span class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ m.characters }}</span>\n      <button\n        class=\"px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n        :disabled=\"busy || characterKeys.every((key) => renderState[key] === 'rendering')\"\n        @click=\"emit('generateAll')\"\n      >\n        {{ m.generateAll }}\n      </button>\n    </div>\n    <div class=\"flex gap-3 flex-wrap\">\n      <div v-for=\"key in characterKeys\" :key=\"key\" class=\"flex flex-col items-center gap-1 w-36\">\n        <!-- Character thumbnail -->\n        <div\n          class=\"relative w-36 h-36 rounded-lg border overflow-hidden bg-gray-50 flex items-center justify-center transition-colors\"\n          :class=\"dragOver[key] ? 'border-blue-400 bg-blue-50' : 'border-gray-200'\"\n          @dragover=\"emit('charDragOver', $event, key)\"\n          @dragleave=\"emit('charDragLeave', key)\"\n          @drop=\"emit('charDrop', $event, key)\"\n        >\n          <img v-if=\"thumbnails[key]\" :src=\"thumbnails[key]\" class=\"w-full h-full object-cover cursor-zoom-in\" :alt=\"key\" @click=\"emit('openLightbox', key)\" />\n          <template v-else-if=\"renderState[key] === 'rendering'\">\n            <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n              <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n              <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n            </svg>\n          </template>\n          <template v-else-if=\"renderState[key] === 'error'\">\n            <span class=\"text-xs text-red-400 text-center px-1\">{{ errors[key] }}</span>\n          </template>\n          <template v-else>\n            <span class=\"text-xs text-gray-300 text-center px-1 leading-tight\">{{ characterPrompt(images, key) }}</span>\n          </template>\n          <!-- Permanent drop hint -->\n          <div v-if=\"!dragOver[key]\" class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\">\n            {{ m.orDropImage }}\n          </div>\n          <!-- Drop overlay -->\n          <div v-if=\"dragOver[key]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n            <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n          </div>\n          <!-- Regenerate button -->\n          <button\n            v-if=\"thumbnails[key] && renderState[key] !== 'rendering'\"\n            class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n            :class=\"busy ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-gray-400 text-gray-600 hover:bg-gray-50'\"\n            :disabled=\"busy\"\n            @click.stop=\"emit('renderCharacter', key, true)\"\n          >\n            <span v-if=\"busy\" class=\"inline-block animate-spin\">↺</span>\n            <span v-else>↺</span>\n          </button>\n          <!-- Generate button -->\n          <button\n            v-else-if=\"!thumbnails[key] && renderState[key] !== 'rendering'\"\n            class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n            :class=\"busy ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-blue-400 text-blue-600 hover:bg-blue-50'\"\n            :disabled=\"busy\"\n            @click.stop=\"emit('renderCharacter', key, false)\"\n          >\n            <svg v-if=\"busy\" class=\"animate-spin w-3 h-3\" viewBox=\"0 0 24 24\" fill=\"none\">\n              <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n              <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n            </svg>\n            <span v-else>{{ m.gen }}</span>\n          </button>\n        </div>\n        <span class=\"text-xs text-gray-600 text-center truncate w-full\">{{ key }}</span>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\n// `imageParams.images` character thumbnails: drag-and-drop upload, per-character\n// render, and generate-all. Pure Tailwind — the parent's `<style scoped>` block\n// only targets the bottom-bar region, so nothing here relies on styles that stop\n// at the component boundary. The `char` prefix the parent uses to disambiguate\n// character state from beat state is redundant inside this component.\nimport { computed } from \"vue\";\nimport { characterPrompt } from \"../helpers\";\nimport type { ImageEntry } from \"../viewTypes\";\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\nconst props = defineProps<{\n  characterKeys: string[];\n  images: Record<string, ImageEntry> | undefined;\n  thumbnails: Record<string, string>;\n  renderState: Record<string, string>;\n  errors: Record<string, string>;\n  dragOver: Record<string, boolean>;\n  movieGenerating: boolean;\n  anyBeatRendering: boolean;\n}>();\n\nconst emit = defineEmits<{\n  generateAll: [];\n  charDragOver: [event: DragEvent, key: string];\n  charDragLeave: [key: string];\n  charDrop: [event: DragEvent, key: string];\n  openLightbox: [key: string];\n  renderCharacter: [key: string, force: boolean];\n}>();\n\n// A movie render or any in-flight beat render locks every per-character\n// action — the generated frames must not change underneath them.\nconst busy = computed(() => props.movieGenerating || props.anyBeatRendering);\n</script>\n","<template>\n  <div class=\"ml-4 shrink-0 flex items-center gap-2\">\n    <!-- Play presentation: opens the lightbox at beat 0 and starts\n         audio. Same gating as Download Movie — only when a movie has\n         been generated, which is our proxy for \"every beat has both\n         an image and audio on disk\". Green outline + green icon\n         share the visual idiom with the (filled) Download button so\n         both completed-artifact actions read as the same family.\n         `isPlayReady` ensures we don't open the lightbox before the\n         first beat's image (and audio, if it has text) finish their\n         async load — moviePath can be set while loadExistingBeatImage\n         is still in flight. -->\n    <button\n      v-if=\"moviePath && !movieGenerating\"\n      class=\"h-8 w-8 flex items-center justify-center rounded border border-green-600 text-green-600 hover:bg-green-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n      :disabled=\"!isPlayReady\"\n      :title=\"m.playPresentation\"\n      :aria-label=\"m.playPresentation\"\n      @click=\"emit('play')\"\n    >\n      <span class=\"material-icons text-base\">play_arrow</span>\n    </button>\n    <!-- Download Movie: authenticated blob fetch through the host\n         adapter, then a synthetic <a download> click. A plain\n         <a href download> can't attach the host's auth headers, which\n         would have forced an auth exemption on the media route — the\n         host-injected `fetchMediaBlob` keeps the auth boundary intact\n         (and hosts that don't provide it simply don't show this\n         button). -->\n    <button\n      v-if=\"moviePath && !movieGenerating && canFetchMedia\"\n      class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n      :disabled=\"movieDownloading\"\n      data-testid=\"mulmo-script-download-movie-button\"\n      @click=\"emit('downloadMovie')\"\n    >\n      <span class=\"material-icons text-base\">download</span>\n      <span>{{ m.movie }}</span>\n    </button>\n    <!-- Regenerate Movie (icon-only): collapses to a square once a\n         movie exists — the adjacent Download / Play already make\n         the subject clear, so the \"Movie\" label only adds noise. -->\n    <button\n      v-if=\"moviePath && !movieGenerating\"\n      class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n      :title=\"m.regenerateMovie\"\n      :aria-label=\"m.regenerateMovie\"\n      data-testid=\"mulmo-script-regenerate-movie-button\"\n      @click=\"emit('generateMovie')\"\n    >\n      <span class=\"material-icons text-base\">refresh</span>\n    </button>\n    <!-- Generate Movie (pill): no movie yet, or one is currently\n         generating. Keeps the label so first-time users know what\n         they're triggering. -->\n    <button\n      v-else\n      class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n      :disabled=\"movieGenerating\"\n      data-testid=\"mulmo-script-generate-movie-button\"\n      @click=\"emit('generateMovie')\"\n    >\n      <svg v-if=\"movieGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n        <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n        <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n      </svg>\n      <span v-if=\"movieGenerating\">{{ m.generating }}</span>\n      <template v-else>\n        <span class=\"material-icons text-sm\">refresh</span>\n        <span>{{ m.movie }}</span>\n      </template>\n    </button>\n    <!-- PDF (#1614): same Generate / Download / Regenerate pattern\n         as the Movie cluster above, kept structurally separate so\n         the two outputs can be requested independently and report\n         status independently. -->\n    <button\n      v-if=\"pdfPath && !pdfGenerating && canFetchMedia\"\n      class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n      :disabled=\"pdfDownloading\"\n      data-testid=\"mulmo-script-download-pdf-button\"\n      @click=\"emit('downloadPdf')\"\n    >\n      <span class=\"material-icons text-base\">download</span>\n      <span>{{ m.pdf }}</span>\n    </button>\n    <button\n      v-if=\"pdfPath && !pdfGenerating\"\n      class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n      :title=\"m.regeneratePdf\"\n      :aria-label=\"m.regeneratePdf\"\n      data-testid=\"mulmo-script-regenerate-pdf-button\"\n      @click=\"emit('generatePdf')\"\n    >\n      <span class=\"material-icons text-base\">refresh</span>\n    </button>\n    <button\n      v-else\n      class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n      :disabled=\"pdfGenerating\"\n      data-testid=\"mulmo-script-generate-pdf-button\"\n      @click=\"emit('generatePdf')\"\n    >\n      <svg v-if=\"pdfGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n        <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n        <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n      </svg>\n      <span v-if=\"pdfGenerating\">{{ m.generatingPdf }}</span>\n      <template v-else>\n        <span class=\"material-icons text-sm\">picture_as_pdf</span>\n        <span>{{ m.pdf }}</span>\n      </template>\n    </button>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\n// Movie + PDF action cluster from the View header. Pure Tailwind — the\n// parent's `<style scoped>` block only targets the bottom-bar region, so\n// nothing here depends on styles that stop at the component boundary.\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\ndefineProps<{\n  moviePath: string | null;\n  movieGenerating: boolean;\n  movieDownloading: boolean;\n  isPlayReady: boolean;\n  canFetchMedia: boolean;\n  pdfPath: string | null;\n  pdfGenerating: boolean;\n  pdfDownloading: boolean;\n}>();\n\nconst emit = defineEmits<{\n  play: [];\n  generateMovie: [];\n  downloadMovie: [];\n  generatePdf: [];\n  downloadPdf: [];\n}>();\n</script>\n","<template>\n  <div class=\"ml-4 shrink-0 flex items-center gap-2\">\n    <!-- Play presentation: opens the lightbox at beat 0 and starts\n         audio. Same gating as Download Movie — only when a movie has\n         been generated, which is our proxy for \"every beat has both\n         an image and audio on disk\". Green outline + green icon\n         share the visual idiom with the (filled) Download button so\n         both completed-artifact actions read as the same family.\n         `isPlayReady` ensures we don't open the lightbox before the\n         first beat's image (and audio, if it has text) finish their\n         async load — moviePath can be set while loadExistingBeatImage\n         is still in flight. -->\n    <button\n      v-if=\"moviePath && !movieGenerating\"\n      class=\"h-8 w-8 flex items-center justify-center rounded border border-green-600 text-green-600 hover:bg-green-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n      :disabled=\"!isPlayReady\"\n      :title=\"m.playPresentation\"\n      :aria-label=\"m.playPresentation\"\n      @click=\"emit('play')\"\n    >\n      <span class=\"material-icons text-base\">play_arrow</span>\n    </button>\n    <!-- Download Movie: authenticated blob fetch through the host\n         adapter, then a synthetic <a download> click. A plain\n         <a href download> can't attach the host's auth headers, which\n         would have forced an auth exemption on the media route — the\n         host-injected `fetchMediaBlob` keeps the auth boundary intact\n         (and hosts that don't provide it simply don't show this\n         button). -->\n    <button\n      v-if=\"moviePath && !movieGenerating && canFetchMedia\"\n      class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n      :disabled=\"movieDownloading\"\n      data-testid=\"mulmo-script-download-movie-button\"\n      @click=\"emit('downloadMovie')\"\n    >\n      <span class=\"material-icons text-base\">download</span>\n      <span>{{ m.movie }}</span>\n    </button>\n    <!-- Regenerate Movie (icon-only): collapses to a square once a\n         movie exists — the adjacent Download / Play already make\n         the subject clear, so the \"Movie\" label only adds noise. -->\n    <button\n      v-if=\"moviePath && !movieGenerating\"\n      class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n      :title=\"m.regenerateMovie\"\n      :aria-label=\"m.regenerateMovie\"\n      data-testid=\"mulmo-script-regenerate-movie-button\"\n      @click=\"emit('generateMovie')\"\n    >\n      <span class=\"material-icons text-base\">refresh</span>\n    </button>\n    <!-- Generate Movie (pill): no movie yet, or one is currently\n         generating. Keeps the label so first-time users know what\n         they're triggering. -->\n    <button\n      v-else\n      class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n      :disabled=\"movieGenerating\"\n      data-testid=\"mulmo-script-generate-movie-button\"\n      @click=\"emit('generateMovie')\"\n    >\n      <svg v-if=\"movieGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n        <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n        <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n      </svg>\n      <span v-if=\"movieGenerating\">{{ m.generating }}</span>\n      <template v-else>\n        <span class=\"material-icons text-sm\">refresh</span>\n        <span>{{ m.movie }}</span>\n      </template>\n    </button>\n    <!-- PDF (#1614): same Generate / Download / Regenerate pattern\n         as the Movie cluster above, kept structurally separate so\n         the two outputs can be requested independently and report\n         status independently. -->\n    <button\n      v-if=\"pdfPath && !pdfGenerating && canFetchMedia\"\n      class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n      :disabled=\"pdfDownloading\"\n      data-testid=\"mulmo-script-download-pdf-button\"\n      @click=\"emit('downloadPdf')\"\n    >\n      <span class=\"material-icons text-base\">download</span>\n      <span>{{ m.pdf }}</span>\n    </button>\n    <button\n      v-if=\"pdfPath && !pdfGenerating\"\n      class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n      :title=\"m.regeneratePdf\"\n      :aria-label=\"m.regeneratePdf\"\n      data-testid=\"mulmo-script-regenerate-pdf-button\"\n      @click=\"emit('generatePdf')\"\n    >\n      <span class=\"material-icons text-base\">refresh</span>\n    </button>\n    <button\n      v-else\n      class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n      :disabled=\"pdfGenerating\"\n      data-testid=\"mulmo-script-generate-pdf-button\"\n      @click=\"emit('generatePdf')\"\n    >\n      <svg v-if=\"pdfGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n        <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n        <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n      </svg>\n      <span v-if=\"pdfGenerating\">{{ m.generatingPdf }}</span>\n      <template v-else>\n        <span class=\"material-icons text-sm\">picture_as_pdf</span>\n        <span>{{ m.pdf }}</span>\n      </template>\n    </button>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\n// Movie + PDF action cluster from the View header. Pure Tailwind — the\n// parent's `<style scoped>` block only targets the bottom-bar region, so\n// nothing here depends on styles that stop at the component boundary.\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\ndefineProps<{\n  moviePath: string | null;\n  movieGenerating: boolean;\n  movieDownloading: boolean;\n  isPlayReady: boolean;\n  canFetchMedia: boolean;\n  pdfPath: string | null;\n  pdfGenerating: boolean;\n  pdfDownloading: boolean;\n}>();\n\nconst emit = defineEmits<{\n  play: [];\n  generateMovie: [];\n  downloadMovie: [];\n  generatePdf: [];\n  downloadPdf: [];\n}>();\n</script>\n","<template>\n  <div class=\"h-full bg-white flex flex-col overflow-hidden\">\n    <!-- Header -->\n    <div class=\"flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0\">\n      <div class=\"min-w-0 flex-1\">\n        <h2 class=\"text-lg font-semibold text-gray-800 truncate\" data-testid=\"mulmo-script-title\">\n          {{ script.title || \"Untitled Script\" }}\n        </h2>\n        <p v-if=\"script.description\" class=\"text-sm text-gray-500 mt-0.5 truncate\" data-testid=\"mulmo-script-description\">\n          {{ script.description }}\n        </p>\n        <div class=\"flex items-center gap-3 mt-1 text-xs text-gray-400\">\n          <span>{{ m.beatCount(beats.length) }}</span>\n          <span v-if=\"script.lang\">{{ script.lang }}</span>\n          <span v-if=\"filePath\" class=\"truncate\">{{ filePath }}</span>\n        </div>\n      </div>\n      <MulmoScriptToolbar\n        :movie-path=\"moviePath\"\n        :movie-generating=\"movieGenerating\"\n        :movie-downloading=\"movieDownloading\"\n        :is-play-ready=\"isPlayReady\"\n        :can-fetch-media=\"canFetchMedia\"\n        :pdf-path=\"pdfPath\"\n        :pdf-generating=\"pdfGenerating\"\n        :pdf-downloading=\"pdfDownloading\"\n        @play=\"playPresentation\"\n        @generate-movie=\"generateMovie\"\n        @download-movie=\"downloadMovie\"\n        @generate-pdf=\"generatePdf\"\n        @download-pdf=\"downloadPdf\"\n      />\n    </div>\n\n    <!--\n      Inline error chip for movie-generation failures (#1197).\n      Previously the catch arm of `generateMovie` raised an `alert()` —\n      blocking, no retry path, and many users just dismissed the modal\n      and saw a stalled spinner with no explanation. The chip stays\n      visible until the next generate attempt clears it.\n    -->\n    <div\n      v-if=\"movieError\"\n      data-testid=\"mulmo-script-movie-error-chip\"\n      class=\"bg-red-50 border border-red-200 text-red-800 text-xs px-3 py-2 mx-4 mt-3 mb-1 rounded flex items-start gap-2\"\n    >\n      <span class=\"material-icons text-base shrink-0 mt-px\">error_outline</span>\n      <div class=\"flex-1 min-w-0\">\n        <div class=\"font-medium\">{{ m.movieGenerationFailed }}</div>\n        <div class=\"break-words whitespace-pre-wrap mt-0.5\">{{ movieError }}</div>\n      </div>\n      <button\n        class=\"shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50\"\n        :disabled=\"movieGenerating\"\n        data-testid=\"mulmo-script-movie-retry-button\"\n        @click=\"generateMovie\"\n      >\n        {{ m.retry }}\n      </button>\n    </div>\n\n    <!-- Characters section -->\n    <CharacterStrip\n      v-if=\"characterKeys.length > 0\"\n      :character-keys=\"characterKeys\"\n      :images=\"script.imageParams?.images\"\n      :thumbnails=\"charImages\"\n      :render-state=\"charRenderState\"\n      :errors=\"charErrors\"\n      :drag-over=\"charDragOver\"\n      :movie-generating=\"movieGenerating\"\n      :any-beat-rendering=\"anyBeatRendering\"\n      @generate-all=\"generateAllCharacters\"\n      @char-drag-over=\"onCharDragOver\"\n      @char-drag-leave=\"onCharDragLeave\"\n      @char-drop=\"onCharDrop\"\n      @open-lightbox=\"openCharacterLightbox\"\n      @render-character=\"renderCharacter\"\n    />\n\n    <!-- Deck editor (#1575, #2945): every beat is a slide → mount the interactive\n         editor from @mulmocast/beat-editor. Lazy-loaded via defineAsyncComponent, so\n         users whose scripts aren't decks never pay the bundle cost.\n\n         It takes and emits a beat ARRAY, so the script goes through beatsOf / withBeats\n         on the way in and out. Writing `{ ...script, beats }` by hand instead drops\n         presentationStyle and slideParams, and nothing tells you it happened.\n\n         No `layout` prop: the editor lays itself out from its own width, so the pane\n         moves below the list on a narrow host (this card) rather than beside it. -->\n    <!-- Two ways to look at the same script, not two kinds of script. The editor edits every\n         beat type; the list is where the media lives (generate audio, render an image, open a\n         clip), which the editor has no equivalent for — so neither replaces the other. -->\n    <div v-if=\"canEditBeats\" class=\"flex shrink-0 gap-1 px-2 pt-1 text-[11px]\">\n      <button type=\"button\" :class=\"beatPaneTabClass(beatPane === 'edit')\" data-testid=\"mulmo-script-tab-edit\" @click=\"beatPane = 'edit'\">\n        {{ m.editTab }}\n      </button>\n      <button type=\"button\" :class=\"beatPaneTabClass(beatPane === 'media')\" data-testid=\"mulmo-script-tab-media\" @click=\"beatPane = 'media'\">\n        {{ m.mediaTab }}\n      </button>\n    </div>\n\n    <!-- A deck save that failed, in the server's own words (#3070). The editor keeps showing the\n         edit either way, so without this the only difference between a save and a silent failure\n         is what comes back on the next reload. Shown in both panes: switching tabs does not make\n         an unsaved edit saved. -->\n    <div\n      v-if=\"deckSaveError\"\n      class=\"shrink-0 mx-2 mt-1 px-2 py-1 rounded bg-red-50 border border-red-200 text-xs text-red-700 break-words\"\n      role=\"alert\"\n      data-testid=\"mulmo-script-deck-save-error\"\n    >\n      {{ m.saveErrorSaveFailed(deckSaveError) }}\n    </div>\n\n    <div v-if=\"showBeatEditor\" class=\"flex-1 overflow-hidden\" data-testid=\"mulmo-script-deck-editor\" @focusout=\"onDeckFocusOut\">\n      <BeatListEditor :beats=\"deckBeats\" @update:beats=\"onDeckBeatsUpdate\" />\n    </div>\n\n    <!-- Per-beat media list: thumbnails, narration, audio / image / movie generation. -->\n    <div v-else ref=\"beatListEl\" class=\"flex-1 overflow-y-auto p-2 space-y-1.5\">\n      <div v-for=\"(beat, index) in beats\" :key=\"index\" class=\"rounded-lg border border-gray-200 overflow-hidden\">\n        <!-- Beat body: thumbnail + narration side by side -->\n        <div class=\"flex gap-3 items-stretch\">\n          <!-- Thumbnail -->\n          <div\n            class=\"relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors\"\n            :class=\"beatDragOver[index] ? 'bg-blue-50' : ''\"\n            @dragover=\"onBeatDragOver($event, index)\"\n            @dragleave=\"onBeatDragLeave(index)\"\n            @drop=\"onBeatDrop($event, index)\"\n          >\n            <!-- Beat number badge (1-based). Sits above the drop-hint\n                 overlay and the inline video player so the index stays\n                 readable in every beat state. -->\n            <div\n              class=\"absolute top-1.5 left-1.5 z-10 px-1.5 py-0.5 rounded bg-black/55 text-white text-xs font-medium leading-none pointer-events-none\"\n              :data-testid=\"`mulmo-script-beat-number-${index}`\"\n            >\n              {{ index + 1 }}\n            </div>\n            <!-- Inline player for the beat's generated video clip.\n                 Replaces the thumbnail while open; the close button\n                 returns to the still image. -->\n            <template v-if=\"beatMovieOpen[index] && beatMovieUrls[index]\">\n              <video :src=\"beatMovieUrls[index]\" class=\"w-full object-contain\" controls autoplay :data-testid=\"`mulmo-script-beat-movie-player-${index}`\" />\n              <button\n                class=\"absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50\"\n                :title=\"m.close\"\n                :aria-label=\"m.close\"\n                :data-testid=\"`mulmo-script-beat-movie-close-${index}`\"\n                @click.stop=\"closeBeatMovie(index)\"\n              >\n                <span class=\"material-icons text-sm\">close</span>\n              </button>\n            </template>\n            <template v-else>\n              <img\n                v-if=\"renderedImages[index]\"\n                :src=\"renderedImages[index]\"\n                class=\"w-full object-contain cursor-zoom-in\"\n                :alt=\"`Beat ${index + 1}`\"\n                @click=\"openLightbox(index)\"\n              />\n              <!-- Play overlay: shown when the beat-movie probe found a\n                   generated clip for this beat. Blob is fetched lazily\n                   on first click (host-authenticated), hence the spinner. -->\n              <button\n                v-if=\"renderedImages[index] && beatMovies[index] && canFetchMedia\"\n                class=\"absolute inset-0 m-auto w-12 h-12 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70\"\n                :title=\"m.play\"\n                :aria-label=\"m.play\"\n                :data-testid=\"`mulmo-script-beat-movie-play-${index}`\"\n                @click.stop=\"playBeatMovie(index)\"\n              >\n                <svg v-if=\"beatMovieLoading[index]\" class=\"animate-spin w-5 h-5\" viewBox=\"0 0 24 24\" fill=\"none\">\n                  <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n                  <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n                </svg>\n                <span v-else class=\"material-icons text-3xl\">play_arrow</span>\n              </button>\n              <button\n                v-if=\"renderedImages[index] && renderState[index] !== 'rendering'\"\n                class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed\"\n                :disabled=\"movieGenerating\"\n                @click.stop=\"regenerateBeat(index)\"\n              >\n                ↺\n              </button>\n              <div v-else-if=\"!renderedImages[index]\" class=\"w-full aspect-video flex flex-col items-center justify-center gap-1 p-2\">\n                <template v-if=\"renderState[index] === 'rendering' || (movieGenerating && !renderedImages[index] && effectiveBeat(index).imagePrompt)\">\n                  <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n                    <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n                    <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n                  </svg>\n                  <span class=\"text-xs text-green-500\">{{ m.rendering }}</span>\n                </template>\n                <template v-else-if=\"renderState[index] === 'error'\">\n                  <span class=\"text-xs text-red-400 text-center\">{{ renderErrors[index] }}</span>\n                </template>\n                <template v-else>\n                  <span v-if=\"effectiveBeat(index).imagePrompt\" class=\"text-xs text-gray-400 text-center italic leading-relaxed px-1\">{{\n                    effectiveBeat(index).imagePrompt\n                  }}</span>\n                  <span v-else class=\"text-xs text-gray-300\">{{ beat.image?.type ?? \"—\" }}</span>\n                </template>\n              </div>\n            </template>\n            <!-- Beat drop hint / overlay -->\n            <div v-if=\"beatDragOver[index]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n              <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n            </div>\n            <div\n              v-else-if=\"!renderedImages[index] && renderState[index] !== 'rendering'\"\n              class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\"\n            >\n              {{ m.orDropImage }}\n            </div>\n            <!-- Generate button for any beat without a rendered image.\n                 renderBeat works for every beat type: imagePrompt /\n                 typed image beats render directly, moviePrompt beats\n                 get a frame extracted from the generated clip, and\n                 text-only beats fall back to a prompt derived from\n                 the narration text (mulmocast prompt.js). -->\n            <button\n              v-if=\"!renderedImages[index] && renderState[index] !== 'rendering' && !movieGenerating && !isBeatImageReference(effectiveBeat(index))\"\n              class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50\"\n              @click=\"renderBeat(index)\"\n            >\n              {{ m.generate }}\n            </button>\n          </div>\n\n          <!-- Narration text -->\n          <div class=\"flex flex-col flex-1 min-w-0 px-2 py-1.5\">\n            <span class=\"text-sm text-gray-800 leading-relaxed\">{{ effectiveBeat(index).text }}</span>\n            <div class=\"flex justify-between mt-auto pt-1\">\n              <!-- Audio controls -->\n              <div class=\"flex items-center gap-1\">\n                <template v-if=\"audioState[index] === 'generating' || (movieGenerating && !beatAudios[index] && effectiveBeat(index).text)\">\n                  <svg class=\"animate-spin w-3 h-3 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n                    <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n                    <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n                  </svg>\n                </template>\n                <button\n                  v-else-if=\"beatAudios[index]\"\n                  class=\"text-xs px-2 py-0.5 rounded border\"\n                  :class=\"playingAudio?.index === index ? 'border-red-400 text-red-600 hover:bg-red-50' : 'border-green-400 text-green-600 hover:bg-green-50'\"\n                  @click=\"playAudio(index)\"\n                >\n                  {{ playingAudio?.index === index ? m.stop : m.play }}\n                </button>\n                <template v-else-if=\"audioErrors[index]\">\n                  <span class=\"text-xs text-red-400 truncate min-w-0 max-w-[20rem]\" :title=\"audioErrors[index]\">\n                    {{ m.errPrefix }} {{ audioErrors[index] }}\n                  </span>\n                  <button\n                    v-if=\"effectiveBeat(index).text\"\n                    class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n                    :disabled=\"movieGenerating\"\n                    @click=\"generateAudio(index)\"\n                  >\n                    ↺\n                  </button>\n                </template>\n                <button\n                  v-else-if=\"effectiveBeat(index).text\"\n                  class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50\"\n                  @click=\"generateAudio(index)\"\n                >\n                  {{ m.generateAudio }}\n                </button>\n              </div>\n              <button\n                class=\"text-gray-400 hover:text-gray-600\"\n                :title=\"sourceOpen[index] ? 'Hide source' : 'Show source'\"\n                :data-testid=\"`mulmo-script-beat-source-toggle-${index}`\"\n                @click=\"toggleSource(index)\"\n              >\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  class=\"w-3.5 h-3.5\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <polyline points=\"16 18 22 12 16 6\" />\n                  <polyline points=\"8 6 2 12 8 18\" />\n                </svg>\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <!-- Source editor -->\n        <div v-if=\"sourceOpen[index]\" class=\"border-t border-gray-100\">\n          <textarea\n            v-model=\"sourceText[index]\"\n            class=\"w-full text-xs text-gray-600 bg-gray-50 p-2 font-mono resize-none\"\n            :class=\"isValidBeat(index) ? 'outline-none' : 'outline outline-2 outline-red-400'\"\n            rows=\"8\"\n            spellcheck=\"false\"\n            :data-testid=\"`mulmo-script-beat-source-textarea-${index}`\"\n          />\n          <div class=\"flex items-center justify-end gap-2 px-2 pb-2\">\n            <span v-if=\"beatSaveErrors[index]\" class=\"text-xs text-red-600\" role=\"alert\">{{\n              beatSaveErrors[index].kind === \"invalidJson\"\n                ? m.saveErrorInvalidJson(beatSaveErrors[index].error)\n                : m.saveErrorSaveFailed(beatSaveErrors[index].error)\n            }}</span>\n            <button\n              class=\"px-2 py-1 text-xs rounded border\"\n              :class=\"\n                isValidBeat(index) && !beatSaving[index]\n                  ? 'border-blue-400 text-blue-600 hover:bg-blue-50 cursor-pointer'\n                  : 'border-gray-200 text-gray-300 cursor-not-allowed'\n              \"\n              :disabled=\"!isValidBeat(index) || !!beatSaving[index]\"\n              :data-testid=\"`mulmo-script-beat-update-button-${index}`\"\n              @click=\"updateBeat(index)\"\n            >\n              {{ beatSaving[index] ? m.saving : m.update }}\n            </button>\n          </div>\n        </div>\n      </div>\n\n      <div v-if=\"beats.length === 0\" class=\"flex items-center justify-center h-32 text-gray-400 text-sm\">{{ m.noBeats }}</div>\n    </div>\n\n    <!-- Bottom bar: Edit Script Source + Copy -->\n    <div class=\"bottom-bar-wrapper\">\n      <details ref=\"sourceDetails\" class=\"script-source\" @toggle=\"onSourceToggle(($event.target as HTMLDetailsElement).open)\">\n        <summary>{{ m.editSource }}</summary>\n        <textarea\n          v-model=\"editableSource\"\n          class=\"script-editor\"\n          :class=\"{ 'script-editor-invalid': sourceChanged && !sourceValid }\"\n          spellcheck=\"false\"\n        ></textarea>\n        <div class=\"editor-actions\">\n          <button class=\"apply-btn\" :disabled=\"!sourceChanged || !sourceValid\" @click=\"applySource\">{{ m.applyChanges }}</button>\n          <button class=\"cancel-btn\" @click=\"cancelSourceEdit\">{{ m.cancel }}</button>\n        </div>\n      </details>\n      <button v-show=\"!editing\" class=\"copy-btn\" :title=\"copied ? 'Copied!' : 'Copy'\" @click=\"copyText\">\n        <span class=\"material-icons\">{{ copied ? \"check\" : \"content_copy\" }}</span>\n      </button>\n    </div>\n\n    <!-- Lightbox -->\n    <BeatLightbox\n      v-if=\"lightbox\"\n      :lightbox=\"lightbox\"\n      :beat-count=\"beats.length\"\n      :beat-texts=\"beatTexts\"\n      :has-prev=\"hasPrev\"\n      :has-next=\"hasNext\"\n      :playing-audio-index=\"playingAudio?.index ?? null\"\n      :audio-progress=\"audioProgress\"\n      :has-current-audio=\"Boolean(beatAudios[lightbox.index])\"\n      @close=\"closeLightbox\"\n      @move=\"lightboxMove\"\n      @jump=\"jumpToBeat\"\n      @play-audio=\"playAudio\"\n    />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, watch } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport { mulmoBeatSchema, mulmoScriptSchema } from \"@mulmocast/types\";\nimport type { MulmoScriptData } from \"../core/types\";\nimport type { MulmoScriptGenerationEvent } from \"../core/contract\";\nimport {\n  isSameScript,\n  beatMayHaveMovie,\n  shouldAutoRenderBeat,\n  effectiveBeat as effectiveBeatOf,\n  isBeatImageReference,\n  isValidBeat as isValidBeatOf,\n  staleSince as staleSinceOf,\n  type StoryRef,\n  scriptSourceText as toScriptSourceText,\n  resolveSilentAdvanceSeconds,\n  clearReactiveRecords,\n  focusLeftContainer,\n  type Beat,\n} from \"./helpers\";\nimport { beatsOf, withBeats, type EditableBeat } from \"@mulmocast/beat-editor\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { readFileAsDataUrl, useClipboardCopy } from \"./support\";\nimport { useMulmoScriptTransport } from \"./transport\";\nimport { useHostAdapter } from \"./hostAdapter\";\nimport { useMediaExport } from \"./composables/useMediaExport\";\nimport { useBeatMovie } from \"./composables/useBeatMovie\";\nimport { useCharacterImages } from \"./composables/useCharacterImages\";\nimport { useDeckEditor } from \"./composables/useDeckEditor\";\nimport type { LightboxState, MulmoScript } from \"./viewTypes\";\nimport BeatLightbox from \"./components/BeatLightbox.vue\";\nimport CharacterStrip from \"./components/CharacterStrip.vue\";\nimport MulmoScriptToolbar from \"./components/MulmoScriptToolbar.vue\";\nimport { useT } from \"../lang/index\";\n\n// Lazy-loaded so the editor's Vue / tailwind chunk stays out of the initial\n// bundle for users whose scripts aren't decks\n// (movies, html_tailwind animations, mixed beats). `defineAsyncComponent`\n// triggers the dynamic import only when `isDeck` first flips true.\nconst BeatListEditor = defineAsyncComponent(() => import(\"@mulmocast/beat-editor\").then((mod) => mod.BeatListEditor));\n\nconst api = useMulmoScriptTransport();\nconst adapter = useHostAdapter();\n// Media bytes (movie / PDF / beat clips) are served behind host auth; hosts\n// opt in by injecting `fetchMediaBlob`. Without it the download / clip-play\n// affordances are hidden (the probes still run — state stays warm for a\n// host that injects later at remount).\nconst canFetchMedia = computed(() => Boolean(adapter.fetchMediaBlob));\n\nconst m = useT();\n\nconst props = defineProps<{\n  selectedResult: ToolResultComplete<MulmoScriptData>;\n}>();\nconst emit = defineEmits<{ updateResult: [result: ToolResultComplete] }>();\n\nconst data = computed(() => props.selectedResult.data);\nconst script = computed<MulmoScript>(() => data.value?.script ?? {});\nconst filePath = computed(() => data.value?.filePath ?? \"\");\n/**\n * Which registered root `filePath` is relative to; `undefined` = the host's default (#3014).\n *\n * The host puts it on the card when it opens a deck the user can see; the agent's tool schema\n * deliberately has no root, so a model cannot name one (#3015). Every dispatch below hands it\n * back, because `stories/deck.json` exists in EVERY root: without it the call addresses the\n * default root's file of that name, which is how a deck inside a repository opened fine and\n * then answered `File not found` to every save and every beat image\n * (receptron/mulmoterminal#1970).\n */\nconst root = computed(() => data.value?.root);\n/** The story as the wire addresses it — the pair that is its identity. */\nconst storyRef = (): StoryRef => ({ filePath: filePath.value, root: root.value });\nconst beats = computed<Beat[]>(() => script.value.beats ?? []);\n\n// Per-beat render state\ntype RenderState = \"idle\" | \"rendering\" | \"done\" | \"error\";\nconst renderState = reactive<Record<number, RenderState>>({});\nconst renderedImages = reactive<Record<number, string>>({});\nconst renderErrors = reactive<Record<number, string>>({});\nconst sourceOpen = reactive<Record<number, boolean>>({});\nconst sourceText = reactive<Record<number, string>>({});\n// Surface update-beat failures inline next to the Update button.\n// Cleared on next successful save or editor close. Store raw error +\n// kind tag so the template picks a localized message, instead of\n// pre-composing an English-prefixed string here.\ninterface BeatSaveError {\n  kind: \"invalidJson\" | \"saveFailed\";\n  error: string;\n}\nconst beatSaveErrors = reactive<Record<number, BeatSaveError>>({});\nconst beatSaving = reactive<Record<number, boolean>>({});\nconst localOverrides = reactive<Record<number, Beat>>({});\nconst beatAudios = reactive<Record<number, string>>({});\nconst audioState = reactive<Record<number, \"generating\" | \"done\" | \"error\">>({});\nconst audioErrors = reactive<Record<number, string>>({});\nconst playingAudio = ref<{ index: number; audio: HTMLAudioElement } | null>(null);\n// Tracks the auto-advance timer running on a silent beat\n// (`beat.text === \"\"`). Beats without text generate no audio, so the\n// Play loop falls back to a `setTimeout(beat.duration)` for cues —\n// without this, Play would stall on the first silent beat (#1073).\nconst silentPlaybackTimer = ref<{ index: number; timer: ReturnType<typeof setTimeout> } | null>(null);\nconst audioProgress = ref(0);\n\n// Default duration (seconds) for a silent beat whose script doesn't\n// set `duration` either. Picked to roughly match the time it takes a\n// reader to scan a `textSlide` — long enough to read, short enough\n// not to feel stuck. The script's own `duration` always wins.\nconst SILENT_BEAT_DEFAULT_SEC = 3;\nconst MS_PER_SECOND = 1000;\nconst beatListEl = ref<HTMLElement | null>(null);\nconst lightbox = ref<LightboxState | null>(null);\nconst beatDragOver = reactive<Record<number, boolean>>({});\n\nconst anyBeatRendering = computed(() => Object.values(renderState).some((state) => state === \"rendering\"));\n\n// Session tagging is host transport: MulmoClaude injects the active chat\n// session id so generations light its per-session sidebar indicator;\n// hosts without sessions leave the adapter empty and the field is simply\n// omitted from generation dispatches.\nconst chatSessionId = computed(() => adapter.chatSessionId?.value);\n\nconst {\n  moviePath,\n  movieGenerating,\n  movieDownloading,\n  movieError,\n  pdfPath,\n  pdfGenerating,\n  pdfDownloading,\n  generateMovie,\n  downloadMovie,\n  refreshMoviePath,\n  generatePdf,\n  downloadPdf,\n  refreshPdfPath,\n  resetMedia,\n} = useMediaExport({ api, adapter, filePath, root, chatSessionId });\n\nconst {\n  beatMovies,\n  beatMovieUrls,\n  beatMovieOpen,\n  beatMovieLoading,\n  loadExistingBeatMovie,\n  playBeatMovie,\n  closeBeatMovie,\n  invalidateBeatMovie,\n  resetBeatMovies,\n} = useBeatMovie({ api, adapter, filePath, root });\n\nconst {\n  charRenderState,\n  charImages,\n  charErrors,\n  charDragOver,\n  characterKeys,\n  onCharDragOver,\n  onCharDragLeave,\n  onCharDrop,\n  loadExistingCharacterImage,\n  refreshMissingCharacterImages,\n  renderCharacter,\n  generateAllCharacters,\n  resetCharacters,\n} = useCharacterImages({ api, filePath, root, chatSessionId, getImages: () => script.value.imageParams?.images });\n\nfunction stopPlayingAudio() {\n  // Single helper that clears both the audio path and the silent\n  // auto-advance timer — callers (lightbox open / arrow nav / Stop\n  // button) get consistent behaviour without remembering which\n  // playback mode the current beat was using (#1073).\n  stopAllPlayback();\n}\n\nfunction openLightbox(index: number) {\n  stopPlayingAudio();\n  lightbox.value = {\n    src: renderedImages[index] ?? \"\",\n    text: effectiveBeat(index).text,\n    index,\n  };\n}\n\n// Backdrop click handler. Stops any in-flight narration so the audio\n// doesn't keep playing after the lightbox is dismissed — without this,\n// the HTMLAudioElement created by playAudio() outlives the modal and\n// the user hears disembodied narration with no UI to stop it.\nfunction closeLightbox() {\n  stopPlayingAudio();\n  lightbox.value = null;\n}\n\n// \"Play presentation\" toolbar action. Opens the lightbox at beat 0 and\n// kicks off its narration audio; the existing on-ended hook then chains\n// through the rest of the deck (lightboxMove(1) → playAudio if the next\n// beat has audio), so one click runs the whole presentation. Only wired\n// to the toolbar button when moviePath is set, which is our proxy for\n// \"every beat has both image and audio on disk\".\n//\n// `moviePath` arrives synchronously from movieStatus, but the per-beat\n// image and audio data URIs are populated asynchronously by\n// loadExistingBeatImage / loadExistingBeatAudio in initializeScript().\n// The Play button can therefore become visible before beat 0's assets\n// hydrate — `isPlayReady` gates the click so the lightbox never opens\n// with an undefined src or silent narration on a beat that does have\n// text.\nconst isPlayReady = computed<boolean>(() => {\n  if (beats.value.length === 0) return false;\n  if (!renderedImages[0]) return false;\n  // Audio is only required when the beat has text (the source of TTS).\n  // Beats without text are valid; they just play silently.\n  if (effectiveBeat(0).text && !beatAudios[0]) return false;\n  return true;\n});\n\nfunction playPresentation() {\n  if (!isPlayReady.value) return;\n  openLightbox(0);\n  playBeat(0);\n}\n\n// Stop whichever playback handle is active. Idempotent. Called by\n// openLightbox, manual stop / pause buttons, and by `playBeat`\n// before kicking off a new beat so we never double-schedule. (#1073)\nfunction stopAllPlayback(): void {\n  if (playingAudio.value) {\n    playingAudio.value.audio.pause();\n    playingAudio.value = null;\n    audioProgress.value = 0;\n  }\n  if (silentPlaybackTimer.value) {\n    clearTimeout(silentPlaybackTimer.value.timer);\n    silentPlaybackTimer.value = null;\n  }\n}\n\n// Single entry point for \"start playback at beat <index>\". Routes\n// on what the script DECLARED, not on what's currently hydrated:\n//\n//   - `text` empty  → silent path (`scheduleSilentAdvance`). The\n//     schema says no audio is generated for empty-text beats, so\n//     `duration` drives auto-advance.\n//   - `text` present + audio loaded → audio path. `audio.ended`\n//     chains via `advanceFromBeat`.\n//   - `text` present + audio NOT loaded → stop. The Play button's\n//     `isPlayReady` gate prevented this for beat 0, but mid-stream\n//     a transient fetch miss must not silently skip the narration\n//     by falling through to the silent timer (Codex review on\n//     #1073 — gating on `beatAudios[index]` would do exactly that).\n//\n// Either path chains to the next beat via `advanceFromBeat`, so a\n// run of silent beats — or audio / silent / audio sequences —\n// plays through without manual interaction.\nfunction playBeat(index: number): void {\n  stopAllPlayback();\n  const hasText = Boolean(effectiveBeat(index).text);\n  if (!hasText) {\n    scheduleSilentAdvance(index);\n    return;\n  }\n  if (beatAudios[index]) {\n    playAudio(index);\n  }\n  // Text beat with no audio yet → stop. The user can re-click Play\n  // once the audio finishes hydrating.\n}\n\nfunction scheduleSilentAdvance(index: number): void {\n  // Defensively narrow the script-supplied duration (zero / negative / NaN /\n  // non-number → default) — a bad value would otherwise collapse to an\n  // immediate timeout and the Play loop would race through every silent beat\n  // in a single tick (Codex review iter-5 on #1365).\n  const seconds = resolveSilentAdvanceSeconds(effectiveBeat(index).duration, SILENT_BEAT_DEFAULT_SEC);\n  const timer = setTimeout(() => {\n    if (silentPlaybackTimer.value?.index !== index) return;\n    silentPlaybackTimer.value = null;\n    if (lightbox.value?.index === index) advanceFromBeat(index);\n  }, seconds * MS_PER_SECOND);\n  silentPlaybackTimer.value = { index, timer };\n}\n\nfunction advanceFromBeat(fromIndex: number): void {\n  lightboxMove(1);\n  const nextIndex = lightbox.value?.index;\n  if (nextIndex === undefined || nextIndex === fromIndex) return;\n  playBeat(nextIndex);\n}\n\nconst hasPrev = computed(() => {\n  if (!lightbox.value) return false;\n  for (let i = lightbox.value.index - 1; i >= 0; i--) {\n    if (renderedImages[i]) return true;\n  }\n  return false;\n});\n\nconst hasNext = computed(() => {\n  if (!lightbox.value) return false;\n  for (let i = lightbox.value.index + 1; i < beats.value.length; i++) {\n    if (renderedImages[i]) return true;\n  }\n  return false;\n});\n\n// Narration text per beat, for the lightbox beat-strip hover tooltips. Reads\n// through `effectiveBeat` so an unsaved in-place edit shows its new text.\nconst beatTexts = computed(() => beats.value.map((_, index) => effectiveBeat(index).text));\n\nfunction jumpToBeat(index: number) {\n  if (!lightbox.value) return;\n  if (index === lightbox.value.index) return;\n  if (!renderedImages[index]) return;\n  // Carry the playback mode forward (audio OR silent timer) so a\n  // user clicking the beat-strip thumbnail mid-playback keeps the\n  // presentation rolling (#1073).\n  const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n  openLightbox(index);\n  if (wasPlaying) playBeat(index);\n}\n\nfunction lightboxMove(delta: number) {\n  if (!lightbox.value) return;\n  const total = beats.value.length;\n  // If a playback was in progress when the user clicked the arrow,\n  // carry it forward to whichever beat we land on — `playBeat`\n  // picks audio vs silent automatically. `openLightbox` stops the\n  // current playback, so capture the flag BEFORE that and chain\n  // AFTER. The on-ended / silent-advance paths already null their\n  // own state before calling `lightboxMove`, so this branch won't\n  // double-fire there.\n  const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n  let i = lightbox.value.index + delta;\n  while (i >= 0 && i < total) {\n    if (renderedImages[i]) {\n      openLightbox(i);\n      if (wasPlaying) playBeat(i);\n      return;\n    }\n    i += delta;\n  }\n}\nconst sourceDetails = ref<HTMLDetailsElement>();\nconst editing = ref(false);\nconst editableSource = ref(\"\");\nconst { copied, copy } = useClipboardCopy();\n\n// Beats may be edited in-place via `updateBeat()` and rendered through\n// `effectiveBeat()`, so the Copy / source-view text must read the merged\n// shape — otherwise the clipboard returns the original prop snapshot\n// until the full result is reloaded.\nconst effectiveScript = computed<MulmoScript>(() => ({\n  ...script.value,\n  beats: beats.value.map((beat, i) => localOverrides[i] ?? beat),\n}));\nconst scriptSourceText = computed(() => toScriptSourceText(effectiveScript.value));\n\n// Persist a saved script back into the parent's toolResult so the in-memory\n// script and reactive beats[] stay in sync without a remount. The parent's\n// handleUpdateResult uses Object.assign (in-place), so the prop watcher won't\n// fire — callers that need a re-read drive initializeScript themselves.\nfunction commitScript(next: MulmoScript): void {\n  emit(\"updateResult\", {\n    ...props.selectedResult,\n    data: { ...props.selectedResult.data, script: next },\n  });\n}\n\n// #1575 — when every beat is a `slide`, swap the per-beat list UI for the\n// interactive deck editor (@mulmocast/beat-editor). Mixed scripts (any non-slide\n// beat) fall back to the existing list. The debounce + flush-on-unmount live\n// in the composable.\nconst { canEditBeats, deckScriptInput, deckSaveError, resetForScriptChange, onDeckUpdate, flushPendingDeckSave, watchForeignWrites } = useDeckEditor({\n  api,\n  filePath,\n  root,\n  effectiveScript,\n  commitScript,\n});\n\n/**\n * Which pane the beats are shown in.\n *\n * `edit` is the beat editor — every beat type, edited in place. `media` is the per-beat list,\n * which is the only place audio / image / movie generation lives. A script with nothing to edit\n * has only the list, so the switch is hidden and this is ignored.\n *\n * `media` is the default because opening the script is what triggers rendering each beat's\n * image: the auto-render on mount lives in that list, so defaulting to `edit` silently stopped\n * thumbnails from being produced at all. Someone who wants to edit clicks once; nobody has to\n * click to get the previews they always got.\n */\nconst beatPane = ref<\"edit\" | \"media\">(\"media\");\nconst showBeatEditor = computed(() => canEditBeats.value && beatPane.value === \"edit\");\n\nconst BEAT_TAB_BASE = \"rounded px-2 py-0.5 font-sans\";\nconst beatPaneTabClass = (active: boolean) => [BEAT_TAB_BASE, active ? \"bg-gray-700 text-white\" : \"bg-gray-100 text-gray-600 hover:bg-gray-200\"];\n\n// An agent (or another window) wrote this script — pull it back off disk so the preview shows\n// what is actually there. Registered here rather than in the composable because reloading is\n// the View's job; the composable only knows that someone else wrote.\nconst unsubscribeForeignWrites = watchForeignWrites(() => {\n  void refreshScriptFromDisk();\n});\n\n// The editor takes and emits a beat array; the composable, the transport and the\n// toolResult all speak whole scripts. `beatsOf` / `withBeats` are the conversion, and\n// `withBeats` is what keeps presentationStyle / slideParams from being dropped on the\n// way back — `{ ...script, beats }` loses them silently.\nconst deckBeats = computed<EditableBeat[]>(() => beatsOf(deckScriptInput.value));\n\nfunction onDeckBeatsUpdate(beats: EditableBeat[]): void {\n  onDeckUpdate(withBeats(deckScriptInput.value, beats));\n}\n\n/**\n * Leaving the editor writes whatever is still in the debounce.\n *\n * Asking the agent to change the script means moving focus out of here first, so this lands\n * ahead of every request without the host having to announce one — MulmoTerminal's agent is a\n * terminal, and there is no \"sent\" event to hook. The debounce is short enough that it has\n * usually fired already; this closes the case where it has not, which would otherwise have the\n * agent read a file missing the last thing the user typed.\n */\nfunction onDeckFocusOut(event: FocusEvent): void {\n  const container = event.currentTarget instanceof Node ? event.currentTarget : null;\n  if (focusLeftContainer(container, event.relatedTarget)) flushPendingDeckSave();\n}\n\nonBeforeUnmount(() => {\n  flushPendingDeckSave();\n  // Release beat-clip blob object URLs — they outlive the component\n  // otherwise (document-scoped, not GC'd with it).\n  resetBeatMovies();\n  unsubscribeGenerationEvents();\n  unsubscribeForeignWrites();\n});\nconst loadedSource = ref(\"\");\nconst sourceChanged = computed(() => editableSource.value !== loadedSource.value);\nconst sourceValid = computed(() => {\n  try {\n    const parsed = JSON.parse(editableSource.value);\n    return mulmoScriptSchema.safeParse(parsed).success;\n  } catch {\n    return false;\n  }\n});\n\nasync function onSourceToggle(open: boolean) {\n  editing.value = open;\n  if (open) {\n    let text = scriptSourceText.value;\n    // Re-read the current file from disk so beat-level edits made\n    // since mount (other tabs, MCP, manual edits) surface in the\n    // editor. Uses the reopen dispatch for the same reason\n    // refreshScriptFromDisk does — `filePath.value` is the wire form\n    // `stories/<rel>` and only the mulmoScript save/reopen op knows\n    // how to map it to the on-disk path under `artifacts/stories/...`.\n    if (filePath.value) {\n      const requested = storyRef();\n      const response = await api.call(\"save\", requested);\n      // The disk read describes the script it was asked for. Navigating away during it would\n      // otherwise seed the source editor with ANOTHER deck's text (#3014).\n      if (staleSince(requested)) return;\n      const diskScript = response.ok ? (response.data.script as MulmoScript | undefined) : undefined;\n      if (diskScript) text = toScriptSourceText(diskScript);\n      // fall through to in-memory script on failure\n    }\n    editableSource.value = text;\n    loadedSource.value = text;\n  }\n}\n\nfunction cancelSourceEdit() {\n  if (sourceDetails.value) sourceDetails.value.open = false;\n}\n\nasync function applySource() {\n  let parsed: MulmoScript;\n  try {\n    parsed = JSON.parse(editableSource.value);\n  } catch (err) {\n    alert(errorMessage(err));\n    return;\n  }\n  const requested = storyRef();\n  const response = await api.call(\"updateScript\", {\n    ...requested,\n    script: parsed,\n  });\n  // The write landed in the script it was asked for. Committing it after the user moved on\n  // would put that script into the card now on screen, and re-initialize against it (#3014) —\n  // the same shape the deck editor's `resetForScriptChange` closes on its own path.\n  if (staleSince(requested)) return;\n  if (!response.ok) {\n    alert(response.error || \"Update failed\");\n    return;\n  }\n\n  // Update the UI with the new script. commitScript emits first so the parent\n  // data is updated (its handleUpdateResult uses in-place Object.assign, so\n  // the prop watcher won't fire), then we manually re-initialize the view.\n  commitScript(parsed);\n\n  if (sourceDetails.value) sourceDetails.value.open = false;\n  await initializeScript();\n}\n\nasync function copyText() {\n  await copy(scriptSourceText.value);\n}\n\nfunction effectiveBeat(index: number): Beat {\n  return effectiveBeatOf(localOverrides, beats.value, index);\n}\n\nfunction toggleSource(index: number) {\n  if (!sourceOpen[index]) {\n    sourceText[index] = toScriptSourceText(effectiveBeat(index));\n    Reflect.deleteProperty(beatSaveErrors, index);\n  }\n  sourceOpen[index] = !sourceOpen[index];\n}\n\nfunction isValidBeat(index: number): boolean {\n  return isValidBeatOf(sourceText[index], mulmoBeatSchema);\n}\n\nasync function updateBeat(index: number) {\n  let beat: Beat;\n  try {\n    // An absent slot parses as invalid JSON, landing on the same\n    // `invalidJson` branch a genuinely malformed edit would.\n    beat = JSON.parse(sourceText[index] ?? \"\");\n  } catch (err) {\n    beatSaveErrors[index] = { kind: \"invalidJson\", error: errorMessage(err) };\n    return;\n  }\n  const prevImage = JSON.stringify(effectiveBeat(index).image);\n  const prevText = effectiveBeat(index).text;\n\n  const requested = storyRef();\n  Reflect.deleteProperty(beatSaveErrors, index);\n  beatSaving[index] = true;\n  const response = await api.call(\"updateBeat\", {\n    ...requested,\n    beatIndex: index,\n    beat,\n  });\n  if (staleSince(requested)) return;\n  Reflect.deleteProperty(beatSaving, index);\n  if (!response.ok) {\n    beatSaveErrors[index] = { kind: \"saveFailed\", error: response.error };\n    return;\n  }\n\n  localOverrides[index] = beat;\n  sourceOpen[index] = false;\n\n  if (JSON.stringify(beat.image) !== prevImage) {\n    Reflect.deleteProperty(renderedImages, index);\n    renderBeat(index);\n  }\n\n  // Audio files are content-addressed by the beat's text\n  // (getBeatAudioPathOrUrl hashes text + voice), so after a text edit\n  // the cached data URI belongs to the OLD narration. Drop it so the\n  // \"Generate Audio\" button reappears, then re-probe — if the new text\n  // matches previously generated audio (e.g. the edit was a revert),\n  // the probe restores Play without a paid TTS call.\n  if (beat.text !== prevText) {\n    // If this beat's old narration is mid-playback, stop it first —\n    // the deletes below remove the Play/Stop control from the row,\n    // which would otherwise leave the stale audio playing with no\n    // way to stop it (Codex review on #2143).\n    if (playingAudio.value?.index === index) stopAllPlayback();\n    Reflect.deleteProperty(beatAudios, index);\n    Reflect.deleteProperty(audioState, index);\n    Reflect.deleteProperty(audioErrors, index);\n    if (beat.text) void loadExistingBeatAudio(index);\n  }\n}\n\nasync function renderBeat(index: number) {\n  const requested = storyRef();\n  renderState[index] = \"rendering\";\n  const response = await api.call(\"renderBeat\", {\n    ...requested,\n    beatIndex: index,\n    chatSessionId: chatSessionId.value,\n  });\n  if (staleSince(requested)) return;\n  if (!response.ok) {\n    renderErrors[index] = response.error || \"Render failed\";\n    renderState[index] = \"error\";\n    return;\n  }\n  renderedImages[index] = response.data.image ?? \"\";\n  renderState[index] = \"done\";\n  refreshMissingCharacterImages();\n  if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\nasync function regenerateBeat(index: number) {\n  const requested = storyRef();\n  Reflect.deleteProperty(renderedImages, index);\n  invalidateBeatMovie(index);\n  renderState[index] = \"rendering\";\n  const response = await api.call(\"renderBeat\", {\n    ...requested,\n    beatIndex: index,\n    force: true,\n    chatSessionId: chatSessionId.value,\n  });\n  if (staleSince(requested)) return;\n  if (!response.ok) {\n    renderErrors[index] = response.error || \"Render failed\";\n    renderState[index] = \"error\";\n    return;\n  }\n  renderedImages[index] = response.data.image ?? \"\";\n  renderState[index] = \"done\";\n  if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\n// Stale-response guard shared by every per-beat/character loader and\n// mutator below: capture the wire ref at call time and discard the\n// response when the user has navigated to a different result meanwhile —\n// otherwise late responses from script A's bulk mount-time probes would\n// write into the per-beat maps that now belong to script B. The ref is the\n// PAIR `(root, filePath)`: the same path exists in every root (#3014).\nfunction staleSince(requested: StoryRef): boolean {\n  return staleSinceOf(storyRef(), requested);\n}\n\nasync function loadExistingBeatImage(index: number) {\n  const requested = storyRef();\n  const response = await api.call(\"beatImage\", { ...requested, beatIndex: index });\n  if (staleSince(requested)) return;\n  // silently ignore errors — image simply hasn't been generated yet\n  if (response.ok && response.data.image) {\n    renderedImages[index] = response.data.image;\n    renderState[index] = \"done\";\n  }\n}\n\nasync function loadExistingBeatAudio(index: number) {\n  const requested = storyRef();\n  const response = await api.call(\"beatAudio\", { ...requested, beatIndex: index });\n  if (staleSince(requested)) return;\n  // silently ignore errors\n  if (response.ok && response.data.audio) {\n    beatAudios[index] = response.data.audio;\n    audioState[index] = \"done\";\n  }\n}\n\nasync function generateAudio(index: number) {\n  const requested = storyRef();\n  audioState[index] = \"generating\";\n  Reflect.deleteProperty(audioErrors, index);\n  const response = await api.call(\"generateBeatAudio\", {\n    ...requested,\n    beatIndex: index,\n    chatSessionId: chatSessionId.value,\n  });\n  if (staleSince(requested)) return;\n  if (!response.ok) {\n    audioErrors[index] = response.error || \"Audio generation failed\";\n    audioState[index] = \"error\";\n    return;\n  }\n  beatAudios[index] = response.data.audio ?? \"\";\n  audioState[index] = \"done\";\n}\n\nfunction playAudio(index: number) {\n  if (playingAudio.value) {\n    playingAudio.value.audio.pause();\n    const wasIndex = playingAudio.value.index;\n    playingAudio.value = null;\n    if (wasIndex === index) return;\n  }\n  const src = beatAudios[index];\n  if (!src) return;\n  const audio = new Audio(src);\n  playingAudio.value = { index, audio };\n  audioProgress.value = 0;\n  audio.addEventListener(\"timeupdate\", () => {\n    if (playingAudio.value?.index !== index) return;\n    if (audio.duration > 0) audioProgress.value = audio.currentTime / audio.duration;\n  });\n  audio.addEventListener(\"ended\", () => {\n    if (playingAudio.value?.index !== index) return;\n    playingAudio.value = null;\n    audioProgress.value = 0;\n    if (lightbox.value?.index === index) advanceFromBeat(index);\n  });\n  audio.play();\n}\n\nfunction onBeatDragOver(event: DragEvent, index: number) {\n  if (!event.dataTransfer?.types.includes(\"Files\")) return;\n  event.preventDefault();\n  beatDragOver[index] = true;\n}\n\nfunction onBeatDragLeave(index: number) {\n  beatDragOver[index] = false;\n}\n\nasync function onBeatDrop(event: DragEvent, index: number) {\n  event.preventDefault();\n  beatDragOver[index] = false;\n  const file = event.dataTransfer?.files[0];\n  if (!file || !file.type.startsWith(\"image/\")) return;\n\n  renderState[index] = \"rendering\";\n  Reflect.deleteProperty(renderErrors, index);\n  let imageData: string;\n  try {\n    imageData = await readFileAsDataUrl(file);\n  } catch (err) {\n    renderErrors[index] = errorMessage(err);\n    renderState[index] = \"error\";\n    return;\n  }\n  const requested = storyRef();\n  const response = await api.call(\"uploadBeatImage\", {\n    ...requested,\n    beatIndex: index,\n    imageData,\n  });\n  if (staleSince(requested)) return;\n  if (!response.ok) {\n    renderErrors[index] = response.error || \"Upload failed\";\n    renderState[index] = \"error\";\n    return;\n  }\n  renderedImages[index] = response.data.image ?? \"\";\n  renderState[index] = \"done\";\n}\n\nfunction openCharacterLightbox(key: string) {\n  // Stop both audio and silent timer — character lightbox is\n  // outside the play loop (#1073).\n  stopAllPlayback();\n  lightbox.value = {\n    src: charImages[key] ?? \"\",\n    text: key,\n    index: -1,\n    isCharacter: true,\n  };\n}\n\n// Probe the server for an existing beat PNG before triggering any\n// generation. Only auto-renders when the disk is empty AND the beat\n// is a deterministic type — imagePrompt beats are left empty so the\n// user clicks Generate explicitly (avoids surprise paid text2image\n// calls on every page refresh).\nasync function hydrateBeatImage(beat: Beat, index: number, hasCharacters: boolean, autoRenderTypes: readonly string[]): Promise<void> {\n  await loadExistingBeatImage(index);\n  if (renderedImages[index]) return;\n  if (shouldAutoRenderBeat(beat, hasCharacters, autoRenderTypes)) {\n    await renderBeat(index);\n  }\n}\n\n/**\n * #1074 — keep the in-memory toolResult in sync with the on-disk\n * script file. `updateBeat` / `updateScript` persist edits to\n * disk, but the session entry that backs\n * `props.selectedResult.data.script` is never rewritten, so a\n * page reload + session-restore would otherwise surface stale\n * pre-edit content.\n *\n * Why the reopen dispatch, not a generic file read: `filePath`\n * is the wire form `stories/<rel>` which only the mulmoScript save\n * op knows how to translate back to the real on-disk path under\n * `artifacts/stories/...`. The reopen op is read-only when `script`\n * is omitted; it does NOT trigger movie generation.\n *\n * The flow silently bails on every failure mode so a missing /\n * malformed / deleted script file never blocks the rest of\n * `initializeScript`.\n *\n * Stale-response guard: capture `uuid` + `filePath` before the\n * `await`. If either has changed by the time the response lands\n * (the user navigated to a different result while the request\n * was in flight, or `props.selectedResult` was swapped under us\n * by a parent watcher), drop the response on the floor — the new\n * `initializeScript` invocation triggered by that change will\n * issue its own refresh against the correct file.\n */\nasync function refreshScriptFromDisk(): Promise<void> {\n  const requested = storyRef();\n  if (!requested.filePath) return;\n  const requestedUuid = props.selectedResult.uuid;\n  const response = await api.call(\"save\", requested);\n  if (props.selectedResult.uuid !== requestedUuid || staleSince(requested)) return;\n  if (!response.ok) return;\n  const diskScript = response.data.script as MulmoScript | undefined;\n  // The server-side reopen op already validated against\n  // `mulmoScriptSchema`, so a non-null `script` is trusted here —\n  // we only need a presence check.\n  if (!diskScript) return;\n  if (isSameScript(diskScript, script.value)) return;\n  commitScript(diskScript);\n}\n\nasync function initializeScript() {\n  // Stop any in-flight playback BEFORE we tear down per-script state\n  // — a pending `silentPlaybackTimer` or running audio from the\n  // previous script would otherwise fire `advanceFromBeat()` against\n  // the new script's lightbox / beat list and either crash or\n  // silently jump the new presentation forward. Also close any open\n  // lightbox so the user lands on the clean View for the new result\n  // (Codex review iter-4 on #1365).\n  stopAllPlayback();\n  lightbox.value = null;\n  // Reset scroll position so new results start at the top\n  if (beatListEl.value) beatListEl.value.scrollTop = 0;\n  // Reset per-script state. resetMedia clears the movie/PDF spinners too —\n  // per-script, so switching away from a generating script doesn't leave the\n  // new script's toolbar spinning; the pendingGenerations snapshot below\n  // re-lights them when the NEW script really does have work in flight.\n  clearReactiveRecords(\n    renderState,\n    renderedImages,\n    renderErrors,\n    sourceOpen,\n    sourceText,\n    beatSaveErrors,\n    beatSaving,\n    localOverrides,\n    beatAudios,\n    audioState,\n    audioErrors,\n    beatDragOver,\n  );\n  // Same reason as `beatSaveErrors` above: this View re-initializes in place on a result\n  // switch, so anything the previous script left behind — the failure banner, an answer still\n  // in flight, an edit still queued — would land on the new one.\n  resetForScriptChange();\n  resetCharacters();\n  resetBeatMovies();\n  resetMedia();\n  if (sourceDetails.value) sourceDetails.value.open = false;\n\n  // #1074 — re-read the script file from disk before per-beat\n  // hydration. When the user switches between tool results inside\n  // the same SPA mount and switches back, the in-memory toolResult\n  // still carries whatever script was captured earlier, and\n  // `localOverrides` (the only thing showing the user's edit since\n  // the last save) is reset by initializeScript on remount.\n  // Re-fetching from disk via the reopen op covers that gap.\n  await refreshScriptFromDisk();\n\n  // Mount-time policy: prefer the existing PNG on the server. Every\n  // beat — deterministic AND imagePrompt — first probes beatImage,\n  // and we only fall through to renderBeat() when the disk has nothing\n  // yet AND the type is safe to auto-render (deterministic content,\n  // no characters waiting). Without this probe a refresh would re-fire\n  // generateBeatImage for every beat, and for imagePrompt beats that\n  // means a paid text2image call against an image we already have.\n  //\n  // Stale-after-edit: if the user edits the script source the on-disk\n  // PNG is no longer in sync with the new content, but we don't try to\n  // detect that here — the per-beat ↺ button is one click away and a\n  // page refresh re-runs this same probe, so the user can opt back into\n  // a fresh render whenever they need to.\n  const AUTO_RENDER_TYPES = [\"textSlide\", \"markdown\", \"chart\", \"mermaid\", \"html_tailwind\", \"slide\"] as const;\n  const hasCharacters = characterKeys.value.length > 0;\n  beats.value.forEach((beat, index) => {\n    void hydrateBeatImage(beat, index, hasCharacters, AUTO_RENDER_TYPES);\n    if (beat.text) loadExistingBeatAudio(index);\n    if (beatMayHaveMovie(beat)) void loadExistingBeatMovie(index);\n  });\n\n  characterKeys.value.forEach((key) => loadExistingCharacterImage(key));\n\n  if (filePath.value) {\n    // Stale-response guard: if the user navigates to a different result\n    // while these calls are in flight, their answers describe the OLD\n    // script — drop them instead of stamping them onto the new one.\n    const requested = storyRef();\n    const isStale = () => staleSince(requested);\n\n    const response = await api.call(\"movieStatus\", requested);\n    if (isStale()) return;\n    if (response.ok && response.data.moviePath) {\n      moviePath.value = response.data.moviePath;\n    }\n    // ignore errors\n    // Also check whether a PDF was previously generated and is still\n    // newer than the source; status returns null otherwise so the UI\n    // re-offers the Generate button.\n    const pdfResponse = await api.call(\"pdfStatus\", requested);\n    if (isStale()) return;\n    if (pdfResponse.ok && pdfResponse.data.pdfPath) {\n      pdfPath.value = pdfResponse.data.pdfPath;\n    }\n\n    // Reflect any generations that were already in flight when we\n    // mounted (user switched away mid-generation and came back).\n    // Snapshot via dispatch; live updates arrive on the pubsub\n    // subscription below.\n    const pending = await api.call(\"pendingGenerations\", requested);\n    if (isStale()) return;\n    if (pending.ok) {\n      for (const entry of pending.data.pending) {\n        reflectGenerationStart(entry);\n      }\n    }\n  }\n}\n\nonMounted(initializeScript);\nwatch(() => props.selectedResult, initializeScript);\n\n// Keep the view in sync with generations running anywhere — this View's\n// own long-held dispatches, a parallel tab, the agent's background\n// autoGenerateMovie. The host publishes `generation` events on the\n// plugin pubsub channel (started + finished, per beat and per artifact);\n// on start we mirror the local \"rendering\" state so spinners show even\n// after a remount, on finish we reload the relevant asset off disk.\nconst unsubscribeGenerationEvents = api.onGenerationEvent({\n  filePath: () => filePath.value,\n  // The PAIR is the identity: `stories/deck.json` exists in every root, so filtering on the\n  // path alone puts ANOTHER repository's spinners on this card (#3014).\n  root: () => root.value,\n  handler: (event) => {\n    if (!event.done) {\n      reflectGenerationStart(event);\n      return;\n    }\n    // Fire-and-forget: swallow + log so a failed reload doesn't\n    // surface as an unhandled rejection.\n    reflectGenerationFinish(event).catch((err) => {\n      console.error(\"[presentMulmoScript] reload on finish failed:\", err);\n    });\n  },\n});\n\nfunction reflectGenerationStart(entry: MulmoScriptGenerationEvent): void {\n  if (entry.kind === \"beatImage\") {\n    const idx = Number(entry.key);\n    if (!renderedImages[idx]) renderState[idx] = \"rendering\";\n  } else if (entry.kind === \"beatAudio\") {\n    const idx = Number(entry.key);\n    if (!beatAudios[idx]) audioState[idx] = \"generating\";\n  } else if (entry.kind === \"characterImage\") {\n    if (!charImages[entry.key]) charRenderState[entry.key] = \"rendering\";\n  } else if (entry.kind === \"movie\") {\n    movieGenerating.value = true;\n  } else if (entry.kind === \"pdf\") {\n    pdfGenerating.value = true;\n  }\n}\n\nasync function reflectGenerationFinish(entry: MulmoScriptGenerationEvent): Promise<void> {\n  if (entry.kind === \"beatImage\") {\n    const idx = Number(entry.key);\n    await loadExistingBeatImage(idx);\n    if (beatMayHaveMovie(effectiveBeat(idx))) await loadExistingBeatMovie(idx);\n    if (renderState[idx] === \"rendering\") Reflect.deleteProperty(renderState, idx);\n    refreshMissingCharacterImages();\n  } else if (entry.kind === \"beatAudio\") {\n    const idx = Number(entry.key);\n    await loadExistingBeatAudio(idx);\n    if (audioState[idx] === \"generating\") Reflect.deleteProperty(audioState, idx);\n  } else if (entry.kind === \"characterImage\") {\n    await loadExistingCharacterImage(entry.key);\n    if (charRenderState[entry.key] === \"rendering\") {\n      Reflect.deleteProperty(charRenderState, entry.key);\n    }\n  } else if (entry.kind === \"movie\") {\n    movieGenerating.value = false;\n    await refreshMoviePath();\n  } else if (entry.kind === \"pdf\") {\n    pdfGenerating.value = false;\n    await refreshPdfPath();\n  }\n}\n</script>\n\n<style scoped>\n.bottom-bar-wrapper {\n  position: relative;\n  flex-shrink: 0;\n}\n\n.script-source {\n  padding: 0.5rem;\n  background: #f5f5f5;\n  border-top: 1px solid #e0e0e0;\n  font-family: Consolas, \"MS Gothic\", \"BIZ UDGothic\", monospace;\n  font-size: 0.85rem;\n}\n\n.script-source summary {\n  cursor: pointer;\n  user-select: none;\n  padding: 0.5rem;\n  background: #e8e8e8;\n  border-radius: 4px;\n  font-weight: 500;\n  color: #333;\n}\n\n.script-source[open] summary {\n  margin-bottom: 0.5rem;\n}\n\n.script-source summary:hover {\n  background: #d8d8d8;\n}\n\n.script-editor {\n  width: 100%;\n  height: 40vh;\n  padding: 1rem;\n  background: #ffffff;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n  color: #333;\n  font-family: \"Courier New\", \"MS Gothic\", \"BIZ UDGothic\", monospace;\n  font-size: 0.9rem;\n  resize: vertical;\n  margin-bottom: 0.5rem;\n  line-height: 1.5;\n}\n\n.script-editor:focus {\n  outline: none;\n  border-color: #4caf50;\n  box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);\n}\n\n.script-editor-invalid {\n  border-color: #ef4444;\n}\n\n.script-editor-invalid:focus {\n  border-color: #ef4444;\n  box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.1);\n}\n\n.editor-actions {\n  display: flex;\n  justify-content: space-between;\n}\n\n.apply-btn {\n  padding: 0.5rem 1rem;\n  background: #4caf50;\n  color: white;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 0.9rem;\n  transition: background 0.2s;\n  font-weight: 500;\n}\n\n.apply-btn:hover {\n  background: #45a049;\n}\n\n.apply-btn:disabled {\n  background: #cccccc;\n  color: #666666;\n  cursor: not-allowed;\n  opacity: 0.6;\n}\n\n.cancel-btn {\n  padding: 0.5rem 1rem;\n  background: #e0e0e0;\n  color: #333;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 0.9rem;\n  transition: background 0.2s;\n  font-weight: 500;\n}\n\n.cancel-btn:hover {\n  background: #d0d0d0;\n}\n\n.copy-btn {\n  position: absolute;\n  bottom: 0.3rem;\n  right: 0.65rem;\n  padding: 0.4rem;\n  background: none;\n  border: none;\n  color: #333;\n  cursor: pointer;\n  z-index: 1;\n}\n\n.copy-btn:hover {\n  color: #000;\n}\n\n.copy-btn .material-icons {\n  font-size: 1.15rem;\n}\n</style>\n","<template>\n  <div class=\"h-full bg-white flex flex-col overflow-hidden\">\n    <!-- Header -->\n    <div class=\"flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0\">\n      <div class=\"min-w-0 flex-1\">\n        <h2 class=\"text-lg font-semibold text-gray-800 truncate\" data-testid=\"mulmo-script-title\">\n          {{ script.title || \"Untitled Script\" }}\n        </h2>\n        <p v-if=\"script.description\" class=\"text-sm text-gray-500 mt-0.5 truncate\" data-testid=\"mulmo-script-description\">\n          {{ script.description }}\n        </p>\n        <div class=\"flex items-center gap-3 mt-1 text-xs text-gray-400\">\n          <span>{{ m.beatCount(beats.length) }}</span>\n          <span v-if=\"script.lang\">{{ script.lang }}</span>\n          <span v-if=\"filePath\" class=\"truncate\">{{ filePath }}</span>\n        </div>\n      </div>\n      <MulmoScriptToolbar\n        :movie-path=\"moviePath\"\n        :movie-generating=\"movieGenerating\"\n        :movie-downloading=\"movieDownloading\"\n        :is-play-ready=\"isPlayReady\"\n        :can-fetch-media=\"canFetchMedia\"\n        :pdf-path=\"pdfPath\"\n        :pdf-generating=\"pdfGenerating\"\n        :pdf-downloading=\"pdfDownloading\"\n        @play=\"playPresentation\"\n        @generate-movie=\"generateMovie\"\n        @download-movie=\"downloadMovie\"\n        @generate-pdf=\"generatePdf\"\n        @download-pdf=\"downloadPdf\"\n      />\n    </div>\n\n    <!--\n      Inline error chip for movie-generation failures (#1197).\n      Previously the catch arm of `generateMovie` raised an `alert()` —\n      blocking, no retry path, and many users just dismissed the modal\n      and saw a stalled spinner with no explanation. The chip stays\n      visible until the next generate attempt clears it.\n    -->\n    <div\n      v-if=\"movieError\"\n      data-testid=\"mulmo-script-movie-error-chip\"\n      class=\"bg-red-50 border border-red-200 text-red-800 text-xs px-3 py-2 mx-4 mt-3 mb-1 rounded flex items-start gap-2\"\n    >\n      <span class=\"material-icons text-base shrink-0 mt-px\">error_outline</span>\n      <div class=\"flex-1 min-w-0\">\n        <div class=\"font-medium\">{{ m.movieGenerationFailed }}</div>\n        <div class=\"break-words whitespace-pre-wrap mt-0.5\">{{ movieError }}</div>\n      </div>\n      <button\n        class=\"shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50\"\n        :disabled=\"movieGenerating\"\n        data-testid=\"mulmo-script-movie-retry-button\"\n        @click=\"generateMovie\"\n      >\n        {{ m.retry }}\n      </button>\n    </div>\n\n    <!-- Characters section -->\n    <CharacterStrip\n      v-if=\"characterKeys.length > 0\"\n      :character-keys=\"characterKeys\"\n      :images=\"script.imageParams?.images\"\n      :thumbnails=\"charImages\"\n      :render-state=\"charRenderState\"\n      :errors=\"charErrors\"\n      :drag-over=\"charDragOver\"\n      :movie-generating=\"movieGenerating\"\n      :any-beat-rendering=\"anyBeatRendering\"\n      @generate-all=\"generateAllCharacters\"\n      @char-drag-over=\"onCharDragOver\"\n      @char-drag-leave=\"onCharDragLeave\"\n      @char-drop=\"onCharDrop\"\n      @open-lightbox=\"openCharacterLightbox\"\n      @render-character=\"renderCharacter\"\n    />\n\n    <!-- Deck editor (#1575, #2945): every beat is a slide → mount the interactive\n         editor from @mulmocast/beat-editor. Lazy-loaded via defineAsyncComponent, so\n         users whose scripts aren't decks never pay the bundle cost.\n\n         It takes and emits a beat ARRAY, so the script goes through beatsOf / withBeats\n         on the way in and out. Writing `{ ...script, beats }` by hand instead drops\n         presentationStyle and slideParams, and nothing tells you it happened.\n\n         No `layout` prop: the editor lays itself out from its own width, so the pane\n         moves below the list on a narrow host (this card) rather than beside it. -->\n    <!-- Two ways to look at the same script, not two kinds of script. The editor edits every\n         beat type; the list is where the media lives (generate audio, render an image, open a\n         clip), which the editor has no equivalent for — so neither replaces the other. -->\n    <div v-if=\"canEditBeats\" class=\"flex shrink-0 gap-1 px-2 pt-1 text-[11px]\">\n      <button type=\"button\" :class=\"beatPaneTabClass(beatPane === 'edit')\" data-testid=\"mulmo-script-tab-edit\" @click=\"beatPane = 'edit'\">\n        {{ m.editTab }}\n      </button>\n      <button type=\"button\" :class=\"beatPaneTabClass(beatPane === 'media')\" data-testid=\"mulmo-script-tab-media\" @click=\"beatPane = 'media'\">\n        {{ m.mediaTab }}\n      </button>\n    </div>\n\n    <!-- A deck save that failed, in the server's own words (#3070). The editor keeps showing the\n         edit either way, so without this the only difference between a save and a silent failure\n         is what comes back on the next reload. Shown in both panes: switching tabs does not make\n         an unsaved edit saved. -->\n    <div\n      v-if=\"deckSaveError\"\n      class=\"shrink-0 mx-2 mt-1 px-2 py-1 rounded bg-red-50 border border-red-200 text-xs text-red-700 break-words\"\n      role=\"alert\"\n      data-testid=\"mulmo-script-deck-save-error\"\n    >\n      {{ m.saveErrorSaveFailed(deckSaveError) }}\n    </div>\n\n    <div v-if=\"showBeatEditor\" class=\"flex-1 overflow-hidden\" data-testid=\"mulmo-script-deck-editor\" @focusout=\"onDeckFocusOut\">\n      <BeatListEditor :beats=\"deckBeats\" @update:beats=\"onDeckBeatsUpdate\" />\n    </div>\n\n    <!-- Per-beat media list: thumbnails, narration, audio / image / movie generation. -->\n    <div v-else ref=\"beatListEl\" class=\"flex-1 overflow-y-auto p-2 space-y-1.5\">\n      <div v-for=\"(beat, index) in beats\" :key=\"index\" class=\"rounded-lg border border-gray-200 overflow-hidden\">\n        <!-- Beat body: thumbnail + narration side by side -->\n        <div class=\"flex gap-3 items-stretch\">\n          <!-- Thumbnail -->\n          <div\n            class=\"relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors\"\n            :class=\"beatDragOver[index] ? 'bg-blue-50' : ''\"\n            @dragover=\"onBeatDragOver($event, index)\"\n            @dragleave=\"onBeatDragLeave(index)\"\n            @drop=\"onBeatDrop($event, index)\"\n          >\n            <!-- Beat number badge (1-based). Sits above the drop-hint\n                 overlay and the inline video player so the index stays\n                 readable in every beat state. -->\n            <div\n              class=\"absolute top-1.5 left-1.5 z-10 px-1.5 py-0.5 rounded bg-black/55 text-white text-xs font-medium leading-none pointer-events-none\"\n              :data-testid=\"`mulmo-script-beat-number-${index}`\"\n            >\n              {{ index + 1 }}\n            </div>\n            <!-- Inline player for the beat's generated video clip.\n                 Replaces the thumbnail while open; the close button\n                 returns to the still image. -->\n            <template v-if=\"beatMovieOpen[index] && beatMovieUrls[index]\">\n              <video :src=\"beatMovieUrls[index]\" class=\"w-full object-contain\" controls autoplay :data-testid=\"`mulmo-script-beat-movie-player-${index}`\" />\n              <button\n                class=\"absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50\"\n                :title=\"m.close\"\n                :aria-label=\"m.close\"\n                :data-testid=\"`mulmo-script-beat-movie-close-${index}`\"\n                @click.stop=\"closeBeatMovie(index)\"\n              >\n                <span class=\"material-icons text-sm\">close</span>\n              </button>\n            </template>\n            <template v-else>\n              <img\n                v-if=\"renderedImages[index]\"\n                :src=\"renderedImages[index]\"\n                class=\"w-full object-contain cursor-zoom-in\"\n                :alt=\"`Beat ${index + 1}`\"\n                @click=\"openLightbox(index)\"\n              />\n              <!-- Play overlay: shown when the beat-movie probe found a\n                   generated clip for this beat. Blob is fetched lazily\n                   on first click (host-authenticated), hence the spinner. -->\n              <button\n                v-if=\"renderedImages[index] && beatMovies[index] && canFetchMedia\"\n                class=\"absolute inset-0 m-auto w-12 h-12 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70\"\n                :title=\"m.play\"\n                :aria-label=\"m.play\"\n                :data-testid=\"`mulmo-script-beat-movie-play-${index}`\"\n                @click.stop=\"playBeatMovie(index)\"\n              >\n                <svg v-if=\"beatMovieLoading[index]\" class=\"animate-spin w-5 h-5\" viewBox=\"0 0 24 24\" fill=\"none\">\n                  <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n                  <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n                </svg>\n                <span v-else class=\"material-icons text-3xl\">play_arrow</span>\n              </button>\n              <button\n                v-if=\"renderedImages[index] && renderState[index] !== 'rendering'\"\n                class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed\"\n                :disabled=\"movieGenerating\"\n                @click.stop=\"regenerateBeat(index)\"\n              >\n                ↺\n              </button>\n              <div v-else-if=\"!renderedImages[index]\" class=\"w-full aspect-video flex flex-col items-center justify-center gap-1 p-2\">\n                <template v-if=\"renderState[index] === 'rendering' || (movieGenerating && !renderedImages[index] && effectiveBeat(index).imagePrompt)\">\n                  <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n                    <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n                    <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n                  </svg>\n                  <span class=\"text-xs text-green-500\">{{ m.rendering }}</span>\n                </template>\n                <template v-else-if=\"renderState[index] === 'error'\">\n                  <span class=\"text-xs text-red-400 text-center\">{{ renderErrors[index] }}</span>\n                </template>\n                <template v-else>\n                  <span v-if=\"effectiveBeat(index).imagePrompt\" class=\"text-xs text-gray-400 text-center italic leading-relaxed px-1\">{{\n                    effectiveBeat(index).imagePrompt\n                  }}</span>\n                  <span v-else class=\"text-xs text-gray-300\">{{ beat.image?.type ?? \"—\" }}</span>\n                </template>\n              </div>\n            </template>\n            <!-- Beat drop hint / overlay -->\n            <div v-if=\"beatDragOver[index]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n              <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n            </div>\n            <div\n              v-else-if=\"!renderedImages[index] && renderState[index] !== 'rendering'\"\n              class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\"\n            >\n              {{ m.orDropImage }}\n            </div>\n            <!-- Generate button for any beat without a rendered image.\n                 renderBeat works for every beat type: imagePrompt /\n                 typed image beats render directly, moviePrompt beats\n                 get a frame extracted from the generated clip, and\n                 text-only beats fall back to a prompt derived from\n                 the narration text (mulmocast prompt.js). -->\n            <button\n              v-if=\"!renderedImages[index] && renderState[index] !== 'rendering' && !movieGenerating && !isBeatImageReference(effectiveBeat(index))\"\n              class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50\"\n              @click=\"renderBeat(index)\"\n            >\n              {{ m.generate }}\n            </button>\n          </div>\n\n          <!-- Narration text -->\n          <div class=\"flex flex-col flex-1 min-w-0 px-2 py-1.5\">\n            <span class=\"text-sm text-gray-800 leading-relaxed\">{{ effectiveBeat(index).text }}</span>\n            <div class=\"flex justify-between mt-auto pt-1\">\n              <!-- Audio controls -->\n              <div class=\"flex items-center gap-1\">\n                <template v-if=\"audioState[index] === 'generating' || (movieGenerating && !beatAudios[index] && effectiveBeat(index).text)\">\n                  <svg class=\"animate-spin w-3 h-3 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n                    <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n                    <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n                  </svg>\n                </template>\n                <button\n                  v-else-if=\"beatAudios[index]\"\n                  class=\"text-xs px-2 py-0.5 rounded border\"\n                  :class=\"playingAudio?.index === index ? 'border-red-400 text-red-600 hover:bg-red-50' : 'border-green-400 text-green-600 hover:bg-green-50'\"\n                  @click=\"playAudio(index)\"\n                >\n                  {{ playingAudio?.index === index ? m.stop : m.play }}\n                </button>\n                <template v-else-if=\"audioErrors[index]\">\n                  <span class=\"text-xs text-red-400 truncate min-w-0 max-w-[20rem]\" :title=\"audioErrors[index]\">\n                    {{ m.errPrefix }} {{ audioErrors[index] }}\n                  </span>\n                  <button\n                    v-if=\"effectiveBeat(index).text\"\n                    class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n                    :disabled=\"movieGenerating\"\n                    @click=\"generateAudio(index)\"\n                  >\n                    ↺\n                  </button>\n                </template>\n                <button\n                  v-else-if=\"effectiveBeat(index).text\"\n                  class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50\"\n                  @click=\"generateAudio(index)\"\n                >\n                  {{ m.generateAudio }}\n                </button>\n              </div>\n              <button\n                class=\"text-gray-400 hover:text-gray-600\"\n                :title=\"sourceOpen[index] ? 'Hide source' : 'Show source'\"\n                :data-testid=\"`mulmo-script-beat-source-toggle-${index}`\"\n                @click=\"toggleSource(index)\"\n              >\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  class=\"w-3.5 h-3.5\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                >\n                  <polyline points=\"16 18 22 12 16 6\" />\n                  <polyline points=\"8 6 2 12 8 18\" />\n                </svg>\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <!-- Source editor -->\n        <div v-if=\"sourceOpen[index]\" class=\"border-t border-gray-100\">\n          <textarea\n            v-model=\"sourceText[index]\"\n            class=\"w-full text-xs text-gray-600 bg-gray-50 p-2 font-mono resize-none\"\n            :class=\"isValidBeat(index) ? 'outline-none' : 'outline outline-2 outline-red-400'\"\n            rows=\"8\"\n            spellcheck=\"false\"\n            :data-testid=\"`mulmo-script-beat-source-textarea-${index}`\"\n          />\n          <div class=\"flex items-center justify-end gap-2 px-2 pb-2\">\n            <span v-if=\"beatSaveErrors[index]\" class=\"text-xs text-red-600\" role=\"alert\">{{\n              beatSaveErrors[index].kind === \"invalidJson\"\n                ? m.saveErrorInvalidJson(beatSaveErrors[index].error)\n                : m.saveErrorSaveFailed(beatSaveErrors[index].error)\n            }}</span>\n            <button\n              class=\"px-2 py-1 text-xs rounded border\"\n              :class=\"\n                isValidBeat(index) && !beatSaving[index]\n                  ? 'border-blue-400 text-blue-600 hover:bg-blue-50 cursor-pointer'\n                  : 'border-gray-200 text-gray-300 cursor-not-allowed'\n              \"\n              :disabled=\"!isValidBeat(index) || !!beatSaving[index]\"\n              :data-testid=\"`mulmo-script-beat-update-button-${index}`\"\n              @click=\"updateBeat(index)\"\n            >\n              {{ beatSaving[index] ? m.saving : m.update }}\n            </button>\n          </div>\n        </div>\n      </div>\n\n      <div v-if=\"beats.length === 0\" class=\"flex items-center justify-center h-32 text-gray-400 text-sm\">{{ m.noBeats }}</div>\n    </div>\n\n    <!-- Bottom bar: Edit Script Source + Copy -->\n    <div class=\"bottom-bar-wrapper\">\n      <details ref=\"sourceDetails\" class=\"script-source\" @toggle=\"onSourceToggle(($event.target as HTMLDetailsElement).open)\">\n        <summary>{{ m.editSource }}</summary>\n        <textarea\n          v-model=\"editableSource\"\n          class=\"script-editor\"\n          :class=\"{ 'script-editor-invalid': sourceChanged && !sourceValid }\"\n          spellcheck=\"false\"\n        ></textarea>\n        <div class=\"editor-actions\">\n          <button class=\"apply-btn\" :disabled=\"!sourceChanged || !sourceValid\" @click=\"applySource\">{{ m.applyChanges }}</button>\n          <button class=\"cancel-btn\" @click=\"cancelSourceEdit\">{{ m.cancel }}</button>\n        </div>\n      </details>\n      <button v-show=\"!editing\" class=\"copy-btn\" :title=\"copied ? 'Copied!' : 'Copy'\" @click=\"copyText\">\n        <span class=\"material-icons\">{{ copied ? \"check\" : \"content_copy\" }}</span>\n      </button>\n    </div>\n\n    <!-- Lightbox -->\n    <BeatLightbox\n      v-if=\"lightbox\"\n      :lightbox=\"lightbox\"\n      :beat-count=\"beats.length\"\n      :beat-texts=\"beatTexts\"\n      :has-prev=\"hasPrev\"\n      :has-next=\"hasNext\"\n      :playing-audio-index=\"playingAudio?.index ?? null\"\n      :audio-progress=\"audioProgress\"\n      :has-current-audio=\"Boolean(beatAudios[lightbox.index])\"\n      @close=\"closeLightbox\"\n      @move=\"lightboxMove\"\n      @jump=\"jumpToBeat\"\n      @play-audio=\"playAudio\"\n    />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, watch } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport { mulmoBeatSchema, mulmoScriptSchema } from \"@mulmocast/types\";\nimport type { MulmoScriptData } from \"../core/types\";\nimport type { MulmoScriptGenerationEvent } from \"../core/contract\";\nimport {\n  isSameScript,\n  beatMayHaveMovie,\n  shouldAutoRenderBeat,\n  effectiveBeat as effectiveBeatOf,\n  isBeatImageReference,\n  isValidBeat as isValidBeatOf,\n  staleSince as staleSinceOf,\n  type StoryRef,\n  scriptSourceText as toScriptSourceText,\n  resolveSilentAdvanceSeconds,\n  clearReactiveRecords,\n  focusLeftContainer,\n  type Beat,\n} from \"./helpers\";\nimport { beatsOf, withBeats, type EditableBeat } from \"@mulmocast/beat-editor\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { readFileAsDataUrl, useClipboardCopy } from \"./support\";\nimport { useMulmoScriptTransport } from \"./transport\";\nimport { useHostAdapter } from \"./hostAdapter\";\nimport { useMediaExport } from \"./composables/useMediaExport\";\nimport { useBeatMovie } from \"./composables/useBeatMovie\";\nimport { useCharacterImages } from \"./composables/useCharacterImages\";\nimport { useDeckEditor } from \"./composables/useDeckEditor\";\nimport type { LightboxState, MulmoScript } from \"./viewTypes\";\nimport BeatLightbox from \"./components/BeatLightbox.vue\";\nimport CharacterStrip from \"./components/CharacterStrip.vue\";\nimport MulmoScriptToolbar from \"./components/MulmoScriptToolbar.vue\";\nimport { useT } from \"../lang/index\";\n\n// Lazy-loaded so the editor's Vue / tailwind chunk stays out of the initial\n// bundle for users whose scripts aren't decks\n// (movies, html_tailwind animations, mixed beats). `defineAsyncComponent`\n// triggers the dynamic import only when `isDeck` first flips true.\nconst BeatListEditor = defineAsyncComponent(() => import(\"@mulmocast/beat-editor\").then((mod) => mod.BeatListEditor));\n\nconst api = useMulmoScriptTransport();\nconst adapter = useHostAdapter();\n// Media bytes (movie / PDF / beat clips) are served behind host auth; hosts\n// opt in by injecting `fetchMediaBlob`. Without it the download / clip-play\n// affordances are hidden (the probes still run — state stays warm for a\n// host that injects later at remount).\nconst canFetchMedia = computed(() => Boolean(adapter.fetchMediaBlob));\n\nconst m = useT();\n\nconst props = defineProps<{\n  selectedResult: ToolResultComplete<MulmoScriptData>;\n}>();\nconst emit = defineEmits<{ updateResult: [result: ToolResultComplete] }>();\n\nconst data = computed(() => props.selectedResult.data);\nconst script = computed<MulmoScript>(() => data.value?.script ?? {});\nconst filePath = computed(() => data.value?.filePath ?? \"\");\n/**\n * Which registered root `filePath` is relative to; `undefined` = the host's default (#3014).\n *\n * The host puts it on the card when it opens a deck the user can see; the agent's tool schema\n * deliberately has no root, so a model cannot name one (#3015). Every dispatch below hands it\n * back, because `stories/deck.json` exists in EVERY root: without it the call addresses the\n * default root's file of that name, which is how a deck inside a repository opened fine and\n * then answered `File not found` to every save and every beat image\n * (receptron/mulmoterminal#1970).\n */\nconst root = computed(() => data.value?.root);\n/** The story as the wire addresses it — the pair that is its identity. */\nconst storyRef = (): StoryRef => ({ filePath: filePath.value, root: root.value });\nconst beats = computed<Beat[]>(() => script.value.beats ?? []);\n\n// Per-beat render state\ntype RenderState = \"idle\" | \"rendering\" | \"done\" | \"error\";\nconst renderState = reactive<Record<number, RenderState>>({});\nconst renderedImages = reactive<Record<number, string>>({});\nconst renderErrors = reactive<Record<number, string>>({});\nconst sourceOpen = reactive<Record<number, boolean>>({});\nconst sourceText = reactive<Record<number, string>>({});\n// Surface update-beat failures inline next to the Update button.\n// Cleared on next successful save or editor close. Store raw error +\n// kind tag so the template picks a localized message, instead of\n// pre-composing an English-prefixed string here.\ninterface BeatSaveError {\n  kind: \"invalidJson\" | \"saveFailed\";\n  error: string;\n}\nconst beatSaveErrors = reactive<Record<number, BeatSaveError>>({});\nconst beatSaving = reactive<Record<number, boolean>>({});\nconst localOverrides = reactive<Record<number, Beat>>({});\nconst beatAudios = reactive<Record<number, string>>({});\nconst audioState = reactive<Record<number, \"generating\" | \"done\" | \"error\">>({});\nconst audioErrors = reactive<Record<number, string>>({});\nconst playingAudio = ref<{ index: number; audio: HTMLAudioElement } | null>(null);\n// Tracks the auto-advance timer running on a silent beat\n// (`beat.text === \"\"`). Beats without text generate no audio, so the\n// Play loop falls back to a `setTimeout(beat.duration)` for cues —\n// without this, Play would stall on the first silent beat (#1073).\nconst silentPlaybackTimer = ref<{ index: number; timer: ReturnType<typeof setTimeout> } | null>(null);\nconst audioProgress = ref(0);\n\n// Default duration (seconds) for a silent beat whose script doesn't\n// set `duration` either. Picked to roughly match the time it takes a\n// reader to scan a `textSlide` — long enough to read, short enough\n// not to feel stuck. The script's own `duration` always wins.\nconst SILENT_BEAT_DEFAULT_SEC = 3;\nconst MS_PER_SECOND = 1000;\nconst beatListEl = ref<HTMLElement | null>(null);\nconst lightbox = ref<LightboxState | null>(null);\nconst beatDragOver = reactive<Record<number, boolean>>({});\n\nconst anyBeatRendering = computed(() => Object.values(renderState).some((state) => state === \"rendering\"));\n\n// Session tagging is host transport: MulmoClaude injects the active chat\n// session id so generations light its per-session sidebar indicator;\n// hosts without sessions leave the adapter empty and the field is simply\n// omitted from generation dispatches.\nconst chatSessionId = computed(() => adapter.chatSessionId?.value);\n\nconst {\n  moviePath,\n  movieGenerating,\n  movieDownloading,\n  movieError,\n  pdfPath,\n  pdfGenerating,\n  pdfDownloading,\n  generateMovie,\n  downloadMovie,\n  refreshMoviePath,\n  generatePdf,\n  downloadPdf,\n  refreshPdfPath,\n  resetMedia,\n} = useMediaExport({ api, adapter, filePath, root, chatSessionId });\n\nconst {\n  beatMovies,\n  beatMovieUrls,\n  beatMovieOpen,\n  beatMovieLoading,\n  loadExistingBeatMovie,\n  playBeatMovie,\n  closeBeatMovie,\n  invalidateBeatMovie,\n  resetBeatMovies,\n} = useBeatMovie({ api, adapter, filePath, root });\n\nconst {\n  charRenderState,\n  charImages,\n  charErrors,\n  charDragOver,\n  characterKeys,\n  onCharDragOver,\n  onCharDragLeave,\n  onCharDrop,\n  loadExistingCharacterImage,\n  refreshMissingCharacterImages,\n  renderCharacter,\n  generateAllCharacters,\n  resetCharacters,\n} = useCharacterImages({ api, filePath, root, chatSessionId, getImages: () => script.value.imageParams?.images });\n\nfunction stopPlayingAudio() {\n  // Single helper that clears both the audio path and the silent\n  // auto-advance timer — callers (lightbox open / arrow nav / Stop\n  // button) get consistent behaviour without remembering which\n  // playback mode the current beat was using (#1073).\n  stopAllPlayback();\n}\n\nfunction openLightbox(index: number) {\n  stopPlayingAudio();\n  lightbox.value = {\n    src: renderedImages[index] ?? \"\",\n    text: effectiveBeat(index).text,\n    index,\n  };\n}\n\n// Backdrop click handler. Stops any in-flight narration so the audio\n// doesn't keep playing after the lightbox is dismissed — without this,\n// the HTMLAudioElement created by playAudio() outlives the modal and\n// the user hears disembodied narration with no UI to stop it.\nfunction closeLightbox() {\n  stopPlayingAudio();\n  lightbox.value = null;\n}\n\n// \"Play presentation\" toolbar action. Opens the lightbox at beat 0 and\n// kicks off its narration audio; the existing on-ended hook then chains\n// through the rest of the deck (lightboxMove(1) → playAudio if the next\n// beat has audio), so one click runs the whole presentation. Only wired\n// to the toolbar button when moviePath is set, which is our proxy for\n// \"every beat has both image and audio on disk\".\n//\n// `moviePath` arrives synchronously from movieStatus, but the per-beat\n// image and audio data URIs are populated asynchronously by\n// loadExistingBeatImage / loadExistingBeatAudio in initializeScript().\n// The Play button can therefore become visible before beat 0's assets\n// hydrate — `isPlayReady` gates the click so the lightbox never opens\n// with an undefined src or silent narration on a beat that does have\n// text.\nconst isPlayReady = computed<boolean>(() => {\n  if (beats.value.length === 0) return false;\n  if (!renderedImages[0]) return false;\n  // Audio is only required when the beat has text (the source of TTS).\n  // Beats without text are valid; they just play silently.\n  if (effectiveBeat(0).text && !beatAudios[0]) return false;\n  return true;\n});\n\nfunction playPresentation() {\n  if (!isPlayReady.value) return;\n  openLightbox(0);\n  playBeat(0);\n}\n\n// Stop whichever playback handle is active. Idempotent. Called by\n// openLightbox, manual stop / pause buttons, and by `playBeat`\n// before kicking off a new beat so we never double-schedule. (#1073)\nfunction stopAllPlayback(): void {\n  if (playingAudio.value) {\n    playingAudio.value.audio.pause();\n    playingAudio.value = null;\n    audioProgress.value = 0;\n  }\n  if (silentPlaybackTimer.value) {\n    clearTimeout(silentPlaybackTimer.value.timer);\n    silentPlaybackTimer.value = null;\n  }\n}\n\n// Single entry point for \"start playback at beat <index>\". Routes\n// on what the script DECLARED, not on what's currently hydrated:\n//\n//   - `text` empty  → silent path (`scheduleSilentAdvance`). The\n//     schema says no audio is generated for empty-text beats, so\n//     `duration` drives auto-advance.\n//   - `text` present + audio loaded → audio path. `audio.ended`\n//     chains via `advanceFromBeat`.\n//   - `text` present + audio NOT loaded → stop. The Play button's\n//     `isPlayReady` gate prevented this for beat 0, but mid-stream\n//     a transient fetch miss must not silently skip the narration\n//     by falling through to the silent timer (Codex review on\n//     #1073 — gating on `beatAudios[index]` would do exactly that).\n//\n// Either path chains to the next beat via `advanceFromBeat`, so a\n// run of silent beats — or audio / silent / audio sequences —\n// plays through without manual interaction.\nfunction playBeat(index: number): void {\n  stopAllPlayback();\n  const hasText = Boolean(effectiveBeat(index).text);\n  if (!hasText) {\n    scheduleSilentAdvance(index);\n    return;\n  }\n  if (beatAudios[index]) {\n    playAudio(index);\n  }\n  // Text beat with no audio yet → stop. The user can re-click Play\n  // once the audio finishes hydrating.\n}\n\nfunction scheduleSilentAdvance(index: number): void {\n  // Defensively narrow the script-supplied duration (zero / negative / NaN /\n  // non-number → default) — a bad value would otherwise collapse to an\n  // immediate timeout and the Play loop would race through every silent beat\n  // in a single tick (Codex review iter-5 on #1365).\n  const seconds = resolveSilentAdvanceSeconds(effectiveBeat(index).duration, SILENT_BEAT_DEFAULT_SEC);\n  const timer = setTimeout(() => {\n    if (silentPlaybackTimer.value?.index !== index) return;\n    silentPlaybackTimer.value = null;\n    if (lightbox.value?.index === index) advanceFromBeat(index);\n  }, seconds * MS_PER_SECOND);\n  silentPlaybackTimer.value = { index, timer };\n}\n\nfunction advanceFromBeat(fromIndex: number): void {\n  lightboxMove(1);\n  const nextIndex = lightbox.value?.index;\n  if (nextIndex === undefined || nextIndex === fromIndex) return;\n  playBeat(nextIndex);\n}\n\nconst hasPrev = computed(() => {\n  if (!lightbox.value) return false;\n  for (let i = lightbox.value.index - 1; i >= 0; i--) {\n    if (renderedImages[i]) return true;\n  }\n  return false;\n});\n\nconst hasNext = computed(() => {\n  if (!lightbox.value) return false;\n  for (let i = lightbox.value.index + 1; i < beats.value.length; i++) {\n    if (renderedImages[i]) return true;\n  }\n  return false;\n});\n\n// Narration text per beat, for the lightbox beat-strip hover tooltips. Reads\n// through `effectiveBeat` so an unsaved in-place edit shows its new text.\nconst beatTexts = computed(() => beats.value.map((_, index) => effectiveBeat(index).text));\n\nfunction jumpToBeat(index: number) {\n  if (!lightbox.value) return;\n  if (index === lightbox.value.index) return;\n  if (!renderedImages[index]) return;\n  // Carry the playback mode forward (audio OR silent timer) so a\n  // user clicking the beat-strip thumbnail mid-playback keeps the\n  // presentation rolling (#1073).\n  const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n  openLightbox(index);\n  if (wasPlaying) playBeat(index);\n}\n\nfunction lightboxMove(delta: number) {\n  if (!lightbox.value) return;\n  const total = beats.value.length;\n  // If a playback was in progress when the user clicked the arrow,\n  // carry it forward to whichever beat we land on — `playBeat`\n  // picks audio vs silent automatically. `openLightbox` stops the\n  // current playback, so capture the flag BEFORE that and chain\n  // AFTER. The on-ended / silent-advance paths already null their\n  // own state before calling `lightboxMove`, so this branch won't\n  // double-fire there.\n  const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n  let i = lightbox.value.index + delta;\n  while (i >= 0 && i < total) {\n    if (renderedImages[i]) {\n      openLightbox(i);\n      if (wasPlaying) playBeat(i);\n      return;\n    }\n    i += delta;\n  }\n}\nconst sourceDetails = ref<HTMLDetailsElement>();\nconst editing = ref(false);\nconst editableSource = ref(\"\");\nconst { copied, copy } = useClipboardCopy();\n\n// Beats may be edited in-place via `updateBeat()` and rendered through\n// `effectiveBeat()`, so the Copy / source-view text must read the merged\n// shape — otherwise the clipboard returns the original prop snapshot\n// until the full result is reloaded.\nconst effectiveScript = computed<MulmoScript>(() => ({\n  ...script.value,\n  beats: beats.value.map((beat, i) => localOverrides[i] ?? beat),\n}));\nconst scriptSourceText = computed(() => toScriptSourceText(effectiveScript.value));\n\n// Persist a saved script back into the parent's toolResult so the in-memory\n// script and reactive beats[] stay in sync without a remount. The parent's\n// handleUpdateResult uses Object.assign (in-place), so the prop watcher won't\n// fire — callers that need a re-read drive initializeScript themselves.\nfunction commitScript(next: MulmoScript): void {\n  emit(\"updateResult\", {\n    ...props.selectedResult,\n    data: { ...props.selectedResult.data, script: next },\n  });\n}\n\n// #1575 — when every beat is a `slide`, swap the per-beat list UI for the\n// interactive deck editor (@mulmocast/beat-editor). Mixed scripts (any non-slide\n// beat) fall back to the existing list. The debounce + flush-on-unmount live\n// in the composable.\nconst { canEditBeats, deckScriptInput, deckSaveError, resetForScriptChange, onDeckUpdate, flushPendingDeckSave, watchForeignWrites } = useDeckEditor({\n  api,\n  filePath,\n  root,\n  effectiveScript,\n  commitScript,\n});\n\n/**\n * Which pane the beats are shown in.\n *\n * `edit` is the beat editor — every beat type, edited in place. `media` is the per-beat list,\n * which is the only place audio / image / movie generation lives. A script with nothing to edit\n * has only the list, so the switch is hidden and this is ignored.\n *\n * `media` is the default because opening the script is what triggers rendering each beat's\n * image: the auto-render on mount lives in that list, so defaulting to `edit` silently stopped\n * thumbnails from being produced at all. Someone who wants to edit clicks once; nobody has to\n * click to get the previews they always got.\n */\nconst beatPane = ref<\"edit\" | \"media\">(\"media\");\nconst showBeatEditor = computed(() => canEditBeats.value && beatPane.value === \"edit\");\n\nconst BEAT_TAB_BASE = \"rounded px-2 py-0.5 font-sans\";\nconst beatPaneTabClass = (active: boolean) => [BEAT_TAB_BASE, active ? \"bg-gray-700 text-white\" : \"bg-gray-100 text-gray-600 hover:bg-gray-200\"];\n\n// An agent (or another window) wrote this script — pull it back off disk so the preview shows\n// what is actually there. Registered here rather than in the composable because reloading is\n// the View's job; the composable only knows that someone else wrote.\nconst unsubscribeForeignWrites = watchForeignWrites(() => {\n  void refreshScriptFromDisk();\n});\n\n// The editor takes and emits a beat array; the composable, the transport and the\n// toolResult all speak whole scripts. `beatsOf` / `withBeats` are the conversion, and\n// `withBeats` is what keeps presentationStyle / slideParams from being dropped on the\n// way back — `{ ...script, beats }` loses them silently.\nconst deckBeats = computed<EditableBeat[]>(() => beatsOf(deckScriptInput.value));\n\nfunction onDeckBeatsUpdate(beats: EditableBeat[]): void {\n  onDeckUpdate(withBeats(deckScriptInput.value, beats));\n}\n\n/**\n * Leaving the editor writes whatever is still in the debounce.\n *\n * Asking the agent to change the script means moving focus out of here first, so this lands\n * ahead of every request without the host having to announce one — MulmoTerminal's agent is a\n * terminal, and there is no \"sent\" event to hook. The debounce is short enough that it has\n * usually fired already; this closes the case where it has not, which would otherwise have the\n * agent read a file missing the last thing the user typed.\n */\nfunction onDeckFocusOut(event: FocusEvent): void {\n  const container = event.currentTarget instanceof Node ? event.currentTarget : null;\n  if (focusLeftContainer(container, event.relatedTarget)) flushPendingDeckSave();\n}\n\nonBeforeUnmount(() => {\n  flushPendingDeckSave();\n  // Release beat-clip blob object URLs — they outlive the component\n  // otherwise (document-scoped, not GC'd with it).\n  resetBeatMovies();\n  unsubscribeGenerationEvents();\n  unsubscribeForeignWrites();\n});\nconst loadedSource = ref(\"\");\nconst sourceChanged = computed(() => editableSource.value !== loadedSource.value);\nconst sourceValid = computed(() => {\n  try {\n    const parsed = JSON.parse(editableSource.value);\n    return mulmoScriptSchema.safeParse(parsed).success;\n  } catch {\n    return false;\n  }\n});\n\nasync function onSourceToggle(open: boolean) {\n  editing.value = open;\n  if (open) {\n    let text = scriptSourceText.value;\n    // Re-read the current file from disk so beat-level edits made\n    // since mount (other tabs, MCP, manual edits) surface in the\n    // editor. Uses the reopen dispatch for the same reason\n    // refreshScriptFromDisk does — `filePath.value` is the wire form\n    // `stories/<rel>` and only the mulmoScript save/reopen op knows\n    // how to map it to the on-disk path under `artifacts/stories/...`.\n    if (filePath.value) {\n      const requested = storyRef();\n      const response = await api.call(\"save\", requested);\n      // The disk read describes the script it was asked for. Navigating away during it would\n      // otherwise seed the source editor with ANOTHER deck's text (#3014).\n      if (staleSince(requested)) return;\n      const diskScript = response.ok ? (response.data.script as MulmoScript | undefined) : undefined;\n      if (diskScript) text = toScriptSourceText(diskScript);\n      // fall through to in-memory script on failure\n    }\n    editableSource.value = text;\n    loadedSource.value = text;\n  }\n}\n\nfunction cancelSourceEdit() {\n  if (sourceDetails.value) sourceDetails.value.open = false;\n}\n\nasync function applySource() {\n  let parsed: MulmoScript;\n  try {\n    parsed = JSON.parse(editableSource.value);\n  } catch (err) {\n    alert(errorMessage(err));\n    return;\n  }\n  const requested = storyRef();\n  const response = await api.call(\"updateScript\", {\n    ...requested,\n    script: parsed,\n  });\n  // The write landed in the script it was asked for. Committing it after the user moved on\n  // would put that script into the card now on screen, and re-initialize against it (#3014) —\n  // the same shape the deck editor's `resetForScriptChange` closes on its own path.\n  if (staleSince(requested)) return;\n  if (!response.ok) {\n    alert(response.error || \"Update failed\");\n    return;\n  }\n\n  // Update the UI with the new script. commitScript emits first so the parent\n  // data is updated (its handleUpdateResult uses in-place Object.assign, so\n  // the prop watcher won't fire), then we manually re-initialize the view.\n  commitScript(parsed);\n\n  if (sourceDetails.value) sourceDetails.value.open = false;\n  await initializeScript();\n}\n\nasync function copyText() {\n  await copy(scriptSourceText.value);\n}\n\nfunction effectiveBeat(index: number): Beat {\n  return effectiveBeatOf(localOverrides, beats.value, index);\n}\n\nfunction toggleSource(index: number) {\n  if (!sourceOpen[index]) {\n    sourceText[index] = toScriptSourceText(effectiveBeat(index));\n    Reflect.deleteProperty(beatSaveErrors, index);\n  }\n  sourceOpen[index] = !sourceOpen[index];\n}\n\nfunction isValidBeat(index: number): boolean {\n  return isValidBeatOf(sourceText[index], mulmoBeatSchema);\n}\n\nasync function updateBeat(index: number) {\n  let beat: Beat;\n  try {\n    // An absent slot parses as invalid JSON, landing on the same\n    // `invalidJson` branch a genuinely malformed edit would.\n    beat = JSON.parse(sourceText[index] ?? \"\");\n  } catch (err) {\n    beatSaveErrors[index] = { kind: \"invalidJson\", error: errorMessage(err) };\n    return;\n  }\n  const prevImage = JSON.stringify(effectiveBeat(index).image);\n  const prevText = effectiveBeat(index).text;\n\n  const requested = storyRef();\n  Reflect.deleteProperty(beatSaveErrors, index);\n  beatSaving[index] = true;\n  const response = await api.call(\"updateBeat\", {\n    ...requested,\n    beatIndex: index,\n    beat,\n  });\n  if (staleSince(requested)) return;\n  Reflect.deleteProperty(beatSaving, index);\n  if (!response.ok) {\n    beatSaveErrors[index] = { kind: \"saveFailed\", error: response.error };\n    return;\n  }\n\n  localOverrides[index] = beat;\n  sourceOpen[index] = false;\n\n  if (JSON.stringify(beat.image) !== prevImage) {\n    Reflect.deleteProperty(renderedImages, index);\n    renderBeat(index);\n  }\n\n  // Audio files are content-addressed by the beat's text\n  // (getBeatAudioPathOrUrl hashes text + voice), so after a text edit\n  // the cached data URI belongs to the OLD narration. Drop it so the\n  // \"Generate Audio\" button reappears, then re-probe — if the new text\n  // matches previously generated audio (e.g. the edit was a revert),\n  // the probe restores Play without a paid TTS call.\n  if (beat.text !== prevText) {\n    // If this beat's old narration is mid-playback, stop it first —\n    // the deletes below remove the Play/Stop control from the row,\n    // which would otherwise leave the stale audio playing with no\n    // way to stop it (Codex review on #2143).\n    if (playingAudio.value?.index === index) stopAllPlayback();\n    Reflect.deleteProperty(beatAudios, index);\n    Reflect.deleteProperty(audioState, index);\n    Reflect.deleteProperty(audioErrors, index);\n    if (beat.text) void loadExistingBeatAudio(index);\n  }\n}\n\nasync function renderBeat(index: number) {\n  const requested = storyRef();\n  renderState[index] = \"rendering\";\n  const response = await api.call(\"renderBeat\", {\n    ...requested,\n    beatIndex: index,\n    chatSessionId: chatSessionId.value,\n  });\n  if (staleSince(requested)) return;\n  if (!response.ok) {\n    renderErrors[index] = response.error || \"Render failed\";\n    renderState[index] = \"error\";\n    return;\n  }\n  renderedImages[index] = response.data.image ?? \"\";\n  renderState[index] = \"done\";\n  refreshMissingCharacterImages();\n  if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\nasync function regenerateBeat(index: number) {\n  const requested = storyRef();\n  Reflect.deleteProperty(renderedImages, index);\n  invalidateBeatMovie(index);\n  renderState[index] = \"rendering\";\n  const response = await api.call(\"renderBeat\", {\n    ...requested,\n    beatIndex: index,\n    force: true,\n    chatSessionId: chatSessionId.value,\n  });\n  if (staleSince(requested)) return;\n  if (!response.ok) {\n    renderErrors[index] = response.error || \"Render failed\";\n    renderState[index] = \"error\";\n    return;\n  }\n  renderedImages[index] = response.data.image ?? \"\";\n  renderState[index] = \"done\";\n  if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\n// Stale-response guard shared by every per-beat/character loader and\n// mutator below: capture the wire ref at call time and discard the\n// response when the user has navigated to a different result meanwhile —\n// otherwise late responses from script A's bulk mount-time probes would\n// write into the per-beat maps that now belong to script B. The ref is the\n// PAIR `(root, filePath)`: the same path exists in every root (#3014).\nfunction staleSince(requested: StoryRef): boolean {\n  return staleSinceOf(storyRef(), requested);\n}\n\nasync function loadExistingBeatImage(index: number) {\n  const requested = storyRef();\n  const response = await api.call(\"beatImage\", { ...requested, beatIndex: index });\n  if (staleSince(requested)) return;\n  // silently ignore errors — image simply hasn't been generated yet\n  if (response.ok && response.data.image) {\n    renderedImages[index] = response.data.image;\n    renderState[index] = \"done\";\n  }\n}\n\nasync function loadExistingBeatAudio(index: number) {\n  const requested = storyRef();\n  const response = await api.call(\"beatAudio\", { ...requested, beatIndex: index });\n  if (staleSince(requested)) return;\n  // silently ignore errors\n  if (response.ok && response.data.audio) {\n    beatAudios[index] = response.data.audio;\n    audioState[index] = \"done\";\n  }\n}\n\nasync function generateAudio(index: number) {\n  const requested = storyRef();\n  audioState[index] = \"generating\";\n  Reflect.deleteProperty(audioErrors, index);\n  const response = await api.call(\"generateBeatAudio\", {\n    ...requested,\n    beatIndex: index,\n    chatSessionId: chatSessionId.value,\n  });\n  if (staleSince(requested)) return;\n  if (!response.ok) {\n    audioErrors[index] = response.error || \"Audio generation failed\";\n    audioState[index] = \"error\";\n    return;\n  }\n  beatAudios[index] = response.data.audio ?? \"\";\n  audioState[index] = \"done\";\n}\n\nfunction playAudio(index: number) {\n  if (playingAudio.value) {\n    playingAudio.value.audio.pause();\n    const wasIndex = playingAudio.value.index;\n    playingAudio.value = null;\n    if (wasIndex === index) return;\n  }\n  const src = beatAudios[index];\n  if (!src) return;\n  const audio = new Audio(src);\n  playingAudio.value = { index, audio };\n  audioProgress.value = 0;\n  audio.addEventListener(\"timeupdate\", () => {\n    if (playingAudio.value?.index !== index) return;\n    if (audio.duration > 0) audioProgress.value = audio.currentTime / audio.duration;\n  });\n  audio.addEventListener(\"ended\", () => {\n    if (playingAudio.value?.index !== index) return;\n    playingAudio.value = null;\n    audioProgress.value = 0;\n    if (lightbox.value?.index === index) advanceFromBeat(index);\n  });\n  audio.play();\n}\n\nfunction onBeatDragOver(event: DragEvent, index: number) {\n  if (!event.dataTransfer?.types.includes(\"Files\")) return;\n  event.preventDefault();\n  beatDragOver[index] = true;\n}\n\nfunction onBeatDragLeave(index: number) {\n  beatDragOver[index] = false;\n}\n\nasync function onBeatDrop(event: DragEvent, index: number) {\n  event.preventDefault();\n  beatDragOver[index] = false;\n  const file = event.dataTransfer?.files[0];\n  if (!file || !file.type.startsWith(\"image/\")) return;\n\n  renderState[index] = \"rendering\";\n  Reflect.deleteProperty(renderErrors, index);\n  let imageData: string;\n  try {\n    imageData = await readFileAsDataUrl(file);\n  } catch (err) {\n    renderErrors[index] = errorMessage(err);\n    renderState[index] = \"error\";\n    return;\n  }\n  const requested = storyRef();\n  const response = await api.call(\"uploadBeatImage\", {\n    ...requested,\n    beatIndex: index,\n    imageData,\n  });\n  if (staleSince(requested)) return;\n  if (!response.ok) {\n    renderErrors[index] = response.error || \"Upload failed\";\n    renderState[index] = \"error\";\n    return;\n  }\n  renderedImages[index] = response.data.image ?? \"\";\n  renderState[index] = \"done\";\n}\n\nfunction openCharacterLightbox(key: string) {\n  // Stop both audio and silent timer — character lightbox is\n  // outside the play loop (#1073).\n  stopAllPlayback();\n  lightbox.value = {\n    src: charImages[key] ?? \"\",\n    text: key,\n    index: -1,\n    isCharacter: true,\n  };\n}\n\n// Probe the server for an existing beat PNG before triggering any\n// generation. Only auto-renders when the disk is empty AND the beat\n// is a deterministic type — imagePrompt beats are left empty so the\n// user clicks Generate explicitly (avoids surprise paid text2image\n// calls on every page refresh).\nasync function hydrateBeatImage(beat: Beat, index: number, hasCharacters: boolean, autoRenderTypes: readonly string[]): Promise<void> {\n  await loadExistingBeatImage(index);\n  if (renderedImages[index]) return;\n  if (shouldAutoRenderBeat(beat, hasCharacters, autoRenderTypes)) {\n    await renderBeat(index);\n  }\n}\n\n/**\n * #1074 — keep the in-memory toolResult in sync with the on-disk\n * script file. `updateBeat` / `updateScript` persist edits to\n * disk, but the session entry that backs\n * `props.selectedResult.data.script` is never rewritten, so a\n * page reload + session-restore would otherwise surface stale\n * pre-edit content.\n *\n * Why the reopen dispatch, not a generic file read: `filePath`\n * is the wire form `stories/<rel>` which only the mulmoScript save\n * op knows how to translate back to the real on-disk path under\n * `artifacts/stories/...`. The reopen op is read-only when `script`\n * is omitted; it does NOT trigger movie generation.\n *\n * The flow silently bails on every failure mode so a missing /\n * malformed / deleted script file never blocks the rest of\n * `initializeScript`.\n *\n * Stale-response guard: capture `uuid` + `filePath` before the\n * `await`. If either has changed by the time the response lands\n * (the user navigated to a different result while the request\n * was in flight, or `props.selectedResult` was swapped under us\n * by a parent watcher), drop the response on the floor — the new\n * `initializeScript` invocation triggered by that change will\n * issue its own refresh against the correct file.\n */\nasync function refreshScriptFromDisk(): Promise<void> {\n  const requested = storyRef();\n  if (!requested.filePath) return;\n  const requestedUuid = props.selectedResult.uuid;\n  const response = await api.call(\"save\", requested);\n  if (props.selectedResult.uuid !== requestedUuid || staleSince(requested)) return;\n  if (!response.ok) return;\n  const diskScript = response.data.script as MulmoScript | undefined;\n  // The server-side reopen op already validated against\n  // `mulmoScriptSchema`, so a non-null `script` is trusted here —\n  // we only need a presence check.\n  if (!diskScript) return;\n  if (isSameScript(diskScript, script.value)) return;\n  commitScript(diskScript);\n}\n\nasync function initializeScript() {\n  // Stop any in-flight playback BEFORE we tear down per-script state\n  // — a pending `silentPlaybackTimer` or running audio from the\n  // previous script would otherwise fire `advanceFromBeat()` against\n  // the new script's lightbox / beat list and either crash or\n  // silently jump the new presentation forward. Also close any open\n  // lightbox so the user lands on the clean View for the new result\n  // (Codex review iter-4 on #1365).\n  stopAllPlayback();\n  lightbox.value = null;\n  // Reset scroll position so new results start at the top\n  if (beatListEl.value) beatListEl.value.scrollTop = 0;\n  // Reset per-script state. resetMedia clears the movie/PDF spinners too —\n  // per-script, so switching away from a generating script doesn't leave the\n  // new script's toolbar spinning; the pendingGenerations snapshot below\n  // re-lights them when the NEW script really does have work in flight.\n  clearReactiveRecords(\n    renderState,\n    renderedImages,\n    renderErrors,\n    sourceOpen,\n    sourceText,\n    beatSaveErrors,\n    beatSaving,\n    localOverrides,\n    beatAudios,\n    audioState,\n    audioErrors,\n    beatDragOver,\n  );\n  // Same reason as `beatSaveErrors` above: this View re-initializes in place on a result\n  // switch, so anything the previous script left behind — the failure banner, an answer still\n  // in flight, an edit still queued — would land on the new one.\n  resetForScriptChange();\n  resetCharacters();\n  resetBeatMovies();\n  resetMedia();\n  if (sourceDetails.value) sourceDetails.value.open = false;\n\n  // #1074 — re-read the script file from disk before per-beat\n  // hydration. When the user switches between tool results inside\n  // the same SPA mount and switches back, the in-memory toolResult\n  // still carries whatever script was captured earlier, and\n  // `localOverrides` (the only thing showing the user's edit since\n  // the last save) is reset by initializeScript on remount.\n  // Re-fetching from disk via the reopen op covers that gap.\n  await refreshScriptFromDisk();\n\n  // Mount-time policy: prefer the existing PNG on the server. Every\n  // beat — deterministic AND imagePrompt — first probes beatImage,\n  // and we only fall through to renderBeat() when the disk has nothing\n  // yet AND the type is safe to auto-render (deterministic content,\n  // no characters waiting). Without this probe a refresh would re-fire\n  // generateBeatImage for every beat, and for imagePrompt beats that\n  // means a paid text2image call against an image we already have.\n  //\n  // Stale-after-edit: if the user edits the script source the on-disk\n  // PNG is no longer in sync with the new content, but we don't try to\n  // detect that here — the per-beat ↺ button is one click away and a\n  // page refresh re-runs this same probe, so the user can opt back into\n  // a fresh render whenever they need to.\n  const AUTO_RENDER_TYPES = [\"textSlide\", \"markdown\", \"chart\", \"mermaid\", \"html_tailwind\", \"slide\"] as const;\n  const hasCharacters = characterKeys.value.length > 0;\n  beats.value.forEach((beat, index) => {\n    void hydrateBeatImage(beat, index, hasCharacters, AUTO_RENDER_TYPES);\n    if (beat.text) loadExistingBeatAudio(index);\n    if (beatMayHaveMovie(beat)) void loadExistingBeatMovie(index);\n  });\n\n  characterKeys.value.forEach((key) => loadExistingCharacterImage(key));\n\n  if (filePath.value) {\n    // Stale-response guard: if the user navigates to a different result\n    // while these calls are in flight, their answers describe the OLD\n    // script — drop them instead of stamping them onto the new one.\n    const requested = storyRef();\n    const isStale = () => staleSince(requested);\n\n    const response = await api.call(\"movieStatus\", requested);\n    if (isStale()) return;\n    if (response.ok && response.data.moviePath) {\n      moviePath.value = response.data.moviePath;\n    }\n    // ignore errors\n    // Also check whether a PDF was previously generated and is still\n    // newer than the source; status returns null otherwise so the UI\n    // re-offers the Generate button.\n    const pdfResponse = await api.call(\"pdfStatus\", requested);\n    if (isStale()) return;\n    if (pdfResponse.ok && pdfResponse.data.pdfPath) {\n      pdfPath.value = pdfResponse.data.pdfPath;\n    }\n\n    // Reflect any generations that were already in flight when we\n    // mounted (user switched away mid-generation and came back).\n    // Snapshot via dispatch; live updates arrive on the pubsub\n    // subscription below.\n    const pending = await api.call(\"pendingGenerations\", requested);\n    if (isStale()) return;\n    if (pending.ok) {\n      for (const entry of pending.data.pending) {\n        reflectGenerationStart(entry);\n      }\n    }\n  }\n}\n\nonMounted(initializeScript);\nwatch(() => props.selectedResult, initializeScript);\n\n// Keep the view in sync with generations running anywhere — this View's\n// own long-held dispatches, a parallel tab, the agent's background\n// autoGenerateMovie. The host publishes `generation` events on the\n// plugin pubsub channel (started + finished, per beat and per artifact);\n// on start we mirror the local \"rendering\" state so spinners show even\n// after a remount, on finish we reload the relevant asset off disk.\nconst unsubscribeGenerationEvents = api.onGenerationEvent({\n  filePath: () => filePath.value,\n  // The PAIR is the identity: `stories/deck.json` exists in every root, so filtering on the\n  // path alone puts ANOTHER repository's spinners on this card (#3014).\n  root: () => root.value,\n  handler: (event) => {\n    if (!event.done) {\n      reflectGenerationStart(event);\n      return;\n    }\n    // Fire-and-forget: swallow + log so a failed reload doesn't\n    // surface as an unhandled rejection.\n    reflectGenerationFinish(event).catch((err) => {\n      console.error(\"[presentMulmoScript] reload on finish failed:\", err);\n    });\n  },\n});\n\nfunction reflectGenerationStart(entry: MulmoScriptGenerationEvent): void {\n  if (entry.kind === \"beatImage\") {\n    const idx = Number(entry.key);\n    if (!renderedImages[idx]) renderState[idx] = \"rendering\";\n  } else if (entry.kind === \"beatAudio\") {\n    const idx = Number(entry.key);\n    if (!beatAudios[idx]) audioState[idx] = \"generating\";\n  } else if (entry.kind === \"characterImage\") {\n    if (!charImages[entry.key]) charRenderState[entry.key] = \"rendering\";\n  } else if (entry.kind === \"movie\") {\n    movieGenerating.value = true;\n  } else if (entry.kind === \"pdf\") {\n    pdfGenerating.value = true;\n  }\n}\n\nasync function reflectGenerationFinish(entry: MulmoScriptGenerationEvent): Promise<void> {\n  if (entry.kind === \"beatImage\") {\n    const idx = Number(entry.key);\n    await loadExistingBeatImage(idx);\n    if (beatMayHaveMovie(effectiveBeat(idx))) await loadExistingBeatMovie(idx);\n    if (renderState[idx] === \"rendering\") Reflect.deleteProperty(renderState, idx);\n    refreshMissingCharacterImages();\n  } else if (entry.kind === \"beatAudio\") {\n    const idx = Number(entry.key);\n    await loadExistingBeatAudio(idx);\n    if (audioState[idx] === \"generating\") Reflect.deleteProperty(audioState, idx);\n  } else if (entry.kind === \"characterImage\") {\n    await loadExistingCharacterImage(entry.key);\n    if (charRenderState[entry.key] === \"rendering\") {\n      Reflect.deleteProperty(charRenderState, entry.key);\n    }\n  } else if (entry.kind === \"movie\") {\n    movieGenerating.value = false;\n    await refreshMoviePath();\n  } else if (entry.kind === \"pdf\") {\n    pdfGenerating.value = false;\n    await refreshPdfPath();\n  }\n}\n</script>\n\n<style scoped>\n.bottom-bar-wrapper {\n  position: relative;\n  flex-shrink: 0;\n}\n\n.script-source {\n  padding: 0.5rem;\n  background: #f5f5f5;\n  border-top: 1px solid #e0e0e0;\n  font-family: Consolas, \"MS Gothic\", \"BIZ UDGothic\", monospace;\n  font-size: 0.85rem;\n}\n\n.script-source summary {\n  cursor: pointer;\n  user-select: none;\n  padding: 0.5rem;\n  background: #e8e8e8;\n  border-radius: 4px;\n  font-weight: 500;\n  color: #333;\n}\n\n.script-source[open] summary {\n  margin-bottom: 0.5rem;\n}\n\n.script-source summary:hover {\n  background: #d8d8d8;\n}\n\n.script-editor {\n  width: 100%;\n  height: 40vh;\n  padding: 1rem;\n  background: #ffffff;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n  color: #333;\n  font-family: \"Courier New\", \"MS Gothic\", \"BIZ UDGothic\", monospace;\n  font-size: 0.9rem;\n  resize: vertical;\n  margin-bottom: 0.5rem;\n  line-height: 1.5;\n}\n\n.script-editor:focus {\n  outline: none;\n  border-color: #4caf50;\n  box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);\n}\n\n.script-editor-invalid {\n  border-color: #ef4444;\n}\n\n.script-editor-invalid:focus {\n  border-color: #ef4444;\n  box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.1);\n}\n\n.editor-actions {\n  display: flex;\n  justify-content: space-between;\n}\n\n.apply-btn {\n  padding: 0.5rem 1rem;\n  background: #4caf50;\n  color: white;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 0.9rem;\n  transition: background 0.2s;\n  font-weight: 500;\n}\n\n.apply-btn:hover {\n  background: #45a049;\n}\n\n.apply-btn:disabled {\n  background: #cccccc;\n  color: #666666;\n  cursor: not-allowed;\n  opacity: 0.6;\n}\n\n.cancel-btn {\n  padding: 0.5rem 1rem;\n  background: #e0e0e0;\n  color: #333;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 0.9rem;\n  transition: background 0.2s;\n  font-weight: 500;\n}\n\n.cancel-btn:hover {\n  background: #d0d0d0;\n}\n\n.copy-btn {\n  position: absolute;\n  bottom: 0.3rem;\n  right: 0.65rem;\n  padding: 0.4rem;\n  background: none;\n  border: none;\n  color: #333;\n  cursor: pointer;\n  z-index: 1;\n}\n\n.copy-btn:hover {\n  color: #000;\n}\n\n.copy-btn .material-icons {\n  font-size: 1.15rem;\n}\n</style>\n","<template>\n  <div class=\"p-2 text-sm\" data-testid=\"mulmo-script-preview\">\n    <div class=\"font-medium text-gray-700 truncate mb-1\" data-testid=\"mulmo-script-preview-title\">\n      {{ title }}\n    </div>\n    <div v-if=\"description\" class=\"text-xs text-gray-500 leading-relaxed\" data-testid=\"mulmo-script-preview-description\">\n      {{ description }}\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptData } from \"../core/types\";\n\nconst props = defineProps<{ result: ToolResultComplete<MulmoScriptData> }>();\n\nconst data = computed(() => props.result.data);\nconst script = computed(() => data.value?.script);\nconst title = computed(() => script.value?.title || data.value?.filePath?.split(\"/\").pop() || \"MulmoScript\");\nconst description = computed(() => script.value?.description);\n</script>\n","<template>\n  <div class=\"p-2 text-sm\" data-testid=\"mulmo-script-preview\">\n    <div class=\"font-medium text-gray-700 truncate mb-1\" data-testid=\"mulmo-script-preview-title\">\n      {{ title }}\n    </div>\n    <div v-if=\"description\" class=\"text-xs text-gray-500 leading-relaxed\" data-testid=\"mulmo-script-preview-description\">\n      {{ description }}\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptData } from \"../core/types\";\n\nconst props = defineProps<{ result: ToolResultComplete<MulmoScriptData> }>();\n\nconst data = computed(() => props.result.data);\nconst script = computed(() => data.value?.script);\nconst title = computed(() => script.value?.title || data.value?.filePath?.split(\"/\").pop() || \"MulmoScript\");\nconst description = computed(() => script.value?.description);\n</script>\n","import \"../style.css\";\n// The editor's own utilities. Tailwind only scans THIS package's source, and a host's\n// Tailwind build only scans what its `@source` names — so nothing here generates the\n// classes `BeatListEditor`'s markup uses. Measured before the migration: the deck editor\n// was rendering without `w-96`, `min-h-0`, `overflow-auto` or `border-r`, and no host\n// imported the stylesheet either.\nimport \"@mulmocast/beat-editor/style.css\";\n\nimport type { ToolPlugin } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptData, SaveMulmoScriptArgs } from \"../core/types\";\nimport { pluginCore } from \"../core/plugin\";\nimport View from \"./View.vue\";\nimport Preview from \"./Preview.vue\";\n\nexport const plugin: ToolPlugin<MulmoScriptData, MulmoScriptData, SaveMulmoScriptArgs> = {\n  ...pluginCore,\n  viewComponent: View,\n  previewComponent: Preview,\n};\n\nexport type { MulmoScriptData, MulmoScriptExecuteContext, SaveMulmoScriptArgs } from \"../core/types\";\nexport type { MulmoScriptDispatchArgs, MulmoScriptDispatchResult, MulmoScriptGenerationEvent, DispatchEnvelope, DispatchFailure } from \"../core/contract\";\nexport { GENERATION_EVENT, SCRIPT_CHANGED_EVENT } from \"../core/contract\";\nexport { TOOL_NAME, TOOL_DEFINITION } from \"../core/definition\";\nexport { MULMOSCRIPT_HOST_ADAPTER_KEY, useHostAdapter, type MulmoScriptHostAdapter } from \"./hostAdapter\";\nexport {\n  useMulmoScriptTransport,\n  type MulmoScriptTransport,\n  type TransportResult,\n  type GenerationSubscription,\n  type ScriptChangedSubscription,\n} from \"./transport\";\nexport { View, Preview };\n\nexport default { plugin };\n"],"mappings":"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"}