@1agh/maude 0.58.0 → 0.58.2
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/apps/studio/.ai/cache/_stats.json +1 -1
- package/apps/studio/acp/bridge.ts +165 -15
- package/apps/studio/artboard-hug-commit.ts +30 -0
- package/apps/studio/client/app.jsx +107 -33
- package/apps/studio/client/panels/ChatPanel.jsx +25 -2
- package/apps/studio/client/panels/timeline-media-cache.js +38 -2
- package/apps/studio/client/styles/6-acp-chat.css +12 -0
- package/apps/studio/clip-ops.ts +84 -35
- package/apps/studio/dist/client.bundle.js +899 -899
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/sync/canvas-path.ts +329 -0
- package/apps/studio/sync/codec.ts +42 -0
- package/apps/studio/sync/index.ts +490 -17
- package/apps/studio/sync/projection.ts +11 -1
- package/apps/studio/sync/remote-docs.ts +122 -15
- package/apps/studio/test/acp-bridge-lifetime.test.ts +106 -0
- package/apps/studio/test/artboard-hug-commit.test.ts +36 -0
- package/apps/studio/test/clip-ops.test.ts +84 -2
- package/apps/studio/test/comment-relay-origin-gate.test.ts +117 -0
- package/apps/studio/test/comments-fs-rebroadcast.test.ts +155 -0
- package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +37 -0
- package/apps/studio/test/sync-canvas-path.test.ts +200 -0
- package/apps/studio/test/sync-path-pull.test.ts +402 -0
- package/apps/studio/test/sync-remote-docs.test.ts +55 -3
- package/apps/studio/test/sync-runtime.test.ts +118 -0
- package/apps/studio/test/timeline-media-cache.test.ts +41 -0
- package/apps/studio/whats-new.json +9 -0
- package/package.json +8 -8
|
@@ -80,7 +80,10 @@ export function stripBucket(widthPx) {
|
|
|
80
80
|
return 7;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
/** Hidden <video>, loaded and waited on through `loadedmetadata`/`canplay` so
|
|
84
|
+
* `.duration`/`.videoWidth`/`.videoHeight` are readable. Caller owns cleanup
|
|
85
|
+
* (`video.remove()`) once done with it. */
|
|
86
|
+
async function loadVideoMetadata(url, timeoutMs) {
|
|
84
87
|
const video = document.createElement('video');
|
|
85
88
|
video.muted = true;
|
|
86
89
|
video.playsInline = true;
|
|
@@ -99,7 +102,7 @@ async function extractStrip(url, count) {
|
|
|
99
102
|
}
|
|
100
103
|
try {
|
|
101
104
|
await new Promise((res, rej) => {
|
|
102
|
-
const to = setTimeout(() => rej(new Error('metadata timeout')),
|
|
105
|
+
const to = setTimeout(() => rej(new Error('metadata timeout')), timeoutMs);
|
|
103
106
|
const ok = () => {
|
|
104
107
|
clearTimeout(to);
|
|
105
108
|
res();
|
|
@@ -114,6 +117,39 @@ async function extractStrip(url, count) {
|
|
|
114
117
|
});
|
|
115
118
|
} catch (err) {
|
|
116
119
|
video.remove();
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
return video;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Real duration (seconds) of a video/audio src, probed via a hidden <video>
|
|
126
|
+
* element — never throws, resolves null on failure so callers (e.g. the
|
|
127
|
+
* timeline drop handler) can fall back to a default duration cleanly. */
|
|
128
|
+
export async function probeMediaDuration(url) {
|
|
129
|
+
let video;
|
|
130
|
+
try {
|
|
131
|
+
video = await loadVideoMetadata(url, 6000);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const dur = video.duration;
|
|
136
|
+
video.remove();
|
|
137
|
+
return Number.isFinite(dur) && dur > 0 ? dur : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** durationInFrames for a clip dropped on the timeline: the real probed
|
|
141
|
+
* duration when available, else a fallback (probe failure, or synthetic
|
|
142
|
+
* content with no inherent duration like a title/image). */
|
|
143
|
+
export function durationFramesForDrop(fps, probedSeconds, fallbackSeconds = 3) {
|
|
144
|
+
const sec = Number.isFinite(probedSeconds) && probedSeconds > 0 ? probedSeconds : fallbackSeconds;
|
|
145
|
+
return Math.max(1, Math.round(fps * sec));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function extractStrip(url, count) {
|
|
149
|
+
let video;
|
|
150
|
+
try {
|
|
151
|
+
video = await loadVideoMetadata(url, 8000);
|
|
152
|
+
} catch (err) {
|
|
117
153
|
console.warn('[timeline] filmstrip extraction failed for', url, err?.message || err);
|
|
118
154
|
throw err;
|
|
119
155
|
}
|
|
@@ -1235,6 +1235,18 @@
|
|
|
1235
1235
|
.chat-qa {
|
|
1236
1236
|
font-family: var(--font-mono);
|
|
1237
1237
|
}
|
|
1238
|
+
/* Issue #74 — the "Implement N comments" action. Its siblings are literal
|
|
1239
|
+
slash commands (hence mono); this one is prose ABOUT the canvas's current
|
|
1240
|
+
state and only appears when there is something to act on, so it reads as
|
|
1241
|
+
the row's contextual action: proportional type + a light accent tint. */
|
|
1242
|
+
.chat-qa--comments {
|
|
1243
|
+
font-family: inherit;
|
|
1244
|
+
border-color: var(--accent-muted);
|
|
1245
|
+
color: var(--accent);
|
|
1246
|
+
}
|
|
1247
|
+
.chat-qa--comments:hover {
|
|
1248
|
+
background: var(--accent-tint);
|
|
1249
|
+
}
|
|
1238
1250
|
|
|
1239
1251
|
/* Composer */
|
|
1240
1252
|
.chat-composer {
|
package/apps/studio/clip-ops.ts
CHANGED
|
@@ -1802,26 +1802,51 @@ export function applyMoveClipToOverlay(
|
|
|
1802
1802
|
});
|
|
1803
1803
|
}
|
|
1804
1804
|
const mediaStart = directMediaTagStart(source, clip);
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1805
|
+
const isPlaceholder = clip.placeholder != null;
|
|
1806
|
+
if (!isPlaceholder && (mediaStart == null || !clip.mediaSrc || clip.durationInFrames == null)) {
|
|
1807
|
+
throw new CanvasEditError(
|
|
1808
|
+
'only a media clip with a direct source, or an AI placeholder, can move between layers',
|
|
1809
|
+
{ canvas: canvasAbsPath, id: stableId }
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
if (isPlaceholder && clip.durationInFrames == null) {
|
|
1813
|
+
throw new CanvasEditError(
|
|
1814
|
+
'only a media clip with a direct source, or an AI placeholder, can move between layers',
|
|
1815
|
+
{ canvas: canvasAbsPath, id: stableId }
|
|
1816
|
+
);
|
|
1810
1817
|
}
|
|
1811
1818
|
const start = clipAbsoluteStart(source, canvasAbsPath, artboardId, stableId);
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1819
|
+
// A placeholder slate has no media element to graft trim/rate onto — it
|
|
1820
|
+
// rides over as-is (prompt + kind), never as media.
|
|
1821
|
+
const ins = isPlaceholder
|
|
1822
|
+
? applyInsertClipAt(canvasAbsPath, source, artboardId, {
|
|
1823
|
+
lane: 'overlay',
|
|
1824
|
+
from: start,
|
|
1825
|
+
durationInFrames: clip.durationInFrames as number,
|
|
1826
|
+
placeholder: {
|
|
1827
|
+
prompt: clip.placeholder?.prompt ?? '',
|
|
1828
|
+
kind: clip.placeholder?.kind ?? 'veo',
|
|
1829
|
+
},
|
|
1830
|
+
})
|
|
1831
|
+
: applyInsertClipAt(canvasAbsPath, source, artboardId, {
|
|
1832
|
+
lane: 'overlay',
|
|
1833
|
+
from: start,
|
|
1834
|
+
durationInFrames: clip.durationInFrames as number,
|
|
1835
|
+
mediaTag: clip.mediaTag === 'Audio' ? 'Audio' : 'Video',
|
|
1836
|
+
src: clip.mediaSrc,
|
|
1837
|
+
});
|
|
1838
|
+
let out = isPlaceholder
|
|
1839
|
+
? ins.source
|
|
1840
|
+
: graftMediaAttrs(
|
|
1841
|
+
canvasAbsPath,
|
|
1842
|
+
ins.source,
|
|
1843
|
+
artboardId,
|
|
1844
|
+
ins.stableId,
|
|
1845
|
+
numAttrText(source, mediaStart as number, 'trimBefore') ??
|
|
1846
|
+
numAttrText(source, mediaStart as number, 'startFrom') ??
|
|
1847
|
+
0,
|
|
1848
|
+
numAttrText(source, mediaStart as number, 'playbackRate') ?? 1
|
|
1849
|
+
);
|
|
1825
1850
|
const again = enumerateClips(canvasAbsPath, out, artboardId);
|
|
1826
1851
|
const insertedHash = again.clips.find((c) => c.stableId === ins.stableId)?.contentHash ?? null;
|
|
1827
1852
|
const original = again.clips.find(
|
|
@@ -1861,24 +1886,48 @@ export function applyMoveClipToStoryline(
|
|
|
1861
1886
|
throw new CanvasEditError('already a storyline beat', { canvas: canvasAbsPath, id: stableId });
|
|
1862
1887
|
}
|
|
1863
1888
|
const mediaStart = directMediaTagStart(source, clip);
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1889
|
+
const isPlaceholder = clip.placeholder != null;
|
|
1890
|
+
if (!isPlaceholder && (mediaStart == null || !clip.mediaSrc || clip.durationInFrames == null)) {
|
|
1891
|
+
throw new CanvasEditError(
|
|
1892
|
+
'only a media clip with a direct source, or an AI placeholder, can move between layers',
|
|
1893
|
+
{ canvas: canvasAbsPath, id: stableId }
|
|
1894
|
+
);
|
|
1869
1895
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1896
|
+
if (isPlaceholder && clip.durationInFrames == null) {
|
|
1897
|
+
throw new CanvasEditError(
|
|
1898
|
+
'only a media clip with a direct source, or an AI placeholder, can move between layers',
|
|
1899
|
+
{ canvas: canvasAbsPath, id: stableId }
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
// A placeholder slate has no media element to graft trim/rate onto — it
|
|
1903
|
+
// rides over as-is (prompt + kind), never as media.
|
|
1904
|
+
const ins = isPlaceholder
|
|
1905
|
+
? applyInsertClipAt(canvasAbsPath, source, artboardId, {
|
|
1906
|
+
lane: 'storyline',
|
|
1907
|
+
durationInFrames: clip.durationInFrames as number,
|
|
1908
|
+
placeholder: {
|
|
1909
|
+
prompt: clip.placeholder?.prompt ?? '',
|
|
1910
|
+
kind: clip.placeholder?.kind ?? 'veo',
|
|
1911
|
+
},
|
|
1912
|
+
})
|
|
1913
|
+
: applyInsertClipAt(canvasAbsPath, source, artboardId, {
|
|
1914
|
+
lane: 'storyline',
|
|
1915
|
+
durationInFrames: clip.durationInFrames as number,
|
|
1916
|
+
mediaTag: clip.mediaTag === 'Audio' ? 'Audio' : 'Video',
|
|
1917
|
+
src: clip.mediaSrc,
|
|
1918
|
+
});
|
|
1919
|
+
let out = isPlaceholder
|
|
1920
|
+
? ins.source
|
|
1921
|
+
: graftMediaAttrs(
|
|
1922
|
+
canvasAbsPath,
|
|
1923
|
+
ins.source,
|
|
1924
|
+
artboardId,
|
|
1925
|
+
ins.stableId,
|
|
1926
|
+
numAttrText(source, mediaStart as number, 'trimBefore') ??
|
|
1927
|
+
numAttrText(source, mediaStart as number, 'startFrom') ??
|
|
1928
|
+
0,
|
|
1929
|
+
numAttrText(source, mediaStart as number, 'playbackRate') ?? 1
|
|
1930
|
+
);
|
|
1882
1931
|
const again = enumerateClips(canvasAbsPath, out, artboardId);
|
|
1883
1932
|
const insertedHash = again.clips.find((c) => c.stableId === ins.stableId)?.contentHash ?? null;
|
|
1884
1933
|
const original = again.clips.find(
|