-
Notifications
You must be signed in to change notification settings - Fork 29.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add prompt snippets support #234220
base: main
Are you sure you want to change the base?
add prompt snippets support #234220
Conversation
@@ -396,18 +405,30 @@ class WriteableStreamImpl<T> implements WriteableStream<T> { | |||
} | |||
|
|||
private flowData(): void { | |||
if (this.buffer.data.length > 0) { | |||
// if buffer is empty, nothing to do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored to remove 1 level of indentation.
6f5026f
to
a927e4e
Compare
@@ -75,7 +75,15 @@ export class ImplicitContextAttachmentWidget extends Disposable { | |||
}); | |||
this.domNode.ariaLabel = ariaLabel; | |||
this.domNode.tabIndex = 0; | |||
const hintElement = dom.append(this.domNode, dom.$('span.chat-implicit-hint', undefined, 'Current file')); | |||
|
|||
const hintElement = dom.append( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made it a bit more readable by splitting params to separate lines.
if (this.implicitContext?.value) { | ||
const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels)); | ||
if (this._implicitContext?.value) { | ||
const implicitPart = store.add(this.instantiationService.createInstance( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put params on separate lines only to improve readability.
|
||
prompt.parts | ||
.forEach((part, i) => { | ||
public async resolveVariables( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly readability improvements in this file, the logic remains the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, but I'd prefer to keep PRs focused more narrowly, and the word "mostly" makes it a little harder to review :)
// to go first so that an replacement logic is simple | ||
resolvedVariables | ||
.sort((left, right) => { | ||
assertDefined( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These asserts are to omit TS hacks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with the !
because we know that these are parts of a prompt and have ranges, but the assert is fine too
) { | ||
super(); | ||
this._register(widget.inputEditor.onDidChangeModelContent(e => { | ||
e.changes.forEach(c => { | ||
// Don't mutate entries in _variables, since they will be returned from the getter | ||
this._variables = coalesce(this._variables.map(ref => { | ||
this._variables = coalesce(this._variables.map((ref) => { | ||
if (c.text === `#file:${ref.filenameWithReferences}`) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the only way I made it work and its not ideal, any better ideas? cc @roblourens @joyceerhl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this trying to do? Is this triggered when that text is inserted all at once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we add the (+N more)
label to a filename after nested references are resolved, this is run and as the result the variables are removed. The logic below infers that the variables text have changed and are hence the variables are no longer valid and need to be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we update the variable objects when we update the labels, this if
check works, - if the variable text is really the same as the variable should have, do nothing.
endLineNumber: ref.range.endLineNumber, | ||
endColumn: ref.range.endColumn + delta | ||
} | ||
ref.range = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@roblourens do you recall the main reason for the // Don't mutate entries in _variables..
above? Because the ref is now a class instance with event listeners now, I do need to mutate the variables here 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because those objects will have been previously returned, and other objects will be holding on to them, and not expecting them to change. Maybe there is somewhere that we diff the previous object with the new one, and mutating the object breaks that. There are other ways we can solve that. But I don't quite understand the problem for you
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe there is somewhere that we diff the previous object with the new one
But that would break only if the diff logic relies on the object pointers, doing diffing by-attribute should be unaffected in either case?
There is no really a problem for me, just wanted to make sure that mutating the objects is fine here. We do need to mutate them now because the ref
variable is a class instance now rather than a simple object it was before. The class instance can have event listeners registered on it by other code, and it handles nested file references resolution which we don't want to again over and over 🤷
const refAtThisPosition = references.find(r => | ||
r.range.startLineNumber === position.lineNumber && | ||
r.range.startColumn === position.column); | ||
if (refAtThisPosition) { | ||
const length = refAtThisPosition.range.endColumn - refAtThisPosition.range.startColumn; | ||
const text = message.substring(0, length); | ||
const range = new OffsetRange(offset, offset + length); | ||
return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.id, refAtThisPosition.modelDescription, refAtThisPosition.data, refAtThisPosition.fullName, refAtThisPosition.icon, refAtThisPosition.isFile); | ||
return new ChatRequestDynamicVariablePart( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored to reduce the number of params used.
(part as ChatRequestDynamicVariablePart).fullName, | ||
(part as ChatRequestDynamicVariablePart).icon, | ||
(part as ChatRequestDynamicVariablePart).isFile | ||
variable, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored to reduce the number of params on the constructor.
…the first unit test
…prove unit tests
…changes, added unit test for the `assertDefined` util
… add more unit tests for it
…d update variable label
…ble test utilities
…or the `PromptFileReference` class
…ence-recursion-proof and add approptiate tests
…lying data source
…eference object only if `prompt-snippets` config value is set
…ile reference object only if `prompt-snippets` config value is set
a927e4e
to
2fa00b9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not done reviewing but wanted to drop the first round of comments. Overall, when I'm working on a large feature, I usually try to start pushing small PRs to main as early as possible. That's the best way to avoid merge conflicts and get feedback early. Don't be afraid of pushing an incomplete feature behind a hidden setting, as long as it won't break anything. We do that often on this team. I also would rather avoid PRs that make multiple unrelated changes. It just gets a bit hard to review.
* assertDefined(null, new Error('Should throw this error.')) | ||
* ``` | ||
*/ | ||
export function assertDefined<T>(value: T, error: string | NonNullable<Error>): asserts value is NonNullable<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this in types.ts, assertIsDefined
@@ -186,7 +186,7 @@ export interface ITransformer<Original, Transformed> { | |||
error?: IErrorTransformer; | |||
} | |||
|
|||
export function newWriteableStream<T>(reducer: IReducer<T>, options?: WriteableStreamOptions): WriteableStream<T> { | |||
export function newWriteableStream<T>(reducer: IReducer<T> | null, options?: WriteableStreamOptions): WriteableStream<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional argument? We generally use undefined over null
const currentFile = localize('openEditor', "Current file context"); | ||
const inactive = localize('enableHint', "disabled"); | ||
const currentFileHint = currentFile + (this.attachment.enabled ? '' : ` (${inactive})`); | ||
const title = `${currentFileHint}\n${uriLabel}`; | ||
const title = `${currentFileHint}${this.getUriLabel(file)}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this returns an array, concatenated to a string, is that right?
state: WorkingSetEntryState.Attached, | ||
kind: 'reference', | ||
}); | ||
|
||
seenEntries.add(child); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a correct fix (cc @joyceerhl) but is this method even related to your new feature? I prefer not grouping unrelated fixes into a PR that's already really big
|
||
prompt.parts | ||
.forEach((part, i) => { | ||
public async resolveVariables( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, but I'd prefer to keep PRs focused more narrowly, and the word "mostly" makes it a little harder to review :)
@@ -193,7 +195,18 @@ export class ChatWidget extends Disposable implements IChatWidget { | |||
return { text: '', parts: [] }; | |||
} | |||
|
|||
this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent }); | |||
assertDefined( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems unnecessary, this is right after a !this.viewModel
check
@@ -1046,6 +1065,29 @@ export class ChatWidget extends Disposable implements IChatWidget { | |||
this.telemetryService.publicLog2<ChatEditingWorkingSetEvent, ChatEditingWorkingSetClassification>('chatEditing/workingSetSize', { originalSize: this.inputPart.attemptedWorkingSetEntriesCount, actualSize: uniqueWorkingSetEntries.size }); | |||
} | |||
|
|||
// factor in nested references of dynamic variables into the implicit attached context | |||
const variableModel = this.getContrib<ChatDynamicVariableModel>(ChatDynamicVariableModel.ID); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the ChatWidget calls to a contrib directly, then it's no longer a "contrib". The point of that model is to have something that can implement a feature on top of an object using its public interface, but is totally decoupled from the object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So either this should no longer be a 'contrib' or something else needs to change, I think I need to understand the overall thing better to decide.
/** | ||
* A file reference token inside a prompt. | ||
*/ | ||
export class FileReference extends BaseToken { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure workbench/common
is the right place for these files, it all seems specific to the chat feature, so should it stay under workbench/contrib/chat/...
?
* fileReference.dispose(); | ||
* ``` | ||
*/ | ||
export class PromptFileReference extends Disposable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this is sort of a model object that doesn't depend on anything else in browser/
so can it go in the common/
layer?
* ); | ||
* ``` | ||
*/ | ||
export const randomInt = (max: number, min: number = 0): number => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be a good one for src/vs/base/common/numbers.ts
?
} | ||
|
||
if (typeof value === 'string') { | ||
return value.trim().toLowerCase() === 'true'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this also expecting a string vs boolean?
@@ -51,7 +51,9 @@ class InputEditorDecorations extends Disposable { | |||
|
|||
this.updateInputEditorDecorations(); | |||
this._register(this.widget.inputEditor.onDidChangeModelContent(() => this.updateInputEditorDecorations())); | |||
this._register(this.widget.onDidChangeParsedInput(() => this.updateInputEditorDecorations())); | |||
this._register(this.widget.onDidChangeParsedInput(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is necessarily an improvement, we use the single-line version a lot
} | ||
|
||
// get all file references in the file contents | ||
const references = await this.codec.decode(fileStream.value).consumeAll(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wondering whether this needs to be a stream? Do you use it as a stream?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's just nice to have the stream since you are working from a file stream, just wondering if there is a case where you would use it that way
I can't get this to work- I set the setting, should I see intellisense for Also, it seems like this might break normal implicit context? |
Related issue: https://github.com/microsoft/vscode-internalbacklog/issues/5220#issuecomment-2486738649