@theia/ai-chat-ui 1.72.2 → 1.72.3

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 (31) hide show
  1. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/ai-chat-ui-frontend-module.js +4 -0
  3. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  4. package/lib/browser/chat-response-renderer/delegation-tool-renderer.d.ts +8 -1
  5. package/lib/browser/chat-response-renderer/delegation-tool-renderer.d.ts.map +1 -1
  6. package/lib/browser/chat-response-renderer/delegation-tool-renderer.js +30 -1
  7. package/lib/browser/chat-response-renderer/delegation-tool-renderer.js.map +1 -1
  8. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +15 -1
  9. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
  10. package/lib/browser/chat-response-renderer/tool-confirmation.js +60 -17
  11. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -1
  12. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +8 -1
  13. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  14. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +30 -4
  15. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  16. package/lib/browser/chat-view-widget.d.ts +1 -0
  17. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  18. package/lib/browser/chat-view-widget.js +3 -0
  19. package/lib/browser/chat-view-widget.js.map +1 -1
  20. package/lib/browser/tool-confirmation-keybinding-contribution.d.ts +25 -0
  21. package/lib/browser/tool-confirmation-keybinding-contribution.d.ts.map +1 -0
  22. package/lib/browser/tool-confirmation-keybinding-contribution.js +106 -0
  23. package/lib/browser/tool-confirmation-keybinding-contribution.js.map +1 -0
  24. package/package.json +12 -12
  25. package/src/browser/ai-chat-ui-frontend-module.ts +5 -0
  26. package/src/browser/chat-response-renderer/delegation-tool-renderer.tsx +34 -3
  27. package/src/browser/chat-response-renderer/tool-confirmation.tsx +122 -30
  28. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +49 -4
  29. package/src/browser/chat-view-widget.tsx +4 -0
  30. package/src/browser/style/index.css +9 -0
  31. package/src/browser/tool-confirmation-keybinding-contribution.ts +104 -0
@@ -16,13 +16,14 @@
16
16
 
17
17
  import * as React from '@theia/core/shared/react';
18
18
  import { nls } from '@theia/core/lib/common/nls';
