@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.
- package/README.md +239 -100
- package/dist/command-manager.js +6 -3
- package/dist/config-field-definitions.d.ts +41 -0
- package/dist/config-field-definitions.js +37 -0
- package/dist/config-manager.d.ts +2 -0
- package/dist/config-manager.js +22 -2
- package/dist/handlers/filesystem-handlers.js +6 -11
- package/dist/handlers/macos-control-handlers.d.ts +16 -0
- package/dist/handlers/macos-control-handlers.js +81 -0
- package/dist/lib.d.ts +10 -0
- package/dist/lib.js +10 -0
- package/dist/remote-device/remote-channel.js +1 -1
- package/dist/server.js +3 -1
- package/dist/tools/config.d.ts +71 -0
- package/dist/tools/config.js +117 -2
- package/dist/tools/macos-control/ax-adapter.d.ts +55 -0
- package/dist/tools/macos-control/ax-adapter.js +438 -0
- package/dist/tools/macos-control/cdp-adapter.d.ts +23 -0
- package/dist/tools/macos-control/cdp-adapter.js +402 -0
- package/dist/tools/macos-control/orchestrator.d.ts +77 -0
- package/dist/tools/macos-control/orchestrator.js +136 -0
- package/dist/tools/macos-control/role-aliases.d.ts +5 -0
- package/dist/tools/macos-control/role-aliases.js +34 -0
- package/dist/tools/macos-control/types.d.ts +129 -0
- package/dist/tools/macos-control/types.js +1 -0
- package/dist/tools/schemas.d.ts +3 -0
- package/dist/tools/schemas.js +1 -0
- package/dist/types.d.ts +0 -1
- package/dist/ui/config-editor/config-editor-runtime.js +14181 -0
- package/dist/ui/config-editor/index.html +13 -0
- package/dist/ui/config-editor/src/app.d.ts +43 -0
- package/dist/ui/config-editor/src/app.js +840 -0
- package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
- package/dist/ui/config-editor/src/array-modal.js +185 -0
- package/dist/ui/config-editor/src/main.d.ts +1 -0
- package/dist/ui/config-editor/src/main.js +2 -0
- package/dist/ui/config-editor/styles.css +586 -0
- package/dist/ui/file-preview/preview-runtime.js +13336 -757
- package/dist/ui/file-preview/shared/preview-file-types.js +3 -1
- package/dist/ui/file-preview/src/app.d.ts +5 -1
- package/dist/ui/file-preview/src/app.js +114 -200
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +1 -5
- package/dist/ui/file-preview/src/components/html-renderer.js +11 -27
- package/dist/ui/file-preview/styles.css +117 -83
- package/dist/ui/resources.d.ts +7 -0
- package/dist/ui/resources.js +16 -2
- package/dist/ui/shared/compact-row.d.ts +11 -0
- package/dist/ui/shared/compact-row.js +18 -0
- package/dist/ui/shared/host-context.d.ts +15 -0
- package/dist/ui/shared/host-context.js +51 -0
- package/dist/ui/shared/tool-bridge.d.ts +30 -0
- package/dist/ui/shared/tool-bridge.js +137 -0
- package/dist/ui/shared/tool-shell.d.ts +9 -0
- package/dist/ui/shared/tool-shell.js +46 -4
- package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
- package/dist/ui/shared/ui-event-tracker.js +27 -0
- package/dist/utils/capture.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -4
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { FilePreviewStructuredContent } from '../../../types.js';
|
|
2
2
|
import type { HtmlPreviewMode } from './types.js';
|
|
3
|
-
|
|
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 {
|
|
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 (!
|
|
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
|
|
43
|
-
|
|
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
|
|
63
|
-
if (!
|
|
43
|
+
function extractRenderPayload(value) {
|
|
44
|
+
if (!isObjectRecord(value)) {
|
|
64
45
|
return undefined;
|
|
65
46
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
697
|
-
|
|
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);
|
|
619
|
+
widgetState.write(payload);
|
|
706
620
|
}
|
|
707
|
-
renderApp(container, payload, 'rendered',
|
|
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
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
781
|
-
|
|
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
|
-
|
|
790
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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"
|
|
44
|
-
return `<iframe class="html-rendered-frame" title="Rendered HTML preview" sandbox="
|
|
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
|
|
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
|
|
38
|
+
html: `<div class="panel-content html-content">${renderSandboxedHtmlFrame(content)}</div>`
|
|
55
39
|
};
|
|
56
40
|
}
|
|
57
41
|
catch {
|