@hcu-lab.me/mcp-device 1.0.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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +22 -0
- package/dist/command-manager.d.ts +8 -0
- package/dist/command-manager.js +241 -0
- package/dist/config-field-definitions.d.ts +41 -0
- package/dist/config-field-definitions.js +37 -0
- package/dist/config-manager.d.ts +102 -0
- package/dist/config-manager.js +291 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +10 -0
- package/dist/custom-stdio.d.ts +51 -0
- package/dist/custom-stdio.js +358 -0
- package/dist/data/official-app-ca.pem +12 -0
- package/dist/data/onboarding-prompts.json +70 -0
- package/dist/device/device-state.d.ts +13 -0
- package/dist/device/device-state.js +100 -0
- package/dist/device/device-status.d.ts +81 -0
- package/dist/device/device-status.js +156 -0
- package/dist/device/device.d.ts +16 -0
- package/dist/device/device.js +190 -0
- package/dist/device/execution-engine.d.ts +144 -0
- package/dist/device/execution-engine.js +195 -0
- package/dist/device/gateway-channel.d.ts +48 -0
- package/dist/device/gateway-channel.js +384 -0
- package/dist/device/gateway-config.d.ts +40 -0
- package/dist/device/gateway-config.js +207 -0
- package/dist/device/gateway-identity.d.ts +35 -0
- package/dist/device/gateway-identity.js +178 -0
- package/dist/device/gateway-pairing.d.ts +25 -0
- package/dist/device/gateway-pairing.js +105 -0
- package/dist/device/gateway-secure-transport.d.ts +25 -0
- package/dist/device/gateway-secure-transport.js +117 -0
- package/dist/device/gateway-tool-adapter.d.ts +17 -0
- package/dist/device/gateway-tool-adapter.js +284 -0
- package/dist/device/gateway-url-policy.d.ts +3 -0
- package/dist/device/gateway-url-policy.js +43 -0
- package/dist/device/linux-service.d.ts +70 -0
- package/dist/device/linux-service.js +268 -0
- package/dist/device/official-trust.d.ts +7 -0
- package/dist/device/official-trust.js +49 -0
- package/dist/device/project-inspection.d.ts +1 -0
- package/dist/device/project-inspection.js +180 -0
- package/dist/device/runtime-owner.d.ts +41 -0
- package/dist/device/runtime-owner.js +177 -0
- package/dist/device/windows-dpapi.d.ts +11 -0
- package/dist/device/windows-dpapi.js +67 -0
- package/dist/device/windows-service.d.ts +56 -0
- package/dist/device/windows-service.js +238 -0
- package/dist/error-handlers.d.ts +7 -0
- package/dist/error-handlers.js +15 -0
- package/dist/handlers/edit-search-handlers.d.ts +6 -0
- package/dist/handlers/edit-search-handlers.js +6 -0
- package/dist/handlers/filesystem-handlers.d.ts +38 -0
- package/dist/handlers/filesystem-handlers.js +440 -0
- package/dist/handlers/history-handlers.d.ts +12 -0
- package/dist/handlers/history-handlers.js +67 -0
- package/dist/handlers/index.d.ts +6 -0
- package/dist/handlers/index.js +7 -0
- package/dist/handlers/process-handlers.d.ts +9 -0
- package/dist/handlers/process-handlers.js +15 -0
- package/dist/handlers/search-handlers.d.ts +17 -0
- package/dist/handlers/search-handlers.js +224 -0
- package/dist/handlers/terminal-handlers.d.ts +21 -0
- package/dist/handlers/terminal-handlers.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +138 -0
- package/dist/mcp-device.d.ts +2 -0
- package/dist/mcp-device.js +7 -0
- package/dist/npm-scripts/remote.d.ts +3 -0
- package/dist/npm-scripts/remote.js +314 -0
- package/dist/search-manager.d.ts +147 -0
- package/dist/search-manager.js +828 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +1563 -0
- package/dist/terminal-manager.d.ts +104 -0
- package/dist/terminal-manager.js +635 -0
- package/dist/tools/config.d.ts +96 -0
- package/dist/tools/config.js +260 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +435 -0
- package/dist/tools/feedback.d.ts +8 -0
- package/dist/tools/feedback.js +132 -0
- package/dist/tools/filesystem.d.ts +79 -0
- package/dist/tools/filesystem.js +947 -0
- package/dist/tools/fuzzySearch.d.ts +12 -0
- package/dist/tools/fuzzySearch.js +74 -0
- package/dist/tools/fuzzySearchCore.d.ts +52 -0
- package/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/tools/improved-process-tools.d.ts +24 -0
- package/dist/tools/improved-process-tools.js +627 -0
- package/dist/tools/mime-types.d.ts +3 -0
- package/dist/tools/mime-types.js +28 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +99 -0
- package/dist/tools/pdf/markdown.d.ts +26 -0
- package/dist/tools/pdf/markdown.js +275 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/process.d.ts +3 -0
- package/dist/tools/process.js +56 -0
- package/dist/tools/prompts.d.ts +28 -0
- package/dist/tools/prompts.js +259 -0
- package/dist/tools/schemas.d.ts +454 -0
- package/dist/tools/schemas.js +242 -0
- package/dist/tools/usage.d.ts +5 -0
- package/dist/tools/usage.js +24 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +1 -0
- package/dist/ui/config-editor/config-editor-runtime.js +150 -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 +831 -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 +587 -0
- package/dist/ui/contracts.d.ts +14 -0
- package/dist/ui/contracts.js +21 -0
- package/dist/ui/file-preview/index.html +16 -0
- package/dist/ui/file-preview/preview-runtime.js +469 -0
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
- package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
- package/dist/ui/file-preview/src/app.d.ts +4 -0
- package/dist/ui/file-preview/src/app.js +680 -0
- package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
- package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
- package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
- package/dist/ui/file-preview/src/components/highlighting.js +54 -0
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
- package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
- package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
- package/dist/ui/file-preview/src/directory-controller.js +240 -0
- package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
- package/dist/ui/file-preview/src/document-layout.js +109 -0
- package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
- package/dist/ui/file-preview/src/document-outline.js +97 -0
- package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
- package/dist/ui/file-preview/src/document-workspace.js +33 -0
- package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
- package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
- package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
- package/dist/ui/file-preview/src/host/external-actions.js +55 -0
- package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
- package/dist/ui/file-preview/src/host/selection-context.js +106 -0
- package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
- package/dist/ui/file-preview/src/image-preview.js +21 -0
- package/dist/ui/file-preview/src/main.d.ts +1 -0
- package/dist/ui/file-preview/src/main.js +5 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
- package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
- package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
- package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
- package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
- package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
- package/dist/ui/file-preview/src/markdown/linking.js +228 -0
- package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
- package/dist/ui/file-preview/src/markdown/outline.js +16 -0
- package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
- package/dist/ui/file-preview/src/markdown/parser.js +38 -0
- package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/preview.js +20 -0
- package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
- package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
- package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/utils.js +15 -0
- package/dist/ui/file-preview/src/model.d.ts +35 -0
- package/dist/ui/file-preview/src/model.js +1 -0
- package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
- package/dist/ui/file-preview/src/panel-actions.js +182 -0
- package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
- package/dist/ui/file-preview/src/path-utils.js +64 -0
- package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
- package/dist/ui/file-preview/src/payload-utils.js +119 -0
- package/dist/ui/file-preview/src/types.d.ts +1 -0
- package/dist/ui/file-preview/src/types.js +1 -0
- package/dist/ui/file-preview/styles.css +1631 -0
- package/dist/ui/resources.d.ts +28 -0
- package/dist/ui/resources.js +86 -0
- package/dist/ui/shared/compact-row.d.ts +11 -0
- package/dist/ui/shared/compact-row.js +18 -0
- package/dist/ui/shared/escape-html.d.ts +4 -0
- package/dist/ui/shared/escape-html.js +11 -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 +25 -0
- package/dist/ui/shared/tool-shell.js +107 -0
- package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
- package/dist/ui/shared/ui-event-tracker.js +27 -0
- package/dist/ui/shared/widget-state.d.ts +33 -0
- package/dist/ui/shared/widget-state.js +158 -0
- package/dist/utils/ab-test.d.ts +12 -0
- package/dist/utils/ab-test.js +104 -0
- package/dist/utils/capture.d.ts +35 -0
- package/dist/utils/capture.js +481 -0
- package/dist/utils/dockerPrompt.d.ts +17 -0
- package/dist/utils/dockerPrompt.js +53 -0
- package/dist/utils/feature-flags.d.ts +59 -0
- package/dist/utils/feature-flags.js +215 -0
- package/dist/utils/files/base.d.ts +173 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/docx.d.ts +47 -0
- package/dist/utils/files/docx.js +679 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +437 -0
- package/dist/utils/files/factory.d.ts +41 -0
- package/dist/utils/files/factory.js +113 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +370 -0
- package/dist/utils/fuzzySearchLogger.d.ts +30 -0
- package/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/utils/lineEndingHandler.d.ts +21 -0
- package/dist/utils/lineEndingHandler.js +77 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +72 -0
- package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
- package/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.js +42 -0
- package/dist/utils/process-detection.d.ts +23 -0
- package/dist/utils/process-detection.js +150 -0
- package/dist/utils/ripgrep-resolver.d.ts +9 -0
- package/dist/utils/ripgrep-resolver.js +74 -0
- package/dist/utils/system-info.d.ts +71 -0
- package/dist/utils/system-info.js +712 -0
- package/dist/utils/toolHistory.d.ts +86 -0
- package/dist/utils/toolHistory.js +257 -0
- package/dist/utils/trackTools.d.ts +6 -0
- package/dist/utils/trackTools.js +54 -0
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/utils/usageTracker.d.ts +133 -0
- package/dist/utils/usageTracker.js +477 -0
- package/dist/utils/welcome-onboarding.d.ts +11 -0
- package/dist/utils/welcome-onboarding.js +99 -0
- package/dist/utils/withTimeout.d.ts +28 -0
- package/dist/utils/withTimeout.js +85 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composition root for the File Preview app. It wires host services, file-type handlers, and specialized controllers together without owning feature logic inline.
|
|
3
|
+
*/
|
|
4
|
+
import { App } from '@modelcontextprotocol/ext-apps';
|
|
5
|
+
import { createCompactRowShellController } from '../../shared/tool-shell.js';
|
|
6
|
+
import { createWidgetStateStorage } from '../../shared/widget-state.js';
|
|
7
|
+
import { renderCompactRow } from '../../shared/compact-row.js';
|
|
8
|
+
import { connectWithSharedHostContext } from '../../shared/host-context.js';
|
|
9
|
+
import { createUiEventTracker } from '../../shared/ui-event-tracker.js';
|
|
10
|
+
import { attachDirectoryHandlers } from './directory-controller.js';
|
|
11
|
+
import { buildDocumentLayout } from './document-layout.js';
|
|
12
|
+
import { getDocumentFullscreenAvailability, parseReadRange, stripReadStatusLine } from './document-workspace.js';
|
|
13
|
+
import { getFileTypeCapabilities, renderPayloadBody } from './file-type-handlers.js';
|
|
14
|
+
import { buildOpenInEditorCommand, buildOpenInFolderCommand, renderMarkdownEditorAppIcon } from './host/external-actions.js';
|
|
15
|
+
import { attachSelectionContext } from './host/selection-context.js';
|
|
16
|
+
import { createMarkdownController } from './markdown/controller.js';
|
|
17
|
+
import { createConflictDialogController, renderConflictDialogMarkup, } from './markdown/conflict-dialog.js';
|
|
18
|
+
import { attachPanelActions } from './panel-actions.js';
|
|
19
|
+
import { extractRenderPayload, extractToolText, getFileExtensionForAnalytics, isLikelyUrl, isPreviewStructuredContent } from './payload-utils.js';
|
|
20
|
+
let isExpanded = false;
|
|
21
|
+
let hideSummaryRow = false;
|
|
22
|
+
let onRender;
|
|
23
|
+
let trackUiEvent;
|
|
24
|
+
let conflictDialogController;
|
|
25
|
+
let rpcCallTool;
|
|
26
|
+
let rpcUpdateContext;
|
|
27
|
+
let openExternalLink;
|
|
28
|
+
let requestDisplayMode;
|
|
29
|
+
let shellController;
|
|
30
|
+
let currentPayload;
|
|
31
|
+
let currentHtmlMode = 'rendered';
|
|
32
|
+
let currentHostContext;
|
|
33
|
+
let rerenderCurrent;
|
|
34
|
+
let syncPayload;
|
|
35
|
+
let persistPayload;
|
|
36
|
+
let localPayloadOverride;
|
|
37
|
+
let hostPayload;
|
|
38
|
+
let inlinePayloadBeforeFullscreen;
|
|
39
|
+
let directoryBackPayload;
|
|
40
|
+
let selectionAbortController = null;
|
|
41
|
+
const markdownEditorAppCache = new Map();
|
|
42
|
+
function getTelemetryToolName(payload) {
|
|
43
|
+
return typeof payload?.sourceTool === 'string' ? payload.sourceTool : 'read_file';
|
|
44
|
+
}
|
|
45
|
+
async function callToolIfReady(name, args) {
|
|
46
|
+
return rpcCallTool ? rpcCallTool(name, args) : undefined;
|
|
47
|
+
}
|
|
48
|
+
function getAvailableDisplayModes() {
|
|
49
|
+
const rawModes = currentHostContext?.availableDisplayModes;
|
|
50
|
+
if (!Array.isArray(rawModes)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
return rawModes.filter((mode) => typeof mode === 'string');
|
|
54
|
+
}
|
|
55
|
+
function getCurrentDisplayMode() {
|
|
56
|
+
return typeof currentHostContext?.displayMode === 'string'
|
|
57
|
+
? currentHostContext.displayMode
|
|
58
|
+
: null;
|
|
59
|
+
}
|
|
60
|
+
function storePayloadOverride(payload) {
|
|
61
|
+
localPayloadOverride = payload;
|
|
62
|
+
currentPayload = payload;
|
|
63
|
+
persistPayload?.(payload);
|
|
64
|
+
}
|
|
65
|
+
function getEffectiveIncomingPayload(payload) {
|
|
66
|
+
if (!localPayloadOverride) {
|
|
67
|
+
return payload;
|
|
68
|
+
}
|
|
69
|
+
if (localPayloadOverride.filePath !== payload.filePath) {
|
|
70
|
+
localPayloadOverride = undefined;
|
|
71
|
+
return payload;
|
|
72
|
+
}
|
|
73
|
+
const incomingContent = stripReadStatusLine(payload.content);
|
|
74
|
+
const overriddenContent = stripReadStatusLine(localPayloadOverride.content);
|
|
75
|
+
if (incomingContent === overriddenContent) {
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
78
|
+
return localPayloadOverride;
|
|
79
|
+
}
|
|
80
|
+
function updateSaveStatusDOM(label, statusClass) {
|
|
81
|
+
const existing = document.querySelector('.panel-save-status');
|
|
82
|
+
if (label) {
|
|
83
|
+
if (existing) {
|
|
84
|
+
existing.textContent = label;
|
|
85
|
+
existing.className = `panel-save-status panel-save-status--${statusClass}`;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const actions = document.querySelector('.panel-topbar-actions');
|
|
89
|
+
if (actions) {
|
|
90
|
+
const span = document.createElement('span');
|
|
91
|
+
span.className = `panel-save-status panel-save-status--${statusClass}`;
|
|
92
|
+
span.textContent = label;
|
|
93
|
+
actions.prepend(span);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (existing) {
|
|
98
|
+
existing.remove();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const markdownController = createMarkdownController({
|
|
102
|
+
callTool: callToolIfReady,
|
|
103
|
+
openExternalLink: async (url) => (openExternalLink ? openExternalLink(url) : undefined),
|
|
104
|
+
requestDisplayMode: async (mode) => (requestDisplayMode ? requestDisplayMode(mode) : undefined),
|
|
105
|
+
getAvailableDisplayModes,
|
|
106
|
+
getCurrentDisplayMode,
|
|
107
|
+
getCurrentPayload: () => currentPayload,
|
|
108
|
+
setExpanded: (expanded) => {
|
|
109
|
+
isExpanded = expanded;
|
|
110
|
+
},
|
|
111
|
+
syncPayload: (payload) => syncPayload?.(payload),
|
|
112
|
+
storePayloadOverride,
|
|
113
|
+
rerender: () => {
|
|
114
|
+
rerenderCurrent?.();
|
|
115
|
+
},
|
|
116
|
+
updateSaveStatus: updateSaveStatusDOM,
|
|
117
|
+
trackUiEvent: (event, params) => trackUiEvent?.(event, params),
|
|
118
|
+
showConflictDialog: (options) => {
|
|
119
|
+
if (conflictDialogController) {
|
|
120
|
+
conflictDialogController.open(options);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Dialog not yet initialized (would only happen if the save failure
|
|
124
|
+
// somehow fires before bootstrapApp). Fall back to the cancel callback
|
|
125
|
+
// so the editor still shows its inline note instead of silently no-op'ing.
|
|
126
|
+
console.warn('[file-preview] conflictDialogController not ready; firing onCancel fallback');
|
|
127
|
+
options.onCancel?.();
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
// Only read_file's own parameters may ride along on a re-read. Forwarding a
|
|
131
|
+
// different tool's args (e.g. edit_block's old_string) makes the server prepend
|
|
132
|
+
// an "unsupported params" warning that would pollute the pulled content.
|
|
133
|
+
const READ_ARG_KEYS = ['path', 'offset', 'length', 'sheet', 'range', 'isUrl'];
|
|
134
|
+
function pickReadArgs(args) {
|
|
135
|
+
if (!args || typeof args !== 'object') {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const src = args;
|
|
139
|
+
// edit_block calls the file "file_path"; read_file/write_file call it "path".
|
|
140
|
+
const path = typeof src.path === 'string'
|
|
141
|
+
? src.path
|
|
142
|
+
: typeof src.file_path === 'string'
|
|
143
|
+
? src.file_path
|
|
144
|
+
: undefined;
|
|
145
|
+
if (!path) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const out = { path };
|
|
149
|
+
for (const key of READ_ARG_KEYS) {
|
|
150
|
+
if (key !== 'path' && src[key] !== undefined) {
|
|
151
|
+
out[key] = src[key];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
// The host doesn't send the tool name with tool-input, so infer mutations from
|
|
157
|
+
// their arg shape: write_file carries `content`; edit_block `old_string`/
|
|
158
|
+
// `new_string`. Drives two things: timing (a mutation's tool-input arrives
|
|
159
|
+
// BEFORE the file changes, so those pull at tool-result time instead of input
|
|
160
|
+
// time) and telemetry (the pulled payload's sourceTool is re-stamped with the
|
|
161
|
+
// originating tool, since the pull itself is always a read_file).
|
|
162
|
+
function inferMutationTool(args) {
|
|
163
|
+
if (!args || typeof args !== 'object') {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const src = args;
|
|
167
|
+
if (src.old_string !== undefined || src.new_string !== undefined) {
|
|
168
|
+
return 'edit_block';
|
|
169
|
+
}
|
|
170
|
+
if (src.content !== undefined) {
|
|
171
|
+
return 'write_file';
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
// Bound the RPC read so a stalled host response resolves to onFail instead of
|
|
176
|
+
// hanging. Kept under the loading watchdog (PREVIEW_WATCHDOG_MS) so the pull
|
|
177
|
+
// gets its chance to fail before the failure row appears.
|
|
178
|
+
const PULL_TIMEOUT_MS = 8000;
|
|
179
|
+
// One in-flight pull per path — the input-time pull and a tool-result-triggered
|
|
180
|
+
// pull for the same call would otherwise read the file twice.
|
|
181
|
+
let pullInFlightPath;
|
|
182
|
+
/**
|
|
183
|
+
* The widget's render path: pull content + metadata over the RPC channel
|
|
184
|
+
* (`app.callServerTool` with origin:'ui'). The notification channel is not used
|
|
185
|
+
* for rendering — the host may strip structuredContent from it or swallow it
|
|
186
|
+
* entirely (images). `args` reproduces the original read exactly
|
|
187
|
+
* (offset/length/sheet/range/isUrl) so partial reads stay faithful.
|
|
188
|
+
*/
|
|
189
|
+
async function pullPayloadByArgs(args, onReady, onFail) {
|
|
190
|
+
const filePath = typeof args.path === 'string' ? args.path : undefined;
|
|
191
|
+
if (!filePath) {
|
|
192
|
+
onFail();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (pullInFlightPath === filePath) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
pullInFlightPath = filePath;
|
|
199
|
+
try {
|
|
200
|
+
// The host can hold the RPC response indefinitely (e.g. it preempts
|
|
201
|
+
// delivery to inline-render an image), so callServerTool would await
|
|
202
|
+
// forever with no throw. Race a timeout so the pull settles either way.
|
|
203
|
+
const raw = await Promise.race([
|
|
204
|
+
callToolIfReady('read_file', { ...args, origin: 'ui' }),
|
|
205
|
+
new Promise((_resolve, reject) => {
|
|
206
|
+
setTimeout(() => reject(new Error(`RPC read_file did not respond within ${PULL_TIMEOUT_MS}ms`)), PULL_TIMEOUT_MS);
|
|
207
|
+
}),
|
|
208
|
+
]);
|
|
209
|
+
const payload = extractRenderPayload(raw);
|
|
210
|
+
if (payload) {
|
|
211
|
+
onReady(payload);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Timed out or rejected — fall through to the caller's fallback.
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
pullInFlightPath = undefined;
|
|
220
|
+
}
|
|
221
|
+
onFail();
|
|
222
|
+
}
|
|
223
|
+
// If a loading state hasn't resolved into a real render within this window, the
|
|
224
|
+
// host most likely never delivered the tool result / RPC response (e.g. it
|
|
225
|
+
// preempted delivery to inline-render an image). Rather than sit on "Preparing
|
|
226
|
+
// preview…" forever, we surface a failure row.
|
|
227
|
+
const PREVIEW_WATCHDOG_MS = 10000;
|
|
228
|
+
let previewWatchdogId;
|
|
229
|
+
function clearPreviewWatchdog() {
|
|
230
|
+
if (previewWatchdogId !== undefined) {
|
|
231
|
+
clearTimeout(previewWatchdogId);
|
|
232
|
+
previewWatchdogId = undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function renderStatusState(container, message) {
|
|
236
|
+
clearPreviewWatchdog();
|
|
237
|
+
container.innerHTML = `
|
|
238
|
+
<main class="shell">
|
|
239
|
+
${renderCompactRow({ label: message, variant: 'status', interactive: false })}
|
|
240
|
+
</main>
|
|
241
|
+
`;
|
|
242
|
+
document.body.classList.add('dc-ready');
|
|
243
|
+
}
|
|
244
|
+
function renderLoadingState(container) {
|
|
245
|
+
container.innerHTML = `
|
|
246
|
+
<main class="shell">
|
|
247
|
+
${renderCompactRow({ label: 'Preparing preview…', variant: 'loading', interactive: false })}
|
|
248
|
+
</main>
|
|
249
|
+
`;
|
|
250
|
+
document.body.classList.add('dc-ready');
|
|
251
|
+
// Fall back to a failure row if nothing renders in time. Any successful
|
|
252
|
+
// render (renderApp) or explicit status clears this first.
|
|
253
|
+
clearPreviewWatchdog();
|
|
254
|
+
previewWatchdogId = setTimeout(() => {
|
|
255
|
+
previewWatchdogId = undefined;
|
|
256
|
+
renderStatusState(container, 'Unable to generate preview in this environment.');
|
|
257
|
+
}, PREVIEW_WATCHDOG_MS);
|
|
258
|
+
}
|
|
259
|
+
export function renderApp(container, payload, htmlMode = 'rendered', expandedState = false) {
|
|
260
|
+
clearPreviewWatchdog();
|
|
261
|
+
isExpanded = expandedState;
|
|
262
|
+
currentHtmlMode = htmlMode;
|
|
263
|
+
shellController?.dispose();
|
|
264
|
+
shellController = undefined;
|
|
265
|
+
if (!payload || payload.fileType !== 'markdown') {
|
|
266
|
+
markdownController.clear();
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
markdownController.disposeHandles();
|
|
270
|
+
}
|
|
271
|
+
if (!payload) {
|
|
272
|
+
selectionAbortController?.abort();
|
|
273
|
+
selectionAbortController = null;
|
|
274
|
+
currentPayload = undefined;
|
|
275
|
+
renderStatusState(container, 'No preview available for this response.');
|
|
276
|
+
onRender?.();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
currentPayload = payload;
|
|
280
|
+
const capabilities = getFileTypeCapabilities(payload);
|
|
281
|
+
if (!capabilities.supportsPreview && hideSummaryRow) {
|
|
282
|
+
isExpanded = false;
|
|
283
|
+
}
|
|
284
|
+
const range = parseReadRange(payload.content);
|
|
285
|
+
const body = renderPayloadBody({
|
|
286
|
+
payload,
|
|
287
|
+
htmlMode,
|
|
288
|
+
startLine: range?.fromLine ?? 1,
|
|
289
|
+
markdownController,
|
|
290
|
+
});
|
|
291
|
+
const markdownWorkspace = payload.fileType === 'markdown' ? markdownController.getState(payload) : undefined;
|
|
292
|
+
const fileExtension = getFileExtensionForAnalytics(payload.filePath);
|
|
293
|
+
const isFullscreen = getCurrentDisplayMode() === 'fullscreen';
|
|
294
|
+
const canGoFullscreen = !isFullscreen && getDocumentFullscreenAvailability({
|
|
295
|
+
availableDisplayModes: getAvailableDisplayModes(),
|
|
296
|
+
}).canFullscreen;
|
|
297
|
+
if (payload.fileType === 'markdown' && payload.defaultEditorName) {
|
|
298
|
+
markdownEditorAppCache.set(payload.filePath, {
|
|
299
|
+
appName: payload.defaultEditorName,
|
|
300
|
+
appPath: payload.defaultEditorPath,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const defaultMarkdownEditor = payload.fileType === 'markdown'
|
|
304
|
+
? markdownEditorAppCache.get(payload.filePath)
|
|
305
|
+
: undefined;
|
|
306
|
+
const layout = buildDocumentLayout({
|
|
307
|
+
payload,
|
|
308
|
+
body,
|
|
309
|
+
capabilities,
|
|
310
|
+
fileExtension,
|
|
311
|
+
htmlMode,
|
|
312
|
+
currentDisplayMode: getCurrentDisplayMode(),
|
|
313
|
+
isExpanded,
|
|
314
|
+
hideSummaryRow,
|
|
315
|
+
markdownWorkspace,
|
|
316
|
+
canGoFullscreen,
|
|
317
|
+
isMarkdownUndoAvailable: markdownWorkspace ? markdownController.isUndoAvailable(markdownWorkspace) : false,
|
|
318
|
+
defaultMarkdownEditorName: defaultMarkdownEditor?.appName,
|
|
319
|
+
markdownEditorAppIcon: renderMarkdownEditorAppIcon(),
|
|
320
|
+
hasDirectoryBackButton: Boolean(directoryBackPayload),
|
|
321
|
+
});
|
|
322
|
+
container.innerHTML = layout.html;
|
|
323
|
+
document.body.classList.add('dc-ready');
|
|
324
|
+
attachPanelActions({
|
|
325
|
+
container,
|
|
326
|
+
payload,
|
|
327
|
+
htmlMode,
|
|
328
|
+
getIsExpanded: () => isExpanded,
|
|
329
|
+
callTool: callToolIfReady,
|
|
330
|
+
trackUiEvent,
|
|
331
|
+
getFileExtensionForAnalytics,
|
|
332
|
+
buildOpenInFolderCommand: (filePath) => buildOpenInFolderCommand(filePath, isLikelyUrl),
|
|
333
|
+
buildOpenInEditorCommand: (filePath) => buildOpenInEditorCommand(filePath, isLikelyUrl, markdownEditorAppCache),
|
|
334
|
+
render: (nextPayload, nextHtmlMode = 'rendered', nextExpanded = isExpanded) => {
|
|
335
|
+
renderApp(container, nextPayload, nextHtmlMode, nextExpanded);
|
|
336
|
+
},
|
|
337
|
+
updateSaveStatus: updateSaveStatusDOM,
|
|
338
|
+
markdownController,
|
|
339
|
+
});
|
|
340
|
+
if (payload.fileType === 'markdown') {
|
|
341
|
+
markdownController.attachHandlers(payload);
|
|
342
|
+
}
|
|
343
|
+
selectionAbortController = attachSelectionContext({
|
|
344
|
+
payload,
|
|
345
|
+
isMarkdownEditing: payload.fileType === 'markdown' && !!markdownWorkspace,
|
|
346
|
+
updateContext: rpcUpdateContext,
|
|
347
|
+
trackUiEvent,
|
|
348
|
+
getFileExtensionForAnalytics,
|
|
349
|
+
previousAbortController: selectionAbortController,
|
|
350
|
+
});
|
|
351
|
+
if (payload.fileType === 'directory') {
|
|
352
|
+
attachDirectoryHandlers({
|
|
353
|
+
container,
|
|
354
|
+
callTool: callToolIfReady,
|
|
355
|
+
buildOpenInFolderCommand: (filePath) => buildOpenInFolderCommand(filePath, isLikelyUrl),
|
|
356
|
+
onOpenPayload: (nextPayload) => {
|
|
357
|
+
directoryBackPayload = payload;
|
|
358
|
+
renderApp(container, nextPayload, 'rendered', true);
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const backBtn = document.getElementById('dir-back');
|
|
363
|
+
if (backBtn && directoryBackPayload) {
|
|
364
|
+
const savedPayload = directoryBackPayload;
|
|
365
|
+
backBtn.addEventListener('click', () => {
|
|
366
|
+
directoryBackPayload = undefined;
|
|
367
|
+
renderApp(container, savedPayload, 'rendered', true);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
if (payload.fileType === 'directory') {
|
|
371
|
+
directoryBackPayload = undefined;
|
|
372
|
+
}
|
|
373
|
+
const compactRow = document.getElementById('compact-toggle');
|
|
374
|
+
shellController = createCompactRowShellController({
|
|
375
|
+
shell: document.getElementById('tool-shell'),
|
|
376
|
+
compactRow,
|
|
377
|
+
initialExpanded: layout.effectiveExpanded,
|
|
378
|
+
onToggle: (expanded) => {
|
|
379
|
+
isExpanded = expanded;
|
|
380
|
+
trackUiEvent?.(expanded ? 'expand' : 'collapse', {
|
|
381
|
+
file_type: payload.fileType,
|
|
382
|
+
file_extension: fileExtension,
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
onScrollAfterExpand: () => {
|
|
386
|
+
trackUiEvent?.('scroll_after_expand', {
|
|
387
|
+
file_type: payload.fileType,
|
|
388
|
+
file_extension: fileExtension,
|
|
389
|
+
});
|
|
390
|
+
},
|
|
391
|
+
onRender,
|
|
392
|
+
});
|
|
393
|
+
onRender?.();
|
|
394
|
+
}
|
|
395
|
+
export function bootstrapApp() {
|
|
396
|
+
const container = document.getElementById('app');
|
|
397
|
+
if (!container) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
renderLoadingState(container);
|
|
401
|
+
// Mount the conflict dialog once at body level. It's position: fixed and
|
|
402
|
+
// must live outside the app container so that re-renders of the document
|
|
403
|
+
// body never wipe it while it's open.
|
|
404
|
+
if (!document.getElementById('md-conflict-modal')) {
|
|
405
|
+
const dialogHost = document.createElement('div');
|
|
406
|
+
dialogHost.innerHTML = renderConflictDialogMarkup();
|
|
407
|
+
const dialogRoot = dialogHost.firstElementChild;
|
|
408
|
+
if (dialogRoot) {
|
|
409
|
+
document.body.appendChild(dialogRoot);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
conflictDialogController = createConflictDialogController({ container: document });
|
|
413
|
+
const app = new App({ name: 'MCP Device File Preview', version: '1.0.0' }, { updateModelContext: { text: {} } }, { autoResize: true });
|
|
414
|
+
const chrome = {
|
|
415
|
+
expanded: isExpanded,
|
|
416
|
+
hideSummaryRow,
|
|
417
|
+
};
|
|
418
|
+
const syncChromeState = () => {
|
|
419
|
+
isExpanded = chrome.expanded;
|
|
420
|
+
hideSummaryRow = chrome.hideSummaryRow;
|
|
421
|
+
};
|
|
422
|
+
const widgetState = createWidgetStateStorage((value) => isPreviewStructuredContent(value) && typeof value.content === 'string');
|
|
423
|
+
const renderAndSync = (payload) => {
|
|
424
|
+
if (payload) {
|
|
425
|
+
widgetState.write(payload);
|
|
426
|
+
}
|
|
427
|
+
renderApp(container, payload, 'rendered', isExpanded);
|
|
428
|
+
};
|
|
429
|
+
const syncFromPersistedWidgetState = () => {
|
|
430
|
+
const persistedPayload = widgetState.read();
|
|
431
|
+
if (!persistedPayload) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (currentPayload
|
|
435
|
+
&& currentPayload.filePath === persistedPayload.filePath
|
|
436
|
+
&& stripReadStatusLine(currentPayload.content) === stripReadStatusLine(persistedPayload.content)) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
renderAndSync(persistedPayload);
|
|
440
|
+
};
|
|
441
|
+
syncPayload = renderAndSync;
|
|
442
|
+
persistPayload = (payload) => {
|
|
443
|
+
widgetState.write(payload);
|
|
444
|
+
};
|
|
445
|
+
rerenderCurrent = () => {
|
|
446
|
+
renderApp(container, currentPayload, currentHtmlMode, isExpanded);
|
|
447
|
+
};
|
|
448
|
+
let pendingCachedPayload;
|
|
449
|
+
let lastToolInputArgs;
|
|
450
|
+
// The mutation tool that produced the current tool call, if any — stamped
|
|
451
|
+
// onto the pulled payload so telemetry attributes to write_file/edit_block
|
|
452
|
+
// rather than the read_file pull that rendered it.
|
|
453
|
+
let lastMutationTool;
|
|
454
|
+
// True once the current tool call has been rendered (via the input-time pull),
|
|
455
|
+
// so a late tool-result notification doesn't trigger a redundant second pull.
|
|
456
|
+
let renderedForCurrentInput = false;
|
|
457
|
+
let initialStateResolved = false;
|
|
458
|
+
const resolveInitialState = (payload, message) => {
|
|
459
|
+
if (initialStateResolved) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
initialStateResolved = true;
|
|
463
|
+
if (payload) {
|
|
464
|
+
hostPayload = payload;
|
|
465
|
+
renderAndSync(payload);
|
|
466
|
+
if (payload.fileType === 'markdown' && getCurrentDisplayMode() === 'fullscreen') {
|
|
467
|
+
void markdownController.requestEditMode(payload);
|
|
468
|
+
}
|
|
469
|
+
if (payload.fileType === 'markdown') {
|
|
470
|
+
void markdownController.refreshFromDisk(payload);
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
renderStatusState(container, message ?? 'No preview available for this response.');
|
|
475
|
+
onRender?.();
|
|
476
|
+
};
|
|
477
|
+
onRender = () => { };
|
|
478
|
+
rpcCallTool = (name, args) => (app.callServerTool({ name, arguments: args }));
|
|
479
|
+
rpcUpdateContext = (text) => {
|
|
480
|
+
const params = text
|
|
481
|
+
? { content: [{ type: 'text', text }] }
|
|
482
|
+
: { content: [] };
|
|
483
|
+
app.updateModelContext(params).catch(() => {
|
|
484
|
+
// Host may not support updateModelContext.
|
|
485
|
+
});
|
|
486
|
+
};
|
|
487
|
+
openExternalLink = async (url) => {
|
|
488
|
+
const result = await app.openLink({ url });
|
|
489
|
+
return result.isError !== true;
|
|
490
|
+
};
|
|
491
|
+
requestDisplayMode = async (mode) => {
|
|
492
|
+
const result = await app.requestDisplayMode({ mode });
|
|
493
|
+
return typeof result.mode === 'string' ? result.mode : null;
|
|
494
|
+
};
|
|
495
|
+
const filePreviewUiEvent = createUiEventTracker((name, args) => app.callServerTool({ name, arguments: args }), {
|
|
496
|
+
component: 'file_preview',
|
|
497
|
+
});
|
|
498
|
+
trackUiEvent = (event, params = {}) => filePreviewUiEvent(event, {
|
|
499
|
+
tool_name: getTelemetryToolName(currentPayload ?? hostPayload),
|
|
500
|
+
...params,
|
|
501
|
+
});
|
|
502
|
+
app.ontoolinput = (params) => {
|
|
503
|
+
const requestedPath = typeof params.arguments?.path === 'string' ? params.arguments.path : undefined;
|
|
504
|
+
const readArgs = pickReadArgs(params.arguments);
|
|
505
|
+
lastMutationTool = inferMutationTool(params.arguments);
|
|
506
|
+
if (readArgs) {
|
|
507
|
+
// Retained so mutations (which pull at tool-result time) know what
|
|
508
|
+
// to re-read — faithfully reproducing offset/length/sheet/range.
|
|
509
|
+
lastToolInputArgs = readArgs;
|
|
510
|
+
}
|
|
511
|
+
if (!initialStateResolved
|
|
512
|
+
&& pendingCachedPayload
|
|
513
|
+
&& requestedPath
|
|
514
|
+
&& pendingCachedPayload.filePath === requestedPath) {
|
|
515
|
+
const cached = pendingCachedPayload;
|
|
516
|
+
pendingCachedPayload = undefined;
|
|
517
|
+
resolveInitialState(cached);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
renderLoadingState(container);
|
|
521
|
+
onRender?.();
|
|
522
|
+
renderedForCurrentInput = false;
|
|
523
|
+
// All previews render from the widget's own origin:'ui' RPC read — the
|
|
524
|
+
// notification channel is unreliable (the host strips structuredContent,
|
|
525
|
+
// and for images swallows the tool-result entirely to inline-render).
|
|
526
|
+
// tool-input always fires and carries the path + read args, so pull here.
|
|
527
|
+
// Exception: mutations (write_file/edit_block) — their tool-input arrives
|
|
528
|
+
// before the file changes, so they pull at tool-result time instead.
|
|
529
|
+
if (readArgs && !lastMutationTool) {
|
|
530
|
+
void pullPayloadByArgs(readArgs, (p) => {
|
|
531
|
+
renderedForCurrentInput = true;
|
|
532
|
+
if (initialStateResolved) {
|
|
533
|
+
renderAndSync(getEffectiveIncomingPayload(p));
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
resolveInitialState(getEffectiveIncomingPayload(p));
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
// Pull failed/timed out — the loading watchdog shows the failure row.
|
|
540
|
+
() => { });
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
app.ontoolresult = (result) => {
|
|
544
|
+
pendingCachedPayload = undefined;
|
|
545
|
+
// Host-facing responses carry no structuredContent, so this notification
|
|
546
|
+
// only signals completion. Reads already rendered from their input-time
|
|
547
|
+
// pull; mutations (write_file/edit_block) pull now that the file changed.
|
|
548
|
+
if (renderedForCurrentInput) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const message = extractToolText(result);
|
|
552
|
+
const isError = result?.isError === true;
|
|
553
|
+
const pullArgs = lastToolInputArgs
|
|
554
|
+
?? (currentPayload?.filePath ? { path: currentPayload.filePath } : undefined);
|
|
555
|
+
const deliver = (pulled) => {
|
|
556
|
+
renderedForCurrentInput = true;
|
|
557
|
+
// The pull is always a read_file; re-stamp the originating mutation
|
|
558
|
+
// tool so telemetry attributes to write_file/edit_block.
|
|
559
|
+
const p = lastMutationTool ? { ...pulled, sourceTool: lastMutationTool } : pulled;
|
|
560
|
+
if (initialStateResolved) {
|
|
561
|
+
renderAndSync(getEffectiveIncomingPayload(p));
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
resolveInitialState(getEffectiveIncomingPayload(p));
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
const renderMessageFallback = () => {
|
|
568
|
+
if (!message) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (!initialStateResolved) {
|
|
572
|
+
resolveInitialState(undefined, message);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
renderStatusState(container, message);
|
|
576
|
+
onRender?.();
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
// A failed tool call has nothing worth re-reading — show its message.
|
|
580
|
+
if (!isError && pullArgs) {
|
|
581
|
+
renderLoadingState(container);
|
|
582
|
+
onRender?.();
|
|
583
|
+
void pullPayloadByArgs(pullArgs, deliver, renderMessageFallback);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
renderMessageFallback();
|
|
587
|
+
};
|
|
588
|
+
app.ontoolcancelled = (params) => {
|
|
589
|
+
resolveInitialState(undefined, params.reason ?? 'Tool was cancelled.');
|
|
590
|
+
};
|
|
591
|
+
const handleVisibilitySync = () => {
|
|
592
|
+
if (document.visibilityState === 'visible') {
|
|
593
|
+
syncFromPersistedWidgetState();
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
const handleFocusSync = () => {
|
|
597
|
+
// Only sync cross-tab state if the page was hidden (tab switch).
|
|
598
|
+
// Simple focus changes within the same page should not trigger a re-render
|
|
599
|
+
// as it destroys the active editor.
|
|
600
|
+
if (document.visibilityState !== 'visible') {
|
|
601
|
+
syncFromPersistedWidgetState();
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const teardown = () => {
|
|
605
|
+
clearPreviewWatchdog();
|
|
606
|
+
shellController?.dispose();
|
|
607
|
+
shellController = undefined;
|
|
608
|
+
markdownController.disposeHandles();
|
|
609
|
+
selectionAbortController?.abort();
|
|
610
|
+
selectionAbortController = null;
|
|
611
|
+
document.removeEventListener('visibilitychange', handleVisibilitySync);
|
|
612
|
+
window.removeEventListener('focus', handleFocusSync);
|
|
613
|
+
};
|
|
614
|
+
document.addEventListener('visibilitychange', handleVisibilitySync);
|
|
615
|
+
window.addEventListener('focus', handleFocusSync);
|
|
616
|
+
app.onteardown = async () => {
|
|
617
|
+
teardown();
|
|
618
|
+
return {};
|
|
619
|
+
};
|
|
620
|
+
void connectWithSharedHostContext({
|
|
621
|
+
app,
|
|
622
|
+
chrome,
|
|
623
|
+
onContextApplied: () => {
|
|
624
|
+
const previousDisplayMode = getCurrentDisplayMode();
|
|
625
|
+
syncChromeState();
|
|
626
|
+
currentHostContext = app.getHostContext();
|
|
627
|
+
const nextDisplayMode = getCurrentDisplayMode();
|
|
628
|
+
const displayModeChanged = previousDisplayMode !== nextDisplayMode;
|
|
629
|
+
// Clicking a display-mode button blurs the editor first, and the
|
|
630
|
+
// editor's onBlur handler already persists dirty drafts, so there
|
|
631
|
+
// is nothing additional to save here.
|
|
632
|
+
if (previousDisplayMode === 'fullscreen'
|
|
633
|
+
&& nextDisplayMode === 'inline'
|
|
634
|
+
&& currentPayload?.fileType === 'markdown') {
|
|
635
|
+
isExpanded = true;
|
|
636
|
+
chrome.expanded = true;
|
|
637
|
+
const restorePayload = inlinePayloadBeforeFullscreen ?? hostPayload;
|
|
638
|
+
const restoreWasPartial = restorePayload ? parseReadRange(restorePayload.content)?.isPartial === true : false;
|
|
639
|
+
if (restoreWasPartial && restorePayload) {
|
|
640
|
+
localPayloadOverride = restorePayload;
|
|
641
|
+
currentPayload = restorePayload;
|
|
642
|
+
widgetState.write(restorePayload);
|
|
643
|
+
void markdownController.handleInlineExitFromFullscreen(restorePayload).then((freshPayload) => {
|
|
644
|
+
if (freshPayload) {
|
|
645
|
+
currentPayload = freshPayload;
|
|
646
|
+
localPayloadOverride = freshPayload;
|
|
647
|
+
widgetState.write(freshPayload);
|
|
648
|
+
rerenderCurrent?.();
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
void markdownController.handleInlineExitFromFullscreen();
|
|
654
|
+
}
|
|
655
|
+
inlinePayloadBeforeFullscreen = undefined;
|
|
656
|
+
}
|
|
657
|
+
if (previousDisplayMode !== 'fullscreen'
|
|
658
|
+
&& nextDisplayMode === 'fullscreen'
|
|
659
|
+
&& currentPayload?.fileType === 'markdown') {
|
|
660
|
+
inlinePayloadBeforeFullscreen = currentPayload;
|
|
661
|
+
if (parseReadRange(currentPayload.content)?.isPartial) {
|
|
662
|
+
void markdownController.requestEditMode(currentPayload);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (initialStateResolved && displayModeChanged) {
|
|
666
|
+
rerenderCurrent?.();
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
onConnected: () => {
|
|
670
|
+
currentHostContext = app.getHostContext();
|
|
671
|
+
pendingCachedPayload = widgetState.read() ?? undefined;
|
|
672
|
+
},
|
|
673
|
+
}).catch(() => {
|
|
674
|
+
renderStatusState(container, 'Failed to connect to host.');
|
|
675
|
+
onRender?.();
|
|
676
|
+
});
|
|
677
|
+
window.addEventListener('beforeunload', () => {
|
|
678
|
+
teardown();
|
|
679
|
+
}, { once: true });
|
|
680
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function inferLanguageFromPath(filePath: string): string;
|
|
2
|
+
export declare function formatJsonIfPossible(content: string, filePath: string): {
|
|
3
|
+
content: string;
|
|
4
|
+
notice?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function renderCodeViewer(code: string, language?: string, startLine?: number): string;
|