19
- import { codicon, ContextMenuRenderer, OpenerService } from '@theia/core/lib/browser';
19
+ import { codicon, ContextMenuRenderer, LocalizedMarkdown, MarkdownRenderer, OpenerService } from '@theia/core/lib/browser';
20
20
  import { ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
21
21
  import { ToolRequest } from '@theia/ai-core';
22
22
  import { CommandMenu, ContextExpressionMatcher, MenuPath } from '@theia/core/lib/common/menu';
23
23
  import { GroupImpl } from '@theia/core/lib/browser/menu/composite-menu-node';
24
24
  import { ToolConfirmationMode as ToolConfirmationPreferenceMode } from '@theia/ai-chat/lib/common/chat-tool-preferences';
25
25
  import { ToolConfirmationManager } from '@theia/ai-chat/lib/browser/chat-tool-preference-bindings';
26
+ import { PendingToolConfirmationTracker } from '@theia/ai-chat/lib/browser/pending-tool-confirmation-tracker';
26
27
  import { MarkdownRender } from './markdown-part-renderer';
27
28
  import { condenseArguments, formatArgsForTooltip } from './toolcall-utils';
28
29
 
@@ -160,9 +161,16 @@ export interface ToolConfirmationCallbacks {
160
161
  onDeny: (scope: ConfirmationScope, reason?: string) => void;
161
162
  }
162
163
 
164
+ export interface ToolConfirmationKeybindingHints {
165
+ allow?: string;
166
+ deny?: string;
167
+ }
168
+
163
169
  export interface ToolConfirmationActionsProps extends ToolConfirmationCallbacks {
164
170
  toolName: string;
165
171
  contextMenuRenderer: ContextMenuRenderer;
172
+ autoFocus?: boolean;
173
+ keybindingHints?: ToolConfirmationKeybindingHints;
166
174
  }
167
175
 
168
176
  export class InlineActionMenuNode implements CommandMenu {
@@ -196,7 +204,9 @@ export const ToolConfirmationActions: React.FC<ToolConfirmationActionsProps> = (
196
204
  toolRequest,
197
205
  onAllow,
198
206
  onDeny,
199
- contextMenuRenderer
207
+ contextMenuRenderer,
208
+ autoFocus,
209
+ keybindingHints
200
210
  }) => {
201
211
  const [allowScope, setAllowScope] = React.useState<ConfirmationScope>('once');
202
212
  const [denyScope, setDenyScope] = React.useState<ConfirmationScope>('once');
@@ -205,6 +215,14 @@ export const ToolConfirmationActions: React.FC<ToolConfirmationActionsProps> = (
205
215
  const [denyReason, setDenyReason] = React.useState('');
206
216
  // eslint-disable-next-line no-null/no-null
207
217
  const denyReasonInputRef = React.useRef<HTMLInputElement>(null);
218
+ // eslint-disable-next-line no-null/no-null
219
+ const allowButtonRef = React.useRef<HTMLButtonElement>(null);
220
+
221
+ React.useEffect(() => {
222
+ if (autoFocus && allowButtonRef.current) {
223
+ allowButtonRef.current.focus();
224
+ }
225
+ }, [autoFocus]);
208
226
 
209
227
  const handleAllow = React.useCallback(() => {
210
228
  if ((allowScope === 'forever' || allowScope === 'session') && toolRequest?.confirmAlwaysAllow) {
@@ -334,6 +352,9 @@ export const ToolConfirmationActions: React.FC<ToolConfirmationActionsProps> = (
334
352
  const selectedScope = type === 'allow' ? allowScope : denyScope;
335
353
  const setScope = type === 'allow' ? setAllowScope : setDenyScope;
336
354
  const handleMain = type === 'allow' ? handleAllow : handleDeny;
355
+ const keybindingHint = type === 'allow' ? keybindingHints?.allow : keybindingHints?.deny;
356
+ const mainLabel = scopeLabel(type, selectedScope);
357
+ const mainTitle = keybindingHint && selectedScope === 'once' ? `${mainLabel} (${keybindingHint})` : undefined;
337
358
 
338
359
  return (
339
360
  <div
@@ -341,10 +362,12 @@ export const ToolConfirmationActions: React.FC<ToolConfirmationActionsProps> = (
341
362
  style={{ display: 'inline-flex', position: 'relative' }}
342
363
  >
343
364
  <button
365
+ ref={type === 'allow' ? allowButtonRef : undefined}
344
366
  className={`theia-button ${type === 'allow' ? 'main' : 'secondary'} theia-tool-confirmation-main-btn`}
345
367
  onClick={handleMain}
368
+ title={mainTitle}
346
369
  >
347
- {scopeLabel(type, selectedScope)}
370
+ {mainLabel}
348
371
  </button>
349
372
  <button
350
373
  className={`theia-button ${type === 'allow' ? 'main' : 'secondary'} theia-tool-confirmation-chevron-btn`}
@@ -433,10 +456,45 @@ export interface ToolConfirmationProps extends Pick<ToolConfirmationCallbacks, '
433
456
  onDeny: (scope?: ConfirmationScope, reason?: string) => void;
434
457
  contextMenuRenderer: ContextMenuRenderer;
435
458
  openerService: OpenerService;
459
+ pendingTracker?: PendingToolConfirmationTracker;
460
+ keybindingHints?: ToolConfirmationKeybindingHints;
461
+ chatId?: string;
462
+ markdownRenderer?: MarkdownRenderer;
436
463
  }
437
464
 
438
- export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, toolRequest, onAllow, onDeny, contextMenuRenderer, openerService }) => {
465
+ /**
466
+ * Command that opens the AI Configuration view on the Tools tab; its id is duplicated here as a
467
+ * string constant to keep ai-chat-ui free of an ai-ide dependency.
468
+ */
469
+ const OPEN_TOOLS_CONFIGURATION_COMMAND_ID = 'aiConfiguration:openTools';
470
+
471
+ const ToolConfirmationIntro: React.FC<{ markdownRenderer: MarkdownRenderer }> = ({ markdownRenderer }) => (
472
+ <div className="theia-tool-confirmation-intro">
473
+ <LocalizedMarkdown
474
+ localizationKey="theia/ai/chat-ui/toolconfirmation/intro"
475
+ defaultMarkdown={
476
+ 'AI agents may want to use tools to act on your workspace. ' +
477
+ 'By default each tool call needs your confirmation. ' +
478
+ 'You can change this default or pre-approve individual tools in the [Tools configuration view]({0}).'
479
+ }
480
+ args={[`command:${OPEN_TOOLS_CONFIGURATION_COMMAND_ID}`]}
481
+ markdownRenderer={markdownRenderer}
482
+ markdownOptions={{
483
+ isTrusted: { enabledCommands: [OPEN_TOOLS_CONFIGURATION_COMMAND_ID] }
484
+ }}
485
+ />
486
+ </div>
487
+ );
488
+
489
+ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({
490
+ response, toolRequest, onAllow, onDeny, contextMenuRenderer, openerService, pendingTracker, keybindingHints, chatId, markdownRenderer
491
+ }) => {
439
492
  const [state, setState] = React.useState<ToolConfirmationState>('waiting');
493
+ // Pure initializer (no side effects): decide whether the intro should render. Marking the chat
494
+ // as "intro shown" happens in the effect below to keep the initializer pure for StrictMode.
495
+ const [showIntro] = React.useState<boolean>(
496
+ () => !!(pendingTracker && chatId && markdownRenderer && !pendingTracker.hasShownIntro(chatId))
497
+ );
440
498
 
441
499
  const handleAllow = React.useCallback((scope: ConfirmationScope) => {
442
500
  setState('allowed');
@@ -448,6 +506,25 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, to
448
506
  onDeny(scope, reason);
449
507
  }, [onDeny]);
450
508
 
509
+ React.useEffect(() => {
510
+ if (showIntro && pendingTracker && chatId) {
511
+ pendingTracker.markIntroShown(chatId);
512
+ }
513
+ }, []);
514
+
515
+ React.useEffect(() => {
516
+ if (!pendingTracker || !chatId || state !== 'waiting') {
517
+ return;
518
+ }
519
+ const disposable = pendingTracker.register({
520
+ chatId,
521
+ response,
522
+ allow: () => handleAllow('once'),
523
+ deny: () => handleDeny('once')
524
+ });
525
+ return () => disposable.dispose();
526
+ }, [pendingTracker, chatId, response, handleAllow, handleDeny, state]);
527
+
451
528
  if (state === 'allowed') {
452
529
  return (
453
530
  <div className="theia-tool-confirmation-status allowed">
@@ -472,32 +549,37 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, to
472
549
  );
473
550
 
474
551
  return (
475
- <div className="theia-tool-confirmation">
476
- <div className="theia-tool-confirmation-header">
477
- <span className={codicon('shield')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/header', 'Confirm Tool Execution')}
478
- </div>
479
- <div className="theia-tool-confirmation-info">
480
- {toolRequest?.description ? (
481
- <details className="theia-tool-confirmation-name">
482
- <summary>{toolNameContent}</summary>
483
- <div className="theia-tool-confirmation-description">
484
- {toolRequest.description}
485
- </div>
486
- </details>
487
- ) : (
488
- <div className="theia-tool-confirmation-name">{toolNameContent}</div>
489
- )}
490
- <ToolArgsDisplay args={response.arguments} openerService={openerService} />
552
+ <>
553
+ {showIntro && markdownRenderer && <ToolConfirmationIntro markdownRenderer={markdownRenderer} />}
554
+ <div className="theia-tool-confirmation">
555
+ <div className="theia-tool-confirmation-header">
556
+ <span className={codicon('shield')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/header', 'Confirm Tool Execution')}
557
+ </div>
558
+ <div className="theia-tool-confirmation-info">
559
+ {toolRequest?.description ? (
560
+ <details className="theia-tool-confirmation-name">
561
+ <summary>{toolNameContent}</summary>
562
+ <div className="theia-tool-confirmation-description">
563
+ {toolRequest.description}
564
+ </div>
565
+ </details>
566
+ ) : (
567
+ <div className="theia-tool-confirmation-name">{toolNameContent}</div>
568
+ )}
569
+ <ToolArgsDisplay args={response.arguments} openerService={openerService} />
570
+ </div>
571
+ <ToolConfirmationActions
572
+ toolName={response.name ?? 'unknown'}
573
+ toolRequest={toolRequest}
574
+ onAllow={handleAllow}
575
+ onDeny={handleDeny}
576
+ contextMenuRenderer={contextMenuRenderer}
577
+ autoFocus={true}
578
+ keybindingHints={keybindingHints}
579
+ />
580
+ <CountdownTimer response={response} />
491
581
  </div>
492
- <ToolConfirmationActions
493
- toolName={response.name ?? 'unknown'}
494
- toolRequest={toolRequest}
495
- onAllow={handleAllow}
496
- onDeny={handleDeny}
497
- contextMenuRenderer={contextMenuRenderer}
498
- />
499
- <CountdownTimer response={response} />
500
- </div>
582
+ </>
501
583
  );
502
584
  };
503
585
 
@@ -539,6 +621,9 @@ export interface WithToolCallConfirmationProps {
539
621
  requestCanceled: boolean;
540
622
  contextMenuRenderer: ContextMenuRenderer;
541
623
  openerService: OpenerService;
624
+ pendingTracker?: PendingToolConfirmationTracker;
625
+ keybindingHints?: ToolConfirmationKeybindingHints;
626
+ markdownRenderer?: MarkdownRenderer;
542
627
  }
543
628
 
544
629
  export function withToolCallConfirmation<P extends object>(
@@ -559,7 +644,10 @@ export function withToolCallConfirmation<P extends object>(
559
644
  showArgsTooltip,
560
645
  requestCanceled,
561
646
  contextMenuRenderer,
562
- openerService
647
+ openerService,
648
+ pendingTracker,
649
+ keybindingHints,
650
+ markdownRenderer
563
651
  } = toolConfirmation;
564
652
 
565
653
  const { confirmationState } = useToolConfirmationState(response, confirmationMode);
@@ -622,6 +710,10 @@ export function withToolCallConfirmation<P extends object>(
622
710
  onDeny={handleDeny}
623
711
  contextMenuRenderer={contextMenuRenderer}
624
712
  openerService={openerService}
713
+ pendingTracker={pendingTracker}
714
+ keybindingHints={keybindingHints}
715
+ chatId={chatId}
716
+ markdownRenderer={markdownRenderer}
625
717
  />
626
718
  );
627
719
  }
@@ -19,14 +19,19 @@ import { inject, injectable } from '@theia/core/shared/inversify';
19
19
  import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
20
20
  import { ReactNode } from '@theia/core/shared/react';
21
21
  import { nls } from '@theia/core/lib/common/nls';
22
- import { codicon, ContextMenuRenderer, HoverService, OpenerService } from '@theia/core/lib/browser';
22
+ import { codicon, ContextMenuRenderer, HoverService, KeybindingRegistry, MarkdownRenderer, OpenerService } from '@theia/core/lib/browser';
23
23
  import * as React from '@theia/core/shared/react';
24
- import { createConfirmationHandlers, ToolConfirmation, useToolConfirmationState } from './tool-confirmation';
24
+ import { createConfirmationHandlers, ToolConfirmation, ToolConfirmationKeybindingHints, useToolConfirmationState } from './tool-confirmation';
25
25
  import { ToolConfirmationMode } from '@theia/ai-chat/lib/common/chat-tool-preferences';
26
26
  import { ResponseNode } from '../chat-tree-view';
27
27
  import { MarkdownRender } from './markdown-part-renderer';
28
28
  import { isToolCallContent, ToolCallResult, ToolInvocationRegistry, ToolRequest } from '@theia/ai-core';
29
29
  import { ToolConfirmationManager } from '@theia/ai-chat/lib/browser/chat-tool-preference-bindings';
30
+ import { PendingToolConfirmationTracker } from '@theia/ai-chat/lib/browser/pending-tool-confirmation-tracker';
31
+ import {
32
+ APPROVE_LATEST_TOOL_CONFIRMATION_COMMAND,
33
+ DENY_LATEST_TOOL_CONFIRMATION_COMMAND
34
+ } from '../tool-confirmation-keybinding-contribution';
30
35
  import { condenseArguments, formatArgsForTooltip } from './toolcall-utils';
31
36
 
32
37
  @injectable()
@@ -47,6 +52,15 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
47
52
  @inject(ContextMenuRenderer)
48
53
  protected contextMenuRenderer: ContextMenuRenderer;
49
54
 
55
+ @inject(PendingToolConfirmationTracker)
56
+ protected pendingToolConfirmationTracker: PendingToolConfirmationTracker;
57
+
58
+ @inject(KeybindingRegistry)
59
+ protected keybindingRegistry: KeybindingRegistry;
60
+
61
+ @inject(MarkdownRenderer)
62
+ protected markdownRenderer: MarkdownRenderer;
63
+
50
64
  canHandle(response: ChatResponseContent): number {
51
65
  if (ToolCallChatResponseContent.is(response)) {
52
66
  return 10;
@@ -68,6 +82,10 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
68
82
  onDeny={handleDeny}
69
83
  contextMenuRenderer={this.contextMenuRenderer}
70
84
  openerService={this.openerService}
85
+ pendingTracker={this.pendingToolConfirmationTracker}
86
+ keybindingHints={this.getKeybindingHints()}
87
+ chatId={chatId}
88
+ markdownRenderer={this.markdownRenderer}
71
89
  />;
72
90
  }
73
91
 
@@ -86,7 +104,24 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
86
104
  responseRenderer={this.renderResult.bind(this)}
87
105
  requestCanceled={parentNode.response.isCanceled}
88
106
  contextMenuRenderer={this.contextMenuRenderer}
89
- openerService={this.openerService} />;
107
+ openerService={this.openerService}
108
+ pendingTracker={this.pendingToolConfirmationTracker}
109
+ keybindingHints={this.getKeybindingHints()}
110
+ markdownRenderer={this.markdownRenderer} />;
111
+ }
112
+
113
+ protected getKeybindingHints(): ToolConfirmationKeybindingHints {
114
+ const allow = this.formatKeybinding(APPROVE_LATEST_TOOL_CONFIRMATION_COMMAND.id);
115
+ const deny = this.formatKeybinding(DENY_LATEST_TOOL_CONFIRMATION_COMMAND.id);
116
+ return { allow, deny };
117
+ }
118
+
119
+ protected formatKeybinding(commandId: string): string | undefined {
120
+ const bindings = this.keybindingRegistry.getKeybindingsForCommand(commandId);
121
+ if (!bindings.length) {
122
+ return undefined;
123
+ }
124
+ return this.keybindingRegistry.acceleratorFor(bindings[0], '+').join('+');
90
125
  }
91
126
 
92
127
  protected renderResult(response: ToolCallChatResponseContent): ReactNode {
@@ -190,6 +225,9 @@ interface ToolCallContentProps {
190
225
  requestCanceled: boolean;
191
226
  contextMenuRenderer: ContextMenuRenderer;
192
227
  openerService: OpenerService;
228
+ pendingTracker?: PendingToolConfirmationTracker;
229
+ keybindingHints?: ToolConfirmationKeybindingHints;
230
+ markdownRenderer?: MarkdownRenderer;
193
231
  }
194
232
 
195
233
  /**
@@ -206,7 +244,10 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({
206
244
  requestCanceled,
207
245
  showArgsTooltip,
208
246
  contextMenuRenderer,
209
- openerService
247
+ openerService,
248
+ pendingTracker,
249
+ keybindingHints,
250
+ markdownRenderer
210
251
  }) => {
211
252
  const { confirmationState, rejectionReason } = useToolConfirmationState(response, confirmationMode);
212
253
  const summaryRef = React.useRef<HTMLElement | undefined>(undefined);
@@ -297,6 +338,10 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({
297
338
  onDeny={handleDeny}
298
339
  contextMenuRenderer={contextMenuRenderer}
299
340
  openerService={openerService}
341
+ pendingTracker={pendingTracker}
342
+ keybindingHints={keybindingHints}
343
+ chatId={chatId}
344
+ markdownRenderer={markdownRenderer}
300
345
  />
301
346
  </span>
302
347
  )}
@@ -341,6 +341,10 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
341
341
  getSettings(): ChatSessionSettings | undefined {
342
342
  return this.chatSession.model.settings;
343
343
  }
344
+
345
+ get sessionId(): string {
346
+ return this.chatSession.id;
347
+ }
344
348
  }
345
349
 
346
350
  export namespace ChatViewWidget {
@@ -991,6 +991,15 @@ div:last-child>.theia-ChatNode {
991
991
  cursor: default;
992
992
  }
993
993
 
994
+ .theia-tool-confirmation-intro {
995
+ margin: calc(var(--theia-ui-padding) * 2) 0;
996
+ line-height: var(--theia-content-line-height);
997
+ }
998
+
999
+ .theia-tool-confirmation-intro p {
1000
+ margin: 0;
1001
+ }
1002
+
994
1003
  .theia-tool-confirmation-header {
995
1004
  font-weight: bold;
996
1005
  margin-bottom: 8px;
@@ -0,0 +1,104 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { Command, CommandContribution, CommandRegistry } from '@theia/core';
18
+ import { ApplicationShell, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser';
19
+ import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
20
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
21
+ import { PendingToolConfirmation, PendingToolConfirmationTracker } from '@theia/ai-chat/lib/browser/pending-tool-confirmation-tracker';
22
+ import { ChatViewWidget } from './chat-view-widget';
23
+
24
+ export const HAS_PENDING_TOOL_CONFIRMATION_CONTEXT_KEY = 'theiaAi.hasPendingToolConfirmation';
25
+
26
+ /**
27
+ * Only fire the approve/deny shortcuts while the chat view is focused, so e.g. `Ctrl+Enter` in an
28
+ * editor is never shadowed by a confirmation pending in some background chat.
29
+ */
30
+ const CHAT_VIEW_FOCUS_WHEN = '(chatInputFocus || chatResponseFocus)';
31
+
32
+ export const APPROVE_LATEST_TOOL_CONFIRMATION_COMMAND = Command.toLocalizedCommand({
33
+ id: 'theia.ai.chat.approveLatestToolConfirmation',
34
+ label: 'Approve Latest Tool Confirmation'
35
+ }, 'theia/ai/chat-ui/toolconfirmation/approveLatest');
36
+
37
+ export const DENY_LATEST_TOOL_CONFIRMATION_COMMAND = Command.toLocalizedCommand({
38
+ id: 'theia.ai.chat.denyLatestToolConfirmation',
39
+ label: 'Deny Latest Tool Confirmation'
40
+ }, 'theia/ai/chat-ui/toolconfirmation/denyLatest');
41
+
42
+ @injectable()
43
+ export class ToolConfirmationKeybindingContribution implements CommandContribution, KeybindingContribution {
44
+
45
+ @inject(PendingToolConfirmationTracker)
46
+ protected readonly pendingTracker: PendingToolConfirmationTracker;
47
+
48
+ @inject(ContextKeyService)
49
+ protected readonly contextKeyService: ContextKeyService;
50
+
51
+ @inject(ApplicationShell)
52
+ protected readonly shell: ApplicationShell;
53
+
54
+ protected hasPendingKey: ContextKey<boolean>;
55
+
56
+ @postConstruct()
57
+ protected init(): void {
58
+ this.hasPendingKey = this.contextKeyService.createKey<boolean>(HAS_PENDING_TOOL_CONFIRMATION_CONTEXT_KEY, false);
59
+ this.pendingTracker.onChanged(() => this.hasPendingKey.set(this.pendingTracker.hasPending()));
60
+ }
61
+
62
+ registerCommands(commands: CommandRegistry): void {
63
+ commands.registerCommand(APPROVE_LATEST_TOOL_CONFIRMATION_COMMAND, {
64
+ isEnabled: () => this.hasPendingInActiveChat(),
65
+ execute: () => this.getLatestInActiveChat()?.allow()
66
+ });
67
+ commands.registerCommand(DENY_LATEST_TOOL_CONFIRMATION_COMMAND, {
68
+ isEnabled: () => this.hasPendingInActiveChat(),
69
+ execute: () => this.getLatestInActiveChat()?.deny()
70
+ });
71
+ }
72
+
73
+ registerKeybindings(keybindings: KeybindingRegistry): void {
74
+ keybindings.registerKeybinding({
75
+ command: APPROVE_LATEST_TOOL_CONFIRMATION_COMMAND.id,
76
+ keybinding: 'ctrlcmd+enter',
77
+ when: `${HAS_PENDING_TOOL_CONFIRMATION_CONTEXT_KEY} && ${CHAT_VIEW_FOCUS_WHEN}`
78
+ });
79
+ keybindings.registerKeybinding({
80
+ command: DENY_LATEST_TOOL_CONFIRMATION_COMMAND.id,
81
+ keybinding: 'ctrlcmd+shift+backspace',
82
+ when: `${HAS_PENDING_TOOL_CONFIRMATION_CONTEXT_KEY} && ${CHAT_VIEW_FOCUS_WHEN}`
83
+ });
84
+ }
85
+
86
+ /**
87
+ * The id of the chat the user is currently interacting with, or `undefined` if no chat view is
88
+ * focused. Used to target the shortcuts at the visible confirmation rather than the globally
89
+ * newest one.
90
+ */
91
+ protected getActiveChatId(): string | undefined {
92
+ return ChatViewWidget.findActive(this.shell)?.sessionId;
93
+ }
94
+
95
+ protected hasPendingInActiveChat(): boolean {
96
+ const chatId = this.getActiveChatId();
97
+ return chatId !== undefined && this.pendingTracker.hasPending(chatId);
98
+ }
99
+
100
+ protected getLatestInActiveChat(): PendingToolConfirmation | undefined {
101
+ const chatId = this.getActiveChatId();
102
+ return chatId === undefined ? undefined : this.pendingTracker.getLatest(chatId);
103
+ }
104
+ }