microsoft / vscode

Visual Studio Code
https://code.visualstudio.com
MIT License
162.66k stars 28.68k forks source link

Expose shell integration command knowledge to extensions #145234

Closed Tyriar closed 1 month ago

Tyriar commented 2 years ago

Shell integration in the terminal could enable APIs like being able to listen to commands that run and get their output/exit code.

Rough example:

interface Terminal {
  executeCommand(command): Thenable<TerminalCommandResult>;
}
whitequark commented 6 months ago

@whitequark an event/stream to get the data of a command while it's happening would work though right?

Yep, would work perfectly. Either line-buffered or just chunks of data as they arrive. I would also be OK with specifying a search pattern (a regex perhaps) so that I get an event every time it triggers, which may be a more performant option in some conditions.

gcampbell-msft commented 6 months ago

@Tyriar Is there a way with this proposed API to know what command you are referring to? I would imagine that we'd need to be able to invoke the command, and then get the result.

Or, is it expected to invoke the command, then listen for event and do your own validation for what task it was?

Tyriar commented 6 months ago

Here's another shot at the API which I think satisfies all the requirements:

This round was made close to the tasks API as they share a lot of similarities. I'll bring it to the API sync on Tuesday, any feedback welcome of course!

declare module 'vscode' {

    // https://github.com/microsoft/vscode/issues/145234

    export interface TerminalCommandExecution {
        /**
         * The {@link Terminal} the command was executed in.
         */
        terminal: Terminal;

        /**
         * The full command line that was executed, including both the command and the arguments.
         */
        commandLine: string | undefined;

        /**
         * The current working directory that was reported by the shell. This will be a {@link Uri}
         * if the string reported by the shell can reliably be mapped to the connected machine.
         */
        cwd: Uri | string | undefined;

        /**
         * The exit code reported by the shell.
         */
        exitCode: Thenable<number | undefined>;

        /**
         * The output of the command when it has finished executing. This is the plain text shown in
         * the terminal buffer and does not include raw escape sequences. Depending on the shell
         * setup, this may include the command line as part of the output.
         *
         * This will be updated on each new line
         * TODO: Is might be too expensive to maintain this, perhaps it should be Thenable<string> instead?
         */
        output: string | undefined;

        /**
         * Raw data with escape sequences. This may be batched heavily in order to avoid flooding the
         * ext host connection.
         * 
         * NOTE: `string` was considered but this could get very large and could add a lot of memory pressure
         */
        rawData: string[];

        /**
         * Fires when {@link rawData} changes.
         * 
         * NOTE: `onDidChangeRawData` was considered but it seems more intuitive to fire with a string instead of string[]
         */
        readonly onDidWriteRawData: Event<string>;
        // TODO: Should this be Event<string | { truncatedCount: number }>? Or `onDidTruncateRawData`?
        // How can an extension know when data was truncated for performance reasons?
    }

    export interface Terminal {
        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         * @param commandLine The command line to execute in the terminal.
         */
        executeCommand(commandLine: string): Thenable<TerminalCommandExecution>;
    }

    export namespace window {
        export const onDidStartTerminalCommand: Event<TerminalCommandExecution>;
        export const onDidEndTerminalCommand: Event<TerminalCommandExecution>;
    }
}
Tyriar commented 6 months ago

@gcampbell-msft I think the above gives you what you're after.

whitequark commented 6 months ago
      readonly onDidChangeRawData: Event<string>;

Does the raw data ever get truncated, or does it just grow unbounded if the process e.g. writes 1GB of text to terminal instantly?

If it gets truncated, will it be impossible to reliably detect a substring in the output?

Tyriar commented 6 months ago

@whitequark I think I'll have to limit it somehow, aggressive batching as needed and probably will truncate at some point where it's unreasonable (10s, 100s of mbs?). So yes it would be impossible to detect a substring in the truncated output in that case (unless you used something like the also proposed quick fix provider).

whitequark commented 6 months ago

will truncate at some point where it's unreasonable (10s, 100s of mbs?).

Repeatedly searching a substring in a 100s of MBs string is going to introduce a lot of CPU load. Probably enough to compete with the UI thread on lower-end hardware, in my experience doing similar things in the browser.

Maintaining it might also be expensive.

Tyriar commented 6 months ago

@whitequark oh I mean it's possible an extension will not get some of the data if the command is doing too much. Thinking more about this though, the event itself would probably give the addition to rawData, not rawData itself. We may also want to consider rawData being string[] to be nicer on memory and not keep creating giant strings.

Tyriar commented 6 months ago

@whitequark made some tweaks to better support your problem

whitequark commented 6 months ago

@Tyriar Thank you. It would be extremely useful to know when rawData was truncated. I think this can be achieved by a single integer field indicating how many characters were emitted before the first string in the rawData array. This would start at 0 and increased every time it's truncated, making it possible for an extension to unambiguously determine whether it's misisng any output or not.

Are events guaranteed to fire on each character written to the terminal or not? If the answer is no, then the event probably should use a similar mechanism as the previous paragraph describes, for the same reason.

I believe that output will suffer the same performance issues as rawData, except that making a guarantee that it's updated on each line makes it a lot more severe as now you cannot batch more than a single line.

Tyriar commented 6 months ago

Are events guaranteed to fire on each character written to the terminal or not? If the answer is no, then the event probably should use a similar mechanism as the previous paragraph describes, for the same reason.

@whitequark good point, added:

        readonly onDidWriteRawData: Event<string>;
        // TODO: Should this be Event<string | { truncatedCount: number }>? Or `onDidTruncateRawData`?
        // How can an extension know when data was truncated for performance reasons?

I believe that output will suffer the same performance issues as rawData, except that making a guarantee that it's updated on each line makes it a lot more severe as now you cannot batch more than a single line.

Added:

/**
 * TODO: Is might be too expensive to maintain this, perhaps it should be Thenable<string> instead?
 */
output: string | undefined;
Tyriar commented 6 months ago

Here's the cleaned up proposal with some fixes:

declare module 'vscode' {

    // https://github.com/microsoft/vscode/issues/145234

    export interface TerminalCommandExecution {
        /**
         * The {@link Terminal} the command was executed in.
         */
        terminal: Terminal;

        /**
         * The full command line that was executed, including both the command and the arguments.
         */
        commandLine: string | undefined;

        /**
         * The current working directory that was reported by the shell. This will be a {@link Uri}
         * if the string reported by the shell can reliably be mapped to the connected machine.
         */
        cwd: Uri | string | undefined;

        /**
         * The exit code reported by the shell.
         * 
         * *Note* This will be rejected if the terminal is determined to not have shell integration
         * activated.
         */
        exitCode: Thenable<number | undefined>;

        /**
         * The output of the command when it has finished executing. This is the plain text shown in
         * the terminal buffer and does not include raw escape sequences. Depending on the shell
         * setup, this may include the command line as part of the output.
         *
         * *Note* This will be rejected if the terminal is determined to not have shell integration
         * activated.
         */
        output: Thenable<string>;

