Skip to content

Commit 6710e8a

Browse files
authored
feat(q): dynamic menu/tree nodes for extensions (#4713)
* feat(q): dynamic menu/tree nodes for extensions Problem: Tree/menu nodes were being re-used across extensions. This created issues for the underlying commands. For example, if toolkit wasn't installed then the switch to Q chat button would not work in Q's status bar menu. Solution: Move these nodes to a common module and allow dynamcic registration of the underylying commands. Note: Should make the isWeb mode check unnecessary, but this wasn't tested. * add warning, rename func
1 parent 2035e1f commit 6710e8a

File tree

8 files changed

+128
-87
lines changed

8 files changed

+128
-87
lines changed

packages/core/src/amazonq/explorer/amazonQChildrenNodes.ts

Lines changed: 6 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@
55

66
import * as vscode from 'vscode'
77
import * as nls from 'vscode-nls'
8-
import { Command, Commands, placeholder } from '../../shared/vscode/commands2'
9-
import { codicon, getIcon } from '../../shared/icons'
8+
import { Commands, placeholder } from '../../shared/vscode/commands2'
9+
import { getIcon } from '../../shared/icons'
1010
import { installAmazonQExtension, reconnect } from '../../codewhisperer/commands/basicCommands'
1111
import { amazonQHelpUrl } from '../../shared/constants'
1212
import { cwTreeNodeSource } from '../../codewhisperer/commands/types'
13-
import { telemetry } from '../../shared/telemetry/telemetry'
14-
import { DataQuickPickItem } from '../../shared/ui/pickerPrompter'
15-
import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
1613
import { VSCODE_EXTENSION_ID } from '../../shared/extensions'
1714
import { globals } from '../../shared'
1815
import { amazonQDismissedKey } from '../../codewhisperer/models/constants'
16+
import { _switchToAmazonQ } from './commonNodes'
1917

2018
const localize = nls.loadMessageBundle()
2119

@@ -31,6 +29,9 @@ export const dismissQTree = Commands.declare('aws.toolkit.amazonq.dismiss', () =
3129
await globals.context.globalState.update(amazonQDismissedKey, true)
3230
await vscode.commands.executeCommand('setContext', amazonQDismissedKey, true)
3331
})
32+
33+
export const toolkitSwitchToAmazonQCommand = Commands.declare('_aws.toolkit.amazonq.focusView', () => _switchToAmazonQ)
34+
3435
// Learn more button of Amazon Q now opens the Amazon Q marketplace page.
3536
export const createLearnMoreNode = () =>
3637
qExtensionPageCommand.build().asTreeNode({
@@ -39,69 +40,6 @@ export const createLearnMoreNode = () =>
3940
contextValue: 'awsAmazonQLearnMoreNode',
4041
})
4142

