@wonderwhy-er/desktop-commander 0.2.37 → 0.2.38

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 (60) hide show
  1. package/README.md +239 -100
  2. package/dist/command-manager.js +6 -3
  3. package/dist/config-field-definitions.d.ts +41 -0
  4. package/dist/config-field-definitions.js +37 -0
  5. package/dist/config-manager.d.ts +2 -0
  6. package/dist/config-manager.js +22 -2
  7. package/dist/handlers/filesystem-handlers.js +6 -11
  8. package/dist/handlers/macos-control-handlers.d.ts +16 -0
  9. package/dist/handlers/macos-control-handlers.js +81 -0
  10. package/dist/lib.d.ts +10 -0
  11. package/dist/lib.js +10 -0
  12. package/dist/remote-device/remote-channel.js +1 -1
  13. package/dist/server.js +3 -1
  14. package/dist/tools/config.d.ts +71 -0
  15. package/dist/tools/config.js +117 -2
  16. package/dist/tools/macos-control/ax-adapter.d.ts +55 -0
  17. package/dist/tools/macos-control/ax-adapter.js +438 -0
  18. package/dist/tools/macos-control/cdp-adapter.d.ts +23 -0
  19. package/dist/tools/macos-control/cdp-adapter.js +402 -0
  20. package/dist/tools/macos-control/orchestrator.d.ts +77 -0
  21. package/dist/tools/macos-control/orchestrator.js +136 -0
  22. package/dist/tools/macos-control/role-aliases.d.ts +5 -0
  23. package/dist/tools/macos-control/role-aliases.js +34 -0
  24. package/dist/tools/macos-control/types.d.ts +129 -0
  25. package/dist/tools/macos-control/types.js +1 -0
  26. package/dist/tools/schemas.d.ts +3 -0
  27. package/dist/tools/schemas.js +1 -0
  28. package/dist/types.d.ts +0 -1
  29. package/dist/ui/config-editor/config-editor-runtime.js +14181 -0
  30. package/dist/ui/config-editor/index.html +13 -0
  31. package/dist/ui/config-editor/src/app.d.ts +43 -0
  32. package/dist/ui/config-editor/src/app.js +840 -0
  33. package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
  34. package/dist/ui/config-editor/src/array-modal.js +185 -0
  35. package/dist/ui/config-editor/src/main.d.ts +1 -0
  36. package/dist/ui/config-editor/src/main.js +2 -0
  37. package/dist/ui/config-editor/styles.css +586 -0
  38. package/dist/ui/file-preview/preview-runtime.js +13336 -757
  39. package/dist/ui/file-preview/shared/preview-file-types.js +3 -1
  40. package/dist/ui/file-preview/src/app.d.ts +5 -1
  41. package/dist/ui/file-preview/src/app.js +114 -200
  42. package/dist/ui/file-preview/src/components/html-renderer.d.ts +1 -5
  43. package/dist/ui/file-preview/src/components/html-renderer.js +11 -27
  44. package/dist/ui/file-preview/styles.css +117 -83
  45. package/dist/ui/resources.d.ts +7 -0
  46. package/dist/ui/resources.js +16 -2
  47. package/dist/ui/shared/compact-row.d.ts +11 -0
  48. package/dist/ui/shared/compact-row.js +18 -0
  49. package/dist/ui/shared/host-context.d.ts +15 -0
  50. package/dist/ui/shared/host-context.js +51 -0
  51. package/dist/ui/shared/tool-bridge.d.ts +30 -0
  52. package/dist/ui/shared/tool-bridge.js +137 -0
  53. package/dist/ui/shared/tool-shell.d.ts +9 -0
  54. package/dist/ui/shared/tool-shell.js +46 -4
  55. package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
  56. package/dist/ui/shared/ui-event-tracker.js +27 -0
  57. package/dist/utils/capture.js +3 -3
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/package.json +8 -4
@@ -31,7 +31,9 @@ export const TEXT_PREVIEW_EXTENSIONS = new Set([
31
31
  '.java',
32
32
  '.go',
33
33
  '.rs',
34
- '.sql'
34
+ '.sql',
35
+ '.srt',
36
+ '.vtt'
35
37
  ]);
