@1agh/maude 0.27.0 → 0.28.1

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 (143) hide show
  1. package/cli/commands/design-link.test.mjs +46 -0
  2. package/cli/commands/design.mjs +69 -3
  3. package/cli/commands/design.test.mjs +97 -1
  4. package/cli/commands/doctor.mjs +110 -5
  5. package/cli/commands/doctor.test.mjs +52 -0
  6. package/cli/lib/cache.mjs +1 -1
  7. package/cli/lib/config-lint.mjs +1 -1
  8. package/cli/lib/copy-tree.mjs +1 -1
  9. package/cli/lib/design-link.mjs +39 -9
  10. package/cli/lib/flow-design-integration.test.mjs +1 -1
  11. package/cli/lib/gitignore-block.test.mjs +2 -2
  12. package/cli/lib/stack-detect.mjs +1 -1
  13. package/package.json +9 -9
  14. package/plugins/design/dev-server/activity.ts +256 -0
  15. package/plugins/design/dev-server/ai-banner.tsx +4 -0
  16. package/plugins/design/dev-server/annotations-context-toolbar.tsx +1 -1
  17. package/plugins/design/dev-server/annotations-layer.tsx +3 -3
  18. package/plugins/design/dev-server/api.ts +1 -1
  19. package/plugins/design/dev-server/artboard-activity-overlay.tsx +67 -0
  20. package/plugins/design/dev-server/bin/scenario-report.mjs +1 -1
  21. package/plugins/design/dev-server/bin/server-up.sh +102 -9
  22. package/plugins/design/dev-server/bin/visual-sanity.sh +19 -3
  23. package/plugins/design/dev-server/canvas-comment-mount.tsx +165 -10
  24. package/plugins/design/dev-server/canvas-cursors.ts +1 -1
  25. package/plugins/design/dev-server/canvas-lib.tsx +72 -36
  26. package/plugins/design/dev-server/canvas-shell.tsx +1 -1
  27. package/plugins/design/dev-server/collab/awareness-bridge.ts +1 -1
  28. package/plugins/design/dev-server/collab/index.ts +4 -4
  29. package/plugins/design/dev-server/collab/persistence.ts +1 -1
  30. package/plugins/design/dev-server/config.schema.json +2 -1
  31. package/plugins/design/dev-server/context-menu.tsx +1 -1
  32. package/plugins/design/dev-server/dist/client.bundle.js +19 -19
  33. package/plugins/design/dev-server/dist/comment-mount.js +84 -6
  34. package/plugins/design/dev-server/dist/runtime/lib0_decoding.js +1 -620
  35. package/plugins/design/dev-server/dist/runtime/lib0_encoding.js +1 -604
  36. package/plugins/design/dev-server/dist/runtime/motion.js +4 -4944
  37. package/plugins/design/dev-server/dist/runtime/motion_react.js +10 -10054
  38. package/plugins/design/dev-server/dist/runtime/pixi-js.js +2775 -52482
  39. package/plugins/design/dev-server/dist/runtime/react-dom.js +1 -231
  40. package/plugins/design/dev-server/dist/runtime/react-dom_client.js +9 -10357
  41. package/plugins/design/dev-server/dist/runtime/react.js +1 -535
  42. package/plugins/design/dev-server/dist/runtime/react_jsx-dev-runtime.js +1 -60
  43. package/plugins/design/dev-server/dist/runtime/react_jsx-runtime.js +1 -85
  44. package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +1 -376
  45. package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +1 -100
  46. package/plugins/design/dev-server/dist/runtime/yjs.js +5 -6687
  47. package/plugins/design/dev-server/draw/brush.ts +3 -3
  48. package/plugins/design/dev-server/draw/index.ts +5 -5
  49. package/plugins/design/dev-server/draw/layout.ts +1 -1
  50. package/plugins/design/dev-server/draw/test/geometry.test.ts +1 -1
  51. package/plugins/design/dev-server/draw/test/layout.test.ts +1 -1
  52. package/plugins/design/dev-server/draw/test/primitives.test.ts +1 -1
  53. package/plugins/design/dev-server/draw/test/serialize.test.ts +1 -1
  54. package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
  55. package/plugins/design/dev-server/examples/perf-100-artboards.tsx +1 -2
  56. package/plugins/design/dev-server/export-dialog.tsx +1 -1
  57. package/plugins/design/dev-server/exporters/html.ts +1 -1
  58. package/plugins/design/dev-server/exporters/index.ts +2 -2
  59. package/plugins/design/dev-server/exporters/pdf.ts +1 -1
  60. package/plugins/design/dev-server/exporters/png.ts +1 -1
  61. package/plugins/design/dev-server/exporters/pptx.ts +1 -1
  62. package/plugins/design/dev-server/exporters/svg.ts +1 -1
  63. package/plugins/design/dev-server/http.ts +1 -1
  64. package/plugins/design/dev-server/inspect.ts +31 -1
  65. package/plugins/design/dev-server/marquee-overlay.tsx +1 -1
  66. package/plugins/design/dev-server/participants-chrome.tsx +86 -1
  67. package/plugins/design/dev-server/server.ts +11 -2
  68. package/plugins/design/dev-server/sync/agent.ts +1 -1
  69. package/plugins/design/dev-server/sync/index.ts +29 -24
  70. package/plugins/design/dev-server/sync/materialize.ts +2 -2
  71. package/plugins/design/dev-server/sync/migrate-seed.ts +1 -1
  72. package/plugins/design/dev-server/sync/projection.ts +3 -3
  73. package/plugins/design/dev-server/test/activity.test.ts +195 -0
  74. package/plugins/design/dev-server/test/ai-activity.test.ts +1 -1
  75. package/plugins/design/dev-server/test/annotations-api.test.ts +1 -2
  76. package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +2 -2
  77. package/plugins/design/dev-server/test/annotations-layer.test.ts +6 -6
  78. package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +1 -1
  79. package/plugins/design/dev-server/test/artboard-activity-overlay.test.tsx +56 -0
  80. package/plugins/design/dev-server/test/boot-self-heal.test.ts +1 -1
  81. package/plugins/design/dev-server/test/canvas-cursors.test.ts +1 -1
  82. package/plugins/design/dev-server/test/canvas-edit.test.ts +1 -1
  83. package/plugins/design/dev-server/test/canvas-header.test.ts +1 -1
  84. package/plugins/design/dev-server/test/canvas-hmr-runtime.test.tsx +114 -0
  85. package/plugins/design/dev-server/test/canvas-lib-inline.test.ts +1 -1
  86. package/plugins/design/dev-server/test/canvas-lib-resolver.test.ts +1 -2
  87. package/plugins/design/dev-server/test/canvas-meta-api.test.ts +1 -2
  88. package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +1 -2
  89. package/plugins/design/dev-server/test/collab-annotations-bridge.test.ts +1 -1
  90. package/plugins/design/dev-server/test/collab-bridge.test.ts +1 -1
  91. package/plugins/design/dev-server/test/collab-protocol.test.ts +2 -2
  92. package/plugins/design/dev-server/test/collab-room.test.ts +1 -1
  93. package/plugins/design/dev-server/test/collab-stress.test.ts +1 -1
  94. package/plugins/design/dev-server/test/comments-api.test.ts +1 -2
  95. package/plugins/design/dev-server/test/compile-entry.test.ts +1 -2
  96. package/plugins/design/dev-server/test/exporters/canva.test.ts +1 -2
  97. package/plugins/design/dev-server/test/exporters/history.test.ts +1 -2
  98. package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +1 -1
  99. package/plugins/design/dev-server/test/exporters/scope.test.ts +1 -2
  100. package/plugins/design/dev-server/test/exporters/zip.test.ts +1 -2
  101. package/plugins/design/dev-server/test/git-lifecycle.test.ts +1 -2
  102. package/plugins/design/dev-server/test/handoff-static-frames.test.ts +1 -1
  103. package/plugins/design/dev-server/test/handoff.test.ts +1 -1
  104. package/plugins/design/dev-server/test/hmr-broadcast.test.ts +1 -1
  105. package/plugins/design/dev-server/test/input-router.test.ts +2 -2
  106. package/plugins/design/dev-server/test/locator.test.ts +1 -1
  107. package/plugins/design/dev-server/test/runtime-bundle.test.ts +1 -1
  108. package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +1 -1
  109. package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +2 -3
  110. package/plugins/design/dev-server/test/shared-doc-projection.test.ts +1 -1
  111. package/plugins/design/dev-server/test/snap-distance-pill.test.ts +1 -1
  112. package/plugins/design/dev-server/test/sync-agent.test.ts +2 -3
  113. package/plugins/design/dev-server/test/sync-atomic-write.test.ts +2 -3
  114. package/plugins/design/dev-server/test/sync-codec.test.ts +1 -1
  115. package/plugins/design/dev-server/test/sync-connection-state.test.ts +1 -1
  116. package/plugins/design/dev-server/test/sync-echo-guard.test.ts +1 -1
  117. package/plugins/design/dev-server/test/sync-fs-mirror.test.ts +1 -2
  118. package/plugins/design/dev-server/test/sync-hardening.test.ts +4 -5
  119. package/plugins/design/dev-server/test/sync-hubs-config.test.ts +1 -2
  120. package/plugins/design/dev-server/test/sync-meta-codec.test.ts +1 -1
  121. package/plugins/design/dev-server/test/sync-runtime.test.ts +37 -14
  122. package/plugins/design/dev-server/test/sync-status.test.ts +1 -1
  123. package/plugins/design/dev-server/test/sync-untrusted.test.ts +1 -2
  124. package/plugins/design/dev-server/test/undo-stack.test.ts +2 -2
  125. package/plugins/design/dev-server/test/use-agent-presence.test.tsx +117 -0
  126. package/plugins/design/dev-server/test/use-artboard-drag.test.ts +3 -3
  127. package/plugins/design/dev-server/test/use-canvas-activity.test.tsx +206 -0
  128. package/plugins/design/dev-server/test/use-selection-set.test.tsx +1 -1
  129. package/plugins/design/dev-server/test/use-snap-guides.test.ts +1 -1
  130. package/plugins/design/dev-server/test/use-undo-stack.test.tsx +2 -2
  131. package/plugins/design/dev-server/use-agent-presence.tsx +244 -0
  132. package/plugins/design/dev-server/use-annotation-resize.tsx +1 -5
  133. package/plugins/design/dev-server/use-annotation-selection.tsx +2 -6
  134. package/plugins/design/dev-server/use-annotations-visibility.tsx +1 -1
  135. package/plugins/design/dev-server/use-artboard-drag.tsx +1 -1
  136. package/plugins/design/dev-server/use-canvas-activity.tsx +252 -0
  137. package/plugins/design/dev-server/use-collab.tsx +3 -4
  138. package/plugins/design/dev-server/use-selection-set.tsx +1 -1
  139. package/plugins/design/dev-server/use-tool-mode.tsx +1 -1
  140. package/plugins/design/dev-server/use-undo-stack.tsx +4 -5
  141. package/plugins/design/dev-server/ws.ts +21 -2
  142. package/plugins/design/templates/_shell.html +108 -3
  143. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +18 -3
