@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.
Files changed (162) hide show
  1. package/README.md +6 -6
  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/ai-banner.tsx +0 -1
  8. package/apps/studio/annotations-align.ts +149 -0
  9. package/apps/studio/annotations-bindings.ts +197 -0
  10. package/apps/studio/annotations-context-toolbar.tsx +436 -186
  11. package/apps/studio/annotations-groups.ts +270 -0
  12. package/apps/studio/annotations-layer.tsx +1714 -1661
  13. package/apps/studio/annotations-model.ts +2077 -0
  14. package/apps/studio/annotations-snap.ts +125 -0
  15. package/apps/studio/api.ts +433 -182
  16. package/apps/studio/bin/_png-playwright.mjs +1 -1
  17. package/apps/studio/bin/annotate.mjs +732 -0
  18. package/apps/studio/bin/annotate.sh +17 -0
  19. package/apps/studio/bin/chat-open.sh +44 -0
  20. package/apps/studio/bin/read-annotations.mjs +152 -17
  21. package/apps/studio/build.ts +1 -1
  22. package/apps/studio/canvas-arrowheads.ts +78 -9
  23. package/apps/studio/canvas-cursors.ts +2 -0
  24. package/apps/studio/canvas-edit.ts +257 -7
  25. package/apps/studio/canvas-icons.tsx +105 -0
  26. package/apps/studio/canvas-lib.tsx +112 -19
  27. package/apps/studio/canvas-list-watch.ts +177 -0
  28. package/apps/studio/canvas-shell.tsx +326 -9
  29. package/apps/studio/client/app.jsx +3579 -250
  30. package/apps/studio/client/canvas-url.js +5 -0
  31. package/apps/studio/client/github.js +99 -0
  32. package/apps/studio/client/index.html +1 -1
  33. package/apps/studio/client/panels/ChatPanel.jsx +770 -0
  34. package/apps/studio/client/panels/CollabModelInfographic.jsx +71 -0
  35. package/apps/studio/client/panels/CreateProject.jsx +334 -0
  36. package/apps/studio/client/panels/DiffView.jsx +590 -0
  37. package/apps/studio/client/panels/GitPanel.jsx +767 -0
  38. package/apps/studio/client/panels/IdentityBar.jsx +294 -0
  39. package/apps/studio/client/panels/OnboardingWizard.jsx +563 -0
  40. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +349 -0
  41. package/apps/studio/client/panels/acp-runtime.js +286 -0
  42. package/apps/studio/client/panels/chat-markdown.jsx +138 -0
  43. package/apps/studio/client/panels/git-grouping.js +86 -0
  44. package/apps/studio/client/styles/0-reset.css +4 -0
  45. package/apps/studio/client/styles/3-shell-maude.css +1326 -14
  46. package/apps/studio/client/styles/4-components.css +39 -0
  47. package/apps/studio/client/styles/5-maude-overrides.css +41 -2
  48. package/apps/studio/client/styles/6-acp-chat.css +771 -0
  49. package/apps/studio/client/styles/_index.css +2 -0
  50. package/apps/studio/client/tour/collab-tour.js +61 -0
  51. package/apps/studio/client/tour/overlay.jsx +116 -18
  52. package/apps/studio/client/tour/usage-tour.js +23 -10
  53. package/apps/studio/client/whats-new.jsx +25 -10
  54. package/apps/studio/collab/registry.ts +13 -0
  55. package/apps/studio/collab/room.ts +36 -0
  56. package/apps/studio/commands/annotation-strokes-command.ts +1 -1
  57. package/apps/studio/commands/move-artboards-command.ts +1 -1
  58. package/apps/studio/comments-overlay.tsx +7 -5
  59. package/apps/studio/context-menu.tsx +4 -3
  60. package/apps/studio/contextual-toolbar.tsx +14 -0
  61. package/apps/studio/cursors-overlay.tsx +37 -21
  62. package/apps/studio/dist/client.bundle.js +31795 -1829
  63. package/apps/studio/dist/comment-mount.js +97 -9
  64. package/apps/studio/dist/styles.css +7938 -1599
  65. package/apps/studio/dom-selection.ts +115 -0
  66. package/apps/studio/export-dialog.tsx +3 -3
  67. package/apps/studio/git/endpoints.ts +338 -0
  68. package/apps/studio/git/service.ts +1334 -0
  69. package/apps/studio/git/watch.ts +97 -0
  70. package/apps/studio/github/endpoints.ts +358 -0
  71. package/apps/studio/github/service.ts +231 -0
  72. package/apps/studio/github/token.ts +53 -0
  73. package/apps/studio/history.ts +20 -3
  74. package/apps/studio/hmr-broadcast.ts +9 -2
  75. package/apps/studio/http.ts +508 -6
  76. package/apps/studio/input-router.tsx +7 -0
  77. package/apps/studio/inspect.ts +37 -8
  78. package/apps/studio/participants-chrome.tsx +70 -16
  79. package/apps/studio/paths.ts +12 -0
  80. package/apps/studio/scaffold-design.ts +57 -0
  81. package/apps/studio/server.ts +70 -4
  82. package/apps/studio/sync/agent.ts +311 -57
  83. package/apps/studio/sync/codec.ts +69 -0
  84. package/apps/studio/sync/cold-start.ts +198 -0
  85. package/apps/studio/sync/connection-state.ts +58 -2
  86. package/apps/studio/sync/hub-link.ts +137 -0
  87. package/apps/studio/sync/index.ts +563 -221
  88. package/apps/studio/sync/journal.ts +190 -0
  89. package/apps/studio/sync/migrate-seed.ts +202 -44
  90. package/apps/studio/sync/projection.ts +22 -3
  91. package/apps/studio/sync/status.ts +15 -4
  92. package/apps/studio/test/acp-bridge.test.ts +127 -0
  93. package/apps/studio/test/acp-env.test.ts +65 -0
  94. package/apps/studio/test/acp-origin-gate.test.ts +78 -0
  95. package/apps/studio/test/acp-transcript.test.ts +112 -0
  96. package/apps/studio/test/activity.test.ts +1 -7
  97. package/apps/studio/test/annotate-write.test.ts +184 -0
  98. package/apps/studio/test/annotations-align.test.ts +88 -0
  99. package/apps/studio/test/annotations-bindings.test.ts +124 -0
  100. package/apps/studio/test/annotations-groups.test.ts +231 -0
  101. package/apps/studio/test/annotations-snap.test.ts +79 -0
  102. package/apps/studio/test/canvas-create-api.test.ts +72 -0
  103. package/apps/studio/test/canvas-edit.test.ts +181 -1
  104. package/apps/studio/test/canvas-list-watch.test.ts +322 -0
  105. package/apps/studio/test/canvas-meta-api.test.ts +161 -27
  106. package/apps/studio/test/canvas-origin-gate.test.ts +43 -0
  107. package/apps/studio/test/chat-markdown.test.tsx +58 -0
  108. package/apps/studio/test/collab-bridge.test.ts +0 -2
  109. package/apps/studio/test/collab-room.test.ts +2 -7
  110. package/apps/studio/test/collab-session-survival.test.tsx +176 -0
  111. package/apps/studio/test/csp-canvas-shell.test.ts +5 -0
  112. package/apps/studio/test/csrf-write-guard.test.ts +78 -0
  113. package/apps/studio/test/editing-presence.test.ts +103 -0
  114. package/apps/studio/test/figjam-v3-model.test.ts +342 -0
  115. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
  116. package/apps/studio/test/fixtures/mock-acp-agent.mjs +45 -0
  117. package/apps/studio/test/git-api.test.ts +0 -0
  118. package/apps/studio/test/git-branches.test.ts +106 -0
  119. package/apps/studio/test/git-grouping.test.ts +106 -0
  120. package/apps/studio/test/git-watch.test.ts +97 -0
  121. package/apps/studio/test/github-api.test.ts +465 -0
  122. package/apps/studio/test/hub-link.test.ts +69 -0
  123. package/apps/studio/test/participants-chrome.test.ts +36 -1
  124. package/apps/studio/test/shared-doc-migrate.test.ts +224 -14
  125. package/apps/studio/test/sync-agent.test.ts +272 -2
  126. package/apps/studio/test/sync-codec.test.ts +65 -0
  127. package/apps/studio/test/sync-cold-seed-dedup.test.ts +187 -0
  128. package/apps/studio/test/sync-cold-start.test.ts +304 -0
  129. package/apps/studio/test/sync-connection-state.test.ts +68 -0
  130. package/apps/studio/test/sync-hardening.test.ts +0 -8
  131. package/apps/studio/test/sync-hubs-config.test.ts +0 -1
  132. package/apps/studio/test/sync-incident-replay.test.ts +211 -0
  133. package/apps/studio/test/sync-journal.test.ts +176 -0
  134. package/apps/studio/test/sync-runtime.test.ts +360 -5
  135. package/apps/studio/test/sync-status.test.ts +66 -0
  136. package/apps/studio/test/tour-overlay.test.tsx +18 -0
  137. package/apps/studio/test/use-annotation-resize.test.ts +159 -0
  138. package/apps/studio/test/use-artboard-drag.test.ts +0 -1
  139. package/apps/studio/test/use-collab.test.ts +0 -0
  140. package/apps/studio/test/use-tool-mode.test.tsx +5 -2
  141. package/apps/studio/tool-palette.tsx +30 -11
  142. package/apps/studio/undo-hud.tsx +0 -1
  143. package/apps/studio/use-agent-presence.tsx +4 -2
  144. package/apps/studio/use-annotation-resize.tsx +484 -54
  145. package/apps/studio/use-annotation-selection.tsx +16 -4
  146. package/apps/studio/use-canvas-activity.tsx +2 -2
  147. package/apps/studio/use-chrome-visibility.tsx +66 -0
  148. package/apps/studio/use-collab.tsx +434 -202
  149. package/apps/studio/use-selection-set.tsx +9 -0
  150. package/apps/studio/use-tool-mode.tsx +5 -1
  151. package/apps/studio/whats-new.json +208 -10
  152. package/apps/studio/whats-new.schema.json +18 -2
  153. package/apps/studio/ws.ts +44 -1
  154. package/cli/commands/design-link.test.mjs +84 -0
  155. package/cli/commands/design.mjs +3 -1
  156. package/cli/commands/design.test.mjs +5 -1
  157. package/cli/lib/design-link.mjs +51 -1
  158. package/cli/lib/gitignore-block.mjs +16 -3
  159. package/cli/lib/gitignore-block.test.mjs +13 -1
  160. package/package.json +11 -9
  161. package/plugins/design/dependencies.json +17 -0
  162. package/plugins/design/templates/_shell.html +58 -12
