@1agh/maude 0.30.0 → 0.31.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.
Files changed (90) hide show
  1. package/README.md +5 -5
  2. package/apps/studio/acp/bridge.ts +285 -0
  3. package/apps/studio/acp/env.ts +48 -0
  4. package/apps/studio/acp/index.ts +132 -0
  5. package/apps/studio/acp/probe.ts +112 -0
  6. package/apps/studio/acp/transcript.ts +149 -0
  7. package/apps/studio/annotations-layer.tsx +6 -1
  8. package/apps/studio/api.ts +225 -66
  9. package/apps/studio/bin/chat-open.sh +44 -0
  10. package/apps/studio/canvas-lib.tsx +112 -19
  11. package/apps/studio/canvas-list-watch.ts +177 -0
  12. package/apps/studio/canvas-shell.tsx +22 -2
  13. package/apps/studio/client/app.jsx +781 -26
  14. package/apps/studio/client/canvas-url.js +5 -0
  15. package/apps/studio/client/github.js +99 -0
  16. package/apps/studio/client/panels/ChatPanel.jsx +770 -0
  17. package/apps/studio/client/panels/CollabModelInfographic.jsx +71 -0
  18. package/apps/studio/client/panels/CreateProject.jsx +334 -0
  19. package/apps/studio/client/panels/DiffView.jsx +590 -0
  20. package/apps/studio/client/panels/GitPanel.jsx +767 -0
  21. package/apps/studio/client/panels/IdentityBar.jsx +294 -0
  22. package/apps/studio/client/panels/OnboardingWizard.jsx +563 -0
  23. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +349 -0
  24. package/apps/studio/client/panels/acp-runtime.js +286 -0
  25. package/apps/studio/client/panels/chat-markdown.jsx +138 -0
  26. package/apps/studio/client/panels/git-grouping.js +86 -0
  27. package/apps/studio/client/styles/0-reset.css +4 -0
  28. package/apps/studio/client/styles/3-shell-maude.css +613 -3
  29. package/apps/studio/client/styles/5-maude-overrides.css +25 -0
  30. package/apps/studio/client/styles/6-acp-chat.css +771 -0
  31. package/apps/studio/client/styles/_index.css +2 -0
  32. package/apps/studio/client/tour/collab-tour.js +61 -0
  33. package/apps/studio/client/tour/overlay.jsx +11 -1
  34. package/apps/studio/client/whats-new.jsx +25 -10
  35. package/apps/studio/collab/registry.ts +13 -0
  36. package/apps/studio/collab/room.ts +36 -0
  37. package/apps/studio/cursors-overlay.tsx +17 -1
  38. package/apps/studio/dist/client.bundle.js +28780 -1534
  39. package/apps/studio/dist/comment-mount.js +4 -2
  40. package/apps/studio/dist/styles.css +5633 -1617
  41. package/apps/studio/git/endpoints.ts +338 -0
  42. package/apps/studio/git/service.ts +1334 -0
  43. package/apps/studio/git/watch.ts +97 -0
  44. package/apps/studio/github/endpoints.ts +358 -0
  45. package/apps/studio/github/service.ts +231 -0
  46. package/apps/studio/github/token.ts +53 -0
  47. package/apps/studio/hmr-broadcast.ts +9 -2
  48. package/apps/studio/http.ts +384 -1
  49. package/apps/studio/participants-chrome.tsx +69 -9
  50. package/apps/studio/paths.ts +12 -0
  51. package/apps/studio/scaffold-design.ts +57 -0
  52. package/apps/studio/server.ts +65 -2
  53. package/apps/studio/sync/agent.ts +81 -1
  54. package/apps/studio/sync/codec.ts +24 -0
  55. package/apps/studio/sync/cold-start.ts +40 -0
  56. package/apps/studio/sync/hub-link.ts +137 -0
  57. package/apps/studio/test/acp-bridge.test.ts +127 -0
  58. package/apps/studio/test/acp-env.test.ts +65 -0
  59. package/apps/studio/test/acp-origin-gate.test.ts +78 -0
  60. package/apps/studio/test/acp-transcript.test.ts +112 -0
  61. package/apps/studio/test/canvas-create-api.test.ts +72 -0
  62. package/apps/studio/test/canvas-list-watch.test.ts +322 -0
  63. package/apps/studio/test/canvas-meta-api.test.ts +161 -27
  64. package/apps/studio/test/canvas-origin-gate.test.ts +35 -0
  65. package/apps/studio/test/chat-markdown.test.tsx +58 -0
  66. package/apps/studio/test/collab-session-survival.test.tsx +176 -0
  67. package/apps/studio/test/csrf-write-guard.test.ts +26 -0
  68. package/apps/studio/test/editing-presence.test.ts +103 -0
  69. package/apps/studio/test/fixtures/mock-acp-agent.mjs +45 -0
  70. package/apps/studio/test/git-api.test.ts +0 -0
  71. package/apps/studio/test/git-branches.test.ts +106 -0
  72. package/apps/studio/test/git-grouping.test.ts +106 -0
  73. package/apps/studio/test/git-watch.test.ts +97 -0
  74. package/apps/studio/test/github-api.test.ts +465 -0
  75. package/apps/studio/test/hub-link.test.ts +69 -0
  76. package/apps/studio/test/participants-chrome.test.ts +36 -1
  77. package/apps/studio/test/sync-cold-seed-dedup.test.ts +187 -0
  78. package/apps/studio/test/sync-cold-start.test.ts +61 -1
  79. package/apps/studio/test/tour-overlay.test.tsx +18 -0
  80. package/apps/studio/tool-palette.tsx +18 -9
  81. package/apps/studio/use-chrome-visibility.tsx +66 -0
  82. package/apps/studio/use-collab.tsx +414 -187
  83. package/apps/studio/whats-new.json +73 -0
  84. package/apps/studio/ws.ts +44 -1
  85. package/cli/commands/design.mjs +1 -0
  86. package/cli/lib/gitignore-block.mjs +16 -3
  87. package/cli/lib/gitignore-block.test.mjs +13 -1
  88. package/package.json +11 -9
  89. package/plugins/design/dependencies.json +17 -0
  90. package/plugins/design/templates/_shell.html +30 -8
