Skip to content

Conversation

ntrogh
Copy link
Contributor

@ntrogh ntrogh commented Aug 28, 2025

This URL handler allows external applications to insert messages into the VS Code chat interface via URLs. The chat message is inserted in the chat input field but is not submitted. The user is prompted if they want to insert the specific prompt in chat.

URL Format

code-oss:chat-message?prompt=<encoded-prompt-text>&mode=<chat-mode>

Parameters

  • prompt (required): The text to insert into the chat input field (URL encoded)
  • mode (optional): The chat mode to set. Defaults to 'agent'. Supports both built-in modes ('ask', 'edit', 'agent') and custom chat modes defined in .chatmode files

Examples

code-oss:chat-message?prompt=Hello%20World
code-oss:chat-message?prompt=Explain%20this%20code&mode=ask
code-oss:chat-message?prompt=Review%20my%20JavaScript%20code%20for%20potential%20bugs%20and%20security%20issues&mode=agent

Test steps

  1. Run Developer: Open URL command in Command Palette
  2. Enter a URL like:
    • code-oss:chat-message?prompt=Test%20prompt
    • code-oss:chat-message?prompt=Hello&mode=ask

The handler should:

  1. Show a confirmation dialog with the prompt preview
  2. Upon confirmation, open the chat view
  3. Set the specified chat mode
  4. Insert the prompt text into the input field but not submit it
  5. Focus the input field for user interaction

@ntrogh ntrogh self-assigned this Aug 28, 2025
@ntrogh ntrogh requested a review from digitarald August 28, 2025 22:29
@ntrogh ntrogh added feature-request Request for new features or functionality panel-chat chat labels Aug 28, 2025
@ntrogh ntrogh requested a review from aeschli August 28, 2025 22:31
@ntrogh
Copy link
Contributor Author

ntrogh commented Aug 28, 2025

@digitarald @aeschli Something I've been wanting for a while was to be able to use a URL to enter a prompt message in chat. Usage scenario is to use integrate this in our docs or other repos like awesome-copilot. You can provide a prompt text and chat mode (optional) as input.

I've based the code off of the chat CLI subcommand and the URL handlers we have for chat modes/prompt files/instructions.

Can you review this PR and check whether the functionality makes sense?

@@ -850,6 +851,7 @@ registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribu
registerWorkbenchContribution2(ChatContextContributions.ID, ChatContextContributions, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(ChatResponseResourceFileSystemProvider.ID, ChatResponseResourceFileSystemProvider, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(PromptUrlHandler.ID, PromptUrlHandler, WorkbenchPhase.BlockRestore);
registerWorkbenchContribution2(ChatMessageUrlHandler.ID, ChatMessageUrlHandler, WorkbenchPhase.BlockRestore);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is no good reason to block restore here, please pick another later phase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bpasero Good point. I'd suggest AfterRestored rather than Eventually because this is in immediate response to a user action. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah after restored sounds good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aeschli Should we also update the PromptUrlHandler registration to happen after restored?

@ntrogh ntrogh requested a review from bpasero August 29, 2025 07:31
@ntrogh ntrogh marked this pull request as ready for review August 29, 2025 09:28
@ntrogh ntrogh added this to the August 2025 milestone Aug 29, 2025
aeschli
aeschli previously approved these changes Aug 29, 2025
Copy link
Member

@bpasero bpasero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is with the code that is commented out?

@ntrogh
Copy link
Contributor Author

ntrogh commented Aug 29, 2025

@bpasero Refactored to use IChatViewOpenOptions upon advice from @aeschli. And I forgot to remove commented-out imports and ctor params that were no longer needed.

@Tyriar
Copy link
Member

Tyriar commented Aug 29, 2025

I have a full review coming

Comment on lines 14 to 17
// import { IChatModeService } from '../common/chatModes.js';
import { ILogService } from '../../../../platform/log/common/log.js';
// import { IHostService } from '../../../services/host/browser/host.js';
// import { mainWindow } from '../../../../base/browser/window.js';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these imports be cleaned up?

Comment on lines 37 to 39
// @IChatModeService private readonly chatModeService: IChatModeService,
@ILogService private readonly logService: ILogService,
// @IHostService private readonly hostService: IHostService,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comments

export class ChatMessageUrlHandler extends Disposable implements IWorkbenchContribution, IURLHandler {

static readonly ID = 'workbench.contrib.chatMessageUrlHandler';
private static readonly MAX_PROMPT_LENGTH = 10000; // characters
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could encode the fast it's characters into the variable to prevent bitrot: MAX_PROMPT_CHAR_LENGTH

Comment on lines 53 to 58
// Verify that workspace is trusted
const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({
message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.")
});

if (!trusted) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment, it's obvious from context

Comment on lines 72 to 73
// Decode and clean prompt text
const decodedPrompt = this.sanitizePromptText(decodeURIComponent(promptText));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment obvious from context

Comment on lines 125 to 131
detail.appendMarkdown(localize('confirmChatMessageSecurity',
"\n\nIf you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'."));

const { confirmed } = await this.dialogService.confirm({
type: 'info',
primaryButton: localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"),
cancelButton: localize('noButton', "No"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible the No could be localized differently in the button and the message

Comment on lines 122 to 126
const detail = new MarkdownString('', { supportHtml: true });
detail.appendMarkdown(localize('confirmChatMessageDetail', "This will insert the following text into chat:\n\n"));
detail.appendMarkdown(`\`\`\`\n${previewText}\n\`\`\``);
detail.appendMarkdown(localize('confirmChatMessageSecurity',
"\n\nIf you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'."));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: appending like this is less efficient on memory/gc. How about this?

new MarkdownString([
  'first',
  'second',
  'third'
].join('\n\n')

Comment on lines 134 to 136
markdownDetails: [{
markdown: detail
}]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you rename detail to markdown you could make this line markdownDetails: [{ markdown }]

Comment on lines 168 to 169
// Remove control characters except tab (9), newline (10), carriage return (13)
cleanText = cleanText.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be handled by InvisibleCharacters.isInvisibleCharacter?

Comment on lines +174 to +175
// Normalize multiple whitespace to single spaces and trim
return cleanText.replace(/\s+/g, ' ').trim();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could whitespace be important to the query?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to be on the safe side as a request could use blanks as a filler to obfuscate malicious content near the end and outside of the viewport.

@Tyriar Tyriar removed feature-request Request for new features or functionality panel-chat chat labels Aug 29, 2025
@Tyriar
Copy link
Member

Tyriar commented Aug 29, 2025

@ntrogh we typically only label PRs with candidate

@ntrogh ntrogh requested a review from Tyriar August 29, 2025 15:32
@ntrogh ntrogh closed this Sep 3, 2025
@OrenMe
Copy link

OrenMe commented Sep 4, 2025

@ntrogh I really like this idea, is this going to be done in another PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants