mbeditor 0.13.0 → 0.14.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -1
  3. data/app/assets/javascripts/mbeditor/application.js +3 -1
  4. data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
  5. data/app/assets/javascripts/mbeditor/collaboration_service.js +264 -22
  6. data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
  7. data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
  8. data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
  9. data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
  10. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
  11. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +440 -334
  12. data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
  13. data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
  14. data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
  15. data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
  16. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +9 -1
  17. data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
  18. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1322 -1431
  19. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
  20. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +82 -72
  21. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +121 -81
  22. data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
  23. data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
  24. data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
  25. data/app/assets/javascripts/mbeditor/editor_plugins.js +694 -306
  26. data/app/assets/javascripts/mbeditor/file_import.js +13 -15
  27. data/app/assets/javascripts/mbeditor/file_service.js +46 -57
  28. data/app/assets/javascripts/mbeditor/git_service.js +15 -1
  29. data/app/assets/javascripts/mbeditor/history_service.js +5 -13
  30. data/app/assets/javascripts/mbeditor/search_service.js +29 -2
  31. data/app/assets/javascripts/mbeditor/tab_manager.js +135 -54
  32. data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
  33. data/app/assets/stylesheets/mbeditor/application.css +6 -1
  34. data/app/assets/stylesheets/mbeditor/editor.css +762 -297
  35. data/app/assets/stylesheets/mbeditor/glass.css +163 -0
  36. data/app/assets/stylesheets/mbeditor/themes.css +90 -30
  37. data/app/channels/mbeditor/collaboration_channel.rb +25 -6
  38. data/app/controllers/mbeditor/application_controller.rb +26 -3
  39. data/app/controllers/mbeditor/editors_controller.rb +125 -465
  40. data/app/services/mbeditor/archive_service.rb +137 -0
  41. data/app/services/mbeditor/collaboration_doc_store.rb +180 -12
  42. data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
  43. data/app/services/mbeditor/editor_state_service.rb +14 -51
  44. data/app/services/mbeditor/file_history_service.rb +222 -0
  45. data/app/services/mbeditor/git_info_service.rb +6 -0
  46. data/app/services/mbeditor/js_globals_service.rb +12 -1
  47. data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
  48. data/app/services/mbeditor/lint_service.rb +137 -0
  49. data/app/services/mbeditor/locked_json_file.rb +67 -0
  50. data/app/services/mbeditor/process_runner.rb +32 -0
  51. data/app/services/mbeditor/rubocop_run_service.rb +17 -5
  52. data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
  53. data/app/services/mbeditor/schema_service.rb +8 -2
  54. data/app/services/mbeditor/search_replace_service.rb +19 -2
  55. data/app/services/mbeditor/test_runner_service.rb +3 -56
  56. data/app/views/layouts/mbeditor/application.html.erb +1 -1
  57. data/lib/mbeditor/audit_log.rb +203 -0
  58. data/lib/mbeditor/configuration.rb +6 -9
  59. data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
  60. data/lib/mbeditor/route_map.rb +4 -1
  61. data/lib/mbeditor/ruby_lsp_client.rb +82 -14
  62. data/lib/mbeditor/version.rb +1 -1
  63. data/lib/mbeditor.rb +1 -0
  64. data/lib/tasks/mbeditor.rake +23 -0
  65. metadata +14 -3
  66. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
