@1agh/maude 0.55.0 → 0.57.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/apps/studio/acp/bridge.ts +385 -26
- package/apps/studio/acp/index.ts +498 -102
- package/apps/studio/acp/running.ts +97 -0
- package/apps/studio/acp/transcript.ts +64 -0
- package/apps/studio/acp/write-scope.ts +459 -0
- package/apps/studio/bin/_smart-frames.mjs +187 -20
- package/apps/studio/bin/_smart-frames.test.mjs +59 -4
- package/apps/studio/bin/_transcribe.mjs +40 -3
- package/apps/studio/bin/smoke.sh +7 -1
- package/apps/studio/build.ts +28 -1
- package/apps/studio/client/app.jsx +78 -33
- package/apps/studio/client/panels/ChatPanel.jsx +19 -1
- package/apps/studio/client/panels/CloudBar.jsx +104 -6
- package/apps/studio/client/panels/GitPanel.jsx +38 -25
- package/apps/studio/client/panels/PermissionPrompt.jsx +146 -6
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +145 -6
- package/apps/studio/client/panels/SettingsPanel.jsx +239 -27
- package/apps/studio/client/panels/acp-runtime.js +96 -5
- package/apps/studio/client/styles/3-shell-maude.css +11 -2
- package/apps/studio/client/styles/4-components.css +72 -0
- package/apps/studio/client/styles/6-acp-chat.css +51 -0
- package/apps/studio/cloud/endpoints.ts +56 -3
- package/apps/studio/collab/persistence.ts +29 -2
- package/apps/studio/config.schema.json +3 -3
- package/apps/studio/context.ts +41 -0
- package/apps/studio/dist/client.bundle.js +1330 -1330
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/generation/gemma-models.ts +312 -14
- package/apps/studio/generation/prefs.ts +7 -2
- package/apps/studio/generation/runtime-probe.ts +50 -0
- package/apps/studio/generation/whisper-models.ts +124 -0
- package/apps/studio/hmr-broadcast.ts +67 -0
- package/apps/studio/http.ts +247 -111
- package/apps/studio/input-router.tsx +55 -2
- package/apps/studio/server.ts +22 -9
- package/apps/studio/sync/autocommit.ts +61 -2
- package/apps/studio/sync/cell-pairing.ts +174 -0
- package/apps/studio/sync/codec.ts +11 -5
- package/apps/studio/sync/connection-state.ts +116 -6
- package/apps/studio/sync/index.ts +288 -26
- package/apps/studio/sync/limits.ts +49 -0
- package/apps/studio/sync/loopback.ts +21 -0
- package/apps/studio/sync/presentation.ts +273 -0
- package/apps/studio/sync/projection.ts +47 -12
- package/apps/studio/sync/remote-docs.ts +191 -0
- package/apps/studio/sync/status.ts +4 -1
- package/apps/studio/sync/supervisor.ts +178 -0
- package/apps/studio/test/acp-activity-endpoint.test.ts +183 -0
- package/apps/studio/test/acp-branch-guard.test.ts +123 -0
- package/apps/studio/test/acp-bridge-lifetime.test.ts +369 -0
- package/apps/studio/test/acp-caps-bridge.test.ts +5 -0
- package/apps/studio/test/acp-commands.test.ts +5 -0
- package/apps/studio/test/acp-elicitation-bridge.test.ts +5 -0
- package/apps/studio/test/acp-permission-prompt.test.ts +175 -1
- package/apps/studio/test/acp-permission.test.ts +18 -2
- package/apps/studio/test/acp-session-allowed-tools.test.ts +62 -14
- package/apps/studio/test/acp-usage-bridge.test.ts +5 -0
- package/apps/studio/test/acp-write-gate.test.ts +323 -0
- package/apps/studio/test/acp-write-scope.test.ts +533 -0
- package/apps/studio/test/bundle-smoke.test.ts +35 -18
- package/apps/studio/test/canvas-origin-gate.test.ts +7 -0
- package/apps/studio/test/cloud-connect-note.test.ts +135 -0
- package/apps/studio/test/cloud-endpoints.test.ts +102 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +5 -2
- package/apps/studio/test/csrf-write-guard.test.ts +19 -2
- package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +45 -0
- package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +118 -0
- package/apps/studio/test/gemma-models.test.ts +245 -0
- package/apps/studio/test/hmr-broadcast.test.ts +57 -1
- package/apps/studio/test/input-router.test.ts +95 -0
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +639 -0
- package/apps/studio/test/sync-autocommit.test.ts +47 -0
- package/apps/studio/test/sync-connect-honesty.test.ts +114 -0
- package/apps/studio/test/sync-connection-state.test.ts +45 -1
- package/apps/studio/test/sync-presentation.test.ts +185 -0
- package/apps/studio/test/sync-remote-docs.test.ts +171 -0
- package/apps/studio/test/sync-supervisor.test.ts +212 -0
- package/apps/studio/test/trusted-request-host.test.ts +66 -0
- package/apps/studio/test/whisper-setup.test.ts +97 -0
- package/apps/studio/whats-new.json +45 -0
- package/apps/studio/ws.ts +9 -1
- package/cli/commands/kg.mjs +9 -2
- package/package.json +8 -8
- package/plugins/design/dependencies.json +21 -3
|
@@ -21,6 +21,11 @@ import { GRID_KEYWORD_UNITS, parseTrackList, serializeTrackList } from '../grid-
|
|
|
21
21
|
// canvas-cursors.ts above.
|
|
22
22
|
import { PAPER_PRESETS, resolvePrintArtboard } from '../print/units.ts';
|
|
23
23
|
import { sizingModeOf, sizingModePatch } from '../sizing-mode.ts';
|
|
24
|
+
// The single "what is the hub link doing" rule, shared with the cloud rail's
|
|
25
|
+
// connect note so the two surfaces can never disagree. Pure data + strings —
|
|
26
|
+
// same "pull only pure logic into the client bundle" shape as the imports
|
|
27
|
+
// above (a type-only SyncStatusSnapshot import that Bun erases).
|
|
28
|
+
import { syncPresentation } from '../sync/presentation.ts';
|
|
24
29
|
import { canvasUrl } from './canvas-url.js';
|
|
25
30
|
import { TreeRowMenu, useRowMenu } from './tree-row-menu.jsx';
|
|
26
31
|
import { useTreeDrag } from './use-tree-drag.js';
|
|
@@ -77,6 +82,7 @@ const Lu = ({ as: C, size = 14 }) => <C size={size} strokeWidth={1.75} style={{
|
|
|
77
82
|
import { PhotoKnobs } from './photo-knobs.jsx';
|
|
78
83
|
import {
|
|
79
84
|
appIsFirstRun,
|
|
85
|
+
invoke,
|
|
80
86
|
isNativeApp,
|
|
81
87
|
onMenuReportBug,
|
|
82
88
|
onUpdateReady,
|
|
@@ -156,7 +162,7 @@ const LAYERS_MODE_STORE = 'mdcc-layers-mode';
|
|
|
156
162
|
// resizable width + a tab strip over the panels assigned to the slot; renders
|
|
157
163
|
// the active panel (passed as children). Collapses to 0 width when nothing is
|
|
158
164
|
// visibly open (it may still host an always-mounted hidden ChatPanel).
|
|
159
|
-
function DockSlot({ side, width, open, ids, activeId, onPick, children }) {
|
|
165
|
+
function DockSlot({ side, width, open, ids, activeId, onPick, children, labels = null }) {
|
|
160
166
|
return (
|
|
161
167
|
<div
|
|
162
168
|
className={'st-dockslot st-dockslot--' + side + (open ? '' : ' is-collapsed')}
|
|
@@ -173,7 +179,7 @@ function DockSlot({ side, width, open, ids, activeId, onPick, children }) {
|
|
|
173
179
|
className={'st-docktab' + (activeId === id ? ' is-active' : '')}
|
|
174
180
|
onClick={() => onPick(id)}
|
|
175
181
|
>
|
|
176
|
-
{(DOCK_PANELS.find((p) => p.id === id) || {}).label || id}
|
|
182
|
+
{labels?.[id] || (DOCK_PANELS.find((p) => p.id === id) || {}).label || id}
|
|
177
183
|
</button>
|
|
178
184
|
))}
|
|
179
185
|
</div>
|
|
@@ -203,14 +209,30 @@ const MDCC_VERSION = typeof __MDCC_VERSION__ !== 'undefined' ? __MDCC_VERSION__
|
|
|
203
209
|
// handleAssistantAttention below (identical support/permission check +
|
|
204
210
|
// try/catch, only the title/body differ). The in-app badge stays the
|
|
205
211
|
// reliable signal if this silently fails for any reason.
|
|
212
|
+
//
|
|
213
|
+
// feature-acp-turn-notifications Task 6 — under Tauri, route through the
|
|
214
|
+
// native `send_notification` command (notify.rs) instead of the Web `Notification` API.
|
|
215
|
+
// This is the SAME native notifier the shell's cross-project poller uses for
|
|
216
|
+
// every OTHER project, so the visible project now goes through the identical
|
|
217
|
+
// OS-level path rather than a webview API whose behavior inside WKWebView was
|
|
218
|
+
// never empirically confirmed (see Task 1's finding in the plan). Falls back
|
|
219
|
+
// to the Web API on any failure — an older desktop build without the command,
|
|
220
|
+
// or a plain browser tab — so the signal degrades rather than disappearing.
|
|
206
221
|
function notifyDesktop(title, body) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
const webFallback = () => {
|
|
223
|
+
try {
|
|
224
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'granted') {
|
|
225
|
+
new Notification(title, { body });
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
/* best-effort — the in-app badge is the reliable signal */
|
|
210
229
|
}
|
|
211
|
-
}
|
|
212
|
-
|
|
230
|
+
};
|
|
231
|
+
if (isNativeApp()) {
|
|
232
|
+
invoke('send_notification', { title, body }).catch(webFallback);
|
|
233
|
+
return;
|
|
213
234
|
}
|
|
235
|
+
webFallback();
|
|
214
236
|
}
|
|
215
237
|
|
|
216
238
|
function readInitialTheme() {
|
|
@@ -2473,6 +2495,9 @@ function Sidebar({
|
|
|
2473
2495
|
onMoveCanvas,
|
|
2474
2496
|
onNewFolder,
|
|
2475
2497
|
onDeleteFolder,
|
|
2498
|
+
// Passed through to CloudBar only — the live `sync:status` payload that makes
|
|
2499
|
+
// the connect note follow the link instead of freezing at attach time.
|
|
2500
|
+
syncStatus,
|
|
2476
2501
|
}) {
|
|
2477
2502
|
const filteredGroups = useMemo(() => {
|
|
2478
2503
|
if (!search) return groups;
|
|
@@ -2846,7 +2871,7 @@ function Sidebar({
|
|
|
2846
2871
|
Absent rather than disabled, because unlike the agent chat there is
|
|
2847
2872
|
nothing here to explain — the capability is not missing, it is
|
|
2848
2873
|
already satisfied. */}
|
|
2849
|
-
{cloud === null ? <CloudBar /> : null}
|
|
2874
|
+
{cloud === null ? <CloudBar syncStatus={syncStatus} /> : null}
|
|
2850
2875
|
{/* Phase 28 (E3) — GitHub identity as a compact avatar docked at the BOTTOM:
|
|
2851
2876
|
sign in, connected account + New/Pull/Share, sign out. Self-contained
|
|
2852
2877
|
(owns its device-code + CreateProject dialogs). Renders nothing in browser. */}
|
|
@@ -3733,7 +3758,14 @@ function Menubar({
|
|
|
3733
3758
|
{ id: 'tree', label: 'Project Tree', shortcut: 'T', checked: sidebarOpen, disabled: false },
|
|
3734
3759
|
{
|
|
3735
3760
|
id: 'changes',
|
|
3736
|
-
|
|
3761
|
+
// In a cell this panel is History (the hub already committed the work),
|
|
3762
|
+
// so the menu names what it opens rather than an unsaved count there is
|
|
3763
|
+
// no way — and no reason — to act on.
|
|
3764
|
+
label: cloud
|
|
3765
|
+
? 'History'
|
|
3766
|
+
: changesCount > 0
|
|
3767
|
+
? `Changes · ${changesCount} unsaved`
|
|
3768
|
+
: 'Changes',
|
|
3737
3769
|
shortcut: '⌘ ⇧ G',
|
|
3738
3770
|
checked: changesOpen,
|
|
3739
3771
|
disabled: false,
|
|
@@ -4530,6 +4562,7 @@ function StatusBar({
|
|
|
4530
4562
|
onToggleTheme,
|
|
4531
4563
|
onClearSelected,
|
|
4532
4564
|
syncStatus,
|
|
4565
|
+
syncProject,
|
|
4533
4566
|
changesCount = 0,
|
|
4534
4567
|
unpushed = 0,
|
|
4535
4568
|
changesOpen = false,
|
|
@@ -4552,32 +4585,21 @@ function StatusBar({
|
|
|
4552
4585
|
// shapes: solo `{linked:false}` (hide), DDR-060 `{notSyncable,tsxCount,reason}`
|
|
4553
4586
|
// (linked but 0 syncable), or the connection-state machine `{state,queuedOps,
|
|
4554
4587
|
// flash,…}` (the common linked case the old notSyncable-only guard never
|
|
4555
|
-
// showed — the "hub sync se neukazuje" bug).
|
|
4588
|
+
// showed — the "hub sync se neukazuje" bug).
|
|
4589
|
+
//
|
|
4590
|
+
// The mapping used to live here, and it referenced `docs` ZERO times: it read
|
|
4591
|
+
// `state` alone, so a link whose every document the hub had refused still
|
|
4592
|
+
// showed a green dot and the word "synced". `syncPresentation` is now the one
|
|
4593
|
+
// rule, shared with the cloud rail's connect note, so the two surfaces cannot
|
|
4594
|
+
// give a person different answers about the same payload.
|
|
4556
4595
|
const syncSlot = (() => {
|
|
4557
|
-
|
|
4558
|
-
if (
|
|
4559
|
-
|
|
4560
|
-
online: false,
|
|
4561
|
-
label: `0 syncable${syncStatus.tsxCount > 0 ? ` · ${syncStatus.tsxCount} tsx` : ''}`,
|
|
4562
|
-
title: syncStatus.reason || 'Linked to a hub, but no canvases are syncable.',
|
|
4563
|
-
};
|
|
4564
|
-
}
|
|
4565
|
-
const q = syncStatus.queuedOps ?? 0;
|
|
4566
|
-
const synced = syncStatus.state === 'online' || syncStatus.flash === 'synced';
|
|
4567
|
-
if (synced) {
|
|
4568
|
-
return {
|
|
4569
|
-
online: true,
|
|
4570
|
-
label: q > 0 ? `${q} ↑` : 'synced',
|
|
4571
|
-
title: q > 0 ? `${q} edit(s) queued to push` : 'All changes synced to the hub',
|
|
4572
|
-
};
|
|
4573
|
-
}
|
|
4596
|
+
const p = syncPresentation(syncStatus, { project: syncProject });
|
|
4597
|
+
if (!p) return null;
|
|
4598
|
+
const detail = p.names.length ? ` (${p.names.join(', ')})` : '';
|
|
4574
4599
|
return {
|
|
4575
|
-
online:
|
|
4576
|
-
label:
|
|
4577
|
-
title:
|
|
4578
|
-
syncStatus.state === 'connecting'
|
|
4579
|
-
? 'Connecting to the hub…'
|
|
4580
|
-
: 'Offline — edits queued, will sync when the hub reconnects',
|
|
4600
|
+
online: p.online,
|
|
4601
|
+
label: p.label,
|
|
4602
|
+
title: `${p.title}${detail}${p.next ? ` — ${p.next}` : ''}`,
|
|
4581
4603
|
};
|
|
4582
4604
|
})();
|
|
4583
4605
|
|
|
@@ -14019,6 +14041,11 @@ function App() {
|
|
|
14019
14041
|
changes: changesOpen,
|
|
14020
14042
|
assistant: assistantOpen,
|
|
14021
14043
|
};
|
|
14044
|
+
// Per-shell overrides for the dock tab strip. In a cell the `changes` panel
|
|
14045
|
+
// IS the history (the hub commits server-side), so its tab must say so —
|
|
14046
|
+
// otherwise the one visible word still promises a working-tree surface that
|
|
14047
|
+
// panel no longer has.
|
|
14048
|
+
const dockLabels = cfg.cloud ? { changes: 'History' } : null;
|
|
14022
14049
|
const leftIds = idsForSide('left');
|
|
14023
14050
|
const rightIds = idsForSide('right');
|
|
14024
14051
|
const leftActive = leftIds.find((id) => panelIsOpen[id]) || null;
|
|
@@ -14075,6 +14102,7 @@ function App() {
|
|
|
14075
14102
|
remoteSync={remoteSync}
|
|
14076
14103
|
onGetLatest={gitGetLatest}
|
|
14077
14104
|
canvasKinds={cfg?.canvasKinds}
|
|
14105
|
+
syncStatus={syncStatus}
|
|
14078
14106
|
/>
|
|
14079
14107
|
);
|
|
14080
14108
|
if (id === 'changes')
|
|
@@ -14083,6 +14111,20 @@ function App() {
|
|
|
14083
14111
|
status={gitStatus && remoteSync ? { ...gitStatus, ...remoteSync } : gitStatus}
|
|
14084
14112
|
project={project}
|
|
14085
14113
|
readOnly={!isNativeApp() || viewerMode}
|
|
14114
|
+
// A cloud cell commits every edit server-side as it lands, so the
|
|
14115
|
+
// working-tree half of this panel describes work that is already
|
|
14116
|
+
// saved. `cfg.cloud` is present exactly when the hub runs the
|
|
14117
|
+
// workspace agent that owns this project's history, which is the
|
|
14118
|
+
// same condition.
|
|
14119
|
+
//
|
|
14120
|
+
// PRESENTATION, NOT A CONTROL — nothing may come to depend on this.
|
|
14121
|
+
// `/_api/git/commit` and `/_api/git/discard` are classified `edit` in
|
|
14122
|
+
// the cell's route manifest and stay reachable by any member with a
|
|
14123
|
+
// session; withdrawing the buttons removes an offer that would
|
|
14124
|
+
// mislead, it does not remove a capability. The real gates are
|
|
14125
|
+
// server-side (`projectReadOnly`, the manifest's role matrix), and
|
|
14126
|
+
// they are unchanged by this flag.
|
|
14127
|
+
historyOnly={!!cfg.cloud}
|
|
14086
14128
|
resizing={resizingFor('changes')}
|
|
14087
14129
|
onClose={() => setChangesOpen(false)}
|
|
14088
14130
|
onCommit={gitCommit}
|
|
@@ -14330,6 +14372,7 @@ function App() {
|
|
|
14330
14372
|
ids={leftIds}
|
|
14331
14373
|
activeId={leftActive}
|
|
14332
14374
|
onPick={togglePanel}
|
|
14375
|
+
labels={dockLabels}
|
|
14333
14376
|
>
|
|
14334
14377
|
{leftHostsAssistant && (
|
|
14335
14378
|
<ChatPanel
|
|
@@ -14402,6 +14445,7 @@ function App() {
|
|
|
14402
14445
|
ids={rightIds}
|
|
14403
14446
|
activeId={rightActive}
|
|
14404
14447
|
onPick={togglePanel}
|
|
14448
|
+
labels={dockLabels}
|
|
14405
14449
|
>
|
|
14406
14450
|
{rightHostsAssistant && (
|
|
14407
14451
|
<ChatPanel
|
|
@@ -15119,6 +15163,7 @@ function App() {
|
|
|
15119
15163
|
onToggleTheme={toggleTheme}
|
|
15120
15164
|
onClearSelected={clearSelected}
|
|
15121
15165
|
syncStatus={syncStatus}
|
|
15166
|
+
syncProject={cfg?.cloud?.projectName || project}
|
|
15122
15167
|
changesCount={unsavedCount}
|
|
15123
15168
|
unpushed={gitStatus?.unpushed || 0}
|
|
15124
15169
|
changesOpen={changesOpen}
|
|
@@ -1855,6 +1855,11 @@ export default function ChatPanel({
|
|
|
1855
1855
|
const connsRef = useRef(new Map()); // chatId → connection
|
|
1856
1856
|
const busyRef = useRef(new Map()); // chatId → busy (aggregated for the menubar)
|
|
1857
1857
|
const hydratedRef = useRef(new Map()); // chatId → initial messages
|
|
1858
|
+
// chatId → the transcript LINE the hydration above covered (the
|
|
1859
|
+
// `X-Maude-Chat-Seq` response header). Handed to the connection's `attach`
|
|
1860
|
+
// so the server replays only what the client missed — Addendum Task 8's
|
|
1861
|
+
// re-attach seam. Absent/0 for a chat that was never hydrated (a new chat).
|
|
1862
|
+
const hydratedSeqRef = useRef(new Map());
|
|
1858
1863
|
const [openChatIds, setOpenChatIds] = useState([]);
|
|
1859
1864
|
const [activeChatId, setActiveChatId] = useState(null);
|
|
1860
1865
|
const [busyChats, setBusyChats] = useState({}); // reactive: chatId → busy (for the dot)
|
|
@@ -1872,6 +1877,13 @@ export default function ChatPanel({
|
|
|
1872
1877
|
const existing = connsRef.current.get(chatId);
|
|
1873
1878
|
if (existing) return existing;
|
|
1874
1879
|
const conn = createAcpConnection();
|
|
1880
|
+
// Addendum Task 8 — bind BEFORE anything can open the socket. The `attach`
|
|
1881
|
+
// frame goes out from `onopen` and is what subscribes this socket to the
|
|
1882
|
+
// chat's server-side bridge (which now outlives its socket, so it may
|
|
1883
|
+
// already be mid-turn from before a reload). `hydratedSeqRef` holds the
|
|
1884
|
+
// transcript line the history fetch covered, so the server replays exactly
|
|
1885
|
+
// the gap — no duplicated tail, no hole.
|
|
1886
|
+
conn.bindChat(chatId, hydratedSeqRef.current.get(chatId) || 0);
|
|
1875
1887
|
connsRef.current.set(chatId, conn);
|
|
1876
1888
|
// Agent-generated title (session_info_update — fires at turn-end, so a
|
|
1877
1889
|
// fresh chat shows the client's "New chat" heuristic until the first
|
|
@@ -1949,7 +1961,12 @@ export default function ChatPanel({
|
|
|
1949
1961
|
return;
|
|
1950
1962
|
}
|
|
1951
1963
|
fetch(`/_api/acp/chat?id=${encodeURIComponent(id)}`)
|
|
1952
|
-
.then((r) =>
|
|
1964
|
+
.then(async (r) => {
|
|
1965
|
+
// Capture the seam marker alongside the body — see hydratedSeqRef.
|
|
1966
|
+
const seq = Number(r.headers.get('X-Maude-Chat-Seq') || 0);
|
|
1967
|
+
hydratedSeqRef.current.set(id, Number.isFinite(seq) ? seq : 0);
|
|
1968
|
+
return r.json();
|
|
1969
|
+
})
|
|
1953
1970
|
.then((msgs) => openChat(id, toThreadMessages(msgs)))
|
|
1954
1971
|
.catch(() => {});
|
|
1955
1972
|
},
|
|
@@ -1965,6 +1982,7 @@ export default function ChatPanel({
|
|
|
1965
1982
|
connsRef.current.delete(id);
|
|
1966
1983
|
busyRef.current.delete(id);
|
|
1967
1984
|
hydratedRef.current.delete(id);
|
|
1985
|
+
hydratedSeqRef.current.delete(id);
|
|
1968
1986
|
setBusyChats((prev) => {
|
|
1969
1987
|
const next = { ...prev };
|
|
1970
1988
|
delete next[id];
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { useEffect, useRef, useState } from 'react';
|
|
16
16
|
|
|
17
|
+
import { safeName, syncPresentation } from '../../sync/presentation.ts';
|
|
17
18
|
import { invoke, isNativeApp, listen, openCloudUrl } from '../github.js';
|
|
18
19
|
|
|
19
20
|
const api = async (path, init) => {
|
|
@@ -247,6 +248,73 @@ export function localIdentityHint({
|
|
|
247
248
|
return related ? 'similar' : 'mismatch';
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
/**
|
|
252
|
+
* What to say after a successful Connect.
|
|
253
|
+
*
|
|
254
|
+
* The old copy said "Linked to <X> — restart the studio server to start
|
|
255
|
+
* syncing", which was three problems in one line: it named a thing the person
|
|
256
|
+
* cannot see (there is no visible server in the desktop app), gave them a task
|
|
257
|
+
* instead of a result, and never explained what connecting had achieved. The
|
|
258
|
+
* server now cycles the sync runtime itself, so this reports an OUTCOME —
|
|
259
|
+
* and on the paths where syncing genuinely didn't start, it says which one and
|
|
260
|
+
* what to do, rather than prescribing a restart for every cause.
|
|
261
|
+
*
|
|
262
|
+
* Returns `{ text, title }` — `text` is the rail's one line, `title` the full
|
|
263
|
+
* sentence on hover (the rail truncates).
|
|
264
|
+
*/
|
|
265
|
+
export function connectOutcomeNote(project, sync, live) {
|
|
266
|
+
// The project name and `sync.detail` both originate at the hub, and the
|
|
267
|
+
// fallback path below puts them in trusted app chrome uncapped — including a
|
|
268
|
+
// `title=` tooltip, where a newline renders. `safeName` strips control and
|
|
269
|
+
// bidi-override characters, collapses whitespace and bounds the length; the
|
|
270
|
+
// live path already went through it inside `syncPresentation`.
|
|
271
|
+
const name = safeName(project, 'the workspace');
|
|
272
|
+
|
|
273
|
+
// The LIVE payload wins whenever there is one.
|
|
274
|
+
//
|
|
275
|
+
// `sync` is the attach response, and it reports an intention: the supervisor
|
|
276
|
+
// answers `{ syncing: true }` as soon as `runtime.start()` did not throw and
|
|
277
|
+
// one local canvas qualified — before a socket, before a token was accepted,
|
|
278
|
+
// before a byte moved. Rendered once and never updated, it was the whole of
|
|
279
|
+
// the reported complaint ("vyskočí modal, pak vidím jen syncing canvases, ale
|
|
280
|
+
// reálně se nic nestane"). `live` comes off the `sync:status` bus that is
|
|
281
|
+
// already wired to this app, so the same sentence now MOVES: connecting →
|
|
282
|
+
// syncing 40/75 → synced. That motion is the feedback that was missing.
|
|
283
|
+
//
|
|
284
|
+
// Same rule as the status bar (`syncPresentation`), deliberately — the old
|
|
285
|
+
// hover text told the person to go look at that slot for the real answer,
|
|
286
|
+
// which only worked as long as the two agreed, and they did not.
|
|
287
|
+
if (live) {
|
|
288
|
+
const p = syncPresentation(live, { project: name });
|
|
289
|
+
if (p) {
|
|
290
|
+
const detail = p.names.length ? ` (${p.names.join(', ')})` : '';
|
|
291
|
+
return {
|
|
292
|
+
text: p.title,
|
|
293
|
+
title: `${p.title}${detail}${p.next ? ` — ${p.next}` : ''}`,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (sync?.syncing) {
|
|
299
|
+
const n = sync.canvases ?? 0;
|
|
300
|
+
return {
|
|
301
|
+
text: `Connecting to ${name} — ${n} canvas${n === 1 ? '' : 'es'}…`,
|
|
302
|
+
title: `Opening ${n} canvas${n === 1 ? '' : 'es'} against the ${name} workspace. This line updates as they settle.`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (sync?.reason === 'nothing-syncable') {
|
|
306
|
+
return {
|
|
307
|
+
text: `Connected to ${name} — nothing to sync yet.`,
|
|
308
|
+
title: safeName(sync.detail, 'No canvases in this project are syncable yet.'),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const detail = safeName(sync?.detail, 'restart Maude to start syncing.');
|
|
312
|
+
return {
|
|
313
|
+
text: `Connected to ${name} — ${detail}`,
|
|
314
|
+
title: detail,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
250
318
|
function Spark({ size = 15 }) {
|
|
251
319
|
return (
|
|
252
320
|
<svg width={size} height={size} viewBox="0 0 32 32" fill="currentColor" aria-hidden="true">
|
|
@@ -301,14 +369,21 @@ function Icon({ name, size = 15 }) {
|
|
|
301
369
|
);
|
|
302
370
|
}
|
|
303
371
|
|
|
304
|
-
export default function CloudBar() {
|
|
372
|
+
export default function CloudBar({ syncStatus }) {
|
|
305
373
|
const [state, setState] = useState('loading'); // loading | out | in
|
|
306
374
|
const [email, setEmail] = useState(null);
|
|
307
375
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
308
376
|
const [device, setDevice] = useState(null); // { userCode, verificationUrl, deviceCode }
|
|
309
377
|
const [projects, setProjects] = useState(null);
|
|
310
378
|
const [busy, setBusy] = useState('');
|
|
311
|
-
|
|
379
|
+
// What the last Connect attached, NOT the sentence it produced.
|
|
380
|
+
//
|
|
381
|
+
// This used to hold the rendered `{ text, title }`, which is why the note was
|
|
382
|
+
// frozen: a string computed once at the moment the dialog closed could only
|
|
383
|
+
// ever describe that moment. Keeping the INPUTS and deriving the sentence on
|
|
384
|
+
// every render is the whole liveness mechanism — `syncStatus` arrives on the
|
|
385
|
+
// already-wired `sync:status` bus, so no polling and no second fetch.
|
|
386
|
+
const [connected, setConnected] = useState(null); // { project, sync }
|
|
312
387
|
const [error, setError] = useState('');
|
|
313
388
|
const [copied, setCopied] = useState(false);
|
|
314
389
|
const [copiedLink, setCopiedLink] = useState(false);
|
|
@@ -321,6 +396,15 @@ export default function CloudBar() {
|
|
|
321
396
|
const railRef = useRef(null);
|
|
322
397
|
const pollRef = useRef(null);
|
|
323
398
|
|
|
399
|
+
// Derived, not stored — so the sentence follows the link instead of freezing
|
|
400
|
+
// at the instant the confirm dialog closed. `syncStatus` is the live payload
|
|
401
|
+
// App already receives; when it is absent (older server, first render) the
|
|
402
|
+
// attach response is still the fallback and the note degrades to what it
|
|
403
|
+
// said before, never to nothing.
|
|
404
|
+
const note = connected
|
|
405
|
+
? connectOutcomeNote(connected.project, connected.sync, syncStatus)
|
|
406
|
+
: null;
|
|
407
|
+
|
|
324
408
|
useEffect(() => {
|
|
325
409
|
api('/_api/cloud/status')
|
|
326
410
|
.then((r) => {
|
|
@@ -391,7 +475,7 @@ export default function CloudBar() {
|
|
|
391
475
|
setBusy('');
|
|
392
476
|
setPending(null);
|
|
393
477
|
if (r.ok && r.json?.ok) {
|
|
394
|
-
|
|
478
|
+
setConnected({ project: r.json.project ?? pending.project, sync: r.json.sync });
|
|
395
479
|
} else {
|
|
396
480
|
setError(r.json?.error || 'The workspace could not be connected.');
|
|
397
481
|
}
|
|
@@ -468,7 +552,7 @@ export default function CloudBar() {
|
|
|
468
552
|
|
|
469
553
|
async function connect(projectId) {
|
|
470
554
|
setBusy(projectId);
|
|
471
|
-
|
|
555
|
+
setConnected(null);
|
|
472
556
|
setError('');
|
|
473
557
|
const r = await api('/_api/cloud/attach', {
|
|
474
558
|
method: 'POST',
|
|
@@ -477,7 +561,7 @@ export default function CloudBar() {
|
|
|
477
561
|
});
|
|
478
562
|
setBusy('');
|
|
479
563
|
if (r.ok && r.json?.ok) {
|
|
480
|
-
|
|
564
|
+
setConnected({ project: r.json.project ?? projectId, sync: r.json.sync });
|
|
481
565
|
} else {
|
|
482
566
|
setError(r.json?.error || 'The workspace could not be connected.');
|
|
483
567
|
}
|
|
@@ -670,7 +754,21 @@ export default function CloudBar() {
|
|
|
670
754
|
<span className="gi-rail-login">{email ?? 'Maude Cloud'}</span>
|
|
671
755
|
<span className="gi-rail-caret"><Icon name="chevron-up" size={13} /></span>
|
|
672
756
|
</button>
|
|
673
|
-
{
|
|
757
|
+
{/* A live region: the sentence now CHANGES as the link settles, and a
|
|
758
|
+
screen-reader user has to hear that transition rather than only
|
|
759
|
+
whatever text happened to be there when they last tabbed past.
|
|
760
|
+
`polite` — it is progress, never an interruption. */}
|
|
761
|
+
{note && (
|
|
762
|
+
<span
|
|
763
|
+
className="gi-rail-hint"
|
|
764
|
+
title={note.title}
|
|
765
|
+
role="status"
|
|
766
|
+
aria-live="polite"
|
|
767
|
+
data-testid="cloud-connect-note"
|
|
768
|
+
>
|
|
769
|
+
{note.text}
|
|
770
|
+
</span>
|
|
771
|
+
)}
|
|
674
772
|
{error && <span className="gi-rail-err" title={error}>{error}</span>}
|
|
675
773
|
{menuOpen && (
|
|
676
774
|
<div className="gi-menu" role="menu" aria-label="Maude Cloud">
|
|
@@ -155,8 +155,19 @@ export default function GitPanel({
|
|
|
155
155
|
// checkboxes) — the developer commits and pushes from their terminal
|
|
156
156
|
// (DDR-119). Native app keeps the full plain-words cycle.
|
|
157
157
|
readOnly = false,
|
|
158
|
+
// THE SERVER ALREADY COMMITTED IT. In a cloud cell the hub owns this
|
|
159
|
+
// project's history and commits every edit as it lands (the studio child's
|
|
160
|
+
// own autocommit is disabled precisely so there is one committer) — so an
|
|
161
|
+
// "unsaved changes" list is not a to-do, it is a lie about work that is
|
|
162
|
+
// already saved, and Save / Publish offer to do a thing that has happened.
|
|
163
|
+
// The read-only variant of this panel used to answer that with "save and
|
|
164
|
+
// publish from your terminal", which is advice a browser user cannot take.
|
|
165
|
+
//
|
|
166
|
+
// History STAYS, and is the point: what the server committed is exactly what
|
|
167
|
+
// someone wants to look back at. Only the working-tree half goes.
|
|
168
|
+
historyOnly = false,
|
|
158
169
|
}) {
|
|
159
|
-
const [tab, setTab] = useState('changes');
|
|
170
|
+
const [tab, setTab] = useState(historyOnly ? 'history' : 'changes');
|
|
160
171
|
const [message, setMessage] = useState('');
|
|
161
172
|
const [unchecked, setUnchecked] = useState(() => new Set()); // default = all checked
|
|
162
173
|
const [expanded, setExpanded] = useState(() => new Set()); // unit keys with supporting files shown
|
|
@@ -443,13 +454,13 @@ export default function GitPanel({
|
|
|
443
454
|
<aside
|
|
444
455
|
className={'st-rpanel gp-panel' + (resizing ? ' is-resizing' : '')}
|
|
445
456
|
style={width ? { width, flexBasis: width } : undefined}
|
|
446
|
-
aria-label=
|
|
457
|
+
aria-label={historyOnly ? 'History' : 'Changes'}
|
|
447
458
|
data-testid="git-panel"
|
|
448
459
|
>
|
|
449
460
|
<div className="gp-head">
|
|
450
461
|
<div className="gp-panel-hd">
|
|
451
|
-
<span className="gp-panel-title">Changes</span>
|
|
452
|
-
{count > 0 && <span className="gp-count">{count} unsaved</span>}
|
|
462
|
+
<span className="gp-panel-title">{historyOnly ? 'History' : 'Changes'}</span>
|
|
463
|
+
{!historyOnly && count > 0 && <span className="gp-count">{count} unsaved</span>}
|
|
453
464
|
<span className="gp-spacer" />
|
|
454
465
|
<span className="gp-draft" title="Your project and shared draft">
|
|
455
466
|
<Icon name="folder" size={12} />
|
|
@@ -465,26 +476,28 @@ export default function GitPanel({
|
|
|
465
476
|
×
|
|
466
477
|
</button>
|
|
467
478
|
</div>
|
|
468
|
-
|
|
469
|
-
<
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
479
|
+
{!historyOnly && (
|
|
480
|
+
<div className="gp-tabs" role="tablist" aria-label="Changes and history">
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
role="tab"
|
|
484
|
+
aria-selected={tab === 'changes'}
|
|
485
|
+
className={'gp-tab' + (tab === 'changes' ? ' is-active' : '')}
|
|
486
|
+
onClick={() => setTab('changes')}
|
|
487
|
+
>
|
|
488
|
+
Changes
|
|
489
|
+
</button>
|
|
490
|
+
<button
|
|
491
|
+
type="button"
|
|
492
|
+
role="tab"
|
|
493
|
+
aria-selected={tab === 'history'}
|
|
494
|
+
className={'gp-tab' + (tab === 'history' ? ' is-active' : '')}
|
|
495
|
+
onClick={openHistory}
|
|
496
|
+
>
|
|
497
|
+
History
|
|
498
|
+
</button>
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
488
501
|
</div>
|
|
489
502
|
|
|
490
503
|
{banner && (
|
|
@@ -530,7 +543,7 @@ export default function GitPanel({
|
|
|
530
543
|
</div>
|
|
531
544
|
)}
|
|
532
545
|
|
|
533
|
-
{tab === 'changes' ? (
|
|
546
|
+
{tab === 'changes' && !historyOnly ? (
|
|
534
547
|
notRepo ? (
|
|
535
548
|
<div className="gp-empty">
|
|
536
549
|
<span className="gp-empty-glyph">
|