        /**
         * Raw data with escape sequences. This may be batched heavily in order to avoid flooding the
         * ext host connection.
         */
        rawData: string[];
        // NOTE: `string` was considered but this could get very large and could add a lot of memory pressure

        /**
         * Fires when {@link rawData} changes.
         */
        readonly onDidWriteRawData: Event<string>;
        // NOTE: `onDidChangeRawData` was considered but it seems more intuitive to fire with a string instead of string[]

        /**
         * Fires when {@link rawData} was truncated for performance reasons. This can happen when
         * the process is writing a large amount of data to the terminal very quickly.
         */
        readonly onDidTruncateRawData: Event<number>;
    }

    export interface Terminal {
        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         * 
         * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)
         * must be activated. Check whether {@link TerminalCommandExecution.exitCode} is rejected to
         * verify whether it was successful.
         * 
         * @param commandLine The command line to execute in the terminal.
         * 
         * @example
         * const command = term.executeCommand('echo "Hello world"');
         * // Fallback to sendText on possible failure
         * command.exitCode
         *   .catch(() => term.sendText('echo "Hello world"'));
         */
        executeCommand(commandLine: string): TerminalCommandExecution;
    }

    export namespace window {
        /**
         * This will be fired when a terminal command is started. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidStartTerminalCommand: Event<TerminalCommandExecution>;

        /**
         * This will be fired when a terminal command is ended. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidEndTerminalCommand: Event<TerminalCommandExecution>;
    }
}
Tyriar commented 6 months ago

Feedback from standup was that people don't want to do escaping themselves, so maybe we have another signature like this:

// Would probably be an options object instead. Not sure if `quoting?` would be needed or
// if we can figure everything out without that
executeCommand(executable: string, args: string[], quoting?: ShellQuoting): TerminalCommandExecution;
starball5 commented 6 months ago

@Tyriar How would the extension know what shell is currently active in the terminal if they wanted to manual quoting? Also, why is args a single string and not an array of strings? How is automatic quoting going to work if args is a single string of (presumably) multiple arguments?

DonJayamanne commented 6 months ago

why is args a single string and not an array of strings

Yes agreed, I too missed that one.

Jim-Elijah commented 6 months ago

Anyone still tracks this issue? Something unexpected happens when I use proposed API terminalExecuteCommandEvent. I followed https://code.visualstudio.com/api/advanced-topics/using-proposed-api#using-a-proposed-api to use terminalExecuteCommandEvent but onDidExecuteTerminalCommand is not triggered. However, it is fine with terminalDataWriteEvent. That is wierd. Any help is appreciated, guys.

image

image

extension.ts

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    console.log('Congratulations, your extension "proposed-api-sample" is now active!');

    /**
     * You can use proposed API here. `vscode.` should start auto complete
     * Proposed API as defined in vscode.proposed.<proposalName>.d.ts.
     */
    vscode.window.onDidExecuteTerminalCommand(({ terminal, commandLine, cwd, exitCode, output }) => {
        console.log('onDidExecuteTerminalCommand')
        console.log('name', terminal.name)
        console.log('commandLine', commandLine)
        console.log('cwd', cwd)
        console.log('exitCode', exitCode)
        console.log('output', output)
        console.log(`--------------------------------`)
    })

    vscode.window.onDidCloseTerminal((terminal) => {
        console.log('onDidCloseTerminal', terminal.name)
    })
    vscode.window.onDidOpenTerminal((terminal) => {
        console.log('onDidOpenTerminal', terminal.name)
    })

    context.subscriptions.push(vscode.commands.registerCommand('extension.helloWorld', () => {
        vscode.window.showInformationMessage('Hello World!');
    }));

    let terminal: vscode.Terminal
    context.subscriptions.push(vscode.commands.registerCommand("extension.sendText", async () => {
        const cmd = await vscode.window.showInputBox({ placeHolder: "Enter command to run in terminal" })
        if (!cmd) {
            return;
        }
        if (!terminal) {
            terminal = vscode.window.createTerminal({ name: 'test-proposal-api' })
        }
        terminal.show()
        terminal.sendText(cmd)
    }));
}

package.json

{
    "enabledApiProposals": [
        "terminalExecuteCommandEvent"
    ],
    "name": "proposed-api-sample",
    "displayName": "proposed-api-sample",
    "description": "Sample showing how to use Proposed API",
    "version": "0.0.1",
    "publisher": "jim-vscode-samples",
    "private": true,
    "license": "MIT",
    "engines": {
        "vscode": "^1.74.0"
    },
    "categories": [
        "Other"
    ],
    "activationEvents": [],
    "main": "./dist/extension.js",
    "contributes": {
        "commands": [
            {
                "command": "extension.helloWorld",
                "title": "Hello World"
            },
            {
                "command": "extension.sendText",
                "title": "Send Text to Terminal"
            }
        ]
    },
    "scripts": {
        "compile": "tsc -p ./",
        "lint": "eslint \"src/**/*.ts\"",
        "watch": "webpack --watch",
        "compile-tests": "tsc -watch -p ./",
        "download-api": "dts dev",
        "postdownload-api": "dts main",
        "post111install": "npm run download-api",
        "package": "webpack --mode production --devtool hidden-source-map && vsce package"
    },
    "devDependencies": {
        "@types/node": "^16.18.34",
        "@typescript-eslint/eslint-plugin": "^6.7.0",
        "@typescript-eslint/parser": "^6.7.0",
        "@vscode/dts": "^0.4.0",
        "eslint": "^8.26.0",
        "node-loader": "^2.0.0",
        "ts-loader": "^9.5.1",
        "typescript": "^5.2.2",
        "webpack": "^5.90.3",
        "webpack-cli": "^5.1.4"
    }
}
Jim-Elijah commented 6 months ago

I know why it happened. It turned out that shell integration had to be manually installed https://code.visualstudio.com/docs/terminal/shell-integration#_manual-installation, and changed powershell's execution policy to Unrestricted(Set-ExecutionPolicy Unrestricted with powershell run as administrator, but I don't think it's a good way). image image If powershell's execution policy is restricted, script below added to $Profile causes an execution policy error, and onDidExecuteTerminalCommand will not be triggered. image So I will be seeking a way to use onDidExecuteTerminalCommand without changing execution policy.

Tyriar commented 6 months ago

Update:

declare module 'vscode' {

    // https://github.com/microsoft/vscode/issues/145234

    export interface TerminalCommandExecution {
        /**
         * The {@link Terminal} the command was executed in.
         */
        terminal: Terminal;

        /**
         * The full command line that was executed, including both the command and the arguments.
         */
        commandLine: string | undefined;

        /**
         * The current working directory that was reported by the shell. This will be a {@link Uri}
         * if the string reported by the shell can reliably be mapped to the connected machine.
         */
        cwd: Uri | string | undefined;

        /**
         * The exit code reported by the shell.
         * 
         * *Note* This will be rejected if the terminal is determined to not have shell integration
         * activated.
         */
        exitCode: Thenable<number | undefined>;

        /**
         * The output of the command when it has finished executing. This is the plain text shown in
         * the terminal buffer and does not include raw escape sequences. Depending on the shell
         * setup, this may include the command line as part of the output.
         *
         * *Note* This will be rejected if the terminal is determined to not have shell integration
         * activated.
         */
        output: Thenable<string>;

