@1agh/maude 0.36.0 → 0.37.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.
@@ -178,26 +178,26 @@ function AiReadiness() {
178
178
  }
179
179
 
180
180
  // ── A · Welcome (door picker) ────────────────────────────────────────────────
181
- function Welcome({ onGithub, onLocal, onHub, signing }) {
181
+ function Welcome({ onGithub, onLocal, onHub, signing, signedIn, identity }) {
182
182
  return (
183
183
  <main className="ob-main">
184
184
  <header className="ob-head">
185
185
  <span className="ob-eyebrow">Welcome</span>
186
186
  <h1>How would you like to start?</h1>
187
- <p>Most people sign in with GitHub — it's the simplest way to work with a team.</p>
187
+ <p>{signedIn ? `You're signed in as @${identity?.login || 'GitHub'}. Open a project, or start a new one.` : "Most people sign in with GitHub — it's the simplest way to work with a team."}</p>
188
188
  </header>
189
189
  <div className="ob-doors">
190
- <button type="button" className="ob-door ob-door--primary" aria-label="Continue with GitHub — recommended" onClick={onGithub} disabled={signing}>
190
+ <button type="button" data-testid="ob-door-github" className="ob-door ob-door--primary" aria-label="Continue with GitHub — recommended" onClick={onGithub} disabled={signing}>
191
191
  <span className="ob-door-icon ob-door-icon--gh"><GitHubMark size={26} /></span>
192
192
  <span className="ob-door-tx">
193
193
  <span className="ob-door-title">Continue with GitHub<span className="ob-door-tag">Recommended</span></span>
194
194
  <span className="ob-door-sub">Start a shared project, or open one a teammate shared with you.</span>
195
195
  </span>
196
196
  <span className="ob-door-cta">
197
- <span className="btn btn--primary ob-door-btn"><GitHubMark size={15} /> {signing ? 'Starting…' : 'Sign in with GitHub'}</span>
197
+ <span className="btn btn--primary ob-door-btn"><GitHubMark size={15} /> {signing ? 'Starting…' : signedIn ? 'Continue' : 'Sign in with GitHub'}</span>
198
198
  </span>
199
199
  </button>
200
- <button type="button" className="ob-door" aria-label="Open a folder on this computer" onClick={onLocal}>
200
+ <button type="button" data-testid="ob-door-local" className="ob-door" aria-label="Open a folder on this computer" onClick={onLocal}>
201
201
  <span className="ob-door-icon"><Icon name="folder-open" size={22} /></span>
202
202
  <span className="ob-door-tx">
203
203
  <span className="ob-door-title">Open a folder on this computer</span>
@@ -205,7 +205,7 @@ function Welcome({ onGithub, onLocal, onHub, signing }) {
205
205
  </span>
206
206
  <span className="ob-door-go" aria-hidden="true"><Icon name="chevron-right" size={16} /></span>
207
207
  </button>
208
- <button type="button" className="ob-door ob-door--advanced" aria-label="Connect to a team hub — advanced" onClick={onHub}>
208
+ <button type="button" data-testid="ob-door-hub" className="ob-door ob-door--advanced" aria-label="Connect to a team hub — advanced" onClick={onHub}>
209
209
  <span className="ob-door-icon ob-door-icon--quiet"><Icon name="server" size={18} /></span>
210
210
  <span className="ob-door-tx">
211
211
  <span className="ob-door-title">Connect to a team hub <span className="ob-door-adv">Advanced</span></span>
@@ -214,7 +214,7 @@ function Welcome({ onGithub, onLocal, onHub, signing }) {
214
214
  <span className="ob-door-go" aria-hidden="true"><Icon name="chevron-right" size={15} /></span>
215
215
  </button>
216
216
  </div>
217
- <p className="ob-foot-note">Maude never touches the terminal. Everything here happens in the app.</p>
217
+ <p className="ob-foot-note">Browse, version, and collaborate all in the app, no terminal.</p>
218
218
  <AiReadiness />
219
219
  <CrashOptIn />
220
220
  </main>
@@ -223,7 +223,7 @@ function Welcome({ onGithub, onLocal, onHub, signing }) {
223
223
 
224
224
  function BackBar({ onBack }) {
225
225
  return (
226
- <button type="button" className="ob-back" onClick={onBack}>
226
+ <button type="button" data-testid="ob-back" className="ob-back" onClick={onBack}>
227
227
  <Icon name="back" size={14} /> Back
228
228
  </button>
229
229
  );
@@ -272,7 +272,7 @@ function GitHubDoor({ identity, onBack }) {
272
272
  <h1>Open a project, or start a new one</h1>
273
273
  <p>Pick up a shared project below, or create a fresh one — it's private until you invite someone.</p>
274
274
  </header>
275
- <button type="button" className="ob-create" aria-label="Start a new project" onClick={() => setCreating(true)}>
275
+ <button type="button" data-testid="ob-github-create" className="ob-create" aria-label="Start a new project" onClick={() => setCreating(true)}>
276
276
  <span className="ob-create-icon"><Icon name="plus" size={20} /></span>
277
277
  <span className="ob-create-tx">
278
278
  <span className="ob-create-title">Start a new project</span>
@@ -280,12 +280,12 @@ function GitHubDoor({ identity, onBack }) {
280
280
  </span>
281
281
  <span className="btn btn--primary ob-create-btn"><Icon name="arrow-right" size={15} /> Create</span>
282
282
  </button>
283
- <div className="ob-section-label">Shared with you</div>
283
+ <div className="ob-section-label">Your projects</div>
284
284
  {err && <div className="callout callout--error ob-callout"><span className="ob-callout-glyph" style={{ color: 'var(--status-error)' }}><Icon name="x" /></span><span>{err}</span></div>}
285
285
  {repos === null && <div className="ob-foot-note">Loading your projects…</div>}
286
- {repos && repos.length === 0 && !err && <div className="ob-foot-note">No shared projects yet — create one to get started.</div>}
286
+ {repos && repos.length === 0 && !err && <div className="ob-foot-note">No projects yet — create one to get started.</div>}
287
287
  {repos && repos.length > 0 && (
288
- <div className="ob-repolist" role="group" aria-label="Shared projects you can open">
288
+ <div className="ob-repolist" role="group" aria-label="Projects you can open">
289
289
  {repos.map((r) => {
290
290
  const isBusy = busy === r.full_name;
291
291
  return (
@@ -417,12 +417,12 @@ function LocalDoor({ onBack }) {
417
417
  {err && <div className="callout callout--error ob-callout"><span className="ob-callout-glyph" style={{ color: 'var(--status-error)' }}><Icon name="x" /></span><span>{err}</span></div>}
418
418
  <div className="ob-form-actions">
419
419
  <button type="button" className="btn btn--ghost" onClick={() => setNeedsSetup(null)} disabled={busy}>Pick a different folder</button>
420
- <button type="button" className="btn btn--primary" onClick={setupHere} disabled={busy}><Icon name="folder" size={15} /> {busy ? 'Setting up…' : 'Set up Maude here'}</button>
420
+ <button type="button" data-testid="ob-local-setup" className="btn btn--primary" onClick={setupHere} disabled={busy}><Icon name="folder" size={15} /> {busy ? 'Setting up…' : 'Set up Maude here'}</button>
421
421
  </div>
422
422
  </>
423
423
  ) : (
424
424
  <>
425
- <button type="button" className="ob-drop" onClick={choose} disabled={busy} aria-label="Choose a project folder">
425
+ <button type="button" data-testid="ob-local-choose" className="ob-drop" onClick={choose} disabled={busy} aria-label="Choose a project folder">
426
426
  <span className="ob-drop-glyph"><Icon name="folder-open" size={34} /></span>
427
427
  <span className="ob-drop-title">{busy ? 'Opening…' : 'Choose a project folder'}</span>
428
428
  <span className="ob-drop-or">click to browse</span>
@@ -506,11 +506,11 @@ function DeviceCodeModal({ device, onClose }) {
506
506
  return (
507
507
  <div className="gi-modal" role="dialog" aria-modal="true" aria-label="Sign in with GitHub" onKeyDown={(e) => { if (e.key === 'Escape') onClose(); }}>
508
508
  <div className="gi-scrim" aria-hidden="true" onClick={onClose} />
509
- <div className="gi-dialog gi-dialog--code">
509
+ <div className="gi-dialog gi-dialog--code" data-testid="ob-device-modal">
510
510
  <div className="gi-dc-head">
511
511
  <span className="gi-dc-marks"><GitHubMark size={26} /></span>
512
512
  <h2>Sign in with GitHub</h2>
513
- <p>Maude opened GitHub in your browser. Enter this code to connect your account.</p>
513
+ <p>Maude opened GitHub in your browser. Enter the code there to connect — you'll grant Maude access to create and manage your project repos.</p>
514
514
  </div>
515
515
  <ol className="gi-dc-steps">
516
516
  <li>
@@ -523,7 +523,7 @@ function DeviceCodeModal({ device, onClose }) {
523
523
  <li><span className="gi-dc-step-n">2</span><span className="gi-dc-step-tx">Enter this code to connect Maude</span></li>
524
524
  </ol>
525
525
  <div className="gi-code">
526
- <span className="gi-code-val">{device.user_code}</span>
526
+ <span className="gi-code-val" data-testid="ob-device-code">{device.user_code}</span>
527
527
  <button type="button" className="btn btn--ghost gi-code-copy" onClick={copyCode} aria-label="Copy the code"><Icon name="copy" size={15} /> {copied ? 'Copied' : 'Copy'}</button>
528
528
  </div>
529
529
  <div className="gi-dc-status" aria-live="polite"><span className="gi-pulse" aria-hidden="true" /><span>Waiting for you to authorize in your browser…</span></div>
@@ -542,6 +542,7 @@ export default function OnboardingWizard() {
542
542
  const [device, setDevice] = useState(null);
543
543
  const [err, setErr] = useState('');
544
544
  const unlistenRef = useRef(null);
545
+ const cancelledRef = useRef(false);
545
546
 
546
547
  // If already signed in (re-onboarding), skip the sign-in step on the GitHub door.
547
548
  useEffect(() => {
@@ -560,16 +561,18 @@ export default function OnboardingWizard() {
560
561
 
561
562
  async function handleGithub() {
562
563
  if (signedIn) { setDoor('github'); return; }
564
+ cancelledRef.current = false;
563
565
  setErr(''); setSigning(true); setDevice(null);
564
566
  try {
565
567
  unlistenRef.current = onDeviceCode((p) => setDevice(p));
566
568
  const login = await signIn();
569
+ if (cancelledRef.current) return; // user hit Cancel — don't auto-advance on a late resolve
567
570
  const r = await fetchIdentity();
568
571
  setIdentity(r.ok && r.json?.ok ? { login: r.json.login, name: r.json.name } : { login });
569
572
  setSignedIn(true);
570
573
  setDoor('github');
571
574
  } catch (e) {
572
- setErr(String(e?.message || e || "Sign-in didn't finish. Please try again."));
575
+ if (!cancelledRef.current) setErr(String(e?.message || e || "Sign-in didn't finish. Please try again."));
573
576
  } finally {
574
577
  setSigning(false);
575
578
  setDevice(null);
@@ -581,10 +584,10 @@ export default function OnboardingWizard() {
581
584
  if (!native) return null;
582
585
 
583
586
  return (
584
- <div className="ob-overlay" role="dialog" aria-modal="true" aria-label="Welcome to Maude">
587
+ <div className="ob-overlay" data-testid="onboarding-wizard" role="dialog" aria-modal="true" aria-label="Welcome to Maude">
585
588
  <div className="ob-shell">
586
589
  <Rail signedInAs={signedIn ? identity?.login : null} />
587
- {door === 'welcome' && <Welcome signing={signing} onGithub={handleGithub} onLocal={() => setDoor('local')} onHub={() => setDoor('hub')} />}
590
+ {door === 'welcome' && <Welcome signing={signing} signedIn={signedIn} identity={identity} onGithub={handleGithub} onLocal={() => setDoor('local')} onHub={() => setDoor('hub')} />}
588
591
  {door === 'github' && <GitHubDoor identity={identity} onBack={() => setDoor('welcome')} />}
589
592
  {door === 'local' && <LocalDoor onBack={() => setDoor('welcome')} />}
590
593
  {door === 'hub' && <HubDoor onBack={() => setDoor('welcome')} />}
@@ -592,7 +595,7 @@ export default function OnboardingWizard() {
592
595
  {err && door === 'welcome' && (
593
596
  <div className="ob-toast callout callout--error" role="alert"><span className="ob-callout-glyph" style={{ color: 'var(--status-error)' }}><Icon name="x" /></span><span>{err}</span></div>
594
597
  )}
595
- {device && <DeviceCodeModal device={device} onClose={() => setDevice(null)} />}
598
+ {device && <DeviceCodeModal device={device} onClose={() => { cancelledRef.current = true; setSigning(false); setDevice(null); }} />}
596
599
  </div>
597
600
  );
598
601
  }
@@ -417,7 +417,7 @@ export default function RepoBranchSwitcher({ project, liveBranch }) {
417
417
  <span className="rb-pop-icon"><Icon name="plus" size={14} /></span>
418
418
  <span className="rb-pop-tx">
419
419
  <span className="rb-pop-name">New branch</span>
420
- <span className="rb-pop-sub">a separate line of work off the current branch</span>
420
+ <span className="rb-pop-sub">a separate line of work off what you're looking at now</span>
421
421
  </span>
422
422
  </button>
423
423
  </div>
@@ -435,11 +435,11 @@ export default function RepoBranchSwitcher({ project, liveBranch }) {
435
435
  <button type="button" className="btn btn--ghost btn--sm" onClick={() => { setNewDraft(false); setDraftName(''); setErr(''); }} disabled={busy}>Cancel</button>
436
436
  <button type="button" data-testid="switcher-new-branch-create" className="btn btn--primary btn--sm" onClick={createDraft} disabled={busy || !slug}><Icon name="draft" size={13} /> {busy ? 'Creating…' : 'Create branch'}</button>
437
437
  </div>
438
- <p className="rb-newdraft-hint">A branch is your own line of work off the current branch. Merge it into {sharedName} when you're happy, or throw it away — nothing else changes.</p>
438
+ <p className="rb-newdraft-hint">A branch is your own line of work off what you're looking at now. Merge it into {sharedName} when you're happy, or throw it away — nothing else changes.</p>
439
439
  </div>
440
440
  )}
441
441
 
442
- {switching ? (
442
+ {(switching || folding) ? (
443
443
  <div className="rb-switching" role="status" aria-live="polite">
444
444
  <Icon name="spinner" size={14} className="rb-spin" />
445
445
  <span>{folding ? <>Merging <b>{folding}</b> → {sharedName}…</> : downloading ? <>Downloading <b>{switching}</b>…</> : <>Opening <b>{switching}</b>…</>}</span>
@@ -466,7 +466,7 @@ export default function RepoBranchSwitcher({ project, liveBranch }) {
466
466
  <span className="rb-sheet-icon"><Icon name="arrow-up-to-line" size={20} /></span>
467
467
  <h2 className="rb-sheet-title" id="rb-sheet-title">Merge this branch → {sharedName}</h2>
468
468
  <p className="rb-sheet-body" id="rb-sheet-body">Everything in <b>“{currentDraft?.name || branch}”</b> becomes part of <b>{sharedName}</b> — the default branch everyone shares.</p>
469
- <p className="rb-sheet-meta">Teammates pick it up the next time they Get latest, and this branch is then deleted. Nothing else changes.</p>
469
+ <p className="rb-sheet-meta">Your work is now part of {sharedName}, kept in History. Teammates pick it up the next time they Get latest, and the empty branch is cleared away nothing is lost.</p>
470
470
  {err && <p className="rb-newdraft-err">{err}</p>}
471
471
  <div className="rb-sheet-actions">
472
472
  <button type="button" className="btn btn--ghost" onClick={() => { setFoldConfirm(false); setErr(''); }}>Cancel</button>
@@ -14,7 +14,7 @@
14
14
  // the one honest hard thing, centered.
15
15
  //
16
16
  // Vocabulary is the canonical set ONLY (teaching-model table) — no raw git terms:
17
- // Save changes locally · Publish for everyone · Pull changes · Draft · Shared version.
17
+ // Save version · Publish · Get latest · branch · Shared version (matches the shipped buttons).
18
18
 
19
19
  import CollabModelInfographic from '../panels/CollabModelInfographic.jsx';
20
20
 
@@ -43,8 +43,8 @@ export const COLLAB_TOUR = [
43
43
  {
44
44
  target: "[data-tour='pull']",
45
45
  changes: true,
46
- title: 'Pull changes',
47
- body: 'When teammates publish, Pull brings their work onto your computer so you’re both looking at the same thing.',
46
+ title: 'Get latest',
47
+ body: 'When teammates publish, Get latest brings their work onto your computer so you’re both looking at the same thing.',
48
48
  placement: 'left',
49
49
  },
50
50
  {
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @file commands/edit-source-command.ts — undo entry for inline source edits
3
+ * @scope apps/studio/commands/edit-source-command.ts
4
+ * @purpose Reversible record of a single in-canvas direct edit that writes
5
+ * the canvas `.tsx` source: an inspector CSS commit/reset
6
+ * (`/_api/edit-css`), an inline text rewrite (`/_api/edit-text`), or
7
+ * a custom HTML-attribute edit (`/_api/edit-attr`). Before this
8
+ * command existed these edits were covered by NEITHER the Cmd+Z
9
+ * undo stack NOR the `_history` snapshot stack, so they were
10
+ * unrecoverable in-app (the reported bug). See the undo/redo
11
+ * coverage RCA: `.ai/logs/rca/issue-undo-redo-coverage-gaps.md`.
12
+ *
13
+ * Why a single `edit-source` kind for all three ops. Each is invertible
14
+ * through the SAME endpoint by re-applying the prior value — `editCss(prop,
15
+ * before)` undoes `editCss(prop, after)`; `null` means "remove the inline
16
+ * prop / attr" (the reset path) or "field was unset, undo by reset". Storing
17
+ * the op + before/after as a flat record keeps one builder + one sink.
18
+ *
19
+ * Origin split (DDR-054). This command runs INSIDE the untrusted canvas iframe
20
+ * (where the undo stack lives), but `/_api/edit-*` are main-origin-only — the
21
+ * iframe cannot fetch them. So `do()` / `undo()` route through the injected
22
+ * `editSourceApplyFn` sink, which posts `dgn:'apply-edit'` to the parent shell;
23
+ * the shell performs the privileged write. Records are built per iframe mount
24
+ * from a serializable `CommandRecord` (DDR-050 rev 2) so the stack survives the
25
+ * `mode:'module'` HMR reload that the source write triggers.
26
+ */
27
+
28
+ import type { CommandRecord, EditCommand } from '../undo-stack.ts';
29
+ import { registerCommand } from '../undo-stack.ts';
30
+
31
+ /** Which main-origin edit route this record re-applies through. */
32
+ export type EditSourceOp = 'css' | 'text' | 'attr';
33
+
34
+ export interface EditSourcePayload {
35
+ op: EditSourceOp;
36
+ /** Canvas file path (repo- or designRoot-relative — same string the edit POST used). */
37
+ canvas: string;
38
+ /** `data-cd-id` of the target element. */
39
+ id: string;
40
+ /**
41
+ * CSS property name (`op:'css'`) or HTML attribute name (`op:'attr'`).
42
+ * Empty string for `op:'text'` (the whole text node is the target).
43
+ */
44
+ key: string;
45
+ /** Value before the edit. `null` = the prop/attr was unset (undo ⇒ reset). */
46
+ before: string | null;
47
+ /** Value after the edit. `null` = the edit removed the prop/attr (the reset path). */
48
+ after: string | null;
49
+ }
50
+
51
+ /**
52
+ * One application of an inline edit. The production binding (canvas-shell.tsx)
53
+ * posts `dgn:'apply-edit'` to the parent shell; tests pass a spy. `value` null
54
+ * means "reset" — remove the inline prop / attr (for `op:'text'` a null value
55
+ * never occurs, text always has a body).
56
+ */
57
+ export type EditSourceApplyFn = (apply: {
58
+ op: EditSourceOp;
59
+ canvas: string;
60
+ id: string;
61
+ key: string;
62
+ value: string | null;
63
+ }) => void | Promise<void>;
64
+
65
+ export const EDIT_SOURCE_KIND = 'edit-source';
66
+
67
+ export interface EditSourceCommandInit {
68
+ payload: EditSourcePayload;
69
+ applyFn: EditSourceApplyFn;
70
+ label?: string;
71
+ }
72
+
73
+ export function createEditSourceCommand(init: EditSourceCommandInit): EditCommand {
74
+ const { payload, applyFn } = init;
75
+ const apply = (value: string | null) =>
76
+ applyFn({ op: payload.op, canvas: payload.canvas, id: payload.id, key: payload.key, value });
77
+ return {
78
+ kind: EDIT_SOURCE_KIND,
79
+ label: init.label ?? defaultLabel(payload),
80
+ async do() {
81
+ await apply(payload.after);
82
+ },
83
+ async undo() {
84
+ await apply(payload.before);
85
+ },
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Build a persistable record. The caller (the shell's CssKnobs for css/attr,
91
+ * canvas-shell's text commit for text) has already applied the edit once, so
92
+ * this record is pushed onto the stack via `useUndoStack().record()` — which
93
+ * appends WITHOUT re-running `do()` — not via `push()`.
94
+ */
95
+ export function buildEditSourceRecord(
96
+ payload: EditSourcePayload
97
+ ): CommandRecord<EditSourcePayload> {
98
+ return { kind: EDIT_SOURCE_KIND, label: defaultLabel(payload), payload };
99
+ }
100
+
101
+ // ─────────────────────────────────────────────────────────────────────────────
102
+ // Registry
103
+
104
+ registerCommand<EditSourcePayload>(EDIT_SOURCE_KIND, (record, sinks) => {
105
+ const applyFn = sinks.editSourceApplyFn as EditSourceApplyFn | undefined;
106
+ if (!applyFn) return null;
107
+ return createEditSourceCommand({ payload: record.payload, applyFn, label: record.label });
108
+ });
109
+
110
+ // ─────────────────────────────────────────────────────────────────────────────
111
+ // Internals
112
+
113
+ function defaultLabel(p: EditSourcePayload): string {
114
+ if (p.op === 'text') return 'edit text';
115
+ const verb = p.after == null ? 'reset' : 'edit';
116
+ const what = p.op === 'attr' ? `@${p.key}` : p.key;
117
+ return `${verb} ${what}`;
118
+ }