@wonderwhy-er/desktop-commander 0.2.37 → 0.2.39
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 +290 -100
- package/dist/command-manager.js +6 -3
- package/dist/config-field-definitions.d.ts +41 -0
- package/dist/config-field-definitions.js +37 -0
- package/dist/config-manager.d.ts +2 -0
- package/dist/config-manager.js +22 -2
- package/dist/handlers/filesystem-handlers.d.ts +5 -0
- package/dist/handlers/filesystem-handlers.js +19 -12
- package/dist/remote-device/desktop-commander-integration.js +1 -1
- package/dist/remote-device/remote-channel.js +1 -1
- package/dist/search-manager.js +31 -38
- package/dist/server.js +11 -4
- package/dist/terminal-manager.js +4 -2
- package/dist/tools/config.d.ts +71 -0
- package/dist/tools/config.js +117 -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/tools/schemas.d.ts +3 -0
- package/dist/tools/schemas.js +1 -0
- package/dist/types.d.ts +0 -1
- package/dist/ui/config-editor/app.d.ts +43 -0
- package/dist/ui/config-editor/app.js +840 -0
- package/dist/ui/config-editor/array-modal.d.ts +19 -0
- package/dist/ui/config-editor/array-modal.js +185 -0
- package/dist/ui/config-editor/config-editor-runtime.js +150 -0
- package/dist/ui/config-editor/index.html +13 -0
- package/dist/ui/config-editor/main.js +2 -0
- package/dist/ui/config-editor/src/App.d.ts +43 -0
- package/dist/ui/config-editor/src/App.js +840 -0
- package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
- package/dist/ui/config-editor/src/array-modal.js +185 -0
- package/dist/ui/config-editor/src/components/layout.d.ts +4 -0
- package/dist/ui/config-editor/src/components/layout.js +83 -0
- package/dist/ui/config-editor/src/components/toolbar.d.ts +1 -0
- package/dist/ui/config-editor/src/components/toolbar.js +21 -0
- package/dist/ui/config-editor/src/config-values.d.ts +6 -0
- package/dist/ui/config-editor/src/config-values.js +61 -0
- package/dist/ui/config-editor/src/contracts.d.ts +14 -0
- package/dist/ui/config-editor/src/contracts.js +3 -0
- package/dist/ui/config-editor/src/directory-browser.d.ts +6 -0
- package/dist/ui/config-editor/src/directory-browser.js +71 -0
- package/dist/ui/config-editor/src/layout.d.ts +5 -0
- package/dist/ui/config-editor/src/layout.js +90 -0
- package/dist/ui/config-editor/src/main.js +2 -0
- package/dist/ui/config-editor/src/parsing.d.ts +5 -0
- package/dist/ui/config-editor/src/parsing.js +50 -0
- package/dist/ui/config-editor/src/toolbar.d.ts +1 -0
- package/dist/ui/config-editor/src/toolbar.js +18 -0
- package/dist/ui/config-editor/src/types.d.ts +17 -0
- package/dist/ui/config-editor/src/types.js +3 -0
- package/dist/ui/config-editor/src/utils/config-values.d.ts +9 -0
- package/dist/ui/config-editor/src/utils/config-values.js +61 -0
- package/dist/ui/config-editor/src/utils/directory-browser.d.ts +31 -0
- package/dist/ui/config-editor/src/utils/directory-browser.js +201 -0
- package/dist/ui/config-editor/src/utils/parsing.d.ts +8 -0
- package/dist/ui/config-editor/src/utils/parsing.js +50 -0
- package/dist/ui/config-editor/styles.css +587 -0
- package/dist/ui/file-preview/app.d.ts +8 -0
- package/dist/ui/file-preview/app.js +2020 -0
- package/dist/ui/file-preview/components/code-viewer.d.ts +6 -0
- package/dist/ui/file-preview/components/code-viewer.js +73 -0
- package/dist/ui/file-preview/components/highlighting.d.ts +2 -0
- package/dist/ui/file-preview/components/highlighting.js +54 -0
- package/dist/ui/file-preview/components/html-renderer.d.ts +5 -0
- package/dist/ui/file-preview/components/html-renderer.js +47 -0
- package/dist/ui/file-preview/components/markdown-renderer.d.ts +1 -0
- package/dist/ui/file-preview/components/markdown-renderer.js +67 -0
- package/dist/ui/file-preview/components/toolbar.d.ts +6 -0
- package/dist/ui/file-preview/components/toolbar.js +75 -0
- package/dist/ui/file-preview/image-preview.d.ts +3 -0
- package/dist/ui/file-preview/image-preview.js +21 -0
- package/dist/ui/file-preview/main.js +5 -0
- package/dist/ui/file-preview/markdown/editor.d.ts +36 -0
- package/dist/ui/file-preview/markdown/editor.js +643 -0
- package/dist/ui/file-preview/markdown/linking.d.ts +9 -0
- package/dist/ui/file-preview/markdown/linking.js +210 -0
- package/dist/ui/file-preview/markdown/outline.d.ts +7 -0
- package/dist/ui/file-preview/markdown/outline.js +40 -0
- package/dist/ui/file-preview/markdown/preview.d.ts +8 -0
- package/dist/ui/file-preview/markdown/preview.js +33 -0
- package/dist/ui/file-preview/markdown/slugify.d.ts +3 -0
- package/dist/ui/file-preview/markdown/slugify.js +31 -0
- package/dist/ui/file-preview/markdown/toc.d.ts +11 -0
- package/dist/ui/file-preview/markdown/toc.js +75 -0
- package/dist/ui/file-preview/markdown/utils.d.ts +1 -0
- package/dist/ui/file-preview/markdown/utils.js +15 -0
- package/dist/ui/file-preview/markdown/workspace-controller.d.ts +25 -0
- package/dist/ui/file-preview/markdown/workspace-controller.js +40 -0
- package/dist/ui/file-preview/preview-runtime.js +399 -13969
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +1 -1
- package/dist/ui/file-preview/shared/preview-file-types.js +3 -1
- package/dist/ui/file-preview/src/App.d.ts +4 -0
- package/dist/ui/file-preview/src/App.js +564 -0
- package/dist/ui/file-preview/src/components/CodeViewer.d.ts +6 -0
- package/dist/ui/file-preview/src/components/CodeViewer.js +60 -0
- package/dist/ui/file-preview/src/components/HtmlRenderer.d.ts +8 -0
- package/dist/ui/file-preview/src/components/HtmlRenderer.js +45 -0
- package/dist/ui/file-preview/src/components/MarkdownRenderer.d.ts +1 -0
- package/dist/ui/file-preview/src/components/MarkdownRenderer.js +15 -0
- package/dist/ui/file-preview/src/components/editor-toolbar.d.ts +15 -0
- package/dist/ui/file-preview/src/components/editor-toolbar.js +384 -0
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +1 -5
- package/dist/ui/file-preview/src/components/html-renderer.js +11 -27
- package/dist/ui/file-preview/src/components/markdown-editor.d.ts +29 -0
- package/dist/ui/file-preview/src/components/markdown-editor.js +535 -0
- 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/block-merge.d.ts +25 -0
- package/dist/ui/file-preview/src/markdown/block-merge.js +86 -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 +38 -0
- package/dist/ui/file-preview/src/markdown/controller.js +921 -0
- package/dist/ui/file-preview/src/markdown/editor.d.ts +35 -0
- package/dist/ui/file-preview/src/markdown/editor.js +691 -0
- package/dist/ui/file-preview/src/markdown/link-modal.d.ts +13 -0
- package/dist/ui/file-preview/src/markdown/link-modal.js +213 -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/raw-editor.d.ts +8 -0
- package/dist/ui/file-preview/src/markdown/raw-editor.js +61 -0
- package/dist/ui/file-preview/src/markdown/selection-toolbar.d.ts +14 -0
- package/dist/ui/file-preview/src/markdown/selection-toolbar.js +128 -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/toc.d.ts +11 -0
- package/dist/ui/file-preview/src/markdown/toc.js +75 -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/markdown-workspace/editor.d.ts +36 -0
- package/dist/ui/file-preview/src/markdown-workspace/editor.js +643 -0
- package/dist/ui/file-preview/src/markdown-workspace/linking.d.ts +9 -0
- package/dist/ui/file-preview/src/markdown-workspace/linking.js +210 -0
- package/dist/ui/file-preview/src/markdown-workspace/outline.d.ts +7 -0
- package/dist/ui/file-preview/src/markdown-workspace/outline.js +40 -0
- package/dist/ui/file-preview/src/markdown-workspace/preview.d.ts +8 -0
- package/dist/ui/file-preview/src/markdown-workspace/preview.js +33 -0
- package/dist/ui/file-preview/src/markdown-workspace/slugify.d.ts +3 -0
- package/dist/ui/file-preview/src/markdown-workspace/slugify.js +31 -0
- package/dist/ui/file-preview/src/markdown-workspace/toc.d.ts +11 -0
- package/dist/ui/file-preview/src/markdown-workspace/toc.js +75 -0
- package/dist/ui/file-preview/src/markdown-workspace/utils.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown-workspace/utils.js +15 -0
- package/dist/ui/file-preview/src/markdown-workspace/workspace-controller.d.ts +25 -0
- package/dist/ui/file-preview/src/markdown-workspace/workspace-controller.js +40 -0
- package/dist/ui/file-preview/src/model.d.ts +34 -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 +1144 -277
- package/dist/ui/file-preview/types.d.ts +1 -0
- package/dist/ui/file-preview/types.js +1 -0
- package/dist/ui/resources.d.ts +7 -0
- package/dist/ui/resources.js +16 -2
- package/dist/ui/server-integration.d.ts +13 -0
- package/dist/ui/server-integration.js +31 -0
- package/dist/ui/shared/ToolHeader.d.ts +9 -0
- package/dist/ui/shared/ToolHeader.js +29 -0
- package/dist/ui/shared/app-bootstrap.d.ts +9 -0
- package/dist/ui/shared/app-bootstrap.js +15 -0
- package/dist/ui/shared/compact-row.d.ts +11 -0
- package/dist/ui/shared/compact-row.js +18 -0
- package/dist/ui/shared/guards.d.ts +1 -0
- package/dist/ui/shared/guards.js +3 -0
- package/dist/ui/shared/host-context.d.ts +15 -0
- package/dist/ui/shared/host-context.js +51 -0
- package/dist/ui/shared/host-lifecycle.d.ts +1 -0
- package/dist/ui/shared/host-lifecycle.js +8 -2
- package/dist/ui/shared/tool-bridge.d.ts +30 -0
- package/dist/ui/shared/tool-bridge.js +137 -0
- package/dist/ui/shared/tool-shell.d.ts +9 -0
- package/dist/ui/shared/tool-shell.js +46 -4
- package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
- package/dist/ui/shared/ui-event-tracker.js +27 -0
- package/dist/ui/shared/widget-state.d.ts +6 -1
- package/dist/ui/shared/widget-state.js +102 -4
- package/dist/utils/capture.js +3 -3
- package/dist/utils/files/base.d.ts +2 -0
- package/dist/utils/open-browser.js +1 -1
- package/dist/utils/ui-call-context.d.ts +8 -0
- package/dist/utils/ui-call-context.js +72 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -4
- package/dist/data/spec-kit-prompts.json +0 -123
- 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/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-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.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/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/app.d.ts +0 -4
- package/dist/ui/file-preview/src/app.js +0 -800
- 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/config-editor/main.d.ts} +0 -0
- /package/dist/{test-docx.d.ts → ui/config-editor/src/main.d.ts} +0 -0
- /package/dist/{tools/docx/xml-view-test.d.ts → ui/file-preview/main.d.ts} +0 -0
- /package/dist/ui/file-preview/src/components/{toolbar.d.ts → Toolbar.d.ts} +0 -0
- /package/dist/ui/file-preview/src/components/{toolbar.js → Toolbar.js} +0 -0
- /package/dist/{tools/pdf-processor.d.ts → ui/file-preview/src/model.js} +0 -0
|
@@ -0,0 +1,2020 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level controller for the File Preview app. It routes structured content into the appropriate renderer, handles host events, and coordinates user-facing state changes.
|
|
3
|
+
*/
|
|
4
|
+
import { formatJsonIfPossible, inferLanguageFromPath, renderCodeViewer } from './components/code-viewer.js';
|
|
5
|
+
import { renderHtmlPreview } from './components/html-renderer.js';
|
|
6
|
+
import { escapeHtml } from './components/highlighting.js';
|
|
7
|
+
import { isAllowedImageMimeType, normalizeImageMimeType } from './image-preview.js';
|
|
8
|
+
import { mountMarkdownEditor, renderMarkdownCopyButton, renderMarkdownEditorShell, renderMarkdownModeToggle } from './markdown/editor.js';
|
|
9
|
+
import { resolveMarkdownLink } from './markdown/linking.js';
|
|
10
|
+
import { extractMarkdownOutline } from './markdown/outline.js';
|
|
11
|
+
import { getRenderedMarkdownCopyText, renderMarkdownWorkspacePreview } from './markdown/preview.js';
|
|
12
|
+
import { slugifyMarkdownHeading } from './markdown/slugify.js';
|
|
13
|
+
import { attachMarkdownToc, renderMarkdownToc } from './markdown/toc.js';
|
|
14
|
+
import { getMarkdownEditAvailability, getMarkdownFullscreenAvailability, parseReadRange, shouldAutoLoadMarkdownOnEnterFullscreen, stripReadStatusLine } from './markdown/workspace-controller.js';
|
|
15
|
+
import { createCompactRowShellController } from '../shared/tool-shell.js';
|
|
16
|
+
import { createWidgetStateStorage } from '../shared/widget-state.js';
|
|
17
|
+
import { renderCompactRow } from '../shared/compact-row.js';
|
|
18
|
+
import { connectWithSharedHostContext, isObjectRecord } from '../shared/host-context.js';
|
|
19
|
+
import { createUiEventTracker } from '../shared/ui-event-tracker.js';
|
|
20
|
+
import { App } from '@modelcontextprotocol/ext-apps';
|
|
21
|
+
let isExpanded = false;
|
|
22
|
+
let hideSummaryRow = false;
|
|
23
|
+
let previewShownFired = false;
|
|
24
|
+
let onRender;
|
|
25
|
+
let trackUiEvent;
|
|
26
|
+
let rpcCallTool;
|
|
27
|
+
let rpcUpdateContext;
|
|
28
|
+
let openExternalLink;
|
|
29
|
+
let requestDisplayMode;
|
|
30
|
+
let shellController;
|
|
31
|
+
let currentPayload;
|
|
32
|
+
let currentHtmlMode = 'rendered';
|
|
33
|
+
let currentHostContext;
|
|
34
|
+
let rerenderCurrent;
|
|
35
|
+
let syncPayload;
|
|
36
|
+
let persistPayload;
|
|
37
|
+
let markdownEditorHandle;
|
|
38
|
+
let markdownTocHandle;
|
|
39
|
+
let localPayloadOverride;
|
|
40
|
+
let directoryBackPayload;
|
|
41
|
+
const markdownEditorAppCache = new Map();
|
|
42
|
+
const markdownEditorAppPending = new Set();
|
|
43
|
+
let markdownWorkspaceState;
|
|
44
|
+
function getFileExtensionForAnalytics(filePath) {
|
|
45
|
+
const normalizedPath = filePath.trim().replace(/\\/g, '/');
|
|
46
|
+
const fileName = normalizedPath.split('/').pop() ?? normalizedPath;
|
|
47
|
+
const dotIndex = fileName.lastIndexOf('.');
|
|
48
|
+
if (dotIndex <= 0 || dotIndex === fileName.length - 1) {
|
|
49
|
+
return 'none';
|
|
50
|
+
}
|
|
51
|
+
return fileName.slice(dotIndex + 1).toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
function isPreviewStructuredContent(value) {
|
|
54
|
+
if (!isObjectRecord(value)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return (typeof value.fileName === 'string' &&
|
|
58
|
+
typeof value.filePath === 'string' &&
|
|
59
|
+
typeof value.fileType === 'string');
|
|
60
|
+
}
|
|
61
|
+
function buildRenderPayload(meta, text) {
|
|
62
|
+
return { ...meta, content: text };
|
|
63
|
+
}
|
|
64
|
+
function extractRenderPayload(value) {
|
|
65
|
+
if (!isObjectRecord(value)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const meta = isPreviewStructuredContent(value.structuredContent)
|
|
69
|
+
? value.structuredContent
|
|
70
|
+
: isPreviewStructuredContent(value)
|
|
71
|
+
? value
|
|
72
|
+
: null;
|
|
73
|
+
if (!meta)
|
|
74
|
+
return undefined;
|
|
75
|
+
const text = extractToolText(value) ?? extractToolText(value.structuredContent) ?? '';
|
|
76
|
+
return buildRenderPayload(meta, text);
|
|
77
|
+
}
|
|
78
|
+
function extractToolText(value) {
|
|
79
|
+
if (!isObjectRecord(value)) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const content = value.content;
|
|
83
|
+
if (!Array.isArray(content)) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
for (const item of content) {
|
|
87
|
+
if (!isObjectRecord(item)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (item.type === 'text' && typeof item.text === 'string' && item.text.trim().length > 0) {
|
|
91
|
+
return item.text;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
function assertSuccessfulEditBlockResult(result) {
|
|
97
|
+
if (!isObjectRecord(result)) {
|
|
98
|
+
throw new Error('edit_block did not return a valid result.');
|
|
99
|
+
}
|
|
100
|
+
const message = extractToolText(result) ?? '';
|
|
101
|
+
if (result.isError === true) {
|
|
102
|
+
throw new Error(message || 'edit_block failed.');
|
|
103
|
+
}
|
|
104
|
+
if (!/Successfully applied/i.test(message)) {
|
|
105
|
+
throw new Error(message || 'edit_block did not confirm success.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function isLikelyUrl(filePath) {
|
|
109
|
+
return /^https?:\/\//i.test(filePath);
|
|
110
|
+
}
|
|
111
|
+
function buildBreadcrumb(filePath) {
|
|
112
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
113
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
114
|
+
return parts.map(p => escapeHtml(p)).join(' <span class="breadcrumb-sep">›</span> ');
|
|
115
|
+
}
|
|
116
|
+
function getParentDirectory(filePath) {
|
|
117
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
118
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
119
|
+
if (lastSlash <= 0) {
|
|
120
|
+
return filePath;
|
|
121
|
+
}
|
|
122
|
+
return normalized.slice(0, lastSlash);
|
|
123
|
+
}
|
|
124
|
+
function getAncestorDirectories(filePath) {
|
|
125
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
126
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
127
|
+
const ancestors = [];
|
|
128
|
+
for (let index = parts.length - 1; index > 0; index -= 1) {
|
|
129
|
+
const prefix = normalized.startsWith('/') ? '/' : '';
|
|
130
|
+
ancestors.push(`${prefix}${parts.slice(0, index).join('/')}`);
|
|
131
|
+
}
|
|
132
|
+
return ancestors;
|
|
133
|
+
}
|
|
134
|
+
function splitListingLines(text) {
|
|
135
|
+
return text.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
136
|
+
}
|
|
137
|
+
function parseFileSearchResults(text) {
|
|
138
|
+
return text.split('\n')
|
|
139
|
+
.map((line) => line.trim())
|
|
140
|
+
.filter((line) => line.startsWith('📁 '))
|
|
141
|
+
.map((line) => line.slice(3).trim());
|
|
142
|
+
}
|
|
143
|
+
function toPosixRelativePath(fromDirectory, targetPath) {
|
|
144
|
+
const fromParts = fromDirectory.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
145
|
+
const targetParts = targetPath.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
146
|
+
let shared = 0;
|
|
147
|
+
while (shared < fromParts.length && shared < targetParts.length && fromParts[shared] === targetParts[shared]) {
|
|
148
|
+
shared += 1;
|
|
149
|
+
}
|
|
150
|
+
const up = new Array(Math.max(fromParts.length - shared, 0)).fill('..');
|
|
151
|
+
const down = targetParts.slice(shared);
|
|
152
|
+
const joined = [...up, ...down].join('/');
|
|
153
|
+
return joined.length > 0 ? joined : '.';
|
|
154
|
+
}
|
|
155
|
+
function stripMarkdownExtension(filePath) {
|
|
156
|
+
return filePath.replace(/\.md$/i, '');
|
|
157
|
+
}
|
|
158
|
+
async function resolveMarkdownLinkSearchRoot(filePath) {
|
|
159
|
+
const ancestors = getAncestorDirectories(filePath);
|
|
160
|
+
const markers = new Set(['[DIR] .git', '[DIR] .obsidian', '[FILE] package.json', '[FILE] pnpm-workspace.yaml', '[FILE] turbo.json']);
|
|
161
|
+
for (const ancestor of ancestors) {
|
|
162
|
+
try {
|
|
163
|
+
const result = await rpcCallTool?.('list_directory', { path: ancestor, depth: 1 });
|
|
164
|
+
const text = extractToolText(result) ?? '';
|
|
165
|
+
const entries = splitListingLines(text);
|
|
166
|
+
if (entries.some((entry) => markers.has(entry))) {
|
|
167
|
+
return ancestor;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Ignore and continue up the tree.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return getParentDirectory(filePath);
|
|
175
|
+
}
|
|
176
|
+
async function searchMarkdownLinkTargets(filePath, query) {
|
|
177
|
+
const trimmedQuery = query.trim();
|
|
178
|
+
if (trimmedQuery.length === 0) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
const rootPath = await resolveMarkdownLinkSearchRoot(filePath);
|
|
182
|
+
const result = await rpcCallTool?.('start_search', {
|
|
183
|
+
path: rootPath,
|
|
184
|
+
pattern: trimmedQuery,
|
|
185
|
+
searchType: 'files',
|
|
186
|
+
filePattern: '*.md',
|
|
187
|
+
maxResults: 20,
|
|
188
|
+
earlyTermination: false,
|
|
189
|
+
literalSearch: true,
|
|
190
|
+
});
|
|
191
|
+
const text = extractToolText(result) ?? '';
|
|
192
|
+
const filePaths = parseFileSearchResults(text);
|
|
193
|
+
const currentDirectory = getParentDirectory(filePath);
|
|
194
|
+
return filePaths.map((targetPath) => {
|
|
195
|
+
const normalized = targetPath.replace(/\\/g, '/');
|
|
196
|
+
const fileName = normalized.split('/').pop() ?? normalized;
|
|
197
|
+
const title = stripMarkdownExtension(fileName);
|
|
198
|
+
const relativePath = toPosixRelativePath(currentDirectory, normalized);
|
|
199
|
+
const wikiPath = stripMarkdownExtension(relativePath.startsWith('./') ? relativePath.slice(2) : relativePath);
|
|
200
|
+
return {
|
|
201
|
+
path: normalized,
|
|
202
|
+
title,
|
|
203
|
+
wikiPath,
|
|
204
|
+
relativePath,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async function loadMarkdownLinkHeadings(currentPayloadPath, targetPath) {
|
|
209
|
+
if (targetPath === currentPayloadPath && markdownWorkspaceState) {
|
|
210
|
+
return extractMarkdownOutline(markdownWorkspaceState.sourceContent).map((item) => ({ id: item.id, text: item.text }));
|
|
211
|
+
}
|
|
212
|
+
const result = await rpcCallTool?.('read_file', {
|
|
213
|
+
path: targetPath,
|
|
214
|
+
offset: 0,
|
|
215
|
+
length: 5000,
|
|
216
|
+
});
|
|
217
|
+
const text = extractToolText(result) ?? '';
|
|
218
|
+
return extractMarkdownOutline(stripReadStatusLine(text)).map((item) => ({ id: item.id, text: item.text }));
|
|
219
|
+
}
|
|
220
|
+
function shellQuote(value) {
|
|
221
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
222
|
+
}
|
|
223
|
+
function encodePowerShellCommand(script) {
|
|
224
|
+
// PowerShell -EncodedCommand expects UTF-16LE bytes.
|
|
225
|
+
const utf16leBytes = [];
|
|
226
|
+
for (let index = 0; index < script.length; index += 1) {
|
|
227
|
+
const codeUnit = script.charCodeAt(index);
|
|
228
|
+
utf16leBytes.push(codeUnit & 0xff, codeUnit >> 8);
|
|
229
|
+
}
|
|
230
|
+
let binary = '';
|
|
231
|
+
for (const byte of utf16leBytes) {
|
|
232
|
+
binary += String.fromCharCode(byte);
|
|
233
|
+
}
|
|
234
|
+
return btoa(binary);
|
|
235
|
+
}
|
|
236
|
+
function buildOpenInFolderCommand(filePath) {
|
|
237
|
+
const trimmedPath = filePath.trim();
|
|
238
|
+
if (!trimmedPath || isLikelyUrl(trimmedPath)) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
242
|
+
if (userAgent.includes('win')) {
|
|
243
|
+
const escapedForPowerShell = trimmedPath.replace(/'/g, "''");
|
|
244
|
+
const script = `Start-Process -FilePath explorer.exe -ArgumentList @('/select,','${escapedForPowerShell}')`;
|
|
245
|
+
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(script)}`;
|
|
246
|
+
}
|
|
247
|
+
if (userAgent.includes('mac')) {
|
|
248
|
+
return `open -R ${shellQuote(trimmedPath)}`;
|
|
249
|
+
}
|
|
250
|
+
return `xdg-open ${shellQuote(getParentDirectory(trimmedPath))}`;
|
|
251
|
+
}
|
|
252
|
+
function buildOpenInEditorCommand(filePath) {
|
|
253
|
+
const trimmedPath = filePath.trim();
|
|
254
|
+
if (!trimmedPath || isLikelyUrl(trimmedPath)) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
const cachedApp = markdownEditorAppCache.get(trimmedPath);
|
|
258
|
+
if (cachedApp?.appPath && navigator.userAgent.toLowerCase().includes('mac')) {
|
|
259
|
+
return `open -a ${shellQuote(cachedApp.appPath)} ${shellQuote(trimmedPath)}`;
|
|
260
|
+
}
|
|
261
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
262
|
+
if (userAgent.includes('win')) {
|
|
263
|
+
const escapedForPowerShell = trimmedPath.replace(/'/g, "''");
|
|
264
|
+
const script = `Start-Process -FilePath '${escapedForPowerShell}'`;
|
|
265
|
+
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(script)}`;
|
|
266
|
+
}
|
|
267
|
+
if (userAgent.includes('mac')) {
|
|
268
|
+
return `open ${shellQuote(trimmedPath)}`;
|
|
269
|
+
}
|
|
270
|
+
return `xdg-open ${shellQuote(trimmedPath)}`;
|
|
271
|
+
}
|
|
272
|
+
async function detectDefaultMarkdownEditor(filePath) {
|
|
273
|
+
const trimmedPath = filePath.trim();
|
|
274
|
+
if (!trimmedPath || markdownEditorAppCache.has(trimmedPath) || markdownEditorAppPending.has(trimmedPath)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
278
|
+
if (!userAgent.includes('mac')) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
markdownEditorAppPending.add(trimmedPath);
|
|
282
|
+
try {
|
|
283
|
+
const detectCommand = `osascript -e ${shellQuote(`set appAlias to default application of (info for POSIX file "${trimmedPath.replace(/"/g, '\\"')}")
|
|
284
|
+
return (name of (info for appAlias)) & linefeed & POSIX path of appAlias`)}`;
|
|
285
|
+
const detectResult = await rpcCallTool?.('start_process', {
|
|
286
|
+
command: detectCommand,
|
|
287
|
+
timeout_ms: 12000,
|
|
288
|
+
});
|
|
289
|
+
const text = extractToolText(detectResult) ?? '';
|
|
290
|
+
if (!text || text.toLowerCase().includes('error') || text.toLowerCase().includes('execution')) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
294
|
+
const appName = lines[lines.length - 2]?.replace(/\.app$/i, '') ?? '';
|
|
295
|
+
const appPath = lines[lines.length - 1] ?? '';
|
|
296
|
+
if (appName && appPath.startsWith('/')) {
|
|
297
|
+
markdownEditorAppCache.set(trimmedPath, {
|
|
298
|
+
appName,
|
|
299
|
+
appPath,
|
|
300
|
+
});
|
|
301
|
+
rerenderCurrent?.();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// Fall back to generic editor label.
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
markdownEditorAppPending.delete(trimmedPath);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function renderMarkdownEditorAppIcon() {
|
|
312
|
+
return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 1 1 3 3L7 19l-4 1 1-4Z"/></svg>';
|
|
313
|
+
}
|
|
314
|
+
function renderRawFallback(source) {
|
|
315
|
+
return `<pre class="code-viewer"><code class="hljs language-text">${escapeHtml(source)}</code></pre>`;
|
|
316
|
+
}
|
|
317
|
+
function parseDirectoryEntries(content) {
|
|
318
|
+
const lines = content.split('\n');
|
|
319
|
+
// First line(s) before listing are the hint message
|
|
320
|
+
const hintLines = [];
|
|
321
|
+
const entryLines = [];
|
|
322
|
+
for (const line of lines) {
|
|
323
|
+
if (/^\[(DIR|FILE|DENIED|WARNING)\]/.test(line.trim())) {
|
|
324
|
+
entryLines.push(line.trim());
|
|
325
|
+
}
|
|
326
|
+
else if (entryLines.length === 0) {
|
|
327
|
+
hintLines.push(line);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Build flat list
|
|
331
|
+
const flat = [];
|
|
332
|
+
for (const line of entryLines) {
|
|
333
|
+
if (line.startsWith('[WARNING]')) {
|
|
334
|
+
// Format: [WARNING] dirName: N items hidden (showing first M of T total)
|
|
335
|
+
const warnBody = line.replace(/^\[WARNING\]\s*/, '');
|
|
336
|
+
const colonIdx = warnBody.indexOf(':');
|
|
337
|
+
const dirName = colonIdx >= 0 ? warnBody.slice(0, colonIdx).trim() : '';
|
|
338
|
+
const msg = colonIdx >= 0 ? warnBody.slice(colonIdx + 1).trim() : warnBody;
|
|
339
|
+
// Depth matches the directory it belongs to — infer from dirName path segments
|
|
340
|
+
const parts = dirName.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
341
|
+
const depth = parts.length; // warning sits inside the dir, so same depth as children
|
|
342
|
+
flat.push({ name: dirName, fullPath: dirName, isDir: false, isDenied: false, isWarning: true, warningText: msg, depth });
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const isDir = line.startsWith('[DIR]');
|
|
346
|
+
const isDenied = line.startsWith('[DENIED]');
|
|
347
|
+
const name = line.replace(/^\[(DIR|FILE|DENIED)\]\s*/, '');
|
|
348
|
+
const parts = name.replace(/\\/g, '/').split('/');
|
|
349
|
+
flat.push({ name, fullPath: name, isDir, isDenied, isWarning: false, warningText: '', depth: parts.length - 1 });
|
|
350
|
+
}
|
|
351
|
+
// Build tree from flat list
|
|
352
|
+
const root = [];
|
|
353
|
+
const stack = [root];
|
|
354
|
+
for (const item of flat) {
|
|
355
|
+
const baseName = item.fullPath.replace(/\\/g, '/').split('/').pop() ?? item.fullPath;
|
|
356
|
+
const entry = { name: baseName, isDir: item.isDir, isDenied: item.isDenied, isWarning: item.isWarning, warningText: item.warningText, depth: item.depth, children: [], relativePath: item.fullPath };
|
|
357
|
+
// Adjust stack to match depth
|
|
358
|
+
while (stack.length > item.depth + 1)
|
|
359
|
+
stack.pop();
|
|
360
|
+
const parent = stack[stack.length - 1];
|
|
361
|
+
parent.push(entry);
|
|
362
|
+
if (item.isDir) {
|
|
363
|
+
stack.push(entry.children);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { hint: hintLines.join('\n').trim(), entries: root };
|
|
367
|
+
}
|
|
368
|
+
let dirEntryIdCounter = 0;
|
|
369
|
+
function renderDirTree(entries, rootPath) {
|
|
370
|
+
if (entries.length === 0)
|
|
371
|
+
return '<div class="dir-tree"><span class="dir-empty">Empty directory</span></div>';
|
|
372
|
+
function renderEntries(items) {
|
|
373
|
+
return items.map(item => {
|
|
374
|
+
const id = `de-${dirEntryIdCounter++}`;
|
|
375
|
+
const fullPath = rootPath + '/' + item.relativePath.replace(/\\/g, '/');
|
|
376
|
+
const ep = escapeHtml(fullPath);
|
|
377
|
+
if (item.isWarning) {
|
|
378
|
+
const parentPath = rootPath + '/' + item.relativePath.replace(/\\/g, '/');
|
|
379
|
+
const epp = escapeHtml(parentPath);
|
|
380
|
+
return `<div class="dir-entry"><button class="dir-row dir-row-warning dir-load-more" data-loadpath="${epp}"><span class="dir-warning-icon">⚠️</span> <span class="dir-warning-text">${escapeHtml(item.warningText)} — click to load all</span></button></div>`;
|
|
381
|
+
}
|
|
382
|
+
if (item.isDenied) {
|
|
383
|
+
return `<div class="dir-entry"><span class="dir-icon">🚫</span> <span class="dir-name-denied">${escapeHtml(item.name)}</span></div>`;
|
|
384
|
+
}
|
|
385
|
+
if (item.isDir) {
|
|
386
|
+
const has = item.children.length > 0;
|
|
387
|
+
const chev = `<span class="dir-chevron${has ? ' expanded' : ''}">${has ? '▼' : '▶'}</span>`;
|
|
388
|
+
const openBtn = `<button class="dir-open-btn" data-openpath="${ep}" title="Open in Finder">📂</button>`;
|
|
389
|
+
const ch = has ? `<div class="dir-children" id="${id}-ch">${renderEntries(item.children)}</div>` : '';
|
|
390
|
+
return `<div class="dir-entry-group" id="${id}"><div class="dir-row dir-row-folder" data-path="${ep}" data-eid="${id}" data-loaded="${has}">${chev} <span class="dir-icon">📁</span> <span class="dir-name">${escapeHtml(item.name)}</span>${openBtn}</div>${ch}</div>`;
|
|
391
|
+
}
|
|
392
|
+
return `<div class="dir-entry"><div class="dir-row dir-row-file" data-path="${ep}"><span class="file-icon">📄</span> <span class="file-name">${escapeHtml(item.name)}</span></div></div>`;
|
|
393
|
+
}).join('');
|
|
394
|
+
}
|
|
395
|
+
return `<div class="dir-tree">${renderEntries(entries)}</div>`;
|
|
396
|
+
}
|
|
397
|
+
function renderDirectoryBody(content, rootPath) {
|
|
398
|
+
dirEntryIdCounter = 0;
|
|
399
|
+
const { hint, entries } = parseDirectoryEntries(content);
|
|
400
|
+
const treeHtml = renderDirTree(entries, rootPath);
|
|
401
|
+
return {
|
|
402
|
+
notice: hint || undefined,
|
|
403
|
+
html: `<div class="panel-content directory-content">${treeHtml}</div>`
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function attachDirectoryHandlers(container, rootPayload) {
|
|
407
|
+
const tree = container.querySelector('.dir-tree');
|
|
408
|
+
if (!tree)
|
|
409
|
+
return;
|
|
410
|
+
tree.addEventListener('click', async (e) => {
|
|
411
|
+
// Handle "open in finder" button — stop propagation so folder doesn't toggle
|
|
412
|
+
const openBtn = e.target.closest('.dir-open-btn');
|
|
413
|
+
if (openBtn) {
|
|
414
|
+
e.stopPropagation();
|
|
415
|
+
const openPath = openBtn.dataset.openpath;
|
|
416
|
+
if (!openPath)
|
|
417
|
+
return;
|
|
418
|
+
const cmd = buildOpenInFolderCommand(openPath);
|
|
419
|
+
if (cmd) {
|
|
420
|
+
try {
|
|
421
|
+
await rpcCallTool?.('start_process', { command: cmd, timeout_ms: 12000 });
|
|
422
|
+
}
|
|
423
|
+
catch { }
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// Handle "load more" warning button — reload parent directory fully
|
|
428
|
+
const loadMoreBtn = e.target.closest('.dir-load-more');
|
|
429
|
+
if (loadMoreBtn) {
|
|
430
|
+
e.stopPropagation();
|
|
431
|
+
const loadPath = loadMoreBtn.dataset.loadpath;
|
|
432
|
+
if (!loadPath)
|
|
433
|
+
return;
|
|
434
|
+
loadMoreBtn.querySelector('.dir-warning-text').textContent = 'Loading…';
|
|
435
|
+
loadMoreBtn.disabled = true;
|
|
436
|
+
try {
|
|
437
|
+
const result = await rpcCallTool?.('list_directory', { path: loadPath, depth: 1 });
|
|
438
|
+
const text = result?.content?.[0]?.text;
|
|
439
|
+
if (text && typeof text === 'string') {
|
|
440
|
+
const parsed = parseDirectoryEntries(text);
|
|
441
|
+
const html = renderDirTree(parsed.entries, loadPath);
|
|
442
|
+
// Replace the parent .dir-children container contents
|
|
443
|
+
const parentChildren = loadMoreBtn.closest('.dir-children');
|
|
444
|
+
if (parentChildren) {
|
|
445
|
+
const temp = document.createElement('div');
|
|
446
|
+
temp.innerHTML = html;
|
|
447
|
+
const inner = temp.querySelector('.dir-tree');
|
|
448
|
+
parentChildren.innerHTML = inner ? inner.innerHTML : '';
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
loadMoreBtn.querySelector('.dir-warning-text').textContent = 'Failed to load';
|
|
454
|
+
loadMoreBtn.disabled = false;
|
|
455
|
+
}
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const target = e.target.closest('.dir-row');
|
|
459
|
+
if (!target)
|
|
460
|
+
return;
|
|
461
|
+
const fullPath = target.dataset.path;
|
|
462
|
+
if (!fullPath)
|
|
463
|
+
return;
|
|
464
|
+
if (target.classList.contains('dir-row-folder')) {
|
|
465
|
+
const eid = target.dataset.eid;
|
|
466
|
+
if (!eid)
|
|
467
|
+
return;
|
|
468
|
+
const childrenEl = document.getElementById(`${eid}-ch`);
|
|
469
|
+
const chevron = target.querySelector('.dir-chevron');
|
|
470
|
+
if (childrenEl) {
|
|
471
|
+
const hidden = childrenEl.classList.toggle('dir-collapsed');
|
|
472
|
+
chevron?.classList.toggle('expanded', !hidden);
|
|
473
|
+
if (chevron)
|
|
474
|
+
chevron.textContent = hidden ? '▶' : '▼';
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
if (target.dataset.loaded === 'true')
|
|
478
|
+
return;
|
|
479
|
+
if (chevron)
|
|
480
|
+
chevron.textContent = '⏳';
|
|
481
|
+
try {
|
|
482
|
+
const result = await rpcCallTool?.('list_directory', { path: fullPath, depth: 2 });
|
|
483
|
+
const text = result?.content?.[0]?.text;
|
|
484
|
+
if (text && typeof text === 'string') {
|
|
485
|
+
target.dataset.loaded = 'true';
|
|
486
|
+
const parsed = parseDirectoryEntries(text);
|
|
487
|
+
const html = renderDirTree(parsed.entries, fullPath);
|
|
488
|
+
const wrapper = document.createElement('div');
|
|
489
|
+
wrapper.className = 'dir-children';
|
|
490
|
+
wrapper.id = `${eid}-ch`;
|
|
491
|
+
const temp = document.createElement('div');
|
|
492
|
+
temp.innerHTML = html;
|
|
493
|
+
const inner = temp.querySelector('.dir-tree');
|
|
494
|
+
wrapper.innerHTML = inner ? inner.innerHTML : '<span class="dir-empty">Empty</span>';
|
|
495
|
+
target.parentElement?.appendChild(wrapper);
|
|
496
|
+
chevron?.classList.add('expanded');
|
|
497
|
+
if (chevron)
|
|
498
|
+
chevron.textContent = '▼';
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
if (chevron)
|
|
503
|
+
chevron.textContent = '⚠';
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (target.classList.contains('dir-row-file')) {
|
|
509
|
+
target.classList.add('dir-loading');
|
|
510
|
+
try {
|
|
511
|
+
const result = await rpcCallTool?.('read_file', { path: fullPath });
|
|
512
|
+
const r = result;
|
|
513
|
+
if (r?.structuredContent) {
|
|
514
|
+
directoryBackPayload = rootPayload;
|
|
515
|
+
const text = r.content?.[0]?.text ?? '';
|
|
516
|
+
const newPayload = buildRenderPayload(r.structuredContent, text);
|
|
517
|
+
renderApp(container.closest('#app'), newPayload, 'rendered', true);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
target.classList.remove('dir-loading');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
function renderImageBody(payload) {
|
|
527
|
+
const mimeType = normalizeImageMimeType(payload.mimeType);
|
|
528
|
+
if (!isAllowedImageMimeType(mimeType)) {
|
|
529
|
+
return {
|
|
530
|
+
notice: 'Preview is unavailable for this image format.',
|
|
531
|
+
html: '<div class="panel-content source-content"></div>'
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (!payload.imageData || payload.imageData.trim().length === 0) {
|
|
535
|
+
return {
|
|
536
|
+
notice: 'Preview is unavailable because image data is missing.',
|
|
537
|
+
html: '<div class="panel-content source-content"></div>'
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const src = `data:${mimeType};base64,${payload.imageData}`;
|
|
541
|
+
return {
|
|
542
|
+
html: `<div class="panel-content image-content"><div class="image-preview"><img src="${escapeHtml(src)}" alt="${escapeHtml(payload.fileName)}" loading="eager" decoding="async"></div></div>`
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function countContentLines(content) {
|
|
546
|
+
const cleaned = stripReadStatusLine(content);
|
|
547
|
+
if (cleaned === '')
|
|
548
|
+
return 0;
|
|
549
|
+
const lines = cleaned.split('\n');
|
|
550
|
+
return lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
|
|
551
|
+
}
|
|
552
|
+
function disposeMarkdownWorkspaceHandles() {
|
|
553
|
+
markdownEditorHandle?.destroy();
|
|
554
|
+
markdownEditorHandle = undefined;
|
|
555
|
+
markdownTocHandle?.dispose();
|
|
556
|
+
markdownTocHandle = undefined;
|
|
557
|
+
}
|
|
558
|
+
function getAvailableDisplayModes() {
|
|
559
|
+
const rawModes = currentHostContext?.availableDisplayModes;
|
|
560
|
+
if (!Array.isArray(rawModes)) {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
return rawModes.filter((mode) => typeof mode === 'string');
|
|
564
|
+
}
|
|
565
|
+
function getCurrentDisplayMode() {
|
|
566
|
+
return typeof currentHostContext?.displayMode === 'string'
|
|
567
|
+
? currentHostContext.displayMode
|
|
568
|
+
: null;
|
|
569
|
+
}
|
|
570
|
+
function getMarkdownWorkspaceState(payload) {
|
|
571
|
+
const cleanedContent = stripReadStatusLine(payload.content);
|
|
572
|
+
if (!markdownWorkspaceState || markdownWorkspaceState.filePath !== payload.filePath || markdownWorkspaceState.sourceContent !== cleanedContent) {
|
|
573
|
+
const outline = extractMarkdownOutline(cleanedContent);
|
|
574
|
+
const isPartial = parseReadRange(payload.content)?.isPartial === true;
|
|
575
|
+
const prevInitial = markdownWorkspaceState?.filePath === payload.filePath
|
|
576
|
+
? markdownWorkspaceState.initialContent
|
|
577
|
+
: undefined;
|
|
578
|
+
markdownWorkspaceState = {
|
|
579
|
+
filePath: payload.filePath,
|
|
580
|
+
initialContent: prevInitial ?? cleanedContent,
|
|
581
|
+
sourceContent: cleanedContent,
|
|
582
|
+
fullDocumentContent: cleanedContent,
|
|
583
|
+
draftContent: cleanedContent,
|
|
584
|
+
pendingExternalPayload: null,
|
|
585
|
+
mode: isPartial ? 'preview' : 'edit',
|
|
586
|
+
dirty: false,
|
|
587
|
+
activeHeadingId: outline[0]?.id ?? null,
|
|
588
|
+
pendingAnchor: null,
|
|
589
|
+
notice: null,
|
|
590
|
+
error: null,
|
|
591
|
+
saving: false,
|
|
592
|
+
loadingDocument: false,
|
|
593
|
+
editorView: 'markdown',
|
|
594
|
+
editorScrollTop: 0,
|
|
595
|
+
saveIndicator: 'idle',
|
|
596
|
+
fileDeleted: false,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return markdownWorkspaceState;
|
|
600
|
+
}
|
|
601
|
+
function updateCurrentPayload(payload) {
|
|
602
|
+
currentPayload = payload;
|
|
603
|
+
}
|
|
604
|
+
function getEffectiveIncomingPayload(payload) {
|
|
605
|
+
if (!localPayloadOverride) {
|
|
606
|
+
return payload;
|
|
607
|
+
}
|
|
608
|
+
if (localPayloadOverride.filePath !== payload.filePath) {
|
|
609
|
+
localPayloadOverride = undefined;
|
|
610
|
+
return payload;
|
|
611
|
+
}
|
|
612
|
+
const incomingContent = stripReadStatusLine(payload.content);
|
|
613
|
+
const overriddenContent = stripReadStatusLine(localPayloadOverride.content);
|
|
614
|
+
if (incomingContent === overriddenContent) {
|
|
615
|
+
return payload;
|
|
616
|
+
}
|
|
617
|
+
return localPayloadOverride;
|
|
618
|
+
}
|
|
619
|
+
function storePayloadOverride(payload) {
|
|
620
|
+
localPayloadOverride = payload;
|
|
621
|
+
currentPayload = payload;
|
|
622
|
+
persistPayload?.(payload);
|
|
623
|
+
}
|
|
624
|
+
async function ensureCompleteMarkdownPayload(payload) {
|
|
625
|
+
const range = parseReadRange(payload.content);
|
|
626
|
+
if (!range?.isPartial) {
|
|
627
|
+
return payload;
|
|
628
|
+
}
|
|
629
|
+
return (await readMarkdownPayload(payload.filePath, range.totalLines)) ?? payload;
|
|
630
|
+
}
|
|
631
|
+
async function readCompleteMarkdownPayload(filePath) {
|
|
632
|
+
const payload = await readMarkdownPayload(filePath);
|
|
633
|
+
if (!payload) {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
return ensureCompleteMarkdownPayload(payload);
|
|
637
|
+
}
|
|
638
|
+
function getMarkdownPayloadContent(payload) {
|
|
639
|
+
return stripReadStatusLine(payload.content);
|
|
640
|
+
}
|
|
641
|
+
function isMarkdownUndoAvailable(workspaceState) {
|
|
642
|
+
return workspaceState.pendingExternalPayload !== null
|
|
643
|
+
|| workspaceState.draftContent !== workspaceState.initialContent;
|
|
644
|
+
}
|
|
645
|
+
function buildMarkdownWorkspaceBody(payload) {
|
|
646
|
+
const workspaceState = getMarkdownWorkspaceState(payload);
|
|
647
|
+
const outline = extractMarkdownOutline(workspaceState.sourceContent);
|
|
648
|
+
const isFullscreen = getCurrentDisplayMode() === 'fullscreen';
|
|
649
|
+
const tocHtml = isFullscreen ? renderMarkdownToc(outline, workspaceState.activeHeadingId) : '';
|
|
650
|
+
if (!workspaceState.activeHeadingId && outline.length > 0) {
|
|
651
|
+
workspaceState.activeHeadingId = outline[0].id;
|
|
652
|
+
}
|
|
653
|
+
const messages = [workspaceState.error, workspaceState.notice];
|
|
654
|
+
const notice = messages.find((value) => typeof value === 'string' && value.trim().length > 0);
|
|
655
|
+
if (workspaceState.mode === 'edit') {
|
|
656
|
+
return {
|
|
657
|
+
notice,
|
|
658
|
+
html: `
|
|
659
|
+
<div class="panel-content markdown-content markdown-content--workspace">
|
|
660
|
+
<div class="markdown-workspace markdown-workspace--edit${tocHtml ? ' markdown-workspace--with-toc' : ''}">
|
|
661
|
+
${tocHtml}
|
|
662
|
+
<section class="markdown-workspace-main markdown-workspace-main--editor">
|
|
663
|
+
${renderMarkdownEditorShell({
|
|
664
|
+
content: workspaceState.draftContent,
|
|
665
|
+
view: workspaceState.editorView,
|
|
666
|
+
})}
|
|
667
|
+
</section>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
`,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
notice,
|
|
675
|
+
html: `<div class="panel-content markdown-content markdown-content--workspace">${renderMarkdownWorkspacePreview({
|
|
676
|
+
content: workspaceState.sourceContent,
|
|
677
|
+
outline,
|
|
678
|
+
activeHeadingId: workspaceState.activeHeadingId,
|
|
679
|
+
showToc: isFullscreen,
|
|
680
|
+
})}</div>`,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function renderBody(payload, htmlMode, startLine = 1) {
|
|
684
|
+
const cleanedContent = stripReadStatusLine(payload.content);
|
|
685
|
+
if (payload.fileType === 'image') {
|
|
686
|
+
return renderImageBody(payload);
|
|
687
|
+
}
|
|
688
|
+
if (payload.fileType === 'directory') {
|
|
689
|
+
return renderDirectoryBody(cleanedContent, payload.filePath);
|
|
690
|
+
}
|
|
691
|
+
if (payload.fileType === 'unsupported') {
|
|
692
|
+
return {
|
|
693
|
+
notice: 'Preview is not available for this file type.',
|
|
694
|
+
html: '<div class="panel-content source-content"></div>'
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (payload.fileType === 'html') {
|
|
698
|
+
return renderHtmlPreview(cleanedContent, htmlMode);
|
|
699
|
+
}
|
|
700
|
+
if (payload.fileType !== 'markdown') {
|
|
701
|
+
const detectedLanguage = inferLanguageFromPath(payload.filePath);
|
|
702
|
+
const formatted = formatJsonIfPossible(cleanedContent, payload.filePath);
|
|
703
|
+
return {
|
|
704
|
+
notice: formatted.notice,
|
|
705
|
+
html: `<div class="panel-content source-content">${renderCodeViewer(formatted.content, detectedLanguage, startLine)}</div>`
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
return buildMarkdownWorkspaceBody(payload);
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
return {
|
|
713
|
+
notice: 'Markdown renderer failed. Showing raw source instead.',
|
|
714
|
+
html: `<div class="panel-content source-content">${renderRawFallback(cleanedContent)}</div>`
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function attachCopyHandler(payload) {
|
|
719
|
+
const fallbackCopy = (text) => {
|
|
720
|
+
const textArea = document.createElement('textarea');
|
|
721
|
+
textArea.value = text;
|
|
722
|
+
textArea.setAttribute('readonly', '');
|
|
723
|
+
textArea.style.position = 'fixed';
|
|
724
|
+
textArea.style.top = '-9999px';
|
|
725
|
+
document.body.appendChild(textArea);
|
|
726
|
+
textArea.select();
|
|
727
|
+
const success = document.execCommand('copy');
|
|
728
|
+
document.body.removeChild(textArea);
|
|
729
|
+
return success;
|
|
730
|
+
};
|
|
731
|
+
const setButtonState = (button, label, fallbackLabel, revertMs) => {
|
|
732
|
+
button.setAttribute('title', label);
|
|
733
|
+
button.setAttribute('aria-label', label);
|
|
734
|
+
button.textContent = label;
|
|
735
|
+
if (revertMs) {
|
|
736
|
+
setTimeout(() => {
|
|
737
|
+
button.textContent = fallbackLabel;
|
|
738
|
+
button.setAttribute('title', fallbackLabel);
|
|
739
|
+
button.setAttribute('aria-label', fallbackLabel);
|
|
740
|
+
}, revertMs);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
const setIconButtonState = (button, label, fallbackLabel, revertMs) => {
|
|
744
|
+
button.setAttribute('title', label);
|
|
745
|
+
button.setAttribute('aria-label', label);
|
|
746
|
+
button.dataset.status = label;
|
|
747
|
+
if (revertMs) {
|
|
748
|
+
setTimeout(() => {
|
|
749
|
+
button.setAttribute('title', fallbackLabel);
|
|
750
|
+
button.setAttribute('aria-label', fallbackLabel);
|
|
751
|
+
delete button.dataset.status;
|
|
752
|
+
}, revertMs);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
const copyTextData = async (text) => {
|
|
756
|
+
try {
|
|
757
|
+
if (navigator.clipboard?.writeText) {
|
|
758
|
+
await navigator.clipboard.writeText(text);
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
return fallbackCopy(text);
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
return fallbackCopy(text);
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
const copyButton = document.getElementById('copy-source');
|
|
768
|
+
copyButton?.addEventListener('click', async () => {
|
|
769
|
+
trackUiEvent?.('copy_clicked', {
|
|
770
|
+
file_type: payload.fileType,
|
|
771
|
+
file_extension: getFileExtensionForAnalytics(payload.filePath)
|
|
772
|
+
});
|
|
773
|
+
const cleanedContent = stripReadStatusLine(payload.content);
|
|
774
|
+
const copied = await copyTextData(cleanedContent);
|
|
775
|
+
setButtonState(copyButton, copied ? 'Copied!' : 'Copy failed', 'Copy', 1500);
|
|
776
|
+
});
|
|
777
|
+
const activeCopyButton = document.getElementById('copy-active-markdown');
|
|
778
|
+
activeCopyButton?.addEventListener('click', async () => {
|
|
779
|
+
const workspaceState = payload.fileType === 'markdown' ? getMarkdownWorkspaceState(payload) : undefined;
|
|
780
|
+
if (!workspaceState) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const source = workspaceState.mode === 'edit'
|
|
784
|
+
? workspaceState.draftContent
|
|
785
|
+
: stripReadStatusLine(payload.content);
|
|
786
|
+
const textToCopy = workspaceState.editorView === 'raw'
|
|
787
|
+
? source
|
|
788
|
+
: (getRenderedMarkdownCopyText(source) || source);
|
|
789
|
+
const copied = await copyTextData(textToCopy);
|
|
790
|
+
if (copied) {
|
|
791
|
+
updateSaveStatusDOM('Copied', 'saved');
|
|
792
|
+
window.setTimeout(() => updateSaveStatusDOM('', ''), 1500);
|
|
793
|
+
}
|
|
794
|
+
setIconButtonState(activeCopyButton, copied ? 'Copied!' : 'Copy failed', 'Copy', 1500);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
function setMarkdownEditorView(payload, view) {
|
|
798
|
+
const workspaceState = getMarkdownWorkspaceState(payload);
|
|
799
|
+
const wrapper = document.querySelector('.panel-content-wrapper');
|
|
800
|
+
workspaceState.editorScrollTop = wrapper?.scrollTop ?? 0;
|
|
801
|
+
workspaceState.editorView = view;
|
|
802
|
+
workspaceState.notice = null;
|
|
803
|
+
workspaceState.error = null;
|
|
804
|
+
rerenderCurrent?.();
|
|
805
|
+
if (typeof workspaceState.editorScrollTop === 'number') {
|
|
806
|
+
window.requestAnimationFrame(() => {
|
|
807
|
+
const nextWrapper = document.querySelector('.panel-content-wrapper');
|
|
808
|
+
if (nextWrapper) {
|
|
809
|
+
nextWrapper.scrollTop = workspaceState.editorScrollTop;
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function attachHtmlToggleHandler(container, payload, htmlMode) {
|
|
815
|
+
const toggleButton = document.getElementById('toggle-html-mode');
|
|
816
|
+
if (!toggleButton || payload.fileType !== 'html') {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
toggleButton.addEventListener('click', () => {
|
|
820
|
+
const nextMode = htmlMode === 'rendered' ? 'source' : 'rendered';
|
|
821
|
+
trackUiEvent?.('html_view_toggled', {
|
|
822
|
+
file_type: payload.fileType,
|
|
823
|
+
file_extension: getFileExtensionForAnalytics(payload.filePath)
|
|
824
|
+
});
|
|
825
|
+
renderApp(container, payload, nextMode, isExpanded);
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
function attachOpenInFolderHandler(payload) {
|
|
829
|
+
const openButton = document.getElementById('open-in-folder');
|
|
830
|
+
if (!openButton) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const command = buildOpenInFolderCommand(payload.filePath);
|
|
834
|
+
if (!command) {
|
|
835
|
+
openButton.disabled = true;
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
openButton.addEventListener('click', async () => {
|
|
839
|
+
trackUiEvent?.('open_in_folder', {
|
|
840
|
+
file_type: payload.fileType,
|
|
841
|
+
file_extension: getFileExtensionForAnalytics(payload.filePath)
|
|
842
|
+
});
|
|
843
|
+
try {
|
|
844
|
+
await rpcCallTool?.('start_process', {
|
|
845
|
+
command,
|
|
846
|
+
timeout_ms: 12000
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
// Keep UI stable if opening folder fails.
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
function attachOpenInEditorHandler(payload) {
|
|
855
|
+
const openButton = document.getElementById('open-in-editor');
|
|
856
|
+
if (!openButton) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const command = buildOpenInEditorCommand(payload.filePath);
|
|
860
|
+
if (!command) {
|
|
861
|
+
openButton.disabled = true;
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
openButton.addEventListener('click', async () => {
|
|
865
|
+
trackUiEvent?.('open_in_editor', {
|
|
866
|
+
file_type: payload.fileType,
|
|
867
|
+
file_extension: getFileExtensionForAnalytics(payload.filePath)
|
|
868
|
+
});
|
|
869
|
+
try {
|
|
870
|
+
await rpcCallTool?.('start_process', {
|
|
871
|
+
command,
|
|
872
|
+
timeout_ms: 12000
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
// Keep UI stable if opening editor fails.
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
function attachLoadAllHandler(container, payload, htmlMode) {
|
|
881
|
+
const beforeBtn = document.getElementById('load-before');
|
|
882
|
+
const afterBtn = document.getElementById('load-after');
|
|
883
|
+
if (!beforeBtn && !afterBtn) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const range = parseReadRange(payload.content);
|
|
887
|
+
if (!range?.isPartial)
|
|
888
|
+
return;
|
|
889
|
+
const currentContent = stripReadStatusLine(payload.content);
|
|
890
|
+
const loadLines = async (btn, direction) => {
|
|
891
|
+
const originalText = btn.textContent;
|
|
892
|
+
btn.textContent = 'Loading…';
|
|
893
|
+
btn.disabled = true;
|
|
894
|
+
trackUiEvent?.(direction === 'before' ? 'load_lines_before' : 'load_lines_after', {
|
|
895
|
+
file_type: payload.fileType,
|
|
896
|
+
file_extension: getFileExtensionForAnalytics(payload.filePath)
|
|
897
|
+
});
|
|
898
|
+
try {
|
|
899
|
+
// Load only the missing portion
|
|
900
|
+
const readArgs = direction === 'before'
|
|
901
|
+
? { path: payload.filePath, offset: 0, length: range.fromLine - 1 }
|
|
902
|
+
: { path: payload.filePath, offset: range.toLine };
|
|
903
|
+
const result = await rpcCallTool?.('read_file', readArgs);
|
|
904
|
+
const resultObj = result;
|
|
905
|
+
const newText = resultObj?.content?.[0]?.text;
|
|
906
|
+
if (newText && typeof newText === 'string') {
|
|
907
|
+
const cleanNew = stripReadStatusLine(newText);
|
|
908
|
+
// Merge: prepend or append the new lines
|
|
909
|
+
const merged = direction === 'before'
|
|
910
|
+
? cleanNew + (cleanNew.endsWith('\n') ? '' : '\n') + currentContent
|
|
911
|
+
: currentContent + (currentContent.endsWith('\n') ? '' : '\n') + cleanNew;
|
|
912
|
+
// Build updated status line reflecting the new range
|
|
913
|
+
const newFrom = direction === 'before' ? 1 : range.fromLine;
|
|
914
|
+
const newTo = direction === 'after' ? range.totalLines : range.toLine;
|
|
915
|
+
const lineCount = newTo - newFrom + 1;
|
|
916
|
+
const remaining = range.totalLines - newTo;
|
|
917
|
+
const isStillPartial = newFrom > 1 || newTo < range.totalLines;
|
|
918
|
+
const statusLine = isStillPartial
|
|
919
|
+
? `[Reading ${lineCount} lines from ${newFrom === 1 ? 'start' : `line ${newFrom}`} (total: ${range.totalLines} lines, ${remaining} remaining)]\n`
|
|
920
|
+
: '';
|
|
921
|
+
const mergedPayload = {
|
|
922
|
+
...payload,
|
|
923
|
+
content: statusLine + merged
|
|
924
|
+
};
|
|
925
|
+
renderApp(container, mergedPayload, htmlMode, isExpanded);
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
btn.textContent = 'Failed to load';
|
|
929
|
+
setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 2000);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
btn.textContent = 'Failed to load';
|
|
934
|
+
setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 2000);
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
beforeBtn?.addEventListener('click', () => void loadLines(beforeBtn, 'before'));
|
|
938
|
+
afterBtn?.addEventListener('click', () => void loadLines(afterBtn, 'after'));
|
|
939
|
+
}
|
|
940
|
+
function findMarkdownHeading(anchor) {
|
|
941
|
+
const trimmedAnchor = anchor.trim();
|
|
942
|
+
if (!trimmedAnchor) {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
return document.getElementById(trimmedAnchor) ?? document.getElementById(slugifyMarkdownHeading(trimmedAnchor));
|
|
946
|
+
}
|
|
947
|
+
function scrollMarkdownHeadingIntoView(anchor) {
|
|
948
|
+
const heading = findMarkdownHeading(anchor);
|
|
949
|
+
if (!heading) {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
const scrollParents = [];
|
|
953
|
+
let current = heading.parentElement;
|
|
954
|
+
while (current) {
|
|
955
|
+
const style = window.getComputedStyle(current);
|
|
956
|
+
const overflowY = style.overflowY;
|
|
957
|
+
const isScrollable = (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay')
|
|
958
|
+
&& current.scrollHeight > current.clientHeight;
|
|
959
|
+
if (isScrollable) {
|
|
960
|
+
scrollParents.push(current);
|
|
961
|
+
}
|
|
962
|
+
current = current.parentElement;
|
|
963
|
+
}
|
|
964
|
+
heading.scrollIntoView({ block: 'start', inline: 'nearest' });
|
|
965
|
+
for (const parent of scrollParents) {
|
|
966
|
+
const parentRect = parent.getBoundingClientRect();
|
|
967
|
+
const headingRect = heading.getBoundingClientRect();
|
|
968
|
+
const nextTop = Math.max(parent.scrollTop + (headingRect.top - parentRect.top) - 24, 0);
|
|
969
|
+
parent.scrollTop = nextTop;
|
|
970
|
+
}
|
|
971
|
+
const rootScroller = document.scrollingElement;
|
|
972
|
+
if (rootScroller) {
|
|
973
|
+
const rootRectTop = heading.getBoundingClientRect().top;
|
|
974
|
+
const nextRootTop = Math.max(rootScroller.scrollTop + rootRectTop - 24, 0);
|
|
975
|
+
rootScroller.scrollTop = nextRootTop;
|
|
976
|
+
}
|
|
977
|
+
heading.setAttribute('tabindex', '-1');
|
|
978
|
+
heading.focus({ preventScroll: true });
|
|
979
|
+
if (markdownWorkspaceState) {
|
|
980
|
+
markdownWorkspaceState.activeHeadingId = heading.id || slugifyMarkdownHeading(anchor);
|
|
981
|
+
}
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
function applyPendingMarkdownAnchor() {
|
|
985
|
+
const workspaceState = markdownWorkspaceState;
|
|
986
|
+
const pendingAnchor = workspaceState?.pendingAnchor;
|
|
987
|
+
if (!workspaceState || !pendingAnchor) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
workspaceState.pendingAnchor = null;
|
|
991
|
+
if (!scrollMarkdownHeadingIntoView(pendingAnchor)) {
|
|
992
|
+
workspaceState.error = `Heading not found: ${pendingAnchor}`;
|
|
993
|
+
rerenderCurrent?.();
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async function refreshMarkdownFromDisk(payload) {
|
|
997
|
+
try {
|
|
998
|
+
const freshResult = await rpcCallTool?.('read_file', { path: payload.filePath });
|
|
999
|
+
const resultText = extractToolText(freshResult) ?? '';
|
|
1000
|
+
if (resultText.toLowerCase().includes('error') && (resultText.toLowerCase().includes('not found') || resultText.toLowerCase().includes('no such file') || resultText.toLowerCase().includes('enoent'))) {
|
|
1001
|
+
if (markdownWorkspaceState) {
|
|
1002
|
+
markdownWorkspaceState.fileDeleted = true;
|
|
1003
|
+
}
|
|
1004
|
+
updateSaveStatusDOM('File deleted', 'saved');
|
|
1005
|
+
// Disable non-applicable buttons via DOM
|
|
1006
|
+
const revert = document.getElementById('revert-markdown');
|
|
1007
|
+
if (revert)
|
|
1008
|
+
revert.disabled = true;
|
|
1009
|
+
const openFolder = document.getElementById('open-in-folder');
|
|
1010
|
+
if (openFolder)
|
|
1011
|
+
openFolder.disabled = true;
|
|
1012
|
+
const openEditor = document.getElementById('open-in-editor');
|
|
1013
|
+
if (openEditor)
|
|
1014
|
+
openEditor.disabled = true;
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const freshPayload = extractRenderPayload(freshResult) ?? null;
|
|
1018
|
+
if (!freshPayload)
|
|
1019
|
+
return;
|
|
1020
|
+
const completeFreshPayload = await ensureCompleteMarkdownPayload(freshPayload);
|
|
1021
|
+
const freshContent = getMarkdownPayloadContent(completeFreshPayload);
|
|
1022
|
+
const currentContent = getMarkdownPayloadContent(payload);
|
|
1023
|
+
if (freshContent === currentContent)
|
|
1024
|
+
return;
|
|
1025
|
+
if (markdownWorkspaceState?.filePath === payload.filePath
|
|
1026
|
+
&& (markdownWorkspaceState.dirty || markdownWorkspaceState.saving)) {
|
|
1027
|
+
markdownWorkspaceState.pendingExternalPayload = completeFreshPayload;
|
|
1028
|
+
markdownWorkspaceState.notice = 'A newer version is available on disk. Discard local edits to reload it.';
|
|
1029
|
+
rerenderCurrent?.();
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
storePayloadOverride(completeFreshPayload);
|
|
1033
|
+
markdownWorkspaceState = undefined;
|
|
1034
|
+
rerenderCurrent?.();
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
// Silently fall back to host payload
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async function readMarkdownPayload(filePath, length) {
|
|
1041
|
+
const result = await rpcCallTool?.('read_file', {
|
|
1042
|
+
path: filePath,
|
|
1043
|
+
...(typeof length === 'number' ? { offset: 0, length } : {}),
|
|
1044
|
+
});
|
|
1045
|
+
return extractRenderPayload(result) ?? null;
|
|
1046
|
+
}
|
|
1047
|
+
async function loadFullMarkdownDocument(payload, options = {}) {
|
|
1048
|
+
const workspaceState = getMarkdownWorkspaceState(payload);
|
|
1049
|
+
const range = parseReadRange(payload.content);
|
|
1050
|
+
if (!range?.isPartial) {
|
|
1051
|
+
if (options.keepEditMode) {
|
|
1052
|
+
workspaceState.mode = 'edit';
|
|
1053
|
+
workspaceState.editorView = 'markdown';
|
|
1054
|
+
workspaceState.notice = null;
|
|
1055
|
+
workspaceState.error = null;
|
|
1056
|
+
workspaceState.draftContent = workspaceState.sourceContent;
|
|
1057
|
+
workspaceState.dirty = false;
|
|
1058
|
+
rerenderCurrent?.();
|
|
1059
|
+
}
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
workspaceState.loadingDocument = true;
|
|
1063
|
+
workspaceState.notice = 'Loading full document…';
|
|
1064
|
+
workspaceState.error = null;
|
|
1065
|
+
rerenderCurrent?.();
|
|
1066
|
+
try {
|
|
1067
|
+
const nextPayload = await readMarkdownPayload(payload.filePath, range.totalLines);
|
|
1068
|
+
if (!nextPayload) {
|
|
1069
|
+
workspaceState.error = 'Failed to load the full document.';
|
|
1070
|
+
workspaceState.notice = null;
|
|
1071
|
+
workspaceState.loadingDocument = false;
|
|
1072
|
+
rerenderCurrent?.();
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
syncPayload?.(nextPayload);
|
|
1076
|
+
const nextState = getMarkdownWorkspaceState(nextPayload);
|
|
1077
|
+
nextState.loadingDocument = false;
|
|
1078
|
+
nextState.notice = null;
|
|
1079
|
+
nextState.error = null;
|
|
1080
|
+
if (options.keepEditMode) {
|
|
1081
|
+
nextState.mode = 'edit';
|
|
1082
|
+
nextState.editorView = 'markdown';
|
|
1083
|
+
nextState.draftContent = nextState.sourceContent;
|
|
1084
|
+
nextState.dirty = false;
|
|
1085
|
+
rerenderCurrent?.();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
workspaceState.loadingDocument = false;
|
|
1090
|
+
workspaceState.notice = null;
|
|
1091
|
+
workspaceState.error = 'Failed to load the full document.';
|
|
1092
|
+
rerenderCurrent?.();
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
async function navigateMarkdownLink(payload, href) {
|
|
1096
|
+
const workspaceState = getMarkdownWorkspaceState(payload);
|
|
1097
|
+
if (workspaceState.mode === 'edit' && workspaceState.dirty) {
|
|
1098
|
+
const shouldDiscard = window.confirm('Discard unsaved changes and follow this link?');
|
|
1099
|
+
if (!shouldDiscard) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const resolvedLink = resolveMarkdownLink(payload.filePath, href);
|
|
1104
|
+
workspaceState.notice = null;
|
|
1105
|
+
workspaceState.error = null;
|
|
1106
|
+
if (resolvedLink.kind === 'external' && resolvedLink.url) {
|
|
1107
|
+
const opened = await openExternalLink?.(resolvedLink.url);
|
|
1108
|
+
if (!opened && markdownWorkspaceState) {
|
|
1109
|
+
markdownWorkspaceState.error = 'The host blocked that external link.';
|
|
1110
|
+
rerenderCurrent?.();
|
|
1111
|
+
}
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (resolvedLink.kind === 'anchor' && resolvedLink.anchor) {
|
|
1115
|
+
if (!scrollMarkdownHeadingIntoView(resolvedLink.anchor) && markdownWorkspaceState) {
|
|
1116
|
+
markdownWorkspaceState.error = `Heading not found: ${resolvedLink.anchor}`;
|
|
1117
|
+
rerenderCurrent?.();
|
|
1118
|
+
}
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (resolvedLink.kind === 'file' && resolvedLink.targetPath) {
|
|
1122
|
+
// Delegate file navigation to the host so it can open the file in its own
|
|
1123
|
+
// viewer (e.g. dc-app file preview modal with back/forward navigation).
|
|
1124
|
+
// Fall back to in-app reading if the host doesn't handle the link.
|
|
1125
|
+
const hostHandled = await openExternalLink?.(resolvedLink.targetPath);
|
|
1126
|
+
if (hostHandled) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const nextPayload = await readMarkdownPayload(resolvedLink.targetPath);
|
|
1130
|
+
if (!nextPayload) {
|
|
1131
|
+
if (markdownWorkspaceState) {
|
|
1132
|
+
markdownWorkspaceState.error = `Unable to open ${resolvedLink.targetPath}.`;
|
|
1133
|
+
rerenderCurrent?.();
|
|
1134
|
+
}
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
syncPayload?.(nextPayload);
|
|
1138
|
+
const nextState = getMarkdownWorkspaceState(nextPayload);
|
|
1139
|
+
nextState.pendingAnchor = resolvedLink.anchor ?? null;
|
|
1140
|
+
nextState.error = null;
|
|
1141
|
+
nextState.notice = null;
|
|
1142
|
+
rerenderCurrent?.();
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async function requestMarkdownEditMode(payload) {
|
|
1146
|
+
const workspaceState = getMarkdownWorkspaceState(payload);
|
|
1147
|
+
workspaceState.error = null;
|
|
1148
|
+
workspaceState.notice = null;
|
|
1149
|
+
if (shouldAutoLoadMarkdownOnEnterFullscreen(payload.content)) {
|
|
1150
|
+
await loadFullMarkdownDocument(payload, { keepEditMode: true });
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const editAvailability = getMarkdownEditAvailability({
|
|
1154
|
+
content: payload.content,
|
|
1155
|
+
});
|
|
1156
|
+
if (!editAvailability.canEdit) {
|
|
1157
|
+
workspaceState.error = editAvailability.reason;
|
|
1158
|
+
rerenderCurrent?.();
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
workspaceState.mode = 'edit';
|
|
1162
|
+
workspaceState.draftContent = workspaceState.fullDocumentContent;
|
|
1163
|
+
workspaceState.dirty = false;
|
|
1164
|
+
workspaceState.editorView = 'markdown';
|
|
1165
|
+
isExpanded = true;
|
|
1166
|
+
rerenderCurrent?.();
|
|
1167
|
+
}
|
|
1168
|
+
async function requestMarkdownFullscreen() {
|
|
1169
|
+
const fullscreenAvailability = getMarkdownFullscreenAvailability({
|
|
1170
|
+
availableDisplayModes: getAvailableDisplayModes(),
|
|
1171
|
+
});
|
|
1172
|
+
if (!fullscreenAvailability.canFullscreen) {
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
const nextMode = await requestDisplayMode?.('fullscreen');
|
|
1176
|
+
return nextMode === 'fullscreen';
|
|
1177
|
+
}
|
|
1178
|
+
function revertMarkdownEditing() {
|
|
1179
|
+
const ws = markdownWorkspaceState;
|
|
1180
|
+
if (!ws)
|
|
1181
|
+
return;
|
|
1182
|
+
const pendingExternalPayload = ws.pendingExternalPayload;
|
|
1183
|
+
if (pendingExternalPayload) {
|
|
1184
|
+
const freshContent = stripReadStatusLine(pendingExternalPayload.content);
|
|
1185
|
+
const outline = extractMarkdownOutline(freshContent);
|
|
1186
|
+
ws.initialContent = freshContent;
|
|
1187
|
+
ws.sourceContent = freshContent;
|
|
1188
|
+
ws.fullDocumentContent = freshContent;
|
|
1189
|
+
ws.draftContent = freshContent;
|
|
1190
|
+
ws.pendingExternalPayload = null;
|
|
1191
|
+
ws.dirty = false;
|
|
1192
|
+
ws.activeHeadingId = outline[0]?.id ?? null;
|
|
1193
|
+
ws.pendingAnchor = null;
|
|
1194
|
+
ws.error = null;
|
|
1195
|
+
ws.notice = null;
|
|
1196
|
+
ws.saving = false;
|
|
1197
|
+
ws.loadingDocument = false;
|
|
1198
|
+
ws.saveIndicator = 'idle';
|
|
1199
|
+
ws.fileDeleted = false;
|
|
1200
|
+
storePayloadOverride(pendingExternalPayload);
|
|
1201
|
+
rerenderCurrent?.();
|
|
1202
|
+
updateSaveStatusDOM('Reloaded from disk', 'saved');
|
|
1203
|
+
window.setTimeout(() => updateSaveStatusDOM('', ''), 1500);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
ws.draftContent = ws.initialContent;
|
|
1207
|
+
ws.dirty = ws.draftContent !== ws.fullDocumentContent;
|
|
1208
|
+
ws.error = null;
|
|
1209
|
+
ws.notice = null;
|
|
1210
|
+
rerenderCurrent?.();
|
|
1211
|
+
updateSaveStatusDOM('Reverted', 'saved');
|
|
1212
|
+
window.setTimeout(() => updateSaveStatusDOM('', ''), 1500);
|
|
1213
|
+
}
|
|
1214
|
+
function computeDiffHunks(oldLines, newLines) {
|
|
1215
|
+
const m = oldLines.length;
|
|
1216
|
+
const n = newLines.length;
|
|
1217
|
+
// For very large files, treat as single change
|
|
1218
|
+
if (m * n > 1000000) {
|
|
1219
|
+
return [{ oldStart: 0, oldEnd: m, newStart: 0, newEnd: n }];
|
|
1220
|
+
}
|
|
1221
|
+
// LCS via DP
|
|
1222
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
1223
|
+
for (let i = 1; i <= m; i++) {
|
|
1224
|
+
for (let j = 1; j <= n; j++) {
|
|
1225
|
+
dp[i][j] = oldLines[i - 1] === newLines[j - 1]
|
|
1226
|
+
? dp[i - 1][j - 1] + 1
|
|
1227
|
+
: Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
// Trace back to find matching line pairs
|
|
1231
|
+
const matches = [];
|
|
1232
|
+
let i = m;
|
|
1233
|
+
let j = n;
|
|
1234
|
+
while (i > 0 && j > 0) {
|
|
1235
|
+
if (oldLines[i - 1] === newLines[j - 1]) {
|
|
1236
|
+
matches.unshift([i - 1, j - 1]);
|
|
1237
|
+
i--;
|
|
1238
|
+
j--;
|
|
1239
|
+
}
|
|
1240
|
+
else if (dp[i - 1][j] >= dp[i][j - 1]) {
|
|
1241
|
+
i--;
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
j--;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
// Gaps between matches are change hunks
|
|
1248
|
+
const hunks = [];
|
|
1249
|
+
let prevOld = 0;
|
|
1250
|
+
let prevNew = 0;
|
|
1251
|
+
for (const [oi, ni] of matches) {
|
|
1252
|
+
if (oi > prevOld || ni > prevNew) {
|
|
1253
|
+
hunks.push({ oldStart: prevOld, oldEnd: oi, newStart: prevNew, newEnd: ni });
|
|
1254
|
+
}
|
|
1255
|
+
prevOld = oi + 1;
|
|
1256
|
+
prevNew = ni + 1;
|
|
1257
|
+
}
|
|
1258
|
+
if (prevOld < m || prevNew < n) {
|
|
1259
|
+
hunks.push({ oldStart: prevOld, oldEnd: m, newStart: prevNew, newEnd: n });
|
|
1260
|
+
}
|
|
1261
|
+
return hunks;
|
|
1262
|
+
}
|
|
1263
|
+
function mergeCloseHunks(hunks, minGap) {
|
|
1264
|
+
if (hunks.length <= 1)
|
|
1265
|
+
return hunks;
|
|
1266
|
+
const merged = [{ ...hunks[0] }];
|
|
1267
|
+
for (let i = 1; i < hunks.length; i++) {
|
|
1268
|
+
const prev = merged[merged.length - 1];
|
|
1269
|
+
const curr = hunks[i];
|
|
1270
|
+
if (curr.oldStart - prev.oldEnd < minGap) {
|
|
1271
|
+
prev.oldEnd = curr.oldEnd;
|
|
1272
|
+
prev.newEnd = curr.newEnd;
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
merged.push({ ...curr });
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return merged;
|
|
1279
|
+
}
|
|
1280
|
+
function computeEditBlocks(oldText, newText) {
|
|
1281
|
+
if (oldText === newText)
|
|
1282
|
+
return [];
|
|
1283
|
+
const oldLines = oldText.split('\n');
|
|
1284
|
+
const newLines = newText.split('\n');
|
|
1285
|
+
const hunks = computeDiffHunks(oldLines, newLines);
|
|
1286
|
+
if (hunks.length === 0)
|
|
1287
|
+
return [];
|
|
1288
|
+
const CONTEXT = 3;
|
|
1289
|
+
const merged = mergeCloseHunks(hunks, CONTEXT * 2 + 1);
|
|
1290
|
+
// If changes cover most of the file, single full edit
|
|
1291
|
+
const totalChanged = merged.reduce((sum, h) => sum + (h.oldEnd - h.oldStart), 0);
|
|
1292
|
+
if (totalChanged > oldLines.length * 0.7) {
|
|
1293
|
+
return [{ old_string: oldText, new_string: newText }];
|
|
1294
|
+
}
|
|
1295
|
+
return merged.map((hunk) => {
|
|
1296
|
+
const ctxBefore = Math.max(0, hunk.oldStart - CONTEXT);
|
|
1297
|
+
const ctxAfter = Math.min(oldLines.length, hunk.oldEnd + CONTEXT);
|
|
1298
|
+
const oldBlock = oldLines.slice(ctxBefore, ctxAfter).join('\n');
|
|
1299
|
+
const newBlock = [
|
|
1300
|
+
...oldLines.slice(ctxBefore, hunk.oldStart),
|
|
1301
|
+
...newLines.slice(hunk.newStart, hunk.newEnd),
|
|
1302
|
+
...oldLines.slice(hunk.oldEnd, ctxAfter),
|
|
1303
|
+
].join('\n');
|
|
1304
|
+
return { old_string: oldBlock, new_string: newBlock };
|
|
1305
|
+
}).filter((block) => block.old_string !== block.new_string);
|
|
1306
|
+
}
|
|
1307
|
+
function updateSaveStatusDOM(label, statusClass) {
|
|
1308
|
+
const existing = document.querySelector('.panel-save-status');
|
|
1309
|
+
if (label) {
|
|
1310
|
+
if (existing) {
|
|
1311
|
+
existing.textContent = label;
|
|
1312
|
+
existing.className = `panel-save-status panel-save-status--${statusClass}`;
|
|
1313
|
+
}
|
|
1314
|
+
else {
|
|
1315
|
+
const actions = document.querySelector('.panel-topbar-actions');
|
|
1316
|
+
if (actions) {
|
|
1317
|
+
const span = document.createElement('span');
|
|
1318
|
+
span.className = `panel-save-status panel-save-status--${statusClass}`;
|
|
1319
|
+
span.textContent = label;
|
|
1320
|
+
actions.prepend(span);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
else if (existing) {
|
|
1325
|
+
existing.remove();
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
async function saveMarkdownDocument() {
|
|
1329
|
+
const ws = markdownWorkspaceState;
|
|
1330
|
+
if (!ws || ws.saving || !ws.dirty || ws.fileDeleted) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
ws.saving = true;
|
|
1334
|
+
ws.saveIndicator = 'saving';
|
|
1335
|
+
ws.error = null;
|
|
1336
|
+
ws.notice = null;
|
|
1337
|
+
try {
|
|
1338
|
+
const blocks = computeEditBlocks(ws.fullDocumentContent, ws.draftContent);
|
|
1339
|
+
if (blocks.length === 0) {
|
|
1340
|
+
ws.saving = false;
|
|
1341
|
+
ws.saveIndicator = 'idle';
|
|
1342
|
+
ws.dirty = false;
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
for (const block of blocks) {
|
|
1346
|
+
const editResult = await rpcCallTool?.('edit_block', {
|
|
1347
|
+
file_path: ws.filePath,
|
|
1348
|
+
old_string: block.old_string,
|
|
1349
|
+
new_string: block.new_string,
|
|
1350
|
+
expected_replacements: 1,
|
|
1351
|
+
});
|
|
1352
|
+
assertSuccessfulEditBlockResult(editResult);
|
|
1353
|
+
}
|
|
1354
|
+
ws.fullDocumentContent = ws.draftContent;
|
|
1355
|
+
ws.sourceContent = ws.draftContent;
|
|
1356
|
+
ws.pendingExternalPayload = null;
|
|
1357
|
+
ws.dirty = false;
|
|
1358
|
+
ws.saving = false;
|
|
1359
|
+
ws.saveIndicator = 'saved';
|
|
1360
|
+
// Update payloads so re-renders and refreshes use saved content (no re-render here)
|
|
1361
|
+
if (currentPayload) {
|
|
1362
|
+
const savedPayload = { ...currentPayload, content: ws.draftContent };
|
|
1363
|
+
storePayloadOverride(savedPayload);
|
|
1364
|
+
}
|
|
1365
|
+
updateSaveStatusDOM('Saved', 'saved');
|
|
1366
|
+
const revert = document.getElementById('revert-markdown');
|
|
1367
|
+
if (revert)
|
|
1368
|
+
revert.disabled = !isMarkdownUndoAvailable(ws);
|
|
1369
|
+
window.setTimeout(() => {
|
|
1370
|
+
if (markdownWorkspaceState?.filePath === ws.filePath && !markdownWorkspaceState.dirty && !markdownWorkspaceState.saving) {
|
|
1371
|
+
markdownWorkspaceState.saveIndicator = 'idle';
|
|
1372
|
+
updateSaveStatusDOM('', '');
|
|
1373
|
+
}
|
|
1374
|
+
}, 1800);
|
|
1375
|
+
}
|
|
1376
|
+
catch (error) {
|
|
1377
|
+
ws.saving = false;
|
|
1378
|
+
ws.saveIndicator = 'idle';
|
|
1379
|
+
const freshPayload = await readCompleteMarkdownPayload(ws.filePath).catch(() => null);
|
|
1380
|
+
let hasExternalChange = ws.pendingExternalPayload !== null;
|
|
1381
|
+
if (freshPayload) {
|
|
1382
|
+
const freshContent = getMarkdownPayloadContent(freshPayload);
|
|
1383
|
+
if (freshContent !== ws.fullDocumentContent) {
|
|
1384
|
+
ws.pendingExternalPayload = freshPayload;
|
|
1385
|
+
hasExternalChange = true;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
ws.notice = null;
|
|
1389
|
+
ws.error = hasExternalChange
|
|
1390
|
+
? 'Save failed because the file changed on disk. Local edits were kept. Discard local edits to reload the latest version.'
|
|
1391
|
+
: error instanceof Error ? error.message : 'Save failed.';
|
|
1392
|
+
rerenderCurrent?.();
|
|
1393
|
+
updateSaveStatusDOM('Save failed', 'saving');
|
|
1394
|
+
window.setTimeout(() => updateSaveStatusDOM('', ''), 3000);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function maybeAutosaveMarkdownDocument() {
|
|
1398
|
+
if (!markdownWorkspaceState?.dirty || markdownWorkspaceState.saving) {
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
void saveMarkdownDocument();
|
|
1402
|
+
}
|
|
1403
|
+
function attachMarkdownWorkspaceHandlers(payload) {
|
|
1404
|
+
if (payload.fileType !== 'markdown') {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const workspaceState = getMarkdownWorkspaceState(payload);
|
|
1408
|
+
const wrapper = document.querySelector('.panel-content-wrapper');
|
|
1409
|
+
const outline = extractMarkdownOutline(workspaceState.sourceContent);
|
|
1410
|
+
if (workspaceState.mode === 'edit') {
|
|
1411
|
+
const editorRoot = document.getElementById('markdown-editor-root');
|
|
1412
|
+
if (editorRoot) {
|
|
1413
|
+
markdownEditorHandle = mountMarkdownEditor({
|
|
1414
|
+
target: editorRoot,
|
|
1415
|
+
value: workspaceState.draftContent,
|
|
1416
|
+
view: workspaceState.editorView,
|
|
1417
|
+
initialScrollTop: workspaceState.editorScrollTop,
|
|
1418
|
+
currentFilePath: payload.filePath,
|
|
1419
|
+
searchLinks: (query) => searchMarkdownLinkTargets(payload.filePath, query),
|
|
1420
|
+
loadHeadings: (targetPath) => loadMarkdownLinkHeadings(payload.filePath, targetPath),
|
|
1421
|
+
onChange: (value) => {
|
|
1422
|
+
workspaceState.draftContent = value;
|
|
1423
|
+
workspaceState.dirty = value !== workspaceState.fullDocumentContent;
|
|
1424
|
+
if (workspaceState.dirty && workspaceState.saveIndicator === 'saved') {
|
|
1425
|
+
workspaceState.saveIndicator = 'idle';
|
|
1426
|
+
}
|
|
1427
|
+
const revert = document.getElementById('revert-markdown');
|
|
1428
|
+
if (revert) {
|
|
1429
|
+
revert.disabled = !isMarkdownUndoAvailable(workspaceState);
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
onBlur: () => {
|
|
1433
|
+
maybeAutosaveMarkdownDocument();
|
|
1434
|
+
},
|
|
1435
|
+
});
|
|
1436
|
+
markdownEditorHandle.focus();
|
|
1437
|
+
}
|
|
1438
|
+
const revertButton = document.getElementById('revert-markdown');
|
|
1439
|
+
revertButton?.addEventListener('click', () => {
|
|
1440
|
+
revertMarkdownEditing();
|
|
1441
|
+
});
|
|
1442
|
+
const expandButton = document.getElementById('expand-fullscreen');
|
|
1443
|
+
expandButton?.addEventListener('click', () => {
|
|
1444
|
+
void requestMarkdownFullscreen();
|
|
1445
|
+
});
|
|
1446
|
+
const rawModeButton = document.getElementById('markdown-mode-raw');
|
|
1447
|
+
rawModeButton?.addEventListener('click', () => {
|
|
1448
|
+
setMarkdownEditorView(payload, 'raw');
|
|
1449
|
+
});
|
|
1450
|
+
const previewModeButton = document.getElementById('markdown-mode-markdown');
|
|
1451
|
+
previewModeButton?.addEventListener('click', () => {
|
|
1452
|
+
setMarkdownEditorView(payload, 'markdown');
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
if (wrapper) {
|
|
1456
|
+
wrapper.addEventListener('click', (event) => {
|
|
1457
|
+
const target = event.target;
|
|
1458
|
+
const link = target?.closest('a[href]');
|
|
1459
|
+
if (!link || !link.closest('.markdown-doc')) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const href = link.getAttribute('href');
|
|
1463
|
+
if (!href) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
event.preventDefault();
|
|
1467
|
+
void navigateMarkdownLink(payload, href);
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
const tocShell = document.querySelector('.markdown-toc-shell');
|
|
1471
|
+
if (tocShell && wrapper) {
|
|
1472
|
+
markdownTocHandle = attachMarkdownToc({
|
|
1473
|
+
shell: tocShell,
|
|
1474
|
+
outline,
|
|
1475
|
+
scrollContainer: wrapper,
|
|
1476
|
+
onSelect: (headingId) => {
|
|
1477
|
+
const selectedHeading = outline.find((item) => item.id === headingId);
|
|
1478
|
+
if (workspaceState.mode === 'edit') {
|
|
1479
|
+
if (selectedHeading) {
|
|
1480
|
+
markdownEditorHandle?.revealLine(selectedHeading.line, selectedHeading.id);
|
|
1481
|
+
workspaceState.activeHeadingId = selectedHeading.id;
|
|
1482
|
+
}
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
scrollMarkdownHeadingIntoView(headingId);
|
|
1486
|
+
},
|
|
1487
|
+
}) ?? undefined;
|
|
1488
|
+
}
|
|
1489
|
+
window.setTimeout(() => {
|
|
1490
|
+
applyPendingMarkdownAnchor();
|
|
1491
|
+
}, 0);
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Tracks native text selection and pushes it to the host via ui/update-model-context.
|
|
1495
|
+
*
|
|
1496
|
+
* How it works:
|
|
1497
|
+
* 1. User drags to select text anywhere in the preview (markdown, code, HTML).
|
|
1498
|
+
* 2. The selectionchange event fires; we extract the selected string.
|
|
1499
|
+
* 3. We call rpcUpdateContext() which sends a ui/update-model-context JSON-RPC
|
|
1500
|
+
* request to the host with the selected text + file path (+ line numbers for code).
|
|
1501
|
+
* 4. The host stores this as widget context.
|
|
1502
|
+
* 5. The LLM can access it by calling read_widget_context(tool_name="desktop-commander:read_file").
|
|
1503
|
+
*
|
|
1504
|
+
* Note: as of Feb 2025, Claude does NOT auto-inject ui/update-model-context into
|
|
1505
|
+
* the LLM's context window. The LLM must actively call read_widget_context to see
|
|
1506
|
+
* the selection. A floating tooltip near the selection tells the user this is working.
|
|
1507
|
+
*/
|
|
1508
|
+
let selectionAbortController = null;
|
|
1509
|
+
function attachTextSelectionHandler(payload) {
|
|
1510
|
+
if (payload.fileType === 'markdown' && getMarkdownWorkspaceState(payload).mode === 'edit') {
|
|
1511
|
+
if (selectionAbortController) {
|
|
1512
|
+
selectionAbortController.abort();
|
|
1513
|
+
selectionAbortController = null;
|
|
1514
|
+
}
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const contentWrapper = document.querySelector('.panel-content-wrapper');
|
|
1518
|
+
if (!contentWrapper)
|
|
1519
|
+
return;
|
|
1520
|
+
// Abort any previous selectionchange listener to avoid leaking listeners/closures
|
|
1521
|
+
if (selectionAbortController) {
|
|
1522
|
+
selectionAbortController.abort();
|
|
1523
|
+
selectionAbortController = null;
|
|
1524
|
+
}
|
|
1525
|
+
selectionAbortController = new AbortController();
|
|
1526
|
+
let hintEl = null;
|
|
1527
|
+
let lastSelectedText = '';
|
|
1528
|
+
let hideTimer = null;
|
|
1529
|
+
function positionHint(selection) {
|
|
1530
|
+
if (!hintEl)
|
|
1531
|
+
return;
|
|
1532
|
+
const range = selection.getRangeAt(0);
|
|
1533
|
+
const rect = range.getBoundingClientRect();
|
|
1534
|
+
const wrapperRect = contentWrapper.getBoundingClientRect();
|
|
1535
|
+
// Position above the selection, centered horizontally
|
|
1536
|
+
let left = rect.left + rect.width / 2 - wrapperRect.left;
|
|
1537
|
+
let top = rect.top - wrapperRect.top + contentWrapper.scrollTop - 32;
|
|
1538
|
+
// Clamp within wrapper bounds
|
|
1539
|
+
const hintWidth = hintEl.offsetWidth || 200;
|
|
1540
|
+
left = Math.max(8, Math.min(left - hintWidth / 2, contentWrapper.clientWidth - hintWidth - 8));
|
|
1541
|
+
top = Math.max(4, top);
|
|
1542
|
+
hintEl.style.left = `${left}px`;
|
|
1543
|
+
hintEl.style.top = `${top}px`;
|
|
1544
|
+
}
|
|
1545
|
+
function showHint(selection) {
|
|
1546
|
+
if (hideTimer) {
|
|
1547
|
+
clearTimeout(hideTimer);
|
|
1548
|
+
hideTimer = null;
|
|
1549
|
+
}
|
|
1550
|
+
if (!hintEl) {
|
|
1551
|
+
hintEl = document.createElement('div');
|
|
1552
|
+
hintEl.className = 'selection-hint';
|
|
1553
|
+
hintEl.textContent = 'AI can see your selection';
|
|
1554
|
+
contentWrapper.appendChild(hintEl);
|
|
1555
|
+
}
|
|
1556
|
+
hintEl.classList.add('visible');
|
|
1557
|
+
positionHint(selection);
|
|
1558
|
+
}
|
|
1559
|
+
function hideHint() {
|
|
1560
|
+
if (!hintEl)
|
|
1561
|
+
return;
|
|
1562
|
+
hintEl.classList.remove('visible');
|
|
1563
|
+
hideTimer = setTimeout(() => { hintEl?.remove(); hintEl = null; }, 200);
|
|
1564
|
+
}
|
|
1565
|
+
function getLineInfo(selection) {
|
|
1566
|
+
const anchorRow = selection.anchorNode?.parentElement?.closest('.code-line');
|
|
1567
|
+
const focusRow = selection.focusNode?.parentElement?.closest('.code-line');
|
|
1568
|
+
if (anchorRow && focusRow) {
|
|
1569
|
+
const a = parseInt(anchorRow.dataset.line ?? '', 10);
|
|
1570
|
+
const f = parseInt(focusRow.dataset.line ?? '', 10);
|
|
1571
|
+
if (!isNaN(a) && !isNaN(f)) {
|
|
1572
|
+
const low = Math.min(a, f);
|
|
1573
|
+
const high = Math.max(a, f);
|
|
1574
|
+
return low === high ? `line ${low}` : `lines ${low}–${high}`;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
return '';
|
|
1578
|
+
}
|
|
1579
|
+
document.addEventListener('selectionchange', () => {
|
|
1580
|
+
const selection = document.getSelection();
|
|
1581
|
+
if (!selection || selection.isCollapsed) {
|
|
1582
|
+
if (lastSelectedText) {
|
|
1583
|
+
lastSelectedText = '';
|
|
1584
|
+
rpcUpdateContext?.('');
|
|
1585
|
+
hideHint();
|
|
1586
|
+
}
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
const text = selection.toString().trim();
|
|
1590
|
+
if (!text || text === lastSelectedText)
|
|
1591
|
+
return;
|
|
1592
|
+
// Only act on selections within our content area
|
|
1593
|
+
const anchorInContent = contentWrapper.contains(selection.anchorNode);
|
|
1594
|
+
const focusInContent = contentWrapper.contains(selection.focusNode);
|
|
1595
|
+
if (!anchorInContent && !focusInContent) {
|
|
1596
|
+
if (lastSelectedText) {
|
|
1597
|
+
lastSelectedText = '';
|
|
1598
|
+
rpcUpdateContext?.('');
|
|
1599
|
+
hideHint();
|
|
1600
|
+
}
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
lastSelectedText = text;
|
|
1604
|
+
const lineInfo = getLineInfo(selection);
|
|
1605
|
+
const locationPart = lineInfo ? ` (${lineInfo})` : '';
|
|
1606
|
+
const context = `User selected text from file ${payload.filePath}${locationPart}:\n\`\`\`\n${text}\n\`\`\``;
|
|
1607
|
+
rpcUpdateContext?.(context);
|
|
1608
|
+
showHint(selection);
|
|
1609
|
+
trackUiEvent?.('text_selected', {
|
|
1610
|
+
file_type: payload.fileType,
|
|
1611
|
+
file_extension: getFileExtensionForAnalytics(payload.filePath),
|
|
1612
|
+
char_count: text.length
|
|
1613
|
+
});
|
|
1614
|
+
}, { signal: selectionAbortController.signal });
|
|
1615
|
+
}
|
|
1616
|
+
function renderStatusState(container, message) {
|
|
1617
|
+
container.innerHTML = `
|
|
1618
|
+
<main class="shell">
|
|
1619
|
+
${renderCompactRow({ label: message, variant: 'status', interactive: false })}
|
|
1620
|
+
</main>
|
|
1621
|
+
`;
|
|
1622
|
+
document.body.classList.add('dc-ready');
|
|
1623
|
+
}
|
|
1624
|
+
function renderLoadingState(container) {
|
|
1625
|
+
container.innerHTML = `
|
|
1626
|
+
<main class="shell">
|
|
1627
|
+
${renderCompactRow({ label: 'Preparing preview…', variant: 'loading', interactive: false })}
|
|
1628
|
+
</main>
|
|
1629
|
+
`;
|
|
1630
|
+
document.body.classList.add('dc-ready');
|
|
1631
|
+
}
|
|
1632
|
+
export function renderApp(container, payload, htmlMode = 'rendered', expandedState = false) {
|
|
1633
|
+
isExpanded = expandedState;
|
|
1634
|
+
currentHtmlMode = htmlMode;
|
|
1635
|
+
shellController?.dispose();
|
|
1636
|
+
shellController = undefined;
|
|
1637
|
+
disposeMarkdownWorkspaceHandles();
|
|
1638
|
+
if (!payload) {
|
|
1639
|
+
currentPayload = undefined;
|
|
1640
|
+
renderStatusState(container, 'No preview available for this response.');
|
|
1641
|
+
onRender?.();
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
updateCurrentPayload(payload);
|
|
1645
|
+
if (payload.fileType !== 'markdown') {
|
|
1646
|
+
markdownWorkspaceState = undefined;
|
|
1647
|
+
}
|
|
1648
|
+
const markdownWorkspace = payload.fileType === 'markdown' ? getMarkdownWorkspaceState(payload) : undefined;
|
|
1649
|
+
const canCopy = payload.fileType !== 'unsupported' && payload.fileType !== 'image';
|
|
1650
|
+
const canOpenInFolder = !isLikelyUrl(payload.filePath);
|
|
1651
|
+
const fileExtension = getFileExtensionForAnalytics(payload.filePath);
|
|
1652
|
+
const supportsPreview = payload.fileType !== 'unsupported';
|
|
1653
|
+
// In DC app (hideSummaryRow), no reason to auto-expand when there's nothing to preview —
|
|
1654
|
+
// the host header already shows the file name and path.
|
|
1655
|
+
if (!supportsPreview && hideSummaryRow) {
|
|
1656
|
+
isExpanded = false;
|
|
1657
|
+
}
|
|
1658
|
+
const range = parseReadRange(payload.content);
|
|
1659
|
+
const body = renderBody(payload, htmlMode, range?.fromLine ?? 1);
|
|
1660
|
+
const notice = body.notice ? `<div class="notice">${body.notice}</div>` : '';
|
|
1661
|
+
const breadcrumb = buildBreadcrumb(payload.filePath);
|
|
1662
|
+
const lineCount = range ? range.toLine - range.fromLine + 1 : countContentLines(payload.content);
|
|
1663
|
+
const fileTypeLabel = payload.fileType === 'markdown' ? 'MARKDOWN'
|
|
1664
|
+
: payload.fileType === 'html' ? 'HTML'
|
|
1665
|
+
: payload.fileType === 'image' ? 'IMAGE'
|
|
1666
|
+
: payload.fileType === 'directory' ? 'DIRECTORY'
|
|
1667
|
+
: fileExtension !== 'none' ? fileExtension.toUpperCase()
|
|
1668
|
+
: 'TEXT';
|
|
1669
|
+
const compactLabel = range?.isPartial
|
|
1670
|
+
? `View lines ${range.fromLine}–${range.toLine}`
|
|
1671
|
+
: payload.fileType === 'directory' ? 'View directory'
|
|
1672
|
+
: 'View file';
|
|
1673
|
+
let footerLabel = range?.isPartial
|
|
1674
|
+
? `${escapeHtml(fileTypeLabel)} • LINES ${range.fromLine}–${range.toLine} OF ${range.totalLines}`
|
|
1675
|
+
: `${escapeHtml(fileTypeLabel)} • ${lineCount} LINE${lineCount !== 1 ? 'S' : ''}`;
|
|
1676
|
+
const markdownWordCount = payload.fileType === 'markdown'
|
|
1677
|
+
? (stripReadStatusLine(markdownWorkspace?.mode === 'edit' ? markdownWorkspace.draftContent : payload.content).trim().split(/\s+/).filter(Boolean).length)
|
|
1678
|
+
: 0;
|
|
1679
|
+
const markdownLineCount = payload.fileType === 'markdown'
|
|
1680
|
+
? countContentLines(stripReadStatusLine(markdownWorkspace?.mode === 'edit' ? markdownWorkspace.draftContent : payload.content))
|
|
1681
|
+
: lineCount;
|
|
1682
|
+
if (markdownWorkspace?.mode === 'edit') {
|
|
1683
|
+
footerLabel = `${escapeHtml(fileTypeLabel)} • EDIT MODE • ${markdownLineCount} LINES • ${markdownWordCount} WORDS`;
|
|
1684
|
+
}
|
|
1685
|
+
const htmlToggle = payload.fileType === 'html'
|
|
1686
|
+
? `<button class="panel-action" id="toggle-html-mode">${htmlMode === 'rendered' ? 'Source' : 'Rendered'}</button>`
|
|
1687
|
+
: '';
|
|
1688
|
+
const copyIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
|
|
1689
|
+
const folderIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
|
|
1690
|
+
const undoIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h11a5 5 0 1 1 0 10h-1"/></svg>`;
|
|
1691
|
+
const expandIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`;
|
|
1692
|
+
const isFullscreen = getCurrentDisplayMode() === 'fullscreen';
|
|
1693
|
+
const canGoFullscreen = !isFullscreen && getMarkdownFullscreenAvailability({ availableDisplayModes: getAvailableDisplayModes() }).canFullscreen;
|
|
1694
|
+
let markdownActions = '';
|
|
1695
|
+
if (payload.fileType === 'markdown' && markdownWorkspace) {
|
|
1696
|
+
const saveStatusLabel = markdownWorkspace.saveIndicator === 'saved'
|
|
1697
|
+
? 'Saved'
|
|
1698
|
+
: '';
|
|
1699
|
+
if (markdownWorkspace.mode === 'edit') {
|
|
1700
|
+
const deleted = markdownWorkspace.fileDeleted;
|
|
1701
|
+
const revertDisabled = deleted || markdownWorkspace.loadingDocument || !isMarkdownUndoAvailable(markdownWorkspace);
|
|
1702
|
+
if (isFullscreen) {
|
|
1703
|
+
markdownActions = `
|
|
1704
|
+
${deleted ? '<span class="panel-save-status panel-save-status--saved">File deleted</span>' : ''}
|
|
1705
|
+
${!deleted && saveStatusLabel ? `<span class="panel-save-status panel-save-status--${markdownWorkspace.saving ? 'saving' : 'saved'}">${saveStatusLabel}</span>` : ''}
|
|
1706
|
+
${renderMarkdownModeToggle(markdownWorkspace.editorView)}
|
|
1707
|
+
${renderMarkdownCopyButton()}
|
|
1708
|
+
<button class="panel-action" id="revert-markdown" ${revertDisabled ? 'disabled' : ''}>${undoIcon} Undo</button>
|
|
1709
|
+
`;
|
|
1710
|
+
}
|
|
1711
|
+
else {
|
|
1712
|
+
markdownActions = `
|
|
1713
|
+
${deleted ? '<span class="panel-save-status panel-save-status--saved">File deleted</span>' : ''}
|
|
1714
|
+
${!deleted && saveStatusLabel ? `<span class="panel-save-status panel-save-status--${markdownWorkspace.saving ? 'saving' : 'saved'}">${saveStatusLabel}</span>` : ''}
|
|
1715
|
+
${canGoFullscreen ? `<button class="panel-action" id="expand-fullscreen" title="Expand" aria-label="Expand">${expandIcon}</button>` : ''}
|
|
1716
|
+
<button class="panel-action" id="copy-active-markdown" title="Copy" aria-label="Copy">${copyIcon}</button>
|
|
1717
|
+
<button class="panel-action" id="revert-markdown" title="Undo" aria-label="Undo" ${revertDisabled ? 'disabled' : ''}>${undoIcon}</button>
|
|
1718
|
+
`;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const defaultMarkdownEditor = payload.fileType === 'markdown' ? markdownEditorAppCache.get(payload.filePath) : undefined;
|
|
1723
|
+
if (payload.fileType === 'markdown' && !defaultMarkdownEditor) {
|
|
1724
|
+
void detectDefaultMarkdownEditor(payload.filePath);
|
|
1725
|
+
}
|
|
1726
|
+
// Content-area banners for missing lines
|
|
1727
|
+
const hasMissingBefore = range?.isPartial && range.fromLine > 1;
|
|
1728
|
+
const hasMissingAfter = range?.isPartial && range.toLine < range.totalLines && (range.totalLines - range.toLine) > 1;
|
|
1729
|
+
const loadBeforeBanner = hasMissingBefore
|
|
1730
|
+
? `<button class="load-lines-banner" id="load-before">↑ Load lines 1–${range.fromLine - 1}</button>`
|
|
1731
|
+
: '';
|
|
1732
|
+
const loadAfterBanner = hasMissingAfter
|
|
1733
|
+
? `<button class="load-lines-banner" id="load-after">↓ Load lines ${range.toLine + 1}–${range.totalLines}</button>`
|
|
1734
|
+
: '';
|
|
1735
|
+
const effectiveExpanded = isExpanded || getCurrentDisplayMode() === 'fullscreen';
|
|
1736
|
+
const backButton = (directoryBackPayload && payload.fileType !== 'directory')
|
|
1737
|
+
? `<button class="panel-action dir-back-btn" id="dir-back" title="Back to directory">← Back</button>`
|
|
1738
|
+
: '';
|
|
1739
|
+
container.innerHTML = `
|
|
1740
|
+
<main id="tool-shell" class="shell tool-shell ${effectiveExpanded ? 'expanded' : 'collapsed'}${hideSummaryRow ? ' host-framed' : ''}${getCurrentDisplayMode() === 'fullscreen' ? ' fullscreen' : ''}">
|
|
1741
|
+
${getCurrentDisplayMode() === 'fullscreen' ? '' : renderCompactRow({ id: 'compact-toggle', label: compactLabel, filename: payload.fileName, variant: 'ready', expandable: true, expanded: isExpanded, interactive: true })}
|
|
1742
|
+
<section class="panel">
|
|
1743
|
+
<div class="panel-topbar">
|
|
1744
|
+
${backButton}
|
|
1745
|
+
${hideSummaryRow ? '' : `<span class="panel-breadcrumb" title="${escapeHtml(payload.filePath)}">${breadcrumb}</span>`}
|
|
1746
|
+
<span class="panel-topbar-actions">
|
|
1747
|
+
${markdownActions}
|
|
1748
|
+
${htmlToggle}
|
|
1749
|
+
${canOpenInFolder && payload.fileType === 'markdown' && markdownWorkspace?.mode === 'edit' && isFullscreen ? `<button class="panel-action" id="open-in-editor" ${markdownWorkspace?.fileDeleted ? 'disabled' : ''}>${renderMarkdownEditorAppIcon()} Open in ${escapeHtml(defaultMarkdownEditor?.appName ?? 'editor')}</button>` : ''}
|
|
1750
|
+
${canOpenInFolder && payload.fileType === 'markdown' && markdownWorkspace?.mode === 'edit' && !isFullscreen ? `<button class="panel-action" id="open-in-folder" title="Open in folder" aria-label="Open in folder" ${markdownWorkspace?.fileDeleted ? 'disabled' : ''}>${folderIcon}</button>` : ''}
|
|
1751
|
+
${canOpenInFolder && !(payload.fileType === 'markdown' && markdownWorkspace?.mode === 'edit') ? `<button class="panel-action" id="open-in-folder">${folderIcon} Open in folder</button>` : ''}
|
|
1752
|
+
${canCopy && supportsPreview && payload.fileType !== 'markdown' ? `<button class="panel-action" id="copy-source" title="Copy source" aria-label="Copy source">${copyIcon} Copy</button>` : ''}
|
|
1753
|
+
</span>
|
|
1754
|
+
</div>
|
|
1755
|
+
${notice}
|
|
1756
|
+
<div class="panel-content-wrapper">
|
|
1757
|
+
${loadBeforeBanner}
|
|
1758
|
+
${body.html}
|
|
1759
|
+
${loadAfterBanner}
|
|
1760
|
+
</div>
|
|
1761
|
+
<div class="panel-footer">
|
|
1762
|
+
<span>${footerLabel}</span>
|
|
1763
|
+
</div>
|
|
1764
|
+
</section>
|
|
1765
|
+
</main>
|
|
1766
|
+
`;
|
|
1767
|
+
document.body.classList.add('dc-ready');
|
|
1768
|
+
attachCopyHandler(payload);
|
|
1769
|
+
attachHtmlToggleHandler(container, payload, htmlMode);
|
|
1770
|
+
attachOpenInFolderHandler(payload);
|
|
1771
|
+
attachOpenInEditorHandler(payload);
|
|
1772
|
+
attachLoadAllHandler(container, payload, htmlMode);
|
|
1773
|
+
attachMarkdownWorkspaceHandlers(payload);
|
|
1774
|
+
attachTextSelectionHandler(payload);
|
|
1775
|
+
if (payload.fileType === 'directory') {
|
|
1776
|
+
attachDirectoryHandlers(container, payload);
|
|
1777
|
+
}
|
|
1778
|
+
// Back to directory navigation
|
|
1779
|
+
const backBtn = document.getElementById('dir-back');
|
|
1780
|
+
if (backBtn && directoryBackPayload) {
|
|
1781
|
+
const savedPayload = directoryBackPayload;
|
|
1782
|
+
backBtn.addEventListener('click', () => {
|
|
1783
|
+
directoryBackPayload = undefined;
|
|
1784
|
+
renderApp(container, savedPayload, 'rendered', true);
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
// Clear back state when showing a directory
|
|
1788
|
+
if (payload.fileType === 'directory') {
|
|
1789
|
+
directoryBackPayload = undefined;
|
|
1790
|
+
}
|
|
1791
|
+
const compactRow = document.getElementById('compact-toggle');
|
|
1792
|
+
shellController = createCompactRowShellController({
|
|
1793
|
+
shell: document.getElementById('tool-shell'),
|
|
1794
|
+
compactRow,
|
|
1795
|
+
initialExpanded: effectiveExpanded,
|
|
1796
|
+
onToggle: (expanded) => {
|
|
1797
|
+
isExpanded = expanded;
|
|
1798
|
+
trackUiEvent?.(expanded ? 'expand' : 'collapse', {
|
|
1799
|
+
file_type: payload.fileType,
|
|
1800
|
+
file_extension: fileExtension
|
|
1801
|
+
});
|
|
1802
|
+
},
|
|
1803
|
+
onScrollAfterExpand: () => {
|
|
1804
|
+
trackUiEvent?.('scroll_after_expand', {
|
|
1805
|
+
file_type: payload.fileType,
|
|
1806
|
+
file_extension: fileExtension
|
|
1807
|
+
});
|
|
1808
|
+
},
|
|
1809
|
+
onRender
|
|
1810
|
+
});
|
|
1811
|
+
onRender?.();
|
|
1812
|
+
if (!previewShownFired) {
|
|
1813
|
+
previewShownFired = true;
|
|
1814
|
+
trackUiEvent?.('preview_shown', {
|
|
1815
|
+
file_type: payload.fileType,
|
|
1816
|
+
file_extension: fileExtension
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
export function bootstrapApp() {
|
|
1821
|
+
const container = document.getElementById('app');
|
|
1822
|
+
if (!container) {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
renderLoadingState(container);
|
|
1826
|
+
// Use the official App class – it connects to the host via PostMessageTransport
|
|
1827
|
+
// (window.parent by default) and speaks standard MCP JSON-RPC 2.0 over postMessage.
|
|
1828
|
+
const app = new App({ name: 'Desktop Commander File Preview', version: '1.0.0' }, { updateModelContext: { text: {} } }, { autoResize: true });
|
|
1829
|
+
const chrome = {
|
|
1830
|
+
expanded: isExpanded,
|
|
1831
|
+
hideSummaryRow,
|
|
1832
|
+
};
|
|
1833
|
+
const syncChromeState = () => {
|
|
1834
|
+
isExpanded = chrome.expanded;
|
|
1835
|
+
hideSummaryRow = chrome.hideSummaryRow;
|
|
1836
|
+
};
|
|
1837
|
+
// Widget state for cross-host persistence (survives page refresh)
|
|
1838
|
+
const widgetState = createWidgetStateStorage((v) => isPreviewStructuredContent(v) && typeof v.content === 'string');
|
|
1839
|
+
const renderAndSync = (payload) => {
|
|
1840
|
+
if (payload) {
|
|
1841
|
+
widgetState.write(payload);
|
|
1842
|
+
}
|
|
1843
|
+
renderApp(container, payload, 'rendered', isExpanded);
|
|
1844
|
+
};
|
|
1845
|
+
const syncFromPersistedWidgetState = () => {
|
|
1846
|
+
const persistedPayload = widgetState.read();
|
|
1847
|
+
if (!persistedPayload) {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (currentPayload
|
|
1851
|
+
&& currentPayload.filePath === persistedPayload.filePath
|
|
1852
|
+
&& stripReadStatusLine(currentPayload.content) === stripReadStatusLine(persistedPayload.content)) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
renderAndSync(persistedPayload);
|
|
1856
|
+
};
|
|
1857
|
+
syncPayload = renderAndSync;
|
|
1858
|
+
persistPayload = (payload) => { widgetState.write(payload); };
|
|
1859
|
+
rerenderCurrent = () => {
|
|
1860
|
+
renderApp(container, currentPayload, currentHtmlMode, isExpanded);
|
|
1861
|
+
};
|
|
1862
|
+
// Cached payload from a previous session, stashed at onConnected. Used when
|
|
1863
|
+
// the host's ontoolinput announces the same file path so we can show the
|
|
1864
|
+
// last-known content instantly instead of flashing a loading state on reopen.
|
|
1865
|
+
// Fresh tool_result still wins and replaces the cached render when it arrives.
|
|
1866
|
+
let pendingCachedPayload;
|
|
1867
|
+
let initialStateResolved = false;
|
|
1868
|
+
const resolveInitialState = (payload, message) => {
|
|
1869
|
+
if (initialStateResolved) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
initialStateResolved = true;
|
|
1873
|
+
if (payload) {
|
|
1874
|
+
renderAndSync(payload);
|
|
1875
|
+
if (payload.fileType === 'markdown' && getCurrentDisplayMode() === 'fullscreen') {
|
|
1876
|
+
void requestMarkdownEditMode(payload);
|
|
1877
|
+
}
|
|
1878
|
+
// Re-read markdown from disk to pick up any saves from a previous session
|
|
1879
|
+
if (payload.fileType === 'markdown') {
|
|
1880
|
+
void refreshMarkdownFromDisk(payload);
|
|
1881
|
+
}
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
renderStatusState(container, message ?? 'No preview available for this response.');
|
|
1885
|
+
onRender?.();
|
|
1886
|
+
};
|
|
1887
|
+
// autoResize handles size reporting; onRender can be a no-op
|
|
1888
|
+
onRender = () => { };
|
|
1889
|
+
// Wire rpcCallTool through the App's callServerTool proxy
|
|
1890
|
+
rpcCallTool = (name, args) => (app.callServerTool({ name, arguments: args }));
|
|
1891
|
+
// Wire rpcUpdateContext through the App's updateModelContext
|
|
1892
|
+
rpcUpdateContext = (text) => {
|
|
1893
|
+
const params = text
|
|
1894
|
+
? { content: [{ type: 'text', text }] }
|
|
1895
|
+
: { content: [] };
|
|
1896
|
+
app.updateModelContext(params).catch(() => {
|
|
1897
|
+
// Host may not support updateModelContext
|
|
1898
|
+
});
|
|
1899
|
+
};
|
|
1900
|
+
openExternalLink = async (url) => {
|
|
1901
|
+
const result = await app.openLink({ url });
|
|
1902
|
+
return result.isError !== true;
|
|
1903
|
+
};
|
|
1904
|
+
requestDisplayMode = async (mode) => {
|
|
1905
|
+
const result = await app.requestDisplayMode({ mode });
|
|
1906
|
+
return typeof result.mode === 'string' ? result.mode : null;
|
|
1907
|
+
};
|
|
1908
|
+
trackUiEvent = createUiEventTracker((name, args) => app.callServerTool({ name, arguments: args }), {
|
|
1909
|
+
component: 'file_preview',
|
|
1910
|
+
baseParams: { tool_name: 'read_file' },
|
|
1911
|
+
});
|
|
1912
|
+
// Register ALL handlers BEFORE connect
|
|
1913
|
+
app.onteardown = async () => {
|
|
1914
|
+
shellController?.dispose();
|
|
1915
|
+
disposeMarkdownWorkspaceHandles();
|
|
1916
|
+
return {};
|
|
1917
|
+
};
|
|
1918
|
+
app.ontoolinput = (params) => {
|
|
1919
|
+
// If we have a cached payload from a previous session for the file the
|
|
1920
|
+
// host is now asking us to preview, render it immediately so reopening
|
|
1921
|
+
// the same document feels instant. Fresh tool_result will replace it.
|
|
1922
|
+
const requestedPath = typeof params.arguments?.path === 'string' ? params.arguments.path : undefined;
|
|
1923
|
+
if (!initialStateResolved
|
|
1924
|
+
&& pendingCachedPayload
|
|
1925
|
+
&& requestedPath
|
|
1926
|
+
&& pendingCachedPayload.filePath === requestedPath) {
|
|
1927
|
+
const cached = pendingCachedPayload;
|
|
1928
|
+
pendingCachedPayload = undefined;
|
|
1929
|
+
resolveInitialState(cached);
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
// Tool is executing – show loading state
|
|
1933
|
+
renderLoadingState(container);
|
|
1934
|
+
onRender?.();
|
|
1935
|
+
};
|
|
1936
|
+
app.ontoolresult = (result) => {
|
|
1937
|
+
// Fresh data wins; discard any cache hint we held for the optimistic render path.
|
|
1938
|
+
pendingCachedPayload = undefined;
|
|
1939
|
+
const payload = extractRenderPayload(result);
|
|
1940
|
+
const message = extractToolText(result);
|
|
1941
|
+
if (!initialStateResolved) {
|
|
1942
|
+
if (payload) {
|
|
1943
|
+
resolveInitialState(getEffectiveIncomingPayload(payload));
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
if (message) {
|
|
1947
|
+
resolveInitialState(undefined, message);
|
|
1948
|
+
}
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
if (payload) {
|
|
1952
|
+
const effectivePayload = getEffectiveIncomingPayload(payload);
|
|
1953
|
+
renderAndSync(effectivePayload);
|
|
1954
|
+
}
|
|
1955
|
+
else if (message) {
|
|
1956
|
+
renderStatusState(container, message);
|
|
1957
|
+
onRender?.();
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
app.ontoolcancelled = (params) => {
|
|
1961
|
+
resolveInitialState(undefined, params.reason ?? 'Tool was cancelled.');
|
|
1962
|
+
};
|
|
1963
|
+
// Connect to the host (defaults to window.parent via PostMessageTransport)
|
|
1964
|
+
void connectWithSharedHostContext({
|
|
1965
|
+
app,
|
|
1966
|
+
chrome,
|
|
1967
|
+
onContextApplied: () => {
|
|
1968
|
+
const previousDisplayMode = getCurrentDisplayMode();
|
|
1969
|
+
syncChromeState();
|
|
1970
|
+
currentHostContext = app.getHostContext();
|
|
1971
|
+
const nextDisplayMode = getCurrentDisplayMode();
|
|
1972
|
+
if (previousDisplayMode === 'fullscreen'
|
|
1973
|
+
&& nextDisplayMode === 'inline'
|
|
1974
|
+
&& currentPayload?.fileType === 'markdown') {
|
|
1975
|
+
isExpanded = true;
|
|
1976
|
+
chrome.expanded = true;
|
|
1977
|
+
if (markdownWorkspaceState) {
|
|
1978
|
+
markdownWorkspaceState.notice = null;
|
|
1979
|
+
markdownWorkspaceState.editorView = 'markdown';
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (initialStateResolved) {
|
|
1983
|
+
rerenderCurrent?.();
|
|
1984
|
+
}
|
|
1985
|
+
},
|
|
1986
|
+
onConnected: () => {
|
|
1987
|
+
currentHostContext = app.getHostContext();
|
|
1988
|
+
// Stash any persisted payload so ontoolinput can show it instantly
|
|
1989
|
+
// when the host announces the same file path. Fresh tool_result still
|
|
1990
|
+
// wins. If the host never sends ontoolresult, the 8s fallback below
|
|
1991
|
+
// surfaces an error so the user doesn't see stale or empty content.
|
|
1992
|
+
pendingCachedPayload = widgetState.read() ?? undefined;
|
|
1993
|
+
// Fallback: if no tool data arrives, show a helpful status message
|
|
1994
|
+
window.setTimeout(() => {
|
|
1995
|
+
if (!initialStateResolved) {
|
|
1996
|
+
resolveInitialState(undefined, 'Preview unavailable after page refresh. Switch threads or re-run the tool.');
|
|
1997
|
+
}
|
|
1998
|
+
}, 8000);
|
|
1999
|
+
},
|
|
2000
|
+
}).catch(() => {
|
|
2001
|
+
renderStatusState(container, 'Failed to connect to host.');
|
|
2002
|
+
onRender?.();
|
|
2003
|
+
});
|
|
2004
|
+
const handleVisibilitySync = () => {
|
|
2005
|
+
if (document.visibilityState === 'visible') {
|
|
2006
|
+
syncFromPersistedWidgetState();
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
const handleFocusSync = () => {
|
|
2010
|
+
syncFromPersistedWidgetState();
|
|
2011
|
+
};
|
|
2012
|
+
document.addEventListener('visibilitychange', handleVisibilitySync);
|
|
2013
|
+
window.addEventListener('focus', handleFocusSync);
|
|
2014
|
+
window.addEventListener('beforeunload', () => {
|
|
2015
|
+
shellController?.dispose();
|
|
2016
|
+
disposeMarkdownWorkspaceHandles();
|
|
2017
|
+
document.removeEventListener('visibilitychange', handleVisibilitySync);
|
|
2018
|
+
window.removeEventListener('focus', handleFocusSync);
|
|
2019
|
+
}, { once: true });
|
|
2020
|
+
}
|