@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.
Files changed (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/bootstrap.d.ts +1 -0
  4. package/dist/bootstrap.js +22 -0
  5. package/dist/command-manager.d.ts +8 -0
  6. package/dist/command-manager.js +241 -0
  7. package/dist/config-field-definitions.d.ts +41 -0
  8. package/dist/config-field-definitions.js +37 -0
  9. package/dist/config-manager.d.ts +102 -0
  10. package/dist/config-manager.js +291 -0
  11. package/dist/config.d.ts +6 -0
  12. package/dist/config.js +10 -0
  13. package/dist/custom-stdio.d.ts +51 -0
  14. package/dist/custom-stdio.js +358 -0
  15. package/dist/data/official-app-ca.pem +12 -0
  16. package/dist/data/onboarding-prompts.json +70 -0
  17. package/dist/device/device-state.d.ts +13 -0
  18. package/dist/device/device-state.js +100 -0
  19. package/dist/device/device-status.d.ts +81 -0
  20. package/dist/device/device-status.js +156 -0
  21. package/dist/device/device.d.ts +16 -0
  22. package/dist/device/device.js +190 -0
  23. package/dist/device/execution-engine.d.ts +144 -0
  24. package/dist/device/execution-engine.js +195 -0
  25. package/dist/device/gateway-channel.d.ts +48 -0
  26. package/dist/device/gateway-channel.js +384 -0
  27. package/dist/device/gateway-config.d.ts +40 -0
  28. package/dist/device/gateway-config.js +207 -0
  29. package/dist/device/gateway-identity.d.ts +35 -0
  30. package/dist/device/gateway-identity.js +178 -0
  31. package/dist/device/gateway-pairing.d.ts +25 -0
  32. package/dist/device/gateway-pairing.js +105 -0
  33. package/dist/device/gateway-secure-transport.d.ts +25 -0
  34. package/dist/device/gateway-secure-transport.js +117 -0
  35. package/dist/device/gateway-tool-adapter.d.ts +17 -0
  36. package/dist/device/gateway-tool-adapter.js +284 -0
  37. package/dist/device/gateway-url-policy.d.ts +3 -0
  38. package/dist/device/gateway-url-policy.js +43 -0
  39. package/dist/device/linux-service.d.ts +70 -0
  40. package/dist/device/linux-service.js +268 -0
  41. package/dist/device/official-trust.d.ts +7 -0
  42. package/dist/device/official-trust.js +49 -0
  43. package/dist/device/project-inspection.d.ts +1 -0
  44. package/dist/device/project-inspection.js +180 -0
  45. package/dist/device/runtime-owner.d.ts +41 -0
  46. package/dist/device/runtime-owner.js +177 -0
  47. package/dist/device/windows-dpapi.d.ts +11 -0
  48. package/dist/device/windows-dpapi.js +67 -0
  49. package/dist/device/windows-service.d.ts +56 -0
  50. package/dist/device/windows-service.js +238 -0
  51. package/dist/error-handlers.d.ts +7 -0
  52. package/dist/error-handlers.js +15 -0
  53. package/dist/handlers/edit-search-handlers.d.ts +6 -0
  54. package/dist/handlers/edit-search-handlers.js +6 -0
  55. package/dist/handlers/filesystem-handlers.d.ts +38 -0
  56. package/dist/handlers/filesystem-handlers.js +440 -0
  57. package/dist/handlers/history-handlers.d.ts +12 -0
  58. package/dist/handlers/history-handlers.js +67 -0
  59. package/dist/handlers/index.d.ts +6 -0
  60. package/dist/handlers/index.js +7 -0
  61. package/dist/handlers/process-handlers.d.ts +9 -0
  62. package/dist/handlers/process-handlers.js +15 -0
  63. package/dist/handlers/search-handlers.d.ts +17 -0
  64. package/dist/handlers/search-handlers.js +224 -0
  65. package/dist/handlers/terminal-handlers.d.ts +21 -0
  66. package/dist/handlers/terminal-handlers.js +35 -0
  67. package/dist/index.d.ts +2 -0
  68. package/dist/index.js +138 -0
  69. package/dist/mcp-device.d.ts +2 -0
  70. package/dist/mcp-device.js +7 -0
  71. package/dist/npm-scripts/remote.d.ts +3 -0
  72. package/dist/npm-scripts/remote.js +314 -0
  73. package/dist/search-manager.d.ts +147 -0
  74. package/dist/search-manager.js +828 -0
  75. package/dist/server.d.ts +46 -0
  76. package/dist/server.js +1563 -0
  77. package/dist/terminal-manager.d.ts +104 -0
  78. package/dist/terminal-manager.js +635 -0
  79. package/dist/tools/config.d.ts +96 -0
  80. package/dist/tools/config.js +260 -0
  81. package/dist/tools/edit.d.ts +38 -0
  82. package/dist/tools/edit.js +435 -0
  83. package/dist/tools/feedback.d.ts +8 -0
  84. package/dist/tools/feedback.js +132 -0
  85. package/dist/tools/filesystem.d.ts +79 -0
  86. package/dist/tools/filesystem.js +947 -0
  87. package/dist/tools/fuzzySearch.d.ts +12 -0
  88. package/dist/tools/fuzzySearch.js +74 -0
  89. package/dist/tools/fuzzySearchCore.d.ts +52 -0
  90. package/dist/tools/fuzzySearchCore.js +125 -0
  91. package/dist/tools/improved-process-tools.d.ts +24 -0
  92. package/dist/tools/improved-process-tools.js +627 -0
  93. package/dist/tools/mime-types.d.ts +3 -0
  94. package/dist/tools/mime-types.js +28 -0
  95. package/dist/tools/pdf/extract-images.d.ts +34 -0
  96. package/dist/tools/pdf/extract-images.js +132 -0
  97. package/dist/tools/pdf/index.d.ts +6 -0
  98. package/dist/tools/pdf/index.js +3 -0
  99. package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
  100. package/dist/tools/pdf/lib/pdf2md.js +76 -0
  101. package/dist/tools/pdf/manipulations.d.ts +13 -0
  102. package/dist/tools/pdf/manipulations.js +99 -0
  103. package/dist/tools/pdf/markdown.d.ts +26 -0
  104. package/dist/tools/pdf/markdown.js +275 -0
  105. package/dist/tools/pdf/utils.d.ts +12 -0
  106. package/dist/tools/pdf/utils.js +34 -0
  107. package/dist/tools/process.d.ts +3 -0
  108. package/dist/tools/process.js +56 -0
  109. package/dist/tools/prompts.d.ts +28 -0
  110. package/dist/tools/prompts.js +259 -0
  111. package/dist/tools/schemas.d.ts +454 -0
  112. package/dist/tools/schemas.js +242 -0
  113. package/dist/tools/usage.d.ts +5 -0
  114. package/dist/tools/usage.js +24 -0
  115. package/dist/types.d.ts +83 -0
  116. package/dist/types.js +1 -0
  117. package/dist/ui/config-editor/config-editor-runtime.js +150 -0
  118. package/dist/ui/config-editor/index.html +13 -0
  119. package/dist/ui/config-editor/src/app.d.ts +43 -0
  120. package/dist/ui/config-editor/src/app.js +831 -0
  121. package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
  122. package/dist/ui/config-editor/src/array-modal.js +185 -0
  123. package/dist/ui/config-editor/src/main.d.ts +1 -0
  124. package/dist/ui/config-editor/src/main.js +2 -0
  125. package/dist/ui/config-editor/styles.css +587 -0
  126. package/dist/ui/contracts.d.ts +14 -0
  127. package/dist/ui/contracts.js +21 -0
  128. package/dist/ui/file-preview/index.html +16 -0
  129. package/dist/ui/file-preview/preview-runtime.js +469 -0
  130. package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
  131. package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
  132. package/dist/ui/file-preview/src/app.d.ts +4 -0
  133. package/dist/ui/file-preview/src/app.js +680 -0
  134. package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
  135. package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
  136. package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
  137. package/dist/ui/file-preview/src/components/highlighting.js +54 -0
  138. package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
  139. package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
  140. package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
  141. package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
  142. package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
  143. package/dist/ui/file-preview/src/directory-controller.js +240 -0
  144. package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
  145. package/dist/ui/file-preview/src/document-layout.js +109 -0
  146. package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
  147. package/dist/ui/file-preview/src/document-outline.js +97 -0
  148. package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
  149. package/dist/ui/file-preview/src/document-workspace.js +33 -0
  150. package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
  151. package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
  152. package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
  153. package/dist/ui/file-preview/src/host/external-actions.js +55 -0
  154. package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
  155. package/dist/ui/file-preview/src/host/selection-context.js +106 -0
  156. package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
  157. package/dist/ui/file-preview/src/image-preview.js +21 -0
  158. package/dist/ui/file-preview/src/main.d.ts +1 -0
  159. package/dist/ui/file-preview/src/main.js +5 -0
  160. package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
  161. package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
  162. package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
  163. package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
  164. package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
  165. package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
  166. package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
  167. package/dist/ui/file-preview/src/markdown/linking.js +228 -0
  168. package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
  169. package/dist/ui/file-preview/src/markdown/outline.js +16 -0
  170. package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
  171. package/dist/ui/file-preview/src/markdown/parser.js +38 -0
  172. package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
  173. package/dist/ui/file-preview/src/markdown/preview.js +20 -0
  174. package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
  175. package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
  176. package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
  177. package/dist/ui/file-preview/src/markdown/utils.js +15 -0
  178. package/dist/ui/file-preview/src/model.d.ts +35 -0
  179. package/dist/ui/file-preview/src/model.js +1 -0
  180. package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
  181. package/dist/ui/file-preview/src/panel-actions.js +182 -0
  182. package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
  183. package/dist/ui/file-preview/src/path-utils.js +64 -0
  184. package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
  185. package/dist/ui/file-preview/src/payload-utils.js +119 -0
  186. package/dist/ui/file-preview/src/types.d.ts +1 -0
  187. package/dist/ui/file-preview/src/types.js +1 -0
  188. package/dist/ui/file-preview/styles.css +1631 -0
  189. package/dist/ui/resources.d.ts +28 -0
  190. package/dist/ui/resources.js +86 -0
  191. package/dist/ui/shared/compact-row.d.ts +11 -0
  192. package/dist/ui/shared/compact-row.js +18 -0
  193. package/dist/ui/shared/escape-html.d.ts +4 -0
  194. package/dist/ui/shared/escape-html.js +11 -0
  195. package/dist/ui/shared/host-context.d.ts +15 -0
  196. package/dist/ui/shared/host-context.js +51 -0
  197. package/dist/ui/shared/tool-bridge.d.ts +30 -0
  198. package/dist/ui/shared/tool-bridge.js +137 -0
  199. package/dist/ui/shared/tool-shell.d.ts +25 -0
  200. package/dist/ui/shared/tool-shell.js +107 -0
  201. package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
  202. package/dist/ui/shared/ui-event-tracker.js +27 -0
  203. package/dist/ui/shared/widget-state.d.ts +33 -0
  204. package/dist/ui/shared/widget-state.js +158 -0
  205. package/dist/utils/ab-test.d.ts +12 -0
  206. package/dist/utils/ab-test.js +104 -0
  207. package/dist/utils/capture.d.ts +35 -0
  208. package/dist/utils/capture.js +481 -0
  209. package/dist/utils/dockerPrompt.d.ts +17 -0
  210. package/dist/utils/dockerPrompt.js +53 -0
  211. package/dist/utils/feature-flags.d.ts +59 -0
  212. package/dist/utils/feature-flags.js +215 -0
  213. package/dist/utils/files/base.d.ts +173 -0
  214. package/dist/utils/files/base.js +5 -0
  215. package/dist/utils/files/binary.d.ts +21 -0
  216. package/dist/utils/files/binary.js +65 -0
  217. package/dist/utils/files/docx.d.ts +47 -0
  218. package/dist/utils/files/docx.js +679 -0
  219. package/dist/utils/files/excel.d.ts +24 -0
  220. package/dist/utils/files/excel.js +437 -0
  221. package/dist/utils/files/factory.d.ts +41 -0
  222. package/dist/utils/files/factory.js +113 -0
  223. package/dist/utils/files/image.d.ts +21 -0
  224. package/dist/utils/files/image.js +78 -0
  225. package/dist/utils/files/index.d.ts +10 -0
  226. package/dist/utils/files/index.js +13 -0
  227. package/dist/utils/files/pdf.d.ts +32 -0
  228. package/dist/utils/files/pdf.js +142 -0
  229. package/dist/utils/files/text.d.ts +63 -0
  230. package/dist/utils/files/text.js +370 -0
  231. package/dist/utils/fuzzySearchLogger.d.ts +30 -0
  232. package/dist/utils/fuzzySearchLogger.js +126 -0
  233. package/dist/utils/lineEndingHandler.d.ts +21 -0
  234. package/dist/utils/lineEndingHandler.js +77 -0
  235. package/dist/utils/logger.d.ts +32 -0
  236. package/dist/utils/logger.js +72 -0
  237. package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
  238. package/dist/utils/mcp-ui-ab-test.js +62 -0
  239. package/dist/utils/open-browser.d.ts +9 -0
  240. package/dist/utils/open-browser.js +42 -0
  241. package/dist/utils/process-detection.d.ts +23 -0
  242. package/dist/utils/process-detection.js +150 -0
  243. package/dist/utils/ripgrep-resolver.d.ts +9 -0
  244. package/dist/utils/ripgrep-resolver.js +74 -0
  245. package/dist/utils/system-info.d.ts +71 -0
  246. package/dist/utils/system-info.js +712 -0
  247. package/dist/utils/toolHistory.d.ts +86 -0
  248. package/dist/utils/toolHistory.js +257 -0
  249. package/dist/utils/trackTools.d.ts +6 -0
  250. package/dist/utils/trackTools.js +54 -0
  251. package/dist/utils/unsupportedParams.d.ts +24 -0
  252. package/dist/utils/unsupportedParams.js +66 -0
  253. package/dist/utils/usageTracker.d.ts +133 -0
  254. package/dist/utils/usageTracker.js +477 -0
  255. package/dist/utils/welcome-onboarding.d.ts +11 -0
  256. package/dist/utils/welcome-onboarding.js +99 -0
  257. package/dist/utils/withTimeout.d.ts +28 -0
  258. package/dist/utils/withTimeout.js +85 -0
  259. package/dist/version.d.ts +1 -0
  260. package/dist/version.js +1 -0
  261. 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;