@1agh/maude 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/apps/studio/acp/bridge.ts +285 -0
- package/apps/studio/acp/env.ts +48 -0
- package/apps/studio/acp/index.ts +132 -0
- package/apps/studio/acp/probe.ts +112 -0
- package/apps/studio/acp/transcript.ts +149 -0
- package/apps/studio/annotations-layer.tsx +6 -1
- package/apps/studio/api.ts +225 -66
- package/apps/studio/bin/chat-open.sh +44 -0
- package/apps/studio/canvas-lib.tsx +112 -19
- package/apps/studio/canvas-list-watch.ts +177 -0
- package/apps/studio/canvas-shell.tsx +22 -2
- package/apps/studio/client/app.jsx +781 -26
- package/apps/studio/client/canvas-url.js +5 -0
- package/apps/studio/client/github.js +99 -0
- package/apps/studio/client/panels/ChatPanel.jsx +770 -0
- package/apps/studio/client/panels/CollabModelInfographic.jsx +71 -0
- package/apps/studio/client/panels/CreateProject.jsx +334 -0
- package/apps/studio/client/panels/DiffView.jsx +590 -0
- package/apps/studio/client/panels/GitPanel.jsx +767 -0
- package/apps/studio/client/panels/IdentityBar.jsx +294 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +563 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +349 -0
- package/apps/studio/client/panels/acp-runtime.js +286 -0
- package/apps/studio/client/panels/chat-markdown.jsx +138 -0
- package/apps/studio/client/panels/git-grouping.js +86 -0
- package/apps/studio/client/styles/0-reset.css +4 -0
- package/apps/studio/client/styles/3-shell-maude.css +613 -3
- package/apps/studio/client/styles/5-maude-overrides.css +25 -0
- package/apps/studio/client/styles/6-acp-chat.css +771 -0
- package/apps/studio/client/styles/_index.css +2 -0
- package/apps/studio/client/tour/collab-tour.js +61 -0
- package/apps/studio/client/tour/overlay.jsx +11 -1
- package/apps/studio/client/whats-new.jsx +25 -10
- package/apps/studio/collab/registry.ts +13 -0
- package/apps/studio/collab/room.ts +36 -0
- package/apps/studio/cursors-overlay.tsx +17 -1
- package/apps/studio/dist/client.bundle.js +28780 -1534
- package/apps/studio/dist/comment-mount.js +4 -2
- package/apps/studio/dist/styles.css +5633 -1617
- package/apps/studio/git/endpoints.ts +338 -0
- package/apps/studio/git/service.ts +1334 -0
- package/apps/studio/git/watch.ts +97 -0
- package/apps/studio/github/endpoints.ts +358 -0
- package/apps/studio/github/service.ts +231 -0
- package/apps/studio/github/token.ts +53 -0
- package/apps/studio/hmr-broadcast.ts +9 -2
- package/apps/studio/http.ts +384 -1
- package/apps/studio/participants-chrome.tsx +69 -9
- package/apps/studio/paths.ts +12 -0
- package/apps/studio/scaffold-design.ts +57 -0
- package/apps/studio/server.ts +65 -2
- package/apps/studio/sync/agent.ts +81 -1
- package/apps/studio/sync/codec.ts +24 -0
- package/apps/studio/sync/cold-start.ts +40 -0
- package/apps/studio/sync/hub-link.ts +137 -0
- package/apps/studio/test/acp-bridge.test.ts +127 -0
- package/apps/studio/test/acp-env.test.ts +65 -0
- package/apps/studio/test/acp-origin-gate.test.ts +78 -0
- package/apps/studio/test/acp-transcript.test.ts +112 -0
- package/apps/studio/test/canvas-create-api.test.ts +72 -0
- package/apps/studio/test/canvas-list-watch.test.ts +322 -0
- package/apps/studio/test/canvas-meta-api.test.ts +161 -27
- package/apps/studio/test/canvas-origin-gate.test.ts +35 -0
- package/apps/studio/test/chat-markdown.test.tsx +58 -0
- package/apps/studio/test/collab-session-survival.test.tsx +176 -0
- package/apps/studio/test/csrf-write-guard.test.ts +26 -0
- package/apps/studio/test/editing-presence.test.ts +103 -0
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +45 -0
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +106 -0
- package/apps/studio/test/git-grouping.test.ts +106 -0
- package/apps/studio/test/git-watch.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +465 -0
- package/apps/studio/test/hub-link.test.ts +69 -0
- package/apps/studio/test/participants-chrome.test.ts +36 -1
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +187 -0
- package/apps/studio/test/sync-cold-start.test.ts +61 -1
- package/apps/studio/test/tour-overlay.test.tsx +18 -0
- package/apps/studio/tool-palette.tsx +18 -9
- package/apps/studio/use-chrome-visibility.tsx +66 -0
- package/apps/studio/use-collab.tsx +414 -187
- package/apps/studio/whats-new.json +73 -0
- package/apps/studio/ws.ts +44 -1
- package/cli/commands/design.mjs +1 -0
- package/cli/lib/gitignore-block.mjs +16 -3
- package/cli/lib/gitignore-block.test.mjs +13 -1
- package/package.json +11 -9
- package/plugins/design/dependencies.json +17 -0
- package/plugins/design/templates/_shell.html +30 -8
|
@@ -13,11 +13,21 @@ import { createRoot } from 'react-dom/client';
|
|
|
13
13
|
// bundle — no React, no input-router. See the tool-cursor handler below.
|
|
14
14
|
import { resolveToolCursor } from '../canvas-cursors.ts';
|
|
15
15
|
import { canvasUrl } from './canvas-url.js';
|
|
16
|
+
import ChatPanel from './panels/ChatPanel.jsx';
|
|
17
|
+
import DiffView from './panels/DiffView.jsx';
|
|
18
|
+
import GitPanel from './panels/GitPanel.jsx';
|
|
19
|
+
import IdentityBar from './panels/IdentityBar.jsx';
|
|
20
|
+
import OnboardingWizard from './panels/OnboardingWizard.jsx';
|
|
21
|
+
import RepoBranchSwitcher from './panels/RepoBranchSwitcher.jsx';
|
|
22
|
+
import { appIsFirstRun, isNativeApp, onUpdateReady, restartToUpdate } from './github.js';
|
|
23
|
+
import { COLLAB_TOUR } from './tour/collab-tour.js';
|
|
16
24
|
import { TourOverlay } from './tour/overlay.jsx';
|
|
17
25
|
import { USAGE_TOUR } from './tour/usage-tour.js';
|
|
18
26
|
import { useWhatsNew, WhatsNewPanel, WhatsNewToast } from './whats-new.jsx';
|
|
19
27
|
|
|
20
28
|
const USAGE_TOUR_STORE = 'mdcc-usage-tour-seen';
|
|
29
|
+
// Phase 29 (E4) — the collab "rychlý kurz" is offered once after onboarding.
|
|
30
|
+
const COLLAB_TOUR_STORE = 'mdcc-collab-tour-seen';
|
|
21
31
|
|
|
22
32
|
const SYSTEM_TAB = '__system__';
|
|
23
33
|
const THEME_STORE = 'mdcc-theme';
|
|
@@ -296,6 +306,13 @@ const STICONS = {
|
|
|
296
306
|
sparkle: (
|
|
297
307
|
<path d="M8 1.8l1.4 4.8L14 8l-4.6 1.4L8 14.2l-1.4-4.8L2 8l4.6-1.4z" fill="currentColor" stroke="none" />
|
|
298
308
|
),
|
|
309
|
+
megaphone: (
|
|
310
|
+
<>
|
|
311
|
+
<path d="M2 6.7 11 4v8L2 9.3z" />
|
|
312
|
+
<path d="M11 5.2a2.4 2.4 0 0 1 0 5.6" />
|
|
313
|
+
<path d="M4.3 9.5v2.3a1.2 1.2 0 0 0 2.4 0v-1.7" />
|
|
314
|
+
</>
|
|
315
|
+
),
|
|
299
316
|
'panel-left': (
|
|
300
317
|
<>
|
|
301
318
|
<rect x="2.5" y="3" width="11" height="10" rx="1.5" />
|
|
@@ -1063,7 +1080,7 @@ function DsFolderRow({ name, dsName, depth, defaultOpen, active, onOpenSystem, c
|
|
|
1063
1080
|
);
|
|
1064
1081
|
}
|
|
1065
1082
|
|
|
1066
|
-
function FileRow({ file, activePath, onOpen, onDelete, openCount: oc, depth, kind, sidecar }) {
|
|
1083
|
+
function FileRow({ file, activePath, onOpen, onDelete, openCount: oc, depth, kind, sidecar, dirty }) {
|
|
1067
1084
|
const isSel = file.path === activePath;
|
|
1068
1085
|
const isCanvas = CANVAS_EXT_RE.test(file.name);
|
|
1069
1086
|
// Non-canvas rows (PROJECT *.md, RUNTIME _active.json, ...) are display-only —
|
|
@@ -1094,6 +1111,11 @@ function FileRow({ file, activePath, onOpen, onDelete, openCount: oc, depth, kin
|
|
|
1094
1111
|
<StIcon name="file" size={13} />
|
|
1095
1112
|
</span>
|
|
1096
1113
|
<span className="st-row-name">{label}</span>
|
|
1114
|
+
{dirty && (
|
|
1115
|
+
<span className="st-git-badge" data-kind={dirty} title={`Unsaved (${dirty})`} aria-label={`Unsaved, ${dirty}`}>
|
|
1116
|
+
{dirty}
|
|
1117
|
+
</span>
|
|
1118
|
+
)}
|
|
1097
1119
|
{oc > 0 && <span className="st-row-badge">{oc}</span>}
|
|
1098
1120
|
</button>
|
|
1099
1121
|
);
|
|
@@ -1130,7 +1152,9 @@ function CanvasRow({
|
|
|
1130
1152
|
openCount: oc,
|
|
1131
1153
|
showHidden,
|
|
1132
1154
|
forceOpen,
|
|
1155
|
+
dirtyByPath,
|
|
1133
1156
|
}) {
|
|
1157
|
+
const dirty = dirtyByPath?.get(primary.path);
|
|
1134
1158
|
const hasSidecars = sidecars.length > 0;
|
|
1135
1159
|
const [openState, setOpenState] = useState(false);
|
|
1136
1160
|
// Sidecars are only revealed when the user opts in via `showHidden` — the
|
|
@@ -1149,6 +1173,7 @@ function CanvasRow({
|
|
|
1149
1173
|
openCount={oc}
|
|
1150
1174
|
depth={depth}
|
|
1151
1175
|
kind={kind}
|
|
1176
|
+
dirty={dirty}
|
|
1152
1177
|
/>
|
|
1153
1178
|
);
|
|
1154
1179
|
}
|
|
@@ -1182,6 +1207,11 @@ function CanvasRow({
|
|
|
1182
1207
|
<StIcon name="chevron-right" className={'st-chev' + (open ? ' is-open' : '')} size={13} />
|
|
1183
1208
|
</span>
|
|
1184
1209
|
<span className="st-row-name">{displayName(primary.name)}</span>
|
|
1210
|
+
{dirty && (
|
|
1211
|
+
<span className="st-git-badge" data-kind={dirty} title={`Unsaved (${dirty})`} aria-label={`Unsaved, ${dirty}`}>
|
|
1212
|
+
{dirty}
|
|
1213
|
+
</span>
|
|
1214
|
+
)}
|
|
1185
1215
|
{oc > 0 && <span className="st-row-badge">{oc}</span>}
|
|
1186
1216
|
</button>
|
|
1187
1217
|
{open &&
|
|
@@ -1214,6 +1244,7 @@ function Tree({
|
|
|
1214
1244
|
activeDsName,
|
|
1215
1245
|
onOpenSystem,
|
|
1216
1246
|
onDelete,
|
|
1247
|
+
dirtyByPath,
|
|
1217
1248
|
}) {
|
|
1218
1249
|
const dirs = Object.keys(node)
|
|
1219
1250
|
.filter((k) => k !== '_files')
|
|
@@ -1255,6 +1286,7 @@ function Tree({
|
|
|
1255
1286
|
kind={kind}
|
|
1256
1287
|
showHidden={showHidden}
|
|
1257
1288
|
forceOpen={forceOpen}
|
|
1289
|
+
dirtyByPath={dirtyByPath}
|
|
1258
1290
|
/>
|
|
1259
1291
|
);
|
|
1260
1292
|
})}
|
|
@@ -1286,6 +1318,7 @@ function Tree({
|
|
|
1286
1318
|
activeDsName={activeDsName}
|
|
1287
1319
|
onOpenSystem={onOpenSystem}
|
|
1288
1320
|
onDelete={onDelete}
|
|
1321
|
+
dirtyByPath={dirtyByPath}
|
|
1289
1322
|
/>
|
|
1290
1323
|
);
|
|
1291
1324
|
if (dsMatch && onOpenSystem) {
|
|
@@ -1353,6 +1386,9 @@ function Sidebar({
|
|
|
1353
1386
|
onCollapse,
|
|
1354
1387
|
width,
|
|
1355
1388
|
resizing,
|
|
1389
|
+
dirtyByPath,
|
|
1390
|
+
project,
|
|
1391
|
+
gitBranch,
|
|
1356
1392
|
}) {
|
|
1357
1393
|
const filteredGroups = useMemo(() => {
|
|
1358
1394
|
if (!search) return groups;
|
|
@@ -1569,6 +1605,7 @@ function Sidebar({
|
|
|
1569
1605
|
activeDsName={activeDsName}
|
|
1570
1606
|
onOpenSystem={isDs ? onOpenSystem : undefined}
|
|
1571
1607
|
onDelete={isDs ? undefined : onDeleteBoard}
|
|
1608
|
+
dirtyByPath={dirtyByPath}
|
|
1572
1609
|
/>
|
|
1573
1610
|
) : (
|
|
1574
1611
|
<div className="st-tree-empty">{search ? 'No matches.' : 'Empty.'}</div>
|
|
@@ -1577,6 +1614,14 @@ function Sidebar({
|
|
|
1577
1614
|
);
|
|
1578
1615
|
})}
|
|
1579
1616
|
</div>
|
|
1617
|
+
{/* Phase 29 (E4) — the project + draft switcher: a compact one-line dock that
|
|
1618
|
+
opens UPWARD, sitting directly above the GitHub identity avatar so the two
|
|
1619
|
+
form one bottom dock. Renders nothing until the project is a git repo. */}
|
|
1620
|
+
<RepoBranchSwitcher project={project} liveBranch={gitBranch} />
|
|
1621
|
+
{/* Phase 28 (E3) — GitHub identity as a compact avatar docked at the BOTTOM:
|
|
1622
|
+
sign in, connected account + New/Pull/Share, sign out. Self-contained
|
|
1623
|
+
(owns its device-code + CreateProject dialogs). Renders nothing in browser. */}
|
|
1624
|
+
<IdentityBar />
|
|
1580
1625
|
</nav>
|
|
1581
1626
|
);
|
|
1582
1627
|
}
|
|
@@ -2200,6 +2245,10 @@ function HelpDropdown({ onAction, onClose }) {
|
|
|
2200
2245
|
{ id: 'help', label: 'Help · commands & flows', shortcut: 'F1' },
|
|
2201
2246
|
{ sep: true },
|
|
2202
2247
|
{ id: 'tour', label: 'Take the tour' },
|
|
2248
|
+
// The collab "how sharing works" course teaches the plain-words Save →
|
|
2249
|
+
// Publish → Pull cycle — a non-technical, native-app concern. A web-studio
|
|
2250
|
+
// dev already knows git, so it's hidden there (DDR-119).
|
|
2251
|
+
...(isNativeApp() ? [{ id: 'collab-tour', label: 'How sharing works' }] : []),
|
|
2203
2252
|
{ id: 'whatsnew', label: "What's new" },
|
|
2204
2253
|
]}
|
|
2205
2254
|
/>
|
|
@@ -2296,6 +2345,9 @@ function Menubar({
|
|
|
2296
2345
|
setOpenMenu,
|
|
2297
2346
|
commentsPanelOpen,
|
|
2298
2347
|
onToggleComments,
|
|
2348
|
+
changesOpen,
|
|
2349
|
+
changesCount,
|
|
2350
|
+
onToggleChanges,
|
|
2299
2351
|
onOpenSystem,
|
|
2300
2352
|
sidebarOpen,
|
|
2301
2353
|
onToggleSidebar,
|
|
@@ -2304,8 +2356,15 @@ function Menubar({
|
|
|
2304
2356
|
onOpenHelp,
|
|
2305
2357
|
onOpenShortcuts,
|
|
2306
2358
|
onStartTour,
|
|
2359
|
+
onStartCollabTour,
|
|
2307
2360
|
annotationsVisible,
|
|
2308
2361
|
onToggleAnnotations,
|
|
2362
|
+
minimapVisible,
|
|
2363
|
+
onToggleMinimap,
|
|
2364
|
+
zoomCtlVisible,
|
|
2365
|
+
onToggleZoomCtl,
|
|
2366
|
+
presentMode,
|
|
2367
|
+
onTogglePresent,
|
|
2309
2368
|
postToActiveCanvas,
|
|
2310
2369
|
onOpenWhatsNew,
|
|
2311
2370
|
whatsNewCount,
|
|
@@ -2315,6 +2374,10 @@ function Menubar({
|
|
|
2315
2374
|
inspectorTab,
|
|
2316
2375
|
onToggleInspector,
|
|
2317
2376
|
onOpenLayers,
|
|
2377
|
+
assistantOpen,
|
|
2378
|
+
onToggleAssistant,
|
|
2379
|
+
assistantBusy,
|
|
2380
|
+
assistantUnseen,
|
|
2318
2381
|
onNewCanvas,
|
|
2319
2382
|
onOpenExport,
|
|
2320
2383
|
onReload,
|
|
@@ -2334,6 +2397,13 @@ function Menubar({
|
|
|
2334
2397
|
|
|
2335
2398
|
const panels = [
|
|
2336
2399
|
{ id: 'tree', label: 'Project Tree', shortcut: 'T', checked: sidebarOpen, disabled: false },
|
|
2400
|
+
{
|
|
2401
|
+
id: 'changes',
|
|
2402
|
+
label: changesCount > 0 ? `Changes · ${changesCount} unsaved` : 'Changes',
|
|
2403
|
+
shortcut: '⌘ ⇧ G',
|
|
2404
|
+
checked: changesOpen,
|
|
2405
|
+
disabled: false,
|
|
2406
|
+
},
|
|
2337
2407
|
{
|
|
2338
2408
|
id: 'comments',
|
|
2339
2409
|
label: 'Comments Sidebar',
|
|
@@ -2362,6 +2432,18 @@ function Menubar({
|
|
|
2362
2432
|
checked: inspectorOpen,
|
|
2363
2433
|
disabled: false,
|
|
2364
2434
|
},
|
|
2435
|
+
// Phase 31 (DDR-123) — native-only ACP chat sidepanel.
|
|
2436
|
+
...(isNativeApp()
|
|
2437
|
+
? [
|
|
2438
|
+
{
|
|
2439
|
+
id: 'assistant',
|
|
2440
|
+
label: 'Assistant',
|
|
2441
|
+
shortcut: '⌘ ⇧ A',
|
|
2442
|
+
checked: assistantOpen,
|
|
2443
|
+
disabled: false,
|
|
2444
|
+
},
|
|
2445
|
+
]
|
|
2446
|
+
: []),
|
|
2365
2447
|
{
|
|
2366
2448
|
id: 'annotate',
|
|
2367
2449
|
label: 'Annotations',
|
|
@@ -2369,7 +2451,27 @@ function Menubar({
|
|
|
2369
2451
|
checked: annotationsVisible,
|
|
2370
2452
|
disabled: false,
|
|
2371
2453
|
},
|
|
2372
|
-
{
|
|
2454
|
+
{
|
|
2455
|
+
id: 'minimap',
|
|
2456
|
+
label: 'Minimap',
|
|
2457
|
+
shortcut: '',
|
|
2458
|
+
checked: minimapVisible,
|
|
2459
|
+
disabled: !activePath || isSystem,
|
|
2460
|
+
},
|
|
2461
|
+
{
|
|
2462
|
+
id: 'zoomctl',
|
|
2463
|
+
label: 'Zoom controls',
|
|
2464
|
+
shortcut: '',
|
|
2465
|
+
checked: zoomCtlVisible,
|
|
2466
|
+
disabled: !activePath || isSystem,
|
|
2467
|
+
},
|
|
2468
|
+
{
|
|
2469
|
+
id: 'present',
|
|
2470
|
+
label: 'Presentation Mode',
|
|
2471
|
+
shortcut: '',
|
|
2472
|
+
checked: presentMode,
|
|
2473
|
+
disabled: !activePath || isSystem,
|
|
2474
|
+
},
|
|
2373
2475
|
];
|
|
2374
2476
|
|
|
2375
2477
|
const DROPDOWN_MENUS = ['file', 'edit', 'view', 'selection', 'tools', 'help'];
|
|
@@ -2489,11 +2591,16 @@ function Menubar({
|
|
|
2489
2591
|
panels={panels}
|
|
2490
2592
|
onToggle={(id) => {
|
|
2491
2593
|
if (id === 'tree') onToggleSidebar();
|
|
2594
|
+
else if (id === 'changes') onToggleChanges();
|
|
2492
2595
|
else if (id === 'comments') onToggleComments();
|
|
2493
2596
|
else if (id === 'hidden') onToggleShowHidden();
|
|
2494
2597
|
else if (id === 'annotate') onToggleAnnotations();
|
|
2495
2598
|
else if (id === 'inspector') onToggleInspector();
|
|
2599
|
+
else if (id === 'assistant') onToggleAssistant?.();
|
|
2496
2600
|
else if (id === 'layers') onOpenLayers?.();
|
|
2601
|
+
else if (id === 'minimap') onToggleMinimap?.();
|
|
2602
|
+
else if (id === 'zoomctl') onToggleZoomCtl?.();
|
|
2603
|
+
else if (id === 'present') onTogglePresent?.();
|
|
2497
2604
|
}}
|
|
2498
2605
|
onZoom={(op) => postToActiveCanvas({ dgn: 'zoom', op })}
|
|
2499
2606
|
hasCanvas={!!activePath && !isSystem}
|
|
@@ -2522,6 +2629,7 @@ function Menubar({
|
|
|
2522
2629
|
if (id === 'shortcuts') onOpenShortcuts?.();
|
|
2523
2630
|
else if (id === 'help') onOpenHelp?.();
|
|
2524
2631
|
else if (id === 'tour') onStartTour?.();
|
|
2632
|
+
else if (id === 'collab-tour') onStartCollabTour?.();
|
|
2525
2633
|
else if (id === 'whatsnew') onOpenWhatsNew?.();
|
|
2526
2634
|
}}
|
|
2527
2635
|
onClose={() => setOpenMenu(null)}
|
|
@@ -2529,6 +2637,20 @@ function Menubar({
|
|
|
2529
2637
|
)}
|
|
2530
2638
|
<div className="st-mb-right" data-tour="status">
|
|
2531
2639
|
{presence ? <div className="st-presence">{presence}</div> : null}
|
|
2640
|
+
{isNativeApp() && (
|
|
2641
|
+
<button
|
|
2642
|
+
type="button"
|
|
2643
|
+
className="st-assistant"
|
|
2644
|
+
data-active={assistantOpen ? 'true' : 'false'}
|
|
2645
|
+
data-busy={assistantBusy ? 'true' : 'false'}
|
|
2646
|
+
data-unseen={assistantUnseen ? 'true' : 'false'}
|
|
2647
|
+
aria-label={`Assistant${assistantBusy ? ' — working' : assistantUnseen ? ' — new reply' : ''}`}
|
|
2648
|
+
data-tip="Assistant ⌘⇧A"
|
|
2649
|
+
onClick={onToggleAssistant}
|
|
2650
|
+
>
|
|
2651
|
+
<StIcon name="sparkle" size={15} />
|
|
2652
|
+
</button>
|
|
2653
|
+
)}
|
|
2532
2654
|
<button
|
|
2533
2655
|
type="button"
|
|
2534
2656
|
className="st-whatsnew"
|
|
@@ -2538,7 +2660,7 @@ function Menubar({
|
|
|
2538
2660
|
data-tip="What's new"
|
|
2539
2661
|
onClick={onOpenWhatsNew}
|
|
2540
2662
|
>
|
|
2541
|
-
<StIcon name="
|
|
2663
|
+
<StIcon name="megaphone" size={15} />
|
|
2542
2664
|
</button>
|
|
2543
2665
|
<span className="st-stamp">{stamp}</span>
|
|
2544
2666
|
<span className="st-mb-file" title={activePath || ''}>
|
|
@@ -2913,6 +3035,10 @@ function StatusBar({
|
|
|
2913
3035
|
onToggleTheme,
|
|
2914
3036
|
onClearSelected,
|
|
2915
3037
|
syncStatus,
|
|
3038
|
+
changesCount = 0,
|
|
3039
|
+
unpushed = 0,
|
|
3040
|
+
changesOpen = false,
|
|
3041
|
+
onOpenChanges,
|
|
2916
3042
|
}) {
|
|
2917
3043
|
const isSystem = activePath === SYSTEM_TAB;
|
|
2918
3044
|
const text =
|
|
@@ -2996,6 +3122,33 @@ function StatusBar({
|
|
|
2996
3122
|
<span className="val">{openCount} open</span>
|
|
2997
3123
|
</span>
|
|
2998
3124
|
|
|
3125
|
+
{/* Phase 28 — changes count, click to open the Changes panel (⌘⇧G). */}
|
|
3126
|
+
{onOpenChanges && (
|
|
3127
|
+
<button
|
|
3128
|
+
type="button"
|
|
3129
|
+
className={
|
|
3130
|
+
'st-sb-slot st-sb-changes' +
|
|
3131
|
+
(changesOpen ? ' is-open' : '') +
|
|
3132
|
+
(changesCount > 0 ? ' has-changes' : unpushed > 0 ? ' has-unpushed' : '')
|
|
3133
|
+
}
|
|
3134
|
+
onClick={onOpenChanges}
|
|
3135
|
+
data-tip="Open Changes · ⌘⇧G"
|
|
3136
|
+
data-tip-pos="top"
|
|
3137
|
+
aria-label="Open Changes panel"
|
|
3138
|
+
aria-pressed={changesOpen}
|
|
3139
|
+
>
|
|
3140
|
+
<span className="st-sb-changes-dot" aria-hidden="true" />
|
|
3141
|
+
<span className="lbl">changes</span>
|
|
3142
|
+
<span className="val">
|
|
3143
|
+
{changesCount > 0
|
|
3144
|
+
? `${changesCount} unsaved`
|
|
3145
|
+
: unpushed > 0
|
|
3146
|
+
? `${unpushed} to publish`
|
|
3147
|
+
: 'all saved'}
|
|
3148
|
+
</span>
|
|
3149
|
+
</button>
|
|
3150
|
+
)}
|
|
3151
|
+
|
|
2999
3152
|
<span className="st-sb-spacer" />
|
|
3000
3153
|
|
|
3001
3154
|
<span className="st-sb-slot" role="group" aria-label="Connection">
|
|
@@ -3204,6 +3357,35 @@ function CommentsPanel({
|
|
|
3204
3357
|
// while offline (with queued-edit count), red when offline > 24h, green flash
|
|
3205
3358
|
// for 3s right after a reconnect. Driven entirely by the 'sync:status' payload
|
|
3206
3359
|
// the dev-server's linked-mode sync runtime broadcasts.
|
|
3360
|
+
// Phase 32 (Task 1) — auto-update notice. Shown after the shell has downloaded +
|
|
3361
|
+
// staged a newer build in the background. Non-blocking: "Restart now" applies it,
|
|
3362
|
+
// "Later" dismisses (the next focus/4h check re-stages and re-surfaces it).
|
|
3363
|
+
function UpdateBanner({ update, onDismiss }) {
|
|
3364
|
+
const [restarting, setRestarting] = useState(false);
|
|
3365
|
+
if (!update) return null;
|
|
3366
|
+
const ver = update.version ? ` (v${update.version})` : '';
|
|
3367
|
+
return (
|
|
3368
|
+
<div role="status" aria-live="polite" className="st-banner st-banner--info">
|
|
3369
|
+
<span className="st-banner-dot" aria-hidden="true" />
|
|
3370
|
+
<span>Maude updated{ver} · restart to apply</span>
|
|
3371
|
+
<button
|
|
3372
|
+
type="button"
|
|
3373
|
+
className="btn btn--primary btn--sm"
|
|
3374
|
+
disabled={restarting}
|
|
3375
|
+
onClick={() => {
|
|
3376
|
+
setRestarting(true);
|
|
3377
|
+
restartToUpdate().catch(() => setRestarting(false));
|
|
3378
|
+
}}
|
|
3379
|
+
>
|
|
3380
|
+
{restarting ? 'Restarting…' : 'Restart now'}
|
|
3381
|
+
</button>
|
|
3382
|
+
<button type="button" className="btn btn--ghost btn--sm" onClick={onDismiss}>
|
|
3383
|
+
Later
|
|
3384
|
+
</button>
|
|
3385
|
+
</div>
|
|
3386
|
+
);
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3207
3389
|
function SyncBanner({ status }) {
|
|
3208
3390
|
// Plan C follow-up — the banner overlapped the menubar and wasn't dismissable.
|
|
3209
3391
|
// Dismissal is keyed on the connection state, so a transition (reconnect flash,
|
|
@@ -5260,6 +5442,20 @@ function App() {
|
|
|
5260
5442
|
// Phase 9 Task 8 — hub-down offline mode banner. Driven by the 'sync:status'
|
|
5261
5443
|
// WS message the linked-mode sync runtime emits. null in solo mode.
|
|
5262
5444
|
const [syncStatus, setSyncStatus] = useState(null);
|
|
5445
|
+
// Phase 27 (E2) — in-UI git layer. `gitStatus` is the live dirty-state the
|
|
5446
|
+
// server broadcasts on `git-status`; `changesOpen` toggles the Changes panel;
|
|
5447
|
+
// `diffTarget` opens the before/after DiffView ({ file, conflict }).
|
|
5448
|
+
const [gitStatus, setGitStatus] = useState(null);
|
|
5449
|
+
// Phase 28 (E3) — remote ahead/behind ("Get latest" nudge). Kept in its OWN
|
|
5450
|
+
// slice, NOT folded into `gitStatus`, because the `git-status` WS broadcast
|
|
5451
|
+
// (line ~5791) replaces `gitStatus` on every dirty-state change and carries
|
|
5452
|
+
// only LOCAL status — merging remote-ahead into it would be clobbered on the
|
|
5453
|
+
// next keystroke. The probe is a real network `git fetch` (server-side, token
|
|
5454
|
+
// from the keychain bridge), so it runs on a slow cadence — mount, a periodic
|
|
5455
|
+
// tick, and after each git action — never on the per-edit WS path.
|
|
5456
|
+
const [remoteSync, setRemoteSync] = useState(null); // { remoteAhead, behind } | null
|
|
5457
|
+
const [changesOpen, setChangesOpen] = useState(false);
|
|
5458
|
+
const [diffTarget, setDiffTarget] = useState(null);
|
|
5263
5459
|
const [search, setSearch] = useState('');
|
|
5264
5460
|
const [systemData, setSystemData] = useState(null);
|
|
5265
5461
|
// Canvas-compile skeleton (single-canvas model → one path at a time).
|
|
@@ -5356,6 +5552,20 @@ function App() {
|
|
|
5356
5552
|
cancelled = true;
|
|
5357
5553
|
};
|
|
5358
5554
|
}, []);
|
|
5555
|
+
// Phase 27 (E2) — seed the git dirty-state on mount; live updates arrive over
|
|
5556
|
+
// the `git-status` WS broadcast (Task 5). Solo/non-git projects → repo:false.
|
|
5557
|
+
useEffect(() => {
|
|
5558
|
+
let cancelled = false;
|
|
5559
|
+
fetch('/_api/git/status')
|
|
5560
|
+
.then((r) => r.json())
|
|
5561
|
+
.then((data) => {
|
|
5562
|
+
if (!cancelled && data) setGitStatus(data);
|
|
5563
|
+
})
|
|
5564
|
+
.catch(() => {});
|
|
5565
|
+
return () => {
|
|
5566
|
+
cancelled = true;
|
|
5567
|
+
};
|
|
5568
|
+
}, []);
|
|
5359
5569
|
const [commentsByFile, setCommentsByFile] = useState({}); // { file: [Comment] }
|
|
5360
5570
|
// Phase 6 — the in-iframe composer owns drafting; the shell no longer holds
|
|
5361
5571
|
// a `draft` state. Mutations route through postMessage → WS instead.
|
|
@@ -5376,10 +5586,144 @@ function App() {
|
|
|
5376
5586
|
// The palette (T4) drives them; the dialog (T5) + panel (T6) consume them.
|
|
5377
5587
|
const [exportDialog, setExportDialog] = useState(null); // null | { mode: 'export'|'handoff', scope? }
|
|
5378
5588
|
const [inspectorOpen, setInspectorOpen] = useState(false);
|
|
5589
|
+
// Phase 31 (DDR-123) — the native ACP chat sidepanel (right dock, native-only).
|
|
5590
|
+
const [assistantOpen, setAssistantOpen] = useState(false);
|
|
5591
|
+
const [assistantBusy, setAssistantBusy] = useState(false);
|
|
5592
|
+
const [assistantUnseen, setAssistantUnseen] = useState(false);
|
|
5593
|
+
const assistantOpenRef = useRef(assistantOpen);
|
|
5594
|
+
useEffect(() => {
|
|
5595
|
+
assistantOpenRef.current = assistantOpen;
|
|
5596
|
+
if (assistantOpen) {
|
|
5597
|
+
setAssistantUnseen(false); // opening clears the unseen badge
|
|
5598
|
+
// Ask for notification permission on a real user gesture (panel open).
|
|
5599
|
+
try {
|
|
5600
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
|
5601
|
+
Notification.requestPermission();
|
|
5602
|
+
}
|
|
5603
|
+
} catch {
|
|
5604
|
+
/* notifications unavailable */
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
}, [assistantOpen]);
|
|
5608
|
+
// ChatPanel owns one connection PER CHAT (so chats run in parallel in the
|
|
5609
|
+
// background) and reports up here: `onBusyChange` for the menubar pulse, and
|
|
5610
|
+
// `onFinished` when any chat's turn ends — badge + notify if you weren't looking.
|
|
5611
|
+
const handleAssistantFinished = useCallback(() => {
|
|
5612
|
+
if (!assistantOpenRef.current || document.hidden) {
|
|
5613
|
+
setAssistantUnseen(true);
|
|
5614
|
+
try {
|
|
5615
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'granted') {
|
|
5616
|
+
new Notification('Claude finished', { body: 'Your assistant turn is ready in Maude.' });
|
|
5617
|
+
}
|
|
5618
|
+
} catch {
|
|
5619
|
+
/* best-effort — the in-app badge is the reliable signal */
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
}, []);
|
|
5379
5623
|
// Inspector tab is lifted so View ▸ Layers can open the panel ON the Layers
|
|
5380
5624
|
// tab (the menu item sat disabled as "Phase 12" long after the tab shipped).
|
|
5381
5625
|
const [inspectorTab, setInspectorTab] = useState('inspect');
|
|
5626
|
+
// The right dock holds exactly ONE panel (Changes / Inspector / Comments) at
|
|
5627
|
+
// a time — opening any panel REPLACES whatever was there. These two helpers
|
|
5628
|
+
// are the single source of that invariant; every open/toggle path routes
|
|
5629
|
+
// through them. (Before, the three booleans were flipped independently across
|
|
5630
|
+
// ~13 call sites and only some closed their siblings, so a panel opened via a
|
|
5631
|
+
// path that left a sibling `true` rendered *behind* it under the fixed
|
|
5632
|
+
// precedence — looking like the new panel "overlapped" the old one.)
|
|
5633
|
+
const openRightPanel = useCallback((which) => {
|
|
5634
|
+
setChangesOpen(which === 'changes');
|
|
5635
|
+
setInspectorOpen(which === 'inspector');
|
|
5636
|
+
setCommentsPanelOpen(which === 'comments');
|
|
5637
|
+
setAssistantOpen(which === 'assistant');
|
|
5638
|
+
}, []);
|
|
5639
|
+
// Functional updates so this is stale-closure-safe inside the keydown /
|
|
5640
|
+
// postMessage listeners; opening always clears the sibling panels.
|
|
5641
|
+
const toggleRightPanel = useCallback((which) => {
|
|
5642
|
+
if (which === 'inspector') {
|
|
5643
|
+
setInspectorOpen((v) => {
|
|
5644
|
+
if (!v) {
|
|
5645
|
+
setChangesOpen(false);
|
|
5646
|
+
setCommentsPanelOpen(false);
|
|
5647
|
+
setAssistantOpen(false);
|
|
5648
|
+
}
|
|
5649
|
+
return !v;
|
|
5650
|
+
});
|
|
5651
|
+
} else if (which === 'comments') {
|
|
5652
|
+
setCommentsPanelOpen((v) => {
|
|
5653
|
+
if (!v) {
|
|
5654
|
+
setChangesOpen(false);
|
|
5655
|
+
setInspectorOpen(false);
|
|
5656
|
+
setAssistantOpen(false);
|
|
5657
|
+
}
|
|
5658
|
+
return !v;
|
|
5659
|
+
});
|
|
5660
|
+
} else if (which === 'changes') {
|
|
5661
|
+
setChangesOpen((v) => {
|
|
5662
|
+
if (!v) {
|
|
5663
|
+
setInspectorOpen(false);
|
|
5664
|
+
setCommentsPanelOpen(false);
|
|
5665
|
+
setAssistantOpen(false);
|
|
5666
|
+
}
|
|
5667
|
+
return !v;
|
|
5668
|
+
});
|
|
5669
|
+
} else if (which === 'assistant') {
|
|
5670
|
+
setAssistantOpen((v) => {
|
|
5671
|
+
if (!v) {
|
|
5672
|
+
setChangesOpen(false);
|
|
5673
|
+
setInspectorOpen(false);
|
|
5674
|
+
setCommentsPanelOpen(false);
|
|
5675
|
+
}
|
|
5676
|
+
return !v;
|
|
5677
|
+
});
|
|
5678
|
+
}
|
|
5679
|
+
}, []);
|
|
5382
5680
|
const whatsNew = useWhatsNew(MDCC_VERSION);
|
|
5681
|
+
// Phase 29 (E4) — first-run onboarding wizard. The native shell boots a minimal
|
|
5682
|
+
// "welcome" project on first launch; we ask it whether this is a first run and, if
|
|
5683
|
+
// so, show the wizard OVER the (empty) canvas browser. Completing any door switches
|
|
5684
|
+
// the sidecar to a real project (the webview reloads → first-run is then false).
|
|
5685
|
+
const [firstRun, setFirstRun] = useState(false);
|
|
5686
|
+
// Offer the collab "rychlý kurz" once, AFTER onboarding (native app, not first run,
|
|
5687
|
+
// not yet seen). A returning user who already took it isn't re-nudged.
|
|
5688
|
+
const [collabNudge, setCollabNudge] = useState(false);
|
|
5689
|
+
useEffect(() => {
|
|
5690
|
+
if (!isNativeApp()) return undefined;
|
|
5691
|
+
let alive = true;
|
|
5692
|
+
appIsFirstRun()
|
|
5693
|
+
.then((v) => {
|
|
5694
|
+
if (!alive) return;
|
|
5695
|
+
setFirstRun(!!v);
|
|
5696
|
+
if (!v && !readBoolStore(COLLAB_TOUR_STORE, false)) setCollabNudge(true);
|
|
5697
|
+
})
|
|
5698
|
+
.catch(() => {});
|
|
5699
|
+
return () => {
|
|
5700
|
+
alive = false;
|
|
5701
|
+
};
|
|
5702
|
+
}, []);
|
|
5703
|
+
const markCollabSeen = useCallback(() => {
|
|
5704
|
+
setCollabNudge(false);
|
|
5705
|
+
try {
|
|
5706
|
+
localStorage.setItem(COLLAB_TOUR_STORE, '1');
|
|
5707
|
+
} catch {}
|
|
5708
|
+
}, []);
|
|
5709
|
+
// Phase 32 (Task 1) — auto-update. The native shell downloads + stages a newer
|
|
5710
|
+
// build in the background and emits `update-ready`; we surface a non-blocking
|
|
5711
|
+
// banner. Native-only (the web studio is updated by its own deploy).
|
|
5712
|
+
const [updateReady, setUpdateReady] = useState(null);
|
|
5713
|
+
useEffect(() => {
|
|
5714
|
+
if (!isNativeApp()) return undefined;
|
|
5715
|
+
let un;
|
|
5716
|
+
onUpdateReady((p) => setUpdateReady(p && typeof p === 'object' ? p : {}))
|
|
5717
|
+
.then((fn) => {
|
|
5718
|
+
un = fn;
|
|
5719
|
+
})
|
|
5720
|
+
.catch(() => {});
|
|
5721
|
+
return () => {
|
|
5722
|
+
try {
|
|
5723
|
+
un?.();
|
|
5724
|
+
} catch {}
|
|
5725
|
+
};
|
|
5726
|
+
}, []);
|
|
5383
5727
|
const [tourSteps, setTourSteps] = useState(null);
|
|
5384
5728
|
const [usageNudge, setUsageNudge] = useState(() => !readBoolStore(USAGE_TOUR_STORE, false));
|
|
5385
5729
|
const startTour = useCallback((steps) => {
|
|
@@ -5401,8 +5745,11 @@ function App() {
|
|
|
5401
5745
|
} catch {}
|
|
5402
5746
|
}, 80);
|
|
5403
5747
|
}
|
|
5404
|
-
if (step.inspector || step.tab || step.requireSelection)
|
|
5748
|
+
if (step.inspector || step.tab || step.requireSelection) openRightPanel('inspector');
|
|
5405
5749
|
if (step.tab) setInspectorTab(step.tab);
|
|
5750
|
+
// Phase 29 (E4) collab tour — open the Changes panel so the Save / Publish /
|
|
5751
|
+
// Get-latest controls the action steps spotlight actually exist to anchor on.
|
|
5752
|
+
if (step.changes) openRightPanel('changes');
|
|
5406
5753
|
},
|
|
5407
5754
|
};
|
|
5408
5755
|
const markUsageSeen = useCallback(() => {
|
|
@@ -5412,6 +5759,13 @@ function App() {
|
|
|
5412
5759
|
} catch {}
|
|
5413
5760
|
}, []);
|
|
5414
5761
|
const [annotationsVisible, setAnnotationsVisible] = useState(true);
|
|
5762
|
+
// Canvas-chrome visibility (View menu). minimap + zoom-controls are
|
|
5763
|
+
// persistent prefs broadcast to every open canvas iframe; presentMode is a
|
|
5764
|
+
// non-destructive "hide ALL chrome + shell, artboards only" overlay with an
|
|
5765
|
+
// Esc / floating-pill escape hatch back to the chrome.
|
|
5766
|
+
const [minimapVisible, setMinimapVisible] = useState(true);
|
|
5767
|
+
const [zoomCtlVisible, setZoomCtlVisible] = useState(true);
|
|
5768
|
+
const [presentMode, setPresentMode] = useState(false);
|
|
5415
5769
|
// P2/P3 (Plan C) — top-bar live state. (Zoom lives in the canvas toolbar pill,
|
|
5416
5770
|
// so the top bar no longer mirrors it.)
|
|
5417
5771
|
// activeArtboards — real artboard count of the open canvas, read from its
|
|
@@ -5454,6 +5808,42 @@ function App() {
|
|
|
5454
5808
|
});
|
|
5455
5809
|
}, [activePath]);
|
|
5456
5810
|
|
|
5811
|
+
// Chrome visibility (minimap / zoom-controls / Presentation Mode) applies to
|
|
5812
|
+
// EVERY open canvas iframe, not just the active one — broadcast to all. A
|
|
5813
|
+
// freshly-loaded iframe is seeded from the dgn:'loaded' handler below.
|
|
5814
|
+
const broadcastChrome = useCallback((patch) => {
|
|
5815
|
+
for (const el of iframesRef.current.values()) {
|
|
5816
|
+
try {
|
|
5817
|
+
el.contentWindow.postMessage({ dgn: 'view-chrome', ...patch }, '*');
|
|
5818
|
+
} catch {}
|
|
5819
|
+
}
|
|
5820
|
+
}, []);
|
|
5821
|
+
const toggleMinimap = useCallback(() => {
|
|
5822
|
+
setMinimapVisible((v) => {
|
|
5823
|
+
const next = !v;
|
|
5824
|
+
broadcastChrome({ minimap: next });
|
|
5825
|
+
return next;
|
|
5826
|
+
});
|
|
5827
|
+
}, [broadcastChrome]);
|
|
5828
|
+
const toggleZoomCtl = useCallback(() => {
|
|
5829
|
+
setZoomCtlVisible((v) => {
|
|
5830
|
+
const next = !v;
|
|
5831
|
+
broadcastChrome({ zoom: next });
|
|
5832
|
+
return next;
|
|
5833
|
+
});
|
|
5834
|
+
}, [broadcastChrome]);
|
|
5835
|
+
const togglePresent = useCallback(() => {
|
|
5836
|
+
setPresentMode((v) => {
|
|
5837
|
+
const next = !v;
|
|
5838
|
+
broadcastChrome({ present: next });
|
|
5839
|
+
return next;
|
|
5840
|
+
});
|
|
5841
|
+
}, [broadcastChrome]);
|
|
5842
|
+
const exitPresent = useCallback(() => {
|
|
5843
|
+
setPresentMode(false);
|
|
5844
|
+
broadcastChrome({ present: false });
|
|
5845
|
+
}, [broadcastChrome]);
|
|
5846
|
+
|
|
5457
5847
|
// P3 (Plan C) — local git user for the menubar presence avatar. One-shot.
|
|
5458
5848
|
useEffect(() => {
|
|
5459
5849
|
let cancelled = false;
|
|
@@ -5657,6 +6047,20 @@ function App() {
|
|
|
5657
6047
|
} else if (m.type === 'sync:status' && m.payload) {
|
|
5658
6048
|
// Phase 9 Task 8 — hub connection state for the offline banner.
|
|
5659
6049
|
setSyncStatus(m.payload);
|
|
6050
|
+
} else if (m.type === 'canvas-list-update') {
|
|
6051
|
+
// Phase 30 — a canvas was created/deleted on THIS dev-server; re-read
|
|
6052
|
+
// the branch-scoped tree so other open tabs reflect it without a
|
|
6053
|
+
// reload. Cross-machine peers get a new canvas via git "Get latest".
|
|
6054
|
+
loadTree();
|
|
6055
|
+
} else if (m.type === 'acp-focus') {
|
|
6056
|
+
// Phase 31 (DDR-123) — `/design:chat` from the terminal asked us to
|
|
6057
|
+
// surface the native ACP chat sidepanel. Native-only (the panel
|
|
6058
|
+
// doesn't exist on the web surface).
|
|
6059
|
+
if (isNativeApp()) openRightPanel('assistant');
|
|
6060
|
+
} else if (m.type === 'git-status' && m.payload) {
|
|
6061
|
+
// Phase 27 (E2) Task 5 — live dirty-state. Updates the Changes-panel
|
|
6062
|
+
// count + tree M/A/D badges reactively, no polling.
|
|
6063
|
+
setGitStatus(m.payload);
|
|
5660
6064
|
} else if (m.type === 'git-lifecycle' && m.payload) {
|
|
5661
6065
|
// Phase 8 Task 7 — branch switch / pull mid-session. Server has
|
|
5662
6066
|
// already flushed every dirty Y.Doc to JSON; just prompt the user.
|
|
@@ -5675,7 +6079,9 @@ function App() {
|
|
|
5675
6079
|
}
|
|
5676
6080
|
connect();
|
|
5677
6081
|
return () => wsRef.current && wsRef.current.close();
|
|
5678
|
-
|
|
6082
|
+
// loadTree is a stable useCallback([]); listed so the canvas-list-update
|
|
6083
|
+
// handler always calls the live reference.
|
|
6084
|
+
}, [loadTree]);
|
|
5679
6085
|
|
|
5680
6086
|
function wsSend(obj) {
|
|
5681
6087
|
const ws = wsRef.current;
|
|
@@ -5684,6 +6090,144 @@ function App() {
|
|
|
5684
6090
|
} catch {}
|
|
5685
6091
|
}
|
|
5686
6092
|
|
|
6093
|
+
// ----- Phase 27 (E2) — git actions -----
|
|
6094
|
+
// All write actions POST same-origin (the dev-server's sameOriginWrite + the
|
|
6095
|
+
// dual-allowlist gate them main-origin only). After a mutation we refresh
|
|
6096
|
+
// status optimistically; the `git-status` WS broadcast also lands shortly.
|
|
6097
|
+
const refreshGitStatus = useCallback(async () => {
|
|
6098
|
+
try {
|
|
6099
|
+
const r = await fetch('/_api/git/status');
|
|
6100
|
+
if (r.ok) setGitStatus(await r.json());
|
|
6101
|
+
} catch {}
|
|
6102
|
+
}, []);
|
|
6103
|
+
|
|
6104
|
+
// Phase 28 (E3) — probe the tracking remote so the Changes panel can surface
|
|
6105
|
+
// the "Get latest" nudge (GitPanel reads `status.remoteAhead` / `status.behind`).
|
|
6106
|
+
// `?remote=1` is what makes the server do the `git fetch` + ahead/behind count;
|
|
6107
|
+
// without it the status is local-only and the nudge never fires. Network call —
|
|
6108
|
+
// call sparingly (mount / interval / post-action), never on the WS hot path.
|
|
6109
|
+
const refreshRemoteSync = useCallback(async () => {
|
|
6110
|
+
try {
|
|
6111
|
+
const r = await fetch('/_api/git/status?remote=1');
|
|
6112
|
+
if (!r.ok) return;
|
|
6113
|
+
const data = await r.json();
|
|
6114
|
+
if (data && data.repo !== false)
|
|
6115
|
+
setRemoteSync({ remoteAhead: !!data.remoteAhead, behind: data.behind || 0 });
|
|
6116
|
+
} catch {}
|
|
6117
|
+
}, []);
|
|
6118
|
+
|
|
6119
|
+
const gitPostJson = useCallback(async (path, body) => {
|
|
6120
|
+
try {
|
|
6121
|
+
const r = await fetch(path, {
|
|
6122
|
+
method: 'POST',
|
|
6123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6124
|
+
body: JSON.stringify(body || {}),
|
|
6125
|
+
});
|
|
6126
|
+
const data = await r.json().catch(() => ({}));
|
|
6127
|
+
return { ok: r.ok, ...data };
|
|
6128
|
+
} catch (e) {
|
|
6129
|
+
return { ok: false, error: 'Network error — is the project still open?' };
|
|
6130
|
+
}
|
|
6131
|
+
}, []);
|
|
6132
|
+
|
|
6133
|
+
const gitCommit = useCallback(
|
|
6134
|
+
async (message, files) => {
|
|
6135
|
+
const res = await gitPostJson('/_api/git/commit', { message, files });
|
|
6136
|
+
if (res.ok) await refreshGitStatus();
|
|
6137
|
+
return res;
|
|
6138
|
+
},
|
|
6139
|
+
[gitPostJson, refreshGitStatus]
|
|
6140
|
+
);
|
|
6141
|
+
|
|
6142
|
+
const gitDiscard = useCallback(
|
|
6143
|
+
async (files) => {
|
|
6144
|
+
const res = await gitPostJson('/_api/git/discard', { files });
|
|
6145
|
+
if (res.ok) await refreshGitStatus();
|
|
6146
|
+
return res;
|
|
6147
|
+
},
|
|
6148
|
+
[gitPostJson, refreshGitStatus]
|
|
6149
|
+
);
|
|
6150
|
+
|
|
6151
|
+
const gitPublish = useCallback(async () => {
|
|
6152
|
+
const res = await gitPostJson('/_api/git/push', {});
|
|
6153
|
+
// Refresh so the "N versions ready to publish" count clears to 0 after a
|
|
6154
|
+
// successful push (the server advanced the local remote-tracking ref), and
|
|
6155
|
+
// re-probe the remote so a stale "Get latest" nudge clears.
|
|
6156
|
+
if (res.ok) {
|
|
6157
|
+
await refreshGitStatus();
|
|
6158
|
+
refreshRemoteSync();
|
|
6159
|
+
}
|
|
6160
|
+
return res;
|
|
6161
|
+
}, [gitPostJson, refreshGitStatus, refreshRemoteSync]);
|
|
6162
|
+
|
|
6163
|
+
const gitGetLatest = useCallback(async () => {
|
|
6164
|
+
const res = await gitPostJson('/_api/git/pull', {});
|
|
6165
|
+
// On success the remote is merged in — clear the nudge by re-probing.
|
|
6166
|
+
if (res.ok) {
|
|
6167
|
+
await refreshGitStatus();
|
|
6168
|
+
refreshRemoteSync();
|
|
6169
|
+
}
|
|
6170
|
+
// A true content conflict → open the visual resolver on the first file.
|
|
6171
|
+
if (res.conflict && Array.isArray(res.files) && res.files.length) {
|
|
6172
|
+
setDiffTarget({ file: res.files[0], conflict: true });
|
|
6173
|
+
}
|
|
6174
|
+
return res;
|
|
6175
|
+
}, [gitPostJson, refreshGitStatus, refreshRemoteSync]);
|
|
6176
|
+
|
|
6177
|
+
// Phase 28 (E3) — finish a Get-latest conflict from the DiffView resolver.
|
|
6178
|
+
// `choice` is 'mine' | 'theirs' | 'both'; the server completes the two-parent
|
|
6179
|
+
// merge commit (and, for 'both', writes our version as a "(mine)" copy).
|
|
6180
|
+
const gitResolveConflict = useCallback(
|
|
6181
|
+
async (choice) => {
|
|
6182
|
+
const res = await gitPostJson('/_api/git/resolve', { choice });
|
|
6183
|
+
if (res.ok) {
|
|
6184
|
+
await refreshGitStatus();
|
|
6185
|
+
refreshRemoteSync();
|
|
6186
|
+
}
|
|
6187
|
+
return res;
|
|
6188
|
+
},
|
|
6189
|
+
[gitPostJson, refreshGitStatus, refreshRemoteSync]
|
|
6190
|
+
);
|
|
6191
|
+
|
|
6192
|
+
// `path` (optional) scopes History to one canvas — the per-file version list
|
|
6193
|
+
// behind the History click-to-preview + DiffView "Saved version" picker
|
|
6194
|
+
// (phase-27.1). Omit for the repo-wide log.
|
|
6195
|
+
const gitLoadLog = useCallback(async (path) => {
|
|
6196
|
+
try {
|
|
6197
|
+
const qs = '/_api/git/log?limit=40' + (path ? `&path=${encodeURIComponent(path)}` : '');
|
|
6198
|
+
const r = await fetch(qs);
|
|
6199
|
+
if (!r.ok) return [];
|
|
6200
|
+
const data = await r.json();
|
|
6201
|
+
return data.entries || [];
|
|
6202
|
+
} catch {
|
|
6203
|
+
return [];
|
|
6204
|
+
}
|
|
6205
|
+
}, []);
|
|
6206
|
+
|
|
6207
|
+
// Repo-relative path → M/A/D/U badge for the tree (paths match: both the tree
|
|
6208
|
+
// and gitStatus use `.design/ui/Foo.tsx`). Keyed off gitStatus so it updates
|
|
6209
|
+
// live with the WS broadcast.
|
|
6210
|
+
const dirtyByPath = useMemo(() => {
|
|
6211
|
+
const KIND = { modified: 'M', added: 'A', deleted: 'D', untracked: 'U' };
|
|
6212
|
+
const m = new Map();
|
|
6213
|
+
for (const f of gitStatus?.files || []) m.set(f.path, KIND[f.status]);
|
|
6214
|
+
return m;
|
|
6215
|
+
}, [gitStatus]);
|
|
6216
|
+
const unsavedCount = gitStatus?.files?.length || 0;
|
|
6217
|
+
|
|
6218
|
+
// Phase 28 (E3) — keep remote ahead/behind fresh so the "Get latest" nudge
|
|
6219
|
+
// surfaces on its own: probe once a repo is known, again whenever the Changes
|
|
6220
|
+
// panel opens, and on a slow 60 s tick WHILE the panel is open (a teammate's
|
|
6221
|
+
// publish then shows up without the user first attempting their own publish).
|
|
6222
|
+
// Declared after `refreshRemoteSync` to avoid a temporal-dead-zone on the dep.
|
|
6223
|
+
useEffect(() => {
|
|
6224
|
+
if (gitStatus?.repo === false) return; // solo / non-git project — no remote
|
|
6225
|
+
refreshRemoteSync();
|
|
6226
|
+
if (!changesOpen) return; // only keep polling while the panel is visible
|
|
6227
|
+
const id = setInterval(refreshRemoteSync, 60000);
|
|
6228
|
+
return () => clearInterval(id);
|
|
6229
|
+
}, [gitStatus?.repo, changesOpen, refreshRemoteSync]);
|
|
6230
|
+
|
|
5687
6231
|
// ----- Tab management (single-canvas) -----
|
|
5688
6232
|
// Single-canvas model: opening a file REPLACES the active one (no tab strip).
|
|
5689
6233
|
// The `tabs` state stays as a 0-or-1 array so the rest of the plumbing
|
|
@@ -5825,15 +6369,17 @@ function App() {
|
|
|
5825
6369
|
}, [activePath]);
|
|
5826
6370
|
|
|
5827
6371
|
// ----- Push comments to iframe whenever they change for active file -----
|
|
6372
|
+
// Presentation Mode hides comment pins: post an empty list while present
|
|
6373
|
+
// (re-posting the real list on exit, since this effect re-runs on the flag).
|
|
5828
6374
|
useEffect(() => {
|
|
5829
6375
|
if (!activePath || activePath === SYSTEM_TAB) return;
|
|
5830
6376
|
const el = iframesRef.current.get(activePath);
|
|
5831
6377
|
if (!el || !el.contentWindow) return;
|
|
5832
|
-
const list = commentsByFile[activePath] || [];
|
|
6378
|
+
const list = presentMode ? [] : commentsByFile[activePath] || [];
|
|
5833
6379
|
try {
|
|
5834
6380
|
el.contentWindow.postMessage({ dgn: 'comments-set', comments: list }, '*');
|
|
5835
6381
|
} catch {}
|
|
5836
|
-
}, [activePath, commentsByFile]);
|
|
6382
|
+
}, [activePath, commentsByFile, presentMode]);
|
|
5837
6383
|
|
|
5838
6384
|
// ----- Inbound messages from iframes -----
|
|
5839
6385
|
useEffect(() => {
|
|
@@ -5917,7 +6463,25 @@ function App() {
|
|
|
5917
6463
|
setLayersTree({ artboardId: m.artboardId, nodes: Array.isArray(m.tree) ? m.tree : [] });
|
|
5918
6464
|
} else if (m.dgn === 'open-inspector') {
|
|
5919
6465
|
// Phase 12 — context-menu "Inspect" / tool-palette Inspect opens the right panel.
|
|
5920
|
-
|
|
6466
|
+
openRightPanel('inspector');
|
|
6467
|
+
} else if (m.dgn === 'present-enter') {
|
|
6468
|
+
// Canvas tool-palette "Presentation mode" button — Present Mode is a
|
|
6469
|
+
// shell-level state (hides the menubar / sidebar / panels), so the
|
|
6470
|
+
// canvas requests it here and the shell flips it on + broadcasts
|
|
6471
|
+
// dgn:'view-chrome' back to every iframe. Enter-only (the palette is
|
|
6472
|
+
// hidden while presenting); exit is Esc or the floating pill. The
|
|
6473
|
+
// inbound origin gate above (DDR-054) already authenticates the canvas.
|
|
6474
|
+
// Hardening (phase-28 audit F-2): honor it ONLY from the ACTIVE canvas
|
|
6475
|
+
// (a background tab's untrusted canvas must not flip the foreground),
|
|
6476
|
+
// and NEVER while a modal dialog is open — present mode hides Sidebar-
|
|
6477
|
+
// descendant modals (OAuth device-code / Share-invite), so an untrusted
|
|
6478
|
+
// canvas could otherwise blank an in-flight confirmation.
|
|
6479
|
+
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
6480
|
+
const modalOpen = !!document.querySelector('[role="dialog"][aria-modal="true"]');
|
|
6481
|
+
if (e.source === activeWin && !modalOpen && !presentMode) {
|
|
6482
|
+
setPresentMode(true);
|
|
6483
|
+
broadcastChrome({ present: true });
|
|
6484
|
+
}
|
|
5921
6485
|
} else if (m.dgn === 'comment-compose' && m.selection) {
|
|
5922
6486
|
// Phase 6 — the iframe overlay owns the composer surface now. The
|
|
5923
6487
|
// shell just mirrors `selected` so the StatusBar / sidebar still
|
|
@@ -5970,8 +6534,9 @@ function App() {
|
|
|
5970
6534
|
// Same forwarding lane for the other shell chords (inspect.ts) — so
|
|
5971
6535
|
// ⌘R / ⌘⇧I / ⌘⇧M / ⌘⇧E / ⌘⇧H behave identically wherever focus is.
|
|
5972
6536
|
if (m.id === 'reload') reloadActive();
|
|
5973
|
-
else if (m.id === 'inspector')
|
|
5974
|
-
else if (m.id === '
|
|
6537
|
+
else if (m.id === 'inspector') toggleRightPanel('inspector');
|
|
6538
|
+
else if (m.id === 'assistant' && isNativeApp()) toggleRightPanel('assistant');
|
|
6539
|
+
else if (m.id === 'comments') toggleRightPanel('comments');
|
|
5975
6540
|
else if (m.id === 'export') setExportDialog({ mode: 'export' });
|
|
5976
6541
|
else if (m.id === 'handoff') setExportDialog({ mode: 'handoff' });
|
|
5977
6542
|
} else if (m.dgn === 'open-export') {
|
|
@@ -5986,7 +6551,9 @@ function App() {
|
|
|
5986
6551
|
// iframe finished loading — drop the compile skeleton, push current
|
|
5987
6552
|
// comments + carry over focused pin if any
|
|
5988
6553
|
setLoadingPath((p) => (p === m.file ? null : p));
|
|
5989
|
-
|
|
6554
|
+
// Presentation Mode suppresses comment pins (same gate as the push
|
|
6555
|
+
// effect above), so a canvas opened while presenting starts pin-free.
|
|
6556
|
+
const list = presentMode ? [] : commentsByFile[m.file] || [];
|
|
5990
6557
|
const el = [...iframesRef.current.entries()].find(([k]) => k === m.file)?.[1];
|
|
5991
6558
|
if (el && el.contentWindow) {
|
|
5992
6559
|
try {
|
|
@@ -5998,6 +6565,15 @@ function App() {
|
|
|
5998
6565
|
try {
|
|
5999
6566
|
el.contentWindow.postMessage({ dgn: 'theme', theme }, '*');
|
|
6000
6567
|
} catch {}
|
|
6568
|
+
// Seed the just-loaded canvas with the current chrome-visibility
|
|
6569
|
+
// state (minimap / zoom-controls toggles + Presentation Mode) so a
|
|
6570
|
+
// canvas opened after a toggle starts in the right state.
|
|
6571
|
+
try {
|
|
6572
|
+
el.contentWindow.postMessage(
|
|
6573
|
+
{ dgn: 'view-chrome', minimap: minimapVisible, zoom: zoomCtlVisible, present: presentMode },
|
|
6574
|
+
'*'
|
|
6575
|
+
);
|
|
6576
|
+
} catch {}
|
|
6001
6577
|
if (focusedCommentId && list.some((c) => c.id === focusedCommentId)) {
|
|
6002
6578
|
try {
|
|
6003
6579
|
el.contentWindow.postMessage({ dgn: 'comment-focus', id: focusedCommentId }, '*');
|
|
@@ -6092,7 +6668,18 @@ function App() {
|
|
|
6092
6668
|
}
|
|
6093
6669
|
window.addEventListener('message', onMessage);
|
|
6094
6670
|
return () => window.removeEventListener('message', onMessage);
|
|
6095
|
-
}, [
|
|
6671
|
+
}, [
|
|
6672
|
+
commentsByFile,
|
|
6673
|
+
focusedCommentId,
|
|
6674
|
+
cfg,
|
|
6675
|
+
theme,
|
|
6676
|
+
reloadActive,
|
|
6677
|
+
presentMode,
|
|
6678
|
+
minimapVisible,
|
|
6679
|
+
zoomCtlVisible,
|
|
6680
|
+
broadcastChrome,
|
|
6681
|
+
activePath,
|
|
6682
|
+
]);
|
|
6096
6683
|
|
|
6097
6684
|
// Tell the active canvas iframe to drop any persistent selection (canvas
|
|
6098
6685
|
// SelectionSet) — used when the comment composer closes via submit /
|
|
@@ -6176,6 +6763,16 @@ function App() {
|
|
|
6176
6763
|
// ⌘F) still fire regardless of focus, mirroring browser convention.
|
|
6177
6764
|
const inCanvasIframe = document.activeElement?.tagName === 'IFRAME';
|
|
6178
6765
|
|
|
6766
|
+
// Esc exits Presentation Mode first — it's the primary way back to the
|
|
6767
|
+
// chrome (the menubar is hidden while presenting). Highest priority so it
|
|
6768
|
+
// wins over the focused-pin / deselect Esc handlers below, and fires even
|
|
6769
|
+
// when focus is inside the canvas iframe.
|
|
6770
|
+
if (presentMode && e.key === 'Escape') {
|
|
6771
|
+
e.preventDefault();
|
|
6772
|
+
exitPresent();
|
|
6773
|
+
return;
|
|
6774
|
+
}
|
|
6775
|
+
|
|
6179
6776
|
// Cmd+K / Ctrl+K — toggle the command palette (works even in inputs).
|
|
6180
6777
|
if (meta && (e.key === 'k' || e.key === 'K')) {
|
|
6181
6778
|
e.preventDefault();
|
|
@@ -6191,14 +6788,27 @@ function App() {
|
|
|
6191
6788
|
// Cmd+Shift+M / Ctrl+Shift+M — toggle right "Comments" panel
|
|
6192
6789
|
if (meta && e.shiftKey && (e.key === 'm' || e.key === 'M')) {
|
|
6193
6790
|
e.preventDefault();
|
|
6194
|
-
|
|
6791
|
+
toggleRightPanel('comments');
|
|
6792
|
+
return;
|
|
6793
|
+
}
|
|
6794
|
+
// Cmd+Shift+G — toggle the Changes (git) panel. Opening it closes the
|
|
6795
|
+
// other right-dock panels (one panel at a time).
|
|
6796
|
+
if (meta && e.shiftKey && (e.key === 'g' || e.key === 'G')) {
|
|
6797
|
+
e.preventDefault();
|
|
6798
|
+
toggleRightPanel('changes');
|
|
6195
6799
|
return;
|
|
6196
6800
|
}
|
|
6197
6801
|
// Cmd+Shift+I — toggle Inspector. Was bare "I", which collided with the
|
|
6198
6802
|
// canvas highlighter tool (same letter, different action by focus).
|
|
6199
6803
|
if (meta && e.shiftKey && (e.key === 'i' || e.key === 'I')) {
|
|
6200
6804
|
e.preventDefault();
|
|
6201
|
-
|
|
6805
|
+
toggleRightPanel('inspector');
|
|
6806
|
+
return;
|
|
6807
|
+
}
|
|
6808
|
+
// Phase 31 (DDR-123) — Cmd+Shift+A opens the native ACP chat sidepanel.
|
|
6809
|
+
if (meta && e.shiftKey && (e.key === 'a' || e.key === 'A') && isNativeApp()) {
|
|
6810
|
+
e.preventDefault();
|
|
6811
|
+
toggleRightPanel('assistant');
|
|
6202
6812
|
return;
|
|
6203
6813
|
}
|
|
6204
6814
|
// Cmd+Shift+E / Cmd+Shift+H — the File-menu chords, previously
|
|
@@ -6325,6 +6935,8 @@ function App() {
|
|
|
6325
6935
|
openSystem,
|
|
6326
6936
|
closeTab,
|
|
6327
6937
|
clearActiveCanvasSelection,
|
|
6938
|
+
presentMode,
|
|
6939
|
+
exitPresent,
|
|
6328
6940
|
]);
|
|
6329
6941
|
|
|
6330
6942
|
const registerIframe = useCallback((path, el) => {
|
|
@@ -6399,7 +7011,7 @@ function App() {
|
|
|
6399
7011
|
label: 'Toggle comments panel',
|
|
6400
7012
|
icon: 'resolve',
|
|
6401
7013
|
kbd: '⌘⇧M',
|
|
6402
|
-
run: () =>
|
|
7014
|
+
run: () => toggleRightPanel('comments'),
|
|
6403
7015
|
},
|
|
6404
7016
|
{
|
|
6405
7017
|
id: 'inspector',
|
|
@@ -6407,7 +7019,7 @@ function App() {
|
|
|
6407
7019
|
label: 'Open inspector',
|
|
6408
7020
|
icon: 'sliders',
|
|
6409
7021
|
kbd: '⌘⇧I',
|
|
6410
|
-
run: () =>
|
|
7022
|
+
run: () => openRightPanel('inspector'),
|
|
6411
7023
|
},
|
|
6412
7024
|
{
|
|
6413
7025
|
id: 'reload',
|
|
@@ -6467,7 +7079,13 @@ function App() {
|
|
|
6467
7079
|
);
|
|
6468
7080
|
|
|
6469
7081
|
return (
|
|
6470
|
-
<div
|
|
7082
|
+
<div
|
|
7083
|
+
className={'maude' + (presentMode ? ' is-present' : '')}
|
|
7084
|
+
data-theme={theme}
|
|
7085
|
+
onContextMenu={onShellContextMenu}
|
|
7086
|
+
>
|
|
7087
|
+
{firstRun && <OnboardingWizard />}
|
|
7088
|
+
<UpdateBanner update={updateReady} onDismiss={() => setUpdateReady(null)} />
|
|
6471
7089
|
<SyncBanner status={syncStatus} />
|
|
6472
7090
|
{!usageNudge && !tourSteps && <WhatsNewToast wn={whatsNew} />}
|
|
6473
7091
|
{gitLifecycle && (
|
|
@@ -6498,7 +7116,10 @@ function App() {
|
|
|
6498
7116
|
openMenu={openMenu}
|
|
6499
7117
|
setOpenMenu={setOpenMenu}
|
|
6500
7118
|
commentsPanelOpen={commentsPanelOpen}
|
|
6501
|
-
onToggleComments={() =>
|
|
7119
|
+
onToggleComments={() => toggleRightPanel('comments')}
|
|
7120
|
+
changesOpen={changesOpen}
|
|
7121
|
+
changesCount={unsavedCount}
|
|
7122
|
+
onToggleChanges={() => toggleRightPanel('changes')}
|
|
6502
7123
|
onOpenSystem={openSystem}
|
|
6503
7124
|
sidebarOpen={sidebarOpen}
|
|
6504
7125
|
onToggleSidebar={() => setSidebarOpen((v) => !v)}
|
|
@@ -6507,22 +7128,34 @@ function App() {
|
|
|
6507
7128
|
onOpenHelp={() => setHelpOpen(true)}
|
|
6508
7129
|
onOpenShortcuts={() => setShortcutsOpen(true)}
|
|
6509
7130
|
onStartTour={() => startTour(USAGE_TOUR)}
|
|
7131
|
+
onStartCollabTour={() => startTour(COLLAB_TOUR)}
|
|
6510
7132
|
annotationsVisible={annotationsVisible}
|
|
6511
7133
|
onToggleAnnotations={toggleAnnotations}
|
|
7134
|
+
minimapVisible={minimapVisible}
|
|
7135
|
+
onToggleMinimap={toggleMinimap}
|
|
7136
|
+
zoomCtlVisible={zoomCtlVisible}
|
|
7137
|
+
onToggleZoomCtl={toggleZoomCtl}
|
|
7138
|
+
presentMode={presentMode}
|
|
7139
|
+
onTogglePresent={togglePresent}
|
|
6512
7140
|
postToActiveCanvas={postToActiveCanvas}
|
|
6513
7141
|
onOpenWhatsNew={whatsNew.openPanel}
|
|
6514
7142
|
whatsNewCount={whatsNew.unseen.length}
|
|
6515
7143
|
artboardCount={activeArtboards}
|
|
6516
7144
|
inspectorOpen={inspectorOpen}
|
|
6517
7145
|
inspectorTab={inspectorTab}
|
|
6518
|
-
onToggleInspector={() =>
|
|
7146
|
+
onToggleInspector={() => toggleRightPanel('inspector')}
|
|
7147
|
+
assistantOpen={assistantOpen}
|
|
7148
|
+
onToggleAssistant={() => toggleRightPanel('assistant')}
|
|
7149
|
+
assistantBusy={assistantBusy}
|
|
7150
|
+
assistantUnseen={assistantUnseen}
|
|
6519
7151
|
onOpenLayers={() => {
|
|
6520
|
-
// Toggle: already open on Layers → close; otherwise open on Layers
|
|
7152
|
+
// Toggle: already open on Layers → close; otherwise open on Layers
|
|
7153
|
+
// (clearing the sibling panels — one dock slot).
|
|
6521
7154
|
if (inspectorOpen && inspectorTab === 'layers') {
|
|
6522
7155
|
setInspectorOpen(false);
|
|
6523
7156
|
} else {
|
|
6524
7157
|
setInspectorTab('layers');
|
|
6525
|
-
|
|
7158
|
+
openRightPanel('inspector');
|
|
6526
7159
|
}
|
|
6527
7160
|
}}
|
|
6528
7161
|
onNewCanvas={() => {
|
|
@@ -6581,6 +7214,9 @@ function App() {
|
|
|
6581
7214
|
onCollapse={() => setSidebarOpen(false)}
|
|
6582
7215
|
width={sbSize.w}
|
|
6583
7216
|
resizing={dragSide === 'sb'}
|
|
7217
|
+
dirtyByPath={dirtyByPath}
|
|
7218
|
+
project={project}
|
|
7219
|
+
gitBranch={gitStatus?.branch}
|
|
6584
7220
|
/>
|
|
6585
7221
|
{sidebarOpen && (
|
|
6586
7222
|
<PanelGrip
|
|
@@ -6608,7 +7244,7 @@ function App() {
|
|
|
6608
7244
|
onIframeLoad={onIframeLoad}
|
|
6609
7245
|
/>
|
|
6610
7246
|
</div>
|
|
6611
|
-
{(inspectorOpen || commentsPanelOpen) && (
|
|
7247
|
+
{(inspectorOpen || commentsPanelOpen || changesOpen || assistantOpen) && (
|
|
6612
7248
|
<PanelGrip
|
|
6613
7249
|
label="Resize side panel"
|
|
6614
7250
|
dir="rtl"
|
|
@@ -6621,9 +7257,36 @@ function App() {
|
|
|
6621
7257
|
}}
|
|
6622
7258
|
/>
|
|
6623
7259
|
)}
|
|
6624
|
-
{/* Right dock — one panel at a time.
|
|
6625
|
-
|
|
6626
|
-
{
|
|
7260
|
+
{/* Right dock — one panel at a time. Changes (E2) takes precedence,
|
|
7261
|
+
then Inspector (T6), then the comments panel. */}
|
|
7262
|
+
{changesOpen ? (
|
|
7263
|
+
<GitPanel
|
|
7264
|
+
status={
|
|
7265
|
+
gitStatus && remoteSync ? { ...gitStatus, ...remoteSync } : gitStatus
|
|
7266
|
+
}
|
|
7267
|
+
project={project}
|
|
7268
|
+
readOnly={!isNativeApp()}
|
|
7269
|
+
width={rpSize.w}
|
|
7270
|
+
resizing={dragSide === 'rp'}
|
|
7271
|
+
onClose={() => setChangesOpen(false)}
|
|
7272
|
+
onCommit={gitCommit}
|
|
7273
|
+
onDiscard={gitDiscard}
|
|
7274
|
+
onPublish={gitPublish}
|
|
7275
|
+
onGetLatest={gitGetLatest}
|
|
7276
|
+
loadLog={gitLoadLog}
|
|
7277
|
+
onOpenCanvas={(p) => openTab(p)}
|
|
7278
|
+
onOpenDiff={(file) => setDiffTarget({ file, beforeSha: 'HEAD', conflict: false })}
|
|
7279
|
+
activeCanvas={
|
|
7280
|
+
activePath && activePath !== SYSTEM_TAB && /\.(tsx|html)$/i.test(activePath)
|
|
7281
|
+
? activePath
|
|
7282
|
+
: null
|
|
7283
|
+
}
|
|
7284
|
+
onPreviewVersion={(sha) =>
|
|
7285
|
+
setDiffTarget({ file: activePath, beforeSha: sha, conflict: false })
|
|
7286
|
+
}
|
|
7287
|
+
designRel={(cfg?.designRel || cfg?.designRoot || '.design').replace(/^\/+|\/+$/g, '')}
|
|
7288
|
+
/>
|
|
7289
|
+
) : inspectorOpen ? (
|
|
6627
7290
|
<InspectorPanel
|
|
6628
7291
|
selected={selected}
|
|
6629
7292
|
cfg={cfg}
|
|
@@ -6666,6 +7329,24 @@ function App() {
|
|
|
6666
7329
|
resizing={dragSide === 'rp'}
|
|
6667
7330
|
/>
|
|
6668
7331
|
) : null}
|
|
7332
|
+
{/* Phase 31 (DDR-123) — the ACP chat panel stays MOUNTED (display:none
|
|
7333
|
+
when inactive) so the chat keeps streaming + its history survives a
|
|
7334
|
+
switch to Changes/Inspector/Comments. Native-only. */}
|
|
7335
|
+
{isNativeApp() && (
|
|
7336
|
+
<ChatPanel
|
|
7337
|
+
hidden={!assistantOpen}
|
|
7338
|
+
activeCanvas={
|
|
7339
|
+
activePath && activePath !== SYSTEM_TAB && /\.(tsx|html)$/i.test(activePath)
|
|
7340
|
+
? activePath
|
|
7341
|
+
: null
|
|
7342
|
+
}
|
|
7343
|
+
width={rpSize.w}
|
|
7344
|
+
resizing={dragSide === 'rp'}
|
|
7345
|
+
onClose={() => setAssistantOpen(false)}
|
|
7346
|
+
onBusyChange={setAssistantBusy}
|
|
7347
|
+
onFinished={handleAssistantFinished}
|
|
7348
|
+
/>
|
|
7349
|
+
)}
|
|
6669
7350
|
</div>
|
|
6670
7351
|
<StatusBar
|
|
6671
7352
|
activePath={activePath}
|
|
@@ -6676,8 +7357,32 @@ function App() {
|
|
|
6676
7357
|
onToggleTheme={toggleTheme}
|
|
6677
7358
|
onClearSelected={clearSelected}
|
|
6678
7359
|
syncStatus={syncStatus}
|
|
7360
|
+
changesCount={unsavedCount}
|
|
7361
|
+
unpushed={gitStatus?.unpushed || 0}
|
|
7362
|
+
changesOpen={changesOpen}
|
|
7363
|
+
onOpenChanges={gitStatus?.repo ? () => setChangesOpen(true) : undefined}
|
|
6679
7364
|
/>
|
|
6680
7365
|
</div>
|
|
7366
|
+
{presentMode && (
|
|
7367
|
+
<button
|
|
7368
|
+
type="button"
|
|
7369
|
+
className="st-present-exit"
|
|
7370
|
+
onClick={exitPresent}
|
|
7371
|
+
aria-label="Exit presentation mode"
|
|
7372
|
+
title="Exit presentation mode (Esc)"
|
|
7373
|
+
>
|
|
7374
|
+
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" aria-hidden="true">
|
|
7375
|
+
<path
|
|
7376
|
+
d="M4 4l8 8M12 4l-8 8"
|
|
7377
|
+
stroke="currentColor"
|
|
7378
|
+
strokeWidth="1.6"
|
|
7379
|
+
strokeLinecap="round"
|
|
7380
|
+
/>
|
|
7381
|
+
</svg>
|
|
7382
|
+
<span>Exit presentation</span>
|
|
7383
|
+
<kbd className="st-present-exit-kbd">Esc</kbd>
|
|
7384
|
+
</button>
|
|
7385
|
+
)}
|
|
6681
7386
|
<CommandPalette
|
|
6682
7387
|
open={paletteOpen}
|
|
6683
7388
|
onClose={() => setPaletteOpen(false)}
|
|
@@ -6691,6 +7396,30 @@ function App() {
|
|
|
6691
7396
|
onClose={() => setExportDialog(null)}
|
|
6692
7397
|
/>
|
|
6693
7398
|
)}
|
|
7399
|
+
{diffTarget && (
|
|
7400
|
+
<DiffView
|
|
7401
|
+
target={diffTarget}
|
|
7402
|
+
cfg={cfg}
|
|
7403
|
+
loadLog={gitLoadLog}
|
|
7404
|
+
onClose={() => setDiffTarget(null)}
|
|
7405
|
+
onRestore={async (file) => {
|
|
7406
|
+
await gitDiscard([file]);
|
|
7407
|
+
setDiffTarget(null);
|
|
7408
|
+
}}
|
|
7409
|
+
onResolve={async (choice) => {
|
|
7410
|
+
// phase-28 (E3): apply the chosen side via /_api/git/resolve, which
|
|
7411
|
+
// completes the two-parent merge commit (and for "both" saves our
|
|
7412
|
+
// version as a "(mine)" copy — zero loss). Close on success; keep the
|
|
7413
|
+
// resolver open with the error otherwise.
|
|
7414
|
+
const res = await gitResolveConflict(choice);
|
|
7415
|
+
if (res.ok) {
|
|
7416
|
+
setDiffTarget(null);
|
|
7417
|
+
} else {
|
|
7418
|
+
window.alert(res.error || 'Could not finish the merge. Get the latest again, then retry.');
|
|
7419
|
+
}
|
|
7420
|
+
}}
|
|
7421
|
+
/>
|
|
7422
|
+
)}
|
|
6694
7423
|
<ShortcutsOverlay open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />
|
|
6695
7424
|
<HelpModal
|
|
6696
7425
|
open={helpOpen}
|
|
@@ -6701,7 +7430,7 @@ function App() {
|
|
|
6701
7430
|
}}
|
|
6702
7431
|
/>
|
|
6703
7432
|
<WhatsNewPanel wn={whatsNew} onStartTour={startTour} />
|
|
6704
|
-
{usageNudge && !tourSteps && (
|
|
7433
|
+
{usageNudge && !tourSteps && !collabNudge && (
|
|
6705
7434
|
<div className="mdcc-tour-nudge" role="status" aria-live="polite">
|
|
6706
7435
|
<div className="mdcc-tour-nudge__body">
|
|
6707
7436
|
New here? Take a 60-second tour of the canvas browser.
|
|
@@ -6726,6 +7455,32 @@ function App() {
|
|
|
6726
7455
|
</button>
|
|
6727
7456
|
</div>
|
|
6728
7457
|
)}
|
|
7458
|
+
{/* Phase 29 (E4) — the collab "rychlý kurz", offered once after onboarding. */}
|
|
7459
|
+
{collabNudge && !tourSteps && (
|
|
7460
|
+
<div className="mdcc-tour-nudge" role="status" aria-live="polite">
|
|
7461
|
+
<div className="mdcc-tour-nudge__body">
|
|
7462
|
+
New to working with a team? See how saving & sharing works — 60 seconds.
|
|
7463
|
+
</div>
|
|
7464
|
+
<button
|
|
7465
|
+
type="button"
|
|
7466
|
+
className="mdcc-tour-nudge__cta"
|
|
7467
|
+
onClick={() => {
|
|
7468
|
+
markCollabSeen();
|
|
7469
|
+
startTour(COLLAB_TOUR);
|
|
7470
|
+
}}
|
|
7471
|
+
>
|
|
7472
|
+
Start
|
|
7473
|
+
</button>
|
|
7474
|
+
<button
|
|
7475
|
+
type="button"
|
|
7476
|
+
className="mdcc-tour-nudge__skip"
|
|
7477
|
+
aria-label="Dismiss"
|
|
7478
|
+
onClick={markCollabSeen}
|
|
7479
|
+
>
|
|
7480
|
+
×
|
|
7481
|
+
</button>
|
|
7482
|
+
</div>
|
|
7483
|
+
)}
|
|
6729
7484
|
<TourOverlay
|
|
6730
7485
|
steps={tourSteps ?? []}
|
|
6731
7486
|
open={!!tourSteps}
|