@@ -0,0 +1,163 @@
1
+ /* ── Liquid Glass chrome (opt-in) ─────────────────────────────────────────────
2
+ *
3
+ * Scoped entirely under html[data-glass], set alongside data-theme by the same
4
+ * effect in MbeditorApp.js. It is a layer over whichever theme is active, not a
5
+ * theme of its own, so every colour here is derived from the --ide-* tokens
6
+ * with color-mix — hardcoding a palette would have made it a twelfth theme that
7
+ * silently replaced the user's chosen one.
8
+ *
9
+ * The functional layer (title bar, activity bar, sidebar, tabs, status bar) is
10
+ * glass. The content layer — Monaco — stays solid: legibility beats the effect,
11
+ * and a blurred backdrop under a text editor costs a re-composite per repaint.
12
+ * ─────────────────────────────────────────────────────────────────────────── */
13
+ html[data-glass] {
14
+ --g-surface: color-mix(in srgb, var(--ide-panel-bg) 62%, transparent);
15
+ --g-surface-2: color-mix(in srgb, var(--ide-panel-bg) 44%, transparent);
16
+ --g-lift: color-mix(in srgb, var(--ide-hover-bg) 82%, transparent);
17
+ --g-hover: color-mix(in srgb, var(--ide-text) 12%, transparent);
18
+ --g-edge: color-mix(in srgb, var(--ide-text) 14%, transparent);
19
+ --g-edge-strong: color-mix(in srgb, var(--ide-text) 28%, transparent);
20
+ --g-shadow: 0 1px 2px rgba(0,0,0,0.28), 0 8px 28px rgba(0,0,0,0.26);
21
+ --g-blur: blur(20px) saturate(180%);
22
+
23
+ /* The field the glass refracts. Two accent-tinted pools over the theme's own
24
+ background, so every theme keeps its own hue instead of turning navy. */
25
+ --g-bg:
26
+ radial-gradient(120% 90% at 12% 0%, color-mix(in srgb, var(--ide-accent) 34%, transparent) 0%, transparent 58%),
27
+ radial-gradient(120% 110% at 82% 100%, color-mix(in srgb, var(--ide-accent-fg) 26%, transparent) 0%, transparent 60%),
28
+ var(--ide-bg);
29
+ }
30
+
31
+ html[data-glass] .ide-shell {
32
+ background: var(--g-bg) fixed;
33
+ padding: 6px 3px;
34
+ gap: 6px;
35
+ }
36
+
37
+ html[data-glass] .ide-activity-bar,
38
+ html[data-glass] .ide-sidebar,
39
+ html[data-glass] .ide-git-right-panel,
40
+ html[data-glass] .ide-main,
41
+ html[data-glass] .ide-log-drawer,
42
+ html[data-glass] .ide-problems-drawer,
43
+ html[data-glass] .ide-statusbar {
44
+ backdrop-filter: var(--g-blur);
45
+ -webkit-backdrop-filter: var(--g-blur);
46
+ background: var(--g-surface);
47
+ border: 1px solid var(--g-edge);
48
+ box-shadow: var(--g-shadow);
49
+ border-radius: 12px;
50
+ /* overflow stays hidden: it is what clips the children to the rounded
51
+ corners. The prototype set the body to `overflow: visible` so the card
52
+ shadows could spill, and that silently removed the scroll container the
53
+ search results live in. A clipped shadow is the cheaper loss. */
54
+ overflow: hidden;
55
+ }
56
+
57
+ /* The centre column is only a layout box: the editor and each bottom drawer
58
+ are separate cards with a gutter between them, like VS Code's panel. */
59
+ /* No flex gap: the 6px Gutter between the editor and a drawer is the gap. */
60
+ html[data-glass] .ide-center-column { gap: 0; background: transparent; }
61
+ html[data-glass] .ide-log-drawer,
62
+ html[data-glass] .ide-problems-drawer { border-top: 1px solid var(--g-edge); }
63
+
64
+ /* The activity bar and the sidebar are one card with a seam, not two cards
65
+ butting rounded corners together — there is no gutter between them to fill. */
66
+ html[data-glass] .ide-activity-bar { border-radius: 12px 0 0 12px; border-right: 1px solid var(--g-edge); }
67
+ html[data-glass] .ide-body:not(:has(.ide-sidebar)) .ide-activity-bar { border-radius: 12px; }
68
+ html[data-glass] .ide-sidebar { border-radius: 0 12px 12px 0; border-left: none; }
69
+
70
+ /* ── Title bar ────────────────────────────────────────────────────────────── */
71
+ /* The title bar is not a card: it sits flat on the backdrop like VS Code's,
72
+ so the first card row starts under a plain strip rather than a second box.
73
+ It carries no fill at all: a filled strip stops short of the window edges
74
+ where the cards below it are inset, and reads as a floating box. The blur
75
+ stays, and is what keeps the icons and the theme swatches off the raw
76
+ accent-tinted gradient without painting a surface. */
77
+ html[data-glass] .ide-titlebar {
78
+ background: transparent;
79
+ backdrop-filter: var(--g-blur);
80
+ -webkit-backdrop-filter: var(--g-blur);
81
+ border: none;
82
+ box-shadow: none;
83
+ height: 35px;
84
+ margin: -6px 0 -6px;
85
+ padding: 0 8px;
86
+ }
87
+ html[data-glass] .ide-titlebar-icon { color: var(--ide-accent-fg); }
88
+ html[data-glass] .ide-titlebar-title { color: var(--ide-text); font-weight: 600; }
89
+ html[data-glass] .ide-titlebar-search {
90
+ background: var(--g-surface-2);
91
+ border: 1px solid var(--g-edge);
92
+ }
93
+ html[data-glass] .ide-titlebar-search:hover { background: var(--g-hover); color: var(--ide-text); }
94
+
95
+ /* ── The gutter ───────────────────────────────────────────────────────────── */
96
+ /* Same 6px as everywhere else — a wider one reads as a gap in the layout
97
+ rather than a handle. The 3-dot grip comes from editor.css. */
98
+ html[data-glass] .ide-gutter { background: transparent; border: none; }
99
+ html[data-glass] .ide-gutter:hover,
100
+ html[data-glass] .ide-gutter.active { background: var(--g-hover); }
101
+
102
+ /* ── Tabs: same chips as Modern, on a transparent strip ───────────────────── */
103
+ html[data-glass] .tab-bar { background: transparent; border-bottom: 1px solid var(--g-edge); }
104
+ html[data-glass] .tab-item:hover { background: var(--g-hover); }
105
+ html[data-glass] .tab-item.active { background: var(--g-lift); }
106
+
107
+ /* ── Editor: the content layer stays solid ────────────────────────────────── */
108
+ html[data-glass] .ide-editor-toolbar { background: transparent; border-bottom: 1px solid var(--g-edge); }
109
+ html[data-glass] .ide-icon-btn:hover { background: var(--g-hover); }
110
+ html[data-glass] .ide-editor-wrapper { background: var(--ide-bg); }
111
+ html[data-glass] .ide-editor-wrapper .monaco-editor,
112
+ html[data-glass] .ide-editor-wrapper .monaco-editor .overflow-guard,
113
+ html[data-glass] .ide-editor-wrapper .monaco-editor-background,
114
+ html[data-glass] .ide-editor-wrapper .monaco-editor .margin { background-color: var(--ide-bg) !important; }
115
+
116
+ /* ── Status bar ───────────────────────────────────────────────────────────── */
117
+ /* Taller than Modern's 22px so the chips have room; the 6px gutter above it
118
+ stays, matching the explorer gutter, and it eats 2px of shell padding below. */
119
+ html[data-glass] .ide-statusbar { border-top: 1px solid var(--g-edge); height: 28px; margin: 0 0 -2px; }
120
+ /* The drawer-open handle lies over the 6px gutter between the editor card and
121
+ the status bar rather than adding a strip of its own above it. */
122
+ /* Sits in the shell's own 6px gap between the body and the status bar. */
123
+ html[data-glass] .ide-gutter-drawer-open { margin: -6px 0; }
124
+ /* .statusbar-version carries .statusbar-btn too, so one rule covers both. */
125
+ html[data-glass] .statusbar-btn { border-radius: 999px; padding: 0 9px; }
126
+ html[data-glass] .statusbar-btn:hover { background: var(--g-hover); }
127
+ html[data-glass] .statusbar-sep { background: var(--g-edge); }
128
+
129
+ /* ── Panels and inputs ────────────────────────────────────────────────────── */
130
+ html[data-glass] .search-panel { background: transparent; }
131
+ /* A sticky header is the one surface in the sidebar that cannot be glass: the
132
+ tree scrolls underneath it, so at 62% the rows showed straight through the
133
+ header's own label and buttons. Opaque against the sidebar it sits in. */
134
+ html[data-glass] .ide-sidebar-scrollable .collapsible-header { background: var(--ide-panel-bg); }
135
+ html[data-glass] #mbeditor-root :is(input[type="text"], input[type="search"], input[type="number"], textarea, select),
136
+ html[data-glass] .search-input {
137
+ background: var(--g-surface-2);
138
+ border: 1px solid var(--g-edge);
139
+ border-radius: 10px;
140
+ }
141
+
142
+ /* ── Quick open ───────────────────────────────────────────────────────────── */
143
+ /* The frosted box itself is the default look now (editor.css) regardless of
144
+ the glass setting — only the capsule-row treatment is glass-specific. */
145
+ html[data-glass] .quick-open-overlay { background: color-mix(in srgb, var(--ide-bg) 30%, transparent); }
146
+ html[data-glass] .quick-open-result { border-radius: 999px; margin: 0 6px; }
147
+ html[data-glass] .quick-open-result:hover { background: var(--g-hover); }
148
+
149
+ /* Reduced transparency: the same layout with solid fills. The blur is the
150
+ expensive part and the part that hurts, so it goes first. */
151
+ @media (prefers-reduced-transparency: reduce) {
152
+ html[data-glass] {
153
+ --g-surface: var(--ide-panel-bg);
154
+ --g-surface-2: var(--ide-input-bg);
155
+ --g-lift: var(--ide-hover-bg);
156
+ --g-hover: var(--ide-hover-bg);
157
+ --g-bg: var(--ide-bg);
158
+ }
159
+ html[data-glass] * {
160
+ backdrop-filter: none !important;
161
+ -webkit-backdrop-filter: none !important;
162
+ }
163
+ }
@@ -7,33 +7,47 @@
7
7
  * document.documentElement.setAttribute('data-theme', editorPrefs.theme)
