mbeditor 0.12.1 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6645751ebceaf2b9754d5aabb417549334518db35027e1bd6c08d9fede2d7a5
4
- data.tar.gz: edb2a8a2696f5aac16fcdcd4bbbf728f5d05a7af14ffca326a59ec7c0d40b860
3
+ metadata.gz: 88d963e43c97cf6974b3e38c079ffd2dba642dc1c13a68237f1a3a4f7c21e9a0
4
+ data.tar.gz: 9e32a366d6ea7ce5014fff94ee89adba7f6909d09aa29f6404142b4155688a3a
5
5
  SHA512:
6
- metadata.gz: 5f9d7559b69d80a1d8d9d094d5513a69048ce62ea385630ca59811e0dafd50ca65afa988e1f560cc32d78fc7b71dceedb78b30f1ba190e00b465d1026e3554d5
7
- data.tar.gz: a8a3de31439ff3a937db30a04dbfd631694f50ccbcce4c3fb3571dfa66cd798cf8df29b45855467a9ff783a919893829472b241523d7230468944a3accad04f9
6
+ metadata.gz: 100805339d3956837f0ec5349de86b5bcf4315ef5dd261e76b80c0acb589cd9036264b70bb65677be3d27be9575007daee02e08c1cbad86ac3136bd331312a6b
7
+ data.tar.gz: d1d0681c8962251ceb0d9f18db0937e20f3c1ae1e2d30754a08a88ee10a47b6a6492641de84b9e83c0b2ab81963e79d1f3d86bb61a985bfa7b4a5eb579d0548d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,83 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.3] - 2026-08-03
11
+
12
+ ### Added
13
+ - **`config.cable_authenticate_with`** — authentication for the collaboration
14
+ WebSocket, falling back to `authenticate_with` when unset.
15
+ - **A "Pairing off" chip and diagnostics panel.** When collaboration cannot
16
+ work, the editor now says so and lists every condition it depends on — the
17
+ vendored libraries, Action Cable's JavaScript, whether the server advertises
18
+ it, whether the socket actually connected, and whether anyone else is here —
19
+ with the fix written beside each failure. Nothing is shown when everything is
20
+ healthy and you are simply alone.
21
+
22
+ ### Fixed
23
+ - **A WebSocket subscribe runs no controller, and this made pairing silently
24
+ impossible for most authenticated apps.** `authenticate_with` is evaluated on
25
+ the cable against a probe exposing `session`, `cookies`, `request` and
26
+ `params` — so a hook reading `Current.user`, calling `UserSession.find`, or
27
+ using a memoised `current_user` gets `nil` or a `NameError`, denies, and the
28
+ socket is rejected. The hook keeps working perfectly over HTTP, so nothing
29
+ looks wrong; collaboration simply never connects.
30
+
31
+ Rejections are now logged as `[mbeditor] WebSocket subscription rejected: …`
32
+ naming the cause, `cable_authenticate_with` provides an escape hatch when one
33
+ proc cannot serve both contexts, and the README says this plainly instead of
34
+ noting that request-scoped state "may be narrower". The README's own example
35
+ used `UserSession.find`, which is exactly the pattern that cannot work.
36
+
37
+ ## [0.12.2] - 2026-08-03
38
+
39
+ ### Added
40
+ - **Inline route hints in controllers.** Every action in a controller file is
41
+ annotated with the verb and path that reach it — `GET /orders/:id` beside
42
+ `def show` — and a public action nothing routes to is flagged `no route`.
43
+ Hovering adds the named-route helper. Answering "is this actually reachable?"
44
+ previously meant reading `config/routes.rb` and expanding `resources` in your
45
+ head.
46
+
47
+ Routes come from the host app's own route set rather than by parsing
48
+ `config/routes.rb`. mbeditor runs inside the app, so the routes are already
49
+ built — and they are the only source that accounts for `resources` expansion,
50
+ `member`/`collection` blocks, scopes, constraints and mounted engines.
51
+
52
+ - **`config.model_graph_max_models`** (default 1000, was a hard-coded 300).
53
+ A schema over the cap silently lost models and only said "(truncated)".
54
+
55
+ ### Changed
56
+ - **Model boxes are sized to their contents** rather than all being one width,
57
+ so a long model or column name is no longer truncated while a model called
58
+ `Tag` wastes most of its box. Layer positions accumulate per layer, since a
59
+ fixed stride would let a wide box overlap the next layer.
60
+ - **Cluster blocks are packed in two dimensions** instead of stacked in a single
61
+ column, which left a schema with one big core and several small islands
62
+ running off the bottom with the right-hand side empty.
63
+ - **The dummy app now carries a real ActiveRecord schema** — 20 models and 44
64
+ associations covering a hub, a chain, a self-reference, a join table, a
65
+ polymorphic association and unconnected islands — so the model graph can be
66
+ demonstrated and tested against something representative.
67
+
68
+ ### Fixed
69
+ - **mbeditor could stop a host app from booting.** The pending-migrations
70
+ middleware was installed whenever `ActiveRecord::Migration::CheckPending` was
71
+ defined, but Rails only puts that middleware in the stack when
72
+ `config.active_record.migration_error` is `:page_load`. Any app loading
73
+ ActiveRecord with a different setting raised "No such middleware to insert
74
+ before" during boot. It now tests the same condition Rails does.
75
+ - **Long labels drew straight out past the edge of their model box.** SVG text
76
+ neither wraps nor ellipsises; labels are now measured against the font the
77
+ theme actually resolves and cut to fit.
78
+ - **"no database connection" hung below the box it belonged to** — the box
79
+ height did not count that placeholder line.
80
+ - **The model search is a real dropdown.** The native `<datalist>` rendered in
81
+ the browser's own chrome: unstyleable, and unable to show the table name and
82
+ column count beside each model. Arrow keys and Enter work as before.
83
+ - **The titlebar wrapped to two lines in a narrow window**, pushing the toolbar
84
+ out of its 32px row. It no longer wraps, and below the width where the toolbar
85
+ drops its button labels the title gives way to the icon alone.
86
+
10
87
  ## [0.12.1] - 2026-08-03
