UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

110 lines (109 loc) 8.09 kB
import { renderCompactRow } from '../../shared/compact-row.js'; import { escapeHtml } from '../../shared/escape-html.js'; import { parseReadRange, stripReadStatusLine } from './document-workspace.js'; import { renderMarkdownCopyButton, renderMarkdownModeToggle } from './markdown/editor.js'; import { buildBreadcrumb, countContentLines } from './payload-utils.js'; function renderCopyIcon() { return '<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>'; } function renderFolderIcon() { return '<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>'; } function renderUndoIcon() { return '<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>'; } function renderExpandIcon() { return '<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>'; } function renderMarkdownSaveStatus(workspace) { if (workspace.fileDeleted) { return '<span class="panel-save-status panel-save-status--saved">File deleted</span>'; } if (workspace.saveIndicator !== 'saved') { return ''; } const variant = workspace.saving ? 'saving' : 'saved'; return `<span class="panel-save-status panel-save-status--${variant}">Saved</span>`; } export function buildDocumentLayout(options) { const range = parseReadRange(options.payload.content); const notice = options.body.notice ? `<div class="notice">${options.body.notice}</div>` : ''; const breadcrumb = buildBreadcrumb(options.payload.filePath); const lineCount = range ? range.toLine - range.fromLine + 1 : countContentLines(options.payload.content); const fileTypeLabel = options.payload.fileType === 'markdown' ? 'MARKDOWN' : options.payload.fileType === 'html' ? 'HTML' : options.payload.fileType === 'image' ? 'IMAGE' : options.payload.fileType === 'directory' ? 'DIRECTORY' : options.fileExtension !== 'none' ? options.fileExtension.toUpperCase() : 'TEXT'; const compactLabel = range?.isPartial ? `View lines ${range.fromLine}${range.toLine}` : options.payload.fileType === 'directory' ? 'View directory' : 'View file'; let footerLabel = range?.isPartial ? `${fileTypeLabel} • LINES ${range.fromLine}${range.toLine} OF ${range.totalLines}` : `${fileTypeLabel}${lineCount} LINE${lineCount !== 1 ? 'S' : ''}`; if (options.markdownWorkspace) { const source = stripReadStatusLine(options.markdownWorkspace.draftContent); const markdownWordCount = source.trim().split(/\s+/).filter(Boolean).length; const markdownLineCount = countContentLines(source); footerLabel = `${fileTypeLabel} • EDIT MODE • ${markdownLineCount} LINES • ${markdownWordCount} WORDS`; } const isFullscreen = options.currentDisplayMode === 'fullscreen'; const htmlToggle = options.payload.fileType === 'html' ? `<button class="panel-action" id="toggle-html-mode">${options.htmlMode === 'rendered' ? 'Source' : 'Rendered'}</button>` : ''; const backButton = options.hasDirectoryBackButton && options.payload.fileType !== 'directory' ? '<button class="panel-action dir-back-btn" id="dir-back" title="Back to directory">← Back</button>' : ''; const isMarkdown = options.payload.fileType === 'markdown'; const isMarkdownEdit = isMarkdown && !!options.markdownWorkspace; const revertDisabled = isMarkdownEdit && (options.markdownWorkspace.fileDeleted || options.markdownWorkspace.loadingDocument || !options.isMarkdownUndoAvailable); const fileDeleted = isMarkdownEdit && options.markdownWorkspace.fileDeleted; const hasMissingBefore = range?.isPartial && range.fromLine > 1; const hasMissingAfter = range?.isPartial && range.toLine < range.totalLines && (range.totalLines - range.toLine) > 1; const loadBeforeBanner = hasMissingBefore ? `<button class="load-lines-banner" id="load-before">↑ Load lines 1–${range.fromLine - 1}</button>` : ''; const loadAfterBanner = hasMissingAfter ? `<button class="load-lines-banner" id="load-after">↓ Load lines ${range.toLine + 1}${range.totalLines}</button>` : ''; const effectiveExpanded = options.isExpanded || isFullscreen; const canOpenInFolder = options.capabilities.canOpenInFolder; const canCopy = options.capabilities.canCopy; return { effectiveExpanded, html: ` <main id="tool-shell" class="shell tool-shell ${effectiveExpanded ? 'expanded' : 'collapsed'}${options.hideSummaryRow ? ' host-framed' : ''}${isFullscreen ? ' fullscreen' : ''}"> ${isFullscreen ? '' : renderCompactRow({ id: 'compact-toggle', label: compactLabel, filename: options.payload.fileName, variant: 'ready', expandable: true, expanded: options.isExpanded, interactive: true })} <section class="panel"> <div class="panel-topbar"> ${backButton} ${options.hideSummaryRow ? '' : `<span class="panel-breadcrumb" title="${escapeHtml(options.payload.filePath)}">${breadcrumb}</span>`} <span class="panel-topbar-actions"> ${isMarkdownEdit ? renderMarkdownSaveStatus(options.markdownWorkspace) : ''} ${htmlToggle} ${isMarkdownEdit && isFullscreen ? renderMarkdownModeToggle(options.markdownWorkspace.editorView) : ''} ${isMarkdown && !isFullscreen && options.canGoFullscreen ? `<button class="panel-action" id="expand-fullscreen" title="Expand" aria-label="Expand">${renderExpandIcon()}</button>` : ''} ${isMarkdownEdit ? `<button class="panel-action" id="revert-markdown" ${isFullscreen ? '' : 'title="Undo" aria-label="Undo" '}${revertDisabled ? 'disabled' : ''}>${renderUndoIcon()}${isFullscreen ? ' Undo' : ''}</button>` : ''} ${isMarkdownEdit && isFullscreen ? renderMarkdownCopyButton() : ''} ${isMarkdown && !isFullscreen ? `<button class="panel-action" id="copy-active-markdown" title="Copy" aria-label="Copy">${renderCopyIcon()}</button>` : ''} ${canCopy && options.capabilities.supportsPreview && !isMarkdown ? `<button class="panel-action" id="copy-source" title="Copy source" aria-label="Copy source">${renderCopyIcon()}</button>` : ''} ${canOpenInFolder && isMarkdownEdit && isFullscreen ? `<button class="panel-action" id="open-in-editor" ${fileDeleted ? 'disabled' : ''}>${options.markdownEditorAppIcon} Open in ${escapeHtml(options.defaultMarkdownEditorName ?? 'editor')}</button>` : ''} ${canOpenInFolder && !(isMarkdownEdit && isFullscreen) ? `<button class="panel-action" id="open-in-folder" title="Open in folder" aria-label="Open in folder" ${isMarkdownEdit && fileDeleted ? 'disabled' : ''}>${renderFolderIcon()}</button>` : ''} </span> </div> ${notice} <div class="panel-content-wrapper"> ${loadBeforeBanner} ${options.body.html} ${loadAfterBanner} </div> <div class="panel-footer"> <span>${footerLabel}</span> </div> </section> </main> `, }; }