8
8
  * ──────────────────────────────────────────────────────────────────────────── */
9
9
 
10
- /* ══ DEFAULT / vs-dark ══════════════════════════════════════════════════════ */
10
+ /* ══ DEFAULT / vs-dark — VS Code "Dark Modern" ══════════════════════════════
11
+ * Values are Dark Modern's, with two raised for WCAG AA: --ide-text-muted
12
+ * (#9d9d9d, ≥5.2:1 on every surface it lands on, including the inactive tab)
13
+ * and --ide-border-input (#808080, 3.29:1 on the input fill — Dark Modern's
14
+ * own #3c3c3c is 1.18:1 and effectively invisible).
15
+ * --ide-accent is the chrome blue; --ide-accent-fg is the lighter foreground
16
+ * blue, because #0078d4 as text on #1f1f1f is only 3.64:1.
17
+ * ──────────────────────────────────────────────────────────────────────────── */
11
18
  :root,
12
19
  [data-theme="vs-dark"] {
13
20
  color-scheme: dark;
14
- --status-hover-bg: rgba(255,255,255,0.08);
15
- --ide-accent-text: #ffffff;
21
+ --status-hover-bg: rgba(255,255,255,0.10);
22
+ /* Chrome foreground: the title bar and status bar are panel-coloured in
23
+ Dark Modern, not accent-filled, so this is a text colour, not "on blue". */
24
+ --ide-accent-text: #cccccc;
25
+ /* Status bar while migrations are pending. Both halves are per-theme:
26
+ * --ide-warning is authored as a foreground colour, so several themes
27
+ * pick a dark ochre that is unreadable as a bar background. */
28
+ --ide-statusbar-warn-bg: #cca700;
29
+ --ide-statusbar-warn-text: #1e1e1e;
16
30
 
17
31
  /* Backgrounds */
18
- --ide-bg: #1e1e1e;
19
- --ide-panel-bg: #252526;
20
- --ide-panel-bg-alt: #202124;
21
- --ide-hover-bg: #2a2d2e;
22
- --ide-active-bg: #094771;
23
- --ide-input-bg: #3c3c3c;
32
+ --ide-bg: #1f1f1f;
33
+ --ide-panel-bg: #181818;
34
+ --ide-panel-bg-alt: #202020;
35
+ --ide-hover-bg: #2a2a2a;
36
+ --ide-active-bg: #04395e;
37
+ --ide-input-bg: #313131;
24
38
 
25
39
  /* Borders */
26
- --ide-border: #3c3c3c;
27
- --ide-border-input: #555555;
40
+ --ide-border: #2b2b2b;
41
+ --ide-border-input: #808080;
28
42
 
29
43
  /* Text */
30
44
  --ide-text: #cccccc;
31
45
  --ide-text-dim: #bbbbbb;
32
- --ide-text-muted: #888888;
46
+ --ide-text-muted: #9d9d9d;
33
47
 
34
48
  /* Accent */
35
- --ide-accent: #007acc;
36
- --ide-accent-fg: #4ec9b0;
49
+ --ide-accent: #0078d4;
50
+ --ide-accent-fg: #4daafc;
37
51
 
38
52
  /* Status */
39
53
  --ide-success: #89d185;
@@ -41,37 +55,43 @@
41
55
  --ide-warning: #f9a825;
42
56
 
43
57
  /* Misc */
44
- --ide-scrollbar: #424242;
58
+ --ide-scrollbar: #3c3c3c;
45
59
  --ide-graph-line: #555555;
46
60
  }
