Skip to content

[pull] main from microsoft:main #149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Jun 23, 2025
Merged
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e741ef4
Update copyFiles.ts
ghLcd9dG Jun 5, 2025
47871a8
Merge branch 'main' into main
ghLcd9dG Jun 15, 2025
8a599c3
Merge branch 'main' into main
ghLcd9dG Jun 20, 2025
39d6463
Update package.nls.json
ghLcd9dG Jun 20, 2025
176ed73
also include chat mode in aria label if verbosity setting is disabled…
meganrogge Jun 23, 2025
e9491c8
update distro (#252201)
sandy081 Jun 23, 2025
de2fc31
Add chat.tools.autoApprove setting information to agent accessibility…
Copilot Jun 23, 2025
de05535
Use correct contrast color for partial selection, clear state on all nav
Tyriar Jun 23, 2025
ae1e423
Add provider ID to terminal suggest telemetry (#252016)
Copilot Jun 23, 2025
cbc4996
Inform screen reader users when there's an error in chat, make keyboa…
meganrogge Jun 23, 2025
de1dbee
add file to chat in file tabs (#252128)
justschen Jun 23, 2025
1a797f9
ComputeAutomaticInstructions: do not handle settings from instruction…
aeschli Jun 23, 2025
8710201
Handle toolsets correctly in custom chat modes (#252114)
roblourens Jun 23, 2025
c663424
Merge pull request #250773 from ghLcd9dG/main
mjbvz Jun 23, 2025
53192c0
Merge pull request #252209 from microsoft/tyriar/252200
Tyriar Jun 23, 2025
a1e227c
Fix typo (an language model call → a language model call) (#252202)
Gallaecio Jun 23, 2025
b5bbf59
improve git bash absolute path support (#252214)
meganrogge Jun 23, 2025
e07ae21
New customization action (gear) in Chat view toolbar (#252217)
aeschli Jun 23, 2025
cf2063d
Add releaseDate to builtin vscode telemetry too (#252047)
roblourens Jun 23, 2025
5b34b12
MCP CLI option should not appear after --help (fix #252130) (#252219)
bpasero Jun 23, 2025
dc1f963
prompt location picker: show full folder name (#252220)
aeschli Jun 23, 2025
4d19bfb
fix: voice chat keybinding steals Settings editor keybinding (#252218)
rzhao271 Jun 23, 2025
fa995da
Pass application_type as native (#252226)
TylerLeonhardt Jun 23, 2025
f1c7514
support first class resource mcp.json for mcp servers (#252102)
sandy081 Jun 23, 2025
449da85
Updating edit context only on line change and selection change and us…
aiday-mar Jun 23, 2025
e985993
Add suppression comment (#252235)
alexdima Jun 23, 2025
1cbcee6
PromptFileVariableEntry: add PromptFileVariableKind (#252233)
aeschli Jun 23, 2025
2e763a8
cache builtin commands for shells to decrease terminal completion lat…
meganrogge Jun 23, 2025
b542aea
Fix gdpr comment (#252237)
roblourens Jun 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
cache builtin commands for shells to decrease terminal completion lat…
  • Loading branch information
meganrogge authored Jun 23, 2025
commit 2e763a8c7b597740a0d6ffcd9726eb3aad07083d
158 changes: 134 additions & 24 deletions extensions/terminal-suggest/src/terminalSuggestMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
*--------------------------------------------------------------------------------------------*/

import { ExecOptionsWithStringEncoding } from 'child_process';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as vscode from 'vscode';
import cdSpec from './completions/cd';
import codeCompletionSpec from './completions/code';
Expand Down Expand Up @@ -39,8 +37,21 @@ export const enum TerminalShellType {
}

const isWindows = osIsWindows();
const cachedGlobals: Map<TerminalShellType, ICompletionResource[] | undefined> = new Map();
type ShellGlobalsCacheEntry = {
commands: ICompletionResource[] | undefined;
existingCommands?: string[];
};

type ShellGlobalsCacheEntryWithMeta = ShellGlobalsCacheEntry & { timestamp: number };
const cachedGlobals: Map<string, ShellGlobalsCacheEntryWithMeta> = new Map();
let pathExecutableCache: PathExecutableCache;
const CACHE_KEY = 'terminalSuggestGlobalsCacheV2';
let globalStorage: vscode.Memento;
const CACHE_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 7; // 7 days

function getCacheKey(machineId: string, remoteAuthority: string | undefined, shellType: TerminalShellType): string {
return `${machineId}:${remoteAuthority ?? 'local'}:${shellType}`;
}

export const availableSpecs: Fig.Spec[] = [
cdSpec,
Expand All @@ -64,36 +75,131 @@ const getShellSpecificGlobals: Map<TerminalShellType, (options: ExecOptionsWithS
[TerminalShellType.PowerShell, getPwshGlobals],
]);

async function getShellGlobals(shellType: TerminalShellType, existingCommands?: Set<string>): Promise<ICompletionResource[] | undefined> {
try {
const cachedCommands = cachedGlobals.get(shellType);
if (cachedCommands) {
return cachedCommands;
}
if (!shellType) {
return;

async function getShellGlobals(
shellType: TerminalShellType,
existingCommands?: Set<string>,
machineId?: string,
remoteAuthority?: string
): Promise<ICompletionResource[] | undefined> {
if (!machineId) {
// fallback: don't cache
return await fetchAndCacheShellGlobals(shellType, existingCommands, undefined, undefined);
}
const cacheKey = getCacheKey(machineId, remoteAuthority, shellType);
const cached = cachedGlobals.get(cacheKey);
const now = Date.now();
const existingCommandsArr = existingCommands ? Array.from(existingCommands) : undefined;
let shouldRefresh = false;
if (cached) {
// Evict if too old
if (now - cached.timestamp > CACHE_MAX_AGE_MS) {
cachedGlobals.delete(cacheKey);
await writeGlobalsCache();
} else {
if (existingCommandsArr && cached.existingCommands) {
if (existingCommandsArr.length !== cached.existingCommands.length) {
shouldRefresh = true;
}
} else if (existingCommandsArr || cached.existingCommands) {
shouldRefresh = true;
}
if (!shouldRefresh && cached.commands) {
// Trigger background refresh
void fetchAndCacheShellGlobals(shellType, existingCommands, machineId, remoteAuthority, true);
return cached.commands;
}
}
}
// No cache or should refresh
return await fetchAndCacheShellGlobals(shellType, existingCommands, machineId, remoteAuthority);
}

async function fetchAndCacheShellGlobals(
shellType: TerminalShellType,
existingCommands?: Set<string>,
machineId?: string,
remoteAuthority?: string,
background?: boolean
): Promise<ICompletionResource[] | undefined> {
try {
let execShellType = shellType;
if (shellType === TerminalShellType.GitBash) {
execShellType = TerminalShellType.Bash; // Git Bash is a bash shell
}
const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell: execShellType, windowsHide: true };
const mixedCommands: (string | ICompletionResource)[] | undefined = await getShellSpecificGlobals.get(shellType)?.(options, existingCommands);
const normalizedCommands = mixedCommands?.map(command => typeof command === 'string' ? ({ label: command }) : command);
cachedGlobals.set(shellType, normalizedCommands);
if (machineId) {
const cacheKey = getCacheKey(machineId, remoteAuthority, shellType);
cachedGlobals.set(cacheKey, {
commands: normalizedCommands,
existingCommands: existingCommands ? Array.from(existingCommands) : undefined,
timestamp: Date.now()
});
await writeGlobalsCache();
}
return normalizedCommands;

} catch (error) {
console.error('Error fetching builtin commands:', error);
if (!background) {
console.error('Error fetching builtin commands:', error);
}
return;
}
}


async function writeGlobalsCache(): Promise<void> {
if (!globalStorage) {
return;
}
// Remove old entries
const now = Date.now();
for (const [key, value] of cachedGlobals.entries()) {
if (now - value.timestamp > CACHE_MAX_AGE_MS) {
cachedGlobals.delete(key);
}
}
const obj: Record<string, ShellGlobalsCacheEntryWithMeta> = {};
for (const [key, value] of cachedGlobals.entries()) {
obj[key] = value;
}
try {
await globalStorage.update(CACHE_KEY, obj);
} catch (err) {
console.error('Failed to write terminal suggest globals cache:', err);
}
}


async function readGlobalsCache(): Promise<void> {
if (!globalStorage) {
return;
}
try {
const obj = globalStorage.get<Record<string, ShellGlobalsCacheEntryWithMeta>>(CACHE_KEY);
if (obj) {
for (const key of Object.keys(obj)) {
cachedGlobals.set(key, obj[key]);
}
}
} catch { }
}



export async function activate(context: vscode.ExtensionContext) {
pathExecutableCache = new PathExecutableCache();
context.subscriptions.push(pathExecutableCache);
let currentTerminalEnv: ITerminalEnvironment = process.env;

globalStorage = context.globalState;
await readGlobalsCache();

// Get a machineId for this install (persisted per machine, not synced)
const machineId = await vscode.env.machineId;
const remoteAuthority = vscode.env.remoteName;

context.subscriptions.push(vscode.window.registerTerminalCompletionProvider({
id: 'terminal-suggest',
async provideTerminalCompletions(terminal: vscode.Terminal, terminalContext: vscode.TerminalCompletionContext, token: vscode.CancellationToken): Promise<vscode.TerminalCompletionItem[] | vscode.TerminalCompletionList | undefined> {
Expand All @@ -110,14 +216,20 @@ export async function activate(context: vscode.ExtensionContext) {
return;
}

const commandsInPath = await pathExecutableCache.getExecutablesInPath(terminal.shellIntegration?.env?.value, terminalShellType);
const shellGlobals = await getShellGlobals(terminalShellType, commandsInPath?.labels) ?? [];
const [commandsInPath, shellGlobals] = await Promise.all([
pathExecutableCache.getExecutablesInPath(terminal.shellIntegration?.env?.value, terminalShellType),
(async () => {
const executables = await pathExecutableCache.getExecutablesInPath(terminal.shellIntegration?.env?.value, terminalShellType);
return getShellGlobals(terminalShellType, executables?.labels, machineId, remoteAuthority);
})()
]);
const shellGlobalsArr = shellGlobals ?? [];
if (!commandsInPath?.completionResources) {
console.debug('#terminalCompletions No commands found in path');
return;
}
// Order is important here, add shell globals first so they are prioritized over path commands
const commands = [...shellGlobals, ...commandsInPath.completionResources];
const commands = [...shellGlobalsArr, ...commandsInPath.completionResources];
const currentCommandString = getCurrentCommandAndArgs(terminalContext.commandLine, terminalContext.cursorPosition, terminalShellType);
const pathSeparator = isWindows ? '\\' : '/';
const tokenType = getTokenType(terminalContext, terminalShellType);
Expand Down Expand Up @@ -191,14 +303,12 @@ export async function resolveCwdFromCurrentCommandString(currentCommandString: s
}
const relativeFolder = lastSlashIndex === -1 ? '' : prefix.slice(0, lastSlashIndex);

// Resolve the absolute path of the prefix
const resolvedPath = path.resolve(currentCwd?.fsPath, relativeFolder);

const stat = await fs.stat(resolvedPath);
// Use vscode.Uri.joinPath for path resolution
const resolvedUri = vscode.Uri.joinPath(currentCwd, relativeFolder);

// Check if the resolved path exists and is a directory
if (stat.isDirectory()) {
return currentCwd.with({ path: resolvedPath });
const stat = await vscode.workspace.fs.stat(resolvedUri);
if (stat.type & vscode.FileType.Directory) {
return resolvedUri;
}
} catch {
// Ignore errors
Expand Down