        /**
         * Raw data with escape sequences. This may be batched heavily in order to avoid flooding the
         * ext host connection.
         */
        rawData: string[];
        // NOTE: `string` was considered but this could get very large and could add a lot of memory pressure

        /**
         * Fires when {@link rawData} changes.
         */
        readonly onDidWriteRawData: Event<string>;
        // NOTE: `onDidChangeRawData` was considered but it seems more intuitive to fire with a string instead of string[]

        /**
         * Fires when {@link rawData} was truncated for performance reasons. This can happen when
         * the process is writing a large amount of data to the terminal very quickly.
         */
        readonly onDidTruncateRawData: Event<number>;
    }

    export interface TerminalCommandExecutionOptions {
        // TODO: These could be split into 2 separate interfaces, or 2 separate option interfaces?
        /**
         * The command line to use.
         */
        commandLine: string | {
            /**
             * An executable to use which will be automatically escaped.
             */
            executable: string;
            /**
             * Arguments to launch the executable with which will be automatically escaped.
             */
            args: string[];
        }
    }

    export interface Terminal {
        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         * 
         * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)
         * must be activated. Check whether {@link TerminalCommandExecution.exitCode} is rejected to
         * verify whether it was successful.
         * 
         * @param options The options to use for the command.
         * 
         * @example
         * const command = term.executeCommand({
         *   commandLine: 'echo "Hello world"'
         * });
         * // Fallback to sendText on possible failure
         * command.exitCode
         *   .catch(() => term.sendText('echo "Hello world"'));
         */
        executeCommand(options: TerminalCommandExecutionOptions): TerminalCommandExecution;
    }

    export namespace window {
        /**
         * This will be fired when a terminal command is started. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidStartTerminalCommand: Event<TerminalCommandExecution>;

        /**
         * This will be fired when a terminal command is ended. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidEndTerminalCommand: Event<TerminalCommandExecution>;
    }
}
Tyriar commented 6 months ago

Lots of feedback from API sync:

Tyriar commented 6 months ago

Update after feedback:

declare module 'vscode' {

    // https://github.com/microsoft/vscode/issues/145234

    export interface TerminalShellExecution {
        /**
         * The {@link Terminal} the command was executed in.
         */
        terminal: Terminal;

        /**
         * The full command line that was executed, including both the command and the arguments.
         */
        commandLine: string | undefined;

        /**
         * The current working directory that was reported by the shell. This will be a {@link Uri}
         * if the string reported by the shell can reliably be mapped to the connected machine.
         */
        cwd: Uri | string | undefined;

        /**
         * The exit code reported by the shell.
         */
        exitCode: Thenable<number | undefined>;

        /**
         * The output of the command when it has finished executing. This is the plain text shown in
         * the terminal buffer and does not include raw escape sequences. Depending on the shell
         * setup, this may include the command line as part of the output.
         *
         * *Note* This will be rejected if the terminal is determined to not have shell integration
         * activated.
         */
        // output: Thenable<string>;
        // TODO: TBD based on terminal buffer exploration.

        /**
         * A per-extension stream of raw data (including escape sequences) that is written to the
         * terminal. This will only include data that was written after `stream` was called for the
         * first time, ie. you must call `stream` immediately after the command is executed via
         * {@link executeCommand} or {@link onDidStartTerminalShellExecution}`to not miss any data.
         */
        dataStream: AsyncIterator<TerminalShellExecutionData>;
    }

    export interface TerminalShellExecutionData {
        /**
         * The data that was written to the terminal.
         */
        data: string;

        /**
         * The number of characters that were truncated. This can happen when the process writes a
         * large amount of data very quickly. If this is non-zero, the data will be the empty
         * string.
         */
        truncatedCount: number;
    }

    export interface TerminalShellExecutionOptions {
        // TODO: These could be split into 2 separate interfaces, or 2 separate option interfaces?
        /**
         * The command line to use.
         */
        commandLine: string | {
            /**
             * An executable to use.
             */
            executable: string;
            /**
             * Arguments to launch the executable with which will be automatically escaped based on
             * the executable type.
             */
            args: string[];
        }
    }

    export interface Terminal {
        shellIntegration?: TerminalShellIntegration;
    }

    export interface TerminalShellIntegration {
        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         *
         * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)
         * must be activated. Check whether {@link TerminalShellExecution.exitCode} is rejected to
         * verify whether it was successful.
         *
         * @param options The options to use for the command.
         *
         * @example
         * const command = term.executeCommand({
         *   commandLine: 'echo "Hello world"'
         * });
         * // Fallback to sendText on possible failure
         * command.exitCode
         *   .catch(() => term.sendText('echo "Hello world"'));
         */
        executeCommand(options: TerminalShellExecutionOptions): TerminalShellExecution;
    }

    export interface TerminalShellIntegrationEvent {
        /**
         * The terminal that shell integration has been activated in.
         */
        terminal: Terminal;
        /**
         * The shell integration object.
         */
        shellIntegration: TerminalShellIntegration;
    }

    export namespace window {
        /**
         * Fires when shell integration activates in a terminal
         */
        export const onDidActivateTerminalShellIntegration: Event<TerminalShellIntegrationEvent>;

        /**
         * This will be fired when a terminal command is started. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidStartTerminalShellExecution: Event<TerminalShellExecution>;

        /**
         * This will be fired when a terminal command is ended. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidEndTerminalShellExecution: Event<TerminalShellExecution>;
    }
}
Tyriar commented 6 months ago

Rough draft of what a terminal buffer API could look like: https://github.com/microsoft/vscode/issues/207504#issuecomment-1992449065

starball5 commented 6 months ago

@Tyriar what happened to the discussion about automatic shell quoting? Does the string signature of commandLine mean no automatic shell quoting, and the object signature mean yes automatic shell quoting?

Tyriar commented 6 months ago

@starball5 yep, I don't think we need shell quoting. You can pass the command line directly, or let VS Code generate the command line (with quoting) based on the executable type (eg. pwsh arg quoting is a little different to other shells).

Tyriar commented 6 months ago

Update (from tyriar/apis):

declare module 'vscode' {

    // https://github.com/microsoft/vscode/issues/145234

    export interface TerminalShellExecution {
        /**
         * The {@link Terminal} the command was executed in.
         */
        terminal: Terminal;

        /**
         * The full command line that was executed, including both the command and the arguments.
         */
        commandLine: string | undefined;

        /**
         * The current working directory that was reported by the shell. This will be a {@link Uri}
         * if the string reported by the shell can reliably be mapped to the connected machine.
         */
        cwd: Uri | string | undefined;

        /**
         * The exit code reported by the shell.
         */
        exitCode: Thenable<number | undefined>;

        /**
         * The output of the command when it has finished executing. This is the plain text shown in
         * the terminal buffer and does not include raw escape sequences. Depending on the shell
         * setup, this may include the command line as part of the output.
         *
         * *Note* This will be rejected if the terminal is determined to not have shell integration
         * activated.
         */
        // output: Thenable<string>;
        // TODO: TBD based on terminal buffer exploration.

        /**
         * A per-extension stream of raw data (including escape sequences) that is written to the
         * terminal. This will only include data that was written after `stream` was called for the
         * first time, ie. you must call `dataStream` immediately after the command is executed via
         * {@link executeCommand} or {@link onDidStartTerminalShellExecution} to not miss any data.
         *
         * @example
         * // Log all data written to the terminal for a command
         * const command = term.shellIntegration.executeCommand({ commandLine: 'echo "Hello world"' });
         * for await (const e of command.dataStream) {
         *   console.log(e.data);
         *   if (e.truncatedCount) {
         *     console.warn(`Data was truncated by ${e.truncatedCount} characters`);
         *   }
         * }
         */
        dataStream: AsyncIterator<TerminalShellExecutionData>;
    }

    export interface TerminalShellExecutionData {
        /**
         * The data that was written to the terminal.
         */
        data: string;

        /**
         * The number of characters that were truncated. This can happen when the process writes a
         * large amount of data very quickly. If this is non-zero, the data will be the empty
         * string.
         */
        truncatedCount: number;
    }

    export interface TerminalShellExecutionOptions {
        // TODO: These could be split into 2 separate interfaces, or 2 separate options interfaces?
        /**
         * The command line to use.
         */
        commandLine: string | {
            /**
             * An executable to use.
             */
            executable: string;
            /**
             * Arguments to launch the executable with which will be automatically escaped based on
             * the executable type.
             */
            args: string[];
        };
    }

    export interface Terminal {
        /**
         * An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered
         * features for the terminal. This will always be undefined immediately after the terminal
         * is created. Listen to {@link window.onDidActivateTerminalShellIntegration} to be notified
         * when shell integration is activated for a terminal.
         */
        shellIntegration?: TerminalShellIntegration;
    }

    export interface TerminalShellIntegration {
        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         *
         * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)
         * must be activated. Check whether {@link TerminalShellExecution.exitCode} is rejected to
         * verify whether it was successful.
         *
         * @param options The options to use for the command.
         *
         * @example
         * // Execute a command in a terminal immediately after being created
         * const myTerm = window.createTerminal();
         * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => {
         *   if (terminal === myTerm) {
         *     const command = shellIntegration.executeCommand({
         *       commandLine: 'echo "Hello world"'
         *     });
         *     const code = await command.exitCode;
         *     console.log(`Command exited with code ${code}`);
         *   }
         * }));
         * // Fallback to sendText if there is no shell integration
         * setTimeout(() => {
         *   if (!myTerm.shellIntegration) {
         *     myTerm.sendText('echo "Hello world"');
         *     // Without shell integration, we can't know when the command has finished
         *   }
         * }, 3000);
         *
         * @example
         * // Send command to terminal that has been alive for a while
         * const commandLine = 'echo "Hello world"';
         * if (term.shellIntegration) {
         *   const command = term.shellIntegration.executeCommand({ commandLine });
         *   command.exitCode.then(code => {
         *     console.log(`Command exited with code ${code}`);
         *   });
         * } else {
         *   term.sendText(commandLine);
         *   // Without shell integration, we can't know when the command has finished
         * }
         */
        executeCommand(options: TerminalShellExecutionOptions): TerminalShellExecution;
    }

    export interface TerminalShellIntegrationActivationEvent {
        /**
         * The terminal that shell integration has been activated in.
         */
        terminal: Terminal;
        /**
         * The shell integration object.
         */
        shellIntegration: TerminalShellIntegration;
    }

    export namespace window {
        // TODO: This could be onDidChange... and also report changes to features:
        //
        // ```ts
        // features: {
        //  cwdReporting: boolean;
        //  explicitCommandLineReporting: boolean;
        // };
        // ```
        //
        // Maybe extensions shouldn't be concerned about these features though?

        /**
         * Fires when shell integration activates in a terminal.
         */
        export const onDidActivateTerminalShellIntegration: Event<TerminalShellIntegrationActivationEvent>;

        /**
         * This will be fired when a terminal command is started. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidStartTerminalShellExecution: Event<TerminalShellExecution>;

        /**
         * This will be fired when a terminal command is ended. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidEndTerminalShellExecution: Event<TerminalShellExecution>;
    }
}
Tyriar commented 6 months ago

Feedback from API sync:

karrtikr commented 6 months ago

Switch to onDidChangeTerminalShellIntegration to future proof us

I was thinking that this may also be useful now if "terminal.integrated.shellIntegration.enabled" is toggled.

cwdReporting: boolean;

I recall there has been a few cases when we're interested in cwd of a user, so it could potentially be a useful feature to include. I can look for such instances in the Python extension to back this.

Tyriar commented 6 months ago

I was thinking that this may also be useful now if "terminal.integrated.shellIntegration.enabled" is toggled.

terminal.integrated.shellIntegration.enabled doesn't necessarily mean shell integration will work or not, just whether VS Code tried to inject the script.

I recall there has been a few cases when we're interested in cwd of a user, so it could potentially be a useful feature to include. I can look for such instances in the Python extension to back this.

Good point, I'll think about adding a cwd property to

Tyriar commented 6 months ago

Update:

declare module 'vscode' {

    // https://github.com/microsoft/vscode/issues/145234

    export interface TerminalShellExecution {
        /**
         * The {@link Terminal} the command was executed in.
         */
        terminal: Terminal;

        /**
         * The full command line that was executed, including both the command and arguments.
         */
        commandLine: string | undefined;

        /**
         * The working directory that was reported by the shell when this command executed. This
         * will be a {@link Uri} if the string reported by the shell can reliably be mapped to the
         * connected machine.
         */
        cwd: Uri | string | undefined;

        /**
         * The exit code reported by the shell.
         */
        exitCode: Thenable<number | undefined>;

        /**
         * A per-extension stream of raw data (including escape sequences) that is written to the
         * terminal. This will only include data that was written after `stream` was called for the
         * first time, ie. you must call `dataStream` immediately after the command is executed via
         * {@link executeCommand} or {@link onDidStartTerminalShellExecution} to not miss any data.
         *
         * @example
         * // Log all data written to the terminal for a command
         * const command = term.shellIntegration.executeCommand({ commandLine: 'echo "Hello world"' });
         * for await (const data of command.dataStream) {
         *   console.log(data);
         * }
         */
        dataStream: AsyncIterator<string>;
    }

    export interface Terminal {
        /**
         * An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered
         * features for the terminal. This will always be undefined immediately after the terminal
         * is created. Listen to {@link window.onDidActivateTerminalShellIntegration} to be notified
         * when shell integration is activated for a terminal.
         */
        shellIntegration?: TerminalShellIntegration;
    }

    export interface TerminalShellIntegration {
        // TODO: Is this fine to share the TerminalShellIntegrationChangeEvent event?
        // TODO: Should we have TerminalShellExecution.cwd if this exists?
        /**
         * The current working directory of the terminal. This will be a {@link Uri} if the string
         * reported by the shell can reliably be mapped to the connected machine.
         */
        cwd: Uri | string | undefined;

        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         *
         * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)
         * must be activated. Check whether {@link TerminalShellExecution.exitCode} is rejected to
         * verify whether it was successful.
         *
         * @param commandLine The command line to execute, this is the exact text that will be sent
         * to the terminal.
         *
         * @example
         * // Execute a command in a terminal immediately after being created
         * const myTerm = window.createTerminal();
         * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => {
         *   if (terminal === myTerm) {
         *     const command = shellIntegration.executeCommand('echo "Hello world"');
         *     const code = await command.exitCode;
         *     console.log(`Command exited with code ${code}`);
         *   }
         * }));
         * // Fallback to sendText if there is no shell integration within 3 seconds of launching
         * setTimeout(() => {
         *   if (!myTerm.shellIntegration) {
         *     myTerm.sendText('echo "Hello world"');
         *     // Without shell integration, we can't know when the command has finished or what the
         *     // exit code was.
         *   }
         * }, 3000);
         *
         * @example
         * // Send command to terminal that has been alive for a while
         * const commandLine = 'echo "Hello world"';
         * if (term.shellIntegration) {
         *   const command = term.shellIntegration.executeCommand({ commandLine });
         *   const code = await command.exitCode;
         *   console.log(`Command exited with code ${code}`);
         * } else {
         *   term.sendText(commandLine);
         *   // Without shell integration, we can't know when the command has finished or what the
         *   // exit code was.
         * }
         */
        executeCommand(commandLine: string): TerminalShellExecution;

        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         *
         * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)
         * must be activated. Check whether {@link TerminalShellExecution.exitCode} is rejected to
         * verify whether it was successful.
         *
         * @param command A command to run.
         * @param args Arguments to launch the executable with which will be automatically escaped
         * based on the executable type.
         *
         * @example
         * // Execute a command in a terminal immediately after being created
         * const myTerm = window.createTerminal();
         * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => {
         *   if (terminal === myTerm) {
         *     const command = shellIntegration.executeCommand({
         *       command: 'echo',
         *       args: ['Hello world']
         *     });
         *     const code = await command.exitCode;
         *     console.log(`Command exited with code ${code}`);
         *   }
         * }));
         * // Fallback to sendText if there is no shell integration within 3 seconds of launching
         * setTimeout(() => {
         *   if (!myTerm.shellIntegration) {
         *     myTerm.sendText('echo "Hello world"');
         *     // Without shell integration, we can't know when the command has finished or what the
         *     // exit code was.
         *   }
         * }, 3000);
         *
         * @example
         * // Send command to terminal that has been alive for a while
         * const commandLine = 'echo "Hello world"';
         * if (term.shellIntegration) {
         *   const command = term.shellIntegration.executeCommand({
         *     command: 'echo',
         *     args: ['Hello world']
         *   });
         *   const code = await command.exitCode;
         *   console.log(`Command exited with code ${code}`);
         * } else {
         *   term.sendText(commandLine);
         *   // Without shell integration, we can't know when the command has finished or what the
         *   // exit code was.
         * }
         */
        executeCommand(executable: string, args: string[]): TerminalShellExecution;
    }

    export interface TerminalShellIntegrationChangeEvent {
        /**
         * The terminal that shell integration has been activated in.
         */
        terminal: Terminal;
        /**
         * The shell integration object.
         */
        shellIntegration: TerminalShellIntegration;
    }

    export namespace window {
        /**
         * Fires when shell integration activates or one of its properties changes in a terminal.
         */
        export const onDidChangeTerminalShellIntegration: Event<TerminalShellIntegrationChangeEvent>;

        /**
         * This will be fired when a terminal command is started. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidStartTerminalShellExecution: Event<TerminalShellExecution>;

        /**
         * This will be fired when a terminal command is ended. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidEndTerminalShellExecution: Event<TerminalShellExecution>;
    }
}
karrtikr commented 6 months ago

This sounds terminal specific:

export namespace window {
        /**
         * Fires when shell integration activates or one of its properties changes in a terminal.
         */
        export const onDidChangeTerminalShellIntegration: Event<TerminalShellIntegrationChangeEvent>;
        ...

Curious, why doesn't these live under the Terminal scope? As a consumer, I only care about a specific terminal, and once that is done, I can dispose the event rather than listening for changes to all terminals.

shellIntegration?: TerminalShellIntegration;

As per API guidelines, I would expect a producer object to be:

shellIntegration: TerminalShellIntegration | undefined;

image

Tyriar commented 6 months ago

Curious, why doesn't these live under the Terminal scope?

@karrtikr because of this; global objects must have global events.

As per API guidelines, I would expect a producer object to be:

Done!

Tyriar commented 5 months ago

TODOs added in https://github.com/microsoft/vscode/pull/209548

Tyriar commented 5 months ago

Latest:

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

declare module 'vscode' {

    // https://github.com/microsoft/vscode/issues/145234

    // TODO: Add missing docs
    // TODO: Review and polish up all docs
    export interface TerminalShellExecution {
        /**
         * The full command line that was executed, including both the command and arguments. The
         * {@link TerminalShellExecutionCommandLineConfidence confidence} of this value depends on
         * the specific shell's shell integration implementation. This value may become more
         * accurate after {@link onDidEndTerminalShellExecution} is fired.
         */
        readonly commandLine: TerminalShellExecutionCommandLine;

        /**
         * The working directory that was reported by the shell when this command executed. This
         * will be a {@link Uri} if the path reported by the shell can reliably be mapped to the
         * connected machine. This requires the shell integration to support working directory
         * reporting.
         */
        readonly cwd: Uri | undefined;

        /**
         * Creates a stream of raw data (including escape sequences) that is written to the
         * terminal. This will only include data that was written after `stream` was called for the
         * first time, ie. you must call `dataStream` immediately after the command is executed via
         * {@link executeCommand} or {@link onDidStartTerminalShellExecution} to not miss any data.
         *
         * @example
         * // Log all data written to the terminal for a command
         * const command = term.shellIntegration.executeCommand({ commandLine: 'echo "Hello world"' });
         * const stream = command.readData();
         * for await (const data of stream) {
         *   console.log(data);
         * }
         */
        // TODO: read? "data" typically means Uint8Array. What's the encoding of the string? Usage here will typically be checking for substrings
        // TODO: dispose function?
        readData(): AsyncIterable<string>;
        // read(): AsyncIterable<string>;
        // createReader(): { stream: AsyncIterable<string>; dispose: () => void }
        // createReadStream(): { stream: AsyncIterable<string>; dispose: () => void };
        //
        // interface DisposableAsyncIterable<T> extends Disposable {
        //  read(): AsyncIterable<T>;
        // }
    }

    /**
     * A command line that was executed in a terminal.
     */
    export interface TerminalShellExecutionCommandLine {
        /**
         * The full command line that was executed, including both the command and its arguments.
         */
        value: string;

        /**
         * Whether the command line value came from a trusted source and is therefore safe to
         * execute without user additional confirmation, such as a notification that asks "Do you
         * want to execute (command)?".
         *
         * This is false when the command line was reported explicitly by the shell integration
         * script (ie. {@link TerminalShellExecutionCommandLineConfidence.High high confidence}),
         * but did not include a nonce for verification.
         */
        isTrusted: boolean;

        /**
         * The confidence of the command line value which is determined by how the value was
         * obtained. This depends upon the implementation of the shell integration script.
         */
        confidence: TerminalShellExecutionCommandLineConfidence;
    }

    /**
     * The confidence of a {@link TerminalShellExecutionCommandLine} value.
     */
    enum TerminalShellExecutionCommandLineConfidence {
        /**
         * The command line value confidence is low. This means that the value was read from the
         * terminal buffer using markers reported by the shell integration script. Additionally one
         * of the following conditions will be met:
         *
         * - The command started on the very left-most column which is unusual, or
         * - The command is multi-line which is more difficult to accurately detect due to line
         *   continuation characters and right prompts.
         * - Command line markers were not reported by the shell integration script.
         */
        Low = 0,

        /**
         * The command line value confidence is medium. This means that the value was read from the
         * terminal buffer using markers reported by the shell integration script. The command is
         * single-line and does not start on the very left-most column (which is unusual).
         */
        Medium = 1,

        /**
         * The command line value confidence is high. This means that the value was explicitly send
         * from the shell integration script.
         */
        High = 2
    }

    export interface Terminal {
        /**
         * An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered
         * features for the terminal. This will always be undefined immediately after the terminal
         * is created. Listen to {@link window.onDidActivateTerminalShellIntegration} to be notified
         * when shell integration is activated for a terminal.
         *
         * Note that this object may remain undefined if shell integation never activates. For
         * example Command Prompt does not support shell integration and a user's shell setup could
         * conflict with the automatic shell integration activation.
         */
        readonly shellIntegration: TerminalShellIntegration | undefined;
    }

    export interface TerminalShellIntegration {
        /**
         * The current working directory of the terminal. This will be a {@link Uri} if the path
         * reported by the shell can reliably be mapped to the connected machine.
         */
        readonly cwd: Uri | undefined;

        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         *
         * @param commandLine The command line to execute, this is the exact text that will be sent
         * to the terminal.
         *
         * @example
         * // Execute a command in a terminal immediately after being created
         * const myTerm = window.createTerminal();
         * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => {
         *   if (terminal === myTerm) {
         *     const command = shellIntegration.executeCommand('echo "Hello world"');
         *     const code = await command.exitCode;
         *     console.log(`Command exited with code ${code}`);
         *   }
         * }));
         * // Fallback to sendText if there is no shell integration within 3 seconds of launching
         * setTimeout(() => {
         *   if (!myTerm.shellIntegration) {
         *     myTerm.sendText('echo "Hello world"');
         *     // Without shell integration, we can't know when the command has finished or what the
         *     // exit code was.
         *   }
         * }, 3000);
         *
         * @example
         * // Send command to terminal that has been alive for a while
         * const commandLine = 'echo "Hello world"';
         * if (term.shellIntegration) {
         *   const command = term.shellIntegration.executeCommand({ commandLine });
         *   const code = await command.exitCode;
         *   console.log(`Command exited with code ${code}`);
         * } else {
         *   term.sendText(commandLine);
         *   // Without shell integration, we can't know when the command has finished or what the
         *   // exit code was.
         * }
         */
        executeCommand(commandLine: string): TerminalShellExecution;

        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         *
         * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)
         * must be activated. Check whether {@link TerminalShellExecution.exitCode} is rejected to
         * verify whether it was successful.
         *
         * @param command A command to run.
         * @param args Arguments to launch the executable with which will be automatically escaped
         * based on the executable type.
         *
         * @example
         * // Execute a command in a terminal immediately after being created
         * const myTerm = window.createTerminal();
         * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => {
         *   if (terminal === myTerm) {
         *     const command = shellIntegration.executeCommand({
         *       command: 'echo',
         *       args: ['Hello world']
         *     });
         *     const code = await command.exitCode;
         *     console.log(`Command exited with code ${code}`);
         *   }
         * }));
         * // Fallback to sendText if there is no shell integration within 3 seconds of launching
         * setTimeout(() => {
         *   if (!myTerm.shellIntegration) {
         *     myTerm.sendText('echo "Hello world"');
         *     // Without shell integration, we can't know when the command has finished or what the
         *     // exit code was.
         *   }
         * }, 3000);
         *
         * @example
         * // Send command to terminal that has been alive for a while
         * const commandLine = 'echo "Hello world"';
         * if (term.shellIntegration) {
         *   const command = term.shellIntegration.executeCommand({
         *     command: 'echo',
         *     args: ['Hello world']
         *   });
         *   const code = await command.exitCode;
         *   console.log(`Command exited with code ${code}`);
         * } else {
         *   term.sendText(commandLine);
         *   // Without shell integration, we can't know when the command has finished or what the
         *   // exit code was.
         * }
         */
        executeCommand(executable: string, args: string[]): TerminalShellExecution;
    }

    export interface TerminalShellIntegrationChangeEvent {
        /**
         * The terminal that shell integration has been activated in.
         */
        readonly terminal: Terminal;

        /**
         * The shell integration object.
         */
        readonly shellIntegration: TerminalShellIntegration;
    }

    export interface TerminalShellExecutionStartEvent {
        /**
         * The terminal that shell integration has been activated in.
         */
        readonly terminal: Terminal;

        /**
         * The shell integration object.
         */
        readonly shellIntegration: TerminalShellIntegration;

        /**
         * The terminal shell execution that has ended.
         */
        readonly execution: TerminalShellExecution;
    }

    export interface TerminalShellExecutionEndEvent {
        /**
         * The terminal that shell integration has been activated in.
         */
        readonly terminal: Terminal;

        /**
         * The shell integration object.
         */
        readonly shellIntegration: TerminalShellIntegration;

        /**
         * The terminal shell execution that has ended.
         */
        readonly execution: TerminalShellExecution;

        /**
         * The exit code reported by the shell. `undefined` means the shell did not report an exit
         * code or the shell reported a command started before the command finished.
         */
        readonly exitCode: number | undefined;
    }

    export namespace window {
        /**
         * Fires when shell integration activates or one of its properties changes in a terminal.
         */
        export const onDidChangeTerminalShellIntegration: Event<TerminalShellIntegrationChangeEvent>;

        /**
         * This will be fired when a terminal command is started. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidStartTerminalShellExecution: Event<TerminalShellExecutionStartEvent>;

        /**
         * This will be fired when a terminal command is ended. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidEndTerminalShellExecution: Event<TerminalShellExecutionEndEvent>;
    }
}
Tyriar commented 5 months ago

Very close to the final API! We will wait for some feedback from users before finalizing.

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

declare module 'vscode' {

    // https://github.com/microsoft/vscode/issues/145234

    /**
     * A command that was executed in a terminal.
     */
    export interface TerminalShellExecution {
        /**
         * The command line that was executed. The {@link TerminalShellExecutionCommandLineConfidence confidence}
         * of this value depends on the specific shell's shell integration implementation. This
         * value may become more accurate after {@link window.onDidEndTerminalShellExecution} is
         * fired.
         *
         * @example
         * // Log the details of the command line on start and end
         * window.onDidStartTerminalShellExecution(event => {
         *   const commandLine = event.execution.commandLine;
         *   console.log(`Command started\n${summarizeCommandLine(commandLine)}`);
         * });
         * window.onDidEndTerminalShellExecution(event => {
         *   const commandLine = event.execution.commandLine;
         *   console.log(`Command ended\n${summarizeCommandLine(commandLine)}`);
         * });
         * function summarizeCommandLine(commandLine: TerminalShellExecutionCommandLine) {
         *   return [
         *     `  Command line: ${command.ommandLine.value}`,
         *     `  Confidence: ${command.ommandLine.confidence}`,
         *     `  Trusted: ${command.ommandLine.isTrusted}
         *   ].join('\n');
         * }
         */
        readonly commandLine: TerminalShellExecutionCommandLine;

        /**
         * The working directory that was reported by the shell when this command executed. This
         * {@link Uri} may represent a file on another machine (eg. ssh into another machine). This
         * requires the shell integration to support working directory reporting.
         */
        readonly cwd: Uri | undefined;

        /**
         * Creates a stream of raw data (including escape sequences) that is written to the
         * terminal. This will only include data that was written after `readData` was called for
         * the first time, ie. you must call `readData` immediately after the command is executed
         * via {@link TerminalShellIntegration.executeCommand} or
         * {@link window.onDidStartTerminalShellExecution} to not miss any data.
         *
         * @example
         * // Log all data written to the terminal for a command
         * const command = term.shellIntegration.executeCommand({ commandLine: 'echo "Hello world"' });
         * const stream = command.read();
         * for await (const data of stream) {
         *   console.log(data);
         * }
         */
        read(): AsyncIterable<string>;
    }

    /**
     * A command line that was executed in a terminal.
     */
    export interface TerminalShellExecutionCommandLine {
        /**
         * The full command line that was executed, including both the command and its arguments.
         */
        readonly value: string;

        /**
         * Whether the command line value came from a trusted source and is therefore safe to
         * execute without user additional confirmation, such as a notification that asks "Do you
         * want to execute (command)?". This verification is likely only needed if you are going to
         * execute the command again.
         *
         * This is `true` only when the command line was reported explicitly by the shell
         * integration script (ie. {@link TerminalShellExecutionCommandLineConfidence.High high confidence})
         * and it used a nonce for verification.
         */
        readonly isTrusted: boolean;

        /**
         * The confidence of the command line value which is determined by how the value was
         * obtained. This depends upon the implementation of the shell integration script.
         */
        readonly confidence: TerminalShellExecutionCommandLineConfidence;
    }

    /**
     * The confidence of a {@link TerminalShellExecutionCommandLine} value.
     */
    enum TerminalShellExecutionCommandLineConfidence {
        /**
         * The command line value confidence is low. This means that the value was read from the
         * terminal buffer using markers reported by the shell integration script. Additionally one
         * of the following conditions will be met:
         *
         * - The command started on the very left-most column which is unusual, or
         * - The command is multi-line which is more difficult to accurately detect due to line
         *   continuation characters and right prompts.
         * - Command line markers were not reported by the shell integration script.
         */
        Low = 0,

        /**
         * The command line value confidence is medium. This means that the value was read from the
         * terminal buffer using markers reported by the shell integration script. The command is
         * single-line and does not start on the very left-most column (which is unusual).
         */
        Medium = 1,

        /**
         * The command line value confidence is high. This means that the value was explicitly sent
         * from the shell integration script or the command was executed via the
         * {@link TerminalShellIntegration.executeCommand} API.
         */
        High = 2
    }

    export interface Terminal {
        /**
         * An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered
         * features for the terminal. This will always be `undefined` immediately after the terminal
         * is created. Listen to {@link window.onDidActivateTerminalShellIntegration} to be notified
         * when shell integration is activated for a terminal.
         *
         * Note that this object may remain undefined if shell integation never activates. For
         * example Command Prompt does not support shell integration and a user's shell setup could
         * conflict with the automatic shell integration activation.
         */
        readonly shellIntegration: TerminalShellIntegration | undefined;
    }

    /**
     * [Shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered capabilities owned by a terminal.
     */
    export interface TerminalShellIntegration {
        /**
         * The current working directory of the terminal. This {@link Uri} may represent a file on
         * another machine (eg. ssh into another machine). This requires the shell integration to
         * support working directory reporting.
         */
        readonly cwd: Uri | undefined;

        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         *
         * @param commandLine The command line to execute, this is the exact text that will be sent
         * to the terminal.
         *
         * @example
         * // Execute a command in a terminal immediately after being created
         * const myTerm = window.createTerminal();
         * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => {
         *   if (terminal === myTerm) {
         *     const command = shellIntegration.executeCommand('echo "Hello world"');
         *     const code = await command.exitCode;
         *     console.log(`Command exited with code ${code}`);
         *   }
         * }));
         * // Fallback to sendText if there is no shell integration within 3 seconds of launching
         * setTimeout(() => {
         *   if (!myTerm.shellIntegration) {
         *     myTerm.sendText('echo "Hello world"');
         *     // Without shell integration, we can't know when the command has finished or what the
         *     // exit code was.
         *   }
         * }, 3000);
         *
         * @example
         * // Send command to terminal that has been alive for a while
         * const commandLine = 'echo "Hello world"';
         * if (term.shellIntegration) {
         *   const command = term.shellIntegration.executeCommand({ commandLine });
         *   const code = await command.exitCode;
         *   console.log(`Command exited with code ${code}`);
         * } else {
         *   term.sendText(commandLine);
         *   // Without shell integration, we can't know when the command has finished or what the
         *   // exit code was.
         * }
         */
        executeCommand(commandLine: string): TerminalShellExecution;

        /**
         * Execute a command, sending ^C as necessary to interrupt any running command if needed.
         *
         * *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)
         * must be activated. Check whether {@link TerminalShellExecution.exitCode} is rejected to
         * verify whether it was successful.
         *
         * @param command A command to run.
         * @param args Arguments to launch the executable with which will be automatically escaped
         * based on the executable type.
         *
         * @example
         * // Execute a command in a terminal immediately after being created
         * const myTerm = window.createTerminal();
         * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => {
         *   if (terminal === myTerm) {
         *     const command = shellIntegration.executeCommand({
         *       command: 'echo',
         *       args: ['Hello world']
         *     });
         *     const code = await command.exitCode;
         *     console.log(`Command exited with code ${code}`);
         *   }
         * }));
         * // Fallback to sendText if there is no shell integration within 3 seconds of launching
         * setTimeout(() => {
         *   if (!myTerm.shellIntegration) {
         *     myTerm.sendText('echo "Hello world"');
         *     // Without shell integration, we can't know when the command has finished or what the
         *     // exit code was.
         *   }
         * }, 3000);
         *
         * @example
         * // Send command to terminal that has been alive for a while
         * const commandLine = 'echo "Hello world"';
         * if (term.shellIntegration) {
         *   const command = term.shellIntegration.executeCommand({
         *     command: 'echo',
         *     args: ['Hello world']
         *   });
         *   const code = await command.exitCode;
         *   console.log(`Command exited with code ${code}`);
         * } else {
         *   term.sendText(commandLine);
         *   // Without shell integration, we can't know when the command has finished or what the
         *   // exit code was.
         * }
         */
        executeCommand(executable: string, args: string[]): TerminalShellExecution;
    }

    export interface TerminalShellIntegrationChangeEvent {
        /**
         * The terminal that shell integration has been activated in.
         */
        readonly terminal: Terminal;

        /**
         * The shell integration object.
         */
        readonly shellIntegration: TerminalShellIntegration;
    }

    export interface TerminalShellExecutionStartEvent {
        /**
         * The terminal that shell integration has been activated in.
         */
        readonly terminal: Terminal;

        /**
         * The shell integration object.
         */
        readonly shellIntegration: TerminalShellIntegration;

        /**
         * The terminal shell execution that has ended.
         */
        readonly execution: TerminalShellExecution;
    }

    export interface TerminalShellExecutionEndEvent {
        /**
         * The terminal that shell integration has been activated in.
         */
        readonly terminal: Terminal;

        /**
         * The shell integration object.
         */
        readonly shellIntegration: TerminalShellIntegration;

        /**
         * The terminal shell execution that has ended.
         */
        readonly execution: TerminalShellExecution;

        /**
         * The exit code reported by the shell. `undefined` means the shell did not report an exit
         * code or the shell reported a command started before the command finished.
         */
        readonly exitCode: number | undefined;
    }

    export namespace window {
        /**
         * Fires when shell integration activates or one of its properties changes in a terminal.
         */
        export const onDidChangeTerminalShellIntegration: Event<TerminalShellIntegrationChangeEvent>;

        /**
         * This will be fired when a terminal command is started. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidStartTerminalShellExecution: Event<TerminalShellExecutionStartEvent>;

        /**
         * This will be fired when a terminal command is ended. This event will fire only when
         * [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
         * activated for the terminal.
         */
        export const onDidEndTerminalShellExecution: Event<TerminalShellExecutionEndEvent>;
    }
}
whitequark commented 5 months ago

