@1agh/maude 0.57.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/apps/studio/api.ts +69 -4
- package/apps/studio/client/app.jsx +140 -9
- package/apps/studio/client/panels/chat-markdown.jsx +76 -5
- package/apps/studio/client/panels/file-preview.jsx +115 -0
- package/apps/studio/client/styles/3-shell-maude.css +45 -0
- package/apps/studio/client/styles/6-acp-chat.css +28 -0
- package/apps/studio/dist/client.bundle.js +1590 -1590
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/http.ts +8 -0
- package/apps/studio/input-router.tsx +39 -1
- package/apps/studio/test/config-version.test.ts +88 -0
- package/apps/studio/test/input-router.test.ts +56 -0
- package/apps/studio/test/whats-new.test.ts +13 -1
- package/apps/studio/whats-new.json +9 -0
- package/apps/studio/whats-new.ts +34 -10
- package/package.json +8 -8
package/apps/studio/http.ts
CHANGED
|
@@ -1507,6 +1507,14 @@ export function createHttp(
|
|
|
1507
1507
|
...ctx.cfg,
|
|
1508
1508
|
canvasOrigin: ctx.canvasOrigin,
|
|
1509
1509
|
readOnly: projectReadOnly(req),
|
|
1510
|
+
// WHICH RELEASE THIS IS. `/_config` reaches the browser in cloud mode,
|
|
1511
|
+
// so the bar for adding a field here is "would I publish it" — and a
|
|
1512
|
+
// version is the git tag, which is public. Nothing else rides along.
|
|
1513
|
+
//
|
|
1514
|
+
// Resolved through `resolveMaudeVersion`, the same function the What's
|
|
1515
|
+
// New feed uses: one answer, so the chip in the status bar and the feed
|
|
1516
|
+
// can never name different versions.
|
|
1517
|
+
version: resolveMaudeVersion(),
|
|
1510
1518
|
// Cloud Phase 27 (DDR-209) — the capability that opens the cookieless
|
|
1511
1519
|
// canvas origin, minted per session by the proxy. Absent on a desktop,
|
|
1512
1520
|
// where the canvas origin is loopback and needs none. `canvasUrl()`
|
|
@@ -446,6 +446,34 @@ export function yieldsToArtboardDrag(
|
|
|
446
446
|
);
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Click-phase counterpart to `yieldsToArtboardDrag` (issue-78). The onClick
|
|
451
|
+
* handler below suppresses the browser's native `click` for every gesture it
|
|
452
|
+
* already routed as `select` on the matching pointerdown, so a just-selected
|
|
453
|
+
* element's own click handler doesn't also fire. But an artboard-chrome
|
|
454
|
+
* control — e.g. the video-timeline badge (`.dc-artboard-video-badge`,
|
|
455
|
+
* DDR-148) rendered as a sibling of `.dc-artboard-body` — never goes through
|
|
456
|
+
* `select`; it's an interactive button that needs its own `onClick` to reach
|
|
457
|
+
* React. Without this exemption a bare click on it while in Move (or a
|
|
458
|
+
* Cmd/Ctrl+click from Browse/an annotation tool) is preventDefault +
|
|
459
|
+
* stopImmediatePropagation'd in the capture phase before React's delegated
|
|
460
|
+
* listener ever runs, so the button silently does nothing. Mirrors
|
|
461
|
+
* `yieldsToArtboardDrag`'s modifier gate: a held Cmd/Ctrl is reserved for
|
|
462
|
+
* deep-select and stays unexempted.
|
|
463
|
+
*/
|
|
464
|
+
export function yieldsClickToArtboardChrome(
|
|
465
|
+
wouldRouteKind: RouterAction['kind'] | null,
|
|
466
|
+
target: EventTarget | null,
|
|
467
|
+
modifiers: { metaKey: boolean; ctrlKey: boolean }
|
|
468
|
+
): boolean {
|
|
469
|
+
return (
|
|
470
|
+
wouldRouteKind === 'select' &&
|
|
471
|
+
!modifiers.metaKey &&
|
|
472
|
+
!modifiers.ctrlKey &&
|
|
473
|
+
isArtboardDragChrome(target)
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
449
477
|
export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
450
478
|
const { hostRef, getActiveTool, isSpaceHeld, callbacks, enabled = true, claimableActions } = opts;
|
|
451
479
|
|
|
@@ -631,8 +659,18 @@ export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
|
631
659
|
? 'context-menu'
|
|
632
660
|
: null;
|
|
633
661
|
if (wouldRouteKind && (!claimableActions || claimableActions.has(wouldRouteKind))) {
|
|
662
|
+
// issue-78 — a bare click on an artboard-chrome control (the video-
|
|
663
|
+
// timeline badge) must still reach its own onClick; see
|
|
664
|
+
// yieldsClickToArtboardChrome for why. Mirrors the onPointerDown/
|
|
665
|
+
// onMouseDown pattern above: preventDefault always runs (so a forged
|
|
666
|
+
// chrome-alike element inside untrusted canvas content can't use this
|
|
667
|
+
// exemption to let its own native click default action — e.g. an
|
|
668
|
+
// <a>'s navigation or an <input type="file">'s picker — fire), but
|
|
669
|
+
// stopImmediatePropagation is skipped so the real chrome button's own
|
|
670
|
+
// React onClick still gets the (now-inert-by-default) event.
|
|
671
|
+
const yields = yieldsClickToArtboardChrome(wouldRouteKind, e.target, e);
|
|
634
672
|
e.preventDefault();
|
|
635
|
-
e.stopImmediatePropagation();
|
|
673
|
+
if (!yields) e.stopImmediatePropagation();
|
|
636
674
|
}
|
|
637
675
|
};
|
|
638
676
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The running version reaches the client — feature-release-reaches-the-fleet.
|
|
2
|
+
//
|
|
3
|
+
// Reading which release a Maude was actually running used to mean probing
|
|
4
|
+
// production by hand, and the first probe of the v0.57.0 incident was MISREAD
|
|
5
|
+
// (a 401 that looked like "route exists" was the blanket answer for any unknown
|
|
6
|
+
// path). The fix has three surfaces reporting one version: the hub's `/health`,
|
|
7
|
+
// this `/_config`, and the status-bar chip the client draws from it.
|
|
8
|
+
//
|
|
9
|
+
// What is asserted here is the middle one, plus the two properties that make it
|
|
10
|
+
// safe to put there: it is resolved through the SINGLE version function, and it
|
|
11
|
+
// carries nothing else.
|
|
12
|
+
|
|
13
|
+
import { afterAll, describe, expect, test } from 'bun:test';
|
|
14
|
+
import type { Subprocess } from 'bun';
|
|
15
|
+
|
|
16
|
+
import { resolveMaudeVersion } from '../whats-new.ts';
|
|
17
|
+
import { bootServer, killProc, makeSandbox, nextPort } from './_helpers.ts';
|
|
18
|
+
|
|
19
|
+
const procs: Subprocess[] = [];
|
|
20
|
+
afterAll(async () => {
|
|
21
|
+
for (const p of procs) await killProc(p);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
async function boot(env: Record<string, string> = {}) {
|
|
25
|
+
const { root } = makeSandbox();
|
|
26
|
+
const port = nextPort();
|
|
27
|
+
procs.push(await bootServer(root, port, env));
|
|
28
|
+
return `http://localhost:${port}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('/_config names the release it is running', () => {
|
|
32
|
+
test('the version is present, and is a real version', async () => {
|
|
33
|
+
const base = await boot();
|
|
34
|
+
const cfg = (await (await fetch(`${base}/_config`)).json()) as { version?: string };
|
|
35
|
+
|
|
36
|
+
expect(typeof cfg.version).toBe('string');
|
|
37
|
+
// `0.0.0` was the private-workspace placeholder these manifests carried
|
|
38
|
+
// before they joined the release line — reporting it is the bug, not a pass.
|
|
39
|
+
expect(cfg.version).not.toBe('0.0.0');
|
|
40
|
+
expect(cfg.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('it is the SAME answer the What’s New feed uses', async () => {
|
|
44
|
+
// One resolution path (DDR-045). Two would eventually disagree, and a chip
|
|
45
|
+
// that contradicts the feed beside it is worse than no chip.
|
|
46
|
+
const base = await boot();
|
|
47
|
+
const cfg = (await (await fetch(`${base}/_config`)).json()) as { version?: string };
|
|
48
|
+
const feed = (await (await fetch(`${base}/_api/whats-new`)).json()) as { version?: string };
|
|
49
|
+
|
|
50
|
+
expect(cfg.version).toBe(resolveMaudeVersion());
|
|
51
|
+
expect(cfg.version).toBe(feed.version);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('the version rides along, and nothing else does', () => {
|
|
56
|
+
test('a cloud tab gets the version too', async () => {
|
|
57
|
+
// `/_config` reaches a browser on someone else's machine in workspace mode.
|
|
58
|
+
// A git tag is public, so the version is fine there — this pins that it
|
|
59
|
+
// actually arrives, since the chip is the cloud's only version surface.
|
|
60
|
+
const base = await boot({
|
|
61
|
+
MAUDE_WORKSPACE_MODE: '1',
|
|
62
|
+
MAUDE_WORKSPACE_ALLOW_DEV_MODULES: '1',
|
|
63
|
+
MAUDE_PROJECT_NAME: 'Alligators',
|
|
64
|
+
});
|
|
65
|
+
const cfg = (await (await fetch(`${base}/_config`)).json()) as {
|
|
66
|
+
version?: string;
|
|
67
|
+
cloud?: unknown;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
expect(typeof cfg.version).toBe('string');
|
|
71
|
+
expect(cfg.cloud).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('no server path or secret came with it', async () => {
|
|
75
|
+
const base = await boot();
|
|
76
|
+
const cfg = (await (await fetch(`${base}/_config`)).json()) as Record<string, unknown>;
|
|
77
|
+
|
|
78
|
+
// The version was the one field this change added. Anything matching these
|
|
79
|
+
// shapes arriving in the browser payload is a leak, and the review that
|
|
80
|
+
// waves it through will be the one that reads "it was already there".
|
|
81
|
+
for (const key of Object.keys(cfg)) {
|
|
82
|
+
expect(key).not.toMatch(/token$/i);
|
|
83
|
+
expect(key).not.toMatch(/secret|password|apiKey/i);
|
|
84
|
+
}
|
|
85
|
+
const serialized = JSON.stringify(cfg);
|
|
86
|
+
expect(serialized).not.toMatch(/BEGIN [A-Z ]*PRIVATE KEY/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isArtboardDragChrome,
|
|
11
11
|
isEditableTarget,
|
|
12
12
|
type Tool,
|
|
13
|
+
yieldsClickToArtboardChrome,
|
|
13
14
|
yieldsToArtboardDrag,
|
|
14
15
|
} from '../input-router.tsx';
|
|
15
16
|
|
|
@@ -552,6 +553,61 @@ describe('input-router / yieldsToArtboardDrag', () => {
|
|
|
552
553
|
});
|
|
553
554
|
});
|
|
554
555
|
|
|
556
|
+
// issue-78 — the actual regression: the router's capture-phase `click`
|
|
557
|
+
// listener claimed every bare click while in the Move tool (and every
|
|
558
|
+
// Cmd/Ctrl+click from Browse/annotation tools) and called
|
|
559
|
+
// `stopImmediatePropagation()` unconditionally, unlike its pointerdown/
|
|
560
|
+
// mousedown siblings above. That killed the native click on any artboard-
|
|
561
|
+
// chrome control BEFORE React's delegated onClick ever ran — including the
|
|
562
|
+
// video-timeline badge (`.dc-artboard-video-badge`, DDR-148), which sits
|
|
563
|
+
// outside `.dc-artboard-body` as chrome. Clicking it did nothing.
|
|
564
|
+
// `yieldsClickToArtboardChrome` is the decision the click handler now gates
|
|
565
|
+
// its suppression on, mirroring `yieldsToArtboardDrag` above.
|
|
566
|
+
describe('input-router / yieldsClickToArtboardChrome', () => {
|
|
567
|
+
const chrome = (): EventTarget =>
|
|
568
|
+
({
|
|
569
|
+
closest: (sel: string) => (sel === '[data-dc-screen]' ? {} : null),
|
|
570
|
+
}) as unknown as EventTarget;
|
|
571
|
+
const body = (): EventTarget => ({ closest: () => ({}) }) as unknown as EventTarget; // matches every selector, incl. .dc-artboard-body
|
|
572
|
+
|
|
573
|
+
test('bare click on the video-timeline badge in Move tool (the reported bug) → true', () => {
|
|
574
|
+
expect(
|
|
575
|
+
yieldsClickToArtboardChrome('select', chrome(), { metaKey: false, ctrlKey: false })
|
|
576
|
+
).toBe(true);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test('cmd+click on artboard chrome (deep-select, not a chrome control) → false', () => {
|
|
580
|
+
expect(yieldsClickToArtboardChrome('select', chrome(), { metaKey: true, ctrlKey: false })).toBe(
|
|
581
|
+
false
|
|
582
|
+
);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('bare click on .dc-artboard-body content (not chrome) → false', () => {
|
|
586
|
+
expect(yieldsClickToArtboardChrome('select', body(), { metaKey: false, ctrlKey: false })).toBe(
|
|
587
|
+
false
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('bare click outside any artboard → false (nothing to yield to)', () => {
|
|
592
|
+
const outside = { closest: () => null } as unknown as EventTarget;
|
|
593
|
+
expect(yieldsClickToArtboardChrome('select', outside, { metaKey: false, ctrlKey: false })).toBe(
|
|
594
|
+
false
|
|
595
|
+
);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test('non-select routed kind (e.g. context-menu) on chrome → false', () => {
|
|
599
|
+
expect(
|
|
600
|
+
yieldsClickToArtboardChrome('context-menu', chrome(), { metaKey: false, ctrlKey: false })
|
|
601
|
+
).toBe(false);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test('null routed kind on chrome → false', () => {
|
|
605
|
+
expect(yieldsClickToArtboardChrome(null, chrome(), { metaKey: false, ctrlKey: false })).toBe(
|
|
606
|
+
false
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
555
611
|
// T25 — Drag-vs-click threshold. Centralized here so every drag-class gesture
|
|
556
612
|
// (artboard drag, artboard marquee, element marquee, annotation pen/rect/etc.)
|
|
557
613
|
// reads the same value. Microsoft Win32 SM_CXDRAG default + d3-drag convention.
|
|
@@ -23,8 +23,20 @@ describe('whats-new feed loader', () => {
|
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
test('resolveMaudeVersion
|
|
26
|
+
test('resolveMaudeVersion resolves a real release version', () => {
|
|
27
27
|
expect(resolveMaudeVersion()).toMatch(/^\d+\.\d+\.\d+/);
|
|
28
|
+
// The studio's own manifest is preferred over the design plugin's (it is
|
|
29
|
+
// the one staged into the cell image), and `0.0.0` — the placeholder both
|
|
30
|
+
// carried before joining the release line — is never an answer.
|
|
31
|
+
expect(resolveMaudeVersion()).not.toBe('0.0.0');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('an unversioned root falls through rather than reporting the placeholder', () => {
|
|
35
|
+
const root = mkdtempSync(join(tmpdir(), 'maude-ver-'));
|
|
36
|
+
writeFileSync(join(root, 'package.json'), JSON.stringify({ version: '0.0.0' }));
|
|
37
|
+
// No plugin manifest above a tmpdir either, so this lands on `dev` — a
|
|
38
|
+
// value obviously not a release, rather than a plausible wrong number.
|
|
39
|
+
expect(resolveMaudeVersion(root)).toBe('dev');
|
|
28
40
|
});
|
|
29
41
|
|
|
30
42
|
test('a missing feed dir yields empty entries and never throws', () => {
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./whats-new.schema.json",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "release-version-visible",
|
|
6
|
+
"version": "0.58.0",
|
|
7
|
+
"date": "2026-08-07",
|
|
8
|
+
"kind": "improvement",
|
|
9
|
+
"title": "See which version of Maude you are running",
|
|
10
|
+
"summary": "The Studio status bar now shows the running version, and the hub admin header shows it beside the bundle hash — so you can tell what you are on without digging. In the cloud it is the same chip, because the cloud serves the same Studio.",
|
|
11
|
+
"surface": "design-ui"
|
|
12
|
+
},
|
|
4
13
|
{
|
|
5
14
|
"id": "cloud-connect-tells-you-what-happened",
|
|
6
15
|
"version": "0.57.0",
|
package/apps/studio/whats-new.ts
CHANGED
|
@@ -43,18 +43,42 @@ export interface WhatsNewFeed {
|
|
|
43
43
|
let cache: WhatsNewFeed | null = null;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Read the installed maude version
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* Read the installed maude version.
|
|
47
|
+
*
|
|
48
|
+
* THE ONE resolution path (DDR-045: disk paths come from `paths.ts`). Everything
|
|
49
|
+
* that wants to name the running version — the What's New feed, `/_config`, the
|
|
50
|
+
* status-bar chip — calls this rather than growing a second answer that can
|
|
51
|
+
* disagree with the first.
|
|
52
|
+
*
|
|
53
|
+
* Two sources, in order:
|
|
54
|
+
*
|
|
55
|
+
* 1. the studio's OWN `package.json`. It rides the release line since the
|
|
56
|
+
* fleet-verification change, and it is the only one of the two that is
|
|
57
|
+
* staged into the cell image — a cloud tab asking "which version am I on"
|
|
58
|
+
* gets an answer rather than `dev`.
|
|
59
|
+
* 2. `plugins/design/.claude-plugin/plugin.json`, which ships in BOTH npm
|
|
60
|
+
* installs and marketplace-cache clones (same rationale as build.ts
|
|
61
|
+
* `readPluginVersion`). Kept as the fallback so an older layout that lacks
|
|
62
|
+
* a version-stamped studio manifest still resolves.
|
|
63
|
+
*
|
|
64
|
+
* Falls back to `dev` — a placeholder that is obviously not a release, never a
|
|
65
|
+
* plausible-looking wrong number.
|
|
50
66
|
*/
|
|
51
67
|
export function resolveMaudeVersion(root: string = DEV_SERVER_ROOT): string {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
const candidates = [
|
|
69
|
+
join(root, 'package.json'),
|
|
70
|
+
join(root, '..', '..', 'plugins', 'design', '.claude-plugin', 'plugin.json'),
|
|
71
|
+
];
|
|
72
|
+
for (const manifest of candidates) {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(readFileSync(manifest, 'utf8')) as { version?: unknown };
|
|
75
|
+
// `0.0.0` is the private-workspace placeholder these manifests carried
|
|
76
|
+
// before they joined the release line. Treating it as an answer is how
|
|
77
|
+
// `/health` came to report `0.0.0` for months.
|
|
78
|
+
if (typeof parsed.version === 'string' && parsed.version !== '0.0.0') return parsed.version;
|
|
79
|
+
} catch {
|
|
80
|
+
/* try the next one */
|
|
81
|
+
}
|
|
58
82
|
}
|
|
59
83
|
return 'dev';
|
|
60
84
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.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": {
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
"test:e2e:desktop:parity:cloud": "pnpm --filter @maude/desktop-e2e e2e:parity:cloud"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"@1agh/maude-darwin-arm64": "0.
|
|
58
|
-
"@1agh/maude-darwin-x64": "0.
|
|
59
|
-
"@1agh/maude-linux-arm64": "0.
|
|
60
|
-
"@1agh/maude-linux-arm64-musl": "0.
|
|
61
|
-
"@1agh/maude-linux-x64": "0.
|
|
62
|
-
"@1agh/maude-linux-x64-musl": "0.
|
|
63
|
-
"@1agh/maude-win32-x64": "0.
|
|
57
|
+
"@1agh/maude-darwin-arm64": "0.58.0",
|
|
58
|
+
"@1agh/maude-darwin-x64": "0.58.0",
|
|
59
|
+
"@1agh/maude-linux-arm64": "0.58.0",
|
|
60
|
+
"@1agh/maude-linux-arm64-musl": "0.58.0",
|
|
61
|
+
"@1agh/maude-linux-x64": "0.58.0",
|
|
62
|
+
"@1agh/maude-linux-x64-musl": "0.58.0",
|
|
63
|
+
"@1agh/maude-win32-x64": "0.58.0"
|
|
64
64
|
},
|
|
65
65
|
"files": [
|
|
66
66
|
"cli",
|