47
61
 
48
- /* ══ LIGHT / vs ═════════════════════════════════════════════════════════════ */
62
+ /* ══ LIGHT / vs — VS Code "Light Modern" ════════════════════════════════════
63
+ * Same treatment as Dark Modern: --ide-text-muted #646464 (≥4.8:1 on bg,
64
+ * panel, hover and the inactive tab), --ide-border-input #8a8a8a (3.45:1 on
65
+ * white), and warning/success darkened to clear 4.5:1 on white.
66
+ * ──────────────────────────────────────────────────────────────────────────── */
49
67
  [data-theme="vs"] {
50
68
  color-scheme: light;
51
- --ide-accent-text: #ffffff;
52
- --status-hover-bg: rgba(0,0,0,0.06);
69
+ --ide-accent-text: #3b3b3b;
70
+ --status-hover-bg: rgba(0,0,0,0.08);
71
+ --ide-statusbar-warn-bg: #e9a700;
72
+ --ide-statusbar-warn-text: #1f1f1f;
53
73
  --ide-bg: #ffffff;
54
- --ide-panel-bg: #f3f3f3;
55
- --ide-panel-bg-alt: #ececec;
74
+ --ide-panel-bg: #f8f8f8;
75
+ --ide-panel-bg-alt: #f0f0f0;
56
76
  --ide-hover-bg: #e8e8e8;
57
- --ide-active-bg: #d4e6f5;
77
+ --ide-active-bg: #e4e6f1;
58
78
  --ide-input-bg: #ffffff;
59
79
 
60
- --ide-border: #cecece;
61
- --ide-border-input: #bdbdbd;
80
+ --ide-border: #e5e5e5;
81
+ --ide-border-input: #8a8a8a;
62
82
 
63
- --ide-text: #333333;
83
+ --ide-text: #3b3b3b;
64
84
  --ide-text-dim: #616161;
65
- --ide-text-muted: #999999;
85
+ --ide-text-muted: #646464;
66
86
 
67
- --ide-accent: #007acc;
68
- --ide-accent-fg: #0070c1;
87
+ --ide-accent: #005fb8;
88
+ --ide-accent-fg: #005fb8;
69
89
 
70
- --ide-success: #388a34;
90
+ --ide-success: #2e7d31;
71
91
  --ide-danger: #d32f2f;
72
- --ide-warning: #b5890a;
92
+ --ide-warning: #8a6800;
73
93
 
74
- --ide-scrollbar: #c0c0c0;
94
+ --ide-scrollbar: #cecece;
75
95
  --ide-graph-line: #bdbdbd;
76
96
  }