@@ -0,0 +1,294 @@
1
+ // Phase 28 (epic E3) Task 6 — the GitHub identity rail (sidebar FOOTER).
2
+ //
3
+ // Compact: just an avatar (signed in) or a small "Sign in with GitHub" button
4
+ // (signed out), docked at the bottom of the sidebar. Clicking the avatar opens an
5
+ // account menu UPWARD (New project / Pull a copy / Share / Sign out). Sign-in runs
6
+ // GitHub's device flow (code modal). All chrome is the shell maude DS — solid
7
+ // surfaces, the .st-dialog scrim+card treatment, no canvas-side `.panel`. In a
8
+ // plain browser (no Tauri shell) the rail renders nothing.
9
+
10
+ import { useEffect, useRef, useState } from 'react';
11
+
12
+ import CreateProject from './CreateProject.jsx';
13
+ import {
14
+ fetchIdentity,
15
+ isNativeApp,
16
+ isSignedIn,
17
+ onDeviceCode,
18
+ openVerification,
19
+ signIn,
20
+ signOut,
21
+ } from '../github.js';
22
+
23
+ function Icon({ name, size = 16 }) {
24
+ const p = {
25
+ 'chevron-up': <polyline points="3.5 10 8 5.5 12.5 10" />,
26
+ external: (
27
+ <>
28
+ <path d="M6 3.5H3.2A.7.7 0 0 0 2.5 4.2v8.6a.7.7 0 0 0 .7.7h8.6a.7.7 0 0 0 .7-.7V10" />
29
+ <line x1="8" y1="8" x2="13" y2="3" />
30
+ <polyline points="9.5 3 13 3 13 6.5" />
31
+ </>
32
+ ),
33
+ copy: (
34
+ <>
35
+ <rect x="5.5" y="5.5" width="7.5" height="7.5" rx="1.2" />
36
+ <path d="M3 10.5V3.2A.7.7 0 0 1 3.7 2.5H10" />
37
+ </>
38
+ ),
39
+ plus: (
40
+ <>
41
+ <line x1="8" y1="3" x2="8" y2="13" />
42
+ <line x1="3" y1="8" x2="13" y2="8" />
43
+ </>
44
+ ),
45
+ download: (
46
+ <>
47
+ <line x1="8" y1="2.5" x2="8" y2="10" />
48
+ <polyline points="4.5 7 8 10.5 11.5 7" />
49
+ <polyline points="3 12.8 3 13.6 13 13.6 13 12.8" />
50
+ </>
51
+ ),
52
+ invite: (
53
+ <>
54
+ <circle cx="6" cy="5.5" r="2.5" />
55
+ <path d="M2 13.5a4 4 0 0 1 8 0" />
56
+ <line x1="13" y1="5" x2="13" y2="9" />
57
+ <line x1="11" y1="7" x2="15" y2="7" />
58
+ </>
59
+ ),
60
+ signout: (
61
+ <>
62
+ <path d="M6.5 13.5H3.2a.7.7 0 0 1-.7-.7V3.2a.7.7 0 0 1 .7-.7h3.3" />
63
+ <line x1="13" y1="8" x2="6.5" y2="8" />
64
+ <polyline points="10 5 13 8 10 11" />
65
+ </>
66
+ ),
67
+ }[name];
68
+ return (
69
+ <svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
70
+ {p}
71
+ </svg>
72
+ );
73
+ }
74
+
75
+ function GitHubMark({ size = 16 }) {
76
+ return (
77
+ <svg width={size} height={size} viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
78
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.6 7.6 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
79
+ </svg>
80
+ );
81
+ }
82
+
83
+ function initialsOf(identity) {
84
+ const src = (identity?.name || identity?.login || '?').trim();
85
+ const parts = src.split(/[\s_-]+/).filter(Boolean);
86
+ const ini = parts.length >= 2 ? parts[0][0] + parts[1][0] : src.slice(0, 2);
87
+ return ini.toUpperCase();
88
+ }
89
+
90
+ function Avatar({ identity, size = 26 }) {
91
+ return (
92
+ <span className="gi-avatar" style={{ width: size, height: size }}>
93
+ {initialsOf(identity)}
94
+ </span>
95
+ );
96
+ }
97
+
98
+ export default function IdentityBar() {
99
+ const native = isNativeApp();
100
+ const [state, setState] = useState('loading'); // loading | out | in
101
+ const [identity, setIdentity] = useState(null);
102
+ const [device, setDevice] = useState(null);
103
+ const [signing, setSigning] = useState(false);
104
+ const [error, setError] = useState('');
105
+ const [menuOpen, setMenuOpen] = useState(false);
106
+ const [view, setView] = useState(null); // null | 'new' | 'get' | 'share'
107
+ const [copied, setCopied] = useState(false);
108
+ const unlistenRef = useRef(null);
109
+ const railRef = useRef(null);
110
+
111
+ useEffect(() => {
112
+ let alive = true;
113
+ if (!native) return;
114
+ (async () => {
115
+ try {
116
+ const signed = await isSignedIn();
117
+ if (!alive) return;
118
+ if (signed) {
119
+ const r = await fetchIdentity();
120
+ if (!alive) return;
121
+ if (r.ok && r.json?.ok) {
122
+ setIdentity({ login: r.json.login, name: r.json.name, avatar_url: r.json.avatar_url });
123
+ setState('in');
124
+ } else setState('out');
125
+ } else setState('out');
126
+ } catch {
127
+ if (alive) setState('out');
128
+ }
129
+ })();
130
+ return () => {
131
+ alive = false;
132
+ unlistenRef.current?.then?.((fn) => fn?.());
133
+ };
134
+ }, [native]);
135
+
136
+ useEffect(() => {
137
+ if (!menuOpen) return;
138
+ const onDoc = (e) => {
139
+ if (railRef.current && !railRef.current.contains(e.target)) setMenuOpen(false);
140
+ };
141
+ document.addEventListener('mousedown', onDoc);
142
+ return () => document.removeEventListener('mousedown', onDoc);
143
+ }, [menuOpen]);
144
+
145
+ async function handleSignIn() {
146
+ setError('');
147
+ setSigning(true);
148
+ setDevice(null);
149
+ try {
150
+ unlistenRef.current = onDeviceCode((payload) => setDevice(payload));
151
+ const login = await signIn();
152
+ const r = await fetchIdentity();
153
+ setIdentity(
154
+ r.ok && r.json?.ok
155
+ ? { login: r.json.login, name: r.json.name, avatar_url: r.json.avatar_url }
156
+ : { login, name: null, avatar_url: null }
157
+ );
158
+ setState('in');
159
+ } catch (e) {
160
+ setError(String(e?.message || e || 'Sign-in didn’t finish. Please try again.'));
161
+ } finally {
162
+ setSigning(false);
163
+ setDevice(null);
164
+ unlistenRef.current?.then?.((fn) => fn?.());
165
+ unlistenRef.current = null;
166
+ }
167
+ }
168
+
169
+ async function handleSignOut() {
170
+ setMenuOpen(false);
171
+ try {
172
+ await signOut();
173
+ } catch {
174
+ /* idempotent */
175
+ }
176
+ setIdentity(null);
177
+ setState('out');
178
+ }
179
+
180
+ function copyCode() {
181
+ if (!device?.user_code) return;
182
+ navigator.clipboard?.writeText(device.user_code).then(
183
+ () => {
184
+ setCopied(true);
185
+ setTimeout(() => setCopied(false), 1500);
186
+ },
187
+ () => {}
188
+ );
189
+ }
190
+
191
+ if (!native) return null;
192
+
193
+ return (
194
+ <div className="gi-rail" ref={railRef}>
195
+ {state === 'loading' && <span className="gi-rail-hint">Checking GitHub…</span>}
196
+
197
+ {state === 'out' && (
198
+ <>
199
+ <button type="button" className="btn btn--primary btn--sm gi-rail-signin" onClick={handleSignIn} disabled={signing}>
200
+ <GitHubMark size={15} /> {signing ? 'Starting…' : 'Sign in with GitHub'}
201
+ </button>
202
+ {error && <span className="gi-rail-err" title={error}>{error}</span>}
203
+ </>
204
+ )}
205
+
206
+ {state === 'in' && (
207
+ <>
208
+ <button
209
+ type="button"
210
+ className={'gi-rail-account' + (menuOpen ? ' is-open' : '')}
211
+ aria-expanded={menuOpen}
212
+ aria-haspopup="menu"
213
+ onClick={() => setMenuOpen((v) => !v)}
214
+ title={`Signed in as @${identity?.login}`}
215
+ >
216
+ <Avatar identity={identity} />
217
+ <span className="gi-rail-login">@{identity?.login}</span>
218
+ <span className="gi-rail-caret">
219
+ <Icon name="chevron-up" size={13} />
220
+ </span>
221
+ </button>
222
+ {menuOpen && (
223
+ <div className="gi-menu" role="menu" aria-label="GitHub account">
224
+ <div className="gi-menu-hd">
225
+ <Avatar identity={identity} size={32} />
226
+ <span className="gi-menu-id">
227
+ <span className="gi-menu-name">{identity?.name || identity?.login}</span>
228
+ <span className="gi-menu-login">@{identity?.login} · connected</span>
229
+ </span>
230
+ </div>
231
+ <button type="button" className="gi-menu-item" role="menuitem" onClick={() => { setMenuOpen(false); setView('new'); }}>
232
+ <Icon name="plus" size={15} /> New project
233
+ </button>
234
+ <button type="button" className="gi-menu-item" role="menuitem" onClick={() => { setMenuOpen(false); setView('get'); }}>
235
+ <Icon name="download" size={15} /> Pull a local copy
236
+ </button>
237
+ <button type="button" className="gi-menu-item" role="menuitem" onClick={() => { setMenuOpen(false); setView('share'); }}>
238
+ <Icon name="invite" size={15} /> Share this project
239
+ </button>
240
+ <div className="gi-menu-sep" />
241
+ <button type="button" className="gi-menu-item gi-menu-item--danger" role="menuitem" onClick={handleSignOut}>
242
+ <Icon name="signout" size={15} /> Sign out
243
+ </button>
244
+ </div>
245
+ )}
246
+ </>
247
+ )}
248
+
249
+ {device && (
250
+ <div className="gi-modal" role="dialog" aria-modal="true" aria-label="Sign in with GitHub" onKeyDown={(e) => { if (e.key === 'Escape') setDevice(null); }}>
251
+ <div className="gi-scrim" aria-hidden="true" onClick={() => setDevice(null)} />
252
+ <div className="gi-dialog gi-dialog--code">
253
+ <div className="gi-dc-head">
254
+ <span className="gi-dc-marks"><GitHubMark size={26} /></span>
255
+ <h2>Sign in with GitHub</h2>
256
+ <p>Maude opened GitHub in your browser. Enter this code to connect your account.</p>
257
+ </div>
258
+ <ol className="gi-dc-steps">
259
+ <li>
260
+ <span className="gi-dc-step-n">1</span>
261
+ <span className="gi-dc-step-tx">
262
+ Go to <span className="gi-dc-url">{(device.verification_uri || 'github.com/login/device').replace(/^https?:\/\//, '')}</span>
263
+ <button type="button" className="btn btn--ghost btn--sm gi-dc-open" onClick={() => openVerification().catch(() => {})}>
264
+ <Icon name="external" size={14} /> Open it again
265
+ </button>
266
+ </span>
267
+ </li>
268
+ <li>
269
+ <span className="gi-dc-step-n">2</span>
270
+ <span className="gi-dc-step-tx">Enter this code to connect Maude</span>
271
+ </li>
272
+ </ol>
273
+ <div className="gi-code">
274
+ <span className="gi-code-val">{device.user_code}</span>
275
+ <button type="button" className="btn btn--ghost gi-code-copy" onClick={copyCode} aria-label="Copy the code">
276
+ <Icon name="copy" size={15} /> {copied ? 'Copied' : 'Copy'}
277
+ </button>
278
+ </div>
279
+ <div className="gi-dc-status" aria-live="polite">
280
+ <span className="gi-pulse" aria-hidden="true" />
281
+ <span>Waiting for you to authorize in your browser…</span>
282
+ </div>
283
+ <div className="gi-dc-foot">
284
+ <button type="button" className="btn btn--ghost" onClick={() => setDevice(null)}>Cancel</button>
285
+ <span className="gi-dc-foot-note">Nothing is stored until you authorize.</span>
286
+ </div>
287
+ </div>
288
+ </div>
289
+ )}
290
+
291
+ {view && <CreateProject view={view} identity={identity} onClose={() => setView(null)} />}
292
+ </div>
293
+ );
294
+ }