Skip to content
9 changes: 9 additions & 0 deletions packages/core/src/amazonq/webview/ui/apps/baseConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,15 @@ export abstract class BaseConnector {
})
}

onPromptInputOptionChange = (tabId: string, optionsValues: Record<string, string>): void => {
this.sendMessageToExtension({
command: 'prompt-input-option-change',
optionsValues,
tabType: this.getTabType(),
tabID: tabId,
})
}

requestGenerativeAIAnswer = (tabID: string, messageId: string, payload: ChatPayload): Promise<any> => {
/**
* When a user presses "enter" send an event that indicates
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/amazonq/webview/ui/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type MessageCommand =
| 'help'
| 'chat-item-voted'
| 'chat-item-feedback'
| 'prompt-input-option-change'
| 'link-was-clicked'
| 'onboarding-page-interaction'
| 'source-link-click'
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/amazonq/webview/ui/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export interface ConnectorProps {
onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void
onNewTab: (tabType: TabType) => void
onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void
onPromptInputOptionChange: (tabId: string, optionsValues: Record<string, string>, eventId?: string) => void
handleCommand: (chatPrompt: ChatPrompt, tabId: string) => void
sendStaticMessages: (tabID: string, messages: ChatItem[]) => void
onContextCommandDataReceived: (message: MynahUIDataModel['contextCommands']) => void
Expand Down Expand Up @@ -617,6 +618,15 @@ export class Connector {
}
}

onPromptInputOptionChange = (tabId: string, optionsValues: Record<string, string>): void => {
switch (this.tabsStorage.getTab(tabId)?.type) {
case 'unknown':
case 'cwc':
this.cwChatConnector.onPromptInputOptionChange(tabId, optionsValues)
break
}
}

sendFeedback = (tabId: string, feedbackPayload: FeedbackPayload): void | undefined => {
switch (this.tabsStorage.getTab(tabId)?.type) {
case 'featuredev':
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/amazonq/webview/ui/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export const createMynahUI = (
}
},
onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string): void => {},
onPromptInputOptionChange: (tabId: string, optionsValues: Record<string, string>, eventId?: string): void => {},
onQuickHandlerCommand: (tabID: string, command?: string, eventId?: string) => {
tabsStorage.updateTabLastCommand(tabID, command)
if (command === 'aws.awsq.transform') {
Expand Down Expand Up @@ -940,6 +941,9 @@ export const createMynahUI = (
onFileActionClick: async (tabID: string, messageId: string, filePath: string, actionName: string) => {
connector.onFileActionClick(tabID, messageId, filePath, actionName)
},
onPromptInputOptionChange: (tabId, optionsValues) => {
connector.onPromptInputOptionChange(tabId, optionsValues)
},
onFileClick: connector.onFileClick,
tabs: {
'tab-1': {
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/amazonq/webview/ui/tabs/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ export class TabDataGenerator {
},
]
: [],
promptInputOptions: [
{
type: 'toggle',
id: 'prompt-type',
value: 'ask',
options: [
{
value: 'pair-programming-on',
icon: 'code-block', // TODO: correct icons
},
{
value: 'pair-programming-off',
icon: 'chat', // TODO: correct icons
},
],
},
],
}
return tabData
}
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/codewhispererChat/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
AcceptDiff,
QuickCommandGroupActionClick,
FileClick,
PromptInputOptionChange,
} from './controllers/chat/model'
import { EditorContextCommand, registerCommands } from './commands/registerCommands'
import { ContextSelectedMessage, CustomFormActionMessage } from './view/connector/connector'
Expand Down Expand Up @@ -56,6 +57,7 @@ export function init(appContext: AmazonQAppInitContext) {
processCustomFormAction: new EventEmitter<CustomFormActionMessage>(),
processContextSelected: new EventEmitter<ContextSelectedMessage>(),
processFileClick: new EventEmitter<FileClick>(),
processPromptInputOptionChange: new EventEmitter<PromptInputOptionChange>(),
}

const cwChatControllerMessageListeners = {
Expand Down Expand Up @@ -117,6 +119,9 @@ export function init(appContext: AmazonQAppInitContext) {
cwChatControllerEventEmitters.processContextSelected
),
processFileClick: new MessageListener<FileClick>(cwChatControllerEventEmitters.processFileClick),
processPromptInputOptionChange: new MessageListener<PromptInputOptionChange>(
cwChatControllerEventEmitters.processPromptInputOptionChange
),
}

const cwChatControllerMessagePublishers = {
Expand Down Expand Up @@ -180,6 +185,9 @@ export function init(appContext: AmazonQAppInitContext) {
cwChatControllerEventEmitters.processContextSelected
),
processFileClick: new MessagePublisher<FileClick>(cwChatControllerEventEmitters.processFileClick),
processPromptInputOptionChange: new MessagePublisher<PromptInputOptionChange>(
cwChatControllerEventEmitters.processPromptInputOptionChange
),
}

new CwChatController(
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/codewhispererChat/clients/chat/v0/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class ChatSession {
private _toolUse: ToolUse | undefined
private _showDiffOnFileWrite: boolean = false
private _context: PromptMessage['context']
private _pairProgrammingModeOn: boolean = true
private _messageIdToUpdate: string | undefined

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

public get pairProgrammingModeOn(): boolean {
return this._pairProgrammingModeOn
}

public setPairProgrammingModeOn(pairProgrammingModeOn: boolean) {
this._pairProgrammingModeOn = pairProgrammingModeOn
}

public get toolUse(): ToolUse | undefined {
return this._toolUse
}
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/codewhispererChat/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export const tools: Tool[] = Object.entries(toolsJson).map(([, toolSpec]) => ({
inputSchema: { json: toolSpec.inputSchema },
},
}))

export const noWriteTools: Tool[] = tools.filter(
(tool) => !['fsWrite', 'executeBash'].includes(tool.toolSpecification?.name || '')
)

export const defaultContextLengths: ContextLengths = {
additionalContextLengths: {
fileContextLength: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import { ConversationState, CursorState, DocumentSymbol, SymbolType, TextDocument } from '@amzn/codewhisperer-streaming'
import { AdditionalContentEntryAddition, ChatTriggerType, RelevantTextDocumentAddition, TriggerPayload } from '../model'
import { undefinedIfEmpty } from '../../../../shared/utilities/textUtilities'
import { tools } from '../../../constants'
import { getLogger } from '../../../../shared/logger/logger'
import vscode from 'vscode'
import { noWriteTools, tools } from '../../../constants'

const fqnNameSizeDownLimit = 1
const fqnNameSizeUpLimit = 256
Expand Down Expand Up @@ -164,7 +164,7 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { c
workspaceFolders: vscode.workspace.workspaceFolders?.map(({ uri }) => uri.fsPath) ?? [],
},
additionalContext: triggerPayload.additionalContents,
tools,
tools: triggerPayload.pairProgrammingModeOn ? tools : noWriteTools,
...(triggerPayload.toolResults !== undefined &&
triggerPayload.toolResults !== null && { toolResults: triggerPayload.toolResults }),
},
Expand Down
85 changes: 85 additions & 0 deletions packages/core/src/codewhispererChat/controllers/chat/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
DocumentReference,
FileClick,
RelevantTextDocumentAddition,
PromptInputOptionChange,
} from './model'
import {
AppToWebViewMessageDispatcher,
Expand Down Expand Up @@ -117,6 +118,7 @@ export interface ChatControllerMessagePublishers {
readonly processCustomFormAction: MessagePublisher<CustomFormActionMessage>
readonly processContextSelected: MessagePublisher<ContextSelectedMessage>
readonly processFileClick: MessagePublisher<FileClick>
readonly processPromptInputOptionChange: MessagePublisher<PromptInputOptionChange>
}

export interface ChatControllerMessageListeners {
Expand All @@ -142,6 +144,7 @@ export interface ChatControllerMessageListeners {
readonly processCustomFormAction: MessageListener<CustomFormActionMessage>
readonly processContextSelected: MessageListener<ContextSelectedMessage>
readonly processFileClick: MessageListener<FileClick>
readonly processPromptInputOptionChange: MessageListener<PromptInputOptionChange>
}

export class ChatController {
Expand Down Expand Up @@ -277,6 +280,9 @@ export class ChatController {
this.chatControllerMessageListeners.processFileClick.onMessage((data) => {
return this.processFileClickMessage(data)
})
this.chatControllerMessageListeners.processPromptInputOptionChange.onMessage((data) => {
return this.processPromptInputOptionChange(data)
})
}

private registerUserPromptsWatcher() {
Expand Down Expand Up @@ -631,6 +637,69 @@ export class ChatController {
telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' })
}

private async processUnavailableToolUseMessage(message: CustomFormActionMessage) {
const tabID = message.tabID
if (!tabID) {
return
}
this.editorContextExtractor
.extractContextForTrigger('ChatMessage')
.then(async (context) => {
const triggerID = randomUUID()
this.triggerEventsStorage.addTriggerEvent({
id: triggerID,
tabID: message.tabID,
message: undefined,
type: 'chat_message',
context,
})
const session = this.sessionStorage.getSession(tabID)
const toolUse = session.toolUse
if (!toolUse || !toolUse.input) {
return
}
session.setToolUse(undefined)

const toolResults: ToolResult[] = []

toolResults.push({
content: [{ text: 'This tool is not an available tool in this mode' }],
toolUseId: toolUse.toolUseId,
status: ToolResultStatus.ERROR,
})

await this.generateResponse(
{
message: '',
trigger: ChatTriggerType.ChatMessage,
query: undefined,
codeSelection: context?.focusAreaContext?.selectionInsideExtendedCodeBlock,
fileText: context?.focusAreaContext?.extendedCodeBlock ?? '',
fileLanguage: context?.activeFileContext?.fileLanguage,
filePath: context?.activeFileContext?.filePath,
matchPolicy: context?.activeFileContext?.matchPolicy,
codeQuery: context?.focusAreaContext?.names,
userIntent: undefined,
customization: getSelectedCustomization(),
toolResults: toolResults,
origin: Origin.IDE,
context: session.context ?? [],
relevantTextDocuments: [],
additionalContents: [],
documentReferences: [],
useRelevantDocuments: false,
contextLengths: {
...defaultContextLengths,
},
},
triggerID
)
})
.catch((e) => {
this.processException(e, tabID)
})
}

private async processToolUseMessage(message: CustomFormActionMessage) {
const tabID = message.tabID
if (!tabID) {
Expand Down Expand Up @@ -763,6 +832,9 @@ export class ChatController {
case 'reject-code-diff':
await this.closeDiffView()
break
case 'tool-unavailable':
await this.processUnavailableToolUseMessage(message)
break
default:
getLogger().warn(`Unhandled action: ${message.action.id}`)
}
Expand All @@ -773,6 +845,18 @@ export class ChatController {
this.handlePromptCreate(message.tabID)
}
}

private async processPromptInputOptionChange(message: PromptInputOptionChange) {
const session = this.sessionStorage.getSession(message.tabID)
const promptTypeValue = message.optionsValues['prompt-type']
// TODO: display message: You turned off pair programmer mode. Q will not include code diffs or run commands in the chat.
if (promptTypeValue === 'pair-programming-on') {
session.setPairProgrammingModeOn(true)
} else {
session.setPairProgrammingModeOn(false)
}
}

private async processFileClickMessage(message: FileClick) {
const session = this.sessionStorage.getSession(message.tabID)
// Check if user clicked on filePath in the contextList or in the fileListTree and perform the functionality accordingly.
Expand Down Expand Up @@ -1353,6 +1437,7 @@ export class ChatController {

triggerPayload.contextLengths.userInputContextLength = triggerPayload.message.length
triggerPayload.contextLengths.focusFileContextLength = triggerPayload.fileText.length
triggerPayload.pairProgrammingModeOn = session.pairProgrammingModeOn

const request = triggerPayloadToChatRequest(triggerPayload)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { ToolType, ToolUtils } from '../../../tools/toolUtils'
import { ChatStream } from '../../../tools/chatStream'
import path from 'path'
import { CommandValidation } from '../../../tools/executeBash'
import { noWriteTools, tools } from '../../../constants'
import { Change } from 'diff'
import { FsWriteParams } from '../../../tools/fsWrite'

Expand Down Expand Up @@ -218,6 +219,18 @@ export class Messenger {
toolUse.name = cwChatEvent.toolUseEvent.name ?? ''
session.setToolUse(toolUse)

const availableToolsNames = (session.pairProgrammingModeOn ? tools : noWriteTools).map(
(item) => item.toolSpecification?.name
)
if (!availableToolsNames.includes(toolUse.name)) {
this.dispatcher.sendCustomFormActionMessage(
new CustomFormActionMessage(tabID, {
id: 'tool-unavailable',
})
)
return
}

const tool = ToolUtils.tryFromToolUse(toolUse)
if ('type' in tool) {
let changeList: Change[] | undefined = undefined
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/codewhispererChat/controllers/chat/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ export interface FileClick {
filePath: string
}

export interface PromptInputOptionChange {
command: string
tabID: string
messageId: string
optionsValues: Record<string, string>
}

export interface ChatItemVotedMessage {
tabID: string
command: string
Expand Down Expand Up @@ -204,6 +211,7 @@ export interface TriggerPayload {
workspaceRulesCount?: number
toolResults?: ToolResult[]
origin?: Origin
pairProgrammingModeOn?: boolean
}

export type ContextLengths = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export class UIMessageListener {
getLogger().error('chatItemFeedback failed: %s', (e as Error).message)
})
break
case 'prompt-input-option-change':
this.promptInputOptionChange(msg)
break
case 'ui-focus':
this.processUIFocus(msg)
break
Expand Down Expand Up @@ -293,4 +296,13 @@ export class UIMessageListener {
filePath: msg.filePath,
})
}

private promptInputOptionChange(msg: any) {
this.chatControllerMessagePublishers.processPromptInputOptionChange.publish({
messageId: msg.messageId,
tabID: msg.tabID,
command: msg.command,
optionsValues: msg.optionsValues,
})
}
}
Loading