Skip to content

Commit 50f10f6

Browse files
committed
chat: restore GitHub issue and PR context attachments (#334544)
* chat: restore GitHub issue and PR context attachments Revert compact inline references for pasted GitHub issue and pull request URLs until attachment rendering is unified. Restore automatic context attachment synchronization in the Agents composer.\n\nRefs #334542\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chat: attach pasted GitHub links as context Keep pasted issue and pull request URLs in the prompt while adding context attachments in regular Chat. Share URL parsing with the Agents composer and preserve undo behavior.\n\nRefs #334548\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chat: use semantic icons for GitHub context Represent pasted GitHub links as generic context so issue and pull request pills render their respective Codicons without generic pasted-content chrome.\n\nRefs #334548\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chat: preserve request link interactions Reject invalid GitHub issue and pull request URL continuations, and keep nested request links keyboard- and pointer-operable when inline request editing is enabled.\n\nRefs #334548\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: avoid forwarding flatMap index as metadata Wrap the GitHub attachment parser callback so Array.prototype.flatMap does not pass its numeric index into the optional metadata parameter.\n\nRefs #334548\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent de8cc55 commit 50f10f6

24 files changed

Lines changed: 573 additions & 977 deletions

src/vs/sessions/contrib/chat/browser/media/chatInput.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,6 @@
145145
background-color: var(--vscode-agentsChatInput-background) !important;
146146
}
147147

148-
.sessions-chat-editor .monaco-editor .mtk1 {
149-
color: var(--vscode-input-foreground);
150-
}
151-
152148
/* Inline decoration highlights for the new-session input editor: slash commands,
153149
* onboarding prompt placeholders, `#file:` variable references, and agent-host
154150
* completion references. */

src/vs/sessions/contrib/chat/browser/newChatInput.ts

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import type { IManagedHoverContent } from '../../../../base/browser/ui/hover/hov
2020
import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
2121
import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
2222
import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js';
23-
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
2423
import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js';
2524
import { ITextModel } from '../../../../editor/common/model.js';
2625
import { IModelService } from '../../../../editor/common/services/model.js';
@@ -30,7 +29,6 @@ import { EditorOptions } from '../../../../editor/common/config/editorOptions.js
3029
import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';
3130
import { SnippetController2 } from '../../../../editor/contrib/snippet/browser/snippetController2.js';
3231
import { CopyPasteController } from '../../../../editor/contrib/dropOrPasteInto/browser/copyPasteController.js';
33-
import { LinkDetector } from '../../../../editor/contrib/links/browser/links.js';
3432
import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js';
3533
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
3634
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
@@ -109,9 +107,8 @@ import { IChatStatusItemService } from '../../../../workbench/contrib/chat/brows
109107
import { handleTerminalCommandPaste, isTerminalCommandInput } from '../../../../workbench/contrib/chat/browser/chatTerminalCommandPaste.js';
110108
import { compactCodiconsIn } from '../../../../workbench/contrib/chat/browser/chatIcons.js';
111109
import { IChatPasteTargetService } from '../../../../workbench/contrib/chat/browser/chat.js';
112-
import { ChatDynamicVariableModel } from '../../../../workbench/contrib/chat/browser/attachments/chatDynamicVariables.js';
110+
import { getGitHubIssueOrPullRequestAttachments } from '../../../../workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.js';
113111
import { NewChatInputPasteTarget } from './newChatInputPasteTarget.js';
114-
import { registerChatInputReferenceDecorationType } from '../../../../workbench/contrib/chat/browser/widget/input/editor/chatInputReferenceDecorations.js';
115112
import { getChatSessionType } from '../../../../workbench/contrib/chat/common/model/chatUri.js';
116113
import { ChatSpeechToTextState, DictationSettingId, IChatSpeechToTextService } from '../../../../workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.js';
117114
import { setupDictationMicGlow } from '../../../../workbench/contrib/chat/browser/speechToText/dictationMicGlow.js';
@@ -134,6 +131,7 @@ import { animatePromptTyping, IPromptTypingAnimation } from './promptTypingAnima
134131
import { PromptTemplatePlaceholderController } from './promptTemplatePlaceholder.js';
135132
import { INewSessionComposer, INewSessionPromptOptionsController, NEW_SESSION_PROMPT_TYPING_DURATION_MS, NewSessionPromptOptionsState, NewSessionWorkspacePreselectionSource } from './newSessionComposerService.js';
136133
import { NewSessionPromptOptionsWidget } from './newSessionPromptOptions.js';
134+
import { isInputGitHubContext, toInputGitHubContextMetadata } from '../common/newChatContextIds.js';
137135

138136

