diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0e23a20d71d..30d482e2886 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -42,8 +42,11 @@ Then clone the repository and install NPM packages:
### Run
-Due to the monorepo structure of the project, you must have the `aws-toolkit-vscode/packages/toolkit` folder open as root folder in the workspace.
-The easiest way to open the project: File > Open Workspace from File > choose `aws-toolkit-vscode/aws-toolkit-vscode.code-workspace`
+Due to the monorepo structure of the project, you must open the project using the
+`aws-toolkit-vscode.code-workspace` project file.
+
+1. Run the `File: Open Workspace from File...` command in vscode.
+2. Select the `aws-toolkit-vscode.code-workspace` project file.
To run the extension from VSCode as a Node.js app:
@@ -164,8 +167,7 @@ See [web.md](./docs/web.md) for working with the web mode implementation of the
See [TESTPLAN.md](./docs/TESTPLAN.md) to understand the project's test
structure, mechanics and philosophy.
-You can run tests directly from VSCode. Due to the monorepo structure of the project, you must have the `aws-toolkit-vscode/packages/toolkit` folder open as root folder in the workspace.
-The easiest way to open the project: File > Open Workspace from File > choose `aws-toolkit-vscode/aws-toolkit-vscode.code-workspace`
+You can run tests directly from VSCode. Due to the monorepo structure of the project, you must [open the project via the `aws-toolkit-vscode.code-workspace` project file](#run).
1. Select `View > Debug`, or select the Debug pane from the sidebar.
2. From the dropdown at the top of the Debug pane, select the `Extension Tests` configuration.
@@ -180,12 +182,13 @@ Tests will write logs to `./.test-reports/testLog.log`.
#### Run a specific test
-To run a single test in VSCode, do any one of:
+To run a single test in VSCode, do any _one_ of the following:
- Run the _Extension Tests (current file)_ launch-config.
-- Use Mocha's [it.only()](https://mochajs.org/#exclusive-tests) or `describe.only()`.
-- Run in your terminal:
-
+ - Note: if you don't see this in the vscode debug menu, confirm that you opened the project
+ [via the `aws-toolkit-vscode.code-workspace` project file](#run).
+- or... Use Mocha's [it.only()](https://mochajs.org/#exclusive-tests) or `describe.only()`.
+- or... Run in your terminal:
- Unix/macOS/POSIX shell:
```
TEST_FILE=../core/src/test/foo.test.ts npm run test
@@ -194,8 +197,7 @@ To run a single test in VSCode, do any one of:
```
$Env:TEST_FILE = "../core/src/test/foo.test.ts"; npm run test
```
-
-- To run all tests in a particular subdirectory, you can edit
+- or... To run all tests in a particular subdirectory, you can edit
`src/test/index.ts:rootTestsPath` to point to a subdirectory:
```
rootTestsPath: __dirname + '/shared/sam/debugger/'
diff --git a/package-lock.json b/package-lock.json
index fa850837f8f..04f05d4c886 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24995,8 +24995,9 @@
},
"node_modules/ts-node": {
"version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -26691,7 +26692,7 @@
},
"packages/amazonq": {
"name": "amazon-q-vscode",
- "version": "1.53.0-SNAPSHOT",
+ "version": "1.54.0-SNAPSHOT",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
@@ -27493,7 +27494,7 @@
},
"packages/toolkit": {
"name": "aws-toolkit-vscode",
- "version": "3.52.0-SNAPSHOT",
+ "version": "3.53.0-SNAPSHOT",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/amazonq/.changes/1.53.0.json b/packages/amazonq/.changes/1.53.0.json
new file mode 100644
index 00000000000..cb548513e35
--- /dev/null
+++ b/packages/amazonq/.changes/1.53.0.json
@@ -0,0 +1,22 @@
+{
+ "date": "2025-03-28",
+ "version": "1.53.0",
+ "entries": [
+ {
+ "type": "Bug Fix",
+ "description": "Amazon Q Chat: Choosing a nested subfolder for `/doc` on Windows results in `The folder you chose did not contain any source files` error"
+ },
+ {
+ "type": "Feature",
+ "description": "Add support for Code search in Q chat"
+ },
+ {
+ "type": "Feature",
+ "description": "(Experimental) Amazon Q inline code suggestions via Amazon Q Language Server. (enable with `aws.experiments.amazonqLSP: true`)"
+ },
+ {
+ "type": "Feature",
+ "description": "Command Palette: Add `Amazon Q: Open Chat` command."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-3f6a2026-9dd8-4b50-8a73-765ead8717b4.json b/packages/amazonq/.changes/next-release/Bug Fix-3f6a2026-9dd8-4b50-8a73-765ead8717b4.json
deleted file mode 100644
index 81b888ea005..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-3f6a2026-9dd8-4b50-8a73-765ead8717b4.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Amazon Q Chat: Choosing a nested subfolder for `/doc` on Windows results in `The folder you chose did not contain any source files` error"
-}
diff --git a/packages/amazonq/.changes/next-release/Feature-5f829ea1-e33b-4687-bc6d-76ed1558cd7e.json b/packages/amazonq/.changes/next-release/Feature-5f829ea1-e33b-4687-bc6d-76ed1558cd7e.json
deleted file mode 100644
index 52370b566ce..00000000000
--- a/packages/amazonq/.changes/next-release/Feature-5f829ea1-e33b-4687-bc6d-76ed1558cd7e.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Feature",
- "description": "Add support for Code search in Q chat"
-}
diff --git a/packages/amazonq/.changes/next-release/Feature-7a3c930e-cc8e-4323-9ecd-c1bf767fd2ed.json b/packages/amazonq/.changes/next-release/Feature-7a3c930e-cc8e-4323-9ecd-c1bf767fd2ed.json
deleted file mode 100644
index 81a1e026db4..00000000000
--- a/packages/amazonq/.changes/next-release/Feature-7a3c930e-cc8e-4323-9ecd-c1bf767fd2ed.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Feature",
- "description": "(Experimental) Amazon Q inline code suggestions via Amazon Q Language Server. (enable with `aws.experiments.amazonqLSP: true`)"
-}
diff --git a/packages/amazonq/.changes/next-release/Feature-fa845e78-1e76-407a-be8a-e64c63b98795.json b/packages/amazonq/.changes/next-release/Feature-fa845e78-1e76-407a-be8a-e64c63b98795.json
deleted file mode 100644
index b3d28917f4f..00000000000
--- a/packages/amazonq/.changes/next-release/Feature-fa845e78-1e76-407a-be8a-e64c63b98795.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Feature",
- "description": "Command Palette: Add `Amazon Q: Open Chat` command."
-}
diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md
index f938e36fbd6..c799cfbc69b 100644
--- a/packages/amazonq/CHANGELOG.md
+++ b/packages/amazonq/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 1.53.0 2025-03-28
+
+- **Bug Fix** Amazon Q Chat: Choosing a nested subfolder for `/doc` on Windows results in `The folder you chose did not contain any source files` error
+- **Feature** Add support for Code search in Q chat
+- **Feature** (Experimental) Amazon Q inline code suggestions via Amazon Q Language Server. (enable with `aws.experiments.amazonqLSP: true`)
+- **Feature** Command Palette: Add `Amazon Q: Open Chat` command.
+
## 1.52.0 2025-03-20
- **Bug Fix** Amazon Q chat: @Folders and @Files are missing `@` prefix in chat history
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index 8789002aba3..c9844eb6eb8 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -2,7 +2,7 @@
"name": "amazon-q-vscode",
"displayName": "Amazon Q",
"description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI",
- "version": "1.53.0-SNAPSHOT",
+ "version": "1.54.0-SNAPSHOT",
"extensionKind": [
"workspace"
],
diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json
index 986b4d465b0..93b857f6ac0 100644
--- a/packages/core/src/codewhisperer/client/user-service-2.json
+++ b/packages/core/src/codewhisperer/client/user-service-2.json
@@ -280,6 +280,22 @@
],
"documentation": "
Lists the findings from a particular code analysis job.
"
},
+ "ListEvents": {
+ "name": "ListEvents",
+ "http": {
+ "method": "POST",
+ "requestUri": "/"
+ },
+ "input": { "shape": "ListEventsRequest" },
+ "output": { "shape": "ListEventsResponse" },
+ "errors": [
+ { "shape": "ThrottlingException" },
+ { "shape": "InternalServerException" },
+ { "shape": "ValidationException" },
+ { "shape": "AccessDeniedException" }
+ ],
+ "documentation": "List events for agent activity
"
+ },
"ListFeatureEvaluations": {
"name": "ListFeatureEvaluations",
"http": {
@@ -689,7 +705,9 @@
"acceptedLineCount": { "shape": "Integer" },
"acceptedSnippetHasReference": { "shape": "Boolean" },
"hasProjectLevelContext": { "shape": "Boolean" },
- "userIntent": { "shape": "UserIntent" }
+ "userIntent": { "shape": "UserIntent" },
+ "addedIdeDiagnostics": { "shape": "IdeDiagnosticList" },
+ "removedIdeDiagnostics": { "shape": "IdeDiagnosticList" }
}
},
"ChatInteractWithMessageEventInteractionTargetString": {
@@ -1079,7 +1097,8 @@
"CreateWorkspaceRequestWorkspaceRootString": {
"type": "string",
"max": 1024,
- "min": 1
+ "min": 1,
+ "sensitive": true
},
"CreateWorkspaceResponse": {
"type": "structure",
@@ -1394,6 +1413,10 @@
"useRelevantDocuments": {
"shape": "Boolean",
"documentation": "Whether service should use relevant document in prompt
"
+ },
+ "workspaceFolders": {
+ "shape": "WorkspaceFolderList",
+ "documentation": "Represents IDE provided list of workspace folders
"
}
},
"documentation": "Represents the state of an Editor
"
@@ -1476,6 +1499,34 @@
"max": 2048,
"min": 0
},
+ "Event": {
+ "type": "structure",
+ "required": ["eventId", "generationId", "eventTimestamp", "eventType", "eventBlob"],
+ "members": {
+ "eventId": { "shape": "UUID" },
+ "generationId": { "shape": "UUID" },
+ "eventTimestamp": { "shape": "SyntheticTimestamp_date_time" },
+ "eventType": { "shape": "EventType" },
+ "eventBlob": { "shape": "EventBlob" }
+ }
+ },
+ "EventBlob": {
+ "type": "blob",
+ "max": 400000,
+ "min": 1,
+ "sensitive": true
+ },
+ "EventList": {
+ "type": "list",
+ "member": { "shape": "Event" },
+ "max": 10,
+ "min": 1
+ },
+ "EventType": {
+ "type": "string",
+ "max": 100,
+ "min": 1
+ },
"ExternalIdentityDetails": {
"type": "structure",
"members": {
@@ -1629,7 +1680,8 @@
"TASK_ASSIST",
"TRANSFORMATIONS",
"CHAT_CUSTOMIZATION",
- "TRANSFORMATIONS_WEBAPP"
+ "TRANSFORMATIONS_WEBAPP",
+ "FEATURE_DEVELOPMENT"
],
"max": 64,
"min": 1
@@ -1807,6 +1859,46 @@
"max": 64,
"min": 1
},
+ "IdeDiagnostic": {
+ "type": "structure",
+ "required": ["ideDiagnosticType"],
+ "members": {
+ "range": {
+ "shape": "Range",
+ "documentation": "The range at which the message applies.
"
+ },
+ "source": {
+ "shape": "IdeDiagnosticSourceString",
+ "documentation": "A human-readable string describing the source of the diagnostic
"
+ },
+ "severity": {
+ "shape": "DiagnosticSeverity",
+ "documentation": "Diagnostic Error type
"
+ },
+ "ideDiagnosticType": {
+ "shape": "IdeDiagnosticType",
+ "documentation": "Type of the diagnostic
"
+ }
+ },
+ "documentation": "Structure to represent metadata about a Diagnostic from user local IDE
"
+ },
+ "IdeDiagnosticList": {
+ "type": "list",
+ "member": { "shape": "IdeDiagnostic" },
+ "documentation": "List of IDE Diagnostics
",
+ "max": 1024,
+ "min": 0
+ },
+ "IdeDiagnosticSourceString": {
+ "type": "string",
+ "max": 1024,
+ "min": 0,
+ "sensitive": true
+ },
+ "IdeDiagnosticType": {
+ "type": "string",
+ "enum": ["SYNTAX_ERROR", "TYPE_ERROR", "REFERENCE_ERROR", "BEST_PRACTICE", "SECURITY", "OTHER"]
+ },
"IdempotencyToken": {
"type": "string",
"max": 256,
@@ -1995,6 +2087,30 @@
"codeAnalysisFindings": { "shape": "SensitiveString" }
}
},
+ "ListEventsRequest": {
+ "type": "structure",
+ "required": ["conversationId"],
+ "members": {
+ "conversationId": { "shape": "UUID" },
+ "maxResults": { "shape": "ListEventsRequestMaxResultsInteger" },
+ "nextToken": { "shape": "NextToken" }
+ }
+ },
+ "ListEventsRequestMaxResultsInteger": {
+ "type": "integer",
+ "box": true,
+ "max": 50,
+ "min": 1
+ },
+ "ListEventsResponse": {
+ "type": "structure",
+ "required": ["conversationId", "events"],
+ "members": {
+ "conversationId": { "shape": "UUID" },
+ "events": { "shape": "EventList" },
+ "nextToken": { "shape": "NextToken" }
+ }
+ },
"ListFeatureEvaluationsRequest": {
"type": "structure",
"required": ["userContext"],
@@ -2023,7 +2139,8 @@
"ListWorkspaceMetadataRequestWorkspaceRootString": {
"type": "string",
"max": 1024,
- "min": 1
+ "min": 1,
+ "sensitive": true
},
"ListWorkspaceMetadataResponse": {
"type": "structure",
@@ -2066,6 +2183,11 @@
"min": 1,
"pattern": "[-a-zA-Z0-9._]*"
},
+ "NextToken": {
+ "type": "string",
+ "max": 1000,
+ "min": 0
+ },
"Notifications": {
"type": "list",
"member": { "shape": "NotificationsFeature" },
@@ -2893,6 +3015,10 @@
"type": "string",
"enum": ["DECLARATION", "USAGE"]
},
+ "SyntheticTimestamp_date_time": {
+ "type": "timestamp",
+ "timestampFormat": "iso8601"
+ },
"TargetCode": {
"type": "structure",
"required": ["relativeTargetPath"],
@@ -3752,7 +3878,9 @@
"generatedLine": { "shape": "PrimitiveInteger" },
"numberOfRecommendations": { "shape": "PrimitiveInteger" },
"perceivedLatencyMilliseconds": { "shape": "Double" },
- "acceptedCharacterCount": { "shape": "PrimitiveInteger" }
+ "acceptedCharacterCount": { "shape": "PrimitiveInteger" },
+ "addedIdeDiagnostics": { "shape": "IdeDiagnosticList" },
+ "removedIdeDiagnostics": { "shape": "IdeDiagnosticList" }
}
},
"ValidationException": {
@@ -3786,6 +3914,18 @@
"programmingLanguage": { "shape": "ProgrammingLanguage" }
}
},
+ "WorkspaceFolderList": {
+ "type": "list",
+ "member": { "shape": "WorkspaceFolderListMemberString" },
+ "max": 100,
+ "min": 0
+ },
+ "WorkspaceFolderListMemberString": {
+ "type": "string",
+ "max": 4096,
+ "min": 1,
+ "sensitive": true
+ },
"WorkspaceList": {
"type": "list",
"member": { "shape": "WorkspaceMetadata" }
diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
index ec1e383bde5..ee21f252ffb 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts
@@ -305,7 +305,20 @@ export class ChatController {
}
private processResponseBodyLinkClick(click: ResponseBodyLinkClickMessage) {
- this.openLinkInExternalBrowser(click)
+ const uri = vscode.Uri.parse(click.link)
+ if (uri.scheme === 'file') {
+ void this.openFile(uri.fsPath)
+ } else {
+ this.openLinkInExternalBrowser(click)
+ }
+ }
+
+ private async openFile(absolutePath: string) {
+ const fileExists = await fs.existsFile(absolutePath)
+ if (fileExists) {
+ const document = await vscode.workspace.openTextDocument(absolutePath)
+ await vscode.window.showTextDocument(document)
+ }
}
private processSourceLinkClick(click: SourceLinkClickMessage) {
diff --git a/packages/core/src/codewhispererChat/tools/fsRead.ts b/packages/core/src/codewhispererChat/tools/fsRead.ts
index a078347dc3d..7ff57935aad 100644
--- a/packages/core/src/codewhispererChat/tools/fsRead.ts
+++ b/packages/core/src/codewhispererChat/tools/fsRead.ts
@@ -4,7 +4,6 @@
*/
import * as vscode from 'vscode'
import { getLogger } from '../../shared/logger/logger'
-import { readDirectoryRecursively } from '../../shared/utilities/workspaceUtils'
import fs from '../../shared/fs/fs'
import { InvokeOutput, maxToolResponseSize, OutputKind, sanitizePath } from './toolShared'
import { Writable } from 'stream'
@@ -18,7 +17,6 @@ export interface FsReadParams {
export class FsRead {
private fsPath: string
private readonly readRange?: number[]
- private isFile?: boolean // true for file, false for directory
private readonly logger = getLogger('fsRead')
constructor(params: FsReadParams) {
@@ -36,24 +34,37 @@ export class FsRead {
this.fsPath = sanitized
const fileUri = vscode.Uri.file(this.fsPath)
- let exists: boolean
+ let fileExists: boolean
try {
- exists = await fs.exists(fileUri)
- if (!exists) {
+ fileExists = await fs.existsFile(fileUri)
+ if (!fileExists) {
throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed.`)
}
} catch (err) {
throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed. (${err})`)
}
- this.isFile = await fs.existsFile(fileUri)
this.logger.debug(`Validation succeeded for path: ${this.fsPath}`)
}
public queueDescription(updates: Writable): void {
const fileName = path.basename(this.fsPath)
const fileUri = vscode.Uri.file(this.fsPath)
- updates.write(`Reading: [${fileName}](${fileUri})`)
+ updates.write(`Reading file: [${fileName}](${fileUri}), `)
+
+ const [start, end] = this.readRange ?? []
+
+ if (start && end) {
+ updates.write(`from line ${start} to ${end}`)
+ } else if (start) {
+ if (start > 0) {
+ updates.write(`from line ${start} to end of file`)
+ } else {
+ updates.write(`${start} line from the end of file to end of file`)
+ }
+ } else {
+ updates.write('all lines')
+ }
updates.end()
}
@@ -61,20 +72,12 @@ export class FsRead {
try {
const fileUri = vscode.Uri.file(this.fsPath)
- if (this.isFile) {
- const fileContents = await this.readFile(fileUri)
- this.logger.info(`Read file: ${this.fsPath}, size: ${fileContents.length}`)
- return this.handleFileRange(fileContents)
- } else if (!this.isFile) {
- const maxDepth = this.getDirectoryDepth() ?? 0
- const listing = await readDirectoryRecursively(fileUri, maxDepth)
- return this.createOutput(listing.join('\n'))
- } else {
- throw new Error(`"${this.fsPath}" is neither a standard file nor directory.`)
- }
+ const fileContents = await this.readFile(fileUri)
+ this.logger.info(`Read file: ${this.fsPath}, size: ${fileContents.length}`)
+ return this.handleFileRange(fileContents)
} catch (error: any) {
this.logger.error(`Failed to read "${this.fsPath}": ${error.message || error}`)
- throw new Error(`[fs_read] Failed to read "${this.fsPath}": ${error.message || error}`)
+ throw new Error(`Failed to read "${this.fsPath}": ${error.message || error}`)
}
}
@@ -118,13 +121,6 @@ export class FsRead {
return [finalStart, finalEnd]
}
- private getDirectoryDepth(): number | undefined {
- if (!this.readRange || this.readRange.length === 0) {
- return 0
- }
- return this.readRange[0]
- }
-
private enforceMaxSize(content: string): string {
const byteCount = Buffer.byteLength(content, 'utf8')
if (byteCount > maxToolResponseSize) {
diff --git a/packages/core/src/codewhispererChat/tools/listDirectory.ts b/packages/core/src/codewhispererChat/tools/listDirectory.ts
new file mode 100644
index 00000000000..1d03d22303a
--- /dev/null
+++ b/packages/core/src/codewhispererChat/tools/listDirectory.ts
@@ -0,0 +1,70 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import * as vscode from 'vscode'
+import { getLogger } from '../../shared/logger/logger'
+import { readDirectoryRecursively } from '../../shared/utilities/workspaceUtils'
+import fs from '../../shared/fs/fs'
+import { InvokeOutput, OutputKind, sanitizePath } from './toolShared'
+import { Writable } from 'stream'
+import path from 'path'
+
+export interface ListDirectoryParams {
+ path: string
+}
+
+export class ListDirectory {
+ private fsPath: string
+ private readonly logger = getLogger('listDirectory')
+
+ constructor(params: ListDirectoryParams) {
+ this.fsPath = params.path
+ }
+
+ public async validate(): Promise {
+ if (!this.fsPath || this.fsPath.trim().length === 0) {
+ throw new Error('Path cannot be empty.')
+ }
+
+ const sanitized = sanitizePath(this.fsPath)
+ this.fsPath = sanitized
+
+ const pathUri = vscode.Uri.file(this.fsPath)
+ let pathExists: boolean
+ try {
+ pathExists = await fs.existsDir(pathUri)
+ if (!pathExists) {
+ throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed.`)
+ }
+ } catch (err) {
+ throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed. (${err})`)
+ }
+ }
+
+ public queueDescription(updates: Writable): void {
+ const fileName = path.basename(this.fsPath)
+ updates.write(`Listing directory on filePath: ${fileName}`)
+ updates.end()
+ }
+
+ public async invoke(updates: Writable): Promise {
+ try {
+ const fileUri = vscode.Uri.file(this.fsPath)
+ const listing = await readDirectoryRecursively(fileUri, 0)
+ return this.createOutput(listing.join('\n'))
+ } catch (error: any) {
+ this.logger.error(`Failed to list directory "${this.fsPath}": ${error.message || error}`)
+ throw new Error(`Failed to list directory "${this.fsPath}": ${error.message || error}`)
+ }
+ }
+
+ private createOutput(content: string): InvokeOutput {
+ return {
+ output: {
+ kind: OutputKind.Text,
+ content: content,
+ },
+ }
+ }
+}
diff --git a/packages/core/src/codewhispererChat/tools/toolUtils.ts b/packages/core/src/codewhispererChat/tools/toolUtils.ts
index 20bfa1afb0e..acda3e19022 100644
--- a/packages/core/src/codewhispererChat/tools/toolUtils.ts
+++ b/packages/core/src/codewhispererChat/tools/toolUtils.ts
@@ -8,17 +8,20 @@ import { FsWrite, FsWriteParams } from './fsWrite'
import { ExecuteBash, ExecuteBashParams } from './executeBash'
import { ToolResult, ToolResultContentBlock, ToolResultStatus, ToolUse } from '@amzn/codewhisperer-streaming'
import { InvokeOutput } from './toolShared'
+import { ListDirectory, ListDirectoryParams } from './listDirectory'
export enum ToolType {
FsRead = 'fsRead',
FsWrite = 'fsWrite',
ExecuteBash = 'executeBash',
+ ListDirectory = 'listDirectory',
}
export type Tool =
| { type: ToolType.FsRead; tool: FsRead }
| { type: ToolType.FsWrite; tool: FsWrite }
| { type: ToolType.ExecuteBash; tool: ExecuteBash }
+ | { type: ToolType.ListDirectory; tool: ListDirectory }
export class ToolUtils {
static displayName(tool: Tool): string {
@@ -29,6 +32,8 @@ export class ToolUtils {
return 'Write to filesystem'
case ToolType.ExecuteBash:
return 'Execute shell command'
+ case ToolType.ListDirectory:
+ return 'List directory from filesystem'
}
}
@@ -40,6 +45,8 @@ export class ToolUtils {
return true
case ToolType.ExecuteBash:
return tool.tool.requiresAcceptance()
+ case ToolType.ListDirectory:
+ return false
}
}
@@ -51,6 +58,8 @@ export class ToolUtils {
return tool.tool.invoke(updates)
case ToolType.ExecuteBash:
return tool.tool.invoke(updates)
+ case ToolType.ListDirectory:
+ return tool.tool.invoke(updates)
}
}
@@ -65,6 +74,9 @@ export class ToolUtils {
case ToolType.ExecuteBash:
tool.tool.queueDescription(updates)
break
+ case ToolType.ListDirectory:
+ tool.tool.queueDescription(updates)
+ break
}
}
@@ -76,6 +88,8 @@ export class ToolUtils {
return tool.tool.validate()
case ToolType.ExecuteBash:
return tool.tool.validate()
+ case ToolType.ListDirectory:
+ return tool.tool.validate()
}
}
@@ -108,6 +122,11 @@ export class ToolUtils {
type: ToolType.ExecuteBash,
tool: new ExecuteBash(value.input as unknown as ExecuteBashParams),
}
+ case ToolType.ListDirectory:
+ return {
+ type: ToolType.ListDirectory,
+ tool: new ListDirectory(value.input as unknown as ListDirectoryParams),
+ }
default:
return {
toolUseId: value.toolUseId,
diff --git a/packages/core/src/codewhispererChat/tools/tool_index.json b/packages/core/src/codewhispererChat/tools/tool_index.json
index 58b8731e264..7319e84253e 100644
--- a/packages/core/src/codewhispererChat/tools/tool_index.json
+++ b/packages/core/src/codewhispererChat/tools/tool_index.json
@@ -1,16 +1,16 @@
{
"fsRead": {
"name": "fsRead",
- "description": "A tool for reading files (e.g. `cat -n`), or listing files/directories (e.g. `ls -la` or `find . -maxdepth 2). The behavior of this tool is determined by the `path` parameter pointing to a file or directory.\n* If `path` is a file, this tool returns the result of running `cat -n`, and the optional `readRange` determines what range of lines will be read from the specified file.\n* If `path` is a directory, this tool returns the listed files and directories of the specified path, as if running `ls -la`. If the `readRange` parameter is provided, the tool acts like the `find . -maxdepth `, where `readRange` is the number of subdirectories deep to search, e.g. [2] will run `find . -maxdepth 2`.",
+ "description": "A tool for reading a file. \n* This tool returns the contents of a file, and the optional `readRange` determines what range of lines will be read from the specified file.",
"inputSchema": {
"type": "object",
"properties": {
"path": {
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.",
+ "description": "Absolute path to a file, e.g. `/repo/file.py`.",
"type": "string"
},
"readRange": {
- "description": "Optional parameter when reading either files or directories.\n* When `path` is a file, if none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[startLine, -1]` shows all lines from `startLine` to the end of the file.\n* When `path` is a directory, if none is given, the results of `ls -l` are given. If provided, the current directory and indicated number of subdirectories will be shown, e.g. [2] will show the current directory and directories two levels deep.",
+ "description": "Optional parameter when reading files.\n* If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[startLine, -1]` shows all lines from `startLine` to the end of the file.",
"items": {
"type": "integer"
},
@@ -22,7 +22,7 @@
},
"fsWrite": {
"name": "fsWrite",
- "description": "A tool for creating and editing files\n * The `create` command will override the file at `path` if it already exists as a file, and otherwise create a new file\n * The `append` command will add content to the end of an existing file, automatically adding a newline if the file doesn't end with one. The file must exist.\n Notes for using the `strReplace` command:\n * The `oldStr` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the `oldStr` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `oldStr` to make it unique\n * The `newStr` parameter should contain the edited lines that should replace the `oldStr`. The `insert` command will insert `newStr` after `insertLine` and place it on its own line.",
+ "description": "A tool for creating and editing a file.\n * The `create` command will override the file at `path` if it already exists as a file, and otherwise create a new file\n * The `append` command will add content to the end of an existing file, automatically adding a newline if the file doesn't end with one. The file must exist.\n Notes for using the `strReplace` command:\n * The `oldStr` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the `oldStr` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `oldStr` to make it unique\n * The `newStr` parameter should contain the edited lines that should replace the `oldStr`. The `insert` command will insert `newStr` after `insertLine` and place it on its own line.",
"inputSchema": {
"type": "object",
"properties": {
@@ -72,5 +72,23 @@
},
"required": ["command", "cwd"]
}
+ },
+ "listDirectory": {
+ "name": "listDirectory",
+ "description": "List the contents of a directory.\n * Use this tool for discovery, before using more targeted tools like fsRead.\n *Useful to try to understand the file structure before diving deeper into specific files.\n *Can be used to explore the codebase.\n *Results clearly distinguish between files, directories or symlinks with [FILE], [DIR] and [LINK] prefixes.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "explanation": {
+ "description": "One sentence explanation as to why this tool is being used, and how it contributes to the goal.",
+ "type": "string"
+ },
+ "path": {
+ "type": "string",
+ "description": "Absolute path to a directory, e.g., `/repo`."
+ }
+ },
+ "required": ["path"]
+ }
}
}
diff --git a/packages/core/src/shared/clients/codewhispererChatClient.ts b/packages/core/src/shared/clients/codewhispererChatClient.ts
index 192a3f13e9f..93b7b3bceb4 100644
--- a/packages/core/src/shared/clients/codewhispererChatClient.ts
+++ b/packages/core/src/shared/clients/codewhispererChatClient.ts
@@ -17,9 +17,7 @@ export async function createCodeWhispererChatStreamingClient(): Promise 500 + attempt ** 10),
+ retryStrategy: new ConfiguredRetryStrategy(1, (attempt: number) => 500 + attempt ** 10),
})
return streamingClient
}
diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts
index 32680cb57b6..4f6c759b0ec 100644
--- a/packages/core/src/shared/logger/logger.ts
+++ b/packages/core/src/shared/logger/logger.ts
@@ -19,6 +19,7 @@ export type LogTopic =
| 'fsRead'
| 'fsWrite'
| 'executeBash'
+ | 'listDirectory'
| 'chatStream'
| 'unknown'
diff --git a/packages/core/src/shared/utilities/workspaceUtils.ts b/packages/core/src/shared/utilities/workspaceUtils.ts
index e97297ad265..61747fa1a1d 100644
--- a/packages/core/src/shared/utilities/workspaceUtils.ts
+++ b/packages/core/src/shared/utilities/workspaceUtils.ts
@@ -673,14 +673,14 @@ export async function findStringInDirectory(searchStr: string, dirPath: string)
}
/**
- * Returns a one-character tag for a directory ('d'), symlink ('l'), or file ('-').
+ * Returns a prefix for a directory ('[DIR]'), symlink ('[LINK]'), or file ('[FILE]').
*/
export function formatListing(name: string, fileType: vscode.FileType, fullPath: string): string {
- let typeChar = '-'
+ let typeChar = '[FILE]'
if (fileType === vscode.FileType.Directory) {
- typeChar = 'd'
+ typeChar = '[DIR]'
} else if (fileType === vscode.FileType.SymbolicLink) {
- typeChar = 'l'
+ typeChar = '[LINK]'
}
return `${typeChar} ${fullPath}`
}
diff --git a/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts b/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts
index befe5ca624b..d7bf76456b1 100644
--- a/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts
+++ b/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts
@@ -43,23 +43,6 @@ describe('FsRead Tool', () => {
assert.strictEqual(result.output.content, 'B\nC\nD')
})
- it('lists directory contents up to depth = 1', async () => {
- await testFolder.mkdir('subfolder')
- await testFolder.write('fileA.txt', 'fileA content')
- await testFolder.write(path.join('subfolder', 'fileB.md'), '# fileB')
-
- const fsRead = new FsRead({ path: testFolder.path, readRange: [1] })
- await fsRead.validate()
- const result = await fsRead.invoke(process.stdout)
-
- const lines = result.output.content.split('\n')
- const hasFileA = lines.some((line: string | string[]) => line.includes('- ') && line.includes('fileA.txt'))
- const hasSubfolder = lines.some((line: string | string[]) => line.includes('d ') && line.includes('subfolder'))
-
- assert.ok(hasFileA, 'Should list fileA.txt in the directory output')
- assert.ok(hasSubfolder, 'Should list the subfolder in the directory output')
- })
-
it('throws error if path does not exist', async () => {
const missingPath = path.join(testFolder.path, 'no_such_file.txt')
const fsRead = new FsRead({ path: missingPath })
@@ -94,29 +77,4 @@ describe('FsRead Tool', () => {
assert.strictEqual(result.output.kind, 'text')
assert.strictEqual(result.output.content, '')
})
-
- it('expands ~ path', async () => {
- const fsRead = new FsRead({ path: '~' })
- await fsRead.validate()
- const result = await fsRead.invoke(process.stdout)
-
- assert.strictEqual(result.output.kind, 'text')
- assert.ok(result.output.content.length > 0)
- })
-
- it('resolves relative path', async () => {
- await testFolder.mkdir('relTest')
- const filePath = path.join('relTest', 'relFile.txt')
- const content = 'Hello from a relative file!'
- await testFolder.write(filePath, content)
-
- const relativePath = path.relative(process.cwd(), path.join(testFolder.path, filePath))
-
- const fsRead = new FsRead({ path: relativePath })
- await fsRead.validate()
- const result = await fsRead.invoke(process.stdout)
-
- assert.strictEqual(result.output.kind, 'text')
- assert.strictEqual(result.output.content, content)
- })
})
diff --git a/packages/core/src/test/codewhispererChat/tools/listDirectory.test.ts b/packages/core/src/test/codewhispererChat/tools/listDirectory.test.ts
new file mode 100644
index 00000000000..10ed6d202bc
--- /dev/null
+++ b/packages/core/src/test/codewhispererChat/tools/listDirectory.test.ts
@@ -0,0 +1,60 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert'
+import { ListDirectory } from '../../../codewhispererChat/tools/listDirectory'
+import { TestFolder } from '../../testUtil'
+import path from 'path'
+
+describe('ListDirectory Tool', () => {
+ let testFolder: TestFolder
+
+ before(async () => {
+ testFolder = await TestFolder.create()
+ })
+
+ it('throws if path is empty', async () => {
+ const listDirectory = new ListDirectory({ path: '' })
+ await assert.rejects(listDirectory.validate(), /Path cannot be empty/i, 'Expected an error about empty path')
+ })
+
+ it('lists directory contents', async () => {
+ await testFolder.mkdir('subfolder')
+ await testFolder.write('fileA.txt', 'fileA content')
+ await testFolder.write(path.join('subfolder', 'fileB.md'), '# fileB')
+
+ const listDirectory = new ListDirectory({ path: testFolder.path })
+ await listDirectory.validate()
+ const result = await listDirectory.invoke(process.stdout)
+
+ const lines = result.output.content.split('\n')
+ const hasFileA = lines.some((line: string | string[]) => line.includes('[FILE] ') && line.includes('fileA.txt'))
+ const hasSubfolder = lines.some(
+ (line: string | string[]) => line.includes('[DIR] ') && line.includes('subfolder')
+ )
+
+ assert.ok(hasFileA, 'Should list fileA.txt in the directory output')
+ assert.ok(hasSubfolder, 'Should list the subfolder in the directory output')
+ })
+
+ it('throws error if path does not exist', async () => {
+ const missingPath = path.join(testFolder.path, 'no_such_file.txt')
+ const listDirectory = new ListDirectory({ path: missingPath })
+
+ await assert.rejects(
+ listDirectory.validate(),
+ /does not exist or cannot be accessed/i,
+ 'Expected an error indicating the path does not exist'
+ )
+ })
+
+ it('expands ~ path', async () => {
+ const listDirectory = new ListDirectory({ path: '~' })
+ await listDirectory.validate()
+ const result = await listDirectory.invoke(process.stdout)
+
+ assert.strictEqual(result.output.kind, 'text')
+ assert.ok(result.output.content.length > 0)
+ })
+})
diff --git a/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts b/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts
index 4395258cbdf..c7ba3363b59 100644
--- a/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts
+++ b/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts
@@ -14,12 +14,14 @@ import { ExecuteBash } from '../../../codewhispererChat/tools/executeBash'
import { ToolUse } from '@amzn/codewhisperer-streaming'
import path from 'path'
import fs from '../../../shared/fs/fs'
+import { ListDirectory } from '../../../codewhispererChat/tools/listDirectory'
describe('ToolUtils', function () {
let sandbox: sinon.SinonSandbox
let mockFsRead: sinon.SinonStubbedInstance
let mockFsWrite: sinon.SinonStubbedInstance
let mockExecuteBash: sinon.SinonStubbedInstance
+ let mockListDirectory: sinon.SinonStubbedInstance
let mockWritable: sinon.SinonStubbedInstance
beforeEach(function () {
@@ -27,6 +29,7 @@ describe('ToolUtils', function () {
mockFsRead = sandbox.createStubInstance(FsRead)
mockFsWrite = sandbox.createStubInstance(FsWrite)
mockExecuteBash = sandbox.createStubInstance(ExecuteBash)
+ mockListDirectory = sandbox.createStubInstance(ListDirectory)
mockWritable = {
write: sandbox.stub(),
} as unknown as sinon.SinonStubbedInstance
@@ -51,6 +54,11 @@ describe('ToolUtils', function () {
const tool: Tool = { type: ToolType.ExecuteBash, tool: mockExecuteBash as unknown as ExecuteBash }
assert.strictEqual(ToolUtils.displayName(tool), 'Execute shell command')
})
+
+ it('returns correct display name for ListDirectory tool', function () {
+ const tool: Tool = { type: ToolType.ListDirectory, tool: mockListDirectory as unknown as ListDirectory }
+ assert.strictEqual(ToolUtils.displayName(tool), 'List directory from filesystem')
+ })
})
describe('requiresAcceptance', function () {
@@ -74,6 +82,11 @@ describe('ToolUtils', function () {
assert(mockExecuteBash.requiresAcceptance.calledTwice)
})
+
+ it('returns false for ListDirectory tool', function () {
+ const tool: Tool = { type: ToolType.ListDirectory, tool: mockListDirectory as unknown as ListDirectory }
+ assert.strictEqual(ToolUtils.requiresAcceptance(tool), false)
+ })
})
describe('invoke', function () {
@@ -124,6 +137,22 @@ describe('ToolUtils', function () {
assert.deepStrictEqual(result, expectedOutput)
assert(mockExecuteBash.invoke.calledOnceWith(mockWritable))
})
+
+ it('delegates to ListDirectory tool invoke method', async function () {
+ const expectedOutput: InvokeOutput = {
+ output: {
+ kind: OutputKind.Text,
+ content: 'test content',
+ },
+ }
+ mockListDirectory.invoke.resolves(expectedOutput)
+
+ const tool: Tool = { type: ToolType.ListDirectory, tool: mockListDirectory as unknown as ListDirectory }
+ const result = await ToolUtils.invoke(tool, mockWritable as unknown as Writable)
+
+ assert.deepStrictEqual(result, expectedOutput)
+ assert(mockListDirectory.invoke.calledOnceWith(mockWritable))
+ })
})
describe('queueDescription', function () {
@@ -147,6 +176,13 @@ describe('ToolUtils', function () {
assert(mockExecuteBash.queueDescription.calledOnceWith(mockWritable))
})
+
+ it('delegates to ListDirectory tool queueDescription method', function () {
+ const tool: Tool = { type: ToolType.ListDirectory, tool: mockListDirectory as unknown as ListDirectory }
+ ToolUtils.queueDescription(tool, mockWritable as unknown as Writable)
+
+ assert(mockListDirectory.queueDescription.calledOnceWith(mockWritable))
+ })
})
describe('validate', function () {
@@ -176,6 +212,15 @@ describe('ToolUtils', function () {
assert(mockExecuteBash.validate.calledOnce)
})
+
+ it('delegates to ListDirectory tool validate method', async function () {
+ mockListDirectory.validate.resolves()
+
+ const tool: Tool = { type: ToolType.ListDirectory, tool: mockListDirectory as unknown as ListDirectory }
+ await ToolUtils.validate(tool)
+
+ assert(mockListDirectory.validate.calledOnce)
+ })
})
describe('tryFromToolUse', function () {
@@ -227,6 +272,22 @@ describe('ToolUtils', function () {
}
})
+ it('creates ListDirectory tool from ToolUse', function () {
+ const toolUse: ToolUse = {
+ toolUseId: 'test-id',
+ name: ToolType.ListDirectory,
+ input: { path: '/test/path' },
+ }
+
+ const result = ToolUtils.tryFromToolUse(toolUse)
+
+ assert.strictEqual('type' in result, true)
+ if ('type' in result) {
+ assert.strictEqual(result.type, ToolType.ListDirectory)
+ assert(result.tool instanceof ListDirectory)
+ }
+ })
+
it('returns error result for unsupported tool', function () {
const toolUse: ToolUse = {
toolUseId: 'test-id',
diff --git a/packages/toolkit/.changes/3.52.0.json b/packages/toolkit/.changes/3.52.0.json
new file mode 100644
index 00000000000..7fb11ef522f
--- /dev/null
+++ b/packages/toolkit/.changes/3.52.0.json
@@ -0,0 +1,14 @@
+{
+ "date": "2025-03-28",
+ "version": "3.52.0",
+ "entries": [
+ {
+ "type": "Bug Fix",
+ "description": "SAM build: prevent running multiple build processes for the same template"
+ },
+ {
+ "type": "Feature",
+ "description": "Lambda: Add 'Process SNS notification messages with Lambda' Serverless Land pattern."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/toolkit/.changes/next-release/Bug Fix-db0d3605-5cb7-4b78-b4ae-29be86f51f99.json b/packages/toolkit/.changes/next-release/Bug Fix-db0d3605-5cb7-4b78-b4ae-29be86f51f99.json
deleted file mode 100644
index 2124088e427..00000000000
--- a/packages/toolkit/.changes/next-release/Bug Fix-db0d3605-5cb7-4b78-b4ae-29be86f51f99.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "SAM build: prevent running multiple build processes for the same template"
-}
diff --git a/packages/toolkit/.changes/next-release/Feature-279a0842-feff-4089-8529-61376bf8cf97.json b/packages/toolkit/.changes/next-release/Feature-279a0842-feff-4089-8529-61376bf8cf97.json
deleted file mode 100644
index 820d634b943..00000000000
--- a/packages/toolkit/.changes/next-release/Feature-279a0842-feff-4089-8529-61376bf8cf97.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Feature",
- "description": "Lambda: Add 'Process SNS notification messages with Lambda' Serverless Land pattern."
-}
diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md
index 1793f0df4bb..34a9087d5c4 100644
--- a/packages/toolkit/CHANGELOG.md
+++ b/packages/toolkit/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 3.52.0 2025-03-28
+
+- **Bug Fix** SAM build: prevent running multiple build processes for the same template
+- **Feature** Lambda: Add 'Process SNS notification messages with Lambda' Serverless Land pattern.
+
## 3.51.0 2025-03-20
- **Feature** Update Step Functions marketplace documentation.
diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json
index 1be6c628ae6..b21c6ef0204 100644
--- a/packages/toolkit/package.json
+++ b/packages/toolkit/package.json
@@ -2,7 +2,7 @@
"name": "aws-toolkit-vscode",
"displayName": "AWS Toolkit",
"description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.",
- "version": "3.52.0-SNAPSHOT",
+ "version": "3.53.0-SNAPSHOT",
"extensionKind": [
"workspace"
],