@1agh/maude 0.53.2 → 0.55.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/annotations-layer.tsx +11 -1
- package/apps/studio/api.ts +13 -7
- package/apps/studio/bin/screenshot.sh +43 -3
- package/apps/studio/build.ts +37 -3
- package/apps/studio/canvas-build-sandbox.ts +101 -9
- package/apps/studio/canvas-build-worker.ts +7 -1
- package/apps/studio/canvas-build.ts +9 -1
- package/apps/studio/client/app.jsx +226 -25
- package/apps/studio/client/export-center.jsx +10 -2
- package/apps/studio/client/github.js +7 -0
- package/apps/studio/client/panels/CloudBar.jsx +461 -40
- package/apps/studio/client/panels/SettingsPanel.jsx +6 -1
- package/apps/studio/client/styles/3-shell-maude.css +30 -0
- package/apps/studio/client/styles/4-components.css +40 -0
- package/apps/studio/cloud/endpoints.ts +48 -6
- 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 +983 -983
- 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 +159 -13
- package/apps/studio/inspect.ts +66 -6
- package/apps/studio/server.ts +114 -17
- 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-origin-gate.test.ts +61 -0
- package/apps/studio/test/canvas-shell-base.test.ts +107 -0
- package/apps/studio/test/cloud-build.test.ts +95 -0
- package/apps/studio/test/cloud-endpoints.test.ts +231 -4
- package/apps/studio/test/cloud-shell-surfaces.test.ts +31 -2
- package/apps/studio/test/collab-readonly-gate.test.ts +194 -0
- package/apps/studio/test/git-system-engine.test.ts +135 -0
- package/apps/studio/test/read-only-gate.test.ts +8 -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/use-collab.tsx +21 -1
- package/apps/studio/whats-new.json +54 -0
- package/apps/studio/ws.ts +44 -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
|
@@ -628,6 +628,14 @@ const STICONS = {
|
|
|
628
628
|
<path d="M4.3 9.5v2.3a1.2 1.2 0 0 0 2.4 0v-1.7" />
|
|
629
629
|
</>
|
|
630
630
|
),
|
|
631
|
+
bug: (
|
|
632
|
+
<>
|
|
633
|
+
<circle cx="8" cy="9" r="3.6" />
|
|
634
|
+
<circle cx="8" cy="4.4" r="1.3" />
|
|
635
|
+
<path d="M6.2 3.4 5.2 2M9.8 3.4l1-1.4" />
|
|
636
|
+
<path d="M4.6 7.6H2.2M4.6 9H2.2M4.6 10.6H2.5M11.4 7.6h2.4M11.4 9h2.4M11.4 10.6h2.3" />
|
|
637
|
+
</>
|
|
638
|
+
),
|
|
631
639
|
'panel-left': (
|
|
632
640
|
<>
|
|
633
641
|
<rect x="2.5" y="3" width="11" height="10" rx="1.5" />
|
|
@@ -2430,7 +2438,12 @@ function Sidebar({
|
|
|
2430
2438
|
// affordances are absent (buttons, composer, row menus, drag & drop).
|
|
2431
2439
|
readOnly = false,
|
|
2432
2440
|
/** `{ dashboardUrl, projectName }` when this is a cloud tab, else null. */
|
|
2433
|
-
|
|
2441
|
+
// Tri-state (see the note where this value is created): `undefined` until
|
|
2442
|
+
// the server config answers, `null` for the desktop, an object for a cloud
|
|
2443
|
+
// tab. Defaulting it to `null` here is what re-broke the boot 404 after the
|
|
2444
|
+
// call sites were fixed — a default fires on `undefined`, so "not known yet"
|
|
2445
|
+
// became "not cloud" one layer further in, invisibly.
|
|
2446
|
+
cloud,
|
|
2434
2447
|
groups,
|
|
2435
2448
|
activePath,
|
|
2436
2449
|
activeDsName,
|
|
@@ -2709,6 +2722,7 @@ function Sidebar({
|
|
|
2709
2722
|
<StIcon name="search" size={13} />
|
|
2710
2723
|
<input
|
|
2711
2724
|
type="search"
|
|
2725
|
+
data-testid="canvas-search"
|
|
2712
2726
|
placeholder="Search canvases…"
|
|
2713
2727
|
value={search}
|
|
2714
2728
|
onChange={(e) => setSearch(e.target.value)}
|
|
@@ -2832,7 +2846,7 @@ function Sidebar({
|
|
|
2832
2846
|
Absent rather than disabled, because unlike the agent chat there is
|
|
2833
2847
|
nothing here to explain — the capability is not missing, it is
|
|
2834
2848
|
already satisfied. */}
|
|
2835
|
-
{cloud
|
|
2849
|
+
{cloud === null ? <CloudBar /> : null}
|
|
2836
2850
|
{/* Phase 28 (E3) — GitHub identity as a compact avatar docked at the BOTTOM:
|
|
2837
2851
|
sign in, connected account + New/Pull/Share, sign out. Self-contained
|
|
2838
2852
|
(owns its device-code + CreateProject dialogs). Renders nothing in browser. */}
|
|
@@ -3622,7 +3636,12 @@ function Menubar({
|
|
|
3622
3636
|
activePath,
|
|
3623
3637
|
project,
|
|
3624
3638
|
/** `{ dashboardUrl, projectName }` when this is a cloud tab, else null. */
|
|
3625
|
-
|
|
3639
|
+
// Tri-state (see the note where this value is created): `undefined` until
|
|
3640
|
+
// the server config answers, `null` for the desktop, an object for a cloud
|
|
3641
|
+
// tab. Defaulting it to `null` here is what re-broke the boot 404 after the
|
|
3642
|
+
// call sites were fixed — a default fires on `undefined`, so "not known yet"
|
|
3643
|
+
// became "not cloud" one layer further in, invisibly.
|
|
3644
|
+
cloud,
|
|
3626
3645
|
tabsCount,
|
|
3627
3646
|
openMenu,
|
|
3628
3647
|
setOpenMenu,
|
|
@@ -3890,7 +3909,12 @@ function Menubar({
|
|
|
3890
3909
|
}, [openMenu, setOpenMenu]);
|
|
3891
3910
|
|
|
3892
3911
|
return (
|
|
3893
|
-
<header
|
|
3912
|
+
<header
|
|
3913
|
+
className="st-menubar"
|
|
3914
|
+
role="menubar"
|
|
3915
|
+
aria-label="Application menubar"
|
|
3916
|
+
data-testid="menubar"
|
|
3917
|
+
>
|
|
3894
3918
|
<span className="st-brand" data-tour="brand">
|
|
3895
3919
|
<span className="st-brand-mark">
|
|
3896
3920
|
<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>
|
|
@@ -4044,11 +4068,45 @@ function Menubar({
|
|
|
4044
4068
|
<span
|
|
4045
4069
|
className="st-stamp st-stamp--viewonly"
|
|
4046
4070
|
data-testid="view-only-stamp"
|
|
4047
|
-
data-tip=
|
|
4071
|
+
data-tip={
|
|
4072
|
+
cloud?.user
|
|
4073
|
+
? `${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.`
|
|
4074
|
+
: 'Your role in this project is viewer — you can browse, comment when available, and export. Ask a project owner for edit access.'
|
|
4075
|
+
}
|
|
4048
4076
|
>
|
|
4049
4077
|
VIEW ONLY
|
|
4050
4078
|
</span>
|
|
4051
4079
|
)}
|
|
4080
|
+
{/* Cloud Phase 27 C4 — WHO YOU ARE, AND HOW TO STOP BEING THEM.
|
|
4081
|
+
A browser tab carries no account identity of its own: the cookie is
|
|
4082
|
+
invisible, and the only thing on screen that reflected it was a
|
|
4083
|
+
three-word stamp. An owner who read VIEW ONLY had no way to tell
|
|
4084
|
+
whether the role was wrong or the ACCOUNT was, and no way to change
|
|
4085
|
+
either — the session outlived any fix by up to twelve hours because
|
|
4086
|
+
nothing in the studio could end it. This ends it. */}
|
|
4087
|
+
{cloud?.user ? (
|
|
4088
|
+
<span className="st-cloudwho" data-testid="cloud-account">
|
|
4089
|
+
<span
|
|
4090
|
+
className="st-cloudwho-email"
|
|
4091
|
+
title={cloud.role ? `Signed in as ${cloud.user} — ${cloud.role}` : cloud.user}
|
|
4092
|
+
>
|
|
4093
|
+
{cloud.user}
|
|
4094
|
+
</span>
|
|
4095
|
+
{/* A FORM, not a link. Signing out revokes the token server-side,
|
|
4096
|
+
so it is a write — and a write behind a GET is one any page can
|
|
4097
|
+
force on you from across the internet. */}
|
|
4098
|
+
<form method="post" action="/auth/browser/signout" className="st-cloudwho-form">
|
|
4099
|
+
<button
|
|
4100
|
+
type="submit"
|
|
4101
|
+
className="st-cloudwho-out"
|
|
4102
|
+
data-testid="cloud-signout"
|
|
4103
|
+
title="Sign out of this project"
|
|
4104
|
+
>
|
|
4105
|
+
Sign out
|
|
4106
|
+
</button>
|
|
4107
|
+
</form>
|
|
4108
|
+
</span>
|
|
4109
|
+
) : null}
|
|
4052
4110
|
{isNativeApp() && !readOnly && (
|
|
4053
4111
|
<button
|
|
4054
4112
|
type="button"
|
|
@@ -4065,6 +4123,16 @@ function Menubar({
|
|
|
4065
4123
|
</button>
|
|
4066
4124
|
)}
|
|
4067
4125
|
{exportCenter && <ExportBadge center={exportCenter} />}
|
|
4126
|
+
<button
|
|
4127
|
+
type="button"
|
|
4128
|
+
className="st-reportbug"
|
|
4129
|
+
data-testid="report-bug-toggle"
|
|
4130
|
+
aria-label="Report a bug"
|
|
4131
|
+
data-tip="Report a bug"
|
|
4132
|
+
onClick={onReportBug}
|
|
4133
|
+
>
|
|
4134
|
+
<StIcon name="bug" size={15} />
|
|
4135
|
+
</button>
|
|
4068
4136
|
<button
|
|
4069
4137
|
type="button"
|
|
4070
4138
|
className="st-whatsnew"
|
|
@@ -4514,7 +4582,7 @@ function StatusBar({
|
|
|
4514
4582
|
})();
|
|
4515
4583
|
|
|
4516
4584
|
return (
|
|
4517
|
-
<footer className="st-statusbar" role="contentinfo">
|
|
4585
|
+
<footer className="st-statusbar" role="contentinfo" data-testid="statusbar">
|
|
4518
4586
|
<span className="st-sb-slot st-sb-active" role="group" aria-label="Active file">
|
|
4519
4587
|
<span className="lead" aria-hidden="true" />
|
|
4520
4588
|
<span className="lbl">active</span>
|
|
@@ -4792,6 +4860,69 @@ function CommentsPanel({
|
|
|
4792
4860
|
// Phase 32 (Task 1) — auto-update notice. Shown after the shell has downloaded +
|
|
4793
4861
|
// staged a newer build in the background. Non-blocking: "Restart now" applies it,
|
|
4794
4862
|
// "Later" dismisses (the next focus/4h check re-stages and re-surfaces it).
|
|
4863
|
+
/**
|
|
4864
|
+
* What this role can do, said once, where a teammate lands — Cloud Phase 27 C3.
|
|
4865
|
+
*
|
|
4866
|
+
* A browser tab arrives with none of the context a desktop has: no window
|
|
4867
|
+
* title, no app the person chose to install, and — for a viewer — a set of
|
|
4868
|
+
* controls that will refuse them without explaining why. One line, dismissible,
|
|
4869
|
+
* remembered per role. It re-appears if the role CHANGES, because "you can edit
|
|
4870
|
+
* this now" is worth saying exactly as much as the first sentence was.
|
|
4871
|
+
*
|
|
4872
|
+
* Copy comes from the role the cell vouched (`role-matrix.mjs` is the authority
|
|
4873
|
+
* on what each one means; this is its sentence, not a second opinion).
|
|
4874
|
+
*/
|
|
4875
|
+
const ROLE_LINE = {
|
|
4876
|
+
owner: 'You own this project — edit, invite and export.',
|
|
4877
|
+
member: 'You can edit this project, comment and export.',
|
|
4878
|
+
viewer: 'You can look at this project, comment and download it, but not change it.',
|
|
4879
|
+
};
|
|
4880
|
+
|
|
4881
|
+
function CloudRoleBanner({ cloud }) {
|
|
4882
|
+
const role = cloud?.role || null;
|
|
4883
|
+
// Dismissal is READ AT RENDER, not seeded into state. `cfg.cloud` is
|
|
4884
|
+
// `undefined` until `/_config` lands, so a `useState` initializer runs while
|
|
4885
|
+
// there is no role yet — and whatever it decided then would stick forever,
|
|
4886
|
+
// which on the first attempt meant the banner never appeared at all. Found by
|
|
4887
|
+
// opening it in a browser; no test in this repo would have said a word.
|
|
4888
|
+
const [dismissedNow, setDismissedNow] = useState(false);
|
|
4889
|
+
if (!role || dismissedNow) return null;
|
|
4890
|
+
const storageKey = `maude-cloud-role-seen:${role}`;
|
|
4891
|
+
let alreadySeen = false;
|
|
4892
|
+
try {
|
|
4893
|
+
alreadySeen = localStorage.getItem(storageKey) === '1';
|
|
4894
|
+
} catch {
|
|
4895
|
+
/* private mode / storage disabled — show it, saying it twice beats never */
|
|
4896
|
+
}
|
|
4897
|
+
if (alreadySeen) return null;
|
|
4898
|
+
const line = ROLE_LINE[role];
|
|
4899
|
+
if (!line) return null;
|
|
4900
|
+
return (
|
|
4901
|
+
<div
|
|
4902
|
+
role="status"
|
|
4903
|
+
aria-live="polite"
|
|
4904
|
+
className="st-banner st-banner--info"
|
|
4905
|
+
data-testid="cloud-role-banner"
|
|
4906
|
+
>
|
|
4907
|
+
<span className="st-banner-dot" aria-hidden="true" />
|
|
4908
|
+
<span>{line}</span>
|
|
4909
|
+
<button
|
|
4910
|
+
type="button"
|
|
4911
|
+
className="btn btn--ghost btn--sm"
|
|
4912
|
+
data-testid="cloud-role-banner-dismiss"
|
|
4913
|
+
onClick={() => {
|
|
4914
|
+
setDismissedNow(true);
|
|
4915
|
+
try {
|
|
4916
|
+
localStorage.setItem(storageKey, '1');
|
|
4917
|
+
} catch {}
|
|
4918
|
+
}}
|
|
4919
|
+
>
|
|
4920
|
+
Got it
|
|
4921
|
+
</button>
|
|
4922
|
+
</div>
|
|
4923
|
+
);
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4795
4926
|
function UpdateBanner({ update, onDismiss }) {
|
|
4796
4927
|
const [restarting, setRestarting] = useState(false);
|
|
4797
4928
|
if (!update) return null;
|
|
@@ -8304,6 +8435,9 @@ function photoAssetOfSelection(el) {
|
|
|
8304
8435
|
}
|
|
8305
8436
|
|
|
8306
8437
|
function InspectorPanel({
|
|
8438
|
+
/** E4 — `inspector-panel` / `layers-panel`, so one parity spec can assert
|
|
8439
|
+
* both shells show them. Undefined elsewhere; the attribute simply absent. */
|
|
8440
|
+
testId,
|
|
8307
8441
|
selected,
|
|
8308
8442
|
onClose,
|
|
8309
8443
|
layersTree,
|
|
@@ -8773,6 +8907,7 @@ function InspectorPanel({
|
|
|
8773
8907
|
style={width ? { width, flexBasis: width } : undefined}
|
|
8774
8908
|
aria-label="Inspector"
|
|
8775
8909
|
data-tour="inspector"
|
|
8910
|
+
data-testid={testId}
|
|
8776
8911
|
>
|
|
8777
8912
|
<div
|
|
8778
8913
|
className="st-rp-tabs"
|
|
@@ -9236,7 +9371,18 @@ function App() {
|
|
|
9236
9371
|
// `config-updated` push (config.json hot-reload — /design:setup-ds rewrites
|
|
9237
9372
|
// it mid-session) — informs canvasUrl() so TSX iframes can pass the right
|
|
9238
9373
|
// ?designRel + ?tokens query to the canvas mount shell.
|
|
9239
|
-
|
|
9374
|
+
// `cloud` is deliberately TRI-STATE, and the third state is the useful one:
|
|
9375
|
+
// undefined — `/_config` has not answered yet; which shell this is is
|
|
9376
|
+
// UNKNOWN, and anything that would behave differently in the
|
|
9377
|
+
// two shells must wait rather than assume the desktop.
|
|
9378
|
+
// null — desktop or plain local browser.
|
|
9379
|
+
// object — a cloud tab (`{ dashboardUrl, projectName, user, role }`).
|
|
9380
|
+
//
|
|
9381
|
+
// Collapsing unknown into "not cloud" is what made the cloud studio open with
|
|
9382
|
+
// two console 404s every time: the sign-in bar and the export centre mounted
|
|
9383
|
+
// for one frame, each fired the request its shell exists to refuse, and then
|
|
9384
|
+
// unmounted. Nothing was broken and everything looked broken.
|
|
9385
|
+
const [cfg, setCfg] = useState({ designRel: '.design', cloud: undefined });
|
|
9240
9386
|
// Cloud Phase 25 C2 — viewer role, known at boot from /_config. Every
|
|
9241
9387
|
// editing affordance in the shell gates on this (absent, not hidden).
|
|
9242
9388
|
const viewerMode = !!cfg.readOnly;
|
|
@@ -9278,13 +9424,11 @@ function App() {
|
|
|
9278
9424
|
// both of these went missing and the cloud studio rendered without
|
|
9279
9425
|
// its chrome and with every canvas iframe unauthenticated.
|
|
9280
9426
|
canvasToken: data.canvasToken,
|
|
9281
|
-
// C2/C4 — `{ dashboardUrl, projectName }` when this is a
|
|
9282
|
-
// Its presence is what tells the shared client it is in a
|
|
9283
|
-
// on somebody else's machine
|
|
9427
|
+
// C2/C4 — `{ dashboardUrl, projectName, user, role }` when this is a
|
|
9428
|
+
// cloud tab. Its presence is what tells the shared client it is in a
|
|
9429
|
+
// browser tab on somebody else's machine; `user`/`role` are what let
|
|
9430
|
+
// it say WHICH account that tab is, and offer the way out.
|
|
9284
9431
|
cloud: data.cloud ?? null,
|
|
9285
|
-
// C2/C4 — `{ dashboardUrl, projectName }` when this is a cloud tab.
|
|
9286
|
-
// Its presence is what tells the shared client it is in a browser tab
|
|
9287
|
-
// on somebody else's machine.
|
|
9288
9432
|
}));
|
|
9289
9433
|
})
|
|
9290
9434
|
.catch(() => {});
|
|
@@ -9633,7 +9777,10 @@ function App() {
|
|
|
9633
9777
|
// toggles on its own and coexists with Inspector/Changes/Comments/Chat.
|
|
9634
9778
|
const toggleTimeline = useCallback(() => setTimelineOpen((v) => !v), []);
|
|
9635
9779
|
const whatsNew = useWhatsNew(MDCC_VERSION);
|
|
9636
|
-
|
|
9780
|
+
// Same tri-state rule as the sign-in bar: the export queue is a LOCAL job
|
|
9781
|
+
// runner, `/_api/export-jobs` is one of the routes a cell refuses outright
|
|
9782
|
+
// (DDR-209 D1), and hydrating before the shell is known 404s every boot.
|
|
9783
|
+
const exportCenter = useExportCenter({ enabled: cfg.cloud === null });
|
|
9637
9784
|
// Phase 29 (E4) — first-run onboarding wizard. The native shell boots a minimal
|
|
9638
9785
|
// "welcome" project on first launch; we ask it whether this is a first run and, if
|
|
9639
9786
|
// so, show the wizard OVER the (empty) canvas browser. Completing any door switches
|
|
@@ -11006,6 +11153,41 @@ function App() {
|
|
|
11006
11153
|
if (path !== SYSTEM_TAB) setLoadingPath(path);
|
|
11007
11154
|
}, []);
|
|
11008
11155
|
|
|
11156
|
+
/**
|
|
11157
|
+
* First open lands on a rendered canvas — Cloud Phase 27 C3.
|
|
11158
|
+
*
|
|
11159
|
+
* A desktop user chose this project, installed the app and knows what is in
|
|
11160
|
+
* it; the empty state is a helpful "pick a screen". A teammate following a
|
|
11161
|
+
* link has done none of that, and an empty pane with an arrow pointing at a
|
|
11162
|
+
* list is the browser saying "the thing you came for is somewhere else".
|
|
11163
|
+
*
|
|
11164
|
+
* CLOUD ONLY, and once. The desktop's empty state is deliberately unchanged —
|
|
11165
|
+
* it earns its keep there (the ⌘-hover lesson, quick setup) — and re-opening
|
|
11166
|
+
* a canvas the person deliberately closed would be a UI arguing with them.
|
|
11167
|
+
*/
|
|
11168
|
+
const autoOpened = useRef(false);
|
|
11169
|
+
useEffect(() => {
|
|
11170
|
+
if (autoOpened.current) return;
|
|
11171
|
+
if (!cfg.cloud) return; // desktop / unknown-yet
|
|
11172
|
+
if (tabs.length > 0) return;
|
|
11173
|
+
if (!groups.length) return; // tree not loaded
|
|
11174
|
+
// The first canvas that is somebody's WORK — a design-system specimen is a
|
|
11175
|
+
// rendered canvas too, and not what a teammate followed a link to see.
|
|
11176
|
+
let first = null;
|
|
11177
|
+
for (const g of groups) {
|
|
11178
|
+
for (const p of g.paths || []) {
|
|
11179
|
+
if (!CANVAS_EXT_RE.test(p)) continue;
|
|
11180
|
+
if (/(^|\/)system\//.test(p)) continue;
|
|
11181
|
+
first = p;
|
|
11182
|
+
break;
|
|
11183
|
+
}
|
|
11184
|
+
if (first) break;
|
|
11185
|
+
}
|
|
11186
|
+
if (!first) return;
|
|
11187
|
+
autoOpened.current = true;
|
|
11188
|
+
openTab(first);
|
|
11189
|
+
}, [cfg.cloud, groups, tabs.length, openTab]);
|
|
11190
|
+
|
|
11009
11191
|
const openSystem = useCallback(
|
|
11010
11192
|
(dsName) => {
|
|
11011
11193
|
// DsFolderRow passes the clicked DS name → scope the System view to it so
|
|
@@ -13475,10 +13657,17 @@ function App() {
|
|
|
13475
13657
|
}
|
|
13476
13658
|
// Cmd+Shift+I — toggle Inspector. Was bare "I", which collided with the
|
|
13477
13659
|
// canvas highlighter tool (same letter, different action by focus).
|
|
13478
|
-
//
|
|
13660
|
+
//
|
|
13661
|
+
// Cloud Phase 27 C1 — a VIEWER GETS THIS TOO, and the line above used to
|
|
13662
|
+
// say the opposite ("the panel is edit chrome"). C1 reversed that on
|
|
13663
|
+
// purpose: read-only means cannot CHANGE, not cannot SEE, and a reviewer
|
|
13664
|
+
// needs structure and measured values as much as anyone. The View menu
|
|
13665
|
+
// already offers it (`viewerHiddenPanels` no longer hides it); the
|
|
13666
|
+
// shortcut had been left behind, so the menu said yes and the keyboard
|
|
13667
|
+
// said no. Found by writing the parity spec that runs in both shells.
|
|
13479
13668
|
if (meta && e.shiftKey && (e.key === 'i' || e.key === 'I')) {
|
|
13480
13669
|
e.preventDefault();
|
|
13481
|
-
|
|
13670
|
+
toggleRightPanel('inspector');
|
|
13482
13671
|
return;
|
|
13483
13672
|
}
|
|
13484
13673
|
// Phase 31 (DDR-123) — Cmd+Shift+A opens the native ACP chat sidepanel.
|
|
@@ -13807,9 +13996,15 @@ function App() {
|
|
|
13807
13996
|
// assigned to it and which one is active (the open one). Layers is only a
|
|
13808
13997
|
// dockable panel in `separate` mode; Assistant is native-only.
|
|
13809
13998
|
const panelAvailable = (id) => {
|
|
13810
|
-
//
|
|
13811
|
-
//
|
|
13812
|
-
|
|
13999
|
+
// The AGENT is genuinely absent for a viewer (it edits; Phase 25 C2).
|
|
14000
|
+
//
|
|
14001
|
+
// Inspector and Layers are NOT — Cloud Phase 27 C1 reversed that, and this
|
|
14002
|
+
// line had been left behind: `viewerHiddenPanels` stopped hiding them in
|
|
14003
|
+
// the View menu while this still refused to give them a dock slot, so the
|
|
14004
|
+
// menu offered a panel that could never appear. Read-only means cannot
|
|
14005
|
+
// CHANGE, not cannot SEE: a reviewer needs structure and measured values,
|
|
14006
|
+
// which is the whole reason C1 exists.
|
|
14007
|
+
if (viewerMode && id === 'assistant') return false;
|
|
13813
14008
|
return id === 'assistant' ? isNativeApp() : id === 'layers' ? layersMode === 'separate' : true;
|
|
13814
14009
|
};
|
|
13815
14010
|
const idsForSide = (side) =>
|
|
@@ -13842,13 +14037,15 @@ function App() {
|
|
|
13842
14037
|
// which owns the resizable width). Assistant is handled separately below as an
|
|
13843
14038
|
// always-mounted ChatPanel so its stream survives a tab switch.
|
|
13844
14039
|
const renderPanelBody = (id) => {
|
|
13845
|
-
//
|
|
13846
|
-
// viewer
|
|
13847
|
-
|
|
14040
|
+
// C1 again, the second half of the same gate. The panels MOUNT for a
|
|
14041
|
+
// viewer; what they must not do is offer an edit, and that is already
|
|
14042
|
+
// handled inside them (`readOnly` is threaded through every control).
|
|
14043
|
+
// Refusing to mount was a blunter instrument than the role model asks for.
|
|
14044
|
+
|
|
13848
14045
|
if (id === 'tree')
|
|
13849
14046
|
return (
|
|
13850
14047
|
<Sidebar
|
|
13851
|
-
cloud={cfg
|
|
14048
|
+
cloud={cfg.cloud}
|
|
13852
14049
|
readOnly={viewerMode}
|
|
13853
14050
|
groups={groups}
|
|
13854
14051
|
activePath={activePath}
|
|
@@ -13903,6 +14100,9 @@ function App() {
|
|
|
13903
14100
|
if (id === 'inspector' || id === 'layers')
|
|
13904
14101
|
return (
|
|
13905
14102
|
<InspectorPanel
|
|
14103
|
+
// Cloud Phase 27 E4 — the parity spec asserts these two panels exist
|
|
14104
|
+
// in BOTH shells, so each needs a name a spec can ask for.
|
|
14105
|
+
testId={id === 'layers' ? 'layers-panel' : 'inspector-panel'}
|
|
13906
14106
|
layersOnly={id === 'layers'}
|
|
13907
14107
|
hideLayersTab={layersMode === 'separate'}
|
|
13908
14108
|
cpMode={cpMode}
|
|
@@ -13983,6 +14183,7 @@ function App() {
|
|
|
13983
14183
|
onContextMenu={onShellContextMenu}
|
|
13984
14184
|
>
|
|
13985
14185
|
{firstRun && <OnboardingWizard />}
|
|
14186
|
+
<CloudRoleBanner cloud={cfg.cloud} />
|
|
13986
14187
|
<UpdateBanner update={updateReady} onDismiss={() => setUpdateReady(null)} />
|
|
13987
14188
|
<SyncBanner status={syncStatus} />
|
|
13988
14189
|
{!usageNudge && !tourSteps && <WhatsNewToast wn={whatsNew} />}
|
|
@@ -14013,7 +14214,7 @@ function App() {
|
|
|
14013
14214
|
// Cloud Phase 27 C2/C4 — the ONE cloud-only input the shared chrome
|
|
14014
14215
|
// takes. A prop, not the whole `cfg`: the Menubar needs to know it is
|
|
14015
14216
|
// in a browser tab on somebody else's machine, and nothing else.
|
|
14016
|
-
cloud={cfg
|
|
14217
|
+
cloud={cfg.cloud}
|
|
14017
14218
|
activePath={activePath}
|
|
14018
14219
|
project={project}
|
|
14019
14220
|
tabsCount={tabs.length}
|
|
@@ -15013,7 +15214,7 @@ function App() {
|
|
|
15013
15214
|
)}
|
|
15014
15215
|
{settingsOpen && (
|
|
15015
15216
|
<SettingsPanel
|
|
15016
|
-
cloud={cfg
|
|
15217
|
+
cloud={cfg.cloud}
|
|
15017
15218
|
onClose={() => setSettingsOpen(false)}
|
|
15018
15219
|
initialTab={typeof settingsOpen === 'string' ? settingsOpen : undefined}
|
|
15019
15220
|
theme={theme}
|
|
@@ -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;
|
|
@@ -39,6 +39,13 @@ export const openVerification = () =>
|
|
|
39
39
|
* Rust side host-locks it to github.com. Throws on an older desktop build without
|
|
40
40
|
* the command — callers fall back to copy-to-clipboard. */
|
|
41
41
|
export const openGitHubUrl = (url) => invoke('open_github_url', { url });
|
|
42
|
+
/** Open a Maude Cloud URL (device activation, share view, dashboard) in the OS
|
|
43
|
+
* browser. Native only — `window.open` is a silent no-op in WKWebView, which is
|
|
44
|
+
* what stranded people mid-sign-in. The Rust side zone-locks it against the
|
|
45
|
+
* address it resolves per call, so this argument can't widen where it goes.
|
|
46
|
+
* Throws on an older desktop build without the command — every caller must keep
|
|
47
|
+
* a visible link the person can use by hand. */
|
|
48
|
+
export const openCloudUrl = (url) => invoke('open_cloud_url', { url });
|
|
42
49
|
/** Show the device code as soon as the shell has it. Returns an unlisten promise. */
|
|
43
50
|
export const onDeviceCode = (cb) => listen('github://device-code', cb);
|
|
44
51
|
/** Fire when sign-in completes (any surface) so other surfaces can flip live. cb(login). */
|