I've reviewed the API and it appears entirely sufficient for my goals (scanning the terminal output for a URI that an extension spawning the terminal will then connect to).

gcampbell-msft commented 5 months ago

@Tyriar I hope to test this out soon to see if it meets our needs! Our needs are simplistic, we want to be able to run a command in the terminal and get the return code of that command. Reading the API, I think this can be done, but I plan to test it in the coming days.

Tyriar commented 5 months ago

@gcampbell-msft yep should be very easy for that by listening to window.onDidEndTerminalShellExecution :+1:

gcampbell-msft commented 5 months ago

@Tyriar This would require invoking the command and then statically waiting for this handler, right?

Is there / was there consideration of an async method where we can directly invoke a command in the terminal and wait for it to complete?

Tyriar commented 5 months ago

@gcampbell-msft yes you can use Terminal.shellIntegration?.executeCommand to execute a command and track its progress. For just listening you would use window.onDid(Start|End)TerminalShellExecution.execution.(commandLine|exitCode)

gcampbell-msft commented 5 months ago

@Tyriar I'm curious on your suggested way, with this API, to create your own terminal and then use the shellIntegration to run the command, since this is event driven rather than all async and await?

Specifically for our use case, it'd be best if we could, all in one async method, create a terminal, use that terminal to invoke a command, and then get the result of the command.

