@1agh/maude 1.3.0 → 1.4.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 (36) hide show
  1. package/apps/studio/annotation-edit-base.ts +36 -0
  2. package/apps/studio/annotations-layer.tsx +13 -2
  3. package/apps/studio/api.ts +51 -2
  4. package/apps/studio/canvas-build.ts +49 -8
  5. package/apps/studio/canvas-edit.ts +77 -0
  6. package/apps/studio/canvas-lib.tsx +25 -10
  7. package/apps/studio/canvas-list-watch.ts +33 -2
  8. package/apps/studio/client/app.jsx +99 -4
  9. package/apps/studio/client/canvas-url.js +39 -1
  10. package/apps/studio/client/panels/TimelinePanel.jsx +75 -62
  11. package/apps/studio/client/photo-knobs.jsx +3 -3
  12. package/apps/studio/client/styles/3-shell-maude.css +1 -0
  13. package/apps/studio/collab/awareness-bridge.ts +65 -14
  14. package/apps/studio/comments-overlay.tsx +86 -2
  15. package/apps/studio/context.ts +93 -1
  16. package/apps/studio/dist/client.bundle.js +619 -619
  17. package/apps/studio/dist/comment-mount.js +2 -2
  18. package/apps/studio/dist/styles.css +1 -1
  19. package/apps/studio/git/accepted-guard.ts +36 -0
  20. package/apps/studio/hmr-broadcast.ts +34 -1
  21. package/apps/studio/http.ts +15 -1
  22. package/apps/studio/managed-projects.ts +22 -0
  23. package/apps/studio/server.ts +3 -0
  24. package/apps/studio/sync/accepted-link.ts +3 -0
  25. package/apps/studio/sync/file-plane.ts +15 -0
  26. package/apps/studio/sync/index.ts +138 -12
  27. package/apps/studio/sync/presentation.ts +30 -0
  28. package/apps/studio/sync/status.ts +5 -1
  29. package/apps/studio/sync/transaction-client.ts +29 -0
  30. package/apps/studio/use-tool-mode.tsx +44 -0
  31. package/apps/studio/whats-new.json +72 -0
  32. package/apps/studio/ws.ts +9 -2
  33. package/package.json +8 -8
  34. package/plugins/design/templates/_shell.html +48 -7
  35. package/plugins/flow/.claude-plugin/config.schema.json +15 -2
  36. package/plugins/flow/README.md +1 -1
@@ -18,6 +18,7 @@ import {
18
18
  useContext,
19
19
  useEffect,
20
20
  useMemo,
21
+ useRef,
21
22
  useState,
22
23
  } from 'react';
23
24
 
@@ -53,6 +54,9 @@ export type ShapeKind = 'square' | 'rounded' | 'circle' | 'diamond' | 'triangle'
53
54
  */
54
55
  export type CanvasMode = 'preview' | 'edit';
55
56
 
57
+ /** Document event two provider instances keep one active tool through. */
58
+ export const TOOL_SYNC_EVENT = 'maude:tool-sync';
59
+
56
60
  /** The resting tool each mode arms (and returns to via `resetTool`). */