139137
const OPEN_OTEL_SETTINGS_COMMAND = 'github.copilot.chat.otel.openSettings';
@@ -213,13 +211,16 @@ KeybindingsRegistry.registerKeybindingRule({
213211
interface IDraftState {
214212
inputText: string;
215213
attachments: readonly IChatRequestVariableEntry[];
216-
contrib?: Record<string, unknown>;
217214
}
218215

219216
export function hasSendableNewChatContent(query: string, attachments: readonly IChatRequestVariableEntry[], hasAdditionalSendContent = false): boolean {
220217
return !!query.trim() || attachments.some(isExplicitFileOrImageVariableEntry) || hasAdditionalSendContent;
221218
}
222219

220+
function getInputGitHubContextAttachments(input: string): readonly IChatRequestVariableEntry[] {
221+
return getGitHubIssueOrPullRequestAttachments(input, toInputGitHubContextMetadata());
222+
}
223+
223224
class NewChatInputStatusActionViewItem extends MenuEntryActionViewItem {
224225
private readonly hoverContentDisposables = this._register(new MutableDisposable<DisposableStore>());
225226
private _container: HTMLElement | undefined;
@@ -448,7 +449,6 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
448449
// Input
449450
private _editor!: CodeEditorWidget;
450451
private _editorContainer!: HTMLElement;
451-
private _dynamicVariableModel!: ChatDynamicVariableModel;
452452
private _sessionControlsContainer: HTMLElement | undefined;
453453
private readonly _promptTemplatePlaceholder = this._register(new MutableDisposable<PromptTemplatePlaceholderController>());
454454
private readonly _promptOptionsWidget = this._register(new MutableDisposable<NewSessionPromptOptionsWidget>());
@@ -524,7 +524,6 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
524524
@IModelService private readonly modelService: IModelService,
525525
@ITextModelService private readonly textModelService: ITextModelService,
526526
@IChatPasteTargetService private readonly chatPasteTargetService: IChatPasteTargetService,
527-
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
528527
@IConfigurationService private readonly configurationService: IConfigurationService,
529528
@IContextKeyService private readonly contextKeyService: IContextKeyService,
530529
@ILogService private readonly logService: ILogService,
@@ -551,7 +550,6 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
551550
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
552551
) {
553552
super();
554-
this._register(registerChatInputReferenceDecorationType(this.codeEditorService));
555553
this._modelSelection = this._register(this.instantiationService.createInstance(SessionModelSelection, this.options.session));
556554
this._canSendRequest = derived(this, reader => {
557555
if (this.options.canSubmitWithoutSession?.read(reader)) {
@@ -896,6 +894,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
896894
const editorOptions: IEditorConstructionOptions = {
897895
...getSimpleEditorOptions(this.configurationService),
898896
readOnly: false,
897+
// Match the workbench chat input so the post-paste selector is offered.
899898
pasteAs: EditorOptions.pasteAs.defaultValue,
900899
ariaLabel: this._getAriaLabel(),
901900
placeholder: this.options.placeholder ?? getRandomChatInputPlaceholder(),
@@ -932,7 +931,6 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
932931
SnippetController2.ID,
933932
PlaceholderTextContribution.ID,
934933
CopyPasteController.ID,
935-
LinkDetector.ID,
936934
]),
937935
};
938936

@@ -1068,26 +1066,17 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
10681066
AgentHostInputCompletionHandler, this._editor, this._contextAttachments,
10691067
));
10701068

1071-
const contextAttachments = this._contextAttachments;
1072-
this._dynamicVariableModel = this._register(this._scopedInstantiationService.createInstance(ChatDynamicVariableModel, {
1073-
inputEditor: this._editor,
1074-
get attachments() { return contextAttachments.attachments; },
1075-
onDidChangeActiveInputEditor: Event.None,
1076-
onDidChangeAttachments: contextAttachments.onDidChangeContext,
1077-
refreshParsedInput: () => { },
1078-
}));
1079-
this._register(this._dynamicVariableModel.onDidChangeReferences(() => this._updateDraftState()));
10801069
this._register(this.chatPasteTargetService.registerTarget(textModel.uri, new NewChatInputPasteTarget(
10811070
this._editor,
10821071
this._contextAttachments,
10831072
this._agentHostInputCompletionHandler,
1084-
this._dynamicVariableModel,
10851073
() => this._getTerminalCommandPrefix(),
10861074
() => this.options.session.get()?.resource,
10871075
textModel.uri,
10881076
)));
10891077

10901078
this._register(this._editor.onDidChangeModelContent(() => {
1079+
this._syncInputGitHubContext();
10911080
this._updateDraftState();
10921081
this._updateSendButtonState();
10931082
this._updateEditorFontFamily();
@@ -1480,14 +1469,9 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
14801469
}
14811470

14821471
private _updateDraftState(): void {
1483-
const contrib: Record<string, unknown> = {};
1484-
if (this._dynamicVariableModel?.variables.length) {
1485-
this._dynamicVariableModel.getInputState(contrib);
1486-
}
14871472
this._draftState = {
14881473
inputText: this._editor?.getModel()?.getValue() ?? '',
14891474
attachments: [...this._contextAttachments.attachments],
1490-
...(Object.keys(contrib).length ? { contrib } : {}),
14911475
};
14921476
}
14931477

@@ -1499,13 +1483,32 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
14991483
this.saveState();
15001484
}
15011485

1486+
private _syncInputGitHubContext(): void {
1487+
const inputAttachments = getInputGitHubContextAttachments(this._editor?.getValue() ?? '');
1488+
const inputAttachmentIds = new Set(inputAttachments.map(attachment => attachment.id));
1489+
const attachments = this._contextAttachments.attachments.filter(attachment =>
1490+
!isInputGitHubContext(attachment) || inputAttachmentIds.has(attachment.id)
1491+
);
1492+
const attachmentIds = new Set(attachments.map(attachment => attachment.id));
1493+
for (const attachment of inputAttachments) {
1494+
if (!attachmentIds.has(attachment.id)) {
1495+
attachments.push(attachment);
1496+
attachmentIds.add(attachment.id);
1497+
}
1498+
}
1499+
if (attachments.length !== this._contextAttachments.attachments.length
1500+
|| attachments.some((attachment, index) => attachment !== this._contextAttachments.attachments[index])) {
1501+
this._contextAttachments.setAttachments(attachments);
1502+
}
1503+
}
1504+
15021505
private _toHistoryEntry(draft: IDraftState): IChatModelInputState {
15031506
return {
15041507
...draft,
15051508
mode: { id: ChatModeKind.Agent, kind: ChatModeKind.Agent },
15061509
selectedModel: undefined,
15071510
selections: [],
1508-
contrib: draft.contrib ?? {},
1511+
contrib: {},
15091512
};
15101513
}
15111514

@@ -1515,7 +1518,6 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
15151518
if (entry) {
15161519
this._editor?.getModel()?.setValue(inputText);
15171520
this._contextAttachments.setAttachments(entry.attachments);
1518-
this._dynamicVariableModel.setInputState(entry.contrib);
15191521
}
15201522
aria.status(inputText);
15211523
if (previous) {
@@ -1538,11 +1540,10 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
15381540

15391541
private async _send(background = false): Promise<boolean> {
15401542
const rawQuery = this._editor.getModel()?.getValue() ?? '';
1541-
const displayQuery = rawQuery.trim();
1543+
const query = rawQuery.trim();
15421544
const queryOffset = rawQuery.length - rawQuery.trimStart().length;
1543-
const query = this._dynamicVariableModel?.getPromptText(displayQuery, queryOffset) ?? displayQuery;
15441545
const hasAdditionalSendContent = this.options.hasAdditionalSendContent?.get() ?? false;
1545-
if (!hasSendableNewChatContent(displayQuery, this._contextAttachments.attachments, hasAdditionalSendContent) || this._sending) {
1546+
if (!hasSendableNewChatContent(query, this._contextAttachments.attachments, hasAdditionalSendContent) || this._sending) {
15461547
return false;
15471548
}
15481549

@@ -1568,7 +1569,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
15681569
return true;
15691570
}
15701571

1571-
const attachments = this._agentHostInputCompletionHandler?.getAttachmentsForSend(displayQuery, queryOffset) ?? [...this._contextAttachments.attachments];
1572+
const attachments = this._agentHostInputCompletionHandler?.getAttachmentsForSend(query, queryOffset) ?? [...this._contextAttachments.attachments];
15721573
const attachedContext = attachments.length > 0
15731574
? attachments
15741575
: undefined;
@@ -1638,7 +1639,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
16381639
if (draft.attachments?.length) {
16391640
this._contextAttachments.setAttachments(draft.attachments.map(IChatRequestVariableEntry.fromExport));
16401641
}
1641-
this._dynamicVariableModel.setInputState(draft.contrib ?? {});
1642+
this._syncInputGitHubContext();
16421643
}
16431644
this._updateSendButtonState();
16441645
}

src/vs/sessions/contrib/chat/browser/newChatInputPasteTarget.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,35 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { URI } from '../../../../base/common/uri.js';
76
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
7+
import { URI } from '../../../../base/common/uri.js';
88
import { IRange, Range } from '../../../../editor/common/core/range.js';
99
import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';
10-
import { ChatDynamicVariableModel } from '../../../../workbench/contrib/chat/browser/attachments/chatDynamicVariables.js';
1110
import { IChatPasteTarget } from '../../../../workbench/contrib/chat/browser/chat.js';
1211
import { isTerminalCommandPaste } from '../../../../workbench/contrib/chat/browser/chatTerminalCommandPaste.js';
1312
import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js';
1413
import { IDynamicVariable } from '../../../../workbench/contrib/chat/common/attachments/chatVariables.js';
1514
import { AgentHostInputCompletionHandler } from './agentHostInputCompletions.js';
1615
import { INewChatAttachments } from './newChatContextAttachments.js';
1716

18-
type PasteCompletionHandler = Pick<AgentHostInputCompletionHandler, 'acceptCompletion' | 'forgetReference'>;
19-
20-
/** Adapts the Agents composer to the shared Chat paste providers. */
17+
/**
18+
* Exposes the Agents window composer to the shared chat paste pipeline. Inline
19+
* references are tracked by the completion handler, which owns their decorations
20+
* and outbound ranges.
21+
*/
2122
export class NewChatInputPasteTarget implements IChatPasteTarget {
2223

2324
constructor(
2425
private readonly editor: ICodeEditor,
2526
private readonly contextAttachments: INewChatAttachments,
26-
private readonly completionHandler: PasteCompletionHandler,
27-
private readonly dynamicVariableModel: ChatDynamicVariableModel,
27+
private readonly completionHandler: AgentHostInputCompletionHandler,
2828
private readonly getTerminalCommandPrefix: () => string | undefined,
2929
private readonly getSessionResource: () => URI | undefined,
3030
private readonly inputUri: URI,
3131
) { }
3232

3333
get sessionResource(): URI {
34+
// A composer draft has no session yet, so its input scopes the cleanup.
3435
return this.getSessionResource() ?? this.inputUri;
3536
}
3637

@@ -39,7 +40,7 @@ export class NewChatInputPasteTarget implements IChatPasteTarget {
3940
}
4041

4142
get inlineReferences(): readonly IDynamicVariable[] {
42-
return this.dynamicVariableModel.variables;
43+
return [];
4344
}
4445

4546
addAttachments(entries: readonly IChatRequestVariableEntry[]): void {
@@ -58,12 +59,9 @@ export class NewChatInputPasteTarget implements IChatPasteTarget {
5859
this.completionHandler.acceptCompletion({ ...entry, range: offsetRange }, text, offsetRange);
5960
}
6061

61-
addInlineReference(reference: IDynamicVariable, expectedText?: string, expectedRangeOffset?: number): void {
62-
this.dynamicVariableModel.addReference(reference, expectedText, expectedRangeOffset);
63-
}
64-
65-
removeInlineReference(reference: IDynamicVariable): void {
66-
this.dynamicVariableModel.removeReference(reference);
62+
addInlineReference(_reference: IDynamicVariable): void {
63+
// The composer has no dynamic-variable model; references that are not
64+
// backed by an attachment are not surfaced here.
6765
}
6866

6967
isTerminalCommandPaste(text: string, range: IRange): boolean {

src/vs/sessions/contrib/chat/common/newChatContextIds.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { URI } from '../../../../base/common/uri.js';
77

88
export const ADDITIONAL_FOLDER_CONTEXT_ID_PREFIX = 'sessions-additional-folder:';
99
export const ADDITIONAL_REPOSITORY_CONTEXT_ID_PREFIX = 'sessions-additional-repository:';
10+
const INPUT_GITHUB_CONTEXT_METADATA_KEY = 'sessionsInputGitHubContext';
11+
12+
interface IHasInputGitHubContextMetadata {
13+
readonly _meta?: Record<string, unknown>;
14+
}
1015

1116
export function getAdditionalFolderContextId(uri: URI): string {
1217
return `${ADDITIONAL_FOLDER_CONTEXT_ID_PREFIX}${uri.toString()}`;
@@ -19,3 +24,12 @@ export function getAdditionalRepositoryContextId(uri: URI): string {
1924
export function isAdditionalWorkspaceContextId(id: string): boolean {
2025
return id.startsWith(ADDITIONAL_FOLDER_CONTEXT_ID_PREFIX) || id.startsWith(ADDITIONAL_REPOSITORY_CONTEXT_ID_PREFIX);
2126
}
27+
28+
export function isInputGitHubContext(source: IHasInputGitHubContextMetadata): boolean {
29+
// eslint-disable-next-line local/code-no-untyped-meta-access -- sanctioned first hop into the input GitHub context slot.
30+
return source._meta?.[INPUT_GITHUB_CONTEXT_METADATA_KEY] === true;
31+
}
32+
33+
export function toInputGitHubContextMetadata(): Record<string, unknown> {
34+
return { [INPUT_GITHUB_CONTEXT_METADATA_KEY]: true };
35+
}

0 commit comments

Comments
 (0)