@1agh/maude 0.60.7 → 1.0.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/acp/index.ts +1 -0
- package/apps/studio/ai-banner.tsx +1 -0
- package/apps/studio/annotations-context-toolbar.tsx +3 -1
- package/apps/studio/annotations-layer.tsx +33 -16
- package/apps/studio/api.ts +108 -18
- package/apps/studio/artboard-guides-overlay.tsx +5 -1
- package/apps/studio/assets-s3.ts +6 -1
- package/apps/studio/bin/_import-figma.mjs +8 -3
- package/apps/studio/build.ts +1 -1
- package/apps/studio/canvas-artifacts.ts +21 -0
- package/apps/studio/canvas-build.ts +14 -9
- package/apps/studio/canvas-comment-mount.tsx +27 -30
- package/apps/studio/canvas-icons.tsx +1 -1
- package/apps/studio/canvas-lib.tsx +94 -5
- package/apps/studio/canvas-list-watch.ts +14 -1
- package/apps/studio/canvas-shell.tsx +8 -0
- package/apps/studio/client/app.jsx +224 -45
- package/apps/studio/client/panels/GitPanel.jsx +121 -13
- package/apps/studio/client/panels/SettingsPanel.jsx +1 -1
- package/apps/studio/client/panels/SyncConsentDialog.jsx +182 -0
- package/apps/studio/client/panels/SyncPanel.jsx +595 -1
- package/apps/studio/client/styles/3-shell-maude.css +38 -0
- package/apps/studio/clip-ops.ts +8 -1
- package/apps/studio/cloud/endpoints.ts +265 -3
- package/apps/studio/collab/origins.ts +3 -1
- package/apps/studio/comments-overlay.tsx +5 -0
- package/apps/studio/config.schema.json +24 -0
- package/apps/studio/context-menu.tsx +40 -27
- package/apps/studio/context.ts +57 -8
- package/apps/studio/cursors-overlay.tsx +25 -13
- package/apps/studio/dist/client.bundle.js +686 -686
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/jobs.ts +77 -15
- package/apps/studio/exporters/remote.ts +201 -0
- package/apps/studio/exporters/video-encode-lib.ts +10 -4
- package/apps/studio/figma/to-strokes.ts +11 -9
- package/apps/studio/gifenc.d.ts +51 -0
- package/apps/studio/git/log-format.ts +88 -0
- package/apps/studio/git/safe-rel.ts +96 -0
- package/apps/studio/git/service.ts +46 -27
- package/apps/studio/hmr-broadcast.ts +10 -0
- package/apps/studio/http.ts +384 -6
- package/apps/studio/participants-chrome.tsx +1 -0
- package/apps/studio/photo-store.ts +7 -0
- package/apps/studio/react-augment.d.ts +17 -0
- package/apps/studio/runtime-bundle.ts +6 -1
- package/apps/studio/server.ts +43 -8
- package/apps/studio/sync/agent.ts +70 -82
- package/apps/studio/sync/asset-push.ts +28 -71
- package/apps/studio/sync/autocommit.ts +106 -5
- package/apps/studio/sync/cell-file-events.ts +117 -0
- package/apps/studio/sync/cell-pairing.ts +20 -5
- package/apps/studio/sync/cell-write-nudge.ts +244 -0
- package/apps/studio/sync/codec.ts +155 -3
- package/apps/studio/sync/cold-start-apply.ts +211 -0
- package/apps/studio/sync/ctl-heal.ts +253 -0
- package/apps/studio/sync/ctl-provider.ts +217 -0
- package/apps/studio/sync/decide-file.ts +335 -0
- package/apps/studio/sync/file-ledger.ts +581 -0
- package/apps/studio/sync/file-membership.ts +32 -0
- package/apps/studio/sync/file-plane.ts +1400 -0
- package/apps/studio/sync/file-pull.ts +41 -4
- package/apps/studio/sync/hub-link.ts +16 -1
- package/apps/studio/sync/hub-listing.ts +46 -0
- package/apps/studio/sync/hubs-config.ts +16 -0
- package/apps/studio/sync/index.ts +915 -308
- package/apps/studio/sync/journal-client.ts +200 -0
- package/apps/studio/sync/migrate-seed.ts +99 -67
- package/apps/studio/sync/poke.ts +50 -0
- package/apps/studio/sync/projection.ts +13 -0
- package/apps/studio/sync/pull-budget.ts +86 -0
- package/apps/studio/sync/settings.ts +110 -0
- package/apps/studio/sync/status.ts +68 -0
- package/apps/studio/sync/trash.ts +243 -0
- package/apps/studio/sync/untrusted.ts +30 -10
- package/apps/studio/test/_helpers.ts +8 -0
- package/apps/studio/test/canvas-build.test.ts +63 -0
- package/apps/studio/test/canvas-list-watch.test.ts +17 -0
- package/apps/studio/test/canvas-move-api.test.ts +31 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +12 -0
- package/apps/studio/test/canvas-shell-build-error.test.ts +49 -0
- package/apps/studio/test/cloud-history-hardening.test.ts +165 -0
- package/apps/studio/test/cloud-history-posture.test.ts +230 -0
- package/apps/studio/test/cloud-session-role.test.ts +30 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +39 -0
- package/apps/studio/test/cold-start-apply.test.ts +303 -0
- package/apps/studio/test/collab-stress.test.ts +9 -1
- package/apps/studio/test/export-lane.test.ts +245 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +1 -1
- package/apps/studio/test/git-log-format.test.ts +95 -0
- package/apps/studio/test/git-safe-rel.test.ts +132 -0
- package/apps/studio/test/hmr-broadcast.test.ts +26 -0
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +131 -0
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +5 -2
- package/apps/studio/test/sync-agent.test.ts +78 -0
- package/apps/studio/test/sync-asset-push.test.ts +71 -108
- package/apps/studio/test/sync-autocommit.test.ts +80 -0
- package/apps/studio/test/sync-cell-write-nudge.test.ts +346 -0
- package/apps/studio/test/sync-ctl-channel.test.ts +508 -0
- package/apps/studio/test/sync-decide-file.test.ts +420 -0
- package/apps/studio/test/sync-file-ledger.test.ts +334 -0
- package/apps/studio/test/sync-file-membership.test.ts +17 -1
- package/apps/studio/test/sync-file-plane.test.ts +976 -0
- package/apps/studio/test/sync-hub-listing.test.ts +46 -0
- package/apps/studio/test/sync-meta-codec.test.ts +76 -0
- package/apps/studio/test/sync-move-retirement.test.ts +231 -0
- package/apps/studio/test/sync-panel-surface.test.ts +20 -0
- package/apps/studio/test/sync-path-pull.test.ts +67 -1
- package/apps/studio/test/sync-pull-budget.test.ts +169 -0
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +83 -0
- package/apps/studio/test/sync-settings-routes.test.ts +195 -0
- package/apps/studio/test/sync-settings.test.ts +151 -0
- package/apps/studio/test/sync-status.test.ts +69 -0
- package/apps/studio/test/sync-trash.test.ts +132 -0
- package/apps/studio/test/workspace-containment.test.ts +45 -9
- package/apps/studio/tsconfig.json +9 -10
- package/apps/studio/use-annotation-resize.tsx +14 -3
- package/apps/studio/use-collab.tsx +3 -1
- package/apps/studio/whats-new.json +90 -0
- package/apps/studio/workspace-mode.ts +110 -62
- package/apps/studio/ws.ts +22 -1
- package/cli/bin/claude-design-server.mjs +19 -0
- package/cli/commands/design.mjs +25 -6
- package/cli/commands/hub-workspace.mjs +243 -22
- package/cli/commands/hub-workspace.test.mjs +171 -0
- package/cli/commands/hub.mjs +71 -1
- package/cli/lib/design-link.mjs +186 -1
- package/cli/lib/design-ownership.mjs +330 -0
- package/cli/lib/design-ownership.test.mjs +329 -0
- package/cli/lib/hubs-config.mjs +21 -0
- package/cli/lib/hubs-config.test.mjs +47 -1
- package/cli/lib/workspace-plan.mjs +298 -5
- package/cli/lib/workspace-plan.test.mjs +215 -1
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +43 -2
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +1 -1
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
- package/apps/studio/server.mjs +0 -1312
- package/apps/studio/sync/asset-pull.ts +0 -210
- package/apps/studio/sync/asset-push-worker.ts +0 -84
- package/apps/studio/sync/asset-sweep.ts +0 -262
- package/apps/studio/test/sync-asset-pull.test.ts +0 -161
- package/apps/studio/test/sync-asset-push-worker.test.ts +0 -183
- package/apps/studio/test/sync-asset-sweep.test.ts +0 -243
|
@@ -33,6 +33,7 @@ import { useTreeDrag } from './use-tree-drag.js';
|
|
|
33
33
|
import ChatPanel from './panels/ChatPanel.jsx';
|
|
34
34
|
import DiffView from './panels/DiffView.jsx';
|
|
35
35
|
import GitPanel from './panels/GitPanel.jsx';
|
|
36
|
+
import SyncConsentDialog from './panels/SyncConsentDialog.jsx';
|
|
36
37
|
import SyncPanel from './panels/SyncPanel.jsx';
|
|
37
38
|
import CloudBar from './panels/CloudBar.jsx';
|
|
38
39
|
import IdentityBar from './panels/IdentityBar.jsx';
|
|
@@ -1537,9 +1538,23 @@ function ExportDialog({
|
|
|
1537
1538
|
comps = [],
|
|
1538
1539
|
activeArtboardId = null,
|
|
1539
1540
|
selection = null,
|
|
1541
|
+
exportLane = 'local',
|
|
1540
1542
|
onClose,
|
|
1541
1543
|
}) {
|
|
1542
|
-
|
|
1544
|
+
// feature-cloud-export-render-workers — on a cell with no render service
|
|
1545
|
+
// (`lane === 'none'`), formats that render through a browser are offered
|
|
1546
|
+
// disabled-with-a-reason instead of firing a request the proxy 404s. ZIP
|
|
1547
|
+
// (JSZip over project files) and the AI-handoff card (clipboard copy) need
|
|
1548
|
+
// no browser and stay live in every lane.
|
|
1549
|
+
const laneBlocked = useCallback(
|
|
1550
|
+
(c) =>
|
|
1551
|
+
(exportLane === 'none' && !c.handoff && c.format !== 'zip') ||
|
|
1552
|
+
// Canva bundles project files the render service can't reach — desktop only
|
|
1553
|
+
// in every workspace lane (exporters/remote.ts REMOTE_UNSUPPORTED_FORMATS).
|
|
1554
|
+
(exportLane !== 'local' && c.format === 'canva'),
|
|
1555
|
+
[exportLane]
|
|
1556
|
+
);
|
|
1557
|
+
const [sel, setSel] = useState(mode === 'handoff' ? 'shadcn' : exportLane === 'none' ? 'zip' : 'png');
|
|
1543
1558
|
const [scope, setScope] = useState(
|
|
1544
1559
|
initialScope && EXPORT_SCOPE_LABELS[initialScope] ? initialScope : 'artboard'
|
|
1545
1560
|
);
|
|
@@ -1622,6 +1637,12 @@ function ExportDialog({
|
|
|
1622
1637
|
setStatus({ ok: true, msg: `Copied: ${cmd} — run it in Claude Code.` });
|
|
1623
1638
|
return;
|
|
1624
1639
|
}
|
|
1640
|
+
if (laneBlocked(card)) {
|
|
1641
|
+
// Belt to the disabled-card braces — a stale selection can't submit a
|
|
1642
|
+
// format this workspace cannot render.
|
|
1643
|
+
setStatus({ ok: false, msg: 'This format needs the render service, which this workspace doesn’t have configured.' });
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1625
1646
|
setBusy(true);
|
|
1626
1647
|
setStatus(null);
|
|
1627
1648
|
// `scale` drives video resolution (deviceScaleFactor → encoder dims);
|
|
@@ -1705,6 +1726,12 @@ function ExportDialog({
|
|
|
1705
1726
|
type="button"
|
|
1706
1727
|
key={c.id}
|
|
1707
1728
|
className={'st-fmt' + (c.id === sel ? ' is-on' : '')}
|
|
1729
|
+
disabled={laneBlocked(c)}
|
|
1730
|
+
title={
|
|
1731
|
+
laneBlocked(c)
|
|
1732
|
+
? 'Needs the render service — not configured on this workspace'
|
|
1733
|
+
: undefined
|
|
1734
|
+
}
|
|
1708
1735
|
onClick={() => {
|
|
1709
1736
|
setSel(c.id);
|
|
1710
1737
|
setStatus(null);
|
|
@@ -1716,6 +1743,14 @@ function ExportDialog({
|
|
|
1716
1743
|
</button>
|
|
1717
1744
|
))}
|
|
1718
1745
|
</div>
|
|
1746
|
+
{exportLane === 'none' && (
|
|
1747
|
+
<div className="st-dialog-note" data-testid="export-lane-note">
|
|
1748
|
+
Rendered exports (PNG, PDF, video…) need the render service, which this
|
|
1749
|
+
workspace doesn't have configured. ZIP and AI handoff still work here —
|
|
1750
|
+
for everything else use the desktop app, or ask your admin to add the
|
|
1751
|
+
<code> maude-render</code> service (see the self-hosting docs).
|
|
1752
|
+
</div>
|
|
1753
|
+
)}
|
|
1719
1754
|
{!card.handoff && (
|
|
1720
1755
|
<div className="st-dialog-row">
|
|
1721
1756
|
<label className="st-dialog-lbl" htmlFor="st-export-scope">
|
|
@@ -2568,7 +2603,7 @@ function Sidebar({
|
|
|
2568
2603
|
// Cloud Phase 25 C2 — viewer role: create / delete / move / rename
|
|
2569
2604
|
// affordances are absent (buttons, composer, row menus, drag & drop).
|
|
2570
2605
|
readOnly = false,
|
|
2571
|
-
/** `{ dashboardUrl
|
|
2606
|
+
/** `{ dashboardUrl?, projectName }` when this is a cloud tab, else null. */
|
|
2572
2607
|
// Tri-state (see the note where this value is created): `undefined` until
|
|
2573
2608
|
// the server config answers, `null` for the desktop, an object for a cloud
|
|
2574
2609
|
// tab. Defaulting it to `null` here is what re-broke the boot 404 after the
|
|
@@ -2612,6 +2647,10 @@ function Sidebar({
|
|
|
2612
2647
|
// Passed through to CloudBar only — lifts linkedHub changes to the app shell
|
|
2613
2648
|
// so the GitPanel's cloud-managed posture (DDR-218) reacts live.
|
|
2614
2649
|
onLinkedHub,
|
|
2650
|
+
// feature-cloud-managed-git-posture — the widened DDR-218 gate, resolved once
|
|
2651
|
+
// in App and handed down. Withdraws the drafts switcher: a local branch
|
|
2652
|
+
// switch moves a HEAD the cell knows nothing about.
|
|
2653
|
+
savingIsManaged = false,
|
|
2615
2654
|
}) {
|
|
2616
2655
|
const filteredGroups = useMemo(() => {
|
|
2617
2656
|
if (!search) return groups;
|
|
@@ -2977,8 +3016,19 @@ function Sidebar({
|
|
|
2977
3016
|
/>
|
|
2978
3017
|
{/* Phase 29 (E4) — the project + draft switcher: a compact one-line dock that
|
|
2979
3018
|
opens UPWARD, sitting directly above the GitHub identity avatar so the two
|
|
2980
|
-
form one bottom dock. Renders nothing until the project is a git repo.
|
|
2981
|
-
|
|
3019
|
+
form one bottom dock. Renders nothing until the project is a git repo.
|
|
3020
|
+
|
|
3021
|
+
WITHDRAWN while somebody else is committing (feature-cloud-managed-git-
|
|
3022
|
+
posture). A draft is a local branch, and switching one moves a HEAD the
|
|
3023
|
+
cell knows nothing about — it would rewrite the working tree under a
|
|
3024
|
+
project whose history is being written elsewhere. Same
|
|
3025
|
+
presentation-not-a-control rule as the rest of DDR-218: `/_api/git/branch`
|
|
3026
|
+
and `/_api/git/checkout` keep exactly their old gates, and a terminal
|
|
3027
|
+
`git checkout` still works (and still flushes into Yjs via DDR-051's
|
|
3028
|
+
watcher). Absent, not disabled — there is nothing to explain here. */}
|
|
3029
|
+
{!savingIsManaged && (
|
|
3030
|
+
<RepoBranchSwitcher project={project} liveBranch={gitBranch} remoteSync={remoteSync} onGetLatest={onGetLatest} />
|
|
3031
|
+
)}
|
|
2982
3032
|
{/* Cloud Phase 23 C3 — Maude Cloud sign-in + remote-project attach, docked
|
|
2983
3033
|
above the GitHub identity. Dev-server-backed, so it works in the desktop
|
|
2984
3034
|
shell AND a plain browser. */}
|
|
@@ -3776,7 +3826,7 @@ function EditDropdown({ onAction, onClose, hasCanvas, readOnly = false }) {
|
|
|
3776
3826
|
function Menubar({
|
|
3777
3827
|
activePath,
|
|
3778
3828
|
project,
|
|
3779
|
-
/** `{ dashboardUrl
|
|
3829
|
+
/** `{ dashboardUrl?, projectName }` when this is a cloud tab, else null. */
|
|
3780
3830
|
// Tri-state (see the note where this value is created): `undefined` until
|
|
3781
3831
|
// the server config answers, `null` for the desktop, an object for a cloud
|
|
3782
3832
|
// tab. Defaulting it to `null` here is what re-broke the boot 404 after the
|
|
@@ -4078,13 +4128,22 @@ function Menubar({
|
|
|
4078
4128
|
component, which is how the two shells started diverging last time. */}
|
|
4079
4129
|
{cloud ? (
|
|
4080
4130
|
<span className="st-cloudback" data-testid="cloud-back">
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4131
|
+
{/* Only when there IS somewhere to go back TO. `dashboardUrl` is
|
|
4132
|
+
absent on a self-hosted hub (one hub serves one project, so there
|
|
4133
|
+
is no dashboard), and a hardcoded fallback here quietly re-created
|
|
4134
|
+
the exact bug the server side just stopped: the link is the ONE
|
|
4135
|
+
way out this tab offers, and it pointed at cloud.maude.sh. The
|
|
4136
|
+
project name below stays either way — a tab must still say which
|
|
4137
|
+
project it is showing. */}
|
|
4138
|
+
{cloud.dashboardUrl ? (
|
|
4139
|
+
<a
|
|
4140
|
+
className="st-cloudback-link"
|
|
4141
|
+
href={cloud.dashboardUrl}
|
|
4142
|
+
title="Back to your projects"
|
|
4143
|
+
>
|
|
4144
|
+
← Dashboard
|
|
4145
|
+
</a>
|
|
4146
|
+
) : null}
|
|
4088
4147
|
{cloud.projectName ? (
|
|
4089
4148
|
<span className="st-cloudback-project" data-testid="cloud-project-name">
|
|
4090
4149
|
{cloud.projectName}
|
|
@@ -4218,8 +4277,8 @@ function Menubar({
|
|
|
4218
4277
|
data-testid="view-only-stamp"
|
|
4219
4278
|
data-tip={
|
|
4220
4279
|
cloud?.user
|
|
4221
|
-
? `${cloud.user} is a viewer on this project — you can browse, comment and export. Ask a project owner for edit access, or sign out if that is not the account you meant to use.`
|
|
4222
|
-
: 'Your role in this project is viewer — you can browse, comment when available, and export. Ask a project owner for edit access.'
|
|
4280
|
+
? `${cloud.user} is a viewer on this project — you can browse, comment and export where available. Ask a project owner for edit access, or sign out if that is not the account you meant to use.`
|
|
4281
|
+
: 'Your role in this project is viewer — you can browse, comment when available, and export where available. Ask a project owner for edit access.'
|
|
4223
4282
|
}
|
|
4224
4283
|
>
|
|
4225
4284
|
VIEW ONLY
|
|
@@ -9610,13 +9669,50 @@ function App() {
|
|
|
9610
9669
|
// UNKNOWN, and anything that would behave differently in the
|
|
9611
9670
|
// two shells must wait rather than assume the desktop.
|
|
9612
9671
|
// null — desktop or plain local browser.
|
|
9613
|
-
// object — a cloud tab (`{ dashboardUrl
|
|
9672
|
+
// object — a cloud tab (`{ dashboardUrl?, projectName, user, role }`).
|
|
9614
9673
|
//
|
|
9615
9674
|
// Collapsing unknown into "not cloud" is what made the cloud studio open with
|
|
9616
9675
|
// two console 404s every time: the sign-in bar and the export centre mounted
|
|
9617
9676
|
// for one frame, each fired the request its shell exists to refuse, and then
|
|
9618
9677
|
// unmounted. Nothing was broken and everything looked broken.
|
|
9619
9678
|
const [cfg, setCfg] = useState({ designRel: '.design', cloud: undefined });
|
|
9679
|
+
// WHO IS SAVING THIS PROJECT — one expression, read by every surface that
|
|
9680
|
+
// would otherwise offer to save it, poll for it, or badge it (DDR-218, widened
|
|
9681
|
+
// by feature-cloud-managed-git-posture).
|
|
9682
|
+
//
|
|
9683
|
+
// These two flags were previously derived at the GitPanel call site alone, so
|
|
9684
|
+
// the panel withdrew correctly while the toolbar menu and the status-bar chip
|
|
9685
|
+
// kept rendering a raw dirty count from `gitStatus.files.length` — the very
|
|
9686
|
+
// "lie about work that is already saved" the withdrawal exists to delete. The
|
|
9687
|
+
// count is the claim, wherever it is drawn; hoisting the rule here is what
|
|
9688
|
+
// keeps a third surface from re-introducing it.
|
|
9689
|
+
//
|
|
9690
|
+
// DECLARED HERE, AT THE TOP, ON PURPOSE. Every local-git POLL below is gated
|
|
9691
|
+
// on it, and a dependency array evaluates during render — a `const` further
|
|
9692
|
+
// down would be in its temporal dead zone at the first `useEffect` that names
|
|
9693
|
+
// it. The same hazard the remote-sync effect's own comment records.
|
|
9694
|
+
//
|
|
9695
|
+
// PRESENTATION, NOT A CONTROL — unchanged from DDR-218. `.git` is untouched:
|
|
9696
|
+
// no hook is installed, no config is written, a terminal `git status` /
|
|
9697
|
+
// `git commit` behaves exactly as before, the local `/_api/git/*` routes keep
|
|
9698
|
+
// exactly their old gates, and `git/watch.ts` still flushes a terminal
|
|
9699
|
+
// `git checkout` into Yjs (DDR-051). What stops is MAUDE's own local git
|
|
9700
|
+
// activity — the polls and the surfaces that describe a repo nobody is
|
|
9701
|
+
// editing through. Disconnect restores every one of them live.
|
|
9702
|
+
const cellManaged = !!cfg.cloud;
|
|
9703
|
+
const cloudManaged = !cfg.cloud && !!cloudLinkedHub?.credentialed;
|
|
9704
|
+
const savingIsManaged = cellManaged || cloudManaged;
|
|
9705
|
+
// The raw truth stays available for surfaces that legitimately need it (the
|
|
9706
|
+
// panel's own file list); only the COUNT — the thing that reads as a to-do —
|
|
9707
|
+
// is withheld when this project's saving is somebody else's job.
|
|
9708
|
+
const unsavedCount = savingIsManaged ? 0 : gitStatus?.files?.length || 0;
|
|
9709
|
+
// The posture, readable from callbacks that must NOT re-subscribe when it
|
|
9710
|
+
// flips: the WS handler is installed once for the socket's lifetime, and the
|
|
9711
|
+
// two git-status refreshers are `useCallback([])`s a dozen call sites depend
|
|
9712
|
+
// on being stable. A ref is how they read a live value without becoming a
|
|
9713
|
+
// reason to tear the socket down.
|
|
9714
|
+
const savingIsManagedRef = useRef(savingIsManaged);
|
|
9715
|
+
savingIsManagedRef.current = savingIsManaged;
|
|
9620
9716
|
// Cloud Phase 25 C2 — viewer role, known at boot from /_config. Every
|
|
9621
9717
|
// editing affordance in the shell gates on this (absent, not hidden).
|
|
9622
9718
|
const viewerMode = !!cfg.readOnly;
|
|
@@ -9662,11 +9758,16 @@ function App() {
|
|
|
9662
9758
|
// Undefined on a server too old to report it, which is why the chip
|
|
9663
9759
|
// renders conditionally rather than printing `vundefined`.
|
|
9664
9760
|
version: data.version,
|
|
9665
|
-
// C2/C4 — `{ dashboardUrl
|
|
9761
|
+
// C2/C4 — `{ dashboardUrl?, projectName, user, role }` when this is a
|
|
9666
9762
|
// cloud tab. Its presence is what tells the shared client it is in a
|
|
9667
9763
|
// browser tab on somebody else's machine; `user`/`role` are what let
|
|
9668
9764
|
// it say WHICH account that tab is, and offer the way out.
|
|
9669
9765
|
cloud: data.cloud ?? null,
|
|
9766
|
+
// feature-cloud-export-render-workers — `local` | `remote` | `none`.
|
|
9767
|
+
// Which export formats this server can actually produce; the export
|
|
9768
|
+
// dialogs gate on it. Absent on older servers → undefined, treated
|
|
9769
|
+
// as `local` (the pre-lane behavior) everywhere it is read.
|
|
9770
|
+
exportLane: data.exportLane,
|
|
9670
9771
|
}));
|
|
9671
9772
|
})
|
|
9672
9773
|
.catch(() => {});
|
|
@@ -9693,7 +9794,20 @@ function App() {
|
|
|
9693
9794
|
}, []);
|
|
9694
9795
|
// Phase 27 (E2) — seed the git dirty-state on mount; live updates arrive over
|
|
9695
9796
|
// the `git-status` WS broadcast (Task 5). Solo/non-git projects → repo:false.
|
|
9797
|
+
//
|
|
9798
|
+
// NOT WHILE SOMEBODY ELSE IS COMMITTING. In cloud-managed posture this asks
|
|
9799
|
+
// about a repo the user is not editing through, and every answer it brings
|
|
9800
|
+
// back is drawn somewhere as a claim about their work. Reactive on
|
|
9801
|
+
// `savingIsManaged`, not mount-only, so Disconnect resumes it live and
|
|
9802
|
+
// Connect stops it live — the requirement `git-cloud-posture.test.ts`
|
|
9803
|
+
// already pins for the panel.
|
|
9696
9804
|
useEffect(() => {
|
|
9805
|
+
if (savingIsManaged) {
|
|
9806
|
+
// Drop what the local repo last said, too. A stale `gitStatus` left
|
|
9807
|
+
// standing keeps the tree's M/A/D badges lit after the posture changed.
|
|
9808
|
+
setGitStatus(null);
|
|
9809
|
+
return undefined;
|
|
9810
|
+
}
|
|
9697
9811
|
let cancelled = false;
|
|
9698
9812
|
fetch('/_api/git/status')
|
|
9699
9813
|
.then((r) => r.json())
|
|
@@ -9704,7 +9818,7 @@ function App() {
|
|
|
9704
9818
|
return () => {
|
|
9705
9819
|
cancelled = true;
|
|
9706
9820
|
};
|
|
9707
|
-
}, []);
|
|
9821
|
+
}, [savingIsManaged]);
|
|
9708
9822
|
const [commentsByFile, setCommentsByFile] = useState({}); // { file: [Comment] }
|
|
9709
9823
|
// Phase 6 — the in-iframe composer owns drafting; the shell no longer holds
|
|
9710
9824
|
// a `draft` state. Mutations route through postMessage → WS instead.
|
|
@@ -10034,10 +10148,15 @@ function App() {
|
|
|
10034
10148
|
// toggles on its own and coexists with Inspector/Changes/Comments/Chat.
|
|
10035
10149
|
const toggleTimeline = useCallback(() => setTimelineOpen((v) => !v), []);
|
|
10036
10150
|
const whatsNew = useWhatsNew(MDCC_VERSION);
|
|
10037
|
-
// Same tri-state rule as the sign-in bar: the
|
|
10038
|
-
//
|
|
10039
|
-
//
|
|
10040
|
-
|
|
10151
|
+
// Same tri-state rule as the sign-in bar: hydrating before the shell is
|
|
10152
|
+
// known 404s every boot. On desktop (`cloud === null`) the export queue is
|
|
10153
|
+
// the LOCAL job runner, always on. In a cloud tab it hydrates only when the
|
|
10154
|
+
// cell actually holds a jobs lane (`exportLane === 'remote'`) — a cell
|
|
10155
|
+
// without a render service still refuses `/_api/export-jobs` outright
|
|
10156
|
+
// (DDR-209 D1 posture, now lane-scoped by feature-cloud-export-render-workers).
|
|
10157
|
+
const exportCenter = useExportCenter({
|
|
10158
|
+
enabled: cfg.cloud === null || cfg.exportLane === 'remote',
|
|
10159
|
+
});
|
|
10041
10160
|
// Phase 29 (E4) — first-run onboarding wizard. The native shell boots a minimal
|
|
10042
10161
|
// "welcome" project on first launch; we ask it whether this is a first run and, if
|
|
10043
10162
|
// so, show the wizard OVER the (empty) canvas browser. Completing any door switches
|
|
@@ -11245,7 +11364,13 @@ function App() {
|
|
|
11245
11364
|
} else if (m.type === 'git-status' && m.payload) {
|
|
11246
11365
|
// Phase 27 (E2) Task 5 — live dirty-state. Updates the Changes-panel
|
|
11247
11366
|
// count + tree M/A/D badges reactively, no polling.
|
|
11248
|
-
|
|
11367
|
+
//
|
|
11368
|
+
// The server PUSHES this, so gating the fetches alone would not be
|
|
11369
|
+
// enough: the broadcast would quietly re-populate everything the
|
|
11370
|
+
// polls stopped asking for. Read through a ref because this handler
|
|
11371
|
+
// is installed once, by an effect that must not re-subscribe the
|
|
11372
|
+
// socket every time the posture flips.
|
|
11373
|
+
if (!savingIsManagedRef.current) setGitStatus(m.payload);
|
|
11249
11374
|
} else if (m.type === 'git-lifecycle' && m.payload) {
|
|
11250
11375
|
// Phase 8 Task 7 — branch switch / pull mid-session. Server has
|
|
11251
11376
|
// already flushed every dirty Y.Doc to JSON; just prompt the user.
|
|
@@ -11283,6 +11408,7 @@ function App() {
|
|
|
11283
11408
|
// dual-allowlist gate them main-origin only). After a mutation we refresh
|
|
11284
11409
|
// status optimistically; the `git-status` WS broadcast also lands shortly.
|
|
11285
11410
|
const refreshGitStatus = useCallback(async () => {
|
|
11411
|
+
if (savingIsManagedRef.current) return; // cloud-managed — nobody polls a repo they don't commit to
|
|
11286
11412
|
try {
|
|
11287
11413
|
const r = await fetch('/_api/git/status');
|
|
11288
11414
|
if (r.ok) setGitStatus(await r.json());
|
|
@@ -11295,6 +11421,11 @@ function App() {
|
|
|
11295
11421
|
// without it the status is local-only and the nudge never fires. Network call —
|
|
11296
11422
|
// call sparingly (mount / interval / post-action), never on the WS hot path.
|
|
11297
11423
|
const refreshRemoteSync = useCallback(async () => {
|
|
11424
|
+
// The ahead/behind probe is a real network `git fetch` against the LOCAL
|
|
11425
|
+
// repo's remote — the one remote a cloud-managed project has nothing to do
|
|
11426
|
+
// with. Gated here as well as at the interval, so a post-action call site
|
|
11427
|
+
// cannot reintroduce it.
|
|
11428
|
+
if (savingIsManagedRef.current) return;
|
|
11298
11429
|
try {
|
|
11299
11430
|
const r = await fetch('/_api/git/status?remote=1');
|
|
11300
11431
|
if (!r.ok) return;
|
|
@@ -11392,35 +11523,49 @@ function App() {
|
|
|
11392
11523
|
}
|
|
11393
11524
|
}, []);
|
|
11394
11525
|
|
|
11526
|
+
// THE HISTORY THAT IS ACTUALLY BEING WRITTEN (feature-cloud-managed-git-
|
|
11527
|
+
// posture). Same signature and same row shape as `gitLoadLog` — the two are
|
|
11528
|
+
// interchangeable at the call site precisely because the format, the argv and
|
|
11529
|
+
// the parser behind them are ONE module (`git/log-format.ts`), imported by
|
|
11530
|
+
// both the local service and the cell.
|
|
11531
|
+
//
|
|
11532
|
+
// Failure is REPORTED, never mistaken for emptiness: `null` means "we could
|
|
11533
|
+
// not reach the cloud" (the panel shows a Retry callout), `[]` means "the
|
|
11534
|
+
// cloud has no versions yet" (the panel shows the empty state). Collapsing
|
|
11535
|
+
// the two is what produced the original bug in the first place.
|
|
11536
|
+
const [cloudHistory, setCloudHistory] = useState(null); // { branch, project, hubHost } | null
|
|
11537
|
+
const gitLoadCloudLog = useCallback(async (path) => {
|
|
11538
|
+
try {
|
|
11539
|
+
const qs =
|
|
11540
|
+
'/_api/cloud/history?limit=40' + (path ? `&path=${encodeURIComponent(path)}` : '');
|
|
11541
|
+
const r = await fetch(qs);
|
|
11542
|
+
if (!r.ok) return null;
|
|
11543
|
+
const data = await r.json();
|
|
11544
|
+
if (!data?.ok) return null;
|
|
11545
|
+
setCloudHistory({
|
|
11546
|
+
branch: data.branch ?? null,
|
|
11547
|
+
project: data.project ?? null,
|
|
11548
|
+
hubHost: data.hubHost ?? null,
|
|
11549
|
+
});
|
|
11550
|
+
return data.entries || [];
|
|
11551
|
+
} catch {
|
|
11552
|
+
return null;
|
|
11553
|
+
}
|
|
11554
|
+
}, []);
|
|
11555
|
+
|
|
11395
11556
|
// Repo-relative path → M/A/D/U badge for the tree (paths match: both the tree
|
|
11396
11557
|
// and gitStatus use `.design/ui/Foo.tsx`). Keyed off gitStatus so it updates
|
|
11397
11558
|
// live with the WS broadcast.
|
|
11398
11559
|
const dirtyByPath = useMemo(() => {
|
|
11399
11560
|
const KIND = { modified: 'M', added: 'A', deleted: 'D', untracked: 'U' };
|
|
11400
11561
|
const m = new Map();
|
|
11562
|
+
// A tree badge is the SAME claim as the withdrawn count, drawn one row at a
|
|
11563
|
+
// time: "this file is unsaved". In cloud-managed posture it is not — the
|
|
11564
|
+
// cell committed it seconds ago. Empty, so the tree simply says nothing.
|
|
11565
|
+
if (savingIsManaged) return m;
|
|
11401
11566
|
for (const f of gitStatus?.files || []) m.set(f.path, KIND[f.status]);
|
|
11402
11567
|
return m;
|
|
11403
|
-
}, [gitStatus]);
|
|
11404
|
-
// WHO IS SAVING THIS PROJECT — one expression, read by every surface that
|
|
11405
|
-
// would otherwise offer to save it (DDR-218 + the cell's `historyOnly`).
|
|
11406
|
-
//
|
|
11407
|
-
// These two flags were previously derived at the GitPanel call site alone, so
|
|
11408
|
-
// the panel withdrew correctly while the toolbar menu and the status-bar chip
|
|
11409
|
-
// kept rendering a raw dirty count from `gitStatus.files.length` — the very
|
|
11410
|
-
// "lie about work that is already saved" the withdrawal exists to delete. The
|
|
11411
|
-
// count is the claim, wherever it is drawn; hoisting the rule here is what
|
|
11412
|
-
// keeps a third surface from re-introducing it.
|
|
11413
|
-
//
|
|
11414
|
-
// PRESENTATION, NOT A CONTROL — unchanged from DDR-218. `.git` is untouched,
|
|
11415
|
-
// a terminal `git` behaves exactly as before, the server routes keep their own
|
|
11416
|
-
// gates, and Disconnect restores every surface live.
|
|
11417
|
-
const cellManaged = !!cfg.cloud;
|
|
11418
|
-
const cloudManaged = !cfg.cloud && !!cloudLinkedHub?.credentialed;
|
|
11419
|
-
const savingIsManaged = cellManaged || cloudManaged;
|
|
11420
|
-
// The raw truth stays available for surfaces that legitimately need it (the
|
|
11421
|
-
// panel's own file list, `dirtyByPath`); only the COUNT — the thing that reads
|
|
11422
|
-
// as a to-do — is withheld when this project's saving is somebody else's job.
|
|
11423
|
-
const unsavedCount = savingIsManaged ? 0 : gitStatus?.files?.length || 0;
|
|
11568
|
+
}, [gitStatus, savingIsManaged]);
|
|
11424
11569
|
|
|
11425
11570
|
// Phase 28 (E3) — keep remote ahead/behind fresh so the "Get latest" nudge
|
|
11426
11571
|
// surfaces on its own: probe once a repo is known, again whenever the Changes
|
|
@@ -11428,6 +11573,13 @@ function App() {
|
|
|
11428
11573
|
// publish then shows up without the user first attempting their own publish).
|
|
11429
11574
|
// Declared after `refreshRemoteSync` to avoid a temporal-dead-zone on the dep.
|
|
11430
11575
|
useEffect(() => {
|
|
11576
|
+
if (savingIsManaged) {
|
|
11577
|
+
// Nothing to nudge about: Publish is withdrawn in this posture, so a
|
|
11578
|
+
// probe could only produce a count nobody may act on. Clear the last
|
|
11579
|
+
// answer as well — a stale ahead/behind would outlive the link.
|
|
11580
|
+
setRemoteSync(null);
|
|
11581
|
+
return undefined;
|
|
11582
|
+
}
|
|
11431
11583
|
if (gitStatus?.repo === false) return; // solo / non-git project — no remote
|
|
11432
11584
|
refreshRemoteSync();
|
|
11433
11585
|
// Poll in the background too — not only while Changes is open — so the dock's
|
|
@@ -11436,7 +11588,7 @@ function App() {
|
|
|
11436
11588
|
// TTL-cached server-side, so a missed tick is cheap to re-issue).
|
|
11437
11589
|
const id = setInterval(refreshRemoteSync, changesOpen ? 60000 : 120000);
|
|
11438
11590
|
return () => clearInterval(id);
|
|
11439
|
-
}, [gitStatus?.repo, changesOpen, refreshRemoteSync]);
|
|
11591
|
+
}, [savingIsManaged, gitStatus?.repo, changesOpen, refreshRemoteSync]);
|
|
11440
11592
|
|
|
11441
11593
|
// ----- Tab management (single-canvas) -----
|
|
11442
11594
|
// Single-canvas model: opening a file REPLACES the active one (no tab strip).
|
|
@@ -12854,6 +13006,18 @@ function App() {
|
|
|
12854
13006
|
if (source) source.postMessage({ dgn: 'export-result', id, ...msg }, replyOrigin);
|
|
12855
13007
|
} catch {}
|
|
12856
13008
|
};
|
|
13009
|
+
// feature-cloud-export-render-workers — a workspace with no render
|
|
13010
|
+
// service can't produce browser-rendered formats; answer the in-canvas
|
|
13011
|
+
// dialog with the reason instead of relaying a request the proxy 404s.
|
|
13012
|
+
// ZIP (and any browser-free format) still goes through.
|
|
13013
|
+
if (cfg.exportLane === 'none' && payload?.format !== 'zip') {
|
|
13014
|
+
reply({
|
|
13015
|
+
ok: false,
|
|
13016
|
+
error:
|
|
13017
|
+
'This format needs the render service, which this workspace doesn’t have configured. ZIP still works here; for rendered formats use the desktop app or ask your admin to add maude-render.',
|
|
13018
|
+
});
|
|
13019
|
+
return;
|
|
13020
|
+
}
|
|
12857
13021
|
try {
|
|
12858
13022
|
// feature-background-export-notification-center — enqueue and reply
|
|
12859
13023
|
// with the job id immediately; the notification center (which
|
|
@@ -14462,6 +14626,7 @@ function App() {
|
|
|
14462
14626
|
canvasKinds={cfg?.canvasKinds}
|
|
14463
14627
|
syncStatus={syncStatus}
|
|
14464
14628
|
onLinkedHub={setCloudLinkedHub}
|
|
14629
|
+
savingIsManaged={savingIsManaged}
|
|
14465
14630
|
/>
|
|
14466
14631
|
);
|
|
14467
14632
|
if (id === 'changes')
|
|
@@ -14498,7 +14663,16 @@ function App() {
|
|
|
14498
14663
|
onDiscard={gitDiscard}
|
|
14499
14664
|
onPublish={gitPublish}
|
|
14500
14665
|
onGetLatest={gitGetLatest}
|
|
14501
|
-
|
|
14666
|
+
// WHICH HISTORY, decided at the ONE place the posture is named — not
|
|
14667
|
+
// inside the panel, which would be a second derivation of the rule
|
|
14668
|
+
// `cloud-managed-save-surfaces.test.ts` exists to keep singular.
|
|
14669
|
+
loadLog={cloudManaged ? gitLoadCloudLog : gitLoadLog}
|
|
14670
|
+
historySource={cloudManaged ? 'cloud' : 'local'}
|
|
14671
|
+
// What the cloud half of the header names. The cell reports its own
|
|
14672
|
+
// project and branch with the log; the hub host is the fallback,
|
|
14673
|
+
// because a header that names the LOCAL folder while listing the
|
|
14674
|
+
// CLOUD's commits is the confusion this feature exists to end.
|
|
14675
|
+
cloudHistory={cloudHistory}
|
|
14502
14676
|
onOpenCanvas={(p) => openTab(p)}
|
|
14503
14677
|
onOpenDiff={(file) => setDiffTarget({ file, beforeSha: 'HEAD', conflict: false })}
|
|
14504
14678
|
activeCanvas={activeCanvasFile}
|
|
@@ -14606,6 +14780,10 @@ function App() {
|
|
|
14606
14780
|
<CloudRoleBanner cloud={cfg.cloud} />
|
|
14607
14781
|
<UpdateBanner update={updateReady} onDismiss={() => setUpdateReady(null)} />
|
|
14608
14782
|
<SyncBanner status={syncStatus} />
|
|
14783
|
+
{/* First-upgrade consent (Task 2) — global on purpose: a consent that
|
|
14784
|
+
only appears if you happen to open the Sync panel is not consent.
|
|
14785
|
+
Skipped during first-run onboarding so two flows don't stack. */}
|
|
14786
|
+
{!firstRun && <SyncConsentDialog status={syncStatus} cloud={cfg.cloud} />}
|
|
14609
14787
|
{!usageNudge && !tourSteps && <WhatsNewToast wn={whatsNew} />}
|
|
14610
14788
|
{!usageNudge && !tourSteps && <ExportToast center={exportCenter} />}
|
|
14611
14789
|
{gitLifecycle && (
|
|
@@ -15652,6 +15830,7 @@ function App() {
|
|
|
15652
15830
|
// scope=artboard fell back to `:first-of-type` (always the first).
|
|
15653
15831
|
activeArtboardId={selected?.artboardId ?? canvasActiveArtboard ?? null}
|
|
15654
15832
|
selection={selected?.selector ? { selector: selected.selector, file: selected.file } : null}
|
|
15833
|
+
exportLane={cfg.exportLane || 'local'}
|
|
15655
15834
|
onClose={() => setExportDialog(null)}
|
|
15656
15835
|
/>
|
|
15657
15836
|
)}
|