UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

183 lines (182 loc) 7.85 kB
import { parseReadRange, stripReadStatusLine } from './document-workspace.js'; import { extractToolText } from './payload-utils.js'; export function attachPanelActions(options) { const queryById = (id) => (options.container.querySelector(`#${id}`)); const fallbackCopy = (text) => { const textArea = document.createElement('textarea'); textArea.value = text; textArea.setAttribute('readonly', ''); textArea.style.position = 'fixed'; textArea.style.top = '-9999px'; document.body.appendChild(textArea); textArea.select(); const success = document.execCommand('copy'); document.body.removeChild(textArea); return success; }; const setButtonState = (button, label, fallbackLabel, revertMs) => { button.setAttribute('title', label); button.setAttribute('aria-label', label); button.textContent = label; if (revertMs) { setTimeout(() => { button.textContent = fallbackLabel; button.setAttribute('title', fallbackLabel); button.setAttribute('aria-label', fallbackLabel); }, revertMs); } }; const setIconButtonState = (button, label, fallbackLabel, revertMs) => { button.setAttribute('title', label); button.setAttribute('aria-label', label); button.dataset.status = label; if (revertMs) { setTimeout(() => { button.setAttribute('title', fallbackLabel); button.setAttribute('aria-label', fallbackLabel); delete button.dataset.status; }, revertMs); } }; const copyTextData = async (text) => { try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true; } return fallbackCopy(text); } catch { return fallbackCopy(text); } }; const fileExtension = options.getFileExtensionForAnalytics(options.payload.filePath); const copyButton = queryById('copy-source'); copyButton?.addEventListener('click', async () => { options.trackUiEvent?.('copy_clicked', { file_type: options.payload.fileType, file_extension: fileExtension, }); const copied = await copyTextData(stripReadStatusLine(options.payload.content)); setButtonState(copyButton, copied ? 'Copied!' : 'Copy failed', 'Copy', 1500); }); const activeCopyButton = queryById('copy-active-markdown'); activeCopyButton?.addEventListener('click', async () => { const textToCopy = options.markdownController.getCopyText(options.payload); if (!textToCopy) { return; } const copied = await copyTextData(textToCopy); if (copied) { options.updateSaveStatus('Copied', 'saved'); window.setTimeout(() => options.updateSaveStatus('', ''), 1500); } setIconButtonState(activeCopyButton, copied ? 'Copied!' : 'Copy failed', 'Copy', 1500); }); const toggleButton = queryById('toggle-html-mode'); toggleButton?.addEventListener('click', () => { const nextMode = options.htmlMode === 'rendered' ? 'source' : 'rendered'; options.trackUiEvent?.('html_view_toggled', { file_type: options.payload.fileType, file_extension: fileExtension, }); options.render(options.payload, nextMode, options.getIsExpanded()); }); const openFolderButton = queryById('open-in-folder'); if (openFolderButton) { const command = options.buildOpenInFolderCommand(options.payload.filePath); if (!command) { openFolderButton.disabled = true; } else { openFolderButton.addEventListener('click', async () => { options.trackUiEvent?.('open_in_folder', { file_type: options.payload.fileType, file_extension: fileExtension, }); try { await options.callTool?.('start_process', { command, timeout_ms: 12000 }); } catch { // Keep UI stable if opening folder fails. } }); } } const openEditorButton = queryById('open-in-editor'); if (openEditorButton) { const command = options.buildOpenInEditorCommand(options.payload.filePath); if (!command) { openEditorButton.disabled = true; } else { openEditorButton.addEventListener('click', async () => { options.trackUiEvent?.('open_in_editor', { file_type: options.payload.fileType, file_extension: fileExtension, }); try { await options.callTool?.('start_process', { command, timeout_ms: 12000 }); } catch { // Keep UI stable if opening editor fails. } }); } } const beforeBtn = queryById('load-before'); const afterBtn = queryById('load-after'); if (!beforeBtn && !afterBtn) { return; } const range = parseReadRange(options.payload.content); if (!range?.isPartial) { return; } const currentContent = stripReadStatusLine(options.payload.content); const loadLines = async (button, direction) => { const originalText = button.textContent; button.textContent = 'Loading…'; button.disabled = true; options.trackUiEvent?.(direction === 'before' ? 'load_lines_before' : 'load_lines_after', { file_type: options.payload.fileType, file_extension: fileExtension, }); try { const readArgs = direction === 'before' ? { path: options.payload.filePath, offset: 0, length: range.fromLine - 1 } : { path: options.payload.filePath, offset: range.toLine }; const result = await options.callTool?.('read_file', readArgs); const newText = extractToolText(result); if (newText && typeof newText === 'string') { const cleanNew = stripReadStatusLine(newText); const merged = direction === 'before' ? `${cleanNew}${cleanNew.endsWith('\n') ? '' : '\n'}${currentContent}` : `${currentContent}${currentContent.endsWith('\n') ? '' : '\n'}${cleanNew}`; const newFrom = direction === 'before' ? 1 : range.fromLine; const newTo = direction === 'after' ? range.totalLines : range.toLine; const lineCount = newTo - newFrom + 1; const remaining = range.totalLines - newTo; const isStillPartial = newFrom > 1 || newTo < range.totalLines; const statusLine = isStillPartial ? `[Reading ${lineCount} lines from ${newFrom === 1 ? 'start' : `line ${newFrom}`} (total: ${range.totalLines} lines, ${remaining} remaining)]\n` : ''; options.render({ ...options.payload, content: statusLine + merged, }, options.htmlMode, options.getIsExpanded()); return; } } catch { // Fall through to button reset. } button.textContent = 'Failed to load'; setTimeout(() => { button.textContent = originalText; button.disabled = false; }, 2000); }; beforeBtn?.addEventListener('click', () => void loadLines(beforeBtn, 'before')); afterBtn?.addEventListener('click', () => void loadLines(afterBtn, 'after')); }