@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
841 lines (838 loc) • 32.3 kB
JavaScript
import { App } from '@modelcontextprotocol/ext-apps';
import { createToolBridge } from '../../shared/tool-bridge.js';
import { createCompactRowShellController } from '../../shared/tool-shell.js';
import { renderCompactRow } from '../../shared/compact-row.js';
import { escapeHtml } from '../../shared/escape-html.js';
import { createWidgetStateStorage } from '../../shared/widget-state.js';
import { connectWithSharedHostContext, isObjectRecord } from '../../shared/host-context.js';
import { createUiEventTracker } from '../../shared/ui-event-tracker.js';
import { createArrayModalController, renderArrayModalMarkup } from './array-modal.js';
import { CONFIG_FIELD_DEFINITIONS, isConfigFieldKey } from '../../../config-field-definitions.js';
let shellController;
const CONFIG_EDITOR_COMPONENT = 'config_editor';
const GET_CONFIG_TOOL_NAME = 'get_config';
const MAX_TELEMETRY_MESSAGE_LENGTH = 180;
function sanitizeTelemetryErrorMessage(message) {
// Keep error signal useful while removing path-like data and bounding payload size.
const collapsed = message.replace(/\s+/g, ' ').trim();
const withoutPaths = collapsed
.replace(/(?:\/|\\)[\w\d_.\-/\\]+/g, '[PATH]')
.replace(/[A-Za-z]:\\[\w\d_.\-/\\]+/g, '[PATH]');
if (withoutPaths.length === 0) {
return 'Unknown error';
}
if (withoutPaths.length <= MAX_TELEMETRY_MESSAGE_LENGTH) {
return withoutPaths;
}
return `${withoutPaths.slice(0, MAX_TELEMETRY_MESSAGE_LENGTH - 3)}...`;
}
function buildConfigUpdateTelemetryParams(args) {
const base = {
config_key: args.configKey,
value_type: args.valueType,
};
if (args.errorStage) {
base.error_stage = args.errorStage;
}
if (args.errorMessage) {
base.error_message = sanitizeTelemetryErrorMessage(args.errorMessage);
}
return base;
}
function isConfigEditorPayload(value) {
return isObjectRecord(value) && Array.isArray(value.entries);
}
function stringifyValueForInput(value) {
if (Array.isArray(value)) {
return value.map((item) => String(item)).join('\n');
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
if (value === null) {
return 'null';
}
try {
return JSON.stringify(value);
}
catch {
return String(value);
}
}
function formatKeyLabel(key) {
return key
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/[_-]+/g, ' ')
.trim()
.split(/\s+/)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
function getConfigFieldMetadata(key) {
if (!isConfigFieldKey(key)) {
return undefined;
}
return {
label: CONFIG_FIELD_DEFINITIONS[key].label,
description: CONFIG_FIELD_DEFINITIONS[key].description,
};
}
function extractPayload(result) {
if (!isObjectRecord(result)) {
return null;
}
const structured = result.structuredContent;
if (!isObjectRecord(structured) || !Array.isArray(structured.entries)) {
return null;
}
const entries = structured.entries
.filter((entry) => isObjectRecord(entry))
.filter((entry) => typeof entry.key === 'string')
.map((entry) => {
const key = entry.key;
const metadata = getConfigFieldMetadata(key);
return {
key,
label: metadata?.label,
description: metadata?.description,
value: entry.value,
valueType: typeof entry.valueType === 'string' ? entry.valueType : 'string',
editable: entry.editable !== false,
};
});
return {
config: isObjectRecord(structured.config) ? structured.config : undefined,
uiHints: isObjectRecord(structured.uiHints)
? {
availableShells: Array.isArray(structured.uiHints.availableShells)
? structured.uiHints.availableShells.filter((value) => typeof value === 'string')
: undefined,
}
: undefined,
entries,
};
}
function extractToolText(result) {
if (!isObjectRecord(result) || !Array.isArray(result.content)) {
return undefined;
}
for (const item of result.content) {
if (!isObjectRecord(item)) {
continue;
}
if (item.type === 'text' && typeof item.text === 'string' && item.text.trim().length > 0) {
return item.text;
}
}
return undefined;
}
function isToolErrorResult(result) {
return isObjectRecord(result) && result.isError === true;
}
function parseDraftValue(rawValue, valueType) {
if (valueType === 'string') {
return { ok: true, value: rawValue.replace(/\r?\n/g, ' ') };
}
if (valueType === 'number') {
if (rawValue.trim() === '') {
return { ok: false, message: 'Enter a valid number.' };
}
const numeric = Number(rawValue);
if (!Number.isFinite(numeric)) {
return { ok: false, message: 'Enter a valid number.' };
}
return { ok: true, value: numeric };
}
if (valueType === 'boolean') {
const normalized = rawValue.trim().toLowerCase();
if (normalized !== 'true' && normalized !== 'false') {
return { ok: false, message: 'Boolean values must be true or false.' };
}
return { ok: true, value: normalized === 'true' };
}
if (valueType === 'null') {
if (rawValue.trim().toLowerCase() !== 'null') {
return { ok: false, message: 'Null values must be exactly null.' };
}
return { ok: true, value: null };
}
if (valueType === 'array') {
const trimmed = rawValue.trim();
if (trimmed.length === 0) {
return { ok: true, value: [] };
}
if (!trimmed.startsWith('[')) {
const items = rawValue
.split(/\r?\n/)
.map((item) => item.trim())
.filter((item) => item.length > 0);
return { ok: true, value: items };
}
try {
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed)) {
return { ok: false, message: 'Array values must be valid JSON arrays.' };
}
if (!parsed.every((item) => typeof item === 'string')) {
return { ok: false, message: 'Array values must contain only strings.' };
}
return { ok: true, value: parsed };
}
catch {
return { ok: false, message: 'Array values must be valid JSON arrays.' };
}
}
return { ok: true, value: rawValue };
}
function areConfigValuesEqual(left, right) {
if (Object.is(left, right)) {
return true;
}
if (Array.isArray(left) && Array.isArray(right)) {
if (left.length !== right.length) {
return false;
}
for (let index = 0; index < left.length; index += 1) {
if (!Object.is(left[index], right[index])) {
return false;
}
}
return true;
}
return false;
}
function parseDraftArrayValues(draft) {
const trimmed = draft.trim();
if (!trimmed) {
return [];
}
if (trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed)) {
return parsed.filter((item) => typeof item === 'string');
}
}
catch {
// fallback below
}
}
return draft
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0);
}
function getSettingSummary(entry) {
if (entry.key !== 'blockedCommands' && entry.key !== 'allowedDirectories') {
return null;
}
const values = Array.isArray(entry.value)
? entry.value.filter((item) => typeof item === 'string' && item.trim().length > 0)
: [];
const count = values.length;
if (entry.key === 'blockedCommands') {
return `${count} command${count === 1 ? '' : 's'} blocked`;
}
if (count === 0) {
return 'All folders allowed (no restriction)';
}
return `${count} folder${count === 1 ? '' : 's'} allowed`;
}
function getShellOptions(payload, currentShell) {
const hintedShells = Array.isArray(payload?.uiHints?.availableShells)
? payload.uiHints.availableShells.filter((shell) => typeof shell === 'string' && shell.trim().length > 0)
: [];
const config = payload?.config;
const systemInfo = isObjectRecord(config?.systemInfo) ? config.systemInfo : null;
const isWindows = Boolean(systemInfo && systemInfo.isWindows === true);
const isMacOS = Boolean(systemInfo && systemInfo.isMacOS === true);
const baseOptions = hintedShells.length > 0
? hintedShells
: isWindows
? ['powershell.exe', 'pwsh.exe', 'cmd.exe', 'bash.exe']
: isMacOS
? ['/bin/zsh', '/bin/bash', '/bin/sh', '/usr/bin/fish', 'zsh', 'bash', 'sh', 'fish']
: ['/bin/bash', '/bin/sh', '/usr/bin/fish', '/bin/zsh', 'bash', 'sh', 'fish', 'zsh'];
const options = new Set();
for (const shell of baseOptions) {
options.add(shell);
}
if (currentShell.trim().length > 0) {
options.add(currentShell);
}
return [...options];
}
export function createConfigEditorController(callTool, trackConfigUiEvent) {
const state = {
payload: null,
selectedKey: null,
draftValue: '',
};
const getSelectedEntry = () => {
if (!state.payload || !state.selectedKey) {
return undefined;
}
return state.payload.entries.find((entry) => entry.key === state.selectedKey);
};
const setPayload = (payload) => {
state.payload = payload;
if (!payload || payload.entries.length === 0) {
state.selectedKey = null;
state.draftValue = '';
return;
}
if (!state.selectedKey || !payload.entries.some((entry) => entry.key === state.selectedKey)) {
state.selectedKey = payload.entries[0].key;
}
const current = getSelectedEntry();
state.draftValue = current ? stringifyValueForInput(current.value) : '';
};
const setSelection = (key) => {
state.selectedKey = key;
const selected = getSelectedEntry();
state.draftValue = selected ? stringifyValueForInput(selected.value) : '';
};
const setDraftValue = (value) => {
state.draftValue = value;
};
const apply = async () => {
const selected = getSelectedEntry();
if (!selected) {
return {
ok: false,
tooltip: {
message: 'Select a config key before applying.',
tone: 'error',
},
};
}
if (!selected.editable) {
return {
ok: false,
tooltip: {
message: `The selected key (${selected.key}) is read-only.`,
tone: 'error',
},
};
}
const parsed = parseDraftValue(state.draftValue, selected.valueType);
if (!parsed.ok) {
return {
ok: false,
tooltip: {
message: parsed.message,
tone: 'error',
},
};
}
if (areConfigValuesEqual(parsed.value, selected.value)) {
state.draftValue = stringifyValueForInput(selected.value);
return { ok: false };
}
try {
const setResult = await callTool('set_config_value', {
key: selected.key,
value: parsed.value,
origin: 'ui',
});
if (isToolErrorResult(setResult)) {
const errorMessage = extractToolText(setResult) ?? `Failed to update ${selected.key}.`;
trackConfigUiEvent?.('config_update_failed', {
tool_name: 'set_config_value',
...buildConfigUpdateTelemetryParams({
configKey: selected.key,
valueType: selected.valueType,
errorMessage,
errorStage: 'set_config_value',
}),
});
return {
ok: false,
tooltip: {
message: errorMessage,
tone: 'error',
},
};
}
trackConfigUiEvent?.('config_update_success', {
tool_name: 'set_config_value',
...buildConfigUpdateTelemetryParams({
configKey: selected.key,
valueType: selected.valueType,
}),
});
const refreshed = await callTool('get_config', {});
if (isToolErrorResult(refreshed)) {
const errorMessage = extractToolText(refreshed) ?? 'Value was updated but config refresh failed.';
return {
ok: false,
tooltip: {
message: errorMessage,
tone: 'error',
},
};
}
const refreshedPayload = extractPayload(refreshed);
if (refreshedPayload) {
setPayload(refreshedPayload);
if (state.selectedKey === selected.key) {
const updatedSelected = getSelectedEntry();
state.draftValue = updatedSelected
? stringifyValueForInput(updatedSelected.value)
: state.draftValue;
}
}
return {
ok: true,
};
}
catch (error) {
const errorMessage = `Failed to apply value: ${error instanceof Error ? error.message : String(error)}`;
trackConfigUiEvent?.('config_update_failed', {
tool_name: 'set_config_value',
...buildConfigUpdateTelemetryParams({
configKey: selected.key,
valueType: selected.valueType,
errorMessage,
errorStage: 'transport',
}),
});
return {
ok: false,
tooltip: {
message: errorMessage,
tone: 'error',
},
};
}
};
return {
state,
callTool,
extractPayload,
setPayload,
setSelection,
setDraftValue,
apply,
};
}
function render(container, controller, chrome, hooks = {}) {
shellController?.dispose();
shellController = undefined;
const { state } = controller;
const entries = state.payload?.entries ?? [];
const shellClasses = [
'shell',
'tool-shell',
chrome.expanded ? 'expanded' : 'collapsed',
'config-shell',
chrome.hideSummaryRow ? 'host-framed' : '',
chrome.compact ? 'compact' : '',
].filter(Boolean).join(' ');
const settingsHtml = entries.map((entry, index) => {
const keyTitle = entry.label ?? formatKeyLabel(entry.key);
const description = entry.description ?? '';
const summary = getSettingSummary(entry);
let controlHtml;
if (entry.key === 'defaultShell') {
const currentShell = String(entry.value ?? '');
const shellOptions = getShellOptions(state.payload, currentShell);
const isCustomShell = !shellOptions.includes(currentShell);
const options = shellOptions
.map((shell) => `<option value="${escapeHtml(shell)}" ${shell === currentShell ? 'selected' : ''}>${escapeHtml(shell)}</option>`)
.join('');
controlHtml = `
<div class="setting-shell-control">
<select class="setting-inline-select" data-action="shell-select" data-key-index="${index}">
${options}
<option value="__custom__" ${isCustomShell ? 'selected' : ''}>Custom...</option>
</select>
<input class="setting-inline-input setting-shell-custom${isCustomShell ? '' : ' hidden'}" data-action="shell-custom" data-key-index="${index}" type="text" value="${escapeHtml(currentShell)}" placeholder="Type custom shell path"/>
</div>
`;
}
else if (entry.valueType === 'boolean') {
const checked = String(entry.value) === 'true' ? 'checked' : '';
controlHtml = `<label class="setting-switch"><input type="checkbox" data-action="toggle-boolean" data-key-index="${index}" ${checked}/><span class="config-boolean-slider"></span></label>`;
}
else if (entry.valueType === 'number') {
controlHtml = `<input class="setting-inline-input" data-action="edit-number" data-key-index="${index}" type="number" step="any" value="${escapeHtml(String(entry.value ?? ''))}"/>`;
}
else if (entry.valueType === 'array') {
controlHtml = `<button class="setting-inline-action" data-action="open-array" data-key-index="${index}">Edit</button>`;
}
else if (entry.valueType === 'null') {
controlHtml = `<span class="setting-inline-static">null</span>`;
}
else {
controlHtml = `<input class="setting-inline-input" data-action="edit-string" data-key-index="${index}" type="text" value="${escapeHtml(String(entry.value ?? ''))}"/>`;
}
return `
<section class="setting-row">
<div class="setting-info">
<h3>${escapeHtml(keyTitle)}</h3>
${description ? `<p>${escapeHtml(description)}</p>` : ''}
<p class="setting-summary${summary ? '' : ' hidden'}" data-setting-summary-key="${escapeHtml(entry.key)}">${summary ? escapeHtml(summary) : ''}</p>
</div>
<div class="setting-control">${controlHtml}</div>
</section>
`;
}).join('');
container.innerHTML = `
<main id="tool-shell" class="${shellClasses}">
${renderCompactRow({ id: 'compact-toggle', label: 'View config', filename: 'Desktop Commander', variant: 'ready', expandable: true, expanded: chrome.expanded, interactive: true })}
<section class="panel config-card">
<div class="panel-content-wrapper">
<div class="settings-stack" aria-label="Desktop Commander settings">${settingsHtml}</div>
</div>
</section>
${renderArrayModalMarkup('List Setting')}
</main>
<div id="config-tooltip" class="config-tooltip" role="status" aria-live="polite" hidden></div>
`;
const toolShell = container.querySelector('#tool-shell');
const compactRow = container.querySelector('.compact-row--ready');
const getUpdatedEntryByKey = (key) => {
return controller.state.payload?.entries.find((item) => item.key === key);
};
const refreshSettingSummary = (key) => {
const summaryElement = container.querySelector(`[data-setting-summary-key="${key}"]`);
if (!summaryElement) {
return;
}
const updatedEntry = getUpdatedEntryByKey(key);
const summary = updatedEntry ? getSettingSummary(updatedEntry) : null;
summaryElement.textContent = summary ?? '';
summaryElement.classList.toggle('hidden', !summary);
};
const emitConfigChanged = (key, fallbackValue) => {
const updatedEntry = getUpdatedEntryByKey(key);
refreshSettingSummary(key);
hooks.onConfigChanged?.({
key,
value: updatedEntry ? updatedEntry.value : fallbackValue,
});
};
const emitTooltip = (result) => {
if (result.tooltip) {
hooks.onTooltip?.(result.tooltip);
}
};
const arrayModal = createArrayModalController({
container,
parseEntryItems: (entry) => parseDraftArrayValues(stringifyValueForInput(entry.value)),
formatEntryTitle: (entry) => entry.label ?? formatKeyLabel(entry.key),
onSave: async (changedKey, items) => {
controller.setSelection(changedKey);
controller.setDraftValue(items.join('\n'));
const result = await controller.apply();
emitTooltip(result);
if (result.ok) {
emitConfigChanged(changedKey, items);
}
},
});
container.querySelectorAll('[data-action]').forEach((element) => {
const target = element;
const action = target.dataset.action;
const keyIndex = Number(target.dataset.keyIndex);
const entry = entries[keyIndex];
if (!entry) {
return;
}
if (action === 'toggle-boolean') {
target.addEventListener('change', async () => {
const input = target;
const previousChecked = input.checked;
controller.setSelection(entry.key);
controller.setDraftValue(input.checked ? 'true' : 'false');
const result = await controller.apply();
emitTooltip(result);
const updatedEntry = getUpdatedEntryByKey(entry.key);
if (updatedEntry && typeof updatedEntry.value === 'boolean') {
input.checked = updatedEntry.value;
}
else if (!result.ok) {
input.checked = !previousChecked;
}
if (result.ok) {
emitConfigChanged(entry.key, input.checked);
}
});
return;
}
if (action === 'edit-number') {
target.addEventListener('blur', async () => {
const input = target;
controller.setSelection(entry.key);
controller.setDraftValue(input.value);
const result = await controller.apply();
emitTooltip(result);
const updatedEntry = getUpdatedEntryByKey(entry.key);
input.value = String(updatedEntry?.value ?? controller.state.draftValue);
if (result.ok) {
emitConfigChanged(entry.key, input.value);
}
});
return;
}
if (action === 'shell-select') {
target.addEventListener('change', async () => {
const select = target;
const customInput = container.querySelector(`input[data-action="shell-custom"][data-key-index="${keyIndex}"]`);
if (select.value === '__custom__') {
customInput?.classList.remove('hidden');
customInput?.focus();
return;
}
customInput?.classList.add('hidden');
controller.setSelection(entry.key);
controller.setDraftValue(select.value);
const result = await controller.apply();
emitTooltip(result);
const updatedEntry = getUpdatedEntryByKey(entry.key);
const shellValue = String(updatedEntry?.value ?? select.value);
const shellCustomInput = container.querySelector(`input[data-action="shell-custom"][data-key-index="${keyIndex}"]`);
if (shellCustomInput) {
shellCustomInput.value = shellValue;
}
if (result.ok) {
emitConfigChanged(entry.key, shellValue);
}
});
return;
}
if (action === 'shell-custom') {
const commitCustomShell = async () => {
const input = target;
if (!input.value.trim()) {
return;
}
controller.setSelection(entry.key);
controller.setDraftValue(input.value.trim());
const result = await controller.apply();
emitTooltip(result);
const updatedEntry = getUpdatedEntryByKey(entry.key);
input.value = String(updatedEntry?.value ?? controller.state.draftValue);
if (result.ok) {
emitConfigChanged(entry.key, input.value);
}
};
target.addEventListener('blur', () => {
void commitCustomShell();
});
target.addEventListener('keydown', (event) => {
if (event.key !== 'Enter') {
return;
}
event.preventDefault();
void commitCustomShell();
});
return;
}
if (action === 'edit-string') {
target.addEventListener('keydown', async (event) => {
if (event.key !== 'Enter') {
return;
}
event.preventDefault();
const input = target;
controller.setSelection(entry.key);
controller.setDraftValue(input.value.replace(/\r?\n/g, ' '));
const result = await controller.apply();
emitTooltip(result);
const updatedEntry = getUpdatedEntryByKey(entry.key);
input.value = String(updatedEntry?.value ?? controller.state.draftValue);
if (result.ok) {
emitConfigChanged(entry.key, input.value);
}
});
return;
}
if (action === 'open-array') {
target.addEventListener('click', () => {
const latestEntry = getUpdatedEntryByKey(entry.key) ?? entry;
arrayModal.open(latestEntry);
});
}
});
shellController = createCompactRowShellController({
shell: toolShell,
compactRow,
initialExpanded: chrome.expanded,
onToggle: (expanded) => {
chrome.expanded = expanded;
hooks.onExpandedChanged?.(expanded);
},
});
}
function markReady() {
document.body.classList.add('dc-ready');
}
export function bootstrapConfigEditorApp() {
const container = document.getElementById('app');
if (!container) {
return;
}
const bridge = createToolBridge();
const trackConfigUiEvent = createUiEventTracker((name, args) => bridge.callTool(name, args), {
component: CONFIG_EDITOR_COMPONENT,
baseParams: { origin: 'ui' },
});
const controller = createConfigEditorController((name, args) => bridge.callTool(name, args), trackConfigUiEvent);
const widgetState = createWidgetStateStorage(isConfigEditorPayload);
const chrome = {
hideSummaryRow: false,
compact: false,
expanded: true,
};
let configEditorShownEventSent = false;
let quietContextSupported = true;
let tooltipHideTimer = null;
const clearTooltipTimer = () => {
if (tooltipHideTimer !== null) {
window.clearTimeout(tooltipHideTimer);
tooltipHideTimer = null;
}
};
const showTooltip = (tooltip) => {
const tooltipElement = container.querySelector('#config-tooltip');
if (!tooltipElement) {
return;
}
clearTooltipTimer();
tooltipElement.className = `config-tooltip config-tooltip--${tooltip.tone}`;
tooltipElement.textContent = tooltip.message;
tooltipElement.hidden = false;
tooltipHideTimer = window.setTimeout(() => {
tooltipElement.hidden = true;
tooltipElement.textContent = '';
tooltipElement.className = 'config-tooltip';
tooltipHideTimer = null;
}, 2600);
};
const syncModelContext = (reason, change) => {
if (!quietContextSupported || !change) {
return;
}
app.updateModelContext({
content: [{ type: 'text', text: `Updated ${change.key} to ${JSON.stringify(change.value)} (${reason}).` }],
structuredContent: {
reason,
changedKey: change.key,
changedValue: change.value,
},
}).catch(() => {
// Host may not support updateModelContext; avoid repeated failed calls.
quietContextSupported = false;
});
};
let renderFrameId = null;
const scheduleRender = () => {
if (renderFrameId !== null) {
return;
}
renderFrameId = window.requestAnimationFrame(() => {
renderFrameId = null;
render(container, controller, chrome, {
onConfigChanged: (change) => {
if (controller.state.payload) {
widgetState.write(controller.state.payload);
}
syncModelContext('widget-edit', change);
},
onTooltip: showTooltip,
onExpandedChanged: (expanded) => {
trackConfigUiEvent(expanded ? 'expand' : 'collapse', {
tool_name: GET_CONFIG_TOOL_NAME,
expanded,
});
},
});
markReady();
});
};
scheduleRender();
const app = new App({ name: 'Desktop Commander Config Editor', version: '1.0.0' }, {}, { autoResize: true });
app.onteardown = async () => {
shellController?.dispose();
if (renderFrameId !== null) {
window.cancelAnimationFrame(renderFrameId);
renderFrameId = null;
}
clearTooltipTimer();
return {};
};
const applyPayload = (payload) => {
controller.setPayload(payload);
widgetState.write(payload);
scheduleRender();
if (!configEditorShownEventSent) {
configEditorShownEventSent = true;
// One-shot impression event for get_config UI card visibility.
trackConfigUiEvent('config_editor_shown', {
tool_name: GET_CONFIG_TOOL_NAME,
entry_count: payload.entries.length,
});
}
};
const refreshConfigFromServer = async () => {
try {
const result = await bridge.callTool('get_config', {});
const payload = controller.extractPayload(result);
if (payload) {
applyPayload(payload);
}
}
catch {
// Best-effort refresh only
}
};
app.ontoolresult = (result) => {
const payload = controller.extractPayload(result);
if (payload) {
applyPayload(payload);
}
};
app.ontoolcancelled = (params) => {
showTooltip({
message: params.reason ?? 'Tool was cancelled.',
tone: 'error',
});
};
void connectWithSharedHostContext({
app,
chrome,
onContextApplied: () => {
// Config editor should default to expanded in all hosts unless the
// host forces framed mode.
if (!chrome.hideSummaryRow) {
chrome.expanded = true;
}
scheduleRender();
},
onConnected: async () => {
const cachedPayload = widgetState.read();
if (cachedPayload) {
controller.setPayload(cachedPayload);
}
scheduleRender();
await refreshConfigFromServer();
},
}).catch(() => {
scheduleRender();
window.setTimeout(() => {
showTooltip({
message: 'Failed to connect to host.',
tone: 'error',
});
}, 0);
});
window.addEventListener('beforeunload', () => {
shellController?.dispose();
clearTooltipTimer();
}, { once: true });
}