@@ -0,0 +1,252 @@
1
+ /**
2
+ * @file use-canvas-activity.tsx — Phase 13 / DDR-029 activity context.
3
+ * @scope plugins/design/dev-server/use-canvas-activity.tsx
4
+ * @purpose Iframe-runtime React context fed by the server's `activity` WS
5
+ * messages. `DCArtboard` reads it to render the "agent works here"
6
+ * overlay on the artboards being edited right now.
7
+ *
8
+ * The bridge: the canvas-shell harness (`templates/_shell.html`) owns the WS
9
+ * connection and re-dispatches every `{ type:'activity', … }` message as a
10
+ * `maude:activity` CustomEvent on `document` (the same pattern used for
11
+ * `maude:meta-refreshed`). This provider subscribes to that event — no second
12
+ * socket, and trivially testable (dispatch the event in a test). In-memory only:
13
+ * activity is ephemeral, never persisted.
14
+ */
15
+
16
+ import {
17
+ createContext,
18
+ type ReactNode,
19
+ useContext,
20
+ useEffect,
21
+ useMemo,
22
+ useRef,
23
+ useState,
24
+ } from 'react';
25
+
26
+ /** Cross-fade window after a file flips `idle` before its overlay is removed. */
27
+ export const ACTIVITY_FADE_MS = 200;
28
+
29
+ /** Wire shape of a server `activity` message (snake_case, matches activity.ts). */
30
+ export interface ActivityMessage {
31
+ type?: 'activity';
32
+ file: string;
33
+ status: 'active' | 'idle';
34
+ artboard_ids?: string[] | null;
35
+ ts: string;
36
+ }
37
+
38
+ interface ActivityEntry {
39
+ status: 'active' | 'idle';
40
+ artboardIds: string[] | null;
41
+ ts: string;
42
+ }
43
+
44
+ type ActivityMap = Record<string, ActivityEntry>;
45
+
46
+ /** What `useCanvasActivity()` returns for a given canvas file. */
47
+ export interface CanvasActivity {
48
+ /** An entry exists (file is active OR within the post-idle fade window). */
49
+ present: boolean;
50
+ /** The file is currently being edited (drives the pulse). */
51
+ active: boolean;
52
+ /** Scoped artboard ids, or null = file-level (every artboard lights up). */
53
+ artboardIds: string[] | null;
54
+ /** Basename of the canvas file, for the badge label. */
55
+ fileLabel: string;
56
+ }
57
+
58
+ interface ActivityContextValue {
59
+ map: ActivityMap;
60
+ /** This canvas's design-root-relative key (server-message keyspace). */
61
+ currentKey: string;
62
+ normalizeKey: (file: string) => string;
63
+ }
64
+
65
+ const ActivityContext = createContext<ActivityContextValue | null>(null);
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Pure helpers — exported for unit tests.
69
+
70
+ /**
71
+ * Normalize a canvas path to the server's activity keyspace: design-root-
72
+ * relative, slash-normalized, no leading slash. Accepts the canonical form
73
+ * (`ui/Foo.tsx`) or the designRel-prefixed form (`.design/ui/Foo.tsx`) that
74
+ * `mountCanvas` passes — stripping `designRel` when known.
75
+ */
76
+ export function activityKey(file: string, designRel?: string): string {
77
+ let s = (file ?? '').replace(/\\/g, '/').replace(/^\/+/, '');
78
+ const dr = (designRel ?? '').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
79
+ if (dr && s.startsWith(`${dr}/`)) s = s.slice(dr.length + 1);
80
+ return s;
81
+ }
82
+
83
+ /** Pure reducer: fold one activity message into the map. */
84
+ export function applyActivityChange(prev: ActivityMap, change: ActivityMessage): ActivityMap {
85
+ if (!change || typeof change.file !== 'string' || !change.file) return prev;
86
+ return {
87
+ ...prev,
88
+ [change.file]: {
89
+ status: change.status === 'active' ? 'active' : 'idle',
90
+ artboardIds: Array.isArray(change.artboard_ids) ? change.artboard_ids : null,
91
+ ts: typeof change.ts === 'string' ? change.ts : '',
92
+ },
93
+ };
94
+ }
95
+
96
+ /** True when an artboard id is in scope for a change (null scope = all). */
97
+ export function matchesArtboard(artboardIds: string[] | null, id: string): boolean {
98
+ return artboardIds === null || artboardIds.includes(id);
99
+ }
100
+
101
+ function basename(key: string): string {
102
+ const i = key.lastIndexOf('/');
103
+ return i >= 0 ? key.slice(i + 1) : key;
104
+ }
105
+
106
+ function readDesignRel(explicit?: string): string | undefined {
107
+ if (explicit) return explicit;
108
+ if (typeof window !== 'undefined') {
109
+ const w = window as unknown as { __canvas_design_rel__?: string };
110
+ if (typeof w.__canvas_design_rel__ === 'string') return w.__canvas_design_rel__;
111
+ }
112
+ return undefined;
113
+ }
114
+
115
+ /**
116
+ * Seed the map from `window.__maude_activity_seed__` — the WS-open snapshot the
117
+ * shell stashes (raw server `activity.state`, keyed design-root-relative with a
118
+ * camelCase `artboardIds`). Load-bearing after the HMR reload a canvas edit
119
+ * triggers: the snapshot can land before React mounts, so the dispatched event
120
+ * is missed and only this synchronous read recovers the in-flight overlay.
121
+ */
122
+ function readActivitySeed(): ActivityMap {
123
+ if (typeof window === 'undefined') return {};
124
+ const w = window as unknown as {
125
+ __maude_activity_seed__?: Record<
126
+ string,
127
+ { status?: 'active' | 'idle'; ts?: string; artboardIds?: string[] | null }
128
+ >;
129
+ };
130
+ const seed = w.__maude_activity_seed__;
131
+ if (!seed || typeof seed !== 'object') return {};
132
+ const out: ActivityMap = {};
133
+ for (const file of Object.keys(seed)) {
134
+ const e = seed[file];
135
+ if (!e || e.status !== 'active') continue; // only resurrect active overlays
136
+ out[file] = {
137
+ status: 'active',
138
+ artboardIds: Array.isArray(e.artboardIds) ? e.artboardIds : null,
139
+ ts: typeof e.ts === 'string' ? e.ts : '',
140
+ };
141
+ }
142
+ return out;
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+
147
+ export interface CanvasActivityProviderProps {
148
+ /** This canvas's file (designRel-prefixed or design-root-relative). */
149
+ file?: string;
150
+ /** designRel for key normalization; defaults to `window.__canvas_design_rel__`. */
151
+ designRel?: string;
152
+ /** Seed (snapshot / tests). Keyed by design-root-relative path. */
153
+ initialState?: ActivityMap;
154
+ children: ReactNode;
155
+ }
156
+
157
+ export function CanvasActivityProvider({
158
+ file,
159
+ designRel,
160
+ initialState,
161
+ children,
162
+ }: CanvasActivityProviderProps) {
163
+ const dr = readDesignRel(designRel);
164
+ const currentKey = useMemo(() => {
165
+ if (file) return activityKey(file, dr);
166
+ // No explicit file (the canvas-lib mount path) → read the design-root-
167
+ // relative canvas path the shell stamps on window. It already matches the
168
+ // server's activity keyspace, so no normalization is needed.
169
+ if (typeof window !== 'undefined') {
170
+ const w = window as unknown as { __canvas_rel__?: string };
171
+ if (typeof w.__canvas_rel__ === 'string' && w.__canvas_rel__) return w.__canvas_rel__;
172
+ }
173
+ return '';
174
+ }, [file, dr]);
175
+ const [map, setMap] = useState<ActivityMap>(() => initialState ?? readActivitySeed());
176
+ // Per-file removal timers for the post-idle cross-fade.
177
+ const fadeTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
178
+
179
+ useEffect(() => {
180
+ if (typeof document === 'undefined') return;
181
+ const timers = fadeTimers.current;
182
+
183
+ function onActivity(ev: Event) {
184
+ const detail = (ev as CustomEvent<ActivityMessage>).detail;
185
+ if (!detail || typeof detail.file !== 'string') return;
186
+ setMap((prev) => applyActivityChange(prev, detail));
187
+
188
+ const pending = timers.get(detail.file);
189
+ if (pending) {
190
+ clearTimeout(pending);
191
+ timers.delete(detail.file);
192
+ }
193
+ if (detail.status === 'idle') {
194
+ // Keep the (now-idle) entry around briefly so the overlay cross-fades
195
+ // out instead of snapping off, then drop it.
196
+ const ts = detail.ts;
197
+ const t = setTimeout(() => {
198
+ timers.delete(detail.file);
199
+ setMap((prev) => {
200
+ const entry = prev[detail.file];
201
+ // Only remove if it's still the same idle entry (not re-activated).
202
+ if (!entry || entry.status !== 'idle' || entry.ts !== ts) return prev;
203
+ const { [detail.file]: _drop, ...rest } = prev;
204
+ return rest;
205
+ });
206
+ }, ACTIVITY_FADE_MS);
207
+ timers.set(detail.file, t);
208
+ }
209
+ }
210
+
211
+ document.addEventListener('maude:activity', onActivity as EventListener);
212
+ return () => {
213
+ document.removeEventListener('maude:activity', onActivity as EventListener);
214
+ for (const t of timers.values()) clearTimeout(t);
215
+ timers.clear();
216
+ };
217
+ }, []);
218
+
219
+ const value = useMemo<ActivityContextValue>(
220
+ () => ({ map, currentKey, normalizeKey: (f: string) => activityKey(f, dr) }),
221
+ [map, currentKey, dr]
222
+ );
223
+
224
+ return <ActivityContext.Provider value={value}>{children}</ActivityContext.Provider>;
225
+ }
226
+
227
+ const EMPTY: CanvasActivity = {
228
+ present: false,
229
+ active: false,
230
+ artboardIds: null,
231
+ fileLabel: '',
232
+ };
233
+
234
+ /**
235
+ * Activity state for a canvas. With no argument, returns the current canvas's
236
+ * activity (the provider knows its own file); pass a `file` to query another.
237
+ * Returns an inert value outside the provider (legacy / specimen mounts), so the
238
+ * overlay simply never renders there.
239
+ */
240
+ export function useCanvasActivity(file?: string): CanvasActivity {
241
+ const ctx = useContext(ActivityContext);
242
+ if (!ctx) return EMPTY;
243
+ const key = file != null ? ctx.normalizeKey(file) : ctx.currentKey;
244
+ if (!key) return EMPTY;
245
+ const entry = ctx.map[key];
246
+ return {
247
+ present: !!entry,
248
+ active: entry?.status === 'active',
249
+ artboardIds: entry?.artboardIds ?? null,
250
+ fileLabel: basename(key),
251
+ };
252
+ }
@@ -16,9 +16,11 @@
16
16
  * <CollabProvider> never resolve these specifiers and pay zero bundle cost.
17
17
  */
18
18
 
19
+ import * as decoding from 'lib0/decoding';
20
+ import * as encoding from 'lib0/encoding';
19
21
  import {
20
- type ReactNode,
21
22
  createContext,
23
+ type ReactNode,
22
24
  useCallback,
23
25
  useContext,
24
26
  useEffect,
@@ -26,9 +28,6 @@ import {
26
28
  useRef,
27
29
  useState,
28
30
  } from 'react';
29
-
30
- import * as decoding from 'lib0/decoding';
31
- import * as encoding from 'lib0/encoding';
32
31
  import { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate } from 'y-protocols/awareness';
33
32
  import { readSyncMessage, writeSyncStep1, writeUpdate } from 'y-protocols/sync';
34
33
  import * as Y from 'yjs';
@@ -17,8 +17,8 @@
17
17
  */
18
18
 
19
19
  import {
20
- type ReactNode,
21
20
  createContext,
21
+ type ReactNode,
22
22
  useCallback,
23
23
  useContext,
24
24
  useEffect,
@@ -12,8 +12,8 @@
12
12
  */
13
13
 
14
14
  import {
15
- type ReactNode,
16
15
  createContext,
16
+ type ReactNode,
17
17
  useCallback,
18
18
  useContext,
19
19
  useEffect,
@@ -18,8 +18,8 @@
18
18
  */
19
19
 
20
20
  import {
21
- type ReactNode,
22
21
  createContext,
22
+ type ReactNode,
23
23
  useCallback,
24
24
  useContext,
25
25
  useEffect,
@@ -31,13 +31,13 @@ import {
31
31
  import {
32
32
  type CommandRecord,
33
33
  type CommandSinks,
34
- type EditCommand,
35
- type UndoStackState,
36
34
  canRedo as canRedoOf,
37
35
  canUndo as canUndoOf,
36
+ type EditCommand,
38
37
  loadStackState,
39
38
  rebuildCommand,
40
39
  saveStackState,
40
+ type UndoStackState,
41
41
  undoReducer,
42
42
  } from './undo-stack.ts';
43
43
 
@@ -349,7 +349,6 @@ export function useUndoSinks(): UndoSinksValue {
349
349
  return useContext(UndoSinksContext) ?? NOOP_SINKS;
350
350
  }
351
351
 
352
- export { UndoStackContext, UndoSinksContext };
353
-
354
352
  // Re-export the EditCommand type for ergonomic single-import in consumers.
355
353
  export type { CommandRecord, EditCommand } from './undo-stack.ts';
354
+ export { UndoSinksContext, UndoStackContext };
@@ -3,6 +3,7 @@
3
3
 
4
4
  import type { ServerWebSocket, WebSocketHandler } from 'bun';
5
5
 
6
+ import type { Activity } from './activity.ts';
6
7
  import type { Api } from './api.ts';
7
8
  import type { Collab, RoomConn } from './collab/index.ts';
8
9
  import type { Context } from './context.ts';
@@ -76,7 +77,13 @@ export interface Ws {
76
77
  clientCount(): number;
77
78
  }
78
79
 
79
- export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Collab): Ws {
80
+ export function createWs(
81
+ ctx: Context,
82
+ api: Api,
83
+ inspect: Inspect,
84
+ collab: Collab,
85
+ activity: Activity
86
+ ): Ws {
80
87
  const clients = new Set<ServerWebSocket<WsData>>();
81
88
 
82
89
  function send(ws: ServerWebSocket<WsData>, payload: unknown) {
@@ -141,6 +148,14 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
141
148
  // Uses broadcastHmr so the segregated canvas origin's HMR-only sockets get it.
142
149
  createHmrBroadcaster(ctx, (msg) => broadcastHmr(msg));
143
150
 
151
+ // Phase 13 / DDR-029 — canvas activity overlay. activity.ts emits
152
+ // `activity:change` per file as edits land + go idle. The overlay renders
153
+ // INSIDE the canvas iframe, which (with the default origin split, DDR-054)
154
+ // holds a `canvas-hmr` socket — so this MUST use broadcastHmr, not broadcast,
155
+ // to reach it. The payload is just a canvas path + status (no secrets); it's
156
+ // the same exposure class the iframe already gets from `canvas-hmr` file paths.
157
+ ctx.bus.on('activity:change', (payload) => broadcastHmr({ type: 'activity', ...payload }));
158
+
144
159
  // Bind a connection to its room. Stored per-socket so close() can find the
145
160
  // right room to disconnect from. Multiplexed via ws.data.id.
146
161
  const collabConns = new Map<string, { roomSlug: string; conn: RoomConn }>();
@@ -175,7 +190,11 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
175
190
  return;
176
191
  }
177
192
  clients.add(ws);
178
- send(ws, { type: 'snapshot', state: inspect.state });
193
+ // Snapshot carries the inspector state AND the activity map so a client
194
+ // opening mid-edit seeds its overlay state (Phase 13). Inspector-origin
195
+ // sockets only — `canvas-hmr` sockets get no snapshot by design and rely
196
+ // on live `activity` broadcasts.
197
+ send(ws, { type: 'snapshot', state: inspect.state, activity: activity.state });
179
198
  },
180
199
  async close(ws) {
181
200
  if (ws.data.kind === 'collab') {
@@ -110,6 +110,10 @@
110
110
  /* HUDs + banners */
111
111
  .dc-ai-banner,
112
112
  .dc-undo-hud,
113
+ /* Activity overlay — live "agent works here" chrome (Phase 13 / DDR-029) */
114
+ .dc-activity-rim,
115
+ /* HMR "holding last good" toast (Phase 13.1 / DDR-077) */
116
+ .dc-hmr-holding,
113
117
  /* Artboard title/label button (editor affordance, not design content) */
114
118
  .dc-artboard-label,
115
119
  /* Generic opt-in hooks for any future floating overlay */
@@ -162,6 +166,52 @@
162
166
  const tokensRel = stripDesignPrefix(params.get('tokens') || '');
163
167
  const componentsRel = stripDesignPrefix(params.get('components') || '');
164
168
  const layoutRel = stripDesignPrefix(params.get('layout') || '');
169
+
170
+ // Phase 13.1 / DDR-077 — error-resilient HMR while an agent edits. Hoisted
171
+ // here so the HMR client (below) can soft-reload the canvas module instead
172
+ // of location.reload()-ing into a half-saved (broken) build.
173
+ const canvasUrl = canvasRel ? ('/' + designRel + '/' + canvasRel) : null;
174
+ const myFileKey = canvasRel ? (designRel + '/' + canvasRel) : null;
175
+ // True while an `ai-activity` entry is live for THIS canvas (an agent is
176
+ // running /design:edit|new on it). Mirrors ai-banner's gate: parent relays
177
+ // ai-activity via postMessage when embedded; the own-WS path (standalone /
178
+ // same-origin inspector socket) is handled in connectHmr below.
179
+ let agentEditing = false;
180
+ window.addEventListener('message', (e) => {
181
+ // DDR-078 security follow-up: only the trusted embedding parent relays
182
+ // ai-activity. Reject canvas self-posts (which could flip the HMR gate)
183
+ // and any non-parent source. Standalone gets it via its own WS below.
184
+ if (e.source !== window.parent || window.parent === window) return;
185
+ const m = e && e.data;
186
+ if (!m || typeof m !== 'object' || m.dgn !== 'ai-activity') return;
187
+ if (myFileKey && m.file === myFileKey) agentEditing = !!m.entry;
188
+ });
189
+
190
+ // Soft reload — re-import the canvas module (cache-busted) and swap it in
191
+ // via the runtime, holding the last good render on a build/import error
192
+ // (no white flash). Only used when an agent is editing; manual edits keep
193
+ // the plain location.reload() below.
194
+ function softReload(version) {
195
+ if (!canvasUrl) { location.reload(); return; }
196
+ const rt = window.__maudeCanvasRuntime;
197
+ if (!rt || !rt.remount) { location.reload(); return; }
198
+ import(canvasUrl + '?v=' + (version || Date.now()))
199
+ .then((mod) => {
200
+ if (mod && typeof mod.default === 'function') {
201
+ rt.remount(mod.default);
202
+ if (rt.setHolding) rt.setHolding(false);
203
+ } else {
204
+ // No usable default export (rare) — fall back to a hard reload.
205
+ location.reload();
206
+ }
207
+ })
208
+ .catch((err) => {
209
+ // Build/transpile/import error → keep the current render, surface a
210
+ // non-destructive "holding" toast. Next good build swaps it in.
211
+ if (rt.setHolding) rt.setHolding(true, (err && (err.message || String(err))) || 'build error');
212
+ console.warn('[canvas-shell] soft-reload held last good (build error):', err);
213
+ });
214
+ }
165
215
  // Phase 6.5 — exporters pass ?hide-chrome=1 to flip the export-mode
166
216
  // stylesheet from `media="not all"` to `media="all"`, hiding the
167
217
  // dev-server overlays during capture. See `<style id="canvas-hide-chrome">`.
@@ -192,7 +242,55 @@
192
242
  ws.addEventListener('message', (ev) => {
193
243
  try {
194
244
  const msg = JSON.parse(ev.data);
195
- if (!msg || msg.type !== 'canvas-hmr') return;
245
+ if (!msg) return;
246
+ // Phase 13.1 / DDR-077 — own-WS path for the agent-active gate (this
247
+ // socket is the inspector channel in standalone / same-origin mode;
248
+ // embedded canvas-hmr sockets get ai-activity via the parent's
249
+ // postMessage relay handled above).
250
+ if (msg.type === 'ai-activity') {
251
+ if (myFileKey && msg.file === myFileKey) agentEditing = !!msg.entry;
252
+ return;
253
+ }
254
+ // Phase 13 / DDR-029 — canvas activity overlay. The server pushes
255
+ // `{ type:'activity', file, status, artboard_ids, ts }` as edits land
256
+ // + go idle; the injected runtime (use-canvas-activity.tsx) listens
257
+ // for the `maude:activity` document event. Same-document bridge — no
258
+ // second socket. The `snapshot` seed re-plays the activity map for a
259
+ // tab that opens mid-edit (inspector-origin only).
260
+ if (msg.type === 'activity') {
261
+ // Maintain a running activity seed (shape matches the snapshot's
262
+ // `activity.state`: camelCase artboardIds). Load-bearing for the
263
+ // Phase 13.1 soft-reload: a soft remount resets the activity
264
+ // provider WITHOUT a page reload (so no fresh snapshot), and the
265
+ // provider re-seeds from this map — without it the overlay would
266
+ // vanish on every agent .tsx save.
267
+ if (!window.__maude_activity_seed__) window.__maude_activity_seed__ = {};
268
+ if (msg.status === 'active') {
269
+ window.__maude_activity_seed__[msg.file] = {
270
+ status: 'active', artboardIds: msg.artboard_ids || null, ts: msg.ts,
271
+ };
272
+ } else {
273
+ delete window.__maude_activity_seed__[msg.file];
274
+ }
275
+ document.dispatchEvent(new CustomEvent('maude:activity', { detail: msg }));
276
+ return;
277
+ }
278
+ if (msg.type === 'snapshot' && msg.activity && typeof msg.activity === 'object') {
279
+ // Stash so the provider can seed on mount even if it isn't listening
280
+ // yet (the snapshot can arrive before React mounts — a real race
281
+ // after the HMR reload that a canvas edit triggers). Also dispatch
282
+ // for any provider that IS already mounted (same-origin live case).
283
+ window.__maude_activity_seed__ = msg.activity;
284
+ for (const file in msg.activity) {
285
+ const e = msg.activity[file];
286
+ if (!e) continue;
287
+ document.dispatchEvent(new CustomEvent('maude:activity', {
288
+ detail: { type: 'activity', file: file, status: e.status, artboard_ids: e.artboardIds || null, ts: e.ts },
289
+ }));
290
+ }
291
+ return;
292
+ }
293
+ if (msg.type !== 'canvas-hmr') return;
196
294
  if (msg.mode === 'css') {
197
295
  const v = msg.version || Date.now();
198
296
  // Match by exact filename when we have one; otherwise refresh all.
@@ -222,7 +320,14 @@
222
320
  } else if (msg.mode === 'module' || msg.mode === 'hard') {
223
321
  // Only reload when the change touches *this* canvas (or `_lib`).
224
322
  if (msg.mode === 'hard' || !msg.file || (canvasRel && msg.file === canvasRel)) {
225
- location.reload();
323
+ // Phase 13.1 / DDR-077 — when an agent is live-editing, soft-
324
+ // reload (hold last good on a broken intermediate). Manual edits
325
+ // (or no runtime yet) keep the plain, immediate hard reload.
326
+ if (agentEditing && window.__maudeCanvasRuntime && window.__maudeCanvasRuntime.remount) {
327
+ softReload(msg.version);
328
+ } else {
329
+ location.reload();
330
+ }
226
331
  }
227
332
  } else if (msg.mode === 'meta') {
228
333
  // Phase 8 — canvas-meta sidecar changed. Filter to our own canvas:
@@ -276,7 +381,7 @@
276
381
  document.head.appendChild(layoutLink);
277
382
  }
278
383
 
279
- const canvasUrl = '/' + designRel + '/' + canvasRel;
384
+ // canvasUrl is hoisted above (Phase 13.1) so the HMR soft-reload can use it.
280
385
  // Phase 4 T5 — read the canvas's sibling .meta.json and stash it on
281
386
  // window so DesignCanvas can seed `layout` + `viewport` synchronously.
282
387
  // The fetch runs in parallel with imports; failure is non-fatal (the
@@ -1,12 +1,12 @@
1
1
  # Sub-agent prompt templates — `/design:setup-ds` Batch B + C
2
2
 
3
- > **Why this file exists.** Per Phase 3.7 Task 4 (and DDR-049), the sub-agent prompt templates that drive Batch B + C fan-out used to live inline in `plugins/design/skills/design-system/SKILL.md` (lines 662-797 pre-extraction). SKILL.md is 1000+ LOC; the prompts were buried. Extracting them here lets us audit + extend the prompts (and especially the three MANDATORY safety blocks below) without scrolling through unrelated bootstrap logic. The skill's "Batches B + C" step loads + interpolates from this file instead of inlining the template.
3
+ > **Why this file exists.** Per Phase 3.7 Task 4 (and DDR-049), the sub-agent prompt templates that drive Batch B + C fan-out used to live inline in `plugins/design/skills/design-system/SKILL.md` (lines 662-797 pre-extraction). SKILL.md is 1000+ LOC; the prompts were buried. Extracting them here lets us audit + extend the prompts (and especially the four MANDATORY safety blocks below) without scrolling through unrelated bootstrap logic. The skill's "Batches B + C" step loads + interpolates from this file instead of inlining the template.
4
4
  >
5
5
  > **Sync rule** — when this file changes, the SKILL.md reference must still resolve. The Phase 3.7 risk register mandates a CI grep check: `SKILL.md` must literally contain the string `SUB-AGENT-PROMPTS.md`. Don't rename the file without updating the skill.
6
6
 
7
7
  The prompts are grouped by **slice** (matches the roster's `fanout:` block). Each section is a self-contained sub-agent brief — copy verbatim, substitute placeholders.
8
8
 
9
- The three MANDATORY safety blocks (ANIMATION SAFETY, RELATIVE-URL SAFETY, PLACEHOLDER POLICY) appear once at the top because they apply to every slice; sub-agent prompts reference them by name (`Apply ANIMATION SAFETY below`) instead of inlining.
9
+ The four MANDATORY safety blocks (ANIMATION SAFETY, RELATIVE-URL SAFETY, PLACEHOLDER POLICY, CODE HYGIENE) appear once at the top because they apply to every slice; sub-agent prompts reference them by name (`Apply ANIMATION SAFETY below`) instead of inlining.
10
10
 
11
11
  ---
12
12
 
@@ -36,6 +36,15 @@ The three MANDATORY safety blocks (ANIMATION SAFETY, RELATIVE-URL SAFETY, PLACEH
36
36
  - **Never assume your own placeholder path is authoritative downstream.** When the roster row's `source:` is empty AND you wrote a placeholder, set `source: placeholder` in your roster update (not `source: written`). The brand-critic and the next bootstrap pass both look for `source: placeholder` rows as "consider revisiting later".
37
37
  - **Forbidden:** authoring an `S`-shaped wordmark for a product whose first letter isn't S, authoring a "hedgehog mascot" SVG because the README copy mentioned hedgehog energy. The studyfi imprint retro D-2 caught both patterns. If the production-asset sweep returned nothing AND the discovery brief didn't explicitly request a mascot, **rewrite the copy** instead of inventing the artifact.
38
38
 
39
+ ### CODE HYGIENE (mandatory — applies to EVERY TSX + CSS file in every slice)
40
+
41
+ > **Why this block exists (setup-ds Round-2 / DDR-082).** These four defects all *pass a parse / transpile-only check but break at render or assert something false* — exactly the class the structural critic can't see. Each was caught by a user mid-flow, never by the loop. The reconcile-time scaffold-integrity gates in `_bootstrap.md` grep for all four; **prevent them here so the grep finds nothing.**
42
+
43
+ - **No empty / stub files.** Every file you claim as `written` in the roster MUST carry real content — the JSDoc header, the `import "./<slug>.css"`, and a non-trivial default export. **Never** flip a roster row to `status: written` for a file you created empty or as a placeholder-to-fill-later. A 0-byte (or near-empty, < 20 B) specimen is a reconciliation **hard-fail**, the same severity as a `pending` row. The roster's `loc:` field is a claim that is verified against disk — report the *real* line count, never an aspirational one.
44
+ - **CSS comment hygiene — never write `*/` inside a `/* … */` block.** A stray `*/` (e.g. inside an example selector string, a URL, or a nested-comment attempt) closes the comment early and the trailing text becomes invalid CSS → the dev-server bundle fails with "Bundle failed" and the specimen renders unstyled. CSS has **no nested comments.** If you must show a `*/` literally in demo copy, escape it in JSX text (`{'*/'}`) — never inside a `/* */` CSS comment.
45
+ - **`React.*` requires a BINDING import.** Any use of `React.useState`, `React.useId`, `React.Fragment`, `React.useEffect`, `React.CSSProperties`, etc. MUST be backed by `import React from "react"` (default) or `import * as React from "react"` (namespace) — those are the only forms that bind the `React` identifier. A NAMED import (`import { useState } from "react"`) or a type-only import (`import type { CSSProperties } from "react"`) does **not** bind `React`, so `React.foo` still throws `ReferenceError: React is not defined` at module-eval in the browser even though it transpiles clean — a runtime crash the structural critic never sees. **Prefer named imports and call them unqualified** (`import { useId, useState } from "react"` → `useState(…)`, not `React.useState(…)`); reach for the `React.` namespace only with the default/namespace import in place. The reconcile gate flags any `React.*` whose file lacks a default/namespace React import.
46
+ - **Contrast-claim discipline — never assert a ratio you didn't compute.** Do NOT write `✓ 4.5:1`, `✓ AAA`, `passes AA`, `7:1`, etc. in a specimen, a CSS comment, or copy **unless you actually computed it** from the real token pair (WCAG relative-luminance or APCA). A hardcoded-but-wrong ratio claim is worse than no claim — it tells the reader a failing pair is safe. If you want to *demonstrate* contrast without computing, label the swatch with the token names and the OKLCH values, not a fabricated ratio. The reconcile grep flags ratio-claim substrings for verification — an unverified claim is a gate failure.
47
+
39
48
  ---
40
49
 
41
50
  ## Sub-agent prompt template (slice-agnostic — used by every Batch B + C slice)
@@ -128,6 +137,10 @@ YOUR SLICE — write these {{N}} files (absolute paths):
128
137
  … (etc.)
129
138
 
130
139
  SAFETY BLOCKS — apply to every file in your slice:
140
+ - **CODE HYGIENE** (see SUB-AGENT-PROMPTS.md). ALWAYS — applies to every TSX +
141
+ CSS file. No empty / stub files (report real `loc:`), no `*/` inside a CSS
142
+ `/* */` block, every `React.*` needs an explicit `import`, never assert a
143
+ contrast ratio you didn't compute. The reconcile gate greps for all four.
131
144
  - **ANIMATION SAFETY** (see SUB-AGENT-PROMPTS.md). Mandatory if your slice
132
145
  includes the `motion` specimen OR any file that uses `@keyframes` /
133
146
  `transition` / `animate`. Bounded geometry + sparkle-≤56px + infinite-alternate
@@ -189,7 +202,9 @@ ANTI-PATTERNS (graphic-design-critic blockers — guaranteed rejection)
189
202
  WHEN DONE
190
203
  After writing all {{N}} files:
191
204
  1. Update each row in {{absolute path to _scaffold-roster.yaml}}: change
192
- `status: pending` → `status: written, loc: <line-count>`.
205
+ `status: pending` → `status: written, loc: <line-count>`. The `loc:` is the
206
+ REAL on-disk line count — never aspirational. An empty / stub / 0-byte file
207
+ flipped to `written` is a reconciliation hard-fail (see CODE HYGIENE).
193
208
  2. **Do NOT add new rows.** If you discover a missing claim (e.g. wordmark
194
209
  referenced but no logo.tsx in roster), include "ROSTER GAP: <description>"
195
210
  in your completion message. The main agent reconciles new rows.