@@ -30,4 +30,6 @@
30
30
  @import url('./4-components-maude.css');
31
31
  /* Plan B — maude redesign of remaining legacy shell surfaces (HelpModal, tour, what's-new, SystemView, CommentBar). */
32
32
  @import url('./5-maude-overrides.css');
33
+ /* Phase 31 (DDR-123) — ACP chat sidepanel (chat-* inside the Assistant st-rpanel). */
34
+ @import url('./6-acp-chat.css');
33
35
  @import url('./5-utilities.css');
@@ -0,0 +1,61 @@
1
+ // collab-tour.js — the version-control "rychlý kurz" (Phase 29, epic E4).
2
+ //
3
+ // Teaches the two-layer collab model on the EXISTING tour engine (overlay.jsx) —
4
+ // never a fork. Per the teaching-model DDR (this phase REVERSES the "hide the
5
+ // duality" recommendation in collab-model-design.md § Part 2): we surface a visible
6
+ // three-verb action-cycle and teach the live-vs-async split honestly, because the
7
+ // cycle has to match real buttons in the app and pure-hiding is leaky.
8
+ //
9
+ // Step 1 is a centered, target-less step whose graphic is the two-layer infographic
10
+ // (the engine already centers target-less steps; the `render` field is the only
11
+ // addition). Steps 2-4 spotlight the real action controls via stable [data-tour]
12
+ // anchors (save-local / publish / pull) — `changes: true` opens the Changes panel
13
+ // first so the buttons exist. Step 5 points at the live presence layer; step 6 is
14
+ // the one honest hard thing, centered.
15
+ //
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.
18
+
19
+ import CollabModelInfographic from '../panels/CollabModelInfographic.jsx';
20
+
21
+ export const COLLAB_TOUR = [
22
+ {
23
+ // Centered, no target — the engine renders target-less steps centered, and the
24
+ // `render` field draws the infographic above the copy.
25
+ render: CollabModelInfographic,
26
+ title: 'Working together, in one picture',
27
+ body: 'There are two layers. Up top, being together just happens. Below, your work moves through three simple steps. Let’s walk them.',
28
+ },
29
+ {
30
+ target: "[data-tour='save-local']",
31
+ changes: true,
32
+ title: 'Save changes locally',
33
+ body: 'When something looks right, save it. That keeps a version on your computer you can always come back to — like a checkpoint, just for you.',
34
+ placement: 'left',
35
+ },
36
+ {
37
+ target: "[data-tour='publish']",
38
+ changes: true,
39
+ title: 'Publish for everyone',
40
+ body: 'Ready to share? Publish sends your saved work to the whole team. Think of it as putting your version on the shared shelf.',
41
+ placement: 'left',
42
+ },
43
+ {
44
+ target: "[data-tour='pull']",
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.',
48
+ placement: 'left',
49
+ },
50
+ {
51
+ target: "[data-tour='status']",
52
+ title: 'Being together is automatic',
53
+ body: 'Cursors, who’s here, and comments need no buttons at all. When you’re both in a canvas, you see each other live — that’s the top layer, always on.',
54
+ placement: 'bottom',
55
+ },
56
+ {
57
+ // Centered — the one honest hard thing.
58
+ title: 'The one thing worth knowing',
59
+ body: 'When you’re here together, publishing already covers your teammate — they’ve seen it live, so Publish is just dropping a bookmark. Only when you work apart can two versions drift. If that happens, Maude shows you both and lets you pick — keep mine, keep theirs, or keep both. Never a confusing merge.',
60
+ },
61
+ ];
@@ -1,10 +1,23 @@
1
1
  // overlay.jsx — hand-rolled guided-tour engine (zero runtime dep, DDR-B).
