@wonderwhy-er/desktop-commander 0.2.40 → 0.2.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -548,11 +548,6 @@ export function bootstrapApp() {
548
548
  onConnected: () => {
549
549
  currentHostContext = app.getHostContext();
550
550
  pendingCachedPayload = widgetState.read() ?? undefined;
551
- window.setTimeout(() => {
552
- if (!initialStateResolved) {
553
- resolveInitialState(undefined, 'Preview unavailable after page refresh. Switch threads or re-run the tool.');
554
- }
555
- }, 8000);
556
551
  },
557
552
  }).catch(() => {
558
553
  renderStatusState(container, 'Failed to connect to host.');
@@ -5,7 +5,7 @@ function parseDirectoryEntries(content) {
5
5
  const hintLines = [];
6
6
  const entryLines = [];
7
7
  for (const line of lines) {
8
- if (/^\[(DIR|FILE|DENIED|WARNING)\]/.test(line.trim())) {
8
+ if (/^\[(DIR|FILE|DENIED|NOT_FOUND|WARNING)\]/.test(line.trim())) {
9
9
  entryLines.push(line.trim());
10
10
  }
11
11
  else if (entryLines.length === 0) {
@@ -25,6 +25,7 @@ function parseDirectoryEntries(content) {
25
25
  fullPath: dirName,
26
26
  isDir: false,
27
27
  isDenied: false,
28
+ isNotFound: false,
28
29
  isWarning: true,
29
30
  warningText: msg,
30
31
  depth: parts.length,
@@ -33,13 +34,15 @@ function parseDirectoryEntries(content) {
33
34
  }
34
35
  const isDir = line.startsWith('[DIR]');
35
36
  const isDenied = line.startsWith('[DENIED]');
36
- const name = line.replace(/^\[(DIR|FILE|DENIED)\]\s*/, '');
37
+ const isNotFound = line.startsWith('[NOT_FOUND]');
38
+ const name = line.replace(/^\[(DIR|FILE|DENIED|NOT_FOUND)\]\s*/, '');
37
39
  const parts = name.replace(/\\/g, '/').split('/');
38
40
  flat.push({
39
41
  name,
40
42
  fullPath: name,
41
43
  isDir,
42
44
  isDenied,
45
+ isNotFound,
43
46
  isWarning: false,
44
47
  warningText: '',
45
48
  depth: parts.length - 1,
@@ -53,6 +56,7 @@ function parseDirectoryEntries(content) {
53
56
  name: baseName,
54
57
  isDir: item.isDir,
55
58
  isDenied: item.isDenied,
59
+ isNotFound: item.isNotFound,
56
60
  isWarning: item.isWarning,
57
61
  warningText: item.warningText,
58
62
  children: [],
@@ -85,6 +89,9 @@ function renderDirTree(entries, rootPath) {
85
89
  if (item.isDenied) {
86
90
  return `<div class="dir-entry"><span class="dir-icon">🚫</span> <span class="dir-name-denied">${escapeHtml(item.name)}</span></div>`;
87
91
  }
92
+ if (item.isNotFound) {
93
+ return `<div class="dir-entry"><span class="dir-icon">❓</span> <span class="dir-name-denied">${escapeHtml(item.name)}</span></div>`;
94
+ }
88
95
  if (item.isDir) {
89
96
  const hasChildren = item.children.length > 0;
90
97
  const chevron = `<span class="dir-chevron${hasChildren ? ' expanded' : ''}">${hasChildren ? '▼' : '▶'}</span>`;
@@ -74,15 +74,26 @@ const handlerRegistry = {
74
74
  },
75
75
  },
76
76
  unsupported: {
77
- getCapabilities: () => ({
78
- supportsPreview: false,
79
- canCopy: false,
80
- canOpenInFolder: true,
81
- }),
82
- renderBody: () => ({
83
- notice: 'Preview is not available for this file type.',
84
- html: '<div class="panel-content source-content"></div>',
85
- }),
77
+ getCapabilities: (payload) => {
78
+ const hasRawContent = stripReadStatusLine(payload.content).trim().length > 0;
79
+ return {
80
+ supportsPreview: hasRawContent,
81
+ canCopy: hasRawContent,
82
+ canOpenInFolder: !isLikelyUrl(payload.filePath),
83
+ };
84
+ },
85
+ renderBody: ({ payload }) => {
86
+ const rawContent = stripReadStatusLine(payload.content);
87
+ if (rawContent.trim().length === 0) {
88
+ return {
89
+ notice: 'Preview is not available for this file type.',
90
+ html: '<div class="panel-content source-content"></div>',
91
+ };
92
+ }
93
+ return {
94
+ html: `<div class="panel-content source-content">${renderRawFallback(rawContent)}</div>`,
95
+ };
96
+ },
86
97
  },
87
98
  };
88
99
  export function getFileTypeCapabilities(payload) {
@@ -41,6 +41,12 @@ export function extractToolText(value) {
41
41
  }
42
42
  return undefined;
43
43
  }
44
+ function extractStructuredContentText(value) {
45
+ if (!isObjectRecord(value)) {
46
+ return undefined;
47
+ }
48
+ return typeof value.content === 'string' ? value.content : undefined;
49
+ }
44
50
  export function extractRenderPayload(value) {
45
51
  if (!isObjectRecord(value)) {
46
52
  return undefined;
@@ -52,7 +58,10 @@ export function extractRenderPayload(value) {
52
58
  : null;
53
59
  if (!meta)
54
60
  return undefined;
55
- const text = extractToolText(value) ?? extractToolText(value.structuredContent) ?? '';
61
+ const text = extractStructuredContentText(value.structuredContent)
62
+ ?? extractToolText(value)
63
+ ?? extractToolText(value.structuredContent)
64
+ ?? '';
56
65
  return buildRenderPayload(meta, text);
57
66
  }
58
67
  export function assertSuccessfulEditBlockResult(result) {
@@ -33,6 +33,9 @@ declare class FeatureFlagManager {
33
33
  * Wait for fresh flags to be fetched from network.
34
34
  * Use this when you need to ensure flags are loaded before making decisions
35
35
  * (e.g., A/B test assignments for new users who don't have a cache yet)
36
+ *
37
+ * Has a hard timeout to prevent blocking MCP startup if the fetch hangs.
38
+ * See: https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/465
36
39
  */
37
40
  waitForFreshFlags(): Promise<void>;
38
41
  /**
@@ -93,10 +93,24 @@ class FeatureFlagManager {
93
93
  * Wait for fresh flags to be fetched from network.
94
94
  * Use this when you need to ensure flags are loaded before making decisions
95
95
  * (e.g., A/B test assignments for new users who don't have a cache yet)
96
+ *
97
+ * Has a hard timeout to prevent blocking MCP startup if the fetch hangs.
98
+ * See: https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/465
96
99
  */
97
100
  async waitForFreshFlags() {
98
101
  if (this.freshFetchPromise) {
99
- await this.freshFetchPromise;
102
+ let safetyTimeoutHandle;
103
+ try {
104
+ const safetyTimeout = new Promise((resolve) => {
105
+ safetyTimeoutHandle = setTimeout(resolve, 5000);
106
+ });
107
+ await Promise.race([this.freshFetchPromise, safetyTimeout]);
108
+ }
109
+ finally {
110
+ if (safetyTimeoutHandle) {
111
+ clearTimeout(safetyTimeoutHandle);
112
+ }
113
+ }
100
114
  }
101
115
  }
102
116
  /**
@@ -127,17 +141,26 @@ class FeatureFlagManager {
127
141
  * Fetch flags from remote URL
128
142
  */
129
143
  async fetchFlags() {
144
+ const FETCH_TIMEOUT_MS = 3000;
145
+ const controller = new AbortController();
146
+ const abortTimeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
147
+ let hardTimeoutHandle;
130
148
  try {
131
149
  // Don't log here - runs async and can interfere with MCP clients
132
- const controller = new AbortController();
133
- const timeout = setTimeout(() => controller.abort(), 5000);
134
- const response = await fetch(this.flagUrl, {
150
+ // Use Promise.race as a hard timeout safety net.
151
+ // On some platforms (Windows + Node 24 / undici 7.x), AbortController.abort()
152
+ // fails to interrupt an in-progress TCP connect — the fetch hangs until the
153
+ // OS-level TCP timeout (~30s on Windows). Promise.race guarantees we reject
154
+ // at the JS level regardless of AbortController behavior.
155
+ // See: https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/465
156
+ const fetchPromise = fetch(this.flagUrl, {
135
157
  signal: controller.signal,
136
158
  headers: {
137
159
  'Cache-Control': 'no-cache',
138
160
  }
139
161
  });
140
- clearTimeout(timeout);
162
+ const hardTimeout = new Promise((_, reject) => hardTimeoutHandle = setTimeout(() => reject(new Error('Feature flags fetch timed out')), FETCH_TIMEOUT_MS));
163
+ const response = await Promise.race([fetchPromise, hardTimeout]);
141
164
  if (!response.ok) {
142
165
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
143
166
  }
@@ -155,6 +178,12 @@ class FeatureFlagManager {
155
178
  logger.debug('Failed to fetch feature flags:', error.message);
156
179
  // Continue with cached values
157
180
  }
181
+ finally {
182
+ clearTimeout(abortTimeout);
183
+ if (hardTimeoutHandle) {
184
+ clearTimeout(hardTimeoutHandle);
185
+ }
186
+ }
158
187
  }
159
188
  /**
160
189
  * Save flags to local cache
@@ -25,8 +25,10 @@ export class ExcelFileHandler {
25
25
  const paginationInfo = totalRows > returnedRows
26
26
  ? `\n[Showing rows ${(options?.offset || 0) + 1}-${(options?.offset || 0) + returnedRows} of ${totalRows} total. Use offset/length to paginate.]`
27
27
  : '';
28
+ const sheetHasSpace = /\s/.test(sheetName);
29
+ const exampleSheet = sheetHasSpace ? sheetName : 'Sheet1';
28
30
  const content = `[Sheet: '${sheetName}' from ${path}]${paginationInfo}
29
- [To MODIFY cells: use edit_block with range param, e.g., edit_block(path, {range: "Sheet1!E5", content: [[newValue]]})]
31
+ [To MODIFY cells: use edit_block with range param, e.g., edit_block(path, {range: "${exampleSheet}!E5", content: [[newValue]]}). read_file accepts the same range form, or pass sheet + range separately.]
30
32
 
31
33
  ${JSON.stringify(data)}`;
32
34
  return {
@@ -260,6 +262,17 @@ ${JSON.stringify(data)}`;
260
262
  if (workbook.worksheets.length === 0) {
261
263
  return { sheetName: '', data: [], totalRows: 0, returnedRows: 0 };
262
264
  }
265
+ // Accept range with embedded sheet prefix (parity with edit_block).
266
+ // E.g. range:"Sheet1!A1:B2" or "'My Sheet'!A1" — strip the sheet
267
+ // prefix and, when the caller did not pass an explicit sheet, use it.
268
+ let cellRangeOnly = range;
269
+ if (range && range.includes('!')) {
270
+ const [sheetFromRange, cellsFromRange] = this.parseRange(range);
271
+ cellRangeOnly = cellsFromRange ?? undefined;
272
+ if (sheetRef === undefined && sheetFromRange) {
273
+ sheetRef = sheetFromRange;
274
+ }
275
+ }
263
276
  // Find target worksheet
264
277
  let worksheet;
265
278
  let sheetName;
@@ -287,8 +300,8 @@ ${JSON.stringify(data)}`;
287
300
  let endRow = worksheet.actualRowCount || 1;
288
301
  let startCol = 1;
289
302
  let endCol = worksheet.actualColumnCount || 1;
290
- if (range) {
291
- const parsed = this.parseCellRange(range);
303
+ if (cellRangeOnly) {
304
+ const parsed = this.parseCellRange(cellRangeOnly);
292
305
  startRow = parsed.startRow;
293
306
  startCol = parsed.startCol;
294
307
  if (parsed.endRow)
@@ -386,7 +399,14 @@ ${JSON.stringify(data)}`;
386
399
  }
387
400
  parseRange(range) {
388
401
  if (range.includes('!')) {
389
- const [sheetName, cellRange] = range.split('!');
402
+ const idx = range.indexOf('!');
403
+ let sheetName = range.slice(0, idx);
404
+ const cellRange = range.slice(idx + 1);
405
+ // Strip Excel-native single quotes around sheet names with spaces:
406
+ // 'My Sheet'!A1 → My Sheet, A1
407
+ if (sheetName.length >= 2 && sheetName.startsWith("'") && sheetName.endsWith("'")) {
408
+ sheetName = sheetName.slice(1, -1).replace(/''/g, "'");
409
+ }
390
410
  return [sheetName, cellRange];
391
411
  }
392
412
  return [range, null];
@@ -395,7 +415,8 @@ ${JSON.stringify(data)}`;
395
415
  // Parse A1 or A1:C10 format
396
416
  const match = range.match(/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i);
397
417
  if (!match) {
398
- throw new Error(`Invalid cell range: ${range}`);
418
+ throw new Error(`Invalid cell range: "${range}". Expected forms: "A1", "A1:C10", or "SheetName!A1:C10" ` +
419
+ `(single-quote sheet names containing spaces: "'My Sheet'!A1:C10").`);
399
420
  }
400
421
  const startCol = this.columnToNumber(match[1]);
401
422
  const startRow = parseInt(match[2], 10);
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.40";
1
+ export declare const VERSION = "0.2.41";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.40';
1
+ export const VERSION = '0.2.41';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.2.40",
3
+ "version": "0.2.41",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "mcpName": "io.github.wonderwhy-er/desktop-commander",
6
6
  "license": "MIT",