Skip to content

Commit 13bad5b

Browse files
authored
fix: expand Chronicle session_files/refs tracking to match VS Code tool names (#312285)
* fix: expand Chronicle session_files/refs tracking to match VS Code tool names * address review: add create_directory with dirPath extraction
1 parent a85cc16 commit 13bad5b

3 files changed

Lines changed: 241 additions & 13 deletions

File tree

extensions/copilot/src/extension/chronicle/common/sessionStoreTracking.ts

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,67 @@
77
* Helpers for extracting file paths and refs from tool calls.
88
*/
99

10-
/** Tools whose arguments contain a file path being modified. */
11-
const FILE_TRACKING_TOOLS = new Set(['apply_patch', 'str_replace_editor', 'create_file', 'create']);
12-
13-
/** GitHub MCP server tool prefix. */
14-
const GH_MCP_PREFIX = 'github-mcp-server-';
10+
/** Tools whose arguments contain a file path being modified or read. */
11+
const FILE_TRACKING_TOOLS = new Set([
12+
// VS Code model-facing tool names (from ToolName enum)
13+
'replace_string_in_file',
14+
'multi_replace_string_in_file',
15+
'insert_edit_into_file',
16+
'create_file',
17+
'create_directory',
18+
'edit_notebook_file',
19+
'apply_patch',
20+
'read_file',
21+
'view_image',
22+
'list_dir',
23+
// CLI-agent tool names (backward compat)
24+
'str_replace_editor',
25+
'create',
26+
]);
27+
28+
/** GitHub MCP server tool prefixes. */
29+
const GH_MCP_PREFIXES = ['mcp_github_', 'github-mcp-server-'];
1530

1631
/**
1732
* Extract absolute file path from tool arguments if available.
18-
* Handles both CLI-style (edit/create with `path`) and VS Code-style
19-
* (apply_patch with `patch`, str_replace_editor with `filePath`, create_file with `filePath`).
33+
* Handles both CLI-style (edit/create with `path`) and VS Code-style tools
34+
* that use `filePath`, as well as `apply_patch` which encodes paths in the patch input.
2035
* @internal Exported for testing.
2136
*/
2237
export function extractFilePath(toolName: string, toolArgs: unknown): string | undefined {
2338
if (!FILE_TRACKING_TOOLS.has(toolName)) { return undefined; }
2439
if (typeof toolArgs !== 'object' || toolArgs === null) { return undefined; }
2540
const args = toolArgs as Record<string, unknown>;
26-
// VS Code tools use 'filePath', CLI tools use 'path'
27-
const filePath = args.filePath ?? args.path;
28-
return typeof filePath === 'string' ? filePath : undefined;
41+
42+
// VS Code tools use 'filePath', CLI tools use 'path', list_dir uses 'path',
43+
// create_directory uses 'dirPath'
44+
const filePath = args.filePath ?? args.path ?? args.dirPath;
45+
if (typeof filePath === 'string') { return filePath; }
46+
47+
// multi_replace_string_in_file stores filePath in each replacement item
48+
if (toolName === 'multi_replace_string_in_file' && Array.isArray(args.replacements)) {
49+
const first = args.replacements[0];
50+
if (typeof first === 'object' && first !== null) {
51+
const fp = (first as Record<string, unknown>).filePath;
52+
if (typeof fp === 'string') { return fp; }
53+
}
54+
}
55+
56+
// apply_patch encodes file paths in the patch input text
57+
if (toolName === 'apply_patch' && typeof args.input === 'string') {
58+
return extractFirstFileFromPatch(args.input);
59+
}
60+
61+
return undefined;
62+
}
63+
64+
/**
65+
* Extract the first file path from an apply_patch input string.
66+
* Matches lines like `*** Update File: /path/to/file` or `*** Add File: /path`.
67+
*/
68+
function extractFirstFileFromPatch(input: string): string | undefined {
69+
const match = input.match(/^\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+)$/m);
70+
return match?.[1]?.trim();
2971
}
3072

