@1agh/maude 1.4.1 → 1.4.3
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/apps/studio/canvas-lib.tsx +28 -6
- package/apps/studio/client/app.jsx +4 -10
- package/apps/studio/client/command-palette-match.js +35 -0
- package/apps/studio/collab/index.ts +40 -10
- package/apps/studio/collab/persistence.ts +16 -4
- package/apps/studio/collab/registry.ts +26 -10
- package/apps/studio/dist/client.bundle.js +480 -480
- package/apps/studio/http.ts +27 -8
- package/apps/studio/sync/index.ts +129 -22
- package/apps/studio/whats-new.json +9 -0
- package/package.json +8 -8
- package/plugins/design/templates/_shell.html +32 -1
- package/plugins/flow/.claude-plugin/config.schema.json +33 -0
package/apps/studio/http.ts
CHANGED
|
@@ -599,20 +599,26 @@ export function localDepsFromSource(
|
|
|
599
599
|
/** mtime signature over the .tsx + every inlined relative dep (`.css` + local
|
|
600
600
|
* modules). A missing/unreadable file contributes 0 — a delete is itself a
|
|
601
601
|
* change (and a later create too), so the signature differs. */
|
|
602
|
-
function canvasFreshnessSig(tsxAbsPath: string, deps: string[]): string {
|
|
602
|
+
function canvasFreshnessSig(tsxAbsPath: string, deps: string[], tsxMtime?: number): string {
|
|
603
603
|
const parts: string[] = [];
|
|
604
604
|
for (const p of [tsxAbsPath, ...deps]) {
|
|
605
|
-
const mt = Bun.file(p).lastModified;
|
|
605
|
+
const mt = p === tsxAbsPath && tsxMtime !== undefined ? tsxMtime : Bun.file(p).lastModified;
|
|
606
606
|
parts.push(`${p}@${Number.isFinite(mt) ? mt : 0}`);
|
|
607
607
|
}
|
|
608
608
|
return parts.join('|');
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
-
|
|
611
|
+
/** @internal Test seam — runs between reading the source and storing the build. */
|
|
612
|
+
export interface ServeCanvasTestHooks {
|
|
613
|
+
afterRead?: () => void | Promise<void>;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export async function serveCanvasTsx(
|
|
612
617
|
absPath: string,
|
|
613
618
|
req: Request,
|
|
614
619
|
ctx: Context,
|
|
615
|
-
locatorAbsPath: string
|
|
620
|
+
locatorAbsPath: string,
|
|
621
|
+
testHooks?: ServeCanvasTestHooks
|
|
616
622
|
): Promise<Response> {
|
|
617
623
|
// Phase 27 (E2) — DiffView "before" pane. `?sha=<ref>` builds the canvas from
|
|
618
624
|
// its source AT a past version (git show) instead of the working-tree file, so
|
|
@@ -634,8 +640,21 @@ async function serveCanvasTsx(
|
|
|
634
640
|
const sig = canvasFreshnessSig(absPath, cached?.deps ?? []);
|
|
635
641
|
|
|
636
642
|
if (!cached || cached.sig !== sig) {
|
|
643
|
+
// THE SIGNATURE OF WHAT WAS READ, taken before the read.
|
|
644
|
+
//
|
|
645
|
+
// It used to be recomputed after the build. A build is slow — out of
|
|
646
|
+
// process on a cell — and a canvas that changed again meanwhile stored the
|
|
647
|
+
// OLD module under the NEW file's signature, so the next request (the HMR
|
|
648
|
+
// for that very change) was a cache hit and served the previous body: a
|
|
649
|
+
// view stuck one edit behind while the file was right (surface row L23 on
|
|
650
|
+
// the hub, 2026-09-16). Stamped before reading, a change during the build
|
|
651
|
+
// leaves the stored signature older than the file, and the next request
|
|
652
|
+
// rebuilds. A dep is stamped before the build reads it, for the same reason.
|
|
653
|
+
const tsxMtime = file.lastModified;
|
|
637
654
|
const source = await file.text();
|
|
638
655
|
const deps = localDepsFromSource(source, absPath, ctx.paths.designRoot);
|
|
656
|
+
const builtSig = canvasFreshnessSig(absPath, deps, tsxMtime);
|
|
657
|
+
await testHooks?.afterRead?.();
|
|
639
658
|
let result: Awaited<ReturnType<typeof buildCanvasModule>>;
|
|
640
659
|
// DDR-209 A′2 — SAME ENGINE, DIFFERENT HOST. On a desktop the process that
|
|
641
660
|
// parses your canvas is the process you own, so an in-process build costs
|
|
@@ -662,7 +681,7 @@ async function serveCanvasTsx(
|
|
|
662
681
|
etag: built.etag,
|
|
663
682
|
} as Awaited<ReturnType<typeof buildCanvasModule>>;
|
|
664
683
|
cached = {
|
|
665
|
-
sig:
|
|
684
|
+
sig: builtSig,
|
|
666
685
|
etag: `${result.etag}-${RUNTIME_BOOT_ID}-${CHROME_EPOCH}`,
|
|
667
686
|
js: result.js,
|
|
668
687
|
deps,
|
|
@@ -694,10 +713,10 @@ async function serveCanvasTsx(
|
|
|
694
713
|
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
695
714
|
});
|
|
696
715
|
}
|
|
697
|
-
//
|
|
698
|
-
//
|
|
716
|
+
// The signature against the freshly-parsed deps — this very edit may have
|
|
717
|
+
// added or removed a `.css` import — stamped before the build (above).
|
|
699
718
|
cached = {
|
|
700
|
-
sig:
|
|
719
|
+
sig: builtSig,
|
|
701
720
|
// Fold in the boot id (restart) + chrome epoch (live edit) so a chrome
|
|
702
721
|
// change busts the browser's cached transpile even when the canvas source
|
|
703
722
|
// (hence result.etag) is unchanged. See RUNTIME_BOOT_ID / CHROME_EPOCH.
|
|
@@ -2009,6 +2009,30 @@ export function createSyncRuntime(
|
|
|
2009
2009
|
* canvas accepted a moment ago is pulled on the next poll, not the one after
|
|
2010
2010
|
* its row is written.
|
|
2011
2011
|
*/
|
|
2012
|
+
/**
|
|
2013
|
+
* Where the PROJECT says a document lives, under accepted revisions — or
|
|
2014
|
+
* null when the project is not the authority (legacy mode, no bootstrap yet,
|
|
2015
|
+
* a retired entry, or a path it never recorded).
|
|
2016
|
+
*
|
|
2017
|
+
* A pulled canvas used to start at the slug-derived fallback
|
|
2018
|
+
* (`ui/surfaceboards-peer.tsx`) and wait for its document's own path to
|
|
2019
|
+
* correct it after the handshake. A fresh copy that reached the handshake
|
|
2020
|
+
* before that path was readable kept the fallback, and on a case-insensitive
|
|
2021
|
+
* disk the lowercase file then sat beside nothing and matched nothing on its
|
|
2022
|
+
* peers (surface run 2026-09-16, L24 final parity). The manifest already
|
|
2023
|
+
* knows the answer before anything is fetched.
|
|
2024
|
+
*/
|
|
2025
|
+
const manifestPathFor = (docName: string, slug?: string): string | null => {
|
|
2026
|
+
if (!acceptedOn()) return null;
|
|
2027
|
+
// By slug when there is one: the listing and the runtime name the same
|
|
2028
|
+
// document in more than one spelling (`ui-x`, `ws/<w>/<b>/ui-x`).
|
|
2029
|
+
const key = slug ?? slugFromDocName(docName);
|
|
2030
|
+
const entry = acceptedLink?.manifest?.docs.find(
|
|
2031
|
+
(d) => !d.retired && (d.doc === docName || slugFromDocName(d.doc) === key)
|
|
2032
|
+
);
|
|
2033
|
+
return typeof entry?.path === 'string' && entry.path.length > 0 ? entry.path : null;
|
|
2034
|
+
};
|
|
2035
|
+
|
|
2012
2036
|
function withAcceptedDocs(
|
|
2013
2037
|
listing: Awaited<ReturnType<typeof fetchRemoteListing>>
|
|
2014
2038
|
): Awaited<ReturnType<typeof fetchRemoteListing>> {
|
|
@@ -2310,7 +2334,7 @@ export function createSyncRuntime(
|
|
|
2310
2334
|
path.join,
|
|
2311
2335
|
path.resolve,
|
|
2312
2336
|
path.sep,
|
|
2313
|
-
{ ...pathOpts, realpath: realpathOfDeepestExisting }
|
|
2337
|
+
{ ...pathOpts, realpath: realpathOfDeepestExisting, pathFor: manifestPathFor }
|
|
2314
2338
|
);
|
|
2315
2339
|
const pullNote = describeRemoteDiff(remoteDiff);
|
|
2316
2340
|
if (pullNote) console.log(`[sync] ${pullNote}`);
|
|
@@ -3322,7 +3346,9 @@ export function createSyncRuntime(
|
|
|
3322
3346
|
if (movedToFromDoc(doc) !== null) return true;
|
|
3323
3347
|
const resolved = resolvePulledTarget({
|
|
3324
3348
|
slug: canvas.slug,
|
|
3325
|
-
|
|
3349
|
+
// The document's own record first (it is what a move rewrites), the
|
|
3350
|
+
// project's manifest when the document has not carried one yet.
|
|
3351
|
+
path: canvasPathFromDoc(doc) ?? manifestPathFor(docNameFor(canvas.slug), canvas.slug),
|
|
3326
3352
|
designRoot: ctx.paths.designRoot,
|
|
3327
3353
|
designRel: ctx.paths.designRel,
|
|
3328
3354
|
canvasGroups: ctx.cfg.canvasGroups,
|
|
@@ -3507,8 +3533,23 @@ export function createSyncRuntime(
|
|
|
3507
3533
|
);
|
|
3508
3534
|
}
|
|
3509
3535
|
// Cold-start reconcile fires once the provider has hub state.
|
|
3510
|
-
const synced = provider.onceSynced().then(() => {
|
|
3536
|
+
const synced = provider.onceSynced().then(async () => {
|
|
3511
3537
|
if (deferSetup) {
|
|
3538
|
+
// A DOCUMENT THAT DOES NOT NAME ITS PATH YET, UNDER A MANIFEST THAT
|
|
3539
|
+
// PREDATES IT. A canvas created a moment ago is listed before this
|
|
3540
|
+
// copy's last bootstrap knew it, and its document can answer the
|
|
3541
|
+
// handshake before the accepted write that carries its path — so
|
|
3542
|
+
// neither source names the path and the slug fallback (lowercase)
|
|
3543
|
+
// would be written for good (surface run 2026-09-16: two long-running
|
|
3544
|
+
// copies held `ui/surfaceassetsbusy.tsx`). Ask the project once.
|
|
3545
|
+
if (
|
|
3546
|
+
acceptedOn() &&
|
|
3547
|
+
canvasPathFromDoc(provider.document) === null &&
|
|
3548
|
+
manifestPathFor(docNameFor(canvas.slug), canvas.slug) === null
|
|
3549
|
+
) {
|
|
3550
|
+
await acceptedLink?.refresh().catch(() => null);
|
|
3551
|
+
}
|
|
3552
|
+
if (stopped) return;
|
|
3512
3553
|
// Settled either way below — run, or the canvas is released.
|
|
3513
3554
|
owedSetups.delete(canvas.slug);
|
|
3514
3555
|
// Abandon before `setup?.()`, so no projection and no agent is ever
|
|
@@ -3892,7 +3933,12 @@ export function createSyncRuntime(
|
|
|
3892
3933
|
// days. Once the body is on disk the ordinary rules apply to it.
|
|
3893
3934
|
for (const slug of [...pulledSlugs]) {
|
|
3894
3935
|
const body = descriptors.get(slug)?.html;
|
|
3895
|
-
|
|
3936
|
+
// …and only once THIS scan has seen it. A scan taken just before the
|
|
3937
|
+
// body landed does not list it; dropping the pin anyway released a
|
|
3938
|
+
// canvas pulled a moment ago and re-adopted it as a local file, whose
|
|
3939
|
+
// cold start then read its own first write as a local edit and held
|
|
3940
|
+
// the canvas ("source sync blocked (local-edit)" on a fresh copy).
|
|
3941
|
+
if (body && existsSync(body) && bySlug.has(slug)) pulledSlugs.delete(slug);
|
|
3896
3942
|
}
|
|
3897
3943
|
const { added, removed } = diffCanvasSet(
|
|
3898
3944
|
[...agents.keys(), ...projections.keys()],
|
|
@@ -4043,6 +4089,14 @@ export function createSyncRuntime(
|
|
|
4043
4089
|
// not told the document exists.
|
|
4044
4090
|
const pullRemoteOnce = async (): Promise<void> => {
|
|
4045
4091
|
if (stopped) return;
|
|
4092
|
+
// Read `token` at call time: a silent renewal swaps it in place.
|
|
4093
|
+
//
|
|
4094
|
+
// The listing FIRST, the manifest after it. A document the storage
|
|
4095
|
+
// listing shows was accepted before it was stored, so a manifest fetched
|
|
4096
|
+
// afterwards names it — and its path. The other order left a window in
|
|
4097
|
+
// which a canvas created between the two requests was pulled with no
|
|
4098
|
+
// path from either source (see `manifestPathFor`).
|
|
4099
|
+
const rawListing = await fetchRemoteListing(linkedHub.url, token);
|
|
4046
4100
|
// Accepted revisions: the save mode can change under a running peer (the
|
|
4047
4101
|
// hub fences every socket when it does), and folders are manifest
|
|
4048
4102
|
// entries — neither is in the document listing.
|
|
@@ -4050,8 +4104,7 @@ export function createSyncRuntime(
|
|
|
4050
4104
|
await acceptedLink.refresh();
|
|
4051
4105
|
if (acceptedOn()) applyProjectDirs(acceptedLink.manifest?.dirs ?? []);
|
|
4052
4106
|
}
|
|
4053
|
-
|
|
4054
|
-
const listing = withAcceptedDocs(await fetchRemoteListing(linkedHub.url, token));
|
|
4107
|
+
const listing = withAcceptedDocs(rawListing);
|
|
4055
4108
|
// null = unreachable, refused, or a hub without the route. Not an error
|
|
4056
4109
|
// here any more than it is at boot — sync continues, we ask again later.
|
|
4057
4110
|
if (stopped || listing === null) return;
|
|
@@ -4078,6 +4131,7 @@ export function createSyncRuntime(
|
|
|
4078
4131
|
// NEVER the fresh-link relaxation after boot. See `strictPullSlugs`.
|
|
4079
4132
|
allowUndeclaredGroup: false,
|
|
4080
4133
|
realpath: realpathOfDeepestExisting,
|
|
4134
|
+
pathFor: manifestPathFor,
|
|
4081
4135
|
}
|
|
4082
4136
|
);
|
|
4083
4137
|
const admitted = targets
|
|
@@ -5504,9 +5558,26 @@ export interface DisposableProviderFactory extends ProviderFactory {
|
|
|
5504
5558
|
}
|
|
5505
5559
|
|
|
5506
5560
|
/**
|
|
5507
|
-
*
|
|
5508
|
-
*
|
|
5509
|
-
*
|
|
5561
|
+
* Most documents one shared socket carries. Hocuspocus 4.3+ closes a socket
|
|
5562
|
+
* with more than 100 documents mid-authentication (`maxPendingDocuments`), so
|
|
5563
|
+
* this stays well under it with room for the refused documents a hub keeps
|
|
5564
|
+
* counting — see the socket choice in `createDefaultProviderFactory`.
|
|
5565
|
+
*/
|
|
5566
|
+
export const SOCKET_DOCUMENT_LIMIT = 64;
|
|
5567
|
+
|
|
5568
|
+
/** `SOCKET_DOCUMENT_LIMIT`, or a smaller positive override from
|
|
5569
|
+
* `MAUDE_SYNC_SOCKET_DOCUMENTS` — so a harness can put a small project on
|
|
5570
|
+
* several sockets without having to build a large one first. */
|
|
5571
|
+
function socketDocumentLimit(): number {
|
|
5572
|
+
const n = Number.parseInt(process.env.MAUDE_SYNC_SOCKET_DOCUMENTS ?? '', 10);
|
|
5573
|
+
return Number.isFinite(n) && n > 0 && n < SOCKET_DOCUMENT_LIMIT ? n : SOCKET_DOCUMENT_LIMIT;
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5576
|
+
/**
|
|
5577
|
+
* The production provider factory — DDR-102: shared
|
|
5578
|
+
* `HocuspocusProviderWebsocket`s per hub URL, each carrying up to
|
|
5579
|
+
* `SOCKET_DOCUMENT_LIMIT` canvases' `HocuspocusProvider`s, instead of one
|
|
5580
|
+
* socket per canvas.
|
|
5510
5581
|
* An 83-canvas project used to open 83 WebSockets at boot — the auth burst
|
|
5511
5582
|
* tripped the hub's per-token rate limit (100/min) the moment two peers
|
|
5512
5583
|
* booted together, and the per-socket retry storm then pinned the bucket
|
|
@@ -5524,10 +5595,13 @@ export function createDefaultProviderFactory(
|
|
|
5524
5595
|
): DisposableProviderFactory {
|
|
5525
5596
|
// biome-ignore lint/suspicious/noExplicitAny: provider runtime typed at call site.
|
|
5526
5597
|
let mod: any = null;
|
|
5527
|
-
// wsUrl → shared
|
|
5528
|
-
// runtime only ever talks to
|
|
5598
|
+
// wsUrl → the shared sockets to that hub, each carrying at most
|
|
5599
|
+
// SOCKET_DOCUMENT_LIMIT documents (in practice a runtime only ever talks to
|
|
5600
|
+
// one hub, but the map keeps the contract exact).
|
|
5529
5601
|
// biome-ignore lint/suspicious/noExplicitAny: provider runtime typed at call site.
|
|
5530
|
-
const sockets = new Map<string, any>();
|
|
5602
|
+
const sockets = new Map<string, Array<{ socket: any; names: Set<string> }>>();
|
|
5603
|
+
const allSockets = () => [...sockets.values()].flat().map((s) => s.socket);
|
|
5604
|
+
const perSocket = socketDocumentLimit();
|
|
5531
5605
|
|
|
5532
5606
|
const factory = async (args: {
|
|
5533
5607
|
url: string;
|
|
@@ -5548,8 +5622,26 @@ export function createDefaultProviderFactory(
|
|
|
5548
5622
|
// the scheme. The provider also accepts http(s):// and upgrades internally
|
|
5549
5623
|
// in newer versions, but ws:// is explicit + portable.
|
|
5550
5624
|
const wsUrl = toWsUrl(args.url);
|
|
5551
|
-
|
|
5552
|
-
|
|
5625
|
+
// A DOCUMENT KEEPS ITS SOCKET; A FULL SOCKET TAKES NO NEW ONES.
|
|
5626
|
+
//
|
|
5627
|
+
// Hocuspocus (4.3+) closes a socket that has more than 100 documents
|
|
5628
|
+
// mid-authentication, and every document on a socket authenticates at
|
|
5629
|
+
// once — when it opens, and again on every reconnect. One socket for a
|
|
5630
|
+
// 123-canvas project was therefore cut off, reopened and cut off again
|
|
5631
|
+
// for as long as the copy ran: it never synced, and each lap spent the
|
|
5632
|
+
// designer's whole per-label allowance (2026-09-16 certification run,
|
|
5633
|
+
// 4 618 closes). Newer hubs raise the limit; deployed ones keep it.
|
|
5634
|
+
//
|
|
5635
|
+
// Names are never taken back off a socket. The hub keeps a pending slot
|
|
5636
|
+
// for a refused document that sends anything after its refusal — a
|
|
5637
|
+
// detach's Close included — until the socket closes, so what a socket has
|
|
5638
|
+
// EVER carried is what counts against the limit, not what it carries now.
|
|
5639
|
+
const shards = sockets.get(wsUrl) ?? [];
|
|
5640
|
+
sockets.set(wsUrl, shards);
|
|
5641
|
+
let shard =
|
|
5642
|
+
shards.find((s) => s.names.has(args.documentName)) ??
|
|
5643
|
+
shards.find((s) => s.names.size < perSocket);
|
|
5644
|
+
if (!shard) {
|
|
5553
5645
|
// CONFIGURED, not defaulted (issue #118). The socket used to be built
|
|
5554
5646
|
// from the URL alone, which inherited `timeout: 0` — no per-attempt
|
|
5555
5647
|
// deadline — and that is the property that let one parked connection
|
|
@@ -5570,13 +5662,28 @@ export function createDefaultProviderFactory(
|
|
|
5570
5662
|
//
|
|
5571
5663
|
// Liveness is the runtime's job instead — see the stall watchdog in
|
|
5572
5664
|
// `start()`, which is a layer we control and can make safe.
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5665
|
+
shard = {
|
|
5666
|
+
socket: new mod.HocuspocusProviderWebsocket({
|
|
5667
|
+
url: wsUrl,
|
|
5668
|
+
messageReconnectTimeout: 30_000,
|
|
5669
|
+
maxDelay: 30_000,
|
|
5670
|
+
}),
|
|
5671
|
+
names: new Set<string>(),
|
|
5672
|
+
};
|
|
5673
|
+
shards.push(shard);
|
|
5674
|
+
if (process.env.MAUDE_SYNC_DEBUG === '1') {
|
|
5675
|
+
const tag = `[sync/socket ${shards.length}]`;
|
|
5676
|
+
const own = shard;
|
|
5677
|
+
own.socket.on('open', () => console.log(`${tag} open · ${own.names.size} document(s)`));
|
|
5678
|
+
own.socket.on('close', (e: { event?: { code?: number; reason?: string } }) =>
|
|
5679
|
+
console.log(
|
|
5680
|
+
`${tag} close ${e?.event?.code ?? '?'} ${e?.event?.reason ?? ''} · ${own.names.size} document(s)`
|
|
5681
|
+
)
|
|
5682
|
+
);
|
|
5683
|
+
}
|
|
5579
5684
|
}
|
|
5685
|
+
shard.names.add(args.documentName);
|
|
5686
|
+
const socket = shard.socket;
|
|
5580
5687
|
// Phase 9.2 (DDR-064) — attach to the shared room doc when the runtime
|
|
5581
5688
|
// injected one; otherwise own a fresh doc (the legacy two-doc path).
|
|
5582
5689
|
const document = args.document ?? new Y.Doc();
|
|
@@ -5723,7 +5830,7 @@ export function createDefaultProviderFactory(
|
|
|
5723
5830
|
|
|
5724
5831
|
return Object.assign(factory, {
|
|
5725
5832
|
dispose(): void {
|
|
5726
|
-
for (const socket of
|
|
5833
|
+
for (const socket of allSockets()) {
|
|
5727
5834
|
try {
|
|
5728
5835
|
socket.destroy();
|
|
5729
5836
|
} catch {
|
|
@@ -5733,7 +5840,7 @@ export function createDefaultProviderFactory(
|
|
|
5733
5840
|
sockets.clear();
|
|
5734
5841
|
},
|
|
5735
5842
|
reconnect(): void {
|
|
5736
|
-
for (const socket of
|
|
5843
|
+
for (const socket of allSockets()) {
|
|
5737
5844
|
try {
|
|
5738
5845
|
// NOT `disconnect()` + `connect()` — that pair WEDGES THE SOCKET SHUT,
|
|
5739
5846
|
// deterministically, in exactly the state this is called from
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./whats-new.schema.json",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "steadier-team-projects",
|
|
6
|
+
"version": "1.4.2",
|
|
7
|
+
"date": "2026-09-16",
|
|
8
|
+
"kind": "fix",
|
|
9
|
+
"title": "Steadier team projects",
|
|
10
|
+
"summary": "Large projects (over a hundred canvases) stay connected, the desktop app no longer freezes after switching canvases back and forth, and an open canvas always shows the newest change — including photo edits — instead of sometimes lagging one behind.",
|
|
11
|
+
"surface": "design-ui"
|
|
12
|
+
},
|
|
4
13
|
{
|
|
5
14
|
"id": "history-on-a-team-project",
|
|
6
15
|
"version": "1.4.1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
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": {
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"test:e2e:desktop:parity:cloud": "pnpm --filter @maude/desktop-e2e e2e:parity:cloud"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"@1agh/maude-darwin-arm64": "1.4.
|
|
62
|
-
"@1agh/maude-darwin-x64": "1.4.
|
|
63
|
-
"@1agh/maude-linux-arm64": "1.4.
|
|
64
|
-
"@1agh/maude-linux-arm64-musl": "1.4.
|
|
65
|
-
"@1agh/maude-linux-x64": "1.4.
|
|
66
|
-
"@1agh/maude-linux-x64-musl": "1.4.
|
|
67
|
-
"@1agh/maude-win32-x64": "1.4.
|
|
61
|
+
"@1agh/maude-darwin-arm64": "1.4.3",
|
|
62
|
+
"@1agh/maude-darwin-x64": "1.4.3",
|
|
63
|
+
"@1agh/maude-linux-arm64": "1.4.3",
|
|
64
|
+
"@1agh/maude-linux-arm64-musl": "1.4.3",
|
|
65
|
+
"@1agh/maude-linux-x64": "1.4.3",
|
|
66
|
+
"@1agh/maude-linux-x64-musl": "1.4.3",
|
|
67
|
+
"@1agh/maude-win32-x64": "1.4.3"
|
|
68
68
|
},
|
|
69
69
|
"files": [
|
|
70
70
|
"cli",
|
|
@@ -272,12 +272,24 @@
|
|
|
272
272
|
// the runtime is mounted — agent edits, manual edits, AND cross-peer synced
|
|
273
273
|
// writes — so presence + in-canvas state survive. Falls back to the plain
|
|
274
274
|
// location.reload() below only when no runtime is present.
|
|
275
|
+
//
|
|
276
|
+
// THE NEWEST REQUEST WINS, NOT THE LAST RESPONSE. Two changes in quick
|
|
277
|
+
// succession start two imports, and nothing makes them finish in the
|
|
278
|
+
// order they started — the first can be the slower build. Remounting
|
|
279
|
+
// whichever resolved last put the OLDER body on screen over the newer
|
|
280
|
+
// one while the file on disk was right (surface row L21, a peer's view
|
|
281
|
+
// stuck one edit behind). Only the latest request may remount or hold.
|
|
282
|
+
// soft-reload:begin
|
|
283
|
+
let softReloadSeq = 0;
|
|
284
|
+
const loadCanvasModule = (url) => import(url);
|
|
275
285
|
function softReload(version) {
|
|
276
286
|
if (!canvasUrl) { location.reload(); return; }
|
|
277
287
|
const rt = window.__maudeCanvasRuntime;
|
|
278
288
|
if (!rt || !rt.remount) { location.reload(); return; }
|
|
279
|
-
|
|
289
|
+
const seq = ++softReloadSeq;
|
|
290
|
+
loadCanvasModule(canvasUrl + (canvasUrl.includes('?') ? '&' : '?') + 'v=' + (version || Date.now()))
|
|
280
291
|
.then((mod) => {
|
|
292
|
+
if (seq !== softReloadSeq) return;
|
|
281
293
|
if (mod && typeof mod.default === 'function') {
|
|
282
294
|
rt.remount(mod.default);
|
|
283
295
|
if (rt.setHolding) rt.setHolding(false);
|
|
@@ -287,16 +299,19 @@
|
|
|
287
299
|
}
|
|
288
300
|
})
|
|
289
301
|
.catch((err) => {
|
|
302
|
+
if (seq !== softReloadSeq) return;
|
|
290
303
|
// Build/transpile/import error → keep the current render, surface a
|
|
291
304
|
// non-destructive "holding" toast. Next good build swaps it in.
|
|
292
305
|
// Same M8 diagnosis as the mount path: the 422 body names the
|
|
293
306
|
// culprit, the TypeError never does.
|
|
294
307
|
diagnoseImportFailure(canvasUrl, err).then((msg) => {
|
|
308
|
+
if (seq !== softReloadSeq) return;
|
|
295
309
|
if (rt.setHolding) rt.setHolding(true, msg || 'build error');
|
|
296
310
|
});
|
|
297
311
|
console.warn('[canvas-shell] soft-reload held last good (build error):', err);
|
|
298
312
|
});
|
|
299
313
|
}
|
|
314
|
+
// soft-reload:end
|
|
300
315
|
// Phase 6.5 — exporters pass ?hide-chrome=1 to flip the export-mode
|
|
301
316
|
// stylesheet from `media="not all"` to `media="all"`, hiding the
|
|
302
317
|
// dev-server overlays during capture. See `<style id="canvas-hide-chrome">`.
|
|
@@ -634,6 +649,20 @@
|
|
|
634
649
|
}
|
|
635
650
|
connectHmr();
|
|
636
651
|
|
|
652
|
+
// NO TOP-LEVEL AWAIT IN THIS MODULE.
|
|
653
|
+
//
|
|
654
|
+
// The boot below used to `await` at the top level, which made this the
|
|
655
|
+
// one asynchronously-evaluated module on the page — suspended, with its
|
|
656
|
+
// own dynamic `import()`s in flight, for as long as the canvas took to
|
|
657
|
+
// build. On the desktop (WKWebView) switching canvases away and straight
|
|
658
|
+
// back, which tears the half-loaded frame down in exactly that state,
|
|
659
|
+
// eventually wedged the whole web content process inside JavaScriptCore's
|
|
660
|
+
// module loader: 100 % CPU in `moduleLoaderEvaluate` microtasks, no script
|
|
661
|
+
// ever running again, every canvas and the shell frozen (surface row L23,
|
|
662
|
+
// 2026-09-16 — reproduced in a full run and in a ten-minute soak). An
|
|
663
|
+
// async function that nothing awaits gives the same boot with a module
|
|
664
|
+
// that finishes evaluating at once, the path every engine exercises most.
|
|
665
|
+
const boot = async () => {
|
|
637
666
|
if (!canvasRel) {
|
|
638
667
|
showError('Missing ?canvas= query parameter. Add ?canvas=ui/<slug>.tsx.');
|
|
639
668
|
} else {
|
|
@@ -711,6 +740,8 @@
|
|
|
711
740
|
diagnoseImportFailure(canvasUrl, err).then(showError);
|
|
712
741
|
}
|
|
713
742
|
}
|
|
743
|
+
};
|
|
744
|
+
boot().catch((err) => showError(String((err && err.stack) || err)));
|
|
714
745
|
</script>
|
|
715
746
|
</body>
|
|
716
747
|
</html>
|
|
@@ -512,6 +512,39 @@
|
|
|
512
512
|
"type": "object",
|
|
513
513
|
"description": "Free-form project-specific defaults (list IDs, milestone names, custom field IDs, status labels, …). Generic commands pass this object through to MCP calls untouched. Schema-free on purpose — your tracker's idiosyncrasies belong here, not in the plugin.",
|
|
514
514
|
"additionalProperties": true
|
|
515
|
+
},
|
|
516
|
+
"artifacts": {
|
|
517
|
+
"type": "object",
|
|
518
|
+
"additionalProperties": false,
|
|
519
|
+
"description": "Where the five workflow artifacts (plan, rca, execution-report, retro, review) LIVE — used by `provider: orbit`; other providers ignore it. Absent → `store: local`, i.e. today's behaviour: the files stay in `.ai/` and orbit receives a copy at close. The full contract is the `flow:orbit-backend` skill, guide 06 — commands never re-derive it.",
|
|
520
|
+
"properties": {
|
|
521
|
+
"store": {
|
|
522
|
+
"type": "string",
|
|
523
|
+
"enum": ["local", "orbit", "both"],
|
|
524
|
+
"default": "local",
|
|
525
|
+
"description": "`local` — files in `.ai/` are the record (close-time push still applies). `orbit` — orbit is the record: each artifact is pushed the moment it is written and no copy is kept in the repo. `both` — write locally AND push at write time; the migration setting, and the safe way to try `orbit` out."
|
|
526
|
+
},
|
|
527
|
+
"local": {
|
|
528
|
+
"type": "string",
|
|
529
|
+
"enum": ["keep", "scratch"],
|
|
530
|
+
"default": "keep",
|
|
531
|
+
"description": "Only meaningful with `store: orbit`. `scratch` — the command writes its artifact under `spoolDir` (gitignored), pushes it, and deletes it on a confirmed push; a FAILED push leaves the file there and the next flow command retries it. `keep` — write to the usual `.ai/` path anyway, so a repo mid-migration keeps its history. A file is never deleted before orbit confirms."
|
|
532
|
+
},
|
|
533
|
+
"spoolDir": {
|
|
534
|
+
"type": "string",
|
|
535
|
+
"default": ".ai/tmp/orbit-spool",
|
|
536
|
+
"description": "Directory for scratch artifacts awaiting a confirmed push, relative to repo root. Must be gitignored — `/flow:init` adds it to `.ai/.gitignore`. Nothing else ever writes here and no command reads it except the spool retry."
|
|
537
|
+
},
|
|
538
|
+
"kinds": {
|
|
539
|
+
"type": "array",
|
|
540
|
+
"uniqueItems": true,
|
|
541
|
+
"items": {
|
|
542
|
+
"type": "string",
|
|
543
|
+
"enum": ["plan", "rca", "execution-report", "retro", "review"]
|
|
544
|
+
},
|
|
545
|
+
"description": "Which artifact kinds this repo routes to orbit. Absent → all five. A kind left out behaves as `store: local` for that kind alone; orbit accepts no other kinds."
|
|
546
|
+
}
|
|
547
|
+
}
|
|
515
548
|
}
|
|
516
549
|
}
|
|
517
550
|
},
|