@1agh/maude 0.36.1 → 0.38.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/README.md +2 -0
- package/apps/studio/acp/bootstrap-brief.ts +93 -0
- package/apps/studio/acp/bridge.ts +114 -1
- package/apps/studio/acp/index.ts +184 -21
- package/apps/studio/acp/plugin-bootstrap.ts +115 -0
- package/apps/studio/acp/transcript.ts +36 -3
- package/apps/studio/activity.ts +45 -0
- package/apps/studio/api.ts +265 -47
- package/apps/studio/bin/_ensure-browser.mjs +305 -0
- package/apps/studio/bin/ensure-browser.sh +26 -0
- package/apps/studio/bin/screenshot.sh +33 -8
- package/apps/studio/bin/smoke.sh +46 -0
- package/apps/studio/canvas-edit.ts +422 -6
- package/apps/studio/canvas-lib.tsx +48 -0
- package/apps/studio/canvas-shell.tsx +684 -12
- package/apps/studio/client/app.jsx +686 -35
- package/apps/studio/client/github.js +6 -0
- package/apps/studio/client/panels/ChatPanel.jsx +593 -31
- package/apps/studio/client/panels/CreateProject.jsx +57 -23
- package/apps/studio/client/panels/GitPanel.jsx +1 -1
- package/apps/studio/client/panels/IdentityBar.jsx +43 -2
- package/apps/studio/client/panels/OnboardingWizard.jsx +24 -21
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +4 -4
- package/apps/studio/client/panels/acp-runtime.js +227 -70
- package/apps/studio/client/panels/chat-context.js +124 -0
- package/apps/studio/client/panels/slash-commands.js +147 -0
- package/apps/studio/client/styles/3-shell-maude.css +15 -0
- package/apps/studio/client/styles/6-acp-chat.css +244 -0
- package/apps/studio/client/tour/collab-tour.js +3 -3
- package/apps/studio/commands/reorder-command.ts +77 -0
- package/apps/studio/config.schema.json +6 -0
- package/apps/studio/dist/client.bundle.js +17 -13
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/github/endpoints.ts +42 -0
- package/apps/studio/hmr-broadcast.ts +27 -19
- package/apps/studio/http.ts +183 -26
- package/apps/studio/inspect.ts +108 -1
- package/apps/studio/paths.ts +32 -0
- package/apps/studio/readiness.ts +79 -30
- package/apps/studio/scaffold-design.ts +95 -2
- package/apps/studio/server.ts +11 -2
- package/apps/studio/test/acp-activity.test.ts +154 -0
- package/apps/studio/test/acp-ai-activity.test.ts +182 -0
- package/apps/studio/test/acp-bootstrap-brief.test.ts +167 -0
- package/apps/studio/test/acp-commands.test.ts +108 -0
- package/apps/studio/test/acp-origin-gate.test.ts +64 -1
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +89 -0
- package/apps/studio/test/acp-session-plugins.test.ts +132 -0
- package/apps/studio/test/acp-transcript.test.ts +53 -0
- package/apps/studio/test/active-state.test.ts +41 -0
- package/apps/studio/test/canvas-freshness-deps.test.ts +64 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +10 -0
- package/apps/studio/test/canvas-reorder.test.ts +211 -0
- package/apps/studio/test/chat-context.test.ts +129 -0
- package/apps/studio/test/csrf-write-guard.test.ts +24 -0
- package/apps/studio/test/edit-suppress.test.ts +170 -0
- package/apps/studio/test/ensure-browser.test.ts +92 -0
- package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +44 -0
- package/apps/studio/test/hmr-broadcast.test.ts +44 -0
- package/apps/studio/test/inspect-selections.test.ts +219 -0
- package/apps/studio/test/paths.test.ts +57 -0
- package/apps/studio/test/readiness.test.ts +83 -13
- package/apps/studio/test/reorder-api.test.ts +210 -0
- package/apps/studio/test/slash-commands.test.ts +117 -0
- package/apps/studio/test/sync-agent.test.ts +9 -2
- package/apps/studio/undo-stack.ts +6 -0
- package/apps/studio/whats-new.json +63 -0
- package/apps/studio/ws.ts +37 -0
- package/cli/commands/design.mjs +1 -0
- package/package.json +11 -9
- package/plugins/design/dependencies.json +3 -3
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { useEffect, useState } from 'react';
|
|
14
14
|
|
|
15
|
-
import { cloneRepo, createProject, initDesign, invite, listRepos, openLocalProject, pickDirectory } from '../github.js';
|
|
15
|
+
import { cloneRepo, createLocalProject, createProject, initDesign, invite, listRepos, openLocalProject, pickDirectory } from '../github.js';
|
|
16
16
|
|
|
17
17
|
function Icon({ name, size = 16 }) {
|
|
18
18
|
const p = {
|
|
@@ -57,6 +57,12 @@ function Icon({ name, size = 16 }) {
|
|
|
57
57
|
</>
|
|
58
58
|
),
|
|
59
59
|
spinner: <path d="M8 2.2a5.8 5.8 0 1 0 5.8 5.8" />,
|
|
60
|
+
laptop: (
|
|
61
|
+
<>
|
|
62
|
+
<rect x="3" y="3.5" width="10" height="7" rx="1" />
|
|
63
|
+
<path d="M1.5 13h13l-1.2-1.8H2.7z" />
|
|
64
|
+
</>
|
|
65
|
+
),
|
|
60
66
|
}[name];
|
|
61
67
|
return (
|
|
62
68
|
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={name === 'spinner' ? 'cp-spin' : undefined}>
|
|
@@ -66,12 +72,12 @@ function Icon({ name, size = 16 }) {
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
const TITLES = {
|
|
69
|
-
new: ['Create a new project', '
|
|
75
|
+
new: ['Create a new project', 'On GitHub, or just locally on your computer.'],
|
|
70
76
|
get: ['Pull a local copy', 'Pick a project, choose where to save it, and open it here.'],
|
|
71
77
|
share: ['Share this project', 'Invite a teammate by their GitHub username.'],
|
|
72
78
|
};
|
|
73
79
|
|
|
74
|
-
export default function CreateProject({ view, identity, onClose }) {
|
|
80
|
+
export default function CreateProject({ view, identity, signedIn, onClose }) {
|
|
75
81
|
const [title, sub] = TITLES[view] || TITLES.new;
|
|
76
82
|
return (
|
|
77
83
|
<div className="cp-modal" role="dialog" aria-modal="true" aria-label={title} onKeyDown={(e) => { if (e.key === 'Escape') onClose(); }}>
|
|
@@ -86,7 +92,7 @@ export default function CreateProject({ view, identity, onClose }) {
|
|
|
86
92
|
<Icon name="x" size={15} />
|
|
87
93
|
</button>
|
|
88
94
|
</div>
|
|
89
|
-
{view === 'new' && <NewView identity={identity} onClose={onClose} />}
|
|
95
|
+
{view === 'new' && <NewView identity={identity} signedIn={signedIn} onClose={onClose} />}
|
|
90
96
|
{view === 'get' && <GetView />}
|
|
91
97
|
{view === 'share' && <ShareView onClose={onClose} />}
|
|
92
98
|
</div>
|
|
@@ -94,20 +100,24 @@ export default function CreateProject({ view, identity, onClose }) {
|
|
|
94
100
|
);
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
function NewView({ identity, onClose }) {
|
|
103
|
+
function NewView({ identity, signedIn, onClose }) {
|
|
98
104
|
const [name, setName] = useState('');
|
|
105
|
+
// 'github' = new repo on GitHub (needs sign-in); 'local' = just a local git repo,
|
|
106
|
+
// no remote. Default to local when signed out (GitHub isn't available then).
|
|
107
|
+
const [where, setWhere] = useState(signedIn ? 'github' : 'local');
|
|
99
108
|
const [isPrivate, setIsPrivate] = useState(true);
|
|
100
109
|
const [desc, setDesc] = useState('');
|
|
101
110
|
const [busy, setBusy] = useState(false);
|
|
102
111
|
const [step, setStep] = useState('');
|
|
103
112
|
const [err, setErr] = useState('');
|
|
104
113
|
|
|
114
|
+
const local = where === 'local';
|
|
105
115
|
const slug = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
106
116
|
const owner = identity?.login || 'you';
|
|
107
117
|
|
|
108
|
-
// New project =
|
|
109
|
-
//
|
|
110
|
-
// and Publish when ready.
|
|
118
|
+
// New project = a ready-to-design local project, then open it. GitHub mode also
|
|
119
|
+
// creates the remote repo + sets origin; local mode is git init only (publish later).
|
|
120
|
+
// You pick where to save it, then design and Publish when ready.
|
|
111
121
|
async function submit() {
|
|
112
122
|
setErr('');
|
|
113
123
|
setBusy(true);
|
|
@@ -115,8 +125,14 @@ function NewView({ identity, onClose }) {
|
|
|
115
125
|
setStep('Choose where to save it…');
|
|
116
126
|
const parentDir = await pickDirectory();
|
|
117
127
|
if (!parentDir) { setBusy(false); setStep(''); return; } // cancelled
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
let r;
|
|
129
|
+
if (local) {
|
|
130
|
+
setStep('Setting up your local project…');
|
|
131
|
+
r = await createLocalProject({ name, parentDir });
|
|
132
|
+
} else {
|
|
133
|
+
setStep('Creating your project on GitHub…');
|
|
134
|
+
r = await createProject({ name, private: isPrivate, description: desc, parentDir });
|
|
135
|
+
}
|
|
120
136
|
if (!(r.ok && r.json?.ok)) {
|
|
121
137
|
setErr(r.json?.error || 'Couldn’t create the project. Try again.');
|
|
122
138
|
setBusy(false);
|
|
@@ -135,30 +151,48 @@ function NewView({ identity, onClose }) {
|
|
|
135
151
|
return (
|
|
136
152
|
<>
|
|
137
153
|
<div className="cp-body">
|
|
138
|
-
<label className="cp-field">
|
|
139
|
-
<span className="cp-field-label">Project name</span>
|
|
140
|
-
<input className="input cp-input" type="text" value={name} placeholder="Acme Rebrand" aria-label="Project name" onChange={(e) => setName(e.target.value)} />
|
|
141
|
-
{slug && <span className="cp-field-help">Creates <b>github.com/{owner}/{slug}</b></span>}
|
|
142
|
-
</label>
|
|
143
154
|
<div className="cp-field">
|
|
144
|
-
<span className="cp-field-label">
|
|
145
|
-
<div className="seg cp-seg" role="group" aria-label="
|
|
146
|
-
<button type="button" aria-pressed={
|
|
147
|
-
<button type="button" aria-pressed={
|
|
155
|
+
<span className="cp-field-label">Where</span>
|
|
156
|
+
<div className="seg cp-seg" role="group" aria-label="Where to create the project">
|
|
157
|
+
<button type="button" aria-pressed={!local} disabled={!signedIn} title={signedIn ? undefined : 'Sign in with GitHub first'} onClick={() => setWhere('github')}><Icon name="globe" size={14} /> GitHub</button>
|
|
158
|
+
<button type="button" aria-pressed={local} onClick={() => setWhere('local')}><Icon name="laptop" size={14} /> This computer only</button>
|
|
148
159
|
</div>
|
|
149
|
-
<span className="cp-field-help">
|
|
160
|
+
<span className="cp-field-help">
|
|
161
|
+
{local
|
|
162
|
+
? 'A local git repo on your computer — no GitHub, no remote. You can publish it later.'
|
|
163
|
+
: signedIn
|
|
164
|
+
? 'A repo on your GitHub account, cloned to your computer.'
|
|
165
|
+
: 'Sign in with GitHub (bottom-left) to publish. For now you can create a local project.'}
|
|
166
|
+
</span>
|
|
150
167
|
</div>
|
|
151
168
|
<label className="cp-field">
|
|
152
|
-
<span className="cp-field-label">
|
|
153
|
-
<
|
|
169
|
+
<span className="cp-field-label">Project name</span>
|
|
170
|
+
<input className="input cp-input" type="text" value={name} placeholder="Acme Rebrand" aria-label="Project name" onChange={(e) => setName(e.target.value)} />
|
|
171
|
+
{slug && <span className="cp-field-help">{local ? <>Creates a project folder <b>{slug}</b></> : <>Creates <b>github.com/{owner}/{slug}</b></>}</span>}
|
|
154
172
|
</label>
|
|
173
|
+
{!local && (
|
|
174
|
+
<>
|
|
175
|
+
<div className="cp-field">
|
|
176
|
+
<span className="cp-field-label">Who can see it</span>
|
|
177
|
+
<div className="seg cp-seg" role="group" aria-label="Project visibility">
|
|
178
|
+
<button type="button" aria-pressed={isPrivate} onClick={() => setIsPrivate(true)}><Icon name="lock" size={14} /> Private</button>
|
|
179
|
+
<button type="button" aria-pressed={!isPrivate} onClick={() => setIsPrivate(false)}><Icon name="globe" size={14} /> Public</button>
|
|
180
|
+
</div>
|
|
181
|
+
<span className="cp-field-help">{isPrivate ? 'Only you and people you invite. The safe default.' : 'Anyone on the internet can see this project.'}</span>
|
|
182
|
+
</div>
|
|
183
|
+
<label className="cp-field">
|
|
184
|
+
<span className="cp-field-label">Description <span className="cp-optional">optional</span></span>
|
|
185
|
+
<textarea className="textarea cp-textarea" rows={2} value={desc} placeholder="What is this project for?" aria-label="Project description" onChange={(e) => setDesc(e.target.value)} />
|
|
186
|
+
</label>
|
|
187
|
+
</>
|
|
188
|
+
)}
|
|
155
189
|
{err && <div className="callout callout--error"><span className="cp-cl-glyph" style={{ color: 'var(--status-error)' }}><Icon name="x" /></span><span>{err}</span></div>}
|
|
156
190
|
</div>
|
|
157
191
|
<div className="cp-ft">
|
|
158
192
|
<span className="cp-spacer" />
|
|
159
193
|
<button type="button" className="btn btn--ghost" onClick={onClose}>Cancel</button>
|
|
160
194
|
<button type="button" className="btn btn--primary" onClick={submit} disabled={busy || !slug}>
|
|
161
|
-
<Icon name="plus" size={15} /> {busy ? step || 'Creating…' : 'Create project'}
|
|
195
|
+
<Icon name="plus" size={15} /> {busy ? step || 'Creating…' : local ? 'Create local project' : 'Create project'}
|
|
162
196
|
</button>
|
|
163
197
|
</div>
|
|
164
198
|
</>
|
|
@@ -343,7 +343,7 @@ export default function GitPanel({
|
|
|
343
343
|
if (res.authRequired)
|
|
344
344
|
setBanner({
|
|
345
345
|
variant: 'info',
|
|
346
|
-
text: res.error || 'Sign in with GitHub to publish
|
|
346
|
+
text: res.error || 'Sign in with GitHub to publish.',
|
|
347
347
|
});
|
|
348
348
|
else if (res.conflict)
|
|
349
349
|
// A push conflict (non-fast-forward) prompts a Get-latest; a Get-latest
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
isNativeApp,
|
|
16
16
|
isSignedIn,
|
|
17
17
|
onDeviceCode,
|
|
18
|
+
onMenuNewProject,
|
|
19
|
+
onSignedIn,
|
|
18
20
|
openVerification,
|
|
19
21
|
signIn,
|
|
20
22
|
signOut,
|
|
@@ -107,6 +109,8 @@ export default function IdentityBar() {
|
|
|
107
109
|
const [copied, setCopied] = useState(false);
|
|
108
110
|
const unlistenRef = useRef(null);
|
|
109
111
|
const railRef = useRef(null);
|
|
112
|
+
const stateRef = useRef(state);
|
|
113
|
+
stateRef.current = state; // keep current for the (subscribe-once) menu listener
|
|
110
114
|
|
|
111
115
|
useEffect(() => {
|
|
112
116
|
let alive = true;
|
|
@@ -116,7 +120,14 @@ export default function IdentityBar() {
|
|
|
116
120
|
const signed = await isSignedIn();
|
|
117
121
|
if (!alive) return;
|
|
118
122
|
if (signed) {
|
|
119
|
-
|
|
123
|
+
let r = await fetchIdentity();
|
|
124
|
+
// A valid keychain token can still hit a transient identity-fetch failure at
|
|
125
|
+
// mount (bridge not ready / GitHub 5xx / rate-limit) — retry before falling back
|
|
126
|
+
// to the signed-out CTA so we don't flash "Sign in" at a signed-in user.
|
|
127
|
+
for (let i = 0; i < 2 && alive && !(r.ok && r.json?.ok); i += 1) {
|
|
128
|
+
await new Promise((res) => setTimeout(res, 800));
|
|
129
|
+
r = await fetchIdentity();
|
|
130
|
+
}
|
|
120
131
|
if (!alive) return;
|
|
121
132
|
if (r.ok && r.json?.ok) {
|
|
122
133
|
setIdentity({ login: r.json.login, name: r.json.name, avatar_url: r.json.avatar_url });
|
|
@@ -133,6 +144,36 @@ export default function IdentityBar() {
|
|
|
133
144
|
};
|
|
134
145
|
}, [native]);
|
|
135
146
|
|
|
147
|
+
// Live-update when sign-in completes in ANOTHER surface (the wizard's Door A emits
|
|
148
|
+
// `github://signed-in`). Without this the rail can sit on "Sign in" with a valid token
|
|
149
|
+
// until a full reload. Flip to signed-in off the event's login, then enrich the profile.
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!native) return undefined;
|
|
152
|
+
const p = onSignedIn(async (login) => {
|
|
153
|
+
setIdentity((cur) => cur || { login, name: null, avatar_url: null });
|
|
154
|
+
setState('in');
|
|
155
|
+
const r = await fetchIdentity();
|
|
156
|
+
if (r.ok && r.json?.ok)
|
|
157
|
+
setIdentity({ login: r.json.login, name: r.json.name, avatar_url: r.json.avatar_url });
|
|
158
|
+
});
|
|
159
|
+
return () => {
|
|
160
|
+
p?.then?.((fn) => fn?.());
|
|
161
|
+
};
|
|
162
|
+
}, [native]);
|
|
163
|
+
|
|
164
|
+
// Native File ▸ New Project… (menu.rs emits `menu://new-project`). Open the create
|
|
165
|
+
// dialog once the GitHub check settled — even when signed out, since it offers a
|
|
166
|
+
// local-only project (no GitHub needed); the dialog gates the GitHub option itself.
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!native) return undefined;
|
|
169
|
+
const p = onMenuNewProject(() => {
|
|
170
|
+
if (stateRef.current !== 'loading') setView('new');
|
|
171
|
+
});
|
|
172
|
+
return () => {
|
|
173
|
+
p?.then?.((fn) => fn?.());
|
|
174
|
+
};
|
|
175
|
+
}, [native]);
|
|
176
|
+
|
|
136
177
|
useEffect(() => {
|
|
137
178
|
if (!menuOpen) return;
|
|
138
179
|
const onDoc = (e) => {
|
|
@@ -288,7 +329,7 @@ export default function IdentityBar() {
|
|
|
288
329
|
</div>
|
|
289
330
|
)}
|
|
290
331
|
|
|
291
|
-
{view && <CreateProject view={view} identity={identity} onClose={() => setView(null)} />}
|
|
332
|
+
{view && <CreateProject view={view} identity={identity} signedIn={state === 'in'} onClose={() => setView(null)} />}
|
|
292
333
|
</div>
|
|
293
334
|
);
|
|
294
335
|
}
|
|
@@ -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
|
|
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">
|
|
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">
|
|
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
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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>
|