@1agh/maude 0.60.3 → 0.60.4

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.
@@ -2577,6 +2577,11 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2577
2577
  // via git "Get latest" — the file travels through git, this event is only a
2578
2578
  // "refresh your list" nudge for online local tabs (loopback inspector WS).
2579
2579
  ctx.bus.emit('canvas-list-update', { action: 'added', rel, slug });
2580
+ // The inverse of `canvas-deleted` (see deleteCanvas for why this is its own
2581
+ // event). Creating a name the project buried earlier has to lift the
2582
+ // tombstone, or every peer would dutifully trash the new canvas for as long
2583
+ // as the gravestone lives.
2584
+ ctx.bus.emit('canvas-created', { slug });
2580
2585
  // designRel-prefixed path — matches the file-tree `file.path` shape so the
2581
2586
  // client can open it directly after reloadTree().
2582
2587
  return { ok: true, file: path.posix.join(paths.designRel, rel), rel, slug };
@@ -2690,6 +2695,15 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2690
2695
 
2691
2696
  // Phase 30 — live tree refresh for other local tabs (see createCanvas).
2692
2697
  ctx.bus.emit('canvas-list-update', { action: 'removed', rel, slug });
2698
+ // A SEPARATE EVENT, DELIBERATELY. `canvas-list-update` is emitted by this
2699
+ // API *and* by the filesystem watcher, so its payload can describe a file
2700
+ // an agent or a `git checkout` produced — which is why discovery treats it
2701
+ // as a nudge and re-derives the truth itself. This one is only ever emitted
2702
+ // HERE, from a privileged route the canvas origin cannot reach, so it
2703
+ // carries something the watcher's version cannot: the user meant it. Sync
2704
+ // needs exactly that, because "gone from this disk" and "deleted from the
2705
+ // project" are different claims and only the second may travel.
2706
+ ctx.bus.emit('canvas-deleted', { slug });
2693
2707
  return {
2694
2708
  ok: true,
2695
2709
  rel,
@@ -4704,6 +4704,11 @@ function StatusBar({
4704
4704
  onOpenSync,
4705
4705
  changesCount = 0,
4706
4706
  unpushed = 0,
4707
+ // Somebody else commits this project (a cloud cell, or the cell behind a
4708
+ // linked+credentialed desktop repo — DDR-218). The chip then names the
4709
+ // mechanism that IS saving rather than reporting a working-tree count the
4710
+ // user has no reason to act on. Default false = today's local-first chip.
4711
+ savingIsManaged = false,
4707
4712
  changesOpen = false,
4708
4713
  onOpenChanges,
4709
4714
  version,
@@ -4811,11 +4816,17 @@ function StatusBar({
4811
4816
  <span className="st-sb-changes-dot" aria-hidden="true" />
4812
4817
  <span className="lbl">changes</span>
4813
4818
  <span className="val">
4814
- {changesCount > 0
4815
- ? `${changesCount} unsaved`
4816
- : unpushed > 0
4817
- ? `${unpushed} to publish`
4818
- : 'all saved'}
4819
+ {savingIsManaged
4820
+ ? // Not "all saved": assets and project config are written into the
4821
+ // cell's checkout but never committed there, so asserting a clean
4822
+ // save would overstate what the cloud actually holds. Naming the
4823
+ // mechanism is both honest and the thing the user needs to know.
4824
+ 'cloud saving'
4825
+ : changesCount > 0
4826
+ ? `${changesCount} unsaved`
4827
+ : unpushed > 0
4828
+ ? `${unpushed} to publish`
4829
+ : 'all saved'}
4819
4830
  </span>
4820
4831
  </button>
4821
4832
  )}
@@ -11383,7 +11394,26 @@ function App() {
11383
11394
  for (const f of gitStatus?.files || []) m.set(f.path, KIND[f.status]);
11384
11395
  return m;
11385
11396
  }, [gitStatus]);
11386
- const unsavedCount = gitStatus?.files?.length || 0;
11397
+ // WHO IS SAVING THIS PROJECT — one expression, read by every surface that
11398
+ // would otherwise offer to save it (DDR-218 + the cell's `historyOnly`).
11399
+ //
11400
+ // These two flags were previously derived at the GitPanel call site alone, so
11401
+ // the panel withdrew correctly while the toolbar menu and the status-bar chip
11402
+ // kept rendering a raw dirty count from `gitStatus.files.length` — the very
11403
+ // "lie about work that is already saved" the withdrawal exists to delete. The
11404
+ // count is the claim, wherever it is drawn; hoisting the rule here is what
11405
+ // keeps a third surface from re-introducing it.
11406
+ //
11407
+ // PRESENTATION, NOT A CONTROL — unchanged from DDR-218. `.git` is untouched,
11408
+ // a terminal `git` behaves exactly as before, the server routes keep their own
11409
+ // gates, and Disconnect restores every surface live.
11410
+ const cellManaged = !!cfg.cloud;
11411
+ const cloudManaged = !cfg.cloud && !!cloudLinkedHub?.credentialed;
11412
+ const savingIsManaged = cellManaged || cloudManaged;
11413
+ // The raw truth stays available for surfaces that legitimately need it (the
11414
+ // panel's own file list, `dirtyByPath`); only the COUNT — the thing that reads
11415
+ // as a to-do — is withheld when this project's saving is somebody else's job.
11416
+ const unsavedCount = savingIsManaged ? 0 : gitStatus?.files?.length || 0;
11387
11417
 
11388
11418
  // Phase 28 (E3) — keep remote ahead/behind fresh so the "Get latest" nudge
11389
11419
  // surfaces on its own: probe once a repo is known, again whenever the Changes
@@ -14446,7 +14476,7 @@ function App() {
14446
14476
  // mislead, it does not remove a capability. The real gates are
14447
14477
  // server-side (`projectReadOnly`, the manifest's role matrix), and
14448
14478
  // they are unchanged by this flag.
14449
- historyOnly={!!cfg.cloud}
14479
+ historyOnly={cellManaged}
14450
14480
  // DDR-218 (fix 8) — the DESKTOP half of the same withdrawal: a repo
14451
14481
  // linked+credentialed to Maude Cloud is cloud-managed (the cell
14452
14482
  // commits every edit as it lands), so the local Changes surface is
@@ -14454,7 +14484,7 @@ function App() {
14454
14484
  // Live: CloudBar lifts every link change (resolve/attach/detach)
14455
14485
  // into `cloudLinkedHub`. Same presentation-not-a-control rule as
14456
14486
  // `historyOnly` above; Disconnect restores the panel in place.
14457
- cloudManaged={!cfg.cloud && !!cloudLinkedHub?.credentialed}
14487
+ cloudManaged={cloudManaged}
14458
14488
  resizing={resizingFor('changes')}
14459
14489
  onClose={() => setChangesOpen(false)}
14460
14490
  onCommit={gitCommit}
@@ -15521,7 +15551,11 @@ function App() {
15521
15551
  // holds (opening Sync closes whatever else the right slot shows).
15522
15552
  onOpenSync={syncStatus ? () => toggleRightPanel('sync') : undefined}
15523
15553
  changesCount={unsavedCount}
15524
- unpushed={gitStatus?.unpushed || 0}
15554
+ // `unpushed` is a LOCAL-git offer ("N to publish"), and the panel has
15555
+ // withdrawn Publish under either managed posture — so the chip must
15556
+ // not keep advertising it either.
15557
+ unpushed={savingIsManaged ? 0 : gitStatus?.unpushed || 0}
15558
+ savingIsManaged={savingIsManaged}
15525
15559
  changesOpen={changesOpen}
15526
15560
  onOpenChanges={gitStatus?.repo ? () => setChangesOpen(true) : undefined}
15527
15561
  version={cfg?.version}
@@ -89,6 +89,15 @@ function readAssets(raw) {
89
89
  return { ...raw, failures };
90
90
  }
91
91
 
92
+ /** The file plane (feature-sync-file-plane), or null when unreadable/absent —
93
+ * absent is the norm: the payload carries `files` only once a flag-on pull
94
+ * has run this boot. */
95
+ function readFiles(raw) {
96
+ if (!raw || typeof raw !== 'object') return null;
97
+ if (![raw.synced, raw.pulled, raw.conflicts].every(isCount)) return null;
98
+ return raw;
99
+ }
100
+
92
101
  export default function SyncPanel({
93
102
  status, // the live `sync:status` payload (never null while mounted)
94
103
  project, // display name for the header sentence (hub-supplied → safeName'd)
@@ -150,6 +159,7 @@ export default function SyncPanel({
150
159
  const truncated = isCount(status?.itemsTruncated) ? status.itemsTruncated : 0;
151
160
  const assets = readAssets(status?.assets);
152
161
  const assetFailures = assets?.failures || [];
162
+ const files = readFiles(status?.files);
153
163
 
154
164
  const { attention, byGroup } = useMemo(() => {
155
165
  const attention = items.filter((i) => i.state === 'auth-rejected');
@@ -329,6 +339,23 @@ export default function SyncPanel({
329
339
  )}
330
340
  </section>
331
341
  )}
342
+
343
+ {files && (
344
+ <section aria-label="Project files" data-testid="sync-files">
345
+ <div className="gp-sect-label">
346
+ project files <span className="gp-group-count">{files.synced}</span>
347
+ </div>
348
+ <div className="sp-assets-line">
349
+ {`${files.synced} synced` + (files.pulled > 0 ? ` · ${files.pulled} pulled` : '')}
350
+ </div>
351
+ {files.conflicts > 0 && (
352
+ <div className="sp-assets-retry" data-testid="sync-files-conflicts">
353
+ {files.conflicts} conflict{files.conflicts === 1 ? '' : 's'} — the older copies are
354
+ kept in _trash/.
355
+ </div>
356
+ )}
357
+ </section>
358
+ )}
332
359
  </div>
333
360
  </aside>
334
361
  );
@@ -47,6 +47,15 @@ export interface LinkedHub {
47
47
  * would split peers of the same project apart. See sync/doc-name.ts.
48
48
  */
49
49
  workspaceId?: string;
50
+ /**
51
+ * feature-sync-file-plane — opt-in for the manifest-driven file plane
52
+ * (Plane B): the downward project-file pull AND the widened upward sweep
53
+ * (stylesheets, docs, code modules — classifier membership, see
54
+ * sync/file-membership.ts). Default OFF this release; `MAUDE_SYNC_FILES=1`
55
+ * forces it on for a session. The flag gates ONLY the new plane — the
56
+ * canvas CRDT lanes and the DDR-217 asset lanes run regardless.
57
+ */
58
+ syncFiles?: boolean;
50
59
  }
51
60
 
52
61
  export interface DevServerConfig {