@wonderwhy-er/desktop-commander 0.2.42 → 0.2.44
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.
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +22 -0
- package/dist/config-manager.d.ts +30 -1
- package/dist/config-manager.js +55 -8
- package/dist/handlers/filesystem-handlers.js +86 -52
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -0
- package/dist/remote-device/desktop-commander-integration.js +8 -2
- package/dist/remote-device/remote-channel.d.ts +15 -0
- package/dist/remote-device/remote-channel.js +136 -27
- package/dist/server.d.ts +5 -1
- package/dist/server.js +118 -27
- package/dist/terminal-manager.d.ts +18 -0
- package/dist/terminal-manager.js +84 -18
- package/dist/tools/edit.d.ts +1 -1
- package/dist/tools/edit.js +34 -26
- package/dist/tools/filesystem.d.ts +1 -0
- package/dist/tools/filesystem.js +23 -13
- package/dist/tools/fuzzySearch.d.ts +10 -20
- package/dist/tools/fuzzySearch.js +66 -105
- package/dist/tools/fuzzySearchCore.d.ts +52 -0
- package/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/tools/improved-process-tools.js +8 -1
- package/dist/tools/schemas.d.ts +33 -1
- package/dist/tools/schemas.js +61 -3
- package/dist/types.d.ts +3 -1
- package/dist/ui/config-editor/config-editor-runtime.js +49 -49
- package/dist/ui/config-editor/src/app.js +2 -11
- package/dist/ui/file-preview/preview-runtime.js +147 -146
- package/dist/ui/file-preview/src/app.js +172 -53
- package/dist/ui/file-preview/src/directory-controller.js +4 -4
- package/dist/ui/file-preview/src/file-type-handlers.js +2 -2
- package/dist/ui/file-preview/src/markdown/controller.js +4 -1
- package/dist/ui/file-preview/src/panel-actions.js +4 -4
- package/dist/ui/file-preview/src/payload-utils.js +23 -7
- package/dist/utils/capture.d.ts +2 -0
- package/dist/utils/capture.js +32 -7
- package/dist/utils/files/base.d.ts +2 -0
- package/dist/utils/files/image.js +1 -1
- package/dist/utils/files/text.js +24 -19
- package/dist/utils/open-browser.d.ts +1 -1
- package/dist/utils/open-browser.js +5 -2
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/utils/usageTracker.d.ts +5 -1
- package/dist/utils/usageTracker.js +14 -3
- package/dist/utils/welcome-onboarding.d.ts +1 -1
- package/dist/utils/welcome-onboarding.js +2 -2
- package/dist/utils/withTimeout.d.ts +17 -0
- package/dist/utils/withTimeout.js +33 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
|
@@ -19,7 +19,6 @@ import { attachPanelActions } from './panel-actions.js';
|
|
|
19
19
|
import { extractRenderPayload, extractToolText, getFileExtensionForAnalytics, isLikelyUrl, isPreviewStructuredContent } from './payload-utils.js';
|
|
20
20
|
let isExpanded = false;
|
|
21
21
|
let hideSummaryRow = false;
|
|
22
|
-
let previewShownFired = false;
|
|
23
22
|
let onRender;
|
|
24
23
|
let trackUiEvent;
|
|
25
24
|
let conflictDialogController;
|
|
@@ -128,43 +127,113 @@ const markdownController = createMarkdownController({
|
|
|
128
127
|
options.onCancel?.();
|
|
129
128
|
},
|
|
130
129
|
});
|
|
130
|
+
// Only read_file's own parameters may ride along on a re-read. Forwarding a
|
|
131
|
+
// different tool's args (e.g. edit_block's old_string) makes the server prepend
|
|
132
|
+
// an "unsupported params" warning that would pollute the pulled content.
|
|
133
|
+
const READ_ARG_KEYS = ['path', 'offset', 'length', 'sheet', 'range', 'isUrl'];
|
|
134
|
+
function pickReadArgs(args) {
|
|
135
|
+
if (!args || typeof args !== 'object') {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const src = args;
|
|
139
|
+
// edit_block calls the file "file_path"; read_file/write_file call it "path".
|
|
140
|
+
const path = typeof src.path === 'string'
|
|
141
|
+
? src.path
|
|
142
|
+
: typeof src.file_path === 'string'
|
|
143
|
+
? src.file_path
|
|
144
|
+
: undefined;
|
|
145
|
+
if (!path) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const out = { path };
|
|
149
|
+
for (const key of READ_ARG_KEYS) {
|
|
150
|
+
if (key !== 'path' && src[key] !== undefined) {
|
|
151
|
+
out[key] = src[key];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
// The host doesn't send the tool name with tool-input, so infer mutations from
|
|
157
|
+
// their arg shape: write_file carries `content`; edit_block `old_string`/
|
|
158
|
+
// `new_string`. Drives two things: timing (a mutation's tool-input arrives
|
|
159
|
+
// BEFORE the file changes, so those pull at tool-result time instead of input
|
|
160
|
+
// time) and telemetry (the pulled payload's sourceTool is re-stamped with the
|
|
161
|
+
// originating tool, since the pull itself is always a read_file).
|
|
162
|
+
function inferMutationTool(args) {
|
|
163
|
+
if (!args || typeof args !== 'object') {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const src = args;
|
|
167
|
+
if (src.old_string !== undefined || src.new_string !== undefined) {
|
|
168
|
+
return 'edit_block';
|
|
169
|
+
}
|
|
170
|
+
if (src.content !== undefined) {
|
|
171
|
+
return 'write_file';
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
// Bound the RPC read so a stalled host response resolves to onFail instead of
|
|
176
|
+
// hanging. Kept under the loading watchdog (PREVIEW_WATCHDOG_MS) so the pull
|
|
177
|
+
// gets its chance to fail before the failure row appears.
|
|
178
|
+
const PULL_TIMEOUT_MS = 8000;
|
|
179
|
+
// One in-flight pull per path — the input-time pull and a tool-result-triggered
|
|
180
|
+
// pull for the same call would otherwise read the file twice.
|
|
181
|
+
let pullInFlightPath;
|
|
131
182
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* re-fetch them from here (no isUrl flag on the refresh path), so skip.
|
|
183
|
+
* The widget's render path: pull content + metadata over the RPC channel
|
|
184
|
+
* (`app.callServerTool` with origin:'ui'). The notification channel is not used
|
|
185
|
+
* for rendering — the host may strip structuredContent from it or swallow it
|
|
186
|
+
* entirely (images). `args` reproduces the original read exactly
|
|
187
|
+
* (offset/length/sheet/range/isUrl) so partial reads stay faithful.
|
|
138
188
|
*/
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
189
|
+
async function pullPayloadByArgs(args, onReady, onFail) {
|
|
190
|
+
const filePath = typeof args.path === 'string' ? args.path : undefined;
|
|
191
|
+
if (!filePath) {
|
|
192
|
+
onFail();
|
|
193
|
+
return;
|
|
142
194
|
}
|
|
143
|
-
if (
|
|
144
|
-
return
|
|
195
|
+
if (pullInFlightPath === filePath) {
|
|
196
|
+
return;
|
|
145
197
|
}
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
async function readAndResolvePayload(payload, onReady) {
|
|
198
|
+
pullInFlightPath = filePath;
|
|
149
199
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
200
|
+
// The host can hold the RPC response indefinitely (e.g. it preempts
|
|
201
|
+
// delivery to inline-render an image), so callServerTool would await
|
|
202
|
+
// forever with no throw. Race a timeout so the pull settles either way.
|
|
203
|
+
const raw = await Promise.race([
|
|
204
|
+
callToolIfReady('read_file', { ...args, origin: 'ui' }),
|
|
205
|
+
new Promise((_resolve, reject) => {
|
|
206
|
+
setTimeout(() => reject(new Error(`RPC read_file did not respond within ${PULL_TIMEOUT_MS}ms`)), PULL_TIMEOUT_MS);
|
|
207
|
+
}),
|
|
208
|
+
]);
|
|
209
|
+
const payload = extractRenderPayload(raw);
|
|
210
|
+
if (payload) {
|
|
211
|
+
onReady(payload);
|
|
159
212
|
return;
|
|
160
213
|
}
|
|
161
214
|
}
|
|
162
215
|
catch {
|
|
163
|
-
//
|
|
216
|
+
// Timed out or rejected — fall through to the caller's fallback.
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
pullInFlightPath = undefined;
|
|
220
|
+
}
|
|
221
|
+
onFail();
|
|
222
|
+
}
|
|
223
|
+
// If a loading state hasn't resolved into a real render within this window, the
|
|
224
|
+
// host most likely never delivered the tool result / RPC response (e.g. it
|
|
225
|
+
// preempted delivery to inline-render an image). Rather than sit on "Preparing
|
|
226
|
+
// preview…" forever, we surface a failure row.
|
|
227
|
+
const PREVIEW_WATCHDOG_MS = 10000;
|
|
228
|
+
let previewWatchdogId;
|
|
229
|
+
function clearPreviewWatchdog() {
|
|
230
|
+
if (previewWatchdogId !== undefined) {
|
|
231
|
+
clearTimeout(previewWatchdogId);
|
|
232
|
+
previewWatchdogId = undefined;
|
|
164
233
|
}
|
|
165
|
-
onReady(payload);
|
|
166
234
|
}
|
|
167
235
|
function renderStatusState(container, message) {
|
|
236
|
+
clearPreviewWatchdog();
|
|
168
237
|
container.innerHTML = `
|
|
169
238
|
<main class="shell">
|
|
170
239
|
${renderCompactRow({ label: message, variant: 'status', interactive: false })}
|
|
@@ -179,8 +248,16 @@ function renderLoadingState(container) {
|
|
|
179
248
|
</main>
|
|
180
249
|
`;
|
|
181
250
|
document.body.classList.add('dc-ready');
|
|
251
|
+
// Fall back to a failure row if nothing renders in time. Any successful
|
|
252
|
+
// render (renderApp) or explicit status clears this first.
|
|
253
|
+
clearPreviewWatchdog();
|
|
254
|
+
previewWatchdogId = setTimeout(() => {
|
|
255
|
+
previewWatchdogId = undefined;
|
|
256
|
+
renderStatusState(container, 'Unable to generate preview in this environment.');
|
|
257
|
+
}, PREVIEW_WATCHDOG_MS);
|
|
182
258
|
}
|
|
183
259
|
export function renderApp(container, payload, htmlMode = 'rendered', expandedState = false) {
|
|
260
|
+
clearPreviewWatchdog();
|
|
184
261
|
isExpanded = expandedState;
|
|
185
262
|
currentHtmlMode = htmlMode;
|
|
186
263
|
shellController?.dispose();
|
|
@@ -314,13 +391,6 @@ export function renderApp(container, payload, htmlMode = 'rendered', expandedSta
|
|
|
314
391
|
onRender,
|
|
315
392
|
});
|
|
316
393
|
onRender?.();
|
|
317
|
-
if (!previewShownFired) {
|
|
318
|
-
previewShownFired = true;
|
|
319
|
-
trackUiEvent?.('preview_shown', {
|
|
320
|
-
file_type: payload.fileType,
|
|
321
|
-
file_extension: fileExtension,
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
394
|
}
|
|
325
395
|
export function bootstrapApp() {
|
|
326
396
|
const container = document.getElementById('app');
|
|
@@ -376,6 +446,14 @@ export function bootstrapApp() {
|
|
|
376
446
|
renderApp(container, currentPayload, currentHtmlMode, isExpanded);
|
|
377
447
|
};
|
|
378
448
|
let pendingCachedPayload;
|
|
449
|
+
let lastToolInputArgs;
|
|
450
|
+
// The mutation tool that produced the current tool call, if any — stamped
|
|
451
|
+
// onto the pulled payload so telemetry attributes to write_file/edit_block
|
|
452
|
+
// rather than the read_file pull that rendered it.
|
|
453
|
+
let lastMutationTool;
|
|
454
|
+
// True once the current tool call has been rendered (via the input-time pull),
|
|
455
|
+
// so a late tool-result notification doesn't trigger a redundant second pull.
|
|
456
|
+
let renderedForCurrentInput = false;
|
|
379
457
|
let initialStateResolved = false;
|
|
380
458
|
const resolveInitialState = (payload, message) => {
|
|
381
459
|
if (initialStateResolved) {
|
|
@@ -423,6 +501,13 @@ export function bootstrapApp() {
|
|
|
423
501
|
});
|
|
424
502
|
app.ontoolinput = (params) => {
|
|
425
503
|
const requestedPath = typeof params.arguments?.path === 'string' ? params.arguments.path : undefined;
|
|
504
|
+
const readArgs = pickReadArgs(params.arguments);
|
|
505
|
+
lastMutationTool = inferMutationTool(params.arguments);
|
|
506
|
+
if (readArgs) {
|
|
507
|
+
// Retained so mutations (which pull at tool-result time) know what
|
|
508
|
+
// to re-read — faithfully reproducing offset/length/sheet/range.
|
|
509
|
+
lastToolInputArgs = readArgs;
|
|
510
|
+
}
|
|
426
511
|
if (!initialStateResolved
|
|
427
512
|
&& pendingCachedPayload
|
|
428
513
|
&& requestedPath
|
|
@@ -434,38 +519,71 @@ export function bootstrapApp() {
|
|
|
434
519
|
}
|
|
435
520
|
renderLoadingState(container);
|
|
436
521
|
onRender?.();
|
|
522
|
+
renderedForCurrentInput = false;
|
|
523
|
+
// All previews render from the widget's own origin:'ui' RPC read — the
|
|
524
|
+
// notification channel is unreliable (the host strips structuredContent,
|
|
525
|
+
// and for images swallows the tool-result entirely to inline-render).
|
|
526
|
+
// tool-input always fires and carries the path + read args, so pull here.
|
|
527
|
+
// Exception: mutations (write_file/edit_block) — their tool-input arrives
|
|
528
|
+
// before the file changes, so they pull at tool-result time instead.
|
|
529
|
+
if (readArgs && !lastMutationTool) {
|
|
530
|
+
void pullPayloadByArgs(readArgs, (p) => {
|
|
531
|
+
renderedForCurrentInput = true;
|
|
532
|
+
if (initialStateResolved) {
|
|
533
|
+
renderAndSync(getEffectiveIncomingPayload(p));
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
resolveInitialState(getEffectiveIncomingPayload(p));
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
// Pull failed/timed out — the loading watchdog shows the failure row.
|
|
540
|
+
() => { });
|
|
541
|
+
}
|
|
437
542
|
};
|
|
438
543
|
app.ontoolresult = (result) => {
|
|
439
544
|
pendingCachedPayload = undefined;
|
|
440
|
-
|
|
545
|
+
// Host-facing responses carry no structuredContent, so this notification
|
|
546
|
+
// only signals completion. Reads already rendered from their input-time
|
|
547
|
+
// pull; mutations (write_file/edit_block) pull now that the file changed.
|
|
548
|
+
if (renderedForCurrentInput) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
441
551
|
const message = extractToolText(result);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
552
|
+
const isError = result?.isError === true;
|
|
553
|
+
const pullArgs = lastToolInputArgs
|
|
554
|
+
?? (currentPayload?.filePath ? { path: currentPayload.filePath } : undefined);
|
|
555
|
+
const deliver = (pulled) => {
|
|
556
|
+
renderedForCurrentInput = true;
|
|
557
|
+
// The pull is always a read_file; re-stamp the originating mutation
|
|
558
|
+
// tool so telemetry attributes to write_file/edit_block.
|
|
559
|
+
const p = lastMutationTool ? { ...pulled, sourceTool: lastMutationTool } : pulled;
|
|
560
|
+
if (initialStateResolved) {
|
|
561
|
+
renderAndSync(getEffectiveIncomingPayload(p));
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
resolveInitialState(getEffectiveIncomingPayload(p));
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
const renderMessageFallback = () => {
|
|
568
|
+
if (!message) {
|
|
449
569
|
return;
|
|
450
570
|
}
|
|
451
|
-
if (
|
|
571
|
+
if (!initialStateResolved) {
|
|
452
572
|
resolveInitialState(undefined, message);
|
|
453
573
|
}
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
if (payload) {
|
|
457
|
-
if (needsContentRead(payload)) {
|
|
458
|
-
renderLoadingState(container);
|
|
459
|
-
void readAndResolvePayload(payload, (p) => renderAndSync(getEffectiveIncomingPayload(p)));
|
|
460
|
-
}
|
|
461
574
|
else {
|
|
462
|
-
|
|
575
|
+
renderStatusState(container, message);
|
|
576
|
+
onRender?.();
|
|
463
577
|
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
|
|
578
|
+
};
|
|
579
|
+
// A failed tool call has nothing worth re-reading — show its message.
|
|
580
|
+
if (!isError && pullArgs) {
|
|
581
|
+
renderLoadingState(container);
|
|
467
582
|
onRender?.();
|
|
583
|
+
void pullPayloadByArgs(pullArgs, deliver, renderMessageFallback);
|
|
584
|
+
return;
|
|
468
585
|
}
|
|
586
|
+
renderMessageFallback();
|
|
469
587
|
};
|
|
470
588
|
app.ontoolcancelled = (params) => {
|
|
471
589
|
resolveInitialState(undefined, params.reason ?? 'Tool was cancelled.');
|
|
@@ -484,6 +602,7 @@ export function bootstrapApp() {
|
|
|
484
602
|
}
|
|
485
603
|
};
|
|
486
604
|
const teardown = () => {
|
|
605
|
+
clearPreviewWatchdog();
|
|
487
606
|
shellController?.dispose();
|
|
488
607
|
shellController = undefined;
|
|
489
608
|
markdownController.disposeHandles();
|
|
@@ -128,7 +128,7 @@ export function attachDirectoryHandlers(options) {
|
|
|
128
128
|
const command = options.buildOpenInFolderCommand(openPath);
|
|
129
129
|
if (command) {
|
|
130
130
|
try {
|
|
131
|
-
await options.callTool?.('start_process', { command, timeout_ms: 12000 });
|
|
131
|
+
await options.callTool?.('start_process', { command, timeout_ms: 12000, origin: 'ui' });
|
|
132
132
|
}
|
|
133
133
|
catch {
|
|
134
134
|
// Keep UI stable if opening folder fails.
|
|
@@ -146,7 +146,7 @@ export function attachDirectoryHandlers(options) {
|
|
|
146
146
|
loadMoreBtn.querySelector('.dir-warning-text').textContent = 'Loading…';
|
|
147
147
|
loadMoreBtn.disabled = true;
|
|
148
148
|
try {
|
|
149
|
-
const result = await options.callTool?.('list_directory', { path: loadPath, depth: 1 });
|
|
149
|
+
const result = await options.callTool?.('list_directory', { path: loadPath, depth: 1, origin: 'ui' });
|
|
150
150
|
const text = extractToolText(result) ?? '';
|
|
151
151
|
if (text) {
|
|
152
152
|
const parsed = parseDirectoryEntries(text);
|
|
@@ -194,7 +194,7 @@ export function attachDirectoryHandlers(options) {
|
|
|
194
194
|
if (chevron)
|
|
195
195
|
chevron.textContent = '⏳';
|
|
196
196
|
try {
|
|
197
|
-
const result = await options.callTool?.('list_directory', { path: fullPath, depth: 2 });
|
|
197
|
+
const result = await options.callTool?.('list_directory', { path: fullPath, depth: 2, origin: 'ui' });
|
|
198
198
|
const text = extractToolText(result) ?? '';
|
|
199
199
|
if (text) {
|
|
200
200
|
target.dataset.loaded = 'true';
|
|
@@ -222,7 +222,7 @@ export function attachDirectoryHandlers(options) {
|
|
|
222
222
|
if (target.classList.contains('dir-row-file')) {
|
|
223
223
|
target.classList.add('dir-loading');
|
|
224
224
|
try {
|
|
225
|
-
const result = await options.callTool?.('read_file', { path: fullPath });
|
|
225
|
+
const result = await options.callTool?.('read_file', { path: fullPath, origin: 'ui' });
|
|
226
226
|
if (!result || typeof result !== 'object' || result === null) {
|
|
227
227
|
return;
|
|
228
228
|
}
|
|
@@ -16,13 +16,13 @@ function renderImageBody(payload) {
|
|
|
16
16
|
html: '<div class="panel-content source-content"></div>',
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
-
if (!payload.
|
|
19
|
+
if (!payload.content || payload.content.trim().length === 0) {
|
|
20
20
|
return {
|
|
21
21
|
notice: 'Preview is unavailable because image data is missing.',
|
|
22
22
|
html: '<div class="panel-content source-content"></div>',
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
const src = `data:${mimeType};base64,${payload.
|
|
25
|
+
const src = `data:${mimeType};base64,${payload.content}`;
|
|
26
26
|
return {
|
|
27
27
|
html: `<div class="panel-content image-content"><div class="image-preview"><img src="${escapeHtml(src)}" alt="${escapeHtml(payload.fileName)}" loading="eager" decoding="async"></div></div>`,
|
|
28
28
|
};
|
|
@@ -281,6 +281,7 @@ export function createMarkdownController(dependencies) {
|
|
|
281
281
|
const rawResult = await dependencies.callTool?.('read_file', {
|
|
282
282
|
path: filePath,
|
|
283
283
|
...(typeof length === 'number' ? { offset: offset ?? 0, length } : {}),
|
|
284
|
+
origin: 'ui',
|
|
284
285
|
});
|
|
285
286
|
return { rawResult, payload: extractRenderPayload(rawResult) ?? null };
|
|
286
287
|
}
|
|
@@ -360,7 +361,7 @@ export function createMarkdownController(dependencies) {
|
|
|
360
361
|
const markers = new Set(['[DIR] .git', '[DIR] .obsidian', '[FILE] package.json', '[FILE] pnpm-workspace.yaml', '[FILE] turbo.json']);
|
|
361
362
|
for (const ancestor of ancestors) {
|
|
362
363
|
try {
|
|
363
|
-
const result = await dependencies.callTool?.('list_directory', { path: ancestor, depth: 1 });
|
|
364
|
+
const result = await dependencies.callTool?.('list_directory', { path: ancestor, depth: 1, origin: 'ui' });
|
|
364
365
|
const text = extractToolText(result) ?? '';
|
|
365
366
|
const entries = splitListingLines(text);
|
|
366
367
|
if (entries.some((entry) => markers.has(entry))) {
|
|
@@ -387,6 +388,7 @@ export function createMarkdownController(dependencies) {
|
|
|
387
388
|
maxResults: 20,
|
|
388
389
|
earlyTermination: false,
|
|
389
390
|
literalSearch: true,
|
|
391
|
+
origin: 'ui',
|
|
390
392
|
});
|
|
391
393
|
const text = extractToolText(result) ?? '';
|
|
392
394
|
const filePaths = parseFileSearchResults(text);
|
|
@@ -681,6 +683,7 @@ export function createMarkdownController(dependencies) {
|
|
|
681
683
|
old_string: block.old_string,
|
|
682
684
|
new_string: block.new_string,
|
|
683
685
|
expected_replacements: 1,
|
|
686
|
+
origin: 'ui',
|
|
684
687
|
});
|
|
685
688
|
assertSuccessfulEditBlockResult(editResult);
|
|
686
689
|
appliedCount++;
|
|
@@ -95,7 +95,7 @@ export function attachPanelActions(options) {
|
|
|
95
95
|
file_extension: fileExtension,
|
|
96
96
|
});
|
|
97
97
|
try {
|
|
98
|
-
await options.callTool?.('start_process', { command, timeout_ms: 12000 });
|
|
98
|
+
await options.callTool?.('start_process', { command, timeout_ms: 12000, origin: 'ui' });
|
|
99
99
|
}
|
|
100
100
|
catch {
|
|
101
101
|
// Keep UI stable if opening folder fails.
|
|
@@ -116,7 +116,7 @@ export function attachPanelActions(options) {
|
|
|
116
116
|
file_extension: fileExtension,
|
|
117
117
|
});
|
|
118
118
|
try {
|
|
119
|
-
await options.callTool?.('start_process', { command, timeout_ms: 12000 });
|
|
119
|
+
await options.callTool?.('start_process', { command, timeout_ms: 12000, origin: 'ui' });
|
|
120
120
|
}
|
|
121
121
|
catch {
|
|
122
122
|
// Keep UI stable if opening editor fails.
|
|
@@ -144,8 +144,8 @@ export function attachPanelActions(options) {
|
|
|
144
144
|
});
|
|
145
145
|
try {
|
|
146
146
|
const readArgs = direction === 'before'
|
|
147
|
-
? { path: options.payload.filePath, offset: 0, length: range.fromLine - 1 }
|
|
148
|
-
: { path: options.payload.filePath, offset: range.toLine };
|
|
147
|
+
? { path: options.payload.filePath, offset: 0, length: range.fromLine - 1, origin: 'ui' }
|
|
148
|
+
: { path: options.payload.filePath, offset: range.toLine, origin: 'ui' };
|
|
149
149
|
const result = await options.callTool?.('read_file', readArgs);
|
|
150
150
|
const newText = extractToolText(result);
|
|
151
151
|
if (newText && typeof newText === 'string') {
|
|
@@ -41,11 +41,27 @@ export function extractToolText(value) {
|
|
|
41
41
|
}
|
|
42
42
|
return undefined;
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
// Join ALL non-empty text blocks, not just the first. Most reads return a
|
|
45
|
+
// single text block, but PDF reads return a summary block followed by
|
|
46
|
+
// per-page text blocks — taking only the first would drop the page text.
|
|
47
|
+
function extractJoinedToolText(value) {
|
|
45
48
|
if (!isObjectRecord(value)) {
|
|
46
49
|
return undefined;
|
|
47
50
|
}
|
|
48
|
-
|
|
51
|
+
const content = value.content;
|
|
52
|
+
if (!Array.isArray(content)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const texts = [];
|
|
56
|
+
for (const item of content) {
|
|
57
|
+
if (isObjectRecord(item)
|
|
58
|
+
&& item.type === 'text'
|
|
59
|
+
&& typeof item.text === 'string'
|
|
60
|
+
&& item.text.trim().length > 0) {
|
|
61
|
+
texts.push(item.text);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return texts.length > 0 ? texts.join('\n') : undefined;
|
|
49
65
|
}
|
|
50
66
|
export function extractRenderPayload(value) {
|
|
51
67
|
if (!isObjectRecord(value)) {
|
|
@@ -58,11 +74,11 @@ export function extractRenderPayload(value) {
|
|
|
58
74
|
: null;
|
|
59
75
|
if (!meta)
|
|
60
76
|
return undefined;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return buildRenderPayload(meta,
|
|
77
|
+
// Content always comes from the read output's content[] text blocks; the
|
|
78
|
+
// structuredContent alongside it is metadata-only. Images arrive as a base64
|
|
79
|
+
// text block too (origin:'ui' reads carry no image block, so the host won't
|
|
80
|
+
// inline-render and stall the RPC).
|
|
81
|
+
return buildRenderPayload(meta, extractJoinedToolText(value) ?? '');
|
|
66
82
|
}
|
|
67
83
|
export function assertSuccessfulEditBlockResult(result) {
|
|
68
84
|
if (!isObjectRecord(result)) {
|
package/dist/utils/capture.d.ts
CHANGED
package/dist/utils/capture.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { platform } from 'os';
|
|
2
2
|
import * as https from 'https';
|
|
3
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
3
4
|
import { configManager, isTelemetryDisabledValue } from '../config-manager.js';
|
|
4
|
-
import { currentClient, currentCallIsRemote } from '../server.js';
|
|
5
|
+
import { currentClient, currentCallIsRemote, currentRemoteClient } from '../server.js';
|
|
6
|
+
// Execution context for tool calls fired programmatically by the widget UIs
|
|
7
|
+
// (file preview, config editor), marked by args.origin === 'ui'. While code
|
|
8
|
+
// runs inside this context, capture() drops every event, so UI refresh churn
|
|
9
|
+
// (pull-by-path reads, in-preview saves, folder expansion, link search, ...)
|
|
10
|
+
// produces zero telemetry. AsyncLocalStorage (rather than a module-level flag)
|
|
11
|
+
// keeps attribution correct when a widget call interleaves with an agent call.
|
|
12
|
+
const uiOriginCallContext = new AsyncLocalStorage();
|
|
13
|
+
export function runInUiOriginCallContext(fn) {
|
|
14
|
+
return uiOriginCallContext.run(true, fn);
|
|
15
|
+
}
|
|
16
|
+
export function isInsideUiOriginCall() {
|
|
17
|
+
return uiOriginCallContext.getStore() === true;
|
|
18
|
+
}
|
|
5
19
|
let VERSION = 'unknown';
|
|
6
20
|
try {
|
|
7
21
|
const versionModule = await import('../version.js');
|
|
@@ -89,11 +103,14 @@ export const captureBase = async (captureURL, event, properties) => {
|
|
|
89
103
|
uniqueUserId = await configManager.getOrCreateClientId();
|
|
90
104
|
}
|
|
91
105
|
// Get current client information for all events
|
|
106
|
+
// For remote calls, attribute to the originating remote client (carried on
|
|
107
|
+
// the tool call) instead of the device's local currentClient.
|
|
108
|
+
const effectiveClient = currentCallIsRemote && currentRemoteClient ? currentRemoteClient : currentClient;
|
|
92
109
|
let clientContext = {};
|
|
93
|
-
if (
|
|
110
|
+
if (effectiveClient) {
|
|
94
111
|
clientContext = {
|
|
95
|
-
client_name:
|
|
96
|
-
client_version:
|
|
112
|
+
client_name: effectiveClient.name,
|
|
113
|
+
client_version: effectiveClient.version,
|
|
97
114
|
};
|
|
98
115
|
}
|
|
99
116
|
// Track if user saw onboarding page
|
|
@@ -265,11 +282,14 @@ const buildEventProperties = async (properties) => {
|
|
|
265
282
|
if (uniqueUserId === 'unknown') {
|
|
266
283
|
uniqueUserId = await configManager.getOrCreateClientId();
|
|
267
284
|
}
|
|
285
|
+
// For remote calls, attribute to the originating remote client (carried on
|
|
286
|
+
// the tool call) instead of the device's local currentClient.
|
|
287
|
+
const effectiveClient = currentCallIsRemote && currentRemoteClient ? currentRemoteClient : currentClient;
|
|
268
288
|
let clientContext = {};
|
|
269
|
-
if (
|
|
289
|
+
if (effectiveClient) {
|
|
270
290
|
clientContext = {
|
|
271
|
-
client_name:
|
|
272
|
-
client_version:
|
|
291
|
+
client_name: effectiveClient.name,
|
|
292
|
+
client_version: effectiveClient.version,
|
|
273
293
|
};
|
|
274
294
|
}
|
|
275
295
|
const sawOnboardingPage = await configManager.getValue('sawOnboardingPage');
|
|
@@ -421,6 +441,11 @@ const postTelemetryPayload = async (endpoint, payload) => {
|
|
|
421
441
|
// can be silently dropped. If we need delivery guarantees on short-lived paths,
|
|
422
442
|
// expose an awaitable variant or flush-before-exit hook.
|
|
423
443
|
export const capture = async (event, properties) => {
|
|
444
|
+
// Tool calls fired programmatically by the widget UIs must produce zero
|
|
445
|
+
// telemetry — drop every event raised while serving one.
|
|
446
|
+
if (isInsideUiOriginCall()) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
424
449
|
void (async () => {
|
|
425
450
|
try {
|
|
426
451
|
const eventProperties = await buildEventProperties(properties);
|
|
@@ -69,6 +69,8 @@ export interface ReadOptions {
|
|
|
69
69
|
range?: string;
|
|
70
70
|
/** Whether to include status messages (default: true) */
|
|
71
71
|
includeStatusMessage?: boolean;
|
|
72
|
+
/** Optional AbortSignal to cancel an in-flight read (frees fd/thread on timeout). */
|
|
73
|
+
signal?: AbortSignal;
|
|
72
74
|
}
|
|
73
75
|
/**
|
|
74
76
|
* Result from reading a file
|
|
@@ -14,7 +14,7 @@ export class ImageFileHandler {
|
|
|
14
14
|
}
|
|
15
15
|
async read(path, options) {
|
|
16
16
|
// Images are always read in full, ignoring offset and length
|
|
17
|
-
const buffer = await fs.readFile(path);
|
|
17
|
+
const buffer = await fs.readFile(path, { signal: options?.signal });
|
|
18
18
|
const content = buffer.toString('base64');
|
|
19
19
|
const mimeType = this.getMimeType(path);
|
|
20
20
|
return {
|