@1agh/maude 0.53.1 → 0.54.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/api.ts +13 -7
- package/apps/studio/bin/screenshot.sh +173 -17
- package/apps/studio/build.ts +37 -3
- package/apps/studio/canvas-build-sandbox.ts +385 -0
- package/apps/studio/canvas-build-worker.ts +85 -0
- package/apps/studio/client/app.jsx +308 -21
- package/apps/studio/client/canvas-url.js +7 -0
- package/apps/studio/client/export-center.jsx +10 -2
- package/apps/studio/client/panels/SettingsPanel.jsx +37 -12
- package/apps/studio/client/report-bug.jsx +125 -30
- package/apps/studio/client/styles/4-components-maude.css +5 -0
- package/apps/studio/client/styles/4-components.css +75 -0
- package/apps/studio/cloud-build.ts +208 -0
- package/apps/studio/collab/room.ts +43 -7
- package/apps/studio/context.ts +12 -5
- package/apps/studio/dist/client.bundle.js +910 -910
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/git/repo-lock.ts +305 -0
- package/apps/studio/git/service.ts +171 -21
- package/apps/studio/http.ts +383 -31
- package/apps/studio/inspect.ts +66 -6
- package/apps/studio/paths.ts +8 -0
- package/apps/studio/server.ts +216 -53
- package/apps/studio/session-scope.ts +101 -0
- package/apps/studio/sync/autocommit.ts +84 -46
- package/apps/studio/sync/index.ts +20 -0
- package/apps/studio/test/canvas-build-sandbox.test.ts +69 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +66 -0
- package/apps/studio/test/canvas-shell-base.test.ts +107 -0
- package/apps/studio/test/canvas-url.test.ts +26 -0
- package/apps/studio/test/cloud-build.test.ts +95 -0
- package/apps/studio/test/cloud-session-role.test.ts +131 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +119 -0
- package/apps/studio/test/collab-readonly-gate.test.ts +194 -0
- package/apps/studio/test/config-projection.test.ts +104 -0
- package/apps/studio/test/git-system-engine.test.ts +135 -0
- package/apps/studio/test/read-only-gate.test.ts +58 -0
- package/apps/studio/test/repo-concurrency.test.ts +146 -0
- package/apps/studio/test/repo-lock.test.ts +350 -0
- package/apps/studio/test/server-lifecycle.test.ts +11 -1
- package/apps/studio/test/session-runtime-state.test.ts +226 -0
- package/apps/studio/test/session-scope.test.ts +100 -0
- package/apps/studio/test/shell-importmap.test.ts +8 -1
- package/apps/studio/test/sync-autocommit.test.ts +76 -0
- package/apps/studio/test/sync-hardening.test.ts +75 -0
- package/apps/studio/test/workspace-containment.test.ts +90 -11
- package/apps/studio/use-collab.tsx +21 -1
- package/apps/studio/whats-new.json +47 -2
- package/apps/studio/workspace-mode.ts +161 -28
- package/apps/studio/ws.ts +74 -8
- package/cli/commands/kg.mjs +49 -19
- package/cli/lib/gitignore-block.mjs +3 -0
- package/package.json +11 -9
- package/plugins/design/dependencies.json +1 -1
- package/plugins/design/templates/_shell.html +66 -32
- package/plugins/flow/.claude-plugin/config.schema.json +1 -1
- package/plugins/flow/dependencies.json +1 -1
|
@@ -2429,6 +2429,13 @@ function Sidebar({
|
|
|
2429
2429
|
// Cloud Phase 25 C2 — viewer role: create / delete / move / rename
|
|
2430
2430
|
// affordances are absent (buttons, composer, row menus, drag & drop).
|
|
2431
2431
|
readOnly = false,
|
|
2432
|
+
/** `{ dashboardUrl, projectName }` when this is a cloud tab, else null. */
|
|
2433
|
+
// Tri-state (see the note where this value is created): `undefined` until
|
|
2434
|
+
// the server config answers, `null` for the desktop, an object for a cloud
|
|
2435
|
+
// tab. Defaulting it to `null` here is what re-broke the boot 404 after the
|
|
2436
|
+
// call sites were fixed — a default fires on `undefined`, so "not known yet"
|
|
2437
|
+
// became "not cloud" one layer further in, invisibly.
|
|
2438
|
+
cloud,
|
|
2432
2439
|
groups,
|
|
2433
2440
|
activePath,
|
|
2434
2441
|
activeDsName,
|
|
@@ -2707,6 +2714,7 @@ function Sidebar({
|
|
|
2707
2714
|
<StIcon name="search" size={13} />
|
|
2708
2715
|
<input
|
|
2709
2716
|
type="search"
|
|
2717
|
+
data-testid="canvas-search"
|
|
2710
2718
|
placeholder="Search canvases…"
|
|
2711
2719
|
value={search}
|
|
2712
2720
|
onChange={(e) => setSearch(e.target.value)}
|
|
@@ -2825,7 +2833,12 @@ function Sidebar({
|
|
|
2825
2833
|
{/* Cloud Phase 23 C3 — Maude Cloud sign-in + remote-project attach, docked
|
|
2826
2834
|
above the GitHub identity. Dev-server-backed, so it works in the desktop
|
|
2827
2835
|
shell AND a plain browser. */}
|
|
2828
|
-
|
|
2836
|
+
{/* Cloud Phase 27 — "Sign in to Maude Cloud" is an offer to do the thing
|
|
2837
|
+
you have already done: you reached this tab THROUGH a Maude account.
|
|
2838
|
+
Absent rather than disabled, because unlike the agent chat there is
|
|
2839
|
+
nothing here to explain — the capability is not missing, it is
|
|
2840
|
+
already satisfied. */}
|
|
2841
|
+
{cloud === null ? <CloudBar /> : null}
|
|
2829
2842
|
{/* Phase 28 (E3) — GitHub identity as a compact avatar docked at the BOTTOM:
|
|
2830
2843
|
sign in, connected account + New/Pull/Share, sign out. Self-contained
|
|
2831
2844
|
(owns its device-code + CreateProject dialogs). Renders nothing in browser. */}
|
|
@@ -3353,11 +3366,21 @@ function ViewDropdown({ panels, onToggle, onClose, onZoom, hasCanvas }) {
|
|
|
3353
3366
|
role="menuitem"
|
|
3354
3367
|
className={'st-dd-item' + (p.checked ? ' is-on' : '')}
|
|
3355
3368
|
aria-disabled={p.disabled ? 'true' : undefined}
|
|
3369
|
+
title={p.hint || undefined}
|
|
3356
3370
|
onClick={() => {
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3371
|
+
// Cloud Phase 27 C2 — a row that states an absence still has
|
|
3372
|
+
// somewhere to send you. A disabled item with a `href` opens it
|
|
3373
|
+
// instead of doing nothing, which is the difference between "this
|
|
3374
|
+
// is not available here" and a dead button.
|
|
3375
|
+
if (p.disabled) {
|
|
3376
|
+
if (p.href) {
|
|
3377
|
+
window.open(p.href, '_blank', 'noopener,noreferrer');
|
|
3378
|
+
onClose();
|
|
3379
|
+
}
|
|
3380
|
+
return;
|
|
3360
3381
|
}
|
|
3382
|
+
onToggle(p.id);
|
|
3383
|
+
onClose();
|
|
3361
3384
|
}}
|
|
3362
3385
|
>
|
|
3363
3386
|
<span className="st-dd-lead">
|
|
@@ -3604,6 +3627,13 @@ function EditDropdown({ onAction, onClose, hasCanvas, readOnly = false }) {
|
|
|
3604
3627
|
function Menubar({
|
|
3605
3628
|
activePath,
|
|
3606
3629
|
project,
|
|
3630
|
+
/** `{ dashboardUrl, projectName }` when this is a cloud tab, else null. */
|
|
3631
|
+
// Tri-state (see the note where this value is created): `undefined` until
|
|
3632
|
+
// the server config answers, `null` for the desktop, an object for a cloud
|
|
3633
|
+
// tab. Defaulting it to `null` here is what re-broke the boot 404 after the
|
|
3634
|
+
// call sites were fixed — a default fires on `undefined`, so "not known yet"
|
|
3635
|
+
// became "not cloud" one layer further in, invisibly.
|
|
3636
|
+
cloud,
|
|
3607
3637
|
tabsCount,
|
|
3608
3638
|
openMenu,
|
|
3609
3639
|
setOpenMenu,
|
|
@@ -3678,8 +3708,19 @@ function Menubar({
|
|
|
3678
3708
|
<span style={{ color: 'var(--u-fg-3)' }}>no canvas open</span>
|
|
3679
3709
|
);
|
|
3680
3710
|
|
|
3681
|
-
// Cloud Phase
|
|
3682
|
-
|
|
3711
|
+
// Cloud Phase 27 C1 — ROLE SHAPES WHAT IS OFFERED, NEVER WHAT IS VISIBLE.
|
|
3712
|
+
//
|
|
3713
|
+
// `inspector` and `layers` used to be on this list and should never have been.
|
|
3714
|
+
// Read-only means cannot CHANGE, not cannot SEE: a reviewer needs the
|
|
3715
|
+
// structure and the measured values to say anything useful, and hiding them
|
|
3716
|
+
// is how a viewer's Maude became a worse Maude rather than a narrower one.
|
|
3717
|
+
// What stays hidden is only what genuinely does not exist for this session —
|
|
3718
|
+
// the agent chat (`assistant`, which needs a `claude` on the machine the app
|
|
3719
|
+
// is running on) and auto-open (a preference for a surface you cannot edit).
|
|
3720
|
+
// In the cloud the `assistant` row is the stated-absence row above, so it must
|
|
3721
|
+
// NOT be filtered away here — a viewer would then get silence, which is the
|
|
3722
|
+
// exact thing C2 forbids.
|
|
3723
|
+
const viewerHiddenPanels = new Set(cloud ? ['autoopen'] : ['assistant', 'autoopen']);
|
|
3683
3724
|
const panels = [
|
|
3684
3725
|
{ id: 'tree', label: 'Project Tree', shortcut: 'T', checked: sidebarOpen, disabled: false },
|
|
3685
3726
|
{
|
|
@@ -3745,7 +3786,26 @@ function Menubar({
|
|
|
3745
3786
|
disabled: false,
|
|
3746
3787
|
},
|
|
3747
3788
|
]
|
|
3748
|
-
:
|
|
3789
|
+
: // Cloud Phase 27 C2 — THE AGENT'S ABSENCE IS STATED WHERE THE AGENT
|
|
3790
|
+
// WOULD BE. It runs on YOUR `claude` subscription, on YOUR machine
|
|
3791
|
+
// (DDR-123), so a browser tab genuinely cannot have it. That is a
|
|
3792
|
+
// legitimate difference — but a menu that silently lacks an item its
|
|
3793
|
+
// user has seen on the desktop reads as "the cloud one is broken", so
|
|
3794
|
+
// the row stays, disabled, saying where to find it. Never a hidden
|
|
3795
|
+
// item, never a dead button, never silence.
|
|
3796
|
+
cloud
|
|
3797
|
+
? [
|
|
3798
|
+
{
|
|
3799
|
+
id: 'assistant',
|
|
3800
|
+
label: 'Assistant — in the desktop app',
|
|
3801
|
+
shortcut: '',
|
|
3802
|
+
checked: false,
|
|
3803
|
+
disabled: true,
|
|
3804
|
+
href: 'https://maude.sh/download',
|
|
3805
|
+
hint: 'The agent runs on your own Claude subscription, on your own machine.',
|
|
3806
|
+
},
|
|
3807
|
+
]
|
|
3808
|
+
: []),
|
|
3749
3809
|
{
|
|
3750
3810
|
id: 'annotate',
|
|
3751
3811
|
label: 'Annotations',
|
|
@@ -3841,13 +3901,41 @@ function Menubar({
|
|
|
3841
3901
|
}, [openMenu, setOpenMenu]);
|
|
3842
3902
|
|
|
3843
3903
|
return (
|
|
3844
|
-
<header
|
|
3904
|
+
<header
|
|
3905
|
+
className="st-menubar"
|
|
3906
|
+
role="menubar"
|
|
3907
|
+
aria-label="Application menubar"
|
|
3908
|
+
data-testid="menubar"
|
|
3909
|
+
>
|
|
3845
3910
|
<span className="st-brand" data-tour="brand">
|
|
3846
3911
|
<span className="st-brand-mark">
|
|
3847
3912
|
<svg viewBox="0 0 32 32" width="100%" height="100%" fill="none" aria-hidden="true"><path d="M16 5l2.8 8.2L27 16l-8.2 2.8L16 27l-2.8-8.2L5 16l8.2-2.8z" fill="currentColor" /></svg>
|
|
3848
3913
|
</span>
|
|
3849
3914
|
<span className="st-brand-name">maude</span>
|
|
3850
3915
|
</span>
|
|
3916
|
+
{/* Cloud Phase 27 C4 — THE WAY BACK OUT.
|
|
3917
|
+
A browser tab has no window title and no app switcher, so a teammate
|
|
3918
|
+
who followed a link has nothing telling them which project they are in
|
|
3919
|
+
or how to leave it. The desktop gets both for free from the OS; the
|
|
3920
|
+
cloud has to say them. This is an ADDITION to the shared client behind
|
|
3921
|
+
the same `cloud` flag as C2's agent notice — never a fork of the
|
|
3922
|
+
component, which is how the two shells started diverging last time. */}
|
|
3923
|
+
{cloud ? (
|
|
3924
|
+
<span className="st-cloudback" data-testid="cloud-back">
|
|
3925
|
+
<a
|
|
3926
|
+
className="st-cloudback-link"
|
|
3927
|
+
href={cloud.dashboardUrl || 'https://cloud.maude.sh'}
|
|
3928
|
+
title="Back to your projects"
|
|
3929
|
+
>
|
|
3930
|
+
← Dashboard
|
|
3931
|
+
</a>
|
|
3932
|
+
{cloud.projectName ? (
|
|
3933
|
+
<span className="st-cloudback-project" data-testid="cloud-project-name">
|
|
3934
|
+
{cloud.projectName}
|
|
3935
|
+
</span>
|
|
3936
|
+
) : null}
|
|
3937
|
+
</span>
|
|
3938
|
+
) : null}
|
|
3851
3939
|
<nav className="st-menus" aria-label="Application menus" data-tour="menus">
|
|
3852
3940
|
{MENU_NAMES.map((name) => {
|
|
3853
3941
|
const key = name.toLowerCase();
|
|
@@ -3972,11 +4060,45 @@ function Menubar({
|
|
|
3972
4060
|
<span
|
|
3973
4061
|
className="st-stamp st-stamp--viewonly"
|
|
3974
4062
|
data-testid="view-only-stamp"
|
|
3975
|
-
data-tip=
|
|
4063
|
+
data-tip={
|
|
4064
|
+
cloud?.user
|
|
4065
|
+
? `${cloud.user} is a viewer on this project — you can browse, comment and export. Ask a project owner for edit access, or sign out if that is not the account you meant to use.`
|
|
4066
|
+
: 'Your role in this project is viewer — you can browse, comment when available, and export. Ask a project owner for edit access.'
|
|
4067
|
+
}
|
|
3976
4068
|
>
|
|
3977
4069
|
VIEW ONLY
|
|
3978
4070
|
</span>
|
|
3979
4071
|
)}
|
|
4072
|
+
{/* Cloud Phase 27 C4 — WHO YOU ARE, AND HOW TO STOP BEING THEM.
|
|
4073
|
+
A browser tab carries no account identity of its own: the cookie is
|
|
4074
|
+
invisible, and the only thing on screen that reflected it was a
|
|
4075
|
+
three-word stamp. An owner who read VIEW ONLY had no way to tell
|
|
4076
|
+
whether the role was wrong or the ACCOUNT was, and no way to change
|
|
4077
|
+
either — the session outlived any fix by up to twelve hours because
|
|
4078
|
+
nothing in the studio could end it. This ends it. */}
|
|
4079
|
+
{cloud?.user ? (
|
|
4080
|
+
<span className="st-cloudwho" data-testid="cloud-account">
|
|
4081
|
+
<span
|
|
4082
|
+
className="st-cloudwho-email"
|
|
4083
|
+
title={cloud.role ? `Signed in as ${cloud.user} — ${cloud.role}` : cloud.user}
|
|
4084
|
+
>
|
|
4085
|
+
{cloud.user}
|
|
4086
|
+
</span>
|
|
4087
|
+
{/* A FORM, not a link. Signing out revokes the token server-side,
|
|
4088
|
+
so it is a write — and a write behind a GET is one any page can
|
|
4089
|
+
force on you from across the internet. */}
|
|
4090
|
+
<form method="post" action="/auth/browser/signout" className="st-cloudwho-form">
|
|
4091
|
+
<button
|
|
4092
|
+
type="submit"
|
|
4093
|
+
className="st-cloudwho-out"
|
|
4094
|
+
data-testid="cloud-signout"
|
|
4095
|
+
title="Sign out of this project"
|
|
4096
|
+
>
|
|
4097
|
+
Sign out
|
|
4098
|
+
</button>
|
|
4099
|
+
</form>
|
|
4100
|
+
</span>
|
|
4101
|
+
) : null}
|
|
3980
4102
|
{isNativeApp() && !readOnly && (
|
|
3981
4103
|
<button
|
|
3982
4104
|
type="button"
|
|
@@ -4442,7 +4564,7 @@ function StatusBar({
|
|
|
4442
4564
|
})();
|
|
4443
4565
|
|
|
4444
4566
|
return (
|
|
4445
|
-
<footer className="st-statusbar" role="contentinfo">
|
|
4567
|
+
<footer className="st-statusbar" role="contentinfo" data-testid="statusbar">
|
|
4446
4568
|
<span className="st-sb-slot st-sb-active" role="group" aria-label="Active file">
|
|
4447
4569
|
<span className="lead" aria-hidden="true" />
|
|
4448
4570
|
<span className="lbl">active</span>
|
|
@@ -4720,6 +4842,69 @@ function CommentsPanel({
|
|
|
4720
4842
|
// Phase 32 (Task 1) — auto-update notice. Shown after the shell has downloaded +
|
|
4721
4843
|
// staged a newer build in the background. Non-blocking: "Restart now" applies it,
|
|
4722
4844
|
// "Later" dismisses (the next focus/4h check re-stages and re-surfaces it).
|
|
4845
|
+
/**
|
|
4846
|
+
* What this role can do, said once, where a teammate lands — Cloud Phase 27 C3.
|
|
4847
|
+
*
|
|
4848
|
+
* A browser tab arrives with none of the context a desktop has: no window
|
|
4849
|
+
* title, no app the person chose to install, and — for a viewer — a set of
|
|
4850
|
+
* controls that will refuse them without explaining why. One line, dismissible,
|
|
4851
|
+
* remembered per role. It re-appears if the role CHANGES, because "you can edit
|
|
4852
|
+
* this now" is worth saying exactly as much as the first sentence was.
|
|
4853
|
+
*
|
|
4854
|
+
* Copy comes from the role the cell vouched (`role-matrix.mjs` is the authority
|
|
4855
|
+
* on what each one means; this is its sentence, not a second opinion).
|
|
4856
|
+
*/
|
|
4857
|
+
const ROLE_LINE = {
|
|
4858
|
+
owner: 'You own this project — edit, invite and export.',
|
|
4859
|
+
member: 'You can edit this project, comment and export.',
|
|
4860
|
+
viewer: 'You can look at this project, comment and download it, but not change it.',
|
|
4861
|
+
};
|
|
4862
|
+
|
|
4863
|
+
function CloudRoleBanner({ cloud }) {
|
|
4864
|
+
const role = cloud?.role || null;
|
|
4865
|
+
// Dismissal is READ AT RENDER, not seeded into state. `cfg.cloud` is
|
|
4866
|
+
// `undefined` until `/_config` lands, so a `useState` initializer runs while
|
|
4867
|
+
// there is no role yet — and whatever it decided then would stick forever,
|
|
4868
|
+
// which on the first attempt meant the banner never appeared at all. Found by
|
|
4869
|
+
// opening it in a browser; no test in this repo would have said a word.
|
|
4870
|
+
const [dismissedNow, setDismissedNow] = useState(false);
|
|
4871
|
+
if (!role || dismissedNow) return null;
|
|
4872
|
+
const storageKey = `maude-cloud-role-seen:${role}`;
|
|
4873
|
+
let alreadySeen = false;
|
|
4874
|
+
try {
|
|
4875
|
+
alreadySeen = localStorage.getItem(storageKey) === '1';
|
|
4876
|
+
} catch {
|
|
4877
|
+
/* private mode / storage disabled — show it, saying it twice beats never */
|
|
4878
|
+
}
|
|
4879
|
+
if (alreadySeen) return null;
|
|
4880
|
+
const line = ROLE_LINE[role];
|
|
4881
|
+
if (!line) return null;
|
|
4882
|
+
return (
|
|
4883
|
+
<div
|
|
4884
|
+
role="status"
|
|
4885
|
+
aria-live="polite"
|
|
4886
|
+
className="st-banner st-banner--info"
|
|
4887
|
+
data-testid="cloud-role-banner"
|
|
4888
|
+
>
|
|
4889
|
+
<span className="st-banner-dot" aria-hidden="true" />
|
|
4890
|
+
<span>{line}</span>
|
|
4891
|
+
<button
|
|
4892
|
+
type="button"
|
|
4893
|
+
className="btn btn--ghost btn--sm"
|
|
4894
|
+
data-testid="cloud-role-banner-dismiss"
|
|
4895
|
+
onClick={() => {
|
|
4896
|
+
setDismissedNow(true);
|
|
4897
|
+
try {
|
|
4898
|
+
localStorage.setItem(storageKey, '1');
|
|
4899
|
+
} catch {}
|
|
4900
|
+
}}
|
|
4901
|
+
>
|
|
4902
|
+
Got it
|
|
4903
|
+
</button>
|
|
4904
|
+
</div>
|
|
4905
|
+
);
|
|
4906
|
+
}
|
|
4907
|
+
|
|
4723
4908
|
function UpdateBanner({ update, onDismiss }) {
|
|
4724
4909
|
const [restarting, setRestarting] = useState(false);
|
|
4725
4910
|
if (!update) return null;
|
|
@@ -8232,6 +8417,9 @@ function photoAssetOfSelection(el) {
|
|
|
8232
8417
|
}
|
|
8233
8418
|
|
|
8234
8419
|
function InspectorPanel({
|
|
8420
|
+
/** E4 — `inspector-panel` / `layers-panel`, so one parity spec can assert
|
|
8421
|
+
* both shells show them. Undefined elsewhere; the attribute simply absent. */
|
|
8422
|
+
testId,
|
|
8235
8423
|
selected,
|
|
8236
8424
|
onClose,
|
|
8237
8425
|
layersTree,
|
|
@@ -8701,6 +8889,7 @@ function InspectorPanel({
|
|
|
8701
8889
|
style={width ? { width, flexBasis: width } : undefined}
|
|
8702
8890
|
aria-label="Inspector"
|
|
8703
8891
|
data-tour="inspector"
|
|
8892
|
+
data-testid={testId}
|
|
8704
8893
|
>
|
|
8705
8894
|
<div
|
|
8706
8895
|
className="st-rp-tabs"
|
|
@@ -9164,7 +9353,18 @@ function App() {
|
|
|
9164
9353
|
// `config-updated` push (config.json hot-reload — /design:setup-ds rewrites
|
|
9165
9354
|
// it mid-session) — informs canvasUrl() so TSX iframes can pass the right
|
|
9166
9355
|
// ?designRel + ?tokens query to the canvas mount shell.
|
|
9167
|
-
|
|
9356
|
+
// `cloud` is deliberately TRI-STATE, and the third state is the useful one:
|
|
9357
|
+
// undefined — `/_config` has not answered yet; which shell this is is
|
|
9358
|
+
// UNKNOWN, and anything that would behave differently in the
|
|
9359
|
+
// two shells must wait rather than assume the desktop.
|
|
9360
|
+
// null — desktop or plain local browser.
|
|
9361
|
+
// object — a cloud tab (`{ dashboardUrl, projectName, user, role }`).
|
|
9362
|
+
//
|
|
9363
|
+
// Collapsing unknown into "not cloud" is what made the cloud studio open with
|
|
9364
|
+
// two console 404s every time: the sign-in bar and the export centre mounted
|
|
9365
|
+
// for one frame, each fired the request its shell exists to refuse, and then
|
|
9366
|
+
// unmounted. Nothing was broken and everything looked broken.
|
|
9367
|
+
const [cfg, setCfg] = useState({ designRel: '.design', cloud: undefined });
|
|
9168
9368
|
// Cloud Phase 25 C2 — viewer role, known at boot from /_config. Every
|
|
9169
9369
|
// editing affordance in the shell gates on this (absent, not hidden).
|
|
9170
9370
|
const viewerMode = !!cfg.readOnly;
|
|
@@ -9196,6 +9396,21 @@ function App() {
|
|
|
9196
9396
|
// decides what the UI offers; the cell (C1) and the dev-server's
|
|
9197
9397
|
// read-only gate (http.ts) are what actually stop a write.
|
|
9198
9398
|
readOnly: !!data.readOnly,
|
|
9399
|
+
// Cloud Phase 27 (DDR-209) — the capability that opens the cookieless
|
|
9400
|
+
// canvas origin. `canvasUrl()` appends it to every iframe URL. Absent
|
|
9401
|
+
// on a desktop, where the canvas origin is loopback and needs none.
|
|
9402
|
+
//
|
|
9403
|
+
// NOTE FOR WHOEVER ADDS THE NEXT FIELD: this is an explicit
|
|
9404
|
+
// projection, not a spread of `data`. A field the server starts
|
|
9405
|
+
// returning does NOT arrive here until it is named — which is how
|
|
9406
|
+
// both of these went missing and the cloud studio rendered without
|
|
9407
|
+
// its chrome and with every canvas iframe unauthenticated.
|
|
9408
|
+
canvasToken: data.canvasToken,
|
|
9409
|
+
// C2/C4 — `{ dashboardUrl, projectName, user, role }` when this is a
|
|
9410
|
+
// cloud tab. Its presence is what tells the shared client it is in a
|
|
9411
|
+
// browser tab on somebody else's machine; `user`/`role` are what let
|
|
9412
|
+
// it say WHICH account that tab is, and offer the way out.
|
|
9413
|
+
cloud: data.cloud ?? null,
|
|
9199
9414
|
}));
|
|
9200
9415
|
})
|
|
9201
9416
|
.catch(() => {});
|
|
@@ -9544,7 +9759,10 @@ function App() {
|
|
|
9544
9759
|
// toggles on its own and coexists with Inspector/Changes/Comments/Chat.
|
|
9545
9760
|
const toggleTimeline = useCallback(() => setTimelineOpen((v) => !v), []);
|
|
9546
9761
|
const whatsNew = useWhatsNew(MDCC_VERSION);
|
|
9547
|
-
|
|
9762
|
+
// Same tri-state rule as the sign-in bar: the export queue is a LOCAL job
|
|
9763
|
+
// runner, `/_api/export-jobs` is one of the routes a cell refuses outright
|
|
9764
|
+
// (DDR-209 D1), and hydrating before the shell is known 404s every boot.
|
|
9765
|
+
const exportCenter = useExportCenter({ enabled: cfg.cloud === null });
|
|
9548
9766
|
// Phase 29 (E4) — first-run onboarding wizard. The native shell boots a minimal
|
|
9549
9767
|
// "welcome" project on first launch; we ask it whether this is a first run and, if
|
|
9550
9768
|
// so, show the wizard OVER the (empty) canvas browser. Completing any door switches
|
|
@@ -10917,6 +11135,41 @@ function App() {
|
|
|
10917
11135
|
if (path !== SYSTEM_TAB) setLoadingPath(path);
|
|
10918
11136
|
}, []);
|
|
10919
11137
|
|
|
11138
|
+
/**
|
|
11139
|
+
* First open lands on a rendered canvas — Cloud Phase 27 C3.
|
|
11140
|
+
*
|
|
11141
|
+
* A desktop user chose this project, installed the app and knows what is in
|
|
11142
|
+
* it; the empty state is a helpful "pick a screen". A teammate following a
|
|
11143
|
+
* link has done none of that, and an empty pane with an arrow pointing at a
|
|
11144
|
+
* list is the browser saying "the thing you came for is somewhere else".
|
|
11145
|
+
*
|
|
11146
|
+
* CLOUD ONLY, and once. The desktop's empty state is deliberately unchanged —
|
|
11147
|
+
* it earns its keep there (the ⌘-hover lesson, quick setup) — and re-opening
|
|
11148
|
+
* a canvas the person deliberately closed would be a UI arguing with them.
|
|
11149
|
+
*/
|
|
11150
|
+
const autoOpened = useRef(false);
|
|
11151
|
+
useEffect(() => {
|
|
11152
|
+
if (autoOpened.current) return;
|
|
11153
|
+
if (!cfg.cloud) return; // desktop / unknown-yet
|
|
11154
|
+
if (tabs.length > 0) return;
|
|
11155
|
+
if (!groups.length) return; // tree not loaded
|
|
11156
|
+
// The first canvas that is somebody's WORK — a design-system specimen is a
|
|
11157
|
+
// rendered canvas too, and not what a teammate followed a link to see.
|
|
11158
|
+
let first = null;
|
|
11159
|
+
for (const g of groups) {
|
|
11160
|
+
for (const p of g.paths || []) {
|
|
11161
|
+
if (!CANVAS_EXT_RE.test(p)) continue;
|
|
11162
|
+
if (/(^|\/)system\//.test(p)) continue;
|
|
11163
|
+
first = p;
|
|
11164
|
+
break;
|
|
11165
|
+
}
|
|
11166
|
+
if (first) break;
|
|
11167
|
+
}
|
|
11168
|
+
if (!first) return;
|
|
11169
|
+
autoOpened.current = true;
|
|
11170
|
+
openTab(first);
|
|
11171
|
+
}, [cfg.cloud, groups, tabs.length, openTab]);
|
|
11172
|
+
|
|
10920
11173
|
const openSystem = useCallback(
|
|
10921
11174
|
(dsName) => {
|
|
10922
11175
|
// DsFolderRow passes the clicked DS name → scope the System view to it so
|
|
@@ -13386,10 +13639,17 @@ function App() {
|
|
|
13386
13639
|
}
|
|
13387
13640
|
// Cmd+Shift+I — toggle Inspector. Was bare "I", which collided with the
|
|
13388
13641
|
// canvas highlighter tool (same letter, different action by focus).
|
|
13389
|
-
//
|
|
13642
|
+
//
|
|
13643
|
+
// Cloud Phase 27 C1 — a VIEWER GETS THIS TOO, and the line above used to
|
|
13644
|
+
// say the opposite ("the panel is edit chrome"). C1 reversed that on
|
|
13645
|
+
// purpose: read-only means cannot CHANGE, not cannot SEE, and a reviewer
|
|
13646
|
+
// needs structure and measured values as much as anyone. The View menu
|
|
13647
|
+
// already offers it (`viewerHiddenPanels` no longer hides it); the
|
|
13648
|
+
// shortcut had been left behind, so the menu said yes and the keyboard
|
|
13649
|
+
// said no. Found by writing the parity spec that runs in both shells.
|
|
13390
13650
|
if (meta && e.shiftKey && (e.key === 'i' || e.key === 'I')) {
|
|
13391
13651
|
e.preventDefault();
|
|
13392
|
-
|
|
13652
|
+
toggleRightPanel('inspector');
|
|
13393
13653
|
return;
|
|
13394
13654
|
}
|
|
13395
13655
|
// Phase 31 (DDR-123) — Cmd+Shift+A opens the native ACP chat sidepanel.
|
|
@@ -13718,9 +13978,15 @@ function App() {
|
|
|
13718
13978
|
// assigned to it and which one is active (the open one). Layers is only a
|
|
13719
13979
|
// dockable panel in `separate` mode; Assistant is native-only.
|
|
13720
13980
|
const panelAvailable = (id) => {
|
|
13721
|
-
//
|
|
13722
|
-
//
|
|
13723
|
-
|
|
13981
|
+
// The AGENT is genuinely absent for a viewer (it edits; Phase 25 C2).
|
|
13982
|
+
//
|
|
13983
|
+
// Inspector and Layers are NOT — Cloud Phase 27 C1 reversed that, and this
|
|
13984
|
+
// line had been left behind: `viewerHiddenPanels` stopped hiding them in
|
|
13985
|
+
// the View menu while this still refused to give them a dock slot, so the
|
|
13986
|
+
// menu offered a panel that could never appear. Read-only means cannot
|
|
13987
|
+
// CHANGE, not cannot SEE: a reviewer needs structure and measured values,
|
|
13988
|
+
// which is the whole reason C1 exists.
|
|
13989
|
+
if (viewerMode && id === 'assistant') return false;
|
|
13724
13990
|
return id === 'assistant' ? isNativeApp() : id === 'layers' ? layersMode === 'separate' : true;
|
|
13725
13991
|
};
|
|
13726
13992
|
const idsForSide = (side) =>
|
|
@@ -13753,12 +14019,15 @@ function App() {
|
|
|
13753
14019
|
// which owns the resizable width). Assistant is handled separately below as an
|
|
13754
14020
|
// always-mounted ChatPanel so its stream survives a tab switch.
|
|
13755
14021
|
const renderPanelBody = (id) => {
|
|
13756
|
-
//
|
|
13757
|
-
// viewer
|
|
13758
|
-
|
|
14022
|
+
// C1 again, the second half of the same gate. The panels MOUNT for a
|
|
14023
|
+
// viewer; what they must not do is offer an edit, and that is already
|
|
14024
|
+
// handled inside them (`readOnly` is threaded through every control).
|
|
14025
|
+
// Refusing to mount was a blunter instrument than the role model asks for.
|
|
14026
|
+
|
|
13759
14027
|
if (id === 'tree')
|
|
13760
14028
|
return (
|
|
13761
14029
|
<Sidebar
|
|
14030
|
+
cloud={cfg.cloud}
|
|
13762
14031
|
readOnly={viewerMode}
|
|
13763
14032
|
groups={groups}
|
|
13764
14033
|
activePath={activePath}
|
|
@@ -13813,6 +14082,9 @@ function App() {
|
|
|
13813
14082
|
if (id === 'inspector' || id === 'layers')
|
|
13814
14083
|
return (
|
|
13815
14084
|
<InspectorPanel
|
|
14085
|
+
// Cloud Phase 27 E4 — the parity spec asserts these two panels exist
|
|
14086
|
+
// in BOTH shells, so each needs a name a spec can ask for.
|
|
14087
|
+
testId={id === 'layers' ? 'layers-panel' : 'inspector-panel'}
|
|
13816
14088
|
layersOnly={id === 'layers'}
|
|
13817
14089
|
hideLayersTab={layersMode === 'separate'}
|
|
13818
14090
|
cpMode={cpMode}
|
|
@@ -13893,6 +14165,7 @@ function App() {
|
|
|
13893
14165
|
onContextMenu={onShellContextMenu}
|
|
13894
14166
|
>
|
|
13895
14167
|
{firstRun && <OnboardingWizard />}
|
|
14168
|
+
<CloudRoleBanner cloud={cfg.cloud} />
|
|
13896
14169
|
<UpdateBanner update={updateReady} onDismiss={() => setUpdateReady(null)} />
|
|
13897
14170
|
<SyncBanner status={syncStatus} />
|
|
13898
14171
|
{!usageNudge && !tourSteps && <WhatsNewToast wn={whatsNew} />}
|
|
@@ -13920,6 +14193,10 @@ function App() {
|
|
|
13920
14193
|
<div className="st-shell">
|
|
13921
14194
|
<Menubar
|
|
13922
14195
|
readOnly={viewerMode}
|
|
14196
|
+
// Cloud Phase 27 C2/C4 — the ONE cloud-only input the shared chrome
|
|
14197
|
+
// takes. A prop, not the whole `cfg`: the Menubar needs to know it is
|
|
14198
|
+
// in a browser tab on somebody else's machine, and nothing else.
|
|
14199
|
+
cloud={cfg.cloud}
|
|
13923
14200
|
activePath={activePath}
|
|
13924
14201
|
project={project}
|
|
13925
14202
|
tabsCount={tabs.length}
|
|
@@ -14919,6 +15196,7 @@ function App() {
|
|
|
14919
15196
|
)}
|
|
14920
15197
|
{settingsOpen && (
|
|
14921
15198
|
<SettingsPanel
|
|
15199
|
+
cloud={cfg.cloud}
|
|
14922
15200
|
onClose={() => setSettingsOpen(false)}
|
|
14923
15201
|
initialTab={typeof settingsOpen === 'string' ? settingsOpen : undefined}
|
|
14924
15202
|
theme={theme}
|
|
@@ -14994,7 +15272,16 @@ function App() {
|
|
|
14994
15272
|
startTour(USAGE_TOUR);
|
|
14995
15273
|
}}
|
|
14996
15274
|
/>
|
|
14997
|
-
|
|
15275
|
+
{/* `activeCanvas` is THIS window's truth. The server's `_active.json` is
|
|
15276
|
+
global and sticky — it survives closing every tab and is written by any
|
|
15277
|
+
other session (an agent driving a headless browser, a second window),
|
|
15278
|
+
so a report built from it can claim a canvas the user does not have
|
|
15279
|
+
open, and capture it. Pass what this client actually shows. */}
|
|
15280
|
+
<ReportBugDialog
|
|
15281
|
+
open={reportBugOpen}
|
|
15282
|
+
activeCanvas={activePath}
|
|
15283
|
+
onClose={() => setReportBugOpen(false)}
|
|
15284
|
+
/>
|
|
14998
15285
|
<WhatsNewPanel wn={whatsNew} onStartTour={startTour} />
|
|
14999
15286
|
<ExportPanel center={exportCenter} />
|
|
15000
15287
|
<ReadinessDialog open={readinessOpen} onClose={() => setReadinessOpen(false)} />
|
|
@@ -41,6 +41,13 @@ export function canvasUrl(p, cfg, opts) {
|
|
|
41
41
|
// reads this flag). Boot-static on purpose — a role is per-session, and a
|
|
42
42
|
// query param can't race the way a post-load message could.
|
|
43
43
|
if (cfg?.readOnly) params.set('ro', '1');
|
|
44
|
+
// Cloud Phase 27 (DDR-209) — the canvas origin's capability. On a desktop the
|
|
45
|
+
// canvas origin is loopback and needs none, so this is absent and the URL is
|
|
46
|
+
// byte-identical to before. In the cloud it is a cookieless, cross-site
|
|
47
|
+
// origin: a cookie scoped widely enough to cover it would be readable by the
|
|
48
|
+
// untrusted canvas content itself, which is the one thing the DDR-054 split
|
|
49
|
+
// exists to prevent — so the capability lives in the URL instead.
|
|
50
|
+
if (cfg?.canvasToken) params.set('t', cfg.canvasToken);
|
|
44
51
|
const ds0 = cfg?.designSystems?.[0];
|
|
45
52
|
// Specimen detection: anything under `system/<ds>/preview/` belongs to that
|
|
46
53
|
// specific DS, so it must render with *that* DS's tokens — not always the
|
|
@@ -65,7 +65,14 @@ async function saveNative(id, filename) {
|
|
|
65
65
|
return saveExport(filename || 'export', id);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
/**
|
|
69
|
+
* @param {{ enabled?: boolean }} [opts] — `enabled` false suppresses the
|
|
70
|
+
* hydration fetch. The cloud shell passes false: `/_api/export-jobs` is
|
|
71
|
+
* refused there by design, so asking is guaranteed noise. It stays UNDEFINED
|
|
72
|
+
* until `/_config` answers, and undefined is not "false" here — see the
|
|
73
|
+
* tri-state note on `cfg.cloud`.
|
|
74
|
+
*/
|
|
75
|
+
export function useExportCenter({ enabled = true } = {}) {
|
|
69
76
|
const [jobs, setJobs] = useState(() => new Map());
|
|
70
77
|
const [panelOpen, setPanelOpen] = useState(false);
|
|
71
78
|
const [toastQueue, setToastQueue] = useState([]);
|
|
@@ -77,6 +84,7 @@ export function useExportCenter() {
|
|
|
77
84
|
const startToastShownRef = useRef(new Set()); // "Exporting…" toast fires at most once per job
|
|
78
85
|
|
|
79
86
|
useEffect(() => {
|
|
87
|
+
if (!enabled) return undefined;
|
|
80
88
|
let alive = true;
|
|
81
89
|
fetch('/_api/export-jobs')
|
|
82
90
|
.then((r) => r.json())
|
|
@@ -99,7 +107,7 @@ export function useExportCenter() {
|
|
|
99
107
|
return () => {
|
|
100
108
|
alive = false;
|
|
101
109
|
};
|
|
102
|
-
}, []);
|
|
110
|
+
}, [enabled]);
|
|
103
111
|
|
|
104
112
|
const upsert = useCallback((job) => {
|
|
105
113
|
if (!job || typeof job.id !== 'string') return;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// (st-scrim / st-dialog). Icons are local Lucide-line paths (IdentityBar
|
|
12
12
|
// precedent); colors are theme tokens only (no hardcoded hex).
|
|
13
13
|
|
|
14
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
14
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
15
15
|
|
|
16
16
|
function Icon({ name, size = 16 }) {
|
|
17
17
|
const p = {
|
|
@@ -588,9 +588,21 @@ const TABS = [
|
|
|
588
588
|
{ id: 'appearance', label: 'Appearance' },
|
|
589
589
|
{ id: 'canvas-view', label: 'Canvas & View' },
|
|
590
590
|
{ id: 'layout', label: 'Layout' },
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
591
|
+
// Cloud Phase 27 — `local: true` means "this tab configures something that
|
|
592
|
+
// runs on the machine Maude is installed on". A browser tab has no ffmpeg, no
|
|
593
|
+
// mlx-vlm, no HuggingFace cache and no provider key of yours, and the routes
|
|
594
|
+
// behind these panes are refused in a cell by design (DDR-209 D1) — so in the
|
|
595
|
+
// cloud they rendered as a wall of `HTTP 404`.
|
|
596
|
+
//
|
|
597
|
+
// ABSENT, not disabled. C2 says state an absence where the thing would be,
|
|
598
|
+
// and that rule earns its keep for the agent chat: the feature exists, you
|
|
599
|
+
// just need the desktop app. These are settings for machinery that is not
|
|
600
|
+
// missing so much as inapplicable — a preference pane for hardware this shell
|
|
601
|
+
// does not have. There is nothing for the reader to act on, so a whole tab of
|
|
602
|
+
// dead controls is worse than no tab.
|
|
603
|
+
{ id: 'ai-generation', label: 'AI generation', local: true },
|
|
604
|
+
{ id: 'subtitles', label: 'Subtitles', local: true },
|
|
605
|
+
{ id: 'video', label: 'Video', local: true },
|
|
594
606
|
];
|
|
595
607
|
const SETTINGS_TAB_STORE = 'mdcc-settings-tab';
|
|
596
608
|
|
|
@@ -789,6 +801,13 @@ function AppearanceTab({ theme, onSetTheme, cpMode, onSetCpMode }) {
|
|
|
789
801
|
}
|
|
790
802
|
|
|
791
803
|
export default function SettingsPanel({
|
|
804
|
+
/** `{ dashboardUrl, projectName }` when this is a cloud tab, else null. */
|
|
805
|
+
// Tri-state (see the note where this value is created): `undefined` until
|
|
806
|
+
// the server config answers, `null` for the desktop, an object for a cloud
|
|
807
|
+
// tab. Defaulting it to `null` here is what re-broke the boot 404 after the
|
|
808
|
+
// call sites were fixed — a default fires on `undefined`, so "not known yet"
|
|
809
|
+
// became "not cloud" one layer further in, invisibly.
|
|
810
|
+
cloud,
|
|
792
811
|
onClose,
|
|
793
812
|
initialTab,
|
|
794
813
|
theme,
|
|
@@ -811,11 +830,17 @@ export default function SettingsPanel({
|
|
|
811
830
|
}) {
|
|
812
831
|
const [providers, setProviders] = useState(null); // null = loading
|
|
813
832
|
const [error, setError] = useState(null);
|
|
833
|
+
// The tabs THIS shell can honour. See TABS for why the local-machine ones are
|
|
834
|
+
// absent in a browser tab rather than disabled.
|
|
835
|
+
const tabs = useMemo(() => TABS.filter((t) => !(cloud && t.local)), [cloud]);
|
|
814
836
|
const [tab, setTab] = useState(() => {
|
|
815
|
-
|
|
837
|
+
// A remembered tab that this shell does not have (you opened Video on the
|
|
838
|
+
// desktop, then opened Settings in the cloud) must not select nothing.
|
|
839
|
+
const has = (id) => TABS.some((t) => t.id === id && !(cloud && t.local));
|
|
840
|
+
if (initialTab && has(initialTab)) return initialTab;
|
|
816
841
|
try {
|
|
817
842
|
const v = localStorage.getItem(SETTINGS_TAB_STORE);
|
|
818
|
-
if (v &&
|
|
843
|
+
if (v && has(v)) return v;
|
|
819
844
|
} catch {}
|
|
820
845
|
return 'appearance';
|
|
821
846
|
});
|
|
@@ -853,19 +878,19 @@ export default function SettingsPanel({
|
|
|
853
878
|
|
|
854
879
|
// Vertical roving focus over the tab rail (native-menu parity).
|
|
855
880
|
function onRailKey(e) {
|
|
856
|
-
const idx =
|
|
881
|
+
const idx = tabs.findIndex((t) => t.id === tab);
|
|
857
882
|
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
|
858
883
|
e.preventDefault();
|
|
859
|
-
setTab(
|
|
884
|
+
setTab(tabs[(idx + 1) % tabs.length].id);
|
|
860
885
|
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
|
|
861
886
|
e.preventDefault();
|
|
862
|
-
setTab(
|
|
887
|
+
setTab(tabs[(idx - 1 + tabs.length) % tabs.length].id);
|
|
863
888
|
} else if (e.key === 'Home') {
|
|
864
889
|
e.preventDefault();
|
|
865
|
-
setTab(
|
|
890
|
+
setTab(tabs[0].id);
|
|
866
891
|
} else if (e.key === 'End') {
|
|
867
892
|
e.preventDefault();
|
|
868
|
-
setTab(
|
|
893
|
+
setTab(tabs[tabs.length - 1].id);
|
|
869
894
|
}
|
|
870
895
|
}
|
|
871
896
|
|
|
@@ -896,7 +921,7 @@ export default function SettingsPanel({
|
|
|
896
921
|
aria-label="Settings categories"
|
|
897
922
|
onKeyDown={onRailKey}
|
|
898
923
|
>
|
|
899
|
-
{
|
|
924
|
+
{tabs.map((t) => (
|
|
900
925
|
<button
|
|
901
926
|
key={t.id}
|
|
902
927
|
type="button"
|