@aws/lsp-codewhisperer 0.0.65 → 0.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/out/language-server/agenticChat/agenticChatController.d.ts +7 -0
  3. package/out/language-server/agenticChat/agenticChatController.js +415 -114
  4. package/out/language-server/agenticChat/agenticChatController.js.map +1 -1
  5. package/out/language-server/agenticChat/constants/constants.d.ts +12 -0
  6. package/out/language-server/agenticChat/constants/constants.js +73 -1
  7. package/out/language-server/agenticChat/constants/constants.js.map +1 -1
  8. package/out/language-server/agenticChat/constants/toolConstants.d.ts +24 -0
  9. package/out/language-server/agenticChat/constants/toolConstants.js +35 -0
  10. package/out/language-server/agenticChat/constants/toolConstants.js.map +1 -0
  11. package/out/language-server/agenticChat/context/additionalContextProvider.js +37 -2
  12. package/out/language-server/agenticChat/context/additionalContextProvider.js.map +1 -1
  13. package/out/language-server/agenticChat/context/agenticChatTriggerContext.d.ts +11 -2
  14. package/out/language-server/agenticChat/context/agenticChatTriggerContext.js +32 -2
  15. package/out/language-server/agenticChat/context/agenticChatTriggerContext.js.map +1 -1
  16. package/out/language-server/agenticChat/context/contextCommandsProvider.js +1 -1
  17. package/out/language-server/agenticChat/context/contextCommandsProvider.js.map +1 -1
  18. package/out/language-server/agenticChat/context/contextUtils.d.ts +16 -0
  19. package/out/language-server/agenticChat/context/contextUtils.js +29 -0
  20. package/out/language-server/agenticChat/context/contextUtils.js.map +1 -1
  21. package/out/language-server/agenticChat/qAgenticChatServer.js +1 -1
  22. package/out/language-server/agenticChat/qAgenticChatServer.js.map +1 -1
  23. package/out/language-server/agenticChat/tools/chatDb/chatDb.d.ts +11 -3
  24. package/out/language-server/agenticChat/tools/chatDb/chatDb.js +103 -31
  25. package/out/language-server/agenticChat/tools/chatDb/chatDb.js.map +1 -1
  26. package/out/language-server/agenticChat/tools/chatDb/util.d.ts +4 -0
  27. package/out/language-server/agenticChat/tools/chatDb/util.js.map +1 -1
  28. package/out/language-server/agenticChat/tools/executeBash.d.ts +0 -6
  29. package/out/language-server/agenticChat/tools/executeBash.js +5 -21
  30. package/out/language-server/agenticChat/tools/executeBash.js.map +1 -1
  31. package/out/language-server/chat/chatSessionService.d.ts +1 -0
  32. package/out/language-server/chat/chatSessionService.js +5 -2
  33. package/out/language-server/chat/chatSessionService.js.map +1 -1
  34. package/out/language-server/chat/quickActions.d.ts +7 -1
  35. package/out/language-server/chat/quickActions.js +7 -1
  36. package/out/language-server/chat/quickActions.js.map +1 -1
  37. package/out/language-server/chat/telemetry/chatTelemetryController.d.ts +1 -0
  38. package/out/language-server/chat/telemetry/chatTelemetryController.js +9 -0
  39. package/out/language-server/chat/telemetry/chatTelemetryController.js.map +1 -1
  40. package/out/language-server/inline-completion/auto-trigger/autoTrigger.d.ts +2 -0
  41. package/out/language-server/inline-completion/auto-trigger/autoTrigger.js +69 -1
  42. package/out/language-server/inline-completion/auto-trigger/autoTrigger.js.map +1 -1
  43. package/out/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.js +6 -1
  44. package/out/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.js.map +1 -1
  45. package/out/language-server/inline-completion/auto-trigger/editPredictionConfig.d.ts +1 -0
  46. package/out/language-server/inline-completion/auto-trigger/editPredictionConfig.js +1 -0
  47. package/out/language-server/inline-completion/auto-trigger/editPredictionConfig.js.map +1 -1
  48. package/out/language-server/inline-completion/codeWhispererServer.js +29 -17
  49. package/out/language-server/inline-completion/codeWhispererServer.js.map +1 -1
  50. package/out/shared/activeUserTracker.d.ts +52 -0
  51. package/out/shared/activeUserTracker.js +164 -0
  52. package/out/shared/activeUserTracker.js.map +1 -0
  53. package/out/shared/codeWhispererService.js +3 -1
  54. package/out/shared/codeWhispererService.js.map +1 -1
  55. package/out/shared/telemetry/telemetryService.js +1 -2
  56. package/out/shared/telemetry/telemetryService.js.map +1 -1
  57. package/out/shared/telemetry/types.d.ts +7 -1
  58. package/out/shared/telemetry/types.js +1 -0
  59. package/out/shared/telemetry/types.js.map +1 -1
  60. package/package.json +8 -6
@@ -9,6 +9,7 @@ const crypto = require("crypto");
9
9
  const path = require("path");
10
10
  const os = require("os");
11
11
  const codewhisperer_streaming_1 = require("@amzn/codewhisperer-streaming");
12
+ const toolConstants_1 = require("./constants/toolConstants");
12
13
  const protocol_1 = require("@aws/language-server-runtimes/protocol");
13
14
  const protocol_2 = require("@aws/language-server-runtimes/protocol");
14
15
  const server_interface_1 = require("@aws/language-server-runtimes/server-interface");
@@ -62,6 +63,7 @@ const modelSelection_1 = require("./constants/modelSelection");
62
63
  const imageVerification_1 = require("../../shared/imageVerification");
63
64
  const path_1 = require("@aws/lsp-core/out/util/path");
64
65
  const agenticChatControllerHelper_1 = require("./utils/agenticChatControllerHelper");
