mrmd-editor 0.6.0 → 0.7.1

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.
@@ -69,12 +69,12 @@ function getInitialState() {
69
69
  runtimes: {
70
70
  // Legacy: single Python runtime info (for backward compat)
71
71
  python: null,
72
- // New: multiple runtime sessions
72
+ // Runtime registry
73
73
  sessions: {
74
74
  // 'shared': { id, url, status, venv, cwd, ... }
75
75
  // 'python-8001': { id, url, status, venv, cwd, dedicated: true, port: 8001 }
76
76
  },
77
- // Document → session attachment
77
+ // Document → runtime attachment
78
78
  attachments: {
79
79
  // 'my-notebook': 'shared'
80
80
  // 'data-analysis': 'python-8001'
@@ -464,55 +464,55 @@ export class ShellStateManager {
464
464
  }
465
465
 
466
466
  // ===========================================================================
467
- // Runtime Session Management
467
+ // Runtime Attachment Management
468
468
  // ===========================================================================
469
469
 
470
470
  /**
471
- * Get the session attached to a document
471
+ * Get the runtime attached to a document
472
472
  * @param {string} docName - Document name
473
- * @returns {string} Session ID (defaults to 'shared')
473
+ * @returns {string} Runtime ID (defaults to 'shared')
474
474
  */
475
- getDocumentSession(docName) {
475
+ getDocumentRuntime(docName) {
476
476
  return this.get(`runtimes.attachments.${docName}`) || 'shared';
477
477
  }
478
478
 
479
479
  /**
480
- * Get session info
481
- * @param {string} sessionId - Session ID
480
+ * Get runtime info
481
+ * @param {string} runtimeId - Runtime ID
482
482
  * @returns {Object|null}
483
483
  */
484
- getSession(sessionId) {
485
- return this.get(`runtimes.sessions.${sessionId}`) || null;
484
+ getRuntime(runtimeId) {
485
+ return this.get(`runtimes.sessions.${runtimeId}`) || null;
486
486
  }
487
487
 
488
488
  /**
489
- * Get all available sessions
489
+ * Get all available runtimes
490
490
  * @returns {Array<{id: string, info: Object}>}
491
491
  */
492
- getSessions() {
493
- const sessions = this.get('runtimes.sessions') || {};
494
- return Object.entries(sessions).map(([id, info]) => ({ id, info }));
492
+ getRuntimes() {
493
+ const runtimes = this.get('runtimes.sessions') || {};
494
+ return Object.entries(runtimes).map(([id, info]) => ({ id, info }));
495
495
  }
496
496
 
497
497
  /**
498
- * Attach a document to a session
498
+ * Attach a document to a runtime
499
499
  * @param {string} docName - Document name
500
- * @param {string} sessionId - Session ID to attach to
500
+ * @param {string} runtimeId - Runtime ID to attach to
501
501
  */
502
- attachDocument(docName, sessionId) {
503
- this._set(`runtimes.attachments.${docName}`, sessionId);
502
+ attachDocument(docName, runtimeId) {
503
+ this._set(`runtimes.attachments.${docName}`, runtimeId);
504
504
  }
505
505
 
506
506
  /**
507
- * Register a runtime session
508
- * @param {string} sessionId - Session ID
509
- * @param {Object} info - Session info (url, status, venv, cwd, etc.)
507
+ * Register runtime metadata
508
+ * @param {string} runtimeId - Runtime ID
509
+ * @param {Object} info - Runtime info (url, status, venv, cwd, etc.)
510
510
  */
511
- registerSession(sessionId, info) {
512
- this._set(`runtimes.sessions.${sessionId}`, info);
511
+ registerRuntime(runtimeId, info) {
512
+ this._set(`runtimes.sessions.${runtimeId}`, info);
513
513
 
514
- // Also update legacy python state if this is the shared session
515
- if (sessionId === 'shared' && info.language === 'python') {
514
+ // Also update legacy python state if this is the shared runtime
515
+ if (runtimeId === 'shared' && info.language === 'python') {
516
516
  this._set('runtimes.python', {
517
517
  language: 'python',
518
518
  version: info.version,
@@ -527,20 +527,20 @@ export class ShellStateManager {
527
527
  }
528
528
 
529
529
  /**
530
- * Create a new dedicated runtime session
531
- * @param {string} docName - Document to attach the session to
530
+ * Create and attach a runtime
531
+ * @param {string} docName - Document to attach runtime to
532
532
  * @param {'shared'|'dedicated'} mode - Runtime mode
533
533
  * @param {string} [venv] - Path to virtual environment (for dedicated runtimes)
534
- * @returns {Promise<Object>} Session info
534
+ * @returns {Promise<Object>} Runtime info
535
535
  */
536
- async createSession(docName, mode = 'dedicated', venv = null) {
536
+ async createRuntime(docName, mode = 'dedicated', venv = null) {
537
537
  try {
538
- const result = await this._client.createSession(docName, mode, venv);
538
+ const result = await this._client.createRuntimeAttachment(docName, mode, venv);
539
539
 
540
- const sessionId = result.id || (mode === 'dedicated' ? `python-${result.runtimes?.python?.port || Date.now()}` : 'shared');
540
+ const runtimeId = result.id || (mode === 'dedicated' ? `python-${result.runtimes?.python?.port || Date.now()}` : 'shared');
541
541
 
542
- const sessionInfo = {
543
- id: sessionId,
542
+ const runtimeInfo = {
543
+ id: runtimeId,
544
544
  url: result.runtimes?.python?.url || result.sync,
545
545
  status: 'ready',
546
546
  dedicated: mode === 'dedicated',
@@ -550,15 +550,15 @@ export class ShellStateManager {
550
550
  docName,
551
551
  };
552
552
 
553
- // Register the session
554
- this.registerSession(sessionId, sessionInfo);
553
+ // Register runtime
554
+ this.registerRuntime(runtimeId, runtimeInfo);
555
555
 
556
- // Attach the document to this session
557
- this.attachDocument(docName, sessionId);
556
+ // Attach document to runtime
557
+ this.attachDocument(docName, runtimeId);
558
558
 
559
- return sessionInfo;
559
+ return runtimeInfo;
560
560
  } catch (error) {
561
- console.error('Failed to create session:', error);
561
+ console.error('Failed to create runtime:', error);
562
562
  throw error;
563
563
  }
564
564
  }
@@ -569,9 +569,30 @@ export class ShellStateManager {
569
569
  * @returns {string|null} Runtime URL
570
570
  */
571
571
  getRuntimeUrl(docName) {
572
- const sessionId = this.getDocumentSession(docName);
573
- const session = this.getSession(sessionId);
574
- return session?.url || null;
572
+ const runtimeId = this.getDocumentRuntime(docName);
573
+ const runtime = this.getRuntime(runtimeId);
574
+ return runtime?.url || null;
575
+ }
576
+
577
+ // Legacy aliases
578
+ getDocumentSession(docName) {
579
+ return this.getDocumentRuntime(docName);
580
+ }
581
+
582
+ getSession(sessionId) {
583
+ return this.getRuntime(sessionId);
584
+ }
585
+
586
+ getSessions() {
587
+ return this.getRuntimes();
588
+ }
589
+
590
+ registerSession(sessionId, info) {
591
+ this.registerRuntime(sessionId, info);
592
+ }
593
+
594
+ async createSession(docName, mode = 'dedicated', venv = null) {
595
+ return this.createRuntime(docName, mode, venv);
575
596
  }
576
597
 
577
598
  // ===========================================================================
@@ -160,6 +160,24 @@ export const statusBarStyles = `
160
160
  margin-left: 4px;
161
161
  }
162
162
 
163
+ /* Active machine pill (simple status bar mode) */
164
+ .mrmd-statusbar__machine-pill {
165
+ display: inline-flex;
166
+ align-items: center;
167
+ padding: 1px 6px;
168
+ border-radius: 10px;
169
+ border: 1px solid var(--mrmd-border, #333);
170
+ background: var(--mrmd-hover-bg, rgba(255,255,255,0.06));
171
+ color: var(--mrmd-fg, #ccc);
172
+ font-size: 10px;
173
+ line-height: 1.2;
174
+ cursor: pointer;
175
+ }
176
+
177
+ .mrmd-statusbar__machine-pill:hover {
178
+ border-color: var(--mrmd-accent, #58a6ff);
179
+ }
180
+
163
181
  /* Unified files segment - takes more space */
164
182
  .mrmd-statusbar__segment--files {
165
183
  min-width: 120px;
@@ -505,6 +523,50 @@ export const filePickerStyles = `
505
523
  min-height: 300px;
506
524
  }
507
525
 
526
+ /* Machine tab bar */
527
+ .mrmd-filepicker__machines {
528
+ display: flex;
529
+ gap: 4px;
530
+ padding: 6px 0;
531
+ margin-bottom: 4px;
532
+ border-bottom: 1px solid var(--mrmd-border, #333);
533
+ overflow-x: auto;
534
+ white-space: nowrap;
535
+ -webkit-overflow-scrolling: touch;
536
+ }
537
+
538
+ .mrmd-filepicker__machine-tab {
539
+ display: inline-flex;
540
+ align-items: center;
541
+ gap: 4px;
542
+ padding: 4px 10px;
543
+ border-radius: 12px;
544
+ border: 1px solid var(--mrmd-border, #333);
545
+ background: transparent;
546
+ color: var(--mrmd-fg-muted, #888);
547
+ font-size: var(--mrmd-ui-font-size-sm, 11px);
548
+ font-family: inherit;
549
+ cursor: pointer;
550
+ white-space: nowrap;
551
+ transition: background 0.1s, color 0.1s, border-color 0.1s;
552
+ }
553
+
554
+ .mrmd-filepicker__machine-tab:hover {
555
+ background: var(--mrmd-hover-bg, rgba(255, 255, 255, 0.08));
556
+ color: var(--mrmd-fg, #ccc);
557
+ }
558
+
559
+ .mrmd-filepicker__machine-tab--active {
560
+ background: var(--mrmd-selection-bg, rgba(0, 122, 204, 0.2));
561
+ color: var(--mrmd-fg, #ccc);
562
+ border-color: var(--mrmd-accent, #58a6ff);
563
+ }
564
+
565
+ .mrmd-filepicker__machine-tab--offline {
566
+ opacity: 0.7;
567
+ border-style: dashed;
568
+ }
569
+
508
570
  /* Path bar */
509
571
  .mrmd-filepicker__path {
510
572
  display: flex;
@@ -601,6 +663,271 @@ export const filePickerStyles = `
601
663
  }
602
664
  `;
603
665
 
666
+ // =============================================================================
667
+ // MOBILE RESPONSIVE STYLES
668
+ // =============================================================================
669
+
670
+ export const mobileStyles = `
671
+ /* ---- Status Bar: mobile-friendly ---- */
672
+ @media (max-width: 768px) {
673
+ .mrmd-statusbar {
674
+ height: auto;
675
+ min-height: 44px;
676
+ padding: 8px 12px;
677
+ gap: 4px;
678
+ font-size: 13px;
679
+ flex-wrap: wrap;
680
+ padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
681
+ }
682
+
683
+ .mrmd-statusbar__segment {
684
+ padding: 8px 12px;
685
+ min-height: 44px;
686
+ font-size: 14px;
687
+ }
688
+
689
+ .mrmd-statusbar__segment--files {
690
+ min-width: 0;
691
+ flex: 1;
692
+ }
693
+
694
+ .mrmd-statusbar__icon {
695
+ width: 18px;
696
+ height: 18px;
697
+ }
698
+
699
+ .mrmd-statusbar__dot {
700
+ width: 10px;
701
+ height: 10px;
702
+ }
703
+
704
+ .mrmd-statusbar__badge {
705
+ font-size: 10px;
706
+ padding: 2px 6px;
707
+ }
708
+
709
+ .mrmd-statusbar__separator {
710
+ display: none;
711
+ }
712
+
713
+ .mrmd-statusbar__spacer {
714
+ display: none;
715
+ }
716
+ }
717
+
718
+ /* ---- Menus → Bottom sheets on mobile ---- */
719
+ @media (max-width: 768px) {
720
+ .mrmd-menu {
721
+ position: fixed !important;
722
+ top: auto !important;
723
+ left: 0 !important;
724
+ right: 0 !important;
725
+ bottom: 0 !important;
726
+ max-width: 100% !important;
727
+ min-width: 100% !important;
728
+ max-height: 70vh;
729
+ border-radius: 16px 16px 0 0;
730
+ box-shadow: 0 -4px 32px rgba(0, 0, 0, 0.4);
731
+ padding: 8px 0;
732
+ padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
733
+ animation: mrmd-slide-up 0.25s cubic-bezier(0.4, 0, 0.2, 1);
734
+ }
735
+
736
+ @keyframes mrmd-slide-up {
737
+ from { transform: translateY(100%); }
738
+ to { transform: translateY(0); }
739
+ }
740
+
741
+ /* Drag handle at top of bottom sheet */
742
+ .mrmd-menu::before {
743
+ content: '';
744
+ display: block;
745
+ width: 36px;
746
+ height: 4px;
747
+ background: var(--mrmd-fg-muted, #666);
748
+ border-radius: 2px;
749
+ margin: 4px auto 8px;
750
+ opacity: 0.4;
751
+ }
752
+
753
+ .mrmd-menu__header {
754
+ padding: 12px 20px 8px;
755
+ font-size: 12px;
756
+ }
757
+
758
+ .mrmd-menu__item {
759
+ padding: 14px 20px;
760
+ min-height: 48px;
761
+ font-size: 15px;
762
+ gap: 14px;
763
+ }
764
+
765
+ .mrmd-menu__item-icon {
766
+ width: 22px;
767
+ height: 22px;
768
+ font-size: 18px;
769
+ }
770
+
771
+ .mrmd-menu__item-shortcut {
772
+ display: none; /* Keyboard shortcuts aren't relevant on touch */
773
+ }
774
+
775
+ .mrmd-menu__info {
776
+ padding: 10px 20px;
777
+ font-size: 12px;
778
+ }
779
+
780
+ .mrmd-menu__divider {
781
+ margin: 6px 16px;
782
+ }
783
+
784
+ .mrmd-menu__file {
785
+ padding: 12px 20px;
786
+ min-height: 48px;
787
+ font-size: 15px;
788
+ }
789
+ }
790
+
791
+ /* ---- Dialogs: full-screen on mobile ---- */
792
+ @media (max-width: 768px) {
793
+ .mrmd-dialog-overlay {
794
+ padding: 0;
795
+ align-items: flex-end;
796
+ }
797
+
798
+ .mrmd-dialog {
799
+ min-width: 100%;
800
+ max-width: 100%;
801
+ max-height: 90vh;
802
+ border-radius: 16px 16px 0 0;
803
+ border: none;
804
+ border-top: 1px solid var(--mrmd-dialog-border, var(--mrmd-border, #454545));
805
+ animation: mrmd-slide-up 0.3s cubic-bezier(0.4, 0, 0.2, 1);
806
+ }
807
+
808
+ .mrmd-dialog__header {
809
+ padding: 16px 20px;
810
+ position: relative;
811
+ }
812
+
813
+ /* Drag handle */
814
+ .mrmd-dialog__header::before {
815
+ content: '';
816
+ position: absolute;
817
+ top: 8px;
818
+ left: 50%;
819
+ transform: translateX(-50%);
820
+ width: 36px;
821
+ height: 4px;
822
+ background: var(--mrmd-fg-muted, #888);
823
+ border-radius: 2px;
824
+ opacity: 0.4;
825
+ }
826
+
827
+ .mrmd-dialog__title {
828
+ font-size: 16px;
829
+ padding-top: 8px;
830
+ }
831
+
832
+ .mrmd-dialog__close {
833
+ width: 44px;
834
+ height: 44px;
835
+ font-size: 22px;
836
+ }
837
+
838
+ .mrmd-dialog__body {
839
+ padding: 16px 20px;
840
+ }
841
+
842
+ .mrmd-dialog__footer {
843
+ padding: 16px 20px;
844
+ padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
845
+ gap: 12px;
846
+ }
847
+
848
+ .mrmd-input {
849
+ padding: 12px 14px;
850
+ font-size: 16px; /* Prevents iOS zoom on focus */
851
+ border-radius: 8px;
852
+ }
853
+
854
+ .mrmd-button {
855
+ padding: 12px 20px;
856
+ font-size: 15px;
857
+ border-radius: 8px;
858
+ min-height: 44px;
859
+ }
860
+ }
861
+
862
+ /* ---- File Picker: full-height on mobile ---- */
863
+ @media (max-width: 768px) {
864
+ .mrmd-filepicker {
865
+ min-height: 50vh;
866
+ }
867
+
868
+ .mrmd-filepicker__path {
869
+ padding: 12px 0;
870
+ gap: 2px;
871
+ }
872
+
873
+ .mrmd-filepicker__path-segment {
874
+ padding: 6px 10px;
875
+ font-size: 14px;
876
+ min-height: 36px;
877
+ }
878
+
879
+ .mrmd-filepicker__item {
880
+ padding: 14px 12px;
881
+ min-height: 52px;
882
+ font-size: 15px;
883
+ gap: 14px;
884
+ }
885
+
886
+ .mrmd-filepicker__item-icon {
887
+ font-size: 20px;
888
+ width: 28px;
889
+ }
890
+
891
+ .mrmd-filepicker__item-info {
892
+ font-size: 12px;
893
+ }
894
+ }
895
+
896
+ /* ---- Touch-specific: larger targets for coarse pointers ---- */
897
+ @media (pointer: coarse) {
898
+ .mrmd-statusbar__segment {
899
+ min-height: 44px;
900
+ }
901
+
902
+ .mrmd-menu__item {
903
+ min-height: 48px;
904
+ }
905
+
906
+ .mrmd-dialog__close {
907
+ min-width: 44px;
908
+ min-height: 44px;
909
+ }
910
+
911
+ .mrmd-button {
912
+ min-height: 44px;
913
+ }
914
+
915
+ .mrmd-filepicker__item {
916
+ min-height: 48px;
917
+ }
918
+
919
+ .mrmd-filepicker__path-segment {
920
+ min-height: 36px;
921
+ }
922
+ }
923
+
924
+ /* ---- Shared mobile animation keyframes ---- */
925
+ @keyframes mrmd-fade-in {
926
+ from { opacity: 0; }
927
+ to { opacity: 1; }
928
+ }
929
+ `;
930
+
604
931
  // =============================================================================
605
932
  // ALL STYLES COMBINED
606
933
  // =============================================================================
@@ -672,6 +999,7 @@ ${menuStyles}
672
999
  ${dialogStyles}
673
1000
  ${filePickerStyles}
674
1001
  ${studioStyles}
1002
+ ${mobileStyles}
675
1003
  `;
676
1004
 
677
1005
  // =============================================================================
@@ -124,7 +124,20 @@ export class PtyClient {
124
124
  };
125
125
 
126
126
  this.ws.onmessage = (event) => {
127
- this.config.onData(event.data);
127
+ // Handle both text and binary messages. Text is the normal case
128
+ // (PTY sends terminal output as text). Binary may arrive if the
129
+ // tunnel proxy incorrectly marks a frame as binary — convert to
130
+ // string so xterm.js can render it (xterm doesn't handle Blobs).
131
+ const data = event.data;
132
+ if (typeof data === 'string') {
133
+ this.config.onData(data);
134
+ } else if (data instanceof Blob) {
135
+ data.text().then(text => this.config.onData(text));
136
+ } else if (data instanceof ArrayBuffer) {
137
+ this.config.onData(new TextDecoder().decode(data));
138
+ } else {
139
+ this.config.onData(data);
140
+ }
128
141
  };
129
142
 
130
143
  this.ws.onerror = (event) => {
@@ -172,16 +185,58 @@ export class PtyClient {
172
185
  }
173
186
 
174
187
  /**
175
- * Send data to the PTY (user input)
188
+ * Send data to the PTY (user input).
189
+ * Large payloads (pastes) are chunked to avoid overwhelming the terminal
190
+ * renderer and WebSocket tunnel on markco.dev.
191
+ *
176
192
  * @param {string} data - Input data
177
193
  */
178
194
  write(data) {
179
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
180
- this.ws.send(JSON.stringify({
181
- type: 'input',
182
- data: data,
183
- }));
195
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
196
+
197
+ // Small inputs (normal typing, control sequences): send immediately
198
+ if (data.length <= 4096) {
199
+ this.ws.send(JSON.stringify({ type: 'input', data }));
200
+ return;
201
+ }
202
+
203
+ // Large paste: chunk it to prevent UI freeze.
204
+ // xterm.js may wrap pastes in bracketed paste markers (\x1b[200~ ... \x1b[201~).
205
+ // We preserve those: send a leading marker once, chunk the body, send trailing marker.
206
+ const BPS = '\x1b[200~';
207
+ const BPE = '\x1b[201~';
208
+ let body = data;
209
+ let hasBracket = false;
210
+
211
+ if (body.startsWith(BPS) && body.endsWith(BPE)) {
212
+ hasBracket = true;
213
+ body = body.slice(BPS.length, -BPE.length);
184
214
  }
215
+
216
+ const CHUNK_SIZE = 2048;
217
+ const CHUNK_DELAY_MS = 6;
218
+ let offset = 0;
219
+ const ws = this.ws;
220
+
221
+ if (hasBracket) {
222
+ ws.send(JSON.stringify({ type: 'input', data: BPS }));
223
+ }
224
+
225
+ const sendNext = () => {
226
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
227
+ if (offset >= body.length) {
228
+ if (hasBracket) {
229
+ ws.send(JSON.stringify({ type: 'input', data: BPE }));
230
+ }
231
+ return;
232
+ }
233
+ const chunk = body.slice(offset, offset + CHUNK_SIZE);
234
+ offset += CHUNK_SIZE;
235
+ ws.send(JSON.stringify({ type: 'input', data: chunk }));
236
+ setTimeout(sendNext, CHUNK_DELAY_MS);
237
+ };
238
+
239
+ sendNext();
185
240
  }
186
241
 
187
242
  /**