@wonderwhy-er/desktop-commander 0.2.38 → 0.2.40
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 +53 -2
- package/dist/handlers/filesystem-handlers.d.ts +5 -0
- package/dist/handlers/filesystem-handlers.js +14 -2
- package/dist/remote-device/desktop-commander-integration.js +1 -1
- package/dist/search-manager.js +31 -38
- package/dist/server.js +9 -4
- package/dist/terminal-manager.js +4 -2
- package/dist/tools/edit.js +34 -1
- package/dist/tools/filesystem.js +91 -3
- package/dist/tools/improved-process-tools.js +2 -1
- package/dist/ui/config-editor/config-editor-runtime.js +65 -14096
- package/dist/ui/config-editor/styles.css +2 -1
- package/dist/ui/file-preview/preview-runtime.js +435 -26533
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +1 -1
- package/dist/ui/file-preview/src/app.d.ts +1 -5
- package/dist/ui/file-preview/src/app.js +384 -534
- package/dist/ui/file-preview/src/components/markdown-renderer.js +47 -9
- package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
- package/dist/ui/file-preview/src/directory-controller.js +233 -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 +98 -0
- package/dist/ui/file-preview/src/host/external-actions.d.ts +19 -0
- package/dist/ui/file-preview/src/host/external-actions.js +94 -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/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 +1040 -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/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 +94 -0
- package/dist/ui/file-preview/styles.css +1066 -233
- package/dist/ui/shared/widget-state.d.ts +6 -1
- package/dist/ui/shared/widget-state.js +102 -4
- package/dist/utils/capture.js +1 -1
- package/dist/utils/files/base.d.ts +2 -0
- package/dist/utils/open-browser.js +1 -1
- package/dist/utils/toolHistory.d.ts +13 -0
- package/dist/utils/toolHistory.js +65 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +12 -1
- package/dist/data/spec-kit-prompts.json +0 -123
- package/dist/handlers/macos-control-handlers.d.ts +0 -16
- package/dist/handlers/macos-control-handlers.js +0 -81
- package/dist/handlers/node-handlers.d.ts +0 -6
- package/dist/handlers/node-handlers.js +0 -73
- package/dist/handlers/test-crash-handler.d.ts +0 -11
- package/dist/handlers/test-crash-handler.js +0 -26
- package/dist/http-index.d.ts +0 -45
- package/dist/http-index.js +0 -51
- package/dist/http-server-auto-tunnel.js +0 -667
- package/dist/http-server-named-tunnel.d.ts +0 -2
- package/dist/http-server-named-tunnel.js +0 -167
- package/dist/http-server-tunnel.d.ts +0 -2
- package/dist/http-server-tunnel.js +0 -111
- package/dist/http-server.d.ts +0 -2
- package/dist/http-server.js +0 -270
- package/dist/index-oauth.d.ts +0 -2
- package/dist/index-oauth.js +0 -201
- package/dist/lib.d.ts +0 -10
- package/dist/lib.js +0 -10
- package/dist/oauth/auth-middleware.d.ts +0 -20
- package/dist/oauth/auth-middleware.js +0 -62
- package/dist/oauth/index.d.ts +0 -3
- package/dist/oauth/index.js +0 -3
- package/dist/oauth/oauth-manager.d.ts +0 -80
- package/dist/oauth/oauth-manager.js +0 -179
- package/dist/oauth/oauth-routes.d.ts +0 -3
- package/dist/oauth/oauth-routes.js +0 -377
- package/dist/oauth/provider.d.ts +0 -22
- package/dist/oauth/provider.js +0 -124
- package/dist/oauth/server.d.ts +0 -18
- package/dist/oauth/server.js +0 -160
- package/dist/oauth/types.d.ts +0 -54
- package/dist/oauth/types.js +0 -2
- package/dist/remote-device/templates/auth-success.d.ts +0 -1
- package/dist/remote-device/templates/auth-success.js +0 -30
- package/dist/setup.log +0 -275
- package/dist/test-docx.d.ts +0 -1
- package/dist/test-setup.js +0 -14
- package/dist/tools/docx/builders/html-builder.d.ts +0 -17
- package/dist/tools/docx/builders/html-builder.js +0 -92
- package/dist/tools/docx/builders/image.d.ts +0 -14
- package/dist/tools/docx/builders/image.js +0 -84
- package/dist/tools/docx/builders/index.d.ts +0 -11
- package/dist/tools/docx/builders/index.js +0 -11
- package/dist/tools/docx/builders/markdown-builder.d.ts +0 -2
- package/dist/tools/docx/builders/markdown-builder.js +0 -260
- package/dist/tools/docx/builders/paragraph.d.ts +0 -12
- package/dist/tools/docx/builders/paragraph.js +0 -29
- package/dist/tools/docx/builders/table.d.ts +0 -10
- package/dist/tools/docx/builders/table.js +0 -138
- package/dist/tools/docx/builders/utils.d.ts +0 -5
- package/dist/tools/docx/builders/utils.js +0 -18
- package/dist/tools/docx/constants.d.ts +0 -32
- package/dist/tools/docx/constants.js +0 -61
- package/dist/tools/docx/converters/markdown-to-html.d.ts +0 -17
- package/dist/tools/docx/converters/markdown-to-html.js +0 -111
- package/dist/tools/docx/create.d.ts +0 -21
- package/dist/tools/docx/create.js +0 -386
- package/dist/tools/docx/dom.d.ts +0 -139
- package/dist/tools/docx/dom.js +0 -448
- package/dist/tools/docx/errors.d.ts +0 -28
- package/dist/tools/docx/errors.js +0 -48
- package/dist/tools/docx/extractors/images.d.ts +0 -14
- package/dist/tools/docx/extractors/images.js +0 -40
- package/dist/tools/docx/extractors/metadata.d.ts +0 -14
- package/dist/tools/docx/extractors/metadata.js +0 -64
- package/dist/tools/docx/extractors/sections.d.ts +0 -14
- package/dist/tools/docx/extractors/sections.js +0 -61
- package/dist/tools/docx/html.d.ts +0 -17
- package/dist/tools/docx/html.js +0 -111
- package/dist/tools/docx/index.d.ts +0 -10
- package/dist/tools/docx/index.js +0 -10
- package/dist/tools/docx/markdown.d.ts +0 -84
- package/dist/tools/docx/markdown.js +0 -507
- package/dist/tools/docx/modify.d.ts +0 -28
- package/dist/tools/docx/modify.js +0 -271
- package/dist/tools/docx/operations/handlers/index.d.ts +0 -39
- package/dist/tools/docx/operations/handlers/index.js +0 -152
- package/dist/tools/docx/operations/html-manipulator.d.ts +0 -24
- package/dist/tools/docx/operations/html-manipulator.js +0 -352
- package/dist/tools/docx/operations/index.d.ts +0 -14
- package/dist/tools/docx/operations/index.js +0 -61
- package/dist/tools/docx/operations/operation-handlers.d.ts +0 -3
- package/dist/tools/docx/operations/operation-handlers.js +0 -67
- package/dist/tools/docx/operations/preprocessor.d.ts +0 -14
- package/dist/tools/docx/operations/preprocessor.js +0 -44
- package/dist/tools/docx/operations/xml-replacer.d.ts +0 -9
- package/dist/tools/docx/operations/xml-replacer.js +0 -35
- package/dist/tools/docx/operations.d.ts +0 -13
- package/dist/tools/docx/operations.js +0 -13
- package/dist/tools/docx/ops/delete-paragraph-at-body-index.d.ts +0 -11
- package/dist/tools/docx/ops/delete-paragraph-at-body-index.js +0 -23
- package/dist/tools/docx/ops/header-replace-text-exact.d.ts +0 -13
- package/dist/tools/docx/ops/header-replace-text-exact.js +0 -55
- package/dist/tools/docx/ops/index.d.ts +0 -17
- package/dist/tools/docx/ops/index.js +0 -70
- package/dist/tools/docx/ops/insert-image-after-text.d.ts +0 -24
- package/dist/tools/docx/ops/insert-image-after-text.js +0 -128
- package/dist/tools/docx/ops/insert-paragraph-after-text.d.ts +0 -12
- package/dist/tools/docx/ops/insert-paragraph-after-text.js +0 -74
- package/dist/tools/docx/ops/insert-table-after-text.d.ts +0 -19
- package/dist/tools/docx/ops/insert-table-after-text.js +0 -57
- package/dist/tools/docx/ops/replace-hyperlink-url.d.ts +0 -12
- package/dist/tools/docx/ops/replace-hyperlink-url.js +0 -37
- package/dist/tools/docx/ops/replace-paragraph-at-body-index.d.ts +0 -9
- package/dist/tools/docx/ops/replace-paragraph-at-body-index.js +0 -25
- package/dist/tools/docx/ops/replace-paragraph-text-exact.d.ts +0 -21
- package/dist/tools/docx/ops/replace-paragraph-text-exact.js +0 -36
- package/dist/tools/docx/ops/replace-table-cell-text.d.ts +0 -25
- package/dist/tools/docx/ops/replace-table-cell-text.js +0 -85
- package/dist/tools/docx/ops/set-color-for-paragraph-exact.d.ts +0 -9
- package/dist/tools/docx/ops/set-color-for-paragraph-exact.js +0 -24
- package/dist/tools/docx/ops/set-color-for-style.d.ts +0 -13
- package/dist/tools/docx/ops/set-color-for-style.js +0 -31
- package/dist/tools/docx/ops/set-paragraph-style-at-body-index.d.ts +0 -8
- package/dist/tools/docx/ops/set-paragraph-style-at-body-index.js +0 -57
- package/dist/tools/docx/ops/table-set-cell-text.d.ts +0 -9
- package/dist/tools/docx/ops/table-set-cell-text.js +0 -40
- package/dist/tools/docx/parsers/image-extractor.d.ts +0 -18
- package/dist/tools/docx/parsers/image-extractor.js +0 -61
- package/dist/tools/docx/parsers/index.d.ts +0 -9
- package/dist/tools/docx/parsers/index.js +0 -9
- package/dist/tools/docx/parsers/paragraph-parser.d.ts +0 -2
- package/dist/tools/docx/parsers/paragraph-parser.js +0 -88
- package/dist/tools/docx/parsers/table-parser.d.ts +0 -9
- package/dist/tools/docx/parsers/table-parser.js +0 -72
- package/dist/tools/docx/parsers/xml-parser.d.ts +0 -25
- package/dist/tools/docx/parsers/xml-parser.js +0 -71
- package/dist/tools/docx/parsers/zip-reader.d.ts +0 -23
- package/dist/tools/docx/parsers/zip-reader.js +0 -52
- package/dist/tools/docx/read.d.ts +0 -27
- package/dist/tools/docx/read.js +0 -308
- package/dist/tools/docx/relationships.d.ts +0 -22
- package/dist/tools/docx/relationships.js +0 -76
- package/dist/tools/docx/structure.d.ts +0 -25
- package/dist/tools/docx/structure.js +0 -102
- package/dist/tools/docx/styled-html-parser.d.ts +0 -23
- package/dist/tools/docx/styled-html-parser.js +0 -1262
- package/dist/tools/docx/types.d.ts +0 -213
- package/dist/tools/docx/types.js +0 -5
- package/dist/tools/docx/utils/escaping.d.ts +0 -13
- package/dist/tools/docx/utils/escaping.js +0 -26
- package/dist/tools/docx/utils/images.d.ts +0 -9
- package/dist/tools/docx/utils/images.js +0 -26
- package/dist/tools/docx/utils/index.d.ts +0 -12
- package/dist/tools/docx/utils/index.js +0 -17
- package/dist/tools/docx/utils/markdown.d.ts +0 -13
- package/dist/tools/docx/utils/markdown.js +0 -32
- package/dist/tools/docx/utils/paths.d.ts +0 -15
- package/dist/tools/docx/utils/paths.js +0 -27
- package/dist/tools/docx/utils/versioning.d.ts +0 -25
- package/dist/tools/docx/utils/versioning.js +0 -55
- package/dist/tools/docx/utils.d.ts +0 -101
- package/dist/tools/docx/utils.js +0 -299
- package/dist/tools/docx/validate.d.ts +0 -33
- package/dist/tools/docx/validate.js +0 -49
- package/dist/tools/docx/validators.d.ts +0 -13
- package/dist/tools/docx/validators.js +0 -40
- package/dist/tools/docx/write.d.ts +0 -17
- package/dist/tools/docx/write.js +0 -88
- package/dist/tools/docx/xml-view-test.d.ts +0 -1
- package/dist/tools/docx/xml-view-test.js +0 -63
- package/dist/tools/docx/xml-view.d.ts +0 -56
- package/dist/tools/docx/xml-view.js +0 -169
- package/dist/tools/docx/zip.d.ts +0 -21
- package/dist/tools/docx/zip.js +0 -35
- package/dist/tools/macos-control/ax-adapter.d.ts +0 -55
- package/dist/tools/macos-control/ax-adapter.js +0 -438
- package/dist/tools/macos-control/cdp-adapter.d.ts +0 -23
- package/dist/tools/macos-control/cdp-adapter.js +0 -402
- package/dist/tools/macos-control/orchestrator.d.ts +0 -77
- package/dist/tools/macos-control/orchestrator.js +0 -136
- package/dist/tools/macos-control/role-aliases.d.ts +0 -5
- package/dist/tools/macos-control/role-aliases.js +0 -34
- package/dist/tools/macos-control/types.d.ts +0 -129
- package/dist/tools/macos-control/types.js +0 -1
- package/dist/tools/pdf-processor.d.ts +0 -1
- package/dist/tools/pdf-processor.js +0 -3
- package/dist/tools/search.d.ts +0 -32
- package/dist/tools/search.js +0 -202
- package/dist/ui/file-preview/src/components/toolbar.d.ts +0 -6
- package/dist/ui/file-preview/src/components/toolbar.js +0 -75
- package/dist/ui/shared/host-lifecycle.d.ts +0 -16
- package/dist/ui/shared/host-lifecycle.js +0 -35
- package/dist/ui/shared/rpc-client.d.ts +0 -14
- package/dist/ui/shared/rpc-client.js +0 -72
- package/dist/ui/shared/theme-adaptation.d.ts +0 -10
- package/dist/ui/shared/theme-adaptation.js +0 -118
- package/dist/ui/shared/tool-header.d.ts +0 -9
- package/dist/ui/shared/tool-header.js +0 -25
- package/dist/utils/crash-logger.d.ts +0 -18
- package/dist/utils/crash-logger.js +0 -44
- package/dist/utils/dedent.d.ts +0 -8
- package/dist/utils/dedent.js +0 -38
- /package/dist/{http-server-auto-tunnel.d.ts → ui/file-preview/src/model.js} +0 -0
|
@@ -1,476 +1,163 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { renderHtmlPreview } from './components/html-renderer.js';
|
|
6
|
-
import { renderMarkdown } from './components/markdown-renderer.js';
|
|
7
|
-
import { escapeHtml } from './components/highlighting.js';
|
|
8
|
-
import { isAllowedImageMimeType, normalizeImageMimeType } from './image-preview.js';
|
|
4
|
+
import { App } from '@modelcontextprotocol/ext-apps';
|
|
9
5
|
import { createCompactRowShellController } from '../../shared/tool-shell.js';
|
|
10
6
|
import { createWidgetStateStorage } from '../../shared/widget-state.js';
|
|
11
7
|
import { renderCompactRow } from '../../shared/compact-row.js';
|
|
12
|
-
import { connectWithSharedHostContext
|
|
8
|
+
import { connectWithSharedHostContext } from '../../shared/host-context.js';
|
|
13
9
|
import { createUiEventTracker } from '../../shared/ui-event-tracker.js';
|
|
14
|
-
import {
|
|
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, detectDefaultMarkdownEditor, 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';
|
|
15
20
|
let isExpanded = false;
|
|
16
21
|
let hideSummaryRow = false;
|
|
17
22
|
let previewShownFired = false;
|
|
18
23
|
let onRender;
|
|
19
24
|
let trackUiEvent;
|
|
25
|
+
let conflictDialogController;
|
|
20
26
|
let rpcCallTool;
|
|
21
27
|
let rpcUpdateContext;
|
|
28
|
+
let openExternalLink;
|
|
29
|
+
let requestDisplayMode;
|
|
22
30
|
let shellController;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
typeof value.fileType === 'string');
|
|
39
|
-
}
|
|
40
|
-
function buildRenderPayload(meta, text) {
|
|
41
|
-
return { ...meta, content: text };
|
|
42
|
-
}
|
|
43
|
-
function extractRenderPayload(value) {
|
|
44
|
-
if (!isObjectRecord(value)) {
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
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);
|
|
56
|
-
}
|
|
57
|
-
function extractToolText(value) {
|
|
58
|
-
if (!isObjectRecord(value)) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
const content = value.content;
|
|
62
|
-
if (!Array.isArray(content)) {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
for (const item of content) {
|
|
66
|
-
if (!isObjectRecord(item)) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (item.type === 'text' && typeof item.text === 'string' && item.text.trim().length > 0) {
|
|
70
|
-
return item.text;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
75
|
-
function isLikelyUrl(filePath) {
|
|
76
|
-
return /^https?:\/\//i.test(filePath);
|
|
77
|
-
}
|
|
78
|
-
function buildBreadcrumb(filePath) {
|
|
79
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
80
|
-
const parts = normalized.split('/').filter(Boolean);
|
|
81
|
-
// Show last 3-4 meaningful segments as breadcrumb
|
|
82
|
-
const tail = parts.slice(-4);
|
|
83
|
-
return tail.map(p => escapeHtml(p)).join(' <span class="breadcrumb-sep">›</span> ');
|
|
84
|
-
}
|
|
85
|
-
function getParentDirectory(filePath) {
|
|
86
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
87
|
-
const lastSlash = normalized.lastIndexOf('/');
|
|
88
|
-
if (lastSlash <= 0) {
|
|
89
|
-
return filePath;
|
|
90
|
-
}
|
|
91
|
-
return normalized.slice(0, lastSlash);
|
|
92
|
-
}
|
|
93
|
-
function shellQuote(value) {
|
|
94
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
95
|
-
}
|
|
96
|
-
function encodePowerShellCommand(script) {
|
|
97
|
-
// PowerShell -EncodedCommand expects UTF-16LE bytes.
|
|
98
|
-
const utf16leBytes = [];
|
|
99
|
-
for (let index = 0; index < script.length; index += 1) {
|
|
100
|
-
const codeUnit = script.charCodeAt(index);
|
|
101
|
-
utf16leBytes.push(codeUnit & 0xff, codeUnit >> 8);
|
|
102
|
-
}
|
|
103
|
-
let binary = '';
|
|
104
|
-
for (const byte of utf16leBytes) {
|
|
105
|
-
binary += String.fromCharCode(byte);
|
|
106
|
-
}
|
|
107
|
-
return btoa(binary);
|
|
31
|
+
let currentPayload;
|
|
32
|
+
let currentHtmlMode = 'rendered';
|
|
33
|
+
let currentHostContext;
|
|
34
|
+
let rerenderCurrent;
|
|
35
|
+
let syncPayload;
|
|
36
|
+
let persistPayload;
|
|
37
|
+
let localPayloadOverride;
|
|
38
|
+
let hostPayload;
|
|
39
|
+
let inlinePayloadBeforeFullscreen;
|
|
40
|
+
let directoryBackPayload;
|
|
41
|
+
let selectionAbortController = null;
|
|
42
|
+
const markdownEditorAppCache = new Map();
|
|
43
|
+
const markdownEditorAppPending = new Set();
|
|
44
|
+
async function callToolIfReady(name, args) {
|
|
45
|
+
return rpcCallTool ? rpcCallTool(name, args) : undefined;
|
|
108
46
|
}
|
|
109
|
-
function
|
|
110
|
-
const
|
|
111
|
-
if (!
|
|
112
|
-
return
|
|
47
|
+
function getAvailableDisplayModes() {
|
|
48
|
+
const rawModes = currentHostContext?.availableDisplayModes;
|
|
49
|
+
if (!Array.isArray(rawModes)) {
|
|
50
|
+
return [];
|
|
113
51
|
}
|
|
114
|
-
|
|
115
|
-
if (userAgent.includes('win')) {
|
|
116
|
-
const escapedForPowerShell = trimmedPath.replace(/'/g, "''");
|
|
117
|
-
const script = `Start-Process -FilePath explorer.exe -ArgumentList @('/select,','${escapedForPowerShell}')`;
|
|
118
|
-
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(script)}`;
|
|
119
|
-
}
|
|
120
|
-
if (userAgent.includes('mac')) {
|
|
121
|
-
return `open -R ${shellQuote(trimmedPath)}`;
|
|
122
|
-
}
|
|
123
|
-
return `xdg-open ${shellQuote(getParentDirectory(trimmedPath))}`;
|
|
52
|
+
return rawModes.filter((mode) => typeof mode === 'string');
|
|
124
53
|
}
|
|
125
|
-
function
|
|
126
|
-
return
|
|
54
|
+
function getCurrentDisplayMode() {
|
|
55
|
+
return typeof currentHostContext?.displayMode === 'string'
|
|
56
|
+
? currentHostContext.displayMode
|
|
57
|
+
: null;
|
|
127
58
|
}
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
59
|
+
function storePayloadOverride(payload) {
|
|
60
|
+
localPayloadOverride = payload;
|
|
61
|
+
currentPayload = payload;
|
|
62
|
+
persistPayload?.(payload);
|
|
131
63
|
}
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
notice: 'Preview is unavailable for this image format.',
|
|
137
|
-
html: '<div class="panel-content source-content"></div>'
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
if (!payload.imageData || payload.imageData.trim().length === 0) {
|
|
141
|
-
return {
|
|
142
|
-
notice: 'Preview is unavailable because image data is missing.',
|
|
143
|
-
html: '<div class="panel-content source-content"></div>'
|
|
144
|
-
};
|
|
64
|
+
function getEffectiveIncomingPayload(payload) {
|
|
65
|
+
if (!localPayloadOverride) {
|
|
66
|
+
return payload;
|
|
145
67
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function countContentLines(content) {
|
|
152
|
-
const cleaned = stripReadStatusLine(content);
|
|
153
|
-
if (cleaned === '')
|
|
154
|
-
return 0;
|
|
155
|
-
const lines = cleaned.split('\n');
|
|
156
|
-
return lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
|
|
157
|
-
}
|
|
158
|
-
function parseReadRange(content) {
|
|
159
|
-
// Parse "[Reading N lines from line M (total: T lines, R remaining)]"
|
|
160
|
-
// or "[Reading N lines from start (total: T lines, R remaining)]"
|
|
161
|
-
const match = content.match(/^\[Reading (\d+) lines from (?:line )?(\d+|start) \(total: (\d+) lines/);
|
|
162
|
-
if (!match)
|
|
163
|
-
return undefined;
|
|
164
|
-
const count = parseInt(match[1], 10);
|
|
165
|
-
const from = match[2] === 'start' ? 1 : parseInt(match[2], 10);
|
|
166
|
-
const total = parseInt(match[3], 10);
|
|
167
|
-
return {
|
|
168
|
-
fromLine: from,
|
|
169
|
-
toLine: from + count - 1,
|
|
170
|
-
totalLines: total,
|
|
171
|
-
isPartial: count < total
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
function renderBody(payload, htmlMode, startLine = 1) {
|
|
175
|
-
const cleanedContent = stripReadStatusLine(payload.content);
|
|
176
|
-
if (payload.fileType === 'image') {
|
|
177
|
-
return renderImageBody(payload);
|
|
68
|
+
if (localPayloadOverride.filePath !== payload.filePath) {
|
|
69
|
+
localPayloadOverride = undefined;
|
|
70
|
+
return payload;
|
|
178
71
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
if (payload.fileType === 'html') {
|
|
186
|
-
return renderHtmlPreview(cleanedContent, htmlMode);
|
|
187
|
-
}
|
|
188
|
-
if (payload.fileType !== 'markdown') {
|
|
189
|
-
const detectedLanguage = inferLanguageFromPath(payload.filePath);
|
|
190
|
-
const formatted = formatJsonIfPossible(cleanedContent, payload.filePath);
|
|
191
|
-
return {
|
|
192
|
-
notice: formatted.notice,
|
|
193
|
-
html: `<div class="panel-content source-content">${renderCodeViewer(formatted.content, detectedLanguage, startLine)}</div>`
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
try {
|
|
197
|
-
return {
|
|
198
|
-
html: `<div class="panel-content markdown-content"><article class="markdown markdown-doc">${renderMarkdown(cleanedContent)}</article></div>`
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
return {
|
|
203
|
-
notice: 'Markdown renderer failed. Showing raw source instead.',
|
|
204
|
-
html: `<div class="panel-content source-content">${renderRawFallback(cleanedContent)}</div>`
|
|
205
|
-
};
|
|
72
|
+
const incomingContent = stripReadStatusLine(payload.content);
|
|
73
|
+
const overriddenContent = stripReadStatusLine(localPayloadOverride.content);
|
|
74
|
+
if (incomingContent === overriddenContent) {
|
|
75
|
+
return payload;
|
|
206
76
|
}
|
|
77
|
+
return localPayloadOverride;
|
|
207
78
|
}
|
|
208
|
-
function
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const textArea = document.createElement('textarea');
|
|
215
|
-
textArea.value = text;
|
|
216
|
-
textArea.setAttribute('readonly', '');
|
|
217
|
-
textArea.style.position = 'fixed';
|
|
218
|
-
textArea.style.top = '-9999px';
|
|
219
|
-
document.body.appendChild(textArea);
|
|
220
|
-
textArea.select();
|
|
221
|
-
const success = document.execCommand('copy');
|
|
222
|
-
document.body.removeChild(textArea);
|
|
223
|
-
return success;
|
|
224
|
-
};
|
|
225
|
-
const setButtonState = (label, revertMs) => {
|
|
226
|
-
copyButton.setAttribute('title', label);
|
|
227
|
-
copyButton.setAttribute('aria-label', label);
|
|
228
|
-
copyButton.textContent = label;
|
|
229
|
-
if (revertMs) {
|
|
230
|
-
setTimeout(() => {
|
|
231
|
-
copyButton.textContent = 'Copy';
|
|
232
|
-
copyButton.setAttribute('title', 'Copy source');
|
|
233
|
-
copyButton.setAttribute('aria-label', 'Copy source');
|
|
234
|
-
}, revertMs);
|
|
79
|
+
function updateSaveStatusDOM(label, statusClass) {
|
|
80
|
+
const existing = document.querySelector('.panel-save-status');
|
|
81
|
+
if (label) {
|
|
82
|
+
if (existing) {
|
|
83
|
+
existing.textContent = label;
|
|
84
|
+
existing.className = `panel-save-status panel-save-status--${statusClass}`;
|
|
235
85
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
86
|
+
else {
|
|
87
|
+
const actions = document.querySelector('.panel-topbar-actions');
|
|
88
|
+
if (actions) {
|
|
89
|
+
const span = document.createElement('span');
|
|
90
|
+
span.className = `panel-save-status panel-save-status--${statusClass}`;
|
|
91
|
+
span.textContent = label;
|
|
92
|
+
actions.prepend(span);
|
|
242
93
|
}
|
|
243
|
-
return fallbackCopy(text);
|
|
244
94
|
}
|
|
245
|
-
catch {
|
|
246
|
-
return fallbackCopy(text);
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
copyButton.addEventListener('click', async () => {
|
|
250
|
-
trackUiEvent?.('copy_clicked', {
|
|
251
|
-
file_type: payload.fileType,
|
|
252
|
-
file_extension: getFileExtensionForAnalytics(payload.filePath)
|
|
253
|
-
});
|
|
254
|
-
const cleanedContent = stripReadStatusLine(payload.content);
|
|
255
|
-
const copied = await copyTextData(cleanedContent);
|
|
256
|
-
setButtonState(copied ? 'Copied!' : 'Copy failed', 1500);
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
function attachHtmlToggleHandler(container, payload, htmlMode) {
|
|
260
|
-
const toggleButton = document.getElementById('toggle-html-mode');
|
|
261
|
-
if (!toggleButton || payload.fileType !== 'html') {
|
|
262
|
-
return;
|
|
263
95
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
trackUiEvent?.('html_view_toggled', {
|
|
267
|
-
file_type: payload.fileType,
|
|
268
|
-
file_extension: getFileExtensionForAnalytics(payload.filePath)
|
|
269
|
-
});
|
|
270
|
-
renderApp(container, payload, nextMode, isExpanded);
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
function attachOpenInFolderHandler(payload) {
|
|
274
|
-
const openButton = document.getElementById('open-in-folder');
|
|
275
|
-
if (!openButton) {
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
const command = buildOpenInFolderCommand(payload.filePath);
|
|
279
|
-
if (!command) {
|
|
280
|
-
openButton.disabled = true;
|
|
281
|
-
return;
|
|
96
|
+
else if (existing) {
|
|
97
|
+
existing.remove();
|
|
282
98
|
}
|
|
283
|
-
openButton.addEventListener('click', async () => {
|
|
284
|
-
trackUiEvent?.('open_in_folder', {
|
|
285
|
-
file_type: payload.fileType,
|
|
286
|
-
file_extension: getFileExtensionForAnalytics(payload.filePath)
|
|
287
|
-
});
|
|
288
|
-
try {
|
|
289
|
-
await rpcCallTool?.('start_process', {
|
|
290
|
-
command,
|
|
291
|
-
timeout_ms: 12000
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
// Keep UI stable if opening folder fails.
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
99
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
? { path: payload.filePath, offset: 0, length: range.fromLine - 1 }
|
|
321
|
-
: { path: payload.filePath, offset: range.toLine };
|
|
322
|
-
const result = await rpcCallTool?.('read_file', readArgs);
|
|
323
|
-
const resultObj = result;
|
|
324
|
-
const newText = resultObj?.content?.[0]?.text;
|
|
325
|
-
if (newText && typeof newText === 'string') {
|
|
326
|
-
const cleanNew = stripReadStatusLine(newText);
|
|
327
|
-
// Merge: prepend or append the new lines
|
|
328
|
-
const merged = direction === 'before'
|
|
329
|
-
? cleanNew + (cleanNew.endsWith('\n') ? '' : '\n') + currentContent
|
|
330
|
-
: currentContent + (currentContent.endsWith('\n') ? '' : '\n') + cleanNew;
|
|
331
|
-
// Build updated status line reflecting the new range
|
|
332
|
-
const newFrom = direction === 'before' ? 1 : range.fromLine;
|
|
333
|
-
const newTo = direction === 'after' ? range.totalLines : range.toLine;
|
|
334
|
-
const lineCount = newTo - newFrom + 1;
|
|
335
|
-
const remaining = range.totalLines - newTo;
|
|
336
|
-
const isStillPartial = newFrom > 1 || newTo < range.totalLines;
|
|
337
|
-
const statusLine = isStillPartial
|
|
338
|
-
? `[Reading ${lineCount} lines from ${newFrom === 1 ? 'start' : `line ${newFrom}`} (total: ${range.totalLines} lines, ${remaining} remaining)]\n`
|
|
339
|
-
: '';
|
|
340
|
-
const mergedPayload = {
|
|
341
|
-
...payload,
|
|
342
|
-
content: statusLine + merged
|
|
343
|
-
};
|
|
344
|
-
renderApp(container, mergedPayload, htmlMode, isExpanded);
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
btn.textContent = 'Failed to load';
|
|
348
|
-
setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 2000);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
catch {
|
|
352
|
-
btn.textContent = 'Failed to load';
|
|
353
|
-
setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 2000);
|
|
100
|
+
const markdownController = createMarkdownController({
|
|
101
|
+
callTool: callToolIfReady,
|
|
102
|
+
openExternalLink: async (url) => (openExternalLink ? openExternalLink(url) : undefined),
|
|
103
|
+
requestDisplayMode: async (mode) => (requestDisplayMode ? requestDisplayMode(mode) : undefined),
|
|
104
|
+
getAvailableDisplayModes,
|
|
105
|
+
getCurrentDisplayMode,
|
|
106
|
+
getCurrentPayload: () => currentPayload,
|
|
107
|
+
setExpanded: (expanded) => {
|
|
108
|
+
isExpanded = expanded;
|
|
109
|
+
},
|
|
110
|
+
syncPayload: (payload) => syncPayload?.(payload),
|
|
111
|
+
storePayloadOverride,
|
|
112
|
+
rerender: () => {
|
|
113
|
+
rerenderCurrent?.();
|
|
114
|
+
},
|
|
115
|
+
updateSaveStatus: updateSaveStatusDOM,
|
|
116
|
+
trackUiEvent: (event, params) => trackUiEvent?.(event, params),
|
|
117
|
+
showConflictDialog: (options) => {
|
|
118
|
+
if (conflictDialogController) {
|
|
119
|
+
conflictDialogController.open(options);
|
|
120
|
+
return;
|
|
354
121
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
122
|
+
// Dialog not yet initialized (would only happen if the save failure
|
|
123
|
+
// somehow fires before bootstrapApp). Fall back to the cancel callback
|
|
124
|
+
// so the editor still shows its inline note instead of silently no-op'ing.
|
|
125
|
+
console.warn('[file-preview] conflictDialogController not ready; firing onCancel fallback');
|
|
126
|
+
options.onCancel?.();
|
|
127
|
+
},
|
|
128
|
+
});
|
|
359
129
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* request to the host with the selected text + file path (+ line numbers for code).
|
|
367
|
-
* 4. The host stores this as widget context.
|
|
368
|
-
* 5. The LLM can access it by calling read_widget_context(tool_name="desktop-commander:read_file").
|
|
369
|
-
*
|
|
370
|
-
* Note: as of Feb 2025, Claude does NOT auto-inject ui/update-model-context into
|
|
371
|
-
* the LLM's context window. The LLM must actively call read_widget_context to see
|
|
372
|
-
* the selection. A floating tooltip near the selection tells the user this is working.
|
|
130
|
+
* Check if a payload needs its file content to be read.
|
|
131
|
+
* Tool results from edit_block/write_file include structuredContent but
|
|
132
|
+
* their text is a success message, not file content. Detect this by
|
|
133
|
+
* checking for the absence of the read status line that read_file always includes.
|
|
134
|
+
* URL payloads are fetched remotely by read_file(isUrl:true); we can't
|
|
135
|
+
* re-fetch them from here (no isUrl flag on the refresh path), so skip.
|
|
373
136
|
*/
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (!contentWrapper)
|
|
378
|
-
return;
|
|
379
|
-
// Abort any previous selectionchange listener to avoid leaking listeners/closures
|
|
380
|
-
if (selectionAbortController) {
|
|
381
|
-
selectionAbortController.abort();
|
|
382
|
-
selectionAbortController = null;
|
|
383
|
-
}
|
|
384
|
-
selectionAbortController = new AbortController();
|
|
385
|
-
let hintEl = null;
|
|
386
|
-
let lastSelectedText = '';
|
|
387
|
-
let hideTimer = null;
|
|
388
|
-
function positionHint(selection) {
|
|
389
|
-
if (!hintEl)
|
|
390
|
-
return;
|
|
391
|
-
const range = selection.getRangeAt(0);
|
|
392
|
-
const rect = range.getBoundingClientRect();
|
|
393
|
-
const wrapperRect = contentWrapper.getBoundingClientRect();
|
|
394
|
-
// Position above the selection, centered horizontally
|
|
395
|
-
let left = rect.left + rect.width / 2 - wrapperRect.left;
|
|
396
|
-
let top = rect.top - wrapperRect.top + contentWrapper.scrollTop - 32;
|
|
397
|
-
// Clamp within wrapper bounds
|
|
398
|
-
const hintWidth = hintEl.offsetWidth || 200;
|
|
399
|
-
left = Math.max(8, Math.min(left - hintWidth / 2, contentWrapper.clientWidth - hintWidth - 8));
|
|
400
|
-
top = Math.max(4, top);
|
|
401
|
-
hintEl.style.left = `${left}px`;
|
|
402
|
-
hintEl.style.top = `${top}px`;
|
|
403
|
-
}
|
|
404
|
-
function showHint(selection) {
|
|
405
|
-
if (hideTimer) {
|
|
406
|
-
clearTimeout(hideTimer);
|
|
407
|
-
hideTimer = null;
|
|
408
|
-
}
|
|
409
|
-
if (!hintEl) {
|
|
410
|
-
hintEl = document.createElement('div');
|
|
411
|
-
hintEl.className = 'selection-hint';
|
|
412
|
-
hintEl.textContent = 'AI can see your selection';
|
|
413
|
-
contentWrapper.appendChild(hintEl);
|
|
414
|
-
}
|
|
415
|
-
hintEl.classList.add('visible');
|
|
416
|
-
positionHint(selection);
|
|
417
|
-
}
|
|
418
|
-
function hideHint() {
|
|
419
|
-
if (!hintEl)
|
|
420
|
-
return;
|
|
421
|
-
hintEl.classList.remove('visible');
|
|
422
|
-
hideTimer = setTimeout(() => { hintEl?.remove(); hintEl = null; }, 200);
|
|
137
|
+
function needsContentRead(payload) {
|
|
138
|
+
if (payload.fileType === 'directory' || payload.fileType === 'image' || payload.fileType === 'unsupported') {
|
|
139
|
+
return false;
|
|
423
140
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const focusRow = selection.focusNode?.parentElement?.closest('.code-line');
|
|
427
|
-
if (anchorRow && focusRow) {
|
|
428
|
-
const a = parseInt(anchorRow.dataset.line ?? '', 10);
|
|
429
|
-
const f = parseInt(focusRow.dataset.line ?? '', 10);
|
|
430
|
-
if (!isNaN(a) && !isNaN(f)) {
|
|
431
|
-
const low = Math.min(a, f);
|
|
432
|
-
const high = Math.max(a, f);
|
|
433
|
-
return low === high ? `line ${low}` : `lines ${low}–${high}`;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
return '';
|
|
141
|
+
if (/^https?:\/\//i.test(payload.filePath)) {
|
|
142
|
+
return false;
|
|
437
143
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
const text = selection.toString().trim();
|
|
449
|
-
if (!text || text === lastSelectedText)
|
|
450
|
-
return;
|
|
451
|
-
// Only act on selections within our content area
|
|
452
|
-
const anchorInContent = contentWrapper.contains(selection.anchorNode);
|
|
453
|
-
const focusInContent = contentWrapper.contains(selection.focusNode);
|
|
454
|
-
if (!anchorInContent && !focusInContent) {
|
|
455
|
-
if (lastSelectedText) {
|
|
456
|
-
lastSelectedText = '';
|
|
457
|
-
rpcUpdateContext?.('');
|
|
458
|
-
hideHint();
|
|
144
|
+
return !parseReadRange(payload.content);
|
|
145
|
+
}
|
|
146
|
+
async function readAndResolvePayload(payload, onReady) {
|
|
147
|
+
try {
|
|
148
|
+
const freshPayload = await markdownController.readPayload(payload.filePath);
|
|
149
|
+
if (freshPayload) {
|
|
150
|
+
onReady(freshPayload);
|
|
151
|
+
if (freshPayload.fileType === 'markdown') {
|
|
152
|
+
void markdownController.refreshFromDisk(freshPayload);
|
|
459
153
|
}
|
|
460
154
|
return;
|
|
461
155
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
showHint(selection);
|
|
468
|
-
trackUiEvent?.('text_selected', {
|
|
469
|
-
file_type: payload.fileType,
|
|
470
|
-
file_extension: getFileExtensionForAnalytics(payload.filePath),
|
|
471
|
-
char_count: text.length
|
|
472
|
-
});
|
|
473
|
-
}, { signal: selectionAbortController.signal });
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Fall through to original payload.
|
|
159
|
+
}
|
|
160
|
+
onReady(payload);
|
|
474
161
|
}
|
|
475
162
|
function renderStatusState(container, message) {
|
|
476
163
|
container.innerHTML = `
|
|
@@ -490,108 +177,149 @@ function renderLoadingState(container) {
|
|
|
490
177
|
}
|
|
491
178
|
export function renderApp(container, payload, htmlMode = 'rendered', expandedState = false) {
|
|
492
179
|
isExpanded = expandedState;
|
|
180
|
+
currentHtmlMode = htmlMode;
|
|
493
181
|
shellController?.dispose();
|
|
494
182
|
shellController = undefined;
|
|
183
|
+
if (!payload || payload.fileType !== 'markdown') {
|
|
184
|
+
markdownController.clear();
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
markdownController.disposeHandles();
|
|
188
|
+
}
|
|
495
189
|
if (!payload) {
|
|
190
|
+
selectionAbortController?.abort();
|
|
191
|
+
selectionAbortController = null;
|
|
192
|
+
currentPayload = undefined;
|
|
496
193
|
renderStatusState(container, 'No preview available for this response.');
|
|
497
194
|
onRender?.();
|
|
498
195
|
return;
|
|
499
196
|
}
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
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) {
|
|
197
|
+
currentPayload = payload;
|
|
198
|
+
const capabilities = getFileTypeCapabilities(payload);
|
|
199
|
+
if (!capabilities.supportsPreview && hideSummaryRow) {
|
|
507
200
|
isExpanded = false;
|
|
508
201
|
}
|
|
509
202
|
const range = parseReadRange(payload.content);
|
|
510
|
-
const body =
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
?
|
|
524
|
-
:
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
${body.html}
|
|
555
|
-
${loadAfterBanner}
|
|
556
|
-
</div>
|
|
557
|
-
<div class="panel-footer">
|
|
558
|
-
<span>${footerLabel}</span>
|
|
559
|
-
</div>
|
|
560
|
-
</section>
|
|
561
|
-
</main>
|
|
562
|
-
`;
|
|
203
|
+
const body = renderPayloadBody({
|
|
204
|
+
payload,
|
|
205
|
+
htmlMode,
|
|
206
|
+
startLine: range?.fromLine ?? 1,
|
|
207
|
+
markdownController,
|
|
208
|
+
});
|
|
209
|
+
const markdownWorkspace = payload.fileType === 'markdown' ? markdownController.getState(payload) : undefined;
|
|
210
|
+
const fileExtension = getFileExtensionForAnalytics(payload.filePath);
|
|
211
|
+
const isFullscreen = getCurrentDisplayMode() === 'fullscreen';
|
|
212
|
+
const canGoFullscreen = !isFullscreen && getDocumentFullscreenAvailability({
|
|
213
|
+
availableDisplayModes: getAvailableDisplayModes(),
|
|
214
|
+
}).canFullscreen;
|
|
215
|
+
const defaultMarkdownEditor = payload.fileType === 'markdown'
|
|
216
|
+
? markdownEditorAppCache.get(payload.filePath)
|
|
217
|
+
: undefined;
|
|
218
|
+
if (payload.fileType === 'markdown' && !defaultMarkdownEditor) {
|
|
219
|
+
void detectDefaultMarkdownEditor({
|
|
220
|
+
filePath: payload.filePath,
|
|
221
|
+
editorAppCache: markdownEditorAppCache,
|
|
222
|
+
editorAppPending: markdownEditorAppPending,
|
|
223
|
+
callTool: callToolIfReady,
|
|
224
|
+
extractToolText,
|
|
225
|
+
onDetected: () => {
|
|
226
|
+
rerenderCurrent?.();
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const layout = buildDocumentLayout({
|
|
231
|
+
payload,
|
|
232
|
+
body,
|
|
233
|
+
capabilities,
|
|
234
|
+
fileExtension,
|
|
235
|
+
htmlMode,
|
|
236
|
+
currentDisplayMode: getCurrentDisplayMode(),
|
|
237
|
+
isExpanded,
|
|
238
|
+
hideSummaryRow,
|
|
239
|
+
markdownWorkspace,
|
|
240
|
+
canGoFullscreen,
|
|
241
|
+
isMarkdownUndoAvailable: markdownWorkspace ? markdownController.isUndoAvailable(markdownWorkspace) : false,
|
|
242
|
+
defaultMarkdownEditorName: defaultMarkdownEditor?.appName,
|
|
243
|
+
markdownEditorAppIcon: renderMarkdownEditorAppIcon(),
|
|
244
|
+
hasDirectoryBackButton: Boolean(directoryBackPayload),
|
|
245
|
+
});
|
|
246
|
+
container.innerHTML = layout.html;
|
|
563
247
|
document.body.classList.add('dc-ready');
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
248
|
+
attachPanelActions({
|
|
249
|
+
container,
|
|
250
|
+
payload,
|
|
251
|
+
htmlMode,
|
|
252
|
+
getIsExpanded: () => isExpanded,
|
|
253
|
+
callTool: callToolIfReady,
|
|
254
|
+
trackUiEvent,
|
|
255
|
+
getFileExtensionForAnalytics,
|
|
256
|
+
buildOpenInFolderCommand: (filePath) => buildOpenInFolderCommand(filePath, isLikelyUrl),
|
|
257
|
+
buildOpenInEditorCommand: (filePath) => buildOpenInEditorCommand(filePath, isLikelyUrl, markdownEditorAppCache),
|
|
258
|
+
render: (nextPayload, nextHtmlMode = 'rendered', nextExpanded = isExpanded) => {
|
|
259
|
+
renderApp(container, nextPayload, nextHtmlMode, nextExpanded);
|
|
260
|
+
},
|
|
261
|
+
updateSaveStatus: updateSaveStatusDOM,
|
|
262
|
+
markdownController,
|
|
263
|
+
});
|
|
264
|
+
if (payload.fileType === 'markdown') {
|
|
265
|
+
markdownController.attachHandlers(payload);
|
|
266
|
+
}
|
|
267
|
+
selectionAbortController = attachSelectionContext({
|
|
268
|
+
payload,
|
|
269
|
+
isMarkdownEditing: payload.fileType === 'markdown' && !!markdownWorkspace,
|
|
270
|
+
updateContext: rpcUpdateContext,
|
|
271
|
+
trackUiEvent,
|
|
272
|
+
getFileExtensionForAnalytics,
|
|
273
|
+
previousAbortController: selectionAbortController,
|
|
274
|
+
});
|
|
275
|
+
if (payload.fileType === 'directory') {
|
|
276
|
+
attachDirectoryHandlers({
|
|
277
|
+
container,
|
|
278
|
+
callTool: callToolIfReady,
|
|
279
|
+
buildOpenInFolderCommand: (filePath) => buildOpenInFolderCommand(filePath, isLikelyUrl),
|
|
280
|
+
onOpenPayload: (nextPayload) => {
|
|
281
|
+
directoryBackPayload = payload;
|
|
282
|
+
renderApp(container, nextPayload, 'rendered', true);
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const backBtn = document.getElementById('dir-back');
|
|
287
|
+
if (backBtn && directoryBackPayload) {
|
|
288
|
+
const savedPayload = directoryBackPayload;
|
|
289
|
+
backBtn.addEventListener('click', () => {
|
|
290
|
+
directoryBackPayload = undefined;
|
|
291
|
+
renderApp(container, savedPayload, 'rendered', true);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (payload.fileType === 'directory') {
|
|
295
|
+
directoryBackPayload = undefined;
|
|
296
|
+
}
|
|
569
297
|
const compactRow = document.getElementById('compact-toggle');
|
|
570
298
|
shellController = createCompactRowShellController({
|
|
571
299
|
shell: document.getElementById('tool-shell'),
|
|
572
300
|
compactRow,
|
|
573
|
-
initialExpanded:
|
|
301
|
+
initialExpanded: layout.effectiveExpanded,
|
|
574
302
|
onToggle: (expanded) => {
|
|
575
303
|
isExpanded = expanded;
|
|
576
304
|
trackUiEvent?.(expanded ? 'expand' : 'collapse', {
|
|
577
305
|
file_type: payload.fileType,
|
|
578
|
-
file_extension: fileExtension
|
|
306
|
+
file_extension: fileExtension,
|
|
579
307
|
});
|
|
580
308
|
},
|
|
581
309
|
onScrollAfterExpand: () => {
|
|
582
310
|
trackUiEvent?.('scroll_after_expand', {
|
|
583
311
|
file_type: payload.fileType,
|
|
584
|
-
file_extension: fileExtension
|
|
312
|
+
file_extension: fileExtension,
|
|
585
313
|
});
|
|
586
314
|
},
|
|
587
|
-
onRender
|
|
315
|
+
onRender,
|
|
588
316
|
});
|
|
589
317
|
onRender?.();
|
|
590
318
|
if (!previewShownFired) {
|
|
591
319
|
previewShownFired = true;
|
|
592
320
|
trackUiEvent?.('preview_shown', {
|
|
593
321
|
file_type: payload.fileType,
|
|
594
|
-
file_extension: fileExtension
|
|
322
|
+
file_extension: fileExtension,
|
|
595
323
|
});
|
|
596
324
|
}
|
|
597
325
|
}
|
|
@@ -601,8 +329,18 @@ export function bootstrapApp() {
|
|
|
601
329
|
return;
|
|
602
330
|
}
|
|
603
331
|
renderLoadingState(container);
|
|
604
|
-
//
|
|
605
|
-
//
|
|
332
|
+
// Mount the conflict dialog once at body level. It's position: fixed and
|
|
333
|
+
// must live outside the app container so that re-renders of the document
|
|
334
|
+
// body never wipe it while it's open.
|
|
335
|
+
if (!document.getElementById('md-conflict-modal')) {
|
|
336
|
+
const dialogHost = document.createElement('div');
|
|
337
|
+
dialogHost.innerHTML = renderConflictDialogMarkup();
|
|
338
|
+
const dialogRoot = dialogHost.firstElementChild;
|
|
339
|
+
if (dialogRoot) {
|
|
340
|
+
document.body.appendChild(dialogRoot);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
conflictDialogController = createConflictDialogController({ container: document });
|
|
606
344
|
const app = new App({ name: 'Desktop Commander File Preview', version: '1.0.0' }, { updateModelContext: { text: {} } }, { autoResize: true });
|
|
607
345
|
const chrome = {
|
|
608
346
|
expanded: isExpanded,
|
|
@@ -612,14 +350,33 @@ export function bootstrapApp() {
|
|
|
612
350
|
isExpanded = chrome.expanded;
|
|
613
351
|
hideSummaryRow = chrome.hideSummaryRow;
|
|
614
352
|
};
|
|
615
|
-
|
|
616
|
-
const widgetState = createWidgetStateStorage((v) => isPreviewStructuredContent(v) && typeof v.content === 'string');
|
|
353
|
+
const widgetState = createWidgetStateStorage((value) => isPreviewStructuredContent(value) && typeof value.content === 'string');
|
|
617
354
|
const renderAndSync = (payload) => {
|
|
618
355
|
if (payload) {
|
|
619
356
|
widgetState.write(payload);
|
|
620
357
|
}
|
|
621
358
|
renderApp(container, payload, 'rendered', isExpanded);
|
|
622
359
|
};
|
|
360
|
+
const syncFromPersistedWidgetState = () => {
|
|
361
|
+
const persistedPayload = widgetState.read();
|
|
362
|
+
if (!persistedPayload) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (currentPayload
|
|
366
|
+
&& currentPayload.filePath === persistedPayload.filePath
|
|
367
|
+
&& stripReadStatusLine(currentPayload.content) === stripReadStatusLine(persistedPayload.content)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
renderAndSync(persistedPayload);
|
|
371
|
+
};
|
|
372
|
+
syncPayload = renderAndSync;
|
|
373
|
+
persistPayload = (payload) => {
|
|
374
|
+
widgetState.write(payload);
|
|
375
|
+
};
|
|
376
|
+
rerenderCurrent = () => {
|
|
377
|
+
renderApp(container, currentPayload, currentHtmlMode, isExpanded);
|
|
378
|
+
};
|
|
379
|
+
let pendingCachedPayload;
|
|
623
380
|
let initialStateResolved = false;
|
|
624
381
|
const resolveInitialState = (payload, message) => {
|
|
625
382
|
if (initialStateResolved) {
|
|
@@ -627,47 +384,66 @@ export function bootstrapApp() {
|
|
|
627
384
|
}
|
|
628
385
|
initialStateResolved = true;
|
|
629
386
|
if (payload) {
|
|
387
|
+
hostPayload = payload;
|
|
630
388
|
renderAndSync(payload);
|
|
389
|
+
if (payload.fileType === 'markdown' && getCurrentDisplayMode() === 'fullscreen') {
|
|
390
|
+
void markdownController.requestEditMode(payload);
|
|
391
|
+
}
|
|
392
|
+
if (payload.fileType === 'markdown') {
|
|
393
|
+
void markdownController.refreshFromDisk(payload);
|
|
394
|
+
}
|
|
631
395
|
return;
|
|
632
396
|
}
|
|
633
397
|
renderStatusState(container, message ?? 'No preview available for this response.');
|
|
634
398
|
onRender?.();
|
|
635
399
|
};
|
|
636
|
-
// autoResize handles size reporting; onRender can be a no-op
|
|
637
400
|
onRender = () => { };
|
|
638
|
-
// Wire rpcCallTool through the App's callServerTool proxy
|
|
639
401
|
rpcCallTool = (name, args) => (app.callServerTool({ name, arguments: args }));
|
|
640
|
-
// Wire rpcUpdateContext through the App's updateModelContext
|
|
641
402
|
rpcUpdateContext = (text) => {
|
|
642
403
|
const params = text
|
|
643
404
|
? { content: [{ type: 'text', text }] }
|
|
644
405
|
: { content: [] };
|
|
645
406
|
app.updateModelContext(params).catch(() => {
|
|
646
|
-
// Host may not support updateModelContext
|
|
407
|
+
// Host may not support updateModelContext.
|
|
647
408
|
});
|
|
648
409
|
};
|
|
410
|
+
openExternalLink = async (url) => {
|
|
411
|
+
const result = await app.openLink({ url });
|
|
412
|
+
return result.isError !== true;
|
|
413
|
+
};
|
|
414
|
+
requestDisplayMode = async (mode) => {
|
|
415
|
+
const result = await app.requestDisplayMode({ mode });
|
|
416
|
+
return typeof result.mode === 'string' ? result.mode : null;
|
|
417
|
+
};
|
|
649
418
|
trackUiEvent = createUiEventTracker((name, args) => app.callServerTool({ name, arguments: args }), {
|
|
650
419
|
component: 'file_preview',
|
|
651
420
|
baseParams: { tool_name: 'read_file' },
|
|
652
421
|
});
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
422
|
+
app.ontoolinput = (params) => {
|
|
423
|
+
const requestedPath = typeof params.arguments?.path === 'string' ? params.arguments.path : undefined;
|
|
424
|
+
if (!initialStateResolved
|
|
425
|
+
&& pendingCachedPayload
|
|
426
|
+
&& requestedPath
|
|
427
|
+
&& pendingCachedPayload.filePath === requestedPath) {
|
|
428
|
+
const cached = pendingCachedPayload;
|
|
429
|
+
pendingCachedPayload = undefined;
|
|
430
|
+
resolveInitialState(cached);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
660
433
|
renderLoadingState(container);
|
|
661
434
|
onRender?.();
|
|
662
435
|
};
|
|
663
436
|
app.ontoolresult = (result) => {
|
|
437
|
+
pendingCachedPayload = undefined;
|
|
664
438
|
const payload = extractRenderPayload(result);
|
|
665
439
|
const message = extractToolText(result);
|
|
666
440
|
if (!initialStateResolved) {
|
|
667
441
|
if (payload) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
442
|
+
if (needsContentRead(payload)) {
|
|
443
|
+
void readAndResolvePayload(payload, (p) => resolveInitialState(getEffectiveIncomingPayload(p)));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
resolveInitialState(getEffectiveIncomingPayload(payload));
|
|
671
447
|
return;
|
|
672
448
|
}
|
|
673
449
|
if (message) {
|
|
@@ -676,7 +452,13 @@ export function bootstrapApp() {
|
|
|
676
452
|
return;
|
|
677
453
|
}
|
|
678
454
|
if (payload) {
|
|
679
|
-
|
|
455
|
+
if (needsContentRead(payload)) {
|
|
456
|
+
renderLoadingState(container);
|
|
457
|
+
void readAndResolvePayload(payload, (p) => renderAndSync(getEffectiveIncomingPayload(p)));
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
renderAndSync(getEffectiveIncomingPayload(payload));
|
|
461
|
+
}
|
|
680
462
|
}
|
|
681
463
|
else if (message) {
|
|
682
464
|
renderStatusState(container, message);
|
|
@@ -686,18 +468,86 @@ export function bootstrapApp() {
|
|
|
686
468
|
app.ontoolcancelled = (params) => {
|
|
687
469
|
resolveInitialState(undefined, params.reason ?? 'Tool was cancelled.');
|
|
688
470
|
};
|
|
689
|
-
|
|
471
|
+
const handleVisibilitySync = () => {
|
|
472
|
+
if (document.visibilityState === 'visible') {
|
|
473
|
+
syncFromPersistedWidgetState();
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const handleFocusSync = () => {
|
|
477
|
+
// Only sync cross-tab state if the page was hidden (tab switch).
|
|
478
|
+
// Simple focus changes within the same page should not trigger a re-render
|
|
479
|
+
// as it destroys the active editor.
|
|
480
|
+
if (document.visibilityState !== 'visible') {
|
|
481
|
+
syncFromPersistedWidgetState();
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
const teardown = () => {
|
|
485
|
+
shellController?.dispose();
|
|
486
|
+
shellController = undefined;
|
|
487
|
+
markdownController.disposeHandles();
|
|
488
|
+
selectionAbortController?.abort();
|
|
489
|
+
selectionAbortController = null;
|
|
490
|
+
document.removeEventListener('visibilitychange', handleVisibilitySync);
|
|
491
|
+
window.removeEventListener('focus', handleFocusSync);
|
|
492
|
+
};
|
|
493
|
+
document.addEventListener('visibilitychange', handleVisibilitySync);
|
|
494
|
+
window.addEventListener('focus', handleFocusSync);
|
|
495
|
+
app.onteardown = async () => {
|
|
496
|
+
teardown();
|
|
497
|
+
return {};
|
|
498
|
+
};
|
|
690
499
|
void connectWithSharedHostContext({
|
|
691
500
|
app,
|
|
692
501
|
chrome,
|
|
693
|
-
onContextApplied:
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
502
|
+
onContextApplied: () => {
|
|
503
|
+
const previousDisplayMode = getCurrentDisplayMode();
|
|
504
|
+
syncChromeState();
|
|
505
|
+
currentHostContext = app.getHostContext();
|
|
506
|
+
const nextDisplayMode = getCurrentDisplayMode();
|
|
507
|
+
const displayModeChanged = previousDisplayMode !== nextDisplayMode;
|
|
508
|
+
// Clicking a display-mode button blurs the editor first, and the
|
|
509
|
+
// editor's onBlur handler already persists dirty drafts, so there
|
|
510
|
+
// is nothing additional to save here.
|
|
511
|
+
if (previousDisplayMode === 'fullscreen'
|
|
512
|
+
&& nextDisplayMode === 'inline'
|
|
513
|
+
&& currentPayload?.fileType === 'markdown') {
|
|
514
|
+
isExpanded = true;
|
|
515
|
+
chrome.expanded = true;
|
|
516
|
+
const restorePayload = inlinePayloadBeforeFullscreen ?? hostPayload;
|
|
517
|
+
const restoreWasPartial = restorePayload ? parseReadRange(restorePayload.content)?.isPartial === true : false;
|
|
518
|
+
if (restoreWasPartial && restorePayload) {
|
|
519
|
+
localPayloadOverride = restorePayload;
|
|
520
|
+
currentPayload = restorePayload;
|
|
521
|
+
widgetState.write(restorePayload);
|
|
522
|
+
void markdownController.handleInlineExitFromFullscreen(restorePayload).then((freshPayload) => {
|
|
523
|
+
if (freshPayload) {
|
|
524
|
+
currentPayload = freshPayload;
|
|
525
|
+
localPayloadOverride = freshPayload;
|
|
526
|
+
widgetState.write(freshPayload);
|
|
527
|
+
rerenderCurrent?.();
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
void markdownController.handleInlineExitFromFullscreen();
|
|
533
|
+
}
|
|
534
|
+
inlinePayloadBeforeFullscreen = undefined;
|
|
535
|
+
}
|
|
536
|
+
if (previousDisplayMode !== 'fullscreen'
|
|
537
|
+
&& nextDisplayMode === 'fullscreen'
|
|
538
|
+
&& currentPayload?.fileType === 'markdown') {
|
|
539
|
+
inlinePayloadBeforeFullscreen = currentPayload;
|
|
540
|
+
if (parseReadRange(currentPayload.content)?.isPartial) {
|
|
541
|
+
void markdownController.requestEditMode(currentPayload);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (initialStateResolved && displayModeChanged) {
|
|
545
|
+
rerenderCurrent?.();
|
|
699
546
|
}
|
|
700
|
-
|
|
547
|
+
},
|
|
548
|
+
onConnected: () => {
|
|
549
|
+
currentHostContext = app.getHostContext();
|
|
550
|
+
pendingCachedPayload = widgetState.read() ?? undefined;
|
|
701
551
|
window.setTimeout(() => {
|
|
702
552
|
if (!initialStateResolved) {
|
|
703
553
|
resolveInitialState(undefined, 'Preview unavailable after page refresh. Switch threads or re-run the tool.');
|
|
@@ -709,6 +559,6 @@ export function bootstrapApp() {
|
|
|
709
559
|
onRender?.();
|
|
710
560
|
});
|
|
711
561
|
window.addEventListener('beforeunload', () => {
|
|
712
|
-
|
|
562
|
+
teardown();
|
|
713
563
|
}, { once: true });
|
|
714
564
|
}
|