Modify content generated describing the ide context to only include deltas after the initial update (#5880)
This commit is contained in:
parent
aa5c80dec4
commit
2269f8a1a8
|
@ -201,6 +201,7 @@ describe('Gemini Client (client.ts)', () => {
|
||||||
getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
|
getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
|
||||||
getIdeModeFeature: vi.fn().mockReturnValue(false),
|
getIdeModeFeature: vi.fn().mockReturnValue(false),
|
||||||
getIdeMode: vi.fn().mockReturnValue(true),
|
getIdeMode: vi.fn().mockReturnValue(true),
|
||||||
|
getDebugMode: vi.fn().mockReturnValue(false),
|
||||||
getWorkspaceContext: vi.fn().mockReturnValue({
|
getWorkspaceContext: vi.fn().mockReturnValue({
|
||||||
getDirectories: vi.fn().mockReturnValue(['/test/dir']),
|
getDirectories: vi.fn().mockReturnValue(['/test/dir']),
|
||||||
}),
|
}),
|
||||||
|
@ -449,8 +450,7 @@ describe('Gemini Client (client.ts)', () => {
|
||||||
const mockChat = {
|
const mockChat = {
|
||||||
addHistory: vi.fn(),
|
addHistory: vi.fn(),
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
client['chat'] = mockChat as unknown as GeminiChat;
|
||||||
client['chat'] = mockChat as any;
|
|
||||||
|
|
||||||
const newContent = {
|
const newContent = {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
|
@ -667,7 +667,7 @@ describe('Gemini Client (client.ts)', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sendMessageStream', () => {
|
describe('sendMessageStream', () => {
|
||||||
it('should include IDE context when ideModeFeature is enabled', async () => {
|
it('should include editor context when ideModeFeature is enabled', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
vi.mocked(ideContext.getIdeContext).mockReturnValue({
|
vi.mocked(ideContext.getIdeContext).mockReturnValue({
|
||||||
workspaceState: {
|
workspaceState: {
|
||||||
|
@ -725,21 +725,30 @@ describe('Gemini Client (client.ts)', () => {
|
||||||
// Assert
|
// Assert
|
||||||
expect(ideContext.getIdeContext).toHaveBeenCalled();
|
expect(ideContext.getIdeContext).toHaveBeenCalled();
|
||||||
const expectedContext = `
|
const expectedContext = `
|
||||||
This is the file that the user is looking at:
|
Here is the user's editor context as a JSON object. This is for your information only.
|
||||||
- Path: /path/to/active/file.ts
|
\`\`\`json
|
||||||
This is the cursor position in the file:
|
${JSON.stringify(
|
||||||
- Cursor Position: Line 5, Character 10
|
{
|
||||||
This is the selected text in the file:
|
activeFile: {
|
||||||
- hello
|
path: '/path/to/active/file.ts',
|
||||||
Here are some other files the user has open, with the most recent at the top:
|
cursor: {
|
||||||
- /path/to/recent/file1.ts
|
line: 5,
|
||||||
- /path/to/recent/file2.ts
|
character: 10,
|
||||||
|
},
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
otherOpenFiles: ['/path/to/recent/file1.ts', '/path/to/recent/file2.ts'],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}
|
||||||
|
\`\`\`
|
||||||
`.trim();
|
`.trim();
|
||||||
const expectedRequest = [{ text: expectedContext }, ...initialRequest];
|
const expectedRequest = [{ text: expectedContext }];
|
||||||
expect(mockTurnRunFn).toHaveBeenCalledWith(
|
expect(mockChat.addHistory).toHaveBeenCalledWith({
|
||||||
expectedRequest,
|
role: 'user',
|
||||||
expect.any(Object),
|
parts: expectedRequest,
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not add context if ideModeFeature is enabled but no open files', async () => {
|
it('should not add context if ideModeFeature is enabled but no open files', async () => {
|
||||||
|
@ -839,18 +848,29 @@ Here are some other files the user has open, with the most recent at the top:
|
||||||
// Assert
|
// Assert
|
||||||
expect(ideContext.getIdeContext).toHaveBeenCalled();
|
expect(ideContext.getIdeContext).toHaveBeenCalled();
|
||||||
const expectedContext = `
|
const expectedContext = `
|
||||||
This is the file that the user is looking at:
|
Here is the user's editor context as a JSON object. This is for your information only.
|
||||||
- Path: /path/to/active/file.ts
|
\`\`\`json
|
||||||
This is the cursor position in the file:
|
${JSON.stringify(
|
||||||
- Cursor Position: Line 5, Character 10
|
{
|
||||||
This is the selected text in the file:
|
activeFile: {
|
||||||
- hello
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: {
|
||||||
|
line: 5,
|
||||||
|
character: 10,
|
||||||
|
},
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}
|
||||||
|
\`\`\`
|
||||||
`.trim();
|
`.trim();
|
||||||
const expectedRequest = [{ text: expectedContext }, ...initialRequest];
|
const expectedRequest = [{ text: expectedContext }];
|
||||||
expect(mockTurnRunFn).toHaveBeenCalledWith(
|
expect(mockChat.addHistory).toHaveBeenCalledWith({
|
||||||
expectedRequest,
|
role: 'user',
|
||||||
expect.any(Object),
|
parts: expectedRequest,
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add context if ideModeFeature is enabled and there are open files but no active file', async () => {
|
it('should add context if ideModeFeature is enabled and there are open files but no active file', async () => {
|
||||||
|
@ -904,15 +924,22 @@ This is the selected text in the file:
|
||||||
// Assert
|
// Assert
|
||||||
expect(ideContext.getIdeContext).toHaveBeenCalled();
|
expect(ideContext.getIdeContext).toHaveBeenCalled();
|
||||||
const expectedContext = `
|
const expectedContext = `
|
||||||
Here are some files the user has open, with the most recent at the top:
|
Here is the user's editor context as a JSON object. This is for your information only.
|
||||||
- /path/to/recent/file1.ts
|
\`\`\`json
|
||||||
- /path/to/recent/file2.ts
|
${JSON.stringify(
|
||||||
|
{
|
||||||
|
otherOpenFiles: ['/path/to/recent/file1.ts', '/path/to/recent/file2.ts'],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}
|
||||||
|
\`\`\`
|
||||||
`.trim();
|
`.trim();
|
||||||
const expectedRequest = [{ text: expectedContext }, ...initialRequest];
|
const expectedRequest = [{ text: expectedContext }];
|
||||||
expect(mockTurnRunFn).toHaveBeenCalledWith(
|
expect(mockChat.addHistory).toHaveBeenCalledWith({
|
||||||
expectedRequest,
|
role: 'user',
|
||||||
expect.any(Object),
|
parts: expectedRequest,
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the turn instance after the stream is complete', async () => {
|
it('should return the turn instance after the stream is complete', async () => {
|
||||||
|
@ -1190,6 +1217,268 @@ Here are some files the user has open, with the most recent at the top:
|
||||||
`${eventCount} events generated (properly bounded by MAX_TURNS)`,
|
`${eventCount} events generated (properly bounded by MAX_TURNS)`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Editor context delta', () => {
|
||||||
|
const mockStream = (async function* () {
|
||||||
|
yield { type: 'content', value: 'Hello' };
|
||||||
|
})();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client['forceFullIdeContext'] = false; // Reset before each delta test
|
||||||
|
vi.spyOn(client, 'tryCompressChat').mockResolvedValue(null);
|
||||||
|
vi.spyOn(client['config'], 'getIdeModeFeature').mockReturnValue(true);
|
||||||
|
mockTurnRunFn.mockReturnValue(mockStream);
|
||||||
|
|
||||||
|
const mockChat: Partial<GeminiChat> = {
|
||||||
|
addHistory: vi.fn(),
|
||||||
|
setHistory: vi.fn(),
|
||||||
|
sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
|
||||||
|
// Assume history is not empty for delta checks
|
||||||
|
getHistory: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue([
|
||||||
|
{ role: 'user', parts: [{ text: 'previous message' }] },
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
client['chat'] = mockChat as GeminiChat;
|
||||||
|
|
||||||
|
const mockGenerator: Partial<ContentGenerator> = {
|
||||||
|
countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
|
||||||
|
generateContent: mockGenerateContentFn,
|
||||||
|
};
|
||||||
|
client['contentGenerator'] = mockGenerator as ContentGenerator;
|
||||||
|
});
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
description: 'sends delta when active file changes',
|
||||||
|
previousActiveFile: {
|
||||||
|
path: '/path/to/old/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
currentActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
shouldSendContext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'sends delta when cursor line changes',
|
||||||
|
previousActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 1, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
currentActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
shouldSendContext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'sends delta when cursor character changes',
|
||||||
|
previousActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 1 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
currentActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
shouldSendContext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'sends delta when selected text changes',
|
||||||
|
previousActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'world',
|
||||||
|
},
|
||||||
|
currentActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
shouldSendContext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'sends delta when selected text is added',
|
||||||
|
previousActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
},
|
||||||
|
currentActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
shouldSendContext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'sends delta when selected text is removed',
|
||||||
|
previousActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
currentActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
},
|
||||||
|
shouldSendContext: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'does not send context when nothing changes',
|
||||||
|
previousActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
currentActiveFile: {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
},
|
||||||
|
shouldSendContext: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(testCases)(
|
||||||
|
'$description',
|
||||||
|
async ({
|
||||||
|
previousActiveFile,
|
||||||
|
currentActiveFile,
|
||||||
|
shouldSendContext,
|
||||||
|
}) => {
|
||||||
|
// Setup previous context
|
||||||
|
client['lastSentIdeContext'] = {
|
||||||
|
workspaceState: {
|
||||||
|
openFiles: [
|
||||||
|
{
|
||||||
|
path: previousActiveFile.path,
|
||||||
|
cursor: previousActiveFile.cursor,
|
||||||
|
selectedText: previousActiveFile.selectedText,
|
||||||
|
isActive: true,
|
||||||
|
timestamp: Date.now() - 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup current context
|
||||||
|
vi.mocked(ideContext.getIdeContext).mockReturnValue({
|
||||||
|
workspaceState: {
|
||||||
|
openFiles: [
|
||||||
|
{ ...currentActiveFile, isActive: true, timestamp: Date.now() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stream = client.sendMessageStream(
|
||||||
|
[{ text: 'Hi' }],
|
||||||
|
new AbortController().signal,
|
||||||
|
'prompt-id-delta',
|
||||||
|
);
|
||||||
|
for await (const _ of stream) {
|
||||||
|
// consume stream
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockChat = client['chat'] as unknown as {
|
||||||
|
addHistory: (typeof vi)['fn'];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldSendContext) {
|
||||||
|
expect(mockChat.addHistory).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
parts: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
text: expect.stringContaining(
|
||||||
|
"Here is a summary of changes in the user's editor context",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expect(mockChat.addHistory).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('sends full context when history is cleared, even if editor state is unchanged', async () => {
|
||||||
|
const activeFile = {
|
||||||
|
path: '/path/to/active/file.ts',
|
||||||
|
cursor: { line: 5, character: 10 },
|
||||||
|
selectedText: 'hello',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup previous context
|
||||||
|
client['lastSentIdeContext'] = {
|
||||||
|
workspaceState: {
|
||||||
|
openFiles: [
|
||||||
|
{
|
||||||
|
path: activeFile.path,
|
||||||
|
cursor: activeFile.cursor,
|
||||||
|
selectedText: activeFile.selectedText,
|
||||||
|
isActive: true,
|
||||||
|
timestamp: Date.now() - 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup current context (same as previous)
|
||||||
|
vi.mocked(ideContext.getIdeContext).mockReturnValue({
|
||||||
|
workspaceState: {
|
||||||
|
openFiles: [
|
||||||
|
{ ...activeFile, isActive: true, timestamp: Date.now() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make history empty
|
||||||
|
const mockChat = client['chat'] as unknown as {
|
||||||
|
getHistory: ReturnType<(typeof vi)['fn']>;
|
||||||
|
addHistory: ReturnType<(typeof vi)['fn']>;
|
||||||
|
};
|
||||||
|
mockChat.getHistory.mockReturnValue([]);
|
||||||
|
|
||||||
|
const stream = client.sendMessageStream(
|
||||||
|
[{ text: 'Hi' }],
|
||||||
|
new AbortController().signal,
|
||||||
|
'prompt-id-history-cleared',
|
||||||
|
);
|
||||||
|
for await (const _ of stream) {
|
||||||
|
// consume stream
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockChat.addHistory).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
parts: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
text: expect.stringContaining(
|
||||||
|
"Here is the user's editor context",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also verify it's the full context, not a delta.
|
||||||
|
const call = mockChat.addHistory.mock.calls[0][0];
|
||||||
|
const contextText = call.parts[0].text;
|
||||||
|
const contextJson = JSON.parse(
|
||||||
|
contextText.match(/```json\n(.*)\n```/s)![1],
|
||||||
|
);
|
||||||
|
expect(contextJson).toHaveProperty('activeFile');
|
||||||
|
expect(contextJson.activeFile.path).toBe('/path/to/active/file.ts');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generateContent', () => {
|
describe('generateContent', () => {
|
||||||
|
|
|
@ -50,6 +50,7 @@ import {
|
||||||
NextSpeakerCheckEvent,
|
NextSpeakerCheckEvent,
|
||||||
} from '../telemetry/types.js';
|
} from '../telemetry/types.js';
|
||||||
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
||||||
|
import { IdeContext, File } from '../ide/ideContext.js';
|
||||||
|
|
||||||
function isThinkingSupported(model: string) {
|
function isThinkingSupported(model: string) {
|
||||||
if (model.startsWith('gemini-2.5')) return true;
|
if (model.startsWith('gemini-2.5')) return true;
|
||||||
|
@ -112,6 +113,8 @@ export class GeminiClient {
|
||||||
|
|
||||||
private readonly loopDetector: LoopDetectionService;
|
private readonly loopDetector: LoopDetectionService;
|
||||||
private lastPromptId: string;
|
private lastPromptId: string;
|
||||||
|
private lastSentIdeContext: IdeContext | undefined;
|
||||||
|
private forceFullIdeContext = true;
|
||||||
|
|
||||||
constructor(private config: Config) {
|
constructor(private config: Config) {
|
||||||
if (config.getProxy()) {
|
if (config.getProxy()) {
|
||||||
|
@ -164,6 +167,7 @@ export class GeminiClient {
|
||||||
|
|
||||||
setHistory(history: Content[]) {
|
setHistory(history: Content[]) {
|
||||||
this.getChat().setHistory(history);
|
this.getChat().setHistory(history);
|
||||||
|
this.forceFullIdeContext = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setTools(): Promise<void> {
|
async setTools(): Promise<void> {
|
||||||
|
@ -189,6 +193,7 @@ export class GeminiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async startChat(extraHistory?: Content[]): Promise<GeminiChat> {
|
async startChat(extraHistory?: Content[]): Promise<GeminiChat> {
|
||||||
|
this.forceFullIdeContext = true;
|
||||||
const envParts = await getEnvironmentContext(this.config);
|
const envParts = await getEnvironmentContext(this.config);
|
||||||
const toolRegistry = await this.config.getToolRegistry();
|
const toolRegistry = await this.config.getToolRegistry();
|
||||||
const toolDeclarations = toolRegistry.getFunctionDeclarations();
|
const toolDeclarations = toolRegistry.getFunctionDeclarations();
|
||||||
|
@ -238,6 +243,174 @@ export class GeminiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getIdeContextParts(forceFullContext: boolean): {
|
||||||
|
contextParts: string[];
|
||||||
|
newIdeContext: IdeContext | undefined;
|
||||||
|
} {
|
||||||
|
const currentIdeContext = ideContext.getIdeContext();
|
||||||
|
if (!currentIdeContext) {
|
||||||
|
return { contextParts: [], newIdeContext: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceFullContext || !this.lastSentIdeContext) {
|
||||||
|
// Send full context as JSON
|
||||||
|
const openFiles = currentIdeContext.workspaceState?.openFiles || [];
|
||||||
|
const activeFile = openFiles.find((f) => f.isActive);
|
||||||
|
const otherOpenFiles = openFiles
|
||||||
|
.filter((f) => !f.isActive)
|
||||||
|
.map((f) => f.path);
|
||||||
|
|
||||||
|
const contextData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (activeFile) {
|
||||||
|
contextData.activeFile = {
|
||||||
|
path: activeFile.path,
|
||||||
|
cursor: activeFile.cursor
|
||||||
|
? {
|
||||||
|
line: activeFile.cursor.line,
|
||||||
|
character: activeFile.cursor.character,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
selectedText: activeFile.selectedText || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherOpenFiles.length > 0) {
|
||||||
|
contextData.otherOpenFiles = otherOpenFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(contextData).length === 0) {
|
||||||
|
return { contextParts: [], newIdeContext: currentIdeContext };
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonString = JSON.stringify(contextData, null, 2);
|
||||||
|
const contextParts = [
|
||||||
|
"Here is the user's editor context as a JSON object. This is for your information only.",
|
||||||
|
'```json',
|
||||||
|
jsonString,
|
||||||
|
'```',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (this.config.getDebugMode()) {
|
||||||
|
console.log(contextParts.join('\n'));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
contextParts,
|
||||||
|
newIdeContext: currentIdeContext,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Calculate and send delta as JSON
|
||||||
|
const delta: Record<string, unknown> = {};
|
||||||
|
const changes: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
const lastFiles = new Map(
|
||||||
|
(this.lastSentIdeContext.workspaceState?.openFiles || []).map(
|
||||||
|
(f: File) => [f.path, f],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const currentFiles = new Map(
|
||||||
|
(currentIdeContext.workspaceState?.openFiles || []).map((f: File) => [
|
||||||
|
f.path,
|
||||||
|
f,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const openedFiles: string[] = [];
|
||||||
|
for (const [path] of currentFiles.entries()) {
|
||||||
|
if (!lastFiles.has(path)) {
|
||||||
|
openedFiles.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (openedFiles.length > 0) {
|
||||||
|
changes.filesOpened = openedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closedFiles: string[] = [];
|
||||||
|
for (const [path] of lastFiles.entries()) {
|
||||||
|
if (!currentFiles.has(path)) {
|
||||||
|
closedFiles.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closedFiles.length > 0) {
|
||||||
|
changes.filesClosed = closedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastActiveFile = (
|
||||||
|
this.lastSentIdeContext.workspaceState?.openFiles || []
|
||||||
|
).find((f: File) => f.isActive);
|
||||||
|
const currentActiveFile = (
|
||||||
|
currentIdeContext.workspaceState?.openFiles || []
|
||||||
|
).find((f: File) => f.isActive);
|
||||||
|
|
||||||
|
if (currentActiveFile) {
|
||||||
|
if (!lastActiveFile || lastActiveFile.path !== currentActiveFile.path) {
|
||||||
|
changes.activeFileChanged = {
|
||||||
|
path: currentActiveFile.path,
|
||||||
|
cursor: currentActiveFile.cursor
|
||||||
|
? {
|
||||||
|
line: currentActiveFile.cursor.line,
|
||||||
|
character: currentActiveFile.cursor.character,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
selectedText: currentActiveFile.selectedText || undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const lastCursor = lastActiveFile.cursor;
|
||||||
|
const currentCursor = currentActiveFile.cursor;
|
||||||
|
if (
|
||||||
|
currentCursor &&
|
||||||
|
(!lastCursor ||
|
||||||
|
lastCursor.line !== currentCursor.line ||
|
||||||
|
lastCursor.character !== currentCursor.character)
|
||||||
|
) {
|
||||||
|
changes.cursorMoved = {
|
||||||
|
path: currentActiveFile.path,
|
||||||
|
cursor: {
|
||||||
|
line: currentCursor.line,
|
||||||
|
character: currentCursor.character,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSelectedText = lastActiveFile.selectedText || '';
|
||||||
|
const currentSelectedText = currentActiveFile.selectedText || '';
|
||||||
|
if (lastSelectedText !== currentSelectedText) {
|
||||||
|
changes.selectionChanged = {
|
||||||
|
path: currentActiveFile.path,
|
||||||
|
selectedText: currentSelectedText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (lastActiveFile) {
|
||||||
|
changes.activeFileChanged = {
|
||||||
|
path: null,
|
||||||
|
previousPath: lastActiveFile.path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(changes).length === 0) {
|
||||||
|
return { contextParts: [], newIdeContext: currentIdeContext };
|
||||||
|
}
|
||||||
|
|
||||||
|
delta.changes = changes;
|
||||||
|
const jsonString = JSON.stringify(delta, null, 2);
|
||||||
|
const contextParts = [
|
||||||
|
"Here is a summary of changes in the user's editor context, in JSON format. This is for your information only.",
|
||||||
|
'```json',
|
||||||
|
jsonString,
|
||||||
|
'```',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (this.config.getDebugMode()) {
|
||||||
|
console.log(contextParts.join('\n'));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
contextParts,
|
||||||
|
newIdeContext: currentIdeContext,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async *sendMessageStream(
|
async *sendMessageStream(
|
||||||
request: PartListUnion,
|
request: PartListUnion,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
|
@ -273,49 +446,17 @@ export class GeminiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.getIdeModeFeature() && this.config.getIdeMode()) {
|
if (this.config.getIdeModeFeature() && this.config.getIdeMode()) {
|
||||||
const ideContextState = ideContext.getIdeContext();
|
const { contextParts, newIdeContext } = this.getIdeContextParts(
|
||||||
const openFiles = ideContextState?.workspaceState?.openFiles;
|
this.forceFullIdeContext || this.getHistory().length === 0,
|
||||||
|
|
||||||
if (openFiles && openFiles.length > 0) {
|
|
||||||
const contextParts: string[] = [];
|
|
||||||
const firstFile = openFiles[0];
|
|
||||||
const activeFile = firstFile.isActive ? firstFile : undefined;
|
|
||||||
|
|
||||||
if (activeFile) {
|
|
||||||
contextParts.push(
|
|
||||||
`This is the file that the user is looking at:\n- Path: ${activeFile.path}`,
|
|
||||||
);
|
);
|
||||||
if (activeFile.cursor) {
|
|
||||||
contextParts.push(
|
|
||||||
`This is the cursor position in the file:\n- Cursor Position: Line ${activeFile.cursor.line}, Character ${activeFile.cursor.character}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (activeFile.selectedText) {
|
|
||||||
contextParts.push(
|
|
||||||
`This is the selected text in the file:\n- ${activeFile.selectedText}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const otherOpenFiles = activeFile ? openFiles.slice(1) : openFiles;
|
|
||||||
|
|
||||||
if (otherOpenFiles.length > 0) {
|
|
||||||
const recentFiles = otherOpenFiles
|
|
||||||
.map((file) => `- ${file.path}`)
|
|
||||||
.join('\n');
|
|
||||||
const heading = activeFile
|
|
||||||
? `Here are some other files the user has open, with the most recent at the top:`
|
|
||||||
: `Here are some files the user has open, with the most recent at the top:`;
|
|
||||||
contextParts.push(`${heading}\n${recentFiles}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contextParts.length > 0) {
|
if (contextParts.length > 0) {
|
||||||
request = [
|
this.getChat().addHistory({
|
||||||
{ text: contextParts.join('\n') },
|
role: 'user',
|
||||||
...(Array.isArray(request) ? request : [request]),
|
parts: [{ text: contextParts.join('\n') }],
|
||||||
];
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.lastSentIdeContext = newIdeContext;
|
||||||
|
this.forceFullIdeContext = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const turn = new Turn(this.getChat(), prompt_id);
|
const turn = new Turn(this.getChat(), prompt_id);
|
||||||
|
@ -648,6 +789,7 @@ export class GeminiClient {
|
||||||
},
|
},
|
||||||
...historyToKeep,
|
...historyToKeep,
|
||||||
]);
|
]);
|
||||||
|
this.forceFullIdeContext = true;
|
||||||
|
|
||||||
const { totalTokens: newTokenCount } =
|
const { totalTokens: newTokenCount } =
|
||||||
await this.getContentGenerator().countTokens({
|
await this.getContentGenerator().countTokens({
|
||||||
|
|
Loading…
Reference in New Issue