@1agh/maude 0.45.2 → 0.47.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/apps/studio/acp/bootstrap-brief.ts +8 -0
- package/apps/studio/acp/bridge.ts +151 -1
- package/apps/studio/annotations-layer.tsx +42 -0
- package/apps/studio/annotations-snap.ts +50 -10
- package/apps/studio/api.ts +760 -15
- package/apps/studio/artboard-guides-overlay.tsx +270 -0
- package/apps/studio/bin/_agent-browser-safe-config.json +1 -0
- package/apps/studio/bin/_agent-browser-safe.mjs +228 -0
- package/apps/studio/bin/_agent-browser-safe.test.mjs +165 -0
- package/apps/studio/bin/_curl-local.mjs +349 -0
- package/apps/studio/bin/_curl-local.test.mjs +280 -0
- package/apps/studio/bin/_pdf-playwright.mjs +35 -4
- package/apps/studio/bin/_png-playwright.mjs +38 -4
- package/apps/studio/bin/_pw-launch.mjs +52 -0
- package/apps/studio/bin/_pw-launch.test.mjs +90 -0
- package/apps/studio/bin/_smart-frames.mjs +419 -0
- package/apps/studio/bin/_smart-frames.test.mjs +140 -0
- package/apps/studio/bin/agent-browser-safe.sh +29 -0
- package/apps/studio/bin/curl-local.sh +28 -0
- package/apps/studio/bin/smart-frames.sh +30 -0
- package/apps/studio/canvas-cursors.ts +6 -0
- package/apps/studio/canvas-edit.ts +972 -6
- package/apps/studio/canvas-icons.tsx +13 -0
- package/apps/studio/canvas-lib.tsx +206 -11
- package/apps/studio/canvas-shell.tsx +809 -50
- package/apps/studio/client/app.jsx +1554 -206
- package/apps/studio/client/panels/ChatPanel.jsx +92 -3
- package/apps/studio/client/panels/SettingsPanel.jsx +211 -0
- package/apps/studio/client/panels/acp-capabilities.js +11 -0
- package/apps/studio/client/styles/3-shell-maude.css +39 -0
- package/apps/studio/client/styles/6-acp-chat.css +41 -0
- package/apps/studio/contextual-toolbar.tsx +5 -3
- package/apps/studio/dist/client.bundle.js +1627 -1627
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +20 -0
- package/apps/studio/export-dialog.tsx +138 -18
- package/apps/studio/exporters/pdf.ts +332 -18
- package/apps/studio/exporters/png.ts +45 -3
- package/apps/studio/footage/schema.ts +17 -1
- package/apps/studio/generation/gemma-models.ts +224 -0
- package/apps/studio/generation/prefs.ts +43 -0
- package/apps/studio/grid-track-handles.ts +179 -0
- package/apps/studio/handoff.ts +35 -0
- package/apps/studio/http.ts +331 -7
- package/apps/studio/input-router.tsx +73 -17
- package/apps/studio/print/marks.ts +113 -0
- package/apps/studio/print/units.ts +269 -0
- package/apps/studio/print-overlay-content.tsx +132 -0
- package/apps/studio/test/acp-mode-banner.test.ts +45 -0
- package/apps/studio/test/acp-session-allowed-tools.test.ts +187 -0
- package/apps/studio/test/annotations-snap.test.ts +56 -0
- package/apps/studio/test/artboard-guides-overlay.test.tsx +152 -0
- package/apps/studio/test/artboard-kinds.test.tsx +83 -0
- package/apps/studio/test/artboard-selection-attrs.test.ts +69 -0
- package/apps/studio/test/browse-posture.test.tsx +107 -0
- package/apps/studio/test/canvas-hide-chrome.test.ts +58 -0
- package/apps/studio/test/canvas-meta-api.test.ts +237 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +4 -0
- package/apps/studio/test/comment-mount.test.ts +2 -1
- package/apps/studio/test/component-map.test.ts +48 -0
- package/apps/studio/test/convert-to-absolute.test.ts +333 -0
- package/apps/studio/test/detach-component.test.ts +94 -0
- package/apps/studio/test/edit-scope-api.test.ts +8 -4
- package/apps/studio/test/element-structural-api.test.ts +74 -0
- package/apps/studio/test/element-structural-edit.test.ts +363 -0
- package/apps/studio/test/exporters/png.test.ts +49 -1
- package/apps/studio/test/grid-track-handles.test.ts +160 -0
- package/apps/studio/test/handoff.test.ts +48 -0
- package/apps/studio/test/input-router.test.ts +82 -8
- package/apps/studio/test/layers-synthetic-groups.test.ts +96 -0
- package/apps/studio/test/pdf-print-boxes.test.ts +326 -0
- package/apps/studio/test/print-marks.test.ts +113 -0
- package/apps/studio/test/print-units.test.ts +173 -0
- package/apps/studio/test/use-snap-guides.test.ts +81 -0
- package/apps/studio/test/use-tool-mode.test.tsx +10 -2
- package/apps/studio/tool-palette.tsx +3 -1
- package/apps/studio/use-canvas-media-drop.tsx +126 -0
- package/apps/studio/use-chrome-visibility.tsx +19 -0
- package/apps/studio/use-element-resize.tsx +24 -3
- package/apps/studio/use-grid-track-handles.tsx +364 -0
- package/apps/studio/use-keyboard-discipline.tsx +15 -0
- package/apps/studio/use-snap-guides.tsx +73 -5
- package/apps/studio/use-spacing-handles.tsx +9 -5
- package/apps/studio/use-tool-mode.tsx +30 -3
- package/apps/studio/web-overlay-content.tsx +52 -0
- package/apps/studio/whats-new.json +55 -0
- package/cli/commands/design.mjs +25 -1
- package/package.json +8 -8
- package/plugins/design/dependencies.json +35 -0
- package/plugins/design/templates/_shell.html +4 -0
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
useThread,
|
|
26
26
|
} from '@assistant-ui/react';
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
flattenSelectOptions,
|
|
30
|
+
modeBlocksEdits,
|
|
31
|
+
parseConfigOptions,
|
|
32
|
+
resolvePersistedPick,
|
|
33
|
+
} from './acp-capabilities.js';
|
|
29
34
|
import { parseUsage } from './acp-usage.js';
|
|
30
35
|
import {
|
|
31
36
|
activityLabel,
|
|
@@ -147,6 +152,17 @@ const SUGGESTIONS = [
|
|
|
147
152
|
'/design:new Pricing "a 3-tier pricing page"',
|
|
148
153
|
];
|
|
149
154
|
|
|
155
|
+
// DDR-185 — shared "did a genuinely new item arrive" check for the
|
|
156
|
+
// permission/elicitation effects below: both need the identical shape (diff
|
|
157
|
+
// the incoming list's ids against the previous snapshot, fire onNew only for
|
|
158
|
+
// a real arrival, never for a resolution/removal), just against a different
|
|
159
|
+
// ref and a different onNew callback.
|
|
160
|
+
function fireOnNewArrival(prevIdsRef, list, onNew) {
|
|
161
|
+
const hasNew = list.some((item) => !prevIdsRef.current.has(item.id));
|
|
162
|
+
prevIdsRef.current = new Set(list.map((item) => item.id));
|
|
163
|
+
if (hasNew) onNew?.();
|
|
164
|
+
}
|
|
165
|
+
|
|
150
166
|
function safeStorageGet(key, fallback) {
|
|
151
167
|
try {
|
|
152
168
|
return localStorage.getItem(key) ?? fallback;
|
|
@@ -208,6 +224,42 @@ function modeFootnote(modes) {
|
|
|
208
224
|
return NO_PROMPT_MODE_IDS.has(modes.currentModeId) ? `${name} — no prompts` : `${name} — you'll be asked`;
|
|
209
225
|
}
|
|
210
226
|
|
|
227
|
+
// The loud, top-of-thread alert shown ONLY while the current mode blocks edits
|
|
228
|
+
// (`modeBlocksEdits`, DDR-184 #3 — the quiet composer footnote above isn't enough
|
|
229
|
+
// when a user typing "change X" silently gets nothing back).
|
|
230
|
+
// Offers a one-click hop to the least-privilege mode that CAN edit (Accept Edits
|
|
231
|
+
// if advertised, else Manual) so the fix is right there, not buried in the picker.
|
|
232
|
+
function ModeBanner({ modes, onSetMode }) {
|
|
233
|
+
if (!modeBlocksEdits(modes)) return null;
|
|
234
|
+
const current = modes.availableModes.find((m) => m.id === modes.currentModeId);
|
|
235
|
+
const name = current?.name || modes.currentModeId;
|
|
236
|
+
const isPlan = modes.currentModeId === 'plan';
|
|
237
|
+
const ids = new Set(modes.availableModes.map((m) => m.id));
|
|
238
|
+
const target = ids.has('acceptEdits') ? 'acceptEdits' : ids.has('default') ? 'default' : null;
|
|
239
|
+
const targetName = target && (modes.availableModes.find((m) => m.id === target)?.name || target);
|
|
240
|
+
return (
|
|
241
|
+
<div className="chat-mode-banner" role="alert" data-testid="chat-mode-banner">
|
|
242
|
+
<span className="chat-mode-banner-icon" aria-hidden="true">⏸</span>
|
|
243
|
+
<span className="chat-mode-banner-text">
|
|
244
|
+
<b>{name}</b>{' — '}
|
|
245
|
+
{isPlan
|
|
246
|
+
? "Claude is planning and won't edit files or run tools."
|
|
247
|
+
: 'edits and tools without a standing approval are denied — nothing will change.'}
|
|
248
|
+
</span>
|
|
249
|
+
{target ? (
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
className="chat-mode-banner-cta"
|
|
253
|
+
onClick={() => onSetMode(target)}
|
|
254
|
+
data-testid="chat-mode-banner-switch"
|
|
255
|
+
>
|
|
256
|
+
Switch to {targetName}
|
|
257
|
+
</button>
|
|
258
|
+
) : null}
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
211
263
|
// ── pasted-path / URL → inline chip (Claude-Code-style attachment badge) ──
|
|
212
264
|
// When the whole clipboard is a single path or URL, we collapse it to a short
|
|
213
265
|
// literal token ([image-1] / [file-1] / [link-1]) in the textarea and stash the
|
|
@@ -1445,6 +1497,8 @@ function ChatThread({
|
|
|
1445
1497
|
designRel,
|
|
1446
1498
|
transcriptView,
|
|
1447
1499
|
onSetTranscriptView,
|
|
1500
|
+
onPermissionRequest,
|
|
1501
|
+
onElicitationRequest,
|
|
1448
1502
|
}) {
|
|
1449
1503
|
// Live session capabilities (feature-acp-panel-dynamic-claude-code-
|
|
1450
1504
|
// capabilities) — `caps` is THIS chat's own connection's mode roster +
|
|
@@ -1578,9 +1632,29 @@ function ChatThread({
|
|
|
1578
1632
|
// Post-turn-end continuation (the tail the client used to drop — RCA F2).
|
|
1579
1633
|
const [bgParts, setBgParts] = useState([]);
|
|
1580
1634
|
useEffect(() => conn.onBackground(setBgParts), [conn]);
|
|
1635
|
+
// DDR-185 — surface a "Maude needs your input" attention ping to the parent
|
|
1636
|
+
// (app.jsx) whenever a GENUINELY NEW permission/elicitation request arrives,
|
|
1637
|
+
// not on every list churn (a request resolving/timing out also updates the
|
|
1638
|
+
// list — that must NOT re-fire the notification). Read via a ref, not the
|
|
1639
|
+
// effect's own dependency array, so a parent re-render that produces a new
|
|
1640
|
+
// callback identity doesn't force conn.onPermission/onElicitation to
|
|
1641
|
+
// re-subscribe.
|
|
1642
|
+
const attentionCbRef = useRef({ onPermissionRequest, onElicitationRequest });
|
|
1643
|
+
useEffect(() => {
|
|
1644
|
+
attentionCbRef.current = { onPermissionRequest, onElicitationRequest };
|
|
1645
|
+
}, [onPermissionRequest, onElicitationRequest]);
|
|
1646
|
+
|
|
1581
1647
|
// Pending permission requests (Milestone B) — one card at a time, oldest first.
|
|
1582
1648
|
const [pendingPermissions, setPendingPermissions] = useState([]);
|
|
1583
|
-
|
|
1649
|
+
const prevPermissionIdsRef = useRef(new Set());
|
|
1650
|
+
useEffect(
|
|
1651
|
+
() =>
|
|
1652
|
+
conn.onPermission((list) => {
|
|
1653
|
+
fireOnNewArrival(prevPermissionIdsRef, list, attentionCbRef.current.onPermissionRequest);
|
|
1654
|
+
setPendingPermissions(list);
|
|
1655
|
+
}),
|
|
1656
|
+
[conn]
|
|
1657
|
+
);
|
|
1584
1658
|
const activePermission = pendingPermissions[0] || null;
|
|
1585
1659
|
const respondPermission = useCallback(
|
|
1586
1660
|
(decision) => activePermission && conn.respondPermission(activePermission.id, decision),
|
|
@@ -1589,7 +1663,15 @@ function ChatThread({
|
|
|
1589
1663
|
// Pending elicitation requests (feature-acp-ask-user-question) — same
|
|
1590
1664
|
// one-at-a-time, oldest-first treatment as pendingPermissions above.
|
|
1591
1665
|
const [pendingElicitations, setPendingElicitations] = useState([]);
|
|
1592
|
-
|
|
1666
|
+
const prevElicitationIdsRef = useRef(new Set());
|
|
1667
|
+
useEffect(
|
|
1668
|
+
() =>
|
|
1669
|
+
conn.onElicitation((list) => {
|
|
1670
|
+
fireOnNewArrival(prevElicitationIdsRef, list, attentionCbRef.current.onElicitationRequest);
|
|
1671
|
+
setPendingElicitations(list);
|
|
1672
|
+
}),
|
|
1673
|
+
[conn]
|
|
1674
|
+
);
|
|
1593
1675
|
const activeElicitation = pendingElicitations[0] || null;
|
|
1594
1676
|
const respondElicitation = useCallback(
|
|
1595
1677
|
(response) => activeElicitation && conn.respondElicitation(activeElicitation.id, response),
|
|
@@ -1627,6 +1709,9 @@ function ChatThread({
|
|
|
1627
1709
|
transcriptView={transcriptView}
|
|
1628
1710
|
onSetTranscriptView={onSetTranscriptView}
|
|
1629
1711
|
/>
|
|
1712
|
+
{/* DDR-184 #3 — loud alert when the current mode can't edit (plan / dontAsk),
|
|
1713
|
+
so a user typing "change X" isn't silently ignored. Above the feed. */}
|
|
1714
|
+
<ModeBanner modes={caps.modes} onSetMode={onSetMode} />
|
|
1630
1715
|
<ThreadPrimitive.Root className="chat-thread">
|
|
1631
1716
|
<ThreadPrimitive.Viewport className="chat-feed" autoScroll>
|
|
1632
1717
|
<ThreadPrimitive.Empty>
|
|
@@ -1687,6 +1772,8 @@ export default function ChatPanel({
|
|
|
1687
1772
|
hidden = false,
|
|
1688
1773
|
onBusyChange,
|
|
1689
1774
|
onFinished,
|
|
1775
|
+
onPermissionRequest,
|
|
1776
|
+
onElicitationRequest,
|
|
1690
1777
|
}) {
|
|
1691
1778
|
// Model/effort/mode picks — persisted across sessions per config id (never a
|
|
1692
1779
|
// fixed model/effort pair; see loadPersistedPicks). Read live by each open
|
|
@@ -2185,6 +2272,8 @@ export default function ChatPanel({
|
|
|
2185
2272
|
designRel={designRel}
|
|
2186
2273
|
transcriptView={transcriptView}
|
|
2187
2274
|
onSetTranscriptView={setTranscriptView}
|
|
2275
|
+
onPermissionRequest={onPermissionRequest}
|
|
2276
|
+
onElicitationRequest={onElicitationRequest}
|
|
2188
2277
|
/>
|
|
2189
2278
|
);
|
|
2190
2279
|
})
|
|
@@ -385,6 +385,203 @@ function WhisperModelCard() {
|
|
|
385
385
|
);
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
// feature-scene-aware-keyframes — the video-analysis frame-selection engine, the
|
|
389
|
+
// sibling of the Subtitles section. Persists `keyframeEngine` to /_api/generate/prefs.
|
|
390
|
+
const KEYFRAME_ENGINES = [
|
|
391
|
+
{ id: 'auto', label: 'Auto', note: 'Use the best available: Gemma scout → ffmpeg → blind.' },
|
|
392
|
+
{ id: 'gemma', label: 'Gemma scout', note: 'Semantic action beats. Needs Apple Silicon + mlx-vlm + a model.' },
|
|
393
|
+
{ id: 'ffmpeg', label: 'ffmpeg scene-detect', note: 'Scene cuts + endpoints. Needs ffmpeg. No model download.' },
|
|
394
|
+
{ id: 'blind', label: 'Blind (even-spaced)', note: 'Chromium fallback — works with neither ffmpeg nor Gemma.' },
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
function KeyframeEngineCard() {
|
|
398
|
+
const [engine, setEngine] = useState(null);
|
|
399
|
+
const [busy, setBusy] = useState(false);
|
|
400
|
+
const [status, setStatus] = useState(null);
|
|
401
|
+
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
fetch('/_api/generate/prefs')
|
|
404
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
405
|
+
.then((d) => setEngine(d?.keyframeEngine || 'auto'))
|
|
406
|
+
.catch(() => setEngine('auto'));
|
|
407
|
+
}, []);
|
|
408
|
+
|
|
409
|
+
async function choose(id) {
|
|
410
|
+
if (id === engine) return;
|
|
411
|
+
const prev = engine;
|
|
412
|
+
setEngine(id);
|
|
413
|
+
setBusy(true);
|
|
414
|
+
setStatus(null);
|
|
415
|
+
try {
|
|
416
|
+
const res = await fetch('/_api/generate/prefs', {
|
|
417
|
+
method: 'POST',
|
|
418
|
+
headers: { 'content-type': 'text/plain' },
|
|
419
|
+
body: JSON.stringify({ keyframeEngine: id }),
|
|
420
|
+
});
|
|
421
|
+
if (!res.ok) throw new Error(await res.text());
|
|
422
|
+
setStatus({ ok: true, msg: 'Saved.' });
|
|
423
|
+
} catch (err) {
|
|
424
|
+
setEngine(prev);
|
|
425
|
+
setStatus({ ok: false, msg: err && err.message ? err.message : 'save failed' });
|
|
426
|
+
} finally {
|
|
427
|
+
setBusy(false);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<div className="st-provider-card">
|
|
433
|
+
<div className="st-provider-hd">
|
|
434
|
+
<span className="st-provider-name">Scene-aware keyframes — analysis engine</span>
|
|
435
|
+
</div>
|
|
436
|
+
<div className="st-provider-notes">
|
|
437
|
+
How Maude picks frames when it analyzes a video (`/design:video-analyze`, `/design:reel`).
|
|
438
|
+
Scene-aware beats a blind frame rate. This is an explicit choice — a chosen-but-unavailable
|
|
439
|
+
engine tells you how to fix it; <strong>Auto</strong> just uses the best you have installed.
|
|
440
|
+
</div>
|
|
441
|
+
<div className="st-engine-radios" role="radiogroup" aria-label="Keyframe engine">
|
|
442
|
+
{KEYFRAME_ENGINES.map((e) => (
|
|
443
|
+
<label key={e.id} className={'st-engine-radio' + (engine === e.id ? ' is-selected' : '')}>
|
|
444
|
+
<input
|
|
445
|
+
type="radio"
|
|
446
|
+
name="keyframe-engine"
|
|
447
|
+
value={e.id}
|
|
448
|
+
checked={engine === e.id}
|
|
449
|
+
disabled={busy || engine === null}
|
|
450
|
+
onChange={() => choose(e.id)}
|
|
451
|
+
/>
|
|
452
|
+
<span className="st-engine-radio-body">
|
|
453
|
+
<span className="st-engine-radio-label">{e.label}</span>
|
|
454
|
+
<span className="st-engine-radio-note">{e.note}</span>
|
|
455
|
+
</span>
|
|
456
|
+
</label>
|
|
457
|
+
))}
|
|
458
|
+
</div>
|
|
459
|
+
{status && (
|
|
460
|
+
<div
|
|
461
|
+
className="st-provider-status"
|
|
462
|
+
style={{ color: status.ok ? 'var(--accent)' : 'var(--danger, #e5484d)' }}
|
|
463
|
+
>
|
|
464
|
+
{status.msg}
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// The MODEL half of the Gemma scout — one click, mirroring WhisperModelCard. The
|
|
472
|
+
// RUNTIME (mlx-vlm) is a manual `pip install` the app can't do, so the download is
|
|
473
|
+
// gated on mlxVlmAvailable and the card says which tier will actually run.
|
|
474
|
+
function GemmaModelCard() {
|
|
475
|
+
const [state, setState] = useState(null); // { models, downloading, mlxVlmAvailable, ffmpegAvailable }
|
|
476
|
+
const [err, setErr] = useState(null);
|
|
477
|
+
|
|
478
|
+
const load = useCallback(() => {
|
|
479
|
+
fetch('/_api/generate/keyframe-model')
|
|
480
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
481
|
+
.then(setState)
|
|
482
|
+
.catch((e) => setErr(e && e.message ? e.message : 'failed to load models'));
|
|
483
|
+
}, []);
|
|
484
|
+
|
|
485
|
+
useEffect(() => {
|
|
486
|
+
load();
|
|
487
|
+
}, [load]);
|
|
488
|
+
|
|
489
|
+
useEffect(() => {
|
|
490
|
+
if (!state?.downloading) return undefined;
|
|
491
|
+
const t = setInterval(load, 1500);
|
|
492
|
+
return () => clearInterval(t);
|
|
493
|
+
}, [state?.downloading, load]);
|
|
494
|
+
|
|
495
|
+
async function download(id) {
|
|
496
|
+
setErr(null);
|
|
497
|
+
try {
|
|
498
|
+
const res = await fetch('/_api/generate/keyframe-model', {
|
|
499
|
+
method: 'POST',
|
|
500
|
+
headers: { 'content-type': 'text/plain' },
|
|
501
|
+
body: JSON.stringify({ id }),
|
|
502
|
+
});
|
|
503
|
+
if (!res.ok) throw new Error(await res.text());
|
|
504
|
+
load();
|
|
505
|
+
} catch (e) {
|
|
506
|
+
setErr(e && e.message ? e.message : 'download failed');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const dl = state?.downloading;
|
|
511
|
+
const mlx = state?.mlxVlmAvailable;
|
|
512
|
+
const ff = state?.ffmpegAvailable;
|
|
513
|
+
const pctOf = (r, t) => (t > 0 ? Math.min(100, Math.round((r / t) * 100)) : 0);
|
|
514
|
+
const activeTier = mlx ? 'Gemma scout' : ff ? 'ffmpeg scene-detect' : 'blind (even-spaced)';
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<div className="st-provider-card">
|
|
518
|
+
<div className="st-provider-hd">
|
|
519
|
+
<span className="st-provider-name">Gemma scout model (optional)</span>
|
|
520
|
+
</div>
|
|
521
|
+
<div className="st-provider-notes">
|
|
522
|
+
The <strong>Gemma</strong> tier adds semantic action-beat detection on top of scene cuts. It
|
|
523
|
+
needs an <strong>Apple-Silicon Mac</strong> and the <code>mlx-vlm</code> runtime
|
|
524
|
+
(<code>pip install mlx-vlm</code> — a manual step Maude can’t do for you). Downloading a model
|
|
525
|
+
below is the one-click half. Models live in your HuggingFace cache, never committed.
|
|
526
|
+
</div>
|
|
527
|
+
{state && (
|
|
528
|
+
<div className="st-provider-notes">
|
|
529
|
+
On this machine the active tier is <strong>{activeTier}</strong>
|
|
530
|
+
{!mlx && ff && ' — install mlx-vlm + download a model to unlock the Gemma scout.'}
|
|
531
|
+
{!mlx && !ff && ' — install ffmpeg for scene-aware frames, or mlx-vlm for the Gemma scout.'}
|
|
532
|
+
</div>
|
|
533
|
+
)}
|
|
534
|
+
{err && (
|
|
535
|
+
<div className="st-provider-status" style={{ color: 'var(--danger, #e5484d)' }}>
|
|
536
|
+
{err}
|
|
537
|
+
</div>
|
|
538
|
+
)}
|
|
539
|
+
{dl?.error && (
|
|
540
|
+
<div className="st-provider-status" style={{ color: 'var(--danger, #e5484d)' }}>
|
|
541
|
+
Download of {dl.id} failed: {dl.error}
|
|
542
|
+
</div>
|
|
543
|
+
)}
|
|
544
|
+
{state === null && !err && <div className="st-settings-intro">Loading…</div>}
|
|
545
|
+
<div className="st-model-list">
|
|
546
|
+
{(state?.models || []).map((m) => {
|
|
547
|
+
const busy = dl && !dl.error && dl.id === m.id;
|
|
548
|
+
return (
|
|
549
|
+
<div key={m.id} className="st-model-row">
|
|
550
|
+
<div className="st-model-info">
|
|
551
|
+
<span className="st-model-label">
|
|
552
|
+
{m.label}
|
|
553
|
+
<span className="st-model-size">~{Math.round(m.sizeMB / 100) / 10} GB</span>
|
|
554
|
+
</span>
|
|
555
|
+
<span className="st-engine-radio-note">{m.note}</span>
|
|
556
|
+
{busy && (
|
|
557
|
+
<span className="st-model-progress">Downloading… {pctOf(dl.received, dl.total)}%</span>
|
|
558
|
+
)}
|
|
559
|
+
</div>
|
|
560
|
+
<div className="st-model-actions">
|
|
561
|
+
{m.downloaded ? (
|
|
562
|
+
<span className="st-provider-configured">
|
|
563
|
+
<Icon name="check" size={12} /> ready
|
|
564
|
+
</span>
|
|
565
|
+
) : (
|
|
566
|
+
<button
|
|
567
|
+
type="button"
|
|
568
|
+
className="st-btn"
|
|
569
|
+
disabled={!mlx || !!(dl && !dl.error)}
|
|
570
|
+
title={mlx ? '' : 'Install mlx-vlm first (pip install mlx-vlm)'}
|
|
571
|
+
onClick={() => download(m.id)}
|
|
572
|
+
>
|
|
573
|
+
{busy ? 'Downloading…' : mlx ? 'Download' : 'Needs mlx-vlm'}
|
|
574
|
+
</button>
|
|
575
|
+
)}
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
);
|
|
579
|
+
})}
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
388
585
|
// feature-unified-settings-modal — one modal for every Maude preference, laid
|
|
389
586
|
// out as a left vertical tab rail + an internally-scrolling pane. Categories:
|
|
390
587
|
const TABS = [
|
|
@@ -393,6 +590,7 @@ const TABS = [
|
|
|
393
590
|
{ id: 'layout', label: 'Layout' },
|
|
394
591
|
{ id: 'ai-generation', label: 'AI generation' },
|
|
395
592
|
{ id: 'subtitles', label: 'Subtitles' },
|
|
593
|
+
{ id: 'video', label: 'Video' },
|
|
396
594
|
];
|
|
397
595
|
const SETTINGS_TAB_STORE = 'mdcc-settings-tab';
|
|
398
596
|
|
|
@@ -820,6 +1018,19 @@ export default function SettingsPanel({
|
|
|
820
1018
|
<TranscriptionEngineCard />
|
|
821
1019
|
<WhisperModelCard />
|
|
822
1020
|
</section>
|
|
1021
|
+
|
|
1022
|
+
{/* Video — scene-aware keyframe engine. Kept mounted so GemmaModelCard's
|
|
1023
|
+
download poll survives a tab switch (like Subtitles above). */}
|
|
1024
|
+
<section
|
|
1025
|
+
role="tabpanel"
|
|
1026
|
+
id="st-spanel-video"
|
|
1027
|
+
aria-labelledby="st-stab-video"
|
|
1028
|
+
hidden={tab !== 'video'}
|
|
1029
|
+
>
|
|
1030
|
+
<div className="st-rp-hd">Video</div>
|
|
1031
|
+
<KeyframeEngineCard />
|
|
1032
|
+
<GemmaModelCard />
|
|
1033
|
+
</section>
|
|
823
1034
|
</div>
|
|
824
1035
|
</div>
|
|
825
1036
|
</div>
|
|
@@ -69,3 +69,14 @@ export function resolvePersistedPick(availableValues, savedValue, defaultValue)
|
|
|
69
69
|
if (savedValue != null && set.has(savedValue)) return savedValue;
|
|
70
70
|
return defaultValue ?? null;
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
// Modes where the agent CANNOT apply an edit and there's no interactive approve
|
|
74
|
+
// path to unblock it (DDR-184 #3). `plan` = "no actual tool execution"; `dontAsk`
|
|
75
|
+
// denies anything without a standing pre-approval (and Maude's default allow-list
|
|
76
|
+
// only covers edits + `maude`, so an off-list step is silently denied). The
|
|
77
|
+
// ChatPanel ModeBanner reads this to decide whether to show the loud alert. Keyed
|
|
78
|
+
// off the mode id, never a label, so it stays truthful as the roster evolves.
|
|
79
|
+
export const NO_EDIT_MODE_IDS = new Set(['plan', 'dontAsk']);
|
|
80
|
+
export function modeBlocksEdits(modes) {
|
|
81
|
+
return !!modes?.currentModeId && NO_EDIT_MODE_IDS.has(modes.currentModeId);
|
|
82
|
+
}
|
|
@@ -404,6 +404,10 @@
|
|
|
404
404
|
.st-rp-hd { font-family: var(--font-mono); font-size: var(--type-xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--fg-2); }
|
|
405
405
|
/* Stage H (INV-3) edit-scope strip — local (this element) vs shared (edits N places). */
|
|
406
406
|
.st-scope { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); font-size: var(--type-xs); border-bottom: 1px solid var(--border-subtle); flex: none; animation: st-pop var(--dur-soft) var(--ease-out); }
|
|
407
|
+
/* feature-4 detach-component — inline action on the Shared badge. */
|
|
408
|
+
.st-scope-detach { margin-left: auto; padding: 1px 8px; border-radius: var(--radius-xs); font-size: var(--type-xs); background: var(--bg-2); color: var(--fg-1); border: 1px solid var(--border-subtle); cursor: pointer; }
|
|
409
|
+
.st-scope-detach:hover { background: var(--bg-3); color: var(--fg-0); }
|
|
410
|
+
.st-scope-detach:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
407
411
|
.st-scope-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
|
|
408
412
|
.st-scope--local { color: var(--fg-2); }
|
|
409
413
|
.st-scope--local .st-scope-dot { background: var(--fg-3); }
|
|
@@ -664,6 +668,30 @@
|
|
|
664
668
|
.st-layer--row:hover .st-layer-eye, .st-layer--row:focus-within .st-layer-eye { opacity: 1; }
|
|
665
669
|
.st-layer-eye:hover { color: var(--fg-0); background: var(--bg-3); }
|
|
666
670
|
.st-layer-eye:focus-visible { outline: none; color: var(--accent); box-shadow: 0 0 0 2px var(--accent-tint); }
|
|
671
|
+
/* feature-4 T7 — synthetic group row (unstamped wrapper): a muted, italic
|
|
672
|
+
structural stand-in. Not selectable, not draggable, no eye — it exists only
|
|
673
|
+
to restore real nesting in the tree. The folder icon reads as "container". */
|
|
674
|
+
.st-layer.is-group { cursor: default; color: var(--fg-3); }
|
|
675
|
+
.st-layer.is-group .st-layer-label { font-style: italic; }
|
|
676
|
+
.st-layer.is-group .st-layer-type { opacity: 0.7; }
|
|
677
|
+
.st-layer.is-group:hover { background: var(--bg-2); }
|
|
678
|
+
/* feature-4 T7a — component-instance rows (Figma purple). ◆ = the component's
|
|
679
|
+
own frame root, ◇ = an inner member. The purple is deliberately Figma-adjacent
|
|
680
|
+
so the vocabulary transfers. */
|
|
681
|
+
.st-layer.is-instance { color: var(--layer-instance, #a78bfa); }
|
|
682
|
+
.st-layer.is-instance .st-layer-label { color: var(--layer-instance, #a78bfa); }
|
|
683
|
+
.st-layer.is-instance .st-layer-type { color: var(--layer-instance, #a78bfa); opacity: 0.75; }
|
|
684
|
+
.st-layer-inst-glyph { flex: none; width: 14px; text-align: center; font-size: 10px; color: var(--layer-instance, #a78bfa); }
|
|
685
|
+
.st-layer.is-sel.is-instance { box-shadow: inset 2px 0 0 var(--layer-instance, #a78bfa); }
|
|
686
|
+
/* feature-4 T7b — locked rows: padlock stays visible when locked; dimmed label. */
|
|
687
|
+
.st-layer-lock { flex: none; width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; background: transparent; border: 0; color: var(--fg-3); cursor: pointer; padding: 0; border-radius: var(--radius-xs); opacity: 0; }
|
|
688
|
+
.st-layer--row:hover .st-layer-lock, .st-layer--row:focus-within .st-layer-lock { opacity: 1; }
|
|
689
|
+
.st-layer-lock:hover { color: var(--fg-0); background: var(--bg-3); }
|
|
690
|
+
.st-layer-lock:focus-visible { outline: none; color: var(--accent); box-shadow: 0 0 0 2px var(--accent-tint); }
|
|
691
|
+
.st-layer.is-locked .st-layer-lock { opacity: 1; color: var(--fg-1); }
|
|
692
|
+
.st-layer.is-locked .st-layer-label { opacity: 0.6; }
|
|
693
|
+
/* feature-4 T7c — inline rename input matches the row's mono type. */
|
|
694
|
+
.st-layer-rename { flex: 1 1 auto; min-width: 40px; font: inherit; color: var(--fg-0); background: var(--bg-1); border: 1px solid var(--accent); border-radius: var(--radius-xs); padding: 0 4px; outline: none; }
|
|
667
695
|
/* a hidden layer stays dim + keeps its eye visible (so it can be re-shown) */
|
|
668
696
|
.st-layer.is-hidden { opacity: 0.45; }
|
|
669
697
|
.st-layer.is-hidden .st-layer-eye { opacity: 1; color: var(--fg-2); }
|
|
@@ -1141,6 +1169,17 @@ body.st-scrubbing, body.st-scrubbing * { cursor: ew-resize !important; user-sele
|
|
|
1141
1169
|
.st-cp-makeflex:hover:not(:disabled) { border-color: var(--accent); color: var(--fg-0); }
|
|
1142
1170
|
.st-cp-makeflex:disabled { opacity: 0.5; cursor: default; }
|
|
1143
1171
|
|
|
1172
|
+
/* feature-3-web-artboards T5 — Grid section's track-list editor. */
|
|
1173
|
+
.st-cp-gridtracks { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-1) 0; }
|
|
1174
|
+
.st-cp-gridtracks + .st-cp-gridtracks { margin-top: var(--space-2); border-top: 1px solid var(--border-subtle); padding-top: var(--space-2); }
|
|
1175
|
+
.st-cp-gridtracks-hd { display: flex; align-items: center; justify-content: space-between; }
|
|
1176
|
+
.st-cp-gridtracks-label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: var(--tracking-wide); color: var(--fg-2); }
|
|
1177
|
+
.st-cp-gridtracks-add { min-height: 22px; padding: 0 var(--space-2); font-size: 10px; }
|
|
1178
|
+
.st-cp-gridtrack-row { display: flex; align-items: center; gap: var(--space-1); }
|
|
1179
|
+
.st-cp-gridtrack-row .st-cp-nsel { flex: 1 1 auto; }
|
|
1180
|
+
.st-cp-gridtrack-remove { flex: 0 0 auto; min-height: 22px; min-width: 22px; padding: 0; justify-content: center; color: var(--fg-2); }
|
|
1181
|
+
.st-cp-gridtrack-remove:hover:not(:disabled) { color: var(--status-error); }
|
|
1182
|
+
|
|
1144
1183
|
/* provenance legend */
|
|
1145
1184
|
.st-cp-legend { display: flex; flex-wrap: wrap; gap: var(--space-3); padding-top: var(--space-1); border-top: 1px solid var(--border-subtle); }
|
|
1146
1185
|
.st-cp-legend span { display: inline-flex; align-items: center; gap: var(--space-1); font-family: var(--font-mono); font-size: 10px; color: var(--fg-2); }
|
|
@@ -1907,4 +1907,45 @@
|
|
|
1907
1907
|
.brand-up-nextstep { margin: var(--space-4) 0 0; font-size: var(--type-sm); color: var(--fg-1); line-height: var(--lh-md); }
|
|
1908
1908
|
.brand-up-nextstep code { font-family: var(--font-mono); background: var(--bg-3); padding: 1px 4px; border-radius: var(--radius-xs); }
|
|
1909
1909
|
.brand-up-actions { display: flex; gap: var(--space-2); margin-top: var(--space-3); }
|
|
1910
|
+
|
|
1911
|
+
/* DDR-184 #3 — loud top-of-thread alert while the current mode can't edit
|
|
1912
|
+
(Plan / Don't-Ask). Warn-toned so it reads as "you're blocked", distinct
|
|
1913
|
+
from the neutral usage banner. One-click hop back to an editing mode. */
|
|
1914
|
+
.chat-mode-banner {
|
|
1915
|
+
display: flex;
|
|
1916
|
+
align-items: center;
|
|
1917
|
+
gap: var(--space-2);
|
|
1918
|
+
margin: 0 var(--space-4) var(--space-2);
|
|
1919
|
+
padding: var(--space-2) var(--space-3);
|
|
1920
|
+
border: 1px solid var(--warn, hsl(38 92% 50%));
|
|
1921
|
+
border-radius: var(--radius-sm);
|
|
1922
|
+
background: var(--bg-2);
|
|
1923
|
+
color: var(--fg-1);
|
|
1924
|
+
font-size: var(--type-xs);
|
|
1925
|
+
line-height: var(--lh-md);
|
|
1926
|
+
}
|
|
1927
|
+
.chat-mode-banner-icon {
|
|
1928
|
+
flex-shrink: 0;
|
|
1929
|
+
color: var(--warn, hsl(38 92% 50%));
|
|
1930
|
+
}
|
|
1931
|
+
.chat-mode-banner-text {
|
|
1932
|
+
flex: 1 1 auto;
|
|
1933
|
+
}
|
|
1934
|
+
.chat-mode-banner-text b {
|
|
1935
|
+
color: var(--fg-0);
|
|
1936
|
+
}
|
|
1937
|
+
.chat-mode-banner-cta {
|
|
1938
|
+
flex-shrink: 0;
|
|
1939
|
+
appearance: none;
|
|
1940
|
+
cursor: pointer;
|
|
1941
|
+
border: 1px solid var(--border-default);
|
|
1942
|
+
border-radius: var(--radius-xs);
|
|
1943
|
+
background: var(--bg-3);
|
|
1944
|
+
color: var(--fg-0);
|
|
1945
|
+
font-size: var(--type-xs);
|
|
1946
|
+
padding: 2px var(--space-2);
|
|
1947
|
+
}
|
|
1948
|
+
.chat-mode-banner-cta:hover {
|
|
1949
|
+
background: var(--bg-4);
|
|
1950
|
+
}
|
|
1910
1951
|
}
|
|
@@ -170,9 +170,11 @@ interface AlignTarget {
|
|
|
170
170
|
/**
|
|
171
171
|
* Resolve every selection to its current world-space box, gated on
|
|
172
172
|
* absolute/fixed positioning — align/distribute/tidy-up write `left`/`top`,
|
|
173
|
-
* which is only meaningful for out-of-flow elements
|
|
174
|
-
*
|
|
175
|
-
*
|
|
173
|
+
* which is only meaningful for out-of-flow elements. This lane still never does
|
|
174
|
+
* an IMPLICIT convert-to-absolute-on-drag (see the Stage-D dogfood notes); the
|
|
175
|
+
* EXPLICIT context-menu "Convert children to absolute position" (feature-4 T8,
|
|
176
|
+
* DDR-188) is the opt-in path to flatten a flow layout first. Returns `null`
|
|
177
|
+
* (all-or-nothing) if any selection
|
|
176
178
|
* doesn't resolve, or isn't out-of-flow, or has a non-numeric `left`/`top`
|
|
177
179
|
* (e.g. positioned via `right`/`bottom` instead — a known v1 limit).
|
|
178
180
|
*/
|