Skip to content

Commit ec0f0be

Browse files
authored
Improve save prompt (microsoft#256960)
* improve save prompt * update
1 parent f7152e2 commit ec0f0be

File tree

3 files changed

+68
-193
lines changed

3 files changed

+68
-193
lines changed

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.
109109
import { registerAction2 } from '../../../../platform/actions/common/actions.js';
110110
import { ChatModeService, IChatModeService } from '../common/chatModes.js';
111111
import { ChatResponseResourceFileSystemProvider } from '../common/chatResponseResourceFileSystemProvider.js';
112-
import { runSaveToPromptAction, SAVE_TO_PROMPT_SLASH_COMMAND_NAME } from './promptSyntax/saveToPromptAction.js';
112+
import { SAVE_TO_PROMPT_ACTION_ID, SAVE_TO_PROMPT_SLASH_COMMAND_NAME } from './promptSyntax/saveToPromptAction.js';
113113
import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js';
114114
import { ChatAttachmentResolveService, IChatAttachmentResolveService } from './chatAttachmentResolveService.js';
115115
import { registerLanguageModelActions } from './actions/chatLanguageModelActions.js';
@@ -636,8 +636,8 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
636636
lastFocusedWidget,
637637
'No currently active chat widget found.',
638638
);
639-
640-
runSaveToPromptAction({ chat: lastFocusedWidget }, commandService);
639+
const options = { chat: lastFocusedWidget };
640+
return commandService.executeCommand(SAVE_TO_PROMPT_ACTION_ID, options,);
641641
}));
642642
this._store.add(slashCommandService.registerSlashCommand({
643643
command: 'help',

src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,28 +46,28 @@ export class PromptFileRewriter {
4646
return undefined;
4747
}
4848
editor.setSelection(tools.range);
49-
await this.rewriteTools(model, newTools, tools.range);
49+
this.rewriteTools(model, newTools, tools.range);
5050
}
5151

5252

5353
public rewriteTools(model: ITextModel, newTools: IToolAndToolSetEnablementMap | undefined, range: Range): void {
54+
const newString = newTools === undefined ? '' : `tools: ${this.getNewValueString(newTools)}`;
55+
model.pushStackElement();
56+
model.pushEditOperations(null, [EditOperation.replaceMove(range, newString)], () => null);
57+
model.pushStackElement();
58+
}
5459

60+
public getNewValueString(tools: IToolAndToolSetEnablementMap): string {
5561
const newToolNames: string[] = [];
56-
if (newTools === undefined) {
57-
model.pushStackElement();
58-
model.pushEditOperations(null, [EditOperation.replaceMove(range, '')], () => null);
59-
model.pushStackElement();
60-
return;
61-
}
6262
const toolsCoveredBySets = new Set<IToolData>();
63-
for (const [item, picked] of newTools) {
63+
for (const [item, picked] of tools) {
6464
if (picked && item instanceof ToolSet) {
6565
for (const tool of item.getTools()) {
6666
toolsCoveredBySets.add(tool);
6767
}
6868
}
6969
}
70-
for (const [item, picked] of newTools) {
70+
for (const [item, picked] of tools) {
7171
if (picked) {
7272
if (item instanceof ToolSet) {
7373
newToolNames.push(item.referenceName);
@@ -76,10 +76,7 @@ export class PromptFileRewriter {
7676
}
7777
}
7878
}
79-
80-
model.pushStackElement();
81-
model.pushEditOperations(null, [EditOperation.replaceMove(range, `tools: [${newToolNames.map(s => `'${s}'`).join(', ')}]`)], () => null);
82-
model.pushStackElement();
79+
return `[${newToolNames.map(s => `'${s}'`).join(', ')}]`;
8380
}
8481
}
8582

src/vs/workbench/contrib/chat/browser/promptSyntax/saveToPromptAction.ts

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

6-
import { assertDefined } from '../../../../../base/common/types.js';
76
import { localize2 } from '../../../../../nls.js';
87
import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';
9-
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
8+
109
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
11-
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
10+
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
1211
import { ILogService } from '../../../../../platform/log/common/log.js';
1312
import { PromptsConfig } from '../../common/promptSyntax/config/config.js';
14-
import { IEditorPane } from '../../../../common/editor.js';
1513
import { IEditorService } from '../../../../services/editor/common/editorService.js';
1614
import { ChatContextKeys } from '../../common/chatContextKeys.js';
1715
import { chatSubcommandLeader, IParsedChatRequest } from '../../common/chatParserTypes.js';
18-
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
1916
import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js';
2017
import { CHAT_CATEGORY } from '../actions/chatActions.js';
2118
import { IChatWidget } from '../chat.js';
22-
19+
import { ChatModeKind } from '../../common/constants.js';
20+
import { PromptFileRewriter } from './promptFileRewriter.js';
21+
import { ILanguageModelChatMetadata } from '../../common/languageModels.js';
22+
import { URI } from '../../../../../base/common/uri.js';
23+
import { Schemas } from '../../../../../base/common/network.js';
2324

2425
/**
2526
* Action ID for the `Save Prompt` action.
2627
*/
27-
const SAVE_TO_PROMPT_ACTION_ID = 'workbench.action.chat.save-to-prompt';
28+
export const SAVE_TO_PROMPT_ACTION_ID = 'workbench.action.chat.save-to-prompt';
2829

2930
/**
3031
* Name of the in-chat slash command associated with this action.
@@ -38,7 +39,7 @@ interface ISaveToPromptActionOptions {
3839
/**
3940
* Chat widget reference to save session of.
4041
*/
41-
chat: IChatWidget;
42+
readonly chat: IChatWidget;
4243
}
4344

4445
/**
@@ -61,77 +62,69 @@ class SaveToPromptAction extends Action2 {
6162
public async run(
6263
accessor: ServicesAccessor,
6364
options: ISaveToPromptActionOptions,
64-
): Promise<IEditorPane> {
65+
): Promise<void> {
6566
const logService = accessor.get(ILogService);
6667
const editorService = accessor.get(IEditorService);
67-
const toolsService = accessor.get(ILanguageModelToolsService);
68+
const rewriter = accessor.get(IInstantiationService).createInstance(PromptFileRewriter);
6869

6970
const logPrefix = 'save to prompt';
70-
const { chat } = options;
71-
72-
const { viewModel } = chat;
73-
assertDefined(
74-
viewModel,
75-
'No view model found on currently the active chat widget.',
76-
);
77-
78-
const { model } = viewModel;
79-
80-
const turns: ITurn[] = [];
81-
for (const request of model.getRequests()) {
82-
const { message, response: responseModel } = request;
83-
84-
if (isSaveToPromptSlashCommand(message)) {
85-
continue;
86-
}
87-
88-
if (responseModel === undefined) {
89-
logService.warn(
90-
`[${logPrefix}]: skipping request '${request.id}' with no response`,
91-
);
71+
const chatWidget = options.chat;
72+
const mode = chatWidget.input.currentModeObs.get();
73+
const model = chatWidget.input.selectedLanguageModel;
74+
75+
const toolAndToolsetMap = chatWidget.input.selectedToolsModel.entriesMap.get();
76+
77+
const output = [];
78+
output.push('---');
79+
output.push(`description: New prompt created from chat session`);
80+
output.push(`mode: ${mode.kind}`);
81+
if (mode.kind === ChatModeKind.Agent) {
82+
output.push(`tools: ${rewriter.getNewValueString(toolAndToolsetMap)}`);
83+
}
84+
if (model) {
85+
output.push(`model: ${ILanguageModelChatMetadata.asQualifiedName(model.metadata)}`);
86+
}
87+
output.push('---');
9288

93-
continue;
94-
}
89+
const viewModel = chatWidget.viewModel;
90+
if (viewModel) {
9591

96-
const { response } = responseModel;
92+
for (const request of viewModel.model.getRequests()) {
93+
const { message, response: responseModel } = request;
9794

98-
const tools = new Set<string>();
99-
for (const record of response.value) {
100-
if (('toolId' in record === false) || !record.toolId) {
95+
if (isSaveToPromptSlashCommand(message)) {
10196
continue;
10297
}
10398

104-
const tool = toolsService.getTool(record.toolId);
105-
if ((tool === undefined) || (!tool.toolReferenceName)) {
99+
if (responseModel === undefined) {
100+
logService.warn(`[${logPrefix}]: skipping request '${request.id}' with no response`);
106101
continue;
107102
}
108103

109-
tools.add(tool.toolReferenceName);
110-
}
111-
112-
turns.push({
113-
request: message.text,
114-
response: response.getMarkdown(),
115-
tools,
116-
});
117-
}
104+
const { response } = responseModel;
118105

119-
const promptText = renderPrompt(turns);
120-
121-
const editor = await editorService.openEditor({
122-
resource: undefined,
123-
contents: promptText,
124-
languageId: PROMPT_LANGUAGE_ID,
125-
});
106+
output.push(`<user>`);
107+
output.push(request.message.text);
108+
output.push(`</user>`);
109+
output.push();
110+
output.push(`<assistant>`);
111+
output.push(response.getMarkdown());
112+
output.push(`</assistant>`);
113+
output.push();
114+
}
115+
const promptText = output.join('\n');
126116

127-
assertDefined(
128-
editor,
129-
'Failed to open untitled editor for the prompt.',
130-
);
117+
const untitledPath = 'new.prompt.md';
118+
const untitledResource = URI.from({ scheme: Schemas.untitled, path: untitledPath });
131119

132-
editor.focus();
120+
const editor = await editorService.openEditor({
121+
resource: untitledResource,
122+
contents: promptText,
123+
languageId: PROMPT_LANGUAGE_ID,
124+
});
133125

134-
return editor;
126+
editor?.focus();
127+
}
135128
}
136129
}
137130

@@ -157,121 +150,6 @@ function isSaveToPromptSlashCommand(message: IParsedChatRequest): boolean {
157150
return true;
158151
}
159152

160-
/**
161-
* Render the response part of a `request`/`response` turn pair.
162-
*/
163-
function renderResponse(response: string): string {
164-
// if response starts with a code block, add an extra new line
165-
// before it, to prevent full blockquote from being be broken
166-
const delimiter = (response.startsWith('```'))
167-
? '\n>'
168-
: ' ';
169-
170-
// add `>` to the beginning of each line of the response
171-
// so it looks like a blockquote citing Copilot
172-
const quotedResponse = response.replaceAll('\n', '\n> ');
173-
174-
return `> Copilot:${delimiter}${quotedResponse}`;
175-
}
176-
177-
/**
178-
* Render a single `request`/`response` turn of the chat session.
179-
*/
180-
function renderTurn(turn: ITurn): string {
181-
const { request, response } = turn;
182-
183-
return `\n${request}\n\n${renderResponse(response)}`;
184-
}
185-
186-
/**
187-
* Render the entire chat session as a markdown prompt.
188-
*/
189-
function renderPrompt(turns: readonly ITurn[]): string {
190-
const content: string[] = [];
191-
const allTools = new Set<string>();
192-
193-
// render each turn and collect tool names
194-
// that were used in the each turn
195-
for (const turn of turns) {
196-
content.push(renderTurn(turn));
197-
198-
// collect all used tools into a set of strings
199-
for (const tool of turn.tools) {
200-
allTools.add(tool);
201-
}
202-
}
203-
204-
const result = [];
205-
206-
// add prompt header
207-
if (allTools.size !== 0) {
208-
result.push(renderHeader(allTools));
209-
}
210-
211-
// add chat request/response turns
212-
result.push(
213-
content.join('\n'),
214-
);
215-
216-
// add trailing empty line
217-
result.push('');
218-
219-
return result.join('\n');
220-
}
221-
222-
223-
/**
224-
* Render the `tools` metadata inside prompt header.
225-
*/
226-
function renderTools(tools: Set<string>): string {
227-
const toolStrings = [...tools].map((tool) => {
228-
return `'${tool}'`;
229-
});
230-
231-
return `tools: [${toolStrings.join(', ')}]`;
232-
}
233-
234-
/**
235-
* Render prompt header.
236-
*/
237-
function renderHeader(tools: Set<string>): string {
238-
// skip rendering the header if no tools provided
239-
if (tools.size === 0) {
240-
return '';
241-
}
242-
243-
return [
244-
'---',
245-
renderTools(tools),
246-
'---',
247-
].join('\n');
248-
}
249-
250-
/**
251-
* Interface for a single `request`/`response` turn
252-
* of a chat session.
253-
*/
254-
interface ITurn {
255-
request: string;
256-
response: string;
257-
tools: Set<string>;
258-
}
259-
260-
/**
261-
* Runs the `Save To Prompt` action with provided options. We export this
262-
* function instead of {@link SAVE_TO_PROMPT_ACTION_ID} directly to
263-
* encapsulate/enforce the correct options to be passed to the action.
264-
*/
265-
export function runSaveToPromptAction(
266-
options: ISaveToPromptActionOptions,
267-
commandService: ICommandService,
268-
) {
269-
return commandService.executeCommand(
270-
SAVE_TO_PROMPT_ACTION_ID,
271-
options,
272-
);
273-
}
274-
275153
/**
276154
* Helper to register all the `Save Prompt` actions.
277155
*/

0 commit comments

Comments
 (0)