gcampbell-msft commented 5 months ago

@Tyriar Also, I notice that there is no way to guarantee that this works because the shell integration could possibly be turned off by the user. Is there a way around this? Another way to integrate with the shell? Etc.

Tyriar commented 5 months ago

@gcampbell-msft there's an example in the jsdoc for executing a command immediately after creating a terminal with a fallback:

// Execute a command in a terminal immediately after being created
const myTerm = window.createTerminal();
window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => {
  if (terminal === myTerm) {
    const command = shellIntegration.executeCommand('echo "Hello world"');
    const code = await command.exitCode;
    console.log(`Command exited with code ${code}`);
  }
}));
// Fallback to sendText if there is no shell integration within 3 seconds of launching
setTimeout(() => {
  if (!myTerm.shellIntegration) {
    myTerm.sendText('echo "Hello world"');
    // Without shell integration, we can't know when the command has finished or what the
    // exit code was.
  }
}, 3000);

You would also want to dispose of the window.onDidActivateTerminalShellIntegration listener when you're done with it.

gcampbell-msft commented 5 months ago

@Tyriar Understood. That makes sense. My general feedback is that this is somewhat complex for someone who wants to invoke a command and simply get the exit code. It definitely makes it possible, by creating a variable outside of the scope and then updating it in the handlers, but in a perfect world we would be able to do this process in a simple async await pattern.