2
2
  //
3
- // One <TourOverlay steps={[…]} open onClose onComplete/> drives both the
4
- // per-feature spotlight tours (a What's New entry's `tour[]`) and the evergreen
5
- // usage tour. A step = { target (CSS selector / [data-tour] anchor), title,
6
- // body, placement? }. A step whose target can't be resolved still shows, just
7
- // centered with no spotlight the tour never dead-ends on a missing element.
3
+ // One <TourOverlay steps={[…]} open onClose onComplete bus hasSelection hasCanvas/>
4
+ // drives both the per-feature spotlight tours (a What's New entry's `tour[]`) and
5
+ // the evergreen usage tour.
6
+ //
7
+ // A step is { target, title, body, placement?, ...setup }. The engine does more
8
+ // than spotlight: before each step it puts the shell into the state the target
9
+ // needs, then waits for the element to actually render before measuring — so the
10
+ // tour *walks the user through the live feature* instead of floating a modal over
11
+ // a missing element. Setup directives (all optional):
12
+ // canvas: true → ensure a canvas is open (bus.setup opens the first)
13
+ // inspector: true → open the right-hand Inspector
14
+ // tab: 'inspect'|'layers'|'css' → switch the Inspector tab
15
+ // requireSelection: true → the target only exists once an element is ⌘-clicked
16
+ // in the canvas; the card shows a "⌘-click to continue"
17
+ // hint and the spotlight snaps onto the real row the
18
+ // moment a selection lands.
19
+ // A step whose target still can't be resolved shows centered (no spotlight) — the
20
+ // tour never dead-ends on a missing element.
8
21
  //
