Skip to content

Commit 1d66049

Browse files
feat(amazonq): Pair programming mode (#21)
* pair programming mode * pairProgrammingModeOn boolean * use session in chatHistory * revert chatHistory * client side toolName validation * remove chatHistory from processUnavailableToolUseMessage * unused import --------- Co-authored-by: Jacob Chung <[email protected]>
1 parent a4627c7 commit 1d66049

File tree

13 files changed

+183
-2
lines changed

13 files changed

+183
-2
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 & 2 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
},

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

Lines changed: 85 additions & 0 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,
@@ -117,6 +118,7 @@ export interface ChatControllerMessagePublishers {
117118
readonly processCustomFormAction: MessagePublisher<CustomFormActionMessage>
118119
readonly processContextSelected: MessagePublisher<ContextSelectedMessage>
119120
readonly processFileClick: MessagePublisher<FileClick>
121+
readonly processPromptInputOptionChange: MessagePublisher<PromptInputOptionChange>
120122
}
121123

122124
export interface ChatControllerMessageListeners {
@@ -142,6 +144,7 @@ export interface ChatControllerMessageListeners {
142144
readonly processCustomFormAction: MessageListener<CustomFormActionMessage>
143145
readonly processContextSelected: MessageListener<ContextSelectedMessage>
144146
readonly processFileClick: MessageListener<FileClick>
147+
readonly processPromptInputOptionChange: MessageListener<PromptInputOptionChange>
145148
}
146149

147150
export class ChatController {
@@ -277,6 +280,9 @@ export class ChatController {
277280
this.chatControllerMessageListeners.processFileClick.onMessage((data) => {
278281
return this.processFileClickMessage(data)
279282
})
283+
this.chatControllerMessageListeners.processPromptInputOptionChange.onMessage((data) => {
284+
return this.processPromptInputOptionChange(data)
285+
})
280286
}
281287

282288
private registerUserPromptsWatcher() {
@@ -631,6 +637,69 @@ export class ChatController {
631637
telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' })
632638
}
633639

640+
private async processUnavailableToolUseMessage(message: CustomFormActionMessage) {
641+
const tabID = message.tabID
642+
if (!tabID) {
643+
return
644+
}
645+
this.editorContextExtractor
646+
.extractContextForTrigger('ChatMessage')
647+
.then(async (context) => {
648+
const triggerID = randomUUID()
649+
this.triggerEventsStorage.addTriggerEvent({
650+
id: triggerID,
651+
tabID: message.tabID,
652+
message: undefined,
653+
type: 'chat_message',
654+
context,
655+
})
656+
const session = this.sessionStorage.getSession(tabID)
657+
const toolUse = session.toolUse
658+
if (!toolUse || !toolUse.input) {
659+
return
660+
}
661+
session.setToolUse(undefined)
662+
663+
const toolResults: ToolResult[] = []
664+
665+
toolResults.push({
666+
content: [{ text: 'This tool is not an available tool in this mode' }],
667+
toolUseId: toolUse.toolUseId,
668+
status: ToolResultStatus.ERROR,
669+
})
670+
671+
await this.generateResponse(
672+
{
673+
message: '',
674+
trigger: ChatTriggerType.ChatMessage,
675+
query: undefined,
676+
codeSelection: context?.focusAreaContext?.selectionInsideExtendedCodeBlock,
677+
fileText: context?.focusAreaContext?.extendedCodeBlock ?? '',
678+
fileLanguage: context?.activeFileContext?.fileLanguage,
679+
filePath: context?.activeFileContext?.filePath,
680+
matchPolicy: context?.activeFileContext?.matchPolicy,
681+
codeQuery: context?.focusAreaContext?.names,
682+
userIntent: undefined,
683+
customization: getSelectedCustomization(),
684+
toolResults: toolResults,
685+
origin: Origin.IDE,
686+
context: session.context ?? [],
687+
relevantTextDocuments: [],
688+
additionalContents: [],
689+
documentReferences: [],
690+
useRelevantDocuments: false,
691+
contextLengths: {
692+
...defaultContextLengths,
693+
},
694+
},
695+
triggerID
696+
)
697+
})
698+
.catch((e) => {
699+
this.processException(e, tabID)
700+
})
701+
}
702+
634703
private async processToolUseMessage(message: CustomFormActionMessage) {
635704
const tabID = message.tabID
636705
if (!tabID) {
@@ -763,6 +832,9 @@ export class ChatController {
763832
case 'reject-code-diff':
764833
await this.closeDiffView()
765834
break
835+
case 'tool-unavailable':
836+
await this.processUnavailableToolUseMessage(message)
837+
break
766838
default:
767839
getLogger().warn(`Unhandled action: ${message.action.id}`)
768840
}
@@ -773,6 +845,18 @@ export class ChatController {
773845
this.handlePromptCreate(message.tabID)
774846
}
775847
}
848+
849+
private async processPromptInputOptionChange(message: PromptInputOptionChange) {
850+
const session = this.sessionStorage.getSession(message.tabID)
851+
const promptTypeValue = message.optionsValues['prompt-type']
852+
// TODO: display message: You turned off pair programmer mode. Q will not include code diffs or run commands in the chat.
853+
if (promptTypeValue === 'pair-programming-on') {
854+
session.setPairProgrammingModeOn(true)
855+
} else {
856+
session.setPairProgrammingModeOn(false)
857+
}
858+
}
859+
776860
private async processFileClickMessage(message: FileClick) {
777861
const session = this.sessionStorage.getSession(message.tabID)
778862
// Check if user clicked on filePath in the contextList or in the fileListTree and perform the functionality accordingly.
@@ -1353,6 +1437,7 @@ export class ChatController {
13531437

13541438
triggerPayload.contextLengths.userInputContextLength = triggerPayload.message.length
13551439
triggerPayload.contextLengths.focusFileContextLength = triggerPayload.fileText.length
1440+
triggerPayload.pairProgrammingModeOn = session.pairProgrammingModeOn
13561441

13571442
const request = triggerPayloadToChatRequest(triggerPayload)
13581443

0 commit comments

Comments
 (0)