77
97
 
@@ -79,6 +99,8 @@
79
99
  [data-theme="hc-black"] {
80
100
  color-scheme: dark;
81
101
  --ide-accent-text: #000000;
102
+ --ide-statusbar-warn-bg: #ffd700;
103
+ --ide-statusbar-warn-text: #000000;
82
104
  --status-hover-bg: rgba(255,255,255,0.12);
83
105
  --ide-bg: #000000;
84
106
  --ide-panel-bg: #0a0a0a;
@@ -109,6 +131,8 @@
109
131
  [data-theme="hc-light"] {
110
132
  color-scheme: light;
111
133
  --ide-accent-text: #ffffff;
134
+ --ide-statusbar-warn-bg: #7d4f01;
135
+ --ide-statusbar-warn-text: #ffffff;
112
136
  --status-hover-bg: rgba(0,0,0,0.06);
113
137
  --ide-bg: #ffffff;
114
138
  --ide-panel-bg: #f8f8f8;
@@ -139,6 +163,8 @@
139
163
  [data-theme="dracula"] {
140
164
  color-scheme: dark;
141
165
  --ide-accent-text: #ffffff;
166
+ --ide-statusbar-warn-bg: #ffb86c;
167
+ --ide-statusbar-warn-text: #282a36;
142
168
  --status-hover-bg: rgba(255,255,255,0.08);
143
169
  --ide-bg: #282a36;
144
170
  --ide-panel-bg: #21222c;
@@ -169,6 +195,8 @@
169
195
  [data-theme="night-owl"] {
170
196
  color-scheme: dark;
171
197
  --ide-accent-text: #ffffff;
198
+ --ide-statusbar-warn-bg: #ffcb8b;
199
+ --ide-statusbar-warn-text: #011627;
172
200
  --status-hover-bg: rgba(255,255,255,0.08);
173
201
  --ide-bg: #011627;
174
202
  --ide-panel-bg: #01111d;
@@ -199,6 +227,8 @@
199
227
  [data-theme="monokai"] {
200
228
  color-scheme: dark;
201
229
  --ide-accent-text: #000000;
230
+ --ide-statusbar-warn-bg: #fd971f;
231
+ --ide-statusbar-warn-text: #272822;
202
232
  --status-hover-bg: rgba(255,255,255,0.08);
203
233
  --ide-bg: #272822;
204
234
  --ide-panel-bg: #1e1f1c;
@@ -229,6 +259,8 @@
229
259
  [data-theme="nord"] {
230
260
  color-scheme: dark;
231
261
  --ide-accent-text: #000000;
262
+ --ide-statusbar-warn-bg: #ebcb8b;
263
+ --ide-statusbar-warn-text: #2e3440;
232
264
  --status-hover-bg: rgba(255,255,255,0.08);
233
265
  --ide-bg: #2e3440;
234
266
  --ide-panel-bg: #252a34;
@@ -259,6 +291,8 @@
259
291
  [data-theme="github-dark"] {
260
292
  color-scheme: dark;
261
293
  --ide-accent-text: #ffffff;
294
+ --ide-statusbar-warn-bg: #d29922;
295
+ --ide-statusbar-warn-text: #0d1117;
262
296
  --status-hover-bg: rgba(255,255,255,0.08);
263
297
  --ide-bg: #0d1117;
264
298
  --ide-panel-bg: #161b22;
@@ -289,6 +323,8 @@
289
323
  [data-theme="tomorrow-night"] {
290
324
  color-scheme: dark;
291
325
  --ide-accent-text: #ffffff;
326
+ --ide-statusbar-warn-bg: #de935f;
327
+ --ide-statusbar-warn-text: #1d1f21;
292
328
  --status-hover-bg: rgba(255,255,255,0.08);
293
329
  --ide-bg: #1d1f21;
294
330
  --ide-panel-bg: #161719;
@@ -319,6 +355,8 @@
319
355
  [data-theme="github-light"] {
320
356
  color-scheme: light;
321
357
  --ide-accent-text: #ffffff;
358
+ --ide-statusbar-warn-bg: #9a6700;
359
+ --ide-statusbar-warn-text: #ffffff;
322
360
  --status-hover-bg: rgba(0,0,0,0.06);
323
361
  --ide-bg: #ffffff;
324
362
  --ide-panel-bg: #f6f8fa;
@@ -483,3 +521,25 @@
483
521
  :root[data-theme="vs"],
484
522
  :root[data-theme="hc-light"],
485
523
  :root[data-theme="github-light"] { color-scheme: light; }
524
+
525
+ /* ══ CHROME SURFACE TOKENS ══════════════════════════════════════════════════
526
+ * The title bar and status bar are accent-filled in the nine legacy themes and
527
+ * panel-coloured in the two Modern ones, so the status bar reads its fill from
528
+ * a token rather than from --ide-accent directly. --ide-on-accent is the text
529
+ * colour for the few things that really do sit on an accent fill (badges,
530
+ * primary buttons); it tracks --ide-accent-text everywhere except Modern,
531
+ * where --ide-accent-text became a chrome text colour rather than "on blue".
532
+ *
533
+ * Specificity: :root and :root[data-theme=…] tie on the rule below only
534
+ * because the Modern block comes last; keep that order.
535
+ * ─────────────────────────────────────────────────────────────────────────── */
536
+ :root {
537
+ --ide-statusbar-bg: var(--ide-accent);
538
+ --ide-on-accent: var(--ide-accent-text);
539
+ }
540
+
541
+ :root[data-theme="vs-dark"],
542
+ :root[data-theme="vs"] {
543
+ --ide-statusbar-bg: var(--ide-panel-bg);
544
+ --ide-on-accent: #ffffff;
545
+ }
@@ -21,27 +21,40 @@ module Mbeditor
21
21
  return reject if @path.empty? && respond_to?(:reject, true)
22
22
 
23
23
  stream_from stream_name if respond_to?(:stream_from)
24
+ CollaborationDocStore.join(room_key)
25
+ @joined = true
24
26
  transmit_initial_state
25
27
  end
26
28
 
27
29
  def unsubscribed
28
- # no-op
30
+ CollaborationDocStore.leave(room_key) if @joined
31
+ @joined = false
29
32
  end
30
33
 
31
34
  def doc_update(data)
32
35
  bytes = data["update"]
33
36
  return if bytes.nil?
34
37
 
35
- CollaborationDocStore.record_update(room_key, bytes)
36
- relay("type" => "doc_update", "update" => bytes)
38
+ seq = CollaborationDocStore.record_update(room_key, bytes)
39
+ relay("type" => "doc_update", "update" => bytes, "seq" => seq)
37
40
  end
38
41
 
39
42
  def snapshot(data)
40
43
  bytes = data["snapshot"]
41
44
  return if bytes.nil?
42
45
 
43
- CollaborationDocStore.replace_snapshot(room_key, bytes)
44
- relay("type" => "snapshot", "snapshot" => bytes)
46
+ applied_seq = data["applied_seq"]
47
+ CollaborationDocStore.replace_snapshot(room_key, bytes, applied_seq: applied_seq)
48
+ relay("type" => "snapshot", "snapshot" => bytes, "applied_seq" => applied_seq)
49
+ end
50
+
51
+ # A client whose delta replay was incomplete (missing dependencies after the
52
+ # buffer overflowed, or a room restarted from scratch) asks the room for a
53
+ # full snapshot. It is relayed to the other subscribers; any bound peer
54
+ # answers with its own state via #snapshot. The requester attaches once that
55
+ # lands (see CollaborationService).
56
+ def request_snapshot(_data = nil)
57
+ relay("type" => "request_snapshot")
45
58
  end
46
59
 
47
60
  def awareness(data)
@@ -66,7 +79,13 @@ module Mbeditor
66
79
  return unless respond_to?(:transmit, true)
67
80
 
68
81
  state = CollaborationDocStore.state_for(room_key)
69
- transmit({ "type" => "sync", "snapshot" => state[:snapshot], "deltas" => state[:deltas] })
82
+ # Exactly one client per empty room is told to seed it from disk; see
83
+ # CollaborationDocStore.claim_seed. Everyone else waits for that content.
84
+ # delta_seqs lets the joiner track how far the replay reaches, so its own
85
+ # later snapshot can tell the server which deltas to keep.
86
+ transmit({ "type" => "sync", "snapshot" => state[:snapshot], "deltas" => state[:deltas],
87
+ "delta_seqs" => state[:delta_seqs], "snapshot_seq" => state[:snapshot_seq],
88
+ "seed" => CollaborationDocStore.claim_seed(room_key) })
70
89
  end
71
90
 
72
91
  # A room is identified by workspace *and* relative path. Keying on the path
@@ -1,9 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require "pathname"
5
-
6
3
  module Mbeditor
4
+ # No `rescue_from StandardError` here, and the repeated
5
+ # `rescue StandardError => e; render json: { error: e.message },
6
+ # status: :unprocessable_content` at the foot of ~29 actions is deliberate,
7
+ # not something waiting to be dried up. Most actions in this engine have no
8
+ # rescue at all and rely on an unhandled error surfacing as a real 500:
9
+ # resilient_routing_test.rb asserts exactly that, to prove ResilientRouter
10
+ # sits below Rails' own exception middleware. A blanket handler swallows that
11
+ # signal for 18 actions here plus `tail` in LogsController. Collapsing it to
12
+ # one handler plus a per-controller allowlist was tried and reverted: it saved
13
+ # 21 lines, moved each action's error behaviour a thousand lines away from the
14
+ # action, and left the same action names duplicated in the allowlist.
7
15
  class ApplicationController < ActionController::Base
8
16
  protect_from_forgery with: :exception
9
17
  # Before the auth hook, and on every controller: a disallowed environment is
@@ -12,6 +20,9 @@ module Mbeditor
12
20
  # missed it entirely and served the environment's log file anywhere.
13
21
  before_action :ensure_allowed_environment!
14
22
  before_action :run_authentication
23
+ # Registered after the two gates above so it times the action itself, not
24
+ # the environment check or the host app's authenticate_with proc.
25
+ around_action :audit_request
15
26
 
16
27
  private
17
28
 
@@ -32,6 +43,18 @@ module Mbeditor
32
43
  render plain: 'Not found', status: :not_found unless allowed.include?(Rails.env.to_sym)
33
44
  end
34
45
 
46
+ def audit_request
47
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
48
+ yield
49
+ ensure
50
+ # One record per request: `action` is a fixed method name of this
51
+ # controller, never request data; `ms` wall time; `status` the HTTP code.
52
+ AuditLog.record(:request,
53
+ action: action_name.to_sym,
54
+ ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round,
55
+ status: response.status)
56
+ end
57
+
35
58
  def workspace_root
36
59
  WorkspaceRootResolver.call
37
60
  end