Tyriar commented 5 months ago

@gcampbell-msft you can wrap it on your end, there are too many things that can go wrong for us to expose some reliable method to always do this. For example you could assume shell integration is active, and if it doesn't work then just point the user at troubleshooting docs. If you do that (which I'd recommend) then it would be quite simple.

gcampbell-msft commented 5 months ago

@Tyriar Got it, that makes sense. Is there a specific documentation on the VS Code site that has documentation on when shellIntegration won't be on, and what troubleshooting there is? Is it just this page? https://code.visualstudio.com/docs/terminal/shell-integration

Tyriar commented 5 months ago

@gcampbell-msft yes just that page. There's a bit of information about it at the bottom. It's hard to give general advice because they're are so many different setups

Tyriar commented 5 months ago

A simple usage sample is available in the samples repo: https://github.com/microsoft/vscode-extension-samples/pull/1000 (it won't work properly until tomorrow due to a bug fix to cwd in core)

degrammer commented 3 months ago

Hi @Tyriar, can you please confirm, if as part of this feature, allowing access to the active terminal selected text is going to be available / planned to work in the short term? (related issue https://github.com/microsoft/vscode/issues/188173).

One of the use cases could be for extensions using a command as part of terminal/context to access the selected text from the terminal to perform things like AI Suggestions, Output sharing, etc.

Thank you 🙏🏼

Tyriar commented 3 months ago

@degrammer selection access will be done as a part of the buffer access api, not this one. That's been but on hold for a while as I have some competing priorities and want to think about the API shape in the background a bit.

We definitely do want to finalize an api for selection eventually as GH copilot uses the current proposal, unfortunately to make it future proof we need to tie it in with the much more complex buffer proposal.

Tyriar commented 3 months ago

Moving to July for Python to adopt first and give feedback before finalizing.

whitequark commented 2 months ago

Is there any way to receive a stream of raw data from a terminal that does not have shell integration? For example if I create a terminal with any command of my choice by setting shellPath to argv[0] and shellArgs to argv[1...].

Since there's only one command that could plausibly be run in that terminal it's not necessary to try and guess which command the output comes from. The current API doesn't seem to offer anything to cover this use case, and I think it would be too limiting for my purpose.