9
22
  // A11y: role="dialog" + aria-modal, focus moves to the primary button on each
10
23
  // step and is trapped within the card, focus is restored on close, Esc skips,
@@ -26,7 +39,7 @@ function getRect(target) {
26
39
  }
27
40
 
28
41
  const CARD_W = 320;
29
- const CARD_H_EST = 168;
42
+ const CARD_H_EST = 184;
30
43
 
31
44
  function computeCardStyle(rect) {
32
45
  if (typeof window === 'undefined' || !rect) {
@@ -34,6 +47,13 @@ function computeCardStyle(rect) {
34
47
  }
35
48
  const vw = window.innerWidth;
36
49
  const vh = window.innerHeight;
50
+ const placeLeft = rect.left > CARD_W + 36 && vw - rect.right < CARD_W + 36;
51
+ if (placeLeft) {
52
+ // Panel-on-the-right targets (the Inspector): park the card to its left so it
53
+ // never covers the very thing it's pointing at.
54
+ const top = Math.min(Math.max(12, rect.top), vh - CARD_H_EST - 12);
55
+ return { top, left: Math.max(12, rect.left - CARD_W - 16), width: CARD_W };
56
+ }
37
57
  const roomBelow = vh - rect.bottom;
38
58
  const top =
39
59
  roomBelow > CARD_H_EST + 24 ? rect.bottom + 12 : Math.max(12, rect.top - CARD_H_EST - 12);
@@ -41,29 +61,84 @@ function computeCardStyle(rect) {
41
61
  return { top, left, width: CARD_W };
42
62
  }
43
63
 
44
- export function TourOverlay({ steps, open, onClose, onComplete }) {
64
+ export function TourOverlay({ steps, open, onClose, onComplete, bus, hasSelection, hasCanvas }) {
45
65
  const [i, setI] = useState(0);
46
66
  const [rect, setRect] = useState(null);
47
67
  const cardRef = useRef(null);
48
68
  const prevFocus = useRef(null);
69
+ const pollRef = useRef(null);
70
+ // The bus is rebuilt by the parent on every render; ref it so its identity
71
+ // churn never re-triggers the per-step setup effect (which would thrash).
72
+ const busRef = useRef(bus);
73
+ busRef.current = bus;
74
+
75
+ const step = steps[i] || null;
49
76
 
50
77
  const measure = useCallback(() => {
51
- const step = steps[i];
52
- setRect(step ? getRect(step.target) : null);
53
- }, [steps, i]);
78
+ if (!step) {
79
+ setRect(null);
80
+ return null;
81
+ }
82
+ const r = getRect(step.target);
83
+ setRect(r);
84
+ return r;
85
+ }, [step]);
54
86
 
55
87
  // Reset to the first step whenever a tour (re)opens.
56
88
  useEffect(() => {
57
89
  if (open) setI(0);
58
90
  }, [open]);
59
91
 
92
+ // Per-step: run the step's setup (open canvas / inspector / switch tab), then
93
+ // poll for the target to render before settling on a measurement. React state
94
+ // updates + the canvas-shell round-trip are async, so a single synchronous
95
+ // querySelector almost always misses on the first frame.
96
+ // biome-ignore lint/correctness/useExhaustiveDependencies: re-run only on step/selection change; bus + measure reached via ref/stable closures on purpose.
60
97
  useLayoutEffect(() => {
61
- if (open) measure();
62
- }, [open, measure]);
98
+ if (!open || !step) return undefined;
99
+ let cancelled = false;
100
+ setRect(null);
101
+
102
+ // Establish the UI state this step wants to highlight.
103
+ try {
104
+ busRef.current?.setup?.(step);
105
+ } catch {}
106
+
107
+ let tries = 0;
108
+ const MAX = 24; // ~3s at 130ms
109
+ const tick = () => {
110
+ if (cancelled) return;
111
+ const r = measure();
112
+ const settled = r || tries >= MAX;
113
+ if (settled) {
114
+ if (r) {
115
+ try {
116
+ document.querySelector(step.target)?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
117
+ } catch {}
118
+ // Re-measure after the scroll so the spotlight is glued post-layout.
119
+ requestAnimationFrame(() => {
120
+ if (!cancelled) measure();
121
+ });
122
+ }
123
+ return;
124
+ }
125
+ tries += 1;
126
+ pollRef.current = setTimeout(tick, 130);
127
+ };
128
+ tick();
129
+
130
+ return () => {
131
+ cancelled = true;
132
+ if (pollRef.current) clearTimeout(pollRef.current);
133
+ };
134
+ // Re-run when the step changes OR a selection arrives (a requireSelection
135
+ // target pops into existence the moment the user ⌘-clicks). `bus`/`measure`
136
+ // are reached through refs/stable closures to avoid identity-churn re-runs.
137
+ }, [open, i, hasSelection]);
63
138
 
