Skip to content

Commit d3f8085

Browse files
laileni-awschungjacctlai95
authored
Pair programming mode (#20)
* pair programming mode * pairProgrammingModeOn boolean * use session in chatHistory * revert chatHistory * client side toolName validation * refactor(chat): simplify history logic (aws#6946) ## Problem History logic cleanup ## Solution - History does not need to be in triggerPayload - ChatHistory does not need tools - We don't need to create and maintain a separate newUserMessage object --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Jacob Chung <[email protected]> Co-authored-by: Tai Lai <[email protected]>
1 parent aefe920 commit d3f8085

File tree

14 files changed

+211
-58
lines changed

14 files changed

+211
-58
lines changed

packages/core/src/amazonq/webview/ui/apps/baseConnector.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ export abstract class BaseConnector {
194194
})
195195
}
196196

197+
onPromptInputOptionChange = (tabId: string, optionsValues: Record<string, string>): void => {
198+
this.sendMessageToExtension({
199+
command: 'prompt-input-option-change',
200+
optionsValues,
201+
tabType: this.getTabType(),
202+
tabID: tabId,
203+
})
204+
}
205+
197206
requestGenerativeAIAnswer = (tabID: string, messageId: string, payload: ChatPayload): Promise<any> => {
198207
/**
199208
* When a user presses "enter" send an event that indicates

packages/core/src/amazonq/webview/ui/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type MessageCommand =
2525
| 'help'
2626
| 'chat-item-voted'
2727
| 'chat-item-feedback'
28+
| 'prompt-input-option-change'
2829
| 'link-was-clicked'
2930
| 'onboarding-page-interaction'
3031
| 'source-link-click'

packages/core/src/amazonq/webview/ui/connector.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export interface ConnectorProps {
9797
onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void
9898
onNewTab: (tabType: TabType) => void
9999
onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void
100+
onPromptInputOptionChange: (tabId: string, optionsValues: Record<string, string>, eventId?: string) => void
100101
handleCommand: (chatPrompt: ChatPrompt, tabId: string) => void
101102
sendStaticMessages: (tabID: string, messages: ChatItem[]) => void
102103
onContextCommandDataReceived: (message: MynahUIDataModel['contextCommands']) => void
@@ -617,6 +618,15 @@ export class Connector {
617618
}
618619
}
619620

621+
onPromptInputOptionChange = (tabId: string, optionsValues: Record<string, string>): void => {
622+
switch (this.tabsStorage.getTab(tabId)?.type) {
623+
case 'unknown':
624+
case 'cwc':
625+
this.cwChatConnector.onPromptInputOptionChange(tabId, optionsValues)
626+
break
627+
}
628+
}
629+
620630
sendFeedback = (tabId: string, feedbackPayload: FeedbackPayload): void | undefined => {
621631
switch (this.tabsStorage.getTab(tabId)?.type) {
622632
case 'featuredev':

packages/core/src/amazonq/webview/ui/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export const createMynahUI = (
239239
}
240240
},
241241
onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string): void => {},
242+
onPromptInputOptionChange: (tabId: string, optionsValues: Record<string, string>, eventId?: string): void => {},
242243
onQuickHandlerCommand: (tabID: string, command?: string, eventId?: string) => {
243244
tabsStorage.updateTabLastCommand(tabID, command)
244245
if (command === 'aws.awsq.transform') {
@@ -940,6 +941,9 @@ export const createMynahUI = (
940941
onFileActionClick: async (tabID: string, messageId: string, filePath: string, actionName: string) => {
941942
connector.onFileActionClick(tabID, messageId, filePath, actionName)
942943
},
944+
onPromptInputOptionChange: (tabId, optionsValues) => {
945+
connector.onPromptInputOptionChange(tabId, optionsValues)
946+
},
943947
onFileClick: connector.onFileClick,
944948
tabs: {
945949
'tab-1': {

packages/core/src/amazonq/webview/ui/tabs/generator.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,23 @@ export class TabDataGenerator {
7272
},
7373
]
7474
: [],
75+
promptInputOptions: [
76+
{
77+
type: 'toggle',
78+
id: 'prompt-type',
79+
value: 'ask',
80+
options: [
81+
{
82+
value: 'pair-programming-on',
83+
icon: 'code-block', // TODO: correct icons
84+
},
85+
{
86+
value: 'pair-programming-off',
87+
icon: 'chat', // TODO: correct icons
88+
},
89+
],
90+
},
91+
],
7592
}
7693
return tabData
7794
}

packages/core/src/codewhispererChat/app.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
AcceptDiff,
2929
QuickCommandGroupActionClick,
3030
FileClick,
31+
PromptInputOptionChange,
3132
} from './controllers/chat/model'
3233
import { EditorContextCommand, registerCommands } from './commands/registerCommands'
3334
import { ContextSelectedMessage, CustomFormActionMessage } from './view/connector/connector'
@@ -56,6 +57,7 @@ export function init(appContext: AmazonQAppInitContext) {
5657
processCustomFormAction: new EventEmitter<CustomFormActionMessage>(),
5758
processContextSelected: new EventEmitter<ContextSelectedMessage>(),
5859
processFileClick: new EventEmitter<FileClick>(),
60+
processPromptInputOptionChange: new EventEmitter<PromptInputOptionChange>(),
5961
}
6062

6163
const cwChatControllerMessageListeners = {
@@ -117,6 +119,9 @@ export function init(appContext: AmazonQAppInitContext) {
117119
cwChatControllerEventEmitters.processContextSelected
118120
),
119121
processFileClick: new MessageListener<FileClick>(cwChatControllerEventEmitters.processFileClick),
122+
processPromptInputOptionChange: new MessageListener<PromptInputOptionChange>(
123+
cwChatControllerEventEmitters.processPromptInputOptionChange
124+
),
120125
}
121126

122127
const cwChatControllerMessagePublishers = {
@@ -180,6 +185,9 @@ export function init(appContext: AmazonQAppInitContext) {
180185
cwChatControllerEventEmitters.processContextSelected
181186
),
182187
processFileClick: new MessagePublisher<FileClick>(cwChatControllerEventEmitters.processFileClick),
188+
processPromptInputOptionChange: new MessagePublisher<PromptInputOptionChange>(
189+
cwChatControllerEventEmitters.processPromptInputOptionChange
190+
),
183191
}
184192

185193
new CwChatController(

packages/core/src/codewhispererChat/clients/chat/v0/chat.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class ChatSession {
2828
private _toolUse: ToolUse | undefined
2929
private _showDiffOnFileWrite: boolean = false
3030
private _context: PromptMessage['context']
31+
private _pairProgrammingModeOn: boolean = true
3132
private _messageIdToUpdate: string | undefined
3233

3334
contexts: Map<string, { first: number; second: number }[]> = new Map()
@@ -38,6 +39,14 @@ export class ChatSession {
3839
return this.sessionId
3940
}
4041

42+
public get pairProgrammingModeOn(): boolean {
43+
return this._pairProgrammingModeOn
44+
}
45+
46+
public setPairProgrammingModeOn(pairProgrammingModeOn: boolean) {
47+
this._pairProgrammingModeOn = pairProgrammingModeOn
48+
}
49+
4150
public get toolUse(): ToolUse | undefined {
4251
return this._toolUse
4352
}

packages/core/src/codewhispererChat/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export const tools: Tool[] = Object.entries(toolsJson).map(([, toolSpec]) => ({
3030
inputSchema: { json: toolSpec.inputSchema },
3131
},
3232
}))
33+
34+
export const noWriteTools: Tool[] = tools.filter(
35+
(tool) => !['fsWrite', 'executeBash'].includes(tool.toolSpecification?.name || '')
36+
)
37+
3338
export const defaultContextLengths: ContextLengths = {
3439
additionalContextLengths: {
3540
fileContextLength: 0,

packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import { ConversationState, CursorState, DocumentSymbol, SymbolType, TextDocument } from '@amzn/codewhisperer-streaming'
77
import { AdditionalContentEntryAddition, ChatTriggerType, RelevantTextDocumentAddition, TriggerPayload } from '../model'
88
import { undefinedIfEmpty } from '../../../../shared/utilities/textUtilities'
9-
import { tools } from '../../../constants'
109
import { getLogger } from '../../../../shared/logger/logger'
1110
import vscode from 'vscode'
11+
import { noWriteTools, tools } from '../../../constants'
1212

1313
const fqnNameSizeDownLimit = 1
1414
const fqnNameSizeUpLimit = 256
@@ -164,7 +164,7 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { c
164164
workspaceFolders: vscode.workspace.workspaceFolders?.map(({ uri }) => uri.fsPath) ?? [],
165165
},
166166
additionalContext: triggerPayload.additionalContents,
167-
tools,
167+
tools: triggerPayload.pairProgrammingModeOn ? tools : noWriteTools,
168168
...(triggerPayload.toolResults !== undefined &&
169169
triggerPayload.toolResults !== null && { toolResults: triggerPayload.toolResults }),
170170
},
@@ -175,7 +175,6 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { c
175175
},
176176
chatTriggerType,
177177
customizationArn: customizationArn,
178-
history: triggerPayload.chatHistory,
179178
},
180179
}
181180
}

packages/core/src/codewhispererChat/controllers/chat/controller.ts

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
DocumentReference,
3333
FileClick,
3434
RelevantTextDocumentAddition,
35+
PromptInputOptionChange,
3536
} from './model'
3637
import {
3738
AppToWebViewMessageDispatcher,
@@ -82,9 +83,9 @@ import {
8283
createSavedPromptCommandId,
8384
aditionalContentNameLimit,
8485
additionalContentInnerContextLimit,
85-
tools,
8686
workspaceChunkMaxSize,
8787
defaultContextLengths,
88+
noWriteTools,
8889
} from '../../constants'
8990
import { ChatSession } from '../../clients/chat/v0/chat'
9091
import { amazonQTabSuffix } from '../../../shared/constants'
@@ -118,6 +119,7 @@ export interface ChatControllerMessagePublishers {
118119
readonly processCustomFormAction: MessagePublisher<CustomFormActionMessage>
119120
readonly processContextSelected: MessagePublisher<ContextSelectedMessage>
120121
readonly processFileClick: MessagePublisher<FileClick>
122+
readonly processPromptInputOptionChange: MessagePublisher<PromptInputOptionChange>
121123
}
122124

123125
export interface ChatControllerMessageListeners {
@@ -143,6 +145,7 @@ export interface ChatControllerMessageListeners {
143145
readonly processCustomFormAction: MessageListener<CustomFormActionMessage>
144146
readonly processContextSelected: MessageListener<ContextSelectedMessage>
145147
readonly processFileClick: MessageListener<FileClick>
148+
readonly processPromptInputOptionChange: MessageListener<PromptInputOptionChange>
146149
}
147150

148151
export class ChatController {
@@ -278,6 +281,9 @@ export class ChatController {
278281
this.chatControllerMessageListeners.processFileClick.onMessage((data) => {
279282
return this.processFileClickMessage(data)
280283
})
284+
this.chatControllerMessageListeners.processPromptInputOptionChange.onMessage((data) => {
285+
return this.processPromptInputOptionChange(data)
286+
})
281287
}
282288

283289
private registerUserPromptsWatcher() {
@@ -632,6 +638,70 @@ export class ChatController {
632638
telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' })
633639
}
634640

641+
private async processUnavailableToolUseMessage(message: CustomFormActionMessage) {
642+
const tabID = message.tabID
643+
if (!tabID) {
644+
return
645+
}
646+
this.editorContextExtractor
647+
.extractContextForTrigger('ChatMessage')
648+
.then(async (context) => {
649+
const triggerID = randomUUID()
650+
this.triggerEventsStorage.addTriggerEvent({
651+
id: triggerID,
652+
tabID: message.tabID,
653+
message: undefined,
654+
type: 'chat_message',
655+
context,
656+
})
657+
const session = this.sessionStorage.getSession(tabID)
658+
const toolUse = session.toolUse
659+
if (!toolUse || !toolUse.input) {
660+
return
661+
}
662+
session.setToolUse(undefined)
663+
664+
const toolResults: ToolResult[] = []
665+
666+
toolResults.push({
667+
content: [{ text: 'This tool is not an available tool in this mode' }],
668+
toolUseId: toolUse.toolUseId,
669+
status: ToolResultStatus.ERROR,
670+
})
671+
672+
await this.generateResponse(
673+
{
674+
message: '',
675+
trigger: ChatTriggerType.ChatMessage,
676+
query: undefined,
677+
codeSelection: context?.focusAreaContext?.selectionInsideExtendedCodeBlock,
678+
fileText: context?.focusAreaContext?.extendedCodeBlock ?? '',
679+
fileLanguage: context?.activeFileContext?.fileLanguage,
680+
filePath: context?.activeFileContext?.filePath,
681+
matchPolicy: context?.activeFileContext?.matchPolicy,
682+
codeQuery: context?.focusAreaContext?.names,
683+
userIntent: undefined,
684+
customization: getSelectedCustomization(),
685+
toolResults: toolResults,
686+
origin: Origin.IDE,
687+
chatHistory: this.chatHistoryStorage.getTabHistory(tabID).getHistory(),
688+
context: session.context ?? [],
689+
relevantTextDocuments: [],
690+
additionalContents: [],
691+
documentReferences: [],
692+
useRelevantDocuments: false,
693+
contextLengths: {
694+
...defaultContextLengths,
695+
},
696+
},
697+
triggerID
698+
)
699+
})
700+
.catch((e) => {
701+
this.processException(e, tabID)
702+
})
703+
}
704+
635705
private async processToolUseMessage(message: CustomFormActionMessage) {
636706
const tabID = message.tabID
637707
if (!tabID) {
@@ -764,6 +834,9 @@ export class ChatController {
764834
case 'reject-code-diff':
765835
await this.closeDiffView()
766836
break
837+
case 'tool-unavailable':
838+
await this.processUnavailableToolUseMessage(message)
839+
break
767840
default:
768841
getLogger().warn(`Unhandled action: ${message.action.id}`)
769842
}
@@ -774,6 +847,18 @@ export class ChatController {
774847
this.handlePromptCreate(message.tabID)
775848
}
776849
}
850+
851+
private async processPromptInputOptionChange(message: PromptInputOptionChange) {
852+
const session = this.sessionStorage.getSession(message.tabID)
853+
const promptTypeValue = message.optionsValues['prompt-type']
854+
// TODO: display message: You turned off pair programmer mode. Q will not include code diffs or run commands in the chat.
855+
if (promptTypeValue === 'pair-programming-on') {
856+
session.setPairProgrammingModeOn(true)
857+
} else {
858+
session.setPairProgrammingModeOn(false)
859+
}
860+
}
861+
777862
private async processFileClickMessage(message: FileClick) {
778863
const session = this.sessionStorage.getSession(message.tabID)
779864
// Check if user clicked on filePath in the contextList or in the fileListTree and perform the functionality accordingly.
@@ -1354,25 +1439,18 @@ export class ChatController {
13541439

13551440
triggerPayload.contextLengths.userInputContextLength = triggerPayload.message.length
13561441
triggerPayload.contextLengths.focusFileContextLength = triggerPayload.fileText.length
1442+
triggerPayload.pairProgrammingModeOn = session.pairProgrammingModeOn
1443+
1444+
const request = triggerPayloadToChatRequest(triggerPayload)
13571445

13581446
const chatHistory = this.chatHistoryStorage.getTabHistory(tabID)
1359-
const newUserMessage = {
1360-
userInputMessage: {
1361-
content: triggerPayload.message,
1362-
userIntent: triggerPayload.userIntent,
1363-
...(triggerPayload.origin && { origin: triggerPayload.origin }),
1364-
userInputMessageContext: {
1365-
tools: tools,
1366-
...(triggerPayload.toolResults && { toolResults: triggerPayload.toolResults }),
1367-
},
1368-
},
1369-
}
1370-
const fixedHistoryMessage = chatHistory.fixHistory(newUserMessage)
1371-
if (fixedHistoryMessage.userInputMessage?.userInputMessageContext) {
1372-
triggerPayload.toolResults = fixedHistoryMessage.userInputMessage.userInputMessageContext.toolResults
1447+
const currentMessage = request.conversationState.currentMessage
1448+
if (currentMessage) {
1449+
chatHistory.fixHistory(currentMessage)
1450+
13731451
}
1374-
triggerPayload.chatHistory = chatHistory.getHistory()
1375-
const request = triggerPayloadToChatRequest(triggerPayload)
1452+
request.conversationState.history = chatHistory.getHistory()
1453+
13761454
const conversationId = chatHistory.getConversationId() || randomUUID()
13771455
chatHistory.setConversationId(conversationId)
13781456
request.conversationState.conversationId = conversationId
@@ -1426,8 +1504,8 @@ export class ChatController {
14261504
}
14271505
this.telemetryHelper.recordEnterFocusConversation(triggerEvent.tabID)
14281506
this.telemetryHelper.recordStartConversation(triggerEvent, triggerPayload)
1429-
if (request.conversationState.currentMessage) {
1430-
chatHistory.appendUserMessage(request.conversationState.currentMessage)
1507+
if (currentMessage) {
1508+
chatHistory.appendUserMessage(currentMessage)
14311509
}
14321510

14331511
getLogger().info(

0 commit comments

Comments
 (0)