diff --git a/.github/workflows/create-release-candidate-branch.yml b/.github/workflows/create-release-candidate-branch.yml index aa37873d2a..2071f69ed4 100644 --- a/.github/workflows/create-release-candidate-branch.yml +++ b/.github/workflows/create-release-candidate-branch.yml @@ -44,6 +44,10 @@ jobs: node-version: '20' cache: 'npm' + # Needed to format the json file being checked in + - name: Install dependencies + run: npm ci + - name: Calculate Release Version id: release-version env: @@ -87,6 +91,9 @@ jobs: git add "$VERSION_FILE" + # Ensure the file does not cause issues when merged to main + npm run format-staged + - name: Create Release Candidate Branch id: release-branch env: @@ -112,5 +119,5 @@ jobs: git config --global user.name "aws-toolkit-automation" # Configure git to use the PAT token for authentication git remote set-url origin "https://x-access-token:${REPO_PAT}@github.com/${{ github.repository }}.git" - git commit -m "Bump agentic version: $RELEASE_VERSION" + git commit -m "chore: bump agentic version: $RELEASE_VERSION" git push --set-upstream origin "$BRANCH_NAME" diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c3df1b76e7..e6abdf7be2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,8 +1,8 @@ { - "chat-client": "0.1.31", + "chat-client": "0.1.32", "core/aws-lsp-core": "0.0.13", "server/aws-lsp-antlr4": "0.1.17", - "server/aws-lsp-codewhisperer": "0.0.72", + "server/aws-lsp-codewhisperer": "0.0.73", "server/aws-lsp-json": "0.1.17", "server/aws-lsp-partiql": "0.0.16", "server/aws-lsp-yaml": "0.1.17" diff --git a/app/aws-lsp-antlr4-runtimes/package.json b/app/aws-lsp-antlr4-runtimes/package.json index e283033afb..bf7cf47bf1 100644 --- a/app/aws-lsp-antlr4-runtimes/package.json +++ b/app/aws-lsp-antlr4-runtimes/package.json @@ -12,7 +12,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" diff --git a/app/aws-lsp-buildspec-runtimes/package.json b/app/aws-lsp-buildspec-runtimes/package.json index 0fa2217def..b9c36946b2 100644 --- a/app/aws-lsp-buildspec-runtimes/package.json +++ b/app/aws-lsp-buildspec-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-buildspec": "^0.0.1" } } diff --git a/app/aws-lsp-cloudformation-runtimes/package.json b/app/aws-lsp-cloudformation-runtimes/package.json index 336e9b48ae..a88386db4e 100644 --- a/app/aws-lsp-cloudformation-runtimes/package.json +++ b/app/aws-lsp-cloudformation-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-cloudformation": "^0.0.1" } } diff --git a/app/aws-lsp-codewhisperer-runtimes/README.md b/app/aws-lsp-codewhisperer-runtimes/README.md index 977d514bd6..71fbcf0456 100644 --- a/app/aws-lsp-codewhisperer-runtimes/README.md +++ b/app/aws-lsp-codewhisperer-runtimes/README.md @@ -76,6 +76,17 @@ The server is managed via scripts/dev-server.js, which ensures: **NOTE**: Tests are currently disabled for Windows as we currently face issues with automatically shutting down devserver and cleaning resources after tests are executed. +## Binary Dependencies + +### registry.node +The file `_bundle-assets/registry-js/win32-x64/registry.node` is a precompiled binary downloaded from the [registry-js](https://github.com/desktop/registry-js) project. + +- **Current version**: v1.16.1 (released May 21, 2024) +- **Source**: https://github.com/desktop/registry-js/releases +- **Purpose**: Provides Windows registry access functionality + +**To update**: Download the latest `registry.node` binary for win32-x64 from the registry-js releases page and replace the existing file. + #### Tests configuration - Test settings are defined in `wdio.conf.ts` - The actual test implementation is in the `test/e2e` folder diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/registry-js/win32-x64/registry.node b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/registry-js/win32-x64/registry.node new file mode 100644 index 0000000000..d7b5091a2c Binary files /dev/null and b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/registry-js/win32-x64/registry.node differ diff --git a/app/aws-lsp-codewhisperer-runtimes/package.json b/app/aws-lsp-codewhisperer-runtimes/package.json index 6a1721382c..487d2c5b1a 100644 --- a/app/aws-lsp-codewhisperer-runtimes/package.json +++ b/app/aws-lsp-codewhisperer-runtimes/package.json @@ -10,10 +10,11 @@ "package:prod": "npm run compile && cross-env NODE_OPTIONS=--max_old_space_size=8172 npm run webpack:prod", "webpack": "webpack", "webpack:prod": "webpack --config webpack.config.prod.js", + "copy:native-deps:agent-standalone": "copyfiles -f _bundle-assets/registry-js/win32-x64/registry.node build/private/bundle/agent-standalone", "copy:resources:agent-standalone": "copyfiles -f --error ../../node_modules/@aws/lsp-identity/src/sso/authorizationCodePkce/resources/**/* build/private/bundle/agent-standalone/resources", "generate:node-assets": "./scripts/download-node.sh && ts-node src/scripts/copy-node-assets.ts", "generate:build-archive": "./scripts/package.sh", - "ci:generate:agent-standalone": "npm run package:prod && npm run copy:resources:agent-standalone && npm run generate:node-assets && npm run generate:build-archive", + "ci:generate:agent-standalone": "npm run package:prod && npm run copy:native-deps:agent-standalone && npm run copy:resources:agent-standalone && npm run generate:node-assets && npm run generate:build-archive", "ci:generate:manifest": "ts-node scripts/create-repo-manifest.ts", "start": "cross-env NODE_OPTIONS=--max_old_space_size=8172 node scripts/dev-server.js start", "stop-dev-server": "node scripts/dev-server.js stop", @@ -22,7 +23,7 @@ "local-build": "node scripts/local-build.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/app/aws-lsp-codewhisperer-runtimes/src/version.json b/app/aws-lsp-codewhisperer-runtimes/src/version.json index 3f5ef793d0..4c93c3f549 100644 --- a/app/aws-lsp-codewhisperer-runtimes/src/version.json +++ b/app/aws-lsp-codewhisperer-runtimes/src/version.json @@ -1,3 +1,3 @@ { - "agenticChat": "1.25.0" + "agenticChat": "1.27.0" } diff --git a/app/aws-lsp-identity-runtimes/package.json b/app/aws-lsp-identity-runtimes/package.json index b4971eae87..46abf7d958 100644 --- a/app/aws-lsp-identity-runtimes/package.json +++ b/app/aws-lsp-identity-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-identity": "^0.0.1" } } diff --git a/app/aws-lsp-json-runtimes/package.json b/app/aws-lsp-json-runtimes/package.json index 9fa7b7e958..24ae3535ac 100644 --- a/app/aws-lsp-json-runtimes/package.json +++ b/app/aws-lsp-json-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-json": "*" }, "devDependencies": { diff --git a/app/aws-lsp-notification-runtimes/package.json b/app/aws-lsp-notification-runtimes/package.json index a355a967bf..1e7641e2a8 100644 --- a/app/aws-lsp-notification-runtimes/package.json +++ b/app/aws-lsp-notification-runtimes/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-notification": "^0.0.1" } } diff --git a/app/aws-lsp-s3-runtimes/package.json b/app/aws-lsp-s3-runtimes/package.json index 1c15a9af5e..ad84f62776 100644 --- a/app/aws-lsp-s3-runtimes/package.json +++ b/app/aws-lsp-s3-runtimes/package.json @@ -10,7 +10,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-s3": "^0.0.1" } } diff --git a/app/aws-lsp-yaml-json-webworker/package.json b/app/aws-lsp-yaml-json-webworker/package.json index bfb5fa6277..7079d1fa3b 100644 --- a/app/aws-lsp-yaml-json-webworker/package.json +++ b/app/aws-lsp-yaml-json-webworker/package.json @@ -11,7 +11,7 @@ "serve:webpack": "NODE_ENV=development webpack serve" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, diff --git a/app/aws-lsp-yaml-runtimes/package.json b/app/aws-lsp-yaml-runtimes/package.json index 86638b8ffc..a59f919477 100644 --- a/app/aws-lsp-yaml-runtimes/package.json +++ b/app/aws-lsp-yaml-runtimes/package.json @@ -11,7 +11,7 @@ "webpack": "webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-yaml": "*" }, "devDependencies": { diff --git a/app/hello-world-lsp-runtimes/package.json b/app/hello-world-lsp-runtimes/package.json index 2ccccc694b..54018d89d0 100644 --- a/app/hello-world-lsp-runtimes/package.json +++ b/app/hello-world-lsp-runtimes/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.121" + "@aws/language-server-runtimes": "^0.2.123" }, "devDependencies": { "@types/chai": "^4.3.5", diff --git a/chat-client/CHANGELOG.md b/chat-client/CHANGELOG.md index aa178a18e6..1cd9acfbe0 100644 --- a/chat-client/CHANGELOG.md +++ b/chat-client/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.1.32](https://github.com/aws/language-servers/compare/chat-client/v0.1.31...chat-client/v0.1.32) (2025-08-11) + + +### Features + +* **amazonq:** read tool ui revamp ([c65428b](https://github.com/aws/language-servers/commit/c65428bab2cf5e47badf1e3a9453babcf881e60c)) + ## [0.1.31](https://github.com/aws/language-servers/compare/chat-client/v0.1.30...chat-client/v0.1.31) (2025-08-06) diff --git a/chat-client/package.json b/chat-client/package.json index 2d3332b597..df0378f3e4 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -1,6 +1,6 @@ { "name": "@aws/chat-client", - "version": "0.1.31", + "version": "0.1.32", "description": "AWS Chat Client", "main": "out/index.js", "repository": { @@ -25,9 +25,9 @@ }, "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/language-server-runtimes-types": "^0.1.50", - "@aws/mynah-ui": "^4.36.2" + "@aws/mynah-ui": "^4.36.4" }, "devDependencies": { "@types/jsdom": "^21.1.6", diff --git a/client/vscode/package.json b/client/vscode/package.json index 4b8c2fdb4c..975a395d66 100644 --- a/client/vscode/package.json +++ b/client/vscode/package.json @@ -352,7 +352,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", diff --git a/core/aws-lsp-core/package.json b/core/aws-lsp-core/package.json index 4c033448eb..aeff582d34 100644 --- a/core/aws-lsp-core/package.json +++ b/core/aws-lsp-core/package.json @@ -28,7 +28,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", diff --git a/integration-tests/q-agentic-chat-server/package.json b/integration-tests/q-agentic-chat-server/package.json index 26202df0c1..5431142f5c 100644 --- a/integration-tests/q-agentic-chat-server/package.json +++ b/integration-tests/q-agentic-chat-server/package.json @@ -9,7 +9,7 @@ "test-integ": "npm run compile && mocha --timeout 30000 \"./out/**/*.test.js\" --retries 2" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "*" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index da8006537f..b42f206c44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "name": "@aws/lsp-antlr4-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" @@ -71,7 +71,7 @@ "name": "@aws/lsp-buildspec-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-buildspec": "^0.0.1" } }, @@ -79,7 +79,7 @@ "name": "@aws/lsp-cloudformation-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-cloudformation": "^0.0.1" } }, @@ -87,7 +87,7 @@ "name": "@aws/lsp-codewhisperer-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", @@ -120,7 +120,7 @@ "name": "@aws/lsp-identity-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-identity": "^0.0.1" } }, @@ -128,7 +128,7 @@ "name": "@aws/lsp-json-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-json": "*" }, "devDependencies": { @@ -148,7 +148,7 @@ "name": "@aws/lsp-notification-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-notification": "^0.0.1" } }, @@ -181,7 +181,7 @@ "name": "@aws/lsp-s3-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-s3": "^0.0.1" }, "bin": { @@ -192,7 +192,7 @@ "name": "@aws/lsp-yaml-json-webworker", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, @@ -212,7 +212,7 @@ "name": "@aws/lsp-yaml-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-yaml": "*" }, "devDependencies": { @@ -234,7 +234,7 @@ "version": "0.0.1", "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.121" + "@aws/language-server-runtimes": "^0.2.123" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -251,13 +251,13 @@ }, "chat-client": { "name": "@aws/chat-client", - "version": "0.1.31", + "version": "0.1.32", "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/language-server-runtimes-types": "^0.1.50", - "@aws/mynah-ui": "^4.36.2" + "@aws/mynah-ui": "^4.36.4" }, "devDependencies": { "@types/jsdom": "^21.1.6", @@ -280,7 +280,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", @@ -296,7 +296,7 @@ "version": "0.0.13", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", @@ -327,7 +327,7 @@ "name": "@aws/q-agentic-chat-server-integration-tests", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "*" }, "devDependencies": { @@ -4036,12 +4036,12 @@ "link": true }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.121", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.121.tgz", - "integrity": "sha512-DDh3ICNVoEi4nhp4JdkkPTsdlHsF0yt6VgxlMGwP90N1hjA4xVESSSla9pB0TcuMPKlmXqOPQGQvkph9FKerVw==", + "version": "0.2.123", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.123.tgz", + "integrity": "sha512-gxjnBcQY+HR9+F1NXQUEQ6ikJhrLMJEbrpIxlBLILtQ75hVtRDsfGET3KW5Nn0dgbrQTx6VqwvXDfolUkmi06g==", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes-types": "^0.1.53", + "@aws/language-server-runtimes-types": "^0.1.55", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.200.0", "@opentelemetry/core": "^2.0.0", @@ -4056,7 +4056,7 @@ "hpagent": "^1.2.0", "jose": "^5.9.6", "mac-ca": "^3.1.1", - "os-proxy-config": "^1.1.2", + "registry-js": "^1.16.1", "rxjs": "^7.8.2", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", @@ -4068,9 +4068,9 @@ } }, "node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.53", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.53.tgz", - "integrity": "sha512-6KCe/YsqF0SciXm8qg/qVuDXGwQqJgRaqrT6YhZUjqs3mclG9G6Gdwu9YEi8t/NYobNWKw0E+aCXW2TFxJgr7A==", + "version": "0.1.55", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.55.tgz", + "integrity": "sha512-KRy3fTCNGvAQxA4amTODXPuubxrYlqKsyJOXPaIn+YDACwJa7shrOryHg6xrib6uHAHT2fEkcTMk9TT4MRPxQA==", "license": "Apache-2.0", "dependencies": { "vscode-languageserver-textdocument": "^1.0.12", @@ -4204,9 +4204,9 @@ "link": true }, "node_modules/@aws/mynah-ui": { - "version": "4.36.2", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.36.2.tgz", - "integrity": "sha512-3ibfK2CTj7dlFFdgTIE1DdEyDpy+P3hdP/Fmlx76T9GGSYiGHqwunDSi59L1P61Kj46WADBrQ52mLUQ6FR8Rzg==", + "version": "4.36.4", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.36.4.tgz", + "integrity": "sha512-vGW4wlNindpr2Ep9x3iuKbrZTXe5KrE8vWpg15DjkN3qK42KMuMEQ67Pqtfgl5EseNYC1ukZm4HIQIMmt+vevA==", "hasInstallScript": true, "license": "Apache License 2.0", "dependencies": { @@ -20292,12 +20292,6 @@ "undici": "^6.16.1" } }, - "node_modules/mac-system-proxy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mac-system-proxy/-/mac-system-proxy-1.0.4.tgz", - "integrity": "sha512-IAkNLxXZrYuM99A2OhPrvUoAxohsxQciJh2D2xnD+R6vypn/AVyOYLsbZsMVCS/fEbLIe67nQ8krEAfqP12BVg==", - "license": "Apache-2.0" - }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -21387,16 +21381,6 @@ "node": ">=0.10.0" } }, - "node_modules/os-proxy-config": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/os-proxy-config/-/os-proxy-config-1.1.2.tgz", - "integrity": "sha512-sV7htE8y6NQORU0oKOUGTwQYe1gSFK3a3Z1i4h6YaqdrA9C0JIsUPQAqEkO8ejjYbRrQ+jsnks5qjtisr7042Q==", - "license": "Apache-2.0", - "dependencies": { - "mac-system-proxy": "^1.0.0", - "windows-system-proxy": "^1.0.0" - } - }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -28090,15 +28074,6 @@ "node": ">=4" } }, - "node_modules/windows-system-proxy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/windows-system-proxy/-/windows-system-proxy-1.0.0.tgz", - "integrity": "sha512-qd1WfyX9gjAqI36RHt95di2+FBr74DhvELd1EASgklCGScjwReHnWnXfUyabp/CJWl/IdnkUzG0Ub6Cv2R4KJQ==", - "license": "Apache-2.0", - "dependencies": { - "registry-js": "^1.15.1" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -28633,7 +28608,7 @@ "version": "0.1.17", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.13" }, "devDependencies": { @@ -28675,7 +28650,7 @@ "name": "@aws/lsp-buildspec", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", @@ -28686,7 +28661,7 @@ "name": "@aws/lsp-cloudformation", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", @@ -28695,7 +28670,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.72", + "version": "0.0.73", "bundleDependencies": [ "@amzn/codewhisperer-streaming", "@amzn/amazon-q-developer-streaming-client" @@ -28708,7 +28683,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.13", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", @@ -28848,7 +28823,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -28913,7 +28888,7 @@ "version": "0.1.17", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -28930,7 +28905,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, @@ -28991,7 +28966,7 @@ "version": "0.0.16", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" @@ -29013,7 +28988,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" @@ -29044,7 +29019,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", @@ -29058,7 +29033,7 @@ "name": "@amzn/device-sso-auth-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "vscode-languageserver": "^9.0.1" }, "devDependencies": { @@ -29069,7 +29044,7 @@ "name": "@aws/hello-world-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/aws-lsp-antlr4/package.json b/server/aws-lsp-antlr4/package.json index aadfe48b08..9d6c925b40 100644 --- a/server/aws-lsp-antlr4/package.json +++ b/server/aws-lsp-antlr4/package.json @@ -28,7 +28,7 @@ "clean": "rm -rf node_modules" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.13" }, "peerDependencies": { diff --git a/server/aws-lsp-buildspec/package.json b/server/aws-lsp-buildspec/package.json index 92960545fb..f59edb5549 100644 --- a/server/aws-lsp-buildspec/package.json +++ b/server/aws-lsp-buildspec/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", diff --git a/server/aws-lsp-cloudformation/package.json b/server/aws-lsp-cloudformation/package.json index 2dd24848b8..bfc8ebd7e5 100644 --- a/server/aws-lsp-cloudformation/package.json +++ b/server/aws-lsp-cloudformation/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", diff --git a/server/aws-lsp-codewhisperer/CHANGELOG.md b/server/aws-lsp-codewhisperer/CHANGELOG.md index 8497df84be..1b2f1011df 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [0.0.73](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.72...lsp-codewhisperer/v0.0.73) (2025-08-11) + + +### Features + +* **amazonq:** read tool ui revamp ([c65428b](https://github.com/aws/language-servers/commit/c65428bab2cf5e47badf1e3a9453babcf881e60c)) + + +### Bug Fixes + +* **amazonq:** add fallback classpath generation ([#2077](https://github.com/aws/language-servers/issues/2077)) ([3a6ef14](https://github.com/aws/language-servers/commit/3a6ef14e78fa2e75b837bba6524751d65038f416)) +* **amazonq:** emit failed status for amazonq_invokeLLM ([#2071](https://github.com/aws/language-servers/issues/2071)) ([ee52a41](https://github.com/aws/language-servers/commit/ee52a41bc869b275fff708d7955b59f43b93bbd4)) +* **amazonq:** fix fallout of [#2051](https://github.com/aws/language-servers/issues/2051) ([#2057](https://github.com/aws/language-servers/issues/2057)) ([565066b](https://github.com/aws/language-servers/commit/565066bb61adda60333c9646db958d4208bcc8af)) +* **amazonq:** leverage lcs to find the chars added and removed ([#2092](https://github.com/aws/language-servers/issues/2092)) ([40379a8](https://github.com/aws/language-servers/commit/40379a887f8d42cc184239ca3175b4e673cc5286)) +* **amazonq:** skips continuous monitoring when WCS sees workspace as idle ([#2066](https://github.com/aws/language-servers/issues/2066)) ([9cb959d](https://github.com/aws/language-servers/commit/9cb959d4cc450d0907f8bf5265ba01d2aa68bcd0)) +* creating a new sesion for Edits trigger with next token ([#2094](https://github.com/aws/language-servers/issues/2094)) ([1da8730](https://github.com/aws/language-servers/commit/1da8730b6ed6ad53b6561368bf722e56d59596a4)) +* remove edit cache logic ([#2079](https://github.com/aws/language-servers/issues/2079)) ([9bc5b9c](https://github.com/aws/language-servers/commit/9bc5b9c1d77e5fee6f518f7f5016d3a0043a5a77)) +* sessionManager misused because there are 2 types of manager now ([#2090](https://github.com/aws/language-servers/issues/2090)) ([8db059a](https://github.com/aws/language-servers/commit/8db059ab83d94fd7c3ba3eb265044add31c80aea)) +* update client name to support Sagemaker AI origin for agentic chat ([#2093](https://github.com/aws/language-servers/issues/2093)) ([a746fe8](https://github.com/aws/language-servers/commit/a746fe845d5e09563b475f01ce44059dca9fd10f)) + ## [0.0.72](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.71...lsp-codewhisperer/v0.0.72) (2025-08-06) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index 09490a4f3f..fdd34884b1 100644 --- a/server/aws-lsp-codewhisperer/package.json +++ b/server/aws-lsp-codewhisperer/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-codewhisperer", - "version": "0.0.72", + "version": "0.0.73", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { @@ -36,7 +36,7 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.56", - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.13", "@modelcontextprotocol/sdk": "^1.15.0", "@smithy/node-http-handler": "^2.5.0", diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts index 48e9e2adec..6c8af7a272 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts @@ -67,6 +67,7 @@ import { McpManager } from './tools/mcp/mcpManager' import { AgenticChatResultStream } from './agenticChatResultStream' import { AgenticChatError } from './errors' import * as sharedUtils from '../../shared/utils' +import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager' describe('AgenticChatController', () => { let mcpInstanceStub: sinon.SinonStub @@ -475,6 +476,15 @@ describe('AgenticChatController', () => { assert.strictEqual(typeof session.conversationId, 'string') }) + it('invokes IdleWorkspaceManager recordActivityTimestamp', async () => { + const recordActivityTimestampStub = sinon.stub(IdleWorkspaceManager, 'recordActivityTimestamp') + + await chatController.onChatPrompt({ tabId: mockTabId, prompt: { prompt: 'Hello' } }, mockCancellationToken) + + sinon.assert.calledOnce(recordActivityTimestampStub) + recordActivityTimestampStub.restore() + }) + it('includes chat history from the database in the request input', async () => { // Mock chat history const mockHistory = [ diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts index 60bba5885a..72217fae07 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -230,6 +230,7 @@ import { ActiveUserTracker } from '../../shared/activeUserTracker' import { UserContext } from '../../client/token/codewhispererbearertokenclient' import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' import { DisplayFindings } from './tools/qCodeAnalysis/displayFindings' +import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager' type ChatHandlers = Omit< LspHandlers, @@ -722,6 +723,8 @@ export class AgenticChatController implements ChatHandlers { // Phase 1: Initial Setup - This happens only once params.prompt.prompt = sanitizeInput(params.prompt.prompt || '') + IdleWorkspaceManager.recordActivityTimestamp() + const maybeDefaultResponse = !params.prompt.command && getDefaultChatResponse(params.prompt.prompt) if (maybeDefaultResponse) { return maybeDefaultResponse @@ -3285,6 +3288,9 @@ export class AgenticChatController implements ChatHandlers { const metric = new Metric({ cwsprChatConversationType: 'Chat', }) + + IdleWorkspaceManager.recordActivityTimestamp() + const triggerContext = await this.#getInlineChatTriggerContext(params) let response: ChatCommandOutput diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codePercentage.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codePercentage.ts index 04c7dc5ab1..0513443a35 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codePercentage.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codePercentage.ts @@ -137,10 +137,20 @@ export class CodePercentageTracker { } } + addTotalTokensForEdits(languageId: string, count: number): void { + const languageBucket = this.getLanguageBucket(languageId) + if (count >= INSERT_CUTOFF_THRESHOLD) { + languageBucket.totalTokens += count + } + } + countAcceptedTokens(languageId: string, tokens: string): void { + this.countAcceptedTokensUsingCount(languageId, tokens.length) + } + + countAcceptedTokensUsingCount(languageId: string, count: number): void { const languageBucket = this.getLanguageBucket(languageId) - const tokenCount = tokens.length - languageBucket.acceptedTokens += tokenCount + languageBucket.acceptedTokens += count } countInvocation(languageId: string): void { diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts index 1636c17b84..29390248f7 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.test.ts @@ -61,6 +61,7 @@ import { INVALID_TOKEN } from '../../shared/constants' import { AmazonQError } from '../../shared/amazonQServiceManager/errors' import * as path from 'path' import { CONTEXT_CHARACTERS_LIMIT } from './constants' +import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager' const updateConfiguration = async ( features: TestFeatures, @@ -770,6 +771,22 @@ describe('CodeWhisperer Server', () => { assert.rejects(promise, ResponseError) }) + it('invokes IdleWorkspaceManager recordActivityTimestamp', async () => { + const recordActivityTimestampStub = sinon.stub(IdleWorkspaceManager, 'recordActivityTimestamp') + + await features.doInlineCompletionWithReferences( + { + textDocument: { uri: SOME_FILE.uri }, + position: { line: 0, character: 0 }, + context: { triggerKind: InlineCompletionTriggerKind.Invoked }, + }, + CancellationToken.None + ) + + sinon.assert.calledOnce(recordActivityTimestampStub) + recordActivityTimestampStub.restore() + }) + describe('Supplemental Context', () => { it('should send supplemental context when using token authentication', async () => { const test_service = sinon.createStubInstance( diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts index f2b1a2c43d..23a72e3528 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts @@ -46,7 +46,7 @@ import { UserWrittenCodeTracker } from '../../shared/userWrittenCodeTracker' import { RecentEditTracker, RecentEditTrackerDefaultConfig } from './tracker/codeEditTracker' import { CursorTracker } from './tracker/cursorTracker' import { RejectedEditTracker, DEFAULT_REJECTED_EDIT_TRACKER_CONFIG } from './tracker/rejectedEditTracker' -import { getAddedAndDeletedChars } from './diffUtils' +import { getAddedAndDeletedLines, getCharacterDifferences } from './diffUtils' import { emitPerceivedLatencyTelemetry, emitServiceInvocationFailure, @@ -55,7 +55,9 @@ import { } from './telemetry' import { DocumentChangedListener } from './documentChangedListener' import { EditCompletionHandler } from './editCompletionHandler' -import { EMPTY_RESULT } from './constants' +import { EMPTY_RESULT, ABAP_EXTENSIONS } from './constants' +import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager' +import { URI } from 'vscode-uri' const mergeSuggestionsWithRightContext = ( rightFileContext: string, @@ -141,6 +143,8 @@ export const CodewhispererServerFactory = // 2. it is not designed to handle concurrent changes to these state variables. // when one handler is at the API call stage, it has not yet update the session state // but another request can start, causing the state to be incorrect. + IdleWorkspaceManager.recordActivityTimestamp() + if (isOnInlineCompletionHandlerInProgress) { logging.log(`Skip concurrent inline completion`) return EMPTY_RESULT @@ -158,7 +162,7 @@ export const CodewhispererServerFactory = if (cursorTracker) { cursorTracker.trackPosition(params.textDocument.uri, params.position) } - const textDocument = await workspace.getTextDocument(params.textDocument.uri) + const textDocument = await getTextDocument(params.textDocument.uri, workspace, logging) const codeWhispererService = amazonQServiceManager.getCodewhispererService() if (params.partialResultToken && currentSession) { @@ -498,7 +502,6 @@ export const CodewhispererServerFactory = partialResultToken: suggestionResponse.responseContext.nextToken, } } else { - session.hasEditsPending = suggestionResponse.responseContext.nextToken ? true : false return { items: suggestionResponse.suggestions .map(suggestion => { @@ -619,36 +622,29 @@ export const CodewhispererServerFactory = ) const isAccepted = acceptedItemId ? true : false const acceptedSuggestion = session.suggestions.find(s => s.itemId === acceptedItemId) - let addedCharactersForEditSuggestion = '' - let deletedCharactersForEditSuggestion = '' - if (acceptedSuggestion !== undefined) { - if (acceptedSuggestion) { - codePercentageTracker.countSuccess(session.language) - if (session.suggestionType === SuggestionType.EDIT && acceptedSuggestion.content) { - // [acceptedSuggestion.insertText] will be undefined for NEP suggestion. Use [acceptedSuggestion.content] instead. - // Since [acceptedSuggestion.content] is in the form of a diff, transform the content into addedCharacters and deletedCharacters. - const addedAndDeletedChars = getAddedAndDeletedChars(acceptedSuggestion.content) - if (addedAndDeletedChars) { - addedCharactersForEditSuggestion = addedAndDeletedChars.addedCharacters - deletedCharactersForEditSuggestion = addedAndDeletedChars.deletedCharacters - - codePercentageTracker.countAcceptedTokens( - session.language, - addedCharactersForEditSuggestion - ) - codePercentageTracker.countTotalTokens( - session.language, - addedCharactersForEditSuggestion, - true - ) - enqueueCodeDiffEntry(session, acceptedSuggestion, addedCharactersForEditSuggestion) - } - } else if (acceptedSuggestion.insertText) { - codePercentageTracker.countAcceptedTokens(session.language, acceptedSuggestion.insertText) - codePercentageTracker.countTotalTokens(session.language, acceptedSuggestion.insertText, true) + let addedLengthForEdits = 0 + let deletedLengthForEdits = 0 + if (acceptedSuggestion) { + codePercentageTracker.countSuccess(session.language) + if (session.suggestionType === SuggestionType.EDIT && acceptedSuggestion.content) { + // [acceptedSuggestion.insertText] will be undefined for NEP suggestion. Use [acceptedSuggestion.content] instead. + // Since [acceptedSuggestion.content] is in the form of a diff, transform the content into addedCharacters and deletedCharacters. + const { addedLines, deletedLines } = getAddedAndDeletedLines(acceptedSuggestion.content) + const charDiffResult = getCharacterDifferences(addedLines, deletedLines) + addedLengthForEdits = charDiffResult.charactersAdded + deletedLengthForEdits = charDiffResult.charactersRemoved + + codePercentageTracker.countAcceptedTokensUsingCount( + session.language, + charDiffResult.charactersAdded + ) + codePercentageTracker.addTotalTokensForEdits(session.language, charDiffResult.charactersAdded) + enqueueCodeDiffEntry(session, acceptedSuggestion, addedLines.join('\n')) + } else if (acceptedSuggestion.insertText) { + codePercentageTracker.countAcceptedTokens(session.language, acceptedSuggestion.insertText) + codePercentageTracker.countTotalTokens(session.language, acceptedSuggestion.insertText, true) - enqueueCodeDiffEntry(session, acceptedSuggestion) - } + enqueueCodeDiffEntry(session, acceptedSuggestion) } } @@ -687,21 +683,15 @@ export const CodewhispererServerFactory = if (firstCompletionDisplayLatency) emitPerceivedLatencyTelemetry(telemetry, session) // Always emit user trigger decision at session close - // Close session unless Edit suggestion was accepted with more pending - const shouldKeepSessionOpen = - session.suggestionType === SuggestionType.EDIT && isAccepted && session.hasEditsPending - - if (!shouldKeepSessionOpen) { - completionSessionManager.closeSession(session) - } - const streakLength = editsEnabled ? completionSessionManager.getAndUpdateStreakLength(isAccepted) : 0 + sessionManager.closeSession(session) + const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(isAccepted) : 0 await emitUserTriggerDecisionTelemetry( telemetry, telemetryService, session, timeSinceLastUserModification, - addedCharactersForEditSuggestion.length, - deletedCharactersForEditSuggestion.length, + addedLengthForEdits, + deletedLengthForEdits, addedDiagnostics, removedDiagnostics, streakLength, @@ -722,10 +712,9 @@ export const CodewhispererServerFactory = userWrittenCodeTracker.customizationArn = customizationArn } logging.debug(`CodePercentageTracker customizationArn updated to ${customizationArn}`) - /* - The flag enableTelemetryEventsToDestination is set to true temporarily. It's value will be determined through destination - configuration post all events migration to STE. It'll be replaced by qConfig['enableTelemetryEventsToDestination'] === true - */ + + // The flag enableTelemetryEventsToDestination is set to true temporarily. It's value will be determined through destination + // configuration post all events migration to STE. It'll be replaced by qConfig['enableTelemetryEventsToDestination'] === true // const enableTelemetryEventsToDestination = true // telemetryService.updateEnableTelemetryEventsToDestination(enableTelemetryEventsToDestination) telemetryService.updateOptOutPreference(optOutTelemetryPreference) @@ -902,3 +891,27 @@ export const CodewhispererServerFactory = export const CodeWhispererServerIAM = CodewhispererServerFactory(getOrThrowBaseIAMServiceManager) export const CodeWhispererServerToken = CodewhispererServerFactory(getOrThrowBaseTokenServiceManager) + +const getLanguageIdFromUri = (uri: string, logging?: any): string => { + try { + const extension = uri.split('.').pop()?.toLowerCase() + return ABAP_EXTENSIONS.has(extension || '') ? 'abap' : '' + } catch (err) { + logging?.log(`Error parsing URI to determine language: ${uri}: ${err}`) + return '' + } +} + +const getTextDocument = async (uri: string, workspace: any, logging: any): Promise => { + let textDocument = await workspace.getTextDocument(uri) + if (!textDocument) { + try { + const content = await workspace.fs.readFile(URI.parse(uri).fsPath) + const languageId = getLanguageIdFromUri(uri) + textDocument = TextDocument.create(uri, languageId, 0, content) + } catch (err) { + logging.log(`Unable to load from ${uri}: ${err}`) + } + } + return textDocument +} diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts index 49a33b35de..c5810924b6 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/constants.ts @@ -4,3 +4,28 @@ export const CONTEXT_CHARACTERS_LIMIT = 10240 export const EMPTY_RESULT = { sessionId: '', items: [] } export const EDIT_DEBOUNCE_INTERVAL_MS = 500 export const EDIT_STALE_RETRY_COUNT = 3 +// ABAP ADT extensions commonly used with Eclipse +export const ABAP_EXTENSIONS = new Set([ + 'asprog', + 'aclass', + 'asinc', + 'aint', + 'assrvds', + 'asbdef', + 'asddls', + 'astablds', + 'astabldt', + 'amdp', + 'apack', + 'asrv', + 'aobj', + 'aexit', + 'abdef', + 'acinc', + 'asfugr', + 'apfugr', + 'asfunc', + 'asfinc', + 'apfunc', + 'apfinc', +]) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/diffUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/diffUtils.ts index 66cbeb3f23..0899dd5e46 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/diffUtils.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/diffUtils.ts @@ -213,6 +213,18 @@ export function applyUnifiedDiff(docText: string, unifiedDiff: string): string { } } +export function getAddedAndDeletedLines(unifiedDiff: string): { addedLines: string[]; deletedLines: string[] } { + const lines = unifiedDiff.split('\n') + const addedLines = lines.filter(line => line.startsWith('+') && !line.startsWith('+++')).map(line => line.slice(1)) + const deletedLines = lines + .filter(line => line.startsWith('-') && !line.startsWith('---')) + .map(line => line.slice(1)) + return { + addedLines, + deletedLines, + } +} + // src https://github.com/aws/aws-toolkit-vscode/blob/3921457b0a2094b831beea0d66cc2cbd2a833890/packages/amazonq/src/app/inline/EditRendering/diffUtils.ts#L147 export function getAddedAndDeletedChars(unifiedDiff: string): { addedCharacters: string @@ -254,3 +266,58 @@ export function getAddedAndDeletedChars(unifiedDiff: string): { deletedCharacters, } } + +/** + * Calculate character differences between added and deleted text blocks using LCS + */ +export interface CharDiffResult { + charactersAdded: number + charactersRemoved: number +} + +/** + * Find longest common subsequence length between two strings + */ +function lcsLength(str1: string, str2: string): number[][] { + const m = str1.length + const n = str2.length + const dp = Array(m + 1) + .fill(null) + .map(() => Array(n + 1).fill(0)) + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (str1[i - 1] === str2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1 + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + } + } + } + + return dp +} + +/** + * Calculate character differences between added and deleted blocks + */ +export function getCharacterDifferences(addedLines: string[], deletedLines: string[]): CharDiffResult { + const addedText = addedLines.join('\n') + const deletedText = deletedLines.join('\n') + + if (addedText.length === 0) { + return { charactersAdded: 0, charactersRemoved: deletedText.length } + } + + if (deletedText.length === 0) { + return { charactersAdded: addedText.length, charactersRemoved: 0 } + } + + const lcsTable = lcsLength(deletedText, addedText) + const lcsLen = lcsTable[deletedText.length][addedText.length] + + return { + charactersAdded: addedText.length - lcsLen, + charactersRemoved: deletedText.length - lcsLen, + } +} diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts index 5b3e7519f4..3c72e368f4 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts @@ -120,16 +120,35 @@ export class EditCompletionHandler { } if (params.partialResultToken && currentSession) { + // Close ACTIVE session. We shouldn't record Discard trigger decision for trigger with nextToken. + if (currentSession && currentSession.state === 'ACTIVE') { + this.sessionManager.discardSession(currentSession) + } + + const newSession = this.sessionManager.createSession({ + document: textDocument, + startPosition: params.position, + triggerType: 'AutoTrigger', + language: currentSession.language, + requestContext: currentSession.requestContext, + autoTriggerType: undefined, + triggerCharacter: '', + classifierResult: undefined, + classifierThreshold: undefined, + credentialStartUrl: currentSession.credentialStartUrl, + supplementalMetadata: currentSession.supplementalMetadata, + customizationArn: currentSession.customizationArn, + }) // subsequent paginated requests for current session try { const suggestionResponse = await this.codeWhispererService.generateSuggestions({ - ...currentSession.requestContext, + ...newSession.requestContext, nextToken: `${params.partialResultToken}`, }) return await this.processSuggestionResponse( suggestionResponse, - currentSession, - false, + newSession, + true, params.context.selectedCompletionInfo?.range ) } catch (error) { @@ -256,16 +275,6 @@ export class EditCompletionHandler { // Close ACTIVE session and record Discard trigger decision immediately if (currentSession && currentSession.state === 'ACTIVE') { - if (this.editsEnabled && currentSession.suggestionType === SuggestionType.EDIT) { - const mergedSuggestions = mergeEditSuggestionsWithFileContext(currentSession, textDocument, fileContext) - - if (mergedSuggestions.length > 0) { - return { - items: mergedSuggestions, - sessionId: currentSession.id, - } - } - } // Emit user trigger decision at session close time for active session this.sessionManager.discardSession(currentSession) const streakLength = this.editsEnabled ? this.sessionManager.getAndUpdateStreakLength(false) : 0 diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts index 8ac71737cf..cb873a2920 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts @@ -80,7 +80,6 @@ export class CodeWhispererSession { includeImportsWithSuggestions?: boolean codewhispererSuggestionImportCount: number = 0 suggestionType?: string - hasEditsPending?: boolean = false // Track the most recent itemId for paginated Edit suggestions constructor(data: SessionData) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts index b990ccfceb..d53d141a2b 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/telemetry.ts @@ -147,13 +147,7 @@ export const emitUserTriggerDecisionTelemetry = async ( streakLength ) - // Mark telemetry as complete unless Edit suggestion was accepted with more pending - const hasPendingEditTelemetry = - session.suggestionType === SuggestionType.EDIT && session.acceptedSuggestionId && session.hasEditsPending - - if (!hasPendingEditTelemetry) { - session.reportedUserDecision = true - } + session.reportedUserDecision = true } export const emitAggregatedUserTriggerDecisionTelemetry = ( diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.test.ts new file mode 100644 index 0000000000..a5d81526c9 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.test.ts @@ -0,0 +1,75 @@ +import { IdleWorkspaceManager } from './IdleWorkspaceManager' +import { WorkspaceFolderManager } from './workspaceFolderManager' +import sinon, { stubInterface, StubbedInstance } from 'ts-sinon' + +describe('IdleWorkspaceManager', () => { + let clock: sinon.SinonFakeTimers + let mockWorkspaceFolderManager: StubbedInstance + + beforeEach(() => { + clock = sinon.useFakeTimers() + mockWorkspaceFolderManager = stubInterface() + sinon.stub(WorkspaceFolderManager, 'getInstance').returns(mockWorkspaceFolderManager) + sinon.stub(console, 'error') + }) + + afterEach(() => { + clock.restore() + sinon.restore() + }) + + describe('isSessionIdle', () => { + it('should return false when session is not idle', () => { + IdleWorkspaceManager.recordActivityTimestamp() + + const result = IdleWorkspaceManager.isSessionIdle() + + expect(result).toBe(false) + }) + + it('should return true when session exceeds idle threshold', () => { + IdleWorkspaceManager.recordActivityTimestamp() + clock.tick(31 * 60 * 1000) // 31 minutes + + const result = IdleWorkspaceManager.isSessionIdle() + + expect(result).toBe(true) + }) + }) + + describe('recordActivityTimestamp', () => { + it('should update activity timestamp', async () => { + IdleWorkspaceManager.recordActivityTimestamp() + + expect(IdleWorkspaceManager.isSessionIdle()).toBe(false) + }) + + it('should not trigger workspace check when session was not idle', async () => { + mockWorkspaceFolderManager.isContinuousMonitoringStopped.returns(false) + + IdleWorkspaceManager.recordActivityTimestamp() + + sinon.assert.notCalled(mockWorkspaceFolderManager.checkRemoteWorkspaceStatusAndReact) + }) + + it('should trigger workspace check when session was idle and monitoring is active', async () => { + // Make session idle first + clock.tick(31 * 60 * 1000) + mockWorkspaceFolderManager.isContinuousMonitoringStopped.returns(false) + mockWorkspaceFolderManager.checkRemoteWorkspaceStatusAndReact.resolves() + + IdleWorkspaceManager.recordActivityTimestamp() + + sinon.assert.calledOnce(mockWorkspaceFolderManager.checkRemoteWorkspaceStatusAndReact) + }) + + it('should not trigger workspace check when session was idle but monitoring is stopped', async () => { + clock.tick(31 * 60 * 1000) + mockWorkspaceFolderManager.isContinuousMonitoringStopped.returns(true) + + IdleWorkspaceManager.recordActivityTimestamp() + + sinon.assert.notCalled(mockWorkspaceFolderManager.checkRemoteWorkspaceStatusAndReact) + }) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts new file mode 100644 index 0000000000..5a8359ccac --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts @@ -0,0 +1,38 @@ +import { WorkspaceFolderManager } from './workspaceFolderManager' + +export class IdleWorkspaceManager { + private static readonly idleThreshold = 30 * 60 * 1000 // 30 minutes + private static lastActivityTimestamp = 0 // treat session as idle as the start + + private constructor() {} + + /** + * Records activity timestamp and triggers workspace status check if session was idle. + * + * When transitioning from idle to active, proactively checks remote workspace status + * (if continuous monitoring is enabled) without blocking the current operation. + */ + public static recordActivityTimestamp(): void { + try { + const wasSessionIdle = IdleWorkspaceManager.isSessionIdle() + IdleWorkspaceManager.lastActivityTimestamp = Date.now() + + const workspaceFolderManager = WorkspaceFolderManager.getInstance() + if (workspaceFolderManager && wasSessionIdle && !workspaceFolderManager.isContinuousMonitoringStopped()) { + // Proactively check the remote workspace status instead of waiting for the next scheduled check + // Fire and forget - don't await to avoid blocking + workspaceFolderManager.checkRemoteWorkspaceStatusAndReact().catch(err => { + // ignore errors + }) + } + } catch (err) { + // ignore errors + } + } + + public static isSessionIdle(): boolean { + const currentTime = Date.now() + const timeSinceLastActivity = currentTime - IdleWorkspaceManager.lastActivityTimestamp + return timeSinceLastActivity > IdleWorkspaceManager.idleThreshold + } +} diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts index 9518f9629a..dfedb0c913 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts @@ -694,6 +694,21 @@ export class ArtifactManager { additionalFiles.push(...updatedFiles) } + // Fallback methods to generate a .classpath at workspace folder root + if (projectRoots.length == 0) { + const javaManager = new JavaProjectAnalyzer(workspacePath) + const structure = await javaManager.analyze() + const generator = new EclipseConfigGenerator(workspaceFolder, this.logging) + const classpathFiles = await generator.generateDotClasspath(structure) + for (const classpathFile of classpathFiles) { + additionalFiles.push(classpathFile) + } + const projectFiles = await generator.generateDotProject(path.basename(workspacePath), structure) + for (const projectFile of projectFiles) { + additionalFiles.push(projectFile) + } + } + return [...files, ...additionalFiles] } diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/dependency/dependencyDiscoverer.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/dependency/dependencyDiscoverer.ts index 8a4a9755cb..ed387ca2ca 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/dependency/dependencyDiscoverer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/dependency/dependencyDiscoverer.ts @@ -191,7 +191,8 @@ export class DependencyDiscoverer { } } - public resetFromDisposal(): void { + public disposeAndReset(): void { + this.dispose() this.sharedState.isDisposed = false this.sharedState.dependencyUploadedSizeSum = 0 } diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts index 09336ffb2f..c536c1087d 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts @@ -221,7 +221,6 @@ export const WorkspaceContextServer = (): Server => features => { isLoggedInUsingBearerToken(credentialsProvider) && abTestingEnabled && !workspaceFolderManager.getOptOutStatus() && - !workspaceFolderManager.getServiceQuotaExceededStatus() && workspaceIdentifier ) } @@ -303,17 +302,15 @@ export const WorkspaceContextServer = (): Server => features => { await evaluateABTesting() isWorkflowInitialized = true - workspaceFolderManager.resetAdminOptOutAndServiceQuotaStatus() + workspaceFolderManager.resetAdminOptOutStatus() if (!isUserEligibleForWorkspaceContext()) { return } fileUploadJobManager.startFileUploadJobConsumer() dependencyEventBundler.startDependencyEventBundler() - await Promise.all([ - workspaceFolderManager.initializeWorkspaceStatusMonitor(), - workspaceFolderManager.processNewWorkspaceFolders(workspaceFolders), - ]) + + workspaceFolderManager.initializeWorkspaceStatusMonitor() logging.log(`Workspace context workflow initialized`) } else if (!isLoggedIn) { if (isWorkflowInitialized) { diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts index 0b4ec53f25..7ab596a930 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.test.ts @@ -6,8 +6,8 @@ import { DependencyDiscoverer } from './dependency/dependencyDiscoverer' import { WorkspaceFolder } from 'vscode-languageserver-protocol' import { ArtifactManager } from './artifactManager' import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' -import { CreateWorkspaceResponse } from '../../client/token/codewhispererbearertokenclient' -import { AWSError } from 'aws-sdk' +import { ListWorkspaceMetadataResponse } from '../../client/token/codewhispererbearertokenclient' +import { IdleWorkspaceManager } from './IdleWorkspaceManager' describe('WorkspaceFolderManager', () => { let mockServiceManager: StubbedInstance @@ -33,8 +33,8 @@ describe('WorkspaceFolderManager', () => { sinon.restore() }) - describe('getServiceQuotaExceededStatus', () => { - it('should return true when service quota is exceeded', async () => { + describe('checkRemoteWorkspaceStatusAndReact', () => { + it('should check and react when IDE session is not idle', async () => { // Setup const workspaceFolders: WorkspaceFolder[] = [ { @@ -43,17 +43,20 @@ describe('WorkspaceFolderManager', () => { }, ] - // Mock the createWorkspace method to throw a ServiceQuotaExceededException - const mockError: AWSError = { - name: 'ServiceQuotaExceededException', - message: 'You have too many active running workspaces.', - code: 'ServiceQuotaExceededException', - time: new Date(), - retryable: false, - statusCode: 400, + // Mock IdleSessionManager to return false (not idle) + sinon.stub(IdleWorkspaceManager, 'isSessionIdle').returns(false) + + // Mock successful response + const mockResponse: ListWorkspaceMetadataResponse = { + workspaces: [ + { + workspaceId: 'test-workspace-id', + workspaceStatus: 'CREATED', + }, + ], } - mockCodeWhispererService.createWorkspace.rejects(mockError) + mockCodeWhispererService.listWorkspaceMetadata.resolves(mockResponse as any) // Create the WorkspaceFolderManager instance using the static createInstance method workspaceFolderManager = WorkspaceFolderManager.createInstance( @@ -66,23 +69,24 @@ describe('WorkspaceFolderManager', () => { 'test-workspace-identifier' ) - // Spy on clearAllWorkspaceResources and related methods - const clearAllWorkspaceResourcesSpy = sinon.stub( + // Spy on resetWebSocketClient + const resetWebSocketClientSpy = sinon.stub(workspaceFolderManager as any, 'resetWebSocketClient') + + // Spy on handleWorkspaceCreatedState + const handleWorkspaceCreatedStateSpy = sinon.stub( workspaceFolderManager as any, - 'clearAllWorkspaceResources' + 'handleWorkspaceCreatedState' ) - // Act - trigger the createNewWorkspace method which sets isServiceQuotaExceeded - await (workspaceFolderManager as any).createNewWorkspace() + // Act - trigger the checkRemoteWorkspaceStatusAndReact method + await workspaceFolderManager.checkRemoteWorkspaceStatusAndReact() - // Assert - expect(workspaceFolderManager.getServiceQuotaExceededStatus()).toBe(true) - - // Verify that clearAllWorkspaceResources was called - sinon.assert.calledOnce(clearAllWorkspaceResourcesSpy) + // Verify that resetWebSocketClient was called once + sinon.assert.notCalled(resetWebSocketClientSpy) + sinon.assert.calledOnce(handleWorkspaceCreatedStateSpy) }) - it('should return false when service quota is not exceeded', async () => { + it('should skip checking and reacting when IDE session is idle', async () => { // Setup const workspaceFolders: WorkspaceFolder[] = [ { @@ -91,15 +95,20 @@ describe('WorkspaceFolderManager', () => { }, ] + // Mock IdleSessionManager to return true (idle) + sinon.stub(IdleWorkspaceManager, 'isSessionIdle').returns(true) + // Mock successful response - const mockResponse: CreateWorkspaceResponse = { - workspace: { - workspaceId: 'test-workspace-id', - workspaceStatus: 'RUNNING', - }, + const mockResponse: ListWorkspaceMetadataResponse = { + workspaces: [ + { + workspaceId: 'test-workspace-id', + workspaceStatus: 'CREATED', + }, + ], } - mockCodeWhispererService.createWorkspace.resolves(mockResponse as any) + mockCodeWhispererService.listWorkspaceMetadata.resolves(mockResponse as any) // Create the WorkspaceFolderManager instance using the static createInstance method workspaceFolderManager = WorkspaceFolderManager.createInstance( @@ -112,20 +121,18 @@ describe('WorkspaceFolderManager', () => { 'test-workspace-identifier' ) - // Spy on clearAllWorkspaceResources - const clearAllWorkspaceResourcesSpy = sinon.stub( - workspaceFolderManager as any, - 'clearAllWorkspaceResources' - ) + // Spy on resetWebSocketClient + const resetWebSocketClientSpy = sinon.stub(workspaceFolderManager as any, 'resetWebSocketClient') - // Act - trigger the createNewWorkspace method - await (workspaceFolderManager as any).createNewWorkspace() + // Act - trigger the checkRemoteWorkspaceStatusAndReact method + await workspaceFolderManager.checkRemoteWorkspaceStatusAndReact() - // Assert - expect(workspaceFolderManager.getServiceQuotaExceededStatus()).toBe(false) - - // Verify that clearAllWorkspaceResources was not called - sinon.assert.notCalled(clearAllWorkspaceResourcesSpy) + // Verify that resetWebSocketClient was called once + sinon.assert.calledOnce(resetWebSocketClientSpy) + sinon.assert.calledWith( + mockLogging.log, + sinon.match(/Session is idle, skipping remote workspace status check/) + ) }) }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts index 00048cd62f..99fc9c4628 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts @@ -20,6 +20,7 @@ import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/A import { URI } from 'vscode-uri' import path = require('path') import { isAwsError } from '../../shared/utils' +import { IdleWorkspaceManager } from './IdleWorkspaceManager' interface WorkspaceState { remoteWorkspaceState: WorkspaceStatus @@ -54,8 +55,8 @@ export class WorkspaceFolderManager { private optOutMonitorInterval: NodeJS.Timeout | undefined private messageQueueConsumerInterval: NodeJS.Timeout | undefined private isOptedOut: boolean = false - // Tracks if the user has reached their maximum allowed remote workspaces quota - private isServiceQuotaExceeded: boolean = false + private isCheckingRemoteWorkspaceStatus: boolean = false + private isArtifactUploadedToRemoteWorkspace: boolean = false static createInstance( serviceManager: AmazonQTokenServiceManager, @@ -138,13 +139,8 @@ export class WorkspaceFolderManager { return this.isOptedOut } - getServiceQuotaExceededStatus(): boolean { - return this.isServiceQuotaExceeded - } - - resetAdminOptOutAndServiceQuotaStatus(): void { + resetAdminOptOutStatus(): void { this.isOptedOut = false - this.isServiceQuotaExceeded = false } getWorkspaceState(): WorkspaceState { @@ -324,14 +320,13 @@ export class WorkspaceFolderManager { this.workspaceState.webSocketClient = webSocketClient } - async initializeWorkspaceStatusMonitor() { + initializeWorkspaceStatusMonitor() { this.logging.log(`Initializing workspace status check for workspace [${this.workspaceIdentifier}]`) // Reset workspace ID to force operations to wait for new remote workspace information this.resetRemoteWorkspaceId() - this.artifactManager.resetFromDisposal() - this.dependencyDiscoverer.resetFromDisposal() + this.isArtifactUploadedToRemoteWorkspace = false // Set up message queue consumer if (this.messageQueueConsumerInterval === undefined) { @@ -350,12 +345,8 @@ export class WorkspaceFolderManager { }, this.MESSAGE_PUBLISH_INTERVAL) } - // Perform a one-time checkRemoteWorkspaceStatusAndReact first - // Pass skipUploads as true since it would be handled by processNewWorkspaceFolders - await this.checkRemoteWorkspaceStatusAndReact(true) - // Set up continuous monitoring which periodically invokes checkRemoteWorkspaceStatusAndReact - if (!this.isOptedOut && !this.isServiceQuotaExceeded && this.continuousMonitorInterval === undefined) { + if (!this.isOptedOut && this.continuousMonitorInterval === undefined) { this.logging.log(`Starting continuous monitor for workspace [${this.workspaceIdentifier}]`) this.continuousMonitorInterval = setInterval(async () => { try { @@ -427,61 +418,92 @@ export class WorkspaceFolderManager { }) } - private async checkRemoteWorkspaceStatusAndReact(skipUploads: boolean = false) { - if (this.workspaceFolders.length === 0) { - this.logging.log(`No workspace folders added, skipping workspace status check`) + public async checkRemoteWorkspaceStatusAndReact() { + if (this.isCheckingRemoteWorkspaceStatus) { + // Skip checking remote workspace if a previous check is still in progress return } + this.isCheckingRemoteWorkspaceStatus = true + try { + if (IdleWorkspaceManager.isSessionIdle()) { + this.resetWebSocketClient() + this.logging.log('Session is idle, skipping remote workspace status check') + return + } - this.logging.log(`Checking remote workspace status for workspace [${this.workspaceIdentifier}]`) - const { metadata, optOut, error } = await this.listWorkspaceMetadata(this.workspaceIdentifier) + if (this.workspaceFolders.length === 0) { + this.logging.log(`No workspace folders added, skipping workspace status check`) + return + } - if (optOut) { - this.logging.log('User opted out, clearing all resources and starting opt-out monitor') - this.isOptedOut = true - this.clearAllWorkspaceResources() - this.startOptOutMonitor() - return - } + this.logging.log(`Checking remote workspace status for workspace [${this.workspaceIdentifier}]`) + const { metadata, optOut, error } = await this.listWorkspaceMetadata(this.workspaceIdentifier) - if (error) { - // Do not do anything if we received an exception but not caused by optOut - return - } + if (optOut) { + this.logging.log('User opted out, clearing all resources and starting opt-out monitor') + this.isOptedOut = true + this.clearAllWorkspaceResources() + this.startOptOutMonitor() + return + } - if (!metadata) { - // Workspace no longer exists, Recreate it. - this.resetRemoteWorkspaceId() // workspaceId would change if remote record is gone - await this.handleWorkspaceCreatedState(skipUploads) - return - } + if (error) { + // Do not do anything if we received an exception but not caused by optOut + return + } - this.workspaceState.remoteWorkspaceState = metadata.workspaceStatus - if (this.workspaceState.workspaceId === undefined) { - this.setRemoteWorkspaceId(metadata.workspaceId) - } + if (!metadata) { + // Workspace no longer exists, Recreate it. + this.resetRemoteWorkspaceId() // workspaceId would change if remote record is gone + await this.handleWorkspaceCreatedState() + return + } - switch (metadata.workspaceStatus) { - case 'READY': - // Check if connection exists - const client = this.workspaceState.webSocketClient - if (!client || !client.isConnected()) { - this.logging.log( - `Workspace is ready but no connection exists or connection lost. Re-establishing connection...` - ) - await this.establishConnection(metadata) - } - break - case 'PENDING': - // Schedule an initial connection when pending - await this.waitForInitialConnection() - break - case 'CREATED': - // Workspace has no environment, Recreate it. - await this.handleWorkspaceCreatedState(skipUploads) - break - default: - this.logging.warn(`Unknown workspace status: ${metadata.workspaceStatus}`) + this.workspaceState.remoteWorkspaceState = metadata.workspaceStatus + if (this.workspaceState.workspaceId === undefined) { + this.setRemoteWorkspaceId(metadata.workspaceId) + } + + switch (metadata.workspaceStatus) { + case 'READY': + // Check if connection exists + const client = this.workspaceState.webSocketClient + if (!client || !client.isConnected()) { + this.logging.log( + `Workspace is ready but no connection exists or connection lost. Re-establishing connection...` + ) + let uploadArtifactsPromise: Promise | undefined + if (!this.isArtifactUploadedToRemoteWorkspace) { + uploadArtifactsPromise = this.uploadAllArtifactsToRemoteWorkspace() + } + await this.establishConnection(metadata) + if (uploadArtifactsPromise) { + await uploadArtifactsPromise + } + } + break + case 'PENDING': + // Schedule an initial connection when pending + let uploadArtifactsPromise: Promise | undefined + if (!this.isArtifactUploadedToRemoteWorkspace) { + uploadArtifactsPromise = this.uploadAllArtifactsToRemoteWorkspace() + } + await this.waitForInitialConnection() + if (uploadArtifactsPromise) { + await uploadArtifactsPromise + } + break + case 'CREATED': + // Workspace has no environment, Recreate it. + await this.handleWorkspaceCreatedState() + break + default: + this.logging.warn(`Unknown workspace status: ${metadata.workspaceStatus}`) + } + } catch (error) { + this.logging.error(`Error checking remote workspace status: ${error}`) + } finally { + this.isCheckingRemoteWorkspaceStatus = false } } @@ -515,9 +537,7 @@ export class WorkspaceFolderManager { ) clearInterval(intervalId) this.optOutMonitorInterval = undefined - this.initializeWorkspaceStatusMonitor().catch(error => { - this.logging.error(`Error while initializing workspace status monitoring: ${error}`) - }) + this.initializeWorkspaceStatusMonitor() this.processNewWorkspaceFolders(this.workspaceFolders).catch(error => { this.logging.error(`Error while processing workspace folders: ${error}`) }) @@ -530,25 +550,18 @@ export class WorkspaceFolderManager { } } - private async handleWorkspaceCreatedState(skipUploads: boolean = false): Promise { + private async handleWorkspaceCreatedState(): Promise { this.logging.log(`No READY / PENDING remote workspace found, creating a new one`) // If remote state is CREATED, call create API to create a new workspace - if (this.workspaceState.webSocketClient) { - this.workspaceState.webSocketClient.destroyClient() - this.workspaceState.webSocketClient = undefined - } + this.resetWebSocketClient() const initialResult = await this.createNewWorkspace() // If creation succeeds, establish connection if (initialResult.response) { this.logging.log(`Workspace [${this.workspaceIdentifier}] created successfully, establishing connection`) + const uploadArtifactsPromise = this.uploadAllArtifactsToRemoteWorkspace() await this.waitForInitialConnection() - if (!skipUploads) { - await this.syncSourceCodesToS3(this.workspaceFolders) - this.dependencyDiscoverer.reSyncDependenciesToS3(this.workspaceFolders).catch(e => { - this.logging.warn(`Error during re-syncing dependencies: ${e}`) - }) - } + await uploadArtifactsPromise return } @@ -570,13 +583,27 @@ export class WorkspaceFolderManager { } this.logging.log(`Retry succeeded for workspace creation, establishing connection`) + const uploadArtifactsPromise = this.uploadAllArtifactsToRemoteWorkspace() await this.waitForInitialConnection() - if (!skipUploads) { - await this.syncSourceCodesToS3(this.workspaceFolders) - this.dependencyDiscoverer.reSyncDependenciesToS3(this.workspaceFolders).catch(e => { - this.logging.warn(`Error during re-syncing dependencies: ${e}`) - }) - } + await uploadArtifactsPromise + } + + private async uploadAllArtifactsToRemoteWorkspace() { + // initialize source codes + this.artifactManager.resetFromDisposal() + await this.syncSourceCodesToS3(this.workspaceFolders) + + // initialize dependencies + this.dependencyDiscoverer.disposeAndReset() + this.dependencyDiscoverer.searchDependencies(this.workspaceFolders).catch(e => { + this.logging.warn(`Error during dependency discovery: ${e}`) + }) + + this.isArtifactUploadedToRemoteWorkspace = true + } + + public isContinuousMonitoringStopped(): boolean { + return this.continuousMonitorInterval === undefined } private stopContinuousMonitoring() { @@ -602,15 +629,15 @@ export class WorkspaceFolderManager { } } - private async createNewWorkspace() { - const createWorkspaceResult = await this.createWorkspace(this.workspaceIdentifier) - - this.isServiceQuotaExceeded = createWorkspaceResult.isServiceQuotaExceeded - if (this.isServiceQuotaExceeded) { - // Stop continuous monitor and all actions - this.clearAllWorkspaceResources() + private resetWebSocketClient() { + if (this.workspaceState.webSocketClient) { + this.workspaceState.webSocketClient.destroyClient() + this.workspaceState.webSocketClient = undefined } + } + private async createNewWorkspace() { + const createWorkspaceResult = await this.createWorkspace(this.workspaceIdentifier) const workspaceDetails = createWorkspaceResult.response if (!workspaceDetails) { this.logging.warn(`Failed to create remote workspace for [${this.workspaceIdentifier}]`) diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts index 32233e1cf8..a162ff44e6 100644 --- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts @@ -439,7 +439,7 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { this.logging.info( `GenerateCompletion request: "endpoint": ${this.codeWhispererEndpoint}, - "predictionType": ${request.predictionTypes?.toString() ?? ''}, + "predictionType": ${request.predictionTypes?.toString() ?? 'Not specified (COMPLETIONS)'}, "filename": ${request.fileContext.filename}, "language": ${request.fileContext.programmingLanguage.languageName}, "supplementalContextCount": ${request.supplementalContexts?.length ?? 0}, diff --git a/server/aws-lsp-codewhisperer/src/shared/utils.test.ts b/server/aws-lsp-codewhisperer/src/shared/utils.test.ts index e83d04bbb3..85cd8817a7 100644 --- a/server/aws-lsp-codewhisperer/src/shared/utils.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/utils.test.ts @@ -134,26 +134,36 @@ describe('getClientName', () => { }) describe('getOriginFromClientInfo', () => { - it('returns MD_IDE for SMUS-IDE client name', () => { + it('returns MD_IDE for client names starting with SMUS-IDE prefix', () => { const result = getOriginFromClientInfo('AmazonQ-For-SMUS-IDE-1.0.0') assert.strictEqual(result, 'MD_IDE') }) - it('returns MD_IDE for SMUS-CE client name', () => { + it('returns MD_IDE for client names starting with SMUS-CE prefix', () => { const result = getOriginFromClientInfo('AmazonQ-For-SMUS-CE-1.0.0') assert.strictEqual(result, 'MD_IDE') }) - it('returns MD_IDE for client names starting with SMUS-IDE prefix', () => { + it('returns MD_IDE for client names starting with SMAI-CE prefix', () => { + const result = getOriginFromClientInfo('AmazonQ-For-SMAI-CE-1.0.0') + assert.strictEqual(result, 'MD_IDE') + }) + + it('returns MD_IDE for SMUS-IDE client name', () => { const result = getOriginFromClientInfo('AmazonQ-For-SMUS-IDE') assert.strictEqual(result, 'MD_IDE') }) - it('returns MD_IDE for client names starting with SMUS-CE prefix', () => { + it('returns MD_IDE for SMUS-CE client name', () => { const result = getOriginFromClientInfo('AmazonQ-For-SMUS-CE') assert.strictEqual(result, 'MD_IDE') }) + it('returns MD_IDE for SMAI-CE client name', () => { + const result = getOriginFromClientInfo('AmazonQ-For-SMAI-CE') + assert.strictEqual(result, 'MD_IDE') + }) + it('returns IDE for non-SMUS client name', () => { const result = getOriginFromClientInfo('VSCode-Extension') assert.strictEqual(result, 'IDE') diff --git a/server/aws-lsp-codewhisperer/src/shared/utils.ts b/server/aws-lsp-codewhisperer/src/shared/utils.ts index e178de4a51..a7a95d8801 100644 --- a/server/aws-lsp-codewhisperer/src/shared/utils.ts +++ b/server/aws-lsp-codewhisperer/src/shared/utils.ts @@ -381,7 +381,12 @@ export function getClientName(lspParams: InitializeParams | undefined): string | } export function getOriginFromClientInfo(clientName: string | undefined): Origin { - if (clientName?.startsWith('AmazonQ-For-SMUS-IDE') || clientName?.startsWith('AmazonQ-For-SMUS-CE')) { + // TODO: Update with a new origin for SMAI case, as a short-term solution Sagemaker AI CE is using same origin as that of Sagemaker Unified Studio's IDE and CE + if ( + clientName?.startsWith('AmazonQ-For-SMUS-IDE') || + clientName?.startsWith('AmazonQ-For-SMUS-CE') || + clientName?.startsWith('AmazonQ-For-SMAI-CE') + ) { return 'MD_IDE' } return 'IDE' diff --git a/server/aws-lsp-identity/package.json b/server/aws-lsp-identity/package.json index 929bee5583..e8cb3b2c8a 100644 --- a/server/aws-lsp-identity/package.json +++ b/server/aws-lsp-identity/package.json @@ -26,7 +26,7 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", diff --git a/server/aws-lsp-json/package.json b/server/aws-lsp-json/package.json index df4d04e39a..298f3820f0 100644 --- a/server/aws-lsp-json/package.json +++ b/server/aws-lsp-json/package.json @@ -26,7 +26,7 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-notification/package.json b/server/aws-lsp-notification/package.json index 6cc3037232..a9e3cf9d4c 100644 --- a/server/aws-lsp-notification/package.json +++ b/server/aws-lsp-notification/package.json @@ -22,7 +22,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, diff --git a/server/aws-lsp-partiql/package.json b/server/aws-lsp-partiql/package.json index 620b3ba16b..9ff685cfdc 100644 --- a/server/aws-lsp-partiql/package.json +++ b/server/aws-lsp-partiql/package.json @@ -24,7 +24,7 @@ "out" ], "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" diff --git a/server/aws-lsp-s3/package.json b/server/aws-lsp-s3/package.json index e971b6af08..caa7801b5f 100644 --- a/server/aws-lsp-s3/package.json +++ b/server/aws-lsp-s3/package.json @@ -9,7 +9,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" diff --git a/server/aws-lsp-yaml/package.json b/server/aws-lsp-yaml/package.json index 2231bd884a..58fd5e591d 100644 --- a/server/aws-lsp-yaml/package.json +++ b/server/aws-lsp-yaml/package.json @@ -26,7 +26,7 @@ "postinstall": "node patchYamlPackage.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "@aws/lsp-core": "^0.0.13", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", diff --git a/server/device-sso-auth-lsp/package.json b/server/device-sso-auth-lsp/package.json index 529b59ad6b..573170a3a9 100644 --- a/server/device-sso-auth-lsp/package.json +++ b/server/device-sso-auth-lsp/package.json @@ -7,7 +7,7 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/hello-world-lsp/package.json b/server/hello-world-lsp/package.json index e24f1d0ff7..827509803e 100644 --- a/server/hello-world-lsp/package.json +++ b/server/hello-world-lsp/package.json @@ -13,7 +13,7 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.121", + "@aws/language-server-runtimes": "^0.2.123", "vscode-languageserver": "^9.0.1" }, "devDependencies": {