64
139
  // Keep the spotlight glued to its target through scroll/resize; restore focus.
65
140
  useEffect(() => {
66
- if (!open) return;
141
+ if (!open) return undefined;
67
142
  prevFocus.current = document.activeElement;
68
143
  const onMove = () => measure();
69
144
  window.addEventListener('resize', onMove);
@@ -90,7 +165,7 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
90
165
 
91
166
  // Keyboard: Esc skips, ←/→ navigate, Tab is trapped within the card buttons.
92
167
  useEffect(() => {
93
- if (!open) return;
168
+ if (!open) return undefined;
94
169
  function onKey(e) {
95
170
  if (e.key === 'Escape') {
96
171
  e.preventDefault();
@@ -123,7 +198,7 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
123
198
 
124
199
  // Move focus to the primary action on each step (accessible dialog behavior).
125
200
  useEffect(() => {
126
- if (!open) return;
201
+ if (!open) return undefined;
127
202
  const t = setTimeout(() => {
128
203
  const primary = cardRef.current?.querySelector('[data-tour-primary]');
129
204
  try {
@@ -133,8 +208,7 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
133
208
  return () => clearTimeout(t);
134
209
  }, [open, i]);
135
210
 
136
- if (!open || !steps.length) return null;
137
- const step = steps[i];
211
+ if (!open || !steps.length || !step) return null;
138
212
  const pad = 6;
139
213
  const spot = rect
140
214
  ? {
@@ -145,6 +219,17 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
145
219
  }
146
220
  : null;
147
221
 
222
+ // A requireSelection step whose target hasn't materialized yet: tell the user
223
+ // the one action only they can take (the canvas iframe is cross-origin, so the
224
+ // tour can't ⌘-click for them — DDR-054).
225
+ const needSelHint = !!step.requireSelection && !hasSelection;
226
+ const needCanvasHint = (!!step.canvas || !!step.requireSelection) && hasCanvas === false;
227
+
228
+ // Optional per-step graphic (Phase 29 / E4): a component drawn above the copy —
229
+ // the collab tour uses it for the centered two-layer infographic step. Widens the
230
+ // card so the diagram has room.
231
+ const Graphic = step.render || null;
232
+
148
233
  return (
149
234
  <div className="mdcc-tour" role="presentation">
150
235
  {spot ? (
@@ -153,7 +238,7 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
153
238
  <div className="mdcc-tour__scrim" aria-hidden="true" />
154
239
  )}
155
240
  <div
156
- className="mdcc-tour__card"
241
+ className={'mdcc-tour__card' + (Graphic ? ' mdcc-tour__card--graphic' : '')}
157
242
  ref={cardRef}
158
243
  role="dialog"
159
244
  aria-modal="true"
@@ -164,12 +249,25 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
164
249
  <div className="mdcc-tour__step">
165
250
  {i + 1} / {steps.length}
166
251
  </div>
252
+ {Graphic && (
253
+ <div className="mdcc-tour__graphic">
254
+ <Graphic />
255
+ </div>
256
+ )}
167
257
  <div className="mdcc-tour__title" id="mdcc-tour-title">
168
258
  {step.title}
169
259
  </div>
170
260
  <div className="mdcc-tour__body" id="mdcc-tour-body">
171
261
  {step.body}
172
262
  </div>
263
+ {(needCanvasHint || needSelHint) && (
264
+ <div className="mdcc-tour__hint" role="status" aria-live="polite">
265
+ <span className="mdcc-tour__hint-dot" aria-hidden="true" />
266
+ {needCanvasHint
267
+ ? 'Open any canvas from the sidebar to follow along.'
268
+ : 'Hold ⌘ and click an element in the canvas — the panel fills in live.'}
269
+ </div>
270
+ )}
173
271
  <div className="mdcc-tour__actions">
174
272
  <button type="button" className="mdcc-tour__skip" onClick={onClose}>
175
273
  Skip
@@ -1,35 +1,48 @@
1
1
  // usage-tour.js — the evergreen "how the Maude UI works" walkthrough.
2
2
  //
3
- // Targets stable elements that exist at first load (no canvas open required):
4
- // the sidebar, the empty viewport, the menus, the What's New badge, and Help.
5
- // Steps whose target is missing are shown centered by the overlay, so this is
6
- // resilient to chrome changes. Reached from Help "Take the tour" and offered
7
- // once on first run.
3
+ // Targets stable [data-tour] anchors (redesign-proof styling classes get
4
+ // renamed, these don't) on elements that exist at first load: the sidebar, the
5
+ // viewport, the menus, the Inspector, the What's New badge, and Help. The
6
+ // Inspector step carries `inspector: true` so the engine opens the panel before
7
+ // spotlighting it. Steps whose target is missing are shown centered by the
8
+ // overlay, so this stays resilient to chrome changes. Reached from Help →
9
+ // "Take the tour" and offered once on first run.
8
10
 
9
11
  export const USAGE_TOUR = [
10
12
  {
11
- target: '.sidebar',
13
+ target: "[data-tour='sidebar']",
12
14
  title: 'Your canvases live here',
13
15
  body: 'Every mock in the project shows up in this tree. Click one to open it in a tab — or use “+ board” to spin up a blank brief-board, no command needed.',
16
+ placement: 'right',
14
17
  },
15
18
  {
16
- target: '.viewport',
19
+ target: "[data-tour='viewport']",
17
20
  title: 'The canvas',
18
21
  body: 'Open a canvas, then ⌘-hover to preview the element under your cursor and ⌘-click to select it. Right-click for Copy CSS / Fit / Reset.',
19
22
  },
20
23
  {
21
- target: '.mb-menus',
24
+ target: "[data-tour='menus']",
22
25
  title: 'Menus & tools',
23
- body: 'View toggles panels (tree, comments, annotations); Selection and Tools act on the active canvas. Press ? for every shortcut.',
26
+ body: 'View toggles panels (tree, comments, Inspector); Selection and Tools act on the active canvas. Press ? for every shortcut.',
27
+ placement: 'bottom',
28
+ },
29
+ {
30
+ target: "[data-tour='inspector']",
31
+ inspector: true,
32
+ title: 'The Inspector',
33
+ body: 'Opened from View → Inspector (or press I). Inspect · Layers · CSS — ⌘-click any element and edit its CSS against your design tokens, right here. No AI round-trip.',
34
+ placement: 'left',
24
35
  },
25
36
  {
26
37
  target: "[data-tour='whatsnew']",
27
38
  title: 'What’s new',
28
- body: 'New features land here. Click the ✦ badge any time to catch up on what shipped.',
39
+ body: 'New features land here. Click the ✦ badge any time to catch up on what shipped — some entries come with a guided tour like this one.',
40
+ placement: 'bottom',
29
41
  },
30
42
  {
31
43
  target: "[data-tour='help']",
32
44
  title: 'Help is one key away',
33
45
  body: 'Press ? for the full keyboard cheat-sheet — and you can restart this tour from the Help menu whenever you like.',
46
+ placement: 'bottom',
34
47
  },
35
48
  ];
@@ -48,20 +48,35 @@ export function useWhatsNew(currentVersion) {
48
48
  const [toastDismissed, setToastDismissed] = useState(() => readToastDismissed());
49
49
  const [panelOpen, setPanelOpen] = useState(false);
50
50
 
51
+ // Fetch the feed on mount AND whenever the window regains focus (Phase 32 /
52
+ // Task 5). The native shell can sit backgrounded for a long time; an auto-update
53
+ // (or a deploy of the web studio) bumps the feed while we're not looking, so we
54
+ // re-evaluate on focus to surface the ✦ badge without a full reload.
51
55
  useEffect(() => {
52
56
  let cancelled = false;
53
- fetch('/_api/whats-new')
54
- .then((r) => r.json())
55
- .then((j) => {
56
- if (cancelled) return;
57
- setEntries(Array.isArray(j?.entries) ? j.entries : []);
58
- if (typeof j?.version === 'string') setFeedVersion(j.version);
59
- })
60
- .catch(() => {
61
- /* offline / restart — no banner */
62
- });
57
+ const refresh = () => {
58
+ fetch('/_api/whats-new')
59
+ .then((r) => r.json())
60
+ .then((j) => {
61
+ if (cancelled) return;
62
+ setEntries(Array.isArray(j?.entries) ? j.entries : []);
63
+ if (typeof j?.version === 'string') setFeedVersion(j.version);
64
+ })
65
+ .catch(() => {
66
+ /* offline / restart — no banner */
67
+ });
68
+ };
69
+ refresh();
70
+ const onFocus = () => refresh();
71
+ const onVisible = () => {
72
+ if (document.visibilityState === 'visible') refresh();
73
+ };
74
+ window.addEventListener('focus', onFocus);
75
+ document.addEventListener('visibilitychange', onVisible);
63
76
  return () => {
64
77
  cancelled = true;
78
+ window.removeEventListener('focus', onFocus);
79
+ document.removeEventListener('visibilitychange', onVisible);
65
80
  };
66
81
  }, []);
67
82
 
@@ -58,6 +58,14 @@ export interface Registry {
58
58
  * updated stroke set without waiting for a cold-open re-seed.
59
59
  */
60
60
  syncRoomFromAnnotations(slug: string, svg: string): void;
61
+ /**
62
+ * Phase 30 — project agent editing-presence onto a slug's room awareness so
63
+ * it crosses the hub (the loopback `ai-activity` bus event does not). `null`
64
+ * clears it. No-op when no room is live for the slug — a peer that joins
65
+ * later will see the agent on the next ai-activity heartbeat. Soft heads-up;
66
+ * never a lock.
67
+ */
68
+ setAgentEditing(slug: string, state: { name: string; since: number } | null): void;
61
69
  /**
62
70
  * Phase 9 Task 5 — attach the hub-side Awareness (from a sync provider) for
63
71
  * a slug so the Room's Awareness (browser peers) is bridged bidirectionally
@@ -192,6 +200,10 @@ export function createRegistry(callbacks: RoomCallbacks): Registry {
192
200
  }, 'inspector-write');
193
201
  }
194
202
 
203
+ function setAgentEditing(slug: string, state: { name: string; since: number } | null): void {
204
+ rooms.get(slug)?.setAgentEditing(state);
205
+ }
206
+
195
207
  async function flushAll(): Promise<void> {
196
208
  await Promise.all(Array.from(rooms.values(), (r) => r.flush()));
197
209
  }
@@ -227,6 +239,7 @@ export function createRegistry(callbacks: RoomCallbacks): Registry {
227
239
  getDoc,
228
240
  syncRoomFromComments,
229
241
  syncRoomFromAnnotations,
242
+ setAgentEditing,
230
243
  attachHubAwareness,
231
244
  pin,
232
245
  unpin,
@@ -57,6 +57,15 @@ export interface Room {
57
57
  connect(conn: RoomConn): Promise<void>;
58
58
  disconnect(conn: RoomConn): void;
59
59
  receive(conn: RoomConn, payload: Uint8Array): void;
60
+ /**
61
+ * Phase 30 — project an agent's "editing" activity onto this room's awareness
62
+ * using the room's own (otherwise-unused) local awareness slot, so the soft
63
+ * editing-presence crosses the hub to remote peers (the loopback `ai-activity`
64
+ * bus event does not). `null` clears it. Idempotent — a re-set of the same
65
+ * `{name, since}` is a no-op (ai-activity heartbeats re-emit every 10 s).
66
+ * NOT a lock; never blocks a peer.
67
+ */
68
+ setAgentEditing(state: { name: string; since: number } | null): void;
60
69
  /** Force the debounced flush to fire now. DDR-051 §3 (branch-switch). */
61
70
  flush(): Promise<void>;
62
71
  /** Tear down — clears timers + removes awareness. */
@@ -192,6 +201,32 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
192
201
  if (reply) conn.send(reply);
193
202
  }
194
203
 
204
+ // Phase 30 — agent editing-presence projected onto the room's own awareness
205
+ // slot. The key short-circuits redundant re-sets (ai-activity heartbeats
206
+ // re-emit the same entry every 10 s). The awareness `update` handler above
207
+ // broadcasts the change to local conns AND relays it to the hub via
208
+ // bridgeAwareness, so a remote peer's overlay attributes the edit.
209
+ let agentEditingKey: string | null = null;
210
+ function setAgentEditing(state: { name: string; since: number } | null): void {
211
+ if (destroyed) return;
212
+ const key = state ? `${state.name}${state.since}` : null;
213
+ if (key === agentEditingKey) return;
214
+ agentEditingKey = key;
215
+ if (state === null) {
216
+ awareness.setLocalState(null);
217
+ return;
218
+ }
219
+ awareness.setLocalState({
220
+ name: state.name,
221
+ color: '', // re-derived client-side from name; wire value is discarded
222
+ cursor: null,
223
+ selection: null,
224
+ annotationSelection: [],
225
+ viewport: { x: 0, y: 0, zoom: 1 },
226
+ editing: { since: state.since },
227
+ });
228
+ }
229
+
195
230
  async function destroy(): Promise<void> {
196
231
  destroyed = true;
197
232
  if (flushTimer) {
@@ -211,6 +246,7 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
211
246
  connect,
212
247
  disconnect,
213
248
  receive,
249
+ setAgentEditing,
214
250
  flush,
215
251
  destroy,
216
252
  size: () => conns.size,
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import type { Stroke } from '../annotations-layer.tsx';
23
- import type { CommandRecord, CommandSinks, EditCommand } from '../undo-stack.ts';
23
+ import type { CommandRecord, EditCommand } from '../undo-stack.ts';
24
24
  import { registerCommand } from '../undo-stack.ts';
25
25
 
26
26
  /**
@@ -17,7 +17,7 @@
17
17
  * stale layout.
18
18
  */
19
19
 
20
- import type { CommandRecord, CommandSinks, EditCommand } from '../undo-stack.ts';
20
+ import type { CommandRecord, EditCommand } from '../undo-stack.ts';
21
21
  import { registerCommand } from '../undo-stack.ts';
22
22
 
23
23
  export interface ArtboardLayoutEntry {
@@ -161,7 +161,7 @@ function screenRectFor(
161
161
  } catch {
162
162
  return null;
163
163
  }
164
- if (!el || !el.isConnected) return null;
164
+ if (!el?.isConnected) return null;
165
165
  const r = el.getBoundingClientRect();
166
166
  if (r.width === 0 && r.height === 0) return null;
167
167
  return { x: r.left, y: r.top, w: r.width, h: r.height };
@@ -204,7 +204,7 @@ export function CommentsOverlay(): React.ReactNode {
204
204
  const mirrorSelection = useCallback(
205
205
  (comment: OverlayComment | undefined) => {
206
206
  if (!selSet) return;
207
- if (!comment || !comment.selector) {
207
+ if (!comment?.selector) {
208
208
  selSet.clear();
209
209
  return;
210
210
  }
@@ -298,7 +298,7 @@ export function CommentsOverlay(): React.ReactNode {
298
298
  const detail = (
299
299
  e as CustomEvent<{ selection?: ComposeSelection; clientX?: number; clientY?: number }>
300
300
  ).detail;
301
- if (!detail || !detail.selection) return;
301
+ if (!detail?.selection) return;
302
302
  setComposer({
303
303
  selection: detail.selection,
304
304
  clientX: typeof detail.clientX === 'number' ? detail.clientX : 0,
@@ -383,7 +383,9 @@ export function CommentsOverlay(): React.ReactNode {
383
383
  const indexById = useMemo(() => {
384
384
  const m = new Map<string, number>();
385
385
  const all = comments.slice().sort((a, b) => a.created.localeCompare(b.created));
386
- all.forEach((c, i) => m.set(c.id, i + 1));
386
+ all.forEach((c, i) => {
387
+ m.set(c.id, i + 1);
388
+ });
387
389
  return m;
388
390
  }, [comments]);
389
391
 
@@ -784,7 +786,7 @@ function CommentPin({
784
786
  return () => {
785
787
  if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
786
788
  };
787
- }, [comment.selector, comment.bounds]);
789
+ }, [comment.selector, comment.bounds, comment.index]);
788
790
 
789
791
  const author = comment.author?.trim() || 'unknown';
790
792
  const label = `Comment ${sequence} by ${author}`;
@@ -237,7 +237,9 @@ const MENU_CSS = `
237
237
  .dc-context-menu .dc-menu-flyout.is-flip { left: auto; right: calc(100% + 3px); }
238
238
  `.trim();
239
239
 
240
- function ensureMenuStyles(): void {
240
+ // Exported (FigJam v3) the annotation layer's right-click menu reuses the
241
+ // same .dc-context-menu visual language without duplicating the stylesheet.
242
+ export function ensureMenuStyles(): void {
241
243
  if (typeof document === 'undefined') return;
242
244
  if (document.getElementById('dc-context-menu-css')) return;
243
245
  const s = document.createElement('style');
@@ -342,7 +344,7 @@ function ContextMenuView({
342
344
  el.querySelectorAll<HTMLButtonElement>('button.dc-menu-item:not([disabled])')
343
345
  );
344
346
  if (items.length === 0) return;
345
- const idx = items.findIndex((b) => b === document.activeElement);
347
+ const idx = items.indexOf(document.activeElement);
346
348
  const nextIdx =
347
349
  e.key === 'ArrowDown'
348
350
  ? (idx + 1) % items.length
@@ -368,7 +370,6 @@ function ContextMenuView({
368
370
  {sections.map((section, si) => {
369
371
  const sectionKey = section.map((i) => i.id).join('|') || `s${si}`;
370
372
  return (
371
- // biome-ignore lint/a11y/useSemanticElements: ARIA group within role="menu"; no native equivalent.
372
373
  <div key={sectionKey} role="group">
373
374
  {si > 0 ? <div className="dc-menu-sep" aria-hidden="true" /> : null}
374
375
  {section.map((item) => (
@@ -207,6 +207,20 @@ export function ContextualToolbar() {
207
207
  return (
208
208
  <div ref={ref} className="dc-elem-ctx-tb" role="toolbar" aria-label="Element actions">
209
209
  <span className="dc-elem-ctx-count">{count === 1 ? '1 element' : `${count} elements`}</span>
210
+ <button
211
+ type="button"
212
+ title="Inspect — open the right panel (Inspect / Layers / CSS)"
213
+ onClick={() => {
214
+ // Phase 12 — open the shell's Inspector on the current selection.
215
+ try {
216
+ window.parent.postMessage({ dgn: 'open-inspector' }, '*');
217
+ } catch {
218
+ /* detached / cross-origin */
219
+ }
220
+ }}
221
+ >
222
+ Inspect
223
+ </button>
210
224
  <button
211
225
  type="button"
212
226
  title="Copy CSS selector"