@1agh/maude 1.2.0 → 1.3.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.
- package/README.md +3 -1
- package/apps/studio/acp/index.ts +43 -7
- package/apps/studio/annotations-layer.tsx +191 -116
- package/apps/studio/annotations-model.ts +39 -0
- package/apps/studio/annotations-sync.ts +50 -0
- package/apps/studio/api.ts +732 -38
- package/apps/studio/bin/annotate.mjs +3 -1
- package/apps/studio/bin/server-up.sh +17 -2
- package/apps/studio/canvas-build.ts +49 -8
- package/apps/studio/canvas-edit.ts +468 -7
- package/apps/studio/canvas-lib.tsx +82 -18
- package/apps/studio/canvas-list-watch.ts +33 -2
- package/apps/studio/canvas-notice-message.ts +16 -0
- package/apps/studio/canvas-notifications.tsx +19 -0
- package/apps/studio/canvas-shell.tsx +57 -6
- package/apps/studio/client/app.jsx +783 -245
- package/apps/studio/client/apply-edit-request.ts +55 -0
- package/apps/studio/client/canvas-url.js +39 -1
- package/apps/studio/client/export-center.jsx +111 -61
- package/apps/studio/client/github.js +38 -1
- package/apps/studio/client/index-loader.ts +76 -0
- package/apps/studio/client/panels/CloudBar.jsx +52 -6
- package/apps/studio/client/panels/GitPanel.jsx +103 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
- package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
- package/apps/studio/client/panels/SyncPanel.jsx +113 -0
- package/apps/studio/client/panels/TeamProjects.jsx +376 -0
- package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
- package/apps/studio/client/photo-knobs.jsx +3 -0
- package/apps/studio/client/share-dialog.jsx +91 -0
- package/apps/studio/client/share-link.js +94 -0
- package/apps/studio/client/styles/3-shell-maude.css +36 -18
- package/apps/studio/client/styles/4-components.css +18 -1
- package/apps/studio/client/tree-row-menu.jsx +48 -3
- package/apps/studio/client/whats-new.jsx +26 -32
- package/apps/studio/cloud/endpoints.ts +132 -0
- package/apps/studio/collab/awareness-bridge.ts +65 -14
- package/apps/studio/collab/index.ts +4 -0
- package/apps/studio/collab/persistence.ts +5 -1
- package/apps/studio/collab/registry.ts +9 -3
- package/apps/studio/collab/room.ts +13 -2
- package/apps/studio/context.ts +59 -0
- package/apps/studio/dist/client.bundle.js +1594 -1562
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/sonner.js +1 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/git/accepted-guard.ts +36 -0
- package/apps/studio/hmr-broadcast.ts +34 -1
- package/apps/studio/http.ts +300 -18
- package/apps/studio/inspect.ts +19 -0
- package/apps/studio/managed-projects.ts +172 -0
- package/apps/studio/notifications.tsx +292 -0
- package/apps/studio/runtime-bundle.ts +2 -0
- package/apps/studio/server.ts +59 -3
- package/apps/studio/sync/accepted-cold-start.ts +225 -0
- package/apps/studio/sync/accepted-link.ts +320 -0
- package/apps/studio/sync/action-stage.ts +343 -0
- package/apps/studio/sync/agent.ts +12 -60
- package/apps/studio/sync/cell-file-events.ts +3 -0
- package/apps/studio/sync/codec.ts +73 -6
- package/apps/studio/sync/ctl-provider.ts +15 -2
- package/apps/studio/sync/document-discovery.ts +81 -0
- package/apps/studio/sync/file-membership.ts +10 -2
- package/apps/studio/sync/file-plane.ts +309 -26
- package/apps/studio/sync/index.ts +1174 -21
- package/apps/studio/sync/migrate-seed.ts +110 -19
- package/apps/studio/sync/poke.ts +4 -2
- package/apps/studio/sync/presentation.ts +315 -2
- package/apps/studio/sync/projection.ts +820 -23
- package/apps/studio/sync/repeated-module.ts +73 -0
- package/apps/studio/sync/revision-barrier.ts +129 -0
- package/apps/studio/sync/seed-repair.ts +46 -0
- package/apps/studio/sync/source-merge.ts +100 -0
- package/apps/studio/sync/source-ops.ts +289 -0
- package/apps/studio/sync/source-recovery.ts +70 -0
- package/apps/studio/sync/source-validation.ts +56 -0
- package/apps/studio/sync/status.ts +83 -1
- package/apps/studio/sync/transaction-client.ts +486 -0
- package/apps/studio/sync/writer-registry.ts +236 -0
- package/apps/studio/text-caret.ts +35 -0
- package/apps/studio/undo-hud.tsx +9 -87
- package/apps/studio/use-canvas-media-drop.tsx +4 -39
- package/apps/studio/use-tool-mode.tsx +44 -0
- package/apps/studio/whats-new.json +137 -0
- package/cli/lib/harness/codex-runtime.mjs +4 -1
- package/package.json +9 -8
- package/plugins/design/dependencies.json +3 -3
- package/plugins/design/templates/_shell.html +95 -7
- package/plugins/flow/dependencies.json +3 -3
|
@@ -27,8 +27,45 @@
|
|
|
27
27
|
// originating clientID — the random 32-bit ids make local↔remote collisions
|
|
28
28
|
// negligible.
|
|
29
29
|
|
|
30
|
+
import * as encoding from 'lib0/encoding';
|
|
30
31
|
import type { Awareness } from 'y-protocols/awareness';
|
|
31
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
applyAwarenessUpdate,
|
|
34
|
+
encodeAwarenessUpdate,
|
|
35
|
+
removeAwarenessStates,
|
|
36
|
+
} from 'y-protocols/awareness';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A departed relayed client, as the hub can carry it.
|
|
40
|
+
*
|
|
41
|
+
* A peer leaving is a REMOVAL — a null awareness state. The hub (Hocuspocus 4)
|
|
42
|
+
* runs every incoming awareness frame through a scratch Awareness for its
|
|
43
|
+
* `beforeHandleAwareness` hook and re-encodes only the states that scratch
|
|
44
|
+
* still HOLDS, so a null state is dropped on the way in: a tab closing behind
|
|
45
|
+
* this studio never left anyone else's presence until the 30 s awareness
|
|
46
|
+
* timeout (plan T31/L19). The one thing it forwards is a state, so a departure
|
|
47
|
+
* crosses as this marker with a newer clock; every bridge turns it back into a
|
|
48
|
+
* real removal on its room. It carries no `name`, so a client that predates
|
|
49
|
+
* the marker drops it at the trust boundary (`sanitizeForeignState`) instead
|
|
50
|
+
* of drawing a nameless avatar.
|
|
51
|
+
*/
|
|
52
|
+
export const DEPARTED = Object.freeze({ __left: true });
|
|
53
|
+
|
|
54
|
+
function isDeparted(state: unknown): boolean {
|
|
55
|
+
return !!state && typeof state === 'object' && (state as { __left?: unknown }).__left === true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Encode a departure for each of `clients`, one clock past what `from` knows. */
|
|
59
|
+
function encodeDepartures(from: Awareness, clients: readonly number[]): Uint8Array {
|
|
60
|
+
const enc = encoding.createEncoder();
|
|
61
|
+
encoding.writeVarUint(enc, clients.length);
|
|
62
|
+
for (const clientID of clients) {
|
|
63
|
+
encoding.writeVarUint(enc, clientID);
|
|
64
|
+
encoding.writeVarUint(enc, (from.meta.get(clientID)?.clock ?? 0) + 1);
|
|
65
|
+
encoding.writeVarString(enc, JSON.stringify(DEPARTED));
|
|
66
|
+
}
|
|
67
|
+
return encoding.toUint8Array(enc);
|
|
68
|
+
}
|
|
32
69
|
|
|
33
70
|
interface AwarenessChange {
|
|
34
71
|
added: number[];
|
|
@@ -49,22 +86,36 @@ export function bridgeAwareness(a: Awareness, b: Awareness): () => void {
|
|
|
49
86
|
// so it relays; a change this bridge applied carries BRIDGE, so it stops.
|
|
50
87
|
const BRIDGE = { awarenessBridge: true };
|
|
51
88
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
// Room → hub. A client that left the room leaves as a DEPARTED marker (see
|
|
90
|
+
// above) — never as the null state the hub would drop. Everything else
|
|
91
|
+
// relays as it is.
|
|
92
|
+
const aToB = ({ added, updated, removed }: AwarenessChange, origin: unknown) => {
|
|
93
|
+
if (origin === BRIDGE) return;
|
|
94
|
+
const present = added.concat(updated);
|
|
95
|
+
if (present.length > 0) applyAwarenessUpdate(b, encodeAwarenessUpdate(a, present), BRIDGE);
|
|
96
|
+
const gone = removed.filter((id) => id !== b.clientID && b.getStates().has(id));
|
|
97
|
+
if (gone.length > 0) applyAwarenessUpdate(b, encodeDepartures(b, gone), BRIDGE);
|
|
98
|
+
};
|
|
99
|
+
// Hub → room. A DEPARTED marker is a removal here; a real removal (the hub's
|
|
100
|
+
// own timeout, a peer's socket closing) relays as ever.
|
|
101
|
+
const bToA = ({ added, updated, removed }: AwarenessChange, origin: unknown) => {
|
|
102
|
+
if (origin === BRIDGE) return;
|
|
103
|
+
const states = b.getStates();
|
|
104
|
+
const departed = added.concat(updated).filter((id) => isDeparted(states.get(id)));
|
|
105
|
+
const live = added.concat(updated).filter((id) => !isDeparted(states.get(id)));
|
|
106
|
+
const changed = live.concat(removed);
|
|
107
|
+
if (changed.length > 0) applyAwarenessUpdate(a, encodeAwarenessUpdate(b, changed), BRIDGE);
|
|
108
|
+
const leaving = departed.filter((id) => a.getStates().has(id));
|
|
109
|
+
if (leaving.length > 0) removeAwarenessStates(a, leaving, BRIDGE);
|
|
110
|
+
};
|
|
63
111
|
|
|
64
|
-
// Initial state exchange — push each side's current states to the other
|
|
112
|
+
// Initial state exchange — push each side's current states to the other
|
|
113
|
+
// (departed markers are not a presence).
|
|
65
114
|
const aClients = Array.from(a.getStates().keys());
|
|
66
115
|
if (aClients.length > 0) applyAwarenessUpdate(b, encodeAwarenessUpdate(a, aClients), BRIDGE);
|
|
67
|
-
const bClients = Array.from(b.getStates().
|
|
116
|
+
const bClients = Array.from(b.getStates().entries())
|
|
117
|
+
.filter(([, state]) => !isDeparted(state))
|
|
118
|
+
.map(([id]) => id);
|
|
68
119
|
if (bClients.length > 0) applyAwarenessUpdate(a, encodeAwarenessUpdate(b, bClients), BRIDGE);
|
|
69
120
|
|
|
70
121
|
a.on('update', aToB);
|
|
@@ -64,6 +64,10 @@ export function createCollab(ctx: Context, api: Api): Collab {
|
|
|
64
64
|
fileForSlug,
|
|
65
65
|
shouldSeed: (slug) => !(ctx.sharedDoc && registryRef?.isPinned(slug)),
|
|
66
66
|
});
|
|
67
|
+
// Accepted revisions: browser writes to a synced canvas's room are refused
|
|
68
|
+
// (the room doc is the hub's accepted replica). Asked per frame — the save
|
|
69
|
+
// mode can change while the studio runs.
|
|
70
|
+
persistence.acceptedMode = () => ctx.syncControl?.current?.()?.acceptedMode?.() === true;
|
|
67
71
|
const registry = createRegistry(persistence);
|
|
68
72
|
registryRef = registry;
|
|
69
73
|
|
|
@@ -284,7 +284,11 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
|
|
|
284
284
|
const svg = map.get('svg');
|
|
285
285
|
if (typeof svg === 'string' && svg) {
|
|
286
286
|
if (withinCap(slug, 'annotations', svg, MAX_ANNOTATIONS_BYTES)) {
|
|
287
|
-
|
|
287
|
+
// Projection must never re-enter onAnnotationsChanged: an old flush
|
|
288
|
+
// finishing after a new edit otherwise republishes the old SVG and
|
|
289
|
+
// rolls back every peer. The API checks freshness after async IO and
|
|
290
|
+
// before its atomic rename; a later doc update schedules a new flush.
|
|
291
|
+
await api.projectAnnotations(file, svg, () => map.get('svg') === svg);
|
|
288
292
|
}
|
|
289
293
|
}
|
|
290
294
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Awareness } from 'y-protocols/awareness';
|
|
10
10
|
|
|
11
|
+
import { ANNOTATION_WRITE_ID, validAnnotationWriteId } from '../annotations-sync.ts';
|
|
11
12
|
import { applyCommentsToDoc } from '../sync/codec.ts';
|
|
12
13
|
import { bridgeAwareness } from './awareness-bridge.ts';
|
|
13
14
|
import { Y_TYPES } from './persistence.ts';
|
|
@@ -52,7 +53,7 @@ export interface Registry {
|
|
|
52
53
|
* post-write SVG; the room replaces `Y.Map.svg` so collab peers see the
|
|
53
54
|
* updated stroke set without waiting for a cold-open re-seed.
|
|
54
55
|
*/
|
|
55
|
-
syncRoomFromAnnotations(slug: string, svg: string): void;
|
|
56
|
+
syncRoomFromAnnotations(slug: string, svg: string, writeId?: string): void;
|
|
56
57
|
/**
|
|
57
58
|
* Phase 30 — project agent editing-presence onto a slug's room awareness so
|
|
58
59
|
* it crosses the hub (the loopback `ai-activity` bus event does not). `null`
|
|
@@ -192,13 +193,18 @@ export function createRegistry(callbacks: RoomCallbacks): Registry {
|
|
|
192
193
|
applyCommentsToDoc(room.doc, comments as unknown[], 'inspector-write');
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
function syncRoomFromAnnotations(slug: string, svg: string): void {
|
|
196
|
+
function syncRoomFromAnnotations(slug: string, svg: string, writeId?: string): void {
|
|
196
197
|
const room = rooms.get(slug);
|
|
197
198
|
if (!room) return;
|
|
198
199
|
const map = room.doc.getMap<string>(Y_TYPES.annotations);
|
|
199
|
-
|
|
200
|
+
// Identical disk notifications remain no-ops. A new UI operation may
|
|
201
|
+
// deliberately restore identical content while an author has pending work.
|
|
202
|
+
const id = validAnnotationWriteId(writeId) ? writeId : undefined;
|
|
203
|
+
if (map.get('svg') === svg && (!id || map.get(ANNOTATION_WRITE_ID) === id)) return;
|
|
200
204
|
room.doc.transact(() => {
|
|
201
205
|
map.set('svg', svg);
|
|
206
|
+
if (id) map.set(ANNOTATION_WRITE_ID, id);
|
|
207
|
+
else map.delete(ANNOTATION_WRITE_ID);
|
|
202
208
|
}, 'inspector-write');
|
|
203
209
|
}
|
|
204
210
|
|
|
@@ -77,6 +77,15 @@ export interface RoomCallbacks {
|
|
|
77
77
|
* the seeded state.
|
|
78
78
|
*/
|
|
79
79
|
seed(slug: string, doc: Y.Doc): Promise<void> | void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Accepted revisions (DDR-241 §7): while true, this room's document is a
|
|
83
|
+
* replica of the project's accepted state — no browser connection may write
|
|
84
|
+
* a persistent lane into it (the hub would drop it and the replica would
|
|
85
|
+
* diverge). Comments and annotations go through the API as proposals.
|
|
86
|
+
* Awareness is unaffected. Optional; absent means false.
|
|
87
|
+
*/
|
|
88
|
+
acceptedMode?: () => boolean;
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
export interface Room {
|
|
@@ -216,6 +225,8 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
216
225
|
*/
|
|
217
226
|
function refusedLanes(conn: RoomConn, roots: ReadonlySet<string>): string[] {
|
|
218
227
|
const refused: string[] = [];
|
|
228
|
+
// Accepted revisions: every persistent lane is the hub's to change.
|
|
229
|
+
if (callbacks.acceptedMode?.()) return [...roots];
|
|
219
230
|
for (const r of roots) {
|
|
220
231
|
if (conn.realm === 'canvas' && (isBodyLane(r) || r === '<unresolved>')) {
|
|
221
232
|
refused.push(r);
|
|
@@ -255,7 +266,7 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
255
266
|
gateRefusalCount += 1;
|
|
256
267
|
discardMirror();
|
|
257
268
|
console.warn(
|
|
258
|
-
`[collab/${slug}] origin gate REFUSED a ${conn.readOnly ? 'read-only' : 'canvas-realm'} ` +
|
|
269
|
+
`[collab/${slug}] origin gate REFUSED a ${callbacks.acceptedMode?.() ? 'accepted-replica' : conn.readOnly ? 'read-only' : 'canvas-realm'} ` +
|
|
259
270
|
`sync frame (lanes: ${refused.join(', ') || 'unclassifiable'}). ` +
|
|
260
271
|
(conn.readOnly
|
|
261
272
|
? 'A viewer-role socket may only write comments — role matrix, cloud collab lane.'
|
|
@@ -351,7 +362,7 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
351
362
|
}
|
|
352
363
|
|
|
353
364
|
function receive(conn: RoomConn, payload: Uint8Array): void {
|
|
354
|
-
if (conn.realm === 'canvas' || conn.readOnly === true) {
|
|
365
|
+
if (conn.realm === 'canvas' || conn.readOnly === true || callbacks.acceptedMode?.()) {
|
|
355
366
|
receiveGated(conn, payload);
|
|
356
367
|
return;
|
|
357
368
|
}
|
package/apps/studio/context.ts
CHANGED
|
@@ -259,6 +259,65 @@ export interface Context {
|
|
|
259
259
|
current?(): {
|
|
260
260
|
cancelAssetSweep(): boolean;
|
|
261
261
|
retireForMove?(fromSlug: string, toRel: string): Promise<boolean>;
|
|
262
|
+
/** Accepted-revisions mode (DDR-241) — see SyncRuntime.proposeLane. */
|
|
263
|
+
proposeLane?(
|
|
264
|
+
slug: string,
|
|
265
|
+
lane: 'comments' | 'annotations' | 'meta',
|
|
266
|
+
text: string,
|
|
267
|
+
opts?: { baseText?: string; writeId?: string }
|
|
268
|
+
): Promise<{ status: 'accepted' | 'rejected'; code?: string }> | null;
|
|
269
|
+
/** Accepted-revisions mode (DDR-241) — see SyncRuntime.proposeFolder. */
|
|
270
|
+
proposeFolder?(
|
|
271
|
+
op:
|
|
272
|
+
| { op: 'dir.create'; path: string }
|
|
273
|
+
| { op: 'dir.delete'; path: string }
|
|
274
|
+
| { op: 'dir.move'; from: string; to: string }
|
|
275
|
+
): Promise<{ status: 'accepted' | 'rejected'; code?: string; queued?: boolean }> | null;
|
|
276
|
+
acceptedMode?(): boolean;
|
|
277
|
+
acceptedHistory?(q: {
|
|
278
|
+
limit?: number;
|
|
279
|
+
before?: number | null;
|
|
280
|
+
path?: string | null;
|
|
281
|
+
}): Promise<unknown[] | null>;
|
|
282
|
+
acceptedVersion?(repoRel: string, revision: number): Promise<string | null>;
|
|
283
|
+
acceptedRestore?(
|
|
284
|
+
repoRel: string,
|
|
285
|
+
revision: number
|
|
286
|
+
): Promise<{ status: 'accepted' | 'rejected'; code?: string; queued?: boolean } | null>;
|
|
287
|
+
acceptedUndo?(
|
|
288
|
+
actionId: string,
|
|
289
|
+
redo?: boolean
|
|
290
|
+
): Promise<{
|
|
291
|
+
status: 'accepted' | 'rejected';
|
|
292
|
+
code?: string;
|
|
293
|
+
queued?: boolean;
|
|
294
|
+
actionId?: string;
|
|
295
|
+
} | null>;
|
|
296
|
+
acceptedActionForContent?(repoRel: string, content: string): string | null;
|
|
297
|
+
prepareOffline?(): Promise<{
|
|
298
|
+
complete: boolean;
|
|
299
|
+
pulled: number;
|
|
300
|
+
passes: number;
|
|
301
|
+
failed: number;
|
|
302
|
+
} | null>;
|
|
303
|
+
conflictVersions?(
|
|
304
|
+
repoRel: string
|
|
305
|
+
): { slug: string; mine: string | null; theirs: string } | null;
|
|
306
|
+
resolveConflict?(
|
|
307
|
+
repoRel: string,
|
|
308
|
+
choice: 'mine' | 'theirs'
|
|
309
|
+
): Promise<{ status: 'accepted' | 'rejected' | 'taken'; code?: string } | null>;
|
|
310
|
+
/** T16 — AI action boundaries (see sync/action-stage.ts). */
|
|
311
|
+
beginAiAction?(key: string, label: string): void;
|
|
312
|
+
endAiAction?(
|
|
313
|
+
key: string,
|
|
314
|
+
outcome: 'done' | 'failed'
|
|
315
|
+
): Promise<{ status: 'accepted' | 'rejected'; code?: string } | null>;
|
|
316
|
+
resolveAiAction?(choice: 'publish' | 'discard'): Promise<{
|
|
317
|
+
status: 'accepted' | 'rejected' | 'discarded';
|
|
318
|
+
code?: string;
|
|
319
|
+
canvases?: number;
|
|
320
|
+
} | null>;
|
|
262
321
|
} | null;
|
|
263
322
|
};
|
|
264
323
|
}
|