@nimbus-sh/worker 0.1.3 → 0.1.5

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.
@@ -101,6 +101,48 @@
101
101
  .preview-bar button { background: none; border: none; color: #8b949e; cursor: pointer; font-size: 14px; padding: 2px 4px; }
102
102
  .preview-bar button:hover { color: #c9d1d9; }
103
103
  #preview-frame { flex: 1; border: none; background: white; width: 100%; }
104
+ #markdown-preview {
105
+ display: none; flex: 1; min-height: 0; overflow: auto;
106
+ background: #0d1117; color: #c9d1d9;
107
+ }
108
+ #markdown-preview.active { display: block; }
109
+ .markdown-body {
110
+ max-width: 860px; margin: 0 auto; padding: 34px 44px 56px;
111
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
112
+ font-size: 14px; line-height: 1.65;
113
+ }
114
+ .markdown-body h1, .markdown-body h2, .markdown-body h3 {
115
+ color: #f0f6fc; line-height: 1.25; margin: 24px 0 12px;
116
+ border-bottom: 1px solid #30363d; padding-bottom: 8px;
117
+ }
118
+ .markdown-body h1 { font-size: 30px; margin-top: 0; }
119
+ .markdown-body h2 { font-size: 22px; }
120
+ .markdown-body h3 { font-size: 18px; border-bottom: 0; padding-bottom: 0; }
121
+ .markdown-body p, .markdown-body ul, .markdown-body ol, .markdown-body table,
122
+ .markdown-body pre, .markdown-body blockquote { margin: 0 0 16px; }
123
+ .markdown-body a { color: #58a6ff; text-decoration: none; }
124
+ .markdown-body a:hover { text-decoration: underline; }
125
+ .markdown-body code {
126
+ background: rgba(110, 118, 129, 0.28); color: #f0f6fc;
127
+ border-radius: 4px; padding: 0.15em 0.35em;
128
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
129
+ font-size: 0.92em;
130
+ }
131
+ .markdown-body pre {
132
+ background: #161b22; border: 1px solid #30363d; border-radius: 6px;
133
+ padding: 14px 16px; overflow: auto;
134
+ }
135
+ .markdown-body pre code { background: transparent; padding: 0; color: #c9d1d9; }
136
+ .markdown-body blockquote {
137
+ border-left: 3px solid #3fb950; color: #8b949e; padding: 4px 0 4px 14px;
138
+ }
139
+ .markdown-body table { border-collapse: collapse; width: 100%; display: block; overflow: auto; }
140
+ .markdown-body th, .markdown-body td {
141
+ border: 1px solid #30363d; padding: 7px 10px; text-align: left;
142
+ }
143
+ .markdown-body th { background: #161b22; color: #f0f6fc; }
144
+ .markdown-body tr:nth-child(2n) td { background: rgba(22, 27, 34, 0.5); }
145
+ .markdown-body hr { border: 0; border-top: 1px solid #30363d; margin: 24px 0; }
104
146
 
105
147
  /* Footer */
106
148
  .footer { padding: 3px 12px; background: #161b22; border-top: 1px solid #30363d; font-size: 11px; color: #8b949e; flex-shrink: 0; display: flex; justify-content: space-between; gap: 12px; }
@@ -372,6 +414,9 @@
372
414
  <button id="btnReloadPreview" title="Reload">&#8635;</button>
373
415
  </div>
374
416
  <iframe id="preview-frame" src="about:blank"></iframe>
417
+ <div id="markdown-preview" aria-label="Markdown preview">
418
+ <article class="markdown-body" id="markdown-preview-body"></article>
419
+ </div>
375
420
  </div>
376
421
  </div>
377
422
  <!-- Ctrl+P command palette. Lazy-populated fuzzy file finder; fs-list
@@ -475,6 +520,8 @@
475
520
  const stText = document.getElementById('statusText');
476
521
  const previewFrame = document.getElementById('preview-frame');
477
522
  const previewPanel = document.getElementById('previewPanel');
523
+ const markdownPreview = document.getElementById('markdown-preview');
524
+ const markdownPreviewBody = document.getElementById('markdown-preview-body');
478
525
  const mainPanel = document.getElementById('mainPanel');
479
526
  const viteStatus = document.getElementById('viteStatus');
480
527
  let ws = null, rd = 1000, statsTimer = null;
@@ -497,8 +544,11 @@
497
544
  // Lazy-init Monaco + the file tree the FIRST time the user
498
545
  // switches to editor mode. Cold session pays neither cost.
499
546
  if (wantEditor) {
500
- Editor.ensureLoaded();
501
- FileTree.ensureLoaded();
547
+ const editorReady = Editor.ensureLoaded();
548
+ const treeReady = FileTree.ensureLoaded();
549
+ Promise.allSettled([editorReady, treeReady])
550
+ .then(() => Editor.openDefaultWelcome())
551
+ .catch(() => {});
502
552
  }
503
553
  setTimeout(() => {
504
554
  // Re-apply persisted pane dims for this layout.
@@ -550,11 +600,13 @@
550
600
  // race on the same path.
551
601
  const pending = new Map();
552
602
  let nextReqId = 1;
603
+ const DEFAULT_WELCOME_PATH = '/home/user/welcome.md';
553
604
 
554
605
  // Current open file state.
555
606
  let openPath = null; // POSIX absolute path or null
556
607
  let originalContent = ''; // last-saved content (for dirty check)
557
608
  let editor = null; // Monaco editor instance
609
+ let markdownUpdateTimer = null;
558
610
 
559
611
  function tabEl() { return document.getElementById('editorTab'); }
560
612
  function setTabFile(path, dirty) {
@@ -668,6 +720,20 @@
668
720
  return map[ext] || 'plaintext';
669
721
  }
670
722
 
723
+ function isMarkdownPath(path) {
724
+ return /\.(md|markdown|mdown|mkdn)$/i.test(path || '');
725
+ }
726
+
727
+ function scheduleMarkdownPreviewUpdate(content) {
728
+ if (!openPath || !isMarkdownPath(openPath)) return;
729
+ if (markdownUpdateTimer) clearTimeout(markdownUpdateTimer);
730
+ markdownUpdateTimer = setTimeout(() => {
731
+ showMarkdownPreview(openPath, content).catch((e) => {
732
+ setStatusFlash('markdown preview failed: ' + (e?.message || e));
733
+ });
734
+ }, 160);
735
+ }
736
+
671
737
  function ensureLoaded() {
672
738
  if (loaded) return Promise.resolve();
673
739
  if (loading) return loading;
@@ -803,6 +869,9 @@
803
869
  if (typeof FileTree !== 'undefined') {
804
870
  try { FileTree.markDirty(openPath, dirty); } catch {}
805
871
  }
872
+ if (isMarkdownPath(openPath)) {
873
+ scheduleMarkdownPreviewUpdate(cur);
874
+ }
806
875
  });
807
876
  // Cursor position → status bar.
808
877
  const statusPos = document.getElementById('editorStatusPos');
@@ -823,7 +892,7 @@
823
892
  setTimeout(() => editor.layout(), 0);
824
893
  }
825
894
 
826
- async function openFile(path) {
895
+ async function openFile(path, options = {}) {
827
896
  if (!path) return;
828
897
  // Dirty-prompt before switching files. If the current buffer has
829
898
  // unsaved changes, ask the user whether to save or discard.
@@ -880,6 +949,13 @@
880
949
  if (typeof FileTree !== 'undefined') {
881
950
  try { FileTree.setSelected(path); FileTree.markDirty(path, false); } catch {}
882
951
  }
952
+ if (isMarkdownPath(path)) {
953
+ showMarkdownPreview(path, content).catch((e) => {
954
+ setStatusFlash('markdown preview failed: ' + (e?.message || e));
955
+ });
956
+ } else if (!options.keepMarkdownPreview) {
957
+ showAppPreview();
958
+ }
883
959
  editor.focus();
884
960
  }
885
961
 
@@ -1027,7 +1103,21 @@
1027
1103
  fileListCachedAt = 0;
1028
1104
  }
1029
1105
 
1030
- return { ensureLoaded, openFile, save, openPalette, tryHandleFsResult, drainFsQueue, invalidateFileListCache };
1106
+ function openDefaultWelcome() {
1107
+ if (openPath) return Promise.resolve();
1108
+ return openFile(DEFAULT_WELCOME_PATH);
1109
+ }
1110
+
1111
+ function refreshMarkdownPreview() {
1112
+ if (!openPath || !editor || !isMarkdownPath(openPath)) return Promise.resolve();
1113
+ return showMarkdownPreview(openPath, editor.getValue());
1114
+ }
1115
+
1116
+ return {
1117
+ ensureLoaded, openFile, save, openPalette, tryHandleFsResult,
1118
+ drainFsQueue, invalidateFileListCache, openDefaultWelcome,
1119
+ refreshMarkdownPreview,
1120
+ };
1031
1121
  })();
1032
1122
 
1033
1123
  // ── FileTree module ──
@@ -1600,30 +1690,102 @@
1600
1690
  // applyPreviewTarget call always paints (even if nothing is running
1601
1691
  // yet — that still wants the VITE_URL placeholder loaded).
1602
1692
  let activePreview = null; // null | 'vite' | 'worker' | 'none'
1693
+ let previewSurface = 'app'; // 'app' | 'markdown'
1694
+ let markdownRendererPromise = null;
1695
+ let markdownRenderer = null;
1603
1696
 
1604
1697
  function previewUrlFor(kind) {
1605
1698
  return kind === 'worker' ? WORKER_URL : VITE_URL;
1606
1699
  }
1607
1700
 
1701
+ function setPreviewUrlBar(value) {
1702
+ const urlBar = document.getElementById('urlBar');
1703
+ if (urlBar) urlBar.value = value;
1704
+ }
1705
+
1706
+ function showAppPreview() {
1707
+ previewSurface = 'app';
1708
+ if (markdownPreview) markdownPreview.classList.remove('active');
1709
+ if (previewFrame) previewFrame.style.display = 'block';
1710
+ previewPanel.classList.remove('markdown-mode');
1711
+ const kind = (activePreview === null || activePreview === 'none') ? 'vite' : activePreview;
1712
+ const url = previewUrlFor(kind);
1713
+ if (previewFrame && (previewFrame.src === 'about:blank' || !previewFrame.src.includes(url))) {
1714
+ previewFrame.src = url;
1715
+ }
1716
+ setPreviewUrlBar(url);
1717
+ }
1718
+
1719
+ async function showMarkdownPreview(path, markdown) {
1720
+ previewSurface = 'markdown';
1721
+ previewPanel.classList.add('markdown-mode');
1722
+ if (previewFrame) previewFrame.style.display = 'none';
1723
+ if (markdownPreview) markdownPreview.classList.add('active');
1724
+ setPreviewUrlBar('Markdown Preview · ' + path);
1725
+ const html = await renderMarkdown(markdown || '');
1726
+ if (markdownPreviewBody) markdownPreviewBody.innerHTML = html;
1727
+ }
1728
+
1729
+ async function renderMarkdown(markdown) {
1730
+ try {
1731
+ const renderer = await ensureMarkdownRenderer();
1732
+ const raw = renderer.marked(markdown, { gfm: true, breaks: false });
1733
+ return renderer.purifier.sanitize(raw, { USE_PROFILES: { html: true } });
1734
+ } catch (e) {
1735
+ return '<pre><code>' + escapeHtml(markdown) + '</code></pre>';
1736
+ }
1737
+ }
1738
+
1739
+ function ensureMarkdownRenderer() {
1740
+ if (markdownRenderer) return Promise.resolve(markdownRenderer);
1741
+ if (markdownRendererPromise) return markdownRendererPromise;
1742
+ markdownRendererPromise = Promise.all([
1743
+ import('https://cdn.jsdelivr.net/npm/marked@12.0.2/+esm'),
1744
+ import('https://cdn.jsdelivr.net/npm/dompurify@3.1.7/+esm'),
1745
+ ]).then(([markedModule, purifyModule]) => {
1746
+ const marked = markedModule.marked || markedModule.parse || markedModule.default;
1747
+ const createDOMPurify = purifyModule.default || purifyModule.DOMPurify || purifyModule;
1748
+ const purifier = typeof createDOMPurify === 'function'
1749
+ ? createDOMPurify(window)
1750
+ : createDOMPurify;
1751
+ if (typeof marked !== 'function' || !purifier || typeof purifier.sanitize !== 'function') {
1752
+ throw new Error('markdown renderer unavailable');
1753
+ }
1754
+ markdownRenderer = { marked, purifier };
1755
+ return markdownRenderer;
1756
+ });
1757
+ return markdownRendererPromise;
1758
+ }
1759
+
1760
+ function escapeHtml(s) {
1761
+ const div = document.createElement('div');
1762
+ div.textContent = String(s);
1763
+ return div.innerHTML;
1764
+ }
1765
+
1608
1766
  function applyPreviewTarget(kind) {
1609
1767
  // Idempotent: only update iframe + URL bar when the backend kind
1610
1768
  // actually changes. Avoids tearing down the inner app on every
1611
1769
  // 5-second stats poll.
1612
- if (activePreview === kind) return;
1770
+ if (activePreview === kind && previewSurface === 'app') return;
1613
1771
  activePreview = kind;
1772
+ if (previewSurface !== 'app') return;
1614
1773
  const url = previewUrlFor(kind);
1615
1774
  // Always use the VITE_URL path when nothing is running — that
1616
1775
  // endpoint serves the styled "no dev server" placeholder, which
1617
1776
  // self-refreshes via its own polling loop.
1618
1777
  previewFrame.src = url;
1619
- const urlBar = document.getElementById('urlBar');
1620
- if (urlBar) urlBar.value = url;
1778
+ setPreviewUrlBar(url);
1621
1779
  // If the user had collapsed the preview because nothing was up,
1622
1780
  // surface it automatically when the first backend comes online.
1623
1781
  if (kind !== 'none' && layout === 'terminal-only') setLayout('split');
1624
1782
  }
1625
1783
 
1626
1784
  function reloadPreview() {
1785
+ if (previewSurface === 'markdown') {
1786
+ Editor.refreshMarkdownPreview().catch(() => {});
1787
+ return;
1788
+ }
1627
1789
  // Refreshing without a running backend should re-poll the
1628
1790
  // placeholder so users see dev-server status update promptly.
1629
1791
  if (previewFrame.src === 'about:blank' ||