@1agh/maude 0.50.0 → 0.51.1
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/client/app.jsx +25 -0
- package/apps/studio/client/github.js +2 -0
- package/apps/studio/client/panels/CloudBar.jsx +119 -15
- package/apps/studio/client/report-bug.jsx +590 -0
- package/apps/studio/client/styles/3-shell-maude.css +2 -0
- package/apps/studio/client/styles/4-components.css +84 -0
- package/apps/studio/cloud/endpoints.ts +86 -38
- package/apps/studio/debug-bundle.ts +144 -0
- package/apps/studio/dist/client.bundle.js +902 -897
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/git/service.ts +1 -1
- package/apps/studio/http.ts +92 -1
- package/apps/studio/server.ts +7 -0
- package/apps/studio/sync/workspace-signin.ts +15 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +8 -0
- package/apps/studio/test/cloud-endpoints.test.ts +130 -0
- package/apps/studio/test/debug-bundle.test.ts +105 -0
- package/apps/studio/test/workspace-signin.test.ts +19 -0
- package/apps/studio/whats-new.json +9 -0
- package/cli/lib/gitignore-block.mjs +1 -0
- package/package.json +9 -8
|
@@ -78,6 +78,7 @@ import { PhotoKnobs } from './photo-knobs.jsx';
|
|
|
78
78
|
import {
|
|
79
79
|
appIsFirstRun,
|
|
80
80
|
isNativeApp,
|
|
81
|
+
onMenuReportBug,
|
|
81
82
|
onUpdateReady,
|
|
82
83
|
pickMediaFile,
|
|
83
84
|
pickMediaFiles,
|
|
@@ -88,6 +89,7 @@ import { TourOverlay } from './tour/overlay.jsx';
|
|
|
88
89
|
import { QUICK_SETUP_TOUR } from './tour/quick-setup-tour.js';
|
|
89
90
|
import { USAGE_TOUR } from './tour/usage-tour.js';
|
|
90
91
|
import { ExportBadge, ExportPanel, ExportToast, useExportCenter } from './export-center.jsx';
|
|
92
|
+
import { ReportBugDialog } from './report-bug.jsx';
|
|
91
93
|
import { useWhatsNew, WhatsNewPanel, WhatsNewToast } from './whats-new.jsx';
|
|
92
94
|
|
|
93
95
|
const USAGE_TOUR_STORE = 'mdcc-usage-tour-seen';
|
|
@@ -3408,6 +3410,7 @@ function HelpDropdown({ onAction, onClose }) {
|
|
|
3408
3410
|
items={[
|
|
3409
3411
|
{ id: 'shortcuts', label: 'Keyboard shortcuts', shortcut: '?' },
|
|
3410
3412
|
{ id: 'help', label: 'Help · commands & flows', shortcut: 'F1' },
|
|
3413
|
+
{ id: 'report-bug', label: 'Report a bug…' },
|
|
3411
3414
|
{ sep: true },
|
|
3412
3415
|
{ id: 'tour', label: 'Take the tour' },
|
|
3413
3416
|
{ id: 'watch-intro', label: 'Watch the intro' },
|
|
@@ -3552,6 +3555,7 @@ function Menubar({
|
|
|
3552
3555
|
onToggleShowHidden,
|
|
3553
3556
|
onOpenHelp,
|
|
3554
3557
|
onOpenShortcuts,
|
|
3558
|
+
onReportBug,
|
|
3555
3559
|
onStartTour,
|
|
3556
3560
|
onStartCollabTour,
|
|
3557
3561
|
annotationsVisible,
|
|
@@ -3878,6 +3882,7 @@ function Menubar({
|
|
|
3878
3882
|
onAction={(id) => {
|
|
3879
3883
|
if (id === 'shortcuts') onOpenShortcuts?.();
|
|
3880
3884
|
else if (id === 'help') onOpenHelp?.();
|
|
3885
|
+
else if (id === 'report-bug') onReportBug?.();
|
|
3881
3886
|
else if (id === 'tour') onStartTour?.();
|
|
3882
3887
|
else if (id === 'collab-tour') onStartCollabTour?.();
|
|
3883
3888
|
else if (id === 'quick-setup') onOpenQuickSetup?.();
|
|
@@ -9158,6 +9163,17 @@ function App() {
|
|
|
9158
9163
|
const [showHidden, setShowHidden] = useState(() => readBoolStore(SHOW_HIDDEN_STORE, false));
|
|
9159
9164
|
const [sectionsExpanded, setSectionsExpanded] = useState(() => readJsonStore(SECTIONS_STORE, {}));
|
|
9160
9165
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
9166
|
+
const [reportBugOpen, setReportBugOpen] = useState(false);
|
|
9167
|
+
|
|
9168
|
+
// Native Help ▸ Report a Bug… (menu.rs emits `menu://report-bug`) — same
|
|
9169
|
+
// lane as IdentityBar's File ▸ New Project… subscription.
|
|
9170
|
+
useEffect(() => {
|
|
9171
|
+
if (!isNativeApp()) return;
|
|
9172
|
+
const p = onMenuReportBug(() => setReportBugOpen(true));
|
|
9173
|
+
return () => {
|
|
9174
|
+
p.then((un) => un()).catch(() => {});
|
|
9175
|
+
};
|
|
9176
|
+
}, []);
|
|
9161
9177
|
const [readinessOpen, setReadinessOpen] = useState(false);
|
|
9162
9178
|
const [introOpen, setIntroOpen] = useState(false);
|
|
9163
9179
|
const [quickSetupOpen, setQuickSetupOpen] = useState(false);
|
|
@@ -13309,6 +13325,13 @@ function App() {
|
|
|
13309
13325
|
kbd: 'F1',
|
|
13310
13326
|
run: () => setHelpOpen(true),
|
|
13311
13327
|
},
|
|
13328
|
+
{
|
|
13329
|
+
id: 'report-bug',
|
|
13330
|
+
group: 'Help',
|
|
13331
|
+
label: 'Report a bug…',
|
|
13332
|
+
icon: 'help',
|
|
13333
|
+
run: () => setReportBugOpen(true),
|
|
13334
|
+
},
|
|
13312
13335
|
],
|
|
13313
13336
|
[openSystem, toggleTheme, reloadActive, whatsNew]
|
|
13314
13337
|
);
|
|
@@ -13522,6 +13545,7 @@ function App() {
|
|
|
13522
13545
|
onToggleShowHidden={() => setShowHidden((v) => !v)}
|
|
13523
13546
|
onOpenHelp={() => setHelpOpen(true)}
|
|
13524
13547
|
onOpenShortcuts={() => setShortcutsOpen(true)}
|
|
13548
|
+
onReportBug={() => setReportBugOpen(true)}
|
|
13525
13549
|
onStartTour={() => startTour(USAGE_TOUR)}
|
|
13526
13550
|
onStartCollabTour={() => startTour(COLLAB_TOUR)}
|
|
13527
13551
|
annotationsVisible={annotationsVisible}
|
|
@@ -14182,6 +14206,7 @@ function App() {
|
|
|
14182
14206
|
startTour(USAGE_TOUR);
|
|
14183
14207
|
}}
|
|
14184
14208
|
/>
|
|
14209
|
+
<ReportBugDialog open={reportBugOpen} onClose={() => setReportBugOpen(false)} />
|
|
14185
14210
|
<WhatsNewPanel wn={whatsNew} onStartTour={startTour} />
|
|
14186
14211
|
<ExportPanel center={exportCenter} />
|
|
14187
14212
|
<ReadinessDialog open={readinessOpen} onClose={() => setReadinessOpen(false)} />
|
|
@@ -45,6 +45,8 @@ export const onDeviceCode = (cb) => listen('github://device-code', cb);
|
|
|
45
45
|
export const onSignedIn = (cb) => listen('github://signed-in', cb);
|
|
46
46
|
/** Fire when the native File ▸ New Project… menu item is chosen. Returns an unlisten promise. */
|
|
47
47
|
export const onMenuNewProject = (cb) => listen('menu://new-project', cb);
|
|
48
|
+
/** Fire when the native Help ▸ Report a Bug… menu item is chosen. Returns an unlisten promise. */
|
|
49
|
+
export const onMenuReportBug = (cb) => listen('menu://report-bug', cb);
|
|
48
50
|
|
|
49
51
|
// ── dev-server endpoints ────────────────────────────────────────────────────────
|
|
50
52
|
async function api(path, opts = {}) {
|
|
@@ -14,12 +14,38 @@
|
|
|
14
14
|
|
|
15
15
|
import { useEffect, useRef, useState } from 'react';
|
|
16
16
|
|
|
17
|
+
import { invoke, isNativeApp, listen } from '../github.js';
|
|
18
|
+
|
|
17
19
|
const api = async (path, init) => {
|
|
18
20
|
const res = await fetch(path, init);
|
|
19
21
|
const json = await res.json().catch(() => ({}));
|
|
20
22
|
return { ok: res.ok, status: res.status, json };
|
|
21
23
|
};
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Parse a maude://open/<project>?code=mhc_… deep link. Untrusted input — a
|
|
27
|
+
* drive-by page can navigate to maude:// — so nothing here is acted on
|
|
28
|
+
* without the person pressing Connect, and the code is only ever exchanged
|
|
29
|
+
* against the app's own configured Maude Cloud address.
|
|
30
|
+
*/
|
|
31
|
+
export function parseDeepLink(url) {
|
|
32
|
+
const m = /^maude:\/\/open\/([a-z0-9]+(?:-[a-z0-9]+)*)\/?\?(.*)$/.exec(String(url ?? ''));
|
|
33
|
+
if (!m) return null;
|
|
34
|
+
const code = new URLSearchParams(m[2]).get('code') ?? '';
|
|
35
|
+
if (!/^mhc_[0-9a-f]{16,128}$/.test(code)) return null;
|
|
36
|
+
return { project: m[1], code };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The share view's address for a project cell URL (viewer's browser home). */
|
|
40
|
+
export function shareViewUrl(projectUrl, projectId) {
|
|
41
|
+
try {
|
|
42
|
+
const u = new URL(projectUrl);
|
|
43
|
+
return `${u.protocol}//view-${u.host}`;
|
|
44
|
+
} catch {
|
|
45
|
+
return `https://view-${projectId}.cloud.maude.sh`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
function Spark({ size = 15 }) {
|
|
24
50
|
return (
|
|
25
51
|
<svg width={size} height={size} viewBox="0 0 32 32" fill="currentColor" aria-hidden="true">
|
|
@@ -76,6 +102,7 @@ export default function CloudBar() {
|
|
|
76
102
|
const [note, setNote] = useState('');
|
|
77
103
|
const [error, setError] = useState('');
|
|
78
104
|
const [copied, setCopied] = useState(false);
|
|
105
|
+
const [pending, setPending] = useState(null); // { project, code } from maude://
|
|
79
106
|
const railRef = useRef(null);
|
|
80
107
|
const pollRef = useRef(null);
|
|
81
108
|
|
|
@@ -89,6 +116,43 @@ export default function CloudBar() {
|
|
|
89
116
|
return () => clearInterval(pollRef.current);
|
|
90
117
|
}, []);
|
|
91
118
|
|
|
119
|
+
// maude:// deep links (Phase 17) — native shell only. The link that LAUNCHED
|
|
120
|
+
// the app is parked in the shell (take once); links while running arrive as
|
|
121
|
+
// events. Either way: ask, never auto-connect.
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!isNativeApp()) return undefined;
|
|
124
|
+
const consume = (url) => {
|
|
125
|
+
const parsed = parseDeepLink(url);
|
|
126
|
+
if (parsed) setPending(parsed);
|
|
127
|
+
};
|
|
128
|
+
invoke('take_pending_deep_link')
|
|
129
|
+
.then((url) => url && consume(url))
|
|
130
|
+
.catch(() => {});
|
|
131
|
+
let unlisten = null;
|
|
132
|
+
listen('maude://deep-link', consume).then((fn) => {
|
|
133
|
+
unlisten = fn;
|
|
134
|
+
});
|
|
135
|
+
return () => unlisten?.();
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
async function connectPending() {
|
|
139
|
+
if (!pending) return;
|
|
140
|
+
setBusy('deeplink');
|
|
141
|
+
setError('');
|
|
142
|
+
const r = await api('/_api/cloud/attach/code', {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'content-type': 'application/json' },
|
|
145
|
+
body: JSON.stringify({ code: pending.code }),
|
|
146
|
+
});
|
|
147
|
+
setBusy('');
|
|
148
|
+
setPending(null);
|
|
149
|
+
if (r.ok && r.json?.ok) {
|
|
150
|
+
setNote(`Linked to ${r.json.project ?? pending.project} — restart the studio server to start syncing.`);
|
|
151
|
+
} else {
|
|
152
|
+
setError(r.json?.error || 'The workspace could not be connected.');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
92
156
|
useEffect(() => {
|
|
93
157
|
if (!menuOpen) return undefined;
|
|
94
158
|
const onDoc = (e) => {
|
|
@@ -186,6 +250,28 @@ export default function CloudBar() {
|
|
|
186
250
|
|
|
187
251
|
return (
|
|
188
252
|
<div className="gi-rail cb-rail" ref={railRef} data-testid="cloud-bar">
|
|
253
|
+
{pending && (
|
|
254
|
+
<div className="gi-rail-confirm" data-testid="cloud-deeplink">
|
|
255
|
+
<span className="gi-rail-hint">Connect this project to {pending.project}?</span>
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
className="btn btn--sm"
|
|
259
|
+
disabled={busy === 'deeplink'}
|
|
260
|
+
onClick={connectPending}
|
|
261
|
+
data-testid="cloud-deeplink-connect"
|
|
262
|
+
>
|
|
263
|
+
{busy === 'deeplink' ? 'Connecting…' : 'Connect'}
|
|
264
|
+
</button>
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
className="btn btn--ghost btn--sm"
|
|
268
|
+
onClick={() => setPending(null)}
|
|
269
|
+
data-testid="cloud-deeplink-dismiss"
|
|
270
|
+
>
|
|
271
|
+
Not now
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
189
275
|
{state === 'out' && (
|
|
190
276
|
<>
|
|
191
277
|
<button type="button" className="btn btn--ghost btn--sm gi-rail-signin" onClick={startSignIn} data-testid="cloud-signin">
|
|
@@ -226,21 +312,39 @@ export default function CloudBar() {
|
|
|
226
312
|
<div className="gi-menu-item" aria-disabled="true">No projects yet — start one on the dashboard.</div>
|
|
227
313
|
)}
|
|
228
314
|
{Array.isArray(projects) &&
|
|
229
|
-
projects.map((p) =>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
315
|
+
projects.map((p) =>
|
|
316
|
+
p.role === 'viewer' ? (
|
|
317
|
+
// Viewer dignity (Phase 17 T4): no dead Connect that would
|
|
318
|
+
// 403 at the workspace — viewing has a first-class home in
|
|
319
|
+
// the browser, and the label says why.
|
|
320
|
+
<button
|
|
321
|
+
key={p.id}
|
|
322
|
+
type="button"
|
|
323
|
+
className="gi-menu-item"
|
|
324
|
+
role="menuitem"
|
|
325
|
+
onClick={() => window.open(shareViewUrl(p.url, p.id), '_blank', 'noopener')}
|
|
326
|
+
title="Viewing works in the browser. Editing in the app needs the member role — ask whoever runs the project."
|
|
327
|
+
data-testid={`cloud-project-${p.id}`}
|
|
328
|
+
>
|
|
329
|
+
<Icon name="external" size={15} /> View {p.name || p.id} in the browser
|
|
330
|
+
<span className="gi-menu-login" style={{ marginLeft: 'auto' }}>viewer</span>
|
|
331
|
+
</button>
|
|
332
|
+
) : (
|
|
333
|
+
<button
|
|
334
|
+
key={p.id}
|
|
335
|
+
type="button"
|
|
336
|
+
className="gi-menu-item"
|
|
337
|
+
role="menuitem"
|
|
338
|
+
disabled={busy === p.id}
|
|
339
|
+
onClick={() => connect(p.id)}
|
|
340
|
+
title={`Connect this project to ${p.url} (${p.role})`}
|
|
341
|
+
data-testid={`cloud-project-${p.id}`}
|
|
342
|
+
>
|
|
343
|
+
<Icon name="link" size={15} /> {busy === p.id ? 'Connecting…' : `Connect ${p.name || p.id}`}
|
|
344
|
+
<span className="gi-menu-login" style={{ marginLeft: 'auto' }}>{p.stateLabel}</span>
|
|
345
|
+
</button>
|
|
346
|
+
)
|
|
347
|
+
)}
|
|
244
348
|
<div className="gi-menu-sep" />
|
|
245
349
|
<button type="button" className="gi-menu-item" role="menuitem" onClick={() => window.open('https://cloud.maude.sh', '_blank', 'noopener')}>
|
|
246
350
|
<Icon name="external" size={15} /> Open the dashboard
|