@mulmoclaude/mulmoscript-plugin 4.1.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vue.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as pluginCore, d as errorMessage, f as TOOL_DEFINITION, p as TOOL_NAME } from "./plugin-CXhOP_xU.js";
1
+ import { a as pluginCore, d as errorMessage, f as TOOL_DEFINITION, p as TOOL_NAME } from "./plugin-CAhxJ5WM.js";
2
2
  import { n as SCRIPT_CHANGED_EVENT, r as shouldReloadForScriptChange, t as GENERATION_EVENT } from "./contract-CIt1ZAtt.js";
3
3
  import { mulmoBeatSchema, mulmoScriptSchema } from "@mulmocast/types";
4
4
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineAsyncComponent, defineComponent, inject, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, reactive, ref, renderList, toDisplayString, unref, vModelText, vShow, watch, withDirectives, withModifiers } from "vue";
@@ -115,22 +115,20 @@ function beatMayHaveMovie(beat) {
115
115
  function isBeatImageReference(beat) {
116
116
  return beat.image?.type === "beat";
117
117
  }
118
- /** Pure check: is every beat in the script a `slide`-typed beat?
119
- * When true, the View mounts `@mulmocast/beat-editor`'s
120
- * `BeatListEditor` instead of the per-beat list UI (#1575).
121
- * Empty / missing `beats[]` returns false there's nothing to edit
122
- * as a deck, fall through to the existing UI which renders an empty
123
- * state. Mixed scripts (any non-`slide` beat) also return false; that
124
- * case is deferred to a future phase. */
125
- function isAllSlideDeck(script) {
118
+ /** Whether the beat editor has anything to edit.
119
+ *
120
+ * Any beat type, not just `slide`: `@mulmocast/beat-editor` renders and edits all eight
121
+ * (`textSlide` / `markdown` / `chart` / `mermaid` / `image` / `movie` / `slide` /
122
+ * `html_tailwind`). The all-slide test this replaces was a limit of the OLD iframe deck
123
+ * editor, which only understood decks it stayed in place through the migration and kept a
124
+ * markdown script read-only for no reason.
125
+ *
126
+ * Empty / missing `beats[]` is false: there is nothing to edit, and the per-beat list already
127
+ * renders an empty state. */
128
+ function hasEditableBeats(script) {
126
129
  if (!isRecord(script)) return false;
127
130
  const { beats } = script;
128
- if (!Array.isArray(beats) || beats.length === 0) return false;
129
- return beats.every((beat) => {
130
- if (!isRecord(beat)) return false;
131
- const { image } = beat;
132
- return isRecord(image) && image.type === "slide";
133
- });
131
+ return Array.isArray(beats) && beats.length > 0;
134
132
  }
135
133
  /** Resolve the beat the View should render at `index`: the user's
136
134
  * in-place edit (`overrides`) wins over the on-disk beat, and an
@@ -200,6 +198,22 @@ function clearReactiveRecords(...records) {
200
198
  Object.keys(record).forEach((key) => Reflect.deleteProperty(record, key));
201
199
  });
202
200
  }
201
+ /**
202
+ * Whether a `focusout` means focus actually LEFT `container`, rather than moving between two
203
+ * fields inside it.
204
+ *
205
+ * The distinction decides whether pending edits are written: treating every focusout as a
206
+ * departure would write on each hop between inputs, and treating none as one would let the user
207
+ * walk away with the last keystroke unsaved.
208
+ *
209
+ * `null` is focus going nowhere the document can name — clicking the page chrome, or the window
210
+ * losing focus. That counts as leaving: the editor is no longer where the typing goes.
211
+ */
212
+ function focusLeftContainer(container, movedTo) {
213
+ if (!container) return false;
214
+ if (!(movedTo instanceof Node)) return true;
215
+ return !container.contains(movedTo);
216
+ }
203
217
  //#endregion
204
218
  //#region src/vue/transport.ts
205
219
  var GENERATION_EVENT_KINDS = /* @__PURE__ */ new Set([
@@ -588,7 +602,7 @@ var DECK_SAVE_DEBOUNCE_MS = 300;
588
602
  */
589
603
  var EDITOR_ORIGIN = `deck-editor-${Math.random().toString(36).slice(2)}`;
590
604
  function useDeckEditor({ api, filePath, effectiveScript, commitScript }) {
591
- const isDeck = computed(() => isAllSlideDeck(effectiveScript.value));
605
+ const canEditBeats = computed(() => hasEditableBeats(effectiveScript.value));
592
606
  const deckScriptInput = computed(() => effectiveScript.value);
593
607
  let deckSaveTimer = null;
594
608
  let pendingDeckScript = null;
@@ -638,7 +652,7 @@ function useDeckEditor({ api, filePath, effectiveScript, commitScript }) {
638
652
  });
639
653
  }
640
654
  return {
641
- isDeck,
655
+ canEditBeats,
642
656
  deckScriptInput,
643
657
  onDeckUpdate,
644
658
  flushPendingDeckSave,
@@ -652,6 +666,8 @@ function useDeckEditor({ api, filePath, effectiveScript, commitScript }) {
652
666
  var useT = createUseT({
653
667
  de: {
654
668
  beatCount: (count) => count === 1 ? `${count} Beat` : `${count} Beats`,
669
+ editTab: "Bearbeiten",
670
+ mediaTab: "Medien",
655
671
  movie: "Video",
656
672
  generating: "Wird generiert…",
657
673
  rendering: "Wird gerendert…",
@@ -684,6 +700,8 @@ var useT = createUseT({
684
700
  },
685
701
  en: {
686
702
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
703
+ editTab: "Edit",
704
+ mediaTab: "Media",
687
705
  movie: "Movie",
688
706
  generating: "Generating…",
689
707
  rendering: "Rendering…",
@@ -716,6 +734,8 @@ var useT = createUseT({
716
734
  },
717
735
  es: {
718
736
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
737
+ editTab: "Editar",
738
+ mediaTab: "Medios",
719
739
  movie: "Vídeo",
720
740
  generating: "Generando…",
721
741
  rendering: "Renderizando…",
@@ -748,6 +768,8 @@ var useT = createUseT({
748
768
  },
749
769
  fr: {
750
770
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
771
+ editTab: "Éditer",
772
+ mediaTab: "Médias",
751
773
  movie: "Film",
752
774
  generating: "Génération…",
753
775
  rendering: "Rendu…",
@@ -780,6 +802,8 @@ var useT = createUseT({
780
802
  },
781
803
  ja: {
782
804
  beatCount: (count) => `${count} ビート`,
805
+ editTab: "編集",
806
+ mediaTab: "メディア",
783
807
  movie: "動画",
784
808
  generating: "生成中…",
785
809
  rendering: "レンダリング中…",
@@ -812,6 +836,8 @@ var useT = createUseT({
812
836
  },
813
837
  ko: {
814
838
  beatCount: (count) => `${count}개 비트`,
839
+ editTab: "편집",
840
+ mediaTab: "미디어",
815
841
  movie: "영상",
816
842
  generating: "생성 중…",
817
843
  rendering: "렌더링 중…",
@@ -844,6 +870,8 @@ var useT = createUseT({
844
870
  },
845
871
  "pt-BR": {
846
872
  beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
873
+ editTab: "Editar",
874
+ mediaTab: "Mídia",
847
875
  movie: "Vídeo",
848
876
  generating: "Gerando…",
849
877
  rendering: "Renderizando…",
@@ -876,6 +904,8 @@ var useT = createUseT({
876
904
  },
877
905
  zh: {
878
906
  beatCount: (count) => `${count} 个 beat`,
907
+ editTab: "编辑",
908
+ mediaTab: "媒体",
879
909
  movie: "视频",
880
910
  generating: "生成中…",
881
911
  rendering: "渲染中…",
@@ -1307,8 +1337,7 @@ var _hoisted_12 = { class: "break-words whitespace-pre-wrap mt-0.5" };
1307
1337
  var _hoisted_13 = ["disabled"];
1308
1338
  var _hoisted_14 = {
1309
1339
  key: 2,
1310
- class: "flex-1 overflow-hidden",
1311
- "data-testid": "mulmo-script-deck-editor"
1340
+ class: "flex shrink-0 gap-1 px-2 pt-1 text-[11px]"
1312
1341
  };
1313
1342
  var _hoisted_15 = { class: "flex gap-3 items-stretch" };
1314
1343
  var _hoisted_16 = [
@@ -1419,6 +1448,7 @@ var _hoisted_53 = ["title"];
1419
1448
  var _hoisted_54 = { class: "material-icons" };
1420
1449
  var SILENT_BEAT_DEFAULT_SEC = 3;
1421
1450
  var MS_PER_SECOND = 1e3;
1451
+ var BEAT_TAB_BASE = "rounded px-2 py-0.5 font-sans";
1422
1452
  var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
1423
1453
  __name: "View",
1424
1454
  props: { selectedResult: {} },
@@ -1585,12 +1615,27 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1585
1615
  }
1586
1616
  });
1587
1617
  }
1588
- const { isDeck, deckScriptInput, onDeckUpdate, flushPendingDeckSave, watchForeignWrites } = useDeckEditor({
1618
+ const { canEditBeats, deckScriptInput, onDeckUpdate, flushPendingDeckSave, watchForeignWrites } = useDeckEditor({
1589
1619
  api,
1590
1620
  filePath,
1591
1621
  effectiveScript,
1592
1622
  commitScript
1593
1623
  });
1624
+ /**
1625
+ * Which pane the beats are shown in.
1626
+ *
1627
+ * `edit` is the beat editor — every beat type, edited in place. `media` is the per-beat list,
1628
+ * which is the only place audio / image / movie generation lives. A script with nothing to edit
1629
+ * has only the list, so the switch is hidden and this is ignored.
1630
+ *
1631
+ * `media` is the default because opening the script is what triggers rendering each beat's
1632
+ * image: the auto-render on mount lives in that list, so defaulting to `edit` silently stopped
1633
+ * thumbnails from being produced at all. Someone who wants to edit clicks once; nobody has to
1634
+ * click to get the previews they always got.
1635
+ */
1636
+ const beatPane = ref("media");
1637
+ const showBeatEditor = computed(() => canEditBeats.value && beatPane.value === "edit");
1638
+ const beatPaneTabClass = (active) => [BEAT_TAB_BASE, active ? "bg-gray-700 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"];
1594
1639
  const unsubscribeForeignWrites = watchForeignWrites(() => {
1595
1640
  refreshScriptFromDisk();
1596
1641
  });
@@ -1598,6 +1643,18 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1598
1643
  function onDeckBeatsUpdate(beats) {
1599
1644
  onDeckUpdate(withBeats(deckScriptInput.value, beats));
1600
1645
  }
1646
+ /**
1647
+ * Leaving the editor writes whatever is still in the debounce.
1648
+ *
1649
+ * Asking the agent to change the script means moving focus out of here first, so this lands
1650
+ * ahead of every request without the host having to announce one — MulmoTerminal's agent is a
1651
+ * terminal, and there is no "sent" event to hook. The debounce is short enough that it has
1652
+ * usually fired already; this closes the case where it has not, which would otherwise have the
1653
+ * agent read a file missing the last thing the user typed.
1654
+ */
1655
+ function onDeckFocusOut(event) {
1656
+ if (focusLeftContainer(event.currentTarget instanceof Node ? event.currentTarget : null, event.relatedTarget)) flushPendingDeckSave();
1657
+ }
1601
1658
  onBeforeUnmount(() => {
1602
1659
  flushPendingDeckSave();
1603
1660
  resetBeatMovies();
@@ -2035,7 +2092,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2035
2092
  "onDownloadPdf"
2036
2093
  ])]),
2037
2094
  unref(movieError) ? (openBlock(), createElementBlock("div", _hoisted_9, [
2038
- _cache[3] || (_cache[3] = createElementVNode("span", { class: "material-icons text-base shrink-0 mt-px" }, "error_outline", -1)),
2095
+ _cache[5] || (_cache[5] = createElementVNode("span", { class: "material-icons text-base shrink-0 mt-px" }, "error_outline", -1)),
2039
2096
  createElementVNode("div", _hoisted_10, [createElementVNode("div", _hoisted_11, toDisplayString(unref(m).movieGenerationFailed), 1), createElementVNode("div", _hoisted_12, toDisplayString(unref(movieError)), 1)]),
2040
2097
  createElementVNode("button", {
2041
2098
  class: "shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50",
@@ -2075,11 +2132,27 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2075
2132
  "onCharDrop",
2076
2133
  "onRenderCharacter"
2077
2134
  ])) : createCommentVNode("", true),
2078
- unref(isDeck) ? (openBlock(), createElementBlock("div", _hoisted_14, [createVNode(unref(BeatListEditor), {
2135
+ unref(canEditBeats) ? (openBlock(), createElementBlock("div", _hoisted_14, [createElementVNode("button", {
2136
+ type: "button",
2137
+ class: normalizeClass(beatPaneTabClass(beatPane.value === "edit")),
2138
+ "data-testid": "mulmo-script-tab-edit",
2139
+ onClick: _cache[1] || (_cache[1] = ($event) => beatPane.value = "edit")
2140
+ }, toDisplayString(unref(m).editTab), 3), createElementVNode("button", {
2141
+ type: "button",
2142
+ class: normalizeClass(beatPaneTabClass(beatPane.value === "media")),
2143
+ "data-testid": "mulmo-script-tab-media",
2144
+ onClick: _cache[2] || (_cache[2] = ($event) => beatPane.value = "media")
2145
+ }, toDisplayString(unref(m).mediaTab), 3)])) : createCommentVNode("", true),
2146
+ showBeatEditor.value ? (openBlock(), createElementBlock("div", {
2147
+ key: 3,
2148
+ class: "flex-1 overflow-hidden",
2149
+ "data-testid": "mulmo-script-deck-editor",
2150
+ onFocusout: onDeckFocusOut
2151
+ }, [createVNode(unref(BeatListEditor), {
2079
2152
  beats: deckBeats.value,
2080
2153
  "onUpdate:beats": onDeckBeatsUpdate
2081
- }, null, 8, ["beats"])])) : (openBlock(), createElementBlock("div", {
2082
- key: 3,
2154
+ }, null, 8, ["beats"])], 32)) : (openBlock(), createElementBlock("div", {
2155
+ key: 4,
2083
2156
  ref_key: "beatListEl",
2084
2157
  ref: beatListEl,
2085
2158
  class: "flex-1 overflow-y-auto p-2 space-y-1.5"
@@ -2109,7 +2182,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2109
2182
  "aria-label": unref(m).close,
2110
2183
  "data-testid": `mulmo-script-beat-movie-close-${index}`,
2111
2184
  onClick: withModifiers(($event) => unref(closeBeatMovie)(index), ["stop"])
2112
- }, [..._cache[4] || (_cache[4] = [createElementVNode("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_19)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
2185
+ }, [..._cache[6] || (_cache[6] = [createElementVNode("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_19)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
2113
2186
  renderedImages[index] ? (openBlock(), createElementBlock("img", {
2114
2187
  key: 0,
2115
2188
  src: renderedImages[index],
@@ -2124,7 +2197,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2124
2197
  "aria-label": unref(m).play,
2125
2198
  "data-testid": `mulmo-script-beat-movie-play-${index}`,
2126
2199
  onClick: withModifiers(($event) => unref(playBeatMovie)(index), ["stop"])
2127
- }, [unref(beatMovieLoading)[index] ? (openBlock(), createElementBlock("svg", _hoisted_22, [..._cache[5] || (_cache[5] = [createElementVNode("circle", {
2200
+ }, [unref(beatMovieLoading)[index] ? (openBlock(), createElementBlock("svg", _hoisted_22, [..._cache[7] || (_cache[7] = [createElementVNode("circle", {
2128
2201
  class: "opacity-25",
2129
2202
  cx: "12",
2130
2203
  cy: "12",
@@ -2141,7 +2214,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2141
2214
  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",
2142
2215
  disabled: unref(movieGenerating),
2143
2216
  onClick: withModifiers(($event) => regenerateBeat(index), ["stop"])
2144
- }, " ↺ ", 8, _hoisted_24)) : !renderedImages[index] ? (openBlock(), createElementBlock("div", _hoisted_25, [renderState[index] === "rendering" || unref(movieGenerating) && !renderedImages[index] && effectiveBeat$1(index).imagePrompt ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[6] || (_cache[6] = createElementVNode("svg", {
2217
+ }, " ↺ ", 8, _hoisted_24)) : !renderedImages[index] ? (openBlock(), createElementBlock("div", _hoisted_25, [renderState[index] === "rendering" || unref(movieGenerating) && !renderedImages[index] && effectiveBeat$1(index).imagePrompt ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[8] || (_cache[8] = createElementVNode("svg", {
2145
2218
  class: "animate-spin w-4 h-4 text-green-400",
2146
2219
  viewBox: "0 0 24 24",
2147
2220
  fill: "none"
@@ -2164,7 +2237,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2164
2237
  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",
2165
2238
  onClick: ($event) => renderBeat(index)
2166
2239
  }, toDisplayString(unref(m).generate), 9, _hoisted_33)) : createCommentVNode("", true)
2167
- ], 42, _hoisted_16), createElementVNode("div", _hoisted_34, [createElementVNode("span", _hoisted_35, toDisplayString(effectiveBeat$1(index).text), 1), createElementVNode("div", _hoisted_36, [createElementVNode("div", _hoisted_37, [audioState[index] === "generating" || unref(movieGenerating) && !beatAudios[index] && effectiveBeat$1(index).text ? (openBlock(), createElementBlock("svg", _hoisted_38, [..._cache[7] || (_cache[7] = [createElementVNode("circle", {
2240
+ ], 42, _hoisted_16), createElementVNode("div", _hoisted_34, [createElementVNode("span", _hoisted_35, toDisplayString(effectiveBeat$1(index).text), 1), createElementVNode("div", _hoisted_36, [createElementVNode("div", _hoisted_37, [audioState[index] === "generating" || unref(movieGenerating) && !beatAudios[index] && effectiveBeat$1(index).text ? (openBlock(), createElementBlock("svg", _hoisted_38, [..._cache[9] || (_cache[9] = [createElementVNode("circle", {
2168
2241
  class: "opacity-25",
2169
2242
  cx: "12",
2170
2243
  cy: "12",
@@ -2196,7 +2269,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2196
2269
  title: sourceOpen[index] ? "Hide source" : "Show source",
2197
2270
  "data-testid": `mulmo-script-beat-source-toggle-${index}`,
2198
2271
  onClick: ($event) => toggleSource(index)
2199
- }, [..._cache[8] || (_cache[8] = [createElementVNode("svg", {
2272
+ }, [..._cache[10] || (_cache[10] = [createElementVNode("svg", {
2200
2273
  xmlns: "http://www.w3.org/2000/svg",
2201
2274
  class: "w-3.5 h-3.5",
2202
2275
  viewBox: "0 0 24 24",
@@ -2222,11 +2295,11 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2222
2295
  ref_key: "sourceDetails",
2223
2296
  ref: sourceDetails,
2224
2297
  class: "script-source",
2225
- onToggle: _cache[2] || (_cache[2] = ($event) => onSourceToggle($event.target.open))
2298
+ onToggle: _cache[4] || (_cache[4] = ($event) => onSourceToggle($event.target.open))
2226
2299
  }, [
2227
2300
  createElementVNode("summary", null, toDisplayString(unref(m).editSource), 1),
2228
2301
  withDirectives(createElementVNode("textarea", {
2229
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => editableSource.value = $event),
2302
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => editableSource.value = $event),
2230
2303
  class: normalizeClass(["script-editor", { "script-editor-invalid": sourceChanged.value && !sourceValid.value }]),
2231
2304
  spellcheck: "false"
2232
2305
  }, null, 2), [[vModelText, editableSource.value]]),
@@ -2244,7 +2317,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
2244
2317
  onClick: copyText
2245
2318
  }, [createElementVNode("span", _hoisted_54, toDisplayString(unref(copied) ? "check" : "content_copy"), 1)], 8, _hoisted_53), [[vShow, !editing.value]])]),
2246
2319
  lightbox.value ? (openBlock(), createBlock(BeatLightbox_default, {
2247
- key: 4,
2320
+ key: 5,
2248
2321
  lightbox: lightbox.value,
2249
2322
  "beat-count": beats.value.length,
2250
2323
  "beat-texts": beatTexts.value,
@@ -2280,7 +2353,7 @@ var _plugin_vue_export_helper_default = (sfc, props) => {
2280
2353
  };
2281
2354
  //#endregion
2282
2355
  //#region src/vue/View.vue
2283
- var View_default = /*#__PURE__*/ _plugin_vue_export_helper_default(View_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-87bf6e4c"]]);
2356
+ var View_default = /*#__PURE__*/ _plugin_vue_export_helper_default(View_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-f9957eaf"]]);
2284
2357
  //#endregion
2285
2358
  //#region src/vue/Preview.vue?vue&type=script&setup=true&lang.ts
2286
2359
  var _hoisted_1 = {