Skip to content

Commit cd4a80d

Browse files
edgarsskoreclaude
andauthored
feat(telemetry): attribute file-tool calls to ui vs llm via call_origin (#524)
Mirror the existing set_config_value call_origin pattern for read_file, write_file, edit_block, and list_directory. The file-preview UI passes origin:'ui' when it fires these tools to refresh/navigate (pagination, folder expansion, link resolution, in-preview edits); the LLM's calls default to 'llm'. server.ts records telemetryData.call_origin on the server_call_tool event so analytics can separate UI-refresh reads from genuine model-driven opens. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 685b21f commit cd4a80d

5 files changed

Lines changed: 30 additions & 8 deletions

File tree

src/server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
12541254
telemetryData.set_config_value_key_name = (args as any).key;
12551255
telemetryData.call_origin = (args as any).origin === 'ui' ? 'ui' : 'llm';
12561256
}
1257+
// Attribute file-surface tool calls to the file-preview UI vs the LLM.
1258+
// The UI passes origin:'ui' when it fires these tools to refresh/navigate
1259+
// (pagination, folder expansion, link resolution, in-preview edits); the
1260+
// first read_file that opens a file comes from the LLM and is recorded as
1261+
// 'llm'. Lets us separate UI-refresh churn from genuine model-driven reads.
1262+
if (
1263+
(name === 'read_file' || name === 'write_file' || name === 'edit_block' || name === 'list_directory') &&
1264+
args && typeof args === 'object'
1265+
) {
1266+
telemetryData.call_origin = (args as any).origin === 'ui' ? 'ui' : 'llm';
1267+
}
12571268
if (name === 'get_prompts' && args && typeof args === 'object') {
12581269
const promptArgs = args as any;
12591270
telemetryData.action = promptArgs.action;

src/tools/schemas.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export const ReadFileArgsSchema = z.object({
5252
length: z.number().optional().default(1000),
5353
sheet: z.string().optional(), // String only for MCP client compatibility (Cursor doesn't support union types in JSON Schema)
5454
range: z.string().optional(),
55-
options: z.record(z.any()).optional()
55+
options: z.record(z.any()).optional(),
56+
// Whether the call came from the file-preview UI (refresh/navigation) or the
57+
// LLM. Used only for telemetry attribution; see call_origin in server.ts.
58+
origin: z.enum(['ui', 'llm']).optional(),
5659
});
5760

5861
export const ReadMultipleFilesArgsSchema = z.object({
@@ -63,6 +66,8 @@ export const WriteFileArgsSchema = z.object({
6366
path: z.string(),
6467
content: z.string(),
6568
mode: z.enum(['rewrite', 'append']).default('rewrite'),
69+
// Telemetry attribution: 'ui' when fired by the file-preview UI, else 'llm'.
70+
origin: z.enum(['ui', 'llm']).optional(),
6671
});
6772

6873
// PDF modification schemas - exported for reuse
@@ -111,6 +116,8 @@ export const CreateDirectoryArgsSchema = z.object({
111116
export const ListDirectoryArgsSchema = z.object({
112117
path: z.string(),
113118
depth: z.number().optional().default(2),
119+
// Telemetry attribution: 'ui' when fired by the file-preview UI, else 'llm'.
120+
origin: z.enum(['ui', 'llm']).optional(),
114121
});
115122

116123
export const MoveFileArgsSchema = z.object({
@@ -136,7 +143,9 @@ export const EditBlockArgsSchema = z.object({
136143
// Structured file range rewrite (Excel, etc.)
137144
range: z.string().optional(),
138145
content: z.any().optional(),
139-
options: z.record(z.any()).optional()
146+
options: z.record(z.any()).optional(),
147+
// Telemetry attribution: 'ui' when fired by the file-preview UI, else 'llm'.
148+
origin: z.enum(['ui', 'llm']).optional(),
140149
}).refine(
141150
data => {
142151
// Helper to check if value is actually provided (not undefined, not empty string)

src/ui/file-preview/src/directory-controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export function attachDirectoryHandlers(options: {
189189
loadMoreBtn.querySelector('.dir-warning-text')!.textContent = 'Loading…';
190190
(loadMoreBtn as HTMLButtonElement).disabled = true;
191191
try {
192-
const result = await options.callTool?.('list_directory', { path: loadPath, depth: 1 });
192+
const result = await options.callTool?.('list_directory', { path: loadPath, depth: 1, origin: 'ui' });
193193
const text = extractToolText(result) ?? '';
194194
if (text) {
195195
const parsed = parseDirectoryEntries(text);
@@ -238,7 +238,7 @@ export function attachDirectoryHandlers(options: {
238238
}
239239
if (chevron) chevron.textContent = '⏳';
240240
try {
241-
const result = await options.callTool?.('list_directory', { path: fullPath, depth: 2 });
241+
const result = await options.callTool?.('list_directory', { path: fullPath, depth: 2, origin: 'ui' });
242242
const text = extractToolText(result) ?? '';
243243
if (text) {
244244
target.dataset.loaded = 'true';
@@ -264,7 +264,7 @@ export function attachDirectoryHandlers(options: {
264264
if (target.classList.contains('dir-row-file')) {
265265
target.classList.add('dir-loading');
266266
try {
267-
const result = await options.callTool?.('read_file', { path: fullPath });
267+
const result = await options.callTool?.('read_file', { path: fullPath, origin: 'ui' });
268268
if (!result || typeof result !== 'object' || result === null) {
269269
return;
270270
}

src/ui/file-preview/src/markdown/controller.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ export function createMarkdownController(dependencies: MarkdownControllerDepende
373373
const rawResult = await dependencies.callTool?.('read_file', {
374374
path: filePath,
375375
...(typeof length === 'number' ? { offset: offset ?? 0, length } : {}),
376+
origin: 'ui',
376377
});
377378
return { rawResult, payload: extractRenderPayload(rawResult) ?? null };
378379
}
@@ -466,7 +467,7 @@ export function createMarkdownController(dependencies: MarkdownControllerDepende
466467

467468
for (const ancestor of ancestors) {
468469
try {
469-
const result = await dependencies.callTool?.('list_directory', { path: ancestor, depth: 1 });
470+
const result = await dependencies.callTool?.('list_directory', { path: ancestor, depth: 1, origin: 'ui' });
470471
const text = extractToolText(result) ?? '';
471472
const entries = splitListingLines(text);
472473
if (entries.some((entry) => markers.has(entry))) {
@@ -827,6 +828,7 @@ export function createMarkdownController(dependencies: MarkdownControllerDepende
827828
old_string: block.old_string,
828829
new_string: block.new_string,
829830
expected_replacements: 1,
831+
origin: 'ui',
830832
});
831833
assertSuccessfulEditBlockResult(editResult);
832834
appliedCount++;

src/ui/file-preview/src/panel-actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ export function attachPanelActions(options: {
173173

174174
try {
175175
const readArgs = direction === 'before'
176-
? { path: options.payload.filePath, offset: 0, length: range.fromLine - 1 }
177-
: { path: options.payload.filePath, offset: range.toLine };
176+
? { path: options.payload.filePath, offset: 0, length: range.fromLine - 1, origin: 'ui' }
177+
: { path: options.payload.filePath, offset: range.toLine, origin: 'ui' };
178178

179179
const result = await options.callTool?.('read_file', readArgs);
180180
const newText = extractToolText(result);

0 commit comments

Comments
 (0)