Skip to content

Commit f124cb2

Browse files
authored
bug: terminal triggered package refresh to use correct env (#1175)
fixes #653 Automatic Package Refresh from Terminal When you install, uninstall, or modify packages directly in the terminal using commands like pip install, conda install, poetry add, or pipenv install, the extension automatically detects these changes and refreshes your package list. The extension monitors terminal commands and recognizes package-modifying operations across all common Python package managers—including pip, uv, conda, mamba, poetry, and pipenv. To ensure the correct environment is updated, the extension uses the environment that's actually activated in that specific terminal, not simply the workspace's selected environment. This means if you activate a different virtual environment in a terminal and run pip install requests, the package list refreshes for that activated environment rather than your project's default. If no environment is explicitly activated in the terminal, the extension falls back to matching the environment based on the terminal's working directory and workspace context. This automatic refresh keeps your package views in sync without requiring manual intervention, ensuring that the Environment details panel and other package-related features always reflect your latest changes.
1 parent 7b701b3 commit f124cb2

File tree

4 files changed

+841
-14
lines changed

4 files changed

+841
-14
lines changed

src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import { cleanupStartupScripts } from './features/terminal/shellStartupSetupHand
6464
import { TerminalActivationImpl } from './features/terminal/terminalActivationState';
6565
import { TerminalEnvVarInjector } from './features/terminal/terminalEnvVarInjector';
6666
import { TerminalManager, TerminalManagerImpl } from './features/terminal/terminalManager';
67+
import { registerTerminalPackageWatcher } from './features/terminal/terminalPackageWatcher';
6768
import { getEnvironmentForTerminal } from './features/terminal/utils';
6869
import { EnvManagerView } from './features/views/envManagersView';
6970
import { ProjectView } from './features/views/projectView';
@@ -461,6 +462,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
461462

462463
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api);
463464

