@1agh/maude 0.58.1 → 0.58.3
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/annotations-bindings.ts +83 -4
- package/apps/studio/api.ts +6 -1
- package/apps/studio/bin/_fetch-asset.mjs +169 -5
- package/apps/studio/bin/_import-asset.mjs +72 -0
- package/apps/studio/bin/_import-figma.mjs +1121 -0
- package/apps/studio/bin/_video-playwright.mjs +86 -3
- package/apps/studio/bin/import-figma.sh +38 -0
- package/apps/studio/bin/read-annotations.mjs +11 -1
- package/apps/studio/bun.lock +16 -22
- package/apps/studio/canvas-edit.ts +29 -5
- package/apps/studio/client/app.jsx +129 -23
- package/apps/studio/client/export-center.jsx +42 -4
- package/apps/studio/client/panels/ChatPanel.jsx +25 -2
- package/apps/studio/client/panels/CloudBar.jsx +92 -1
- package/apps/studio/client/panels/FigmaImportPanel.jsx +264 -0
- package/apps/studio/client/panels/GitPanel.jsx +26 -6
- package/apps/studio/client/panels/SettingsPanel.jsx +181 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +26 -2
- package/apps/studio/client/panels/TimelinePanel.jsx +2 -2
- package/apps/studio/client/panels/timeline-parse.js +3 -3
- package/apps/studio/client/styles/3-shell-maude.css +7 -0
- package/apps/studio/client/styles/4-components.css +134 -0
- package/apps/studio/client/styles/6-acp-chat.css +12 -0
- package/apps/studio/clip-ops.ts +93 -17
- package/apps/studio/cloud/endpoints.ts +78 -10
- package/apps/studio/cloud/renew.ts +183 -0
- package/apps/studio/context.ts +2 -1
- package/apps/studio/dist/client.bundle.js +1491 -1491
- package/apps/studio/dist/runtime/@remotion_media.js +56 -136
- package/apps/studio/dist/runtime/@remotion_player.js +18 -18
- package/apps/studio/dist/runtime/@remotion_transitions.js +9 -9
- package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -1
- package/apps/studio/dist/runtime/remotion.js +12 -12
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/_browser-bundles.ts +20 -6
- package/apps/studio/exporters/_runtime.ts +19 -0
- package/apps/studio/exporters/degraded.ts +92 -0
- package/apps/studio/exporters/index.ts +5 -0
- package/apps/studio/exporters/jobs.ts +19 -0
- package/apps/studio/exporters/unsupported-media.ts +170 -0
- package/apps/studio/exporters/video-encode-lib.ts +27 -1
- package/apps/studio/exporters/video-render-lib.ts +6 -0
- package/apps/studio/exporters/video.ts +62 -1
- package/apps/studio/figma/assets.test.ts +372 -0
- package/apps/studio/figma/assets.ts +398 -0
- package/apps/studio/figma/client.test.ts +395 -0
- package/apps/studio/figma/client.ts +513 -0
- package/apps/studio/figma/comments-to-strokes.test.ts +194 -0
- package/apps/studio/figma/comments-to-strokes.ts +173 -0
- package/apps/studio/figma/endpoints.ts +200 -0
- package/apps/studio/figma/sanitize.test.ts +256 -0
- package/apps/studio/figma/sanitize.ts +315 -0
- package/apps/studio/figma/style-map.ts +352 -0
- package/apps/studio/figma/to-artboard.test.ts +808 -0
- package/apps/studio/figma/to-artboard.ts +701 -0
- package/apps/studio/figma/to-render.test.ts +180 -0
- package/apps/studio/figma/to-render.ts +306 -0
- package/apps/studio/figma/to-strokes-roundtrip.test.ts +152 -0
- package/apps/studio/figma/to-strokes.test.ts +705 -0
- package/apps/studio/figma/to-strokes.ts +749 -0
- package/apps/studio/figma/to-tokens.test.ts +321 -0
- package/apps/studio/figma/to-tokens.ts +305 -0
- package/apps/studio/figma/types.ts +539 -0
- package/apps/studio/figma/url.test.ts +167 -0
- package/apps/studio/figma/url.ts +160 -0
- package/apps/studio/http.ts +129 -0
- package/apps/studio/sync/asset-push.ts +124 -0
- package/apps/studio/sync/canvas-path.ts +329 -0
- package/apps/studio/sync/codec.ts +42 -0
- package/apps/studio/sync/connection-state.ts +11 -0
- package/apps/studio/sync/hub-link.ts +63 -7
- package/apps/studio/sync/hubs-config.ts +31 -3
- package/apps/studio/sync/index.ts +755 -32
- package/apps/studio/sync/migrate-flat-fallback.ts +121 -0
- package/apps/studio/sync/presentation.ts +45 -1
- package/apps/studio/sync/projection.ts +11 -1
- package/apps/studio/sync/remote-docs.ts +122 -15
- package/apps/studio/sync/supervisor.ts +5 -1
- package/apps/studio/sync/workspace-signin.ts +7 -3
- package/apps/studio/test/acp-bridge-lifetime.test.ts +106 -0
- package/apps/studio/test/annotations-bindings.test.ts +150 -12
- package/apps/studio/test/canvas-create-api.test.ts +4 -1
- package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
- package/apps/studio/test/capture-determinism-shape.test.ts +135 -0
- package/apps/studio/test/clip-addressing.test.ts +6 -1
- package/apps/studio/test/clip-ops.test.ts +5 -1
- package/apps/studio/test/cloud-endpoints.test.ts +96 -0
- package/apps/studio/test/cloud-renew.test.ts +205 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +11 -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/exporters/degraded-propagation.test.ts +123 -0
- package/apps/studio/test/exporters/unsupported-media.test.ts +123 -0
- package/apps/studio/test/fetch-asset-gate.test.ts +189 -0
- package/apps/studio/test/figma-provenance.test.ts +108 -0
- package/apps/studio/test/figma-routes.test.ts +294 -0
- package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +37 -0
- package/apps/studio/test/git-cloud-posture.test.ts +50 -0
- package/apps/studio/test/hub-link.test.ts +11 -0
- package/apps/studio/test/import-figma.test.ts +479 -0
- package/apps/studio/test/sync-asset-push.test.ts +124 -0
- package/apps/studio/test/sync-canvas-path.test.ts +200 -0
- package/apps/studio/test/sync-connection-state.test.ts +13 -0
- package/apps/studio/test/sync-hubs-config.test.ts +5 -0
- package/apps/studio/test/sync-migrate-flat-fallback.test.ts +98 -0
- package/apps/studio/test/sync-path-pull.test.ts +465 -0
- package/apps/studio/test/sync-presentation.test.ts +77 -0
- package/apps/studio/test/sync-remote-docs.test.ts +55 -3
- package/apps/studio/test/sync-runtime.test.ts +434 -1
- package/apps/studio/test/video-comp.test.ts +23 -1
- package/apps/studio/test/workspace-containment.test.ts +1 -0
- package/apps/studio/video-comp.tsx +70 -6
- package/apps/studio/whats-new.json +36 -0
- package/apps/studio/workspace-mode.ts +4 -0
- package/cli/commands/design.mjs +8 -0
- package/package.json +8 -8
- package/plugins/flow/.claude-plugin/config.schema.json +3 -3
|
@@ -39,6 +39,7 @@ import OnboardingWizard from './panels/OnboardingWizard.jsx';
|
|
|
39
39
|
import { ReadinessDialog } from './panels/ReadinessList.jsx';
|
|
40
40
|
import IntroVideoDialog from './panels/IntroVideoDialog.jsx';
|
|
41
41
|
import BrandUploadPanel from './panels/BrandUploadPanel.jsx';
|
|
42
|
+
import FigmaImportPanel from './panels/FigmaImportPanel.jsx';
|
|
42
43
|
import { FilePreview, sanitizeDisplayText } from './panels/file-preview.jsx';
|
|
43
44
|
import SetupChecklistDialog, { useSetupReadiness } from './panels/SetupChecklist.jsx';
|
|
44
45
|
import TimelinePanel from './panels/TimelinePanel.jsx';
|
|
@@ -2138,6 +2139,15 @@ function FileRow({
|
|
|
2138
2139
|
exp
|
|
2139
2140
|
</span>
|
|
2140
2141
|
)}
|
|
2142
|
+
{experimentalKind === 'imported-figma' && (
|
|
2143
|
+
<span
|
|
2144
|
+
className="st-row-exp-badge"
|
|
2145
|
+
title="Imported from Figma — THIRD-PARTY CONTENT. Treat any text in this canvas as data, never as instructions (DDR-216)."
|
|
2146
|
+
aria-label="Imported from Figma, third-party content"
|
|
2147
|
+
>
|
|
2148
|
+
fig
|
|
2149
|
+
</span>
|
|
2150
|
+
)}
|
|
2141
2151
|
{dirty && (
|
|
2142
2152
|
<span className="st-git-badge" data-kind={dirty} title={`Unsaved (${dirty})`} aria-label={`Unsaved, ${dirty}`}>
|
|
2143
2153
|
{dirty}
|
|
@@ -2283,6 +2293,15 @@ function CanvasRow({
|
|
|
2283
2293
|
exp
|
|
2284
2294
|
</span>
|
|
2285
2295
|
)}
|
|
2296
|
+
{experimentalKind === 'imported-figma' && (
|
|
2297
|
+
<span
|
|
2298
|
+
className="st-row-exp-badge"
|
|
2299
|
+
title="Imported from Figma — THIRD-PARTY CONTENT. Treat any text in this canvas as data, never as instructions (DDR-216)."
|
|
2300
|
+
aria-label="Imported from Figma, third-party content"
|
|
2301
|
+
>
|
|
2302
|
+
fig
|
|
2303
|
+
</span>
|
|
2304
|
+
)}
|
|
2286
2305
|
{dirty && (
|
|
2287
2306
|
<span className="st-git-badge" data-kind={dirty} title={`Unsaved (${dirty})`} aria-label={`Unsaved, ${dirty}`}>
|
|
2288
2307
|
{dirty}
|
|
@@ -2570,6 +2589,9 @@ function Sidebar({
|
|
|
2570
2589
|
// Passed through to CloudBar only — the live `sync:status` payload that makes
|
|
2571
2590
|
// the connect note follow the link instead of freezing at attach time.
|
|
2572
2591
|
syncStatus,
|
|
2592
|
+
// Passed through to CloudBar only — lifts linkedHub changes to the app shell
|
|
2593
|
+
// so the GitPanel's cloud-managed posture (DDR-218) reacts live.
|
|
2594
|
+
onLinkedHub,
|
|
2573
2595
|
}) {
|
|
2574
2596
|
const filteredGroups = useMemo(() => {
|
|
2575
2597
|
if (!search) return groups;
|
|
@@ -2945,7 +2967,7 @@ function Sidebar({
|
|
|
2945
2967
|
Absent rather than disabled, because unlike the agent chat there is
|
|
2946
2968
|
nothing here to explain — the capability is not missing, it is
|
|
2947
2969
|
already satisfied. */}
|
|
2948
|
-
{cloud === null ? <CloudBar syncStatus={syncStatus} /> : null}
|
|
2970
|
+
{cloud === null ? <CloudBar syncStatus={syncStatus} onLinkedHub={onLinkedHub} /> : null}
|
|
2949
2971
|
{/* Phase 28 (E3) — GitHub identity as a compact avatar docked at the BOTTOM:
|
|
2950
2972
|
sign in, connected account + New/Pull/Share, sign out. Self-contained
|
|
2951
2973
|
(owns its device-code + CreateProject dialogs). Renders nothing in browser. */}
|
|
@@ -9456,6 +9478,10 @@ function App() {
|
|
|
9456
9478
|
// Phase 9 Task 8 — hub-down offline mode banner. Driven by the 'sync:status'
|
|
9457
9479
|
// WS message the linked-mode sync runtime emits. null in solo mode.
|
|
9458
9480
|
const [syncStatus, setSyncStatus] = useState(null);
|
|
9481
|
+
// DDR-218 — the folder's cloud link ({url, credentialed} | null), lifted from
|
|
9482
|
+
// CloudBar (status resolve / attach / detach). Gates the GitPanel's
|
|
9483
|
+
// cloud-managed posture; linked-but-uncredentialed keeps the full panel.
|
|
9484
|
+
const [cloudLinkedHub, setCloudLinkedHub] = useState(null);
|
|
9459
9485
|
// Phase 27 (E2) — in-UI git layer. `gitStatus` is the live dirty-state the
|
|
9460
9486
|
// server broadcasts on `git-status`; `changesOpen` toggles the Changes panel;
|
|
9461
9487
|
// `diffTarget` opens the before/after DiffView ({ file, conflict }).
|
|
@@ -9658,6 +9684,7 @@ function App() {
|
|
|
9658
9684
|
const [introOpen, setIntroOpen] = useState(false);
|
|
9659
9685
|
const [quickSetupOpen, setQuickSetupOpen] = useState(false);
|
|
9660
9686
|
const [brandUploadOpen, setBrandUploadOpen] = useState(false);
|
|
9687
|
+
const [figmaImportOpen, setFigmaImportOpen] = useState(false);
|
|
9661
9688
|
// DDR-166 plan, Phase 2 (T7) — the persistent "Setup" affordance in the empty
|
|
9662
9689
|
// canvas state (below) only renders while the project's own setup (design
|
|
9663
9690
|
// system / first canvas / brand assets) is incomplete; native-only concern.
|
|
@@ -11794,13 +11821,23 @@ function App() {
|
|
|
11794
11821
|
|
|
11795
11822
|
// ----- Inbound messages from iframes -----
|
|
11796
11823
|
useEffect(() => {
|
|
11824
|
+
// Does this comment id belong to the canvas the user is actually looking
|
|
11825
|
+
// at? Scopes the patch/delete relays below — see the SECURITY note there.
|
|
11826
|
+
//
|
|
11827
|
+
// The leading `!!activePath` is load-bearing, not defensive noise: with no
|
|
11828
|
+
// active canvas the branches' `activeWin` is `null`, and `e.source` is also
|
|
11829
|
+
// `null` for a message whose source context was discarded before dispatch —
|
|
11830
|
+
// so `e.source === activeWin` can pass as `null === null`. This is the
|
|
11831
|
+
// second conjunct that closes that path. Don't "simplify" it away.
|
|
11832
|
+
const ownsActiveComment = (id) =>
|
|
11833
|
+
!!activePath && (commentsByFile[activePath] || []).some((c) => c && c.id === id);
|
|
11797
11834
|
function onMessage(e) {
|
|
11798
11835
|
// Cross-origin hardening (DDR-054): only accept dgn control messages from
|
|
11799
11836
|
// the canvas-content origin — the split origin when on, else our own origin
|
|
11800
11837
|
// for the same-origin iframe. Drops spoofed messages from any other window.
|
|
11801
|
-
//
|
|
11802
|
-
// "
|
|
11803
|
-
//
|
|
11838
|
+
// Every canvas iframe shares that origin, so the check below proves only
|
|
11839
|
+
// "a canvas said this", NOT "the canvas the user is looking at said it" —
|
|
11840
|
+
// each mutating branch additionally gates on `e.source === activeWin`.
|
|
11804
11841
|
const expectedOrigin = cfg?.canvasOrigin || window.location.origin;
|
|
11805
11842
|
if (e.origin !== expectedOrigin) return;
|
|
11806
11843
|
const m = e.data;
|
|
@@ -12408,13 +12445,26 @@ function App() {
|
|
|
12408
12445
|
// Phase 6 — iframe overlay finished composing. Relay through the
|
|
12409
12446
|
// existing WS `comments-add` channel; server-side persistence +
|
|
12410
12447
|
// broadcast back are identical to the legacy shell-composer flow.
|
|
12448
|
+
//
|
|
12449
|
+
// SECURITY — gated on `activeWin` and PINNED to `activePath`, the same
|
|
12450
|
+
// shape as every other mutating branch in this handler. The origin
|
|
12451
|
+
// check at the top passes for EVERY canvas iframe (they all share
|
|
12452
|
+
// `canvasOrigin`), so without this a canvas sitting in a BACKGROUND
|
|
12453
|
+
// tab could post comments onto any file it cared to name, with no user
|
|
12454
|
+
// gesture. This handler's preamble waived that on the grounds that
|
|
12455
|
+
// comments are an "inert store" — the ACP panel's one-click
|
|
12456
|
+
// "Implement N comments" action retires that premise, because open
|
|
12457
|
+
// comments are now a feed the agent acts on. `p.file` is ignored
|
|
12458
|
+
// rather than validated: the active canvas is the only file the user
|
|
12459
|
+
// can actually see themselves commenting on.
|
|
12411
12460
|
const p = m.payload;
|
|
12412
12461
|
const txt = String(p.text).trim();
|
|
12413
|
-
|
|
12462
|
+
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
12463
|
+
if (e.source === activeWin && txt && activePath && activePath !== SYSTEM_TAB) {
|
|
12414
12464
|
wsSend({
|
|
12415
12465
|
type: 'comments-add',
|
|
12416
12466
|
payload: {
|
|
12417
|
-
file:
|
|
12467
|
+
file: activePath,
|
|
12418
12468
|
selector: p.selector,
|
|
12419
12469
|
index: p.index,
|
|
12420
12470
|
dom_path: p.dom_path,
|
|
@@ -12428,10 +12478,21 @@ function App() {
|
|
|
12428
12478
|
}
|
|
12429
12479
|
} else if (m.dgn === 'comment-patch' && m.id && m.patch && typeof m.patch === 'object') {
|
|
12430
12480
|
// Phase 6 — thread popover routes resolve / reopen through here.
|
|
12431
|
-
|
|
12481
|
+
// SECURITY — same gate as comment-submit, plus an ownership check: a
|
|
12482
|
+
// bare id would otherwise reach ANY comment in the project. Patch and
|
|
12483
|
+
// delete change or remove somebody else's note, which is exactly why
|
|
12484
|
+
// ws.ts refuses both for a viewer session; the iframe lane needs the
|
|
12485
|
+
// matching restriction.
|
|
12486
|
+
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
12487
|
+
if (e.source === activeWin && ownsActiveComment(m.id)) {
|
|
12488
|
+
wsSend({ type: 'comments-patch', id: m.id, patch: m.patch });
|
|
12489
|
+
}
|
|
12432
12490
|
} else if (m.dgn === 'comment-delete' && m.id) {
|
|
12433
|
-
|
|
12434
|
-
|
|
12491
|
+
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
12492
|
+
if (e.source === activeWin && ownsActiveComment(m.id)) {
|
|
12493
|
+
wsSend({ type: 'comments-delete', id: m.id });
|
|
12494
|
+
setFocusedCommentId((prev) => (prev === m.id ? null : prev));
|
|
12495
|
+
}
|
|
12435
12496
|
} else if (m.dgn === 'comment-click' && m.id) {
|
|
12436
12497
|
setFocusedCommentId(m.id);
|
|
12437
12498
|
} else if (m.dgn === 'artboards' && typeof m.count === 'number') {
|
|
@@ -12518,27 +12579,48 @@ function App() {
|
|
|
12518
12579
|
} else if (m.dgn === 'shell-shortcut') {
|
|
12519
12580
|
// Same forwarding lane for the other shell chords (inspect.ts) — so
|
|
12520
12581
|
// ⌘R / ⌘⇧I / ⌘⇧M / ⌘⇧E / ⌘⇧H behave identically wherever focus is.
|
|
12521
|
-
|
|
12522
|
-
|
|
12523
|
-
|
|
12524
|
-
|
|
12525
|
-
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
|
|
12582
|
+
//
|
|
12583
|
+
// SECURITY — every one of these is a chord the user pressed INSIDE the
|
|
12584
|
+
// canvas they are looking at, so it gets the same `activeWin` gate as
|
|
12585
|
+
// its siblings. Ungated, a background canvas could reload the active
|
|
12586
|
+
// file out from under an edit or pop the Export/Handoff dialog on
|
|
12587
|
+
// demand — a modal-timing primitive, and the mirror image of the
|
|
12588
|
+
// present-enter branch that was already hardened against modal HIDING.
|
|
12589
|
+
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
12590
|
+
if (e.source === activeWin) {
|
|
12591
|
+
if (m.id === 'reload') reloadActive();
|
|
12592
|
+
else if (m.id === 'inspector') toggleRightPanel('inspector');
|
|
12593
|
+
else if (m.id === 'assistant' && isNativeApp()) toggleRightPanel('assistant');
|
|
12594
|
+
else if (m.id === 'comments') toggleRightPanel('comments');
|
|
12595
|
+
else if (m.id === 'changes') toggleRightPanel('changes');
|
|
12596
|
+
else if (m.id === 'timeline') toggleTimeline();
|
|
12597
|
+
else if (m.id === 'export') setExportDialog({ mode: 'export' });
|
|
12598
|
+
else if (m.id === 'handoff') setExportDialog({ mode: 'handoff' });
|
|
12599
|
+
}
|
|
12529
12600
|
} else if (m.dgn === 'open-export') {
|
|
12530
12601
|
// Plan C — the in-canvas toolbar / context menu route here so they open
|
|
12531
12602
|
// the SAME shell Export dialog as the menubar (one look, all settings).
|
|
12532
12603
|
// Carry the context-menu's scope hint (e.g. "Export selection").
|
|
12533
|
-
|
|
12534
|
-
|
|
12535
|
-
|
|
12536
|
-
|
|
12604
|
+
//
|
|
12605
|
+
// SECURITY — gated with `shell-shortcut` above rather than separately:
|
|
12606
|
+
// both reach the same `setExportDialog`, so leaving this one open would
|
|
12607
|
+
// hand back the capability the other now refuses.
|
|
12608
|
+
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
12609
|
+
if (e.source === activeWin) {
|
|
12610
|
+
setExportDialog({
|
|
12611
|
+
mode: 'export',
|
|
12612
|
+
scope: m.detail && typeof m.detail.scope === 'string' ? m.detail.scope : undefined,
|
|
12613
|
+
});
|
|
12614
|
+
}
|
|
12537
12615
|
} else if (m.dgn === 'open-timeline-request') {
|
|
12538
12616
|
// Artboard-chrome context menu's "Open Timeline" (video-comp artboards
|
|
12539
12617
|
// only). Scope the Timeline to the right-clicked artboard, then open it.
|
|
12540
|
-
|
|
12541
|
-
|
|
12618
|
+
// SECURITY — same class: a context-menu action from the canvas in view.
|
|
12619
|
+
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
12620
|
+
if (e.source === activeWin) {
|
|
12621
|
+
if (typeof m.artboardId === 'string') setCanvasActiveArtboard(m.artboardId.slice(0, 120));
|
|
12622
|
+
setTimelineOpen(true);
|
|
12623
|
+
}
|
|
12542
12624
|
} else if (m.dgn === 'loaded' && m.file) {
|
|
12543
12625
|
// iframe finished loading — drop the compile skeleton, push current
|
|
12544
12626
|
// comments + carry over focused pin if any
|
|
@@ -14190,6 +14272,12 @@ function App() {
|
|
|
14190
14272
|
(panelSide[id] || PANEL_SIDES_DEFAULTS[id]) === 'left' ? dragSide === 'sb' : dragSide === 'rp';
|
|
14191
14273
|
const activeCanvasFile =
|
|
14192
14274
|
activePath && activePath !== SYSTEM_TAB && /\.(tsx|html)$/i.test(activePath) ? activePath : null;
|
|
14275
|
+
// Issue #74 — drives the chat panel's "Implement N comments" quick action.
|
|
14276
|
+
// Canvas-wide on purpose: the verb operates on every open comment of the
|
|
14277
|
+
// active canvas, so it is deliberately NOT scoped to the current selection.
|
|
14278
|
+
const activeOpenComments = activeCanvasFile
|
|
14279
|
+
? openCount(commentsByFile[activeCanvasFile])
|
|
14280
|
+
: 0;
|
|
14193
14281
|
|
|
14194
14282
|
// Render a panel body by id (width undefined ⇒ fills the .st-dockslot wrapper,
|
|
14195
14283
|
// which owns the resizable width). Assistant is handled separately below as an
|
|
@@ -14236,6 +14324,7 @@ function App() {
|
|
|
14236
14324
|
onGetLatest={gitGetLatest}
|
|
14237
14325
|
canvasKinds={cfg?.canvasKinds}
|
|
14238
14326
|
syncStatus={syncStatus}
|
|
14327
|
+
onLinkedHub={setCloudLinkedHub}
|
|
14239
14328
|
/>
|
|
14240
14329
|
);
|
|
14241
14330
|
if (id === 'changes')
|
|
@@ -14258,6 +14347,14 @@ function App() {
|
|
|
14258
14347
|
// server-side (`projectReadOnly`, the manifest's role matrix), and
|
|
14259
14348
|
// they are unchanged by this flag.
|
|
14260
14349
|
historyOnly={!!cfg.cloud}
|
|
14350
|
+
// DDR-218 (fix 8) — the DESKTOP half of the same withdrawal: a repo
|
|
14351
|
+
// linked+credentialed to Maude Cloud is cloud-managed (the cell
|
|
14352
|
+
// commits every edit as it lands), so the local Changes surface is
|
|
14353
|
+
// the second save mechanism the sync RCA's user was confused by.
|
|
14354
|
+
// Live: CloudBar lifts every link change (resolve/attach/detach)
|
|
14355
|
+
// into `cloudLinkedHub`. Same presentation-not-a-control rule as
|
|
14356
|
+
// `historyOnly` above; Disconnect restores the panel in place.
|
|
14357
|
+
cloudManaged={!cfg.cloud && !!cloudLinkedHub?.credentialed}
|
|
14261
14358
|
resizing={resizingFor('changes')}
|
|
14262
14359
|
onClose={() => setChangesOpen(false)}
|
|
14263
14360
|
onCommit={gitCommit}
|
|
@@ -14512,6 +14609,7 @@ function App() {
|
|
|
14512
14609
|
hidden={leftActive !== 'assistant'}
|
|
14513
14610
|
activeCanvas={activeCanvasFile}
|
|
14514
14611
|
selected={selected}
|
|
14612
|
+
openComments={activeOpenComments}
|
|
14515
14613
|
designRel={(cfg?.designRel || cfg?.designRoot || '.design').replace(/^\/+|\/+$/g, '')}
|
|
14516
14614
|
resizing={resizingFor('assistant')}
|
|
14517
14615
|
onClose={() => setAssistantOpen(false)}
|
|
@@ -14586,6 +14684,7 @@ function App() {
|
|
|
14586
14684
|
hidden={rightActive !== 'assistant'}
|
|
14587
14685
|
activeCanvas={activeCanvasFile}
|
|
14588
14686
|
selected={selected}
|
|
14687
|
+
openComments={activeOpenComments}
|
|
14589
14688
|
designRel={(cfg?.designRel || cfg?.designRoot || '.design').replace(/^\/+|\/+$/g, '')}
|
|
14590
14689
|
resizing={resizingFor('assistant')}
|
|
14591
14690
|
onClose={() => setAssistantOpen(false)}
|
|
@@ -15496,8 +15595,15 @@ function App() {
|
|
|
15496
15595
|
onClose={() => setQuickSetupOpen(false)}
|
|
15497
15596
|
onStartTour={() => startTour(QUICK_SETUP_TOUR)}
|
|
15498
15597
|
onBringBrand={() => setBrandUploadOpen(true)}
|
|
15598
|
+
onImportFigma={() => setFigmaImportOpen(true)}
|
|
15499
15599
|
/>
|
|
15500
15600
|
<BrandUploadPanel open={brandUploadOpen} onClose={() => setBrandUploadOpen(false)} />
|
|
15601
|
+
{figmaImportOpen && (
|
|
15602
|
+
<FigmaImportPanel
|
|
15603
|
+
onClose={() => setFigmaImportOpen(false)}
|
|
15604
|
+
onImported={() => loadTree()}
|
|
15605
|
+
/>
|
|
15606
|
+
)}
|
|
15501
15607
|
{usageNudge && !tourSteps && !collabNudge && (
|
|
15502
15608
|
<div className="mdcc-tour-nudge" role="status" aria-live="polite">
|
|
15503
15609
|
<div className="mdcc-tour-nudge__body">
|
|
@@ -293,6 +293,10 @@ function etaLabel(job) {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
function StatusPill({ job }) {
|
|
296
|
+
// A degraded export is `done` — the file is real — but it is NOT clean, and a
|
|
297
|
+
// plain "Ready" pill is exactly how a muted mp4 shipped unnoticed for four
|
|
298
|
+
// attempts (RCA issue-mp4-audio-export-html5audio-silent-degrade).
|
|
299
|
+
const degraded = job.status === 'done' && !!job.degraded;
|
|
296
300
|
const statusText =
|
|
297
301
|
job.status === 'queued'
|
|
298
302
|
? 'Queued'
|
|
@@ -301,17 +305,43 @@ function StatusPill({ job }) {
|
|
|
301
305
|
? `${job.progress.current} of ${job.progress.total}`
|
|
302
306
|
: 'Rendering…'
|
|
303
307
|
: job.status === 'done'
|
|
304
|
-
?
|
|
308
|
+
? degraded && job.degraded.audioDropped
|
|
309
|
+
? 'Ready · no audio'
|
|
310
|
+
: degraded
|
|
311
|
+
? 'Ready · degraded'
|
|
312
|
+
: 'Ready'
|
|
305
313
|
: 'Failed';
|
|
306
314
|
const eta = job.status === 'running' ? etaLabel(job) : null;
|
|
307
315
|
return (
|
|
308
|
-
<span
|
|
316
|
+
<span
|
|
317
|
+
className={`st-export-pill st-export-pill--${job.status}${
|
|
318
|
+
degraded ? ' st-export-pill--degraded' : ''
|
|
319
|
+
}`}
|
|
320
|
+
>
|
|
309
321
|
{statusText}
|
|
310
322
|
{eta ? ` · ${eta}` : ''}
|
|
311
323
|
</span>
|
|
312
324
|
);
|
|
313
325
|
}
|
|
314
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The "your file is missing something" row. Rendered on a `done` job that came
|
|
329
|
+
* back degraded — in the panel row and in the completion toast, because the
|
|
330
|
+
* whole failure mode of this bug was that neither surface said anything.
|
|
331
|
+
*/
|
|
332
|
+
function DegradedNote({ degraded }) {
|
|
333
|
+
if (!degraded) return null;
|
|
334
|
+
return (
|
|
335
|
+
<div className="st-export-degraded" data-testid="export-degraded-note">
|
|
336
|
+
<span aria-hidden="true">⚠</span>{' '}
|
|
337
|
+
{degraded.audioDropped
|
|
338
|
+
? 'Exported without audio.'
|
|
339
|
+
: 'Exported with reduced fidelity.'}
|
|
340
|
+
{degraded.remedy ? <div className="st-export-degraded__fix">{degraded.remedy}</div> : null}
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
315
345
|
export function ExportBadge({ center }) {
|
|
316
346
|
const busy = center.busyCount > 0;
|
|
317
347
|
return (
|
|
@@ -350,8 +380,14 @@ export function ExportToast({ center }) {
|
|
|
350
380
|
×
|
|
351
381
|
</button>
|
|
352
382
|
<div className="st-toast-hd">
|
|
353
|
-
<span aria-hidden="true">{pending ? '⋯' : ok ? '⬇' : '⚠'}</span>
|
|
354
|
-
{pending
|
|
383
|
+
<span aria-hidden="true">{pending ? '⋯' : ok && !job.degraded ? '⬇' : '⚠'}</span>
|
|
384
|
+
{pending
|
|
385
|
+
? 'Exporting…'
|
|
386
|
+
: ok
|
|
387
|
+
? job.degraded
|
|
388
|
+
? 'Export ready — with a problem'
|
|
389
|
+
: 'Export ready'
|
|
390
|
+
: 'Export failed'}
|
|
355
391
|
</div>
|
|
356
392
|
<div className="st-toast-title">{jobLabel(job)}</div>
|
|
357
393
|
{pending ? (
|
|
@@ -364,6 +400,7 @@ export function ExportToast({ center }) {
|
|
|
364
400
|
{ok ? (job.filename ?? 'Ready to download.') : (job.error ?? 'Something went wrong.')}
|
|
365
401
|
</div>
|
|
366
402
|
)}
|
|
403
|
+
{ok && <DegradedNote degraded={job.degraded} />}
|
|
367
404
|
{ok && (
|
|
368
405
|
<div className="st-toast-actions">
|
|
369
406
|
<button
|
|
@@ -437,6 +474,7 @@ export function ExportPanel({ center }) {
|
|
|
437
474
|
{(job.status === 'queued' || job.status === 'running') && (
|
|
438
475
|
<ProgressBar progress={job.progress} />
|
|
439
476
|
)}
|
|
477
|
+
{job.status === 'done' && <DegradedNote degraded={job.degraded} />}
|
|
440
478
|
{job.status === 'done' && (
|
|
441
479
|
<div className="st-export-item__ft">
|
|
442
480
|
<span className="st-export-item__filename">{job.filename}</span>
|
|
@@ -1058,9 +1058,29 @@ function ChatEmpty() {
|
|
|
1058
1058
|
);
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
1061
|
-
function QuickActions() {
|
|
1061
|
+
function QuickActions({ openComments = 0 }) {
|
|
1062
1062
|
return (
|
|
1063
1063
|
<div className="chat-quick">
|
|
1064
|
+
{/* Issue #74 — the reporter asked for a button that IMPLEMENTS the
|
|
1065
|
+
comments they already dropped on the canvas, not one that adds a new
|
|
1066
|
+
one. `/design:edit` already owns the whole behaviour (edit.md §3: a
|
|
1067
|
+
generic feedback string + open comments → iterate each as a scoped
|
|
1068
|
+
edit, resolve each after it lands), so this is purely a shortcut to
|
|
1069
|
+
the phrasing that triggers it. Shown only when there IS something to
|
|
1070
|
+
implement — a permanently-disabled verb would just be noise.
|
|
1071
|
+
Prefill, never fire blind: same rule as QUICK_ACTIONS above, so the
|
|
1072
|
+
user can narrow the ask ("…but skip the one about the colour")
|
|
1073
|
+
before sending. */}
|
|
1074
|
+
{openComments > 0 ? (
|
|
1075
|
+
<ThreadPrimitive.Suggestion
|
|
1076
|
+
prompt="/design:edit fix open comments"
|
|
1077
|
+
send={false}
|
|
1078
|
+
className="btn btn--ghost btn--sm chat-qa chat-qa--comments"
|
|
1079
|
+
data-testid="chat-qa-implement-comments"
|
|
1080
|
+
>
|
|
1081
|
+
{`✓ Implement ${openComments} comment${openComments === 1 ? '' : 's'}`}
|
|
1082
|
+
</ThreadPrimitive.Suggestion>
|
|
1083
|
+
) : null}
|
|
1064
1084
|
{QUICK_ACTIONS.map((a) => (
|
|
1065
1085
|
<ThreadPrimitive.Suggestion
|
|
1066
1086
|
key={a.label}
|
|
@@ -1495,6 +1515,7 @@ function ChatThread({
|
|
|
1495
1515
|
activeCanvas,
|
|
1496
1516
|
selected,
|
|
1497
1517
|
designRel,
|
|
1518
|
+
openComments,
|
|
1498
1519
|
transcriptView,
|
|
1499
1520
|
onSetTranscriptView,
|
|
1500
1521
|
onPermissionRequest,
|
|
@@ -1735,7 +1756,7 @@ function ChatThread({
|
|
|
1735
1756
|
) : (
|
|
1736
1757
|
<ErrorCard error={turnError} onRetry={retryLastTurn} />
|
|
1737
1758
|
)}
|
|
1738
|
-
<QuickActions />
|
|
1759
|
+
<QuickActions openComments={openComments} />
|
|
1739
1760
|
<Composer
|
|
1740
1761
|
activeCanvas={activeCanvas}
|
|
1741
1762
|
chatCtx={ctxDismissed ? null : chatCtx}
|
|
@@ -1766,6 +1787,7 @@ export default function ChatPanel({
|
|
|
1766
1787
|
activeCanvas,
|
|
1767
1788
|
selected,
|
|
1768
1789
|
designRel,
|
|
1790
|
+
openComments = 0,
|
|
1769
1791
|
width,
|
|
1770
1792
|
resizing,
|
|
1771
1793
|
onClose,
|
|
@@ -2288,6 +2310,7 @@ export default function ChatPanel({
|
|
|
2288
2310
|
activeCanvas={activeCanvas}
|
|
2289
2311
|
selected={selected}
|
|
2290
2312
|
designRel={designRel}
|
|
2313
|
+
openComments={openComments}
|
|
2291
2314
|
transcriptView={transcriptView}
|
|
2292
2315
|
onSetTranscriptView={setTranscriptView}
|
|
2293
2316
|
onPermissionRequest={onPermissionRequest}
|
|
@@ -189,6 +189,34 @@ function projectFromHubUrl(hubUrl, cloudHost) {
|
|
|
189
189
|
return norm(host.slice(0, -(cloudHost.length + 1)).replace(/^view-/, ''));
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Is this project row the one THIS folder is already linked to?
|
|
194
|
+
*
|
|
195
|
+
* Fix 7 of the 2026-08-10 sync RCA: every row used to render the same
|
|
196
|
+
* `Connect <name>` button, including the project this folder was already
|
|
197
|
+
* syncing with — the UI lying about an established link. Only a CREDENTIALED
|
|
198
|
+
* link counts (the `linkedHub` half alone comes from a committed config.json —
|
|
199
|
+
* attacker-authorable, B2), so a linked-but-uncredentialed row still offers
|
|
200
|
+
* Connect: it genuinely needs the sign-in.
|
|
201
|
+
*
|
|
202
|
+
* Addresses inside the configured cloud zone compare by the project label they
|
|
203
|
+
* carry (`projectFromHubUrl`, NFKD-folded like the platform); addresses that
|
|
204
|
+
* carry none (self-hosts, e2e stubs at `http://127.0.0.1:<port>`) fall back to
|
|
205
|
+
* an exact origin match, so two stubs on different ports never collapse.
|
|
206
|
+
*/
|
|
207
|
+
export function isLinkedProjectRow({ projectUrl, linkedHubUrl, linkedHubCredentialed, cloudUrl }) {
|
|
208
|
+
if (!linkedHubUrl || !linkedHubCredentialed) return false;
|
|
209
|
+
const cloudHost = hostOf(cloudUrl);
|
|
210
|
+
const linked = projectFromHubUrl(linkedHubUrl, cloudHost);
|
|
211
|
+
const row = projectFromHubUrl(projectUrl, cloudHost);
|
|
212
|
+
if (linked && row) return linked === row;
|
|
213
|
+
try {
|
|
214
|
+
return new URL(String(projectUrl)).origin === new URL(String(linkedHubUrl)).origin;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
192
220
|
/**
|
|
193
221
|
* Does this folder look like the cloud project a link is naming?
|
|
194
222
|
*
|
|
@@ -326,6 +354,7 @@ function Spark({ size = 15 }) {
|
|
|
326
354
|
function Icon({ name, size = 15 }) {
|
|
327
355
|
const p = {
|
|
328
356
|
'chevron-up': <polyline points="3.5 10 8 5.5 12.5 10" />,
|
|
357
|
+
check: <polyline points="3 8.5 6.5 12 13 4.5" />,
|
|
329
358
|
external: (
|
|
330
359
|
<>
|
|
331
360
|
<path d="M6 3.5H3.2A.7.7 0 0 0 2.5 4.2v8.6a.7.7 0 0 0 .7.7h8.6a.7.7 0 0 0 .7-.7V10" />
|
|
@@ -369,7 +398,11 @@ function Icon({ name, size = 15 }) {
|
|
|
369
398
|
);
|
|
370
399
|
}
|
|
371
400
|
|
|
372
|
-
|
|
401
|
+
// `onLinkedHub` lifts the resolved link state to the app shell (DDR-218): the
|
|
402
|
+
// GitPanel's cloud-managed posture turns on the same `linkedHub.credentialed`
|
|
403
|
+
// fact this panel's Connected row does, and CloudBar owns every action that
|
|
404
|
+
// changes it (status resolve, attach, detach) — so it reports, the app decides.
|
|
405
|
+
export default function CloudBar({ syncStatus, onLinkedHub }) {
|
|
373
406
|
const [state, setState] = useState('loading'); // loading | out | in
|
|
374
407
|
const [email, setEmail] = useState(null);
|
|
375
408
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
@@ -417,6 +450,7 @@ export default function CloudBar({ syncStatus }) {
|
|
|
417
450
|
linkedHub: r.json?.linkedHub ?? null,
|
|
418
451
|
resolved: true,
|
|
419
452
|
});
|
|
453
|
+
onLinkedHub?.(r.json?.linkedHub ?? null);
|
|
420
454
|
if (r.ok && r.json?.connected) {
|
|
421
455
|
setEmail(r.json.email);
|
|
422
456
|
setState('in');
|
|
@@ -476,11 +510,22 @@ export default function CloudBar({ syncStatus }) {
|
|
|
476
510
|
setPending(null);
|
|
477
511
|
if (r.ok && r.json?.ok) {
|
|
478
512
|
setConnected({ project: r.json.project ?? pending.project, sync: r.json.sync });
|
|
513
|
+
adoptLink(r.json.url);
|
|
479
514
|
} else {
|
|
480
515
|
setError(r.json?.error || 'The workspace could not be connected.');
|
|
481
516
|
}
|
|
482
517
|
}
|
|
483
518
|
|
|
519
|
+
// A successful attach IS a credentialed link — mirror it into `local` so the
|
|
520
|
+
// project list flips to Connected without a second status fetch (the server
|
|
521
|
+
// just wrote both halves: linkedHub in config.json + the hub credential).
|
|
522
|
+
function adoptLink(url) {
|
|
523
|
+
if (typeof url === 'string' && url) {
|
|
524
|
+
setLocal((l) => ({ ...l, linkedHub: { url, credentialed: true } }));
|
|
525
|
+
onLinkedHub?.({ url, credentialed: true });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
484
529
|
// Escape answers the decision modal the same way the scrim does: Not now.
|
|
485
530
|
// Declining must always be the cheap move — the dashboard re-mints a code on
|
|
486
531
|
// the next "Open in Maude".
|
|
@@ -562,12 +607,29 @@ export default function CloudBar({ syncStatus }) {
|
|
|
562
607
|
setBusy('');
|
|
563
608
|
if (r.ok && r.json?.ok) {
|
|
564
609
|
setConnected({ project: r.json.project ?? projectId, sync: r.json.sync });
|
|
610
|
+
adoptLink(r.json.url);
|
|
565
611
|
} else {
|
|
566
612
|
setError(r.json?.error || 'The workspace could not be connected.');
|
|
567
613
|
}
|
|
568
614
|
setMenuOpen(false);
|
|
569
615
|
}
|
|
570
616
|
|
|
617
|
+
/** The in-app `maude design unlink` — drops the link AND stops syncing now. */
|
|
618
|
+
async function disconnect() {
|
|
619
|
+
setBusy('disconnect');
|
|
620
|
+
setError('');
|
|
621
|
+
const r = await api('/_api/cloud/detach', { method: 'POST' });
|
|
622
|
+
setBusy('');
|
|
623
|
+
setMenuOpen(false);
|
|
624
|
+
if (r.ok && r.json?.ok) {
|
|
625
|
+
setLocal((l) => ({ ...l, linkedHub: null }));
|
|
626
|
+
setConnected(null);
|
|
627
|
+
onLinkedHub?.(null);
|
|
628
|
+
} else {
|
|
629
|
+
setError(r.json?.error || 'The workspace could not be disconnected.');
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
571
633
|
async function signOut() {
|
|
572
634
|
setMenuOpen(false);
|
|
573
635
|
await api('/_api/cloud/signout', { method: 'POST' });
|
|
@@ -801,6 +863,35 @@ export default function CloudBar({ syncStatus }) {
|
|
|
801
863
|
<Icon name="external" size={15} /> View {p.name || p.id} in the browser
|
|
802
864
|
<span className="gi-menu-login" style={{ marginLeft: 'auto' }}>viewer</span>
|
|
803
865
|
</button>
|
|
866
|
+
) : isLinkedProjectRow({
|
|
867
|
+
projectUrl: p.url,
|
|
868
|
+
linkedHubUrl: local.linkedHub?.url,
|
|
869
|
+
linkedHubCredentialed: local.linkedHub?.credentialed,
|
|
870
|
+
cloudUrl,
|
|
871
|
+
}) ? (
|
|
872
|
+
// The project this folder is ALREADY linked to. Connected is
|
|
873
|
+
// a state, not an action — the one action left is leaving.
|
|
874
|
+
// (Same wrapper shape as the "Loading projects…" row; the
|
|
875
|
+
// button is the focusable menu entry.)
|
|
876
|
+
<div
|
|
877
|
+
key={p.id}
|
|
878
|
+
className="gi-menu-item is-connected"
|
|
879
|
+
data-testid={`cloud-project-${p.id}`}
|
|
880
|
+
>
|
|
881
|
+
<Icon name="check" size={15} /> {p.name || p.id}
|
|
882
|
+
<span className="gi-menu-login" style={{ marginLeft: 'auto' }}>Connected</span>
|
|
883
|
+
<button
|
|
884
|
+
type="button"
|
|
885
|
+
className="btn btn--ghost btn--sm"
|
|
886
|
+
role="menuitem"
|
|
887
|
+
disabled={busy === 'disconnect'}
|
|
888
|
+
onClick={disconnect}
|
|
889
|
+
aria-label={`Disconnect this folder from ${p.name || p.id}`}
|
|
890
|
+
data-testid={`cloud-disconnect-${p.id}`}
|
|
891
|
+
>
|
|
892
|
+
{busy === 'disconnect' ? 'Disconnecting…' : 'Disconnect'}
|
|
893
|
+
</button>
|
|
894
|
+
</div>
|
|
804
895
|
) : (
|
|
805
896
|
<button
|
|
806
897
|
key={p.id}
|