42-
export const switchToAmazonQCommand = Commands.declare(
43-
'_aws.amazonq.focusView',
44-
() =>
45-
async (signIn: boolean = false) => {
46-
telemetry.ui_click.emit({
47-
elementId: 'amazonq_switchToQChat',
48-
passive: false,
49-
})
50-
if (signIn) {
51-
await vscode.commands.executeCommand('setContext', 'aws.amazonq.showLoginView', true)
52-
}
53-
54-
// Attempt to show both, in case something is wrong with the state of the tree. Only the active
55-
// one will be shown. This way, even if the state of the tree is broken, the buttons still take
56-
// you to Amazon Q.
57-
await vscode.commands.executeCommand('aws.AmazonQChatView.focus')
58-
await vscode.commands.executeCommand('aws.amazonq.AmazonCommonAuth.focus')
59-
}
60-
)
61-
62-
export function switchToAmazonQNode(type: 'item'): DataQuickPickItem<'openChatPanel'>
63-
export function switchToAmazonQNode(type: 'tree'): TreeNode<Command>
64-
export function switchToAmazonQNode(type: 'item' | 'tree'): DataQuickPickItem<'openChatPanel'> | TreeNode<Command>
65-
export function switchToAmazonQNode(type: 'item' | 'tree'): any {
66-
switch (type) {
67-
case 'tree':
68-
return switchToAmazonQCommand.build().asTreeNode({
69-
label: 'Open Chat Panel',
70-
iconPath: getIcon('vscode-comment'),
71-
contextValue: 'awsToAmazonQChatNode',
72-
})
73-
case 'item':
74-
return {
75-
data: 'openChatPanel',
76-
label: 'Open Chat Panel',
77-
iconPath: getIcon('vscode-comment'),
78-
onClick: () => switchToAmazonQCommand.execute(),
79-
} as DataQuickPickItem<'openChatPanel'>
80-
}
81-
}
82-
83-
export function createSignIn(type: 'item'): DataQuickPickItem<'signIn'>
84-
export function createSignIn(type: 'tree'): TreeNode<Command>
85-
export function createSignIn(type: 'item' | 'tree'): DataQuickPickItem<'signIn'> | TreeNode<Command>
86-
export function createSignIn(type: 'item' | 'tree'): any {
87-
const label = localize('AWS.codewhisperer.signInNode.label', 'Sign in to get started')
88-
const icon = getIcon('vscode-account')
89-
90-
switch (type) {
91-
case 'tree':
92-
return switchToAmazonQCommand.build(true).asTreeNode({
93-
label: label,
94-
iconPath: icon,
95-
})
96-
case 'item':
97-
return {
98-
data: 'signIn',
99-
label: codicon`${icon} ${label}`,
100-
onClick: () => switchToAmazonQCommand.execute(true),
101-
} as DataQuickPickItem<'signIn'>
102-
}
103-
}
104-
10543
export function createInstallQNode() {
10644
return installAmazonQExtension.build().asTreeNode({
10745
label: 'Install the Amazon Q Extension', // TODO: localize

packages/core/src/amazonq/explorer/amazonQTreeNode.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ import { ResourceTreeDataProvider, TreeNode } from '../../shared/treeview/resour
99
import { AuthState, AuthUtil, isPreviousQUser } from '../../codewhisperer/util/authUtil'
1010
import {
1111
createLearnMoreNode,
12-
switchToAmazonQNode,
1312
createInstallQNode,
1413
createDismissNode,
15-
createSignIn,
14+
toolkitSwitchToAmazonQCommand,
1615
} from './amazonQChildrenNodes'
1716
import { Command, Commands } from '../../shared/vscode/commands2'
1817
import { listCodeWhispererCommands } from '../../codewhisperer/ui/statusBarMenu'
@@ -21,6 +20,7 @@ import { vsCodeState } from '../../codewhisperer/models/model'
2120
import { isExtensionActive, isExtensionInstalled } from '../../shared/utilities/vsCodeUtils'
2221
import { VSCODE_EXTENSION_ID } from '../../shared/extensions'
2322
import { getLogger } from '../../shared/logger'
23+
import { createSignIn, switchToAmazonQNode } from './commonNodes'
2424

2525
export class AmazonQNode implements TreeNode {
2626
public readonly id = 'amazonq'
@@ -89,11 +89,13 @@ export class AmazonQNode implements TreeNode {
8989
}
9090

9191
if (AmazonQNode.amazonQState !== 'connected') {
92-
return [createSignIn('tree'), createLearnMoreNode()]
92+
return [createSignIn('tree', toolkitSwitchToAmazonQCommand), createLearnMoreNode()]
9393
}
9494

9595
return [
96-
vsCodeState.isFreeTierLimitReached ? createFreeTierLimitMet('tree') : switchToAmazonQNode('tree'),
96+
vsCodeState.isFreeTierLimitReached
97+
? createFreeTierLimitMet('tree')
98+
: switchToAmazonQNode('tree', toolkitSwitchToAmazonQCommand),
9799
createNewMenuButton(),
98100
]
99101
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
import * as nls from 'vscode-nls'
8+
import { Command, DeclaredCommand } from '../../shared/vscode/commands2'
9+
import { codicon, getIcon } from '../../shared/icons'
10+
import { telemetry } from '../../shared/telemetry/telemetry'
11+
import { DataQuickPickItem } from '../../shared/ui/pickerPrompter'
12+
import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
13+
14+
const localize = nls.loadMessageBundle()
15+
16+
/**
17+
* Do not call this function directly, use the necessary equivalent command registered by the extensions:
18+
* - switchToAmazonQCommand, _aws.amazonq.focusView (for Amazon Q code)
19+
* - toolkitSwitchToAmazonQCommand, _aws.toolkit.amazonq.focusView (for Toolkit code)
20+
*/
21+
export async function _switchToAmazonQ(signIn: boolean = false) {
22+
if (signIn) {
23+
await vscode.commands.executeCommand('setContext', 'aws.amazonq.showLoginView', true)
24+
} else {
25+
telemetry.ui_click.emit({
26+
elementId: 'amazonq_switchToQChat',
27+
passive: false,
28+
})
29+
}
30+
31+
// Attempt to show both, in case something is wrong with the state of the webviews.
32+
// Only the active one will be shown.
33+
await vscode.commands.executeCommand('aws.AmazonQChatView.focus')
34+
await vscode.commands.executeCommand('aws.amazonq.AmazonCommonAuth.focus')
35+
}
36+
37+
/**
38+
* Common nodes that can be used by mutliple UIs, e.g. status bar menu, explorer tree, etc.
39+
* Individual extensions may register their own commands for the nodes, so it must be passed in.
40+
*
41+
* TODO: If the Amazon Q explorer tree is removed, we should remove support for multiple commands
42+
* and only use the one registered in Amazon Q.
43+
*/
44+
45+
export function switchToAmazonQNode(
46+
type: 'item',
47+
cmd: DeclaredCommand<typeof _switchToAmazonQ>
48+
): DataQuickPickItem<'openChatPanel'>
49+
export function switchToAmazonQNode(type: 'tree', cmd: DeclaredCommand<typeof _switchToAmazonQ>): TreeNode<Command>
50+
export function switchToAmazonQNode(
51+
type: 'item' | 'tree',
52+
cmd: DeclaredCommand<typeof _switchToAmazonQ>
53+
): DataQuickPickItem<'openChatPanel'> | TreeNode<Command>
54+
export function switchToAmazonQNode(type: 'item' | 'tree', cmd: DeclaredCommand<typeof _switchToAmazonQ>): any {
55+
switch (type) {
56+
case 'tree':
57+
return cmd.build().asTreeNode({
58+
label: 'Open Chat Panel',
59+
iconPath: getIcon('vscode-comment'),
60+
contextValue: 'awsToAmazonQChatNode',
61+
})
62+
case 'item':
63+
return {
64+
data: 'openChatPanel',
65+
label: 'Open Chat Panel',
66+
iconPath: getIcon('vscode-comment'),
67+
onClick: () => cmd.execute(),
68+
} as DataQuickPickItem<'openChatPanel'>
69+
}
70+
}
71+
72+
export function createSignIn(type: 'item', cmd: DeclaredCommand<typeof _switchToAmazonQ>): DataQuickPickItem<'signIn'>
73+
export function createSignIn(type: 'tree', cmd: DeclaredCommand<typeof _switchToAmazonQ>): TreeNode<Command>
74+
export function createSignIn(
75+
type: 'item' | 'tree',
76+
cmd: DeclaredCommand<typeof _switchToAmazonQ>
77+
): DataQuickPickItem<'signIn'> | TreeNode<Command>
78+
export function createSignIn(type: 'item' | 'tree', cmd: DeclaredCommand<typeof _switchToAmazonQ>): any {
79+
const label = localize('AWS.codewhisperer.signInNode.label', 'Sign in to get started')
80+
const icon = getIcon('vscode-account')
81+
82+
switch (type) {
83+
case 'tree':
84+
return cmd.build(true).asTreeNode({
85+
label: label,
86+
iconPath: icon,
87+
})
88+
case 'item':
89+
return {
90+
data: 'signIn',
91+
label: codicon`${icon} ${label}`,
92+
onClick: () => cmd.execute(true),
93+
} as DataQuickPickItem<'signIn'>
94+
}
95+
}

packages/core/src/codewhisperer/activation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActi
6363
import { listCodeWhispererCommands } from './ui/statusBarMenu'
6464
import { updateUserProxyUrl } from './client/agent'
6565
import { Container } from './service/serviceContainer'
66+
import { switchToAmazonQCommand } from './ui/codeWhispererNodes'
6667

6768
export async function activate(context: ExtContext): Promise<void> {
6869
const codewhispererSettings = CodeWhispererSettings.instance
@@ -210,6 +211,8 @@ export async function activate(context: ExtContext): Promise<void> {
210211
applySecurityFix.register(),
211212
// quick pick with codewhisperer options
212213
listCodeWhispererCommands.register(),
214+
// switch to Q node for status bar menu
215+
switchToAmazonQCommand.register(),
213216
// manual trigger
214217
Commands.register({ id: 'aws.codeWhisperer', autoconnect: true }, async () => {
215218
invokeRecommendation(

packages/core/src/codewhisperer/ui/codeWhispererNodes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import { cwQuickPickSource, cwTreeNodeSource } from '../commands/types'
2626
import { AuthUtil } from '../util/authUtil'
2727
import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
2828
import { submitFeedback } from '../../feedback/vue/submitFeedback'
29+
import { _switchToAmazonQ } from '../../amazonq/explorer/commonNodes'
30+
31+
export const switchToAmazonQCommand = Commands.declare('_aws.amazonq.focusView', () => _switchToAmazonQ)
2932

3033
export function createAutoSuggestions(pause: boolean): DataQuickPickItem<'autoSuggestions'> {
3134
const labelResume = localize('AWS.codewhisperer.resumeCodeWhispererNode.label', 'Resume Auto-Suggestions')

packages/core/src/codewhisperer/ui/statusBarMenu.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,28 @@ import {
1818
createFeedbackNode,
1919
createGitHubNode,
2020
createDocumentationNode,
21+
switchToAmazonQCommand,
2122
} from './codeWhispererNodes'
2223
import { hasVendedIamCredentials } from '../../auth/auth'
2324
import { AuthUtil } from '../util/authUtil'
2425
import { DataQuickPickItem, createQuickPick } from '../../shared/ui/pickerPrompter'
2526
import { CodeSuggestionsState, vsCodeState } from '../models/model'
2627
import { Commands } from '../../shared/vscode/commands2'
2728
import { createExitButton } from '../../shared/ui/buttons'
28-
import { isWeb } from '../../common/webUtils'
2929
import { telemetry } from '../../shared/telemetry/telemetry'
3030
import { once } from '../../shared/utilities/functionUtils'
3131
import { getLogger } from '../../shared/logger'
32+
import { createSignIn, switchToAmazonQNode } from '../../amazonq/explorer/commonNodes'
3233

3334
function getAmazonQCodeWhispererNodes() {
34-
// TODO: Remove when web is supported for amazonq
35-
let amazonq
36-
if (!isWeb()) {
37-
amazonq = require('../../amazonq/explorer/amazonQChildrenNodes')
38-
}
39-
4035
const autoTriggerEnabled = CodeSuggestionsState.instance.isSuggestionsEnabled()
4136

4237
if (AuthUtil.instance.isConnectionExpired()) {
4338
return [createReconnect('item'), createLearnMore()]
4439
}
4540

4641
if (!AuthUtil.instance.isConnected()) {
47-
return [amazonq.createSignIn('item'), createLearnMore()]
42+
return [createSignIn('item', switchToAmazonQCommand), createLearnMore()]
4843
}
4944

5045
if (vsCodeState.isFreeTierLimitReached) {
@@ -75,7 +70,7 @@ function getAmazonQCodeWhispererNodes() {
7570

7671
// Amazon Q + others
7772
createSeparator('Other Features'),
78-
...(amazonq ? [amazonq.switchToAmazonQNode('item')] : []),
73+
switchToAmazonQNode('item', switchToAmazonQCommand),
7974
createSecurityScan(),
8075
]
8176
}

packages/core/src/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import {
5454
learnMoreAmazonQCommand,
5555
qExtensionPageCommand,
5656
dismissQTree,
57-
switchToAmazonQCommand,
57+
toolkitSwitchToAmazonQCommand,
5858
} from './amazonq/explorer/amazonQChildrenNodes'
5959
import { AuthUtil, isPreviousQUser } from './codewhisperer/util/authUtil'
6060
import { installAmazonQExtension } from './codewhisperer/commands/basicCommands'
@@ -168,7 +168,7 @@ export async function activate(context: vscode.ExtensionContext) {
168168
learnMoreAmazonQCommand.register()
169169
qExtensionPageCommand.register()
170170
dismissQTree.register()
171-
switchToAmazonQCommand.register()
171+
toolkitSwitchToAmazonQCommand.register()
172172
installAmazonQExtension.register()
173173

174174
if (!isExtensionInstalled(VSCODE_EXTENSION_ID.amazonq)) {

packages/core/src/test/codewhisperer/commands/basicCommands.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ import { waitUntil } from '../../../shared/utilities/timeoutUtils'
4949
import { listCodeWhispererCommands } from '../../../codewhisperer/ui/statusBarMenu'
5050
import { CodeSuggestionsState } from '../../../codewhisperer/models/model'
5151
import { cwQuickPickSource } from '../../../codewhisperer/commands/types'
52-
import { switchToAmazonQNode, createSignIn } from '../../../amazonq/explorer/amazonQChildrenNodes'
52+
import { toolkitSwitchToAmazonQCommand } from '../../../amazonq/explorer/amazonQChildrenNodes'
5353
import { isTextEditor } from '../../../shared/utilities/editorUtilities'
5454
import { refreshStatusBar } from '../../../codewhisperer/service/inlineCompletionService'
55+
import { createSignIn, switchToAmazonQNode } from '../../../amazonq/explorer/commonNodes'
5556

5657
describe('CodeWhisperer-basicCommands', function () {
5758
let targetCommand: Command<any> & vscode.Disposable
@@ -316,7 +317,11 @@ describe('CodeWhisperer-basicCommands', function () {
316317
sinon.stub(AuthUtil.instance, 'isConnected').returns(false)
317318

318319
getTestWindow().onDidShowQuickPick(e => {
319-
e.assertContainsItems(createSignIn('item'), createLearnMore(), ...genericItems())
320+
e.assertContainsItems(
321+
createSignIn('item', toolkitSwitchToAmazonQCommand),
322+
createLearnMore(),
323+
...genericItems()
324+
)
320325
e.dispose() // skip needing to select an item to continue
321326
})
322327

@@ -343,7 +348,7 @@ describe('CodeWhisperer-basicCommands', function () {
343348
createAutoSuggestions(false),
344349
createOpenReferenceLog(),
345350
createGettingStarted(),
346-
switchToAmazonQNode('item'),
351+
switchToAmazonQNode('item', toolkitSwitchToAmazonQCommand),
347352
createSecurityScan(),
348353
...genericItems(),
349354
createSettingsNode(),
@@ -366,7 +371,7 @@ describe('CodeWhisperer-basicCommands', function () {
366371
createSelectCustomization(),
367372
createOpenReferenceLog(),
368373
createGettingStarted(),
369-
switchToAmazonQNode('item'),
374+
switchToAmazonQNode('item', toolkitSwitchToAmazonQCommand),
370375
createSecurityScan(),
371376
...genericItems(),
372377
createSettingsNode(),

0 commit comments

Comments
 (0)