Skip to content
This repository was archived by the owner on Aug 1, 2025. It is now read-only.

Commit e59b9c9

Browse files
authored
Chat: display file size warning on large files (#3118)
CONTEXT: https://sourcegraph.slack.com/archives/C05AGQYD528/p1707508300392389 Part of https://github.com/sourcegraph/cody/issues/2636 & https://github.com/sourcegraph/cody/issues/2965 Update chat UI to provide better file size awareness based on Tim's figma design: https://www.figma.com/file/jEsnzgsf0hNuJbqB9pKaGm/VS-Code---Cody---Commands-up-front-and-ChatGPT%2B%2B?type=design&node-id=331-103646&mode=design&t=jidOC06CYg9nSWOI-4 ![image](https://github.com/sourcegraph/cody/assets/68532117/5308fc4a-29fa-4e31-8759-14366df6ffc5) This PR includes the following changes: - Filter the `sorted results` in the `getFileContextFiles` function to remove non-files and files that are larger than 1MB - Set title as `large-file` for files that contain more tokens than we allow for file using the file size returned from `vscode.workspace.fs.stat` API - When users try to add a file using @-mentioned, they should see a warning below the file name if the file is too large - ~~Truncating prompt messages in the PromptBuilder class to include truncated context without all context being ignored due to excessive length.~~ - ~~Use the exisiting truncatePrompt function and MAX_AVAILABLE_PROMPT_LENGTH constant for truncating prompts.~~ - Display warning in at-mentions selector for large files ### Note - This PR focuses on providing information to users in the UI for files they are trying to add using `@-mention` that are too large. - ~~We are also truncating the context files to make sure large files will be truncated and used as context, instead of excluding the file.~~ Follow-up works include: - truncate large files - show users if a file was truncated ## Test plan <!-- Required. See https://sourcegraph.com/docs/dev/background-information/testing_principles. --> 1. @-mentioned a large file [SimpleChatPanelProvider](https://sourcegraph.sourcegraph.com/github.com/sourcegraph/cody@916ef190bf286505c5dcb254b87c0ad142b86c5a/-/blob/vscode/src/chat/chat-view/SimpleChatPanelProvider.ts) ![image](https://github.com/sourcegraph/cody/assets/68532117/5308fc4a-29fa-4e31-8759-14366df6ffc5) raph/cody/assets/68532117/7ea38043-c148-45d7-9a00-1d6aeb53f552)
1 parent b50e9c6 commit e59b9c9

File tree

5 files changed

+99
-15
lines changed

5 files changed

+99
-15
lines changed

vscode/src/editor/utils/editor-context.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,37 @@ afterEach(() => {
1212
})
1313

1414
describe('getFileContextFiles', () => {
15+
/**
16+
* Mocks the fs.stat function to return a fake stat object for the given URI.
17+
* This allows tests to mock filesystem access for specific files.
18+
*/
19+
function setFileStat(uri: vscode.Uri, isFile = true) {
20+
vscode.workspace.fs.stat = vi.fn().mockImplementation(() => {
21+
const relativePath = uriBasename(uri)
22+
return {
23+
type: isFile ? vscode.FileType.File : vscode.FileType.SymbolicLink,
24+
ctime: 1,
25+
mtime: 1,
26+
size: 1,
27+
isDirectory: () => false,
28+
isFile: () => isFile,
29+
isSymbolicLink: () => !isFile,
30+
uri,
31+
with: vi.fn(),
32+
toString: vi.fn().mockReturnValue(relativePath),
33+
}
34+
})
35+
}
36+
1537
function setFiles(relativePaths: string[]) {
1638
vscode.workspace.findFiles = vi
1739
.fn()
1840
.mockResolvedValueOnce(relativePaths.map(f => testFileUri(f)))
41+
42+
for (const relativePath of relativePaths) {
43+
const isFile = relativePath !== 'symlink'
44+
setFileStat(testFileUri(relativePath), isFile)
45+
}
1946
}
2047

2148
async function runSearch(query: string, maxResults: number): Promise<(string | undefined)[]> {
@@ -67,6 +94,16 @@ describe('getFileContextFiles', () => {
6794
expect(vscode.workspace.findFiles).toBeCalledTimes(1)
6895
})
6996

97+
it('do not return non-file (e.g. symlinks) result', async () => {
98+
setFiles(['symlink'])
99+
100+
expect(await runSearch('symlink', 5)).toMatchInlineSnapshot(`
101+
[]
102+
`)
103+
104+
expect(vscode.workspace.findFiles).toBeCalledTimes(1)
105+
})
106+
70107
it('filters out ignored files', async () => {
71108
ignores.setActiveState(true)
72109
ignores.setIgnoreFiles(testFileUri(''), [

vscode/src/editor/utils/editor-context.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
type ContextFileSymbol,
1515
type ContextFileType,
1616
type SymbolKind,
17+
MAX_CURRENT_FILE_TOKENS,
1718
} from '@sourcegraph/cody-shared'
1819

1920
import { getOpenTabsUris, getWorkspaceSymbols } from '.'
21+
import { CHARS_PER_TOKEN } from '@sourcegraph/cody-shared/src/prompt/constants'
2022

2123
const findWorkspaceFiles = async (
2224
cancellationToken: vscode.CancellationToken
@@ -85,7 +87,7 @@ export async function getFileContextFiles(
8587
threshold: -100000,
8688
})
8789

88-
// Remove ignored files and apply a penalty for segments that are in the low scoring list.
90+
// Apply a penalty for segments that are in the low scoring list.
8991
const adjustedResults = [...results].map(result => {
9092
const segments = result.obj.uri.fsPath.split(path.sep)
9193
for (const lowScoringPathSegment of lowScoringPathSegments) {
@@ -98,20 +100,42 @@ export async function getFileContextFiles(
98100
}
99101
return result
100102
})
101-
102103
// fuzzysort can return results in different order for the same query if
103104
// they have the same score :( So we do this hacky post-limit sorting (first
104105
// by score, then by path) to ensure the order stays the same.
105-
const sortedResults = adjustedResults.sort((a, b) => {
106-
return (
107-
b.score - a.score ||
108-
new Intl.Collator(undefined, { numeric: true }).compare(a.obj.uri.fsPath, b.obj.uri.fsPath)
109-
)
110-
})
106+
const sortedResults = adjustedResults
107+
.sort((a, b) => {
108+
return (
109+
b.score - a.score ||
110+
new Intl.Collator(undefined, { numeric: true }).compare(a.obj.uri.path, b.obj.uri.path)
111+
)
112+
})
113+
.flatMap(result => createContextFileFromUri(result.obj.uri, 'user', 'file'))
111114

112115
// TODO(toolmantim): Add fuzzysort.highlight data to the result so we can show it in the UI
113116

114-
return sortedResults.flatMap(result => createContextFileFromUri(result.obj.uri, 'user', 'file'))
117+
const filtered = []
118+
try {
119+
for (const sorted of sortedResults) {
120+
// Remove file larger than 1MB and non-text files
121+
// NOTE: Sourcegraph search only includes files up to 1MB
122+
const fileStat = await vscode.workspace.fs.stat(sorted.uri)
123+
if (fileStat.type !== vscode.FileType.File || fileStat.size > 1000000) {
124+
continue
125+
}
126+
// Check if file contains more characters than the token limit based on fileStat.size
127+
// and set the title of the result as 'large-file' for webview to display file size
128+
// warning.
129+
if (fileStat.size > CHARS_PER_TOKEN * MAX_CURRENT_FILE_TOKENS) {
130+
sorted.title = 'large-file'
131+
}
132+
filtered.push(sorted)
133+
}
134+
} catch (error) {
135+
console.log('atMention:getFileContextFiles:failed', error)
136+
}
137+
138+
return filtered
115139
}
116140

117141
export async function getSymbolContextFiles(

vscode/test/e2e/chat-atFile.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,17 @@ test('@-file & @#-symbol in chat view', async ({ page, sidebar }) => {
152152

153153
// "ArrowLeft" / "ArrowRight" keys close the selection without altering current input.
154154
const noMatches = chatPanelFrame.getByRole('heading', { name: 'No matching files found' })
155-
await page.keyboard.type(' @abcdefg')
155+
await chatInput.type(' @abcdefg', { delay: 50 })
156156
await expect(chatInput).toHaveValue('Explain the @Main.java ! @abcdefgfile')
157+
await noMatches.hover()
157158
await expect(noMatches).toBeVisible()
158159
await chatInput.press('ArrowLeft')
159160
await expect(noMatches).not.toBeVisible()
160161
await chatInput.press('ArrowRight')
161162
await expect(noMatches).not.toBeVisible()
162-
await chatInput.press('?')
163+
await chatInput.type('?', { delay: 50 })
163164
await expect(chatInput).toHaveValue('Explain the @Main.java ! @abcdefg?file')
165+
await noMatches.hover()
164166
await expect(noMatches).toBeVisible()
165167
// Selection close on submit
166168
await chatInput.press('Enter')

vscode/webviews/UserContextSelector.module.css

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343

4444
.selection-item {
4545
display: flex;
46-
align-items: center;
46+
flex-direction: column;
47+
align-items: self-start;
4748
font-size: inherit;
4849
width: 100%;
4950
cursor: pointer;
@@ -63,6 +64,10 @@
6364
overflow: hidden;
6465
}
6566

67+
.warning-container {
68+
margin: 0;
69+
}
70+
6671
.selection-title {
6772
white-space: nowrap;
6873
text-overflow: ellipsis;
@@ -71,8 +76,7 @@
7176

7277
.selection-description {
7378
margin-left: 0.25rem;
74-
font-size: smaller;
75-
opacity: 0.7;
79+
opacity: 0.8;
7680
white-space: nowrap;
7781
text-overflow: ellipsis;
7882
overflow: hidden;
@@ -111,4 +115,4 @@ body[data-vscode-theme-kind='vscode-high-contrast'] .selected {
111115

112116
.empty-symbol-search-tip i {
113117
vertical-align: middle;
114-
}
118+
}

vscode/webviews/UserContextSelector.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ export const UserContextSelectorComponent: React.FunctionComponent<
9797
: ''
9898
const description =
9999
match.type === 'file' ? undefined : displayPath(match.uri) + range
100+
const warning =
101+
match.type === 'file' && match.title === 'large-file'
102+
? 'File too large. Type @# to choose a symbol'
103+
: undefined
100104
return (
101105
<React.Fragment key={`${icon}${title}${range}${description}`}>
102106
<button
@@ -105,6 +109,7 @@ export const UserContextSelectorComponent: React.FunctionComponent<
105109
styles.selectionItem,
106110
selected === i && styles.selected
107111
)}
112+
title={title}
108113
onClick={() => onSelected(match, formInput)}
109114
type="button"
110115
>
@@ -124,6 +129,18 @@ export const UserContextSelectorComponent: React.FunctionComponent<
124129
</span>
125130
)}
126131
</span>
132+
{warning && (
133+
<p
134+
className={classNames(
135+
styles.titleAndDescriptionContainer,
136+
styles.warningContainer
137+
)}
138+
>
139+
<span className={styles.selectionDescription}>
140+
{warning}
141+
</span>
142+
</p>
143+
)}
127144
</button>
128145
</React.Fragment>
129146
)

0 commit comments

Comments
 (0)