57
61
  export const MODE_DEFAULT_TOOL: Readonly<Record<CanvasMode, Tool>> = Object.freeze({
58
62
  preview: 'browse',
@@ -231,6 +235,46 @@ export function ToolProvider({
231
235
  // FigJam look); sharp squares stay one popover click away.
232
236
  const [shapeKind, setShapeKind] = useState<ShapeKind>('rounded');
233
237
 
238
+ // ONE ACTIVE TOOL PER CANVAS DOCUMENT, across provider instances.
239
+ //
240
+ // A UI canvas carries TWO providers: the comment-mount layer's (it owns the
241
+ // comment drop) and canvas-lib's own (it owns the palette) — separate
242
+ // bundles, so separate contexts. They used to converge only through a tool
243
+ // KEYDOWN, which both routers see; a palette CLICK reached canvas-lib's
244
+ // provider alone. So "Comment" in the palette lit up while the comment
245
+ // layer stayed in browse, and clicking the canvas dropped nothing — you had
246
+ // to know to press C (plan T31/L11). A change here is announced on the
247
+ // document and every other instance adopts it. The boot posture is NOT
248
+ // announced (the two instances deliberately boot differently — see
249
+ // `initial`); only a change after mount is.
250
+ const syncId = useRef(Math.random().toString(36).slice(2));
251
+ const toolRef = useRef(tool);
252
+ toolRef.current = tool;
253
+ const announced = useRef(false);
254
+ useEffect(() => {
255
+ if (!announced.current) {
256
+ announced.current = true;
257
+ return;
258
+ }
259
+ if (typeof document === 'undefined') return;
260
+ document.dispatchEvent(
261
+ new CustomEvent(TOOL_SYNC_EVENT, { detail: { tool, from: syncId.current } })
262
+ );
263
+ }, [tool]);
264
+ useEffect(() => {
265
+ if (typeof document === 'undefined') return;
266
+ const onSync = (e: Event) => {
267
+ const d = (e as CustomEvent<{ tool?: unknown; from?: unknown }>).detail;
268
+ if (!d || d.from === syncId.current || typeof d.tool !== 'string') return;
269
+ // Already there: nothing to do, and — for a sticky-locked tool — not
270
+ // touching it keeps the lock.
271
+ if (d.tool === toolRef.current) return;
272
+ setTool(d.tool as Tool);
273
+ };
274
+ document.addEventListener(TOOL_SYNC_EVENT, onSync);
275
+ return () => document.removeEventListener(TOOL_SYNC_EVENT, onSync);
276
+ }, [setTool]);
277
+
234
278
  // Cursor sync — applied inside the canvas (this hook runs in the canvas
235
279
  // context). The active tool's cursor is set on <body> AND forced across the
236
280
  // whole canvas working area via an `!important` rule, so the custom cursor
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "$schema": "./whats-new.schema.json",
3
3
  "entries": [
4
+ {
5
+ "id": "design-system-on-every-desktop",
6
+ "version": "1.4.0",
7
+ "date": "2026-09-16",
8
+ "kind": "improvement",
9
+ "title": "Your team’s design system on your desktop too",
10
+ "summary": "When you open a team project on the desktop, its design system now appears in the Design System section — tokens, specimens and all — the same as on the machine that made it. Specimens someone adds show up for everyone.",
11
+ "surface": "design-ui"
12
+ },
13
+ {
14
+ "id": "sync-reliability-sept",
15
+ "version": "1.4.0",
16
+ "date": "2026-09-16",
17
+ "kind": "fix",
18
+ "title": "Steadier syncing in shared projects",
19
+ "summary": "Renaming a folder no longer makes you miss a teammate’s later edits to the canvases inside it; replacing an image keeps showing the new one on your screen; a file the workspace can’t store right now is named in the Sync panel and delivers by itself when it can; and a too-big file you remove stops being reported.",
20
+ "surface": "design-ui"
21
+ },
22
+ {
23
+ "id": "edits-follow-moves",
24
+ "version": "1.4.0",
25
+ "date": "2026-09-16",
26
+ "kind": "improvement",
27
+ "title": "Your edit follows a canvas a teammate moved",
28
+ "summary": "If a teammate moves a canvas — or renames its folder — while you are editing it offline, your change now lands at the canvas’s new place for everyone instead of waiting for review. Deleting a canvas you were editing still asks you what to keep.",
29
+ "surface": "design-ui"
30
+ },
31
+ {
32
+ "id": "edit-own-comments",
33
+ "version": "1.4.0",
34
+ "date": "2026-09-16",
35
+ "kind": "feature",
36
+ "title": "Fix a typo in your own comment",
37
+ "summary": "Open a comment you wrote and choose Edit — change the text and save; everyone sees the new wording and the replies stay. On a Maude Cloud project in the browser, your comments and replies now carry your name instead of the workspace’s.",
38
+ "surface": "design-ui"
39
+ },
40
+ {
41
+ "id": "live-canvases-long-sessions",
42
+ "version": "1.3.1",
43
+ "date": "2026-09-15",
44
+ "kind": "fix",
45
+ "title": "Canvases stay live in long sessions",
46
+ "summary": "A canvas open in your browser on a Maude Cloud project kept working for about 15 minutes and then quietly stopped showing teammates’ changes until you reloaded the page — it now stays live however long you work. And editing a design system’s tokens.css restyles every open canvas that uses it, straight away.",
47
+ "surface": "design-ui"
48
+ },
49
+ {
50
+ "id": "team-access-and-presence",
51
+ "version": "1.3.1",
52
+ "date": "2026-09-15",
53
+ "kind": "fix",
54
+ "title": "Lost access? Maude asks you to sign in again",
55
+ "summary": "If an owner removes your access or your sign-in expires, the Sync panel now tells you to sign in again — instead of \"check your connection\" — and keeps your changes on this computer until you do. Signing in again sends them. And when a teammate closes a canvas, their avatar and cursor now leave everyone else’s screen straight away.",
56
+ "surface": "design-ui"
57
+ },
58
+ {
59
+ "id": "comment-tool-and-live-tree",
60
+ "version": "1.3.1",
61
+ "date": "2026-09-15",
62
+ "kind": "fix",
63
+ "title": "The Comment tool works from the toolbar; new images and folders show up for everyone at once",
64
+ "summary": "Picking Comment in the canvas toolbar now lets you click an element to comment — before, only the C key did. An image saved next to your canvases appears in the file tree straight away, on your machine and your teammates’. A folder you create or move outside Maude, or move in the tree, reaches the team in seconds instead of on the next launch.",
65
+ "surface": "design-ui"
66
+ },
67
+ {
68
+ "id": "inspector-keeps-number-types",
69
+ "version": "1.3.1",
70
+ "date": "2026-09-15",
71
+ "kind": "fix",
72
+ "title": "Editing a number in the inspector keeps it a number",
73
+ "summary": "Changing a value like an artboard height or a video’s length used to turn it into text in the canvas source — some components then misread it. Numbers and on/off values now stay what they were, and leaving a value unchanged no longer rewrites the file.",
74
+ "surface": "design-ui"
75
+ },
4
76
  {
5
77
  "id": "artboard-rename-and-files",
6
78
  "version": "1.3.0",
package/apps/studio/ws.ts CHANGED
@@ -36,6 +36,9 @@ export type WsData =
36
36
  * a desktop, which is what makes every broadcast below reach everybody
37
37
  * exactly as it always did. */
38
38
  session: string;
39
+ /** WHO opened this socket, as the proxy vouched it — Cloud Phase 27.
40
+ * `''` on a desktop, where the project's git identity is the person. */
41
+ user?: string;
39
42
  }
40
43
  | {
41
44
  id: string;
@@ -446,8 +449,12 @@ export function createWs(
446
449
  else if (msg.type === 'tabs' && Array.isArray(msg.tabs)) inspect.setOpenTabs(msg.tabs);
447
450
  else if (msg.type === 'select' && msg.selection) inspect.setSelected(msg.selection);
448
451
  else if (msg.type === 'clear-select') inspect.setSelected(null);
449
- else if (msg.type === 'comments-add' && msg.payload) await api.commentsAdd(msg.payload);
450
- else if (msg.type === 'comments-patch' && msg.id)
452
+ else if (msg.type === 'comments-add' && msg.payload) {
453
+ // In a cell the author is the vouched member, never a claim in the
454
+ // frame and never the cell's own git identity (the machine).
455
+ const vouched = ws.data.kind === 'inspector' ? ws.data.user : '';
456
+ await api.commentsAdd(vouched ? { ...msg.payload, author: vouched } : msg.payload);
457
+ } else if (msg.type === 'comments-patch' && msg.id)
451
458
  await api.commentsPatch(msg.id, msg.patch || {});
452
459
  else if (msg.type === 'comments-delete' && msg.id) await api.commentsDelete(msg.id);
453
460
  else if (msg.type === 'comments-request' && typeof msg.file === 'string') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agh/maude",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -56,13 +56,13 @@
56
56
  "test:e2e:desktop:parity:cloud": "pnpm --filter @maude/desktop-e2e e2e:parity:cloud"
57
57
  },
58
58
  "optionalDependencies": {
59
- "@1agh/maude-darwin-arm64": "1.3.0",
60
- "@1agh/maude-darwin-x64": "1.3.0",
61
- "@1agh/maude-linux-arm64": "1.3.0",
62
- "@1agh/maude-linux-arm64-musl": "1.3.0",
63
- "@1agh/maude-linux-x64": "1.3.0",
64
- "@1agh/maude-linux-x64-musl": "1.3.0",
65
- "@1agh/maude-win32-x64": "1.3.0"
59
+ "@1agh/maude-darwin-arm64": "1.4.0",
60
+ "@1agh/maude-darwin-x64": "1.4.0",
61
+ "@1agh/maude-linux-arm64": "1.4.0",
62
+ "@1agh/maude-linux-arm64-musl": "1.4.0",
63
+ "@1agh/maude-linux-x64": "1.4.0",
64
+ "@1agh/maude-linux-x64-musl": "1.4.0",
65
+ "@1agh/maude-win32-x64": "1.4.0"
66
66
  },
67
67
  "files": [
68
68
  "cli",
@@ -205,7 +205,7 @@
205
205
  // module, its tokens, its components CSS and its meta as separate
206
206
  // requests. Getting the document through and nothing else is what
207
207
  // "refused to connect" looks like from the outside.
208
- const cap = params.get('t') || '';
208
+ let cap = params.get('t') || '';
209
209
  // THE BASE THIS DOCUMENT WAS SERVED FROM, not the origin root.
210
210
  //
211
211
  // The fleet's canvas origin puts the project in the PATH
@@ -223,7 +223,7 @@
223
223
  return cap ? abs + (abs.includes('?') ? '&' : '?') + 't=' + encodeURIComponent(cap) : abs;
224
224
  };
225
225
  const canvasQuery = shaParam ? ('?sha=' + encodeURIComponent(shaParam)) : '';
226
- const canvasUrl = canvasRel ? withCap('/' + designRel + '/' + canvasRel + canvasQuery) : null;
226
+ let canvasUrl = canvasRel ? withCap('/' + designRel + '/' + canvasRel + canvasQuery) : null;
227
227
  const myFileKey = canvasRel ? (designRel + '/' + canvasRel) : null;
228
228
  // True while an `ai-activity` entry is live for THIS canvas (an agent is
229
229
  // running /design:edit|new on it). Mirrors ai-banner's gate: parent relays
@@ -250,6 +250,19 @@
250
250
  // The panel just optimistically applied an edit → skip the redundant
251
251
  // follow-up module reload (set here, parent-verified).
252
252
  lastCssOptimisticAt = Date.now();
253
+ } else if (m.dgn === 'canvas-cap') {
254
+ // A re-minted capability (it expires; app.jsx refreshes it). Every
255
+ // URL built from here on carries it, and asking for this shell
256
+ // document with it re-plants the origin's cookie — the one module
257
+ // imports, assets and the live sockets fall back to. Without this an
258
+ // open canvas stopped loading and updating once its first capability
259
+ // expired. Only a well-formed token, and only in the cloud (a
260
+ // desktop canvas never had one).
261
+ const t = m.t;
262
+ if (!cap || typeof t !== 'string' || t.length > 4096 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(t)) return;
263
+ cap = t;
264
+ if (canvasRel) canvasUrl = withCap('/' + designRel + '/' + canvasRel + canvasQuery);
265
+ fetch(withCap('/_canvas-shell.html'), { cache: 'no-store', credentials: 'same-origin' }).catch(() => {});
253
266
  }
254
267
  });
255
268
 
@@ -394,12 +407,22 @@
394
407
  if (msg.type !== 'canvas-hmr') return;
395
408
  if (msg.mode === 'css') {
396
409
  const v = msg.version || Date.now();
397
- // Match by exact filename when we have one; otherwise refresh all.
398
- const targetFile = (msg.file || '').split('/').pop();
410
+ // Match by PATH when we have one; otherwise refresh all. By path,
411
+ // not by file name: every design system has a `tokens.css`, and
412
+ // a change to one canvas's imported `system/<a>/tokens.css`
413
+ // "matched" the shell's link to `system/<b>/tokens.css` — which
414
+ // swapped the wrong sheet and skipped the reload below that the
415
+ // changed one needed (plan T31/L16).
416
+ const changed = msg.file ? '/' + msg.file : '';
399
417
  let matched = false;
400
418
  for (const link of document.querySelectorAll('link[rel="stylesheet"]')) {
401
419
  const href = link.getAttribute('href') || '';
402
- if (!targetFile || href.includes(targetFile)) {
420
+ if (!href) continue;
421
+ let linkPath = '';
422
+ try {
423
+ linkPath = decodeURIComponent(new URL(href, location.href).pathname);
424
+ } catch {}
425
+ if (!changed || linkPath.endsWith(changed)) {
403
426
  const base = href.split('?')[0];
404
427
  link.setAttribute('href', base + '?v=' + v);
405
428
  matched = true;
@@ -416,7 +439,21 @@
416
439
  if (!matched && msg.file && canvasRel) {
417
440
  const cssDir = msg.file.replace(/[^/]+$/, '');
418
441
  const canvasDir = canvasRel.replace(/[^/]+$/, '');
419
- if (cssDir === canvasDir) location.reload();
442
+ if (cssDir === canvasDir) {
443
+ location.reload();
444
+ } else {
445
+ // …and a stylesheet imported from ANYWHERE else (a design
446
+ // system's tokens.css) is inlined the same way. The module's
447
+ // style tag names what it inlined; re-import so it re-injects.
448
+ // (Canvas script could add names here, but that can only
449
+ // FORCE a reload — the safe direction.)
450
+ var bundled = document.querySelector('style[data-canvas-css="bundled"]');
451
+ var names = bundled && bundled.dataset.canvasCssSources ? bundled.dataset.canvasCssSources.split('|') : [];
452
+ if (names.indexOf(msg.file) !== -1) {
453
+ if (window.__maudeCanvasRuntime && window.__maudeCanvasRuntime.remount) softReload(v);
454
+ else location.reload();
455
+ }
456
+ }
420
457
  }
421
458
  } else if (msg.mode === 'module' || msg.mode === 'hard') {
422
459
  // Only reload when the change touches *this* canvas (or `_lib`).
@@ -443,7 +480,11 @@
443
480
  // direction; it can never suppress one.)
444
481
  var hasComps = false;
445
482
  try { hasComps = !!(window.__maude_comps__ && window.__maude_comps__().length); } catch (err) { hasComps = false; }
446
- if (!hasComps && Date.now() - lastCssOptimisticAt < 1500) return;
483
+ // …but NEVER a sync write (`remote`): it can carry a
484
+ // teammate's change merged under this person's own edit (a
485
+ // lost race re-applied onto theirs), and skipping it left
486
+ // their change off this canvas until a manual reload.
487
+ if (!hasComps && !msg.remote && Date.now() - lastCssOptimisticAt < 1500) return;
447
488
  }
448
489
  // Phase 30 (F4) — prefer an in-place hot-swap for ANY canvas-body
449
490
  // (`module`) change whenever the canvas runtime is mounted: the
@@ -475,7 +475,7 @@
475
475
  "tracker": {
476
476
  "type": "object",
477
477
  "additionalProperties": false,
478
- "description": "Issue / task tracker. Used by /flow:done, /flow:bug-rca, /flow:bug-fix to optionally create or update tickets.",
478
+ "description": "Issue / task tracker. Used by /flow:done, /flow:bug-rca, /flow:bug-fix and /flow:status to optionally read, create or update tickets. `orbit` additionally routes /flow:plan (find or create the task), /flow:execute (working-state reports) and /flow:done (close-time artifact push, warn-only) through the `orbit-backend` skill.",
479
479
  "properties": {
480
480
  "provider": {
481
481
  "type": "string",
@@ -487,13 +487,26 @@
487
487
  "notion",
488
488
  "asana",
489
489
  "shortcut",
490
+ "orbit",
490
491
  "none"
491
492
  ],
492
493
  "default": "none"
493
494
  },
494
495
  "mcp": {
495
496
  "type": "string",
496
- "description": "MCP tool name prefix used to call the provider (e.g. `mcp__claude_ai_ClickUp`, `mcp__github`). Generic commands resolve tool names by appending the operation (`<mcp>_<op>`). Omit if no MCP server is configured — commands will print payloads for manual handling."
497
+ "description": "MCP tool name prefix used to call the provider (e.g. `mcp__claude_ai_ClickUp`, `mcp__github`, `mcp__orbit`). Generic commands resolve tool names by appending the operation (`<mcp>_<op>`). Omit if no MCP server is configured — commands will print payloads for manual handling."
498
+ },
499
+ "baseUrl": {
500
+ "type": "string",
501
+ "format": "uri",
502
+ "pattern": "^https?://",
503
+ "description": "Base URL of a self-hosted tracker instance — used by `provider: orbit` (e.g. `https://orbit.example.com`); other providers ignore it. A committed pointer, never a secret: it names the MCP endpoint the project's `.mcp.json` entry targets (`<baseUrl>/api/mcp`) and the task links (`<baseUrl>/t/ORB-123`)."
504
+ },
505
+ "tokenEnv": {
506
+ "type": "string",
507
+ "pattern": "^[A-Z_][A-Z0-9_]*$",
508
+ "default": "ORBIT_MCP_TOKEN",
509
+ "description": "Name of the environment variable holding the per-machine bearer token for the tracker's MCP server — used by `provider: orbit`; other providers ignore it. Only the variable NAME is stored here; the token lives in the user's environment and `.mcp.json` references it as `${<tokenEnv>}`. Flow commands check whether it is set — they never read, print or write its value."
497
510
  },
498
511
  "defaults": {
499
512
  "type": "object",
@@ -40,6 +40,6 @@ For the full catalog (group definitions, member descriptions, rename history), s
40
40
 
41
41
  ## Skills
42
42
 
43
- `workflow-state`, `ddr-keeper`, `scenario`, `agent-browser`, `agent-device`, `codebase-intelligence`, `a11y-checker`, `question-protocol`, `make-skill-template`, `claude-md-keeper`, `debugging-rules`, `a11y-rules`, `motion-rules`, `responsive-rules`, `testing-rules`, `skill-loader`.
43
+ `workflow-state`, `ddr-keeper`, `scenario`, `agent-browser`, `agent-device`, `codebase-intelligence`, `a11y-checker`, `question-protocol`, `make-skill-template`, `claude-md-keeper`, `debugging-rules`, `a11y-rules`, `motion-rules`, `responsive-rules`, `testing-rules`, `skill-loader`, `orbit-backend` (the `orbit` tracker provider's contract).
44
44
 
45
45
  See [`skills/`](./skills/) for the full list and per-skill SKILL.md files.