66
+ const activeUserTracker_1 = require("../../shared/activeUserTracker");
65
67
  class AgenticChatController {
66
68
  #features;
67
69
  #chatSessionManagementService;
@@ -81,6 +83,7 @@ class AgenticChatController {
81
83
  #mcpEventHandler;
82
84
  #paidTierMode;
83
85
  #origin;
86
+ #activeUserTracker;
84
87
  // latency metrics
85
88
  #llmRequestStartTime = 0;
86
89
  #toolCallLatencies = [];
@@ -100,7 +103,9 @@ class AgenticChatController {
100
103
  #getMessageIdForToolUse(toolType, toolUse) {
101
104
  const toolUseId = toolUse.toolUseId;
102
105
  // Return plain toolUseId for executeBash, add "_permission" suffix for all other tools
103
- return toolUse.name === 'executeBash' || toolType === 'executeBash' ? toolUseId : `${toolUseId}_permission`;
106
+ return toolUse.name === toolConstants_1.EXECUTE_BASH || toolType === toolConstants_1.EXECUTE_BASH
107
+ ? toolUseId
108
+ : `${toolUseId}${toolConstants_1.SUFFIX_PERMISSION}`;
104
109
  }
105
110
  /**
106
111
  * Logs system information that can be helpful for debugging customer issues
@@ -119,6 +124,14 @@ class AgenticChatController {
119
124
  };
120
125
  this.#features.logging.info(`System Information: ${JSON.stringify(systemInfo)}`);
121
126
  }
127
+ /**
128
+ * Determines the appropriate message ID for a compaction confirmation
129
+ * @param messageId The original messageId
130
+ * @returns The message ID to use
131
+ */
132
+ #getMessageIdForCompact(messageId) {
133
+ return `${messageId}_compact`;
134
+ }
122
135
  constructor(chatSessionManagementService, features, telemetryService, serviceManager) {
123
136
  this.#features = features;
124
137
  this.#chatSessionManagementService = chatSessionManagementService;
@@ -136,6 +149,7 @@ class AgenticChatController {
136
149
  this.#contextCommandsProvider = new contextCommandsProvider_1.ContextCommandsProvider(this.#features.logging, this.#features.chat, this.#features.workspace, this.#features.lsp);
137
150
  this.#mcpEventHandler = new mcpEventHandler_1.McpEventHandler(features, telemetryService);
138
151
  this.#origin = (0, utils_2.getOriginFromClientInfo)(this.#features.lsp.getClientInitializeParams()?.clientInfo?.name);
152
+ this.#activeUserTracker = activeUserTracker_1.ActiveUserTracker.getInstance(this.#features);
139
153
  }
140
154
  async onExecuteCommand(params, _token) {
141
155
  this.#log(`onExecuteCommand: ${params.command}`);
@@ -152,16 +166,16 @@ class AgenticChatController {
152
166
  async onButtonClick(params) {
153
167
  this.#log(`onButtonClick event with params: ${JSON.stringify(params)}`);
154
168
  const session = this.#chatSessionManagementService.getSession(params.tabId);
155
- if (params.buttonId === 'run-shell-command' ||
156
- params.buttonId === 'reject-shell-command' ||
157
- params.buttonId === 'reject-mcp-tool' ||
158
- params.buttonId === 'allow-tools') {
169
+ if (params.buttonId === toolConstants_1.BUTTON_RUN_SHELL_COMMAND ||
170
+ params.buttonId === toolConstants_1.BUTTON_REJECT_SHELL_COMMAND ||
171
+ params.buttonId === toolConstants_1.BUTTON_REJECT_MCP_TOOL ||
172
+ params.buttonId === toolConstants_1.BUTTON_ALLOW_TOOLS) {
159
173
  if (!session.data) {
160
174
  return { success: false, failureReason: `could not find chat session for tab: ${params.tabId} ` };
161
175
  }
162
176
  // For 'allow-tools', remove suffix as permission card needs to be seperate from file list card
163
- const messageId = params.buttonId === 'allow-tools' && params.messageId.endsWith('_permission')
164
- ? params.messageId.replace('_permission', '')
177
+ const messageId = params.buttonId === toolConstants_1.BUTTON_ALLOW_TOOLS && params.messageId.endsWith(toolConstants_1.SUFFIX_PERMISSION)
178
+ ? params.messageId.replace(toolConstants_1.SUFFIX_PERMISSION, '')
165
179
  : params.messageId;
166
180
  const handler = session.data.getDeferredToolExecution(messageId);
167
181
  if (!handler?.reject || !handler.resolve) {
@@ -170,7 +184,7 @@ class AgenticChatController {
170
184
  failureReason: `could not find deferred tool execution for message: ${messageId} `,
171
185
  };
172
186
  }
173
- params.buttonId === 'reject-shell-command' || params.buttonId === 'reject-mcp-tool'
187
+ params.buttonId === toolConstants_1.BUTTON_REJECT_SHELL_COMMAND || params.buttonId === toolConstants_1.BUTTON_REJECT_MCP_TOOL
174
188
  ? (() => {
175
189
  handler.reject(new toolShared_1.ToolApprovalException('Command was rejected.', true));
176
190
  this.#stoppedToolUses.add(messageId);
@@ -180,7 +194,7 @@ class AgenticChatController {
180
194
  success: true,
181
195
  };
182
196
  }
183
- else if (params.buttonId === 'undo-changes') {
197
+ else if (params.buttonId === toolConstants_1.BUTTON_UNDO_CHANGES) {
184
198
  const toolUseId = params.messageId;
185
199
  try {
186
200
  await this.#undoFileChange(toolUseId, session.data);
@@ -194,23 +208,23 @@ class AgenticChatController {
194
208
  success: true,
195
209
  };
196
210
  }
197
- else if (params.buttonId === 'undo-all-changes') {
198
- const toolUseId = params.messageId.replace('_undoall', '');
211
+ else if (params.buttonId === toolConstants_1.BUTTON_UNDO_ALL_CHANGES) {
212
+ const toolUseId = params.messageId.replace(toolConstants_1.SUFFIX_UNDOALL, '');
199
213
  await this.#undoAllFileChanges(params.tabId, toolUseId, session.data);
200
214
  return {
201
215
  success: true,
202
216
  };
203
217
  }
204
- else if (params.buttonId === 'stop-shell-command') {
218
+ else if (params.buttonId === toolConstants_1.BUTTON_STOP_SHELL_COMMAND) {
205
219
  this.#stoppedToolUses.add(params.messageId);
206
220
  await this.#renderStoppedShellCommand(params.tabId, params.messageId);
207
221
  return { success: true };
208
222
  }
209
- else if (params.buttonId === 'paidtier-upgrade-q-learnmore') {
223
+ else if (params.buttonId === toolConstants_1.BUTTON_PAIDTIER_UPGRADE_Q_LEARNMORE) {
210
224
  (0, paidTier_1.onPaidTierLearnMore)(this.#features.lsp, this.#features.logging);
211
225
  return { success: true };
212
226
  }
213
- else if (params.buttonId === 'paidtier-upgrade-q') {
227
+ else if (params.buttonId === toolConstants_1.BUTTON_PAIDTIER_UPGRADE_Q) {
214
228
  await this.onManageSubscription(params.tabId);
215
229
  return { success: true };
216
230
  }
@@ -242,7 +256,7 @@ class AgenticChatController {
242
256
  return;
243
257
  }
244
258
  const fileList = cachedToolUse.chatResult?.header?.fileList;
245
- const button = cachedToolUse.chatResult?.header?.buttons?.filter(button => button.id !== 'undo-changes');
259
+ const button = cachedToolUse.chatResult?.header?.buttons?.filter(button => button.id !== toolConstants_1.BUTTON_UNDO_CHANGES);
246
260
  const updatedHeader = {
247
261
  ...cachedToolUse.chatResult?.header,
248
262
  buttons: button,
@@ -290,7 +304,7 @@ class AgenticChatController {
290
304
  return;
291
305
  }
292
306
  for (const messageId of [...toUndo].reverse()) {
293
- await this.onButtonClick({ buttonId: 'undo-changes', messageId, tabId });
307
+ await this.onButtonClick({ buttonId: toolConstants_1.BUTTON_UNDO_CHANGES, messageId, tabId });
294
308
  }
295
309
  }
296
310
  async onOpenFileDialog(params, token) {
@@ -394,6 +408,7 @@ class AgenticChatController {
394
408
  this.#contextCommandsProvider?.dispose();
395
409
  this.#userWrittenCodeTracker?.dispose();
396
410
  this.#mcpEventHandler.dispose();
411
+ this.#activeUserTracker.dispose();
397
412
  clearInterval(this.#abTestingFetchingTimeout);
398
413
  }
399
414
  async onListConversations(params) {
@@ -457,6 +472,8 @@ class AgenticChatController {
457
472
  if (!success) {
458
473
  return new server_interface_1.ResponseError(protocol_2.ErrorCodes.InternalError, sessionResult.error);
459
474
  }
475
+ const compactIds = session.getAllDeferredCompactMessageIds();
476
+ await this.#invalidateCompactCommand(params.tabId, compactIds);
460
477
  session.rejectAllDeferredToolExecutions(new toolShared_1.ToolApprovalException('Command ignored: new prompt', false));
461
478
  await this.#invalidateAllShellCommands(params.tabId, session);
462
479
  const metric = new metric_1.Metric({
@@ -464,6 +481,10 @@ class AgenticChatController {
464
481
  experimentName: this.#abTestingAllocation?.experimentName,
465
482
  userVariation: this.#abTestingAllocation?.userVariation,
466
483
  });
484
+ const isNewActiveUser = this.#activeUserTracker.isNewActiveUser();
485
+ if (isNewActiveUser) {
486
+ this.#telemetryController.emitActiveUser();
487
+ }
467
488
  try {
468
489
  const triggerContext = await this.#getTriggerContext(params, metric);
469
490
  if (triggerContext.programmingLanguage?.languageName) {
@@ -482,6 +503,8 @@ class AgenticChatController {
482
503
  this.#log('cancellation requested');
483
504
  // Abort all operations immediately
484
505
  session.abortRequest();
506
+ const compactIds = session.getAllDeferredCompactMessageIds();
507
+ await this.#invalidateCompactCommand(params.tabId, compactIds);
485
508
  void this.#invalidateAllShellCommands(params.tabId, session);
486
509
  session.rejectAllDeferredToolExecutions(new lsp_core_1.CancellationError('user'));
487
510
  // Then update UI to inform the user
@@ -519,19 +542,31 @@ class AgenticChatController {
519
542
  const customContext = await this.#additionalContextProvider.getImageBlocksFromContext(params.context, params.tabId);
520
543
  // Add image context to triggerContext.documentReference for transparency
521
544
  await this.#additionalContextProvider.appendCustomContextToTriggerContext(triggerContext, params.context, params.tabId);
522
- // Get the initial request input
523
- const initialRequestInput = await this.#prepareRequestInput(params, session, triggerContext, additionalContext, chatResultStream, customContext);
524
- // Generate a unique ID for this prompt
525
- const promptId = crypto.randomUUID();
526
- session.setCurrentPromptId(promptId);
527
- // Start the agent loop
528
- const finalResult = await this.#runAgentLoop(initialRequestInput, session, metric, chatResultStream, params.tabId, promptId, session.conversationId, token, triggerContext.documentReference, additionalContext.filter(item => item.pinned));
529
- // Phase 5: Result Handling - This happens only once
530
- return await this.#handleFinalResult(finalResult, session, params, metric, triggerContext, isNewConversation, chatResultStream);
545
+ let finalResult;
546
+ if (params.prompt.command === quickActions_1.QuickAction.Compact) {
547
+ // Get the compaction request input
548
+ const compactionRequestInput = this.#getCompactionRequestInput(session);
549
+ // Generate a unique ID for this prompt
550
+ const promptId = crypto.randomUUID();
551
+ session.setCurrentPromptId(promptId);
552
+ // Start the compaction call
553
+ finalResult = await this.#runCompaction(compactionRequestInput, session, metric, chatResultStream, params.tabId, promptId, session.conversationId, token, triggerContext.documentReference);
554
+ }
555
+ else {
556
+ // Get the initial request input
557
+ const initialRequestInput = await this.#prepareRequestInput(params, session, triggerContext, additionalContext, chatResultStream, customContext);
558
+ // Generate a unique ID for this prompt
559
+ const promptId = crypto.randomUUID();
560
+ session.setCurrentPromptId(promptId);
561
+ // Start the agent loop
562
+ finalResult = await this.#runAgentLoop(initialRequestInput, session, metric, chatResultStream, params.tabId, promptId, session.conversationId, token, triggerContext.documentReference, additionalContext.filter(item => item.pinned));
563
+ }
564
+ // Result Handling - This happens only once
565
+ return await this.#handleFinalResult(finalResult, session, params.tabId, metric, triggerContext, isNewConversation, chatResultStream);
531
566
  }
532
567
  catch (err) {
533
568
  // HACK: the chat-client needs to have a partial event with the associated messageId sent before it can accept the final result.
534
- // Without this, the `thinking` indicator never goes away.
569
+ // Without this, the `working` indicator never goes away.
535
570
  // Note: buttons being explicitly empty is required for this hack to work.
536
571
  const errorMessageId = `error-message-id-${(0, uuid_1.v4)()}`;
537
572
  await this.#sendProgressToClient({
@@ -563,9 +598,131 @@ class AgenticChatController {
563
598
  this.#debug('Preparing request input');
564
599
  // Get profileArn from the service manager if available
565
600
  const profileArn = this.#serviceManager?.getActiveProfileArn();
566
- const requestInput = await this.#triggerContext.getChatParamsFromTrigger(params, triggerContext, codewhisperer_streaming_1.ChatTriggerType.MANUAL, this.#customizationArn, chatResultStream, profileArn, [], this.#getTools(session), additionalContext, session.modelId, this.#origin, customContext);
601
+ const requestInput = await this.#triggerContext.getChatParamsFromTrigger(params, triggerContext, codewhisperer_streaming_1.ChatTriggerType.MANUAL, this.#customizationArn, chatResultStream, profileArn, this.#getTools(session), additionalContext, session.modelId, this.#origin, customContext);
602
+ return requestInput;
603
+ }
604
+ /**
605
+ * Prepares the initial request input for the chat prompt
606
+ */
607
+ #getCompactionRequestInput(session) {
608
+ this.#debug('Preparing compaction request input');
609
+ // Get profileArn from the service manager if available
610
+ const profileArn = this.#serviceManager?.getActiveProfileArn();
611
+ const requestInput = this.#triggerContext.getCompactionChatCommandInput(profileArn, this.#getTools(session), session.modelId, this.#origin);
567
612
  return requestInput;
568
613
  }
614
+ /**
615
+ * Runs the compaction, making requests and processing tool uses until completion
616
+ */
617
+ #shouldCompact(currentRequestCount) {
618
+ // 80% of 570K limit
619
+ if (currentRequestCount > 456_000) {
620
+ this.#debug(`Current request total character count is: ${currentRequestCount}, prompting user to compact`);
621
+ return true;
622
+ }
623
+ else {
624
+ return false;
625
+ }
626
+ }
627
+ /**
628
+ * Runs the compaction to compact history into a single summary
629
+ */
630
+ async #runCompaction(compactionRequestInput, session, metric, chatResultStream, tabId, promptId, conversationIdentifier, token, documentReference) {
631
+ let currentRequestInput = { ...compactionRequestInput };
632
+ let finalResult = null;
633
+ metric.recordStart();
634
+ this.#debug(`Running compaction for conversation id:`, conversationIdentifier || '');
635
+ this.#timeToFirstChunk = -1;
636
+ this.#timeBetweenChunks = [];
637
+ // Check for cancellation
638
+ if (this.#isPromptCanceled(token, session, promptId)) {
639
+ this.#debug('Stopping compaction loop - cancelled by user');
640
+ throw new lsp_core_1.CancellationError('user');
641
+ }
642
+ const currentMessage = currentRequestInput.conversationState?.currentMessage;
643
+ let messages = [];
644
+ let characterCount = 0;
645
+ if (currentMessage) {
646
+ // Get and process the messages from history DB to maintain invariants for service requests
647
+ try {
648
+ const { messages: historyMessages, count: historyCharCount } = this.#chatHistoryDb.fixAndGetHistory(tabId, currentMessage, []);
649
+ messages = historyMessages;
650
+ characterCount = historyCharCount;
651
+ }
652
+ catch (err) {
653
+ if (err instanceof chatDb_1.ToolResultValidationError) {
654
+ this.#features.logging.error(`Tool validation error: ${err.message}`);
655
+ return (finalResult || {
656
+ success: false,
657
+ error: 'Compaction loop failed to produce a final result',
658
+ data: { chatResult: {}, toolUses: {} },
659
+ });
660
+ }
661
+ }
662
+ }
663
+ currentRequestInput.conversationState.history = messages.map(msg => (0, util_1.messageToStreamingMessage)(msg));
664
+ const resultStreamWriter = chatResultStream.getResultStreamWriter();
665
+ if (currentRequestInput.conversationState.history.length == 0) {
666
+ // early terminate
667
+ await resultStreamWriter.write({
668
+ type: 'answer',
669
+ body: 'History is empty, there is nothing to compact.',
670
+ messageId: (0, uuid_1.v4)(),
671
+ });
672
+ return {
673
+ success: true,
674
+ data: {
675
+ chatResult: {},
676
+ toolUses: {},
677
+ },
678
+ };
679
+ }
680
+ else {
681
+ await resultStreamWriter.write({
682
+ type: 'answer',
683
+ body: 'Compacting your chat history, this may take a moment.',
684
+ messageId: (0, uuid_1.v4)(),
685
+ });
686
+ }
687
+ await resultStreamWriter.close();
688
+ // Add loading message before making the request
689
+ const loadingMessageId = `loading-${(0, uuid_1.v4)()}`;
690
+ await chatResultStream.writeResultBlock({ ...constants_1.loadingMessage, messageId: loadingMessageId });
691
+ this.#debug(`Compacting history with ${characterCount} characters`);
692
+ this.#llmRequestStartTime = Date.now();
693
+ // Phase 3: Request Execution
694
+ // Note: these logs are very noisy, but contain information redacted on the backend.
695
+ this.#debug(`generateAssistantResponse/SendMessage Request: ${JSON.stringify(currentRequestInput, undefined, 2)}`);
696
+ const response = await session.getChatResponse(currentRequestInput);
697
+ if (response.$metadata.requestId) {
698
+ metric.mergeWith({
699
+ requestIds: [response.$metadata.requestId],
700
+ });
701
+ }
702
+ this.#features.logging.info(`generateAssistantResponse/SendMessage ResponseMetadata: ${lsp_core_2.loggingUtils.formatObj(response.$metadata)}`);
703
+ await chatResultStream.removeResultBlock(loadingMessageId);
704
+ // Phase 4: Response Processing
705
+ const result = await this.#processAgenticChatResponseWithTimeout(response, metric.mergeWith({
706
+ cwsprChatResponseCode: response.$metadata.httpStatusCode,
707
+ cwsprChatMessageId: response.$metadata.requestId,
708
+ }), chatResultStream, session, documentReference, true);
709
+ const llmLatency = Date.now() - this.#llmRequestStartTime;
710
+ this.#debug(`LLM Response Latency for compaction: ${llmLatency}`);
711
+ 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);
712
+ // replace the history with summary in history DB
713
+ if (result.data?.chatResult.body !== undefined) {
714
+ this.#chatHistoryDb.replaceWithSummary(tabId, 'cwc', conversationIdentifier ?? '', {
715
+ body: result.data?.chatResult.body,
716
+ type: 'prompt',
717
+ shouldDisplayMessage: true,
718
+ timestamp: new Date(),
719
+ });
720
+ }
721
+ else {
722
+ this.#features.logging.warn('No ChatResult body in response, skipping adding to history');
723
+ }
724
+ return result;
725
+ }
569
726
  /**
570
727
  * Runs the agent loop, making requests and processing tool uses until completion
571
728
  */
@@ -574,6 +731,7 @@ class AgenticChatController {
574
731
  let finalResult = null;
575
732
  let iterationCount = 0;
576
733
  let shouldDisplayMessage = true;
734
+ let currentRequestCount = 0;
577
735
  metric.recordStart();
578
736
  this.logSystemInformation();
579
737
  while (true) {
@@ -600,7 +758,11 @@ class AgenticChatController {
600
758
  if (currentMessage) {
601
759
  // Get and process the messages from history DB to maintain invariants for service requests
602
760
  try {
603
- messages = this.#chatHistoryDb.fixAndGetHistory(tabId, currentMessage, pinnedContextMessages);
761
+ const newUserInputCount = this.#chatHistoryDb.calculateNewMessageCharacterCount(currentMessage, pinnedContextMessages);
762
+ const { messages: historyMessages, count: historyCharacterCount } = this.#chatHistoryDb.fixAndGetHistory(tabId, currentMessage, pinnedContextMessages, newUserInputCount);
763
+ messages = historyMessages;
764
+ currentRequestCount = newUserInputCount + historyCharacterCount;
765
+ this.#debug(`Request total character count: ${currentRequestCount}`);
604
766
  }
605
767
  catch (err) {
606
768
  if (err instanceof chatDb_1.ToolResultValidationError) {
@@ -609,7 +771,7 @@ class AgenticChatController {
609
771
  }
610
772
  }
611
773
  }
612
- // Do not include chatHistory for requests going to Mynah Backend
774
+ // Do not include chatHistory for requests going to Mynah Backend
613
775
  currentRequestInput.conversationState.history = currentRequestInput.conversationState?.currentMessage
614
776
  ?.userInputMessage?.userIntent
615
777
  ? []
@@ -620,7 +782,7 @@ class AgenticChatController {
620
782
  this.#llmRequestStartTime = Date.now();
621
783
  // Phase 3: Request Execution
622
784
  // Note: these logs are very noisy, but contain information redacted on the backend.
623
- this.#debug(`generateAssistantResponse/SendMessage Request: ${JSON.stringify(currentRequestInput, undefined, 2)}`);
785
+ this.#debug(`generateAssistantResponse/SendMessage Request: ${JSON.stringify(currentRequestInput, this.#imageReplacer, 2)}`);
624
786
  const response = await session.getChatResponse(currentRequestInput);
625
787
  if (response.$metadata.requestId) {
626
788
  metric.mergeWith({
@@ -758,6 +920,16 @@ class AgenticChatController {
758
920
  }
759
921
  currentRequestInput = this.#updateRequestInputWithToolResults(currentRequestInput, toolResults, content);
760
922
  }
923
+ if (this.#shouldCompact(currentRequestCount)) {
924
+ const messageId = this.#getMessageIdForCompact((0, uuid_1.v4)());
925
+ const confirmationResult = this.#processCompactConfirmation(messageId);
926
+ const cachedButtonBlockId = await chatResultStream.writeResultBlock(confirmationResult);
927
+ await this.waitForCompactApproval(messageId, chatResultStream, cachedButtonBlockId, session);
928
+ // Get the compaction request input
929
+ const compactionRequestInput = this.#getCompactionRequestInput(session);
930
+ // Start the compaction call
931
+ return await this.#runCompaction(compactionRequestInput, session, metric, chatResultStream, tabId, promptId, session.conversationId, token, documentReference);
932
+ }
761
933
  return (finalResult || {
762
934
  success: false,
763
935
  error: 'Agent loop failed to produce a final result',
@@ -890,36 +1062,36 @@ class AgenticChatController {
890
1062
  // remove progress UI
891
1063
  await chatResultStream.removeResultBlockAndUpdateUI(agenticChatResultStream_1.progressPrefix + toolUse.toolUseId);
892
1064
  // fsRead and listDirectory write to an existing card and could show nothing in the current position
893
- if (!['fsWrite', 'fsReplace', 'fsRead', 'listDirectory'].includes(toolUse.name)) {
1065
+ if (![toolConstants_1.FS_WRITE, toolConstants_1.FS_REPLACE, toolConstants_1.FS_READ, toolConstants_1.LIST_DIRECTORY].includes(toolUse.name)) {
894
1066
  await this.#showUndoAllIfRequired(chatResultStream, session);
895
1067
  }
896
1068
  // fsWrite can take a long time, so we render fsWrite Explanatory upon partial streaming responses.
897
- if (toolUse.name !== 'fsWrite' && toolUse.name !== 'fsReplace') {
1069
+ if (toolUse.name !== toolConstants_1.FS_WRITE && toolUse.name !== toolConstants_1.FS_REPLACE) {
898
1070
  const { explanation } = toolUse.input;
899
1071
  if (explanation) {
900
1072
  await chatResultStream.writeResultBlock({
901
1073
  type: 'directive',
902
- messageId: toolUse.toolUseId + '_explanation',
1074
+ messageId: toolUse.toolUseId + toolConstants_1.SUFFIX_EXPLANATION,
903
1075
  body: explanation,
904
1076
  });
905
1077
  }
906
1078
  }
907
1079
  switch (toolUse.name) {
908
- case 'fsRead':
909
- case 'listDirectory':
910
- case 'grepSearch':
911
- case 'fileSearch':
912
- case 'fsWrite':
913
- case 'fsReplace':
914
- case 'executeBash': {
1080
+ case toolConstants_1.FS_READ:
1081
+ case toolConstants_1.LIST_DIRECTORY:
1082
+ case toolConstants_1.GREP_SEARCH:
1083
+ case toolConstants_1.FILE_SEARCH:
1084
+ case toolConstants_1.FS_WRITE:
1085
+ case toolConstants_1.FS_REPLACE:
1086
+ case toolConstants_1.EXECUTE_BASH: {
915
1087
  const toolMap = {
916
- fsRead: { Tool: fsRead_1.FsRead },
917
- listDirectory: { Tool: listDirectory_1.ListDirectory },
918
- fsWrite: { Tool: fsWrite_1.FsWrite },
919
- fsReplace: { Tool: fsReplace_1.FsReplace },
920
- executeBash: { Tool: executeBash_1.ExecuteBash },
921
- grepSearch: { Tool: grepSearch_1.GrepSearch },
922
- fileSearch: { Tool: fileSearch_1.FileSearch },
1088
+ [toolConstants_1.FS_READ]: { Tool: fsRead_1.FsRead },
1089
+ [toolConstants_1.LIST_DIRECTORY]: { Tool: listDirectory_1.ListDirectory },
1090
+ [toolConstants_1.FS_WRITE]: { Tool: fsWrite_1.FsWrite },
1091
+ [toolConstants_1.FS_REPLACE]: { Tool: fsReplace_1.FsReplace },
1092
+ [toolConstants_1.EXECUTE_BASH]: { Tool: executeBash_1.ExecuteBash },
1093
+ [toolConstants_1.GREP_SEARCH]: { Tool: grepSearch_1.GrepSearch },
1094
+ [toolConstants_1.FILE_SEARCH]: { Tool: fileSearch_1.FileSearch },
923
1095
  };
924
1096
  const { Tool } = toolMap[toolUse.name];
925
1097
  const tool = new Tool(this.#features);
@@ -933,11 +1105,11 @@ class AgenticChatController {
933
1105
  const { requiresAcceptance, warning, commandCategory } = await tool.requiresAcceptance(toolUse.input, approvedPaths);
934
1106
  // Honor built-in permission if available, otherwise use tool's requiresAcceptance
935
1107
  // const requiresAcceptance = builtInPermission || toolRequiresAcceptance
936
- if (requiresAcceptance || toolUse.name === 'executeBash') {
1108
+ if (requiresAcceptance || toolUse.name === toolConstants_1.EXECUTE_BASH) {
937
1109
  // for executeBash, we till send the confirmation message without action buttons
938
1110
  const confirmationResult = this.#processToolConfirmation(toolUse, requiresAcceptance, warning, commandCategory);
939
1111
  cachedButtonBlockId = await chatResultStream.writeResultBlock(confirmationResult);
940
- const isExecuteBash = toolUse.name === 'executeBash';
1112
+ const isExecuteBash = toolUse.name === toolConstants_1.EXECUTE_BASH;
941
1113
  if (isExecuteBash) {
942
1114
  this.#telemetryController.emitInteractWithAgenticChat('GeneratedCommand', tabId, session.pairProgrammingMode, session.getConversationType(), this.#abTestingAllocation?.experimentName, this.#abTestingAllocation?.userVariation);
943
1115
  }
@@ -994,7 +1166,7 @@ class AgenticChatController {
994
1166
  }
995
1167
  break;
996
1168
  }
997
- if (toolUse.name === 'fsWrite' || toolUse.name === 'fsReplace') {
1169
+ if (toolUse.name === toolConstants_1.FS_WRITE || toolUse.name === toolConstants_1.FS_REPLACE) {
998
1170
  const input = toolUse.input;
999
1171
  const document = await this.#triggerContext.getTextDocumentFromPath(input.path, true, true);
1000
1172
  session.toolUseLookup.set(toolUse.toolUseId, {
@@ -1044,27 +1216,27 @@ class AgenticChatController {
1044
1216
  content: [toolResultContent],
1045
1217
  });
1046
1218
  switch (toolUse.name) {
1047
- case 'fsRead':
1048
- case 'listDirectory':
1049
- case 'fileSearch':
1219
+ case toolConstants_1.FS_READ:
1220
+ case toolConstants_1.LIST_DIRECTORY:
1221
+ case toolConstants_1.FILE_SEARCH:
1050
1222
  const initialListDirResult = this.#processReadOrListOrSearch(toolUse, chatResultStream);
1051
1223
  if (initialListDirResult) {
1052
1224
  await chatResultStream.writeResultBlock(initialListDirResult);
1053
1225
  }
1054
1226
  break;
1055
1227
  // no need to write tool result for listDir,fsRead,fileSearch into chat stream
1056
- case 'executeBash':
1228
+ case toolConstants_1.EXECUTE_BASH:
1057
1229
  // no need to write tool result for listDir and fsRead into chat stream
1058
1230
  // executeBash will stream the output instead of waiting until the end
1059
1231
  break;
1060
- case 'grepSearch':
1232
+ case toolConstants_1.GREP_SEARCH:
1061
1233
  const grepSearchResult = this.#processGrepSearchResult(toolUse, result, chatResultStream);
1062
1234
  if (grepSearchResult) {
1063
1235
  await chatResultStream.writeResultBlock(grepSearchResult);
1064
1236
  }
1065
1237
  break;
1066
- case 'fsReplace':
1067
- case 'fsWrite':
1238
+ case toolConstants_1.FS_REPLACE:
1239
+ case toolConstants_1.FS_WRITE:
1068
1240
  const input = toolUse.input;
1069
1241
  // Load from the filesystem instead of workspace.
1070
1242
  // Workspace is likely out of date - when files
@@ -1149,12 +1321,12 @@ class AgenticChatController {
1149
1321
  continue;
1150
1322
  }
1151
1323
  // Rethrow error for executeBash or any named tool
1152
- if (toolUse.name === 'executeBash' || toolUse.name) {
1324
+ if (toolUse.name === toolConstants_1.EXECUTE_BASH || toolUse.name) {
1153
1325
  throw err;
1154
1326
  }
1155
1327
  }
1156
1328
  // display fs write failure status in the UX of that file card
1157
- if ((toolUse.name === 'fsWrite' || toolUse.name === 'fsReplace') && toolUse.toolUseId) {
1329
+ if ((toolUse.name === toolConstants_1.FS_WRITE || toolUse.name === toolConstants_1.FS_REPLACE) && toolUse.toolUseId) {
1158
1330
  const existingCard = chatResultStream.getMessageBlockId(toolUse.toolUseId);
1159
1331
  const fsParam = toolUse.input;
1160
1332
  if (fsParam.path) {
@@ -1188,7 +1360,7 @@ class AgenticChatController {
1188
1360
  }
1189
1361
  }
1190
1362
  }
1191
- else if (toolUse.name === 'executeBash' && toolUse.toolUseId) {
1363
+ else if (toolUse.name === toolConstants_1.EXECUTE_BASH && toolUse.toolUseId) {
1192
1364
  const existingCard = chatResultStream.getMessageBlockId(toolUse.toolUseId);
1193
1365
  const command = toolUse.input.command;
1194
1366
  const completedErrorResult = {
@@ -1246,10 +1418,10 @@ class AgenticChatController {
1246
1418
  * Updates the currentUndoAllId state in the session
1247
1419
  */
1248
1420
  #updateUndoAllState(toolUse, session) {
1249
- if (toolUse.name === 'fsRead' || toolUse.name === 'listDirectory') {
1421
+ if (toolUse.name === toolConstants_1.FS_READ || toolUse.name === toolConstants_1.LIST_DIRECTORY) {
1250
1422
  return;
1251
1423
  }
1252
- if (toolUse.name === 'fsWrite' || toolUse.name === 'fsReplace') {
1424
+ if (toolUse.name === toolConstants_1.FS_WRITE || toolUse.name === toolConstants_1.FS_REPLACE) {
1253
1425
  if (session.currentUndoAllId === undefined) {
1254
1426
  session.currentUndoAllId = toolUse.toolUseId;
1255
1427
  }
@@ -1284,10 +1456,10 @@ class AgenticChatController {
1284
1456
  }
1285
1457
  await chatResultStream.writeResultBlock({
1286
1458
  type: 'answer',
1287
- messageId: `${session.currentUndoAllId}_undoall`,
1459
+ messageId: `${session.currentUndoAllId}${toolConstants_1.SUFFIX_UNDOALL}`,
1288
1460
  buttons: [
1289
1461
  {
1290
- id: 'undo-all-changes',
1462
+ id: toolConstants_1.BUTTON_UNDO_ALL_CHANGES,
1291
1463
  text: 'Undo all changes',
1292
1464
  icon: 'undo',
1293
1465
  status: 'clear',
@@ -1315,11 +1487,11 @@ class AgenticChatController {
1315
1487
  #validateToolResult(toolUse, result) {
1316
1488
  let maxToolResponseSize;
1317
1489
  switch (toolUse.name) {
1318
- case 'fsRead':
1319
- case 'executeBash':
1490
+ case toolConstants_1.FS_READ:
1491
+ case toolConstants_1.EXECUTE_BASH:
1320
1492
  // fsRead and executeBash already have truncation logic
1321
1493
  return;
1322
- case 'listDirectory':
1494
+ case toolConstants_1.LIST_DIRECTORY:
1323
1495
  maxToolResponseSize = 50_000;
1324
1496
  break;
1325
1497
  default:
@@ -1371,7 +1543,7 @@ class AgenticChatController {
1371
1543
  if (toolUse.name === qCodeReview_1.QCodeReview.toolName) {
1372
1544
  return this.#getToolOverWritableStream(chatResultStream, toolUse);
1373
1545
  }
1374
- if (toolUse.name !== 'executeBash') {
1546
+ if (toolUse.name !== toolConstants_1.EXECUTE_BASH) {
1375
1547
  return;
1376
1548
  }
1377
1549
  const toolMsgId = toolUse.toolUseId;
@@ -1379,7 +1551,7 @@ class AgenticChatController {
1379
1551
  let headerEmitted = false;
1380
1552
  const initialHeader = {
1381
1553
  body: 'shell',
1382
- buttons: [{ id: 'stop-shell-command', text: 'Stop', icon: 'stop' }],
1554
+ buttons: [{ id: toolConstants_1.BUTTON_STOP_SHELL_COMMAND, text: 'Cancel', icon: 'stop' }],
1383
1555
  };
1384
1556
  const completedHeader = {
1385
1557
  body: 'shell',
@@ -1427,7 +1599,7 @@ class AgenticChatController {
1427
1599
  #getUpdateToolConfirmResult(toolUse, isAccept, originalToolName, toolType) {
1428
1600
  const toolName = originalToolName ?? (toolType || toolUse.name);
1429
1601
  // Handle bash commands with special formatting
1430
- if (toolName === 'executeBash') {
1602
+ if (toolName === toolConstants_1.EXECUTE_BASH) {
1431
1603
  return {
1432
1604
  messageId: toolUse.toolUseId,
1433
1605
  type: 'tool',
@@ -1443,7 +1615,7 @@ class AgenticChatController {
1443
1615
  text: 'Rejected',
1444
1616
  },
1445
1617
  }),
1446
- buttons: isAccept ? [{ id: 'stop-shell-command', text: 'Stop', icon: 'stop' }] : [],
1618
+ buttons: isAccept ? [{ id: toolConstants_1.BUTTON_STOP_SHELL_COMMAND, text: 'Cancel', icon: 'stop' }] : [],
1447
1619
  },
1448
1620
  };
1449
1621
  }
@@ -1451,10 +1623,10 @@ class AgenticChatController {
1451
1623
  let header;
1452
1624
  let body;
1453
1625
  switch (toolName) {
1454
- case 'fsReplace':
1455
- case 'fsWrite':
1456
- case 'fsRead':
1457
- case 'listDirectory':
1626
+ case toolConstants_1.FS_REPLACE:
1627
+ case toolConstants_1.FS_WRITE:
1628
+ case toolConstants_1.FS_READ:
1629
+ case toolConstants_1.LIST_DIRECTORY:
1458
1630
  header = {
1459
1631
  body: undefined,
1460
1632
  status: {
@@ -1464,7 +1636,7 @@ class AgenticChatController {
1464
1636
  },
1465
1637
  };
1466
1638
  break;
1467
- case 'fileSearch':
1639
+ case toolConstants_1.FILE_SEARCH:
1468
1640
  const searchPath = toolUse.input.path;
1469
1641
  header = {
1470
1642
  body: 'File Search',
@@ -1531,7 +1703,7 @@ class AgenticChatController {
1531
1703
  status: {
1532
1704
  status: 'error',
1533
1705
  icon: 'stop',
1534
- text: 'Stopped',
1706
+ text: 'Canceled',
1535
1707
  },
1536
1708
  buttons: [],
1537
1709
  },
@@ -1540,6 +1712,97 @@ class AgenticChatController {
1540
1712
  },
1541
1713
  });
1542
1714
  }
1715
+ #processCompactConfirmation(messageId) {
1716
+ const buttons = [{ id: 'allow-tools', text: 'Allow', icon: 'ok', status: 'clear' }];
1717
+ const header = {
1718
+ icon: 'warning',
1719
+ iconForegroundStatus: 'warning',
1720
+ body: constants_2.COMPACTION_HEADER_BODY,
1721
+ buttons,
1722
+ };
1723
+ const body = constants_2.COMPACTION_BODY;
1724
+ return {
1725
+ type: 'tool',
1726
+ messageId,
1727
+ header,
1728
+ body,
1729
+ };
1730
+ }
1731
+ /**
1732
+ * Creates a promise that does not resolve until the user accepts or rejects the compaction usage.
1733
+ * @param messageId
1734
+ * @param resultStream
1735
+ * @param promptBlockId id of approval block. This allows us to overwrite the buttons with 'accepted' or 'rejected' text.
1736
+ */
1737
+ async waitForCompactApproval(messageId, resultStream, promptBlockId, session) {
1738
+ const deferred = this.#createDeferred();
1739
+ session.setDeferredToolExecution(messageId, deferred.resolve, deferred.reject);
1740
+ this.#log(`Prompting for compaction approval for messageId: ${messageId}`);
1741
+ await deferred.promise;
1742
+ // Note: we want to overwrite the button block because it already exists in the stream.
1743
+ await resultStream.overwriteResultBlock(this.#getUpdateCompactConfirmResult(messageId), promptBlockId);
1744
+ }
1745
+ /**
1746
+ * Creates an updated ChatResult for compaction confirmation
1747
+ * @param messageId The messageId
1748
+ * @returns ChatResult with appropriate confirmation UI
1749
+ */
1750
+ #getUpdateCompactConfirmResult(messageId) {
1751
+ let header;
1752
+ let body;
1753
+ header = {
1754
+ body: undefined,
1755
+ status: {
1756
+ status: 'success',
1757
+ icon: 'ok',
1758
+ text: 'Allowed',
1759
+ },
1760
+ };
1761
+ return {
1762
+ messageId,
1763
+ type: 'tool',
1764
+ body,
1765
+ header,
1766
+ };
1767
+ }
1768
+ #renderStopShellCommandButton() {
1769
+ const stopKey = this.#getKeyBinding('aws.amazonq.stopCmdExecution');
1770
+ return {
1771
+ id: 'stop-shell-command',
1772
+ text: 'Cancel',
1773
+ icon: 'stop',
1774
+ ...(stopKey ? { description: `Stop: ${stopKey}` } : {}),
1775
+ };
1776
+ }
1777
+ #getKeyBinding(commandId) {
1778
+ // Check for feature flag
1779
+ const shortcut = this.#features.lsp.getClientInitializeParams()?.initializationOptions?.aws?.awsClientCapabilities?.q
1780
+ ?.shortcut;
1781
+ if (!shortcut) {
1782
+ return null;
1783
+ }
1784
+ let defaultKey = '';
1785
+ const OS = os.platform();
1786
+ switch (commandId) {
1787
+ case 'aws.amazonq.runCmdExecution':
1788
+ defaultKey = OS === 'darwin' ? constants_2.DEFAULT_MACOS_RUN_SHORTCUT : constants_2.DEFAULT_WINDOW_RUN_SHORTCUT;
1789
+ break;
1790
+ case 'aws.amazonq.rejectCmdExecution':
1791
+ defaultKey = OS === 'darwin' ? constants_2.DEFAULT_MACOS_REJECT_SHORTCUT : constants_2.DEFAULT_WINDOW_REJECT_SHORTCUT;
1792
+ break;
1793
+ case 'aws.amazonq.stopCmdExecution':
1794
+ defaultKey = OS === 'darwin' ? constants_2.DEFAULT_MACOS_STOP_SHORTCUT : constants_2.DEFAULT_WINDOW_STOP_SHORTCUT;
1795
+ break;
1796
+ default:
1797
+ this.#log(`#getKeyBinding: ${commandId} shortcut is supported by Q `);
1798
+ break;
1799
+ }
1800
+ if (defaultKey === '') {
1801
+ return null;
1802
+ }
1803
+ //TODO: handle case: user change default keybind, suggestion: read `keybinding.json` provided by VSC
1804
+ return defaultKey;
1805
+ }
1543
1806
  #processToolConfirmation(toolUse, requiresAcceptance, warning, commandCategory, toolType, builtInPermission) {
1544
1807
  const toolName = toolType || toolUse.name;
1545
1808
  let buttons = [];
@@ -1547,13 +1810,13 @@ class AgenticChatController {
1547
1810
  let body;
1548
1811
  // Configure tool-specific UI elements
1549
1812
  switch (toolName) {
1550
- case 'executeBash': {
1813
+ case toolConstants_1.EXECUTE_BASH: {
1551
1814
  const commandString = toolUse.input.command;
1552
1815
  buttons = requiresAcceptance
1553
1816
  ? [
1554
- { id: 'run-shell-command', text: 'Run', icon: 'play' },
1817
+ { id: toolConstants_1.BUTTON_RUN_SHELL_COMMAND, text: 'Run', icon: 'play' },
1555
1818
  {
1556
- id: 'reject-shell-command',
1819
+ id: toolConstants_1.BUTTON_REJECT_SHELL_COMMAND,
1557
1820
  status: 'dimmed-clear',
1558
1821
  text: 'Reject',
1559
1822
  icon: 'cancel',
@@ -1585,12 +1848,12 @@ class AgenticChatController {
1585
1848
  body = '```shell\n' + commandString;
1586
1849
  break;
1587
1850
  }
1588
- case 'fsWrite': {
1851
+ case toolConstants_1.FS_WRITE: {
1589
1852
  const writeFilePath = toolUse.input.path;
1590
1853
  // Validate the path using our synchronous utility
1591
1854
  (0, pathValidation_1.validatePathBasic)(writeFilePath);
1592
1855
  this.#debug(`Processing ${toolUse.name} for path: ${writeFilePath}`);
1593
- buttons = [{ id: 'allow-tools', text: 'Allow', icon: 'ok', status: 'clear' }];
1856
+ buttons = [{ id: toolConstants_1.BUTTON_ALLOW_TOOLS, text: 'Allow', icon: 'ok', status: 'clear' }];
1594
1857
  header = {
1595
1858
  icon: 'warning',
1596
1859
  iconForegroundStatus: 'warning',
@@ -1604,12 +1867,12 @@ class AgenticChatController {
1604
1867
  : `I need permission to modify files outside of your workspace.\n\`${writeFilePath}\``;
1605
1868
  break;
1606
1869
  }
1607
- case 'fsReplace': {
1870
+ case toolConstants_1.FS_REPLACE: {
1608
1871
  const writeFilePath = toolUse.input.path;
1609
1872
  // For replace, we need to verify the file exists
1610
1873
  (0, pathValidation_1.validatePathExists)(writeFilePath);
1611
1874
  this.#debug(`Processing ${toolUse.name} for path: ${writeFilePath}`);
1612
- buttons = [{ id: 'allow-tools', text: 'Allow', icon: 'ok', status: 'clear' }];
1875
+ buttons = [{ id: toolConstants_1.BUTTON_ALLOW_TOOLS, text: 'Allow', icon: 'ok', status: 'clear' }];
1613
1876
  header = {
1614
1877
  icon: 'warning',
1615
1878
  iconForegroundStatus: 'warning',
@@ -1623,9 +1886,9 @@ class AgenticChatController {
1623
1886
  : `I need permission to modify files outside of your workspace.\n\`${writeFilePath}\``;
1624
1887
  break;
1625
1888
  }
1626
- case 'fsRead':
1627
- case 'listDirectory': {
1628
- buttons = [{ id: 'allow-tools', text: 'Allow', icon: 'ok', status: 'clear' }];
1889
+ case toolConstants_1.FS_READ:
1890
+ case toolConstants_1.LIST_DIRECTORY: {
1891
+ buttons = [{ id: toolConstants_1.BUTTON_ALLOW_TOOLS, text: 'Allow', icon: 'ok', status: 'clear' }];
1629
1892
  header = {
1630
1893
  icon: 'tools',
1631
1894
  iconForegroundStatus: 'tools',
@@ -1634,7 +1897,7 @@ class AgenticChatController {
1634
1897
  : '#### Allow read-only tools outside your workspace',
1635
1898
  buttons,
1636
1899
  };
1637
- if (toolName === 'fsRead') {
1900
+ if (toolName === toolConstants_1.FS_READ) {
1638
1901
  const paths = toolUse.input.paths;
1639
1902
  // Validate paths using our synchronous utility
1640
1903
  (0, pathValidation_1.validatePaths)(paths);
@@ -1658,7 +1921,7 @@ class AgenticChatController {
1658
1921
  }
1659
1922
  default: {
1660
1923
  // — DEFAULT ⇒ MCP tools
1661
- buttons = [{ id: 'allow-tools', text: 'Allow', icon: 'ok', status: 'clear' }];
1924
+ buttons = [{ id: toolConstants_1.BUTTON_ALLOW_TOOLS, text: 'Allow', icon: 'ok', status: 'clear' }];
1662
1925
  header = {
1663
1926
  icon: 'tools',
1664
1927
  iconForegroundStatus: 'warning',
@@ -1676,7 +1939,7 @@ class AgenticChatController {
1676
1939
  type: 'tool',
1677
1940
  messageId: this.#getMessageIdForToolUse(toolType, toolUse),
1678
1941
  header,
1679
- body: warning ? (toolName === 'executeBash' ? '' : '\n\n') + body : body,
1942
+ body: warning ? (toolName === toolConstants_1.EXECUTE_BASH ? '' : '\n\n') + body : body,
1680
1943
  };
1681
1944
  }
1682
1945
  else {
@@ -1689,9 +1952,9 @@ class AgenticChatController {
1689
1952
  icon: 'tools',
1690
1953
  body: `${toolName}`,
1691
1954
  buttons: [
1692
- { id: 'allow-tools', text: 'Run', icon: 'play', status: 'clear' },
1955
+ { id: toolConstants_1.BUTTON_ALLOW_TOOLS, text: 'Run', icon: 'play', status: 'clear' },
1693
1956
  {
1694
- id: 'reject-mcp-tool',
1957
+ id: toolConstants_1.BUTTON_REJECT_MCP_TOOL,
1695
1958
  text: 'Reject',
1696
1959
  icon: 'cancel',
1697
1960
  status: 'dimmed-clear',
@@ -1737,7 +2000,7 @@ class AgenticChatController {
1737
2000
  },
1738
2001
  },
1739
2002
  },
1740
- buttons: [{ id: 'undo-changes', text: 'Undo', icon: 'undo' }],
2003
+ buttons: [{ id: toolConstants_1.BUTTON_UNDO_CHANGES, text: 'Undo', icon: 'undo' }],
1741
2004
  },
1742
2005
  };
1743
2006
  }
@@ -1751,7 +2014,7 @@ class AgenticChatController {
1751
2014
  chatResultStream.setMessageIdToUpdateForTool(toolUse.name, messageIdToUpdate);
1752
2015
  }
1753
2016
  let currentPaths = [];
1754
- if (toolUse.name === 'fsRead') {
2017
+ if (toolUse.name === toolConstants_1.FS_READ) {
1755
2018
  currentPaths = toolUse.input?.paths;
1756
2019
  }
1757
2020
  else {
@@ -1782,9 +2045,9 @@ class AgenticChatController {
1782
2045
  }
1783
2046
  else {
1784
2047
  title =
1785
- toolUse.name === 'fsRead'
2048
+ toolUse.name === toolConstants_1.FS_READ
1786
2049
  ? `${itemCount} file${itemCount > 1 ? 's' : ''} read`
1787
- : toolUse.name === 'fileSearch'
2050
+ : toolUse.name === toolConstants_1.FILE_SEARCH
1788
2051
  ? `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} searched`
1789
2052
  : `${itemCount} ${itemCount === 1 ? 'directory' : 'directories'} listed`;
1790
2053
  }
@@ -1811,7 +2074,7 @@ class AgenticChatController {
1811
2074
  * Process grep search results and format them for display in the chat UI
1812
2075
  */
1813
2076
  #processGrepSearchResult(toolUse, result, chatResultStream) {
1814
- if (toolUse.name !== 'grepSearch') {
2077
+ if (toolUse.name !== toolConstants_1.GREP_SEARCH) {
1815
2078
  return undefined;
1816
2079
  }
1817
2080
  let messageIdToUpdate = toolUse.toolUseId;
@@ -1891,22 +2154,22 @@ class AgenticChatController {
1891
2154
  /**
1892
2155
  * Handles the final result after the agent loop completes
1893
2156
  */
1894
- async #handleFinalResult(result, session, params, metric, triggerContext, isNewConversation, chatResultStream) {
2157
+ async #handleFinalResult(result, session, tabId, metric, triggerContext, isNewConversation, chatResultStream) {
1895
2158
  if (!result.success) {
1896
2159
  throw new errors_2.AgenticChatError(result.error, 'FailedResult');
1897
2160
  }
1898
2161
  const conversationId = session.conversationId;
1899
2162
  this.#debug('Final session conversation id:', conversationId || '');
1900
2163
  if (conversationId) {
1901
- this.#telemetryController.setConversationId(params.tabId, conversationId);
2164
+ this.#telemetryController.setConversationId(tabId, conversationId);
1902
2165
  if (isNewConversation) {
1903
- this.#telemetryController.updateTriggerInfo(params.tabId, {
2166
+ this.#telemetryController.updateTriggerInfo(tabId, {
1904
2167
  startTrigger: {
1905
2168
  hasUserSnippet: metric.metric.cwsprChatHasCodeSnippet ?? false,
1906
2169
  triggerType: triggerContext.triggerType,
1907
2170
  },
1908
2171
  });
1909
- this.#telemetryController.emitStartConversationMetric(params.tabId, metric.metric);
2172
+ this.#telemetryController.emitStartConversationMetric(tabId, metric.metric);
1910
2173
  }
1911
2174
  }
1912
2175
  metric.setDimension('codewhispererCustomizationArn', this.#customizationArn);
@@ -1940,8 +2203,8 @@ class AgenticChatController {
1940
2203
  cwsprChatPinnedPromptContextCount: triggerContext.contextInfo.pinnedContextCount.promptContextCount,
1941
2204
  });
1942
2205
  }
1943
- await this.#telemetryController.emitAddMessageMetric(params.tabId, metric.metric, 'Succeeded');
1944
- this.#telemetryController.updateTriggerInfo(params.tabId, {
2206
+ await this.#telemetryController.emitAddMessageMetric(tabId, metric.metric, 'Succeeded');
2207
+ this.#telemetryController.updateTriggerInfo(tabId, {
1945
2208
  lastMessageTrigger: {
1946
2209
  ...triggerContext,
1947
2210
  messageId: result.data?.chatResult.messageId,
@@ -2191,7 +2454,7 @@ class AgenticChatController {
2191
2454
  const session = this.#chatSessionManagementService.getSession(params.tabId);
2192
2455
  const toolUseId = params.messageId;
2193
2456
  const toolUse = toolUseId ? session.data?.toolUseLookup.get(toolUseId) : undefined;
2194
- if (toolUse?.name === 'fsWrite' || toolUse?.name === 'fsReplace') {
2457
+ if (toolUse?.name === toolConstants_1.FS_WRITE || toolUse?.name === toolConstants_1.FS_REPLACE) {
2195
2458
  const input = toolUse.input;
2196
2459
  this.#features.lsp.workspace.openFileDiff({
2197
2460
  originalFileUri: input.path,
@@ -2200,7 +2463,7 @@ class AgenticChatController {
2200
2463
  fileContent: toolUse.fileChange?.after,
2201
2464
  });
2202
2465
  }
2203
- else if (toolUse?.name === 'fsRead') {
2466
+ else if (toolUse?.name === toolConstants_1.FS_READ) {
2204
2467
  await this.#features.lsp.window.showDocument({ uri: vscode_uri_1.URI.file(params.filePath).toString() });
2205
2468
  }
2206
2469
  else {
@@ -2410,9 +2673,31 @@ class AgenticChatController {
2410
2673
  });
2411
2674
  return triggerContext;
2412
2675
  }
2676
+ async #invalidateCompactCommand(tabId, messageIds) {
2677
+ for (const messageId of messageIds) {
2678
+ await this.#features.chat.sendChatUpdate({
2679
+ tabId,
2680
+ state: { inProgress: false },
2681
+ data: {
2682
+ messages: [
2683
+ {
2684
+ messageId,
2685
+ type: 'tool',
2686
+ body: constants_2.COMPACTION_BODY,
2687
+ header: {
2688
+ body: constants_2.COMPACTION_HEADER_BODY,
2689
+ status: { icon: 'block', text: 'Ignored' },
2690
+ buttons: [],
2691
+ },
2692
+ },
2693
+ ],
2694
+ },
2695
+ });
2696
+ }
2697
+ }
2413
2698
  async #invalidateAllShellCommands(tabId, session) {
2414
2699
  for (const [toolUseId, toolUse] of session.toolUseLookup.entries()) {
2415
- if (toolUse.name !== 'executeBash' || this.#stoppedToolUses.has(toolUseId))
2700
+ if (toolUse.name !== toolConstants_1.EXECUTE_BASH || this.#stoppedToolUses.has(toolUseId))
2416
2701
  continue;
2417
2702
  const params = toolUse.input;
2418
2703
  const command = params.command;
@@ -2637,7 +2922,7 @@ class AgenticChatController {
2637
2922
  return;
2638
2923
  }
2639
2924
  }
2640
- async #processAgenticChatResponseWithTimeout(response, metric, chatResultStream, session, contextList) {
2925
+ async #processAgenticChatResponseWithTimeout(response, metric, chatResultStream, session, contextList, isCompaction) {
2641
2926
  const abortController = new AbortController();
2642
2927
  let timeoutId;
2643
2928
  const timeoutPromise = new Promise((_, reject) => {
@@ -2647,7 +2932,7 @@ class AgenticChatController {
2647
2932
  }, constants_2.RESPONSE_TIMEOUT_MS);
2648
2933
  });
2649
2934
  const streamWriter = chatResultStream.getResultStreamWriter();
2650
- const processResponsePromise = this.#processAgenticChatResponse(response, metric, chatResultStream, streamWriter, session, contextList, abortController.signal);
2935
+ const processResponsePromise = this.#processAgenticChatResponse(response, metric, chatResultStream, streamWriter, session, contextList, abortController.signal, isCompaction);
2651
2936
  try {
2652
2937
  const result = await Promise.race([processResponsePromise, timeoutPromise]);
2653
2938
  clearTimeout(timeoutId);
@@ -2671,7 +2956,7 @@ class AgenticChatController {
2671
2956
  }
2672
2957
  const toolUses = Object.values(data.toolUses);
2673
2958
  for (const toolUse of toolUses) {
2674
- if ((toolUse.name === 'fsWrite' || toolUse.name === 'fsReplace') && typeof toolUse.input === 'string') {
2959
+ if ((toolUse.name === toolConstants_1.FS_WRITE || toolUse.name === toolConstants_1.FS_REPLACE) && typeof toolUse.input === 'string') {
2675
2960
  const filepath = extractKey(toolUse.input, 'path');
2676
2961
  const msgId = agenticChatResultStream_1.progressPrefix + toolUse.toolUseId;
2677
2962
  // render fs write UI as soon as fs write starts
@@ -2700,7 +2985,7 @@ class AgenticChatController {
2700
2985
  }
2701
2986
  // render the tool use explanatory as soon as this is received for fsWrite/fsReplace
2702
2987
  const explanation = extractKey(toolUse.input, 'explanation');
2703
- const messageId = agenticChatResultStream_1.progressPrefix + toolUse.toolUseId + '_explanation';
2988
+ const messageId = agenticChatResultStream_1.progressPrefix + toolUse.toolUseId + toolConstants_1.SUFFIX_EXPLANATION;
2704
2989
  if (explanation && !chatResultStream.hasMessage(messageId)) {
2705
2990
  await streamWriter.close();
2706
2991
  await chatResultStream.writeResultBlock({
@@ -2712,12 +2997,12 @@ class AgenticChatController {
2712
2997
  }
2713
2998
  }
2714
2999
  }
2715
- async #processAgenticChatResponse(response, metric, chatResultStream, streamWriter, session, contextList, abortSignal) {
3000
+ async #processAgenticChatResponse(response, metric, chatResultStream, streamWriter, session, contextList, abortSignal, isCompaction) {
2716
3001
  const requestId = response.$metadata.requestId;
2717
3002
  const chatEventParser = new agenticChatEventParser_1.AgenticChatEventParser(requestId, metric, this.#features.logging);
2718
3003
  // Display context transparency list once at the beginning of response
2719
3004
  // Use a flag to track if contextList has been sent already to avoid ux flickering
2720
- if (contextList?.filePaths && contextList.filePaths.length > 0 && !session.contextListSent) {
3005
+ if (!isCompaction && contextList?.filePaths && contextList.filePaths.length > 0 && !session.contextListSent) {
2721
3006
  await streamWriter.write({ body: '', contextList });
2722
3007
  session.contextListSent = true;
2723
3008
  }
@@ -2751,7 +3036,7 @@ class AgenticChatController {
2751
3036
  if (chatEvent.assistantResponseEvent && result.data.chatResult.body) {
2752
3037
  this.recordChunk('chunk');
2753
3038
  }
2754
- // make sure to save code reference events
3039
+ // update the UI with response
2755
3040
  if (chatEvent.assistantResponseEvent || chatEvent.codeReferenceEvent) {
2756
3041
  await streamWriter.write(result.data.chatResult);
2757
3042
  }
@@ -2761,9 +3046,17 @@ class AgenticChatController {
2761
3046
  }
2762
3047
  }
2763
3048
  if (isEmptyResponse) {
2764
- // If the response is empty, we need to send an empty answer message to remove the Thinking... indicator
3049
+ // If the response is empty, we need to send an empty answer message to remove the Working... indicator
2765
3050
  await streamWriter.write({ type: 'answer', body: '', messageId: (0, uuid_1.v4)() });
2766
3051
  }
3052
+ if (isCompaction) {
3053
+ // Show a dummy message to the UI for compaction completion
3054
+ await streamWriter.write({
3055
+ type: 'answer',
3056
+ body: 'Conversation history has been compacted successfully!',
3057
+ messageId: (0, uuid_1.v4)(),
3058
+ });
3059
+ }
2767
3060
  await streamWriter.close();
2768
3061
  metric.mergeWith({
2769
3062
  cwsprChatFullResponseLatency: metric.getTimeElapsed(),
@@ -3027,6 +3320,14 @@ class AgenticChatController {
3027
3320
  #debug(...messages) {
3028
3321
  this.#features.logging.debug(messages.join(' '));
3029
3322
  }
3323
+ // Helper function to sanitize the 'images' field for logging by replacing large binary data (e.g., Uint8Array) with a concise summary.
3324
+ // This prevents logs from being overwhelmed by raw byte arrays and keeps log output readable.
3325
+ #imageReplacer(key, value) {
3326
+ if (key === 'bytes' && value && typeof value.length === 'number') {
3327
+ return `[Uint8Array, length: ${value.length}]`;
3328
+ }
3329
+ return value;
3330
+ }
3030
3331
  }
3031
3332
  exports.AgenticChatController = AgenticChatController;
3032
3333
  //# sourceMappingURL=agenticChatController.js.map