Skip to content

Commit 8600846

Browse files
committed
Preview UX
1 parent 6dd25b5 commit 8600846

File tree

6 files changed

+231
-47
lines changed

6 files changed

+231
-47
lines changed

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

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ChatItemButton,
99
ChatItemFormItem,
1010
ChatItemType,
11+
MynahIconsType,
1112
MynahUIDataModel,
1213
QuickActionCommand,
1314
} from '@aws/mynah-ui'
@@ -109,6 +110,10 @@ export class Connector extends BaseConnector {
109110
title: messageData.title,
110111
buttons: messageData.buttons ?? undefined,
111112
fileList: messageData.fileList ?? undefined,
113+
header: messageData.header ?? undefined,
114+
padding: messageData.padding ?? undefined,
115+
fullWidth: messageData.fullWidth ?? undefined,
116+
codeBlockActions: messageData.codeBlockActions ?? undefined,
112117
}
113118

114119
if (messageData.relatedSuggestions !== undefined) {
@@ -147,6 +152,10 @@ export class Connector extends BaseConnector {
147152
: undefined,
148153
buttons: messageData.buttons ?? undefined,
149154
canBeVoted: messageData.canBeVoted ?? false,
155+
header: messageData.header ?? undefined,
156+
padding: messageData.padding ?? undefined,
157+
fullWidth: messageData.fullWidth ?? undefined,
158+
codeBlockActions: messageData.codeBlockActions ?? undefined,
150159
}
151160
this.onChatAnswerReceived(messageData.tabID, answer, messageData)
152161

@@ -270,35 +279,32 @@ export class Connector extends BaseConnector {
270279
) {
271280
return
272281
}
282+
// Can not assign body as "undefined" or "null" because both of these values will be overriden at main.ts in onChatAnswerUpdated
283+
// TODO: Refactor in next PR if necessary.
273284
const answer: ChatItem = {
274285
type: ChatItemType.ANSWER,
275286
messageId: messageId,
276287
buttons: [],
288+
body: ' ',
277289
}
278290
switch (action.id) {
279291
case 'accept-code-diff':
280-
answer.buttons = [
281-
{
282-
keepCardAfterClick: true,
283-
text: 'Accepted code',
284-
id: 'accepted-code-diff',
292+
if (answer.header) {
293+
answer.header.status = {
294+
icon: 'ok' as MynahIconsType,
295+
text: 'Accepted',
285296
status: 'success',
286-
position: 'outside',
287-
disabled: true,
288-
},
289-
]
297+
}
298+
}
290299
break
291300
case 'reject-code-diff':
292-
answer.buttons = [
293-
{
294-
keepCardAfterClick: true,
295-
text: 'Rejected code',
296-
id: 'rejected-code-diff',
301+
if (answer.header) {
302+
answer.header.status = {
303+
icon: 'cancel' as MynahIconsType,
304+
text: 'Rejected',
297305
status: 'error',
298-
position: 'outside',
299-
disabled: true,
300-
},
301-
]
306+
}
307+
}
302308
break
303309
case 'confirm-tool-use':
304310
answer.buttons = [

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ export const createMynahUI = (
330330
...(item.followUp !== undefined ? { followUp: item.followUp } : {}),
331331
...(item.footer !== undefined ? { footer: item.footer } : {}),
332332
...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}),
333+
...(item.header !== undefined ? { header: item.header } : { header: undefined }),
333334
})
334335
} else {
335336
mynahUI.updateLastChatAnswer(tabID, {
@@ -338,6 +339,7 @@ export const createMynahUI = (
338339
...(item.followUp !== undefined ? { followUp: item.followUp } : {}),
339340
...(item.footer !== undefined ? { footer: item.footer } : {}),
340341
...(item.canBeVoted !== undefined ? { canBeVoted: item.canBeVoted } : {}),
342+
...(item.header !== undefined ? { header: item.header } : { header: undefined }),
341343
})
342344
}
343345
},
@@ -353,6 +355,11 @@ export const createMynahUI = (
353355
...(item.fileList !== undefined ? { fileList: item.fileList } : {}),
354356
...(item.header !== undefined ? { header: item.header } : { header: undefined }),
355357
...(item.buttons !== undefined ? { buttons: item.buttons } : { buttons: undefined }),
358+
...(item.fullWidth !== undefined ? { fullWidth: item.fullWidth } : { fullWidth: undefined }),
359+
...(item.padding !== undefined ? { padding: item.padding } : { padding: undefined }),
360+
...(item.codeBlockActions !== undefined
361+
? { codeBlockActions: item.codeBlockActions }
362+
: { codeBlockActions: undefined }),
356363
})
357364
if (
358365
item.messageId !== undefined &&

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

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ import { LspController } from '../../../../amazonq/lsp/lspController'
3939
import { extractCodeBlockLanguage } from '../../../../shared/markdown'
4040
import { extractAuthFollowUp } from '../../../../amazonq/util/authUtils'
4141
import { helpMessage } from '../../../../amazonq/webview/ui/texts/constants'
42-
import { ChatItemButton, ChatItemContent, ChatItemFormItem, MynahUIDataModel } from '@aws/mynah-ui'
42+
import { ChatItemButton, ChatItemContent, ChatItemFormItem, MynahIconsType, MynahUIDataModel } from '@aws/mynah-ui'
4343
import { ChatHistoryManager } from '../../../storages/chatHistory'
4444
import { ToolType, ToolUtils } from '../../../tools/toolUtils'
4545
import { ChatStream } from '../../../tools/chatStream'
46-
import path from 'path'
4746
import { getWorkspaceForFile } from '../../../../shared/utilities/workspaceUtils'
47+
import path from 'path'
4848

4949
export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help'
5050

@@ -222,7 +222,7 @@ export class Messenger {
222222
}
223223
const requiresAcceptance = ToolUtils.requiresAcceptance(tool)
224224
const chatStream = new ChatStream(this, tabID, triggerID, toolUse, requiresAcceptance)
225-
ToolUtils.queueDescription(tool, chatStream)
225+
await ToolUtils.queueDescription(tool, chatStream)
226226

227227
if (!requiresAcceptance) {
228228
// Need separate id for read tool and safe bash command execution as 'confirm-tool-use' id is required to change button status from `Confirm` to `Confirmed` state in cwChatConnector.ts which will impact generic tool execution.
@@ -440,31 +440,40 @@ export class Messenger {
440440
buttons.push({
441441
id: 'confirm-tool-use',
442442
text: 'Confirm',
443-
position: 'outside',
444443
status: 'info',
445444
})
446445
} else if (toolUse?.name === ToolType.FsWrite) {
447-
// FileList
448446
const absoluteFilePath = (toolUse?.input as any).path
449447
const projectPath = getWorkspaceForFile(absoluteFilePath)
450448
const relativePath = projectPath ? path.relative(projectPath, absoluteFilePath) : absoluteFilePath
449+
// FileList
451450
fileList = {
452-
fileTreeTitle: 'Code suggestions',
453-
rootFolderTitle: path.basename(projectPath ?? 'Default'),
451+
fileTreeTitle: '',
452+
hideFileCount: true,
454453
filePaths: [relativePath],
454+
details: {
455+
[relativePath]: {
456+
// eslint-disable-next-line unicorn/no-null
457+
icon: null,
458+
label: 'Created',
459+
changes: {
460+
added: 36,
461+
deleted: 0,
462+
total: 36,
463+
},
464+
},
465+
},
455466
}
456467
// Buttons
457468
buttons.push({
458469
id: 'reject-code-diff',
459-
text: 'Reject',
460-
position: 'outside',
461-
status: 'error',
470+
status: 'clear',
471+
icon: 'cancel' as MynahIconsType,
462472
})
463473
buttons.push({
464474
id: 'accept-code-diff',
465-
text: 'Accept',
466-
position: 'outside',
467-
status: 'success',
475+
status: 'clear',
476+
icon: 'ok' as MynahIconsType,
468477
})
469478
}
470479

@@ -482,8 +491,17 @@ export class Messenger {
482491
codeBlockLanguage: undefined,
483492
contextList: undefined,
484493
canBeVoted: false,
485-
buttons,
486-
fileList,
494+
buttons: toolUse?.name === ToolType.FsWrite ? undefined : buttons,
495+
fileList: undefined,
496+
fullWidth: toolUse?.name === ToolType.FsWrite,
497+
padding: !(toolUse?.name === ToolType.FsWrite),
498+
header:
499+
toolUse?.name === ToolType.FsWrite
500+
? { icon: 'code-block' as MynahIconsType, buttons: buttons, fileList: fileList }
501+
: undefined,
502+
codeBlockActions:
503+
// eslint-disable-next-line unicorn/no-null, prettier/prettier
504+
toolUse?.name === ToolType.FsWrite ? { 'insert-to-cursor': null, copy: null } : undefined,
487505
},
488506
tabID
489507
)

packages/core/src/codewhispererChat/tools/fsWrite.ts

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { getLogger } from '../../shared/logger/logger'
88
import vscode from 'vscode'
99
import { fs } from '../../shared/fs/fs'
1010
import { Writable } from 'stream'
11-
import path from 'path'
1211

1312
interface BaseParams {
1413
path: string
@@ -70,11 +69,116 @@ export class FsWrite {
7069
}
7170
}
7271

73-
public queueDescription(updates: Writable): void {
74-
const fileName = path.basename(this.params.path)
75-
updates.write(
76-
`Please see the generated code below for \`${fileName}\`. Click on the file to review the changes in the code editor and select Accept or Reject.`
77-
)
72+
private generateSmartDiff(oldStr: string, newStr: string): string {
73+
// Split both strings into arrays of lines
74+
const oldLines = oldStr.split('\n')
75+
const newLines = newStr.split('\n')
76+
let result = ''
77+
78+
// If strings are identical, return empty string
79+
if (oldStr === newStr) {
80+
return result
81+
}
82+
83+
let i = 0 // Index for oldLines
84+
let j = 0 // Index for newLines
85+
86+
// Loop through both arrays until we've processed all lines
87+
while (i < oldLines.length || j < newLines.length) {
88+
if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
89+
// Line is unchanged - prefix with space
90+
result += ` ${oldLines[i]}\n`
91+
i++
92+
j++
93+
} else {
94+
// Line is different
95+
if (i < oldLines.length) {
96+
// Remove line - prefix with minus
97+
result += `-${oldLines[i]}\n`
98+
i++
99+
}
100+
if (j < newLines.length) {
101+
// Add line - prefix with plus
102+
result += `+${newLines[j]}\n`
103+
j++
104+
}
105+
}
106+
}
107+
108+
return result
109+
}
110+
111+
private async getInsertContext(path: string, insertLine: number, newStr: string): Promise<string> {
112+
const fileContent = await fs.readFileText(path)
113+
const lines = fileContent.split('\n')
114+
const startLine = Math.max(0, insertLine - 2)
115+
const endLine = Math.min(lines.length, insertLine + 3)
116+
117+
const contextLines: string[] = []
118+
119+
// Add lines before insertion point
120+
for (let i = startLine; i < insertLine; i++) {
121+
contextLines.push(` ${lines[i]}`)
122+
}
123+
124+
// Add the new line with a '+' prefix
125+
contextLines.push(`+${newStr}`)
126+
127+
// Add lines after insertion point
128+
for (let i = insertLine; i < endLine; i++) {
129+
contextLines.push(` ${lines[i]}`)
130+
}
131+
132+
return contextLines.join('\n')
133+
}
134+
135+
private async handleAppendContent(sanitizedPath: string, newStr: string) {
136+
const fileContent = await fs.readFileText(sanitizedPath)
137+
const needsNewline = fileContent.length !== 0 && !fileContent.endsWith('\n')
138+
139+
let contentToAppend = newStr
140+
if (needsNewline) {
141+
contentToAppend = '\n' + contentToAppend
142+
}
143+
144+
// Get the last 3 lines from existing content
145+
const lines = fileContent.split('\n')
146+
const last3Lines = lines.slice(-3)
147+
148+
// Format the output with the last 3 lines and new content
149+
// const formattedOutput = [
150+
// ...last3Lines,
151+
// `+ ${contentToAppend.trim()}`, // Add '+' prefix to new content
152+
// ].join('\n')
153+
154+
return `${last3Lines.join('\n')}\n+ ${contentToAppend.trim()}` // [last3Lines, contentToAppend.trim()] // `${last3Lines.join('\n')}\n+ ${contentToAppend.trim()}`
155+
}
156+
157+
public async queueDescription(updates: Writable): Promise<void> {
158+
// const fileName = path.basename(this.params.path)
159+
switch (this.params.command) {
160+
case 'create':
161+
updates.write(`\`\`\`diff-typescript
162+
${'+' + this.params.fileText?.replace(/\n/g, '\n+')}
163+
`)
164+
break
165+
case 'strReplace':
166+
updates.write(`\`\`\`diff-typescript
167+
${this.generateSmartDiff(this.params.oldStr, this.params.newStr)}
168+
\`\`\`
169+
`)
170+
break
171+
case 'insert':
172+
updates.write(`\`\`\`diff-typescript
173+
${await this.getInsertContext(this.params.path, this.params.insertLine, this.params.newStr)}
174+
\`\`\``)
175+
break
176+
case 'append':
177+
updates.write(`\`\`\`diff-typescript
178+
${await this.handleAppendContent(this.params.path, this.params.newStr)}
179+
\`\`\``)
180+
break
181+
}
78182
updates.end()
79183
}
80184

@@ -138,20 +242,29 @@ export class FsWrite {
138242
await fs.writeFile(sanitizedPath, newContent)
139243
}
140244

141-
private async handleInsert(params: InsertParams, sanitizedPath: string): Promise<void> {
245+
private async getLineToInsert(
246+
sanitizedPath: string,
247+
insertLine: number,
248+
newStr: string
249+
): Promise<[number, string]> {
142250
const fileContent = await fs.readFileText(sanitizedPath)
143251
const lines = fileContent.split('\n')
144252

145253
const numLines = lines.length
146-
const insertLine = Math.max(0, Math.min(params.insertLine, numLines))
254+
const insertLineInFile = Math.max(0, Math.min(insertLine, numLines))
147255

148256
let newContent: string
149-
if (insertLine === 0) {
150-
newContent = params.newStr + '\n' + fileContent
257+
if (insertLineInFile === 0) {
258+
newContent = newStr + '\n' + fileContent
151259
} else {
152-
newContent = [...lines.slice(0, insertLine), params.newStr, ...lines.slice(insertLine)].join('\n')
260+
newContent = [...lines.slice(0, insertLineInFile), newStr, ...lines.slice(insertLineInFile)].join('\n')
153261
}
154262

263+
return [insertLineInFile, newContent]
264+
}
265+
266+
private async handleInsert(params: InsertParams, sanitizedPath: string): Promise<void> {
267+
const [, newContent] = await this.getLineToInsert(sanitizedPath, params.insertLine, params.newStr)
155268
await fs.writeFile(sanitizedPath, newContent)
156269
}
157270

packages/core/src/codewhispererChat/tools/toolUtils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,13 @@ export class ToolUtils {
6363
}
6464
}
6565

66-
static queueDescription(tool: Tool, updates: Writable): void {
66+
static async queueDescription(tool: Tool, updates: Writable): Promise<void> {
6767
switch (tool.type) {
6868
case ToolType.FsRead:
6969
tool.tool.queueDescription(updates)
7070
break
7171
case ToolType.FsWrite:
72-
tool.tool.queueDescription(updates)
72+
await tool.tool.queueDescription(updates)
7373
break
7474
case ToolType.ExecuteBash:
7575
tool.tool.queueDescription(updates)

0 commit comments

Comments
 (0)