11
88
 
12
89
  ### Added
data/README.md CHANGED
@@ -67,8 +67,12 @@ Mbeditor.configure do |config|
67
67
  config.excluded_paths = %w[.git tmp log node_modules .bundle coverage vendor/bundle]
68
68
  config.rubocop_command = "bundle exec rubocop"
69
69
 
70
- # Optional authentication (runs as a before_action in the engine controllers)
71
- # config.authenticate_with = proc { redirect_to login_path unless UserSession.find }
70
+ # Optional authentication (runs as a before_action in the engine controllers,
71
+ # and on the collaboration WebSocket subscribe). Resolve the user from
72
+ # `session` — a WebSocket subscribe runs no controller, so Current.user,
73
+ # UserSession.find and a memoised current_user are all unavailable there.
74
+ # config.authenticate_with = proc { redirect_to login_path unless User.find_by(id: session[:user_id]) }
75
+ # config.cable_authenticate_with = proc { ... } # when one proc cannot serve both
72
76
 
73
77
  # Optional test runner (Minitest or RSpec)
74
78
  # config.test_framework = :minitest # :minitest or :rspec — auto-detected when nil
@@ -123,6 +127,7 @@ end
123
127
  | Option | Default | Description |
124
128
  |--------|---------|-------------|
125
129
  | `authenticate_with` | `nil` | Proc run as a `before_action` in all engine controllers. Executed via `instance_exec` inside the controller, so it has access to `session`, `cookies`, `redirect_to`, and auth-library class methods (e.g. Authlogic's `UserSession.find`) — but not helper methods from the host's `ApplicationController`. The same hook is also evaluated when the collaboration / editor **WebSocket** subscribes (see [Collaborative pairing](#collaborative-pairing-optional)); if it halts or raises, that subscription is rejected (fail-closed). Over the cable the proc runs against a request-derived probe, so request-scoped state may be narrower than over HTTP. |
130
+ | `cable_authenticate_with` | `nil` | Authentication for the collaboration WebSocket; `nil` falls back to `authenticate_with`. Set this when the HTTP hook cannot work on the cable — a WebSocket subscribe runs no controller, so `Current.*`, an Authlogic `UserSession` and a memoised `current_user` are `nil` or raise, and the hook then denies the socket while working perfectly over HTTP. See [Collaborative pairing](#collaborative-pairing-optional). |
126
131
  | `authentication_cache_ttl` | `0` | Seconds to cache the auth result in the session (`0` = no caching). Set e.g. `300` to avoid calling `authenticate_with` on every request when the proc is expensive. Trade-off: after host logout, mbeditor stays accessible for up to TTL seconds. |
127
132
 
128
133
  ### Test runner
@@ -335,20 +340,45 @@ should be able to reach the port.
335
340
 
336
341
  **2. Set an authentication hook — it runs on the WebSocket handshake.**
337
342
  Configure `authenticate_with` (see the [Authentication](#authentication) options).
338
- The same hook that gates the HTTP editor is now also evaluated when the collaboration /
343
+ The same hook that gates the HTTP editor is also evaluated when the collaboration /
339
344
  editor WebSocket subscribes: if it halts (e.g. `redirect_to`/`render`/`head`) — or raises —
340
345
  the socket subscription is **rejected (fail-closed)**, so pairing cannot bypass your auth.
341
346
 
347
+ > **A WebSocket subscribe runs no controller.** This is the single most common reason
348
+ > pairing appears to do nothing. Anything a `before_action` populates is unavailable on
349
+ > the cable: `Current.user` (and any other `ActiveSupport::CurrentAttributes`) is `nil`,
350
+ > `UserSession.find` raises because Authlogic's controller adapter was never activated,
351
+ > and a memoised `current_user` is undefined. The hook then denies — correctly, given it
352
+ > was handed nobody — and the socket is rejected. It works perfectly over HTTP the whole
353
+ > time, so nothing looks wrong until you notice collaboration never connects.
354
+
355
+ The hook is evaluated against a probe exposing `session`, `cookies`, `request`, `params`
356
+ and the halt methods. Resolve the user from `session` so the same hook works in both
357
+ contexts:
358
+
359
+ ```ruby
360
+ Mbeditor.configure do |c|
361
+ c.authenticate_with = proc do
362
+ user = Current.user || User.find_by(id: session[:user_credentials_id])
363
+ head :forbidden unless user&.super_admin_access?
364
+ end
365
+ end
366
+ ```
367
+
368
+ When one proc genuinely cannot serve both, give the cable its own:
369
+
342
370
  ```ruby
343
371
  Mbeditor.configure do |c|
344
- # Runs as a controller before_action AND on the cable subscribe.
345
- c.authenticate_with = proc { head :forbidden unless UserSession.find }
372
+ c.authenticate_with = proc { head :forbidden unless Current.user&.super_admin_access? }
373
+ c.cable_authenticate_with = proc do
374
+ head :forbidden unless User.find_by(id: session[:user_credentials_id])&.super_admin_access?
375
+ end
346
376
  end
347
377
  ```
348
378
 
349
- Because the cable mount can bypass parts of the host middleware stack, a hook that leans on
350
- request-scoped state (full `session`, encrypted `cookies`) may see less over the WebSocket
351
- than it does over HTTP. For defence in depth, also authenticate at your host app's
379
+ Every rejection is logged as `[mbeditor] WebSocket subscription rejected: …` with the
380
+ reason, so a denial is visible rather than silent. The editor also shows a **Pairing off**
381
+ chip when collaboration cannot work, listing each failing condition. For defence in depth, also authenticate at your host app's
352
382
  `ApplicationCable::Connection` (the standard `identified_by` / `reject_unauthorized_connection`
353
383
  pattern) — mbeditor's hook is an additional gate, not a replacement for securing the cable
354
384
  connection itself.
@@ -120,6 +120,67 @@ var CollaborationService = (function () {
120
120
  });
121
121
  }
122
122
 
123
+ // Every condition collaboration depends on, reported separately.
124
+ //
125
+ // isAvailable() collapses five things into one boolean, which is useless when
126
+ // pairing does not work: "false" could be a missing vendored library, a cable
127
+ // the server never advertised, a rejected handshake, or simply nobody else
128
+ // being here. Someone debugging this on another machine cannot paste a console
129
+ // snippet back, so the editor has to be able to say which.
130
+ function diagnostics() {
131
+ var cableStatus = (typeof WebSocketService !== 'undefined' &&
132
+ typeof WebSocketService.cableStatus === 'function')
133
+ ? WebSocketService.cableStatus() : 'unknown';
134
+
135
+ var checks = [
136
+ {
137
+ key: 'libraries',
138
+ label: 'Collaboration libraries loaded',
139
+ ok: typeof window.Y !== 'undefined' && typeof window.MonacoBinding !== 'undefined',
140
+ detail: 'vendor/assets/javascripts/yjs-collab.js is served by the asset pipeline. ' +
141
+ 'If this fails, the host app is not serving it — try rails assets:clobber, ' +
142
+ 'or check config.assets.precompile.'
143
+ },
144
+ {
145
+ key: 'actioncable',
146
+ label: 'Action Cable JavaScript present',
147
+ ok: typeof window.ActionCable !== 'undefined',
148
+ detail: 'The host app must make actioncable.js available to the page.'
149
+ },
150
+ {
151
+ key: 'advertised',
152
+ label: 'Server advertises Action Cable',
153
+ ok: typeof WebSocketService !== 'undefined' && WebSocketService.isCableAvailable(),
154
+ detail: 'Action Cable must be enabled and mounted in the host app routes.'
155
+ },
156
+ {
157
+ key: 'connected',
158
+ label: 'WebSocket connected',
159
+ ok: cableStatus === 'connected',
160
+ detail: cableStatus === 'rejected'
161
+ ? 'The server rejected the subscription. Usually action_cable.allowed_request_origins ' +
162
+ 'not listing the origin you are reaching the editor through, or an authenticate_with ' +
163
+ 'hook halting on the cable (it fails closed). Look for "Request origin not allowed" in the log.'
164
+ : 'Status: ' + cableStatus + '.'
165
+ },
166
+ {
167
+ key: 'peers',
168
+ label: 'Another participant connected',
169
+ ok: _peerPresent,
170
+ detail: 'Presence is held per web process. If two people are on different Puma workers ' +
171
+ 'they never see each other — run a single worker (WEB_CONCURRENCY=0) while pairing.'
172
+ }
173
+ ];
174
+
175
+ return {
176
+ ok: checks.every(function (c) { return c.ok; }),
177
+ cableStatus: cableStatus,
178
+ checks: checks,
179
+ // The first failing check is the one worth acting on; the rest follow from it.
180
+ firstProblem: checks.filter(function (c) { return !c.ok; })[0] || null
181
+ };
182
+ }
183
+
123
184
  // Subscribe to availability transitions. Returns an unsubscribe function.
124
185
  function onAvailabilityChange(fn) {
125
186
  _availabilityListeners.push(fn);
@@ -674,6 +735,7 @@ var CollaborationService = (function () {
674
735
  return {
675
736
  isAvailable: isAvailable,
676
737
  setPeerPresent: setPeerPresent,
738
+ diagnostics: diagnostics,
677
739
  onAvailabilityChange: onAvailabilityChange,
678
740
  isEnabledFor: isEnabledFor,
679
741
  ensureRoom: ensureRoom,
@@ -90,6 +90,7 @@ var EditorPanel = function EditorPanel(_ref) {
90
90
 
91
91
  var blameDecorationsRef = useRef([]);
92
92
  var gitLineDecorationsRef = useRef([]);
93
+ var routeDecorationsRef = useRef([]);
93
94
  // Latest git line-diff refresh, read by the poll effect so its interval does
94
95
  // not have to be torn down whenever the active tab changes.
95
96
  var gitLineRefreshRef = useRef(null);
@@ -1530,6 +1531,83 @@ var EditorPanel = function EditorPanel(_ref) {
1530
1531
  }
1531
1532
  }, [isBlameVisible, tab.path, blameData, isBlameLoading]);
1532
1533
 
1534
+ // Inline route hints for controllers.
1535
+ //
1536
+ // Every `def` in a controller gets the verb and path that reach it drawn after
1537
+ // the line, and a public action nothing routes to is called out — the question
1538
+ // "is this actually reachable?" otherwise means reading config/routes.rb and
1539
+ // mentally expanding `resources`. Routes come from the host app's own route
1540
+ // set, which is the only thing that knows how those expand.
1541
+ //
1542
+ // Decorations, not view zones: a hint belongs on the line, and a zone would
1543
+ // push the code around every time a file was opened.
1544
+ useEffect(function () {
1545
+ var editor = monacoRef.current;
1546
+ if (!editor || !window.monaco) return;
1547
+
1548
+ var clear = function () {
1549
+ if (!routeDecorationsRef.current.length) return;
1550
+ routeDecorationsRef.current = editor.deltaDecorations(routeDecorationsRef.current, []);
1551
+ };
1552
+
1553
+ if (!tab.path || !/^app\/controllers\/.+_controller\.rb$/.test(tab.path)) {
1554
+ clear();
1555
+ return;
1556
+ }
1557
+
1558
+ var cancelled = false;
1559
+ FileService.getRoutes(tab.path).then(function (data) {
1560
+ if (cancelled) return;
1561
+ var actions = (data && data.actions) || {};
1562
+ var model = editor.getModel();
1563
+ if (!model) return;
1564
+
1565
+ var decorations = [];
1566
+ var text = model.getValue().split('\n');
1567
+ // Only methods above the first `private`/`protected` are candidates for a
1568
+ // route; flagging a helper below it as unrouted would be noise.
1569
+ var visibilityEnded = false;
1570
+
1571
+ text.forEach(function (line, i) {
1572
+ if (/^\s*(private|protected)\s*$/.test(line)) { visibilityEnded = true; return; }
1573
+ var m = line.match(/^\s*def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_]*[?!=]?)/);
1574
+ if (!m) return;
1575
+
1576
+ var name = m[1];
1577
+ var routes = actions[name];
1578
+ var lineNo = i + 1;
1579
+ var col = line.length + 1;
1580
+
1581
+ if (routes && routes.length) {
1582
+ var label = routes.map(function (r) { return r.verb + ' ' + r.path; }).join(' · ');
1583
+ decorations.push({
1584
+ range: new window.monaco.Range(lineNo, col, lineNo, col),
1585
+ options: {
1586
+ after: { content: ' ' + label, inlineClassName: 'mbeditor-route-hint' },
1587
+ hoverMessage: routes.map(function (r) {
1588
+ return { value: '`' + r.verb + ' ' + r.path + '`' + (r.name ? ' — `' + r.name + '_path`' : '') };
1589
+ }),
1590
+ showIfCollapsed: true
1591
+ }
1592
+ });
1593
+ } else if (!visibilityEnded && !/[?!=]$/.test(name) && name !== 'initialize') {
1594
+ decorations.push({
1595
+ range: new window.monaco.Range(lineNo, col, lineNo, col),
1596
+ options: {
1597
+ after: { content: ' no route', inlineClassName: 'mbeditor-route-hint-none' },
1598
+ hoverMessage: [{ value: 'No route in this application dispatches to this action.' }],
1599
+ showIfCollapsed: true
1600
+ }
1601
+ });
1602
+ }
1603
+ });
1604
+
1605
+ routeDecorationsRef.current = editor.deltaDecorations(routeDecorationsRef.current, decorations);
1606
+ }).catch(function () { /* hints are additive; a failure just means none */ });
1607
+
1608
+ return function () { cancelled = true; clear(); };
1609
+ }, [tab.path, tab.externalContentVersion, monacoReady]);
1610
+
1533
1611
  // Render Blame block headers (author + summary) above contiguous commit regions.
1534
1612
  useEffect(function () {
1535
1613
  if (!monacoRef.current || !window.monaco || !isBlameVisible || !blameData) return;
@@ -2450,6 +2450,30 @@ var MbeditorApp = function MbeditorApp() {
2450
2450
  // external-change detection for solo users. The service is told about the roster
2451
2451
  // from the presence handler above, not from an effect here, so it hears about
2452
2452
  // every message rather than only about a change in the participant count.
2453
+ var _collabDiag = useState(false);
2454
+ var collabDiagOpen = _collabDiag[0], setCollabDiagOpen = _collabDiag[1];
2455
+ var _collabTrouble = useState(null);
2456
+ var collabTrouble = _collabTrouble[0], setCollabTrouble = _collabTrouble[1];
2457
+
2458
+ // Re-checked on a slow interval because the failures worth reporting — a
2459
+ // rejected handshake, a dropped socket — happen asynchronously and nothing
2460
+ // else would notice. Only the first failing check's key is stored, so an
2461
+ // unchanged state is an identical string and React bails rather than
2462
+ // re-rendering the app every tick.
2463
+ useEffect(function () {
2464
+ function check() {
2465
+ if (typeof CollaborationService === 'undefined' ||
2466
+ typeof CollaborationService.diagnostics !== 'function') return;
2467
+ var d = CollaborationService.diagnostics();
2468
+ // "Nobody else is here" is not a fault; everything else is.
2469
+ var problem = d.firstProblem && d.firstProblem.key !== 'peers' ? d.firstProblem.key : null;
2470
+ setCollabTrouble(function (prev) { return prev === problem ? prev : problem; });
2471
+ }
2472
+ check();
2473
+ var id = setInterval(check, 10000);
2474
+ return function () { clearInterval(id); };
2475
+ }, []);
2476
+
2453
2477
  var collabPeerIds = Object.keys(collabRoster);
2454
2478
 
2455
2479
  // A labelled peer chip costs ~110px (name + filename), and the titlebar button
@@ -4217,6 +4241,28 @@ var MbeditorApp = function MbeditorApp() {
4217
4241
  !toolbarIconOnly && " Git"
4218
4242
  )
4219
4243
  ),
4244
+ // Collaboration trouble chip. Deliberately not shown when everything is
4245
+ // healthy and you are simply alone — that was the whole point of hiding
4246
+ // the solo chip. It appears only when a condition pairing needs has
4247
+ // actually failed, so silence means "nobody here" and never "quietly
4248
+ // broken", which is exactly the ambiguity that made a real pairing
4249
+ // failure impossible to diagnose from the other machine.
4250
+ collabTrouble && React.createElement(
4251
+ React.Fragment,
4252
+ null,
4253
+ React.createElement("div", { className: "statusbar-sep" }),
4254
+ React.createElement(
4255
+ "button",
4256
+ {
4257
+ type: "button",
4258
+ className: "statusbar-btn mbeditor-collab-trouble",
4259
+ title: "Pairing is not available — click for details",
4260
+ onClick: function () { setCollabDiagOpen(true); }
4261
+ },
4262
+ React.createElement("i", { className: "fas fa-user-slash" }),
4263
+ !toolbarIconOnly && " Pairing off"
4264
+ )
4265
+ ),
4220
4266
  // Your own chip appears only once someone else is actually connected.
4221
4267
  // Alone it told you nothing — Action Cable is up in any normal dev setup,
4222
4268
  // so it sat in the toolbar permanently announcing a session of one. While
@@ -4358,6 +4404,49 @@ var MbeditorApp = function MbeditorApp() {
4358
4404
  )
4359
4405
  );
4360
4406
  })(),
4407
+ // The diagnostics panel. Every condition listed with its own state, because
4408
+ // the person who needs this is usually on the other machine and cannot paste
4409
+ // a console snippet back.
4410
+ collabDiagOpen && (function () {
4411
+ var d = CollaborationService.diagnostics();
4412
+ return React.createElement(
4413
+ 'div',
4414
+ { className: 'mbeditor-modal-backdrop', onClick: function () { setCollabDiagOpen(false); } },
4415
+ React.createElement(
4416
+ 'div',
4417
+ { className: 'mbeditor-collab-diag', onClick: function (e) { e.stopPropagation(); } },
4418
+ React.createElement('div', { className: 'mbeditor-collab-diag-title' }, 'Pair programming status'),
4419
+ React.createElement(
4420
+ 'ul',
4421
+ { className: 'mbeditor-collab-diag-list' },
4422
+ d.checks.map(function (c) {
4423
+ return React.createElement(
4424
+ 'li',
4425
+ { key: c.key, className: 'mbeditor-collab-diag-row' + (c.ok ? '' : ' is-bad') },
4426
+ React.createElement('i', { className: c.ok ? 'fas fa-check' : 'fas fa-times' }),
4427
+ React.createElement(
4428
+ 'div',
4429
+ null,
4430
+ React.createElement('div', { className: 'mbeditor-collab-diag-label' }, c.label),
4431
+ !c.ok && React.createElement('div', { className: 'mbeditor-collab-diag-detail' }, c.detail)
4432
+ )
4433
+ );
4434
+ })
4435
+ ),
4436
+ React.createElement(
4437
+ 'div',
4438
+ { className: 'mbeditor-collab-diag-foot' },
4439
+ d.ok
4440
+ ? 'Pairing is working.'
4441
+ : 'The first failing item above is the one to fix; the rest follow from it.'
4442
+ ),
4443
+ React.createElement('button', {
4444
+ type: 'button', className: 'ide-model-graph-btn',
4445
+ onClick: function () { setCollabDiagOpen(false); }
4446
+ }, 'Close')
4447
+ )
4448
+ );
4449
+ })(),
4361
4450
  showHelp && React.createElement(ShortcutHelp, { onClose: function () { return setShowHelp(false); } }),
4362
4451
  React.createElement(
4363
4452
  "div",