diff --git a/.github/workflows/lsp-ci.yaml b/.github/workflows/lsp-ci.yaml index f717b46ee1..bda44ec74a 100644 --- a/.github/workflows/lsp-ci.yaml +++ b/.github/workflows/lsp-ci.yaml @@ -20,9 +20,15 @@ jobs: run: | npm ci npm run check:formatting - - name: Test + - name: Test with Coverage run: | - npm run test + npm run test:coverage + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v5 + with: + flags: unittests + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} build: name: Package runs-on: ubuntu-latest diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 008d875b1c..fc20ebb843 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,9 +1,9 @@ { - "chat-client": "0.1.24", - "core/aws-lsp-core": "0.0.11", - "server/aws-lsp-antlr4": "0.1.15", - "server/aws-lsp-codewhisperer": "0.0.65", - "server/aws-lsp-json": "0.1.15", - "server/aws-lsp-partiql": "0.0.14", - "server/aws-lsp-yaml": "0.1.15" + "chat-client": "0.1.25", + "core/aws-lsp-core": "0.0.12", + "server/aws-lsp-antlr4": "0.1.16", + "server/aws-lsp-codewhisperer": "0.0.66", + "server/aws-lsp-json": "0.1.16", + "server/aws-lsp-partiql": "0.0.15", + "server/aws-lsp-yaml": "0.1.16" } diff --git a/README.md b/README.md index e559c296a0..fad70742fe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Language Servers for AWS +[![codecov](https://codecov.io/github/aws/language-servers/graph/badge.svg?token=ZSHpIVkG8S)](https://codecov.io/github/aws/language-servers) + Language servers for integration with IDEs and Editors, which implement the protocol (LSP extensions) defined in the [language-server-runtimes](https://github.com/aws/language-server-runtimes/tree/main/runtimes) repo. ## Where things go diff --git a/app/aws-lsp-antlr4-runtimes/package.json b/app/aws-lsp-antlr4-runtimes/package.json index 6a0a52f760..9140397279 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.105", + "@aws/language-server-runtimes": "^0.2.112", "@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 aeaa60226f..50f049d945 100644 --- a/app/aws-lsp-buildspec-runtimes/package.json +++ b/app/aws-lsp-buildspec-runtimes/package.json @@ -7,6 +7,7 @@ "compile": "tsc --build" }, "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@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 817591a6b6..ab11794d3b 100644 --- a/app/aws-lsp-cloudformation-runtimes/package.json +++ b/app/aws-lsp-cloudformation-runtimes/package.json @@ -7,6 +7,7 @@ "compile": "tsc --build" }, "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-cloudformation": "^0.0.1" } } diff --git a/app/aws-lsp-codewhisperer-runtimes/package.json b/app/aws-lsp-codewhisperer-runtimes/package.json index e3098f3784..85d68fb736 100644 --- a/app/aws-lsp-codewhisperer-runtimes/package.json +++ b/app/aws-lsp-codewhisperer-runtimes/package.json @@ -15,7 +15,7 @@ "local-build": "node scripts/local-build.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/app/aws-lsp-identity-runtimes/package.json b/app/aws-lsp-identity-runtimes/package.json index 745bfc2020..44737e1759 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.105", + "@aws/language-server-runtimes": "^0.2.112", "@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 91fcc2fa77..9e8068dbb3 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.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-json": "*" }, "devDependencies": { diff --git a/app/aws-lsp-notification-runtimes/package.json b/app/aws-lsp-notification-runtimes/package.json index edad7a094d..c2239ac481 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.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-notification": "^0.0.1" } } diff --git a/app/aws-lsp-partiql-runtimes/package.json b/app/aws-lsp-partiql-runtimes/package.json index 3391f57bac..0b8d41bd79 100644 --- a/app/aws-lsp-partiql-runtimes/package.json +++ b/app/aws-lsp-partiql-runtimes/package.json @@ -11,7 +11,7 @@ "package": "npm run compile && npm run compile:webpack" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.89", + "@aws/language-server-runtimes": "^0.2.109", "@aws/lsp-partiql": "^0.0.5" }, "devDependencies": { diff --git a/app/aws-lsp-s3-runtimes/package.json b/app/aws-lsp-s3-runtimes/package.json index 94e6880b7a..d1e0062658 100644 --- a/app/aws-lsp-s3-runtimes/package.json +++ b/app/aws-lsp-s3-runtimes/package.json @@ -10,6 +10,7 @@ "compile": "tsc --build" }, "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@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 7b794693d0..fc288cd7c2 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.105", + "@aws/language-server-runtimes": "^0.2.112", "@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 3bb7d51d44..5dc2b4b4ad 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.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-yaml": "*" }, "devDependencies": { diff --git a/app/hello-world-lsp-runtimes/package.json b/app/hello-world-lsp-runtimes/package.json index f7e0d97049..2ed83d1499 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.105" + "@aws/language-server-runtimes": "^0.2.112" }, "devDependencies": { "@types/chai": "^4.3.5", diff --git a/chat-client/.c8rc.json b/chat-client/.c8rc.json new file mode 100644 index 0000000000..d70dc0699d --- /dev/null +++ b/chat-client/.c8rc.json @@ -0,0 +1,12 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text", "html", "lcov"], + "reports-dir": "coverage", + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts", "src/**/test/**", "src/**/*.d.ts"], + "branches": 80, + "lines": 80, + "functions": 80, + "statements": 80 +} diff --git a/chat-client/CHANGELOG.md b/chat-client/CHANGELOG.md index 907995fda0..c5d6f08deb 100644 --- a/chat-client/CHANGELOG.md +++ b/chat-client/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [0.1.25](https://github.com/aws/language-servers/compare/chat-client/v0.1.24...chat-client/v0.1.25) (2025-07-17) + + +### Features + +* add conversation compaction ([#1895](https://github.com/aws/language-servers/issues/1895)) ([8bb7144](https://github.com/aws/language-servers/commit/8bb7144e45cfce6cc9337fd49de7edbee61105b8)) + + +### Bug Fixes + +* **amazonq:** change to use promptStickyCard to show image verification notification ([#1904](https://github.com/aws/language-servers/issues/1904)) ([caaefef](https://github.com/aws/language-servers/commit/caaefef2c9b2af66840ec2f7ccabe9bf937c2983)) +* remove disclaimer message ([#1884](https://github.com/aws/language-servers/issues/1884)) ([0845eed](https://github.com/aws/language-servers/commit/0845eeda8d73ed1df3b8801e79dad1ddd7016781)) +* replace thinking with working and replace stop with cancel ([#1922](https://github.com/aws/language-servers/issues/1922)) ([371e731](https://github.com/aws/language-servers/commit/371e731545f7572d3356d061cd8b94db35e4c333)) +* use document change events for auto trigger classifier input ([#1912](https://github.com/aws/language-servers/issues/1912)) ([2204da6](https://github.com/aws/language-servers/commit/2204da6193f2030ee546f61c969b1a664d8025e3)) + ## [0.1.24](https://github.com/aws/language-servers/compare/chat-client/v0.1.23...chat-client/v0.1.24) (2025-07-15) diff --git a/chat-client/package.json b/chat-client/package.json index 99d9ebe1fe..41ae843229 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -1,6 +1,6 @@ { "name": "@aws/chat-client", - "version": "0.1.24", + "version": "0.1.25", "description": "AWS Chat Client", "main": "out/index.js", "repository": { @@ -16,12 +16,16 @@ "scripts": { "compile": "tsc --build && npm run package", "test:unit": "ts-mocha -b \"./src/**/*.test.ts\"", + "test:unit:coverage": "c8 ts-mocha -b \"./src/**/*.test.ts\"", "test": "npm run test:unit", + "test:coverage": "npm run test:unit:coverage", + "coverage:report": "c8 report --reporter=html --reporter=text", "fix:prettier": "prettier . --write", "package": "webpack" }, "dependencies": { "@aws/chat-client-ui-types": "^0.1.51", + "@aws/language-server-runtimes": "^0.2.112", "@aws/language-server-runtimes-types": "^0.1.45", "@aws/mynah-ui": "^4.35.9" }, @@ -29,6 +33,7 @@ "@types/jsdom": "^21.1.6", "@types/mocha": "^10.0.9", "assert": "^2.0.0", + "c8": "^10.1.2", "jsdom": "^24.0.0", "sinon": "^19.0.2", "ts-mocha": "^11.1.0", diff --git a/chat-client/src/client/chat.ts b/chat-client/src/client/chat.ts index 3091a356dc..a23a73263c 100644 --- a/chat-client/src/client/chat.ts +++ b/chat-client/src/client/chat.ts @@ -120,8 +120,6 @@ import { modelSelectionForRegion } from './texts/modelSelection' const getDefaultTabConfig = (agenticMode?: boolean) => { return { tabTitle: 'Chat', - promptInputInfo: - 'Amazon Q Developer uses generative AI. You may need to verify responses. See the [AWS Responsible AI Policy](https://aws.amazon.com/machine-learning/responsible-ai/policy/).', promptInputPlaceholder: `Ask a question. Use${agenticMode ? ' @ to add context,' : ''} / for quick actions`, } } diff --git a/chat-client/src/client/features/rules.test.ts b/chat-client/src/client/features/rules.test.ts new file mode 100644 index 0000000000..c8f93ed4eb --- /dev/null +++ b/chat-client/src/client/features/rules.test.ts @@ -0,0 +1,287 @@ +import { MynahUI, DetailedListItem } from '@aws/mynah-ui' +import { Messager } from '../messager' +import * as sinon from 'sinon' +import { RulesList, ContextRule, convertRulesListToDetailedListGroup } from './rules' +import { ListRulesResult, RulesFolder } from '@aws/language-server-runtimes-types' +import * as assert from 'assert' + +describe('rules', () => { + let mynahUi: MynahUI + let messager: Messager + let openTopBarButtonOverlayStub: sinon.SinonStub + let showCustomFormStub: sinon.SinonStub + let rulesList: RulesList + + beforeEach(() => { + mynahUi = { + openTopBarButtonOverlay: sinon.stub(), + showCustomForm: sinon.stub(), + } as unknown as MynahUI + openTopBarButtonOverlayStub = mynahUi.openTopBarButtonOverlay as sinon.SinonStub + showCustomFormStub = mynahUi.showCustomForm as sinon.SinonStub + + messager = { + onRuleClick: sinon.stub(), + } as unknown as Messager + + rulesList = new RulesList(mynahUi, messager) + }) + + afterEach(() => { + sinon.restore() + }) + + describe('showLoading', () => { + it('opens top bar button overlay with loading message', () => { + const mockOverlay = { + update: sinon.stub(), + close: sinon.stub(), + } + openTopBarButtonOverlayStub.returns(mockOverlay) + + rulesList.showLoading('test-tab-id') + + sinon.assert.calledOnce(openTopBarButtonOverlayStub) + const arg = openTopBarButtonOverlayStub.getCall(0).args[0] + assert.equal(arg.tabId, 'test-tab-id') + assert.equal(arg.topBarButtonOverlay.list[0].groupName, 'Loading rules...') + assert.equal(arg.topBarButtonOverlay.selectable, false) + }) + }) + + describe('show', () => { + it('opens top bar button overlay when called first time', () => { + const mockParams: ListRulesResult = { + tabId: 'test-tab-id', + rules: [ + { + folderName: 'test-folder', + active: true, + rules: [ + { + id: 'rule-1', + name: 'Test Rule', + active: true, + }, + ], + }, + ], + filterOptions: [ + { + id: 'filter-1', + type: 'textinput', + icon: 'search', + }, + ], + } + + const mockOverlay = { + update: sinon.stub(), + close: sinon.stub(), + } + openTopBarButtonOverlayStub.returns(mockOverlay) + + rulesList.show(mockParams) + + sinon.assert.calledOnce(openTopBarButtonOverlayStub) + const arg = openTopBarButtonOverlayStub.getCall(0).args[0] + assert.equal(arg.tabId, 'test-tab-id') + assert.equal(arg.topBarButtonOverlay.selectable, 'clickable') + }) + + it('updates existing overlay when called second time', () => { + const mockParams: ListRulesResult = { + tabId: 'test-tab-id', + rules: [], + } + + const mockOverlay = { + update: sinon.stub(), + close: sinon.stub(), + } + openTopBarButtonOverlayStub.returns(mockOverlay) + + // First call + rulesList.showLoading('test-tab-id') + + // Second call + rulesList.show(mockParams) + + sinon.assert.calledOnce(mockOverlay.update) + }) + }) + + describe('rule click handling', () => { + let mockOverlay: ReturnType + let onItemClick: (item: DetailedListItem) => void + + beforeEach(() => { + mockOverlay = { + update: sinon.stub(), + close: sinon.stub(), + } + openTopBarButtonOverlayStub.returns(mockOverlay) + + rulesList.showLoading('test-tab-id') + onItemClick = openTopBarButtonOverlayStub.getCall(0).args[0].events.onItemClick + }) + + it('shows custom form when create rule is clicked', () => { + const createRuleItem: DetailedListItem = { + id: ContextRule.CreateRuleId, + description: 'Create a new rule', + } + + onItemClick(createRuleItem) + + sinon.assert.calledOnce(showCustomFormStub) + const formArgs = showCustomFormStub.getCall(0).args + assert.equal(formArgs[0], 'test-tab-id') + assert.equal(formArgs[1][0].id, ContextRule.RuleNameFieldId) + assert.equal(formArgs[2][0].id, ContextRule.CancelButtonId) + assert.equal(formArgs[2][1].id, ContextRule.SubmitButtonId) + }) + + it('calls messager when regular rule is clicked', () => { + const ruleItem: DetailedListItem = { + id: 'test-rule-id', + description: 'Test Rule', + } + + onItemClick(ruleItem) + + sinon.assert.calledOnce(messager.onRuleClick as sinon.SinonStub) + const callArgs = (messager.onRuleClick as sinon.SinonStub).getCall(0).args[0] + assert.equal(callArgs.tabId, 'test-tab-id') + assert.equal(callArgs.type, 'rule') + assert.equal(callArgs.id, 'test-rule-id') + }) + + it('does nothing when item has no id', () => { + const itemWithoutId: DetailedListItem = { + description: 'Item without ID', + } + + onItemClick(itemWithoutId) + + sinon.assert.notCalled(messager.onRuleClick as sinon.SinonStub) + sinon.assert.notCalled(showCustomFormStub) + }) + }) + + describe('folder click handling', () => { + it('calls messager when folder is clicked', () => { + const mockOverlay = { + update: sinon.stub(), + close: sinon.stub(), + } + openTopBarButtonOverlayStub.returns(mockOverlay) + + rulesList.showLoading('test-tab-id') + const onGroupClick = openTopBarButtonOverlayStub.getCall(0).args[0].events.onGroupClick + + onGroupClick('test-folder') + + sinon.assert.calledOnce(messager.onRuleClick as sinon.SinonStub) + const callArgs = (messager.onRuleClick as sinon.SinonStub).getCall(0).args[0] + assert.equal(callArgs.tabId, 'test-tab-id') + assert.equal(callArgs.type, 'folder') + assert.equal(callArgs.id, 'test-folder') + }) + }) + + describe('keyboard handling', () => { + it('closes overlay when Escape is pressed', () => { + const mockOverlay = { + update: sinon.stub(), + close: sinon.stub(), + } + openTopBarButtonOverlayStub.returns(mockOverlay) + + rulesList.showLoading('test-tab-id') + const onKeyPress = openTopBarButtonOverlayStub.getCall(0).args[0].events.onKeyPress + + const escapeEvent = { key: 'Escape' } as KeyboardEvent + onKeyPress(escapeEvent) + + sinon.assert.calledOnce(mockOverlay.close) + }) + + it('does nothing when other keys are pressed', () => { + const mockOverlay = { + update: sinon.stub(), + close: sinon.stub(), + } + openTopBarButtonOverlayStub.returns(mockOverlay) + + rulesList.showLoading('test-tab-id') + const onKeyPress = openTopBarButtonOverlayStub.getCall(0).args[0].events.onKeyPress + + const enterEvent = { key: 'Enter' } as KeyboardEvent + onKeyPress(enterEvent) + + sinon.assert.notCalled(mockOverlay.close) + }) + }) + + describe('close', () => { + it('closes the overlay', () => { + const mockOverlay = { + update: sinon.stub(), + close: sinon.stub(), + } + openTopBarButtonOverlayStub.returns(mockOverlay) + + rulesList.showLoading('test-tab-id') + rulesList.close() + + sinon.assert.calledOnce(mockOverlay.close) + }) + }) + + describe('convertRulesListToDetailedListGroup', () => { + it('converts rules folder to detailed list group', () => { + const rulesFolder: RulesFolder[] = [ + { + folderName: 'test-folder', + active: true, + rules: [ + { + id: 'rule-1', + name: 'Test Rule 1', + active: true, + }, + { + id: 'rule-2', + name: 'Test Rule 2', + active: false, + }, + ], + }, + { + folderName: 'inactive-folder', + active: 'indeterminate', + rules: [], + }, + ] + + const result = convertRulesListToDetailedListGroup(rulesFolder) + + assert.equal(result.length, 3) // 2 folders + create rule group + assert.equal(result[0].groupName, 'test-folder') + assert.equal(result[0].children?.length, 2) + assert.equal(result[0].children?.[0].id, 'rule-1') + assert.equal(result[0].children?.[0].description, 'Test Rule 1') + assert.equal(result[1].groupName, 'inactive-folder') + assert.equal(result[1].children?.length, 0) + assert.equal(result[2].children?.[0].id, ContextRule.CreateRuleId) + }) + + it('handles empty rules array', () => { + const result = convertRulesListToDetailedListGroup([]) + + assert.equal(result.length, 1) // Only create rule group + assert.equal(result[0].children?.[0].id, ContextRule.CreateRuleId) + }) + }) +}) diff --git a/chat-client/src/client/imageVerification.test.ts b/chat-client/src/client/imageVerification.test.ts new file mode 100644 index 0000000000..3d769b2088 --- /dev/null +++ b/chat-client/src/client/imageVerification.test.ts @@ -0,0 +1,294 @@ +import * as assert from 'assert' +import * as sinon from 'sinon' +import { + isSupportedImageExtension, + isFileSizeValid, + areImageDimensionsValid, + verifyClientImage, + verifyClientImages, + DEFAULT_IMAGE_VERIFICATION_OPTIONS, + MAX_IMAGE_CONTEXT, +} from './imageVerification' + +class MockImage { + onload: (() => void) | null = null + onerror: (() => void) | null = null + width = 800 + height = 600 + _src = '' + get src() { + return this._src + } + set src(value: string) { + this._src = value + // Simulate image loading + Promise.resolve().then(() => this.onload?.()) + } +} + +class MockFileReader { + onload: ((event: any) => void) | null = null + onerror: (() => void) | null = null + result: string | ArrayBuffer | null = null + readAsDataURL(file: File) { + setTimeout(() => { + this.result = 'data:image/png;base64,mock-data' + this.onload?.({ target: { result: this.result } }) + }, 0) + } +} + +describe('imageVerification', () => { + let imageStub: sinon.SinonStub + let urlStub: sinon.SinonStub + let fileReaderStub: sinon.SinonStub + + beforeEach(() => { + imageStub = sinon.stub(global, 'Image').callsFake(() => new MockImage()) + urlStub = sinon.stub(global, 'URL').value({ + createObjectURL: sinon.stub().returns('blob:mock-url'), + revokeObjectURL: sinon.stub(), + }) + fileReaderStub = sinon.stub(global, 'FileReader').callsFake(() => new MockFileReader()) + }) + + afterEach(() => { + sinon.restore() + }) + + describe('constants', () => { + it('has correct MAX_IMAGE_CONTEXT value', () => { + assert.equal(MAX_IMAGE_CONTEXT, 20) + }) + + it('has correct default options', () => { + assert.equal(DEFAULT_IMAGE_VERIFICATION_OPTIONS.maxSizeBytes, 3.75 * 1024 * 1024) + assert.equal(DEFAULT_IMAGE_VERIFICATION_OPTIONS.maxDimension, 8000) + assert.deepEqual(DEFAULT_IMAGE_VERIFICATION_OPTIONS.supportedExtensions, [ + 'jpeg', + 'jpg', + 'png', + 'gif', + 'webp', + ]) + }) + }) + + describe('isSupportedImageExtension', () => { + it('returns true for supported extensions', () => { + assert.equal(isSupportedImageExtension('jpg'), true) + assert.equal(isSupportedImageExtension('jpeg'), true) + assert.equal(isSupportedImageExtension('png'), true) + assert.equal(isSupportedImageExtension('gif'), true) + assert.equal(isSupportedImageExtension('webp'), true) + }) + + it('returns true for supported extensions with dots', () => { + assert.equal(isSupportedImageExtension('.jpg'), true) + assert.equal(isSupportedImageExtension('.png'), true) + }) + + it('returns true for uppercase extensions', () => { + assert.equal(isSupportedImageExtension('JPG'), true) + assert.equal(isSupportedImageExtension('PNG'), true) + }) + + it('returns false for unsupported extensions', () => { + assert.equal(isSupportedImageExtension('txt'), false) + assert.equal(isSupportedImageExtension('pdf'), false) + assert.equal(isSupportedImageExtension('doc'), false) + }) + }) + + describe('isFileSizeValid', () => { + it('returns true for valid file sizes', () => { + assert.equal(isFileSizeValid(1024), true) // 1KB + assert.equal(isFileSizeValid(1024 * 1024), true) // 1MB + }) + + it('returns false for oversized files', () => { + const maxSize = DEFAULT_IMAGE_VERIFICATION_OPTIONS.maxSizeBytes + assert.equal(isFileSizeValid(maxSize + 1), false) + }) + + it('accepts custom max size', () => { + assert.equal(isFileSizeValid(2048, 1024), false) + assert.equal(isFileSizeValid(512, 1024), true) + }) + }) + + describe('areImageDimensionsValid', () => { + it('returns true for valid dimensions', () => { + assert.equal(areImageDimensionsValid(800, 600), true) + assert.equal(areImageDimensionsValid(1920, 1080), true) + }) + + it('returns false for oversized dimensions', () => { + const maxDim = DEFAULT_IMAGE_VERIFICATION_OPTIONS.maxDimension + assert.equal(areImageDimensionsValid(maxDim + 1, 600), false) + assert.equal(areImageDimensionsValid(800, maxDim + 1), false) + }) + + it('accepts custom max dimension', () => { + assert.equal(areImageDimensionsValid(1200, 800, 1000), false) + assert.equal(areImageDimensionsValid(800, 600, 1000), true) + }) + }) + + describe('verifyClientImage', () => { + let mockFile: File + + beforeEach(() => { + mockFile = { + name: 'test.jpg', + size: 1024 * 1024, // 1MB + type: 'image/jpeg', + } as File + }) + + it('validates a correct image file', async () => { + const result = await verifyClientImage(mockFile, 'test.jpg') + assert.equal(result.isValid, true) + assert.equal(result.errors.length, 0) + }) + + it('rejects unsupported file extension', async () => { + const result = await verifyClientImage(mockFile, 'test.txt') + assert.equal(result.isValid, false) + assert.equal(result.errors.length, 1) + assert.ok(result.errors[0].includes('File must be an image')) + }) + + it('rejects oversized files', async () => { + const largeFile = { + ...mockFile, + size: DEFAULT_IMAGE_VERIFICATION_OPTIONS.maxSizeBytes + 1, + } as File + + const result = await verifyClientImage(largeFile, 'large.jpg') + assert.equal(result.isValid, false) + assert.equal(result.errors.length, 1) + assert.ok(result.errors[0].includes('must be no more than')) + }) + + it('rejects images with oversized dimensions', async () => { + // Stub Image to return oversized dimensions + imageStub.callsFake(() => ({ + onload: null, + onerror: null, + width: DEFAULT_IMAGE_VERIFICATION_OPTIONS.maxDimension + 1, + height: 600, + _src: '', + get src() { + return this._src + }, + set src(value: string) { + this._src = value + Promise.resolve().then(() => this.onload?.()) + }, + })) + + const result = await verifyClientImage(mockFile, 'oversized.jpg') + assert.equal(result.isValid, false) + assert.equal(result.errors.length, 1) + assert.ok(result.errors[0].includes('must be no more than')) + }) + + it('handles image loading errors', async () => { + // Stub Image to fail loading + imageStub.callsFake(() => ({ + onload: null, + onerror: null, + width: 0, + height: 0, + _src: '', + get src() { + return this._src + }, + set src(value: string) { + this._src = value + Promise.resolve().then(() => this.onerror?.()) + }, + })) + + // Stub FileReader to also fail + fileReaderStub.callsFake(() => ({ + onload: null, + onerror: null, + result: null, + readAsDataURL() { + setTimeout(() => this.onerror?.(), 0) + }, + })) + + const result = await verifyClientImage(mockFile, 'failing.jpg') + assert.equal(result.isValid, false) + assert.equal(result.errors.length, 1) + assert.ok(result.errors[0].includes('Unable to read image dimensions')) + }) + }) + + describe('verifyClientImages', () => { + let mockFileList: FileList + + beforeEach(() => { + const validFile = { + name: 'valid.jpg', + size: 1024 * 1024, + type: 'image/jpeg', + } as File + + const invalidFile = { + name: 'invalid.txt', + size: 1024, + type: 'text/plain', + } as File + + mockFileList = { + length: 2, + 0: validFile, + 1: invalidFile, + item: (index: number) => (index === 0 ? validFile : invalidFile), + } as unknown as FileList + }) + + it('separates valid and invalid files', async () => { + const result = await verifyClientImages(mockFileList) + assert.equal(result.validFiles.length, 1) + assert.equal(result.errors.length, 1) + assert.equal(result.validFiles[0].name, 'valid.jpg') + assert.ok(result.errors[0].includes('invalid.txt')) + }) + + it('handles empty file list', async () => { + const emptyFileList = { + length: 0, + item: () => null, + } as unknown as FileList + + const result = await verifyClientImages(emptyFileList) + assert.equal(result.validFiles.length, 0) + assert.equal(result.errors.length, 0) + }) + + it('handles files without names', async () => { + const fileWithoutName = { + name: '', + size: 1024, + type: 'image/jpeg', + } as File + + const fileListWithUnnamed = { + length: 1, + 0: fileWithoutName, + item: () => fileWithoutName, + } as unknown as FileList + + const result = await verifyClientImages(fileListWithUnnamed) + // File without extension should be rejected + assert.equal(result.validFiles.length, 0) + assert.equal(result.errors.length, 1) + assert.ok(result.errors[0].includes('Unknown file')) + }) + }) +}) diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 3f84938f90..17749e5e5e 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -179,8 +179,8 @@ export const handleChatPrompt = ( // Add cancellation message BEFORE showing the new prompt mynahUi.addChatItem(tabId, { type: ChatItemType.DIRECTIVE, - messageId: 'stopped' + Date.now(), - body: 'You stopped your current work and asked me to work on the following task instead.', + messageId: 'canceled' + Date.now(), + body: 'You canceled your current work and asked me to work on the following task instead.', }) // Reset loading state @@ -200,7 +200,8 @@ export const handleChatPrompt = ( prompt.command && ['/dev', '/test', '/doc'].includes(prompt.command) - if (prompt.command && !isReroutedCommand) { + if (prompt.command && !isReroutedCommand && prompt.command !== '/compact') { + // Send /compact quick action as normal regular chat prompt // Handle non-rerouted commands (/clear, /help, /transform, /review) as quick actions // Temporary solution to handle clear quick actions on the client side if (prompt.command === '/clear') { @@ -703,8 +704,8 @@ export const createMynahUi = ( // Add cancellation message when stop button is clicked mynahUi.addChatItem(tabId, { type: ChatItemType.DIRECTIVE, - messageId: 'stopped' + Date.now(), - body: 'You stopped your current work, please provide additional examples or ask another question.', + messageId: 'canceled' + Date.now(), + body: 'You canceled your current work, please provide additional examples or ask another question.', }) }, 500) // 500ms delay }, @@ -778,13 +779,21 @@ export const createMynahUi = ( // Add valid files to context commands mynahUi.addCustomContextToPrompt(tabId, commands, insertPosition) } - const uniqueErrors = [...new Set(errors)] - for (const error of uniqueErrors) { - mynahUi.notify({ - content: error, - type: NotificationType.WARNING, - }) + + const imageVerificationBanner: Partial = { + messageId: 'image-verification-banner', + header: { + icon: 'warning', + iconStatus: 'warning', + body: '### Invalid Image', + }, + body: `${errors.join('\n')}`, + canBeDismissed: true, } + + mynahUi.updateStore(tabId, { + promptInputStickyCard: imageVerificationBanner, + }) }, } @@ -1371,7 +1380,7 @@ export const createMynahUi = ( // Adding this conditional check to show the stop message in the center. const contentHorizontalAlignment: ChatItem['contentHorizontalAlignment'] = undefined - // If message.header?.status?.text is Stopped or Rejected or Ignored or Completed etc.. card should be in disabled state. + // If message.header?.status?.text is Canceled or Rejected or Ignored or Completed etc.. card should be in disabled state. const shouldMute = message.header?.status?.text !== undefined && message.header?.status?.text !== 'Completed' return { @@ -1716,9 +1725,9 @@ export const uiComponentsTexts = { save: 'Save', cancel: 'Cancel', submit: 'Submit', - stopGenerating: 'Stop', + stopGenerating: 'Cancel', copyToClipboard: 'Copied to clipboard', noMoreTabsTooltip: 'You can only open ten conversation tabs at a time.', codeSuggestionWithReferenceTitle: 'Some suggestions contain code with references.', - spinnerText: 'Thinking...', + spinnerText: 'Working...', } diff --git a/chat-client/src/client/texts/pairProgramming.test.ts b/chat-client/src/client/texts/pairProgramming.test.ts new file mode 100644 index 0000000000..3ffec1bcaf --- /dev/null +++ b/chat-client/src/client/texts/pairProgramming.test.ts @@ -0,0 +1,122 @@ +import * as assert from 'assert' +import { ChatItemType, MynahIcons } from '@aws/mynah-ui' +import { + programmerModeCard, + pairProgrammingPromptInput, + pairProgrammingModeOn, + pairProgrammingModeOff, + testRerouteCard, + docRerouteCard, + devRerouteCard, + createRerouteCard, +} from './pairProgramming' + +describe('pairProgramming', () => { + describe('programmerModeCard', () => { + it('has correct properties', () => { + assert.equal(programmerModeCard.type, ChatItemType.ANSWER) + assert.equal(programmerModeCard.title, 'NEW FEATURE') + assert.equal(programmerModeCard.messageId, 'programmerModeCardId') + assert.equal(programmerModeCard.fullWidth, true) + assert.equal(programmerModeCard.canBeDismissed, true) + assert.ok(programmerModeCard.body?.includes('Amazon Q can now help')) + assert.equal(programmerModeCard.header?.icon, 'code-block') + assert.equal(programmerModeCard.header?.iconStatus, 'primary') + }) + }) + + describe('pairProgrammingPromptInput', () => { + it('has correct properties', () => { + assert.equal(pairProgrammingPromptInput.type, 'switch') + assert.equal(pairProgrammingPromptInput.id, 'pair-programmer-mode') + assert.equal(pairProgrammingPromptInput.tooltip, 'Turn OFF agentic coding') + if (pairProgrammingPromptInput.type === 'switch') { + // Type guard for switch type + assert.equal(pairProgrammingPromptInput.alternateTooltip, 'Turn ON agentic coding') + } + assert.equal(pairProgrammingPromptInput.value, 'true') + assert.equal(pairProgrammingPromptInput.icon, 'code-block') + }) + }) + + describe('pairProgrammingModeOn', () => { + it('has correct properties', () => { + assert.equal(pairProgrammingModeOn.type, ChatItemType.DIRECTIVE) + assert.equal(pairProgrammingModeOn.contentHorizontalAlignment, 'center') + assert.equal(pairProgrammingModeOn.fullWidth, true) + assert.equal(pairProgrammingModeOn.body, 'Agentic coding - ON') + }) + }) + + describe('pairProgrammingModeOff', () => { + it('has correct properties', () => { + assert.equal(pairProgrammingModeOff.type, ChatItemType.DIRECTIVE) + assert.equal(pairProgrammingModeOff.contentHorizontalAlignment, 'center') + assert.equal(pairProgrammingModeOff.fullWidth, true) + assert.equal(pairProgrammingModeOff.body, 'Agentic coding - OFF') + }) + }) + + describe('testRerouteCard', () => { + it('has correct properties', () => { + assert.equal(testRerouteCard.type, ChatItemType.ANSWER) + assert.equal(testRerouteCard.border, true) + assert.equal(testRerouteCard.header?.padding, true) + assert.equal(testRerouteCard.header?.iconForegroundStatus, 'warning') + assert.equal(testRerouteCard.header?.icon, MynahIcons.INFO) + assert.ok(testRerouteCard.header?.body?.includes('generate unit tests')) + assert.ok(testRerouteCard.body?.includes("You don't need to explicitly use /test")) + }) + }) + + describe('docRerouteCard', () => { + it('has correct properties', () => { + assert.equal(docRerouteCard.type, ChatItemType.ANSWER) + assert.equal(docRerouteCard.border, true) + assert.equal(docRerouteCard.header?.padding, true) + assert.equal(docRerouteCard.header?.iconForegroundStatus, 'warning') + assert.equal(docRerouteCard.header?.icon, MynahIcons.INFO) + assert.ok(docRerouteCard.header?.body?.includes('generate documentation')) + assert.ok(docRerouteCard.body?.includes("You don't need to explicitly use /doc")) + }) + }) + + describe('devRerouteCard', () => { + it('has correct properties', () => { + assert.equal(devRerouteCard.type, ChatItemType.ANSWER) + assert.equal(devRerouteCard.border, true) + assert.equal(devRerouteCard.header?.padding, true) + assert.equal(devRerouteCard.header?.iconForegroundStatus, 'warning') + assert.equal(devRerouteCard.header?.icon, MynahIcons.INFO) + assert.ok(devRerouteCard.header?.body?.includes('generate code')) + assert.ok(devRerouteCard.body?.includes("You don't need to explicitly use /dev")) + }) + }) + + describe('createRerouteCard', () => { + it('returns testRerouteCard for /test command', () => { + const result = createRerouteCard('/test') + assert.deepEqual(result, testRerouteCard) + }) + + it('returns docRerouteCard for /doc command', () => { + const result = createRerouteCard('/doc') + assert.deepEqual(result, docRerouteCard) + }) + + it('returns devRerouteCard for /dev command', () => { + const result = createRerouteCard('/dev') + assert.deepEqual(result, devRerouteCard) + }) + + it('returns devRerouteCard for unknown command', () => { + const result = createRerouteCard('/unknown') + assert.deepEqual(result, devRerouteCard) + }) + + it('returns devRerouteCard for empty string', () => { + const result = createRerouteCard('') + assert.deepEqual(result, devRerouteCard) + }) + }) +}) diff --git a/chat-client/src/client/utils.test.ts b/chat-client/src/client/utils.test.ts new file mode 100644 index 0000000000..38e64404a0 --- /dev/null +++ b/chat-client/src/client/utils.test.ts @@ -0,0 +1,251 @@ +import * as assert from 'assert' +import { MynahIcons } from '@aws/mynah-ui' +import { Button, ChatMessage } from '@aws/language-server-runtimes-types' +import { FeatureContext } from '@aws/chat-client-ui-types' +import { + toMynahIcon, + toMynahButtons, + toMynahHeader, + toMynahFileList, + toDetailsWithoutIcon, + toMynahContextCommand, +} from './utils' + +describe('utils', () => { + describe('toMynahIcon', () => { + it('returns valid MynahIcon when icon exists', () => { + const result = toMynahIcon(MynahIcons.CHAT) + assert.equal(result, MynahIcons.CHAT) + }) + + it('returns undefined for invalid icon', () => { + const result = toMynahIcon('invalid-icon') + assert.equal(result, undefined) + }) + + it('returns undefined for undefined input', () => { + const result = toMynahIcon(undefined) + assert.equal(result, undefined) + }) + }) + + describe('toMynahButtons', () => { + it('converts buttons with valid icons', () => { + const buttons: Button[] = [ + { id: 'btn1', text: 'Button 1', icon: MynahIcons.CHAT }, + { id: 'btn2', text: 'Button 2', icon: 'invalid-icon' }, + ] + + const result = toMynahButtons(buttons) + assert.equal(result?.length, 2) + assert.equal(result?.[0].icon, MynahIcons.CHAT) + assert.equal(result?.[1].icon, undefined) + }) + + it('returns undefined for undefined input', () => { + const result = toMynahButtons(undefined) + assert.equal(result, undefined) + }) + + it('handles empty array', () => { + const result = toMynahButtons([]) + assert.deepEqual(result, []) + }) + }) + + describe('toMynahHeader', () => { + it('converts header with all properties', () => { + const header: ChatMessage['header'] = { + icon: MynahIcons.CHAT, + buttons: [{ id: 'btn1', text: 'Button', icon: MynahIcons.OK }], + status: { text: 'Status', icon: MynahIcons.WARNING }, + summary: { + content: { + body: 'Test summary', + }, + }, + } + + const result = toMynahHeader(header) + assert.equal(result?.icon, MynahIcons.CHAT) + assert.equal(result?.buttons?.length, 1) + assert.equal(result?.status?.text, 'Status') + assert.equal(result?.status?.icon, MynahIcons.WARNING) + assert.equal(result?.summary?.content?.body, 'Test summary') + }) + + it('handles header without status', () => { + const header: ChatMessage['header'] = { + icon: MynahIcons.CHAT, + } + + const result = toMynahHeader(header) + assert.equal(result?.status, undefined) + }) + + it('returns undefined for undefined header', () => { + const result = toMynahHeader(undefined) + assert.equal(result, undefined) + }) + + it('handles header with invalid icons', () => { + const header: ChatMessage['header'] = { + icon: 'invalid-icon', + status: { text: 'Status', icon: 'invalid-status-icon' }, + } + + const result = toMynahHeader(header) + assert.equal(result?.icon, undefined) + assert.equal(result?.status?.icon, undefined) + }) + }) + + describe('toMynahFileList', () => { + it('converts file list with all properties', () => { + const fileList: ChatMessage['fileList'] = { + filePaths: ['src/file1.ts', 'src/file2.ts'], + rootFolderTitle: 'Project Root', + details: { + 'src/file1.ts': { + lineRanges: [{ first: 1, second: 10 }], + description: 'First file', + fullPath: '/full/path/src/file1.ts', + }, + 'src/file2.ts': { + lineRanges: [{ first: -1, second: -1 }], + description: 'Second file', + }, + }, + } + + const result = toMynahFileList(fileList) + assert.equal(result?.rootFolderTitle, 'Project Root') + assert.equal(result?.filePaths?.length, 2) + assert.equal(result?.flatList, true) + assert.equal(result?.hideFileCount, true) + assert.equal(result?.collapsed, true) + assert.equal(result?.details?.['src/file1.ts']?.label, 'line 1 - 10') + assert.equal(result?.details?.['src/file1.ts']?.description, 'First file') + assert.equal(result?.details?.['src/file1.ts']?.visibleName, 'file1.ts') + assert.equal(result?.details?.['src/file2.ts']?.label, '') + }) + + it('uses default root folder title when not provided', () => { + const fileList: ChatMessage['fileList'] = { + filePaths: ['file.ts'], + } + + const result = toMynahFileList(fileList) + assert.equal(result?.rootFolderTitle, 'Context') + }) + + it('returns undefined for undefined input', () => { + const result = toMynahFileList(undefined) + assert.equal(result, undefined) + }) + + it('handles file paths with different structures', () => { + const fileList: ChatMessage['fileList'] = { + filePaths: ['simple.ts', 'folder/nested.ts', 'deep/nested/path/file.ts'], + details: { + 'simple.ts': {}, + 'folder/nested.ts': {}, + 'deep/nested/path/file.ts': {}, + }, + } + + const result = toMynahFileList(fileList) + assert.equal(result?.details?.['simple.ts']?.visibleName, 'simple.ts') + assert.equal(result?.details?.['folder/nested.ts']?.visibleName, 'nested.ts') + assert.equal(result?.details?.['deep/nested/path/file.ts']?.visibleName, 'file.ts') + }) + + it('handles multiple line ranges', () => { + const fileList: ChatMessage['fileList'] = { + filePaths: ['file.ts'], + details: { + 'file.ts': { + lineRanges: [ + { first: 1, second: 5 }, + { first: 10, second: 15 }, + ], + }, + }, + } + + const result = toMynahFileList(fileList) + assert.equal(result?.details?.['file.ts']?.label, 'line 1 - 5, line 10 - 15') + }) + }) + + describe('toDetailsWithoutIcon', () => { + it('removes icons from details', () => { + const details = { + 'file1.ts': { + label: 'File 1', + icon: MynahIcons.FILE, + description: 'First file', + }, + 'file2.ts': { + label: 'File 2', + description: 'Second file', + }, + } + + const result = toDetailsWithoutIcon(details) + assert.equal(result['file1.ts'].icon, null) + assert.equal(result['file1.ts'].label, 'File 1') + assert.equal(result['file2.ts'].icon, null) + assert.equal(result['file2.ts'].label, 'File 2') + }) + + it('handles undefined input', () => { + const result = toDetailsWithoutIcon(undefined) + assert.deepEqual(result, {}) + }) + + it('handles empty object', () => { + const result = toDetailsWithoutIcon({}) + assert.deepEqual(result, {}) + }) + }) + + describe('toMynahContextCommand', () => { + it('converts feature context with string value', () => { + const feature: FeatureContext = { + value: { stringValue: 'test-command' }, + variation: 'Test Command Description', + } + + const result = toMynahContextCommand(feature) + assert.equal(result.command, 'test-command') + assert.equal(result.id, 'test-command') + assert.equal(result.description, 'Test Command Description') + }) + + it('returns empty object for undefined feature', () => { + const result = toMynahContextCommand(undefined) + assert.deepEqual(result, {}) + }) + + it('returns empty object for feature without string value', () => { + const feature: FeatureContext = { + value: {}, + variation: 'Description', + } + + const result = toMynahContextCommand(feature) + assert.deepEqual(result, {}) + }) + + it('returns empty object for feature with empty string value', () => { + const feature: FeatureContext = { + value: { stringValue: '' }, + variation: 'Description', + } + + const result = toMynahContextCommand(feature) + assert.deepEqual(result, {}) + }) + }) +}) diff --git a/chat-client/src/test/jsDomInjector.ts b/chat-client/src/test/jsDomInjector.ts index 2d80344ef1..b73ad67484 100644 --- a/chat-client/src/test/jsDomInjector.ts +++ b/chat-client/src/test/jsDomInjector.ts @@ -15,6 +15,8 @@ export function injectJSDOM() { global.HTMLElement = dom.window.HTMLElement global.CustomEvent = dom.window.CustomEvent global.MutationObserver = dom.window.MutationObserver + global.Image = dom.window.Image + global.FileReader = dom.window.FileReader // jsdom doesn't have support for innerText: https://github.com/jsdom/jsdom/issues/1245 which mynah ui uses Object.defineProperty(global.Element.prototype, 'innerText', { diff --git a/client/vscode/package.json b/client/vscode/package.json index 5a95af4553..98387eba0d 100644 --- a/client/vscode/package.json +++ b/client/vscode/package.json @@ -347,7 +347,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.40", - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", diff --git a/core/aws-lsp-core/.c8rc.json b/core/aws-lsp-core/.c8rc.json new file mode 100644 index 0000000000..252b6831fb --- /dev/null +++ b/core/aws-lsp-core/.c8rc.json @@ -0,0 +1,12 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text", "html", "lcov"], + "reports-dir": "coverage", + "include": ["out/**/*.js"], + "exclude": ["out/**/*.test.js", "out/**/*.spec.js", "out/**/test/**"], + "branches": 80, + "lines": 80, + "functions": 80, + "statements": 80 +} diff --git a/core/aws-lsp-core/CHANGELOG.md b/core/aws-lsp-core/CHANGELOG.md index beec4af0a1..6861a1f46e 100644 --- a/core/aws-lsp-core/CHANGELOG.md +++ b/core/aws-lsp-core/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.0.12](https://github.com/aws/language-servers/compare/lsp-core/v0.0.11...lsp-core/v0.0.12) (2025-07-17) + + +### Bug Fixes + +* add proper encoding support for shell output ([#1903](https://github.com/aws/language-servers/issues/1903)) ([44a6d62](https://github.com/aws/language-servers/commit/44a6d629af7702662a02f384a6a542c0d72ccc39)) +* use document change events for auto trigger classifier input ([#1912](https://github.com/aws/language-servers/issues/1912)) ([2204da6](https://github.com/aws/language-servers/commit/2204da6193f2030ee546f61c969b1a664d8025e3)) + ## [0.0.11](https://github.com/aws/language-servers/compare/lsp-core/v0.0.10...lsp-core/v0.0.11) (2025-07-02) diff --git a/core/aws-lsp-core/package.json b/core/aws-lsp-core/package.json index adc967b2d4..d1ed41e742 100644 --- a/core/aws-lsp-core/package.json +++ b/core/aws-lsp-core/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-core", - "version": "0.0.11", + "version": "0.0.12", "description": "Core library, contains common code and utilities", "main": "out/index.js", "repository": { @@ -22,12 +22,16 @@ "compile": "tsc --build", "test": "npm run test-unit", "test-unit": "mocha --timeout 0 \"./out/**/*.test.js\"", + "test-unit:coverage": "npm run compile && c8 mocha --timeout 0 \"./out/**/*.test.js\"", + "test:coverage": "npm run test-unit:coverage", + "coverage:report": "c8 report --reporter=html --reporter=text", "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@gerhobbelt/gitignore-parser": "^0.2.0-9", - "jose": "^5.2.4", "cross-spawn": "7.0.6", + "jose": "^5.2.4", "request-light": "^0.8.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", @@ -35,10 +39,11 @@ }, "devDependencies": { "@types/chai": "^4.3.5", - "@types/cross-spawn": "^6.0.2", "@types/chai-as-promised": "^7.1.5", + "@types/cross-spawn": "^6.0.2", "@types/mocha": "^10.0.9", "@types/mock-fs": "^4.13.1", + "c8": "^10.1.2", "chai": "^4.3.7", "chai-as-promised": "^7.1.1", "mocha": "^11.0.1", diff --git a/core/aws-lsp-core/src/util/processUtils.ts b/core/aws-lsp-core/src/util/processUtils.ts index eb54ee6a5d..eca985d548 100644 --- a/core/aws-lsp-core/src/util/processUtils.ts +++ b/core/aws-lsp-core/src/util/processUtils.ts @@ -6,6 +6,7 @@ import * as crossSpawn from 'cross-spawn' import { Logging } from '@aws/language-server-runtimes/server-interface' import { PollingSet } from './pollingSet' import { waitUntil } from './timeoutUtils' +import * as Encoding from 'encoding-japanese' export interface RunParameterContext { /** Reports an error parsed from the stdin/stdout streams. */ @@ -56,6 +57,21 @@ export interface ChildProcessResult { export const eof = Symbol('EOF') +/** + * Decodes a Buffer chunk from a shell process using encoding-japanese + * to handle various encodings including UTF-16LE (Windows cmd.exe /u) + */ +function decodeShellChunk(buf: Buffer): string { + const byteArray = new Uint8Array(buf) + const detectedEncoding = Encoding.detect(byteArray) || 'UTF8' + + return Encoding.convert(byteArray, { + from: detectedEncoding, + to: 'UNICODE', + type: 'string', + }) as string +} + export interface ProcessStats { memory: number cpu: number @@ -328,20 +344,24 @@ export class ChildProcess { this.#childProcess.stdout?.on('error', errorHandler) this.#childProcess.stderr?.on('error', errorHandler) - this.#childProcess.stdout?.on('data', (data: { toString(): string }) => { + this.#childProcess.stdout?.on('data', (chunk: Buffer) => { + const decoded = decodeShellChunk(chunk) + if (options.collect) { - this.#stdoutChunks.push(data.toString()) + this.#stdoutChunks.push(decoded) } - options.onStdout?.(data.toString(), paramsContext) + options.onStdout?.(decoded, paramsContext) }) - this.#childProcess.stderr?.on('data', (data: { toString(): string }) => { + this.#childProcess.stderr?.on('data', (chunk: Buffer) => { + const decoded = decodeShellChunk(chunk) + if (options.collect) { - this.#stderrChunks.push(data.toString()) + this.#stderrChunks.push(decoded) } - options.onStderr?.(data.toString(), paramsContext) + options.onStderr?.(decoded, paramsContext) }) // Emitted when streams are closed. diff --git a/integration-tests/q-agentic-chat-server/package.json b/integration-tests/q-agentic-chat-server/package.json index 70780086dc..f562e5521e 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.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-core": "*" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index 6afdbee51b..d4b8476f19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,18 @@ "integration-tests/*" ], "dependencies": { - "@aws/language-server-runtimes": "^0.2.108", +<<<<<<< HEAD + "@aws/language-server-runtimes": "^0.2.111", +======= + "@aws/language-server-runtimes": "^0.2.112", +>>>>>>> aws/main "@smithy/types": "4.2.0", "clean": "^4.0.2", "typescript": "^5.8.2" }, "devDependencies": { + "@aws-sdk/client-iam": "^3.840.0", + "@aws-sdk/client-sts": "^3.840.0", "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", "@types/ignore-walk": "^4.0.3", @@ -46,7 +52,7 @@ "name": "@aws/lsp-antlr4-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-antlr4": "*", "antlr4-c3": "^3.4.1", "antlr4ng": "^3.0.4" @@ -69,6 +75,7 @@ "name": "@aws/lsp-buildspec-runtimes", "version": "0.0.1", "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-buildspec": "^0.0.1" } }, @@ -76,6 +83,7 @@ "name": "@aws/lsp-cloudformation-runtimes", "version": "0.0.1", "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-cloudformation": "^0.0.1" } }, @@ -83,7 +91,7 @@ "name": "@aws/lsp-codewhisperer-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-codewhisperer": "*", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", @@ -116,7 +124,7 @@ "name": "@aws/lsp-identity-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-identity": "^0.0.1" } }, @@ -124,7 +132,7 @@ "name": "@aws/lsp-json-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-json": "*" }, "devDependencies": { @@ -144,7 +152,7 @@ "name": "@aws/lsp-notification-runtimes", "version": "0.1.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-notification": "^0.0.1" } }, @@ -152,7 +160,7 @@ "name": "@aws/lsp-partiql-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.89", + "@aws/language-server-runtimes": "^0.2.109", "@aws/lsp-partiql": "^0.0.5" }, "devDependencies": { @@ -177,6 +185,7 @@ "name": "@aws/lsp-s3-runtimes", "version": "0.0.1", "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-s3": "^0.0.1" }, "bin": { @@ -187,7 +196,7 @@ "name": "@aws/lsp-yaml-json-webworker", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*" }, @@ -207,7 +216,7 @@ "name": "@aws/lsp-yaml-runtimes", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-yaml": "*" }, "devDependencies": { @@ -229,7 +238,7 @@ "version": "0.0.1", "dependencies": { "@aws/hello-world-lsp": "^0.0.1", - "@aws/language-server-runtimes": "^0.2.105" + "@aws/language-server-runtimes": "^0.2.112" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -246,10 +255,11 @@ }, "chat-client": { "name": "@aws/chat-client", - "version": "0.1.24", + "version": "0.1.25", "license": "Apache-2.0", "dependencies": { "@aws/chat-client-ui-types": "^0.1.51", + "@aws/language-server-runtimes": "^0.2.112", "@aws/language-server-runtimes-types": "^0.1.45", "@aws/mynah-ui": "^4.35.9" }, @@ -257,6 +267,7 @@ "@types/jsdom": "^21.1.6", "@types/mocha": "^10.0.9", "assert": "^2.0.0", + "c8": "^10.1.2", "jsdom": "^24.0.0", "sinon": "^19.0.2", "ts-mocha": "^11.1.0", @@ -273,7 +284,7 @@ "@aws-sdk/credential-providers": "^3.731.1", "@aws-sdk/types": "^3.734.0", "@aws/chat-client-ui-types": "^0.1.40", - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@types/uuid": "^9.0.8", "@types/vscode": "^1.98.0", "jose": "^5.2.4", @@ -286,9 +297,10 @@ }, "core/aws-lsp-core": { "name": "@aws/lsp-core", - "version": "0.0.11", + "version": "0.0.12", "license": "Apache-2.0", "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "cross-spawn": "7.0.6", "jose": "^5.2.4", @@ -303,6 +315,7 @@ "@types/cross-spawn": "^6.0.2", "@types/mocha": "^10.0.9", "@types/mock-fs": "^4.13.1", + "c8": "^10.1.2", "chai": "^4.3.7", "chai-as-promised": "^7.1.1", "mocha": "^11.0.1", @@ -318,7 +331,7 @@ "name": "@aws/q-agentic-chat-server-integration-tests", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-core": "*" }, "devDependencies": { @@ -1266,44 +1279,31 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.839.0.tgz", - "integrity": "sha512-7zDInY+qltKxeG+9d/97nbs+FWINcAi5bChBrleUQkuQ/dA9pSP1URo/6JlVzD2Ejvksm+hVK6z3VUWZaIAVOw==", + "node_modules/@aws-sdk/client-iam": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-iam/-/client-iam-3.840.0.tgz", + "integrity": "sha512-+HWqpTwXQYhFzgwfjGFHfo+a0mRQwYq29BEYlgfcydo8UOApc1oxsVmEmnYh2nbukaefUkOaMDb1xORybsE6Lw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/credential-provider-node": "3.839.0", - "@aws-sdk/middleware-bucket-endpoint": "3.830.0", - "@aws-sdk/middleware-expect-continue": "3.821.0", - "@aws-sdk/middleware-flexible-checksums": "3.839.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-location-constraint": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-sdk-s3": "3.839.0", - "@aws-sdk/middleware-ssec": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/signature-v4-multi-region": "3.839.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.839.0", - "@aws-sdk/xml-builder": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-node": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.6.0", - "@smithy/eventstream-serde-browser": "^4.0.4", - "@smithy/eventstream-serde-config-resolver": "^4.1.2", - "@smithy/eventstream-serde-node": "^4.0.4", "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/hash-blob-browser": "^4.0.4", "@smithy/hash-node": "^4.0.4", - "@smithy/hash-stream-node": "^4.0.4", "@smithy/invalid-dependency": "^4.0.4", - "@smithy/md5-js": "^4.0.4", "@smithy/middleware-content-length": "^4.0.4", "@smithy/middleware-endpoint": "^4.1.13", "@smithy/middleware-retry": "^4.1.14", @@ -1323,34 +1323,33 @@ "@smithy/util-endpoints": "^3.0.6", "@smithy/util-middleware": "^4.0.4", "@smithy/util-retry": "^4.0.6", - "@smithy/util-stream": "^4.2.2", "@smithy/util-utf8": "^4.0.0", "@smithy/util-waiter": "^4.0.6", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.839.0.tgz", - "integrity": "sha512-AZABysUhbfcwXVlMo97/vwHgsfJNF81wypCAowpqAJkSjP2KrqsqHpb71/RoR2w8JGmEnBBXRD4wIxDhnmifWg==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.840.0.tgz", + "integrity": "sha512-3Zp+FWN2hhmKdpS0Ragi5V2ZPsZNScE3jlbgoJjzjI/roHZqO+e3/+XFN4TlM0DsPKYJNp+1TAjmhxN6rOnfYA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.839.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.6.0", "@smithy/fetch-http-handler": "^5.0.4", @@ -1382,12 +1381,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.839.0.tgz", - "integrity": "sha512-KdwL5RaK7eUIlOpdOoZ5u+2t4X1rdX/MTZgz3IV/aBzjVUoGsp+uUnbyqXomLQSUitPHp72EE/NHDsvWW/IHvQ==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/core": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.840.0.tgz", + "integrity": "sha512-x3Zgb39tF1h2XpU+yA4OAAQlW6LVEfXNlSedSYJ7HGKXqA/E9h3rWQVpYfhXXVVsLdYXdNw5KBUkoAoruoZSZA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@aws-sdk/xml-builder": "3.821.0", "@smithy/core": "^3.6.0", "@smithy/node-config-provider": "^4.1.3", @@ -1407,13 +1408,15 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.839.0.tgz", - "integrity": "sha512-cWTadewPPz1OvObZJB+olrgh8VwcgIVcT293ZUT9V0CMF0UU7QaPwJP7uNXcNxltTh+sk1yhjH4UlcnJigZZbA==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.840.0.tgz", + "integrity": "sha512-EzF6VcJK7XvQ/G15AVEfJzN2mNXU8fcVpXo4bRyr1S6t2q5zx6UPH/XjDbn18xyUmOq01t+r8gG+TmHEVo18fA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -1422,13 +1425,15 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.839.0.tgz", - "integrity": "sha512-fv0BZwrDhWDju4D1MCLT4I2aPjr0dVQ6P+MpqvcGNOA41Oa9UdRhYTV5iuy5NLXzIzoCmnS+XfSq5Kbsf6//xw==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.840.0.tgz", + "integrity": "sha512-wbnUiPGLVea6mXbUh04fu+VJmGkQvmToPeTYdHE8eRZq3NRDi3t3WltT+jArLBKD/4NppRpMjf2ju4coMCz91g==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/node-http-handler": "^4.0.6", "@smithy/property-provider": "^4.0.4", @@ -1442,19 +1447,21 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.839.0.tgz", - "integrity": "sha512-GHm0hF4CiDxIDR7TauMaA6iI55uuSqRxMBcqTAHaTPm6+h1A+MS+ysQMxZ+Jvwtoy8WmfTIGrJVxSCw0sK2hvA==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.840.0.tgz", + "integrity": "sha512-7F290BsWydShHb+7InXd+IjJc3mlEIm9I0R57F/Pjl1xZB69MdkhVGCnuETWoBt4g53ktJd6NEjzm/iAhFXFmw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/credential-provider-env": "3.839.0", - "@aws-sdk/credential-provider-http": "3.839.0", - "@aws-sdk/credential-provider-process": "3.839.0", - "@aws-sdk/credential-provider-sso": "3.839.0", - "@aws-sdk/credential-provider-web-identity": "3.839.0", - "@aws-sdk/nested-clients": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-env": "3.840.0", + "@aws-sdk/credential-provider-http": "3.840.0", + "@aws-sdk/credential-provider-process": "3.840.0", + "@aws-sdk/credential-provider-sso": "3.840.0", + "@aws-sdk/credential-provider-web-identity": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/credential-provider-imds": "^4.0.6", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", @@ -1465,18 +1472,20 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.839.0.tgz", - "integrity": "sha512-7bR+U2h+ft0V8chyeu9Bh/pvau4ZkQMeRt5f0dAULoepZQ77QQVRP4H04yJPTg9DCtqbVULQ3uf5YOp1/08vQw==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.840.0.tgz", + "integrity": "sha512-KufP8JnxA31wxklLm63evUPSFApGcH8X86z3mv9SRbpCm5ycgWIGVCTXpTOdgq6rPZrwT9pftzv2/b4mV/9clg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.839.0", - "@aws-sdk/credential-provider-http": "3.839.0", - "@aws-sdk/credential-provider-ini": "3.839.0", - "@aws-sdk/credential-provider-process": "3.839.0", - "@aws-sdk/credential-provider-sso": "3.839.0", - "@aws-sdk/credential-provider-web-identity": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/credential-provider-env": "3.840.0", + "@aws-sdk/credential-provider-http": "3.840.0", + "@aws-sdk/credential-provider-ini": "3.840.0", + "@aws-sdk/credential-provider-process": "3.840.0", + "@aws-sdk/credential-provider-sso": "3.840.0", + "@aws-sdk/credential-provider-web-identity": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/credential-provider-imds": "^4.0.6", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", @@ -1487,13 +1496,15 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.839.0.tgz", - "integrity": "sha512-qShpekjociUZ+isyQNa0P7jo+0q3N2+0eJDg8SGyP6K6hHTcGfiqxTDps+IKl6NreCPhZCBzyI9mWkP0xSDR6g==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.840.0.tgz", + "integrity": "sha512-HkDQWHy8tCI4A0Ps2NVtuVYMv9cB4y/IuD/TdOsqeRIAT12h8jDb98BwQPNLAImAOwOWzZJ8Cu0xtSpX7CQhMw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", @@ -1503,15 +1514,17 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.839.0.tgz", - "integrity": "sha512-w10zBLHhU8SBQcdrSPMI02haLoRGZg+gP7mH/Er8VhIXfHefbr7o4NirmB0hwdw/YAH8MLlC9jj7c2SJlsNhYA==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.840.0.tgz", + "integrity": "sha512-2qgdtdd6R0Z1y0KL8gzzwFUGmhBHSUx4zy85L2XV1CXhpRNwV71SVWJqLDVV5RVWVf9mg50Pm3AWrUC0xb0pcA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.839.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/token-providers": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/client-sso": "3.840.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/token-providers": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", @@ -1521,14 +1534,16 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.839.0.tgz", - "integrity": "sha512-EvqTc7J1kgmiuxknpCp1S60hyMQvmKxsI5uXzQtcogl/N55rxiXEqnCLI5q6p33q91PJegrcMCM5Q17Afhm5qA==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.840.0.tgz", + "integrity": "sha512-dpEeVXG8uNZSmVXReE4WP0lwoioX2gstk4RnUgrdUE3YaPq8A+hJiVAyc3h+cjDeIqfbsQbZm9qFetKC2LF9dQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/nested-clients": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -1537,12 +1552,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", - "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.840.0.tgz", + "integrity": "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -1551,12 +1568,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", - "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-logger": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.840.0.tgz", + "integrity": "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, @@ -1564,12 +1583,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", - "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.840.0.tgz", + "integrity": "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -1578,14 +1599,16 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.839.0.tgz", - "integrity": "sha512-2u74uRM1JWq6Sf7+3YpjejPM9YkomGt4kWhrmooIBEq1k5r2GTbkH7pNCxBQwBueXM21jAGVDxxeClpTx+5hig==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.840.0.tgz", + "integrity": "sha512-hiiMf7BP5ZkAFAvWRcK67Mw/g55ar7OCrvrynC92hunx/xhMkrgSLM0EXIZ1oTn3uql9kH/qqGF0nqsK6K555A==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", "@smithy/core": "^3.6.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", @@ -1595,23 +1618,25 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.839.0.tgz", - "integrity": "sha512-Glic0pg2THYP3aRhJORwJJBe1JLtJoEdWV/MFZNyzCklfMwEzpWtZAyxy+tQyFmMeW50uBAnh2R0jhMMcf257w==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/nested-clients": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.840.0.tgz", + "integrity": "sha512-LXYYo9+n4hRqnRSIMXLBb+BLz+cEmjMtTudwK1BF6Bn2RfdDv29KuyeDRrPCS3TwKl7ZKmXUmE9n5UuHAPfBpA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.839.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.6.0", "@smithy/fetch-http-handler": "^5.0.4", @@ -1643,12 +1668,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", - "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.840.0.tgz", + "integrity": "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/node-config-provider": "^4.1.3", "@smithy/types": "^4.3.1", "@smithy/util-config-provider": "^4.0.0", @@ -1659,24 +1686,77 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", - "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/token-providers": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.840.0.tgz", + "integrity": "sha512-6BuTOLTXvmgwjK7ve7aTg9JaWFdM5UoMolLVPMyh3wTv9Ufalh8oklxYHUBIxsKkBGO2WiHXytveuxH6tAgTYg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/types": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", + "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-endpoints": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.840.0.tgz", + "integrity": "sha512-eqE9ROdg/Kk0rj3poutyRCFauPDXIf/WSvCqFiRDDVi6QOnCv/M0g2XW8/jSvkJlOyaXkNCptapIp6BeeFFGYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.840.0.tgz", + "integrity": "sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.839.0.tgz", - "integrity": "sha512-MuunkIG1bJVMtTH7MbjXOrhHleU5wjHz5eCAUc6vj7M9rwol71nqjj9b8RLnkO5gsJcKc29Qk8iV6xQuzKWNMw==", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.840.0.tgz", + "integrity": "sha512-Fy5JUEDQU1tPm2Yw/YqRYYc27W5+QD/J4mYvQvdWjUGZLB5q3eLFMGD35Uc28ZFoGMufPr4OCxK/bRfWROBRHQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/node-config-provider": "^4.1.3", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -1693,10 +1773,12 @@ } } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/abort-controller": { + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/abort-controller": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -1705,10 +1787,12 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/node-http-handler": { + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/node-http-handler": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.0.4", "@smithy/protocol-http": "^5.1.2", @@ -1720,10 +1804,12 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/types": { + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/types": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -1731,75 +1817,82 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.731.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.731.0.tgz", - "integrity": "sha512-O4C/UYGgqMsBg21MMApFdgyh8BX568hQhbdoNFmRVTBoSnCZ3w+H4a1wBPX4Gyl0NX+ab6Xxo9rId8HiyPXJ0A==", + "node_modules/@aws-sdk/client-s3": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.839.0.tgz", + "integrity": "sha512-7zDInY+qltKxeG+9d/97nbs+FWINcAi5bChBrleUQkuQ/dA9pSP1URo/6JlVzD2Ejvksm+hVK6z3VUWZaIAVOw==", "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.731.0", - "@aws-sdk/middleware-host-header": "3.731.0", - "@aws-sdk/middleware-logger": "3.731.0", - "@aws-sdk/middleware-recursion-detection": "3.731.0", - "@aws-sdk/middleware-user-agent": "3.731.0", - "@aws-sdk/region-config-resolver": "3.731.0", - "@aws-sdk/types": "3.731.0", - "@aws-sdk/util-endpoints": "3.731.0", - "@aws-sdk/util-user-agent-browser": "3.731.0", - "@aws-sdk/util-user-agent-node": "3.731.0", - "@smithy/config-resolver": "^4.0.0", - "@smithy/core": "^3.0.0", - "@smithy/fetch-http-handler": "^5.0.0", - "@smithy/hash-node": "^4.0.0", - "@smithy/invalid-dependency": "^4.0.0", - "@smithy/middleware-content-length": "^4.0.0", - "@smithy/middleware-endpoint": "^4.0.0", - "@smithy/middleware-retry": "^4.0.0", - "@smithy/middleware-serde": "^4.0.0", - "@smithy/middleware-stack": "^4.0.0", - "@smithy/node-config-provider": "^4.0.0", - "@smithy/node-http-handler": "^4.0.0", - "@smithy/protocol-http": "^5.0.0", - "@smithy/smithy-client": "^4.0.0", - "@smithy/types": "^4.0.0", - "@smithy/url-parser": "^4.0.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/credential-provider-node": "3.839.0", + "@aws-sdk/middleware-bucket-endpoint": "3.830.0", + "@aws-sdk/middleware-expect-continue": "3.821.0", + "@aws-sdk/middleware-flexible-checksums": "3.839.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-location-constraint": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-sdk-s3": "3.839.0", + "@aws-sdk/middleware-ssec": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/signature-v4-multi-region": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.839.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/eventstream-serde-browser": "^4.0.4", + "@smithy/eventstream-serde-config-resolver": "^4.1.2", + "@smithy/eventstream-serde-node": "^4.0.4", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-blob-browser": "^4.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/hash-stream-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/md5-js": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.0", - "@smithy/util-defaults-mode-node": "^4.0.0", - "@smithy/util-endpoints": "^3.0.0", - "@smithy/util-middleware": "^4.0.0", - "@smithy/util-retry": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-stream": "^4.2.2", "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" + "@smithy/util-waiter": "^4.0.6", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.839.0.tgz", - "integrity": "sha512-7QnMApYfQBT441YkxObxt1hZ8TdqZH7h0NdYsvbLdEqGROXBDDT+Wq7ZVfsnKjuVUGQ/t75bIqFn7M8cdyESfA==", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.839.0.tgz", + "integrity": "sha512-AZABysUhbfcwXVlMo97/vwHgsfJNF81wypCAowpqAJkSjP2KrqsqHpb71/RoR2w8JGmEnBBXRD4wIxDhnmifWg==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.839.0", - "@aws-sdk/credential-provider-node": "3.839.0", "@aws-sdk/middleware-host-header": "3.821.0", "@aws-sdk/middleware-logger": "3.821.0", "@aws-sdk/middleware-recursion-detection": "3.821.0", @@ -1840,23 +1933,995 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/client-sso": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.839.0.tgz", - "integrity": "sha512-AZABysUhbfcwXVlMo97/vwHgsfJNF81wypCAowpqAJkSjP2KrqsqHpb71/RoR2w8JGmEnBBXRD4wIxDhnmifWg==", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.839.0.tgz", + "integrity": "sha512-KdwL5RaK7eUIlOpdOoZ5u+2t4X1rdX/MTZgz3IV/aBzjVUoGsp+uUnbyqXomLQSUitPHp72EE/NHDsvWW/IHvQ==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.6.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.839.0.tgz", + "integrity": "sha512-cWTadewPPz1OvObZJB+olrgh8VwcgIVcT293ZUT9V0CMF0UU7QaPwJP7uNXcNxltTh+sk1yhjH4UlcnJigZZbA==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.839.0.tgz", + "integrity": "sha512-fv0BZwrDhWDju4D1MCLT4I2aPjr0dVQ6P+MpqvcGNOA41Oa9UdRhYTV5iuy5NLXzIzoCmnS+XfSq5Kbsf6//xw==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.839.0.tgz", + "integrity": "sha512-GHm0hF4CiDxIDR7TauMaA6iI55uuSqRxMBcqTAHaTPm6+h1A+MS+ysQMxZ+Jvwtoy8WmfTIGrJVxSCw0sK2hvA==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/credential-provider-env": "3.839.0", + "@aws-sdk/credential-provider-http": "3.839.0", + "@aws-sdk/credential-provider-process": "3.839.0", + "@aws-sdk/credential-provider-sso": "3.839.0", + "@aws-sdk/credential-provider-web-identity": "3.839.0", + "@aws-sdk/nested-clients": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.839.0.tgz", + "integrity": "sha512-7bR+U2h+ft0V8chyeu9Bh/pvau4ZkQMeRt5f0dAULoepZQ77QQVRP4H04yJPTg9DCtqbVULQ3uf5YOp1/08vQw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.839.0", + "@aws-sdk/credential-provider-http": "3.839.0", + "@aws-sdk/credential-provider-ini": "3.839.0", + "@aws-sdk/credential-provider-process": "3.839.0", + "@aws-sdk/credential-provider-sso": "3.839.0", + "@aws-sdk/credential-provider-web-identity": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.839.0.tgz", + "integrity": "sha512-qShpekjociUZ+isyQNa0P7jo+0q3N2+0eJDg8SGyP6K6hHTcGfiqxTDps+IKl6NreCPhZCBzyI9mWkP0xSDR6g==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.839.0.tgz", + "integrity": "sha512-w10zBLHhU8SBQcdrSPMI02haLoRGZg+gP7mH/Er8VhIXfHefbr7o4NirmB0hwdw/YAH8MLlC9jj7c2SJlsNhYA==", + "dependencies": { + "@aws-sdk/client-sso": "3.839.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/token-providers": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.839.0.tgz", + "integrity": "sha512-EvqTc7J1kgmiuxknpCp1S60hyMQvmKxsI5uXzQtcogl/N55rxiXEqnCLI5q6p33q91PJegrcMCM5Q17Afhm5qA==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/nested-clients": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", + "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", + "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", + "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.839.0.tgz", + "integrity": "sha512-2u74uRM1JWq6Sf7+3YpjejPM9YkomGt4kWhrmooIBEq1k5r2GTbkH7pNCxBQwBueXM21jAGVDxxeClpTx+5hig==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@smithy/core": "^3.6.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.839.0.tgz", + "integrity": "sha512-Glic0pg2THYP3aRhJORwJJBe1JLtJoEdWV/MFZNyzCklfMwEzpWtZAyxy+tQyFmMeW50uBAnh2R0jhMMcf257w==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.839.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", + "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", + "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.839.0.tgz", + "integrity": "sha512-MuunkIG1bJVMtTH7MbjXOrhHleU5wjHz5eCAUc6vj7M9rwol71nqjj9b8RLnkO5gsJcKc29Qk8iV6xQuzKWNMw==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.731.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.731.0.tgz", + "integrity": "sha512-O4C/UYGgqMsBg21MMApFdgyh8BX568hQhbdoNFmRVTBoSnCZ3w+H4a1wBPX4Gyl0NX+ab6Xxo9rId8HiyPXJ0A==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.731.0", + "@aws-sdk/middleware-host-header": "3.731.0", + "@aws-sdk/middleware-logger": "3.731.0", + "@aws-sdk/middleware-recursion-detection": "3.731.0", + "@aws-sdk/middleware-user-agent": "3.731.0", + "@aws-sdk/region-config-resolver": "3.731.0", + "@aws-sdk/types": "3.731.0", + "@aws-sdk/util-endpoints": "3.731.0", + "@aws-sdk/util-user-agent-browser": "3.731.0", + "@aws-sdk/util-user-agent-node": "3.731.0", + "@smithy/config-resolver": "^4.0.0", + "@smithy/core": "^3.0.0", + "@smithy/fetch-http-handler": "^5.0.0", + "@smithy/hash-node": "^4.0.0", + "@smithy/invalid-dependency": "^4.0.0", + "@smithy/middleware-content-length": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/middleware-retry": "^4.0.0", + "@smithy/middleware-serde": "^4.0.0", + "@smithy/middleware-stack": "^4.0.0", + "@smithy/node-config-provider": "^4.0.0", + "@smithy/node-http-handler": "^4.0.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/smithy-client": "^4.0.0", + "@smithy/types": "^4.0.0", + "@smithy/url-parser": "^4.0.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.0", + "@smithy/util-defaults-mode-node": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "@smithy/util-middleware": "^4.0.0", + "@smithy/util-retry": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.839.0.tgz", + "integrity": "sha512-7QnMApYfQBT441YkxObxt1hZ8TdqZH7h0NdYsvbLdEqGROXBDDT+Wq7ZVfsnKjuVUGQ/t75bIqFn7M8cdyESfA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/credential-provider-node": "3.839.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.839.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/client-sso": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.839.0.tgz", + "integrity": "sha512-AZABysUhbfcwXVlMo97/vwHgsfJNF81wypCAowpqAJkSjP2KrqsqHpb71/RoR2w8JGmEnBBXRD4wIxDhnmifWg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.839.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/core": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.839.0.tgz", + "integrity": "sha512-KdwL5RaK7eUIlOpdOoZ5u+2t4X1rdX/MTZgz3IV/aBzjVUoGsp+uUnbyqXomLQSUitPHp72EE/NHDsvWW/IHvQ==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.6.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.839.0.tgz", + "integrity": "sha512-cWTadewPPz1OvObZJB+olrgh8VwcgIVcT293ZUT9V0CMF0UU7QaPwJP7uNXcNxltTh+sk1yhjH4UlcnJigZZbA==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.839.0.tgz", + "integrity": "sha512-fv0BZwrDhWDju4D1MCLT4I2aPjr0dVQ6P+MpqvcGNOA41Oa9UdRhYTV5iuy5NLXzIzoCmnS+XfSq5Kbsf6//xw==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.839.0.tgz", + "integrity": "sha512-GHm0hF4CiDxIDR7TauMaA6iI55uuSqRxMBcqTAHaTPm6+h1A+MS+ysQMxZ+Jvwtoy8WmfTIGrJVxSCw0sK2hvA==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/credential-provider-env": "3.839.0", + "@aws-sdk/credential-provider-http": "3.839.0", + "@aws-sdk/credential-provider-process": "3.839.0", + "@aws-sdk/credential-provider-sso": "3.839.0", + "@aws-sdk/credential-provider-web-identity": "3.839.0", + "@aws-sdk/nested-clients": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.839.0.tgz", + "integrity": "sha512-7bR+U2h+ft0V8chyeu9Bh/pvau4ZkQMeRt5f0dAULoepZQ77QQVRP4H04yJPTg9DCtqbVULQ3uf5YOp1/08vQw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.839.0", + "@aws-sdk/credential-provider-http": "3.839.0", + "@aws-sdk/credential-provider-ini": "3.839.0", + "@aws-sdk/credential-provider-process": "3.839.0", + "@aws-sdk/credential-provider-sso": "3.839.0", + "@aws-sdk/credential-provider-web-identity": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.839.0.tgz", + "integrity": "sha512-qShpekjociUZ+isyQNa0P7jo+0q3N2+0eJDg8SGyP6K6hHTcGfiqxTDps+IKl6NreCPhZCBzyI9mWkP0xSDR6g==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.839.0.tgz", + "integrity": "sha512-w10zBLHhU8SBQcdrSPMI02haLoRGZg+gP7mH/Er8VhIXfHefbr7o4NirmB0hwdw/YAH8MLlC9jj7c2SJlsNhYA==", + "dependencies": { + "@aws-sdk/client-sso": "3.839.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/token-providers": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.839.0.tgz", + "integrity": "sha512-EvqTc7J1kgmiuxknpCp1S60hyMQvmKxsI5uXzQtcogl/N55rxiXEqnCLI5q6p33q91PJegrcMCM5Q17Afhm5qA==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/nested-clients": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", + "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-logger": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", + "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", + "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.839.0.tgz", + "integrity": "sha512-2u74uRM1JWq6Sf7+3YpjejPM9YkomGt4kWhrmooIBEq1k5r2GTbkH7pNCxBQwBueXM21jAGVDxxeClpTx+5hig==", + "dependencies": { + "@aws-sdk/core": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@smithy/core": "^3.6.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/nested-clients": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.839.0.tgz", + "integrity": "sha512-Glic0pg2THYP3aRhJORwJJBe1JLtJoEdWV/MFZNyzCklfMwEzpWtZAyxy+tQyFmMeW50uBAnh2R0jhMMcf257w==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.839.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.839.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", + "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", + "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.839.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.839.0.tgz", + "integrity": "sha512-MuunkIG1bJVMtTH7MbjXOrhHleU5wjHz5eCAUc6vj7M9rwol71nqjj9b8RLnkO5gsJcKc29Qk8iV6xQuzKWNMw==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.839.0", + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { + "version": "3.731.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.731.0.tgz", + "integrity": "sha512-NrdkJg6oOUbXR2r9WvHP408CLyvST8cJfp1/jP9pemtjvjPoh6NukbCtiSFdOOb1eryP02CnqQWItfJC1p2Y/Q==", + "dependencies": { + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { + "version": "3.731.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.731.0.tgz", + "integrity": "sha512-riztxTAfncFS9yQWcBJffGgOgLoKSa63ph+rxWJxKl6BHAmWEvHICj1qDcVmnWfIcvJ5cClclY75l9qKaUH7rQ==", + "dependencies": { + "@aws-sdk/types": "3.731.0", + "@smithy/types": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.840.0.tgz", + "integrity": "sha512-h+mu89Wk81Ne+B624GT/pBM5VjuAZueSeQNixhgtQ1QHi6bZzrpz8+lvMSibKO+kXFyQsTLzkyibbxnhLpWQZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-node": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.6.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/middleware-retry": "^4.1.14", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.5", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.21", + "@smithy/util-defaults-mode-node": "^4.0.21", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/client-sso": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.840.0.tgz", + "integrity": "sha512-3Zp+FWN2hhmKdpS0Ragi5V2ZPsZNScE3jlbgoJjzjI/roHZqO+e3/+XFN4TlM0DsPKYJNp+1TAjmhxN6rOnfYA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.839.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.6.0", "@smithy/fetch-http-handler": "^5.0.4", @@ -1888,12 +2953,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/core": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.839.0.tgz", - "integrity": "sha512-KdwL5RaK7eUIlOpdOoZ5u+2t4X1rdX/MTZgz3IV/aBzjVUoGsp+uUnbyqXomLQSUitPHp72EE/NHDsvWW/IHvQ==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/core": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.840.0.tgz", + "integrity": "sha512-x3Zgb39tF1h2XpU+yA4OAAQlW6LVEfXNlSedSYJ7HGKXqA/E9h3rWQVpYfhXXVVsLdYXdNw5KBUkoAoruoZSZA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@aws-sdk/xml-builder": "3.821.0", "@smithy/core": "^3.6.0", "@smithy/node-config-provider": "^4.1.3", @@ -1913,13 +2980,15 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.839.0.tgz", - "integrity": "sha512-cWTadewPPz1OvObZJB+olrgh8VwcgIVcT293ZUT9V0CMF0UU7QaPwJP7uNXcNxltTh+sk1yhjH4UlcnJigZZbA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.840.0.tgz", + "integrity": "sha512-EzF6VcJK7XvQ/G15AVEfJzN2mNXU8fcVpXo4bRyr1S6t2q5zx6UPH/XjDbn18xyUmOq01t+r8gG+TmHEVo18fA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -1928,13 +2997,15 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.839.0.tgz", - "integrity": "sha512-fv0BZwrDhWDju4D1MCLT4I2aPjr0dVQ6P+MpqvcGNOA41Oa9UdRhYTV5iuy5NLXzIzoCmnS+XfSq5Kbsf6//xw==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.840.0.tgz", + "integrity": "sha512-wbnUiPGLVea6mXbUh04fu+VJmGkQvmToPeTYdHE8eRZq3NRDi3t3WltT+jArLBKD/4NppRpMjf2ju4coMCz91g==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/node-http-handler": "^4.0.6", "@smithy/property-provider": "^4.0.4", @@ -1948,19 +3019,21 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.839.0.tgz", - "integrity": "sha512-GHm0hF4CiDxIDR7TauMaA6iI55uuSqRxMBcqTAHaTPm6+h1A+MS+ysQMxZ+Jvwtoy8WmfTIGrJVxSCw0sK2hvA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.840.0.tgz", + "integrity": "sha512-7F290BsWydShHb+7InXd+IjJc3mlEIm9I0R57F/Pjl1xZB69MdkhVGCnuETWoBt4g53ktJd6NEjzm/iAhFXFmw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/credential-provider-env": "3.839.0", - "@aws-sdk/credential-provider-http": "3.839.0", - "@aws-sdk/credential-provider-process": "3.839.0", - "@aws-sdk/credential-provider-sso": "3.839.0", - "@aws-sdk/credential-provider-web-identity": "3.839.0", - "@aws-sdk/nested-clients": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/credential-provider-env": "3.840.0", + "@aws-sdk/credential-provider-http": "3.840.0", + "@aws-sdk/credential-provider-process": "3.840.0", + "@aws-sdk/credential-provider-sso": "3.840.0", + "@aws-sdk/credential-provider-web-identity": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/credential-provider-imds": "^4.0.6", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", @@ -1971,18 +3044,20 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.839.0.tgz", - "integrity": "sha512-7bR+U2h+ft0V8chyeu9Bh/pvau4ZkQMeRt5f0dAULoepZQ77QQVRP4H04yJPTg9DCtqbVULQ3uf5YOp1/08vQw==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.840.0.tgz", + "integrity": "sha512-KufP8JnxA31wxklLm63evUPSFApGcH8X86z3mv9SRbpCm5ycgWIGVCTXpTOdgq6rPZrwT9pftzv2/b4mV/9clg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.839.0", - "@aws-sdk/credential-provider-http": "3.839.0", - "@aws-sdk/credential-provider-ini": "3.839.0", - "@aws-sdk/credential-provider-process": "3.839.0", - "@aws-sdk/credential-provider-sso": "3.839.0", - "@aws-sdk/credential-provider-web-identity": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/credential-provider-env": "3.840.0", + "@aws-sdk/credential-provider-http": "3.840.0", + "@aws-sdk/credential-provider-ini": "3.840.0", + "@aws-sdk/credential-provider-process": "3.840.0", + "@aws-sdk/credential-provider-sso": "3.840.0", + "@aws-sdk/credential-provider-web-identity": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/credential-provider-imds": "^4.0.6", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", @@ -1993,13 +3068,15 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.839.0.tgz", - "integrity": "sha512-qShpekjociUZ+isyQNa0P7jo+0q3N2+0eJDg8SGyP6K6hHTcGfiqxTDps+IKl6NreCPhZCBzyI9mWkP0xSDR6g==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.840.0.tgz", + "integrity": "sha512-HkDQWHy8tCI4A0Ps2NVtuVYMv9cB4y/IuD/TdOsqeRIAT12h8jDb98BwQPNLAImAOwOWzZJ8Cu0xtSpX7CQhMw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", @@ -2009,15 +3086,17 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.839.0.tgz", - "integrity": "sha512-w10zBLHhU8SBQcdrSPMI02haLoRGZg+gP7mH/Er8VhIXfHefbr7o4NirmB0hwdw/YAH8MLlC9jj7c2SJlsNhYA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.840.0.tgz", + "integrity": "sha512-2qgdtdd6R0Z1y0KL8gzzwFUGmhBHSUx4zy85L2XV1CXhpRNwV71SVWJqLDVV5RVWVf9mg50Pm3AWrUC0xb0pcA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.839.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/token-providers": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/client-sso": "3.840.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/token-providers": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", @@ -2027,14 +3106,16 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.839.0.tgz", - "integrity": "sha512-EvqTc7J1kgmiuxknpCp1S60hyMQvmKxsI5uXzQtcogl/N55rxiXEqnCLI5q6p33q91PJegrcMCM5Q17Afhm5qA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.840.0.tgz", + "integrity": "sha512-dpEeVXG8uNZSmVXReE4WP0lwoioX2gstk4RnUgrdUE3YaPq8A+hJiVAyc3h+cjDeIqfbsQbZm9qFetKC2LF9dQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/nested-clients": "3.839.0", - "@aws-sdk/types": "3.821.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -2043,12 +3124,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", - "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.840.0.tgz", + "integrity": "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -2057,12 +3140,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-logger": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", - "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-logger": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.840.0.tgz", + "integrity": "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, @@ -2070,12 +3155,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", - "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.840.0.tgz", + "integrity": "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -2084,14 +3171,16 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.839.0.tgz", - "integrity": "sha512-2u74uRM1JWq6Sf7+3YpjejPM9YkomGt4kWhrmooIBEq1k5r2GTbkH7pNCxBQwBueXM21jAGVDxxeClpTx+5hig==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.840.0.tgz", + "integrity": "sha512-hiiMf7BP5ZkAFAvWRcK67Mw/g55ar7OCrvrynC92hunx/xhMkrgSLM0EXIZ1oTn3uql9kH/qqGF0nqsK6K555A==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.839.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", "@smithy/core": "^3.6.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", @@ -2101,23 +3190,25 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/nested-clients": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.839.0.tgz", - "integrity": "sha512-Glic0pg2THYP3aRhJORwJJBe1JLtJoEdWV/MFZNyzCklfMwEzpWtZAyxy+tQyFmMeW50uBAnh2R0jhMMcf257w==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/nested-clients": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.840.0.tgz", + "integrity": "sha512-LXYYo9+n4hRqnRSIMXLBb+BLz+cEmjMtTudwK1BF6Bn2RfdDv29KuyeDRrPCS3TwKl7ZKmXUmE9n5UuHAPfBpA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.839.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.839.0", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.840.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.840.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.6.0", "@smithy/fetch-http-handler": "^5.0.4", @@ -2149,12 +3240,14 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", - "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.840.0.tgz", + "integrity": "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", + "@aws-sdk/types": "3.840.0", "@smithy/node-config-provider": "^4.1.3", "@smithy/types": "^4.3.1", "@smithy/util-config-provider": "^4.0.0", @@ -2165,44 +3258,31 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", - "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", - "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/types": "^4.3.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.839.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.839.0.tgz", - "integrity": "sha512-MuunkIG1bJVMtTH7MbjXOrhHleU5wjHz5eCAUc6vj7M9rwol71nqjj9b8RLnkO5gsJcKc29Qk8iV6xQuzKWNMw==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/token-providers": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.840.0.tgz", + "integrity": "sha512-6BuTOLTXvmgwjK7ve7aTg9JaWFdM5UoMolLVPMyh3wTv9Ufalh8oklxYHUBIxsKkBGO2WiHXytveuxH6tAgTYg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.839.0", - "@aws-sdk/types": "3.821.0", - "@smithy/node-config-provider": "^4.1.3", + "@aws-sdk/core": "3.840.0", + "@aws-sdk/nested-clients": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/abort-controller": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", - "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", + "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -2211,62 +3291,66 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/node-http-handler": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", - "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-endpoints": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.840.0.tgz", + "integrity": "sha512-eqE9ROdg/Kk0rj3poutyRCFauPDXIf/WSvCqFiRDDVi6QOnCv/M0g2XW8/jSvkJlOyaXkNCptapIp6BeeFFGYw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/querystring-builder": "^4.0.4", + "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", + "@smithy/util-endpoints": "^3.0.6", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/types": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", - "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.840.0.tgz", + "integrity": "sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { - "version": "3.731.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.731.0.tgz", - "integrity": "sha512-NrdkJg6oOUbXR2r9WvHP408CLyvST8cJfp1/jP9pemtjvjPoh6NukbCtiSFdOOb1eryP02CnqQWItfJC1p2Y/Q==", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.840.0.tgz", + "integrity": "sha512-Fy5JUEDQU1tPm2Yw/YqRYYc27W5+QD/J4mYvQvdWjUGZLB5q3eLFMGD35Uc28ZFoGMufPr4OCxK/bRfWROBRHQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.0.0", + "@aws-sdk/middleware-user-agent": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { - "version": "3.731.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.731.0.tgz", - "integrity": "sha512-riztxTAfncFS9yQWcBJffGgOgLoKSa63ph+rxWJxKl6BHAmWEvHICj1qDcVmnWfIcvJ5cClclY75l9qKaUH7rQ==", - "dependencies": { - "@aws-sdk/types": "3.731.0", - "@smithy/types": "^4.0.0", - "@smithy/util-endpoints": "^3.0.0", - "tslib": "^2.6.2" }, - "engines": { - "node": ">=18.0.0" + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/abort-controller": { + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/abort-controller": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" @@ -2275,10 +3359,12 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/node-http-handler": { + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/node-http-handler": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.0.4", "@smithy/protocol-http": "^5.1.2", @@ -2290,10 +3376,12 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/types": { + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/types": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -4025,11 +5113,12 @@ "link": true }, "node_modules/@aws/language-server-runtimes": { - "version": "0.2.108", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.108.tgz", - "integrity": "sha512-AdvT1RMlKxaaligGavR4QzhcoVHdemia1KyMEDdBzGOoQ4sODu/HSK8VVAwp4hoGjxbOtWUsaPcDduIf00oqvg==", + "version": "0.2.112", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.112.tgz", + "integrity": "sha512-BoUF3VwkvVE5AjGtt12tPG/ffP8xexjwr/6kJj6vinntoBgxOqXASKRpVJSsvU14ZN3ac2T/deogNPqAgz0P3Q==", + "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes-types": "^0.1.45", + "@aws/language-server-runtimes-types": "^0.1.48", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.200.0", "@opentelemetry/core": "^2.0.0", @@ -4056,9 +5145,10 @@ } }, "node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.45", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.45.tgz", - "integrity": "sha512-QmINGPqPUYMyz4lmJVf+r+muAlVocpTvxSmK/VJytgtxVnvjIy56gL0fd8PpUOFjWUxjqFKs6DfbMqKx5hal3A==", + "version": "0.1.48", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.48.tgz", + "integrity": "sha512-Z4umzi/i64rOqTnBTuOz/2ERGi33+EiKBWvd3YkG90amCCle13Z9/SuklJjMTA0Vt3tzO9zc07VBv0JI1ZdbJQ==", + "license": "Apache-2.0", "dependencies": { "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5" @@ -9115,6 +10205,13 @@ "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", "dev": true }, + "node_modules/@types/encoding-japanese": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/encoding-japanese/-/encoding-japanese-2.2.1.tgz", + "integrity": "sha512-6jjepuTusvySxMLP7W6usamlbgf0F4sIDvm7EzYePjLHY7zWUv4yz2PLUnu0vuNVtXOTLu2cRdFcDg40J5Owsw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -14273,6 +15370,15 @@ "node": ">= 0.8" } }, + "node_modules/encoding-japanese": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz", + "integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==", + "license": "MIT", + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -27083,11 +28189,11 @@ }, "server/aws-lsp-antlr4": { "name": "@aws/lsp-antlr4", - "version": "0.1.15", + "version": "0.1.16", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11" + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12" }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.24.1", @@ -27128,6 +28234,7 @@ "name": "@aws/lsp-buildspec", "version": "0.0.1", "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-json": "*", "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", @@ -27138,6 +28245,7 @@ "name": "@aws/lsp-cloudformation", "version": "0.0.1", "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-core": "*", "@aws/lsp-json": "*", "vscode-languageserver": "^9.0.1", @@ -27146,7 +28254,7 @@ }, "server/aws-lsp-codewhisperer": { "name": "@aws/lsp-codewhisperer", - "version": "0.0.65", + "version": "0.0.66", "bundleDependencies": [ "@amzn/codewhisperer-streaming", "@amzn/amazon-q-developer-streaming-client" @@ -27159,8 +28267,8 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.40", - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "@modelcontextprotocol/sdk": "^1.9.0", "@smithy/node-http-handler": "^2.5.0", "adm-zip": "^0.5.10", @@ -27171,6 +28279,7 @@ "chokidar": "^4.0.3", "deepmerge": "^4.3.1", "diff": "^7.0.0", + "encoding-japanese": "^2.2.0", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "fdir": "^6.4.3", @@ -27195,6 +28304,7 @@ "@types/adm-zip": "^0.5.5", "@types/archiver": "^6.0.2", "@types/diff": "^7.0.2", + "@types/encoding-japanese": "^2.2.1", "@types/ignore-walk": "^4.0.3", "@types/local-indexing": "file:./types/types-local-indexing-1.1.0.tgz", "@types/lokijs": "^1.5.14", @@ -27298,8 +28408,8 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", "https-proxy-agent": "^7.0.5", @@ -27341,11 +28451,11 @@ }, "server/aws-lsp-json": { "name": "@aws/lsp-json", - "version": "0.1.15", + "version": "0.1.16", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" }, @@ -27361,8 +28471,8 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, "devDependencies": { @@ -27400,10 +28510,10 @@ }, "server/aws-lsp-partiql": { "name": "@aws/lsp-partiql", - "version": "0.0.14", + "version": "0.0.15", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "antlr4-c3": "3.4.2", "antlr4ng": "3.0.14", "web-tree-sitter": "0.22.6" @@ -27425,19 +28535,20 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" } }, "server/aws-lsp-yaml": { "name": "@aws/lsp-yaml", - "version": "0.1.15", + "version": "0.1.16", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", "yaml-language-server": "1.13.0" @@ -27450,7 +28561,7 @@ "name": "@amzn/device-sso-auth-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "vscode-languageserver": "^9.0.1" }, "devDependencies": { @@ -27461,7 +28572,7 @@ "name": "@aws/hello-world-lsp", "version": "0.0.1", "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/package.json b/package.json index 18dcee99bc..f9c1952c1c 100644 --- a/package.json +++ b/package.json @@ -30,17 +30,19 @@ "test-integ": "npm run compile && npm run test-integ --workspaces --if-present", "test-unit": "npm run compile && npm run test-unit --workspaces --if-present", "test:coverage": "npm run compile && npm run test:coverage --workspaces --if-present", - "coverage:report": "c8 report --reporter=html --reporter=text", - "coverage:check": "c8 check-coverage --lines 70 --functions 70 --branches 70 --statements 70", + "coverage:report": "npm run coverage:report --workspaces --if-present", + "coverage:check": "npm run coverage:check --workspaces --if-present", "package": "npm run compile && npm run package --workspaces --if-present" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.108", + "@aws/language-server-runtimes": "^0.2.112", "@smithy/types": "4.2.0", "clean": "^4.0.2", "typescript": "^5.8.2" }, "devDependencies": { + "@aws-sdk/client-iam": "^3.840.0", + "@aws-sdk/client-sts": "^3.840.0", "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", "@types/ignore-walk": "^4.0.3", diff --git a/server/aws-lsp-antlr4/CHANGELOG.md b/server/aws-lsp-antlr4/CHANGELOG.md index e95d3ab122..d3d1442ed6 100644 --- a/server/aws-lsp-antlr4/CHANGELOG.md +++ b/server/aws-lsp-antlr4/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.16](https://github.com/aws/language-servers/compare/lsp-antlr4/v0.1.15...lsp-antlr4/v0.1.16) (2025-07-17) + + +### Bug Fixes + +* use document change events for auto trigger classifier input ([#1912](https://github.com/aws/language-servers/issues/1912)) ([2204da6](https://github.com/aws/language-servers/commit/2204da6193f2030ee546f61c969b1a664d8025e3)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.11 to ^0.0.12 + ## [0.1.15](https://github.com/aws/language-servers/compare/lsp-antlr4/v0.1.14...lsp-antlr4/v0.1.15) (2025-07-02) diff --git a/server/aws-lsp-antlr4/package.json b/server/aws-lsp-antlr4/package.json index c80d1dee9c..521ab5f784 100644 --- a/server/aws-lsp-antlr4/package.json +++ b/server/aws-lsp-antlr4/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-antlr4", - "version": "0.1.15", + "version": "0.1.16", "description": "ANTLR4 language server", "main": "out/index.js", "repository": { @@ -28,8 +28,8 @@ "clean": "rm -rf node_modules" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11" + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12" }, "peerDependencies": { "antlr4-c3": ">=3.4 < 4", diff --git a/server/aws-lsp-buildspec/package.json b/server/aws-lsp-buildspec/package.json index c36113d855..cda2641124 100644 --- a/server/aws-lsp-buildspec/package.json +++ b/server/aws-lsp-buildspec/package.json @@ -7,8 +7,9 @@ "compile": "tsc --build" }, "dependencies": { - "@aws/lsp-yaml": "*", + "@aws/language-server-runtimes": "^0.2.112", "@aws/lsp-json": "*", + "@aws/lsp-yaml": "*", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" } diff --git a/server/aws-lsp-cloudformation/package.json b/server/aws-lsp-cloudformation/package.json index 70bf4e56ae..6f588655eb 100644 --- a/server/aws-lsp-cloudformation/package.json +++ b/server/aws-lsp-cloudformation/package.json @@ -7,6 +7,7 @@ "compile": "tsc --build" }, "dependencies": { + "@aws/language-server-runtimes": "^0.2.112", "@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 1c1613bf29..049bf557b8 100644 --- a/server/aws-lsp-codewhisperer/CHANGELOG.md +++ b/server/aws-lsp-codewhisperer/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## [0.0.66](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.65...lsp-codewhisperer/v0.0.66) (2025-07-17) + + +### Features + +* add active user tracking with state persistence ([#1892](https://github.com/aws/language-servers/issues/1892)) ([a5587c5](https://github.com/aws/language-servers/commit/a5587c59e4a07074ad8afba930c6596dc28c693b)) +* add conversation compaction ([#1895](https://github.com/aws/language-servers/issues/1895)) ([8bb7144](https://github.com/aws/language-servers/commit/8bb7144e45cfce6cc9337fd49de7edbee61105b8)) +* adding streakLength back to UTDE telemetry ([#1902](https://github.com/aws/language-servers/issues/1902)) ([152f1c5](https://github.com/aws/language-servers/commit/152f1c5f23698f8c574120bcf4f2214e4540e7e6)) + + +### Bug Fixes + +* add proper encoding support for shell output ([#1903](https://github.com/aws/language-servers/issues/1903)) ([44a6d62](https://github.com/aws/language-servers/commit/44a6d629af7702662a02f384a6a542c0d72ccc39)) +* align auto trigger classifier with documentChangeEvent ([#1914](https://github.com/aws/language-servers/issues/1914)) ([f308e17](https://github.com/aws/language-servers/commit/f308e17912df0b8f03f4e655cc34f2f875f4e65c)) +* **amazonq:** replacing image's large binary in log ([#1905](https://github.com/aws/language-servers/issues/1905)) ([a06ed62](https://github.com/aws/language-servers/commit/a06ed626e118c5f846e494630ef0577ce1ace628)) +* editor state does not use the same language id as file context ([#1924](https://github.com/aws/language-servers/issues/1924)) ([c10866d](https://github.com/aws/language-servers/commit/c10866d70070173aba63be1c78945a4da6129018)) +* pinned `@Code` symbols do not persist between IDE sessions ([#1887](https://github.com/aws/language-servers/issues/1887)) ([b5c715f](https://github.com/aws/language-servers/commit/b5c715ff5ee303c2d48ffb9c1c6c98a9d985e2f1)) +* replace thinking with working and replace stop with cancel ([#1922](https://github.com/aws/language-servers/issues/1922)) ([371e731](https://github.com/aws/language-servers/commit/371e731545f7572d3356d061cd8b94db35e4c333)) +* should trigger edits if one of the following lines is non-empty ([#1915](https://github.com/aws/language-servers/issues/1915)) ([b298602](https://github.com/aws/language-servers/commit/b2986026293e26bff0cacbaf1554999c12fb429c)) +* treat `echo`/`find`/`grep` as mutating ([#1921](https://github.com/aws/language-servers/issues/1921)) ([ef801a3](https://github.com/aws/language-servers/commit/ef801a3b9c435c25899eaa3712cabf6d5c4b9922)) +* use document change events for auto trigger classifier input ([#1912](https://github.com/aws/language-servers/issues/1912)) ([2204da6](https://github.com/aws/language-servers/commit/2204da6193f2030ee546f61c969b1a664d8025e3)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.11 to ^0.0.12 + ## [0.0.65](https://github.com/aws/language-servers/compare/lsp-codewhisperer/v0.0.64...lsp-codewhisperer/v0.0.65) (2025-07-15) diff --git a/server/aws-lsp-codewhisperer/package.json b/server/aws-lsp-codewhisperer/package.json index afe9be727c..603950d492 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.65", + "version": "0.0.66", "description": "CodeWhisperer Language Server", "main": "out/index.js", "repository": { @@ -36,8 +36,8 @@ "@aws-sdk/util-arn-parser": "^3.723.0", "@aws-sdk/util-retry": "^3.374.0", "@aws/chat-client-ui-types": "^0.1.40", - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "@modelcontextprotocol/sdk": "^1.9.0", "@smithy/node-http-handler": "^2.5.0", "adm-zip": "^0.5.10", @@ -48,6 +48,7 @@ "chokidar": "^4.0.3", "deepmerge": "^4.3.1", "diff": "^7.0.0", + "encoding-japanese": "^2.2.0", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "fdir": "^6.4.3", @@ -55,6 +56,7 @@ "got": "^11.8.5", "hpagent": "^1.2.0", "ignore": "^7.0.3", + "image-size": "^2.0.2", "js-md5": "^0.8.3", "jszip": "^3.10.1", "lokijs": "^1.5.12", @@ -65,15 +67,14 @@ "vscode-uri": "^3.1.0", "ws": "^8.18.0", "xml2js": "^0.6.2", - "xmlbuilder2": "^3.1.1", - "image-size": "^2.0.2" + "xmlbuilder2": "^3.1.1" }, "devDependencies": { "@types/adm-zip": "^0.5.5", "@types/archiver": "^6.0.2", "@types/diff": "^7.0.2", + "@types/encoding-japanese": "^2.2.1", "@types/ignore-walk": "^4.0.3", - "ignore-walk": "^7.0.0", "@types/local-indexing": "file:./types/types-local-indexing-1.1.0.tgz", "@types/lokijs": "^1.5.14", "@types/uuid": "^9.0.8", @@ -81,6 +82,7 @@ "assert": "^2.1.0", "c8": "^10.1.2", "copyfiles": "^2.4.1", + "ignore-walk": "^7.0.0", "mock-fs": "^5.2.0", "sinon": "^19.0.2", "ts-loader": "^9.4.4", 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 028759030b..4169675bdc 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 @@ -524,6 +524,59 @@ describe('AgenticChatController', () => { assert.deepStrictEqual(requestInput.conversationState?.history, expectedRequestHistory) }) + it('includes chat history from the database in the compaction request input', async () => { + // Mock chat history + const mockHistory = [ + { + type: 'prompt', + body: 'Previous question', + userInputMessageContext: { + toolResults: [], + }, + }, + { type: 'answer', body: 'Previous answer' }, + ] + const expectedRequestHistory = [ + { + userInputMessage: { + content: 'Previous question', + origin: 'IDE', + userInputMessageContext: { toolResults: [] }, + userIntent: undefined, + }, + }, + { + assistantResponseMessage: { + content: 'Previous answer', + messageId: undefined, + toolUses: [], + }, + }, + ] + + chatDbInitializedStub.returns(true) + getMessagesStub.returns(mockHistory) + + // Make the request + const result = await chatController.onChatPrompt( + { tabId: mockTabId, prompt: { prompt: '', command: '/compact' } }, + mockCancellationToken + ) + + // Verify that history was requested from the db + sinon.assert.calledWith(getMessagesStub, mockTabId) + + assert.ok(generateAssistantResponseStub.calledOnce) + + // Verify that the history was passed to the request + const requestInput: GenerateAssistantResponseCommandInput = generateAssistantResponseStub.firstCall.firstArg + assert.deepStrictEqual(requestInput.conversationState?.history, expectedRequestHistory) + assert.deepStrictEqual( + requestInput.conversationState?.currentMessage?.userInputMessage?.content, + constants.COMPACTION_PROMPT + ) + }) + it('skips adding user message to history when token is cancelled', async () => { // Create a cancellation token that is already cancelled const cancelledToken = { 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 857cdf1654..08f2680c4c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -16,6 +16,28 @@ import { ToolUseEvent, ImageBlock, } from '@amzn/codewhisperer-streaming' +import { + FS_READ, + FS_WRITE, + FS_REPLACE, + LIST_DIRECTORY, + GREP_SEARCH, + FILE_SEARCH, + EXECUTE_BASH, + Q_CODE_REVIEW, + BUTTON_RUN_SHELL_COMMAND, + BUTTON_REJECT_SHELL_COMMAND, + BUTTON_REJECT_MCP_TOOL, + BUTTON_ALLOW_TOOLS, + BUTTON_UNDO_CHANGES, + BUTTON_UNDO_ALL_CHANGES, + BUTTON_STOP_SHELL_COMMAND, + BUTTON_PAIDTIER_UPGRADE_Q_LEARNMORE, + BUTTON_PAIDTIER_UPGRADE_Q, + SUFFIX_PERMISSION, + SUFFIX_UNDOALL, + SUFFIX_EXPLANATION, +} from './constants/toolConstants' import { SendMessageCommandInput, SendMessageCommandOutput, @@ -43,7 +65,6 @@ import { FollowUpClickParams, ListAvailableModelsParams, ListAvailableModelsResult, - Model, OpenFileDialogParams, OpenFileDialogResult, } from '@aws/language-server-runtimes/protocol' @@ -59,7 +80,6 @@ import { ListConversationsParams, ListMcpServersParams, McpServerClickParams, - McpServerClickResult, TabBarActionParams, CreatePromptParams, FileClickParams, @@ -102,7 +122,6 @@ import { getSsoConnectionType, isUsageLimitError, isNullish, - enabledModelSelection, getOriginFromClientInfo, } from '../../shared/utils' import { HELP_MESSAGE, loadingMessage } from '../chat/constants' @@ -112,7 +131,6 @@ import { AmazonQServicePendingProfileError, AmazonQServicePendingSigninError, } from '../../shared/amazonQServiceManager/errors' -import { AmazonQIAMServiceManager } from '../../shared/amazonQServiceManager/AmazonQIAMServiceManager' import { AmazonQBaseServiceManager } from '../../shared/amazonQServiceManager/BaseAmazonQServiceManager' import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/AmazonQTokenServiceManager' import { AmazonQWorkspaceConfig } from '../../shared/amazonQServiceManager/configurationUtils' @@ -124,7 +142,7 @@ import { } from './agenticChatEventParser' import { ChatSessionService } from '../chat/chatSessionService' import { AgenticChatResultStream, progressPrefix, ResultStreamWriter } from './agenticChatResultStream' -import { executeToolMessage, toolResultMessage } from './textFormatting' +import { toolResultMessage } from './textFormatting' import { AdditionalContentEntryAddition, AgenticChatTriggerContext, @@ -159,6 +177,14 @@ import { RESPONSE_TIMEOUT_MS, RESPONSE_TIMEOUT_PARTIAL_MSG, DEFAULT_MODEL_ID, + COMPACTION_BODY, + COMPACTION_HEADER_BODY, + DEFAULT_MACOS_RUN_SHORTCUT, + DEFAULT_WINDOW_RUN_SHORTCUT, + DEFAULT_MACOS_REJECT_SHORTCUT, + DEFAULT_WINDOW_REJECT_SHORTCUT, + DEFAULT_MACOS_STOP_SHORTCUT, + DEFAULT_WINDOW_STOP_SHORTCUT, } from './constants/constants' import { AgenticChatError, @@ -180,7 +206,6 @@ import { McpTool } from './tools/mcp/mcpTool' import { freeTierLimitUserMsg, onPaidTierLearnMore, - paidTierLearnMoreUrl, paidTierManageSubscription, PaidTierMode, qProName, @@ -190,6 +215,7 @@ import { MODEL_OPTIONS, MODEL_OPTIONS_FOR_REGION } from './constants/modelSelect import { DEFAULT_IMAGE_VERIFICATION_OPTIONS, verifyServerImage } from '../../shared/imageVerification' import { sanitize } from '@aws/lsp-core/out/util/path' import { getLatestAvailableModel } from './utils/agenticChatControllerHelper' +import { ActiveUserTracker } from '../../shared/activeUserTracker' import { UserContext } from '../../client/token/codewhispererbearertokenclient' import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' @@ -233,6 +259,7 @@ export class AgenticChatController implements ChatHandlers { #mcpEventHandler: McpEventHandler #paidTierMode: PaidTierMode | undefined #origin: Origin + #activeUserTracker: ActiveUserTracker // latency metrics #llmRequestStartTime: number = 0 @@ -260,7 +287,9 @@ export class AgenticChatController implements ChatHandlers { #getMessageIdForToolUse(toolType: string | undefined, toolUse: ToolUse): string { const toolUseId = toolUse.toolUseId! // Return plain toolUseId for executeBash, add "_permission" suffix for all other tools - return toolUse.name === 'executeBash' || toolType === 'executeBash' ? toolUseId : `${toolUseId}_permission` + return toolUse.name === EXECUTE_BASH || toolType === EXECUTE_BASH + ? toolUseId + : `${toolUseId}${SUFFIX_PERMISSION}` } /** @@ -283,6 +312,15 @@ export class AgenticChatController implements ChatHandlers { this.#features.logging.info(`System Information: ${JSON.stringify(systemInfo)}`) } + /** + * Determines the appropriate message ID for a compaction confirmation + * @param messageId The original messageId + * @returns The message ID to use + */ + #getMessageIdForCompact(messageId: string): string { + return `${messageId}_compact` + } + constructor( chatSessionManagementService: ChatSessionManagementService, features: Features, @@ -316,6 +354,7 @@ export class AgenticChatController implements ChatHandlers { ) this.#mcpEventHandler = new McpEventHandler(features, telemetryService) this.#origin = getOriginFromClientInfo(this.#features.lsp.getClientInitializeParams()?.clientInfo?.name) + this.#activeUserTracker = ActiveUserTracker.getInstance(this.#features) } async onExecuteCommand(params: ExecuteCommandParams, _token: CancellationToken): Promise { @@ -335,18 +374,18 @@ export class AgenticChatController implements ChatHandlers { this.#log(`onButtonClick event with params: ${JSON.stringify(params)}`) const session = this.#chatSessionManagementService.getSession(params.tabId) if ( - params.buttonId === 'run-shell-command' || - params.buttonId === 'reject-shell-command' || - params.buttonId === 'reject-mcp-tool' || - params.buttonId === 'allow-tools' + params.buttonId === BUTTON_RUN_SHELL_COMMAND || + params.buttonId === BUTTON_REJECT_SHELL_COMMAND || + params.buttonId === BUTTON_REJECT_MCP_TOOL || + params.buttonId === BUTTON_ALLOW_TOOLS ) { if (!session.data) { return { success: false, failureReason: `could not find chat session for tab: ${params.tabId} ` } } // For 'allow-tools', remove suffix as permission card needs to be seperate from file list card const messageId = - params.buttonId === 'allow-tools' && params.messageId.endsWith('_permission') - ? params.messageId.replace('_permission', '') + params.buttonId === BUTTON_ALLOW_TOOLS && params.messageId.endsWith(SUFFIX_PERMISSION) + ? params.messageId.replace(SUFFIX_PERMISSION, '') : params.messageId const handler = session.data.getDeferredToolExecution(messageId) @@ -356,7 +395,7 @@ export class AgenticChatController implements ChatHandlers { failureReason: `could not find deferred tool execution for message: ${messageId} `, } } - params.buttonId === 'reject-shell-command' || params.buttonId === 'reject-mcp-tool' + params.buttonId === BUTTON_REJECT_SHELL_COMMAND || params.buttonId === BUTTON_REJECT_MCP_TOOL ? (() => { handler.reject(new ToolApprovalException('Command was rejected.', true)) this.#stoppedToolUses.add(messageId) @@ -365,7 +404,7 @@ export class AgenticChatController implements ChatHandlers { return { success: true, } - } else if (params.buttonId === 'undo-changes') { + } else if (params.buttonId === BUTTON_UNDO_CHANGES) { const toolUseId = params.messageId try { await this.#undoFileChange(toolUseId, session.data) @@ -384,21 +423,21 @@ export class AgenticChatController implements ChatHandlers { return { success: true, } - } else if (params.buttonId === 'undo-all-changes') { - const toolUseId = params.messageId.replace('_undoall', '') + } else if (params.buttonId === BUTTON_UNDO_ALL_CHANGES) { + const toolUseId = params.messageId.replace(SUFFIX_UNDOALL, '') await this.#undoAllFileChanges(params.tabId, toolUseId, session.data) return { success: true, } - } else if (params.buttonId === 'stop-shell-command') { + } else if (params.buttonId === BUTTON_STOP_SHELL_COMMAND) { this.#stoppedToolUses.add(params.messageId) await this.#renderStoppedShellCommand(params.tabId, params.messageId) return { success: true } - } else if (params.buttonId === 'paidtier-upgrade-q-learnmore') { + } else if (params.buttonId === BUTTON_PAIDTIER_UPGRADE_Q_LEARNMORE) { onPaidTierLearnMore(this.#features.lsp, this.#features.logging) return { success: true } - } else if (params.buttonId === 'paidtier-upgrade-q') { + } else if (params.buttonId === BUTTON_PAIDTIER_UPGRADE_Q) { await this.onManageSubscription(params.tabId) return { success: true } @@ -432,7 +471,7 @@ export class AgenticChatController implements ChatHandlers { return } const fileList = cachedToolUse.chatResult?.header?.fileList - const button = cachedToolUse.chatResult?.header?.buttons?.filter(button => button.id !== 'undo-changes') + const button = cachedToolUse.chatResult?.header?.buttons?.filter(button => button.id !== BUTTON_UNDO_CHANGES) const updatedHeader = { ...cachedToolUse.chatResult?.header, @@ -487,7 +526,7 @@ export class AgenticChatController implements ChatHandlers { return } for (const messageId of [...toUndo].reverse()) { - await this.onButtonClick({ buttonId: 'undo-changes', messageId, tabId }) + await this.onButtonClick({ buttonId: BUTTON_UNDO_CHANGES, messageId, tabId }) } } @@ -599,6 +638,7 @@ export class AgenticChatController implements ChatHandlers { this.#contextCommandsProvider?.dispose() this.#userWrittenCodeTracker?.dispose() this.#mcpEventHandler.dispose() + this.#activeUserTracker.dispose() clearInterval(this.#abTestingFetchingTimeout) } @@ -679,6 +719,8 @@ export class AgenticChatController implements ChatHandlers { return new ResponseError(ErrorCodes.InternalError, sessionResult.error) } + const compactIds = session.getAllDeferredCompactMessageIds() + await this.#invalidateCompactCommand(params.tabId, compactIds) session.rejectAllDeferredToolExecutions(new ToolApprovalException('Command ignored: new prompt', false)) await this.#invalidateAllShellCommands(params.tabId, session) @@ -688,6 +730,11 @@ export class AgenticChatController implements ChatHandlers { userVariation: this.#abTestingAllocation?.userVariation, }) + const isNewActiveUser = this.#activeUserTracker.isNewActiveUser() + if (isNewActiveUser) { + this.#telemetryController.emitActiveUser() + } + try { const triggerContext = await this.#getTriggerContext(params, metric) if (triggerContext.programmingLanguage?.languageName) { @@ -707,6 +754,8 @@ export class AgenticChatController implements ChatHandlers { // Abort all operations immediately session.abortRequest() + const compactIds = session.getAllDeferredCompactMessageIds() + await this.#invalidateCompactCommand(params.tabId, compactIds) void this.#invalidateAllShellCommands(params.tabId, session) session.rejectAllDeferredToolExecutions(new CancellationError('user')) @@ -768,39 +817,62 @@ export class AgenticChatController implements ChatHandlers { params.context, params.tabId ) - // Get the initial request input - const initialRequestInput = await this.#prepareRequestInput( - params, - session, - triggerContext, - additionalContext, - chatResultStream, - customContext - ) - // Generate a unique ID for this prompt - const promptId = crypto.randomUUID() - session.setCurrentPromptId(promptId) + let finalResult + if (params.prompt.command === QuickAction.Compact) { + // Get the compaction request input + const compactionRequestInput = this.#getCompactionRequestInput(session) + // Generate a unique ID for this prompt + const promptId = crypto.randomUUID() + session.setCurrentPromptId(promptId) + + // Start the compaction call + finalResult = await this.#runCompaction( + compactionRequestInput, + session, + metric, + chatResultStream, + params.tabId, + promptId, + session.conversationId, + token, + triggerContext.documentReference + ) + } else { + // Get the initial request input + const initialRequestInput = await this.#prepareRequestInput( + params, + session, + triggerContext, + additionalContext, + chatResultStream, + customContext + ) - // Start the agent loop - const finalResult = await this.#runAgentLoop( - initialRequestInput, - session, - metric, - chatResultStream, - params.tabId, - promptId, - session.conversationId, - token, - triggerContext.documentReference, - additionalContext.filter(item => item.pinned) - ) + // Generate a unique ID for this prompt + const promptId = crypto.randomUUID() + session.setCurrentPromptId(promptId) + + // Start the agent loop + finalResult = await this.#runAgentLoop( + initialRequestInput, + session, + metric, + chatResultStream, + params.tabId, + promptId, + session.conversationId, + token, + triggerContext.documentReference, + additionalContext.filter(item => item.pinned) + ) + } - // Phase 5: Result Handling - This happens only once + // Result Handling - This happens only once return await this.#handleFinalResult( finalResult, session, - params, + params.tabId, metric, triggerContext, isNewConversation, @@ -808,7 +880,7 @@ export class AgenticChatController implements ChatHandlers { ) } catch (err) { // HACK: the chat-client needs to have a partial event with the associated messageId sent before it can accept the final result. - // Without this, the `thinking` indicator never goes away. + // Without this, the `working` indicator never goes away. // Note: buttons being explicitly empty is required for this hack to work. const errorMessageId = `error-message-id-${uuid()}` await this.#sendProgressToClient( @@ -865,7 +937,6 @@ export class AgenticChatController implements ChatHandlers { this.#customizationArn, chatResultStream, profileArn, - [], this.#getTools(session), additionalContext, session.modelId, @@ -875,6 +946,186 @@ export class AgenticChatController implements ChatHandlers { return requestInput } + /** + * Prepares the initial request input for the chat prompt + */ + #getCompactionRequestInput(session: ChatSessionService): ChatCommandInput { + this.#debug('Preparing compaction request input') + // Get profileArn from the service manager if available + const profileArn = this.#serviceManager?.getActiveProfileArn() + const requestInput = this.#triggerContext.getCompactionChatCommandInput( + profileArn, + this.#getTools(session), + session.modelId, + this.#origin + ) + return requestInput + } + + /** + * Runs the compaction, making requests and processing tool uses until completion + */ + #shouldCompact(currentRequestCount: number): boolean { + // 80% of 570K limit + if (currentRequestCount > 456_000) { + this.#debug(`Current request total character count is: ${currentRequestCount}, prompting user to compact`) + return true + } else { + return false + } + } + + /** + * Runs the compaction to compact history into a single summary + */ + async #runCompaction( + compactionRequestInput: ChatCommandInput, + session: ChatSessionService, + metric: Metric, + chatResultStream: AgenticChatResultStream, + tabId: string, + promptId: string, + conversationIdentifier?: string, + token?: CancellationToken, + documentReference?: FileList + ): Promise> { + let currentRequestInput = { ...compactionRequestInput } + let finalResult: Result | null = null + metric.recordStart() + + this.#debug(`Running compaction for conversation id:`, conversationIdentifier || '') + + this.#timeToFirstChunk = -1 + this.#timeBetweenChunks = [] + + // Check for cancellation + if (this.#isPromptCanceled(token, session, promptId)) { + this.#debug('Stopping compaction loop - cancelled by user') + throw new CancellationError('user') + } + + const currentMessage = currentRequestInput.conversationState?.currentMessage + let messages: DbMessage[] = [] + let characterCount = 0 + if (currentMessage) { + // Get and process the messages from history DB to maintain invariants for service requests + try { + const { messages: historyMessages, count: historyCharCount } = this.#chatHistoryDb.fixAndGetHistory( + tabId, + currentMessage, + [] + ) + messages = historyMessages + characterCount = historyCharCount + } catch (err) { + if (err instanceof ToolResultValidationError) { + this.#features.logging.error(`Tool validation error: ${err.message}`) + return ( + finalResult || { + success: false, + error: 'Compaction loop failed to produce a final result', + data: { chatResult: {}, toolUses: {} }, + } + ) + } + } + } + + currentRequestInput.conversationState!.history = messages.map(msg => messageToStreamingMessage(msg)) + + const resultStreamWriter = chatResultStream.getResultStreamWriter() + + if (currentRequestInput.conversationState!.history.length == 0) { + // early terminate + await resultStreamWriter.write({ + type: 'answer', + body: 'History is empty, there is nothing to compact.', + messageId: uuid(), + }) + return { + success: true, + data: { + chatResult: {}, + toolUses: {}, + }, + } + } else { + await resultStreamWriter.write({ + type: 'answer', + body: 'Compacting your chat history, this may take a moment.', + messageId: uuid(), + }) + } + await resultStreamWriter.close() + + // Add loading message before making the request + const loadingMessageId = `loading-${uuid()}` + await chatResultStream.writeResultBlock({ ...loadingMessage, messageId: loadingMessageId }) + + this.#debug(`Compacting history with ${characterCount} characters`) + this.#llmRequestStartTime = Date.now() + // Phase 3: Request Execution + // Note: these logs are very noisy, but contain information redacted on the backend. + this.#debug( + `generateAssistantResponse/SendMessage Request: ${JSON.stringify(currentRequestInput, undefined, 2)}` + ) + const response = await session.getChatResponse(currentRequestInput) + if (response.$metadata.requestId) { + metric.mergeWith({ + requestIds: [response.$metadata.requestId], + }) + } + this.#features.logging.info( + `generateAssistantResponse/SendMessage ResponseMetadata: ${loggingUtils.formatObj(response.$metadata)}` + ) + await chatResultStream.removeResultBlock(loadingMessageId) + + // Phase 4: Response Processing + const result = await this.#processAgenticChatResponseWithTimeout( + response, + metric.mergeWith({ + cwsprChatResponseCode: response.$metadata.httpStatusCode, + cwsprChatMessageId: response.$metadata.requestId, + }), + chatResultStream, + session, + documentReference, + true + ) + + const llmLatency = Date.now() - this.#llmRequestStartTime + this.#debug(`LLM Response Latency for compaction: ${llmLatency}`) + this.#telemetryController.emitAgencticLoop_InvokeLLM( + response.$metadata.requestId!, + conversationIdentifier ?? '', + 'AgenticChatWithCompaction', + undefined, + undefined, + 'Succeeded', + this.#features.runtime.serverInfo.version ?? '', + session.modelId, + llmLatency, + [], + this.#timeToFirstChunk, + this.#timeBetweenChunks, + session.pairProgrammingMode + ) + + // replace the history with summary in history DB + if (result.data?.chatResult.body !== undefined) { + this.#chatHistoryDb.replaceWithSummary(tabId, 'cwc', conversationIdentifier ?? '', { + body: result.data?.chatResult.body, + type: 'prompt' as any, + shouldDisplayMessage: true, + timestamp: new Date(), + }) + } else { + this.#features.logging.warn('No ChatResult body in response, skipping adding to history') + } + + return result + } + /** * Runs the agent loop, making requests and processing tool uses until completion */ @@ -894,6 +1145,7 @@ export class AgenticChatController implements ChatHandlers { let finalResult: Result | null = null let iterationCount = 0 let shouldDisplayMessage = true + let currentRequestCount = 0 metric.recordStart() this.logSystemInformation() while (true) { @@ -929,7 +1181,20 @@ export class AgenticChatController implements ChatHandlers { if (currentMessage) { // Get and process the messages from history DB to maintain invariants for service requests try { - messages = this.#chatHistoryDb.fixAndGetHistory(tabId, currentMessage, pinnedContextMessages) + const newUserInputCount = this.#chatHistoryDb.calculateNewMessageCharacterCount( + currentMessage, + pinnedContextMessages + ) + const { messages: historyMessages, count: historyCharacterCount } = + this.#chatHistoryDb.fixAndGetHistory( + tabId, + currentMessage, + pinnedContextMessages, + newUserInputCount + ) + messages = historyMessages + currentRequestCount = newUserInputCount + historyCharacterCount + this.#debug(`Request total character count: ${currentRequestCount}`) } catch (err) { if (err instanceof ToolResultValidationError) { this.#features.logging.warn(`Tool validation error: ${err.message}`) @@ -938,7 +1203,7 @@ export class AgenticChatController implements ChatHandlers { } } - // Do not include chatHistory for requests going to Mynah Backend + // Do not include chatHistory for requests going to Mynah Backend currentRequestInput.conversationState!.history = currentRequestInput.conversationState?.currentMessage ?.userInputMessage?.userIntent ? [] @@ -952,7 +1217,7 @@ export class AgenticChatController implements ChatHandlers { // Phase 3: Request Execution // Note: these logs are very noisy, but contain information redacted on the backend. this.#debug( - `generateAssistantResponse/SendMessage Request: ${JSON.stringify(currentRequestInput, undefined, 2)}` + `generateAssistantResponse/SendMessage Request: ${JSON.stringify(currentRequestInput, this.#imageReplacer, 2)}` ) const response = await session.getChatResponse(currentRequestInput) if (response.$metadata.requestId) { @@ -985,7 +1250,6 @@ export class AgenticChatController implements ChatHandlers { } } shouldDisplayMessage = true - // Phase 4: Response Processing const result = await this.#processAgenticChatResponseWithTimeout( response, @@ -1152,6 +1416,27 @@ export class AgenticChatController implements ChatHandlers { currentRequestInput = this.#updateRequestInputWithToolResults(currentRequestInput, toolResults, content) } + if (this.#shouldCompact(currentRequestCount)) { + const messageId = this.#getMessageIdForCompact(uuid()) + const confirmationResult = this.#processCompactConfirmation(messageId) + const cachedButtonBlockId = await chatResultStream.writeResultBlock(confirmationResult) + await this.waitForCompactApproval(messageId, chatResultStream, cachedButtonBlockId, session) + // Get the compaction request input + const compactionRequestInput = this.#getCompactionRequestInput(session) + // Start the compaction call + return await this.#runCompaction( + compactionRequestInput, + session, + metric, + chatResultStream, + tabId, + promptId, + session.conversationId, + token, + documentReference + ) + } + return ( finalResult || { success: false, @@ -1254,6 +1539,7 @@ export class AgenticChatController implements ChatHandlers { #getPendingToolUses(toolUses: Record): Array { return Object.values(toolUses).filter(toolUse => toolUse.stop) } + /** * Creates a promise that does not resolve until the user accepts or rejects the tool usage. * @param toolUseId @@ -1317,36 +1603,36 @@ export class AgenticChatController implements ChatHandlers { await chatResultStream.removeResultBlockAndUpdateUI(progressPrefix + toolUse.toolUseId) // fsRead and listDirectory write to an existing card and could show nothing in the current position - if (!['fsWrite', 'fsReplace', 'fsRead', 'listDirectory'].includes(toolUse.name)) { + if (![FS_WRITE, FS_REPLACE, FS_READ, LIST_DIRECTORY].includes(toolUse.name)) { await this.#showUndoAllIfRequired(chatResultStream, session) } // fsWrite can take a long time, so we render fsWrite Explanatory upon partial streaming responses. - if (toolUse.name !== 'fsWrite' && toolUse.name !== 'fsReplace') { + if (toolUse.name !== FS_WRITE && toolUse.name !== FS_REPLACE) { const { explanation } = toolUse.input as unknown as ExplanatoryParams if (explanation) { await chatResultStream.writeResultBlock({ type: 'directive', - messageId: toolUse.toolUseId + '_explanation', + messageId: toolUse.toolUseId + SUFFIX_EXPLANATION, body: explanation, }) } } switch (toolUse.name) { - case 'fsRead': - case 'listDirectory': - case 'grepSearch': - case 'fileSearch': - case 'fsWrite': - case 'fsReplace': - case 'executeBash': { + case FS_READ: + case LIST_DIRECTORY: + case GREP_SEARCH: + case FILE_SEARCH: + case FS_WRITE: + case FS_REPLACE: + case EXECUTE_BASH: { const toolMap = { - fsRead: { Tool: FsRead }, - listDirectory: { Tool: ListDirectory }, - fsWrite: { Tool: FsWrite }, - fsReplace: { Tool: FsReplace }, - executeBash: { Tool: ExecuteBash }, - grepSearch: { Tool: GrepSearch }, - fileSearch: { Tool: FileSearch }, + [FS_READ]: { Tool: FsRead }, + [LIST_DIRECTORY]: { Tool: ListDirectory }, + [FS_WRITE]: { Tool: FsWrite }, + [FS_REPLACE]: { Tool: FsReplace }, + [EXECUTE_BASH]: { Tool: ExecuteBash }, + [GREP_SEARCH]: { Tool: GrepSearch }, + [FILE_SEARCH]: { Tool: FileSearch }, } const { Tool } = toolMap[toolUse.name as keyof typeof toolMap] @@ -1369,7 +1655,7 @@ export class AgenticChatController implements ChatHandlers { // Honor built-in permission if available, otherwise use tool's requiresAcceptance // const requiresAcceptance = builtInPermission || toolRequiresAcceptance - if (requiresAcceptance || toolUse.name === 'executeBash') { + if (requiresAcceptance || toolUse.name === EXECUTE_BASH) { // for executeBash, we till send the confirmation message without action buttons const confirmationResult = this.#processToolConfirmation( toolUse, @@ -1378,7 +1664,7 @@ export class AgenticChatController implements ChatHandlers { commandCategory ) cachedButtonBlockId = await chatResultStream.writeResultBlock(confirmationResult) - const isExecuteBash = toolUse.name === 'executeBash' + const isExecuteBash = toolUse.name === EXECUTE_BASH if (isExecuteBash) { this.#telemetryController.emitInteractWithAgenticChat( 'GeneratedCommand', @@ -1476,7 +1762,7 @@ export class AgenticChatController implements ChatHandlers { break } - if (toolUse.name === 'fsWrite' || toolUse.name === 'fsReplace') { + if (toolUse.name === FS_WRITE || toolUse.name === FS_REPLACE) { const input = toolUse.input as unknown as FsWriteParams | FsReplaceParams const document = await this.#triggerContext.getTextDocumentFromPath(input.path, true, true) @@ -1529,27 +1815,27 @@ export class AgenticChatController implements ChatHandlers { }) switch (toolUse.name) { - case 'fsRead': - case 'listDirectory': - case 'fileSearch': + case FS_READ: + case LIST_DIRECTORY: + case FILE_SEARCH: const initialListDirResult = this.#processReadOrListOrSearch(toolUse, chatResultStream) if (initialListDirResult) { await chatResultStream.writeResultBlock(initialListDirResult) } break // no need to write tool result for listDir,fsRead,fileSearch into chat stream - case 'executeBash': + case EXECUTE_BASH: // no need to write tool result for listDir and fsRead into chat stream // executeBash will stream the output instead of waiting until the end break - case 'grepSearch': + case GREP_SEARCH: const grepSearchResult = this.#processGrepSearchResult(toolUse, result, chatResultStream) if (grepSearchResult) { await chatResultStream.writeResultBlock(grepSearchResult) } break - case 'fsReplace': - case 'fsWrite': + case FS_REPLACE: + case FS_WRITE: const input = toolUse.input as unknown as FsWriteParams | FsReplaceParams // Load from the filesystem instead of workspace. // Workspace is likely out of date - when files @@ -1657,13 +1943,13 @@ export class AgenticChatController implements ChatHandlers { } // Rethrow error for executeBash or any named tool - if (toolUse.name === 'executeBash' || toolUse.name) { + if (toolUse.name === EXECUTE_BASH || toolUse.name) { throw err } } // display fs write failure status in the UX of that file card - if ((toolUse.name === 'fsWrite' || toolUse.name === 'fsReplace') && toolUse.toolUseId) { + if ((toolUse.name === FS_WRITE || toolUse.name === FS_REPLACE) && toolUse.toolUseId) { const existingCard = chatResultStream.getMessageBlockId(toolUse.toolUseId) const fsParam = toolUse.input as unknown as FsWriteParams | FsReplaceParams if (fsParam.path) { @@ -1696,7 +1982,7 @@ export class AgenticChatController implements ChatHandlers { await chatResultStream.writeResultBlock(errorResult) } } - } else if (toolUse.name === 'executeBash' && toolUse.toolUseId) { + } else if (toolUse.name === EXECUTE_BASH && toolUse.toolUseId) { const existingCard = chatResultStream.getMessageBlockId(toolUse.toolUseId) const command = (toolUse.input as unknown as ExecuteBashParams).command const completedErrorResult = { @@ -1757,10 +2043,10 @@ export class AgenticChatController implements ChatHandlers { * Updates the currentUndoAllId state in the session */ #updateUndoAllState(toolUse: ToolUse, session: ChatSessionService) { - if (toolUse.name === 'fsRead' || toolUse.name === 'listDirectory') { + if (toolUse.name === FS_READ || toolUse.name === LIST_DIRECTORY) { return } - if (toolUse.name === 'fsWrite' || toolUse.name === 'fsReplace') { + if (toolUse.name === FS_WRITE || toolUse.name === FS_REPLACE) { if (session.currentUndoAllId === undefined) { session.currentUndoAllId = toolUse.toolUseId } @@ -1798,10 +2084,10 @@ export class AgenticChatController implements ChatHandlers { await chatResultStream.writeResultBlock({ type: 'answer', - messageId: `${session.currentUndoAllId}_undoall`, + messageId: `${session.currentUndoAllId}${SUFFIX_UNDOALL}`, buttons: [ { - id: 'undo-all-changes', + id: BUTTON_UNDO_ALL_CHANGES, text: 'Undo all changes', icon: 'undo', status: 'clear', @@ -1834,11 +2120,11 @@ export class AgenticChatController implements ChatHandlers { #validateToolResult(toolUse: ToolUse, result: ToolResultContentBlock) { let maxToolResponseSize switch (toolUse.name) { - case 'fsRead': - case 'executeBash': + case FS_READ: + case EXECUTE_BASH: // fsRead and executeBash already have truncation logic return - case 'listDirectory': + case LIST_DIRECTORY: maxToolResponseSize = 50_000 break default: @@ -1901,7 +2187,7 @@ export class AgenticChatController implements ChatHandlers { if (toolUse.name === QCodeReview.toolName) { return this.#getToolOverWritableStream(chatResultStream, toolUse) } - if (toolUse.name !== 'executeBash') { + if (toolUse.name !== EXECUTE_BASH) { return } @@ -1911,7 +2197,7 @@ export class AgenticChatController implements ChatHandlers { const initialHeader: ChatMessage['header'] = { body: 'shell', - buttons: [{ id: 'stop-shell-command', text: 'Stop', icon: 'stop' }], + buttons: [{ id: BUTTON_STOP_SHELL_COMMAND, text: 'Cancel', icon: 'stop' }], } const completedHeader: ChatMessage['header'] = { @@ -1972,7 +2258,7 @@ export class AgenticChatController implements ChatHandlers { const toolName = originalToolName ?? (toolType || toolUse.name) // Handle bash commands with special formatting - if (toolName === 'executeBash') { + if (toolName === EXECUTE_BASH) { return { messageId: toolUse.toolUseId, type: 'tool', @@ -1988,7 +2274,7 @@ export class AgenticChatController implements ChatHandlers { text: 'Rejected', }, }), - buttons: isAccept ? [{ id: 'stop-shell-command', text: 'Stop', icon: 'stop' }] : [], + buttons: isAccept ? [{ id: BUTTON_STOP_SHELL_COMMAND, text: 'Cancel', icon: 'stop' }] : [], }, } } @@ -2001,10 +2287,10 @@ export class AgenticChatController implements ChatHandlers { let body: string | undefined switch (toolName) { - case 'fsReplace': - case 'fsWrite': - case 'fsRead': - case 'listDirectory': + case FS_REPLACE: + case FS_WRITE: + case FS_READ: + case LIST_DIRECTORY: header = { body: undefined, status: { @@ -2015,7 +2301,7 @@ export class AgenticChatController implements ChatHandlers { } break - case 'fileSearch': + case FILE_SEARCH: const searchPath = (toolUse.input as unknown as FileSearchParams).path header = { body: 'File Search', @@ -2085,7 +2371,7 @@ export class AgenticChatController implements ChatHandlers { status: { status: 'error', icon: 'stop', - text: 'Stopped', + text: 'Canceled', }, buttons: [], }, @@ -2095,6 +2381,117 @@ export class AgenticChatController implements ChatHandlers { }) } + #processCompactConfirmation(messageId: string): ChatResult { + const buttons = [{ id: 'allow-tools', text: 'Allow', icon: 'ok', status: 'clear' }] + const header = { + icon: 'warning', + iconForegroundStatus: 'warning', + body: COMPACTION_HEADER_BODY, + buttons, + } as any + const body = COMPACTION_BODY + return { + type: 'tool', + messageId, + header, + body, + } + } + + /** + * Creates a promise that does not resolve until the user accepts or rejects the compaction usage. + * @param messageId + * @param resultStream + * @param promptBlockId id of approval block. This allows us to overwrite the buttons with 'accepted' or 'rejected' text. + */ + async waitForCompactApproval( + messageId: string, + resultStream: AgenticChatResultStream, + promptBlockId: number, + session: ChatSessionService + ) { + const deferred = this.#createDeferred() + session.setDeferredToolExecution(messageId, deferred.resolve, deferred.reject) + this.#log(`Prompting for compaction approval for messageId: ${messageId}`) + await deferred.promise + // Note: we want to overwrite the button block because it already exists in the stream. + await resultStream.overwriteResultBlock(this.#getUpdateCompactConfirmResult(messageId), promptBlockId) + } + + /** + * Creates an updated ChatResult for compaction confirmation + * @param messageId The messageId + * @returns ChatResult with appropriate confirmation UI + */ + #getUpdateCompactConfirmResult(messageId: string): ChatResult { + let header: { + body: string | undefined + status: { status: 'info' | 'success' | 'warning' | 'error'; icon: string; text: string } + } + let body: string | undefined + + header = { + body: undefined, + status: { + status: 'success', + icon: 'ok', + text: 'Allowed', + }, + } + + return { + messageId, + type: 'tool', + body, + header, + } + } + + #renderStopShellCommandButton() { + const stopKey = this.#getKeyBinding('aws.amazonq.stopCmdExecution') + return { + id: 'stop-shell-command', + text: 'Cancel', + icon: 'stop', + ...(stopKey ? { description: `Stop: ${stopKey}` } : {}), + } + } + + #getKeyBinding(commandId: string): string | null { + // Check for feature flag + const shortcut = + this.#features.lsp.getClientInitializeParams()?.initializationOptions?.aws?.awsClientCapabilities?.q + ?.shortcut + if (!shortcut) { + return null + } + let defaultKey = '' + const OS = os.platform() + + switch (commandId) { + case 'aws.amazonq.runCmdExecution': + defaultKey = OS === 'darwin' ? DEFAULT_MACOS_RUN_SHORTCUT : DEFAULT_WINDOW_RUN_SHORTCUT + break + case 'aws.amazonq.rejectCmdExecution': + defaultKey = OS === 'darwin' ? DEFAULT_MACOS_REJECT_SHORTCUT : DEFAULT_WINDOW_REJECT_SHORTCUT + break + case 'aws.amazonq.stopCmdExecution': + defaultKey = OS === 'darwin' ? DEFAULT_MACOS_STOP_SHORTCUT : DEFAULT_WINDOW_STOP_SHORTCUT + break + default: + this.#log(`#getKeyBinding: ${commandId} shortcut is supported by Q `) + break + } + + if (defaultKey === '') { + return null + } + + //TODO: handle case: user change default keybind, suggestion: read `keybinding.json` provided by VSC + + return defaultKey + } + #processToolConfirmation( toolUse: ToolUse, requiresAcceptance: Boolean, @@ -2122,13 +2519,13 @@ export class AgenticChatController implements ChatHandlers { // Configure tool-specific UI elements switch (toolName) { - case 'executeBash': { + case EXECUTE_BASH: { const commandString = (toolUse.input as unknown as ExecuteBashParams).command buttons = requiresAcceptance ? [ - { id: 'run-shell-command', text: 'Run', icon: 'play' }, + { id: BUTTON_RUN_SHELL_COMMAND, text: 'Run', icon: 'play' }, { - id: 'reject-shell-command', + id: BUTTON_REJECT_SHELL_COMMAND, status: 'dimmed-clear' as Status, text: 'Reject', icon: 'cancel', @@ -2167,14 +2564,14 @@ export class AgenticChatController implements ChatHandlers { break } - case 'fsWrite': { + case FS_WRITE: { const writeFilePath = (toolUse.input as unknown as FsWriteParams).path // Validate the path using our synchronous utility validatePathBasic(writeFilePath) this.#debug(`Processing ${toolUse.name} for path: ${writeFilePath}`) - buttons = [{ id: 'allow-tools', text: 'Allow', icon: 'ok', status: 'clear' }] + buttons = [{ id: BUTTON_ALLOW_TOOLS, text: 'Allow', icon: 'ok', status: 'clear' }] header = { icon: 'warning', iconForegroundStatus: 'warning', @@ -2189,14 +2586,14 @@ export class AgenticChatController implements ChatHandlers { break } - case 'fsReplace': { + case FS_REPLACE: { const writeFilePath = (toolUse.input as unknown as FsReplaceParams).path // For replace, we need to verify the file exists validatePathExists(writeFilePath) this.#debug(`Processing ${toolUse.name} for path: ${writeFilePath}`) - buttons = [{ id: 'allow-tools', text: 'Allow', icon: 'ok', status: 'clear' }] + buttons = [{ id: BUTTON_ALLOW_TOOLS, text: 'Allow', icon: 'ok', status: 'clear' }] header = { icon: 'warning', iconForegroundStatus: 'warning', @@ -2211,9 +2608,9 @@ export class AgenticChatController implements ChatHandlers { break } - case 'fsRead': - case 'listDirectory': { - buttons = [{ id: 'allow-tools', text: 'Allow', icon: 'ok', status: 'clear' }] + case FS_READ: + case LIST_DIRECTORY: { + buttons = [{ id: BUTTON_ALLOW_TOOLS, text: 'Allow', icon: 'ok', status: 'clear' }] header = { icon: 'tools', iconForegroundStatus: 'tools', @@ -2223,7 +2620,7 @@ export class AgenticChatController implements ChatHandlers { buttons, } - if (toolName === 'fsRead') { + if (toolName === FS_READ) { const paths = (toolUse.input as unknown as FsReadParams).paths // Validate paths using our synchronous utility @@ -2251,7 +2648,7 @@ export class AgenticChatController implements ChatHandlers { default: { // — DEFAULT ⇒ MCP tools - buttons = [{ id: 'allow-tools', text: 'Allow', icon: 'ok', status: 'clear' }] + buttons = [{ id: BUTTON_ALLOW_TOOLS, text: 'Allow', icon: 'ok', status: 'clear' }] header = { icon: 'tools', iconForegroundStatus: 'warning', @@ -2271,7 +2668,7 @@ export class AgenticChatController implements ChatHandlers { type: 'tool', messageId: this.#getMessageIdForToolUse(toolType, toolUse), header, - body: warning ? (toolName === 'executeBash' ? '' : '\n\n') + body : body, + body: warning ? (toolName === EXECUTE_BASH ? '' : '\n\n') + body : body, } } else { return { @@ -2283,9 +2680,9 @@ export class AgenticChatController implements ChatHandlers { icon: 'tools', body: `${toolName}`, buttons: [ - { id: 'allow-tools', text: 'Run', icon: 'play', status: 'clear' }, + { id: BUTTON_ALLOW_TOOLS, text: 'Run', icon: 'play', status: 'clear' }, { - id: 'reject-mcp-tool', + id: BUTTON_REJECT_MCP_TOOL, text: 'Reject', icon: 'cancel', status: 'dimmed-clear' as Status, @@ -2338,7 +2735,7 @@ export class AgenticChatController implements ChatHandlers { }, }, }, - buttons: [{ id: 'undo-changes', text: 'Undo', icon: 'undo' }], + buttons: [{ id: BUTTON_UNDO_CHANGES, text: 'Undo', icon: 'undo' }], }, } } @@ -2353,7 +2750,7 @@ export class AgenticChatController implements ChatHandlers { chatResultStream.setMessageIdToUpdateForTool(toolUse.name!, messageIdToUpdate) } let currentPaths = [] - if (toolUse.name === 'fsRead') { + if (toolUse.name === FS_READ) { currentPaths = (toolUse.input as unknown as FsReadParams)?.paths } else { currentPaths.push((toolUse.input as unknown as ListDirectoryParams | FileSearchParams)?.path) @@ -2383,9 +2780,9 @@ export class AgenticChatController implements ChatHandlers { title = 'Gathering context' } else { title = - toolUse.name === 'fsRead' + toolUse.name === FS_READ ? `${itemCount} file${itemCount > 1 ? 's' : ''} read` - : toolUse.name === 'fileSearch' + : toolUse.name === FILE_SEARCH ? `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} searched` : `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed` } @@ -2418,7 +2815,7 @@ export class AgenticChatController implements ChatHandlers { result: any, chatResultStream: AgenticChatResultStream ): ChatMessage | undefined { - if (toolUse.name !== 'grepSearch') { + if (toolUse.name !== GREP_SEARCH) { return undefined } @@ -2520,7 +2917,7 @@ export class AgenticChatController implements ChatHandlers { async #handleFinalResult( result: Result, session: ChatSessionService, - params: ChatParams, + tabId: string, metric: Metric, triggerContext: TriggerContext, isNewConversation: boolean, @@ -2533,17 +2930,17 @@ export class AgenticChatController implements ChatHandlers { this.#debug('Final session conversation id:', conversationId || '') if (conversationId) { - this.#telemetryController.setConversationId(params.tabId, conversationId) + this.#telemetryController.setConversationId(tabId, conversationId) if (isNewConversation) { - this.#telemetryController.updateTriggerInfo(params.tabId, { + this.#telemetryController.updateTriggerInfo(tabId, { startTrigger: { hasUserSnippet: metric.metric.cwsprChatHasCodeSnippet ?? false, triggerType: triggerContext.triggerType, }, }) - this.#telemetryController.emitStartConversationMetric(params.tabId, metric.metric) + this.#telemetryController.emitStartConversationMetric(tabId, metric.metric) } } @@ -2578,9 +2975,9 @@ export class AgenticChatController implements ChatHandlers { cwsprChatPinnedPromptContextCount: triggerContext.contextInfo.pinnedContextCount.promptContextCount, }) } - await this.#telemetryController.emitAddMessageMetric(params.tabId, metric.metric, 'Succeeded') + await this.#telemetryController.emitAddMessageMetric(tabId, metric.metric, 'Succeeded') - this.#telemetryController.updateTriggerInfo(params.tabId, { + this.#telemetryController.updateTriggerInfo(tabId, { lastMessageTrigger: { ...triggerContext, messageId: result.data?.chatResult.messageId, @@ -2911,7 +3308,7 @@ export class AgenticChatController implements ChatHandlers { const toolUseId = params.messageId const toolUse = toolUseId ? session.data?.toolUseLookup.get(toolUseId) : undefined - if (toolUse?.name === 'fsWrite' || toolUse?.name === 'fsReplace') { + if (toolUse?.name === FS_WRITE || toolUse?.name === FS_REPLACE) { const input = toolUse.input as unknown as FsWriteParams | FsReplaceParams this.#features.lsp.workspace.openFileDiff({ originalFileUri: input.path, @@ -2919,7 +3316,7 @@ export class AgenticChatController implements ChatHandlers { isDeleted: false, fileContent: toolUse.fileChange?.after, }) - } else if (toolUse?.name === 'fsRead') { + } else if (toolUse?.name === FS_READ) { await this.#features.lsp.window.showDocument({ uri: URI.file(params.filePath).toString() }) } else { const absolutePath = params.fullPath ?? (await this.#resolveAbsolutePath(params.filePath)) @@ -3169,9 +3566,32 @@ export class AgenticChatController implements ChatHandlers { return triggerContext } + async #invalidateCompactCommand(tabId: string, messageIds: string[]) { + for (const messageId of messageIds) { + await this.#features.chat.sendChatUpdate({ + tabId, + state: { inProgress: false }, + data: { + messages: [ + { + messageId, + type: 'tool', + body: COMPACTION_BODY, + header: { + body: COMPACTION_HEADER_BODY, + status: { icon: 'block', text: 'Ignored' }, + buttons: [], + }, + }, + ], + }, + }) + } + } + async #invalidateAllShellCommands(tabId: string, session: ChatSessionService) { for (const [toolUseId, toolUse] of session.toolUseLookup.entries()) { - if (toolUse.name !== 'executeBash' || this.#stoppedToolUses.has(toolUseId)) continue + if (toolUse.name !== EXECUTE_BASH || this.#stoppedToolUses.has(toolUseId)) continue const params = toolUse.input as unknown as ExecuteBashParams const command = params.command @@ -3425,7 +3845,8 @@ export class AgenticChatController implements ChatHandlers { metric: Metric, chatResultStream: AgenticChatResultStream, session: ChatSessionService, - contextList?: FileList + contextList?: FileList, + isCompaction?: boolean ): Promise> { const abortController = new AbortController() let timeoutId: NodeJS.Timeout | undefined @@ -3448,7 +3869,8 @@ export class AgenticChatController implements ChatHandlers { streamWriter, session, contextList, - abortController.signal + abortController.signal, + isCompaction ) try { const result = await Promise.race([processResponsePromise, timeoutPromise]) @@ -3477,7 +3899,7 @@ export class AgenticChatController implements ChatHandlers { } const toolUses = Object.values(data.toolUses) for (const toolUse of toolUses) { - if ((toolUse.name === 'fsWrite' || toolUse.name === 'fsReplace') && typeof toolUse.input === 'string') { + if ((toolUse.name === FS_WRITE || toolUse.name === FS_REPLACE) && typeof toolUse.input === 'string') { const filepath = extractKey(toolUse.input, 'path') const msgId = progressPrefix + toolUse.toolUseId // render fs write UI as soon as fs write starts @@ -3506,7 +3928,7 @@ export class AgenticChatController implements ChatHandlers { } // render the tool use explanatory as soon as this is received for fsWrite/fsReplace const explanation = extractKey(toolUse.input, 'explanation') - const messageId = progressPrefix + toolUse.toolUseId + '_explanation' + const messageId = progressPrefix + toolUse.toolUseId + SUFFIX_EXPLANATION if (explanation && !chatResultStream.hasMessage(messageId)) { await streamWriter.close() await chatResultStream.writeResultBlock({ @@ -3526,14 +3948,15 @@ export class AgenticChatController implements ChatHandlers { streamWriter: ResultStreamWriter, session: ChatSessionService, contextList?: FileList, - abortSignal?: AbortSignal + abortSignal?: AbortSignal, + isCompaction?: boolean ): Promise> { const requestId = response.$metadata.requestId! const chatEventParser = new AgenticChatEventParser(requestId, metric, this.#features.logging) // Display context transparency list once at the beginning of response // Use a flag to track if contextList has been sent already to avoid ux flickering - if (contextList?.filePaths && contextList.filePaths.length > 0 && !session.contextListSent) { + if (!isCompaction && contextList?.filePaths && contextList.filePaths.length > 0 && !session.contextListSent) { await streamWriter.write({ body: '', contextList }) session.contextListSent = true } @@ -3569,7 +3992,7 @@ export class AgenticChatController implements ChatHandlers { this.recordChunk('chunk') } - // make sure to save code reference events + // update the UI with response if (chatEvent.assistantResponseEvent || chatEvent.codeReferenceEvent) { await streamWriter.write(result.data.chatResult) } @@ -3585,9 +4008,18 @@ export class AgenticChatController implements ChatHandlers { } } if (isEmptyResponse) { - // If the response is empty, we need to send an empty answer message to remove the Thinking... indicator + // If the response is empty, we need to send an empty answer message to remove the Working... indicator await streamWriter.write({ type: 'answer', body: '', messageId: uuid() }) } + + if (isCompaction) { + // Show a dummy message to the UI for compaction completion + await streamWriter.write({ + type: 'answer', + body: 'Conversation history has been compacted successfully!', + messageId: uuid(), + }) + } await streamWriter.close() metric.mergeWith({ @@ -3904,4 +4336,13 @@ export class AgenticChatController implements ChatHandlers { #debug(...messages: string[]) { this.#features.logging.debug(messages.join(' ')) } + + // Helper function to sanitize the 'images' field for logging by replacing large binary data (e.g., Uint8Array) with a concise summary. + // This prevents logs from being overwhelmed by raw byte arrays and keeps log output readable. + #imageReplacer(key: string, value: any): string | any { + if (key === 'bytes' && value && typeof value.length === 'number') { + return `[Uint8Array, length: ${value.length}]` + } + return value + } } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts index f09624705b..67572d4d13 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/constants.ts @@ -13,3 +13,78 @@ export const RESPONSE_TIMEOUT_MS = 240_000 // LLM Constants export const GENERATE_ASSISTANT_RESPONSE_INPUT_LIMIT = 500_000 export const DEFAULT_MODEL_ID = BedrockModel.CLAUDE_SONNET_4_20250514_V1_0 + +// Compaction +export const COMPACTION_BODY = + 'The context window is almost full (80%). Amazon Q can compact your chat history to improve response quality.' +export const COMPACTION_HEADER_BODY = 'Compact chat history?' +export const COMPACTION_PROMPT = ` +[SYSTEM NOTE: This is an automated summarization request, not from the user]\n\n + +Your task is to generate a concise summary of the conversation history between an AI coding agent (assistant) and user once the LLM context window is reached. +This summary will replace the raw conversation history, so it should contain important information from the history such that it enables continuning the conversation with the user in a coherent manner. +Output the summary in markdown format with sections provided in format tag. + + + +The summary should have following main sections: +## Conversation Summary +- contains an entry for each key topic discussed +## Files and Code Summary +- contains entries on what was learned about the files and code discussed. If relevant, it includes file paths, function signatures, and key changes +## Key Insights +- contains a summary for each key insight learned from the conversation (such as user preferences, technical details, decisions made, etc.) +## Most Recent Topic +- contains a detailed summary of the most recent topic discussed along with details of actions taken so far to address the user needs along with ALL tools executed + + + +- Add an entry to Key Insights section for any SIGNIFICANT tool usages whose result is important for continuing the conversation +- DO NOT respond conversationally. DO NOT address the user directly. +- For files that were read/written, exclude the full raw content but keep their path and what was learned about them +- If a file was loaded multiple times, only keep information about its latest fetch +- For code pieces, capture file paths and key changes +- Summarize code content concisely rather than including full snippets +- Remove chat conventions (greetings, offers to help, etc.) +- Only output the summary and nothing else + + + +- Information essential for continuing the conversation effectively +- Technical details and code patterns that maintain context +- User primary goals and requirements +- Adding more details for recent conversations/actions over older ones + + + +## Conversation Summary +- **Topic1**: Summary of what was done to address Topic1 and the final conclusion +- **Topic2**: Summary of what was done to address Topic2 and the final conclusion + +## Files and Code Summary +- **fileA path**: learnings about fileA +- **fileB path**: learnings about fileB +- **codeSnippetA**: learnings from codeSnippetA + +## Key Insights +- **INSIGHT**: Insight1 +- **INSIGHT**: Insight2 + +## Most Recent Topic +**Topic**: the most recent topic being discussed +**Progress**: Key actions taken so far to address the topic +**Tools Used**: +- **toolUsage1**: Summary of what was done in toolUsage1 and ultimate result + +` + +// shortcut constant +export const DEFAULT_MACOS_RUN_SHORTCUT = '⇧ ⌘ ↵' +export const DEFAULT_WINDOW_RUN_SHORTCUT = 'Ctrl + ⇧ + ↵' +export const DEFAULT_LINUX_RUN_SHORTCUT = 'Meta + ⇧ + ↵' +export const DEFAULT_MACOS_STOP_SHORTCUT = '⇧ ⌘ ⌫' +export const DEFAULT_WINDOW_STOP_SHORTCUT = 'Ctrl + ⇧ + ⌫' +export const DEFAULT_LINUX_STOP_SHORTCUT = 'Meta + ⇧ + ⌫' +export const DEFAULT_MACOS_REJECT_SHORTCUT = '⇧ ⌘ R' +export const DEFAULT_WINDOW_REJECT_SHORTCUT = 'Ctrl + ⇧ + R' +export const DEFAULT_LINUX_REJECT_SHORTCUT = 'Meta + ⇧ + R' diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/toolConstants.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/toolConstants.ts new file mode 100644 index 0000000000..d4423564bc --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/toolConstants.ts @@ -0,0 +1,38 @@ +/** + * Constants related to tools used in agenticChatController.ts + * This file centralizes all tool names and related constants to improve code quality and maintainability. + */ + +// File system tools +export const FS_READ = 'fsRead' +export const FS_WRITE = 'fsWrite' +export const FS_REPLACE = 'fsReplace' + +// Directory tools +export const LIST_DIRECTORY = 'listDirectory' + +// Search tools +export const GREP_SEARCH = 'grepSearch' +export const FILE_SEARCH = 'fileSearch' + +// Shell tools +export const EXECUTE_BASH = 'executeBash' + +// Code analysis tools +export const Q_CODE_REVIEW = 'qCodeReview' + +// Tool use button IDs +export const BUTTON_RUN_SHELL_COMMAND = 'run-shell-command' +export const BUTTON_REJECT_SHELL_COMMAND = 'reject-shell-command' +export const BUTTON_REJECT_MCP_TOOL = 'reject-mcp-tool' +export const BUTTON_ALLOW_TOOLS = 'allow-tools' +export const BUTTON_UNDO_CHANGES = 'undo-changes' +export const BUTTON_UNDO_ALL_CHANGES = 'undo-all-changes' +export const BUTTON_STOP_SHELL_COMMAND = 'stop-shell-command' +export const BUTTON_PAIDTIER_UPGRADE_Q_LEARNMORE = 'paidtier-upgrade-q-learnmore' +export const BUTTON_PAIDTIER_UPGRADE_Q = 'paidtier-upgrade-q' + +// Message ID suffixes +export const SUFFIX_PERMISSION = '_permission' +export const SUFFIX_UNDOALL = '_undoall' +export const SUFFIX_EXPLANATION = '_explanation' diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts index 3f1bb97676..6c745f20b3 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.test.ts @@ -3,13 +3,14 @@ import * as sinon from 'sinon' import { URI } from 'vscode-uri' import { TestFeatures } from '@aws/language-server-runtimes/testing' import * as assert from 'assert' -import { AdditionalContextPrompt } from 'local-indexing' +import { AdditionalContextPrompt, ContextCommandItem } from 'local-indexing' import { AdditionalContextProvider } from './additionalContextProvider' -import { getUserPromptsDirectory } from './contextUtils' +import { getInitialContextInfo, getUserPromptsDirectory } from './contextUtils' import { LocalProjectContextController } from '../../../shared/localProjectContextController' import { workspaceUtils } from '@aws/lsp-core' import { ChatDatabase } from '../tools/chatDb/chatDb' import { TriggerContext } from './agenticChatTriggerContext' +import { expect } from 'chai' describe('AdditionalContextProvider', () => { let provider: AdditionalContextProvider @@ -17,6 +18,7 @@ describe('AdditionalContextProvider', () => { let chatHistoryDb: ChatDatabase let fsExistsStub: sinon.SinonStub let getContextCommandPromptStub: sinon.SinonStub + let getContextCommandItemsStub: sinon.SinonStub let fsReadDirStub: sinon.SinonStub let localProjectContextControllerInstanceStub: sinon.SinonStub @@ -27,7 +29,8 @@ describe('AdditionalContextProvider', () => { testFeatures.workspace.fs.exists = fsExistsStub testFeatures.workspace.fs.readdir = fsReadDirStub testFeatures.chat.sendPinnedContext = sinon.stub() - getContextCommandPromptStub = sinon.stub() + getContextCommandPromptStub = sinon.stub().returns([]) + getContextCommandItemsStub = sinon.stub().returns([]) chatHistoryDb = { getHistory: sinon.stub().returns([]), searchMessages: sinon.stub().returns([]), @@ -49,6 +52,7 @@ describe('AdditionalContextProvider', () => { provider = new AdditionalContextProvider(testFeatures, chatHistoryDb) localProjectContextControllerInstanceStub = sinon.stub(LocalProjectContextController, 'getInstance').resolves({ getContextCommandPrompt: getContextCommandPromptStub, + getContextCommandItems: getContextCommandItemsStub, } as unknown as LocalProjectContextController) }) @@ -500,6 +504,60 @@ describe('AdditionalContextProvider', () => { ]) }) + it('should update pinned code symbol IDs when they no longer match current index', async () => { + // Mock LocalProjectContextController.getInstance + getContextCommandItemsStub.returns([ + { + id: 'new-symbol-id', + symbol: { + name: 'calculateTotal', + kind: 'Function', + range: { + start: { line: 9, column: 0 }, + end: { line: 19, column: 1 }, + }, + }, + workspaceFolder: '/workspace', + relativePath: 'src/utils.ts', + type: 'file', + }, + ] as ContextCommandItem[]) + + // Create a trigger context + const triggerContext = { + workspaceFolder: { uri: '/workspace', name: 'workspace' }, + contextInfo: getInitialContextInfo(), + } + + // Mock pinned context with an outdated symbol ID + const pinnedContext = [ + { + id: 'old-symbol-id', // This ID no longer exists in the index + command: 'calculateTotal', + label: 'code', + description: `Function, ${path.join('workspace', 'src', 'utils.ts')}`, + route: ['/workspace', '/src/utils.ts'], + pinned: true, + }, + ] + + // Mock chatDb.getPinnedContext to return our pinned context + ;(chatHistoryDb.getPinnedContext as sinon.SinonStub).returns(pinnedContext) + + // Call getAdditionalContext + await provider.getAdditionalContext(triggerContext, 'tab1') + + // Verify that LocalProjectContextController.getInstance was called + sinon.assert.called(localProjectContextControllerInstanceStub) + + // Verify that getContextCommandPrompt was called with updated ID + const contextCommandPromptCall = getContextCommandPromptStub + .getCalls() + .find(call => call.args[0].some((item: ContextCommandItem) => item.id === 'new-symbol-id')) + + expect(contextCommandPromptCall).to.exist + }) + describe('convertPinnedContextToChatMessages', () => { it('should return empty array for no pinned context', async () => { const result = await provider.convertPinnedContextToChatMessages() @@ -531,7 +589,7 @@ describe('AdditionalContextProvider', () => { assert.strictEqual(result.length, 2) assert.strictEqual(result[0].userInputMessage?.content?.includes(''), true) assert.strictEqual(result[0].userInputMessage?.content?.includes('Follow this rule'), true) - assert.strictEqual(result[1].assistantResponseMessage?.content, 'Thinking...') + assert.strictEqual(result[1].assistantResponseMessage?.content, 'Working...') }) it('should convert file context to fileContext XML', async () => { diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.ts index fbde944238..b4f6d9964e 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.ts @@ -25,6 +25,7 @@ import { getUserPromptsDirectory, getInitialContextInfo, promptFileExtension, + getCodeSymbolDescription, } from './contextUtils' import { LocalProjectContextController } from '../../../shared/localProjectContextController' import { Features } from '../../types' @@ -241,6 +242,45 @@ export class AdditionalContextProvider { if (contextInfo.some(item => item.id === '@workspace')) { triggerContext.hasWorkspace = true } + // Handle code symbol ID mismatches between indexing sessions + // When a workspace is re-indexed, code symbols receive new IDs + // If a pinned symbol's ID is no longer found in the current index: + // 1. Extract the symbol's name, filepath, and kind (without line numbers) + // 2. Search for a matching symbol in the current index with the same attributes + // 3. Update the pinned symbol's ID to reference the newly indexed equivalent + try { + let pinnedCodeItems = contextInfo.filter(item => item.pinned).filter(item => item.label === 'code') + if (pinnedCodeItems.length > 0) { + const localProjectContextController = await LocalProjectContextController.getInstance() + + const availableContextItems = await localProjectContextController.getContextCommandItems() + const availableCodeContextItems = availableContextItems.filter(item => item.symbol) + for (const command of pinnedCodeItems) { + // First check if the pinned symbol's ID still exists in the current index + let matchedId = availableCodeContextItems.find(item => item.id === command.id) + if (!matchedId) { + // If ID no longer exists, try to find a matching symbol by name and description + // Remove line numbers from description for comparison + const pinnedItemDescription = command.description?.replace(/,\s*L\d+[-]\d+$/, '') + if (pinnedItemDescription) { + const matchedDescription = availableCodeContextItems.find(availableItem => { + let availableItemDescription = getCodeSymbolDescription(availableItem, false) + return ( + command.command === availableItem.symbol?.name && + availableItemDescription === pinnedItemDescription + ) + }) + + if (matchedDescription) { + command.id = matchedDescription.id + } + } + } + } + } + } catch { + // Do nothing if local project indexing fails + } const contextCounts = getInitialContextInfo() @@ -574,7 +614,6 @@ export class AdditionalContextProvider { } onPinnedContextAdd(params: PinnedContextParams) { - // add to this.#pinnedContext if that id isnt already in there let itemToAdd = params.contextCommandGroups[0]?.commands?.[0] if (itemToAdd) { this.chatDb.addPinnedContext(params.tabId, itemToAdd) @@ -765,7 +804,7 @@ export class AdditionalContextProvider { // Create fake assistant response const assistantMessage: ChatMessage = { assistantResponseMessage: { - content: 'Thinking...', + content: 'Working...', }, } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts index 01265b963d..2671cfadd0 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts @@ -37,6 +37,7 @@ import { ContextInfo, mergeFileLists, mergeRelevantTextDocuments } from './conte import { WorkspaceFolderManager } from '../../workspaceContext/workspaceFolderManager' import { getRelativePathWithWorkspaceFolder } from '../../workspaceContext/util' import { ChatCommandInput } from '../../../shared/streamingClientService' +import { COMPACTION_PROMPT } from '../constants/constants' export interface TriggerContext extends Partial { userIntent?: UserIntent @@ -106,6 +107,43 @@ export class AgenticChatTriggerContext { } } + /** + * Creates chat parameters from trigger context for sending to the backend + * @param profileArn Optional ARN for profile + * @param tools Optional Bedrock tools + * @param modelId Optional model ID + * @param origin Optional origin + * @returns ChatCommandInput - which is either SendMessageInput or GenerateAssistantResponseInput + */ + getCompactionChatCommandInput( + profileArn?: string, + tools: BedrockTools = [], + modelId?: string, + origin?: Origin + ): ChatCommandInput { + const data: ChatCommandInput = { + conversationState: { + chatTriggerType: ChatTriggerType.MANUAL, + currentMessage: { + userInputMessage: { + content: COMPACTION_PROMPT, + userInputMessageContext: { + tools, + envState: this.#mapPlatformToEnvState(process.platform), + }, + userIntent: undefined, + origin: origin ? origin : 'IDE', + modelId, + }, + }, + customizationArn: undefined, + }, + profileArn, + } + + return data + } + /** * Creates chat parameters from trigger context for sending to the backend * @param params Chat parameters or inline chat parameters @@ -128,7 +166,6 @@ export class AgenticChatTriggerContext { customizationArn?: string, chatResultStream?: AgenticChatResultStream, profileArn?: string, - history: ChatMessage[] = [], tools: BedrockTools = [], additionalContent?: AdditionalContentEntryAddition[], modelId?: string, @@ -254,7 +291,6 @@ export class AgenticChatTriggerContext { }, }, customizationArn, - history, }, profileArn, } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContexts.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContexts.test.ts index 0d8d1569fe..eb4fa7e8a3 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContexts.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContexts.test.ts @@ -143,7 +143,6 @@ describe('AgenticChatTriggerContext', () => { undefined, undefined, [], - [], undefined, modelId ) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts index b332e868da..632360cd67 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextCommandsProvider.ts @@ -3,7 +3,7 @@ import { FSWatcher, watch } from 'chokidar' import { ContextCommand, ContextCommandGroup } from '@aws/language-server-runtimes/protocol' import { Disposable } from 'vscode-languageclient/node' import { Chat, Logging, Lsp, Workspace } from '@aws/language-server-runtimes/server-interface' -import { getUserPromptsDirectory, promptFileExtension } from './contextUtils' +import { getCodeSymbolDescription, getUserPromptsDirectory, promptFileExtension } from './contextUtils' import { ContextCommandItem } from 'local-indexing' import { LocalProjectContextController } from '../../../shared/localProjectContextController' import { URI } from 'vscode-uri' @@ -193,7 +193,7 @@ export class ContextCommandsProvider implements Disposable { codeCmds.push({ ...baseItem, command: item.symbol.name, - description: `${item.symbol.kind}, ${path.join(wsFolderName, item.relativePath)}, L${item.symbol.range.start.line + 1}-${item.symbol.range.end.line + 1}`, + description: getCodeSymbolDescription(item, true), label: 'code', icon: 'code-block', }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.test.ts index 06cdbb71dc..b3c0a339e3 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.test.ts @@ -8,11 +8,13 @@ import { promptFileExtension, mergeRelevantTextDocuments, mergeFileLists, + getCodeSymbolDescription, } from './contextUtils' import * as pathUtils from '@aws/lsp-core/out/util/path' import { sanitizeFilename } from '@aws/lsp-core/out/util/text' import { FileList } from '@aws/language-server-runtimes/server-interface' import { RelevantTextDocumentAddition } from './agenticChatTriggerContext' +import { ContextCommandItem } from 'local-indexing' describe('contextUtils', () => { let getUserHomeDirStub: sinon.SinonStub @@ -331,4 +333,99 @@ describe('contextUtils', () => { expect(result.details?.['file.js'].lineRanges).to.deep.equal([{ first: 1, second: 5 }]) }) }) + + describe('getCodeSymbolDescription', () => { + it('should return empty string when no symbol exists', () => { + const item = { + workspaceFolder: '/workspace', + type: 'file', + relativePath: 'src/file.ts', + id: 'id1', + // No symbol property + } as ContextCommandItem + + const result = getCodeSymbolDescription(item) + expect(result).to.equal('') + }) + + it('should format description without line numbers', () => { + const item = { + workspaceFolder: '/workspace', + type: 'file', + relativePath: 'src/utils.ts', + id: 'id1', + symbol: { + kind: 'Function', + name: 'myFunction', + range: { + start: { line: 9, column: 0 }, + end: { line: 19, column: 1 }, + }, + }, + } as ContextCommandItem + + const result = getCodeSymbolDescription(item, false) + expect(result).to.equal(`Function, ${path.join('workspace', 'src', 'utils.ts')}`) + }) + + it('should format description with line numbers', () => { + const item = { + workspaceFolder: '/workspace', + type: 'file', + relativePath: 'src/utils.ts', + id: 'id1', + symbol: { + kind: 'Class', + name: 'MyClass', + range: { + start: { line: 9, column: 0 }, + end: { line: 19, column: 1 }, + }, + }, + } as ContextCommandItem + + const result = getCodeSymbolDescription(item, true) + expect(result).to.equal(`Class, ${path.join('workspace', 'src', 'utils.ts')}, L10-20`) + }) + + it('should handle different workspace folder names', () => { + const item = { + workspaceFolder: '/projects/my-project', + type: 'file', + relativePath: 'src/utils.ts', + id: 'id1', + symbol: { + kind: 'Method', + name: 'myMethod', + range: { + start: { line: 4, column: 2 }, + end: { line: 7, column: 3 }, + }, + }, + } as ContextCommandItem + + const result = getCodeSymbolDescription(item, true) + expect(result).to.equal(`Method, ${path.join('my-project', 'src', 'utils.ts')}, L5-8`) + }) + + it('should handle different symbol kinds', () => { + const item = { + workspaceFolder: '/workspace', + type: 'file', + relativePath: 'src/models.ts', + id: 'id1', + symbol: { + kind: 'Interface', + name: 'MyInterface', + range: { + start: { line: 0, column: 0 }, + end: { line: 5, column: 1 }, + }, + }, + } as ContextCommandItem + + const result = getCodeSymbolDescription(item, false) + expect(result).to.equal(`Interface, ${path.join('workspace', 'src', 'models.ts')}`) + }) + }) }) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.ts index 927cd737d6..6fa6cee098 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/contextUtils.ts @@ -3,6 +3,7 @@ import * as path from 'path' import { sanitizeFilename } from '@aws/lsp-core/out/util/text' import { RelevantTextDocumentAddition } from './agenticChatTriggerContext' import { FileDetails, FileList } from '@aws/language-server-runtimes/server-interface' +import { ContextCommandItem } from 'local-indexing' export interface ContextInfo { pinnedContextCount: { fileContextCount: number @@ -226,3 +227,33 @@ export function mergeFileLists(fileList1: FileList, fileList2: FileList): FileLi details: mergedDetails, } } + +/** + * Generates a description string for a code symbol with optional line numbers. + * + * @param item - The ContextCommandItem containing symbol and workspace information + * @param includeLineNumbers - Whether to include line number range in the description + * @returns A formatted string containing the symbol kind, path and optionally line numbers, + * or empty string if no symbol exists + * @example + * // Without line numbers: + * // "Function, myProject/src/utils.ts" + * + * // With line numbers: + * // "Function, myProject/src/utils.ts, L10-L20" + */ +export function getCodeSymbolDescription(item: ContextCommandItem, includeLineNumbers?: boolean): string { + const wsFolderName = path.basename(item.workspaceFolder) + + if (item.symbol) { + const symbolKind = item.symbol.kind + const symbolPath = path.join(wsFolderName, item.relativePath) + const symbolLineNumbers = `L${item.symbol.range.start.line + 1}-${item.symbol.range.end.line + 1}` + const parts = [symbolKind, symbolPath] + if (includeLineNumbers) { + parts.push(symbolLineNumbers) + } + return parts.join(', ') + } + return '' +} diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts index ab4d6c25ae..6b334f86a1 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/qAgenticChatServer.ts @@ -6,17 +6,16 @@ import { InitializeParams, Server } from '@aws/language-server-runtimes/server-interface' import { AgenticChatController } from './agenticChatController' import { ChatSessionManagementService } from '../chat/chatSessionManagementService' -import { CLEAR_QUICK_ACTION, HELP_QUICK_ACTION } from '../chat/quickActions' +import { CLEAR_QUICK_ACTION, COMPACT_QUICK_ACTION, HELP_QUICK_ACTION } from '../chat/quickActions' import { TelemetryService } from '../../shared/telemetry/telemetryService' import { makeUserContextObject } from '../../shared/telemetryUtils' -import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/AmazonQTokenServiceManager' import { AmazonQBaseServiceManager } from '../../shared/amazonQServiceManager/BaseAmazonQServiceManager' import { getOrThrowBaseTokenServiceManager } from '../../shared/amazonQServiceManager/AmazonQTokenServiceManager' import { getOrThrowBaseIAMServiceManager } from '../../shared/amazonQServiceManager/AmazonQIAMServiceManager' import { AmazonQWorkspaceConfig } from '../../shared/amazonQServiceManager/configurationUtils' import { TabBarController } from './tabBarController' import { AmazonQServiceInitializationError } from '../../shared/amazonQServiceManager/errors' -import { isUsingIAMAuth, safeGet, enabledModelSelection } from '../../shared/utils' +import { isUsingIAMAuth, safeGet } from '../../shared/utils' import { enabledMCP } from './tools/mcp/mcpUtils' import { QClientCapabilities } from '../configuration/qConfigurationServer' @@ -55,7 +54,7 @@ export const QAgenticChatServer = quickActions: { quickActionsCommandGroups: [ { - commands: [HELP_QUICK_ACTION, CLEAR_QUICK_ACTION], + commands: [HELP_QUICK_ACTION, CLEAR_QUICK_ACTION, COMPACT_QUICK_ACTION], }, ], }, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts index d4799a42c9..23ef53ff77 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts @@ -11,6 +11,7 @@ import { Message } from './util' import { ChatMessage, ToolResultStatus } from '@amzn/codewhisperer-streaming' import * as fs from 'fs' import * as util from './util' +import { sleep } from '@aws/lsp-core/out/util/timeoutUtils' describe('ChatDatabase', () => { let mockFeatures: Features @@ -64,6 +65,32 @@ describe('ChatDatabase', () => { sinon.restore() }) + describe('replaceWithSummary', () => { + it('should create a new history with summary message', async () => { + await chatDb.databaseInitialize(0) + const tabId = 'tab-1' + const tabType = 'cwc' + const conversationId = 'conv-1' + const summaryMessage = { + body: 'This is a summary of the conversation', + type: 'prompt' as any, + timestamp: new Date(), + } + + // Call the method + chatDb.replaceWithSummary(tabId, tabType, conversationId, summaryMessage) + + // Verify the messages array contains the summary and a dummy response + const messages = chatDb.getMessages(tabId, 250) + assert.strictEqual(messages.length, 2) + assert.strictEqual(messages[0].body, summaryMessage.body) + assert.strictEqual(messages[0].type, 'prompt') + assert.strictEqual(messages[1].body, 'Working...') + assert.strictEqual(messages[1].type, 'answer') + assert.strictEqual(messages[1].shouldDisplayMessage, false) + }) + }) + describe('ensureValidMessageSequence', () => { it('should preserve valid alternating sequence', () => { const messages: Message[] = [ @@ -431,6 +458,59 @@ describe('ChatDatabase', () => { }) }) + describe('calculateNewMessageCharacterCount', () => { + it('should calculate character count for new message and pinned context', () => { + const newUserMessage = { + userInputMessage: { + content: 'Test message', + userInputMessageContext: {}, + }, + } as ChatMessage + + const pinnedContextMessages = [ + { + userInputMessage: { + content: 'Pinned context 1', + }, + }, + { + assistantResponseMessage: { + content: 'Pinned response 1', + }, + }, + ] + + // Stub the calculateMessagesCharacterCount method + const calculateMessagesCharacterCountStub = sinon.stub(chatDb, 'calculateMessagesCharacterCount') + calculateMessagesCharacterCountStub.onFirstCall().returns(11) // 'Test message' + calculateMessagesCharacterCountStub.onSecondCall().returns(30) // Pinned context messages + + // Stub the calculateToolSpecCharacterCount method + const calculateToolSpecCharacterCountStub = sinon.stub(chatDb as any, 'calculateToolSpecCharacterCount') + calculateToolSpecCharacterCountStub.returns(50) // Tool spec count + + const result = chatDb.calculateNewMessageCharacterCount(newUserMessage, pinnedContextMessages) + + // Verify the result is the sum of all character counts + assert.strictEqual(result, 91) // 11 + 30 + 50 + + // Verify the methods were called with correct arguments + sinon.assert.calledWith(calculateMessagesCharacterCountStub.firstCall, [ + { + body: 'Test message', + type: 'prompt', + userIntent: undefined, + origin: 'IDE', + userInputMessageContext: {}, + }, + ]) + + // Clean up + calculateMessagesCharacterCountStub.restore() + calculateToolSpecCharacterCountStub.restore() + }) + }) + describe('getWorkspaceIdentifier', () => { const MOCK_MD5_HASH = '5bc032692b81700eb516f317861fbf32' const MOCK_SHA256_HASH = 'bb6b72d3eab82acaabbda8ca6c85658b83e178bb57760913ccdd938bbeaede9f' @@ -562,3 +642,6 @@ describe('ChatDatabase', () => { }) }) }) +function uuid(): `${string}-${string}-${string}-${string}-${string}` { + throw new Error('Function not implemented.') +} diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts index f1c140ec8f..59b948d7c1 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts @@ -22,6 +22,7 @@ import { getChatDbNameFromWorkspaceId, getSha256WorkspaceId, getMd5WorkspaceId, + MessagesWithCharacterCount, } from './util' import * as crypto from 'crypto' import * as path from 'path' @@ -510,14 +511,20 @@ export class ChatDatabase { let historyId = this.#historyIdMapping.get(tabId) if (!historyId) { - historyId = crypto.randomUUID() - this.#features.logging.log(`Creating new historyId=${historyId} for tabId=${tabId}`) - this.setHistoryIdMapping(tabId, historyId) + historyId = this.createHistoryId(tabId) } return historyId } + createHistoryId(tabId: string) { + const historyId = crypto.randomUUID() + this.#features.logging.log(`Creating new historyId=${historyId} for tabId=${tabId}`) + this.setHistoryIdMapping(tabId, historyId) + + return historyId + } + /** * Adds a message to a conversation within a specified tab. * @@ -569,6 +576,63 @@ export class ChatDatabase { } } + /** + * Replace history with summary/dummyResponse pair within a specified tab. + * + * This method manages chat messages by creating a new history with compacted summary and dummy response pairs + */ + replaceWithSummary(tabId: string, tabType: TabType, conversationId: string, message: Message) { + if (this.isInitialized()) { + const clientType = this.#features.lsp.getClientInitializeParams()?.clientInfo?.name || 'unknown' + const tabCollection = this.#db.getCollection(TabCollection) + + this.#features.logging.log( + `Replace history with summary: tabId=${tabId}, tabType=${tabType}, conversationId=${conversationId}` + ) + + const oldHistoryId = this.getOrCreateHistoryId(tabId) + // create a new historyId to start fresh + const historyId = this.createHistoryId(tabId) + + const tabData = historyId ? tabCollection.findOne({ historyId }) : undefined + const tabTitle = + (message.type === 'prompt' && message.shouldDisplayMessage !== false && message.body.trim().length > 0 + ? message.body + : tabData?.title) || 'Amazon Q Chat' + message = this.formatChatHistoryMessage(message) + this.#features.logging.log(`Overriding tab with new historyId=${historyId}`) + tabCollection.insert({ + historyId, + updatedAt: new Date(), + isOpen: true, + tabType: tabType, + title: tabTitle, + conversations: [ + { + conversationId, + clientType, + updatedAt: new Date(), + messages: [ + // summary + message, + // dummy response + { + body: 'Working...', + type: 'answer', + shouldDisplayMessage: false, + timestamp: new Date(), + }, + ], + }, + ], + }) + + if (oldHistoryId) { + tabCollection.findAndRemove({ historyId: oldHistoryId }) + } + } + } + formatChatHistoryMessage(message: Message): Message { if (message.type === ('prompt' as ChatItemType)) { let hasToolResults = false @@ -596,9 +660,15 @@ export class ChatDatabase { * 3. The toolUse and toolResult relationship is valid * 4. The history character length is <= MaxConversationHistoryCharacters - newUserMessageCharacterCount. Oldest messages are dropped. */ - fixAndGetHistory(tabId: string, newUserMessage: ChatMessage, pinnedContextMessages: ChatMessage[]) { + fixAndGetHistory( + tabId: string, + newUserMessage: ChatMessage, + pinnedContextMessages: ChatMessage[], + newUserInputCount?: number + ): MessagesWithCharacterCount { + let messagesWithCount: MessagesWithCharacterCount = { messages: [], count: 0 } if (!this.isInitialized()) { - return [] + return messagesWithCount } this.#features.logging.info(`Fixing history: tabId=${tabId}`) @@ -612,10 +682,14 @@ export class ChatDatabase { // 3. Fix new user prompt: Ensure lastMessage in history toolUse and newMessage toolResult relationship is valid this.validateAndFixNewMessageToolResults(allMessages, newUserMessage) + if (!newUserInputCount) { + newUserInputCount = this.calculateNewMessageCharacterCount(newUserMessage, pinnedContextMessages) + } + // 4. NOTE: Keep this trimming logic at the end of the preprocess. // Make sure max characters ≤ remaining Character Budget, must be put at the end of preprocessing - allMessages = this.trimMessagesToMaxLength(allMessages, newUserMessage, pinnedContextMessages) - + messagesWithCount = this.trimMessagesToMaxLength(allMessages, newUserInputCount) + allMessages = messagesWithCount.messages // Edge case: If the history is empty and the next message contains tool results, then we have to just abandon them. if ( allMessages.length === 0 && @@ -630,10 +704,13 @@ export class ChatDatabase { // Prepend pinned context fake message pair to beginning of history if (pinnedContextMessages.length === 2) { - allMessages = [...pinnedContextMessages.map(msg => chatMessageToMessage(msg)), ...allMessages] + messagesWithCount.messages = [ + ...pinnedContextMessages.map(msg => chatMessageToMessage(msg)), + ...allMessages, + ] } - return allMessages + return messagesWithCount } /** @@ -662,25 +739,11 @@ export class ChatDatabase { return !!ctx && (!ctx.toolResults || ctx.toolResults.length === 0) && message.body !== '' } - private trimMessagesToMaxLength( - messages: Message[], - newUserMessage: ChatMessage, - pinnedContextMessages: ChatMessage[] - ): Message[] { - let totalCharacters = this.calculateMessagesCharacterCount(messages) - this.#features.logging.debug(`Current history characters: ${totalCharacters}`) - const currentUserInputCharacterCount = this.calculateMessagesCharacterCount([ - chatMessageToMessage(newUserMessage), - ...pinnedContextMessages.map(msg => chatMessageToMessage(msg)), - ]) - const currentInputToolSpecCount = this.calculateToolSpecCharacterCount(newUserMessage) - const currentUserInputCount = currentUserInputCharacterCount + currentInputToolSpecCount - this.#features.logging.debug( - `Current user message characters input: ${currentUserInputCharacterCount} + toolSpec: ${currentInputToolSpecCount}` - ) - const maxHistoryCharacterSize = Math.max(0, MaxOverallCharacters - currentUserInputCount) + private trimMessagesToMaxLength(messages: Message[], newUserInputCount: number): MessagesWithCharacterCount { + let historyCharacterCount = this.calculateMessagesCharacterCount(messages) + const maxHistoryCharacterSize = Math.max(0, MaxOverallCharacters - newUserInputCount) this.#features.logging.debug(`Current remaining character budget: ${maxHistoryCharacterSize}`) - while (totalCharacters > maxHistoryCharacterSize && messages.length > 2) { + while (historyCharacterCount > maxHistoryCharacterSize && messages.length > 2) { // Find the next valid user message to start from const indexToTrim = this.findIndexToTrim(messages) if (indexToTrim !== undefined && indexToTrim > 0) { @@ -692,12 +755,14 @@ export class ChatDatabase { this.#features.logging.debug( 'Could not find a valid point to trim, reset history to reduce character count' ) - return [] + return { messages: [], count: 0 } } - totalCharacters = this.calculateMessagesCharacterCount(messages) - this.#features.logging.debug(`Current history characters: ${totalCharacters}`) + historyCharacterCount = this.calculateMessagesCharacterCount(messages) + } + return { + messages, + count: historyCharacterCount, } - return messages } private calculateToolSpecCharacterCount(currentMessage: ChatMessage): number { @@ -714,17 +779,36 @@ export class ChatDatabase { return count } - private calculateMessagesCharacterCount(allMessages: Message[]): number { - let count = 0 + calculateNewMessageCharacterCount(newUserMessage: ChatMessage, pinnedContextMessages: ChatMessage[]): number { + const currentUserInputCharacterCount = this.calculateMessagesCharacterCount([ + chatMessageToMessage(newUserMessage), + ]) + const pinnedContextCount = this.calculateMessagesCharacterCount([ + ...pinnedContextMessages.map(msg => chatMessageToMessage(msg)), + ]) + const currentInputToolSpecCount = this.calculateToolSpecCharacterCount(newUserMessage) + const totalCount = currentUserInputCharacterCount + currentInputToolSpecCount + pinnedContextCount + this.#features.logging.debug( + `Current user message characters input: ${currentUserInputCharacterCount} + toolSpec: ${currentInputToolSpecCount} + pinnedContext: ${pinnedContextCount} = total: ${totalCount}` + ) + return totalCount + } + + calculateMessagesCharacterCount(allMessages: Message[]): number { + let bodyCount = 0 + let toolUsesCount = 0 + let toolResultsCount = 0 + let editorStateCount = 0 + for (const message of allMessages) { // Count characters of all message text - count += message.body.length + bodyCount += message.body.length // Count characters in tool uses if (message.toolUses) { try { for (const toolUse of message.toolUses) { - count += JSON.stringify(toolUse).length + toolUsesCount += JSON.stringify(toolUse).length } } catch (e) { this.#features.logging.error(`Error counting toolUses: ${String(e)}`) @@ -734,7 +818,7 @@ export class ChatDatabase { if (message.userInputMessageContext?.toolResults) { try { for (const toolResul of message.userInputMessageContext.toolResults) { - count += JSON.stringify(toolResul).length + toolResultsCount += JSON.stringify(toolResul).length } } catch (e) { this.#features.logging.error(`Error counting toolResults: ${String(e)}`) @@ -742,13 +826,18 @@ export class ChatDatabase { } if (message.userInputMessageContext?.editorState) { try { - count += JSON.stringify(message.userInputMessageContext?.editorState).length + editorStateCount += JSON.stringify(message.userInputMessageContext?.editorState).length } catch (e) { this.#features.logging.error(`Error counting editorState: ${String(e)}`) } } } - return count + + const totalCount = bodyCount + toolUsesCount + toolResultsCount + editorStateCount + this.#features.logging.debug( + `Messages characters: body: ${bodyCount} + toolUses: ${toolUsesCount} + toolResults: ${toolResultsCount} + editorState: ${editorStateCount} = total: ${totalCount}` + ) + return totalCount } /** @@ -808,7 +897,7 @@ export class ChatDatabase { if (messages.length > 0 && messages[messages.length - 1].type === ('prompt' as ChatItemType)) { // Add an assistant response to both request and DB to maintain a valid sequence const dummyResponse: Message = { - body: 'Thinking...', + body: 'Working...', type: 'answer', shouldDisplayMessage: false, timestamp: new Date(), diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts index 312dd354e5..45f37929fc 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts @@ -123,6 +123,11 @@ export type TabWithDbMetadata = { */ export type DbReference = { collection: Collection; db: Loki } +export type MessagesWithCharacterCount = { + messages: Message[] + count: number +} + /** * Converts Message to codewhisperer-streaming ChatMessage */ diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.test.ts index 1ffda4f822..c524550ee7 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.test.ts @@ -114,7 +114,7 @@ describe('ExecuteBash Tool', () => { getTextDocument: async s => ({}) as TextDocument, }, }) - const result = await execBash.requiresAcceptance({ command: 'echo hello world', cwd: workspaceFolder }) + const result = await execBash.requiresAcceptance({ command: 'ps aux', cwd: workspaceFolder }) assert.equal( result.requiresAcceptance, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.ts index ba35188564..be43d97353 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.ts @@ -27,12 +27,10 @@ export const commandCategories = new Map([ ['cat', CommandCategory.ReadOnly], ['bat', CommandCategory.ReadOnly], ['pwd', CommandCategory.ReadOnly], - ['echo', CommandCategory.ReadOnly], ['file', CommandCategory.ReadOnly], ['less', CommandCategory.ReadOnly], ['more', CommandCategory.ReadOnly], ['tree', CommandCategory.ReadOnly], - ['find', CommandCategory.ReadOnly], ['top', CommandCategory.ReadOnly], ['htop', CommandCategory.ReadOnly], ['ps', CommandCategory.ReadOnly], @@ -54,7 +52,6 @@ export const commandCategories = new Map([ ['diff', CommandCategory.ReadOnly], ['head', CommandCategory.ReadOnly], ['tail', CommandCategory.ReadOnly], - ['grep', CommandCategory.ReadOnly], // Mutable commands ['chmod', CommandCategory.Mutate], @@ -81,6 +78,9 @@ export const commandCategories = new Map([ ['exec', CommandCategory.Mutate], ['eval', CommandCategory.Mutate], ['xargs', CommandCategory.Mutate], + ['echo', CommandCategory.Mutate], + ['grep', CommandCategory.Mutate], + ['find', CommandCategory.Mutate], // Destructive commands ['rm', CommandCategory.Destructive], @@ -538,7 +538,7 @@ export class ExecuteBash { outputQueue.push({ timestamp, isStdout: true, - content: IS_WINDOWS_PLATFORM ? ExecuteBash.decodeWinUtf(chunk) : chunk, + content: chunk, isFirst, }) processQueue() @@ -553,7 +553,7 @@ export class ExecuteBash { outputQueue.push({ timestamp, isStdout: false, - content: IS_WINDOWS_PLATFORM ? ExecuteBash.decodeWinUtf(chunk) : chunk, + content: chunk, isFirst, }) processQueue() @@ -638,24 +638,6 @@ export class ExecuteBash { }) } - /** - * Re‑creates the raw bytes from the received string (Buffer.from(text, 'binary')). - * Detects UTF‑16 LE by checking whether every odd byte in the first 32 bytes is 0x00. - * Decodes with buf.toString('utf16le') when the pattern matches, otherwise falls back to UTF‑8. - */ - private static decodeWinUtf(raw: string): string { - const buffer = Buffer.from(raw, 'binary') - - let utf16 = true - for (let i = 1, n = Math.min(buffer.length, 32); i < n; i += 2) { - if (buffer[i] !== 0x00) { - utf16 = false - break - } - } - return utf16 ? buffer.toString('utf16le') : buffer.toString('utf8') - } - private static handleChunk(chunk: string, buffer: string[], writer?: WritableStreamDefaultWriter) { try { // Trim trailing newlines from the chunk before writing diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/chatSessionService.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/chatSessionService.ts index 6a15aa619c..d553866b99 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/chatSessionService.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/chatSessionService.ts @@ -82,13 +82,17 @@ export class ChatSessionService { this.#deferredToolExecution[messageId] = { resolve, reject } } + public getAllDeferredCompactMessageIds(): string[] { + return Object.keys(this.#deferredToolExecution).filter(messageId => messageId.endsWith('_compact')) + } + public rejectAllDeferredToolExecutions(error: Error): void { - for (const messageId in this.#deferredToolExecution) { + Object.keys(this.#deferredToolExecution).forEach(messageId => { const handler = this.#deferredToolExecution[messageId] if (handler && handler.reject) { handler.reject(error) } - } + }) // Clear all handlers after rejecting them this.#deferredToolExecution = {} } diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/quickActions.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/quickActions.ts index 0ffc71a6f3..17892949fc 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/quickActions.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/quickActions.ts @@ -1,6 +1,7 @@ export enum QuickAction { Clear = '/clear', Help = '/help', + Compact = '/compact', } export const HELP_QUICK_ACTION = { @@ -14,3 +15,9 @@ export const CLEAR_QUICK_ACTION = { description: 'Clear this session', icon: 'trash', } + +export const COMPACT_QUICK_ACTION = { + command: QuickAction.Compact, + description: 'Compact this conversation', + icon: 'folder', +} diff --git a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts index fcdf424700..a0713e5837 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/chat/telemetry/chatTelemetryController.ts @@ -169,6 +169,16 @@ export class ChatTelemetryController { } } + public emitActiveUser() { + this.#telemetry.emitMetric({ + name: ChatTelemetryEventName.ActiveUser, + data: { + credentialStartUrl: this.#credentialsProvider.getConnectionMetadata()?.sso?.startUrl, + result: 'Succeeded', + }, + }) + } + public emitAgencticLoop_InvokeLLM( requestId: string, conversationId: string, diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts index adfdd476fd..9397c709d9 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.test.ts @@ -1,6 +1,6 @@ import assert = require('assert') import { FileContext } from '../../../shared/codeWhispererService' -import { autoTrigger, triggerType } from './autoTrigger' +import { autoTrigger, getAutoTriggerType, triggerType } from './autoTrigger' describe('Auto Trigger', async () => { const createBasicFileContext = (left: string = '', right: string = ''): FileContext => ({ @@ -103,7 +103,59 @@ describe('Auto Trigger', async () => { assert.equal(trigger, 'Classifier') }) }) + describe('getAutoTriggerType', () => { + const createContentChange = (text: string) => [{ text }] + it('should return undefined for multi-line changes', () => { + const changes = [{ text: 'line1\n' }, { text: 'line2' }] + assert.strictEqual(getAutoTriggerType(changes), undefined) + }) + + it('should return undefined for empty changes', () => { + assert.strictEqual(getAutoTriggerType(createContentChange('')), undefined) + }) + + it('should return "Enter" for newline changes', () => { + assert.strictEqual(getAutoTriggerType(createContentChange('\n')), 'Enter') + assert.strictEqual(getAutoTriggerType(createContentChange('\r\n')), 'Enter') + assert.strictEqual(getAutoTriggerType(createContentChange('\n ')), 'Enter') + }) + + it('should return undefined for tab changes', () => { + assert.strictEqual(getAutoTriggerType(createContentChange(' ')), undefined) + assert.strictEqual(getAutoTriggerType(createContentChange(' ')), undefined) + }) + + it('should return "SpecialCharacters" for special character changes', () => { + assert.strictEqual(getAutoTriggerType(createContentChange('(')), 'SpecialCharacters') + assert.strictEqual(getAutoTriggerType(createContentChange('()')), 'SpecialCharacters') + assert.strictEqual(getAutoTriggerType(createContentChange('[')), 'SpecialCharacters') + assert.strictEqual(getAutoTriggerType(createContentChange('[]')), 'SpecialCharacters') + assert.strictEqual(getAutoTriggerType(createContentChange('{')), 'SpecialCharacters') + assert.strictEqual(getAutoTriggerType(createContentChange('{}')), 'SpecialCharacters') + assert.strictEqual(getAutoTriggerType(createContentChange(':')), 'SpecialCharacters') + }) + + it('should return "Classifier" for single character changes', () => { + assert.strictEqual(getAutoTriggerType(createContentChange('a')), 'Classifier') + assert.strictEqual(getAutoTriggerType(createContentChange('1')), 'Classifier') + assert.strictEqual(getAutoTriggerType(createContentChange('.')), 'Classifier') + }) + + it('should return undefined for single line reformat', () => { + assert.strictEqual(getAutoTriggerType(createContentChange(' ')), undefined) + assert.strictEqual(getAutoTriggerType(createContentChange(' ')), undefined) + }) + + it('should return undefined for multi-character non-special changes', () => { + assert.strictEqual(getAutoTriggerType(createContentChange('abc')), undefined) + assert.strictEqual(getAutoTriggerType(createContentChange('123')), undefined) + }) + + it('should return undefined for multi-line input', () => { + assert.strictEqual(getAutoTriggerType(createContentChange('line1\nline2')), undefined) + }) + }) describe('Right Context should trigger validation', () => { it('should not trigger when there is immediate right context in VSCode', () => { const params = createBasicParams({ diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts index 5c110c9e2a..e9c5e31247 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts @@ -1,6 +1,7 @@ import { Logging } from '@aws/language-server-runtimes/server-interface' import { FileContext } from '../../../shared/codeWhispererService' import typedCoefficients = require('./coefficients.json') +import { TextDocumentContentChangeEvent } from 'vscode-languageserver-textdocument' type TypedCoefficients = typeof typedCoefficients type Coefficients = TypedCoefficients & { @@ -61,6 +62,78 @@ export const triggerType = (fileContext: FileContext): CodewhispererAutomatedTri return 'Classifier' } +// Enter key should always start with ONE '\n' or '\r\n' and potentially following spaces due to IDE reformat +function isEnterKey(str: string): boolean { + if (str.length === 0) { + return false + } + return ( + (str.startsWith('\r\n') && str.substring(2).trim() === '') || + (str[0] === '\n' && str.substring(1).trim() === '') + ) +} + +function isSingleLine(str: string): boolean { + let newLineCounts = 0 + for (const ch of str) { + if (ch === '\n') { + newLineCounts += 1 + } + } + + // since pressing Enter key possibly will generate string like '\n ' due to indention + if (isEnterKey(str)) { + return true + } + if (newLineCounts >= 1) { + return false + } + return true +} + +function isUserTypingSpecialChar(str: string): boolean { + return ['(', '()', '[', '[]', '{', '{}', ':'].includes(str) +} + +function isTabKey(str: string): boolean { + const tabSize = 4 // TODO: Use IDE real tab size + if (str.length % tabSize === 0 && str.trim() === '') { + return true + } + return false +} + +// Reference: https://github.com/aws/aws-toolkit-vscode/blob/amazonq/v1.74.0/packages/core/src/codewhisperer/service/keyStrokeHandler.ts#L222 +// Enter, Special character guarantees a trigger +// Regular keystroke input will be evaluated by classifier +export const getAutoTriggerType = ( + contentChanges: TextDocumentContentChangeEvent[] +): CodewhispererAutomatedTriggerType | undefined => { + if (contentChanges.length !== 1) { + // Won't trigger cwspr on multi-line changes + // event.contentChanges.length will be 2 when user press Enter key multiple times + return undefined + } + const changedText = contentChanges[0].text + if (isSingleLine(changedText)) { + if (changedText.length === 0) { + return undefined + } else if (isEnterKey(changedText)) { + return 'Enter' + } else if (isTabKey(changedText)) { + return undefined + } else if (isUserTypingSpecialChar(changedText)) { + return 'SpecialCharacters' + } else if (changedText.length === 1) { + return 'Classifier' + } else if (new RegExp('^[ ]+$').test(changedText)) { + // single line && single place reformat should consist of space chars only + return undefined + } + } + return undefined +} + // Normalize values based on minn and maxx values in the coefficients. const normalize = (val: number, field: keyof typeof typedCoefficients.minn & keyof typeof typedCoefficients.maxx) => (val - typedCoefficients.minn[field]) / (typedCoefficients.maxx[field] - typedCoefficients.minn[field]) diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.ts index 6109f76b72..eee9edbdcb 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.ts @@ -54,7 +54,12 @@ export const editPredictionAutoTrigger = ({ ) // [condition 2] Non-empty content in one of the lines following the current line - const hasNonEmptySuffix = rightContextLines.length > 1 && rightContextLines[1].trim().length > 0 + let hasNonEmptySuffix = false + const maxLinesToScanForContent = Math.min(rightContextLines.length, config.maxLinesToScanForContent + 1) + if (maxLinesToScanForContent > 0) { + const linesToScanForContent = rightContextLines.slice(1, maxLinesToScanForContent) + hasNonEmptySuffix = linesToScanForContent.some(line => line.trim().length > 0) + } const shouldTrigger = hasRecentEdit && hasNonEmptySuffix diff --git a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionConfig.ts b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionConfig.ts index 7f84f9c722..c419a4944f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionConfig.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/editPredictionConfig.ts @@ -18,6 +18,7 @@ export interface EditPredictionConfig { // Edit tracking editHistoryDurationMs: number editAdjacentLineRange: number + maxLinesToScanForContent: number // Feature flags enableLanguageKeywordTrigger: boolean @@ -36,6 +37,7 @@ export const DEFAULT_EDIT_PREDICTION_CONFIG: EditPredictionConfig = { cursorUpdateIntervalMs: 250, // 250 milliseconds editHistoryDurationMs: 300000, // 5 minutes editAdjacentLineRange: 3, + maxLinesToScanForContent: 3, enableLanguageKeywordTrigger: true, enableOperatorDelimiterTrigger: true, enableUserPauseTrigger: true, 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 0aa1a37254..732f17fb98 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 @@ -17,7 +17,7 @@ import { IdeDiagnostic, } from '@aws/language-server-runtimes/server-interface' import { AWSError } from 'aws-sdk' -import { autoTrigger, triggerType } from './auto-trigger/autoTrigger' +import { autoTrigger, getAutoTriggerType, triggerType } from './auto-trigger/autoTrigger' import { CodeWhispererServiceToken, GenerateSuggestionsRequest, @@ -426,10 +426,22 @@ export const CodewhispererServerFactory = ? workspaceState.workspaceId : undefined - // TODO: Can we get this derived from a keyboard event in the future? - // This picks the last non-whitespace character, if any, before the cursor - const triggerCharacter = fileContext.leftFileContent.trim().at(-1) ?? '' - const codewhispererAutoTriggerType = triggerType(fileContext) + let triggerCharacters = '' + let codewhispererAutoTriggerType = undefined + // Reference: https://github.com/aws/aws-toolkit-vscode/blob/amazonq/v1.74.0/packages/core/src/codewhisperer/service/classifierTrigger.ts#L477 + if ( + params.documentChangeParams?.contentChanges && + params.documentChangeParams.contentChanges.length > 0 && + params.documentChangeParams.contentChanges[0].text !== undefined + ) { + triggerCharacters = params.documentChangeParams.contentChanges[0].text + codewhispererAutoTriggerType = getAutoTriggerType(params.documentChangeParams.contentChanges) + } else { + // if the client does not emit document change for the trigger, use left most character. + triggerCharacters = fileContext.leftFileContent.trim().at(-1) ?? '' + codewhispererAutoTriggerType = triggerType(fileContext) + } + const previousSession = sessionManager.getPreviousSession() const previousDecision = previousSession?.getAggregatedUserTriggerDecision() ?? '' const previousSuggestionType = previousSession?.suggestionType ?? '' @@ -439,11 +451,18 @@ export const CodewhispererServerFactory = if (initializeParams !== undefined) { ideCategory = getIdeCategory(initializeParams) } + + // See: https://github.com/aws/aws-toolkit-vscode/blob/amazonq/v1.74.0/packages/core/src/codewhisperer/service/keyStrokeHandler.ts#L132 + // In such cases, do not auto trigger. + if (codewhispererAutoTriggerType === undefined) { + return EMPTY_RESULT + } + const autoTriggerResult = autoTrigger( { fileContext, // The left/right file context and programming language lineNum: params.position.line, // the line number of the invocation, this is the line of the cursor - char: triggerCharacter, // Add the character just inserted, if any, before the invication position + char: triggerCharacters, // Add the character just inserted, if any, before the invication position ide: ideCategory ?? '', os: '', // TODO: We should get this in a platform-agnostic way (i.e., compatible with the browser) previousDecision, // The last decision by the user on the previous invocation @@ -523,7 +542,7 @@ export const CodewhispererServerFactory = const editPredictionAutoTriggerResult = editPredictionAutoTrigger({ fileContext: fileContext, lineNum: params.position.line, - char: triggerCharacter, + char: triggerCharacters, previousDecision: previousDecision, cursorHistory: cursorTracker, recentEdits: recentEditTracker, @@ -569,7 +588,7 @@ export const CodewhispererServerFactory = document: { relativeFilePath: textDocument.uri, programmingLanguage: { - languageName: textDocument.languageId, + languageName: requestContext.fileContext.programmingLanguage.languageName, }, text: textDocument.getText(), }, @@ -601,8 +620,7 @@ export const CodewhispererServerFactory = } // Emit user trigger decision at session close time for active session sessionManager.discardSession(currentSession) - // TODO add streakLength back once the model is updated - // const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(false) : 0 + const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(false) : 0 await emitUserTriggerDecisionTelemetry( telemetry, telemetryService, @@ -611,7 +629,8 @@ export const CodewhispererServerFactory = 0, 0, [], - [] + [], + streakLength ) } @@ -643,7 +662,7 @@ export const CodewhispererServerFactory = language: fileContext.programmingLanguage.languageName, requestContext: requestContext, autoTriggerType: isAutomaticLspTriggerKind ? codewhispererAutoTriggerType : undefined, - triggerCharacter: triggerCharacter, + triggerCharacter: triggerCharacters, classifierResult: autoTriggerResult?.classifierResult, classifierThreshold: autoTriggerResult?.classifierThreshold, credentialStartUrl: credentialsProvider.getConnectionMetadata?.()?.sso?.startUrl ?? undefined, @@ -714,13 +733,17 @@ export const CodewhispererServerFactory = if (session.discardInflightSessionOnNewInvocation) { session.discardInflightSessionOnNewInvocation = false sessionManager.discardSession(session) - // TODO add streakLength back once the model is updated - // const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(false) : 0 + const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(false) : 0 await emitUserTriggerDecisionTelemetry( telemetry, telemetryService, session, - timeSinceLastUserModification + timeSinceLastUserModification, + 0, + 0, + [], + [], + streakLength ) } @@ -1004,8 +1027,7 @@ export const CodewhispererServerFactory = // Always emit user trigger decision at session close sessionManager.closeSession(session) - // TODO add streakLength back once the model is updated - // const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(isAccepted) : 0 + const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(isAccepted) : 0 await emitUserTriggerDecisionTelemetry( telemetry, telemetryService, @@ -1014,7 +1036,8 @@ export const CodewhispererServerFactory = addedCharactersForEditSuggestion.length, deletedCharactersForEditSuggestion.length, addedDiagnostics, - removedDiagnostics + removedDiagnostics, + streakLength ) } diff --git a/server/aws-lsp-codewhisperer/src/shared/activeUserTracker.test.ts b/server/aws-lsp-codewhisperer/src/shared/activeUserTracker.test.ts new file mode 100644 index 0000000000..1389d2adb7 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/shared/activeUserTracker.test.ts @@ -0,0 +1,191 @@ +import { ActiveUserTracker, DEFAULT_ACTIVE_USER_WINDOW_MINUTES } from './activeUserTracker' +import * as sinon from 'sinon' +import * as assert from 'assert' +import { Features } from '@aws/language-server-runtimes/server-interface/server' + +describe('ActiveUserTracker', function () { + // Increase the timeout for all tests in this suite + this.timeout(10000) + let clock: sinon.SinonFakeTimers + let mockFeatures: Features + let mockFs: any + let mockClientId: string + + beforeEach(function () { + // Ensure singleton is completely reset + if ((ActiveUserTracker as any).instance) { + ;(ActiveUserTracker as any).instance.dispose() + } + ;(ActiveUserTracker as any).instance = undefined + + // Use fake timers starting at timestamp 0 + clock = sinon.useFakeTimers(0) + + // Setup mock file system + mockFs = { + exists: sinon.stub().resolves(false), + readFile: sinon.stub().resolves(''), + writeFile: sinon.stub().resolves(undefined), + mkdir: sinon.stub().resolves(undefined), + rm: sinon.stub().resolves(undefined), + } + + // Setup mock client ID + mockClientId = 'test-client-id' + + // Setup mock features + mockFeatures = { + workspace: { + fs: mockFs, + } as any, + logging: { + debug: sinon.stub(), + info: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub(), + } as any, + lsp: { + getClientInitializeParams: sinon.stub().returns({ + initializationOptions: { + aws: { + clientInfo: { + clientId: mockClientId, + }, + }, + }, + }), + } as any, + } as Features + }) + + afterEach(function () { + // Restore real timers + clock.restore() + + // Restore all stubs + sinon.restore() + + // Ensure singleton is disposed + if ((ActiveUserTracker as any).instance) { + ;(ActiveUserTracker as any).instance.dispose() + } + }) + + it('should return true for first call', function () { + const tracker = ActiveUserTracker.getInstance(mockFeatures) + const result = tracker.isNewActiveUser() + assert.strictEqual(result, true) + }) + + it('should return false for calls within window', function () { + const tracker = ActiveUserTracker.getInstance(mockFeatures) + + // First call returns true and starts a window + assert.strictEqual(tracker.isNewActiveUser(), true) + + // Advance time within window by 100 ms + clock.tick(100) + + // Second call within window should return false + assert.strictEqual(tracker.isNewActiveUser(), false) + }) + + it('should return true for calls after window expires', function () { + const tracker = ActiveUserTracker.getInstance(mockFeatures) + + // First call returns true + assert.strictEqual(tracker.isNewActiveUser(), true) + + // Advance time past the window (convert minutes to milliseconds) + clock.tick((DEFAULT_ACTIVE_USER_WINDOW_MINUTES + 1) * 60 * 1000) + + // Call after window expires returns true + assert.strictEqual(tracker.isNewActiveUser(), true) + }) + + it('should reset tracker on dispose', function () { + const tracker = ActiveUserTracker.getInstance(mockFeatures) + tracker.isNewActiveUser() + + tracker.dispose() + + // After dispose, next call should return true + assert.strictEqual(tracker.isNewActiveUser(), true) + }) + + it('should use client ID from features', function () { + const tracker = ActiveUserTracker.getInstance(mockFeatures) + + // Verify the client ID was used + assert.strictEqual((tracker as any).clientId, mockClientId) + }) + + it('should update client ID when it changes', function () { + // First create with initial client ID + const tracker = ActiveUserTracker.getInstance(mockFeatures) + assert.strictEqual((tracker as any).clientId, mockClientId) + + // Now change the client ID + const newClientId = 'new-client-id' + const newFeatures = { ...mockFeatures } + newFeatures.lsp = { + getClientInitializeParams: sinon.stub().returns({ + initializationOptions: { aws: { clientInfo: { clientId: newClientId } } }, + }), + } as any + + // Get instance with new client ID + const updatedTracker = ActiveUserTracker.getInstance(newFeatures) + + // Verify it's the same instance but with updated client ID + assert.strictEqual(updatedTracker, tracker) + assert.strictEqual((updatedTracker as any).clientId, newClientId) + }) + + it('should call persistState when a new active user event occurs', function () { + // Setup - create a spy on the private persistState method + const tracker = ActiveUserTracker.getInstance(mockFeatures) + const persistStateSpy = sinon.spy(tracker as any, 'persistState') + + // Trigger a new active user event which should call persistState + tracker.isNewActiveUser() + + // Verify that persistState was called + assert.strictEqual(persistStateSpy.called, true, 'persistState should be called') + }) + + it('should load state from disk on initialization', function () { + // Setup - prepare mock file system with existing state + const timestamp = 12345 + const existingState = { + clients: { + [mockClientId]: timestamp, + }, + } + + mockFs.exists.resolves(true) + mockFs.readFile.resolves(JSON.stringify(existingState)) + + // Reset singleton for this test + ;(ActiveUserTracker as any).instance = undefined + + // Act - create a new tracker which will load state + const tracker = ActiveUserTracker.getInstance(mockFeatures) + + // Force synchronous execution of promises + clock.runAll() + + // Directly set the timestamp to simulate the async load completing + // This is necessary because the real loadPersistedState is async and we can't await it in the test + ;(tracker as any).windowStartTimestamp = timestamp + + // Assert - verify file was read + assert.strictEqual(mockFs.exists.calledOnce, true) + + // Assert - verify timestamp was loaded + assert.strictEqual((tracker as any).windowStartTimestamp, timestamp) + + // Verify behavior - should return false for calls within window + assert.strictEqual(tracker.isNewActiveUser(), false) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/shared/activeUserTracker.ts b/server/aws-lsp-codewhisperer/src/shared/activeUserTracker.ts new file mode 100644 index 0000000000..57232969e6 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/shared/activeUserTracker.ts @@ -0,0 +1,191 @@ +/** + * This class tracks active user events based on time windows. + * If multiple messages are sent within the same time window, it will count as a single active user event. + * The time window is not reset by subsequent messages within the window. + * A new message outside the window will reset the window and count as a new active user event. + * + * The window state is persisted to disk to survive across IDE restarts and user logout/login cycles. + */ + +import * as os from 'os' +import path = require('path') +import { Features } from '@aws/language-server-runtimes/server-interface/server' + +// Default window size in minutes (24 hours) +export const DEFAULT_ACTIVE_USER_WINDOW_MINUTES = 24 * 60 + +// The state file contains a map of client IDs to their window start timestamps +interface WindowState { + clients: Record +} + +export class ActiveUserTracker { + private static instance: ActiveUserTracker | undefined + private windowStartTimestamp: number = -1 + private windowSizeMs: number + private clientId: string + private stateFilePath: string + private features: Features + + private constructor(features: Features) { + this.windowSizeMs = DEFAULT_ACTIVE_USER_WINDOW_MINUTES * 60 * 1000 + this.features = features + this.clientId = this.getClientId() + this.stateFilePath = this.getStateFilePath() + // Initialize with default state + void this.loadPersistedState() + } + + /** + * Gets the singleton instance of ActiveUserTracker + * @param features Features object containing workspace, logging, lsp, and other services + * @returns The ActiveUserTracker instance + */ + public static getInstance(features: Features): ActiveUserTracker { + if (!ActiveUserTracker.instance) { + ActiveUserTracker.instance = new ActiveUserTracker(features) + } + + const currentClientId = + features.lsp.getClientInitializeParams()?.initializationOptions?.aws?.clientInfo?.clientId + if (currentClientId && ActiveUserTracker.instance.clientId !== currentClientId) { + // Client ID changed, update it and load its state + features.logging.debug( + `Client ID changed from ${ActiveUserTracker.instance.clientId} to ${currentClientId}` + ) + ActiveUserTracker.instance.clientId = currentClientId + + // Don't reset the timestamp here - just load the state for the new client ID + // If there's no state for this client ID, loadPersistedState will leave windowStartTimestamp as -1 + // which will trigger a new active user event on the next isNewActiveUser() call + void ActiveUserTracker.instance.loadPersistedState() + } + + return ActiveUserTracker.instance + } + + /** + * Determines if it should count as a new active user event + * @returns true if this is a new active user event, false if it's within an existing window + */ + public isNewActiveUser(): boolean { + const currentTime = Date.now() + + // If this is the first message or the window has expired + if (this.windowStartTimestamp === -1 || currentTime - this.windowStartTimestamp > this.windowSizeMs) { + // This is a new active user event - start a new window + this.windowStartTimestamp = currentTime + this.features.logging.debug( + `New active user event for client ${this.clientId}, setting timestamp: ${this.windowStartTimestamp}` + ) + void this.persistState() + return true + } + + // Message is within the current window, do NOT update the timestamp + // The window continues from its original start time + return false + } + + /** + * Resets the tracker + */ + public dispose(): void { + // Just reset the in-memory state and clear the singleton instance + this.windowStartTimestamp = -1 + ActiveUserTracker.instance = undefined + } + + /** + * Gets the client ID from the LSP initialization parameters or generates a machine ID if not available + * @returns The client ID + */ + private getClientId(): string { + const clientId = this.features.lsp.getClientInitializeParams()?.initializationOptions?.aws?.clientInfo?.clientId + if (clientId) { + return clientId + } + + // Generate a machine-specific ID if no client ID is available + const hostname = os.hostname() + const username = os.userInfo().username + return `${hostname}-${username}` + } + + /** + * Gets the path to the state file + * @returns The path to the state file + */ + private getStateFilePath(): string { + const amazonQDir = path.join(os.homedir(), '.aws', 'amazonq') + + // Directory will be created when needed in persistState() + return path.join(amazonQDir, 'active-user-state.json') + } + + /** + * Loads the persisted state from disk + */ + private async loadPersistedState(): Promise { + try { + const exists = await this.features.workspace.fs.exists(this.stateFilePath) + if (exists) { + const data = await this.features.workspace.fs.readFile(this.stateFilePath, { encoding: 'utf8' }) + const state = JSON.parse(data) as WindowState + + // If the client exists in the state, restore its timestamp + if (state.clients && state.clients[this.clientId] !== undefined) { + this.windowStartTimestamp = state.clients[this.clientId] + this.features.logging.debug( + `Loaded active user state for client ${this.clientId}, timestamp: ${this.windowStartTimestamp}` + ) + } + } + } catch (error) { + // If there's any error reading the state, delete the corrupted file and start fresh + this.windowStartTimestamp = -1 + this.features.logging.warn(`Error loading active user state: ${error}`) + try { + const exists = await this.features.workspace.fs.exists(this.stateFilePath) + if (exists) { + await this.features.workspace.fs.rm(this.stateFilePath) + } + } catch (deleteError) { + // Ignore errors when deleting corrupted file + } + } + } + + /** + * Persists the current state to disk + */ + private async persistState(): Promise { + try { + // Try to read existing state file to update it + let state: WindowState = { clients: {} } + + // Create directory if it doesn't exist + const dirPath = path.dirname(this.stateFilePath) + await this.features.workspace.fs.mkdir(dirPath, { recursive: true }) + + const exists = await this.features.workspace.fs.exists(this.stateFilePath) + if (exists) { + try { + const data = await this.features.workspace.fs.readFile(this.stateFilePath, { encoding: 'utf8' }) + state = JSON.parse(data) as WindowState + } catch (error) { + // If there's any error reading the state, start fresh + state = { clients: {} } + this.features.logging.warn(`Error parsing active user state file: ${error}`) + } + } + + // Update or add the current client timestamp + state.clients[this.clientId] = this.windowStartTimestamp + + await this.features.workspace.fs.writeFile(this.stateFilePath, JSON.stringify(state), { mode: 0o600 }) + } catch (error) { + this.features.logging.warn(`Error persisting active user state: ${error}`) + } + } +} diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts index b845b216f3..2c20e1f21c 100644 --- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts @@ -255,14 +255,19 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase { // add error check if (this.customizationArn) request.customizationArn = this.customizationArn const response = await this.client.generateCompletions(this.withProfileArn(request)).promise() - this.logging.info(`GenerateCompletion response: + this.logging.info( + `GenerateCompletion response: "requestId": ${response.$response.requestId}, "responseCompletionCount": ${response.completions?.length ?? 0}, "responsePredictionCount": ${response.predictions?.length ?? 0}, "suggestionType": ${request.predictionTypes?.toString() ?? ''}, "filename": ${request.fileContext.filename}, "language": ${request.fileContext.programmingLanguage.languageName}, - "supplementalContextLength": ${request.supplementalContexts?.length ?? 0}`) + "supplementalContextLength": ${request.supplementalContexts?.length ?? 0}, + "request.nextToken": ${request.nextToken}, + "response.nextToken": ${response.nextToken}` + ) + const responseContext = { requestId: response?.$response?.requestId, codewhispererSessionId: response?.$response?.httpResponse?.headers['x-amzn-sessionid'], diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts index 4e78a39fc1..14df60adb9 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.test.ts @@ -276,6 +276,7 @@ describe('TelemetryService', () => { deletedCharacterCount: undefined, addedIdeDiagnostics: undefined, removedIdeDiagnostics: undefined, + streakLength: 0, }, }, optOutPreference: 'OPTIN', diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts index 6d7be5ca4d..6cc0b4fdfe 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/telemetryService.ts @@ -271,8 +271,7 @@ export class TelemetryService { deletedCharacterCount: deletedCharacterCount, addedIdeDiagnostics: addedIdeDiagnostics, removedIdeDiagnostics: removedIdeDiagnostics, - // TODO add streakLength back once the model is updated - // streakLength: streakLength, + streakLength: streakLength ?? 0, } return this.invokeSendTelemetryEvent({ userTriggerDecisionEvent: event, diff --git a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts index 0296a2596e..16dccbedff 100644 --- a/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts +++ b/server/aws-lsp-codewhisperer/src/shared/telemetry/types.ts @@ -208,6 +208,7 @@ export enum ChatTelemetryEventName { ChatHistoryAction = 'amazonq_performChatHistoryAction', ExportTab = 'amazonq_exportTab', UiClick = 'ui_click', + ActiveUser = 'amazonq_activeUser', } export interface ChatTelemetryEventMap { @@ -230,6 +231,7 @@ export interface ChatTelemetryEventMap { [ChatTelemetryEventName.ChatHistoryAction]: ChatHistoryActionEvent [ChatTelemetryEventName.ExportTab]: ExportTabEvent [ChatTelemetryEventName.UiClick]: UiClickEvent + [ChatTelemetryEventName.ActiveUser]: ActiveUserEvent } export type AgencticLoop_InvokeLLMEvent = { @@ -262,6 +264,11 @@ export type InteractWithAgenticChatEvent = { enabled?: boolean } +export type ActiveUserEvent = { + credentialStartUrl?: string + result: string +} + export type ModifyCodeEvent = { cwsprChatConversationId: string cwsprChatMessageId: string diff --git a/server/aws-lsp-identity/package.json b/server/aws-lsp-identity/package.json index ac659983d9..1692e811af 100644 --- a/server/aws-lsp-identity/package.json +++ b/server/aws-lsp-identity/package.json @@ -26,8 +26,8 @@ "dependencies": { "@aws-sdk/client-sso-oidc": "^3.616.0", "@aws-sdk/token-providers": "^3.744.0", - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "@smithy/node-http-handler": "^3.2.5", "@smithy/shared-ini-file-loader": "^4.0.1", "https-proxy-agent": "^7.0.5", diff --git a/server/aws-lsp-identity/src/language-server/profiles/profileService.test.ts b/server/aws-lsp-identity/src/language-server/profiles/profileService.test.ts index f2da8c62e9..216f3010af 100644 --- a/server/aws-lsp-identity/src/language-server/profiles/profileService.test.ts +++ b/server/aws-lsp-identity/src/language-server/profiles/profileService.test.ts @@ -23,6 +23,7 @@ let observability: StubbedInstance let profile1: Profile let profile2: Profile let profile3: Profile +let profile4: Profile let ssoSession1: SsoSession let ssoSession2: SsoSession @@ -52,6 +53,16 @@ describe('ProfileService', async () => { }, } + profile4 = { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'profile4', + settings: { + aws_access_key_id: 'access-key', + aws_secret_access_key: 'secret-key', + aws_session_token: 'session-token', + }, + } + ssoSession1 = { name: 'ssoSession1', settings: { @@ -71,7 +82,7 @@ describe('ProfileService', async () => { store = stubInterface({ load: Promise.resolve({ - profiles: [profile1, profile2, profile3], + profiles: [profile1, profile2, profile3, profile4], ssoSessions: [ssoSession1, ssoSession2], } satisfies ProfileData), save: Promise.resolve(), @@ -87,7 +98,7 @@ describe('ProfileService', async () => { it('listProfiles return profiles and sso-sessions', async () => { const actual = await sut.listProfiles({}) - expect(actual.profiles).to.be.an('array').that.has.deep.members([profile1, profile2, profile3]) + expect(actual.profiles).to.be.an('array').that.has.deep.members([profile1, profile2, profile3, profile4]) expect(actual.ssoSessions).to.be.an('array').that.has.deep.members([ssoSession1, ssoSession2]) }) @@ -198,23 +209,6 @@ describe('ProfileService', async () => { expectAwsError(sut, { profile: undefined! }, AwsErrorCodes.E_INVALID_PROFILE, 'Profile required.') }) - it('updateProfile throws on non-SSO token profile', async () => { - const profile = { - kinds: [ProfileKind.Unknown], - name: 'profile-name', - settings: { - sso_session: 'sso-session-name', - }, - } - - await expectAwsError( - sut, - { profile }, - AwsErrorCodes.E_INVALID_PROFILE, - 'Profile must be non-legacy sso-session type.' - ) - }) - it('updateProfile throws on no profile name', async () => { const profile = { kinds: [ProfileKind.SsoTokenProfile], @@ -253,7 +247,7 @@ describe('ProfileService', async () => { await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Sso-session name required on profile.') }) - it('updateProfile throws on no sso-session on profile', async () => { + it('updateProfile throws on no sso-session on SSO token profile', async () => { const profile = { kinds: [ProfileKind.SsoTokenProfile], name: 'profile-name', @@ -265,6 +259,100 @@ describe('ProfileService', async () => { await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Sso-session name required on profile.') }) + it('updateProfile throws on missing access key for IamCredentialsProfile', async () => { + const profile = { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'profile-name', + settings: { + aws_secret_access_key: 'secret-key', + }, + } + + await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Access key required on profile.') + }) + + it('updateProfile throws on missing secret key for IamCredentialsProfile', async () => { + const profile = { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'profile-name', + settings: { + aws_access_key_id: 'access-key', + }, + } + + await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Secret key required on profile.') + }) + + it('updateProfile throws on missing role ARN for IamSourceProfileProfile', async () => { + const profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'profile-name', + settings: { + source_profile: 'source', + }, + } + + await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Role ARN required on profile.') + }) + + it('updateProfile throws on missing source profile for IamSourceProfileProfile', async () => { + const profile = { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'profile-name', + settings: { + role_arn: 'role-arn', + }, + } + + await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Source profile required on profile.') + }) + + it('updateProfile throws on missing role ARN for IamCredentialSourceProfile', async () => { + const profile = { + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'profile-name', + settings: { + credential_source: 'Ec2InstanceMetadata', + region: 'region', + }, + } + + await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Role ARN required on profile.') + }) + + it('updateProfile throws on missing credential source for IamCredentialSourceProfile', async () => { + const profile = { + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'profile-name', + settings: { + role_arn: 'role-arn', + region: 'region', + }, + } + + await expectAwsError( + sut, + { profile }, + AwsErrorCodes.E_INVALID_PROFILE, + 'Credential source required on profile.' + ) + }) + + it('updateProfile throws on missing credential process for process profile', async () => { + const profile = { + kinds: [ProfileKind.IamCredentialProcessProfile], + name: 'profile-name', + settings: {}, + } + + await expectAwsError( + sut, + { profile }, + AwsErrorCodes.E_INVALID_PROFILE, + 'Credential process required on profile.' + ) + }) + it('updateProfile throws when profile cannot be created', async () => { const profile = { kinds: [ProfileKind.SsoTokenProfile], @@ -411,7 +499,7 @@ describe('ProfileService', async () => { }) describe('profileService.DuckTypers', () => { - it('profileDuckTypers.eval returns true on valid profiles', () => { + it('profileDuckTypers.SsoTokenProfile.eval returns true on valid profiles', () => { const profiles = [ { sso_session: 'my-sso-session', @@ -428,7 +516,7 @@ describe('profileService.DuckTypers', () => { } }) - it('profileDuckTypers returns false on invalid profiles', () => { + it('profileDuckTypers.SsoTokenProfile.eval returns false on invalid profiles', () => { const profiles = [ { SSO_session: 'my-sso-session', @@ -484,6 +572,102 @@ describe('profileService.DuckTypers', () => { expect(actual).to.be.false } }) + + it('profileDuckTypers.IamCredentialsProfile.eval returns true on valid profiles', () => { + const profiles = [ + { + aws_access_key_id: 'access-key', + aws_secret_access_key: 'secret-key', + }, + { + aws_access_key_id: 'access-key', + aws_secret_access_key: 'secret-key', + aws_session_token: 'session-token', + }, + ] + + for (const profile of profiles) { + const actual = profileDuckTypers.IamCredentialsProfile.eval(profile) + expect(actual).to.be.true + } + }) + + it('profileDuckTypers.IamCredentialsProfile.eval returns false on invalid profiles', () => { + const profiles = [ + { + sso_session: 'my-sso-session', + }, + null, + { + sso_account_id: '123', + }, + ] + + for (const profile of profiles) { + const actual = profileDuckTypers.IamCredentialsProfile.eval(profile as object) + expect(actual).to.be.false + } + }) + + it('profileDuckTypers.IamSourceProfileProfile.eval returns true on valid profiles', () => { + const profiles = [ + { + role_arn: 'role-arn', + source_profile: 'source-profile', + }, + { + role_arn: 'role-arn', + source_profile: 'source-profile', + role_session_name: 'role-session-name', + mfa_serial: 'mfa-serial', + }, + ] + + for (const profile of profiles) { + const actual = profileDuckTypers.IamSourceProfileProfile.eval(profile) + expect(actual).to.be.true + } + }) + + it('profileDuckTypers.IamCredentialSourceProfile.eval returns true on valid profiles', () => { + const profiles = [ + { + role_arn: 'role-arn', + credential_source: 'credential-source', + region: 'region', + }, + { + role_arn: 'role-arn', + credential_source: 'credential-source', + region: 'region', + role_session_name: 'role-session-name', + }, + ] + + for (const profile of profiles) { + const actual = profileDuckTypers.IamCredentialSourceProfile.eval(profile) + expect(actual).to.be.true + } + }) + + it('profileDuckTypers.IamCredentialProcessProfile.eval returns true on valid profiles', () => { + const profiles = [ + { + credential_process: 'credential-process', + }, + { + aws_access_key_id: 'access-key', + aws_secret_access_key: 'secret-key', + aws_session_token: 'session-token', + credential_process: 'credential-process', + }, + ] + + for (const profile of profiles) { + const actual = profileDuckTypers.IamCredentialProcessProfile.eval(profile) + expect(actual).to.be.true + } + }) }) describe('profileService.functions', () => { diff --git a/server/aws-lsp-identity/src/language-server/profiles/profileService.ts b/server/aws-lsp-identity/src/language-server/profiles/profileService.ts index 247ccd1833..a37e27e30e 100644 --- a/server/aws-lsp-identity/src/language-server/profiles/profileService.ts +++ b/server/aws-lsp-identity/src/language-server/profiles/profileService.ts @@ -30,6 +30,18 @@ export const ProfileFields = { sso_account_id: 'sso_account_id', sso_role_name: 'sso_role_name', sso_session: 'sso_session', + aws_access_key_id: 'aws_access_key_id', + aws_secret_access_key: 'aws_secret_access_key', + aws_session_token: 'aws_session_token', + role_arn: 'role_arn', + role_session_name: 'role_session_name', + credential_process: 'credential_process', + credential_source: 'credential_source', + source_profile: 'source_profile', + mfa_serial: 'mfa_serial', + external_id: 'external_id', + credential_cache: 'credential_cache', + credential_cache_location: 'credential_cache_location', } as const export const SsoSessionFields = { @@ -38,12 +50,70 @@ export const SsoSessionFields = { sso_start_url: 'sso_start_url', } as const -export const profileDuckTypers = { - SsoTokenProfile: new DuckTyper() - .requireProperty(ProfileFields.sso_session) - .disallowProperty(ProfileFields.sso_account_id) - .disallowProperty(ProfileFields.sso_role_name), -} +export const profileTypes = { + SsoTokenProfile: { + kind: ProfileKind.SsoTokenProfile, + required: [ProfileFields.sso_session], + optional: [ProfileFields.region], + disallowed: [ProfileFields.sso_account_id, ProfileFields.sso_role_name], + }, + IamCredentialsProfile: { + kind: ProfileKind.IamCredentialsProfile, + required: [ProfileFields.aws_access_key_id, ProfileFields.aws_secret_access_key], + optional: [ProfileFields.aws_session_token], + disallowed: [], + }, + IamSourceProfileProfile: { + kind: ProfileKind.IamSourceProfileProfile, + required: [ProfileFields.role_arn, ProfileFields.source_profile], + optional: [ + ProfileFields.external_id, + ProfileFields.role_session_name, + ProfileFields.region, + ProfileFields.mfa_serial, + ProfileFields.credential_cache, + ProfileFields.credential_cache_location, + ], + disallowed: [ProfileFields.credential_source], + }, + IamCredentialSourceProfile: { + kind: ProfileKind.IamCredentialSourceProfile, + required: [ProfileFields.role_arn, ProfileFields.credential_source], + optional: [ + ProfileFields.external_id, + ProfileFields.role_session_name, + ProfileFields.region, + ProfileFields.credential_cache, + ProfileFields.credential_cache_location, + ], + disallowed: [ProfileFields.source_profile], + }, + IamCredentialProcessProfile: { + kind: ProfileKind.IamCredentialProcessProfile, + required: [ProfileFields.credential_process], + optional: [], + disallowed: [], + }, +} as const + +export const profileDuckTypers = Object.fromEntries( + Object.entries(profileTypes).map(([key, def]) => [ + key, + (() => { + const typer = new DuckTyper() + for (const field of def.required) { + typer.requireProperty(field) + } + for (const field of def.optional) { + typer.optionalProperty(field) + } + for (const field of def.disallowed) { + typer.disallowProperty(field) + } + return typer + })(), + ]) +) export const ssoSessionDuckTyper = new DuckTyper() .requireProperty(SsoSessionFields.sso_start_url) @@ -94,53 +164,78 @@ export class ProfileService { const profile = params.profile! this.throwOnInvalidProfile( - !profile.kinds.includes(ProfileKind.SsoTokenProfile), - 'Profile must be non-legacy sso-session type.' + !profile.kinds.some(kind => Object.values(ProfileKind).includes(kind)), + 'Profile must be non-legacy sso-session or iam-credentials type.' ) this.throwOnInvalidProfile(!profile.name, 'Profile name required.') this.throwOnInvalidProfile(!profile.settings, 'Settings required on profile.') const profileSettings = profile.settings! - this.throwOnInvalidProfile(!profileSettings.sso_session, 'Sso-session name required on profile.') - - // Validate sso-session - this.throwOnInvalidSsoSession(!params.ssoSession, 'Sso-session required.') - const ssoSession: SsoSession = params.ssoSession! - - this.throwOnInvalidSsoSession(!ssoSession.name, 'Sso-session name required.') - this.throwOnInvalidSsoSession(!ssoSession.settings, 'Settings required on sso-session.') - const ssoSessionSettings = ssoSession.settings! - - this.throwOnInvalidSsoSession(!ssoSessionSettings.sso_region, 'Sso-session region required.') - this.throwOnInvalidSsoSession(!ssoSessionSettings.sso_start_url, 'Sso-session start URL required.') - - this.throwOnInvalidProfile( - profileSettings.sso_session !== ssoSession.name, - 'Profile sso-session name must be the same as provided sso-session.' - ) - + // Get profiles and SSO sessions to check whether a duplicate will be created const { profiles, ssoSessions } = await this.profileStore.load().catch(reason => { throw AwsError.wrap(reason, AwsErrorCodes.E_CANNOT_READ_SHARED_CONFIG) }) - // Enforce options + // Check if the profile can be created if (!options.createNonexistentProfile && !profiles.some(p => p.name === profile.name)) { this.observability.logging.log(`Cannot create profile. options: ${JSON.stringify(options)}`) throw new AwsError('Cannot create profile.', AwsErrorCodes.E_CANNOT_CREATE_PROFILE) } - if (!options.createNonexistentSsoSession && !ssoSessions.some(s => s.name === ssoSession.name)) { - this.observability.logging.log(`Cannot create sso-session. options: ${JSON.stringify(options)}`) - throw new AwsError('Cannot create sso-session.', AwsErrorCodes.E_CANNOT_CREATE_SSO_SESSION) + // TODO: can this be refactored and simplified using the existing DuckTypers? + // Validate SSO profile + if (profile.kinds.includes(ProfileKind.SsoTokenProfile)) { + this.throwOnInvalidProfile(!profileSettings.sso_session, 'Sso-session name required on profile.') + this.throwOnInvalidSsoSession(!params.ssoSession, 'Sso-session required.') + const ssoSession: SsoSession = params.ssoSession! + + this.throwOnInvalidSsoSession(!ssoSession.name, 'Sso-session name required.') + this.throwOnInvalidSsoSession(!ssoSession.settings, 'Settings required on sso-session.') + const ssoSessionSettings = ssoSession.settings! + + this.throwOnInvalidSsoSession(!ssoSessionSettings.sso_region, 'Sso-session region required.') + this.throwOnInvalidSsoSession(!ssoSessionSettings.sso_start_url, 'Sso-session start URL required.') + + this.throwOnInvalidProfile( + profileSettings.sso_session !== ssoSession.name, + 'Profile sso-session name must be the same as provided sso-session.' + ) + + // Check if the SSO session can be created + if (!options.createNonexistentSsoSession && !ssoSessions.some(s => s.name === ssoSession.name)) { + this.observability.logging.log(`Cannot create sso-session. options: ${JSON.stringify(options)}`) + throw new AwsError('Cannot create sso-session.', AwsErrorCodes.E_CANNOT_CREATE_SSO_SESSION) + } + + // Check if the SSO session can be updated + if ( + !options.updateSharedSsoSession && + this.isSharedSsoSession(ssoSession.name, profiles, profile.name) && + this.willUpdateExistingSsoSession(ssoSession, ssoSessions) + ) { + this.observability.logging.log(`Cannot update shared sso-session. options: ${JSON.stringify(options)}`) + throw new AwsError('Cannot update shared sso-session.', AwsErrorCodes.E_CANNOT_OVERWRITE_SSO_SESSION) + } + } + + // Validate IAM profiles + if (profile.kinds.includes(ProfileKind.IamCredentialsProfile)) { + this.throwOnInvalidProfile(!profileSettings.aws_access_key_id, 'Access key required on profile.') + this.throwOnInvalidProfile(!profileSettings.aws_secret_access_key, 'Secret key required on profile.') + } + + if (profile.kinds.includes(ProfileKind.IamCredentialSourceProfile)) { + this.throwOnInvalidProfile(!profileSettings.role_arn, 'Role ARN required on profile.') + this.throwOnInvalidProfile(!profileSettings.credential_source, 'Credential source required on profile.') + } + + if (profile.kinds.includes(ProfileKind.IamSourceProfileProfile)) { + this.throwOnInvalidProfile(!profileSettings.role_arn, 'Role ARN required on profile.') + this.throwOnInvalidProfile(!profileSettings.source_profile, 'Source profile required on profile.') } - if ( - !options.updateSharedSsoSession && - this.isSharedSsoSession(ssoSession.name, profiles, profile.name) && - this.willUpdateExistingSsoSession(ssoSession, ssoSessions) - ) { - this.observability.logging.log(`Cannot update shared sso-session. options: ${JSON.stringify(options)}`) - throw new AwsError('Cannot update shared sso-session.', AwsErrorCodes.E_CANNOT_OVERWRITE_SSO_SESSION) + if (profile.kinds.includes(ProfileKind.IamCredentialProcessProfile)) { + this.throwOnInvalidProfile(!profileSettings.credential_process, 'Credential process required on profile.') } await this.profileStore diff --git a/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.test.ts b/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.test.ts index c6d938e53c..fa0e85bb72 100644 --- a/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.test.ts +++ b/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.test.ts @@ -81,7 +81,7 @@ describe('SharedConfigProfileStore', async () => { mock.restore() }) - it('loads SSO token profiles and sso-sessions, but not services', async () => { + it('loads profiles and sso-sessions', async () => { setupTest(config, credentials) const actual = await sut.load() @@ -89,20 +89,17 @@ describe('SharedConfigProfileStore', async () => { expect(actual).to.deep.equal({ profiles: [ { - kinds: [ProfileKind.Unknown], + kinds: [ProfileKind.IamCredentialsProfile], name: 'default', settings: { - region: 'us-west-2', - sso_session: undefined, + aws_access_key_id: 'AAAAAAAA', + aws_secret_access_key: 'BBBBBBBB', }, }, { kinds: [ProfileKind.Unknown], name: 'subsettings', - settings: { - region: undefined, - sso_session: undefined, - }, + settings: {}, }, { kinds: [ProfileKind.SsoTokenProfile], @@ -183,26 +180,22 @@ describe('SharedConfigProfileStore', async () => { expect(after).to.deep.equal({ profiles: [ { - kinds: ['Unknown'], + kinds: [ProfileKind.IamCredentialsProfile], name: 'default', settings: { - region: 'us-west-2', - sso_session: undefined, + aws_access_key_id: 'AAAAAAAA', + aws_secret_access_key: 'BBBBBBBB', }, }, { - kinds: ['Unknown'], + kinds: [ProfileKind.Unknown], name: 'subsettings', - settings: { - region: undefined, - sso_session: undefined, - }, + settings: {}, }, { kinds: [ProfileKind.SsoTokenProfile], name: 'config-only.profile', settings: { - region: undefined, sso_session: 'test-sso-session', }, }, @@ -283,20 +276,17 @@ describe('SharedConfigProfileStore', async () => { expect(after).to.deep.equal({ profiles: [ { - kinds: ['Unknown'], + kinds: [ProfileKind.IamCredentialsProfile], name: 'default', settings: { - region: 'us-west-2', - sso_session: undefined, + aws_access_key_id: 'AAAAAAAA', + aws_secret_access_key: 'BBBBBBBB', }, }, { - kinds: ['Unknown'], + kinds: [ProfileKind.Unknown], name: 'subsettings', - settings: { - region: undefined, - sso_session: undefined, - }, + settings: {}, }, { kinds: ['SsoTokenProfile'], @@ -325,6 +315,38 @@ describe('SharedConfigProfileStore', async () => { sso_session: 'new-sso-session', }, }, + { + kinds: [ProfileKind.IamCredentialsProfile], + name: 'iam-user.profile', + settings: { + aws_access_key_id: 'new-access-key', + aws_secret_access_key: 'new-secret-key', + aws_session_token: 'new-session-token', + }, + }, + { + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'role-source.profile', + settings: { + role_arn: 'new-role-arn', + source_profile: 'new-source-profile', + }, + }, + { + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'role-instance.profile', + settings: { + role_arn: 'new-role-arn', + credential_source: 'new-source', + }, + }, + { + kinds: [ProfileKind.IamCredentialProcessProfile], + name: 'process.profile', + settings: { + credential_process: 'new-credential-process', + }, + }, ], ssoSessions: [ { @@ -348,51 +370,74 @@ describe('SharedConfigProfileStore', async () => { await sut.save(data) const after = await sut.load() + after.profiles.sort((a, b) => a.name.localeCompare(b.name)) + after.ssoSessions.sort((a, b) => a.name.localeCompare(b.name)) expect(after).to.deep.equal({ profiles: [ { - kinds: ['Unknown'], + kinds: [ProfileKind.SsoTokenProfile], + name: 'config-only.profile', + settings: { + region: 'us-west-1', + sso_session: 'new-sso-session', + }, + }, + { + kinds: [ProfileKind.SsoTokenProfile], + name: 'credentials-only.profile', + settings: { + region: 'us-east-1', + sso_session: 'test-sso-session', + }, + }, + { + kinds: [ProfileKind.IamCredentialsProfile], name: 'default', settings: { - region: 'us-west-2', - sso_session: undefined, + aws_access_key_id: 'AAAAAAAA', + aws_secret_access_key: 'BBBBBBBB', }, }, { - kinds: ['Unknown'], - name: 'subsettings', + kinds: [ProfileKind.IamCredentialsProfile], + name: 'iam-user.profile', settings: { - region: undefined, - sso_session: undefined, + aws_access_key_id: 'new-access-key', + aws_secret_access_key: 'new-secret-key', + aws_session_token: 'new-session-token', }, }, { - kinds: [ProfileKind.SsoTokenProfile], - name: 'config-only.profile', + kinds: [ProfileKind.IamCredentialProcessProfile], + name: 'process.profile', settings: { - region: 'us-west-1', - sso_session: 'new-sso-session', + credential_process: 'new-credential-process', }, }, { - kinds: [ProfileKind.SsoTokenProfile], - name: 'credentials-only.profile', + kinds: [ProfileKind.IamCredentialSourceProfile], + name: 'role-instance.profile', settings: { - region: 'us-east-1', - sso_session: 'test-sso-session', + role_arn: 'new-role-arn', + credential_source: 'new-source', }, }, - ], - ssoSessions: [ { - name: 'test-sso-session', + kinds: [ProfileKind.IamSourceProfileProfile], + name: 'role-source.profile', settings: { - sso_region: 'us-east-1', - sso_registration_scopes: ['my-scope'], - sso_start_url: 'http://newnowhere', + role_arn: 'new-role-arn', + source_profile: 'new-source-profile', }, }, + { + kinds: [ProfileKind.Unknown], + name: 'subsettings', + settings: {}, + }, + ], + ssoSessions: [ { name: 'new-sso-session', settings: { @@ -400,6 +445,14 @@ describe('SharedConfigProfileStore', async () => { sso_start_url: 'http://somewhere', }, }, + { + name: 'test-sso-session', + settings: { + sso_region: 'us-east-1', + sso_registration_scopes: ['my-scope'], + sso_start_url: 'http://newnowhere', + }, + }, ], }) }) diff --git a/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.ts b/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.ts index 2edbe2bb0f..0b612a46a0 100644 --- a/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.ts +++ b/server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.ts @@ -3,11 +3,12 @@ import { ProfileData, profileDuckTypers, ProfileStore, + profileTypes, ssoSessionDuckTyper, } from './profileService' import { parseKnownFiles, SharedConfigInit } from '@smithy/shared-ini-file-loader' import { IniSection, IniSectionType, ParsedIniData } from '@smithy/types' -import { AwsErrorCodes, ProfileKind, SsoSession } from '@aws/language-server-runtimes/server-interface' +import { AwsErrorCodes, Profile, ProfileKind, SsoSession } from '@aws/language-server-runtimes/server-interface' import { SectionHeader } from '../../sharedConfig/types' import { saveKnownFiles } from '../../sharedConfig' import { normalizeParsedIniData } from '../../sharedConfig/saveKnownFiles' @@ -44,22 +45,33 @@ export class SharedConfigProfileStore implements ProfileStore { for (const [parsedSectionName, settings] of Object.entries(parsedIni)) { const sectionHeader = SectionHeader.fromParsedSectionName(parsedSectionName) switch (sectionHeader.type) { - case IniSectionType.PROFILE: - result.profiles.push({ - kinds: [ - // As more profile kinds are added this will get more complex and need refactored - profileDuckTypers.SsoTokenProfile.eval(settings) - ? ProfileKind.SsoTokenProfile - : ProfileKind.Unknown, - ], + // Convert config file profile into profile object + case IniSectionType.PROFILE: { + const profile: Profile = { + kinds: [], name: sectionHeader.name, - settings: { - // Only apply settings expected on Profile - region: settings.region, - sso_session: settings.sso_session, - }, - }) + settings: {}, + } + // Add the kinds and settings for each matched profile type + for (const [profileType, fields] of Object.entries(profileTypes)) { + if (profileDuckTypers[profileType].eval(settings)) { + profile.kinds.push(fields.kind) + const relevantFields = [...fields.required, ...fields.optional] + for (const field of relevantFields) { + if (settings[field] !== undefined) { + profile.settings![field] = settings[field] + } + } + } + } + // If the profile does not match any profile type, mark it as an unknown profile + if (profile.kinds.length === 0) { + profile.kinds.push(ProfileKind.Unknown) + } + result.profiles.push(profile) break + } + // Convert config file SSO session into SSO session object case IniSectionType.SSO_SESSION: { if (!ssoSessionDuckTyper.eval(settings)) { continue @@ -91,9 +103,9 @@ export class SharedConfigProfileStore implements ProfileStore { return result } - // If a setting is set to undefined or null, it will be removed from shared config files - // If the settings property is set to undefined or null, the entire section will be removed - // from the shared config files. This is equivalent to deleting a section. + // If a setting is set to undefined, null, or an empty string, it will be removed from shared + // config files. If the settings property is set to undefined or null, the entire section will + // be removed from the shared config files. This is equivalent to deleting a section. // Any settings or sections in the shared config files that are not passed into data will // be preserved as-is. async save(data: ProfileData, init?: SharedConfigInit): Promise { @@ -114,9 +126,12 @@ export class SharedConfigProfileStore implements ProfileStore { IniSectionType.PROFILE, data.profiles, parsedKnownFiles, - (section, parsedSection) => - !section.kinds.includes(ProfileKind.SsoTokenProfile) || - profileDuckTypers.SsoTokenProfile.eval(parsedSection) + (section, parsedSection) => { + return section.kinds.every(kind => { + const duckTyper = profileDuckTypers[kind] + return duckTyper ? duckTyper.eval(parsedSection) : true + }) + } ) } @@ -192,7 +207,9 @@ export class SharedConfigProfileStore implements ProfileStore { // If setting not passed then preserve setting in file as-is value = value?.toString().trim() if (value === undefined || value === null || value === '') { - Object.hasOwn(parsedSection, name) && delete parsedSection[name] + if (Object.hasOwn(parsedSection, name)) { + delete parsedSection[name] + } } else { if (controlCharsRegex.test(value)) { throwAwsError(`Setting [${name}] cannot contain control characters.`) diff --git a/server/aws-lsp-json/CHANGELOG.md b/server/aws-lsp-json/CHANGELOG.md index f4d847a6f4..c16adea0ff 100644 --- a/server/aws-lsp-json/CHANGELOG.md +++ b/server/aws-lsp-json/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.16](https://github.com/aws/language-servers/compare/lsp-json/v0.1.15...lsp-json/v0.1.16) (2025-07-17) + + +### Bug Fixes + +* use document change events for auto trigger classifier input ([#1912](https://github.com/aws/language-servers/issues/1912)) ([2204da6](https://github.com/aws/language-servers/commit/2204da6193f2030ee546f61c969b1a664d8025e3)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.11 to ^0.0.12 + ## [0.1.15](https://github.com/aws/language-servers/compare/lsp-json/v0.1.14...lsp-json/v0.1.15) (2025-07-02) diff --git a/server/aws-lsp-json/package.json b/server/aws-lsp-json/package.json index 17c0bed834..c3b9e4ab1e 100644 --- a/server/aws-lsp-json/package.json +++ b/server/aws-lsp-json/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-json", - "version": "0.1.15", + "version": "0.1.16", "description": "JSON Language Server", "main": "out/index.js", "repository": { @@ -26,8 +26,8 @@ "prepack": "shx cp ../../LICENSE ../../NOTICE ../../SECURITY.md ." }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "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 669961a43c..6cd283939e 100644 --- a/server/aws-lsp-notification/package.json +++ b/server/aws-lsp-notification/package.json @@ -22,8 +22,8 @@ "coverage:report": "c8 report --reporter=html --reporter=text" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/aws-lsp-partiql/CHANGELOG.md b/server/aws-lsp-partiql/CHANGELOG.md index d3de47d50a..8b9af69338 100644 --- a/server/aws-lsp-partiql/CHANGELOG.md +++ b/server/aws-lsp-partiql/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.15](https://github.com/aws/language-servers/compare/lsp-partiql/v0.0.14...lsp-partiql/v0.0.15) (2025-07-17) + + +### Bug Fixes + +* use document change events for auto trigger classifier input ([#1912](https://github.com/aws/language-servers/issues/1912)) ([2204da6](https://github.com/aws/language-servers/commit/2204da6193f2030ee546f61c969b1a664d8025e3)) + ## [0.0.14](https://github.com/aws/language-servers/compare/lsp-partiql/v0.0.13...lsp-partiql/v0.0.14) (2025-06-26) diff --git a/server/aws-lsp-partiql/package.json b/server/aws-lsp-partiql/package.json index 029b09c336..9e37ff2808 100644 --- a/server/aws-lsp-partiql/package.json +++ b/server/aws-lsp-partiql/package.json @@ -3,7 +3,7 @@ "author": "Amazon Web Services", "license": "Apache-2.0", "description": "PartiQL language server", - "version": "0.0.14", + "version": "0.0.15", "repository": { "type": "git", "url": "https://github.com/aws/language-servers" @@ -24,7 +24,7 @@ "out" ], "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", + "@aws/language-server-runtimes": "^0.2.112", "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 3771c78c30..0c9059d62e 100644 --- a/server/aws-lsp-s3/package.json +++ b/server/aws-lsp-s3/package.json @@ -9,7 +9,8 @@ "dependencies": { "@aws-sdk/client-s3": "^3.623.0", "@aws-sdk/types": "^3.734.0", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8" } diff --git a/server/aws-lsp-yaml/CHANGELOG.md b/server/aws-lsp-yaml/CHANGELOG.md index f26d08952e..8e58c69c67 100644 --- a/server/aws-lsp-yaml/CHANGELOG.md +++ b/server/aws-lsp-yaml/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.1.16](https://github.com/aws/language-servers/compare/lsp-yaml/v0.1.15...lsp-yaml/v0.1.16) (2025-07-17) + + +### Bug Fixes + +* use document change events for auto trigger classifier input ([#1912](https://github.com/aws/language-servers/issues/1912)) ([2204da6](https://github.com/aws/language-servers/commit/2204da6193f2030ee546f61c969b1a664d8025e3)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @aws/lsp-core bumped from ^0.0.11 to ^0.0.12 + ## [0.1.15](https://github.com/aws/language-servers/compare/lsp-yaml/v0.1.14...lsp-yaml/v0.1.15) (2025-07-02) diff --git a/server/aws-lsp-yaml/package.json b/server/aws-lsp-yaml/package.json index 3d73f0af55..cb0e62fa65 100644 --- a/server/aws-lsp-yaml/package.json +++ b/server/aws-lsp-yaml/package.json @@ -1,6 +1,6 @@ { "name": "@aws/lsp-yaml", - "version": "0.1.15", + "version": "0.1.16", "description": "YAML Language Server", "main": "out/index.js", "repository": { @@ -26,8 +26,8 @@ "postinstall": "node patchYamlPackage.js" }, "dependencies": { - "@aws/language-server-runtimes": "^0.2.105", - "@aws/lsp-core": "^0.0.11", + "@aws/language-server-runtimes": "^0.2.112", + "@aws/lsp-core": "^0.0.12", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.8", "yaml-language-server": "1.13.0" diff --git a/server/device-sso-auth-lsp/package.json b/server/device-sso-auth-lsp/package.json index de087a3dc1..71a0a8bbda 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.105", + "@aws/language-server-runtimes": "^0.2.112", "vscode-languageserver": "^9.0.1" }, "devDependencies": { diff --git a/server/hello-world-lsp/package.json b/server/hello-world-lsp/package.json index 87d47f3eb1..2837bb8302 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.105", + "@aws/language-server-runtimes": "^0.2.112", "vscode-languageserver": "^9.0.1" }, "devDependencies": {