465+
// Register manager-agnostic terminal watcher for package-modifying commands
466+
registerTerminalPackageWatcher(api, terminalActivation, outputChannel, context.subscriptions);
467+
464468
// Register listener for interpreter settings changes for interpreter re-selection
465469
context.subscriptions.push(
466470
registerInterpreterSettingsChangeListener(envManagers, projectManager, nativeFinder, api),
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Disposable, LogOutputChannel, Terminal } from 'vscode';
2+
import { PythonEnvironment, PythonEnvironmentApi } from '../../api';
3+
import { traceVerbose } from '../../common/logging';
4+
import { onDidEndTerminalShellExecution } from '../../common/window.apis';
5+
import { TerminalEnvironment } from './terminalActivationState';
6+
import { getEnvironmentForTerminal } from './utils';
7+
8+
/**
9+
* Detects if a terminal command is a package-modifying command that should
10+
* trigger a package list refresh. This is manager-agnostic - it detects
11+
* pip, uv, conda, and poetry commands.
12+
*/
13+
export function isPackageModifyingCommand(command: string): boolean {
14+
// pip install/uninstall (including python -m pip, pip3, uv pip, etc.)
15+
if (/(?:^|\s)(?:\S+\s+)*(?:pip\d*)\s+(install|uninstall)\b/.test(command)) {
16+
return true;
17+
}
18+
19+
// uv pip install/uninstall
20+
if (/(?:^|\s)uv\s+pip\s+(install|uninstall)\b/.test(command)) {
21+
return true;
22+
}
23+
24+
// conda install/remove/uninstall
25+
if (/(?:^|\s)(?:conda|mamba|micromamba)\s+(install|remove|uninstall)\b/.test(command)) {
26+
return true;
27+
}
28+
29+
// poetry add/remove
30+
if (/(?:^|\s)poetry\s+(add|remove)\b/.test(command)) {
31+
return true;
32+
}
33+
34+
// pipenv install/uninstall
35+
if (/(?:^|\s)pipenv\s+(install|uninstall)\b/.test(command)) {
36+
return true;
37+
}
38+
39+
return false;
40+
}
41+
42+
/**
43+
* Gets the environment to use for package refresh in a terminal.
44+
*
45+
* Priority order:
46+
* 1. Terminal's tracked activated environment (from terminalActivation state)
47+
* 2. Environment based on terminal cwd/workspace heuristics
48+
*
49+
* This ensures we use the actual environment activated in the terminal,
50+
* not just the workspace's selected environment.
51+
*/
52+
export async function getEnvironmentForPackageRefresh(
53+
terminal: Terminal,
54+
terminalEnv: TerminalEnvironment,
55+
api: PythonEnvironmentApi,
56+
): Promise<PythonEnvironment | undefined> {
57+
// First try to get the environment that's tracked as activated in this terminal
58+
const activatedEnv = terminalEnv.getEnvironment(terminal);
59+
if (activatedEnv) {
60+
traceVerbose(`Using terminal's activated environment: ${activatedEnv.displayName}`);
61+
return activatedEnv;
62+
}
63+
64+
// Fall back to heuristics based on terminal cwd and workspace
65+
traceVerbose('No activated environment tracked for terminal, using heuristic lookup');
66+
return getEnvironmentForTerminal(api, terminal);
67+
}
68+
69+
/**
70+
* Registers a manager-agnostic terminal watcher that listens for package-modifying
71+
* commands and triggers a refresh on the appropriate package manager for the
72+
* currently selected environment.
73+
*
74+
* This ensures that regardless of what command the user runs (pip, conda, etc.),
75+
* the refresh is performed using the configured package manager for the workspace's
76+
* selected environment.
77+
*/
78+
export function registerTerminalPackageWatcher(
79+
api: PythonEnvironmentApi,
80+
terminalEnv: TerminalEnvironment,
81+
log: LogOutputChannel,
82+
disposables: Disposable[],
83+
): void {
84+
disposables.push(
85+
onDidEndTerminalShellExecution(async (e) => {
86+
const commandLine = e.execution.commandLine.value;
87+
const terminal = e.terminal;
88+
89+
if (isPackageModifyingCommand(commandLine)) {
90+
traceVerbose(`Package-modifying command detected: ${commandLine}`);
91+
92+
try {
93+
// Get the environment for this terminal - prioritizes activated env over workspace selection
94+
const env = await getEnvironmentForPackageRefresh(terminal, terminalEnv, api);
95+
96+
if (env) {
97+
traceVerbose(
98+
`Refreshing packages for environment: ${env.displayName} (${env.envId.managerId})`,
99+
);
100+
// This delegates to the correct package manager based on the environment
101+
await api.refreshPackages(env);
102+
} else {
103+
traceVerbose('No environment found for terminal, skipping package refresh');
104+
}
105+
} catch (error) {
106+
log.error(`Error refreshing packages after terminal command: ${error}`);
107+
}
108+
}
109+
}),
110+
);
111+
}

src/managers/builtin/main.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { Disposable, LogOutputChannel } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { createSimpleDebounce } from '../../common/utils/debounce';
4-
import { onDidEndTerminalShellExecution } from '../../common/window.apis';
54
import { createFileSystemWatcher, onDidDeleteFiles } from '../../common/workspace.apis';
65
import { getPythonApi } from '../../features/pythonApi';
76
import { NativePythonFinder } from '../common/nativePythonFinder';
87
import { PipPackageManager } from './pipManager';
9-
import { isPipInstallCommand } from './pipUtils';
108
import { SysPythonManager } from './sysPythonManager';
119
import { VenvManager } from './venvManager';
1210

@@ -42,16 +40,4 @@ export async function registerSystemPythonFeatures(
4240
venvDebouncedRefresh.trigger();
4341
}),
4442
);
45-
46-
disposables.push(
47-
onDidEndTerminalShellExecution(async (e) => {
48-
const cwd = e.terminal.shellIntegration?.cwd;
49-
if (isPipInstallCommand(e.execution.commandLine.value) && cwd) {
50-
const env = await venvManager.get(cwd);
51-
if (env) {
52-
await pkgManager.refresh(env);
53-
}
54-
}
55-
}),
56-
);
5743
}

0 commit comments

Comments
 (0)