3173
/**
@@ -137,7 +179,8 @@ export function extractRepoFromMcpTool(toolArgs: unknown): string | undefined {
137179

138180
/**
139181
* Check whether a tool name is a GitHub MCP server tool.
182+
* Matches both VS Code-style `mcp_github_*` and CLI-style `github-mcp-server-*` prefixes.
140183
*/
141184
export function isGitHubMcpTool(toolName: string): boolean {
142-
return toolName.startsWith(GH_MCP_PREFIX);
185+
return GH_MCP_PREFIXES.some(prefix => toolName.startsWith(prefix));
143186
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { describe, expect, it } from 'vitest';
7+
import { extractFilePath, extractRefsFromMcpTool, extractRefsFromTerminal, extractRepoFromMcpTool, isGitHubMcpTool } from '../sessionStoreTracking';
8+
9+
describe('extractFilePath', () => {
10+
it('extracts filePath from replace_string_in_file', () => {
11+
expect(extractFilePath('replace_string_in_file', { filePath: '/src/foo.ts', oldString: 'a', newString: 'b' }))
12+
.toBe('/src/foo.ts');
13+
});
14+
15+
it('extracts filePath from multi_replace_string_in_file replacements array', () => {
16+
expect(extractFilePath('multi_replace_string_in_file', {
17+
explanation: 'fix imports',
18+
replacements: [
19+
{ filePath: '/src/bar.ts', oldString: 'a', newString: 'b' },
20+
{ filePath: '/src/baz.ts', oldString: 'c', newString: 'd' },
21+
]
22+
})).toBe('/src/bar.ts');
23+
});
24+
25+
it('extracts filePath from insert_edit_into_file', () => {
26+
expect(extractFilePath('insert_edit_into_file', { filePath: '/src/edit.ts', code: '// new' }))
27+
.toBe('/src/edit.ts');
28+
});
29+
30+
it('extracts filePath from create_file', () => {
31+
expect(extractFilePath('create_file', { filePath: '/src/new.ts', content: '' }))
32+
.toBe('/src/new.ts');
33+
});
34+
35+
it('extracts filePath from edit_notebook_file', () => {
36+
expect(extractFilePath('edit_notebook_file', { filePath: '/nb.ipynb', editType: 'edit', cellId: 'c1' }))
37+
.toBe('/nb.ipynb');
38+
});
39+
40+
it('extracts filePath from read_file', () => {
41+
expect(extractFilePath('read_file', { filePath: '/src/read.ts', startLine: 1, endLine: 10 }))
42+
.toBe('/src/read.ts');
43+
});
44+
45+
it('extracts path from list_dir', () => {
46+
expect(extractFilePath('list_dir', { path: '/src' }))
47+
.toBe('/src');
48+
});
49+
50+
it('extracts dirPath from create_directory', () => {
51+
expect(extractFilePath('create_directory', { dirPath: '/src/new-dir' }))
52+
.toBe('/src/new-dir');
53+
});
54+
55+
it('extracts file path from apply_patch input text', () => {
56+
const input = '*** Begin Patch\n*** Update File: /src/hello.ts\n@@class Foo\n- bar\n+ baz\n*** End Patch';
57+
expect(extractFilePath('apply_patch', { input }))
58+
.toBe('/src/hello.ts');
59+
});
60+
61+
it('extracts file path from apply_patch Add File', () => {
62+
const input = '*** Begin Patch\n*** Add File: /src/new.ts\n+export const x = 1;\n*** End Patch';
63+
expect(extractFilePath('apply_patch', { input }))
64+
.toBe('/src/new.ts');
65+
});
66+
67+
it('extracts file path from apply_patch Delete File', () => {
68+
const input = '*** Begin Patch\n*** Delete File: /src/old.ts\n*** End Patch';
69+
expect(extractFilePath('apply_patch', { input }))
70+
.toBe('/src/old.ts');
71+
});
72+
73+
it('falls back to filePath arg for apply_patch when present', () => {
74+
expect(extractFilePath('apply_patch', { filePath: '/from/arg.ts', input: '*** Update File: /from/input.ts' }))
75+
.toBe('/from/arg.ts');
76+
});
77+
78+
it('extracts path from CLI str_replace_editor (backward compat)', () => {
79+
expect(extractFilePath('str_replace_editor', { path: '/cli/file.py' }))
80+
.toBe('/cli/file.py');
81+
});
82+
83+
it('extracts path from CLI create tool (backward compat)', () => {
84+
expect(extractFilePath('create', { path: '/cli/new.py' }))
85+
.toBe('/cli/new.py');
86+
});
87+
88+
it('returns undefined for unknown tools', () => {
89+
expect(extractFilePath('run_in_terminal', { command: 'ls' }))
90+
.toBeUndefined();
91+
expect(extractFilePath('file_search', { query: '**/*.ts' }))
92+
.toBeUndefined();
93+
});
94+
95+
it('returns undefined for null args', () => {
96+
expect(extractFilePath('create_file', null))
97+
.toBeUndefined();
98+
});
99+
100+
it('returns undefined when filePath is not a string', () => {
101+
expect(extractFilePath('create_file', { filePath: 42 }))
102+
.toBeUndefined();
103+
});
104+
});
105+
106+
describe('isGitHubMcpTool', () => {
107+
it('matches mcp_github_ prefix (VS Code)', () => {
108+
expect(isGitHubMcpTool('mcp_github_issue_write')).toBe(true);
109+
expect(isGitHubMcpTool('mcp_github_create_pull_request')).toBe(true);
110+
expect(isGitHubMcpTool('mcp_github_search_issues')).toBe(true);
111+
});
112+
113+
it('matches github-mcp-server- prefix (CLI)', () => {
114+
expect(isGitHubMcpTool('github-mcp-server-create_issue')).toBe(true);
115+
});
116+
117+
it('rejects non-GitHub MCP tools', () => {
118+
expect(isGitHubMcpTool('read_file')).toBe(false);
119+
expect(isGitHubMcpTool('mcp_perplexity_ask')).toBe(false);
120+
expect(isGitHubMcpTool('run_in_terminal')).toBe(false);
121+
});
122+
});
123+
124+
describe('extractRefsFromMcpTool', () => {
125+
it('extracts PR ref from mcp_github_pull_request_read', () => {
126+
const refs = extractRefsFromMcpTool('mcp_github_pull_request_read', { pullNumber: 42, owner: 'ms', repo: 'vscode' });
127+
expect(refs).toEqual([{ ref_type: 'pr', ref_value: '42' }]);
128+
});
129+
130+
it('extracts issue ref from mcp_github_issue_write', () => {
131+
const refs = extractRefsFromMcpTool('mcp_github_issue_write', { issue_number: 123, owner: 'ms', repo: 'vscode' });
132+
expect(refs).toEqual([{ ref_type: 'issue', ref_value: '123' }]);
133+
});
134+
135+
it('extracts commit ref from mcp_github_get_commit', () => {
136+
const refs = extractRefsFromMcpTool('mcp_github_get_commit', { sha: 'abc123', owner: 'ms', repo: 'vscode' });
137+
expect(refs).toEqual([{ ref_type: 'commit', ref_value: 'abc123' }]);
138+
});
139+
140+
it('returns empty for non-matching tool name', () => {
141+
expect(extractRefsFromMcpTool('mcp_github_search_code', { query: 'foo' })).toEqual([]);
142+
});
143+
});
144+
145+
describe('extractRepoFromMcpTool', () => {
146+
it('extracts owner/repo', () => {
147+
expect(extractRepoFromMcpTool({ owner: 'microsoft', repo: 'vscode' }))
148+
.toBe('microsoft/vscode');
149+
});
150+
151+
it('returns undefined when missing', () => {
152+
expect(extractRepoFromMcpTool({ owner: 'microsoft' })).toBeUndefined();
153+
expect(extractRepoFromMcpTool({})).toBeUndefined();
154+
});
155+
});
156+
157+
describe('extractRefsFromTerminal', () => {
158+
it('extracts PR ref from gh pr create output', () => {
159+
const refs = extractRefsFromTerminal(
160+
{ command: 'gh pr create --title "fix"' },
161+
'https://github.com/microsoft/vscode/pull/999',
162+
);
163+
expect(refs).toEqual([{ ref_type: 'pr', ref_value: '999' }]);
164+
});
165+
166+
it('extracts issue ref from gh issue create output', () => {
167+
const refs = extractRefsFromTerminal(
168+
{ command: 'gh issue create --title "bug"' },
169+
'https://github.com/microsoft/vscode/issues/456',
170+
);
171+
expect(refs).toEqual([{ ref_type: 'issue', ref_value: '456' }]);
172+
});
173+
174+
it('extracts commit SHA from git commit output', () => {
175+
const refs = extractRefsFromTerminal(
176+
{ command: 'git commit -m "fix"' },
177+
'[main abc1234] fix\n 1 file changed',
178+
);
179+
expect(refs).toEqual([{ ref_type: 'commit', ref_value: 'abc1234' }]);
180+
});
181+
182+
it('returns empty for unrelated commands', () => {
183+
expect(extractRefsFromTerminal({ command: 'ls -la' }, 'output')).toEqual([]);
184+
});
185+
});

extensions/copilot/src/extension/chronicle/common/test/standupPrompt.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ describe('extractFilePath', () => {
100100
expect(extractFilePath('create', { path: '/src/new.ts' })).toBe('/src/new.ts');
101101
});
102102

103-
it('returns undefined for non-file-tracking tools', () => {
104-
expect(extractFilePath('read_file', { filePath: '/src/index.ts' })).toBeUndefined();
103+
it('extracts filePath from read_file args', () => {
104+
expect(extractFilePath('read_file', { filePath: '/src/index.ts' })).toBe('/src/index.ts');
105105
});
106106

107107
it('returns undefined for null args', () => {

0 commit comments

Comments
 (0)