36
38
  const TEXT_PREVIEW_BASENAMES = new Set([
37
39
  '.env',
@@ -1,4 +1,8 @@
1
1
  import type { FilePreviewStructuredContent } from '../../../types.js';
2
2
  import type { HtmlPreviewMode } from './types.js';
3
- export declare function renderApp(container: HTMLElement, payload?: FilePreviewStructuredContent, htmlMode?: HtmlPreviewMode, expandedState?: boolean): void;
3
+ type RenderPayload = FilePreviewStructuredContent & {
4
+ content: string;
5
+ };
6
+ export declare function renderApp(container: HTMLElement, payload?: RenderPayload, htmlMode?: HtmlPreviewMode, expandedState?: boolean): void;
4
7
  export declare function bootstrapApp(): void;
8
+ export {};
@@ -6,12 +6,14 @@ import { renderHtmlPreview } from './components/html-renderer.js';
6
6
  import { renderMarkdown } from './components/markdown-renderer.js';
7
7
  import { escapeHtml } from './components/highlighting.js';
8
8
  import { isAllowedImageMimeType, normalizeImageMimeType } from './image-preview.js';
9
- import { createWindowRpcClient, isTrustedParentMessageSource } from '../../shared/rpc-client.js';
10
- import { createToolShellController } from '../../shared/tool-shell.js';
11
- import { createUiHostLifecycle } from '../../shared/host-lifecycle.js';
12
- import { createUiThemeAdapter } from '../../shared/theme-adaptation.js';
9
+ import { createCompactRowShellController } from '../../shared/tool-shell.js';
13
10
  import { createWidgetStateStorage } from '../../shared/widget-state.js';
11
+ import { renderCompactRow } from '../../shared/compact-row.js';
12
+ import { connectWithSharedHostContext, isObjectRecord } from '../../shared/host-context.js';
13
+ import { createUiEventTracker } from '../../shared/ui-event-tracker.js';
14
+ import { App } from '@modelcontextprotocol/ext-apps';
14
15
  let isExpanded = false;
16
+ let hideSummaryRow = false;
15
17
  let previewShownFired = false;
16
18
  let onRender;
17
19
  let trackUiEvent;
@@ -27,52 +29,33 @@ function getFileExtensionForAnalytics(filePath) {
27
29
  }
28
30
  return fileName.slice(dotIndex + 1).toLowerCase();
29
31
  }
30
- function isObject(value) {
31
- return typeof value === 'object' && value !== null;
32
- }
33
32
  function isPreviewStructuredContent(value) {
34
- if (!isObject(value)) {
33
+ if (!isObjectRecord(value)) {
35
34
  return false;
36
35
  }
37
36
  return (typeof value.fileName === 'string' &&
38
37
  typeof value.filePath === 'string' &&
39
- typeof value.fileType === 'string' &&
40
- typeof value.content === 'string');
38
+ typeof value.fileType === 'string');
41
39
  }
42
- function readStructuredContentFromWindow() {
43
- const candidates = [
44
- window.__DC_FILE_PREVIEW__,
45
- window.__MCP_TOOL_RESULT__,
46
- window.toolResult,
47
- window.structuredContent
48
- ];
49
- for (const candidate of candidates) {
50
- if (!isObject(candidate)) {
51
- continue;
52
- }
53
- if (isPreviewStructuredContent(candidate.structuredContent)) {
54
- return candidate.structuredContent;
55
- }
56
- if (isPreviewStructuredContent(candidate)) {
57
- return candidate;
58
- }
59
- }
60
- return undefined;
40
+ function buildRenderPayload(meta, text) {
41
+ return { ...meta, content: text };
61
42
  }
62
- function extractStructuredContent(value) {
63
- if (!isObject(value)) {
43
+ function extractRenderPayload(value) {
44
+ if (!isObjectRecord(value)) {
64
45
  return undefined;
65
46
  }
66
- if (isPreviewStructuredContent(value.structuredContent)) {
67
- return value.structuredContent;
68
- }
69
- if (isPreviewStructuredContent(value)) {
70
- return value;
71
- }
72
- return undefined;
47
+ const meta = isPreviewStructuredContent(value.structuredContent)
48
+ ? value.structuredContent
49
+ : isPreviewStructuredContent(value)
50
+ ? value
51
+ : null;
52
+ if (!meta)
53
+ return undefined;
54
+ const text = extractToolText(value) ?? extractToolText(value.structuredContent) ?? '';
55
+ return buildRenderPayload(meta, text);
73
56
  }
74
57
  function extractToolText(value) {
75
- if (!isObject(value)) {
58
+ if (!isObjectRecord(value)) {
76
59
  return undefined;
77
60
  }
78
61
  const content = value.content;
@@ -80,7 +63,7 @@ function extractToolText(value) {
80
63
  return undefined;
81
64
  }
82
65
  for (const item of content) {
83
- if (!isObject(item)) {
66
+ if (!isObjectRecord(item)) {
84
67
  continue;
85
68
  }
86
69
  if (item.type === 'text' && typeof item.text === 'string' && item.text.trim().length > 0) {
@@ -89,31 +72,6 @@ function extractToolText(value) {
89
72
  }
90
73
  return undefined;
91
74
  }
92
- function extractToolTextFromEvent(value) {
93
- if (!isObject(value)) {
94
- return undefined;
95
- }
96
- const direct = extractToolText(value);
97
- if (direct) {
98
- return direct;
99
- }
100
- if (isObject(value.result)) {
101
- const nested = extractToolText(value.result);
102
- if (nested) {
103
- return nested;
104
- }
105
- }
106
- if (isObject(value.params)) {
107
- const paramsText = extractToolText(value.params);
108
- if (paramsText) {
109
- return paramsText;
110
- }
111
- if (isObject(value.params.result)) {
112
- return extractToolText(value.params.result);
113
- }
114
- }
115
- return undefined;
116
- }
117
75
  function isLikelyUrl(filePath) {
118
76
  return /^https?:\/\//i.test(filePath);
119
77
  }
@@ -517,9 +475,7 @@ function attachTextSelectionHandler(payload) {
517
475
  function renderStatusState(container, message) {
518
476
  container.innerHTML = `
519
477
  <main class="shell">
520
- <div class="compact-row compact-row--status">
521
- <span class="compact-label">${escapeHtml(message)}</span>
522
- </div>
478
+ ${renderCompactRow({ label: message, variant: 'status', interactive: false })}
523
479
  </main>
524
480
  `;
525
481
  document.body.classList.add('dc-ready');
@@ -527,9 +483,7 @@ function renderStatusState(container, message) {
527
483
  function renderLoadingState(container) {
528
484
  container.innerHTML = `
529
485
  <main class="shell">
530
- <div class="compact-row compact-row--loading">
531
- <span class="compact-label">Preparing preview…</span>
532
- </div>
486
+ ${renderCompactRow({ label: 'Preparing preview…', variant: 'loading', interactive: false })}
533
487
  </main>
534
488
  `;
535
489
  document.body.classList.add('dc-ready');
@@ -547,6 +501,11 @@ export function renderApp(container, payload, htmlMode = 'rendered', expandedSta
547
501
  const canOpenInFolder = !isLikelyUrl(payload.filePath);
548
502
  const fileExtension = getFileExtensionForAnalytics(payload.filePath);
549
503
  const supportsPreview = payload.fileType !== 'unsupported';
504
+ // In DC app (hideSummaryRow), no reason to auto-expand when there's nothing to preview —
505
+ // the host header already shows the file name and path.
506
+ if (!supportsPreview && hideSummaryRow) {
507
+ isExpanded = false;
508
+ }
550
509
  const range = parseReadRange(payload.content);
551
510
  const body = renderBody(payload, htmlMode, range?.fromLine ?? 1);
552
511
  const notice = body.notice ? `<div class="notice">${body.notice}</div>` : '';
@@ -578,12 +537,8 @@ export function renderApp(container, payload, htmlMode = 'rendered', expandedSta
578
537
  ? `<button class="load-lines-banner" id="load-after">↓ Load lines ${range.toLine + 1}–${range.totalLines}</button>`
579
538
  : '';
580
539
  container.innerHTML = `
581
- <main id="tool-shell" class="shell tool-shell ${isExpanded ? 'expanded' : 'collapsed'}">
582
- <div class="compact-row compact-row--ready" id="compact-toggle" role="button" tabindex="0" aria-expanded="${isExpanded}">
583
- <svg class="compact-chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 6l6 6-6 6z"/></svg>
584
- <span class="compact-label">${compactLabel}</span>
585
- <span class="compact-filename">${escapeHtml(payload.fileName)}</span>
586
- </div>
540
+ <main id="tool-shell" class="shell tool-shell ${isExpanded ? 'expanded' : 'collapsed'}${hideSummaryRow ? ' host-framed' : ''}">
541
+ ${renderCompactRow({ id: 'compact-toggle', label: compactLabel, filename: payload.fileName, variant: 'ready', expandable: true, expanded: isExpanded, interactive: true })}
587
542
  <section class="panel">
588
543
  <div class="panel-topbar">
589
544
  <span class="panel-breadcrumb" title="${escapeHtml(payload.filePath)}">${breadcrumb}</span>
@@ -611,26 +566,13 @@ export function renderApp(container, payload, htmlMode = 'rendered', expandedSta
611
566
  attachOpenInFolderHandler(payload);
612
567
  attachLoadAllHandler(container, payload, htmlMode);
613
568
  attachTextSelectionHandler(payload);
614
- // Compact row click toggles expand/collapse
615
569
  const compactRow = document.getElementById('compact-toggle');
616
- const handleCompactClick = () => {
617
- shellController?.toggle();
618
- };
619
- const handleCompactKeydown = (e) => {
620
- if (e.key === 'Enter' || e.key === ' ') {
621
- e.preventDefault();
622
- shellController?.toggle();
623
- }
624
- };
625
- compactRow?.addEventListener('click', handleCompactClick);
626
- compactRow?.addEventListener('keydown', handleCompactKeydown);
627
- shellController = createToolShellController({
570
+ shellController = createCompactRowShellController({
628
571
  shell: document.getElementById('tool-shell'),
629
- toggleButton: null, // No separate toggle button; compact row handles it
572
+ compactRow,
630
573
  initialExpanded: isExpanded,
631
574
  onToggle: (expanded) => {
632
575
  isExpanded = expanded;
633
- compactRow?.setAttribute('aria-expanded', String(expanded));
634
576
  trackUiEvent?.(expanded ? 'expand' : 'collapse', {
635
577
  file_type: payload.fileType,
636
578
  file_extension: fileExtension
@@ -659,52 +601,24 @@ export function bootstrapApp() {
659
601
  return;
660
602
  }
661
603
  renderLoadingState(container);
662
- const rpcClient = createWindowRpcClient({
663
- targetWindow: window.parent,
664
- timeoutMs: 15000,
665
- isTrustedSource: (source) => isTrustedParentMessageSource(source, window.parent)
666
- });
667
- const hostLifecycle = createUiHostLifecycle(rpcClient, {
668
- appName: 'Desktop Commander File Preview',
669
- appVersion: '1.0.0'
670
- });
671
- const themeAdapter = createUiThemeAdapter();
672
- rpcCallTool = (name, args) => (rpcClient.request('tools/call', {
673
- name,
674
- arguments: args
675
- }));
676
- rpcUpdateContext = (text) => {
677
- const params = text
678
- ? { content: [{ type: 'text', text }] }
679
- : { content: [] };
680
- rpcClient.request('ui/update-model-context', params).catch(() => {
681
- // Host may not support ui/update-model-context yet
682
- });
604
+ // Use the official App class – it connects to the host via PostMessageTransport
605
+ // (window.parent by default) and speaks standard MCP JSON-RPC 2.0 over postMessage.
606
+ const app = new App({ name: 'Desktop Commander File Preview', version: '1.0.0' }, { updateModelContext: { text: {} } }, { autoResize: true });
607
+ const chrome = {
608
+ expanded: isExpanded,
609
+ hideSummaryRow,
683
610
  };
684
- trackUiEvent = (event, params = {}) => {
685
- void rpcCallTool?.('track_ui_event', {
686
- event,
687
- component: 'file_preview',
688
- params: {
689
- tool_name: 'read_file',
690
- ...params
691
- }
692
- }).catch(() => {
693
- // Analytics failures should not impact UX.
694
- });
611
+ const syncChromeState = () => {
612
+ isExpanded = chrome.expanded;
613
+ hideSummaryRow = chrome.hideSummaryRow;
695
614
  };
696
- onRender = () => {
697
- hostLifecycle.notifyRender();
698
- };
699
- // ChatGPT widget state persistence (other hosts use standard ui/notifications/tool-result)
700
- const widgetState = createWidgetStateStorage(isPreviewStructuredContent);
701
- onRender?.();
702
- themeAdapter.applyFromData(window.__MCP_HOST_CONTEXT__);
615
+ // Widget state for cross-host persistence (survives page refresh)
616
+ const widgetState = createWidgetStateStorage((v) => isPreviewStructuredContent(v) && typeof v.content === 'string');
703
617
  const renderAndSync = (payload) => {
704
618
  if (payload) {
705
- widgetState.write(payload); // Persist for refresh recovery (cross-host)
619
+ widgetState.write(payload);
706
620
  }
707
- renderApp(container, payload, 'rendered', false);
621
+ renderApp(container, payload, 'rendered', isExpanded);
708
622
  };
709
623
  let initialStateResolved = false;
710
624
  const resolveInitialState = (payload, message) => {
@@ -719,82 +633,82 @@ export function bootstrapApp() {
719
633
  renderStatusState(container, message ?? 'No preview available for this response.');
720
634
  onRender?.();
721
635
  };
722
- // Try to restore from widget state first (ChatGPT only - survives refresh)
723
- const cachedPayload = widgetState.read();
724
- if (cachedPayload) {
725
- window.setTimeout(() => {
726
- resolveInitialState(cachedPayload);
727
- }, 50);
728
- }
729
- // Then check window globals
730
- const initialPayload = readStructuredContentFromWindow();
731
- if (initialPayload) {
732
- window.setTimeout(() => {
733
- resolveInitialState(initialPayload);
734
- }, 140);
735
- }
736
- // Timeout fallback: if no data arrives after retry, show helpful message
737
- window.setTimeout(() => {
636
+ // autoResize handles size reporting; onRender can be a no-op
637
+ onRender = () => { };
638
+ // Wire rpcCallTool through the App's callServerTool proxy
639
+ rpcCallTool = (name, args) => (app.callServerTool({ name, arguments: args }));
640
+ // Wire rpcUpdateContext through the App's updateModelContext
641
+ rpcUpdateContext = (text) => {
642
+ const params = text
643
+ ? { content: [{ type: 'text', text }] }
644
+ : { content: [] };
645
+ app.updateModelContext(params).catch(() => {
646
+ // Host may not support updateModelContext
647
+ });
648
+ };
649
+ trackUiEvent = createUiEventTracker((name, args) => app.callServerTool({ name, arguments: args }), {
650
+ component: 'file_preview',
651
+ baseParams: { tool_name: 'read_file' },
652
+ });
653
+ // Register ALL handlers BEFORE connect
654
+ app.onteardown = async () => {
655
+ shellController?.dispose();
656
+ return {};
657
+ };
658
+ app.ontoolinput = (_params) => {
659
+ // Tool is executing – show loading state
660
+ renderLoadingState(container);
661
+ onRender?.();
662
+ };
663
+ app.ontoolresult = (result) => {
664
+ const payload = extractRenderPayload(result);
665
+ const message = extractToolText(result);
738
666
  if (!initialStateResolved) {
739
- resolveInitialState(undefined, 'Preview unavailable after page refresh (known issue, fix in progress). Switch threads or re-run the tool.');
740
- }
741
- }, 8000);
742
- window.addEventListener('message', (event) => {
743
- try {
744
- if (rpcClient.handleMessageEvent(event)) {
745
- return;
746
- }
747
- if (!isTrustedParentMessageSource(event.source, window.parent)) {
748
- return;
749
- }
750
- if (!isObject(event.data)) {
751
- return;
752
- }
753
- themeAdapter.applyFromData(event.data);
754
- if (event.data.method === 'ui/notifications/tool-result') {
755
- const params = event.data.params;
756
- const candidate = isObject(params) && isObject(params.result) ? params.result : params;
757
- const payload = extractStructuredContent(candidate);
758
- const message = extractToolTextFromEvent(event.data) ?? extractToolText(candidate);
759
- if (!initialStateResolved) {
760
- if (payload) {
761
- renderLoadingState(container);
762
- onRender?.();
763
- window.setTimeout(() => resolveInitialState(payload), 120);
764
- return;
765
- }
766
- if (message) {
767
- resolveInitialState(undefined, message);
768
- }
769
- return;
770
- }
771
- if (payload) {
772
- renderAndSync(payload);
773
- }
774
- else if (message) {
775
- renderStatusState(container, message);
776
- onRender?.();
777
- }
667
+ if (payload) {
668
+ renderLoadingState(container);
669
+ onRender?.();
670
+ window.setTimeout(() => resolveInitialState(payload), 120);
778
671
  return;
779
672
  }
780
- const payload = extractStructuredContent(event.data);
781
- if (payload) {
782
- if (!initialStateResolved) {
783
- resolveInitialState(payload);
784
- return;
785
- }
786
- renderAndSync(payload);
673
+ if (message) {
674
+ resolveInitialState(undefined, message);
787
675
  }
676
+ return;
788
677
  }
789
- catch {
790
- renderStatusState(container, 'Preview failed to render.');
678
+ if (payload) {
679
+ renderAndSync(payload);
680
+ }
681
+ else if (message) {
682
+ renderStatusState(container, message);
791
683
  onRender?.();
792
684
  }
685
+ };
686
+ app.ontoolcancelled = (params) => {
687
+ resolveInitialState(undefined, params.reason ?? 'Tool was cancelled.');
688
+ };
689
+ // Connect to the host (defaults to window.parent via PostMessageTransport)
690
+ void connectWithSharedHostContext({
691
+ app,
692
+ chrome,
693
+ onContextApplied: syncChromeState,
694
+ onConnected: () => {
695
+ // Try to restore from persisted widget state (survives refresh on some hosts)
696
+ const cachedPayload = widgetState.read();
697
+ if (cachedPayload) {
698
+ window.setTimeout(() => resolveInitialState(cachedPayload), 50);
699
+ }
700
+ // Fallback: if no tool data arrives, show a helpful status message
701
+ window.setTimeout(() => {
702
+ if (!initialStateResolved) {
703
+ resolveInitialState(undefined, 'Preview unavailable after page refresh. Switch threads or re-run the tool.');
704
+ }
705
+ }, 8000);
706
+ },
707
+ }).catch(() => {
708
+ renderStatusState(container, 'Failed to connect to host.');
709
+ onRender?.();
793
710
  });
794
- hostLifecycle.observeResize();
795
711
  window.addEventListener('beforeunload', () => {
796
712
  shellController?.dispose();
797
- rpcClient.dispose();
798
713
  }, { once: true });
799
- hostLifecycle.initialize();
800
714
  }
@@ -1,9 +1,5 @@
1
1
  import type { HtmlPreviewMode } from '../types.js';
2
- interface HtmlRenderOptions {
3
- allowUnsafeScripts?: boolean;
4
- }
5
- export declare function renderHtmlPreview(content: string, mode: HtmlPreviewMode, options?: HtmlRenderOptions): {
2
+ export declare function renderHtmlPreview(content: string, mode: HtmlPreviewMode): {
6
3
  html: string;
7
4
  notice?: string;
8
5
  };
9
- export {};
@@ -1,24 +1,13 @@
1
1
  /**
2
- * HTML preview renderer with guardrails for display modes. It controls when to show rendered HTML versus source text and ensures fallback behavior is predictable.
2
+ * HTML preview renderer with display mode control. It handles rendered HTML versus
3
+ * source text display and ensures fallback behavior is predictable.
4
+ *
5
+ * The rendered preview runs inside a nested sandboxed iframe, which is itself inside
6
+ * the MCP app's sandboxed iframe chain. Scripts and external resources (CDNs) are
7
+ * allowed since the sandbox isolation prevents any escape.
3
8
  */
4
9
  import { renderCodeViewer } from './code-viewer.js';
5
10
  import { escapeHtml } from './highlighting.js';
6
- function sanitizeHtml(rawHtml) {
7
- const blockedTagPattern = /<\/?(script|iframe|object|embed|link|meta|base|form)[^>]*>/gi;
8
- let safe = rawHtml.replace(blockedTagPattern, '');
9
- safe = safe.replace(/\son[a-z]+\s*=\s*(".*?"|'.*?'|[^\s>]+)/gi, '');
10
- safe = safe.replace(/\s(href|src)\s*=\s*(".*?"|'.*?'|[^\s>]+)/gi, (match, attr, value) => {
11
- const strippedValue = String(value).replace(/^['"]|['"]$/g, '').trim().toLowerCase();
12
- if (strippedValue.startsWith('javascript:')) {
13
- return ` ${attr}="#"`;
14
- }
15
- if (strippedValue.startsWith('data:text/html')) {
16
- return ` ${attr}="#"`;
17
- }
18
- return match;
19
- });
20
- return safe;
21
- }
22
11
  function resolveThemeFrameStyles() {
23
12
  if (typeof window === 'undefined' || typeof document === 'undefined') {
24
13
  return {
@@ -33,17 +22,12 @@ function resolveThemeFrameStyles() {
33
22
  const fontFamily = rootStyles.getPropertyValue('--font-sans').trim() || 'system-ui, sans-serif';
34
23
  return { background, text, fontFamily };
35
24
  }
36
- function renderSandboxedHtmlFrame(content, allowUnsafeScripts) {
37
- const htmlContent = allowUnsafeScripts ? content : sanitizeHtml(content);
38
- const csp = allowUnsafeScripts
39
- ? ''
40
- : `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: http: data:; style-src 'unsafe-inline';">`;
41
- const sandbox = allowUnsafeScripts ? 'allow-scripts allow-forms allow-popups' : '';
25
+ function renderSandboxedHtmlFrame(content) {
42
26
  const palette = resolveThemeFrameStyles();
43
- const frameDocument = `<!doctype html><html><head><meta charset="utf-8" />${csp}<style>html,body{margin:0;padding:0;background:${palette.background};color:${palette.text};}body{font-family:${palette.fontFamily};padding:16px;line-height:1.5;}img{max-width:100%;height:auto;}</style></head><body>${htmlContent}</body></html>`;
44
- return `<iframe class="html-rendered-frame" title="Rendered HTML preview" sandbox="${sandbox}" referrerpolicy="no-referrer" srcdoc="${escapeHtml(frameDocument)}"></iframe>`;
27
+ const frameDocument = `<!doctype html><html><head><meta charset="utf-8" /><style>html,body{margin:0;padding:0;background:${palette.background};color:${palette.text};}body{font-family:${palette.fontFamily};padding:16px;line-height:1.5;}img{max-width:100%;height:auto;}</style></head><body>${content}</body></html>`;
28
+ return `<iframe class="html-rendered-frame" title="Rendered HTML preview" sandbox="allow-scripts allow-forms allow-popups" referrerpolicy="no-referrer" srcdoc="${escapeHtml(frameDocument)}"></iframe>`;
45
29
  }
46
- export function renderHtmlPreview(content, mode, options = {}) {
30
+ export function renderHtmlPreview(content, mode) {
47
31
  if (mode === 'source') {
48
32
  return {
49
33
  html: `<div class="panel-content source-content">${renderCodeViewer(content, 'html')}</div>`
@@ -51,7 +35,7 @@ export function renderHtmlPreview(content, mode, options = {}) {
51
35
  }
52
36
  try {
53
37
  return {
54
- html: `<div class="panel-content html-content">${renderSandboxedHtmlFrame(content, options.allowUnsafeScripts === true)}</div>`
38
+ html: `<div class="panel-content html-content">${renderSandboxedHtmlFrame(content)}</div>`
55
39
  };
56
40
  }
57
41
  catch {