@1agh/maude 0.53.0 → 0.53.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/studio/bin/_import-asset.mjs +1 -6
- package/apps/studio/bin/_import-brand.mjs +1 -1
- package/apps/studio/bin/_video-playwright.mjs +5 -1
- package/apps/studio/bin/screenshot.sh +131 -15
- package/apps/studio/canvas-build-sandbox.ts +385 -0
- package/apps/studio/canvas-build-worker.ts +85 -0
- package/apps/studio/client/app.jsx +112 -8
- package/apps/studio/client/canvas-url.js +7 -0
- package/apps/studio/client/panels/SettingsPanel.jsx +32 -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 +35 -0
- package/apps/studio/dist/client.bundle.js +831 -831
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/http.ts +252 -23
- package/apps/studio/paths.ts +8 -0
- package/apps/studio/server.ts +102 -36
- package/apps/studio/test/canvas-build-sandbox.test.ts +69 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +5 -0
- package/apps/studio/test/canvas-url.test.ts +26 -0
- package/apps/studio/test/cloud-session-role.test.ts +131 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +90 -0
- package/apps/studio/test/config-projection.test.ts +104 -0
- package/apps/studio/test/read-only-gate.test.ts +50 -0
- package/apps/studio/test/report-proxy.test.ts +125 -0
- package/apps/studio/test/workspace-containment.test.ts +90 -11
- package/apps/studio/whats-new.json +11 -2
- package/apps/studio/workspace-mode.ts +161 -28
- package/apps/studio/ws.ts +30 -0
- package/package.json +8 -8
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The cell's canvas build, in its own process — Cloud Phase 25 A1, moved to the
|
|
2
|
+
// studio by Cloud Phase 27 / DDR-209 A′2.
|
|
3
|
+
//
|
|
4
|
+
// WHY A SEPARATE PROCESS, AND WHY BUN.
|
|
5
|
+
//
|
|
6
|
+
// Phase 25 A0 decided the cell BUILDS a tenant's canvas and the member's browser
|
|
7
|
+
// EVALUATES it. "Build is not evaluation" is only true if the build cannot be
|
|
8
|
+
// made to do anything else, so the build runs where it can be bounded:
|
|
9
|
+
//
|
|
10
|
+
// - its own process, spawned with an EMPTY environment — the derived cell
|
|
11
|
+
// secret, the project token key and the tenant's storage credentials are
|
|
12
|
+
// all env vars in the server, and none of them exist here;
|
|
13
|
+
// - a wall-clock deadline and an RSS ceiling enforced by the parent, so a
|
|
14
|
+
// pathological import graph costs one killed process, not the cell;
|
|
15
|
+
// - an import allowlist (canvas-build.ts `restrictImportsTo`), so a tenant's
|
|
16
|
+
// source can reach the runtime packages, `@maude/canvas-lib`, and its own
|
|
17
|
+
// files — and nothing else on this disk.
|
|
18
|
+
//
|
|
19
|
+
// WHY IT MOVED. It used to live at `apps/hub/src/canvas/build-worker.ts` and
|
|
20
|
+
// import this engine across the repo. DDR-209 A′2 deletes the hub's canvas
|
|
21
|
+
// implementation and runs the REAL studio in the cell, so the host has to live
|
|
22
|
+
// where the route it protects lives. Nothing about the contract changed — the
|
|
23
|
+
// empty env, the allowlist and the ceilings are the same ones, and
|
|
24
|
+
// `scripts/check-containment.sh` still asserts every one of them.
|
|
25
|
+
//
|
|
26
|
+
// It runs under BUN rather than the hub's Node so the output is the SAME
|
|
27
|
+
// artifact the desktop produces: same `Bun.hash`-derived `data-cd-id`s (so a
|
|
28
|
+
// comment anchored in the browser resolves in the desktop and back), and the
|
|
29
|
+
// same `Bun.build` bundle. The alternative — esbuild — was measured at 0.6%
|
|
30
|
+
// median drift in the Phase 21 spike; identical beats measured-close, and it
|
|
31
|
+
// removes a whole class of "renders differently in the browser" bug reports.
|
|
32
|
+
//
|
|
33
|
+
// Protocol: argv gives the design root and the canvas path; stdout carries one
|
|
34
|
+
// JSON object. Nothing else is printed on the happy path, so the parent parses
|
|
35
|
+
// stdout wholesale.
|
|
36
|
+
|
|
37
|
+
import { join } from 'node:path';
|
|
38
|
+
|
|
39
|
+
const [, , designRoot, canvasAbs] = process.argv;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Where the build engine lives.
|
|
43
|
+
*
|
|
44
|
+
* A DYNAMIC import, not a static one, because this file runs from two very
|
|
45
|
+
* different layouts: a dev checkout (a sibling of `canvas-build.ts`) and the
|
|
46
|
+
* cell image, where the Dockerfile stages the studio source wherever it likes.
|
|
47
|
+
* `MAUDE_STUDIO_SRC` is passed through the sandbox's otherwise-empty environment
|
|
48
|
+
* on purpose — it is a path, not a secret.
|
|
49
|
+
*
|
|
50
|
+
* `import.meta.dir`, NOT `paths.ts`: this file is deliberately dependency-free
|
|
51
|
+
* so the sandboxed child imports the engine and nothing else. DDR-045's rule is
|
|
52
|
+
* about the SERVER's disk paths under `bun --compile`; the worker is never
|
|
53
|
+
* compiled — it is always run as source by an explicit `bun <path>` the parent
|
|
54
|
+
* resolved through `paths.ts` already.
|
|
55
|
+
*/
|
|
56
|
+
function studioDir(): string {
|
|
57
|
+
return process.env.MAUDE_STUDIO_SRC || import.meta.dir;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
if (!designRoot || !canvasAbs) {
|
|
62
|
+
process.stdout.write(JSON.stringify({ ok: false, error: 'usage: <designRoot> <canvasAbs>' }));
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
const { buildCanvasModule } = await import(join(studioDir(), 'canvas-build.ts'));
|
|
66
|
+
const source = await Bun.file(canvasAbs).text();
|
|
67
|
+
const built = await buildCanvasModule(canvasAbs, source, {
|
|
68
|
+
designRoot,
|
|
69
|
+
restrictImportsTo: designRoot,
|
|
70
|
+
});
|
|
71
|
+
process.stdout.write(
|
|
72
|
+
JSON.stringify({ ok: true, js: built.js, locator: built.locator, etag: built.etag })
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
main().catch((err) => {
|
|
77
|
+
// The message is the product surface: a rejected import, a syntax error or a
|
|
78
|
+
// missing sibling all arrive here, and the person who wrote the canvas is
|
|
79
|
+
// the one who has to act on it. Bun collapses plugin throws into the build
|
|
80
|
+
// log, so the allowlist's own wording survives in `err.message`.
|
|
81
|
+
process.stdout.write(
|
|
82
|
+
JSON.stringify({ ok: false, error: String(err?.message ?? err).slice(0, 4000) })
|
|
83
|
+
);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
});
|
|
@@ -2429,6 +2429,8 @@ 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
|
+
cloud = null,
|
|
2432
2434
|
groups,
|
|
2433
2435
|
activePath,
|
|
2434
2436
|
activeDsName,
|
|
@@ -2825,7 +2827,12 @@ function Sidebar({
|
|
|
2825
2827
|
{/* Cloud Phase 23 C3 — Maude Cloud sign-in + remote-project attach, docked
|
|
2826
2828
|
above the GitHub identity. Dev-server-backed, so it works in the desktop
|
|
2827
2829
|
shell AND a plain browser. */}
|
|
2828
|
-
|
|
2830
|
+
{/* Cloud Phase 27 — "Sign in to Maude Cloud" is an offer to do the thing
|
|
2831
|
+
you have already done: you reached this tab THROUGH a Maude account.
|
|
2832
|
+
Absent rather than disabled, because unlike the agent chat there is
|
|
2833
|
+
nothing here to explain — the capability is not missing, it is
|
|
2834
|
+
already satisfied. */}
|
|
2835
|
+
{cloud ? null : <CloudBar />}
|
|
2829
2836
|
{/* Phase 28 (E3) — GitHub identity as a compact avatar docked at the BOTTOM:
|
|
2830
2837
|
sign in, connected account + New/Pull/Share, sign out. Self-contained
|
|
2831
2838
|
(owns its device-code + CreateProject dialogs). Renders nothing in browser. */}
|
|
@@ -3353,11 +3360,21 @@ function ViewDropdown({ panels, onToggle, onClose, onZoom, hasCanvas }) {
|
|
|
3353
3360
|
role="menuitem"
|
|
3354
3361
|
className={'st-dd-item' + (p.checked ? ' is-on' : '')}
|
|
3355
3362
|
aria-disabled={p.disabled ? 'true' : undefined}
|
|
3363
|
+
title={p.hint || undefined}
|
|
3356
3364
|
onClick={() => {
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3365
|
+
// Cloud Phase 27 C2 — a row that states an absence still has
|
|
3366
|
+
// somewhere to send you. A disabled item with a `href` opens it
|
|
3367
|
+
// instead of doing nothing, which is the difference between "this
|
|
3368
|
+
// is not available here" and a dead button.
|
|
3369
|
+
if (p.disabled) {
|
|
3370
|
+
if (p.href) {
|
|
3371
|
+
window.open(p.href, '_blank', 'noopener,noreferrer');
|
|
3372
|
+
onClose();
|
|
3373
|
+
}
|
|
3374
|
+
return;
|
|
3360
3375
|
}
|
|
3376
|
+
onToggle(p.id);
|
|
3377
|
+
onClose();
|
|
3361
3378
|
}}
|
|
3362
3379
|
>
|
|
3363
3380
|
<span className="st-dd-lead">
|
|
@@ -3604,6 +3621,8 @@ function EditDropdown({ onAction, onClose, hasCanvas, readOnly = false }) {
|
|
|
3604
3621
|
function Menubar({
|
|
3605
3622
|
activePath,
|
|
3606
3623
|
project,
|
|
3624
|
+
/** `{ dashboardUrl, projectName }` when this is a cloud tab, else null. */
|
|
3625
|
+
cloud = null,
|
|
3607
3626
|
tabsCount,
|
|
3608
3627
|
openMenu,
|
|
3609
3628
|
setOpenMenu,
|
|
@@ -3678,8 +3697,19 @@ function Menubar({
|
|
|
3678
3697
|
<span style={{ color: 'var(--u-fg-3)' }}>no canvas open</span>
|
|
3679
3698
|
);
|
|
3680
3699
|
|
|
3681
|
-
// Cloud Phase
|
|
3682
|
-
|
|
3700
|
+
// Cloud Phase 27 C1 — ROLE SHAPES WHAT IS OFFERED, NEVER WHAT IS VISIBLE.
|
|
3701
|
+
//
|
|
3702
|
+
// `inspector` and `layers` used to be on this list and should never have been.
|
|
3703
|
+
// Read-only means cannot CHANGE, not cannot SEE: a reviewer needs the
|
|
3704
|
+
// structure and the measured values to say anything useful, and hiding them
|
|
3705
|
+
// is how a viewer's Maude became a worse Maude rather than a narrower one.
|
|
3706
|
+
// What stays hidden is only what genuinely does not exist for this session —
|
|
3707
|
+
// the agent chat (`assistant`, which needs a `claude` on the machine the app
|
|
3708
|
+
// is running on) and auto-open (a preference for a surface you cannot edit).
|
|
3709
|
+
// In the cloud the `assistant` row is the stated-absence row above, so it must
|
|
3710
|
+
// NOT be filtered away here — a viewer would then get silence, which is the
|
|
3711
|
+
// exact thing C2 forbids.
|
|
3712
|
+
const viewerHiddenPanels = new Set(cloud ? ['autoopen'] : ['assistant', 'autoopen']);
|
|
3683
3713
|
const panels = [
|
|
3684
3714
|
{ id: 'tree', label: 'Project Tree', shortcut: 'T', checked: sidebarOpen, disabled: false },
|
|
3685
3715
|
{
|
|
@@ -3745,7 +3775,26 @@ function Menubar({
|
|
|
3745
3775
|
disabled: false,
|
|
3746
3776
|
},
|
|
3747
3777
|
]
|
|
3748
|
-
:
|
|
3778
|
+
: // Cloud Phase 27 C2 — THE AGENT'S ABSENCE IS STATED WHERE THE AGENT
|
|
3779
|
+
// WOULD BE. It runs on YOUR `claude` subscription, on YOUR machine
|
|
3780
|
+
// (DDR-123), so a browser tab genuinely cannot have it. That is a
|
|
3781
|
+
// legitimate difference — but a menu that silently lacks an item its
|
|
3782
|
+
// user has seen on the desktop reads as "the cloud one is broken", so
|
|
3783
|
+
// the row stays, disabled, saying where to find it. Never a hidden
|
|
3784
|
+
// item, never a dead button, never silence.
|
|
3785
|
+
cloud
|
|
3786
|
+
? [
|
|
3787
|
+
{
|
|
3788
|
+
id: 'assistant',
|
|
3789
|
+
label: 'Assistant — in the desktop app',
|
|
3790
|
+
shortcut: '',
|
|
3791
|
+
checked: false,
|
|
3792
|
+
disabled: true,
|
|
3793
|
+
href: 'https://maude.sh/download',
|
|
3794
|
+
hint: 'The agent runs on your own Claude subscription, on your own machine.',
|
|
3795
|
+
},
|
|
3796
|
+
]
|
|
3797
|
+
: []),
|
|
3749
3798
|
{
|
|
3750
3799
|
id: 'annotate',
|
|
3751
3800
|
label: 'Annotations',
|
|
@@ -3848,6 +3897,29 @@ function Menubar({
|
|
|
3848
3897
|
</span>
|
|
3849
3898
|
<span className="st-brand-name">maude</span>
|
|
3850
3899
|
</span>
|
|
3900
|
+
{/* Cloud Phase 27 C4 — THE WAY BACK OUT.
|
|
3901
|
+
A browser tab has no window title and no app switcher, so a teammate
|
|
3902
|
+
who followed a link has nothing telling them which project they are in
|
|
3903
|
+
or how to leave it. The desktop gets both for free from the OS; the
|
|
3904
|
+
cloud has to say them. This is an ADDITION to the shared client behind
|
|
3905
|
+
the same `cloud` flag as C2's agent notice — never a fork of the
|
|
3906
|
+
component, which is how the two shells started diverging last time. */}
|
|
3907
|
+
{cloud ? (
|
|
3908
|
+
<span className="st-cloudback" data-testid="cloud-back">
|
|
3909
|
+
<a
|
|
3910
|
+
className="st-cloudback-link"
|
|
3911
|
+
href={cloud.dashboardUrl || 'https://cloud.maude.sh'}
|
|
3912
|
+
title="Back to your projects"
|
|
3913
|
+
>
|
|
3914
|
+
← Dashboard
|
|
3915
|
+
</a>
|
|
3916
|
+
{cloud.projectName ? (
|
|
3917
|
+
<span className="st-cloudback-project" data-testid="cloud-project-name">
|
|
3918
|
+
{cloud.projectName}
|
|
3919
|
+
</span>
|
|
3920
|
+
) : null}
|
|
3921
|
+
</span>
|
|
3922
|
+
) : null}
|
|
3851
3923
|
<nav className="st-menus" aria-label="Application menus" data-tour="menus">
|
|
3852
3924
|
{MENU_NAMES.map((name) => {
|
|
3853
3925
|
const key = name.toLowerCase();
|
|
@@ -9196,6 +9268,23 @@ function App() {
|
|
|
9196
9268
|
// decides what the UI offers; the cell (C1) and the dev-server's
|
|
9197
9269
|
// read-only gate (http.ts) are what actually stop a write.
|
|
9198
9270
|
readOnly: !!data.readOnly,
|
|
9271
|
+
// Cloud Phase 27 (DDR-209) — the capability that opens the cookieless
|
|
9272
|
+
// canvas origin. `canvasUrl()` appends it to every iframe URL. Absent
|
|
9273
|
+
// on a desktop, where the canvas origin is loopback and needs none.
|
|
9274
|
+
//
|
|
9275
|
+
// NOTE FOR WHOEVER ADDS THE NEXT FIELD: this is an explicit
|
|
9276
|
+
// projection, not a spread of `data`. A field the server starts
|
|
9277
|
+
// returning does NOT arrive here until it is named — which is how
|
|
9278
|
+
// both of these went missing and the cloud studio rendered without
|
|
9279
|
+
// its chrome and with every canvas iframe unauthenticated.
|
|
9280
|
+
canvasToken: data.canvasToken,
|
|
9281
|
+
// C2/C4 — `{ dashboardUrl, projectName }` when this is a cloud tab.
|
|
9282
|
+
// Its presence is what tells the shared client it is in a browser tab
|
|
9283
|
+
// on somebody else's machine.
|
|
9284
|
+
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.
|
|
9199
9288
|
}));
|
|
9200
9289
|
})
|
|
9201
9290
|
.catch(() => {});
|
|
@@ -13759,6 +13848,7 @@ function App() {
|
|
|
13759
13848
|
if (id === 'tree')
|
|
13760
13849
|
return (
|
|
13761
13850
|
<Sidebar
|
|
13851
|
+
cloud={cfg?.cloud ?? null}
|
|
13762
13852
|
readOnly={viewerMode}
|
|
13763
13853
|
groups={groups}
|
|
13764
13854
|
activePath={activePath}
|
|
@@ -13920,6 +14010,10 @@ function App() {
|
|
|
13920
14010
|
<div className="st-shell">
|
|
13921
14011
|
<Menubar
|
|
13922
14012
|
readOnly={viewerMode}
|
|
14013
|
+
// Cloud Phase 27 C2/C4 — the ONE cloud-only input the shared chrome
|
|
14014
|
+
// takes. A prop, not the whole `cfg`: the Menubar needs to know it is
|
|
14015
|
+
// in a browser tab on somebody else's machine, and nothing else.
|
|
14016
|
+
cloud={cfg?.cloud ?? null}
|
|
13923
14017
|
activePath={activePath}
|
|
13924
14018
|
project={project}
|
|
13925
14019
|
tabsCount={tabs.length}
|
|
@@ -14919,6 +15013,7 @@ function App() {
|
|
|
14919
15013
|
)}
|
|
14920
15014
|
{settingsOpen && (
|
|
14921
15015
|
<SettingsPanel
|
|
15016
|
+
cloud={cfg?.cloud ?? null}
|
|
14922
15017
|
onClose={() => setSettingsOpen(false)}
|
|
14923
15018
|
initialTab={typeof settingsOpen === 'string' ? settingsOpen : undefined}
|
|
14924
15019
|
theme={theme}
|
|
@@ -14994,7 +15089,16 @@ function App() {
|
|
|
14994
15089
|
startTour(USAGE_TOUR);
|
|
14995
15090
|
}}
|
|
14996
15091
|
/>
|
|
14997
|
-
|
|
15092
|
+
{/* `activeCanvas` is THIS window's truth. The server's `_active.json` is
|
|
15093
|
+
global and sticky — it survives closing every tab and is written by any
|
|
15094
|
+
other session (an agent driving a headless browser, a second window),
|
|
15095
|
+
so a report built from it can claim a canvas the user does not have
|
|
15096
|
+
open, and capture it. Pass what this client actually shows. */}
|
|
15097
|
+
<ReportBugDialog
|
|
15098
|
+
open={reportBugOpen}
|
|
15099
|
+
activeCanvas={activePath}
|
|
15100
|
+
onClose={() => setReportBugOpen(false)}
|
|
15101
|
+
/>
|
|
14998
15102
|
<WhatsNewPanel wn={whatsNew} onStartTour={startTour} />
|
|
14999
15103
|
<ExportPanel center={exportCenter} />
|
|
15000
15104
|
<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
|
|
@@ -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,8 @@ 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
|
+
cloud = null,
|
|
792
806
|
onClose,
|
|
793
807
|
initialTab,
|
|
794
808
|
theme,
|
|
@@ -811,11 +825,17 @@ export default function SettingsPanel({
|
|
|
811
825
|
}) {
|
|
812
826
|
const [providers, setProviders] = useState(null); // null = loading
|
|
813
827
|
const [error, setError] = useState(null);
|
|
828
|
+
// The tabs THIS shell can honour. See TABS for why the local-machine ones are
|
|
829
|
+
// absent in a browser tab rather than disabled.
|
|
830
|
+
const tabs = useMemo(() => TABS.filter((t) => !(cloud && t.local)), [cloud]);
|
|
814
831
|
const [tab, setTab] = useState(() => {
|
|
815
|
-
|
|
832
|
+
// A remembered tab that this shell does not have (you opened Video on the
|
|
833
|
+
// desktop, then opened Settings in the cloud) must not select nothing.
|
|
834
|
+
const has = (id) => TABS.some((t) => t.id === id && !(cloud && t.local));
|
|
835
|
+
if (initialTab && has(initialTab)) return initialTab;
|
|
816
836
|
try {
|
|
817
837
|
const v = localStorage.getItem(SETTINGS_TAB_STORE);
|
|
818
|
-
if (v &&
|
|
838
|
+
if (v && has(v)) return v;
|
|
819
839
|
} catch {}
|
|
820
840
|
return 'appearance';
|
|
821
841
|
});
|
|
@@ -853,19 +873,19 @@ export default function SettingsPanel({
|
|
|
853
873
|
|
|
854
874
|
// Vertical roving focus over the tab rail (native-menu parity).
|
|
855
875
|
function onRailKey(e) {
|
|
856
|
-
const idx =
|
|
876
|
+
const idx = tabs.findIndex((t) => t.id === tab);
|
|
857
877
|
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
|
858
878
|
e.preventDefault();
|
|
859
|
-
setTab(
|
|
879
|
+
setTab(tabs[(idx + 1) % tabs.length].id);
|
|
860
880
|
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
|
|
861
881
|
e.preventDefault();
|
|
862
|
-
setTab(
|
|
882
|
+
setTab(tabs[(idx - 1 + tabs.length) % tabs.length].id);
|
|
863
883
|
} else if (e.key === 'Home') {
|
|
864
884
|
e.preventDefault();
|
|
865
|
-
setTab(
|
|
885
|
+
setTab(tabs[0].id);
|
|
866
886
|
} else if (e.key === 'End') {
|
|
867
887
|
e.preventDefault();
|
|
868
|
-
setTab(
|
|
888
|
+
setTab(tabs[tabs.length - 1].id);
|
|
869
889
|
}
|
|
870
890
|
}
|
|
871
891
|
|
|
@@ -896,7 +916,7 @@ export default function SettingsPanel({
|
|
|
896
916
|
aria-label="Settings categories"
|
|
897
917
|
onKeyDown={onRailKey}
|
|
898
918
|
>
|
|
899
|
-
{
|
|
919
|
+
{tabs.map((t) => (
|
|
900
920
|
<button
|
|
901
921
|
key={t.id}
|
|
902
922
|
type="button"
|
|
@@ -14,11 +14,30 @@
|
|
|
14
14
|
|
|
15
15
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
16
16
|
|
|
17
|
-
import { invoke, isNativeApp, openGitHubUrl } from './github.js';
|
|
17
|
+
import { invoke, isNativeApp, openGitHubUrl, pickMediaFile } from './github.js';
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Two of these are auto-captured (window + artboard), so the ceiling leaves
|
|
20
|
+
// room for the user to still attach their own.
|
|
21
|
+
const MAX_SHOTS = 4;
|
|
20
22
|
const PUBLIC_ISSUES_URL = 'https://github.com/1aGh/maude/issues/new';
|
|
21
23
|
|
|
24
|
+
/** How each attached shot got here — shown per tile so the user can tell the
|
|
25
|
+
* auto-captured artboard (which never contains Maude's own chrome) apart from
|
|
26
|
+
* one they attached themselves. */
|
|
27
|
+
const SHOT_LABEL = {
|
|
28
|
+
shell: 'Maude window (auto)',
|
|
29
|
+
artboard: 'Active artboard (auto)',
|
|
30
|
+
attached: 'Attached by you',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The OS window-capture shortcut, for the "a Maude UI bug needs the chrome"
|
|
34
|
+
* hint. `platform` is the node `process.platform` string from the bundle. */
|
|
35
|
+
function windowShotHint(platform) {
|
|
36
|
+
if (platform === 'darwin') return '⇧⌘4 then Space';
|
|
37
|
+
if (platform === 'win32') return 'Win+Shift+S';
|
|
38
|
+
return 'your screenshot tool';
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
function randHex(n) {
|
|
23
42
|
const bytes = new Uint8Array(n / 2);
|
|
24
43
|
crypto.getRandomValues(bytes);
|
|
@@ -124,6 +143,7 @@ function ShotTile({ shot, redacting, onRects, onRemove }) {
|
|
|
124
143
|
/>
|
|
125
144
|
))}
|
|
126
145
|
</div>
|
|
146
|
+
<div className="rb-shot-src">{SHOT_LABEL[shot.source] ?? SHOT_LABEL.attached}</div>
|
|
127
147
|
<div className="rb-shot-actions">
|
|
128
148
|
{shot.rects.length > 0 && (
|
|
129
149
|
<button type="button" className="rb-link" onClick={() => onRects([])}>
|
|
@@ -138,11 +158,11 @@ function ShotTile({ shot, redacting, onRects, onRemove }) {
|
|
|
138
158
|
);
|
|
139
159
|
}
|
|
140
160
|
|
|
141
|
-
export function ReportBugDialog({ open, onClose }) {
|
|
161
|
+
export function ReportBugDialog({ open, onClose, activeCanvas = null }) {
|
|
142
162
|
const [step, setStep] = useState('describe'); // describe | preview | sending | done | fallback
|
|
143
163
|
const [description, setDescription] = useState('');
|
|
144
164
|
const [bundle, setBundle] = useState(null);
|
|
145
|
-
const [shots, setShots] = useState([]); // {url, blob, rects:[{x,y,w,h} normalized]}
|
|
165
|
+
const [shots, setShots] = useState([]); // {url, blob, source, rects:[{x,y,w,h} normalized]}
|
|
146
166
|
const [crashLogs, setCrashLogs] = useState([]); // native only: {name, firstLine}
|
|
147
167
|
const [consent, setConsent] = useState({
|
|
148
168
|
logTail: true,
|
|
@@ -150,6 +170,7 @@ export function ReportBugDialog({ open, onClose }) {
|
|
|
150
170
|
projectName: false,
|
|
151
171
|
crashLogs: true,
|
|
152
172
|
});
|
|
173
|
+
const [shellPending, setShellPending] = useState(false);
|
|
153
174
|
const [redacting, setRedacting] = useState(false);
|
|
154
175
|
const [result, setResult] = useState(null); // {issueUrl, issueNumber} | {fallbackDir}
|
|
155
176
|
const [error, setError] = useState(null);
|
|
@@ -161,21 +182,52 @@ export function ReportBugDialog({ open, onClose }) {
|
|
|
161
182
|
setBundle(null);
|
|
162
183
|
setShots([]);
|
|
163
184
|
setCrashLogs([]);
|
|
185
|
+
setShellPending(false);
|
|
164
186
|
setRedacting(false);
|
|
165
187
|
setResult(null);
|
|
166
188
|
setError(null);
|
|
167
189
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
168
190
|
}, [shots]);
|
|
169
191
|
|
|
170
|
-
const addShotBlob = useCallback((blob) => {
|
|
192
|
+
const addShotBlob = useCallback((blob, source = 'attached') => {
|
|
171
193
|
if (!blob) return;
|
|
172
194
|
setShots((prev) =>
|
|
173
195
|
prev.length >= MAX_SHOTS
|
|
174
196
|
? prev
|
|
175
|
-
: [...prev, { url: URL.createObjectURL(blob), blob, rects: [] }]
|
|
197
|
+
: [...prev, { url: URL.createObjectURL(blob), blob, source, rects: [] }]
|
|
176
198
|
);
|
|
177
199
|
}, []);
|
|
178
200
|
|
|
201
|
+
// Attach a screenshot the user took themselves — the ONLY lane that can carry
|
|
202
|
+
// Maude's own chrome (menubar, sidebar, dialogs), since the auto-capture
|
|
203
|
+
// renders the canvas headlessly and the shell is never in frame. Native
|
|
204
|
+
// WKWebView won't present a panel for an HTML <input type=file> at all, so it
|
|
205
|
+
// routes through the same Rust open-dialog the AssetPicker uses.
|
|
206
|
+
const attachScreenshot = useCallback(async () => {
|
|
207
|
+
if (isNativeApp()) {
|
|
208
|
+
try {
|
|
209
|
+
const picked = await pickMediaFile();
|
|
210
|
+
if (picked?.bytes) addShotBlob(new Blob([new Uint8Array(picked.bytes)]), 'attached');
|
|
211
|
+
} catch {
|
|
212
|
+
/* cancelled or dialog failure — paste (⌘V) still works */
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Browser — imperative off-screen <input>, clicked inside the gesture
|
|
217
|
+
// (mirrors app.jsx's openFilePicker; display:none inputs don't lay out).
|
|
218
|
+
const input = document.createElement('input');
|
|
219
|
+
input.type = 'file';
|
|
220
|
+
input.accept = 'image/*';
|
|
221
|
+
input.style.cssText = 'position:fixed;left:-9999px;top:0';
|
|
222
|
+
document.body.appendChild(input);
|
|
223
|
+
input.addEventListener('change', () => {
|
|
224
|
+
const file = input.files?.[0];
|
|
225
|
+
if (file) addShotBlob(file, 'attached');
|
|
226
|
+
input.remove();
|
|
227
|
+
});
|
|
228
|
+
input.click();
|
|
229
|
+
}, [addShotBlob]);
|
|
230
|
+
|
|
179
231
|
// On open: pull the scrubbed bundle, capture the active artboard, list
|
|
180
232
|
// native crash logs. Every leg fails soft — a report with fewer attachments
|
|
181
233
|
// beats no dialog.
|
|
@@ -186,14 +238,37 @@ export function ReportBugDialog({ open, onClose }) {
|
|
|
186
238
|
.then((r) => (r.ok ? r.json() : null))
|
|
187
239
|
.then((b) => alive && setBundle(b))
|
|
188
240
|
.catch(() => {});
|
|
189
|
-
|
|
241
|
+
// Artboard shot only when THIS window actually has a canvas open. The
|
|
242
|
+
// export resolves its target from the server's `_active.json`, so with
|
|
243
|
+
// nothing open it would attach a stale canvas from another session.
|
|
244
|
+
if (activeCanvas) {
|
|
245
|
+
fetch('/_api/export', {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
248
|
+
body: JSON.stringify({ format: 'png', scope: 'artboard' }),
|
|
249
|
+
})
|
|
250
|
+
.then((r) => (r.ok ? r.blob() : null))
|
|
251
|
+
.then((blob) => alive && blob && addShotBlob(blob, 'artboard'))
|
|
252
|
+
.catch(() => {});
|
|
253
|
+
}
|
|
254
|
+
// The window shot — Maude's own chrome, which the artboard capture above
|
|
255
|
+
// structurally cannot see. Slow (boots a headless browser, opens the canvas),
|
|
256
|
+
// so it lands second and shows a pending row meanwhile. `canvas` is sent
|
|
257
|
+
// explicitly — including as null, which means "open nothing", so an empty
|
|
258
|
+
// window is reported as an empty window.
|
|
259
|
+
setShellPending(true);
|
|
260
|
+
fetch('/_api/shell-shot', {
|
|
190
261
|
method: 'POST',
|
|
191
262
|
headers: { 'Content-Type': 'application/json' },
|
|
192
|
-
body: JSON.stringify({
|
|
263
|
+
body: JSON.stringify({ canvas: activeCanvas }),
|
|
193
264
|
})
|
|
194
265
|
.then((r) => (r.ok ? r.blob() : null))
|
|
195
|
-
.then((blob) =>
|
|
196
|
-
|
|
266
|
+
.then((blob) => {
|
|
267
|
+
if (!alive) return;
|
|
268
|
+
if (blob) addShotBlob(blob, 'shell');
|
|
269
|
+
setShellPending(false);
|
|
270
|
+
})
|
|
271
|
+
.catch(() => alive && setShellPending(false));
|
|
197
272
|
if (isNativeApp()) {
|
|
198
273
|
invoke('list_crash_logs')
|
|
199
274
|
.then((logs) => alive && setCrashLogs(Array.isArray(logs) ? logs.slice(0, 3) : []))
|
|
@@ -203,7 +278,7 @@ export function ReportBugDialog({ open, onClose }) {
|
|
|
203
278
|
alive = false;
|
|
204
279
|
};
|
|
205
280
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
206
|
-
}, [open]);
|
|
281
|
+
}, [open, activeCanvas]);
|
|
207
282
|
|
|
208
283
|
// Paste-to-attach — the lane that covers native-shell bugs the canvas
|
|
209
284
|
// capture can't see (user screenshots the window, pastes here).
|
|
@@ -238,7 +313,9 @@ export function ReportBugDialog({ open, onClose }) {
|
|
|
238
313
|
},
|
|
239
314
|
context: {
|
|
240
315
|
projectName: consent.projectName ? (bundle?.context?.projectName ?? null) : null,
|
|
241
|
-
|
|
316
|
+
// This window's canvas, never the server's sticky `_active.json` — the
|
|
317
|
+
// report must not name a file the reporter does not have open.
|
|
318
|
+
activeCanvas: consent.activeCanvas ? activeCanvas : null,
|
|
242
319
|
},
|
|
243
320
|
description: description.trim(),
|
|
244
321
|
attachments: {
|
|
@@ -385,21 +462,42 @@ export function ReportBugDialog({ open, onClose }) {
|
|
|
385
462
|
<div className="rb-section">
|
|
386
463
|
<div className="rb-section-hd">
|
|
387
464
|
<span>Screenshots ({shots.length})</span>
|
|
388
|
-
|
|
389
|
-
<
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
465
|
+
<span className="rb-hd-actions">
|
|
466
|
+
{shots.length < MAX_SHOTS && (
|
|
467
|
+
<button
|
|
468
|
+
type="button"
|
|
469
|
+
className="rb-link"
|
|
470
|
+
data-testid="report-bug-attach"
|
|
471
|
+
onClick={attachScreenshot}
|
|
472
|
+
>
|
|
473
|
+
Attach screenshot…
|
|
474
|
+
</button>
|
|
475
|
+
)}
|
|
476
|
+
{shots.length > 0 && (
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
className={`rb-link${redacting ? ' is-on' : ''}`}
|
|
480
|
+
data-testid="report-bug-redact"
|
|
481
|
+
onClick={() => setRedacting((v) => !v)}
|
|
482
|
+
>
|
|
483
|
+
{redacting ? 'Done redacting' : 'Redact…'}
|
|
484
|
+
</button>
|
|
485
|
+
)}
|
|
486
|
+
</span>
|
|
398
487
|
</div>
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
488
|
+
<p className="rb-hint">
|
|
489
|
+
Maude captures its own window and the active artboard for you. The window shot is
|
|
490
|
+
a fresh render, so anything mid-flight — an open menu, a stuck spinner — won't be
|
|
491
|
+
in it: grab that yourself ({windowShotHint(bundle?.app?.platform)}) and attach or
|
|
492
|
+
paste (⌘V) it here.
|
|
493
|
+
</p>
|
|
494
|
+
{shellPending && (
|
|
495
|
+
<p className="rb-hint" data-testid="report-bug-shell-pending">
|
|
496
|
+
Capturing the Maude window…
|
|
402
497
|
</p>
|
|
498
|
+
)}
|
|
499
|
+
{shots.length === 0 && !shellPending ? (
|
|
500
|
+
<p className="rb-hint">No screenshot attached yet — up to {MAX_SHOTS}.</p>
|
|
403
501
|
) : (
|
|
404
502
|
<>
|
|
405
503
|
{redacting && (
|
|
@@ -421,9 +519,6 @@ export function ReportBugDialog({ open, onClose }) {
|
|
|
421
519
|
/>
|
|
422
520
|
))}
|
|
423
521
|
</div>
|
|
424
|
-
{shots.length < MAX_SHOTS && (
|
|
425
|
-
<p className="rb-hint">Paste (⌘V) to attach another screenshot.</p>
|
|
426
|
-
)}
|
|
427
522
|
</>
|
|
428
523
|
)}
|
|
429
524
|
</div>
|
|
@@ -439,7 +534,7 @@ export function ReportBugDialog({ open, onClose }) {
|
|
|
439
534
|
{bundle?.app?.arch ?? ''} · {isNativeApp() ? 'desktop app' : 'browser'}
|
|
440
535
|
</span>
|
|
441
536
|
</label>
|
|
442
|
-
{
|
|
537
|
+
{activeCanvas && (
|
|
443
538
|
<label className="rb-check">
|
|
444
539
|
<input
|
|
445
540
|
type="checkbox"
|
|
@@ -447,7 +542,7 @@ export function ReportBugDialog({ open, onClose }) {
|
|
|
447
542
|
onChange={(e) => setConsent({ ...consent, activeCanvas: e.target.checked })}
|
|
448
543
|
/>
|
|
449
544
|
<span>
|
|
450
|
-
Active canvas path — <code>{
|
|
545
|
+
Active canvas path — <code>{activeCanvas}</code>
|
|
451
546
|
</span>
|
|
452
547
|
</label>
|
|
453
548
|
)}
|