@1agh/maude 0.29.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.
- package/README.md +6 -6
- package/apps/studio/acp/bridge.ts +285 -0
- package/apps/studio/acp/env.ts +48 -0
- package/apps/studio/acp/index.ts +132 -0
- package/apps/studio/acp/probe.ts +112 -0
- package/apps/studio/acp/transcript.ts +149 -0
- package/apps/studio/ai-banner.tsx +0 -1
- package/apps/studio/annotations-align.ts +149 -0
- package/apps/studio/annotations-bindings.ts +197 -0
- package/apps/studio/annotations-context-toolbar.tsx +436 -186
- package/apps/studio/annotations-groups.ts +270 -0
- package/apps/studio/annotations-layer.tsx +1714 -1661
- package/apps/studio/annotations-model.ts +2077 -0
- package/apps/studio/annotations-snap.ts +125 -0
- package/apps/studio/api.ts +433 -182
- package/apps/studio/bin/_png-playwright.mjs +1 -1
- package/apps/studio/bin/annotate.mjs +732 -0
- package/apps/studio/bin/annotate.sh +17 -0
- package/apps/studio/bin/chat-open.sh +44 -0
- package/apps/studio/bin/read-annotations.mjs +152 -17
- package/apps/studio/build.ts +1 -1
- package/apps/studio/canvas-arrowheads.ts +78 -9
- package/apps/studio/canvas-cursors.ts +2 -0
- package/apps/studio/canvas-edit.ts +257 -7
- package/apps/studio/canvas-icons.tsx +105 -0
- package/apps/studio/canvas-lib.tsx +112 -19
- package/apps/studio/canvas-list-watch.ts +177 -0
- package/apps/studio/canvas-shell.tsx +326 -9
- package/apps/studio/client/app.jsx +3579 -250
- package/apps/studio/client/canvas-url.js +5 -0
- package/apps/studio/client/github.js +99 -0
- package/apps/studio/client/index.html +1 -1
- package/apps/studio/client/panels/ChatPanel.jsx +770 -0
- package/apps/studio/client/panels/CollabModelInfographic.jsx +71 -0
- package/apps/studio/client/panels/CreateProject.jsx +334 -0
- package/apps/studio/client/panels/DiffView.jsx +590 -0
- package/apps/studio/client/panels/GitPanel.jsx +767 -0
- package/apps/studio/client/panels/IdentityBar.jsx +294 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +563 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +349 -0
- package/apps/studio/client/panels/acp-runtime.js +286 -0
- package/apps/studio/client/panels/chat-markdown.jsx +138 -0
- package/apps/studio/client/panels/git-grouping.js +86 -0
- package/apps/studio/client/styles/0-reset.css +4 -0
- package/apps/studio/client/styles/3-shell-maude.css +1326 -14
- package/apps/studio/client/styles/4-components.css +39 -0
- package/apps/studio/client/styles/5-maude-overrides.css +41 -2
- package/apps/studio/client/styles/6-acp-chat.css +771 -0
- package/apps/studio/client/styles/_index.css +2 -0
- package/apps/studio/client/tour/collab-tour.js +61 -0
- package/apps/studio/client/tour/overlay.jsx +116 -18
- package/apps/studio/client/tour/usage-tour.js +23 -10
- package/apps/studio/client/whats-new.jsx +25 -10
- package/apps/studio/collab/registry.ts +13 -0
- package/apps/studio/collab/room.ts +36 -0
- package/apps/studio/commands/annotation-strokes-command.ts +1 -1
- package/apps/studio/commands/move-artboards-command.ts +1 -1
- package/apps/studio/comments-overlay.tsx +7 -5
- package/apps/studio/context-menu.tsx +4 -3
- package/apps/studio/contextual-toolbar.tsx +14 -0
- package/apps/studio/cursors-overlay.tsx +37 -21
- package/apps/studio/dist/client.bundle.js +31795 -1829
- package/apps/studio/dist/comment-mount.js +97 -9
- package/apps/studio/dist/styles.css +7938 -1599
- package/apps/studio/dom-selection.ts +115 -0
- package/apps/studio/export-dialog.tsx +3 -3
- package/apps/studio/git/endpoints.ts +338 -0
- package/apps/studio/git/service.ts +1334 -0
- package/apps/studio/git/watch.ts +97 -0
- package/apps/studio/github/endpoints.ts +358 -0
- package/apps/studio/github/service.ts +231 -0
- package/apps/studio/github/token.ts +53 -0
- package/apps/studio/history.ts +20 -3
- package/apps/studio/hmr-broadcast.ts +9 -2
- package/apps/studio/http.ts +508 -6
- package/apps/studio/input-router.tsx +7 -0
- package/apps/studio/inspect.ts +37 -8
- package/apps/studio/participants-chrome.tsx +70 -16
- package/apps/studio/paths.ts +12 -0
- package/apps/studio/scaffold-design.ts +57 -0
- package/apps/studio/server.ts +70 -4
- package/apps/studio/sync/agent.ts +311 -57
- package/apps/studio/sync/codec.ts +69 -0
- package/apps/studio/sync/cold-start.ts +198 -0
- package/apps/studio/sync/connection-state.ts +58 -2
- package/apps/studio/sync/hub-link.ts +137 -0
- package/apps/studio/sync/index.ts +563 -221
- package/apps/studio/sync/journal.ts +190 -0
- package/apps/studio/sync/migrate-seed.ts +202 -44
- package/apps/studio/sync/projection.ts +22 -3
- package/apps/studio/sync/status.ts +15 -4
- package/apps/studio/test/acp-bridge.test.ts +127 -0
- package/apps/studio/test/acp-env.test.ts +65 -0
- package/apps/studio/test/acp-origin-gate.test.ts +78 -0
- package/apps/studio/test/acp-transcript.test.ts +112 -0
- package/apps/studio/test/activity.test.ts +1 -7
- package/apps/studio/test/annotate-write.test.ts +184 -0
- package/apps/studio/test/annotations-align.test.ts +88 -0
- package/apps/studio/test/annotations-bindings.test.ts +124 -0
- package/apps/studio/test/annotations-groups.test.ts +231 -0
- package/apps/studio/test/annotations-snap.test.ts +79 -0
- package/apps/studio/test/canvas-create-api.test.ts +72 -0
- package/apps/studio/test/canvas-edit.test.ts +181 -1
- package/apps/studio/test/canvas-list-watch.test.ts +322 -0
- package/apps/studio/test/canvas-meta-api.test.ts +161 -27
- package/apps/studio/test/canvas-origin-gate.test.ts +43 -0
- package/apps/studio/test/chat-markdown.test.tsx +58 -0
- package/apps/studio/test/collab-bridge.test.ts +0 -2
- package/apps/studio/test/collab-room.test.ts +2 -7
- package/apps/studio/test/collab-session-survival.test.tsx +176 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +5 -0
- package/apps/studio/test/csrf-write-guard.test.ts +78 -0
- package/apps/studio/test/editing-presence.test.ts +103 -0
- package/apps/studio/test/figjam-v3-model.test.ts +342 -0
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +45 -0
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +106 -0
- package/apps/studio/test/git-grouping.test.ts +106 -0
- package/apps/studio/test/git-watch.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +465 -0
- package/apps/studio/test/hub-link.test.ts +69 -0
- package/apps/studio/test/participants-chrome.test.ts +36 -1
- package/apps/studio/test/shared-doc-migrate.test.ts +224 -14
- package/apps/studio/test/sync-agent.test.ts +272 -2
- package/apps/studio/test/sync-codec.test.ts +65 -0
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +187 -0
- package/apps/studio/test/sync-cold-start.test.ts +304 -0
- package/apps/studio/test/sync-connection-state.test.ts +68 -0
- package/apps/studio/test/sync-hardening.test.ts +0 -8
- package/apps/studio/test/sync-hubs-config.test.ts +0 -1
- package/apps/studio/test/sync-incident-replay.test.ts +211 -0
- package/apps/studio/test/sync-journal.test.ts +176 -0
- package/apps/studio/test/sync-runtime.test.ts +360 -5
- package/apps/studio/test/sync-status.test.ts +66 -0
- package/apps/studio/test/tour-overlay.test.tsx +18 -0
- package/apps/studio/test/use-annotation-resize.test.ts +159 -0
- package/apps/studio/test/use-artboard-drag.test.ts +0 -1
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-tool-mode.test.tsx +5 -2
- package/apps/studio/tool-palette.tsx +30 -11
- package/apps/studio/undo-hud.tsx +0 -1
- package/apps/studio/use-agent-presence.tsx +4 -2
- package/apps/studio/use-annotation-resize.tsx +484 -54
- package/apps/studio/use-annotation-selection.tsx +16 -4
- package/apps/studio/use-canvas-activity.tsx +2 -2
- package/apps/studio/use-chrome-visibility.tsx +66 -0
- package/apps/studio/use-collab.tsx +434 -202
- package/apps/studio/use-selection-set.tsx +9 -0
- package/apps/studio/use-tool-mode.tsx +5 -1
- package/apps/studio/whats-new.json +208 -10
- package/apps/studio/whats-new.schema.json +18 -2
- package/apps/studio/ws.ts +44 -1
- package/cli/commands/design-link.test.mjs +84 -0
- package/cli/commands/design.mjs +3 -1
- package/cli/commands/design.test.mjs +5 -1
- package/cli/lib/design-link.mjs +51 -1
- package/cli/lib/gitignore-block.mjs +16 -3
- package/cli/lib/gitignore-block.test.mjs +13 -1
- package/package.json +11 -9
- package/plugins/design/dependencies.json +17 -0
- package/plugins/design/templates/_shell.html +58 -12
package/apps/studio/inspect.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Active-canvas state, selected-element tracking, and HTML injection
|
|
2
2
|
// (inspector overlay + canvas runtime). See plan Task 7 + DDR-007.
|
|
3
3
|
|
|
4
|
-
import path from 'node:path';
|
|
5
4
|
import type { Context } from './context.ts';
|
|
6
5
|
|
|
7
6
|
export interface SelectedElement {
|
|
@@ -356,16 +355,31 @@ const INSPECTOR_SCRIPT = `
|
|
|
356
355
|
new MutationObserver(function(){ startTick(); }).observe(document.documentElement, { subtree: true, attributes: true, attributeFilter: ['style', 'class'], childList: true });
|
|
357
356
|
}
|
|
358
357
|
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
// handler can't swallow
|
|
358
|
+
// Shell chords — keydown fired inside the canvas iframe never reaches the
|
|
359
|
+
// shell's window-scoped listener (iframe keyboard isolation), so each shell
|
|
360
|
+
// chord must be forwarded to the parent. preventDefault is load-bearing for
|
|
361
|
+
// ⌘R: without it the BROWSER reloads the whole shell while focus is in the
|
|
362
|
+
// canvas (the advertised behavior is "reload the active canvas"). Capture
|
|
363
|
+
// phase so canvas-lib's pan/zoom keydown handler can't swallow them first.
|
|
365
364
|
document.addEventListener('keydown', function(e) {
|
|
366
|
-
if ((e.metaKey || e.ctrlKey)
|
|
365
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
366
|
+
var k = (e.key || '').toLowerCase();
|
|
367
|
+
if (k === 'k' && !e.shiftKey) {
|
|
367
368
|
e.preventDefault();
|
|
368
369
|
try { window.parent.postMessage({ dgn: 'toggle-palette' }, '*'); } catch (err) {}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (k === 'r' && !e.shiftKey) {
|
|
373
|
+
e.preventDefault();
|
|
374
|
+
try { window.parent.postMessage({ dgn: 'shell-shortcut', id: 'reload' }, '*'); } catch (err) {}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (e.shiftKey) {
|
|
378
|
+
var id = k === 'i' ? 'inspector' : k === 'm' ? 'comments' : k === 'e' ? 'export' : k === 'h' ? 'handoff' : null;
|
|
379
|
+
if (id) {
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
try { window.parent.postMessage({ dgn: 'shell-shortcut', id: id }, '*'); } catch (err) {}
|
|
382
|
+
}
|
|
369
383
|
}
|
|
370
384
|
}, true);
|
|
371
385
|
|
|
@@ -389,6 +403,21 @@ const INSPECTOR_SCRIPT = `
|
|
|
389
403
|
if (t) t.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
390
404
|
} catch (e) {}
|
|
391
405
|
}
|
|
406
|
+
} else if (m.dgn === 'theme') {
|
|
407
|
+
// Catch the chrome-theme seed the shell posts in reply to our 'loaded'
|
|
408
|
+
// ping (sent below). THIS listener registers during page-load — before
|
|
409
|
+
// the React canvas-shell mounts and adds its OWN 'dgn:theme' listener —
|
|
410
|
+
// so without this branch the shell's reply lands in the gap between
|
|
411
|
+
// page-load and React-mount and is dropped, leaving the canvas stuck on
|
|
412
|
+
// its hardcoded dark default until a manual theme toggle re-broadcasts
|
|
413
|
+
// it (the recurring "open a canvas under a light shell → canvas chrome is
|
|
414
|
+
// dark; toggle twice to fix; next canvas reverts" bug). We set the same
|
|
415
|
+
// attribute the canvas-shell chrome reads (data-maude-theme on <html>);
|
|
416
|
+
// canvas-shell's default-dark guard then sees the value already present
|
|
417
|
+
// and won't override it, and its own listener takes over live toggles.
|
|
418
|
+
if (m.theme === 'light' || m.theme === 'dark') {
|
|
419
|
+
try { document.documentElement.setAttribute('data-maude-theme', m.theme); } catch (e) {}
|
|
420
|
+
}
|
|
392
421
|
}
|
|
393
422
|
/* force-clear is now consumed by canvas-shell.tsx — the inspector
|
|
394
423
|
overlay has no per-element selection state to clear anymore. */
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* own avatar to release. tldraw-style.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { useCallback, useEffect,
|
|
17
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
18
18
|
|
|
19
19
|
import { useViewportControllerContext } from './canvas-lib.tsx';
|
|
20
20
|
import { type AgentPresence, useAgentPresence } from './use-agent-presence.tsx';
|
|
@@ -139,6 +139,35 @@ const CHROME_CSS = `
|
|
|
139
139
|
white-space: normal;
|
|
140
140
|
max-width: 200px;
|
|
141
141
|
}
|
|
142
|
+
/* Phase 30 / DDR-120 — soft editing-presence. A peer (or a bridged agent)
|
|
143
|
+
actively editing this canvas gets a gently-pulsing ring + a ✎ marker in the
|
|
144
|
+
peer's OWN identity hue (--dc-edit-ring, set inline from peer.color) so the
|
|
145
|
+
agent pulses violet, not the indigo accent. A heads-up, never a lock. */
|
|
146
|
+
.dc-participant--editing { animation: dc-participant-edit-pulse 1.8s ease-in-out infinite; }
|
|
147
|
+
@keyframes dc-participant-edit-pulse {
|
|
148
|
+
0%, 100% { box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 0 0 2px var(--dc-edit-ring, var(--maude-hud-accent, oklch(56% 0.170 50))); }
|
|
149
|
+
50% { box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 0 0 4px color-mix(in oklab, var(--dc-edit-ring, var(--maude-hud-accent, oklch(56% 0.170 50))) 45%, transparent); }
|
|
150
|
+
}
|
|
151
|
+
.dc-participant-edit-marker {
|
|
152
|
+
position: absolute;
|
|
153
|
+
right: -3px;
|
|
154
|
+
top: -3px;
|
|
155
|
+
width: 13px;
|
|
156
|
+
height: 13px;
|
|
157
|
+
border-radius: 50%;
|
|
158
|
+
background: var(--maude-chrome-bg-0, #fff);
|
|
159
|
+
color: var(--dc-edit-ring, var(--maude-hud-accent, oklch(56% 0.170 50)));
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: center;
|
|
163
|
+
font-size: 8px;
|
|
164
|
+
line-height: 1;
|
|
165
|
+
box-shadow: 0 0 0 1px rgba(0,0,0,0.14);
|
|
166
|
+
pointer-events: none;
|
|
167
|
+
}
|
|
168
|
+
@media (prefers-reduced-motion: reduce) {
|
|
169
|
+
.dc-participant--editing { animation: none; box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 0 0 2px var(--dc-edit-ring, var(--maude-hud-accent, oklch(56% 0.170 50))); }
|
|
170
|
+
}
|
|
142
171
|
`.trim();
|
|
143
172
|
|
|
144
173
|
function ensureChromeStyles(): void {
|
|
@@ -150,6 +179,17 @@ function ensureChromeStyles(): void {
|
|
|
150
179
|
document.head.appendChild(s);
|
|
151
180
|
}
|
|
152
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Phase 30 / DDR-120 — true when a foreign peer is THIS machine's own
|
|
184
|
+
* server-side editing echo: a peer carrying my own name with editing set but
|
|
185
|
+
* no cursor/selection. That's the room's projection of my own agent/inspector
|
|
186
|
+
* edit (which I already see via the local ai-activity avatar), so I skip it. A
|
|
187
|
+
* remote peer never matches (different name), so they still get the heads-up.
|
|
188
|
+
*/
|
|
189
|
+
function isOwnEditingEcho(peer: ForeignAwareness, myName: string | undefined): boolean {
|
|
190
|
+
return peer.name === myName && !!peer.editing && !peer.cursor && !peer.selection;
|
|
191
|
+
}
|
|
192
|
+
|
|
153
193
|
function initialsFor(name: string): string {
|
|
154
194
|
const cleaned = name.trim();
|
|
155
195
|
if (!cleaned) return '?';
|
|
@@ -169,6 +209,7 @@ interface AvatarProps {
|
|
|
169
209
|
function Avatar({ peer, isFollowing, onToggleFollow }: AvatarProps): JSX.Element {
|
|
170
210
|
const [open, setOpen] = useState(false);
|
|
171
211
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
212
|
+
const editing = !!peer.editing;
|
|
172
213
|
|
|
173
214
|
useEffect(() => {
|
|
174
215
|
if (!open) return;
|
|
@@ -180,11 +221,16 @@ function Avatar({ peer, isFollowing, onToggleFollow }: AvatarProps): JSX.Element
|
|
|
180
221
|
return () => document.removeEventListener('mousedown', onDocClick);
|
|
181
222
|
}, [open]);
|
|
182
223
|
|
|
224
|
+
const cls = `dc-participant${isFollowing ? ' dc-participant--following' : ''}${editing ? ' dc-participant--editing' : ''}`;
|
|
225
|
+
// `--dc-edit-ring` drives the editing pulse/marker off the peer's own hue.
|
|
226
|
+
const style = (
|
|
227
|
+
editing ? { background: peer.color, '--dc-edit-ring': peer.color } : { background: peer.color }
|
|
228
|
+
) as React.CSSProperties;
|
|
183
229
|
return (
|
|
184
230
|
<div
|
|
185
231
|
ref={rootRef}
|
|
186
|
-
className={
|
|
187
|
-
style={
|
|
232
|
+
className={cls}
|
|
233
|
+
style={style}
|
|
188
234
|
onClick={() => setOpen((v) => !v)}
|
|
189
235
|
onKeyDown={(e) => {
|
|
190
236
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
@@ -192,19 +238,22 @@ function Avatar({ peer, isFollowing, onToggleFollow }: AvatarProps): JSX.Element
|
|
|
192
238
|
setOpen((v) => !v);
|
|
193
239
|
}
|
|
194
240
|
}}
|
|
195
|
-
// biome-ignore lint/a11y/useSemanticElements: 28px round chip needs raw <div> + inline bg + custom focus ring; <button> reset is more code than the rule saves.
|
|
196
241
|
role="button"
|
|
197
242
|
tabIndex={0}
|
|
198
|
-
title={peer.name}
|
|
199
|
-
aria-label={peer.name}
|
|
243
|
+
title={editing ? `${peer.name} — editing this canvas` : peer.name}
|
|
244
|
+
aria-label={editing ? `${peer.name}, editing this canvas` : peer.name}
|
|
200
245
|
aria-expanded={open}
|
|
201
246
|
>
|
|
202
247
|
{initialsFor(peer.name)}
|
|
248
|
+
{editing && (
|
|
249
|
+
<span className="dc-participant-edit-marker" aria-hidden="true">
|
|
250
|
+
✎
|
|
251
|
+
</span>
|
|
252
|
+
)}
|
|
203
253
|
{open && (
|
|
204
|
-
// biome-ignore lint/a11y/useKeyWithClickEvents: stop-propagation wrapper; keyboard focus stays in the button child.
|
|
205
|
-
// biome-ignore lint/a11y/useSemanticElements: popover wrapper carries no semantic role; the inner <button> is the actionable element.
|
|
206
254
|
<div className="dc-participant-popover" onClick={(e) => e.stopPropagation()}>
|
|
207
255
|
<div className="dc-participant-popover__name">{peer.name}</div>
|
|
256
|
+
{editing && <div className="dc-participant-popover__sub">Editing this canvas</div>}
|
|
208
257
|
<button
|
|
209
258
|
type="button"
|
|
210
259
|
className={`dc-participant-popover__btn${isFollowing ? ' dc-participant-popover__btn--stop' : ''}`}
|
|
@@ -250,7 +299,6 @@ function AgentAvatar({ agent }: { agent: AgentPresence }): JSX.Element {
|
|
|
250
299
|
setOpen((v) => !v);
|
|
251
300
|
}
|
|
252
301
|
}}
|
|
253
|
-
// biome-ignore lint/a11y/useSemanticElements: matches the peer Avatar chip pattern.
|
|
254
302
|
role="button"
|
|
255
303
|
tabIndex={0}
|
|
256
304
|
title={`${agent.name} (AI agent)`}
|
|
@@ -262,8 +310,6 @@ function AgentAvatar({ agent }: { agent: AgentPresence }): JSX.Element {
|
|
|
262
310
|
✦
|
|
263
311
|
</span>
|
|
264
312
|
{open && (
|
|
265
|
-
// biome-ignore lint/a11y/useKeyWithClickEvents: stop-propagation wrapper; no actionable child.
|
|
266
|
-
// biome-ignore lint/a11y/useSemanticElements: popover wrapper carries no semantic role.
|
|
267
313
|
<div className="dc-participant-popover" onClick={(e) => e.stopPropagation()}>
|
|
268
314
|
<div className="dc-participant-popover__name">{agent.name}</div>
|
|
269
315
|
<div className="dc-participant-popover__sub">AI agent · {agent.author}</div>
|
|
@@ -324,13 +370,21 @@ export function ParticipantsChrome(): JSX.Element | null {
|
|
|
324
370
|
controller.setViewport(v);
|
|
325
371
|
}, [controller, followTarget, peers]);
|
|
326
372
|
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
373
|
+
// Phase 30 / DDR-120 — drop the authoring machine's own server-side editing
|
|
374
|
+
// echo: a foreign peer carrying MY name with editing set but no cursor/
|
|
375
|
+
// selection is the room's projection of my own agent/inspector edit, which I
|
|
376
|
+
// already see via the local ai-activity avatar. A remote peer never matches
|
|
377
|
+
// (different name), so they still get the "X is editing" heads-up.
|
|
378
|
+
const myName = collab?.myName;
|
|
379
|
+
const visiblePeers = peers.filter((p) => !isOwnEditingEcho(p, myName));
|
|
380
|
+
|
|
381
|
+
// Render whenever there's a visible human peer OR an active agent (so an agent
|
|
382
|
+
// shows even with no human collaborators connected).
|
|
383
|
+
if (visiblePeers.length === 0 && !agent) return null;
|
|
330
384
|
|
|
331
385
|
return (
|
|
332
386
|
<div className="dc-participants" aria-label="Active collaborators">
|
|
333
|
-
{
|
|
387
|
+
{visiblePeers.map((p) => (
|
|
334
388
|
<Avatar
|
|
335
389
|
key={p.clientID}
|
|
336
390
|
peer={p}
|
|
@@ -343,4 +397,4 @@ export function ParticipantsChrome(): JSX.Element | null {
|
|
|
343
397
|
);
|
|
344
398
|
}
|
|
345
399
|
|
|
346
|
-
export { initialsFor };
|
|
400
|
+
export { initialsFor, isOwnEditingEcho };
|
package/apps/studio/paths.ts
CHANGED
|
@@ -82,6 +82,18 @@ function isDevServerDir(dir: string): boolean {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function resolveDevServerRoot(): string {
|
|
85
|
+
// (0) Explicit override. Used when the runtime can't be found by walk-up — e.g.
|
|
86
|
+
// the Tauri desktop bundle (DDR-106) ships apps/studio/ as an app resource and
|
|
87
|
+
// sets MAUDE_DEV_SERVER_ROOT to it, because the sidecar binary sits alone in
|
|
88
|
+
// Maude.app/Contents/MacOS/ with no apps/studio/ up-tree (the bundle ships the
|
|
89
|
+
// full source, so `http.ts` is present at the resource root).
|
|
90
|
+
const override = process.env.MAUDE_DEV_SERVER_ROOT;
|
|
91
|
+
// Require the same `http.ts` anchor walk-up uses (not merely a `dist/` dir) so a
|
|
92
|
+
// planted dist-only directory can't hijack the runtime root (security review F4).
|
|
93
|
+
if (override && !isVirtualBunfsPath(override) && isDevServerDir(override)) {
|
|
94
|
+
return override;
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
// (1) Dev mode: import.meta.url is a real file:// path AND lands in the
|
|
86
98
|
// dev-server dir. Common case for `bun run server.ts`, tests, etc.
|
|
87
99
|
const importDir = getImportMetaDir();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// scaffold-design.ts — write a minimal, BOOTABLE `.design/` into a folder (Phase 28).
|
|
2
|
+
//
|
|
3
|
+
// Used by two native-app flows: "New project" (create a GitHub repo → init a local
|
|
4
|
+
// project) and the "open a repo with no Maude design system" fallback ("set it up?").
|
|
5
|
+
// It deliberately scaffolds only the MINIMUM the dev-server needs to boot (per
|
|
6
|
+
// context.ts the only hard requirement is a `.design/` dir; config needs just
|
|
7
|
+
// `name` + `designRoot`). A real design system is still created by /design:setup-ds
|
|
8
|
+
// (agent-driven) — this just gets the project to open instead of crash-looping.
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { basename, join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
const CONFIG_SCHEMA =
|
|
14
|
+
'https://raw.githubusercontent.com/1aGh/maude/main/apps/studio/config.schema.json';
|
|
15
|
+
|
|
16
|
+
/** Whether `dir` is already a Maude project (has `.design/config.json`). */
|
|
17
|
+
export function hasDesign(dir: string): boolean {
|
|
18
|
+
return existsSync(join(dir, '.design', 'config.json'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ScaffoldResult {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Scaffold a minimal bootable `.design/` (no design system yet). Refuses to
|
|
27
|
+
* clobber an existing project. `name` is the human project label. */
|
|
28
|
+
export function scaffoldDesign(dir: string, name?: string): ScaffoldResult {
|
|
29
|
+
const designDir = join(dir, '.design');
|
|
30
|
+
const configPath = join(designDir, 'config.json');
|
|
31
|
+
if (existsSync(configPath)) {
|
|
32
|
+
return { ok: false, error: 'This folder is already a Maude project.' };
|
|
33
|
+
}
|
|
34
|
+
const projectName = (name && name.trim()) || basename(dir) || 'Untitled';
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(join(designDir, 'ui'), { recursive: true });
|
|
37
|
+
mkdirSync(join(designDir, 'system'), { recursive: true });
|
|
38
|
+
const config = {
|
|
39
|
+
$schema: CONFIG_SCHEMA,
|
|
40
|
+
name: projectName,
|
|
41
|
+
designRoot: '.design',
|
|
42
|
+
canvasGroups: [
|
|
43
|
+
{ label: 'Design system', path: 'system' },
|
|
44
|
+
{ label: 'UI kit', path: 'ui' },
|
|
45
|
+
],
|
|
46
|
+
designSystems: [],
|
|
47
|
+
completenessProfile: 'standard',
|
|
48
|
+
};
|
|
49
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
50
|
+
// Keep the empty group dirs in git so a fresh clone still has the structure.
|
|
51
|
+
writeFileSync(join(designDir, 'ui', '.gitkeep'), '', 'utf8');
|
|
52
|
+
writeFileSync(join(designDir, 'system', '.gitkeep'), '', 'utf8');
|
|
53
|
+
return { ok: true };
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return { ok: false, error: e instanceof Error ? e.message : 'Could not set up the project.' };
|
|
56
|
+
}
|
|
57
|
+
}
|
package/apps/studio/server.ts
CHANGED
|
@@ -16,14 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
import { spawn } from 'node:child_process';
|
|
18
18
|
|
|
19
|
+
import { createAcp } from './acp/index.ts';
|
|
19
20
|
import { createActivity } from './activity.ts';
|
|
20
21
|
import { createApi } from './api.ts';
|
|
21
22
|
import { bootSelfHeal } from './boot-self-heal.ts';
|
|
22
|
-
import {
|
|
23
|
+
import { createCanvasListWatch } from './canvas-list-watch.ts';
|
|
24
|
+
import { type AiActivityEntry, createAiActivity } from './collab/ai-activity.ts';
|
|
23
25
|
import { createGitLifecycle } from './collab/git-lifecycle.ts';
|
|
24
26
|
import { createCollab } from './collab/index.ts';
|
|
25
27
|
import { createContext } from './context.ts';
|
|
26
28
|
import { createFsWatch } from './fs-watch.ts';
|
|
29
|
+
import { createGitWatch } from './git/watch.ts';
|
|
27
30
|
import { createHttp } from './http.ts';
|
|
28
31
|
import { createInspect } from './inspect.ts';
|
|
29
32
|
import { startHeapWatch } from './mem.ts';
|
|
@@ -77,11 +80,28 @@ await inspect.load();
|
|
|
77
80
|
|
|
78
81
|
collab = createCollab(ctx, api);
|
|
79
82
|
const aiActivity = createAiActivity(ctx);
|
|
83
|
+
|
|
84
|
+
// Phase 30 — bridge agent `ai-activity` onto the per-canvas room awareness so a
|
|
85
|
+
// remote peer sees "X is editing" cross-machine. The `ai-activity` bus event is
|
|
86
|
+
// loopback-only (inspector WS); awareness is the one channel that crosses the
|
|
87
|
+
// hub. Soft heads-up — projected onto the room's own awareness slot, cleared
|
|
88
|
+
// when the activity ends/expires. No-op when no room is live for the slug.
|
|
89
|
+
ctx.bus.on('ai-activity', (payload: { file: string; entry: AiActivityEntry | null }) => {
|
|
90
|
+
if (!collab) return;
|
|
91
|
+
const slug = api.fileSlug(payload.file);
|
|
92
|
+
collab.registry.setAgentEditing(
|
|
93
|
+
slug,
|
|
94
|
+
payload.entry ? { name: payload.entry.author, since: payload.entry.startedAt } : null
|
|
95
|
+
);
|
|
96
|
+
});
|
|
80
97
|
const gitLifecycle = createGitLifecycle(ctx, collab.registry);
|
|
81
98
|
// Phase 13 / DDR-029 — fs-watch-driven canvas activity overlay. Subscribes to
|
|
82
99
|
// `fs:any` and emits `activity:change`; ws.ts forwards it to canvas iframes.
|
|
83
100
|
const activity = createActivity(ctx);
|
|
84
|
-
|
|
101
|
+
// Phase 31 (DDR-123) — ACP chat bridge manager. Owns one claude-agent-acp
|
|
102
|
+
// subprocess per /_ws/acp socket; main-origin + loopback only (wired below).
|
|
103
|
+
const acp = createAcp(ctx);
|
|
104
|
+
const ws = createWs(ctx, api, inspect, collab, activity, acp);
|
|
85
105
|
const http = createHttp(ctx, api, inspect, aiActivity);
|
|
86
106
|
const fsWatch = createFsWatch(ctx);
|
|
87
107
|
|
|
@@ -141,6 +161,26 @@ function startServer(port: number): BunServer {
|
|
|
141
161
|
return new Response('Upgrade failed', { status: 400 });
|
|
142
162
|
}
|
|
143
163
|
|
|
164
|
+
// Phase 31 (DDR-123) — ACP chat bridge WS. Main origin ONLY (this server,
|
|
165
|
+
// never startCanvasServer) and loopback-guarded like collab: the bridge
|
|
166
|
+
// spawns the user's `claude` and can drive file edits, so the untrusted
|
|
167
|
+
// canvas origin and remote hosts must never reach it. Checked BEFORE the
|
|
168
|
+
// generic `/_ws` inspector branch since `/_ws/acp`.startsWith('/_ws').
|
|
169
|
+
if (pathname === '/_ws/acp') {
|
|
170
|
+
if (!isLoopbackHost(req.headers.get('host'))) {
|
|
171
|
+
return new Response('ACP chat is loopback-only', { status: 403 });
|
|
172
|
+
}
|
|
173
|
+
const ok = srv.upgrade(req, {
|
|
174
|
+
data: {
|
|
175
|
+
id: crypto.randomUUID(),
|
|
176
|
+
remote: req.headers.get('x-forwarded-for') ?? '127.0.0.1',
|
|
177
|
+
kind: 'acp',
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
if (ok) return undefined as unknown as Response;
|
|
181
|
+
return new Response('Upgrade failed', { status: 400 });
|
|
182
|
+
}
|
|
183
|
+
|
|
144
184
|
// Legacy inspector WS — JSON frames, designer-facing live tab state.
|
|
145
185
|
if (pathname.startsWith('/_ws')) {
|
|
146
186
|
const ok = srv.upgrade(req, {
|
|
@@ -294,8 +334,11 @@ let server: BunServer;
|
|
|
294
334
|
|
|
295
335
|
// T2 (9.1-A) — advertise the main origin so the canvas origin's CSP can
|
|
296
336
|
// allowlist it in `frame-ancestors` (the legit embedder). Must be set before
|
|
297
|
-
// the canvas listener serves its first shell.
|
|
298
|
-
|
|
337
|
+
// the canvas listener serves its first shell. Both loopback host spellings are
|
|
338
|
+
// listed: the server binds 127.0.0.1, so a user who opens the printed URL as
|
|
339
|
+
// `127.0.0.1:<port>` is the same legit embedder — with only `localhost` allowed
|
|
340
|
+
// the canvas iframe was silently refused (blank sad-page, no error anywhere).
|
|
341
|
+
ctx.mainOrigin = `http://localhost:${server.port} http://127.0.0.1:${server.port}`;
|
|
299
342
|
|
|
300
343
|
// T2 (9.1-A) — segregated canvas-content origin. ON BY DEFAULT (opt-OUT) since
|
|
301
344
|
// phase-9.1: a second listener binds an OS-assigned free port, advertised as
|
|
@@ -336,6 +379,19 @@ await Bun.write(
|
|
|
336
379
|
fsWatch.start();
|
|
337
380
|
startHeapWatch();
|
|
338
381
|
|
|
382
|
+
// Phase 27 Task 5 (E2) — live dirty-state. Subscribes to `fs:any` (after
|
|
383
|
+
// fsWatch.start so it sees every event) and broadcasts `git-status` to the shell
|
|
384
|
+
// on each versionable change. No-op for a non-git project (gitStatus → repo:false).
|
|
385
|
+
const gitWatch = createGitWatch(ctx);
|
|
386
|
+
|
|
387
|
+
// Phase 31 follow-up — external-canvas list watcher. Subscribes to `fs:any` and
|
|
388
|
+
// emits `canvas-list-update` when a canvas file appears/disappears on disk from
|
|
389
|
+
// OUTSIDE the dev-server (ACP agent `/design:new`, agent Write, git checkout),
|
|
390
|
+
// so the browser file tree refreshes without a reload — the symmetric
|
|
391
|
+
// counterpart to api.ts's create/delete emit. RCA:
|
|
392
|
+
// `.ai/logs/rca/issue-acp-new-canvas-not-in-filetree.md`.
|
|
393
|
+
const canvasListWatch = createCanvasListWatch(ctx);
|
|
394
|
+
|
|
339
395
|
// Phase 9 Task 4 — bidirectional sync agent. No-op when the project isn't
|
|
340
396
|
// linked to a hub (`.design/config.json` has no `linkedHub` field). Kicked
|
|
341
397
|
// off after fsWatch so the agent's bus subscription receives every fs event.
|
|
@@ -368,6 +424,16 @@ if (!process.env.NO_OPEN) {
|
|
|
368
424
|
async function shutdown() {
|
|
369
425
|
console.log('\n Stopping…');
|
|
370
426
|
fsWatch.stop();
|
|
427
|
+
try {
|
|
428
|
+
gitWatch.stop();
|
|
429
|
+
} catch {
|
|
430
|
+
/* timer cleanup is best-effort */
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
canvasListWatch.stop();
|
|
434
|
+
} catch {
|
|
435
|
+
/* timer cleanup is best-effort */
|
|
436
|
+
}
|
|
371
437
|
try {
|
|
372
438
|
activity.stop();
|
|
373
439
|
} catch {
|