@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
110 lines (109 loc) • 8.09 kB
JavaScript
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>
`,
};
}