@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
183 lines (182 loc) • 7.85 kB
JavaScript
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'));
}